mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-10 22:41:14 +00:00
parent
3d0fc3ccc5
commit
d4c7493674
8 changed files with 184 additions and 192 deletions
5
.changeset/great-mice-turn.md
Normal file
5
.changeset/great-mice-turn.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Remove redundant zod schemas
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { CustomModesSettingsSchema } from "./CustomModesSchema"
|
||||
import { customModesSettingsSchema } from "../../schemas"
|
||||
import { ModeConfig } from "../../shared/modes"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { arePathsEqual, getWorkspacePath } from "../../utils/path"
|
||||
|
|
@ -62,7 +62,7 @@ export class CustomModesManager {
|
|||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
const settings = JSON.parse(content)
|
||||
const result = CustomModesSettingsSchema.safeParse(settings)
|
||||
const result = customModesSettingsSchema.safeParse(settings)
|
||||
if (!result.success) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -144,7 +144,8 @@ export class CustomModesManager {
|
|||
return
|
||||
}
|
||||
|
||||
const result = CustomModesSettingsSchema.safeParse(config)
|
||||
const result = customModesSettingsSchema.safeParse(config)
|
||||
|
||||
if (!result.success) {
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { ModeConfig } from "../../shared/modes"
|
||||
import { TOOL_GROUPS, ToolGroup } from "../../shared/tool-groups"
|
||||
|
||||
// Create a schema for valid tool groups using the keys of TOOL_GROUPS
|
||||
const ToolGroupSchema = z.enum(Object.keys(TOOL_GROUPS) as [ToolGroup, ...ToolGroup[]])
|
||||
|
||||
// Schema for group options with regex validation
|
||||
const GroupOptionsSchema = z.object({
|
||||
fileRegex: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(pattern) => {
|
||||
if (!pattern) return true // Optional, so empty is valid
|
||||
try {
|
||||
new RegExp(pattern)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ message: "Invalid regular expression pattern" },
|
||||
),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
// Schema for a group entry - either a tool group string or a tuple of [group, options]
|
||||
const GroupEntrySchema = z.union([ToolGroupSchema, z.tuple([ToolGroupSchema, GroupOptionsSchema])])
|
||||
|
||||
// Schema for array of groups
|
||||
const GroupsArraySchema = z.array(GroupEntrySchema).refine(
|
||||
(groups) => {
|
||||
const seen = new Set()
|
||||
return groups.every((group) => {
|
||||
// For tuples, check the group name (first element)
|
||||
const groupName = Array.isArray(group) ? group[0] : group
|
||||
if (seen.has(groupName)) return false
|
||||
seen.add(groupName)
|
||||
return true
|
||||
})
|
||||
},
|
||||
{ message: "Duplicate groups are not allowed" },
|
||||
)
|
||||
|
||||
// Schema for mode configuration
|
||||
export const CustomModeSchema = z.object({
|
||||
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
roleDefinition: z.string().min(1, "Role definition is required"),
|
||||
customInstructions: z.string().optional(),
|
||||
groups: GroupsArraySchema,
|
||||
}) satisfies z.ZodType<ModeConfig>
|
||||
|
||||
// Schema for the entire custom modes settings file
|
||||
export const CustomModesSettingsSchema = z.object({
|
||||
customModes: z.array(CustomModeSchema).refine(
|
||||
(modes) => {
|
||||
const slugs = new Set()
|
||||
return modes.every((mode) => {
|
||||
if (slugs.has(mode.slug)) {
|
||||
return false
|
||||
}
|
||||
slugs.add(mode.slug)
|
||||
return true
|
||||
})
|
||||
},
|
||||
{
|
||||
message: "Duplicate mode slugs are not allowed",
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
export type CustomModesSettings = z.infer<typeof CustomModesSettingsSchema>
|
||||
|
||||
/**
|
||||
* Validates a custom mode configuration against the schema
|
||||
* @throws {z.ZodError} if validation fails
|
||||
*/
|
||||
export function validateCustomMode(mode: unknown): asserts mode is ModeConfig {
|
||||
CustomModeSchema.parse(mode)
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { CustomModesSettingsSchema } from "../CustomModesSchema"
|
||||
// npx jest src/core/config/__tests__/CustomModesSettings.test.ts
|
||||
|
||||
import { customModesSettingsSchema } from "../../../schemas"
|
||||
import { ModeConfig } from "../../../shared/modes"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
|
|
@ -17,7 +19,7 @@ describe("CustomModesSettings", () => {
|
|||
}
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(validSettings)
|
||||
customModesSettingsSchema.parse(validSettings)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
|
|
@ -27,7 +29,7 @@ describe("CustomModesSettings", () => {
|
|||
}
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(validSettings)
|
||||
customModesSettingsSchema.parse(validSettings)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
|
|
@ -44,7 +46,7 @@ describe("CustomModesSettings", () => {
|
|||
}
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(validSettings)
|
||||
customModesSettingsSchema.parse(validSettings)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
|
|
@ -52,7 +54,7 @@ describe("CustomModesSettings", () => {
|
|||
const invalidSettings = {} as any
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(invalidSettings)
|
||||
customModesSettingsSchema.parse(invalidSettings)
|
||||
}).toThrow(ZodError)
|
||||
})
|
||||
|
||||
|
|
@ -68,10 +70,10 @@ describe("CustomModesSettings", () => {
|
|||
}
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(invalidSettings)
|
||||
customModesSettingsSchema.parse(invalidSettings)
|
||||
}).toThrow(ZodError)
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(invalidSettings)
|
||||
customModesSettingsSchema.parse(invalidSettings)
|
||||
}).toThrow("Slug must contain only letters numbers and dashes")
|
||||
})
|
||||
|
||||
|
|
@ -81,17 +83,17 @@ describe("CustomModesSettings", () => {
|
|||
}
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(invalidSettings)
|
||||
customModesSettingsSchema.parse(invalidSettings)
|
||||
}).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test("rejects null or undefined", () => {
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(null)
|
||||
customModesSettingsSchema.parse(null)
|
||||
}).toThrow(ZodError)
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(undefined)
|
||||
customModesSettingsSchema.parse(undefined)
|
||||
}).toThrow(ZodError)
|
||||
})
|
||||
|
||||
|
|
@ -104,7 +106,7 @@ describe("CustomModesSettings", () => {
|
|||
}
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(duplicateSettings)
|
||||
customModesSettingsSchema.parse(duplicateSettings)
|
||||
}).toThrow("Duplicate mode slugs are not allowed")
|
||||
})
|
||||
|
||||
|
|
@ -119,7 +121,7 @@ describe("CustomModesSettings", () => {
|
|||
}
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(invalidSettings)
|
||||
customModesSettingsSchema.parse(invalidSettings)
|
||||
}).toThrow(ZodError)
|
||||
})
|
||||
|
||||
|
|
@ -134,7 +136,7 @@ describe("CustomModesSettings", () => {
|
|||
}
|
||||
|
||||
expect(() => {
|
||||
CustomModesSettingsSchema.parse(validSettings)
|
||||
customModesSettingsSchema.parse(validSettings)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
import { CustomModeSchema } from "../CustomModesSchema"
|
||||
import { ModeConfig } from "../../../shared/modes"
|
||||
|
||||
describe("GroupConfigSchema", () => {
|
||||
const validBaseMode = {
|
||||
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||
name: "Test Mode",
|
||||
roleDefinition: "Test role definition",
|
||||
}
|
||||
|
||||
describe("group format validation", () => {
|
||||
test("accepts single group", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["read"] as const,
|
||||
} satisfies ModeConfig
|
||||
|
||||
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
||||
})
|
||||
|
||||
test("accepts multiple groups", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["read", "edit", "browser"] as const,
|
||||
} satisfies ModeConfig
|
||||
|
||||
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
||||
})
|
||||
|
||||
test("accepts all available groups", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["read", "edit", "browser", "command", "mcp"] as const,
|
||||
} satisfies ModeConfig
|
||||
|
||||
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
||||
})
|
||||
|
||||
test("rejects non-array group format", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: "not-an-array" as any,
|
||||
}
|
||||
|
||||
expect(() => CustomModeSchema.parse(mode)).toThrow()
|
||||
})
|
||||
|
||||
test("rejects invalid group names", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["invalid_group"] as any,
|
||||
}
|
||||
|
||||
expect(() => CustomModeSchema.parse(mode)).toThrow()
|
||||
})
|
||||
|
||||
test("rejects duplicate groups", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["read", "read"] as any,
|
||||
}
|
||||
|
||||
expect(() => CustomModeSchema.parse(mode)).toThrow("Duplicate groups are not allowed")
|
||||
})
|
||||
|
||||
test("rejects null or undefined groups", () => {
|
||||
const modeWithNull = {
|
||||
...validBaseMode,
|
||||
groups: null as any,
|
||||
}
|
||||
|
||||
const modeWithUndefined = {
|
||||
...validBaseMode,
|
||||
groups: undefined as any,
|
||||
}
|
||||
|
||||
expect(() => CustomModeSchema.parse(modeWithNull)).toThrow()
|
||||
expect(() => CustomModeSchema.parse(modeWithUndefined)).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
// npx jest src/core/config/__tests__/ModeConfig.test.ts
|
||||
|
||||
import { ZodError } from "zod"
|
||||
import { CustomModeSchema, validateCustomMode } from "../CustomModesSchema"
|
||||
|
||||
import { modeConfigSchema } from "../../../schemas"
|
||||
import { ModeConfig } from "../../../shared/modes"
|
||||
|
||||
function validateCustomMode(mode: unknown): asserts mode is ModeConfig {
|
||||
modeConfigSchema.parse(mode)
|
||||
}
|
||||
|
||||
describe("CustomModeSchema", () => {
|
||||
describe("validateCustomMode", () => {
|
||||
test("accepts valid mode configuration", () => {
|
||||
|
|
@ -129,8 +136,8 @@ describe("CustomModeSchema", () => {
|
|||
],
|
||||
}
|
||||
|
||||
expect(() => CustomModeSchema.parse(modeWithJustRegex)).not.toThrow()
|
||||
expect(() => CustomModeSchema.parse(modeWithDescription)).not.toThrow()
|
||||
expect(() => modeConfigSchema.parse(modeWithJustRegex)).not.toThrow()
|
||||
expect(() => modeConfigSchema.parse(modeWithDescription)).not.toThrow()
|
||||
})
|
||||
|
||||
it("validates file regex patterns", () => {
|
||||
|
|
@ -144,7 +151,7 @@ describe("CustomModeSchema", () => {
|
|||
roleDefinition: "Test",
|
||||
groups: ["read", ["edit", { fileRegex: pattern }]],
|
||||
}
|
||||
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
||||
expect(() => modeConfigSchema.parse(mode)).not.toThrow()
|
||||
})
|
||||
|
||||
invalidPatterns.forEach((pattern) => {
|
||||
|
|
@ -154,7 +161,7 @@ describe("CustomModeSchema", () => {
|
|||
roleDefinition: "Test",
|
||||
groups: ["read", ["edit", { fileRegex: pattern }]],
|
||||
}
|
||||
expect(() => CustomModeSchema.parse(mode)).toThrow()
|
||||
expect(() => modeConfigSchema.parse(mode)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -166,7 +173,84 @@ describe("CustomModeSchema", () => {
|
|||
groups: ["read", "read", ["edit", { fileRegex: "\\.md$" }], ["edit", { fileRegex: "\\.txt$" }]],
|
||||
}
|
||||
|
||||
expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
|
||||
expect(() => modeConfigSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
|
||||
})
|
||||
})
|
||||
|
||||
const validBaseMode = {
|
||||
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||
name: "Test Mode",
|
||||
roleDefinition: "Test role definition",
|
||||
}
|
||||
|
||||
describe("group format validation", () => {
|
||||
test("accepts single group", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["read"] as const,
|
||||
} satisfies ModeConfig
|
||||
|
||||
expect(() => modeConfigSchema.parse(mode)).not.toThrow()
|
||||
})
|
||||
|
||||
test("accepts multiple groups", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["read", "edit", "browser"] as const,
|
||||
} satisfies ModeConfig
|
||||
|
||||
expect(() => modeConfigSchema.parse(mode)).not.toThrow()
|
||||
})
|
||||
|
||||
test("accepts all available groups", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["read", "edit", "browser", "command", "mcp"] as const,
|
||||
} satisfies ModeConfig
|
||||
|
||||
expect(() => modeConfigSchema.parse(mode)).not.toThrow()
|
||||
})
|
||||
|
||||
test("rejects non-array group format", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: "not-an-array" as any,
|
||||
}
|
||||
|
||||
expect(() => modeConfigSchema.parse(mode)).toThrow()
|
||||
})
|
||||
|
||||
test("rejects invalid group names", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["invalid_group"] as any,
|
||||
}
|
||||
|
||||
expect(() => modeConfigSchema.parse(mode)).toThrow()
|
||||
})
|
||||
|
||||
test("rejects duplicate groups", () => {
|
||||
const mode = {
|
||||
...validBaseMode,
|
||||
groups: ["read", "read"] as any,
|
||||
}
|
||||
|
||||
expect(() => modeConfigSchema.parse(mode)).toThrow("Duplicate groups are not allowed")
|
||||
})
|
||||
|
||||
test("rejects null or undefined groups", () => {
|
||||
const modeWithNull = {
|
||||
...validBaseMode,
|
||||
groups: null as any,
|
||||
}
|
||||
|
||||
const modeWithUndefined = {
|
||||
...validBaseMode,
|
||||
groups: undefined as any,
|
||||
}
|
||||
|
||||
expect(() => modeConfigSchema.parse(modeWithNull)).toThrow()
|
||||
expect(() => modeConfigSchema.parse(modeWithUndefined)).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -152,7 +152,24 @@ export type HistoryItem = z.infer<typeof historyItemSchema>
|
|||
*/
|
||||
|
||||
export const groupOptionsSchema = z.object({
|
||||
fileRegex: z.string().optional(),
|
||||
fileRegex: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(pattern) => {
|
||||
if (!pattern) {
|
||||
return true // Optional, so empty is valid.
|
||||
}
|
||||
|
||||
try {
|
||||
new RegExp(pattern)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ message: "Invalid regular expression pattern" },
|
||||
),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
|
|
@ -170,17 +187,62 @@ export type GroupEntry = z.infer<typeof groupEntrySchema>
|
|||
* ModeConfig
|
||||
*/
|
||||
|
||||
const groupEntryArraySchema = z.array(groupEntrySchema).refine(
|
||||
(groups) => {
|
||||
const seen = new Set()
|
||||
|
||||
return groups.every((group) => {
|
||||
// For tuples, check the group name (first element).
|
||||
const groupName = Array.isArray(group) ? group[0] : group
|
||||
|
||||
if (seen.has(groupName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
seen.add(groupName)
|
||||
return true
|
||||
})
|
||||
},
|
||||
{ message: "Duplicate groups are not allowed" },
|
||||
)
|
||||
|
||||
export const modeConfigSchema = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
roleDefinition: z.string(),
|
||||
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
roleDefinition: z.string().min(1, "Role definition is required"),
|
||||
customInstructions: z.string().optional(),
|
||||
groups: z.array(groupEntrySchema),
|
||||
groups: groupEntryArraySchema,
|
||||
source: z.enum(["global", "project"]).optional(),
|
||||
})
|
||||
|
||||
export type ModeConfig = z.infer<typeof modeConfigSchema>
|
||||
|
||||
/**
|
||||
* CustomModesSettings
|
||||
*/
|
||||
|
||||
export const customModesSettingsSchema = z.object({
|
||||
customModes: z.array(modeConfigSchema).refine(
|
||||
(modes) => {
|
||||
const slugs = new Set()
|
||||
|
||||
return modes.every((mode) => {
|
||||
if (slugs.has(mode.slug)) {
|
||||
return false
|
||||
}
|
||||
|
||||
slugs.add(mode.slug)
|
||||
return true
|
||||
})
|
||||
},
|
||||
{
|
||||
message: "Duplicate mode slugs are not allowed",
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
export type CustomModesSettings = z.infer<typeof customModesSettingsSchema>
|
||||
|
||||
/**
|
||||
* PromptComponent
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
ModeConfig,
|
||||
GroupEntry,
|
||||
} from "../../../../src/shared/modes"
|
||||
import { CustomModeSchema } from "../../../../src/core/config/CustomModesSchema"
|
||||
import { modeConfigSchema } from "../../../../src/schemas"
|
||||
import { supportPrompt, SupportPromptType } from "../../../../src/shared/support-prompt"
|
||||
|
||||
import { TOOL_GROUPS, ToolGroup } from "../../../../src/shared/tool-groups"
|
||||
|
|
@ -223,7 +223,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
}
|
||||
|
||||
// Validate the mode against the schema
|
||||
const result = CustomModeSchema.safeParse(newMode)
|
||||
const result = modeConfigSchema.safeParse(newMode)
|
||||
|
||||
if (!result.success) {
|
||||
// Map Zod errors to specific fields
|
||||
result.error.errors.forEach((error) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue