@cocorograph/hub-agent 0.5.21 → 0.5.24

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": "@cocorograph/hub-agent",
3
- "version": "0.5.21",
3
+ "version": "0.5.24",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -21,7 +21,17 @@
21
21
 
22
22
  set -euo pipefail
23
23
 
24
- NODE_MIN_MAJOR=20
24
+ # Node.js のサポート範囲ポリシー(Active LTS のみ)。
25
+ # - 既存 node のメジャーが [MIN, MAX] に収まっていれば現状維持
26
+ # - 範囲外なら NODE_DEFAULT_BREW_FORMULA (最新 Active LTS) にアップ / ダウングレード
27
+ # 2026-05 時点の LTS スケジュール:
28
+ # - node 22: Active LTS → 2027/04 EoL
29
+ # - node 24: Active LTS → 2028/04 EoL(最新、サポート期間最長)
30
+ # - node 26: current(LTS ではない、SSL/CA 周りの問題報告あり)
31
+ NODE_MIN_MAJOR=22
32
+ NODE_MAX_MAJOR=24
33
+ NODE_DEFAULT_BREW_FORMULA="node@24"
34
+
25
35
  PACKAGE_NAME="@cocorograph/hub-agent"
26
36
  CLAUDE_CODE_PACKAGE="@anthropic-ai/claude-code"
27
37
 
@@ -225,23 +235,55 @@ ensure_pkg() {
225
235
  fi
226
236
  }
227
237
 
238
+ # macOS で brew 経由で Active LTS の node (NODE_DEFAULT_BREW_FORMULA) を導入/切替する。
239
+ # 既存の無印 node が link されていれば unlink してから新 formula を link --overwrite --force。
240
+ _install_node_lts_brew() {
241
+ local formula="$NODE_DEFAULT_BREW_FORMULA"
242
+ color_step "$formula (Active LTS) を install"
243
+ brew install "$formula"
244
+ # 既存無印 node が link されていれば外す(v26 current 等を退かす)
245
+ if brew list node >/dev/null 2>&1; then
246
+ color_step "既存 'node' formula を unlink ($formula を優先するため)"
247
+ brew unlink node 2>/dev/null || true
248
+ fi
249
+ color_step "$formula を link --overwrite --force"
250
+ brew link --overwrite --force "$formula"
251
+ hash -r 2>/dev/null || true
252
+ }
253
+
254
+ # Node.js のサポート範囲ポリシー判定。
255
+ # - 未インストール → LTS を install
256
+ # - [MIN, MAX] 範囲内 → 現状維持(ユーザーの環境を尊重)
257
+ # - 範囲未満 → LTS にアップグレード
258
+ # - 範囲超過(current 系等) → LTS にダウングレード
228
259
  ensure_node_version() {
229
260
  local v
230
261
  v=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1)
231
262
  if [[ -z "$v" ]]; then
232
- ensure_pkg node node nodejs
233
- return
234
- fi
235
- if (( v < NODE_MIN_MAJOR )); then
236
- color_warn "node $v は古いです (>=${NODE_MIN_MAJOR} 必須)。brew で更新を試みます"
263
+ color_step "node が未インストール → $NODE_DEFAULT_BREW_FORMULA (Active LTS) を install"
237
264
  if [[ "$(uname)" == "Darwin" ]]; then
238
- brew upgrade node || brew install node
265
+ _install_node_lts_brew
239
266
  else
240
- color_err "Node ${NODE_MIN_MAJOR}+ を手動で install してください (nvm 推奨)"
241
- exit 1
267
+ ensure_pkg node node nodejs
242
268
  fi
269
+ return
270
+ fi
271
+
272
+ if (( v >= NODE_MIN_MAJOR && v <= NODE_MAX_MAJOR )); then
273
+ color_ok "node v$v は動作保証範囲 (${NODE_MIN_MAJOR} ≤ v ≤ ${NODE_MAX_MAJOR}) 内。現状維持"
274
+ return
275
+ fi
276
+
277
+ if (( v < NODE_MIN_MAJOR )); then
278
+ color_warn "node v$v は古い (>=${NODE_MIN_MAJOR} 必須) → $NODE_DEFAULT_BREW_FORMULA にアップグレード"
243
279
  else
