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.
- package/README.md +240 -31
- package/dist/adapter.js +85 -44
- package/dist/cli.js +51 -31
- package/dist/codex-harness.js +271 -0
- package/dist/codex-prepare.js +199 -0
- package/dist/codex-presentation.js +259 -0
- package/dist/commands/install.js +88 -173
- package/dist/commands/prepare.js +74 -181
- package/dist/commands/probe.js +41 -176
- package/dist/commands/uninstall.js +48 -139
- package/dist/commands/update.js +29 -88
- package/dist/harness.js +1 -0
- package/dist/lifecycle.js +1 -86
- package/dist/manifest-overlay.js +2 -2
- package/dist/strict-json.js +1 -1
- package/dist/validator.js +1 -1
- package/package.json +14 -16
- package/bin/superpowers-manager.js +0 -16
|
@@ -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
|
|
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
|
|
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 {
|
|
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:
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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:
|
|
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.
|
|
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) {
|
package/dist/commands/update.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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:
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
}
|
package/dist/harness.js
ADDED
|
@@ -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
|
-
|
|
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`
|
package/dist/manifest-overlay.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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.
|
package/dist/strict-json.js
CHANGED
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
|
+
"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.
|
|
21
|
+
"packageManager": "pnpm@11.25.0",
|
|
22
22
|
"scripts": {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"check
|
|
33
|
-
"
|
|
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": "
|
|
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();
|