@cocorograph/hub-agent 0.5.7 → 0.5.8

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.7",
3
+ "version": "0.5.8",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/main.mjs CHANGED
@@ -319,6 +319,7 @@ async function dispatch(msg, ctx) {
319
319
  try {
320
320
  await createTmuxSession(name, cwd, {
321
321
  claudeCmd: typeof msg.claude_cmd === "string" ? msg.claude_cmd : undefined,
322
+ logger: ctx.logger,
322
323
  })
323
324
  ctx.client.send({
324
325
  type: "tmux.create_session.result",
@@ -363,6 +364,7 @@ async function dispatch(msg, ctx) {
363
364
  // worktree dir で tmux session を作成 (claude_cmd は createSession の default)
364
365
  await createTmuxSession(wtName, wtPath, {
365
366
  claudeCmd: typeof msg.claude_cmd === "string" ? msg.claude_cmd : undefined,
367
+ logger: ctx.logger,
366
368
  })
367
369
  ctx.client.send({
368
370
  type: "worktree.create.result",
package/src/tmux.mjs CHANGED
@@ -33,6 +33,20 @@ function sanitizeTmuxName(s) {
33
33
  return s.replace(/[.:]/g, "-")
34
34
  }
35
35
 
36
+ /**
37
+ * 先頭の `~` / `~/` を `os.homedir()` に展開する。
38
+ *
39
+ * tmux は `-c` に渡された cwd をシェル展開しないため、フロントから
40
+ * 受け取った `~/hub/projects/...` を絶対パスに変換する必要がある。
41
+ * 既に絶対パス / 相対パスなら noop。
42
+ */
43
+ export function expandTilde(p) {
44
+ if (typeof p !== "string" || p.length === 0) return p
45
+ if (p === "~") return os.homedir()
46
+ if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2))
47
+ return p
48
+ }
49
+
36
50
  /**
37
51
  * git branch 名から worktree dir 名 (= tmux session 名) を導出する。
38
52
  * 例: "feature/multi-area" -> "feature-multi-area"
@@ -70,7 +84,10 @@ async function branchExists(repoDir, branch) {
70
84
  * 移植元: D00000_cockpit/webapp/lib/worktrees.ts (createWorktree)
71
85
  */
72
86
  export async function createWorktreeDir(parentDir, branch) {
73
- const repoDir = path.resolve(HUB_PROJECTS_BASE, parentDir)
87
+ // parentDir `~/...` 形式が来ても受け付けられるよう先に展開する。
88
+ // 展開後が絶対パスなら path.resolve は HUB_PROJECTS_BASE を無視して
89
+ // そのまま使う (Node の path.resolve 仕様)。
90
+ const repoDir = path.resolve(HUB_PROJECTS_BASE, expandTilde(parentDir))
74
91
  if (repoDir !== HUB_PROJECTS_BASE && !repoDir.startsWith(HUB_PROJECTS_BASE + path.sep)) {
75
92
  throw new Error("dir outside projects base")
76
93
  }
@@ -302,6 +319,20 @@ export async function killManySessions(names, opts = {}) {
302
319
  * - opts.claudeCmd で send-keys 内容を上書き可 (空文字なら claude 自動起動しない)
303
320
  */
304
321
  export async function createSession(name, cwd, opts = {}) {
322
+ // tmux は `-c` をシェル展開しないため、ここで `~` を絶対パスに展開する。
323
+ // 展開後の cwd は同名引数で以降の検証 / tmux に渡す。
324
+ const resolvedCwd = expandTilde(cwd)
325
+ // cwd 存在チェック (旧 Cockpit 単体実装と同等の防御)
326
+ // 無いまま tmux に渡すと pane の current_path が HOME など別ディレクトリに
327
+ // fallback して、SessionStart hook が project を解決できなくなる。
328
+ try {
329
+ const st = await fs.stat(resolvedCwd)
330
+ if (!st.isDirectory()) throw new Error("cwd not a directory")
331
+ } catch (err) {
332
+ if (err?.code === "ENOENT") throw new Error(`cwd not found: ${resolvedCwd}`)
333
+ if (err?.message === "cwd not a directory") throw err
334
+ throw new Error(`cwd stat failed: ${err?.message || String(err)}`)
335
+ }
305
336
  // 既存チェック
306
337
  try {
307
338
  await execFileP(tmuxBin(opts), ["has-session", "-t", name])
@@ -311,7 +342,30 @@ export async function createSession(name, cwd, opts = {}) {
311
342
  if (msg === "duplicate session") throw err
312
343
  // has-session が非 0 = セッション無し
313
344
  }
314
- await execFileP(tmuxBin(opts), ["new-session", "-d", "-s", name, "-c", cwd])
345
+ await execFileP(tmuxBin(opts), ["new-session", "-d", "-s", name, "-c", resolvedCwd])
346
+ // 起動直後 pane の current_path を確認し、想定 cwd と異なれば WARN ログを残す
347
+ // (Phase 3: 任意検証。tmux 側のクオート / 権限問題で fallback した場合を検知する)
348
+ if (opts.logger) {
349
+ try {
350
+ const { stdout } = await execFileP(tmuxBin(opts), [
351
+ "display-message",
352
+ "-p",
353
+ "-t",
354
+ `${name}:`,
355
+ "-F",
356
+ "#{pane_current_path}",
357
+ ])
358
+ const actual = (stdout || "").trim()
359
+ if (actual && actual !== resolvedCwd) {
360
+ opts.logger.warn(
361
+ { session: name, requested: resolvedCwd, actual },
362
+ "tmux pane cwd differs from requested cwd",
363
+ )
364
+ }
365
+ } catch {
366
+ // display-message 失敗は致命的でないので飲み込む
367
+ }
368
+ }
315
369
  const claudeCmd = opts.claudeCmd ?? DEFAULT_CLAUDE_CMD
316
370
  if (claudeCmd) {
317
371
  await execFileP(tmuxBin(opts), ["send-keys", "-t", name, claudeCmd, "Enter"])