@tyhld/conductor 0.3.0 → 0.7.0

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 (46) hide show
  1. package/README.md +301 -19
  2. package/dist/cli.js +143 -18
  3. package/dist/ear-routing.js +57 -0
  4. package/dist/ear.js +57 -0
  5. package/dist/env-file-perm.js +67 -0
  6. package/dist/nudge.js +172 -0
  7. package/dist/realtime-parse.js +116 -0
  8. package/dist/realtime.js +251 -0
  9. package/dist/relay-runner.js +65 -0
  10. package/dist/relay.js +911 -134
  11. package/dist/websocket-transport.js +66 -0
  12. package/launchd/ear-install.sh +96 -0
  13. package/package.json +30 -1
  14. package/sales-template/README.md +185 -25
  15. package/sales-template/install.sh +890 -0
  16. package/sales-template/launchd/install.sh +151 -0
  17. package/sales-template/settings.json +26 -97
  18. package/sales-template/setup.sh +754 -117
  19. package/sales-template/systemd/README.md +28 -4
  20. package/sales-template/systemd/install.sh +54 -5
  21. package/sales-template/systemd/paste-cache-prune-install.sh +75 -0
  22. package/sales-template/systemd/tyhld-paste-cache-prune.service +25 -0
  23. package/sales-template/systemd/tyhld-paste-cache-prune.timer +19 -0
  24. package/sales-template/uninstall.sh +245 -0
  25. package/scripts/hooks/README.md +246 -0
  26. package/scripts/hooks/cc2_guard.py +145 -0
  27. package/scripts/hooks/codex-hooks.sample.json +58 -0
  28. package/scripts/hooks/hook_datalink.py +440 -0
  29. package/scripts/hooks/install-codex-hooks.sh +127 -0
  30. package/scripts/hooks/notification_hook.py +167 -0
  31. package/scripts/hooks/permission_request_hook.py +207 -0
  32. package/scripts/hooks/policy.py +759 -0
  33. package/scripts/hooks/settings.sample.json +142 -0
  34. package/scripts/hooks/stop_hook.py +275 -0
  35. package/scripts/hooks/summary_ja.py +155 -0
  36. package/scripts/hooks/test_hook_datalink.py +282 -0
  37. package/scripts/hooks/test_policy.py +1241 -0
  38. package/skills/conductor-craftsman/SKILL.md +40 -0
  39. package/systemd/conductor-ear.service +63 -0
  40. package/systemd/conductor@.service +62 -0
  41. package/systemd/ear-install.sh +131 -0
  42. package/systemd/guard-sync-install.sh +94 -0
  43. package/systemd/tyhld-guard-sync.service +28 -0
  44. package/systemd/tyhld-guard-sync.timer +25 -0
  45. package/sales-template/cc2_guard.py +0 -395
  46. package/sales-template/systemd/conductor@.service +0 -47
@@ -25,10 +25,24 @@ systemd が conductor@<プロジェクト> を自動起動
25
25
 
26
26
  | ファイル | 役割 |
27
27
  |----------|------|
28
- | `conductor@.service` | systemd ユニットファイル(テンプレート) |
29
- | `install.sh` | インストーラ(node検出→symlink→配置→enable→linger) |
28
+ | `install.sh` | 現場ごとの常駐のインストーラ(node検出→symlink→配置→enable→linger) |
29
+ | `paste-cache-prune-install.sh` | 貼り付け一時ファイルの毎日掃除タイマー(PCに1本) |
30
30
  | `README.md` | このファイル |
31
31
 
32
+ ユニットファイルの**正本は配布物の `systemd/` に1つだけ**置いています(写しをここに持ちません。
33
+ 2箇所に同じものがあると、片方だけ直したときに中身がズレるためです):
34
+
35
+ | ユニット | 本数 | 役割 | 入れる処理 |
36
+ |---|---|---|---|
37
+ | `systemd/conductor@.service` | 現場ごとに1本 | heartbeat + relay(指示の配達・完了検出) | このフォルダの `install.sh` |
38
+ | `systemd/conductor-ear.service` | **PCに1本** | 指示の着信合図を受け、各現場の relay を前倒しする「耳」 | `systemd/ear-install.sh`(`setup.sh` が自動で呼びます) |
39
+
40
+ `ExecStart` は配置時にインストーラが「その機で実際に動く `conductor` のフルパス」へ置換します。
41
+ ユニットをそのままコピーしても動きません(必ずインストーラ経由で入れてください)。
42
+
43
+ **耳が入らなくてもセットアップは止まりません。** 耳は「指示が届くまでの最大15秒の待ちを消す」加速装置で、
44
+ 入らない機でも各現場の relay は15秒ごとの確認で従来どおり完全に動きます。
45
+
32
46
  **conductor-orchestrator.service は含まない**: ADR-001 の決定により CC1B(LLM判断層)は撤去。orchestrator サービスは販売構成では不要。
