@cocorograph/hub-agent 0.8.65 → 0.8.67
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/bin/hub-agent.mjs +6 -9
- package/package.json +1 -1
- package/scripts/install.sh +358 -2
- package/src/service-install.mjs +140 -15
package/bin/hub-agent.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import { loadPlugins } from "../src/plugin-loader.mjs"
|
|
|
38
38
|
import { uninstallPlugin } from "../src/plugin-uninstall.mjs"
|
|
39
39
|
import {
|
|
40
40
|
canSuggestForcedRestart,
|
|
41
|
+
formatInstallServiceReport,
|
|
41
42
|
installService,
|
|
42
43
|
restartService,
|
|
43
44
|
restartWithPolicy,
|
|
@@ -215,16 +216,12 @@ program
|
|
|
215
216
|
.action(async (opts) => {
|
|
216
217
|
try {
|
|
217
218
|
const r = await installService({ bin: opts.bin })
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.
|
|
222
|
-
console.log(
|
|
223
|
-
} else {
|
|
224
|
-
console.log(`unit: ${r.unit}`)
|
|
225
|
-
console.log("確認: systemctl --user status hub-agent.service")
|
|
219
|
+
// 表示と警告の判断は formatInstallServiceReport (テスト可能な純関数) が持つ。
|
|
220
|
+
// 順序を崩さないよう、種別つきの 1 本の配列をそのまま流す。
|
|
221
|
+
for (const { stream, text } of formatInstallServiceReport(r)) {
|
|
222
|
+
if (stream === "warn") console.warn(text)
|
|
223
|
+
else console.log(text)
|
|
226
224
|
}
|
|
227
|
-
console.log("ログ: ~/.hub/agent.log")
|
|
228
225
|
} catch (err) {
|
|
229
226
|
console.error(`install-service failed: ${err.message}`)
|
|
230
227
|
process.exit(1)
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -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 とは契約を分けている)。
|
|
@@ -996,8 +1347,11 @@ verify_setup() {
|
|
|
996
1347
|
done < <(_hub_target_clis)
|
|
997
1348
|
|
|
998
1349
|
# 4. OS サービス起動 (Darwin 限定の確認。Linux は systemctl --user で別途)
|
|
1350
|
+
# ⚠️ 判定は `launchctl print` で行う。install-service 側が登録の出現を待つのに使う
|
|
1351
|
+
# 道具と揃えるため (src/service-install.mjs の waitUntilBootedIn)。`launchctl list | grep`
|
|
1352
|
+
# に戻すと、待つ側と確認する側で別の状態を見ることになり誤検知が復活する。
|
|
999
1353
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
1000
|
-
if launchctl
|
|
1354
|
+
if launchctl print "gui/$(id -u)/co.cocorograph.hub-agent" >/dev/null 2>&1; then
|
|
1001
1355
|
color_ok "launchd service 起動中 (co.cocorograph.hub-agent)"
|
|
1002
1356
|
else
|
|
1003
1357
|
color_warn "launchd service が見つかりません"
|
|
@@ -1042,7 +1396,7 @@ EOF
|
|
|
1042
1396
|
|
|
1043
1397
|
main() {
|
|
1044
1398
|
color_step "hub-agent ワンライナーセットアップを開始"
|
|
1045
|
-
STEP_TOTAL=
|
|
1399
|
+
STEP_TOTAL=16
|
|
1046
1400
|
STEP_NUM=0
|
|
1047
1401
|
|
|
1048
1402
|
step_header "Homebrew"
|
|
@@ -1055,6 +1409,8 @@ main() {
|
|
|
1055
1409
|
ensure_build_tools
|
|
1056
1410
|
step_header "Python 3"
|
|
1057
1411
|
ensure_python3
|
|
1412
|
+
step_header "Hub Python (uv / shared venv)"
|
|
1413
|
+
ensure_hub_python
|
|
1058
1414
|
step_header "GNU coreutils (timeout)"
|
|
1059
1415
|
ensure_timeout
|
|
1060
1416
|
step_header "npm global prefix"
|
package/src/service-install.mjs
CHANGED
|
@@ -154,6 +154,29 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
154
154
|
* @returns {Promise<boolean>} 消えたら true / timeout なら false
|
|
155
155
|
*/
|
|
156
156
|
async function waitUntilBootedOut(uid, opts = {}) {
|
|
157
|
+
// 非 0 = 登録に存在しない = bootout 完了
|
|
158
|
+
return waitForRegistration(uid, false, opts)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* launchd の登録状態が `want` になるまで polling する。`waitUntilBootedOut` /
|
|
163
|
+
* `waitUntilBootedIn` の共通実装。
|
|
164
|
+
*
|
|
165
|
+
* ⚠️ 判定は `launchctl print` の終了コードで行う。`launchctl list | grep` は同じ状態を
|
|
166
|
+
* 別の道具で見ることになり、待つ側と確認する側がずれると誤検知が復活するため、
|
|
167
|
+
* hub-agent 側と `scripts/install.sh` の verify_setup で道具を揃えている。
|
|
168
|
+
*
|
|
169
|
+
* ⚠️ 成功条件は `(status === 0) === want` の 1 箇所だけで表す。`status` は spawn 自体が
|
|
170
|
+
* 失敗すると `null` になり得るので、「0 か否か」に正規化してから比較する
|
|
171
|
+
* (bootout 待ちでは null は「登録に存在しない」=完了、bootin 待ちでは「まだ現れない」
|
|
172
|
+
* =継続、という従来の挙動がそのまま保たれる)。
|
|
173
|
+
*
|
|
174
|
+
* @param {number} uid
|
|
175
|
+
* @param {boolean} want true = 登録に現れるまで待つ / false = 消えるまで待つ
|
|
176
|
+
* @param {{ timeoutMs?: number, intervalMs?: number, spawn?: Function, sleep?: Function, now?: Function }} [opts]
|
|
177
|
+
* @returns {Promise<boolean>} 期待状態になったら true / timeout なら false
|
|
178
|
+
*/
|
|
179
|
+
async function waitForRegistration(uid, want, opts = {}) {
|
|
157
180
|
const timeoutMs = opts.timeoutMs ?? 5000
|
|
158
181
|
const intervalMs = opts.intervalMs ?? 200
|
|
159
182
|
const spawn = opts.spawn ?? spawnSync
|
|
@@ -164,13 +187,33 @@ async function waitUntilBootedOut(uid, opts = {}) {
|
|
|
164
187
|
const r = spawn("launchctl", ["print", `gui/${uid}/${PLIST_LABEL}`], {
|
|
165
188
|
stdio: "ignore",
|
|
166
189
|
})
|
|
167
|
-
|
|
168
|
-
if (r.status !== 0) return true
|
|
190
|
+
if ((r.status === 0) === want) return true
|
|
169
191
|
await sleepFn(intervalMs)
|
|
170
192
|
}
|
|
171
193
|
return false
|
|
172
194
|
}
|
|
173
195
|
|
|
196
|
+
/**
|
|
197
|
+
* `launchctl bootstrap` / `kickstart -k` の直後は、launchd 側の登録が見えるまでに
|
|
198
|
+
* わずかな遅れがある。install-service がそこで返ってしまうと、直後に登録を確認する
|
|
199
|
+
* 呼び出し側 (`scripts/install.sh` の verify_setup 等) が「サービスが見つからない」と
|
|
200
|
+
* 誤検知する。本関数は対象 service が launchd の登録に現れるまで polling して待つ。
|
|
201
|
+
* `waitUntilBootedOut` の対称形。
|
|
202
|
+
*
|
|
203
|
+
* 判定手段の統一 (`launchctl print`) は `waitForRegistration` の注意書きを参照。
|
|
204
|
+
*
|
|
205
|
+
* (2026-09-09: ワンライナー完走時に毎回「launchd service が見つかりません」が出ていた
|
|
206
|
+
* 誤検知の修正。install-service は成功しており、agent も起動していた)
|
|
207
|
+
*
|
|
208
|
+
* @param {number} uid
|
|
209
|
+
* @param {{ timeoutMs?: number, intervalMs?: number }} [opts]
|
|
210
|
+
* @returns {Promise<boolean>} 現れたら true / timeout なら false
|
|
211
|
+
*/
|
|
212
|
+
async function waitUntilBootedIn(uid, opts = {}) {
|
|
213
|
+
// 0 = 登録に存在する
|
|
214
|
+
return waitForRegistration(uid, true, opts)
|
|
215
|
+
}
|
|
216
|
+
|
|
174
217
|
/**
|
|
175
218
|
* bootout 後の bootstrap を、過渡的失敗 (旧サービスの抜け残り等) に対して
|
|
176
219
|
* リトライする。`run` と違い最終的に失敗したときだけ throw する。
|
|
@@ -257,7 +300,52 @@ function detectHubAgentBin() {
|
|
|
257
300
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "hub-agent.mjs")
|
|
258
301
|
}
|
|
259
302
|
|
|
260
|
-
|
|
303
|
+
/**
|
|
304
|
+
* darwin の launchd 登録シーケンス:
|
|
305
|
+
* bootout → (登録が消えるまで待機) → bootstrap (リトライ) → kickstart → (登録が現れるまで待機)。
|
|
306
|
+
*
|
|
307
|
+
* - bootout は非同期なので、待たずに bootstrap するとレースで失敗し offline 化する
|
|
308
|
+
* (2026-05-30 の事故)。`waitUntilBootedOut` で確実に抜けてから bootstrap する。
|
|
309
|
+
* - kickstart は「既に走っている daemon を再起動」する用途。bootstrap + RunAtLoad で
|
|
310
|
+
* 既に起動済みのことがあり、その場合 kickstart 失敗は致命的でないので throw しない。
|
|
311
|
+
* - kickstart もまた launchd 側の登録が見えるまで待たない。ここで待たずに返すと、直後に
|
|
312
|
+
* 登録を確認する呼び出し側 (`scripts/install.sh` の verify_setup 等) が誤検知する
|
|
313
|
+
* (2026-09-09 の誤検知)。現れなかった場合も throw しない: bootstrap は成功しているので
|
|
314
|
+
* 致命ではなく、判断は呼び出し側の確認へ委ねる。
|
|
315
|
+
* ⚠️ ただし「確認できなかった」事実は捨てない。非 0 で終わると `scripts/install.sh` の
|
|
316
|
+
* `retry 2 hub-agent install-service` が再インストール + 再起動を走らせてしまうので、
|
|
317
|
+
* exit code は変えず `registered` で返し、CLI 側は警告だけ出す。
|
|
318
|
+
*
|
|
319
|
+
* ⚠️ この順序をソース文字列の走査で固定するのは**やめた** (2026-09-09 cross-review 3 巡目)。
|
|
320
|
+
* コメント化・別オブジェクトへの退避など、文字列一致では追い切れない後退が巡ごとに出たため、
|
|
321
|
+
* 依存を差し替えて**実際に順序と返り値を確認できる**形へ切り出してある。同ファイルの
|
|
322
|
+
* `restartWithPolicy` と同じ考え方。`deps` は本番では既定値のまま使う。
|
|
323
|
+
*
|
|
324
|
+
* @param {{uid: number, dest: string, bin: string}} target
|
|
325
|
+
* @param {{spawn?: Function, bootedOut?: Function, bootstrap?: Function, bootedIn?: Function}} [deps]
|
|
326
|
+
*/
|
|
327
|
+
export async function activateDarwinService({ uid, dest, bin }, deps = {}) {
|
|
328
|
+
const spawn = deps.spawn ?? spawnSync
|
|
329
|
+
const bootedOut = deps.bootedOut ?? waitUntilBootedOut
|
|
330
|
+
const bootstrap = deps.bootstrap ?? bootstrapWithRetry
|
|
331
|
+
const bootedIn = deps.bootedIn ?? waitUntilBootedIn
|
|
332
|
+
|
|
333
|
+
spawn("launchctl", ["bootout", `gui/${uid}`, dest], { stdio: "ignore" })
|
|
334
|
+
await bootedOut(uid)
|
|
335
|
+
await bootstrap(uid, dest)
|
|
336
|
+
spawn("launchctl", ["kickstart", "-k", `gui/${uid}/${PLIST_LABEL}`], {
|
|
337
|
+
stdio: "ignore",
|
|
338
|
+
})
|
|
339
|
+
const registered = await bootedIn(uid)
|
|
340
|
+
return { platform: "darwin", path: dest, label: PLIST_LABEL, bin, registered }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {{bin?: string, activate?: Function}} [opts]
|
|
345
|
+
* `activate` は darwin の launchd 登録シーケンス。テストから差し替えるための口で、
|
|
346
|
+
* 本番では既定 (`activateDarwinService`) のまま使う。
|
|
347
|
+
*/
|
|
348
|
+
export async function installService({ bin, activate = activateDarwinService } = {}) {
|
|
261
349
|
const hubAgentBin = bin || detectHubAgentBin()
|
|
262
350
|
await ensureLogFile()
|
|
263
351
|
|
|
@@ -269,18 +357,7 @@ export async function installService({ bin } = {}) {
|
|
|
269
357
|
await fs.writeFile(dest, expanded, { mode: 0o644 })
|
|
270
358
|
|
|
271
359
|
const uid = ensureUnixUid()
|
|
272
|
-
|
|
273
|
-
// bootout は非同期なので待たずに bootstrap するとレースで失敗し offline 化する
|
|
274
|
-
// (2026-05-30 の事故)。waitUntilBootedOut で確実に抜けてから bootstrap する。
|
|
275
|
-
spawnSync("launchctl", ["bootout", `gui/${uid}`, dest], { stdio: "ignore" })
|
|
276
|
-
await waitUntilBootedOut(uid)
|
|
277
|
-
await bootstrapWithRetry(uid, dest)
|
|
278
|
-
// kickstart は「既に走っている daemon を再起動」する用途。bootstrap + RunAtLoad で
|
|
279
|
-
// 既に起動済みのことがあり、その場合 kickstart 失敗は致命的でないので throw しない。
|
|
280
|
-
spawnSync("launchctl", ["kickstart", "-k", `gui/${uid}/${PLIST_LABEL}`], {
|
|
281
|
-
stdio: "ignore",
|
|
282
|
-
})
|
|
283
|
-
return { platform: "darwin", path: dest, label: PLIST_LABEL, bin: hubAgentBin }
|
|
360
|
+
return activate({ uid, dest, bin: hubAgentBin })
|
|
284
361
|
}
|
|
285
362
|
|
|
286
363
|
if (process.platform === "linux") {
|
|
@@ -389,6 +466,50 @@ export async function restartWithPolicy({
|
|
|
389
466
|
return { mode: "deferred", result: await deferredRestart(), service }
|
|
390
467
|
}
|
|
391
468
|
|
|
469
|
+
/**
|
|
470
|
+
* install-service の結果を CLI 表示へ変換する。`restartWithPolicy` と同じく、
|
|
471
|
+
* CLI 側の判断をテスト可能な純関数へ切り出したもの (bin/hub-agent.mjs から使う)。
|
|
472
|
+
*
|
|
473
|
+
* ⚠️ 確認コマンドは launchd の登録を待つ側 (`waitForRegistration`) と
|
|
474
|
+
* `scripts/install.sh` の verify_setup に揃える。`launchctl list | grep` へ戻すと、
|
|
475
|
+
* 同じ状態を 3 箇所が別々の道具で見ることになり、誤検知が復活する。
|
|
476
|
+
*
|
|
477
|
+
* ⚠️ 警告は `registered === false` のときだけ。bootstrap 自体は成功しているので
|
|
478
|
+
* exit code は変えない (非 0 にすると `scripts/install.sh` の `retry 2` が
|
|
479
|
+
* 再インストール + 再起動を走らせてしまう)。linux は登録待ちを持たず `registered` を
|
|
480
|
+
* 返さないので、`undefined` で警告しないこと。
|
|
481
|
+
*
|
|
482
|
+
* @param {{platform: string, path: string, bin: string, label?: string, unit?: string, registered?: boolean}} result
|
|
483
|
+
* @returns {{stream: 'log'|'warn', text: string}[]} 出力すべき行を順序どおりに
|
|
484
|
+
*/
|
|
485
|
+
export function formatInstallServiceReport(result) {
|
|
486
|
+
const out = [
|
|
487
|
+
{ stream: "log", text: `installed: ${result.platform} ${result.path}` },
|
|
488
|
+
{ stream: "log", text: `bin: ${result.bin}` },
|
|
489
|
+
]
|
|
490
|
+
if (result.platform === "darwin") {
|
|
491
|
+
out.push({ stream: "log", text: `label: ${result.label}` })
|
|
492
|
+
out.push({
|
|
493
|
+
stream: "log",
|
|
494
|
+
text: "確認: launchctl print gui/$(id -u)/co.cocorograph.hub-agent",
|
|
495
|
+
})
|
|
496
|
+
if (result.registered === false) {
|
|
497
|
+
// ⚠️ 警告は確認コマンドの直後に置く (読む人が対処法とセットで見るため)。
|
|
498
|
+
// 種別つきの 1 本の配列で返すのは、この順序を stdout / stderr の分離と
|
|
499
|
+
// 両立させるため。呼び出し側で 2 本のリストへ分けると順序が崩れる。
|
|
500
|
+
out.push({
|
|
501
|
+
stream: "warn",
|
|
502
|
+
text: "warn: launchd への登録を確認できませんでした (待機 timeout)。上のコマンドで確認してください",
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
out.push({ stream: "log", text: `unit: ${result.unit}` })
|
|
507
|
+
out.push({ stream: "log", text: "確認: systemctl --user status hub-agent.service" })
|
|
508
|
+
}
|
|
509
|
+
out.push({ stream: "log", text: "ログ: ~/.hub/agent.log" })
|
|
510
|
+
return out
|
|
511
|
+
}
|
|
512
|
+
|
|
392
513
|
export async function uninstallService() {
|
|
393
514
|
if (process.platform === "darwin") {
|
|
394
515
|
const uid = ensureUnixUid()
|
|
@@ -498,6 +619,10 @@ export const _internal = {
|
|
|
498
619
|
linuxUnitPath,
|
|
499
620
|
repoTemplatesDir,
|
|
500
621
|
waitUntilBootedOut,
|
|
622
|
+
waitUntilBootedIn,
|
|
623
|
+
waitForRegistration,
|
|
624
|
+
formatInstallServiceReport,
|
|
625
|
+
activateDarwinService,
|
|
501
626
|
bootstrapWithRetry,
|
|
502
627
|
currentUsername,
|
|
503
628
|
enableLinger,
|