@tyhld/conductor 0.12.85 → 0.12.87

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/dist/selfcheck.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * 【結果】手元の控え(~/.local/state/conductor/selfcheck/last.json と history/)に必ず残す。
22
22
  * 管制の口が在れば送る(★口が無い・届かないときも落ちない=送れなかったことを控えに書くだけ)。
23
- * 送る中身は buildSelfcheckPayload の1か所(口の形は ai-kana の便 2f5eb92c が決める)。
23
+ * 送る中身は buildSelfcheckPayload の1か所(口の形は ai-kana の便 2f5eb92c が決めた形・便 29babc5d で合わせた)。
24
24
  */
25
25
  import { spawnSync } from 'node:child_process';
26
26
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
@@ -33,7 +33,12 @@ import { readDrain } from './autoupdate-flags.js';
33
33
  // ─────────────────────────────────────────────────────────────────────────────
34
34
  // 形
35
35
  // ─────────────────────────────────────────────────────────────────────────────
36
- export const SELFCHECK_TRIGGERS = ['daily', 'after-conductor', 'after-claude', 'after-rollback', 'manual'];
36
+ /**
37
+ * きっかけの名前。管制が知っているのは daily / after-conductor / after-claude / after-rollback の4つで、
38
+ * 知らない語(manual・candidate)は管制が null に倒す(便 2f5eb92c の決まり)。
39
+ * candidate … 更新係が新しい候補の版を見つけたときに流す(候補を管制へ届けるため・便 29babc5d)
40
+ */
41
+ export const SELFCHECK_TRIGGERS = ['daily', 'after-conductor', 'after-claude', 'after-rollback', 'manual', 'candidate'];
37
42
  export const defaultExec = (cmd, args, opts = {}) => {
38
43
  const r = spawnSync(cmd, args, {
39
44
  cwd: opts.cwd,
@@ -57,8 +62,16 @@ export const defaultExec = (cmd, args, opts = {}) => {
57
62
  export function selfcheckDir(home) {
58
63
  return path.join(home, '.local', 'state', 'conductor', 'selfcheck');
59
64
  }
60
- /** 管制の受け口(★1か所。口の形は ai-kana の便 2f5eb92c に合わせてここだけ直す)。 */
61
- export const SELFCHECK_ENDPOINT = '/api/conductor/selfcheck';
65
+ /**
66
+ * 管制の受け口(★1か所)。ai-kana の便 2f5eb92c が作った「機体の自己診断の結果と版を受ける口」。
67
+ * ★以前は /api/conductor/selfcheck へ別の形で送っていて、管制に届いていなかった(便 29babc5d で直した)。
68
+ */
69
+ export const SELFCHECK_ENDPOINT = '/api/conductor/machines/selfcheck';
70
+ /**
71
+ * Claude Code の公式の安定版の番号が置かれている所(★公式の導入台本 https://claude.ai/install.sh が
72
+ * 版を取りに行くのと同じ置き場。2026-10-02 に台本の中身で確かめた・stable=2.1.285)。
73
+ */
74
+ export const CLAUDE_STABLE_URL = 'https://downloads.claude.ai/claude-code-releases/stable';
62
75
  /** 控えの数の上限(古い物から消す)。 */
63
76
  export const SELFCHECK_HISTORY_MAX = 30;
64
77
  /** 小さな試験の上限時間。 */
@@ -367,9 +380,10 @@ export function npmBesideNode(execPath = process.execPath) {
367
380
  const beside = path.join(path.dirname(execPath), 'npm');
368
381
  return existsSync(beside) ? beside : null;
369
382
  }
370
- /** ⑤ GitHub・npm(外側。内側は小さな試験の後で足す) */
383
+ /** ⑤ GitHub・npm(外側。内側は小さな試験の後で足す)。公開 npm の latest も返す(候補に使う)。 */
371
384
  function checkNetworkOutside(d, version) {
372
385
  const item = { key: 'network', label: 'GitHub・npm への通信', ok: null, detail: [] };
386
+ let latest = null;
373
387
  const npm = d.findNpm();
374
388
  if (!npm)
375
389
  fail(item, 'fault', `配布物の node の隣に npm がありません(${path.dirname(process.execPath)})`);
@@ -377,6 +391,8 @@ function checkNetworkOutside(d, version) {
377
391
  const env = { ...d.env, PATH: `${path.dirname(npm)}:${d.env.PATH ?? ''}` };
378
392
  const r = d.exec(npm, ['view', '@tyhld/conductor', 'version', '--@tyhld:registry=https://registry.npmjs.org'], { env, timeoutMs: 60_000 });
379
393
  const v = r.stdout.trim().match(/^\d+\.\d+\.\d+\S*/)?.[0];
394
+ if (r.status === 0 && v)
395
+ latest = v;
380
396
  if (r.status === 0 && v)
381
397
  pass(item, `公開 npm の @tyhld/conductor = ${v}${version && v !== version ? `(この機は ${version})` : ''}`);
382
398
  else
@@ -400,7 +416,35 @@ function checkNetworkOutside(d, version) {
400
416
  else
401
417
  fail(item, 'fault', `GitHub の API へ届きません(終了 ${api.status})`);
402
418
  }
403
- return item;
419
+ return { item, latest };
420
+ }
421
+ const SEMVER = /^\d+\.\d+\.\d+$/;
422
+ /** 公式の安定版の番号を聞く(★取れなければ null・自己診断の合否には使わない)。 */
423
+ export async function fetchClaudeStable(fetchImpl) {
424
+ try {
425
+ const res = await fetchImpl(CLAUDE_STABLE_URL, { redirect: 'manual', signal: AbortSignal.timeout(15_000) });
426
+ if (!res.ok)
427
+ return null;
428
+ const v = (await res.text()).trim();
429
+ return SEMVER.test(v) ? v : null;
430
+ }
431
+ catch {
432
+ return null;
433
+ }
434
+ }
435
+ /**
436
+ * 候補の版(入っている版より新しい物だけ)。★先行機かどうかはこの機では決めない(管制が決める)。
437
+ * conductor … 公開 npm の latest
438
+ * claude-code … 公式の安定版(えふさん確定 2026-10-02 夜:安定版を追う)
439
+ */
440
+ export function pickCandidates(installed, found) {
441
+ const out = [];
442
+ const newer = (v, now) => v !== null && SEMVER.test(v) && (now === null || !SEMVER.test(now) || compareVersions(v, now) > 0);
443
+ if (newer(found.conductor, installed.conductor))
444
+ out.push({ component: 'conductor', version: found.conductor });
445
+ if (newer(found.claude, installed.claude))
446
+ out.push({ component: 'claude-code', version: found.claude });
447
+ return out;
404
448
  }
405
449
  /** 囲いの設定だけを配布物の雛形から取り出す(★作業場所が現場と同じ囲いになるように)。 */
406
450
  function siteSettingsFor(distRoot) {
@@ -499,18 +543,41 @@ function checkGuard(d, work) {
499
543
  }
500
544
  return item;
501
545
  }
502
- /** 管制へ送る中身(★1か所)。口の形が決まったらここだけ合わせる。 */
546
+ /** 項目ごとの合否(管制の checks)。見られなかった項目は skip。 */
547
+ export function checksOf(r) {
548
+ const out = {};
549
+ for (const i of r.items)
550
+ out[i.key] = i.ok === true ? 'pass' : i.ok === false ? 'fail' : 'skip';
551
+ return out;
552
+ }
553
+ /**
554
+ * 管制へ送る中身(★1か所)。管制の決まり(便 2f5eb92c・POST /api/conductor/machines/selfcheck)の欄で送る。
555
+ * 必須: machine・result(★小文字の "pass" / "fail")
556
+ * 決まりの欄: trigger・checks・conductorVersion・conductorBin・claudeVersion・claudeBin・rolledBack・
557
+ * humanActionKind・humanActionCommand・candidates
558
+ * ★いままでの詳しい中身(items・humanActions・mcp・drain・claudePin・usage)も一緒に載せる
559
+ * (管制は知らない欄を黙って捨てる=害が無い。手元の控えと同じ中身を残しておける)。
560
+ * ★rolledBack は「直前に旧版へ戻したか」=きっかけが after-rollback のときだけ true。
561
+ * ★人の手番は管制の行が1つなので、最初の1件を載せる(全部は humanActions に在る)。
562
+ */
503
563
  export function buildSelfcheckPayload(r) {
564
+ const first = r.humanActions[0];
504
565
  return {
505
566
  schema: r.schema,
506
567
  machine: r.machine,
507
568
  trigger: r.trigger,
508
- result: r.result,
569
+ result: r.result === 'PASS' ? 'pass' : 'fail',
570
+ checks: checksOf(r),
509
571
  startedAt: r.startedAt,
510
572
  finishedAt: r.finishedAt,
511
573
  conductorVersion: r.conductorVersion,
574
+ conductorBin: r.conductorBin,
512
575
  claudeVersion: r.claude.version,
513
576
  claudeBin: r.claude.bin,
577
+ rolledBack: r.trigger === 'after-rollback',
578
+ humanActionKind: first ? first.what : null,
579
+ humanActionCommand: first ? first.command : null,
580
+ candidates: r.candidates,
514
581
  claudePin: r.claude.pin.state === 'none' ? null : r.claude.pin,
515
582
  drain: r.drain,
516
583
  mcp: r.mcp,
@@ -556,8 +623,21 @@ export async function sendSelfcheck(d, r) {
556
623
  signal: AbortSignal.timeout(20_000),
557
624
  });
558
625
  const json = (res.headers.get('content-type') ?? '').includes('application/json');
559
- if (res.ok && json)
626
+ if (res.ok && json) {
627
+ // ★管制は表がまだ無い(SQL を貼る前)とき 200 で { ok:false, note:「まだ準備中」 } と答える。
628
+ // 受け取られていないので「送れた」と数えない(落ちもしない)。
629
+ let body = {};
630
+ try {
631
+ body = (await res.json());
632
+ }
633
+ catch {
634
+ return { sent: false, detail: `HTTP ${res.status}(答えを読めません)` };
635
+ }
636
+ if (body.ok === false) {
637
+ return { sent: false, detail: `HTTP ${res.status}(管制は受け取っていません: ${typeof body.note === 'string' ? body.note : '理由なし'})` };
638
+ }
560
639
  return { sent: true, detail: `HTTP ${res.status}` };
640
+ }
561
641
  if (res.ok || (res.status >= 300 && res.status < 400)) {
562
642
  return { sent: false, detail: `HTTP ${res.status}(JSON の答えではない=管制にまだ受け口がありません)` };
563
643
  }
@@ -578,7 +658,8 @@ export async function runSelfcheck(d, trigger) {
578
658
  const claude = checkClaude(d);
579
659
  const central = await checkCentral(d);
580
660
  const sandbox = checkSandboxParts(d);
581
- const network = checkNetworkOutside(d, version);
661
+ const net = checkNetworkOutside(d, version);
662
+ const network = net.item;
582
663
  const guard = checkGuard(d, path.join(runDir, 'work'));
583
664
  // ⑥ 小さな試験(③の MCP・④の書き込み・⑤の内側の通信もここで見る)
584
665
  const headless = { key: 'headless', label: '小さな headless 試験', ok: null, detail: [] };
@@ -640,6 +721,7 @@ export async function runSelfcheck(d, trigger) {
640
721
  }
641
722
  const items = [conductor, claude.item, central, sandbox, network, headless, guard];
642
723
  const humanActions = items.flatMap((i) => i.human ?? []);
724
+ const candidates = pickCandidates({ conductor: version, claude: claude.version }, { conductor: net.latest, claude: await fetchClaudeStable(d.fetchImpl) });
643
725
  return {
644
726
  schema: 'conductor-selfcheck/1',
645
727
  machine: d.machine,
@@ -661,6 +743,8 @@ export async function runSelfcheck(d, trigger) {
661
743
  durationMs: sum.result.durationMs,
662
744
  }
663
745
  : null,
746
+ conductorBin: path.join(d.distRoot, 'dist', 'cli.js'),
747
+ candidates,
664
748
  };
665
749
  }
666
750
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.85",
3
+ "version": "0.12.87",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,8 @@
23
23
  "scripts/dist-source.sh",
24
24
  "scripts/conductor-run.sh",
25
25
  "scripts/tyhld-selfcheck.sh",
26
+ "scripts/tyhld-autoupdate.sh",
27
+ "scripts/autoupdate-install.sh",
26
28
  "scripts/kc.sh",
27
29
  "scripts/codex",
28
30
  "!scripts/codex/__pycache__",
@@ -106,6 +106,12 @@ APT_PACKAGES=(
106
106
  python3-pip
107
107
  python3-venv
108
108
  rsync
109
+ # ★囲い(sandbox)の部品。無いと職人が起動を断る(設定 sandbox.failIfUnavailable のため)。
110
+ # えふさん確定 2026-10-02・判断1(b)=1行インストーラでここまで入れる。
111
+ # 実物(2026-10-02・ノートPC): これが無くて便 7901973d が3回続けて起動に失敗した。
112
+ # ★先頭の確かめ(preflight)は残す=入れられなかった機では止まって案内する。
113
+ bubblewrap # 囲いの本体。職人の書き込みを決めた場所だけに閉じる
114
+ socat # 囲いの中と外をつなぐ通信の中継
109
115
  )
110
116
 
111
117
  # 確認だけモード(何も入れない・何も聞かない)。
@@ -387,7 +393,7 @@ run() {
387
393
  "$@"
388
394
  }
389
395
 
390
- # >>> preflight >>> ★この印の間が正本。install.sh と update-guard.sh で1文字も違わないことをテストが固定する。
396
+ # >>> preflight >>> ★この印の間が正本。install.sh・update-guard.sh・setup.sh の3つで1文字も違わないことをテストが固定する。
391
397
  # ═════════════════════════════════════════════════════════════════════════════
392
398
  # 先頭の確かめ(職人が始まるのに要る物・便 ff8f4be9)
393
399
  #
@@ -432,6 +438,14 @@ preflight_ver_lt() {
432
438
  return 1
433
439
  }
434
440
 
441
+ # ホームの外の置き場(★src/headless.ts の CLAUDE_SYSTEM_BIN_DIRS と同じ・テストで一致を固定)。
442
+ # ★検査だけが作り物のフォルダに差し替える(CONDUCTOR_PREFLIGHT_SYSTEM_DIRS)。
443
+ # HOME を隔離してもここは絶対パスなので、機体に claude が在ると検査が機体に依って落ちる
444
+ # (実測 2026-10-02 メインPC: /usr/bin/claude = 2.1.86)。中継の側は便 ff8f4be9 で
445
+ # 同じ差し替え(resolveClaudeBin(systemDirs))を入れてある。作りをそろえる。
446
+ # ★差し替えても安全は変わらない(探す場所が変わるだけ・見つからなければ need では止まる)。
447
+ PREFLIGHT_SYSTEM_BIN_DIRS="${CONDUCTOR_PREFLIGHT_SYSTEM_DIRS-/usr/local/bin:/usr/bin}"
448
+
435
449
  # 中継と同じ候補を同じ順で出す(1行に1つ)。
436
450
  preflight_claude_candidates() {
437
451
  if [[ -n "${CONDUCTOR_CLAUDE_BIN:-}" ]]; then
@@ -440,6 +454,7 @@ preflight_claude_candidates() {
440
454
  fi
441
455
  local d
442
456
  local -a nv=()
457
+ local -a sysdirs=()
443
458
  for d in "$HOME/.nvm/versions/node"/v*; do
444
459
  [[ -d "$d" ]] && nv+=("${d##*/}")
445
460
  done
@@ -449,7 +464,12 @@ preflight_claude_candidates() {
449
464
  printf '%s\n' "$HOME/.nvm/versions/node/$d/bin/claude"
450
465
  done < <(printf '%s\n' "${nv[@]}" | sort -t. -k1.2,1nr -k2,2nr -k3,3nr)
451
466
  fi
452
- printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude" /usr/local/bin/claude /usr/bin/claude
467
+ printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude"
468
+ IFS=':' read -r -a sysdirs <<< "$PREFLIGHT_SYSTEM_BIN_DIRS"
469
+ for d in ${sysdirs[@]+"${sysdirs[@]}"}; do
470
+ [[ -n "$d" ]] && printf '%s\n' "$d/claude"
471
+ done
472
+ return 0
453
473
  }
454
474
 
455
475
  # その claude の入れ方に合う更新の1行(src/headless.ts の claudeUpdateHint と同じ見分け方)。
@@ -287,6 +287,207 @@ else
287
287
  fi
288
288
  SVC_RESTART_HINT="${SVC_RESTART_CMD} を実行してください"
289
289
 
290
+ # ─── ⓪ 職人が始まるのに要る物の確かめ(★install.sh・update-guard.sh と同じ正本) ──
291
+ # 【なぜ setup.sh にも入れるか(えふさん確定 2026-10-02・判断5-1/前便 ff8f4be9 の要判断3)】
292
+ # setup.sh を単独で打つ道(Mac・試作の型・開発機の作り直し)は、1行インストーラを通らない。
293
+ # その道だけ確かめが無いと、囲いの部品が無い機・古い Claude Code の機に設置してしまい、
294
+ # 設置は成功したのに職人が1件も始まらない(実物 2026-10-02・便 7901973d が3回続けて起動失敗)。
295
+ # ★印の間は3つの台本で1文字も違わない(test/preflight.test.ts が固定する)。ここを手で直さない。
296
+ # >>> preflight >>> ★この印の間が正本。install.sh・update-guard.sh・setup.sh の3つで1文字も違わないことをテストが固定する。
297
+ # ═════════════════════════════════════════════════════════════════════════════
298
+ # 先頭の確かめ(職人が始まるのに要る物・便 ff8f4be9)
299
+ #
300
+ # 【なぜ要るか(実機 2026-10-02・ノートPC)】
301
+ # 便 7901973d が3回続けて起動に失敗した。囲いの部品 bubblewrap・socat が入っておらず、
302
+ # 設定 sandbox.failIfUnavailable のため職人が起動を断っていた。更新までは気づかれずに動き、
303
+ # 更新の後に職人が始まらなくなった。また、最低版(えふさん確定 2.1.282)より古い
304
+ # Claude Code では中継が職人を始めない。=設定を配る前にここで気づき、人が打つ1行を出す。
305
+ #
306
+ # 【決め】
307
+ # ・確かめるだけ。★台本が自分で sudo を打たない・勝手に入れない(人の窓)。
308
+ # ・Claude Code は中継(src/headless.ts の resolveClaudeBin)と同じ場所を同じ順で探し、
309
+ # いちばん新しい版で判定する(CONDUCTOR_CLAUDE_BIN の指定があればそれだけ)。
310
+ # ・囲いの部品は、その機の仕組みに要る物だけを見る。
311
+ # Linux(WSL を含む)… bwrap と socat
312
+ # Mac … OS に組み込み(追加の部品は要らない)=何も求めない
313
+ # それ以外 … 止めずに「確かめられませんでした」と出す
314
+ # ・出す1行は、そのままコピペで動く形(山かっこの穴埋めなし)。
315
+ #
316
+ # 【使い方】preflight_check <claude が無いときの扱い: need|later>
317
+ # 結果: PREFLIGHT_NG(足りない物の説明と1行。空なら通過)/PREFLIGHT_INFO(止めない注意)
318
+ # /PREFLIGHT_CLAUDE_BIN(中継が使うはずの claude・見つからなければ空)
319
+ # ★later … 後の段で Claude Code を入れる台本(1行インストーラ)用。無いことだけは止めない
320
+ # (古いときは止める)。
321
+ # ═════════════════════════════════════════════════════════════════════════════
322
+ # ★src/headless.ts の CLAUDE_MIN_VERSION と同じ値(テストで一致を固定)。
323
+ PREFLIGHT_CLAUDE_MIN='2.1.282'
324
+
325
+ # a が b より古いか(数字3つだけを見る。読めない字は 0=古い扱い)。
326
+ preflight_ver_lt() {
327
+ local a="${1%%-*}" b="${2%%-*}" i x y
328
+ local -a A B
329
+ IFS='.' read -r -a A <<< "$a"
330
+ IFS='.' read -r -a B <<< "$b"
331
+ for i in 0 1 2; do
332
+ x="${A[i]:-0}"; y="${B[i]:-0}"
333
+ [[ "$x" =~ ^[0-9]+$ ]] || x=0
334
+ [[ "$y" =~ ^[0-9]+$ ]] || y=0
335
+ if (( x < y )); then return 0; fi
336
+ if (( x > y )); then return 1; fi
337
+ done
338
+ return 1
339
+ }
340
+
341
+ # ホームの外の置き場(★src/headless.ts の CLAUDE_SYSTEM_BIN_DIRS と同じ・テストで一致を固定)。
342
+ # ★検査だけが作り物のフォルダに差し替える(CONDUCTOR_PREFLIGHT_SYSTEM_DIRS)。
343
+ # HOME を隔離してもここは絶対パスなので、機体に claude が在ると検査が機体に依って落ちる
344
+ # (実測 2026-10-02 メインPC: /usr/bin/claude = 2.1.86)。中継の側は便 ff8f4be9 で
345
+ # 同じ差し替え(resolveClaudeBin(systemDirs))を入れてある。作りをそろえる。
346
+ # ★差し替えても安全は変わらない(探す場所が変わるだけ・見つからなければ need では止まる)。
347
+ PREFLIGHT_SYSTEM_BIN_DIRS="${CONDUCTOR_PREFLIGHT_SYSTEM_DIRS-/usr/local/bin:/usr/bin}"
348
+
349
+ # 中継と同じ候補を同じ順で出す(1行に1つ)。
350
+ preflight_claude_candidates() {
351
+ if [[ -n "${CONDUCTOR_CLAUDE_BIN:-}" ]]; then
352
+ printf '%s\n' "$CONDUCTOR_CLAUDE_BIN"
353
+ return 0
354
+ fi
355
+ local d
356
+ local -a nv=()
357
+ local -a sysdirs=()
358
+ for d in "$HOME/.nvm/versions/node"/v*; do
359
+ [[ -d "$d" ]] && nv+=("${d##*/}")
360
+ done
361
+ if (( ${#nv[@]} > 0 )); then
362
+ # 新しい node の版から(v24.21.0 → 24 21 0 で降順)
363
+ while IFS= read -r d; do
364
+ printf '%s\n' "$HOME/.nvm/versions/node/$d/bin/claude"
365
+ done < <(printf '%s\n' "${nv[@]}" | sort -t. -k1.2,1nr -k2,2nr -k3,3nr)
366
+ fi
367
+ printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude"
368
+ IFS=':' read -r -a sysdirs <<< "$PREFLIGHT_SYSTEM_BIN_DIRS"
369
+ for d in ${sysdirs[@]+"${sysdirs[@]}"}; do
370
+ [[ -n "$d" ]] && printf '%s\n' "$d/claude"
371
+ done
372
+ return 0
373
+ }
374
+
375
+ # その claude の入れ方に合う更新の1行(src/headless.ts の claudeUpdateHint と同じ見分け方)。
376
+ preflight_claude_update_line() {
377
+ local bin="$1" real="$1" t i
378
+ # 実体を辿る(★readlink -f は使わない=無いパスでも文字列を返すため。1段ずつ辿る)
379
+ for i in 1 2 3 4 5 6 7 8; do
380
+ [[ -L "$real" ]] || break
381
+ t="$(readlink "$real" 2>/dev/null || true)"
382
+ [[ -n "$t" ]] || break
383
+ case "$t" in
384
+ /*) real="$t" ;;
385
+ *) real="$(dirname "$real")/$t" ;;
386
+ esac
387
+ done
388
+ case "$real$bin" in
389
+ */node_modules/@anthropic-ai/claude-code/*|*/.nvm/versions/node/*)
390
+ printf ' PATH="%s:$PATH" npm install -g @anthropic-ai/claude-code@latest\n' "$(dirname "$bin")"
391
+ return 0 ;;
392
+ esac
393
+ case "$real" in
394
+ "$HOME"/.local/share/claude/*|"$HOME"/.claude/local/*)
395
+ printf ' "%s" update\n' "$bin"
396
+ return 0 ;;
397
+ esac
398
+ printf ' (入れ方を見分けられませんでした。どちらか合う方を打ってください)\n'
399
+ printf ' ネイティブ導入なら: "%s" update\n' "$bin"
400
+ printf ' npm 導入なら : npm install -g @anthropic-ai/claude-code@latest\n'
401
+ }
402
+
403
+ preflight_check() {
404
+ local claude_mode="${1:-need}" c v best='' best_v='' unknown='' os missing=''
405
+ PREFLIGHT_NG=''
406
+ PREFLIGHT_INFO=''
407
+ PREFLIGHT_CLAUDE_BIN=''
408
+
409
+ # ── ① Claude Code が在り、最低版以上か ──
410
+ while IFS= read -r c; do
411
+ [[ -n "$c" && -x "$c" ]] || continue
412
+ v="$("$c" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)"
413
+ if [[ -z "$v" ]]; then
414
+ [[ -n "$unknown" ]] || unknown="$c"
415
+ continue
416
+ fi
417
+ if [[ -z "$best_v" ]] || preflight_ver_lt "$best_v" "$v"; then
418
+ best="$c"; best_v="$v"
419
+ fi
420
+ done < <(preflight_claude_candidates)
421
+
422
+ # shellcheck disable=SC2034 # update-guard.sh の ⓪-b が使う(install.sh では使わない)
423
+ PREFLIGHT_CLAUDE_BIN="${best:-$unknown}"
424
+ if [[ -n "$best_v" ]]; then
425
+ if preflight_ver_lt "$best_v" "$PREFLIGHT_CLAUDE_MIN"; then
426
+ PREFLIGHT_NG+="■ 職人のソフト(Claude Code)が古いです(${best} = ${best_v}、必要な版 ${PREFLIGHT_CLAUDE_MIN} 以上)。"$'\n'
427
+ PREFLIGHT_NG+=" この機の入れ方に合う1行:"$'\n'
428
+ PREFLIGHT_NG+="$(preflight_claude_update_line "$best")"$'\n'
429
+ else
430
+ PREFLIGHT_INFO+="✓ 職人のソフト(Claude Code): ${best} = ${best_v}(必要な版 ${PREFLIGHT_CLAUDE_MIN} 以上)"$'\n'
431
+ fi
432
+ elif [[ -n "$unknown" ]]; then
433
+ # ★中継も版の読めない claude は止めずに使う。ここでも止めない(作りをそろえる)。
434
+ PREFLIGHT_INFO+="⚠ 職人のソフト(Claude Code)の版を確かめられませんでした(${unknown})。"$'\n'
435
+ elif [[ "$claude_mode" == "need" ]]; then
436
+ PREFLIGHT_NG+="■ 職人のソフト(Claude Code)が見つかりません。"$'\n'
437
+ PREFLIGHT_NG+=" 入れる1行:"$'\n'
438
+ PREFLIGHT_NG+=" npm install -g @anthropic-ai/claude-code@latest"$'\n'
439
+ fi
440
+
441
+ # ── ② 囲いの部品(その機の仕組みに要る物だけ) ──
442
+ os="$(uname -s 2>/dev/null || echo unknown)"
443
+ case "$os" in
444
+ Linux)
445
+ command -v bwrap >/dev/null 2>&1 || missing+=' bwrap(bubblewrap)'
446
+ command -v socat >/dev/null 2>&1 || missing+=' socat'
447
+ if [[ -n "$missing" ]]; then
448
+ PREFLIGHT_NG+="■ 囲いの部品が足りません:${missing}"$'\n'
449
+ PREFLIGHT_NG+=" (無いと、職人が安全のため起動を断ります)"$'\n'
450
+ if command -v apt-get >/dev/null 2>&1; then
451
+ PREFLIGHT_NG+=" 入れる1行(Ubuntu のパスワードを聞かれます):"$'\n'
452
+ PREFLIGHT_NG+=" sudo apt-get install -y bubblewrap socat"$'\n'
453
+ # ★WSL かどうか(検査は CONDUCTOR_PROC_VERSION_FILE で見本を渡せる・既定は本物)
454
+ if [[ -r "${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}" ]] && grep -qi microsoft "${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}"; then
455
+ PREFLIGHT_NG+=" sudo のパスワードが分からないとき(Windows の PowerShell で打つ):"$'\n'
456
+ PREFLIGHT_NG+=" wsl.exe -u root apt-get install -y bubblewrap socat"$'\n'
457
+ fi
458
+ else
459
+ PREFLIGHT_NG+=" この機の入れ方で bubblewrap と socat を入れてください(apt-get が見つかりません)。"$'\n'
460
+ fi
461
+ else
462
+ PREFLIGHT_INFO+="✓ 囲いの部品: bwrap・socat"$'\n'
463
+ fi
464
+ ;;
465
+ Darwin)
466
+ PREFLIGHT_INFO+="✓ 囲いの部品: Mac は OS に組み込み(追加の部品は要りません)"$'\n'
467
+ ;;
468
+ *)
469
+ PREFLIGHT_INFO+="⚠ 囲いの部品: この機(${os})では確かめられませんでした(止めずに進みます)。"$'\n'
470
+ ;;
471
+ esac
472
+ [[ -z "$PREFLIGHT_NG" ]]
473
+ }
474
+ # <<< preflight <<<
475
+
476
+ step "⓪ 職人が始まるのに要る物の確かめ(★ここで足りなければ1つも置きません)"
477
+ # ★`later`(職人が【無いこと】だけはここで止めない)を使う理由。
478
+ # setup.sh は Claude Code を入れないが、「無い」ときの案内は ① が持っている
479
+ # (入れる2行+ログインの手順=お客さまがそのまま読める形)。ここで `need` にすると
480
+ # その案内に届かず、短い1行だけで終わってしまう。★古い版・囲いの部品不足は、ここで止める。
481
+ if preflight_check later; then
482
+ if [[ -n "$PREFLIGHT_INFO" ]]; then printf '%s' "$PREFLIGHT_INFO"; fi
483
+ else
484
+ printf '%s' "$PREFLIGHT_INFO" >&9
485
+ echo >&9
486
+ printf '%s' "$PREFLIGHT_NG" >&9
487
+ echo "[setup] 上の1行を打ってから、もう一度 setup.sh を実行してください。" >&9
488
+ exit 1
489
+ fi
490
+
290
491
  # ─── ① 前提チェック ──────────────────────────────────────────────────────────
291
492
  step "① 前提チェック"
292
493