@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/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
+ }
@@ -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
+ }