superpowers-manager 0.1.6 → 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.
Files changed (59) hide show
  1. package/README.md +184 -64
  2. package/bin/superpowers-manager.js +9 -237
  3. package/dist/adapter-result.js +204 -0
  4. package/dist/adapter.js +649 -0
  5. package/dist/atomic.js +142 -0
  6. package/dist/cli-arguments.js +32 -0
  7. package/dist/cli.js +293 -0
  8. package/dist/codex-json.js +89 -0
  9. package/dist/codex-state.js +78 -0
  10. package/dist/commands/context.js +1 -0
  11. package/dist/commands/install.js +376 -0
  12. package/dist/commands/pin.js +79 -0
  13. package/dist/commands/prepare.js +527 -0
  14. package/dist/commands/probe.js +392 -0
  15. package/dist/commands/track-latest.js +44 -0
  16. package/dist/commands/uninstall.js +308 -0
  17. package/dist/commands/unpin.js +95 -0
  18. package/dist/commands/update.js +138 -0
  19. package/dist/domain/fingerprint.js +4 -0
  20. package/dist/domain/refs.js +37 -0
  21. package/dist/effective-selection.js +123 -0
  22. package/dist/generated-plugin.js +748 -0
  23. package/dist/git.js +38 -0
  24. package/dist/hooks.js +290 -0
  25. package/dist/lifecycle.js +220 -0
  26. package/dist/manifest-overlay.js +125 -0
  27. package/dist/provenance.js +111 -0
  28. package/dist/python-json-format.js +83 -0
  29. package/dist/python-json.js +29 -0
  30. package/dist/python-text.js +74 -0
  31. package/dist/safe-path.js +137 -0
  32. package/dist/safety-error.js +10 -0
  33. package/dist/selection-state-cli.js +85 -0
  34. package/dist/selection-store.js +172 -0
  35. package/dist/selection.js +200 -0
  36. package/dist/status.js +24 -0
  37. package/dist/strict-json.js +248 -0
  38. package/dist/upstream-cli.js +110 -0
  39. package/dist/upstream-version.js +40 -0
  40. package/dist/upstream.js +253 -0
  41. package/dist/validate-generated-plugin-cli.js +118 -0
  42. package/dist/validator.js +317 -0
  43. package/dist/workspace.js +139 -0
  44. package/package.json +25 -2
  45. package/scripts/adapters/codex/adapter +0 -598
  46. package/scripts/adapters/codex/lib.sh +0 -391
  47. package/scripts/adapters/codex/validate-generated-plugin.py +0 -314
  48. package/scripts/core/adapter.sh +0 -92
  49. package/scripts/core/common.sh +0 -35
  50. package/scripts/core/lifecycle.sh +0 -127
  51. package/scripts/core/provenance.sh +0 -135
  52. package/scripts/core/status.sh +0 -27
  53. package/scripts/core/upstream.sh +0 -146
  54. package/scripts/core/validate-adapter-response.py +0 -245
  55. package/scripts/install +0 -60
  56. package/scripts/prepare +0 -126
  57. package/scripts/probe +0 -61
  58. package/scripts/uninstall +0 -44
  59. package/scripts/update +0 -31
package/README.md CHANGED
@@ -19,7 +19,7 @@ npx superpowers-manager update
19
19
  Use the official marketplace for the simplest native Codex installation. Use
20
20
  Superpowers Manager when you want immediate stable-upstream freshness after a
21
21
  user-triggered install/update, per-invocation release or commit selection,
22
- recorded upstream provenance, diagnostics, Codex-specific hook-free packaging,
22
+ recorded upstream provenance, diagnostics, upstream-owned Codex hook handling,
23
23
  and explicit install/update/probe/uninstall lifecycle control.
24
24
 
25
25
  | Choose | Best fit |
@@ -39,46 +39,79 @@ It never removes the legacy provider automatically.
39
39
 
40
40
  ## Requirements and platforms
41
41
 
