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,89 @@
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";
4
+ import { validateSource } from "../../selection.js";
5
+ import { SafetyError } from "../../safety-error.js";
6
+ import { isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, snapshotReceiptBinding, } from "../../snapshot-package.js";
7
+ const BOOTSTRAP = ".opencode/plugins/superpowers.js";
8
+ const BOOTSTRAP_SIZE = 5464;
9
+ const BOOTSTRAP_SHA256 = "a5c5e1dbb0abfbd6ec3322b724b9a7b3318bbbb3d83f9a661c56ae0ed0a3adb8";
10
+ function isReceiptCompatibility(value) {
11
+ if (!value || typeof value !== "object" || Array.isArray(value))
12
+ return false;
13
+ const compatibility = value;
14
+ if (typeof compatibility.reason !== "string")
15
+ return false;
16
+ if (compatibility.kind === "supported" ||
17
+ compatibility.kind === "experimental")
18
+ return typeof compatibility.generation === "string";
19
+ return ((compatibility.kind === "unknown" ||
20
+ compatibility.kind === "unsupported") &&
21
+ compatibility.generation === undefined);
22
+ }
23
+ export async function assessOpenCodeCompatibility(root, selection) {
24
+ try {
25
+ validateSource(selection.effectiveSource);
26
+ const pkg = await readArtifactObject(root, join(root, "package.json"));
27
+ if (pkg.name !== "superpowers" ||
28
+ pkg.type !== "module" ||
29
+ pkg.main !== BOOTSTRAP ||
30
+ typeof pkg.version !== "string" ||
31
+ !SEMVER_RE.test(pkg.version))
32
+ throw new Error("package metadata");
33
+ requireNoSnapshotDependencies(pkg);
34
+ await validateNativeSkill(root);
35
+ await requireSnapshotBootstrap(root, BOOTSTRAP, BOOTSTRAP_SIZE, BOOTSTRAP_SHA256);
36
+ const official = isOfficialSnapshotSource(selection.effectiveSource);
37
+ return {
38
+ kind: official ? "supported" : "experimental",
39
+ generation: "opencode-native-bootstrap-v1",
40
+ reason: official
41
+ ? "qualified upstream-native OpenCode bootstrap"
42
+ : "qualified OpenCode mechanics from a custom source",
43
+ };
44
+ }
45
+ catch {
46
+ return {
47
+ kind: "unsupported",
48
+ reason: `OpenCode package does not match the qualified native bootstrap profile: ${root}`,
49
+ };
50
+ }
51
+ }
52
+ export async function readOpenCodeReceipt(root) {
53
+ const path = join(root, ARTIFACT_RECEIPT);
54
+ try {
55
+ const value = await readArtifactObject(root, path);
56
+ if (value.schema !== 1 ||
57
+ value.manager !== "superpowers-manager" ||
58
+ value.harness !== "opencode" ||
59
+ typeof value.source !== "string" ||
60
+ typeof value.commit !== "string" ||
61
+ !COMMIT_RE.test(value.commit) ||
62
+ typeof value.digest !== "string" ||
63
+ !ARTIFACT_DIGEST_RE.test(value.digest) ||
64
+ typeof value.binding !== "string" ||
65
+ !ARTIFACT_DIGEST_RE.test(value.binding))
66
+ throw new Error("receipt fields");
67
+ validateSource(value.source);
68
+ if (snapshotReceiptBinding(value) !==
69
+ value.binding)
70
+ throw new Error("receipt binding");
71
+ if (!isReceiptCompatibility(value.compatibility))
72
+ throw new Error("receipt compatibility");
73
+ return value;
74
+ }
75
+ catch (cause) {
76
+ throw new SafetyError("opencode-package", `invalid OpenCode artifact receipt: ${path}`, { cause });
77
+ }
78
+ }
79
+ export async function readOpenCodePackageAssessment(root) {
80
+ const receipt = await readOpenCodeReceipt(root);
81
+ if ((await digestArtifactTree(root)) !== receipt.digest)
82
+ throw new SafetyError("opencode-package", `OpenCode artifact digest mismatch: ${root}`);
83
+ return {
84
+ receipt,
85
+ compatibility: await assessOpenCodeCompatibility(root, {
86
+ effectiveSource: receipt.source,
87
+ }),
88
+ };
89
+ }
@@ -0,0 +1,38 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { assertNoFollowType, canonicalizeProspectivePath, isContained, } from "../../safe-path.js";
4
+ function overlaps(left, right) {
5
+ return isContained(left, right) || isContained(right, left);
6
+ }
7
+ export async function assertOpenCodePreparationSeparate(paths) {
8
+ await assertNoFollowType(paths.configRoot, ["directory", "missing"]);
9
+ await assertNoFollowType(paths.managerRoot, ["directory", "missing"]);
10
+ const roots = [paths.preparedRoot, paths.installedRoot, paths.recoveryRoot];
11
+ const canonical = await Promise.all(roots.map(async (root) => {
12
+ await assertNoFollowType(root, ["directory", "missing"]);
13
+ return await canonicalizeProspectivePath(root);
14
+ }));
15
+ if (overlaps(canonical[0], canonical[1]) ||
16
+ overlaps(canonical[0], canonical[2]) ||
17
+ overlaps(canonical[1], canonical[2]))
18
+ throw new Error("storage roots overlap");
19
+ }
20
+ export function openCodePaths(env, cwd) {
21
+ const home = env.HOME && env.HOME.length > 0 ? env.HOME : homedir();
22
+ const homeDir = resolve(cwd, home);
23
+ const configured = env.XDG_CONFIG_HOME;
24
+ if (configured !== undefined &&
25
+ configured.length > 0 &&
26
+ !isAbsolute(configured))
27
+ throw new Error("XDG_CONFIG_HOME must be absolute");
28
+ const configRoot = join(configured && configured.length > 0 ? configured : join(homeDir, ".config"), "opencode");
29
+ const managerRoot = join(configRoot, "superpowers-manager");
30
+ return {
31
+ homeDir,
32
+ configRoot,
33
+ managerRoot,
34
+ preparedRoot: join(managerRoot, "prepared"),
35
+ installedRoot: join(managerRoot, "installed"),
36
+ recoveryRoot: join(managerRoot, "recovery"),
37
+ };
38
+ }
@@ -0,0 +1,30 @@
1
+ import { failureResult, successResult, } from "../../adapter-result.js";
2
+ import { createSnapshotPreparation } from "../../snapshot-prepare.js";
3
+ import { assertOpenCodePreparationSeparate, openCodePaths } from "./paths.js";
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
+ export async function validateOpenCodePreparationBeforeFetch(ctx) {
12
+ try {
13
+ const paths = openCodePaths(ctx.env ?? {}, process.cwd());
14
+ await assertOpenCodePreparationSeparate(paths);
15
+ return successResult("prepare", null, []);
16
+ }
17
+ catch {
18
+ return failureResult("prepare", "invalid-package", "cannot validate OpenCode artifact storage", [], []);
19
+ }
20
+ }
21
+ const preparation = createSnapshotPreparation({
22
+ harness: "opencode",
23
+ label: "OpenCode",
24
+ preparationLocation: openCodePreparationLocation,
25
+ assessCompatibility: assessOpenCodeCompatibility,
26
+ readAssessment: readOpenCodePackageAssessment,
27
+ });
28
+ export const prepareOpenCodeCandidate = preparation.prepareCandidate;
29
+ export const inspectOpenCodePrepared = preparation.inspectPrepared;
30
+ export const readOpenCodePrepared = preparation.readPrepared;
@@ -0,0 +1,28 @@
1
+ import { displaySource } from "../../selection.js";
2
+ import { createSnapshotPresentation } from "../../snapshot-presentation.js";
3
+ import { displayPath } from "../../validator.js";
4
+ const renderProbe = (facts) => {
5
+ const fields = [
6
+ ["harness", "opencode"],
7
+ ["desired_commit", facts.selection.desiredCommit],
8
+ ["upstream_source_origin", facts.selection.upstreamSourceOrigin],
9
+ ["effective_source", displaySource(facts.selection.effectiveSource)],
10
+ ["prepared_identity", facts.prepared.observedIdentity],
11
+ ["installed_identity", facts.installed.observedIdentity],
12
+ ["installation_state", facts.installed.kind],
13
+ ["resource_state", facts.resourceState ?? "idle"],
14
+ ["ownership", facts.ownership.presentationValue],
15
+ ["conflicts", (facts.ownership.presentationConflicts ?? []).join("; ")],
16
+ ["update_control", facts.control.presentationValue],
17
+ ["compatibility", facts.compatibility.kind],
18
+ ["compatibility_reason", facts.compatibility.reason],
19
+ ["status", facts.status],
20
+ ].map(([key, value]) => [key, displayPath(value)]);
21
+ return {
22
+ human: fields
23
+ .map(([key, value]) => `${key.replaceAll("_", " ")}: ${value || "not present"}\n`)
24
+ .join(""),
25
+ porcelain: fields.map(([key, value]) => `${key}=${value}\n`).join(""),
26
+ };
27
+ };
28
+ export const openCodePresentation = createSnapshotPresentation("OpenCode", renderProbe, (input) => input.receiptDigest === null && input.registration === null);
@@ -0,0 +1,174 @@
1
+ import { inspectionFailure, successResult, } from "../../adapter-result.js";
2
+ import { displayPath } from "../../validator.js";
3
+ import { observeSnapshot, sameSnapshotSource, } from "../../snapshot-package.js";
4
+ import { classifyPathNoFollow } from "../../safe-path.js";
5
+ import { inspectOpenCodeDiscovery, } from "./discovery.js";
6
+ import { assessOpenCodeCompatibility, readOpenCodePackageAssessment, readOpenCodeReceipt, } from "./package.js";
7
+ import { openCodePaths } from "./paths.js";
8
+ function blocked(...stderr) {
9
+ return {
10
+ kind: "blocked",
11
+ output: { stdout: [], stderr: stderr.map((line) => displayPath(line)) },
12
+ };
13
+ }
14
+ function pathsFor(ctx) {
15
+ return openCodePaths(ctx.env ?? {}, process.cwd());
16
+ }
17
+ async function observeFacts(ctx) {
18
+ const paths = pathsFor(ctx);
19
+ const [discovery, snapshot, recoveryKind] = await Promise.all([
20
+ inspectOpenCodeDiscovery(paths, ctx.env ?? {}, process.cwd()),
21
+ observeSnapshot(paths.installedRoot, readOpenCodeReceipt),
22
+ classifyPathNoFollow(paths.recoveryRoot),
23
+ ]);
24
+ return {
25
+ paths,
26
+ discovery,
27
+ snapshot,
28
+ recovery: recoveryKind === "missing" ? "absent" : "required",
29
+ };
30
+ }
31
+ function assertInspectableFacts(facts) {
32
+ if (facts.discovery.managedEntries.length > 1)
33
+ throw new Error("ambiguous Manager registration");
34
+ if (facts.snapshot.kind === "unverified")
35
+ throw new Error("unverified Manager snapshot");
36
+ if (facts.discovery.managedEntries.length === 1 &&
37
+ facts.snapshot.kind === "absent")
38
+ throw new Error("unverified Manager registration");
39
+ }
40
+ function mutationDecision(facts) {
41
+ if (facts.recovery === "required")
42
+ return blocked(`error: OpenCode recovery required; preserve material at ${facts.paths.recoveryRoot}`);
43
+ if (facts.discovery.blockedInputs.length > 0)
44
+ return blocked("OpenCode configuration activity could not be established read-only:", ...facts.discovery.blockedInputs.map((input) => `- ${input}`));
45
+ if (facts.discovery.conflicts.length > 0)
46
+ return blocked("Conflicting unmanaged Superpowers OpenCode resources require manual resolution:", ...facts.discovery.conflicts.map((conflict) => `- ${conflict}`), "Remove or disable each resource manually, then retry.");
47
+ return { kind: "allowed" };
48
+ }
49
+ function registrationFor(facts) {
50
+ const managed = facts.discovery.managedEntries[0];
51
+ return managed === undefined
52
+ ? null
53
+ : {
54
+ observation: managed.observation,
55
+ entryIndex: managed.entry.index,
56
+ spec: managed.entry.spec,
57
+ };
58
+ }
59
+ export async function inspectOpenCodeOwnership(ctx) {
60
+ const operation = "inspect-opencode-ownership";
61
+ try {
62
+ const facts = await observeFacts(ctx);
63
+ assertInspectableFacts(facts);
64
+ const registration = registrationFor(facts);
65
+ const removalVerification = registration !== null
66
+ ? blocked("error: the Manager OpenCode registration is still present after removal")
67
+ : facts.snapshot.kind === "owned"
68
+ ? blocked("error: the Manager-owned OpenCode snapshot is still present after removal")
69
+ : facts.discovery.registrationUncertain
70
+ ? blocked("error: unresolved OpenCode configuration remains after removal", ...facts.discovery.blockedInputs.map((input) => `- ${input}`))
71
+ : { kind: "allowed" };
72
+ const conflicts = [
73
+ ...facts.discovery.conflicts,
74
+ ...facts.discovery.blockedInputs.map((input) => `${input} is unresolved`),
75
+ ];
76
+ const presentationValue = facts.snapshot.kind === "owned"
77
+ ? registration === null
78
+ ? `managed snapshot ${facts.snapshot.digest}`
79
+ : `managed ${facts.snapshot.digest}`
80
+ : facts.discovery.registrationUncertain
81
+ ? "unresolved configuration"
82
+ : "absent";
83
+ return successResult(operation, {
84
+ installEligibility: mutationDecision(facts),
85
+ removalInput: {
86
+ installedRoot: facts.paths.installedRoot,
87
+ registration,
88
+ receiptDigest: facts.snapshot.kind === "owned"
89
+ ? facts.snapshot.receipt.digest
90
+ : null,
91
+ },
92
+ removalVerification,
93
+ postRemovalOutput: { stdout: [], stderr: [] },
94
+ presentationValue,
95
+ presentationConflicts: conflicts,
96
+ }, []);
97
+ }
98
+ catch {
99
+ return inspectionFailure(operation, "OpenCode ownership");
100
+ }
101
+ }
102
+ function mismatch(operation, observedIdentity) {
103
+ return successResult(operation, { kind: "mismatch", observedIdentity }, []);
104
+ }
105
+ export async function inspectOpenCodeInstalled(selection, ctx) {
106
+ const operation = "inspect-opencode-installed";
107
+ let facts;
108
+ try {
109
+ facts = await observeFacts(ctx);
110
+ }
111
+ catch {
112
+ return inspectionFailure(operation, "OpenCode installed state");
113
+ }
114
+ if (facts.snapshot.kind === "absent") {
115
+ if (facts.discovery.managedEntries.length > 0)
116
+ return mismatch(operation, "registered without an installed snapshot");
117
+ return facts.discovery.registrationUncertain
118
+ ? mismatch(operation, "unresolved OpenCode configuration")
119
+ : successResult(operation, { kind: "absent", observedIdentity: "" }, []);
120
+ }
121
+ if (facts.snapshot.kind === "unverified")
122
+ return mismatch(operation, "unverified installed snapshot");
123
+ const observed = facts.snapshot.digest;
124
+ if (facts.discovery.managedEntries.length > 1)
125
+ return inspectionFailure(operation, "OpenCode installed state");
126
+ if (facts.discovery.managedEntries.length !== 1 ||
127
+ facts.discovery.conflicts.length > 0 ||
128
+ facts.discovery.blockedInputs.length > 0)
129
+ return mismatch(operation, observed);
130
+ try {
131
+ const prepared = await readOpenCodePackageAssessment(facts.paths.preparedRoot);
132
+ const installedProfile = await assessOpenCodeCompatibility(facts.paths.installedRoot, { effectiveSource: facts.snapshot.receipt.source });
133
+ if ((prepared.compatibility.kind !== "supported" &&
134
+ prepared.compatibility.kind !== "experimental") ||
135
+ (installedProfile.kind !== "supported" &&
136
+ installedProfile.kind !== "experimental") ||
137
+ prepared.receipt.commit !== selection.desiredCommit ||
138
+ !sameSnapshotSource(prepared.receipt.source, selection.effectiveSource) ||
139
+ facts.snapshot.receipt.commit !== prepared.receipt.commit ||
140
+ !sameSnapshotSource(facts.snapshot.receipt.source, prepared.receipt.source) ||
141
+ facts.snapshot.digest !== prepared.receipt.digest)
142
+ return mismatch(operation, observed);
143
+ }
144
+ catch {
145
+ return mismatch(operation, observed);
146
+ }
147
+ return successResult(operation, { kind: "current", observedIdentity: observed }, []);
148
+ }
149
+ export async function inspectOpenCodeControl(ctx) {
150
+ const operation = "inspect-opencode-control";
151
+ try {
152
+ const facts = await observeFacts(ctx);
153
+ assertInspectableFacts(facts);
154
+ const mutationEligibility = mutationDecision(facts);
155
+ return successResult(operation, {
156
+ probeEligibility: { kind: "allowed" },
157
+ mutationEligibility,
158
+ presentationValue: facts.recovery === "required"
159
+ ? `recovery required at ${facts.paths.recoveryRoot}`
160
+ : facts.discovery.managedEntries.length === 1
161
+ ? "managed local registration"
162
+ : facts.discovery.conflicts.length > 0 ||
163
+ facts.discovery.blockedInputs.length > 0
164
+ ? "unmanaged or unresolved configuration"
165
+ : "unregistered",
166
+ ...(facts.recovery === "required"
167
+ ? { recoveryState: "required" }
168
+ : {}),
169
+ }, []);
170
+ }
171
+ catch {
172
+ return inspectionFailure(operation, "OpenCode update control");
173
+ }
174
+ }
@@ -1,25 +1,12 @@
1
- import { createHash } from "node:crypto";
2
- import { lstat } from "node:fs/promises";
3
1
  import { join } from "node:path";
