superpowers-manager 0.1.4 → 0.1.6
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 +121 -78
- package/bin/superpowers-manager.js +22 -13
- package/package.json +2 -1
- package/scripts/adapters/codex/adapter +598 -0
- package/scripts/adapters/codex/lib.sh +391 -0
- package/scripts/{validate-generated-plugin.py → adapters/codex/validate-generated-plugin.py} +3 -3
- package/scripts/core/adapter.sh +92 -0
- package/scripts/core/common.sh +35 -0
- package/scripts/core/lifecycle.sh +127 -0
- package/scripts/core/provenance.sh +135 -0
- package/scripts/core/status.sh +27 -0
- package/scripts/core/upstream.sh +146 -0
- package/scripts/core/validate-adapter-response.py +245 -0
- package/scripts/install +29 -27
- package/scripts/prepare +33 -43
- package/scripts/probe +29 -18
- package/scripts/uninstall +33 -50
- package/scripts/update +3 -8
- package/scripts/lib.sh +0 -651
package/scripts/lib.sh
DELETED
|
@@ -1,651 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
|
|
3
|
-
SPW_UPSTREAM_URL_DEFAULT="https://github.com/obra/superpowers"
|
|
4
|
-
SPW_PLUGIN_ID="superpowers@superpowers-manager"
|
|
5
|
-
SPW_MARKETPLACE_NAME="superpowers-manager"
|
|
6
|
-
SPW_LEGACY_PLUGIN_ID="superpowers@superpowers-wrapper"
|
|
7
|
-
SPW_LEGACY_MARKETPLACE_NAME="superpowers-wrapper"
|
|
8
|
-
|
|
9
|
-
spw_die() {
|
|
10
|
-
echo "error: $*" >&2
|
|
11
|
-
exit 1
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
spw_require_command() {
|
|
15
|
-
command_name="$1"
|
|
16
|
-
if ! command -v "$command_name" >/dev/null 2>&1; then
|
|
17
|
-
spw_die "required command not found: $command_name"
|
|
18
|
-
fi
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
spw_root() {
|
|
22
|
-
CDPATH= cd -- "$(dirname "$0")/.." && pwd
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
spw_config_ref() {
|
|
26
|
-
root="$1"
|
|
27
|
-
if [ -n "${SUPERPOWERS_REF:-}" ]; then
|
|
28
|
-
printf '%s\n' "$SUPERPOWERS_REF"
|
|
29
|
-
return
|
|
30
|
-
fi
|
|
31
|
-
sed -n '1{s/[[:space:]]*$//;p;}' "$root/config/upstream-ref"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
spw_short_commit() {
|
|
35
|
-
commit="$1"
|
|
36
|
-
printf '%s' "$commit" | cut -c 1-7
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
spw_is_semver_base() {
|
|
40
|
-
version="$1"
|
|
41
|
-
printf '%s' "$version" | grep -Eq '^(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-]*))*))?$'
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
spw_sanitize_ref_for_version() {
|
|
45
|
-
ref="$1"
|
|
46
|
-
sanitized=$(printf '%s' "$ref" | sed 's/[^0-9A-Za-z-][^0-9A-Za-z-]*/-/g; s/^-*//; s/-*$//')
|
|
47
|
-
sanitized=$(printf '%s' "$sanitized" | cut -c 1-48 | sed 's/^-*//; s/-*$//')
|
|
48
|
-
if [ -z "$sanitized" ]; then
|
|
49
|
-
sanitized="unknown"
|
|
50
|
-
fi
|
|
51
|
-
printf '%s' "$sanitized"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
spw_manifest_version_for_ref() {
|
|
55
|
-
requested_ref="$1"
|
|
56
|
-
resolution_kind="$2"
|
|
57
|
-
resolved_ref="$3"
|
|
58
|
-
commit="$4"
|
|
59
|
-
short=$(spw_short_commit "$commit")
|
|
60
|
-
|
|
61
|
-
case "$resolution_kind" in
|
|
62
|
-
latest-release|tag)
|
|
63
|
-
base=$(printf '%s' "$resolved_ref" | sed -n 's/^v//p')
|
|
64
|
-
if [ -n "$base" ] && spw_is_semver_base "$base"; then
|
|
65
|
-
printf '%s+manager.%s\n' "$base" "$short"
|
|
66
|
-
return
|
|
67
|
-
fi
|
|
68
|
-
;;
|
|
69
|
-
ref)
|
|
70
|
-
if [ "$requested_ref" = "main" ]; then
|
|
71
|
-
printf '0.0.0-main+manager.%s\n' "$short"
|
|
72
|
-
return
|
|
73
|
-
fi
|
|
74
|
-
sanitized=$(spw_sanitize_ref_for_version "$requested_ref")
|
|
75
|
-
printf '0.0.0-ref-%s+manager.%s\n' "$sanitized" "$short"
|
|
76
|
-
return
|
|
77
|
-
;;
|
|
78
|
-
raw-commit)
|
|
79
|
-
;;
|
|
80
|
-
esac
|
|
81
|
-
|
|
82
|
-
printf '0.0.0+manager.%s\n' "$short"
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
spw_manifest_version_for_commit() {
|
|
86
|
-
commit="$1"
|
|
87
|
-
spw_manifest_version_for_ref "$commit" "raw-commit" "$commit" "$commit"
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
spw_commit_matches() {
|
|
91
|
-
desired="$1"
|
|
92
|
-
observed="$2"
|
|
93
|
-
short=$(printf '%s' "$desired" | cut -c 1-7)
|
|
94
|
-
[ -n "$observed" ] && { [ "$observed" = "$desired" ] || [ "$observed" = "$short" ]; }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
spw_select_latest_release_from_ls_remote() {
|
|
98
|
-
awk '
|
|
99
|
-
$2 ~ /^refs\/tags\/v[0-9]+\.[0-9]+\.[0-9]+(\^\{\})?$/ {
|
|
100
|
-
ref = $2
|
|
101
|
-
peeled = 0
|
|
102
|
-
if (ref ~ /\^\{\}$/) {
|
|
103
|
-
peeled = 1
|
|
104
|
-
sub(/\^\{\}$/, "", ref)
|
|
105
|
-
}
|
|
106
|
-
tag = ref
|
|
107
|
-
sub(/^refs\/tags\//, "", tag)
|
|
108
|
-
split(substr(tag, 2), parts, ".")
|
|
109
|
-
key = sprintf("%010d.%010d.%010d", parts[1], parts[2], parts[3])
|
|
110
|
-
tag_by_key[key] = tag
|
|
111
|
-
if (peeled || !(tag in sha_by_tag)) {
|
|
112
|
-
sha_by_tag[tag] = $1
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
END {
|
|
116
|
-
for (key in tag_by_key) {
|
|
117
|
-
tag = tag_by_key[key]
|
|
118
|
-
print key, tag, sha_by_tag[tag]
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
' | sort | tail -n 1 | awk '
|
|
122
|
-
NF == 3 { print $2 " " $3; found = 1 }
|
|
123
|
-
END { if (!found) exit 1 }
|
|
124
|
-
'
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
spw_resolve_ref() {
|
|
128
|
-
upstream_url="$1"
|
|
129
|
-
requested_ref="$2"
|
|
130
|
-
spw_require_command git
|
|
131
|
-
|
|
132
|
-
if [ "$requested_ref" = "latest-release" ]; then
|
|
133
|
-
if ! output=$(git ls-remote --tags "$upstream_url" 'refs/tags/v*' 2>&1); then
|
|
134
|
-
spw_die "cannot query upstream tags from $upstream_url: $output"
|
|
135
|
-
fi
|
|
136
|
-
if ! selected=$(printf '%s\n' "$output" | spw_select_latest_release_from_ls_remote); then
|
|
137
|
-
spw_die "no stable semver tag found for latest-release"
|
|
138
|
-
fi
|
|
139
|
-
printf 'latest-release %s\n' "$selected"
|
|
140
|
-
return
|
|
141
|
-
fi
|
|
142
|
-
|
|
143
|
-
if printf '%s' "$requested_ref" | grep -Eq '^[0-9a-fA-F]{40}$'; then
|
|
144
|
-
printf 'raw-commit %s %s\n' "$requested_ref" "$requested_ref"
|
|
145
|
-
return
|
|
146
|
-
fi
|
|
147
|
-
|
|
148
|
-
if ! tag_output=$(git ls-remote --tags "$upstream_url" "refs/tags/$requested_ref" "refs/tags/$requested_ref^{}" 2>&1); then
|
|
149
|
-
spw_die "cannot query upstream tag $requested_ref from $upstream_url: $tag_output"
|
|
150
|
-
fi
|
|
151
|
-
resolved=$(printf '%s\n' "$tag_output" | awk -v tag_ref="refs/tags/$requested_ref" '
|
|
152
|
-
NF >= 2 && ($2 == tag_ref || $2 == tag_ref "^{}") {
|
|
153
|
-
sha = $1
|
|
154
|
-
}
|
|
155
|
-
END { if (sha != "") print sha }
|
|
156
|
-
')
|
|
157
|
-
if [ -n "$resolved" ]; then
|
|
158
|
-
printf 'tag %s %s\n' "$requested_ref" "$resolved"
|
|
159
|
-
return
|
|
160
|
-
fi
|
|
161
|
-
|
|
162
|
-
if ! ref_output=$(git ls-remote "$upstream_url" "$requested_ref" 2>&1); then
|
|
163
|
-
spw_die "cannot query upstream ref $requested_ref from $upstream_url: $ref_output"
|
|
164
|
-
fi
|
|
165
|
-
resolved=$(printf '%s\n' "$ref_output" | awk 'NF >= 2 { print $1; exit }')
|
|
166
|
-
if [ -n "$resolved" ]; then
|
|
167
|
-
printf 'ref %s %s\n' "$requested_ref" "$resolved"
|
|
168
|
-
return
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
spw_die "cannot resolve upstream ref: $requested_ref"
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
spw_json_get() {
|
|
175
|
-
file="$1"
|
|
176
|
-
key="$2"
|
|
177
|
-
if ! value=$(
|
|
178
|
-
python3 - "$file" "$key" 2>&1 <<'PY'
|
|
179
|
-
import json
|
|
180
|
-
import sys
|
|
181
|
-
|
|
182
|
-
path, dotted_key = sys.argv[1:]
|
|
183
|
-
MAX_JSON_NESTING = 256
|
|
184
|
-
|
|
185
|
-
def reject_constant(constant):
|
|
186
|
-
raise ValueError(f"non-standard numeric constant: {constant}")
|
|
187
|
-
|
|
188
|
-
def nesting_exceeds_limit(value):
|
|
189
|
-
stack = [(value, 0)]
|
|
190
|
-
while stack:
|
|
191
|
-
current, depth = stack.pop()
|
|
192
|
-
if isinstance(current, dict):
|
|
193
|
-
next_depth = depth + 1
|
|
194
|
-
if next_depth > MAX_JSON_NESTING:
|
|
195
|
-
return True
|
|
196
|
-
stack.extend((child, next_depth) for child in current.values())
|
|
197
|
-
elif isinstance(current, list):
|
|
198
|
-
next_depth = depth + 1
|
|
199
|
-
if next_depth > MAX_JSON_NESTING:
|
|
200
|
-
return True
|
|
201
|
-
stack.extend((child, next_depth) for child in current)
|
|
202
|
-
return False
|
|
203
|
-
|
|
204
|
-
try:
|
|
205
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
206
|
-
data = json.load(f, parse_constant=reject_constant)
|
|
207
|
-
except RecursionError:
|
|
208
|
-
sys.exit(f"JSON nesting exceeds limit in {path}")
|
|
209
|
-
except json.JSONDecodeError as exc:
|
|
210
|
-
sys.exit(
|
|
211
|
-
f"invalid JSON in {path}: "
|
|
212
|
-
f"line {exc.lineno} column {exc.colno}: {exc.msg}"
|
|
213
|
-
)
|
|
214
|
-
except (OSError, UnicodeError) as exc:
|
|
215
|
-
sys.exit(f"cannot read JSON in {path}: {exc}")
|
|
216
|
-
except ValueError as exc:
|
|
217
|
-
sys.exit(f"invalid JSON in {path}: {exc}")
|
|
218
|
-
|
|
219
|
-
if nesting_exceeds_limit(data):
|
|
220
|
-
sys.exit(f"JSON nesting exceeds limit in {path}")
|
|
221
|
-
|
|
222
|
-
if not isinstance(data, dict):
|
|
223
|
-
sys.exit(f"JSON value must be an object in {path}")
|
|
224
|
-
|
|
225
|
-
value = data
|
|
226
|
-
for part in dotted_key.split("."):
|
|
227
|
-
if not isinstance(value, dict):
|
|
228
|
-
value = ""
|
|
229
|
-
break
|
|
230
|
-
value = value.get(part, "")
|
|
231
|
-
try:
|
|
232
|
-
print(value if value is not None else "")
|
|
233
|
-
except (OSError, UnicodeError) as exc:
|
|
234
|
-
sys.exit(f"cannot output JSON value from {path}: {exc}")
|
|
235
|
-
PY
|
|
236
|
-
); then
|
|
237
|
-
spw_die "$value"
|
|
238
|
-
fi
|
|
239
|
-
printf '%s\n' "$value"
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
spw_copy_path_if_present() {
|
|
243
|
-
src="$1"
|
|
244
|
-
dst="$2"
|
|
245
|
-
if [ -e "$src" ]; then
|
|
246
|
-
rm -rf "$dst"
|
|
247
|
-
cp -R "$src" "$dst"
|
|
248
|
-
fi
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
spw_require_upstream_path() {
|
|
252
|
-
path="$1"
|
|
253
|
-
label="$2"
|
|
254
|
-
if [ ! -e "$path" ]; then
|
|
255
|
-
spw_die "required upstream path missing: $label"
|
|
256
|
-
fi
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
spw_write_metadata_json() {
|
|
260
|
-
file="$1"
|
|
261
|
-
source="$2"
|
|
262
|
-
requested_ref="$3"
|
|
263
|
-
resolved_ref="$4"
|
|
264
|
-
commit="$5"
|
|
265
|
-
upstream_manifest_version="$6"
|
|
266
|
-
python3 - "$file" "$source" "$requested_ref" "$resolved_ref" "$commit" "$upstream_manifest_version" <<'PY'
|
|
267
|
-
import json, sys
|
|
268
|
-
path, source, requested, resolved, commit, version = sys.argv[1:]
|
|
269
|
-
data = {
|
|
270
|
-
"source": source,
|
|
271
|
-
"requested_ref": requested,
|
|
272
|
-
"resolved_ref": resolved,
|
|
273
|
-
"commit": commit,
|
|
274
|
-
"upstream_manifest_version": version,
|
|
275
|
-
}
|
|
276
|
-
try:
|
|
277
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
278
|
-
json.dump(data, f, indent=2, allow_nan=False)
|
|
279
|
-
f.write("\n")
|
|
280
|
-
except RecursionError as exc:
|
|
281
|
-
sys.exit(f"JSON nesting exceeds limit while writing {path}: {exc}")
|
|
282
|
-
except (OSError, UnicodeError, ValueError) as exc:
|
|
283
|
-
sys.exit(f"cannot write JSON to {path}: {exc}")
|
|
284
|
-
PY
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
spw_apply_manifest_overlay() {
|
|
288
|
-
manifest="$1"
|
|
289
|
-
version="$2"
|
|
290
|
-
python3 - "$manifest" "$version" <<'PY'
|
|
291
|
-
import json
|
|
292
|
-
import sys
|
|
293
|
-
|
|
294
|
-
path, version = sys.argv[1:]
|
|
295
|
-
MAX_JSON_NESTING = 256
|
|
296
|
-
|
|
297
|
-
def reject_constant(constant):
|
|
298
|
-
raise ValueError(f"non-standard numeric constant: {constant}")
|
|
299
|
-
|
|
300
|
-
def nesting_exceeds_limit(value):
|
|
301
|
-
stack = [(value, 0)]
|
|
302
|
-
while stack:
|
|
303
|
-
current, depth = stack.pop()
|
|
304
|
-
if isinstance(current, dict):
|
|
305
|
-
next_depth = depth + 1
|
|
306
|
-
if next_depth > MAX_JSON_NESTING:
|
|
307
|
-
return True
|
|
308
|
-
stack.extend((child, next_depth) for child in current.values())
|
|
309
|
-
elif isinstance(current, list):
|
|
310
|
-
next_depth = depth + 1
|
|
311
|
-
if next_depth > MAX_JSON_NESTING:
|
|
312
|
-
return True
|
|
313
|
-
stack.extend((child, next_depth) for child in current)
|
|
314
|
-
return False
|
|
315
|
-
|
|
316
|
-
try:
|
|
317
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
318
|
-
data = json.load(f, parse_constant=reject_constant)
|
|
319
|
-
except RecursionError:
|
|
320
|
-
sys.exit(f"JSON nesting exceeds limit in {path}")
|
|
321
|
-
except json.JSONDecodeError as exc:
|
|
322
|
-
sys.exit(
|
|
323
|
-
f"invalid manifest JSON in {path}: "
|
|
324
|
-
f"line {exc.lineno} column {exc.colno}: {exc.msg}"
|
|
325
|
-
)
|
|
326
|
-
except (OSError, UnicodeError) as exc:
|
|
327
|
-
sys.exit(f"cannot read manifest JSON in {path}: {exc}")
|
|
328
|
-
except ValueError as exc:
|
|
329
|
-
sys.exit(f"invalid manifest JSON in {path}: {exc}")
|
|
330
|
-
|
|
331
|
-
if nesting_exceeds_limit(data):
|
|
332
|
-
sys.exit(f"JSON nesting exceeds limit in {path}")
|
|
333
|
-
|
|
334
|
-
if not isinstance(data, dict):
|
|
335
|
-
sys.exit(f"manifest must be a JSON object: {path}")
|
|
336
|
-
|
|
337
|
-
data["version"] = version
|
|
338
|
-
data["skills"] = "./skills/"
|
|
339
|
-
data.pop("hooks", None)
|
|
340
|
-
|
|
341
|
-
try:
|
|
342
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
343
|
-
json.dump(data, f, indent=2, allow_nan=False)
|
|
344
|
-
f.write("\n")
|
|
345
|
-
except RecursionError as exc:
|
|
346
|
-
sys.exit(f"manifest JSON nesting exceeds limit while writing {path}: {exc}")
|
|
347
|
-
except (OSError, UnicodeError, ValueError) as exc:
|
|
348
|
-
sys.exit(f"cannot write manifest JSON in {path}: {exc}")
|
|
349
|
-
PY
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
spw_status_for_commits() {
|
|
353
|
-
desired="$1"
|
|
354
|
-
generated="$2"
|
|
355
|
-
installed="$3"
|
|
356
|
-
|
|
357
|
-
if [ -z "$generated" ]; then
|
|
358
|
-
printf '%s\n' "needs prepare"
|
|
359
|
-
elif ! spw_commit_matches "$desired" "$generated"; then
|
|
360
|
-
printf '%s\n' "needs prepare"
|
|
361
|
-
elif [ -z "$installed" ]; then
|
|
362
|
-
printf '%s\n' "needs install"
|
|
363
|
-
elif ! spw_commit_matches "$desired" "$installed"; then
|
|
364
|
-
printf '%s\n' "needs install"
|
|
365
|
-
else
|
|
366
|
-
printf '%s\n' "current"
|
|
367
|
-
fi
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
spw_metadata_commit_or_empty() {
|
|
371
|
-
file="$1"
|
|
372
|
-
if [ -f "$file" ]; then
|
|
373
|
-
spw_json_get "$file" "commit"
|
|
374
|
-
fi
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
spw_manifest_short_sha_or_empty() {
|
|
378
|
-
file="$1"
|
|
379
|
-
if [ ! -f "$file" ]; then
|
|
380
|
-
return 0
|
|
381
|
-
fi
|
|
382
|
-
version=$(spw_json_get "$file" "version")
|
|
383
|
-
case "$version" in
|
|
384
|
-
*+manager.*)
|
|
385
|
-
short="${version##*.}"
|
|
386
|
-
case "$short" in
|
|
387
|
-
""|*[!0-9a-fA-F]*)
|
|
388
|
-
;;
|
|
389
|
-
*)
|
|
390
|
-
printf '%s\n' "$short"
|
|
391
|
-
;;
|
|
392
|
-
esac
|
|
393
|
-
;;
|
|
394
|
-
esac
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
# Codex installs a plugin into a versioned cache directory:
|
|
398
|
-
# ~/.codex/plugins/cache/<marketplace>/superpowers/<version>/...
|
|
399
|
-
# so the metadata/manifest live one directory below the plugin name, not
|
|
400
|
-
# directly inside it (confirmed by the Task 1 behavior probe against the live
|
|
401
|
-
# install). Match both the versioned layout and a flat manager-cache layout (no
|
|
402
|
-
# intervening version directory).
|
|
403
|
-
spw_find_installed_metadata() {
|
|
404
|
-
search_root="${SUPERPOWERS_INSTALLED_SEARCH_ROOT:-$HOME/.codex}"
|
|
405
|
-
find "$search_root" \
|
|
406
|
-
\( -path "*/plugins/cache/$SPW_MARKETPLACE_NAME/superpowers/.superpowers-upstream.json" \
|
|
407
|
-
-o -path "*/plugins/cache/$SPW_MARKETPLACE_NAME/superpowers/*/.superpowers-upstream.json" \) \
|
|
408
|
-
-type f 2>/dev/null | head -n 1
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
spw_find_installed_manifest() {
|
|
412
|
-
search_root="${SUPERPOWERS_INSTALLED_SEARCH_ROOT:-$HOME/.codex}"
|
|
413
|
-
find "$search_root" \
|
|
414
|
-
\( -path "*/plugins/cache/$SPW_MARKETPLACE_NAME/superpowers/.codex-plugin/plugin.json" \
|
|
415
|
-
-o -path "*/plugins/cache/$SPW_MARKETPLACE_NAME/superpowers/*/.codex-plugin/plugin.json" \) \
|
|
416
|
-
-type f 2>/dev/null | head -n 1
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
# Given a JSON document as the FIRST ARGUMENT (a string), print "present" if any
|
|
420
|
-
# element of the top-level array <array_key> is an object whose <field> equals
|
|
421
|
-
# <value>, else print "absent" (exit 0). On unparseable JSON or invalid schema,
|
|
422
|
-
# print nothing and exit 2 so callers can fail closed rather than treat an
|
|
423
|
-
# unreadable listing as "absent".
|
|
424
|
-
# The JSON is passed as an argument (exactly as spw_json_get takes a file path),
|
|
425
|
-
# NOT on stdin: the here-doc below is Python's stdin (its program source), so a
|
|
426
|
-
# json.load(sys.stdin) here would read the program, not the caller's JSON.
|
|
427
|
-
spw_json_array_has() {
|
|
428
|
-
json="$1"
|
|
429
|
-
array_key="$2"
|
|
430
|
-
field="$3"
|
|
431
|
-
value="$4"
|
|
432
|
-
python3 - "$json" "$array_key" "$field" "$value" <<'PY'
|
|
433
|
-
import json, sys
|
|
434
|
-
raw, array_key, field, value = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
|
435
|
-
try:
|
|
436
|
-
data = json.loads(
|
|
437
|
-
raw,
|
|
438
|
-
parse_constant=lambda constant: (_ for _ in ()).throw(ValueError(constant)),
|
|
439
|
-
)
|
|
440
|
-
except (json.JSONDecodeError, RecursionError, ValueError):
|
|
441
|
-
sys.exit(2)
|
|
442
|
-
if not isinstance(data, dict):
|
|
443
|
-
sys.exit(2)
|
|
444
|
-
items = data.get(array_key)
|
|
445
|
-
if not isinstance(items, list):
|
|
446
|
-
sys.exit(2)
|
|
447
|
-
for item in items:
|
|
448
|
-
if not isinstance(item, dict):
|
|
449
|
-
sys.exit(2)
|
|
450
|
-
item_value = item.get(field)
|
|
451
|
-
if not isinstance(item_value, str) or not item_value:
|
|
452
|
-
sys.exit(2)
|
|
453
|
-
found = any(item[field] == value for item in items)
|
|
454
|
-
print("present" if found else "absent")
|
|
455
|
-
PY
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
spw_snapshot_get() {
|
|
459
|
-
snapshot="$1"
|
|
460
|
-
key="$2"
|
|
461
|
-
printf '%s\n' "$snapshot" | awk -F= -v key="$key" '$1 == key { print $2; found = 1 } END { if (!found) exit 1 }'
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
spw_codex_identity_snapshot() {
|
|
465
|
-
codex_bin="$1"
|
|
466
|
-
if ! plugin_listing=$("$codex_bin" plugin list --json 2>/dev/null); then
|
|
467
|
-
spw_die "cannot list Codex plugins via '$codex_bin plugin list --json'"
|
|
468
|
-
fi
|
|
469
|
-
if ! manager_plugin=$(spw_json_array_has "$plugin_listing" installed pluginId "$SPW_PLUGIN_ID"); then
|
|
470
|
-
spw_die "cannot parse output of '$codex_bin plugin list --json'"
|
|
471
|
-
fi
|
|
472
|
-
if ! legacy_plugin=$(spw_json_array_has "$plugin_listing" installed pluginId "$SPW_LEGACY_PLUGIN_ID"); then
|
|
473
|
-
spw_die "cannot parse output of '$codex_bin plugin list --json'"
|
|
474
|
-
fi
|
|
475
|
-
|
|
476
|
-
if ! marketplace_listing=$("$codex_bin" plugin marketplace list --json 2>/dev/null); then
|
|
477
|
-
spw_die "cannot list Codex marketplaces via '$codex_bin plugin marketplace list --json'"
|
|
478
|
-
fi
|
|
479
|
-
if ! manager_marketplace=$(spw_json_array_has "$marketplace_listing" marketplaces name "$SPW_MARKETPLACE_NAME"); then
|
|
480
|
-
spw_die "cannot parse output of '$codex_bin plugin marketplace list --json'"
|
|
481
|
-
fi
|
|
482
|
-
if ! legacy_marketplace=$(spw_json_array_has "$marketplace_listing" marketplaces name "$SPW_LEGACY_MARKETPLACE_NAME"); then
|
|
483
|
-
spw_die "cannot parse output of '$codex_bin plugin marketplace list --json'"
|
|
484
|
-
fi
|
|
485
|
-
|
|
486
|
-
manager=false
|
|
487
|
-
legacy=false
|
|
488
|
-
if [ "$manager_plugin" = present ] || [ "$manager_marketplace" = present ]; then
|
|
489
|
-
manager=true
|
|
490
|
-
fi
|
|
491
|
-
if [ "$legacy_plugin" = present ] || [ "$legacy_marketplace" = present ]; then
|
|
492
|
-
legacy=true
|
|
493
|
-
fi
|
|
494
|
-
case "$manager:$legacy" in
|
|
495
|
-
true:true) identity_state=both ;;
|
|
496
|
-
true:false) identity_state=manager ;;
|
|
497
|
-
false:true) identity_state=legacy ;;
|
|
498
|
-
false:false) identity_state=neither ;;
|
|
499
|
-
esac
|
|
500
|
-
|
|
501
|
-
printf 'manager_plugin=%s\n' "$([ "$manager_plugin" = present ] && printf true || printf false)"
|
|
502
|
-
printf 'manager_marketplace=%s\n' "$([ "$manager_marketplace" = present ] && printf true || printf false)"
|
|
503
|
-
printf 'legacy_plugin=%s\n' "$([ "$legacy_plugin" = present ] && printf true || printf false)"
|
|
504
|
-
printf 'legacy_marketplace=%s\n' "$([ "$legacy_marketplace" = present ] && printf true || printf false)"
|
|
505
|
-
printf 'identity_state=%s\n' "$identity_state"
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
spw_require_no_legacy_state() {
|
|
509
|
-
case "$1" in
|
|
510
|
-
neither|manager) return 0 ;;
|
|
511
|
-
legacy|both)
|
|
512
|
-
printf '%s\n' \
|
|
513
|
-
'Legacy superpowers-wrapper Codex state is installed.' \
|
|
514
|
-
'Run: npx superpowers-wrapper@0.1.1 uninstall' \
|
|
515
|
-
'Then run: npx superpowers-manager@0.1.4 install' >&2
|
|
516
|
-
return 1
|
|
517
|
-
;;
|
|
518
|
-
*) spw_die "unknown Codex identity state: $1" ;;
|
|
519
|
-
esac
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
# Print the registered root of <marketplace_name> from a marketplace-list JSON
|
|
523
|
-
# document (as a string argument, like spw_json_array_has), or nothing if the
|
|
524
|
-
# marketplace is absent. Exit 2 on unparseable JSON, a non-object item, or an
|
|
525
|
-
# item without a non-empty string name: such an item cannot be proven unrelated
|
|
526
|
-
# and must not be mistaken for an absent manager marketplace. Validate root only
|
|
527
|
-
# on the matching item; unrelated marketplace roots are never read. A matching
|
|
528
|
-
# item without a non-empty string root also exits 2. Empty output is unambiguous:
|
|
529
|
-
# a valid registered root is always a non-empty path.
|
|
530
|
-
spw_marketplace_root_from_json() {
|
|
531
|
-
json="$1"
|
|
532
|
-
marketplace_name="$2"
|
|
533
|
-
python3 - "$json" "$marketplace_name" <<'PY'
|
|
534
|
-
import json, sys
|
|
535
|
-
raw, name = sys.argv[1], sys.argv[2]
|
|
536
|
-
try:
|
|
537
|
-
data = json.loads(raw)
|
|
538
|
-
except (json.JSONDecodeError, RecursionError):
|
|
539
|
-
sys.exit(2)
|
|
540
|
-
if not isinstance(data, dict):
|
|
541
|
-
sys.exit(2)
|
|
542
|
-
items = data.get("marketplaces")
|
|
543
|
-
if not isinstance(items, list):
|
|
544
|
-
sys.exit(2)
|
|
545
|
-
for item in items:
|
|
546
|
-
if not isinstance(item, dict):
|
|
547
|
-
sys.exit(2)
|
|
548
|
-
item_name = item.get("name")
|
|
549
|
-
if not isinstance(item_name, str) or not item_name:
|
|
550
|
-
sys.exit(2)
|
|
551
|
-
for item in items:
|
|
552
|
-
if item["name"] == name:
|
|
553
|
-
item_root = item.get("root")
|
|
554
|
-
if not isinstance(item_root, str) or not item_root:
|
|
555
|
-
sys.exit(2)
|
|
556
|
-
print(item_root)
|
|
557
|
-
sys.exit(0)
|
|
558
|
-
PY
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
# Print "same" if the two paths refer to the same physical location, else
|
|
562
|
-
# "different". Physical-path normalization via os.path.realpath on both
|
|
563
|
-
# sides (handles /var vs /private/var and other symlinked roots); if
|
|
564
|
-
# realpath fails, fall back to comparing the original strings.
|
|
565
|
-
spw_paths_equal() {
|
|
566
|
-
python3 - "$1" "$2" <<'PY'
|
|
567
|
-
import os, sys
|
|
568
|
-
a, b = sys.argv[1], sys.argv[2]
|
|
569
|
-
try:
|
|
570
|
-
na, nb = os.path.realpath(a), os.path.realpath(b)
|
|
571
|
-
except OSError:
|
|
572
|
-
na, nb = a, b
|
|
573
|
-
print("same" if na == nb else "different")
|
|
574
|
-
PY
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
# Reconcile Codex's manager marketplace pointer to <current_root>:
|
|
578
|
-
# absent -> add
|
|
579
|
-
# same physical root -> keep
|
|
580
|
-
# different root -> remove, then add
|
|
581
|
-
# List/parse failures abort before any marketplace change. If remove
|
|
582
|
-
# succeeds and add fails, print a recovery command for the current root AND
|
|
583
|
-
# the previous root so the user can restore last-known-good state. Only the
|
|
584
|
-
# superpowers-manager marketplace is ever touched.
|
|
585
|
-
spw_reconcile_marketplace() {
|
|
586
|
-
codex_bin="$1"
|
|
587
|
-
current_root="$2"
|
|
588
|
-
if ! listing=$("$codex_bin" plugin marketplace list --json 2>/dev/null); then
|
|
589
|
-
spw_die "cannot list Codex marketplaces via '$codex_bin plugin marketplace list --json'"
|
|
590
|
-
fi
|
|
591
|
-
if ! registered_root=$(spw_marketplace_root_from_json "$listing" "$SPW_MARKETPLACE_NAME"); then
|
|
592
|
-
spw_die "cannot parse output of '$codex_bin plugin marketplace list --json'"
|
|
593
|
-
fi
|
|
594
|
-
if [ -z "$registered_root" ]; then
|
|
595
|
-
if ! "$codex_bin" plugin marketplace add "$current_root"; then
|
|
596
|
-
spw_die "codex marketplace add failed for $current_root"
|
|
597
|
-
fi
|
|
598
|
-
return 0
|
|
599
|
-
fi
|
|
600
|
-
if [ "$(spw_paths_equal "$current_root" "$registered_root")" = same ]; then
|
|
601
|
-
return 0
|
|
602
|
-
fi
|
|
603
|
-
echo "marketplace $SPW_MARKETPLACE_NAME registered at $registered_root; re-registering at $current_root"
|
|
604
|
-
if ! "$codex_bin" plugin marketplace remove "$SPW_MARKETPLACE_NAME"; then
|
|
605
|
-
spw_die "codex marketplace remove failed for $SPW_MARKETPLACE_NAME (registered at $registered_root)"
|
|
606
|
-
fi
|
|
607
|
-
if ! "$codex_bin" plugin marketplace add "$current_root"; then
|
|
608
|
-
echo "error: marketplace $SPW_MARKETPLACE_NAME was removed but re-adding failed." >&2
|
|
609
|
-
echo "recover with: $codex_bin plugin marketplace add $current_root" >&2
|
|
610
|
-
echo "previous root (last known good): $registered_root" >&2
|
|
611
|
-
exit 1
|
|
612
|
-
fi
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
# Return the currently installed manager commit/fingerprint, or empty if the
|
|
616
|
-
# installed plugin cannot be detected. This is intentionally local-only: callers
|
|
617
|
-
# pass the desired commit into spw_verify_refresh so post-mutation verification
|
|
618
|
-
# never refetches or re-resolves upstream after Codex state has changed.
|
|
619
|
-
spw_installed_commit_or_empty() {
|
|
620
|
-
installed_metadata=$(spw_find_installed_metadata || true)
|
|
621
|
-
installed_manifest=$(spw_find_installed_manifest || true)
|
|
622
|
-
installed_commit=""
|
|
623
|
-
if [ -n "$installed_metadata" ]; then
|
|
624
|
-
installed_commit=$(spw_metadata_commit_or_empty "$installed_metadata" || true)
|
|
625
|
-
fi
|
|
626
|
-
if [ -z "$installed_commit" ] && [ -n "$installed_manifest" ]; then
|
|
627
|
-
installed_commit=$(spw_manifest_short_sha_or_empty "$installed_manifest" || true)
|
|
628
|
-
fi
|
|
629
|
-
printf '%s\n' "$installed_commit"
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
# After an install, confirm the installed manager refreshed to <desired_commit>.
|
|
633
|
-
# Never prints a success line while the installed manager is detectably stale.
|
|
634
|
-
# Shared by scripts/install and scripts/update.
|
|
635
|
-
spw_verify_refresh() {
|
|
636
|
-
desired_commit="$1"
|
|
637
|
-
installed_commit=$(spw_installed_commit_or_empty || true)
|
|
638
|
-
printf 'desired_commit=%s\n' "$desired_commit"
|
|
639
|
-
printf 'installed_commit=%s\n' "$installed_commit"
|
|
640
|
-
if [ -n "$installed_commit" ] && spw_commit_matches "$desired_commit" "$installed_commit"; then
|
|
641
|
-
echo "manager updated"
|
|
642
|
-
return 0
|
|
643
|
-
fi
|
|
644
|
-
if [ -n "$installed_commit" ]; then
|
|
645
|
-
echo "error: installed manager is still stale after install; the local plugin cache did not refresh." >&2
|
|
646
|
-
echo "hint: retry with SUPERPOWERS_INSTALL_REFRESH_MODE=remove-add" >&2
|
|
647
|
-
exit 1
|
|
648
|
-
fi
|
|
649
|
-
echo "error: installed manager not detectable, cannot confirm refresh; verify with 'codex plugin list --json'." >&2
|
|
650
|
-
return 1
|
|
651
|
-
}
|