diff --git a/docs/testcases.md b/docs/testcases.md index d829e6b..d7a57d2 100644 --- a/docs/testcases.md +++ b/docs/testcases.md @@ -197,6 +197,13 @@ Run explicitly with `bun run test:e2e` (not included in `bun test`). - CLI invoked via `Bun.spawn` running `bun run src/index.ts --yes` - On test failure, state file is kept and a manual cleanup message is displayed +### CLI Prompt Handling + +| # | Case | Input | Expected | +|---|------|-------|----------| +| C-1 | Apply confirmation accepts affirmative answers | `y`, `yes`, mixed case, surrounding whitespace | Apply continues | +| C-2 | Apply confirmation defaults to no | empty input, `n`, `no` | Apply is cancelled | + ### E2E-1: Full Lifecycle Precondition: agup.yaml with environment + skill + agent placed in temp directory diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 03bd35b..c915815 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { parseCliArgs } from "./index.ts"; +import { parseCliArgs, parseConfirmationAnswer } from "./index.ts"; describe("parseCliArgs", () => { test("parses global options before the command", () => { @@ -78,3 +78,17 @@ describe("parseCliArgs", () => { ); }); }); + +describe("parseConfirmationAnswer", () => { + test("accepts y and yes in a case-insensitive way", () => { + expect(parseConfirmationAnswer("y")).toBe(true); + expect(parseConfirmationAnswer("YES")).toBe(true); + expect(parseConfirmationAnswer(" yes ")).toBe(true); + }); + + test("rejects empty and non-affirmative answers", () => { + expect(parseConfirmationAnswer("")).toBe(false); + expect(parseConfirmationAnswer("n")).toBe(false); + expect(parseConfirmationAnswer("no")).toBe(false); + }); +}); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 593d054..93d703a 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,5 +1,6 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; +import { createInterface } from "node:readline/promises"; import { fileURLToPath } from "node:url"; import { parseYaml, @@ -304,13 +305,23 @@ function operationDetail(op: Operation): string { } } +export function parseConfirmationAnswer(answer: string): boolean { + const normalized = answer.trim().toLowerCase(); + return normalized === "y" || normalized === "yes"; +} + async function confirm(message: string): Promise { - process.stdout.write(`${message} [y/N] `); - for await (const line of console) { - const answer = (line as string).trim().toLowerCase(); - return answer === "y" || answer === "yes"; + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + try { + const answer = await rl.question(`${message} [y/N] `); + return parseConfirmationAnswer(answer); + } finally { + rl.close(); } - return false; } async function createApiClient(): Promise {