superpowers-manager 0.2.0 → 0.3.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 +112 -47
- package/bin/superpowers-manager.js +9 -283
- package/dist/adapter-result.js +204 -0
- package/dist/adapter.js +649 -0
- package/dist/atomic.js +142 -0
- package/dist/cli-arguments.js +32 -0
- package/dist/cli.js +293 -0
- package/dist/codex-json.js +89 -0
- package/dist/codex-state.js +78 -0
- package/dist/commands/context.js +1 -0
- package/dist/commands/install.js +376 -0
- package/dist/commands/pin.js +79 -0
- package/dist/commands/prepare.js +527 -0
- package/dist/commands/probe.js +392 -0
- package/dist/commands/track-latest.js +44 -0
- package/dist/commands/uninstall.js +308 -0
- package/dist/commands/unpin.js +95 -0
- package/dist/commands/update.js +138 -0
- package/dist/domain/fingerprint.js +4 -0
- package/dist/domain/refs.js +37 -0
- package/dist/effective-selection.js +123 -0
- package/dist/generated-plugin.js +748 -0
- package/dist/git.js +38 -0
- package/dist/hooks.js +290 -0
- package/dist/lifecycle.js +220 -0
- package/dist/manifest-overlay.js +125 -0
- package/dist/provenance.js +111 -0
- package/dist/python-json-format.js +83 -0
- package/dist/python-json.js +29 -0
- package/dist/python-text.js +74 -0
- package/dist/safe-path.js +137 -0
- package/dist/safety-error.js +10 -0
- package/dist/selection-state-cli.js +85 -0
- package/dist/selection-store.js +172 -0
- package/dist/selection.js +200 -0
- package/dist/status.js +24 -0
- package/dist/strict-json.js +248 -0
- package/dist/upstream-cli.js +110 -0
- package/dist/upstream-version.js +40 -0
- package/dist/upstream.js +253 -0
- package/dist/validate-generated-plugin-cli.js +118 -0
- package/dist/validator.js +317 -0
- package/dist/workspace.js +139 -0
- package/package.json +25 -2
- package/scripts/adapters/codex/adapter +0 -603
- package/scripts/adapters/codex/lib.sh +0 -449
- package/scripts/adapters/codex/validate-generated-plugin.py +0 -314
- package/scripts/core/adapter.sh +0 -103
- package/scripts/core/common.sh +0 -59
- package/scripts/core/lifecycle.sh +0 -141
- package/scripts/core/provenance.sh +0 -135
- package/scripts/core/selection-state.py +0 -414
- package/scripts/core/selection.sh +0 -168
- package/scripts/core/status.sh +0 -27
- package/scripts/core/upstream.sh +0 -280
- package/scripts/core/validate-adapter-response.py +0 -284
- package/scripts/install +0 -59
- package/scripts/pin +0 -51
- package/scripts/prepare +0 -117
- package/scripts/probe +0 -82
- package/scripts/track-latest +0 -30
- package/scripts/uninstall +0 -35
- package/scripts/unpin +0 -41
- package/scripts/update +0 -34
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Validate the manager-owned generated Superpowers plugin contract."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import re
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Any, Sequence
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
SEMVER_RE = re.compile(
|
|
16
|
-
r"^(0|[1-9][0-9]*)\."
|
|
17
|
-
r"(0|[1-9][0-9]*)\."
|
|
18
|
-
r"(0|[1-9][0-9]*)"
|
|
19
|
-
r"(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
|
|
20
|
-
r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?"
|
|
21
|
-
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
|
|
22
|
-
)
|
|
23
|
-
MAX_JSON_NESTING = 256
|
|
24
|
-
PROVENANCE_KEYS = {
|
|
25
|
-
"source",
|
|
26
|
-
"requested_ref",
|
|
27
|
-
"resolved_ref",
|
|
28
|
-
"commit",
|
|
29
|
-
"upstream_manifest_version",
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def reject_json_constant(constant: str) -> None:
|
|
34
|
-
raise ValueError(f"non-standard numeric constant: {constant}")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def json_nesting_exceeds_limit(value: Any) -> bool:
|
|
38
|
-
stack = [(value, 0)]
|
|
39
|
-
while stack:
|
|
40
|
-
current, depth = stack.pop()
|
|
41
|
-
if isinstance(current, dict):
|
|
42
|
-
next_depth = depth + 1
|
|
43
|
-
if next_depth > MAX_JSON_NESTING:
|
|
44
|
-
return True
|
|
45
|
-
stack.extend((child, next_depth) for child in current.values())
|
|
46
|
-
elif isinstance(current, list):
|
|
47
|
-
next_depth = depth + 1
|
|
48
|
-
if next_depth > MAX_JSON_NESTING:
|
|
49
|
-
return True
|
|
50
|
-
stack.extend((child, next_depth) for child in current)
|
|
51
|
-
return False
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
55
|
-
parser = argparse.ArgumentParser(
|
|
56
|
-
description="Validate a generated Superpowers manager candidate."
|
|
57
|
-
)
|
|
58
|
-
parser.add_argument("--plugin-root", required=True)
|
|
59
|
-
parser.add_argument("--source", required=True)
|
|
60
|
-
parser.add_argument("--requested-ref", required=True)
|
|
61
|
-
parser.add_argument("--resolved-ref", required=True)
|
|
62
|
-
parser.add_argument("--commit", required=True)
|
|
63
|
-
parser.add_argument("--manifest-version", required=True)
|
|
64
|
-
parser.add_argument("--upstream-manifest-version", required=True)
|
|
65
|
-
return parser.parse_args(argv)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def load_json_object(path: Path, label: str, errors: list[str]) -> dict[str, Any] | None:
|
|
69
|
-
try:
|
|
70
|
-
text = path.read_text(encoding="utf-8")
|
|
71
|
-
except (OSError, UnicodeError):
|
|
72
|
-
errors.append(f"{label} is unreadable UTF-8")
|
|
73
|
-
return None
|
|
74
|
-
try:
|
|
75
|
-
value = json.loads(text, parse_constant=reject_json_constant)
|
|
76
|
-
except RecursionError:
|
|
77
|
-
errors.append(f"{label} exceeds maximum JSON nesting")
|
|
78
|
-
return None
|
|
79
|
-
except (json.JSONDecodeError, ValueError):
|
|
80
|
-
errors.append(f"{label} must contain valid JSON")
|
|
81
|
-
return None
|
|
82
|
-
if json_nesting_exceeds_limit(value):
|
|
83
|
-
errors.append(f"{label} exceeds maximum JSON nesting")
|
|
84
|
-
return None
|
|
85
|
-
if not isinstance(value, dict):
|
|
86
|
-
errors.append(f"{label} must contain a JSON object")
|
|
87
|
-
return None
|
|
88
|
-
return value
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def validate_local_path(
|
|
92
|
-
plugin_root: Path,
|
|
93
|
-
raw_value: Any,
|
|
94
|
-
label: str,
|
|
95
|
-
errors: list[str],
|
|
96
|
-
*,
|
|
97
|
-
require_directory: bool = False,
|
|
98
|
-
) -> None:
|
|
99
|
-
if not isinstance(raw_value, str) or not raw_value.strip():
|
|
100
|
-
errors.append(f"{label} must be a non-empty relative path")
|
|
101
|
-
return
|
|
102
|
-
raw_path = Path(raw_value)
|
|
103
|
-
if raw_path.is_absolute():
|
|
104
|
-
errors.append(f"{label} must be a relative path")
|
|
105
|
-
return
|
|
106
|
-
try:
|
|
107
|
-
root = plugin_root.resolve()
|
|
108
|
-
target = (root / raw_path).resolve(strict=False)
|
|
109
|
-
except (OSError, RuntimeError, ValueError):
|
|
110
|
-
errors.append(f"{label} could not be resolved")
|
|
111
|
-
return
|
|
112
|
-
try:
|
|
113
|
-
target.relative_to(root)
|
|
114
|
-
except ValueError:
|
|
115
|
-
errors.append(f"{label} escapes the plugin root")
|
|
116
|
-
return
|
|
117
|
-
try:
|
|
118
|
-
if not target.exists():
|
|
119
|
-
errors.append(f"{label} target `{raw_value}` does not exist")
|
|
120
|
-
elif require_directory and not target.is_dir():
|
|
121
|
-
errors.append(f"{label} target `{raw_value}` must be a directory")
|
|
122
|
-
except OSError:
|
|
123
|
-
errors.append(f"{label} target `{raw_value}` could not be inspected")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def validate_manifest(plugin_root: Path, expected_version: str, errors: list[str]) -> None:
|
|
127
|
-
path = plugin_root / ".codex-plugin" / "plugin.json"
|
|
128
|
-
if not path.is_file():
|
|
129
|
-
errors.append("missing required file `.codex-plugin/plugin.json`")
|
|
130
|
-
return
|
|
131
|
-
manifest = load_json_object(path, "plugin manifest", errors)
|
|
132
|
-
if manifest is None:
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
if manifest.get("name") != "superpowers":
|
|
136
|
-
errors.append("plugin manifest field `name` must equal `superpowers`")
|
|
137
|
-
version = manifest.get("version")
|
|
138
|
-
if version != expected_version:
|
|
139
|
-
errors.append("plugin manifest field `version` must equal expected version")
|
|
140
|
-
if not isinstance(version, str) or SEMVER_RE.fullmatch(version) is None:
|
|
141
|
-
errors.append("plugin manifest field `version` must be SemVer 2.0.0")
|
|
142
|
-
description = manifest.get("description")
|
|
143
|
-
if not isinstance(description, str) or not description.strip():
|
|
144
|
-
errors.append("plugin manifest field `description` must be non-empty")
|
|
145
|
-
if manifest.get("skills") != "./skills/":
|
|
146
|
-
errors.append("plugin manifest field `skills` must equal `./skills/`")
|
|
147
|
-
if "hooks" in manifest:
|
|
148
|
-
errors.append("plugin manifest field `hooks` must be absent")
|
|
149
|
-
|
|
150
|
-
validate_local_path(
|
|
151
|
-
plugin_root,
|
|
152
|
-
manifest.get("skills"),
|
|
153
|
-
"plugin manifest field `skills`",
|
|
154
|
-
errors,
|
|
155
|
-
require_directory=True,
|
|
156
|
-
)
|
|
157
|
-
if "apps" in manifest:
|
|
158
|
-
validate_local_path(plugin_root, manifest["apps"], "plugin manifest field `apps`", errors)
|
|
159
|
-
if "mcpServers" in manifest:
|
|
160
|
-
mcp_servers = manifest["mcpServers"]
|
|
161
|
-
if isinstance(mcp_servers, str):
|
|
162
|
-
validate_local_path(
|
|
163
|
-
plugin_root, mcp_servers, "plugin manifest field `mcpServers`", errors
|
|
164
|
-
)
|
|
165
|
-
elif not isinstance(mcp_servers, dict):
|
|
166
|
-
errors.append("plugin manifest field `mcpServers` must be a string or object")
|
|
167
|
-
|
|
168
|
-
if "interface" not in manifest:
|
|
169
|
-
return
|
|
170
|
-
interface = manifest["interface"]
|
|
171
|
-
if not isinstance(interface, dict):
|
|
172
|
-
errors.append("plugin manifest field `interface` must be an object")
|
|
173
|
-
return
|
|
174
|
-
for field in ("composerIcon", "logo", "logoDark"):
|
|
175
|
-
if field in interface:
|
|
176
|
-
validate_local_path(
|
|
177
|
-
plugin_root,
|
|
178
|
-
interface[field],
|
|
179
|
-
f"plugin manifest field `interface.{field}`",
|
|
180
|
-
errors,
|
|
181
|
-
)
|
|
182
|
-
if "screenshots" in interface:
|
|
183
|
-
screenshots = interface["screenshots"]
|
|
184
|
-
if not isinstance(screenshots, list):
|
|
185
|
-
errors.append("plugin manifest field `interface.screenshots` must be an array")
|
|
186
|
-
else:
|
|
187
|
-
for index, value in enumerate(screenshots):
|
|
188
|
-
validate_local_path(
|
|
189
|
-
plugin_root,
|
|
190
|
-
value,
|
|
191
|
-
f"plugin manifest field `interface.screenshots[{index}]`",
|
|
192
|
-
errors,
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def validate_skill_frontmatter(skill_md: Path, skill_name: str, errors: list[str]) -> None:
|
|
197
|
-
try:
|
|
198
|
-
contents = skill_md.read_text(encoding="utf-8")
|
|
199
|
-
except (OSError, UnicodeError):
|
|
200
|
-
errors.append(f"skill `{skill_name}` has unreadable UTF-8 `SKILL.md`")
|
|
201
|
-
return
|
|
202
|
-
if not contents:
|
|
203
|
-
errors.append(f"skill `{skill_name}` has empty `SKILL.md`")
|
|
204
|
-
return
|
|
205
|
-
lines = contents.splitlines()
|
|
206
|
-
if not lines or lines[0] != "---":
|
|
207
|
-
errors.append(f"skill `{skill_name}` must start with `---`")
|
|
208
|
-
return
|
|
209
|
-
try:
|
|
210
|
-
closing_index = lines.index("---", 1)
|
|
211
|
-
except ValueError:
|
|
212
|
-
errors.append(f"skill `{skill_name}` frontmatter is not closed")
|
|
213
|
-
return
|
|
214
|
-
frontmatter = lines[1:closing_index]
|
|
215
|
-
for key in ("name", "description"):
|
|
216
|
-
matches = [line for line in frontmatter if line.startswith(f"{key}:")]
|
|
217
|
-
if len(matches) != 1:
|
|
218
|
-
errors.append(
|
|
219
|
-
f"skill `{skill_name}` frontmatter must contain exactly one top-level `{key}:`"
|
|
220
|
-
)
|
|
221
|
-
continue
|
|
222
|
-
value = matches[0].split(":", 1)[1].strip()
|
|
223
|
-
if value in {"", "''", '\"\"'} or value.startswith("#"):
|
|
224
|
-
errors.append(f"skill `{skill_name}` frontmatter field `{key}` must be non-empty")
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def validate_tree(plugin_root: Path, errors: list[str]) -> None:
|
|
228
|
-
required_files = (
|
|
229
|
-
".codex-plugin/plugin.template.json",
|
|
230
|
-
".superpowers-upstream.json",
|
|
231
|
-
"LICENSE",
|
|
232
|
-
"README.md",
|
|
233
|
-
"CODE_OF_CONDUCT.md",
|
|
234
|
-
)
|
|
235
|
-
for relative in required_files:
|
|
236
|
-
if not (plugin_root / relative).is_file():
|
|
237
|
-
errors.append(f"missing required file `{relative}`")
|
|
238
|
-
if os.path.lexists(plugin_root / "hooks"):
|
|
239
|
-
errors.append("generated plugin must not contain `hooks/`")
|
|
240
|
-
|
|
241
|
-
skills_root = plugin_root / "skills"
|
|
242
|
-
if not skills_root.is_dir():
|
|
243
|
-
errors.append("missing required directory `skills/`")
|
|
244
|
-
return
|
|
245
|
-
try:
|
|
246
|
-
skill_dirs = sorted(
|
|
247
|
-
path
|
|
248
|
-
for path in skills_root.iterdir()
|
|
249
|
-
if not path.name.startswith(".") and path.is_dir()
|
|
250
|
-
)
|
|
251
|
-
except OSError:
|
|
252
|
-
errors.append("skills directory could not be enumerated")
|
|
253
|
-
return
|
|
254
|
-
if not skill_dirs:
|
|
255
|
-
errors.append("`skills/` must contain at least one skill directory")
|
|
256
|
-
return
|
|
257
|
-
for skill_dir in skill_dirs:
|
|
258
|
-
skill_md = skill_dir / "SKILL.md"
|
|
259
|
-
if not skill_md.is_file():
|
|
260
|
-
errors.append(f"skill `{skill_dir.name}` is missing `SKILL.md`")
|
|
261
|
-
continue
|
|
262
|
-
validate_skill_frontmatter(skill_md, skill_dir.name, errors)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def validate_provenance(args: argparse.Namespace, plugin_root: Path, errors: list[str]) -> None:
|
|
266
|
-
path = plugin_root / ".superpowers-upstream.json"
|
|
267
|
-
if not path.is_file():
|
|
268
|
-
return
|
|
269
|
-
provenance = load_json_object(path, "provenance", errors)
|
|
270
|
-
if provenance is None:
|
|
271
|
-
return
|
|
272
|
-
if set(provenance) != PROVENANCE_KEYS:
|
|
273
|
-
errors.append("provenance keys do not match the manager-owned contract")
|
|
274
|
-
expected = {
|
|
275
|
-
"source": args.source,
|
|
276
|
-
"requested_ref": args.requested_ref,
|
|
277
|
-
"resolved_ref": args.resolved_ref,
|
|
278
|
-
"commit": args.commit,
|
|
279
|
-
"upstream_manifest_version": args.upstream_manifest_version,
|
|
280
|
-
}
|
|
281
|
-
for key, value in expected.items():
|
|
282
|
-
if provenance.get(key) != value:
|
|
283
|
-
errors.append(f"provenance field `{key}` does not match expected value")
|
|
284
|
-
if re.fullmatch(r"[0-9a-f]{40}", args.commit) is None:
|
|
285
|
-
errors.append("commit must be 40 lowercase hexadecimal characters")
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def validate(args: argparse.Namespace) -> list[str]:
|
|
289
|
-
errors: list[str] = []
|
|
290
|
-
try:
|
|
291
|
-
plugin_root = Path(args.plugin_root).expanduser().resolve()
|
|
292
|
-
except (OSError, RuntimeError, ValueError):
|
|
293
|
-
errors.append("plugin root could not be resolved")
|
|
294
|
-
return errors
|
|
295
|
-
validate_manifest(plugin_root, args.manifest_version, errors)
|
|
296
|
-
validate_tree(plugin_root, errors)
|
|
297
|
-
validate_provenance(args, plugin_root, errors)
|
|
298
|
-
return errors
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def main(argv: Sequence[str] | None = None) -> int:
|
|
302
|
-
args = parse_args(argv)
|
|
303
|
-
errors = validate(args)
|
|
304
|
-
if errors:
|
|
305
|
-
print("Generated plugin validation failed:", file=sys.stderr)
|
|
306
|
-
for error in errors:
|
|
307
|
-
print(f"- {error}", file=sys.stderr)
|
|
308
|
-
return 1
|
|
309
|
-
print(f"generated plugin validation passed: {args.plugin_root}")
|
|
310
|
-
return 0
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if __name__ == "__main__":
|
|
314
|
-
raise SystemExit(main())
|
package/scripts/core/adapter.sh
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Sourced module; callers own set -eu.
|
|
3
|
-
|
|
4
|
-
spw_adapter_package_root() {
|
|
5
|
-
if [ -n "${SPW_PACKAGE_ROOT:-}" ]; then
|
|
6
|
-
printf '%s\n' "$SPW_PACKAGE_ROOT"
|
|
7
|
-
elif [ -n "${root:-}" ]; then
|
|
8
|
-
printf '%s\n' "$root"
|
|
9
|
-
else
|
|
10
|
-
spw_root
|
|
11
|
-
fi
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
SPW_ADAPTER_ROOT=$(spw_adapter_package_root)
|
|
15
|
-
SPW_ADAPTER="${SPW_ADAPTER:-$SPW_ADAPTER_ROOT/scripts/adapters/codex/adapter}"
|
|
16
|
-
SPW_ADAPTER_RESPONSE_VALIDATOR="${SPW_ADAPTER_RESPONSE_VALIDATOR:-$SPW_ADAPTER_ROOT/scripts/core/validate-adapter-response.py}"
|
|
17
|
-
|
|
18
|
-
spw_invoke_adapter() {
|
|
19
|
-
operation="$1"
|
|
20
|
-
result_file="$2"
|
|
21
|
-
inspect_view="$3"
|
|
22
|
-
shift 3
|
|
23
|
-
[ "${1:-}" = "--" ] || spw_die "internal adapter invocation missing --"
|
|
24
|
-
shift
|
|
25
|
-
|
|
26
|
-
response_file="${result_file}.response"
|
|
27
|
-
rm -f "$result_file"
|
|
28
|
-
adapter_exit=0
|
|
29
|
-
"$SPW_ADAPTER" "$operation" "$@" > "$response_file" || adapter_exit=$?
|
|
30
|
-
|
|
31
|
-
args=""
|
|
32
|
-
if [ -n "$inspect_view" ]; then
|
|
33
|
-
args="$inspect_view"
|
|
34
|
-
fi
|
|
35
|
-
validator_exit=0
|
|
36
|
-
if [ -n "$args" ]; then
|
|
37
|
-
python3 -S "$SPW_ADAPTER_RESPONSE_VALIDATOR" \
|
|
38
|
-
--operation "$operation" --adapter-exit "$adapter_exit" \
|
|
39
|
-
--response "$response_file" --result "$result_file" \
|
|
40
|
-
--inspect-view "$inspect_view" || validator_exit=$?
|
|
41
|
-
else
|
|
42
|
-
python3 -S "$SPW_ADAPTER_RESPONSE_VALIDATOR" \
|
|
43
|
-
--operation "$operation" --adapter-exit "$adapter_exit" \
|
|
44
|
-
--response "$response_file" --result "$result_file" || validator_exit=$?
|
|
45
|
-
fi
|
|
46
|
-
if [ "$validator_exit" -ne 0 ]; then
|
|
47
|
-
rm -f "$result_file"
|
|
48
|
-
return 1
|
|
49
|
-
fi
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
spw_adapter_result_get() {
|
|
53
|
-
result_file="$1"
|
|
54
|
-
dotted_key="$2"
|
|
55
|
-
spw_json_get "$result_file" "$dotted_key"
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
spw_adapter_result_boolean() {
|
|
59
|
-
result_file="$1"
|
|
60
|
-
dotted_key="$2"
|
|
61
|
-
value=$(spw_adapter_result_get "$result_file" "$dotted_key")
|
|
62
|
-
case "$value" in
|
|
63
|
-
true|True)
|
|
64
|
-
printf 'true\n'
|
|
65
|
-
;;
|
|
66
|
-
false|False)
|
|
67
|
-
printf 'false\n'
|
|
68
|
-
;;
|
|
69
|
-
*)
|
|
70
|
-
spw_die "expected Boolean adapter result at $dotted_key in $result_file"
|
|
71
|
-
;;
|
|
72
|
-
esac
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
spw_inspect_fingerprint() {
|
|
76
|
-
result_file="$1"
|
|
77
|
-
spw_invoke_adapter inspect "$result_file" fingerprint -- --view fingerprint
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
spw_inspect_ownership() {
|
|
81
|
-
result_file="$1"
|
|
82
|
-
spw_invoke_adapter inspect "$result_file" ownership -- --view ownership
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
spw_inspect_update_control() {
|
|
86
|
-
result_file="$1"
|
|
87
|
-
spw_invoke_adapter inspect "$result_file" update-control -- --view update-control
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
spw_adapter_install() {
|
|
91
|
-
result_file="$1"
|
|
92
|
-
package_root="$2"
|
|
93
|
-
spw_invoke_adapter install "$result_file" "" -- --package-root "$package_root"
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
spw_adapter_uninstall() {
|
|
97
|
-
result_file="$1"
|
|
98
|
-
plugin_present="$2"
|
|
99
|
-
marketplace_present="$3"
|
|
100
|
-
spw_invoke_adapter uninstall "$result_file" "" -- \
|
|
101
|
-
--plugin-present "$plugin_present" \
|
|
102
|
-
--marketplace-present "$marketplace_present"
|
|
103
|
-
}
|
package/scripts/core/common.sh
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Sourced module; callers own set -eu.
|
|
3
|
-
|
|
4
|
-
spw_die() {
|
|
5
|
-
echo "error: $*" >&2
|
|
6
|
-
exit 1
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
spw_usage_error() {
|
|
10
|
-
echo "error: $*" >&2
|
|
11
|
-
exit 2
|
|
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_make_workspace() {
|
|
22
|
-
mktemp -d "$1/$2.XXXXXX"
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
spw_cleanup_workspace_trap() {
|
|
26
|
-
_spw_workspace_trap_status=$?
|
|
27
|
-
trap - 0 HUP INT TERM
|
|
28
|
-
rm -rf "$_spw_workspace_trap_path" || :
|
|
29
|
-
exit "$_spw_workspace_trap_status"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
spw_install_workspace_trap() {
|
|
33
|
-
_spw_workspace_trap_path="$1"
|
|
34
|
-
trap spw_cleanup_workspace_trap 0
|
|
35
|
-
trap 'exit 129' HUP
|
|
36
|
-
trap 'exit 130' INT
|
|
37
|
-
trap 'exit 143' TERM
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
spw_root() {
|
|
41
|
-
CDPATH= cd -- "$(dirname "$0")/.." && pwd
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
spw_copy_path_if_present() {
|
|
45
|
-
src="$1"
|
|
46
|
-
dst="$2"
|
|
47
|
-
if [ -e "$src" ]; then
|
|
48
|
-
rm -rf "$dst"
|
|
49
|
-
cp -R "$src" "$dst"
|
|
50
|
-
fi
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
spw_require_upstream_path() {
|
|
54
|
-
path="$1"
|
|
55
|
-
label="$2"
|
|
56
|
-
if [ ! -e "$path" ]; then
|
|
57
|
-
spw_die "required upstream path missing: $label"
|
|
58
|
-
fi
|
|
59
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Sourced module; callers own set -eu.
|
|
3
|
-
|
|
4
|
-
# Replace the live tree with a single atomic rename, keeping a backup so a
|
|
5
|
-
# failed swap restores the previous working generated tree. candidate and
|
|
6
|
-
# live_root must be same-filesystem siblings for atomic renames.
|
|
7
|
-
spw_replace_generated_tree() {
|
|
8
|
-
candidate="$1"
|
|
9
|
-
live_root="$2"
|
|
10
|
-
parent=$(dirname "$live_root")
|
|
11
|
-
backup="$parent/.superpowers.bak.$$"
|
|
12
|
-
|
|
13
|
-
rm -rf "$backup"
|
|
14
|
-
if [ -e "$live_root" ]; then
|
|
15
|
-
mv "$live_root" "$backup"
|
|
16
|
-
fi
|
|
17
|
-
if mv "$candidate" "$live_root"; then
|
|
18
|
-
rm -rf "$backup"
|
|
19
|
-
else
|
|
20
|
-
if [ -e "$backup" ]; then
|
|
21
|
-
mv "$backup" "$live_root"
|
|
22
|
-
fi
|
|
23
|
-
rm -rf "$candidate"
|
|
24
|
-
spw_die "failed to install generated tree into $live_root; previous tree restored"
|
|
25
|
-
fi
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
spw_generated_metadata_path() (
|
|
29
|
-
generated_root="$1"
|
|
30
|
-
printf '%s\n' "$generated_root/plugins/superpowers/.superpowers-upstream.json"
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
spw_generated_commit_or_empty() (
|
|
34
|
-
generated_root="$1"
|
|
35
|
-
generated_metadata=$(spw_generated_metadata_path "$generated_root")
|
|
36
|
-
spw_metadata_commit_lenient_or_empty "$generated_metadata"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
spw_probe_field() {
|
|
40
|
-
printf '%s\n' "$1" | awk -F= -v key="$2" '$1 == key { print $2 }'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
spw_require_no_legacy_state() {
|
|
44
|
-
identity_state="$1"
|
|
45
|
-
case "$identity_state" in
|
|
46
|
-
neither|manager)
|
|
47
|
-
return 0
|
|
48
|
-
;;
|
|
49
|
-
legacy|both)
|
|
50
|
-
printf '%s\n' \
|
|
51
|
-
'Legacy superpowers-wrapper Codex state is installed.' \
|
|
52
|
-
'Run: npx superpowers-wrapper@0.1.1 uninstall' \
|
|
53
|
-
'Then run: npx superpowers-manager install' >&2
|
|
54
|
-
return 1
|
|
55
|
-
;;
|
|
56
|
-
*)
|
|
57
|
-
spw_die "unknown adapter identity state: $identity_state"
|
|
58
|
-
;;
|
|
59
|
-
esac
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
spw_require_managed_update_control() {
|
|
63
|
-
case "$1" in
|
|
64
|
-
managed) return 0 ;;
|
|
65
|
-
unsupported)
|
|
66
|
-
spw_die "adapter cannot guarantee manager-controlled updates"
|
|
67
|
-
;;
|
|
68
|
-
*) spw_die "unknown adapter update-control capability: $1" ;;
|
|
69
|
-
esac
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
spw_report_legacy_state() {
|
|
73
|
-
case "$1" in
|
|
74
|
-
legacy|both)
|
|
75
|
-
printf '%s\n' \
|
|
76
|
-
'Legacy superpowers-wrapper Codex state remains installed.' \
|
|
77
|
-
'Run: npx superpowers-wrapper@0.1.1 uninstall'
|
|
78
|
-
;;
|
|
79
|
-
neither|manager)
|
|
80
|
-
;;
|
|
81
|
-
*)
|
|
82
|
-
spw_die "unknown adapter identity state: $1"
|
|
83
|
-
;;
|
|
84
|
-
esac
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
spw_verify_installed_fingerprint() {
|
|
88
|
-
desired_commit="$1"
|
|
89
|
-
install_result="$2"
|
|
90
|
-
inspect_result="$3"
|
|
91
|
-
if ! spw_inspect_fingerprint "$inspect_result"; then
|
|
92
|
-
echo "error: installed manager fingerprint inspection failed after install." >&2
|
|
93
|
-
return 1
|
|
94
|
-
fi
|
|
95
|
-
if ! installed_commit=$(spw_adapter_result_get "$inspect_result" "fingerprint"); then
|
|
96
|
-
echo "error: cannot parse installed manager fingerprint inspection result after install." >&2
|
|
97
|
-
return 1
|
|
98
|
-
fi
|
|
99
|
-
printf 'desired_commit=%s\n' "$desired_commit"
|
|
100
|
-
printf 'installed_commit=%s\n' "$installed_commit"
|
|
101
|
-
if [ -n "$installed_commit" ] && spw_commit_matches "$desired_commit" "$installed_commit"; then
|
|
102
|
-
echo "manager updated"
|
|
103
|
-
return 0
|
|
104
|
-
fi
|
|
105
|
-
|
|
106
|
-
hint=""
|
|
107
|
-
if [ -f "$install_result" ]; then
|
|
108
|
-
if [ -n "$installed_commit" ]; then
|
|
109
|
-
hint=$(spw_adapter_result_get "$install_result" "verification_hints.mismatch" || true)
|
|
110
|
-
else
|
|
111
|
-
hint=$(spw_adapter_result_get "$install_result" "verification_hints.missing" || true)
|
|
112
|
-
fi
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
if [ -n "$installed_commit" ]; then
|
|
116
|
-
echo "error: installed manager fingerprint does not match the prepared plugin after install." >&2
|
|
117
|
-
else
|
|
118
|
-
echo "error: installed manager fingerprint is not detectable after install." >&2
|
|
119
|
-
fi
|
|
120
|
-
if [ -n "$hint" ]; then
|
|
121
|
-
echo "hint: $hint" >&2
|
|
122
|
-
fi
|
|
123
|
-
return 1
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
spw_verify_uninstalled_resources() {
|
|
127
|
-
inspect_result="$1"
|
|
128
|
-
|
|
129
|
-
if ! plugin_present=$(spw_adapter_result_boolean "$inspect_result" "resources.plugin"); then
|
|
130
|
-
return 1
|
|
131
|
-
fi
|
|
132
|
-
if ! marketplace_present=$(spw_adapter_result_boolean "$inspect_result" "resources.marketplace"); then
|
|
133
|
-
return 1
|
|
134
|
-
fi
|
|
135
|
-
if [ "$plugin_present" = true ]; then
|
|
136
|
-
spw_die "owned plugin resource is still installed after removal"
|
|
137
|
-
fi
|
|
138
|
-
if [ "$marketplace_present" = true ]; then
|
|
139
|
-
spw_die "owned marketplace resource is still registered after removal"
|
|
140
|
-
fi
|
|
141
|
-
}
|