185 lines
6.8 KiB
TypeScript
185 lines
6.8 KiB
TypeScript
import chalk from "chalk";
|
|
import type { Command } from "commander";
|
|
import { loadConfig, SessionNotRestorableError, WorkspaceMissingError } from "@composio/ao-core";
|
|
import { git, getTmuxActivity } from "../lib/shell.js";
|
|
import { formatAge } from "../lib/format.js";
|
|
import { getSessionManager } from "../lib/create-session-manager.js";
|
|
|
|
export function registerSession(program: Command): void {
|
|
const session = program.command("session").description("Session management (ls, kill, cleanup)");
|
|
|
|
session
|
|
.command("ls")
|
|
.description("List all sessions")
|
|
.option("-p, --project <id>", "Filter by project ID")
|
|
.action(async (opts: { project?: string }) => {
|
|
const config = loadConfig();
|
|
if (opts.project && !config.projects[opts.project]) {
|
|
console.error(chalk.red(`Unknown project: ${opts.project}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
const sm = await getSessionManager(config);
|
|
const sessions = await sm.list(opts.project);
|
|
|
|
// Group sessions by project
|
|
const byProject = new Map<string, typeof sessions>();
|
|
for (const s of sessions) {
|
|
const list = byProject.get(s.projectId) ?? [];
|
|
list.push(s);
|
|
byProject.set(s.projectId, list);
|
|
}
|
|
|
|
// Iterate over all configured projects (not just ones with sessions)
|
|
const projectIds = opts.project ? [opts.project] : Object.keys(config.projects);
|
|
|
|
for (const projectId of projectIds) {
|
|
const project = config.projects[projectId];
|
|
if (!project) continue;
|
|
console.log(chalk.bold(`\n${project.name || projectId}:`));
|
|
|
|
const projectSessions = (byProject.get(projectId) ?? []).sort((a, b) =>
|
|
a.id.localeCompare(b.id),
|
|
);
|
|
|
|
if (projectSessions.length === 0) {
|
|
console.log(chalk.dim(" (no active sessions)"));
|
|
continue;
|
|
}
|
|
|
|
for (const s of projectSessions) {
|
|
// Get live branch from worktree if available
|
|
let branchStr = s.branch || "";
|
|
if (s.workspacePath) {
|
|
const liveBranch = await git(["branch", "--show-current"], s.workspacePath);
|
|
if (liveBranch) branchStr = liveBranch;
|
|
}
|
|
|
|
// Get tmux activity age
|
|
const tmuxTarget = s.runtimeHandle?.id ?? s.id;
|
|
const activityTs = await getTmuxActivity(tmuxTarget);
|
|
const age = activityTs ? formatAge(activityTs) : "-";
|
|
|
|
const parts = [chalk.green(s.id), chalk.dim(`(${age})`)];
|
|
if (branchStr) parts.push(chalk.cyan(branchStr));
|
|
if (s.status) parts.push(chalk.dim(`[${s.status}]`));
|
|
const prUrl = s.metadata["pr"];
|
|
if (prUrl) parts.push(chalk.blue(prUrl));
|
|
|
|
console.log(` ${parts.join(" ")}`);
|
|
}
|
|
}
|
|
console.log();
|
|
});
|
|
|
|
session
|
|
.command("kill")
|
|
.description("Kill a session and remove its worktree")
|
|
.argument("<session>", "Session name to kill")
|
|
.action(async (sessionName: string) => {
|
|
const config = loadConfig();
|
|
const sm = await getSessionManager(config);
|
|
|
|
try {
|
|
await sm.kill(sessionName);
|
|
console.log(chalk.green(`\nSession ${sessionName} killed.`));
|
|
} catch (err) {
|
|
console.error(chalk.red(`Failed to kill session ${sessionName}: ${err}`));
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
session
|
|
.command("cleanup")
|
|
.description("Kill sessions where PR is merged or issue is closed")
|
|
.option("-p, --project <id>", "Filter by project ID")
|
|
.option("--dry-run", "Show what would be cleaned up without doing it")
|
|
.action(async (opts: { project?: string; dryRun?: boolean }) => {
|
|
const config = loadConfig();
|
|
if (opts.project && !config.projects[opts.project]) {
|
|
console.error(chalk.red(`Unknown project: ${opts.project}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.bold("Checking for completed sessions...\n"));
|
|
|
|
const sm = await getSessionManager(config);
|
|
|
|
if (opts.dryRun) {
|
|
// Dry-run delegates to sm.cleanup() with dryRun flag so it uses the
|
|
// same live checks (PR state, runtime alive, tracker) as actual cleanup.
|
|
const result = await sm.cleanup(opts.project, { dryRun: true });
|
|
|
|
if (result.errors.length > 0) {
|
|
for (const { sessionId, error } of result.errors) {
|
|
console.error(chalk.red(` Error checking ${sessionId}: ${error}`));
|
|
}
|
|
}
|
|
|
|
if (result.killed.length === 0 && result.errors.length === 0) {
|
|
console.log(chalk.dim(" No sessions to clean up."));
|
|
} else {
|
|
for (const id of result.killed) {
|
|
console.log(chalk.yellow(` Would kill ${id}`));
|
|
}
|
|
if (result.killed.length > 0) {
|
|
console.log(
|
|
chalk.dim(
|
|
`\nDry run complete. ${result.killed.length} session${result.killed.length !== 1 ? "s" : ""} would be cleaned.`,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
const result = await sm.cleanup(opts.project);
|
|
|
|
if (result.killed.length === 0 && result.errors.length === 0) {
|
|
console.log(chalk.dim(" No sessions to clean up."));
|
|
} else {
|
|
if (result.killed.length > 0) {
|
|
for (const id of result.killed) {
|
|
console.log(chalk.green(` Cleaned: ${id}`));
|
|
}
|
|
}
|
|
if (result.errors.length > 0) {
|
|
for (const { sessionId, error } of result.errors) {
|
|
console.error(chalk.red(` Error cleaning ${sessionId}: ${error}`));
|
|
}
|
|
}
|
|
console.log(chalk.green(`\nCleanup complete. ${result.killed.length} sessions cleaned.`));
|
|
}
|
|
}
|
|
});
|
|
|
|
session
|
|
.command("restore")
|
|
.description("Restore a terminated/crashed session in-place")
|
|
.argument("<session>", "Session name to restore")
|
|
.action(async (sessionName: string) => {
|
|
const config = loadConfig();
|
|
const sm = await getSessionManager(config);
|
|
|
|
try {
|
|
const restored = await sm.restore(sessionName);
|
|
console.log(chalk.green(`\nSession ${sessionName} restored.`));
|
|
if (restored.workspacePath) {
|
|
console.log(chalk.dim(` Worktree: ${restored.workspacePath}`));
|
|
}
|
|
if (restored.branch) {
|
|
console.log(chalk.dim(` Branch: ${restored.branch}`));
|
|
}
|
|
const tmuxTarget = restored.runtimeHandle?.id ?? sessionName;
|
|
console.log(chalk.dim(` Attach: tmux attach -t ${tmuxTarget}`));
|
|
} catch (err) {
|
|
if (err instanceof SessionNotRestorableError) {
|
|
console.error(chalk.red(`Cannot restore: ${err.reason}`));
|
|
} else if (err instanceof WorkspaceMissingError) {
|
|
console.error(chalk.red(`Workspace missing: ${err.message}`));
|
|
} else {
|
|
console.error(chalk.red(`Failed to restore session ${sessionName}: ${err}`));
|
|
}
|
|
process.exit(1);
|
|
}
|
|
});
|
|
}
|