pgserve 2.5.0 → 2.6.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 CHANGED
@@ -101,17 +101,14 @@ psql postgresql://localhost:8432/myapp
101
101
  ## Installation
102
102
 
103
103
  ```bash
104
- # Zero install (recommended)
105
- npx pgserve
106
-
107
- # Global install
108
- npm install -g pgserve
104
+ # Canonical install — signed binary from GitHub Releases
105
+ curl -fsSL https://raw.githubusercontent.com/namastexlabs/pgserve/main/install.sh | bash
109
106
 
110
- # Project dependency
111
- npm install pgserve
107
+ # Pinned version
108
+ PGSERVE_VERSION=v2.6.0 curl -fsSL .../install.sh | bash
112
109
  ```
113
110
 
114
- > PostgreSQL binaries are automatically downloaded on first run (~100MB).
111
+ > `install.sh` fetches the signed tarball from GitHub Releases and verifies it via `gh attestation verify` (Sigstore Rekor public-good). Requires the [`gh` CLI](https://cli.github.com/). pgserve no longer depends on npm — the install + upgrade path is binary tarballs all the way down.
115
112
 
116
113
  ### Windows
117
114
 
@@ -47,6 +47,25 @@ const __installSubcommands = new Set([
47
47
  // `verify` shells out to cosign + writes an HMAC cache token. Pure node
48
48
  // (no bun) so it must skip the bun probe like the install surface above.
49
49
  'verify',
50
+ // pgserve singleton (v2.4) — wish Group 3, read-only V1. `doctor` runs
51
+ // entirely in node (admin.json + supervisor probes + socket reachability)
52
+ // and must skip the bun probe so it works on any installed binary.
53
+ 'doctor',
54
+ // pgserve singleton (v2.4) — wish Group 3. `trust` manages the
55
+ // user-extensible cosign trust store at ~/.pgserve/trust/identities.json.
56
+ // Pure node, must skip bun probe.
57
+ 'trust',
58
+ // pgserve singleton (v2.4) — wish Group 3, verb 3. `gc` shells out to
59
+ // psql to scan pgserve_meta + pg_database, classify orphans, and
60
+ // (under --apply) DROP DATABASE. Pure node + child_process, must skip
61
+ // the bun probe.
62
+ 'gc',
63
+ // pgserve singleton (v2.4) — wish Group 3, verb 4. `provision` shells
64
+ // out to psql to CREATE ROLE / CREATE DATABASE / GRANT / INSERT INTO
65
+ // pgserve_meta. Idempotency-driven (no advisory lock — see
66
+ // src/commands/provision.js header for why). Pure node + child_process,
67
+ // must skip bun probe.
68
+ 'provision',
50
69
  ]);
51
70
  if (__subcommand && __installSubcommands.has(__subcommand)) {
52
71
  const cli = require(path.join(__dirname, '..', 'src', 'cli-install.cjs'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # aggregate-manifest.sh — Group 8 of autopg-distribution-cutover.
4
+ #
5
+ # Walks dist/autopg-<version>-<platform>.tar.gz, gathers each tarball's
6
+ # SHA256 + signature URL + provenance URL + platform tuple, and emits
7
+ # dist/manifest.json that consumers (install.sh, autopg update) read to
8
+ # resolve a download for a given (channel, version, platform).
9
+ #
10
+ # This runs after Group 8 signing + provenance generation and before
11
+ # Group 9 (CDN publish).
12
+ #
13
+ # Usage:
14
+ # bash scripts/aggregate-manifest.sh --version 2.260503.1
15
+ # bash scripts/aggregate-manifest.sh --version 2.260503.1 --base-url https://cdn.automagik.dev/autopg/stable/2.260503.1
16
+ #
17
+ # Output:
18
+ # dist/manifest.json
19
+ #
20
+ # Exit codes:
21
+ # 0 ok
22
+ # 1 IO failure
23
+ # 2 invalid args / missing inputs
24
+
25
+ set -euo pipefail
26
+
27
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
28
+ DIST_DIR="${AUTOPG_DIST_DIR:-${REPO_ROOT}/dist}"
29
+
30
+ usage() {
31
+ cat <<EOF
32
+ Usage: $0 --version <v> [--base-url <url>] [--channel <c>] [--cosign-pub-url <url>]
33
+
34
+ --version autopg version, e.g. 2.260503.1 (or read from package.json)
35
+ --base-url absolute base URL prefix for tarball URLs
36
+ (default: relative — consumers resolve against the
37
+ directory the manifest sits in).
38
+ --channel channel hint embedded in the manifest (stable|beta|canary).
39
+ default: stable
40
+ --cosign-pub-url absolute URL to the published cosign public key.
41
+ default: <base-url>/../../keys/cosign.pub for production
42
+ (cdn.automagik.dev layout) or "keys/cosign.pub" relative.
43
+
44
+ Reads:
45
+ dist/autopg-<version>-<platform>.tar.gz
46
+ dist/autopg-<version>-<platform>.tar.gz.sha256
47
+ dist/autopg-<version>-<platform>.tar.gz.sig (optional)
48
+ dist/autopg-<version>-<platform>.tar.gz.intoto.jsonl (optional)
49
+
50
+ Writes:
51
+ dist/manifest.json
52
+ EOF
53
+ }
54
+
55
+ parse_args() {
56
+ VERSION="${AUTOPG_VERSION:-}"
57
+ BASE_URL=""
58
+ CHANNEL="stable"
59
+ COSIGN_PUB_URL=""
60
+ while [[ $# -gt 0 ]]; do
61
+ case "$1" in
62
+ --version) VERSION="$2"; shift 2 ;;
63
+ --base-url) BASE_URL="$2"; shift 2 ;;
64
+ --channel) CHANNEL="$2"; shift 2 ;;
65
+ --cosign-pub-url) COSIGN_PUB_URL="$2"; shift 2 ;;
66
+ -h|--help) usage; exit 0 ;;
67
+ *) echo "unknown arg: $1" >&2; usage; exit 2 ;;
68
+ esac
69
+ done
70
+ if [[ -z "$VERSION" ]]; then
71
+ VERSION=$(node -p "require('${REPO_ROOT}/package.json').version" 2>/dev/null || echo "")
72
+ fi
73
+ if [[ -z "$VERSION" ]]; then
74
+ echo "error: --version required (or set in package.json)" >&2; exit 2
75
+ fi
76
+ }
77
+
78
+ # Portable SHA256 — sha256sum on linux, shasum -a 256 on macOS.
79
+ sha256_of() {
80
+ if command -v sha256sum >/dev/null 2>&1; then
81
+ sha256sum "$1" | awk '{print $1}'
82
+ else
83
+ shasum -a 256 "$1" | awk '{print $1}'
84
+ fi
85
+ }
86
+
87
+ prefix_url() {
88
+ local rel="$1"
89
+ if [[ -n "$BASE_URL" ]]; then
90
+ printf '%s/%s' "${BASE_URL%/}" "$rel"
91
+ else
92
+ printf '%s' "$rel"
93
+ fi
94
+ }
95
+
96
+ emit_entry() {
97
+ local tarball="$1"
98
+ local first="$2"
99
+ local base platform sha sz
100
+ base=$(basename "$tarball")
101
+ # Strip "autopg-<version>-" prefix and ".tar.gz" suffix to get platform.
102
+ platform="${base#autopg-${VERSION}-}"
103
+ platform="${platform%.tar.gz}"
104
+
105
+ if [[ ! -f "${tarball}.sha256" ]]; then
106
+ echo "error: ${tarball}.sha256 missing — run assemble-tarball.sh first" >&2
107
+ return 1
108
+ fi
109
+ sha=$(awk '{print $1}' "${tarball}.sha256")
110
+ sz=$(stat -c %s "$tarball" 2>/dev/null || stat -f %z "$tarball")
111
+
112
+ local sig_url="" prov_url=""
113
+ if [[ -f "${tarball}.sig" ]]; then
114
+ sig_url=$(prefix_url "${base}.sig")
115
+ fi
116
+ if [[ -f "${tarball}.intoto.jsonl" ]]; then
117
+ prov_url=$(prefix_url "${base}.intoto.jsonl")
118
+ fi
119
+
120
+ if [[ "$first" -eq 0 ]]; then printf ',\n'; fi
121
+ printf ' {\n'
122
+ printf ' "platform": "%s",\n' "$platform"
123
+ printf ' "file": "%s",\n' "$base"
124
+ printf ' "url": "%s",\n' "$(prefix_url "$base")"
125
+ printf ' "sha256": "%s",\n' "$sha"
126
+ printf ' "size": %d,\n' "$sz"
127
+ printf ' "signature_url": "%s",\n' "$sig_url"
128
+ printf ' "provenance_url": "%s"\n' "$prov_url"
129
+ printf ' }'
130
+ }
131
+
132
+ main() {
133
+ parse_args "$@"
134
+ [[ -d "$DIST_DIR" ]] || { echo "error: $DIST_DIR not a directory" >&2; exit 2; }
135
+
136
+ local tarballs=()
137
+ while IFS= read -r line; do
138
+ tarballs+=("$line")
139
+ done < <(find "$DIST_DIR" -maxdepth 1 -name "autopg-${VERSION}-*.tar.gz" -type f | LC_ALL=C sort)
140
+
141
+ if [[ ${#tarballs[@]} -eq 0 ]]; then
142
+ echo "error: no autopg-${VERSION}-*.tar.gz files in ${DIST_DIR}" >&2
143
+ exit 2
144
+ fi
145
+
146
+ local generated_at
147
+ generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
148
+
149
+ local out="${DIST_DIR}/manifest.json"
150
+ {
151
+ printf '{\n'
152
+ printf ' "name": "autopg",\n'
153
+ printf ' "version": "%s",\n' "$VERSION"
154
+ printf ' "channel": "%s",\n' "$CHANNEL"
155
+ printf ' "schemaVersion": 1,\n'
156
+ printf ' "generated_at": "%s",\n' "$generated_at"
157
+ local cpub
158
+ if [[ -n "$COSIGN_PUB_URL" ]]; then
159
+ cpub="$COSIGN_PUB_URL"
160
+ elif [[ -n "$BASE_URL" ]]; then
161
+ # CDN layout: <base>/autopg/<channel>/<version>/manifest.json
162
+ # Public key lives at: <base>/autopg/keys/cosign.pub
163
+ # If caller passes the full <base>/autopg/<channel>/<version> as
164
+ # base-url, walk two levels up to reach the keys/ sibling.
165
+ cpub="${BASE_URL%/*}"
166
+ cpub="${cpub%/*}/keys/cosign.pub"
167
+ else
168
+ cpub="keys/cosign.pub"
169
+ fi
170
+ printf ' "cosign_pub_url": "%s",\n' "$cpub"
171
+ printf ' "platforms": [\n'
172
+ local first=1
173
+ for t in "${tarballs[@]}"; do
174
+ emit_entry "$t" "$first"
175
+ first=0
176
+ done
177
+ printf '\n ]\n'
178
+ printf '}\n'
179
+ } > "$out"
180
+
181
+ echo "==> manifest: $out ($(wc -l < "$out") lines, ${#tarballs[@]} platforms)"
182
+ }
183
+
184
+ main "$@"
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # assemble-tarball.sh — Group 7 of autopg-distribution-cutover.
4
+ #
5
+ # Assembles a single platform tarball with the locked shape:
6
+ #
7
+ # autopg/
8
+ # autopg # static binary (from build-binary.sh)
9
+ # postgres/
10
+ # bin/* # postgres + initdb + libpq + ...
11
+ # share/* # timezone data, locale, etc.
12
+ # manifest.json # per-file SHA256 + size
13
+ #
14
+ # The tarball lives at:
15
+ # dist/autopg-<version>-<platform>.tar.gz
16
+ # and a sibling .sha256 file holds the outer hash for Group 8 (cosign sign)
17
+ # and Group 9 (CDN publish) to consume.
18
+ #
19
+ # Inputs come from dist/<platform>/autopg/{autopg, postgres/}, populated by
20
+ # build-binary.sh + fetch-postgres-bins.sh.
21
+ #
22
+ # Usage:
23
+ # scripts/assemble-tarball.sh --platform linux-x64-glibc
24
+ # scripts/assemble-tarball.sh --all --version 2.260503.1
25
+
26
+ set -euo pipefail
27
+
28
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
29
+ DIST_DIR="${AUTOPG_DIST_DIR:-${REPO_ROOT}/dist}"
30
+
31
+ PLATFORMS=(linux-x64-glibc linux-x64-musl linux-arm64 darwin-x64 darwin-arm64)
32
+
33
+ usage() {
34
+ cat <<EOF
35
+ Usage: $0 (--platform <p> | --all) [--version <v>]
36
+
37
+ Platforms: ${PLATFORMS[*]}
38
+
39
+ Inputs (must already exist):
40
+ dist/<platform>/autopg/autopg (build-binary.sh)
41
+ dist/<platform>/autopg/postgres/bin/* (fetch-postgres-bins.sh)
42
+ dist/<platform>/autopg/postgres/share/* (fetch-postgres-bins.sh)
43
+
44
+ Outputs:
45
+ dist/autopg-<version>-<platform>.tar.gz
46
+ dist/autopg-<version>-<platform>.tar.gz.sha256
47
+ EOF
48
+ }
49
+
50
+ parse_args() {
51
+ TARGET_PLATFORM=""
52
+ ASSEMBLE_ALL=0
53
+ VERSION="${AUTOPG_VERSION:-}"
54
+
55
+ while [[ $# -gt 0 ]]; do
56
+ case "$1" in
57
+ --platform) TARGET_PLATFORM="$2"; shift 2 ;;
58
+ --all) ASSEMBLE_ALL=1; shift ;;
59
+ --version) VERSION="$2"; shift 2 ;;
60
+ -h|--help) usage; exit 0 ;;
61
+ *) echo "unknown arg: $1" >&2; usage; exit 2 ;;
62
+ esac
63
+ done
64
+
65
+ if [[ "$ASSEMBLE_ALL" -eq 0 && -z "$TARGET_PLATFORM" ]]; then
66
+ echo "error: pass --platform <p> or --all" >&2; usage; exit 2
67
+ fi
68
+
69
+ if [[ -z "$VERSION" ]]; then
70
+ VERSION=$(node -p "require('${REPO_ROOT}/package.json').version" 2>/dev/null || echo "0.0.0")
71
+ fi
72
+ }
73
+
74
+ # Portable SHA256 — use sha256sum on linux, shasum -a 256 on macOS.
75
+ sha256_of() {
76
+ if command -v sha256sum >/dev/null 2>&1; then
77
+ sha256sum "$1" | awk '{print $1}'
78
+ else
79
+ shasum -a 256 "$1" | awk '{print $1}'
80
+ fi
81
+ }
82
+
83
+ # Emit manifest.json for the given platform's staged tree.
84
+ # Walks autopg/ relative to <root>/, skipping manifest.json itself.
85
+ emit_manifest() {
86
+ local root="$1" platform="$2" out="$3"
87
+
88
+ pushd "$root" >/dev/null
89
+ {
90
+ printf '{\n'
91
+ printf ' "name": "autopg",\n'
92
+ printf ' "version": "%s",\n' "$VERSION"
93
+ printf ' "platform": "%s",\n' "$platform"
94
+ printf ' "schemaVersion": 1,\n'
95
+ printf ' "files": [\n'
96
+
97
+ local first=1
98
+ while IFS= read -r f; do
99
+ [[ "$f" == "autopg/manifest.json" ]] && continue
100
+ local h sz
101
+ h=$(sha256_of "$f")
102
+ sz=$(stat -c %s "$f" 2>/dev/null || stat -f %z "$f")
103
+ if [[ $first -eq 1 ]]; then
104
+ first=0
105
+ else
106
+ printf ',\n'
107
+ fi
108
+ printf ' { "path": "%s", "sha256": "%s", "size": %d }' "$f" "$h" "$sz"
109
+ done < <(find autopg -type f | LC_ALL=C sort)
110
+
111
+ printf '\n ]\n'
112
+ printf '}\n'
113
+ } > "$out"
114
+ popd >/dev/null
115
+ }
116
+
117
+ # Verify staged inputs are present + executable.
118
+ verify_inputs() {
119
+ local stage="$1" platform="$2"
120
+ local missing=0
121
+ for required in autopg/autopg autopg/postgres/bin/postgres; do
122
+ if [[ ! -f "${stage}/${required}" ]]; then
123
+ echo "error: ${platform}: missing ${required}" >&2
124
+ missing=1
125
+ fi
126
+ done
127
+ return $missing
128
+ }
129
+
130
+ assemble_one() {
131
+ local platform="$1"
132
+ local stage="${DIST_DIR}/${platform}"
133
+ local tarball="${DIST_DIR}/autopg-${VERSION}-${platform}.tar.gz"
134
+ local outer_sha="${tarball}.sha256"
135
+
136
+ if [[ ! -d "${stage}/autopg" ]]; then
137
+ echo "error: ${stage}/autopg/ does not exist (run build-binary.sh + fetch-postgres-bins.sh first)" >&2
138
+ return 1
139
+ fi
140
+
141
+ echo "==> [${platform}] assemble tarball"
142
+ # `assemble_one` is invoked from main as `assemble_one ... || rc=$?` which
143
+ # disables `set -e` for the duration of this function. Each potentially-
144
+ # failing command needs explicit `|| return 1` to halt early instead of
145
+ # silently producing a corrupt tarball (gemini bot review HIGH on PR #84).
146
+ verify_inputs "$stage" "$platform" || return 1
147
+
148
+ # 1) emit per-file manifest BEFORE the tarball is rolled — manifest is
149
+ # bundled inside.
150
+ emit_manifest "$stage" "$platform" "${stage}/autopg/manifest.json" || return 1
151
+
152
+ # 2) ensure binaries are executable inside the tar.
153
+ chmod +x "${stage}/autopg/autopg" || true
154
+ find "${stage}/autopg/postgres/bin" -type f -exec chmod +x {} +
155
+
156
+ # 3) build deterministic tarball: sorted entries, locked mtime.
157
+ local tar_flags=()
158
+ if tar --help 2>&1 | grep -q -- '--sort=name'; then
159
+ tar_flags+=(--sort=name)
160
+ fi
161
+ if tar --help 2>&1 | grep -q -- '--mtime='; then
162
+ tar_flags+=(--mtime=2026-01-01)
163
+ fi
164
+ if tar --help 2>&1 | grep -q -- '--owner='; then
165
+ tar_flags+=(--owner=0 --group=0 --numeric-owner)
166
+ fi
167
+
168
+ tar -C "$stage" -czf "$tarball" "${tar_flags[@]}" autopg/ || return 1
169
+ echo " ✓ tarball: $tarball ($(du -h "$tarball" | cut -f1))"
170
+
171
+ # 4) outer SHA256 — Group 8 cosign-signs this; Group 9 publishes both.
172
+ sha256_of "$tarball" > "$outer_sha" || return 1
173
+ echo " ✓ sha256: $(cat "$outer_sha") $(basename "$tarball")"
174
+ }
175
+
176
+ main() {
177
+ parse_args "$@"
178
+ mkdir -p "$DIST_DIR"
179
+
180
+ local rc=0
181
+ if [[ "$ASSEMBLE_ALL" -eq 1 ]]; then
182
+ for p in "${PLATFORMS[@]}"; do
183
+ assemble_one "$p" || rc=$?
184
+ done
185
+ else
186
+ assemble_one "$TARGET_PLATFORM" || rc=$?
187
+ fi
188
+ exit $rc
189
+ }
190
+
191
+ main "$@"
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # build-binary.sh — Group 7 of autopg-distribution-cutover.
4
+ #
5
+ # Compiles the autopg CLI to a static binary using `bun build --compile`
6
+ # for one or all of the 5 supported platforms:
7
+ #
8
+ # linux-x64-glibc, linux-x64-musl, linux-arm64,
9
+ # darwin-x64, darwin-arm64
10
+ #
11
+ # Outputs land at: dist/<platform>/autopg/autopg
12
+ #
13
+ # Per the wish G7 fallback contract (distribution-exodus G1): if
14
+ # `bun build --compile` fails for a target, retry with `pkg`/`nexe`
15
+ # when AUTOPG_BUILD_FALLBACK=1. The fallback is recorded in the build
16
+ # log so Group 9's CDN publish can surface it.
17
+ #
18
+ # Usage:
19
+ # scripts/build-binary.sh --platform linux-x64-glibc
20
+ # scripts/build-binary.sh --all
21
+ # scripts/build-binary.sh --platform darwin-arm64 --version 2.260503.1
22
+ #
23
+ # Exit codes:
24
+ # 0 success
25
+ # 1 bun build failed AND fallback disabled or also failed
26
+ # 2 invalid arguments / unsupported platform
27
+
28
+ set -euo pipefail
29
+
30
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
31
+ ENTRY_POINT="${AUTOPG_ENTRY_POINT:-bin/postgres-server.js}"
32
+ DIST_DIR="${AUTOPG_DIST_DIR:-${REPO_ROOT}/dist}"
33
+ FALLBACK_ENABLED="${AUTOPG_BUILD_FALLBACK:-0}"
34
+
35
+ PLATFORMS=(linux-x64-glibc linux-x64-musl linux-arm64 darwin-x64 darwin-arm64)
36
+
37
+ # Map autopg platform tag → bun --target value.
38
+ bun_target_for() {
39
+ case "$1" in
40
+ linux-x64-glibc) echo "bun-linux-x64" ;;
41
+ linux-x64-musl) echo "bun-linux-x64-musl" ;;
42
+ linux-arm64) echo "bun-linux-arm64" ;;
43
+ darwin-x64) echo "bun-darwin-x64" ;;
44
+ darwin-arm64) echo "bun-darwin-arm64" ;;
45
+ *) return 1 ;;
46
+ esac
47
+ }
48
+
49
+ usage() {
50
+ cat <<EOF
51
+ Usage: $0 (--platform <p> | --all) [--version <v>] [--entry <path>]
52
+
53
+ Platforms: ${PLATFORMS[*]}
54
+
55
+ Environment:
56
+ AUTOPG_ENTRY_POINT Override entry file (default: bin/postgres-server.js)
57
+ AUTOPG_DIST_DIR Override output root (default: \$REPO/dist)
58
+ AUTOPG_BUILD_FALLBACK Set to 1 to retry failed bun builds via pkg/nexe
59
+ EOF
60
+ }
61
+
62
+ parse_args() {
63
+ TARGET_PLATFORM=""
64
+ BUILD_ALL=0
65
+ VERSION="${AUTOPG_VERSION:-}"
66
+
67
+ while [[ $# -gt 0 ]]; do
68
+ case "$1" in
69
+ --platform) TARGET_PLATFORM="$2"; shift 2 ;;
70
+ --all) BUILD_ALL=1; shift ;;
71
+ --version) VERSION="$2"; shift 2 ;;
72
+ --entry) ENTRY_POINT="$2"; shift 2 ;;
73
+ -h|--help) usage; exit 0 ;;
74
+ *) echo "unknown arg: $1" >&2; usage; exit 2 ;;
75
+ esac
76
+ done
77
+
78
+ if [[ "$BUILD_ALL" -eq 0 && -z "$TARGET_PLATFORM" ]]; then
79
+ echo "error: pass --platform <p> or --all" >&2
80
+ usage; exit 2
81
+ fi
82
+
83
+ if [[ -z "$VERSION" ]]; then
84
+ VERSION=$(node -p "require('${REPO_ROOT}/package.json').version" 2>/dev/null || echo "0.0.0")
85
+ fi
86
+ }
87
+
88
+ build_one() {
89
+ local platform="$1"
90
+ local target
91
+ target="$(bun_target_for "$platform")" || {
92
+ echo "error: unsupported platform: $platform" >&2
93
+ return 2
94
+ }
95
+
96
+ local out_dir="${DIST_DIR}/${platform}/autopg"
97
+ # `build_one` runs without `set -e` (called via `|| rc=$?` in main), so
98
+ # mkdir failures must propagate explicitly (gemini PR #84 HIGH review).
99
+ mkdir -p "$out_dir" || return 1
100
+ local outfile="${out_dir}/autopg"
101
+
102
+ echo "==> [${platform}] bun build --compile --target=${target}"
103
+ if bun build --compile \
104
+ --target="${target}" \
105
+ --define BUILD_VERSION="'${VERSION}'" \
106
+ "${REPO_ROOT}/${ENTRY_POINT}" \
107
+ --outfile "${outfile}" 2>&1 | tee -a "${DIST_DIR}/build.log"; then
108
+ echo " ✓ built: ${outfile}"
109
+ record_build "$platform" "bun" "ok"
110
+ return 0
111
+ fi
112
+
113
+ echo " ✗ bun build failed for ${platform}" >&2
114
+
115
+ if [[ "$FALLBACK_ENABLED" -eq 1 ]]; then
116
+ echo "==> [${platform}] retry via pkg/nexe (AUTOPG_BUILD_FALLBACK=1)"
117
+ if try_fallback "$platform" "$outfile"; then
118
+ record_build "$platform" "fallback" "ok"
119
+ return 0
120
+ fi
121
+ record_build "$platform" "fallback" "fail"
122
+ else
123
+ record_build "$platform" "bun" "fail"
124
+ fi
125
+
126
+ return 1
127
+ }
128
+
129
+ # Fallback: try pkg first, then nexe. Both consume the same entrypoint and
130
+ # emit a single executable. We only attempt the fallback when bun fails;
131
+ # this is per the distribution-exodus G1 contract.
132
+ try_fallback() {
133
+ local platform="$1"
134
+ local outfile="$2"
135
+
136
+ if command -v pkg >/dev/null 2>&1; then
137
+ echo " -> trying pkg"
138
+ local pkg_target
139
+ pkg_target="$(pkg_target_for "$platform")" || return 1
140
+ if pkg --target "$pkg_target" \
141
+ --output "$outfile" \
142
+ "${REPO_ROOT}/${ENTRY_POINT}" 2>&1 | tee -a "${DIST_DIR}/build.log"; then
143
+ echo " ✓ pkg succeeded"
144
+ return 0
145
+ fi
146
+ fi
147
+
148
+ if command -v nexe >/dev/null 2>&1; then
149
+ echo " -> trying nexe"
150
+ local nexe_target
151
+ nexe_target="$(nexe_target_for "$platform")" || return 1
152
+ if nexe --target "$nexe_target" \
153
+ --output "$outfile" \
154
+ "${REPO_ROOT}/${ENTRY_POINT}" 2>&1 | tee -a "${DIST_DIR}/build.log"; then
155
+ echo " ✓ nexe succeeded"
156
+ return 0
157
+ fi
158
+ fi
159
+
160
+ echo " ✗ no fallback worked (install pkg or nexe to enable)" >&2
161
+ return 1
162
+ }
163
+
164
+ pkg_target_for() {
165
+ case "$1" in
166
+ linux-x64-glibc) echo "node20-linux-x64" ;;
167
+ linux-x64-musl) echo "node20-linuxstatic-x64" ;;
168
+ linux-arm64) echo "node20-linux-arm64" ;;
169
+ darwin-x64) echo "node20-macos-x64" ;;
170
+ darwin-arm64) echo "node20-macos-arm64" ;;
171
+ *) return 1 ;;
172
+ esac
173
+ }
174
+
175
+ nexe_target_for() {
176
+ case "$1" in
177
+ linux-x64-glibc) echo "linux-x64-20.0.0" ;;
178
+ linux-x64-musl) echo "alpine-x64-20.0.0" ;;
179
+ linux-arm64) echo "linux-arm64-20.0.0" ;;
180
+ darwin-x64) echo "mac-x64-20.0.0" ;;
181
+ darwin-arm64) echo "mac-arm64-20.0.0" ;;
182
+ *) return 1 ;;
183
+ esac
184
+ }
185
+
186
+ record_build() {
187
+ local platform="$1" tool="$2" status="$3"
188
+ local rec="${DIST_DIR}/build-record.tsv"
189
+ mkdir -p "$DIST_DIR"
190
+ printf '%s\t%s\t%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$platform" "$tool" "$status" "$VERSION" >> "$rec"
191
+ }
192
+
193
+ main() {
194
+ parse_args "$@"
195
+ mkdir -p "$DIST_DIR"
196
+ : > "${DIST_DIR}/build.log"
197
+
198
+ local rc=0
199
+ if [[ "$BUILD_ALL" -eq 1 ]]; then
200
+ for p in "${PLATFORMS[@]}"; do
201
+ build_one "$p" || rc=$?
202
+ done
203
+ else
204
+ build_one "$TARGET_PLATFORM" || rc=$?
205
+ fi
206
+
207
+ if [[ $rc -ne 0 ]]; then
208
+ echo "error: at least one build target failed (see ${DIST_DIR}/build.log)" >&2
209
+ fi
210
+ exit $rc
211
+ }
212
+
213
+ main "$@"