superpowers-manager 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,7 @@ and explicit install/update/probe/uninstall lifecycle control.
31
31
 
32
32
  ```sh
33
33
  npx superpowers-wrapper@0.1.1 uninstall
34
- npx superpowers-manager@0.1.4 install
34
+ npx superpowers-manager install
35
35
  ```
36
36
 
37
37
  The manager detects legacy wrapper-owned Codex state and stops before mutation.
@@ -41,28 +41,33 @@ It never removes the legacy provider automatically.
41
41
 
42
42
  Superpowers Manager requires Node 24+, `git`, Python 3, and a POSIX `sh`.
43
43
  Codex CLI is required for `probe`, `install`, `update`, and `uninstall`;
44
- `prepare` does not require it.
44
+ `prepare`, `pin`, `track-latest`, and `unpin` do not require it. Codex is the
45
+ only supported integration today.
45
46
 
46
47
  macOS and Linux are tested. WSL2 is supported. The native Windows path is
47
48
  untested; the launcher looks for Git Bash, `git`, and `python3`, but path
48
49
  handling between MSYS and Codex remains a known risk area.
49
50
 
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.
51
+ Network needs depend on the saved policy. `pin` verifies its target;
52
+ `track-latest` and invocation overrides resolve from upstream when applied; and
53
+ `prepare`, `install`, and `update` fetch or verify the effective source. A probe
54
+ of a saved exact pin can reuse its recorded identity without upstream access.
55
+ Updates remain user-triggered; the manager does not run automatic or background
56
+ updates.
53
57
 
54
58
  ## What it does
55
59
 
56
60
  - Resolves an upstream ref, defaulting to the latest stable `vX.Y.Z` release
57
61
  tag.
