Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 3fd2ca4

Browse files
committed
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
1 parent 70db387 commit 3fd2ca4

6 files changed

Lines changed: 247 additions & 114 deletions

File tree

packages/types/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"build": "tsup",
2121
"build:watch": "tsup --watch --outDir npm/dist --onSuccess 'echo ✅ Types rebuilt to npm/dist'",
2222
"npm:publish": "node scripts/publish-npm.cjs",
23-
"clean": "rimraf dist .turbo"
23+
"clean": "rimraf dist .turbo",
24+
"generate:schema": "tsx scripts/generate-roomodes-schema.ts"
2425
},
2526
"dependencies": {
2627
"zod": "3.25.76"
@@ -31,6 +32,7 @@
3132
"@types/node": "^24.1.0",
3233
"globals": "^16.3.0",
3334
"tsup": "^8.4.0",
34-
"vitest": "^3.2.3"
35+
"vitest": "^3.2.3",
36+
"zod-to-json-schema": "^3.25.1"
3537
}
3638
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Generates the JSON Schema for .roomodes configuration files from the Zod
3+
* schemas defined in packages/types/src/mode.ts.
4+
*
5+
* This ensures the schema stays in sync with the TypeScript types. Run via:
6+
* pnpm --filter @roo-code/types generate:schema
7+
*
8+
* The output is written to schemas/roomodes.json at the repository root.
9+
*/
10+
11+
import * as fs from "fs"
12+
import * as path from "path"
13+
import { fileURLToPath } from "url"
14+
import { zodToJsonSchema } from "zod-to-json-schema"
15+
import { z } from "zod"
16+
17+
import { toolGroups, deprecatedToolGroups } from "../src/tool.js"
18+
import { groupOptionsSchema, modeConfigSchema } from "../src/mode.js"
19+
20+
// ---------------------------------------------------------------------------
21+
// 1. Build a ToolGroup enum that includes deprecated groups so existing
22+
// configs still validate.
23+
// ---------------------------------------------------------------------------
24+
const allToolGroups = [...toolGroups, ...deprecatedToolGroups] as [string, ...string[]]
25+
const allToolGroupsSchema = z.enum(allToolGroups)
26+
27+
// ---------------------------------------------------------------------------
28+
// 2. Build a GroupEntry schema that uses the extended tool group list.
29+
// ---------------------------------------------------------------------------
30+
const groupEntrySchema = z.union([allToolGroupsSchema, z.tuple([allToolGroupsSchema, groupOptionsSchema])])
31+
32+
// ---------------------------------------------------------------------------
33+
// 3. Build the RuleFile schema (used during import/export but not part of
34+
// the core Zod types).
35+
// ---------------------------------------------------------------------------
36+
const ruleFileSchema = z.object({
37+
relativePath: z.string(),
38+
content: z.string().optional(),
39+
})
40+
41+
// ---------------------------------------------------------------------------
42+
// 4. Build an extended ModeConfig schema that includes rulesFiles and uses
43+
// the extended groups (with deprecated entries).
44+
// ---------------------------------------------------------------------------
45+
const exportedModeConfigSchema = modeConfigSchema.omit({ groups: true }).extend({
46+
groups: z.array(groupEntrySchema),
47+
rulesFiles: z.array(ruleFileSchema).optional(),
48+
})
49+
50+
// ---------------------------------------------------------------------------
51+
// 5. Build the top-level .roomodes schema.
52+
// ---------------------------------------------------------------------------
53+
const roomodesSchema = z
54+
.object({
55+
customModes: z.array(exportedModeConfigSchema),
56+
})
57+
.strict()
58+
59+
// ---------------------------------------------------------------------------
60+
// 6. Convert to JSON Schema (draft-07).
61+
// ---------------------------------------------------------------------------
62+
const jsonSchema = zodToJsonSchema(roomodesSchema, {
63+
$refStrategy: "none",
64+
target: "jsonSchema7",
65+
}) as Record<string, unknown>
66+
67+
// ---------------------------------------------------------------------------
68+
// 7. Add metadata.
69+
// ---------------------------------------------------------------------------
70+
jsonSchema["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json"
71+
jsonSchema["title"] = "Roo Code Custom Modes"
72+
jsonSchema["description"] = "Schema for .roomodes configuration files used by Roo Code to define custom modes."
73+
74+
// ---------------------------------------------------------------------------
75+
// 8. Write to disk.
76+
// ---------------------------------------------------------------------------
77+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
78+
const repoRoot = path.resolve(__dirname, "../../..")
79+
const outPath = path.join(repoRoot, "schemas", "roomodes.json")
80+
fs.mkdirSync(path.dirname(outPath), { recursive: true })
81+
fs.writeFileSync(outPath, JSON.stringify(jsonSchema, null, "\t") + "\n", "utf-8")
82+
83+
console.log(`Generated ${path.relative(repoRoot, outPath)}`)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, it, expect } from "vitest"
2+
import * as fs from "fs"
3+
import * as path from "path"
4+
import { fileURLToPath } from "url"
5+
import { zodToJsonSchema } from "zod-to-json-schema"
6+
import { z } from "zod"
7+
8+
import { toolGroups, deprecatedToolGroups } from "../tool.js"
9+
import { groupOptionsSchema, modeConfigSchema } from "../mode.js"
10+
11+
/**
12+
* This test verifies that the checked-in schemas/roomodes.json matches what
13+
* would be generated from the current Zod schemas. If this test fails, run:
14+
*
15+
* pnpm --filter @roo-code/types generate:schema
16+
*
17+
* to regenerate the schema file.
18+
*/
19+
describe("roomodes schema sync", () => {
20+
it("should match the dynamically generated schema from Zod types", () => {
21+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
22+
const schemaPath = path.resolve(__dirname, "../../../../schemas/roomodes.json")
23+
const checkedIn = JSON.parse(fs.readFileSync(schemaPath, "utf-8"))
24+
25+
// Reproduce the same generation logic as scripts/generate-roomodes-schema.ts
26+
const allToolGroups = [...toolGroups, ...deprecatedToolGroups] as [string, ...string[]]
27+
const allToolGroupsSchema = z.enum(allToolGroups)
28+
const groupEntrySchema = z.union([allToolGroupsSchema, z.tuple([allToolGroupsSchema, groupOptionsSchema])])
29+
const ruleFileSchema = z.object({
30+
relativePath: z.string(),
31+
content: z.string().optional(),
32+
})
33+
const exportedModeConfigSchema = modeConfigSchema.omit({ groups: true }).extend({
34+
groups: z.array(groupEntrySchema),
35+
rulesFiles: z.array(ruleFileSchema).optional(),
36+
})
37+
const roomodesSchema = z
38+
.object({
39+
customModes: z.array(exportedModeConfigSchema),
40+
})
41+
.strict()
42+
43+
const generated = zodToJsonSchema(roomodesSchema, {
44+
$refStrategy: "none",
45+
target: "jsonSchema7",
46+
}) as Record<string, unknown>
47+
48+
generated["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json"
49+
generated["title"] = "Roo Code Custom Modes"
50+
generated["description"] = "Schema for .roomodes configuration files used by Roo Code to define custom modes."
51+
52+
expect(checkedIn).toEqual(generated)
53+
})
54+
})

pnpm-lock.yaml

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)