diff --git a/packages/types/package.json b/packages/types/package.json index d66d87ac72..538268df47 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -20,7 +20,8 @@ "build": "tsup", "build:watch": "tsup --watch --outDir npm/dist --onSuccess 'echo ✅ Types rebuilt to npm/dist'", "npm:publish": "node scripts/publish-npm.cjs", - "clean": "rimraf dist .turbo" + "clean": "rimraf dist .turbo", + "generate:schema": "tsx scripts/generate-roomodes-schema.ts" }, "dependencies": { "zod": "3.25.76" @@ -31,6 +32,7 @@ "@types/node": "^24.1.0", "globals": "^16.3.0", "tsup": "^8.4.0", - "vitest": "^3.2.3" + "vitest": "^3.2.3", + "zod-to-json-schema": "^3.25.1" } } diff --git a/packages/types/scripts/generate-roomodes-schema.ts b/packages/types/scripts/generate-roomodes-schema.ts new file mode 100644 index 0000000000..ce22957504 --- /dev/null +++ b/packages/types/scripts/generate-roomodes-schema.ts @@ -0,0 +1,83 @@ +/** + * Generates the JSON Schema for .roomodes configuration files from the Zod + * schemas defined in packages/types/src/mode.ts. + * + * This ensures the schema stays in sync with the TypeScript types. Run via: + * pnpm --filter @roo-code/types generate:schema + * + * The output is written to schemas/roomodes.json at the repository root. + */ + +import * as fs from "fs" +import * as path from "path" +import { fileURLToPath } from "url" +import { zodToJsonSchema } from "zod-to-json-schema" +import { z } from "zod" + +import { toolGroups, deprecatedToolGroups } from "../src/tool.js" +import { groupOptionsSchema, modeConfigSchema } from "../src/mode.js" + +// --------------------------------------------------------------------------- +// 1. Build a ToolGroup enum that includes deprecated groups so existing +// configs still validate. +// --------------------------------------------------------------------------- +const allToolGroups = [...toolGroups, ...deprecatedToolGroups] as [string, ...string[]] +const allToolGroupsSchema = z.enum(allToolGroups) + +// --------------------------------------------------------------------------- +// 2. Build a GroupEntry schema that uses the extended tool group list. +// --------------------------------------------------------------------------- +const groupEntrySchema = z.union([allToolGroupsSchema, z.tuple([allToolGroupsSchema, groupOptionsSchema])]) + +// --------------------------------------------------------------------------- +// 3. Build the RuleFile schema (used during import/export but not part of +// the core Zod types). +// --------------------------------------------------------------------------- +const ruleFileSchema = z.object({ + relativePath: z.string(), + content: z.string().optional(), +}) + +// --------------------------------------------------------------------------- +// 4. Build an extended ModeConfig schema that includes rulesFiles and uses +// the extended groups (with deprecated entries). +// --------------------------------------------------------------------------- +const exportedModeConfigSchema = modeConfigSchema.omit({ groups: true }).extend({ + groups: z.array(groupEntrySchema), + rulesFiles: z.array(ruleFileSchema).optional(), +}) + +// --------------------------------------------------------------------------- +// 5. Build the top-level .roomodes schema. +// --------------------------------------------------------------------------- +const roomodesSchema = z + .object({ + customModes: z.array(exportedModeConfigSchema), + }) + .strict() + +// --------------------------------------------------------------------------- +// 6. Convert to JSON Schema (draft-07). +// --------------------------------------------------------------------------- +const jsonSchema = zodToJsonSchema(roomodesSchema, { + $refStrategy: "none", + target: "jsonSchema7", +}) as Record + +// --------------------------------------------------------------------------- +// 7. Add metadata. +// --------------------------------------------------------------------------- +jsonSchema["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json" +jsonSchema["title"] = "Roo Code Custom Modes" +jsonSchema["description"] = "Schema for .roomodes configuration files used by Roo Code to define custom modes." + +// --------------------------------------------------------------------------- +// 8. Write to disk. +// --------------------------------------------------------------------------- +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const repoRoot = path.resolve(__dirname, "../../..") +const outPath = path.join(repoRoot, "schemas", "roomodes.json") +fs.mkdirSync(path.dirname(outPath), { recursive: true }) +fs.writeFileSync(outPath, JSON.stringify(jsonSchema, null, "\t") + "\n", "utf-8") + +console.log(`Generated ${path.relative(repoRoot, outPath)}`) diff --git a/packages/types/src/__tests__/roomodes-schema-sync.spec.ts b/packages/types/src/__tests__/roomodes-schema-sync.spec.ts new file mode 100644 index 0000000000..1347bb49b5 --- /dev/null +++ b/packages/types/src/__tests__/roomodes-schema-sync.spec.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest" +import * as fs from "fs" +import * as path from "path" +import { fileURLToPath } from "url" +import { zodToJsonSchema } from "zod-to-json-schema" +import { z } from "zod" + +import { toolGroups, deprecatedToolGroups } from "../tool.js" +import { groupOptionsSchema, modeConfigSchema } from "../mode.js" + +/** + * This test verifies that the checked-in schemas/roomodes.json matches what + * would be generated from the current Zod schemas. If this test fails, run: + * + * pnpm --filter @roo-code/types generate:schema + * + * to regenerate the schema file. + */ +describe("roomodes schema sync", () => { + it("should match the dynamically generated schema from Zod types", () => { + const __dirname = path.dirname(fileURLToPath(import.meta.url)) + const schemaPath = path.resolve(__dirname, "../../../../schemas/roomodes.json") + const checkedIn = JSON.parse(fs.readFileSync(schemaPath, "utf-8")) + + // Reproduce the same generation logic as scripts/generate-roomodes-schema.ts + const allToolGroups = [...toolGroups, ...deprecatedToolGroups] as [string, ...string[]] + const allToolGroupsSchema = z.enum(allToolGroups) + const groupEntrySchema = z.union([allToolGroupsSchema, z.tuple([allToolGroupsSchema, groupOptionsSchema])]) + const ruleFileSchema = z.object({ + relativePath: z.string(), + content: z.string().optional(), + }) + const exportedModeConfigSchema = modeConfigSchema.omit({ groups: true }).extend({ + groups: z.array(groupEntrySchema), + rulesFiles: z.array(ruleFileSchema).optional(), + }) + const roomodesSchema = z + .object({ + customModes: z.array(exportedModeConfigSchema), + }) + .strict() + + const generated = zodToJsonSchema(roomodesSchema, { + $refStrategy: "none", + target: "jsonSchema7", + }) as Record + + generated["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json" + generated["title"] = "Roo Code Custom Modes" + generated["description"] = "Schema for .roomodes configuration files used by Roo Code to define custom modes." + + expect(checkedIn).toEqual(generated) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 722a6028c0..17c9cc12fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -731,6 +731,9 @@ importers: vitest: specifier: ^3.2.3 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + zod-to-json-schema: + specifier: ^3.25.1 + version: 3.25.1(zod@3.25.76) packages/vscode-shim: devDependencies: @@ -11020,6 +11023,11 @@ packages: peerDependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: 3.25.76 + zod-to-ts@1.2.0: resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} peerDependencies: @@ -14992,7 +15000,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -22298,6 +22306,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.76): dependencies: typescript: 5.8.3 diff --git a/schemas/roomodes.json b/schemas/roomodes.json index 593382f693..2b4349afcb 100644 --- a/schemas/roomodes.json +++ b/schemas/roomodes.json @@ -1,122 +1,96 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json", - "title": "Roo Code Custom Modes", - "description": "Schema for .roomodes configuration files used by Roo Code to define custom modes.", "type": "object", - "required": ["customModes"], - "additionalProperties": false, "properties": { "customModes": { "type": "array", - "description": "List of custom mode definitions.", "items": { - "$ref": "#/definitions/CustomMode" + "type": "object", + "properties": { + "slug": { + "type": "string", + "pattern": "^[a-zA-Z0-9-]+$" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "roleDefinition": { + "type": "string", + "minLength": 1 + }, + "whenToUse": { + "type": "string" + }, + "description": { + "type": "string" + }, + "customInstructions": { + "type": "string" + }, + "source": { + "type": "string", + "enum": ["global", "project"] + }, + "groups": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "enum": ["read", "edit", "command", "mcp", "modes", "browser"] + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "enum": ["read", "edit", "command", "mcp", "modes", "browser"] + }, + { + "type": "object", + "properties": { + "fileRegex": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + ] + } + }, + "rulesFiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "relativePath": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "required": ["relativePath"], + "additionalProperties": false + } + } + }, + "required": ["slug", "name", "roleDefinition", "groups"], + "additionalProperties": false } } }, - "definitions": { - "ToolGroup": { - "type": "string", - "enum": ["read", "edit", "browser", "command", "mcp", "modes"], - "description": "A tool group name that grants the mode access to a set of tools. Note: 'browser' is deprecated but still accepted for backward compatibility." - }, - "GroupOptions": { - "type": "object", - "description": "Options that restrict a tool group's file access.", - "properties": { - "fileRegex": { - "type": "string", - "description": "A regular expression pattern to restrict which files the tool group can access." - }, - "description": { - "type": "string", - "description": "A human-readable description of the file restriction." - } - }, - "additionalProperties": false - }, - "GroupEntryTuple": { - "type": "array", - "description": "A tuple of [toolGroupName, options] for tool groups with file restrictions.", - "items": [{ "$ref": "#/definitions/ToolGroup" }, { "$ref": "#/definitions/GroupOptions" }], - "additionalItems": false, - "minItems": 2, - "maxItems": 2 - }, - "GroupEntry": { - "description": "A tool group permission entry. Either a simple tool group name string, or a [toolGroupName, options] tuple for groups with file restrictions.", - "oneOf": [{ "$ref": "#/definitions/ToolGroup" }, { "$ref": "#/definitions/GroupEntryTuple" }] - }, - "RuleFile": { - "type": "object", - "description": "A rules file associated with a mode, used during import/export.", - "required": ["relativePath"], - "additionalProperties": false, - "properties": { - "relativePath": { - "type": "string", - "description": "The relative file path for the rules file." - }, - "content": { - "type": "string", - "description": "The text content of the rules file." - } - } - }, - "CustomMode": { - "type": "object", - "description": "A custom mode definition.", - "required": ["slug", "name", "roleDefinition", "groups"], - "additionalProperties": false, - "properties": { - "slug": { - "type": "string", - "pattern": "^[a-zA-Z0-9-]+$", - "description": "A unique identifier for the mode, containing only letters, numbers, and dashes." - }, - "name": { - "type": "string", - "minLength": 1, - "description": "The display name of the mode." - }, - "roleDefinition": { - "type": "string", - "minLength": 1, - "description": "The system prompt that defines the mode's role and behavior." - }, - "whenToUse": { - "type": "string", - "description": "A description of when this mode should be used, shown in the mode selection UI." - }, - "description": { - "type": "string", - "description": "A short description of the mode." - }, - "customInstructions": { - "type": "string", - "description": "Additional instructions appended to the system prompt." - }, - "groups": { - "type": "array", - "description": "The tool groups this mode has access to.", - "items": { - "$ref": "#/definitions/GroupEntry" - } - }, - "source": { - "type": "string", - "enum": ["global", "project"], - "description": "Where this mode was defined. Automatically set by Roo Code." - }, - "rulesFiles": { - "type": "array", - "description": "Rules files associated with this mode, used during import/export.", - "items": { - "$ref": "#/definitions/RuleFile" - } - } - } - } - } + "required": ["customModes"], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json", + "title": "Roo Code Custom Modes", + "description": "Schema for .roomodes configuration files used by Roo Code to define custom modes." } diff --git a/src/utils/__tests__/roomodes-schema.spec.ts b/src/utils/__tests__/roomodes-schema.spec.ts index 335e400645..2b20dcb41d 100644 --- a/src/utils/__tests__/roomodes-schema.spec.ts +++ b/src/utils/__tests__/roomodes-schema.spec.ts @@ -1,3 +1,11 @@ +/** + * Validates the generated schemas/roomodes.json against sample configurations + * using AJV. The schema itself is dynamically generated from the Zod types in + * packages/types/src/mode.ts -- see packages/types/scripts/generate-roomodes-schema.ts. + * + * A separate drift-detection test in packages/types ensures the checked-in + * schema stays in sync with the Zod source of truth. + */ import { describe, it, expect, beforeAll } from "vitest" import Ajv from "ajv" import * as fs from "fs"