superpowers-manager 0.3.0 → 0.4.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.
@@ -3,61 +3,28 @@
3
3
  // and the adapter arrives through ctx.adapter.
4
4
  import { tmpdir } from "node:os";
5
5
  import { oneLine } from "../cli-arguments.js";
6
- import { reportLegacyState, verifyUninstalledResources } from "../lifecycle.js";
7
6
  import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
8
7
  import { replayOutcome } from "./probe.js";
9
- function presenceFlag(result, key) {
10
- const outcome = result.outcome;
11
- if (result.status !== 0 || !outcome.ok) {
12
- return { kind: "call-failed" };
13
- }
14
- const value = outcome.result;
15
- // Mirrors src/lifecycle.ts's verifyUninstalledResources: a missing or
16
- // non-object `resources` falls THROUGH to the Boolean check rather than
17
- // getting its own message, for parity with
18
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:70::expected`'s
19
- // single "expected Boolean..." text on {}.
20
- const resources = typeof value === "object" && value !== null && !Array.isArray(value)
21
- ? value.resources
22
- : undefined;
23
- const bag = typeof resources === "object" &&
24
- resources !== null &&
25
- !Array.isArray(resources)
26
- ? resources
27
- : {};
28
- const flag = bag[key];
29
- if (typeof flag !== "boolean") {
30
- return { kind: "malformed", key };
31
- }
32
- return { kind: "ok", value: flag };
33
- }
34
- async function invoke(ctx, env, argv, outcomes) {
8
+ async function invoke(call, failure, outcomes) {
35
9
  let result;
36
10
  try {
37
- result = await ctx.adapter(argv, { root: ctx.root, env });
11
+ result = await call();
38
12
  }
39
13
  catch {
40
- return {
41
- ok: false,
42
- message: `cannot invoke Codex adapter for ${argv.join(" ")}`,
43
- };
14
+ return { ok: false, message: failure.unexpected };
44
15
  }
45
- // Clause 1: replay every outcome, on both the success and the failure
46
- // path, before any decision -- collected here and replayed by the caller
47
- // once gatherUninstall's try/catch has resolved, for the same EPIPE reason
48
- // gatherProbe carries its outcomes out rather than writing in place
49
- // (`src/commands/probe.ts:289::The outcomes are CARRIED OUT rather than replayed in place`).
50
16
  outcomes.push(result.outcome);
51
17
  const outcome = result.outcome;
52
18
  if (result.status !== 0 || !outcome.ok) {
53
19
  return {
54
20
  ok: false,
55
- message: outcome.ok
56
- ? `adapter reported a failure status for ${argv.join(" ")}`
57
- : null,
21
+ message: outcome.ok ? failure.invalidStatus : null,
58
22
  };
59
23
  }
60
- return { ok: true, result };
24
+ return {
25
+ ok: true,
26
+ result: { status: result.status, outcome },
27
+ };
61
28
  }
62
29
  // withWorkspace throws for mkdtemp failure before the callback ever runs
63
30
  // ("cannot create workspace",
@@ -85,7 +52,7 @@ class GatherFailure extends Error {
85
52
  }
86
53
  // Every step that can throw or fail closed, returning the outcome as data and
87
54
  // performing NO writes. Same shape as gatherProbe
88
- // (`src/commands/probe.ts:280-281::readonly status: 0;`) and for the same
55
+ // (`src/commands/probe.ts:114-117::readonly facts: ProbeSnapshot<R>;`) and for the same
89
56
  // reason: a write inside this try could raise EPIPE, be caught here, and be
90
57
  // relabelled as a domain failure.
91
58
  async function gatherUninstall(ctx) {
@@ -108,106 +75,41 @@ async function gatherUninstall(ctx) {
108
75
  status: 1,
109
76
  outcomes,
110
77
  message,
78
+ output: null,
111
79
  });
80
+ const adapterContext = { root: ctx.root, env };
112
81
  // Stage 1: inspect ownership, before removal.
113
- const first = await invoke(ctx, env, ["inspect", "--view", "ownership"], outcomes);
114
- if (!first.ok)
115
- return failed(first.message);
116
- const pluginFlag = presenceFlag(first.result, "plugin");
117
- if (pluginFlag.kind === "malformed") {
118
- return failed(`expected a Boolean adapter result at resources.${pluginFlag.key}`);
119
- }
120
- if (pluginFlag.kind === "call-failed") {
121
- // Unreachable via this call path: `first.ok` above already proved
122
- // status === 0 && outcome.ok, the only inputs presenceFlag reads
123
- // to decide this arm. Handled anyway so presenceFlag stays total
124
- // and this switch stays exhaustive rather than assuming its caller
125
- // always gates first.
126
- return failed(null);
127
- }
128
- const marketplaceFlag = presenceFlag(first.result, "marketplace");
129
- if (marketplaceFlag.kind === "malformed") {
130
- return failed(`expected a Boolean adapter result at resources.${marketplaceFlag.key}`);
131
- }
132
- if (marketplaceFlag.kind === "call-failed") {
133
- return failed(null);
134
- }
135
- // Stage 2: uninstall, with the two presence Booleans read above.
136
- const uninstallStage = await invoke(ctx, env, [
137
- "uninstall",
138
- "--plugin-present",
139
- String(pluginFlag.value),
140
- "--marketplace-present",
141
- String(marketplaceFlag.value),
142
- ], outcomes);
143
- if (!uninstallStage.ok)
144
- return failed(uninstallStage.message);
82
+ const beforeFailure = ctx.adapter.presentation.callFailure("remove-ownership", adapterContext);
83
+ const before = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), beforeFailure, outcomes);
84
+ if (!before.ok)
85
+ return failed(before.message);
86
+ // Stage 2: remove, passing the private input through untouched.
87
+ const removalInput = before.result.outcome.result.removalInput;
88
+ const removeFailure = ctx.adapter.presentation.callFailure("remove", adapterContext, removalInput);
89
+ const removed = await invoke(() => ctx.adapter.remove(removalInput, adapterContext), removeFailure, outcomes);
90
+ if (!removed.ok)
91
+ return failed(removed.message);
145
92
  // Stage 3: inspect ownership AGAIN. This overwrites the first
146
93
  // inspection (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:29-30::spw_verify_uninstalled_resources`), so everything below reads the
