@cocorograph/hub-agent 0.5.21 → 0.5.23
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 +12 -1
- package/src/main.mjs +47 -0
- package/src/tmux.mjs +91 -4
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -300,15 +300,26 @@ do_install_service() {
|
|
|
300
300
|
|
|
301
301
|
macos_perm_guidance() {
|
|
302
302
|
[[ "$(uname)" != "Darwin" ]] && return 0
|
|
303
|
-
|
|
303
|
+
# 現在 PATH 上の node 実体パスを案内に埋め込む(フルディスクアクセス登録時に役立つ)
|
|
304
|
+
local node_path
|
|
305
|
+
node_path="$(command -v node 2>/dev/null || echo '/path/to/node')"
|
|
306
|
+
cat <<EOF
|
|
304
307
|
|
|
305
308
|
📣 macOS の権限ダイアログについて
|
|
306
309
|
hub-agent は tmux/pty を中継するため、初回起動時に macOS から
|
|
307
310
|
「node がローカルネットワーク上の機器を検出することを求めています」
|
|
308
311
|
「node がアクセシビリティを制御することを求めています」
|
|
312
|
+
「node がほかのアプリからのデータへのアクセス権を求めています」
|
|
309
313
|
などのダイアログが出る場合があります。すべて「許可」を選んでください。
|
|
310
314
|
許可は「システム設定 > プライバシーとセキュリティ」から後で変更できます。
|
|
311
315
|
|
|
316
|
+
▶ ダイアログが頻繁に出る場合(特に Claude Code 経由で様々な cwd を使う場合):
|
|
317
|
+
「フルディスクアクセス」に node 本体を追加すると静かになります。
|
|
318
|
+
システム設定 > プライバシーとセキュリティ > フルディスクアクセス
|
|
319
|
+
→ + ボタンで以下のパスを追加:
|
|
320
|
+
${node_path}
|
|
321
|
+
(node バイナリパスが変わったら再登録が必要です)
|
|
322
|
+
|
|
312
323
|
EOF
|
|
313
324
|
}
|
|
314
325
|
|
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
|
*
|