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
package/dist/commands/install.js
CHANGED
|
@@ -4,62 +4,62 @@
|
|
|
4
4
|
// and the adapter arrives through ctx.adapter.
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { oneLine } from "../cli-arguments.js";
|
|
7
|
-
import { requireManagedUpdateControl, requireNoLegacyState, verifyInstalledFingerprint, } from "../lifecycle.js";
|
|
8
|
-
import { generatedMetadataPath, readStrictProvenanceField, } from "../provenance.js";
|
|
9
7
|
import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
|
|
10
|
-
import {
|
|
8
|
+
import { gatherProbe, replayOutcome, validInstalled, validOutput, } from "./probe.js";
|
|
11
9
|
import { runPrepare } from "./prepare.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// TypeScript's narrowing of `outcome.result`, not a live branch.
|
|
20
|
-
const outcome = result.outcome;
|
|
21
|
-
const parsed = outcome.ok
|
|
22
|
-
? outcome.result
|
|
23
|
-
: null;
|
|
24
|
-
const raw = parsed?.[key];
|
|
25
|
-
if (raw === null || raw === undefined)
|
|
26
|
-
return { kind: "ok", value: "" };
|
|
27
|
-
if (typeof raw === "string")
|
|
28
|
-
return { kind: "ok", value: raw };
|
|
29
|
-
return {
|
|
30
|
-
kind: "malformed",
|
|
31
|
-
message: `adapter returned a non-string ${key} for ${label}`,
|
|
32
|
-
};
|
|
10
|
+
import { runWithMutation } from "./mutation.js";
|
|
11
|
+
import { activationBlock } from "../harness-compatibility.js";
|
|
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`);
|
|
33
17
|
}
|
|
34
|
-
async function invoke(
|
|
18
|
+
async function invoke(call, failure, outcomes, valid = () => true) {
|
|
35
19
|
let result;
|
|
36
20
|
try {
|
|
37
|
-
result = await
|
|
21
|
+
result = await call();
|
|
22
|
+
if (!result ||
|
|
23
|
+
!Number.isInteger(result.status) ||
|
|
24
|
+
!result.outcome ||
|
|
25
|
+
typeof result.outcome.ok !== "boolean" ||
|
|
26
|
+
typeof result.outcome.operation !== "string" ||
|
|
27
|
+
!Array.isArray(result.outcome.messages) ||
|
|
28
|
+
!result.outcome.messages.every((message) => message &&
|
|
29
|
+
(message.channel === "stdout" || message.channel === "stderr") &&
|
|
30
|
+
typeof message.text === "string") ||
|
|
31
|
+
(result.outcome.ok
|
|
32
|
+
? result.outcome.error !== null || !valid(result.outcome.result)
|
|
33
|
+
: !result.outcome.error ||
|
|
34
|
+
typeof result.outcome.error.code !== "string" ||
|
|
35
|
+
typeof result.outcome.error.message !== "string" ||
|
|
36
|
+
!Array.isArray(result.outcome.error.hints) ||
|
|
37
|
+
!result.outcome.error.hints.every((hint) => typeof hint === "string"))) {
|
|
38
|
+
return { ok: false, message: failure.invalidStatus, result: null };
|
|
39
|
+
}
|
|
38
40
|
}
|
|
39
41
|
catch {
|
|
40
|
-
return {
|
|
41
|
-
ok: false,
|
|
42
|
-
message: `cannot invoke Codex adapter for ${argv.join(" ")}`,
|
|
43
|
-
result: null,
|
|
44
|
-
};
|
|
42
|
+
return { ok: false, message: failure.unexpected, result: null };
|
|
45
43
|
}
|
|
46
|
-
// Clause 1: replay every outcome, on both the success and the failure
|
|
47
|
-
// path, before any decision -- collected here and replayed by runInstall
|
|
48
|
-
// once gatherInstallStages's try/catch has resolved, for the same EPIPE
|
|
49
|
-
// reason gatherProbe carries its outcomes out rather than writing in place
|
|
50
|
-
// (src/commands/probe.ts).
|
|
51
44
|
outcomes.push(result.outcome);
|
|
52
45
|
const outcome = result.outcome;
|
|
53
46
|
if (result.status !== 0 || !outcome.ok) {
|
|
54
47
|
return {
|
|
55
48
|
ok: false,
|
|
56
|
-
message: outcome.ok
|
|
57
|
-
? `adapter reported a failure status for ${argv.join(" ")}`
|
|
58
|
-
: null,
|
|
49
|
+
message: outcome.ok ? failure.invalidStatus : null,
|
|
59
50
|
result,
|
|
60
51
|
};
|
|
61
52
|
}
|
|
62
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
result: { status: result.status, outcome },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function validReceipt(value) {
|
|
59
|
+
return (value !== null &&
|
|
60
|
+
typeof value === "object" &&
|
|
61
|
+
validOutput(value.missingVerificationOutput) &&
|
|
62
|
+
validOutput(value.mismatchVerificationOutput));
|
|
63
63
|
}
|
|
64
64
|
// withWorkspace can throw AFTER its callback has already returned a fully
|
|
65
65
|
// computed StageOutcome: a post-success cleanup failure discards that return
|
|
@@ -84,7 +84,7 @@ class GatherFailure extends Error {
|
|
|
84
84
|
// created via spw_make_workspace + spw_install_workspace_trap. Performs no
|
|
85
85
|
// writes of its own -- same EPIPE-avoidance shape as gatherProbe and
|
|
86
86
|
// src/commands/uninstall.ts's gatherUninstall.
|
|
87
|
-
async function gatherInstallStages(ctx,
|
|
87
|
+
async function gatherInstallStages(ctx, selection, artifact) {
|
|
88
88
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:38::tmp_parent=` -- ${TMPDIR:-/tmp}. Matches
|
|
89
89
|
// src/commands/uninstall.ts's gatherUninstall.
|
|
90
90
|
const parent = ctx.env.TMPDIR ?? tmpdir();
|
|
@@ -98,36 +98,61 @@ async function gatherInstallStages(ctx, desiredCommit) {
|
|
|
98
98
|
outcomes,
|
|
99
99
|
message,
|
|
100
100
|
});
|
|
101
|
+
const adapterContext = { root: ctx.root, env };
|
|
101
102
|
// Stage 1: inspect ownership, re-checked even though gatherProbe just
|
|
102
103
|
// reported it. Mutation authority requires CURRENT, VALIDATED
|
|
103
104
|
// evidence -- a probe's answer is neither by the time this runs.
|
|
104
|
-
const
|
|
105
|
+
const ownershipFailure = ctx.adapter.presentation.callFailure("install-ownership", adapterContext);
|
|
106
|
+
const ownership = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure, outcomes);
|
|
105
107
|
if (!ownership.ok)
|
|
106
108
|
return failed(ownership.message);
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
const ownershipDecision = ownership.result.outcome.result.installEligibility;
|
|
110
|
+
if (ownershipDecision.kind === "blocked") {
|
|
111
|
+
return {
|
|
112
|
+
kind: "blocked",
|
|
113
|
+
outcomes,
|
|
114
|
+
output: ownershipDecision.output,
|
|
115
|
+
};
|
|
113
116
|
}
|
|
114
|
-
if (legacy.kind === "unknown")
|
|
115
|
-
return failed(legacy.message);
|
|
116
117
|
// Stage 2: inspect update-control, re-checked for the same reason.
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const control = readStringField(updateControl.result, "update_control", "inspect --view update-control");
|
|
121
|
-
if (control.kind === "malformed")
|
|
118
|
+
const controlFailure = ctx.adapter.presentation.callFailure("install-control", adapterContext);
|
|
119
|
+
const control = await invoke(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure, outcomes);
|
|
120
|
+
if (!control.ok)
|
|
122
121
|
return failed(control.message);
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
return
|
|
122
|
+
const mutationDecision = control.result.outcome.result.mutationEligibility;
|
|
123
|
+
if (mutationDecision.kind === "blocked") {
|
|
124
|
+
return {
|
|
125
|
+
kind: "blocked",
|
|
126
|
+
outcomes,
|
|
127
|
+
output: mutationDecision.output,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
126
130
|
// Stage 3: the mutation itself. Nothing above may have issued this --
|
|
127
131
|
// that is the whole point of stages 1 and 2 running first.
|
|
128
|
-
const
|
|
132
|
+
const activationFailure = activationBlock(artifact.compatibility, ctx.options.allowExperimental);
|
|
133
|
+
if (activationFailure !== null)
|
|
134
|
+
return failed(activationFailure);
|
|
135
|
+
const installFailure = ctx.adapter.presentation.callFailure("install", adapterContext);
|
|
136
|
+
const install = await invoke(() => ctx.adapter.install(artifact, adapterContext), installFailure, outcomes, validReceipt);
|
|
129
137
|
if (!install.ok)
|
|
130
138
|
return failed(install.message);
|
|
139
|
+
let transaction;
|
|
140
|
+
try {
|
|
141
|
+
const candidate = install.result.outcome.result.transaction;
|
|
142
|
+
if (candidate !== undefined) {
|
|
143
|
+
if (candidate === null || typeof candidate !== "object")
|
|
144
|
+
throw new Error("invalid transaction");
|
|
145
|
+
const finalize = candidate.finalize.bind(candidate);
|
|
146
|
+
const rollback = candidate.rollback.bind(candidate);
|
|
147
|
+
if (typeof finalize !== "function" ||
|
|
148
|
+
typeof rollback !== "function")
|
|
149
|
+
throw new Error("invalid transaction");
|
|
150
|
+
transaction = { finalize, rollback };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return failed("invalid installation transaction receipt; preserve recovery material for manual resolution");
|
|
155
|
+
}
|
|
131
156
|
// Stage 4: inspect fingerprint, to verify the mutation actually took.
|
|
132
157
|
//
|
|
133
158
|
// Deliberately NOT short-circuited on `!inspected.ok`, unlike stages
|
|
@@ -137,32 +162,78 @@ async function gatherInstallStages(ctx, desiredCommit) {
|
|
|
137
162
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:91-94::spw_inspect_fingerprint`,
|
|
138
163
|
// is what turns a failed inspection into "error: installed manager
|
|
139
164
|
// fingerprint inspection failed after install."
|
|
140
|
-
//
|
|
141
|
-
// (`src/
|
|
142
|
-
// exists for
|
|
165
|
+
// renderInstallVerification's failed-inspection arm
|
|
166
|
+
// (`src/codex-presentation.ts:332::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
|
|
167
|
+
// exists for this result-bearing path; the lifecycle compatibility
|
|
168
|
+
// export delegates through the same arm. Returning
|
|
143
169
|
// failed() instead reported the adapter's own generic diagnostic and
|
|
144
170
|
// dropped the post-install verification claim -- a mutation had
|
|
145
171
|
// already been issued at stage 3,
|
|
146
172
|
// so "the install could not be verified" is the contract, not "an
|
|
147
|
-
// adapter call failed". A ctx.adapter THROW
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
// adapter call failed". A ctx.adapter THROW has no result to render.
|
|
174
|
+
// A pending transaction still rolls back before reporting that failure.
|
|
175
|
+
const inspectionFailure = ctx.adapter.presentation.callFailure("post-install", adapterContext);
|
|
176
|
+
const inspected = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes, validInstalled);
|
|
177
|
+
const inspection = inspected.result;
|
|
178
|
+
const verified = inspection !== null &&
|
|
179
|
+
inspection.status === 0 &&
|
|
180
|
+
inspection.outcome.ok &&
|
|
181
|
+
inspection.outcome.result.kind === "current";
|
|
182
|
+
if (transaction !== undefined) {
|
|
183
|
+
const operation = verified ? "finalize" : "rollback";
|
|
184
|
+
const settlement = await invoke(() => (verified ? transaction.finalize() : transaction.rollback()), {
|
|
185
|
+
unexpected: `installation ${operation} did not complete; preserve recovery material for manual resolution`,
|
|
186
|
+
invalidStatus: `installation ${operation} returned an invalid result; preserve recovery material for manual resolution`,
|
|
187
|
+
}, outcomes, (value) => value === null);
|
|
188
|
+
if (!verified) {
|
|
189
|
+
const restored = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes, validInstalled);
|
|
190
|
+
const restoration = restored.ok
|
|
191
|
+
? `installed state after rollback: ${restored.result.outcome.result.kind}; identity=${restored.result.outcome.result.observedIdentity}`
|
|
192
|
+
: "installed state after rollback could not be inspected";
|
|
193
|
+
const message = !settlement.ok
|
|
194
|
+
? settlement.message
|
|
195
|
+
: !inspected.ok
|
|
196
|
+
? inspected.message
|
|
197
|
+
: "installation could not be verified";
|
|
198
|
+
return {
|
|
199
|
+
kind: "verified",
|
|
200
|
+
outcomes,
|
|
201
|
+
status: 1,
|
|
202
|
+
stdout: [],
|
|
203
|
+
stderr: [
|
|
204
|
+
...(message === null ? [] : [`error: ${message}`]),
|
|
205
|
+
restoration,
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (!settlement.ok) {
|
|
210
|
+
const actual = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes, validInstalled);
|
|
211
|
+
return {
|
|
212
|
+
kind: "verified",
|
|
213
|
+
outcomes,
|
|
214
|
+
status: 1,
|
|
215
|
+
stdout: [],
|
|
216
|
+
stderr: [
|
|
217
|
+
"error: installation was verified, but transaction cleanup did not complete; preserve recovery material for manual resolution",
|
|
218
|
+
actual.ok
|
|
219
|
+
? `installed state after failed finalization: ${actual.result.outcome.result.kind}; identity=${actual.result.outcome.result.observedIdentity}`
|
|
220
|
+
: "installed state after failed finalization could not be inspected",
|
|
221
|
+
...(settlement.message === null
|
|
222
|
+
? []
|
|
223
|
+
: [`error: ${settlement.message}`]),
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
153
227
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
inspectResult = inspected.result;
|
|
158
|
-
}
|
|
159
|
-
const verdict = verifyInstalledFingerprint(desiredCommit, install.result, inspectResult);
|
|
228
|
+
if (inspection === null)
|
|
229
|
+
return failed(inspected.ok ? null : inspected.message);
|
|
230
|
+
const output = ctx.adapter.presentation.renderInstallVerification(selection.desiredCommit, install.result, inspection);
|
|
160
231
|
return {
|
|
161
232
|
kind: "verified",
|
|
162
233
|
outcomes,
|
|
163
|
-
status:
|
|
164
|
-
stdout:
|
|
165
|
-
stderr:
|
|
234
|
+
status: verified ? 0 : 1,
|
|
235
|
+
stdout: output.stdout,
|
|
236
|
+
stderr: output.stderr,
|
|
166
237
|
};
|
|
167
238
|
}, {
|
|
168
239
|
// Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure,
|
|
@@ -182,44 +253,15 @@ async function gatherInstallStages(ctx, desiredCommit) {
|
|
|
182
253
|
throw new GatherFailure(cause, outcomes);
|
|
183
254
|
}
|
|
184
255
|
}
|
|
185
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:29-32::printf` kept its shell `case ... *)` wildcard even though the
|
|
186
|
-
// three inputs the shell's own probe could ever emit were bounded by the same
|
|
187
|
-
// three-way status.sh logic this port's statusForCommits (src/status.ts) now
|
|
188
|
-
// owns exactly. statusForCommits can only ever return "needs prepare",
|
|
189
|
-
// "needs install" or "current" -- so this branch is NOT reachable through
|
|
190
|
-
// runInstall's own call to gatherProbe today. It stays anyway, because
|
|
191
|
-
// ProbeFacts.status is typed `string`, not that three-literal union, and a
|
|
192
|
-
// future caller that builds facts by hand (as this module's own unit test
|
|
193
|
-
// does, directly) must still see it fail closed rather than silently
|
|
194
|
-
// proceed.
|
|
195
|
-
//
|
|
196
|
-
// Exported specifically so a direct test can reach it. That is ONE STEP
|
|
197
|
-
// FURTHER than
|
|
198
|
-
// `src/commands/probe.ts:367-372::This guard is NOT the production path.`'s own
|
|
199
|
-
// precedent: that comment licenses RETAINING an unreachable branch inside an
|
|
200
|
-
// already-public function (runProbe was public before that comment existed),
|
|
201
|
-
// not EXPORTING a new one. This module does the latter, deliberately, because
|
|
202
|
-
// runInstall
|
|
203
|
-
// itself has no way to construct the unreachable input. Not part of the
|
|
204
|
-
// interface Task 5 or Task 8 consume.
|
|
205
|
-
//
|
|
206
|
-
// A test against this export proves the two writes below are correct for a
|
|
207
|
-
// given `facts`. It does NOT prove that runInstall's own `else if` branch
|
|
208
|
-
// actually reaches and calls this function, or that runInstall returns 1
|
|
209
|
-
// afterward -- those are properties of the call site, not of this function,
|
|
210
|
-
// and need their own coverage there.
|
|
211
|
-
export function renderUnknownProbeStatus(facts, ctx) {
|
|
212
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:30-31::printf` -- the porcelain reaches the terminal ONLY here.
|
|
213
|
-
// Every other path swallows it, matching `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:18::porcelain`'s
|
|
214
|
-
// probe_output=$(...) capture.
|
|
215
|
-
ctx.stdout.write(formatPorcelain(facts));
|
|
216
|
-
ctx.stderr.write(`error: unknown probe status: ${facts.status}\n`);
|
|
217
|
-
}
|
|
218
256
|
export async function runInstall(argv, ctx) {
|
|
257
|
+
if (ctx.adapter.presentation.installNotice !== "")
|
|
258
|
+
ctx.stdout.write(`${ctx.adapter.presentation.installNotice}\n`);
|
|
259
|
+
return await runWithMutation("install", ctx, async (scoped) => performInstall(argv, scoped));
|
|
260
|
+
}
|
|
261
|
+
async function performInstall(argv, ctx) {
|
|
219
262
|
// scripts/install never reads "$@", so extra arguments are silently
|
|
220
263
|
// ignored -- the same asymmetry runPrepare and runUninstall document.
|
|
221
264
|
void argv;
|
|
222
|
-
ctx.stdout.write(NOTE);
|
|
223
265
|
let probe;
|
|
224
266
|
try {
|
|
225
267
|
probe = await gatherProbe(ctx);
|
|
@@ -231,20 +273,20 @@ export async function runInstall(argv, ctx) {
|
|
|
231
273
|
// runs only after this try/catch has resolved.
|
|
232
274
|
//
|
|
233
275
|
// This is a SECOND consumer of gatherProbe's throw channel --
|
|
234
|
-
// `src/commands/probe.ts:
|
|
276
|
+
// `src/commands/probe.ts:469-527::THREE exceptions, all inherited and none a regression:`'s
|
|
235
277
|
// runProbe catch is the first. Because both consumers wrap the identical
|
|
236
278
|
// function, its long comment there enumerates exactly what can reach THIS
|
|
237
|
-
// stream too, including the three foreign-text exceptions at :
|
|
238
|
-
// 1. :
|
|
279
|
+
// stream too, including the three foreign-text exceptions at :251-296:
|
|
280
|
+
// 1. :251-262 -- resolveRef splices git's own combined stdout+stderr
|
|
239
281
|
// into its text. Reached on probe's DEFAULT path, which that comment
|
|
240
282
|
// defines as every invocation NOT resolving a saved pin: a 40-hex
|
|
241
283
|
// ref returns a "raw-commit" resolution at
|
|
242
284
|
// `src/upstream.ts:162-164::return { kind: "raw-commit"`
|
|
243
285
|
// before any git call, so it reaches no splice at all.
|
|
244
|
-
// 2. :
|
|
286
|
+
// 2. :263-280 -- src/selection-store.ts's read path interpolates the
|
|
245
287
|
// caught error's own message, so Node errno prose can appear.
|
|
246
288
|
// AGENTS.md grandfathers that module's wording.
|
|
247
|
-
// 3. :
|
|
289
|
+
// 3. :281-296 -- a SPAWN-level git failure, a different channel from
|
|
248
290
|
// exception 1's exit-status one: on the non-ENOENT arm of
|
|
249
291
|
// `src/git.ts:47-52::if (typeof failure.code === "string") {`, runGit
|
|
250
292
|
// rejects with "cannot run git: " followed by the Node spawn error's
|
|
@@ -263,72 +305,45 @@ export async function runInstall(argv, ctx) {
|
|
|
263
305
|
return 1;
|
|
264
306
|
}
|
|
265
307
|
const facts = probe.facts;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// its own "malformed" arm for a non-string value, and an empty string
|
|
269
|
-
// there is legitimately absent (the JSON-null convention), not an error.
|
|
270
|
-
// This check exists because a probe that reports no identity state at all
|
|
271
|
-
// is a different failure than one that reports an unrecognised one:
|
|
272
|
-
// requireNoLegacyState("") would otherwise reach its "unknown" arm and
|
|
273
|
-
// print "unknown adapter identity state: " (empty-suffixed), which names a
|
|
274
|
-
// symptom rather than the actual cause. Do NOT add a second copy of this
|
|
275
|
-
// check at :198 -- it guards a different value with a different failure
|
|
276
|
-
// mode.
|
|
277
|
-
if (facts.identityState.length === 0) {
|
|
278
|
-
ctx.stderr.write("error: probe did not report adapter identity state\n");
|
|
279
|
-
return 1;
|
|
280
|
-
}
|
|
281
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:20-21::report`, run BEFORE the workspace is ever created and before
|
|
282
|
-
// any further adapter call -- a legacy identity is fatal on sight, not
|
|
283
|
-
// something worth spending a mutation attempt on.
|
|
284
|
-
const legacy = requireNoLegacyState(facts.identityState);
|
|
285
|
-
if (legacy.kind === "blocked") {
|
|
286
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:50-53::'Legacy superpowers-wrapper Codex state is`
|
|
287
|
-
// is a single printf writing three bare lines to stderr, no `error: `
|
|
288
|
-
// prefix; :54 is the `return 1` that follows it, reached without spw_die.
|
|
289
|
-
for (const line of legacy.lines)
|
|
290
|
-
ctx.stderr.write(`${line}\n`);
|
|
308
|
+
if (facts.ownership.installEligibility.kind === "blocked") {
|
|
309
|
+
writeOutput(facts.ownership.installEligibility.output, ctx);
|
|
291
310
|
return 1;
|
|
292
311
|
}
|
|
293
|
-
if (
|
|
294
|
-
|
|
312
|
+
if (facts.resourceState === "recovery-required" &&
|
|
313
|
+
facts.control.probeEligibility.kind === "blocked") {
|
|
314
|
+
writeOutput(facts.control.probeEligibility.output, ctx);
|
|
295
315
|
return 1;
|
|
296
316
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
renderUnknownProbeStatus(facts, ctx);
|
|
309
|
-
return 1;
|
|
317
|
+
switch (facts.status) {
|
|
318
|
+
case "needs prepare": {
|
|
319
|
+
const prepareStatus = await runPrepare([], ctx);
|
|
320
|
+
if (prepareStatus !== 0)
|
|
321
|
+
return prepareStatus;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case "needs install":
|
|
325
|
+
break;
|
|
326
|
+
case "current":
|
|
327
|
+
break;
|
|
310
328
|
}
|
|
311
|
-
|
|
312
|
-
// generatedCommitOrEmpty gatherProbe already used for facts.generatedCommit:
|
|
313
|
-
// a throw, an absent key, or a non-string value are all treated as absent
|
|
314
|
-
// here, matching spw_metadata_commit_or_empty's `|| true` plus the
|
|
315
|
-
// following `[ -n ]` check.
|
|
316
|
-
let desiredCommit = "";
|
|
329
|
+
let prepared;
|
|
317
330
|
try {
|
|
318
|
-
|
|
319
|
-
if (typeof value === "string")
|
|
320
|
-
desiredCommit = value;
|
|
331
|
+
prepared = await ctx.adapter.readPrepared({ root: ctx.root, env: ctx.env });
|
|
321
332
|
}
|
|
322
333
|
catch {
|
|
323
|
-
|
|
334
|
+
ctx.stderr.write("error: cannot read prepared harness state\n");
|
|
335
|
+
return 1;
|
|
324
336
|
}
|
|
325
|
-
|
|
326
|
-
|
|
337
|
+
replayOutcome(prepared.outcome, ctx);
|
|
338
|
+
if (!prepared.outcome.ok)
|
|
339
|
+
return 1;
|
|
340
|
+
if (prepared.status !== 0) {
|
|
341
|
+
ctx.stderr.write("error: adapter reported a failure status for prepared harness inspection\n");
|
|
327
342
|
return 1;
|
|
328
343
|
}
|
|
329
344
|
let stage;
|
|
330
345
|
try {
|
|
331
|
-
stage = await gatherInstallStages(ctx,
|
|
346
|
+
stage = await gatherInstallStages(ctx, facts.selection, prepared.outcome.result);
|
|
332
347
|
}
|
|
333
348
|
catch (cause) {
|
|
334
349
|
const outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
|
|
@@ -343,8 +358,7 @@ export async function runInstall(argv, ctx) {
|
|
|
343
358
|
replayOutcome(each, ctx);
|
|
344
359
|
let status;
|
|
345
360
|
if (outcome.kind === "blocked") {
|
|
346
|
-
|
|
347
|
-
ctx.stderr.write(`${line}\n`);
|
|
361
|
+
writeOutput(outcome.output, ctx);
|
|
348
362
|
status = 1;
|
|
349
363
|
}
|
|
350
364
|
else if (outcome.kind === "failed") {
|
|
@@ -355,8 +369,8 @@ export async function runInstall(argv, ctx) {
|
|
|
355
369
|
}
|
|
356
370
|
else {
|
|
357
371
|
// :99-100 printed both lines BEFORE deciding, on every path that got this
|
|
358
|
-
// far;
|
|
359
|
-
// are written in order regardless of
|
|
372
|
+
// far; the verified outcome already carries both renderer arrays, so they
|
|
373
|
+
// are written in order regardless of its status.
|
|
360
374
|
for (const line of outcome.stdout)
|
|
361
375
|
ctx.stdout.write(`${line}\n`);
|
|
362
376
|
for (const line of outcome.stderr)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { computeEffectiveSelection } from "../effective-selection.js";
|
|
2
|
+
import { upstreamCacheRoot } from "../upstream-workspace.js";
|
|
3
|
+
import { SafetyError } from "../safety-error.js";
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
import { oneLine } from "../cli-arguments.js";
|
|
6
|
+
export async function withMutation(command, ctx, action) {
|
|
7
|
+
// Selection validation precedes every adapter call and lock creation. A
|
|
8
|
+
// nested mutation receives the already resolved value and cannot observe a
|
|
9
|
+
// saved-selection change midway through one invocation.
|
|
10
|
+
const selection = command === "uninstall"
|
|
11
|
+
? ctx.selection
|
|
12
|
+
: (ctx.selection ?? (await computeEffectiveSelection(ctx.root, ctx.env)));
|
|
13
|
+
const scoped = selection === undefined ? ctx : { ...ctx, selection };
|
|
14
|
+
const adapterContext = { root: ctx.root, env: ctx.env };
|
|
15
|
+
let location;
|
|
16
|
+
try {
|
|
17
|
+
location = ctx.adapter.preparationLocation(adapterContext);
|
|
18
|
+
}
|
|
19
|
+
catch (cause) {
|
|
20
|
+
throw new SafetyError("mutation", "cannot determine preparation location", {
|
|
21
|
+
cause,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (!isAbsolute(location.destinationRoot))
|
|
25
|
+
throw new SafetyError("mutation", "adapter returned a non-absolute preparation destination");
|
|
26
|
+
if (!location.stagingLeaf ||
|
|
27
|
+
location.stagingLeaf === "." ||
|
|
28
|
+
location.stagingLeaf === ".." ||
|
|
29
|
+
location.stagingLeaf.includes("/") ||
|
|
30
|
+
location.stagingLeaf.includes("\\"))
|
|
31
|
+
throw new SafetyError("mutation", "adapter returned an invalid preparation staging leaf");
|
|
32
|
+
let mutationRoots;
|
|
33
|
+
try {
|
|
34
|
+
mutationRoots = await ctx.adapter.mutationRoots(adapterContext);
|
|
35
|
+
if (!Array.isArray(mutationRoots) ||
|
|
36
|
+
mutationRoots.some((root) => typeof root !== "string" || !isAbsolute(root)))
|
|
37
|
+
throw new Error("invalid roots");
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
throw new SafetyError("mutation", "cannot determine harness mutation resources", { cause });
|
|
41
|
+
}
|
|
42
|
+
const resources = [location.destinationRoot, ...mutationRoots];
|
|
43
|
+
if (command === "prepare") {
|
|
44
|
+
resources.push(upstreamCacheRoot(ctx.root, ctx.env, process.cwd()));
|
|
45
|
+
}
|
|
46
|
+
// The coordinator sorts each acquisition and refuses conflicts immediately.
|
|
47
|
+
// A nested prepare may add a cache lock that sorts before an outer target;
|
|
48
|
+
// it cannot deadlock because no acquisition waits, while already-held roots
|
|
49
|
+
// are reentrant for this invocation.
|
|
50
|
+
return await ctx.coordination.withResources(resources, async () => action(scoped));
|
|
51
|
+
}
|
|
52
|
+
export async function runWithMutation(command, ctx, action) {
|
|
53
|
+
let actionThrew = false;
|
|
54
|
+
try {
|
|
55
|
+
return await withMutation(command, ctx, async (scoped) => {
|
|
56
|
+
try {
|
|
57
|
+
return await action(scoped);
|
|
58
|
+
}
|
|
59
|
+
catch (cause) {
|
|
60
|
+
actionThrew = true;
|
|
61
|
+
throw cause;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (cause) {
|
|
66
|
+
if (actionThrew)
|
|
67
|
+
throw cause;
|
|
68
|
+
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
}
|