@tyhld/conductor 0.12.84 → 0.12.86

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.84",
3
+ "version": "0.12.86",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,9 @@
22
22
  "scripts/update-guard.sh",
23
23
  "scripts/dist-source.sh",
24
24
  "scripts/conductor-run.sh",
25
+ "scripts/tyhld-selfcheck.sh",
26
+ "scripts/tyhld-autoupdate.sh",
27
+ "scripts/autoupdate-install.sh",
25
28
  "scripts/kc.sh",
26
29
  "scripts/codex",
27
30
  "!scripts/codex/__pycache__",
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # autoupdate-install.sh — 自社PCに「自動更新」を入れる(便 29babc5d・設計 3ed9e2b2 の A3)。
4
+ #
5
+ # 【誰が・いつ打つか】★人が、自社PCの更新の窓で1回だけ打つ(えふさん確定 2026-10-02)。
6
+ # まず確認だけ: bash "$(npm root -g)/@tyhld/conductor/scripts/autoupdate-install.sh"
7
+ # 入れる: APPLY=1 bash "$(npm root -g)/@tyhld/conductor/scripts/autoupdate-install.sh"
8
+ # ★sudo は使わない(要る物は「人が打つ1行」として出して止まる)。何度打っても同じ結果になる。
9
+ #
10
+ # 【★お客さまの機には入れない】sales-template/install.sh・setup.sh はこの台本を呼ばない
11
+ # (自社で実績を取ってから、販売版の受入条件として別に判断する=えふさん確定 2026-10-02)。
12
+ #
13
+ # 【やること】
14
+ # ⓪ 確かめ … systemd(ユーザー)が使える/入っている采配くん(公開済みの配布物)/接続設定/
15
+ # 囲いの部品(bwrap・socat)/Claude Code の入り方
16
+ # ★Claude Code が公式推奨の1方式(https://claude.ai/install.sh のネイティブ導入)だけになっていなければ、
17
+ # 人が打つ1行を出して止まる(★この台本は自分で外さない・入れない)。
18
+ # ① 鍵 … ~/.tyhld/bin/autoupdate/enabled(★職人が書けない場所。これが在る機だけ更新係が動く)
19
+ # ② 入口 … ~/.tyhld/bin/tyhld-autoupdate・tyhld-selfcheck(+常駐の入口一式・隣に dist-source.sh)
20
+ # ③ systemd … tyhld-autoupdate-boot.service(起動時1回)・tyhld-autoupdate.timer(数分ごと)・
21
+ # tyhld-selfcheck.timer(毎日)。常駐(conductor@・耳・司令塔)は起動時の1回の後に起きる
22
+ # (常駐の側に After= の drop-in を置く)。撤去の台帳にも載せる。
23
+ # ④ Claude Code 自身の自動更新を止める … ~/.claude/settings.json の env に DISABLE_AUTOUPDATER=1
24
+ # (控えを取ってから。★DISABLE_UPDATES は使わない=人の claude update まで塞ぐため)。
25
+ # ★上げるのは更新係1つだけ(管制の目標の版を、公式の台本で名指しして入れる)。
26
+ # ⑤ Windows 側で打つ2行を表示する(WSL を起こして居続けさせる近道・AC 接続時に眠らせない)。
27
+ #
28
+ # 【試験用の入口】★安全境界ではない(ほかの CONDUCTOR_* と同じ)。
29
+ # AUTOUPDATE_SYSTEM_BIN_DIRS … ホームの外の claude の置き場(既定 "/usr/local/bin /usr/bin")
30
+ # CONDUCTOR_PROC_VERSION_FILE … WSL かどうかを見る見本(既定 /proc/version)
31
+ # AUTOUPDATE_SKIP_SYSTEMD_CHECK=1 … systemd の有無の確かめを飛ばす(作り物の systemctl で流すとき)
32
+ set -uo pipefail
33
+
34
+ APPLY="${APPLY:-0}"
35
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
+ . "$HERE/dist-source.sh"
37
+
38
+ UNIT_DIR="$HOME/.config/systemd/user"
39
+ BIN_DIR="$HOME/.tyhld/bin"
40
+ AU_DIR="$BIN_DIR/autoupdate"
41
+ KEY="$AU_DIR/enabled"
42
+ LEDGER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/conductor"
43
+ LEDGER_FILE="$LEDGER_DIR/installed-units.txt"
44
+ CLAUDE_SETTINGS="$HOME/.claude/settings.json"
45
+ SYSTEM_BIN_DIRS="${AUTOUPDATE_SYSTEM_BIN_DIRS:-/usr/local/bin /usr/bin}"
46
+ UNITS=(tyhld-autoupdate-boot.service tyhld-autoupdate.service tyhld-autoupdate.timer tyhld-selfcheck.service tyhld-selfcheck.timer)
47
+ ORDER_DROPIN_NAME='10-tyhld-autoupdate.conf'
48
+ RELAY_UNITS=(conductor@.service conductor-ear.service conductor-build-runner.service)
49
+
50
+ info() { echo "[autoupdate-install] $*"; }
51
+ warn() { echo "[autoupdate-install] 注意: $*" >&2; }
52
+
53
+ NG=() # 止まる理由(人が打つ1行つき)
54
+ ng() { NG+=("$1"); }
55
+
56
+ if [[ "$APPLY" == "1" ]]; then info "モード: 適用"; else info "モード: 確認のみ(1バイトも書きません。入れるには APPLY=1 を付けて打ってください)"; fi
57
+
58
+ # ─── ⓪ 確かめ ───────────────────────────────────────────────────────────────
59
+ info "⓪ 確かめ"
60
+
61
+ # systemd(ユーザー)
62
+ if [[ "${AUTOUPDATE_SKIP_SYSTEMD_CHECK:-0}" != "1" ]]; then
63
+ if ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user show-environment >/dev/null 2>&1; then
64
+ ng "systemd(ユーザー)が使えません。WSL なら /etc/wsl.conf に [boot] systemd=true が要ります(人の手番・CC1 に知らせてください)"
65
+ fi
66
+ fi
67
+
68
+ # 入っている采配くん(★公開済みの配布物。作業ツリーは使わない)
69
+ if dist_runtime_resolve; then
70
+ info " 采配くん: $DIST_ROOT(版 $DIST_VERSION)"
71
+ else
72
+ ng "動かす采配くんを確かめられません(${DIST_REASON})。
73
+ npm install -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org"
74
+ fi
75
+ for f in scripts/tyhld-autoupdate.sh scripts/tyhld-selfcheck.sh systemd/tyhld-autoupdate-boot.service systemd/tyhld-autoupdate.service systemd/tyhld-autoupdate.timer systemd/tyhld-selfcheck.service systemd/tyhld-selfcheck.timer; do
76
+ if [[ -n "$DIST_ROOT" && ! -f "$DIST_ROOT/$f" ]]; then
77
+ ng "入っている采配くん(版 ${DIST_VERSION:-不明})に $f がありません=自動更新が入る前の版です。
78
+ npm install -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org"
79
+ break
80
+ fi
81
+ done
82
+
83
+ # 接続設定(管制に目標の版を聞く・自己診断を届けるのに要る)。★中身は読まない。
84
+ if [[ -f "$HOME/.conductor.env" ]]; then
85
+ info " 接続設定: $HOME/.conductor.env が在ります"
86
+ else
87
+ ng "接続設定($HOME/.conductor.env)がありません。この機は采配くんの常駐が入っていません(CC1 に知らせてください)"
88
+ fi
89
+
90
+ # 囲いの部品(無いと職人が起動を断る)
91
+ if [[ "$(uname -s 2>/dev/null)" == "Linux" ]]; then
92
+ missing=()
93
+ for b in bwrap socat; do command -v "$b" >/dev/null 2>&1 || missing+=("$b"); done
94
+ if (( ${#missing[@]} > 0 )); then
95
+ ng "囲いの部品がありません(${missing[*]})。
96
+ sudo apt-get install -y bubblewrap socat"
97
+ else
98
+ info " 囲いの部品: bwrap・socat が在ります"
99
+ fi
100
+ fi
101
+
102
+ # Claude Code の入り方(★公式推奨=ネイティブ導入の1方式だけにそろえる・えふさん確定 2026-10-02 夜)
103
+ # 公式(code.claude.com/docs/en/setup)で推奨と明記されているのは Native Install (Recommended)。
104
+ # ネイティブ導入は ~/.local/bin/claude → ~/.local/share/claude/versions/<版>。
105
+ # ★ほかの入り方が残っていると、中継がいちばん新しい方を選び、更新係が戻せない(設計 3ed9e2b2 の発見4)。
106
+ NATIVE_LINK="$HOME/.local/bin/claude"
107
+ NATIVE_DIR="$HOME/.local/share/claude/versions"
108
+ native_ok=0
109
+ if [[ -e "$NATIVE_LINK" ]]; then
110
+ real="$(readlink -f "$NATIVE_LINK" 2>/dev/null || printf '%s' "$NATIVE_LINK")"
111
+ [[ "$real" == "$NATIVE_DIR/"* ]] && native_ok=1
112
+ fi
113
+ others=()
114
+ if [[ -d "$HOME/.nvm/versions/node" ]]; then
115
+ for d in "$HOME"/.nvm/versions/node/*/bin; do
116
+ [[ -e "$d/claude" ]] && others+=("$d/claude")
117
+ done
118
+ fi
119
+ [[ -e "$HOME/.claude/local/claude" ]] && others+=("$HOME/.claude/local/claude")
120
+ for d in $SYSTEM_BIN_DIRS; do
121
+ [[ -e "$d/claude" ]] && others+=("$d/claude")
122
+ done
123
+ if (( native_ok == 0 )) && [[ -e "$NATIVE_LINK" ]]; then
124
+ others+=("$NATIVE_LINK")
125
+ fi
126
+
127
+ claude_lines=()
128
+ if (( native_ok == 0 )); then
129
+ claude_lines+=("curl -fsSL https://claude.ai/install.sh | bash -s stable")
130
+ fi
131
+ for c in "${others[@]}"; do
132
+ real="$(readlink -f "$c" 2>/dev/null || printf '%s' "$c")"
133
+ case "$c" in
134
+ "$HOME"/.nvm/versions/node/*/bin/claude)
135
+ claude_lines+=("PATH=\"$(dirname "$c"):\$PATH\" npm uninstall -g @anthropic-ai/claude-code")
136
+ ;;
137
+ "$HOME/.claude/local/claude")
138
+ claude_lines+=("rm -r \"\$HOME/.claude/local\"")
139
+ ;;
140
+ "$NATIVE_LINK")
141
+ claude_lines+=("rm \"\$HOME/.local/bin/claude\"")
142
+ ;;
143
+ *)
144
+ if [[ "$real" == */lib/node_modules/@anthropic-ai/claude-code/* ]]; then
145
+ prefix="${real%%/lib/node_modules/*}"
146
+ if [[ -x "$prefix/bin/npm" ]]; then
147
+ claude_lines+=("sudo \"$prefix/bin/npm\" uninstall -g @anthropic-ai/claude-code")
148
+ else
149
+ claude_lines+=("sudo rm \"$c\"" "sudo rm -r \"$prefix/lib/node_modules/@anthropic-ai/claude-code\"")
150
+ fi
151
+ else
152
+ ng "Claude Code の入り方を見分けられない物があります: $c(実体 $real)。この台本は外しません。CC1 に知らせてください。
153
+ ls -l \"$c\""
154
+ fi
155
+ ;;
156
+ esac
157
+ done
158
+ if (( ${#claude_lines[@]} > 0 )); then
159
+ msg="Claude Code が公式推奨の1方式(ネイティブ導入)だけになっていません(ネイティブ導入: $([[ $native_ok == 1 ]] && echo 在る || echo 無い)・ほか: ${others[*]:-無し})。
160
+ ★この台本は自分で外さない・入れない。上から順に打ってから、もう一度この台本を打ってください:"
161
+ for l in "${claude_lines[@]}"; do msg+=$'\n'" $l"; done
162
+ ng "$msg"
163
+ else
164
+ info " Claude Code: 公式推奨の1方式だけです($(readlink -f "$NATIVE_LINK" 2>/dev/null))"
165
+ fi
166
+
167
+ # ─── ⑤ Windows 側で打つ2行(★止まるときも先に見せる)────────────────────────────
168
+ PROC="${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}"
169
+ windows_lines() {
170
+ local distro="${WSL_DISTRO_NAME:-Ubuntu}"
171
+ echo
172
+ info "⑤ Windows の PowerShell で打つ2行(管理者でなくてよい・1回だけ):"
173
+ echo " 1) ログオンのときに WSL(${distro})を起こし、最小化の窓で居続けさせる近道を置く"
174
+ printf '%s\n' "\$s=(New-Object -ComObject WScript.Shell).CreateShortcut(\"\$env:APPDATA\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\tyhld-wsl.lnk\");\$s.TargetPath=\"\$env:WINDIR\\System32\\wsl.exe\";\$s.Arguments=\"-d ${distro} --exec /bin/sleep infinity\";\$s.WindowStyle=7;\$s.Save()"
175
+ echo " 2) 電源(AC)につないでいるあいだは眠らせない"
176
+ echo "powercfg /change standby-timeout-ac 0"
177
+ echo " ★その窓(最小化の wsl)を閉じると WSL が止まり得ます。Windows のログオンは人のままです。"
178
+ }
179
+ IS_WSL=0
180
+ if [[ -r "$PROC" ]] && grep -qi microsoft "$PROC"; then IS_WSL=1; fi
181
+
182
+ if (( ${#NG[@]} > 0 )); then
183
+ echo
184
+ warn "足りない物があります(★1バイトも書いていません)。上から順に直してから、もう一度打ってください:"
185
+ for m in "${NG[@]}"; do echo " ✗ $m" >&2; done
186
+ (( IS_WSL == 1 )) && windows_lines
187
+ exit 1
188
+ fi
189
+
190
+ # ─── ①〜④ 入れる ───────────────────────────────────────────────────────────
191
+ if [[ "$APPLY" != "1" ]]; then
192
+ info "①(予定)鍵を置きます: $KEY"
193
+ info "②(予定)入口を置きます: $BIN_DIR/tyhld-autoupdate・tyhld-selfcheck(配布物 $DIST_ROOT から)"
194
+ info "③(予定)systemd: ${UNITS[*]}(起動時の1回・数分ごと・毎日の自己診断)"
195
+ info " (予定)常駐(${RELAY_UNITS[*]})を起動時の1回の後に起こす drop-in"
196
+ info "④(予定)$CLAUDE_SETTINGS の env に DISABLE_AUTOUPDATER=1(控えを取ってから)"
197
+ (( IS_WSL == 1 )) && windows_lines
198
+ echo
199
+ info "確認だけで終わりました。入れるには: APPLY=1 bash \"\$(npm root -g)/@tyhld/conductor/scripts/autoupdate-install.sh\""
200
+ exit 0
201
+ fi
202
+
203
+ ledger_add() {
204
+ mkdir -p "$LEDGER_DIR"
205
+ touch "$LEDGER_FILE"
206
+ grep -qxF "$1" "$LEDGER_FILE" || echo "$1" >> "$LEDGER_FILE"
207
+ }
208
+
209
+ # ① 鍵
210
+ mkdir -p "$AU_DIR"
211
+ if [[ ! -f "$KEY" ]]; then
212
+ printf '%s installed-by=autoupdate-install.sh version=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$DIST_VERSION" > "$KEY"
213
+ fi
214
+ info "① 鍵: $KEY"
215
+
216
+ # ② 入口(常駐の入口一式+更新係・自己診断)
217
+ if ! dist_runtime_bundle_install "$DIST_ROOT"; then
218
+ warn "常駐の入口を置けませんでした: $BIN_DIR(★ここで止めます)"
219
+ exit 1
220
+ fi
221
+ dist_runtime_hint_write "$DIST_ROOT"
222
+ for pair in "scripts/tyhld-autoupdate.sh:tyhld-autoupdate" "scripts/tyhld-selfcheck.sh:tyhld-selfcheck"; do
223
+ from="$DIST_ROOT/${pair%%:*}"; to="$BIN_DIR/${pair##*:}"
224
+ cp -f "$from" "$to.tmp" && mv -f "$to.tmp" "$to" && chmod 755 "$to" && cmp -s "$from" "$to" || {
225
+ warn "入口を置けませんでした: $to(★ここで止めます)"; exit 1; }
226
+ done
227
+ info "② 入口: $BIN_DIR/tyhld-autoupdate・$BIN_DIR/tyhld-selfcheck(配布物 $DIST_ROOT・版 $DIST_VERSION)"
228
+
229
+ # ③ systemd
230
+ mkdir -p "$UNIT_DIR"
231
+ for u in "${UNITS[@]}"; do
232
+ cp -f "$DIST_ROOT/systemd/$u" "$UNIT_DIR/$u"
233
+ done
234
+ for r in "${RELAY_UNITS[@]}"; do
235
+ mkdir -p "$UNIT_DIR/$r.d"
236
+ printf '%s\n' \
237
+ '# tyhld: 常駐は、起動時の更新係(tyhld-autoupdate-boot)の後に起きる(autoupdate-install.sh が置いた)。' \
238
+ '# ★After= だけ(更新係が失敗しても常駐は起きる)。' \
239
+ '[Unit]' \
240
+ 'After=tyhld-autoupdate-boot.service' > "$UNIT_DIR/$r.d/$ORDER_DROPIN_NAME"
241
+ done
242
+ systemctl --user daemon-reload
243
+ systemctl --user enable tyhld-autoupdate-boot.service >/dev/null 2>&1 || warn "enable に失敗しました: tyhld-autoupdate-boot.service"
244
+ for t in tyhld-autoupdate.timer tyhld-selfcheck.timer; do
245
+ systemctl --user enable --now "$t" >/dev/null 2>&1 || warn "enable --now に失敗しました: $t"
246
+ done
247
+ for u in "${UNITS[@]}"; do ledger_add "$u"; done
248
+ loginctl enable-linger "${USER:-$(id -un)}" >/dev/null 2>&1 || true
249
+ info "③ systemd: ${UNITS[*]}(常駐は起動時の1回の後に起きます)"
250
+
251
+ # ④ Claude Code 自身の自動更新を止める(★上げるのは更新係1つだけ)
252
+ mkdir -p "$(dirname "$CLAUDE_SETTINGS")"
253
+ if [[ -f "$CLAUDE_SETTINGS" ]]; then
254
+ bak="$CLAUDE_SETTINGS.bak.$(date '+%Y%m%d-%H%M%S')"
255
+ cp -p "$CLAUDE_SETTINGS" "$bak"
256
+ info "④ 控え: $bak"
257
+ fi
258
+ if ! python3 - "$CLAUDE_SETTINGS" <<'PY'
259
+ import json, os, sys
260
+ p = sys.argv[1]
261
+ d = {}
262
+ if os.path.exists(p):
263
+ with open(p, encoding='utf-8') as f:
264
+ d = json.load(f)
265
+ if not isinstance(d, dict):
266
+ sys.exit(3)
267
+ env = d.setdefault('env', {})
268
+ if not isinstance(env, dict):
269
+ sys.exit(3)
270
+ env['DISABLE_AUTOUPDATER'] = '1'
271
+ tmp = p + '.tmp'
272
+ with open(tmp, 'w', encoding='utf-8') as f:
273
+ json.dump(d, f, ensure_ascii=False, indent=2)
274
+ f.write('\n')
275
+ os.replace(tmp, p)
276
+ PY
277
+ then
278
+ warn "$CLAUDE_SETTINGS を読めない形なので、DISABLE_AUTOUPDATER を書けませんでした(控えは上)。CC1 に知らせてください"
279
+ exit 1
280
+ fi
281
+ info "④ Claude Code 自身の自動更新を止めました($CLAUDE_SETTINGS の env.DISABLE_AUTOUPDATER=1)"
282
+
283
+ (( IS_WSL == 1 )) && windows_lines
284
+ echo
285
+ info "入りました。続けて自己診断を1回流してください: ~/.tyhld/bin/tyhld-selfcheck --now"
@@ -30,7 +30,9 @@
30
30
  # systemd は Restart=always なので、入れ直せば次の起動で自動的に戻る。
31
31
  #
32
32
  # 【★公開 npm には問い合わせない】常駐は回線が切れても動き続ける必要がある。
33
- # 入っている版が古いかどうかは、毎時同期が見て知らせる(同じ見張りを2つ作らない)。
33
+ # 版をそろえるのは、自動更新が入っている機(自社PC)では更新係(tyhld-autoupdate・便 29babc5d)、
34
+ # 入っていない機では毎時同期が見て知らせる(同じ見張りを2つ作らない)。
35
+ # ★ここで出す「人の手番」は、配布物そのものが無い・欠けている(=更新係も動けない)ときだけ。
34
36
  #
35
37
  # 【置き場】この入口は ~/.tyhld/bin/conductor-run として置かれ、隣に dist-source.sh と
36
38
  # skill_sync_notify.py を置く(導入処理と毎時同期が配布物から写す)。
@@ -20,8 +20,16 @@
20
20
  #
21
21
  # 【この部品が決めること】★これだけ
22
22
  # ・配ってよい中身の置き場(DIST_ROOT)=この機に入っている @tyhld/conductor の配布物
23
- # ・それが「公開済みの正規版」か(入っている版 == 公開 npm のいまの版)
24
- # ・違う/確かめられないときは【配らない】と言い切ること
23
+ # ・それが「公開されている正規の版で、管制で不具合の印が無い版」か(★便 29babc5d で変えた)
24
+ # 以前は「入っている版 == 公開 npm のいまの版(latest)」だけを配っていた。自社PCの段階配布では
25
+ # 後続の機が数時間 latest より古い(正規の)版のまま動くので、そのあいだ番人が止まってしまう。
26
+ # 【えふさん確定(2026-10-02 夜・判断の窓1)】「入っている版=公開の最新」→
27
+ # 「公開されている正規の版で、管制で不具合の印が無い版」。
28
+ # 正規の版 … 公開 npm にその版が在り、「使わないで」の印(npm deprecate)が付いていない
29
+ # 管制の印 … 管制の目標の版の口に入っている版を添えて聞く(installedConductorFlag)。
30
+ # "suspect"・"bad" なら配らない。欄が無い・"unknown"・管制に届かないときは
31
+ # 「印が無い」として配る(★番人を配らないほうが危ない)
32
+ # ・違う/確かめられないときは【配らない】と言い切ること(公開 npm に聞けないときも配らない=今までどおり)
25
33
  #
26
34
  # 【この部品が決めないこと】
27
35
  # ・番人の判定・禁止の中身・鉄則の中身(1文字も触らない)
@@ -36,6 +44,8 @@
36
44
  # 【試験用の入口】★安全境界ではない(環境変数を置ける人は cp も打てる)。
37
45
  # CONDUCTOR_DIST_ROOT … 配布物の置き場を名指しする(試験で偽の配布物を指すため)
38
46
  # CONDUCTOR_DIST_PUBLISHED … 公開 npm への問い合わせの代わりに使う値(試験で網を切らないため)
47
+ # CONDUCTOR_DIST_PUBLISHED_VERSIONS … 同じく「公開 npm に在る版」の一覧(空白区切り・既定は上の1つだけ)
48
+ # CONDUCTOR_DIST_DEPRECATED_VERSIONS … 同じく「使わないで」の印が付いた版の一覧(空白区切り)
39
49
 
40
50
  # ★取得先の宣言は install.sh / setup.sh の「registry-pin」正本と同じ形にする。
41
51
  # (公開 npm 1本。手元の設定が何であってもここだけは公開 npm を見る=便 9858450b)
@@ -48,6 +58,7 @@ DIST_ROOT='' # 配ってよい中身の置き場(確かめられたと
48
58
  DIST_VERSION='' # そこに入っている版
49
59
  DIST_PUBLISHED='' # 公開 npm のいまの版
50
60
  DIST_REASON='' # 配れないときの理由(1語・記録にそのまま載る)
61
+ DIST_CENTRAL_FLAG='' # 管制の印(ok / suspect / bad / unknown)。配り元を決めたときに入る
51
62
 
52
63
  #: どの道で置き場を見つけたか(override / path / hint / scan / npm)。記録と画面に出す。
53
64
  DIST_METHOD=''
@@ -176,8 +187,9 @@ DIST_NODE=''
176
187
  # 常駐と耳を動かす置き場を決める(便 81a08d58)。★作業ツリーは選ばない。
177
188
  # 返り 0 … DIST_ROOT / DIST_NODE で動かしてよい
178
189
  # 返り 1 … 動かしてはいけない(DIST_REASON に理由)
179
- # ★公開 npm には問い合わせない。常駐は回線が切れても動き続ける必要があり、
180
- # 版が古いかどうかは毎時同期(dist_source_resolve)が見て知らせる=同じ見張りを2つ作らない。
190
+ # ★公開 npm には問い合わせない。常駐は回線が切れても動き続ける必要がある。
191
+ # 版をそろえるのは、自動更新が入っている機(自社PC)では更新係(tyhld-autoupdate)、
192
+ # 入っていない機では毎時同期(dist_source_resolve)が見て知らせる=同じ見張りを2つ作らない。
181
193
  dist_runtime_resolve() {
182
194
  DIST_ROOT=''; DIST_NODE=''; DIST_VERSION=''; DIST_PUBLISHED=''; DIST_REASON=''
183
195
  if ! dist_pkg_find; then
@@ -208,30 +220,170 @@ except Exception:
208
220
  pass' "$pj" 2>/dev/null
209
221
  }
210
222
 
211
- # 公開 npm が「いま配っている版」(取れなければ空)。★固定の下限は使わない(便 9858450b)。
212
- dist_published_version() {
213
- # ★検査用の入口。空文字を入れれば「問い合わせられなかった」を作れる(網を切らずに確かめる)。
223
+ # 配布物を動かしている node と【同じ場所の npm】を返す(見つからなければ空・返り 1)。
224
+ # 【なぜ要るか(実機 2026-10-02・ノートPC・便 3ed9e2b2 の発見1)】
225
+ # 毎時同期(tyhld-guard-sync)は systemd の中で動く。そこの PATH には nvm の node も npm も無く、
226
+ # /usr/bin/npm も /usr/local/bin/npm も無い機があった。裸の `npm view` が毎回「見つからない」で
227
+ # 失敗し、それを「回線」と記録していた=番人・鉄則が1度も配られていなかった。
228
+ # 【どう決めるか】配布物の置き場から node を決め(dist_node_for=常駐と同じ node)、
229
+ # その node の隣(同じ bin)の npm を使う。node が近道(/usr/local/bin/node → nvm)なら実体を辿る。
230
+ # それでも無いときだけ PATH の npm を使う(人が打ったとき)。
231
+ dist_npm_for() {
232
+ local root="$1" node real n
233
+ node="$(dist_node_for "$root" 2>/dev/null)" || node=''
234
+ if [[ -n "$node" ]]; then
235
+ for n in "$node" "$(readlink -f "$node" 2>/dev/null)"; do
236
+ [[ -n "$n" && -x "$(dirname "$n")/npm" ]] || continue
237
+ printf '%s' "$(dirname "$n")/npm"
238
+ return 0
239
+ done
240
+ fi
241
+ real="$(type -P npm 2>/dev/null || true)"
242
+ [[ -n "$real" ]] && { printf '%s' "$real"; return 0; }
243
+ return 1
244
+ }
245
+
246
+ #: 公開版の問い合わせができなかった理由(dist_published_query が入れる)。
247
+ #: no-npm … 問い合わせに使う npm が見つからない
248
+ #: unverifiable … npm は在るが、問い合わせに失敗した(回線・取得先・npm の不調のどれか)
249
+ DIST_PUBLISHED_ERR=''
250
+ #: 問い合わせに使った npm(記録と画面に出す)。
251
+ DIST_NPM=''
252
+
253
+ # 公開 npm が「いま配っている版」を DIST_PUBLISHED に入れる(取れなければ空+DIST_PUBLISHED_ERR)。
254
+ # ★固定の下限は使わない(便 9858450b)。★$(…) の中で呼ばない(結果を変数で返すため)。
255
+ dist_published_query() {
256
+ local root="${1:-}" v
257
+ DIST_PUBLISHED=''; DIST_PUBLISHED_ERR=''; DIST_NPM=''
258
+ # ★検査用の入口。空文字を入れれば「問い合わせに失敗した」を作れる(網を切らずに確かめる)。
214
259
  if [[ -n "${CONDUCTOR_DIST_PUBLISHED+x}" ]]; then
215
- printf '%s' "$CONDUCTOR_DIST_PUBLISHED"
260
+ DIST_PUBLISHED="$CONDUCTOR_DIST_PUBLISHED"
261
+ [[ -n "$DIST_PUBLISHED" ]] || DIST_PUBLISHED_ERR='unverifiable'
262
+ return 0
263
+ fi
264
+ DIST_NPM="$(dist_npm_for "$root")" || DIST_NPM=''
265
+ if [[ -z "$DIST_NPM" ]]; then
266
+ DIST_PUBLISHED_ERR='no-npm'
216
267
  return 0
217
268
  fi
218
- local v
219
- v="$(npm view "$CONDUCTOR_PKG" version "$CONDUCTOR_REGISTRY_ARG" 2>/dev/null | tr -d '[:space:]')"
269
+ # ★npm は `#!/usr/bin/env node` で始まる。PATH の先頭に同じ場所を足して、同じ node で動かす。
270
+ v="$(PATH="$(dirname "$DIST_NPM"):${PATH:-}" "$DIST_NPM" view "$CONDUCTOR_PKG" version "$CONDUCTOR_REGISTRY_ARG" 2>/dev/null | tr -d '[:space:]')"
220
271
  if [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
221
- printf '%s' "${BASH_REMATCH[0]}"
272
+ DIST_PUBLISHED="${BASH_REMATCH[0]}"
273
+ else
274
+ DIST_PUBLISHED_ERR='unverifiable'
275
+ fi
276
+ return 0
277
+ }
278
+
279
+ # 公開 npm が「いま配っている版」(取れなければ空)。★昔からの呼び方を残す。
280
+ dist_published_version() {
281
+ dist_published_query "${1:-}"
282
+ printf '%s' "$DIST_PUBLISHED"
283
+ }
284
+
285
+ #: 入っている版が公開 npm に在るか(yes / no / 空=確かめられない)と、「使わないで」の印の文。
286
+ DIST_INSTALLED_PUBLISHED=''
287
+ DIST_DEPRECATED=''
288
+
289
+ # 入っている版が「公開 npm に在る正規の版」かを確かめる(★便 29babc5d・えふさん確定 2026-10-02 夜)。
290
+ # 公開 npm に版を名指しして聞く(npm view <pkg>@<版> version deprecated --json)。
291
+ # 在る → yes(「使わないで」の印があれば DIST_DEPRECATED に入る)
292
+ # E404 → no(公開されていない=正規の版ではない)
293
+ # ほかの失敗 → 空(確かめられない)
294
+ dist_version_published_query() {
295
+ local root="$1" ver="$2" out rc parsed v
296
+ DIST_INSTALLED_PUBLISHED=''; DIST_DEPRECATED=''
297
+ if [[ -n "${CONDUCTOR_DIST_PUBLISHED+x}" ]]; then
298
+ local list="${CONDUCTOR_DIST_PUBLISHED_VERSIONS:-$CONDUCTOR_DIST_PUBLISHED}"
299
+ [[ -n "$list" ]] || return 0
300
+ DIST_INSTALLED_PUBLISHED='no'
301
+ for v in $list; do [[ "$v" == "$ver" ]] && DIST_INSTALLED_PUBLISHED='yes'; done
302
+ for v in ${CONDUCTOR_DIST_DEPRECATED_VERSIONS:-}; do [[ "$v" == "$ver" ]] && DIST_DEPRECATED='使わないでの印があります'; done
303
+ return 0
222
304
  fi
305
+ [[ -n "$DIST_NPM" ]] || DIST_NPM="$(dist_npm_for "$root")" || DIST_NPM=''
306
+ [[ -n "$DIST_NPM" ]] || return 0
307
+ out="$(PATH="$(dirname "$DIST_NPM"):${PATH:-}" "$DIST_NPM" view "$CONDUCTOR_PKG@$ver" version deprecated --json "$CONDUCTOR_REGISTRY_ARG" 2>/dev/null)"
308
+ rc=$?
309
+ parsed="$(printf '%s' "$out" | python3 -c 'import json, sys
310
+ ver, rc = sys.argv[1], int(sys.argv[2])
311
+ try:
312
+ d = json.loads(sys.stdin.read() or "null")
313
+ except Exception:
314
+ d = None
315
+ if rc == 0 and d == ver:
316
+ print("yes|")
317
+ elif rc == 0 and isinstance(d, dict) and d.get("version") == ver:
318
+ dep = d.get("deprecated") or ""
319
+ print("yes|" + (dep if isinstance(dep, str) else "使わないでの印があります").replace("\n", " ").replace("|", "/"))
320
+ elif isinstance(d, dict) and isinstance(d.get("error"), dict) and d["error"].get("code") == "E404":
321
+ print("no|")
322
+ else:
323
+ print("|")' "$ver" "$rc" 2>/dev/null)"
324
+ [[ "$parsed" == *'|'* ]] || return 0
325
+ DIST_INSTALLED_PUBLISHED="${parsed%%|*}"
326
+ DIST_DEPRECATED="${parsed#*|}"
327
+ return 0
328
+ }
329
+
330
+ # 管制に「入っている版に不具合の印が無いか」を聞く(★便 29babc5d)。結果は DIST_CENTRAL_FLAG。
331
+ # GET <管制>/api/conductor/machines/target?machine=<機体名>&conductorVersion=<入っている版>
332
+ # → 答えの installedConductorFlag が "ok" | "suspect" | "bad" | "unknown"。
333
+ # ★欄が無い・届かない・ログイン画面へ回される・接続設定が無い は、すべて unknown(=印が無い)。
334
+ # ★接続設定は環境変数、無ければ ~/.conductor.env の CONDUCTOR_URL・CONDUCTOR_TOKEN・CONDUCTOR_MACHINE
335
+ # の3つだけを読む(★値は画面にも控えにも出さない・この関数の外へ持ち出さない)。
336
+ dist_central_flag_query() {
337
+ local ver="$1" url="${CONDUCTOR_URL:-}" tok="${CONDUCTOR_TOKEN:-}" machine="${CONDUCTOR_MACHINE:-}"
338
+ local envf="${HOME:-}/.conductor.env" line key val got
339
+ DIST_CENTRAL_FLAG='unknown'
340
+ if [[ ( -z "$url" || -z "$tok" ) && -r "$envf" ]]; then
341
+ while IFS= read -r line || [[ -n "$line" ]]; do
342
+ line="${line#export }"
343
+ key="${line%%=*}"; val="${line#*=}"
344
+ val="${val%\"}"; val="${val#\"}"; val="${val%\'}"; val="${val#\'}"
345
+ case "$key" in
346
+ CONDUCTOR_URL) [[ -n "$url" ]] || url="$val" ;;
347
+ CONDUCTOR_TOKEN) [[ -n "$tok" ]] || tok="$val" ;;
348
+ CONDUCTOR_MACHINE) [[ -n "$machine" ]] || machine="$val" ;;
349
+ esac
350
+ done < "$envf"
351
+ fi
352
+ [[ -n "$url" && -n "$tok" && -n "$ver" ]] || return 0
353
+ [[ -n "$machine" ]] || machine="$(hostname 2>/dev/null)"
354
+ got="$(DCF_URL="$url" DCF_KEY="$tok" DCF_MACHINE="$machine" DCF_VERSION="$ver" python3 -c 'import json, os, urllib.parse, urllib.request
355
+ class NoRedirect(urllib.request.HTTPRedirectHandler):
356
+ def redirect_request(self, *a, **k):
357
+ return None
358
+ q = urllib.parse.urlencode({"machine": os.environ["DCF_MACHINE"], "conductorVersion": os.environ["DCF_VERSION"]})
359
+ req = urllib.request.Request(os.environ["DCF_URL"].rstrip("/") + "/api/conductor/machines/target?" + q,
360
+ headers={"Authorization": "Bearer " + os.environ["DCF_KEY"]})
361
+ try:
362
+ with urllib.request.build_opener(NoRedirect).open(req, timeout=15) as r:
363
+ if "application/json" in (r.headers.get("content-type") or ""):
364
+ f = json.loads(r.read().decode("utf-8")).get("installedConductorFlag")
365
+ print(f if f in ("ok", "suspect", "bad", "unknown") else "unknown")
366
+ except Exception:
367
+ pass' 2>/dev/null)" || got=''
368
+ case "$got" in ok|suspect|bad|unknown) DIST_CENTRAL_FLAG="$got" ;; *) DIST_CENTRAL_FLAG='unknown' ;; esac
369
+ return 0
223
370
  }
224
371
 
225
372
  # 配ってよい中身の置き場を決める。
226
373
  # 返り 0 … DIST_ROOT を配り元にしてよい
227
374
  # 返り 1 … 配ってはいけない(DIST_REASON に理由)
228
375
  # 理由の語(記録にそのまま載る)
229
- # not-installed … 配布物がこの機に入っていない
230
- # no-version … 入っている配布物の版が読めない
231
- # unverifiable … 公開 npm に問い合わせられない(回線・取得先の不調)
232
- # version-drift … 入っている版が公開 npm のいまの版と違う
376
+ # not-installed … 配布物がこの機に入っていない
377
+ # no-version … 入っている配布物の版が読めない
378
+ # no-npm … 公開 npm に問い合わせる npm が見つからない(配布物の node の隣にも PATH にも無い)
379
+ # unverifiable … npm は在るが、公開 npm への問い合わせに失敗した(回線・取得先・npm の不調)
380
+ # unpublished … 入っている版が公開 npm に無い(正規の版ではない)
381
+ # deprecated … 入っている版に「使わないで」の印(npm deprecate)が付いている
382
+ # flagged-suspect … 管制で「疑い」の印が付いている版
383
+ # flagged-bad … 管制で「不具合」の印が付いている版
384
+ # ★2026-10-02 まで在った version-drift(公開の最新と違う)は、もう配らない理由ではない(便 29babc5d)。
233
385
  dist_source_resolve() {
234
- DIST_ROOT=''; DIST_VERSION=''; DIST_PUBLISHED=''; DIST_REASON=''
386
+ DIST_ROOT=''; DIST_VERSION=''; DIST_PUBLISHED=''; DIST_REASON=''; DIST_CENTRAL_FLAG=''
235
387
  local root
236
388
  # ★常駐・耳(dist_runtime_resolve)と同じ探し方を使う=同じ機で別の版を選ばない。
237
389
  if ! dist_pkg_find; then
@@ -244,20 +396,67 @@ dist_source_resolve() {
244
396
  DIST_REASON='no-version'
245
397
  return 1
246
398
  fi
247
- DIST_PUBLISHED="$(dist_published_version)"
399
+ dist_published_query "$root"
248
400
  if [[ -z "$DIST_PUBLISHED" ]]; then
249
401
  # ★ここで「まあいいか」と配らない。問い合わせられない=正規版かどうか分からない、である。
250
- DIST_REASON='unverifiable'
402
+ # ★理由は「npm が無い」と「問い合わせに失敗した」を分ける(回線と言い切らない)。
403
+ DIST_REASON="${DIST_PUBLISHED_ERR:-unverifiable}"
251
404
  return 1
252
405
  fi
253
- if [[ "$DIST_VERSION" != "$DIST_PUBLISHED" ]]; then
254
- DIST_REASON='version-drift'
406
+ # ★公開の最新と同じなら、それだけで公開 npm に在る正規の版(問い合わせを1回で済ませる)。
407
+ if [[ "$DIST_VERSION" == "$DIST_PUBLISHED" ]]; then
408
+ DIST_INSTALLED_PUBLISHED='yes'; DIST_DEPRECATED=''
409
+ else
410
+ dist_version_published_query "$root" "$DIST_VERSION"
411
+ fi
412
+ case "$DIST_INSTALLED_PUBLISHED" in
413
+ yes) ;;
414
+ no) DIST_REASON='unpublished'; return 1 ;;
415
+ *) DIST_REASON='unverifiable'; return 1 ;;
416
+ esac
417
+ if [[ -n "$DIST_DEPRECATED" ]]; then
418
+ DIST_REASON='deprecated'
255
419
  return 1
256
420
  fi
421
+ dist_central_flag_query "$DIST_VERSION"
422
+ case "$DIST_CENTRAL_FLAG" in
423
+ suspect) DIST_REASON='flagged-suspect'; return 1 ;;
424
+ bad) DIST_REASON='flagged-bad'; return 1 ;;
425
+ esac
257
426
  DIST_ROOT="$root"
258
427
  return 0
259
428
  }
260
429
 
430
+ # 自動更新が入っている機か(自社PCの鍵 ~/.tyhld/bin/autoupdate/enabled が在るか・便 29babc5d)。
431
+ dist_autoupdate_enabled() {
432
+ [[ -f "${HOME:-}/.tyhld/bin/autoupdate/enabled" ]]
433
+ }
434
+
435
+ # 版がそろっていないときに人が読む「次の1手」を1行ずつ出す(★文言の正本は1か所=ここ)。
436
+ # 【なぜ分けるか(設計 3ed9e2b2 の 3/4「いまの決まりが書いてある場所(11)」)】
437
+ # 自動更新が入っている機(自社PC)は、更新係が管制の決めた版へそろえる=人が npm で入れると
438
+ # 段階配布の順番が崩れる。入っていない機(お客さまの機・自動更新を入れる前の機)だけ、人が入れ直す。
439
+ # 引数: 理由の語(DIST_REASON)。
440
+ dist_human_step() {
441
+ local reason="${1:-}"
442
+ case "$reason" in
443
+ flagged-bad|flagged-suspect)
444
+ echo "入っている版 ${DIST_VERSION:-不明} に、管制で ${reason#flagged-} の印が付いています。"
445
+ if dist_autoupdate_enabled; then
446
+ echo "この機は自動更新が入っています。管制が直した版を決めれば、次の自動更新で入れ替わります(人の操作は要りません)。"
447
+ else
448
+ echo "人の手番: CC1 に知らせてください(直した版が公開されたら npm install -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org)。"
449
+ fi
450
+ return 0 ;;
451
+ esac
452
+ if dist_autoupdate_enabled; then
453
+ echo "この機は自動更新が入っています。次の自動更新(管制が決めた版を、作業中の便が終わってから入れます)でそろいます。管制の「PCの状態」で確かめてください。"
454
+ echo " 自動更新が止まっているときだけ CC1 に知らせてください(人が npm で入れると段階配布の順番が崩れます)。"
455
+ else
456
+ echo "人の手番: npm install -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org"
457
+ fi
458
+ }
459
+
261
460
  # ─── 常駐・耳の入口(便 81a08d58)──────────────────────────────────────────
262
461
  #: 入口を置く場所。★node の版が変わっても動かない場所(~/.tyhld の下・職人は書けない)。
263
462
  DIST_RUNTIME_BIN_DIR="${DIST_RUNTIME_BIN_DIR:-${HOME:-}/.tyhld/bin}"