From 36e54a097e3ed8a649cab7a2c3dc60c99d42698b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 10 May 2026 16:18:00 +0800 Subject: [PATCH] Refactor worktree create request parsing (#885) --- .../{create.test.ts => create-input.test.ts} | 82 +++---------- .../cli/src/commands/worktree/create-input.ts | 104 ++++++++++++++++ packages/cli/src/commands/worktree/create.ts | 116 +----------------- 3 files changed, 122 insertions(+), 180 deletions(-) rename packages/cli/src/commands/worktree/{create.test.ts => create-input.test.ts} (53%) create mode 100644 packages/cli/src/commands/worktree/create-input.ts diff --git a/packages/cli/src/commands/worktree/create.test.ts b/packages/cli/src/commands/worktree/create-input.test.ts similarity index 53% rename from packages/cli/src/commands/worktree/create.test.ts rename to packages/cli/src/commands/worktree/create-input.test.ts index e0949601b..2091fcd6e 100644 --- a/packages/cli/src/commands/worktree/create.test.ts +++ b/packages/cli/src/commands/worktree/create-input.test.ts @@ -1,21 +1,17 @@ import { describe, expect, it } from "vitest"; -import { - buildCreateWorktreeInput, - toDaemonCreateInput, - type WorktreeCreateOptions, -} from "./create.js"; +import { buildCreateWorktreeRequest, type WorktreeCreateOptions } from "./create-input.js"; const REPO = "/tmp/repo"; function build(options: WorktreeCreateOptions): unknown { try { - return buildCreateWorktreeInput(options, REPO); + return buildCreateWorktreeRequest(options, REPO); } catch (err) { return err; } } -describe("buildCreateWorktreeInput", () => { +describe("buildCreateWorktreeRequest", () => { it("requires --mode", () => { expect(build({})).toMatchObject({ code: "MISSING_MODE" }); }); @@ -28,17 +24,20 @@ describe("buildCreateWorktreeInput", () => { expect(build({ mode: "branch-off" })).toMatchObject({ code: "MISSING_NEW_BRANCH" }); }); - it("branch-off parses with new branch only", () => { + it("branch-off builds a daemon request with a new branch", () => { expect(build({ mode: "branch-off", newBranch: "feature-x" })).toEqual({ cwd: REPO, - target: { mode: "branch-off", newBranch: "feature-x" }, + worktreeSlug: "feature-x", + action: "branch-off", }); }); - it("branch-off parses with base ref", () => { + it("branch-off includes the base ref when provided", () => { expect(build({ mode: "branch-off", newBranch: "feature-x", base: "main" })).toEqual({ cwd: REPO, - target: { mode: "branch-off", newBranch: "feature-x", base: "main" }, + worktreeSlug: "feature-x", + action: "branch-off", + refName: "main", }); }); @@ -46,10 +45,11 @@ describe("buildCreateWorktreeInput", () => { expect(build({ mode: "checkout-branch" })).toMatchObject({ code: "MISSING_BRANCH" }); }); - it("checkout-branch parses with branch", () => { + it("checkout-branch builds a checkout request for the branch", () => { expect(build({ mode: "checkout-branch", branch: "feat/x" })).toEqual({ cwd: REPO, - target: { mode: "checkout-branch", branch: "feat/x" }, + action: "checkout", + refName: "feat/x", }); }); @@ -69,62 +69,8 @@ describe("buildCreateWorktreeInput", () => { }); }); - it("checkout-pr parses positive integers", () => { + it("checkout-pr builds a checkout request for the pull request", () => { expect(build({ mode: "checkout-pr", prNumber: "42" })).toEqual({ - cwd: REPO, - target: { mode: "checkout-pr", prNumber: 42 }, - }); - }); -}); - -describe("toDaemonCreateInput", () => { - it("maps branch-off without base", () => { - expect( - toDaemonCreateInput({ - cwd: REPO, - target: { mode: "branch-off", newBranch: "feature-x" }, - }), - ).toEqual({ - cwd: REPO, - worktreeSlug: "feature-x", - action: "branch-off", - }); - }); - - it("maps branch-off with base ref", () => { - expect( - toDaemonCreateInput({ - cwd: REPO, - target: { mode: "branch-off", newBranch: "feature-x", base: "main" }, - }), - ).toEqual({ - cwd: REPO, - worktreeSlug: "feature-x", - action: "branch-off", - refName: "main", - }); - }); - - it("maps checkout-branch to action=checkout + refName", () => { - expect( - toDaemonCreateInput({ - cwd: REPO, - target: { mode: "checkout-branch", branch: "feat/x" }, - }), - ).toEqual({ - cwd: REPO, - action: "checkout", - refName: "feat/x", - }); - }); - - it("maps checkout-pr to action=checkout + githubPrNumber", () => { - expect( - toDaemonCreateInput({ - cwd: REPO, - target: { mode: "checkout-pr", prNumber: 42 }, - }), - ).toEqual({ cwd: REPO, action: "checkout", githubPrNumber: 42, diff --git a/packages/cli/src/commands/worktree/create-input.ts b/packages/cli/src/commands/worktree/create-input.ts new file mode 100644 index 000000000..15b741f14 --- /dev/null +++ b/packages/cli/src/commands/worktree/create-input.ts @@ -0,0 +1,104 @@ +import type { DaemonClient } from "@getpaseo/server"; +import type { CommandError, CommandOptions } from "../../output/index.js"; + +export interface WorktreeCreateOptions extends CommandOptions { + host?: string; + cwd?: string; + mode?: string; + newBranch?: string; + base?: string; + branch?: string; + prNumber?: string; +} + +const VALID_MODES = ["branch-off", "checkout-branch", "checkout-pr"] as const; + +type CreatePaseoWorktreeRequest = Parameters[0]; + +export function buildCreateWorktreeRequest( + options: WorktreeCreateOptions, + cwd: string, +): CreatePaseoWorktreeRequest { + const mode = options.mode; + if (!mode) { + throw cmdError( + "MISSING_MODE", + "--mode is required", + `Expected one of: ${VALID_MODES.join(", ")}`, + ); + } + + switch (mode) { + case "branch-off": + return buildBranchOffRequest(options, cwd); + case "checkout-branch": + return buildCheckoutBranchRequest(options, cwd); + case "checkout-pr": + return buildCheckoutPrRequest(options, cwd); + default: + throw cmdError( + "INVALID_MODE", + `Invalid --mode: ${mode}`, + `Expected one of: ${VALID_MODES.join(", ")}`, + ); + } +} + +function buildBranchOffRequest( + options: WorktreeCreateOptions, + cwd: string, +): CreatePaseoWorktreeRequest { + if (!options.newBranch) { + throw cmdError("MISSING_NEW_BRANCH", "--new-branch is required for --mode branch-off"); + } + + return { + cwd, + worktreeSlug: options.newBranch, + action: "branch-off", + ...(options.base ? { refName: options.base } : {}), + }; +} + +function buildCheckoutBranchRequest( + options: WorktreeCreateOptions, + cwd: string, +): CreatePaseoWorktreeRequest { + if (!options.branch) { + throw cmdError("MISSING_BRANCH", "--branch is required for --mode checkout-branch"); + } + + return { + cwd, + action: "checkout", + refName: options.branch, + }; +} + +function buildCheckoutPrRequest( + options: WorktreeCreateOptions, + cwd: string, +): CreatePaseoWorktreeRequest { + if (options.prNumber === undefined || options.prNumber === "") { + throw cmdError("MISSING_PR_NUMBER", "--pr-number is required for --mode checkout-pr"); + } + + const prNumber = Number(options.prNumber); + if (!Number.isInteger(prNumber) || prNumber <= 0) { + throw cmdError( + "INVALID_PR_NUMBER", + `Invalid --pr-number: ${options.prNumber}`, + "Expected a positive integer", + ); + } + + return { + cwd, + action: "checkout", + githubPrNumber: prNumber, + }; +} + +function cmdError(code: string, message: string, details?: string): CommandError { + return details ? { code, message, details } : { code, message }; +} diff --git a/packages/cli/src/commands/worktree/create.ts b/packages/cli/src/commands/worktree/create.ts index 7e8f183b4..a7a4796b8 100644 --- a/packages/cli/src/commands/worktree/create.ts +++ b/packages/cli/src/commands/worktree/create.ts @@ -2,12 +2,8 @@ import path from "node:path"; import type { Command } from "commander"; import type { DaemonClient } from "@getpaseo/server"; import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import type { - CommandError, - CommandOptions, - OutputSchema, - SingleResult, -} from "../../output/index.js"; +import type { CommandError, OutputSchema, SingleResult } from "../../output/index.js"; +import { buildCreateWorktreeRequest, type WorktreeCreateOptions } from "./create-input.js"; export interface WorktreeCreateResult { name: string; @@ -24,110 +20,6 @@ export const createSchema: OutputSchema = { ], }; -export interface WorktreeCreateOptions extends CommandOptions { - host?: string; - cwd?: string; - mode?: string; - newBranch?: string; - base?: string; - branch?: string; - prNumber?: string; -} - -export type WorktreeCreateTarget = - | { mode: "branch-off"; newBranch: string; base?: string } - | { mode: "checkout-branch"; branch: string } - | { mode: "checkout-pr"; prNumber: number }; - -export interface ParsedWorktreeCreateInput { - cwd: string; - target: WorktreeCreateTarget; -} - -const VALID_MODES = ["branch-off", "checkout-branch", "checkout-pr"] as const; - -export function buildCreateWorktreeInput( - options: WorktreeCreateOptions, - cwd: string, -): ParsedWorktreeCreateInput { - const mode = options.mode; - if (!mode) { - throw cmdError( - "MISSING_MODE", - "--mode is required", - `Expected one of: ${VALID_MODES.join(", ")}`, - ); - } - - switch (mode) { - case "branch-off": { - if (!options.newBranch) { - throw cmdError("MISSING_NEW_BRANCH", "--new-branch is required for --mode branch-off"); - } - return { - cwd, - target: { - mode: "branch-off", - newBranch: options.newBranch, - ...(options.base ? { base: options.base } : {}), - }, - }; - } - case "checkout-branch": { - if (!options.branch) { - throw cmdError("MISSING_BRANCH", "--branch is required for --mode checkout-branch"); - } - return { cwd, target: { mode: "checkout-branch", branch: options.branch } }; - } - case "checkout-pr": { - if (options.prNumber === undefined || options.prNumber === "") { - throw cmdError("MISSING_PR_NUMBER", "--pr-number is required for --mode checkout-pr"); - } - const parsed = Number(options.prNumber); - if (!Number.isInteger(parsed) || parsed <= 0) { - throw cmdError( - "INVALID_PR_NUMBER", - `Invalid --pr-number: ${options.prNumber}`, - "Expected a positive integer", - ); - } - return { cwd, target: { mode: "checkout-pr", prNumber: parsed } }; - } - default: - throw cmdError( - "INVALID_MODE", - `Invalid --mode: ${mode}`, - `Expected one of: ${VALID_MODES.join(", ")}`, - ); - } -} - -export function toDaemonCreateInput(parsed: ParsedWorktreeCreateInput) { - switch (parsed.target.mode) { - case "branch-off": - return { - cwd: parsed.cwd, - worktreeSlug: parsed.target.newBranch, - action: "branch-off" as const, - ...(parsed.target.base ? { refName: parsed.target.base } : {}), - }; - case "checkout-branch": - return { - cwd: parsed.cwd, - action: "checkout" as const, - refName: parsed.target.branch, - }; - case "checkout-pr": - return { - cwd: parsed.cwd, - action: "checkout" as const, - githubPrNumber: parsed.target.prNumber, - }; - default: - throw new Error("unreachable"); - } -} - function cmdError(code: string, message: string, details?: string): CommandError { return details ? { code, message, details } : { code, message }; } @@ -137,7 +29,7 @@ export async function runCreateCommand( _command: Command, ): Promise> { const cwd = options.cwd ?? process.cwd(); - const parsed = buildCreateWorktreeInput(options, cwd); + const request = buildCreateWorktreeRequest(options, cwd); const host = getDaemonHost({ host: options.host }); let client: DaemonClient; @@ -153,7 +45,7 @@ export async function runCreateCommand( } try { - const response = await client.createPaseoWorktree(toDaemonCreateInput(parsed)); + const response = await client.createPaseoWorktree(request); const workspace = response.workspace; if (!workspace || response.error) {