freecode-sh 0.1.0-alpha.12 → 0.1.0-alpha.15

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,641 @@
1
+ #!/bin/sh
2
+
3
+ # Free Code native installer for macOS.
4
+ # Review this script before running it: https://freecode.sh/install
5
+
6
+ set -eu
7
+
8
+ DEFAULT_CHANNEL="alpha"
9
+ DEFAULT_DOWNLOAD_BASE_URL="https://releases.freecode.sh/releases"
10
+ DEFAULT_CHANNEL_BASE_URL="https://releases.freecode.sh/channels"
11
+ EXPECTED_TEAM_IDENTIFIER="GD597P8KJ2"
12
+ LEGACY_ALPHA6_MANIFEST_SHA256="1bf63e0d6114b12d61c07094936bd2606ae82ce87d5b054fbfb01c5f3d3646d8"
13
+ LAUNCHER_RELATIVE_PATH="bin/freecode"
14
+ INTERFACE_RELATIVE_PATH="libexec/freecode/freecode-tui"
15
+ ENGINE_RELATIVE_PATH="libexec/freecode/freecode-engine"
16
+
17
+ VERSION="${FREECODE_INSTALL_VERSION:-}"
18
+ CHANNEL="${FREECODE_INSTALL_CHANNEL:-$DEFAULT_CHANNEL}"
19
+ DOWNLOAD_BASE_URL="${FREECODE_INSTALL_BASE_URL:-$DEFAULT_DOWNLOAD_BASE_URL}"
20
+ CHANNEL_BASE_URL="${FREECODE_CHANNEL_BASE_URL:-$DEFAULT_CHANNEL_BASE_URL}"
21
+ INSTALL_ROOT="${FREECODE_INSTALL_ROOT:-${HOME:?HOME is required}/.local/share/freecode}"
22
+ BIN_DIR="${FREECODE_BIN_DIR:-${HOME}/.local/bin}"
23
+ OS_NAME="${FREECODE_INSTALL_OS:-$(uname -s)}"
24
+ MACHINE="${FREECODE_INSTALL_ARCH:-$(uname -m)}"
25
+ TEMP_DIR=""
26
+ STAGE_DIR=""
27
+ INSTALL_LOCK_PATH=""
28
+ INSTALL_LOCK_HELD="false"
29
+ INSTALL_LOCK_OWNER_TOKEN=""
30
+ INSTALL_LOCK_GUARD_HELD="false"
31
+ ALLOW_UNSIGNED_INSTALL="${FREECODE_ALLOW_UNSIGNED_INSTALL:-false}"
32
+ EMBEDDED_INSTALL="${FREECODE_INSTALL_EMBEDDED:-false}"
33
+ LAUNCH_AFTER_INSTALL="${FREECODE_INSTALL_LAUNCH:-true}"
34
+ RELEASE_MODE=""
35
+ PATH_PROFILE=""
36
+ PATH_CONFIGURED="false"
37
+
38
+ usage() {
39
+ cat <<'EOF'
40
+ Install Free Code's native macOS release.
41
+
42
+ Usage:
43
+ curl -fsSL https://freecode.sh/install.sh | sh
44
+ curl -fsSL https://freecode.sh/install.sh | sh -s -- --version 0.1.0-alpha.1
45
+
46
+ Options:
47
+ --version VERSION Install an exact immutable version.
48
+ --channel CHANNEL Resolve a channel (default: alpha).
49
+ --base-url URL Override the immutable release download base URL.
50
+ --channel-base-url URL Override the channel manifest base URL.
51
+ --install-root PATH Version store (default: ~/.local/share/freecode).
52
+ --bin-dir PATH Executable link directory (default: ~/.local/bin).
53
+ --no-launch Install without opening Free Code.
54
+ -h, --help Show this help.
55
+
56
+ Environment equivalents:
57
+ FREECODE_INSTALL_VERSION, FREECODE_INSTALL_CHANNEL,
58
+ FREECODE_INSTALL_BASE_URL, FREECODE_CHANNEL_BASE_URL,
59
+ FREECODE_INSTALL_ROOT, FREECODE_BIN_DIR, FREECODE_INSTALL_LAUNCH
60
+
61
+ Internal testing only:
62
+ FREECODE_ALLOW_UNSIGNED_INSTALL=true permits an explicitly marked unsigned
63
+ release outside the alpha channel. Never use this for a normal install.
64
+ EOF
65
+ }
66
+
67
+ fail() {
68
+ printf 'freecode installer: %s\n' "$*" >&2
69
+ exit 1
70
+ }
71
+
72
+ cleanup() {
73
+ if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then
74
+ rm -rf "$TEMP_DIR"
75
+ fi
76
+ if [ -n "$STAGE_DIR" ] && [ -d "$STAGE_DIR" ]; then
77
+ rm -rf "$STAGE_DIR"
78
+ fi
79
+ release_install_lock
80
+ }
81
+
82
+ require_value() {
83
+ [ "$#" -ge 2 ] || fail "$1 requires a value"
84
+ [ -n "$2" ] || fail "$1 requires a non-empty value"
85
+ }
86
+
87
+ release_install_lock() {
88
+ if [ "$INSTALL_LOCK_HELD" = "true" ]; then
89
+ lock_owner=$(sed -n '1p' "$INSTALL_LOCK_PATH" 2>/dev/null || true)
90
+ lock_owner_token=$(sed -n '2p' "$INSTALL_LOCK_PATH" 2>/dev/null || true)
91
+ if [ "$lock_owner" = "$$" ] && [ "$lock_owner_token" = "$INSTALL_LOCK_OWNER_TOKEN" ]; then
92
+ rm -f "$INSTALL_LOCK_PATH"
93
+ fi
94
+ fi
95
+ INSTALL_LOCK_HELD="false"
96
+ if [ "$INSTALL_LOCK_GUARD_HELD" = "true" ]; then
97
+ exec 9>&-
98
+ INSTALL_LOCK_GUARD_HELD="false"
99
+ fi
100
+ }
101
+
102
+ create_install_lock() {
103
+ (
104
+ umask 077
105
+ set -C
106
+ printf '%s\n%s\n' "$$" "$INSTALL_LOCK_OWNER_TOKEN" > "$INSTALL_LOCK_PATH"
107
+ ) 2>/dev/null
108
+ }
109
+
110
+ process_start_token() {
111
+ LC_ALL=C ps -o lstart= -p "$1" 2>/dev/null \
112
+ | sed -n '1{s/^[[:space:]]*//;s/[[:space:]]*$//;p;}'
113
+ }
114
+
115
+ acquire_install_lock() {
116
+ INSTALL_LOCK_PATH="$INSTALL_ROOT/.update-lock"
117
+ INSTALL_LOCK_OWNER_TOKEN=$(process_start_token "$$")
118
+ [ -n "$INSTALL_LOCK_OWNER_TOKEN" ] \
119
+ || fail "could not identify the installer process for update locking"
120
+ mkdir -p "$INSTALL_ROOT"
121
+ exec 9>"$INSTALL_LOCK_PATH.guard"
122
+ # Descriptor-mode locks remain attached to fd 9 after the helper exits and
123
+ # are held until release_install_lock closes the descriptor.
124
+ if [ -x /usr/bin/lockf ]; then
125
+ guard_acquired=false
126
+ /usr/bin/lockf -s -t 120 9 && guard_acquired=true
127
+ elif [ "$(uname -s)" = "Linux" ] && command -v flock >/dev/null 2>&1; then
128
+ # The Linux CI host emulates Darwin to exercise this macOS-only installer.
129
+ guard_acquired=false
130
+ flock -w 120 9 && guard_acquired=true
131
+ else
132
+ exec 9>&-
133
+ fail "the system lock utility is unavailable"
134
+ fi
135
+ if [ "$guard_acquired" != "true" ]; then
136
+ exec 9>&-
137
+ fail "another Free Code install or update is still in progress"
138
+ fi
139
+ INSTALL_LOCK_GUARD_HELD="true"
140
+ attempts=0
141
+ while ! create_install_lock; do
142
+ # Read both the regular-file protocol and the short-lived directory format
143
+ # used by development builds before the lock contract was unified.
144
+ if [ -d "$INSTALL_LOCK_PATH" ]; then
145
+ lock_owner=$(sed -n '1p' "$INSTALL_LOCK_PATH/pid" 2>/dev/null || true)
146
+ lock_owner_token=""
147
+ else
148
+ lock_owner=$(sed -n '1p' "$INSTALL_LOCK_PATH" 2>/dev/null || true)
149
+ lock_owner_token=$(sed -n '2p' "$INSTALL_LOCK_PATH" 2>/dev/null || true)
150
+ fi
151
+ case "$lock_owner" in
152
+ ''|*[!0-9]*) ;;
153
+ *)
154
+ if ! kill -0 "$lock_owner" 2>/dev/null; then
155
+ rm -rf "$INSTALL_LOCK_PATH"
156
+ continue
157
+ fi
158
+ if [ -n "$lock_owner_token" ]; then
159
+ current_owner_token=$(process_start_token "$lock_owner")
160
+ if [ -n "$current_owner_token" ] && [ "$current_owner_token" != "$lock_owner_token" ]; then
161
+ rm -rf "$INSTALL_LOCK_PATH"
162
+ continue
163
+ fi
164
+ fi
165
+ ;;
166
+ esac
167
+ attempts=$((attempts + 1))
168
+ [ "$attempts" -lt 120 ] \
169
+ || fail "another Free Code install or update is still in progress"
170
+ sleep 1
171
+ done
172
+ INSTALL_LOCK_HELD="true"
173
+
174
+ if [ -n "${FREECODE_TEST_HOLD_INSTALL_LOCK_SECONDS:-}" ]; then
175
+ case "$DOWNLOAD_BASE_URL" in
176
+ file://*) sleep "$FREECODE_TEST_HOLD_INSTALL_LOCK_SECONDS" ;;
177
+ *) fail "the test install delay is permitted only with local file URLs" ;;
178
+ esac
179
+ fi
180
+ }
181
+
182
+ configure_profile() {
183
+ profile=$1
184
+ path_line=$2
185
+ legacy_path_line=${3:-}
186
+ profile_dir=$(dirname "$profile")
187
+
188
+ [ -d "$profile_dir" ] || return 1
189
+ if [ -e "$profile" ] || [ -L "$profile" ]; then
190
+ [ -f "$profile" ] || return 1
191
+ fi
192
+ if grep -Fqx "$path_line" "$profile" 2>/dev/null \
193
+ || { [ -n "$legacy_path_line" ] && grep -Fqx "$legacy_path_line" "$profile" 2>/dev/null; }
194
+ then
195
+ return 0
196
+ fi
197
+ if [ -s "$profile" ] && ! printf '\n' >> "$profile"; then
198
+ return 1
199
+ fi
200
+ printf '# Free Code\n%s\n' "$path_line" >> "$profile"
201
+ }
202
+
203
+ configure_path() {
204
+ [ "$BIN_DIR" = "$HOME/.local/bin" ] || return 0
205
+
206
+ guarded_path_line='case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$HOME/.local/bin:$PATH" ;; esac'
207
+ legacy_path_line='export PATH="$HOME/.local/bin:$PATH"'
208
+ detected_shell=${SHELL:-/bin/zsh}
209
+
210
+ case "$detected_shell" in
211
+ */zsh)
212
+ zsh_config_dir=${ZDOTDIR:-$HOME}
213
+ if configure_profile "$zsh_config_dir/.zprofile" "$guarded_path_line" "$legacy_path_line" \
214
+ && configure_profile "$zsh_config_dir/.zshrc" "$guarded_path_line" "$legacy_path_line"
215
+ then
216
+ PATH_PROFILE="$zsh_config_dir/.zprofile and $zsh_config_dir/.zshrc"
217
+ PATH_CONFIGURED="true"
218
+ fi
219
+ ;;
220
+ */bash)
221
+ if [ -e "$HOME/.bash_profile" ] || [ -L "$HOME/.bash_profile" ]; then
222
+ bash_login_profile="$HOME/.bash_profile"
223
+ elif [ -e "$HOME/.bash_login" ] || [ -L "$HOME/.bash_login" ]; then
224
+ bash_login_profile="$HOME/.bash_login"
225
+ elif [ -e "$HOME/.profile" ] || [ -L "$HOME/.profile" ]; then
226
+ bash_login_profile="$HOME/.profile"
227
+ else
228
+ bash_login_profile="$HOME/.bash_profile"
229
+ fi
230
+ if configure_profile "$bash_login_profile" "$guarded_path_line" "$legacy_path_line" \
231
+ && configure_profile "$HOME/.bashrc" "$guarded_path_line" "$legacy_path_line"
232
+ then
233
+ PATH_PROFILE="$bash_login_profile and $HOME/.bashrc"
234
+ PATH_CONFIGURED="true"
235
+ fi
236
+ ;;
237
+ */fish)
238
+ fish_config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d"
239
+ if mkdir -p "$fish_config_dir" 2>/dev/null \
240
+ && configure_profile "$fish_config_dir/freecode.fish" \
241
+ 'contains -- "$HOME/.local/bin" $PATH; or set -gx PATH "$HOME/.local/bin" $PATH'
242
+ then
243
+ PATH_PROFILE="$fish_config_dir/freecode.fish"
244
+ PATH_CONFIGURED="true"
245
+ fi
246
+ ;;
247
+ esac
248
+ }
249
+
250
+ trap cleanup EXIT HUP INT TERM
251
+
252
+ while [ "$#" -gt 0 ]; do
253
+ case "$1" in
254
+ --version)
255
+ require_value "$@"
256
+ VERSION=$2
257
+ shift 2
258
+ ;;
259
+ --channel)
260
+ require_value "$@"
261
+ CHANNEL=$2
262
+ shift 2
263
+ ;;
264
+ --base-url)
265
+ require_value "$@"
266
+ DOWNLOAD_BASE_URL=$2
267
+ shift 2
268
+ ;;
269
+ --channel-base-url)
270
+ require_value "$@"
271
+ CHANNEL_BASE_URL=$2
272
+ shift 2
273
+ ;;
274
+ --install-root)
275
+ require_value "$@"
276
+ INSTALL_ROOT=$2
277
+ shift 2
278
+ ;;
279
+ --bin-dir)
280
+ require_value "$@"
281
+ BIN_DIR=$2
282
+ shift 2
283
+ ;;
284
+ --no-launch)
285
+ LAUNCH_AFTER_INSTALL="false"
286
+ shift
287
+ ;;
288
+ -h|--help)
289
+ usage
290
+ exit 0
291
+ ;;
292
+ --)
293
+ shift
294
+ break
295
+ ;;
296
+ *)
297
+ fail "unknown option: $1"
298
+ ;;
299
+ esac
300
+ done
301
+
302
+ [ "$#" -eq 0 ] || fail "unexpected positional arguments: $*"
303
+ command -v curl >/dev/null 2>&1 || fail "curl is required"
304
+ command -v tar >/dev/null 2>&1 || fail "tar is required"
305
+ command -v shasum >/dev/null 2>&1 || fail "shasum is required"
306
+
307
+ case "$ALLOW_UNSIGNED_INSTALL" in
308
+ true|false) ;;
309
+ *) fail "FREECODE_ALLOW_UNSIGNED_INSTALL must be exactly true or false" ;;
310
+ esac
311
+
312
+ case "$EMBEDDED_INSTALL" in
313
+ true|false) ;;
314
+ *) fail "FREECODE_INSTALL_EMBEDDED must be exactly true or false" ;;
315
+ esac
316
+
317
+ case "$LAUNCH_AFTER_INSTALL" in
318
+ true|false) ;;
319
+ *) fail "FREECODE_INSTALL_LAUNCH must be exactly true or false" ;;
320
+ esac
321
+
322
+ case "$OS_NAME" in
323
+ Darwin) ;;
324
+ *) fail "only macOS is supported right now (detected $OS_NAME)" ;;
325
+ esac
326
+
327
+ case "$MACHINE" in
328
+ arm64|aarch64)
329
+ ARCH="arm64"
330
+ ;;
331
+ x86_64|amd64)
332
+ # Prefer the native Apple Silicon build when this shell is running under Rosetta.
333
+ if [ "${FREECODE_INSTALL_ARCH:-}" = "" ] && [ "$(sysctl -in sysctl.proc_translated 2>/dev/null || true)" = "1" ]; then
334
+ ARCH="arm64"
335
+ else
336
+ ARCH="x64"
337
+ fi
338
+ ;;
339
+ *)
340
+ fail "unsupported Mac architecture: $MACHINE"
341
+ ;;
342
+ esac
343
+
344
+ validate_version() {
345
+ printf '%s\n' "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$' \
346
+ || fail "invalid release version: $1"
347
+ }
348
+
349
+ validate_channel() {
350
+ printf '%s\n' "$1" | grep -Eq '^[0-9A-Za-z][0-9A-Za-z._-]*$' \
351
+ || fail "invalid release channel: $1"
352
+ }
353
+
354
+ validate_release_security() {
355
+ manifest_path=$1
356
+ context=$2
357
+ release_mode=$(sed -n 's/.*"releaseMode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest_path" | head -n 1)
358
+ manifest_channel=$(sed -n 's/.*"channel"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest_path" | head -n 1)
359
+ case "$release_mode" in
360
+ signed-notarized)
361
+ RELEASE_MODE="signed-notarized"
362
+ ;;
363
+ unsigned-override)
364
+ if [ "$CHANNEL" = "alpha" ] && [ "$manifest_channel" = "alpha" ]; then
365
+ printf 'WARNING: %s is an unsigned Free Code alpha and is not Apple notarized. Its SHA-256 checksum will still be verified.\n' "$context" >&2
366
+ elif [ "$ALLOW_UNSIGNED_INSTALL" = "true" ]; then
367
+ printf 'WARNING: %s declares unsigned-override security metadata outside the alpha channel.\n' "$context" >&2
368
+ else
369
+ fail "$context is unsigned and is not an alpha release; refusing to install it"
370
+ fi
371
+ RELEASE_MODE="unsigned-override"
372
+ ;;
373
+ *) fail "$context does not contain valid signed-notarized release security metadata" ;;
374
+ esac
375
+ }
376
+
377
+ verify_release_signatures() {
378
+ root=$1
379
+ if [ "$RELEASE_MODE" = "unsigned-override" ]; then
380
+ printf 'WARNING: skipping Apple signature and Team ID verification for this explicitly marked unsigned release.\n' >&2
381
+ return
382
+ fi
383
+
384
+ [ -x /usr/bin/codesign ] || fail "/usr/bin/codesign is required to verify this release"
385
+ for executable in \
386
+ "$root/$LAUNCHER_RELATIVE_PATH" \
387
+ "$root/$INTERFACE_RELATIVE_PATH" \
388
+ "$root/$ENGINE_RELATIVE_PATH"
389
+ do
390
+ /usr/bin/codesign --verify --strict --verbose=2 "$executable" \
391
+ || fail "Apple signature verification failed for $executable"
392
+ SIGNATURE_DETAILS=$(/usr/bin/codesign --display --verbose=4 "$executable" 2>&1) \
393
+ || fail "could not inspect Apple signature for $executable"
394
+ TEAM_IDENTIFIER=$(printf '%s\n' "$SIGNATURE_DETAILS" | sed -n 's/^TeamIdentifier=//p' | head -n 1)
395
+ [ "$TEAM_IDENTIFIER" = "$EXPECTED_TEAM_IDENTIFIER" ] \
396
+ || fail "unexpected Apple TeamIdentifier for $executable (expected $EXPECTED_TEAM_IDENTIFIER, received ${TEAM_IDENTIFIER:-missing})"
397
+ done
398
+ }
399
+
400
+ write_release_public_key() {
401
+ destination=$1
402
+ if [ -n "${FREECODE_TEST_RELEASE_PUBLIC_KEY_FILE:-}" ]; then
403
+ case "$DOWNLOAD_BASE_URL" in
404
+ file://*) cp "$FREECODE_TEST_RELEASE_PUBLIC_KEY_FILE" "$destination" ;;
405
+ *) fail "the test release key override is permitted only with local file URLs" ;;
406
+ esac
407
+ return
408
+ fi
409
+ cat > "$destination" <<'EOF'
410
+ -----BEGIN PUBLIC KEY-----
411
+ MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA8so4sWArOV0GXihYvoSo
412
+ xDLI1yMoILnVfCjYQwe4CKQ9EC10JV30cntMJZWXvXZS6STqQGIjyWisa6Op4nId
413
+ OuXKDjaEcQIeftgZBzzyGrMmAMC3W57VAi3d+w/F2XnkXBaH3NOphfftF6Fxt3Q0
414
+ 2CxN3PjuuFaOGytKsYF5uDLCvEb+MpzJlD3HZoBzhhMCAoY6wmCdlgaDxD20UFdy
415
+ EigF5wwpcUWpYmbYMFp+6SLe/Fi8IrYirwiCv5eLur6PzFFcwFrJgUeZAdxmGuM8
416
+ eJzZlc7mK0+gzi8KpC2FZ9eOx1SafPE4An4+u8rZ7WA1Qk5eCAhNU9lm+JAeeKcC
417
+ XzhQpPUafybJg75dIbIayOC3tf5mn+X+Vl5+uY19nT2qcJsjgUeIcqbMs9FGXiIA
418
+ 2G292N5WppyiDcx8yAIG6rOrAUGzHMYCrhvxQLAx2KP5QfC53VZ3La7yZOATY5ic
419
+ F78qSSBrKDrT42vinbosbX++Wr34YB6pXhiZF6jHPHrxAgMBAAE=
420
+ -----END PUBLIC KEY-----
421
+ EOF
422
+ }
423
+
424
+ authenticate_release_manifest() {
425
+ manifest_path=$1
426
+ signature_path=$2
427
+ context=$3
428
+ if [ -s "$signature_path" ] && \
429
+ /usr/bin/openssl dgst -sha256 \
430
+ -verify "$RELEASE_PUBLIC_KEY" \
431
+ -signature "$signature_path" \
432
+ "$manifest_path" >/dev/null 2>&1
433
+ then
434
+ return
435
+ fi
436
+
437
+ manifest_sha=$(shasum -a 256 "$manifest_path" | awk '{ print $1 }')
438
+ if [ "$manifest_sha" = "$LEGACY_ALPHA6_MANIFEST_SHA256" ]; then
439
+ printf 'WARNING: %s predates release-manifest signatures; accepting its exact pinned alpha.6 manifest.\n' "$context" >&2
440
+ return
441
+ fi
442
+ fail "$context does not have a valid Free Code release signature"
443
+ }
444
+
445
+ manifest_asset_value() {
446
+ manifest_path=$1
447
+ target=$2
448
+ field=$3
449
+ awk -v target="$target" -v field="$field" '
450
+ index($0, "\"" target "\"") && index($0, ":") { in_target = 1; next }
451
+ in_target && index($0, "\"" field "\"") {
452
+ value = $0
453
+ sub(/^[^:]*:[[:space:]]*/, "", value)
454
+ sub(/,[[:space:]]*$/, "", value)
455
+ sub(/^"/, "", value)
456
+ sub(/"$/, "", value)
457
+ print value
458
+ exit
459
+ }
460
+ in_target && /^[[:space:]]*}[,]?[[:space:]]*$/ { exit }
461
+ ' "$manifest_path"
462
+ }
463
+
464
+ DOWNLOAD_BASE_URL=${DOWNLOAD_BASE_URL%/}
465
+ CHANNEL_BASE_URL=${CHANNEL_BASE_URL%/}
466
+ validate_channel "$CHANNEL"
467
+
468
+ TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/freecode-install.XXXXXX")
469
+ RELEASE_PUBLIC_KEY="$TEMP_DIR/release-signing-public.pem"
470
+ write_release_public_key "$RELEASE_PUBLIC_KEY"
471
+
472
+ if [ -z "$VERSION" ]; then
473
+ CHANNEL_MANIFEST="$TEMP_DIR/channel.json"
474
+ CHANNEL_SIGNATURE="$TEMP_DIR/channel.json.sig"
475
+ CHANNEL_URL="$CHANNEL_BASE_URL/$CHANNEL.json"
476
+ printf 'Resolving Free Code channel %s...\n' "$CHANNEL"
477
+ curl -fsSL --retry 3 --connect-timeout 15 "$CHANNEL_URL" -o "$CHANNEL_MANIFEST" \
478
+ || fail "could not download channel manifest: $CHANNEL_URL"
479
+ curl -fsSL --retry 3 --connect-timeout 15 "$CHANNEL_URL.sig" -o "$CHANNEL_SIGNATURE" 2>/dev/null \
480
+ || rm -f "$CHANNEL_SIGNATURE"
481
+ authenticate_release_manifest "$CHANNEL_MANIFEST" "$CHANNEL_SIGNATURE" "channel $CHANNEL"
482
+ VERSION=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$CHANNEL_MANIFEST" | head -n 1)
483
+ [ -n "$VERSION" ] || fail "channel manifest does not contain a version"
484
+ validate_release_security "$CHANNEL_MANIFEST" "channel $CHANNEL"
485
+ fi
486
+ validate_version "$VERSION"
487
+
488
+ mkdir -p "$INSTALL_ROOT"
489
+ acquire_install_lock
490
+
491
+ ASSET="freecode-darwin-$ARCH.tar.gz"
492
+ TAG="v$VERSION"
493
+ RELEASE_URL="$DOWNLOAD_BASE_URL/$TAG"
494
+ ARCHIVE="$TEMP_DIR/$ASSET"
495
+ RELEASE_MANIFEST="$TEMP_DIR/release-manifest.json"
496
+ RELEASE_SIGNATURE="$TEMP_DIR/release-manifest.json.sig"
497
+
498
+ curl -fsSL --retry 3 --connect-timeout 15 "$RELEASE_URL/release-manifest.json" -o "$RELEASE_MANIFEST" \
499
+ || fail "could not download release manifest: $RELEASE_URL/release-manifest.json"
500
+ curl -fsSL --retry 3 --connect-timeout 15 "$RELEASE_URL/release-manifest.json.sig" -o "$RELEASE_SIGNATURE" 2>/dev/null \
501
+ || rm -f "$RELEASE_SIGNATURE"
502
+ authenticate_release_manifest "$RELEASE_MANIFEST" "$RELEASE_SIGNATURE" "release $VERSION"
503
+
504
+ MANIFEST_VERSION=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$RELEASE_MANIFEST" | head -n 1)
505
+ [ "$MANIFEST_VERSION" = "$VERSION" ] \
506
+ || fail "release manifest version ${MANIFEST_VERSION:-missing} does not match requested version $VERSION"
507
+ validate_release_security "$RELEASE_MANIFEST" "release $VERSION"
508
+
509
+ MANIFEST_TARGET="darwin-$ARCH"
510
+ MANIFEST_ARCHIVE=$(manifest_asset_value "$RELEASE_MANIFEST" "$MANIFEST_TARGET" archive)
511
+ [ "$MANIFEST_ARCHIVE" = "$ASSET" ] \
512
+ || fail "release manifest does not declare the expected archive $ASSET"
513
+ EXPECTED_SHA=$(manifest_asset_value "$RELEASE_MANIFEST" "$MANIFEST_TARGET" sha256)
514
+ printf '%s\n' "$EXPECTED_SHA" | grep -Eq '^[0-9a-fA-F]{64}$' \
515
+ || fail "release checksum is not a SHA-256 digest"
516
+ EXPECTED_SIZE=$(manifest_asset_value "$RELEASE_MANIFEST" "$MANIFEST_TARGET" size)
517
+ printf '%s\n' "$EXPECTED_SIZE" | grep -Eq '^[1-9][0-9]*$' \
518
+ || fail "release manifest does not contain a valid archive size"
519
+
520
+ printf 'Downloading Free Code %s for darwin-%s...\n' "$VERSION" "$ARCH"
521
+ curl -fsSL --retry 3 --connect-timeout 15 "$RELEASE_URL/$ASSET" -o "$ARCHIVE" \
522
+ || fail "could not download release asset: $RELEASE_URL/$ASSET"
523
+ ACTUAL_SIZE=$(wc -c < "$ARCHIVE" | tr -d '[:space:]')
524
+ [ "$ACTUAL_SIZE" = "$EXPECTED_SIZE" ] \
525
+ || fail "download size verification failed for $ASSET"
526
+ ACTUAL_SHA=$(shasum -a 256 "$ARCHIVE" | awk '{ print $1 }')
527
+ [ "$ACTUAL_SHA" = "$EXPECTED_SHA" ] \
528
+ || fail "checksum verification failed for $ASSET"
529
+
530
+ if tar -tzf "$ARCHIVE" | grep -Eq '(^/|(^|/)\.\.(/|$))'; then
531
+ fail "release archive contains an unsafe path"
532
+ fi
533
+
534
+ mkdir -p "$INSTALL_ROOT/versions" "$BIN_DIR"
535
+ VERSION_DIR="$INSTALL_ROOT/versions/$VERSION"
536
+ NEW_INSTALL="false"
537
+
538
+ if [ ! -d "$VERSION_DIR" ]; then
539
+ STAGE_DIR=$(mktemp -d "$INSTALL_ROOT/.install-$VERSION.XXXXXX")
540
+ tar -xzf "$ARCHIVE" -C "$STAGE_DIR"
541
+ [ -x "$STAGE_DIR/$LAUNCHER_RELATIVE_PATH" ] \
542
+ || fail "release archive is missing executable $LAUNCHER_RELATIVE_PATH"
543
+ [ -x "$STAGE_DIR/$INTERFACE_RELATIVE_PATH" ] \
544
+ || fail "release archive is missing $INTERFACE_RELATIVE_PATH"
545
+ [ -x "$STAGE_DIR/$ENGINE_RELATIVE_PATH" ] \
546
+ || fail "release archive is missing $ENGINE_RELATIVE_PATH"
547
+ verify_release_signatures "$STAGE_DIR"
548
+ mv "$STAGE_DIR" "$VERSION_DIR"
549
+ STAGE_DIR=""
550
+ NEW_INSTALL="true"
551
+ else
552
+ [ -x "$VERSION_DIR/$LAUNCHER_RELATIVE_PATH" ] \
553
+ || fail "existing install is incomplete: $VERSION_DIR"
554
+ [ -x "$VERSION_DIR/$INTERFACE_RELATIVE_PATH" ] \
555
+ || fail "existing install is incomplete: $VERSION_DIR/$INTERFACE_RELATIVE_PATH"
556
+ [ -x "$VERSION_DIR/$ENGINE_RELATIVE_PATH" ] \
557
+ || fail "existing install is incomplete: $VERSION_DIR/$ENGINE_RELATIVE_PATH"
558
+ verify_release_signatures "$VERSION_DIR"
559
+ fi
560
+
561
+ PREVIOUS_LINK=""
562
+ if [ -L "$BIN_DIR/freecode" ]; then
563
+ PREVIOUS_LINK=$(readlink "$BIN_DIR/freecode")
564
+ elif [ -e "$BIN_DIR/freecode" ]; then
565
+ fail "refusing to replace non-symlink executable: $BIN_DIR/freecode"
566
+ fi
567
+ LINK_TEMP="$BIN_DIR/.freecode-link.$$"
568
+ rm -f "$LINK_TEMP"
569
+ ln -s "$VERSION_DIR/$LAUNCHER_RELATIVE_PATH" "$LINK_TEMP"
570
+ mv -f "$LINK_TEMP" "$BIN_DIR/freecode"
571
+
572
+ rollback_activation() {
573
+ rm -f "$BIN_DIR/freecode"
574
+ if [ -n "$PREVIOUS_LINK" ]; then
575
+ ln -s "$PREVIOUS_LINK" "$BIN_DIR/freecode"
576
+ fi
577
+ if [ "$NEW_INSTALL" = "true" ]; then
578
+ rm -rf "$VERSION_DIR"
579
+ fi
580
+ }
581
+
582
+ printf 'Verifying Free Code %s...\n' "$VERSION"
583
+ VERSION_OUTPUT=$("$BIN_DIR/freecode" --version 2>&1) || {
584
+ rollback_activation
585
+ fail "freecode --version failed; restored the previous install"
586
+ }
587
+ printf '%s\n' "$VERSION_OUTPUT"
588
+ [ "$(printf '%s\n' "$VERSION_OUTPUT" | sed -n '1p')" = "freecode-sh $VERSION" ] || {
589
+ rollback_activation
590
+ fail "freecode --version reported the wrong release; restored the previous install"
591
+ }
592
+ if ! "$BIN_DIR/freecode" doctor; then
593
+ rollback_activation
594
+ fail "freecode doctor failed; restored the previous install"
595
+ fi
596
+
597
+ rm -f "$INSTALL_ROOT/current-version"
598
+ printf '%s\n' "$BIN_DIR" > "$INSTALL_ROOT/bin-dir"
599
+ printf '%s\n' "$CHANNEL" > "$INSTALL_ROOT/channel"
600
+ printf '%s\n' "$DOWNLOAD_BASE_URL" > "$INSTALL_ROOT/release-base-url"
601
+ printf '%s\n' "$CHANNEL_BASE_URL" > "$INSTALL_ROOT/channel-base-url"
602
+
603
+ release_install_lock
604
+
605
+ printf '\nFree Code %s installed successfully.\n' "$VERSION"
606
+ printf 'Executable: %s/freecode\n' "$BIN_DIR"
607
+
608
+ if [ "$EMBEDDED_INSTALL" = "true" ]; then
609
+ exit 0
610
+ fi
611
+
612
+ configure_path
613
+
614
+ if [ "$PATH_CONFIGURED" = "true" ]; then
615
+ printf 'Added Free Code to your PATH in %s.\n' "$PATH_PROFILE"
616
+ else
617
+ printf '\nFree Code could not update your shell profile automatically.\n'
618
+ printf 'Add this line to your shell profile for future Terminal windows:\n'
619
+ printf ' export PATH="%s:$PATH"\n' "$BIN_DIR"
620
+ fi
621
+
622
+ printf '\nVerify the install:\n'
623
+ printf ' %s/freecode --version\n' "$BIN_DIR"
624
+ printf ' %s/freecode doctor\n' "$BIN_DIR"
625
+ printf '\nUninstall:\n'
626
+ printf ' rm -f "%s/freecode" && rm -rf "%s"\n' "$BIN_DIR" "$INSTALL_ROOT"
627
+
628
+ if [ "$LAUNCH_AFTER_INSTALL" = "true" ]; then
629
+ TTY_DEVICE="${FREECODE_TEST_TTY_FILE:-}"
630
+ if [ -z "$TTY_DEVICE" ] && [ -t 1 ]; then
631
+ TTY_DEVICE="/dev/tty"
632
+ fi
633
+ if [ -n "$TTY_DEVICE" ] && [ -r "$TTY_DEVICE" ]; then
634
+ printf '\nStarting Free Code...\n'
635
+ if ! "$BIN_DIR/freecode" < "$TTY_DEVICE"; then
636
+ printf '\nFree Code closed before startup finished. The installation is still ready.\n' >&2
637
+ fi
638
+ else
639
+ printf '\nFree Code is installed. Open a Terminal and run freecode when you are ready.\n'
640
+ fi
641
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freecode-sh",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.15",
4
4
  "description": "A free coding agent for your terminal.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Free Code",
@@ -21,37 +21,25 @@
21
21
  ],
