superpowers-manager 0.8.0 → 0.9.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 (66) hide show
  1. package/README.md +36 -16
  2. package/dist/atomic.js +1 -3
  3. package/dist/cli-arguments.js +4 -1
  4. package/dist/cli.js +14 -43
  5. package/dist/commands/adapter-call.js +24 -0
  6. package/dist/commands/install.js +16 -63
  7. package/dist/commands/mutation.js +5 -5
  8. package/dist/commands/pin.js +10 -5
  9. package/dist/commands/prepare.js +42 -108
  10. package/dist/commands/probe.js +33 -19
  11. package/dist/commands/uninstall.js +18 -46
  12. package/dist/commands/unpin.js +2 -1
  13. package/dist/commands/update.js +8 -29
  14. package/dist/domain/refs.js +0 -3
  15. package/dist/effective-selection.js +11 -15
  16. package/dist/git-tree.js +4 -6
  17. package/dist/harness-command-result.js +33 -3
  18. package/dist/harnesses/claude-code/harness.js +39 -0
  19. package/dist/harnesses/claude-code/install.js +380 -0
  20. package/dist/harnesses/claude-code/native.js +79 -0
  21. package/dist/harnesses/claude-code/paths.js +35 -0
  22. package/dist/harnesses/claude-code/prepare.js +117 -0
  23. package/dist/harnesses/claude-code/presentation.js +25 -0
  24. package/dist/harnesses/claude-code/state.js +253 -0
  25. package/dist/harnesses/codex/adapter.js +27 -68
  26. package/dist/harnesses/codex/conflicts.js +3 -8
  27. package/dist/harnesses/codex/generated-plugin.js +4 -11
  28. package/dist/harnesses/codex/harness.js +4 -26
  29. package/dist/harnesses/codex/hooks.js +11 -27
  30. package/dist/harnesses/codex/lifecycle.js +22 -107
  31. package/dist/harnesses/codex/manifest-overlay.js +9 -27
  32. package/dist/harnesses/codex/paths.js +29 -0
  33. package/dist/harnesses/codex/prepare.js +9 -43
  34. package/dist/harnesses/codex/presentation.js +1 -1
  35. package/dist/harnesses/codex/publication.js +5 -40
  36. package/dist/harnesses/codex/state.js +1 -6
  37. package/dist/harnesses/codex/stored-state.js +1 -6
  38. package/dist/harnesses/opencode/config.js +2 -4
  39. package/dist/harnesses/opencode/discovery.js +1 -5
  40. package/dist/harnesses/opencode/install.js +12 -93
  41. package/dist/harnesses/opencode/native.js +28 -49
  42. package/dist/harnesses/opencode/package.js +11 -55
  43. package/dist/harnesses/opencode/prepare.js +2 -7
  44. package/dist/harnesses/opencode/presentation.js +2 -7
  45. package/dist/harnesses/pi/install.js +31 -120
  46. package/dist/harnesses/pi/native.js +9 -34
  47. package/dist/harnesses/pi/package.js +9 -50
  48. package/dist/harnesses/pi/prepare.js +3 -8
  49. package/dist/harnesses/pi/presentation.js +2 -7
  50. package/dist/harnesses/pi/settings.js +5 -9
  51. package/dist/harnesses/pi/shared-skills.js +23 -53
  52. package/dist/harnesses/pi/state.js +3 -10
  53. package/dist/provenance.js +3 -19
  54. package/dist/resource-lock.js +11 -16
  55. package/dist/safe-path.js +28 -1
  56. package/dist/selection.js +15 -19
  57. package/dist/snapshot-journal.js +101 -0
  58. package/dist/snapshot-package.js +62 -1
  59. package/dist/snapshot-prepare.js +9 -2
  60. package/dist/snapshot-presentation.js +9 -0
  61. package/dist/upstream.js +8 -12
  62. package/dist/validator.js +26 -23
  63. package/dist/workspace.js +29 -35
  64. package/package.json +10 -9
  65. package/dist/python-json.js +0 -4
  66. package/dist/upstream-workspace.js +0 -8