42
- Superpowers Manager requires Node 24+, `git`, Python 3, and a POSIX `sh`.
43
- Codex CLI is required for `probe`, `install`, `update`, and `uninstall`;
44
- `prepare` does not require it.
42
+ Superpowers Manager requires Node 24+, and Codex is the only supported
43
+ integration today. Every other requirement is per command, enforced by
44
+ preflight before any dispatch. This table is checked against production by
45
+ `tests/bin/readme-requirements.test.js`; edit it when a command's requirements
46
+ change, in the same pull request.
47
+
48
+ <!-- requirements:begin -->
49
+ | Command | git | Python 3 | Codex CLI |
50
+ |---|---|---|---|
51
+ | `pin` | yes | no | no |
52
+ | `track-latest` | no | no | no |
53
+ | `unpin` | no | no | no |
54
+ | `prepare` | yes | only with SUPERPOWERS_VALIDATOR | no |
55
+ | `probe` | yes | no | yes |
56
+ | `install` | yes | no | yes |
57
+ | `update` | yes | no | yes |
58
+ | `uninstall` | no | no | yes |
59
+ <!-- requirements:end -->
60
+
61
+ See `docs/baseline/behavioral-inventory.md`'s `CLI-PREFLIGHT-01` for the
62
+ behaviour this table describes.
45
63
 
46
64
  macOS and Linux are tested. WSL2 is supported. The native Windows path is
47
- untested; the launcher looks for Git Bash, `git`, and `python3`, but path
48
- handling between MSYS and Codex remains a known risk area.
65
+ untested; the launcher no longer looks for Git Bash at all — every command runs
66
+ in-process, so no POSIX shell is discovered or required — but path handling
67
+ between MSYS and Codex remains a known risk area.
49
68
 
50
- Prepare, install, probe, and update resolve the requested upstream ref over the
51
- network. Updates are user-triggered and need upstream network access; the
52
- manager does not run automatic or background updates.
69
+ Network needs depend on the saved policy. `pin` verifies its target;
70
+ `track-latest` and invocation overrides resolve from upstream when applied; and
71
+ `prepare`, `install`, and `update` fetch or verify the effective source. A probe
72
+ of a saved exact pin can reuse its recorded identity without upstream access.
73
+ Updates remain user-triggered; the manager does not run automatic or background
74
+ updates.
53
75
 
54
76
  ## What it does
55
77
 
56
78
  - Resolves an upstream ref, defaulting to the latest stable `vX.Y.Z` release
57
79
  tag.
58
80
  - Clones/fetches upstream at that commit and assembles a Codex plugin tree under
