superpowers-manager 0.3.0 → 0.5.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 +63 -325
- package/dist/adapter.js +91 -46
- package/dist/artifact-tree.js +110 -0
- package/dist/atomic.js +103 -7
- package/dist/cli-arguments.js +37 -0
- package/dist/cli.js +97 -40
- package/dist/codex-compatibility.js +126 -0
- package/dist/codex-conflicts.js +99 -0
- package/dist/codex-harness.js +321 -0
- package/dist/codex-json.js +7 -0
- package/dist/codex-prepare.js +255 -0
- package/dist/codex-presentation.js +282 -0
- package/dist/commands/install.js +187 -173
- package/dist/commands/mutation.js +71 -0
- package/dist/commands/prepare.js +87 -188
- package/dist/commands/probe.js +191 -176
- package/dist/commands/uninstall.js +52 -139
- package/dist/commands/update.js +33 -88
- package/dist/generated-plugin.js +3 -3
- package/dist/harness-compatibility.js +13 -0
- package/dist/harness.js +1 -0
- package/dist/lifecycle.js +1 -86
- package/dist/manifest-overlay.js +2 -2
- package/dist/pi-compatibility.js +73 -0
- package/dist/pi-harness.js +63 -0
- package/dist/pi-install.js +495 -0
- package/dist/pi-native.js +81 -0
- package/dist/pi-package.js +153 -0
- package/dist/pi-paths.js +30 -0
- package/dist/pi-prepare.js +111 -0
- package/dist/pi-presentation.js +67 -0
- package/dist/pi-settings.js +171 -0
- package/dist/pi-shared-skills.js +413 -0
- package/dist/pi-state.js +311 -0
- package/dist/resource-lock.js +265 -0
- package/dist/safe-path.js +21 -5
- package/dist/strict-json.js +1 -1
- package/dist/upstream-workspace.js +8 -0
- package/dist/validator.js +3 -2
- package/package.json +19 -18
- package/bin/superpowers-manager.js +0 -16
|
@@ -3,61 +3,29 @@
|
|
|
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";
|
|
7
|
+
import { runWithMutation } from "./mutation.js";
|
|
8
8
|
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) {
|
|
9
|
+
async function invoke(call, failure, outcomes) {
|
|
35
10
|
let result;
|
|
36
11
|
try {
|
|
37
|
-
result = await
|
|
12
|
+
result = await call();
|
|
38
13
|
}
|
|
39
14
|
catch {
|
|
40
|
-
return {
|
|
41
|
-
ok: false,
|
|
42
|
-
message: `cannot invoke Codex adapter for ${argv.join(" ")}`,
|
|
43
|
-
};
|
|
15
|
+
return { ok: false, message: failure.unexpected };
|
|
44
16
|
}
|
|
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
17
|
outcomes.push(result.outcome);
|
|
51
18
|
const outcome = result.outcome;
|
|
52
19
|
if (result.status !== 0 || !outcome.ok) {
|
|
53
20
|
return {
|
|
54
21
|
ok: false,
|
|
55
|
-
message: outcome.ok
|
|
56
|
-
? `adapter reported a failure status for ${argv.join(" ")}`
|
|
57
|
-
: null,
|
|
22
|
+
message: outcome.ok ? failure.invalidStatus : null,
|
|
58
23
|
};
|
|
59
24
|
}
|
|
60
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
result: { status: result.status, outcome },
|
|
28
|
+
};
|
|
61
29
|
}
|
|
62
30
|
// withWorkspace throws for mkdtemp failure before the callback ever runs
|
|
63
31
|
// ("cannot create workspace",
|
|
@@ -85,7 +53,7 @@ class GatherFailure extends Error {
|
|
|
85
53
|
}
|
|
86
54
|
// Every step that can throw or fail closed, returning the outcome as data and
|
|
87
55
|
// performing NO writes. Same shape as gatherProbe
|
|
88
|
-
// (`src/commands/probe.ts
|
|
56
|
+
// (`src/commands/probe.ts::readonly facts: ProbeSnapshot<R>;`) and for the same
|
|
89
57
|
// reason: a write inside this try could raise EPIPE, be caught here, and be
|
|
90
58
|
// relabelled as a domain failure.
|
|
91
59
|
async function gatherUninstall(ctx) {
|
|
@@ -108,106 +76,41 @@ async function gatherUninstall(ctx) {
|
|
|
108
76
|
status: 1,
|
|
109
77
|
outcomes,
|
|
110
78
|
message,
|
|
79
|
+
output: null,
|
|
111
80
|
});
|
|
81
|
+
const adapterContext = { root: ctx.root, env };
|
|
112
82
|
// 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);
|
|
83
|
+
const beforeFailure = ctx.adapter.presentation.callFailure("remove-ownership", adapterContext);
|
|
84
|
+
const before = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), beforeFailure, outcomes);
|
|
85
|
+
if (!before.ok)
|
|
86
|
+
return failed(before.message);
|
|
87
|
+
// Stage 2: remove, passing the private input through untouched.
|
|
88
|
+
const removalInput = before.result.outcome.result.removalInput;
|
|
89
|
+
const removeFailure = ctx.adapter.presentation.callFailure("remove", adapterContext, removalInput);
|
|
90
|
+
const removed = await invoke(() => ctx.adapter.remove(removalInput, adapterContext), removeFailure, outcomes);
|
|
91
|
+
if (!removed.ok)
|
|
92
|
+
return failed(removed.message);
|
|
145
93
|
// Stage 3: inspect ownership AGAIN. This overwrites the first
|
|
146
94
|
// inspection (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:29-30::spw_verify_uninstalled_resources`), so everything below reads the
|
|
147
95
|
// 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");
|
|
190
|
-
}
|
|
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);
|
|
96
|
+
const afterFailure = ctx.adapter.presentation.callFailure("post-remove", adapterContext);
|
|
97
|
+
const after = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), afterFailure, outcomes);
|
|
98
|
+
if (!after.ok)
|
|
99
|
+
return failed(after.message);
|
|
100
|
+
const ownership = after.result.outcome.result;
|
|
101
|
+
if (ownership.removalVerification.kind === "blocked") {
|
|
102
|
+
return {
|
|
103
|
+
status: 1,
|
|
104
|
+
outcomes,
|
|
105
|
+
message: null,
|
|
106
|
+
output: ownership.removalVerification.output,
|
|
107
|
+
};
|
|
201
108
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 };
|
|
109
|
+
return {
|
|
110
|
+
status: 0,
|
|
111
|
+
outcomes,
|
|
112
|
+
output: ctx.adapter.presentation.renderRemovalCompletion(ownership, removalInput),
|
|
113
|
+
};
|
|
211
114
|
}, {
|
|
212
115
|
// Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure,
|
|
213
116
|
// so the UninstallOutcome the callback already computed still comes
|
|
@@ -232,6 +135,9 @@ async function gatherUninstall(ctx) {
|
|
|
232
135
|
}
|
|
233
136
|
}
|
|
234
137
|
export async function runUninstall(argv, ctx) {
|
|
138
|
+
return await runWithMutation("uninstall", ctx, async (scoped) => performUninstall(argv, scoped));
|
|
139
|
+
}
|
|
140
|
+
async function performUninstall(argv, ctx) {
|
|
235
141
|
// scripts/uninstall never reads "$@", so extra arguments are silently
|
|
236
142
|
// ignored -- the same asymmetry runPrepare documents at its own
|
|
237
143
|
// `void argv;`.
|
|
@@ -257,7 +163,7 @@ export async function runUninstall(argv, ctx) {
|
|
|
257
163
|
// blindly.
|
|
258
164
|
//
|
|
259
165
|
// A cause outside ctx.adapter's AdapterFailure guard
|
|
260
|
-
// (`src/adapter.ts:
|
|
166
|
+
// (`src/adapter.ts:973-999::async function runCodexOperation(`) does NOT
|
|
261
167
|
// reach here: invoke() catches it inside gatherUninstall and converts it
|
|
262
168
|
// to a hand-written message carried as UninstallOutcome data, exactly as
|
|
263
169
|
// src/commands/probe.ts's inspect() does for the same cause.
|
|
@@ -285,12 +191,19 @@ export async function runUninstall(argv, ctx) {
|
|
|
285
191
|
if (outcome.message !== null) {
|
|
286
192
|
ctx.stderr.write(`error: ${outcome.message}\n`);
|
|
287
193
|
}
|
|
194
|
+
if (outcome.output !== null) {
|
|
195
|
+
for (const line of outcome.output.stdout)
|
|
196
|
+
ctx.stdout.write(`${line}\n`);
|
|
197
|
+
for (const line of outcome.output.stderr)
|
|
198
|
+
ctx.stderr.write(`${line}\n`);
|
|
199
|
+
}
|
|
288
200
|
status = 1;
|
|
289
201
|
}
|
|
290
202
|
else {
|
|
291
|
-
for (const line of outcome.
|
|
203
|
+
for (const line of outcome.output.stdout)
|
|
292
204
|
ctx.stdout.write(`${line}\n`);
|
|
293
|
-
|
|
205
|
+
for (const line of outcome.output.stderr)
|
|
206
|
+
ctx.stderr.write(`${line}\n`);
|
|
294
207
|
status = 0;
|
|
295
208
|
}
|
|
296
209
|
if (cleanupWarning !== null) {
|
package/dist/commands/update.js
CHANGED
|
@@ -6,38 +6,20 @@
|
|
|
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
|
-
|
|
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
|
+
import { runWithMutation } from "./mutation.js";
|
|
13
|
+
function writeOutput(output, ctx) {
|
|
14
|
+
for (const line of output.stdout)
|
|
15
|
+
ctx.stdout.write(`${line}\n`);
|
|
16
|
+
for (const line of output.stderr)
|
|
17
|
+
ctx.stderr.write(`${line}\n`);
|
|
39
18
|
}
|
|
40
19
|
export async function runUpdate(argv, ctx) {
|
|
20
|
+
return await runWithMutation("update", ctx, async (scoped) => performUpdate(argv, scoped));
|
|
21
|
+
}
|
|
22
|
+
async function performUpdate(argv, ctx) {
|
|
41
23
|
// scripts/update never reads "$@", so extra arguments are silently
|
|
42
24
|
// ignored -- the same asymmetry runPrepare and runInstall document.
|
|
43
25
|
void argv;
|
|
@@ -52,7 +34,7 @@ export async function runUpdate(argv, ctx) {
|
|
|
52
34
|
// install's NOTE line.
|
|
53
35
|
//
|
|
54
36
|
// This is a THIRD consumer of gatherProbe's throw channel --
|
|
55
|
-
// `src/commands/probe.ts:
|
|
37
|
+
// `src/commands/probe.ts:469-527::THREE exceptions, all inherited and none a regression:`'s
|
|
56
38
|
// runProbe catch is the first and src/commands/install.ts's runInstall catch
|
|
57
39
|
// is the second. Because all three wrap the identical function, runProbe's
|
|
58
40
|
// long comment there enumerates exactly what can reach this stream too; not
|
|
@@ -70,69 +52,32 @@ export async function runUpdate(argv, ctx) {
|
|
|
70
52
|
return 1;
|
|
71
53
|
}
|
|
72
54
|
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");
|
|
81
|
-
return 1;
|
|
82
|
-
}
|
|
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`);
|
|
91
|
-
return 1;
|
|
92
|
-
}
|
|
93
|
-
if (legacy.kind === "unknown") {
|
|
94
|
-
ctx.stderr.write(`error: ${legacy.message}\n`);
|
|
55
|
+
if (facts.ownership.installEligibility.kind === "blocked") {
|
|
56
|
+
writeOutput(facts.ownership.installEligibility.output, ctx);
|
|
95
57
|
return 1;
|
|
96
58
|
}
|
|
97
|
-
|
|
98
|
-
|
|
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");
|
|
59
|
+
if (facts.control.probeEligibility.kind === "blocked") {
|
|
60
|
+
writeOutput(facts.control.probeEligibility.output, ctx);
|
|
103
61
|
return 1;
|
|
104
62
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
ctx.
|
|
114
|
-
return
|
|
63
|
+
switch (facts.status) {
|
|
64
|
+
case "current": {
|
|
65
|
+
if (facts.control.mutationEligibility.kind === "blocked") {
|
|
66
|
+
writeOutput(facts.control.mutationEligibility.output, ctx);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const rendered = ctx.adapter.presentation.renderProbe(facts);
|
|
70
|
+
ctx.stdout.write(rendered.porcelain);
|
|
71
|
+
ctx.stdout.write(`${ctx.adapter.presentation.currentNotice}\n`);
|
|
72
|
+
return 0;
|
|
115
73
|
}
|
|
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);
|
|
74
|
+
case "needs prepare": {
|
|
75
|
+
const prepareStatus = await runPrepare([], ctx);
|
|
76
|
+
if (prepareStatus !== 0)
|
|
77
|
+
return prepareStatus;
|
|
78
|
+
return await runInstall([], ctx);
|
|
79
|
+
}
|
|
80
|
+
case "needs install":
|
|
81
|
+
return await runInstall([], ctx);
|
|
135
82
|
}
|
|
136
|
-
renderUnknownProbeStatus(facts, ctx);
|
|
137
|
-
return 1;
|
|
138
83
|
}
|
package/dist/generated-plugin.js
CHANGED
|
@@ -14,7 +14,7 @@ export const DEFAULT_FS_DEPS = {
|
|
|
14
14
|
// The Python's own SEMVER_RE (:15-22). Deliberately not SEMVER_BASE_SOURCE
|
|
15
15
|
// from src/domain/refs.ts, which omits the `+build` component; widening the
|
|
16
16
|
// shared constant would change TAG_RE and is out of scope.
|
|
17
|
-
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-]+)*)?$/;
|
|
17
|
+
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-]+)*)?$/;
|
|
18
18
|
const MANIFEST_JSON_PROFILE = {
|
|
19
19
|
duplicateKeys: "last-wins",
|
|
20
20
|
nonStandardConstants: "reject",
|
|
@@ -444,7 +444,7 @@ const REQUIRED_FILES = [
|
|
|
444
444
|
"CODE_OF_CONDUCT.md",
|
|
445
445
|
];
|
|
446
446
|
/** Python `sorted()` orders by code point; JavaScript's default sort does not. */
|
|
447
|
-
function compareByCodePoint(left, right) {
|
|
447
|
+
export function compareByCodePoint(left, right) {
|
|
448
448
|
// `Array.from` splits by code point, exactly as spreading would; oxlint's
|
|
449
449
|
// `no-misused-spread` rejects the spread form, and grapheme segmentation is
|
|
450
450
|
// the wrong unit here — Python compares code points.
|
|
@@ -459,7 +459,7 @@ function compareByCodePoint(left, right) {
|
|
|
459
459
|
}
|
|
460
460
|
return leftPoints.length - rightPoints.length;
|
|
461
461
|
}
|
|
462
|
-
async function validateSkillFrontmatter(skillMd, skillName, errors, deps) {
|
|
462
|
+
export async function validateSkillFrontmatter(skillMd, skillName, errors, deps) {
|
|
463
463
|
let contents;
|
|
464
464
|
try {
|
|
465
465
|
contents = STRICT_DECODER.decode(await deps.readFile(skillMd));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function activationBlock(value, allowExperimental) {
|
|
2
|
+
switch (value.kind) {
|
|
3
|
+
case "supported":
|
|
4
|
+
return null;
|
|
5
|
+
case "experimental":
|
|
6
|
+
return allowExperimental
|
|
7
|
+
? null
|
|
8
|
+
: "experimental integration requires --allow-experimental";
|
|
9
|
+
case "unknown":
|
|
10
|
+
case "unsupported":
|
|
11
|
+
return value.reason;
|
|
12
|
+
}
|
|
13
|
+
}
|
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:92::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.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readArtifactFile, readArtifactObject, validateNativeSkill, } from "./artifact-tree.js";
|
|
5
|
+
import { SEMVER_RE } from "./generated-plugin.js";
|
|
6
|
+
import { validateSource } from "./selection.js";
|
|
7
|
+
const OFFICIAL_SOURCES = new Set([
|
|
8
|
+
"https://github.com/obra/superpowers",
|
|
9
|
+
"https://github.com/obra/superpowers.git",
|
|
10
|
+
"ssh://git@github.com/obra/superpowers.git",
|
|
11
|
+
"git@github.com:obra/superpowers.git",
|
|
12
|
+
]);
|
|
13
|
+
export function samePiSource(left, right) {
|
|
14
|
+
validateSource(left);
|
|
15
|
+
validateSource(right);
|
|
16
|
+
return (left === right ||
|
|
17
|
+
(OFFICIAL_SOURCES.has(left) && OFFICIAL_SOURCES.has(right)));
|
|
18
|
+
}
|
|
19
|
+
export async function assessPiCompatibility(root, selection) {
|
|
20
|
+
try {
|
|
21
|
+
validateSource(selection.effectiveSource);
|
|
22
|
+
const official = OFFICIAL_SOURCES.has(selection.effectiveSource);
|
|
23
|
+
const pkg = await readArtifactObject(root, join(root, "package.json"));
|
|
24
|
+
if (pkg.name !== "superpowers" ||
|
|
25
|
+
pkg.type !== "module" ||
|
|
26
|
+
typeof pkg.version !== "string" ||
|
|
27
|
+
!SEMVER_RE.test(pkg.version))
|
|
28
|
+
throw new Error("package metadata");
|
|
29
|
+
const pi = pkg.pi;
|
|
30
|
+
if (!pi ||
|
|
31
|
+
typeof pi !== "object" ||
|
|
32
|
+
Array.isArray(pi) ||
|
|
33
|
+
Object.keys(pi).length !== 2 ||
|
|
34
|
+
JSON.stringify(pi.extensions) !== '["./.pi/extensions/superpowers.ts"]' ||
|
|
35
|
+
JSON.stringify(pi.skills) !== '["./skills"]')
|
|
36
|
+
throw new Error("resource declaration");
|
|
37
|
+
for (const key of [
|
|
38
|
+
"dependencies",
|
|
39
|
+
"optionalDependencies",
|
|
40
|
+
"peerDependencies",
|
|
41
|
+
"bundleDependencies",
|
|
42
|
+
"bundledDependencies",
|
|
43
|
+
]) {
|
|
44
|
+
const value = pkg[key];
|
|
45
|
+
if (value !== undefined &&
|
|
46
|
+
(value === null ||
|
|
47
|
+
typeof value !== "object" ||
|
|
48
|
+
Object.keys(value).length !== 0))
|
|
49
|
+
throw new Error("runtime dependencies");
|
|
50
|
+
}
|
|
51
|
+
await validateNativeSkill(root);
|
|
52
|
+
const bootstrap = join(root, ".pi/extensions/superpowers.ts");
|
|
53
|
+
const bytes = await readArtifactFile(root, bootstrap, 4283);
|
|
54
|
+
if ((await lstat(bootstrap)).mode & 0o111 ||
|
|
55
|
+
bytes.length !== 4283 ||
|
|
56
|
+
createHash("sha256").update(bytes).digest("hex") !==
|
|
57
|
+
"39c27000c047f8a1399a76e032e4cd239d7bdb0be168a121f46a2ac719deaae0")
|
|
58
|
+
throw new Error("bootstrap implementation");
|
|
59
|
+
return {
|
|
60
|
+
kind: official ? "supported" : "experimental",
|
|
61
|
+
generation: "pi-native-bootstrap-v1",
|
|
62
|
+
reason: official
|
|
63
|
+
? "qualified upstream-native Pi bootstrap"
|
|
64
|
+
: "qualified Pi mechanics from a custom source",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return {
|
|
69
|
+
kind: "unsupported",
|
|
70
|
+
reason: `Pi package does not match the qualified native bootstrap profile: ${root}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|