@@ -1,6 +1,6 @@
1
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";
2
+ import { cp, mkdir, readdir, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
4
  import { findNodeAtLocation } from "jsonc-parser";
5
5
  import { failureResult, successResult, } from "../../adapter-result.js";
6
6
  import { digestArtifactTree } from "../../artifact-tree.js";
@@ -14,6 +14,7 @@ import { readOpenCodePackageAssessment, readOpenCodeReceipt, } from "./package.j
14
14
  import { assertOpenCodePreparationSeparate, openCodePaths, } from "./paths.js";
15
15
  import { inspectOpenCodeControl, inspectOpenCodeOwnership, } from "./state.js";
16
16
  import { displayPath } from "../../validator.js";
17
+ import { createJournal, createSnapshotJournal, hasRecovery, identity, journalPath, publicationPaths, requireNoRecovery, } from "../../snapshot-journal.js";
17
18
  const DEFAULTS = {
18
19
  run: runOpenCode,
19
20
  beginPublication: beginDirectoryPublication,
@@ -35,20 +36,6 @@ function accepted(result) {
35
36
  throw new Error("OpenCode operation did not succeed");
36
37
  return result.outcome.result;
37
38
  }
38
- function sameIdentity(left, right) {
39
- return left.dev === right.dev && left.ino === right.ino;
40
- }
41
- async function identity(path) {
42
- await assertNoFollowType(path, ["directory"]);
43
- return await lstat(path);
44
- }
45
- async function requireIdentity(path, expected) {
46
- if (!sameIdentity(await identity(path), expected))
47
- throw new Error("OpenCode directory changed");
48
- }
49
- function journalPath(pending) {
50
- return join(pending.paths.recoveryRoot, "transaction.json");
51
- }
52
39
  function journalBytes(journal) {
53
40
  const bytes = Buffer.from(`${JSON.stringify(journal)}\n`);
54
41
  if (bytes.length > MAX_JOURNAL_BYTES)
@@ -57,31 +44,14 @@ function journalBytes(journal) {
57
44
  }
58
45
  async function validatePaths(paths) {
59
46
  await assertOpenCodePreparationSeparate(paths);
60
- await assertNoFollowType(paths.configRoot, ["directory", "missing"]);
61
- await assertNoFollowType(paths.managerRoot, ["directory", "missing"]);
62
- await assertNoFollowType(paths.installedRoot, ["directory", "missing"]);
63
47
  return await canonicalizeProspectivePath(paths.installedRoot);
64
48
  }
65
- async function requireNoRecovery(paths) {
66
- await assertNoFollowType(paths.recoveryRoot, ["missing"]);
67
- }
68
- async function hasRecovery(paths) {
69
- try {
70
- return (await classifyPathNoFollow(paths.recoveryRoot)) !== "missing";
71
- }
72
- catch {
73
- return true;
74
- }
75
- }
76
- async function syncDirectoryStrict(path) {
77
- const directory = await open(path, "r");
78
- try {
79
- await directory.sync();
80
- }
81
- finally {
82
- await directory.close();
83
- }
84
- }
49
+ const { requireIdentity, requireJournal, phase, retireJournal } = createSnapshotJournal({
50
+ label: "OpenCode",
51
+ validatePaths,
52
+ serialize: journalBytes,
53
+ readCap: MAX_JOURNAL_BYTES,
54
+ });
85
55
  function artifactSame(left, right) {
86
56
  return left === null
87
57
  ? right === null
@@ -200,39 +170,6 @@ async function requireActivationEligibility(ctx) {
200
170
  control.mutationEligibility.kind !== "allowed")
201
171
  throw new Error("OpenCode ownership or control blocked");
202
172
  }
203
- async function requireJournal(pending) {
204
- if ((await validatePaths(pending.paths)) !== pending.canonicalRoot ||
205
- pending.journal.installedRoot !== pending.canonicalRoot ||
206
- !/^[a-f0-9]{32}$/u.test(pending.journal.token))
207
- throw new Error("OpenCode recovery root changed");
208
- await requireIdentity(pending.paths.recoveryRoot, pending.recoveryIdentity);
209
- const path = journalPath(pending);
210
- await assertNoFollowType(path, ["regular-file"]);
211
- const observed = await lstat(path);
212
- if (!sameIdentity(observed, pending.journalIdentity) ||
213
- observed.size > MAX_JOURNAL_BYTES ||
214
- !(await readFile(path)).equals(journalBytes(pending.journal)))
215
- throw new Error("OpenCode recovery journal changed");
216
- if (pending.backup !==
217
- join(dirname(pending.canonicalRoot), `.${basename(pending.canonicalRoot)}.bak.${pending.journal.token}`) ||
218
- pending.stage !==
219
- join(dirname(pending.canonicalRoot), `.${basename(pending.canonicalRoot)}.stage.${pending.journal.token}`))
220
- throw new Error("OpenCode recovery paths changed");
221
- }
222
- async function phase(pending, next, changes = {}) {
223
- await requireJournal(pending);
224
- const journal = { ...pending.journal, ...changes, phase: next };
225
- const bytes = journalBytes(journal);
226
- await atomicWriteFile(journalPath(pending), bytes, {
227
- validate: async (temporary) => {
228
- if (!(await readFile(temporary)).equals(bytes))
229
- throw new Error("OpenCode journal write changed");
230
- },
231
- });
232
- await syncDirectoryStrict(pending.paths.recoveryRoot);
233
- pending.journal = journal;
234
- pending.journalIdentity = await lstat(journalPath(pending));
235
- }
236
173
  async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration, ctx) {
237
174
  const canonicalRoot = await validatePaths(paths);
238
175
  await requireNoRecovery(paths);
@@ -253,36 +190,18 @@ async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration,
253
190
  await mkdir(paths.managerRoot, { recursive: true });
254
191
  await mkdir(paths.recoveryRoot, { mode: 0o700 });
255
192
  const recoveryIdentity = await identity(paths.recoveryRoot);
256
- const path = join(paths.recoveryRoot, "transaction.json");
257
- const handle = await open(path, "wx", 0o600);
258
- try {
259
- await handle.writeFile(bytes);
260
- await handle.sync();
261
- }
262
- finally {
263
- await handle.close();
264
- }
265
- await syncDirectoryStrict(paths.recoveryRoot);
193
+ const journalIdentity = await createJournal(paths.recoveryRoot, bytes);
266
194
  return {
267
195
  paths,
268
196
  canonicalRoot,
269
197
  recoveryIdentity,
270
- backup: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.bak.${token}`),
271
- stage: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.stage.${token}`),
198
+ ...publicationPaths(canonicalRoot, token),
272
199
  ctx,
273
200
  journal,
274
- journalIdentity: await lstat(path),
201
+ journalIdentity,
275
202
  settled: false,
276
203
  };
277
204
  }
278
- async function retireJournal(pending) {
279
- await requireJournal(pending);
280
- if ((await readdir(pending.paths.recoveryRoot)).some((name) => name !== "transaction.json"))
281
- throw new Error("unknown OpenCode recovery material");
282
- await unlink(journalPath(pending));
283
- await requireIdentity(pending.paths.recoveryRoot, pending.recoveryIdentity);
284
- await rmdir(pending.paths.recoveryRoot);
285
- }
286
205
  async function recoveryGuidance(pending) {
287
206
  try {
288
207
  const kind = await classifyPathNoFollow(pending.paths.recoveryRoot);
@@ -1,57 +1,36 @@
1
- import { tmpdir } from "node:os";
2
- import { dirname, isAbsolute, resolve, sep } from "node:path";
1
+ import { dirname, resolve } from "node:path";
3
2
  import { mkdir } from "node:fs/promises";
4
- import { failureResult, } from "../../adapter-result.js";
5
- import { nativeCommandResult, } from "../../harness-command-result.js";
3
+ import {} from "../../adapter-result.js";
4
+ import { runIsolatedNative, } from "../../harness-command-result.js";
6
5
  import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
7
- import { withWorkspace } from "../../workspace.js";
8
6
  const OPEN_CODE_EXECUTABLE = {
9
7
  ...BOUNDED_EXECUTABLE,
10
8
  inheritEnvironment: false,
11
9
  };
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
10
  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
- }
11
+ return await runIsolatedNative("OpenCode", (ctx.env ?? {}).SUPERPOWERS_OPENCODE || "opencode", ctx, async (executable, workspace, env) => {
12
+ const home = resolve(workspace, "home");
13
+ const data = resolve(workspace, "data");
14
+ const cache = resolve(workspace, "cache");
15
+ const state = resolve(workspace, "state");
16
+ const temporary = resolve(workspace, "tmp");
17
+ const managed = resolve(workspace, "managed");
18
+ await Promise.all([home, data, cache, state, temporary, managed].map((path) => mkdir(path)));
19
+ return await execute([executable, ...args], OPEN_CODE_EXECUTABLE, {
20
+ PATH: env.PATH,
21
+ HOME: home,
22
+ XDG_CONFIG_HOME: dirname(paths.configRoot),
23
+ XDG_DATA_HOME: data,
24
+ XDG_CACHE_HOME: cache,
25
+ XDG_STATE_HOME: state,
26
+ TMPDIR: temporary,
27
+ OPENCODE_DISABLE_AUTOUPDATE: "1",
28
+ OPENCODE_DISABLE_MODELS_FETCH: "1",
29
+ OPENCODE_DISABLE_PROJECT_CONFIG: "1",
30
+ OPENCODE_PURE: "1",
31
+ OPENCODE_TEST_MANAGED_CONFIG_DIR: managed,
32
+ GIT_CONFIG_GLOBAL: "/dev/null",
33
+ GIT_CONFIG_NOSYSTEM: "1",
34
+ }, temporary, workspace);
35
+ });
57
36
  }
@@ -1,10 +1,9 @@
1
1
  import { join } from "node:path";
2
- import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
3
- import { COMMIT_RE, SEMVER_RE } from "../../domain/refs.js";
2
+ import { readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
3
+ import { SEMVER_RE } from "../../domain/refs.js";
4
4
  import { assertNoFollowType } from "../../safe-path.js";
5
5
  import { validateSource } from "../../selection.js";
6
- import { SafetyError } from "../../safety-error.js";
7
- import { isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, snapshotReceiptBinding, } from "../../snapshot-package.js";
6
+ import { createSnapshotReceipts, isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, } from "../../snapshot-package.js";
8
7
  const BOOTSTRAP = ".opencode/plugins/superpowers.js";
9
8
  const ENTRYPOINT = "index.js";
10
9
  // Exact-byte allowlist, matched as a unit: a bootstrap and the V2 entrypoint
@@ -32,19 +31,6 @@ const QUALIFIED_PROFILES = [
32
31
  },
33
32
  },
34
33
  ];