59
- `plugins/superpowers/` (skills, assets, license/readme, and manifest;
60
- upstream's `hooks/` directory is deliberately excluded).
81
+ `plugins/superpowers/` (skills, assets, license/readme, manifest, and any
82
+ hook files that the upstream manifest allows).
61
83
  - Stamps the generated manifest with a ref-aware manager version ending in
62
84
  `+manager.<short-sha>` and writes the upstream provenance to
63
85
  `.superpowers-upstream.json`.
64
- - Validates the generated tree with the manager's shipped, Python-standard-library
65
- contract validator before swapping it into place (a failed run never destroys
66
- a previously generated tree).
86
+ - Validates the generated tree with the manager's shipped contract validator
87
+ before swapping it into place (a failed run never destroys a previously
88
+ generated tree).
67
89
  - Registers the `superpowers-manager` marketplace and installs or refreshes
68
90
  `superpowers@superpowers-manager` in Codex.
69
91
 
70
- The generated plugin carries upstream skills, assets, and documentation. The
71
- manager excludes upstream `hooks/`, removes the manifest `hooks` field, and
72
- validates that both stay absent. This is a Codex-specific adapter policy, not a
73
- claim about how Superpowers should be packaged for future or other harnesses.
74
- Changing the hook-free policy requires a separate design and current
75
- compatibility evidence.
92
+ The generated plugin carries upstream skills, assets, documentation, and the
93
+ upstream-owned hook policy. When upstream provides a Codex manifest, its
94
+ `hooks` declaration is preserved: exact `{}` suppresses the `hooks/` subtree;
95
+ active declarations conditionally materialize the upstream hook files; and a
96
+ missing `hooks` key or explicit empty array (`[]`) declaration uses Codex's
97
+ default discovery only when upstream ships `hooks/hooks.json`. Manifest-less
98
+ fallback refs use the committed minimal template and generate neither a manifest
99
+ `hooks` key nor a `hooks/` directory.
100
+ The manager never creates its own hooks or mutates Codex trust state.
101
+
102
+ Review upstream hook command definitions with Codex's `/hooks` flow. Codex
103
+ trust applies to a command definition, not to the contents of scripts that the
104
+ definition references. An explicit update of an older pin can therefore change
105
+ the contents of a trusted referenced script, or reveal a newly untrusted
106
+ upstream definition; review it before trusting or running it. This policy
107
+ eliminates latent hook-policy drift.
76
108
 
77
109
  ## Runtime architecture
78
110
 
79
- - `scripts/core/` owns the shared lifecycle, status, and protocol validation.
80
- - `scripts/adapters/codex/` owns build, inspection, reconciliation, and Codex
81
- mutation.
111
+ - The in-process TypeScript runtime under `src/` owns lifecycle dispatch,
112
+ status, and adapter-result handling;
113
+ see the [in-process adapter result contract](https://github.com/j7an/superpowers-manager/blob/main/docs/adapter-result-contract.md).
114
+ - `src/adapter.ts` owns build, inspection, reconciliation, and Codex mutation.
82
115
  - Codex is the only supported adapter today; no public harness selector ships
83
116
  yet.
84
117
 
@@ -92,10 +125,33 @@ after the built-in check. It receives the candidate plugin root as its only
92
125
  argument. It cannot replace or bypass built-in validation, and either check
93
126
  failing prevents the tree swap and all Codex mutation.
94
127
 
95
- Direct `scripts/install` keeps harness-specific validation in the Codex adapter:
96
- phase 1 prepares the exact candidate first, then the adapter performs its Codex
97
- and refresh-mode preflight before any Codex mutation. The Node dispatcher keeps
98
- its existing Codex preflight before dispatching to the shell lifecycle.
128
+ `SUPERPOWERS_VALIDATOR_EXECUTABLE=/path/to/validator` adds an optional external
129
+ validator after the built-in check. It names an executable, receiving the
130
+ candidate plugin root as its only argument. The manager constructs no shell
131
+ command string and supplies argv directly. On Linux, `execvp` may hand that
132
+ same argv to `/bin/sh` after `ENOEXEC`, and the values are preserved rather
133
+ than re-parsed, so a candidate path containing spaces or shell metacharacters
134
+ is passed through intact and cannot inject a command — on either platform. A
135
+ file with the exec bit set but no valid executable format is handled by the
136
+ platform's own `execvp` fallback, not by the manager: on Linux, `/bin/sh` runs
137
+ it as shell source; on darwin, it is
138
+ rejected outright as not runnable. Give the validator a shebang line or make
139
+ it a real binary, or its behavior will differ by platform. Exit 0 accepts;
140
+ any nonzero exit rejects and prevents the tree swap. The manager stops
141
+ waiting after 30 seconds and keeps at most 64 KiB of each of its output
142
+ streams. It cannot replace or bypass built-in validation, and it is not a
143
+ security boundary: it runs with the manager's own privileges, by design.
144
+
145
+ `SUPERPOWERS_VALIDATOR` is deprecated in favor of `SUPERPOWERS_VALIDATOR_EXECUTABLE`
146
+ and continues to work unchanged. The legacy variable requires `python3`; the
147
+ executable hook does not. Setting both is rejected at preflight, before any
148
+ network or Codex access, rather than silently resolved. Removal of the legacy
149
+ variable is deferred to a future major release.
150
+
151
+ The in-process install command keeps harness-specific validation in the Codex
152
+ adapter: phase 1 prepares the exact candidate first, then the adapter performs
153
+ its Codex and refresh-mode preflight before any Codex mutation. The CLI runs
154
+ its Codex preflight before invoking the lifecycle handler.
99
155
 
100
156
  ## Provider ownership
101
157
 
@@ -121,6 +177,9 @@ manager will not remove it for you.
121
177
 
122
178
  | Command | Codex side effects | Purpose |
123
179
  |---|---|---|
180
+ | `npx superpowers-manager pin REF` | None | Resolve and save an exact upstream release tag or full commit |
181
+ | `npx superpowers-manager track-latest` | None | Save a policy that resolves the latest stable release when applied |
182
+ | `npx superpowers-manager unpin` | None | Remove saved selection and return to the packaged fallback policy |
124
183
  | `npx superpowers-manager prepare` | None | Resolve upstream, build a staged plugin tree, validate it, and replace the generated tree only on success |
125
184
  | `npx superpowers-manager probe` | None | Report requested and resolved refs, desired/generated/installed commits, identity state, and status |
126
185
  | `npx superpowers-manager install` | Marketplace and plugin state | Prepare and validate, register the marketplace, install the plugin, and verify installed state |
@@ -136,43 +195,91 @@ when resulting manager state cannot be verified.
136
195
 
137
196
  ## Choosing the upstream version
138
197
 
139
- `SUPERPOWERS_REF` selects a stable release tag, full commit SHA, branch, or
140
- other resolvable upstream ref for that invocation. The stateless npx package
141
- records the requested ref, resolved ref, and exact commit, but does not persist
142
- the selection for a later invocation that omits `SUPERPOWERS_REF`.
198
+ Save a tag pin, commit pin, or explicit latest-stable policy for future
199
+ invocations:
143
200
 
144
- Without `SUPERPOWERS_REF`, the manager reads `config/upstream-ref`, which ships
145
- as `latest-release`. Accepted values include:
201
+ ```sh
202
+ npx superpowers-manager pin v6.1.1
203
+ npx superpowers-manager pin 0123456789abcdef0123456789abcdef01234567 # replace with a real upstream commit
204
+ npx superpowers-manager track-latest
205
+ npx superpowers-manager unpin
206
+ ```
207
+
208
+ The selection commands save intent only; they do not prepare generated content
209
+ or change Codex state. Run `npx superpowers-manager install` for a first
210
+ installation or `npx superpowers-manager update` to apply the saved policy to
211
+ an existing installation. There is no background updater: selection changes
212
+ take effect only through a user-triggered `prepare`, `install`, or `update`.
213
+
214
+ `pin` accepts only an exact `vMAJOR.MINOR.PATCH` tag (optionally with a SemVer
215
+ prerelease suffix) or a full 40-character commit. It resolves and verifies the
216
+ target before saving both the exact commit identity and its source. The saved
217
+ identity is the contract, not the contents of a clone cache: clearing or moving
218
+ `SUPERPOWERS_CACHE_DIR` does not change the selection. `track-latest` saves the
219
+ source and resolves the highest stable `vX.Y.Z` tag each time the policy is
220
+ applied. `unpin` removes only the saved selection and restores the packaged
221
+ `config/upstream-ref` fallback, currently `latest-release`.
222
+
223
+ The user-wide selection file is `selection.json` under the first configured
224
+ location:
146
225
 
147
- - `latest-release` — highest stable `vX.Y.Z` tag (prereleases are excluded).
148
- - A specific tag, e.g. `v6.0.3`.
149
- - A full 40-character commit SHA.
150
- - Any other ref upstream resolves (e.g. a branch name).
226
+ 1. `$SUPERPOWERS_CONFIG_DIR`
227
+ 2. `$XDG_CONFIG_HOME/superpowers-manager`
228
+ 3. `$HOME/.config/superpowers-manager`
229
+
230
+ Each location must be absolute. The ref and source precedence chains are
231
+ independent:
232
+
233
+ - Ref: `SUPERPOWERS_REF`, then saved selection, then `config/upstream-ref`.
234
+ - Source: `SUPERPOWERS_UPSTREAM_URL`, then saved source, then the official
235
+ `https://github.com/obra/superpowers` source.
236
+
237
+ `SUPERPOWERS_REF` is an invocation-only override and is not persisted. Unlike
238
+ an exact saved pin, it accepts stable tags, full commit SHAs, branches, and
239
+ other resolvable upstream refs for that invocation:
151
240
 
152
241
  ```sh
153
- SUPERPOWERS_REF=v6.0.3 npx superpowers-manager prepare
154
- SUPERPOWERS_REF=main npx superpowers-manager probe
155
- SUPERPOWERS_REF=feature/foo npx superpowers-manager install
156
- SUPERPOWERS_REF=latest-release npx superpowers-manager update
242
+ SUPERPOWERS_REF=feature/foo npx superpowers-manager probe
157
243
  ```
158
244
 
159
- `SUPERPOWERS_CACHE_DIR` may point to a persistent upstream clone cache to avoid
160
- re-cloning between package materializations. It caches Git objects; it does not
161
- persist ref selection or trigger updates.
245
+ `pin` and `track-latest` bind the current `SUPERPOWERS_UPSTREAM_URL`, or the
246
+ official source when that variable is unset, to the saved intent. An invocation
247
+ may override only the ref or only the source; `probe` exposes
248
+ `selection_origin`, `selection_mode`, `upstream_source_origin`,
249
+ `effective_source`, the saved-selection fields, and a mixed-origin warning in
250
+ human output so that combination remains visible.
251
+
252
+ A saved exact pin lets `probe` determine the desired upstream identity without
253
+ contacting Git, even when the saved source is temporarily unavailable. This is
254
+ not a general offline installation promise: `pin` verifies the requested
255
+ target, `track-latest` must resolve when applied, and `prepare`/`install`/`update`
256
+ must fetch or verify content from the effective source. Codex inspection is
257
+ still required for lifecycle status.
258
+
259
+ HTTP(S) upstream URLs containing userinfo are now rejected, including
260
+ token-only userinfo. This is an intentional compatibility break: use a Git
261
+ credential helper for HTTP(S) authentication or an SSH source instead of
262
+ embedding credentials in the URL.
263
+
264
+ Saved selection does not claim or transfer provider ownership. The manager
265
+ continues to mutate only `superpowers@superpowers-manager` and never removes or
266
+ updates the official provider or any other provider automatically.
162
267
 
163
268
  ## How versioning works
164
269
 
165
270
  - **Upstream manifest first:** when upstream provides
166
271
  `.codex-plugin/plugin.json`, `prepare` uses it as the generated manifest base
167
- so future upstream metadata fields are preserved by default.
272
+ so future upstream metadata fields, including its hook declaration, are
273
+ preserved by default.
168
274
  - **Fallback template:** `plugins/superpowers/.codex-plugin/plugin.template.json`
169
275
  is committed as a minimal fallback for older upstream refs that do not ship a
170
276
  Codex manifest. It carries the placeholder version
171
- `0.0.0+manager.template`.
277
+ `0.0.0+manager.template`, declares no hooks, and produces no `hooks/`
278
+ directory.
172
279
  - **Manager overlay:** `prepare` replaces the version with a ref-aware manager
173
- version, forces `skills` to `./skills/`, and enforces the manager's current
174
- hook-free policy: no manifest `hooks` key and no copied `hooks/` directory.
175
- Unknown upstream manifest fields remain preserved.
280
+ version and forces `skills` to `./skills/`. Unknown upstream manifest fields,
281
+ including `hooks`, remain preserved; the adapter materializes hook files only
282
+ according to the upstream declaration.
176
283
  - Stable tags generate release-looking versions such as
177
284
  `6.0.3+manager.896224c`; explicit prerelease tags generate versions such as
178
285
  `6.1.0-beta.1+manager.abc1234`.
@@ -202,11 +309,16 @@ persist ref selection or trigger updates.
202
309
  ## Tests
203
310
 
204
311
  ```sh
312
+ pnpm install --frozen-lockfile
313
+ pnpm run build
205
314
  sh tests/run.sh # Layers 1-3: host-side hermetic checks while iterating
206
315
  sh tests/container.sh # Layers 1-4: blocking Docker acceptance command
207
316
  sh tests/manual/codex-behavior-probe.sh # optional native-only compatibility residue
208
317
  ```
209
318
 
319
+ `pnpm run build` deletes `dist/` before compiling, so every build is a clean
320
+ rebuild and stale artifacts of deleted or renamed modules cannot survive.
321
+
210
322
  Layers 1-3 stay offline and hermetic: they use a fake local upstream repo plus
211
323
  host-side fixtures, and they perform no mutation of the developer's or runner's
212
324
  real Codex state.
@@ -219,28 +331,36 @@ an isolated container home with networking disabled. That container run may
219
331
  mutate the throwaway container-local Codex state, but it still performs no
220
332
  mutation of the developer's or runner's real Codex state.
221
333
 
334
+ The container probe uses Codex's `hooks/list` only as compatibility evidence
335
+ for the tested Codex build. It is not a stable Superpowers Manager API.
336
+
222
337
  The manual probe is opt-in and covers native-only compatibility residue such as
223
- path/cache behavior against an intentionally real local Codex install. It is
224
- not part of acceptance. GitHub Actions runs the blocking container acceptance
225
- command on pull requests and pushes to `main`.
338
+ path/cache and version-precedence behavior against an intentionally real local
339
+ Codex install. It is not part of acceptance. GitHub Actions runs both the
340
+ toolchain check and the blocking container acceptance command on pull requests
341
+ and pushes to `main`.
226
342
 
227
343
  ## Repository layout
228
344
 
229
345
  ```
230
346
  .agents/plugins/marketplace.json # local marketplace definition (tracked)
231
- config/upstream-ref # which upstream ref to track (tracked)
347
+ .github/ # CI, security, dependency, and release workflows (tracked)
348
+ bin/superpowers-manager.js # public thin loader for dist/cli.js
349
+ config/upstream-ref # packaged upstream fallback policy (tracked)
350
+ docs/ # adapter result contract and baseline evidence (tracked)
351
+ package.json pnpm-lock.yaml tsconfig.json # package contract and locked toolchain (tracked)
232
352
  plugins/superpowers/
233
- .codex-plugin/plugin.template.json # fallback manifest template (tracked)
234
- .codex-plugin/plugin.json # generated manifest (gitignored)
235
- skills/ assets/ LICENSE ... # generated from upstream (gitignored)
236
- .superpowers-upstream.json # generated provenance (gitignored)
237
- scripts/
238
- adapters/codex/ # Codex adapter entrypoint + validator helpers
239
- core/ # shared lifecycle/provenance/status modules
240
- prepare probe install update uninstall # user-facing shell entrypoints
241
- tests/ # hermetic suite + manual Codex probe
242
- .cache/upstream/ # upstream clone cache (gitignored)
353
+ .codex-plugin/plugin.template.json # fallback manifest template (tracked)
354
+ generated plugin tree # prepared from upstream (gitignored)
355
+ src/ # in-process TypeScript runtime (tracked)
356
+ cli.ts # CLI parsing, preflight, and command dispatch
357
+ commands/ # lifecycle and selection command handlers
358
+ adapter.ts # Codex integration
359
+ tests/ # hermetic suite and manual Codex probe (tracked)
360
+ migration-inventory/ # frozen historical evidence
361
+ dist/ # generated ESM build tree (gitignored)
243
362
  ```
244
363
 
245
364
  Everything under `plugins/superpowers/` except the fallback manifest template is
246
- generated by `prepare` and ignored by Git; re-run `prepare` to regenerate it.
365
+ generated by `npx superpowers-manager prepare` and ignored by Git; re-run that
366
+ command to regenerate it.
@@ -1,244 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
 
4
- import * as fs from 'node:fs';
5
- import * as path from 'node:path';
6
- import { spawnSync } from 'node:child_process';
4
+ import { existsSync } from "node:fs";
7
5
 
8
- /**
9
- * @typedef {'prepare' | 'probe' | 'install' | 'update' | 'uninstall'} Subcommand
10
- */
6
+ const cliUrl = new URL("../dist/cli.js", import.meta.url);
11
7
 
12
- /**
13
- * @typedef {{ kind: 'run', cmd: Subcommand, args: string[] }} RunParseResult
14
- * @typedef {{ kind: 'help' }} HelpParseResult
15
- * @typedef {{ kind: 'version' }} VersionParseResult
16
- * @typedef {{ kind: 'usage-error', message: string }} UsageErrorParseResult
17
- * @typedef {RunParseResult | HelpParseResult | VersionParseResult | UsageErrorParseResult} ParseResult
18
- */
19
-
20
- /**
21
- * @typedef {{ ok: true, shell: string }} PreflightOk
22
- * @typedef {{ ok: false, errors: string[] }} PreflightError
23
- * @typedef {PreflightOk | PreflightError} PreflightResult
24
- */
25
-
26
- /**
27
- * @typedef {{ file: string, argv: string[] }} SpawnDescriptor
28
- */
29
-
30
- const SUBCOMMANDS = ['prepare', 'probe', 'install', 'update', 'uninstall'];
31
- const CODEX_SUBCOMMANDS = ['probe', 'install', 'update', 'uninstall'];
32
- // Mirrors upstream Superpowers' hooks/run-hook.cmd discovery order.
33
- const GIT_BASH_CANDIDATES = [
34
- 'C:\\Program Files\\Git\\bin\\bash.exe',
35
- 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
36
- ];
37
-
38
- // Walk upward from the bin's physical location to the directory containing
39
- // package.json. realpathSync first: npm/npx expose the bin through a symlink
40
- // or shim outside the package root.
41
- /**
42
- * @param {string} scriptPath
43
- * @returns {string | null}
44
- */
45
- function resolvePackageRoot(scriptPath) {
46
- let dir;
47
- try {
48
- dir = path.dirname(fs.realpathSync(scriptPath));
49
- } catch (err) {
50
- return null;
51
- }
52
- for (;;) {
53
- if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
54
- const parent = path.dirname(dir);
55
- if (parent === dir) return null;
56
- dir = parent;
57
- }
58
- }
59
-
60
- /**
61
- * @param {string} moduleFilename
62
- * @param {string | undefined} argvPath
63
- * @returns {boolean}
64
- */
65
- function isMain(moduleFilename, argvPath) {
66
- if (!argvPath) return false;
67
- return moduleFilename === fs.realpathSync(argvPath);
8
+ if (!existsSync(cliUrl)) {
9
+ console.error(
10
+ "dist/ not built — run `pnpm install --frozen-lockfile && pnpm run build`",
11
+ );
12
+ process.exit(1);
68
13
  }
69
14
 
70
- /**
71
- * @param {string[]} argv
72
- * @returns {ParseResult}
73
- */
74
- function parseArgs(argv) {
75
- /** @type {string | undefined} */
76
- const first = argv[0];
77
- if (argv.length === 0) return { kind: 'run', cmd: 'update', args: [] };
78
- if (first === '--help' || first === '-h') return { kind: 'help' };
79
- if (first === '--version') return { kind: 'version' };
80
- if (first && SUBCOMMANDS.includes(first)) {
81
- return { kind: 'run', cmd: /** @type {Subcommand} */ (first), args: argv.slice(1) };
82
- }
83
- return { kind: 'usage-error', message: `unknown subcommand: ${first}` };
84
- }
85
-
86
- // Search env.PATH for an executable named `name`; on win32 also try PATHEXT
87
- // extensions. Returns the full path or null.
88
- /**
89
- * @param {string} name
90
- * @param {NodeJS.ProcessEnv} env
91
- * @param {NodeJS.Platform} platform
92
- * @returns {string | null}
93
- */
94
- function findTool(name, env, platform) {
95
- const pathVar = env.PATH || env.Path || '';
96
- const dirs = pathVar.split(path.delimiter).filter(Boolean);
97
- const exts = platform === 'win32'
98
- ? (env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
99
- : [''];
100
- for (const dir of dirs) {
101
- for (const ext of exts) {
102
- const candidate = path.join(dir, name + ext.toLowerCase());
103
- try {
104
- fs.accessSync(candidate, fs.constants.X_OK);
105
- return candidate;
106
- } catch (err) { /* keep looking */ }
107
- }
108
- }
109
- return null;
110
- }
111
-
112
- // POSIX: `sh` on PATH. Windows: Git Bash at its standard install paths,
113
- // then `bash` on PATH.
114
- /**
115
- * @param {NodeJS.ProcessEnv} env
116
- * @param {NodeJS.Platform} platform
117
- * @returns {string | null}
118
- */
119
- function discoverShell(env, platform) {
120
- if (platform !== 'win32') return findTool('sh', env, platform);
121
- for (const candidate of GIT_BASH_CANDIDATES) {
122
- try {
123
- fs.accessSync(candidate, fs.constants.X_OK);
124
- return candidate;
125
- } catch (err) { /* keep looking */ }
126
- }
127
- return findTool('bash', env, platform);
128
- }
129
-
130
- // Tool preflight; never touches Codex state. codex is required only for the
131
- // subcommands that mutate or read Codex.
132
- /**
133
- * @param {Subcommand} cmd
134
- * @param {NodeJS.ProcessEnv} env
135
- * @param {NodeJS.Platform} platform
136
- * @returns {PreflightResult}
137
- */
138
- function preflight(cmd, env, platform) {
139
- const errors = [];
140
- for (const tool of ['git', 'python3']) {
141
- if (!findTool(tool, env, platform)) {
142
- errors.push(`required command not found: ${tool} — install ${tool} and re-run`);
143
- }
144
- }
145
- const shell = discoverShell(env, platform);
146
- if (!shell) {
147
- errors.push(platform === 'win32'
148
- ? 'no POSIX shell found — install Git for Windows (provides bash) or use WSL2'
149
- : 'required command not found: sh');
150
- }
151
- if (CODEX_SUBCOMMANDS.includes(cmd)) {
152
- const codexBin = env.SUPERPOWERS_CODEX || 'codex';
153
- // An explicit override may be a path rather than a PATH-resolvable name.
154
- const found = codexBin.includes(path.sep)
155
- ? fs.existsSync(codexBin)
156
- : Boolean(findTool(codexBin, env, platform));
157
- if (!found) {
158
- errors.push(`required command not found: ${codexBin} — install the Codex CLI or set SUPERPOWERS_CODEX`);
159
- }
160
- }
161
- if (errors.length) return { ok: false, errors };
162
- return { ok: true, shell: /** @type {string} */ (shell) };
163
- }
164
-
165
- // POSIX executes the script directly (#!/bin/sh shebang); Windows cannot
166
- // spawn extensionless scripts, so the discovered shell runs the script as
167
- // its first argument.
168
- /**
169
- * @param {Subcommand} cmd
170
- * @param {string[]} args
171
- * @param {string} root
172
- * @param {string} shell
173
- * @param {NodeJS.Platform} platform
174
- * @returns {SpawnDescriptor}
175
- */
176
- function buildSpawn(cmd, args, root, shell, platform) {
177
- const script = path.join(root, 'scripts', cmd);
178
- if (platform === 'win32') {
179
- return { file: shell, argv: [script, ...args] };
180
- }
181
- return { file: script, argv: args };
182
- }
183
-
184
- function usage() {
185
- return [
186
- 'usage: superpowers-manager [prepare|probe|install|update|uninstall] [args...]',
187
- '',
188
- ' prepare fetch the pinned upstream ref and generate the plugin tree',
189
- ' probe report upstream/generated/installed status (accepts --porcelain)',
190
- ' install register this package root as a Codex marketplace and install the plugin',
191
- ' update probe, then prepare/install only if needed (default when no subcommand)',
192
- ' uninstall remove the manager plugin and marketplace from Codex',
193
- '',
194
- 'Environment overrides (passed through to the scripts): SUPERPOWERS_REF,',
195
- 'SUPERPOWERS_UPSTREAM_URL, SUPERPOWERS_CODEX, SUPERPOWERS_CACHE_DIR,',
196
- 'SUPERPOWERS_PLUGIN_ROOT, SUPERPOWERS_MANIFEST_TEMPLATE,',
197
- 'SUPERPOWERS_VALIDATOR,',
198
- 'SUPERPOWERS_INSTALLED_SEARCH_ROOT, SUPERPOWERS_INSTALL_REFRESH_MODE',
199
- ].join('\n');
200
- }
201
-
202
- function main() {
203
- const parsed = parseArgs(process.argv.slice(2));
204
- if (parsed.kind === 'help') {
205
- console.log(usage());
206
- process.exit(0);
207
- }
208
- const root = resolvePackageRoot(import.meta.filename);
209
- if (!root) {
210
- console.error('error: cannot resolve the superpowers-manager package root');
211
- process.exit(1);
212
- }
213
- if (parsed.kind === 'version') {
214
- console.log(JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version);
215
- process.exit(0);
216
- }
217
- if (parsed.kind === 'usage-error') {
218
- console.error(`error: ${parsed.message}`);
219
- console.error(usage());
220
- process.exit(2);
221
- }
222
- const pf = preflight(parsed.cmd, process.env, process.platform);
223
- if (!pf.ok) {
224
- for (const e of pf.errors) console.error(`error: ${e}`);
225
- process.exit(1);
226
- }
227
- const script = path.join(root, 'scripts', parsed.cmd);
228
- if (!fs.existsSync(script)) {
229
- console.error(`error: missing script: ${script}`);
230
- process.exit(1);
231
- }
232
- const spawn = buildSpawn(parsed.cmd, parsed.args, root, pf.shell, process.platform);
233
- // env is inherited wholesale, so every SUPERPOWERS_* override passes through.
234
- const res = spawnSync(spawn.file, spawn.argv, { stdio: 'inherit', env: process.env });
235
- if (res.error) {
236
- console.error(`error: cannot run ${spawn.file}: ${res.error.message}`);
237
- process.exit(1);
238
- }
239
- process.exit(res.status === null ? 1 : res.status);
240
- }
241
-
242
- export { resolvePackageRoot, isMain, parseArgs, findTool, discoverShell, preflight, buildSpawn, usage };
243
-
244
- if (isMain(import.meta.filename, process.argv[1])) main();
15
+ const { main } = await import(cliUrl.href);
16
+ main();