244
- color_ok "node $v"
280
+ color_warn "node v$v は新しすぎ (current 系) → 安定動作のため $NODE_DEFAULT_BREW_FORMULA (LTS) にダウングレード"
281
+ fi
282
+ if [[ "$(uname)" == "Darwin" ]]; then
283
+ _install_node_lts_brew
284
+ else
285
+ color_err "Node $NODE_MIN_MAJOR〜$NODE_MAX_MAJOR の LTS を手動で install してください (nvm 推奨)"
286
+ exit 1
245
287
  fi
246
288
  }
247
289
 
@@ -300,15 +342,26 @@ do_install_service() {
300
342
 
301
343
  macos_perm_guidance() {
302
344
  [[ "$(uname)" != "Darwin" ]] && return 0
303
- cat <<'EOF'
345
+ # 現在 PATH 上の node 実体パスを案内に埋め込む(フルディスクアクセス登録時に役立つ)
346
+ local node_path
347
+ node_path="$(command -v node 2>/dev/null || echo '/path/to/node')"
348
+ cat <<EOF
304
349
 
305
350
  📣 macOS の権限ダイアログについて
306
351
  hub-agent は tmux/pty を中継するため、初回起動時に macOS から
307
352
  「node がローカルネットワーク上の機器を検出することを求めています」
308
353
  「node がアクセシビリティを制御することを求めています」
354
+ 「node がほかのアプリからのデータへのアクセス権を求めています」
309
355
  などのダイアログが出る場合があります。すべて「許可」を選んでください。
310
356
  許可は「システム設定 > プライバシーとセキュリティ」から後で変更できます。
311
357
 
358
+ ▶ ダイアログが頻繁に出る場合(特に Claude Code 経由で様々な cwd を使う場合):
359
+ 「フルディスクアクセス」に node 本体を追加すると静かになります。
360
+ システム設定 > プライバシーとセキュリティ > フルディスクアクセス
361
+ → + ボタンで以下のパスを追加:
362
+ ${node_path}
363
+ (node バイナリパスが変わったら再登録が必要です)
364
+
312
365
  EOF
313
366
  }
314
367
 
package/src/main.mjs CHANGED
@@ -32,6 +32,8 @@ import {
32
32
  killManySessions,
33
33
  killSession as killTmuxSession,
34
34
  listSessions as listTmuxSessions,
35
+ listWorktreeStubs,
36
+ removeWorktree as removeWorktreeDir,
35
37
  } from "./tmux.mjs"
36
38
  import { getSessionUsages, getUsage } from "./usage.mjs"
37
39
 
@@ -506,16 +508,28 @@ async function dispatch(msg, ctx) {
506
508
  ...s,
507
509
  last_event: lastEventByName.get(s.name) || null,
508
510
  }))
511
+ // cockpit (PR 1719) で未起動 worktree をサイドバーに可視化するために
512
+ // filesystem 上は存在するが tmux session が無い worktree dir のリストを
513
+ // 同梱する。古い cockpit は worktree_stubs を無視するので互換 OK。
514
+ const liveNames = new Set(enriched.map((s) => s.name))
515
+ let worktreeStubs = []
516
+ try {
517
+ worktreeStubs = await listWorktreeStubs(liveNames)
518
+ } catch (err) {
519
+ ctx.logger?.warn?.({ err: err?.message }, "listWorktreeStubs failed")
520
+ }
509
521
  ctx.client.send({
510
522
  type: "tmux.sessions",
511
523
  request_id: msg.request_id,
512
524
  sessions: enriched,
525
+ worktree_stubs: worktreeStubs,
513
526
  })