35
- function isReceiptCompatibility(value) {
36
- if (!value || typeof value !== "object" || Array.isArray(value))
37
- return false;
38
- const compatibility = value;
39
- if (typeof compatibility.reason !== "string")
40
- return false;
41
- if (compatibility.kind === "supported" ||
42
- compatibility.kind === "experimental")
43
- return typeof compatibility.generation === "string";
44
- return ((compatibility.kind === "unknown" ||
45
- compatibility.kind === "unsupported") &&
46
- compatibility.generation === undefined);
47
- }
48
34
  export async function assessOpenCodeCompatibility(root, selection) {
49
35
  try {
50
36
  validateSource(selection.effectiveSource);
@@ -84,41 +70,11 @@ export async function assessOpenCodeCompatibility(root, selection) {
84
70
  };
85
71
  }
86
72
  }
87
- export async function readOpenCodeReceipt(root) {
88
- const path = join(root, ARTIFACT_RECEIPT);
89
- try {
90
- const value = await readArtifactObject(root, path);
91
- if (value.schema !== 1 ||
92
- value.manager !== "superpowers-manager" ||
93
- value.harness !== "opencode" ||
94
- typeof value.source !== "string" ||
95
- typeof value.commit !== "string" ||
96
- !COMMIT_RE.test(value.commit) ||
97
- typeof value.digest !== "string" ||
98
- !ARTIFACT_DIGEST_RE.test(value.digest) ||
99
- typeof value.binding !== "string" ||
100
- !ARTIFACT_DIGEST_RE.test(value.binding))
101
- throw new Error("receipt fields");
102
- validateSource(value.source);
103
- if (snapshotReceiptBinding(value) !==
104
- value.binding)
105
- throw new Error("receipt binding");
106
- if (!isReceiptCompatibility(value.compatibility))
107
- throw new Error("receipt compatibility");
108
- return value;
109
- }
110
- catch (cause) {
111
- throw new SafetyError("opencode-package", `invalid OpenCode artifact receipt: ${path}`, { cause });
112
- }
113
- }
114
- export async function readOpenCodePackageAssessment(root) {
115
- const receipt = await readOpenCodeReceipt(root);
116
- if ((await digestArtifactTree(root)) !== receipt.digest)
117
- throw new SafetyError("opencode-package", `OpenCode artifact digest mismatch: ${root}`);
118
- return {
119
- receipt,
120
- compatibility: await assessOpenCodeCompatibility(root, {
121
- effectiveSource: receipt.source,
122
- }),
123
- };
124
- }
73
+ const receipts = createSnapshotReceipts({
74
+ harness: "opencode",
75
+ label: "OpenCode",
76
+ strictGeneration: true,
77
+ assessCompatibility: assessOpenCodeCompatibility,
78
+ });
79
+ export const readOpenCodeReceipt = receipts.readReceipt;
80
+ export const readOpenCodePackageAssessment = receipts.readAssessment;
@@ -2,12 +2,6 @@ import { failureResult, successResult, } from "../../adapter-result.js";
2
2
  import { createSnapshotPreparation } from "../../snapshot-prepare.js";
