-
Notifications
You must be signed in to change notification settings - Fork 394
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·1557 lines (1399 loc) · 60.1 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·1557 lines (1399 loc) · 60.1 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
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
set -euo pipefail
# PilotDeck one-line installer for macOS and Linux.
# Usage:
# curl -fsSL https://raw.githubusercontent.com/OpenBMB/PilotDeck/main/install.sh | bash
REPO_URL="${PILOTDECK_REPO_URL:-https://github.com/OpenBMB/PilotDeck.git}"
BRANCH="${PILOTDECK_BRANCH:-main}"
INSTALL_DIR="${PILOTDECK_INSTALL_DIR:-$HOME/.pilotdeck/app}"
CONFIG_FILE="${PILOTDECK_CONFIG_PATH:-$HOME/.pilotdeck/pilotdeck.yaml}"
BIN_LINK="${PILOTDECK_BIN_LINK:-/usr/local/bin/pilotdeck}"
MAX_PORT_TRIES="${PILOTDECK_MAX_PORT_TRIES:-20}"
MIN_NODE_VERSION="22.13.0"
MAX_NODE_MAJOR="22"
NODE_INSTALL_VERSION="${PILOTDECK_NODE_VERSION:-22}"
NODE_FALLBACK_VERSION="${PILOTDECK_NODE_FALLBACK_VERSION:-22.13.0}"
NODE_DIRECT_INSTALL_ROOT="$HOME/.local/share/pilotdeck-node"
NODE_DIST_MIRROR="${PILOTDECK_NODE_DIST_MIRROR:-https://nodejs.org/dist}"
NODE_DIST_FALLBACK_MIRRORS="${PILOTDECK_NODE_DIST_FALLBACK_MIRRORS:-}"
APT_UPDATED=0
# 1 = repo was (re)cloned or its HEAD changed; drives whether we reinstall/rebuild.
REPO_CHANGED=1
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
DIM='\033[2m'
BOLD='\033[1m'
RESET='\033[0m'
ok() { printf " ${GREEN}✓${RESET} %s\n" "$1"; }
warn() { printf " ${YELLOW}→${RESET} %s\n" "$1"; }
fail() { printf " ${RED}✗${RESET} %s\n" "$1"; exit 1; }
# Localized string picker: L "<english>" "<chinese>" prints one based on PD_LANG.
# Any ${vars} are expanded by the caller before L runs, so interpolation works
# in both languages.
L() {
if [[ "${PD_LANG:-en}" == "zh" ]]; then
printf "%s" "$2"
else
printf "%s" "$1"
fi
}
# Language for user-facing guidance (en|zh). Resolution order:
# 1. PILOTDECK_LANG env var (en/zh)
# 2. Interactive prompt when a terminal is available (works with curl | bash
# because we read from /dev/tty, not the piped stdin)
# 3. Auto-detect from the locale ($LANG / $LC_ALL); default to English.
PD_LANG="en"
select_language() {
case "${PILOTDECK_LANG:-}" in
zh|cn|zh_CN|zh-CN|中文) PD_LANG="zh"; return ;;
en|en_US|en-US|english|English) PD_LANG="en"; return ;;
esac
# Prompt only if we can actually open the controlling terminal. This works
# under `curl ... | bash` (stdin is the pipe, but /dev/tty is the terminal)
# and cleanly falls back when there is no terminal at all (CI, nohup, etc.).
if { exec 3</dev/tty; } 2>/dev/null; then
printf "\n${BOLD}Select language / 选择语言${RESET}\n"
printf " 1) English\n"
printf " 2) 中文\n"
printf "> "
local answer=""
read -r answer <&3 || answer=""
exec 3<&-
case "$answer" in
2|zh|cn|中文|中) PD_LANG="zh" ;;
*) PD_LANG="en" ;;
esac
return
fi
case "${LC_ALL:-}${LANG:-}" in
*zh*|*ZH*) PD_LANG="zh" ;;
*) PD_LANG="en" ;;
esac
}
# Post-install onboarding guide, shown once the app is ready to start.
# Explains the single most important next step — configuring a model + API key —
# plus the manual config path, CLI commands, and where to find docs.
print_getting_started() {
local ui_url="$1"
if [[ "$PD_LANG" == "zh" ]]; then
print_getting_started_zh "$ui_url"
else
print_getting_started_en "$ui_url"
fi
}
print_getting_started_en() {
local ui_url="$1"
echo ""
echo -e "${BOLD}Getting started${RESET}"
echo "==============="
echo ""
echo -e " ${BOLD}1. Configure your model & API key${RESET}"
echo -e " PilotDeck ships with a placeholder config, so your first stop is onboarding."
echo -e " Open ${GREEN}${ui_url}${RESET} — it redirects to the onboarding screen where you"
echo -e " choose a provider, paste an API key, and pick a model."
echo -e " ${DIM}Supported: OpenAI, Anthropic, Google Gemini, DeepSeek, Qwen, Kimi, MiniMax,${RESET}"
echo -e " ${DIM}and any OpenAI-compatible endpoint.${RESET}"
echo ""
echo -e " ${BOLD}2. Prefer editing the config by hand?${RESET}"
echo -e " Edit ${DIM}${CONFIG_FILE}${RESET} and set your provider url + apiKey, e.g.:"
echo ""
echo -e " ${DIM}agent:${RESET}"
echo -e " ${DIM} model: deepseek/deepseek-v4-pro${RESET}"
echo -e " ${DIM}model:${RESET}"
echo -e " ${DIM} providers:${RESET}"
echo -e " ${DIM} deepseek:${RESET}"
echo -e " ${DIM} protocol: openai${RESET}"
echo -e " ${DIM} url: https://api.deepseek.com/v1${RESET}"
echo -e " ${DIM} apiKey: sk-your-api-key${RESET}"
echo ""
echo -e " ${BOLD}3. Manage PilotDeck from the CLI${RESET}"
echo -e " ${GREEN}pilotdeck${RESET} start the server"
echo -e " ${GREEN}pilotdeck status${RESET} show install path, config, and URL"
echo -e " ${GREEN}pilotdeck help${RESET} list all commands"
echo ""
echo -e " ${BOLD}Docs & community${RESET}"
echo -e " Tutorial: ${DIM}https://pilotdeck.openbmb.cn/pilotdeck.github.io/docs/en/introduction${RESET}"
echo -e " Website: ${DIM}https://pilotdeck.openbmb.cn${RESET}"
echo -e " Issues: ${DIM}https://github.com/OpenBMB/PilotDeck/issues${RESET}"
echo ""
}
print_getting_started_zh() {
local ui_url="$1"
echo ""
echo -e "${BOLD}快速上手${RESET}"
echo "========"
echo ""
echo -e " ${BOLD}1. 配置模型与 API Key${RESET}"
echo -e " PilotDeck 初始使用占位配置,因此第一步是完成引导配置。"
echo -e " 打开 ${GREEN}${ui_url}${RESET} — 页面会自动跳转到引导界面,"
echo -e " 在这里选择服务商、粘贴 API Key 并选择模型。"
echo -e " ${DIM}已支持:OpenAI、Anthropic、Google Gemini、DeepSeek、Qwen、Kimi、MiniMax,${RESET}"
echo -e " ${DIM}以及任意兼容 OpenAI 协议的接口。${RESET}"
echo ""
echo -e " ${BOLD}2. 更喜欢手动改配置?${RESET}"
echo -e " 编辑 ${DIM}${CONFIG_FILE}${RESET},填入服务商的 url 和 apiKey,例如:"
echo ""
echo -e " ${DIM}agent:${RESET}"
echo -e " ${DIM} model: deepseek/deepseek-v4-pro${RESET}"
echo -e " ${DIM}model:${RESET}"
echo -e " ${DIM} providers:${RESET}"
echo -e " ${DIM} deepseek:${RESET}"
echo -e " ${DIM} protocol: openai${RESET}"
echo -e " ${DIM} url: https://api.deepseek.com/v1${RESET}"
echo -e " ${DIM} apiKey: sk-your-api-key${RESET}"
echo ""
echo -e " ${BOLD}3. 通过命令行管理 PilotDeck${RESET}"
echo -e " ${GREEN}pilotdeck${RESET} 启动服务"
echo -e " ${GREEN}pilotdeck status${RESET} 查看安装路径、配置和访问地址"
echo -e " ${GREEN}pilotdeck help${RESET} 查看全部命令"
echo ""
echo -e " ${BOLD}文档与社区${RESET}"
echo -e " 教程: ${DIM}https://pilotdeck.openbmb.cn/pilotdeck.github.io/docs/en/introduction${RESET}"
echo -e " 官网: ${DIM}https://pilotdeck.openbmb.cn${RESET}"
echo -e " 反馈: ${DIM}https://github.com/OpenBMB/PilotDeck/issues${RESET}"
echo ""
}
# Sentinel written by scripts/bootstrap-pilotdeck-config.mjs for an unconfigured install.
ONBOARD_SENTINEL="PLACEHOLDER_RUN_ONBOARDING_TO_REPLACE"
# True when the config already holds a real provider/model (not the placeholder).
config_is_configured() {
[[ -f "$CONFIG_FILE" ]] || return 1
if grep -q "$ONBOARD_SENTINEL" "$CONFIG_FILE" 2>/dev/null; then return 1; fi
if grep -q "_placeholder/_placeholder" "$CONFIG_FILE" 2>/dev/null; then return 1; fi
return 0
}
# Write a minimal, valid pilotdeck.yaml from the collected provider details.
write_pilotdeck_config() {
local pid="$1" protocol="$2" url="$3" api_key="$4" model="$5"
mkdir -p "$(dirname "$CONFIG_FILE")"
if [[ -f "$CONFIG_FILE" ]]; then
cp "$CONFIG_FILE" "${CONFIG_FILE}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
fi
cat > "$CONFIG_FILE" <<YAML
schemaVersion: 1
agent:
model: ${pid}/${model}
model:
providers:
${pid}:
protocol: ${protocol}
url: ${url}
apiKey: ${api_key}
models:
${model}:
capabilities:
maxOutputTokens: 16384
adapters:
feishu:
enabled: false
appId: ""
appSecret: ""
cron:
enabled: true
timezone: Asia/Shanghai
maxConcurrentRuns: 2
runTimeoutMinutes: 60
YAML
}
# Interactive terminal setup wizard: pick a provider, enter an API key, choose a
# model, and write the config. Skips cleanly when non-interactive, already
# configured, or PILOTDECK_SKIP_ONBOARDING=1.
run_onboarding() {
if [[ "${PILOTDECK_SKIP_ONBOARDING:-0}" == "1" ]]; then
return
fi
if config_is_configured; then
ok "$(L "Existing model configuration detected; skipping setup wizard." "检测到已有模型配置,跳过配置向导。")"
return
fi
if ! { exec 3</dev/tty; } 2>/dev/null; then
warn "$(L "No interactive terminal; skipping setup wizard. Configure via the Web UI." "非交互式终端,跳过配置向导。可在 Web UI 中完成配置。")"
return
fi
echo ""
echo -e "${BOLD}$(L "Model setup" "模型配置")${RESET}"
echo "$(L "Configure a provider and API key now, or skip and use the Web UI later." "现在就配置服务商和 API Key,或先跳过、稍后用 Web UI 配置。")"
printf "%s [Y/n] " "$(L "Configure now?" "现在配置?")"
local yn=""; read -r yn <&3 || yn=""
case "$yn" in
n|N|no|NO|No)
exec 3<&-
warn "$(L "Skipped. Open the Web UI to finish onboarding." "已跳过。请打开 Web UI 完成配置。")"
return
;;
esac
echo ""
echo "$(L "Choose a provider:" "请选择服务商:")"
echo " 1) OpenAI"
echo " 2) Anthropic (Claude)"
echo " 3) Google Gemini"
echo " 4) DeepSeek"
echo " 5) $(L "Qwen (Alibaba DashScope)" "通义千问(阿里云百炼)")"
echo " 6) $(L "Kimi (Moonshot)" "Kimi(月之暗面)")"
echo " 7) MiniMax"
echo " 8) $(L "Zhipu GLM" "智谱 GLM")"
echo " 9) $(L "Custom OpenAI-compatible endpoint" "自定义 OpenAI 兼容接口")"
printf "> "
local choice=""; read -r choice <&3 || choice=""
local pid="" protocol="" url="" model=""
case "$choice" in
1) pid=openai; protocol=openai; url=https://api.openai.com/v1; model=gpt-4o ;;
2) pid=anthropic; protocol=anthropic; url=https://api.anthropic.com; model=claude-sonnet-4.6 ;;
3) pid=google; protocol=google; url=https://generativelanguage.googleapis.com; model=gemini-2.5-pro ;;
4) pid=deepseek; protocol=openai; url=https://api.deepseek.com/v1; model=deepseek-chat ;;
5) pid=dashscope; protocol=openai; url=https://dashscope.aliyuncs.com/compatible-mode/v1; model=qwen-max ;;
6) pid=moonshot; protocol=openai; url=https://api.moonshot.cn/v1; model=kimi-k2 ;;
7) pid=minimax; protocol=openai; url=https://api.minimaxi.com/v1; model=MiniMax-M2.5 ;;
8) pid=zhipu; protocol=openai; url=https://api.z.ai/api/paas/v4; model=glm-4.6 ;;
9)
protocol=openai
printf "%s " "$(L "Provider id (e.g. myprovider):" "服务商 id(如 myprovider):")"
read -r pid <&3 || pid=""
[[ -n "$pid" ]] || pid=custom
printf "%s " "$(L "Base URL (OpenAI-compatible):" "接口地址(OpenAI 兼容):")"
read -r url <&3 || url=""
;;
*)
exec 3<&-
warn "$(L "Invalid choice; skipping setup." "无效选项,已跳过配置。")"
return
;;
esac
if [[ -n "$model" ]]; then
printf "%s [%s] " "$(L "Model" "模型")" "$model"
local model_in=""; read -r model_in <&3 || model_in=""
model="${model_in:-$model}"
else
printf "%s " "$(L "Model name:" "模型名称:")"
read -r model <&3 || model=""
fi
printf "%s " "$(L "API key:" "API Key:")"
local api_key=""; read -rs api_key <&3 || api_key=""
echo ""
exec 3<&-
if [[ -z "$api_key" || -z "$url" || -z "$model" ]]; then
warn "$(L "Missing a required value; skipping. Configure later in the Web UI." "缺少必填项,已跳过。可稍后在 Web UI 中配置。")"
return
fi
write_pilotdeck_config "$pid" "$protocol" "$url" "$api_key" "$model"
ok "$(L "Saved config for ${pid}/${model} to" "已保存 ${pid}/${model} 的配置到") ${DIM}${CONFIG_FILE}${RESET}"
}
version_at_least() {
local version="${1#v}"
local minimum="${2#v}"
local v_major v_minor v_patch min_major min_minor min_patch
IFS=. read -r v_major v_minor v_patch _ <<< "$version"
IFS=. read -r min_major min_minor min_patch _ <<< "$minimum"
v_major="${v_major:-0}"
v_minor="${v_minor:-0}"
v_patch="${v_patch:-0}"
min_major="${min_major:-0}"
min_minor="${min_minor:-0}"
min_patch="${min_patch:-0}"
v_patch="${v_patch%%[^0-9]*}"
min_patch="${min_patch%%[^0-9]*}"
if (( v_major > min_major )); then return 0; fi
if (( v_major < min_major )); then return 1; fi
if (( v_minor > min_minor )); then return 0; fi
if (( v_minor < min_minor )); then return 1; fi
(( v_patch >= min_patch ))
}
node_major() {
local version="${1#v}"
printf "%s" "${version%%.*}"
}
expected_node_arch() {
case "$(uname -m)" in
x86_64|amd64) printf "x64" ;;
arm64|aarch64) printf "arm64" ;;
*) return 1 ;;
esac
}
node_arch_matches_host() {
local expected actual
expected="$(expected_node_arch 2>/dev/null || true)"
[[ -n "$expected" ]] || return 0
actual="$(node -p "process.arch" 2>/dev/null || true)"
[[ "$actual" == "$expected" ]]
}
node_tarball_platform() {
local os
local arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux) os="linux" ;;
Darwin) os="darwin" ;;
*) return 1 ;;
esac
case "$arch" in
x86_64|amd64) arch="x64" ;;
arm64|aarch64) arch="arm64" ;;
*) return 1 ;;
esac
printf "%s-%s" "$os" "$arch"
}
load_direct_node_runtime() {
local platform
local node_dir
platform="$(node_tarball_platform 2>/dev/null || true)"
[[ -n "$platform" ]] || return 0
node_dir="$NODE_DIRECT_INSTALL_ROOT/node-v${NODE_FALLBACK_VERSION}-${platform}"
if [[ -x "$node_dir/bin/node" ]]; then
export PATH="$node_dir/bin:$PATH"
export npm_config_nodedir="${npm_config_nodedir:-$node_dir}"
export npm_config_disturl="${npm_config_disturl:-${NODE_DIST_MIRROR%/}}"
fi
}
node_supports_sqlite() {
node -e "import('node:sqlite').then(() => {}, () => process.exit(1))" >/dev/null 2>&1
}
load_node_managers() {
if ! command -v fnm >/dev/null 2>&1 && [[ -d "$HOME/.local/share/fnm" ]]; then
export PATH="$HOME/.local/share/fnm:$PATH"
if command -v fnm >/dev/null 2>&1; then
eval "$(fnm env --shell bash)"
fi
fi
if ! command -v nvm >/dev/null 2>&1 && [[ -s "${NVM_DIR:-$HOME/.nvm}/nvm.sh" ]]; then
# nvm is a shell function, so non-interactive scripts must source it.
# shellcheck disable=SC1090
source "${NVM_DIR:-$HOME/.nvm}/nvm.sh"
fi
}
install_node_runtime() {
load_node_managers
if command -v fnm >/dev/null 2>&1; then
fnm install "$NODE_INSTALL_VERSION" </dev/null
fnm use "$NODE_INSTALL_VERSION"
elif command -v nvm >/dev/null 2>&1; then
nvm install "$NODE_INSTALL_VERSION" </dev/null
nvm use "$NODE_INSTALL_VERSION"
else
install_node_tarball
fi
}
install_node_tarball() {
local platform
local tarball
local url
local install_root
local node_dir
local tmp_dir
local mirrors
local mirror
local downloaded=0
platform="$(node_tarball_platform)" || fail "$(L "Unsupported platform for direct Node.js download. Please install Node.js 22 manually." "当前平台不支持直接下载 Node.js。请手动安装 Node.js 22。")"
tarball="node-v${NODE_FALLBACK_VERSION}-${platform}.tar.xz"
install_root="$NODE_DIRECT_INSTALL_ROOT"
node_dir="$install_root/node-v${NODE_FALLBACK_VERSION}-${platform}"
tmp_dir="$(mktemp -d)"
mirrors="$NODE_DIST_MIRROR"
if [[ -n "$NODE_DIST_FALLBACK_MIRRORS" ]]; then
mirrors="$mirrors $NODE_DIST_FALLBACK_MIRRORS"
fi
for mirror in $mirrors; do
url="${mirror%/}/v${NODE_FALLBACK_VERSION}/${tarball}"
warn "$(L "Downloading Node.js ${NODE_FALLBACK_VERSION} from ${url}" "正在从 ${url} 下载 Node.js ${NODE_FALLBACK_VERSION}")"
if run_with_timeout 180 curl -fL "$url" -o "$tmp_dir/$tarball"; then
downloaded=1
NODE_DIST_MIRROR="${mirror%/}"
break
fi
warn "$(L "Node.js download failed from ${mirror}; trying the next mirror if available." "从 ${mirror} 下载 Node.js 失败;如果还有镜像,将尝试下一个。")"
done
if [[ "$downloaded" != "1" ]]; then
rm -rf "$tmp_dir"
fail "$(L "Could not download Node.js. Check your network/proxy, or explicitly set PILOTDECK_NODE_DIST_MIRROR / PILOTDECK_NODE_DIST_FALLBACK_MIRRORS to a trusted reachable Node.js mirror." "无法下载 Node.js。请检查网络/代理,或显式将 PILOTDECK_NODE_DIST_MIRROR / PILOTDECK_NODE_DIST_FALLBACK_MIRRORS 设置为可信且可访问的 Node.js 镜像。")"
fi
mkdir -p "$install_root"
rm -rf "$node_dir"
tar -xJf "$tmp_dir/$tarball" -C "$install_root"
rm -rf "$tmp_dir"
export PATH="$node_dir/bin:$PATH"
export npm_config_nodedir="$node_dir"
export npm_config_disturl="${NODE_DIST_MIRROR%/}"
ok "$(L "Node.js installed directly at ${node_dir}" "已直接安装 Node.js 到 ${node_dir}")"
}
ensure_node_runtime() {
local node_version
load_node_managers
load_direct_node_runtime
if command -v node >/dev/null 2>&1; then
node_version="$(node --version)"
if version_at_least "$node_version" "$MIN_NODE_VERSION" && [[ "$(node_major "$node_version")" == "$MAX_NODE_MAJOR" ]] && node_supports_sqlite && node_arch_matches_host; then
ok "$(L "Node.js ${node_version} found" "已找到 Node.js ${node_version}")"
return
fi
warn "$(L "Node.js ${node_version} is not the supported Node.js 22 runtime for this machine (need >=${MIN_NODE_VERSION}, <23, node:sqlite, and matching CPU architecture). Installing/using Node.js ${NODE_INSTALL_VERSION}..." "Node.js ${node_version} 不是当前机器支持的 Node.js 22 运行时(需要 >=${MIN_NODE_VERSION} 且 <23、支持 node:sqlite,并匹配 CPU 架构)。正在安装/使用 Node.js ${NODE_INSTALL_VERSION}...")"
else
warn "$(L "Node.js not found. Installing Node.js ${NODE_FALLBACK_VERSION}..." "未找到 Node.js,正在安装 Node.js ${NODE_FALLBACK_VERSION}...")"
fi
install_node_runtime
node_version="$(node --version 2>/dev/null || true)"
if [[ -z "$node_version" ]] || ! version_at_least "$node_version" "$MIN_NODE_VERSION" || [[ "$(node_major "$node_version")" != "$MAX_NODE_MAJOR" ]] || ! node_supports_sqlite || ! node_arch_matches_host; then
fail "$(L "Node.js >=${MIN_NODE_VERSION} and <23 with node:sqlite and matching CPU architecture is required. Current: ${node_version:-not found}, arch $(node -p "process.arch" 2>/dev/null || printf unknown), host $(uname -m)." "需要带 node:sqlite、且匹配当前 CPU 架构的 Node.js >=${MIN_NODE_VERSION} 且 <23。当前:${node_version:-未找到}, 架构 $(node -p "process.arch" 2>/dev/null || printf unknown), 主机 $(uname -m)。")"
fi
ok "$(L "Node.js ${node_version} installed" "已安装 Node.js ${node_version}")"
}
# Portable timeout: use GNU timeout if available, else fall back to a bg+kill approach.
# Returns 124 on timeout (same convention as GNU timeout).
run_with_timeout() {
local secs="$1"; shift
if command -v timeout >/dev/null 2>&1; then
timeout "$secs" "$@"
else
"$@" &
local pid=$!
( sleep "$secs" && kill "$pid" 2>/dev/null ) &
local watchdog=$!
if wait "$pid" 2>/dev/null; then
kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
return 0
else
local rc=$?
kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
# 143 = SIGTERM (128+15), treat as timeout
if [[ $rc -eq 143 ]]; then return 124; fi
return $rc
fi
fi
}
run_as_root() {
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
fail "$(L "Need root privileges to install system packages. Please install sudo or run as root." "安装系统软件包需要 root 权限。请安装 sudo 或以 root 身份运行。")"
fi
}
has_linux_package_manager() {
command -v apt-get >/dev/null 2>&1 || \
command -v dnf >/dev/null 2>&1 || \
command -v yum >/dev/null 2>&1 || \
command -v pacman >/dev/null 2>&1 || \
command -v zypper >/dev/null 2>&1
}
can_install_system_packages() {
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
return 0
fi
command -v sudo >/dev/null 2>&1
}
can_install_optional_system_packages() {
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
return 0
fi
command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1
}
missing_required_linux_system_packages() {
local missing=()
command -v git >/dev/null 2>&1 || missing+=(git)
command -v python3 >/dev/null 2>&1 || missing+=(python3)
command -v make >/dev/null 2>&1 || missing+=(make)
has_cxx_compiler || missing+=(c++-compiler)
if [[ "${#missing[@]}" -gt 0 ]]; then
printf "%s" "${missing[*]}"
fi
}
print_minimum_requirements() {
if [[ "$PLATFORM" == "linux" ]]; then
warn "$(L "Minimum requirements: bash, curl, network access, git, Python 3, make, and a C++ compiler. sudo/root is only needed when required system packages are missing." "最低要求:bash、curl、网络访问、git、Python 3、make 和 C++ 编译器。仅当缺少必需系统软件包时才需要 sudo/root。")"
else
warn "$(L "Minimum requirements: bash, curl, network access, and Xcode Command Line Tools. Homebrew is only needed if optional tools such as ripgrep or Git LFS are missing." "最低要求:bash、curl、网络访问和 Xcode 命令行工具。仅当缺少 ripgrep 或 Git LFS 等可选工具时才需要 Homebrew。")"
fi
}
check_bootstrap_requirements() {
print_minimum_requirements
if [[ -z "${BASH_VERSION:-}" ]]; then
fail "$(L "This installer must run with bash. Try: curl -fsSL https://raw.githubusercontent.com/OpenBMB/PilotDeck/main/install.sh | bash" "该安装器必须使用 bash 运行。请尝试:curl -fsSL https://raw.githubusercontent.com/OpenBMB/PilotDeck/main/install.sh | bash")"
fi
if ! command -v curl >/dev/null 2>&1; then
fail "$(L "curl is required to install Node.js and optional browser assets. Please install curl and re-run this installer." "安装 Node.js 和可选浏览器资源需要 curl。请先安装 curl 后重新运行安装器。")"
fi
ok "$(L "curl found" "已找到 curl")"
if ! command -v tar >/dev/null 2>&1; then
fail "$(L "tar is required to unpack Node.js. Please install tar and re-run this installer." "解压 Node.js 需要 tar。请先安装 tar 后重新运行安装器。")"
fi
ok "$(L "tar found" "已找到 tar")"
if ! tar --help 2>/dev/null | grep -q -- '-J'; then
warn "$(L "tar may not support .tar.xz archives; install xz/xz-utils if the Node.js download cannot be unpacked." "当前 tar 可能不支持 .tar.xz;如果 Node.js 下载后无法解压,请安装 xz/xz-utils。")"
fi
if [[ "$PLATFORM" == "linux" ]]; then
local missing_packages
missing_packages="$(missing_required_linux_system_packages)"
if [[ -z "$missing_packages" ]]; then
ok "$(L "required Linux build dependencies already present" "Linux 必需构建依赖已齐备")"
return
fi
warn "$(L "Missing required Linux build dependencies: ${missing_packages}" "缺少 Linux 必需构建依赖:${missing_packages}")"
if ! has_linux_package_manager; then
fail "$(L "Unsupported Linux package manager. Please install apt, dnf, yum, pacman, or zypper, or install dependencies manually before re-running." "不支持当前 Linux 包管理器。请安装 apt、dnf、yum、pacman 或 zypper,或先手动安装依赖后再重新运行。")"
fi
ok "$(L "supported Linux package manager found" "已找到支持的 Linux 包管理器")"
if ! can_install_system_packages; then
fail "$(L "Installing missing required system packages needs root or sudo. Please run as root, configure sudo, or preinstall git, python3, make, and a C++ compiler." "安装缺失的必需系统软件包需要 root 或 sudo。请以 root 身份运行、配置 sudo,或预先安装 git、python3、make 和 C++ 编译器。")"
fi
ok "$(L "root user or sudo command available for system package installs" "root 用户或 sudo 命令可用于安装系统软件包")"
else
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
fail "$(L "macOS requires Xcode Command Line Tools for git, lsof, and native module builds. Install them with: xcode-select --install" "macOS 需要 Xcode 命令行工具来提供 git、lsof 和原生模块构建能力。请运行:xcode-select --install")"
fi
ok "$(L "Xcode Command Line Tools found" "已找到 Xcode 命令行工具")"
if ! command -v brew >/dev/null 2>&1; then
warn "$(L "Homebrew is not installed. If ripgrep or Git LFS are missing, install Homebrew first, then run: brew install ripgrep git-lfs" "未安装 Homebrew。若缺少 ripgrep 或 Git LFS,请先安装 Homebrew,然后运行:brew install ripgrep git-lfs")"
fi
fi
}
install_linux_packages() {
local requested=("$@")
local apt_packages=()
local dnf_packages=()
local pacman_packages=()
local zypper_packages=()
local package
for package in "${requested[@]}"; do
case "$package" in
build-tools)
apt_packages+=(build-essential python3)
dnf_packages+=(gcc gcc-c++ make python3)
pacman_packages+=(base-devel python)
zypper_packages+=(gcc gcc-c++ make python3)
;;
*)
apt_packages+=("$package")
dnf_packages+=("$package")
pacman_packages+=("$package")
zypper_packages+=("$package")
;;
esac
done
if command -v apt-get >/dev/null 2>&1; then
if [[ "$APT_UPDATED" -eq 0 ]]; then
run_as_root apt-get update
APT_UPDATED=1
fi
run_as_root apt-get install -y "${apt_packages[@]}"
elif command -v dnf >/dev/null 2>&1; then
run_as_root dnf install -y "${dnf_packages[@]}"
elif command -v yum >/dev/null 2>&1; then
run_as_root yum install -y "${dnf_packages[@]}"
elif command -v pacman >/dev/null 2>&1; then
run_as_root pacman -Sy --needed --noconfirm "${pacman_packages[@]}"
elif command -v zypper >/dev/null 2>&1; then
run_as_root zypper --non-interactive install "${zypper_packages[@]}"
else
fail "$(L "Unsupported Linux package manager. Please install manually: ${requested[*]}" "不支持的 Linux 包管理器。请手动安装:${requested[*]}")"
fi
}
install_git() {
if [[ "$PLATFORM" == "linux" ]]; then
install_linux_packages git
else
fail "$(L "git is not installed. Please install Xcode Command Line Tools: xcode-select --install" "未安装 git。请先安装 Xcode 命令行工具:xcode-select --install")"
fi
}
install_ripgrep() {
if [[ "$PLATFORM" == "macos" ]] && command -v brew >/dev/null 2>&1; then
brew install ripgrep </dev/null
elif [[ "$PLATFORM" == "linux" ]]; then
install_linux_packages ripgrep
else
fail "$(L "ripgrep (rg) is required. On macOS, install Homebrew and run: brew install ripgrep" "需要 ripgrep(rg)。在 macOS 上请安装 Homebrew 后运行:brew install ripgrep")"
fi
}
install_git_lfs() {
if [[ "$PLATFORM" == "macos" ]] && command -v brew >/dev/null 2>&1; then
brew install git-lfs </dev/null
elif [[ "$PLATFORM" == "linux" ]]; then
install_linux_packages git-lfs
else
fail "$(L "git-lfs is required for PilotDeck assets. On macOS, install Homebrew and run: brew install git-lfs" "PilotDeck 素材需要 git-lfs。在 macOS 上请安装 Homebrew 后运行:brew install git-lfs")"
fi
}
install_lsof() {
if [[ "$PLATFORM" == "linux" ]]; then
install_linux_packages lsof
else
fail "$(L "lsof is required but missing. Please install Xcode Command Line Tools: xcode-select --install" "缺少必需的 lsof。请先安装 Xcode 命令行工具:xcode-select --install")"
fi
}
has_cxx_compiler() {
command -v g++ >/dev/null 2>&1 || command -v c++ >/dev/null 2>&1 || command -v clang++ >/dev/null 2>&1
}
python_has_distutils() {
local python_bin="${PYTHON:-python3}"
command -v "$python_bin" >/dev/null 2>&1 || return 1
"$python_bin" - <<'PY' >/dev/null 2>&1
import distutils.version
PY
}
select_python_with_distutils() {
local candidates=()
if [[ -n "${PYTHON:-}" ]]; then
candidates+=("$PYTHON")
fi
candidates+=(python3 /usr/bin/python3 /opt/homebrew/bin/python3 /usr/local/bin/python3 python)
local candidate resolved
local seen=""
for candidate in "${candidates[@]}"; do
if ! command -v "$candidate" >/dev/null 2>&1; then
continue
fi
resolved="$(command -v "$candidate")"
case " $seen " in
*" $resolved "*) continue ;;
esac
seen="${seen} ${resolved}"
if "$resolved" - <<'PY' >/dev/null 2>&1
import distutils.version
PY
then
export PYTHON="$resolved"
return 0
fi
done
return 1
}
macos_has_usable_xcode_tools() {
# A CLT-only installation commonly does not provide xcodebuild and may lack
# pkgutil receipts, but it is still sufficient for node-gyp when xcrun can
# locate a compiler through the selected developer directory.
command -v xcrun >/dev/null 2>&1 || return 1
xcode-select -p >/dev/null 2>&1 || return 1
xcrun --find clang >/dev/null 2>&1 || return 1
xcrun --find clang++ >/dev/null 2>&1 || return 1
return 0
}
ensure_native_build_tools() {
if command -v python3 >/dev/null 2>&1 && command -v make >/dev/null 2>&1 && has_cxx_compiler; then
if [[ "$PLATFORM" == "macos" ]]; then
if ! python_has_distutils && ! select_python_with_distutils; then
fail "$(L "Python distutils is required when native npm packages build from source. Install/use a Python that provides distutils, e.g. set PYTHON=/usr/bin/python3 before running the installer, or install setuptools for your Python." "原生 npm 依赖从源码编译时需要 Python distutils。请安装/使用带 distutils 的 Python,例如运行安装器前设置 PYTHON=/usr/bin/python3,或为当前 Python 安装 setuptools。")"
fi
ok "$(L "Python with distutils found: ${PYTHON:-python3}" "已找到带 distutils 的 Python:${PYTHON:-python3}")"
if ! macos_has_usable_xcode_tools; then
if command -v xcode-select >/dev/null 2>&1; then
warn "$(L "Opening Xcode Command Line Tools installer prompt..." "正在打开 Xcode Command Line Tools 安装提示...")"
xcode-select --install >/dev/null 2>&1 || true
fi
fail "$(L "macOS native npm builds require a complete Xcode Command Line Tools installation. Run: xcode-select --install. If it is already installed but broken, reinstall it or run: sudo xcode-select --reset" "macOS 原生 npm 编译需要完整的 Xcode Command Line Tools。请运行:xcode-select --install。如果已安装但状态异常,请重新安装或运行:sudo xcode-select --reset")"
fi
fi
ok "$(L "native build tools found" "已找到原生编译工具")"
return
fi
if [[ "$PLATFORM" == "linux" ]]; then
warn "$(L "native build tools not found. Installing build tools for node-pty/better-sqlite3..." "未找到原生编译工具。正在为 node-pty/better-sqlite3 安装编译工具...")"
install_linux_packages build-tools
ok "$(L "native build tools installed" "已安装原生编译工具")"
else
fail "$(L "native build tools are missing. Please install Xcode Command Line Tools: xcode-select --install" "缺少原生编译工具。请先安装 Xcode 命令行工具:xcode-select --install")"
fi
}
is_port_free() {
local port="$1"
if command -v lsof >/dev/null 2>&1; then
! lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
elif command -v ss >/dev/null 2>&1; then
! ss -tlnH "sport = :$port" 2>/dev/null | grep -q .
else
! (echo >/dev/tcp/127.0.0.1/"$port") 2>/dev/null
fi
}
find_free_port() {
local base="$1"
local offset candidate
for ((offset = 0; offset < MAX_PORT_TRIES; offset++)); do
candidate=$((base + offset))
if is_port_free "$candidate"; then
printf "%s" "$candidate"
return 0
fi
done
return 1
}
resolve_runtime_ports() {
local server_base="${SERVER_PORT:-3001}"
local gateway_base="${PILOTDECK_GATEWAY_PORT:-18789}"
SERVER_PORT="$(find_free_port "$server_base")" || \
fail "$(L "Could not find a free UI port within ${MAX_PORT_TRIES} ports from ${server_base}." "从 ${server_base} 起 ${MAX_PORT_TRIES} 个端口内未找到空闲的 UI 端口。")"
PILOTDECK_GATEWAY_PORT="$(find_free_port "$gateway_base")" || \
fail "$(L "Could not find a free gateway port within ${MAX_PORT_TRIES} ports from ${gateway_base}." "从 ${gateway_base} 起 ${MAX_PORT_TRIES} 个端口内未找到空闲的网关端口。")"
PILOTDECK_GATEWAY_URL="ws://127.0.0.1:${PILOTDECK_GATEWAY_PORT}/ws"
export SERVER_PORT PILOTDECK_GATEWAY_PORT PILOTDECK_GATEWAY_URL
if [[ "$SERVER_PORT" != "$server_base" ]]; then
warn "$(L "UI port ${server_base} is busy; using ${SERVER_PORT} instead." "UI 端口 ${server_base} 被占用,改用 ${SERVER_PORT}。")"
fi
if [[ "$PILOTDECK_GATEWAY_PORT" != "$gateway_base" ]]; then
warn "$(L "Gateway port ${gateway_base} is busy; using ${PILOTDECK_GATEWAY_PORT} instead." "网关端口 ${gateway_base} 被占用,改用 ${PILOTDECK_GATEWAY_PORT}。")"
fi
}
github_repo_slug() {
case "$REPO_URL" in
https://github.com/*.git)
local slug="${REPO_URL#https://github.com/}"
printf "%s" "${slug%.git}"
;;
git@github.com:*.git)
local slug="${REPO_URL#git@github.com:}"
printf "%s" "${slug%.git}"
;;
*)
return 1
;;
esac
}
normalize_github_remote() {
local url="$1"
case "$url" in
https://github.com/*)
local slug="${url#https://github.com/}"
slug="${slug%.git}"
printf "%s" "$slug"
;;
git@github.com:*)
local slug="${url#git@github.com:}"
slug="${slug%.git}"
printf "%s" "$slug"
;;
ssh://git@github.com/*)
local slug="${url#ssh://git@github.com/}"
slug="${slug%.git}"
printf "%s" "$slug"
;;
*)
printf "%s" "$url"
;;
esac
}
clone_without_lfs_smudge() {
if [[ "${PILOTDECK_INSTALL_LFS:-0}" == "1" ]]; then
"$@"
else
GIT_LFS_SKIP_SMUDGE=1 "$@"
fi
}
clone_repo() {
local slug
if slug="$(github_repo_slug)" && command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
clone_without_lfs_smudge gh repo clone "$slug" "$INSTALL_DIR" -- --branch "$BRANCH" --depth 1 || \
fail "$(L "Could not clone ${REPO_URL}. Check repository access and network connectivity." "无法克隆 ${REPO_URL}。请检查仓库访问权限和网络连接。")"
else
clone_without_lfs_smudge git clone --branch "$BRANCH" --depth 1 "$REPO_URL" "$INSTALL_DIR" || \
fail "$(L "Could not clone ${REPO_URL}. If this repository is private, authenticate with GitHub first." "无法克隆 ${REPO_URL}。若为私有仓库,请先完成 GitHub 认证。")"
fi
}
repo_remote_url() {
git -C "$1" remote get-url origin 2>/dev/null || true
}
repo_has_changes() {
[[ -n "$(git -C "$1" status --porcelain 2>/dev/null)" ]]
}
backup_existing_installation() {
local source_dir="$1"
local backup_dir timestamp
timestamp="$(date +%Y%m%d-%H%M%S)"
backup_dir="${source_dir}.backup.${timestamp}"
while [[ -e "$backup_dir" ]]; do
timestamp="$(date +%Y%m%d-%H%M%S)-$RANDOM"
backup_dir="${source_dir}.backup.${timestamp}"
done
mv "$source_dir" "$backup_dir"
warn "$(L "Existing installation moved to ${backup_dir}" "已将现有安装移动到 ${backup_dir}")"
}
checkout_existing_installation() {
cd "$INSTALL_DIR"
local before after
before="$(git rev-parse HEAD 2>/dev/null || echo none)"
# Fetch from the configured (HTTPS) URL rather than the existing "origin"
# remote, which may be an SSH URL (git@github.com) that hangs when port 22
# connectivity is unavailable. Cap the fetch so a bad network can't stall the installer.
if run_with_timeout "${PILOTDECK_FETCH_TIMEOUT:-45}" env GIT_LFS_SKIP_SMUDGE=1 git fetch "$REPO_URL" "$BRANCH"; then
GIT_LFS_SKIP_SMUDGE=1 git checkout -B "$BRANCH" FETCH_HEAD || return 1
else
warn "$(L "Could not fetch updates (network/SSH issue); keeping the current checkout." "无法拉取更新(网络/SSH 问题),沿用当前已安装的代码。")"
fi
after="$(git rev-parse HEAD 2>/dev/null || echo none)"
if [[ "$before" == "$after" ]]; then
REPO_CHANGED=0
else
REPO_CHANGED=1
fi
return 0
}
install_or_update_repo() {
mkdir -p "$(dirname "$INSTALL_DIR")"
if [[ -d "$INSTALL_DIR/.git" ]]; then
local current_remote current_remote_normalized expected_remote_normalized
current_remote="$(repo_remote_url "$INSTALL_DIR")"
current_remote_normalized="$(normalize_github_remote "$current_remote")"
expected_remote_normalized="$(normalize_github_remote "$REPO_URL")"
if [[ "$current_remote_normalized" != "$expected_remote_normalized" ]]; then
warn "$(L "Existing installation uses ${current_remote:-unknown remote}; expected ${REPO_URL}." "现有安装使用的是 ${current_remote:-未知远程};预期为 ${REPO_URL}。")"
backup_existing_installation "$INSTALL_DIR"
clone_repo
ok "$(L "Repository cloned" "仓库已克隆")"
return
fi
if repo_has_changes "$INSTALL_DIR"; then
warn "$(L "Existing installation has local changes; preserving it before reinstalling." "现有安装存在本地改动;重新安装前先予以保留。")"
backup_existing_installation "$INSTALL_DIR"
clone_repo
ok "$(L "Repository cloned" "仓库已克隆")"
return
fi
warn "$(L "Existing installation found. Updating..." "检测到现有安装,正在更新...")"
if checkout_existing_installation; then
ok "$(L "Updated to latest ${BRANCH}" "已更新到最新的 ${BRANCH}")"
else
warn "$(L "Fast update failed; preserving existing checkout before reinstalling." "快速更新失败;重新安装前先保留现有代码。")"
cd "$(dirname "$INSTALL_DIR")"
backup_existing_installation "$INSTALL_DIR"
clone_repo
ok "$(L "Repository cloned" "仓库已克隆")"
fi
return
fi
if [[ -d "$INSTALL_DIR" ]]; then
warn "$(L "Cleaning incomplete installation at $INSTALL_DIR" "正在清理位于 $INSTALL_DIR 的不完整安装")"
rm -rf "$INSTALL_DIR"
fi
clone_repo
ok "$(L "Repository cloned" "仓库已克隆")"
}
ensure_lfs_assets() {
if [[ "${PILOTDECK_INSTALL_LFS:-0}" != "1" ]]; then
warn "$(L "Skipping Git LFS media download. Set PILOTDECK_INSTALL_LFS=1 to fetch demo images/videos." "跳过 Git LFS 媒体下载。设置 PILOTDECK_INSTALL_LFS=1 可下载演示图片/视频。")"
return
fi
if [[ "${GIT_LFS_SKIP_SMUDGE:-}" == "1" ]]; then
warn "$(L "GIT_LFS_SKIP_SMUDGE=1 is set; large media assets were intentionally skipped." "已设置 GIT_LFS_SKIP_SMUDGE=1;大型媒体素材已被有意跳过。")"
return
fi
if ! command -v git-lfs >/dev/null 2>&1 && ! git lfs version >/dev/null 2>&1; then
fail "$(L "git-lfs command not found after installation." "安装后仍未找到 git-lfs 命令。")"
fi
cd "$INSTALL_DIR"
git lfs install --local >/dev/null
git lfs pull
local pointer_file=""
for pointer_file in assets/banner.png ui/public/favicon.png ui/src/assets/pilotdeck-logo.png; do
if [[ -f "$pointer_file" ]] && grep -q "version https://git-lfs.github.com/spec/v1" "$pointer_file"; then
fail "$(L "Git LFS asset was not downloaded correctly: ${pointer_file}" "Git LFS 素材未正确下载:${pointer_file}")"
fi
done
ok "$(L "Git LFS assets downloaded" "Git LFS 素材已下载")"
}
# Skip the (slow) npm install + frontend build when nothing changed: repo HEAD
# unchanged and all build artifacts already present. Force with PILOTDECK_FORCE_DEPS=1.
deps_up_to_date() {
[[ "${PILOTDECK_FORCE_DEPS:-0}" != "1" ]] || return 1
[[ "${REPO_CHANGED:-1}" == "0" ]] || return 1
[[ -d "$INSTALL_DIR/node_modules" ]] || return 1
[[ -f "$INSTALL_DIR/dist/src/cli/pilotdeck.js" ]] || return 1
[[ -f "$INSTALL_DIR/src/context/memory/edgeclaw-memory-core/lib/index.js" ]] || return 1