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.
@@ -0,0 +1,135 @@
1
+ #!/bin/sh
2
+ # Sourced module; callers own set -eu.
3
+
4
+ spw_json_get() {
5
+ file="$1"
6
+ key="$2"
7
+ if ! value=$(
8
+ python3 - "$file" "$key" 2>&1 <<'PY'
9
+ import json
10
+ import sys
11
+
12
+ path, dotted_key = sys.argv[1:]
13
+ MAX_JSON_NESTING = 256
14
+
15
+ def reject_constant(constant):
16
+ raise ValueError(f"non-standard numeric constant: {constant}")
17
+
18
+ def nesting_exceeds_limit(value):
19
+ stack = [(value, 0)]
20
+ while stack:
21
+ current, depth = stack.pop()
22
+ if isinstance(current, dict):
23
+ next_depth = depth + 1
24
+ if next_depth > MAX_JSON_NESTING:
25
+ return True
26
+ stack.extend((child, next_depth) for child in current.values())
27
+ elif isinstance(current, list):
28
+ next_depth = depth + 1
29
+ if next_depth > MAX_JSON_NESTING:
30
+ return True
31
+ stack.extend((child, next_depth) for child in current)
32
+ return False
33
+
34
+ try:
35
+ with open(path, "r", encoding="utf-8") as f:
36
+ data = json.load(f, parse_constant=reject_constant)
37
+ except RecursionError:
38
+ sys.exit(f"JSON nesting exceeds limit in {path}")
39
+ except json.JSONDecodeError as exc:
40
+ sys.exit(
41
+ f"invalid JSON in {path}: "
42
+ f"line {exc.lineno} column {exc.colno}: {exc.msg}"
43
+ )
44
+ except (OSError, UnicodeError) as exc:
45
+ sys.exit(f"cannot read JSON in {path}: {exc}")
46
+ except ValueError as exc:
47
+ sys.exit(f"invalid JSON in {path}: {exc}")
48
+
49
+ if nesting_exceeds_limit(data):
50
+ sys.exit(f"JSON nesting exceeds limit in {path}")
51
+
52
+ if not isinstance(data, dict):
53
+ sys.exit(f"JSON value must be an object in {path}")
54
+
55
+ value = data
56
+ for part in dotted_key.split("."):
57
+ if not isinstance(value, dict):
58
+ value = ""
59
+ break
60
+ value = value.get(part, "")
61
+ try:
62
+ print(value if value is not None else "")
63
+ except (OSError, UnicodeError) as exc:
64
+ sys.exit(f"cannot output JSON value from {path}: {exc}")
65
+ PY
66
+ ); then
67
+ spw_die "$value"
68
+ fi
69
+ printf '%s\n' "$value"
70
+ }
71
+
72
+ spw_write_metadata_json() {
73
+ file="$1"
74
+ source="$2"
75
+ requested_ref="$3"
76
+ resolved_ref="$4"
77
+ commit="$5"
78
+ upstream_manifest_version="$6"
79
+ python3 - "$file" "$source" "$requested_ref" "$resolved_ref" "$commit" "$upstream_manifest_version" <<'PY'
80
+ import json, sys
81
+ path, source, requested, resolved, commit, version = sys.argv[1:]
82
+ data = {
83
+ "source": source,
84
+ "requested_ref": requested,
85
+ "resolved_ref": resolved,
86
+ "commit": commit,
87
+ "upstream_manifest_version": version,
88
+ }
89
+ try:
90
+ with open(path, "w", encoding="utf-8") as f:
91
+ json.dump(data, f, indent=2, allow_nan=False)
92
+ f.write("\n")
93
+ except RecursionError as exc:
94
+ sys.exit(f"JSON nesting exceeds limit while writing {path}: {exc}")
95
+ except (OSError, UnicodeError, ValueError) as exc:
96
+ sys.exit(f"cannot write JSON to {path}: {exc}")
97
+ PY
98
+ }
99
+
100
+ spw_metadata_commit_or_empty() {
101
+ file="$1"
102
+ if [ -f "$file" ]; then
103
+ spw_json_get "$file" "commit"
104
+ fi
105
+ }
106
+
107
+ spw_metadata_commit_lenient_or_empty() {
108
+ file="$1"
109
+ [ -f "$file" ] || return 0
110
+
111
+ if value=$(
112
+ python3 -S - "$file" 2>/dev/null <<'PY'
113
+ import json
114
+ import re
115
+ import sys
116
+
117
+ path = sys.argv[1]
118
+
119
+ def reject_constant(constant):
120
+ raise ValueError(f"non-standard numeric constant: {constant}")
121
+
122
+ try:
123
+ with open(path, "r", encoding="utf-8") as handle:
124
+ data = json.load(handle, parse_constant=reject_constant)
125
+ except (OSError, UnicodeError, ValueError, RecursionError):
126
+ raise SystemExit(1)
127
+
128
+ commit = data.get("commit") if isinstance(data, dict) else None
129
+ if isinstance(commit, str) and re.fullmatch(r"[0-9a-fA-F]{40}", commit):
130
+ print(commit)
131
+ PY
132
+ ); then
133
+ printf '%s\n' "$value"
134
+ fi
135
+ }
@@ -0,0 +1,27 @@
1
+ #!/bin/sh
2
+ # Sourced module; callers own set -eu.
3
+
4
+ spw_commit_matches() {
5
+ desired="$1"
6
+ observed="$2"
7
+ short=$(printf '%s' "$desired" | cut -c 1-7)
8
+ [ -n "$observed" ] && { [ "$observed" = "$desired" ] || [ "$observed" = "$short" ]; }
9
+ }
10
+
11
+ spw_status_for_commits() {
12
+ desired="$1"
13
+ generated="$2"
14
+ installed="$3"
15
+
16
+ if [ -z "$generated" ]; then
17
+ printf '%s\n' "needs prepare"
18
+ elif ! spw_commit_matches "$desired" "$generated"; then
19
+ printf '%s\n' "needs prepare"
20
+ elif [ -z "$installed" ]; then
21
+ printf '%s\n' "needs install"
22
+ elif ! spw_commit_matches "$desired" "$installed"; then
23
+ printf '%s\n' "needs install"
24
+ else
25
+ printf '%s\n' "current"
26
+ fi
27
+ }
@@ -0,0 +1,146 @@
1
+ #!/bin/sh
2
+ # Sourced module; callers own set -eu.
3
+
4
+ SPW_UPSTREAM_URL_DEFAULT="https://github.com/obra/superpowers"
5
+
6
+ spw_config_ref() (
7
+ config_root="$1"
8
+ if [ -n "${SUPERPOWERS_REF:-}" ]; then
9
+ printf '%s\n' "$SUPERPOWERS_REF"
10
+ return
11
+ fi
12
+ sed -n '1{s/[[:space:]]*$//;p;}' "$config_root/config/upstream-ref"
13
+ )
14
+
15
+ spw_short_commit() {
16
+ commit="$1"
17
+ printf '%s' "$commit" | cut -c 1-7
18
+ }
19
+
20
+ spw_is_semver_base() {
21
+ version="$1"
22
+ 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-]*))*))?$'
23
+ }
24
+
25
+ spw_sanitize_ref_for_version() {
26
+ ref="$1"
27
+ sanitized=$(printf '%s' "$ref" | sed 's/[^0-9A-Za-z-][^0-9A-Za-z-]*/-/g; s/^-*//; s/-*$//')
28
+ sanitized=$(printf '%s' "$sanitized" | cut -c 1-48 | sed 's/^-*//; s/-*$//')
29
+ if [ -z "$sanitized" ]; then
30
+ sanitized="unknown"
31
+ fi
32
+ printf '%s' "$sanitized"
33
+ }
34
+
35
+ spw_manifest_version_for_ref() {
36
+ requested_ref="$1"
37
+ resolution_kind="$2"
38
+ resolved_ref="$3"
39
+ commit="$4"
40
+ short=$(spw_short_commit "$commit")
41
+
42
+ case "$resolution_kind" in
43
+ latest-release|tag)
44
+ base=$(printf '%s' "$resolved_ref" | sed -n 's/^v//p')
45
+ if [ -n "$base" ] && spw_is_semver_base "$base"; then
46
+ printf '%s+manager.%s\n' "$base" "$short"
47
+ return
48
+ fi
49
+ ;;
50
+ ref)
51
+ if [ "$requested_ref" = "main" ]; then
52
+ printf '0.0.0-main+manager.%s\n' "$short"
53
+ return
54
+ fi
55
+ sanitized=$(spw_sanitize_ref_for_version "$requested_ref")
56
+ printf '0.0.0-ref-%s+manager.%s\n' "$sanitized" "$short"
57
+ return
58
+ ;;
59
+ raw-commit)
60
+ ;;
61
+ esac
62
+
63
+ printf '0.0.0+manager.%s\n' "$short"
64
+ }
65
+
66
+ spw_manifest_version_for_commit() {
67
+ commit="$1"
68
+ spw_manifest_version_for_ref "$commit" "raw-commit" "$commit" "$commit"
69
+ }
70
+
71
+ spw_select_latest_release_from_ls_remote() {
72
+ awk '
73
+ $2 ~ /^refs\/tags\/v[0-9]+\.[0-9]+\.[0-9]+(\^\{\})?$/ {
74
+ ref = $2
75
+ peeled = 0
76
+ if (ref ~ /\^\{\}$/) {
77
+ peeled = 1
78
+ sub(/\^\{\}$/, "", ref)
79
+ }
80
+ tag = ref
81
+ sub(/^refs\/tags\//, "", tag)
82
+ split(substr(tag, 2), parts, ".")
83
+ key = sprintf("%010d.%010d.%010d", parts[1], parts[2], parts[3])
84
+ tag_by_key[key] = tag
85
+ if (peeled || !(tag in sha_by_tag)) {
86
+ sha_by_tag[tag] = $1
87
+ }
88
+ }
89
+ END {
90
+ for (key in tag_by_key) {
91
+ tag = tag_by_key[key]
92
+ print key, tag, sha_by_tag[tag]
93
+ }
94
+ }
95
+ ' | sort | tail -n 1 | awk '
96
+ NF == 3 { print $2 " " $3; found = 1 }
97
+ END { if (!found) exit 1 }
98
+ '
99
+ }
100
+
101
+ spw_resolve_ref() {
102
+ upstream_url="$1"
103
+ requested_ref="$2"
104
+ spw_require_command git
105
+
106
+ if [ "$requested_ref" = "latest-release" ]; then
107
+ if ! output=$(git ls-remote --tags "$upstream_url" 'refs/tags/v*' 2>&1); then
108
+ spw_die "cannot query upstream tags from $upstream_url: $output"
109
+ fi
110
+ if ! selected=$(printf '%s\n' "$output" | spw_select_latest_release_from_ls_remote); then
111
+ spw_die "no stable semver tag found for latest-release"
112
+ fi
113
+ printf 'latest-release %s\n' "$selected"
114
+ return
115
+ fi
116
+
117
+ if printf '%s' "$requested_ref" | grep -Eq '^[0-9a-fA-F]{40}$'; then
118
+ printf 'raw-commit %s %s\n' "$requested_ref" "$requested_ref"
119
+ return
120
+ fi
121
+
122
+ if ! tag_output=$(git ls-remote --tags "$upstream_url" "refs/tags/$requested_ref" "refs/tags/$requested_ref^{}" 2>&1); then
123
+ spw_die "cannot query upstream tag $requested_ref from $upstream_url: $tag_output"
124
+ fi
125
+ resolved=$(printf '%s\n' "$tag_output" | awk -v tag_ref="refs/tags/$requested_ref" '
126
+ NF >= 2 && ($2 == tag_ref || $2 == tag_ref "^{}") {
127
+ sha = $1
128
+ }
129
+ END { if (sha != "") print sha }
130
+ ')
131
+ if [ -n "$resolved" ]; then
132
+ printf 'tag %s %s\n' "$requested_ref" "$resolved"
133
+ return
134
+ fi
135
+
136
+ if ! ref_output=$(git ls-remote "$upstream_url" "$requested_ref" 2>&1); then
137
+ spw_die "cannot query upstream ref $requested_ref from $upstream_url: $ref_output"
138
+ fi
139
+ resolved=$(printf '%s\n' "$ref_output" | awk 'NF >= 2 { print $1; exit }')
140
+ if [ -n "$resolved" ]; then
141
+ printf 'ref %s %s\n' "$requested_ref" "$resolved"
142
+ return
143
+ fi
144
+
145
+ spw_die "cannot resolve upstream ref: $requested_ref"
146
+ }
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ ENVELOPE_KEYS = {"protocol", "operation", "ok", "messages", "result", "error"}
11
+ MESSAGE_KEYS = {"channel", "text"}
12
+ ERROR_KEYS = {"code", "message", "hints"}
13
+ OWNERSHIP_KEYS = {"plugin", "marketplace"}
14
+ VERIFICATION_HINT_KEYS = {"mismatch", "missing"}
15
+ OPERATIONS = {"build", "inspect", "install", "uninstall"}
16
+ FINGERPRINT_RE = re.compile(r"(?:[0-9a-fA-F]{7}|[0-9a-fA-F]{40})\Z")
17
+ MAX_NESTING = 64
18
+
19
+
20
+ class ProtocolError(ValueError):
21
+ pass
22
+
23
+
24
+ def reject_constant(value: str) -> None:
25
+ raise ProtocolError(f"non-standard JSON constant: {value}")
26
+
27
+
28
+ def nesting_exceeds_limit(value: object) -> bool:
29
+ stack = [(value, 0)]
30
+ while stack:
31
+ current, depth = stack.pop()
32
+ if isinstance(current, dict):
33
+ next_depth = depth + 1
34
+ if next_depth > MAX_NESTING:
35
+ return True
36
+ stack.extend((child, next_depth) for child in current.values())
37
+ elif isinstance(current, list):
38
+ next_depth = depth + 1
39
+ if next_depth > MAX_NESTING:
40
+ return True
41
+ stack.extend((child, next_depth) for child in current)
42
+ return False
43
+
44
+
45
+ def require_object(value: object, label: str) -> dict[str, object]:
46
+ if not isinstance(value, dict):
47
+ raise ProtocolError(f"{label} must be an object")
48
+ return value
49
+
50
+
51
+ def require_exact_keys(value: dict[str, object], expected: set[str], label: str) -> None:
52
+ actual = set(value)
53
+ if actual != expected:
54
+ raise ProtocolError(
55
+ f"{label} keys must be {sorted(expected)}, got {sorted(actual)}"
56
+ )
57
+
58
+
59
+ def contains_terminal_control(value: str) -> bool:
60
+ return any(
61
+ ord(character) < 0x20
62
+ or 0x7F <= ord(character) <= 0x9F
63
+ or 0xD800 <= ord(character) <= 0xDFFF
64
+ for character in value
65
+ )
66
+
67
+
68
+ def require_non_empty_string(value: object, label: str) -> str:
69
+ if not isinstance(value, str) or not value or "\n" in value or "\r" in value:
70
+ raise ProtocolError(f"{label} must be a non-empty single-line string")
71
+ if contains_terminal_control(value):
72
+ raise ProtocolError(f"{label} must not contain terminal control characters")
73
+ return value
74
+
75
+
76
+ def validate_messages(value: object) -> list[dict[str, str]]:
77
+ if not isinstance(value, list):
78
+ raise ProtocolError("messages must be an array")
79
+ messages: list[dict[str, str]] = []
80
+ for index, item in enumerate(value):
81
+ message = require_object(item, f"messages[{index}]")
82
+ require_exact_keys(message, MESSAGE_KEYS, f"messages[{index}]")
83
+ channel = message.get("channel")
84
+ if channel not in {"stdout", "stderr"}:
85
+ raise ProtocolError(f"messages[{index}].channel is invalid")
86
+ text = require_non_empty_string(message.get("text"), f"messages[{index}].text")
87
+ messages.append({"channel": str(channel), "text": text})
88
+ return messages
89
+
90
+
91
+ def validate_error(value: object) -> dict[str, object]:
92
+ error = require_object(value, "error")
93
+ require_exact_keys(error, ERROR_KEYS, "error")
94
+ code = require_non_empty_string(error.get("code"), "error.code")
95
+ message = require_non_empty_string(error.get("message"), "error.message")
96
+ hints_value = error.get("hints")
97
+ if not isinstance(hints_value, list):
98
+ raise ProtocolError("error.hints must be an array")
99
+ hints = [
100
+ require_non_empty_string(hint, f"error.hints[{index}]")
101
+ for index, hint in enumerate(hints_value)
102
+ ]
103
+ return {"code": code, "message": message, "hints": hints}
104
+
105
+
106
+ def validate_result(
107
+ operation: str, value: object, inspect_view: str | None
108
+ ) -> dict[str, object]:
109
+ result = require_object(value, "result")
110
+ if operation in {"build", "uninstall"}:
111
+ require_exact_keys(result, set(), f"{operation} result")
112
+ return {}
113
+ if operation == "inspect":
114
+ if inspect_view == "fingerprint":
115
+ require_exact_keys(result, {"view", "fingerprint"}, "inspect result")
116
+ if result.get("view") != "fingerprint":
117
+ raise ProtocolError("inspect result view must be fingerprint")
118
+ fingerprint = result.get("fingerprint")
119
+ if fingerprint is not None and (
120
+ not isinstance(fingerprint, str)
121
+ or FINGERPRINT_RE.fullmatch(fingerprint) is None
122
+ ):
123
+ raise ProtocolError("fingerprint must be null, 7 hex, or 40 hex")
124
+ return {"view": "fingerprint", "fingerprint": fingerprint}
125
+ if inspect_view == "ownership":
126
+ require_exact_keys(
127
+ result,
128
+ {"view", "resources", "legacy_resources", "identity_state"},
129
+ "inspect result",
130
+ )
131
+ if result.get("view") != "ownership":
132
+ raise ProtocolError("inspect result view must be ownership")
133
+ resources = require_object(result.get("resources"), "resources")
134
+ legacy_resources = require_object(
135
+ result.get("legacy_resources"), "legacy_resources"
136
+ )
137
+ for label, group in (
138
+ ("resources", resources),
139
+ ("legacy_resources", legacy_resources),
140
+ ):
141
+ require_exact_keys(group, OWNERSHIP_KEYS, label)
142
+ if not all(isinstance(group[key], bool) for key in OWNERSHIP_KEYS):
143
+ raise ProtocolError(f"{label} values must be Boolean")
144
+ manager_present = any(resources.values())
145
+ legacy_present = any(legacy_resources.values())
146
+ expected_state = {
147
+ (False, False): "neither",
148
+ (True, False): "manager",
149
+ (False, True): "legacy",
150
+ (True, True): "both",
151
+ }[(manager_present, legacy_present)]
152
+ identity_state = result.get("identity_state")
153
+ if identity_state != expected_state:
154
+ raise ProtocolError(
155
+ f"identity_state must be {expected_state} for the reported resources"
156
+ )
157
+ return {
158
+ "view": "ownership",
159
+ "resources": resources,
160
+ "legacy_resources": legacy_resources,
161
+ "identity_state": identity_state,
162
+ }
163
+ raise ProtocolError("inspect view must be fingerprint or ownership")
164
+ if operation == "install":
165
+ require_exact_keys(result, {"verification_hints"}, "install result")
166
+ hints = require_object(result.get("verification_hints"), "verification_hints")
167
+ unknown = set(hints) - VERIFICATION_HINT_KEYS
168
+ if unknown:
169
+ raise ProtocolError(f"unknown verification hint keys: {sorted(unknown)}")
170
+ normalized = {
171
+ key: require_non_empty_string(value, f"verification_hints.{key}")
172
+ for key, value in hints.items()
173
+ }
174
+ return {"verification_hints": normalized}
175
+ raise ProtocolError(f"unsupported operation: {operation}")
176
+
177
+
178
+ def validate_envelope(
179
+ raw: object, operation: str, adapter_exit: int, inspect_view: str | None
180
+ ) -> tuple[list[dict[str, str]], dict[str, object] | None, dict[str, object] | None]:
181
+ envelope = require_object(raw, "response")
182
+ require_exact_keys(envelope, ENVELOPE_KEYS, "response")
183
+ protocol = envelope.get("protocol")
184
+ if type(protocol) is not int or protocol != 1:
185
+ raise ProtocolError("protocol must equal integer 1")
186
+ if envelope.get("operation") != operation:
187
+ raise ProtocolError("response operation does not match invocation")
188
+ ok = envelope.get("ok")
189
+ if not isinstance(ok, bool):
190
+ raise ProtocolError("ok must be Boolean")
191
+ messages = validate_messages(envelope.get("messages"))
192
+ if ok:
193
+ if adapter_exit != 0:
194
+ raise ProtocolError("successful response requires adapter exit 0")
195
+ if envelope.get("error") is not None:
196
+ raise ProtocolError("successful response error must be null")
197
+ result = validate_result(operation, envelope.get("result"), inspect_view)
198
+ return messages, result, None
199
+ if adapter_exit == 0:
200
+ raise ProtocolError("failure response requires nonzero adapter exit")
201
+ if envelope.get("result") is not None:
202
+ raise ProtocolError("failure response result must be null")
203
+ error = validate_error(envelope.get("error"))
204
+ return messages, None, error
205
+
206
+
207
+ def replay(messages: list[dict[str, str]]) -> None:
208
+ for message in messages:
209
+ stream = sys.stdout if message["channel"] == "stdout" else sys.stderr
210
+ print(message["text"], file=stream)
211
+
212
+
213
+ def main() -> int:
214
+ parser = argparse.ArgumentParser()
215
+ parser.add_argument("--operation", choices=sorted(OPERATIONS), required=True)
216
+ parser.add_argument("--adapter-exit", type=int, required=True)
217
+ parser.add_argument("--response", type=Path, required=True)
218
+ parser.add_argument("--result", type=Path, required=True)
219
+ parser.add_argument("--inspect-view", choices=("fingerprint", "ownership"))
220
+ args = parser.parse_args()
221
+ try:
222
+ with args.response.open(encoding="utf-8") as handle:
223
+ raw = json.load(handle, parse_constant=reject_constant)
224
+ if nesting_exceeds_limit(raw):
225
+ raise ProtocolError("response JSON nesting exceeds limit")
226
+ messages, result, error = validate_envelope(
227
+ raw, args.operation, args.adapter_exit, args.inspect_view
228
+ )
229
+ replay(messages)
230
+ if error is not None:
231
+ print(f"error: {error['message']}", file=sys.stderr)
232
+ for hint in error["hints"]:
233
+ print(f"hint: {hint}", file=sys.stderr)
234
+ return 1
235
+ with args.result.open("w", encoding="utf-8") as handle:
236
+ json.dump(result, handle, allow_nan=False, separators=(",", ":"))
237
+ handle.write("\n")
238
+ return 0
239
+ except (OSError, UnicodeError, json.JSONDecodeError, ProtocolError, RecursionError) as exc:
240
+ print(f"error: invalid adapter response: {exc}", file=sys.stderr)
241
+ return 2
242
+
243
+
244
+ if __name__ == "__main__":
245
+ raise SystemExit(main())
package/scripts/install CHANGED
@@ -2,27 +2,22 @@
2
2
  set -eu
3
3
 
4
4
  root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
5
- . "$root/scripts/lib.sh"
5
+ . "$root/scripts/core/common.sh"
6
+ . "$root/scripts/core/provenance.sh"
7
+ . "$root/scripts/core/status.sh"
8
+ . "$root/scripts/core/lifecycle.sh"
9
+ . "$root/scripts/core/adapter.sh"
6
10
 
7
- codex_bin="${SUPERPOWERS_CODEX:-codex}"
8
- refresh_mode="${SUPERPOWERS_INSTALL_REFRESH_MODE:-add-only}"
9
-
10
- spw_require_command "$codex_bin"
11
11
  spw_require_command python3
12
12
 
13
- case "$refresh_mode" in
14
- add-only|remove-add) ;;
15
- *) spw_die "unsupported SUPERPOWERS_INSTALL_REFRESH_MODE: $refresh_mode" ;;
16
- esac
17
-
18
- echo "Note: remove or disable conflicting Superpowers providers yourself before relying on manager-provided skills."
13
+ echo "Note: remove or disable conflicting Superpowers providers yourself before relying on manager skills."
19
14
 
