@pablozaiden/installer 0.0.0-development
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 +330 -0
- package/index.ts +3 -0
- package/install.sh +396 -0
- package/package.json +37 -0
- package/src/contract.ts +170 -0
- package/src/manifest.ts +214 -0
- package/src/update.ts +502 -0
package/install.sh
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
INSTALLER_VERSION="0.0.0-development"
|
|
5
|
+
DEFAULT_INSTALL_DIR="\$HOME/.local/bin"
|
|
6
|
+
DEFAULT_MANIFEST_PATHS=".github/installer.json .installer.json"
|
|
7
|
+
DEFAULT_CHECKSUM_EXTENSION=".sha256"
|
|
8
|
+
DEFAULT_REF="main"
|
|
9
|
+
|
|
10
|
+
usage() {
|
|
11
|
+
cat <<'USAGE'
|
|
12
|
+
Usage:
|
|
13
|
+
install.sh <owner>/<repo> [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--ref <ref> Repository ref to read manifests from (default: main)
|
|
17
|
+
--binary <name> Binary name to install when no manifest is available
|
|
18
|
+
--asset-prefix <prefix> Asset prefix for the most recent --binary
|
|
19
|
+
--install-dir <dir> Install directory (default: $HOME/.local/bin)
|
|
20
|
+
--checksum required|optional|none
|
|
21
|
+
Checksum policy when no manifest is available (default: required)
|
|
22
|
+
--help Show this help
|
|
23
|
+
|
|
24
|
+
Preferred use:
|
|
25
|
+
curl -fsSL https://raw.githubusercontent.com/pablozaiden/installer/main/install.sh | sh -s -- pablozaiden/link
|
|
26
|
+
|
|
27
|
+
Consuming repositories should publish .github/installer.json or .installer.json.
|
|
28
|
+
USAGE
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
error() {
|
|
32
|
+
echo "Error: $*" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
require_command() {
|
|
37
|
+
command -v "$1" >/dev/null 2>&1 || error "$1 is required."
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
detect_os() {
|
|
41
|
+
case "$(uname -s)" in
|
|
42
|
+
Linux*) echo "linux" ;;
|
|
43
|
+
Darwin*) echo "darwin" ;;
|
|
44
|
+
*) echo "unsupported" ;;
|
|
45
|
+
esac
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
detect_arch() {
|
|
49
|
+
case "$(uname -m)" in
|
|
50
|
+
x86_64|amd64) echo "x64" ;;
|
|
51
|
+
aarch64|arm64) echo "arm64" ;;
|
|
52
|
+
*) echo "unsupported" ;;
|
|
53
|
+
esac
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
make_temp_file() {
|
|
57
|
+
prefix=${1:-installer}
|
|
58
|
+
case "${OS:-}" in
|
|
59
|
+
darwin) mktemp -t "$prefix.XXXXXX" ;;
|
|
60
|
+
*) mktemp "${TMPDIR:-/tmp}/$prefix.XXXXXX" ;;
|
|
61
|
+
esac
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
expand_install_dir() {
|
|
65
|
+
case "$1" in
|
|
66
|
+
"\$HOME") echo "$HOME" ;;
|
|
67
|
+
"\$HOME/"*) echo "$HOME/${1#"\$HOME/"}" ;;
|
|
68
|
+
"~") echo "$HOME" ;;
|
|
69
|
+
"~/"*) echo "$HOME/${1#"~/"}" ;;
|
|
70
|
+
*) echo "$1" ;;
|
|
71
|
+
esac
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
json_string_value() {
|
|
75
|
+
key=$1
|
|
76
|
+
sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
json_boolean_value() {
|
|
80
|
+
key=$1
|
|
81
|
+
sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\\(true\\|false\\).*/\\1/p" | head -n 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
json_number_value() {
|
|
85
|
+
key=$1
|
|
86
|
+
sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p" | head -n 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
manifest_string_value() {
|
|
90
|
+
key=$1
|
|
91
|
+
printf "%s\n" "$MANIFEST_JSON" | tr '\n' ' ' | json_string_value "$key"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
manifest_schema_version() {
|
|
95
|
+
printf "%s\n" "$MANIFEST_JSON" | tr '\n' ' ' | json_number_value "schemaVersion"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
manifest_checksum_value() {
|
|
99
|
+
key=$1
|
|
100
|
+
printf "%s\n" "$MANIFEST_JSON" \
|
|
101
|
+
| tr '\n' ' ' \
|
|
102
|
+
| sed -n 's/.*"checksums"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' \
|
|
103
|
+
| json_string_value "$key"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
manifest_checksum_boolean() {
|
|
107
|
+
key=$1
|
|
108
|
+
printf "%s\n" "$MANIFEST_JSON" \
|
|
109
|
+
| tr '\n' ' ' \
|
|
110
|
+
| sed -n 's/.*"checksums"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' \
|
|
111
|
+
| json_boolean_value "$key"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
manifest_platform_arches() {
|
|
115
|
+
os=$1
|
|
116
|
+
printf "%s\n" "$MANIFEST_JSON" \
|
|
117
|
+
| tr '\n' ' ' \
|
|
118
|
+
| sed -n "s/.*\"platforms\"[[:space:]]*:[[:space:]]*{[^}]*\"$os\"[[:space:]]*:[[:space:]]*\\[\\([^]]*\\)\\].*/\\1/p" \
|
|
119
|
+
| tr ',' '\n' \
|
|
120
|
+
| sed 's/[ " ]//g' \
|
|
121
|
+
| sed '/^$/d'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
manifest_has_platforms() {
|
|
125
|
+
printf "%s\n" "$MANIFEST_JSON" | tr '\n' ' ' | sed -n 's/.*"platforms"[[:space:]]*:[[:space:]]*{.*/true/p' | head -n 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
manifest_binary_lines() {
|
|
129
|
+
printf "%s\n" "$MANIFEST_JSON" \
|
|
130
|
+
| tr '\n' ' ' \
|
|
131
|
+
| sed -n 's/.*"binaries"[[:space:]]*:[[:space:]]*\[\(.*\)$/\1/p' \
|
|
132
|
+
| sed 's/\][[:space:]]*,[[:space:]]*"[^"]*"[[:space:]]*:.*//' \
|
|
133
|
+
| sed 's/\][[:space:]]*}.*//' \
|
|
134
|
+
| sed 's/}[[:space:]]*,[[:space:]]*{/\}\n\{/g' \
|
|
135
|
+
| while IFS= read -r object || [ -n "$object" ]; do
|
|
136
|
+
name=$(printf "%s\n" "$object" | json_string_value "name")
|
|
137
|
+
[ -n "$name" ] || continue
|
|
138
|
+
prefix=$(printf "%s\n" "$object" | json_string_value "assetPrefix")
|
|
139
|
+
required=$(printf "%s\n" "$object" | json_boolean_value "required")
|
|
140
|
+
message=$(printf "%s\n" "$object" | json_string_value "postInstallMessage")
|
|
141
|
+
[ -n "$prefix" ] || prefix=$name
|
|
142
|
+
[ -n "$required" ] || required=true
|
|
143
|
+
printf "%s|%s|%s|%s\n" "$name" "$prefix" "$required" "$message"
|
|
144
|
+
done
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
manual_binary_lines() {
|
|
148
|
+
printf "%s\n" "$MANUAL_BINARIES" | while IFS='|' read -r name prefix || [ -n "$name" ]; do
|
|
149
|
+
[ -n "$name" ] || continue
|
|
150
|
+
[ -n "$prefix" ] || prefix=$name
|
|
151
|
+
printf "%s|%s|true|\n" "$name" "$prefix"
|
|
152
|
+
done
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fetch_manifest() {
|
|
156
|
+
repository=$1
|
|
157
|
+
ref=$2
|
|
158
|
+
for path in $DEFAULT_MANIFEST_PATHS; do
|
|
159
|
+
url="${RAW_BASE_URL:-https://raw.githubusercontent.com}/$repository/$ref/$path"
|
|
160
|
+
if body=$(curl -fsSL "$url" 2>/dev/null); then
|
|
161
|
+
MANIFEST_JSON=$body
|
|
162
|
+
MANIFEST_SOURCE=$path
|
|
163
|
+
return 0
|
|
164
|
+
fi
|
|
165
|
+
done
|
|
166
|
+
return 1
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
latest_release_tag() {
|
|
170
|
+
repository=$1
|
|
171
|
+
curl -fsSL "${GITHUB_API_BASE_URL:-https://api.github.com}/repos/$repository/releases/latest" \
|
|
172
|
+
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
173
|
+
| head -n 1
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
download_file() {
|
|
177
|
+
url=$1
|
|
178
|
+
output=$2
|
|
179
|
+
if ! curl -fsSL "$url" -o "$output"; then
|
|
180
|
+
return 1
|
|
181
|
+
fi
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
sha256_of_file() {
|
|
185
|
+
file=$1
|
|
186
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
187
|
+
sha256sum "$file" | awk '{print $1}'
|
|
188
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
189
|
+
shasum -a 256 "$file" | awk '{print $1}'
|
|
190
|
+
else
|
|
191
|
+
error "sha256sum or shasum is required to verify checksums."
|
|
192
|
+
fi
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
verify_checksum() {
|
|
196
|
+
asset_name=$1
|
|
197
|
+
binary_file=$2
|
|
198
|
+
checksum_file=$3
|
|
199
|
+
expected=$(awk 'NF {print $1; exit}' "$checksum_file" | tr 'A-F' 'a-f')
|
|
200
|
+
case "$expected" in
|
|
201
|
+
????????????????????????????????????????????????????????????????) ;;
|
|
202
|
+
*) error "Checksum for $asset_name did not contain a valid SHA-256 entry." ;;
|
|
203
|
+
esac
|
|
204
|
+
actual=$(sha256_of_file "$binary_file" | tr 'A-F' 'a-f')
|
|
205
|
+
if [ "$expected" != "$actual" ]; then
|
|
206
|
+
error "Checksum verification failed for $asset_name: expected $expected, got $actual."
|
|
207
|
+
fi
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
install_binary() {
|
|
211
|
+
repository=$1
|
|
212
|
+
tag=$2
|
|
213
|
+
os=$3
|
|
214
|
+
arch=$4
|
|
215
|
+
install_dir=$5
|
|
216
|
+
checksum_policy=$6
|
|
217
|
+
checksum_extension=$7
|
|
218
|
+
binary_name=$8
|
|
219
|
+
asset_prefix=$9
|
|
220
|
+
|
|
221
|
+
asset_name="$asset_prefix-$tag-$os-$arch"
|
|
222
|
+
download_url="${GITHUB_RELEASE_BASE_URL:-https://github.com}/$repository/releases/download/$tag/$asset_name"
|
|
223
|
+
temp_file=$(make_temp_file "$binary_name")
|
|
224
|
+
checksum_file=$(make_temp_file "$binary_name-checksum")
|
|
225
|
+
|
|
226
|
+
echo "Downloading $asset_name..."
|
|
227
|
+
if ! download_file "$download_url" "$temp_file"; then
|
|
228
|
+
rm -f "$temp_file" "$checksum_file"
|
|
229
|
+
error "Failed to download from $download_url"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
if [ "$checksum_policy" = "none" ]; then
|
|
233
|
+
echo "Skipping checksum verification for $asset_name; checksum policy is none."
|
|
234
|
+
else
|
|
235
|
+
checksum_name="$asset_name$checksum_extension"
|
|
236
|
+
checksum_url="$download_url$checksum_extension"
|
|
237
|
+
if download_file "$checksum_url" "$checksum_file"; then
|
|
238
|
+
echo "Verifying $checksum_name..."
|
|
239
|
+
verify_checksum "$asset_name" "$temp_file" "$checksum_file"
|
|
240
|
+
elif [ "$checksum_policy" = "required" ]; then
|
|
241
|
+
rm -f "$temp_file" "$checksum_file"
|
|
242
|
+
error "Failed to download required checksum from $checksum_url"
|
|
243
|
+
else
|
|
244
|
+
echo "Skipping checksum verification for $asset_name; $checksum_name was not published."
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
mv "$temp_file" "$install_dir/$binary_name"
|
|
249
|
+
rm -f "$checksum_file"
|
|
250
|
+
chmod +x "$install_dir/$binary_name"
|
|
251
|
+
echo "Installed $binary_name to $install_dir/$binary_name"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
REPOSITORY=
|
|
255
|
+
REF=$DEFAULT_REF
|
|
256
|
+
INSTALL_DIR=$DEFAULT_INSTALL_DIR
|
|
257
|
+
CHECKSUM_POLICY=required
|
|
258
|
+
CHECKSUM_OPTION_SET=false
|
|
259
|
+
CHECKSUM_EXTENSION=$DEFAULT_CHECKSUM_EXTENSION
|
|
260
|
+
MANUAL_BINARIES=
|
|
261
|
+
LAST_BINARY=
|
|
262
|
+
|
|
263
|
+
while [ $# -gt 0 ]; do
|
|
264
|
+
case "$1" in
|
|
265
|
+
--help|-h)
|
|
266
|
+
usage
|
|
267
|
+
exit 0
|
|
268
|
+
;;
|
|
269
|
+
--ref)
|
|
270
|
+
[ $# -gt 1 ] || error "--ref requires a value."
|
|
271
|
+
REF=$2
|
|
272
|
+
shift 2
|
|
273
|
+
;;
|
|
274
|
+
--binary)
|
|
275
|
+
[ $# -gt 1 ] || error "--binary requires a value."
|
|
276
|
+
LAST_BINARY=$2
|
|
277
|
+
MANUAL_BINARIES="${MANUAL_BINARIES}${MANUAL_BINARIES:+
|
|
278
|
+
}$LAST_BINARY|$LAST_BINARY"
|
|
279
|
+
shift 2
|
|
280
|
+
;;
|
|
281
|
+
--asset-prefix)
|
|
282
|
+
[ $# -gt 1 ] || error "--asset-prefix requires a value."
|
|
283
|
+
[ -n "$LAST_BINARY" ] || error "--asset-prefix must follow --binary."
|
|
284
|
+
prefix=$2
|
|
285
|
+
MANUAL_BINARIES=$(printf "%s\n" "$MANUAL_BINARIES" | awk -F'|' -v binary="$LAST_BINARY" -v prefix="$prefix" 'BEGIN { OFS = "|" } $1 == binary { $2 = prefix } { print }')
|
|
286
|
+
shift 2
|
|
287
|
+
;;
|
|
288
|
+
--install-dir)
|
|
289
|
+
[ $# -gt 1 ] || error "--install-dir requires a value."
|
|
290
|
+
INSTALL_DIR=$2
|
|
291
|
+
shift 2
|
|
292
|
+
;;
|
|
293
|
+
--checksum)
|
|
294
|
+
[ $# -gt 1 ] || error "--checksum requires a value."
|
|
295
|
+
case "$2" in
|
|
296
|
+
required|optional|none) CHECKSUM_POLICY=$2 ;;
|
|
297
|
+
*) error "--checksum must be required, optional, or none." ;;
|
|
298
|
+
esac
|
|
299
|
+
CHECKSUM_OPTION_SET=true
|
|
300
|
+
shift 2
|
|
301
|
+
;;
|
|
302
|
+
--*)
|
|
303
|
+
error "Unknown option: $1"
|
|
304
|
+
;;
|
|
305
|
+
*)
|
|
306
|
+
[ -z "$REPOSITORY" ] || error "Unexpected positional argument: $1"
|
|
307
|
+
REPOSITORY=$1
|
|
308
|
+
shift
|
|
309
|
+
;;
|
|
310
|
+
esac
|
|
311
|
+
done
|
|
312
|
+
|
|
313
|
+
[ -n "$REPOSITORY" ] || {
|
|
314
|
+
usage
|
|
315
|
+
exit 1
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case "$REPOSITORY" in
|
|
319
|
+
*/*) ;;
|
|
320
|
+
*) error "Repository must be <owner>/<repo>." ;;
|
|
321
|
+
esac
|
|
322
|
+
|
|
323
|
+
require_command curl
|
|
324
|
+
require_command sed
|
|
325
|
+
require_command awk
|
|
326
|
+
|
|
327
|
+
OS=$(detect_os)
|
|
328
|
+
ARCH=$(detect_arch)
|
|
329
|
+
[ "$OS" != "unsupported" ] || error "Unsupported operating system: $(uname -s). Supported systems are Linux and macOS."
|
|
330
|
+
[ "$ARCH" != "unsupported" ] || error "Unsupported architecture: $(uname -m). Supported architectures are x64 and arm64."
|
|
331
|
+
|
|
332
|
+
echo "Installer version: $INSTALLER_VERSION"
|
|
333
|
+
echo "Detected platform: $OS-$ARCH"
|
|
334
|
+
|
|
335
|
+
MANIFEST_JSON=
|
|
336
|
+
MANIFEST_SOURCE=
|
|
337
|
+
if fetch_manifest "$REPOSITORY" "$REF"; then
|
|
338
|
+
echo "Loaded installer manifest: $MANIFEST_SOURCE"
|
|
339
|
+
manifest_schema_version=$(manifest_schema_version)
|
|
340
|
+
[ "$manifest_schema_version" = "1" ] || error "Unsupported installer manifest schemaVersion: ${manifest_schema_version:-missing}. Supported schemaVersion is 1."
|
|
341
|
+
manifest_repo=$(manifest_string_value "repo")
|
|
342
|
+
[ -z "$manifest_repo" ] || REPOSITORY=$manifest_repo
|
|
343
|
+
manifest_install_dir=$(manifest_string_value "installDir")
|
|
344
|
+
[ -z "$manifest_install_dir" ] || INSTALL_DIR=$manifest_install_dir
|
|
345
|
+
manifest_checksum_required=$(manifest_checksum_boolean "required")
|
|
346
|
+
if [ "$CHECKSUM_OPTION_SET" != "true" ] && [ -n "$manifest_checksum_required" ]; then
|
|
347
|
+
if [ "$manifest_checksum_required" = "true" ]; then
|
|
348
|
+
CHECKSUM_POLICY=required
|
|
349
|
+
else
|
|
350
|
+
CHECKSUM_POLICY=optional
|
|
351
|
+
fi
|
|
352
|
+
fi
|
|
353
|
+
manifest_checksum_extension=$(manifest_checksum_value "extension")
|
|
354
|
+
[ -z "$manifest_checksum_extension" ] || CHECKSUM_EXTENSION=$manifest_checksum_extension
|
|
355
|
+
manifest_supported_arches=$(manifest_platform_arches "$OS")
|
|
356
|
+
if [ "$(manifest_has_platforms)" = "true" ] && { [ -z "$manifest_supported_arches" ] || ! printf "%s\n" "$manifest_supported_arches" | grep -qx "$ARCH"; }; then
|
|
357
|
+
error "Manifest does not support platform $OS-$ARCH."
|
|
358
|
+
fi
|
|
359
|
+
BINARY_LINES=$(manifest_binary_lines)
|
|
360
|
+
else
|
|
361
|
+
[ -n "$MANUAL_BINARIES" ] || error "No installer manifest found. Add .github/installer.json or pass --binary <name>."
|
|
362
|
+
BINARY_LINES=$(manual_binary_lines)
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
[ -n "$BINARY_LINES" ] || error "No binaries configured for installation."
|
|
366
|
+
|
|
367
|
+
INSTALL_DIR=$(expand_install_dir "$INSTALL_DIR")
|
|
368
|
+
mkdir -p "$INSTALL_DIR"
|
|
369
|
+
|
|
370
|
+
echo "Fetching latest release..."
|
|
371
|
+
LATEST_TAG=$(latest_release_tag "$REPOSITORY")
|
|
372
|
+
[ -n "$LATEST_TAG" ] || error "Could not determine latest release version."
|
|
373
|
+
echo "Latest version: $LATEST_TAG"
|
|
374
|
+
|
|
375
|
+
printf "%s\n" "$BINARY_LINES" | while IFS='|' read -r binary_name asset_prefix required post_message; do
|
|
376
|
+
[ -n "$binary_name" ] || continue
|
|
377
|
+
install_binary "$REPOSITORY" "$LATEST_TAG" "$OS" "$ARCH" "$INSTALL_DIR" "$CHECKSUM_POLICY" "$CHECKSUM_EXTENSION" "$binary_name" "$asset_prefix"
|
|
378
|
+
[ -z "$post_message" ] || echo "$post_message"
|
|
379
|
+
done
|
|
380
|
+
|
|
381
|
+
case ":$PATH:" in
|
|
382
|
+
*":$INSTALL_DIR:"*)
|
|
383
|
+
echo ""
|
|
384
|
+
echo "Installation complete!"
|
|
385
|
+
;;
|
|
386
|
+
*)
|
|
387
|
+
echo ""
|
|
388
|
+
echo "Warning: $INSTALL_DIR is not in your PATH."
|
|
389
|
+
echo ""
|
|
390
|
+
echo "Add it to your shell profile:"
|
|
391
|
+
echo " export PATH=\"$INSTALL_DIR:\$PATH\""
|
|
392
|
+
echo ""
|
|
393
|
+
echo "Or run installed binaries directly from:"
|
|
394
|
+
echo " $INSTALL_DIR"
|
|
395
|
+
;;
|
|
396
|
+
esac
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pablozaiden/installer",
|
|
3
|
+
"version": "0.0.0-development",
|
|
4
|
+
"description": "Reusable GitHub release binary installer and updater utilities",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"src/**/*",
|
|
10
|
+
"install.sh",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./index.ts",
|
|
16
|
+
"default": "./index.ts"
|
|
17
|
+
},
|
|
18
|
+
"./manifest": {
|
|
19
|
+
"types": "./src/manifest.ts",
|
|
20
|
+
"default": "./src/manifest.ts"
|
|
21
|
+
},
|
|
22
|
+
"./update": {
|
|
23
|
+
"types": "./src/update.ts",
|
|
24
|
+
"default": "./src/update.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "bunx tsc -p tsconfig.json --noEmit",
|
|
29
|
+
"test": "bun test"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/bun": "latest"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"typescript": "^5"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/contract.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export const DEFAULT_INSTALL_DIR = "$HOME/.local/bin";
|
|
2
|
+
export const DEFAULT_CHECKSUM_EXTENSION = ".sha256";
|
|
3
|
+
export const DEFAULT_INSTALLER_MANIFEST_PATHS = [
|
|
4
|
+
".github/installer.json",
|
|
5
|
+
".installer.json",
|
|
6
|
+
] as const;
|
|
7
|
+
export const DEFAULT_GITHUB_API_VERSION = "2022-11-28";
|
|
8
|
+
|
|
9
|
+
export const SUPPORTED_RELEASE_TARGETS = [
|
|
10
|
+
"linux-x64",
|
|
11
|
+
"linux-arm64",
|
|
12
|
+
"darwin-x64",
|
|
13
|
+
"darwin-arm64",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type ReleaseOs = "linux" | "darwin";
|
|
17
|
+
export type ReleaseArch = "x64" | "arm64";
|
|
18
|
+
export type ReleaseTarget = `${ReleaseOs}-${ReleaseArch}`;
|
|
19
|
+
|
|
20
|
+
export type ReleasePlatform = {
|
|
21
|
+
os: ReleaseOs;
|
|
22
|
+
arch: ReleaseArch;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type GitHubRepository = `${string}/${string}`;
|
|
26
|
+
|
|
27
|
+
export function normalizeReleaseVersion(rawValue: string): string {
|
|
28
|
+
const trimmed = rawValue.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
throw new Error("Missing release version.");
|
|
31
|
+
}
|
|
32
|
+
return trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizeReleaseTag(rawValue: string): string {
|
|
36
|
+
return `v${normalizeReleaseVersion(rawValue)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ParsedVersion = {
|
|
40
|
+
major: number;
|
|
41
|
+
minor: number;
|
|
42
|
+
patch: number;
|
|
43
|
+
prerelease: string[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function parseVersion(value: string): ParsedVersion | null {
|
|
47
|
+
const parsed = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(normalizeReleaseVersion(value));
|
|
48
|
+
if (!parsed) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
major: Number(parsed[1]),
|
|
54
|
+
minor: Number(parsed[2]),
|
|
55
|
+
patch: Number(parsed[3]),
|
|
56
|
+
prerelease: parsed[4]?.split(".") ?? [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function comparePrereleaseIdentifiers(left: string[], right: string[]): number {
|
|
61
|
+
const limit = Math.max(left.length, right.length);
|
|
62
|
+
for (let index = 0; index < limit; index += 1) {
|
|
63
|
+
const leftPart = left[index];
|
|
64
|
+
const rightPart = right[index];
|
|
65
|
+
if (leftPart === undefined) {
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
if (rightPart === undefined) {
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
if (leftPart === rightPart) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
|
|
76
|
+
const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
|
|
77
|
+
if (leftNumber !== null && rightNumber !== null) {
|
|
78
|
+
return leftNumber === rightNumber ? 0 : leftNumber < rightNumber ? -1 : 1;
|
|
79
|
+
}
|
|
80
|
+
if (leftNumber !== null) {
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
if (rightNumber !== null) {
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
return leftPart.localeCompare(rightPart);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function compareReleaseVersions(left: string, right: string): number {
|
|
93
|
+
const leftVersion = parseVersion(left);
|
|
94
|
+
const rightVersion = parseVersion(right);
|
|
95
|
+
if (!leftVersion || !rightVersion) {
|
|
96
|
+
return normalizeReleaseVersion(left).localeCompare(normalizeReleaseVersion(right));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (leftVersion.major !== rightVersion.major) {
|
|
100
|
+
return leftVersion.major < rightVersion.major ? -1 : 1;
|
|
101
|
+
}
|
|
102
|
+
if (leftVersion.minor !== rightVersion.minor) {
|
|
103
|
+
return leftVersion.minor < rightVersion.minor ? -1 : 1;
|
|
104
|
+
}
|
|
105
|
+
if (leftVersion.patch !== rightVersion.patch) {
|
|
106
|
+
return leftVersion.patch < rightVersion.patch ? -1 : 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const leftHasPrerelease = leftVersion.prerelease.length > 0;
|
|
110
|
+
const rightHasPrerelease = rightVersion.prerelease.length > 0;
|
|
111
|
+
if (!leftHasPrerelease && !rightHasPrerelease) {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
if (!leftHasPrerelease) {
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
if (!rightHasPrerelease) {
|
|
118
|
+
return -1;
|
|
119
|
+
}
|
|
120
|
+
return comparePrereleaseIdentifiers(leftVersion.prerelease, rightVersion.prerelease);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function releasePlatformToTarget(platform: ReleasePlatform): ReleaseTarget {
|
|
124
|
+
return `${platform.os}-${platform.arch}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function parseReleaseTarget(target: string): ReleasePlatform {
|
|
128
|
+
const [os, arch] = target.split("-");
|
|
129
|
+
if (!os || !arch) {
|
|
130
|
+
throw new Error(`Invalid release target "${target}". Expected <os>-<arch>.`);
|
|
131
|
+
}
|
|
132
|
+
return resolveReleasePlatform(os, arch);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveReleasePlatform(platform: string, arch: string): ReleasePlatform {
|
|
136
|
+
const os = platform === "linux" || platform === "darwin" ? platform : undefined;
|
|
137
|
+
const normalizedArch = arch === "x64" || arch === "amd64"
|
|
138
|
+
? "x64"
|
|
139
|
+
: arch === "arm64" || arch === "aarch64"
|
|
140
|
+
? "arm64"
|
|
141
|
+
: undefined;
|
|
142
|
+
|
|
143
|
+
if (os && normalizedArch) {
|
|
144
|
+
return {
|
|
145
|
+
os,
|
|
146
|
+
arch: normalizedArch,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}. Supported release targets are Linux and macOS on x64 and arm64.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildReleaseAssetName(assetPrefix: string, tag: string, target: ReleasePlatform): string {
|
|
154
|
+
return `${assetPrefix}-${normalizeReleaseTag(tag)}-${target.os}-${target.arch}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function assertGitHubRepository(value: string): GitHubRepository {
|
|
158
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value)) {
|
|
159
|
+
throw new Error(`Invalid GitHub repository "${value}". Expected <owner>/<repo>.`);
|
|
160
|
+
}
|
|
161
|
+
return value as GitHubRepository;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function githubApiRepositoryUrl(repository: GitHubRepository): string {
|
|
165
|
+
return `https://api.github.com/repos/${repository}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function githubRawRepositoryUrl(repository: GitHubRepository, ref = "main"): string {
|
|
169
|
+
return `https://raw.githubusercontent.com/${repository}/${ref}`;
|
|
170
|
+
}
|