Repository navigation
Expand file tree
/
Copy pathinstall
More file actions
executable file
·964 lines (840 loc) · 32.3 KB
/
Copy pathinstall
File metadata and controls
executable file
·964 lines (840 loc) · 32.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
#!/usr/bin/env bash
# #156:管道与未定义变量失败即退出,禁止静默半安装
set -euo pipefail
# #156:应用名固定 opencode,资产前缀与 APP 绑定
APP=opencode
# #156:默认 SMARK2022/opencode,可用 OPENCODE_REPO 覆盖
# #156:环境变量而非 flag:CI 可全局注入,无需改每条命令
REPO=${OPENCODE_REPO:-SMARK2022/opencode}
# #156:颜色仅装饰,不改变控制流
MUTED='\033[0;2m'
RED='\033[0;31m'
# #156:ORANGE 而非 YELLOW:多数终端主题下黄色与前景难分
ORANGE='\033[38;5;214m'
# #156:颜色复位,避免污染后续输出
NC='\033[0m'
# #156:帮助文案与 CLI/环境变量契约同步
usage() {
cat <<EOF_USAGE
OpenCode Installer
Usage: install [options]
Options:
-h, --help Display this help message
-v, --version <version> Install a specific version, for example 1.14.41-smark or v1.14.41-smark
-b, --binary <path> Install from a local binary instead of downloading
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
--path-profile <path>
Add opencode to this shell config file. Can be repeated.
--all-shell-profiles
Add opencode to all existing supported shell config files.
--interactive Ask which shell config files to update.
--allow-sudo Allow running under sudo. Not needed for user installs.
Environment variables:
OPENCODE_REPO GitHub repo to download from. Default: SMARK2022/opencode
OPENCODE_INSTALL_DIR Install directory. Default: ~/.opencode/bin
XDG_BIN_DIR Used as install directory when OPENCODE_INSTALL_DIR is not set
VERSION Same as --version
By default, the installer updates every existing supported shell profile it finds.
Examples:
curl -fsSL https://github.com/SMARK2022/opencode/releases/latest/download/install | bash
curl -fsSL https://github.com/SMARK2022/opencode/releases/latest/download/install | bash -s -- --version 1.14.41-smark
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://github.com/SMARK2022/opencode/releases/latest/download/install -o /tmp/opencode-install && env OPENCODE_INSTALL_DIR=/usr/local/bin bash /tmp/opencode-install
./install --binary /path/to/opencode --path-profile "$HOME/.bash_profile"
EOF_USAGE
}
# #156:统一消息通道,避免错误文案格式漂移
print_message() {
local level=$1
local message=$2
local color=""
case $level in
info) color="${NC}" ;;
warning) color="${ORANGE}" ;;
error) color="${RED}" ;;
esac
echo -e "${color}${message}${NC}"
}
# #156:必需外部命令缺失 fail-closed
need_cmd() {
# #156:command -v 而非 which:跨 busybox/POSIX 行为一致且不产生额外输出
if ! command -v "$1" >/dev/null 2>&1; then
print_message error "Error: '$1' is required but not installed."
# #156:不可恢复错误统一非零退出
exit 1
fi
}
# #156:跨平台 sha256,供 checksums 比对
sha256_file() {
# #156:sha256sum 在主流发行版必装;两者都缺时返回空串,由调用方软跳过校验
if command -v sha256sum >/dev/null 2>&1; then
# #156:awk 只取首字段:行尾是文件名,不能整行当哈希比对
sha256sum "$1" | awk '{print $1}'
# #156:macOS 无 GNU coreutils 时 shasum -a 256 与 sha256 输出等价
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{print $1}'
else
echo ""
fi
}
requested_version=${VERSION:-}
no_modify_path=false
binary_path=""
# #167:显式 profile 列表(可重复 --path-profile)
path_profiles=()
# #167:是否强制 all-shell 扫描
all_shell_profiles_requested=false
# #167:交互选择 profile
interactive_path=false
# #167:sudo 显式放行开关
allow_sudo=false
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--version)
if [[ -n "${2:-}" ]]; then
requested_version="$2"
shift 2
else
print_message error "Error: --version requires a version argument"
exit 1
fi
;;
-b|--binary)
if [[ -n "${2:-}" ]]; then
binary_path="$2"
shift 2
else
print_message error "Error: --binary requires a path argument"
exit 1
fi
;;
--no-modify-path)
no_modify_path=true
shift
;;
# #167:收集显式 profile 路径
--path-profile)
if [[ -n "${2:-}" ]]; then
path_profiles+=("$2")
shift 2
else
print_message error "Error: --path-profile requires a path argument"
exit 1
fi
;;
--all-shell-profiles)
all_shell_profiles_requested=true
shift
;;
--interactive)
interactive_path=true
shift
;;
--allow-sudo)
allow_sudo=true
shift
;;
*)
print_message warning "Warning: Unknown option '$1'"
shift
;;
esac
done
requested_version="${requested_version#v}"
# This installer only installs OpenCode 1. OpenCode 1 clients run it with VERSION set when upgrading,
# so refusing 2.x also keeps older clients from replacing themselves with OpenCode 2 by accident.
if [[ "$requested_version" =~ ^([0-9]+)\. ]] && [ "${BASH_REMATCH[1]}" -ge 2 ]; then
echo -e "${RED}Error: OpenCode ${requested_version} is not an OpenCode 1 release, so this installer can't install it.${NC}"
echo -e "${MUTED}To install OpenCode 2, run: ${NC}curl -fsSL https://opencode.ai/v2/install | bash"
echo -e "${MUTED}To upgrade an existing OpenCode 1 install, run: ${NC}opencode upgrade --major"
echo -e "${MUTED}Docs: https://opencode.ai/v2/docs${NC}"
exit 1
fi
# #156:安装目录 OPENCODE_INSTALL_DIR > XDG_BIN_DIR > 默认
resolve_install_dir() {
if [ -n "${OPENCODE_INSTALL_DIR:-}" ]; then
echo "$OPENCODE_INSTALL_DIR"
elif [ -n "${XDG_BIN_DIR:-}" ]; then
echo "$XDG_BIN_DIR"
else
echo "$HOME/.opencode/bin"
fi
}
INSTALL_DIR=$(resolve_install_dir)
# #167:uid=0 且带 SUDO_USER 时默认拒绝,避免 root 改用户 profile
check_sudo() {
local uid
uid=$(id -u 2>/dev/null || echo "")
if [ "$uid" != "0" ] || [ -z "${SUDO_USER:-}" ]; then
return 0
fi
if [ "$allow_sudo" != "true" ]; then
print_message error "Error: refusing to run under sudo by default."
print_message error "Re-run without sudo for user installs, or pass --allow-sudo with an explicit OPENCODE_INSTALL_DIR for system installs."
exit 1
fi
# #167:sudo 放行但未显式 profile 时强制跳过 PATH 改写,防止 root 写用户 rc
if [ "$no_modify_path" != "true" ] && [ "${#path_profiles[@]}" -eq 0 ]; then
no_modify_path=true
print_message warning "Running under sudo; skipping shell profile modification. Use --path-profile to explicitly choose a user profile."
fi
}
check_sudo
# #156:mkdir -p 幂等:已存在时保留既有安装不中断;#167:mkdir 失败 fail-closed
if ! mkdir -p "$INSTALL_DIR"; then
print_message error "Error: cannot create install directory: $INSTALL_DIR"
exit 1
fi
# #167:目录不可写直接失败,提示 OPENCODE_INSTALL_DIR / --allow-sudo
if [ ! -w "$INSTALL_DIR" ]; then
print_message error "Error: install directory is not writable: $INSTALL_DIR"
print_message error "Choose a writable OPENCODE_INSTALL_DIR, or explicitly run with sudo and --allow-sudo for system directories."
exit 1
fi
raw_os=""
os=""
arch=""
filename=""
url=""
specific_version=""
asset_kind="archive"
exe_name="opencode"
if [ -n "$binary_path" ]; then
if [ ! -f "$binary_path" ]; then
print_message error "Error: Binary not found at ${binary_path}"
exit 1
fi
specific_version="local"
else
need_cmd curl
# curl 无 wget 回退:探测与下载单一路径,失败语义可预测
# #156:OS 探测 fail-closed,未知系统退出
raw_os=$(uname -s)
case "$raw_os" in
Darwin*) os="darwin" ;;
Linux*) os="linux" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
*)
print_message error "Unsupported OS: $raw_os"
exit 1
;;
esac
# #156:CPU 架构探测映射到 release 资产后缀
arch=$(uname -m)
case "$arch" in
# #156:arch 映射 arm64/x64 必须与 workflow 资产名一致
aarch64|arm64) arch="arm64" ;;
# #156:x64 命名不用 amd64 后缀,贴合 release 文件名
x86_64|amd64) arch="x64" ;;
*)
print_message error "Unsupported architecture: $arch"
exit 1
;;
esac
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
if [ "$rosetta_flag" = "1" ]; then
arch="arm64"
fi
fi
combo="$os-$arch"
case "$combo" in
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
;;
*)
print_message error "Unsupported OS/Arch: $os/$arch"
exit 1
;;
esac
if [ -z "$requested_version" ]; then
# #156:release 下载 URL 与 tag/version 对齐
# 最新发布可能为2.x;同一仓库只选1.x,并让版本与下载资产固定到同一tag。
specific_version=$(curl -fsSL --max-time 30 "https://api.github.com/repos/${REPO}/releases?per_page=100" | sed -n 's/.*"tag_name": *"v\{0,1\}\(1\.[0-9][0-9]*\.[0-9][0-9]*[^"]*\)".*/\1/p' | sed -n '1p')
if [ -z "$specific_version" ]; then
print_message error "Failed to fetch latest version information from ${REPO}"
exit 1
fi
base_url="https://github.com/${REPO}/releases/download/v${specific_version}"
else
requested_version="${requested_version#v}"
base_url="https://github.com/${REPO}/releases/download/v${requested_version}"
specific_version="$requested_version"
# #156:HTTP 非 2xx / 404 驱动候选切换
# #156:-w 与 -o 分离:只取状态码不落盘,不污染安装目录
http_status=$(curl -sI -L -o /dev/null -w "%{http_code}" "https://github.com/${REPO}/releases/tag/v${requested_version}")
if [ "$http_status" = "404" ]; then
print_message error "Error: Release v${requested_version} not found"
print_message info "${MUTED}Available releases: https://github.com/${REPO}/releases${NC}"
exit 1
fi
fi
# #156:os-arch 与 workflow 资产后缀同源,双方改名必须同步
target="$os-$arch"
# Keep installer target names exactly aligned with the workflow assets.
# Do not append baseline/musl unless the workflow also publishes those assets.
# #156:先固定名再 versioned 旧名,避免 404
candidates=()
if [ "$os" = "linux" ]; then
candidates+=("${APP}-${target}.tar.gz")
candidates+=("${APP}-${target}-${specific_version}.tar.gz")
elif [ "$os" = "darwin" ]; then
candidates+=("${APP}-${target}.zip")
candidates+=("${APP}-${target}-${specific_version}.zip")
candidates+=("${APP}-${target}-${specific_version}.tar.gz")
else
candidates+=("${APP}-${target}.zip")
candidates+=("${APP}-${target}-${specific_version}.zip")
candidates+=("${APP}-${target}.exe")
candidates+=("${APP}-${target}-${specific_version}.exe")
fi
tmp_probe="${TMPDIR:-/tmp}/opencode_probe_$$"
mkdir -p "$tmp_probe"
filename=""
for candidate in "${candidates[@]}"; do
candidate_url="${base_url}/${candidate}"
http_status=$(curl -sI -L -o /dev/null -w "%{http_code}" "$candidate_url" || true)
if [ "$http_status" = "200" ] || [ "$http_status" = "302" ]; then
filename="$candidate"
url="$candidate_url"
break
fi
done
# #156:清理临时目录,避免残留半下载
rm -rf "$tmp_probe"
if [ -z "$filename" ]; then
print_message error "No compatible release asset found for ${combo}. Tried:"
for candidate in "${candidates[@]}"; do
print_message error " ${base_url}/${candidate}"
done
exit 1
fi
case "$filename" in
*.tar.gz)
need_cmd tar
asset_kind="tar.gz"
;;
*.zip)
need_cmd unzip
asset_kind="zip"
;;
*.exe)
asset_kind="exe"
;;
*)
print_message error "Unsupported asset format: $filename"
exit 1
;;
esac
fi
# #167:以目标安装目录为主;PATH 上同版本仅警告,不再 exit 0 跳过安装
check_version() {
local target_name="opencode"
if [ "${os:-}" = "windows" ]; then
target_name="opencode.exe"
fi
local target_bin="${INSTALL_DIR}/${target_name}"
local installed_version=""
local existing_path=""
local existing_version=""
# #167:目标已有同版本 → 重装刷新,禁止静默跳过
if [ -x "$target_bin" ]; then
installed_version=$("$target_bin" --version 2>/dev/null || echo "")
if [[ "$installed_version" == "$specific_version" ]]; then
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already exists at ${NC}$target_bin${MUTED}; reinstalling to refresh it."
else
print_message info "${MUTED}Installed target version: ${NC}$installed_version ${MUTED}at ${NC}$target_bin"
fi
fi
if command -v "$target_name" >/dev/null 2>&1; then
existing_path=$(command -v "$target_name")
elif [ "$target_name" = "opencode" ] && command -v opencode.exe >/dev/null 2>&1; then
existing_path=$(command -v opencode.exe)
fi
# #167:PATH 上别处同/异版本只警告,仍安装到 OPENCODE_INSTALL_DIR
if [ -n "$existing_path" ] && [ "$existing_path" != "$target_bin" ]; then
existing_version=$("$existing_path" --version 2>/dev/null || echo "")
print_message warning "${MUTED}Found existing opencode at ${NC}$existing_path${MUTED}, version: ${NC}$existing_version"
print_message warning "${MUTED}Will still install requested version to ${NC}$target_bin"
fi
}
unbuffered_sed() {
if echo | sed -u -e "" >/dev/null 2>&1; then
sed -nu "$@"
elif echo | sed -l -e "" >/dev/null 2>&1; then
sed -nl "$@"
else
local pad
pad="$(printf "\n%512s" "")"
sed -ne "s/$/\\${pad}/" "$@"
fi
}
print_progress() {
local bytes="$1"
local length="$2"
[ "$length" -gt 0 ] || return 0
local width=50
local percent=$(( bytes * 100 / length ))
[ "$percent" -gt 100 ] && percent=100
local on=$(( percent * width / 100 ))
local off=$(( width - on ))
local filled
local empty
filled=$(printf "%*s" "$on" "")
filled=${filled// /■}
empty=$(printf "%*s" "$off" "")
empty=${empty// /・}
printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
}
download_with_progress() {
local download_url="$1"
local output="$2"
if [ -t 2 ]; then
exec 4>&2
else
exec 4>/dev/null
fi
# #156:TMPDIR+PID 后缀隔离:不依赖 mktemp 可用性,$$ 天然唯一
local tmp_dir=${TMPDIR:-/tmp}
local basename="${tmp_dir}/opencode_install_$$"
local tracefile="${basename}.trace"
rm -f "$tracefile"
mkfifo "$tracefile"
printf "\033[?25l" >&4
# RETURN trap:函数退出即恢复光标与关闭 fd,trace 不跨调用残留
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
(
curl --trace-ascii "$tracefile" -s -L -o "$output" "$download_url"
) &
local curl_pid=$!
unbuffered_sed \
-e 'y/ACDEGHLNORTV/acdeghlnortv/' \
-e '/^0000: content-length:/p' \
-e '/^<= recv data/p' \
"$tracefile" | \
{
local length=0
local bytes=0
while IFS=" " read -r -a line; do
[ "${#line[@]}" -lt 2 ] && continue
local tag="${line[0]} ${line[1]}"
if [ "$tag" = "0000: content-length:" ]; then
length="${line[2]}"
length=$(echo "$length" | tr -d '\r')
bytes=0
elif [ "$tag" = "<= recv" ]; then
local size="${line[3]}"
bytes=$(( bytes + size ))
if [ "$length" -gt 0 ]; then
print_progress "$bytes" "$length"
fi
fi
done
}
wait $curl_pid
local ret=$?
echo "" >&4
return $ret
}
# #156:有 checksums 则硬校验,缺文件/工具 soft-warn
verify_checksum_if_possible() {
local file_path="$1"
local asset_name="$2"
local tmp_dir="$3"
local actual
actual=$(sha256_file "$file_path")
if [ -z "$actual" ]; then
print_message warning "No sha256 tool found; skipping checksum verification."
return 0
fi
# #156:checksums 与二进制同 release 批次
local checksums_file="${tmp_dir}/checksums.txt"
if ! curl -fsSL "${base_url}/checksums.txt" -o "$checksums_file"; then
print_message warning "checksums.txt not found; local sha256: $actual"
return 0
fi
local expected
# #156:按资产名精确匹配行,避免子串误中其它平台条目
expected=$(grep -E "[[:space:]]${asset_name}$" "$checksums_file" | awk '{print $1}' | head -n 1 || true)
if [ -z "$expected" ]; then
print_message warning "No checksum entry for ${asset_name}; local sha256: $actual"
return 0
fi
if [ "$expected" != "$actual" ]; then
print_message error "Checksum mismatch for ${asset_name}"
print_message error "Expected: $expected"
print_message error "Actual: $actual"
exit 1
fi
print_message info "${MUTED}Checksum verified: ${NC}$actual"
}
# #156:原子安装并 chmod,避免半写可执行文件
install_executable() {
local src="$1"
local dst_name="$2"
local dst="${INSTALL_DIR}/${dst_name}"
# 原子替换:先把新二进制写进同目录临时文件,再 rename 覆盖目标。
# 运行中的 daemon 仍映射旧 inode,避免 macOS 内核对被覆盖 Mach-O 做
# cs_invalid_page 校验失败而 SIGKILL 进程;升级后紧接的 --version 验证
# 也因此能映射到完整有效的新二进制,不再卡在 _dyld_start。Windows 独占
# 锁定运行中的 .exe,rename 会失败——退回原地覆盖(仅在该 .exe 未被持有时
# 可行;被持有时 cp 同样失败,属既有行为,Windows 无代码签名页失效问题)。
# 临时文件与目标同目录保证 mv 是同文件系统纯 rename。
local tmp
tmp=$(mktemp "${dst}.install.XXXXXX") || { cp "$src" "$dst" && chmod 755 "$dst"; return; }
# 清理上一轮崩溃可能残留的同前缀临时文件,避免目录堆积。
rm -f "${dst}.install."* 2>/dev/null || true
cp "$src" "$tmp"
chmod 755 "$tmp"
if ! mv -f "$tmp" "$dst" 2>/dev/null; then
# rename 失败(Windows 运行中 .exe 被独占锁,或跨文件系统)退回原地覆盖。
rm -f "$tmp"
cp "$src" "$dst"
chmod 755 "$dst"
fi
if [ "${os:-}" = "darwin" ] && command -v xattr >/dev/null 2>&1; then
# #156:清除 mac quarantine,避免 Gatekeeper 拦截
xattr -d com.apple.quarantine "$dst" 2>/dev/null || true
fi
}
# #156:下载→校验→安装串联,任一步失败即停
download_and_install() {
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
print_message info "${MUTED}Repository: ${NC}${REPO}"
print_message info "${MUTED}Asset: ${NC}${filename}"
print_message info "${MUTED}Install dir: ${NC}${INSTALL_DIR}"
local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
mkdir -p "$tmp_dir"
local asset_path="$tmp_dir/$filename"
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$asset_path"; then
curl -# -L -o "$asset_path" "$url"
fi
verify_checksum_if_possible "$asset_path" "$filename" "$tmp_dir"
if [ "$asset_kind" = "tar.gz" ]; then
# #156:tar.gz 解包到临时目录再定位二进制
tar -xzf "$asset_path" -C "$tmp_dir"
elif [ "$asset_kind" = "zip" ]; then
unzip -q "$asset_path" -d "$tmp_dir"
fi
if [ "$asset_kind" = "exe" ]; then
install_executable "$asset_path" "opencode.exe"
else
local extracted=""
if [ "$os" = "windows" ]; then
# #156:解包产物可能嵌套目录,find 按名定位避免猜路径
extracted=$(find "$tmp_dir" -type f -name "opencode.exe" | head -n 1 || true)
exe_name="opencode.exe"
else
extracted=$(find "$tmp_dir" -type f -name "opencode" | head -n 1 || true)
exe_name="opencode"
fi
if [ -z "$extracted" ]; then
print_message error "Cannot find executable in downloaded asset: $filename"
find "$tmp_dir" -maxdepth 4 -type f >&2 || true
exit 1
fi
install_executable "$extracted" "$exe_name"
fi
rm -rf "$tmp_dir"
}
# #156:本地 --binary 旁路网络下载
install_from_binary() {
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}from: ${NC}$binary_path"
local base
base=$(basename "$binary_path")
if [[ "$base" == *.exe ]]; then
install_executable "$binary_path" "opencode.exe"
else
install_executable "$binary_path" "opencode"
fi
}
if [ -n "$binary_path" ]; then
install_from_binary
else
check_version
download_and_install
fi
# #156:可选写 shell rc;#167:多 profile / 去重 / 可创建缺失文件
add_to_path() {
# #167:写入失败不得中断已成功的二进制安装
# #167:命令行使用引号路径,避免空格目录破坏 rc
local config_file=$1
local command=$2
local config_dir
# #167:profile 不存在时尽量创建,失败则只打印手工指引
if [ ! -e "$config_file" ]; then
config_dir=$(dirname "$config_file")
if ! mkdir -p "$config_dir" || ! touch "$config_file"; then
print_message warning "Manually add the directory to $config_file or similar:"
print_message info " $command"
return 0
fi
fi
# #167:按 install dir 去重(含 $HOME/ 与 ~/ 写法),避免重复 PATH 行
if profile_has_install_dir "$config_file"; then
print_message info "opencode install directory already exists in $config_file, skipping write."
elif [[ -w $config_file ]]; then
echo -e "\n# opencode" >> "$config_file"
echo "$command" >> "$config_file"
print_message info "${MUTED}Successfully added ${NC}opencode ${MUTED}to \$PATH in ${NC}$config_file"
else
print_message warning "Manually add the directory to $config_file or similar:"
print_message info " $command"
fi
}
# #167:识别字面路径 / $HOME 相对 / ~ 相对是否已写入
profile_has_install_dir() {
# #167:去重必须覆盖三种写法,否则默认全量扫描会双写
# #167:文件不存在视为未写入,允许后续 touch 创建
local config_file=$1
local home_relative=""
local tilde_relative=""
[ -f "$config_file" ] || return 1
if profile_has_path_line "$config_file" "$INSTALL_DIR"; then
return 0
fi
if [[ "$INSTALL_DIR" == "$HOME/"* ]]; then
home_relative="\$HOME/${INSTALL_DIR#"$HOME/"}"
tilde_relative="~/${INSTALL_DIR#"$HOME/"}"
if profile_has_path_line "$config_file" "$home_relative" || profile_has_path_line "$config_file" "$tilde_relative"; then
return 0
fi
fi
return 1
}
# #167:只认 PATH=/export PATH=/fish_add_path 行上的 install dir 片段
profile_has_path_line() {
# #167:排除注释/普通文本中偶然出现的目录字符串
# #167:fish_add_path 与 export PATH 共用同一去重入口
local config_file=$1
local install_dir=$2
grep -F "$install_dir" "$config_file" 2>/dev/null | grep -E '(^|[[:space:]])(export[[:space:]]+PATH=|PATH=|fish_add_path)' >/dev/null 2>&1
}
# #167:profile_targets 去重追加
append_profile() {
# #167:同一 run 内目标列表必须稳定去重
# #167:保持用户指定顺序,先 explicit 后扫描
local file=$1
local existing
if [ "${#profile_targets[@]}" -gt 0 ]; then
for existing in "${profile_targets[@]}"; do
if [ "$existing" = "$file" ]; then
return 0
fi
done
fi
profile_targets+=("$file")
}
# #167:展开 ~/ 为 $HOME,供 --path-profile
normalize_profile_path() {
# #167:仅展开 tilde,不解析其他 shell 扩展,避免意外求值
# #167:相对路径保持原样交给调用方
case "$1" in
"~")
echo "$HOME"
;;
"~/"*)
echo "$HOME/${1#"~/"}"
;;
*)
echo "$1"
;;
esac
}
# #167:按 shell 列出候选 profile;ash/sh 不再写 /etc/profile
append_shell_profile_candidates() {
# #167:zsh 增加 .zprofile,覆盖登录 shell 场景
# #167:故意不写 /etc/profile,避免系统级污染
local shell_name=$1
case $shell_name in
fish)
append_candidate "$HOME/.config/fish/config.fish"
;;
zsh)
append_candidate "${ZDOTDIR:-$HOME}/.zshrc"
append_candidate "${ZDOTDIR:-$HOME}/.zprofile"
append_candidate "${ZDOTDIR:-$HOME}/.zshenv"
append_candidate "$XDG_CONFIG_HOME/zsh/.zshrc"
append_candidate "$XDG_CONFIG_HOME/zsh/.zprofile"
append_candidate "$XDG_CONFIG_HOME/zsh/.zshenv"
;;
bash)
append_candidate "$HOME/.bashrc"
append_candidate "$HOME/.bash_profile"
append_candidate "$HOME/.profile"
append_candidate "$XDG_CONFIG_HOME/bash/.bashrc"
append_candidate "$XDG_CONFIG_HOME/bash/.bash_profile"
;;
ash|sh)
append_candidate "$HOME/.ashrc"
append_candidate "$HOME/.profile"
;;
*)
append_shell_profile_candidates bash
;;
esac
}
# #167:候选列表去重
append_candidate() {
# #167:候选去重与目标去重分离,避免扫描阶段丢文件
local file=$1
local existing
if [ "${#profile_candidates[@]}" -gt 0 ]; then
for existing in "${profile_candidates[@]}"; do
if [ "$existing" = "$file" ]; then
return 0
fi
done
fi
profile_candidates+=("$file")
}
# #167:默认扫描 bash/zsh/fish/sh 全部候选
append_all_shell_profile_candidates() {
# #167:顺序 bash→zsh→fish→sh,稳定可测
# #167:覆盖跨 shell 用户(bash 登录但日常 zsh)
append_shell_profile_candidates bash
append_shell_profile_candidates zsh
append_shell_profile_candidates fish
append_shell_profile_candidates sh
}
# #167:无已存在 profile 时按当前 shell 创建默认目标
select_default_profile() {
# #167:优先已存在文件,减少凭空创建
# #167:最终 fallback 按当前 SHELL 选一个可创建路径
local file
for file in "${profile_candidates[@]}"; do
if [ -f "$file" ]; then
append_profile "$file"
return 0
fi
done
case $current_shell in
fish)
append_profile "$HOME/.config/fish/config.fish"
;;
zsh)
append_profile "${ZDOTDIR:-$HOME}/.zshrc"
;;
bash)
append_profile "$HOME/.bashrc"
;;
*)
append_profile "$HOME/.profile"
;;
esac
}
# #167:选择所有已存在候选;若无则 fallback 默认
select_existing_profiles() {
# #167:默认策略核心:所有已存在候选都写
# #167:空结果才 fallback,避免“静默不写 PATH”
local file
for file in "${profile_candidates[@]}"; do
if [ -f "$file" ]; then
append_profile "$file"
fi
done
if [ "${#profile_targets[@]}" -eq 0 ]; then
select_default_profile
fi
}
# #167:/dev/tty 交互;无 TTY 返回失败由调用方回退
select_interactive_profiles() {
# #167:读写 /dev/tty,不与管道 stdin 争用
# #167:用户全选 N 时允许零写入并警告
local file
local reply
if [ ! -r /dev/tty ] || [ ! -w /dev/tty ]; then
print_message warning "--interactive requested but no TTY is available; using default profile selection."
return 1
fi
print_message info "Select shell config files to update for opencode PATH:"
for file in "${profile_candidates[@]}"; do
if [ -f "$file" ]; then
printf "Update %s? [y/N] " "$file" > /dev/tty
else
printf "Create and update %s? [y/N] " "$file" > /dev/tty
fi
IFS= read -r reply < /dev/tty || reply=""
case $reply in
y|Y|yes|YES|Yes)
append_profile "$file"
;;
esac
done
if [ "${#profile_targets[@]}" -eq 0 ]; then
print_message warning "No shell config file selected; PATH was not modified."
fi
return 0
}
# #167:fish 用 fish_add_path;其它 shell 写 export PATH=...
path_command_for_profile() {
# #167:路径一律双引号,兼容含空格的 INSTALL_DIR
# #167:fish 专用 fish_add_path,避免 export 语法无效
local config_file=$1
case "$config_file" in
*/fish/config.fish)
echo "fish_add_path \"$INSTALL_DIR\""
;;
*)
echo "export PATH=\"$INSTALL_DIR:\$PATH\""
;;
esac
}
XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
current_shell=$(basename "${SHELL:-sh}")
# SHELL 未导出时回退 sh:cron/CI 等非交互环境不保证设置 SHELL
profile_candidates=()
profile_targets=()
if [[ "$no_modify_path" != "true" ]]; then
# #167:优先级 explicit path-profile > all-shell > interactive > 默认全量已存在
# #167:--no-modify-path 整段跳过,保留二进制安装成功语义
# #167:explicit 列表不再二次扫描,尊重用户白名单
if [ "${#path_profiles[@]}" -gt 0 ]; then
for file in "${path_profiles[@]}"; do
append_profile "$(normalize_profile_path "$file")"
done
elif [ "$all_shell_profiles_requested" = "true" ]; then
append_all_shell_profile_candidates
select_existing_profiles
elif [ "$interactive_path" = "true" ]; then
append_all_shell_profile_candidates
if ! select_interactive_profiles; then
profile_candidates=()
append_all_shell_profile_candidates
select_existing_profiles
fi
else
append_all_shell_profile_candidates
select_existing_profiles
fi
if [ "${#profile_targets[@]}" -gt 0 ]; then
for file in "${profile_targets[@]}"; do
add_to_path "$file" "$(path_command_for_profile "$file")"
done
fi
if [ "${#profile_targets[@]}" -eq 0 ]; then
print_message warning "No shell config file selected. You may need to manually add to PATH:"
print_message info " export PATH=\"$INSTALL_DIR:\$PATH\""
fi
fi
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" = "true" ]; then
# GITHUB_ACTIONS 环境检测:CI 内自动生效,本地安装零干扰
# #156:CI 写入 GITHUB_PATH 供后续 step 发现二进制
echo "$INSTALL_DIR" >> "$GITHUB_PATH"
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
fi
echo -e ""
echo -e "${MUTED} ${NC} ▄ "
echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█"
echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀"
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
echo -e ""
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
echo -e ""
echo -e "cd <project> ${MUTED}# Open directory${NC}"
echo -e "opencode ${MUTED}# Run command${NC}"
echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
echo -e ""