@vaubaan/harness 1.3.0 → 1.4.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.
- package/NOTICE +1 -1
- package/README.md +229 -112
- package/dist/agents/coding.agent.md +1 -2
- package/dist/agents/merge.agent.md +2 -2
- package/dist/agents/planning.agent.md +1 -1
- package/dist/agents/review.agent.md +1 -1
- package/dist/cli/index.js +1438 -1287
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +804 -667
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +29 -6
- package/dist/init.js +1145 -470
- package/dist/init.js.map +1 -1
- package/dist/linters/index.js +3 -2
- package/dist/linters/index.js.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +41 -8
- package/dist/render.js.map +1 -1
- package/dist/skills/spec-writer.skill.md +1 -1
- package/dist/skills/test-plan-writer.skill.md +1 -1
- package/package.json +9 -4
- package/dist/{render-B9_kNtQc.d.ts → render-Cu2XrkkY.d.ts} +3 -3
package/dist/init.js
CHANGED
|
@@ -1,183 +1,217 @@
|
|
|
1
1
|
// src/cli/init.ts
|
|
2
|
-
import { existsSync as
|
|
3
|
-
import { dirname as
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
// src/loader.ts
|
|
7
|
-
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
8
|
-
import { join } from "path";
|
|
9
|
-
import fg from "fast-glob";
|
|
10
|
-
import { z as z2 } from "zod";
|
|
11
|
-
|
|
12
|
-
// src/parser/common.ts
|
|
13
|
-
import { readFileSync } from "fs";
|
|
14
|
-
import matter from "gray-matter";
|
|
15
|
-
|
|
16
|
-
// src/parser/schemas.ts
|
|
17
|
-
import { z } from "zod";
|
|
18
|
-
|
|
19
|
-
// src/types.ts
|
|
20
|
-
var AGENT_IDS = ["coding", "review", "merge", "planning"];
|
|
21
|
-
var ENGINES = ["claude-code", "codex-cli", "opencode"];
|
|
22
|
-
var CAPABILITIES = ["multi-step", "slash-command", "inline-exec"];
|
|
23
|
-
var CAPABILITIES_MATRIX = {
|
|
24
|
-
"claude-code": ["multi-step", "slash-command", "inline-exec"],
|
|
25
|
-
"codex-cli": ["multi-step", "slash-command", "inline-exec"],
|
|
26
|
-
opencode: ["multi-step", "slash-command", "inline-exec"]
|
|
27
|
-
};
|
|
28
|
-
var DEGRADED_CAPABILITIES = {
|
|
29
|
-
"codex-cli": ["multi-step"]
|
|
30
|
-
};
|
|
2
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
3
|
+
import { dirname as dirname5, join as join11, resolve as resolve6 } from "path";
|
|
31
4
|
|
|
32
|
-
// src/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Skill: SkillFrontmatterSchema,
|
|
66
|
-
Cmd: CmdFrontmatterSchema,
|
|
67
|
-
Agent: AgentFrontmatterSchema
|
|
68
|
-
};
|
|
69
|
-
var ParseError = class extends Error {
|
|
70
|
-
constructor(file, issues) {
|
|
71
|
-
const messages = issues.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`).join("\n");
|
|
72
|
-
super(`Frontmatter validation failed for ${file}:
|
|
73
|
-
${messages}`);
|
|
74
|
-
this.file = file;
|
|
75
|
-
this.issues = issues;
|
|
76
|
-
this.name = "ParseError";
|
|
5
|
+
// src/cli/managed-paths.ts
|
|
6
|
+
import { lstatSync, realpathSync, statSync } from "fs";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, resolve, win32 } from "path";
|
|
8
|
+
var MANAGED_FILES = /* @__PURE__ */ new Set([
|
|
9
|
+
"AGENTS.md",
|
|
10
|
+
"CLAUDE.md",
|
|
11
|
+
".mcp.json",
|
|
12
|
+
".harness-manifest.json",
|
|
13
|
+
".claude/settings.json",
|
|
14
|
+
".codex/config.toml",
|
|
15
|
+
".codex/hooks.json",
|
|
16
|
+
".opencode/opencode.json",
|
|
17
|
+
".opencode/mcp.json"
|
|
18
|
+
]);
|
|
19
|
+
var MANAGED_DIRECTORIES = [
|
|
20
|
+
".claude/agents/",
|
|
21
|
+
".claude/commands/",
|
|
22
|
+
".claude/skills/",
|
|
23
|
+
".codex/agents/",
|
|
24
|
+
".codex/commands/",
|
|
25
|
+
".codex/skills/",
|
|
26
|
+
".agents/skills/",
|
|
27
|
+
".opencode/agents/",
|
|
28
|
+
".opencode/commands/",
|
|
29
|
+
".opencode/skills/",
|
|
30
|
+
".opencode/plugins/"
|
|
31
|
+
];
|
|
32
|
+
function canonicalTargetRoot(root) {
|
|
33
|
+
let ancestor = resolve(root);
|
|
34
|
+
const missing = [];
|
|
35
|
+
while (!lstatSync(ancestor, { throwIfNoEntry: false })) {
|
|
36
|
+
missing.unshift(basename(ancestor));
|
|
37
|
+
ancestor = dirname(ancestor);
|
|
77
38
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// src/parser/common.ts
|
|
83
|
-
function parseWithSchema(filePath, source, schema) {
|
|
84
|
-
const parsed = matter(source);
|
|
85
|
-
const result = schema.safeParse(parsed.data);
|
|
86
|
-
if (!result.success) {
|
|
87
|
-
throw new ParseError(filePath, result.error.issues);
|
|
39
|
+
const canonical = realpathSync(ancestor);
|
|
40
|
+
if (!statSync(canonical).isDirectory()) {
|
|
41
|
+
throw new Error(`harness target is not a directory: ${root}`);
|
|
88
42
|
}
|
|
89
|
-
return
|
|
90
|
-
meta: result.data,
|
|
91
|
-
body: parsed.content.trim(),
|
|
92
|
-
sourcePath: filePath
|
|
93
|
-
};
|
|
43
|
+
return join(canonical, ...missing);
|
|
94
44
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
45
|
+
function assertManagedLocation(path) {
|
|
46
|
+
const invalidSegment = path.split("/").some((part) => part === "" || part === "." || part === "..");
|
|
47
|
+
if (isAbsolute(path) || win32.isAbsolute(path) || path.includes("\\") || path.includes("\0") || invalidSegment) {
|
|
48
|
+
throw new Error(`unsafe harness path: ${path}`);
|
|
49
|
+
}
|
|
50
|
+
if (!MANAGED_FILES.has(path) && !MANAGED_DIRECTORIES.some((prefix) => path.startsWith(prefix))) {
|
|
51
|
+
throw new Error(`path is outside harness-managed locations: ${path}`);
|
|
52
|
+
}
|
|
99
53
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
54
|
+
function assertNoSymbolicComponents(root, path) {
|
|
55
|
+
const parts = path.split("/");
|
|
56
|
+
let current = root;
|
|
57
|
+
for (const [index, part] of parts.entries()) {
|
|
58
|
+
current = join(current, part);
|
|
59
|
+
const info = lstatSync(current, { throwIfNoEntry: false });
|
|
60
|
+
if (!info) return;
|
|
61
|
+
if (info.isSymbolicLink()) {
|
|
62
|
+
throw new Error(`symbolic link in harness path: ${path}`);
|
|
63
|
+
}
|
|
64
|
+
const isLeaf = index === parts.length - 1;
|
|
65
|
+
if (isLeaf ? !info.isFile() : !info.isDirectory()) {
|
|
66
|
+
throw new Error(`unexpected file type in harness path: ${path}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
104
69
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
70
|
+
function assertManagedPaths(root, files) {
|
|
71
|
+
for (const path of files) assertManagedLocation(path);
|
|
72
|
+
const canonicalRoot2 = canonicalTargetRoot(root);
|
|
73
|
+
for (const path of files) assertNoSymbolicComponents(canonicalRoot2, path);
|
|
109
74
|
}
|
|
110
75
|
|
|
111
|
-
// src/
|
|
112
|
-
import {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
76
|
+
// src/cli/manifest.ts
|
|
77
|
+
import { createHash } from "crypto";
|
|
78
|
+
import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
79
|
+
import { join as join4 } from "path";
|
|
80
|
+
import { z as z3 } from "zod";
|
|
116
81
|
|
|
117
|
-
// src/
|
|
118
|
-
var
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
82
|
+
// src/version.ts
|
|
83
|
+
var VERSION = "1.4.0";
|
|
84
|
+
|
|
85
|
+
// src/cli/opencode-config.ts
|
|
86
|
+
import { existsSync, readFileSync } from "fs";
|
|
87
|
+
import { join as join2 } from "path";
|
|
88
|
+
import { isDeepStrictEqual } from "util";
|
|
89
|
+
import { z } from "zod";
|
|
90
|
+
var OPENCODE_CONFIG_PATH = ".opencode/opencode.json";
|
|
91
|
+
var CLAUDE_MCP_PATH = ".mcp.json";
|
|
92
|
+
var OPENCODE_SCHEMA = "https://opencode.ai/config.json";
|
|
93
|
+
var ObjectSchema = z.record(z.unknown());
|
|
94
|
+
var OPENCODE_TARGET = {
|
|
95
|
+
path: OPENCODE_CONFIG_PATH,
|
|
96
|
+
key: "mcp",
|
|
97
|
+
label: "OpenCode",
|
|
98
|
+
schema: OPENCODE_SCHEMA
|
|
99
|
+
};
|
|
100
|
+
var CLAUDE_TARGET = {
|
|
101
|
+
path: CLAUDE_MCP_PATH,
|
|
102
|
+
key: "mcpServers",
|
|
103
|
+
label: "Claude Code"
|
|
104
|
+
};
|
|
105
|
+
var OpenCodeConfigOwnershipSchema = z.object({
|
|
106
|
+
createdFile: z.boolean(),
|
|
107
|
+
addedMcp: z.boolean(),
|
|
108
|
+
addedSchema: z.boolean(),
|
|
109
|
+
entries: z.record(ObjectSchema)
|
|
110
|
+
});
|
|
111
|
+
function parseConfig(target, content) {
|
|
112
|
+
try {
|
|
113
|
+
const config = ObjectSchema.parse(JSON.parse(content));
|
|
114
|
+
if (Object.hasOwn(config, target.key)) ObjectSchema.parse(config[target.key]);
|
|
115
|
+
return config;
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error(`invalid ${target.path}: expected a JSON object with an MCP object`);
|
|
118
|
+
}
|
|
131
119
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
120
|
+
function removeOwnedEntries(target, config, ownership, warnings) {
|
|
121
|
+
if (Object.hasOwn(config, target.key)) {
|
|
122
|
+
const mcp = ObjectSchema.parse(config[target.key]);
|
|
123
|
+
for (const [id, installed] of Object.entries(ownership.entries)) {
|
|
124
|
+
if (!Object.hasOwn(mcp, id)) continue;
|
|
125
|
+
if (isDeepStrictEqual(mcp[id], installed)) delete mcp[id];
|
|
126
|
+
else warnings.push(`preserved modified ${target.label} MCP server: ${id}`);
|
|
127
|
+
}
|
|
128
|
+
config[target.key] = mcp;
|
|
129
|
+
if (ownership.addedMcp && Object.keys(mcp).length === 0)
|
|
130
|
+
Reflect.deleteProperty(config, target.key);
|
|
131
|
+
}
|
|
132
|
+
if (target.schema && ownership.addedSchema && config.$schema === target.schema)
|
|
133
|
+
Reflect.deleteProperty(config, "$schema");
|
|
137
134
|
}
|
|
138
|
-
function
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (existsSync(disablePath)) {
|
|
149
|
-
try {
|
|
150
|
-
const parsed = DisableYamlSchema.safeParse(parseYAML(readFileSync2(disablePath, "utf8")));
|
|
151
|
-
if (parsed.success && parsed.data.disable) {
|
|
152
|
-
disable = parsed.data.disable;
|
|
135
|
+
function addMissingEntries(target, config, desired, createdFile, warnings) {
|
|
136
|
+
const requested = ObjectSchema.parse(parseConfig(target, desired.content)[target.key]);
|
|
137
|
+
const mcp = ObjectSchema.parse(config[target.key] ?? {});
|
|
138
|
+
const entries = {};
|
|
139
|
+
for (const [id, entry] of Object.entries(requested)) {
|
|
140
|
+
if (Object.hasOwn(mcp, id)) {
|
|
141
|
+
if (!isDeepStrictEqual(mcp[id], entry)) {
|
|
142
|
+
warnings.push(
|
|
143
|
+
`preserved existing ${target.label} MCP server instead of replacing it: ${id}`
|
|
144
|
+
);
|
|
153
145
|
}
|
|
154
|
-
|
|
146
|
+
continue;
|
|
155
147
|
}
|
|
148
|
+
entries[id] = ObjectSchema.parse(entry);
|
|
149
|
+
mcp[id] = entry;
|
|
156
150
|
}
|
|
157
|
-
|
|
151
|
+
if (Object.keys(entries).length === 0) return void 0;
|
|
152
|
+
const ownership = {
|
|
153
|
+
createdFile,
|
|
154
|
+
addedMcp: !Object.hasOwn(config, target.key),
|
|
155
|
+
addedSchema: target.schema !== void 0 && !Object.hasOwn(config, "$schema"),
|
|
156
|
+
entries
|
|
157
|
+
};
|
|
158
|
+
config[target.key] = mcp;
|
|
159
|
+
if (target.schema && ownership.addedSchema) config.$schema = target.schema;
|
|
160
|
+
return ownership;
|
|
158
161
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
function planMcpJson(target, root, desired, previous) {
|
|
163
|
+
const warnings = [];
|
|
164
|
+
if (!desired && !previous) return { remove: false, warnings };
|
|
165
|
+
const path = join2(root, target.path);
|
|
166
|
+
const existed = existsSync(path);
|
|
167
|
+
if (!existed && !desired) return { remove: false, warnings };
|
|
168
|
+
const original = existed ? readFileSync(path, "utf8") : void 0;
|
|
169
|
+
const config = original === void 0 ? {} : parseConfig(target, original);
|
|
170
|
+
const before = structuredClone(config);
|
|
171
|
+
if (previous) removeOwnedEntries(target, config, previous, warnings);
|
|
172
|
+
const createdFile = !existed || previous?.createdFile === true;
|
|
173
|
+
const ownership = desired ? addMissingEntries(target, config, desired, createdFile, warnings) : void 0;
|
|
174
|
+
if (!ownership && createdFile && Object.keys(config).length === 0) {
|
|
175
|
+
return { remove: existed, warnings };
|
|
168
176
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
return {
|
|
178
|
+
remove: false,
|
|
179
|
+
ownership,
|
|
180
|
+
warnings,
|
|
181
|
+
...!existed || !isDeepStrictEqual(before, config) ? { write: { path: target.path, content: `${JSON.stringify(config, null, 2)}
|
|
182
|
+
` } } : {}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function planOpenCodeConfig(root, desired, previous) {
|
|
186
|
+
return planMcpJson(OPENCODE_TARGET, root, desired, previous);
|
|
187
|
+
}
|
|
188
|
+
function planClaudeMcpConfig(root, desired, previous) {
|
|
189
|
+
return planMcpJson(CLAUDE_TARGET, root, desired, previous);
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
// src/cli/update-config.ts
|
|
193
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
194
|
+
import { join as join3 } from "path";
|
|
195
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
196
|
+
import { z as z2 } from "zod";
|
|
197
|
+
|
|
179
198
|
// src/renderers/common.ts
|
|
180
199
|
import { stringify as yamlStringify } from "yaml";
|
|
200
|
+
|
|
201
|
+
// src/types.ts
|
|
202
|
+
var AGENT_IDS = ["coding", "review", "merge", "planning"];
|
|
203
|
+
var ENGINES = ["claude-code", "codex-cli", "opencode"];
|
|
204
|
+
var CAPABILITIES = ["multi-step", "slash-command", "inline-exec"];
|
|
205
|
+
var CAPABILITIES_MATRIX = {
|
|
206
|
+
"claude-code": ["multi-step", "slash-command", "inline-exec"],
|
|
207
|
+
"codex-cli": ["multi-step", "slash-command", "inline-exec"],
|
|
208
|
+
opencode: ["multi-step", "slash-command", "inline-exec"]
|
|
209
|
+
};
|
|
210
|
+
var DEGRADED_CAPABILITIES = {
|
|
211
|
+
"codex-cli": ["multi-step"]
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/renderers/common.ts
|
|
181
215
|
var STOP_HOOK_PACKAGE = "@vaubaan/harness";
|
|
182
216
|
function checkSkillCapabilities(skill, engine) {
|
|
183
217
|
const blockingWarnings = [];
|
|
@@ -543,18 +577,26 @@ ${body}` });
|
|
|
543
577
|
renderStopHook() {
|
|
544
578
|
const content = `import { spawnSync } from "node:child_process";
|
|
545
579
|
|
|
546
|
-
|
|
580
|
+
// Runs the harness lint and returns the report to inject, or "" when the
|
|
581
|
+
// turn may end.
|
|
582
|
+
const lintReport = (directory, sessionID) => {
|
|
583
|
+
const r = spawnSync(
|
|
584
|
+
"npx",
|
|
585
|
+
["-y", "${STOP_HOOK_PACKAGE}", "stop-hook", "--engine", "opencode", "--cwd", directory],
|
|
586
|
+
{ input: JSON.stringify({ sessionID }), encoding: "utf-8" },
|
|
587
|
+
);
|
|
588
|
+
const report = (r.stderr ?? "").trim();
|
|
589
|
+
return r.status === 2 ? report : "";
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// OpenCode 1.x entry point, exposed as server() below.
|
|
593
|
+
const HarnessStopHook = async ({ client, directory }) => ({
|
|
547
594
|
event: async ({ event }) => {
|
|
548
595
|
if (event.type !== "session.idle") return;
|
|
549
596
|
const sessionID = event.properties?.sessionID;
|
|
550
597
|
if (!sessionID) return;
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
["-y", "${STOP_HOOK_PACKAGE}", "stop-hook", "--engine", "opencode", "--cwd", directory],
|
|
554
|
-
{ input: JSON.stringify({ sessionID }), encoding: "utf-8" },
|
|
555
|
-
);
|
|
556
|
-
const report = (r.stderr ?? "").trim();
|
|
557
|
-
if (r.status !== 2 || report === "") return;
|
|
598
|
+
const report = lintReport(directory, sessionID);
|
|
599
|
+
if (report === "") return;
|
|
558
600
|
// OpenCode defaults to its build agent when a prompt omits agent.
|
|
559
601
|
// Preserve the last conversation persona, excluding compaction summaries.
|
|
560
602
|
const history = await client.session.messages({ path: { id: sessionID } }).catch(() => undefined);
|
|
@@ -570,6 +612,31 @@ export const HarnessStopHook = async ({ client, directory }) => ({
|
|
|
570
612
|
});
|
|
571
613
|
},
|
|
572
614
|
});
|
|
615
|
+
|
|
616
|
+
// One default export for both majors: OpenCode 1.x calls server(),
|
|
617
|
+
// OpenCode 2.x calls setup(). A V2 session keeps its agent, so the report
|
|
618
|
+
// is sent without naming the persona.
|
|
619
|
+
export default {
|
|
620
|
+
id: "vaubaan-harness-stop-hook",
|
|
621
|
+
server: HarnessStopHook,
|
|
622
|
+
setup(context) {
|
|
623
|
+
const directory = context.location.directory;
|
|
624
|
+
const controller = new AbortController();
|
|
625
|
+
const listen = async () => {
|
|
626
|
+
for await (const event of context.event.subscribe({ signal: controller.signal })) {
|
|
627
|
+
if (event.type !== "session.idle") continue;
|
|
628
|
+
if (event.location?.directory && event.location.directory !== directory) continue;
|
|
629
|
+
const sessionID = event.data?.sessionID;
|
|
630
|
+
if (!sessionID) continue;
|
|
631
|
+
const report = lintReport(directory, sessionID);
|
|
632
|
+
if (report === "") continue;
|
|
633
|
+
await context.session.prompt({ sessionID, text: report }).catch(() => undefined);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
listen().catch(() => undefined);
|
|
637
|
+
return () => controller.abort();
|
|
638
|
+
},
|
|
639
|
+
};
|
|
573
640
|
`;
|
|
574
641
|
return {
|
|
575
642
|
files: [{ path: `${BASE3}/plugins/harness-stop-hook.ts`, content }],
|
|
@@ -588,7 +655,451 @@ function getRenderer(engine) {
|
|
|
588
655
|
return RENDERERS[engine];
|
|
589
656
|
}
|
|
590
657
|
|
|
591
|
-
// src/
|
|
658
|
+
// src/cli/update-config.ts
|
|
659
|
+
var LEGACY_OPENCODE_CONFIG = ".opencode/mcp.json";
|
|
660
|
+
var SHARED_CONFIG_PATHS = [
|
|
661
|
+
".mcp.json",
|
|
662
|
+
".codex/config.toml",
|
|
663
|
+
OPENCODE_CONFIG_PATH,
|
|
664
|
+
LEGACY_OPENCODE_CONFIG,
|
|
665
|
+
".claude/settings.json",
|
|
666
|
+
".codex/hooks.json"
|
|
667
|
+
];
|
|
668
|
+
var ObjectSchema2 = z2.record(z2.unknown());
|
|
669
|
+
var HookEntrySchema = z2.object({ group: ObjectSchema2, hook: ObjectSchema2 });
|
|
670
|
+
var HookConfigOwnershipSchema = z2.object({
|
|
671
|
+
createdFile: z2.boolean(),
|
|
672
|
+
addedHooks: z2.boolean(),
|
|
673
|
+
addedStop: z2.boolean(),
|
|
674
|
+
entries: z2.array(HookEntrySchema)
|
|
675
|
+
});
|
|
676
|
+
var HookConfigsOwnershipSchema = z2.object({
|
|
677
|
+
".claude/settings.json": HookConfigOwnershipSchema.optional(),
|
|
678
|
+
".codex/hooks.json": HookConfigOwnershipSchema.optional()
|
|
679
|
+
}).strict();
|
|
680
|
+
var HOOK_ENGINES = {
|
|
681
|
+
".claude/settings.json": "claude-code",
|
|
682
|
+
".codex/hooks.json": "codex-cli"
|
|
683
|
+
};
|
|
684
|
+
function parseObject(content, path) {
|
|
685
|
+
try {
|
|
686
|
+
return ObjectSchema2.parse(JSON.parse(content));
|
|
687
|
+
} catch {
|
|
688
|
+
throw new Error(`invalid shared configuration ${path}: expected a JSON object`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function asObject(value) {
|
|
692
|
+
const parsed = ObjectSchema2.safeParse(value);
|
|
693
|
+
return parsed.success ? parsed.data : void 0;
|
|
694
|
+
}
|
|
695
|
+
function stopHooks(config, path) {
|
|
696
|
+
if (!Object.hasOwn(config, "hooks")) return [];
|
|
697
|
+
const hooks = asObject(config.hooks);
|
|
698
|
+
if (!hooks || Object.hasOwn(hooks, "Stop") && !Array.isArray(hooks.Stop)) {
|
|
699
|
+
throw new Error(`invalid shared configuration ${path}: expected hooks.Stop to be an array`);
|
|
700
|
+
}
|
|
701
|
+
return Array.isArray(hooks.Stop) ? hooks.Stop : [];
|
|
702
|
+
}
|
|
703
|
+
function groupMetadata(group) {
|
|
704
|
+
return Object.fromEntries(Object.entries(group).filter(([key]) => key !== "hooks"));
|
|
705
|
+
}
|
|
706
|
+
function sameEntry(left, right) {
|
|
707
|
+
return isDeepStrictEqual2(left.group, right.group) && isDeepStrictEqual2(left.hook, right.hook);
|
|
708
|
+
}
|
|
709
|
+
function appendEntry(entries, entry) {
|
|
710
|
+
if (!entries.some((existing) => sameEntry(existing, entry))) entries.push(entry);
|
|
711
|
+
}
|
|
712
|
+
function defaultStopGroup(engine) {
|
|
713
|
+
const file = getRenderer(engine).renderStopHook().files[0];
|
|
714
|
+
if (!file) throw new Error(`missing Stop hook renderer for ${engine}`);
|
|
715
|
+
const group = asObject(stopHooks(parseObject(file.content, file.path), file.path)[0]);
|
|
716
|
+
if (!group || !Array.isArray(group.hooks) || !asObject(group.hooks[0])) {
|
|
717
|
+
throw new Error(`invalid Stop hook renderer for ${engine}`);
|
|
718
|
+
}
|
|
719
|
+
return group;
|
|
720
|
+
}
|
|
721
|
+
function normalizeHarnessHooks(groups, expected, previous, claimExisting, warnings, path) {
|
|
722
|
+
const defaultHook = ObjectSchema2.parse(expected.hooks[0]);
|
|
723
|
+
const entries = [...previous?.entries ?? []];
|
|
724
|
+
let found = false;
|
|
725
|
+
const normalized = groups.map((value) => {
|
|
726
|
+
const group = asObject(value);
|
|
727
|
+
if (!group) return value;
|
|
728
|
+
const flat = group.type === "command" && group.command === defaultHook.command;
|
|
729
|
+
const hooks = flat ? [group] : Array.isArray(group.hooks) ? group.hooks : [];
|
|
730
|
+
const metadata = flat ? groupMetadata(expected) : groupMetadata(group);
|
|
731
|
+
for (const value2 of hooks) {
|
|
732
|
+
const hook = asObject(value2);
|
|
733
|
+
if (!hook || hook.type !== "command" || hook.command !== defaultHook.command) continue;
|
|
734
|
+
found = true;
|
|
735
|
+
const entry = { group: metadata, hook };
|
|
736
|
+
if (!claimExisting) continue;
|
|
737
|
+
if (previous && !previous.entries.some((old) => sameEntry(old, entry))) {
|
|
738
|
+
warnings.push(`preserved modified harness Stop hook: ${path}`);
|
|
739
|
+
} else {
|
|
740
|
+
appendEntry(entries, entry);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return flat && claimExisting ? { ...metadata, hooks: [group] } : value;
|
|
744
|
+
});
|
|
745
|
+
if (!found && !previous?.entries.length) {
|
|
746
|
+
normalized.push(expected);
|
|
747
|
+
appendEntry(entries, { group: groupMetadata(expected), hook: defaultHook });
|
|
748
|
+
} else if (!found) {
|
|
749
|
+
warnings.push(`preserved removed or modified harness Stop hook: ${path}`);
|
|
750
|
+
}
|
|
751
|
+
return { groups: normalized, entries };
|
|
752
|
+
}
|
|
753
|
+
function removeOwnedHooks(config, ownership, path, warnings) {
|
|
754
|
+
const groups = stopHooks(config, path);
|
|
755
|
+
const remaining = [];
|
|
756
|
+
const matched = /* @__PURE__ */ new Set();
|
|
757
|
+
for (const value of groups) {
|
|
758
|
+
const group = asObject(value);
|
|
759
|
+
if (!group || !Array.isArray(group.hooks)) {
|
|
760
|
+
remaining.push(value);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const metadata = groupMetadata(group);
|
|
764
|
+
const hooks2 = group.hooks.filter((value2) => {
|
|
765
|
+
const hook = asObject(value2);
|
|
766
|
+
const entry = hook && ownership.entries.find((old) => sameEntry(old, { group: metadata, hook }));
|
|
767
|
+
if (entry) matched.add(entry);
|
|
768
|
+
return !entry;
|
|
769
|
+
});
|
|
770
|
+
if (hooks2.length > 0 || hooks2.length === group.hooks.length) {
|
|
771
|
+
remaining.push({ ...group, hooks: hooks2 });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (matched.size < ownership.entries.length) {
|
|
775
|
+
warnings.push(`preserved removed or modified harness Stop hook: ${path}`);
|
|
776
|
+
}
|
|
777
|
+
const hooks = asObject(config.hooks);
|
|
778
|
+
if (!hooks) return;
|
|
779
|
+
if (ownership.addedStop && remaining.length === 0) Reflect.deleteProperty(hooks, "Stop");
|
|
780
|
+
else if (Object.hasOwn(hooks, "Stop")) hooks.Stop = remaining;
|
|
781
|
+
if (ownership.addedHooks && Object.keys(hooks).length === 0)
|
|
782
|
+
Reflect.deleteProperty(config, "hooks");
|
|
783
|
+
else config.hooks = hooks;
|
|
784
|
+
}
|
|
785
|
+
function recordMutation(plan, path, before, after, createdFile) {
|
|
786
|
+
if (createdFile && Object.keys(after).length === 0) {
|
|
787
|
+
if (before) plan.removals.push(path);
|
|
788
|
+
} else if (!isDeepStrictEqual2(before, after)) {
|
|
789
|
+
plan.writes.push({ path, content: `${JSON.stringify(after, null, 2)}
|
|
790
|
+
` });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function planHookConfigCleanup(root, ownership) {
|
|
794
|
+
const plan = { writes: [], removals: [], warnings: [] };
|
|
795
|
+
if (!ownership) return plan;
|
|
796
|
+
const validated = HookConfigsOwnershipSchema.parse(ownership);
|
|
797
|
+
assertManagedPaths(root, Object.keys(validated));
|
|
798
|
+
for (const path of Object.keys(HOOK_ENGINES)) {
|
|
799
|
+
const owned = validated[path];
|
|
800
|
+
if (!owned || !existsSync2(join3(root, path))) continue;
|
|
801
|
+
const before = parseObject(readFileSync2(join3(root, path), "utf8"), path);
|
|
802
|
+
const config = structuredClone(before);
|
|
803
|
+
removeOwnedHooks(config, owned, path, plan.warnings);
|
|
804
|
+
recordMutation(plan, path, before, config, owned.createdFile);
|
|
805
|
+
}
|
|
806
|
+
return plan;
|
|
807
|
+
}
|
|
808
|
+
function planActiveHook(root, path, previous, claimExisting, plan) {
|
|
809
|
+
assertManagedPaths(root, [path]);
|
|
810
|
+
const existed = existsSync2(join3(root, path));
|
|
811
|
+
if (!existed && previous?.entries.length) {
|
|
812
|
+
plan.warnings.push(`preserved removed harness Stop hook configuration: ${path}`);
|
|
813
|
+
return previous;
|
|
814
|
+
}
|
|
815
|
+
const before = existed ? parseObject(readFileSync2(join3(root, path), "utf8"), path) : void 0;
|
|
816
|
+
const config = structuredClone(before ?? {});
|
|
817
|
+
const groups = stopHooks(config, path);
|
|
818
|
+
const hooks = asObject(config.hooks) ?? {};
|
|
819
|
+
const normalized = normalizeHarnessHooks(
|
|
820
|
+
groups,
|
|
821
|
+
defaultStopGroup(HOOK_ENGINES[path]),
|
|
822
|
+
previous,
|
|
823
|
+
claimExisting,
|
|
824
|
+
plan.warnings,
|
|
825
|
+
path
|
|
826
|
+
);
|
|
827
|
+
const legacyWholeFile = !previous && claimExisting && existed;
|
|
828
|
+
const ownership = {
|
|
829
|
+
createdFile: !existed || previous?.createdFile === true || legacyWholeFile,
|
|
830
|
+
addedHooks: previous?.addedHooks ?? (legacyWholeFile || !Object.hasOwn(config, "hooks")),
|
|
831
|
+
addedStop: previous?.addedStop ?? (legacyWholeFile || !Object.hasOwn(hooks, "Stop")),
|
|
832
|
+
entries: normalized.entries
|
|
833
|
+
};
|
|
834
|
+
if (!isDeepStrictEqual2(groups, normalized.groups)) {
|
|
835
|
+
config.hooks = { ...hooks, Stop: normalized.groups };
|
|
836
|
+
}
|
|
837
|
+
recordMutation(plan, path, before, config, ownership.createdFile);
|
|
838
|
+
return ownership.entries.length > 0 ? ownership : void 0;
|
|
839
|
+
}
|
|
840
|
+
function planLegacyOpenCode(root, previous, plan) {
|
|
841
|
+
if (!previous.files.includes(LEGACY_OPENCODE_CONFIG)) return;
|
|
842
|
+
const legacyPath = join3(root, LEGACY_OPENCODE_CONFIG);
|
|
843
|
+
if (!existsSync2(legacyPath)) return;
|
|
844
|
+
assertManagedPaths(root, [LEGACY_OPENCODE_CONFIG, OPENCODE_CONFIG_PATH]);
|
|
845
|
+
const legacy = parseObject(readFileSync2(legacyPath, "utf8"), LEGACY_OPENCODE_CONFIG);
|
|
846
|
+
if (!asObject(legacy.mcp)) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`invalid shared configuration ${LEGACY_OPENCODE_CONFIG}: expected an MCP object`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const merged = planOpenCodeConfig(
|
|
852
|
+
root,
|
|
853
|
+
{ path: OPENCODE_CONFIG_PATH, content: JSON.stringify(legacy) },
|
|
854
|
+
void 0
|
|
855
|
+
);
|
|
856
|
+
if (merged.write) plan.writes.push(merged.write);
|
|
857
|
+
plan.warnings.push(...merged.warnings);
|
|
858
|
+
if (merged.ownership) {
|
|
859
|
+
const old = previous.opencodeConfig;
|
|
860
|
+
plan.opencodeConfig = {
|
|
861
|
+
createdFile: merged.ownership.createdFile || old?.createdFile === true,
|
|
862
|
+
addedMcp: merged.ownership.addedMcp || old?.addedMcp === true,
|
|
863
|
+
addedSchema: merged.ownership.addedSchema || old?.addedSchema === true,
|
|
864
|
+
entries: { ...old?.entries, ...merged.ownership.entries }
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
plan.removals.push(LEGACY_OPENCODE_CONFIG);
|
|
868
|
+
}
|
|
869
|
+
function planUpdateConfigs(root, previous, hookEngines) {
|
|
870
|
+
const plan = {
|
|
871
|
+
writes: [],
|
|
872
|
+
removals: [],
|
|
873
|
+
ownedFiles: [],
|
|
874
|
+
warnings: [],
|
|
875
|
+
...previous.opencodeConfig ? { opencodeConfig: previous.opencodeConfig } : {}
|
|
876
|
+
};
|
|
877
|
+
planLegacyOpenCode(root, previous, plan);
|
|
878
|
+
const hookConfigs = {};
|
|
879
|
+
for (const path of Object.keys(HOOK_ENGINES)) {
|
|
880
|
+
const owned = previous.hookConfigs?.[path];
|
|
881
|
+
if (hookEngines.includes(HOOK_ENGINES[path])) {
|
|
882
|
+
hookConfigs[path] = planActiveHook(
|
|
883
|
+
root,
|
|
884
|
+
path,
|
|
885
|
+
owned,
|
|
886
|
+
previous.files.includes(path) || Boolean(owned),
|
|
887
|
+
plan
|
|
888
|
+
);
|
|
889
|
+
} else if (owned) {
|
|
890
|
+
const cleanup = planHookConfigCleanup(root, { [path]: owned });
|
|
891
|
+
plan.writes.push(...cleanup.writes);
|
|
892
|
+
plan.removals.push(...cleanup.removals);
|
|
893
|
+
plan.warnings.push(...cleanup.warnings);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (Object.values(hookConfigs).some(Boolean)) plan.hookConfigs = hookConfigs;
|
|
897
|
+
if (hookEngines.includes("opencode")) {
|
|
898
|
+
const plugin = getRenderer("opencode").renderStopHook();
|
|
899
|
+
plan.writes.push(...plugin.files);
|
|
900
|
+
plan.ownedFiles.push(...plugin.files.map((file) => file.path));
|
|
901
|
+
plan.warnings.push(...plugin.warnings);
|
|
902
|
+
}
|
|
903
|
+
return plan;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/cli/manifest.ts
|
|
907
|
+
var MANIFEST_FILENAME = ".harness-manifest.json";
|
|
908
|
+
var MANIFEST_VERSION = "6";
|
|
909
|
+
var ManifestSchema = z3.object({
|
|
910
|
+
version: z3.enum(["4", "5", MANIFEST_VERSION]),
|
|
911
|
+
engines: z3.array(z3.string()),
|
|
912
|
+
renderedAt: z3.string(),
|
|
913
|
+
files: z3.array(z3.string()),
|
|
914
|
+
stopHookInstalled: z3.boolean().optional(),
|
|
915
|
+
stopHookEngines: z3.array(z3.string()).optional(),
|
|
916
|
+
opencodeConfig: OpenCodeConfigOwnershipSchema.optional(),
|
|
917
|
+
/** Entries the harness added to `.mcp.json` (Claude Code). */
|
|
918
|
+
mcpConfig: OpenCodeConfigOwnershipSchema.optional(),
|
|
919
|
+
hookConfigs: HookConfigsOwnershipSchema.optional(),
|
|
920
|
+
harnessVersion: z3.string().optional(),
|
|
921
|
+
overridesDir: z3.string().optional(),
|
|
922
|
+
fileHashes: z3.record(z3.string().regex(/^[a-f0-9]{64}$/)).optional(),
|
|
923
|
+
preservedFiles: z3.array(z3.string()).optional()
|
|
924
|
+
});
|
|
925
|
+
function manifestPath(targetDir) {
|
|
926
|
+
return join4(targetDir, MANIFEST_FILENAME);
|
|
927
|
+
}
|
|
928
|
+
function hashContent(content) {
|
|
929
|
+
return createHash("sha256").update(content).digest("hex");
|
|
930
|
+
}
|
|
931
|
+
function readManifest(targetDir) {
|
|
932
|
+
assertManagedPaths(targetDir, [MANIFEST_FILENAME]);
|
|
933
|
+
const path = manifestPath(targetDir);
|
|
934
|
+
if (!existsSync3(path)) return null;
|
|
935
|
+
const raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
936
|
+
return ManifestSchema.parse(raw);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/cli/plan-render.ts
|
|
940
|
+
import { existsSync as existsSync5 } from "fs";
|
|
941
|
+
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
942
|
+
import { fileURLToPath } from "url";
|
|
943
|
+
|
|
944
|
+
// src/loader.ts
|
|
945
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
946
|
+
import { join as join5 } from "path";
|
|
947
|
+
import fg from "fast-glob";
|
|
948
|
+
import { z as z5 } from "zod";
|
|
949
|
+
|
|
950
|
+
// src/parser/common.ts
|
|
951
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
952
|
+
import matter from "gray-matter";
|
|
953
|
+
|
|
954
|
+
// src/parser/schemas.ts
|
|
955
|
+
import { z as z4 } from "zod";
|
|
956
|
+
var IdSchema = z4.string().min(1).regex(/^[a-z][a-z0-9-]*$/, "id must be kebab-case (lowercase + digits + hyphens)");
|
|
957
|
+
var SkillFrontmatterSchema = z4.object({
|
|
958
|
+
id: IdSchema,
|
|
959
|
+
description: z4.string().min(1),
|
|
960
|
+
engines: z4.array(z4.enum(ENGINES)).min(1).optional(),
|
|
961
|
+
requires: z4.array(z4.enum(CAPABILITIES)).optional(),
|
|
962
|
+
tags: z4.array(z4.string()).optional()
|
|
963
|
+
}).strict();
|
|
964
|
+
var CmdArgEnumSchema = z4.object({
|
|
965
|
+
name: z4.string().min(1),
|
|
966
|
+
type: z4.literal("enum"),
|
|
967
|
+
values: z4.array(z4.string()).min(1),
|
|
968
|
+
default: z4.string().optional()
|
|
969
|
+
}).strict();
|
|
970
|
+
var CmdArgStringSchema = z4.object({
|
|
971
|
+
name: z4.string().min(1),
|
|
972
|
+
type: z4.literal("string"),
|
|
973
|
+
default: z4.string().optional()
|
|
974
|
+
}).strict();
|
|
975
|
+
var CmdArgSchema = z4.discriminatedUnion("type", [CmdArgEnumSchema, CmdArgStringSchema]);
|
|
976
|
+
var CmdFrontmatterSchema = z4.object({
|
|
977
|
+
id: IdSchema,
|
|
978
|
+
trigger: z4.string().regex(/^\/[a-z][a-z0-9-]*$/, "trigger must be /kebab-case"),
|
|
979
|
+
invokes_skill: IdSchema,
|
|
980
|
+
args: z4.array(CmdArgSchema).optional(),
|
|
981
|
+
description: z4.string().min(1)
|
|
982
|
+
}).strict();
|
|
983
|
+
var AgentFrontmatterSchema = z4.object({
|
|
984
|
+
id: z4.enum(AGENT_IDS),
|
|
985
|
+
description: z4.string().min(1)
|
|
986
|
+
}).strict();
|
|
987
|
+
var Schemas = {
|
|
988
|
+
Skill: SkillFrontmatterSchema,
|
|
989
|
+
Cmd: CmdFrontmatterSchema,
|
|
990
|
+
Agent: AgentFrontmatterSchema
|
|
991
|
+
};
|
|
992
|
+
var ParseError = class extends Error {
|
|
993
|
+
constructor(file, issues) {
|
|
994
|
+
const messages = issues.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`).join("\n");
|
|
995
|
+
super(`Frontmatter validation failed for ${file}:
|
|
996
|
+
${messages}`);
|
|
997
|
+
this.file = file;
|
|
998
|
+
this.issues = issues;
|
|
999
|
+
this.name = "ParseError";
|
|
1000
|
+
}
|
|
1001
|
+
file;
|
|
1002
|
+
issues;
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// src/parser/common.ts
|
|
1006
|
+
function parseWithSchema(filePath, source, schema) {
|
|
1007
|
+
const parsed = matter(source);
|
|
1008
|
+
const result = schema.safeParse(parsed.data);
|
|
1009
|
+
if (!result.success) {
|
|
1010
|
+
throw new ParseError(filePath, result.error.issues);
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
meta: result.data,
|
|
1014
|
+
body: parsed.content.trim(),
|
|
1015
|
+
sourcePath: filePath
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/parser/agent.ts
|
|
1020
|
+
function parseAgent(filePath, source) {
|
|
1021
|
+
return parseWithSchema(filePath, source, Schemas.Agent);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/parser/cmd.ts
|
|
1025
|
+
function parseCmd(filePath, source) {
|
|
1026
|
+
return parseWithSchema(filePath, source, Schemas.Cmd);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/parser/skill.ts
|
|
1030
|
+
function parseSkill(filePath, source) {
|
|
1031
|
+
return parseWithSchema(filePath, source, Schemas.Skill);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/util/yaml.ts
|
|
1035
|
+
import { parse as yamlParse } from "yaml";
|
|
1036
|
+
function parseYAML(source) {
|
|
1037
|
+
return yamlParse(source);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/loader.ts
|
|
1041
|
+
var DisableYamlSchema = z5.object({ disable: z5.array(z5.string()).optional() }).passthrough();
|
|
1042
|
+
function loadHarness(packageDir, opts = {}) {
|
|
1043
|
+
const agentsDir = opts.agentsDir ?? join5(packageDir, "agents");
|
|
1044
|
+
const skillsDir = opts.skillsDir ?? join5(packageDir, "skills");
|
|
1045
|
+
const commandsDir = opts.commandsDir ?? join5(packageDir, "commands");
|
|
1046
|
+
const agentFiles = fg.sync("*.agent.md", { cwd: agentsDir, absolute: true });
|
|
1047
|
+
const skillFiles = fg.sync("*.skill.md", { cwd: skillsDir, absolute: true });
|
|
1048
|
+
const cmdFiles = fg.sync("*.cmd.md", { cwd: commandsDir, absolute: true });
|
|
1049
|
+
return {
|
|
1050
|
+
agents: agentFiles.map((f) => parseAgent(f, readFileSync5(f, "utf8"))),
|
|
1051
|
+
skills: skillFiles.map((f) => parseSkill(f, readFileSync5(f, "utf8"))),
|
|
1052
|
+
cmds: cmdFiles.map((f) => parseCmd(f, readFileSync5(f, "utf8")))
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function loadProjectSkills(targetDir) {
|
|
1056
|
+
const skillsDir = join5(targetDir, "docs", "skills");
|
|
1057
|
+
if (!existsSync4(skillsDir)) return [];
|
|
1058
|
+
const skillFiles = fg.sync("*.skill.md", { cwd: skillsDir, absolute: true });
|
|
1059
|
+
return skillFiles.map((f) => parseSkill(f, readFileSync5(f, "utf8")));
|
|
1060
|
+
}
|
|
1061
|
+
function loadOverrides(worktreeDir) {
|
|
1062
|
+
const overridesDir = join5(worktreeDir, ".harness-overrides");
|
|
1063
|
+
if (!existsSync4(overridesDir)) return void 0;
|
|
1064
|
+
const added = loadHarness(overridesDir, {
|
|
1065
|
+
agentsDir: join5(overridesDir, "agents"),
|
|
1066
|
+
skillsDir: join5(overridesDir, "skills"),
|
|
1067
|
+
commandsDir: join5(overridesDir, "commands")
|
|
1068
|
+
});
|
|
1069
|
+
const disablePath = join5(overridesDir, "disable.yaml");
|
|
1070
|
+
let disable = [];
|
|
1071
|
+
if (existsSync4(disablePath)) {
|
|
1072
|
+
try {
|
|
1073
|
+
const parsed = DisableYamlSchema.safeParse(parseYAML(readFileSync5(disablePath, "utf8")));
|
|
1074
|
+
if (parsed.success && parsed.data.disable) {
|
|
1075
|
+
disable = parsed.data.disable;
|
|
1076
|
+
}
|
|
1077
|
+
} catch {
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return { add: added, disable };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/mcp.ts
|
|
1084
|
+
var MCP_SERVER_IDS = ["context7"];
|
|
1085
|
+
var MCP_SERVERS = {
|
|
1086
|
+
context7: {
|
|
1087
|
+
id: "context7",
|
|
1088
|
+
description: "Upstash Context7 \u2014 fetches up-to-date library docs via resolve-library-id + get-library-docs.",
|
|
1089
|
+
command: "npx",
|
|
1090
|
+
args: ["-y", "@upstash/context7-mcp"]
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
function resolveMcpIds(requested) {
|
|
1094
|
+
return requested.map((id) => {
|
|
1095
|
+
if (!MCP_SERVER_IDS.includes(id)) {
|
|
1096
|
+
throw new Error(`unknown MCP server: ${id} (expected one of ${MCP_SERVER_IDS.join(", ")})`);
|
|
1097
|
+
}
|
|
1098
|
+
return id;
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// src/render.ts
|
|
592
1103
|
var RenderError = class extends Error {
|
|
593
1104
|
constructor(message) {
|
|
594
1105
|
super(message);
|
|
@@ -688,320 +1199,470 @@ function render(input) {
|
|
|
688
1199
|
return { files, warnings };
|
|
689
1200
|
}
|
|
690
1201
|
|
|
691
|
-
// src/cli/
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1202
|
+
// src/cli/plan-render.ts
|
|
1203
|
+
var QUALITY_DOC_PATH = "docs/quality/code-quality.md";
|
|
1204
|
+
function resolveEngineList(requested) {
|
|
1205
|
+
if (!requested) return [...ENGINES];
|
|
1206
|
+
if (requested.length === 0) throw new Error("no engine selected");
|
|
1207
|
+
const engines = requested.map((e) => {
|
|
1208
|
+
if (!ENGINES.includes(e)) {
|
|
1209
|
+
throw new Error(`unknown engine: ${e} (expected one of ${ENGINES.join(", ")})`);
|
|
1210
|
+
}
|
|
1211
|
+
return e;
|
|
1212
|
+
});
|
|
1213
|
+
return [...new Set(engines)];
|
|
1214
|
+
}
|
|
1215
|
+
function defaultPackageDir() {
|
|
1216
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
1217
|
+
if (existsSync5(join6(here, "agents"))) return here;
|
|
1218
|
+
return resolve2(here, "..");
|
|
1219
|
+
}
|
|
1220
|
+
function planInit(args) {
|
|
1221
|
+
const engines = resolveEngineList(args.engines);
|
|
1222
|
+
const packageDir = args.packageDir ?? defaultPackageDir();
|
|
1223
|
+
const harness = loadHarness(packageDir);
|
|
1224
|
+
const overrides = args.overridesDir ? loadOverrides(args.overridesDir) : void 0;
|
|
1225
|
+
const projectSkills = loadProjectSkills(args.targetDir);
|
|
1226
|
+
const hasQualityDoc = existsSync5(join6(args.targetDir, QUALITY_DOC_PATH));
|
|
1227
|
+
const mcpServers = args.mcpServers ? resolveMcpIds(args.mcpServers) : void 0;
|
|
1228
|
+
const stopHookInstalled = args.withStopHook !== false;
|
|
1229
|
+
const output = render({
|
|
1230
|
+
harness,
|
|
1231
|
+
engines,
|
|
1232
|
+
overrides,
|
|
1233
|
+
projectSkills,
|
|
1234
|
+
hasQualityDoc,
|
|
1235
|
+
mcpServers,
|
|
1236
|
+
withStopHook: stopHookInstalled
|
|
1237
|
+
});
|
|
1238
|
+
return { engines, output, stopHookInstalled };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/cli/uninstall.ts
|
|
1242
|
+
import {
|
|
1243
|
+
existsSync as existsSync6,
|
|
1244
|
+
readFileSync as readFileSync6,
|
|
1245
|
+
readdirSync,
|
|
1246
|
+
rmdirSync,
|
|
1247
|
+
unlinkSync,
|
|
1248
|
+
writeFileSync as writeFileSync2
|
|
1249
|
+
} from "fs";
|
|
1250
|
+
import { dirname as dirname3, join as join7, relative, resolve as resolve3 } from "path";
|
|
1251
|
+
function pruneEmptyDirs(root, leaves) {
|
|
1252
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1253
|
+
for (const leaf of leaves) {
|
|
1254
|
+
let cur = leaf;
|
|
1255
|
+
while (cur !== root && cur.startsWith(`${root}/`)) {
|
|
1256
|
+
candidates.add(cur);
|
|
1257
|
+
cur = dirname3(cur);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const pruned = [];
|
|
1261
|
+
const sorted = [...candidates].sort((a, b) => b.length - a.length);
|
|
1262
|
+
for (const dir of sorted) {
|
|
1263
|
+
if (!existsSync6(dir)) continue;
|
|
1264
|
+
if (readdirSync(dir).length > 0) continue;
|
|
1265
|
+
rmdirSync(dir);
|
|
1266
|
+
pruned.push(relative(root, dir));
|
|
1267
|
+
}
|
|
1268
|
+
return pruned;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/cli/update-transaction.ts
|
|
1272
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
1273
|
+
import {
|
|
1274
|
+
chmodSync as chmodSync2,
|
|
1275
|
+
closeSync,
|
|
1276
|
+
fsyncSync,
|
|
1277
|
+
lstatSync as lstatSync2,
|
|
1278
|
+
mkdirSync,
|
|
1279
|
+
openSync,
|
|
1280
|
+
readFileSync as readFileSync7,
|
|
1281
|
+
readdirSync as readdirSync2,
|
|
1282
|
+
realpathSync as realpathSync2,
|
|
1283
|
+
rmdirSync as rmdirSync2,
|
|
1284
|
+
unlinkSync as unlinkSync2,
|
|
1285
|
+
writeFileSync as writeFileSync3
|
|
1286
|
+
} from "fs";
|
|
1287
|
+
import { homedir } from "os";
|
|
1288
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve as resolve4, sep } from "path";
|
|
1289
|
+
var MANIFEST_PATH = ".harness-manifest.json";
|
|
1290
|
+
var PRIVATE_FILES = /* @__PURE__ */ new Set([
|
|
1291
|
+
MANIFEST_PATH,
|
|
697
1292
|
".mcp.json",
|
|
698
|
-
".harness-manifest.json",
|
|
699
|
-
".claude/settings.json",
|
|
700
1293
|
".codex/config.toml",
|
|
701
1294
|
".codex/hooks.json",
|
|
1295
|
+
".claude/settings.json",
|
|
702
1296
|
".opencode/opencode.json",
|
|
703
1297
|
".opencode/mcp.json"
|
|
704
1298
|
]);
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
".claude/commands/",
|
|
708
|
-
".claude/skills/",
|
|
709
|
-
".codex/agents/",
|
|
710
|
-
".codex/commands/",
|
|
711
|
-
".codex/skills/",
|
|
712
|
-
".agents/skills/",
|
|
713
|
-
".opencode/agents/",
|
|
714
|
-
".opencode/commands/",
|
|
715
|
-
".opencode/skills/",
|
|
716
|
-
".opencode/plugins/"
|
|
717
|
-
];
|
|
718
|
-
function canonicalTargetRoot(root) {
|
|
719
|
-
let ancestor = resolve(root);
|
|
1299
|
+
function canonicalRoot(root) {
|
|
1300
|
+
let ancestor = resolve4(root);
|
|
720
1301
|
const missing = [];
|
|
721
|
-
while (!
|
|
722
|
-
missing.unshift(
|
|
723
|
-
ancestor =
|
|
724
|
-
}
|
|
725
|
-
const canonical = realpathSync(ancestor);
|
|
726
|
-
if (!statSync(canonical).isDirectory()) {
|
|
727
|
-
throw new Error(`harness target is not a directory: ${root}`);
|
|
1302
|
+
while (!lstatSync2(ancestor, { throwIfNoEntry: false })) {
|
|
1303
|
+
missing.unshift(basename2(ancestor));
|
|
1304
|
+
ancestor = dirname4(ancestor);
|
|
728
1305
|
}
|
|
729
|
-
return
|
|
1306
|
+
return join8(realpathSync2(ancestor), ...missing);
|
|
730
1307
|
}
|
|
731
|
-
function
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
1308
|
+
function contains(parent, child) {
|
|
1309
|
+
const path = relative2(parent, child);
|
|
1310
|
+
return path === "" || !isAbsolute2(path) && path !== ".." && !path.startsWith(`..${sep}`);
|
|
1311
|
+
}
|
|
1312
|
+
function assertBackupLocation(root, backup) {
|
|
1313
|
+
if (contains(root, backup) || contains(backup, root)) {
|
|
1314
|
+
throw new Error(`update backup must be outside the target project: ${backup}`);
|
|
735
1315
|
}
|
|
736
|
-
|
|
737
|
-
|
|
1316
|
+
const ancestors = [];
|
|
1317
|
+
for (let path = backup; ; path = dirname4(path)) {
|
|
1318
|
+
ancestors.unshift(path);
|
|
1319
|
+
if (dirname4(path) === path) break;
|
|
738
1320
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
current = join2(current, part);
|
|
745
|
-
const info = lstatSync(current, { throwIfNoEntry: false });
|
|
746
|
-
if (!info) return;
|
|
747
|
-
if (info.isSymbolicLink()) {
|
|
748
|
-
throw new Error(`symbolic link in harness path: ${path}`);
|
|
749
|
-
}
|
|
750
|
-
const isLeaf = index === parts.length - 1;
|
|
751
|
-
if (isLeaf ? !info.isFile() : !info.isDirectory()) {
|
|
752
|
-
throw new Error(`unexpected file type in harness path: ${path}`);
|
|
1321
|
+
for (const path of ancestors) {
|
|
1322
|
+
const info = lstatSync2(path, { throwIfNoEntry: false });
|
|
1323
|
+
if (!info) continue;
|
|
1324
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
1325
|
+
throw new Error(`unsafe update backup directory: ${path}`);
|
|
753
1326
|
}
|
|
754
1327
|
}
|
|
1328
|
+
if (lstatSync2(backup, { throwIfNoEntry: false })) {
|
|
1329
|
+
throw new Error(`update backup directory already exists: ${backup}`);
|
|
1330
|
+
}
|
|
755
1331
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1332
|
+
function createDirectories(path, created, mode = 511) {
|
|
1333
|
+
const missing = [];
|
|
1334
|
+
for (let current = path; !lstatSync2(current, { throwIfNoEntry: false }); current = dirname4(current)) {
|
|
1335
|
+
missing.unshift(current);
|
|
1336
|
+
}
|
|
1337
|
+
for (const directory of missing) {
|
|
1338
|
+
mkdirSync(directory, { mode });
|
|
1339
|
+
created.push(directory);
|
|
1340
|
+
}
|
|
760
1341
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync } from "fs";
|
|
765
|
-
import { join as join5 } from "path";
|
|
766
|
-
import { z as z5 } from "zod";
|
|
767
|
-
|
|
768
|
-
// src/version.ts
|
|
769
|
-
var VERSION = "1.3.0";
|
|
770
|
-
|
|
771
|
-
// src/cli/opencode-config.ts
|
|
772
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
773
|
-
import { join as join3 } from "path";
|
|
774
|
-
import { isDeepStrictEqual } from "util";
|
|
775
|
-
import { z as z3 } from "zod";
|
|
776
|
-
var OPENCODE_CONFIG_PATH = ".opencode/opencode.json";
|
|
777
|
-
var OPENCODE_SCHEMA = "https://opencode.ai/config.json";
|
|
778
|
-
var ObjectSchema = z3.record(z3.unknown());
|
|
779
|
-
var OpenCodeConfigOwnershipSchema = z3.object({
|
|
780
|
-
createdFile: z3.boolean(),
|
|
781
|
-
addedMcp: z3.boolean(),
|
|
782
|
-
addedSchema: z3.boolean(),
|
|
783
|
-
entries: z3.record(ObjectSchema)
|
|
784
|
-
});
|
|
785
|
-
function parseConfig(content) {
|
|
1342
|
+
function writeBackupFile(path, content) {
|
|
1343
|
+
createDirectories(dirname4(path), [], 448);
|
|
1344
|
+
const fd = openSync(path, "wx", 384);
|
|
786
1345
|
try {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
throw new Error(`invalid ${OPENCODE_CONFIG_PATH}: expected a JSON object with an MCP object`);
|
|
1346
|
+
writeFileSync3(fd, content);
|
|
1347
|
+
fsyncSync(fd);
|
|
1348
|
+
} finally {
|
|
1349
|
+
closeSync(fd);
|
|
792
1350
|
}
|
|
793
1351
|
}
|
|
794
|
-
function
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1352
|
+
function saveSnapshot(root, paths, backup) {
|
|
1353
|
+
const entries = [];
|
|
1354
|
+
createDirectories(dirname4(backup), [], 448);
|
|
1355
|
+
mkdirSync(backup, { mode: 448 });
|
|
1356
|
+
for (const path of paths) {
|
|
1357
|
+
const full = join8(root, path);
|
|
1358
|
+
const info = lstatSync2(full, { throwIfNoEntry: false });
|
|
1359
|
+
if (!info) {
|
|
1360
|
+
entries.push({ path, exists: false });
|
|
1361
|
+
continue;
|
|
801
1362
|
}
|
|
802
|
-
|
|
803
|
-
|
|
1363
|
+
const content = readFileSync7(full);
|
|
1364
|
+
writeBackupFile(join8(backup, "files", path), content);
|
|
1365
|
+
entries.push({
|
|
1366
|
+
path,
|
|
1367
|
+
exists: true,
|
|
1368
|
+
mode: info.mode & 4095,
|
|
1369
|
+
sha256: createHash2("sha256").update(content).digest("hex")
|
|
1370
|
+
});
|
|
804
1371
|
}
|
|
805
|
-
|
|
806
|
-
|
|
1372
|
+
writeBackupFile(
|
|
1373
|
+
join8(backup, "snapshot.json"),
|
|
1374
|
+
`${JSON.stringify({ version: 1, root, createdAt: (/* @__PURE__ */ new Date()).toISOString(), entries }, null, 2)}
|
|
1375
|
+
`
|
|
1376
|
+
);
|
|
1377
|
+
return entries;
|
|
807
1378
|
}
|
|
808
|
-
function
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1379
|
+
function errorMessage(error) {
|
|
1380
|
+
return error instanceof Error ? error.message : String(error);
|
|
1381
|
+
}
|
|
1382
|
+
function rollback(root, backup, entries, touched, createdDirectories) {
|
|
1383
|
+
const errors = [];
|
|
1384
|
+
for (const path of [...touched].reverse()) {
|
|
1385
|
+
try {
|
|
1386
|
+
assertManagedPaths(root, [path]);
|
|
1387
|
+
const entry = entries.get(path);
|
|
1388
|
+
const full = join8(root, path);
|
|
1389
|
+
const current = lstatSync2(full, { throwIfNoEntry: false });
|
|
1390
|
+
if (!entry?.exists) {
|
|
1391
|
+
if (current) unlinkSync2(full);
|
|
1392
|
+
continue;
|
|
816
1393
|
}
|
|
817
|
-
|
|
1394
|
+
const original = readFileSync7(join8(backup, "files", path));
|
|
1395
|
+
if (!current || !readFileSync7(full).equals(original)) {
|
|
1396
|
+
if (current) unlinkSync2(full);
|
|
1397
|
+
writeFileSync3(full, original, { mode: 384 });
|
|
1398
|
+
}
|
|
1399
|
+
chmodSync2(full, entry.mode);
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
errors.push(`${path}: ${errorMessage(error)}`);
|
|
818
1402
|
}
|
|
819
|
-
entries[id] = ObjectSchema.parse(entry);
|
|
820
|
-
mcp[id] = entry;
|
|
821
1403
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (ownership.addedSchema) config.$schema = OPENCODE_SCHEMA;
|
|
831
|
-
return ownership;
|
|
1404
|
+
for (const directory of [...createdDirectories].reverse()) {
|
|
1405
|
+
try {
|
|
1406
|
+
if (readdirSync2(directory).length === 0) rmdirSync2(directory);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
errors.push(`${directory}: ${errorMessage(error)}`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return errors;
|
|
832
1412
|
}
|
|
833
|
-
function
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1413
|
+
function applyUpdateTransaction(root, writes, removals, backupDir) {
|
|
1414
|
+
const paths = [.../* @__PURE__ */ new Set([...removals, ...writes.map((file) => file.path)])];
|
|
1415
|
+
assertManagedPaths(root, paths);
|
|
1416
|
+
const target = canonicalRoot(root);
|
|
1417
|
+
const backup = resolve4(
|
|
1418
|
+
backupDir ?? join8(homedir(), ".local", "state", "vaubaan-harness", "backups", randomUUID())
|
|
1419
|
+
);
|
|
1420
|
+
assertBackupLocation(target, backup);
|
|
1421
|
+
let snapshot;
|
|
1422
|
+
try {
|
|
1423
|
+
snapshot = saveSnapshot(target, paths, backup);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
throw new Error(
|
|
1426
|
+
`could not prepare update backup at ${backup}; project unchanged: ${errorMessage(error)}`,
|
|
1427
|
+
{ cause: error }
|
|
1428
|
+
);
|
|
847
1429
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1430
|
+
const entries = new Map(snapshot.map((entry) => [entry.path, entry]));
|
|
1431
|
+
const touched = /* @__PURE__ */ new Set();
|
|
1432
|
+
const createdDirectories = [];
|
|
1433
|
+
try {
|
|
1434
|
+
assertManagedPaths(target, paths);
|
|
1435
|
+
for (const path of removals) {
|
|
1436
|
+
if (!lstatSync2(join8(target, path), { throwIfNoEntry: false })) continue;
|
|
1437
|
+
touched.add(path);
|
|
1438
|
+
unlinkSync2(join8(target, path));
|
|
1439
|
+
}
|
|
1440
|
+
for (const file of writes) {
|
|
1441
|
+
const full = join8(target, file.path);
|
|
1442
|
+
createDirectories(dirname4(full), createdDirectories);
|
|
1443
|
+
touched.add(file.path);
|
|
1444
|
+
const entry = entries.get(file.path);
|
|
1445
|
+
const mode = file.path === MANIFEST_PATH ? 384 : entry?.exists ? entry.mode : PRIVATE_FILES.has(file.path) ? 384 : 420;
|
|
1446
|
+
if (entry?.exists && file.path === MANIFEST_PATH) chmodSync2(full, mode);
|
|
1447
|
+
writeFileSync3(full, file.content, { mode });
|
|
1448
|
+
if (entry?.exists) chmodSync2(full, mode);
|
|
1449
|
+
}
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
const failures = rollback(target, backup, entries, touched, createdDirectories);
|
|
1452
|
+
const restoration = failures.length > 0 ? `rollback errors: ${failures.join("; ")}` : "rollback completed";
|
|
1453
|
+
throw new Error(`update failed; backup: ${backup}; ${restoration}: ${errorMessage(error)}`, {
|
|
1454
|
+
cause: error
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
return { backupDir: backup };
|
|
855
1458
|
}
|
|
856
1459
|
|
|
857
|
-
// src/cli/update
|
|
858
|
-
import { existsSync as
|
|
859
|
-
import { join as
|
|
860
|
-
import { isDeepStrictEqual as
|
|
861
|
-
import { z as z4 } from "zod";
|
|
862
|
-
var ObjectSchema2 = z4.record(z4.unknown());
|
|
863
|
-
var HookEntrySchema = z4.object({ group: ObjectSchema2, hook: ObjectSchema2 });
|
|
864
|
-
var HookConfigOwnershipSchema = z4.object({
|
|
865
|
-
createdFile: z4.boolean(),
|
|
866
|
-
addedHooks: z4.boolean(),
|
|
867
|
-
addedStop: z4.boolean(),
|
|
868
|
-
entries: z4.array(HookEntrySchema)
|
|
869
|
-
});
|
|
870
|
-
var HookConfigsOwnershipSchema = z4.object({
|
|
871
|
-
".claude/settings.json": HookConfigOwnershipSchema.optional(),
|
|
872
|
-
".codex/hooks.json": HookConfigOwnershipSchema.optional()
|
|
873
|
-
}).strict();
|
|
1460
|
+
// src/cli/update.ts
|
|
1461
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
1462
|
+
import { join as join10, resolve as resolve5 } from "path";
|
|
1463
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
|
|
874
1464
|
|
|
875
|
-
// src/cli/manifest.ts
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
hookConfigs: HookConfigsOwnershipSchema.optional(),
|
|
887
|
-
harnessVersion: z5.string().optional(),
|
|
888
|
-
overridesDir: z5.string().optional(),
|
|
889
|
-
fileHashes: z5.record(z5.string().regex(/^[a-f0-9]{64}$/)).optional(),
|
|
890
|
-
preservedFiles: z5.array(z5.string()).optional()
|
|
1465
|
+
// src/cli/update-manifest.ts
|
|
1466
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
1467
|
+
import { join as join9 } from "path";
|
|
1468
|
+
import { z as z6 } from "zod";
|
|
1469
|
+
var EngineSchema = z6.enum(ENGINES);
|
|
1470
|
+
var LegacySchema = z6.object({
|
|
1471
|
+
version: z6.enum(["1", "2", "3"]),
|
|
1472
|
+
engine: EngineSchema.optional(),
|
|
1473
|
+
engines: z6.array(EngineSchema).nonempty().optional(),
|
|
1474
|
+
renderedAt: z6.string(),
|
|
1475
|
+
files: z6.array(z6.string())
|
|
891
1476
|
});
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
function
|
|
896
|
-
|
|
897
|
-
const manifest = {
|
|
898
|
-
version: MANIFEST_VERSION,
|
|
899
|
-
harnessVersion: VERSION,
|
|
900
|
-
engines: [...engines].sort(),
|
|
901
|
-
renderedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
902
|
-
files: [...files].sort(),
|
|
903
|
-
fileHashes: Object.fromEntries(
|
|
904
|
-
files.filter((file) => existsSync4(join5(targetDir, file))).sort().map((file) => [file, hashContent(readFileSync5(join5(targetDir, file)))])
|
|
905
|
-
),
|
|
906
|
-
...extras.overridesDir ? { overridesDir: extras.overridesDir } : {},
|
|
907
|
-
...extras.preservedFiles ? { preservedFiles: extras.preservedFiles } : {},
|
|
908
|
-
...extras.hookConfigs ? { hookConfigs: extras.hookConfigs } : {},
|
|
909
|
-
...extras.opencodeConfig ? { opencodeConfig: extras.opencodeConfig } : {},
|
|
910
|
-
...extras.stopHookInstalled !== void 0 ? { stopHookInstalled: extras.stopHookInstalled } : {},
|
|
911
|
-
...extras.stopHookEngines !== void 0 ? { stopHookEngines: [...extras.stopHookEngines].sort() } : {}
|
|
912
|
-
};
|
|
913
|
-
const path = manifestPath(targetDir);
|
|
914
|
-
if (existsSync4(path)) chmodSync(path, 384);
|
|
915
|
-
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}
|
|
916
|
-
`, { mode: 384 });
|
|
917
|
-
}
|
|
918
|
-
function hashContent(content) {
|
|
919
|
-
return createHash("sha256").update(content).digest("hex");
|
|
1477
|
+
|
|
1478
|
+
// src/cli/update.ts
|
|
1479
|
+
var CODEX_CONFIG_PATH = ".codex/config.toml";
|
|
1480
|
+
function currentContent(root, path) {
|
|
1481
|
+
return existsSync8(join10(root, path)) ? readFileSync9(join10(root, path), "utf8") : void 0;
|
|
920
1482
|
}
|
|
921
|
-
function
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
return ManifestSchema.parse(raw);
|
|
1483
|
+
function ownedUnchanged(root, previous, path) {
|
|
1484
|
+
const content = currentContent(root, path);
|
|
1485
|
+
if (content === void 0 || !previous.files.includes(path)) return false;
|
|
1486
|
+
const hash = previous.fileHashes?.[path];
|
|
1487
|
+
return hash === void 0 || hash === hashContent(content);
|
|
927
1488
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const full = join6(root, rel);
|
|
939
|
-
if (existsSync5(full)) {
|
|
940
|
-
unlinkSync(full);
|
|
941
|
-
filesRemoved.push(rel);
|
|
942
|
-
dirsToConsider.add(dirname2(full));
|
|
943
|
-
} else {
|
|
944
|
-
missing.push(rel);
|
|
1489
|
+
function checkArtifactConflicts(root, previous, writes, removals) {
|
|
1490
|
+
const desired = new Map(writes.map((file) => [file.path, file.content]));
|
|
1491
|
+
const conflicts = [];
|
|
1492
|
+
for (const path of [...desired.keys(), ...removals]) {
|
|
1493
|
+
if (SHARED_CONFIG_PATHS.includes(path)) continue;
|
|
1494
|
+
const content = currentContent(root, path);
|
|
1495
|
+
if (content === void 0 || content === desired.get(path)) continue;
|
|
1496
|
+
if (!previous.files.includes(path)) {
|
|
1497
|
+
conflicts.push(`existing file outside harness manifest: ${path}`);
|
|
1498
|
+
continue;
|
|
945
1499
|
}
|
|
1500
|
+
const hash = previous.fileHashes?.[path];
|
|
1501
|
+
if (hash && hash !== hashContent(content)) conflicts.push(`modified harness file: ${path}`);
|
|
1502
|
+
}
|
|
1503
|
+
if (conflicts.length) {
|
|
1504
|
+
throw new Error(
|
|
1505
|
+
`${conflicts.join("\n")}
|
|
1506
|
+
move their content to docs/ or .harness-overrides/, or keep them as yours with --preserve <path>`
|
|
1507
|
+
);
|
|
946
1508
|
}
|
|
947
|
-
return { filesRemoved, dirsPruned: pruneEmptyDirs(root, dirsToConsider), missing };
|
|
948
1509
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1510
|
+
function legacyMcpOwnership(root, previous) {
|
|
1511
|
+
if (!ownedUnchanged(root, previous, CLAUDE_MCP_PATH)) return void 0;
|
|
1512
|
+
try {
|
|
1513
|
+
const servers = JSON.parse(currentContent(root, CLAUDE_MCP_PATH) ?? "").mcpServers;
|
|
1514
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return void 0;
|
|
1515
|
+
return { createdFile: true, addedMcp: true, addedSchema: false, entries: servers };
|
|
1516
|
+
} catch {
|
|
1517
|
+
return void 0;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function planMcp(root, settings, rendered, pendingWrites) {
|
|
1521
|
+
const { previous } = settings;
|
|
1522
|
+
const plan = {
|
|
1523
|
+
writes: [],
|
|
1524
|
+
removals: [],
|
|
1525
|
+
owned: [],
|
|
1526
|
+
plannedOpenCode: false,
|
|
1527
|
+
warnings: []
|
|
1528
|
+
};
|
|
1529
|
+
const claudeOwnership = previous.mcpConfig ?? legacyMcpOwnership(root, previous);
|
|
1530
|
+
const codexOwned = ownedUnchanged(root, previous, CODEX_CONFIG_PATH);
|
|
1531
|
+
if (settings.mcpServers === void 0) {
|
|
1532
|
+
if (codexOwned) plan.owned.push(CODEX_CONFIG_PATH);
|
|
1533
|
+
plan.mcpConfig = claudeOwnership;
|
|
1534
|
+
return plan;
|
|
1535
|
+
}
|
|
1536
|
+
const desired = (path) => rendered.find((file) => file.path === path);
|
|
1537
|
+
if (!pendingWrites.some((file) => file.path === OPENCODE_CONFIG_PATH)) {
|
|
1538
|
+
plan.plannedOpenCode = true;
|
|
1539
|
+
if (previous.files.includes(OPENCODE_CONFIG_PATH) && !previous.opencodeConfig) {
|
|
1540
|
+
plan.warnings.push(`preserved legacy shared configuration: ${OPENCODE_CONFIG_PATH}`);
|
|
956
1541
|
}
|
|
1542
|
+
const opencode = planOpenCodeConfig(
|
|
1543
|
+
root,
|
|
1544
|
+
desired(OPENCODE_CONFIG_PATH),
|
|
1545
|
+
previous.opencodeConfig
|
|
1546
|
+
);
|
|
1547
|
+
if (opencode.write) plan.writes.push(opencode.write);
|
|
1548
|
+
if (opencode.remove) plan.removals.push(OPENCODE_CONFIG_PATH);
|
|
1549
|
+
plan.opencodeConfig = opencode.ownership;
|
|
1550
|
+
plan.warnings.push(...opencode.warnings);
|
|
957
1551
|
}
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1552
|
+
const claude = planClaudeMcpConfig(root, desired(CLAUDE_MCP_PATH), claudeOwnership);
|
|
1553
|
+
if (claude.write) plan.writes.push(claude.write);
|
|
1554
|
+
if (claude.remove) plan.removals.push(CLAUDE_MCP_PATH);
|
|
1555
|
+
plan.mcpConfig = claude.ownership;
|
|
1556
|
+
plan.warnings.push(...claude.warnings);
|
|
1557
|
+
const codex = desired(CODEX_CONFIG_PATH);
|
|
1558
|
+
const codexExists = existsSync8(join10(root, CODEX_CONFIG_PATH));
|
|
1559
|
+
if (codex && (!codexExists || codexOwned)) {
|
|
1560
|
+
plan.writes.push(codex);
|
|
1561
|
+
plan.owned.push(CODEX_CONFIG_PATH);
|
|
1562
|
+
} else if (codex) {
|
|
1563
|
+
plan.warnings.push(
|
|
1564
|
+
`left ${CODEX_CONFIG_PATH} unchanged; add this block to it to enable MCP in Codex:
|
|
1565
|
+
${codex.content.trimEnd()}`
|
|
1566
|
+
);
|
|
1567
|
+
} else if (codexOwned) {
|
|
1568
|
+
plan.removals.push(CODEX_CONFIG_PATH);
|
|
965
1569
|
}
|
|
966
|
-
return
|
|
1570
|
+
return plan;
|
|
967
1571
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1572
|
+
function planInstallation(args, settings) {
|
|
1573
|
+
const root = resolve5(args.targetDir);
|
|
1574
|
+
const { previous, engines, hookEngines } = settings;
|
|
1575
|
+
const preserved = [
|
|
1576
|
+
.../* @__PURE__ */ new Set([...previous.preservedFiles ?? [], ...args.preserveFiles ?? []])
|
|
1577
|
+
].sort();
|
|
1578
|
+
if (preserved.includes(MANIFEST_FILENAME))
|
|
1579
|
+
throw new Error("cannot preserve the harness manifest");
|
|
1580
|
+
if (preserved.some((path) => SHARED_CONFIG_PATHS.includes(path))) {
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
"--preserve applies to generated artifacts; shared configurations use entry-level preservation"
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
assertManagedPaths(root, [...previous.files, ...preserved, ...SHARED_CONFIG_PATHS]);
|
|
1586
|
+
const requestedOverrides = args.overridesDir ?? previous.overridesDir;
|
|
1587
|
+
const overridesDir = args.overridesDir ? resolve5(args.overridesDir) : previous.overridesDir ? resolve5(root, previous.overridesDir) : root;
|
|
1588
|
+
if (requestedOverrides && !existsSync8(join10(overridesDir, ".harness-overrides"))) {
|
|
1589
|
+
throw new Error(`recorded overrides are missing: ${overridesDir}/.harness-overrides`);
|
|
1590
|
+
}
|
|
1591
|
+
const hasOverrides = existsSync8(join10(overridesDir, ".harness-overrides"));
|
|
1592
|
+
const disablePath = join10(overridesDir, ".harness-overrides", "disable.yaml");
|
|
1593
|
+
if (existsSync8(disablePath)) {
|
|
1594
|
+
try {
|
|
1595
|
+
DisableYamlSchema.parse(parseYAML(readFileSync9(disablePath, "utf8")));
|
|
1596
|
+
} catch {
|
|
1597
|
+
throw new Error(`invalid override selection: ${disablePath}`);
|
|
976
1598
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const here = dirname3(fileURLToPath(import.meta.url));
|
|
982
|
-
if (existsSync6(join7(here, "agents"))) return here;
|
|
983
|
-
return resolve3(here, "..");
|
|
984
|
-
}
|
|
985
|
-
function planInit(args) {
|
|
986
|
-
const engines = resolveEngineList(args.engines);
|
|
987
|
-
const packageDir = args.packageDir ?? defaultPackageDir();
|
|
988
|
-
const harness = loadHarness(packageDir);
|
|
989
|
-
const overrides = args.overridesDir ? loadOverrides(args.overridesDir) : void 0;
|
|
990
|
-
const projectSkills = loadProjectSkills(args.targetDir);
|
|
991
|
-
const hasQualityDoc = existsSync6(join7(args.targetDir, QUALITY_DOC_PATH));
|
|
992
|
-
const mcpServers = args.mcpServers ? resolveMcpIds(args.mcpServers) : void 0;
|
|
993
|
-
const stopHookInstalled = args.withStopHook !== false;
|
|
994
|
-
const output = render({
|
|
995
|
-
harness,
|
|
1599
|
+
}
|
|
1600
|
+
const rendered = planInit({
|
|
1601
|
+
targetDir: root,
|
|
1602
|
+
packageDir: args.packageDir,
|
|
996
1603
|
engines,
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
mcpServers,
|
|
1001
|
-
withStopHook: stopHookInstalled
|
|
1604
|
+
overridesDir: hasOverrides ? overridesDir : void 0,
|
|
1605
|
+
mcpServers: settings.mcpServers,
|
|
1606
|
+
withStopHook: false
|
|
1002
1607
|
});
|
|
1003
|
-
|
|
1608
|
+
const generated = rendered.output.files.filter(
|
|
1609
|
+
(file) => !SHARED_CONFIG_PATHS.includes(file.path)
|
|
1610
|
+
);
|
|
1611
|
+
const config = planUpdateConfigs(root, previous, hookEngines);
|
|
1612
|
+
const mcp = planMcp(root, settings, rendered.output.files, config.writes);
|
|
1613
|
+
const output = [...generated, ...config.writes, ...mcp.writes].filter(
|
|
1614
|
+
(file) => !preserved.includes(file.path)
|
|
1615
|
+
);
|
|
1616
|
+
const owned = [
|
|
1617
|
+
.../* @__PURE__ */ new Set([...generated.map((file) => file.path), ...config.ownedFiles, ...mcp.owned])
|
|
1618
|
+
].filter((file) => !preserved.includes(file)).sort();
|
|
1619
|
+
const removals = [
|
|
1620
|
+
.../* @__PURE__ */ new Set([
|
|
1621
|
+
...previous.files.filter(
|
|
1622
|
+
(file) => !owned.includes(file) && !SHARED_CONFIG_PATHS.includes(file)
|
|
1623
|
+
),
|
|
1624
|
+
...config.removals,
|
|
1625
|
+
...mcp.removals
|
|
1626
|
+
])
|
|
1627
|
+
].filter((file) => !preserved.includes(file));
|
|
1628
|
+
assertManagedPaths(root, [...output.map((file) => file.path), ...removals]);
|
|
1629
|
+
checkArtifactConflicts(root, previous, output, removals);
|
|
1630
|
+
const hashes = Object.fromEntries(
|
|
1631
|
+
owned.map((path) => {
|
|
1632
|
+
const content = output.find((file) => file.path === path)?.content ?? currentContent(root, path);
|
|
1633
|
+
if (content === void 0) throw new Error(`missing planned harness file: ${path}`);
|
|
1634
|
+
return [path, hashContent(content)];
|
|
1635
|
+
})
|
|
1636
|
+
);
|
|
1637
|
+
const opencodeConfig = mcp.plannedOpenCode ? mcp.opencodeConfig : config.opencodeConfig;
|
|
1638
|
+
const manifest = {
|
|
1639
|
+
version: MANIFEST_VERSION,
|
|
1640
|
+
harnessVersion: VERSION,
|
|
1641
|
+
engines: [...engines].sort(),
|
|
1642
|
+
renderedAt: previous.renderedAt,
|
|
1643
|
+
files: owned,
|
|
1644
|
+
fileHashes: hashes,
|
|
1645
|
+
stopHookInstalled: hookEngines.length > 0,
|
|
1646
|
+
stopHookEngines: [...hookEngines].sort(),
|
|
1647
|
+
...hasOverrides ? { overridesDir: overridesDir === root ? "." : overridesDir } : {},
|
|
1648
|
+
...preserved.length ? { preservedFiles: preserved } : {},
|
|
1649
|
+
...opencodeConfig ? { opencodeConfig } : {},
|
|
1650
|
+
...mcp.mcpConfig ? { mcpConfig: mcp.mcpConfig } : {},
|
|
1651
|
+
...config.hookConfigs ? { hookConfigs: config.hookConfigs } : {}
|
|
1652
|
+
};
|
|
1653
|
+
const writes = output.filter((file) => currentContent(root, file.path) !== file.content);
|
|
1654
|
+
const existingRemovals = removals.filter((path) => existsSync8(join10(root, path)));
|
|
1655
|
+
if (writes.length || existingRemovals.length || !isDeepStrictEqual3(previous, manifest)) {
|
|
1656
|
+
manifest.renderedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1657
|
+
writes.push({ path: MANIFEST_FILENAME, content: `${JSON.stringify(manifest, null, 2)}
|
|
1658
|
+
` });
|
|
1659
|
+
}
|
|
1660
|
+
const warnings = [...rendered.output.warnings, ...config.warnings, ...mcp.warnings];
|
|
1661
|
+
if (preserved.length) warnings.push(`preserved user-owned artifacts: ${preserved.join(", ")}`);
|
|
1662
|
+
return { writes, removals: existingRemovals, owned, manifest, warnings };
|
|
1004
1663
|
}
|
|
1664
|
+
|
|
1665
|
+
// src/cli/init.ts
|
|
1005
1666
|
function writeFilesToTarget(files, targetDir) {
|
|
1006
1667
|
assertManagedPaths(
|
|
1007
1668
|
targetDir,
|
|
@@ -1009,43 +1670,57 @@ function writeFilesToTarget(files, targetDir) {
|
|
|
1009
1670
|
);
|
|
1010
1671
|
const written = [];
|
|
1011
1672
|
for (const f of files) {
|
|
1012
|
-
const full =
|
|
1013
|
-
|
|
1014
|
-
|
|
1673
|
+
const full = join11(targetDir, f.path);
|
|
1674
|
+
mkdirSync2(dirname5(full), { recursive: true });
|
|
1675
|
+
writeFileSync4(full, f.content);
|
|
1015
1676
|
written.push(f.path);
|
|
1016
1677
|
}
|
|
1017
1678
|
return written;
|
|
1018
1679
|
}
|
|
1680
|
+
function emptyManifest(engines) {
|
|
1681
|
+
return { version: MANIFEST_VERSION, engines: [...engines], renderedAt: "", files: [] };
|
|
1682
|
+
}
|
|
1019
1683
|
function initCommand(args) {
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1684
|
+
const root = resolve6(args.targetDir);
|
|
1685
|
+
const engines = resolveEngineList(args.engines);
|
|
1686
|
+
const stopHookInstalled = args.withStopHook !== false;
|
|
1687
|
+
const previous = readManifest(root) ?? emptyManifest(engines);
|
|
1688
|
+
const plan = planInstallation(
|
|
1689
|
+
{
|
|
1690
|
+
targetDir: root,
|
|
1691
|
+
packageDir: args.packageDir,
|
|
1692
|
+
overridesDir: args.overridesDir,
|
|
1693
|
+
preserveFiles: args.preserveFiles
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
previous,
|
|
1697
|
+
sourceVersion: previous.version,
|
|
1698
|
+
engines,
|
|
1699
|
+
hookEngines: stopHookInstalled ? engines : [],
|
|
1700
|
+
mcpServers: args.mcpServers ?? []
|
|
1701
|
+
}
|
|
1702
|
+
);
|
|
1703
|
+
const touchedExisting = [...plan.writes.map((file) => file.path), ...plan.removals].some(
|
|
1704
|
+
(path) => path !== MANIFEST_FILENAME && existsSync9(join11(root, path))
|
|
1705
|
+
);
|
|
1706
|
+
let backupDir;
|
|
1707
|
+
if (plan.writes.length || plan.removals.length) {
|
|
1708
|
+
backupDir = applyUpdateTransaction(root, plan.writes, plan.removals, args.backupDir).backupDir;
|
|
1709
|
+
if (!touchedExisting && !args.backupDir) {
|
|
1710
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
1711
|
+
backupDir = void 0;
|
|
1712
|
+
}
|
|
1713
|
+
pruneEmptyDirs(root, new Set(plan.removals.map((path) => dirname5(join11(root, path)))));
|
|
1047
1714
|
}
|
|
1048
|
-
|
|
1715
|
+
const sharedWritten = plan.writes.map((file) => file.path).filter((path) => SHARED_CONFIG_PATHS.includes(path) && !plan.owned.includes(path));
|
|
1716
|
+
return {
|
|
1717
|
+
filesWritten: [...plan.owned, ...sharedWritten],
|
|
1718
|
+
staleFilesRemoved: plan.removals,
|
|
1719
|
+
warnings: plan.warnings,
|
|
1720
|
+
stopHookInstalled,
|
|
1721
|
+
engines,
|
|
1722
|
+
...backupDir ? { backupDir } : {}
|
|
1723
|
+
};
|
|
1049
1724
|
}
|
|
1050
1725
|
export {
|
|
1051
1726
|
initCommand,
|