@cocorograph/hub-agent 0.8.66 → 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 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
- console.log(`installed: ${r.platform} ${r.path}`)
219
- console.log(`bin: ${r.bin}`)
220
- if (r.platform === "darwin") {
221
- console.log(`label: ${r.label}`)
222
- console.log("確認: launchctl list | grep co.cocorograph.hub-agent")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.66",
3
+ "version": "0.8.67",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1347,8 +1347,11 @@ verify_setup() {
1347
1347
  done < <(_hub_target_clis)
1348
1348
 
1349
1349
  # 4. OS サービス起動 (Darwin 限定の確認。Linux は systemctl --user で別途)
1350
+ # ⚠️ 判定は `launchctl print` で行う。install-service 側が登録の出現を待つのに使う
1351
+ # 道具と揃えるため (src/service-install.mjs の waitUntilBootedIn)。`launchctl list | grep`
1352
+ # に戻すと、待つ側と確認する側で別の状態を見ることになり誤検知が復活する。
1350
1353
  if [[ "$(uname)" == "Darwin" ]]; then
1351
- if launchctl list 2>/dev/null | grep -q "co.cocorograph.hub-agent"; then
1354
+ if launchctl print "gui/$(id -u)/co.cocorograph.hub-agent" >/dev/null 2>&1; then
1352
1355
  color_ok "launchd service 起動中 (co.cocorograph.hub-agent)"
1353
1356
  else
1354
1357
  color_warn "launchd service が見つかりません"
@@ -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
- // 非 0 = 登録に存在しない = bootout 完了
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
- export async function installService({ bin } = {}) {
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
- // 既存ロード解除 → (登録が消えるまで待機) → bootstrap (リトライ) → kickstart。
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,