superpowers-manager 0.2.0 → 0.4.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.
Files changed (68) hide show
  1. package/README.md +340 -66
  2. package/dist/adapter-result.js +204 -0
  3. package/dist/adapter.js +690 -0
  4. package/dist/atomic.js +142 -0
  5. package/dist/cli-arguments.js +32 -0
  6. package/dist/cli.js +313 -0
  7. package/dist/codex-harness.js +271 -0
  8. package/dist/codex-json.js +89 -0
  9. package/dist/codex-prepare.js +199 -0
  10. package/dist/codex-presentation.js +259 -0
  11. package/dist/codex-state.js +78 -0
  12. package/dist/commands/context.js +1 -0
  13. package/dist/commands/install.js +291 -0
  14. package/dist/commands/pin.js +79 -0
  15. package/dist/commands/prepare.js +420 -0
  16. package/dist/commands/probe.js +257 -0
  17. package/dist/commands/track-latest.js +44 -0
  18. package/dist/commands/uninstall.js +217 -0
  19. package/dist/commands/unpin.js +95 -0
  20. package/dist/commands/update.js +79 -0
  21. package/dist/domain/fingerprint.js +4 -0
  22. package/dist/domain/refs.js +37 -0
  23. package/dist/effective-selection.js +123 -0
  24. package/dist/generated-plugin.js +748 -0
  25. package/dist/git.js +38 -0
  26. package/dist/harness.js +1 -0
  27. package/dist/hooks.js +290 -0
  28. package/dist/lifecycle.js +135 -0
  29. package/dist/manifest-overlay.js +125 -0
  30. package/dist/provenance.js +111 -0
  31. package/dist/python-json-format.js +83 -0
  32. package/dist/python-json.js +29 -0
  33. package/dist/python-text.js +74 -0
  34. package/dist/safe-path.js +137 -0
  35. package/dist/safety-error.js +10 -0
  36. package/dist/selection-state-cli.js +85 -0
  37. package/dist/selection-store.js +172 -0
  38. package/dist/selection.js +200 -0
  39. package/dist/status.js +24 -0
  40. package/dist/strict-json.js +248 -0
  41. package/dist/upstream-cli.js +110 -0
  42. package/dist/upstream-version.js +40 -0
  43. package/dist/upstream.js +253 -0
  44. package/dist/validate-generated-plugin-cli.js +118 -0
  45. package/dist/validator.js +317 -0
  46. package/dist/workspace.js +139 -0
  47. package/package.json +25 -4
  48. package/bin/superpowers-manager.js +0 -290
  49. package/scripts/adapters/codex/adapter +0 -603
  50. package/scripts/adapters/codex/lib.sh +0 -449
  51. package/scripts/adapters/codex/validate-generated-plugin.py +0 -314
  52. package/scripts/core/adapter.sh +0 -103
  53. package/scripts/core/common.sh +0 -59
  54. package/scripts/core/lifecycle.sh +0 -141
  55. package/scripts/core/provenance.sh +0 -135
  56. package/scripts/core/selection-state.py +0 -414
  57. package/scripts/core/selection.sh +0 -168
  58. package/scripts/core/status.sh +0 -27
  59. package/scripts/core/upstream.sh +0 -280
  60. package/scripts/core/validate-adapter-response.py +0 -284
  61. package/scripts/install +0 -59
  62. package/scripts/pin +0 -51
  63. package/scripts/prepare +0 -117
  64. package/scripts/probe +0 -82
  65. package/scripts/track-latest +0 -30
  66. package/scripts/uninstall +0 -35
  67. package/scripts/unpin +0 -41
  68. 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())
@@ -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
- }
@@ -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
- }