147
94
  // POST-uninstall state, not the pre-uninstall one read above.
148
- const second = await invoke(ctx, env, ["inspect", "--view", "ownership"], outcomes);
149
- if (!second.ok)
150
- return failed(second.message);
151
- const verify = verifyUninstalledResources(second.result);
152
- if (!verify.ok)
153
- return failed(verify.message);
154
- // identity_state comes from this SAME second inspection, matching
155
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:31::spw_adapter_result_get`'s spw_adapter_result_get read of the
156
- // overwritten inspect_result. `second.ok` already proved this call
157
- // succeeded, so outcome.ok is true here; the explicit check below is
158
- // for TypeScript's narrowing (a local variable, not a re-derivation
159
- // of that fact) rather than a live branch.
160
- const secondOutcome = second.result.outcome;
161
- if (!secondOutcome.ok)
162
- return failed(null);
163
- // Mirrors the inspect null/non-string branches
164
- // (`src/commands/probe.ts:256-263::if (value === null || value === undefined) {`)
165
- // and the fingerprint read
166
- // (`src/lifecycle.ts:181-194::const raw = inspected.value.fingerprint;`):
167
- // a JSON null or a missing key defaults to "" (the Python reader's own
168
- // convention for a JSON null, scripts/core/provenance.sh's
169
- // spw_json_get), but a present, non-null, NON-STRING value is a
170
- // distinct, fail-closed "malformed" case with its own text -- never
171
- // silently stringified.
172
- // (A previous draft of this comment claimed parity with the shell's
173
- // stringify-and-compare behaviour at
174
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/provenance.sh:62::print(value`;
175
- // that was wrong on inspection, and it cited these same two call sites
176
- // as support even though both of them fail closed on a non-string value
177
- // rather than stringifying it. AGENTS.md's
178
- // fail-closed rule wins over shell parity here.)
179
- const parsed = secondOutcome.result;
180
- const identityRaw = parsed?.identity_state;
181
- let identityState;
182
- if (identityRaw === null || identityRaw === undefined) {
183
- identityState = "";
184
- }
185
- else if (typeof identityRaw === "string") {
186
- identityState = identityRaw;
187
- }
188
- else {
189
- return failed("adapter returned a non-string identity_state for inspect --view ownership");
95
+ const afterFailure = ctx.adapter.presentation.callFailure("post-remove", adapterContext);
96
+ const after = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), afterFailure, outcomes);
97
+ if (!after.ok)
98
+ return failed(after.message);
99
+ const ownership = after.result.outcome.result;
100
+ if (ownership.removalVerification.kind === "blocked") {
101
+ return {
102
+ status: 1,
103
+ outcomes,
104
+ message: null,
105
+ output: ownership.removalVerification.output,
106
+ };
190
107
  }
