@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 +1 -1
- package/scripts/install.sh +64 -11
- package/src/main.mjs +47 -0
- package/src/tmux.mjs +91 -4
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -21,7 +21,17 @@
|
|
|
21
21
|
|
|
22
22
|
set -euo pipefail
|
|
23
23
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
+
_install_node_lts_brew
|
|
239
266
|
else
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
180
|
+
await fs.stat(path.join(wtPath, ".git"))
|
|
175
181
|
} catch {
|
|
176
182
|
continue
|
|
177
183
|
}
|
|
178
|
-
|
|
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
|
*
|