@cocorograph/hub-agent 0.6.31 → 0.6.33
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/src/main.mjs +35 -0
- package/src/tmux.mjs +90 -0
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
killManySessions,
|
|
37
37
|
killSession as killTmuxSession,
|
|
38
38
|
listSessions as listTmuxSessions,
|
|
39
|
+
listWorktreeNameHistory,
|
|
39
40
|
listWorktreeStubs,
|
|
40
41
|
removeWorktree as removeWorktreeDir,
|
|
41
42
|
} from "./tmux.mjs"
|
|
@@ -1113,6 +1114,40 @@ async function dispatch(msg, ctx) {
|
|
|
1113
1114
|
}
|
|
1114
1115
|
return
|
|
1115
1116
|
}
|
|
1117
|
+
case "worktree.list_history": {
|
|
1118
|
+
// body: { request_id, dir }
|
|
1119
|
+
// cockpit (Feature A) の worktree 作成ダイアログから呼ばれる。指定 workspace で
|
|
1120
|
+
// 過去に使った worktree 名 (~/.claude/projects のログ dir 由来) を返す。
|
|
1121
|
+
const dir = (msg.dir || "").trim()
|
|
1122
|
+
if (!dir) {
|
|
1123
|
+
ctx.client.send({
|
|
1124
|
+
type: "worktree.list_history.result",
|
|
1125
|
+
request_id: msg.request_id,
|
|
1126
|
+
ok: false,
|
|
1127
|
+
error: "dir required",
|
|
1128
|
+
names: [],
|
|
1129
|
+
})
|
|
1130
|
+
return
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
const names = await listWorktreeNameHistory(dir)
|
|
1134
|
+
ctx.client.send({
|
|
1135
|
+
type: "worktree.list_history.result",
|
|
1136
|
+
request_id: msg.request_id,
|
|
1137
|
+
ok: true,
|
|
1138
|
+
names,
|
|
1139
|
+
})
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
ctx.client.send({
|
|
1142
|
+
type: "worktree.list_history.result",
|
|
1143
|
+
request_id: msg.request_id,
|
|
1144
|
+
ok: false,
|
|
1145
|
+
error: err.message,
|
|
1146
|
+
names: [],
|
|
1147
|
+
})
|
|
1148
|
+
}
|
|
1149
|
+
return
|
|
1150
|
+
}
|
|
1116
1151
|
case "worktree.remove": {
|
|
1117
1152
|
// body: { request_id, name }
|
|
1118
1153
|
// cockpit (PR 1719) のサイドバー削除ボタンから呼ばれる。
|
package/src/tmux.mjs
CHANGED
|
@@ -267,6 +267,96 @@ export async function removeWorktree(name, opts = {}) {
|
|
|
267
267
|
await execFileP("git", ["-C", parentRepo, "worktree", "remove", "--force", resolved])
|
|
268
268
|
return { name: sanitized, wt_path: resolved }
|
|
269
269
|
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Claude Code がセッションログを格納する `~/.claude/projects/<encoded>` の
|
|
273
|
+
* ディレクトリ名エンコードを再現する。Claude Code は cwd の絶対パスに含まれる
|
|
274
|
+
* `/` `.` `_` をすべて `-` に置換したものを dir 名に使う。
|
|
275
|
+
*
|
|
276
|
+
* 例: `/Users/kaz/hub/projects/D00000_hub.cocorograph.com/.claude/worktrees`
|
|
277
|
+
* → `-Users-kaz-hub-projects-D00000-hub-cocorograph-com--claude-worktrees`
|
|
278
|
+
*
|
|
279
|
+
* 実データ (cockpit / cocomiru / orbit 等の worktree ログ dir) で prefix 一致を
|
|
280
|
+
* 検証済み (2026-06-01)。worktree 名はブランチ sanitize 済で `_` `.` を含まないため、
|
|
281
|
+
* encode 後も名前が無損失で復元できる。
|
|
282
|
+
*/
|
|
283
|
+
function encodeClaudeProjectPath(p) {
|
|
284
|
+
return p.replace(/[/._]/g, "-")
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 指定 workspace で過去に使った worktree 名の履歴を返す。
|
|
289
|
+
*
|
|
290
|
+
* cockpit の worktree 作成ダイアログで「過去に使った名前」をサジェストし、物理削除
|
|
291
|
+
* した worktree を同じ名前で簡単に復元できるようにするための情報源 (cockpit Feature A)。
|
|
292
|
+
*
|
|
293
|
+
* データ源は `buildWorktreeIndex` (現存 worktree の fs 走査) ではなく
|
|
294
|
+
* `~/.claude/projects/` の **セッションログ dir**。worktree dir を git worktree remove
|
|
295
|
+
* しても Claude Code のログ dir は残るため、「かつて使った名前」= 復元候補が得られる。
|
|
296
|
+
*
|
|
297
|
+
* - worktree の cwd は `<projectsBase>/<workspaceDir>/.claude/worktrees/<name>` なので
|
|
298
|
+
* encode 後は `<encodedWtBase>-<name>` という prefix を持つ。これで当該 workspace の
|
|
299
|
+
* 履歴だけを抽出する (他 repo の同名 worktree と混ざらない)。
|
|
300
|
+
* - いま現存する worktree (= buildWorktreeIndex に居る) は復元する必要がないため除外。
|
|
301
|
+
* - last_used (ログ dir の mtime) 降順でソートして返す。
|
|
302
|
+
*
|
|
303
|
+
* @param {string} workspaceDir 例: "D00000_hub.cocorograph.com" / "~/hub/projects/..."
|
|
304
|
+
* @returns {Promise<Array<{ name: string, last_used: number }>>}
|
|
305
|
+
*/
|
|
306
|
+
export async function listWorktreeNameHistory(workspaceDir) {
|
|
307
|
+
const out = []
|
|
308
|
+
if (!workspaceDir || typeof workspaceDir !== "string") return out
|
|
309
|
+
const projectsBase = hubProjectsBase()
|
|
310
|
+
const repoDir = path.resolve(projectsBase, expandTilde(workspaceDir))
|
|
311
|
+
// path traversal 防止: projectsBase 配下のみ許可
|
|
312
|
+
if (repoDir !== projectsBase && !repoDir.startsWith(projectsBase + path.sep)) {
|
|
313
|
+
return out
|
|
314
|
+
}
|
|
315
|
+
const wtBase = path.join(repoDir, ".claude", "worktrees")
|
|
316
|
+
// dir 名は encode(`${wtBase}/${name}`) = `${encode(wtBase)}-${name}` (区切り `/` も `-`)
|
|
317
|
+
const prefix = encodeClaudeProjectPath(wtBase) + "-"
|
|
318
|
+
|
|
319
|
+
const claudeProjects = path.join(os.homedir(), ".claude", "projects")
|
|
320
|
+
let entries
|
|
321
|
+
try {
|
|
322
|
+
entries = await fs.readdir(claudeProjects, { withFileTypes: true })
|
|
323
|
+
} catch {
|
|
324
|
+
// ~/.claude/projects が無い環境では履歴なし
|
|
325
|
+
return out
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 現存 worktree (この workspace 直下のもの) は復元候補から除外する。
|
|
329
|
+
const existing = new Set()
|
|
330
|
+
try {
|
|
331
|
+
const idx = await buildWorktreeIndex()
|
|
332
|
+
for (const info of idx.values()) {
|
|
333
|
+
if (path.dirname(info.cwd) === wtBase) existing.add(path.basename(info.cwd))
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
// index 構築失敗時は除外なしで続行 (best-effort)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 同名が複数ログ dir に跨る場合は最新 mtime を採用する。
|
|
340
|
+
const seen = new Map()
|
|
341
|
+
for (const e of entries) {
|
|
342
|
+
if (!e.isDirectory() || !e.name.startsWith(prefix)) continue
|
|
343
|
+
const name = e.name.slice(prefix.length)
|
|
344
|
+
if (!name || existing.has(name)) continue
|
|
345
|
+
let mtime = 0
|
|
346
|
+
try {
|
|
347
|
+
const st = await fs.stat(path.join(claudeProjects, e.name))
|
|
348
|
+
mtime = st.mtimeMs
|
|
349
|
+
} catch {
|
|
350
|
+
// stat 失敗時は 0 のまま (末尾に並ぶ)
|
|
351
|
+
}
|
|
352
|
+
const prev = seen.get(name)
|
|
353
|
+
if (prev == null || mtime > prev) seen.set(name, mtime)
|
|
354
|
+
}
|
|
355
|
+
for (const [name, last_used] of seen) out.push({ name, last_used })
|
|
356
|
+
out.sort((a, b) => b.last_used - a.last_used)
|
|
357
|
+
return out
|
|
358
|
+
}
|
|
359
|
+
|
|
270
360
|
/**
|
|
271
361
|
* Claude CLI コマンドを組み立てる。
|
|
272
362
|
*
|