mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-22 00:31:46 +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: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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue