@microck/canonfig 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/cli.js +1 -1
  3. package/dist/harness-configuration/adapters/amp.js +87 -0
  4. package/dist/harness-configuration/adapters/antigravity.js +50 -0
  5. package/dist/harness-configuration/adapters/claude.js +43 -0
  6. package/dist/harness-configuration/adapters/codex.js +60 -0
  7. package/dist/harness-configuration/adapters/copilot.js +79 -0
  8. package/dist/harness-configuration/adapters/cursor.js +67 -0
  9. package/dist/harness-configuration/adapters/descriptor.js +10 -0
  10. package/dist/harness-configuration/adapters/devin.js +66 -0
  11. package/dist/harness-configuration/adapters/droid.js +32 -0
  12. package/dist/harness-configuration/adapters/grok.js +44 -0
  13. package/dist/harness-configuration/adapters/hermes.js +105 -0
  14. package/dist/harness-configuration/adapters/index.js +50 -0
  15. package/dist/harness-configuration/adapters/kilo.js +18 -0
  16. package/dist/harness-configuration/adapters/kimi.js +148 -0
  17. package/dist/harness-configuration/adapters/omp.js +64 -0
  18. package/dist/harness-configuration/adapters/open-code-family.js +94 -0
  19. package/dist/harness-configuration/adapters/opencode.js +18 -0
  20. package/dist/harness-configuration/adapters/pi.js +93 -0
  21. package/dist/harness-configuration/adapters/qwen.js +164 -0
  22. package/dist/harness-configuration/adapters/shared-common.js +66 -0
  23. package/dist/harness-configuration/adapters/shared-documents.js +117 -0
  24. package/dist/harness-configuration/adapters/shared-hooks.js +186 -0
  25. package/dist/harness-configuration/adapters/shared-mcp.js +214 -0
  26. package/dist/harness-configuration/adapters/shared.js +4 -0
  27. package/dist/harness-configuration/adapters/tools.js +24 -0
  28. package/dist/harness-configuration/cli-arguments.js +105 -0
  29. package/dist/harness-configuration/cli-output.js +77 -0
  30. package/dist/harness-configuration/cli.js +196 -0
  31. package/dist/harness-configuration/core/compiler.js +175 -0
  32. package/dist/harness-configuration/core/config.js +74 -0
  33. package/dist/harness-configuration/core/diff.js +60 -0
  34. package/dist/harness-configuration/core/doctor.js +40 -0
  35. package/dist/harness-configuration/core/errors.js +13 -0
  36. package/dist/harness-configuration/core/filesystem.js +172 -0
  37. package/dist/harness-configuration/core/frontmatter.js +49 -0
  38. package/dist/harness-configuration/core/hash.js +4 -0
  39. package/dist/harness-configuration/core/path.js +50 -0
  40. package/dist/harness-configuration/core/planner.js +255 -0
  41. package/dist/harness-configuration/core/render-cleanup.js +91 -0
  42. package/dist/harness-configuration/core/render-json.js +195 -0
  43. package/dist/harness-configuration/core/render-text.js +134 -0
  44. package/dist/harness-configuration/core/render-utils.js +202 -0
  45. package/dist/harness-configuration/core/render.js +54 -0
  46. package/dist/harness-configuration/core/scaffold.js +103 -0
  47. package/dist/harness-configuration/core/schema-components.js +167 -0
  48. package/dist/harness-configuration/core/schema-config.js +98 -0
  49. package/dist/harness-configuration/core/schema-runtime.js +143 -0
  50. package/dist/harness-configuration/core/schema-types.js +8 -0
  51. package/dist/harness-configuration/core/schema.js +13 -0
  52. package/dist/harness-configuration/core/state.js +42 -0
  53. package/dist/harness-configuration/core/types.js +5 -0
  54. package/dist/harness-configuration/core/validation.js +113 -0
  55. package/dist/harness-configuration/templates/runtime.js +212 -0
  56. package/dist/runtime/main.js +20 -14
  57. package/package.json +5 -5