20
15
  # Prepare-before-mutate: probe, and (re)generate the plugin tree if it is
21
16
  # missing or stale — including validation — entirely before any Codex
22
17
  # marketplace or plugin mutation. A failed prepare leaves Codex untouched.
23
18
  probe_output=$(sh "$root/scripts/probe" --porcelain)
24
19
  identity_state=$(printf '%s\n' "$probe_output" | awk -F= '$1 == "identity_state" { print $2 }')
25
- [ -n "$identity_state" ] || spw_die "probe did not report Codex identity state"
20
+ [ -n "$identity_state" ] || spw_die "probe did not report adapter identity state"
26
21
  spw_require_no_legacy_state "$identity_state"
27
22
  status=$(printf '%s\n' "$probe_output" | awk -F= '$1 == "status" { print $2 }')
28
23
  case "$status" in
@@ -40,19 +35,26 @@ esac
40
35
  desired_commit=$(spw_metadata_commit_or_empty "$root/plugins/superpowers/.superpowers-upstream.json" || true)
41
36
  [ -n "$desired_commit" ] || spw_die "generated metadata missing desired commit after prepare"
42
37
 
43
- # Reconcile the manager marketplace pointer to this package root, then always
44
- # run the Codex install step: `install` means "make Codex use this root" and
45
- # never skips it because the generated tree looks current.
46
- spw_reconcile_marketplace "$codex_bin" "$root"
47
-
48
- case "$refresh_mode" in
49
- remove-add)
50
- "$codex_bin" plugin remove "$SPW_PLUGIN_ID" || true
51
- ;;
52
- esac
53
-
54
- if ! "$codex_bin" plugin add "$SPW_PLUGIN_ID"; then
55
- spw_die "codex plugin add failed for $SPW_PLUGIN_ID"
38
+ tmp_parent="${TMPDIR:-/tmp}"
39
+ if ! install_workspace=$(mktemp -d "$tmp_parent/superpowers-manager.install.XXXXXX"); then
40
+ spw_die "cannot create install workspace under $tmp_parent"
41
+ fi
42
+ cleanup_install_workspace() {
43
+ status=$?
44
+ trap - 0 HUP INT TERM
45
+ rm -rf "$install_workspace" || :
46
+ exit "$status"
47
+ }
48
+ trap cleanup_install_workspace 0
49
+ trap 'exit 129' HUP
50
+ trap 'exit 130' INT
51
+ trap 'exit 143' TERM
52
+
53
+ TMPDIR="$install_workspace"
54
+ export TMPDIR
55
+ install_result="$install_workspace/install.json"
56
+ inspect_result="$install_workspace/inspect.json"
57
+ spw_adapter_install "$install_result" "$root"
58
+ if ! spw_verify_installed_fingerprint "$desired_commit" "$install_result" "$inspect_result"; then
59
+ exit 1
56
60
  fi
57
-
58
- spw_verify_refresh "$desired_commit"