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
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { access, copyFile, mkdir, readFile, realpath, stat, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
7
|
+
import { AdapterMessageLog, failureResult, successResult, } from "./adapter-result.js";
|
|
8
|
+
import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./codex-json.js";
|
|
9
|
+
import { oneLine } from "./cli-arguments.js";
|
|
10
|
+
import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./codex-state.js";
|
|
11
|
+
import { validateGeneratedPlugin } from "./generated-plugin.js";
|
|
12
|
+
import { classifyHooks, materializeHooks, readManifest, } from "./hooks.js";
|
|
13
|
+
import { applyManifestOverlay } from "./manifest-overlay.js";
|
|
14
|
+
import { readCodexBuildSource } from "./provenance.js";
|
|
15
|
+
import { isAcceptedSplitValue } from "./validate-generated-plugin-cli.js";
|
|
16
|
+
import { withWorkspace, workspaceRemovalFailure } from "./workspace.js";
|
|
17
|
+
const PLUGIN_ID = "superpowers@superpowers-manager";
|
|
18
|
+
const MARKETPLACE_NAME = "superpowers-manager";
|
|
19
|
+
const LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
|
|
20
|
+
const LEGACY_MARKETPLACE_NAME = "superpowers-wrapper";
|
|
21
|
+
class AdapterFailure extends Error {
|
|
22
|
+
code;
|
|
23
|
+
hints;
|
|
24
|
+
constructor(code, message, hints = []) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.hints = hints;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function fail(code, message, hints = []) {
|
|
31
|
+
throw new AdapterFailure(code, message, hints);
|
|
32
|
+
}
|
|
33
|
+
function parseFlags(argv, allowed) {
|
|
34
|
+
const values = {};
|
|
35
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
36
|
+
const flag = argv[index];
|
|
37
|
+
if (!allowed.includes(flag)) {
|
|
38
|
+
fail("invalid-arguments", `unknown flag: ${flag}`);
|
|
39
|
+
}
|
|
40
|
+
if (index + 1 >= argv.length) {
|
|
41
|
+
fail("invalid-arguments", `missing value for ${flag}`);
|
|
42
|
+
}
|
|
43
|
+
if (Object.hasOwn(values, flag)) {
|
|
44
|
+
fail("invalid-arguments", `duplicate flag: ${flag}`);
|
|
45
|
+
}
|
|
46
|
+
values[flag] = argv[index + 1];
|
|
47
|
+
}
|
|
48
|
+
for (const flag of allowed) {
|
|
49
|
+
if (!Object.hasOwn(values, flag)) {
|
|
50
|
+
fail("invalid-arguments", `missing required flag: ${flag}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
function runCommand(file, args, env) {
|
|
56
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/common.sh:71::NODE_OPTIONS`
|
|
57
|
+
// is the system's only scrubbing site and it dies with scripts/ in 4c.
|
|
58
|
+
// Without this, NODE_OPTIONS would NEWLY reach codex.
|
|
59
|
+
// Preserves the property that was load-bearing — the child is clean — and
|
|
60
|
+
// drops the part that was never true, that the dispatcher scrubbed itself.
|
|
61
|
+
// Carried matrix row 11.
|
|
62
|
+
const childEnv = { ...env };
|
|
63
|
+
delete childEnv.NODE_OPTIONS;
|
|
64
|
+
delete childEnv.NODE_PATH;
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
execFile(file, [...args], {
|
|
67
|
+
encoding: "buffer",
|
|
68
|
+
env: childEnv,
|
|
69
|
+
maxBuffer: Infinity,
|
|
70
|
+
shell: false,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
}, (error, stdout, stderr) => {
|
|
73
|
+
if (error === null) {
|
|
74
|
+
resolve({ status: 0, signal: null, stdout, stderr });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const failure = error;
|
|
78
|
+
if (typeof failure.code === "string") {
|
|
79
|
+
reject(failure);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
resolve({
|
|
83
|
+
status: typeof failure.code === "number" ? failure.code : null,
|
|
84
|
+
signal: failure.signal ?? null,
|
|
85
|
+
stdout,
|
|
86
|
+
stderr,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Exported only so tests/unit/adapter.test.js can assert the env scrub
|
|
92
|
+
// directly. No production caller uses this name.
|
|
93
|
+
export { runCommand as runCommandForTest };
|
|
94
|
+
function commandFailed(result) {
|
|
95
|
+
return result.status !== 0;
|
|
96
|
+
}
|
|
97
|
+
// Exported only so its unit test can reach it. No integration test can cover
|
|
98
|
+
// it: on Linux glibc, `execvp`'s ENOEXEC falls back to `/bin/sh`, so the spawn
|
|
99
|
+
// still succeeds, and every other candidate errno is already peeled off by an
|
|
100
|
+
// `X_OK` check or the `ENOENT`/`EACCES` branch. See the test's own comment
|
|
101
|
+
// for the full analysis.
|
|
102
|
+
export function mapCodexLaunchFailure(cause, codexBin) {
|
|
103
|
+
const code = cause !== null && typeof cause === "object" && "code" in cause
|
|
104
|
+
? String(cause.code)
|
|
105
|
+
: "";
|
|
106
|
+
if (code === "ENOENT" || code === "EACCES") {
|
|
107
|
+
fail("command-not-found", `required Codex command not found: ${codexBin}`);
|
|
108
|
+
}
|
|
109
|
+
// A bounded, enumerable, path-free errno is not the cause's message, so the no-interpolation rule
|
|
110
|
+
// does not reach it; unvalidated, String(cause.code) would put another module's free-form text
|
|
111
|
+
// in a sentence this module signs. appendBytes below escapes it, but escaping is not ownership:
|
|
112
|
+
// the rule governs whose wording the operator reads, not whether it reaches the terminal intact.
|
|
113
|
+
const detail = /^E[A-Z0-9]+$/.test(code) ? `: ${code}` : "";
|
|
114
|
+
return {
|
|
115
|
+
status: 1,
|
|
116
|
+
signal: null,
|
|
117
|
+
stdout: Buffer.alloc(0),
|
|
118
|
+
// Trailing newline matches how a real process writes stderr;
|
|
119
|
+
// `src/adapter-result.ts:133::appendBytes` splits on newlines and terminates the
|
|
120
|
+
// final chunk at end-of-buffer either way.
|
|
121
|
+
stderr: Buffer.from(`cannot launch Codex command ${codexBin}${detail}\n`, "utf8"),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function runCodexCommand(codexBin, args, env) {
|
|
125
|
+
try {
|
|
126
|
+
return await runCommand(codexBin, args, env);
|
|
127
|
+
}
|
|
128
|
+
catch (cause) {
|
|
129
|
+
return mapCodexLaunchFailure(cause, codexBin);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function reportOrphanedWorkspace(log) {
|
|
133
|
+
return (path) => {
|
|
134
|
+
log.appendText("stderr", workspaceRemovalFailure(path));
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function mutationCommand(log, codexBin, args, env) {
|
|
138
|
+
const result = await runCodexCommand(codexBin, args, env);
|
|
139
|
+
log.appendBytes("stderr", result.stdout);
|
|
140
|
+
log.appendBytes("stderr", result.stderr);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
async function listingCommand(log, codexBin, args, env) {
|
|
144
|
+
const result = await runCodexCommand(codexBin, args, env);
|
|
145
|
+
log.appendBytes("stderr", result.stderr);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
async function executable(path) {
|
|
149
|
+
try {
|
|
150
|
+
await access(path, constants.X_OK);
|
|
151
|
+
return (await stat(path)).isFile();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function commandAvailable(command, env) {
|
|
158
|
+
if (command.includes("/"))
|
|
159
|
+
return executable(command);
|
|
160
|
+
if (env.PATH === undefined)
|
|
161
|
+
return false;
|
|
162
|
+
for (const directory of env.PATH.split(delimiter)) {
|
|
163
|
+
if (await executable(join(directory, command))) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
async function requireCodex(codexBin, env) {
|
|
170
|
+
if (!(await commandAvailable(codexBin, env))) {
|
|
171
|
+
fail("command-not-found", `required Codex command not found: ${codexBin}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const BUILD_FLAGS = [
|
|
175
|
+
"--upstream-root",
|
|
176
|
+
"--candidate-root",
|
|
177
|
+
"--requested-ref",
|
|
178
|
+
"--resolved-ref",
|
|
179
|
+
"--commit",
|
|
180
|
+
"--manager-version",
|
|
181
|
+
"--upstream-manifest-version",
|
|
182
|
+
"--fallback-manifest",
|
|
183
|
+
];
|
|
184
|
+
async function directoryExists(path) {
|
|
185
|
+
try {
|
|
186
|
+
return (await stat(path)).isDirectory();
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function fileExists(path) {
|
|
193
|
+
try {
|
|
194
|
+
return (await stat(path)).isFile();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function runBuild(argv, root, env, log) {
|
|
201
|
+
const flags = parseFlags(argv, BUILD_FLAGS);
|
|
202
|
+
const upstreamRoot = flags["--upstream-root"];
|
|
203
|
+
const candidateRoot = flags["--candidate-root"];
|
|
204
|
+
const fallbackManifest = flags["--fallback-manifest"];
|
|
205
|
+
if (!isAbsolute(upstreamRoot)) {
|
|
206
|
+
fail("invalid-arguments", "--upstream-root must be an absolute path");
|
|
207
|
+
}
|
|
208
|
+
if (!isAbsolute(candidateRoot)) {
|
|
209
|
+
fail("invalid-arguments", "--candidate-root must be an absolute path");
|
|
210
|
+
}
|
|
211
|
+
if (!(await directoryExists(upstreamRoot))) {
|
|
212
|
+
fail("invalid-arguments", `upstream root not found: ${upstreamRoot}`);
|
|
213
|
+
}
|
|
214
|
+
if (!(await directoryExists(candidateRoot))) {
|
|
215
|
+
fail("invalid-arguments", `candidate root not found: ${candidateRoot}`);
|
|
216
|
+
}
|
|
217
|
+
if (!(await fileExists(fallbackManifest))) {
|
|
218
|
+
fail("invalid-arguments", `fallback manifest not found: ${fallbackManifest}`);
|
|
219
|
+
}
|
|
220
|
+
let entered = false;
|
|
221
|
+
try {
|
|
222
|
+
return await withWorkspace(tmpdir(), "superpowers-manager.adapter-build.", async () => {
|
|
223
|
+
entered = true;
|
|
224
|
+
const candidateManifest = join(candidateRoot, ".codex-plugin/plugin.json");
|
|
225
|
+
const upstreamManifest = join(upstreamRoot, ".codex-plugin/plugin.json");
|
|
226
|
+
const manifestSource = (await fileExists(upstreamManifest))
|
|
227
|
+
? "upstream"
|
|
228
|
+
: "fallback";
|
|
229
|
+
try {
|
|
230
|
+
await mkdir(join(candidateRoot, ".codex-plugin"), {
|
|
231
|
+
recursive: true,
|
|
232
|
+
});
|
|
233
|
+
await copyFile(manifestSource === "upstream" ? upstreamManifest : fallbackManifest, candidateManifest);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
fail("build-failed", manifestSource === "upstream"
|
|
237
|
+
? "cannot copy upstream manifest into candidate"
|
|
238
|
+
: "cannot copy fallback manifest into candidate");
|
|
239
|
+
}
|
|
240
|
+
let plan;
|
|
241
|
+
let sourceRoot;
|
|
242
|
+
let realCandidateRoot;
|
|
243
|
+
try {
|
|
244
|
+
sourceRoot = await realpath(upstreamRoot);
|
|
245
|
+
realCandidateRoot = await realpath(candidateRoot);
|
|
246
|
+
const manifest = await readManifest(candidateManifest);
|
|
247
|
+
plan = await classifyHooks(manifest, manifestSource, sourceRoot);
|
|
248
|
+
}
|
|
249
|
+
catch (cause) {
|
|
250
|
+
log.appendText("stderr", `hook classification failed: ${oneLine(cause)}`);
|
|
251
|
+
fail("build-failed", "failed to prepare upstream Codex hooks");
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
await materializeHooks(plan, sourceRoot, realCandidateRoot);
|
|
255
|
+
}
|
|
256
|
+
catch (cause) {
|
|
257
|
+
log.appendText("stderr", `hook materialization failed: ${oneLine(cause)}`);
|
|
258
|
+
fail("build-failed", "failed to prepare upstream Codex hooks");
|
|
259
|
+
}
|
|
260
|
+
let source;
|
|
261
|
+
try {
|
|
262
|
+
// Decoded fatally, not leniently: the file can change between this read and `readManifest`'s read above.
|
|
263
|
+
const rawManifestBytes = await readFile(candidateManifest);
|
|
264
|
+
source = new TextDecoder("utf-8", {
|
|
265
|
+
fatal: true,
|
|
266
|
+
ignoreBOM: true,
|
|
267
|
+
}).decode(rawManifestBytes);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Deliberately drops the cause. The Python interpolated the raw OSError here:
|
|
271
|
+
// `git show 0b6d50e1e9c688397285c6fa274dc8c9437d8ba3:scripts/adapters/codex/apply-manifest-overlay.py:42::read`,
|
|
272
|
+
// putting "[Errno 2] No such file or directory" on the operator's stream.
|
|
273
|
+
// The prefix is preserved; the errno leak is not.
|
|
274
|
+
log.appendText("stderr", `cannot read manifest JSON in ${candidateManifest}`);
|
|
275
|
+
fail("build-failed", "failed to apply manager manifest overlay");
|
|
276
|
+
}
|
|
277
|
+
let overlaid;
|
|
278
|
+
try {
|
|
279
|
+
overlaid = applyManifestOverlay(source, flags["--manager-version"], candidateManifest);
|
|
280
|
+
}
|
|
281
|
+
catch (cause) {
|
|
282
|
+
// applyManifestOverlay's own messages already name the manifest
|
|
283
|
+
// path — three are the frozen CPython wording, and the fourth (the
|
|
284
|
+
// numeric-overflow diagnostic, which has no CPython oracle wording
|
|
285
|
+
// to match) now carries the path via its own rewrap in
|
|
286
|
+
// src/manifest-overlay.ts. Emit as-is, with no added prefix — a
|
|
287
|
+
// prefix here would double up the path these messages already
|
|
288
|
+
// name.
|
|
289
|
+
log.appendText("stderr", oneLine(cause));
|
|
290
|
+
fail("build-failed", "failed to apply manager manifest overlay");
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
await writeFile(candidateManifest, overlaid, "utf8");
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
log.appendText("stderr", `cannot write manifest JSON in ${candidateManifest}`);
|
|
297
|
+
fail("build-failed", "failed to apply manager manifest overlay");
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
await copyFile(fallbackManifest, join(candidateRoot, ".codex-plugin/plugin.template.json"));
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
fail("build-failed", "cannot copy fallback manifest template into candidate");
|
|
304
|
+
}
|
|
305
|
+
let upstreamSource;
|
|
306
|
+
try {
|
|
307
|
+
upstreamSource = await readCodexBuildSource(join(candidateRoot, ".superpowers-upstream.json"));
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
fail("invalid-provenance", "candidate provenance is missing or invalid");
|
|
311
|
+
}
|
|
312
|
+
// The seven values the validator CLI would receive in split form:
|
|
313
|
+
// --plugin-root, --requested-ref, --resolved-ref, --commit,
|
|
314
|
+
// --manifest-version, --manifest-source, --upstream-manifest-version.
|
|
315
|
+
// The eighth, --source, is passed attached, where argparse accepts any
|
|
316
|
+
// dash-leading value, so it is deliberately absent here. Each value is
|
|
317
|
+
// paired with the ADAPTER-facing flag name to report: --manager-version
|
|
318
|
+
// (the CLI calls it --manifest-version) and --plugin-root /
|
|
319
|
+
// --manifest-source (derived, not user-supplied) deliberately differ
|
|
320
|
+
// from the validator CLI's own names, since the operator can only act
|
|
321
|
+
// on the adapter's surface.
|
|
322
|
+
const splitValues = [
|
|
323
|
+
{ value: candidateRoot, name: "--plugin-root" },
|
|
324
|
+
{ value: flags["--requested-ref"], name: "--requested-ref" },
|
|
325
|
+
{ value: flags["--resolved-ref"], name: "--resolved-ref" },
|
|
326
|
+
{ value: flags["--commit"], name: "--commit" },
|
|
327
|
+
{ value: flags["--manager-version"], name: "--manager-version" },
|
|
328
|
+
{ value: manifestSource, name: "--manifest-source" },
|
|
329
|
+
{
|
|
330
|
+
value: flags["--upstream-manifest-version"],
|
|
331
|
+
name: "--upstream-manifest-version",
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
const firstRejected = splitValues.find(({ value }) => !isAcceptedSplitValue(value));
|
|
335
|
+
if (firstRejected !== undefined) {
|
|
336
|
+
// Declared exception to message-record parity: argparse wrote usage
|
|
337
|
+
// records here; this guard precedes the call and writes a
|
|
338
|
+
// differently-worded record naming the rejected flag instead. The
|
|
339
|
+
// failure code and message are unchanged.
|
|
340
|
+
const text = "Generated plugin validation failed:\n" +
|
|
341
|
+
`- validator argument \`${firstRejected.name}\` has a dash-leading value the argument parser rejects\n`;
|
|
342
|
+
log.appendBytes("stderr", Buffer.from(text, "utf8"));
|
|
343
|
+
fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
|
|
344
|
+
}
|
|
345
|
+
let errors;
|
|
346
|
+
try {
|
|
347
|
+
errors = await validateGeneratedPlugin({
|
|
348
|
+
pluginRoot: candidateRoot,
|
|
349
|
+
source: upstreamSource,
|
|
350
|
+
requestedRef: flags["--requested-ref"],
|
|
351
|
+
resolvedRef: flags["--resolved-ref"],
|
|
352
|
+
commit: flags["--commit"],
|
|
353
|
+
manifestVersion: flags["--manager-version"],
|
|
354
|
+
manifestSource,
|
|
355
|
+
upstreamManifestVersion: flags["--upstream-manifest-version"],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
|
|
360
|
+
}
|
|
361
|
+
if (errors.length > 0) {
|
|
362
|
+
// appendBytes, not appendText: one record per line, matching what
|
|
363
|
+
// mutationCommand writes from the subprocess streams.
|
|
364
|
+
const text = "Generated plugin validation failed:\n" +
|
|
365
|
+
errors.map((error) => `- ${error}\n`).join("");
|
|
366
|
+
log.appendBytes("stderr", Buffer.from(text, "utf8"));
|
|
367
|
+
fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
|
|
368
|
+
}
|
|
369
|
+
log.appendBytes("stdout", Buffer.from(`generated plugin validation passed: ${candidateRoot}\n`, "utf8"));
|
|
370
|
+
return {};
|
|
371
|
+
}, { onCleanupFailure: reportOrphanedWorkspace(log) });
|
|
372
|
+
}
|
|
373
|
+
catch (cause) {
|
|
374
|
+
if (!entered) {
|
|
375
|
+
fail("build-failed", `cannot create adapter build workspace under ${tmpdir()}`);
|
|
376
|
+
}
|
|
377
|
+
throw cause;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function runInstall(argv, env, log) {
|
|
381
|
+
const flags = parseFlags(argv, ["--package-root"]);
|
|
382
|
+
const packageRoot = flags["--package-root"];
|
|
383
|
+
if (!isAbsolute(packageRoot)) {
|
|
384
|
+
fail("invalid-arguments", "--package-root must be an absolute path");
|
|
385
|
+
}
|
|
386
|
+
if (!(await directoryExists(packageRoot))) {
|
|
387
|
+
fail("invalid-arguments", `package root not found: ${packageRoot}`);
|
|
388
|
+
}
|
|
389
|
+
const codexBin = env.SUPERPOWERS_CODEX || "codex";
|
|
390
|
+
const refreshMode = env.SUPERPOWERS_INSTALL_REFRESH_MODE || "add-only";
|
|
391
|
+
await requireCodex(codexBin, env);
|
|
392
|
+
if (refreshMode !== "add-only" && refreshMode !== "remove-add") {
|
|
393
|
+
fail("invalid-arguments", `unsupported SUPERPOWERS_INSTALL_REFRESH_MODE: ${refreshMode}`);
|
|
394
|
+
}
|
|
395
|
+
let entered = false;
|
|
396
|
+
try {
|
|
397
|
+
return await withWorkspace(tmpdir(), "superpowers-manager.adapter-install.", async () => {
|
|
398
|
+
entered = true;
|
|
399
|
+
const marketplaceList = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
|
|
400
|
+
if (commandFailed(marketplaceList)) {
|
|
401
|
+
fail("install-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
|
|
402
|
+
}
|
|
403
|
+
let registeredRoot;
|
|
404
|
+
try {
|
|
405
|
+
registeredRoot = marketplaceRootFromJson(marketplaceList.stdout, MARKETPLACE_NAME);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
fail("install-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
|
|
409
|
+
}
|
|
410
|
+
if (registeredRoot.length === 0) {
|
|
411
|
+
const added = await mutationCommand(log, codexBin, ["plugin", "marketplace", "add", packageRoot], env);
|
|
412
|
+
if (commandFailed(added)) {
|
|
413
|
+
fail("install-failed", `codex marketplace add failed for ${packageRoot}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else if (!(await pathsEqual(packageRoot, registeredRoot))) {
|
|
417
|
+
log.appendText("stdout", `marketplace ${MARKETPLACE_NAME} registered at ${registeredRoot}; re-registering at ${packageRoot}`);
|
|
418
|
+
const removed = await mutationCommand(log, codexBin, ["plugin", "marketplace", "remove", MARKETPLACE_NAME], env);
|
|
419
|
+
if (commandFailed(removed)) {
|
|
420
|
+
fail("install-failed", `codex marketplace remove failed for ${MARKETPLACE_NAME} (registered at ${registeredRoot})`);
|
|
421
|
+
}
|
|
422
|
+
const added = await mutationCommand(log, codexBin, ["plugin", "marketplace", "add", packageRoot], env);
|
|
423
|
+
if (commandFailed(added)) {
|
|
424
|
+
fail("install-failed", `marketplace ${MARKETPLACE_NAME} was removed but re-adding failed.`, [
|
|
425
|
+
`recover with: ${codexBin} plugin marketplace add ${packageRoot}`,
|
|
426
|
+
`previous root (last known good): ${registeredRoot}`,
|
|
427
|
+
]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (refreshMode === "remove-add") {
|
|
431
|
+
await mutationCommand(log, codexBin, ["plugin", "remove", PLUGIN_ID], env);
|
|
432
|
+
}
|
|
433
|
+
const pluginAdded = await mutationCommand(log, codexBin, ["plugin", "add", PLUGIN_ID], env);
|
|
434
|
+
if (commandFailed(pluginAdded)) {
|
|
435
|
+
fail("install-failed", `codex plugin add failed for ${PLUGIN_ID}`);
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
verification_hints: {
|
|
439
|
+
...(refreshMode === "add-only"
|
|
440
|
+
? {
|
|
441
|
+
mismatch: "retry with SUPERPOWERS_INSTALL_REFRESH_MODE=remove-add",
|
|
442
|
+
}
|
|
443
|
+
: {}),
|
|
444
|
+
missing: "verify with 'codex plugin list --json'.",
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}, { onCleanupFailure: reportOrphanedWorkspace(log) });
|
|
448
|
+
}
|
|
449
|
+
catch (cause) {
|
|
450
|
+
if (!entered) {
|
|
451
|
+
fail("install-failed", `cannot create adapter install workspace under ${tmpdir()}`);
|
|
452
|
+
}
|
|
453
|
+
throw cause;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function runUninstall(argv, env, log) {
|
|
457
|
+
const flags = parseFlags(argv, ["--plugin-present", "--marketplace-present"]);
|
|
458
|
+
const pluginPresent = flags["--plugin-present"];
|
|
459
|
+
const marketplacePresent = flags["--marketplace-present"];
|
|
460
|
+
if (pluginPresent !== "true" && pluginPresent !== "false") {
|
|
461
|
+
fail("invalid-arguments", "--plugin-present must be true or false");
|
|
462
|
+
}
|
|
463
|
+
if (marketplacePresent !== "true" && marketplacePresent !== "false") {
|
|
464
|
+
fail("invalid-arguments", "--marketplace-present must be true or false");
|
|
465
|
+
}
|
|
466
|
+
const codexBin = env.SUPERPOWERS_CODEX || "codex";
|
|
467
|
+
await requireCodex(codexBin, env);
|
|
468
|
+
let entered = false;
|
|
469
|
+
try {
|
|
470
|
+
return await withWorkspace(tmpdir(), "superpowers-manager.adapter-uninstall.", async () => {
|
|
471
|
+
entered = true;
|
|
472
|
+
if (pluginPresent === "true") {
|
|
473
|
+
const result = await mutationCommand(log, codexBin, ["plugin", "remove", PLUGIN_ID], env);
|
|
474
|
+
if (commandFailed(result)) {
|
|
475
|
+
fail("uninstall-failed", `codex plugin remove failed for ${PLUGIN_ID}`);
|
|
476
|
+
}
|
|
477
|
+
log.appendText("stdout", `removed plugin ${PLUGIN_ID}`);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
log.appendText("stdout", "plugin not installed; skipping");
|
|
481
|
+
}
|
|
482
|
+
if (marketplacePresent === "true") {
|
|
483
|
+
const result = await mutationCommand(log, codexBin, ["plugin", "marketplace", "remove", MARKETPLACE_NAME], env);
|
|
484
|
+
if (commandFailed(result)) {
|
|
485
|
+
fail("uninstall-failed", `codex plugin marketplace remove failed for ${MARKETPLACE_NAME}`);
|
|
486
|
+
}
|
|
487
|
+
log.appendText("stdout", `removed marketplace ${MARKETPLACE_NAME}`);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
log.appendText("stdout", "marketplace not registered; skipping");
|
|
491
|
+
}
|
|
492
|
+
return {};
|
|
493
|
+
}, { onCleanupFailure: reportOrphanedWorkspace(log) });
|
|
494
|
+
}
|
|
495
|
+
catch (cause) {
|
|
496
|
+
if (!entered) {
|
|
497
|
+
fail("uninstall-failed", `cannot create adapter uninstall workspace under ${tmpdir()}`);
|
|
498
|
+
}
|
|
499
|
+
throw cause;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function runInspect(argv, env, log) {
|
|
503
|
+
const flags = parseFlags(argv, ["--view"]);
|
|
504
|
+
const view = flags["--view"];
|
|
505
|
+
if (view === "update-control") {
|
|
506
|
+
return { view: "update-control", update_control: "managed" };
|
|
507
|
+
}
|
|
508
|
+
if (view === "fingerprint") {
|
|
509
|
+
const codexBin = env.SUPERPOWERS_CODEX || "codex";
|
|
510
|
+
await requireCodex(codexBin, env);
|
|
511
|
+
let entered = false;
|
|
512
|
+
try {
|
|
513
|
+
return await withWorkspace(tmpdir(), "superpowers-manager.adapter-fingerprint.", async () => {
|
|
514
|
+
entered = true;
|
|
515
|
+
const listing = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
|
|
516
|
+
if (commandFailed(listing)) {
|
|
517
|
+
fail("inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
|
|
518
|
+
}
|
|
519
|
+
let activeVersion;
|
|
520
|
+
try {
|
|
521
|
+
activeVersion = activePluginVersionFromJson(listing.stdout, PLUGIN_ID);
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
|
|
525
|
+
}
|
|
526
|
+
if (activeVersion.length === 0) {
|
|
527
|
+
return { view: "fingerprint", fingerprint: null };
|
|
528
|
+
}
|
|
529
|
+
let searchRoot = env.SUPERPOWERS_INSTALLED_SEARCH_ROOT;
|
|
530
|
+
if (!searchRoot) {
|
|
531
|
+
if (env.HOME === undefined) {
|
|
532
|
+
fail("inspect-failed", "cannot inspect active Codex plugin fingerprint without HOME");
|
|
533
|
+
}
|
|
534
|
+
searchRoot = join(env.HOME || "/", ".codex");
|
|
535
|
+
}
|
|
536
|
+
const activeRoot = installedRootForVersion(searchRoot, MARKETPLACE_NAME, "superpowers", activeVersion);
|
|
537
|
+
const fingerprint = await installedCommitFromRoot(activeRoot);
|
|
538
|
+
if (fingerprint.length === 0) {
|
|
539
|
+
fail("inspect-failed", `cannot inspect active Codex plugin fingerprint under ${activeRoot}`);
|
|
540
|
+
}
|
|
541
|
+
return { view: "fingerprint", fingerprint };
|
|
542
|
+
}, { onCleanupFailure: reportOrphanedWorkspace(log) });
|
|
543
|
+
}
|
|
544
|
+
catch (cause) {
|
|
545
|
+
if (!entered) {
|
|
546
|
+
fail("inspect-failed", `cannot create adapter fingerprint workspace under ${tmpdir()}`);
|
|
547
|
+
}
|
|
548
|
+
throw cause;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (view === "ownership") {
|
|
552
|
+
const codexBin = env.SUPERPOWERS_CODEX || "codex";
|
|
553
|
+
await requireCodex(codexBin, env);
|
|
554
|
+
let entered = false;
|
|
555
|
+
try {
|
|
556
|
+
return await withWorkspace(tmpdir(), "superpowers-manager.adapter-inspect.", async () => {
|
|
557
|
+
entered = true;
|
|
558
|
+
const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
|
|
559
|
+
if (commandFailed(plugins)) {
|
|
560
|
+
fail("inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
|
|
561
|
+
}
|
|
562
|
+
const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
|
|
563
|
+
if (commandFailed(marketplaces)) {
|
|
564
|
+
fail("inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
|
|
565
|
+
}
|
|
566
|
+
let managerPlugin;
|
|
567
|
+
let legacyPlugin;
|
|
568
|
+
let managerMarketplace;
|
|
569
|
+
let legacyMarketplace;
|
|
570
|
+
try {
|
|
571
|
+
managerPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", PLUGIN_ID);
|
|
572
|
+
legacyPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", LEGACY_PLUGIN_ID);
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
managerMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", MARKETPLACE_NAME);
|
|
579
|
+
legacyMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", LEGACY_MARKETPLACE_NAME);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
fail("inspect-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
|
|
583
|
+
}
|
|
584
|
+
const managerPresent = managerPlugin || managerMarketplace;
|
|
585
|
+
const legacyPresent = legacyPlugin || legacyMarketplace;
|
|
586
|
+
return {
|
|
587
|
+
view: "ownership",
|
|
588
|
+
resources: {
|
|
589
|
+
plugin: managerPlugin,
|
|
590
|
+
marketplace: managerMarketplace,
|
|
591
|
+
},
|
|
592
|
+
legacy_resources: {
|
|
593
|
+
plugin: legacyPlugin,
|
|
594
|
+
marketplace: legacyMarketplace,
|
|
595
|
+
},
|
|
596
|
+
identity_state: managerPresent
|
|
597
|
+
? legacyPresent
|
|
598
|
+
? "both"
|
|
599
|
+
: "manager"
|
|
600
|
+
: legacyPresent
|
|
601
|
+
? "legacy"
|
|
602
|
+
: "neither",
|
|
603
|
+
};
|
|
604
|
+
}, { onCleanupFailure: reportOrphanedWorkspace(log) });
|
|
605
|
+
}
|
|
606
|
+
catch (cause) {
|
|
607
|
+
if (!entered) {
|
|
608
|
+
fail("inspect-failed", `cannot create adapter inspect workspace under ${tmpdir()}`);
|
|
609
|
+
}
|
|
610
|
+
throw cause;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
fail("invalid-arguments", `unsupported inspect view: ${view}`);
|
|
614
|
+
}
|
|
615
|
+
export async function runAdapter(argv, context) {
|
|
616
|
+
const rawOperation = argv[0];
|
|
617
|
+
const operation = rawOperation || "adapter";
|
|
618
|
+
const args = argv.slice(1);
|
|
619
|
+
const env = { ...process.env, ...context.env };
|
|
620
|
+
const log = new AdapterMessageLog();
|
|
621
|
+
try {
|
|
622
|
+
let result;
|
|
623
|
+
if (rawOperation === undefined || rawOperation.length === 0) {
|
|
624
|
+
fail("invalid-arguments", "missing adapter operation");
|
|
625
|
+
}
|
|
626
|
+
else if (operation === "build") {
|
|
627
|
+
result = await runBuild(args, context.root, env, log);
|
|
628
|
+
}
|
|
629
|
+
else if (operation === "install") {
|
|
630
|
+
result = await runInstall(args, env, log);
|
|
631
|
+
}
|
|
632
|
+
else if (operation === "uninstall") {
|
|
633
|
+
result = await runUninstall(args, env, log);
|
|
634
|
+
}
|
|
635
|
+
else if (operation === "inspect") {
|
|
636
|
+
result = await runInspect(args, env, log);
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
fail("unsupported-operation", `unsupported adapter operation: ${operation}`);
|
|
640
|
+
}
|
|
641
|
+
return successResult(operation, result, log.snapshot());
|
|
642
|
+
}
|
|
643
|
+
catch (cause) {
|
|
644
|
+
if (cause instanceof AdapterFailure) {
|
|
645
|
+
return failureResult(operation, cause.code, cause.message, cause.hints, log.snapshot());
|
|
646
|
+
}
|
|
647
|
+
throw cause;
|
|
648
|
+
}
|
|
649
|
+
}
|