superpowers-manager 0.2.0 → 0.3.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 +112 -47
- package/bin/superpowers-manager.js +9 -283
- package/dist/adapter-result.js +204 -0
- package/dist/adapter.js +649 -0
- package/dist/atomic.js +142 -0
- package/dist/cli-arguments.js +32 -0
- package/dist/cli.js +293 -0
- package/dist/codex-json.js +89 -0
- package/dist/codex-state.js +78 -0
- package/dist/commands/context.js +1 -0
- package/dist/commands/install.js +376 -0
- package/dist/commands/pin.js +79 -0
- package/dist/commands/prepare.js +527 -0
- package/dist/commands/probe.js +392 -0
- package/dist/commands/track-latest.js +44 -0
- package/dist/commands/uninstall.js +308 -0
- package/dist/commands/unpin.js +95 -0
- package/dist/commands/update.js +138 -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/hooks.js +290 -0
- package/dist/lifecycle.js +220 -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 -2
- 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,392 @@
|
|
|
1
|
+
import { assertFailureWritable, writeAdapterFailure, } from "../adapter-result.js";
|
|
2
|
+
import { oneLine } from "../cli-arguments.js";
|
|
3
|
+
import { computeEffectiveSelection } from "../effective-selection.js";
|
|
4
|
+
import { generatedCommitOrEmpty } from "../provenance.js";
|
|
5
|
+
import { displaySource } from "../selection.js";
|
|
6
|
+
import { statusForCommits } from "../status.js";
|
|
7
|
+
export const PROBE_USAGE = "error: usage: superpowers-manager probe [--porcelain]\n";
|
|
8
|
+
// One ordered table drives both formats, so the porcelain key order
|
|
9
|
+
// (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:43-59::printf 'requested_ref`) and the human label order (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:61-77::printf 'requested ref`) cannot
|
|
10
|
+
// drift apart. Two parallel lists could each stay self-consistent while
|
|
11
|
+
// disagreeing with one another.
|
|
12
|
+
function fields(f) {
|
|
13
|
+
return [
|
|
14
|
+
{ key: "requested_ref", label: "requested ref", value: f.requestedRef },
|
|
15
|
+
{ key: "resolved_ref", label: "resolved ref", value: f.resolvedRef },
|
|
16
|
+
{ key: "desired_commit", label: "desired commit", value: f.desiredCommit },
|
|
17
|
+
{
|
|
18
|
+
key: "generated_commit",
|
|
19
|
+
label: "generated plugin commit",
|
|
20
|
+
value: f.generatedCommit,
|
|
21
|
+
absent: "not present",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: "installed_commit",
|
|
25
|
+
label: "installed manager commit or fingerprint",
|
|
26
|
+
value: f.installedCommit,
|
|
27
|
+
absent: "not detected",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "identity_state",
|
|
31
|
+
label: "Codex identity state",
|
|
32
|
+
value: f.identityState,
|
|
33
|
+
},
|
|
34
|
+
{ key: "status", label: "status", value: f.status },
|
|
35
|
+
{
|
|
36
|
+
key: "selection_origin",
|
|
37
|
+
label: "selection origin",
|
|
38
|
+
value: f.selectionOrigin,
|
|
39
|
+
},
|
|
40
|
+
{ key: "selection_mode", label: "selection mode", value: f.selectionMode },
|
|
41
|
+
{
|
|
42
|
+
key: "upstream_source_origin",
|
|
43
|
+
label: "upstream source origin",
|
|
44
|
+
value: f.upstreamSourceOrigin,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "effective_source",
|
|
48
|
+
label: "effective source",
|
|
49
|
+
value: f.effectiveSource,
|
|
50
|
+
},
|
|
51
|
+
{ key: "saved_mode", label: "saved mode", value: f.savedMode },
|
|
52
|
+
{ key: "saved_source", label: "saved source", value: f.savedSource },
|
|
53
|
+
{
|
|
54
|
+
key: "saved_requested_ref",
|
|
55
|
+
label: "saved requested ref",
|
|
56
|
+
value: f.savedRequestedRef,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "saved_resolved_ref",
|
|
60
|
+
label: "saved resolved ref",
|
|
61
|
+
value: f.savedResolvedRef,
|
|
62
|
+
},
|
|
63
|
+
{ key: "saved_commit", label: "saved commit", value: f.savedCommit },
|
|
64
|
+
{ key: "update_control", label: "update control", value: f.updateControl },
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
// Derived from the same fields() table, so the key list a test asserts and
|
|
68
|
+
// the key list formatPorcelain emits cannot be different lists. The facts
|
|
69
|
+
// argument is irrelevant here — fields() is total over ProbeFacts and the
|
|
70
|
+
// keys do not depend on the values.
|
|
71
|
+
const NO_FACTS = {
|
|
72
|
+
requestedRef: "",
|
|
73
|
+
resolvedRef: "",
|
|
74
|
+
desiredCommit: "",
|
|
75
|
+
generatedCommit: "",
|
|
76
|
+
installedCommit: "",
|
|
77
|
+
identityState: "",
|
|
78
|
+
status: "",
|
|
79
|
+
selectionOrigin: "",
|
|
80
|
+
selectionMode: "",
|
|
81
|
+
upstreamSourceOrigin: "",
|
|
82
|
+
effectiveSource: "",
|
|
83
|
+
savedMode: "",
|
|
84
|
+
savedSource: "",
|
|
85
|
+
savedRequestedRef: "",
|
|
86
|
+
savedResolvedRef: "",
|
|
87
|
+
savedCommit: "",
|
|
88
|
+
updateControl: "",
|
|
89
|
+
};
|
|
90
|
+
export const PROBE_PORCELAIN_KEYS = fields(NO_FACTS).map((field) => field.key);
|
|
91
|
+
export function formatPorcelain(f) {
|
|
92
|
+
return fields(f)
|
|
93
|
+
.map((field) => `${field.key}=${field.value}\n`)
|
|
94
|
+
.join("");
|
|
95
|
+
}
|
|
96
|
+
export function formatHuman(f) {
|
|
97
|
+
let text = fields(f)
|
|
98
|
+
.map((field) => {
|
|
99
|
+
const shown = field.value.length === 0 && field.absent !== undefined
|
|
100
|
+
? field.absent
|
|
101
|
+
: field.value;
|
|
102
|
+
return `${field.label}: ${shown}\n`;
|
|
103
|
+
})
|
|
104
|
+
.join("");
|
|
105
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:78-81::if [ "$SPW_SELECTION_ORIGIN`.
|
|
106
|
+
if (f.selectionOrigin !== f.upstreamSourceOrigin) {
|
|
107
|
+
text +=
|
|
108
|
+
"warning: effective ref and source have mixed origins " +
|
|
109
|
+
`(ref: ${f.selectionOrigin}, source: ${f.upstreamSourceOrigin})\n`;
|
|
110
|
+
}
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
// Ports scripts/core/validate-adapter-response.py's replay (:235-238) and its
|
|
114
|
+
// error/hint block (:269-272). The shell ran that validator on EVERY adapter
|
|
115
|
+
// response, so adapter messages reached the operator on their declared streams
|
|
116
|
+
// in array order, and a controlled failure printed `error:` plus one `hint:`
|
|
117
|
+
// per hint. DIAG-ADAPTER-01 retains that contract
|
|
118
|
+
// (docs/baseline/protocol-disposition.md); dropping it here would be a
|
|
119
|
+
// silent diagnostics regression, not a simplification.
|
|
120
|
+
//
|
|
121
|
+
// Interpolating error.message and each hint is the sanctioned form (AGENTS.md):
|
|
122
|
+
// src/adapter.ts has 52 `fail()` sites and none interpolates a caught error's
|
|
123
|
+
// message, so the callee owns every failure reachable on this path. Those two
|
|
124
|
+
// writes now live in writeAdapterFailure (src/adapter-result.ts), which
|
|
125
|
+
// validates all three strings before the first of them reaches the stream.
|
|
126
|
+
//
|
|
127
|
+
// This writes to ctx, so it MUST NOT be called from inside gatherProbe's try
|
|
128
|
+
// -- see the ProbeOutcome note below.
|
|
129
|
+
export function replayOutcome(outcome, ctx) {
|
|
130
|
+
// Hoisted ABOVE the message loop, and that ordering is the point: a failure
|
|
131
|
+
// whose code, message, or a hint carries a terminal control character must
|
|
132
|
+
// leave both streams untouched, not the context lines followed by nothing.
|
|
133
|
+
// Returns immediately on a succeeding outcome.
|
|
134
|
+
assertFailureWritable(outcome);
|
|
135
|
+
for (const message of outcome.messages) {
|
|
136
|
+
const stream = message.channel === "stdout" ? ctx.stdout : ctx.stderr;
|
|
137
|
+
stream.write(`${message.text}\n`);
|
|
138
|
+
}
|
|
139
|
+
// The message loop stays unguarded on purpose: D8b scopes the check to code,
|
|
140
|
+
// message, and hints, and AdapterMessageLog escapes message text before it is
|
|
141
|
+
// stored, so scanning it here would re-check a population that cannot fail.
|
|
142
|
+
writeAdapterFailure(ctx, outcome);
|
|
143
|
+
}
|
|
144
|
+
// `runAdapter` reports a CONTROLLED failure by RETURN VALUE, not by throwing
|
|
145
|
+
// (`src/adapter-result.ts:32-35::export interface AdapterResult`). The shell got
|
|
146
|
+
// fail-closed behaviour for free: spw_invoke_adapter returned 1 and
|
|
147
|
+
// scripts/probe ran under `set -eu`.
|
|
148
|
+
// Omitting the status check here would read a failed inspection as absent
|
|
149
|
+
// evidence and report it as success.
|
|
150
|
+
//
|
|
151
|
+
// It does still THROW for a non-AdapterFailure cause (runAdapter's closing
|
|
152
|
+
// `throw cause`, src/adapter.ts). That is caught here rather than in runProbe's
|
|
153
|
+
// outer catch, because the two need different diagnostics -- see spec §3.3a.
|
|
154
|
+
//
|
|
155
|
+
// Reached through ctx.adapter, not a direct module-level dependency on the
|
|
156
|
+
// adapter module: src/commands/prepare.ts and this module are the two the
|
|
157
|
+
// injected double must observe, because install reaches the adapter through
|
|
158
|
+
// gatherProbe and runPrepare. Spec §4.5.
|
|
159
|
+
async function inspect(view, key, ctx) {
|
|
160
|
+
let result;
|
|
161
|
+
try {
|
|
162
|
+
result = await ctx.adapter(["inspect", "--view", view], {
|
|
163
|
+
root: ctx.root,
|
|
164
|
+
env: ctx.env,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Deliberately does NOT interpolate the cause. A rethrown non-
|
|
169
|
+
// AdapterFailure is by construction the one failure src/adapter.ts chose
|
|
170
|
+
// not to own: free-form text of unknown provenance, which AGENTS.md bars
|
|
171
|
+
// from this stream. `view` is a bounded token -- one of three literals
|
|
172
|
+
// this function is ever called with -- so naming the input is safe.
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
outcome: null,
|
|
176
|
+
message: `cannot inspect Codex adapter state for view ${view}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const outcome = result.outcome;
|
|
180
|
+
if (result.status !== 0 || !outcome.ok) {
|
|
181
|
+
// The `outcome.ok && status !== 0` combination cannot arise from
|
|
182
|
+
// successResult/failureResult, so it gets its own hand-written message
|
|
183
|
+
// rather than falling through to a replay that would print nothing.
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
outcome,
|
|
187
|
+
message: outcome.ok
|
|
188
|
+
? `adapter reported a failure status for inspect --view ${view}`
|
|
189
|
+
: null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const value = outcome.result?.[key];
|
|
193
|
+
// The Python reader printed the empty string for a JSON null
|
|
194
|
+
// (scripts/core/provenance.sh's spw_json_get), and `fingerprint` is null
|
|
195
|
+
// whenever no plugin version is active (`src/adapter.ts:820::fingerprint: null`).
|
|
196
|
+
if (value === null || value === undefined) {
|
|
197
|
+
return { ok: true, value: "", outcome };
|
|
198
|
+
}
|
|
199
|
+
if (typeof value !== "string") {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
outcome,
|
|
203
|
+
message: `adapter returned a non-string ${key} for inspect --view ${view}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, value, outcome };
|
|
207
|
+
}
|
|
208
|
+
// Runs every step that can throw or fail closed, returning the outcome as
|
|
209
|
+
// data. It performs no writes of its own, so nothing inside runProbe's try can
|
|
210
|
+
// raise EPIPE — the same shape as src/commands/unpin.ts's attemptUnpin.
|
|
211
|
+
//
|
|
212
|
+
// The outcomes are CARRIED OUT rather than replayed in place, for that same
|
|
213
|
+
// reason: replayOutcome writes to ctx, and a write inside this try could
|
|
214
|
+
// raise EPIPE and be caught and relabelled as a selection failure. runProbe
|
|
215
|
+
// replays them, in collection order, after the try/catch has resolved. The
|
|
216
|
+
// operator-visible result is identical -- probe emits nothing else until the
|
|
217
|
+
// very end -- and the EPIPE hazard never exists.
|
|
218
|
+
// Exported for src/commands/install.ts and src/commands/update.ts, which need
|
|
219
|
+
// probe's FACTS rather than its rendering. The historical
|
|
220
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:39-41::spw_probe_field`
|
|
221
|
+
// implementation awk-parsed the porcelain back into fields; that round trip is
|
|
222
|
+
// gone.
|
|
223
|
+
export async function gatherProbe(ctx) {
|
|
224
|
+
// Order mirrors `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:24-40::spw_compute_effective_selection` exactly.
|
|
225
|
+
const selection = await computeEffectiveSelection(ctx.root, ctx.env);
|
|
226
|
+
const generatedCommit = await generatedCommitOrEmpty(ctx.root);
|
|
227
|
+
const outcomes = [];
|
|
228
|
+
const collect = async (view, key) => {
|
|
229
|
+
const result = await inspect(view, key, ctx);
|
|
230
|
+
if (result.outcome !== null)
|
|
231
|
+
outcomes.push(result.outcome);
|
|
232
|
+
return result;
|
|
233
|
+
};
|
|
234
|
+
const fingerprint = await collect("fingerprint", "fingerprint");
|
|
235
|
+
if (!fingerprint.ok) {
|
|
236
|
+
return { status: 1, outcomes, message: fingerprint.message };
|
|
237
|
+
}
|
|
238
|
+
const ownership = await collect("ownership", "identity_state");
|
|
239
|
+
if (!ownership.ok) {
|
|
240
|
+
return { status: 1, outcomes, message: ownership.message };
|
|
241
|
+
}
|
|
242
|
+
const updateControl = await collect("update-control", "update_control");
|
|
243
|
+
if (!updateControl.ok) {
|
|
244
|
+
return { status: 1, outcomes, message: updateControl.message };
|
|
245
|
+
}
|
|
246
|
+
const saved = selection.saved;
|
|
247
|
+
return {
|
|
248
|
+
status: 0,
|
|
249
|
+
outcomes,
|
|
250
|
+
facts: {
|
|
251
|
+
requestedRef: selection.requestedRef,
|
|
252
|
+
resolvedRef: selection.resolvedRef,
|
|
253
|
+
desiredCommit: selection.desiredCommit,
|
|
254
|
+
generatedCommit,
|
|
255
|
+
installedCommit: fingerprint.value,
|
|
256
|
+
identityState: ownership.value,
|
|
257
|
+
status: statusForCommits(selection.desiredCommit, generatedCommit, fingerprint.value),
|
|
258
|
+
selectionOrigin: selection.selectionOrigin,
|
|
259
|
+
selectionMode: selection.selectionMode,
|
|
260
|
+
upstreamSourceOrigin: selection.upstreamSourceOrigin,
|
|
261
|
+
effectiveSource: displaySource(selection.effectiveSource),
|
|
262
|
+
savedMode: saved.saved_mode,
|
|
263
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:26-30::if [ -`: an absent saved source stays empty rather than
|
|
264
|
+
// being run through displaySource, which would render <redacted-source>.
|
|
265
|
+
savedSource: saved.saved_source.length > 0 ? displaySource(saved.saved_source) : "",
|
|
266
|
+
savedRequestedRef: saved.saved_requested_ref,
|
|
267
|
+
savedResolvedRef: saved.saved_resolved_ref,
|
|
268
|
+
savedCommit: saved.saved_commit,
|
|
269
|
+
updateControl: updateControl.value,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
export async function runProbe(argv, ctx) {
|
|
274
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:42::porcelain` tested only `[ "${1:-}" = "--porcelain" ]`, so a typo'd
|
|
275
|
+
// flag silently produced human output. Rejecting it is a deliberate
|
|
276
|
+
// narrowing, recorded as a port-only entry in
|
|
277
|
+
// tests/migration-inventory/probe.md.
|
|
278
|
+
//
|
|
279
|
+
// This guard is NOT the production path. src/cli.ts's parseArgs rejects the
|
|
280
|
+
// same inputs first, before preflight, with the usage block the CLI's other
|
|
281
|
+
// usage errors carry — exactly the arrangement track-latest and unpin have,
|
|
282
|
+
// where the in-module check is an unreachable-from-CLI duplicate. It stays
|
|
283
|
+
// because runProbe is also called directly by tests and by any future
|
|
284
|
+
// in-process caller that has not been through parseArgs.
|
|
285
|
+
const porcelain = argv.length === 1 && argv[0] === "--porcelain";
|
|
286
|
+
if (!porcelain && argv.length !== 0) {
|
|
287
|
+
ctx.stderr.write(PROBE_USAGE);
|
|
288
|
+
return 2;
|
|
289
|
+
}
|
|
290
|
+
let outcome;
|
|
291
|
+
try {
|
|
292
|
+
outcome = await gatherProbe(ctx);
|
|
293
|
+
}
|
|
294
|
+
catch (cause) {
|
|
295
|
+
// Hand-written messages, per AGENTS.md's reader-diagnostics rule. Reachable
|
|
296
|
+
// here: the selectionErrors validateSource raises
|
|
297
|
+
// (`src/selection.ts:137::malformed`, :140, requireSingleLineString at :77),
|
|
298
|
+
// reached from
|
|
299
|
+
// computeEffectiveSelection's validateSource call
|
|
300
|
+
// (src/effective-selection.ts); the ones validateRecord raises
|
|
301
|
+
// (`src/selection.ts:198::integer`, :219, requireObject at :47, requireExactKeys at
|
|
302
|
+
// :64, validatePinnedRecord at :170-183), reached from
|
|
303
|
+
// `src/selection-store.ts:103::return validateRecord` on the read path; the
|
|
304
|
+
// selectionErrors requireAbsolute and selectionConfigDir
|
|
305
|
+
// (src/effective-selection.ts) raise
|
|
306
|
+
// for a non-absolute or missing config directory; readConfigRef's `cannot
|
|
307
|
+
// read packaged upstream ref <path>` (src/upstream.ts); and resolveRef's own
|
|
308
|
+
// two no-match diagnostics, `no stable semver tag found for latest-release`
|
|
309
|
+
// and `cannot resolve upstream ref: <ref>` (src/upstream.ts). normalizeSaved
|
|
310
|
+
// (`src/selection.ts:222-241::normalizeSaved`) throws nothing at all.
|
|
311
|
+
//
|
|
312
|
+
// THREE exceptions, all inherited and none a regression:
|
|
313
|
+
// 1. resolveRef splices git's combined stdout+stderr into its own text
|
|
314
|
+
// (its three `cannot query upstream ...` throws, src/upstream.ts),
|
|
315
|
+
// reached via computeEffectiveSelection's resolveRef call
|
|
316
|
+
// (src/effective-selection.ts). The packaged ref is `latest-release`,
|
|
317
|
+
// which resolveRef answers with an ls-remote, so an ordinary unpinned
|
|
318
|
+
// run reaches this rather than only an exotic corner -- though not
|
|
319
|
+
// every unpinned run does: resolveRef's COMMIT_INPUT_RE branch returns
|
|
320
|
+
// before any runGit call, so a 40-hex requested ref splices nothing.
|
|
321
|
+
// Pinned by
|
|
322
|
+
// `tests/unit/upstream.test.js:460::void test("resolveRef reports a query failure for latest`,
|
|
323
|
+
// :471, and :483.
|
|
324
|
+
// 2. The
|
|
325
|
+
// `src/selection-store.ts:120-124::cause.module === "selection") {`
|
|
326
|
+
// site (same shape at :49, :86, :98) is the module AGENTS.md's
|
|
327
|
+
// `src/selection-store.ts` bullet grandfathers:
|
|
328
|
+
// it interpolates the caught error's own message, so Node errno
|
|
329
|
+
// prose can reach this stream. Reached on the READ path only, via
|
|
330
|
+
// loadSavedSelection (src/effective-selection.ts) ->
|
|
331
|
+
// readSelectionState
|
|
332
|
+
// (`src/selection-store.ts:149::export async function readSelectionState`).
|
|
333
|
+
// This module's four write-only interpolating sites -- ensureStateDirectory
|
|
334
|
+
// (:172), finalStateDiagnostic (:197), and the two in
|
|
335
|
+
// writeSelectionState's own catch (:225, :231) -- are all
|
|
336
|
+
// unreachable from probe, which never writes.
|
|
337
|
+
// 3. Every runGit call site inside resolveRef (src/upstream.ts) can
|
|
338
|
+
// reject instead of resolving. On the non-ENOENT arm of
|
|
339
|
+
// `src/git.ts:47-52::if (typeof`, runGit builds the message
|
|
340
|
+
// "cannot run git: " followed by the Node spawn error's own message
|
|
341
|
+
// (:51) and rejects with a SafetyError carrying it (:52), so that
|
|
342
|
+
// Node spawn-level text reaches ctx.stderr through this catch. The
|
|
343
|
+
// ENOENT arm (:50) is hand-written and carries nothing. A non-zero
|
|
344
|
+
// *exit status* is handled by exception 1 above; this is the
|
|
345
|
+
// *spawn-level* case, where runGit throws rather than returning a
|
|
346
|
+
// status.
|
|
347
|
+
//
|
|
348
|
+
// oneLine() at this catch collapses each of exceptions 1 and 3 -- the two
|
|
349
|
+
// carrying git-derived text -- to a single line. It collapses CR/LF only,
|
|
350
|
+
// so it bounds how much of that text lands, not what it may contain.
|
|
351
|
+
//
|
|
352
|
+
// fetchExactCommit is deliberately NOT in this list, unlike prepare's
|
|
353
|
+
// fetchExactCommit exception, which runPrepare's catch block documents.
|
|
354
|
+
// Its only callers are `src/upstream-cli.ts:83::await fetchExactCommit` and
|
|
355
|
+
// gatherPrepare's own call in src/commands/prepare.ts (git grep -n
|
|
356
|
+
// fetchExactCommit -- src/ is the check that keeps "only" true), so probe
|
|
357
|
+
// never reaches it and
|
|
358
|
+
// its splice sites cannot appear on this stream. Do not add it back by
|
|
359
|
+
// symmetry with prepare.
|
|
360
|
+
//
|
|
361
|
+
// generatedCommitOrEmpty is not in this list either: it cannot throw at
|
|
362
|
+
// all. It (src/provenance.ts) delegates to readGeneratedCommitLenient,
|
|
363
|
+
// which catches every failure and returns "".
|
|
364
|
+
//
|
|
365
|
+
// A non-AdapterFailure re-thrown by runAdapter's closing `throw cause`
|
|
366
|
+
// (src/adapter.ts) does NOT reach here: inspect() catches it and converts
|
|
367
|
+
// it to a hand-written message per AGENTS.md's reader-diagnostics rule -- a
|
|
368
|
+
// rethrown cause is exactly the failure src/adapter.ts declined to own, so
|
|
369
|
+
// its text must never reach this stream. See §3.3a.
|
|
370
|
+
//
|
|
371
|
+
// gatherProbe performs no writes of its own, so this catch cannot also be
|
|
372
|
+
// reached by an EPIPE from probe's own output — every write below runs
|
|
373
|
+
// only after this try/catch has resolved.
|
|
374
|
+
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
// Replay first, on both paths: the shell validator replayed every response's
|
|
378
|
+
// messages whether or not that response was a failure at
|
|
379
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/validate-adapter-response.py:268::replay(messages)`.
|
|
380
|
+
for (const each of outcome.outcomes)
|
|
381
|
+
replayOutcome(each, ctx);
|
|
382
|
+
if (outcome.status === 1) {
|
|
383
|
+
// null means replayOutcome already emitted the adapter's own `error:`
|
|
384
|
+
// and `hint:` lines for the failing outcome.
|
|
385
|
+
if (outcome.message !== null) {
|
|
386
|
+
ctx.stderr.write(`error: ${outcome.message}\n`);
|
|
387
|
+
}
|
|
388
|
+
return 1;
|
|
389
|
+
}
|
|
390
|
+
ctx.stdout.write(porcelain ? formatPorcelain(outcome.facts) : formatHuman(outcome.facts));
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { oneLine } from "../cli-arguments.js";
|
|
2
|
+
import { selectionStatePath, loadSavedSelection, UPSTREAM_URL_DEFAULT, } from "../effective-selection.js";
|
|
3
|
+
import { validateSource } from "../selection.js";
|
|
4
|
+
import { writeSelectionState } from "../selection-store.js";
|
|
5
|
+
export async function runTrackLatest(argv, ctx) {
|
|
6
|
+
if (argv.length !== 0) {
|
|
7
|
+
ctx.stderr.write("error: usage: superpowers-manager track-latest\n");
|
|
8
|
+
return 2;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
// Read first. This is a deliberate redundant boundary check, not the
|
|
12
|
+
// enforcing guard: writeSelectionState below already refuses to overwrite
|
|
13
|
+
// a corrupt existing record on its own (src/selection-store.ts's
|
|
14
|
+
// writeSelectionState calls readSelectionState on the same path before
|
|
15
|
+
// writing, under "Invalid existing state must block overwrite" —
|
|
16
|
+
// readSelectionState's own readOpenedRecord/parseRecordBytes is what
|
|
17
|
+
// validates an existing record, via validateRecord at
|
|
18
|
+
// `src/selection-store.ts:103::return validateRecord`). Calling loadSavedSelection here preserves
|
|
19
|
+
// `git show 349fe2ed405b371ec2de1347bb3fc50c6bc15dc4:scripts/track-latest:20-21::read`'s read-then-write shape, so this command
|
|
20
|
+
// stays fail-closed on its own terms rather than solely by depending on
|
|
21
|
+
// the store's internals.
|
|
22
|
+
await loadSavedSelection(ctx.env);
|
|
23
|
+
const source = ctx.env.SUPERPOWERS_UPSTREAM_URL || UPSTREAM_URL_DEFAULT;
|
|
24
|
+
validateSource(source);
|
|
25
|
+
await writeSelectionState(selectionStatePath(ctx.env), {
|
|
26
|
+
schema_version: 1,
|
|
27
|
+
mode: "track-latest",
|
|
28
|
+
source,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
// Every throw reachable here is a hand-written SafetyError from
|
|
33
|
+
// loadSavedSelection, validateSource, selectionStatePath, or
|
|
34
|
+
// writeSelectionState — re-emitting a subordinate module's own
|
|
35
|
+
// diagnostic is the sanctioned form of interpolation (see AGENTS.md's
|
|
36
|
+
// diagnostics convention). The try ends here, before the stdout write
|
|
37
|
+
// below: an EPIPE from that write must never be caught and relabelled
|
|
38
|
+
// as one of those modules' own diagnostics.
|
|
39
|
+
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
ctx.stdout.write("saved upstream selection: latest stable release\n");
|
|
43
|
+
return 0;
|
|
44
|
+
}
|