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>
This commit is contained in:
paseo-bot[bot]
2026-10-10 15:53:10 +02:00
committed by GitHub
co-authored by Jason
parent bd78e5efa4
commit 0147edaaac
5 changed files with 95 additions and 5 deletions
@@ -2,6 +2,7 @@ import path from "path";
import type { Command } from "commander";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import { resolveRepositoryDirectory } from "./repository-directory.js";
import type {
CommandOptions,
SingleResult,
@@ -31,6 +32,7 @@ export const archiveSchema: OutputSchema<WorktreeArchiveResult> = {
export interface WorktreeArchiveOptions extends CommandOptions {
host?: string;
cwd?: string;
}
export type WorktreeArchiveCommandResult = SingleResult<WorktreeArchiveResult>;
@@ -49,6 +51,7 @@ export async function runArchiveCommandWithDeps(
deps: { connectToDaemon: typeof connectToDaemon },
): Promise<WorktreeArchiveCommandResult> {
const host = getDaemonHost({ target: options.daemonTarget });
const cwd = resolveRepositoryDirectory(options);
// Validate arguments
if (!nameArg || nameArg.trim().length === 0) {
@@ -76,7 +79,7 @@ export async function runArchiveCommandWithDeps(
try {
// Get the list of worktrees first to resolve the name
const listResponse = await client.getPaseoWorktreeList({});
const listResponse = await client.getPaseoWorktreeList({ cwd });
if (listResponse.error) {
const error: CommandError = {
+6 -2
View File
@@ -9,7 +9,10 @@ export function createWorktreeCommand(): Command {
const worktree = new Command("worktree").description("Manage Paseo-managed git worktrees");
addJsonAndDaemonHostOptions(
worktree.command("ls").description("List Paseo-managed git worktrees"),
worktree
.command("ls")
.description("List Paseo-managed git worktrees")
.option("--cwd <path>", "Repository directory (default: current)"),
).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(
@@ -31,7 +34,8 @@ export function createWorktreeCommand(): Command {
worktree
.command("archive")
.description("Archive a worktree (removes worktree and associated branch)")
.argument("<name>", "Worktree name or branch name"),
.argument("<name>", "Worktree name or branch name")
.option("--cwd <path>", "Repository directory (default: current)"),
).action(withOutput(runArchiveCommand));
return worktree;
+4 -1
View File
@@ -1,6 +1,7 @@
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 */
@@ -40,12 +41,14 @@ 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 {
@@ -53,7 +56,7 @@ export async function runLsCommand(
const agents = agentsPayload.entries.map((entry) => entry.agent);
// Get worktree list from daemon
const response = await client.getPaseoWorktreeList({});
const response = await client.getPaseoWorktreeList({ cwd });
await client.close();
@@ -0,0 +1,13 @@
import { resolve } from "node:path";
import type { DaemonTarget } from "../../utils/daemon-target.js";
export function resolveRepositoryDirectory(options: {
cwd?: string;
daemonTarget: DaemonTarget;
}): string {
const cwd = options.cwd ?? process.cwd();
if (options.daemonTarget.kind === "instance") {
return resolve(cwd);
}
return cwd;
}
+68 -1
View File
@@ -21,7 +21,10 @@
import assert from "node:assert";
import { getAvailablePort } from "./helpers/network.ts";
import { $ } from "zx";
import { mkdtemp, rm } from "fs/promises";
import { mkdtemp, rm, mkdir } from "fs/promises";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { runPaseoCli, startTestDaemon } from "./helpers/test-daemon.ts";
import { tmpdir } from "os";
import { join } from "path";
@@ -50,6 +53,7 @@ try {
const result = await $`npx paseo worktree ls --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "worktree ls --help should exit 0");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("--cwd"), "help should mention --cwd option");
console.log("✓ worktree ls --help shows options\n");
}
@@ -86,6 +90,7 @@ try {
const result = await $`npx paseo worktree archive --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "worktree archive --help should exit 0");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("--cwd"), "help should mention --cwd option");
assert(result.stdout.includes("<name>"), "help should mention required name argument");
console.log("✓ worktree archive --help shows options\n");
}
@@ -167,4 +172,66 @@ try {
await rm(paseoHome, { recursive: true, force: true });
}
// Exercise repository selection through the real CLI and daemon. An empty
// request used to fail before Git could inspect even a valid repository.
const daemon = await startTestDaemon();
const git = promisify(execFile);
try {
const repo = join(daemon.workDir, "repository");
await mkdir(repo);
await git("git", ["init", repo]);
await git("git", [
"-C",
repo,
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"commit",
"--allow-empty",
"-m",
"Initial commit",
]);
for (const args of [
["worktree", "ls", "--json"],
["worktree", "ls", "--cwd", repo, "--json"],
["worktree", "ls", "--cwd", "./repository", "--json"],
["worktree", "ls", "--host", `127.0.0.1:${daemon.port}`, "--cwd", ".", "--json"],
]) {
const explicit = args.includes("--cwd");
const result = await runPaseoCli(daemon, args, {
cwd: explicit ? daemon.workDir : repo,
env: { FORCE_COLOR: undefined },
});
assert.strictEqual(result.exitCode, 0, result.stderr);
assert.deepStrictEqual(JSON.parse(result.stdout), []);
}
for (const args of [
["worktree", "archive", "missing", "--json"],
["worktree", "archive", "missing", "--cwd", repo, "--json"],
["worktree", "archive", "missing", "--cwd", "./repository", "--json"],
[
"worktree",
"archive",
"missing",
"--host",
`127.0.0.1:${daemon.port}`,
"--cwd",
".",
"--json",
],
]) {
const result = await runPaseoCli(daemon, args, {
cwd: args.includes("--cwd") ? daemon.workDir : repo,
env: { FORCE_COLOR: undefined },
});
assert.notStrictEqual(result.exitCode, 0);
assert.strictEqual(JSON.parse(result.stderr).error.code, "WORKTREE_NOT_FOUND", result.stderr);
}
} finally {
await daemon.stop();
}
console.log("=== All worktree tests passed ===");