superpowers-manager 0.4.0 → 0.5.1

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 (44) hide show
  1. package/README.md +76 -517
  2. package/dist/artifact-tree.js +111 -0
  3. package/dist/atomic.js +103 -7
  4. package/dist/cli-arguments.js +37 -0
  5. package/dist/cli.js +49 -12
  6. package/dist/commands/install.js +116 -17
  7. package/dist/commands/mutation.js +71 -0
  8. package/dist/commands/prepare.js +23 -17
  9. package/dist/commands/probe.js +164 -14
  10. package/dist/commands/uninstall.js +9 -5
  11. package/dist/commands/update.js +6 -2
  12. package/dist/domain/refs.js +4 -0
  13. package/dist/harness-compatibility.js +13 -0
  14. package/dist/{adapter.js → harnesses/codex/adapter.js} +17 -13
  15. package/dist/harnesses/codex/compatibility.js +126 -0
  16. package/dist/harnesses/codex/conflicts.js +99 -0
  17. package/dist/{generated-plugin.js → harnesses/codex/generated-plugin.js} +7 -73
  18. package/dist/{codex-harness.js → harnesses/codex/harness.js} +55 -5
  19. package/dist/{hooks.js → harnesses/codex/hooks.js} +6 -6
  20. package/dist/{codex-json.js → harnesses/codex/json.js} +9 -2
  21. package/dist/{lifecycle.js → harnesses/codex/lifecycle.js} +2 -2
  22. package/dist/{manifest-overlay.js → harnesses/codex/manifest-overlay.js} +5 -5
  23. package/dist/{codex-prepare.js → harnesses/codex/prepare.js} +72 -16
  24. package/dist/{codex-presentation.js → harnesses/codex/presentation.js} +27 -4
  25. package/dist/{codex-state.js → harnesses/codex/state.js} +3 -3
  26. package/dist/harnesses/pi/compatibility.js +73 -0
  27. package/dist/harnesses/pi/harness.js +63 -0
  28. package/dist/harnesses/pi/install.js +495 -0
  29. package/dist/harnesses/pi/native.js +81 -0
  30. package/dist/harnesses/pi/package.js +153 -0
  31. package/dist/harnesses/pi/paths.js +30 -0
  32. package/dist/harnesses/pi/prepare.js +111 -0
  33. package/dist/harnesses/pi/presentation.js +67 -0
  34. package/dist/harnesses/pi/settings.js +171 -0
  35. package/dist/harnesses/pi/shared-skills.js +413 -0
  36. package/dist/harnesses/pi/state.js +311 -0
  37. package/dist/python-text.js +16 -0
  38. package/dist/resource-lock.js +265 -0
  39. package/dist/safe-path.js +21 -5
  40. package/dist/skill-validation.js +52 -0
  41. package/dist/upstream-workspace.js +8 -0
  42. package/dist/validate-generated-plugin-cli.js +1 -1
  43. package/dist/validator.js +2 -1
  44. package/package.json +12 -8
@@ -1,9 +1,10 @@
1
- import { normalizeCodexInstall, normalizeCodexInstalled, } from "./codex-harness.js";
2
- import { displaySource } from "./selection.js";
1
+ import { normalizeCodexInstall, normalizeCodexInstalled } from "./harness.js";
2
+ import { displaySource } from "../../selection.js";
3
3
  // One ordered table drives both formats, preserving the public human and
4
4
  // porcelain field order exposed through the harness presentation.
