superpowers-manager 0.5.1 → 0.6.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.
Files changed (51) hide show
  1. package/README.md +13 -11
  2. package/dist/cli.js +9 -14
  3. package/dist/commands/install.js +18 -9
  4. package/dist/commands/mutation.js +7 -0
  5. package/dist/commands/prepare.js +5 -51
  6. package/dist/commands/probe.js +8 -10
  7. package/dist/commands/uninstall.js +2 -2
  8. package/dist/effective-selection.js +3 -3
  9. package/dist/harnesses/codex/adapter.js +336 -457
  10. package/dist/harnesses/codex/compatibility.js +2 -17
  11. package/dist/harnesses/codex/generated-plugin.js +1 -1
  12. package/dist/harnesses/codex/harness.js +45 -272
  13. package/dist/harnesses/codex/hooks.js +1 -1
  14. package/dist/harnesses/codex/json.js +6 -2
  15. package/dist/harnesses/codex/lifecycle.js +110 -54
  16. package/dist/harnesses/codex/manifest-overlay.js +22 -17
  17. package/dist/harnesses/codex/marketplace.js +148 -0
  18. package/dist/harnesses/codex/paths.js +97 -0
  19. package/dist/harnesses/codex/prepare.js +34 -2
  20. package/dist/harnesses/codex/presentation.js +10 -17
  21. package/dist/harnesses/codex/publication.js +520 -0
  22. package/dist/harnesses/codex/recovery.js +338 -0
  23. package/dist/harnesses/codex/state.js +131 -0
  24. package/dist/harnesses/codex/stored-state.js +249 -0
  25. package/dist/provenance.js +3 -3
  26. package/dist/python-json-format.js +0 -69
  27. package/dist/python-json.js +2 -27
  28. package/dist/python-text.js +9 -69
  29. package/dist/validator.js +35 -72
  30. package/node_modules/smol-toml/LICENSE +24 -0
  31. package/node_modules/smol-toml/README.md +418 -0
  32. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  33. package/node_modules/smol-toml/dist/date.js +127 -0
  34. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  35. package/node_modules/smol-toml/dist/error.js +63 -0
  36. package/node_modules/smol-toml/dist/extract.js +69 -0
  37. package/node_modules/smol-toml/dist/index.cjs +734 -0
  38. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  39. package/node_modules/smol-toml/dist/index.js +33 -0
  40. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  41. package/node_modules/smol-toml/dist/parse.js +149 -0
  42. package/node_modules/smol-toml/dist/primitive.js +238 -0
  43. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  44. package/node_modules/smol-toml/dist/stringify.js +181 -0
  45. package/node_modules/smol-toml/dist/struct.js +179 -0
  46. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  47. package/node_modules/smol-toml/dist/util.js +89 -0
  48. package/node_modules/smol-toml/package.json +68 -0
  49. package/package.json +10 -2
  50. package/dist/selection-state-cli.js +0 -85
  51. package/dist/upstream-cli.js +0 -110
@@ -2,23 +2,31 @@ import { Buffer } from "node:buffer";
2
2
  import { execFile } from "node:child_process";
3
3
  import { constants } from "node:fs";
4
4
  import { access, copyFile, mkdir, readFile, realpath, stat, writeFile, } from "node:fs/promises";
5
- import { tmpdir } from "node:os";
6
5
  import { delimiter, isAbsolute, join } from "node:path";
