superpowers-manager 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -82
- package/bin/superpowers-manager.js +85 -30
- package/package.json +2 -1
- package/scripts/adapters/codex/adapter +603 -0
- package/scripts/adapters/codex/lib.sh +449 -0
- package/scripts/{validate-generated-plugin.py → adapters/codex/validate-generated-plugin.py} +3 -3
- package/scripts/core/adapter.sh +103 -0
- package/scripts/core/common.sh +59 -0
- package/scripts/core/lifecycle.sh +141 -0
- package/scripts/core/provenance.sh +135 -0
- package/scripts/core/selection-state.py +414 -0
- package/scripts/core/selection.sh +168 -0
- package/scripts/core/status.sh +27 -0
- package/scripts/core/upstream.sh +280 -0
- package/scripts/core/validate-adapter-response.py +284 -0
- package/scripts/install +30 -29
- package/scripts/pin +51 -0
- package/scripts/prepare +60 -79
- package/scripts/probe +62 -30
- package/scripts/track-latest +30 -0
- package/scripts/uninstall +21 -47
- package/scripts/unpin +41 -0
- package/scripts/update +8 -10
- package/scripts/lib.sh +0 -651
|
@@ -0,0 +1,284 @@
|
|
|
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
|
+
MAX_RESPONSE_BYTES = 1_048_576
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProtocolError(ValueError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def reject_constant(value: str) -> None:
|
|
26
|
+
raise ProtocolError(f"non-standard JSON constant: {value}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def reject_duplicate_keys(
|
|
30
|
+
pairs: list[tuple[str, object]],
|
|
31
|
+
) -> dict[str, object]:
|
|
32
|
+
obj: dict[str, object] = {}
|
|
33
|
+
for key, value in pairs:
|
|
34
|
+
if key in obj:
|
|
35
|
+
raise ProtocolError("duplicate object key")
|
|
36
|
+
obj[key] = value
|
|
37
|
+
return obj
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def nesting_exceeds_limit(value: object) -> bool:
|
|
41
|
+
stack = [(value, 0)]
|
|
42
|
+
while stack:
|
|
43
|
+
current, depth = stack.pop()
|
|
44
|
+
if isinstance(current, dict):
|
|
45
|
+
next_depth = depth + 1
|
|
46
|
+
if next_depth > MAX_NESTING:
|
|
47
|
+
return True
|
|
48
|
+
stack.extend((child, next_depth) for child in current.values())
|
|
49
|
+
elif isinstance(current, list):
|
|
50
|
+
next_depth = depth + 1
|
|
51
|
+
if next_depth > MAX_NESTING:
|
|
52
|
+
return True
|
|
53
|
+
stack.extend((child, next_depth) for child in current)
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def require_object(value: object, label: str) -> dict[str, object]:
|
|
58
|
+
if not isinstance(value, dict):
|
|
59
|
+
raise ProtocolError(f"{label} must be an object")
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def require_exact_keys(value: dict[str, object], expected: set[str], label: str) -> None:
|
|
64
|
+
actual = set(value)
|
|
65
|
+
if actual != expected:
|
|
66
|
+
raise ProtocolError(
|
|
67
|
+
f"{label} keys must be {sorted(expected)}, got {sorted(actual)}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def contains_terminal_control(value: str) -> bool:
|
|
72
|
+
return any(
|
|
73
|
+
ord(character) < 0x20
|
|
74
|
+
or 0x7F <= ord(character) <= 0x9F
|
|
75
|
+
or 0xD800 <= ord(character) <= 0xDFFF
|
|
76
|
+
for character in value
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def require_non_empty_string(value: object, label: str) -> str:
|
|
81
|
+
if not isinstance(value, str) or not value or "\n" in value or "\r" in value:
|
|
82
|
+
raise ProtocolError(f"{label} must be a non-empty single-line string")
|
|
83
|
+
if contains_terminal_control(value):
|
|
84
|
+
raise ProtocolError(f"{label} must not contain terminal control characters")
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate_messages(value: object) -> list[dict[str, str]]:
|
|
89
|
+
if not isinstance(value, list):
|
|
90
|
+
raise ProtocolError("messages must be an array")
|
|
91
|
+
messages: list[dict[str, str]] = []
|
|
92
|
+
for index, item in enumerate(value):
|
|
93
|
+
message = require_object(item, f"messages[{index}]")
|
|
94
|
+
require_exact_keys(message, MESSAGE_KEYS, f"messages[{index}]")
|
|
95
|
+
channel = message.get("channel")
|
|
96
|
+
if channel not in {"stdout", "stderr"}:
|
|
97
|
+
raise ProtocolError(f"messages[{index}].channel is invalid")
|
|
98
|
+
text = require_non_empty_string(message.get("text"), f"messages[{index}].text")
|
|
99
|
+
messages.append({"channel": str(channel), "text": text})
|
|
100
|
+
return messages
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def validate_error(value: object) -> dict[str, object]:
|
|
104
|
+
error = require_object(value, "error")
|
|
105
|
+
require_exact_keys(error, ERROR_KEYS, "error")
|
|
106
|
+
code = require_non_empty_string(error.get("code"), "error.code")
|
|
107
|
+
message = require_non_empty_string(error.get("message"), "error.message")
|
|
108
|
+
hints_value = error.get("hints")
|
|
109
|
+
if not isinstance(hints_value, list):
|
|
110
|
+
raise ProtocolError("error.hints must be an array")
|
|
111
|
+
hints = [
|
|
112
|
+
require_non_empty_string(hint, f"error.hints[{index}]")
|
|
113
|
+
for index, hint in enumerate(hints_value)
|
|
114
|
+
]
|
|
115
|
+
return {"code": code, "message": message, "hints": hints}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def validate_result(
|
|
119
|
+
operation: str, value: object, inspect_view: str | None
|
|
120
|
+
) -> dict[str, object]:
|
|
121
|
+
result = require_object(value, "result")
|
|
122
|
+
if operation in {"build", "uninstall"}:
|
|
123
|
+
require_exact_keys(result, set(), f"{operation} result")
|
|
124
|
+
return {}
|
|
125
|
+
if operation == "inspect":
|
|
126
|
+
if inspect_view == "fingerprint":
|
|
127
|
+
require_exact_keys(result, {"view", "fingerprint"}, "inspect result")
|
|
128
|
+
if result.get("view") != "fingerprint":
|
|
129
|
+
raise ProtocolError("inspect result view must be fingerprint")
|
|
130
|
+
fingerprint = result.get("fingerprint")
|
|
131
|
+
if fingerprint is not None and (
|
|
132
|
+
not isinstance(fingerprint, str)
|
|
133
|
+
or FINGERPRINT_RE.fullmatch(fingerprint) is None
|
|
134
|
+
):
|
|
135
|
+
raise ProtocolError("fingerprint must be null, 7 hex, or 40 hex")
|
|
136
|
+
return {"view": "fingerprint", "fingerprint": fingerprint}
|
|
137
|
+
if inspect_view == "ownership":
|
|
138
|
+
require_exact_keys(
|
|
139
|
+
result,
|
|
140
|
+
{"view", "resources", "legacy_resources", "identity_state"},
|
|
141
|
+
"inspect result",
|
|
142
|
+
)
|
|
143
|
+
if result.get("view") != "ownership":
|
|
144
|
+
raise ProtocolError("inspect result view must be ownership")
|
|
145
|
+
resources = require_object(result.get("resources"), "resources")
|
|
146
|
+
legacy_resources = require_object(
|
|
147
|
+
result.get("legacy_resources"), "legacy_resources"
|
|
148
|
+
)
|
|
149
|
+
for label, group in (
|
|
150
|
+
("resources", resources),
|
|
151
|
+
("legacy_resources", legacy_resources),
|
|
152
|
+
):
|
|
153
|
+
require_exact_keys(group, OWNERSHIP_KEYS, label)
|
|
154
|
+
if not all(isinstance(group[key], bool) for key in OWNERSHIP_KEYS):
|
|
155
|
+
raise ProtocolError(f"{label} values must be Boolean")
|
|
156
|
+
manager_present = any(resources.values())
|
|
157
|
+
legacy_present = any(legacy_resources.values())
|
|
158
|
+
expected_state = {
|
|
159
|
+
(False, False): "neither",
|
|
160
|
+
(True, False): "manager",
|
|
161
|
+
(False, True): "legacy",
|
|
162
|
+
(True, True): "both",
|
|
163
|
+
}[(manager_present, legacy_present)]
|
|
164
|
+
identity_state = result.get("identity_state")
|
|
165
|
+
if identity_state != expected_state:
|
|
166
|
+
raise ProtocolError(
|
|
167
|
+
f"identity_state must be {expected_state} for the reported resources"
|
|
168
|
+
)
|
|
169
|
+
return {
|
|
170
|
+
"view": "ownership",
|
|
171
|
+
"resources": resources,
|
|
172
|
+
"legacy_resources": legacy_resources,
|
|
173
|
+
"identity_state": identity_state,
|
|
174
|
+
}
|
|
175
|
+
if inspect_view == "update-control":
|
|
176
|
+
require_exact_keys(
|
|
177
|
+
result,
|
|
178
|
+
{"view", "update_control"},
|
|
179
|
+
"update-control inspect result",
|
|
180
|
+
)
|
|
181
|
+
if result.get("view") != "update-control":
|
|
182
|
+
raise ProtocolError("inspect result view must be update-control")
|
|
183
|
+
value = result.get("update_control")
|
|
184
|
+
if value not in {"managed", "unsupported"}:
|
|
185
|
+
raise ProtocolError(
|
|
186
|
+
"update-control value must be managed or unsupported"
|
|
187
|
+
)
|
|
188
|
+
return {"view": "update-control", "update_control": value}
|
|
189
|
+
raise ProtocolError(
|
|
190
|
+
"inspect view must be fingerprint or ownership, or update-control"
|
|
191
|
+
)
|
|
192
|
+
if operation == "install":
|
|
193
|
+
require_exact_keys(result, {"verification_hints"}, "install result")
|
|
194
|
+
hints = require_object(result.get("verification_hints"), "verification_hints")
|
|
195
|
+
unknown = set(hints) - VERIFICATION_HINT_KEYS
|
|
196
|
+
if unknown:
|
|
197
|
+
raise ProtocolError(f"unknown verification hint keys: {sorted(unknown)}")
|
|
198
|
+
normalized = {
|
|
199
|
+
key: require_non_empty_string(value, f"verification_hints.{key}")
|
|
200
|
+
for key, value in hints.items()
|
|
201
|
+
}
|
|
202
|
+
return {"verification_hints": normalized}
|
|
203
|
+
raise ProtocolError(f"unsupported operation: {operation}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def validate_envelope(
|
|
207
|
+
raw: object, operation: str, adapter_exit: int, inspect_view: str | None
|
|
208
|
+
) -> tuple[list[dict[str, str]], dict[str, object] | None, dict[str, object] | None]:
|
|
209
|
+
envelope = require_object(raw, "response")
|
|
210
|
+
require_exact_keys(envelope, ENVELOPE_KEYS, "response")
|
|
211
|
+
protocol = envelope.get("protocol")
|
|
212
|
+
if type(protocol) is not int or protocol != 1:
|
|
213
|
+
raise ProtocolError("protocol must equal integer 1")
|
|
214
|
+
if envelope.get("operation") != operation:
|
|
215
|
+
raise ProtocolError("response operation does not match invocation")
|
|
216
|
+
ok = envelope.get("ok")
|
|
217
|
+
if not isinstance(ok, bool):
|
|
218
|
+
raise ProtocolError("ok must be Boolean")
|
|
219
|
+
messages = validate_messages(envelope.get("messages"))
|
|
220
|
+
if ok:
|
|
221
|
+
if adapter_exit != 0:
|
|
222
|
+
raise ProtocolError("successful response requires adapter exit 0")
|
|
223
|
+
if envelope.get("error") is not None:
|
|
224
|
+
raise ProtocolError("successful response error must be null")
|
|
225
|
+
result = validate_result(operation, envelope.get("result"), inspect_view)
|
|
226
|
+
return messages, result, None
|
|
227
|
+
if adapter_exit == 0:
|
|
228
|
+
raise ProtocolError("failure response requires nonzero adapter exit")
|
|
229
|
+
if envelope.get("result") is not None:
|
|
230
|
+
raise ProtocolError("failure response result must be null")
|
|
231
|
+
error = validate_error(envelope.get("error"))
|
|
232
|
+
return messages, None, error
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def replay(messages: list[dict[str, str]]) -> None:
|
|
236
|
+
for message in messages:
|
|
237
|
+
stream = sys.stdout if message["channel"] == "stdout" else sys.stderr
|
|
238
|
+
print(message["text"], file=stream)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def main() -> int:
|
|
242
|
+
parser = argparse.ArgumentParser()
|
|
243
|
+
parser.add_argument("--operation", choices=sorted(OPERATIONS), required=True)
|
|
244
|
+
parser.add_argument("--adapter-exit", type=int, required=True)
|
|
245
|
+
parser.add_argument("--response", type=Path, required=True)
|
|
246
|
+
parser.add_argument("--result", type=Path, required=True)
|
|
247
|
+
parser.add_argument(
|
|
248
|
+
"--inspect-view", choices=("fingerprint", "ownership", "update-control")
|
|
249
|
+
)
|
|
250
|
+
args = parser.parse_args()
|
|
251
|
+
try:
|
|
252
|
+
response_size = args.response.stat().st_size
|
|
253
|
+
if response_size > MAX_RESPONSE_BYTES:
|
|
254
|
+
raise ProtocolError(
|
|
255
|
+
f"response exceeds {MAX_RESPONSE_BYTES}-byte limit"
|
|
256
|
+
)
|
|
257
|
+
with args.response.open(encoding="utf-8") as handle:
|
|
258
|
+
raw = json.load(
|
|
259
|
+
handle,
|
|
260
|
+
parse_constant=reject_constant,
|
|
261
|
+
object_pairs_hook=reject_duplicate_keys,
|
|
262
|
+
)
|
|
263
|
+
if nesting_exceeds_limit(raw):
|
|
264
|
+
raise ProtocolError("response JSON nesting exceeds limit")
|
|
265
|
+
messages, result, error = validate_envelope(
|
|
266
|
+
raw, args.operation, args.adapter_exit, args.inspect_view
|
|
267
|
+
)
|
|
268
|
+
replay(messages)
|
|
269
|
+
if error is not None:
|
|
270
|
+
print(f"error: {error['message']}", file=sys.stderr)
|
|
271
|
+
for hint in error["hints"]:
|
|
272
|
+
print(f"hint: {hint}", file=sys.stderr)
|
|
273
|
+
return 1
|
|
274
|
+
with args.result.open("w", encoding="utf-8") as handle:
|
|
275
|
+
json.dump(result, handle, allow_nan=False, separators=(",", ":"))
|
|
276
|
+
handle.write("\n")
|
|
277
|
+
return 0
|
|
278
|
+
except (OSError, UnicodeError, json.JSONDecodeError, ProtocolError, RecursionError) as exc:
|
|
279
|
+
print(f"error: invalid adapter response: {exc}", file=sys.stderr)
|
|
280
|
+
return 2
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
raise SystemExit(main())
|
package/scripts/install
CHANGED
|
@@ -2,29 +2,24 @@
|
|
|
2
2
|
set -eu
|
|
3
3
|
|
|
4
4
|
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
|
5
|
-
. "$root/scripts/
|
|
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
|
-
|
|
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
|
-
identity_state=$(
|
|
25
|
-
[ -n "$identity_state" ] || spw_die "probe did not report
|
|
19
|
+
identity_state=$(spw_probe_field "$probe_output" 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
|
-
status=$(
|
|
22
|
+
status=$(spw_probe_field "$probe_output" status)
|
|
28
23
|
case "$status" in
|
|
29
24
|
needs\ prepare)
|
|
30
25
|
sh "$root/scripts/prepare"
|
|
@@ -40,19 +35,25 @@ 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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
tmp_parent="${TMPDIR:-/tmp}"
|
|
39
|
+
if ! install_workspace=$(spw_make_workspace "$tmp_parent" "superpowers-manager.install"); then
|
|
40
|
+
spw_die "cannot create install workspace under $tmp_parent"
|
|
41
|
+
fi
|
|
42
|
+
spw_install_workspace_trap "$install_workspace"
|
|
43
|
+
|
|
44
|
+
TMPDIR="$install_workspace"
|
|
45
|
+
export TMPDIR
|
|
46
|
+
install_result="$install_workspace/install.json"
|
|
47
|
+
inspect_result="$install_workspace/inspect.json"
|
|
48
|
+
ownership_result="$install_workspace/ownership.json"
|
|
49
|
+
update_control_result="$install_workspace/update-control.json"
|
|
50
|
+
spw_inspect_ownership "$ownership_result"
|
|
51
|
+
spw_inspect_update_control "$update_control_result"
|
|
52
|
+
identity_state=$(spw_adapter_result_get "$ownership_result" "identity_state")
|
|
53
|
+
update_control=$(spw_adapter_result_get "$update_control_result" "update_control")
|
|
54
|
+
spw_require_no_legacy_state "$identity_state"
|
|
55
|
+
spw_require_managed_update_control "$update_control"
|
|
56
|
+
spw_adapter_install "$install_result" "$root"
|
|
57
|
+
if ! spw_verify_installed_fingerprint "$desired_commit" "$install_result" "$inspect_result"; then
|
|
58
|
+
exit 1
|
|
56
59
|
fi
|
|
57
|
-
|
|
58
|
-
spw_verify_refresh "$desired_commit"
|
package/scripts/pin
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
|
5
|
+
. "$root/scripts/core/common.sh"
|
|
6
|
+
. "$root/scripts/core/upstream.sh"
|
|
7
|
+
. "$root/scripts/core/selection.sh"
|
|
8
|
+
|
|
9
|
+
[ "$#" -eq 1 ] || spw_usage_error 'usage: superpowers-manager pin REF'
|
|
10
|
+
requested_ref="$1"
|
|
11
|
+
|
|
12
|
+
if spw_is_pinnable_tag "$requested_ref"; then
|
|
13
|
+
requested_kind=tag
|
|
14
|
+
elif spw_is_full_commit "$requested_ref"; then
|
|
15
|
+
requested_kind=raw-commit
|
|
16
|
+
else
|
|
17
|
+
spw_usage_error 'pin REF must be an exact v-prefixed SemVer tag or full 40-hex commit'
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
spw_require_command python3
|
|
21
|
+
state_path=$(spw_selection_state_path)
|
|
22
|
+
tmp_parent="${TMPDIR:-/tmp}"
|
|
23
|
+
if ! pin_workspace=$(spw_make_workspace "$tmp_parent" "superpowers-manager.pin"); then
|
|
24
|
+
spw_die "cannot create pin workspace under $tmp_parent"
|
|
25
|
+
fi
|
|
26
|
+
spw_install_workspace_trap "$pin_workspace"
|
|
27
|
+
|
|
28
|
+
normalized="$pin_workspace/selection.json"
|
|
29
|
+
spw_selection_state "$root" read \
|
|
30
|
+
--path="$state_path" --output="$normalized"
|
|
31
|
+
|
|
32
|
+
source="${SUPERPOWERS_UPSTREAM_URL:-$SPW_UPSTREAM_URL_DEFAULT}"
|
|
33
|
+
spw_selection_state "$root" \
|
|
34
|
+
validate-source --source="$source"
|
|
35
|
+
|
|
36
|
+
if [ "$requested_kind" = tag ]; then
|
|
37
|
+
spw_require_command git
|
|
38
|
+
commit=$(spw_resolve_exact_tag "$source" "$requested_ref")
|
|
39
|
+
resolved_ref="$requested_ref"
|
|
40
|
+
else
|
|
41
|
+
spw_require_command git
|
|
42
|
+
requested_ref=$(printf '%s' "$requested_ref" | tr '[:upper:]' '[:lower:]')
|
|
43
|
+
resolved_ref="$requested_ref"
|
|
44
|
+
commit=$(spw_verify_raw_commit "$source" "$requested_ref" "$pin_workspace")
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
spw_selection_state "$root" write-pinned \
|
|
48
|
+
--path="$state_path" --source="$source" --requested-ref="$requested_ref" \
|
|
49
|
+
--resolved-ref="$resolved_ref" --commit="$commit"
|
|
50
|
+
|
|
51
|
+
printf 'pinned upstream selection to %s at %s\n' "$requested_ref" "$commit"
|
package/scripts/prepare
CHANGED
|
@@ -2,54 +2,63 @@
|
|
|
2
2
|
set -eu
|
|
3
3
|
|
|
4
4
|
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
|
5
|
-
. "$root/scripts/
|
|
5
|
+
. "$root/scripts/core/common.sh"
|
|
6
|
+
. "$root/scripts/core/upstream.sh"
|
|
7
|
+
. "$root/scripts/core/selection.sh"
|
|
8
|
+
. "$root/scripts/core/provenance.sh"
|
|
9
|
+
. "$root/scripts/core/lifecycle.sh"
|
|
10
|
+
. "$root/scripts/core/adapter.sh"
|
|
6
11
|
|
|
7
|
-
upstream_url="${SUPERPOWERS_UPSTREAM_URL:-$SPW_UPSTREAM_URL_DEFAULT}"
|
|
8
12
|
cache_parent="${SUPERPOWERS_CACHE_DIR:-$root/.cache/upstream}"
|
|
9
|
-
cache="$cache_parent/superpowers"
|
|
10
13
|
plugin_root="${SUPERPOWERS_PLUGIN_ROOT:-$root/plugins/superpowers}"
|
|
11
14
|
manifest_template="${SUPERPOWERS_MANIFEST_TEMPLATE:-$root/plugins/superpowers/.codex-plugin/plugin.template.json}"
|
|
12
|
-
builtin_validator="$root/scripts/validate-generated-plugin.py"
|
|
13
15
|
additional_validator="${SUPERPOWERS_VALIDATOR:-}"
|
|
16
|
+
invocation_root=$(pwd -P)
|
|
17
|
+
case "$cache_parent" in
|
|
18
|
+
/*) ;;
|
|
19
|
+
*) cache_parent="$invocation_root/$cache_parent" ;;
|
|
20
|
+
esac
|
|
21
|
+
case "$plugin_root" in
|
|
22
|
+
/*) ;;
|
|
23
|
+
*) plugin_root="$invocation_root/$plugin_root" ;;
|
|
24
|
+
esac
|
|
25
|
+
cache="$cache_parent/superpowers"
|
|
14
26
|
tmp_parent=$(dirname "$plugin_root")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
trap cleanup_tmp_plugin 0
|
|
25
|
-
trap 'exit 129' HUP
|
|
26
|
-
trap 'exit 130' INT
|
|
27
|
-
trap 'exit 143' TERM
|
|
27
|
+
mkdir -p "$tmp_parent"
|
|
28
|
+
if ! prepare_workspace=$(spw_make_workspace "$tmp_parent" ".superpowers.prepare"); then
|
|
29
|
+
spw_die "cannot create prepare workspace under $tmp_parent"
|
|
30
|
+
fi
|
|
31
|
+
spw_install_workspace_trap "$prepare_workspace"
|
|
32
|
+
tmp_plugin="$prepare_workspace/superpowers"
|
|
33
|
+
adapter_result="$prepare_workspace/adapter-build.json"
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
TMPDIR="$prepare_workspace"
|
|
36
|
+
export TMPDIR
|
|
30
37
|
|
|
31
|
-
spw_require_command git
|
|
32
38
|
spw_require_command python3
|
|
39
|
+
spw_compute_effective_selection "$root" "$prepare_workspace"
|
|
40
|
+
spw_require_command git
|
|
41
|
+
spw_require_command "$SPW_ADAPTER"
|
|
33
42
|
[ -f "$manifest_template" ] || spw_die "missing fallback manifest template: $manifest_template"
|
|
34
|
-
[ -f "$builtin_validator" ] || spw_die "missing built-in plugin validator: $builtin_validator"
|
|
35
|
-
|
|
36
|
-
resolved_record=$(spw_resolve_ref "$upstream_url" "$requested_ref")
|
|
37
|
-
resolution_kind=$(printf '%s\n' "$resolved_record" | awk '{ print $1 }')
|
|
38
|
-
resolved_ref=$(printf '%s\n' "$resolved_record" | awk '{ print $2 }')
|
|
39
|
-
commit=$(printf '%s\n' "$resolved_record" | awk '{ print $3 }')
|
|
40
43
|
|
|
41
44
|
mkdir -p "$cache_parent" "$tmp_parent"
|
|
42
|
-
if [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
fi
|
|
45
|
+
if [ "$SPW_SELECTION_MODE" = "pinned" ]; then
|
|
46
|
+
spw_fetch_exact_commit \
|
|
47
|
+
"$SPW_EFFECTIVE_SOURCE" "$SPW_DESIRED_COMMIT" "$cache" "$prepare_workspace"
|
|
46
48
|
else
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
git_source=$(spw_git_safe_source "$SPW_EFFECTIVE_SOURCE")
|
|
50
|
+
if [ -d "$cache/.git" ]; then
|
|
51
|
+
if ! git -C "$cache" fetch --tags --prune -- "$git_source"; then
|
|
52
|
+
spw_die "cannot fetch upstream repo: $SPW_EFFECTIVE_SOURCE"
|
|
53
|
+
fi
|
|
54
|
+
else
|
|
55
|
+
if ! git clone -- "$git_source" "$cache"; then
|
|
56
|
+
spw_die "cannot clone upstream repo: $SPW_EFFECTIVE_SOURCE"
|
|
57
|
+
fi
|
|
49
58
|
fi
|
|
50
59
|
fi
|
|
51
|
-
if ! git -C "$cache" checkout --detach "$
|
|
52
|
-
spw_die "cannot check out upstream commit: $
|
|
60
|
+
if ! git -C "$cache" checkout --detach "$SPW_DESIRED_COMMIT" >/dev/null; then
|
|
61
|
+
spw_die "cannot check out upstream commit: $SPW_DESIRED_COMMIT"
|
|
53
62
|
fi
|
|
54
63
|
|
|
55
64
|
spw_require_upstream_path "$cache/skills" "skills/"
|
|
@@ -60,11 +69,6 @@ spw_require_upstream_path "$cache/CODE_OF_CONDUCT.md" "CODE_OF_CONDUCT.md"
|
|
|
60
69
|
rm -rf "$tmp_plugin"
|
|
61
70
|
mkdir -p "$tmp_plugin/.codex-plugin"
|
|
62
71
|
upstream_manifest="$cache/.codex-plugin/plugin.json"
|
|
63
|
-
if [ -f "$upstream_manifest" ]; then
|
|
64
|
-
cp "$upstream_manifest" "$tmp_plugin/.codex-plugin/plugin.json"
|
|
65
|
-
else
|
|
66
|
-
cp "$manifest_template" "$tmp_plugin/.codex-plugin/plugin.json"
|
|
67
|
-
fi
|
|
68
72
|
|
|
69
73
|
spw_copy_path_if_present "$cache/skills" "$tmp_plugin/skills"
|
|
70
74
|
spw_copy_path_if_present "$cache/assets" "$tmp_plugin/assets"
|
|
@@ -79,32 +83,26 @@ fi
|
|
|
79
83
|
|
|
80
84
|
spw_write_metadata_json \
|
|
81
85
|
"$tmp_plugin/.superpowers-upstream.json" \
|
|
82
|
-
"$
|
|
83
|
-
"$
|
|
84
|
-
"$
|
|
85
|
-
"$
|
|
86
|
+
"$SPW_EFFECTIVE_SOURCE" \
|
|
87
|
+
"$SPW_REQUESTED_REF" \
|
|
88
|
+
"$SPW_RESOLVED_REF" \
|
|
89
|
+
"$SPW_DESIRED_COMMIT" \
|
|
86
90
|
"$upstream_manifest_version"
|
|
87
91
|
|
|
88
92
|
manager_version=$(
|
|
89
|
-
spw_manifest_version_for_ref
|
|
93
|
+
spw_manifest_version_for_ref \
|
|
94
|
+
"$SPW_REQUESTED_REF" "$SPW_RESOLUTION_KIND" \
|
|
95
|
+
"$SPW_RESOLVED_REF" "$SPW_DESIRED_COMMIT"
|
|
90
96
|
)
|
|
91
|
-
|
|
92
|
-
"$
|
|
93
|
-
"$
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
--
|
|
100
|
-
--source "$upstream_url" \
|
|
101
|
-
--requested-ref "$requested_ref" \
|
|
102
|
-
--resolved-ref "$resolved_ref" \
|
|
103
|
-
--commit "$commit" \
|
|
104
|
-
--manifest-version "$manager_version" \
|
|
105
|
-
--upstream-manifest-version "$upstream_manifest_version"; then
|
|
106
|
-
spw_die "built-in generated plugin validation failed"
|
|
107
|
-
fi
|
|
97
|
+
spw_invoke_adapter build "$adapter_result" "" -- \
|
|
98
|
+
--upstream-root "$cache" \
|
|
99
|
+
--candidate-root "$tmp_plugin" \
|
|
100
|
+
--requested-ref "$SPW_REQUESTED_REF" \
|
|
101
|
+
--resolved-ref "$SPW_RESOLVED_REF" \
|
|
102
|
+
--commit "$SPW_DESIRED_COMMIT" \
|
|
103
|
+
--manager-version "$manager_version" \
|
|
104
|
+
--upstream-manifest-version "$upstream_manifest_version" \
|
|
105
|
+
--fallback-manifest "$manifest_template"
|
|
108
106
|
|
|
109
107
|
if [ -n "$additional_validator" ]; then
|
|
110
108
|
[ -f "$additional_validator" ] || \
|
|
@@ -114,23 +112,6 @@ if [ -n "$additional_validator" ]; then
|
|
|
114
112
|
fi
|
|
115
113
|
fi
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
# failed swap restores the previous working generated tree (the spec requires
|
|
119
|
-
# that a failed prepare never destroy it). tmp_plugin is a same-filesystem
|
|
120
|
-
# sibling of plugin_root, so the renames cannot fail mid-copy.
|
|
121
|
-
backup="$tmp_parent/.superpowers.bak.$$"
|
|
122
|
-
rm -rf "$backup"
|
|
123
|
-
if [ -e "$plugin_root" ]; then
|
|
124
|
-
mv "$plugin_root" "$backup"
|
|
125
|
-
fi
|
|
126
|
-
if mv "$tmp_plugin" "$plugin_root"; then
|
|
127
|
-
rm -rf "$backup"
|
|
128
|
-
else
|
|
129
|
-
if [ -e "$backup" ]; then
|
|
130
|
-
mv "$backup" "$plugin_root"
|
|
131
|
-
fi
|
|
132
|
-
rm -rf "$tmp_plugin"
|
|
133
|
-
spw_die "failed to install generated tree into $plugin_root; previous tree restored"
|
|
134
|
-
fi
|
|
115
|
+
spw_replace_generated_tree "$tmp_plugin" "$plugin_root"
|
|
135
116
|
|
|
136
|
-
echo "prepared $
|
|
117
|
+
echo "prepared $SPW_RESOLVED_REF at $SPW_DESIRED_COMMIT"
|