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/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
- }