superpowers-manager 0.2.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 +340 -66
- package/dist/adapter-result.js +204 -0
- package/dist/adapter.js +690 -0
- package/dist/atomic.js +142 -0
- package/dist/cli-arguments.js +32 -0
- package/dist/cli.js +313 -0
- package/dist/codex-harness.js +271 -0
- package/dist/codex-json.js +89 -0
- package/dist/codex-prepare.js +199 -0
- package/dist/codex-presentation.js +259 -0
- package/dist/codex-state.js +78 -0
- package/dist/commands/context.js +1 -0
- package/dist/commands/install.js +291 -0
- package/dist/commands/pin.js +79 -0
- package/dist/commands/prepare.js +420 -0
- package/dist/commands/probe.js +257 -0
- package/dist/commands/track-latest.js +44 -0
- package/dist/commands/uninstall.js +217 -0
- package/dist/commands/unpin.js +95 -0
- package/dist/commands/update.js +79 -0
- package/dist/domain/fingerprint.js +4 -0
- package/dist/domain/refs.js +37 -0
- package/dist/effective-selection.js +123 -0
- package/dist/generated-plugin.js +748 -0
- package/dist/git.js +38 -0
- package/dist/harness.js +1 -0
- package/dist/hooks.js +290 -0
- package/dist/lifecycle.js +135 -0
- package/dist/manifest-overlay.js +125 -0
- package/dist/provenance.js +111 -0
- package/dist/python-json-format.js +83 -0
- package/dist/python-json.js +29 -0
- package/dist/python-text.js +74 -0
- package/dist/safe-path.js +137 -0
- package/dist/safety-error.js +10 -0
- package/dist/selection-state-cli.js +85 -0
- package/dist/selection-store.js +172 -0
- package/dist/selection.js +200 -0
- package/dist/status.js +24 -0
- package/dist/strict-json.js +248 -0
- package/dist/upstream-cli.js +110 -0
- package/dist/upstream-version.js +40 -0
- package/dist/upstream.js +253 -0
- package/dist/validate-generated-plugin-cli.js +118 -0
- package/dist/validator.js +317 -0
- package/dist/workspace.js +139 -0
- package/package.json +25 -4
- package/bin/superpowers-manager.js +0 -290
- package/scripts/adapters/codex/adapter +0 -603
- package/scripts/adapters/codex/lib.sh +0 -449
- package/scripts/adapters/codex/validate-generated-plugin.py +0 -314
- package/scripts/core/adapter.sh +0 -103
- package/scripts/core/common.sh +0 -59
- package/scripts/core/lifecycle.sh +0 -141
- package/scripts/core/provenance.sh +0 -135
- package/scripts/core/selection-state.py +0 -414
- package/scripts/core/selection.sh +0 -168
- package/scripts/core/status.sh +0 -27
- package/scripts/core/upstream.sh +0 -280
- package/scripts/core/validate-adapter-response.py +0 -284
- package/scripts/install +0 -59
- package/scripts/pin +0 -51
- package/scripts/prepare +0 -117
- package/scripts/probe +0 -82
- package/scripts/track-latest +0 -30
- package/scripts/uninstall +0 -35
- package/scripts/unpin +0 -41
- package/scripts/update +0 -34
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { COMMIT_INPUT_RE } from "./domain/refs.js";
|
|
4
|
+
import { SafetyError } from "./safety-error.js";
|
|
5
|
+
import { parseStrictJson, } from "./strict-json.js";
|
|
6
|
+
const INSTALLED_PROFILE = {
|
|
7
|
+
duplicateKeys: "last-wins",
|
|
8
|
+
nonStandardConstants: "reject",
|
|
9
|
+
maxDepth: 256,
|
|
10
|
+
};
|
|
11
|
+
function fail(message, cause) {
|
|
12
|
+
throw new SafetyError("codex-state", message, { cause });
|
|
13
|
+
}
|
|
14
|
+
function object(value) {
|
|
15
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
16
|
+
? value
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
async function jsonStringField(path, key) {
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = parseStrictJson(await readFile(path), INSTALLED_PROFILE);
|
|
23
|
+
}
|
|
24
|
+
catch (cause) {
|
|
25
|
+
fail(`cannot read installed Codex JSON ${path}`, cause);
|
|
26
|
+
}
|
|
27
|
+
const record = object(parsed);
|
|
28
|
+
if (record === undefined)
|
|
29
|
+
fail(`invalid installed Codex JSON ${path}`);
|
|
30
|
+
const value = record[key];
|
|
31
|
+
if (value === undefined || value === null)
|
|
32
|
+
return "";
|
|
33
|
+
if (typeof value !== "string")
|
|
34
|
+
fail(`invalid installed Codex JSON ${path}`);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
export function installedRootForVersion(searchRoot, marketplace, plugin, version) {
|
|
38
|
+
return join(searchRoot, "plugins", "cache", marketplace, plugin, version);
|
|
39
|
+
}
|
|
40
|
+
export async function codexMetadataCommit(path) {
|
|
41
|
+
const commit = await jsonStringField(path, "commit");
|
|
42
|
+
if (!COMMIT_INPUT_RE.test(commit) && !/^[0-9a-fA-F]{7}$/.test(commit)) {
|
|
43
|
+
fail(`invalid installed Codex commit in ${path}`);
|
|
44
|
+
}
|
|
45
|
+
return commit;
|
|
46
|
+
}
|
|
47
|
+
export async function manifestShortSha(path) {
|
|
48
|
+
const version = await jsonStringField(path, "version");
|
|
49
|
+
if (!version.includes("+manager."))
|
|
50
|
+
return "";
|
|
51
|
+
const short = version.slice(version.lastIndexOf(".") + 1);
|
|
52
|
+
return /^[0-9a-fA-F]{7}$/.test(short) ? short : "";
|
|
53
|
+
}
|
|
54
|
+
export async function installedCommitFromRoot(activeRoot) {
|
|
55
|
+
try {
|
|
56
|
+
return await codexMetadataCommit(join(activeRoot, ".superpowers-upstream.json"));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// The shell predecessor deliberately fell back to the generated manifest.
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return await manifestShortSha(join(activeRoot, ".codex-plugin", "plugin.json"));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function comparablePath(path) {
|
|
69
|
+
try {
|
|
70
|
+
return await realpath(path);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return resolve(path);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function pathsEqual(a, b) {
|
|
77
|
+
return (await comparablePath(a)) === (await comparablePath(b));
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Ports scripts/install. The shell sourced common.sh, provenance.sh,
|
|
2
|
+
// status.sh, lifecycle.sh and adapter.sh; the predicates now live in
|
|
3
|
+
// src/lifecycle.ts, the generated-metadata read lives in src/provenance.ts,
|
|
4
|
+
// and the adapter arrives through ctx.adapter.
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { oneLine } from "../cli-arguments.js";
|
|
7
|
+
import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
|
|
8
|
+
import { gatherProbe, replayOutcome } from "./probe.js";
|
|
9
|
+
import { runPrepare } from "./prepare.js";
|
|
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`);
|
|
15
|
+
}
|
|
16
|
+
async function invoke(call, failure, outcomes) {
|
|
17
|
+
let result;
|
|
18
|
+
try {
|
|
19
|
+
result = await call();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { ok: false, message: failure.unexpected, result: null };
|
|
23
|
+
}
|
|
24
|
+
outcomes.push(result.outcome);
|
|
25
|
+
const outcome = result.outcome;
|
|
26
|
+
if (result.status !== 0 || !outcome.ok) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
message: outcome.ok ? failure.invalidStatus : null,
|
|
30
|
+
result,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
result: { status: result.status, outcome },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// withWorkspace can throw AFTER its callback has already returned a fully
|
|
39
|
+
// computed StageOutcome: a post-success cleanup failure discards that return
|
|
40
|
+
// value entirely and rejects instead, UNLESS an `onCleanupFailure` reporter is
|
|
41
|
+
// supplied -- which gatherInstallStages below does, precisely so this class
|
|
42
|
+
// stays reserved for mkdtemp failure (nothing collected yet) and the
|
|
43
|
+
// callback's own throw (never reachable here; see gatherInstallStages).
|
|
44
|
+
// Carries the outcomes collected so far so runInstall's catch can still
|
|
45
|
+
// replay them instead of discarding them with a bare re-throw. Same shape as
|
|
46
|
+
// src/commands/uninstall.ts's GatherFailure, duplicated for the same reason
|
|
47
|
+
// invoke() is: no shared dependency between the two modules.
|
|
48
|
+
class GatherFailure extends Error {
|
|
49
|
+
inner;
|
|
50
|
+
outcomes;
|
|
51
|
+
constructor(inner, outcomes) {
|
|
52
|
+
super("install gather failed");
|
|
53
|
+
this.inner = inner;
|
|
54
|
+
this.outcomes = outcomes;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:44-58::TMPDIR=`, wrapped in the temporary workspace scripts/install
|
|
58
|
+
// created via spw_make_workspace + spw_install_workspace_trap. Performs no
|
|
59
|
+
// writes of its own -- same EPIPE-avoidance shape as gatherProbe and
|
|
60
|
+
// src/commands/uninstall.ts's gatherUninstall.
|
|
61
|
+
async function gatherInstallStages(ctx, selection, artifact) {
|
|
62
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:38::tmp_parent=` -- ${TMPDIR:-/tmp}. Matches
|
|
63
|
+
// src/commands/uninstall.ts's gatherUninstall.
|
|
64
|
+
const parent = ctx.env.TMPDIR ?? tmpdir();
|
|
65
|
+
const outcomes = [];
|
|
66
|
+
let cleanupWarning = null;
|
|
67
|
+
try {
|
|
68
|
+
const outcome = await withWorkspace(parent, "superpowers-manager.install.", async (workspace) => {
|
|
69
|
+
const env = { ...ctx.env, TMPDIR: workspace };
|
|
70
|
+
const failed = (message) => ({
|
|
71
|
+
kind: "failed",
|
|
72
|
+
outcomes,
|
|
73
|
+
message,
|
|
74
|
+
});
|
|
75
|
+
const adapterContext = { root: ctx.root, env };
|
|
76
|
+
// Stage 1: inspect ownership, re-checked even though gatherProbe just
|
|
77
|
+
// reported it. Mutation authority requires CURRENT, VALIDATED
|
|
78
|
+
// evidence -- a probe's answer is neither by the time this runs.
|
|
79
|
+
const ownershipFailure = ctx.adapter.presentation.callFailure("install-ownership", adapterContext);
|
|
80
|
+
const ownership = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure, outcomes);
|
|
81
|
+
if (!ownership.ok)
|
|
82
|
+
return failed(ownership.message);
|
|
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
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Stage 2: inspect update-control, re-checked for the same reason.
|
|
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)
|
|
95
|
+
return failed(control.message);
|
|
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
|
+
}
|
|
104
|
+
// Stage 3: the mutation itself. Nothing above may have issued this --
|
|
105
|
+
// that is the whole point of stages 1 and 2 running first.
|
|
106
|
+
const installFailure = ctx.adapter.presentation.callFailure("install", adapterContext);
|
|
107
|
+
const install = await invoke(() => ctx.adapter.install(artifact, adapterContext), installFailure, outcomes);
|
|
108
|
+
if (!install.ok)
|
|
109
|
+
return failed(install.message);
|
|
110
|
+
// Stage 4: inspect fingerprint, to verify the mutation actually took.
|
|
111
|
+
//
|
|
112
|
+
// Deliberately NOT short-circuited on `!inspected.ok`, unlike stages
|
|
113
|
+
// 1-3. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:57::spw_verify_installed_fingerprint` handed the inspect result to
|
|
114
|
+
// spw_verify_installed_fingerprint whatever it contained, and that
|
|
115
|
+
// function's first guard, from
|
|
116
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:91-94::spw_inspect_fingerprint`,
|
|
117
|
+
// is what turns a failed inspection into "error: installed manager
|
|
118
|
+
// fingerprint inspection failed after install."
|
|
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
|
|
123
|
+
// failed() instead reported the adapter's own generic diagnostic and
|
|
124
|
+
// dropped the post-install verification claim -- a mutation had
|
|
125
|
+
// already been issued at stage 3,
|
|
126
|
+
// so "the install could not be verified" is the contract, not "an
|
|
127
|
+
// adapter call failed". A ctx.adapter THROW is the one case with no
|
|
128
|
+
// result to verify against, and keeps the short-circuit.
|
|
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);
|
|
133
|
+
}
|
|
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";
|
|
141
|
+
return {
|
|
142
|
+
kind: "verified",
|
|
143
|
+
outcomes,
|
|
144
|
+
status: verified ? 0 : 1,
|
|
145
|
+
stdout: output.stdout,
|
|
146
|
+
stderr: output.stderr,
|
|
147
|
+
};
|
|
148
|
+
}, {
|
|
149
|
+
// Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure,
|
|
150
|
+
// so the StageOutcome the callback already computed still comes back
|
|
151
|
+
// as `outcome` below instead of being discarded. `report` runs
|
|
152
|
+
// synchronously, as the option requires (src/workspace.ts).
|
|
153
|
+
onCleanupFailure: (path) => {
|
|
154
|
+
cleanupWarning = workspaceRemovalFailure(path);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
return { outcome, cleanupWarning };
|
|
158
|
+
}
|
|
159
|
+
catch (cause) {
|
|
160
|
+
// Reachable only for mkdtemp failure (nothing collected yet) -- the
|
|
161
|
+
// callback above never throws, so a post-success cleanup failure is
|
|
162
|
+
// already handled by onCleanupFailure and cannot reach here.
|
|
163
|
+
throw new GatherFailure(cause, outcomes);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export async function runInstall(argv, ctx) {
|
|
167
|
+
// scripts/install never reads "$@", so extra arguments are silently
|
|
168
|
+
// ignored -- the same asymmetry runPrepare and runUninstall document.
|
|
169
|
+
void argv;
|
|
170
|
+
ctx.stdout.write(`${ctx.adapter.presentation.installNotice}\n`);
|
|
171
|
+
let probe;
|
|
172
|
+
try {
|
|
173
|
+
probe = await gatherProbe(ctx);
|
|
174
|
+
}
|
|
175
|
+
catch (cause) {
|
|
176
|
+
// gatherProbe performs no writes of its own (src/commands/probe.ts), so
|
|
177
|
+
// this catch cannot also be reached by an EPIPE from install's own
|
|
178
|
+
// output: the NOTE line above already left the try, and everything below
|
|
179
|
+
// runs only after this try/catch has resolved.
|
|
180
|
+
//
|
|
181
|
+
// This is a SECOND consumer of gatherProbe's throw channel --
|
|
182
|
+
// `src/commands/probe.ts:251-309::THREE exceptions, all inherited and none a regression:`'s
|
|
183
|
+
// runProbe catch is the first. Because both consumers wrap the identical
|
|
184
|
+
// function, its long comment there enumerates exactly what can reach THIS
|
|
185
|
+
// stream too, including the three foreign-text exceptions at :251-296:
|
|
186
|
+
// 1. :251-262 -- resolveRef splices git's own combined stdout+stderr
|
|
187
|
+
// into its text. Reached on probe's DEFAULT path, which that comment
|
|
188
|
+
// defines as every invocation NOT resolving a saved pin: a 40-hex
|
|
189
|
+
// ref returns a "raw-commit" resolution at
|
|
190
|
+
// `src/upstream.ts:162-164::return { kind: "raw-commit"`
|
|
191
|
+
// before any git call, so it reaches no splice at all.
|
|
192
|
+
// 2. :263-280 -- src/selection-store.ts's read path interpolates the
|
|
193
|
+
// caught error's own message, so Node errno prose can appear.
|
|
194
|
+
// AGENTS.md grandfathers that module's wording.
|
|
195
|
+
// 3. :281-296 -- a SPAWN-level git failure, a different channel from
|
|
196
|
+
// exception 1's exit-status one: on the non-ENOENT arm of
|
|
197
|
+
// `src/git.ts:47-52::if (typeof failure.code === "string") {`, runGit
|
|
198
|
+
// rejects with "cannot run git: " followed by the Node spawn error's
|
|
199
|
+
// own message.
|
|
200
|
+
// Not repeated in full here; read it there.
|
|
201
|
+
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
// Replay first, on both paths, before any decision -- clause 1.
|
|
205
|
+
for (const outcome of probe.outcomes)
|
|
206
|
+
replayOutcome(outcome, ctx);
|
|
207
|
+
if (probe.status === 1) {
|
|
208
|
+
if (probe.message !== null) {
|
|
209
|
+
ctx.stderr.write(`error: ${probe.message}\n`);
|
|
210
|
+
}
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
const facts = probe.facts;
|
|
214
|
+
if (facts.ownership.installEligibility.kind === "blocked") {
|
|
215
|
+
writeOutput(facts.ownership.installEligibility.output, ctx);
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
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;
|
|
229
|
+
}
|
|
230
|
+
let prepared;
|
|
231
|
+
try {
|
|
232
|
+
prepared = await ctx.adapter.readPrepared({ root: ctx.root, env: ctx.env });
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
ctx.stderr.write("error: cannot read prepared harness state\n");
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
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");
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
let stage;
|
|
246
|
+
try {
|
|
247
|
+
stage = await gatherInstallStages(ctx, facts.selection, prepared.outcome.result);
|
|
248
|
+
}
|
|
249
|
+
catch (cause) {
|
|
250
|
+
const outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
|
|
251
|
+
for (const outcome of outcomes)
|
|
252
|
+
replayOutcome(outcome, ctx);
|
|
253
|
+
const inner = cause instanceof GatherFailure ? cause.inner : cause;
|
|
254
|
+
ctx.stderr.write(`error: ${oneLine(inner)}\n`);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
const { outcome, cleanupWarning } = stage;
|
|
258
|
+
for (const each of outcome.outcomes)
|
|
259
|
+
replayOutcome(each, ctx);
|
|
260
|
+
let status;
|
|
261
|
+
if (outcome.kind === "blocked") {
|
|
262
|
+
writeOutput(outcome.output, ctx);
|
|
263
|
+
status = 1;
|
|
264
|
+
}
|
|
265
|
+
else if (outcome.kind === "failed") {
|
|
266
|
+
if (outcome.message !== null) {
|
|
267
|
+
ctx.stderr.write(`error: ${outcome.message}\n`);
|
|
268
|
+
}
|
|
269
|
+
status = 1;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// :99-100 printed both lines BEFORE deciding, on every path that got this
|
|
273
|
+
// far; the verified outcome already carries both renderer arrays, so they
|
|
274
|
+
// are written in order regardless of its status.
|
|
275
|
+
for (const line of outcome.stdout)
|
|
276
|
+
ctx.stdout.write(`${line}\n`);
|
|
277
|
+
for (const line of outcome.stderr)
|
|
278
|
+
ctx.stderr.write(`${line}\n`);
|
|
279
|
+
status = outcome.status;
|
|
280
|
+
}
|
|
281
|
+
if (cleanupWarning !== null) {
|
|
282
|
+
// A leaked workspace is reported even when the domain outcome above was
|
|
283
|
+
// itself a success: the fingerprint verification that produced that
|
|
284
|
+
// success already completed against the adapter before cleanup ran, so
|
|
285
|
+
// it is not being reported as unverified -- but something did still go
|
|
286
|
+
// wrong, and AGENTS.md's fail-closed rule extends to it.
|
|
287
|
+
ctx.stderr.write(`error: ${cleanupWarning}\n`);
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
return status;
|
|
291
|
+
}
|