superpowers-manager 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +42 -26
  2. package/dist/adapter-result.js +5 -9
  3. package/dist/atomic.js +4 -14
  4. package/dist/cli-arguments.js +1 -27
  5. package/dist/cli.js +25 -40
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +25 -105
  8. package/dist/commands/pin.js +1 -1
  9. package/dist/commands/prepare.js +37 -81
  10. package/dist/commands/probe.js +35 -262
  11. package/dist/commands/track-latest.js +1 -1
  12. package/dist/commands/uninstall.js +15 -91
  13. package/dist/commands/update.js +5 -25
  14. package/dist/effective-selection.js +4 -6
  15. package/dist/git-tree.js +85 -0
  16. package/dist/harness-command-result.js +47 -0
  17. package/dist/harnesses/codex/adapter.js +12 -41
  18. package/dist/harnesses/codex/conflicts.js +2 -3
  19. package/dist/harnesses/codex/hooks.js +1 -1
  20. package/dist/harnesses/codex/json.js +1 -11
  21. package/dist/harnesses/codex/lifecycle.js +0 -48
  22. package/dist/harnesses/codex/manifest-overlay.js +1 -1
  23. package/dist/harnesses/codex/paths.js +14 -29
  24. package/dist/harnesses/codex/presentation.js +2 -23
  25. package/dist/harnesses/codex/publication.js +0 -6
  26. package/dist/harnesses/codex/recovery.js +5 -5
  27. package/dist/harnesses/codex/stored-state.js +1 -1
  28. package/dist/harnesses/opencode/config.js +256 -0
  29. package/dist/harnesses/opencode/discovery.js +587 -0
  30. package/dist/harnesses/opencode/harness.js +37 -0
  31. package/dist/harnesses/opencode/install.js +601 -0
  32. package/dist/harnesses/opencode/native.js +57 -0
  33. package/dist/harnesses/opencode/package.js +89 -0
  34. package/dist/harnesses/opencode/paths.js +38 -0
  35. package/dist/harnesses/opencode/prepare.js +30 -0
  36. package/dist/harnesses/opencode/presentation.js +28 -0
  37. package/dist/harnesses/opencode/state.js +174 -0
  38. package/dist/harnesses/pi/compatibility.js +5 -37
  39. package/dist/harnesses/pi/install.js +7 -5
  40. package/dist/harnesses/pi/native.js +3 -47
  41. package/dist/harnesses/pi/package.js +5 -107
  42. package/dist/harnesses/pi/prepare.js +13 -104
  43. package/dist/harnesses/pi/presentation.js +23 -66
  44. package/dist/harnesses/pi/shared-skills.js +28 -39
  45. package/dist/harnesses/pi/state.js +7 -27
  46. package/dist/provenance.js +2 -2
  47. package/dist/safe-path.js +2 -2
  48. package/dist/selection-store.js +2 -22
  49. package/dist/snapshot-package.js +78 -0
  50. package/dist/snapshot-prepare.js +99 -0
  51. package/dist/snapshot-presentation.js +44 -0
  52. package/dist/validator.js +4 -1
  53. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  54. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  55. package/node_modules/jsonc-parser/README.md +364 -0
  56. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  57. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  58. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  59. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  60. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  61. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  62. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  63. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  64. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  65. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  66. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  67. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  68. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  69. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  70. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  71. package/node_modules/jsonc-parser/package.json +37 -0
  72. package/package.json +8 -4
  73. package/dist/domain/fingerprint.js +0 -4
  74. package/dist/status.js +0 -24
  75. package/dist/validate-generated-plugin-cli.js +0 -118
