superpowers-manager 0.5.1 → 0.6.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 (51) hide show
  1. package/README.md +13 -11
  2. package/dist/cli.js +9 -14
  3. package/dist/commands/install.js +18 -9
  4. package/dist/commands/mutation.js +7 -0
  5. package/dist/commands/prepare.js +5 -51
  6. package/dist/commands/probe.js +8 -10
  7. package/dist/commands/uninstall.js +2 -2
  8. package/dist/effective-selection.js +3 -3
  9. package/dist/harnesses/codex/adapter.js +336 -457
  10. package/dist/harnesses/codex/compatibility.js +2 -17
  11. package/dist/harnesses/codex/generated-plugin.js +1 -1
  12. package/dist/harnesses/codex/harness.js +45 -272
  13. package/dist/harnesses/codex/hooks.js +1 -1
  14. package/dist/harnesses/codex/json.js +6 -2
  15. package/dist/harnesses/codex/lifecycle.js +110 -54
  16. package/dist/harnesses/codex/manifest-overlay.js +22 -17
  17. package/dist/harnesses/codex/marketplace.js +148 -0
  18. package/dist/harnesses/codex/paths.js +97 -0
  19. package/dist/harnesses/codex/prepare.js +34 -2
  20. package/dist/harnesses/codex/presentation.js +10 -17
  21. package/dist/harnesses/codex/publication.js +520 -0
  22. package/dist/harnesses/codex/recovery.js +338 -0
  23. package/dist/harnesses/codex/state.js +131 -0
  24. package/dist/harnesses/codex/stored-state.js +249 -0
  25. package/dist/provenance.js +3 -3
  26. package/dist/python-json-format.js +0 -69
  27. package/dist/python-json.js +2 -27
  28. package/dist/python-text.js +9 -69
  29. package/dist/validator.js +35 -72
  30. package/node_modules/smol-toml/LICENSE +24 -0
  31. package/node_modules/smol-toml/README.md +418 -0
  32. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  33. package/node_modules/smol-toml/dist/date.js +127 -0
  34. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  35. package/node_modules/smol-toml/dist/error.js +63 -0
  36. package/node_modules/smol-toml/dist/extract.js +69 -0
  37. package/node_modules/smol-toml/dist/index.cjs +734 -0
  38. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  39. package/node_modules/smol-toml/dist/index.js +33 -0
  40. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  41. package/node_modules/smol-toml/dist/parse.js +149 -0
  42. package/node_modules/smol-toml/dist/primitive.js +238 -0
  43. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  44. package/node_modules/smol-toml/dist/stringify.js +181 -0
  45. package/node_modules/smol-toml/dist/struct.js +179 -0
  46. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  47. package/node_modules/smol-toml/dist/util.js +89 -0
  48. package/node_modules/smol-toml/package.json +68 -0
  49. package/package.json +10 -2
  50. package/dist/selection-state-cli.js +0 -85
  51. package/dist/upstream-cli.js +0 -110
@@ -0,0 +1,148 @@
1
+ import { copyFile, cp, lstat, mkdir, readdir, rm, writeFile, } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
4
+ import { assertNoFollowType, classifyPathNoFollow } from "../../safe-path.js";
5
+ import { SafetyError } from "../../safety-error.js";
6
+ import { readCodexAssessment } from "./compatibility.js";
7
+ const MARKETPLACE_PATH = ".agents/plugins/marketplace.json";
8
+ const PLUGIN_PATH = "plugins/superpowers";
9
+ function inspectionError(root, cause) {
10
+ return new SafetyError("codex-marketplace", `cannot inspect owned Codex marketplace: ${root}`, { cause });
11
+ }
12
+ function validateMarketplace(value) {
13
+ const plugins = value.plugins;
14
+ if (!Array.isArray(plugins) || plugins.length !== 1) {
15
+ throw new Error("marketplace plugins");
16
+ }
17
+ const plugin = plugins[0];
18
+ if (plugin === null || typeof plugin !== "object" || Array.isArray(plugin)) {
19
+ throw new Error("marketplace plugin");
20
+ }
21
+ const source = plugin.source;
22
+ if (plugin.name !== "superpowers" ||
23
+ source === null ||
24
+ typeof source !== "object" ||
25
+ Array.isArray(source) ||
26
+ source.source !== "local" ||
27
+ source.path !== "./plugins/superpowers") {
28
+ throw new Error("marketplace source");
29
+ }
30
+ }
31
+ async function requireEntries(root, expected) {
32
+ const actual = await readdir(root);
33
+ if (actual.length !== expected.length ||
34
+ actual.some((entry) => !expected.includes(entry))) {
35
+ throw new Error("tree entries");
36
+ }
37
+ }
38
+ async function readMarketplace(root) {
39
+ try {
40
+ await assertNoFollowType(root, ["directory"]);
41
+ const before = await lstat(root);
42
+ await requireEntries(root, [".agents", "plugins", ARTIFACT_RECEIPT]);
43
+ await assertNoFollowType(join(root, ".agents"), ["directory"]);
44
+ await assertNoFollowType(join(root, "plugins"), ["directory"]);
45
+ await assertNoFollowType(join(root, ARTIFACT_RECEIPT), ["regular-file"]);
46
+ await requireEntries(join(root, ".agents"), ["plugins"]);
47
+ await requireEntries(join(root, ".agents", "plugins"), [
48
+ "marketplace.json",
49
+ ]);
50
+ await requireEntries(join(root, "plugins"), ["superpowers"]);
51
+ const pluginRoot = join(root, PLUGIN_PATH);
52
+ await assertNoFollowType(pluginRoot, ["directory"]);
53
+ validateMarketplace(await readArtifactObject(root, join(root, MARKETPLACE_PATH)));
54
+ const receipt = await readArtifactObject(root, join(root, ARTIFACT_RECEIPT));
55
+ if (receipt.schema !== 1 ||
56
+ receipt.manager !== "superpowers-manager" ||
57
+ receipt.harness !== "codex" ||
58
+ receipt.generation !== "codex-marketplace-v1" ||
59
+ typeof receipt.digest !== "string" ||
60
+ !ARTIFACT_DIGEST_RE.test(receipt.digest)) {
61
+ throw new Error("receipt");
62
+ }
63
+ const digest = await digestArtifactTree(root);
64
+ if (digest !== receipt.digest)
65
+ throw new Error("digest");
66
+ const artifact = await readCodexAssessment(pluginRoot);
67
+ const after = await lstat(root);
68
+ if (before.dev !== after.dev || before.ino !== after.ino) {
69
+ throw new Error("root changed");
70
+ }
71
+ return {
72
+ root,
73
+ dev: before.dev,
74
+ ino: before.ino,
75
+ digest,
76
+ artifact,
77
+ };
78
+ }
79
+ catch (cause) {
80
+ throw inspectionError(root, cause);
81
+ }
82
+ }
83
+ async function removeOwnedCandidate(root, identity) {
84
+ try {
85
+ await assertNoFollowType(root, ["directory"]);
86
+ const current = await lstat(root);
87
+ if (current.dev !== identity.dev || current.ino !== identity.ino)
88
+ return;
89
+ await rm(root, { recursive: true });
90
+ }
91
+ catch {
92
+ // A candidate that cannot still be proven as ours must remain untouched.
93
+ }
94
+ }
95
+ export async function stageCodexMarketplace(artifact, packageRoot, candidateRoot) {
96
+ let candidateIdentity = null;
97
+ try {
98
+ const assessed = await readCodexAssessment(artifact.root);
99
+ if (assessed.commit !== artifact.commit)
100
+ throw new Error("artifact commit");
101
+ const sourceDigest = await digestArtifactTree(artifact.root);
102
+ await assertNoFollowType(candidateRoot, ["missing"]);
103
+ await mkdir(candidateRoot, { mode: 0o700 });
104
+ const candidate = await lstat(candidateRoot);
105
+ candidateIdentity = { dev: candidate.dev, ino: candidate.ino };
106
+ await mkdir(join(candidateRoot, ".agents", "plugins"), { recursive: true });
107
+ await mkdir(join(candidateRoot, "plugins"), { recursive: true });
108
+ await copyFile(join(packageRoot, MARKETPLACE_PATH), join(candidateRoot, MARKETPLACE_PATH), 0);
109
+ await cp(artifact.root, join(candidateRoot, PLUGIN_PATH), {
110
+ dereference: false,
111
+ errorOnExist: true,
112
+ force: false,
113
+ recursive: true,
114
+ verbatimSymlinks: true,
115
+ });
116
+ const copied = await readCodexAssessment(join(candidateRoot, PLUGIN_PATH));
117
+ if (copied.commit !== artifact.commit ||
118
+ (await digestArtifactTree(copied.root)) !== sourceDigest) {
119
+ throw new Error("copied artifact");
120
+ }
121
+ validateMarketplace(await readArtifactObject(candidateRoot, join(candidateRoot, MARKETPLACE_PATH)));
122
+ const receipt = {
123
+ schema: 1,
124
+ manager: "superpowers-manager",
125
+ harness: "codex",
126
+ generation: "codex-marketplace-v1",
127
+ digest: await digestArtifactTree(candidateRoot),
128
+ };
129
+ await writeFile(join(candidateRoot, ARTIFACT_RECEIPT), JSON.stringify(receipt) + "\n", { flag: "wx" });
130
+ return await readMarketplace(candidateRoot);
131
+ }
132
+ catch (cause) {
133
+ if (candidateIdentity !== null) {
134
+ await removeOwnedCandidate(candidateRoot, candidateIdentity);
135
+ }
136
+ throw new SafetyError("codex-marketplace", `cannot stage owned Codex marketplace: ${candidateRoot}`, { cause });
137
+ }
138
+ }
139
+ export async function readCodexMarketplace(root) {
140
+ try {
141
+ if ((await classifyPathNoFollow(root)) === "missing")
142
+ return null;
143
+ }
144
+ catch (cause) {
145
+ throw inspectionError(root, cause);
146
+ }
147
+ return await readMarketplace(root);
148
+ }
@@ -0,0 +1,97 @@
1
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { assertNoFollowType, canonicalizeProspectivePath, } from "../../safe-path.js";
3
+ import { SafetyError } from "../../safety-error.js";
4
+ export function codexHome(env, cwd) {
5
+ const configured = env.CODEX_HOME;
6
+ if (configured !== undefined && configured.length > 0) {
7
+ return resolve(cwd, configured);
8
+ }
9
+ const home = env.HOME;
10
+ if (home === undefined || home.length === 0) {
11
+ throw new SafetyError("codex-paths", "cannot determine Codex state root without HOME");
12
+ }
13
+ return join(resolve(cwd, home), ".codex");
14
+ }
15
+ export function codexPaths(env, cwd) {
16
+ const root = codexHome(env, cwd);
17
+ const managerRoot = join(root, "superpowers-manager");
18
+ const marketplaceRoot = join(managerRoot, "marketplace");
19
+ return {
20
+ codexHome: root,
21
+ managerRoot,
22
+ preparedRoot: join(managerRoot, "prepared"),
23
+ marketplaceRoot,
24
+ publishedPluginRoot: join(marketplaceRoot, "plugins", "superpowers"),
25
+ recoveryRoot: join(managerRoot, "recovery"),
26
+ };
27
+ }
28
+ function overlaps(a, b) {
29
+ const inside = (root, leaf) => {
30
+ const suffix = relative(root, leaf);
31
+ return (suffix === "" ||
32
+ (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
33
+ };
34
+ return inside(a, b) || inside(b, a);
35
+ }
36
+ function separationError() {
37
+ return new SafetyError("codex-paths", "preparation overlaps Codex published or recovery storage", { details: { kind: "overlap" } });
38
+ }
39
+ async function inspectRoot(path, root) {
40
+ try {
41
+ await assertNoFollowType(path, ["directory", "missing"]);
42
+ return await canonicalizeProspectivePath(path);
43
+ }
44
+ catch (cause) {
45
+ throw new SafetyError("codex-paths", `cannot inspect ${root} root: ${path}`, { cause, details: { kind: "inspection", root, path } });
46
+ }
47
+ }
48
+ export function codexPathFailureDetails(cause) {
49
+ if (!(cause instanceof SafetyError) || cause.module !== "codex-paths") {
50
+ return null;
51
+ }
52
+ const details = cause.details;
53
+ if (details === null || typeof details !== "object" || !("kind" in details)) {
54
+ return null;
55
+ }
56
+ if (details.kind === "overlap")
57
+ return { kind: "overlap" };
58
+ if (details.kind !== "inspection" ||
59
+ !("root" in details) ||
60
+ !("path" in details) ||
61
+ (details.root !== "preparation" &&
62
+ details.root !== "marketplace" &&
63
+ details.root !== "recovery") ||
64
+ typeof details.path !== "string") {
65
+ return null;
66
+ }
67
+ return { kind: "inspection", root: details.root, path: details.path };
68
+ }
69
+ export async function assertCodexPreparationSeparate(preparedRoot, paths) {
70
+ const recovery = await inspectRoot(paths.recoveryRoot, "recovery");
71
+ const marketplace = await inspectRoot(paths.marketplaceRoot, "marketplace");
72
+ let prepared;
73
+ try {
74
+ prepared = await inspectRoot(preparedRoot, "preparation");
75
+ }
76
+ catch (cause) {
77
+ const failure = codexPathFailureDetails(cause);
78
+ if (failure?.kind !== "inspection" || failure.root !== "preparation") {
79
+ throw cause;
80
+ }
81
+ try {
82
+ const canonical = await canonicalizeProspectivePath(preparedRoot);
83
+ if (overlaps(canonical, marketplace) || overlaps(canonical, recovery)) {
84
+ throw separationError();
85
+ }
86
+ }
87
+ catch (resolutionCause) {
88
+ if (codexPathFailureDetails(resolutionCause)?.kind === "overlap") {
89
+ throw resolutionCause;
90
+ }
91
+ }
92
+ throw cause;
93
+ }
94
+ if (overlaps(prepared, marketplace) || overlaps(prepared, recovery)) {
95
+ throw separationError();
96
+ }
97
+ }
@@ -9,6 +9,8 @@ import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance,
9
9
  import { classifyPathNoFollow } from "../../safe-path.js";
10
10
  import { SafetyError } from "../../safety-error.js";
11
11
  import { manifestVersionForRef } from "../../upstream-version.js";
12
+ import { assertCodexPreparationSeparate, codexPathFailureDetails, codexPaths, } from "./paths.js";
13
+ import { readCodexRecovery } from "./recovery.js";
12
14
  // Order is inherited from the original prepare command; the first miss wins.
13
15
  const REQUIRED_UPSTREAM = [
14
16
  { path: "skills", label: "skills/" },
@@ -75,7 +77,7 @@ async function copyPathIfPresent(source, destination) {
75
77
  await owned(`cannot clear candidate path: ${destination}`, () => rm(destination, { recursive: true, force: true }));
76
78
  await owned(`cannot copy upstream path into candidate: ${source}`, () => cp(source, destination, { recursive: true, verbatimSymlinks: true }));
77
79
  }
78
- export function resolveFromCwd(value, cwd) {
80
+ function resolveFromCwd(value, cwd) {
79
81
  return isAbsolute(value) ? value : resolve(cwd, value);
80
82
  }
81
83
  function manifestTemplate(ctx) {
@@ -86,11 +88,41 @@ function manifestTemplate(ctx) {
86
88
  export function codexPreparationLocation(ctx) {
87
89
  const env = ctx.env ?? {};
88
90
  return {
89
- destinationRoot: resolveFromCwd(env.SUPERPOWERS_PLUGIN_ROOT || join(ctx.root, "plugins", "superpowers"), process.cwd()),
91
+ destinationRoot: env.SUPERPOWERS_PLUGIN_ROOT !== undefined &&
92
+ env.SUPERPOWERS_PLUGIN_ROOT.length > 0
93
+ ? resolveFromCwd(env.SUPERPOWERS_PLUGIN_ROOT, process.cwd())
94
+ : codexPaths(env, process.cwd()).preparedRoot,
90
95
  stagingLeaf: "superpowers",
91
96
  };
92
97
  }
93
98
  export async function validateCodexPreparationBeforeFetch(ctx) {
99
+ const paths = codexPaths(ctx.env ?? {}, process.cwd());
100
+ const preparedRoot = codexPreparationLocation(ctx).destinationRoot;
101
+ try {
102
+ await assertCodexPreparationSeparate(preparedRoot, paths);
103
+ }
104
+ catch (cause) {
105
+ const failure = codexPathFailureDetails(cause);
106
+ if (failure?.kind === "inspection") {
107
+ if (failure.root === "recovery") {
108
+ return failureResult("prepare", "recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}`, [], []);
109
+ }
110
+ return failureResult("prepare", "prepare-failed", failure.root === "marketplace"
111
+ ? `cannot inspect Codex marketplace storage at ${paths.marketplaceRoot}`
112
+ : `cannot inspect Codex preparation root at ${preparedRoot}`, [], []);
113
+ }
114
+ return failureResult("prepare", failure?.kind === "overlap" ? "preparation-overlap" : "prepare-failed", failure?.kind === "overlap"
115
+ ? `Codex preparation overlaps Codex published or recovery storage: ${preparedRoot}`
116
+ : `cannot validate Codex preparation storage separation at ${preparedRoot}`, [], []);
117
+ }
118
+ try {
119
+ if ((await readCodexRecovery(paths)) !== null) {
120
+ return failureResult("prepare", "recovery-required", `Codex recovery is required before preparation; preserve material at ${paths.recoveryRoot}`, [], []);
121
+ }
122
+ }
123
+ catch {
124
+ return failureResult("prepare", "recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}`, [], []);
125
+ }
94
126
  const template = manifestTemplate(ctx);
95
127
  if (!(await regularFileExists(template))) {
96
128
  return failureResult("prepare", "missing-template", `missing fallback manifest template: ${template}`, [], []);
@@ -1,8 +1,8 @@
1
- import { normalizeCodexInstall, normalizeCodexInstalled } from "./harness.js";
2
1
  import { displaySource } from "../../selection.js";
2
+ import { hasTerminalControl } from "../../adapter-result.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
- export function fields(f) {
5
+ function fields(f) {
6
6
  return [
7
7
  { key: "harness", label: "harness", value: f.harness },
8
8
  { key: "requested_ref", label: "requested ref", value: f.requestedRef },
@@ -110,23 +110,15 @@ function verificationOutput(kind, hint) {
110
110
  stderr.push(`hint: ${hint}`);
111
111
  return { stdout: [], stderr };
112
112
  }
113
+ function safeHint(value) {
114
+ return typeof value === "string" && !hasTerminalControl(value) ? value : "";
115
+ }
113
116
  export function codexInstallReceipt(missingHint, mismatchHint) {
114
117
  return {
115
- missingVerificationOutput: verificationOutput("missing", missingHint),
116
- mismatchVerificationOutput: verificationOutput("mismatch", mismatchHint),
118
+ missingVerificationOutput: verificationOutput("missing", safeHint(missingHint)),
119
+ mismatchVerificationOutput: verificationOutput("mismatch", safeHint(mismatchHint)),
117
120
  };
118
121
  }
119
- // Compatibility export target for src/harnesses/codex/lifecycle.ts. Delegate to the production
120
- // normalizers and renderer so the retained verdict cannot drift from commands.
121
- export function verifyInstalledFingerprint(desiredCommit, installResult, inspectResult) {
122
- const receipt = normalizeCodexInstall(installResult);
123
- const inspection = normalizeCodexInstalled(inspectResult, desiredCommit);
124
- const output = codexPresentation.renderInstallVerification(desiredCommit, receipt, inspection);
125
- const ok = inspection.status === 0 &&
126
- inspection.outcome.ok &&
127
- inspection.outcome.result.kind === "current";
128
- return { ok, stdout: output.stdout, stderr: output.stderr };
129
- }
130
122
  function probeFacts(facts) {
131
123
  const selection = facts.selection;
132
124
  const saved = selection.saved;
@@ -251,17 +243,18 @@ export const codexPresentation = {
251
243
  if (inspection.outcome.result.kind === "current") {
252
244
  return { stdout: [...stdout, "manager updated"], stderr: [] };
253
245
  }
246
+ const missing = inspection.outcome.result.kind === "absent" || observed.length === 0;
254
247
  if (receipt.status !== 0 || !receipt.outcome.ok) {
255
248
  return {
256
249
  stdout,
257
250
  stderr: [
258
- inspection.outcome.result.kind === "absent"
251
+ missing
259
252
  ? "error: installed manager fingerprint is not detectable after install."
260
253
  : "error: installed manager fingerprint does not match the prepared plugin after install.",
261
254
  ],
262
255
  };
263
256
  }
264
- const output = inspection.outcome.result.kind === "absent"
257
+ const output = missing
265
258
  ? receipt.outcome.result.missingVerificationOutput
266
259
  : receipt.outcome.result.mismatchVerificationOutput;
267
260
  return { stdout, stderr: output.stderr };