@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,214 @@
1
+ export function secretValue(value) {
2
+ return typeof value === "string" ? value : `\${${value.fromEnv}}`;
3
+ }
4
+ export function enabledMcpServerEntries(context) {
5
+ return Object.entries(context.config.mcp.servers).filter(([, server]) => server.enabled);
6
+ }
7
+ export function hasEnabledMcpServers(context) {
8
+ return enabledMcpServerEntries(context).length > 0;
9
+ }
10
+ export function standardMcpProjectionDiagnostics(context, target, includeType = true) {
11
+ const diagnostics = [];
12
+ for (const [name, server] of enabledMcpServerEntries(context)) {
13
+ const omitted = [];
14
+ if (server.timeoutMs !== undefined)
15
+ omitted.push("timeoutMs");
16
+ if (server.enabledTools?.length)
17
+ omitted.push("enabledTools");
18
+ if (server.disabledTools?.length)
19
+ omitted.push("disabledTools");
20
+ if (!includeType && server.transport === "sse")
21
+ omitted.push("sse transport discriminator");
22
+ if (omitted.length > 0) {
23
+ diagnostics.push({
24
+ level: "warning",
25
+ code: "MCP_OPTION_UNSUPPORTED",
26
+ target,
27
+ message: `${target} cannot represent ${omitted.join(", ")} for MCP server ${name} in its standard JSON projection; those options were omitted.`,
28
+ });
29
+ }
30
+ }
31
+ return diagnostics;
32
+ }
33
+ export function codexMcpDiagnostics(context) {
34
+ return enabledMcpServerEntries(context).flatMap(([name, server]) => server.transport === "sse"
35
+ ? [{
36
+ level: "warning",
37
+ code: "MCP_TRANSPORT_UNSUPPORTED",
38
+ target: "codex",
39
+ message: `Codex project MCP config supports streamable HTTP URLs but cannot preserve legacy SSE transport for server ${name}; the URL is emitted as streamable HTTP.`,
40
+ }]
41
+ : []);
42
+ }
43
+ export function standardMcpServer(server, includeType = true) {
44
+ if (server.transport === "stdio") {
45
+ return {
46
+ ...(includeType ? { type: "stdio" } : {}),
47
+ command: server.command,
48
+ args: server.args,
49
+ ...(Object.keys(server.env).length > 0
50
+ ? { env: Object.fromEntries(Object.entries(server.env).map(([key, value]) => [key, secretValue(value)])) }
51
+ : {}),
52
+ ...(server.cwd ? { cwd: server.cwd } : {}),
53
+ };
54
+ }
55
+ return {
56
+ ...(includeType ? { type: server.transport === "sse" ? "sse" : "http" } : {}),
57
+ url: server.url,
58
+ ...(Object.keys(server.headers).length > 0
59
+ ? { headers: Object.fromEntries(Object.entries(server.headers).map(([key, value]) => [key, secretValue(value)])) }
60
+ : {}),
61
+ };
62
+ }
63
+ export function standardMcpMap(context, includeType = true) {
64
+ return Object.fromEntries(enabledMcpServerEntries(context)
65
+ .map(([name, server]) => [name, standardMcpServer(server, includeType)]));
66
+ }
67
+ export function piMcpMap(context) {
68
+ return Object.fromEntries(enabledMcpServerEntries(context)
69
+ .map(([name, server]) => {
70
+ if (server.transport === "stdio") {
71
+ return [name, {
72
+ command: server.command,
73
+ args: server.args,
74
+ ...(server.cwd ? { cwd: server.cwd } : {}),
75
+ ...(Object.keys(server.env).length
76
+ ? { env: Object.fromEntries(Object.entries(server.env).map(([key, value]) => [key, secretValue(value)])) }
77
+ : {}),
78
+ }];
79
+ }
80
+ return [name, {
81
+ transport: server.transport,
82
+ url: server.url,
83
+ ...(Object.keys(server.headers).length
84
+ ? { headers: Object.fromEntries(Object.entries(server.headers).map(([key, value]) => [key, secretValue(value)])) }
85
+ : {}),
86
+ }];
87
+ }));
88
+ }
89
+ export function antigravityMcpMap(context) {
90
+ return Object.fromEntries(enabledMcpServerEntries(context)
91
+ .map(([name, server]) => {
92
+ if (server.transport === "stdio") {
93
+ return [name, {
94
+ command: server.command,
95
+ args: server.args,
96
+ ...(Object.keys(server.env).length
97
+ ? { env: Object.fromEntries(Object.entries(server.env).map(([key, value]) => [key, secretValue(value)])) }
98
+ : {}),
99
+ }];
100
+ }
101
+ return [name, {
102
+ serverUrl: server.url,
103
+ ...(Object.keys(server.headers).length
104
+ ? { headers: Object.fromEntries(Object.entries(server.headers).map(([key, value]) => [key, secretValue(value)])) }
105
+ : {}),
106
+ }];
107
+ }));
108
+ }
109
+ export function openCodeMcpMap(context) {
110
+ return Object.fromEntries(Object.entries(context.config.mcp.servers).map(([name, server]) => {
111
+ if (server.transport === "stdio") {
112
+ return [name, {
113
+ type: "local",
114
+ command: [server.command, ...server.args],
115
+ enabled: server.enabled,
116
+ ...(Object.keys(server.env).length
117
+ ? { environment: Object.fromEntries(Object.entries(server.env).map(([key, value]) => [key, secretValue(value)])) }
118
+ : {}),
119
+ ...(server.timeoutMs ? { timeout: server.timeoutMs } : {}),
120
+ }];
121
+ }
122
+ return [name, {
123
+ type: "remote",
124
+ url: server.url,
125
+ enabled: server.enabled,
126
+ ...(Object.keys(server.headers).length
127
+ ? { headers: Object.fromEntries(Object.entries(server.headers).map(([key, value]) => [key, secretValue(value)])) }
128
+ : {}),
129
+ ...(server.timeoutMs ? { timeout: server.timeoutMs } : {}),
130
+ }];
131
+ }));
132
+ }
133
+ function tomlString(value) {
134
+ return JSON.stringify(value);
135
+ }
136
+ function tomlKey(value) {
137
+ return /^[A-Za-z0-9_-]+$/.test(value) ? value : JSON.stringify(value);
138
+ }
139
+ function tomlArray(values) {
140
+ return `[${values.map(tomlString).join(", ")}]`;
141
+ }
142
+ function tomlInlineTable(entries) {
143
+ return `{ ${Object.entries(entries).map(([key, value]) => `${tomlKey(key)} = ${tomlString(value)}`).join(", ")} }`;
144
+ }
145
+ export function mcpToml(context, remoteHeadersKey = "http_headers") {
146
+ const sections = [];
147
+ for (const [name, server] of enabledMcpServerEntries(context)) {
148
+ const lines = [`[mcp_servers.${tomlKey(name)}]`];
149
+ if (server.transport === "stdio") {
150
+ lines.push(`command = ${tomlString(server.command)}`);
151
+ if (server.args.length)
152
+ lines.push(`args = ${tomlArray(server.args)}`);
153
+ if (server.cwd)
154
+ lines.push(`cwd = ${tomlString(server.cwd)}`);
155
+ if (Object.keys(server.env).length) {
156
+ lines.push(`env = ${tomlInlineTable(Object.fromEntries(Object.entries(server.env).map(([key, value]) => [key, secretValue(value)])))}`);
157
+ }
158
+ }
159
+ else {
160
+ lines.push(`url = ${tomlString(server.url)}`);
161
+ if (Object.keys(server.headers).length) {
162
+ lines.push(`${remoteHeadersKey} = ${tomlInlineTable(Object.fromEntries(Object.entries(server.headers).map(([key, value]) => [key, secretValue(value)])))}`);
163
+ }
164
+ }
165
+ if (server.enabledTools?.length)
166
+ lines.push(`enabled_tools = ${tomlArray(server.enabledTools)}`);
167
+ if (server.disabledTools?.length)
168
+ lines.push(`disabled_tools = ${tomlArray(server.disabledTools)}`);
169
+ sections.push(lines.join("\n"));
170
+ }
171
+ return sections.join("\n\n");
172
+ }
173
+ export function jsonMcpArtifact(pathname, owner, context, pathSegments = ["mcpServers"], includeType = true) {
174
+ return {
175
+ kind: "json",
176
+ path: pathname,
177
+ owner,
178
+ operations: [{
179
+ kind: "managed-map",
180
+ path: pathSegments,
181
+ entries: standardMcpMap(context, includeType),
182
+ collision: "error",
183
+ }],
184
+ };
185
+ }
186
+ export function grokMcpToml(context) {
187
+ const sections = [];
188
+ for (const [name, server] of enabledMcpServerEntries(context)) {
189
+ const lines = [`[mcp_servers.${tomlKey(name)}]`];
190
+ if (server.transport === "stdio") {
191
+ lines.push(`command = ${tomlString(server.command)}`);
192
+ if (server.args.length)
193
+ lines.push(`args = ${tomlArray(server.args)}`);
194
+ if (server.cwd)
195
+ lines.push(`cwd = ${tomlString(server.cwd)}`);
196
+ if (Object.keys(server.env).length) {
197
+ lines.push(`env = ${tomlInlineTable(Object.fromEntries(Object.entries(server.env).map(([key, value]) => [key, secretValue(value)])))}`);
198
+ }
199
+ }
200
+ else {
201
+ lines.push(`url = ${tomlString(server.url)}`);
202
+ if (Object.keys(server.headers).length) {
203
+ lines.push(`headers = ${tomlInlineTable(Object.fromEntries(Object.entries(server.headers).map(([key, value]) => [key, secretValue(value)])))}`);
204
+ }
205
+ }
206
+ if (server.timeoutMs) {
207
+ const seconds = Math.max(1, Math.ceil(server.timeoutMs / 1000));
208
+ lines.push(`startup_timeout_sec = ${seconds}`);
209
+ lines.push(`tool_timeout_sec = ${seconds}`);
210
+ }
211
+ sections.push(lines.join("\n"));
212
+ }
213
+ return sections.join("\n\n");
214
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./shared-common.js";
2
+ export * from "./shared-documents.js";
3
+ export * from "./shared-hooks.js";
4
+ export * from "./shared-mcp.js";
@@ -0,0 +1,24 @@
1
+ const maps = {
2
+ codex: { read: ["read_file"], write: ["apply_patch"], search: ["grep", "glob"], shell: ["shell"], web: ["web_search"], mcp: ["mcp"], subagent: ["spawn_agent"], test: ["shell"], git: ["shell"] },
3
+ "claude-code": { read: ["Read"], write: ["Edit", "Write"], search: ["Grep", "Glob"], shell: ["Bash"], web: ["WebFetch", "WebSearch"], mcp: [], subagent: ["Task"], test: ["Bash"], git: ["Bash"] },
4
+ amp: { read: ["read"], write: ["edit", "write"], search: ["grep", "glob"], shell: ["Bash"], web: ["web"], mcp: ["mcp"], subagent: ["agent"], test: ["Bash"], git: ["Bash"] },
5
+ "oh-my-pi": { read: ["read"], write: ["edit", "write"], search: ["search", "find"], shell: ["bash"], web: ["web_search"], mcp: ["mcp"], subagent: ["task"], test: ["bash"], git: ["bash"] },
6
+ pi: { read: ["read"], write: ["edit", "write"], search: ["grep", "find"], shell: ["bash"], web: ["web"], mcp: ["mcp"], subagent: ["task"], test: ["bash"], git: ["bash"] },
7
+ "factory-droid": { read: ["Read"], write: ["Edit", "Create", "ApplyPatch"], search: ["Grep", "Glob", "LS"], shell: ["Execute"], web: ["FetchUrl", "WebSearch"], mcp: ["mcp__.*"], subagent: ["Task"], test: ["Execute"], git: ["Execute"] },
8
+ cursor: { read: ["Read"], write: ["Edit", "Write"], search: ["Grep", "Glob"], shell: ["Shell"], web: ["WebFetch", "WebSearch"], mcp: ["MCP"], subagent: ["Agent"], test: ["Shell"], git: ["Shell"] },
9
+ devin: { read: ["read"], write: ["edit", "write"], search: ["grep", "glob"], shell: ["exec"], web: ["web"], mcp: ["mcp"], subagent: ["subagent"], test: ["exec"], git: ["exec"] },
10
+ opencode: { read: ["read"], write: ["edit", "write"], search: ["grep", "glob"], shell: ["bash"], web: ["webfetch"], mcp: [], subagent: ["task"], test: ["bash"], git: ["bash"] },
11
+ "grok-build": { read: ["Read"], write: ["Edit", "Write"], search: ["Grep", "Glob"], shell: ["Bash"], web: ["WebFetch", "WebSearch"], mcp: ["mcp__.*"], subagent: ["Agent"], test: ["Bash"], git: ["Bash"] },
12
+ antigravity: { read: ["view_file"], write: ["write_to_file", "replace_file_content", "multi_replace_file_content"], search: ["grep_search", "find_by_name", "list_dir"], shell: ["run_command"], web: ["browser_*", "search_web"], mcp: ["mcp_*"], subagent: ["task"], test: ["run_command"], git: ["run_command"] },
13
+ "copilot-cli": { read: ["Read"], write: ["Edit", "Write"], search: ["Grep", "Glob"], shell: ["Bash"], web: ["WebFetch", "WebSearch"], mcp: ["mcp__*"], subagent: ["Task"], test: ["Bash"], git: ["Bash"] },
14
+ kimi: { read: ["Read"], write: ["Edit", "Write"], search: ["Grep", "Glob"], shell: ["Bash"], web: ["WebSearch", "FetchURL"], mcp: ["mcp__*"], subagent: ["Agent", "AgentSwarm"], test: ["Bash"], git: ["Bash"] },
15
+ kilo: { read: ["read"], write: ["edit", "write"], search: ["grep", "glob"], shell: ["bash"], web: ["webfetch"], mcp: [], subagent: ["task"], test: ["bash"], git: ["bash"] },
16
+ hermes: { read: ["read_file"], write: ["write_file", "patch"], search: ["search_files"], shell: ["terminal"], web: ["web_search", "web_extract"], mcp: ["mcp"], subagent: ["delegate_task"], test: ["terminal"], git: ["terminal"] },
17
+ qwen: { read: ["read_file"], write: ["edit", "write_file"], search: ["grep_search", "glob", "list_directory"], shell: ["run_shell_command"], web: ["web_fetch", "web_search"], mcp: ["mcp__*"], subagent: ["agent"], test: ["run_shell_command"], git: ["run_shell_command"] },
18
+ };
19
+ export function nativeToolsForCapabilities(target, capabilities) {
20
+ return [...new Set(capabilities.flatMap((capability) => maps[target][capability]))];
21
+ }
22
+ export function nativeTools(target, agent) {
23
+ return nativeToolsForCapabilities(target, agent.tools);
24
+ }
@@ -0,0 +1,105 @@
1
+ import path from "node:path";
2
+ import { parseTargetList } from "./core/config.js";
3
+ import { CanonfigError } from "./core/errors.js";
4
+ export const harnessHelpText = `Canonfig harness configuration
5
+
6
+ Usage: canonfig harness <command> [options]
7
+
8
+ Commands:
9
+ init Create .canonfig/harness.yaml and canonical source directories
10
+ validate Validate canonical sources and selected adapter translations
11
+ targets List built-in harness adapters and support levels
12
+ plan Show native files that would change
13
+ apply Apply the current plan atomically
14
+ sync Alias for apply
15
+ status Report pending changes, conflicts, and diagnostics
16
+ diff Print a unified-style diff for pending changes
17
+ clean Remove only configuration currently owned by Canonfig
18
+ doctor Probe selected harness executables
19
+
20
+ Options:
21
+ --root <path> Repository root or descendant working directory
22
+ --target <id> Select one target; repeatable
23
+ --targets <ids> Select comma-separated targets
24
+ --strict Reject shim, lossy, and unsupported mappings
25
+ --force Take ownership of explicit collisions or managed edits
26
+ --all Include unchanged files in plan output
27
+ --dry-run Do not write during apply or clean
28
+ --no-input Never prompt; accepted for scheduled invocations
29
+ --json Emit the stable canonfig.cli/v1 envelope
30
+ `;
31
+ export const parseHarnessArguments = (arguments_) => {
32
+ const [command = "help", ...rest] = arguments_;
33
+ let root = process.cwd();
34
+ let json = false;
35
+ let strict = false;
36
+ let force = false;
37
+ let all = false;
38
+ let dryRun = false;
39
+ const requestedTargets = [];
40
+ for (let index = 0; index < rest.length; index += 1) {
41
+ const argument = rest[index];
42
+ if (argument === "--json") {
43
+ json = true;
44
+ continue;
45
+ }
46
+ if (argument === "--strict") {
47
+ strict = true;
48
+ continue;
49
+ }
50
+ if (argument === "--force") {
51
+ force = true;
52
+ continue;
53
+ }
54
+ if (argument === "--all") {
55
+ all = true;
56
+ continue;
57
+ }
58
+ if (argument === "--dry-run") {
59
+ dryRun = true;
60
+ continue;
61
+ }
62
+ if (argument === "--no-input")
63
+ continue;
64
+ if (argument === "--help" || argument === "-h") {
65
+ return {
66
+ command: "help",
67
+ root: path.resolve(root),
68
+ json,
69
+ strict,
70
+ force,
71
+ all,
72
+ dryRun,
73
+ };
74
+ }
75
+ if (argument === "--root"
76
+ || argument === "--cwd"
77
+ || argument === "--target"
78
+ || argument === "--targets") {
79
+ const value = rest[index + 1];
80
+ if (value === undefined || value.startsWith("-")) {
81
+ throw new CanonfigError("HARNESS_OPTION_VALUE_REQUIRED", `${argument} requires a value.`);
82
+ }
83
+ index += 1;
84
+ if (argument === "--root" || argument === "--cwd")
85
+ root = path.resolve(value);
86
+ else
87
+ requestedTargets.push(value);
88
+ continue;
89
+ }
90
+ throw new CanonfigError("HARNESS_OPTION_UNKNOWN", `Unknown harness option: ${argument}`);
91
+ }
92
+ const targets = requestedTargets.length === 0
93
+ ? undefined
94
+ : parseTargetList(requestedTargets.join(","));
95
+ return {
96
+ command,
97
+ root: path.resolve(root),
98
+ json,
99
+ strict,
100
+ force,
101
+ all,
102
+ dryRun,
103
+ ...(targets === undefined ? {} : { targets }),
104
+ };
105
+ };
@@ -0,0 +1,77 @@
1
+ import { Schema } from "effect";
2
+ import { CliExitCode } from "../cli/exit-codes.js";
3
+ import { renderCliResult } from "../cli/render.js";
4
+ import { CanonfigError } from "./core/errors.js";
5
+ const actionSymbol = (entry) => {
6
+ switch (entry.action) {
7
+ case "create": return "+";
8
+ case "update": return "~";
9
+ case "delete": return "-";
10
+ case "conflict": return "!";
11
+ case "unchanged": return "=";
12
+ }
13
+ };
14
+ export const renderHumanPlan = (plan, includeUnchanged) => {
15
+ const lines = [];
16
+ for (const entry of plan.entries) {
17
+ if (!includeUnchanged && entry.action === "unchanged")
18
+ continue;
19
+ lines.push(`${actionSymbol(entry)} ${entry.action.padEnd(9)} ${entry.path}`);
20
+ if (entry.reason !== undefined)
21
+ lines.push(` ${entry.reason}`);
22
+ }
23
+ for (const diagnostic of plan.diagnostics) {
24
+ const target = diagnostic.target === undefined ? "" : `[${diagnostic.target}] `;
25
+ const location = diagnostic.path === undefined ? "" : ` (${diagnostic.path})`;
26
+ lines.push(`${diagnostic.level.toUpperCase()} ${target}${diagnostic.code}: ${diagnostic.message}${location}`);
27
+ }
28
+ const counts = ["create", "update", "delete", "unchanged", "conflict"]
29
+ .map((action) => `${plan.entries.filter((entry) => entry.action === action).length} ${action}`)
30
+ .join(", ");
31
+ lines.push(counts);
32
+ return `${lines.join("\n")}\n`;
33
+ };
34
+ export const toCliPayload = (value) => Schema.decodeUnknownSync(Schema.MutableJson)(JSON.parse(JSON.stringify(value)));
35
+ export const planPayload = (plan) => toCliPayload({
36
+ root: plan.root,
37
+ targets: plan.targets,
38
+ entries: plan.entries.map(({ content: _content, before: _before, after: _after, nextState: _nextState, ...entry }) => entry),
39
+ diagnostics: plan.diagnostics,
40
+ });
41
+ export const diagnosticsPayload = (diagnostics) => toCliPayload(diagnostics);
42
+ export const isHarnessPlanBlocked = (plan) => plan.entries.some((entry) => entry.action === "conflict")
43
+ || plan.diagnostics.some((diagnostic) => diagnostic.level === "error");
44
+ export const renderHarnessResult = (io, input) => {
45
+ const rendered = input.json
46
+ ? renderCliResult({
47
+ command: input.command,
48
+ message: input.message,
49
+ data: input.data,
50
+ exitCode: input.exitCode,
51
+ }, "json")
52
+ : (input.human ?? renderCliResult({
53
+ command: input.command,
54
+ message: input.message,
55
+ data: input.data,
56
+ exitCode: input.exitCode,
57
+ }, "human"));
58
+ if (input.exitCode === CliExitCode.success)
59
+ io.writeStdout(rendered);
60
+ else
61
+ io.writeStderr(rendered);
62
+ io.setExitCode(input.exitCode);
63
+ };
64
+ export const harnessFailureExitCode = (error) => {
65
+ if (!(error instanceof CanonfigError))
66
+ return CliExitCode.internal;
67
+ if (/CONFLICT|COLLISION|EDITED|ESCAPE|STALE/u.test(error.code)) {
68
+ return CliExitCode.conflictOrDrift;
69
+ }
70
+ if (/APPLY|WRITE|ROLLBACK/u.test(error.code)) {
71
+ return CliExitCode.verificationOrApplyFailure;
72
+ }
73
+ if (/INVALID|NOT_FOUND|UNKNOWN|REQUIRED|EMPTY|PARSE/u.test(error.code)) {
74
+ return CliExitCode.usageOrConfiguration;
75
+ }
76
+ return CliExitCode.internal;
77
+ };
@@ -0,0 +1,196 @@
1
+ import { CliExitCode } from "../cli/exit-codes.js";
2
+ import { harnessHelpText, parseHarnessArguments, } from "./cli-arguments.js";
3
+ import { diagnosticsPayload, harnessFailureExitCode, isHarnessPlanBlocked, planPayload, renderHarnessResult, renderHumanPlan, toCliPayload, } from "./cli-output.js";
4
+ import { HarnessConfigurationCompiler, createDefaultRegistry, } from "./core/compiler.js";
5
+ import { findRepositoryRoot } from "./core/config.js";
6
+ import { formatPlanDiff } from "./core/diff.js";
7
+ import { doctorTargets } from "./core/doctor.js";
8
+ import { CanonfigError, errorMessage } from "./core/errors.js";
9
+ import { applyPlan, createPlan } from "./core/planner.js";
10
+ import { scaffoldProject } from "./core/scaffold.js";
11
+ import { TARGET_IDS } from "./core/types.js";
12
+ export const isHarnessConfigurationCommand = (arguments_) => arguments_[0] === "harness";
13
+ const renderHelp = (parsed, io) => {
14
+ renderHarnessResult(io, {
15
+ command: "harness.help",
16
+ message: "Harness configuration help",
17
+ data: {
18
+ commands: [
19
+ "init",
20
+ "validate",
21
+ "targets",
22
+ "plan",
23
+ "apply",
24
+ "status",
25
+ "diff",
26
+ "clean",
27
+ "doctor",
28
+ ],
29
+ },
30
+ exitCode: CliExitCode.success,
31
+ json: parsed.json,
32
+ human: harnessHelpText,
33
+ });
34
+ };
35
+ export const runHarnessConfigurationCli = async (arguments_, io) => {
36
+ let parsed;
37
+ try {
38
+ parsed = parseHarnessArguments(arguments_);
39
+ const registry = createDefaultRegistry();
40
+ const compiler = new HarnessConfigurationCompiler(registry);
41
+ const commandName = `harness.${parsed.command}`;
42
+ if (parsed.command === "help") {
43
+ renderHelp(parsed, io);
44
+ return;
45
+ }
46
+ if (parsed.command === "init") {
47
+ const written = await scaffoldProject(parsed.root, {
48
+ targets: parsed.targets,
49
+ force: parsed.force,
50
+ });
51
+ renderHarnessResult(io, {
52
+ command: commandName,
53
+ message: written.length === 0
54
+ ? "No harness source files changed"
55
+ : "Harness source initialized",
56
+ data: { root: parsed.root, written },
57
+ exitCode: CliExitCode.success,
58
+ json: parsed.json,
59
+ human: written.length === 0
60
+ ? "No files changed.\n"
61
+ : `${written.map((file) => `+ ${file}`).join("\n")}\n`,
62
+ });
63
+ return;
64
+ }
65
+ if (parsed.command === "targets") {
66
+ const descriptors = registry.list().map((adapter) => adapter.descriptor);
67
+ renderHarnessResult(io, {
68
+ command: commandName,
69
+ message: "Harness targets listed",
70
+ data: toCliPayload(descriptors),
71
+ exitCode: CliExitCode.success,
72
+ json: parsed.json,
73
+ human: `${descriptors.map((descriptor) => [
74
+ `${descriptor.id.padEnd(16)} ${descriptor.name}`,
75
+ ` ${Object.entries(descriptor.capabilities)
76
+ .map(([feature, level]) => `${feature}:${level}`)
77
+ .join(" ")}`,
78
+ ].join("\n")).join("\n")}\n`,
79
+ });
80
+ return;
81
+ }
82
+ if (parsed.command === "doctor") {
83
+ const results = doctorTargets(registry, parsed.targets);
84
+ renderHarnessResult(io, {
85
+ command: commandName,
86
+ message: "Harness probes completed",
87
+ data: toCliPayload(results),
88
+ exitCode: CliExitCode.success,
89
+ json: parsed.json,
90
+ human: `${results.map((result) => `${result.id.padEnd(16)} ${(result.found ? "found" : "missing").padEnd(8)} ${result.executable ?? result.error ?? ""}`).join("\n")}\n`,
91
+ });
92
+ return;
93
+ }
94
+ const root = await findRepositoryRoot(parsed.root);
95
+ if (parsed.command === "clean") {
96
+ const plan = await createPlan(root, [...TARGET_IDS], [], [], { force: parsed.force });
97
+ const exitCode = isHarnessPlanBlocked(plan)
98
+ ? CliExitCode.conflictOrDrift
99
+ : CliExitCode.success;
100
+ if (exitCode === CliExitCode.success && !parsed.dryRun)
101
+ await applyPlan(plan);
102
+ renderHarnessResult(io, {
103
+ command: commandName,
104
+ message: exitCode !== CliExitCode.success
105
+ ? "Harness cleanup blocked"
106
+ : parsed.dryRun
107
+ ? "Harness cleanup planned"
108
+ : "Harness-owned configuration cleaned",
109
+ data: planPayload(plan),
110
+ exitCode,
111
+ json: parsed.json,
112
+ human: renderHumanPlan(plan, parsed.all),
113
+ });
114
+ return;
115
+ }
116
+ const plan = await compiler.plan({
117
+ root,
118
+ targets: parsed.targets,
119
+ strict: parsed.strict,
120
+ force: parsed.force,
121
+ });
122
+ const exitCode = isHarnessPlanBlocked(plan)
123
+ ? CliExitCode.conflictOrDrift
124
+ : CliExitCode.success;
125
+ if (parsed.command === "validate") {
126
+ renderHarnessResult(io, {
127
+ command: commandName,
128
+ message: exitCode === CliExitCode.success
129
+ ? "Harness configuration is valid"
130
+ : "Harness configuration validation failed",
131
+ data: diagnosticsPayload(plan.diagnostics),
132
+ exitCode,
133
+ json: parsed.json,
134
+ human: exitCode === CliExitCode.success
135
+ ? "Harness configuration is valid.\n"
136
+ : renderHumanPlan(plan, false),
137
+ });
138
+ return;
139
+ }
140
+ if (parsed.command === "plan" || parsed.command === "status") {
141
+ renderHarnessResult(io, {
142
+ command: commandName,
143
+ message: parsed.command === "status"
144
+ ? (exitCode === CliExitCode.success
145
+ ? "Harness configuration status computed"
146
+ : "Harness configuration has conflicts")
147
+ : "Harness configuration plan computed",
148
+ data: planPayload(plan),
149
+ exitCode,
150
+ json: parsed.json,
151
+ human: renderHumanPlan(plan, parsed.all),
152
+ });
153
+ return;
154
+ }
155
+ if (parsed.command === "diff") {
156
+ const diff = formatPlanDiff(plan);
157
+ renderHarnessResult(io, {
158
+ command: commandName,
159
+ message: "Harness configuration diff computed",
160
+ data: planPayload(plan),
161
+ exitCode,
162
+ json: parsed.json,
163
+ human: diff === "" ? "No pending changes.\n" : `${diff}\n`,
164
+ });
165
+ return;
166
+ }
167
+ if (parsed.command === "apply" || parsed.command === "sync") {
168
+ if (exitCode === CliExitCode.success && !parsed.dryRun)
169
+ await applyPlan(plan);
170
+ renderHarnessResult(io, {
171
+ command: commandName,
172
+ message: exitCode !== CliExitCode.success
173
+ ? "Harness configuration apply blocked"
174
+ : parsed.dryRun
175
+ ? "Harness configuration apply planned"
176
+ : "Harness configuration applied",
177
+ data: planPayload(plan),
178
+ exitCode,
179
+ json: parsed.json,
180
+ human: renderHumanPlan(plan, parsed.all),
181
+ });
182
+ return;
183
+ }
184
+ throw new CanonfigError("HARNESS_COMMAND_UNKNOWN", `Unknown harness command: ${parsed.command}`);
185
+ }
186
+ catch (error) {
187
+ const exitCode = harnessFailureExitCode(error);
188
+ renderHarnessResult(io, {
189
+ command: `harness.${parsed?.command ?? "unknown"}`,
190
+ message: errorMessage(error),
191
+ data: error instanceof CanonfigError ? { code: error.code } : undefined,
192
+ exitCode,
193
+ json: parsed?.json ?? arguments_.includes("--json"),
194
+ });
195
+ }
196
+ };