7
- import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
8
- import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
6
+ import { AdapterMessageLog, failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
7
+ import { activePluginVersionFromJson, codexInstalledPluginsFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
9
8
  import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./conflicts.js";
10
9
  import { oneLine } from "../../cli-arguments.js";
11
- import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./state.js";
10
+ import { installedRootForVersion, pathsEqual } from "./state.js";
11
+ import { codexHome } from "./paths.js";
12
+ import { readCodexStoredState } from "./stored-state.js";
12
13
  import { validateGeneratedPlugin } from "./generated-plugin.js";
13
14
  import { classifyHooks, materializeHooks, readManifest, } from "./hooks.js";
14
15
  import { applyManifestOverlay } from "./manifest-overlay.js";
15
16
  import { readCodexBuildSource } from "../../provenance.js";
16
17
  import { isAcceptedSplitValue } from "../../validate-generated-plugin-cli.js";
17
- import { withWorkspace, workspaceRemovalFailure } from "../../workspace.js";
18
+ import { codexControlInspection, codexOwnershipInspection, } from "./lifecycle.js";
19
+ import { codexInstallReceipt } from "./presentation.js";
18
20
  const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
19
21
  const MARKETPLACE_NAME = "superpowers-manager";
20
22
  const LEGACY_PLUGIN_ID = CODEX_LEGACY_PLUGIN_ID;
21
23
  const LEGACY_MARKETPLACE_NAME = "superpowers-wrapper";
24
+ export function codexInstallRefreshMode(env) {
25
+ const refreshMode = env.SUPERPOWERS_INSTALL_REFRESH_MODE || "add-only";
26
+ return refreshMode === "add-only" || refreshMode === "remove-add"
27
+ ? refreshMode
28
+ : null;
29
+ }
22
30
  class AdapterFailure extends Error {
23
31
  code;
24
32
  hints;
@@ -31,28 +39,6 @@ class AdapterFailure extends Error {
31
39
  function fail(code, message, hints = []) {
32
40
  throw new AdapterFailure(code, message, hints);
33
41
  }
34
- function parseFlags(argv, allowed) {
35
- const values = {};
36
- for (let index = 0; index < argv.length; index += 2) {
37
- const flag = argv[index];
38
- if (!allowed.includes(flag)) {
39
- fail("invalid-arguments", `unknown flag: ${flag}`);
40
- }
41
- if (index + 1 >= argv.length) {
42
- fail("invalid-arguments", `missing value for ${flag}`);
43
- }
44
- if (Object.hasOwn(values, flag)) {
45
- fail("invalid-arguments", `duplicate flag: ${flag}`);
46
- }
47
- values[flag] = argv[index + 1];
48
- }
49
- for (const flag of allowed) {
50
- if (!Object.hasOwn(values, flag)) {
51
- fail("invalid-arguments", `missing required flag: ${flag}`);
52
- }
53
- }
54
- return values;
55
- }
56
42
  function runCommand(file, args, env) {
57
43
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/common.sh:71::NODE_OPTIONS`
58
44
  // is the system's only scrubbing site and it dies with scripts/ in 4c.
@@ -95,6 +81,14 @@ export { runCommand as runCommandForTest };
95
81
  function commandFailed(result) {
96
82
  return result.status !== 0;
97
83
  }
84
+ async function storedStateAfterListingFailure(env, code, message) {
85
+ try {
86
+ return await readCodexStoredState(env, process.cwd());
87
+ }
88
+ catch {
89
+ fail(code, message);
90
+ }
91
+ }
98
92
  // Exported only so its unit test can reach it. No integration test can cover
99
93
  // it: on Linux glibc, `execvp`'s ENOEXEC falls back to `/bin/sh`, so the spawn
100
94
  // still succeeds, and every other candidate errno is already peeled off by an
@@ -130,11 +124,6 @@ async function runCodexCommand(codexBin, args, env) {
130
124
  return mapCodexLaunchFailure(cause, codexBin);
131
125
  }
132
126
  }
133
- function reportOrphanedWorkspace(log) {
134
- return (path) => {
135
- log.appendText("stderr", workspaceRemovalFailure(path));
136
- };
137
- }
138
127
  async function mutationCommand(log, codexBin, args, env) {
139
128
  const result = await runCodexCommand(codexBin, args, env);
140
129
  log.appendBytes("stderr", result.stdout);
@@ -172,16 +161,6 @@ async function requireCodex(codexBin, env) {
172
161
  fail("command-not-found", `required Codex command not found: ${codexBin}`);
173
162
  }
174
163
  }
175
- const BUILD_FLAGS = [
176
- "--upstream-root",
177
- "--candidate-root",
178
- "--requested-ref",
179
- "--resolved-ref",
180
- "--commit",
181
- "--manager-version",
182
- "--upstream-manifest-version",
183
- "--fallback-manifest",
184
- ];
185
164
  async function directoryExists(path) {
186
165
  try {
187
166
  return (await stat(path)).isDirectory();
@@ -215,165 +194,153 @@ async function runBuild(input, env, log) {
215
194
  if (!(await fileExists(fallbackManifest))) {
216
195
  fail("invalid-arguments", `fallback manifest not found: ${fallbackManifest}`);
217
196
  }
218
- let entered = false;
197
+ const candidateManifest = join(candidateRoot, ".codex-plugin/plugin.json");
198
+ const upstreamManifest = join(upstreamRoot, ".codex-plugin/plugin.json");
199
+ const manifestSource = (await fileExists(upstreamManifest))
200
+ ? "upstream"
201
+ : "fallback";
219
202
  try {
220
- return await withWorkspace(tmpdir(), "superpowers-manager.adapter-build.", async () => {
221
- entered = true;
222
- const candidateManifest = join(candidateRoot, ".codex-plugin/plugin.json");
223
- const upstreamManifest = join(upstreamRoot, ".codex-plugin/plugin.json");
224
- const manifestSource = (await fileExists(upstreamManifest))
225
- ? "upstream"
226
- : "fallback";
227
- try {
228
- await mkdir(join(candidateRoot, ".codex-plugin"), {
229
- recursive: true,
230
- });
231
- await copyFile(manifestSource === "upstream" ? upstreamManifest : fallbackManifest, candidateManifest);
232
- }
233
- catch {
234
- fail("build-failed", manifestSource === "upstream"
235
- ? "cannot copy upstream manifest into candidate"
236
- : "cannot copy fallback manifest into candidate");
237
- }
238
- let plan;
239
- let sourceRoot;
240
- let realCandidateRoot;
241
- try {
242
- sourceRoot = await realpath(upstreamRoot);
243
- realCandidateRoot = await realpath(candidateRoot);
244
- const manifest = await readManifest(candidateManifest);
245
- plan = await classifyHooks(manifest, manifestSource, sourceRoot);
246
- }
247
- catch (cause) {
248
- log.appendText("stderr", `hook classification failed: ${oneLine(cause)}`);
249
- fail("build-failed", "failed to prepare upstream Codex hooks");
250
- }
251
- try {
252
- await materializeHooks(plan, sourceRoot, realCandidateRoot);
253
- }
254
- catch (cause) {
255
- log.appendText("stderr", `hook materialization failed: ${oneLine(cause)}`);
256
- fail("build-failed", "failed to prepare upstream Codex hooks");
257
- }
258
- let source;
259
- try {
260
- // Decoded fatally, not leniently: the file can change between this read and `readManifest`'s read above.
261
- const rawManifestBytes = await readFile(candidateManifest);
262
- source = new TextDecoder("utf-8", {
263
- fatal: true,
264
- ignoreBOM: true,
265
- }).decode(rawManifestBytes);
266
- }
267
- catch {
268
- // Deliberately drops the cause. The Python interpolated the raw OSError here:
269
- // `git show 0b6d50e1e9c688397285c6fa274dc8c9437d8ba3:scripts/adapters/codex/apply-manifest-overlay.py:42::read`,
270
- // putting "[Errno 2] No such file or directory" on the operator's stream.
271
- // The prefix is preserved; the errno leak is not.
272
- log.appendText("stderr", `cannot read manifest JSON in ${candidateManifest}`);
273
- fail("build-failed", "failed to apply manager manifest overlay");
274
- }
275
- let overlaid;
276
- try {
277
- overlaid = applyManifestOverlay(source, input.managerVersion, candidateManifest);
278
- }
279
- catch (cause) {
280
- // applyManifestOverlay's own messages already name the manifest
281
- // path — three are the frozen CPython wording, and the fourth (the
282
- // numeric-overflow diagnostic, which has no CPython oracle wording
283
- // to match) now carries the path via its own rewrap in
284
- // src/harnesses/codex/manifest-overlay.ts. Emit as-is, with no added prefix — a
285
- // prefix here would double up the path these messages already
286
- // name.
287
- log.appendText("stderr", oneLine(cause));
288
- fail("build-failed", "failed to apply manager manifest overlay");
289
- }
290
- try {
291
- await writeFile(candidateManifest, overlaid, "utf8");
292
- }
293
- catch {
294
- log.appendText("stderr", `cannot write manifest JSON in ${candidateManifest}`);
295
- fail("build-failed", "failed to apply manager manifest overlay");
296
- }
297
- try {
298
- await copyFile(fallbackManifest, join(candidateRoot, ".codex-plugin/plugin.template.json"));
299
- }
300
- catch {
301
- fail("build-failed", "cannot copy fallback manifest template into candidate");
302
- }
303
- let upstreamSource;
304
- try {
305
- upstreamSource = await readCodexBuildSource(join(candidateRoot, ".superpowers-upstream.json"));
306
- }
307
- catch {
308
- fail("invalid-provenance", "candidate provenance is missing or invalid");
309
- }
310
- // The seven values the validator CLI would receive in split form:
311
- // --plugin-root, --requested-ref, --resolved-ref, --commit,
312
- // --manifest-version, --manifest-source, --upstream-manifest-version.
313
- // The eighth, --source, is passed attached, where argparse accepts any
314
- // dash-leading value, so it is deliberately absent here. Each value is
315
- // paired with the ADAPTER-facing flag name to report: --manager-version
316
- // (the CLI calls it --manifest-version) and --plugin-root /
317
- // --manifest-source (derived, not user-supplied) deliberately differ
318
- // from the validator CLI's own names, since the operator can only act
319
- // on the adapter's surface.
320
- const splitValues = [
321
- { value: candidateRoot, name: "--plugin-root" },
322
- { value: input.requestedRef, name: "--requested-ref" },
323
- { value: input.resolvedRef, name: "--resolved-ref" },
324
- { value: input.commit, name: "--commit" },
325
- { value: input.managerVersion, name: "--manager-version" },
326
- { value: manifestSource, name: "--manifest-source" },
327
- {
328
- value: input.upstreamManifestVersion,
329
- name: "--upstream-manifest-version",
330
- },
331
- ];
332
- const firstRejected = splitValues.find(({ value }) => !isAcceptedSplitValue(value));
333
- if (firstRejected !== undefined) {
334
- // Declared exception to message-record parity: argparse wrote usage
335
- // records here; this guard precedes the call and writes a
336
- // differently-worded record naming the rejected flag instead. The
337
- // failure code and message are unchanged.
338
- const text = "Generated plugin validation failed:\n" +
339
- `- validator argument \`${firstRejected.name}\` has a dash-leading value the argument parser rejects\n`;
340
- log.appendBytes("stderr", Buffer.from(text, "utf8"));
341
- fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
342
- }
343
- let errors;
344
- try {
345
- errors = await validateGeneratedPlugin({
346
- pluginRoot: candidateRoot,
347
- source: upstreamSource,
348
- requestedRef: input.requestedRef,
349
- resolvedRef: input.resolvedRef,
350
- commit: input.commit,
351
- manifestVersion: input.managerVersion,
352
- manifestSource,
353
- upstreamManifestVersion: input.upstreamManifestVersion,
354
- });
355
- }
356
- catch {
357
- fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
358
- }
359
- if (errors.length > 0) {
360
- // appendBytes, not appendText: one record per line, matching what
361
- // mutationCommand writes from the subprocess streams.
362
- const text = "Generated plugin validation failed:\n" +
363
- errors.map((error) => `- ${error}\n`).join("");
364
- log.appendBytes("stderr", Buffer.from(text, "utf8"));
365
- fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
366
- }
367
- log.appendBytes("stdout", Buffer.from(`generated plugin validation passed: ${candidateRoot}\n`, "utf8"));
368
- return {};
369
- }, { onCleanupFailure: reportOrphanedWorkspace(log) });
203
+ await mkdir(join(candidateRoot, ".codex-plugin"), {
204
+ recursive: true,
205
+ });
206
+ await copyFile(manifestSource === "upstream" ? upstreamManifest : fallbackManifest, candidateManifest);
207
+ }
208
+ catch {
209
+ fail("build-failed", manifestSource === "upstream"
210
+ ? "cannot copy upstream manifest into candidate"
211
+ : "cannot copy fallback manifest into candidate");
212
+ }
213
+ let plan;
214
+ let sourceRoot;
215
+ let realCandidateRoot;
216
+ try {
217
+ sourceRoot = await realpath(upstreamRoot);
218
+ realCandidateRoot = await realpath(candidateRoot);
219
+ const manifest = await readManifest(candidateManifest);
220
+ plan = await classifyHooks(manifest, manifestSource, sourceRoot);
370
221
  }
371
222
  catch (cause) {
372
- if (!entered) {
373
- fail("build-failed", `cannot create adapter build workspace under ${tmpdir()}`);
374
- }
375
- throw cause;
223
+ log.appendText("stderr", `hook classification failed: ${oneLine(cause)}`);
224
+ fail("build-failed", "failed to prepare upstream Codex hooks");
225
+ }
226
+ try {
227
+ await materializeHooks(plan, sourceRoot, realCandidateRoot);
228
+ }
229
+ catch (cause) {
230
+ log.appendText("stderr", `hook materialization failed: ${oneLine(cause)}`);
231
+ fail("build-failed", "failed to prepare upstream Codex hooks");
232
+ }
233
+ let source;
234
+ try {
235
+ // Decoded fatally, not leniently: the file can change between this read and `readManifest`'s read above.
236
+ const rawManifestBytes = await readFile(candidateManifest);
237
+ source = new TextDecoder("utf-8", {
238
+ fatal: true,
239
+ ignoreBOM: true,
240
+ }).decode(rawManifestBytes);
241
+ }
242
+ catch {
243
+ // Deliberately drops the cause. The Python interpolated the raw OSError here:
244
+ // `git show 0b6d50e1e9c688397285c6fa274dc8c9437d8ba3:scripts/adapters/codex/apply-manifest-overlay.py:42::read`,
245
+ // putting "[Errno 2] No such file or directory" on the operator's stream.
246
+ // The prefix is preserved; the errno leak is not.
247
+ log.appendText("stderr", `cannot read manifest JSON in ${candidateManifest}`);
248
+ fail("build-failed", "failed to apply manager manifest overlay");
249
+ }
250
+ let overlaid;
251
+ try {
252
+ overlaid = applyManifestOverlay(source, input.managerVersion, candidateManifest);
253
+ }
254
+ catch (cause) {
255
+ // applyManifestOverlay's own messages already name the manifest
256
+ // path — three are the frozen CPython wording, and the fourth (the
257
+ // numeric-overflow diagnostic, which has no CPython oracle wording
258
+ // to match) now carries the path via its own rewrap in
259
+ // src/harnesses/codex/manifest-overlay.ts. Emit as-is, with no added prefix — a
260
+ // prefix here would double up the path these messages already
261
+ // name.
262
+ log.appendText("stderr", oneLine(cause));
263
+ fail("build-failed", "failed to apply manager manifest overlay");
264
+ }
265
+ try {
266
+ await writeFile(candidateManifest, overlaid, "utf8");
376
267
  }
268
+ catch {
269
+ log.appendText("stderr", `cannot write manifest JSON in ${candidateManifest}`);
270
+ fail("build-failed", "failed to apply manager manifest overlay");
271
+ }
272
+ try {
273
+ await copyFile(fallbackManifest, join(candidateRoot, ".codex-plugin/plugin.template.json"));
274
+ }
275
+ catch {
276
+ fail("build-failed", "cannot copy fallback manifest template into candidate");
277
+ }
278
+ let upstreamSource;
279
+ try {
280
+ upstreamSource = await readCodexBuildSource(join(candidateRoot, ".superpowers-upstream.json"));
281
+ }
282
+ catch {
283
+ fail("invalid-provenance", "candidate provenance is missing or invalid");
284
+ }
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
+ let errors;
319
+ try {
320
+ errors = await validateGeneratedPlugin({
321
+ pluginRoot: candidateRoot,
322
+ source: upstreamSource,
323
+ requestedRef: input.requestedRef,
324
+ resolvedRef: input.resolvedRef,
325
+ commit: input.commit,
326
+ manifestVersion: input.managerVersion,
327
+ manifestSource,
328
+ upstreamManifestVersion: input.upstreamManifestVersion,
329
+ });
330
+ }
331
+ catch {
332
+ fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
333
+ }
334
+ if (errors.length > 0) {
335
+ // appendBytes, not appendText: one record per line, matching what
336
+ // mutationCommand writes from the subprocess streams.
337
+ const text = "Generated plugin validation failed:\n" +
338
+ errors.map((error) => `- ${error}\n`).join("");
339
+ log.appendBytes("stderr", Buffer.from(text, "utf8"));
340
+ fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
341
+ }
342
+ log.appendBytes("stdout", Buffer.from(`generated plugin validation passed: ${candidateRoot}\n`, "utf8"));
343
+ return {};
377
344
  }
