superpowers-manager 0.5.2 → 0.7.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 (83) hide show
  1. package/README.md +44 -26
  2. package/dist/adapter-result.js +5 -9
  3. package/dist/atomic.js +4 -14
  4. package/dist/cli-arguments.js +1 -27
  5. package/dist/cli.js +32 -52
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +26 -107
  8. package/dist/commands/mutation.js +7 -0
  9. package/dist/commands/pin.js +1 -1
  10. package/dist/commands/prepare.js +39 -129
  11. package/dist/commands/probe.js +35 -264
  12. package/dist/commands/track-latest.js +1 -1
  13. package/dist/commands/uninstall.js +16 -92
  14. package/dist/commands/update.js +5 -25
  15. package/dist/effective-selection.js +7 -9
  16. package/dist/git-tree.js +85 -0
  17. package/dist/harness-command-result.js +47 -0
  18. package/dist/harnesses/codex/adapter.js +231 -510
  19. package/dist/harnesses/codex/compatibility.js +2 -17
  20. package/dist/harnesses/codex/conflicts.js +2 -3
  21. package/dist/harnesses/codex/harness.js +18 -263
  22. package/dist/harnesses/codex/hooks.js +1 -1
  23. package/dist/harnesses/codex/json.js +1 -11
  24. package/dist/harnesses/codex/lifecycle.js +80 -72
  25. package/dist/harnesses/codex/manifest-overlay.js +23 -18
  26. package/dist/harnesses/codex/paths.js +14 -29
  27. package/dist/harnesses/codex/presentation.js +8 -37
  28. package/dist/harnesses/codex/publication.js +13 -37
  29. package/dist/harnesses/codex/recovery.js +5 -5
  30. package/dist/harnesses/codex/stored-state.js +1 -1
  31. package/dist/harnesses/opencode/config.js +256 -0
  32. package/dist/harnesses/opencode/discovery.js +587 -0
  33. package/dist/harnesses/opencode/harness.js +37 -0
  34. package/dist/harnesses/opencode/install.js +601 -0
  35. package/dist/harnesses/opencode/native.js +57 -0
  36. package/dist/harnesses/opencode/package.js +89 -0
  37. package/dist/harnesses/opencode/paths.js +38 -0
  38. package/dist/harnesses/opencode/prepare.js +30 -0
  39. package/dist/harnesses/opencode/presentation.js +28 -0
  40. package/dist/harnesses/opencode/state.js +174 -0
  41. package/dist/harnesses/pi/compatibility.js +5 -37
  42. package/dist/harnesses/pi/install.js +7 -5
  43. package/dist/harnesses/pi/native.js +3 -47
  44. package/dist/harnesses/pi/package.js +5 -107
  45. package/dist/harnesses/pi/prepare.js +13 -104
  46. package/dist/harnesses/pi/presentation.js +23 -66
  47. package/dist/harnesses/pi/shared-skills.js +28 -39
  48. package/dist/harnesses/pi/state.js +7 -27
  49. package/dist/provenance.js +2 -2
  50. package/dist/python-json-format.js +0 -69
  51. package/dist/python-json.js +2 -27
  52. package/dist/python-text.js +9 -69
  53. package/dist/safe-path.js +2 -2
  54. package/dist/selection-store.js +2 -22
  55. package/dist/snapshot-package.js +78 -0
  56. package/dist/snapshot-prepare.js +99 -0
  57. package/dist/snapshot-presentation.js +44 -0
  58. package/dist/validator.js +39 -73
  59. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  60. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  61. package/node_modules/jsonc-parser/README.md +364 -0
  62. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  63. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  64. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  65. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  66. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  67. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  68. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  69. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  70. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  71. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  72. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  73. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  74. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  75. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  76. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  77. package/node_modules/jsonc-parser/package.json +37 -0
  78. package/package.json +8 -4
  79. package/dist/domain/fingerprint.js +0 -4
  80. package/dist/selection-state-cli.js +0 -85
  81. package/dist/status.js +0 -24
  82. package/dist/upstream-cli.js +0 -110
  83. package/dist/validate-generated-plugin-cli.js +0 -118
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Superpowers Manager
2
2
 