@@ -0,0 +1,601 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { cp, lstat, mkdir, open, readFile, readdir, rm, rmdir, unlink, } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { findNodeAtLocation } from "jsonc-parser";
5
+ import { failureResult, successResult, } from "../../adapter-result.js";
6
+ import { digestArtifactTree } from "../../artifact-tree.js";
7
+ import { atomicWriteFile, beginDirectoryPublication, } from "../../atomic.js";
8
+ import { assertNoFollowType, canonicalizeProspectivePath, classifyPathNoFollow, } from "../../safe-path.js";
9
+ import { removeObservedOpenCodeEntry } from "./config.js";
10
+ import { inspectOpenCodeDiscovery, OPEN_CODE_PURE_MODE_INPUT, } from "./discovery.js";
11
+ import { normalizeSnapshotRuntimeVersion } from "../../harness-command-result.js";
12
+ import { runOpenCode } from "./native.js";
13
+ import { readOpenCodePackageAssessment, readOpenCodeReceipt, } from "./package.js";
14
+ import { assertOpenCodePreparationSeparate, openCodePaths, } from "./paths.js";
15
+ import { inspectOpenCodeControl, inspectOpenCodeOwnership, } from "./state.js";
16
+ import { displayPath } from "../../validator.js";
17
+ const DEFAULTS = {
18
+ run: runOpenCode,
19
+ beginPublication: beginDirectoryPublication,
20
+ };
21
+ // The journal can contain data from two independently bounded 1 MiB receipts
22
+ // plus one bounded 1 MiB config. Six bytes of JSON output per input byte covers
23
+ // control escaping; counting the registration spec independently brings that
24
+ // conservative subtotal to 24 MiB. The remaining 8 MiB bounds the actual
25
+ // filesystem paths and fixed hashes/metadata without admitting an unbounded
26
+ // recovery record.
27
+ const MAX_JOURNAL_BYTES = 32 * 1024 * 1024;
28
+ function fail(code, message) {
29
+ return failureResult("opencode-lifecycle", code, displayPath(message), [], []);
30
+ }
31
+ function accepted(result) {
32
+ if (result.status !== 0 || !result.outcome.ok)
33
+ throw new Error("OpenCode operation did not succeed");
34
+ return result.outcome.result;
35
+ }
36
+ function sameIdentity(left, right) {
37
+ return left.dev === right.dev && left.ino === right.ino;
38
+ }
39
+ async function identity(path) {
40
+ await assertNoFollowType(path, ["directory"]);
41
+ return await lstat(path);
42
+ }
43
+ async function requireIdentity(path, expected) {
44
+ if (!sameIdentity(await identity(path), expected))
45
+ throw new Error("OpenCode directory changed");
46
+ }
47
+ function journalPath(pending) {
48
+ return join(pending.paths.recoveryRoot, "transaction.json");
49
+ }
50
+ function journalBytes(journal) {
51
+ const bytes = Buffer.from(`${JSON.stringify(journal)}\n`);
52
+ if (bytes.length > MAX_JOURNAL_BYTES)
53
+ throw new Error("OpenCode recovery journal exceeds its byte limit");
54
+ return bytes;
55
+ }
56
+ async function validatePaths(paths) {
57
+ await assertOpenCodePreparationSeparate(paths);
58
+ await assertNoFollowType(paths.configRoot, ["directory", "missing"]);
59
+ await assertNoFollowType(paths.managerRoot, ["directory", "missing"]);
60
+ await assertNoFollowType(paths.installedRoot, ["directory", "missing"]);
61
+ return await canonicalizeProspectivePath(paths.installedRoot);
62
+ }
63
+ async function requireNoRecovery(paths) {
64
+ await assertNoFollowType(paths.recoveryRoot, ["missing"]);
65
+ }
66
+ async function hasRecovery(paths) {
67
+ try {
68
+ return (await classifyPathNoFollow(paths.recoveryRoot)) !== "missing";
69
+ }
70
+ catch {
71
+ return true;
72
+ }
73
+ }
74
+ async function syncDirectoryStrict(path) {
75
+ const directory = await open(path, "r");
76
+ try {
77
+ await directory.sync();
78
+ }
79
+ finally {
80
+ await directory.close();
81
+ }
82
+ }
83
+ function artifactSame(left, right) {
84
+ return left === null
85
+ ? right === null
86
+ : right !== null &&
87
+ left.binding === right.binding &&
88
+ left.digest === right.digest &&
89
+ left.commit === right.commit &&
90
+ left.source === right.source;
91
+ }
92
+ function snapshotEvidence(receipt) {
93
+ return receipt === null
94
+ ? null
95
+ : {
96
+ binding: receipt.binding,
97
+ digest: receipt.digest,
98
+ commit: receipt.commit,
99
+ source: receipt.source,
100
+ };
101
+ }
102
+ async function snapshot(root) {
103
+ if ((await assertNoFollowType(root, ["directory", "missing"])) === "missing")
104
+ return null;
105
+ const receipt = await readOpenCodeReceipt(root);
106
+ if (receipt.digest !== (await digestArtifactTree(root)))
107
+ throw new Error("OpenCode snapshot changed");
108
+ return receipt;
109
+ }
110
+ async function requireSnapshot(root, expected) {
111
+ if (!artifactSame(await snapshot(root), expected))
112
+ throw new Error("OpenCode artifact changed");
113
+ }
114
+ async function discovery(paths, ctx) {
115
+ const observed = await inspectOpenCodeDiscovery(paths, ctx.env ?? {}, process.cwd());
116
+ if (observed.managedEntries.length > 1)
117
+ throw new Error("ambiguous OpenCode Manager registration");
118
+ return observed;
119
+ }
120
+ function registrationRecord(registration) {
121
+ if (registration === undefined)
122
+ return null;
123
+ const observation = registration.observation;
124
+ const node = findNodeAtLocation(observation.document.root, [
125
+ "plugin",
126
+ registration.entry.index,
127
+ ]);
128
+ if (node === undefined)
129
+ throw new Error("OpenCode registration syntax changed");
130
+ return {
131
+ configPath: observation.document.path,
132
+ entryIndex: registration.entry.index,
133
+ spec: registration.entry.spec,
134
+ optionsDigest: createHash("sha256")
135
+ .update(JSON.stringify(registration.entry.options))
136
+ .digest("hex"),
137
+ observedBytesDigest: createHash("sha256")
138
+ .update(observation.bytes)
139
+ .digest("hex"),
140
+ observedDev: observation.identity.dev,
141
+ observedIno: observation.identity.ino,
142
+ observedMode: observation.identity.mode,
143
+ rawEntry: observation.document.text.slice(node.offset, node.offset + node.length),
144
+ };
145
+ }
146
+ function sameRegistration(registration, expected) {
147
+ return expected === null
148
+ ? registration === undefined
149
+ : registration !== undefined &&
150
+ registration.observation.document.path === expected.configPath &&
151
+ registration.entry.spec === expected.spec &&
152
+ createHash("sha256")
153
+ .update(JSON.stringify(registration.entry.options))
154
+ .digest("hex") === expected.optionsDigest;
155
+ }
156
+ function registrationInputSame(input, current) {
157
+ return input === null
158
+ ? current === null
159
+ : current !== null &&
160
+ input.entryIndex === current.entryIndex &&
161
+ input.spec === current.spec &&
162
+ input.observation.document.path === current.observation.document.path &&
163
+ input.observation.identity.dev === current.observation.identity.dev &&
164
+ input.observation.identity.ino === current.observation.identity.ino &&
165
+ input.observation.identity.mode === current.observation.identity.mode &&
166
+ input.observation.bytes.equals(current.observation.bytes);
167
+ }
168
+ function requireSafeActivationDiscovery(observed) {
169
+ if (observed.conflicts.length > 0 || observed.blockedInputs.length > 0)
170
+ throw new Error("OpenCode configuration changed");
171
+ }
172
+ async function requireStableActivationRegistration(paths, ctx, expected) {
173
+ const observed = await discovery(paths, ctx);
174
+ requireSafeActivationDiscovery(observed);
175
+ if (!sameRegistration(observed.managedEntries[0], expected))
176
+ throw new Error("OpenCode registration changed");
177
+ return observed;
178
+ }
179
+ function requireSafeRemovalDiscovery(observed) {
180
+ const unresolvedSkillInput = observed.blockedInputs.some((input) => input !== OPEN_CODE_PURE_MODE_INPUT);
181
+ if (observed.registrationUncertain ||
182
+ observed.ownedActivationAliases.length !== 0 ||
183
+ unresolvedSkillInput)
184
+ throw new Error("OpenCode registration removal is uncertain");
185
+ }
186
+ function requireNoOwnedActivation(observed) {
187
+ requireSafeRemovalDiscovery(observed);
188
+ if (observed.managedEntries.length !== 0)
189
+ throw new Error("OpenCode registration removal is uncertain");
190
+ }
191
+ async function requireActivationEligibility(ctx) {
192
+ const ownership = accepted(await inspectOpenCodeOwnership(ctx));
193
+ const control = accepted(await inspectOpenCodeControl(ctx));
194
+ if (ownership.installEligibility.kind !== "allowed" ||
195
+ control.mutationEligibility.kind !== "allowed")
196
+ throw new Error("OpenCode ownership or control blocked");
197
+ }
198
+ async function requireJournal(pending) {
199
+ if ((await validatePaths(pending.paths)) !== pending.canonicalRoot ||
200
+ pending.journal.installedRoot !== pending.canonicalRoot ||
201
+ !/^[a-f0-9]{32}$/u.test(pending.journal.token))
202
+ throw new Error("OpenCode recovery root changed");
203
+ await requireIdentity(pending.paths.recoveryRoot, pending.recoveryIdentity);
204
+ const path = journalPath(pending);
205
+ await assertNoFollowType(path, ["regular-file"]);
206
+ const observed = await lstat(path);
207
+ if (!sameIdentity(observed, pending.journalIdentity) ||
208
+ observed.size > MAX_JOURNAL_BYTES ||
209
+ !(await readFile(path)).equals(journalBytes(pending.journal)))
210
+ throw new Error("OpenCode recovery journal changed");
211
+ if (pending.backup !==
212
+ join(dirname(pending.canonicalRoot), `.${basename(pending.canonicalRoot)}.bak.${pending.journal.token}`) ||
213
+ pending.stage !==
214
+ join(dirname(pending.canonicalRoot), `.${basename(pending.canonicalRoot)}.stage.${pending.journal.token}`))
215
+ throw new Error("OpenCode recovery paths changed");
216
+ }
217
+ async function phase(pending, next, changes = {}) {
218
+ await requireJournal(pending);
219
+ const journal = { ...pending.journal, ...changes, phase: next };
220
+ const bytes = journalBytes(journal);
221
+ await atomicWriteFile(journalPath(pending), bytes, {
222
+ validate: async (temporary) => {
223
+ if (!(await readFile(temporary)).equals(bytes))
224
+ throw new Error("OpenCode journal write changed");
225
+ },
226
+ });
227
+ await syncDirectoryStrict(pending.paths.recoveryRoot);
228
+ pending.journal = journal;
229
+ pending.journalIdentity = await lstat(journalPath(pending));
230
+ }
231
+ async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration, ctx) {
232
+ const canonicalRoot = await validatePaths(paths);
233
+ await requireNoRecovery(paths);
234
+ const token = randomBytes(16).toString("hex");
235
+ const journal = {
236
+ schema: 1,
237
+ token,
238
+ phase: newArtifact === null ? "removing" : "staging",
239
+ installedRoot: canonicalRoot,
240
+ oldArtifact: snapshotEvidence(oldArtifact),
241
+ newArtifact: snapshotEvidence(newArtifact),
242
+ priorRegistration,
243
+ createdRegistration: null,
244
+ };
245
+ // Serialization and its byte bound are validated before this function
246
+ // creates any Manager or recovery path.
247
+ const bytes = journalBytes(journal);
248
+ await mkdir(paths.managerRoot, { recursive: true });
249
+ await mkdir(paths.recoveryRoot, { mode: 0o700 });
250
+ const recoveryIdentity = await identity(paths.recoveryRoot);
251
+ const path = join(paths.recoveryRoot, "transaction.json");
252
+ const handle = await open(path, "wx", 0o600);
253
+ try {
254
+ await handle.writeFile(bytes);
255
+ await handle.sync();
256
+ }
257
+ finally {
258
+ await handle.close();
259
+ }
260
+ await syncDirectoryStrict(paths.recoveryRoot);
261
+ return {
262
+ paths,
263
+ canonicalRoot,
264
+ recoveryIdentity,
265
+ backup: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.bak.${token}`),
266
+ stage: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.stage.${token}`),
267
+ ctx,
268
+ journal,
269
+ journalIdentity: await lstat(path),
270
+ settled: false,
271
+ };
272
+ }
273
+ async function retireJournal(pending) {
274
+ await requireJournal(pending);
275
+ if ((await readdir(pending.paths.recoveryRoot)).some((name) => name !== "transaction.json"))
276
+ throw new Error("unknown OpenCode recovery material");
277
+ await unlink(journalPath(pending));
278
+ await requireIdentity(pending.paths.recoveryRoot, pending.recoveryIdentity);
279
+ await rmdir(pending.paths.recoveryRoot);
280
+ }
281
+ async function recoveryGuidance(pending) {
282
+ try {
283
+ const kind = await classifyPathNoFollow(pending.paths.recoveryRoot);
284
+ if (kind === "missing")
285
+ return `recovery directory ${pending.paths.recoveryRoot} is absent; verify installed state before further mutation`;
286
+ if (kind !== "directory")
287
+ return `recovery path ${pending.paths.recoveryRoot} changed; preserve it for manual inspection`;
288
+ await requireIdentity(pending.paths.recoveryRoot, pending.recoveryIdentity);
289
+ const entries = await readdir(pending.paths.recoveryRoot);
290
+ return entries.some((name) => name !== "transaction.json")
291
+ ? `recovery directory ${pending.paths.recoveryRoot} contains unexpected material; preserve it for manual inspection`
292
+ : `preserve the recovery journal at ${journalPath(pending)} for manual inspection`;
293
+ }
294
+ catch {
295
+ return `preserve recovery state at ${pending.paths.recoveryRoot} for manual inspection`;
296
+ }
297
+ }
298
+ async function requirePublication(pending) {
299
+ await requireJournal(pending);
300
+ if (pending.publishedIdentity === undefined)
301
+ throw new Error("OpenCode publication missing");
302
+ await requireIdentity(pending.canonicalRoot, pending.publishedIdentity);
303
+ await requireSnapshot(pending.canonicalRoot, pending.journal.newArtifact);
304
+ if (pending.journal.oldArtifact === null)
305
+ await assertNoFollowType(pending.backup, ["missing"]);
306
+ else
307
+ await requireSnapshot(pending.backup, pending.journal.oldArtifact);
308
+ }
309
+ async function finalizeOpenCodePublication(pending, publication) {
310
+ if (pending.settled)
311
+ return fail("already-settled", "OpenCode installation transaction has already been settled");
312
+ pending.settled = true;
313
+ let publicationCleanupCompleted = false;
314
+ try {
315
+ await requirePublication(pending);
316
+ const expected = pending.journal.priorRegistration ?? pending.journal.createdRegistration;
317
+ await requireStableActivationRegistration(pending.paths, pending.ctx, expected);
318
+ const assessment = await readOpenCodePackageAssessment(pending.canonicalRoot);
319
+ if (assessment.compatibility.kind !== "supported" &&
320
+ assessment.compatibility.kind !== "experimental")
321
+ throw new Error("OpenCode compatibility changed");
322
+ await phase(pending, "finalizing");
323
+ await publication.finalize();
324
+ publicationCleanupCompleted = true;
325
+ await retireJournal(pending);
326
+ return successResult("finalize-opencode", null, []);
327
+ }
328
+ catch {
329
+ return fail("recovery-required", publicationCleanupCompleted
330
+ ? `OpenCode activation was verified and backup cleanup completed, but journal retirement failed; ${await recoveryGuidance(pending)}`
331
+ : `OpenCode activation or cleanup could not be verified; preserve recovery material at ${pending.paths.recoveryRoot} and ${pending.backup}`);
332
+ }
333
+ }
334
+ async function rollbackOpenCodePublication(pending, publication) {
335
+ if (pending.settled)
336
+ return fail("already-settled", "OpenCode installation transaction has already been settled");
337
+ pending.settled = true;
338
+ let restored = false;
339
+ try {
340
+ await requirePublication(pending);
341
+ let observed = await discovery(pending.paths, pending.ctx);
342
+ const expected = pending.journal.priorRegistration ?? pending.journal.createdRegistration;
343
+ if (!sameRegistration(observed.managedEntries[0], expected))
344
+ throw new Error("OpenCode registration changed");
345
+ await phase(pending, "rolling-back");
346
+ if (pending.journal.priorRegistration === null &&
347
+ pending.journal.createdRegistration !== null) {
348
+ const current = observed.managedEntries[0];
349
+ if (current === undefined)
350
+ throw new Error("OpenCode created registration missing");
351
+ await removeObservedOpenCodeEntry(current.observation, current.entry.index);
352
+ observed = await discovery(pending.paths, pending.ctx);
353
+ requireNoOwnedActivation(observed);
354
+ }
355
+ await requirePublication(pending);
356
+ await publication.rollback();
357
+ await requireSnapshot(pending.canonicalRoot, pending.journal.oldArtifact);
358
+ const after = await discovery(pending.paths, pending.ctx);
359
+ if (!sameRegistration(after.managedEntries[0], pending.journal.priorRegistration))
360
+ throw new Error("OpenCode registration restoration unverified");
361
+ if (pending.journal.priorRegistration === null)
362
+ requireNoOwnedActivation(after);
363
+ await phase(pending, "restored");
364
+ restored = true;
365
+ await retireJournal(pending);
366
+ return successResult("rollback-opencode", null, []);
367
+ }
368
+ catch {
369
+ return fail("recovery-required", restored
370
+ ? `OpenCode restoration was verified, but journal retirement failed; ${await recoveryGuidance(pending)}`
371
+ : `OpenCode restoration could not be verified; preserve the installed snapshot and recovery material at ${pending.paths.recoveryRoot} and ${pending.backup}`);
372
+ }
373
+ }
374
+ async function recoverBeforePublication(pending) {
375
+ try {
376
+ await requireJournal(pending);
377
+ await requireSnapshot(pending.canonicalRoot, pending.journal.oldArtifact);
378
+ await assertNoFollowType(pending.backup, ["missing"]);
379
+ const observed = await discovery(pending.paths, pending.ctx);
380
+ if (!sameRegistration(observed.managedEntries[0], pending.journal.priorRegistration))
381
+ return false;
382
+ if ((await assertNoFollowType(pending.stage, ["directory", "missing"])) !==
383
+ "missing") {
384
+ if (pending.stageIdentity === undefined)
385
+ return false;
386
+ await requireIdentity(pending.stage, pending.stageIdentity);
387
+ await phase(pending, "restored");
388
+ await rm(pending.stage, { recursive: true });
389
+ }
390
+ await retireJournal(pending);
391
+ return true;
392
+ }
393
+ catch {
394
+ return false;
395
+ }
396
+ }
397
+ export async function installOpenCode(artifact, ctx, deps = DEFAULTS) {
398
+ const paths = openCodePaths(ctx.env ?? {}, process.cwd());
399
+ let pending;
400
+ let publication;
401
+ try {
402
+ await validatePaths(paths);
403
+ await requireNoRecovery(paths);
404
+ if (artifact.root !== paths.preparedRoot)
405
+ throw new Error("unexpected OpenCode prepared root");
406
+ const assessment = await readOpenCodePackageAssessment(artifact.root);
407
+ if (assessment.receipt.digest !== artifact.identity ||
408
+ assessment.receipt.commit !== artifact.commit ||
409
+ (assessment.compatibility.kind !== "supported" &&
410
+ assessment.compatibility.kind !== "experimental"))
411
+ throw new Error("OpenCode prepared artifact changed");
412
+ await requireActivationEligibility(ctx);
413
+ const previous = await snapshot(paths.installedRoot);
414
+ let observed = await discovery(paths, ctx);
415
+ requireSafeActivationDiscovery(observed);
416
+ let priorRegistration = registrationRecord(observed.managedEntries[0]);
417
+ if (previous === null && priorRegistration !== null)
418
+ throw new Error("unowned OpenCode registration");
419
+ accepted(normalizeSnapshotRuntimeVersion("OpenCode", await deps.run(["--version"], paths, ctx)));
420
+ await requireSnapshot(paths.installedRoot, previous);
421
+ observed = await requireStableActivationRegistration(paths, ctx, priorRegistration);
422
+ priorRegistration = registrationRecord(observed.managedEntries[0]);
423
+ pending = await beginJournal(paths, previous, assessment.receipt, priorRegistration, ctx);
424
+ await mkdir(pending.stage);
425
+ pending.stageIdentity = await identity(pending.stage);
426
+ await cp(artifact.root, pending.stage, {
427
+ recursive: true,
428
+ verbatimSymlinks: true,
429
+ force: false,
430
+ });
431
+ const staged = await readOpenCodePackageAssessment(pending.stage);
432
+ if (!artifactSame(staged.receipt, assessment.receipt) ||
433
+ (staged.compatibility.kind !== "supported" &&
434
+ staged.compatibility.kind !== "experimental"))
435
+ throw new Error("OpenCode staged artifact changed");
436
+ await phase(pending, "publishing");
437
+ await requireIdentity(pending.stage, pending.stageIdentity);
438
+ await requireSnapshot(paths.installedRoot, previous);
439
+ observed = await requireStableActivationRegistration(paths, ctx, priorRegistration);
440
+ publication = await deps.beginPublication(pending.stage, pending.canonicalRoot, { backupPath: pending.backup });
441
+ pending.publishedIdentity = pending.stageIdentity;
442
+ await phase(pending, "published");
443
+ if (priorRegistration === null) {
444
+ await phase(pending, "registering");
445
+ observed = await requireStableActivationRegistration(paths, ctx, null);
446
+ const native = await deps
447
+ .run(["plugin", pending.canonicalRoot, "--global"], paths, ctx)
448
+ .catch(() => fail("native-failed", "OpenCode native installation failed"));
449
+ observed = await discovery(paths, ctx);
450
+ requireSafeActivationDiscovery(observed);
451
+ const created = registrationRecord(observed.managedEntries[0]);
452
+ if (created === null || created.spec !== pending.canonicalRoot)
453
+ throw new Error("OpenCode activation unverified");
454
+ await phase(pending, "registered", { createdRegistration: created });
455
+ accepted(native);
456
+ }
457
+ await requirePublication(pending);
458
+ observed = await discovery(paths, ctx);
459
+ requireSafeActivationDiscovery(observed);
460
+ const expected = priorRegistration ?? pending.journal.createdRegistration;
461
+ if (!sameRegistration(observed.managedEntries[0], expected))
462
+ throw new Error("OpenCode activation unverified");
463
+ await phase(pending, "ready");
464
+ const owned = pending;
465
+ const published = publication;
466
+ return successResult("install-opencode", {
467
+ missingVerificationOutput: {
468
+ stdout: [],
469
+ stderr: [
470
+ "error: installed OpenCode snapshot is not detectable after activation",
471
+ ],
472
+ },
473
+ mismatchVerificationOutput: {
474
+ stdout: [],
475
+ stderr: [
476
+ "error: installed OpenCode snapshot or registration does not match the prepared artifact",
477
+ ],
478
+ },
479
+ transaction: {
480
+ finalize: () => finalizeOpenCodePublication(owned, published),
481
+ rollback: () => rollbackOpenCodePublication(owned, published),
482
+ },
483
+ }, []);
484
+ }
485
+ catch {
486
+ if (pending !== undefined) {
487
+ const restored = publication === undefined
488
+ ? await recoverBeforePublication(pending)
489
+ : (await rollbackOpenCodePublication(pending, publication)).outcome
490
+ .ok;
491
+ if (restored)
492
+ return fail("activation-failed", "OpenCode activation failed; the previous snapshot and registration were restored");
493
+ }
494
+ if ((await hasRecovery(paths)) || pending !== undefined)
495
+ return fail("recovery-required", `OpenCode mutation requires manual recovery; preserve material at ${paths.recoveryRoot} and any installed snapshot or sibling backup`);
496
+ return fail("activation-refused", `cannot activate OpenCode artifact ${artifact.root}; verify prepared content, runtime, ownership, and configuration under ${paths.configRoot}`);
497
+ }
498
+ }
499
+ export async function removeOpenCode(input, ctx, deps = DEFAULTS) {
500
+ const paths = openCodePaths(ctx.env ?? {}, process.cwd());
501
+ let pending;
502
+ let deregistered = false;
503
+ let backupVerified = false;
504
+ let removalVerified = false;
505
+ try {
506
+ await validatePaths(paths);
507
+ await requireNoRecovery(paths);
508
+ const current = accepted(await inspectOpenCodeOwnership(ctx)).removalInput;
509
+ if (input.installedRoot !== paths.installedRoot ||
510
+ input.receiptDigest !== current.receiptDigest ||
511
+ !registrationInputSame(input.registration, current.registration))
512
+ throw new Error("stale OpenCode removal input");
513
+ const previous = await snapshot(paths.installedRoot);
514
+ let observed = await discovery(paths, ctx);
515
+ const registered = observed.managedEntries[0];
516
+ if (previous === null) {
517
+ if (registered !== undefined || observed.registrationUncertain)
518
+ throw new Error("unverified OpenCode registration");
519
+ return successResult("remove-opencode", null, []);
520
+ }
521
+ if (input.receiptDigest === null ||
522
+ previous.digest !== input.receiptDigest ||
523
+ !registrationInputSame(input.registration, registered === undefined
524
+ ? null
525
+ : {
526
+ observation: registered.observation,
527
+ entryIndex: registered.entry.index,
528
+ spec: registered.entry.spec,
529
+ }))
530
+ throw new Error("OpenCode ownership changed");
531
+ requireSafeRemovalDiscovery(observed);
532
+ pending = await beginJournal(paths, previous, null, registrationRecord(registered), ctx);
533
+ const installedIdentity = await identity(paths.installedRoot);
534
+ await assertNoFollowType(pending.backup, ["missing"]);
535
+ await cp(paths.installedRoot, pending.backup, {
536
+ recursive: true,
537
+ verbatimSymlinks: true,
538
+ force: false,
539
+ errorOnExist: true,
540
+ });
541
+ pending.removalBackupIdentity = await identity(pending.backup);
542
+ await requireSnapshot(pending.backup, previous);
543
+ backupVerified = true;
544
+ observed = await discovery(paths, ctx);
545
+ requireSafeRemovalDiscovery(observed);
546
+ if (!sameRegistration(observed.managedEntries[0], pending.journal.priorRegistration))
547
+ throw new Error("OpenCode registration changed");
548
+ if (registered !== undefined) {
549
+ await removeObservedOpenCodeEntry(registered.observation, registered.entry.index);
550
+ observed = await discovery(paths, ctx);
551
+ requireNoOwnedActivation(observed);
552
+ deregistered = true;
553
+ }
554
+ else {
555
+ requireNoOwnedActivation(observed);
556
+ }
557
+ await phase(pending, "deregistered");
558
+ await requireIdentity(paths.installedRoot, installedIdentity);
559
+ await requireSnapshot(paths.installedRoot, previous);
560
+ await requireIdentity(pending.backup, pending.removalBackupIdentity);
561
+ await requireSnapshot(pending.backup, previous);
562
+ observed = await discovery(paths, ctx);
563
+ requireNoOwnedActivation(observed);
564
+ await (deps.rm ?? rm)(paths.installedRoot, { recursive: true });
565
+ await requireSnapshot(paths.installedRoot, null);
566
+ const verification = accepted(await inspectOpenCodeOwnership(ctx));
567
+ if (verification.removalVerification.kind !== "allowed")
568
+ throw new Error("OpenCode desired removal state could not be verified");
569
+ removalVerified = true;
570
+ await requireIdentity(pending.backup, pending.removalBackupIdentity);
571
+ await requireSnapshot(pending.backup, previous);
572
+ await (deps.rm ?? rm)(pending.backup, { recursive: true });
573
+ await requireSnapshot(pending.backup, null);
574
+ await retireJournal(pending);
575
+ return successResult("remove-opencode", null, []);
576
+ }
577
+ catch {
578
+ let intactBackup = false;
579
+ if (pending !== undefined &&
580
+ backupVerified &&
581
+ pending.removalBackupIdentity !== undefined) {
582
+ try {
583
+ await requireIdentity(pending.backup, pending.removalBackupIdentity);
584
+ await requireSnapshot(pending.backup, pending.journal.oldArtifact);
585
+ intactBackup = true;
586
+ }
587
+ catch {
588
+ // The controlled failure below never claims unverified recovery bytes.
589
+ }
590
+ }
591
+ if (removalVerified)
592
+ return fail("cleanup-required", `OpenCode removal was verified, but backup or journal cleanup remains at ${paths.managerRoot}`);
593
+ return fail(pending !== undefined || (await hasRecovery(paths))
594
+ ? "recovery-required"
595
+ : "removal-refused", intactBackup
596
+ ? `OpenCode removal did not complete; preserve the verified prior snapshot backup at ${pending.backup} and recovery journal at ${paths.recoveryRoot}`
597
+ : deregistered
598
+ ? `OpenCode registration was removed but snapshot cleanup failed; preserve recovery material at ${paths.recoveryRoot}`
599
+ : `cannot verify OpenCode removal at ${paths.installedRoot}; preserve the snapshot and any recovery material at ${paths.recoveryRoot}`);
600
+ }
601
+ }
@@ -0,0 +1,57 @@
1
+ import { tmpdir } from "node:os";
2
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
3
+ import { mkdir } from "node:fs/promises";
4
+ import { failureResult, } from "../../adapter-result.js";
5
+ import { nativeCommandResult, } from "../../harness-command-result.js";
6
+ import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
7
+ import { withWorkspace } from "../../workspace.js";
8
+ const OPEN_CODE_EXECUTABLE = {
9
+ ...BOUNDED_EXECUTABLE,
10
+ inheritEnvironment: false,
11
+ };
12
+ function selectedExecutable(env, invocationCwd) {
13
+ const configured = env.SUPERPOWERS_OPENCODE || "opencode";
14
+ return configured.includes(sep) && !isAbsolute(configured)
15
+ ? resolve(invocationCwd, configured)
16
+ : configured;
17
+ }
18
+ export async function runOpenCode(args, paths, ctx, execute = runBoundedCommand) {
19
+ const operation = "opencode-command";
20
+ const env = ctx.env ?? {};
21
+ const executable = selectedExecutable(env, process.cwd());
22
+ let entered = false;
23
+ try {
24
+ return await withWorkspace(env.TMPDIR ?? tmpdir(), "superpowers-manager.opencode.", async (workspace) => {
25
+ entered = true;
26
+ const home = resolve(workspace, "home");
27
+ const data = resolve(workspace, "data");
28
+ const cache = resolve(workspace, "cache");
29
+ const state = resolve(workspace, "state");
30
+ const temporary = resolve(workspace, "tmp");
31
+ const managed = resolve(workspace, "managed");
32
+ await Promise.all([home, data, cache, state, temporary, managed].map((path) => mkdir(path)));
33
+ const result = await execute([executable, ...args], OPEN_CODE_EXECUTABLE, {
34
+ PATH: env.PATH,
35
+ HOME: home,
36
+ XDG_CONFIG_HOME: dirname(paths.configRoot),
37
+ XDG_DATA_HOME: data,
38
+ XDG_CACHE_HOME: cache,
39
+ XDG_STATE_HOME: state,
40
+ TMPDIR: temporary,
41
+ OPENCODE_DISABLE_AUTOUPDATE: "1",
42
+ OPENCODE_DISABLE_MODELS_FETCH: "1",
43
+ OPENCODE_DISABLE_PROJECT_CONFIG: "1",
44
+ OPENCODE_PURE: "1",
45
+ OPENCODE_TEST_MANAGED_CONFIG_DIR: managed,
46
+ GIT_CONFIG_GLOBAL: "/dev/null",
47
+ GIT_CONFIG_NOSYSTEM: "1",
48
+ }, temporary, workspace);
49
+ return nativeCommandResult("OpenCode", result);
50
+ });
51
+ }
52
+ catch {
53
+ return failureResult(operation, "workspace-failed", entered
54
+ ? "cannot complete OpenCode command in its isolated workspace"
55
+ : "cannot create an isolated OpenCode command workspace", [], []);
56
+ }
57
+ }