superpowers-manager 0.6.0 → 0.7.1
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 +42 -26
- package/dist/adapter-result.js +5 -9
- package/dist/atomic.js +4 -14
- package/dist/cli-arguments.js +1 -27
- package/dist/cli.js +25 -40
- package/dist/commands/adapter-call.js +28 -0
- package/dist/commands/install.js +25 -105
- package/dist/commands/pin.js +1 -1
- package/dist/commands/prepare.js +37 -81
- package/dist/commands/probe.js +35 -262
- package/dist/commands/track-latest.js +1 -1
- package/dist/commands/uninstall.js +15 -91
- package/dist/commands/update.js +5 -25
- package/dist/effective-selection.js +4 -6
- package/dist/git-tree.js +85 -0
- package/dist/harness-command-result.js +47 -0
- package/dist/harnesses/codex/adapter.js +12 -41
- package/dist/harnesses/codex/conflicts.js +2 -3
- package/dist/harnesses/codex/hooks.js +1 -1
- package/dist/harnesses/codex/json.js +1 -11
- package/dist/harnesses/codex/lifecycle.js +0 -48
- package/dist/harnesses/codex/manifest-overlay.js +1 -1
- package/dist/harnesses/codex/paths.js +14 -29
- package/dist/harnesses/codex/presentation.js +2 -23
- package/dist/harnesses/codex/publication.js +0 -6
- package/dist/harnesses/codex/recovery.js +5 -5
- package/dist/harnesses/codex/stored-state.js +1 -1
- package/dist/harnesses/opencode/config.js +256 -0
- package/dist/harnesses/opencode/discovery.js +617 -0
- package/dist/harnesses/opencode/harness.js +37 -0
- package/dist/harnesses/opencode/install.js +601 -0
- package/dist/harnesses/opencode/native.js +57 -0
- package/dist/harnesses/opencode/package.js +89 -0
- package/dist/harnesses/opencode/paths.js +38 -0
- package/dist/harnesses/opencode/prepare.js +30 -0
- package/dist/harnesses/opencode/presentation.js +28 -0
- package/dist/harnesses/opencode/state.js +174 -0
- package/dist/harnesses/pi/compatibility.js +5 -37
- package/dist/harnesses/pi/install.js +7 -5
- package/dist/harnesses/pi/native.js +3 -47
- package/dist/harnesses/pi/package.js +5 -107
- package/dist/harnesses/pi/prepare.js +13 -104
- package/dist/harnesses/pi/presentation.js +23 -66
- package/dist/harnesses/pi/shared-skills.js +28 -39
- package/dist/harnesses/pi/state.js +7 -27
- package/dist/provenance.js +2 -2
- package/dist/safe-path.js +2 -2
- package/dist/selection-store.js +2 -22
- package/dist/snapshot-package.js +78 -0
- package/dist/snapshot-prepare.js +99 -0
- package/dist/snapshot-presentation.js +44 -0
- package/dist/validator.js +4 -1
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +8 -4
- package/dist/domain/fingerprint.js +0 -4
- package/dist/status.js +0 -24
- package/dist/validate-generated-plugin-cli.js +0 -118
|
@@ -1,72 +1,21 @@
|
|
|
1
|
-
// Ports scripts/uninstall. The shell sourced common.sh, provenance.sh,
|
|
2
|
-
// lifecycle.sh and adapter.sh; the predicates now live in src/harnesses/codex/lifecycle.ts
|
|
3
|
-
// and the adapter arrives through ctx.adapter.
|
|
4
1
|
import { tmpdir } from "node:os";
|
|
5
2
|
import { oneLine } from "../cli-arguments.js";
|
|
6
3
|
import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
|
|
4
|
+
import { GatherFailure, callAdapter, } from "./adapter-call.js";
|
|
7
5
|
import { runWithMutation } from "./mutation.js";
|
|
8
6
|
import { replayOutcome } from "./probe.js";
|
|
9
7
|
async function invoke(call, failure, outcomes) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
result
|
|
13
|
-
|
|
14
|
-
catch {
|
|
15
|
-
return { ok: false, message: failure.unexpected };
|
|
16
|
-
}
|
|
17
|
-
outcomes.push(result.outcome);
|
|
18
|
-
const outcome = result.outcome;
|
|
19
|
-
if (result.status !== 0 || !outcome.ok) {
|
|
20
|
-
return {
|
|
21
|
-
ok: false,
|
|
22
|
-
message: outcome.ok ? failure.invalidStatus : null,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
ok: true,
|
|
27
|
-
result: { status: result.status, outcome },
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
// withWorkspace throws for mkdtemp failure before the callback ever runs
|
|
31
|
-
// ("cannot create workspace",
|
|
32
|
-
// `src/workspace.ts:120::throw new SafetyError("workspace", "cannot create workspace"`).
|
|
33
|
-
// A bare re-throw would silently drop every outcome collected before that
|
|
34
|
-
// point -- a narrow
|
|
35
|
-
// DIAG-ADAPTER-01 regression the shell never had, since it replayed each
|
|
36
|
-
// adapter response as it went rather than batching replay to the end. This
|
|
37
|
-
// carries the outcomes collected so far alongside the original cause, so
|
|
38
|
-
// runUninstall's catch can still replay them before reporting the cause.
|
|
39
|
-
//
|
|
40
|
-
// The post-success cleanup failure no longer reaches here: onCleanupFailure
|
|
41
|
-
// below suppresses withWorkspace's throw for that case and records the
|
|
42
|
-
// warning as data, so the computed UninstallOutcome survives it. The
|
|
43
|
-
// outcome-carrying is still load-bearing for mkdtemp, and the shape stays
|
|
44
|
-
// identical to src/commands/install.ts's GatherFailure.
|
|
45
|
-
class GatherFailure extends Error {
|
|
46
|
-
inner;
|
|
47
|
-
outcomes;
|
|
48
|
-
constructor(inner, outcomes) {
|
|
49
|
-
super("uninstall gather failed");
|
|
50
|
-
this.inner = inner;
|
|
51
|
-
this.outcomes = outcomes;
|
|
52
|
-
}
|
|
8
|
+
const result = await callAdapter(call, failure);
|
|
9
|
+
if (result.result !== null)
|
|
10
|
+
outcomes.push(result.result.outcome);
|
|
11
|
+
return result;
|
|
53
12
|
}
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// (`src/commands/probe.ts::readonly facts: ProbeSnapshot<R>;`) and for the same
|
|
57
|
-
// reason: a write inside this try could raise EPIPE, be caught here, and be
|
|
58
|
-
// relabelled as a domain failure.
|
|
13
|
+
// Collect outcomes without writing so output failures cannot be classified as
|
|
14
|
+
// inspection failures. Replay collected outcomes after gathering completes.
|
|
59
15
|
async function gatherUninstall(ctx) {
|
|
60
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:20-21::TMPDIR=` exported TMPDIR="$uninstall_workspace" so every
|
|
61
|
-
// child confined its temporary files to the tree the workspace trap
|
|
62
|
-
// removed. The AdapterContext passed to ctx.adapter below does the same.
|
|
63
16
|
const parent = ctx.env.TMPDIR ?? tmpdir();
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// withWorkspace call, not just the callback -- can still see whatever was
|
|
67
|
-
// collected before a workspace throw. Both `mkdtemp` failure (nothing
|
|
68
|
-
// collected yet) and a post-success cleanup failure (everything the
|
|
69
|
-
// callback collected) reach this same array.
|
|
17
|
+
// Kept outside the workspace callback so a workspace failure preserves every
|
|
18
|
+
// outcome collected before it.
|
|
70
19
|
const outcomes = [];
|
|
71
20
|
let cleanupWarning = null;
|
|
72
21
|
try {
|
|
@@ -90,9 +39,8 @@ async function gatherUninstall(ctx) {
|
|
|
90
39
|
const removed = await invoke(() => ctx.adapter.remove(removalInput, adapterContext), removeFailure, outcomes);
|
|
91
40
|
if (!removed.ok)
|
|
92
41
|
return failed(removed.message);
|
|
93
|
-
// Stage 3: inspect ownership
|
|
94
|
-
//
|
|
95
|
-
// POST-uninstall state, not the pre-uninstall one read above.
|
|
42
|
+
// Stage 3: inspect ownership again. Everything below reads this
|
|
43
|
+
// post-removal state, not the inspection before removal.
|
|
96
44
|
const afterFailure = ctx.adapter.presentation.callFailure("post-remove", adapterContext);
|
|
97
45
|
const after = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), afterFailure, outcomes);
|
|
98
46
|
if (!after.ok)
|
|
@@ -131,7 +79,7 @@ async function gatherUninstall(ctx) {
|
|
|
131
79
|
// cannot reach here. Wrapping with `outcomes` anyway keeps the class
|
|
132
80
|
// total over its declared contract rather than assuming the callback's
|
|
133
81
|
// purity at the throw site.
|
|
134
|
-
throw new GatherFailure(cause, outcomes);
|
|
82
|
+
throw new GatherFailure("uninstall gather failed", cause, outcomes);
|
|
135
83
|
}
|
|
136
84
|
}
|
|
137
85
|
export async function runUninstall(argv, ctx) {
|
|
@@ -147,30 +95,8 @@ async function performUninstall(argv, ctx) {
|
|
|
147
95
|
run = await gatherUninstall(ctx);
|
|
148
96
|
}
|
|
149
97
|
catch (cause) {
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// (`src/workspace.ts:120::throw new SafetyError("workspace", "cannot create workspace"`),
|
|
153
|
-
// alongside whatever outcomes were collected before that throw -- none,
|
|
154
|
-
// for that cause. Replaying first, before
|
|
155
|
-
// reporting the cause, keeps the arm honest for any outcome-bearing
|
|
156
|
-
// throw the class is declared to carry (DIAG-ADAPTER-01). The
|
|
157
|
-
// post-success cleanup failure no longer arrives here: gatherUninstall's
|
|
158
|
-
// onCleanupFailure records it as `cleanupWarning` and the computed
|
|
159
|
-
// outcome survives, which is what `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:34-35::complete` did. The
|
|
160
|
-
// `instanceof` guard is defensive rather than load-bearing --
|
|
161
|
-
// gatherUninstall's own catch is the only thing that can throw here, and
|
|
162
|
-
// it always wraps -- but this catch does not assume that invariant
|
|
163
|
-
// blindly.
|
|
164
|
-
//
|
|
165
|
-
// A cause outside ctx.adapter's AdapterFailure guard
|
|
166
|
-
// (`src/harnesses/codex/adapter.ts:829::async function runCodexOperation<T = JsonValue>(`) does NOT
|
|
167
|
-
// reach here: invoke() catches it inside gatherUninstall and converts it
|
|
168
|
-
// to a hand-written message carried as UninstallOutcome data, exactly as
|
|
169
|
-
// src/commands/probe.ts's inspect() does for the same cause.
|
|
170
|
-
//
|
|
171
|
-
// gatherUninstall performs no writes of its own, so this catch cannot
|
|
172
|
-
// also be reached by an EPIPE from uninstall's own output -- every write
|
|
173
|
-
// below runs only after this try/catch has resolved.
|
|
98
|
+
// Replay collected outcomes before reporting a gather failure. The
|
|
99
|
+
// cleanup reporter retains post-success cleanup failures as data.
|
|
174
100
|
const outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
|
|
175
101
|
for (const outcome of outcomes)
|
|
176
102
|
replayOutcome(outcome, ctx);
|
|
@@ -179,9 +105,7 @@ async function performUninstall(argv, ctx) {
|
|
|
179
105
|
return 1;
|
|
180
106
|
}
|
|
181
107
|
const { outcome, cleanupWarning } = run;
|
|
182
|
-
// Replay
|
|
183
|
-
// response's messages whether or not that response was a failure
|
|
184
|
-
// (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/validate-adapter-response.py:268::replay(messages)`).
|
|
108
|
+
// Replay before reporting a command-level result on either path.
|
|
185
109
|
for (const each of outcome.outcomes)
|
|
186
110
|
replayOutcome(each, ctx);
|
|
187
111
|
let status;
|
package/dist/commands/update.js
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// update issues no ctx.adapter call of its own: every adapter interaction
|
|
4
|
-
// this module needs already happens inside gatherProbe, runPrepare or
|
|
5
|
-
// runInstall, each of which owns its own §4.2a-conformant invoke() gate. That
|
|
6
|
-
// is also why update opens no temporary workspace of its own -- there is no
|
|
7
|
-
// mutation here for one to protect.
|
|
1
|
+
// Update delegates adapter calls to probe, prepare, and install. Those
|
|
2
|
+
// mutating commands own their workspaces, so update opens none itself.
|
|
8
3
|
import { oneLine } from "../cli-arguments.js";
|
|
4
|
+
import { runInstall, writeOutput } from "./install.js";
|
|
9
5
|
import { gatherProbe, replayOutcome } from "./probe.js";
|
|
10
|
-
import { runInstall } from "./install.js";
|
|
11
6
|
import { runPrepare } from "./prepare.js";
|
|
12
7
|
import { runWithMutation } from "./mutation.js";
|
|
13
|
-
function writeOutput(output, ctx) {
|
|
14
|
-
for (const line of output.stdout)
|
|
15
|
-
ctx.stdout.write(`${line}\n`);
|
|
16
|
-
for (const line of output.stderr)
|
|
17
|
-
ctx.stderr.write(`${line}\n`);
|
|
18
|
-
}
|
|
19
8
|
export async function runUpdate(argv, ctx) {
|
|
20
9
|
return await runWithMutation("update", ctx, async (scoped) => performUpdate(argv, scoped));
|
|
21
10
|
}
|
|
@@ -28,17 +17,8 @@ async function performUpdate(argv, ctx) {
|
|
|
28
17
|
probe = await gatherProbe(ctx);
|
|
29
18
|
}
|
|
30
19
|
catch (cause) {
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// output -- update writes nothing before this call resolves, unlike
|
|
34
|
-
// install's NOTE line.
|
|
35
|
-
//
|
|
36
|
-
// This is a THIRD consumer of gatherProbe's throw channel --
|
|
37
|
-
// `src/commands/probe.ts:469-527::THREE exceptions, all inherited and none a regression:`'s
|
|
38
|
-
// runProbe catch is the first and src/commands/install.ts's runInstall catch
|
|
39
|
-
// is the second. Because all three wrap the identical function, runProbe's
|
|
40
|
-
// long comment there enumerates exactly what can reach this stream too; not
|
|
41
|
-
// repeated here.
|
|
20
|
+
// Gathering does not write, so this catch cannot misclassify an output
|
|
21
|
+
// failure. oneLine() bounds any inherited diagnostic to one line.
|
|
42
22
|
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
43
23
|
return 1;
|
|
44
24
|
}
|
|
@@ -43,10 +43,9 @@ export async function loadSavedSelection(env) {
|
|
|
43
43
|
export const UPSTREAM_URL_DEFAULT = "https://github.com/obra/superpowers";
|
|
44
44
|
// Ports the env > saved > package-default precedence ladder from
|
|
45
45
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/selection.sh:88-162::spw_compute_effective_selection`.
|
|
46
|
-
// validateSource runs before any ref resolution
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// invoked.
|
|
46
|
+
// validateSource runs before any ref resolution. A credential-bearing source
|
|
47
|
+
// must fail before Git is invoked; the resolver-log check lives in
|
|
48
|
+
// `tests/baseline/selection-location.test.ts::SEL-PRECEDENCE-VALIDATE-01 invalid saved state stops resolution`.
|
|
50
49
|
export async function computeEffectiveSelection(root, env) {
|
|
51
50
|
const saved = await loadSavedSelection(env);
|
|
52
51
|
let upstreamSourceOrigin;
|
|
@@ -63,8 +62,7 @@ export async function computeEffectiveSelection(root, env) {
|
|
|
63
62
|
upstreamSourceOrigin = "package-default";
|
|
64
63
|
effectiveSource = UPSTREAM_URL_DEFAULT;
|
|
65
64
|
}
|
|
66
|
-
// Before any Git access
|
|
67
|
-
// "source validation precedes ref resolution".
|
|
65
|
+
// Before any Git access; the selection-location test above checks the log.
|
|
68
66
|
validateSource(effectiveSource);
|
|
69
67
|
let selectionOrigin;
|
|
70
68
|
let selectionMode;
|
package/dist/git-tree.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { ARTIFACT_RECEIPT } from "./artifact-tree.js";
|
|
6
|
+
import { COMMIT_RE } from "./domain/refs.js";
|
|
7
|
+
import { assertNoFollowType, assertSymlinkTargetContained, } from "./safe-path.js";
|
|
8
|
+
import { SafetyError } from "./safety-error.js";
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
11
|
+
async function gitBytes(repository, args) {
|
|
12
|
+
try {
|
|
13
|
+
return (await exec("git", ["--no-replace-objects", "-C", repository, ...args], {
|
|
14
|
+
encoding: null,
|
|
15
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
16
|
+
})).stdout;
|
|
17
|
+
}
|
|
18
|
+
catch (cause) {
|
|
19
|
+
throw new SafetyError("git-tree", `cannot read Git tree objects from repository: ${repository}`, { cause });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function materializeGitTree(repository, commit, destination) {
|
|
23
|
+
try {
|
|
24
|
+
if (!COMMIT_RE.test(commit))
|
|
25
|
+
throw new Error("commit");
|
|
26
|
+
if (!(await gitBytes(repository, ["cat-file", "-t", commit])).equals(Buffer.from("commit\n")))
|
|
27
|
+
throw new Error("not a commit");
|
|
28
|
+
await assertNoFollowType(destination, ["missing"]);
|
|
29
|
+
const bytes = await gitBytes(repository, [
|
|
30
|
+
"ls-tree",
|
|
31
|
+
"-rz",
|
|
32
|
+
"--full-tree",
|
|
33
|
+
commit,
|
|
34
|
+
]);
|
|
35
|
+
const entries = [];
|
|
36
|
+
let offset = 0;
|
|
37
|
+
while (offset < bytes.length) {
|
|
38
|
+
const end = bytes.indexOf(0, offset);
|
|
39
|
+
if (end < 0)
|
|
40
|
+
throw new Error("tree framing");
|
|
41
|
+
const record = DECODER.decode(bytes.subarray(offset, end));
|
|
42
|
+
offset = end + 1;
|
|
43
|
+
const match = /^(100644|100755|120000) blob ([a-f0-9]{40})\t([\s\S]+)$/.exec(record);
|
|
44
|
+
if (!match)
|
|
45
|
+
throw new Error("tree entry");
|
|
46
|
+
const path = match[3];
|
|
47
|
+
if (path
|
|
48
|
+
.split("/")
|
|
49
|
+
.some((part) => !part ||
|
|
50
|
+
part === "." ||
|
|
51
|
+
part === ".." ||
|
|
52
|
+
part.toLowerCase() === ".git") ||
|
|
53
|
+
isAbsolute(path) ||
|
|
54
|
+
path.includes("\\") ||
|
|
55
|
+
path.split("/")[0] === ARTIFACT_RECEIPT)
|
|
56
|
+
throw new Error("tree path");
|
|
57
|
+
entries.push({ mode: match[1], oid: match[2], path });
|
|
58
|
+
}
|
|
59
|
+
await mkdir(destination, { recursive: true });
|
|
60
|
+
for (const entry of entries)
|
|
61
|
+
await mkdir(dirname(join(destination, entry.path)), { recursive: true });
|
|
62
|
+
for (const entry of entries.filter((entry) => entry.mode !== "120000")) {
|
|
63
|
+
const path = join(destination, entry.path), mode = entry.mode === "100755" ? 0o755 : 0o644;
|
|
64
|
+
await writeFile(path, await gitBytes(repository, ["cat-file", "blob", entry.oid]), { flag: "wx", mode });
|
|
65
|
+
await chmod(path, mode);
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries.filter((entry) => entry.mode === "120000")) {
|
|
68
|
+
const target = DECODER.decode(await gitBytes(repository, ["cat-file", "blob", entry.oid]));
|
|
69
|
+
const path = join(destination, entry.path), suffix = relative(resolve(destination), resolve(dirname(path), target));
|
|
70
|
+
if (!target ||
|
|
71
|
+
target.includes("\0") ||
|
|
72
|
+
isAbsolute(target) ||
|
|
73
|
+
suffix === ".." ||
|
|
74
|
+
suffix.startsWith("../") ||
|
|
75
|
+
isAbsolute(suffix))
|
|
76
|
+
throw new Error("link escape");
|
|
77
|
+
await symlink(target, path);
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries.filter((entry) => entry.mode === "120000"))
|
|
80
|
+
await assertSymlinkTargetContained(destination, join(destination, entry.path));
|
|
81
|
+
}
|
|
82
|
+
catch (cause) {
|
|
83
|
+
throw new SafetyError("git-tree", `cannot materialize Git tree from ${repository} into ${destination}`, { cause });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { AdapterMessageLog, failureResult, successResult, } from "./adapter-result.js";
|
|
2
|
+
import { SEMVER_RE } from "./domain/refs.js";
|
|
3
|
+
function appendCaptured(log, label, channel, captured) {
|
|
4
|
+
log.appendBytes(channel, Buffer.from(captured.text));
|
|
5
|
+
if (captured.droppedBytes > 0)
|
|
6
|
+
log.appendText(channel, `${label} ${channel} omitted ${captured.droppedBytes} bytes at the capture limit`);
|
|
7
|
+
}
|
|
8
|
+
export function nativeCommandResult(label, result) {
|
|
9
|
+
const operation = `${label.toLowerCase()}-command`;
|
|
10
|
+
if (result.kind === "launchFailed") {
|
|
11
|
+
const errno = /^[A-Z][A-Z0-9_]*$/u.test(result.errno)
|
|
12
|
+
? result.errno
|
|
13
|
+
: "UNKNOWN";
|
|
14
|
+
return failureResult(operation, "launch-failed", `cannot launch configured ${label} executable (${errno})`, [], []);
|
|
15
|
+
}
|
|
16
|
+
const log = new AdapterMessageLog();
|
|
17
|
+
if (result.kind === "timedOut") {
|
|
18
|
+
appendCaptured(log, label, "stdout", result.stdout);
|
|
19
|
+
appendCaptured(log, label, "stderr", result.stderr);
|
|
20
|
+
return failureResult(operation, "timeout", `${label} command timed out after ${result.afterMs} ms`, [], log.snapshot());
|
|
21
|
+
}
|
|
22
|
+
if (result.code !== 0) {
|
|
23
|
+
appendCaptured(log, label, "stdout", result.stdout);
|
|
24
|
+
appendCaptured(log, label, "stderr", result.stderr);
|
|
25
|
+
return failureResult(operation, "nonzero-exit", result.code === null
|
|
26
|
+
? `${label} command exited without a status`
|
|
27
|
+
: `${label} command exited with status ${result.code}`, [], log.snapshot());
|
|
28
|
+
}
|
|
29
|
+
if (result.stdout.droppedBytes > 0 || result.stderr.droppedBytes > 0) {
|
|
30
|
+
appendCaptured(log, label, "stdout", result.stdout);
|
|
31
|
+
appendCaptured(log, label, "stderr", result.stderr);
|
|
32
|
+
return failureResult(operation, "output-limit", `${label} command output exceeded the capture limit`, [], log.snapshot());
|
|
33
|
+
}
|
|
34
|
+
appendCaptured(log, label, "stderr", result.stderr);
|
|
35
|
+
return successResult(operation, { stdout: result.stdout.text }, log.snapshot());
|
|
36
|
+
}
|
|
37
|
+
export function normalizeSnapshotRuntimeVersion(label, result) {
|
|
38
|
+
if (!result.outcome.ok)
|
|
39
|
+
return { status: result.status, outcome: result.outcome };
|
|
40
|
+
const operation = `${label.toLowerCase()}-runtime`;
|
|
41
|
+
if (result.status !== 0)
|
|
42
|
+
return failureResult(operation, "invalid-status", `${label} runtime inspection returned a nonzero status`, [], result.outcome.messages);
|
|
43
|
+
const version = result.outcome.result.stdout.trim();
|
|
44
|
+
if (!SEMVER_RE.test(version))
|
|
45
|
+
return failureResult(operation, "invalid-version", `${label} runtime inspection returned an invalid version response`, [], result.outcome.messages);
|
|
46
|
+
return successResult(operation, version, result.outcome.messages);
|
|
47
|
+
}
|
|
@@ -14,8 +14,7 @@ import { validateGeneratedPlugin } from "./generated-plugin.js";
|
|
|
14
14
|
import { classifyHooks, materializeHooks, readManifest, } from "./hooks.js";
|
|
15
15
|
import { applyManifestOverlay } from "./manifest-overlay.js";
|
|
16
16
|
import { readCodexBuildSource } from "../../provenance.js";
|
|
17
|
-
import {
|
|
18
|
-
import { codexControlInspection, codexOwnershipInspection, } from "./lifecycle.js";
|
|
17
|
+
import { codexOwnershipInspection } from "./lifecycle.js";
|
|
19
18
|
import { codexInstallReceipt } from "./presentation.js";
|
|
20
19
|
const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
|
|
21
20
|
const MARKETPLACE_NAME = "superpowers-manager";
|
|
@@ -282,39 +281,6 @@ async function runBuild(input, env, log) {
|
|
|
282
281
|
catch {
|
|
283
282
|
fail("invalid-provenance", "candidate provenance is missing or invalid");
|
|
284
283
|
}
|
|
285
|
-
// The seven values the validator CLI would receive in split form:
|
|
286
|
-
// --plugin-root, --requested-ref, --resolved-ref, --commit,
|
|
287
|
-
// --manifest-version, --manifest-source, --upstream-manifest-version.
|
|
288
|
-
// The eighth, --source, is passed attached, where argparse accepts any
|
|
289
|
-
// dash-leading value, so it is deliberately absent here. Each value is
|
|
290
|
-
// paired with the ADAPTER-facing flag name to report: --manager-version
|
|
291
|
-
// (the CLI calls it --manifest-version) and --plugin-root /
|
|
292
|
-
// --manifest-source (derived, not user-supplied) deliberately differ
|
|
293
|
-
// from the validator CLI's own names, since the operator can only act
|
|
294
|
-
// on the adapter's surface.
|
|
295
|
-
const splitValues = [
|
|
296
|
-
{ value: candidateRoot, name: "--plugin-root" },
|
|
297
|
-
{ value: input.requestedRef, name: "--requested-ref" },
|
|
298
|
-
{ value: input.resolvedRef, name: "--resolved-ref" },
|
|
299
|
-
{ value: input.commit, name: "--commit" },
|
|
300
|
-
{ value: input.managerVersion, name: "--manager-version" },
|
|
301
|
-
{ value: manifestSource, name: "--manifest-source" },
|
|
302
|
-
{
|
|
303
|
-
value: input.upstreamManifestVersion,
|
|
304
|
-
name: "--upstream-manifest-version",
|
|
305
|
-
},
|
|
306
|
-
];
|
|
307
|
-
const firstRejected = splitValues.find(({ value }) => !isAcceptedSplitValue(value));
|
|
308
|
-
if (firstRejected !== undefined) {
|
|
309
|
-
// Declared exception to message-record parity: argparse wrote usage
|
|
310
|
-
// records here; this guard precedes the call and writes a
|
|
311
|
-
// differently-worded record naming the rejected flag instead. The
|
|
312
|
-
// failure code and message are unchanged.
|
|
313
|
-
const text = "Generated plugin validation failed:\n" +
|
|
314
|
-
`- validator argument \`${firstRejected.name}\` has a dash-leading value the argument parser rejects\n`;
|
|
315
|
-
log.appendBytes("stderr", Buffer.from(text, "utf8"));
|
|
316
|
-
fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
|
|
317
|
-
}
|
|
318
284
|
let errors;
|
|
319
285
|
try {
|
|
320
286
|
errors = await validateGeneratedPlugin({
|
|
@@ -447,16 +413,17 @@ async function runOwnership(context, env, log) {
|
|
|
447
413
|
const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
|
|
448
414
|
if (commandFailed(plugins)) {
|
|
449
415
|
const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
|
|
450
|
-
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.
|
|
416
|
+
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
|
|
451
417
|
return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
|
|
452
418
|
}
|
|
453
419
|
let managerPlugin;
|
|
454
420
|
let legacyPlugin;
|
|
455
421
|
let conflicts;
|
|
456
422
|
try {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
423
|
+
const installed = codexInstalledPluginsFromJson(plugins.stdout);
|
|
424
|
+
managerPlugin = installed.some((plugin) => plugin.pluginId === PLUGIN_ID);
|
|
425
|
+
legacyPlugin = installed.some((plugin) => plugin.pluginId === LEGACY_PLUGIN_ID);
|
|
426
|
+
conflicts = await inspectCodexConflicts({ root: context.root, env }, installed);
|
|
460
427
|
}
|
|
461
428
|
catch {
|
|
462
429
|
fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
|
|
@@ -464,7 +431,7 @@ async function runOwnership(context, env, log) {
|
|
|
464
431
|
const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
|
|
465
432
|
if (commandFailed(marketplaces)) {
|
|
466
433
|
const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
|
|
467
|
-
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.
|
|
434
|
+
const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
|
|
468
435
|
return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
|
|
469
436
|
}
|
|
470
437
|
let managerMarketplace;
|
|
@@ -505,7 +472,11 @@ export function codexInspectOwnership(context) {
|
|
|
505
472
|
return runCodexOperation("inspect", context, (env, log) => runOwnership(context, env, log));
|
|
506
473
|
}
|
|
507
474
|
export function codexInspectControl(context) {
|
|
508
|
-
return runCodexOperation("inspect", context, async () =>
|
|
475
|
+
return runCodexOperation("inspect", context, async () => ({
|
|
476
|
+
probeEligibility: { kind: "allowed" },
|
|
477
|
+
mutationEligibility: { kind: "allowed" },
|
|
478
|
+
presentationValue: "managed",
|
|
479
|
+
}));
|
|
509
480
|
}
|
|
510
481
|
export function codexReadNativeState(context) {
|
|
511
482
|
return runCodexOperation("native-state", context, async (env, log) => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { codexInstalledPluginsFromJson, } from "./json.js";
|
|
4
3
|
export const CODEX_MANAGER_PLUGIN_ID = "superpowers@superpowers-manager";
|
|
5
4
|
export const CODEX_LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
|
|
6
5
|
const SUPERPOWERS_PLUGIN_PREFIX = "superpowers@";
|
|
@@ -85,9 +84,9 @@ async function nativeRouteConflict(ctx) {
|
|
|
85
84
|
}
|
|
86
85
|
return NATIVE_ROUTE_CONFLICT;
|
|
87
86
|
}
|
|
88
|
-
export async function inspectCodexConflicts(ctx,
|
|
87
|
+
export async function inspectCodexConflicts(ctx, installedPlugins) {
|
|
89
88
|
const conflicts = new Set();
|
|
90
|
-
for (const plugin of
|
|
89
|
+
for (const plugin of installedPlugins) {
|
|
91
90
|
const conflict = unmanagedPluginConflict(plugin);
|
|
92
91
|
if (conflict.length > 0)
|
|
93
92
|
conflicts.add(conflict);
|
|
@@ -5,7 +5,7 @@ import { SafetyError } from "../../safety-error.js";
|
|
|
5
5
|
import { parseStrictJson } from "../../strict-json.js";
|
|
6
6
|
// Annotated, not `as const`, matching
|
|
7
7
|
// `src/provenance.ts:20::const PROVENANCE_STRICT_PROFILE` and
|
|
8
|
-
// `src/selection-store.ts:
|
|
8
|
+
// `src/selection-store.ts:15::const SELECTION_JSON_PROFILE`. The annotation
|
|
9
9
|
// rejects a misspelled or unsupported profile field here rather than at the
|
|
10
10
|
// parseStrictJson call.
|
|
11
11
|
const MANIFEST_PROFILE = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SafetyError } from "../../safety-error.js";
|
|
2
|
+
import { hasTerminalControl } from "../../adapter-result.js";
|
|
2
3
|
import { parseStrictJson, } from "../../strict-json.js";
|
|
3
4
|
const ACCEPT_CONSTANTS = {
|
|
4
5
|
duplicateKeys: "last-wins",
|
|
@@ -45,17 +46,6 @@ function checkedItems(raw, profile, arrayKey, field) {
|
|
|
45
46
|
return candidate;
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
|
-
function hasTerminalControl(value) {
|
|
49
|
-
for (const character of value) {
|
|
50
|
-
const code = character.codePointAt(0);
|
|
51
|
-
if (code < 0x20 ||
|
|
52
|
-
(code >= 0x7f && code <= 0x9f) ||
|
|
53
|
-
(code >= 0xd800 && code <= 0xdfff)) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
49
|
export function installedListingHas(raw, arrayKey, field, value) {
|
|
60
50
|
return checkedItems(raw, ACCEPT_CONSTANTS, arrayKey, field).some((item) => item[field] === value);
|
|
61
51
|
}
|
|
@@ -52,24 +52,6 @@ export function reportLegacyState(identityState) {
|
|
|
52
52
|
}
|
|
53
53
|
return unknownState(identityState);
|
|
54
54
|
}
|
|
55
|
-
// Ported from
|
|
56
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:62-70::spw_require_managed_update_control`.
|
|
57
|
-
// Both refusals reached spw_die in the shell, so both carry the `error: ` prefix
|
|
58
|
-
// at the call site and neither is special.
|
|
59
|
-
export function requireManagedUpdateControl(value) {
|
|
60
|
-
if (value === "managed")
|
|
61
|
-
return { ok: true };
|
|
62
|
-
if (value === "unsupported") {
|
|
63
|
-
return {
|
|
64
|
-
ok: false,
|
|
65
|
-
message: "adapter cannot guarantee manager-controlled updates",
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
ok: false,
|
|
70
|
-
message: `unknown adapter update-control capability: ${value}`,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
55
|
function installDecision(legacy) {
|
|
74
56
|
switch (legacy.kind) {
|
|
75
57
|
case "ok":
|
|
@@ -159,33 +141,3 @@ export function codexOwnershipInspection(identityState, removalInput, conflicts)
|
|
|
159
141
|
presentationConflicts: conflicts,
|
|
160
142
|
};
|
|
161
143
|
}
|
|
162
|
-
export function codexControlInspection(updateControl) {
|
|
163
|
-
const probeEligibility = updateControl.length === 0
|
|
164
|
-
? {
|
|
165
|
-
kind: "blocked",
|
|
166
|
-
output: {
|
|
167
|
-
stdout: [],
|
|
168
|
-
stderr: [
|
|
169
|
-
"error: probe did not report adapter update-control capability",
|
|
170
|
-
],
|
|
171
|
-
},
|
|
172
|
-
}
|
|
173
|
-
: { kind: "allowed" };
|
|
174
|
-
const managed = requireManagedUpdateControl(updateControl);
|
|
175
|
-
const mutationEligibility = probeEligibility.kind === "blocked"
|
|
176
|
-
? probeEligibility
|
|
177
|
-
: managed.ok
|
|
178
|
-
? { kind: "allowed" }
|
|
179
|
-
: {
|
|
180
|
-
kind: "blocked",
|
|
181
|
-
output: {
|
|
182
|
-
stdout: [],
|
|
183
|
-
stderr: [`error: ${managed.message}`],
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
return {
|
|
187
|
-
probeEligibility,
|
|
188
|
-
mutationEligibility,
|
|
189
|
-
presentationValue: updateControl,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
@@ -74,7 +74,7 @@ function setMember(entries, key, value) {
|
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Map strict-json's diagnostics onto the CPython wording the callers assert.
|
|
77
|
-
* Repository policy at `AGENTS.md:
|
|
77
|
+
* Repository policy at `AGENTS.md:89::most diagnostics are asserted as`
|
|
78
78
|
* freezes most of these diagnostics as complete-string contracts.
|
|
79
79
|
*/
|
|
80
80
|
function translate(cause, source, path) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { assertNoFollowType, canonicalizeProspectivePath, } from "../../safe-path.js";
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { assertNoFollowType, canonicalizeProspectivePath, isContained, } from "../../safe-path.js";
|
|
3
3
|
import { SafetyError } from "../../safety-error.js";
|
|
4
4
|
export function codexHome(env, cwd) {
|
|
5
5
|
const configured = env.CODEX_HOME;
|
|
@@ -26,15 +26,18 @@ export function codexPaths(env, cwd) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
function overlaps(a, b) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
return isContained(a, b) || isContained(b, a);
|
|
30
|
+
}
|
|
31
|
+
class CodexPathError extends SafetyError {
|
|
32
|
+
constructor(message, details, cause) {
|
|
33
|
+
super("codex-paths", message, {
|
|
34
|
+
details,
|
|
35
|
+
...(cause === undefined ? {} : { cause }),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
35
38
|
}
|
|
36
39
|
function separationError() {
|
|
37
|
-
return new
|
|
40
|
+
return new CodexPathError("preparation overlaps Codex published or recovery storage", { kind: "overlap" });
|
|
38
41
|
}
|
|
39
42
|
async function inspectRoot(path, root) {
|
|
40
43
|
try {
|
|
@@ -42,29 +45,11 @@ async function inspectRoot(path, root) {
|
|
|
42
45
|
return await canonicalizeProspectivePath(path);
|
|
43
46
|
}
|
|
44
47
|
catch (cause) {
|
|
45
|
-
throw new
|
|
48
|
+
throw new CodexPathError(`cannot inspect ${root} root: ${path}`, { kind: "inspection", root, path }, cause);
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
export function codexPathFailureDetails(cause) {
|
|
49
|
-
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
const details = cause.details;
|
|
53
|
-
if (details === null || typeof details !== "object" || !("kind" in details)) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
if (details.kind === "overlap")
|
|
57
|
-
return { kind: "overlap" };
|
|
58
|
-
if (details.kind !== "inspection" ||
|
|
59
|
-
!("root" in details) ||
|
|
60
|
-
!("path" in details) ||
|
|
61
|
-
(details.root !== "preparation" &&
|
|
62
|
-
details.root !== "marketplace" &&
|
|
63
|
-
details.root !== "recovery") ||
|
|
64
|
-
typeof details.path !== "string") {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
return { kind: "inspection", root: details.root, path: details.path };
|
|
52
|
+
return cause instanceof CodexPathError ? (cause.details ?? null) : null;
|
|
68
53
|
}
|
|
69
54
|
export async function assertCodexPreparationSeparate(preparedRoot, paths) {
|
|
70
55
|
const recovery = await inspectRoot(paths.recoveryRoot, "recovery");
|