agentwheel 0.19.10 → 0.20.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.
@@ -0,0 +1,2235 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ acquireApplyLock,
4
+ artifactFormatSchema,
5
+ artifactTypeSchema,
6
+ assertApplyJournalRecoveryAllowed,
7
+ assertGovernedRuntimeTransportSupported,
8
+ composedFromEntrySchema,
9
+ declareMutationPath,
10
+ defaultInstallationType,
11
+ fileKindSchema,
12
+ installManifestPath,
13
+ installationTypeSchema,
14
+ localPathExists,
15
+ localTransport,
16
+ metadataDir,
17
+ mutationMetadataForApplyJournal,
18
+ packageSupersedesEntrySchema,
19
+ readApplyJournal,
20
+ recordBackup,
21
+ removeApplyJournal,
22
+ rollbackCompletedOperations,
23
+ sourceLockPath,
24
+ writeApplyJournal
25
+ } from "./chunk-HOYIIUL5.js";
26
+ import {
27
+ pathExists,
28
+ writeJsonAtomic
29
+ } from "./chunk-7VPI5J5Y.js";
30
+
31
+ // src/install/apply.ts
32
+ import { execFile } from "child_process";
33
+ import { mkdtemp as mkdtemp2, readFile as readFile8, rm as rm2, writeFile as writeFile8 } from "fs/promises";
34
+ import { tmpdir as tmpdir2 } from "os";
35
+ import { basename as basename2, dirname as dirname8, join as join3 } from "path";
36
+ import { promisify } from "util";
37
+
38
+ // src/model/graph-lock.ts
39
+ import { createHash } from "crypto";
40
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
41
+ import { dirname } from "path";
42
+ import { z as z2 } from "zod";
43
+
44
+ // src/model/cache-identity.ts
45
+ import { z } from "zod";
46
+ var immutableCacheIdentitySchema = z.string().regex(
47
+ /^(?:[0-9a-f]{40}|content-[0-9a-f]{64})$/i,
48
+ "Expected a Git commit or content-addressed SHA-256 cache identity"
49
+ );
50
+ function normalizeImmutableCacheIdentity(value) {
51
+ return value === void 0 ? void 0 : immutableCacheIdentitySchema.parse(value).toLowerCase();
52
+ }
53
+
54
+ // src/model/graph-lock.ts
55
+ var CURRENT_GRAPH_LOCK_VERSION = 1;
56
+ var graphLockNodeSchema = z2.object({
57
+ id: z2.string().min(1),
58
+ name: z2.string().min(1),
59
+ version: z2.string().min(1),
60
+ source: z2.string().min(1),
61
+ normalizedSource: z2.string().min(1),
62
+ driver: z2.string().min(1),
63
+ requestedRef: z2.string().min(1).optional(),
64
+ resolvedCommit: z2.string().min(1).optional(),
65
+ cacheIdentity: immutableCacheIdentitySchema.optional(),
66
+ sourceHash: z2.string().min(16),
67
+ mode: z2.enum(["pinned", "tracking"]),
68
+ requiredBy: z2.array(z2.string().min(1)),
69
+ selected: z2.array(z2.string().min(1)),
70
+ selectionReasons: z2.record(z2.string(), z2.array(z2.string().min(1))).optional()
71
+ });
72
+ var graphLockRootSchema = z2.object({
73
+ rootId: z2.string().min(1),
74
+ source: z2.string().min(1),
75
+ normalizedSource: z2.string().min(1),
76
+ graphNodeId: z2.string().min(1),
77
+ mode: z2.enum(["pinned", "tracking"]),
78
+ selected: z2.array(z2.string().min(1)),
79
+ aliases: z2.record(z2.string(), z2.string().min(1)).optional(),
80
+ overrides: z2.array(z2.string().min(1)).optional(),
81
+ selectionImport: z2.object({
82
+ configPath: z2.string().min(1),
83
+ configHash: z2.string().min(16),
84
+ exportHash: z2.string().min(16),
85
+ exportName: z2.string().min(1),
86
+ extends: z2.array(z2.string().min(1)),
87
+ inherited: z2.array(z2.string().min(1)),
88
+ additions: z2.array(z2.string().min(1)),
89
+ exclusions: z2.array(z2.string().min(1)),
90
+ effective: z2.array(z2.string().min(1))
91
+ }).optional()
92
+ });
93
+ var graphLockEdgeSchema = z2.object({
94
+ from: z2.string().min(1),
95
+ to: z2.string().min(1),
96
+ alias: z2.string().min(1),
97
+ source: z2.string().min(1),
98
+ normalizedSource: z2.string().min(1),
99
+ requestedRef: z2.string().min(1).optional(),
100
+ version: z2.string().min(1).optional(),
101
+ mode: z2.enum(["pinned", "tracking"]),
102
+ optional: z2.boolean().default(false),
103
+ selected: z2.array(z2.string().min(1))
104
+ });
105
+ var graphLockIncludeEdgeSchema = z2.object({
106
+ fromNodeId: z2.string().min(1),
107
+ alias: z2.string().min(1),
108
+ toNodeId: z2.string().min(1),
109
+ selector: z2.string().min(1),
110
+ sourceHash: z2.string().min(16)
111
+ });
112
+ var graphLockArtifactSchema = z2.object({
113
+ graphNodeId: z2.string().min(1),
114
+ dependencyRole: z2.enum(["root", "direct", "transitive", "fragment"]),
115
+ type: artifactTypeSchema,
116
+ name: z2.string().min(1),
117
+ installName: z2.string().min(1),
118
+ logicalSelector: z2.string().min(1),
119
+ owners: z2.array(z2.string().min(1)),
120
+ relativePath: z2.string().min(1),
121
+ kind: fileKindSchema,
122
+ hash: z2.string().min(16),
123
+ channel: z2.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed"),
124
+ composedFrom: z2.array(composedFromEntrySchema).optional(),
125
+ supersedes: z2.array(packageSupersedesEntrySchema).optional()
126
+ });
127
+ var graphLockPlainNameIncumbentSchema = z2.object({
128
+ adapter: z2.string().min(1),
129
+ targetFingerprint: z2.string().min(1),
130
+ type: artifactTypeSchema,
131
+ name: z2.string().min(1),
132
+ graphNodeId: z2.string().min(1)
133
+ });
134
+ var graphLockNamespacingSchema = z2.object({
135
+ graphNodeId: z2.string().min(1),
136
+ type: artifactTypeSchema,
137
+ name: z2.string().min(1),
138
+ installName: z2.string().min(1),
139
+ reason: z2.enum(["alias", "transitive-collision"])
140
+ });
141
+ var graphLockOverrideSchema = z2.object({
142
+ rootId: z2.string().min(1),
143
+ selector: z2.string().min(1),
144
+ graphNodeId: z2.string().min(1),
145
+ overriddenGraphNodeId: z2.string().min(1),
146
+ type: artifactTypeSchema,
147
+ name: z2.string().min(1),
148
+ installName: z2.string().min(1)
149
+ });
150
+ var graphLockCanonicalSchema = z2.object({
151
+ targetFingerprint: z2.string().min(1).optional(),
152
+ roots: z2.array(graphLockRootSchema),
153
+ nodes: z2.array(graphLockNodeSchema),
154
+ edges: z2.array(graphLockEdgeSchema),
155
+ includeEdges: z2.array(graphLockIncludeEdgeSchema).default([]),
156
+ artifacts: z2.array(graphLockArtifactSchema).default([]),
157
+ namespacing: z2.array(graphLockNamespacingSchema).default([]),
158
+ overrides: z2.array(graphLockOverrideSchema).default([]),
159
+ plainNameIncumbents: z2.array(graphLockPlainNameIncumbentSchema).default([])
160
+ });
161
+ var graphLockSchema = z2.object({
162
+ version: z2.literal(CURRENT_GRAPH_LOCK_VERSION),
163
+ generatedAt: z2.string().datetime().optional(),
164
+ canonical: graphLockCanonicalSchema
165
+ });
166
+ async function readGraphLock(path) {
167
+ return canonicalizeGraphLock(graphLockSchema.parse(JSON.parse(await readFile(path, "utf8"))));
168
+ }
169
+ async function writeGraphLock(path, lock) {
170
+ declareMutationPath(path);
171
+ await mkdir(dirname(path), { recursive: true });
172
+ const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
173
+ await writeFile(temp, stringifyGraphLock(lock), "utf8");
174
+ await rename(temp, path);
175
+ }
176
+ function stringifyGraphLock(lock) {
177
+ return `${stableStringify(canonicalizeGraphLock(lock))}
178
+ `;
179
+ }
180
+ function canonicalGraphLockJson(lock) {
181
+ return `${stableStringify(canonicalizeGraphLock(lock).canonical)}
182
+ `;
183
+ }
184
+ function canonicalizeGraphLock(lock) {
185
+ const parsed = graphLockSchema.parse(lock);
186
+ return {
187
+ version: CURRENT_GRAPH_LOCK_VERSION,
188
+ canonical: {
189
+ targetFingerprint: parsed.canonical.targetFingerprint,
190
+ roots: [...parsed.canonical.roots].map((root) => ({
191
+ ...root,
192
+ selected: sortedUnique(root.selected),
193
+ overrides: root.overrides ? sortedUnique(root.overrides) : void 0,
194
+ selectionImport: root.selectionImport ? {
195
+ ...root.selectionImport,
196
+ inherited: sortedUnique(root.selectionImport.inherited),
197
+ additions: sortedUnique(root.selectionImport.additions),
198
+ exclusions: sortedUnique(root.selectionImport.exclusions),
199
+ effective: sortedUnique(root.selectionImport.effective)
200
+ } : void 0
201
+ })).sort((a, b) => `${a.rootId}:${a.graphNodeId}`.localeCompare(`${b.rootId}:${b.graphNodeId}`)),
202
+ nodes: [...parsed.canonical.nodes].map((node) => ({
203
+ ...node,
204
+ requiredBy: sortedUnique(node.requiredBy),
205
+ selected: sortedUnique(node.selected),
206
+ selectionReasons: canonicalSelectionReasons(node.selectionReasons)
207
+ })).sort((a, b) => a.id.localeCompare(b.id)),
208
+ edges: [...parsed.canonical.edges].map((edge) => ({ ...edge, selected: sortedUnique(edge.selected) })).sort((a, b) => `${a.from}:${a.alias}:${a.to}`.localeCompare(`${b.from}:${b.alias}:${b.to}`)),
209
+ includeEdges: [...parsed.canonical.includeEdges].sort((a, b) => `${a.fromNodeId}:${a.alias}:${a.toNodeId}:${a.selector}`.localeCompare(`${b.fromNodeId}:${b.alias}:${b.toNodeId}:${b.selector}`)),
210
+ artifacts: [...parsed.canonical.artifacts].map((artifact) => ({ ...artifact, owners: sortedUnique(artifact.owners) })).sort((a, b) => a.logicalSelector.localeCompare(b.logicalSelector)),
211
+ namespacing: [...parsed.canonical.namespacing].sort((a, b) => `${a.type}:${a.installName}:${a.graphNodeId}:${a.name}`.localeCompare(`${b.type}:${b.installName}:${b.graphNodeId}:${b.name}`)),
212
+ overrides: [...parsed.canonical.overrides].sort((a, b) => `${a.type}:${a.installName}:${a.graphNodeId}:${a.overriddenGraphNodeId}`.localeCompare(`${b.type}:${b.installName}:${b.graphNodeId}:${b.overriddenGraphNodeId}`)),
213
+ plainNameIncumbents: [...parsed.canonical.plainNameIncumbents].sort((a, b) => `${a.adapter}:${a.targetFingerprint}:${a.type}:${a.name}`.localeCompare(`${b.adapter}:${b.targetFingerprint}:${b.type}:${b.name}`))
214
+ }
215
+ };
216
+ }
217
+ function canonicalSelectionReasons(reasons) {
218
+ if (!reasons) return void 0;
219
+ const out = {};
220
+ for (const key of Object.keys(reasons).sort((a, b) => a.localeCompare(b))) {
221
+ out[key] = sortedUnique(reasons[key] ?? []);
222
+ }
223
+ return Object.keys(out).length > 0 ? out : void 0;
224
+ }
225
+ function computeTargetFingerprint(parts) {
226
+ return createHash("sha256").update(stableStringify(parts)).digest("hex");
227
+ }
228
+ function sortedUnique(values) {
229
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
230
+ }
231
+ function stableStringify(value) {
232
+ return JSON.stringify(stableValue(value), null, 2);
233
+ }
234
+ function stableValue(value) {
235
+ if (Array.isArray(value)) return value.map(stableValue);
236
+ if (!value || typeof value !== "object") return value;
237
+ const out = {};
238
+ for (const key of Object.keys(value).sort((a, b) => a.localeCompare(b))) {
239
+ const item = value[key];
240
+ if (item !== void 0) out[key] = stableValue(item);
241
+ }
242
+ return out;
243
+ }
244
+
245
+ // src/install/json-merge.ts
246
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
247
+ import { dirname as dirname2 } from "path";
248
+ import { mkdir as mkdir2 } from "fs/promises";
249
+ async function mergeJsonFile(sourcePath, destPath) {
250
+ const source = JSON.parse(await readFile2(sourcePath, "utf8"));
251
+ const current = await pathExists(destPath) ? JSON.parse(await readFile2(destPath, "utf8")) : {};
252
+ const merged = deepMerge(current, source);
253
+ await mkdir2(dirname2(destPath), { recursive: true });
254
+ await writeFile2(destPath, `${JSON.stringify(merged, null, 2)}
255
+ `, "utf8");
256
+ }
257
+ function deepMerge(base, incoming) {
258
+ if (Array.isArray(base) && Array.isArray(incoming)) {
259
+ return dedupeArray([...base, ...incoming]);
260
+ }
261
+ if (isRecord(base) && isRecord(incoming)) {
262
+ const out = { ...base };
263
+ for (const [key, value] of Object.entries(incoming)) {
264
+ out[key] = key in out ? deepMerge(out[key], value) : value;
265
+ }
266
+ return out;
267
+ }
268
+ return incoming;
269
+ }
270
+ function isRecord(value) {
271
+ return typeof value === "object" && value !== null && !Array.isArray(value);
272
+ }
273
+ function dedupeArray(values) {
274
+ const seen = /* @__PURE__ */ new Set();
275
+ const out = [];
276
+ for (const value of values) {
277
+ const key = JSON.stringify(value);
278
+ if (seen.has(key)) continue;
279
+ seen.add(key);
280
+ out.push(value);
281
+ }
282
+ return out;
283
+ }
284
+
285
+ // src/install/openclaw-json-merge.ts
286
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
287
+ import { dirname as dirname3 } from "path";
288
+ async function mergeOpenClawJsonFile(sourcePath, destPath) {
289
+ const source = await renderOpenClawJsonMergeSource(sourcePath);
290
+ const current = await pathExists(destPath) ? JSON.parse(await readFile3(destPath, "utf8")) : {};
291
+ const merged = mergeOpenClawJson(current, source);
292
+ await mkdir3(dirname3(destPath), { recursive: true });
293
+ await writeFile3(destPath, `${JSON.stringify(merged, null, 2)}
294
+ `, "utf8");
295
+ }
296
+ async function renderOpenClawJsonMergeSource(sourcePath) {
297
+ return expandEnvPlaceholders(
298
+ normalizeOpenClawConfig(JSON.parse(await readFile3(sourcePath, "utf8"))),
299
+ sourcePath
300
+ );
301
+ }
302
+ function mergeOpenClawJson(base, incoming, path = []) {
303
+ if (isMcpServerCodexAgentsPath(path) && Array.isArray(incoming)) {
304
+ return incoming;
305
+ }
306
+ if (path.join(".") === "agents.list" && Array.isArray(base) && Array.isArray(incoming)) {
307
+ return mergeOpenClawAgentsById(base, incoming);
308
+ }
309
+ if (isAgentwheelSkillRouterRepositoriesPath(path) && Array.isArray(base) && Array.isArray(incoming)) {
310
+ return mergeOpenClawRecordsByKey(base, incoming, "name");
311
+ }
312
+ if (Array.isArray(base) && Array.isArray(incoming)) {
313
+ return deepMerge(base, incoming);
314
+ }
315
+ if (isRecord(base) && isRecord(incoming)) {
316
+ const out = { ...base };
317
+ for (const [key, value] of Object.entries(incoming)) {
318
+ out[key] = key in out ? mergeOpenClawJson(out[key], value, [...path, key]) : value;
319
+ }
320
+ return out;
321
+ }
322
+ return incoming;
323
+ }
324
+ function isMcpServerCodexAgentsPath(path) {
325
+ return path.length === 5 && path[0] === "mcp" && path[1] === "servers" && path[3] === "codex" && path[4] === "agents";
326
+ }
327
+ function isAgentwheelSkillRouterRepositoriesPath(path) {
328
+ return path.join(".") === "plugins.entries.agentwheel-skill-router.config.repositories";
329
+ }
330
+ function expandEnvPlaceholders(value, sourcePath) {
331
+ if (typeof value === "string") {
332
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
333
+ const replacement = process.env[name];
334
+ if (replacement === void 0) {
335
+ throw new Error(`Missing environment variable ${name} while rendering OpenClaw JSON merge artifact ${sourcePath}`);
336
+ }
337
+ return replacement;
338
+ });
339
+ }
340
+ if (Array.isArray(value)) return value.map((item) => expandEnvPlaceholders(item, sourcePath));
341
+ if (!isRecord(value)) return value;
342
+ return Object.fromEntries(
343
+ Object.entries(value).map(([key, child]) => [key, expandEnvPlaceholders(child, sourcePath)])
344
+ );
345
+ }
346
+ function normalizeOpenClawConfig(value) {
347
+ if (!isRecord(value)) return value;
348
+ const rootMcpServers = isRecord(value.mcpServers) ? value.mcpServers : void 0;
349
+ if (!rootMcpServers) return value;
350
+ const out = { ...value };
351
+ const normalizedServers = {};
352
+ for (const [name, server] of Object.entries(rootMcpServers)) {
353
+ if (isRecord(server)) normalizedServers[name] = normalizeOpenClawMcpServer(server);
354
+ }
355
+ const mcp = isRecord(out.mcp) ? out.mcp : {};
356
+ out.mcp = deepMerge(mcp, { servers: normalizedServers });
357
+ delete out.mcpServers;
358
+ return out;
359
+ }
360
+ function normalizeOpenClawMcpServer(server) {
361
+ const out = { ...server };
362
+ const type = typeof out.type === "string" ? out.type : void 0;
363
+ if (type && typeof out.transport !== "string") out.transport = type;
364
+ delete out.type;
365
+ return out;
366
+ }
367
+ function mergeOpenClawAgentsById(base, incoming) {
368
+ const out = [...base];
369
+ const indexById = /* @__PURE__ */ new Map();
370
+ for (const [index, value] of out.entries()) {
371
+ const id = isRecord(value) && typeof value.id === "string" ? value.id : void 0;
372
+ if (id) indexById.set(id, index);
373
+ }
374
+ for (const value of incoming) {
375
+ const id = isRecord(value) && typeof value.id === "string" ? value.id : void 0;
376
+ if (!id || !indexById.has(id)) {
377
+ out.push(value);
378
+ if (id) indexById.set(id, out.length - 1);
379
+ continue;
380
+ }
381
+ out[indexById.get(id)] = value;
382
+ }
383
+ return out;
384
+ }
385
+ function mergeOpenClawRecordsByKey(base, incoming, key) {
386
+ const replacements = /* @__PURE__ */ new Map();
387
+ for (const value of incoming) {
388
+ const recordKey = isRecord(value) && typeof value[key] === "string" ? value[key] : void 0;
389
+ if (recordKey) replacements.set(recordKey, value);
390
+ }
391
+ const out = [];
392
+ const emitted = /* @__PURE__ */ new Set();
393
+ for (const value of base) {
394
+ const recordKey = isRecord(value) && typeof value[key] === "string" ? value[key] : void 0;
395
+ const replacement = recordKey ? replacements.get(recordKey) : void 0;
396
+ if (!recordKey || !replacement) {
397
+ out.push(value);
398
+ continue;
399
+ }
400
+ if (!emitted.has(recordKey)) {
401
+ out.push(replacement);
402
+ emitted.add(recordKey);
403
+ }
404
+ }
405
+ for (const value of incoming) {
406
+ const recordKey = isRecord(value) && typeof value[key] === "string" ? value[key] : void 0;
407
+ if (!recordKey || !emitted.has(recordKey)) {
408
+ out.push(value);
409
+ if (recordKey) emitted.add(recordKey);
410
+ }
411
+ }
412
+ return out;
413
+ }
414
+
415
+ // src/install/manifest.ts
416
+ import { createHash as createHash2 } from "crypto";
417
+ import { join, resolve } from "path";
418
+
419
+ // src/model/manifest.ts
420
+ import { z as z3 } from "zod";
421
+ var mergeValueSchema = z3.lazy(() => z3.union([
422
+ z3.null(),
423
+ z3.boolean(),
424
+ z3.number(),
425
+ z3.string(),
426
+ z3.array(mergeValueSchema),
427
+ z3.record(z3.string(), mergeValueSchema)
428
+ ]));
429
+ var mergeRemovalSchema = z3.record(z3.string(), mergeValueSchema);
430
+ var dependencyRoleSchema = z3.enum(["root", "direct", "transitive", "fragment"]);
431
+ var legacyUnownedWorkspaceOwner = "legacy:unowned";
432
+ var CURRENT_INSTALL_MANIFEST_VERSION = 2;
433
+ var CURRENT_SOURCE_LOCK_VERSION = 1;
434
+ var semanticPluginSpecSchema = z3.object({
435
+ runtime: z3.enum(["openclaw", "claude", "codex", "copilot", "hermes"]),
436
+ pluginName: z3.string().min(1),
437
+ marketplaceName: z3.string().min(1).optional(),
438
+ stateRoot: z3.string().min(1).optional(),
439
+ installCommands: z3.array(z3.array(z3.string()).min(1)).min(1),
440
+ uninstallCommands: z3.array(z3.array(z3.string()).min(1)).min(1),
441
+ enableCommands: z3.array(z3.array(z3.string()).min(1)).optional(),
442
+ disableCommands: z3.array(z3.array(z3.string()).min(1)).optional()
443
+ });
444
+ var manifestEntryV1Schema = z3.object({
445
+ path: z3.string().min(1),
446
+ artifactType: artifactTypeSchema,
447
+ artifactName: z3.string().min(1),
448
+ kind: fileKindSchema,
449
+ hash: z3.string().min(16),
450
+ sourceHash: z3.string().min(16),
451
+ updatedAt: z3.string().datetime(),
452
+ channel: z3.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed"),
453
+ packageName: z3.string().min(1).optional(),
454
+ semanticCommand: z3.array(z3.string()).optional(),
455
+ semanticPlugin: semanticPluginSpecSchema.optional(),
456
+ executed: z3.boolean().optional(),
457
+ mergeStrategy: z3.enum(["json-deep", "openclaw-json-deep", "yaml-deep", "codex-toml-mcp"]).optional(),
458
+ mergeRemoval: mergeRemovalSchema.optional(),
459
+ mergeCreatedDestination: z3.boolean().optional(),
460
+ mode: z3.enum(["managed-block"]).optional(),
461
+ composedFrom: z3.array(composedFromEntrySchema).optional()
462
+ });
463
+ var manifestEntrySchema = manifestEntryV1Schema.extend({
464
+ installName: z3.string().min(1),
465
+ logicalSelector: z3.string().min(1).optional(),
466
+ graphNodeId: z3.string().min(1).optional(),
467
+ dependencyRole: dependencyRoleSchema.default("root"),
468
+ owners: z3.array(z3.string().min(1)).min(1),
469
+ refCount: z3.number().int().positive(),
470
+ workspaceOwner: z3.string().min(1).default(legacyUnownedWorkspaceOwner),
471
+ graphLockDigest: z3.string().min(1).optional()
472
+ }).transform((entry) => {
473
+ const owners = [...new Set(entry.owners)].sort();
474
+ return {
475
+ ...entry,
476
+ owners,
477
+ refCount: owners.length
478
+ };
479
+ });
480
+ var installManifestV1Schema = z3.object({
481
+ version: z3.literal(1),
482
+ adapter: z3.string().min(1),
483
+ targetRoot: z3.string().min(1),
484
+ generatedAt: z3.string().datetime(),
485
+ adapterCode: z3.object({
486
+ modulePath: z3.string().min(1),
487
+ hash: z3.string().min(16)
488
+ }).optional(),
489
+ entries: z3.array(manifestEntryV1Schema)
490
+ }).transform((manifest) => ({
491
+ ...manifest,
492
+ legacy: true
493
+ }));
494
+ var installManifestV2Schema = z3.object({
495
+ version: z3.literal(CURRENT_INSTALL_MANIFEST_VERSION),
496
+ adapter: z3.string().min(1),
497
+ installationType: installationTypeSchema.default(defaultInstallationType),
498
+ stateKey: z3.string().min(1).optional(),
499
+ targetRoot: z3.string().min(1),
500
+ generatedAt: z3.string().datetime(),
501
+ revision: z3.string().min(16),
502
+ adapterCode: z3.object({
503
+ modulePath: z3.string().min(1),
504
+ hash: z3.string().min(16)
505
+ }).optional(),
506
+ entries: z3.array(manifestEntrySchema)
507
+ }).transform((manifest) => ({
508
+ ...manifest,
509
+ legacy: false
510
+ }));
511
+ var installManifestSchema = z3.union([installManifestV2Schema, installManifestV1Schema]);
512
+ var sourceLockSchema = z3.object({
513
+ version: z3.literal(CURRENT_SOURCE_LOCK_VERSION),
514
+ driver: z3.string().min(1),
515
+ source: z3.string().min(1),
516
+ resolvedPath: z3.string().min(1),
517
+ packageName: z3.string().min(1).optional(),
518
+ packageVersion: z3.string().min(1).optional(),
519
+ mode: z3.enum(["pinned", "tracking"]).default("pinned"),
520
+ requestedRef: z3.string().min(1).optional(),
521
+ resolvedCommit: z3.string().min(1).optional(),
522
+ cacheIdentity: immutableCacheIdentitySchema.optional(),
523
+ sourceHash: z3.string().min(16).optional(),
524
+ generatedAt: z3.string().datetime(),
525
+ artifacts: z3.array(
526
+ z3.object({
527
+ type: artifactTypeSchema,
528
+ name: z3.string().min(1),
529
+ relativePath: z3.string().min(1),
530
+ kind: fileKindSchema,
531
+ hash: z3.string().min(16),
532
+ format: artifactFormatSchema.optional(),
533
+ composedFrom: z3.array(composedFromEntrySchema).optional()
534
+ })
535
+ )
536
+ });
537
+
538
+ // src/install/manifest.ts
539
+ async function readInstallManifest(targetRoot, adapter, transport = localTransport, scope = {}) {
540
+ const path = installManifestPath(targetRoot, adapter, scope);
541
+ if (!await transport.pathExists(path)) return void 0;
542
+ const raw = JSON.parse(await transport.readFile(path));
543
+ const parsed = installManifestSchema.parse(raw);
544
+ return {
545
+ ...parsed,
546
+ revision: computeManifestRevision(raw)
547
+ };
548
+ }
549
+ async function listInstallManifests(targetRoot, adapter, transport = localTransport) {
550
+ const dir = metadataDir(targetRoot);
551
+ const suffix = ".install-manifest.json";
552
+ const prefix = `${adapter}.`;
553
+ const found = [];
554
+ for (const fileName of await transport.listDir(dir)) {
555
+ if (!fileName.endsWith(suffix)) continue;
556
+ const stateKey = fileName.slice(0, -suffix.length);
557
+ if (stateKey !== adapter && !stateKey.startsWith(prefix)) continue;
558
+ const path = join(dir, fileName);
559
+ try {
560
+ const raw = JSON.parse(await transport.readFile(path));
561
+ const parsed = installManifestSchema.parse(raw);
562
+ found.push({ path, fileName, stateKey, manifest: { ...parsed, revision: computeManifestRevision(raw) } });
563
+ } catch (error) {
564
+ throw new Error(`Unreadable install manifest at ${path}: ${error instanceof Error ? error.message : String(error)}`);
565
+ }
566
+ }
567
+ return found.sort((a, b) => a.fileName.localeCompare(b.fileName));
568
+ }
569
+ async function writeInstallManifest(manifest, transport = localTransport) {
570
+ const next = withManifestRevision(manifest);
571
+ await transport.writeJsonAtomic(installManifestPath(next.targetRoot, next.adapter, {
572
+ installationType: next.installationType,
573
+ stateKey: next.stateKey
574
+ }), stripReadOnlyManifestFields(next));
575
+ }
576
+ async function writeSourceLock(targetRoot, adapter, lock, transport = localTransport, scope = {}) {
577
+ await transport.writeJsonAtomic(sourceLockPath(targetRoot, adapter, scope), lock);
578
+ }
579
+ async function removeStateFiles(targetRoot, adapter, transport = localTransport, scope = {}) {
580
+ await transport.rm(installManifestPath(targetRoot, adapter, scope));
581
+ await transport.rm(sourceLockPath(targetRoot, adapter, scope));
582
+ }
583
+ function normalizeTargetRoot(path) {
584
+ return resolve(path);
585
+ }
586
+ function withManifestRevision(manifest) {
587
+ if (manifest.version !== 2) {
588
+ throw new Error("Install manifest writes must use version 2");
589
+ }
590
+ const raw = stripReadOnlyManifestFields(manifest);
591
+ const normalized = installManifestV2Schema.parse({
592
+ installationType: defaultInstallationType,
593
+ ...raw
594
+ });
595
+ const withoutRevision = stripRuntimeManifestFields(normalized);
596
+ return {
597
+ ...normalized,
598
+ legacy: false,
599
+ revision: computeManifestRevision(withoutRevision)
600
+ };
601
+ }
602
+ function computeManifestRevision(manifest) {
603
+ return createHash2("sha256").update(canonicalJson(stripRuntimeManifestFields(manifest))).digest("hex");
604
+ }
605
+ function canonicalInstallManifestJson(manifest) {
606
+ return canonicalJson(stripRuntimeManifestFields(manifest));
607
+ }
608
+ function stripRuntimeManifestFields(value) {
609
+ if (Array.isArray(value)) return value.map(stripRuntimeManifestFields);
610
+ if (!value || typeof value !== "object") return value;
611
+ const record = value;
612
+ const out = {};
613
+ for (const [key, item] of Object.entries(record)) {
614
+ if (key === "revision" || key === "legacy" || item === void 0) continue;
615
+ out[key] = stripRuntimeManifestFields(item);
616
+ }
617
+ return out;
618
+ }
619
+ function stripReadOnlyManifestFields(value) {
620
+ if (Array.isArray(value)) return value.map(stripReadOnlyManifestFields);
621
+ if (!value || typeof value !== "object") return value;
622
+ const record = value;
623
+ const out = {};
624
+ for (const [key, item] of Object.entries(record)) {
625
+ if (key === "legacy" || item === void 0) continue;
626
+ out[key] = stripReadOnlyManifestFields(item);
627
+ }
628
+ return out;
629
+ }
630
+ function canonicalJson(value) {
631
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
632
+ if (!value || typeof value !== "object") return JSON.stringify(value);
633
+ const record = value;
634
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
635
+ }
636
+
637
+ // src/install/path-safety.ts
638
+ import { isAbsolute, relative, resolve as resolve2 } from "path";
639
+ function assertSafeInstallName(name, label) {
640
+ if (!name || name === "." || name === "..") {
641
+ throw new Error(`Invalid install name for ${label}: ${JSON.stringify(name)}`);
642
+ }
643
+ if (name.includes("/") || name.includes("\\")) {
644
+ throw new Error(`Invalid install name for ${label}: path separators are not allowed (${JSON.stringify(name)})`);
645
+ }
646
+ }
647
+ function assertOperationContained(operation, targetRoot) {
648
+ const root = resolve2(targetRoot);
649
+ const dest = resolve2(operation.destPath);
650
+ const rel = relative(root, dest);
651
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return;
652
+ throw new Error(`Refusing operation outside target root: ${operation.relativeDestPath} -> ${operation.destPath}`);
653
+ }
654
+
655
+ // src/install/toml-merge.ts
656
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
657
+ import { dirname as dirname4 } from "path";
658
+ async function mergeCodexTomlMcp(sourcePath, destPath) {
659
+ const source = JSON.parse(await readFile4(sourcePath, "utf8"));
660
+ const servers = extractMcpServers(source);
661
+ const current = await pathExists(destPath) ? await readFile4(destPath, "utf8") : "";
662
+ const withoutManaged = removeManagedMcpSections(current, Object.keys(servers));
663
+ const merged = appendMcpServers(withoutManaged, servers);
664
+ await mkdir4(dirname4(destPath), { recursive: true });
665
+ await writeFile4(destPath, merged, "utf8");
666
+ }
667
+ function mismatchedCodexTomlMcpServers(source, currentContent) {
668
+ const servers = extractMcpServers(source);
669
+ return Object.entries(servers).filter(([name, server]) => extractMcpServerBlock(currentContent, name) !== formatMcpServer(name, server)).map(([name]) => name);
670
+ }
671
+ function extractMcpServers(source) {
672
+ const raw = isRecord2(source.mcpServers) ? source.mcpServers : source;
673
+ const servers = {};
674
+ for (const [name, value] of Object.entries(raw)) {
675
+ if (!isRecord2(value)) continue;
676
+ servers[name] = value;
677
+ }
678
+ if (Object.keys(servers).length === 0) {
679
+ throw new Error("Codex MCP TOML merge needs a JSON object with mcpServers");
680
+ }
681
+ return servers;
682
+ }
683
+ function removeManagedMcpSections(content, serverNames) {
684
+ if (serverNames.length === 0 || content.trim() === "") return content;
685
+ const names = new Set(serverNames);
686
+ const lines = content.split(/\r?\n/);
687
+ const kept = [];
688
+ let skipping = false;
689
+ for (const line of lines) {
690
+ const section = line.match(/^\s*\[([^\]]+)]\s*$/)?.[1];
691
+ if (section) {
692
+ const match = section.match(/^mcp_servers\.([^.\]]+)(?:\.|$)/);
693
+ skipping = match ? names.has(unquoteTomlKey(match[1] ?? "")) : false;
694
+ }
695
+ if (!skipping) kept.push(line);
696
+ }
697
+ return kept.join("\n").replace(/\n{3,}$/g, "\n\n");
698
+ }
699
+ function appendMcpServers(content, servers) {
700
+ const blocks = Object.entries(servers).sort(([a], [b]) => a.localeCompare(b)).map(([name, server]) => formatMcpServer(name, server));
701
+ const prefix = content.trimEnd();
702
+ return `${prefix ? `${prefix}
703
+
704
+ ` : ""}${blocks.join("\n\n")}
705
+ `;
706
+ }
707
+ function formatMcpServer(name, server) {
708
+ const env = isRecord2(server.env) ? server.env : void 0;
709
+ const lines = [`[mcp_servers.${quoteTomlKey(name)}]`];
710
+ for (const [key, value] of Object.entries(server)) {
711
+ if (key === "env" || value === void 0) continue;
712
+ lines.push(`${quoteTomlKey(key)} = ${formatTomlValue(value)}`);
713
+ }
714
+ if (env && Object.keys(env).length > 0) {
715
+ lines.push("", `[mcp_servers.${quoteTomlKey(name)}.env]`);
716
+ for (const [key, value] of Object.entries(env)) {
717
+ lines.push(`${quoteTomlKey(key)} = ${formatTomlValue(value)}`);
718
+ }
719
+ }
720
+ return lines.join("\n");
721
+ }
722
+ function extractMcpServerBlock(content, serverName) {
723
+ const lines = content.split(/\r?\n/);
724
+ const blocks = [];
725
+ let block;
726
+ let collecting = false;
727
+ for (const line of lines) {
728
+ const section = line.match(/^\s*\[([^\]]+)]\s*$/)?.[1];
729
+ if (section) {
730
+ const match = section.match(/^mcp_servers\.([^\.\]]+)(?:\.|$)/);
731
+ const name = match?.[1] ? unquoteTomlKey(match[1]) : void 0;
732
+ if (collecting && block) {
733
+ while (block.at(-1)?.trim() === "") block.pop();
734
+ blocks.push(block);
735
+ }
736
+ collecting = name === serverName;
737
+ block = collecting ? [] : void 0;
738
+ }
739
+ if (collecting) block?.push(line);
740
+ }
741
+ if (collecting && block) {
742
+ while (block.at(-1)?.trim() === "") block.pop();
743
+ blocks.push(block);
744
+ }
745
+ if (blocks.length === 0) return void 0;
746
+ return blocks.map((lines2) => lines2.join("\n")).join("\n\n");
747
+ }
748
+ function formatTomlValue(value) {
749
+ if (typeof value === "string") return JSON.stringify(value);
750
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
751
+ if (Array.isArray(value)) return `[${value.map(formatTomlValue).join(", ")}]`;
752
+ if (isRecord2(value)) {
753
+ const entries = Object.entries(value).map(([key, child]) => `${quoteTomlKey(key)} = ${formatTomlValue(child)}`);
754
+ return `{ ${entries.join(", ")} }`;
755
+ }
756
+ return '""';
757
+ }
758
+ function quoteTomlKey(key) {
759
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
760
+ }
761
+ function unquoteTomlKey(key) {
762
+ if (!key.startsWith('"')) return key;
763
+ try {
764
+ return JSON.parse(key);
765
+ } catch {
766
+ return key;
767
+ }
768
+ }
769
+ function isRecord2(value) {
770
+ return typeof value === "object" && value !== null && !Array.isArray(value);
771
+ }
772
+
773
+ // src/install/yaml-merge.ts
774
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
775
+ import { dirname as dirname5 } from "path";
776
+ import { parse, stringify } from "yaml";
777
+ async function mergeYamlFile(sourcePath, destPath) {
778
+ const source = parseYamlValue(await readFile5(sourcePath, "utf8"));
779
+ const current = await pathExists(destPath) ? parseYamlValue(await readFile5(destPath, "utf8")) : {};
780
+ const merged = deepMerge2(current, source);
781
+ await mkdir5(dirname5(destPath), { recursive: true });
782
+ await writeFile5(destPath, stringify(merged), "utf8");
783
+ }
784
+ function parseYamlValue(content) {
785
+ return normalizeYamlValue(parse(content));
786
+ }
787
+ function normalizeYamlValue(value) {
788
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
789
+ return value;
790
+ }
791
+ if (Array.isArray(value)) return value.map(normalizeYamlValue);
792
+ if (isPlainObject(value)) {
793
+ const out = {};
794
+ for (const [key, child] of Object.entries(value)) {
795
+ out[key] = normalizeYamlValue(child);
796
+ }
797
+ return out;
798
+ }
799
+ return value === void 0 ? null : String(value);
800
+ }
801
+ function deepMerge2(base, incoming) {
802
+ if (Array.isArray(base) && Array.isArray(incoming)) {
803
+ return dedupeArray2([...base, ...incoming]);
804
+ }
805
+ if (isRecord3(base) && isRecord3(incoming)) {
806
+ const out = { ...base };
807
+ for (const [key, value] of Object.entries(incoming)) {
808
+ out[key] = key in out ? deepMerge2(out[key], value) : value;
809
+ }
810
+ return out;
811
+ }
812
+ return incoming;
813
+ }
814
+ function isPlainObject(value) {
815
+ return typeof value === "object" && value !== null && !Array.isArray(value);
816
+ }
817
+ function isRecord3(value) {
818
+ return typeof value === "object" && value !== null && !Array.isArray(value);
819
+ }
820
+ function dedupeArray2(values) {
821
+ const seen = /* @__PURE__ */ new Set();
822
+ const out = [];
823
+ for (const value of values) {
824
+ const key = JSON.stringify(value);
825
+ if (seen.has(key)) continue;
826
+ seen.add(key);
827
+ out.push(value);
828
+ }
829
+ return out;
830
+ }
831
+
832
+ // src/install/merge-removal.ts
833
+ import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
834
+ import { dirname as dirname6 } from "path";
835
+ import { parse as parse2, stringify as stringify2 } from "yaml";
836
+ var MergeAdoptionMismatchError = class extends Error {
837
+ };
838
+ function assertExactMergeContribution(contribution, strategy, currentContent) {
839
+ if (strategy === "codex-toml-mcp") {
840
+ assertExactMcpMergeContribution(contribution, strategy, currentContent);
841
+ return;
842
+ }
843
+ const current = parseMergeDestination(currentContent, strategy);
844
+ const mismatch = strategy === "openclaw-json-deep" ? firstOpenClawContributionMismatch(current, contribution) : firstMergeContributionMismatch(current, contribution);
845
+ if (mismatch) {
846
+ throw new MergeAdoptionMismatchError(`exact merge contribution differs or is missing at ${mismatch}`);
847
+ }
848
+ }
849
+ async function assertMergedSourceContribution(sourcePath, strategy, currentContent) {
850
+ assertExactMergeContribution(await readMergeSource(sourcePath, strategy), strategy, currentContent);
851
+ }
852
+ function assertExactMcpMergeContribution(removal, strategy, currentContent) {
853
+ if (strategy === "codex-toml-mcp") {
854
+ const mismatched = mismatchedCodexTomlMcpServers(removal, currentContent);
855
+ if (mismatched.length > 0) {
856
+ throw new MergeAdoptionMismatchError(`exact MCP retirement precondition failed: Codex MCP server content differs or is missing for ${mismatched.join(", ")}`);
857
+ }
858
+ return;
859
+ }
860
+ if (strategy !== "json-deep") {
861
+ throw new MergeAdoptionMismatchError(`exact MCP retirement precondition failed: strategy ${strategy} is not supported`);
862
+ }
863
+ const mismatch = firstMcpContributionMismatch(parseMergeDestination(currentContent, strategy), removal);
864
+ if (mismatch) {
865
+ throw new MergeAdoptionMismatchError(`exact MCP retirement precondition failed: destination differs or is missing at ${mismatch}`);
866
+ }
867
+ }
868
+ function combineMergeRemovals(existing, incoming) {
869
+ return combineMergeValues(existing, incoming);
870
+ }
871
+ function hasMergeRemovalContent(removal) {
872
+ if (!removal) return false;
873
+ return Object.entries(removal).some(([key, value]) => {
874
+ return !(key === "mcpServers" && isRecord4(value) && Object.keys(value).length === 0);
875
+ });
876
+ }
877
+ async function mergeRemovalForInstall(sourcePath, strategy, currentContent, options = {}) {
878
+ const source = await readMergeSource(sourcePath, strategy);
879
+ if (currentContent === void 0) return source;
880
+ if (options.adoptExisting) {
881
+ if (strategy === "codex-toml-mcp") {
882
+ const mismatched = mismatchedCodexTomlMcpServers(source, currentContent);
883
+ if (mismatched.length > 0) {
884
+ throw new MergeAdoptionMismatchError(`cannot adopt merged contribution: Codex MCP server content differs or is missing for ${mismatched.join(", ")}`);
885
+ }
886
+ return source;
887
+ }
888
+ const current = parseMergeDestination(currentContent, strategy);
889
+ const mismatch = options.adoptExisting === "mcp" ? firstMcpContributionMismatch(current, source) : strategy === "openclaw-json-deep" ? firstOpenClawContributionMismatch(current, source) : firstMergeContributionMismatch(current, source);
890
+ if (mismatch) {
891
+ throw new MergeAdoptionMismatchError(`cannot adopt merged contribution: destination differs or is missing at ${mismatch}`);
892
+ }
893
+ return source;
894
+ }
895
+ if (strategy === "codex-toml-mcp") {
896
+ const existingServers = codexTomlMcpServerNames(currentContent);
897
+ const servers = isRecord4(source.mcpServers) ? source.mcpServers : source;
898
+ return { mcpServers: Object.fromEntries(Object.entries(servers).filter(([name]) => !existingServers.has(name))) };
899
+ }
900
+ return introducedMergeContent(parseMergeDestination(currentContent, strategy), source);
901
+ }
902
+ async function removeMergeContribution(destPath, strategy, removal) {
903
+ const content = await readFile6(destPath, "utf8");
904
+ if (strategy === "codex-toml-mcp") {
905
+ const servers = isRecord4(removal.mcpServers) ? removal.mcpServers : removal;
906
+ await writeFile6(destPath, removeCodexTomlMcpSections(content, Object.keys(servers)), "utf8");
907
+ return;
908
+ }
909
+ const current = parseMergeDestination(content, strategy);
910
+ removeIntroducedContent(current, removal);
911
+ await mkdir6(dirname6(destPath), { recursive: true });
912
+ await writeFile6(destPath, strategy === "yaml-deep" ? stringify2(current) : `${JSON.stringify(current, null, 2)}
913
+ `, "utf8");
914
+ }
915
+ function mergeContributionAbsent(removal, strategy, currentContent) {
916
+ if (strategy === "codex-toml-mcp") {
917
+ const servers = isRecord4(removal.mcpServers) ? removal.mcpServers : removal;
918
+ const currentServers = codexTomlMcpServerNames(currentContent);
919
+ return Object.keys(servers).every((name) => !currentServers.has(name));
920
+ }
921
+ return !containsRemovalContribution(parseMergeDestination(currentContent, strategy), removal);
922
+ }
923
+ async function readMergeSource(sourcePath, strategy) {
924
+ if (strategy === "openclaw-json-deep") {
925
+ return requireRecord(await renderOpenClawJsonMergeSource(sourcePath), "OpenClaw JSON merge source");
926
+ }
927
+ const content = await readFile6(sourcePath, "utf8");
928
+ if (strategy === "yaml-deep") return requireRecord(normalizeYamlValue2(parse2(content)), "YAML merge source");
929
+ return requireRecord(JSON.parse(content), "JSON merge source");
930
+ }
931
+ function parseMergeDestination(content, strategy) {
932
+ if (strategy === "yaml-deep") return requireRecord(normalizeYamlValue2(parse2(content)), "YAML merge destination");
933
+ return requireRecord(JSON.parse(content), "JSON merge destination");
934
+ }
935
+ function introducedMergeContent(base, incoming) {
936
+ const introduced = {};
937
+ for (const [key, incomingValue] of Object.entries(incoming)) {
938
+ if (!(key in base)) {
939
+ introduced[key] = incomingValue;
940
+ continue;
941
+ }
942
+ const baseValue = base[key];
943
+ if (isRecord4(baseValue) && isRecord4(incomingValue)) {
944
+ const nested = introducedMergeContent(baseValue, incomingValue);
945
+ if (Object.keys(nested).length > 0) introduced[key] = nested;
946
+ } else if (Array.isArray(baseValue) && Array.isArray(incomingValue)) {
947
+ const additions = incomingValue.filter((value) => !baseValue.some((existing) => sameValue(existing, value)));
948
+ if (additions.length > 0) introduced[key] = additions;
949
+ }
950
+ }
951
+ return introduced;
952
+ }
953
+ function firstMcpContributionMismatch(current, incoming) {
954
+ if (!isRecord4(current) || !isRecord4(incoming)) return "$";
955
+ if (!isRecord4(current.mcpServers) || !isRecord4(incoming.mcpServers)) return "$.mcpServers";
956
+ for (const [name, incomingServer] of Object.entries(incoming.mcpServers)) {
957
+ if (!(name in current.mcpServers) || !sameMcpValue(current.mcpServers[name], incomingServer)) {
958
+ return `$.mcpServers.${name}`;
959
+ }
960
+ }
961
+ for (const [key, incomingValue] of Object.entries(incoming)) {
962
+ if (key === "mcpServers") continue;
963
+ if (!(key in current) || !sameMcpValue(current[key], incomingValue)) return `$.${key}`;
964
+ }
965
+ return void 0;
966
+ }
967
+ function firstMergeContributionMismatch(current, contribution, path = "$") {
968
+ if (isRecord4(contribution)) {
969
+ if (!isRecord4(current)) return path;
970
+ for (const [key, value] of Object.entries(contribution)) {
971
+ if (!(key in current)) return `${path}.${key}`;
972
+ const mismatch = firstMergeContributionMismatch(current[key], value, `${path}.${key}`);
973
+ if (mismatch) return mismatch;
974
+ }
975
+ return void 0;
976
+ }
977
+ if (Array.isArray(contribution)) {
978
+ if (!Array.isArray(current)) return path;
979
+ for (const value of contribution) {
980
+ if (!current.some((candidate) => sameMcpValue(candidate, value))) return path;
981
+ }
982
+ return void 0;
983
+ }
984
+ return current === contribution ? void 0 : path;
985
+ }
986
+ function firstOpenClawContributionMismatch(current, contribution, path = []) {
987
+ const displayPath = path.length === 0 ? "$" : `$.${path.join(".")}`;
988
+ if (isOpenClawExactArrayPath(path)) {
989
+ return sameMcpValue(current, contribution) ? void 0 : displayPath;
990
+ }
991
+ const keyedBy = openClawKeyedArrayField(path);
992
+ if (keyedBy) {
993
+ if (!Array.isArray(current) || !Array.isArray(contribution)) return displayPath;
994
+ const currentByKey = groupRecordsByStringKey(current, keyedBy);
995
+ for (const [key, entries] of currentByKey) {
996
+ if (entries.length !== 1) return `${displayPath}[${keyedBy}=${JSON.stringify(key)}]`;
997
+ }
998
+ const incomingByKey = groupRecordsByStringKey(contribution, keyedBy);
999
+ for (const [key, entries] of incomingByKey) {
1000
+ if (entries.length !== 1) return `${displayPath}[${keyedBy}=${JSON.stringify(key)}]`;
1001
+ const matches = currentByKey.get(key);
1002
+ if (!matches || matches.length !== 1 || !sameMcpValue(matches[0], entries[0])) {
1003
+ return `${displayPath}[${keyedBy}=${JSON.stringify(key)}]`;
1004
+ }
1005
+ }
1006
+ for (const value of contribution.filter((entry) => recordStringKey(entry, keyedBy) === void 0)) {
1007
+ if (!current.some((candidate) => sameMcpValue(candidate, value))) return displayPath;
1008
+ }
1009
+ return void 0;
1010
+ }
1011
+ if (isRecord4(contribution)) {
1012
+ if (!isRecord4(current)) return displayPath;
1013
+ for (const [key, value] of Object.entries(contribution)) {
1014
+ if (!(key in current)) return `${displayPath}.${key}`;
1015
+ const mismatch = firstOpenClawContributionMismatch(current[key], value, [...path, key]);
1016
+ if (mismatch) return mismatch;
1017
+ }
1018
+ return void 0;
1019
+ }
1020
+ if (Array.isArray(contribution)) {
1021
+ if (!Array.isArray(current)) return displayPath;
1022
+ for (const value of contribution) {
1023
+ if (!current.some((candidate) => sameMcpValue(candidate, value))) return displayPath;
1024
+ }
1025
+ return void 0;
1026
+ }
1027
+ return current === contribution ? void 0 : displayPath;
1028
+ }
1029
+ function isOpenClawExactArrayPath(path) {
1030
+ return path.length === 5 && path[0] === "mcp" && path[1] === "servers" && path[3] === "codex" && path[4] === "agents";
1031
+ }
1032
+ function openClawKeyedArrayField(path) {
1033
+ if (path.join(".") === "agents.list") return "id";
1034
+ if (path.join(".") === "plugins.entries.agentwheel-skill-router.config.repositories") return "name";
1035
+ return void 0;
1036
+ }
1037
+ function groupRecordsByStringKey(values, key) {
1038
+ const grouped = /* @__PURE__ */ new Map();
1039
+ for (const value of values) {
1040
+ const recordKey = recordStringKey(value, key);
1041
+ if (recordKey === void 0) continue;
1042
+ grouped.set(recordKey, [...grouped.get(recordKey) ?? [], value]);
1043
+ }
1044
+ return grouped;
1045
+ }
1046
+ function recordStringKey(value, key) {
1047
+ return isRecord4(value) && typeof value[key] === "string" ? value[key] : void 0;
1048
+ }
1049
+ function combineMergeValues(existing, incoming) {
1050
+ if (isRecord4(existing) && isRecord4(incoming)) {
1051
+ const combined = { ...existing };
1052
+ for (const [key, incomingValue] of Object.entries(incoming)) {
1053
+ combined[key] = key in combined ? combineMergeValues(combined[key], incomingValue) : incomingValue;
1054
+ }
1055
+ return combined;
1056
+ }
1057
+ if (Array.isArray(existing) && Array.isArray(incoming)) {
1058
+ return [...existing, ...incoming.filter((value) => !existing.some((current) => sameValue(current, value)))];
1059
+ }
1060
+ return incoming;
1061
+ }
1062
+ function removeIntroducedContent(current, removal) {
1063
+ for (const [key, removalValue] of Object.entries(removal)) {
1064
+ if (!(key in current)) continue;
1065
+ const currentValue = current[key];
1066
+ if (isRecord4(currentValue) && isRecord4(removalValue)) {
1067
+ removeIntroducedContent(currentValue, removalValue);
1068
+ if (Object.keys(currentValue).length === 0) delete current[key];
1069
+ } else if (Array.isArray(currentValue) && Array.isArray(removalValue)) {
1070
+ const remaining = currentValue.filter((value) => !removalValue.some((removed) => sameValue(removed, value)));
1071
+ if (remaining.length === 0) delete current[key];
1072
+ else current[key] = remaining;
1073
+ } else delete current[key];
1074
+ }
1075
+ }
1076
+ function containsRemovalContribution(current, removal) {
1077
+ for (const [key, removalValue] of Object.entries(removal)) {
1078
+ if (!(key in current)) continue;
1079
+ const currentValue = current[key];
1080
+ if (isRecord4(currentValue) && isRecord4(removalValue) && Object.keys(removalValue).length > 0) {
1081
+ if (containsRemovalContribution(currentValue, removalValue)) return true;
1082
+ continue;
1083
+ }
1084
+ if (Array.isArray(currentValue) && Array.isArray(removalValue) && removalValue.length > 0) {
1085
+ if (removalValue.some((removed) => currentValue.some((candidate) => sameValue(candidate, removed)))) return true;
1086
+ continue;
1087
+ }
1088
+ return true;
1089
+ }
1090
+ return false;
1091
+ }
1092
+ function codexTomlMcpServerNames(content) {
1093
+ const names = /* @__PURE__ */ new Set();
1094
+ for (const line of content.split(/\r?\n/)) {
1095
+ const section = line.match(/^\s*\[([^\]]+)]\s*$/)?.[1];
1096
+ const match = section?.match(/^mcp_servers\.([^.\]]+)(?:\.|$)/);
1097
+ if (match?.[1]) names.add(unquoteTomlKey2(match[1]));
1098
+ }
1099
+ return names;
1100
+ }
1101
+ function removeCodexTomlMcpSections(content, serverNames) {
1102
+ if (serverNames.length === 0 || content.trim() === "") return content;
1103
+ const names = new Set(serverNames);
1104
+ const kept = [];
1105
+ let skipping = false;
1106
+ for (const line of content.split(/\r?\n/)) {
1107
+ const section = line.match(/^\s*\[([^\]]+)]\s*$/)?.[1];
1108
+ if (section) {
1109
+ const match = section.match(/^mcp_servers\.([^.\]]+)(?:\.|$)/);
1110
+ skipping = match ? names.has(unquoteTomlKey2(match[1] ?? "")) : false;
1111
+ }
1112
+ if (!skipping) kept.push(line);
1113
+ }
1114
+ return kept.join("\n").replace(/\n{3,}$/g, "\n\n");
1115
+ }
1116
+ function normalizeYamlValue2(value) {
1117
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
1118
+ if (Array.isArray(value)) return value.map(normalizeYamlValue2);
1119
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, normalizeYamlValue2(child)]));
1120
+ return value === void 0 ? null : String(value);
1121
+ }
1122
+ function requireRecord(value, label) {
1123
+ if (!isRecord4(value)) throw new Error(`${label} must be an object`);
1124
+ return value;
1125
+ }
1126
+ function isRecord4(value) {
1127
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1128
+ }
1129
+ function sameValue(left, right) {
1130
+ return JSON.stringify(left) === JSON.stringify(right);
1131
+ }
1132
+ function sameMcpValue(left, right) {
1133
+ if (Array.isArray(left) || Array.isArray(right)) {
1134
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => sameMcpValue(value, right[index]));
1135
+ }
1136
+ if (isRecord4(left) || isRecord4(right)) {
1137
+ if (!isRecord4(left) || !isRecord4(right)) return false;
1138
+ const leftKeys = Object.keys(left).sort();
1139
+ const rightKeys = Object.keys(right).sort();
1140
+ return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && sameMcpValue(left[key], right[key]));
1141
+ }
1142
+ return left === right;
1143
+ }
1144
+ function unquoteTomlKey2(key) {
1145
+ if (!key.startsWith('"')) return key;
1146
+ try {
1147
+ return JSON.parse(key);
1148
+ } catch {
1149
+ return key;
1150
+ }
1151
+ }
1152
+
1153
+ // src/install/desired.ts
1154
+ function normalizeOwners(owners) {
1155
+ const normalized = [...new Set(owners.map((owner) => owner.trim()).filter(Boolean))].sort();
1156
+ if (normalized.length === 0) {
1157
+ throw new Error("Desired artifacts must declare at least one owner");
1158
+ }
1159
+ return normalized;
1160
+ }
1161
+
1162
+ // src/install/instructions-block.ts
1163
+ import { createHash as createHash3 } from "crypto";
1164
+ import { mkdtemp, readFile as readFile7, realpath, rm, writeFile as writeFile7 } from "fs/promises";
1165
+ import { tmpdir } from "os";
1166
+ import { basename, dirname as dirname7, join as join2, relative as relative2 } from "path";
1167
+ var managedInstructionBlockMode = "managed-block";
1168
+ var managedInstructionBanner = "<!-- agentwheel-managed: edit fragments, not this block -->";
1169
+ async function desiredManagedInstructionBlockHash(sourcePath) {
1170
+ const source = await readFile7(sourcePath, "utf8");
1171
+ return hashText(managedBlockBody(source));
1172
+ }
1173
+ async function readManagedInstructionBlockState(destPath, selector, transport) {
1174
+ if (!await transport.pathExists(destPath)) {
1175
+ return { exists: false, hasBlock: false, drifted: false };
1176
+ }
1177
+ const content = await transport.readFile(destPath);
1178
+ const block = findManagedInstructionBlock(content, selector);
1179
+ if (!block) return { exists: true, hasBlock: false, drifted: false };
1180
+ const hash = hashText(block.body);
1181
+ return {
1182
+ exists: true,
1183
+ hasBlock: true,
1184
+ hash,
1185
+ markerHash: block.markerHash,
1186
+ drifted: hash !== block.markerHash
1187
+ };
1188
+ }
1189
+ async function writeManagedInstructionBlock(sourcePath, destPath, selector, transport, options = {}) {
1190
+ const source = await readFile7(sourcePath, "utf8");
1191
+ const desired = renderManagedInstructionBlock(selector, source);
1192
+ const existing = await readOptionalText(destPath, transport);
1193
+ const merged = upsertManagedInstructionBlock(existing ?? "", selector, desired.block, options);
1194
+ await writeTextWithTransport(destPath, merged, transport);
1195
+ return desired.hash;
1196
+ }
1197
+ async function removeManagedInstructionBlock(destPath, selector, transport, options = {}) {
1198
+ if (!await transport.pathExists(destPath)) return;
1199
+ const existing = await transport.readFile(destPath);
1200
+ const updated = removeManagedBlockFromContent(existing, selector, options);
1201
+ await writeTextWithTransport(destPath, updated, transport);
1202
+ }
1203
+ async function managedInstructionBlockLanded(destPath, selector, expectedHash, transport) {
1204
+ if (!expectedHash) return false;
1205
+ const state = await readManagedInstructionBlockState(destPath, selector, transport);
1206
+ return state.exists && state.hasBlock && !state.drifted && state.hash === expectedHash;
1207
+ }
1208
+ async function managedInstructionPhysicalKey(destPath, transport) {
1209
+ if (transport.kind !== "local" || !await transport.pathExists(destPath)) return destPath;
1210
+ return realpath(destPath);
1211
+ }
1212
+ async function claudeInstructionBridgesAgents(claudePath, agentsPath, transport) {
1213
+ if (!await transport.pathExists(claudePath)) return false;
1214
+ if (await samePhysicalPath(claudePath, agentsPath, transport)) return true;
1215
+ const claudeContent = await transport.readFile(claudePath);
1216
+ return referencesAgentsMd(claudeContent, claudePath, agentsPath);
1217
+ }
1218
+ function managedInstructionSelector(selector, artifactType, artifactName) {
1219
+ if (!selector) return `${artifactType}/${artifactName}`;
1220
+ const unscoped = selector.split(":").at(-1);
1221
+ return unscoped?.includes("/") ? unscoped : selector;
1222
+ }
1223
+ function renderManagedInstructionBlock(selector, source) {
1224
+ const body = managedBlockBody(source);
1225
+ const hash = hashText(body);
1226
+ return {
1227
+ block: `<!-- BEGIN openpack:include ${selector} sha256:${hash} -->
1228
+ ${body}<!-- END openpack:include ${selector} -->
1229
+ `,
1230
+ hash
1231
+ };
1232
+ }
1233
+ function upsertManagedInstructionBlock(content, selector, block, options) {
1234
+ const { expectedHash, allowDrift = false } = options;
1235
+ const existing = findManagedInstructionBlock(content, selector);
1236
+ if (!existing) {
1237
+ if (expectedHash) throw new Error(`Managed instruction block missing for ${selector}`);
1238
+ return appendManagedInstructionBlock(content, block);
1239
+ }
1240
+ if (!allowDrift) {
1241
+ assertCleanBlock(existing, selector);
1242
+ if (expectedHash && hashText(existing.body) !== expectedHash) {
1243
+ throw new Error(`Managed instruction block drift detected for ${selector}`);
1244
+ }
1245
+ }
1246
+ return `${content.slice(0, existing.start)}${block}${content.slice(existing.end)}`;
1247
+ }
1248
+ function removeManagedBlockFromContent(content, selector, options) {
1249
+ const { expectedHash, allowDrift = false } = options;
1250
+ const existing = findManagedInstructionBlock(content, selector);
1251
+ if (!existing) return content;
1252
+ if (!allowDrift) {
1253
+ assertCleanBlock(existing, selector);
1254
+ if (expectedHash && hashText(existing.body) !== expectedHash) {
1255
+ throw new Error(`Managed instruction block drift detected for ${selector}`);
1256
+ }
1257
+ }
1258
+ return `${content.slice(0, existing.start)}${content.slice(existing.end)}`;
1259
+ }
1260
+ function findManagedInstructionBlock(content, selector) {
1261
+ const beginPattern = new RegExp(`<!-- BEGIN openpack:include ${escapeRegex(selector)} sha256:([a-f0-9]+) -->\\r?\\n?`);
1262
+ const begin = beginPattern.exec(content);
1263
+ if (!begin || begin.index === void 0) return void 0;
1264
+ const markerHash = begin[1];
1265
+ const bodyStart = begin.index + begin[0].length;
1266
+ const endMarker = `<!-- END openpack:include ${selector} -->`;
1267
+ const endIndex = content.indexOf(endMarker, bodyStart);
1268
+ if (endIndex < 0) {
1269
+ return {
1270
+ start: begin.index,
1271
+ end: content.length,
1272
+ body: content.slice(bodyStart),
1273
+ markerHash
1274
+ };
1275
+ }
1276
+ let blockEnd = endIndex + endMarker.length;
1277
+ if (content[blockEnd] === "\r" && content[blockEnd + 1] === "\n") blockEnd += 2;
1278
+ else if (content[blockEnd] === "\n") blockEnd += 1;
1279
+ return {
1280
+ start: begin.index,
1281
+ end: blockEnd,
1282
+ body: content.slice(bodyStart, endIndex),
1283
+ markerHash
1284
+ };
1285
+ }
1286
+ function managedBlockBody(source) {
1287
+ return `${managedInstructionBanner}
1288
+ ${ensureTrailingNewline(source)}`;
1289
+ }
1290
+ function appendManagedInstructionBlock(content, block) {
1291
+ if (content.length === 0) return block;
1292
+ const separator = content.endsWith("\n\n") ? "" : content.endsWith("\n") ? "\n" : "\n\n";
1293
+ return `${content}${separator}${block}`;
1294
+ }
1295
+ function assertCleanBlock(block, selector) {
1296
+ const actual = hashText(block.body);
1297
+ if (actual !== block.markerHash) {
1298
+ throw new Error(`Managed instruction block drift detected for ${selector}`);
1299
+ }
1300
+ }
1301
+ function referencesAgentsMd(content, claudePath, agentsPath) {
1302
+ const claudeDir = dirname7(claudePath);
1303
+ for (const rawLine of content.split(/\r?\n/)) {
1304
+ const line = rawLine.trim();
1305
+ const atImport = /^@import\s+(.+)$/i.exec(line);
1306
+ const atPath = /^@(.+AGENTS\.md)$/i.exec(line);
1307
+ const referenced = atImport?.[1] ?? atPath?.[1];
1308
+ if (!referenced) continue;
1309
+ const cleaned = referenced.trim().replace(/^["']|["']$/g, "");
1310
+ if (!/AGENTS\.md$/i.test(cleaned)) continue;
1311
+ const candidate = cleaned.startsWith("/") ? cleaned : join2(claudeDir, cleaned);
1312
+ if (relative2(dirname7(agentsPath), candidate).replaceAll("\\", "/") === "AGENTS.md") return true;
1313
+ if (candidate === agentsPath) return true;
1314
+ }
1315
+ return false;
1316
+ }
1317
+ async function samePhysicalPath(left, right, transport) {
1318
+ if (transport.kind !== "local") return false;
1319
+ if (!await transport.pathExists(left) || !await transport.pathExists(right)) return false;
1320
+ return await realpath(left) === await realpath(right);
1321
+ }
1322
+ async function readOptionalText(path, transport) {
1323
+ if (!await transport.pathExists(path)) return void 0;
1324
+ return transport.readFile(path);
1325
+ }
1326
+ async function writeTextWithTransport(path, content, transport) {
1327
+ if (transport.kind === "local") {
1328
+ await transport.writeFileAtomic(path, content);
1329
+ return;
1330
+ }
1331
+ const tempRoot = await mkdtemp(join2(tmpdir(), "agentwheel-instructions-"));
1332
+ const localPath = join2(tempRoot, basename(path) || "instructions.md");
1333
+ try {
1334
+ await writeFile7(localPath, content, "utf8");
1335
+ await transport.atomicCopy(localPath, path, "file");
1336
+ } finally {
1337
+ await rm(tempRoot, { recursive: true, force: true });
1338
+ }
1339
+ }
1340
+ function ensureTrailingNewline(content) {
1341
+ return content.endsWith("\n") ? content : `${content}
1342
+ `;
1343
+ }
1344
+ function hashText(content) {
1345
+ return createHash3("sha256").update(content).digest("hex");
1346
+ }
1347
+ function escapeRegex(value) {
1348
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1349
+ }
1350
+
1351
+ // src/install/apply.ts
1352
+ var execFileAsync = promisify(execFile);
1353
+ async function applyInstallPlan(plan, sourceLock, options = {}) {
1354
+ return applyPlanTransactionally(plan, { ...options, sourceLock });
1355
+ }
1356
+ async function applyCombinedInstallPlan(plan, options = {}) {
1357
+ return applyPlanTransactionally(plan, options);
1358
+ }
1359
+ async function recoverPendingApply(targetRoot, adapter, transport = localTransport, scope = {}) {
1360
+ assertGovernedRuntimeTransportSupported(transport);
1361
+ const lock = await acquireApplyLock(targetRoot, adapter, transport, {}, scope);
1362
+ try {
1363
+ const journal = await readApplyJournal(targetRoot, adapter, transport, scope);
1364
+ if (!journal) return void 0;
1365
+ assertApplyJournalRecoveryAllowed(journal);
1366
+ if (journal.operations.some((operation) => operation.action === "plugin" || operation.action === "program" || operation.semanticPlugin)) {
1367
+ throw new Error("Cannot automatically recover a journal containing semantic plugin or programmatic operations");
1368
+ }
1369
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1370
+ const entries = [];
1371
+ const startedByIndex = new Map(journal.completed.map((operation) => [operation.index, operation]));
1372
+ for (const [index, operation] of journal.operations.entries()) {
1373
+ assertOperationContained(operation, journal.targetRoot);
1374
+ const started = startedByIndex.get(index);
1375
+ if (started?.completed || started && await operationLanded(operation, transport)) {
1376
+ started.completed = true;
1377
+ await writeApplyJournal(journal, transport);
1378
+ const entry2 = await entryForCompletedOperation(operation, transport, now, journal.graphLockDigest);
1379
+ if (entry2) entries.push(entry2);
1380
+ continue;
1381
+ }
1382
+ if (operationNeedsSource(operation) && operation.sourcePath && !await localPathExists(operation.sourcePath)) {
1383
+ await rollbackStartedOperations(journal, transport);
1384
+ await removeApplyJournal(targetRoot, adapter, transport, scope);
1385
+ return void 0;
1386
+ }
1387
+ const backup = started ?? await recordBackup(operation, index, targetRoot, adapter, transport, scope);
1388
+ if (!started && isJournaledMutation(operation)) {
1389
+ journal.completed.push(backup);
1390
+ startedByIndex.set(index, backup);
1391
+ await writeApplyJournal(journal, transport);
1392
+ }
1393
+ const entry = await applyOperation(operation, { transport, now, graphLockDigest: journal.graphLockDigest });
1394
+ if (entry) entries.push(entry);
1395
+ if (isJournaledMutation(operation)) {
1396
+ backup.completed = true;
1397
+ await writeApplyJournal(journal, transport);
1398
+ }
1399
+ }
1400
+ return await commitJournalState(journal, transport, journal.mode === "uninstall" ? void 0 : entries, now);
1401
+ } finally {
1402
+ await lock.release();
1403
+ }
1404
+ }
1405
+ async function applyPlanTransactionally(plan, options = {}) {
1406
+ const transport = options.transport ?? localTransport;
1407
+ const scope = { installationType: plan.installationType, stateKey: plan.stateKey };
1408
+ if (plan.hasBlockingChanges) {
1409
+ const blockers = plan.operations.filter((operation) => operation.action === "drift" || operation.action === "conflict");
1410
+ throw new Error(`Refusing to apply with blocking changes: ${blockers.map((item) => item.relativeDestPath).join(", ")}`);
1411
+ }
1412
+ assertGovernedRuntimeTransportSupported(transport);
1413
+ const lock = await acquireApplyLock(plan.targetRoot, plan.adapter, transport, options.lock, scope);
1414
+ try {
1415
+ await assertBaseRevision(plan, transport);
1416
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1417
+ const graphLockDigest = options.graphLockDigest ?? plan.graphLockDigest;
1418
+ const mutation = mutationMetadataForApplyJournal();
1419
+ const journal = {
1420
+ version: mutation ? 2 : 1,
1421
+ ...mutation ? { mutation } : {},
1422
+ adapter: plan.adapter,
1423
+ installationType: plan.installationType,
1424
+ stateKey: plan.stateKey,
1425
+ targetRoot: plan.targetRoot,
1426
+ baseRevision: plan.baseRevision,
1427
+ graphLockDigest,
1428
+ createdAt: now,
1429
+ updatedAt: now,
1430
+ operations: plan.operations,
1431
+ completed: [],
1432
+ manifest: {
1433
+ version: 2,
1434
+ adapter: plan.adapter,
1435
+ installationType: plan.installationType,
1436
+ stateKey: plan.stateKey,
1437
+ targetRoot: plan.targetRoot,
1438
+ generatedAt: now,
1439
+ revision: "pending-apply-0000",
1440
+ legacy: false,
1441
+ adapterCode: plan.adapterCode,
1442
+ entries: []
1443
+ },
1444
+ sourceLock: options.sourceLock,
1445
+ graphLockPath: options.graphLock?.path,
1446
+ graphLock: options.graphLock?.lock
1447
+ };
1448
+ await writeApplyJournal(journal, transport);
1449
+ const entries = [];
1450
+ for (const [index, operation] of plan.operations.entries()) {
1451
+ assertOperationContained(operation, plan.targetRoot);
1452
+ const backup = isJournaledMutation(operation) ? await recordBackup(operation, index, plan.targetRoot, plan.adapter, transport, scope) : void 0;
1453
+ if (backup) {
1454
+ journal.completed.push(backup);
1455
+ await writeApplyJournal(journal, transport);
1456
+ }
1457
+ const entry = await applyOperation(operation, {
1458
+ transport,
1459
+ now,
1460
+ executePlugins: options.executePlugins,
1461
+ graphLockDigest,
1462
+ plan
1463
+ });
1464
+ if (entry) entries.push(entry);
1465
+ if (backup) {
1466
+ backup.completed = true;
1467
+ await writeApplyJournal(journal, transport);
1468
+ }
1469
+ }
1470
+ return await commitJournalState(journal, transport, entries, now);
1471
+ } finally {
1472
+ await lock.release();
1473
+ }
1474
+ }
1475
+ async function uninstall(plan, options = {}) {
1476
+ const resolvedOptions = typeof options === "boolean" ? { dryRun: options } : options;
1477
+ const transport = resolvedOptions.transport ?? localTransport;
1478
+ const scope = { installationType: plan.installationType, stateKey: plan.stateKey };
1479
+ if (resolvedOptions.keepFiles && resolvedOptions.force) {
1480
+ throw new Error("--keep-files cannot be combined with --force.");
1481
+ }
1482
+ if (plan.hasBlockingChanges) {
1483
+ const blockers = plan.operations.filter((operation) => operation.action === "conflict");
1484
+ throw new Error(`Refusing to uninstall with blocking changes: ${blockers.map((item) => item.relativeDestPath).join(", ")}`);
1485
+ }
1486
+ const removable = plan.operations.filter((operation) => operation.action === "remove" || resolvedOptions.force && isForceRemovableKeep(operation)).map((operation) => operation.action === "keep" ? { ...operation, action: "remove", overrideDrift: true, reason: `${operation.reason}; force removing drifted managed file` } : operation);
1487
+ const kept = plan.operations.filter((operation) => operation.action === "keep" && (!resolvedOptions.force || !isForceRemovableKeep(operation)));
1488
+ const skipped = plan.operations.filter((operation) => operation.action === "skip");
1489
+ const removedDrifted = resolvedOptions.force ? plan.operations.filter((operation) => operation.action === "keep" && isForceRemovableKeep(operation)).length : 0;
1490
+ if (resolvedOptions.dryRun) return { removed: resolvedOptions.keepFiles ? 0 : removable.length, kept: kept.length, removedDrifted };
1491
+ assertGovernedRuntimeTransportSupported(transport);
1492
+ const preservedKept = resolvedOptions.keepFiles ? kept.filter((operation) => shouldPreserveKeptOperationWhenKeepingFiles(operation)) : kept;
1493
+ const preserved = [...preservedKept, ...skipped].filter((operation) => operation.preserveInManifest !== false);
1494
+ for (const operation of [...removable, ...preserved]) assertOperationContained(operation, plan.targetRoot);
1495
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1496
+ const finalManifest = withManifestRevision({
1497
+ version: 2,
1498
+ adapter: plan.adapter,
1499
+ installationType: plan.installationType,
1500
+ stateKey: plan.stateKey,
1501
+ targetRoot: plan.targetRoot,
1502
+ generatedAt: now,
1503
+ revision: "pending-uninstall-0",
1504
+ legacy: false,
1505
+ adapterCode: plan.adapterCode,
1506
+ entries: preserved.map((operation) => {
1507
+ if (!operation.manifestHash || !operation.desiredHash) {
1508
+ throw new Error(`Invalid preserved operation missing manifest/source hash: ${operation.relativeDestPath}`);
1509
+ }
1510
+ return manifestEntryForOperation(operation, {
1511
+ now,
1512
+ hash: operation.manifestHash,
1513
+ sourceHash: operation.desiredHash,
1514
+ graphLockDigest: operation.graphLockDigest ?? plan.graphLockDigest
1515
+ });
1516
+ }).sort((a, b) => a.path.localeCompare(b.path))
1517
+ });
1518
+ const lock = await acquireApplyLock(plan.targetRoot, plan.adapter, transport, resolvedOptions.lock, scope);
1519
+ try {
1520
+ await assertBaseRevision(plan, transport);
1521
+ await assertExactMergeRemovalPreconditions(removable, transport);
1522
+ const mutation = mutationMetadataForApplyJournal();
1523
+ const journal = {
1524
+ version: mutation ? 2 : 1,
1525
+ ...mutation ? { mutation } : {},
1526
+ mode: "uninstall",
1527
+ adapter: plan.adapter,
1528
+ installationType: plan.installationType,
1529
+ stateKey: plan.stateKey,
1530
+ targetRoot: plan.targetRoot,
1531
+ baseRevision: plan.baseRevision,
1532
+ graphLockDigest: plan.graphLockDigest,
1533
+ createdAt: now,
1534
+ updatedAt: now,
1535
+ operations: resolvedOptions.keepFiles ? [] : removable,
1536
+ completed: [],
1537
+ manifest: finalManifest,
1538
+ graphLockPath: resolvedOptions.graphLock?.path,
1539
+ graphLock: resolvedOptions.graphLock?.lock,
1540
+ graphLockRemovePath: resolvedOptions.removeGraphLockPath,
1541
+ workspaceConfigPath: resolvedOptions.workspaceConfig?.path,
1542
+ workspaceConfig: resolvedOptions.workspaceConfig?.data
1543
+ };
1544
+ await writeApplyJournal(journal, transport);
1545
+ for (const [index, operation] of (resolvedOptions.keepFiles ? [] : removable).entries()) {
1546
+ const backup = await recordBackup(operation, index, plan.targetRoot, plan.adapter, transport, scope);
1547
+ journal.completed.push(backup);
1548
+ await writeApplyJournal(journal, transport);
1549
+ await applyOperation(operation, { transport, now, graphLockDigest: plan.graphLockDigest });
1550
+ backup.completed = true;
1551
+ await writeApplyJournal(journal, transport);
1552
+ }
1553
+ await commitJournalState(journal, transport, void 0, now);
1554
+ } finally {
1555
+ await lock.release();
1556
+ }
1557
+ return { removed: resolvedOptions.keepFiles ? 0 : removable.length, kept: kept.length, removedDrifted };
1558
+ }
1559
+ function shouldPreserveKeptOperationWhenKeepingFiles(operation) {
1560
+ return operation.preserveInManifest === true;
1561
+ }
1562
+ function isForceRemovableKeep(operation) {
1563
+ return operation.action === "keep" && operation.preserveInManifest !== true;
1564
+ }
1565
+ async function commitJournalState(journal, transport, entries, now) {
1566
+ const manifest = withManifestRevision({
1567
+ ...journal.manifest,
1568
+ generatedAt: now,
1569
+ entries: entries ? entries.sort((a, b) => a.path.localeCompare(b.path)) : journal.manifest.entries
1570
+ });
1571
+ if (journal.mode === "uninstall" && manifest.entries.length === 0) {
1572
+ await removeStateFiles(journal.targetRoot, journal.adapter, transport, {
1573
+ installationType: journal.installationType,
1574
+ stateKey: journal.stateKey
1575
+ });
1576
+ } else {
1577
+ if (journal.sourceLock) await writeSourceLock(journal.targetRoot, journal.adapter, journal.sourceLock, transport, {
1578
+ installationType: journal.installationType,
1579
+ stateKey: journal.stateKey
1580
+ });
1581
+ await writeInstallManifest(manifest, transport);
1582
+ }
1583
+ if (journal.graphLockPath && journal.graphLock) await writeGraphLock(journal.graphLockPath, journal.graphLock);
1584
+ if (journal.graphLockRemovePath) {
1585
+ declareMutationPath(journal.graphLockRemovePath);
1586
+ await rm2(journal.graphLockRemovePath, { force: true });
1587
+ }
1588
+ if (journal.workspaceConfigPath && journal.workspaceConfig) {
1589
+ declareMutationPath(journal.workspaceConfigPath);
1590
+ await writeJsonAtomic(journal.workspaceConfigPath, journal.workspaceConfig);
1591
+ }
1592
+ const verifiedManifest = await assertCommittedJournalState(journal, manifest, transport);
1593
+ await removeApplyJournal(journal.targetRoot, journal.adapter, transport, {
1594
+ installationType: journal.installationType,
1595
+ stateKey: journal.stateKey
1596
+ });
1597
+ if (await readApplyJournal(journal.targetRoot, journal.adapter, transport, {
1598
+ installationType: journal.installationType,
1599
+ stateKey: journal.stateKey
1600
+ })) {
1601
+ throw new Error("Agentwheel apply journal remained after verified state commit.");
1602
+ }
1603
+ return verifiedManifest ?? manifest;
1604
+ }
1605
+ async function assertCommittedJournalState(journal, expectedManifest, transport) {
1606
+ const scope = { installationType: journal.installationType, stateKey: journal.stateKey };
1607
+ const actualManifest = await readInstallManifest(journal.targetRoot, journal.adapter, transport, scope);
1608
+ if (journal.mode === "uninstall" && expectedManifest.entries.length === 0) {
1609
+ if (actualManifest) throw new Error("Uninstall postcheck found an install manifest that should have been removed.");
1610
+ } else if (!actualManifest) {
1611
+ throw new Error(
1612
+ `Install manifest postcheck failed: expected ${expectedManifest.revision}, found missing.`
1613
+ );
1614
+ } else if (canonicalInstallManifestJson(actualManifest) !== canonicalInstallManifestJson(expectedManifest)) {
1615
+ throw new Error("Install manifest postcheck found content that differs from the verified apply result.");
1616
+ }
1617
+ for (const operation of journal.operations.filter(isJournaledMutation)) {
1618
+ if (!await operationLanded(operation, transport)) {
1619
+ throw new Error(`Runtime postcheck failed for ${operation.relativeDestPath}.`);
1620
+ }
1621
+ }
1622
+ if (journal.graphLockPath && journal.graphLock) {
1623
+ const actual = await readGraphLock(journal.graphLockPath);
1624
+ if (canonicalGraphLockJson(actual) !== canonicalGraphLockJson(journal.graphLock)) {
1625
+ throw new Error(`Graph-lock postcheck failed for ${journal.graphLockPath}.`);
1626
+ }
1627
+ }
1628
+ if (journal.graphLockRemovePath && await pathExists(journal.graphLockRemovePath)) {
1629
+ throw new Error(`Graph-lock removal postcheck failed for ${journal.graphLockRemovePath}.`);
1630
+ }
1631
+ if (journal.workspaceConfigPath && journal.workspaceConfig) {
1632
+ const actual = JSON.parse(await readFile8(journal.workspaceConfigPath, "utf8"));
1633
+ if (canonicalJson2(actual) !== canonicalJson2(journal.workspaceConfig)) {
1634
+ throw new Error(`Workspace config postcheck failed for ${journal.workspaceConfigPath}.`);
1635
+ }
1636
+ }
1637
+ return actualManifest;
1638
+ }
1639
+ function canonicalJson2(value) {
1640
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson2(item === void 0 ? null : item)).join(",")}]`;
1641
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1642
+ const record = value;
1643
+ return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record[key])}`).join(",")}}`;
1644
+ }
1645
+ async function applyOperation(operation, context) {
1646
+ const { transport, now } = context;
1647
+ if (operation.action === "plugin") {
1648
+ if (!operation.desiredHash) {
1649
+ throw new Error(`Invalid plugin operation missing hash: ${operation.relativeDestPath}`);
1650
+ }
1651
+ if (context.executePlugins) {
1652
+ await executePluginInstall(operation, transport);
1653
+ }
1654
+ return manifestEntryForOperation(operation, {
1655
+ now,
1656
+ hash: operation.desiredHash,
1657
+ sourceHash: operation.desiredHash,
1658
+ graphLockDigest: context.graphLockDigest,
1659
+ executed: context.executePlugins === true
1660
+ });
1661
+ }
1662
+ if (operation.action === "program") {
1663
+ if (!context.plan?.adapterCode || !operation.desiredHash || !operation.programmaticOperation) {
1664
+ throw new Error(`Invalid programmatic operation: ${operation.relativeDestPath}`);
1665
+ }
1666
+ if (operation.programmaticApply) {
1667
+ if (transport.kind !== "local") {
1668
+ throw new Error(`Cannot execute programmatic adapter operation over ${transport.description}.`);
1669
+ }
1670
+ await operation.programmaticApply(operation.programmaticOperation, {
1671
+ targetRoot: context.plan.targetRoot,
1672
+ adapterName: context.plan.adapter
1673
+ });
1674
+ }
1675
+ return manifestEntryForOperation(operation, {
1676
+ now,
1677
+ hash: operation.desiredHash,
1678
+ sourceHash: operation.desiredHash,
1679
+ graphLockDigest: context.graphLockDigest
1680
+ });
1681
+ }
1682
+ if (operation.action === "create" || operation.action === "update") {
1683
+ if (!operation.sourcePath || !operation.desiredHash) {
1684
+ throw new Error(`Invalid operation missing source/hash: ${operation.relativeDestPath}`);
1685
+ }
1686
+ if (operation.mode === managedInstructionBlockMode) {
1687
+ const selector = managedInstructionSelector(operation.logicalSelector, operation.artifactType, operation.artifactName);
1688
+ const hash2 = await writeManagedInstructionBlock(operation.sourcePath, operation.destPath, selector, transport, managedBlockMutationOptions(operation));
1689
+ if (hash2 !== operation.desiredHash) {
1690
+ throw new Error(`Managed block hash verification failed for ${operation.relativeDestPath}: expected ${operation.desiredHash}, got ${hash2}`);
1691
+ }
1692
+ return manifestEntryForOperation(operation, {
1693
+ now,
1694
+ hash: hash2,
1695
+ sourceHash: operation.desiredHash,
1696
+ graphLockDigest: context.graphLockDigest
1697
+ });
1698
+ }
1699
+ if (operation.mergeStrategy === "json-deep") {
1700
+ await mergeWithTransport(operation.sourcePath, operation.destPath, transport, mergeJsonFile);
1701
+ } else if (operation.mergeStrategy === "openclaw-json-deep") {
1702
+ await mergeOpenClawJsonWithTransport(operation.sourcePath, operation.destPath, transport);
1703
+ } else if (operation.mergeStrategy === "yaml-deep") {
1704
+ await mergeWithTransport(operation.sourcePath, operation.destPath, transport, mergeYamlFile);
1705
+ } else if (operation.mergeStrategy === "codex-toml-mcp") {
1706
+ await mergeWithTransport(operation.sourcePath, operation.destPath, transport, mergeCodexTomlMcp);
1707
+ } else if (operation.mergeStrategy) {
1708
+ throw new Error(`Merge strategy '${operation.mergeStrategy}' is not implemented by apply.`);
1709
+ } else {
1710
+ await transport.atomicCopy(operation.sourcePath, operation.destPath, operation.kind);
1711
+ }
1712
+ const hash = await transport.hashPath(operation.destPath);
1713
+ if (!operation.mergeStrategy && hash !== operation.desiredHash) {
1714
+ throw new Error(`Hash verification failed for ${operation.relativeDestPath}: expected ${operation.desiredHash}, got ${hash}`);
1715
+ }
1716
+ return manifestEntryForOperation(operation, {
1717
+ now,
1718
+ hash,
1719
+ sourceHash: operation.desiredHash,
1720
+ graphLockDigest: context.graphLockDigest
1721
+ });
1722
+ }
1723
+ if (operation.action === "skip") {
1724
+ if (!operation.desiredHash) {
1725
+ throw new Error(`Invalid skip operation missing hash: ${operation.relativeDestPath}`);
1726
+ }
1727
+ return manifestEntryForOperation(operation, {
1728
+ now,
1729
+ hash: (operation.mergeStrategy || operation.mode === managedInstructionBlockMode) && operation.currentHash ? operation.currentHash : operation.desiredHash,
1730
+ sourceHash: operation.desiredHash,
1731
+ graphLockDigest: context.graphLockDigest
1732
+ });
1733
+ }
1734
+ if (operation.action === "keep") {
1735
+ if (!operation.manifestHash || !operation.desiredHash) {
1736
+ throw new Error(`Invalid keep operation missing manifest/source hash: ${operation.relativeDestPath}`);
1737
+ }
1738
+ return manifestEntryForOperation(operation, {
1739
+ now,
1740
+ hash: operation.manifestHash,
1741
+ sourceHash: operation.desiredHash,
1742
+ graphLockDigest: operation.graphLockDigest ?? context.graphLockDigest
1743
+ });
1744
+ }
1745
+ if (operation.action === "remove") {
1746
+ if (operation.semanticPlugin) {
1747
+ if (operation.execute !== false) await executePluginUninstall(operation, transport);
1748
+ return void 0;
1749
+ }
1750
+ if (operation.mode === managedInstructionBlockMode) {
1751
+ const selector = managedInstructionSelector(operation.logicalSelector, operation.artifactType, operation.artifactName);
1752
+ await removeManagedInstructionBlock(operation.destPath, selector, transport, managedBlockMutationOptions(operation));
1753
+ } else if (operation.mergeStrategy) {
1754
+ if (operation.mergeCreatedDestination) {
1755
+ await transport.rm(operation.destPath);
1756
+ } else if (operation.mergeRemoval) {
1757
+ if (operation.exactMergeRemoval) {
1758
+ if (!await transport.pathExists(operation.destPath)) {
1759
+ throw new Error(`Exact MCP retirement destination is missing: ${operation.relativeDestPath}`);
1760
+ }
1761
+ assertExactMcpMergeContribution(
1762
+ operation.mergeRemoval,
1763
+ operation.mergeStrategy,
1764
+ await transport.readFile(operation.destPath)
1765
+ );
1766
+ }
1767
+ await removeMergeWithTransport(operation.destPath, operation.mergeStrategy, operation.mergeRemoval, transport);
1768
+ }
1769
+ } else {
1770
+ await transport.rm(operation.destPath);
1771
+ }
1772
+ return void 0;
1773
+ }
1774
+ return void 0;
1775
+ }
1776
+ async function executePluginInstall(operation, transport) {
1777
+ const commands = semanticInstallCommands(operation);
1778
+ if (commands.length === 0) throw new Error(`Invalid plugin operation missing command: ${operation.relativeDestPath}`);
1779
+ if (!operation.sourcePath) {
1780
+ throw new Error(`Invalid plugin operation missing source path: ${operation.relativeDestPath}`);
1781
+ }
1782
+ if (operation.semanticPlugin?.stateRoot) {
1783
+ await prepareSemanticPluginState(operation, transport);
1784
+ }
1785
+ await executeSemanticCommands(operation, commands, transport, { stageSource: operation.semanticPlugin?.stateRoot ? false : true });
1786
+ }
1787
+ async function executePluginUninstall(operation, transport) {
1788
+ const commands = operation.semanticPlugin?.uninstallCommands ?? [];
1789
+ if (commands.length === 0) throw new Error(`Invalid semantic plugin operation missing uninstall command: ${operation.relativeDestPath}`);
1790
+ try {
1791
+ await executeSemanticCommands(operation, commands, transport);
1792
+ } catch (error) {
1793
+ if (!isPluginAlreadyAbsentError(operation, error)) throw error;
1794
+ console.warn(`WARNING plugin-already-absent ${operation.relativeDestPath}: ${firstErrorLine(error)}`);
1795
+ }
1796
+ if (operation.semanticPlugin?.stateRoot) {
1797
+ await transport.rm(operation.semanticPlugin.stateRoot);
1798
+ }
1799
+ }
1800
+ async function prepareSemanticPluginState(operation, transport) {
1801
+ const spec = operation.semanticPlugin;
1802
+ if (!spec?.stateRoot) return;
1803
+ if (!operation.sourcePath) {
1804
+ throw new Error(`Invalid semantic plugin operation missing source path: ${operation.relativeDestPath}`);
1805
+ }
1806
+ if (spec.runtime === "claude") {
1807
+ await prepareClaudeMarketplace(operation, transport);
1808
+ return;
1809
+ }
1810
+ if (spec.runtime === "codex") {
1811
+ await prepareCodexMarketplace(operation, transport);
1812
+ return;
1813
+ }
1814
+ if (spec.runtime === "copilot") {
1815
+ await prepareCopilotPlugin(operation, transport);
1816
+ return;
1817
+ }
1818
+ if (spec.runtime === "hermes") {
1819
+ await prepareHermesGitShim(operation, transport);
1820
+ }
1821
+ }
1822
+ async function prepareClaudeMarketplace(operation, transport) {
1823
+ const spec = requireSemanticPluginState(operation);
1824
+ const marketplaceName = requireMarketplaceName(operation);
1825
+ const marketplaceRoot = join3(spec.stateRoot, "marketplace");
1826
+ await transport.rm(spec.stateRoot);
1827
+ await transport.atomicCopy(requireSourcePath(operation), join3(marketplaceRoot, "plugins", spec.pluginName), operation.kind);
1828
+ await transport.writeJsonAtomic(join3(marketplaceRoot, ".claude-plugin", "marketplace.json"), {
1829
+ name: marketplaceName,
1830
+ owner: { name: "Agentwheel" },
1831
+ plugins: [
1832
+ {
1833
+ name: spec.pluginName,
1834
+ source: `./plugins/${spec.pluginName}`,
1835
+ description: `Installed by Agentwheel${operation.packageName ? ` from ${operation.packageName}` : ""}`
1836
+ }
1837
+ ]
1838
+ });
1839
+ }
1840
+ async function prepareCodexMarketplace(operation, transport) {
1841
+ const spec = requireSemanticPluginState(operation);
1842
+ const marketplaceName = requireMarketplaceName(operation);
1843
+ const marketplaceRoot = join3(spec.stateRoot, "marketplace");
1844
+ await transport.rm(spec.stateRoot);
1845
+ await transport.atomicCopy(requireSourcePath(operation), join3(marketplaceRoot, "plugins", spec.pluginName), operation.kind);
1846
+ await transport.writeJsonAtomic(join3(marketplaceRoot, ".agents", "plugins", "marketplace.json"), {
1847
+ name: marketplaceName,
1848
+ interface: {
1849
+ displayName: "Agentwheel"
1850
+ },
1851
+ plugins: [
1852
+ {
1853
+ name: spec.pluginName,
1854
+ source: {
1855
+ source: "local",
1856
+ path: `./plugins/${spec.pluginName}`
1857
+ },
1858
+ policy: {
1859
+ installation: "AVAILABLE",
1860
+ authentication: "ON_INSTALL"
1861
+ },
1862
+ category: "Agentwheel"
1863
+ }
1864
+ ]
1865
+ });
1866
+ }
1867
+ async function prepareCopilotPlugin(operation, transport) {
1868
+ const spec = requireSemanticPluginState(operation);
1869
+ await transport.rm(spec.stateRoot);
1870
+ await transport.atomicCopy(requireSourcePath(operation), join3(spec.stateRoot, "plugin"), operation.kind);
1871
+ }
1872
+ async function prepareHermesGitShim(operation, transport) {
1873
+ const spec = requireSemanticPluginState(operation);
1874
+ const repoRoot = join3(spec.stateRoot, "repo");
1875
+ await transport.rm(spec.stateRoot);
1876
+ await transport.atomicCopy(requireSourcePath(operation), repoRoot, operation.kind);
1877
+ if (!transport.execFile) {
1878
+ throw new Error(`Cannot prepare Hermes plugin git shim over ${transport.description}: transport does not support remote commands.`);
1879
+ }
1880
+ await transport.execFile("git", ["init", repoRoot], { cwd: operation.destPath });
1881
+ await transport.execFile("git", ["-C", repoRoot, "add", "-A"], { cwd: operation.destPath });
1882
+ await transport.execFile("git", [
1883
+ "-C",
1884
+ repoRoot,
1885
+ "-c",
1886
+ "user.name=agentwheel",
1887
+ "-c",
1888
+ "user.email=agentwheel@example.invalid",
1889
+ "commit",
1890
+ "-m",
1891
+ `agentwheel plugin ${operation.desiredHash ?? "unknown"}`
1892
+ ], { cwd: operation.destPath });
1893
+ }
1894
+ function requireSemanticPluginState(operation) {
1895
+ const spec = operation.semanticPlugin;
1896
+ if (!spec?.stateRoot) throw new Error(`Invalid semantic plugin operation missing state root: ${operation.relativeDestPath}`);
1897
+ return spec;
1898
+ }
1899
+ function requireSourcePath(operation) {
1900
+ if (!operation.sourcePath) throw new Error(`Invalid semantic plugin operation missing source path: ${operation.relativeDestPath}`);
1901
+ return operation.sourcePath;
1902
+ }
1903
+ function requireMarketplaceName(operation) {
1904
+ const marketplaceName = operation.semanticPlugin?.marketplaceName;
1905
+ if (!marketplaceName) throw new Error(`Invalid semantic plugin operation missing marketplace name: ${operation.relativeDestPath}`);
1906
+ return marketplaceName;
1907
+ }
1908
+ async function executeSemanticCommands(operation, commands, transport, options = {}) {
1909
+ if (transport.kind === "local") {
1910
+ for (const [command, ...args] of commands) {
1911
+ if (!command) throw new Error(`Invalid semantic plugin command for ${operation.relativeDestPath}`);
1912
+ await execFileAsync(command, args);
1913
+ }
1914
+ return;
1915
+ }
1916
+ if (!transport.execFile) {
1917
+ throw new Error(`Cannot execute semantic plugin command over ${transport.description}: transport does not support remote commands.`);
1918
+ }
1919
+ if (!options.stageSource) {
1920
+ for (const [command, ...args] of commands) {
1921
+ if (!command) throw new Error(`Invalid semantic plugin command for ${operation.relativeDestPath}`);
1922
+ await transport.execFile(command, args, { cwd: operation.destPath });
1923
+ }
1924
+ return;
1925
+ }
1926
+ if (!operation.sourcePath) {
1927
+ throw new Error(`Invalid semantic plugin operation missing source path: ${operation.relativeDestPath}`);
1928
+ }
1929
+ const stagingRoot = join3(operation.destPath, ".agentwheel", "plugin-staging", `${process.pid}-${Date.now()}`);
1930
+ const remoteSourcePath = join3(stagingRoot, basename2(operation.sourcePath));
1931
+ try {
1932
+ await transport.atomicCopy(operation.sourcePath, remoteSourcePath, operation.kind);
1933
+ for (const [command, ...args] of commands) {
1934
+ if (!command) throw new Error(`Invalid semantic plugin command for ${operation.relativeDestPath}`);
1935
+ const remoteArgs = args.map((arg) => arg === operation.sourcePath ? remoteSourcePath : arg);
1936
+ await transport.execFile(command, remoteArgs, { cwd: operation.destPath });
1937
+ }
1938
+ } finally {
1939
+ await transport.rm(stagingRoot);
1940
+ }
1941
+ }
1942
+ function isPluginAlreadyAbsentError(operation, error) {
1943
+ const output = commandErrorOutput(error).toLowerCase();
1944
+ if (!output || output.includes("command not found") || output.includes("module not found")) return false;
1945
+ const pluginName = (operation.semanticPlugin?.pluginName ?? operation.artifactName).toLowerCase();
1946
+ const escapedName = escapeRegExp(pluginName);
1947
+ const namedAbsent = [
1948
+ new RegExp(`${escapedName}.{0,120}\\b(not installed|not found|does not exist|absent)\\b`, "s"),
1949
+ new RegExp(`\\b(not installed|not found|does not exist|absent)\\b.{0,120}${escapedName}`, "s"),
1950
+ new RegExp(`\\bunknown plugin\\b.{0,120}${escapedName}`, "s"),
1951
+ new RegExp(`${escapedName}.{0,120}\\bunknown plugin\\b`, "s")
1952
+ ].some((pattern) => pattern.test(output));
1953
+ const genericAbsent = /\b(no such|unknown)\s+plugins?\b/.test(output) || /\bplugins?\b.{0,80}\bnot installed\b/s.test(output);
1954
+ return namedAbsent || genericAbsent;
1955
+ }
1956
+ function commandErrorOutput(error) {
1957
+ if (typeof error === "object" && error !== null) {
1958
+ const stderr = "stderr" in error ? String(error.stderr ?? "") : "";
1959
+ if (stderr.trim()) return stderr;
1960
+ const stdout = "stdout" in error ? String(error.stdout ?? "") : "";
1961
+ if (stdout.trim()) return stdout;
1962
+ }
1963
+ return error instanceof Error ? error.message : String(error);
1964
+ }
1965
+ function firstErrorLine(error) {
1966
+ return commandErrorOutput(error).split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "plugin is already absent";
1967
+ }
1968
+ function escapeRegExp(value) {
1969
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1970
+ }
1971
+ function semanticInstallCommands(operation) {
1972
+ if (operation.semanticPlugin) return operation.semanticPlugin.installCommands;
1973
+ return operation.semanticCommand ? [operation.semanticCommand] : [];
1974
+ }
1975
+ function managedBlockMutationOptions(operation) {
1976
+ return {
1977
+ expectedHash: operation.overrideDrift ? void 0 : operation.manifestHash,
1978
+ allowDrift: operation.overrideDrift === true
1979
+ };
1980
+ }
1981
+ async function entryForCompletedOperation(operation, transport, now, graphLockDigest) {
1982
+ if (operation.action === "remove") return void 0;
1983
+ if (operation.action === "create" || operation.action === "update") {
1984
+ if (operation.mode === managedInstructionBlockMode) {
1985
+ const selector = managedInstructionSelector(operation.logicalSelector, operation.artifactType, operation.artifactName);
1986
+ const state = await readManagedInstructionBlockState(operation.destPath, selector, transport);
1987
+ return manifestEntryForOperation(operation, {
1988
+ now,
1989
+ hash: state.hash ?? requireDesiredHash(operation),
1990
+ sourceHash: requireDesiredHash(operation),
1991
+ graphLockDigest
1992
+ });
1993
+ }
1994
+ return manifestEntryForOperation(operation, {
1995
+ now,
1996
+ hash: await transport.hashPath(operation.destPath),
1997
+ sourceHash: requireDesiredHash(operation),
1998
+ graphLockDigest
1999
+ });
2000
+ }
2001
+ if (operation.action === "skip") {
2002
+ return manifestEntryForOperation(operation, {
2003
+ now,
2004
+ hash: (operation.mergeStrategy || operation.mode === managedInstructionBlockMode) && operation.currentHash ? operation.currentHash : requireDesiredHash(operation),
2005
+ sourceHash: requireDesiredHash(operation),
2006
+ graphLockDigest
2007
+ });
2008
+ }
2009
+ return void 0;
2010
+ }
2011
+ function manifestEntryForOperation(operation, values) {
2012
+ const owners = normalizeOwners(operation.owners ?? [operation.packageName ?? operation.artifactName]);
2013
+ return {
2014
+ path: operation.relativeDestPath,
2015
+ artifactType: operation.artifactType,
2016
+ artifactName: operation.artifactName,
2017
+ installName: operation.installName ?? operation.artifactName,
2018
+ logicalSelector: operation.logicalSelector ?? `${operation.artifactType}/${operation.artifactName}`,
2019
+ graphNodeId: operation.graphNodeId,
2020
+ dependencyRole: operation.dependencyRole ?? "root",
2021
+ owners,
2022
+ refCount: owners.length,
2023
+ workspaceOwner: operation.workspaceOwner ?? "workspace:unknown",
2024
+ kind: operation.kind,
2025
+ hash: values.hash,
2026
+ sourceHash: values.sourceHash,
2027
+ updatedAt: values.now,
2028
+ channel: operation.channel,
2029
+ packageName: operation.packageName,
2030
+ semanticCommand: operation.semanticCommand,
2031
+ semanticPlugin: operation.semanticPlugin,
2032
+ executed: values.executed ?? operation.execute,
2033
+ mergeStrategy: operation.mergeStrategy,
2034
+ mergeRemoval: operation.mergeRemoval,
2035
+ mergeCreatedDestination: operation.mergeCreatedDestination,
2036
+ mode: operation.mode,
2037
+ composedFrom: operation.composedFrom,
2038
+ graphLockDigest: operation.graphLockDigest ?? values.graphLockDigest
2039
+ };
2040
+ }
2041
+ async function assertBaseRevision(plan, transport) {
2042
+ const current = await readInstallManifest(plan.targetRoot, plan.adapter, transport, {
2043
+ installationType: plan.installationType,
2044
+ stateKey: plan.stateKey
2045
+ });
2046
+ const currentRevision = current?.revision ?? null;
2047
+ if (currentRevision !== plan.baseRevision) {
2048
+ throw new Error(`Install manifest changed since planning for ${plan.adapter}; replan needed`);
2049
+ }
2050
+ }
2051
+ async function assertExactMergeRemovalPreconditions(operations, transport) {
2052
+ for (const operation of operations) {
2053
+ if (!operation.exactMergeRemoval) continue;
2054
+ if (!operation.mergeStrategy || !operation.mergeRemoval) {
2055
+ throw new Error(`Invalid exact MCP retirement operation: ${operation.relativeDestPath}`);
2056
+ }
2057
+ if (!await transport.pathExists(operation.destPath)) {
2058
+ throw new Error(`Exact MCP retirement destination is missing: ${operation.relativeDestPath}`);
2059
+ }
2060
+ assertExactMcpMergeContribution(
2061
+ operation.mergeRemoval,
2062
+ operation.mergeStrategy,
2063
+ await transport.readFile(operation.destPath)
2064
+ );
2065
+ }
2066
+ }
2067
+ function isJournaledMutation(operation) {
2068
+ if (operation.semanticPlugin && (operation.action === "plugin" || operation.action === "remove")) return false;
2069
+ return operation.action === "create" || operation.action === "update" || operation.action === "remove";
2070
+ }
2071
+ function operationNeedsSource(operation) {
2072
+ return operation.action === "create" || operation.action === "update";
2073
+ }
2074
+ async function operationLanded(operation, transport) {
2075
+ if (operation.action === "remove") {
2076
+ if (operation.mode === managedInstructionBlockMode) {
2077
+ const selector = managedInstructionSelector(operation.logicalSelector, operation.artifactType, operation.artifactName);
2078
+ const state = await readManagedInstructionBlockState(operation.destPath, selector, transport);
2079
+ return !state.exists || !state.hasBlock;
2080
+ }
2081
+ if (operation.mergeStrategy) {
2082
+ const exists = await transport.pathExists(operation.destPath);
2083
+ if (operation.mergeCreatedDestination) return !exists;
2084
+ if (!operation.mergeRemoval || !exists) return true;
2085
+ return mergeContributionAbsent(
2086
+ operation.mergeRemoval,
2087
+ operation.mergeStrategy,
2088
+ await transport.readFile(operation.destPath)
2089
+ );
2090
+ }
2091
+ return !await transport.pathExists(operation.destPath);
2092
+ }
2093
+ if (operation.action !== "create" && operation.action !== "update") return false;
2094
+ if (!await transport.pathExists(operation.destPath)) return false;
2095
+ if (operation.mode === managedInstructionBlockMode) {
2096
+ const selector = managedInstructionSelector(operation.logicalSelector, operation.artifactType, operation.artifactName);
2097
+ return managedInstructionBlockLanded(operation.destPath, selector, operation.desiredHash, transport);
2098
+ }
2099
+ if (operation.mergeStrategy) {
2100
+ if (!operation.sourcePath) return false;
2101
+ try {
2102
+ await assertMergedSourceContribution(
2103
+ operation.sourcePath,
2104
+ operation.mergeStrategy,
2105
+ await transport.readFile(operation.destPath)
2106
+ );
2107
+ return true;
2108
+ } catch {
2109
+ return false;
2110
+ }
2111
+ }
2112
+ if (!operation.desiredHash) return false;
2113
+ return await transport.hashPath(operation.destPath) === operation.desiredHash;
2114
+ }
2115
+ async function rollbackStartedOperations(journal, transport) {
2116
+ if (transport.kind !== "local" && journal.completed.some((item) => item.hadExisting && !item.backupPath)) {
2117
+ throw new Error(`Cannot automatically roll back ${transport.description}: remote journal has no restorable backups; restore manually or rerun with staged sources available to finish recovery.`);
2118
+ }
2119
+ await rollbackCompletedOperations(journal.completed, transport);
2120
+ }
2121
+ function requireDesiredHash(operation) {
2122
+ if (!operation.desiredHash) throw new Error(`Invalid operation missing desired hash: ${operation.relativeDestPath}`);
2123
+ return operation.desiredHash;
2124
+ }
2125
+ async function mergeWithTransport(sourcePath, destPath, transport, merge) {
2126
+ if (transport.kind === "local") {
2127
+ await merge(sourcePath, destPath);
2128
+ return;
2129
+ }
2130
+ const tempRoot = await mkdtemp2(join3(tmpdir2(), "agentwheel-merge-"));
2131
+ const localDest = join3(tempRoot, basename2(destPath) || "merged");
2132
+ try {
2133
+ if (await transport.pathExists(destPath)) {
2134
+ await writeFile8(localDest, await transport.readFile(destPath), "utf8");
2135
+ }
2136
+ await merge(sourcePath, localDest);
2137
+ await transport.atomicCopy(localDest, destPath, "file");
2138
+ } finally {
2139
+ await rm2(tempRoot, { recursive: true, force: true });
2140
+ }
2141
+ }
2142
+ async function removeMergeWithTransport(destPath, strategy, removal, transport) {
2143
+ if (transport.kind === "local") {
2144
+ await removeMergeContribution(destPath, strategy, removal);
2145
+ return;
2146
+ }
2147
+ const tempRoot = await mkdtemp2(join3(tmpdir2(), "agentwheel-merge-remove-"));
2148
+ const localDest = join3(tempRoot, basename2(destPath) || "merged");
2149
+ try {
2150
+ await writeFile8(localDest, await transport.readFile(destPath), "utf8");
2151
+ await removeMergeContribution(localDest, strategy, removal);
2152
+ await transport.atomicCopy(localDest, destPath, "file");
2153
+ } finally {
2154
+ await rm2(tempRoot, { recursive: true, force: true });
2155
+ }
2156
+ }
2157
+ async function mergeOpenClawJsonWithTransport(sourcePath, destPath, transport) {
2158
+ const tempRoot = await mkdtemp2(join3(tmpdir2(), "agentwheel-openclaw-merge-"));
2159
+ const localDest = join3(tempRoot, basename2(destPath) || "openclaw.json");
2160
+ const validationPath = transport.kind === "local" ? localDest : `${destPath}.validate-agentwheel-${process.pid}-${Date.now()}`;
2161
+ try {
2162
+ if (await transport.pathExists(destPath)) {
2163
+ await writeFile8(localDest, await transport.readFile(destPath), "utf8");
2164
+ }
2165
+ await mergeOpenClawJsonFile(sourcePath, localDest);
2166
+ if (transport.kind !== "local") {
2167
+ await transport.atomicCopy(localDest, validationPath, "file");
2168
+ }
2169
+ await validateOpenClawConfig(validationPath, destPath, transport);
2170
+ await transport.atomicCopy(localDest, destPath, "file");
2171
+ } finally {
2172
+ if (transport.kind !== "local") await transport.rm(validationPath);
2173
+ await rm2(tempRoot, { recursive: true, force: true });
2174
+ }
2175
+ }
2176
+ async function validateOpenClawConfig(configPath, destPath, transport) {
2177
+ if (!transport.execFile) {
2178
+ throw new Error(`Cannot validate OpenClaw config over ${transport.description}: transport does not support command execution.`);
2179
+ }
2180
+ const openClawHome = dirname8(destPath);
2181
+ const bundledBin = join3(openClawHome, "npm", "node_modules", ".bin", "openclaw");
2182
+ const script = String.raw`
2183
+ set -euo pipefail
2184
+ cfg=$1
2185
+ bundled_bin=$2
2186
+ if [ -x "$bundled_bin" ]; then
2187
+ bin="$bundled_bin"
2188
+ elif command -v openclaw >/dev/null 2>&1; then
2189
+ bin="openclaw"
2190
+ else
2191
+ echo "OpenClaw binary not found; cannot validate $cfg" >&2
2192
+ exit 127
2193
+ fi
2194
+ out=$(OPENCLAW_CONFIG_PATH="$cfg" "$bin" config validate --json 2>&1) || {
2195
+ printf '%s\n' "$out" >&2
2196
+ exit 1
2197
+ }
2198
+ printf '%s' "$out" | node -e 'let s=""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const data = JSON.parse(s); if (!data.valid) { console.error(JSON.stringify(data, null, 2)); process.exit(1); } });'
2199
+ `;
2200
+ await transport.execFile("bash", ["-lc", script, "agentwheel-openclaw-validate", configPath, bundledBin]);
2201
+ }
2202
+
2203
+ export {
2204
+ normalizeImmutableCacheIdentity,
2205
+ readGraphLock,
2206
+ canonicalGraphLockJson,
2207
+ canonicalizeGraphLock,
2208
+ computeTargetFingerprint,
2209
+ legacyUnownedWorkspaceOwner,
2210
+ installManifestSchema,
2211
+ readInstallManifest,
2212
+ listInstallManifests,
2213
+ writeInstallManifest,
2214
+ normalizeTargetRoot,
2215
+ withManifestRevision,
2216
+ computeManifestRevision,
2217
+ assertSafeInstallName,
2218
+ assertOperationContained,
2219
+ MergeAdoptionMismatchError,
2220
+ assertExactMergeContribution,
2221
+ combineMergeRemovals,
2222
+ hasMergeRemovalContent,
2223
+ mergeRemovalForInstall,
2224
+ normalizeOwners,
2225
+ managedInstructionBlockMode,
2226
+ desiredManagedInstructionBlockHash,
2227
+ readManagedInstructionBlockState,
2228
+ managedInstructionPhysicalKey,
2229
+ claudeInstructionBridgesAgents,
2230
+ managedInstructionSelector,
2231
+ applyInstallPlan,
2232
+ applyCombinedInstallPlan,
2233
+ recoverPendingApply,
2234
+ uninstall
2235
+ };