superpowers-manager 0.5.0 → 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 (62) hide show
  1. package/README.md +39 -9
  2. package/dist/artifact-tree.js +2 -1
  3. package/dist/cli.js +3 -3
  4. package/dist/commands/install.js +18 -8
  5. package/dist/commands/prepare.js +5 -5
  6. package/dist/commands/probe.js +4 -4
  7. package/dist/commands/uninstall.js +3 -3
  8. package/dist/commands/update.js +1 -1
  9. package/dist/domain/refs.js +4 -0
  10. package/dist/{adapter.js → harnesses/codex/adapter.js} +156 -27
  11. package/dist/{codex-compatibility.js → harnesses/codex/compatibility.js} +7 -7
  12. package/dist/{codex-conflicts.js → harnesses/codex/conflicts.js} +1 -1
  13. package/dist/{generated-plugin.js → harnesses/codex/generated-plugin.js} +8 -74
  14. package/dist/{codex-harness.js → harnesses/codex/harness.js} +39 -21
  15. package/dist/{hooks.js → harnesses/codex/hooks.js} +7 -7
  16. package/dist/{codex-json.js → harnesses/codex/json.js} +8 -4
  17. package/dist/{lifecycle.js → harnesses/codex/lifecycle.js} +2 -2
  18. package/dist/{manifest-overlay.js → harnesses/codex/manifest-overlay.js} +4 -4
  19. package/dist/harnesses/codex/marketplace.js +148 -0
  20. package/dist/harnesses/codex/paths.js +97 -0
  21. package/dist/{codex-prepare.js → harnesses/codex/prepare.js} +41 -9
  22. package/dist/{codex-presentation.js → harnesses/codex/presentation.js} +7 -6
  23. package/dist/harnesses/codex/publication.js +538 -0
  24. package/dist/harnesses/codex/recovery.js +338 -0
  25. package/dist/harnesses/codex/state.js +209 -0
  26. package/dist/harnesses/codex/stored-state.js +249 -0
  27. package/dist/{pi-compatibility.js → harnesses/pi/compatibility.js} +3 -3
  28. package/dist/{pi-harness.js → harnesses/pi/harness.js} +7 -7
  29. package/dist/{pi-install.js → harnesses/pi/install.js} +8 -8
  30. package/dist/{pi-native.js → harnesses/pi/native.js} +4 -4
  31. package/dist/{pi-package.js → harnesses/pi/package.js} +6 -6
  32. package/dist/{pi-prepare.js → harnesses/pi/prepare.js} +6 -6
  33. package/dist/{pi-settings.js → harnesses/pi/settings.js} +3 -3
  34. package/dist/{pi-shared-skills.js → harnesses/pi/shared-skills.js} +1 -1
  35. package/dist/{pi-state.js → harnesses/pi/state.js} +8 -8
  36. package/dist/provenance.js +3 -3
  37. package/dist/python-text.js +16 -0
  38. package/dist/skill-validation.js +52 -0
  39. package/dist/validate-generated-plugin-cli.js +1 -1
  40. package/node_modules/smol-toml/LICENSE +24 -0
  41. package/node_modules/smol-toml/README.md +418 -0
  42. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  43. package/node_modules/smol-toml/dist/date.js +127 -0
  44. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  45. package/node_modules/smol-toml/dist/error.js +63 -0
  46. package/node_modules/smol-toml/dist/extract.js +69 -0
  47. package/node_modules/smol-toml/dist/index.cjs +734 -0
  48. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  49. package/node_modules/smol-toml/dist/index.js +33 -0
  50. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  51. package/node_modules/smol-toml/dist/parse.js +149 -0
  52. package/node_modules/smol-toml/dist/primitive.js +238 -0
  53. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  54. package/node_modules/smol-toml/dist/stringify.js +181 -0
  55. package/node_modules/smol-toml/dist/struct.js +179 -0
  56. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  57. package/node_modules/smol-toml/dist/util.js +89 -0
  58. package/node_modules/smol-toml/package.json +68 -0
  59. package/package.json +13 -4
  60. package/dist/codex-state.js +0 -78
  61. /package/dist/{pi-paths.js → harnesses/pi/paths.js} +0 -0
  62. /package/dist/{pi-presentation.js → harnesses/pi/presentation.js} +0 -0
package/README.md CHANGED
@@ -1,9 +1,45 @@
1
1
  # Superpowers Manager
