diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a6b76..3f28a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Each released version is tagged in git (`v0.0.1`, `v0.1.0`, etc.) and includes t ### Removed +- **`@inquirer/prompts` dropped; CLI is purely flag-driven now.** Every interactive prompt is gone: init's idea input, tech/tasks' spec picker, and scaffold's confirm. Each was a TTY-only convenience layer on top of flags that already drove the canonical input path. With api mode gone (see entry below), the remaining prompts had nothing meaningful to gate — the CLI's job is to load context and hand off to the host coding agent, not run a Q&A loop. Concrete behavior changes: `tech` / `tasks` with multiple specs and no slug arg now error with the available list (was: TTY → inquirer picker, non-TTY → error); `scaffold` requires `--yes` to confirm before writing (was: TTY → inquirer confirm, non-TTY → error); `init` greenfield without `--idea` always prints the structured agent handoff (was: TTY → inquirer input, non-TTY → handoff). The TTY/non-TTY distinction is gone entirely — `src/utils/tty.js`, `test/setup.js`, and `vitest.config.js` are deleted; `deps.isInteractive` is no longer a dependency-injection seam. CLAUDE.md and README.md updated to drop "TTY-only fallback" framing and replace with "flags drive input; CLI never blocks on stdin." — Ankur + - **API mode dropped — Draftwise runs only inside coding agents now.** Every command (`scan`, `explain`, `new`, `tech`, `tasks`, `init`) used to support two execution paths: agent mode (print scanner data + instruction, let the host coding agent do the synthesis) and api mode (call Claude directly via `@anthropic-ai/sdk` and write the spec from the CLI). The api path is gone. `src/ai/provider.js`, `src/ai/providers/claude.js`, and the `@anthropic-ai/sdk` dependency are removed. `init` loses its `--ai-mode`, `--provider`, `--api-key-env`, `--stack`, and `--answers` flags plus the corresponding interactive prompts; `new`/`tech`/`tasks` lose `--force` and (for `new`) `--answers`; `scaffold` is unchanged. `config.yaml` schema simplifies — the `ai:` block (`mode`/`provider`/`api_key_env`/`model`/`max_tokens`) is no longer used; `loadConfig` returns only `projectState`/`stack`/`scanMaxFiles`. Existing configs with an `ai:` block keep working: `loadConfig` prints a one-line notice telling the user the block is safe to delete, then ignores it. The shared overwrite-guard and answers-flag utilities and the `src/ai/prompts/principles.js` + `src/ai/prompts/spec-quality.js` modules are removed — every prompt module now exports just `buildAgentInstruction` (or `AGENT_INSTRUCTION` for `scan`), since the host coding agent is the only consumer. CLAUDE.md and README.md updated end-to-end. — Ankur ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 31b42bf..a08f50e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ src/index.js → command router (dynamic imports, help) src/commands/ → one file per CLI command, default export async fn src/core/scanner.js → codebase scanning (frameworks, routes, components, models) src/ai/prompts/ → one prompt module per command. Each exports a `buildAgentInstruction(...)` (or `AGENT_INSTRUCTION` constant for `scan`) that the host coding agent reads — section structure, hard rules, save path. No SDK call from the CLI; the agent does the synthesis. -src/utils/ → config.js (yaml loader; returns projectState/stack/scanMaxFiles), specs.js (list .draftwise/specs/), slug.js, overview.js (read .draftwise/overview.md for greenfield context), scan-cache.js (fingerprinted scan cache, drop-in for scan()), flow-filter.js (narrow scan to flow-relevant items), scan-warnings.js (truncation + missing-framework messages), fs.js (shared pathExists), scan-projection.js (shared compactScan that trims a raw scan into a prompt-sized projection), scan-context.js (shared greenfield/brownfield branch for new/tech/tasks), draftwise-dir.js (`requireDraftwiseDir` guard), tty.js (isInteractive helper), agent-handoff.js (shared orienting prefix logged before every agent-mode handoff), project-state.js (filesystem auto-detect for `init` — bail-fast walk for source files using scanner's IGNORE_DIRS + CODE_EXTENSIONS), skill-providers.js (provider dir mapping + Claude-only frontmatter trim + `detectInstalledProviders` filesystem check shared across `skills install` / `uninstall` / `help`) +src/utils/ → config.js (yaml loader; returns projectState/stack/scanMaxFiles), specs.js (list .draftwise/specs/), slug.js, overview.js (read .draftwise/overview.md for greenfield context), scan-cache.js (fingerprinted scan cache, drop-in for scan()), flow-filter.js (narrow scan to flow-relevant items), scan-warnings.js (truncation + missing-framework messages), fs.js (shared pathExists), scan-projection.js (shared compactScan that trims a raw scan into a prompt-sized projection), scan-context.js (shared greenfield/brownfield branch for new/tech/tasks), draftwise-dir.js (`requireDraftwiseDir` guard), agent-handoff.js (shared orienting prefix logged before every agent-mode handoff), project-state.js (filesystem auto-detect for `init` — bail-fast walk for source files using scanner's IGNORE_DIRS + CODE_EXTENSIONS), skill-providers.js (provider dir mapping + Claude-only frontmatter trim + `detectInstalledProviders` filesystem check shared across `skills install` / `uninstall` / `help`) test/ → vitest, mirrors src structure .claude-plugin/ → plugin marketplace declaration (see "Claude Code plugin" below) plugin/ → plugin source tree shipped via the marketplace @@ -65,10 +65,10 @@ The single most important module is `src/core/scanner.js` — it parses the user - **eslint + prettier** for code style - **YAML** for config (`yaml` package) - **Markdown** for all spec documents -- **`@inquirer/prompts`** for interactive prompts (init's idea prompt, tech/tasks spec picker, scaffold's confirm) — strictly the TTY-only convenience layer; flags drive the canonical input path (see "How input works" below). -- **No AI SDK.** Draftwise no longer calls models from the CLI. Every command prints scanner data plus an instruction for the host coding agent (Claude Code, Cursor, Gemini CLI, etc.), which does the synthesis using its own model. +- **No interactive prompts.** Every command takes its input via flags or positional args. Missing required input → error with a usage hint, or (for `draftwise init` greenfield without `--idea`) a structured agent handoff. No `@inquirer/prompts`, no `node:readline` — the CLI never blocks on stdin. +- **No AI SDK.** Draftwise doesn't call models from the CLI. Every command prints scanner data plus an instruction for the host coding agent (Claude Code, Cursor, Gemini CLI, etc.), which does the synthesis using its own model. -**On dependency pinning:** `@inquirer/prompts` and `yaml` use caret ranges because they're stable 1.x+ packages where minor bumps follow semver. Dependabot (`.github/dependabot.yml`) opens PRs for both. +**On dependency pinning:** `yaml` uses a caret range because it's a stable 2.x package where minor bumps follow semver. Dependabot (`.github/dependabot.yml`) opens PRs for it. No TypeScript for v1 — keep it simple. May migrate later if the codebase grows. @@ -104,7 +104,7 @@ scan: **Conversation lives in the host agent.** The CLI is a non-conversational executor: it loads context (scanner data or greenfield plan), prints an instruction, and exits. The host coding agent walks the PM through clarifying questions in chat, then writes the spec to disk. `draftwise new` doesn't run a Q&A loop from the CLI itself — its instruction tells the agent to. -**Flags drive input; inquirer is a TTY-only fallback.** Every command takes its full input set as flags (`--mode`, `--idea`, `--yes`, etc.) parsed via Node's built-in `util.parseArgs`. When stdin is a TTY and a required value is missing, inquirer fires to fill it in — that's the only place inquirer lives. When stdin is not a TTY (CI, coding-agent shell), most commands error with a specific usage hint instead of hanging on a prompt; `draftwise init` is special — when it can't proceed without asking the user something (greenfield without `--idea`), it prints a structured **agent handoff** (the question in chat-friendly format + a re-invocation template, all under `AGENT_HANDOFF_PREFIX`) and exits cleanly, so the host coding agent reads stderr, asks the user in chat, and re-invokes with the collected flag. TTY-detection helper in `src/utils/tty.js`; tests opt into either path via `deps.isInteractive`. +**Flags drive input; no interactive prompts.** Every command takes its full input set as flags (`--mode`, `--idea`, `--yes`) or positional args, parsed via Node's built-in `util.parseArgs`. Missing required input errors with a specific usage hint — the CLI never blocks waiting for stdin. `draftwise init` is the one special case: greenfield without `--idea` prints a structured **agent handoff** (the question in chat-friendly format + a re-invocation template, all under `AGENT_HANDOFF_PREFIX`) and exits cleanly, so the host coding agent reads stderr, asks the user in chat, and re-invokes with the collected flag. A plain-terminal user reads the same handoff as a usage hint. **Single repo, single feature spec at a time.** No cross-spec dependency tracking. No multi-repo. Keep scope tight. @@ -127,7 +127,7 @@ draftwise skills uninstall [--provider=...] [--scope=...] → remove s draftwise skills help → list known harnesses + install state ``` -Each command is a separate file under `src/commands/` with a single `export default async function(args, deps = {}) {}`. The `deps` object is the dependency-injection seam used by tests — `cwd`, `log`, `scan`, `loadConfig`, `readOverview`, `isInteractive`, and per-command prompt overrides. +Each command is a separate file under `src/commands/` with a single `export default async function(args, deps = {}) {}`. The `deps` object is the dependency-injection seam used by tests — `cwd`, `log`, `scan`, `loadConfig`, `readOverview`, `listSpecs`, etc. --- @@ -216,7 +216,7 @@ Don't try to make v1 universal. A great experience for JS/TS is better than a me ## Conventions -- **File per command.** `src/commands/.js` with `export default async function(args, deps = {}) {}`. The `deps` argument is how tests inject `cwd`, `log`, `scan`, `loadConfig`, `readOverview`, `isInteractive`, and prompts. +- **File per command.** `src/commands/.js` with `export default async function(args, deps = {}) {}`. The `deps` argument is how tests inject `cwd`, `log`, `scan`, `loadConfig`, `readOverview`, `listSpecs`, etc. - **Async/await everywhere.** No `.then()` chains. - **Console output for CLI feedback.** Plain text for now — colored output (kleur/chalk) is deferred until there's a need. - **Errors bubble up to `src/index.js`.** It catches and prints a friendly message, then exits non-zero. diff --git a/README.md b/README.md index 398211c..af8c254 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ Claude Code · GitHub Copilot · Cursor · Gemini CLI · Codex CLI · Antigravit In practice it has only been smoke-tested in a plain terminal so far. If you run it inside one of these and something breaks, please open an issue. -**Non-interactive use.** Every command runs without a TTY — pass values as flags instead of letting inquirer prompt for them. That's what makes the agent integration possible: a slash-command wrapper (or any host agent) can collect answers in chat and re-invoke `draftwise ` with `--mode=...`, `--idea="..."`, `--yes`, etc. Run `draftwise --help` for the per-command flag list. When `draftwise init` is run in a non-TTY shell as greenfield without `--idea`, it prints a structured handoff with the question to ask the user — copy it into your AI assistant if you're not already inside one. +**Flags-driven, never blocks.** Every command takes its input via flags or positional args; missing required input errors with a usage hint instead of waiting on stdin. That's what makes the agent integration possible: a slash-command wrapper (or any host agent) collects answers in chat and re-invokes `draftwise ` with `--mode=...`, `--idea="..."`, `--yes`, etc. Run `draftwise --help` for the per-command flag list. `draftwise init` greenfield without `--idea` is the one case that prints a structured handoff instead of erroring — copy it into your AI assistant if you're not already inside one. --- diff --git a/package-lock.json b/package-lock.json index 989a363..439eb78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.2.1", "license": "MIT", "dependencies": { - "@inquirer/prompts": "^8.4.2", "yaml": "^2.8.3" }, "bin": { @@ -254,334 +253,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inquirer/ansi": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", - "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.4.tgz", - "integrity": "sha512-w6KF8ZYRvqHhROkOTHXYC3qIV/KYEu5o12oLqQySvch61vrYtRxNSHTONSdJqWiFJPlCUQAHT5OgOIyuTr+MHQ==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.9", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", - "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "11.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", - "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/editor": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.1.tgz", - "integrity": "sha512-6y11LgmNpmn5D2aB5FgnCfBUBK8ZstwLCalyJmORcJZ/WrhOjm16mu6eSqIx8DnErxDqSLr+Jkp+GP8/Nwd5tA==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/external-editor": "^3.0.0", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.13.tgz", - "integrity": "sha512-dF2zvrFo9LshkcB23/O1il13kBkBltWIXzut1evfbuBLXMiGIuC45c+ZQ0uukjCDsvI8OWqun4FRYMnzFCQa3g==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz", - "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==", - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.2" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", - "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - } - }, - "node_modules/@inquirer/input": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.12.tgz", - "integrity": "sha512-uiMFBl4LqFzJClh80Q3f9hbOFJ6kgkDWI4LjAeBuyO6EanVVMF69AgOvpi1qdqjDSjDN6578B6nky9ceEpI+1Q==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.12.tgz", - "integrity": "sha512-/vrwhEf7Xsuh+YlHF4IjSy3g1cyrQuPaSiHIxCEbLu8qnfvrcvJyCkoktOOF+xV9gSb77/G0n3h04RbMDW2sIg==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.12.tgz", - "integrity": "sha512-CBh7YHju623lxJRcAOo498ZUwIuMy63bqW/vVq0tQAZVv+lkWlHkP9ealYE1utWSisEShY5VMdzIXRmyEODzcQ==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.9", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.2.tgz", - "integrity": "sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==", - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^5.1.4", - "@inquirer/confirm": "^6.0.12", - "@inquirer/editor": "^5.1.1", - "@inquirer/expand": "^5.0.13", - "@inquirer/input": "^5.0.12", - "@inquirer/number": "^4.0.12", - "@inquirer/password": "^5.0.12", - "@inquirer/rawlist": "^5.2.8", - "@inquirer/search": "^4.1.8", - "@inquirer/select": "^5.1.4" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.8.tgz", - "integrity": "sha512-Su7FQvp5buZmCymN3PPoYv31ZQQX4ve2j02k7piGgKAWgE+AQRB5YoYVveGXcl3TZ9ldgRMSxj56YfDFmmaqLg==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.8.tgz", - "integrity": "sha512-fGiHKGD6DyPIYUWxoXnQTeXeyYqSOUrasDMABBmMHUalH/LxkuzY0xVRtimXAt1sUeeyYkVuKQx1bebMuN11Kw==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.4.tgz", - "integrity": "sha512-2kWcGKPMLAXAWRp1AH1SLsQmX+j0QjeljyXMUji9WMZC8nRDO0b7qquIGr6143E7KMLt3VAIGNXzwa/6PXQs4Q==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.9", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", - "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1153,21 +824,6 @@ "node": ">=18" } }, - "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", - "license": "MIT" - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1438,30 +1094,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-string-truncated-width": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", - "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", - "license": "MIT" - }, - "node_modules/fast-string-width": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "license": "MIT", - "dependencies": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "node_modules/fast-wrap-ansi": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", - "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", - "license": "MIT", - "dependencies": { - "fast-string-width": "^3.0.2" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1572,22 +1204,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2005,15 +1621,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2247,12 +1854,6 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2283,18 +1884,6 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index bdd991a..a940da5 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "vitest": "^4.1.5" }, "dependencies": { - "@inquirer/prompts": "^8.4.2", "yaml": "^2.8.3" }, "files": [ diff --git a/plugin/skills/draftwise/SKILL.md b/plugin/skills/draftwise/SKILL.md index 4b13ed3..6273a4b 100644 --- a/plugin/skills/draftwise/SKILL.md +++ b/plugin/skills/draftwise/SKILL.md @@ -42,7 +42,7 @@ Draftwise has implicit dependencies. Surface them in chat before invoking the CL ## Common patterns across verbs - **`!`draftwise $ARGUMENTS`** is the starting shell call. Pass through whatever flags the user already gave; collect missing required ones in chat first. -- **Structured handoff (init in non-TTY)** — if `draftwise init` prints a block starting with "INIT — answer these in chat..." it's asking you to walk the user through the listed questions. Follow the INSTRUCTION block at the bottom verbatim — re-invoke `draftwise init` with the user's collected flags. +- **Structured handoff (init greenfield without --idea)** — if `draftwise init` prints a block starting with "INIT — answer..." it's asking you to walk the user through the listed question. Follow the INSTRUCTION block at the bottom verbatim — re-invoke `draftwise init` with the user's collected flag. - **Agent-mode handoff (scan / explain / new / tech / tasks)** — the CLI always prints SCANNER OUTPUT or PROJECT PLAN followed by an INSTRUCTION block. It expects YOU to do the synthesis. Follow the INSTRUCTION exactly; ground every claim in the scanner data shown. Don't invent files, routes, or models that aren't there. The CLI never writes the spec — that's your job. - **Existing target file** — when re-running `new` / `tech` / `tasks`, ask the user before overwriting their hand-edits. The CLI doesn't guard against this (it doesn't write specs at all), so the conversation has to. - **`.draftwise/` not found** — point the user at `/draftwise init`. diff --git a/plugin/skills/draftwise/reference/init.md b/plugin/skills/draftwise/reference/init.md index faeed69..0d96d70 100644 --- a/plugin/skills/draftwise/reference/init.md +++ b/plugin/skills/draftwise/reference/init.md @@ -24,7 +24,7 @@ If the detection looks wrong (e.g. the user is starting fresh in a folder that h ## How to ask for the idea (greenfield only) -The CLI's only ask in non-TTY is the project idea. Add a sentence of context so the user can answer well: "A sentence or two on what you want to build — once init finishes I'll ask follow-up questions about stack and structure." If the user gives a one-word idea ("a blog", "a todo app"), ask for one more concrete sentence before invoking — the host-agent conversation goes much better with concrete input than with placeholder ideas. +The CLI's only ask is the project idea (greenfield only). Add a sentence of context so the user can answer well: "A sentence or two on what you want to build — once init finishes I'll ask follow-up questions about stack and structure." If the user gives a one-word idea ("a blog", "a todo app"), ask for one more concrete sentence before invoking — the host-agent conversation goes much better with concrete input than with placeholder ideas. ## What to do after greenfield init finishes diff --git a/src/commands/init.js b/src/commands/init.js index 47ee84c..a275d85 100644 --- a/src/commands/init.js +++ b/src/commands/init.js @@ -2,11 +2,9 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { parseArgs } from 'node:util'; import { stringify as yamlStringify } from 'yaml'; -import { input } from '@inquirer/prompts'; import { cachedScan as defaultScan } from '../utils/scan-cache.js'; import { describeScanWarnings } from '../utils/scan-warnings.js'; import { pathExists } from '../utils/fs.js'; -import { isInteractive as defaultIsInteractive } from '../utils/tty.js'; import { AGENT_HANDOFF_PREFIX } from '../utils/agent-handoff.js'; import { detectProjectState as defaultDetectProjectState } from '../utils/project-state.js'; import { buildAgentInstruction as buildGreenfieldAgentInstruction } from '../ai/prompts/greenfield.js'; @@ -32,10 +30,10 @@ accordingly. Greenfield prints an instruction your coding agent follows to walk through stack selection. Brownfield scans the codebase. Refuses to overwrite an existing .draftwise/. -Non-TTY (CI, coding-agent shell): brownfield needs no further input -and runs straight through. Greenfield without --idea prints a -structured agent handoff so the host coding agent can ask the user -in chat and re-invoke with the flag. +Brownfield needs no further input and runs straight through. +Greenfield without --idea prints a structured agent handoff so +the host coding agent can ask the user in chat and re-invoke with +the flag. `; const ARG_OPTIONS = { @@ -74,15 +72,6 @@ a chat and have it generate the plan, then save it back to this file. `; } -const DEFAULT_PROMPTS = { - promptIdea: () => - input({ - message: 'In a sentence or two — what do you want to build?', - validate: (v) => - v.trim().length > 0 ? true : 'Please describe the idea.', - }), -}; - function buildConfigYaml({ projectState, stack }) { const project = { state: projectState }; if (stack) project.stack = stack; @@ -165,29 +154,7 @@ async function runBrownfield({ cwd, log, scan, draftwiseDir }) { log('Next: draftwise scan'); } -async function runGreenfield({ - log, - draftwiseDir, - prompts, - isInteractive, - ideaFlag, -}) { - log(''); - - let idea = ideaFlag; - if (idea === undefined || idea === null) { - if (isInteractive()) { - idea = await prompts.promptIdea(); - } else { - throw new Error( - 'Greenfield init needs --idea "". Pass it as a flag, or run init in an interactive terminal.', - ); - } - } - if (typeof idea !== 'string' || idea.trim().length === 0) { - throw new Error('--idea must be a non-empty string.'); - } - +async function runGreenfield({ log, draftwiseDir, idea }) { log(''); log('Handing the greenfield conversation off to your coding agent.'); log(AGENT_HANDOFF_PREFIX); @@ -225,10 +192,8 @@ export default async function init(args = [], deps = {}) { const cwd = deps.cwd ?? process.cwd(); const log = deps.log ?? ((msg) => console.error(msg)); const scan = deps.scan ?? defaultScan; - const isInteractive = deps.isInteractive ?? defaultIsInteractive; const detectProjectState = deps.detectProjectState ?? defaultDetectProjectState; - const prompts = { ...DEFAULT_PROMPTS, ...(deps.prompts ?? {}) }; let parsed; try { @@ -269,19 +234,17 @@ export default async function init(args = [], deps = {}) { modeSource = 'detected'; } - // Non-TTY structured handoff: greenfield without --idea is the only - // remaining case where init can't proceed without asking the user. Print - // the question in AGENT_HANDOFF_PREFIX format so the host coding agent - // (Claude Code, Cursor, etc.) reads it from stderr, asks the user in chat, - // and re-invokes with --idea filled in. - if ( - !isInteractive() && - projectState === 'greenfield' && - !flags.idea - ) { + // Greenfield without --idea: print the structured handoff so the host + // coding agent (Claude Code, Cursor, etc.) reads it from stderr, asks the + // user in chat, and re-invokes with --idea filled in. A plain-terminal + // user reads it as a usage hint and re-runs with the flag. + if (projectState === 'greenfield' && !flags.idea) { log(buildInitHandoff(projectState, modeSource)); return; } + if (projectState === 'greenfield' && flags.idea.trim().length === 0) { + throw new Error('--idea must be a non-empty string.'); + } log('Welcome to Draftwise. Setting up .draftwise/ for this project.'); log(''); @@ -307,8 +270,6 @@ export default async function init(args = [], deps = {}) { return runGreenfield({ log, draftwiseDir, - prompts, - isInteractive, - ideaFlag: flags.idea, + idea: flags.idea, }); } diff --git a/src/commands/scaffold.js b/src/commands/scaffold.js index 7db16d0..430df40 100644 --- a/src/commands/scaffold.js +++ b/src/commands/scaffold.js @@ -1,43 +1,35 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { join, dirname, resolve, sep } from 'node:path'; import { parseArgs } from 'node:util'; -import { confirm } from '@inquirer/prompts'; import { pathExists } from '../utils/fs.js'; import { requireDraftwiseDir } from '../utils/draftwise-dir.js'; import { loadConfig as defaultLoadConfig } from '../utils/config.js'; -import { isInteractive as defaultIsInteractive } from '../utils/tty.js'; -export const HELP = `draftwise scaffold — create initial files from a greenfield plan +export const HELP = `draftwise scaffold --yes — create initial files from a greenfield plan Usage: - draftwise scaffold # interactive — confirms before writing (TTY only) - draftwise scaffold --yes # skip the confirmation, just write the files + draftwise scaffold --yes # confirm and write the files Flags: - --yes, -y # Skip the confirmation prompt. - -Reads .draftwise/scaffold.json (written by \`draftwise init\` in -greenfield mode), confirms before writing, then creates each -initial_files entry with placeholder content. Skips files that -already exist. Refuses paths that escape the project root. Does -NOT run setup commands — they're printed for manual execution. - -In non-TTY (CI, coding-agent shell), --yes is required; without it -the command errors instead of hanging on the inquirer prompt. + --yes, -y # Required. Confirms you want files created. + +Reads .draftwise/scaffold.json (written by your coding agent during +greenfield init's handoff) and creates each initial_files entry with +placeholder content. Skips files that already exist. Refuses paths +that escape the project root. Does NOT run setup commands — they're +printed for manual execution. + +--yes is required so this never writes files without explicit +confirmation. If your setup commands include a project scaffolder +(e.g. create-next-app, create-vite), run those FIRST — scaffold +won't overwrite existing files but it may interfere with a fresh +scaffolder run. `; const ARG_OPTIONS = { yes: { type: 'boolean', short: 'y' }, }; -const DEFAULT_PROMPTS = { - confirmScaffold: ({ stack, fileCount }) => - confirm({ - message: `About to create ${fileCount} file${fileCount === 1 ? '' : 's'} for the "${stack}" plan. If your setup commands include a project scaffolder (e.g. create-next-app, create-vite), run those FIRST — this will not overwrite existing files but it may interfere with a fresh scaffolder run. Proceed?`, - default: false, - }), -}; - function placeholderFor(path, purpose) { const ext = path.slice(path.lastIndexOf('.')).toLowerCase(); const note = purpose ? ` ${purpose}` : ''; @@ -72,8 +64,6 @@ export default async function scaffoldCommand(args = [], deps = {}) { const cwd = deps.cwd ?? process.cwd(); const log = deps.log ?? ((msg) => console.error(msg)); const loadConfig = deps.loadConfig ?? defaultLoadConfig; - const isInteractive = deps.isInteractive ?? defaultIsInteractive; - const prompts = { ...DEFAULT_PROMPTS, ...(deps.prompts ?? {}) }; let parsed; try { @@ -88,7 +78,7 @@ export default async function scaffoldCommand(args = [], deps = {}) { cause: err, }); } - const skipConfirm = Boolean(parsed.values.yes); + const confirmed = Boolean(parsed.values.yes); const draftwiseDir = await requireDraftwiseDir(cwd); @@ -103,10 +93,16 @@ export default async function scaffoldCommand(args = [], deps = {}) { return; } + if (!confirmed) { + throw new Error( + 'draftwise scaffold needs --yes to confirm before writing files. Run again with --yes once you are ready (and after running any project scaffolder like create-next-app first).', + ); + } + const scaffoldPath = join(draftwiseDir, 'scaffold.json'); if (!(await pathExists(scaffoldPath))) { throw new Error( - '.draftwise/scaffold.json not found. This file is generated by `draftwise init` in greenfield mode. If you ran init in agent mode, ask your coding agent to write scaffold.json from the conversation, or write it manually.', + '.draftwise/scaffold.json not found. Greenfield init\'s handoff tells your coding agent to write this from the stack-selection conversation; if it\'s missing, ask your agent to write it (or write it manually).', ); } @@ -136,25 +132,6 @@ export default async function scaffoldCommand(args = [], deps = {}) { } log(''); - let proceed; - if (skipConfirm) { - proceed = true; - } else if (isInteractive()) { - proceed = await prompts.confirmScaffold({ - stack: plan.stack ?? 'project', - fileCount: initialFiles.length, - }); - } else { - throw new Error( - 'draftwise scaffold needs confirmation before writing files. Pass --yes to confirm, or run in an interactive terminal.', - ); - } - if (!proceed) { - log('Aborted. No files were written.'); - return; - } - - log(''); let created = 0; let skipped = 0; let blocked = 0; diff --git a/src/commands/tasks.js b/src/commands/tasks.js index 64fdcb8..23c827b 100644 --- a/src/commands/tasks.js +++ b/src/commands/tasks.js @@ -1,13 +1,11 @@ import { readFile } from 'node:fs/promises'; import { parseArgs } from 'node:util'; -import { select } from '@inquirer/prompts'; import { cachedScan as defaultScan } from '../utils/scan-cache.js'; import { loadConfig as defaultLoadConfig } from '../utils/config.js'; import { listSpecs as defaultListSpecs } from '../utils/specs.js'; import { readOverview as defaultReadOverview } from '../utils/overview.js'; import { requireDraftwiseDir } from '../utils/draftwise-dir.js'; import { loadScanContext } from '../utils/scan-context.js'; -import { isInteractive as defaultIsInteractive } from '../utils/tty.js'; import { AGENT_HANDOFF_PREFIX } from '../utils/agent-handoff.js'; import { buildAgentInstruction } from '../ai/prompts/tasks.js'; @@ -23,24 +21,12 @@ agent, which writes tasks.md (numbered tasks with Goal / Files / Depends on / Parallel with / Acceptance). In greenfield, the first 1-3 tasks are project scaffolding (run setup commands, install deps). -Non-TTY (CI, coding-agent shell): when multiple technical specs -exist and no is supplied, the command errors with -the available slugs instead of running the picker. +When multiple technical specs exist and no is +supplied, the command errors with the available slugs. `; const ARG_OPTIONS = {}; -const DEFAULT_PROMPTS = { - pickSpec: ({ specs }) => - select({ - message: 'Which feature do you want a task breakdown for?', - choices: specs.map((s) => ({ - name: s.hasTasks ? `${s.slug} (tasks.md exists)` : s.slug, - value: s.slug, - })), - }), -}; - export default async function tasksCommand(args = [], deps = {}) { const cwd = deps.cwd ?? process.cwd(); const log = deps.log ?? ((msg) => console.error(msg)); @@ -48,8 +34,6 @@ export default async function tasksCommand(args = [], deps = {}) { const loadConfig = deps.loadConfig ?? defaultLoadConfig; const listSpecs = deps.listSpecs ?? defaultListSpecs; const readOverview = deps.readOverview ?? defaultReadOverview; - const isInteractive = deps.isInteractive ?? defaultIsInteractive; - const prompts = { ...DEFAULT_PROMPTS, ...(deps.prompts ?? {}) }; await requireDraftwiseDir(cwd); @@ -90,9 +74,6 @@ export default async function tasksCommand(args = [], deps = {}) { } else if (specs.length === 1) { target = specs[0]; log(`Using the only technical spec: ${target.slug}`); - } else if (isInteractive()) { - const slug = await prompts.pickSpec({ specs }); - target = specs.find((s) => s.slug === slug); } else { const available = specs.map((s) => s.slug).join(', '); throw new Error( diff --git a/src/commands/tech.js b/src/commands/tech.js index 331dadf..3038a78 100644 --- a/src/commands/tech.js +++ b/src/commands/tech.js @@ -1,13 +1,11 @@ import { readFile } from 'node:fs/promises'; import { parseArgs } from 'node:util'; -import { select } from '@inquirer/prompts'; import { cachedScan as defaultScan } from '../utils/scan-cache.js'; import { loadConfig as defaultLoadConfig } from '../utils/config.js'; import { listSpecs as defaultListSpecs } from '../utils/specs.js'; import { readOverview as defaultReadOverview } from '../utils/overview.js'; import { requireDraftwiseDir } from '../utils/draftwise-dir.js'; import { loadScanContext } from '../utils/scan-context.js'; -import { isInteractive as defaultIsInteractive } from '../utils/tty.js'; import { AGENT_HANDOFF_PREFIX } from '../utils/agent-handoff.js'; import { buildAgentInstruction } from '../ai/prompts/tech.js'; @@ -16,33 +14,18 @@ export const HELP = `draftwise tech [] — draft technical-spec.md from Usage: draftwise tech # auto-pick if exactly one product spec exists draftwise tech # target a specific feature - draftwise tech # multiple specs → picks via inquirer (TTY only) Reads the product spec, prints it plus scanner data (brownfield) or the project plan (greenfield) and an instruction for your coding agent, which writes technical-spec.md grounded in real code or marked "(new)" for greenfield. -Non-TTY (CI, coding-agent shell): when multiple product specs exist -and no is supplied, the command errors with the -available slugs instead of running the picker. +When multiple product specs exist and no is supplied, +the command errors with the available slugs. `; const ARG_OPTIONS = {}; -const DEFAULT_PROMPTS = { - pickSpec: ({ specs }) => - select({ - message: 'Which feature spec do you want a technical spec for?', - choices: specs.map((s) => ({ - name: s.hasTechnicalSpec - ? `${s.slug} (technical-spec.md exists)` - : s.slug, - value: s.slug, - })), - }), -}; - export default async function techCommand(args = [], deps = {}) { const cwd = deps.cwd ?? process.cwd(); const log = deps.log ?? ((msg) => console.error(msg)); @@ -50,8 +33,6 @@ export default async function techCommand(args = [], deps = {}) { const loadConfig = deps.loadConfig ?? defaultLoadConfig; const listSpecs = deps.listSpecs ?? defaultListSpecs; const readOverview = deps.readOverview ?? defaultReadOverview; - const isInteractive = deps.isInteractive ?? defaultIsInteractive; - const prompts = { ...DEFAULT_PROMPTS, ...(deps.prompts ?? {}) }; await requireDraftwiseDir(cwd); @@ -92,9 +73,6 @@ export default async function techCommand(args = [], deps = {}) { } else if (specs.length === 1) { target = specs[0]; log(`Using the only product spec: ${target.slug}`); - } else if (isInteractive()) { - const slug = await prompts.pickSpec({ specs }); - target = specs.find((s) => s.slug === slug); } else { const available = specs.map((s) => s.slug).join(', '); throw new Error( diff --git a/src/utils/tty.js b/src/utils/tty.js deleted file mode 100644 index 56e6b13..0000000 --- a/src/utils/tty.js +++ /dev/null @@ -1,9 +0,0 @@ -// Returns true when running in an interactive shell where inquirer prompts can -// fire safely. False inside a coding-agent shell (Claude Code, Cursor) or CI, -// where stdin isn't a TTY and inquirer would either hang waiting for input or -// throw a force-exit error. Commands check this before falling back to a -// prompt for a missing flag value — non-TTY callers must supply every value -// via flags. -export function isInteractive() { - return Boolean(process.stdin.isTTY); -} diff --git a/test/commands/init.test.js b/test/commands/init.test.js index 9af2b74..142995b 100644 --- a/test/commands/init.test.js +++ b/test/commands/init.test.js @@ -8,9 +8,6 @@ function fakeScan(files) { return async (root) => ({ root, files }); } -const interactiveTrue = () => true; -const interactiveFalse = () => false; - const detectBrownfield = async () => 'brownfield'; describe('draftwise init', () => { @@ -31,8 +28,6 @@ describe('draftwise init', () => { init([], { cwd: dir, log: () => {}, - prompts: { promptIdea: async () => 'unused' }, - isInteractive: interactiveTrue, scan: fakeScan(['src/foo.js']), }), ).rejects.toThrow(/already exists/); @@ -43,7 +38,6 @@ describe('draftwise init', () => { await init([], { cwd: dir, log: () => {}, - isInteractive: interactiveTrue, detectProjectState: detectBrownfield, scan: fakeScan(['src/foo.js', 'src/bar.ts']), }); @@ -65,12 +59,26 @@ describe('draftwise init', () => { expect(gitignore).toContain('.cache/'); }); + it('runs end-to-end with no flags (no questions to ask)', async () => { + await init([], { + cwd: dir, + log: () => {}, + detectProjectState: detectBrownfield, + scan: fakeScan(['src/foo.js']), + }); + + const config = await readFile( + join(dir, '.draftwise', 'config.yaml'), + 'utf8', + ); + expect(config).toContain('state: brownfield'); + }); + it('errors when --mode=brownfield is forced but the repo has no source files', async () => { await expect( init(['--mode=brownfield'], { cwd: dir, log: () => {}, - isInteractive: interactiveTrue, scan: fakeScan([]), }), ).rejects.toThrow(/No source files/); @@ -83,7 +91,6 @@ describe('draftwise init', () => { await init(['--mode=greenfield', '--idea=a recipe sharing app for home cooks'], { cwd: dir, log: (m) => logs.push(m), - isInteractive: interactiveTrue, scan: fakeScan([]), }); @@ -110,89 +117,21 @@ describe('draftwise init', () => { expect(config).not.toContain('stack:'); }); - it('prompts for --idea when interactive and not supplied', async () => { - let promptCalls = 0; - await init(['--mode=greenfield'], { - cwd: dir, - log: () => {}, - isInteractive: interactiveTrue, - prompts: { - promptIdea: async () => { - promptCalls++; - return 'a brand new idea'; - }, - }, - scan: fakeScan([]), - }); - - expect(promptCalls).toBe(1); - const overview = await readFile( - join(dir, '.draftwise', 'overview.md'), - 'utf8', - ); - expect(overview).toContain('a brand new idea'); - }); - it('does NOT require source files (empty repo is fine)', async () => { await init(['--mode=greenfield', '--idea=a brand new idea'], { cwd: dir, log: () => {}, - isInteractive: interactiveTrue, scan: fakeScan([]), }); await access(join(dir, '.draftwise', 'overview.md')); }); - }); - - describe('non-TTY (flags-driven)', () => { - function noPrompts() { - const fail = () => { - throw new Error('inquirer prompt fired in non-TTY test'); - }; - return { promptIdea: fail }; - } - - it('brownfield runs end-to-end with no flags (no questions to ask)', async () => { - await init([], { - cwd: dir, - log: () => {}, - isInteractive: interactiveFalse, - prompts: noPrompts(), - detectProjectState: detectBrownfield, - scan: fakeScan(['src/foo.js']), - }); - - const config = await readFile( - join(dir, '.draftwise', 'config.yaml'), - 'utf8', - ); - expect(config).toContain('state: brownfield'); - }); - it('greenfield + --idea writes the placeholder plan', async () => { - await init(['--mode=greenfield', '--idea=A new idea'], { - cwd: dir, - log: () => {}, - isInteractive: interactiveFalse, - prompts: noPrompts(), - scan: fakeScan([]), - }); - - const overview = await readFile( - join(dir, '.draftwise', 'overview.md'), - 'utf8', - ); - expect(overview).toContain('A new idea'); - }); - - it('prints the structured handoff when greenfield + non-TTY + no --idea', async () => { + it('prints the structured handoff when greenfield without --idea', async () => { const logs = []; await init([], { cwd: dir, log: (m) => logs.push(m), - isInteractive: interactiveFalse, - prompts: noPrompts(), scan: fakeScan([]), }); @@ -209,15 +148,15 @@ describe('draftwise init', () => { readFile(join(dir, '.draftwise', 'config.yaml'), 'utf8'), ).rejects.toThrow(); }); + }); - it('still throws (does NOT handoff) on a .draftwise/ that already exists', async () => { + describe('argument validation', () => { + it('still throws on a .draftwise/ that already exists', async () => { await mkdir(join(dir, '.draftwise')); await expect( init([], { cwd: dir, log: () => {}, - isInteractive: interactiveFalse, - prompts: noPrompts(), scan: fakeScan(['src/foo.js']), }), ).rejects.toThrow(/already exists/); @@ -228,8 +167,6 @@ describe('draftwise init', () => { init(['--mode=halffield'], { cwd: dir, log: () => {}, - isInteractive: interactiveFalse, - prompts: noPrompts(), scan: fakeScan([]), }), ).rejects.toThrow(/Invalid --mode value/); @@ -240,8 +177,6 @@ describe('draftwise init', () => { init(['--mode=brownfield', '--bogus=yes'], { cwd: dir, log: () => {}, - isInteractive: interactiveFalse, - prompts: noPrompts(), scan: fakeScan(['src/foo.js']), }), ).rejects.toThrow(/Invalid arguments to draftwise init/); @@ -256,7 +191,6 @@ describe('draftwise init', () => { await init([], { cwd: dir, log: (m) => logs.push(m), - isInteractive: interactiveTrue, scan: fakeScan(['index.ts']), }); @@ -275,7 +209,6 @@ describe('draftwise init', () => { await init(['--idea=a thing'], { cwd: dir, log: (m) => logs.push(m), - isInteractive: interactiveTrue, scan: fakeScan([]), }); @@ -296,7 +229,6 @@ describe('draftwise init', () => { await init(['--mode=greenfield', '--idea=a thing'], { cwd: dir, log: (m) => logs.push(m), - isInteractive: interactiveTrue, scan: fakeScan(['app.py']), }); diff --git a/test/commands/scaffold.test.js b/test/commands/scaffold.test.js index 9bc58f6..af02320 100644 --- a/test/commands/scaffold.test.js +++ b/test/commands/scaffold.test.js @@ -41,10 +41,6 @@ async function seedScaffold(dir, plan = SAMPLE_PLAN) { ); } -function fakePrompts(answer) { - return { confirmScaffold: async () => answer }; -} - const greenfieldConfig = async () => ({ projectState: 'greenfield' }); const brownfieldConfig = async () => ({ projectState: 'brownfield' }); @@ -67,10 +63,23 @@ describe('draftwise scaffold', () => { ).rejects.toThrow(/Run `draftwise init` first/); }); + it('errors without --yes', async () => { + await seedScaffold(dir); + await expect( + scaffoldCommand([], { + cwd: dir, + log: () => {}, + loadConfig: greenfieldConfig, + }), + ).rejects.toThrow(/needs --yes to confirm/); + // No file writes happened. + expect(await pathExists(join(dir, 'app/page.tsx'))).toBe(false); + }); + it('errors if scaffold.json is missing', async () => { await mkdir(join(dir, '.draftwise')); await expect( - scaffoldCommand([], { + scaffoldCommand(['--yes'], { cwd: dir, log: () => {}, loadConfig: greenfieldConfig, @@ -82,7 +91,7 @@ describe('draftwise scaffold', () => { // Seed a scaffold.json so a "missing-file" code path can't possibly fire — // we want to confirm the brownfield check happens BEFORE the file check. await seedScaffold(dir); - await scaffoldCommand([], { + await scaffoldCommand(['--yes'], { cwd: dir, log: (m) => logs.push(m), loadConfig: brownfieldConfig, @@ -96,7 +105,7 @@ describe('draftwise scaffold', () => { await mkdir(join(dir, '.draftwise')); await writeFile(join(dir, '.draftwise', 'scaffold.json'), '{not json', 'utf8'); await expect( - scaffoldCommand([], { + scaffoldCommand(['--yes'], { cwd: dir, log: () => {}, loadConfig: greenfieldConfig, @@ -106,34 +115,20 @@ describe('draftwise scaffold', () => { it('does nothing when initial_files is empty', async () => { await seedScaffold(dir, { ...SAMPLE_PLAN, initial_files: [] }); - await scaffoldCommand([], { + await scaffoldCommand(['--yes'], { cwd: dir, log: (m) => logs.push(m), loadConfig: greenfieldConfig, - prompts: fakePrompts(true), }); expect(logs.join('\n')).toContain('Nothing to do'); }); - it('aborts cleanly when the user declines confirmation', async () => { - await seedScaffold(dir); - await scaffoldCommand([], { - cwd: dir, - log: (m) => logs.push(m), - loadConfig: greenfieldConfig, - prompts: fakePrompts(false), - }); - expect(logs.join('\n')).toContain('Aborted'); - expect(await pathExists(join(dir, 'app/page.tsx'))).toBe(false); - }); - it('creates each initial file with placeholder content and prints setup commands', async () => { await seedScaffold(dir); - await scaffoldCommand([], { + await scaffoldCommand(['--yes'], { cwd: dir, log: (m) => logs.push(m), loadConfig: greenfieldConfig, - prompts: fakePrompts(true), }); expect(await pathExists(join(dir, 'app/page.tsx'))).toBe(true); @@ -163,11 +158,10 @@ describe('draftwise scaffold', () => { ], }); - await scaffoldCommand([], { + await scaffoldCommand(['--yes'], { cwd: dir, log: (m) => logs.push(m), loadConfig: greenfieldConfig, - prompts: fakePrompts(true), }); const out = logs.join('\n'); @@ -187,54 +181,14 @@ describe('draftwise scaffold', () => { await mkdir(join(dir, 'app'), { recursive: true }); await writeFile(join(dir, 'app/page.tsx'), 'existing content', 'utf8'); - await scaffoldCommand([], { + await scaffoldCommand(['--yes'], { cwd: dir, log: (m) => logs.push(m), loadConfig: greenfieldConfig, - prompts: fakePrompts(true), }); const tsx = await readFile(join(dir, 'app/page.tsx'), 'utf8'); expect(tsx).toBe('existing content'); expect(logs.join('\n')).toContain('skipped (exists): app/page.tsx'); }); - - describe('non-TTY (flags-driven)', () => { - function noPrompts() { - return { - confirmScaffold: () => { - throw new Error('inquirer prompt fired in non-TTY test'); - }, - }; - } - - it('--yes runs without prompting in non-TTY', async () => { - await seedScaffold(dir); - - await scaffoldCommand(['--yes'], { - cwd: dir, - log: () => {}, - isInteractive: () => false, - loadConfig: greenfieldConfig, - prompts: noPrompts(), - }); - - const tsx = await readFile(join(dir, 'app/page.tsx'), 'utf8'); - expect(tsx).toContain('TODO'); - }); - - it('errors in non-TTY without --yes', async () => { - await seedScaffold(dir); - - await expect( - scaffoldCommand([], { - cwd: dir, - log: () => {}, - isInteractive: () => false, - loadConfig: greenfieldConfig, - prompts: noPrompts(), - }), - ).rejects.toThrow(/Pass --yes to confirm/); - }); - }); }); diff --git a/test/commands/tasks.test.js b/test/commands/tasks.test.js index 0f6ac69..654edf4 100644 --- a/test/commands/tasks.test.js +++ b/test/commands/tasks.test.js @@ -182,30 +182,19 @@ describe('draftwise tasks', () => { expect(out).toContain('Plan'); }); - describe('non-TTY (flags-driven)', () => { - function noPrompts() { - const fail = () => { - throw new Error('inquirer prompt fired in non-TTY test'); - }; - return { pickSpec: fail }; - } - - it('errors when multiple specs exist and no slug arg, instead of prompting', async () => { - await seedSpec(dir, 'collab-albums', { technical: '# T1' }); - await seedSpec(dir, 'photo-uploads', { technical: '# T2' }); - - await expect( - tasksCommand([], { - cwd: dir, - log: () => {}, - isInteractive: () => false, - prompts: noPrompts(), - scan: async () => SAMPLE_SCAN, - loadConfig: async () => ({ projectState: 'brownfield' }), - }), - ).rejects.toThrow( - /Multiple technical specs.*Available:.*collab-albums/, - ); - }); + it('errors when there are multiple specs and no slug arg', async () => { + await seedSpec(dir, 'collab-albums', { technical: '# T1' }); + await seedSpec(dir, 'photo-uploads', { technical: '# T2' }); + + await expect( + tasksCommand([], { + cwd: dir, + log: () => {}, + scan: async () => SAMPLE_SCAN, + loadConfig: async () => ({ projectState: 'brownfield' }), + }), + ).rejects.toThrow( + /Multiple technical specs.*Available:.*collab-albums/, + ); }); }); diff --git a/test/commands/tech.test.js b/test/commands/tech.test.js index fec5109..c55a24e 100644 --- a/test/commands/tech.test.js +++ b/test/commands/tech.test.js @@ -98,19 +98,18 @@ describe('draftwise tech', () => { ).rejects.toThrow(/No product spec found for "ghost"/); }); - it('prompts the user when there are multiple specs and no slug arg', async () => { + it('errors when there are multiple specs and no slug arg', async () => { await seedSpec(dir, 'alpha'); await seedSpec(dir, 'beta'); - await techCommand([], { - cwd: dir, - log: (m) => logs.push(m), - scan: async () => SAMPLE_SCAN, - loadConfig: async () => ({ projectState: 'brownfield' }), - prompts: { pickSpec: async () => 'beta' }, - }); - - expect(logs.join('\n')).toContain('SPEC: beta'); + await expect( + techCommand([], { + cwd: dir, + log: (m) => logs.push(m), + scan: async () => SAMPLE_SCAN, + loadConfig: async () => ({ projectState: 'brownfield' }), + }), + ).rejects.toThrow(/Multiple product specs.*Available:.*alpha/); }); it('dumps product spec + scanner + instruction without writing', async () => { @@ -170,29 +169,4 @@ describe('draftwise tech', () => { expect(out).not.toContain('SCANNER OUTPUT'); expect(out).toContain('Plan'); }); - - describe('non-TTY (flags-driven)', () => { - function noPrompts() { - const fail = () => { - throw new Error('inquirer prompt fired in non-TTY test'); - }; - return { pickSpec: fail }; - } - - it('errors when multiple specs exist and no slug arg, instead of prompting', async () => { - await seedSpec(dir, 'collab-albums', '# A'); - await seedSpec(dir, 'photo-uploads', '# B'); - - await expect( - techCommand([], { - cwd: dir, - log: () => {}, - isInteractive: () => false, - prompts: noPrompts(), - scan: async () => SAMPLE_SCAN, - loadConfig: async () => ({ projectState: 'brownfield' }), - }), - ).rejects.toThrow(/Multiple product specs.*Available:.*collab-albums/); - }); - }); }); diff --git a/test/integration/pipeline.test.js b/test/integration/pipeline.test.js index e022c0c..61de637 100644 --- a/test/integration/pipeline.test.js +++ b/test/integration/pipeline.test.js @@ -78,7 +78,7 @@ describe('integration: pipeline (brownfield)', () => { } it('init creates the .draftwise/ skeleton and a parseable config', async () => { - await init([], { cwd: dir, log, isInteractive: () => false }); + await init([], { cwd: dir, log }); expect(await exists(join(dir, '.draftwise'))).toBe(true); expect(await exists(join(dir, '.draftwise', 'specs'))).toBe(true); @@ -95,7 +95,7 @@ describe('integration: pipeline (brownfield)', () => { }); it('scan, explain, new — all reach correct agent-mode output', async () => { - await init([], { cwd: dir, log, isInteractive: () => false }); + await init([], { cwd: dir, log }); // scan dumps scanner output + instruction. logs.length = 0; @@ -124,7 +124,7 @@ describe('integration: pipeline (brownfield)', () => { }); it('list and show find specs that the host agent would have written', async () => { - await init([], { cwd: dir, log, isInteractive: () => false }); + await init([], { cwd: dir, log }); // Agent doesn't write specs from inside draftwise — the host coding // agent does. Simulate that step by seeding spec files at the same @@ -172,7 +172,7 @@ describe('integration: pipeline (brownfield)', () => { }); it('show errors gracefully when the requested type is missing', async () => { - await init([], { cwd: dir, log, isInteractive: () => false }); + await init([], { cwd: dir, log }); const featureDir = join(dir, '.draftwise', 'specs', 'half-baked'); await mkdir(featureDir, { recursive: true }); @@ -208,7 +208,7 @@ describe('integration: pipeline (greenfield)', () => { it('init greenfield → scan / explain short-circuit cleanly', async () => { await init( ['--mode=greenfield', '--idea=a recipe sharing app for home cooks'], - { cwd: dir, log, isInteractive: () => false }, + { cwd: dir, log }, ); expect(await exists(join(dir, '.draftwise', 'config.yaml'))).toBe(true); diff --git a/test/setup.js b/test/setup.js deleted file mode 100644 index 78e8f02..0000000 --- a/test/setup.js +++ /dev/null @@ -1,10 +0,0 @@ -// Tests run in vitest, where stdin is not a TTY by default. The new flags-first -// architecture treats non-TTY as "no inquirer fallback," which would break every -// existing test that injects `prompts` and expects them to fire. -// -// Setting process.stdin.isTTY = true here makes the default isInteractive() -// return true under test, restoring the previous behavior where injected prompts -// drive the conversation. Tests that want to exercise the non-TTY code path -// (errors when required flags are missing, decline-all opportunities, etc.) -// override per-test via `deps.isInteractive: () => false`. -process.stdin.isTTY = true; diff --git a/vitest.config.js b/vitest.config.js deleted file mode 100644 index 62b4cc6..0000000 --- a/vitest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - setupFiles: ['./test/setup.js'], - }, -});