191
- const verdict = reportLegacyState(identityState);
192
- if (verdict.kind === "unknown")
193
- return failed(verdict.message);
194
- const lines = [];
195
- // verdict.kind is "ok" or "report" here (reportLegacyState never
196
- // returns "blocked" -- that arm belongs to requireNoLegacyState --
197
- // but LegacyVerdict is one shared union, so this narrows rather than
198
- // assumes).
199
- if (verdict.kind !== "ok") {
200
- lines.push(...verdict.lines);
201
- }
202
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:34-35::complete`. The first line ports verbatim; the second
203
- // changes scripts/prepare to npx superpowers-manager prepare (spec
204
- // §3.6) -- a deliberate, observable divergence, recorded as a
205
- // port-only entry in tests/migration-inventory/uninstall-commands.md.
206
- lines.push("uninstall complete");
207
- lines.push("note: local generated artifacts under plugins/superpowers/ and " +
208
- ".cache/upstream/ were left in place; remove them manually or " +
209
- "regenerate with npx superpowers-manager prepare.");
210
- return { status: 0, outcomes, lines };
108
+ return {
109
+ status: 0,
110
+ outcomes,
111
+ output: ctx.adapter.presentation.renderRemovalCompletion(ownership),
112
+ };
211
113
  }, {
212
114
  // Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure,
213
115
  // so the UninstallOutcome the callback already computed still comes
@@ -257,7 +159,7 @@ export async function runUninstall(argv, ctx) {
257
159
  // blindly.
258
160
  //
259
161
  // A cause outside ctx.adapter's AdapterFailure guard
260
- // (`src/adapter.ts:1001::if (cause instanceof AdapterFailure) {`) does NOT
162
+ // (`src/adapter.ts:973-999::async function runCodexOperation(`) does NOT
261
163
  // reach here: invoke() catches it inside gatherUninstall and converts it
262
164
  // to a hand-written message carried as UninstallOutcome data, exactly as
263
165
  // src/commands/probe.ts's inspect() does for the same cause.
@@ -285,12 +187,19 @@ export async function runUninstall(argv, ctx) {
285
187
  if (outcome.message !== null) {
286
188
  ctx.stderr.write(`error: ${outcome.message}\n`);
287
189
  }
190
+ if (outcome.output !== null) {
191
+ for (const line of outcome.output.stdout)
192
+ ctx.stdout.write(`${line}\n`);
193
+ for (const line of outcome.output.stderr)
194
+ ctx.stderr.write(`${line}\n`);
195
+ }
288
196
  status = 1;
289
197
  }
290
198
  else {
291
- for (const line of outcome.lines) {
199
+ for (const line of outcome.output.stdout)
292
200
  ctx.stdout.write(`${line}\n`);
293
- }
201
+ for (const line of outcome.output.stderr)
202
+ ctx.stderr.write(`${line}\n`);
294
203
  status = 0;
295
204
  }
296
205
  if (cleanupWarning !== null) {
@@ -6,36 +6,14 @@
6
6
  // is also why update opens no temporary workspace of its own -- there is no
7
7
  // mutation here for one to protect.
8
8
  import { oneLine } from "../cli-arguments.js";
9
- import { requireManagedUpdateControl, requireNoLegacyState, } from "../lifecycle.js";
10
- import { formatPorcelain, gatherProbe, replayOutcome, } from "./probe.js";
9
+ import { gatherProbe, replayOutcome } from "./probe.js";
11
10
  import { runInstall } from "./install.js";
12
11
  import { runPrepare } from "./prepare.js";
13
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/update:29-32::echo "$probe_output`'s `case ... *)` wildcard. Same unreachability shape as
14
- // src/commands/install.ts's renderUnknownProbeStatus: statusForCommits
15
- // (src/status.ts) can only ever return "needs prepare", "needs
16
- // install" or "current", so this branch is NOT reachable through runUpdate's
17
- // own call to gatherProbe today. ProbeFacts.status is typed `string`, not
18
- // that three-literal union, so a future caller that builds facts by hand (as
19
- // this module's own unit test does, directly) must still see it fail closed
20
- // rather than silently proceed.
21
- //
22
- // Exported specifically so a direct test can reach it -- not part of the
23
- // interface Task 5 or Task 8 consume. A test against this export proves the
24
- // two writes below are correct for a given `facts`; it does NOT prove that
25
- // runUpdate's own final `else` branch actually reaches and calls this
26
- // function, or that runUpdate returns 1 afterward -- those are properties of
27
- // the call site, not of this function, and need their own coverage there.
28
- export function renderUnknownProbeStatus(facts, ctx) {
29
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/update:30-31::echo "$probe_output` -- `echo "$probe_output"` (stdout), then the error
30
- // to stderr via an explicit `>&2`. This is a DIFFERENT mechanism from
31
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:30-31::printf`'s `spw_die`, even though both end up writing the
32
- // porcelain to stdout and an `error: `-prefixed line to stderr: install's
33
- // error text flows through spw_die's own `error: $*` formatting, while
34
- // update's is spelled out inline in the shell literal already. The two
35
- // command modules therefore each need their own case pinning this arm --
36
- // one must not be inferred from the other.
37
- ctx.stdout.write(formatPorcelain(facts));
38
- ctx.stderr.write(`error: unknown probe status: ${facts.status}\n`);
12
+ function writeOutput(output, ctx) {
13
+ for (const line of output.stdout)
14
+ ctx.stdout.write(`${line}\n`);
15
+ for (const line of output.stderr)
16
+ ctx.stderr.write(`${line}\n`);
39
17
  }
40
18
  export async function runUpdate(argv, ctx) {
41
19
  // scripts/update never reads "$@", so extra arguments are silently
@@ -52,7 +30,7 @@ export async function runUpdate(argv, ctx) {
52
30
  // install's NOTE line.
53
31
  //
54
32
  // This is a THIRD consumer of gatherProbe's throw channel --
55
- // `src/commands/probe.ts:381-433::THREE exceptions, all inherited and none a regression:`'s
33
+ // `src/commands/probe.ts:251-309::THREE exceptions, all inherited and none a regression:`'s
56
34
  // runProbe catch is the first and src/commands/install.ts's runInstall catch
57
35
  // is the second. Because all three wrap the identical function, runProbe's
58
36
  // long comment there enumerates exactly what can reach this stream too; not
@@ -70,69 +48,32 @@ export async function runUpdate(argv, ctx) {
70
48
  return 1;
71
49
  }
72
50
  const facts = probe.facts;
73
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/update:9-10::identity_state=`. Guards the PROBE-derived value only, matching
74
- // src/commands/install.ts's identical guard at `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:19-20::identity_state=$(spw_probe_field`: a
75
- // probe that reports no identity state at all is a different failure from
76
- // one that reports an unrecognised one, and requireNoLegacyState("") would
77
- // otherwise reach its "unknown" arm and print a symptom rather than the
78
- // actual cause.
79
- if (facts.identityState.length === 0) {
80
- ctx.stderr.write("error: probe did not report adapter identity state\n");
51
+ if (facts.ownership.installEligibility.kind === "blocked") {
52
+ writeOutput(facts.ownership.installEligibility.output, ctx);
81
53
  return 1;
82
54
  }
83
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/update:11::spw_require_no_legacy_state`.
84
- const legacy = requireNoLegacyState(facts.identityState);
85
- if (legacy.kind === "blocked") {
86
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:50-53::'Legacy superpowers-wrapper Codex state is`
87
- // is a single printf writing three bare lines to stderr, no `error: `
88
- // prefix; :54 is the `return 1` that follows it, reached without spw_die.
89
- for (const line of legacy.lines)
90
- ctx.stderr.write(`${line}\n`);
55
+ if (facts.control.probeEligibility.kind === "blocked") {
56
+ writeOutput(facts.control.probeEligibility.output, ctx);
91
57
  return 1;
92
58
  }
93
- if (legacy.kind === "unknown") {
94
- ctx.stderr.write(`error: ${legacy.message}\n`);
95
- return 1;
96
- }
97
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/update:13-14::update_control=`. install checks only identity_state
98
- // (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:20::report`); update checks update_control too -- the first of
99
- // §4.4's two corrections. Both emptiness checks run before the switch, not
100
- // just one.
101
- if (facts.updateControl.length === 0) {
102
- ctx.stderr.write("error: probe did not report adapter update-control capability\n");
103
- return 1;
104
- }
105
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/update:16-34::case`, the four-way switch.
106
- if (facts.status === "current") {
107
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/update:18::spw_require_managed_update_control` -- gated BEFORE printing anything. §4.4's second
108
- // correction: an update that printed "manager is current" under an
109
- // unsupported adapter would be asserting managed control it had not
110
- // verified.
111
- const managed = requireManagedUpdateControl(facts.updateControl);
112
- if (!managed.ok) {
113
- ctx.stderr.write(`error: ${managed.message}\n`);
114
- return 1;
59
+ switch (facts.status) {
60
+ case "current": {
61
+ if (facts.control.mutationEligibility.kind === "blocked") {
62
+ writeOutput(facts.control.mutationEligibility.output, ctx);
63
+ return 1;
64
+ }
65
+ const rendered = ctx.adapter.presentation.renderProbe(facts);
66
+ ctx.stdout.write(rendered.porcelain);
67
+ ctx.stdout.write(`${ctx.adapter.presentation.currentNotice}\n`);
68
+ return 0;
115
69
  }
116
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/update:19-20::printf`. Unlike install, which never lets the porcelain
117
- // reach the terminal on a successful run (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:18::porcelain`'s
118
- // probe_output=$(...) capture), update prints it here.
119
- ctx.stdout.write(formatPorcelain(facts));
120
- ctx.stdout.write("manager is current\n");
121
- return 0;
122
- }
123
- if (facts.status === "needs prepare") {
124
- // Called as a FUNCTION: a failure propagates as a status, never through
125
- // `set -eu`. runPrepare has already replayed its own outcomes and
126
- // written its own diagnostics by the time it returns, so nothing further
127
- // is written here on that path.
128
- const prepareStatus = await runPrepare([], ctx);
129
- if (prepareStatus !== 0)
130
- return prepareStatus;
131
- return await runInstall([], ctx);
132
- }
133
- if (facts.status === "needs install") {
134
- return await runInstall([], ctx);
70
+ case "needs prepare": {
71
+ const prepareStatus = await runPrepare([], ctx);
72
+ if (prepareStatus !== 0)
73
+ return prepareStatus;
74
+ return await runInstall([], ctx);
75
+ }
76
+ case "needs install":
77
+ return await runInstall([], ctx);
135
78
  }
136
- renderUnknownProbeStatus(facts, ctx);
137
- return 1;
138
79
  }
@@ -0,0 +1 @@
1
+ export {};
package/dist/lifecycle.js CHANGED
@@ -52,8 +52,7 @@ export function reportLegacyState(identityState) {
52
52
  }
53
53
  return unknownState(identityState);
54
54
  }
55
- import { hasTerminalControl } from "./adapter-result.js";
56
- import { commitMatches } from "./status.js";
55
+ export { verifyInstalledFingerprint, } from "./codex-presentation.js";
57
56
  // Ported from
58
57
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:62-70::spw_require_managed_update_control`.
59
58
  // Both refusals reached spw_die in the shell, so both carry the `error: ` prefix
@@ -85,90 +84,6 @@ function readResult(adapterResult) {
85
84
  return { kind: "object", value: value };
86
85
  }
87
86
  // Ported from
88
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:87-124::spw_verify_installed_fingerprint`.
89
- // The shell performed the inspection itself at :91; a pure function cannot, so
90
- // the caller performs it and a failure arrives here as an AdapterResult with a
91
- // non-zero status.
92
- export function verifyInstalledFingerprint(desiredCommit, installResult, inspectResult) {
93
- const inspected = readResult(inspectResult);
94
- if (inspected.kind === "call-failed") {
95
- return {
96
- ok: false,
97
- stdout: [],
98
- stderr: [
99
- "error: installed manager fingerprint inspection failed after install.",
100
- ],
101
- };
102
- }
103
- if (inspected.kind === "unusable") {
104
- // A well-formed outcome whose `result` is not an object is the port's
105
- // analogue of the shell's own split: the inspect call SUCCEEDED and only
106
- // the content is unusable. This is the branch
107
- // tests/unit/lifecycle.test.js's "an unparseable fingerprint result
108
- // names parsing, not inspection" exercises. Spec §6.2.3 items 3 and 4.
109
- return {
110
- ok: false,
111
- stdout: [],
112
- stderr: [
113
- "error: cannot parse installed manager fingerprint inspection result after install.",
114
- ],
115
- };
116
- }
117
- const raw = inspected.value.fingerprint;
118
- // The Python reader printed the empty string for a JSON null, and
119
- // `fingerprint` is null whenever no plugin version is active
120
- // (`src/adapter.ts:820::fingerprint: null`). Anything non-string and non-null
121
- // is unparseable.
122
- //
123
- // PORT-ONLY, and intentional. The shell cannot construct this trigger:
124
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/provenance.sh:62::print(value`
125
- // stringifies any non-null scalar, so a non-string fingerprint never reaches
126
- // spw_verify_installed_fingerprint.
127
- // The port CAN encounter the shape and must fail closed rather than
128
- // coerce. Pinned by a test below and recorded in
129
- // tests/migration-inventory/codex-state-units.md. Spec §6.2.3 item 3.
130
- if (raw !== null && raw !== undefined && typeof raw !== "string") {
131
- return {
132
- ok: false,
133
- stdout: [],
134
- stderr: [
135
- "error: cannot parse installed manager fingerprint inspection result after install.",
136
- ],
137
- };
138
- }
139
- const installedCommit = typeof raw === "string" ? raw : "";
140
- // :99-100 printed both lines BEFORE deciding, on every path that got this
141
- // far. Moving them into the success branch would silently drop them from the
142
- // failure output an operator reads to diagnose the mismatch.
143
- const stdout = [
144
- `desired_commit=${desiredCommit}`,
145
- `installed_commit=${installedCommit}`,
146
- ];
147
- if (installedCommit.length > 0 &&
148
- commitMatches(desiredCommit, installedCommit)) {
149
- return { ok: true, stdout: [...stdout, "manager updated"], stderr: [] };
150
- }
151
- // :106-113 — which hint key is read depends on whether a commit was
152
- // detected at all, and the hint is optional in both directions.
153
- const installed = readResult(installResult);
154
- const hints = installed.kind === "object" ? installed.value.verification_hints : null;
155
- let hint = "";
156
- if (typeof hints === "object" && hints !== null && !Array.isArray(hints)) {
157
- const key = installedCommit.length > 0 ? "mismatch" : "missing";
158
- const value = hints[key];
159
- if (typeof value === "string" && !hasTerminalControl(value))
160
- hint = value;
161
- }
162
- const stderr = [
163
- installedCommit.length > 0
164
- ? "error: installed manager fingerprint does not match the prepared plugin after install."
165
- : "error: installed manager fingerprint is not detectable after install.",
166
- ];
167
- if (hint.length > 0)
168
- stderr.push(`hint: ${hint}`);
169
- return { ok: false, stdout, stderr };
170
- }
171
- // Ported from
172
87
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:126-141::spw_verify_uninstalled_resources`,
173
88
  // with the Boolean coercion of
174
89
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:58-73::spw_adapter_result_boolean`
@@ -69,7 +69,7 @@ function setMember(entries, key, value) {
69
69
  }
70
70
  /**
71
71
  * Map strict-json's diagnostics onto the CPython wording the callers assert.
72
- * Repository policy at `AGENTS.md:90::most diagnostics are asserted as`
72
+ * Repository policy at `AGENTS.md:91::most diagnostics are asserted as`
73
73
  * freezes most of these diagnostics as complete-string contracts.
74
74
  */
75
75
  function translate(cause, source, path) {
@@ -82,7 +82,7 @@ function translate(cause, source, path) {
82
82
  return new SafetyError("manifest-overlay", `JSON nesting exceeds limit in ${path}`, { cause });
83
83
  }
84
84
  // strict-json reports `… at character N`
85
- // (`src/strict-json.ts:305::at character ${this.index}`), a zero-based offset. CPython reports
85
+ // (`src/strict-json.ts:312::at character ${this.index}`), a zero-based offset. CPython reports
86
86
  // one-based line and column. Convert so the message keeps CPython's shape.
87
87
  // This does NOT claim the two parsers fail at the same place on the same
88
88
  // malformed input — see the spec.
@@ -56,10 +56,10 @@ function parseWith(input, profile, builder) {
56
56
  }
57
57
  }
58
58
  class Parser {
59
+ index = 0;
59
60
  text;
60
61
  profile;
61
62
  builder;
62
- index = 0;
63
63
  constructor(text, profile, builder) {
64
64
  this.text = text;
65
65
  this.profile = profile;
package/dist/validator.js CHANGED
@@ -19,10 +19,10 @@ export const BOUNDED_EXECUTABLE = {
19
19
  // everything, which is the unbounded policy's contract. push() always consumes the
20
20
  // whole chunk: ceasing to consume would block the child on a full pipe.
21
21
  class Sink {
22
- limit;
23
22
  chunks = [];
24
23
  kept = 0;
25
24
  dropped = 0;
25
+ limit;
26
26
  constructor(limit) {
27
27
  this.limit = limit;
28
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superpowers-manager",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Install and update the latest stable obra/superpowers release directly from upstream, without waiting for agent marketplaces to catch up. Codex supported today.",
5
5
  "keywords": [
6
6
  "superpowers",
@@ -18,19 +18,19 @@
18
18
  ],
19
19
  "license": "MIT",
20
20
  "type": "module",
21
- "packageManager": "pnpm@11.22.0",
21
+ "packageManager": "pnpm@11.25.0",
22
22
  "scripts": {
23
- "prebuild": "node --input-type=module -e \"import{rmSync}from'node:fs';rmSync('dist',{recursive:true,force:true})\"",
24
- "build": "tsc -p tsconfig.json",
25
- "postbuild": "node tests/write-build-id.js",
26
- "prepack": "node --input-type=module -e \"import{existsSync}from'node:fs';if(!existsSync('dist/cli.js')){console.error('error: dist/cli.js is missing — run pnpm install --frozen-lockfile && pnpm run build');process.exit(1)}\"",
27
- "lint": "oxlint src/ bin/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.js tests/build-id.js tests/write-build-id.js tests/assert-matcher-gate.js",
28
- "format": "prettier --write src/ bin/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.js tests/build-id.js tests/write-build-id.js tests/assert-matcher-gate.js",
29
- "format:check": "prettier --check src/ bin/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.js tests/build-id.js tests/write-build-id.js tests/assert-matcher-gate.js",
30
- "typecheck:js": "tsc -p tests/tsconfig.json",
31
- "test": "node tests/run-node-suites.js",
32
- "check:static": "pnpm run format:check && pnpm run lint && pnpm run build && pnpm run typecheck:js",
33
- "check": "pnpm run check:static && pnpm run test"
23
+ "lint": "oxlint src/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.ts tests/assert-matcher-gate.ts",
24
+ "format": "prettier --write src/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.ts tests/assert-matcher-gate.ts",
25
+ "format:check": "prettier --check src/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.ts tests/assert-matcher-gate.ts",
26
+ "test": "node tests/run-node-suites.ts",
27
+ "test:unit": "node tests/run-node-suites.ts --group unit",
28
+ "test:integration": "node tests/run-node-suites.ts --group integration",
29
+ "test:harness:codex": "sh tests/container.sh codex-spike",
30
+ "test:acceptance": "sh tests/acceptance.sh",
31
+ "check:static": "pnpm run format:check && pnpm run lint && pnpm run typecheck",
32
+ "check": "pnpm run check:static && pnpm run test",
33
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tests/tsconfig.json --noEmit"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^24.13.3",
@@ -49,15 +49,13 @@
49
49
  "url": "https://github.com/j7an/superpowers-manager/issues"
50
50
  },
51
51
  "bin": {
52
- "superpowers-manager": "bin/superpowers-manager.js"
52
+ "superpowers-manager": "dist/cli.js"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=24"
56
56
  },
57
57
  "files": [
58
- "bin/",
59
58
  "dist/",
60
- "!dist/.build-id",
61
59
  "config/upstream-ref",
62
60
  ".agents/plugins/marketplace.json",
63
61
  "plugins/superpowers/.codex-plugin/plugin.template.json",
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env node
2
- // @ts-check
3
-
4
- import { existsSync } from "node:fs";
5
-
6
- const cliUrl = new URL("../dist/cli.js", import.meta.url);
7
-
8
- if (!existsSync(cliUrl)) {
9
- console.error(
10
- "dist/ not built — run `pnpm install --frozen-lockfile && pnpm run build`",
11
- );
12
- process.exit(1);
13
- }
14
-
15
- const { main } = await import(cliUrl.href);
16
- main();