Files
paseo/packages/cli/src/commands/worktree/ls.ts
T
paseo-bot[bot]andJason 0147edaaac Restore repository selection for CLI worktree commands (#6467)
* fix(cli): pass cwd to worktree ls and archive

worktree ls/archive called getPaseoWorktreeList({}) which fails because
the daemon requires cwd or repoRoot. Default to process.cwd(), accept
optional --cwd, and cover both with unit and CLI help tests.

Closes #2010

* test(cli): reproduce worktree listing without repository scope

* fix(cli): resolve local worktree paths from the calling directory

Local instance paths belong to the CLI working directory. Explicit endpoint paths remain daemon-relative, matching the project command convention. Cover both through the real daemon.

* refactor(cli): make repository directory selection explicit

---------

Co-authored-by: Jason <mildreins@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-10 15:53:10 +02:00

105 lines
3.0 KiB
TypeScript

import type { Command } from "commander";
import { basename } from "node:path";
import { connectToDaemon } from "../../utils/client.js";
import { resolveRepositoryDirectory } from "./repository-directory.js";
import type { CommandOptions, ListResult, OutputSchema, CommandError } from "../../output/index.js";
/** Worktree list item for display */
export interface WorktreeListItem {
name: string;
branch: string;
cwd: string;
agent: string;
}
/** Shorten home directory in path */
function shortenPath(path: string): string {
const home = process.env.HOME;
if (home && path.startsWith(home)) {
return "~" + path.slice(home.length);
}
return path;
}
/** Extract worktree name from path */
function extractWorktreeName(path: string): string {
return basename(path);
}
/** Schema for worktree ls output */
export const worktreeLsSchema: OutputSchema<WorktreeListItem> = {
idField: "name",
columns: [
{ header: "NAME", field: "name", width: 20 },
{ header: "BRANCH", field: "branch", width: 25 },
{ header: "CWD", field: "cwd", width: 45 },
{ header: "AGENT", field: "agent", width: 10 },
],
};
export type WorktreeLsResult = ListResult<WorktreeListItem>;
export interface WorktreeLsOptions extends CommandOptions {
host?: string;
cwd?: string;
}
export async function runLsCommand(
options: WorktreeLsOptions,
_command: Command,
): Promise<WorktreeLsResult> {
const cwd = resolveRepositoryDirectory(options);
const client = await connectToDaemon({ target: options.daemonTarget });
try {
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
const agents = agentsPayload.entries.map((entry) => entry.agent);
// Get worktree list from daemon
const response = await client.getPaseoWorktreeList({ cwd });
await client.close();
if (response.error) {
const error: CommandError = {
code: "WORKTREE_LIST_FAILED",
message: `Failed to list worktrees: ${response.error.message}`,
};
throw error;
}
// Build a map of worktree paths to agent IDs
const worktreeAgentMap = new Map<string, string>();
for (const agent of agents) {
worktreeAgentMap.set(agent.cwd, agent.id.slice(0, 7));
}
const items: WorktreeListItem[] = response.worktrees.map((wt) => ({
name: extractWorktreeName(wt.worktreePath),
branch: wt.branchName ?? "-",
cwd: shortenPath(wt.worktreePath),
agent: worktreeAgentMap.get(wt.worktreePath) ?? "-",
}));
return {
type: "list",
data: items,
schema: worktreeLsSchema,
};
} catch (err) {
await client.close().catch(() => {});
// Re-throw CommandError as-is
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "WORKTREE_LIST_FAILED",
message: `Failed to list worktrees: ${message}`,
};
throw error;
}
}