diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index 89746fc31..9be795632 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -78,8 +78,10 @@ export function registerDashboard(program: Command): void { process.exit(1); }); + let browserTimer: ReturnType | undefined; + if (opts.open !== false) { - setTimeout(() => { + browserTimer = setTimeout(() => { const browser = spawn("open", [`http://localhost:${port}`], { stdio: "ignore", }); @@ -90,6 +92,7 @@ export function registerDashboard(program: Command): void { } child.on("exit", (code) => { + if (browserTimer) clearTimeout(browserTimer); process.exit(code ?? 0); }); }); diff --git a/packages/cli/src/commands/review-check.ts b/packages/cli/src/commands/review-check.ts index 56ebcf7a2..2dc856a3e 100644 --- a/packages/cli/src/commands/review-check.ts +++ b/packages/cli/src/commands/review-check.ts @@ -18,6 +18,9 @@ async function checkPRReviews( prNumber: string, ): Promise<{ pendingComments: number; reviewDecision: string | null }> { const [owner, name] = repo.split("/"); + if (!owner || !name) { + return { pendingComments: 0, reviewDecision: null }; + } // Use GraphQL with variable passing (-F) to avoid injection via repo names const query = diff --git a/packages/cli/src/commands/send.ts b/packages/cli/src/commands/send.ts index 60f6105bf..5cdc1b8d5 100644 --- a/packages/cli/src/commands/send.ts +++ b/packages/cli/src/commands/send.ts @@ -98,7 +98,13 @@ export function registerSend(program: Command): void { // Send the message if (opts.file) { - const content = readFileSync(opts.file, "utf-8"); + let content: string; + try { + content = readFileSync(opts.file, "utf-8"); + } catch (err) { + console.error(chalk.red(`Cannot read file: ${opts.file} (${err})`)); + process.exit(1); + } const tmpFile = join(tmpdir(), `ao-send-${Date.now()}.txt`); writeFileSync(tmpFile, content); try { diff --git a/packages/cli/src/commands/spawn.ts b/packages/cli/src/commands/spawn.ts index dc21e76a0..e432de242 100644 --- a/packages/cli/src/commands/spawn.ts +++ b/packages/cli/src/commands/spawn.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, symlinkSync, unlinkSync } from "node:fs"; +import { existsSync, lstatSync, mkdirSync, symlinkSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import chalk from "chalk"; import ora from "ora"; @@ -10,6 +10,14 @@ import { banner } from "../lib/format.js"; import { getAgent } from "../lib/plugins.js"; import { escapeRegex } from "../lib/session-utils.js"; +/** + * Find the next available session number for a prefix. + * + * There is an inherent TOCTOU gap between reading the session list and creating + * the tmux session. If two spawns race, tmux new-session will fail with a + * duplicate name error, which spawnSession already handles by throwing to the + * caller (batch-spawn catches per-item failures and continues). + */ async function getNextSessionNumber(prefix: string): Promise { const sessions = await getTmuxSessions(); let max = 0; @@ -99,7 +107,8 @@ async function spawnSession( const dest = join(worktreePath, file); if (existsSync(src) && !existsSync(dest)) { try { - symlinkSync(src, dest); + const type = lstatSync(src).isDirectory() ? "dir" : "file"; + symlinkSync(src, dest, type); } catch { // ignore }