4
- import { readArtifactFile, readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
2
+ import { readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
5
3
  import { SEMVER_RE } from "../../domain/refs.js";
6
4
  import { validateSource } from "../../selection.js";
7
- const OFFICIAL_SOURCES = new Set([
8
- "https://github.com/obra/superpowers",
9
- "https://github.com/obra/superpowers.git",
10
- "ssh://git@github.com/obra/superpowers.git",
11
- "git@github.com:obra/superpowers.git",
12
- ]);
13
- export function samePiSource(left, right) {
14
- validateSource(left);
15
- validateSource(right);
16
- return (left === right ||
17
- (OFFICIAL_SOURCES.has(left) && OFFICIAL_SOURCES.has(right)));
18
- }
5
+ import { isOfficialSnapshotSource, requireNoSnapshotDependencies, requireSnapshotBootstrap, } from "../../snapshot-package.js";
19
6
  export async function assessPiCompatibility(root, selection) {
20
7
  try {
21
8
  validateSource(selection.effectiveSource);
22
- const official = OFFICIAL_SOURCES.has(selection.effectiveSource);
9
+ const official = isOfficialSnapshotSource(selection.effectiveSource);
23
10
  const pkg = await readArtifactObject(root, join(root, "package.json"));
24
11
  if (pkg.name !== "superpowers" ||
25
12
  pkg.type !== "module" ||
@@ -34,28 +21,9 @@ export async function assessPiCompatibility(root, selection) {
34
21
  JSON.stringify(pi.extensions) !== '["./.pi/extensions/superpowers.ts"]' ||
35
22
  JSON.stringify(pi.skills) !== '["./skills"]')
36
23
  throw new Error("resource declaration");
37
- for (const key of [
38
- "dependencies",
39
- "optionalDependencies",
40
- "peerDependencies",
41
- "bundleDependencies",
42
- "bundledDependencies",
43
- ]) {
44
- const value = pkg[key];
45
- if (value !== undefined &&
46
- (value === null ||
47
- typeof value !== "object" ||
48
- Object.keys(value).length !== 0))
49
- throw new Error("runtime dependencies");
50
- }
24
+ requireNoSnapshotDependencies(pkg);
51
25
  await validateNativeSkill(root);
52
- const bootstrap = join(root, ".pi/extensions/superpowers.ts");
53
- const bytes = await readArtifactFile(root, bootstrap, 4283);
54
- if ((await lstat(bootstrap)).mode & 0o111 ||
55
- bytes.length !== 4283 ||
56
- createHash("sha256").update(bytes).digest("hex") !==
57
- "39c27000c047f8a1399a76e032e4cd239d7bdb0be168a121f46a2ac719deaae0")
58
- throw new Error("bootstrap implementation");
26
+ await requireSnapshotBootstrap(root, ".pi/extensions/superpowers.ts", 4283, "39c27000c047f8a1399a76e032e4cd239d7bdb0be168a121f46a2ac719deaae0");
59
27
  return {
60
28
  kind: official ? "supported" : "experimental",
61
29
  generation: "pi-native-bootstrap-v1",
@@ -3,8 +3,10 @@ import { cp, lstat, mkdir, open, readFile, readdir, rm, rmdir, unlink, } from "n
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { failureResult, successResult, } from "../../adapter-result.js";
5
5
  import { atomicWriteFile, beginDirectoryPublication, } from "../../atomic.js";
6
- import { normalizePiRuntimeVersion, runPi } from "./native.js";
7
- import { digestPiTree, readPiPackageAssessment, readPiReceipt, } from "./package.js";
6
+ import { digestArtifactTree } from "../../artifact-tree.js";
7
+ import { normalizeSnapshotRuntimeVersion } from "../../harness-command-result.js";
8
+ import { runPi } from "./native.js";
9
+ import { readPiPackageAssessment, readPiReceipt, } from "./package.js";
8
10
  import { piPaths } from "./paths.js";
9
11
  import { readPiSettings, resolveCanonicalPiLocalSource, } from "./settings.js";
10
12
  import { inspectPiControl, inspectPiOwnership, } from "./state.js";
@@ -89,7 +91,7 @@ async function snapshot(root) {
89
91
  if ((await assertNoFollowType(root, ["directory", "missing"])) === "missing")
90
92
  return null;
91
93
  const receipt = await readPiReceipt(root);
92
- if (receipt.digest !== (await digestPiTree(root)))
94
+ if (receipt.digest !== (await digestArtifactTree(root)))
93
95
  throw new Error("Pi snapshot changed");
94
96
  return receipt;
95
97
  }
@@ -347,7 +349,7 @@ export async function installPi(artifact, ctx, deps = DEFAULTS) {
347
349
  const priorRegistration = await registration(paths, deps);
348
350
  if (previous === null && priorRegistration !== null)
349
351
  throw new Error("unowned Pi registration");
350
- accepted(normalizePiRuntimeVersion(await deps.run(["--version"], paths, ctx)));
352
+ accepted(normalizeSnapshotRuntimeVersion("Pi", await deps.run(["--version"], paths, ctx)));
351
353
  // Recheck after the native preflight, before claiming mutation ownership.
352
354
  await requireSnapshot(paths.installedRoot, previous);
353
355
  if (!sameRegistration(await registration(paths, deps), priorRegistration))
@@ -449,7 +451,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
449
451
  return successResult("remove-pi", null, []);
450
452
  }
451
453
  if (registered !== null)
452
- accepted(normalizePiRuntimeVersion(await deps.run(["--version"], paths, ctx)));
454
+ accepted(normalizeSnapshotRuntimeVersion("Pi", await deps.run(["--version"], paths, ctx)));
453
455
  await requireSnapshot(paths.installedRoot, previous);
454
456
  if (!sameRegistration(await registration(paths, deps), registered))
455
457
  throw new Error("Pi registration changed");
@@ -1,7 +1,7 @@
1
1
  import { tmpdir } from "node:os";
2
2
  import { isAbsolute, resolve, sep } from "node:path";
3
- import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
4
- import { SEMVER_RE } from "../../domain/refs.js";
3
+ import { failureResult, } from "../../adapter-result.js";
4
+ import { nativeCommandResult, } from "../../harness-command-result.js";
5
5
  import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
6
6
  import { withWorkspace } from "../../workspace.js";
7
7
  function selectedExecutable(env, invocationCwd) {
@@ -10,12 +10,6 @@ function selectedExecutable(env, invocationCwd) {
10
10
  ? resolve(invocationCwd, configured)
11
11
  : configured;
12
12
  }
13
- function appendCaptured(log, channel, captured) {
14
- log.appendBytes(channel, Buffer.from(captured.text));
15
- if (captured.droppedBytes > 0) {
16
- log.appendText(channel, `Pi ${channel} omitted ${captured.droppedBytes} bytes at the capture limit`);
17
- }
18
- }
19
13
  export async function runPi(args, paths, ctx, execute = runBoundedCommand) {
20
14
  const operation = "pi-command";
21
15
  const env = ctx.env ?? {};
@@ -32,32 +26,7 @@ export async function runPi(args, paths, ctx, execute = runBoundedCommand) {
32
26
  PI_OFFLINE: "1",
33
27
  PI_SKIP_VERSION_CHECK: "1",
34
28
  }, workspace, workspace);
35
- if (result.kind === "launchFailed") {
36
- const errno = /^[A-Z][A-Z0-9_]*$/u.test(result.errno)
37
- ? result.errno
38
- : "UNKNOWN";
39
- return failureResult(operation, "launch-failed", `cannot launch configured Pi executable (${errno})`, [], []);
40
- }
41
- const log = new AdapterMessageLog();
42
- if (result.kind === "timedOut") {
43
- appendCaptured(log, "stdout", result.stdout);
44
- appendCaptured(log, "stderr", result.stderr);
45
- return failureResult(operation, "timeout", `Pi command timed out after ${result.afterMs} ms`, [], log.snapshot());
46
- }
47
- if (result.code !== 0) {
48
- appendCaptured(log, "stdout", result.stdout);
49
- appendCaptured(log, "stderr", result.stderr);
50
- return failureResult(operation, "nonzero-exit", result.code === null
51
- ? "Pi command exited without a status"
52
- : `Pi command exited with status ${result.code}`, [], log.snapshot());
53
- }
54
- if (result.stdout.droppedBytes > 0 || result.stderr.droppedBytes > 0) {
55
- appendCaptured(log, "stdout", result.stdout);
56
- appendCaptured(log, "stderr", result.stderr);
57
- return failureResult(operation, "output-limit", "Pi command output exceeded the capture limit", [], log.snapshot());
58
- }
59
- appendCaptured(log, "stderr", result.stderr);
60
- return successResult(operation, { stdout: result.stdout.text }, log.snapshot());
29
+ return nativeCommandResult("Pi", result);
61
30
  });
62
31
  }
63
32
  catch {
@@ -66,16 +35,3 @@ export async function runPi(args, paths, ctx, execute = runBoundedCommand) {
66
35
  : "cannot create an isolated Pi command workspace", [], []);
67
36
  }
68
37
  }
69
- export function normalizePiRuntimeVersion(result) {
70
- if (!result.outcome.ok) {
71
- return { status: result.status, outcome: result.outcome };
72
- }
73
- if (result.status !== 0) {
74
- return failureResult("pi-runtime", "invalid-status", "Pi runtime inspection returned a nonzero status", [], result.outcome.messages);
75
- }
76
- const version = result.outcome.result.stdout.trim();
77
- if (!SEMVER_RE.test(version)) {
78
- return failureResult("pi-runtime", "invalid-version", "Pi runtime inspection returned an invalid version response", [], result.outcome.messages);
79
- }
80
- return successResult("pi-runtime", version, result.outcome.messages);
81
- }