@cocorograph/hub-agent 0.8.65 → 0.8.66

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/install.sh +354 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.65",
3
+ "version": "0.8.66",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -45,6 +45,13 @@ NODE_DEFAULT_BREW_FORMULA="node@24"
45
45
  PACKAGE_NAME="@cocorograph/hub-agent"
46
46
  CLAUDE_CODE_PACKAGE="@anthropic-ai/claude-code"
47
47
  CODEX_PACKAGE="@openai/codex"
48
+ UV_VERSION="0.12.8"
49
+ HUB_PYTHON_MINOR="3.13"
50
+ HUB_VENV_DIR="$HOME/.local/share/hub-venv"
51
+ # Hub Python の依存バージョンを更新するときは、この配列を変更する。
52
+ HUB_PYTHON_PACKAGES=("PyYAML==6.0.3" "Pillow==12.3.0" "openpyxl==3.1.5")
53
+ # HUB_PYTHON_PACKAGES と同じ 3 件を指す。依存を増やすときは両方を更新する。
54
+ HUB_PYTHON_IMPORTS=("yaml" "PIL" "openpyxl")
48
55
 
49
56
  color_step() { printf "\033[1;34m==> %s\033[0m\n" "$1"; }
50
57
  color_ok() { printf "\033[1;32m✓ %s\033[0m\n" "$1"; }