33
47
 
34
48
  ## セットアップ手順
@@ -45,7 +59,7 @@ systemd が conductor@<プロジェクト> を自動起動
45
59
  ```bash
46
60
  cat > "$HOME/.conductor.env" <<'EOF'
47
61
  CONDUCTOR_URL=https://<采配くん管制サーバURL>
48
- CONDUCTOR_SHARED_SECRET=<シークレット>
62
+ CONDUCTOR_TOKEN=<トークン>
49
63
  EOF
50
64
  chmod 600 "$HOME/.conductor.env"
51
65
  ```
@@ -118,7 +132,7 @@ install.sh はシェルで使っている node の実体パスを検出し、
118
132
 
119
133
  | 項目 | 説明 | 例 |
120
134
  |------|------|-----|
121
- | `~/.conductor.env` | 管制サーバへの接続情報 | `CONDUCTOR_URL=https://...` / `CONDUCTOR_SHARED_SECRET=...` |
135
+ | `~/.conductor.env` | 管制サーバへの接続情報 | `CONDUCTOR_URL=https://...` / `CONDUCTOR_TOKEN=...` |
122
136
  | プロジェクト名 | install.sh の第1引数 | `my-app` |
123
137
  | プロジェクトディレクトリ | install.sh の第2引数(既定は `~/projects/<名前>`) | `/home/user/work/my-app` |
124
138
  | sudo パスワード | symlink 作成時に必要(初回のみ) | — |
@@ -145,6 +159,16 @@ rm -rf ~/.config/systemd/user/conductor@my-app.service.d
145
159
  systemctl --user daemon-reload