2
2
 
3
- Install and explicitly refresh the latest stable [`obra/superpowers`](https://github.com/obra/superpowers) release. Superpowers Manager supports Codex and the Pi coding agent without waiting for a marketplace copy to catch up.
3
+ Install [`obra/superpowers`](https://github.com/obra/superpowers) directly from upstream for Codex and the Pi coding agent. Stay on a known-good release or commit, try a branch, and upgrade when you choose—without waiting for a marketplace copy to catch up.
4
4
 
5
5
  > Unofficial community integration. Not affiliated with the `obra/superpowers` maintainers.
6
6
 
7
+ ## Choose what you run
8
+
9
+ Start with the latest stable release by default, or choose the upstream version that works for you. There is no background updater.
10
+
11
+ | What you want | How to choose it |
12
+ |---|---|
13
+ | Stay on a known-good version | `pin` a release tag or full 40-character commit SHA |
14
+ | Try an upstream fix before its release | Set `SUPERPOWERS_REF` to a branch or other resolvable ref for that invocation |
15
+ | Return to the latest stable release | Save `track-latest`, then explicitly install or update |
16
+
17
+ For example, save a release pin and install it for your agent:
18
+
19
+ ```sh
20
+ npx superpowers-manager pin v6.1.1
21
+ npx superpowers-manager install --harness codex
22
+ ```
23
+
24
+ Use `--harness pi` for Pi. The pin is shared by both agents, but each installation changes only when you run its `install` or `update` command. A saved pin keeps subsequent installs and updates on that version unless you change or override the selection.
25
+
26
+ To try a branch, replace `feature/foo` with an existing upstream branch:
27
+
28
+ ```sh
29
+ SUPERPOWERS_REF=feature/foo npx superpowers-manager install --harness codex
30
+ ```
31
+
32
+ This override applies only to that command and leaves your saved selection unchanged; the installed version remains until you explicitly change it. Branches can move. Persistent `pin` accepts exact version tags (including prereleases) and full commit SHAs.
33
+
34
+ When you are ready to return to the latest stable release:
35
+
36
+ ```sh
37
+ npx superpowers-manager track-latest
38
+ npx superpowers-manager update --harness codex
39
+ ```
40
+
41
+ See [version selection](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md#choosing-a-source) for source overrides, precedence, and `unpin`.
42
+
7
43
  ## Start with your agent
8
44
 
9
45
  The two harnesses share upstream selection, while each has independent prepared and installed state. Omitting `--harness` selects Codex.
@@ -20,7 +56,7 @@ Codex installs the Manager-owned marketplace plugin after preparation and
20
56
  validation. Review upstream hook definitions with Codex's `/hooks` flow before
21
57
  trusting or running them; updates can change a referenced script or surface a
22
58
  new definition. The manager neither creates hooks nor changes Codex trust state.
23
- See the [Codex reference](https://github.com/j7an/superpowers-manager/blob/main/docs/codex.md).
59
+ See the [Codex reference](https://github.com/j7an/superpowers-manager/blob/main/docs/codex.md#durable-storage-migration-and-recovery) for durable storage, migration, and recovery.
24
60
 
25
61
  ### Pi
26
62
 
@@ -53,7 +89,7 @@ The manager never removes another Superpowers provider. See the [ownership guida
53
89
 
54
90
  ## Shared selection and lifecycle
55
91
 
56
- Use `pin`, `track-latest`, or `unpin` to save shared upstream intent. They do not activate Codex or Pi; run the selected harness's `install` or `update` to apply that choice. There is no background updater.
92
+ The [selection commands above](#choose-what-you-run) save shared upstream intent. They do not activate Codex or Pi; run the selected harness's `install` or `update` to apply that choice. `unpin` restores the packaged fallback policy.
57
93
 
58
94
  | Command | Purpose |
59
95
  |---|---|
@@ -65,12 +101,6 @@ Use `pin`, `track-latest`, or `unpin` to save shared upstream intent. They do no
65
101
 
66
102
  Use `--harness codex` or `--harness pi` with each targeted lifecycle command.
67
103
 
68
- ```sh
69
- npx superpowers-manager pin v6.1.1
70
- npx superpowers-manager track-latest
71
- npx superpowers-manager unpin
72
- ```
73
-
74
104
  ```text
75
105
  shared selection -> prepare / inspect / activate Codex
76
106
  -> prepare / inspect / activate Pi
@@ -4,7 +4,8 @@ import { join } from "node:path";
4
4
  import { assertExistingContained, assertNoFollowType, assertSymlinkTargetContained, } from "./safe-path.js";
5
5
  import { SafetyError } from "./safety-error.js";
6
6
  import { parseStrictJson } from "./strict-json.js";
7
- import { compareByCodePoint, DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "./generated-plugin.js";
7
+ import { compareByCodePoint } from "./python-text.js";
8
+ import { DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "./skill-validation.js";
8
9
  export const ARTIFACT_RECEIPT = ".superpowers-manager.json";
9
10
  export const ARTIFACT_DIGEST_RE = /^[a-f0-9]{64}$/;
10
11
  const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { extractHarnessOptions, oneLine, UsageError } from "./cli-arguments.js";
5
- import { codexHarness } from "./codex-harness.js";
6
- import { piHarness } from "./pi-harness.js";
5
+ import { codexHarness } from "./harnesses/codex/harness.js";
6
+ import { piHarness } from "./harnesses/pi/harness.js";
7
7
  import { createResourceCoordinator } from "./resource-lock.js";
8
8
  import { runInstall } from "./commands/install.js";
9
9
  import { runPin } from "./commands/pin.js";
@@ -341,7 +341,7 @@ async function dispatch(adapter, parsed) {
341
341
  }
342
342
  return status;
343
343
  }
344
- export { resolvePackageRoot, isMain, parseArgs, findTool, commandRequirements, commandRequirementsFor, preflight, preflightFor, usage, dispatch, main, };
344
+ export { resolvePackageRoot, isMain, parseArgs, commandRequirements, commandRequirementsFor, preflight, usage, main, };
345
345
  if (isMain(import.meta.filename, process.argv[1])) {
346
346
  main().catch((cause) => {
347
347
  console.error(`error: ${oneLine(cause)}`);
@@ -1,6 +1,6 @@
1
1
  // Ports scripts/install. The shell sourced common.sh, provenance.sh,
2
2
  // status.sh, lifecycle.sh and adapter.sh; the predicates now live in
3
- // src/lifecycle.ts, the generated-metadata read lives in src/provenance.ts,
3
+ // src/harnesses/codex/lifecycle.ts, the generated-metadata read lives in src/provenance.ts,
4
4
  // and the adapter arrives through ctx.adapter.
5
5
  import { tmpdir } from "node:os";
6
6
  import { oneLine } from "../cli-arguments.js";
@@ -163,7 +163,7 @@ async function gatherInstallStages(ctx, selection, artifact) {
163
163
  // is what turns a failed inspection into "error: installed manager
164
164
  // fingerprint inspection failed after install."
165
165
  // renderInstallVerification's failed-inspection arm
166
- // (`src/codex-presentation.ts:332::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
166
+ // (`src/harnesses/codex/presentation.ts:329::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
167
167
  // exists for this result-bearing path; the lifecycle compatibility
168
168
  // export delegates through the same arm. Returning
169
169
  // failed() instead reported the adapter's own generic diagnostic and
@@ -190,18 +190,28 @@ async function gatherInstallStages(ctx, selection, artifact) {
190
190
  const restoration = restored.ok
191
191
  ? `installed state after rollback: ${restored.result.outcome.result.kind}; identity=${restored.result.outcome.result.observedIdentity}`
192
192
  : "installed state after rollback could not be inspected";
193
- const message = !settlement.ok
193
+ const verificationOutput = inspection === null
194
+ ? null
195
+ : ctx.adapter.presentation.renderInstallVerification(selection.desiredCommit, install.result, inspection);
196
+ const inspectionMessage = verificationOutput === null && !inspected.ok
197
+ ? inspected.message
198
+ : null;
199
+ const settlementMessage = !settlement.ok
194
200
  ? settlement.message
195
- : !inspected.ok
196
- ? inspected.message
197
- : "installation could not be verified";
201
+ : null;
198
202
  return {
199
203
  kind: "verified",
200
204
  outcomes,
201
205
  status: 1,
202
- stdout: [],
206
+ stdout: verificationOutput?.stdout ?? [],
203
207
  stderr: [
204
- ...(message === null ? [] : [`error: ${message}`]),
208
+ ...(verificationOutput?.stderr ?? []),
209
+ ...(inspectionMessage === null
210
+ ? []
211
+ : [`error: ${inspectionMessage}`]),
212
+ ...(settlementMessage === null
213
+ ? []
214
+ : [`error: ${settlementMessage}`]),
205
215
  restoration,
206
216
  ],
207
217
  };
@@ -14,7 +14,7 @@ import { runWithMutation } from "./mutation.js";
14
14
  // Every message this module writes is hand-written here. The cause is attached
15
15
  // for debuggability and never reaches a stream: oneLine (src/cli-arguments.ts)
16
16
  // reads .message only. Same arrangement as hookError
17
- // (`src/hooks.ts:44::function hookError`).
17
+ // (`src/harnesses/codex/hooks.ts:44::function hookError`).
18
18
  function prepareError(message, cause) {
19
19
  return new SafetyError("prepare", message, { cause });
20
20
  }
@@ -341,8 +341,8 @@ async function performPrepare(argv, ctx) {
341
341
  // failure (its cause is the shared runner's (src/validator.ts) captured
342
342
  // spawn error, which oneLine never reads -- it takes .message only);
343
343
  // readManifest's three hookError messages
344
- // (`src/hooks.ts:113-138::readManifest`), pinned by
345
- // `tests/unit/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
344
+ // (`src/harnesses/codex/hooks.ts:113-138::readManifest`), pinned by
345
+ // `tests/unit/harnesses/codex/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
346
346
  // errno; and SafetyErrors from gitSafeSource, writeProvenance, and
347
347
  // withWorkspace.
348
348
  //
@@ -382,7 +382,7 @@ async function performPrepare(argv, ctx) {
382
382
  // only, so it bounds how much of that text lands, not what it may
383
383
  // contain.
384
384
  //
385
- // runAdapter's closing `throw cause` (src/adapter.ts) does NOT arrive
385
+ // runAdapter's closing `throw cause` (src/harnesses/codex/adapter.ts) does NOT arrive
386
386
  // here -- the call site catches it and converts it to a hand-written
387
387
  // message per AGENTS.md's reader-diagnostics rule.
388
388
  //
@@ -418,7 +418,7 @@ async function performPrepare(argv, ctx) {
418
418
  // completed before cleanup ran, so it is not being reported as unverified
419
419
  // -- but something did still go wrong, and AGENTS.md's fail-closed rule
420
420
  // extends to it. Mirrors
421
- // `src/commands/install.ts:546-554::if (cleanupWarning`.
421
+ // `src/commands/install.ts:562-570::if (cleanupWarning`.
422
422
  ctx.stderr.write(`error: ${cleanupWarning}\n`);
423
423
  return 1;
424
424
  }
@@ -11,7 +11,7 @@ export const PROBE_USAGE = "error: usage: superpowers-manager probe [--porcelain
11
11
  // silent diagnostics regression, not a simplification.
12
12
  //
13
13
  // Interpolating error.message and each hint is the sanctioned form (AGENTS.md):
14
- // src/adapter.ts has 52 `fail()` sites and none interpolates a caught error's
14
+ // src/harnesses/codex/adapter.ts has 52 `fail()` sites and none interpolates a caught error's
15
15
  // message, so the callee owns every failure reachable on this path. Those two
16
16
  // writes now live in writeAdapterFailure (src/adapter-result.ts), which
17
17
  // validates all three strings before the first of them reaches the stream.
@@ -121,7 +121,7 @@ function validControl(value) {
121
121
  // evidence and report it as success.
122
122
  //
123
123
  // It does still THROW for a non-AdapterFailure cause (runAdapter's closing
124
- // `throw cause`, src/adapter.ts). That is caught here rather than in runProbe's
124
+ // `throw cause`, src/harnesses/codex/adapter.ts). That is caught here rather than in runProbe's
125
125
  // outer catch, because the two need different diagnostics -- see spec §3.3a.
126
126
  //
127
127
  // Reached through ctx.adapter, not a direct module-level dependency on the
@@ -377,9 +377,9 @@ export async function runProbe(argv, ctx) {
377
377
  // which catches every failure and returns "".
378
378
  //
379
379
  // A non-AdapterFailure re-thrown by runAdapter's closing `throw cause`
380
- // (src/adapter.ts) does NOT reach here: inspect() catches it and converts
380
+ // (src/harnesses/codex/adapter.ts) does NOT reach here: inspect() catches it and converts
381
381
  // it to a hand-written message per AGENTS.md's reader-diagnostics rule -- a
382
- // rethrown cause is exactly the failure src/adapter.ts declined to own, so
382
+ // rethrown cause is exactly the failure src/harnesses/codex/adapter.ts declined to own, so
383
383
  // its text must never reach this stream. See §3.3a.
384
384
  //
385
385
  // gatherProbe performs no writes of its own, so this catch cannot also be
@@ -1,5 +1,5 @@
1
1
  // Ports scripts/uninstall. The shell sourced common.sh, provenance.sh,
2
- // lifecycle.sh and adapter.sh; the predicates now live in src/lifecycle.ts
2
+ // lifecycle.sh and adapter.sh; the predicates now live in src/harnesses/codex/lifecycle.ts
3
3
  // and the adapter arrives through ctx.adapter.
4
4
  import { tmpdir } from "node:os";
5
5
  import { oneLine } from "../cli-arguments.js";
@@ -126,7 +126,7 @@ async function gatherUninstall(ctx) {
126
126
  // Reachable only for mkdtemp failure, with nothing collected yet: this
127
127
  // callback never throws -- every ctx.adapter throw is already caught
128
128
  // inside invoke(), and presenceFlag/verifyUninstalledResources/
129
- // reportLegacyState are pure (src/lifecycle.ts's header comment) -- so a
129
+ // reportLegacyState are pure (src/harnesses/codex/lifecycle.ts's header comment) -- so a
130
130
  // post-success cleanup failure is handled by onCleanupFailure above and
131
131
  // cannot reach here. Wrapping with `outcomes` anyway keeps the class
132
132
  // total over its declared contract rather than assuming the callback's
@@ -163,7 +163,7 @@ async function performUninstall(argv, ctx) {
163
163
  // blindly.
164
164
  //
165
165
  // A cause outside ctx.adapter's AdapterFailure guard
166
- // (`src/adapter.ts:973-999::async function runCodexOperation(`) does NOT
166
+ // (`src/harnesses/codex/adapter.ts:1081::async function runCodexOperation<T = JsonValue>(`) does NOT
167
167
  // reach here: invoke() catches it inside gatherUninstall and converts it
168
168
  // to a hand-written message carried as UninstallOutcome data, exactly as
169
169
  // src/commands/probe.ts's inspect() does for the same cause.
@@ -1,5 +1,5 @@
1
1
  // Ports scripts/update. The shell sourced common.sh and lifecycle.sh; the
2
- // predicates now live in src/lifecycle.ts. Unlike install and uninstall,
2
+ // predicates now live in src/harnesses/codex/lifecycle.ts. Unlike install and uninstall,
3
3
  // update issues no ctx.adapter call of its own: every adapter interaction
4
4
  // this module needs already happens inside gatherProbe, runPrepare or
5
5
  // runInstall, each of which owns its own §4.2a-conformant invoke() gate. That
@@ -3,6 +3,10 @@ const SEMVER_BASE_SOURCE = "(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0
3
3
  "(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?";
4
4
  export const SEMVER_BASE_RE = new RegExp(`^${SEMVER_BASE_SOURCE}$`);
5
5
  export const TAG_RE = new RegExp(`^v${SEMVER_BASE_SOURCE}$`);
6
+ // The Python's own SEMVER_RE (:15-22). Deliberately not SEMVER_BASE_SOURCE
7
+ // from this module, which omits the `+build` component; widening the shared
8
+ // constant would change TAG_RE and is out of scope.
9
+ export const SEMVER_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
6
10
  export const COMMIT_RE = /^[0-9a-f]{40}$/;
7
11
  export const COMMIT_INPUT_RE = /^[0-9A-Fa-f]{40}$/;
8
12
  export function isTagRef(value) {
@@ -4,21 +4,29 @@ import { constants } from "node:fs";
4
4
  import { access, copyFile, mkdir, readFile, realpath, stat, writeFile, } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { delimiter, isAbsolute, join } from "node:path";
7
- import { AdapterMessageLog, failureResult, successResult, } from "./adapter-result.js";
8
- import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./codex-json.js";
9
- import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./codex-conflicts.js";
10
- import { oneLine } from "./cli-arguments.js";
11
- import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./codex-state.js";
7
+ import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
8
+ import { activePluginVersionFromJson, codexInstalledPluginsFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
9
+ import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./conflicts.js";
10
+ import { oneLine } from "../../cli-arguments.js";
11
+ import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./state.js";
12
+ import { codexHome } from "./paths.js";
13
+ import { readCodexStoredState } from "./stored-state.js";
12
14
  import { validateGeneratedPlugin } from "./generated-plugin.js";
13
15
  import { classifyHooks, materializeHooks, readManifest, } from "./hooks.js";
14
16
  import { applyManifestOverlay } from "./manifest-overlay.js";
15
- import { readCodexBuildSource } from "./provenance.js";
16
- import { isAcceptedSplitValue } from "./validate-generated-plugin-cli.js";
17
- import { withWorkspace, workspaceRemovalFailure } from "./workspace.js";
17
+ import { readCodexBuildSource } from "../../provenance.js";
18
+ import { isAcceptedSplitValue } from "../../validate-generated-plugin-cli.js";
19
+ import { withWorkspace, workspaceRemovalFailure } from "../../workspace.js";
18
20
  const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
19
21
  const MARKETPLACE_NAME = "superpowers-manager";
20
22
  const LEGACY_PLUGIN_ID = CODEX_LEGACY_PLUGIN_ID;
21
23
  const LEGACY_MARKETPLACE_NAME = "superpowers-wrapper";
24
+ export function codexInstallRefreshMode(env) {
25
+ const refreshMode = env.SUPERPOWERS_INSTALL_REFRESH_MODE || "add-only";
26
+ return refreshMode === "add-only" || refreshMode === "remove-add"
27
+ ? refreshMode
28
+ : null;
29
+ }
22
30
  class AdapterFailure extends Error {
23
31
  code;
24
32
  hints;
@@ -89,12 +97,20 @@ function runCommand(file, args, env) {
89
97
  });
90
98
  });
91
99
  }
92
- // Exported only so tests/unit/adapter.test.js can assert the env scrub
100
+ // Exported only so tests/unit/harnesses/codex/adapter.test.ts can assert the env scrub
93
101
  // directly. No production caller uses this name.
94
102
  export { runCommand as runCommandForTest };
95
103
  function commandFailed(result) {
96
104
  return result.status !== 0;
97
105
  }
106
+ async function storedStateAfterListingFailure(env, code, message) {
107
+ try {
108
+ return await readCodexStoredState(env, process.cwd());
109
+ }
110
+ catch {
111
+ fail(code, message);
112
+ }
113
+ }
98
114
  // Exported only so its unit test can reach it. No integration test can cover
99
115
  // it: on Linux glibc, `execvp`'s ENOEXEC falls back to `/bin/sh`, so the spawn
100
116
  // still succeeds, and every other candidate errno is already peeled off by an
@@ -281,7 +297,7 @@ async function runBuild(input, env, log) {
281
297
  // path — three are the frozen CPython wording, and the fourth (the
282
298
  // numeric-overflow diagnostic, which has no CPython oracle wording
283
299
  // to match) now carries the path via its own rewrap in
284
- // src/manifest-overlay.ts. Emit as-is, with no added prefix — a
300
+ // src/harnesses/codex/manifest-overlay.ts. Emit as-is, with no added prefix — a
285
301
  // prefix here would double up the path these messages already
286
302
  // name.
287
303
  log.appendText("stderr", oneLine(cause));
@@ -383,25 +399,27 @@ async function runInstall(packageRoot, env, log) {
383
399
  fail("invalid-arguments", `package root not found: ${packageRoot}`);
384
400
  }
385
401
  const codexBin = env.SUPERPOWERS_CODEX || "codex";
386
- const refreshMode = env.SUPERPOWERS_INSTALL_REFRESH_MODE || "add-only";
402
+ const refreshMode = codexInstallRefreshMode(env);
387
403
  await requireCodex(codexBin, env);
388
- if (refreshMode !== "add-only" && refreshMode !== "remove-add") {
389
- fail("invalid-arguments", `unsupported SUPERPOWERS_INSTALL_REFRESH_MODE: ${refreshMode}`);
404
+ if (refreshMode === null) {
405
+ fail("invalid-arguments", `unsupported SUPERPOWERS_INSTALL_REFRESH_MODE: ${env.SUPERPOWERS_INSTALL_REFRESH_MODE}`);
390
406
  }
391
407
  let entered = false;
392
408
  try {
393
409
  return await withWorkspace(tmpdir(), "superpowers-manager.adapter-install.", async () => {
394
410
  entered = true;
395
411
  const marketplaceList = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
396
- if (commandFailed(marketplaceList)) {
397
- fail("install-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
398
- }
399
412
  let registeredRoot;
400
- try {
401
- registeredRoot = marketplaceRootFromJson(marketplaceList.stdout, MARKETPLACE_NAME);
413
+ if (commandFailed(marketplaceList)) {
414
+ registeredRoot = (await storedStateAfterListingFailure(env, "install-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`)).managerMarketplaceRoot;
402
415
  }
403
- catch {
404
- fail("install-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
416
+ else {
417
+ try {
418
+ registeredRoot = marketplaceRootFromJson(marketplaceList.stdout, MARKETPLACE_NAME);
419
+ }
420
+ catch {
421
+ fail("install-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
422
+ }
405
423
  }
406
424
  if (registeredRoot.length === 0) {
407
425
  const added = await mutationCommand(log, codexBin, ["plugin", "marketplace", "add", packageRoot], env);
@@ -543,16 +561,34 @@ async function runInspect(view, context, env, log) {
543
561
  entered = true;
544
562
  const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
545
563
  if (commandFailed(plugins)) {
546
- fail("inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
547
- }
548
- const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
549
- if (commandFailed(marketplaces)) {
550
- fail("inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
564
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
565
+ const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedListingJson);
566
+ const managerPresent = stored.managerPluginPresent ||
567
+ stored.managerMarketplaceRoot.length > 0;
568
+ const legacyPresent = stored.legacyPluginPresent ||
569
+ stored.legacyMarketplaceRoot !== null;
570
+ return {
571
+ view: "ownership",
572
+ resources: {
573
+ plugin: stored.managerPluginPresent,
574
+ marketplace: true,
575
+ },
576
+ legacy_resources: {
577
+ plugin: stored.legacyPluginPresent,
578
+ marketplace: stored.legacyMarketplaceRoot !== null,
579
+ },
580
+ identity_state: managerPresent
581
+ ? legacyPresent
582
+ ? "both"
583
+ : "manager"
584
+ : legacyPresent
585
+ ? "legacy"
586
+ : "neither",
587
+ conflicts: [...conflicts],
588
+ };
551
589
  }
552
590
  let managerPlugin;
553
591
  let legacyPlugin;
554
- let managerMarketplace;
555
- let legacyMarketplace;
556
592
  let conflicts;
557
593
  try {
558
594
  managerPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", PLUGIN_ID);
@@ -562,6 +598,35 @@ async function runInspect(view, context, env, log) {
562
598
  catch {
563
599
  fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
564
600
  }
601
+ const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
602
+ if (commandFailed(marketplaces)) {
603
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
604
+ const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedListingJson);
605
+ const managerPresent = true;
606
+ const legacyPresent = stored.legacyPluginPresent ||
607
+ stored.legacyMarketplaceRoot !== null;
608
+ return {
609
+ view: "ownership",
610
+ resources: {
611
+ plugin: stored.managerPluginPresent,
612
+ marketplace: true,
613
+ },
614
+ legacy_resources: {
615
+ plugin: stored.legacyPluginPresent,
616
+ marketplace: stored.legacyMarketplaceRoot !== null,
617
+ },
618
+ identity_state: managerPresent
619
+ ? legacyPresent
620
+ ? "both"
621
+ : "manager"
622
+ : legacyPresent
623
+ ? "legacy"
624
+ : "neither",
625
+ conflicts: [...conflicts],
626
+ };
627
+ }
628
+ let managerMarketplace;
629
+ let legacyMarketplace;
565
630
  try {
566
631
  managerMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", MARKETPLACE_NAME);
567
632
  legacyMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", LEGACY_MARKETPLACE_NAME);
@@ -628,6 +693,70 @@ export function codexRemove(input, context) {
628
693
  export function codexInspect(view, context) {
629
694
  return runCodexOperation("inspect", context, (env, log) => runInspect(view, context, env, log));
630
695
  }
696
+ export function codexReadNativeState(context) {
697
+ return runCodexOperation("native-state", context, async (env, log) => {
698
+ const codexBin = env.SUPERPOWERS_CODEX || "codex";
699
+ await requireCodex(codexBin, env);
700
+ const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
701
+ if (commandFailed(plugins)) {
702
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
703
+ return {
704
+ marketplaceRoot: stored.managerMarketplaceRoot,
705
+ pluginPresent: stored.managerPluginPresent,
706
+ pluginEnabled: stored.managerPluginEnabled,
707
+ activeVersion: null,
708
+ activeRoot: null,
709
+ };
710
+ }
711
+ let manager;
712
+ let activeVersion;
713
+ try {
714
+ const managers = codexInstalledPluginsFromJson(plugins.stdout).filter((plugin) => plugin.pluginId === PLUGIN_ID);
715
+ if (managers.length > 1)
716
+ fail("inspect-failed", "Codex manager plugin appears more than once");
717
+ manager = managers[0];
718
+ activeVersion =
719
+ manager === undefined
720
+ ? ""
721
+ : activePluginVersionFromJson(plugins.stdout, PLUGIN_ID);
722
+ }
723
+ catch (cause) {
724
+ if (cause instanceof AdapterFailure)
725
+ throw cause;
726
+ fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
727
+ }
728
+ const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
729
+ if (commandFailed(marketplaces)) {
730
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
731
+ return {
732
+ marketplaceRoot: stored.managerMarketplaceRoot,
733
+ pluginPresent: stored.managerPluginPresent,
734
+ pluginEnabled: stored.managerPluginEnabled,
735
+ activeVersion: null,
736
+ activeRoot: null,
737
+ };
738
+ }
739
+ let marketplaceRoot;
740
+ try {
741
+ marketplaceRoot = marketplaceRootFromJson(marketplaces.stdout, MARKETPLACE_NAME, true);
742
+ }
743
+ catch (cause) {
744
+ if (cause instanceof AdapterFailure)
745
+ throw cause;
746
+ fail("inspect-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
747
+ }
748
+ const searchRoot = env.SUPERPOWERS_INSTALLED_SEARCH_ROOT || codexHome(env, process.cwd());
749
+ return {
750
+ marketplaceRoot: marketplaceRoot || null,
751
+ pluginPresent: manager !== undefined,
752
+ pluginEnabled: manager?.enabled === true,
753
+ activeVersion: activeVersion || null,
754
+ activeRoot: activeVersion
755
+ ? installedRootForVersion(searchRoot, MARKETPLACE_NAME, "superpowers", activeVersion)
756
+ : null,
757
+ };
758
+ });
759
+ }
631
760
  export async function runAdapter(argv, context) {
632
761
  const rawOperation = argv[0];
633
762
  const operation = rawOperation || "adapter";
@@ -1,14 +1,14 @@
1
1
  import { join } from "node:path";
2
- import { validateNativeSkill } from "./artifact-tree.js";
3
- import { classifyPathNoFollow, assertExistingContained } from "./safe-path.js";
2
+ import { validateNativeSkill } from "../../artifact-tree.js";
3
+ import { classifyPathNoFollow, assertExistingContained, } from "../../safe-path.js";
4
4
  import { classifyHooks, readManifest } from "./hooks.js";
5
- import { SEMVER_RE } from "./generated-plugin.js";
6
- import { validateSource } from "./selection.js";
5
+ import { SEMVER_RE } from "../../domain/refs.js";
6
+ import { validateSource } from "../../selection.js";
7
7
  import { writeFile } from "node:fs/promises";
8
- import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "./artifact-tree.js";
9
- import { COMMIT_RE } from "./domain/refs.js";
8
+ import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
9
+ import { COMMIT_RE } from "../../domain/refs.js";
10
10
  import { validateGeneratedPlugin } from "./generated-plugin.js";
11
- import { SafetyError } from "./safety-error.js";
11
+ import { SafetyError } from "../../safety-error.js";
12
12
  export async function writeCodexAssessment(root, selection, manifestSource) {
13
13
  const receipt = {
14
14
  schema: 1,
@@ -1,6 +1,6 @@
1
1
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { codexInstalledPluginsFromJson, } from "./codex-json.js";
3
+ import { codexInstalledPluginsFromJson, } from "./json.js";
4
4
  export const CODEX_MANAGER_PLUGIN_ID = "superpowers@superpowers-manager";
5
5
  export const CODEX_LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
6
6
  const SUPERPOWERS_PLUGIN_PREFIX = "superpowers@";