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.
@@ -0,0 +1,168 @@
1
+ #!/bin/sh
2
+ # Sourced module; callers own set -eu.
3
+
4
+ spw_selection_config_dir() {
5
+ if [ "${SUPERPOWERS_CONFIG_DIR+x}" = x ]; then
6
+ case "$SUPERPOWERS_CONFIG_DIR" in
7
+ /*) printf '%s\n' "$SUPERPOWERS_CONFIG_DIR" ;;
8
+ *) echo "error: SUPERPOWERS_CONFIG_DIR must be absolute" >&2; return 1 ;;
9
+ esac
10
+ return
11
+ fi
12
+
13
+ if [ -n "${XDG_CONFIG_HOME:-}" ]; then
14
+ case "$XDG_CONFIG_HOME" in
15
+ /*) printf '%s/superpowers-manager\n' "$XDG_CONFIG_HOME" ;;
16
+ *) echo "error: XDG_CONFIG_HOME must be absolute" >&2; return 1 ;;
17
+ esac
18
+ return
19
+ fi
20
+
21
+ if [ -z "${HOME:-}" ]; then
22
+ echo "error: HOME is required to locate selection state" >&2
23
+ return 1
24
+ fi
25
+ case "$HOME" in
26
+ /*) printf '%s/.config/superpowers-manager\n' "$HOME" ;;
27
+ *) echo "error: HOME must be absolute" >&2; return 1 ;;
28
+ esac
29
+ }
30
+
31
+ spw_selection_state_path() {
32
+ _spw_selection_config_dir=$(spw_selection_config_dir) || return $?
33
+ printf '%s/selection.json\n' "$_spw_selection_config_dir"
34
+ }
35
+
36
+ spw_selection_state() {
37
+ _spw_selection_state_root="$1"
38
+ shift
39
+ if _spw_selection_state_error=$(
40
+ python3 -S "$_spw_selection_state_root/scripts/core/selection-state.py" \
41
+ "$@" 2>&1
42
+ ); then
43
+ :
44
+ else
45
+ _spw_selection_state_error=${_spw_selection_state_error#error: }
46
+ spw_die "$_spw_selection_state_error"
47
+ fi
48
+ }
49
+
50
+ spw_load_saved_selection() {
51
+ _spw_selection_root="$1"
52
+ _spw_selection_workspace="$2"
53
+ SPW_SELECTION_STATE_PATH=$(spw_selection_state_path)
54
+ if ! _spw_selection_normalized=$(mktemp "$_spw_selection_workspace/selection-state.XXXXXX"); then
55
+ spw_die "cannot create normalized selection scratch file"
56
+ fi
57
+
58
+ if _spw_selection_error=$(
59
+ python3 -S "$_spw_selection_root/scripts/core/selection-state.py" read \
60
+ --path "$SPW_SELECTION_STATE_PATH" \
61
+ --output "$_spw_selection_normalized" 2>&1
62
+ ); then
63
+ :
64
+ else
65
+ rm -f "$_spw_selection_normalized"
66
+ _spw_selection_error=${_spw_selection_error#error: }
67
+ spw_die "$_spw_selection_error"
68
+ fi
69
+
70
+ SPW_SAVED_MODE=$(spw_json_get "$_spw_selection_normalized" saved_mode)
71
+ SPW_SAVED_SOURCE=$(spw_json_get "$_spw_selection_normalized" saved_source)
72
+ SPW_SAVED_REQUESTED_REF=$(spw_json_get "$_spw_selection_normalized" saved_requested_ref)
73
+ SPW_SAVED_RESOLVED_REF=$(spw_json_get "$_spw_selection_normalized" saved_resolved_ref)
74
+ SPW_SAVED_COMMIT=$(spw_json_get "$_spw_selection_normalized" saved_commit)
75
+ rm -f "$_spw_selection_normalized"
76
+
77
+ export SPW_SELECTION_STATE_PATH
78
+ export SPW_SAVED_MODE SPW_SAVED_SOURCE SPW_SAVED_REQUESTED_REF
79
+ export SPW_SAVED_RESOLVED_REF SPW_SAVED_COMMIT
80
+ }
81
+
82
+ spw_compute_effective_selection() {
83
+ _spw_selection_root="$1"
84
+ _spw_selection_workspace="$2"
85
+ spw_load_saved_selection "$_spw_selection_root" "$_spw_selection_workspace"
86
+
87
+ if [ -n "${SUPERPOWERS_UPSTREAM_URL:-}" ]; then
88
+ SPW_UPSTREAM_SOURCE_ORIGIN="environment"
89
+ SPW_EFFECTIVE_SOURCE="$SUPERPOWERS_UPSTREAM_URL"
90
+ elif [ "$SPW_SAVED_MODE" != "none" ]; then
91
+ SPW_UPSTREAM_SOURCE_ORIGIN="user-config"
92
+ SPW_EFFECTIVE_SOURCE="$SPW_SAVED_SOURCE"
93
+ else
94
+ SPW_UPSTREAM_SOURCE_ORIGIN="package-default"
95
+ SPW_EFFECTIVE_SOURCE="$SPW_UPSTREAM_URL_DEFAULT"
96
+ fi
97
+
98
+ spw_selection_state "$_spw_selection_root" \
99
+ validate-source --source="$SPW_EFFECTIVE_SOURCE"
100
+
101
+ _spw_selection_uses_saved_pin=false
102
+ if [ -n "${SUPERPOWERS_REF:-}" ]; then
103
+ SPW_SELECTION_ORIGIN="environment"
104
+ SPW_SELECTION_MODE="override"
105
+ SPW_REQUESTED_REF="$SUPERPOWERS_REF"
106
+ elif [ "$SPW_SAVED_MODE" = "pinned" ]; then
107
+ SPW_SELECTION_ORIGIN="user-config"
108
+ SPW_SELECTION_MODE="pinned"
109
+ SPW_REQUESTED_REF="$SPW_SAVED_REQUESTED_REF"
110
+ _spw_selection_uses_saved_pin=true
111
+ elif [ "$SPW_SAVED_MODE" = "track-latest" ]; then
112
+ SPW_SELECTION_ORIGIN="user-config"
113
+ SPW_SELECTION_MODE="track-latest"
114
+ SPW_REQUESTED_REF="latest-release"
115
+ else
116
+ SPW_SELECTION_ORIGIN="package-default"
117
+ SPW_SELECTION_MODE="default"
118
+ SPW_REQUESTED_REF=$(spw_config_ref "$_spw_selection_root")
119
+ fi
120
+
121
+ if [ "$_spw_selection_uses_saved_pin" = true ]; then
122
+ SPW_RESOLVED_REF="$SPW_SAVED_RESOLVED_REF"
123
+ SPW_DESIRED_COMMIT="$SPW_SAVED_COMMIT"
124
+ if printf '%s' "$SPW_SAVED_REQUESTED_REF" | grep -Eq '^[0-9a-f]{40}$'; then
125
+ SPW_RESOLUTION_KIND="raw-commit"
126
+ else
127
+ SPW_RESOLUTION_KIND="tag"
128
+ fi
129
+ else
130
+ _spw_selection_resolution=$(
131
+ spw_resolve_ref "$SPW_EFFECTIVE_SOURCE" "$SPW_REQUESTED_REF"
132
+ )
133
+ if _spw_selection_fields=$(
134
+ printf '%s\n' "$_spw_selection_resolution" | awk '
135
+ NR == 1 && NF == 3 { printf "%s\t%s\t%s\n", $1, $2, $3; next }
136
+ { exit 1 }
137
+ END { if (NR != 1) exit 1 }
138
+ '
139
+ ); then
140
+ :
141
+ else
142
+ spw_die "invalid ref resolution result"
143
+ fi
144
+ _spw_selection_tab=$(printf '\tX')
145
+ _spw_selection_tab=${_spw_selection_tab%X}
146
+ IFS=$_spw_selection_tab read -r \
147
+ SPW_RESOLUTION_KIND SPW_RESOLVED_REF SPW_DESIRED_COMMIT <<EOF
148
+ $_spw_selection_fields
149
+ EOF
150
+ fi
151
+
152
+ export SPW_SELECTION_ORIGIN SPW_SELECTION_MODE
153
+ export SPW_UPSTREAM_SOURCE_ORIGIN SPW_EFFECTIVE_SOURCE
154
+ export SPW_REQUESTED_REF SPW_RESOLVED_REF SPW_DESIRED_COMMIT
155
+ export SPW_RESOLUTION_KIND
156
+ }
157
+
158
+ spw_display_source() {
159
+ _spw_selection_display_source="$1"
160
+ if _spw_selection_display=$(
161
+ python3 -S "$(spw_root)/scripts/core/selection-state.py" \
162
+ display-source --source="$_spw_selection_display_source" 2>/dev/null
163
+ ); then
164
+ printf '%s\n' "$_spw_selection_display"
165
+ else
166
+ printf '%s\n' '<redacted-source>'
167
+ fi
168
+ }
@@ -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,280 @@
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_is_pinnable_tag() {
26
+ ref="$1"
27
+ case "$ref" in
28
+ *'
29
+ '*|*"$(printf '\r')"*) return 1 ;;
30
+ v*) spw_is_semver_base "${ref#v}" ;;
31
+ *) return 1 ;;
32
+ esac
33
+ }
34
+
35
+ spw_is_full_commit() {
36
+ ref="$1"
37
+ [ "${#ref}" -eq 40 ] || return 1
38
+ case "$ref" in
39
+ *[!0-9A-Fa-f]*) return 1 ;;
40
+ *) return 0 ;;
41
+ esac
42
+ }
43
+
44
+ spw_git_safe_source() {
45
+ source="$1"
46
+ case "$source" in
47
+ /*|*://*|*:*|~*) printf '%s\n' "$source" ;;
48
+ *) printf '%s/%s\n' "$(pwd -P)" "$source" ;;
49
+ esac
50
+ }
51
+
52
+ spw_resolve_exact_tag() {
53
+ source="$1"
54
+ ref="$2"
55
+ source_display=$(spw_display_source "$source")
56
+ query_source=$(spw_git_safe_source "$source")
57
+ if ! output=$(git ls-remote --tags -- "$query_source" "refs/tags/$ref" "refs/tags/$ref^{}" 2>&1); then
58
+ spw_die "cannot query exact upstream tag $ref from $source_display: $output"
59
+ fi
60
+ commit=$(printf '%s\n' "$output" | awk -v direct="refs/tags/$ref" '
61
+ $2 == direct { direct_sha = $1 }
62
+ $2 == direct "^{}" { peeled_sha = $1 }
63
+ END { if (peeled_sha != "") print peeled_sha; else if (direct_sha != "") print direct_sha }
64
+ ')
65
+ [ -n "$commit" ] || spw_die "upstream tag not found: $ref"
66
+ printf '%s\n' "$commit"
67
+ }
68
+
69
+ spw_verify_raw_commit() (
70
+ source="$1"
71
+ commit=$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]')
72
+ workspace="$3"
73
+ source_display=$(spw_display_source "$source")
74
+ fetch_source=$(spw_git_safe_source "$source")
75
+
76
+ if ! verify_workspace=$(spw_make_workspace "$workspace" "superpowers-manager.commit"); then
77
+ spw_die "cannot create raw-commit verification workspace under $workspace"
78
+ fi
79
+ spw_install_workspace_trap "$verify_workspace"
80
+
81
+ if ! init_output=$(git init "$verify_workspace" 2>&1); then
82
+ spw_die "cannot initialize raw-commit verification workspace: $init_output"
83
+ fi
84
+ if fetch_output=$(git -C "$verify_workspace" fetch --no-tags -- "$fetch_source" "$commit" 2>&1); then
85
+ :
86
+ else
87
+ case "$fetch_output" in
88
+ *'not our ref'*|*'unadvertised object'*|*"couldn't find remote ref"*)
89
+ spw_die "source cannot supply requested commit: $commit"
90
+ ;;
91
+ *)
92
+ spw_die "cannot fetch requested commit from $source_display"
93
+ ;;
94
+ esac
95
+ fi
96
+ if ! object_type=$(git -C "$verify_workspace" cat-file -t "$commit" 2>/dev/null); then
97
+ spw_die "requested object is not a commit: $commit"
98
+ fi
99
+ if [ "$object_type" != commit ]; then
100
+ spw_die "requested object is not a commit: $commit"
101
+ fi
102
+ printf '%s\n' "$commit"
103
+ )
104
+
105
+ spw_fetch_exact_commit() (
106
+ source="$1"
107
+ commit="$2"
108
+ repository="$3"
109
+ workspace="$4"
110
+ source_display=$(spw_display_source "$source")
111
+ fetch_source=$(spw_git_safe_source "$source")
112
+
113
+ if ! fetch_workspace=$(spw_make_workspace "$workspace" "superpowers-manager.fetch"); then
114
+ spw_die "cannot create exact-commit fetch workspace under $workspace"
115
+ fi
116
+ spw_install_workspace_trap "$fetch_workspace"
117
+
118
+ if ! init_output=$(git init "$fetch_workspace" 2>&1); then
119
+ spw_die "cannot initialize exact-commit fetch workspace: $init_output"
120
+ fi
121
+ if fetch_output=$(git -C "$fetch_workspace" fetch --no-tags -- "$fetch_source" "$commit" 2>&1); then
122
+ :
123
+ else
124
+ case "$fetch_output" in
125
+ *'not our ref'*|*'unadvertised object'*|*"couldn't find remote ref"*)
126
+ spw_die "source cannot supply requested commit: $commit"
127
+ ;;
128
+ *)
129
+ spw_die "cannot fetch requested commit from $source_display"
130
+ ;;
131
+ esac
132
+ fi
133
+ if ! object_type=$(git -C "$fetch_workspace" cat-file -t "$commit" 2>/dev/null); then
134
+ spw_die "requested object is not a commit: $commit"
135
+ fi
136
+ if [ "$object_type" != commit ]; then
137
+ spw_die "requested object is not a commit: $commit"
138
+ fi
139
+
140
+ if [ ! -d "$repository/.git" ]; then
141
+ if ! init_output=$(git init "$repository" 2>&1); then
142
+ spw_die "cannot initialize upstream cache repository: $init_output"
143
+ fi
144
+ fi
145
+ if ! transfer_output=$(
146
+ git -C "$repository" fetch --no-tags -- "$fetch_workspace" "$commit" 2>&1
147
+ ); then
148
+ spw_die "cannot transfer requested commit into upstream cache: $transfer_output"
149
+ fi
150
+ if ! object_type=$(git -C "$repository" cat-file -t "$commit" 2>/dev/null); then
151
+ spw_die "cannot verify requested commit in upstream cache: $commit"
152
+ fi
153
+ if [ "$object_type" != commit ]; then
154
+ spw_die "cannot verify requested commit in upstream cache: $commit"
155
+ fi
156
+ )
157
+
158
+ spw_sanitize_ref_for_version() {
159
+ ref="$1"
160
+ sanitized=$(printf '%s' "$ref" | sed 's/[^0-9A-Za-z-][^0-9A-Za-z-]*/-/g; s/^-*//; s/-*$//')
161
+ sanitized=$(printf '%s' "$sanitized" | cut -c 1-48 | sed 's/^-*//; s/-*$//')
162
+ if [ -z "$sanitized" ]; then
163
+ sanitized="unknown"
164
+ fi
165
+ printf '%s' "$sanitized"
166
+ }
167
+
168
+ spw_manifest_version_for_ref() {
169
+ requested_ref="$1"
170
+ resolution_kind="$2"
171
+ resolved_ref="$3"
172
+ commit="$4"
173
+ short=$(spw_short_commit "$commit")
174
+
175
+ case "$resolution_kind" in
176
+ latest-release|tag)
177
+ base=$(printf '%s' "$resolved_ref" | sed -n 's/^v//p')
178
+ if [ -n "$base" ] && spw_is_semver_base "$base"; then
179
+ printf '%s+manager.%s\n' "$base" "$short"
180
+ return
181
+ fi
182
+ ;;
183
+ ref)
184
+ if [ "$requested_ref" = "main" ]; then
185
+ printf '0.0.0-main+manager.%s\n' "$short"
186
+ return
187
+ fi
188
+ sanitized=$(spw_sanitize_ref_for_version "$requested_ref")
189
+ printf '0.0.0-ref-%s+manager.%s\n' "$sanitized" "$short"
190
+ return
191
+ ;;
192
+ raw-commit)
193
+ ;;
194
+ esac
195
+
196
+ printf '0.0.0+manager.%s\n' "$short"
197
+ }
198
+
199
+ spw_manifest_version_for_commit() {
200
+ commit="$1"
201
+ spw_manifest_version_for_ref "$commit" "raw-commit" "$commit" "$commit"
202
+ }
203
+
204
+ spw_select_latest_release_from_ls_remote() {
205
+ awk '
206
+ $2 ~ /^refs\/tags\/v[0-9]+\.[0-9]+\.[0-9]+(\^\{\})?$/ {
207
+ ref = $2
208
+ peeled = 0
209
+ if (ref ~ /\^\{\}$/) {
210
+ peeled = 1
211
+ sub(/\^\{\}$/, "", ref)
212
+ }
213
+ tag = ref
214
+ sub(/^refs\/tags\//, "", tag)
215
+ split(substr(tag, 2), parts, ".")
216
+ key = sprintf("%010d.%010d.%010d", parts[1], parts[2], parts[3])
217
+ tag_by_key[key] = tag
218
+ if (peeled || !(tag in sha_by_tag)) {
219
+ sha_by_tag[tag] = $1
220
+ }
221
+ }
222
+ END {
223
+ for (key in tag_by_key) {
224
+ tag = tag_by_key[key]
225
+ print key, tag, sha_by_tag[tag]
226
+ }
227
+ }
228
+ ' | sort | tail -n 1 | awk '
229
+ NF == 3 { print $2 " " $3; found = 1 }
230
+ END { if (!found) exit 1 }
231
+ '
232
+ }
233
+
234
+ spw_resolve_ref() {
235
+ upstream_url="$1"
236
+ requested_ref="$2"
237
+ spw_require_command git
238
+ query_source=$(spw_git_safe_source "$upstream_url")
239
+
240
+ if [ "$requested_ref" = "latest-release" ]; then
241
+ if ! output=$(git ls-remote --tags -- "$query_source" 'refs/tags/v*' 2>&1); then
242
+ spw_die "cannot query upstream tags from $upstream_url: $output"
243
+ fi
244
+ if ! selected=$(printf '%s\n' "$output" | spw_select_latest_release_from_ls_remote); then
245
+ spw_die "no stable semver tag found for latest-release"
246
+ fi
247
+ printf 'latest-release %s\n' "$selected"
248
+ return
249
+ fi
250
+
251
+ if printf '%s' "$requested_ref" | grep -Eq '^[0-9a-fA-F]{40}$'; then
252
+ printf 'raw-commit %s %s\n' "$requested_ref" "$requested_ref"
253
+ return
254
+ fi
255
+
256
+ if ! tag_output=$(git ls-remote --tags -- "$query_source" "refs/tags/$requested_ref" "refs/tags/$requested_ref^{}" 2>&1); then
257
+ spw_die "cannot query upstream tag $requested_ref from $upstream_url: $tag_output"
258
+ fi
259
+ resolved=$(printf '%s\n' "$tag_output" | awk -v tag_ref="refs/tags/$requested_ref" '
260
+ NF >= 2 && ($2 == tag_ref || $2 == tag_ref "^{}") {
261
+ sha = $1
262
+ }
263
+ END { if (sha != "") print sha }
264
+ ')
265
+ if [ -n "$resolved" ]; then
266
+ printf 'tag %s %s\n' "$requested_ref" "$resolved"
267
+ return
268
+ fi
269
+
270
+ if ! ref_output=$(git ls-remote -- "$query_source" "$requested_ref" 2>&1); then
271
+ spw_die "cannot query upstream ref $requested_ref from $upstream_url: $ref_output"
272
+ fi
273
+ resolved=$(printf '%s\n' "$ref_output" | awk 'NF >= 2 { print $1; exit }')
274
+ if [ -n "$resolved" ]; then
275
+ printf 'ref %s %s\n' "$requested_ref" "$resolved"
276
+ return
277
+ fi
278
+
279
+ spw_die "cannot resolve upstream ref: $requested_ref"
280
+ }