146
160
  ```
147
161
 
162
+ 耳(PCに1本)はこちら:
163
+
164
+ ```bash
165
+ systemctl --user status conductor-ear # 状態確認
166
+ journalctl --user -u conductor-ear -f # ログ確認
167
+ systemctl --user restart conductor-ear # 再起動
168
+ systemctl --user disable --now conductor-ear # 停止(relay は15秒ごとの確認で動き続けます)
169
+ bash ../../systemd/ear-install.sh # 入れ直す
170
+ ```
171
+
148
172
  ## トラブルシューティング
149
173
 
150
174
  ### サービスが起動しない
@@ -18,7 +18,7 @@
18
18
  #
19
19
  # 前提:
20
20
  # - Linux で systemd が有効(WSL の場合: /etc/wsl.conf の [boot] systemd=true)。
21
- # - ~/.conductor.env が用意済み(CONDUCTOR_URL / CONDUCTOR_SHARED_SECRET)。
21
+ # - ~/.conductor.env が用意済み(CONDUCTOR_URL / CONDUCTOR_TOKEN)。
22
22
  # - @tyhld/conductor が npm グローバルインストール済み。
23
23
  # - node がインストール済み(fnm / nvm / volta / システムパッケージ いずれか)。
24
24
  # - sudo が使える(node symlink を /usr/local/bin に作成するため)。
@@ -51,8 +51,12 @@ command -v systemctl >/dev/null 2>&1 || \
51
51
  systemctl --user show-environment >/dev/null 2>&1 || \
52
52
  fail "systemd のユーザーセッションに接続できません(systemctl --user が不可)。"
53
53
 
54
- UNIT_SRC="$SCRIPT_DIR/conductor@.service"
55
- [[ -f "$UNIT_SRC" ]] || fail "ユニットファイルがありません: $UNIT_SRC"
54
+ # ★ユニットの正本は配布物の systemd/ に1つだけ(写しを持たない)。
55
+ # 以前は sales-template/systemd/ にも同じユニットの写しがあり、片方だけ直すと中身がズレた。
56
+ # 正本の在り処は「このスクリプトから見て ../../systemd」。npm 配布物でもリポジトリでも
57
+ # 同じ相対位置になる(package.json の files に systemd を含めてある)。
58
+ UNIT_SRC="$(cd "$SCRIPT_DIR/../.." && pwd)/systemd/conductor@.service"
59
+ [[ -f "$UNIT_SRC" ]] || fail "ユニットファイルがありません: $UNIT_SRC(配布物が壊れている可能性があります)"
56
60
 
57
61
  [[ -d "$PROJECT_DIR" ]] || fail "プロジェクトディレクトリが存在しません: $PROJECT_DIR"
58
62
 
@@ -105,20 +109,46 @@ echo "[install] systemd PATH での node 解決: OK"
105
109
  # npm グローバルインストール先は環境依存(~/.npm-global/bin, /usr/local/bin,
106
110
  # ~/.volta/bin 等)で systemd PATH にない可能性がある。
107
111
  # ユニット配置時に ExecStart にフルパスを書き込んで根治する。
112
+ #
113
+ # ★どちらを使うかの順番(2026-09-04 に直した見落とし)
114
+ # ① 開発機(~/projects/conductor/dist/cli.js が在る)… そのリポジトリの中身を使う
115
+ # ② お客さま機(無い) … npm で入れた全体版を使う
116
+ #
117
+ # 【なぜ①を先に見るか】以前は npm の全体版を先に掴んでいた。開発機で
118
+ # `npm i -g @tyhld/conductor` を一度でも打つと、常駐がリポジトリではなく
119
+ # 【配布済みの古い版】で動いてしまう。直したはずの不具合が現場で再現し続け、
120
+ # 原因を「直っていない」と読み違える(=いちばん質の悪い事故)。
121
+ # リポジトリが在る機体=開発機なので、そこではリポジトリを正とする。
122
+ REPO_CLI="$HOME/projects/conductor/dist/cli.js"
108
123
  CONDUCTOR_BIN=""
109
- if command -v conductor >/dev/null 2>&1; then
124
+ CONDUCTOR_SRC=""
125
+
126
+ if [[ -f "$REPO_CLI" ]]; then
127
+ # 開発機。node のフルパスを頭に付ける(systemd はシェルの PATH を継承しないため)。
128
+ NODE_BIN="$(command -v node || true)"
129
+ [[ -n "$NODE_BIN" && -L "$NODE_BIN" ]] && NODE_BIN="$(readlink -f "$NODE_BIN")"
130
+ [[ -n "$NODE_BIN" ]] || NODE_BIN="/usr/bin/env node"
131
+ CONDUCTOR_BIN="$NODE_BIN $REPO_CLI"
132
+ CONDUCTOR_SRC="この機体のリポジトリ(開発機)"
133
+ elif command -v conductor >/dev/null 2>&1; then
110
134
  CONDUCTOR_BIN="$(command -v conductor)"
111
135
  [[ -L "$CONDUCTOR_BIN" ]] && CONDUCTOR_BIN="$(readlink -f "$CONDUCTOR_BIN")"
136
+ CONDUCTOR_SRC="npm で入れた全体版"
112
137
  fi
113
138
 
114
139
  if [[ -z "$CONDUCTOR_BIN" ]]; then
115
140
  fail "conductor コマンドが見つかりません。先に npm i -g @tyhld/conductor を実行してください。"
116
141
  fi
117
142
 
143
+ # ★どちらを使ったかを必ず1行出す(読み違えを防ぐ)。
144
+ echo "[install] 常駐が使う采配くん: ${CONDUCTOR_SRC}"
118
145
  echo "[install] 検出した conductor: $CONDUCTOR_BIN"
119
146
 
120
147
  # conductor が実際に動くか簡易検証
121
- if ! "$CONDUCTOR_BIN" --help >/dev/null 2>&1; then
148
+ # ★開発機では CONDUCTOR_BIN が「node cli.js の2語」になるので、語に割って呼ぶ。
149
+ # まとめて1語として呼ぶと、そんな名前のコマンドは無い=必ず失敗する。
150
+ read -r -a CONDUCTOR_CMD <<< "$CONDUCTOR_BIN"
151
+ if ! "${CONDUCTOR_CMD[@]}" --help >/dev/null 2>&1; then
122
152
  warn "conductor コマンドの実行に失敗しました。インストールを確認してください。"
123
153
  fi
124
154
 
@@ -127,9 +157,25 @@ UNIT_DIR="$HOME/.config/systemd/user"
127
157
  UNIT_DST="$UNIT_DIR/conductor@.service"
128
158
  mkdir -p "$UNIT_DIR"
129
159
 
160
+ # 台帳: 「このインストーラが入れたユニット」だけを記録し、uninstall.sh がそれを読んで消す。
161
+ # ユニット名を `conductor@*` で決め打ち走査しないため、`@` 無しの単発ユニットを取りこぼさない。
162
+ # 顧客が自作した conductor-*.service を巻き込まないのも台帳方式の利点。
163
+ LEDGER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/conductor"
164
+ LEDGER_FILE="$LEDGER_DIR/installed-units.txt"
165
+
166
+ # 台帳へ1行追記する(冪等: 既にある行は重複させない)。
167
+ ledger_add() {
168
+ local unit="$1"
169
+ mkdir -p "$LEDGER_DIR"
170
+ touch "$LEDGER_FILE"
171
+ grep -qxF "$unit" "$LEDGER_FILE" || echo "$unit" >> "$LEDGER_FILE"
172
+ }
173
+
130
174
  # テンプレートをコピーし、プレースホルダをフルパスに置換
131
175
  sed "s|__CONDUCTOR_BIN__|${CONDUCTOR_BIN}|g" "$UNIT_SRC" > "$UNIT_DST"
132
176
  echo "[install] 配置: $UNIT_DST (ExecStart=$CONDUCTOR_BIN)"
177
+ # テンプレート本体も台帳に載せる(撤去時にファイルを消す対象)。
178
+ ledger_add "conductor@.service"
133
179
 
134
180
  # --- 4) WorkingDirectory の解決 -----------------------------------------------
135
181
  DEFAULT_DIR="$HOME/projects/$SITE"
@@ -156,6 +202,8 @@ echo "[install] daemon-reload 実行"
156
202
 
157
203
  if systemctl --user enable --now "conductor@${SITE}.service"; then
158
204
  echo "[install] enable --now: conductor@${SITE}"
205
+ # enable に成功したものだけ台帳へ(入れていないものを撤去対象にしない)。
206
+ ledger_add "conductor@${SITE}.service"
159
207
  else
160
208
  fail "enable --now に失敗しました: conductor@${SITE}"
161
209
  fi
@@ -177,3 +225,4 @@ echo
177
225
  echo "ログ確認: journalctl --user -u conductor@${SITE} -f"
178
226
  echo "停止: systemctl --user disable --now conductor@${SITE}.service"
179
227
  echo "再起動: systemctl --user restart conductor@${SITE}.service"
228
+ echo "撤去: bash uninstall.sh --dry-run (確認後に --dry-run を外す)"
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # paste-cache-prune-install.sh — 貼り付け一時ファイル(~/.claude/paste-cache)を
4
+ # 毎日自動で古い分だけ消す systemd --user タイマーを設置・有効化する。
5
+ #
6
+ # やること(すべて冪等):
7
+ # 1. tyhld-paste-cache-prune.service / .timer を ~/.config/systemd/user/ へコピー(上書き)。
8
+ # 2. systemctl --user daemon-reload。
9
+ # 3. systemctl --user enable --now tyhld-paste-cache-prune.timer。
10
+ # 4. loginctl enable-linger(ログオフ中も回るように。既に有効なら無害)。
11
+ # 5. 撤去用の台帳(installed-units.txt)へ登録(uninstall.sh がこれを読んで消す)。
12
+ #
13
+ # 安全:
14
+ # - 触るのは ~/.claude/paste-cache だけ(実際の削除はユニット側で範囲を固定している)。
15
+ # - systemd --user が使えない環境では警告して素通り(本体セットアップを止めない)。
16
+ #
17
+ # 使い方:
18
+ # bash paste-cache-prune-install.sh
19
+ #
20
+ set -euo pipefail
21
+
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ UNIT_DIR="$HOME/.config/systemd/user"
24
+ SERVICE="tyhld-paste-cache-prune.service"
25
+ TIMER="tyhld-paste-cache-prune.timer"
26
+
27
+ # 撤去用の台帳(install.sh と同じ場所・同じ方式)。
28
+ LEDGER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/conductor"
29
+ LEDGER_FILE="$LEDGER_DIR/installed-units.txt"
30
+
31
+ info() { echo "[paste-cache-prune] $*"; }
32
+ warn() { echo "[paste-cache-prune] 注意: $*" >&2; }
33
+
34
+ ledger_add() {
35
+ local unit="$1"
36
+ mkdir -p "$LEDGER_DIR"
37
+ touch "$LEDGER_FILE"
38
+ grep -qxF "$unit" "$LEDGER_FILE" || echo "$unit" >> "$LEDGER_FILE"
39
+ }
40
+
41
+ # ユニット正本がそろっているか(無ければ素通り)。
42
+ for u in "$SERVICE" "$TIMER"; do
43
+ if [[ ! -f "$SCRIPT_DIR/$u" ]]; then
44
+ warn "ユニットが見つかりません: $SCRIPT_DIR/$u(自動掃除タイマーはスキップ)"
45
+ exit 0
46
+ fi
47
+ done
48
+
49
+ # systemd --user が使えないなら素通り(本体を止めない)。
50
+ if ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user show-environment >/dev/null 2>&1; then
51
+ warn "systemd --user が使えないため、自動掃除タイマーは設置しません。"
52
+ warn " (既存機では update-guard.sh の手動掃除が代わりに古い分を消します)"
53
+ exit 0
54
+ fi
55
+
56
+ mkdir -p "$UNIT_DIR"
57
+ cp -f "$SCRIPT_DIR/$SERVICE" "$UNIT_DIR/$SERVICE"
58
+ cp -f "$SCRIPT_DIR/$TIMER" "$UNIT_DIR/$TIMER"
59
+ info "配置: $UNIT_DIR/$SERVICE, $UNIT_DIR/$TIMER"
60
+
61
+ systemctl --user daemon-reload
62
+
63
+ if systemctl --user enable --now "$TIMER"; then
64
+ info "有効化: $TIMER(毎日1回・既定は 1 日より古い paste-cache を削除)"
65
+ ledger_add "$SERVICE"
66
+ ledger_add "$TIMER"
67
+ else
68
+ warn "enable --now に失敗しました: $TIMER"
69
+ exit 0
70
+ fi
71
+
72
+ # ログオフ中も回るように(他用途で既に有効なことも多い。失敗は握りつぶす)。
73
+ loginctl enable-linger "$USER" >/dev/null 2>&1 || true
74
+
75
+ systemctl --user --no-pager list-timers "$TIMER" 2>/dev/null || true
@@ -0,0 +1,25 @@
1
+ # tyhld-paste-cache-prune.service — 画面貼り付けの一時ファイルを古い分だけ掃除する(1回実行)。
2
+ #
3
+ # なぜ要るか:
4
+ # Claude Code は長文を画面に貼り付けると、その本文を ~/.claude/paste-cache/<hash>.txt に
5
+ # 平文で退避する。送信後は本文が会話履歴側にも入るため、この一時ファイルは古くなれば不要。
6
+ # 放置すると平文の貼り付け(秘密を含み得る)が溜まり続けるので、古い分だけ自動で消す。
7
+ #
8
+ # 安全:
9
+ # - ★触るのは ~/.claude/paste-cache だけ。ほかのフォルダには一切触れない。
10
+ # - mtime 基準で「今日ぶん(未送信・参照中かもしれない分)」は残す=全消しはしない。
11
+ # - フォルダが無ければ何もしない。
12
+ #
13
+ # 消す範囲は PASTE_CACHE_KEEP_DAYS 日より古いファイル(既定 1)。環境変数で調整可。
14
+ # ・systemd の変数展開を避けるため、シェルへ渡す $ は $$ でエスケープしている。
15
+ # ・%h は systemd がユーザーのホームに展開する。
16
+ #
17
+ # .timer から1日1回呼ばれる(tyhld-paste-cache-prune.timer)。
18
+
19
+ [Unit]
20
+ Description=tyhld: prune old Claude Code paste-cache files (~/.claude/paste-cache)
21
+
22
+ [Service]
23
+ Type=oneshot
24
+ Environment=PASTE_CACHE_KEEP_DAYS=1
25
+ ExecStart=/bin/sh -c 'd="%h/.claude/paste-cache"; [ -d "$$d" ] || exit 0; find "$$d" -mindepth 1 -maxdepth 1 -type f -mtime +$${PASTE_CACHE_KEEP_DAYS:-1} -delete'
@@ -0,0 +1,19 @@
1
+ # tyhld-paste-cache-prune.timer — 上の掃除サービスを毎日1回起動する。
2
+ #
3
+ # 顧客PCは update-guard.sh を手で回さないため、この常時タイマーが恒久策の本体。
4
+ # - OnCalendar=daily … 毎日1回。
5
+ # - Persistent=true … PCが消えていて実行を取りこぼした日は、次回起動直後に1度だけ追い実行。
6
+ # - RandomizedDelaySec … 起動直後に集中しないよう少しばらす。
7
+ #
8
+ # enable --now は setup.sh(新規機)と update-guard.sh(既存機・APPLY時)の両方が行う(冪等)。
9
+
10
+ [Unit]
11
+ Description=tyhld: daily prune of Claude Code paste-cache
12
+
13
+ [Timer]
14
+ OnCalendar=daily
15
+ Persistent=true
16
+ RandomizedDelaySec=15min
17
+
18
+ [Install]
19
+ WantedBy=timers.target
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # uninstall.sh — 采配くんを顧客PCから撤去する(systemd 常駐の停止・無効化・ユニット削除)。
4
+ #
5
+ # 【なぜこのファイルが sales-template にあるか】
6
+ # 撤去スクリプトは従来 scripts/ にしか無く、npm パッケージの files に scripts が含まれないため
7
+ # 顧客の手元に一切届いていなかった(2026-07-09 調査)。撤去手段は配布物に同梱されて初めて意味を持つ。
8
+ #
9
+ # 【台帳方式】インストーラが ~/.config/conductor/installed-units.txt に「自分が入れたユニット」を
10
+ # 記録する。ここではそれを読んで消す。ユニット名を `conductor@*` で決め打ち走査しないため、
11
+ # `@` を含まない単発ユニット(旧 conductor-orchestrator.service 等)も取りこぼさず、
12
+ # かつ顧客が自作した無関係な conductor-*.service を巻き込まない。
13
+ #
14
+ # 【順序は不変】disable --now → ファイル削除 → daemon-reload。
15
+ # 先にファイルを消すと disable が WantedBy の symlink を残す。
16
+ #
17
+ # 使い方:
18
+ # bash uninstall.sh # 確認プロンプトあり
19
+ # bash uninstall.sh --yes # 確認せず実行
20
+ # bash uninstall.sh --dry-run # 何を消すか表示するだけ(何も変更しない)
21
+ #
22
+ # このスクリプトが「やらない」こと(意図的。案内のみ出す):
23
+ # - loginctl disable-linger … linger は他用途でも使われ得るため外さない
24
+ # - ~/.conductor.env の削除 … 接続情報(秘密)を含むため消さない
25
+ # - npm -g @tyhld/conductor の削除 … パッケージ管理は利用者の領分
26
+ #
27
+ set -euo pipefail
28
+
29
+ UNIT_DIR="$HOME/.config/systemd/user"
30
+ LEDGER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/conductor"
31
+ LEDGER_FILE="$LEDGER_DIR/installed-units.txt"
32
+
33
+ # ★Mac(launchd)は仕組みが違うので、そちらの撤去を先に済ませて終わる(ADR-019)。
34
+ # 台帳 installed-launchd.txt に「入れた LaunchAgent の名前」が入っている。
35
+ if [[ "$(uname -s)" == "Darwin" ]]; then
36
+ MAC_LEDGER="$LEDGER_DIR/installed-launchd.txt"
37
+ AGENT_DIR="$HOME/Library/LaunchAgents"
38
+ MAC_DRY=0
39
+ MAC_YES=0
40
+ for a in "$@"; do
41
+ case "$a" in --dry-run) MAC_DRY=1 ;; --yes|-y) MAC_YES=1 ;; esac
42
+ done
43
+ if [[ ! -f "$MAC_LEDGER" ]]; then
44
+ echo "撤去対象はありません(台帳が無い=采配くんの常駐は入っていないようです)。"
45
+ exit 0
46
+ fi
47
+ echo "撤去する常駐(launchd):"
48
+ while IFS= read -r label || [[ -n "$label" ]]; do
49
+ [[ -z "$label" ]] && continue
50
+ echo " - $label"
51
+ done < "$MAC_LEDGER"
52
+ if [[ $MAC_DRY -eq 1 ]]; then
53
+ echo "(--dry-run のため何も変更していません)"
54
+ exit 0
55
+ fi
56
+ if [[ $MAC_YES -ne 1 ]]; then
57
+ read -r -p "撤去してよろしいですか (y/N): " ans
58
+ [[ "$(printf '%s' "$ans" | tr '[:upper:]' '[:lower:]')" == "y" ]] || { echo "中止しました。"; exit 0; }
59
+ fi
60
+ while IFS= read -r label || [[ -n "$label" ]]; do
61
+ [[ -z "$label" ]] && continue
62
+ launchctl bootout "gui/$(id -u)/${label}" >/dev/null 2>&1 || \
63
+ launchctl unload "$AGENT_DIR/${label}.plist" >/dev/null 2>&1 || true
64
+ rm -f "$AGENT_DIR/${label}.plist"
65
+ echo " 撤去: $label"
66
+ done < "$MAC_LEDGER"
67
+ rm -f "$MAC_LEDGER"
68
+ echo "撤去しました。"
69
+ echo "★消していないもの(意図的): ~/.conductor.env(接続情報)/番人一式(~/.tyhld)/npm の采配くん本体"
70
+ exit 0
71
+ fi
72
+
73
+ ASSUME_YES=0
74
+ DRY_RUN=0
75
+ for arg in "$@"; do
76
+ case "$arg" in
77
+ --yes|-y) ASSUME_YES=1 ;;
78
+ --dry-run) DRY_RUN=1 ;;
79
+ -h|--help)
80
+ echo "使い方: bash uninstall.sh [--yes] [--dry-run]"
81
+ exit 0
82
+ ;;
83
+ *)
84
+ echo "エラー: 不明な引数: ${arg}" >&2
85
+ exit 1
86
+ ;;
87
+ esac
88
+ done
89
+
90
+ if ! command -v systemctl >/dev/null 2>&1; then
91
+ echo "エラー: systemctl が見つかりません。systemd が有効か確認してください。" >&2
92
+ exit 1
93
+ fi
94
+ if ! systemctl --user show-environment >/dev/null 2>&1; then
95
+ echo "エラー: systemd のユーザーセッションに接続できません(systemctl --user が不可)。" >&2
96
+ exit 1
97
+ fi
98
+
99
+ # --- 1) 撤去対象の決定(台帳 → 無ければ従来 glob へフォールバック) -----------
100
+ targets=()
101
+ if [[ -f "$LEDGER_FILE" ]]; then
102
+ while IFS= read -r raw || [[ -n "$raw" ]]; do
103
+ line="${raw#"${raw%%[![:space:]]*}"}"
104
+ line="${line%"${line##*[![:space:]]}"}"
105
+ [[ -z "$line" || "${line:0:1}" == "#" ]] && continue
106
+ targets+=("$line")
107
+ done < "$LEDGER_FILE"
108
+ fi
109
+
110
+ USED_FALLBACK=0
111
+ if [[ ${#targets[@]} -eq 0 ]]; then
112
+ # 台帳導入前にインストールされた環境を救う。`@` 無しの単発ユニットも対象に含める。
113
+ USED_FALLBACK=1
114
+ echo "警告: 台帳が見つかりません(${LEDGER_FILE})。従来の走査へフォールバックします。" >&2
115
+ echo "警告: conductor@*.service と conductor-*.service を対象にします。" >&2
116
+ echo "警告: 自作の conductor-*.service があれば巻き込む恐れがあります。まず --dry-run で確認してください。" >&2
117
+ # ★mapfile は bash 4 以降。Mac 標準の bash 3.2 でも動くよう while read で読む(ADR-019)。
118
+ while IFS= read -r _line; do
119
+ [[ -n "$_line" ]] && targets+=("$_line")
120
+ done < <(
121
+ {
122
+ systemctl --user list-unit-files --no-legend 'conductor@*.service' 2>/dev/null | awk '{print $1}'
123
+ systemctl --user list-units --no-legend --all 'conductor@*.service' 2>/dev/null | awk '{print $1}'
124
+ systemctl --user list-unit-files --no-legend 'conductor-*.service' 2>/dev/null | awk '{print $1}'
125
+ systemctl --user list-units --no-legend --all 'conductor-*.service' 2>/dev/null | awk '{print $1}'
126
+ } | sed '/^$/d' | sort -u
127
+ )
128
+ fi
129
+
130
+ # ★連想配列(declare -A)は bash 4 以降。3.2 でも動くよう、見た名前を1行ずつ持って照合する。
131
+ seen_list=""
132
+ uniq_targets=()
133
+ for u in "${targets[@]}"; do
134
+ [[ -z "$u" ]] && continue
135
+ case "
136
+ $seen_list" in
137
+ *"
138
+ $u
139
+ "*) continue ;;
140
+ esac
141
+ seen_list="$seen_list$u
142
+ "
143
+ uniq_targets+=("$u")
144
+ done
145
+
146
+ # drop-in ディレクトリも列挙(表示と削除で使う)。
147
+ shopt -s nullglob
148
+ dropins=("$UNIT_DIR"/conductor@*.service.d)
149
+ shopt -u nullglob
150
+
151
+ if [[ ${#uniq_targets[@]} -eq 0 && ${#dropins[@]} -eq 0 ]]; then
152
+ echo "撤去対象はありません。采配くんの systemd 常駐は既に入っていないようです。"
153
+ exit 0
154
+ fi
155
+
156
+ # --- 2) 何を消すかを必ず列挙する --------------------------------------------
157
+ echo
158
+ echo "以下を撤去します:"
159
+ echo " 台帳: ${LEDGER_FILE}$([[ $USED_FALLBACK -eq 1 ]] && echo ' (不在→フォールバック走査)')"
160
+ for u in "${uniq_targets[@]}"; do
161
+ if [[ "$u" == "conductor@.service" ]]; then
162
+ echo " - ${u}(ファイル削除のみ/テンプレート本体)"
163
+ else
164
+ echo " - ${u}(disable --now → ファイル削除)"
165
+ fi
166
+ done
167
+ for d in "${dropins[@]}"; do
168
+ echo " - $(basename "$d")(drop-in ディレクトリ削除)"
169
+ done
170
+ echo " 最後に daemon-reload を1回実行します。"
171
+ echo
172
+ echo "消さないもの(必要なら手動で):"
173
+ echo " - linger 設定 : loginctl disable-linger \"\$USER\""
174
+ echo " - 接続情報 : \$HOME/.conductor.env (秘密を含むため残します)"
175
+ echo " - npm パッケージ : npm uninstall -g @tyhld/conductor"
176
+ echo
177
+
178
+ if [[ $DRY_RUN -eq 1 ]]; then
179
+ echo "--dry-run のため、ここで終了します(何も変更していません)。"
180
+ exit 0
181
+ fi
182
+
183
+ if [[ $ASSUME_YES -ne 1 ]]; then
184
+ read -r -p "本当に撤去しますか? [y/N]: " answer
185
+ case "$answer" in
186
+ y|Y|yes|YES) ;;
187
+ *) echo "中止しました。"; exit 0 ;;
188
+ esac
189
+ fi
190
+
191
+ # --- 3) disable --now(失敗は握りつぶす=冪等。既に無効でも進む) -------------
192
+ disabled=()
193
+ for u in "${uniq_targets[@]}"; do
194
+ [[ "$u" == "conductor@.service" ]] && continue # テンプレート本体は起動単位ではない
195
+ if systemctl --user disable --now "$u" 2>/dev/null; then
196
+ echo "[uninstall] disable --now: ${u}"
197
+ disabled+=("$u")
198
+ else
199
+ echo "[uninstall] warn: disable に失敗(既に無効かもしれません): ${u}" >&2
200
+ fi
201
+ done
202
+
203
+ # --- 4) ユニットファイル+drop-in を削除(★disable の後) --------------------
204
+ removed=()
205
+ for u in "${uniq_targets[@]}"; do
206
+ if [[ -f "$UNIT_DIR/$u" ]]; then
207
+ rm -f "$UNIT_DIR/$u"
208
+ removed+=("$u")
209
+ fi
210
+ done
211
+ if [[ -f "$UNIT_DIR/conductor@.service" ]]; then
212
+ rm -f "$UNIT_DIR/conductor@.service"
213
+ removed+=("conductor@.service")
214
+ fi
215
+ for d in "${dropins[@]}"; do
216
+ rm -rf "$d"
217
+ removed+=("$(basename "$d")")
218
+ done
219
+
220
+ systemctl --user daemon-reload
221
+ echo "[uninstall] daemon-reload 実行"
222
+
223
+ # --- 5) 台帳の後始末(撤去できたものを消す。空なら台帳ごと削除) --------------
224
+ if [[ -f "$LEDGER_FILE" ]]; then
225
+ survivors=()
226
+ for u in "${uniq_targets[@]}"; do
227
+ [[ -f "$UNIT_DIR/$u" ]] && survivors+=("$u")
228
+ done
229
+ if [[ ${#survivors[@]} -eq 0 ]]; then
230
+ rm -f "$LEDGER_FILE"
231
+ rmdir "$LEDGER_DIR" 2>/dev/null || true
232
+ echo "[uninstall] 台帳を削除(全て撤去済み): ${LEDGER_FILE}"
233
+ else
234
+ printf '%s\n' "${survivors[@]}" > "$LEDGER_FILE"
235
+ echo "[uninstall] warn: 撤去できなかった ${#survivors[@]} 件を台帳に残しました: ${survivors[*]}" >&2
236
+ fi
237
+ fi
238
+
239
+ # --- 6) まとめ --------------------------------------------------------------
240
+ echo
241
+ echo "[uninstall] 完了。"
242
+ echo " 無効化: ${#disabled[@]}件${disabled[*]:+ — ${disabled[*]}}"
243
+ echo " 削除: ${#removed[@]}件${removed[*]:+ — ${removed[*]}}"
244
+ echo
245
+ echo "残したもの: linger 設定 / \$HOME/.conductor.env / npm パッケージ(上の案内を参照)"