378
345
  async function runInstall(packageRoot, env, log) {
379
346
  if (!isAbsolute(packageRoot)) {
@@ -383,224 +350,133 @@ async function runInstall(packageRoot, env, log) {
383
350
  fail("invalid-arguments", `package root not found: ${packageRoot}`);
384
351
  }
385
352
  const codexBin = env.SUPERPOWERS_CODEX || "codex";
386
- const refreshMode = env.SUPERPOWERS_INSTALL_REFRESH_MODE || "add-only";
353
+ const refreshMode = codexInstallRefreshMode(env);
387
354
  await requireCodex(codexBin, env);
388
- if (refreshMode !== "add-only" && refreshMode !== "remove-add") {
389
- fail("invalid-arguments", `unsupported SUPERPOWERS_INSTALL_REFRESH_MODE: ${refreshMode}`);
355
+ if (refreshMode === null) {
356
+ fail("invalid-arguments", `unsupported SUPERPOWERS_INSTALL_REFRESH_MODE: ${env.SUPERPOWERS_INSTALL_REFRESH_MODE}`);
390
357
  }
391
- let entered = false;
392
- try {
393
- return await withWorkspace(tmpdir(), "superpowers-manager.adapter-install.", async () => {
394
- entered = true;
395
- const marketplaceList = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
396
- if (commandFailed(marketplaceList)) {
397
- fail("install-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
398
- }
399
- let registeredRoot;
400
- try {
401
- registeredRoot = marketplaceRootFromJson(marketplaceList.stdout, MARKETPLACE_NAME);
402
- }
403
- catch {
404
- fail("install-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
405
- }
406
- if (registeredRoot.length === 0) {
407
- const added = await mutationCommand(log, codexBin, ["plugin", "marketplace", "add", packageRoot], env);
408
- if (commandFailed(added)) {
409
- fail("install-failed", `codex marketplace add failed for ${packageRoot}`);
410
- }
411
- }
412
- else if (!(await pathsEqual(packageRoot, registeredRoot))) {
413
- log.appendText("stdout", `marketplace ${MARKETPLACE_NAME} registered at ${registeredRoot}; re-registering at ${packageRoot}`);
414
- const removed = await mutationCommand(log, codexBin, ["plugin", "marketplace", "remove", MARKETPLACE_NAME], env);
415
- if (commandFailed(removed)) {
416
- fail("install-failed", `codex marketplace remove failed for ${MARKETPLACE_NAME} (registered at ${registeredRoot})`);
417
- }
418
- const added = await mutationCommand(log, codexBin, ["plugin", "marketplace", "add", packageRoot], env);
419
- if (commandFailed(added)) {
420
- fail("install-failed", `marketplace ${MARKETPLACE_NAME} was removed but re-adding failed.`, [
421
- `recover with: ${codexBin} plugin marketplace add ${packageRoot}`,
422
- `previous root (last known good): ${registeredRoot}`,
423
- ]);
424
- }
425
- }
426
- if (refreshMode === "remove-add") {
427
- await mutationCommand(log, codexBin, ["plugin", "remove", PLUGIN_ID], env);
428
- }
429
- const pluginAdded = await mutationCommand(log, codexBin, ["plugin", "add", PLUGIN_ID], env);
430
- if (commandFailed(pluginAdded)) {
431
- fail("install-failed", `codex plugin add failed for ${PLUGIN_ID}`);
432
- }
433
- return {
434
- verification_hints: {
435
- ...(refreshMode === "add-only"
436
- ? {
437
- mismatch: "retry with SUPERPOWERS_INSTALL_REFRESH_MODE=remove-add",
438
- }
439
- : {}),
440
- missing: "verify with 'codex plugin list --json'.",
441
- },
442
- };
443
- }, { onCleanupFailure: reportOrphanedWorkspace(log) });
358
+ const marketplaceList = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
359
+ let registeredRoot;
360
+ if (commandFailed(marketplaceList)) {
361
+ registeredRoot = (await storedStateAfterListingFailure(env, "install-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`)).managerMarketplaceRoot;
444
362
  }
445
- catch (cause) {
446
- if (!entered) {
447
- fail("install-failed", `cannot create adapter install workspace under ${tmpdir()}`);
363
+ else {
364
+ try {
365
+ registeredRoot = marketplaceRootFromJson(marketplaceList.stdout, MARKETPLACE_NAME);
366
+ }
367
+ catch {
368
+ fail("install-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
448
369
  }
449
- throw cause;
450
370
  }
371
+ if (registeredRoot.length === 0) {
372
+ const added = await mutationCommand(log, codexBin, ["plugin", "marketplace", "add", packageRoot], env);
373
+ if (commandFailed(added)) {
374
+ fail("install-failed", `codex marketplace add failed for ${packageRoot}`);
375
+ }
376
+ }
377
+ else if (!(await pathsEqual(packageRoot, registeredRoot))) {
378
+ log.appendText("stdout", `marketplace ${MARKETPLACE_NAME} registered at ${registeredRoot}; re-registering at ${packageRoot}`);
379
+ const removed = await mutationCommand(log, codexBin, ["plugin", "marketplace", "remove", MARKETPLACE_NAME], env);
380
+ if (commandFailed(removed)) {
381
+ fail("install-failed", `codex marketplace remove failed for ${MARKETPLACE_NAME} (registered at ${registeredRoot})`);
382
+ }
383
+ const added = await mutationCommand(log, codexBin, ["plugin", "marketplace", "add", packageRoot], env);
384
+ if (commandFailed(added)) {
385
+ fail("install-failed", `marketplace ${MARKETPLACE_NAME} was removed but re-adding failed.`, [
386
+ `recover with: ${codexBin} plugin marketplace add ${packageRoot}`,
387
+ `previous root (last known good): ${registeredRoot}`,
388
+ ]);
389
+ }
390
+ }
391
+ if (refreshMode === "remove-add") {
392
+ await mutationCommand(log, codexBin, ["plugin", "remove", PLUGIN_ID], env);
393
+ }
394
+ const pluginAdded = await mutationCommand(log, codexBin, ["plugin", "add", PLUGIN_ID], env);
395
+ if (commandFailed(pluginAdded)) {
396
+ fail("install-failed", `codex plugin add failed for ${PLUGIN_ID}`);
397
+ }
398
+ return codexInstallReceipt("verify with 'codex plugin list --json'.", refreshMode === "add-only"
399
+ ? "retry with SUPERPOWERS_INSTALL_REFRESH_MODE=remove-add"
400
+ : "");
451
401
  }
452
402
  async function runUninstall(input, env, log) {
453
403
  const { pluginPresent, marketplacePresent } = input;
454
404
  const codexBin = env.SUPERPOWERS_CODEX || "codex";
455
405
  await requireCodex(codexBin, env);
456
- let entered = false;
457
- try {
458
- return await withWorkspace(tmpdir(), "superpowers-manager.adapter-uninstall.", async () => {
459
- entered = true;
460
- if (pluginPresent) {
461
- const result = await mutationCommand(log, codexBin, ["plugin", "remove", PLUGIN_ID], env);
462
- if (commandFailed(result)) {
463
- fail("uninstall-failed", `codex plugin remove failed for ${PLUGIN_ID}`);
464
- }
465
- log.appendText("stdout", `removed plugin ${PLUGIN_ID}`);
466
- }
467
- else {
468
- log.appendText("stdout", "plugin not installed; skipping");
469
- }
470
- if (marketplacePresent) {
471
- const result = await mutationCommand(log, codexBin, ["plugin", "marketplace", "remove", MARKETPLACE_NAME], env);
472
- if (commandFailed(result)) {
473
- fail("uninstall-failed", `codex plugin marketplace remove failed for ${MARKETPLACE_NAME}`);
474
- }
475
- log.appendText("stdout", `removed marketplace ${MARKETPLACE_NAME}`);
476
- }
477
- else {
478
- log.appendText("stdout", "marketplace not registered; skipping");
479
- }
480
- return {};
481
- }, { onCleanupFailure: reportOrphanedWorkspace(log) });
406
+ if (pluginPresent) {
407
+ const result = await mutationCommand(log, codexBin, ["plugin", "remove", PLUGIN_ID], env);
408
+ if (commandFailed(result)) {
409
+ fail("uninstall-failed", `codex plugin remove failed for ${PLUGIN_ID}`);
410
+ }
411
+ log.appendText("stdout", `removed plugin ${PLUGIN_ID}`);
482
412
  }
483
- catch (cause) {
484
- if (!entered) {
485
- fail("uninstall-failed", `cannot create adapter uninstall workspace under ${tmpdir()}`);
413
+ else {
414
+ log.appendText("stdout", "plugin not installed; skipping");
415
+ }
416
+ if (marketplacePresent) {
417
+ const result = await mutationCommand(log, codexBin, ["plugin", "marketplace", "remove", MARKETPLACE_NAME], env);
418
+ if (commandFailed(result)) {
419
+ fail("uninstall-failed", `codex plugin marketplace remove failed for ${MARKETPLACE_NAME}`);
486
420
  }
487
- throw cause;
421
+ log.appendText("stdout", `removed marketplace ${MARKETPLACE_NAME}`);
422
+ }
423
+ else {
424
+ log.appendText("stdout", "marketplace not registered; skipping");
488
425
  }
426
+ return {};
489
427
  }
490
- async function runInspect(view, context, env, log) {
491
- if (view === "update-control") {
492
- return { view: "update-control", update_control: "managed" };
428
+ function ownershipFromResources(pluginPresent, marketplacePresent, legacyPresent, conflicts) {
429
+ if (conflicts.some((item) => typeof item !== "string" ||
430
+ item.length === 0 ||
431
+ hasTerminalControl(item))) {
432
+ fail("malformed-result", "expected an array of strings at conflicts");
433
+ }
434
+ const managerPresent = pluginPresent || marketplacePresent;
435
+ const identityState = managerPresent
436
+ ? legacyPresent
437
+ ? "both"
438
+ : "manager"
439
+ : legacyPresent
440
+ ? "legacy"
441
+ : "neither";
442
+ return codexOwnershipInspection(identityState, { pluginPresent, marketplacePresent }, conflicts);
443
+ }
444
+ async function runOwnership(context, env, log) {
445
+ const codexBin = env.SUPERPOWERS_CODEX || "codex";
446
+ await requireCodex(codexBin, env);
447
+ const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
448
+ if (commandFailed(plugins)) {
449
+ 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.installedListingJson);
451
+ return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
452
+ }
453
+ let managerPlugin;
454
+ let legacyPlugin;
455
+ let conflicts;
456
+ try {
457
+ managerPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", PLUGIN_ID);
458
+ legacyPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", LEGACY_PLUGIN_ID);
459
+ conflicts = await inspectCodexConflicts({ root: context.root, env }, plugins.stdout.toString("utf8"));
493
460
  }
494
- if (view === "fingerprint") {
495
- const codexBin = env.SUPERPOWERS_CODEX || "codex";
496
- await requireCodex(codexBin, env);
497
- let entered = false;
498
- try {
499
- return await withWorkspace(tmpdir(), "superpowers-manager.adapter-fingerprint.", async () => {
500
- entered = true;
501
- const listing = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
502
- if (commandFailed(listing)) {
503
- fail("inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
504
- }
505
- let activeVersion;
506
- try {
507
- activeVersion = activePluginVersionFromJson(listing.stdout, PLUGIN_ID);
508
- }
509
- catch {
510
- fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
511
- }
512
- if (activeVersion.length === 0) {
513
- return { view: "fingerprint", fingerprint: null };
514
- }
515
- let searchRoot = env.SUPERPOWERS_INSTALLED_SEARCH_ROOT;
516
- if (!searchRoot) {
517
- if (env.HOME === undefined) {
518
- fail("inspect-failed", "cannot inspect active Codex plugin fingerprint without HOME");
519
- }
520
- searchRoot = join(env.HOME || "/", ".codex");
521
- }
522
- const activeRoot = installedRootForVersion(searchRoot, MARKETPLACE_NAME, "superpowers", activeVersion);
523
- const fingerprint = await installedCommitFromRoot(activeRoot);
524
- if (fingerprint.length === 0) {
525
- fail("inspect-failed", `cannot inspect active Codex plugin fingerprint under ${activeRoot}`);
526
- }
527
- return { view: "fingerprint", fingerprint };
528
- }, { onCleanupFailure: reportOrphanedWorkspace(log) });
529
- }
530
- catch (cause) {
531
- if (!entered) {
532
- fail("inspect-failed", `cannot create adapter fingerprint workspace under ${tmpdir()}`);
533
- }
534
- throw cause;
535
- }
461
+ catch {
462
+ fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
536
463
  }
537
- if (view === "ownership") {
538
- const codexBin = env.SUPERPOWERS_CODEX || "codex";
539
- await requireCodex(codexBin, env);
540
- let entered = false;
541
- try {
542
- return await withWorkspace(tmpdir(), "superpowers-manager.adapter-inspect.", async () => {
543
- entered = true;
544
- const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
545
- if (commandFailed(plugins)) {
546
- fail("inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
547
- }
548
- const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
549
- if (commandFailed(marketplaces)) {
550
- fail("inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
551
- }
552
- let managerPlugin;
553
- let legacyPlugin;
554
- let managerMarketplace;
555
- let legacyMarketplace;
556
- let conflicts;
557
- try {
558
- managerPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", PLUGIN_ID);
559
- legacyPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", LEGACY_PLUGIN_ID);
560
- conflicts = await inspectCodexConflicts({ root: context.root, env }, plugins.stdout.toString("utf8"));
561
- }
562
- catch {
563
- fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
564
- }
565
- try {
566
- managerMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", MARKETPLACE_NAME);
567
- legacyMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", LEGACY_MARKETPLACE_NAME);
568
- }
569
- catch {
570
- fail("inspect-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
571
- }
572
- const managerPresent = managerPlugin || managerMarketplace;
573
- const legacyPresent = legacyPlugin || legacyMarketplace;
574
- return {
575
- view: "ownership",
576
- resources: {
577
- plugin: managerPlugin,
578
- marketplace: managerMarketplace,
579
- },
580
- legacy_resources: {
581
- plugin: legacyPlugin,
582
- marketplace: legacyMarketplace,
583
- },
584
- identity_state: managerPresent
585
- ? legacyPresent
586
- ? "both"
587
- : "manager"
588
- : legacyPresent
589
- ? "legacy"
590
- : "neither",
591
- conflicts: [...conflicts],
592
- };
593
- }, { onCleanupFailure: reportOrphanedWorkspace(log) });
594
- }
595
- catch (cause) {
596
- if (!entered) {
597
- fail("inspect-failed", `cannot create adapter inspect workspace under ${tmpdir()}`);
598
- }
599
- throw cause;
600
- }
464
+ const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
465
+ if (commandFailed(marketplaces)) {
466
+ 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.installedListingJson);
468
+ return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
601
469
  }
602
- const unsupportedView = view;
603
- fail("invalid-arguments", `unsupported inspect view: ${unsupportedView}`);
470
+ let managerMarketplace;
471
+ let legacyMarketplace;
472
+ try {
473
+ managerMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", MARKETPLACE_NAME);
474
+ legacyMarketplace = installedListingHas(marketplaces.stdout, "marketplaces", "name", LEGACY_MARKETPLACE_NAME);
475
+ }
476
+ catch {
477
+ fail("inspect-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
478
+ }
479
+ return ownershipFromResources(managerPlugin, managerMarketplace, legacyPlugin || legacyMarketplace, conflicts);
604
480
  }
605
481
  async function runCodexOperation(operation, context, execute) {
606
482
  const env = { ...process.env, ...context.env };
@@ -625,70 +501,73 @@ export function codexInstall(packageRoot, context) {
625
501
  export function codexRemove(input, context) {
626
502
  return runCodexOperation("uninstall", context, (env, log) => runUninstall(input, env, log));
627
503
  }
628
- export function codexInspect(view, context) {
629
- return runCodexOperation("inspect", context, (env, log) => runInspect(view, context, env, log));
504
+ export function codexInspectOwnership(context) {
505
+ return runCodexOperation("inspect", context, (env, log) => runOwnership(context, env, log));
630
506
  }
631
- export async function runAdapter(argv, context) {
632
- const rawOperation = argv[0];
633
- const operation = rawOperation || "adapter";
634
- const args = argv.slice(1);
635
- try {
636
- if (rawOperation === undefined || rawOperation.length === 0) {
637
- fail("invalid-arguments", "missing adapter operation");
638
- }
639
- else if (operation === "build") {
640
- const flags = parseFlags(args, BUILD_FLAGS);
641
- return codexBuild({
642
- upstreamRoot: flags["--upstream-root"],
643
- candidateRoot: flags["--candidate-root"],
644
- requestedRef: flags["--requested-ref"],
645
- resolvedRef: flags["--resolved-ref"],
646
- commit: flags["--commit"],
647
- managerVersion: flags["--manager-version"],
648
- upstreamManifestVersion: flags["--upstream-manifest-version"],
649
- fallbackManifest: flags["--fallback-manifest"],
650
- }, context);
507
+ export function codexInspectControl(context) {
508
+ return runCodexOperation("inspect", context, async () => codexControlInspection("managed"));
509
+ }
510
+ export function codexReadNativeState(context) {
511
+ return runCodexOperation("native-state", context, async (env, log) => {
512
+ const codexBin = env.SUPERPOWERS_CODEX || "codex";
513
+ await requireCodex(codexBin, env);
514
+ const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
515
+ if (commandFailed(plugins)) {
516
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
517
+ return {
518
+ marketplaceRoot: stored.managerMarketplaceRoot,
519
+ pluginPresent: stored.managerPluginPresent,
520
+ pluginEnabled: stored.managerPluginEnabled,
521
+ activeVersion: null,
522
+ activeRoot: null,
523
+ };
651
524
  }
652
- else if (operation === "install") {
653
- const flags = parseFlags(args, ["--package-root"]);
654
- return codexInstall(flags["--package-root"], context);
525
+ let manager;
526
+ let activeVersion;
527
+ try {
528
+ const managers = codexInstalledPluginsFromJson(plugins.stdout).filter((plugin) => plugin.pluginId === PLUGIN_ID);
529
+ if (managers.length > 1)
530
+ fail("inspect-failed", "Codex manager plugin appears more than once");
531
+ manager = managers[0];
532
+ activeVersion =
533
+ manager === undefined
534
+ ? ""
535
+ : activePluginVersionFromJson(plugins.stdout, PLUGIN_ID);
655
536
  }
656
- else if (operation === "uninstall") {
657
- const flags = parseFlags(args, [
658
- "--plugin-present",
659
- "--marketplace-present",
660
- ]);
661
- const pluginPresent = flags["--plugin-present"];
662
- const marketplacePresent = flags["--marketplace-present"];
663
- if (pluginPresent !== "true" && pluginPresent !== "false") {
664
- fail("invalid-arguments", "--plugin-present must be true or false");
665
- }
666
- if (marketplacePresent !== "true" && marketplacePresent !== "false") {
667
- fail("invalid-arguments", "--marketplace-present must be true or false");
668
- }
669
- return codexRemove({
670
- pluginPresent: pluginPresent === "true",
671
- marketplacePresent: marketplacePresent === "true",
672
- }, context);
537
+ catch (cause) {
538
+ if (cause instanceof AdapterFailure)
539
+ throw cause;
540
+ fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
673
541
  }
674
- else if (operation === "inspect") {
675
- const flags = parseFlags(args, ["--view"]);
676
- const view = flags["--view"];
677
- if (view !== "ownership" &&
678
- view !== "update-control" &&
679
- view !== "fingerprint") {
680
- fail("invalid-arguments", `unsupported inspect view: ${view}`);
681
- }
682
- return codexInspect(view, context);
542
+ const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
543
+ if (commandFailed(marketplaces)) {
544
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
545
+ return {
546
+ marketplaceRoot: stored.managerMarketplaceRoot,
547
+ pluginPresent: stored.managerPluginPresent,
548
+ pluginEnabled: stored.managerPluginEnabled,
549
+ activeVersion: null,
550
+ activeRoot: null,
551
+ };
683
552
  }
684
- else {
685
- fail("unsupported-operation", `unsupported adapter operation: ${operation}`);
553
+ let marketplaceRoot;
554
+ try {
555
+ marketplaceRoot = marketplaceRootFromJson(marketplaces.stdout, MARKETPLACE_NAME, true);
686
556
  }
687
- }
688
- catch (cause) {
689
- if (cause instanceof AdapterFailure) {
690
- return failureResult(operation, cause.code, cause.message, cause.hints, []);
557
+ catch (cause) {
558
+ if (cause instanceof AdapterFailure)
559
+ throw cause;
560
+ fail("inspect-failed", `cannot parse output of '${codexBin} plugin marketplace list --json'`);
691
561
  }
692
- throw cause;
693
- }
562
+ const searchRoot = env.SUPERPOWERS_INSTALLED_SEARCH_ROOT || codexHome(env, process.cwd());
563
+ return {
564
+ marketplaceRoot: marketplaceRoot || null,
565
+ pluginPresent: manager !== undefined,
566
+ pluginEnabled: manager?.enabled === true,
567
+ activeVersion: activeVersion || null,
568
+ activeRoot: activeVersion
569
+ ? installedRootForVersion(searchRoot, MARKETPLACE_NAME, "superpowers", activeVersion)
570
+ : null,
571
+ };
572
+ });
694
573
  }