@@ -0,0 +1,202 @@
1
+ export function isRecord(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ export function deepEqual(left, right) {
5
+ if (Object.is(left, right))
6
+ return true;
7
+ if (Array.isArray(left) && Array.isArray(right)) {
8
+ return left.length === right.length
9
+ && left.every((value, index) => deepEqual(value, right[index]));
10
+ }
11
+ if (isRecord(left) && isRecord(right)) {
12
+ const leftKeys = Object.keys(left).sort();
13
+ const rightKeys = Object.keys(right).sort();
14
+ return deepEqual(leftKeys, rightKeys)
15
+ && leftKeys.every((key) => deepEqual(left[key], right[key]));
16
+ }
17
+ return false;
18
+ }
19
+ function stripJsonComments(text) {
20
+ let output = "";
21
+ let index = 0;
22
+ let inString = false;
23
+ let escaped = false;
24
+ while (index < text.length) {
25
+ const character = text[index];
26
+ const next = text[index + 1];
27
+ if (inString) {
28
+ output += character;
29
+ if (escaped)
30
+ escaped = false;
31
+ else if (character === "\\")
32
+ escaped = true;
33
+ else if (character === '"')
34
+ inString = false;
35
+ index += 1;
36
+ continue;
37
+ }
38
+ if (character === '"') {
39
+ inString = true;
40
+ output += character;
41
+ index += 1;
42
+ continue;
43
+ }
44
+ if (character === "/" && next === "/") {
45
+ while (index < text.length && text[index] !== "\n")
46
+ index += 1;
47
+ output += "\n";
48
+ index += 1;
49
+ continue;
50
+ }
51
+ if (character === "/" && next === "*") {
52
+ index += 2;
53
+ while (index + 1 < text.length
54
+ && !(text[index] === "*" && text[index + 1] === "/")) {
55
+ if (text[index] === "\n")
56
+ output += "\n";
57
+ index += 1;
58
+ }
59
+ index += 2;
60
+ continue;
61
+ }
62
+ output += character;
63
+ index += 1;
64
+ }
65
+ return output;
66
+ }
67
+ function removeTrailingCommas(text) {
68
+ let output = "";
69
+ let inString = false;
70
+ let escaped = false;
71
+ for (let index = 0; index < text.length; index += 1) {
72
+ const character = text[index];
73
+ if (inString) {
74
+ output += character;
75
+ if (escaped)
76
+ escaped = false;
77
+ else if (character === "\\")
78
+ escaped = true;
79
+ else if (character === '"')
80
+ inString = false;
81
+ continue;
82
+ }
83
+ if (character === '"') {
84
+ inString = true;
85
+ output += character;
86
+ continue;
87
+ }
88
+ if (character === ",") {
89
+ let cursor = index + 1;
90
+ while (cursor < text.length && /\s/u.test(text[cursor]))
91
+ cursor += 1;
92
+ if (text[cursor] === "}" || text[cursor] === "]")
93
+ continue;
94
+ }
95
+ output += character;
96
+ }
97
+ return output;
98
+ }
99
+ export function parseJsonDocument(text, conflicts) {
100
+ try {
101
+ const parsed = JSON.parse(removeTrailingCommas(stripJsonComments(text)));
102
+ if (!isRecord(parsed)) {
103
+ conflicts.push("JSON/JSONC root must be an object.");
104
+ return {};
105
+ }
106
+ return parsed;
107
+ }
108
+ catch (error) {
109
+ conflicts.push(`Invalid JSON/JSONC: ${error instanceof Error ? error.message : String(error)}`);
110
+ return {};
111
+ }
112
+ }
113
+ export function serializeJsonDocument(document) {
114
+ return `${JSON.stringify(document, undefined, 2)}\n`;
115
+ }
116
+ export function getAtPath(root, path) {
117
+ let current = root;
118
+ for (const segment of path) {
119
+ if (!isRecord(current))
120
+ return undefined;
121
+ current = current[segment];
122
+ }
123
+ return current;
124
+ }
125
+ export function setAtPath(root, path, value) {
126
+ if (path.length === 0) {
127
+ if (!isRecord(value))
128
+ throw new TypeError("JSON root replacement must be an object");
129
+ for (const key of Object.keys(root))
130
+ delete root[key];
131
+ Object.assign(root, value);
132
+ return;
133
+ }
134
+ let parent = root;
135
+ for (const segment of path.slice(0, -1)) {
136
+ const current = parent[segment];
137
+ if (!isRecord(current))
138
+ parent[segment] = {};
139
+ parent = parent[segment];
140
+ }
141
+ const key = path[path.length - 1];
142
+ if (value === undefined)
143
+ delete parent[key];
144
+ else
145
+ parent[key] = value;
146
+ }
147
+ export function commentMarkers(marker, style) {
148
+ const prefix = style === "html" ? "<!-- " : style === "slash" ? "// " : "# ";
149
+ const suffix = style === "html" ? " -->" : "";
150
+ return {
151
+ begin: `${prefix}canonfig:begin ${marker}${suffix}`,
152
+ end: `${prefix}canonfig:end ${marker}${suffix}`,
153
+ };
154
+ }
155
+ export function locateBlock(text, begin, end) {
156
+ const start = text.indexOf(begin);
157
+ if (start < 0)
158
+ return undefined;
159
+ const endStart = text.indexOf(end, start + begin.length);
160
+ if (endStart < 0)
161
+ return undefined;
162
+ let endOffset = endStart + end.length;
163
+ if (text[endOffset] === "\r" && text[endOffset + 1] === "\n")
164
+ endOffset += 2;
165
+ else if (text[endOffset] === "\n")
166
+ endOffset += 1;
167
+ return { start, end: endOffset, block: text.slice(start, endOffset) };
168
+ }
169
+ export function identityOf(value, identity) {
170
+ return identity === undefined || !isRecord(value) ? value : value[identity];
171
+ }
172
+ export function containsMarker(value, marker) {
173
+ if (typeof value === "string")
174
+ return value.includes(marker);
175
+ if (Array.isArray(value))
176
+ return value.some((item) => containsMarker(item, marker));
177
+ return isRecord(value)
178
+ && Object.values(value).some((item) => containsMarker(item, marker));
179
+ }
180
+ export function tomlBlockMarkers(marker) {
181
+ return {
182
+ begin: `# canonfig:begin ${marker}`,
183
+ end: `# canonfig:end ${marker}`,
184
+ };
185
+ }
186
+ export function findTomlSection(lines, section) {
187
+ const escaped = section.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
188
+ const target = new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`, "u");
189
+ for (let index = 0; index < lines.length; index += 1) {
190
+ if (!target.test(lines[index] ?? ""))
191
+ continue;
192
+ let end = lines.length;
193
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
194
+ if (/^\s*\[\[?.+?\]\]?\s*(?:#.*)?$/u.test(lines[cursor] ?? "")) {
195
+ end = cursor;
196
+ break;
197
+ }
198
+ }
199
+ return { header: index, end };
200
+ }
201
+ return undefined;
202
+ }
@@ -0,0 +1,54 @@
1
+ import { sha256 } from "./hash.js";
2
+ import { unapplyPrevious } from "./render-cleanup.js";
3
+ import { applyJsonArtifact } from "./render-json.js";
4
+ import { appendManagedText, applyTomlArtifact } from "./render-text.js";
5
+ export function renderArtifacts(artifacts, current, previous, force = false) {
6
+ const conflicts = [];
7
+ const output = unapplyPrevious(current, previous, force, conflicts);
8
+ const cleanup = [];
9
+ if (artifacts.length === 0)
10
+ return { content: output, cleanup, conflicts };
11
+ const replacements = artifacts.filter((artifact) => artifact.kind === "replace");
12
+ if (replacements.length > 0) {
13
+ if (artifacts.length !== 1) {
14
+ conflicts.push("A replace artifact cannot share a path with merge artifacts.");
15
+ }
16
+ const replacement = replacements[0];
17
+ if (replacement === undefined)
18
+ return { content: output, cleanup, conflicts };
19
+ if (previous === undefined
20
+ && current !== undefined
21
+ && sha256(current) !== sha256(replacement.content)
22
+ && !force) {
23
+ conflicts.push("File already exists and is not owned by Canonfig.");
24
+ }
25
+ return {
26
+ content: replacement.content,
27
+ cleanup: [{ kind: "replace" }],
28
+ conflicts,
29
+ };
30
+ }
31
+ if (output instanceof Uint8Array) {
32
+ conflicts.push("Cannot merge text configuration into an existing binary file.");
33
+ return { content: output, cleanup, conflicts };
34
+ }
35
+ let text = output ?? "";
36
+ for (const artifact of artifacts) {
37
+ if (artifact.kind === "managed-text") {
38
+ const result = appendManagedText(text, artifact, force, conflicts);
39
+ text = result.text;
40
+ cleanup.push(result.cleanup);
41
+ }
42
+ else if (artifact.kind === "json") {
43
+ const result = applyJsonArtifact(text, artifact, force, conflicts);
44
+ text = result.text;
45
+ cleanup.push(...result.cleanup);
46
+ }
47
+ else if (artifact.kind === "toml") {
48
+ const result = applyTomlArtifact(text, artifact, force, conflicts);
49
+ text = result.text;
50
+ cleanup.push(...result.cleanup);
51
+ }
52
+ }
53
+ return { content: text, cleanup, conflicts };
54
+ }
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { TARGET_IDS } from "./types.js";
5
+ import { CanonfigError } from "./errors.js";
6
+ import { assertRealPathInside, resolveInside } from "./path.js";
7
+ const ROOT_INSTRUCTIONS = `# Repository instructions
8
+
9
+ Describe the project, architecture, validation commands, constraints, and definition of done here.
10
+ `;
11
+ const EXAMPLE_RULE = `# Source-code rules
12
+
13
+ - Keep changes scoped.
14
+ - Run the smallest relevant validation command before finishing.
15
+ `;
16
+ const EXAMPLE_AGENT = `Review the requested change for correctness, security, regressions, and missing tests.
17
+ Return concrete findings before general commentary.
18
+ `;
19
+ const EXAMPLE_COMMAND = `Inspect the current changes, run relevant checks, and produce a release-readiness report.
20
+ `;
21
+ const EXAMPLE_SKILL = `---
22
+ name: repository-checks
23
+ description: Discover and run the repository's relevant validation commands.
24
+ ---
25
+
26
+ # Repository checks
27
+
28
+ 1. Inspect package and build metadata.
29
+ 2. Select the narrowest relevant checks.
30
+ 3. Report commands, results, and unresolved failures.
31
+ `;
32
+ async function writeNew(root, relativePath, content, force) {
33
+ const filePath = resolveInside(root, relativePath);
34
+ await assertRealPathInside(root, filePath);
35
+ try {
36
+ await fs.access(filePath);
37
+ if (!force)
38
+ return false;
39
+ }
40
+ catch (error) {
41
+ if (error.code !== "ENOENT")
42
+ throw error;
43
+ }
44
+ await assertRealPathInside(root, filePath);
45
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
46
+ await assertRealPathInside(root, filePath);
47
+ await fs.writeFile(filePath, content, "utf8");
48
+ return true;
49
+ }
50
+ export async function scaffoldProject(root, options = {}) {
51
+ const targets = [...new Set(options.targets ?? TARGET_IDS)];
52
+ if (targets.length === 0)
53
+ throw new CanonfigError("TARGET_EMPTY", "At least one target is required for init.");
54
+ const force = options.force ?? false;
55
+ const config = {
56
+ version: 1,
57
+ project: { name: path.basename(path.resolve(root)) },
58
+ targets: Object.fromEntries(targets.map((target) => [target, { enabled: true, options: {} }])),
59
+ instructions: {
60
+ root: "instructions/AGENTS.md",
61
+ rules: [{
62
+ id: "source",
63
+ file: "rules/source.md",
64
+ paths: ["src/**", "tests/**"],
65
+ activation: "path",
66
+ description: "Rules for source and test files",
67
+ }],
68
+ },
69
+ skills: { roots: ["skills"] },
70
+ mcp: { servers: {} },
71
+ hooks: [],
72
+ agents: [{
73
+ id: "reviewer",
74
+ file: "agents/reviewer.md",
75
+ description: "Reviews changes for correctness and regressions",
76
+ model: "inherit",
77
+ tools: ["read", "search", "test", "git"],
78
+ writable: false,
79
+ }],
80
+ commands: [{
81
+ id: "release-check",
82
+ file: "commands/release-check.md",
83
+ description: "Run a release-readiness review",
84
+ argumentHint: "[scope]",
85
+ }],
86
+ permissions: { rules: [] },
87
+ extensions: {},
88
+ };
89
+ const files = [
90
+ [".canonfig/harness.yaml", YAML.stringify(config, { lineWidth: 120 })],
91
+ [".canonfig/instructions/AGENTS.md", ROOT_INSTRUCTIONS],
92
+ [".canonfig/rules/source.md", EXAMPLE_RULE],
93
+ [".canonfig/agents/reviewer.md", EXAMPLE_AGENT],
94
+ [".canonfig/commands/release-check.md", EXAMPLE_COMMAND],
95
+ [".canonfig/skills/repository-checks/SKILL.md", EXAMPLE_SKILL],
96
+ ];
97
+ const written = [];
98
+ for (const [relativePath, content] of files) {
99
+ if (await writeNew(root, relativePath, content, force))
100
+ written.push(relativePath);
101
+ }
102
+ return written;
103
+ }
@@ -0,0 +1,167 @@
1
+ import { TARGET_IDS } from "./types.js";
2
+ import { booleanValue, enumValue, idValue, objectValue, optionalString, positiveInteger, relativePath, secretRecord, stringArray, stringValue, } from "./schema-runtime.js";
3
+ import { CAPABILITIES, HOOK_EVENTS, } from "./schema-types.js";
4
+ export const MCP_NAME_PATTERN = /^[A-Za-z0-9._-]+$/u;
5
+ export function parseMcpServer(input, validator, path) {
6
+ const value = objectValue(input, validator, path);
7
+ const transport = enumValue(value.transport, ["stdio", "streamable-http", "sse"], validator, [...path, "transport"], "stdio");
8
+ const enabled = booleanValue(value.enabled, validator, [...path, "enabled"], true);
9
+ const timeoutMs = positiveInteger(value.timeoutMs, validator, [...path, "timeoutMs"]);
10
+ const enabledTools = value.enabledTools === undefined
11
+ ? undefined
12
+ : stringArray(value.enabledTools, validator, [...path, "enabledTools"]);
13
+ const disabledTools = value.disabledTools === undefined
14
+ ? undefined
15
+ : stringArray(value.disabledTools, validator, [...path, "disabledTools"]);
16
+ const common = {
17
+ enabled,
18
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
19
+ ...(enabledTools === undefined ? {} : { enabledTools }),
20
+ ...(disabledTools === undefined ? {} : { disabledTools }),
21
+ };
22
+ if (transport === "stdio") {
23
+ const cwd = optionalString(value.cwd, validator, [...path, "cwd"]);
24
+ return {
25
+ ...common,
26
+ transport,
27
+ command: stringValue(value.command, validator, [...path, "command"], {
28
+ min: 1,
29
+ }),
30
+ args: stringArray(value.args, validator, [...path, "args"]),
31
+ ...(cwd === undefined ? {} : { cwd }),
32
+ env: secretRecord(value.env, validator, [...path, "env"]),
33
+ };
34
+ }
35
+ const url = stringValue(value.url, validator, [...path, "url"], { min: 1 });
36
+ try {
37
+ const parsed = new URL(url);
38
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
39
+ validator.issue([...path, "url"], "Expected an HTTP(S) URL.");
40
+ }
41
+ }
42
+ catch {
43
+ validator.issue([...path, "url"], "Expected a valid URL.");
44
+ }
45
+ return {
46
+ ...common,
47
+ transport,
48
+ url,
49
+ headers: secretRecord(value.headers, validator, [...path, "headers"]),
50
+ };
51
+ }
52
+ export function parseHook(input, validator, path) {
53
+ const value = objectValue(input, validator, path);
54
+ const matcherValue = value.matcher === undefined
55
+ ? {}
56
+ : objectValue(value.matcher, validator, [...path, "matcher"]);
57
+ const inputRegex = optionalString(matcherValue.inputRegex, validator, [...path, "matcher", "inputRegex"]);
58
+ let capabilities = [];
59
+ if (matcherValue.capabilities !== undefined) {
60
+ if (!Array.isArray(matcherValue.capabilities)) {
61
+ validator.issue([...path, "matcher", "capabilities"], "Expected an array.");
62
+ }
63
+ else {
64
+ capabilities = matcherValue.capabilities.map((item, index) => enumValue(item, CAPABILITIES, validator, [...path, "matcher", "capabilities", index], "read"));
65
+ }
66
+ }
67
+ const matcher = {
68
+ capabilities,
69
+ tools: stringArray(matcherValue.tools, validator, [...path, "matcher", "tools"]),
70
+ ...(inputRegex === undefined ? {} : { inputRegex }),
71
+ };
72
+ const run = stringArray(value.run, validator, [...path, "run"]);
73
+ if (run.length === 0) {
74
+ validator.issue([...path, "run"], "Expected at least one command argument.");
75
+ }
76
+ return {
77
+ id: idValue(value.id, validator, [...path, "id"]),
78
+ event: enumValue(value.event, HOOK_EVENTS, validator, [...path, "event"], "before_tool"),
79
+ enabled: booleanValue(value.enabled, validator, [...path, "enabled"], true),
80
+ matcher,
81
+ run,
82
+ timeoutMs: positiveInteger(value.timeoutMs, validator, [...path, "timeoutMs"], 600_000) ?? 10_000,
83
+ onFailure: value.onFailure === undefined
84
+ ? "block"
85
+ : enumValue(value.onFailure, ["block", "warn", "ignore"], validator, [...path, "onFailure"], "block"),
86
+ };
87
+ }
88
+ export function parseRule(input, validator, path) {
89
+ const value = objectValue(input, validator, path);
90
+ const activation = value.activation === undefined
91
+ ? undefined
92
+ : enumValue(value.activation, ["always", "path", "manual", "model"], validator, [...path, "activation"], "always");
93
+ const description = optionalString(value.description, validator, [...path, "description"]);
94
+ return {
95
+ id: idValue(value.id, validator, [...path, "id"]),
96
+ file: relativePath(value.file, validator, [...path, "file"]),
97
+ paths: stringArray(value.paths, validator, [...path, "paths"]),
98
+ ...(activation === undefined ? {} : { activation }),
99
+ ...(description === undefined ? {} : { description }),
100
+ };
101
+ }
102
+ export function parseAgent(input, validator, path) {
103
+ const value = objectValue(input, validator, path);
104
+ const toolInput = value.tools === undefined ? ["read", "search"] : value.tools;
105
+ let tools = [];
106
+ if (!Array.isArray(toolInput)) {
107
+ validator.issue([...path, "tools"], "Expected an array.");
108
+ }
109
+ else {
110
+ tools = toolInput.map((item, index) => enumValue(item, CAPABILITIES, validator, [...path, "tools", index], "read"));
111
+ }
112
+ return {
113
+ id: idValue(value.id, validator, [...path, "id"]),
114
+ file: relativePath(value.file, validator, [...path, "file"]),
115
+ description: stringValue(value.description, validator, [...path, "description"], { min: 1 }),
116
+ model: value.model === undefined
117
+ ? "inherit"
118
+ : stringValue(value.model, validator, [...path, "model"]),
119
+ tools,
120
+ writable: booleanValue(value.writable, validator, [...path, "writable"], false),
121
+ };
122
+ }
123
+ export function parseCommand(input, validator, path) {
124
+ const value = objectValue(input, validator, path);
125
+ const argumentHint = optionalString(value.argumentHint, validator, [...path, "argumentHint"]);
126
+ return {
127
+ id: idValue(value.id, validator, [...path, "id"]),
128
+ file: relativePath(value.file, validator, [...path, "file"]),
129
+ description: stringValue(value.description, validator, [...path, "description"], { min: 1 }),
130
+ ...(argumentHint === undefined ? {} : { argumentHint }),
131
+ };
132
+ }
133
+ export function parsePermissionRule(input, validator, path) {
134
+ const value = objectValue(input, validator, path);
135
+ const reason = optionalString(value.reason, validator, [...path, "reason"]);
136
+ return {
137
+ pattern: stringValue(value.pattern, validator, [...path, "pattern"], {
138
+ min: 1,
139
+ }),
140
+ action: enumValue(value.action, ["allow", "ask", "deny"], validator, [...path, "action"], "ask"),
141
+ ...(reason === undefined ? {} : { reason }),
142
+ };
143
+ }
144
+ export function parseTargetEntry(input, validator, path) {
145
+ const value = objectValue(input, validator, path);
146
+ return {
147
+ enabled: booleanValue(value.enabled, validator, [...path, "enabled"], true),
148
+ options: value.options === undefined
149
+ ? {}
150
+ : objectValue(value.options, validator, [...path, "options"]),
151
+ };
152
+ }
153
+ export function parseTargets(input, validator, path) {
154
+ if (Array.isArray(input)) {
155
+ return input.map((value, index) => enumValue(value, TARGET_IDS, validator, [...path, index], "codex"));
156
+ }
157
+ const value = objectValue(input, validator, path);
158
+ const output = {};
159
+ for (const [key, entry] of Object.entries(value)) {
160
+ if (!TARGET_IDS.includes(key)) {
161
+ validator.issue([...path, key], `Unknown target. Expected one of: ${TARGET_IDS.join(", ")}.`);
162
+ continue;
163
+ }
164
+ output[key] = parseTargetEntry(entry, validator, [...path, key]);
165
+ }
166
+ return output;
167
+ }
@@ -0,0 +1,98 @@
1
+ import { TARGET_IDS } from "./types.js";
2
+ import { objectValue, optionalString, relativePath, } from "./schema-runtime.js";
3
+ import { MCP_NAME_PATTERN, parseAgent, parseCommand, parseHook, parseMcpServer, parsePermissionRule, parseRule, parseTargets, } from "./schema-components.js";
4
+ function parsedArray(input, validator, path, parser) {
5
+ if (input === undefined)
6
+ return [];
7
+ if (!Array.isArray(input)) {
8
+ validator.issue(path, "Expected an array.");
9
+ return [];
10
+ }
11
+ return input.map((item, index) => parser(item, validator, [...path, index]));
12
+ }
13
+ function duplicateIds(validator, label, items) {
14
+ const seen = new Set();
15
+ for (const item of items) {
16
+ if (seen.has(item.id))
17
+ validator.issue([], `Duplicate ${label} id: ${item.id}`);
18
+ seen.add(item.id);
19
+ }
20
+ }
21
+ export function parseConfig(input, validator, path) {
22
+ const value = objectValue(input, validator, path);
23
+ if (value.version !== 1)
24
+ validator.issue(["version"], "Expected literal value 1.");
25
+ const projectValue = value.project === undefined
26
+ ? {}
27
+ : objectValue(value.project, validator, ["project"]);
28
+ const projectName = optionalString(projectValue.name, validator, ["project", "name"]);
29
+ const instructionsValue = value.instructions === undefined
30
+ ? {}
31
+ : objectValue(value.instructions, validator, ["instructions"]);
32
+ const rules = parsedArray(instructionsValue.rules, validator, ["instructions", "rules"], parseRule);
33
+ const skillsValue = value.skills === undefined
34
+ ? {}
35
+ : objectValue(value.skills, validator, ["skills"]);
36
+ const rootsInput = skillsValue.roots === undefined ? ["skills"] : skillsValue.roots;
37
+ let roots = [];
38
+ if (!Array.isArray(rootsInput)) {
39
+ validator.issue(["skills", "roots"], "Expected an array.");
40
+ }
41
+ else {
42
+ roots = rootsInput.map((item, index) => relativePath(item, validator, ["skills", "roots", index]));
43
+ }
44
+ const mcpValue = value.mcp === undefined
45
+ ? {}
46
+ : objectValue(value.mcp, validator, ["mcp"]);
47
+ const serversValue = mcpValue.servers === undefined
48
+ ? {}
49
+ : objectValue(mcpValue.servers, validator, ["mcp", "servers"]);
50
+ const servers = {};
51
+ for (const [name, server] of Object.entries(serversValue)) {
52
+ if (!MCP_NAME_PATTERN.test(name)) {
53
+ validator.issue(["mcp", "servers", name], "Invalid MCP server name.");
54
+ }
55
+ servers[name] = parseMcpServer(server, validator, ["mcp", "servers", name]);
56
+ }
57
+ const hooks = parsedArray(value.hooks, validator, ["hooks"], parseHook);
58
+ const agents = parsedArray(value.agents, validator, ["agents"], parseAgent);
59
+ const commands = parsedArray(value.commands, validator, ["commands"], parseCommand);
60
+ const permissionsValue = value.permissions === undefined
61
+ ? {}
62
+ : objectValue(value.permissions, validator, ["permissions"]);
63
+ const permissionRules = parsedArray(permissionsValue.rules, validator, ["permissions", "rules"], parsePermissionRule);
64
+ const extensionsValue = value.extensions === undefined
65
+ ? {}
66
+ : objectValue(value.extensions, validator, ["extensions"]);
67
+ const extensions = {};
68
+ for (const [key, extension] of Object.entries(extensionsValue)) {
69
+ if (!TARGET_IDS.includes(key)) {
70
+ validator.issue(["extensions", key], `Unknown target. Expected one of: ${TARGET_IDS.join(", ")}.`);
71
+ continue;
72
+ }
73
+ extensions[key] = objectValue(extension, validator, ["extensions", key]);
74
+ }
75
+ duplicateIds(validator, "rule", rules);
76
+ duplicateIds(validator, "hook", hooks);
77
+ duplicateIds(validator, "agent", agents);
78
+ duplicateIds(validator, "command", commands);
79
+ const targets = value.targets === undefined
80
+ ? (validator.issue(["targets"], "Required."), [])
81
+ : parseTargets(value.targets, validator, ["targets"]);
82
+ return {
83
+ version: 1,
84
+ project: projectName === undefined ? {} : { name: projectName },
85
+ targets,
86
+ instructions: {
87
+ root: relativePath(instructionsValue.root, validator, ["instructions", "root"], "instructions/AGENTS.md"),
88
+ rules,
89
+ },
90
+ skills: { roots },
91
+ mcp: { servers },
92
+ hooks,
93
+ agents,
94
+ commands,
95
+ permissions: { rules: permissionRules },
96
+ extensions,
97
+ };
98
+ }