5
5
  export function fields(f) {
6
6
  return [
7
+ { key: "harness", label: "harness", value: f.harness },
7
8
  { key: "requested_ref", label: "requested ref", value: f.requestedRef },
8
9
  { key: "resolved_ref", label: "resolved ref", value: f.resolvedRef },
9
10
  { key: "desired_commit", label: "desired commit", value: f.desiredCommit },
@@ -58,6 +59,7 @@ export function fields(f) {
58
59
  ];
59
60
  }
60
61
  const NO_FACTS = {
62
+ harness: "codex",
61
63
  requestedRef: "",
62
64
  resolvedRef: "",
63
65
  desiredCommit: "",
@@ -114,7 +116,7 @@ export function codexInstallReceipt(missingHint, mismatchHint) {
114
116
  mismatchVerificationOutput: verificationOutput("mismatch", mismatchHint),
115
117
  };
116
118
  }
117
- // Compatibility export target for src/lifecycle.ts. Delegate to the production
119
+ // Compatibility export target for src/harnesses/codex/lifecycle.ts. Delegate to the production
118
120
  // normalizers and renderer so the retained verdict cannot drift from commands.
119
121
  export function verifyInstalledFingerprint(desiredCommit, installResult, inspectResult) {
120
122
  const receipt = normalizeCodexInstall(installResult);
@@ -129,6 +131,7 @@ function probeFacts(facts) {
129
131
  const selection = facts.selection;
130
132
  const saved = selection.saved;
131
133
  return {
134
+ harness: "codex",
132
135
  requestedRef: selection.requestedRef,
133
136
  resolvedRef: selection.resolvedRef,
134
137
  desiredCommit: selection.desiredCommit,
@@ -205,7 +208,27 @@ export const codexPresentation = {
205
208
  currentNotice: "manager is current",
206
209
  renderProbe(snapshot) {
207
210
  const facts = probeFacts(snapshot);
208
- return { human: formatHuman(facts), porcelain: formatPorcelain(facts) };
211
+ const conflicts = snapshot.ownership.presentationConflicts ?? [];
212
+ const additional = [
213
+ ["installation_state", snapshot.installed.kind],
214
+ ["resource_state", snapshot.resourceState ?? "idle"],
215
+ ["compatibility", snapshot.compatibility.kind],
216
+ ["compatibility_reason", snapshot.compatibility.reason],
217
+ ];
218
+ return {
219
+ human: formatHuman(facts) +
220
+ additional
221
+ .map(([key, value]) => `${key.replaceAll("_", " ")}: ${value}\n`)
222
+ .join("") +
223
+ conflicts
224
+ .map((conflict) => `ownership conflict: ${conflict}\n`)
225
+ .join(""),
226
+ porcelain: formatPorcelain(facts) +
227
+ additional.map(([key, value]) => `${key}=${value}\n`).join("") +
228
+ conflicts
229
+ .map((conflict) => `ownership_conflict=${conflict}\n`)
230
+ .join(""),
231
+ };
209
232
  },
210
233
  renderInstallVerification(desiredCommit, receipt, inspection) {
211
234
  if (inspection.status !== 0 || !inspection.outcome.ok) {
@@ -1,8 +1,8 @@
1
1
  import { readFile, realpath } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
- import { COMMIT_INPUT_RE } from "./domain/refs.js";
4
- import { SafetyError } from "./safety-error.js";
5
- import { parseStrictJson, } from "./strict-json.js";
3
+ import { COMMIT_INPUT_RE } from "../../domain/refs.js";
4
+ import { SafetyError } from "../../safety-error.js";
5
+ import { parseStrictJson, } from "../../strict-json.js";
6
6
  const INSTALLED_PROFILE = {
7
7
  duplicateKeys: "last-wins",
8
8
  nonStandardConstants: "reject",
@@ -0,0 +1,73 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { readArtifactFile, readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
5
+ import { SEMVER_RE } from "../../domain/refs.js";
6
+ 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
+ }
19
+ export async function assessPiCompatibility(root, selection) {
20
+ try {
21
+ validateSource(selection.effectiveSource);
22
+ const official = OFFICIAL_SOURCES.has(selection.effectiveSource);
23
+ const pkg = await readArtifactObject(root, join(root, "package.json"));
24
+ if (pkg.name !== "superpowers" ||
25
+ pkg.type !== "module" ||
26
+ typeof pkg.version !== "string" ||
27
+ !SEMVER_RE.test(pkg.version))
28
+ throw new Error("package metadata");
29
+ const pi = pkg.pi;
30
+ if (!pi ||
31
+ typeof pi !== "object" ||
32
+ Array.isArray(pi) ||
33
+ Object.keys(pi).length !== 2 ||
34
+ JSON.stringify(pi.extensions) !== '["./.pi/extensions/superpowers.ts"]' ||
35
+ JSON.stringify(pi.skills) !== '["./skills"]')
36
+ 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
+ }
51
+ 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");
59
+ return {
60
+ kind: official ? "supported" : "experimental",
61
+ generation: "pi-native-bootstrap-v1",
62
+ reason: official
63
+ ? "qualified upstream-native Pi bootstrap"
64
+ : "qualified Pi mechanics from a custom source",
65
+ };
66
+ }
67
+ catch {
68
+ return {
69
+ kind: "unsupported",
70
+ reason: `Pi package does not match the qualified native bootstrap profile: ${root}`,
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,63 @@
1
+ import { failureResult, successResult } from "../../adapter-result.js";
2
+ import { classifyPathNoFollow } from "../../safe-path.js";
3
+ import { installPi, removePi } from "./install.js";
4
+ import { piPaths } from "./paths.js";
5
+ import { inspectPiPrepared, piPreparationLocation, preparePiCandidate, readPiPrepared, } from "./prepare.js";
6
+ import { piPresentation } from "./presentation.js";
7
+ import { inspectPiControl, inspectPiInstalled, inspectPiOwnership, } from "./state.js";
8
+ export const piHarness = {
9
+ preparationLocation: piPreparationLocation,
10
+ mutationRoots: async (ctx) => [
11
+ piPaths(ctx.env ?? {}, process.cwd()).agentDir,
12
+ ],
13
+ validatePreparationBeforeFetch: async () => successResult("validate-pi-preparation", null, []),
14
+ prepareCandidate: preparePiCandidate,
15
+ inspectPrepared: inspectPiPrepared,
16
+ readPrepared: readPiPrepared,
17
+ inspectOwnership: inspectPiOwnership,
18
+ async inspectUpdateControl(ctx) {
19
+ const paths = piPaths(ctx.env ?? {}, process.cwd());
20
+ try {
21
+ if ((await classifyPathNoFollow(paths.recoveryRoot)) !== "missing") {
22
+ const decision = {
23
+ kind: "blocked",
24
+ output: {
25
+ stdout: [],
26
+ stderr: [
27
+ `error: Pi recovery required; preserve material at ${paths.recoveryRoot} for manual resolution`,
28
+ ],
29
+ },
30
+ };
31
+ return successResult("inspect-pi-control", {
32
+ probeEligibility: decision,
33
+ mutationEligibility: decision,
34
+ presentationValue: `recovery required at ${paths.recoveryRoot}`,
35
+ recoveryState: "required",
36
+ }, []);
37
+ }
38
+ }
39
+ catch {
40
+ return failureResult("inspect-pi-control", "invalid-state", `cannot inspect Pi recovery state at ${paths.recoveryRoot}`, [], []);
41
+ }
42
+ return await inspectPiControl(ctx);
43
+ },
44
+ inspectInstalled: inspectPiInstalled,
45
+ install: installPi,
46
+ remove: removePi,
47
+ requirements(command, env) {
48
+ if (command !== "install" &&
49
+ command !== "update" &&
50
+ command !== "uninstall")
51
+ return [];
52
+ const executable = env.SUPERPOWERS_PI || "pi";
53
+ return [
54
+ {
55
+ name: "pi",
56
+ executable,
57
+ lookup: "explicit-path-or-path",
58
+ missingMessage: `required command not found: ${executable} — install the Pi CLI or set SUPERPOWERS_PI`,
59
+ },
60
+ ];
61
+ },
62
+ presentation: piPresentation,
63
+ };