From 6709f9a1921e4e85c3c9a1d2cf096855001fcfd6 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 12 Aug 2026 14:15:37 +0700 Subject: [PATCH] feat(cli): add 'ccam lanes shell' to attach a real terminal to a lane's tmux session --- bin/ccam.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/bin/ccam.js b/bin/ccam.js index 3f38fce..1278ab7 100755 --- a/bin/ccam.js +++ b/bin/ccam.js @@ -2300,6 +2300,31 @@ async function cmdLanesPipeline(args) { } } +/** + * Attach a real terminal to the exact tmux session the dashboard uses for + * this lane's run. Creates it if it doesn't exist yet (`tmux new-session -A` + * is create-or-attach, the same idempotent semantics as clicking Start on + * the dashboard). The user types `claude` themselves inside — this command's + * only job is landing them in the right named session. + */ +async function cmdLanesShell(args) { + const resolved = await resolveLaneArg(args); + if (!resolved) return; + const { lane } = await get(`/api/lanes/${resolved.laneId}`); + const sessionName = `ccam-lane-${lane.id}`; + const child = spawn("tmux", ["new-session", "-A", "-s", sessionName, "-c", lane.cwd], { + stdio: "inherit", + }); + await new Promise((resolve) => { + child.on("close", resolve); + child.on("error", (err) => { + console.error(c.red(`✖ ${err?.message || err}`)); + process.exitCode = 1; + resolve(); + }); + }); +} + async function cmdFeatureShow(args) { const slug = args.find((arg) => !arg.startsWith("--")); if (!slug) { @@ -2484,6 +2509,11 @@ const COMMAND_GROUPS = [ "[] []", "Show, or switch, which pipeline template a lane renders against", ], + [ + "lanes shell", + "[]", + "Attach a real terminal to the exact tmux session the dashboard uses for a lane's run", + ], [ "lanes reset|remove|purge", " [--force] [--keep-db] --yes", @@ -3362,6 +3392,9 @@ async function runCommand(argv) { if (rest[0] === "pipeline") { return cmdLanesPipeline(rest.slice(1)); } + if (rest[0] === "shell") { + return cmdLanesShell(rest.slice(1)); + } if (rest[0] === "gc") { return cmdLanesGc(rest.slice(1)); }