3
3
  import { assertOpenCodePreparationSeparate, openCodePaths } from "./paths.js";
4
4
  import { assessOpenCodeCompatibility, readOpenCodePackageAssessment, } from "./package.js";
5
- export function openCodePreparationLocation(ctx) {
6
- return {
7
- destinationRoot: openCodePaths(ctx.env ?? {}, process.cwd()).preparedRoot,
8
- stagingLeaf: "superpowers",
9
- };
10
- }
11
5
  export async function validateOpenCodePreparationBeforeFetch(ctx) {
12
6
  try {
13
7
  const paths = openCodePaths(ctx.env ?? {}, process.cwd());
@@ -21,10 +15,11 @@ export async function validateOpenCodePreparationBeforeFetch(ctx) {
21
15
  const preparation = createSnapshotPreparation({
22
16
  harness: "opencode",
23
17
  label: "OpenCode",
24
- preparationLocation: openCodePreparationLocation,
18
+ paths: openCodePaths,
25
19
  assessCompatibility: assessOpenCodeCompatibility,
26
20
  readAssessment: readOpenCodePackageAssessment,
27
21
  });
22
+ export const openCodePreparationLocation = preparation.preparationLocation;
28
23
  export const prepareOpenCodeCandidate = preparation.prepareCandidate;
29
24
  export const inspectOpenCodePrepared = preparation.inspectPrepared;
30
25
  export const readOpenCodePrepared = preparation.readPrepared;
@@ -1,5 +1,5 @@
1
1
  import { displaySource } from "../../selection.js";
2
- import { createSnapshotPresentation } from "../../snapshot-presentation.js";
2
+ import { createSnapshotPresentation, probeOutput, } from "../../snapshot-presentation.js";
3
3
  import { displayPath } from "../../validator.js";
4
4
  const renderProbe = (facts) => {
5
5
  const fields = [
@@ -19,11 +19,6 @@ const renderProbe = (facts) => {
19
19
  ["compatibility_reason", facts.compatibility.reason],
20
20
  ["status", facts.status],
21
21
  ].map(([key, value]) => [key, displayPath(value)]);
22
- return {
23
- human: fields
24
- .map(([key, value]) => `${key.replaceAll("_", " ")}: ${value || "not present"}\n`)
25
- .join(""),
26
- porcelain: fields.map(([key, value]) => `${key}=${value}\n`).join(""),
27
- };
22
+ return probeOutput(fields);
28
23
  };
29
24
  export const openCodePresentation = createSnapshotPresentation("OpenCode", renderProbe, (input) => input.receiptDigest === null && input.registration === null);