superpowers-manager 0.1.6 → 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 +79 -24
- package/bin/superpowers-manager.js +66 -20
- package/package.json +1 -1
- package/scripts/adapters/codex/adapter +55 -50
- package/scripts/adapters/codex/lib.sh +75 -17
- package/scripts/core/adapter.sh +13 -2
- package/scripts/core/common.sh +24 -0
- package/scripts/core/lifecycle.sh +14 -0
- package/scripts/core/selection-state.py +414 -0
- package/scripts/core/selection.sh +168 -0
- package/scripts/core/upstream.sh +137 -3
- package/scripts/core/validate-adapter-response.py +42 -3
- package/scripts/install +12 -13
- package/scripts/pin +51 -0
- package/scripts/prepare +30 -39
- package/scripts/probe +46 -25
- package/scripts/track-latest +30 -0
- package/scripts/uninstall +2 -11
- package/scripts/unpin +41 -0
- package/scripts/update +5 -2
package/README.md
CHANGED
|
@@ -41,15 +41,19 @@ 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`
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
|
@@ -76,7 +80,8 @@ compatibility evidence.
|
|
|
76
80
|
|
|
77
81
|
## Runtime architecture
|
|
78
82
|
|
|
79
|
-
- `scripts/core/` owns the shared lifecycle, status, and protocol validation
|
|
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).
|
|
80
85
|
- `scripts/adapters/codex/` owns build, inspection, reconciliation, and Codex
|
|
81
86
|
mutation.
|
|
82
87
|
- Codex is the only supported adapter today; no public harness selector ships
|
|
@@ -121,6 +126,9 @@ manager will not remove it for you.
|
|
|
121
126
|
|
|
122
127
|
| Command | Codex side effects | Purpose |
|
|
123
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 |
|
|
124
132
|
| `npx superpowers-manager prepare` | None | Resolve upstream, build a staged plugin tree, validate it, and replace the generated tree only on success |
|
|
125
133
|
| `npx superpowers-manager probe` | None | Report requested and resolved refs, desired/generated/installed commits, identity state, and status |
|
|
126
134
|
| `npx superpowers-manager install` | Marketplace and plugin state | Prepare and validate, register the marketplace, install the plugin, and verify installed state |
|
|
@@ -136,29 +144,75 @@ when resulting manager state cannot be verified.
|
|
|
136
144
|
|
|
137
145
|
## Choosing the upstream version
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
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`.
|
|
147
|
+
Save a tag pin, commit pin, or explicit latest-stable policy for future
|
|
148
|
+
invocations:
|
|
143
149
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
```
|
|
156
|
+
|
|
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`.
|
|
162
|
+
|
|
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.
|
|
146
185
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- Any other ref upstream resolves (e.g. a branch name).
|
|
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:
|
|
151
189
|
|
|
152
190
|
```sh
|
|
153
|
-
SUPERPOWERS_REF=
|
|
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
|
|
191
|
+
SUPERPOWERS_REF=feature/foo npx superpowers-manager probe
|
|
157
192
|
```
|
|
158
193
|
|
|
159
|
-
`
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
162
216
|
|
|
163
217
|
## How versioning works
|
|
164
218
|
|
|
@@ -236,8 +290,9 @@ plugins/superpowers/
|
|
|
236
290
|
.superpowers-upstream.json # generated provenance (gitignored)
|
|
237
291
|
scripts/
|
|
238
292
|
adapters/codex/ # Codex adapter entrypoint + validator helpers
|
|
239
|
-
core/ # shared lifecycle
|
|
240
|
-
|
|
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
|
|
241
296
|
tests/ # hermetic suite + manual Codex probe
|
|
242
297
|
.cache/upstream/ # upstream clone cache (gitignored)
|
|
243
298
|
```
|
|
@@ -6,7 +6,7 @@ import * as path from 'node:path';
|
|
|
6
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 @@ import { spawnSync } from 'node:child_process';
|
|
|
27
27
|
* @typedef {{ file: string, argv: string[] }} SpawnDescriptor
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
const SUBCOMMANDS = [
|
|
31
|
-
|
|
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',
|
|
@@ -78,7 +92,20 @@ function parseArgs(argv) {
|
|
|
78
92
|
if (first === '--help' || first === '-h') return { kind: 'help' };
|
|
79
93
|
if (first === '--version') return { kind: 'version' };
|
|
80
94
|
if (first && SUBCOMMANDS.includes(first)) {
|
|
81
|
-
|
|
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 };
|
|
82
109
|
}
|
|
83
110
|
return { kind: 'usage-error', message: `unknown subcommand: ${first}` };
|
|
84
111
|
}
|
|
@@ -127,8 +154,15 @@ function discoverShell(env, platform) {
|
|
|
127
154
|
return findTool('bash', env, platform);
|
|
128
155
|
}
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
132
166
|
/**
|
|
133
167
|
* @param {Subcommand} cmd
|
|
134
168
|
* @param {NodeJS.ProcessEnv} env
|
|
@@ -137,8 +171,17 @@ function discoverShell(env, platform) {
|
|
|
137
171
|
*/
|
|
138
172
|
function preflight(cmd, env, platform) {
|
|
139
173
|
const errors = [];
|
|
140
|
-
for (const tool of [
|
|
141
|
-
if (
|
|
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)) {
|
|
142
185
|
errors.push(`required command not found: ${tool} — install ${tool} and re-run`);
|
|
143
186
|
}
|
|
144
187
|
}
|
|
@@ -148,16 +191,6 @@ function preflight(cmd, env, platform) {
|
|
|
148
191
|
? 'no POSIX shell found — install Git for Windows (provides bash) or use WSL2'
|
|
149
192
|
: 'required command not found: sh');
|
|
150
193
|
}
|
|
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
194
|
if (errors.length) return { ok: false, errors };
|
|
162
195
|
return { ok: true, shell: /** @type {string} */ (shell) };
|
|
163
196
|
}
|
|
@@ -183,8 +216,14 @@ function buildSpawn(cmd, args, root, shell, platform) {
|
|
|
183
216
|
|
|
184
217
|
function usage() {
|
|
185
218
|
return [
|
|
186
|
-
'usage: superpowers-manager [
|
|
219
|
+
'usage: superpowers-manager [command] [args...]',
|
|
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',
|
|
187
225
|
'',
|
|
226
|
+
'Apply and lifecycle commands:',
|
|
188
227
|
' prepare fetch the pinned upstream ref and generate the plugin tree',
|
|
189
228
|
' probe report upstream/generated/installed status (accepts --porcelain)',
|
|
190
229
|
' install register this package root as a Codex marketplace and install the plugin',
|
|
@@ -193,9 +232,13 @@ function usage() {
|
|
|
193
232
|
'',
|
|
194
233
|
'Environment overrides (passed through to the scripts): SUPERPOWERS_REF,',
|
|
195
234
|
'SUPERPOWERS_UPSTREAM_URL, SUPERPOWERS_CODEX, SUPERPOWERS_CACHE_DIR,',
|
|
235
|
+
'SUPERPOWERS_CONFIG_DIR, XDG_CONFIG_HOME,',
|
|
196
236
|
'SUPERPOWERS_PLUGIN_ROOT, SUPERPOWERS_MANIFEST_TEMPLATE,',
|
|
197
237
|
'SUPERPOWERS_VALIDATOR,',
|
|
198
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.',
|
|
199
242
|
].join('\n');
|
|
200
243
|
}
|
|
201
244
|
|
|
@@ -239,6 +282,9 @@ function main() {
|
|
|
239
282
|
process.exit(res.status === null ? 1 : res.status);
|
|
240
283
|
}
|
|
241
284
|
|
|
242
|
-
export {
|
|
285
|
+
export {
|
|
286
|
+
resolvePackageRoot, isMain, parseArgs, findTool, discoverShell,
|
|
287
|
+
commandRequirements, preflight, buildSpawn, usage,
|
|
288
|
+
};
|
|
243
289
|
|
|
244
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.
|
|
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",
|
|
@@ -203,24 +203,15 @@ spw_codex_build() {
|
|
|
203
203
|
[ -f "$validator" ] || spw_codex_failure build "build-failed" "missing built-in plugin validator: $validator" "" ""
|
|
204
204
|
|
|
205
205
|
tmp_parent="${TMPDIR:-/tmp}"
|
|
206
|
-
if ! scratch=$(
|
|
206
|
+
if ! scratch=$(spw_make_workspace "$tmp_parent" "superpowers-manager.adapter-build"); then
|
|
207
207
|
spw_codex_failure build "build-failed" "cannot create adapter build workspace under $tmp_parent" "" ""
|
|
208
208
|
fi
|
|
209
|
+
spw_install_workspace_trap "$scratch"
|
|
209
210
|
stdout_file="$scratch/stdout"
|
|
210
211
|
stderr_file="$scratch/stderr"
|
|
211
212
|
messages_file="$scratch/messages"
|
|
212
213
|
candidate_manifest="$candidate_root/.codex-plugin/plugin.json"
|
|
213
214
|
provenance_path="$candidate_root/.superpowers-upstream.json"
|
|
214
|
-
cleanup() {
|
|
215
|
-
status=$?
|
|
216
|
-
trap - 0 HUP INT TERM
|
|
217
|
-
rm -rf "$scratch" || :
|
|
218
|
-
exit "$status"
|
|
219
|
-
}
|
|
220
|
-
trap cleanup 0
|
|
221
|
-
trap 'exit 129' HUP
|
|
222
|
-
trap 'exit 130' INT
|
|
223
|
-
trap 'exit 143' TERM
|
|
224
215
|
|
|
225
216
|
mkdir -p "$candidate_root/.codex-plugin"
|
|
226
217
|
: > "$messages_file"
|
|
@@ -250,7 +241,7 @@ spw_codex_build() {
|
|
|
250
241
|
|
|
251
242
|
if python3 -S "$validator" \
|
|
252
243
|
--plugin-root "$candidate_root" \
|
|
253
|
-
--source
|
|
244
|
+
--source="$upstream_source" \
|
|
254
245
|
--requested-ref "$requested_ref" \
|
|
255
246
|
--resolved-ref "$resolved_ref" \
|
|
256
247
|
--commit "$commit" \
|
|
@@ -302,23 +293,14 @@ spw_codex_install() {
|
|
|
302
293
|
esac
|
|
303
294
|
|
|
304
295
|
tmp_parent="${TMPDIR:-/tmp}"
|
|
305
|
-
if ! scratch=$(
|
|
296
|
+
if ! scratch=$(spw_make_workspace "$tmp_parent" "superpowers-manager.adapter-install"); then
|
|
306
297
|
spw_codex_failure install "install-failed" "cannot create adapter install workspace under $tmp_parent" "" ""
|
|
307
298
|
fi
|
|
299
|
+
spw_install_workspace_trap "$scratch"
|
|
308
300
|
stdout_file="$scratch/stdout"
|
|
309
301
|
stderr_file="$scratch/stderr"
|
|
310
302
|
messages_file="$scratch/messages"
|
|
311
303
|
hints_file="$scratch/hints"
|
|
312
|
-
cleanup() {
|
|
313
|
-
status=$?
|
|
314
|
-
trap - 0 HUP INT TERM
|
|
315
|
-
rm -rf "$scratch" || :
|
|
316
|
-
exit "$status"
|
|
317
|
-
}
|
|
318
|
-
trap cleanup 0
|
|
319
|
-
trap 'exit 129' HUP
|
|
320
|
-
trap 'exit 130' INT
|
|
321
|
-
trap 'exit 143' TERM
|
|
322
304
|
|
|
323
305
|
: > "$messages_file"
|
|
324
306
|
: > "$hints_file"
|
|
@@ -426,22 +408,13 @@ spw_codex_uninstall() {
|
|
|
426
408
|
spw_codex_require_command uninstall "$codex_bin"
|
|
427
409
|
|
|
428
410
|
tmp_parent="${TMPDIR:-/tmp}"
|
|
429
|
-
if ! scratch=$(
|
|
411
|
+
if ! scratch=$(spw_make_workspace "$tmp_parent" "superpowers-manager.adapter-uninstall"); then
|
|
430
412
|
spw_codex_failure uninstall "uninstall-failed" "cannot create adapter uninstall workspace under $tmp_parent" "" ""
|
|
431
413
|
fi
|
|
414
|
+
spw_install_workspace_trap "$scratch"
|
|
432
415
|
stdout_file="$scratch/stdout"
|
|
433
416
|
stderr_file="$scratch/stderr"
|
|
434
417
|
messages_file="$scratch/messages"
|
|
435
|
-
cleanup() {
|
|
436
|
-
status=$?
|
|
437
|
-
trap - 0 HUP INT TERM
|
|
438
|
-
rm -rf "$scratch" || :
|
|
439
|
-
exit "$status"
|
|
440
|
-
}
|
|
441
|
-
trap cleanup 0
|
|
442
|
-
trap 'exit 129' HUP
|
|
443
|
-
trap 'exit 130' INT
|
|
444
|
-
trap 'exit 143' TERM
|
|
445
418
|
|
|
446
419
|
: > "$messages_file"
|
|
447
420
|
|
|
@@ -491,12 +464,53 @@ spw_codex_inspect() {
|
|
|
491
464
|
|
|
492
465
|
case "$view" in
|
|
493
466
|
fingerprint)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
467
|
+
codex_bin="${SUPERPOWERS_CODEX:-codex}"
|
|
468
|
+
spw_require_command python3
|
|
469
|
+
spw_codex_require_command inspect "$codex_bin"
|
|
470
|
+
|
|
471
|
+
tmp_parent="${TMPDIR:-/tmp}"
|
|
472
|
+
if ! scratch=$(spw_make_workspace "$tmp_parent" "superpowers-manager.adapter-fingerprint"); then
|
|
473
|
+
spw_codex_failure inspect "inspect-failed" "cannot create adapter fingerprint workspace under $tmp_parent" "" ""
|
|
474
|
+
fi
|
|
475
|
+
spw_install_workspace_trap "$scratch"
|
|
476
|
+
stderr_file="$scratch/stderr"
|
|
477
|
+
messages_file="$scratch/messages"
|
|
478
|
+
|
|
479
|
+
: > "$messages_file"
|
|
480
|
+
: > "$stderr_file"
|
|
481
|
+
if ! plugin_listing=$("$codex_bin" plugin list --json 2>"$stderr_file"); then
|
|
482
|
+
spw_codex_append_messages "$messages_file" stderr "$stderr_file"
|
|
483
|
+
spw_codex_failure inspect "inspect-failed" "cannot list Codex plugins via '$codex_bin plugin list --json'" "" "$messages_file"
|
|
484
|
+
fi
|
|
485
|
+
spw_codex_append_messages "$messages_file" stderr "$stderr_file"
|
|
486
|
+
if ! active_version=$(spw_active_plugin_version_from_json \
|
|
487
|
+
"$plugin_listing" "$SPW_PLUGIN_ID"); then
|
|
488
|
+
spw_codex_failure inspect "inspect-failed" "cannot parse output of '$codex_bin plugin list --json'" "" "$messages_file"
|
|
489
|
+
fi
|
|
490
|
+
if [ -z "$active_version" ]; then
|
|
491
|
+
spw_codex_success inspect \
|
|
492
|
+
'{"view":"fingerprint","fingerprint":null}' "$messages_file"
|
|
493
|
+
return 0
|
|
499
494
|
fi
|
|
495
|
+
|
|
496
|
+
search_root="${SUPERPOWERS_INSTALLED_SEARCH_ROOT:-$HOME/.codex}"
|
|
497
|
+
active_root=$(spw_installed_root_for_version \
|
|
498
|
+
"$search_root" "$SPW_MARKETPLACE_NAME" "superpowers" "$active_version")
|
|
499
|
+
fingerprint=$(spw_installed_commit_from_root_or_empty "$active_root")
|
|
500
|
+
if [ -z "$fingerprint" ]; then
|
|
501
|
+
spw_codex_failure inspect "inspect-failed" \
|
|
502
|
+
"cannot inspect active Codex plugin fingerprint under $active_root" "" "$messages_file"
|
|
503
|
+
fi
|
|
504
|
+
spw_codex_success inspect \
|
|
505
|
+
"{\"view\":\"fingerprint\",\"fingerprint\":\"$fingerprint\"}" \
|
|
506
|
+
"$messages_file"
|
|
507
|
+
;;
|
|
508
|
+
update-control)
|
|
509
|
+
# Capability declaration for the fixed Codex local-package topology,
|
|
510
|
+
# backed by Layer-4 A/B evidence; revisit if topology or observed update
|
|
511
|
+
# behavior changes.
|
|
512
|
+
spw_codex_success inspect \
|
|
513
|
+
'{"view":"update-control","update_control":"managed"}' ""
|
|
500
514
|
;;
|
|
501
515
|
ownership)
|
|
502
516
|
codex_bin="${SUPERPOWERS_CODEX:-codex}"
|
|
@@ -504,21 +518,12 @@ spw_codex_inspect() {
|
|
|
504
518
|
spw_codex_require_command inspect "$codex_bin"
|
|
505
519
|
|
|
506
520
|
tmp_parent="${TMPDIR:-/tmp}"
|
|
507
|
-
if ! scratch=$(
|
|
521
|
+
if ! scratch=$(spw_make_workspace "$tmp_parent" "superpowers-manager.adapter-inspect"); then
|
|
508
522
|
spw_codex_failure inspect "inspect-failed" "cannot create adapter inspect workspace under $tmp_parent" "" ""
|
|
509
523
|
fi
|
|
524
|
+
spw_install_workspace_trap "$scratch"
|
|
510
525
|
stderr_file="$scratch/stderr"
|
|
511
526
|
messages_file="$scratch/messages"
|
|
512
|
-
cleanup() {
|
|
513
|
-
status=$?
|
|
514
|
-
trap - 0 HUP INT TERM
|
|
515
|
-
rm -rf "$scratch" || :
|
|
516
|
-
exit "$status"
|
|
517
|
-
}
|
|
518
|
-
trap cleanup 0
|
|
519
|
-
trap 'exit 129' HUP
|
|
520
|
-
trap 'exit 130' INT
|
|
521
|
-
trap 'exit 143' TERM
|
|
522
527
|
|
|
523
528
|
: > "$messages_file"
|
|
524
529
|
|
|
@@ -99,20 +99,77 @@ print("same" if na == nb else "different")
|
|
|
99
99
|
PY
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
spw_active_plugin_version_from_json() {
|
|
103
|
+
json="$1"
|
|
104
|
+
plugin_id="$2"
|
|
105
|
+
python3 - "$json" "$plugin_id" <<'PY'
|
|
106
|
+
import json
|
|
107
|
+
import sys
|
|
108
|
+
|
|
109
|
+
raw, plugin_id = sys.argv[1:]
|
|
110
|
+
|
|
111
|
+
def reject_constant(value):
|
|
112
|
+
raise ValueError(f"non-standard JSON constant: {value}")
|
|
113
|
+
|
|
114
|
+
def has_terminal_control(value):
|
|
115
|
+
return any(
|
|
116
|
+
ord(character) < 0x20
|
|
117
|
+
or 0x7F <= ord(character) <= 0x9F
|
|
118
|
+
or 0xD800 <= ord(character) <= 0xDFFF
|
|
119
|
+
for character in value
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
data = json.loads(raw, parse_constant=reject_constant)
|
|
124
|
+
except (json.JSONDecodeError, RecursionError, ValueError):
|
|
125
|
+
sys.exit(2)
|
|
126
|
+
|
|
127
|
+
if not isinstance(data, dict):
|
|
128
|
+
sys.exit(2)
|
|
129
|
+
items = data.get("installed")
|
|
130
|
+
if not isinstance(items, list):
|
|
131
|
+
sys.exit(2)
|
|
132
|
+
|
|
133
|
+
matches = []
|
|
134
|
+
for item in items:
|
|
135
|
+
if not isinstance(item, dict):
|
|
136
|
+
sys.exit(2)
|
|
137
|
+
item_plugin_id = item.get("pluginId")
|
|
138
|
+
if not isinstance(item_plugin_id, str) or not item_plugin_id:
|
|
139
|
+
sys.exit(2)
|
|
140
|
+
if item_plugin_id == plugin_id:
|
|
141
|
+
matches.append(item)
|
|
142
|
+
|
|
143
|
+
if len(matches) > 1:
|
|
144
|
+
sys.exit(2)
|
|
145
|
+
if not matches:
|
|
146
|
+
sys.exit(0)
|
|
147
|
+
|
|
148
|
+
version = matches[0].get("version")
|
|
149
|
+
if (
|
|
150
|
+
not isinstance(version, str)
|
|
151
|
+
or not version
|
|
152
|
+
or version in {".", ".."}
|
|
153
|
+
or "/" in version
|
|
154
|
+
or "\\" in version
|
|
155
|
+
or has_terminal_control(version)
|
|
156
|
+
):
|
|
157
|
+
sys.exit(2)
|
|
158
|
+
print(version)
|
|
159
|
+
PY
|
|
108
160
|
}
|
|
109
161
|
|
|
110
|
-
|
|
111
|
-
search_root="$
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
spw_installed_root_for_version() {
|
|
163
|
+
search_root="$1"
|
|
164
|
+
marketplace="$2"
|
|
165
|
+
plugin="$3"
|
|
166
|
+
version="$4"
|
|
167
|
+
case "$search_root" in
|
|
168
|
+
/) search_root= ;;
|
|
169
|
+
*/) search_root=${search_root%/} ;;
|
|
170
|
+
esac
|
|
171
|
+
printf '%s/plugins/cache/%s/%s/%s\n' \
|
|
172
|
+
"$search_root" "$marketplace" "$plugin" "$version"
|
|
116
173
|
}
|
|
117
174
|
|
|
118
175
|
spw_codex_json_get_or_empty() {
|
|
@@ -265,14 +322,15 @@ spw_manifest_short_sha_or_empty() {
|
|
|
265
322
|
esac
|
|
266
323
|
}
|
|
267
324
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
325
|
+
spw_installed_commit_from_root_or_empty() {
|
|
326
|
+
active_root="$1"
|
|
327
|
+
installed_metadata="$active_root/.superpowers-upstream.json"
|
|
328
|
+
installed_manifest="$active_root/.codex-plugin/plugin.json"
|
|
271
329
|
installed_commit=""
|
|
272
|
-
if [ -
|
|
330
|
+
if [ -f "$installed_metadata" ]; then
|
|
273
331
|
installed_commit=$(spw_codex_metadata_commit_or_empty "$installed_metadata" || true)
|
|
274
332
|
fi
|
|
275
|
-
if [ -z "$installed_commit" ] && [ -
|
|
333
|
+
if [ -z "$installed_commit" ] && [ -f "$installed_manifest" ]; then
|
|
276
334
|
installed_commit=$(spw_manifest_short_sha_or_empty "$installed_manifest" || true)
|
|
277
335
|
fi
|
|
278
336
|
printf '%s\n' "$installed_commit"
|
package/scripts/core/adapter.sh
CHANGED
|
@@ -24,6 +24,7 @@ spw_invoke_adapter() {
|
|
|
24
24
|
shift
|
|
25
25
|
|
|
26
26
|
response_file="${result_file}.response"
|
|
27
|
+
rm -f "$result_file"
|
|
27
28
|
adapter_exit=0
|
|
28
29
|
"$SPW_ADAPTER" "$operation" "$@" > "$response_file" || adapter_exit=$?
|
|
29
30
|
|
|
@@ -31,15 +32,20 @@ spw_invoke_adapter() {
|
|
|
31
32
|
if [ -n "$inspect_view" ]; then
|
|
32
33
|
args="$inspect_view"
|
|
33
34
|
fi
|
|
35
|
+
validator_exit=0
|
|
34
36
|
if [ -n "$args" ]; then
|
|
35
37
|
python3 -S "$SPW_ADAPTER_RESPONSE_VALIDATOR" \
|
|
36
38
|
--operation "$operation" --adapter-exit "$adapter_exit" \
|
|
37
39
|
--response "$response_file" --result "$result_file" \
|
|
38
|
-
--inspect-view "$inspect_view"
|
|
40
|
+
--inspect-view "$inspect_view" || validator_exit=$?
|
|
39
41
|
else
|
|
40
42
|
python3 -S "$SPW_ADAPTER_RESPONSE_VALIDATOR" \
|
|
41
43
|
--operation "$operation" --adapter-exit "$adapter_exit" \
|
|
42
|
-
--response "$response_file" --result "$result_file"
|
|
44
|
+
--response "$response_file" --result "$result_file" || validator_exit=$?
|
|
45
|
+
fi
|
|
46
|
+
if [ "$validator_exit" -ne 0 ]; then
|
|
47
|
+
rm -f "$result_file"
|
|
48
|
+
return 1
|
|
43
49
|
fi
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -76,6 +82,11 @@ spw_inspect_ownership() {
|
|
|
76
82
|
spw_invoke_adapter inspect "$result_file" ownership -- --view ownership
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
spw_inspect_update_control() {
|
|
86
|
+
result_file="$1"
|
|
87
|
+
spw_invoke_adapter inspect "$result_file" update-control -- --view update-control
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
spw_adapter_install() {
|
|
80
91
|
result_file="$1"
|
|
81
92
|
package_root="$2"
|
package/scripts/core/common.sh
CHANGED
|
@@ -6,6 +6,11 @@ spw_die() {
|
|
|
6
6
|
exit 1
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
spw_usage_error() {
|
|
10
|
+
echo "error: $*" >&2
|
|
11
|
+
exit 2
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
spw_require_command() {
|
|
10
15
|
command_name="$1"
|
|
11
16
|
if ! command -v "$command_name" >/dev/null 2>&1; then
|
|
@@ -13,6 +18,25 @@ spw_require_command() {
|
|
|
13
18
|
fi
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
spw_make_workspace() {
|
|
22
|
+
mktemp -d "$1/$2.XXXXXX"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
spw_cleanup_workspace_trap() {
|
|
26
|
+
_spw_workspace_trap_status=$?
|
|
27
|
+
trap - 0 HUP INT TERM
|
|
28
|
+
rm -rf "$_spw_workspace_trap_path" || :
|
|
29
|
+
exit "$_spw_workspace_trap_status"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
spw_install_workspace_trap() {
|
|
33
|
+
_spw_workspace_trap_path="$1"
|
|
34
|
+
trap spw_cleanup_workspace_trap 0
|
|
35
|
+
trap 'exit 129' HUP
|
|
36
|
+
trap 'exit 130' INT
|
|
37
|
+
trap 'exit 143' TERM
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
spw_root() {
|
|
17
41
|
CDPATH= cd -- "$(dirname "$0")/.." && pwd
|
|
18
42
|
}
|