superpowers-manager 0.5.2 → 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.
package/README.md CHANGED
@@ -113,18 +113,20 @@ The [usage reference](https://github.com/j7an/superpowers-manager/blob/main/docs
113
113
  The installed package requires Node >=24. Native source, tests, and packaging tooling require Node >=24.12.0. Every other requirement is command-specific and is checked before dispatch. This table is derived from production by `tests/bin/readme-requirements.test.ts`.
114
114
 
115
115
  <!-- requirements:begin -->
116
- | Command | git | Python 3 | Codex CLI (default) | Pi CLI (`--harness pi`) |
117
- |---|---|---|---|---|
118
- | `pin` | yes | no | no | no |
119
- | `track-latest` | no | no | no | no |
120
- | `unpin` | no | no | no | no |
121
- | `prepare` | yes | only with SUPERPOWERS_VALIDATOR | no | no |
122
- | `probe` | yes | no | yes | no |
123
- | `install` | yes | no | yes | yes |
124
- | `update` | yes | no | yes | yes |
125
- | `uninstall` | no | no | yes | yes |
116
+ | Command | git | Codex CLI (default) | Pi CLI (`--harness pi`) |
117
+ |---|---|---|---|
118
+ | `pin` | yes | no | no |
119
+ | `track-latest` | no | no | no |
120
+ | `unpin` | no | no | no |
121
+ | `prepare` | yes | no | no |
122
+ | `probe` | yes | yes | no |
123
+ | `install` | yes | yes | yes |
124
+ | `update` | yes | yes | yes |
125
+ | `uninstall` | no | yes | yes |
126
126
  <!-- requirements:end -->
127
127
 
128
+ Generated Codex manifests preserve upstream JSON number tokens while rejecting non-finite decimal or exponent values.
129
+
128
130
  macOS and Linux are tested. WSL2 is supported for the established Codex path; Pi in WSL2 and native Windows remain untested. The manager runs in-process and does not require Git Bash. The [Pi reference](https://github.com/j7an/superpowers-manager/blob/main/docs/pi.md#runtime-compatibility) explains its runtime qualification and admission checks.
129
131
 
130
132
  Native test versions qualify integration mechanisms; they are not runtime allowlists, and an untested runtime is not automatically certified as compatible.
package/dist/cli.js CHANGED
@@ -46,8 +46,8 @@ const IN_PROCESS_HANDLERS = {
46
46
  // required because `spw_invoke_adapter` ran validate-adapter-response.py once
47
47
  // per adapter call
48
48
  // (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:37-44::--response "$response_file" --result "$result_file" \`);
49
- // the in-process path has no validator process. It remains CONDITIONAL for
50
- // `prepare` through commandRequirements(env) below, unchanged from slice 3.4.
49
+ // the in-process path no longer uses that adapter-response validator.
50
+ // No command has a Python requirement in commandRequirements(env).
51
51
  // No command requires a POSIX shell any more.
52
52
  const SHARED_COMMAND_REQUIREMENTS = {
53
53
  pin: ["git"],
@@ -170,13 +170,6 @@ function findTool(name, env, platform) {
170
170
  }
171
171
  return null;
172
172
  }
173
- // python3 is required by `prepare` only when SUPERPOWERS_VALIDATOR names one:
174
- // after the port, that optional spawn (runValidator, in src/validator.ts,
175
- // called from src/commands/prepare.ts) is Python's only remaining consumer
176
- // on the prepare path. The conditional lives here, in the accessor preflight reads,
177
- // rather than inside preflight — an accessor that under-reports what
178
- // preflight enforces is the blind spot slice 2 closed when it made
179
- // CLI-PREFLIGHT-01 derive its map from production.
180
173
  function sharedRequirement(name) {
181
174
  return {
182
175
  name,
@@ -188,9 +181,6 @@ function sharedRequirement(name) {
188
181
  function commandRequirementsFor(env, adapter) {
189
182
  const forCommand = (command) => {
190
183
  const shared = SHARED_COMMAND_REQUIREMENTS[command].map(sharedRequirement);
191
- if (command === "prepare" && env.SUPERPOWERS_VALIDATOR) {
192
- shared.push(sharedRequirement("python3"));
193
- }
194
184
  return [...shared, ...adapter.requirements(command, env)];
195
185
  };
196
186
  return {
@@ -217,6 +207,8 @@ function commandRequirements(env) {
217
207
  // spawned command in-process, so there is no shell to discover.
218
208
  function preflightFor(cmd, env, platform, adapter) {
219
209
  const errors = [...configurationErrors(cmd, env)];
210
+ if (errors.length > 0)
211
+ return { ok: false, errors };
220
212
  for (const requirement of commandRequirementsFor(env, adapter)[cmd]) {
221
213
  const found = requirement.lookup === "explicit-path-or-path" &&
222
214
  requirement.executable.includes(path.sep)
@@ -260,9 +252,12 @@ function usage() {
260
252
  "SUPERPOWERS_UPSTREAM_URL, SUPERPOWERS_CODEX, SUPERPOWERS_CACHE_DIR,",
261
253
  "SUPERPOWERS_CONFIG_DIR, XDG_CONFIG_HOME,",
262
254
  "SUPERPOWERS_PLUGIN_ROOT, SUPERPOWERS_MANIFEST_TEMPLATE,",
263
- "SUPERPOWERS_VALIDATOR, SUPERPOWERS_VALIDATOR_EXECUTABLE,",
255
+ "SUPERPOWERS_VALIDATOR_EXECUTABLE,",
264
256
  "SUPERPOWERS_INSTALLED_SEARCH_ROOT, SUPERPOWERS_INSTALL_REFRESH_MODE",
265
257
  "",
258
+ "SUPERPOWERS_VALIDATOR is removed; unset it and use",
259
+ "SUPERPOWERS_VALIDATOR_EXECUTABLE with an executable validator.",
260
+ "",
266
261
  "Selection state uses SUPERPOWERS_CONFIG_DIR when set; otherwise it uses",
267
262
  "$XDG_CONFIG_HOME/superpowers-manager, then $HOME/.config/superpowers-manager.",
268
263
  ].join("\n");
@@ -163,9 +163,8 @@ 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/harnesses/codex/presentation.ts:329::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
167
- // exists for this result-bearing path; the lifecycle compatibility
168
- // export delegates through the same arm. Returning
166
+ // (`src/harnesses/codex/presentation.ts:305::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
167
+ // exists for this result-bearing path. Returning
169
168
  // failed() instead reported the adapter's own generic diagnostic and
170
169
  // dropped the post-install verification claim -- a mutation had
171
170
  // already been issued at stage 3,
@@ -3,6 +3,7 @@ import { upstreamCacheRoot } from "../upstream-workspace.js";
3
3
  import { SafetyError } from "../safety-error.js";
4
4
  import { isAbsolute } from "node:path";
5
5
  import { oneLine } from "../cli-arguments.js";
6
+ import { configurationErrors } from "../validator.js";
6
7
  export async function withMutation(command, ctx, action) {
7
8
  // Selection validation precedes every adapter call and lock creation. A
8
9
  // nested mutation receives the already resolved value and cannot observe a
@@ -50,6 +51,12 @@ export async function withMutation(command, ctx, action) {
50
51
  return await ctx.coordination.withResources(resources, async () => action(scoped));
51
52
  }
52
53
  export async function runWithMutation(command, ctx, action) {
54
+ const errors = configurationErrors(command, ctx.env);
55
+ if (errors.length > 0) {
56
+ for (const message of errors)
57
+ ctx.stderr.write(`error: ${message}\n`);
58
+ return 1;
59
+ }
53
60
  let actionThrew = false;
54
61
  try {
55
62
  return await withMutation(command, ctx, async (scoped) => {
@@ -7,7 +7,7 @@ import { runGit } from "../git.js";
7
7
  import { SafetyError } from "../safety-error.js";
8
8
  import { fetchExactCommit, gitSafeSource } from "../upstream.js";
9
9
  import { upstreamCacheRoot } from "../upstream-workspace.js";
10
- import { BOUNDED_EXECUTABLE, UNBOUNDED_LEGACY, launchFailureMessage, resolveValidator, runValidator, displayPath, } from "../validator.js";
10
+ import { BOUNDED_EXECUTABLE, launchFailureMessage, resolveValidator, runValidator, displayPath, } from "../validator.js";
11
11
  import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
12
12
  import { replayOutcome } from "./probe.js";
13
13
  import { runWithMutation } from "./mutation.js";
@@ -18,18 +18,6 @@ import { runWithMutation } from "./mutation.js";
18
18
  function prepareError(message, cause) {
19
19
  return new SafetyError("prepare", message, { cause });
20
20
  }
21
- // `[ -f ]` — regular file. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:108::[ -f "$additional_validator` uses -f, and
22
- // tests/baseline/cli-parity.test.js's "CLI-ENV-MANIFEST-TEMPLATE-01 fallback
23
- // template bytes and non-file rejection" also covers the separately extracted
24
- // fallback check.
25
- async function regularFileExists(path) {
26
- try {
27
- return (await stat(path)).isFile();
28
- }
29
- catch {
30
- return false;
31
- }
32
- }
33
21
  // `[ -d ]` — `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:50::if [ -d`. A regular file named `.git` is what a git
34
22
  // worktree or `clone --separate-git-dir` leaves behind; `-e` would take the
35
23
  // fetch branch and let git follow its `gitdir:` pointer, where the shell took
@@ -112,7 +100,6 @@ async function gatherPrepare(ctx) {
112
100
  throw prepareError("adapter returned an invalid preparation staging leaf");
113
101
  }
114
102
  const pluginRoot = location.destinationRoot;
115
- const additionalValidator = env.SUPERPOWERS_VALIDATOR || "";
116
103
  const executableValidator = env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "";
117
104
  const tmpParent = dirname(pluginRoot);
118
105
  await owned(`cannot create directory: ${tmpParent}`, () => mkdir(tmpParent, { recursive: true }));
@@ -214,37 +201,7 @@ async function gatherPrepare(ctx) {
214
201
  return failed(prepared.outcome.result.compatibility.reason);
215
202
  }
216
203
  let validator = NO_VALIDATOR_OUTPUT;
217
- if (additionalValidator.length > 0) {
218
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:108::[ -f "$additional_validator` — `[ -f ]`.
219
- if (!(await regularFileExists(additionalValidator))) {
220
- return {
221
- kind: "failed",
222
- outcomes,
223
- validator,
224
- message: `additional plugin validator not found: ${additionalValidator}`,
225
- };
226
- }
227
- const ran = await runValidator(["python3", additionalValidator, candidate], UNBOUNDED_LEGACY, env, workspace);
228
- if (ran.kind === "launchFailed") {
229
- // PARITY, and it must stay a THROW. The legacy path rejects the workspace
230
- // callback on a spawn failure; withWorkspace returns the callback error
231
- // and the `outcomes` collected above are LOST. That loss is a recorded,
232
- // deliberately unassigned defect. Returning a failed outcome here would
233
- // replay those outcomes instead -- observably different control flow for
234
- // CLI-ENV-VALIDATOR-01, which this PR is not authorized to change.
235
- throw prepareError(`cannot execute additional plugin validator: ${additionalValidator}`, ran.cause);
236
- }
237
- validator = { stdout: ran.stdout.text, stderr: ran.stderr.text };
238
- if (ran.kind !== "exited" || ran.code !== 0) {
239
- return {
240
- kind: "failed",
241
- outcomes,
242
- validator,
243
- message: "additional plugin validation failed",
244
- };
245
- }
246
- }
247
- else if (executableValidator.length > 0) {
204
+ if (executableValidator.length > 0) {
248
205
  const resolution = await resolveValidator(executableValidator);
249
206
  const ran = await runValidator([executableValidator, candidate], BOUNDED_EXECUTABLE, env, workspace);
250
207
  // The disclosure lands BEFORE any early return, so a launch failure still
@@ -336,10 +293,7 @@ async function performPrepare(argv, ctx) {
336
293
  catch (cause) {
337
294
  // Hand-written messages, per AGENTS.md's reader-diagnostics rule.
338
295
  // Reachable here: prepareError(), from this module's owned() wrappers,
339
- // its two manifest-version checks, asResolutionKind, and the
340
- // additional-validator branch's own throw on a legacy-validator launch
341
- // failure (its cause is the shared runner's (src/validator.ts) captured
342
- // spawn error, which oneLine never reads -- it takes .message only);
296
+ // its two manifest-version checks, and asResolutionKind;
343
297
  // readManifest's three hookError messages
344
298
  // (`src/harnesses/codex/hooks.ts:113-138::readManifest`), pinned by
345
299
  // `tests/unit/harnesses/codex/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
@@ -382,7 +336,7 @@ async function performPrepare(argv, ctx) {
382
336
  // only, so it bounds how much of that text lands, not what it may
383
337
  // contain.
384
338
  //
385
- // runAdapter's closing `throw cause` (src/harnesses/codex/adapter.ts) does NOT arrive
339
+ // runCodexOperation's closing `throw cause` (src/harnesses/codex/adapter.ts) does NOT arrive
386
340
  // here -- the call site catches it and converts it to a hand-written
387
341
  // message per AGENTS.md's reader-diagnostics rule.
388
342
  //
@@ -418,7 +372,7 @@ async function performPrepare(argv, ctx) {
418
372
  // completed before cleanup ran, so it is not being reported as unverified
419
373
  // -- but something did still go wrong, and AGENTS.md's fail-closed rule
420
374
  // extends to it. Mirrors
421
- // `src/commands/install.ts:562-570::if (cleanupWarning`.
375
+ // `src/commands/install.ts:561-570::if (cleanupWarning`.
422
376
  ctx.stderr.write(`error: ${cleanupWarning}\n`);
423
377
  return 1;
424
378
  }
@@ -7,7 +7,7 @@ export const PROBE_USAGE = "error: usage: superpowers-manager probe [--porcelain
7
7
  // response, so adapter messages reached the operator on their declared streams
8
8
  // in array order, and a controlled failure printed `error:` plus one `hint:`
9
9
  // per hint. DIAG-ADAPTER-01 retains that contract
10
- // (docs/baseline/protocol-disposition.md); dropping it here would be a
10
+ // (docs/adapter-result-contract.md); dropping it here would be a
11
11
  // silent diagnostics regression, not a simplification.
12
12
  //
13
13
  // Interpolating error.message and each hint is the sanctioned form (AGENTS.md):
@@ -113,14 +113,14 @@ function validControl(value) {
113
113
  candidate.probeEligibility.kind === "blocked" &&
114
114
  candidate.mutationEligibility.kind === "blocked")));
115
115
  }
116
- // `runAdapter` reports a CONTROLLED failure by RETURN VALUE, not by throwing
116
+ // `runCodexOperation` reports a CONTROLLED failure by RETURN VALUE, not by throwing
117
117
  // (`src/adapter-result.ts:32-35::export interface AdapterResult`). The shell got
118
118
  // fail-closed behaviour for free: spw_invoke_adapter returned 1 and
119
119
  // scripts/probe ran under `set -eu`.
120
120
  // Omitting the status check here would read a failed inspection as absent
121
121
  // evidence and report it as success.
122
122
  //
123
- // It does still THROW for a non-AdapterFailure cause (runAdapter's closing
123
+ // It does still THROW for a non-AdapterFailure cause (runCodexOperation's closing
124
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
  //
@@ -287,8 +287,7 @@ export async function gatherProbe(ctx) {
287
287
  export async function runProbe(argv, ctx) {
288
288
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:42::porcelain` tested only `[ "${1:-}" = "--porcelain" ]`, so a typo'd
289
289
  // flag silently produced human output. Rejecting it is a deliberate
290
- // narrowing, recorded as a port-only entry in
291
- // tests/migration-inventory/probe.md.
290
+ // narrowing from the historical shell behavior.
292
291
  //
293
292
  // This guard is NOT the production path. src/cli.ts's parseArgs rejects the
294
293
  // same inputs first, before preflight, with the usage block the CLI's other
@@ -365,10 +364,9 @@ export async function runProbe(argv, ctx) {
365
364
  //
366
365
  // fetchExactCommit is deliberately NOT in this list, unlike prepare's
367
366
  // fetchExactCommit exception, which runPrepare's catch block documents.
368
- // Its only callers are `src/upstream-cli.ts:83::await fetchExactCommit` and
369
- // gatherPrepare's own call in src/commands/prepare.ts (git grep -n
370
- // fetchExactCommit -- src/ is the check that keeps "only" true), so probe
371
- // never reaches it and
367
+ // Its only remaining caller is gatherPrepare's call in
368
+ // src/commands/prepare.ts (git grep -n fetchExactCommit -- src/ is the
369
+ // check that keeps "only" true), so probe never reaches it and
372
370
  // its splice sites cannot appear on this stream. Do not add it back by
373
371
  // symmetry with prepare.
374
372
  //
@@ -376,7 +374,7 @@ export async function runProbe(argv, ctx) {
376
374
  // all. It (src/provenance.ts) delegates to readGeneratedCommitLenient,
377
375
  // which catches every failure and returns "".
378
376
  //
379
- // A non-AdapterFailure re-thrown by runAdapter's closing `throw cause`
377
+ // A non-AdapterFailure re-thrown by runCodexOperation's closing `throw cause`
380
378
  // (src/harnesses/codex/adapter.ts) does NOT reach here: inspect() catches it and converts
381
379
  // it to a hand-written message per AGENTS.md's reader-diagnostics rule -- a
382
380
  // rethrown cause is exactly the failure src/harnesses/codex/adapter.ts declined to own, so
@@ -125,7 +125,7 @@ async function gatherUninstall(ctx) {
125
125
  catch (cause) {
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
- // inside invoke(), and presenceFlag/verifyUninstalledResources/
128
+ // inside invoke(), and presenceFlag/
129
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
@@ -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/harnesses/codex/adapter.ts:1081::async function runCodexOperation<T = JsonValue>(`) does NOT
166
+ // (`src/harnesses/codex/adapter.ts:829::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.
@@ -34,9 +34,9 @@ export function selectionConfigDir(env) {
34
34
  export function selectionStatePath(env) {
35
35
  return `${selectionConfigDir(env)}/selection.json`;
36
36
  }
37
- // Composes exactly what `src/selection-state-cli.ts:39::const normalized`
38
- // composes. The shell wrote a normalized document to a mktemp file and read
39
- // five fields back with five python3 invocations; in-process this is one call.
37
+ // Normalizes the record returned by readSelectionState directly. The shell
38
+ // wrote a normalized document to a mktemp file and read five fields back with
39
+ // five python3 invocations; in-process this is one call.
40
40
  export async function loadSavedSelection(env) {
41
41
  return normalizeSaved(await readSelectionState(selectionStatePath(env)));
42
42
  }