514
527
  } catch (err) {
515
528
  ctx.client.send({
516
529
  type: "tmux.sessions",
517
530
  request_id: msg.request_id,
518
531
  sessions: [],
532
+ worktree_stubs: [],
519
533
  error: err.message,
520
534
  })
521
535
  }
@@ -612,6 +626,39 @@ async function dispatch(msg, ctx) {
612
626
  }
613
627
  return
614
628
  }
629
+ case "worktree.remove": {
630
+ // body: { request_id, name }
631
+ // cockpit (PR 1719) のサイドバー削除ボタンから呼ばれる。
632
+ // git worktree remove --force を実行し、走行中 session があれば事前に kill する。
633
+ const name = (msg.name || "").trim()
634
+ if (!name) {
635
+ ctx.client.send({
636
+ type: "worktree.remove.result",
637
+ request_id: msg.request_id,
638
+ ok: false,
639
+ error: "name required",
640
+ })
641
+ return
642
+ }
643
+ try {
644
+ const { name: removedName, wt_path: wtPath } = await removeWorktreeDir(name)
645
+ ctx.client.send({
646
+ type: "worktree.remove.result",
647
+ request_id: msg.request_id,
648
+ ok: true,
649
+ name: removedName,
650
+ wt_path: wtPath,
651
+ })
652
+ } catch (err) {
653
+ ctx.client.send({
654
+ type: "worktree.remove.result",
655
+ request_id: msg.request_id,
656
+ ok: false,
657
+ error: err.message,
658
+ })
659
+ }
660
+ return
661
+ }
615
662
  case "tmux.kill_session": {
616
663
  const names = Array.isArray(msg.session_names)
617
664
  ? msg.session_names
package/src/tmux.mjs CHANGED
@@ -142,14 +142,19 @@ export async function createWorktreeDir(parentDir, branch) {
142
142
 
143
143
  /**
144
144
  * `~/hub/projects/<project>/.claude/worktrees/<wt>` を走査し、
145
- * worktree_session_nameparent_session_name Map を返す。
145
+ * worktree session 名 詳細情報の Map を返す。
146
146
  *
147
147
  * - 各 worktree dir は `.git` ファイル/ディレクトリの存在で git worktree と判定
148
148
  * - session 名はディレクトリ名を sanitize したもの (tmux session 名命名規則)
149
149
  *
150
+ * 戻り値は `{ parent_session_name, cwd, branch }` の dict。cockpit (cockpit
151
+ * worktree-discovery PR 1719) が tmux.list_sessions 応答に worktree_stubs として
152
+ * 同梱するために branch / cwd まで返す。`branch` は `git -C <wtPath> branch
153
+ * --show-current` を best-effort で取る。
154
+ *
150
155
  * 移植元: D00000_cockpit/webapp/lib/workspaces.ts (findWorktrees)
151
156
  */
152
- async function buildWorktreeParentMap() {
157
+ async function buildWorktreeIndex() {
153
158
  const out = new Map()
154
159
  const projectsBase = hubProjectsBase()
155
160
  let projects
@@ -169,17 +174,99 @@ async function buildWorktreeParentMap() {
169
174
  }
170
175
  for (const wt of wts) {
171
176
  if (!wt.isDirectory() || wt.name.startsWith(".")) continue
177
+ const wtPath = path.join(wtBase, wt.name)
172
178
  try {
173
179
  // .git が存在する = 正規 git worktree
174
- await fs.stat(path.join(wtBase, wt.name, ".git"))
180
+ await fs.stat(path.join(wtPath, ".git"))
175
181
  } catch {
176
182
  continue
177
183
  }
178
- out.set(sanitizeTmuxName(wt.name), sanitizeTmuxName(p.name))
184
+ let branch = null
185
+ try {
186
+ const r = await execFileP("git", ["-C", wtPath, "branch", "--show-current"])
187
+ branch = (r.stdout || "").trim() || null
188
+ } catch {
189
+ // best-effort: detached / 取得失敗時は null のままにする
190
+ }
191
+ const sessionName = sanitizeTmuxName(wt.name)
192
+ out.set(sessionName, {
193
+ name: sessionName,
194
+ parent_session_name: sanitizeTmuxName(p.name),
195
+ cwd: wtPath,
196
+ branch,
197
+ })
179
198
  }
180
199
  }
181
200
  return out
182
201
  }
202
+
203
+ /**
204
+ * 後方互換: 旧 `buildWorktreeParentMap` を `buildWorktreeIndex` で薄ラップ。
205
+ * Map<wtName, parentName> 形式を期待していた既存呼び出し (listSessions 内) 向け。
206
+ */
207
+ async function buildWorktreeParentMap() {
208
+ const idx = await buildWorktreeIndex()
209
+ const out = new Map()
210
+ for (const [name, info] of idx) out.set(name, info.parent_session_name)
211
+ return out
212
+ }
213
+
214
+ /**
215
+ * 走行中の tmux session 名 set を受け取り、それに含まれない worktree (= 未起動 stub)
216
+ * のリストを返す。cockpit `tmux.sessions` 応答の `worktree_stubs` フィールドに乗せる
217
+ * ためのヘルパー。
218
+ */
219
+ export async function listWorktreeStubs(liveSessionNames) {
220
+ const idx = await buildWorktreeIndex()
221
+ const stubs = []
222
+ for (const [name, info] of idx) {
223
+ if (liveSessionNames.has(name)) continue
224
+ stubs.push(info)
225
+ }
226
+ return stubs
227
+ }
228
+
229
+ /**
230
+ * `git worktree remove --force <wtPath>` を実行する。事前に live tmux session が
231
+ * あれば kill する。cockpit `worktree.remove` ハンドラから呼ばれる。
232
+ *
233
+ * - name から path を解決するには buildWorktreeIndex で fs を走査する (path 検証も兼ねる)
234
+ * - HUB_PROJECTS_BASE 配下に居ない path は拒否 (path traversal 防止)
235
+ * - 未コミットの変更等で git remove が失敗した場合はそのエラーを上位に投げる
236
+ */
237
+ export async function removeWorktree(name, opts = {}) {
238
+ const sanitized = sanitizeTmuxName(String(name || "").trim())
239
+ if (!sanitized) throw new Error("worktree name required")
240
+ const idx = await buildWorktreeIndex()
241
+ const info = idx.get(sanitized)
242
+ if (!info) throw new Error("worktree not found")
243
+ const projectsBase = hubProjectsBase()
244
+ const resolved = path.resolve(info.cwd)
245
+ if (!resolved.startsWith(projectsBase + path.sep)) {
246
+ throw new Error("worktree path outside projects base")
247
+ }
248
+ // 走行中の session があれば kill
249
+ try {
250
+ await execFileP(tmuxBin(opts), ["kill-session", "-t", sanitized])
251
+ } catch (err) {
252
+ const msg = err?.message || String(err)
253
+ // session 不在は正常 (停止済みの worktree を消すパスもある)
254
+ if (
255
+ !msg.includes("can't find session") &&
256
+ !msg.includes("no current session") &&
257
+ !msg.includes("no server running")
258
+ ) {
259
+ throw err
260
+ }
261
+ }
262
+ // 親 repo を解決し `git -C <parent> worktree remove --force <wtPath>` を実行
263
+ const wtSegment = path.join(".claude", "worktrees", path.basename(resolved))
264
+ const parentRepo = resolved.endsWith(wtSegment)
265
+ ? resolved.slice(0, -wtSegment.length - 1)
266
+ : path.resolve(resolved, "..", "..", "..")
267
+ await execFileP("git", ["-C", parentRepo, "worktree", "remove", "--force", resolved])
268
+ return { name: sanitized, wt_path: resolved }
269
+ }
183
270
  /**
184
271
  * Claude CLI コマンドを組み立てる。
185
272
  *