58
- - Clones or fetches that exact upstream commit and assembles a Codex plugin tree
59
- under `plugins/superpowers/`.
60
- - Preserves the upstream manifest when available, while applying the
61
- manager-owned plugin contract.
62
- - Records `source`, `requested_ref`, `resolved_ref`, `commit`, and the upstream
63
- manifest version in `.superpowers-upstream.json`.
64
- - Validates the candidate tree before replacing a previous generated tree or
65
- mutating Codex state.
62
+ - Clones/fetches upstream at that commit and assembles a Codex plugin tree under
63
+ `plugins/superpowers/` (skills, assets, license/readme, and manifest;
64
+ upstream's `hooks/` directory is deliberately excluded).
65
+ - Stamps the generated manifest with a ref-aware manager version ending in
66
+ `+manager.<short-sha>` and writes the upstream provenance to
67
+ `.superpowers-upstream.json`.
68
+ - Validates the generated tree with the manager's shipped, Python-standard-library
69
+ contract validator before swapping it into place (a failed run never destroys
70
+ a previously generated tree).
66
71
  - Registers the `superpowers-manager` marketplace and installs or refreshes
67
72
  `superpowers@superpowers-manager` in Codex.
68
73
 
@@ -73,31 +78,29 @@ claim about how Superpowers should be packaged for future or other harnesses.
73
78
  Changing the hook-free policy requires a separate design and current
74
79
  compatibility evidence.
75
80
 
76
- ## Choosing the upstream version
77
-
78
- `SUPERPOWERS_REF` selects a stable release tag, full commit SHA, branch, or
79
- other resolvable upstream ref for that invocation. The stateless npx package
80
- records the requested ref, resolved ref, and exact commit, but does not persist
81
- the selection for a later invocation that omits `SUPERPOWERS_REF`.
81
+ ## Runtime architecture
82
82
 
83
- Without `SUPERPOWERS_REF`, the manager reads `config/upstream-ref`, which ships
84
- as `latest-release`. Accepted values include:
83
+ - `scripts/core/` owns the shared lifecycle, status, and protocol validation;
84
+ see the [version-1 adapter response protocol](https://github.com/j7an/superpowers-manager/blob/main/docs/adapter-protocol.md).
85
+ - `scripts/adapters/codex/` owns build, inspection, reconciliation, and Codex
86
+ mutation.
87
+ - Codex is the only supported adapter today; no public harness selector ships
88
+ yet.
85
89
 
86
- - `latest-release` — the highest stable `vX.Y.Z` tag; prereleases are excluded.
87
- - A specific tag, such as `v6.0.3`.
88
- - A full 40-character commit SHA.
89
- - Any other ref upstream resolves, such as `main` or a branch name.
90
+ Validation checks the manager-owned manifest overlay, generated-tree structure,
91
+ skill frontmatter envelope, known local paths, and provenance. It deliberately
92
+ does not implement a general YAML parser or mirror every Codex ingestion rule;
93
+ upstream owns skill semantics and Codex owns its evolving schema.
90
94
 
91
- ```sh
92
- SUPERPOWERS_REF=v6.0.3 npx superpowers-manager prepare
93
- SUPERPOWERS_REF=main npx superpowers-manager probe
94
- SUPERPOWERS_REF=feature/foo npx superpowers-manager install
95
- SUPERPOWERS_REF=latest-release npx superpowers-manager update
96
- ```
95
+ `SUPERPOWERS_VALIDATOR=/path/to/validator.py` adds an optional Python validator
96
+ after the built-in check. It receives the candidate plugin root as its only
97
+ argument. It cannot replace or bypass built-in validation, and either check
98
+ failing prevents the tree swap and all Codex mutation.
97
99
 
98
- `SUPERPOWERS_CACHE_DIR` may point to a persistent upstream clone cache to avoid
99
- re-cloning between package materializations. It caches Git objects; it does not
100
- persist ref selection or trigger updates.
100
+ Direct `scripts/install` keeps harness-specific validation in the Codex adapter:
101
+ phase 1 prepares the exact candidate first, then the adapter performs its Codex
102
+ and refresh-mode preflight before any Codex mutation. The Node dispatcher keeps
103
+ its existing Codex preflight before dispatching to the shell lifecycle.
101
104
 
102
105
  ## Provider ownership
103
106
 
@@ -123,6 +126,9 @@ manager will not remove it for you.
123
126
 
124
127
  | Command | Codex side effects | Purpose |
125
128
  |---|---|---|
129
+ | `npx superpowers-manager pin REF` | None | Resolve and save an exact upstream release tag or full commit |
130
+ | `npx superpowers-manager track-latest` | None | Save a policy that resolves the latest stable release when applied |
131
+ | `npx superpowers-manager unpin` | None | Remove saved selection and return to the packaged fallback policy |
126
132
  | `npx superpowers-manager prepare` | None | Resolve upstream, build a staged plugin tree, validate it, and replace the generated tree only on success |
127
133
  | `npx superpowers-manager probe` | None | Report requested and resolved refs, desired/generated/installed commits, identity state, and status |
128
134
  | `npx superpowers-manager install` | Marketplace and plugin state | Prepare and validate, register the marketplace, install the plugin, and verify installed state |
@@ -130,74 +136,166 @@ manager will not remove it for you.
130
136
  | `npx superpowers-manager uninstall` | Marketplace and plugin state | Remove only manager-owned Codex state and verify removal |
131
137
 
132
138
  Calling `npx superpowers-manager` without a subcommand is equivalent to
133
- `update`. `probe` is read-only. Install and update prepare and validate before changing Codex state.
134
- Uninstall inspects and removes only manager-owned Codex state. Those mutating
135
- commands fail closed when required state cannot be inspected and refuse to
136
- report success when the resulting state cannot be verified.
139
+ `update`. `probe` is read-only. Install and update stop before prepare or Codex
140
+ mutation when legacy state is present. Uninstall removes only manager-owned
141
+ state and reports any legacy residue without modifying it. These commands fail
142
+ closed when required state cannot be inspected and refuse to report success
143
+ when resulting manager state cannot be verified.
137
144
 
138
- ### Refresh modes
145
+ ## Choosing the upstream version
139
146
 
140
- `install` and `update` accept `SUPERPOWERS_INSTALL_REFRESH_MODE`:
147
+ Save a tag pin, commit pin, or explicit latest-stable policy for future
148
+ invocations:
149
+
150
+ ```sh
151
+ npx superpowers-manager pin v6.1.1
152
+ npx superpowers-manager pin 0123456789abcdef0123456789abcdef01234567 # replace with a real upstream commit
153
+ npx superpowers-manager track-latest
154
+ npx superpowers-manager unpin
155
+ ```
141
156
 
142
- - `add-only` (default) asks Codex to add the plugin from the current local
143
- marketplace source.
144
- - `remove-add` removes only the manager-owned plugin before adding it again.
157
+ The selection commands save intent only; they do not prepare generated content
158
+ or change Codex state. Run `npx superpowers-manager install` for a first
159
+ installation or `npx superpowers-manager update` to apply the saved policy to
160
+ an existing installation. There is no background updater: selection changes
161
+ take effect only through a user-triggered `prepare`, `install`, or `update`.
145
162
 
146
- Use `remove-add` only when an add-only refresh fails to take:
163
+ `pin` accepts only an exact `vMAJOR.MINOR.PATCH` tag (optionally with a SemVer
164
+ prerelease suffix) or a full 40-character commit. It resolves and verifies the
165
+ target before saving both the exact commit identity and its source. The saved
166
+ identity is the contract, not the contents of a clone cache: clearing or moving
167
+ `SUPERPOWERS_CACHE_DIR` does not change the selection. `track-latest` saves the
168
+ source and resolves the highest stable `vX.Y.Z` tag each time the policy is
169
+ applied. `unpin` removes only the saved selection and restores the packaged
170
+ `config/upstream-ref` fallback, currently `latest-release`.
171
+
172
+ The user-wide selection file is `selection.json` under the first configured
173
+ location:
174
+
175
+ 1. `$SUPERPOWERS_CONFIG_DIR`
176
+ 2. `$XDG_CONFIG_HOME/superpowers-manager`
177
+ 3. `$HOME/.config/superpowers-manager`
178
+
179
+ Each location must be absolute. The ref and source precedence chains are
180
+ independent:
181
+
182
+ - Ref: `SUPERPOWERS_REF`, then saved selection, then `config/upstream-ref`.
183
+ - Source: `SUPERPOWERS_UPSTREAM_URL`, then saved source, then the official
184
+ `https://github.com/obra/superpowers` source.
185
+
186
+ `SUPERPOWERS_REF` is an invocation-only override and is not persisted. Unlike
187
+ an exact saved pin, it accepts stable tags, full commit SHAs, branches, and
188
+ other resolvable upstream refs for that invocation:
147
189
 
148
190
  ```sh
149
- SUPERPOWERS_INSTALL_REFRESH_MODE=remove-add npx superpowers-manager update
191
+ SUPERPOWERS_REF=feature/foo npx superpowers-manager probe
150
192
  ```
151
193
 
152
- ## How generated versions work
194
+ `pin` and `track-latest` bind the current `SUPERPOWERS_UPSTREAM_URL`, or the
195
+ official source when that variable is unset, to the saved intent. An invocation
196
+ may override only the ref or only the source; `probe` exposes
197
+ `selection_origin`, `selection_mode`, `upstream_source_origin`,
198
+ `effective_source`, the saved-selection fields, and a mixed-origin warning in
199
+ human output so that combination remains visible.
200
+
201
+ A saved exact pin lets `probe` determine the desired upstream identity without
202
+ contacting Git, even when the saved source is temporarily unavailable. This is
203
+ not a general offline installation promise: `pin` verifies the requested
204
+ target, `track-latest` must resolve when applied, and `prepare`/`install`/`update`
205
+ must fetch or verify content from the effective source. Codex inspection is
206
+ still required for lifecycle status.
207
+
208
+ HTTP(S) upstream URLs containing userinfo are now rejected, including
209
+ token-only userinfo. This is an intentional compatibility break: use a Git
210
+ credential helper for HTTP(S) authentication or an SSH source instead of
211
+ embedding credentials in the URL.
212
+
213
+ Saved selection does not claim or transfer provider ownership. The manager
214
+ continues to mutate only `superpowers@superpowers-manager` and never removes or
215
+ updates the official provider or any other provider automatically.
216
+
217
+ ## How versioning works
218
+
219
+ - **Upstream manifest first:** when upstream provides
220
+ `.codex-plugin/plugin.json`, `prepare` uses it as the generated manifest base
221
+ so future upstream metadata fields are preserved by default.
222
+ - **Fallback template:** `plugins/superpowers/.codex-plugin/plugin.template.json`
223
+ is committed as a minimal fallback for older upstream refs that do not ship a
224
+ Codex manifest. It carries the placeholder version
225
+ `0.0.0+manager.template`.
226
+ - **Manager overlay:** `prepare` replaces the version with a ref-aware manager
227
+ version, forces `skills` to `./skills/`, and enforces the manager's current
228
+ hook-free policy: no manifest `hooks` key and no copied `hooks/` directory.
229
+ Unknown upstream manifest fields remain preserved.
230
+ - Stable tags generate release-looking versions such as
231
+ `6.0.3+manager.896224c`; explicit prerelease tags generate versions such as
232
+ `6.1.0-beta.1+manager.abc1234`.
233
+ - Branch builds deliberately stay below real releases:
234
+ `main` generates `0.0.0-main+manager.<short-sha>` and other named refs
235
+ generate `0.0.0-ref-<sanitized-ref>+manager.<short-sha>`.
236
+ - Raw 40-character commit SHAs generate `0.0.0+manager.<short-sha>`.
237
+ - **`.superpowers-upstream.json`** records the authoritative provenance:
238
+ `source`, `requested_ref`, `resolved_ref`, `commit`, and the upstream manifest
239
+ version. The generated manifest version is for human readability and Codex
240
+ package identity; the upstream `commit` is what `probe`/`update` compare
241
+ against.
242
+
243
+ ## Refresh modes
153
244
 
154
- - If upstream provides `.codex-plugin/plugin.json`, the manager uses it as the
155
- manifest base and preserves unknown upstream fields.
156
- - The committed fallback template supports older refs without an upstream
157
- manifest and uses the placeholder version `0.0.0+manager.template`.
158
- - Stable tags produce versions such as `6.0.3+manager.896224c`; explicit
159
- prereleases produce versions such as `6.1.0-beta.1+manager.abc1234`.
160
- - `main` produces `0.0.0-main+manager.<short-sha>`; other named refs produce
161
- `0.0.0-ref-<sanitized-ref>+manager.<short-sha>`.
162
- - Full commit SHAs produce `0.0.0+manager.<short-sha>`.
245
+ `install` and `update` accept `SUPERPOWERS_INSTALL_REFRESH_MODE`:
163
246
 
164
- The generated version is for readable Codex package identity. The exact commit
165
- in `.superpowers-upstream.json` is the authoritative value compared by `probe`
166
- and `update`.
247
+ - `add-only` (default) — `plugin add` re-reads the local source, which refreshes
248
+ a mutated tree. Verified sufficient for local marketplaces.
249
+ - `remove-add` — removes the manager's own plugin first, then re-adds it. Use
250
+ only if a refresh ever fails to take:
167
251
 
168
- `SUPERPOWERS_VALIDATOR=/path/to/validator.py` adds an optional Python validator
169
- after the built-in generated-tree validation. It receives the candidate plugin
170
- root as its only argument. It cannot bypass the built-in validator, and either
171
- check failing prevents replacement and Codex mutation.
252
+ ```sh
253
+ SUPERPOWERS_INSTALL_REFRESH_MODE=remove-add npx superpowers-manager update
254
+ ```
172
255
 
173
256
  ## Tests
174
257
 
175
258
  ```sh
176
- sh tests/run.sh # Layers 1-3: hermetic, no network or real Codex state
177
- sh tests/container.sh # Layer 4: blocking isolated real-Codex CLI check
259
+ sh tests/run.sh # Layers 1-3: host-side hermetic checks while iterating
260
+ sh tests/container.sh # Layers 1-4: blocking Docker acceptance command
261
+ sh tests/manual/codex-behavior-probe.sh # optional native-only compatibility residue
178
262
  ```
179
263
 
180
- The container suite runs with networking disabled and a throwaway home, so it
181
- may mutate only isolated container state. The optional
182
- `tests/manual/codex-behavior-probe.sh` is reserved for intentional native-only
183
- compatibility residue and is not part of acceptance.
264
+ Layers 1-3 stay offline and hermetic: they use a fake local upstream repo plus
265
+ host-side fixtures, and they perform no mutation of the developer's or runner's
266
+ real Codex state.
267
+
268
+ Layer 4 is the Docker acceptance path. It is the required completion command
269
+ because the isolated-container Codex probe graduated from a temporary
270
+ nonblocking spike to a blocking acceptance gate. `sh tests/container.sh` runs
271
+ the inner `sh tests/run.sh` suite and then the real Codex offline probe inside
272
+ an isolated container home with networking disabled. That container run may
273
+ mutate the throwaway container-local Codex state, but it still performs no
274
+ mutation of the developer's or runner's real Codex state.
275
+
276
+ The manual probe is opt-in and covers native-only compatibility residue such as
277
+ path/cache behavior against an intentionally real local Codex install. It is
278
+ not part of acceptance. GitHub Actions runs the blocking container acceptance
279
+ command on pull requests and pushes to `main`.
184
280
 
185
281
  ## Repository layout
186
282
 
187
- ```text
188
- .agents/plugins/marketplace.json # local manager marketplace definition
189
- bin/superpowers-manager.js # npm CLI launcher
190
- config/upstream-ref # default upstream ref
283
+ ```
284
+ .agents/plugins/marketplace.json # local marketplace definition (tracked)
285
+ config/upstream-ref # which upstream ref to track (tracked)
191
286
  plugins/superpowers/
192
- .codex-plugin/plugin.template.json # tracked fallback manifest
193
- .codex-plugin/plugin.json # generated manifest (ignored)
194
- skills/ assets/ LICENSE ... # generated from upstream (ignored)
195
- .superpowers-upstream.json # generated provenance (ignored)
196
- scripts/ # prepare/probe/install/update/uninstall
197
- tests/ # hermetic and isolated-container suites
198
- .cache/upstream/ # optional upstream clone cache (ignored)
287
+ .codex-plugin/plugin.template.json # fallback manifest template (tracked)
288
+ .codex-plugin/plugin.json # generated manifest (gitignored)
289
+ skills/ assets/ LICENSE ... # generated from upstream (gitignored)
290
+ .superpowers-upstream.json # generated provenance (gitignored)
291
+ scripts/
292
+ adapters/codex/ # Codex adapter entrypoint + validator helpers
293
+ core/ # shared lifecycle, selection, provenance, and status modules
294
+ pin track-latest unpin # persistent-selection entrypoints
295
+ prepare probe install update uninstall # lifecycle entrypoints
296
+ tests/ # hermetic suite + manual Codex probe
297
+ .cache/upstream/ # upstream clone cache (gitignored)
199
298
  ```
200
299
 
201
- Everything under `plugins/superpowers/` except the fallback manifest template
202
- is generated by `prepare` and ignored by Git. Do not edit generated plugin
203
- content; regenerate it instead.
300
+ Everything under `plugins/superpowers/` except the fallback manifest template is
301
+ generated by `prepare` and ignored by Git; re-run `prepare` to regenerate it.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
2
+ // @ts-check
3
3
 
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { spawnSync } = require('child_process');
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
7
 
8
8
  /**
9
- * @typedef {'prepare' | 'probe' | 'install' | 'update' | 'uninstall'} Subcommand
9
+ * @typedef {'pin' | 'track-latest' | 'unpin' | 'prepare' | 'probe' | 'install' | 'update' | 'uninstall'} Subcommand
10
10
  */
11
11
 
12
12
  /**
@@ -27,8 +27,22 @@ const { spawnSync } = require('child_process');
27
27
  * @typedef {{ file: string, argv: string[] }} SpawnDescriptor
28
28
  */
29
29
 
30
- const SUBCOMMANDS = ['prepare', 'probe', 'install', 'update', 'uninstall'];
31
- const CODEX_SUBCOMMANDS = ['probe', 'install', 'update', 'uninstall'];
30
+ const SUBCOMMANDS = [
31
+ 'pin', 'track-latest', 'unpin', 'prepare', 'probe', 'install', 'update', 'uninstall',
32
+ ];
33
+ const PIN_TAG_RE = /^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?$/;
34
+ const PIN_COMMIT_RE = /^[0-9A-Fa-f]{40}$/;
35
+ /** @type {Record<Subcommand, string[]>} */
36
+ const COMMAND_REQUIREMENTS = {
37
+ pin: ['git', 'python3'],
38
+ 'track-latest': ['python3'],
39
+ unpin: [],
40
+ prepare: ['git', 'python3'],
41
+ probe: ['git', 'python3', 'codex'],
42
+ install: ['git', 'python3', 'codex'],
43
+ update: ['git', 'python3', 'codex'],
44
+ uninstall: ['python3', 'codex'],
45
+ };
32
46
  // Mirrors upstream Superpowers' hooks/run-hook.cmd discovery order.
33
47
  const GIT_BASH_CANDIDATES = [
34
48
  'C:\\Program Files\\Git\\bin\\bash.exe',
@@ -57,18 +71,41 @@ function resolvePackageRoot(scriptPath) {
57
71
  }
58
72
  }
59
73
 
74
+ /**
75
+ * @param {string} moduleFilename
76
+ * @param {string | undefined} argvPath
77
+ * @returns {boolean}
78
+ */
79
+ function isMain(moduleFilename, argvPath) {
80
+ if (!argvPath) return false;
81
+ return moduleFilename === fs.realpathSync(argvPath);
82
+ }
83
+
60
84
  /**
61
85
  * @param {string[]} argv
62
86
  * @returns {ParseResult}
63
87
  */
64
88
  function parseArgs(argv) {
65
- if (argv.length === 0) return { kind: 'run', cmd: 'update', args: [] };
66
89
  /** @type {string | undefined} */
67
90
  const first = argv[0];
91
+ if (argv.length === 0) return { kind: 'run', cmd: 'update', args: [] };
68
92
  if (first === '--help' || first === '-h') return { kind: 'help' };
69
93
  if (first === '--version') return { kind: 'version' };
70
94
  if (first && SUBCOMMANDS.includes(first)) {
71
- return { kind: 'run', cmd: /** @type {Subcommand} */ (first), args: argv.slice(1) };
95
+ const args = argv.slice(1);
96
+ if (first === 'pin' && args.length !== 1) {
97
+ return { kind: 'usage-error', message: 'usage: superpowers-manager pin REF' };
98
+ }
99
+ if (first === 'pin' && !PIN_TAG_RE.test(args[0]) && !PIN_COMMIT_RE.test(args[0])) {
100
+ return {
101
+ kind: 'usage-error',
102
+ message: 'pin REF must be an exact v-prefixed SemVer tag or full 40-hex commit',
103
+ };
104
+ }
105
+ if ((first === 'track-latest' || first === 'unpin') && args.length !== 0) {
106
+ return { kind: 'usage-error', message: `usage: superpowers-manager ${first}` };
107
+ }
108
+ return { kind: 'run', cmd: /** @type {Subcommand} */ (first), args };
72
109
  }
73
110
  return { kind: 'usage-error', message: `unknown subcommand: ${first}` };
74
111
  }
@@ -117,8 +154,15 @@ function discoverShell(env, platform) {
117
154
  return findTool('bash', env, platform);
118
155
  }
119
156
 
120
- // Tool preflight; never touches Codex state. codex is required for subcommands
121
- // whose scripts read or mutate Codex state.
157
+ /**
158
+ * @returns {Record<Subcommand, string[]>}
159
+ */
160
+ function commandRequirements() {
161
+ return COMMAND_REQUIREMENTS;
162
+ }
163
+
164
+ // Tool preflight; never touches Codex state. Requirements are specific to the
165
+ // selected command, while a POSIX shell remains mandatory for every command.
122
166
  /**
123
167
  * @param {Subcommand} cmd
124
168
  * @param {NodeJS.ProcessEnv} env
@@ -127,8 +171,17 @@ function discoverShell(env, platform) {
127
171
  */
128
172
  function preflight(cmd, env, platform) {
129
173
  const errors = [];
130
- for (const tool of ['git', 'python3']) {
131
- if (!findTool(tool, env, platform)) {
174
+ for (const tool of COMMAND_REQUIREMENTS[cmd]) {
175
+ if (tool === 'codex') {
176
+ const codexBin = env.SUPERPOWERS_CODEX || 'codex';
177
+ // An explicit override may be a path rather than a PATH-resolvable name.
178
+ const found = codexBin.includes(path.sep)
179
+ ? fs.existsSync(codexBin)
180
+ : Boolean(findTool(codexBin, env, platform));
181
+ if (!found) {
182
+ errors.push(`required command not found: ${codexBin} — install the Codex CLI or set SUPERPOWERS_CODEX`);
183
+ }
184
+ } else if (!findTool(tool, env, platform)) {
132
185
  errors.push(`required command not found: ${tool} — install ${tool} and re-run`);
133
186
  }
134
187
  }
@@ -138,19 +191,8 @@ function preflight(cmd, env, platform) {
138
191
  ? 'no POSIX shell found — install Git for Windows (provides bash) or use WSL2'
139
192
  : 'required command not found: sh');
140
193
  }
141
- if (CODEX_SUBCOMMANDS.includes(cmd)) {
142
- const codexBin = env.SUPERPOWERS_CODEX || 'codex';
143
- // An explicit override may be a path rather than a PATH-resolvable name.
144
- const found = codexBin.includes(path.sep)
145
- ? fs.existsSync(codexBin)
146
- : Boolean(findTool(codexBin, env, platform));
147
- if (!found) {
148
- errors.push(`required command not found: ${codexBin} — install the Codex CLI or set SUPERPOWERS_CODEX`);
149
- }
150
- }
151
- return errors.length
152
- ? { ok: false, errors }
153
- : { ok: true, shell: /** @type {string} */ (shell) };
194
+ if (errors.length) return { ok: false, errors };
195
+ return { ok: true, shell: /** @type {string} */ (shell) };
154
196
  }
155
197
 
156
198
  // POSIX executes the script directly (#!/bin/sh shebang); Windows cannot
@@ -174,8 +216,14 @@ function buildSpawn(cmd, args, root, shell, platform) {
174
216
 
175
217
  function usage() {
176
218
  return [
177
- 'usage: superpowers-manager [prepare|probe|install|update|uninstall] [args...]',
219
+ 'usage: superpowers-manager [command] [args...]',
178
220
  '',
221
+ 'Selection commands (save intent only; they do not prepare or install it):',
222
+ ' pin REF save an exact upstream release tag or commit',
223
+ ' track-latest save selection of the latest stable upstream release',
224
+ ' unpin remove the saved selection and return to the packaged fallback',
225
+ '',
226
+ 'Apply and lifecycle commands:',
179
227
  ' prepare fetch the pinned upstream ref and generate the plugin tree',
180
228
  ' probe report upstream/generated/installed status (accepts --porcelain)',
181
229
  ' install register this package root as a Codex marketplace and install the plugin',
@@ -184,9 +232,13 @@ function usage() {
184
232
  '',
185
233
  'Environment overrides (passed through to the scripts): SUPERPOWERS_REF,',
186
234
  'SUPERPOWERS_UPSTREAM_URL, SUPERPOWERS_CODEX, SUPERPOWERS_CACHE_DIR,',
235
+ 'SUPERPOWERS_CONFIG_DIR, XDG_CONFIG_HOME,',
187
236
  'SUPERPOWERS_PLUGIN_ROOT, SUPERPOWERS_MANIFEST_TEMPLATE,',
188
237
  'SUPERPOWERS_VALIDATOR,',
189
238
  'SUPERPOWERS_INSTALLED_SEARCH_ROOT, SUPERPOWERS_INSTALL_REFRESH_MODE',
239
+ '',
240
+ 'Selection state uses SUPERPOWERS_CONFIG_DIR when set; otherwise it uses',
241
+ '$XDG_CONFIG_HOME/superpowers-manager, then $HOME/.config/superpowers-manager.',
190
242
  ].join('\n');
191
243
  }
192
244
 
@@ -196,7 +248,7 @@ function main() {
196
248
  console.log(usage());
197
249
  process.exit(0);
198
250
  }
199
- const root = resolvePackageRoot(__filename);
251
+ const root = resolvePackageRoot(import.meta.filename);
200
252
  if (!root) {
201
253
  console.error('error: cannot resolve the superpowers-manager package root');
202
254
  process.exit(1);
@@ -230,6 +282,9 @@ function main() {
230
282
  process.exit(res.status === null ? 1 : res.status);
231
283
  }
232
284
 
233
- module.exports = { resolvePackageRoot, parseArgs, findTool, discoverShell, preflight, buildSpawn, usage };
285
+ export {
286
+ resolvePackageRoot, isMain, parseArgs, findTool, discoverShell,
287
+ commandRequirements, preflight, buildSpawn, usage,
288
+ };
234
289
 
235
- if (require.main === module) main();
290
+ if (isMain(import.meta.filename, process.argv[1])) main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superpowers-manager",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Install and update the latest stable obra/superpowers release directly from upstream, without waiting for agent marketplaces to catch up. Codex supported today.",
5
5
  "keywords": [
6
6
  "superpowers",
@@ -17,6 +17,7 @@
17
17
  "updater"
18
18
  ],
19
19
  "license": "MIT",
20
+ "type": "module",
20
21
  "repository": {
21
22
  "type": "git",
22
23
  "url": "git+https://github.com/j7an/superpowers-manager.git"