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
package/dist/commands/prepare.js
CHANGED
|
@@ -1,39 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
3
|
import { atomicReplaceDir } from "../atomic.js";
|
|
4
4
|
import { oneLine } from "../cli-arguments.js";
|
|
5
5
|
import { computeEffectiveSelection } from "../effective-selection.js";
|
|
6
6
|
import { runGit } from "../git.js";
|
|
7
|
-
import { readManifest } from "../hooks.js";
|
|
8
|
-
import { writeProvenance } from "../provenance.js";
|
|
9
7
|
import { SafetyError } from "../safety-error.js";
|
|
10
|
-
import { manifestVersionForRef } from "../upstream-version.js";
|
|
11
8
|
import { fetchExactCommit, gitSafeSource } from "../upstream.js";
|
|
12
9
|
import { BOUNDED_EXECUTABLE, UNBOUNDED_LEGACY, launchFailureMessage, resolveValidator, runValidator, displayPath, } from "../validator.js";
|
|
13
10
|
import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
|
|
14
11
|
import { replayOutcome } from "./probe.js";
|
|
15
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:64-67::spw_require_upstream_path "$cache/skills`, via spw_require_upstream_path. Order is the shell's;
|
|
16
|
-
// the first miss wins.
|
|
17
|
-
const REQUIRED_UPSTREAM = [
|
|
18
|
-
{ path: "skills", label: "skills/" },
|
|
19
|
-
{ path: "LICENSE", label: "LICENSE" },
|
|
20
|
-
{ path: "README.md", label: "README.md" },
|
|
21
|
-
{ path: "CODE_OF_CONDUCT.md", label: "CODE_OF_CONDUCT.md" },
|
|
22
|
-
];
|
|
23
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:73-77::spw_copy_path_if_present "$cache/skills`. Same names on both sides.
|
|
24
|
-
const COPY_PATHS = [
|
|
25
|
-
"skills",
|
|
26
|
-
"assets",
|
|
27
|
-
"LICENSE",
|
|
28
|
-
"README.md",
|
|
29
|
-
"CODE_OF_CONDUCT.md",
|
|
30
|
-
];
|
|
31
|
-
const RESOLUTION_KINDS = [
|
|
32
|
-
"latest-release",
|
|
33
|
-
"tag",
|
|
34
|
-
"ref",
|
|
35
|
-
"raw-commit",
|
|
36
|
-
];
|
|
37
12
|
// Every message this module writes is hand-written here. The cause is attached
|
|
38
13
|
// for debuggability and never reaches a stream: oneLine (src/cli-arguments.ts)
|
|
39
14
|
// reads .message only. Same arrangement as hookError
|
|
@@ -41,28 +16,10 @@ const RESOLUTION_KINDS = [
|
|
|
41
16
|
function prepareError(message, cause) {
|
|
42
17
|
return new SafetyError("prepare", message, { cause });
|
|
43
18
|
}
|
|
44
|
-
// `[ -
|
|
45
|
-
// Two call sites, each mirroring a distinct `-e` in the shell: the
|
|
46
|
-
// REQUIRED_UPSTREAM loop
|
|
47
|
-
// (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/common.sh:53-59::spw_require_upstream_path`
|
|
48
|
-
// — e.g. skills/ is a directory, not a regular file) and copyPathIfPresent's
|
|
49
|
-
// guard (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/common.sh:44-51::spw_copy_path_if_present`). The `.git`
|
|
50
|
-
// check below is NOT a third site: it needs `-d` (F1), not `-e`, and uses
|
|
51
|
-
// directoryExists instead.
|
|
52
|
-
async function pathExists(path) {
|
|
53
|
-
try {
|
|
54
|
-
await stat(path);
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// `[ -f ]` — regular file. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:42::missing`, :80, and :108 all use -f, and
|
|
19
|
+
// `[ -f ]` — regular file. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:108::[ -f "$additional_validator` uses -f, and
|
|
62
20
|
// tests/baseline/cli-parity.test.js's "CLI-ENV-MANIFEST-TEMPLATE-01 fallback
|
|
63
|
-
// template bytes and non-file rejection"
|
|
64
|
-
//
|
|
65
|
-
// stat-only predicate would accept it.
|
|
21
|
+
// template bytes and non-file rejection" also covers the separately extracted
|
|
22
|
+
// fallback check.
|
|
66
23
|
async function regularFileExists(path) {
|
|
67
24
|
try {
|
|
68
25
|
return (await stat(path)).isFile();
|
|
@@ -84,8 +41,8 @@ async function directoryExists(path) {
|
|
|
84
41
|
return false;
|
|
85
42
|
}
|
|
86
43
|
}
|
|
87
|
-
// mkdir
|
|
88
|
-
//
|
|
44
|
+
// mkdir throws raw ErrnoExceptions. Every command-owned call goes through here
|
|
45
|
+
// so the message on the stream is this module's, not Node's.
|
|
89
46
|
async function owned(message, fn) {
|
|
90
47
|
try {
|
|
91
48
|
return await fn();
|
|
@@ -94,50 +51,6 @@ async function owned(message, fn) {
|
|
|
94
51
|
throw prepareError(message, cause);
|
|
95
52
|
}
|
|
96
53
|
}
|
|
97
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/common.sh:44-51::spw_copy_path_if_present`.
|
|
98
|
-
// `cp -R` copies symlinks AS symlinks;
|
|
99
|
-
// fs.cp's default rewrites relative link targets against the destination,
|
|
100
|
-
// which would change what the adapter's containment checks see.
|
|
101
|
-
async function copyPathIfPresent(source, destination) {
|
|
102
|
-
if (!(await pathExists(source)))
|
|
103
|
-
return;
|
|
104
|
-
await owned(`cannot clear candidate path: ${destination}`, () => rm(destination, { recursive: true, force: true }));
|
|
105
|
-
await owned(`cannot copy upstream path into candidate: ${source}`, () => cp(source, destination, { recursive: true, verbatimSymlinks: true }));
|
|
106
|
-
}
|
|
107
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:17-24::case "$cache_parent`. Only cache_parent and plugin_root are resolved against
|
|
108
|
-
// the invocation cwd; manifest_template (:14) is not.
|
|
109
|
-
function resolveFromCwd(value, cwd) {
|
|
110
|
-
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
111
|
-
}
|
|
112
|
-
// readManifest (`src/hooks.ts:113::readManifest`) owns the read and the parse completely: byte
|
|
113
|
-
// read so invalid UTF-8 is rejected rather than replaced, cause dropped, three
|
|
114
|
-
// hand-written messages naming the path, object check included. Its
|
|
115
|
-
// diagnostics are pinned by
|
|
116
|
-
// `tests/unit/hooks.test.js:108::void test("readManifest diagnostics`. This wrapper adds only
|
|
117
|
-
// the `version` type check.
|
|
118
|
-
export async function readUpstreamManifestVersion(path) {
|
|
119
|
-
const manifest = await readManifest(path);
|
|
120
|
-
const value = manifest.version;
|
|
121
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/provenance.sh:56-62::for part in dotted_key.split`
|
|
122
|
-
// — absent key and JSON null both yield "".
|
|
123
|
-
if (value === undefined || value === null)
|
|
124
|
-
return "";
|
|
125
|
-
if (typeof value !== "string") {
|
|
126
|
-
// The shell stringified any other type through Python's print()
|
|
127
|
-
// (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/provenance.sh:62::print(value`), so `"version": 6` became "6" and flowed into both the
|
|
128
|
-
// provenance record and --upstream-manifest-version. Fail closed instead.
|
|
129
|
-
// Spec divergence 7.
|
|
130
|
-
throw prepareError(`upstream manifest version is not a string: ${path}`);
|
|
131
|
-
}
|
|
132
|
-
return value;
|
|
133
|
-
}
|
|
134
|
-
function asResolutionKind(value) {
|
|
135
|
-
for (const kind of RESOLUTION_KINDS) {
|
|
136
|
-
if (kind === value)
|
|
137
|
-
return kind;
|
|
138
|
-
}
|
|
139
|
-
throw prepareError(`unknown upstream resolution kind: ${value}`);
|
|
140
|
-
}
|
|
141
54
|
// Each stream's marker goes to that stream's own destination, so a reader of either
|
|
142
55
|
// sees its own truncation and neither is silently short.
|
|
143
56
|
function withTruncationMarker(captured, stream) {
|
|
@@ -167,16 +80,38 @@ function disclosureLine(resolution) {
|
|
|
167
80
|
return `[superpowers-manager: running external validator ${configured}${via} -> ${displayPath(resolution.resolved)}]\n`;
|
|
168
81
|
}
|
|
169
82
|
const NO_VALIDATOR_OUTPUT = { stdout: "", stderr: "" };
|
|
83
|
+
function validStagingLeaf(value) {
|
|
84
|
+
return (value.length > 0 &&
|
|
85
|
+
value !== "." &&
|
|
86
|
+
value !== ".." &&
|
|
87
|
+
!value.includes("/") &&
|
|
88
|
+
!value.includes("\\"));
|
|
89
|
+
}
|
|
170
90
|
async function gatherPrepare(ctx) {
|
|
171
91
|
const env = ctx.env;
|
|
172
92
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:16::invocation_root=` — captured before the two case statements below.
|
|
173
93
|
// getcwd(3) returns the physical path, so this matches `pwd -P` without a
|
|
174
94
|
// realpath call.
|
|
175
95
|
const cwd = process.cwd();
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
96
|
+
const configuredCache = env.SUPERPOWERS_CACHE_DIR || join(ctx.root, ".cache", "upstream");
|
|
97
|
+
const cacheParent = isAbsolute(configuredCache)
|
|
98
|
+
? configuredCache
|
|
99
|
+
: resolve(cwd, configuredCache);
|
|
100
|
+
const adapterContext = { root: ctx.root, env };
|
|
101
|
+
let location;
|
|
102
|
+
try {
|
|
103
|
+
location = ctx.adapter.preparationLocation(adapterContext);
|
|
104
|
+
}
|
|
105
|
+
catch (cause) {
|
|
106
|
+
throw prepareError("cannot determine preparation location", cause);
|
|
107
|
+
}
|
|
108
|
+
if (!isAbsolute(location.destinationRoot)) {
|
|
109
|
+
throw prepareError("adapter returned a non-absolute preparation destination");
|
|
110
|
+
}
|
|
111
|
+
if (!validStagingLeaf(location.stagingLeaf)) {
|
|
112
|
+
throw prepareError("adapter returned an invalid preparation staging leaf");
|
|
113
|
+
}
|
|
114
|
+
const pluginRoot = location.destinationRoot;
|
|
180
115
|
const additionalValidator = env.SUPERPOWERS_VALIDATOR || "";
|
|
181
116
|
const executableValidator = env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "";
|
|
182
117
|
const cache = join(cacheParent, "superpowers");
|
|
@@ -184,17 +119,30 @@ async function gatherPrepare(ctx) {
|
|
|
184
119
|
await owned(`cannot create directory: ${tmpParent}`, () => mkdir(tmpParent, { recursive: true }));
|
|
185
120
|
let cleanupWarning = null;
|
|
186
121
|
const outcome = await withWorkspace(tmpParent, ".superpowers.prepare.", async (workspace) => {
|
|
122
|
+
const outcomes = [];
|
|
187
123
|
const failed = (message) => ({
|
|
188
124
|
kind: "failed",
|
|
189
|
-
outcomes
|
|
125
|
+
outcomes,
|
|
190
126
|
validator: NO_VALIDATOR_OUTPUT,
|
|
191
127
|
message,
|
|
192
128
|
});
|
|
193
|
-
const candidate = join(workspace,
|
|
129
|
+
const candidate = join(workspace, location.stagingLeaf);
|
|
194
130
|
const selection = await computeEffectiveSelection(ctx.root, env);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
131
|
+
let prefetch;
|
|
132
|
+
try {
|
|
133
|
+
prefetch =
|
|
134
|
+
await ctx.adapter.validatePreparationBeforeFetch(adapterContext);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
138
|
+
.unexpected);
|
|
139
|
+
}
|
|
140
|
+
outcomes.push(prefetch.outcome);
|
|
141
|
+
if (!prefetch.outcome.ok)
|
|
142
|
+
return failed(null);
|
|
143
|
+
if (prefetch.status !== 0) {
|
|
144
|
+
return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
145
|
+
.invalidStatus);
|
|
198
146
|
}
|
|
199
147
|
await owned(`cannot create directory: ${cacheParent}`, () => mkdir(cacheParent, { recursive: true }));
|
|
200
148
|
if (selection.selectionMode === "pinned") {
|
|
@@ -237,86 +185,31 @@ async function gatherPrepare(ctx) {
|
|
|
237
185
|
if (checkedOut.status !== 0) {
|
|
238
186
|
return failed(`cannot check out upstream commit: ${selection.desiredCommit}`);
|
|
239
187
|
}
|
|
240
|
-
|
|
241
|
-
// spw_require_upstream_path is `[ -e ]`, not `[ -f ]`: skills/ is a
|
|
242
|
-
// directory.
|
|
243
|
-
if (!(await pathExists(join(cache, required.path)))) {
|
|
244
|
-
return failed(`required upstream path missing: ${required.label}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
await owned(`cannot clear candidate root: ${candidate}`, () => rm(candidate, { recursive: true, force: true }));
|
|
248
|
-
await owned(`cannot create candidate root: ${candidate}`, () => mkdir(join(candidate, ".codex-plugin"), { recursive: true }));
|
|
249
|
-
for (const name of COPY_PATHS) {
|
|
250
|
-
await copyPathIfPresent(join(cache, name), join(candidate, name));
|
|
251
|
-
}
|
|
252
|
-
const upstreamManifest = join(cache, ".codex-plugin", "plugin.json");
|
|
253
|
-
let upstreamManifestVersion = "";
|
|
254
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:80::if [ -f` — `[ -f ]`.
|
|
255
|
-
if (await regularFileExists(upstreamManifest)) {
|
|
256
|
-
upstreamManifestVersion =
|
|
257
|
-
await readUpstreamManifestVersion(upstreamManifest);
|
|
258
|
-
}
|
|
259
|
-
await writeProvenance(join(candidate, ".superpowers-upstream.json"), {
|
|
260
|
-
source: selection.effectiveSource,
|
|
261
|
-
requested_ref: selection.requestedRef,
|
|
262
|
-
resolved_ref: selection.resolvedRef,
|
|
263
|
-
commit: selection.desiredCommit,
|
|
264
|
-
upstream_manifest_version: upstreamManifestVersion,
|
|
265
|
-
});
|
|
266
|
-
const managerVersion = manifestVersionForRef({
|
|
267
|
-
requestedRef: selection.requestedRef,
|
|
268
|
-
resolutionKind: asResolutionKind(selection.resolutionKind),
|
|
269
|
-
resolvedRef: selection.resolvedRef,
|
|
270
|
-
commit: selection.desiredCommit,
|
|
271
|
-
});
|
|
272
|
-
let built;
|
|
188
|
+
let prepared;
|
|
273
189
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"--requested-ref",
|
|
281
|
-
selection.requestedRef,
|
|
282
|
-
"--resolved-ref",
|
|
283
|
-
selection.resolvedRef,
|
|
284
|
-
"--commit",
|
|
285
|
-
selection.desiredCommit,
|
|
286
|
-
"--manager-version",
|
|
287
|
-
managerVersion,
|
|
288
|
-
"--upstream-manifest-version",
|
|
289
|
-
upstreamManifestVersion,
|
|
290
|
-
"--fallback-manifest",
|
|
291
|
-
manifestTemplate,
|
|
292
|
-
], { root: ctx.root, env });
|
|
190
|
+
prepared = await ctx.adapter.prepareCandidate({
|
|
191
|
+
upstreamRoot: cache,
|
|
192
|
+
workspaceRoot: workspace,
|
|
193
|
+
candidateRoot: candidate,
|
|
194
|
+
selection,
|
|
195
|
+
}, adapterContext);
|
|
293
196
|
}
|
|
294
197
|
catch {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// non-AdapterFailure cause (runAdapter's closing `throw cause`,
|
|
298
|
-
// src/adapter.ts). That cause is by construction the one failure
|
|
299
|
-
// src/adapter.ts declined to own, so its text must never reach
|
|
300
|
-
// ctx.stderr. Caught here rather than in runPrepare's outer catch,
|
|
301
|
-
// the same treatment
|
|
302
|
-
// `src/commands/probe.ts:205-228::It does still THROW for a non-AdapterFailure cause`
|
|
303
|
-
// gives it.
|
|
304
|
-
return failed("cannot build the generated plugin candidate");
|
|
198
|
+
return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
199
|
+
.unexpected);
|
|
305
200
|
}
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
return
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
: null,
|
|
319
|
-
};
|
|
201
|
+
outcomes.push(prepared.outcome);
|
|
202
|
+
if (prepared.status !== 0 || !prepared.outcome.ok) {
|
|
203
|
+
return failed(prepared.outcome.ok
|
|
204
|
+
? ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
205
|
+
.invalidStatus
|
|
206
|
+
: null);
|
|
207
|
+
}
|
|
208
|
+
if (prepared.outcome.result.root !== candidate) {
|
|
209
|
+
return failed("adapter returned an unexpected preparation root");
|
|
210
|
+
}
|
|
211
|
+
if (prepared.outcome.result.commit !== selection.desiredCommit) {
|
|
212
|
+
return failed("adapter returned an unexpected preparation commit");
|
|
320
213
|
}
|
|
321
214
|
let validator = NO_VALIDATOR_OUTPUT;
|
|
322
215
|
if (additionalValidator.length > 0) {
|
|
@@ -443,7 +336,7 @@ export async function runPrepare(argv, ctx) {
|
|
|
443
336
|
// spawn error, which oneLine never reads -- it takes .message only);
|
|
444
337
|
// readManifest's three hookError messages
|
|
445
338
|
// (`src/hooks.ts:113-138::readManifest`), pinned by
|
|
446
|
-
// `tests/unit/hooks.test.
|
|
339
|
+
// `tests/unit/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
|
|
447
340
|
// errno; and SafetyErrors from gitSafeSource, writeProvenance, and
|
|
448
341
|
// withWorkspace.
|
|
449
342
|
//
|
|
@@ -454,7 +347,7 @@ export async function runPrepare(argv, ctx) {
|
|
|
454
347
|
// This is the DEFAULT invocation -- plain `prepare`, `track-latest`,
|
|
455
348
|
// and any non-40-hex SUPERPOWERS_REF -- not an exotic corner. Pinned
|
|
456
349
|
// by
|
|
457
|
-
// `tests/unit/upstream.test.
|
|
350
|
+
// `tests/unit/upstream.test.ts:453-462::void test("resolveRef reports a query failure for latest`,
|
|
458
351
|
// :471-481, and :483-501.
|
|
459
352
|
// 2. fetchExactCommit splices the same combined stdout+stderr into its
|
|
460
353
|
// own text on the PINNED path (both of its own splice sites in
|
|
@@ -519,7 +412,7 @@ export async function runPrepare(argv, ctx) {
|
|
|
519
412
|
// completed before cleanup ran, so it is not being reported as unverified
|
|
520
413
|
// -- but something did still go wrong, and AGENTS.md's fail-closed rule
|
|
521
414
|
// extends to it. Mirrors
|
|
522
|
-
// `src/commands/install.ts:
|
|
415
|
+
// `src/commands/install.ts:395-403::if (cleanupWarning`.
|
|
523
416
|
ctx.stderr.write(`error: ${cleanupWarning}\n`);
|
|
524
417
|
return 1;
|
|
525
418
|
}
|
package/dist/commands/probe.js
CHANGED
|
@@ -1,115 +1,7 @@
|
|
|
1
1
|
import { assertFailureWritable, writeAdapterFailure, } from "../adapter-result.js";
|
|
2
2
|
import { oneLine } from "../cli-arguments.js";
|
|
3
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
4
|
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
5
|
// Ports scripts/core/validate-adapter-response.py's replay (:235-238) and its
|
|
114
6
|
// error/hint block (:269-272). The shell ran that validator on EVERY adapter
|
|
115
7
|
// response, so adapter messages reached the operator on their declared streams
|
|
@@ -156,25 +48,13 @@ export function replayOutcome(outcome, ctx) {
|
|
|
156
48
|
// adapter module: src/commands/prepare.ts and this module are the two the
|
|
157
49
|
// injected double must observe, because install reaches the adapter through
|
|
158
50
|
// gatherProbe and runPrepare. Spec §4.5.
|
|
159
|
-
async function inspect(
|
|
51
|
+
async function inspect(call, unexpected, invalidStatus) {
|
|
160
52
|
let result;
|
|
161
53
|
try {
|
|
162
|
-
result = await
|
|
163
|
-
root: ctx.root,
|
|
164
|
-
env: ctx.env,
|
|
165
|
-
});
|
|
54
|
+
result = await call();
|
|
166
55
|
}
|
|
167
56
|
catch {
|
|
168
|
-
|
|
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
|
-
};
|
|
57
|
+
return { ok: false, result: null, message: unexpected };
|
|
178
58
|
}
|
|
179
59
|
const outcome = result.outcome;
|
|
180
60
|
if (result.status !== 0 || !outcome.ok) {
|
|
@@ -183,27 +63,11 @@ async function inspect(view, key, ctx) {
|
|
|
183
63
|
// rather than falling through to a replay that would print nothing.
|
|
184
64
|
return {
|
|
185
65
|
ok: false,
|
|
186
|
-
|
|
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}`,
|
|
66
|
+
result,
|
|
67
|
+
message: outcome.ok ? invalidStatus : null,
|
|
204
68
|
};
|
|
205
69
|
}
|
|
206
|
-
return { ok: true,
|
|
70
|
+
return { ok: true, result: { status: result.status, outcome } };
|
|
207
71
|
}
|
|
208
72
|
// Runs every step that can throw or fail closed, returning the outcome as
|
|
209
73
|
// data. It performs no writes of its own, so nothing inside runProbe's try can
|
|
@@ -223,50 +87,50 @@ async function inspect(view, key, ctx) {
|
|
|
223
87
|
export async function gatherProbe(ctx) {
|
|
224
88
|
// Order mirrors `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:24-40::spw_compute_effective_selection` exactly.
|
|
225
89
|
const selection = await computeEffectiveSelection(ctx.root, ctx.env);
|
|
226
|
-
const generatedCommit = await generatedCommitOrEmpty(ctx.root);
|
|
227
90
|
const outcomes = [];
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
91
|
+
const adapterContext = { root: ctx.root, env: ctx.env };
|
|
92
|
+
const collect = async (call, unexpected, invalidStatus) => {
|
|
93
|
+
const result = await inspect(call, unexpected, invalidStatus);
|
|
94
|
+
if (result.result !== null)
|
|
95
|
+
outcomes.push(result.result.outcome);
|
|
232
96
|
return result;
|
|
233
97
|
};
|
|
234
|
-
const
|
|
235
|
-
if (!
|
|
236
|
-
return { status: 1, outcomes, message:
|
|
98
|
+
const prepared = await collect(() => ctx.adapter.inspectPrepared(selection, adapterContext), "cannot inspect prepared harness state", "adapter reported a failure status for prepared harness inspection");
|
|
99
|
+
if (!prepared.ok) {
|
|
100
|
+
return { status: 1, outcomes, message: prepared.message };
|
|
101
|
+
}
|
|
102
|
+
const failure = (site) => ctx.adapter.presentation.callFailure(site, adapterContext);
|
|
103
|
+
const installedFailure = failure("probe-installed");
|
|
104
|
+
const installed = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus);
|
|
105
|
+
if (!installed.ok) {
|
|
106
|
+
return { status: 1, outcomes, message: installed.message };
|
|
237
107
|
}
|
|
238
|
-
const
|
|
108
|
+
const ownershipFailure = failure("probe-ownership");
|
|
109
|
+
const ownership = await collect(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure.unexpected, ownershipFailure.invalidStatus);
|
|
239
110
|
if (!ownership.ok) {
|
|
240
111
|
return { status: 1, outcomes, message: ownership.message };
|
|
241
112
|
}
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
113
|
+
const controlFailure = failure("probe-control");
|
|
114
|
+
const control = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus);
|
|
115
|
+
if (!control.ok) {
|
|
116
|
+
return { status: 1, outcomes, message: control.message };
|
|
245
117
|
}
|
|
246
|
-
const
|
|
118
|
+
const preparedState = prepared.result.outcome.result;
|
|
119
|
+
const installedState = installed.result.outcome.result;
|
|
247
120
|
return {
|
|
248
121
|
status: 0,
|
|
249
122
|
outcomes,
|
|
250
123
|
facts: {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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,
|
|
124
|
+
selection,
|
|
125
|
+
prepared: preparedState,
|
|
126
|
+
installed: installedState,
|
|
127
|
+
ownership: ownership.result.outcome.result,
|
|
128
|
+
control: control.result.outcome.result,
|
|
129
|
+
status: preparedState.kind !== "current"
|
|
130
|
+
? "needs prepare"
|
|
131
|
+
: installedState.kind !== "current"
|
|
132
|
+
? "needs install"
|
|
133
|
+
: "current",
|
|
270
134
|
},
|
|
271
135
|
};
|
|
272
136
|
}
|
|
@@ -319,7 +183,7 @@ export async function runProbe(argv, ctx) {
|
|
|
319
183
|
// every unpinned run does: resolveRef's COMMIT_INPUT_RE branch returns
|
|
320
184
|
// before any runGit call, so a 40-hex requested ref splices nothing.
|
|
321
185
|
// Pinned by
|
|
322
|
-
// `tests/unit/upstream.test.
|
|
186
|
+
// `tests/unit/upstream.test.ts:453::void test("resolveRef reports a query failure for latest`,
|
|
323
187
|
// :471, and :483.
|
|
324
188
|
// 2. The
|
|
325
189
|
// `src/selection-store.ts:120-124::cause.module === "selection") {`
|
|
@@ -387,6 +251,7 @@ export async function runProbe(argv, ctx) {
|
|
|
387
251
|
}
|
|
388
252
|
return 1;
|
|
389
253
|
}
|
|
390
|
-
ctx.
|
|
254
|
+
const rendered = ctx.adapter.presentation.renderProbe(outcome.facts);
|
|
255
|
+
ctx.stdout.write(porcelain ? rendered.porcelain : rendered.human);
|
|
391
256
|
return 0;
|
|
392
257
|
}
|