22
22
  "type": "module",
23
23
  "bin": {
24
- "freecode": "./freecode.mjs"
24
+ "freecode": "./freecode.mjs",
25
+ "freecode-sh": "./freecode.mjs"
25
26
  },
26
27
  "files": [
27
28
  "freecode.mjs",
28
- "standalone.tsx",
29
- "engine.manifest.json",
30
- "release-manifest.json",
31
- "README.md",
32
- "THIRD_PARTY_NOTICES.md"
29
+ "install.sh"
33
30
  ],
34
31
  "engines": {
35
32
  "node": ">=20.9.0"
36
33
  },
37
34
  "os": [
38
- "darwin",
39
- "linux",
40
- "win32"
35
+ "darwin"
36
+ ],
37
+ "cpu": [
38
+ "arm64",
39
+ "x64"
41
40
  ],
42
41
  "publishConfig": {
43
42
  "access": "public",
44
- "tag": "alpha"
45
- },
46
- "dependencies": {
47
- "@opencode-ai/sdk": "1.17.11",
48
- "@opentui/core": "0.4.5",
49
- "@opentui/keymap": "0.4.5",
50
- "@opentui/solid": "0.4.5",
51
- "bun": "1.3.14",
52
- "diff": "9.0.0",
53
- "opencode-ai": "1.17.11",
54
- "solid-js": "1.9.12",
55
- "ws": "8.21.0"
43
+ "tag": "latest"
56
44
  }
57
45
  }