3
- Install [`obra/superpowers`](https://github.com/obra/superpowers) directly from upstream for Codex and the Pi coding agent. Stay on a known-good release or commit, try a branch, and upgrade when you choose—without waiting for a marketplace copy to catch up.
3
+ Install [`obra/superpowers`](https://github.com/obra/superpowers) directly from upstream for Codex, the Pi coding agent, and OpenCode. Stay on a known-good release or commit, try a branch, and upgrade when you choose—without waiting for a marketplace copy to catch up.
4
4
 
5
5
  > Unofficial community integration. Not affiliated with the `obra/superpowers` maintainers.
6
6
 
@@ -8,11 +8,11 @@ Install [`obra/superpowers`](https://github.com/obra/superpowers) directly from
8
8
 
9
9
  Start with the latest stable release by default, or choose the upstream version that works for you. There is no background updater.
10
10
 
11
- | What you want | How to choose it |
12
- |---|---|
13
- | Stay on a known-good version | `pin` a release tag or full 40-character commit SHA |
11
+ | What you want | How to choose it |
12
+ | -------------------------------------- | ----------------------------------------------------------------------------- |
13
+ | Stay on a known-good version | `pin` a release tag or full 40-character commit SHA |
14
14
  | Try an upstream fix before its release | Set `SUPERPOWERS_REF` to a branch or other resolvable ref for that invocation |
15
- | Return to the latest stable release | Save `track-latest`, then explicitly install or update |
15
+ | Return to the latest stable release | Save `track-latest`, then explicitly install or update |
16
16
 
17
17
  For example, save a release pin and install it for your agent:
18
18
 
@@ -21,7 +21,7 @@ npx superpowers-manager pin v6.1.1
21
21
  npx superpowers-manager install --harness codex
22
22
  ```
23
23
 
24
- Use `--harness pi` for Pi. The pin is shared by both agents, but each installation changes only when you run its `install` or `update` command. A saved pin keeps subsequent installs and updates on that version unless you change or override the selection.
24
+ Use `--harness pi` for Pi or `--harness opencode` for OpenCode. The pin is shared by all harnesses, but each installation changes only when you run its `install` or `update` command. A saved pin keeps subsequent installs and updates on that version unless you change or override the selection.
25
25
 
26
26
  To try a branch, replace `feature/foo` with an existing upstream branch:
27
27
 
@@ -42,7 +42,7 @@ See [version selection](https://github.com/j7an/superpowers-manager/blob/main/do
42
42
 
43
43
  ## Start with your agent
44
44
 
45
- The two harnesses share upstream selection, while each has independent prepared and installed state. Omitting `--harness` selects Codex.
45
+ The three harnesses share upstream selection, while each has independent prepared and installed state. Omitting `--harness` selects Codex.
46
46
 
47
47
  ### Codex
48
48
 
@@ -71,6 +71,19 @@ after a successful install, activating update, or removal. See the
71
71
  [Pi reference](https://github.com/j7an/superpowers-manager/blob/main/docs/pi.md)
72
72
  for compatibility, experimental opt-in, and recovery behavior.
73
73
 
74
+ ### OpenCode
75
+
76
+ ```sh
77
+ npx superpowers-manager install --harness opencode
78
+ npx superpowers-manager probe --harness opencode
79
+ npx superpowers-manager update --harness opencode
80
+ ```
81
+
82
+ OpenCode installs a separately validated, Manager-owned frozen snapshot. Restart
83
+ OpenCode after a successful install, activating update, or removal. See the
84
+ [OpenCode reference](https://github.com/j7an/superpowers-manager/blob/main/docs/opencode.md)
85
+ for global configuration, conflicts, and recovery behavior.
86
+
74
87
  To remove a Manager-owned installation, choose one harness explicitly:
75
88
 
76
89
  ### Uninstall Codex
@@ -85,25 +98,26 @@ npx superpowers-manager uninstall --harness codex
85
98
  npx superpowers-manager uninstall --harness pi
86
99
  ```
87
100
 
101
+ ### Uninstall OpenCode
102
+
103
+ ```sh
104
+ npx superpowers-manager uninstall --harness opencode
105
+ ```
106
+
88
107
  The manager never removes another Superpowers provider. See the [ownership guidance](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md#provider-ownership) before switching providers.
89
108
 
90
109
  ## Shared selection and lifecycle
91
110
 
92
- The [selection commands above](#choose-what-you-run) save shared upstream intent. They do not activate Codex or Pi; run the selected harness's `install` or `update` to apply that choice. `unpin` restores the packaged fallback policy.
111
+ The [selection commands above](#choose-what-you-run) save shared upstream intent. They do not activate Codex, Pi, or OpenCode; run the selected harness's `install` or `update` to apply that choice. `unpin` restores the packaged fallback policy.
93
112
 
94
- | Command | Purpose |
95
- |---|---|
96
- | `prepare` | Build and validate the selected harness's prepared output |
97
- | `install` | Apply the shared selection to the selected harness |
98
- | `probe` | Inspect the selected harness without mutation |
99
- | `update` | Explicitly refresh the selected harness when needed and verify it |
100
- | `uninstall` | Remove only Manager-owned state from the selected harness |
113
+ See [targeted lifecycle commands](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md#targeted-lifecycle) for preparation, inspection, installation, updates, and removal.
101
114
 
102
- Use `--harness codex` or `--harness pi` with each targeted lifecycle command.
115
+ Use `--harness codex`, `--harness pi`, or `--harness opencode` with each targeted lifecycle command.
103
116
 
104
117
  ```text
105
118
  shared selection -> prepare / inspect / activate Codex
106
119
  -> prepare / inspect / activate Pi
120
+ -> prepare / inspect / activate OpenCode
107
121
  ```
108
122
 
109
123
  The [usage reference](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md) covers version selection, precedence, offline behavior, `prepare`, validators, and every lifecycle command. The harness references cover [Codex registration and hooks](https://github.com/j7an/superpowers-manager/blob/main/docs/codex.md) and [Pi snapshots and recovery](https://github.com/j7an/superpowers-manager/blob/main/docs/pi.md).
@@ -113,18 +127,22 @@ The [usage reference](https://github.com/j7an/superpowers-manager/blob/main/docs
113
127
  The installed package requires Node >=24. Native source, tests, and packaging tooling require Node >=24.12.0. Every other requirement is command-specific and is checked before dispatch. This table is derived from production by `tests/bin/readme-requirements.test.ts`.
114
128
 
115
129
  <!-- requirements:begin -->
116
- | Command | git | Python 3 | Codex CLI (default) | Pi CLI (`--harness pi`) |
117
- |---|---|---|---|---|
118
- | `pin` | yes | no | no | no |
119
- | `track-latest` | no | no | no | no |
120
- | `unpin` | no | no | no | no |
121
- | `prepare` | yes | only with SUPERPOWERS_VALIDATOR | no | no |
122
- | `probe` | yes | no | yes | no |
123
- | `install` | yes | no | yes | yes |
124
- | `update` | yes | no | yes | yes |
125
- | `uninstall` | no | no | yes | yes |
130
+
131
+ | Command | git | Codex CLI (default) | Pi CLI (`--harness pi`) | OpenCode CLI (`--harness opencode`) |
132
+ | -------------- | --- | ------------------- | ----------------------- | ----------------------------------- |
133
+ | `pin` | yes | no | no | no |
134
+ | `track-latest` | no | no | no | no |
135
+ | `unpin` | no | no | no | no |
136
+ | `prepare` | yes | no | no | no |
137
+ | `probe` | yes | yes | no | no |
138
+ | `install` | yes | yes | yes | yes |
139
+ | `update` | yes | yes | yes | yes |
140
+ | `uninstall` | no | yes | yes | no |
141
+
126
142
  <!-- requirements:end -->
127
143
 
144
+ Generated Codex manifests preserve upstream JSON number tokens while rejecting non-finite decimal or exponent values.
145
+
128
146
  macOS and Linux are tested. WSL2 is supported for the established Codex path; Pi in WSL2 and native Windows remain untested. The manager runs in-process and does not require Git Bash. The [Pi reference](https://github.com/j7an/superpowers-manager/blob/main/docs/pi.md#runtime-compatibility) explains its runtime qualification and admission checks.
129
147
 
130
148
  Native test versions qualify integration mechanisms; they are not runtime allowlists, and an untested runtime is not automatically certified as compatible.
@@ -133,6 +133,9 @@ export function failureResult(operation, code, message, hints, messages) {
133
133
  },
134
134
  };
135
135
  }
136
+ export function inspectionFailure(operation, subject) {
137
+ return failureResult(operation, "invalid-state", `cannot inspect ${subject}`, [], []);
138
+ }
136
139
  export function hasTerminalControl(value) {
137
140
  for (const character of value) {
138
141
  const code = character.codePointAt(0);
@@ -148,10 +151,8 @@ export function hasTerminalControl(value) {
148
151
  // every command replays through -- wrote these three strings to the terminal
149
152
  // unfiltered.
150
153
  //
151
- // NOT requireProtocolString: that one message
152
- // ("protocol strings must not contain terminal control characters") names no
153
- // member, and D8b requires the thrown message to name the failing one. So this
154
- // scans with the predicate underneath it instead.
154
+ // This scans with the predicate underneath it because D8b requires the thrown
155
+ // message to name the failing member.
155
156
  //
156
157
  // Order is code, then message, then hints by ascending index, so the thrown
157
158
  // message is a function of the outcome rather than of iteration order.
@@ -197,8 +198,3 @@ export function writeAdapterFailure(ctx, outcome) {
197
198
  ctx.stderr.write(`hint: ${hint}\n`);
198
199
  }
199
200
  }
200
- export function requireProtocolString(value) {
201
- if (hasTerminalControl(value)) {
202
- throw new Error("protocol strings must not contain terminal control characters");
203
- }
204
- }
package/dist/atomic.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { constants, lstat, open, readFile, rename, rm, unlink, } from "node:fs/promises";
2
+ import { constants, lstat, open, rename, rm, unlink } from "node:fs/promises";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
+ import { isErrno } from "./safe-path.js";
4
5
  import { SafetyError } from "./safety-error.js";
5
6
  export async function atomicWriteFile(path, bytes, options) {
6
7
  const renamePath = options.hooks?.rename ?? rename;
7
8
  let handle;
8
9
  let temporary;
9
- let phase = "pre-replacement";
10
10
  try {
11
11
  for (let attempt = 0; attempt < 100; attempt += 1) {
12
12
  const suffix = randomBytes(8).toString("hex");
@@ -36,17 +36,10 @@ export async function atomicWriteFile(path, bytes, options) {
36
36
  await options.validate(temporary);
37
37
  await renamePath(temporary, path);
38
38
  temporary = undefined;
39
- phase = "post-replacement";
40
- await options.hooks?.afterReplace?.(path);
41
39
  await fsyncDirectoryBestEffort(dirname(path));
42
40
  }
43
41
  catch (cause) {
44
- let finalBytes;
45
- if (phase === "post-replacement") {
46
- finalBytes = await readFile(path).catch(() => undefined);
47
- }
48
- const details = finalBytes === undefined ? { phase } : { phase, finalBytes };
49
- throw new SafetyError("atomic", `atomic file write failed during ${phase}`, { cause, details });
42
+ throw new SafetyError("atomic", "atomic file write failed during pre-replacement", { cause, details: { phase: "pre-replacement" } });
50
43
  }
51
44
  finally {
52
45
  if (handle !== undefined) {
@@ -76,11 +69,8 @@ async function exists(path) {
76
69
  return true;
77
70
  }
78
71
  catch (cause) {
79
- if (cause instanceof Error &&
80
- "code" in cause &&
81
- cause.code === "ENOENT") {
72
+ if (isErrno(cause, "ENOENT"))
82
73
  return false;
83
- }
84
74
  throw cause;
85
75
  }
86
76
  }
@@ -20,7 +20,7 @@ export function extractHarnessOptions(command, argv) {
20
20
  : token.slice("--harness=".length);
21
21
  if (!value || value.startsWith("--"))
22
22
  throw new UsageError("option --harness requires a value");
23
- if (value !== "codex" && value !== "pi")
23
+ if (value !== "codex" && value !== "pi" && value !== "opencode")
24
24
  throw new UsageError(`unknown harness: ${value}`);
25
25
  harness = value;
26
26
  }
@@ -37,32 +37,6 @@ export function extractHarnessOptions(command, argv) {
37
37
  }
38
38
  return { options: { harness, allowExperimental }, args };
39
39
  }
40
- export function parseFlags(argv, names) {
41
- const allowed = new Set(names);
42
- const result = {};
43
- for (let index = 0; index < argv.length; index += 1) {
44
- const token = argv[index];
45
- if (!token.startsWith("--"))
46
- throw new UsageError(`unexpected argument: ${token}`);
47
- const separator = token.indexOf("=");
48
- const name = separator === -1 ? token.slice(2) : token.slice(2, separator);
49
- if (!allowed.has(name))
50
- throw new UsageError(`unknown option: --${name}`);
51
- const value = separator === -1 ? argv[index + 1] : token.slice(separator + 1);
52
- if (value === undefined || (separator === -1 && value.startsWith("--"))) {
53
- throw new UsageError(`option --${name} requires a value`);
54
- }
55
- result[name] = value;
56
- if (separator === -1)
57
- index += 1;
58
- }
59
- for (const name of names) {
60
- if (!Object.hasOwn(result, name)) {
61
- throw new UsageError(`required option is missing: --${name}`);
62
- }
63
- }
64
- return result;
65
- }
66
40
  export function oneLine(value) {
67
41
  const message = value instanceof Error ? value.message : String(value);
68
42
  return message.replace(/[\r\n]+/g, " ");
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import * as path from "node:path";
4
4
  import { extractHarnessOptions, oneLine, UsageError } from "./cli-arguments.js";
5
5
  import { codexHarness } from "./harnesses/codex/harness.js";
6
6
  import { piHarness } from "./harnesses/pi/harness.js";
7
+ import { openCodeHarness } from "./harnesses/opencode/harness.js";
7
8
  import { createResourceCoordinator } from "./resource-lock.js";
8
9
  import { runInstall } from "./commands/install.js";
9
10
  import { runPin } from "./commands/pin.js";
@@ -46,8 +47,8 @@ const IN_PROCESS_HANDLERS = {
46
47
  // required because `spw_invoke_adapter` ran validate-adapter-response.py once
47
48
  // per adapter call
48
49
  // (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:37-44::--response "$response_file" --result "$result_file" \`);
49
- // the in-process path has no validator process. It remains CONDITIONAL for
50
- // `prepare` through commandRequirements(env) below, unchanged from slice 3.4.
50
+ // the in-process path no longer uses that adapter-response validator.
51
+ // No command has a Python requirement in requirementsFor().
51
52
  // No command requires a POSIX shell any more.
52
53
  const SHARED_COMMAND_REQUIREMENTS = {
53
54
  pin: ["git"],
@@ -170,13 +171,6 @@ function findTool(name, env, platform) {
170
171
  }
171
172
  return null;
172
173
  }
173
- // python3 is required by `prepare` only when SUPERPOWERS_VALIDATOR names one:
174
- // after the port, that optional spawn (runValidator, in src/validator.ts,
175
- // called from src/commands/prepare.ts) is Python's only remaining consumer
176
- // on the prepare path. The conditional lives here, in the accessor preflight reads,
177
- // rather than inside preflight — an accessor that under-reports what
178
- // preflight enforces is the blind spot slice 2 closed when it made
179
- // CLI-PREFLIGHT-01 derive its map from production.
180
174
  function sharedRequirement(name) {
181
175
  return {
182
176
  name,
@@ -185,39 +179,22 @@ function sharedRequirement(name) {
185
179
  missingMessage: `required command not found: ${name} — install ${name} and re-run`,
186
180
  };
187
181
  }
188
- function commandRequirementsFor(env, adapter) {
189
- const forCommand = (command) => {
190
- const shared = SHARED_COMMAND_REQUIREMENTS[command].map(sharedRequirement);
191
- if (command === "prepare" && env.SUPERPOWERS_VALIDATOR) {
192
- shared.push(sharedRequirement("python3"));
193
- }
194
- return [...shared, ...adapter.requirements(command, env)];
195
- };
196
- return {
197
- pin: forCommand("pin"),
198
- "track-latest": forCommand("track-latest"),
199
- unpin: forCommand("unpin"),
200
- prepare: forCommand("prepare"),
201
- probe: forCommand("probe"),
202
- install: forCommand("install"),
203
- update: forCommand("update"),
204
- uninstall: forCommand("uninstall"),
205
- };
206
- }
207
- function commandRequirements(env) {
208
- return Object.fromEntries(SUBCOMMANDS.map((command) => [
209
- command,
210
- commandRequirementsFor(env, codexHarness)[command].map((requirement) => requirement.name),
211
- ]));
182
+ function requirementsFor(command, env, adapter) {
183
+ return [
184
+ ...SHARED_COMMAND_REQUIREMENTS[command].map(sharedRequirement),
185
+ ...adapter.requirements(command, env),
186
+ ];
212
187
  }
213
188
  // Preflight; never touches Codex state. It is the union of two exported
214
189
  // accessors: configurationErrors (validator configuration) and
215
- // commandRequirements (tool availability), both specific to the selected
190
+ // requirementsFor (tool availability), both specific to the selected
216
191
  // command. No command requires a POSIX shell: slice 4b flipped the last
217
192
  // spawned command in-process, so there is no shell to discover.
218
- function preflightFor(cmd, env, platform, adapter) {
193
+ function preflight(cmd, env, platform, adapter) {
219
194
  const errors = [...configurationErrors(cmd, env)];
220
- for (const requirement of commandRequirementsFor(env, adapter)[cmd]) {
195
+ if (errors.length > 0)
196
+ return { ok: false, errors };
197
+ for (const requirement of requirementsFor(cmd, env, adapter)) {
221
198
  const found = requirement.lookup === "explicit-path-or-path" &&
222
199
  requirement.executable.includes(path.sep)
223
200
  ? fs.existsSync(requirement.executable)
@@ -229,9 +206,6 @@ function preflightFor(cmd, env, platform, adapter) {
229
206
  return { ok: false, errors };
230
207
  return { ok: true };
231
208
  }
232
- function preflight(cmd, env, platform) {
233
- return preflightFor(cmd, env, platform, codexHarness);
234
- }
235
209
  function usage() {
236
210
  return [
237
211
  "usage: superpowers-manager [command] [args...]",
@@ -249,19 +223,23 @@ function usage() {
249
223
  " uninstall remove the manager plugin and marketplace from Codex",
250
224
  "",
251
225
  "Target one invocation (default: codex; selection commands are shared):",
252
- " probe --harness pi [--porcelain]",
253
- " prepare --harness pi",
254
- " install --harness pi [--allow-experimental]",
255
- " update --harness pi [--allow-experimental]",
256
- " uninstall --harness pi",
257
- " --harness=codex and --harness=pi are also accepted after the command.",
226
+ " probe --harness opencode [--porcelain]",
227
+ " prepare --harness opencode",
228
+ " install --harness opencode [--allow-experimental]",
229
+ " update --harness opencode [--allow-experimental]",
230
+ " uninstall --harness opencode",
231
+ " --harness=codex, --harness=pi, and --harness=opencode are also accepted after the command.",
258
232
  "",
259
233
  "Environment overrides (used by in-process commands): SUPERPOWERS_REF,",
260
234
  "SUPERPOWERS_UPSTREAM_URL, SUPERPOWERS_CODEX, SUPERPOWERS_CACHE_DIR,",
261
235
  "SUPERPOWERS_CONFIG_DIR, XDG_CONFIG_HOME,",
262
236
  "SUPERPOWERS_PLUGIN_ROOT, SUPERPOWERS_MANIFEST_TEMPLATE,",
263
- "SUPERPOWERS_VALIDATOR, SUPERPOWERS_VALIDATOR_EXECUTABLE,",
264
- "SUPERPOWERS_INSTALLED_SEARCH_ROOT, SUPERPOWERS_INSTALL_REFRESH_MODE",
237
+ "SUPERPOWERS_VALIDATOR_EXECUTABLE,",
238
+ "SUPERPOWERS_INSTALLED_SEARCH_ROOT, SUPERPOWERS_INSTALL_REFRESH_MODE,",
239
+ "SUPERPOWERS_OPENCODE",
240
+ "",
241
+ "SUPERPOWERS_VALIDATOR is removed; unset it and use",
242
+ "SUPERPOWERS_VALIDATOR_EXECUTABLE with an executable validator.",
265
243
  "",
266
244
  "Selection state uses SUPERPOWERS_CONFIG_DIR when set; otherwise it uses",
267
245
  "$XDG_CONFIG_HOME/superpowers-manager, then $HOME/.config/superpowers-manager.",
@@ -288,9 +266,11 @@ async function main() {
288
266
  console.error(usage());
289
267
  process.exit(2);
290
268
  }
291
- const status = parsed.options.harness === "pi"
292
- ? await dispatch(piHarness, parsed)
293
- : await dispatch(codexHarness, parsed);
269
+ const status = parsed.options.harness === "opencode"
270
+ ? await dispatch(openCodeHarness, parsed)
271
+ : parsed.options.harness === "pi"
272
+ ? await dispatch(piHarness, parsed)
273
+ : await dispatch(codexHarness, parsed);
294
274
  process.exit(status);
295
275
  }
296
276
  async function dispatch(adapter, parsed) {
@@ -299,7 +279,7 @@ async function dispatch(adapter, parsed) {
299
279
  console.error("error: cannot resolve the superpowers-manager package root");
300
280
  return 1;
301
281
  }
302
- const pf = preflightFor(parsed.cmd, process.env, process.platform, adapter);
282
+ const pf = preflight(parsed.cmd, process.env, process.platform, adapter);
303
283
  if (!pf.ok) {
304
284
  for (const e of pf.errors)
305
285
  console.error(`error: ${e}`);
@@ -341,7 +321,7 @@ async function dispatch(adapter, parsed) {
341
321
  }
342
322
  return status;
343
323
  }
344
- export { resolvePackageRoot, isMain, parseArgs, commandRequirements, commandRequirementsFor, preflight, usage, main, };
324
+ export { resolvePackageRoot, isMain, parseArgs, requirementsFor, preflight, usage, main, };
345
325
  if (isMain(import.meta.filename, process.argv[1])) {
346
326
  main().catch((cause) => {
347
327
  console.error(`error: ${oneLine(cause)}`);
@@ -0,0 +1,28 @@
1
+ export async function callAdapter(call, failure) {
2
+ let result;
3
+ try {
4
+ result = await call();
5
+ }
6
+ catch {
7
+ return { ok: false, result: null, message: failure.unexpected };
8
+ }
9
+ const outcome = result.outcome;
10
+ if (result.status !== 0 || !outcome.ok)
11
+ return {
12
+ ok: false,
13
+ result,
14
+ message: outcome.ok ? failure.invalidStatus : null,
15
+ };
16
+ return { ok: true, result: { status: result.status, outcome } };
17
+ }
18
+ // Preserve outcomes collected before gathering throws so command catches can
19
+ // replay them before reporting the underlying failure.
20
+ export class GatherFailure extends Error {
21
+ inner;
22
+ outcomes;
23
+ constructor(message, inner, outcomes) {
24
+ super(message);
25
+ this.inner = inner;
26
+ this.outcomes = outcomes;
27
+ }
28
+ }
@@ -1,92 +1,35 @@
1
- // Ports scripts/install. The shell sourced common.sh, provenance.sh,
2
- // status.sh, lifecycle.sh and adapter.sh; the predicates now live in
3
- // src/harnesses/codex/lifecycle.ts, the generated-metadata read lives in src/provenance.ts,
4
- // and the adapter arrives through ctx.adapter.
5
1
  import { tmpdir } from "node:os";
6
2
  import { oneLine } from "../cli-arguments.js";
7
3
  import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
8
- import { gatherProbe, replayOutcome, validInstalled, validOutput, } from "./probe.js";
4
+ import { GatherFailure, callAdapter, } from "./adapter-call.js";
5
+ import { gatherProbe, replayOutcome } from "./probe.js";
9
6
  import { runPrepare } from "./prepare.js";
10
7
  import { runWithMutation } from "./mutation.js";
11
8
  import { activationBlock } from "../harness-compatibility.js";
12
- function writeOutput(output, ctx) {
9
+ export function writeOutput(output, ctx) {
13
10
  for (const line of output.stdout)
14
11
  ctx.stdout.write(`${line}\n`);
15
12
  for (const line of output.stderr)
16
13
  ctx.stderr.write(`${line}\n`);
17
14
  }
18
- async function invoke(call, failure, outcomes, valid = () => true) {
19
- let result;
20
- try {
21
- result = await call();
22
- if (!result ||
23
- !Number.isInteger(result.status) ||
24
- !result.outcome ||
25
- typeof result.outcome.ok !== "boolean" ||
26
- typeof result.outcome.operation !== "string" ||
27
- !Array.isArray(result.outcome.messages) ||
28
- !result.outcome.messages.every((message) => message &&
29
- (message.channel === "stdout" || message.channel === "stderr") &&
30
- typeof message.text === "string") ||
31
- (result.outcome.ok
32
- ? result.outcome.error !== null || !valid(result.outcome.result)
33
- : !result.outcome.error ||
34
- typeof result.outcome.error.code !== "string" ||
35
- typeof result.outcome.error.message !== "string" ||
36
- !Array.isArray(result.outcome.error.hints) ||
37
- !result.outcome.error.hints.every((hint) => typeof hint === "string"))) {
38
- return { ok: false, message: failure.invalidStatus, result: null };
15
+ async function invoke(call, failure, outcomes, acceptValue) {
16
+ let result = await callAdapter(call, failure);
17
+ if (result.ok && acceptValue !== undefined) {
18
+ try {
19
+ if (!acceptValue(result.result.outcome.result))
20
+ result = { ok: false, result: null, message: failure.invalidStatus };
21
+ }
22
+ catch {
23
+ result = { ok: false, result: null, message: failure.invalidStatus };
39
24
  }
40
25
  }
41
- catch {
42
- return { ok: false, message: failure.unexpected, result: null };
43
- }
44
- outcomes.push(result.outcome);
45
- const outcome = result.outcome;
46
- if (result.status !== 0 || !outcome.ok) {
47
- return {
48
- ok: false,
49
- message: outcome.ok ? failure.invalidStatus : null,
50
- result,
51
- };
52
- }
53
- return {
54
- ok: true,
55
- result: { status: result.status, outcome },
56
- };
57
- }
58
- function validReceipt(value) {
59
- return (value !== null &&
60
- typeof value === "object" &&
61
- validOutput(value.missingVerificationOutput) &&
62
- validOutput(value.mismatchVerificationOutput));
63
- }
64
- // withWorkspace can throw AFTER its callback has already returned a fully
65
- // computed StageOutcome: a post-success cleanup failure discards that return
66
- // value entirely and rejects instead, UNLESS an `onCleanupFailure` reporter is
67
- // supplied -- which gatherInstallStages below does, precisely so this class
68
- // stays reserved for mkdtemp failure (nothing collected yet) and the
69
- // callback's own throw (never reachable here; see gatherInstallStages).
70
- // Carries the outcomes collected so far so runInstall's catch can still
71
- // replay them instead of discarding them with a bare re-throw. Same shape as
72
- // src/commands/uninstall.ts's GatherFailure, duplicated for the same reason
73
- // invoke() is: no shared dependency between the two modules.
74
- class GatherFailure extends Error {
75
- inner;
76
- outcomes;
77
- constructor(inner, outcomes) {
78
- super("install gather failed");
79
- this.inner = inner;
80
- this.outcomes = outcomes;
81
- }
26
+ if (result.result !== null)
27
+ outcomes.push(result.result.outcome);
28
+ return result;
82
29
  }
83
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:44-58::TMPDIR=`, wrapped in the temporary workspace scripts/install
84
- // created via spw_make_workspace + spw_install_workspace_trap. Performs no
85
- // writes of its own -- same EPIPE-avoidance shape as gatherProbe and
86
- // src/commands/uninstall.ts's gatherUninstall.
30
+ // Collect outcomes without writing so output failures cannot be classified as
31
+ // inspection failures. Replay collected outcomes after gathering completes.
87
32
  async function gatherInstallStages(ctx, selection, artifact) {
88
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:38::tmp_parent=` -- ${TMPDIR:-/tmp}. Matches
89
- // src/commands/uninstall.ts's gatherUninstall.
90
33
  const parent = ctx.env.TMPDIR ?? tmpdir();
91
34
  const outcomes = [];
92
35
  let cleanupWarning = null;
@@ -133,7 +76,7 @@ async function gatherInstallStages(ctx, selection, artifact) {
133
76
  if (activationFailure !== null)
134
77
  return failed(activationFailure);
135
78
  const installFailure = ctx.adapter.presentation.callFailure("install", adapterContext);
136
- const install = await invoke(() => ctx.adapter.install(artifact, adapterContext), installFailure, outcomes, validReceipt);
79
+ const install = await invoke(() => ctx.adapter.install(artifact, adapterContext), installFailure, outcomes);
137
80
  if (!install.ok)
138
81
  return failed(install.message);
139
82
  let transaction;
@@ -163,9 +106,8 @@ async function gatherInstallStages(ctx, selection, artifact) {
163
106
  // is what turns a failed inspection into "error: installed manager
164
107
  // fingerprint inspection failed after install."
165
108
  // renderInstallVerification's failed-inspection arm
166
- // (`src/harnesses/codex/presentation.ts:329::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
167
- // exists for this result-bearing path; the lifecycle compatibility
168
- // export delegates through the same arm. Returning
109
+ // (`src/harnesses/codex/presentation.ts:280::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
110
+ // exists for this result-bearing path. Returning
169
111
  // failed() instead reported the adapter's own generic diagnostic and
170
112
  // dropped the post-install verification claim -- a mutation had
171
113
  // already been issued at stage 3,
@@ -173,7 +115,7 @@ async function gatherInstallStages(ctx, selection, artifact) {
173
115
  // adapter call failed". A ctx.adapter THROW has no result to render.
174
116
  // A pending transaction still rolls back before reporting that failure.
175
117
  const inspectionFailure = ctx.adapter.presentation.callFailure("post-install", adapterContext);
176
- const inspected = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes, validInstalled);
118
+ const inspected = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
177
119
  const inspection = inspected.result;
178
120
  const verified = inspection !== null &&
179
121
  inspection.status === 0 &&
@@ -186,7 +128,7 @@ async function gatherInstallStages(ctx, selection, artifact) {
186
128
  invalidStatus: `installation ${operation} returned an invalid result; preserve recovery material for manual resolution`,
187
129
  }, outcomes, (value) => value === null);
188
130
  if (!verified) {
189
- const restored = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes, validInstalled);
131
+ const restored = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
190
132
  const restoration = restored.ok
191
133
  ? `installed state after rollback: ${restored.result.outcome.result.kind}; identity=${restored.result.outcome.result.observedIdentity}`
192
134
  : "installed state after rollback could not be inspected";
@@ -217,7 +159,7 @@ async function gatherInstallStages(ctx, selection, artifact) {
217
159
  };
218
160
  }
219
161
  if (!settlement.ok) {
220
- const actual = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes, validInstalled);
162
+ const actual = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
221
163
  return {
222
164
  kind: "verified",
223
165
  outcomes,
@@ -260,7 +202,7 @@ async function gatherInstallStages(ctx, selection, artifact) {
260
202
  // Reachable only for mkdtemp failure (nothing collected yet) -- the
261
203
  // callback above never throws, so a post-success cleanup failure is
262
204
  // already handled by onCleanupFailure and cannot reach here.
263
- throw new GatherFailure(cause, outcomes);
205
+ throw new GatherFailure("install gather failed", cause, outcomes);
264
206
  }
265
207
  }
266
208
  export async function runInstall(argv, ctx) {
@@ -277,31 +219,8 @@ async function performInstall(argv, ctx) {
277
219
  probe = await gatherProbe(ctx);
278
220
  }
279
221
  catch (cause) {
280
- // gatherProbe performs no writes of its own (src/commands/probe.ts), so
281
- // this catch cannot also be reached by an EPIPE from install's own
282
- // output: the NOTE line above already left the try, and everything below
283
- // runs only after this try/catch has resolved.
284
- //
285
- // This is a SECOND consumer of gatherProbe's throw channel --
286
- // `src/commands/probe.ts:469-527::THREE exceptions, all inherited and none a regression:`'s
287
- // runProbe catch is the first. Because both consumers wrap the identical
288
- // function, its long comment there enumerates exactly what can reach THIS
289
- // stream too, including the three foreign-text exceptions at :251-296:
290
- // 1. :251-262 -- resolveRef splices git's own combined stdout+stderr
291
- // into its text. Reached on probe's DEFAULT path, which that comment
292
- // defines as every invocation NOT resolving a saved pin: a 40-hex
293
- // ref returns a "raw-commit" resolution at
294
- // `src/upstream.ts:162-164::return { kind: "raw-commit"`
295
- // before any git call, so it reaches no splice at all.
296
- // 2. :263-280 -- src/selection-store.ts's read path interpolates the
297
- // caught error's own message, so Node errno prose can appear.
298
- // AGENTS.md grandfathers that module's wording.
299
- // 3. :281-296 -- a SPAWN-level git failure, a different channel from
300
- // exception 1's exit-status one: on the non-ENOENT arm of
301
- // `src/git.ts:47-52::if (typeof failure.code === "string") {`, runGit
302
- // rejects with "cannot run git: " followed by the Node spawn error's
303
- // own message.
304
- // Not repeated in full here; read it there.
222
+ // Gathering does not write, so this catch cannot misclassify an output
223
+ // failure. oneLine() bounds any inherited diagnostic to one line.
305
224
  ctx.stderr.write(`error: ${oneLine(cause)}\n`);
306
225
  return 1;
307
226
  }