@@ -598,6 +605,7 @@ ensure_build_tools() {
598
605
  ensure_python3() {
599
606
  if python3 --version >/dev/null 2>&1; then
600
607
  color_ok "$(python3 --version 2>&1)"
608
+ _check_python3_version
601
609
  return 0
602
610
  fi
603
611
 
@@ -615,12 +623,355 @@ ensure_python3() {
615
623
 
616
624
  if python3 --version >/dev/null 2>&1; then
617
625
  color_ok "$(python3 --version 2>&1)"
626
+ _check_python3_version
618
627
  else
619
628
  color_err "python3 の導入後も実行できません"
620
629
  exit 1
621
630
  fi
622
631
  }
623
632
 
633
+ _check_python3_version() {
634
+ local version
635
+ version="$(python3 -c 'import platform; print(platform.python_version())')" || {
636
+ color_err "python3 のバージョンを確認できません"
637
+ exit 1
638
+ }
639
+ if python3 -c 'import sys; raise SystemExit(0 if sys.version_info < (3, 9) else 1)'; then
640
+ color_err "hub の配布物は Python 3.9 以上が必要です(現在: $version)"
641
+ exit 1
642
+ fi
643
+ if python3 -c 'import sys; raise SystemExit(0 if sys.version_info < (3, 11) else 1)'; then
644
+ color_warn "Python 3.11 未満では一部機能が無効になります(Codex の MCP 承認チェック)。現在: $version"
645
+ fi
646
+ }
647
+
648
+ _hub_uv_triple() {
649
+ local os arch libc="gnu"
650
+ os="$(uname -s)"
651
+ arch="$(uname -m)"
652
+ if [[ "$os" == "Linux" ]] && ldd --version 2>&1 | grep -qi musl; then
653
+ libc="musl"
654
+ fi
655
+ case "$os:$arch" in
656
+ Darwin:arm64) printf '%s\n' "aarch64-apple-darwin" ;;
657
+ Darwin:x86_64) printf '%s\n' "x86_64-apple-darwin" ;;
658
+ Linux:x86_64) printf '%s\n' "x86_64-unknown-linux-$libc" ;;
659
+ Linux:aarch64|Linux:arm64) printf '%s\n' "aarch64-unknown-linux-$libc" ;;
660
+ *) color_err "uv が対応していないプラットフォームです: $os $arch"; return 1 ;;
661
+ esac
662
+ }
663
+
664
+ _install_hub_uv() (
665
+ set -e
666
+ local triple url tmp expected actual uv_src local_bin destination staged=""
667
+ triple="$(_hub_uv_triple)" || exit 1
668
+ local_bin="$HOME/.local/bin"
669
+ destination="$local_bin/uv"
670
+ mkdir -p "$local_bin"
671
+ if [[ -e "$destination" || -L "$destination" ]]; then
672
+ color_err "$destination は uv と名前衝突しています。既存ファイルを退避するか PATH を修正してください"
673
+ exit 1
674
+ fi
675
+ url="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${triple}.tar.gz"
676
+ tmp="$(mktemp -d)"
677
+ trap 'rm -rf "$tmp"' EXIT
678
+ staged="$(mktemp "$local_bin/.uv.tmp.XXXXXX")"
679
+ trap 'rm -rf "$tmp"; if [[ -n "$staged" ]]; then rm -f "$staged"; fi' EXIT
680
+ trap 'exit 130' INT
681
+ trap 'exit 143' TERM
682
+ retry 3 curl -fsSL "$url" -o "$tmp/uv.tar.gz" || exit 1
683
+ retry 3 curl -fsSL "$url.sha256" -o "$tmp/uv.tar.gz.sha256" || exit 1
684
+ expected="$(head -1 "$tmp/uv.tar.gz.sha256")"
685
+ expected="${expected%%[[:space:]]*}"
686
+ if present shasum; then
687
+ actual="$(shasum -a 256 "$tmp/uv.tar.gz")"
688
+ elif present sha256sum; then
689
+ actual="$(sha256sum "$tmp/uv.tar.gz")"
690
+ else
691
+ color_err "sha256 検算コマンド (shasum / sha256sum) がありません"
692
+ exit 1
693
+ fi
694
+ actual="${actual%%[[:space:]]*}"
695
+ if [[ -z "$expected" || "$actual" != "$expected" ]]; then
696
+ color_err "uv tarball の sha256 が一致しません"
697
+ exit 1
698
+ fi
699
+ tar -xzf "$tmp/uv.tar.gz" -C "$tmp"
700
+ uv_src="$(find "$tmp" -type f -name uv | head -1)"
701
+ [[ -n "$uv_src" ]] || { color_err "uv tarball に uv がありません"; exit 1; }
702
+ mv "$uv_src" "$staged"
703
+ chmod +x "$staged"
704
+ "$staged" --version >/dev/null 2>&1 || { color_err "取得した uv を実行できません"; exit 1; }
705
+ if [[ -e "$destination" || -L "$destination" ]]; then
706
+ color_err "$destination は uv と名前衝突しています。既存ファイルを退避するか PATH を修正してください"
707
+ exit 1
708
+ fi
709
+ mv "$staged" "$destination"
710
+ )
711
+
712
+ _hub_venv_usable() {
713
+ local venv_dir="${1:-$HUB_VENV_DIR}" python base_prefix version package spec distribution expected
714
+ python="$venv_dir/bin/python3"
715
+ [[ -x "$python" ]] || return 1
716
+ base_prefix="$($python -c 'import sys; print(sys.base_prefix)' 2>/dev/null)" || return 1
717
+ [[ -e "$base_prefix/bin/python3" ]] || return 1
718
+ version="$($python -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")' 2>/dev/null)" || return 1
719
+ [[ "$version" == "$HUB_PYTHON_MINOR" ]] || return 1
720
+ for package in "${HUB_PYTHON_PACKAGES[@]}"; do
721
+ distribution="${package%%==*}"
722
+ expected="${package#*==}"
723
+ "$python" -c 'import importlib.metadata, sys; raise SystemExit(0 if importlib.metadata.version(sys.argv[1]) == sys.argv[2] else 1)' \
724
+ "$distribution" "$expected" >/dev/null 2>&1 || return 1
725
+ done
726
+ for spec in "${HUB_PYTHON_IMPORTS[@]}"; do
727
+ "$python" -c 'import importlib, sys; importlib.import_module(sys.argv[1])' "$spec" >/dev/null 2>&1 || return 1
728
+ done
729
+ }
730
+
731
+ _build_hub_venv() (
732
+ set -e
733
+ local new_dir="$1" built=0
734
+ trap '(( built == 1 )) || rm -rf "$new_dir"' EXIT
735
+ trap 'exit 130' INT
736
+ trap 'exit 143' TERM
737
+ rm -rf "$new_dir"
738
+ uv venv --python "$HUB_PYTHON_MINOR" "$new_dir" || exit 1
739
+ uv pip install --python "$new_dir/bin/python3" --only-binary :all: "${HUB_PYTHON_PACKAGES[@]}" || exit 1
740
+ _hub_venv_usable "$new_dir" || { color_err "構築した Hub Python venv の検証に失敗しました"; exit 1; }
741
+ built=1
742
+ )
743
+
744
+ _acquire_hub_python_lock() {
745
+ local lock_dir="$1" attempts=0 owner_file token owner_pid
746
+ owner_file="$lock_dir/owner"
747
+ # ⚠️ ここで時刻を採らない。`printf -v t '%(%s)T'` は bash 4.2 以降の機能で、macOS 標準の
748
+ # bash 3.2 では「invalid format character」で失敗する。`set -euo pipefail` の下なので
749
+ # インストーラ自体がここで止まる(実測。テストの bash は Homebrew の 5.x なので緑になってしまう)。
750
+ # mkdir lock では「観測 → 回収 → 再取得」の間に別プロセスが取り直した lock を、遅れて来た
751
+ # 回収者が持ち去れる。回収の rename が保証するのは、同時に回収する複数の勝者が 1 つになる
752
+ # ことまでで、破壊的操作の直前に所有を再確認しても TOCTOU の窓は消えない。
753
+ # そのため自動回収は行わず、異常終了後は表示されたディレクトリを人が削除して再実行する。
754
+ token="$$-${RANDOM:-0}-${RANDOM:-0}"
755
+ while ! mkdir "$lock_dir" 2>/dev/null; do
756
+ attempts=$((attempts + 1))
757
+ if (( attempts >= 50 )); then
758
+ owner_pid=""
759
+ if [[ -f "$owner_file" ]]; then
760
+ {
761
+ read -r _ || true
762
+ read -r owner_pid || true
763
+ } < "$owner_file"
764
+ fi
765
+ if [[ "$owner_pid" =~ ^[0-9]+$ ]]; then
766
+ color_err "Hub Python lock を取得できませんでした: $lock_dir。保持しているプロセス (PID $owner_pid) が動いているか ps -p $owner_pid で確かめ、動いていなければこのディレクトリを削除して再実行してください"
767
+ else
768
+ color_err "Hub Python lock を取得できませんでした: $lock_dir。インストーラが走っていないならこのディレクトリを削除して再実行してください"
769
+ fi
770
+ return 1
771
+ fi
772
+ sleep 0.1
773
+ done
774
+ if ! printf '%s\n%s\n' "$token" "$$" > "$owner_file"; then
775
+ rm -rf "$lock_dir" 2>/dev/null || true
776
+ return 1
777
+ fi
778
+ HUB_PYTHON_LOCK_TOKEN="$token"
779
+ }
780
+
781
+ _replace_hub_venv() (
782
+ local new_dir="$1" old_dir="$2" moved_old=0 installed=0
783
+ cleanup_replacement() {
784
+ if (( moved_old == 1 && installed == 0 )) && [[ -e "$old_dir" ]] && [[ ! -e "$HUB_VENV_DIR" ]]; then
785
+ mv "$old_dir" "$HUB_VENV_DIR"
786
+ fi
787
+ }
788
+ trap cleanup_replacement EXIT
789
+ if [[ -e "$HUB_VENV_DIR" || -L "$HUB_VENV_DIR" ]]; then
790
+ mv "$HUB_VENV_DIR" "$old_dir" || return 1
791
+ moved_old=1
792
+ fi
793
+ mv "$new_dir" "$HUB_VENV_DIR" || return 1
794
+ installed=1
795
+ )
796
+
797
+ _publish_hub_python_wrapper() (
798
+ local local_bin="$HOME/.local/bin" wrapper="$HOME/.local/bin/hub-python3"
799
+ local staged content wrapper_content
800
+ local marker='# hub-agent managed: hub-python3 (hub-agent のインストーラが生成・更新します。手で編集しないでください)'
801
+ local managed_line="exec \"$HUB_VENV_DIR/bin/python3\" \"\$@\""
802
+ mkdir -p "$local_bin"
803
+ if [[ -L "$wrapper" ]]; then
804
+ color_err "$wrapper は名前衝突しています。既存 symlink を退避するか PATH を修正してください"
805
+ return 1
806
+ fi
807
+ staged="$(mktemp "$local_bin/.hub-python3.tmp.XXXXXX")" || return 1
808
+ trap 'rm -f "$staged"' EXIT
809
+ printf '%s\n' '#!/usr/bin/env sh' "$marker" "$managed_line" > "$staged" || return 1
810
+ chmod +x "$staged" || return 1
811
+ # ⚠️ `$(<file)` は末尾の改行を落とすので、この比較は「末尾改行の数の違い」と「最終行の改行の
812
+ # 有無」を区別しない。内容のある行が 1 行でも違えば不一致になり、末尾改行だけが違う wrapper を
813
+ # 上書きしても失われる情報は無いので許容する。サンドボックスに cat / stat / wc / cmp が無く、
814
+ # バイト単位の比較には別の機構が要るため、そのために機構を足すことはしない。
815
+ # 以前付けていた sentinel の "x" は末尾改行の差を捉えているように見えて実際は効いていなかった
816
+ # (実測: `$(<a)x` は "a\n" と "a\n\n\n" と "a" のすべてで同じ値になる)ので外した。
817
+ content="$(<"$staged")"
818
+ [[ "$content" == $'#!/usr/bin/env sh\n'"$marker"$'\n'"$managed_line" ]] || return 1
819
+ _hub_python_wrapper_is_managed() {
820
+ [[ -f "$wrapper" && ! -L "$wrapper" ]] || return 1
821
+ wrapper_content="$(<"$wrapper")"
822
+ [[ "$wrapper_content" == "$content" ]] || return 1
823
+ # 未公開ブランチにしか存在しなかった marker なしの旧形は受け入れない。将来内容を変える版では
824
+ # marker で識別し、既知の旧内容を完全一致で列挙する。パスのワイルドカード許可には戻さない。
825
+ # marker をコピーした同一ユーザーの別 wrapper も管理物とみなす。脅威モデルは攻撃者ではなく、
826
+ # 同一ユーザーによる並行実行と中断であるため、このなりすまし耐性は担保しない。
827
+ }
828
+ if [[ -e "$wrapper" ]] && ! _hub_python_wrapper_is_managed; then
829
+ color_err "$wrapper は名前衝突しています。既存ファイルを退避するか PATH を修正してください"
830
+ return 1
831
+ fi
832
+ if [[ -L "$wrapper" ]] || { [[ -e "$wrapper" ]] && ! _hub_python_wrapper_is_managed; }; then
833
+ color_err "$wrapper は公開直前に名前衝突しました。既存ファイルを退避するか PATH を修正してください"
834
+ return 1
835
+ fi
836
+ mv "$staged" "$wrapper" || return 1
837
+ )
838
+
839
+ # lock がまだ自分のものかを確かめる。これは相互排除ではなく best-effort の検出であり、確認と
840
+ # 操作の間の TOCTOU の窓は残る。自動回収の削除後に自分の lock を失うのは、人が lock
841
+ # ディレクトリを削除して別のインストーラを起動した場合だけなので、そのとき黙って並行実行しない
842
+ # ために破壊的操作の直前で検出する。
843
+ _hub_python_lock_is_mine() {
844
+ local lock_dir="$1" want="$2" owner_token=""
845
+ [[ -n "$want" && -f "$lock_dir/owner" ]] || return 1
846
+ read -r owner_token < "$lock_dir/owner" || true
847
+ [[ "$owner_token" == "$want" ]]
848
+ }
849
+
850
+ _hub_python_wrapper_verified() {
851
+ # ⚠️ realpath(sys.executable) で比較しないこと。venv の bin/python3 は土台 interpreter への
852
+ # symlink なので、realpath は venv でも素の土台でも同じ値に解決される(実測)。それでは
853
+ # 「site-packages を持たない素の uv interpreter を指す wrapper」を成功扱いにしてしまう。
854
+ # 識別に使えるのは sys.prefix(venv 自身のパス)。
855
+ local expected_prefix actual_prefix spec
856
+ expected_prefix="$(cd "$HUB_VENV_DIR" 2>/dev/null && pwd -P)" || expected_prefix=""
857
+ actual_prefix="$(hub-python3 -c 'import os, sys; print(os.path.realpath(sys.prefix))' 2>/dev/null)" || actual_prefix=""
858
+ if [[ -z "$expected_prefix" || "$actual_prefix" != "$expected_prefix" ]]; then
859
+ color_err "hub-python3 が Hub Python venv を指していません (expected prefix: ${expected_prefix:-unknown}, actual: ${actual_prefix:-unknown})"
860
+ return 1
861
+ fi
862
+ # 版一致だけでは wheel の破損を見逃すため、wrapper 経由で実際に import できることまで確認する。
863
+ # ⚠️ この import 検査は `_hub_venv_usable` の import 検査と同じ interpreter を通るため重複しており、
864
+ # 独立した回帰テストは無い(wrapper 側だけを失敗させる状態を作れない)。担保は上の prefix 比較で、
865
+ # ここは公開後の wrapper 経路を一度通す念のための確認だと理解して読むこと。
866
+ for spec in "${HUB_PYTHON_IMPORTS[@]}"; do
867
+ if ! hub-python3 -c 'import importlib, sys; importlib.import_module(sys.argv[1])' "$spec" >/dev/null 2>&1; then
868
+ color_err "hub-python3 経由で $spec を import できません"
869
+ return 1
870
+ fi
871
+ done
872
+ color_ok "hub-python3 が Hub Python venv を指すことを確認"
873
+ }
874
+
875
+ _finalize_hub_python() (
876
+ # 退避先は lock token ごとの実行固有名にし、他実行の退避物と削除対象を分離する。
877
+ local new_dir="$1" old_dir="" leftover="" found_leftover=0
878
+ local lock_dir="${HUB_VENV_DIR}.lock" lock_token="" owner_token=""
879
+ trap 'rm -rf "$new_dir"; owner_token=""; if [[ -f "$lock_dir/owner" ]]; then read -r owner_token < "$lock_dir/owner" || true; fi; if [[ -n "$lock_token" && "$owner_token" == "$lock_token" ]]; then rm -rf "$lock_dir"; fi' EXIT
880
+ trap 'exit 130' INT
881
+ trap 'exit 143' TERM
882
+ _acquire_hub_python_lock "$lock_dir" || exit 1
883
+ lock_token="$HUB_PYTHON_LOCK_TOKEN"
884
+ old_dir="${HUB_VENV_DIR}.old.${lock_token}"
885
+ if _hub_venv_usable; then
886
+ color_ok "Hub Python venv は利用可能 ($HUB_VENV_DIR)"
887
+ elif [[ -n "$new_dir" ]]; then
888
+ if ! _hub_python_lock_is_mine "$lock_dir" "$lock_token"; then
889
+ color_err "Hub Python lock を他のインストーラに引き継がれたため差し替えを中止しました: $lock_dir"
890
+ exit 1
891
+ fi
892
+ # 残骸を別名で溜めず、stale lock と同様に手動復旧へ統一する。
893
+ # ⚠️ ここへ来るのは公開先が使えないと判定した場合なので、残骸は「直前まで使えていた venv」で
894
+ # ある可能性が高い。削除だけを案内せず、戻す選択肢を先に示す。
895
+ for leftover in "${HUB_VENV_DIR}".old*; do
896
+ if [[ -e "$leftover" || -L "$leftover" ]]; then
897
+ color_err "退避先が既に存在します: $leftover"
898
+ found_leftover=1
899
+ fi
900
+ done
901
+ if (( found_leftover == 1 )); then
902
+ color_err "前回の中断が残っている可能性があります。直前まで使えていた venv かもしれません。中身を確認して $HUB_VENV_DIR へ手で入れ替えるか、不要なら削除してから再実行してください"
903
+ exit 1
904
+ fi
905
+ _replace_hub_venv "$new_dir" "$old_dir" || exit 1
906
+ color_ok "Hub Python venv を構築 ($HUB_VENV_DIR)"
907
+ else
908
+ color_err "Hub Python venv の再構築結果がありません"
909
+ exit 1
910
+ fi
911
+ if ! _hub_python_lock_is_mine "$lock_dir" "$lock_token"; then
912
+ color_err "Hub Python lock を他のインストーラに引き継がれたため wrapper の公開を中止しました: $lock_dir"
913
+ exit 1
914
+ fi
915
+ _publish_hub_python_wrapper || exit 1
916
+ if ! _hub_python_wrapper_verified; then
917
+ if [[ -e "$old_dir" || -L "$old_dir" ]]; then
918
+ color_err "公開後の検証に失敗しました。公開先 $HUB_VENV_DIR は変更していません。直前まで使えていた venv は $old_dir にあります。中身を確認して手で入れ替えるか、$old_dir を削除してから再実行してください"
919
+ else
920
+ color_err "公開後の検証に失敗しました。公開先 $HUB_VENV_DIR は変更していません。手で確認してください"
921
+ fi
922
+ exit 1
923
+ fi
924
+ # 公開後の最終検証に成功してから、lock を保持したまま実行固有名の退避物を削除する。
925
+ # 差し替えを行わなかった実行ではこのパスは存在せず、他実行が残した退避物は削除しない。
926
+ if ! rm -rf "$old_dir" 2>/dev/null; then
927
+ color_warn "元の venv を削除できませんでした: $old_dir。中身を確認して手動で削除してください"
928
+ fi
929
+ )
930
+
931
+ ensure_hub_python() {
932
+ local local_bin="$HOME/.local/bin" uv_path
933
+ export PATH="$PATH:$local_bin"
934
+ hash -r 2>/dev/null || true
935
+ if present uv; then
936
+ uv_path="$(command -v uv)"
937
+ uv_path="$(cd "$(dirname "$uv_path")" && pwd -P)/$(basename "$uv_path")"
938
+ if ! uv --version >/dev/null 2>&1 || ! uv venv --help >/dev/null 2>&1 \
939
+ || ! uv pip --help >/dev/null 2>&1 || ! uv python --help >/dev/null 2>&1; then
940
+ color_err "発見した uv を利用できません: $uv_path(上書きせず停止します)"
941
+ exit 1
942
+ fi
943
+ color_ok "uv already installed ($uv_path)"
944
+ else
945
+ _install_hub_uv || exit 1
946
+ color_ok "uv $UV_VERSION を $local_bin に導入"
947
+ fi
948
+ persist_profile_snippet 'export PATH="$PATH:$HOME/.local/bin"' "~/.local/bin PATH" \
949
+ "# >>> hub-agent: local-bin PATH (Hub Python) >>>" "# <<< hub-agent: local-bin PATH <<<" \
950
+ "$HOME/.zprofile" "$(bash_login_profile)" || exit 1
951
+
952
+ retry 3 uv python install "$HUB_PYTHON_MINOR" || exit 1
953
+ if _hub_venv_usable; then
954
+ _finalize_hub_python "" || exit 1
955
+ else
956
+ # ⚠️ 構築成功から finalize の trap 設置までの中断で未公開の .new が残らないよう、
957
+ # 構築と finalize を同じ cleanup スコープに入れる。差し替え成功後は .new のパスが
958
+ # 消えているので、この trap の rm は no-op になる。
959
+ (
960
+ new_dir="${HUB_VENV_DIR}.new.$$"
961
+ trap 'rm -rf "$new_dir"' EXIT
962
+ trap 'exit 130' INT
963
+ trap 'exit 143' TERM
964
+ _build_hub_venv "$new_dir" || exit 1
965
+ _finalize_hub_python "$new_dir" || exit 1
966
+ ) || exit 1
967
+ fi
968
+ # 公開後の検証成功後にだけ、Hub Python を指す profile を更新する。
969
+ export HUB_AI_PYTHON="$HUB_VENV_DIR/bin/python3"
970
+ persist_profile_snippet "export HUB_AI_PYTHON=\"$HUB_VENV_DIR/bin/python3\"" "HUB_AI_PYTHON" \
971
+ "# >>> hub-agent: HUB_AI_PYTHON >>>" "# <<< hub-agent: HUB_AI_PYTHON <<<" \
972
+ "$HOME/.zprofile" "$(bash_login_profile)" || exit 1
973
+ }
974
+
624
975
  # timeout を保証する(補助コマンド。AI セッションの Bash やスクリプトが使うが、
625
976
  # hub-agent の常駐動作には必須ではないため、導入失敗は警告して継続する。
626
977
  # 必須ランタイムの python3 とは契約を分けている)。
@@ -1042,7 +1393,7 @@ EOF
1042
1393
 
1043
1394
  main() {
1044
1395
  color_step "hub-agent ワンライナーセットアップを開始"
1045
- STEP_TOTAL=15
1396
+ STEP_TOTAL=16
1046
1397
  STEP_NUM=0
1047
1398
 
1048
1399
  step_header "Homebrew"
@@ -1055,6 +1406,8 @@ main() {
1055
1406
  ensure_build_tools
1056
1407
  step_header "Python 3"
1057
1408
  ensure_python3
1409
+ step_header "Hub Python (uv / shared venv)"
1410
+ ensure_hub_python
1058
1411
  step_header "GNU coreutils (timeout)"
1059
1412
  ensure_timeout
1060
1413
  step_header "npm global prefix"