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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { normalizeCodexInstall, normalizeCodexInstalled, } from "./codex-harness.js";
|
|
2
|
+
import { displaySource } from "./selection.js";
|
|
3
|
+
// One ordered table drives both formats, preserving the public human and
|
|
4
|
+
// porcelain field order exposed through the harness presentation.
|
|
5
|
+
export function fields(f) {
|
|
6
|
+
return [
|
|
7
|
+
{ key: "requested_ref", label: "requested ref", value: f.requestedRef },
|
|
8
|
+
{ key: "resolved_ref", label: "resolved ref", value: f.resolvedRef },
|
|
9
|
+
{ key: "desired_commit", label: "desired commit", value: f.desiredCommit },
|
|
10
|
+
{
|
|
11
|
+
key: "generated_commit",
|
|
12
|
+
label: "generated plugin commit",
|
|
13
|
+
value: f.generatedCommit,
|
|
14
|
+
absent: "not present",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "installed_commit",
|
|
18
|
+
label: "installed manager commit or fingerprint",
|
|
19
|
+
value: f.installedCommit,
|
|
20
|
+
absent: "not detected",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: "identity_state",
|
|
24
|
+
label: "Codex identity state",
|
|
25
|
+
value: f.identityState,
|
|
26
|
+
},
|
|
27
|
+
{ key: "status", label: "status", value: f.status },
|
|
28
|
+
{
|
|
29
|
+
key: "selection_origin",
|
|
30
|
+
label: "selection origin",
|
|
31
|
+
value: f.selectionOrigin,
|
|
32
|
+
},
|
|
33
|
+
{ key: "selection_mode", label: "selection mode", value: f.selectionMode },
|
|
34
|
+
{
|
|
35
|
+
key: "upstream_source_origin",
|
|
36
|
+
label: "upstream source origin",
|
|
37
|
+
value: f.upstreamSourceOrigin,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "effective_source",
|
|
41
|
+
label: "effective source",
|
|
42
|
+
value: f.effectiveSource,
|
|
43
|
+
},
|
|
44
|
+
{ key: "saved_mode", label: "saved mode", value: f.savedMode },
|
|
45
|
+
{ key: "saved_source", label: "saved source", value: f.savedSource },
|
|
46
|
+
{
|
|
47
|
+
key: "saved_requested_ref",
|
|
48
|
+
label: "saved requested ref",
|
|
49
|
+
value: f.savedRequestedRef,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "saved_resolved_ref",
|
|
53
|
+
label: "saved resolved ref",
|
|
54
|
+
value: f.savedResolvedRef,
|
|
55
|
+
},
|
|
56
|
+
{ key: "saved_commit", label: "saved commit", value: f.savedCommit },
|
|
57
|
+
{ key: "update_control", label: "update control", value: f.updateControl },
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
const NO_FACTS = {
|
|
61
|
+
requestedRef: "",
|
|
62
|
+
resolvedRef: "",
|
|
63
|
+
desiredCommit: "",
|
|
64
|
+
generatedCommit: "",
|
|
65
|
+
installedCommit: "",
|
|
66
|
+
identityState: "",
|
|
67
|
+
status: "",
|
|
68
|
+
selectionOrigin: "",
|
|
69
|
+
selectionMode: "",
|
|
70
|
+
upstreamSourceOrigin: "",
|
|
71
|
+
effectiveSource: "",
|
|
72
|
+
savedMode: "",
|
|
73
|
+
savedSource: "",
|
|
74
|
+
savedRequestedRef: "",
|
|
75
|
+
savedResolvedRef: "",
|
|
76
|
+
savedCommit: "",
|
|
77
|
+
updateControl: "",
|
|
78
|
+
};
|
|
79
|
+
export const PROBE_PORCELAIN_KEYS = fields(NO_FACTS).map((field) => field.key);
|
|
80
|
+
export function formatPorcelain(f) {
|
|
81
|
+
return fields(f)
|
|
82
|
+
.map((field) => `${field.key}=${field.value}\n`)
|
|
83
|
+
.join("");
|
|
84
|
+
}
|
|
85
|
+
export function formatHuman(f) {
|
|
86
|
+
let text = fields(f)
|
|
87
|
+
.map((field) => {
|
|
88
|
+
const shown = field.value.length === 0 && field.absent !== undefined
|
|
89
|
+
? field.absent
|
|
90
|
+
: field.value;
|
|
91
|
+
return `${field.label}: ${shown}\n`;
|
|
92
|
+
})
|
|
93
|
+
.join("");
|
|
94
|
+
if (f.selectionOrigin !== f.upstreamSourceOrigin) {
|
|
95
|
+
text +=
|
|
96
|
+
"warning: effective ref and source have mixed origins " +
|
|
97
|
+
`(ref: ${f.selectionOrigin}, source: ${f.upstreamSourceOrigin})\n`;
|
|
98
|
+
}
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
function verificationOutput(kind, hint) {
|
|
102
|
+
const stderr = [
|
|
103
|
+
kind === "mismatch"
|
|
104
|
+
? "error: installed manager fingerprint does not match the prepared plugin after install."
|
|
105
|
+
: "error: installed manager fingerprint is not detectable after install.",
|
|
106
|
+
];
|
|
107
|
+
if (hint.length > 0)
|
|
108
|
+
stderr.push(`hint: ${hint}`);
|
|
109
|
+
return { stdout: [], stderr };
|
|
110
|
+
}
|
|
111
|
+
export function codexInstallReceipt(missingHint, mismatchHint) {
|
|
112
|
+
return {
|
|
113
|
+
missingVerificationOutput: verificationOutput("missing", missingHint),
|
|
114
|
+
mismatchVerificationOutput: verificationOutput("mismatch", mismatchHint),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Compatibility export target for src/lifecycle.ts. Delegate to the production
|
|
118
|
+
// normalizers and renderer so the retained verdict cannot drift from commands.
|
|
119
|
+
export function verifyInstalledFingerprint(desiredCommit, installResult, inspectResult) {
|
|
120
|
+
const receipt = normalizeCodexInstall(installResult);
|
|
121
|
+
const inspection = normalizeCodexInstalled(inspectResult, desiredCommit);
|
|
122
|
+
const output = codexPresentation.renderInstallVerification(desiredCommit, receipt, inspection);
|
|
123
|
+
const ok = inspection.status === 0 &&
|
|
124
|
+
inspection.outcome.ok &&
|
|
125
|
+
inspection.outcome.result.kind === "current";
|
|
126
|
+
return { ok, stdout: output.stdout, stderr: output.stderr };
|
|
127
|
+
}
|
|
128
|
+
function probeFacts(facts) {
|
|
129
|
+
const selection = facts.selection;
|
|
130
|
+
const saved = selection.saved;
|
|
131
|
+
return {
|
|
132
|
+
requestedRef: selection.requestedRef,
|
|
133
|
+
resolvedRef: selection.resolvedRef,
|
|
134
|
+
desiredCommit: selection.desiredCommit,
|
|
135
|
+
generatedCommit: facts.prepared.observedIdentity,
|
|
136
|
+
installedCommit: facts.installed.observedIdentity,
|
|
137
|
+
identityState: facts.ownership.presentationValue,
|
|
138
|
+
status: facts.status,
|
|
139
|
+
selectionOrigin: selection.selectionOrigin,
|
|
140
|
+
selectionMode: selection.selectionMode,
|
|
141
|
+
upstreamSourceOrigin: selection.upstreamSourceOrigin,
|
|
142
|
+
effectiveSource: displaySource(selection.effectiveSource),
|
|
143
|
+
savedMode: saved.saved_mode,
|
|
144
|
+
savedSource: saved.saved_source.length > 0 ? displaySource(saved.saved_source) : "",
|
|
145
|
+
savedRequestedRef: saved.saved_requested_ref,
|
|
146
|
+
savedResolvedRef: saved.saved_resolved_ref,
|
|
147
|
+
savedCommit: saved.saved_commit,
|
|
148
|
+
updateControl: facts.control.presentationValue,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function invocationFailure(argv) {
|
|
152
|
+
return {
|
|
153
|
+
unexpected: `cannot invoke Codex adapter for ${argv}`,
|
|
154
|
+
invalidStatus: `adapter reported a failure status for ${argv}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function callFailure(site, ctx, removalInput) {
|
|
158
|
+
switch (site) {
|
|
159
|
+
case "prepare":
|
|
160
|
+
return {
|
|
161
|
+
unexpected: "cannot build the generated plugin candidate",
|
|
162
|
+
invalidStatus: "adapter reported failure without an error outcome",
|
|
163
|
+
};
|
|
164
|
+
case "probe-installed":
|
|
165
|
+
return {
|
|
166
|
+
unexpected: "cannot inspect Codex adapter state for view fingerprint",
|
|
167
|
+
invalidStatus: "adapter reported a failure status for inspect --view fingerprint",
|
|
168
|
+
};
|
|
169
|
+
case "probe-ownership":
|
|
170
|
+
return {
|
|
171
|
+
unexpected: "cannot inspect Codex adapter state for view ownership",
|
|
172
|
+
invalidStatus: "adapter reported a failure status for inspect --view ownership",
|
|
173
|
+
};
|
|
174
|
+
case "probe-control":
|
|
175
|
+
return {
|
|
176
|
+
unexpected: "cannot inspect Codex adapter state for view update-control",
|
|
177
|
+
invalidStatus: "adapter reported a failure status for inspect --view update-control",
|
|
178
|
+
};
|
|
179
|
+
case "install-ownership":
|
|
180
|
+
case "remove-ownership":
|
|
181
|
+
case "post-remove":
|
|
182
|
+
return invocationFailure("inspect --view ownership");
|
|
183
|
+
case "install-control":
|
|
184
|
+
return invocationFailure("inspect --view update-control");
|
|
185
|
+
case "install":
|
|
186
|
+
return invocationFailure(`install --package-root ${ctx.root}`);
|
|
187
|
+
case "post-install":
|
|
188
|
+
return {
|
|
189
|
+
unexpected: "cannot invoke Codex adapter for inspect --view fingerprint",
|
|
190
|
+
invalidStatus: "installed manager fingerprint inspection failed after install.",
|
|
191
|
+
};
|
|
192
|
+
case "remove": {
|
|
193
|
+
const input = removalInput ?? {
|
|
194
|
+
pluginPresent: false,
|
|
195
|
+
marketplacePresent: false,
|
|
196
|
+
};
|
|
197
|
+
return invocationFailure(`uninstall --plugin-present ${String(input.pluginPresent)}` +
|
|
198
|
+
` --marketplace-present ${String(input.marketplacePresent)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export const codexPresentation = {
|
|
203
|
+
installNotice: "Note: remove or disable conflicting Superpowers providers yourself before" +
|
|
204
|
+
" relying on manager skills.",
|
|
205
|
+
currentNotice: "manager is current",
|
|
206
|
+
renderProbe(snapshot) {
|
|
207
|
+
const facts = probeFacts(snapshot);
|
|
208
|
+
return { human: formatHuman(facts), porcelain: formatPorcelain(facts) };
|
|
209
|
+
},
|
|
210
|
+
renderInstallVerification(desiredCommit, receipt, inspection) {
|
|
211
|
+
if (inspection.status !== 0 || !inspection.outcome.ok) {
|
|
212
|
+
const malformed = !inspection.outcome.ok &&
|
|
213
|
+
inspection.outcome.error.code === "malformed-result";
|
|
214
|
+
return {
|
|
215
|
+
stdout: [],
|
|
216
|
+
stderr: [
|
|
217
|
+
malformed
|
|
218
|
+
? "error: cannot parse installed manager fingerprint inspection result after install."
|
|
219
|
+
: "error: installed manager fingerprint inspection failed after install.",
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const observed = inspection.outcome.result.observedIdentity;
|
|
224
|
+
const stdout = [
|
|
225
|
+
`desired_commit=${desiredCommit}`,
|
|
226
|
+
`installed_commit=${observed}`,
|
|
227
|
+
];
|
|
228
|
+
if (inspection.outcome.result.kind === "current") {
|
|
229
|
+
return { stdout: [...stdout, "manager updated"], stderr: [] };
|
|
230
|
+
}
|
|
231
|
+
if (receipt.status !== 0 || !receipt.outcome.ok) {
|
|
232
|
+
return {
|
|
233
|
+
stdout,
|
|
234
|
+
stderr: [
|
|
235
|
+
inspection.outcome.result.kind === "absent"
|
|
236
|
+
? "error: installed manager fingerprint is not detectable after install."
|
|
237
|
+
: "error: installed manager fingerprint does not match the prepared plugin after install.",
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const output = inspection.outcome.result.kind === "absent"
|
|
242
|
+
? receipt.outcome.result.missingVerificationOutput
|
|
243
|
+
: receipt.outcome.result.mismatchVerificationOutput;
|
|
244
|
+
return { stdout, stderr: output.stderr };
|
|
245
|
+
},
|
|
246
|
+
renderRemovalCompletion(ownership) {
|
|
247
|
+
return {
|
|
248
|
+
stdout: [
|
|
249
|
+
...ownership.postRemovalOutput.stdout,
|
|
250
|
+
"uninstall complete",
|
|
251
|
+
"note: local generated artifacts under plugins/superpowers/ and " +
|
|
252
|
+
".cache/upstream/ were left in place; remove them manually or " +
|
|
253
|
+
"regenerate with npx superpowers-manager prepare.",
|
|
254
|
+
],
|
|
255
|
+
stderr: ownership.postRemovalOutput.stderr,
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
callFailure,
|
|
259
|
+
};
|
package/dist/commands/install.js
CHANGED
|
@@ -4,62 +4,36 @@
|
|
|
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 } from "./probe.js";
|
|
11
9
|
import { runPrepare } from "./prepare.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Callers only reach this after invoke() has already proven
|
|
18
|
-
// `result.status === 0 && result.outcome.ok`; the guard below is for
|
|
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
|
+
function writeOutput(output, ctx) {
|
|
11
|
+
for (const line of output.stdout)
|
|
12
|
+
ctx.stdout.write(`${line}\n`);
|
|
13
|
+
for (const line of output.stderr)
|
|
14
|
+
ctx.stderr.write(`${line}\n`);
|
|
33
15
|
}
|
|
34
|
-
async function invoke(
|
|
16
|
+
async function invoke(call, failure, outcomes) {
|
|
35
17
|
let result;
|
|
36
18
|
try {
|
|
37
|
-
result = await
|
|
19
|
+
result = await call();
|
|
38
20
|
}
|
|
39
21
|
catch {
|
|
40
|
-
return {
|
|
41
|
-
ok: false,
|
|
42
|
-
message: `cannot invoke Codex adapter for ${argv.join(" ")}`,
|
|
43
|
-
result: null,
|
|
44
|
-
};
|
|
22
|
+
return { ok: false, message: failure.unexpected, result: null };
|
|
45
23
|
}
|
|
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
24
|
outcomes.push(result.outcome);
|
|
52
25
|
const outcome = result.outcome;
|
|
53
26
|
if (result.status !== 0 || !outcome.ok) {
|
|
54
27
|
return {
|
|
55
28
|
ok: false,
|
|
56
|
-
message: outcome.ok
|
|
57
|
-
? `adapter reported a failure status for ${argv.join(" ")}`
|
|
58
|
-
: null,
|
|
29
|
+
message: outcome.ok ? failure.invalidStatus : null,
|
|
59
30
|
result,
|
|
60
31
|
};
|
|
61
32
|
}
|
|
62
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
result: { status: result.status, outcome },
|
|
36
|
+
};
|
|
63
37
|
}
|
|
64
38
|
// withWorkspace can throw AFTER its callback has already returned a fully
|
|
65
39
|
// computed StageOutcome: a post-success cleanup failure discards that return
|
|
@@ -84,7 +58,7 @@ class GatherFailure extends Error {
|
|
|
84
58
|
// created via spw_make_workspace + spw_install_workspace_trap. Performs no
|
|
85
59
|
// writes of its own -- same EPIPE-avoidance shape as gatherProbe and
|
|
86
60
|
// src/commands/uninstall.ts's gatherUninstall.
|
|
87
|
-
async function gatherInstallStages(ctx,
|
|
61
|
+
async function gatherInstallStages(ctx, selection, artifact) {
|
|
88
62
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:38::tmp_parent=` -- ${TMPDIR:-/tmp}. Matches
|
|
89
63
|
// src/commands/uninstall.ts's gatherUninstall.
|
|
90
64
|
const parent = ctx.env.TMPDIR ?? tmpdir();
|
|
@@ -98,34 +72,39 @@ async function gatherInstallStages(ctx, desiredCommit) {
|
|
|
98
72
|
outcomes,
|
|
99
73
|
message,
|
|
100
74
|
});
|
|
75
|
+
const adapterContext = { root: ctx.root, env };
|
|
101
76
|
// Stage 1: inspect ownership, re-checked even though gatherProbe just
|
|
102
77
|
// reported it. Mutation authority requires CURRENT, VALIDATED
|
|
103
78
|
// evidence -- a probe's answer is neither by the time this runs.
|
|
104
|
-
const
|
|
79
|
+
const ownershipFailure = ctx.adapter.presentation.callFailure("install-ownership", adapterContext);
|
|
80
|
+
const ownership = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure, outcomes);
|
|
105
81
|
if (!ownership.ok)
|
|
106
82
|
return failed(ownership.message);
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
83
|
+
const ownershipDecision = ownership.result.outcome.result.installEligibility;
|
|
84
|
+
if (ownershipDecision.kind === "blocked") {
|
|
85
|
+
return {
|
|
86
|
+
kind: "blocked",
|
|
87
|
+
outcomes,
|
|
88
|
+
output: ownershipDecision.output,
|
|
89
|
+
};
|
|
113
90
|
}
|
|
114
|
-
if (legacy.kind === "unknown")
|
|
115
|
-
return failed(legacy.message);
|
|
116
91
|
// 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")
|
|
92
|
+
const controlFailure = ctx.adapter.presentation.callFailure("install-control", adapterContext);
|
|
93
|
+
const control = await invoke(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure, outcomes);
|
|
94
|
+
if (!control.ok)
|
|
122
95
|
return failed(control.message);
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
return
|
|
96
|
+
const mutationDecision = control.result.outcome.result.mutationEligibility;
|
|
97
|
+
if (mutationDecision.kind === "blocked") {
|
|
98
|
+
return {
|
|
99
|
+
kind: "blocked",
|
|
100
|
+
outcomes,
|
|
101
|
+
output: mutationDecision.output,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
126
104
|
// Stage 3: the mutation itself. Nothing above may have issued this --
|
|
127
105
|
// that is the whole point of stages 1 and 2 running first.
|
|
128
|
-
const
|
|
106
|
+
const installFailure = ctx.adapter.presentation.callFailure("install", adapterContext);
|
|
107
|
+
const install = await invoke(() => ctx.adapter.install(artifact, adapterContext), installFailure, outcomes);
|
|
129
108
|
if (!install.ok)
|
|
130
109
|
return failed(install.message);
|
|
131
110
|
// Stage 4: inspect fingerprint, to verify the mutation actually took.
|
|
@@ -137,32 +116,34 @@ async function gatherInstallStages(ctx, desiredCommit) {
|
|
|
137
116
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:91-94::spw_inspect_fingerprint`,
|
|
138
117
|
// is what turns a failed inspection into "error: installed manager
|
|
139
118
|
// fingerprint inspection failed after install."
|
|
140
|
-
//
|
|
141
|
-
// (`src/
|
|
142
|
-
// exists for
|
|
119
|
+
// renderInstallVerification's failed-inspection arm
|
|
120
|
+
// (`src/codex-presentation.ts:306::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
|
|
121
|
+
// exists for this result-bearing path; the lifecycle compatibility
|
|
122
|
+
// export delegates through the same arm. Returning
|
|
143
123
|
// failed() instead reported the adapter's own generic diagnostic and
|
|
144
124
|
// dropped the post-install verification claim -- a mutation had
|
|
145
125
|
// already been issued at stage 3,
|
|
146
126
|
// so "the install could not be verified" is the contract, not "an
|
|
147
127
|
// adapter call failed". A ctx.adapter THROW is the one case with no
|
|
148
128
|
// result to verify against, and keeps the short-circuit.
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
if (inspected.ok) {
|
|
152
|
-
|
|
129
|
+
const inspectionFailure = ctx.adapter.presentation.callFailure("post-install", adapterContext);
|
|
130
|
+
const inspected = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
|
|
131
|
+
if (!inspected.ok && inspected.result === null) {
|
|
132
|
+
return failed(inspected.message);
|
|
153
133
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
134
|
+
const inspection = inspected.result;
|
|
135
|
+
if (inspection === null)
|
|
136
|
+
return failed(null);
|
|
137
|
+
const output = ctx.adapter.presentation.renderInstallVerification(selection.desiredCommit, install.result, inspection);
|
|
138
|
+
const verified = inspection.status === 0 &&
|
|
139
|
+
inspection.outcome.ok &&
|
|
140
|
+
inspection.outcome.result.kind === "current";
|
|
160
141
|
return {
|
|
161
142
|
kind: "verified",
|
|
162
143
|
outcomes,
|
|
163
|
-
status:
|
|
164
|
-
stdout:
|
|
165
|
-
stderr:
|
|
144
|
+
status: verified ? 0 : 1,
|
|
145
|
+
stdout: output.stdout,
|
|
146
|
+
stderr: output.stderr,
|
|
166
147
|
};
|
|
167
148
|
}, {
|
|
168
149
|
// Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure,
|
|
@@ -182,44 +163,11 @@ async function gatherInstallStages(ctx, desiredCommit) {
|
|
|
182
163
|
throw new GatherFailure(cause, outcomes);
|
|
183
164
|
}
|
|
184
165
|
}
|
|
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
166
|
export async function runInstall(argv, ctx) {
|
|
219
167
|
// scripts/install never reads "$@", so extra arguments are silently
|
|
220
168
|
// ignored -- the same asymmetry runPrepare and runUninstall document.
|
|
221
169
|
void argv;
|
|
222
|
-
ctx.stdout.write(
|
|
170
|
+
ctx.stdout.write(`${ctx.adapter.presentation.installNotice}\n`);
|
|
223
171
|
let probe;
|
|
224
172
|
try {
|
|
225
173
|
probe = await gatherProbe(ctx);
|
|
@@ -231,20 +179,20 @@ export async function runInstall(argv, ctx) {
|
|
|
231
179
|
// runs only after this try/catch has resolved.
|
|
232
180
|
//
|
|
233
181
|
// This is a SECOND consumer of gatherProbe's throw channel --
|
|
234
|
-
// `src/commands/probe.ts:
|
|
182
|
+
// `src/commands/probe.ts:251-309::THREE exceptions, all inherited and none a regression:`'s
|
|
235
183
|
// runProbe catch is the first. Because both consumers wrap the identical
|
|
236
184
|
// function, its long comment there enumerates exactly what can reach THIS
|
|
237
|
-
// stream too, including the three foreign-text exceptions at :
|
|
238
|
-
// 1. :
|
|
185
|
+
// stream too, including the three foreign-text exceptions at :251-296:
|
|
186
|
+
// 1. :251-262 -- resolveRef splices git's own combined stdout+stderr
|
|
239
187
|
// into its text. Reached on probe's DEFAULT path, which that comment
|
|
240
188
|
// defines as every invocation NOT resolving a saved pin: a 40-hex
|
|
241
189
|
// ref returns a "raw-commit" resolution at
|
|
242
190
|
// `src/upstream.ts:162-164::return { kind: "raw-commit"`
|
|
243
191
|
// before any git call, so it reaches no splice at all.
|
|
244
|
-
// 2. :
|
|
192
|
+
// 2. :263-280 -- src/selection-store.ts's read path interpolates the
|
|
245
193
|
// caught error's own message, so Node errno prose can appear.
|
|
246
194
|
// AGENTS.md grandfathers that module's wording.
|
|
247
|
-
// 3. :
|
|
195
|
+
// 3. :281-296 -- a SPAWN-level git failure, a different channel from
|
|
248
196
|
// exception 1's exit-status one: on the non-ENOENT arm of
|
|
249
197
|
// `src/git.ts:47-52::if (typeof failure.code === "string") {`, runGit
|
|
250
198
|
// rejects with "cannot run git: " followed by the Node spawn error's
|
|
@@ -263,72 +211,40 @@ export async function runInstall(argv, ctx) {
|
|
|
263
211
|
return 1;
|
|
264
212
|
}
|
|
265
213
|
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");
|
|
214
|
+
if (facts.ownership.installEligibility.kind === "blocked") {
|
|
215
|
+
writeOutput(facts.ownership.installEligibility.output, ctx);
|
|
279
216
|
return 1;
|
|
280
217
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
if (legacy.kind === "unknown") {
|
|
294
|
-
ctx.stderr.write(`error: ${legacy.message}\n`);
|
|
295
|
-
return 1;
|
|
296
|
-
}
|
|
297
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:22-33::status=`.
|
|
298
|
-
if (facts.status === "needs prepare") {
|
|
299
|
-
// Called as a FUNCTION: a failure propagates as a status, never through
|
|
300
|
-
// `set -eu`. runPrepare has already replayed its own outcomes and
|
|
301
|
-
// written its own diagnostics by the time it returns, so nothing further
|
|
302
|
-
// is written here on that path.
|
|
303
|
-
const prepareStatus = await runPrepare([], ctx);
|
|
304
|
-
if (prepareStatus !== 0)
|
|
305
|
-
return prepareStatus;
|
|
306
|
-
}
|
|
307
|
-
else if (facts.status !== "needs install" && facts.status !== "current") {
|
|
308
|
-
renderUnknownProbeStatus(facts, ctx);
|
|
309
|
-
return 1;
|
|
218
|
+
switch (facts.status) {
|
|
219
|
+
case "needs prepare": {
|
|
220
|
+
const prepareStatus = await runPrepare([], ctx);
|
|
221
|
+
if (prepareStatus !== 0)
|
|
222
|
+
return prepareStatus;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case "needs install":
|
|
226
|
+
break;
|
|
227
|
+
case "current":
|
|
228
|
+
break;
|
|
310
229
|
}
|
|
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 = "";
|
|
230
|
+
let prepared;
|
|
317
231
|
try {
|
|
318
|
-
|
|
319
|
-
if (typeof value === "string")
|
|
320
|
-
desiredCommit = value;
|
|
232
|
+
prepared = await ctx.adapter.readPrepared({ root: ctx.root, env: ctx.env });
|
|
321
233
|
}
|
|
322
234
|
catch {
|
|
323
|
-
|
|
235
|
+
ctx.stderr.write("error: cannot read prepared harness state\n");
|
|
236
|
+
return 1;
|
|
324
237
|
}
|
|
325
|
-
|
|
326
|
-
|
|
238
|
+
replayOutcome(prepared.outcome, ctx);
|
|
239
|
+
if (!prepared.outcome.ok)
|
|
240
|
+
return 1;
|
|
241
|
+
if (prepared.status !== 0) {
|
|
242
|
+
ctx.stderr.write("error: adapter reported a failure status for prepared harness inspection\n");
|
|
327
243
|
return 1;
|
|
328
244
|
}
|
|
329
245
|
let stage;
|
|
330
246
|
try {
|
|
331
|
-
stage = await gatherInstallStages(ctx,
|
|
247
|
+
stage = await gatherInstallStages(ctx, facts.selection, prepared.outcome.result);
|
|
332
248
|
}
|
|
333
249
|
catch (cause) {
|
|
334
250
|
const outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
|
|
@@ -343,8 +259,7 @@ export async function runInstall(argv, ctx) {
|
|
|
343
259
|
replayOutcome(each, ctx);
|
|
344
260
|
let status;
|
|
345
261
|
if (outcome.kind === "blocked") {
|
|
346
|
-
|
|
347
|
-
ctx.stderr.write(`${line}\n`);
|
|
262
|
+
writeOutput(outcome.output, ctx);
|
|
348
263
|
status = 1;
|
|
349
264
|
}
|
|
350
265
|
else if (outcome.kind === "failed") {
|
|
@@ -355,8 +270,8 @@ export async function runInstall(argv, ctx) {
|
|
|
355
270
|
}
|
|
356
271
|
else {
|
|
357
272
|
// :99-100 printed both lines BEFORE deciding, on every path that got this
|
|
358
|
-
// far;
|
|
359
|
-
// are written in order regardless of
|
|
273
|
+
// far; the verified outcome already carries both renderer arrays, so they
|
|
274
|
+
// are written in order regardless of its status.
|
|
360
275
|
for (const line of outcome.stdout)
|
|
361
276
|
ctx.stdout.write(`${line}\n`);
|
|
362
277
|
for (const line of outcome.stderr)
|