diff --git a/src/hooks/start-work/parse-user-request.test.ts b/src/hooks/start-work/parse-user-request.test.ts new file mode 100644 index 00000000..e5d61a4c --- /dev/null +++ b/src/hooks/start-work/parse-user-request.test.ts @@ -0,0 +1,78 @@ +/// + +import { describe, expect, test } from "bun:test" +import { parseUserRequest } from "./parse-user-request" + +describe("parseUserRequest", () => { + describe("when no user-request tag", () => { + test("#given prompt without tag #when parsing #then returns nulls", () => { + const result = parseUserRequest("Just a regular message without any tags") + expect(result.planName).toBeNull() + expect(result.explicitWorktreePath).toBeNull() + }) + }) + + describe("when user-request tag is empty", () => { + test("#given empty user-request tag #when parsing #then returns nulls", () => { + const result = parseUserRequest(" ") + expect(result.planName).toBeNull() + expect(result.explicitWorktreePath).toBeNull() + }) + }) + + describe("when only plan name given", () => { + test("#given plan name without worktree flag #when parsing #then returns plan name with null worktree", () => { + const result = parseUserRequest("\nmy-plan\n") + expect(result.planName).toBe("my-plan") + expect(result.explicitWorktreePath).toBeNull() + }) + }) + + describe("when only --worktree flag given", () => { + test("#given --worktree with path only #when parsing #then returns worktree path with null plan", () => { + const result = parseUserRequest("--worktree /home/user/repo-feat") + expect(result.planName).toBeNull() + expect(result.explicitWorktreePath).toBe("/home/user/repo-feat") + }) + }) + + describe("when plan name and --worktree are both given", () => { + test("#given plan name before --worktree #when parsing #then returns both", () => { + const result = parseUserRequest("my-plan --worktree /path/to/worktree") + expect(result.planName).toBe("my-plan") + expect(result.explicitWorktreePath).toBe("/path/to/worktree") + }) + + test("#given --worktree before plan name #when parsing #then returns both", () => { + const result = parseUserRequest("--worktree /path/to/worktree my-plan") + expect(result.planName).toBe("my-plan") + expect(result.explicitWorktreePath).toBe("/path/to/worktree") + }) + }) + + describe("when --worktree flag has no path", () => { + test("#given --worktree without path #when parsing #then worktree path is null", () => { + const result = parseUserRequest("--worktree") + expect(result.explicitWorktreePath).toBeNull() + }) + }) + + describe("when ultrawork keywords are present", () => { + test("#given plan name with ultrawork keyword #when parsing #then strips keyword from plan name", () => { + const result = parseUserRequest("my-plan ultrawork") + expect(result.planName).toBe("my-plan") + }) + + test("#given plan name with ulw keyword and worktree #when parsing #then strips ulw, preserves worktree", () => { + const result = parseUserRequest("my-plan ulw --worktree /path/to/wt") + expect(result.planName).toBe("my-plan") + expect(result.explicitWorktreePath).toBe("/path/to/wt") + }) + + test("#given only ultrawork keyword with worktree #when parsing #then plan name is null, worktree preserved", () => { + const result = parseUserRequest("ultrawork --worktree /wt") + expect(result.planName).toBeNull() + expect(result.explicitWorktreePath).toBe("/wt") + }) + }) +}) diff --git a/src/hooks/start-work/parse-user-request.ts b/src/hooks/start-work/parse-user-request.ts new file mode 100644 index 00000000..627deb67 --- /dev/null +++ b/src/hooks/start-work/parse-user-request.ts @@ -0,0 +1,29 @@ +const KEYWORD_PATTERN = /\b(ultrawork|ulw)\b/gi +const WORKTREE_FLAG_PATTERN = /--worktree(?:\s+(\S+))?/ + +export interface ParsedUserRequest { + planName: string | null + explicitWorktreePath: string | null +} + +export function parseUserRequest(promptText: string): ParsedUserRequest { + const match = promptText.match(/\s*([\s\S]*?)\s*<\/user-request>/i) + if (!match) return { planName: null, explicitWorktreePath: null } + + let rawArg = match[1].trim() + if (!rawArg) return { planName: null, explicitWorktreePath: null } + + const worktreeMatch = rawArg.match(WORKTREE_FLAG_PATTERN) + const explicitWorktreePath = worktreeMatch ? (worktreeMatch[1] ?? null) : null + + if (worktreeMatch) { + rawArg = rawArg.replace(worktreeMatch[0], "").trim() + } + + const cleanedArg = rawArg.replace(KEYWORD_PATTERN, "").trim() + + return { + planName: cleanedArg || null, + explicitWorktreePath, + } +}