mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-10 03:28:03 +00:00
refactor: dynamically generate roomodes JSON schema from Zod types
Replace the hand-crafted schemas/roomodes.json with one generated from the Zod schemas in packages/types/src/mode.ts using zod-to-json-schema. This ensures the schema stays in sync when TypeScript types change. - Add zod-to-json-schema dev dependency to packages/types - Create packages/types/scripts/generate-roomodes-schema.ts - Add generate:schema npm script to packages/types - Add drift-detection test in packages/types to catch schema/type mismatches - Update existing AJV validation tests with documentation comment
This commit is contained in:
parent
70db387a69
commit
3fd2ca47c8
6 changed files with 248 additions and 115 deletions
|
|
@ -20,7 +20,8 @@
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"build:watch": "tsup --watch --outDir npm/dist --onSuccess 'echo ✅ Types rebuilt to npm/dist'",
|
"build:watch": "tsup --watch --outDir npm/dist --onSuccess 'echo ✅ Types rebuilt to npm/dist'",
|
||||||
"npm:publish": "node scripts/publish-npm.cjs",
|
"npm:publish": "node scripts/publish-npm.cjs",
|
||||||
"clean": "rimraf dist .turbo"
|
"clean": "rimraf dist .turbo",
|
||||||
|
"generate:schema": "tsx scripts/generate-roomodes-schema.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "3.25.76"
|
"zod": "3.25.76"
|
||||||
|
|
@ -31,6 +32,7 @@
|
||||||
"@types/node": "^24.1.0",
|
"@types/node": "^24.1.0",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.3",
|
||||||
|
"zod-to-json-schema": "^3.25.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
83
packages/types/scripts/generate-roomodes-schema.ts
Normal file
83
packages/types/scripts/generate-roomodes-schema.ts
Normal file
|
|
@ -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<string, unknown>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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)}`)
|
||||||
54
packages/types/src/__tests__/roomodes-schema-sync.spec.ts
Normal file
54
packages/types/src/__tests__/roomodes-schema-sync.spec.ts
Normal file
|
|
@ -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<string, unknown>
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
|
|
@ -731,6 +731,9 @@ importers:
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.2.3
|
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)
|
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:
|
packages/vscode-shim:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
|
@ -11020,6 +11023,11 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: 3.25.76
|
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:
|
zod-to-ts@1.2.0:
|
||||||
resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==}
|
resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -14992,7 +15000,7 @@ snapshots:
|
||||||
sirv: 3.0.1
|
sirv: 3.0.1
|
||||||
tinyglobby: 0.2.14
|
tinyglobby: 0.2.14
|
||||||
tinyrainbow: 2.0.0
|
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':
|
'@vitest/utils@3.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -22298,6 +22306,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
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):
|
zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.76):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
|
|
|
||||||
|
|
@ -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",
|
"type": "object",
|
||||||
"required": ["customModes"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"customModes": {
|
"customModes": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "List of custom mode definitions.",
|
|
||||||
"items": {
|
"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": {
|
"required": ["customModes"],
|
||||||
"ToolGroup": {
|
"additionalProperties": false,
|
||||||
"type": "string",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"enum": ["read", "edit", "browser", "command", "mcp", "modes"],
|
"$id": "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json",
|
||||||
"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."
|
"title": "Roo Code Custom Modes",
|
||||||
},
|
"description": "Schema for .roomodes configuration files used by Roo Code to define custom modes."
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { describe, it, expect, beforeAll } from "vitest"
|
||||||
import Ajv from "ajv"
|
import Ajv from "ajv"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue