superpowers-manager 0.5.1 → 0.5.2

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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/commands/install.js +16 -6
  4. package/dist/commands/prepare.js +1 -1
  5. package/dist/commands/uninstall.js +1 -1
  6. package/dist/harnesses/codex/adapter.js +147 -18
  7. package/dist/harnesses/codex/generated-plugin.js +1 -1
  8. package/dist/harnesses/codex/harness.js +35 -17
  9. package/dist/harnesses/codex/hooks.js +1 -1
  10. package/dist/harnesses/codex/json.js +6 -2
  11. package/dist/harnesses/codex/lifecycle.js +1 -1
  12. package/dist/harnesses/codex/marketplace.js +148 -0
  13. package/dist/harnesses/codex/paths.js +97 -0
  14. package/dist/harnesses/codex/prepare.js +34 -2
  15. package/dist/harnesses/codex/presentation.js +4 -3
  16. package/dist/harnesses/codex/publication.js +538 -0
  17. package/dist/harnesses/codex/recovery.js +338 -0
  18. package/dist/harnesses/codex/state.js +131 -0
  19. package/dist/harnesses/codex/stored-state.js +249 -0
  20. package/dist/provenance.js +3 -3
  21. package/node_modules/smol-toml/LICENSE +24 -0
  22. package/node_modules/smol-toml/README.md +418 -0
  23. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  24. package/node_modules/smol-toml/dist/date.js +127 -0
  25. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  26. package/node_modules/smol-toml/dist/error.js +63 -0
  27. package/node_modules/smol-toml/dist/extract.js +69 -0
  28. package/node_modules/smol-toml/dist/index.cjs +734 -0
  29. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  30. package/node_modules/smol-toml/dist/index.js +33 -0
  31. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  32. package/node_modules/smol-toml/dist/parse.js +149 -0
  33. package/node_modules/smol-toml/dist/primitive.js +238 -0
  34. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  35. package/node_modules/smol-toml/dist/stringify.js +181 -0
  36. package/node_modules/smol-toml/dist/struct.js +179 -0
  37. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  38. package/node_modules/smol-toml/dist/util.js +89 -0
  39. package/node_modules/smol-toml/package.json +68 -0
  40. package/package.json +10 -2
@@ -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}`, [], []);
@@ -2,7 +2,7 @@ import { normalizeCodexInstall, normalizeCodexInstalled } from "./harness.js";
2
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
- 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 },
@@ -251,17 +251,18 @@ export const codexPresentation = {
251
251
  if (inspection.outcome.result.kind === "current") {
252
252
  return { stdout: [...stdout, "manager updated"], stderr: [] };
253
253
  }
254
+ const missing = inspection.outcome.result.kind === "absent" || observed.length === 0;
254
255
  if (receipt.status !== 0 || !receipt.outcome.ok) {
255
256
  return {
256
257
  stdout,
257
258
  stderr: [
258
- inspection.outcome.result.kind === "absent"
259
+ missing
259
260
  ? "error: installed manager fingerprint is not detectable after install."
260
261
  : "error: installed manager fingerprint does not match the prepared plugin after install.",
261
262
  ],
262
263
  };
263
264
  }
264
- const output = inspection.outcome.result.kind === "absent"
265
+ const output = missing
265
266
  ? receipt.outcome.result.missingVerificationOutput
266
267
  : receipt.outcome.result.mismatchVerificationOutput;
267
268
  return { stdout, stderr: output.stderr };