From 31346710c6de8ea80320ce834977e1fe451f0317 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 03:20:10 +0000 Subject: [PATCH 1/5] Initial plan From 3182395a46b2d906de2882a35d9601c8825b8a41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 03:28:43 +0000 Subject: [PATCH 2/5] Add create-next-shadcn command with tests Co-authored-by: ericelliott <364727+ericelliott@users.noreply.github.com> --- bin/aidd.js | 31 ++++++- bin/create-next-shadcn-e2e.test.js | 53 +++++++++++ lib/create-next-shadcn.js | 38 ++++++++ lib/create-next-shadcn.test.js | 136 +++++++++++++++++++++++++++++ 4 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 bin/create-next-shadcn-e2e.test.js create mode 100644 lib/create-next-shadcn.js create mode 100644 lib/create-next-shadcn.test.js diff --git a/bin/aidd.js b/bin/aidd.js index 859801d..1e74d0d 100755 --- a/bin/aidd.js +++ b/bin/aidd.js @@ -3,6 +3,7 @@ import { Command } from "commander"; import { executeClone } from "../lib/cli-core.js"; import { generateAllIndexes } from "../lib/index-generator.js"; +import { executeCreateNextShadcn } from "../lib/create-next-shadcn.js"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import path from "path"; @@ -35,7 +36,7 @@ const [, handleCliErrors] = errorCauses({ const createCli = () => { const program = new Command(); - return program + program .name("aidd") .description("AI Driven Development - Install the AIDD Framework") .version(packageJson.version) @@ -104,6 +105,10 @@ To install for Cursor: Install without Cursor integration: npx aidd my-project + +Scaffold a new Next.js + shadcn app (requires Claude Code CLI): + + npx aidd create-next-shadcn `, ) .addHelpText( @@ -205,6 +210,30 @@ https://paralleldrive.com process.exit(result.success ? 0 : 1); }, ); + + // Add create-next-shadcn command + program + .command("create-next-shadcn") + .description( + "Scaffold a new Next.js app with AIDD, shadcn, and baseline tests using Claude Code", + ) + .action(async () => { + console.log( + chalk.blue("๐Ÿš€ Creating Next.js + shadcn app with Claude Code...\n"), + ); + + const result = await executeCreateNextShadcn(); + + if (!result.success) { + console.error(chalk.red(`โŒ ${result.error}`)); + process.exit(1); + } + + console.log(chalk.green("\nโœ… Project scaffolding complete!")); + process.exit(0); + }); + + return program; }; // Execute CLI diff --git a/bin/create-next-shadcn-e2e.test.js b/bin/create-next-shadcn-e2e.test.js new file mode 100644 index 0000000..ea544fd --- /dev/null +++ b/bin/create-next-shadcn-e2e.test.js @@ -0,0 +1,53 @@ +import { assert } from "riteway/vitest"; +import { describe, test } from "vitest"; +import { exec } from "child_process"; +import { promisify } from "util"; +import { fileURLToPath } from "url"; +import path from "path"; + +const execAsync = promisify(exec); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const cliPath = path.join(__dirname, "./aidd.js"); + +describe("CLI create-next-shadcn command", () => { + test("help output includes create-next-shadcn command", async () => { + const { stdout } = await execAsync(`node ${cliPath} --help`); + + assert({ + given: "CLI help command is run", + should: "include create-next-shadcn in Quick Start section", + actual: stdout.includes("create-next-shadcn"), + expected: true, + }); + }); + + test("create-next-shadcn command has help", async () => { + const { stdout } = await execAsync( + `node ${cliPath} create-next-shadcn --help`, + ); + + assert({ + given: "create-next-shadcn help is requested", + should: "show command description", + actual: + stdout.includes("create-next-shadcn") && + stdout.includes("Next.js") && + stdout.includes("shadcn"), + expected: true, + }); + }); + + test("create-next-shadcn mentions Claude Code requirement", async () => { + const { stdout } = await execAsync( + `node ${cliPath} create-next-shadcn --help`, + ); + + assert({ + given: "create-next-shadcn help is requested", + should: "mention Claude Code requirement", + actual: stdout.includes("Claude Code") || stdout.includes("Claude"), + expected: true, + }); + }); +}); diff --git a/lib/create-next-shadcn.js b/lib/create-next-shadcn.js new file mode 100644 index 0000000..ec6aa25 --- /dev/null +++ b/lib/create-next-shadcn.js @@ -0,0 +1,38 @@ +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); + +const PROMPT_URL = + "https://raw.githubusercontent.com/paralleldrive/aidd/main/docs/new-project-setup-nextjs-shadcn.md"; + +/** + * Execute the create-next-shadcn command by piping instructions to Claude CLI + * @returns {Promise<{success: boolean, error?: string}>} + */ +export const executeCreateNextShadcn = async () => { + try { + // Check if claude CLI is installed + await execAsync("which claude"); + } catch { + return { + success: false, + error: + "Claude Code CLI is not installed. Please install it first: https://docs.claude.ai/cli", + }; + } + + try { + // Execute claude with piped prompt instructions + const command = `{ echo "Follow these instructions exactly. Stop when complete." && echo && curl -fsSL "${PROMPT_URL}"; } | claude`; + + await execAsync(command); + + return { success: true }; + } catch (error) { + return { + success: false, + error: `Failed to execute claude: ${error.message}`, + }; + } +}; diff --git a/lib/create-next-shadcn.test.js b/lib/create-next-shadcn.test.js new file mode 100644 index 0000000..98a3391 --- /dev/null +++ b/lib/create-next-shadcn.test.js @@ -0,0 +1,136 @@ +import { assert } from "riteway/vitest"; +import { describe, test, vi, beforeEach, afterEach } from "vitest"; +import * as childProcess from "child_process"; +import * as util from "util"; + +// Mock child_process before importing the module +vi.mock("child_process"); +vi.mock("util"); + +describe("executeCreateNextShadcn", () => { + let execAsyncMock; + + beforeEach(async () => { + execAsyncMock = vi.fn(); + vi.mocked(util.promisify).mockReturnValue(execAsyncMock); + }); + + afterEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + }); + + test("should check for claude CLI", async () => { + const called = []; + execAsyncMock.mockImplementation((cmd) => { + called.push(cmd); + if (cmd === "which claude") { + return Promise.resolve({ + stdout: "/usr/local/bin/claude\n", + stderr: "", + }); + } + return Promise.resolve({ stdout: "OK", stderr: "" }); + }); + + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); + await executeCreateNextShadcn(); + + assert({ + given: "executeCreateNextShadcn is called", + should: "check if claude CLI exists", + actual: called.some((cmd) => cmd === "which claude"), + expected: true, + }); + }); + + test("should return error when claude CLI is not found", async () => { + execAsyncMock.mockImplementation((cmd) => { + if (cmd === "which claude") { + return Promise.reject(new Error("not found")); + } + return Promise.resolve({ stdout: "OK", stderr: "" }); + }); + + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); + const result = await executeCreateNextShadcn(); + + assert({ + given: "claude CLI is not installed", + should: "return error with installation instructions", + actual: result.success, + expected: false, + }); + + assert({ + given: "claude CLI is not installed", + should: "include error message about claude installation", + actual: result.error?.includes("Claude Code CLI"), + expected: true, + }); + }); + + test("should execute claude with correct prompt URL", async () => { + const called = []; + execAsyncMock.mockImplementation((cmd) => { + called.push(cmd); + return Promise.resolve({ stdout: "OK", stderr: "" }); + }); + + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); + await executeCreateNextShadcn(); + + const claudeCommand = called.find((cmd) => cmd.includes("| claude")); + + assert({ + given: "claude CLI exists", + should: "execute claude command", + actual: claudeCommand !== undefined, + expected: true, + }); + }); + + test("should pipe prompt instructions to claude", async () => { + const called = []; + execAsyncMock.mockImplementation((cmd) => { + called.push(cmd); + return Promise.resolve({ stdout: "OK", stderr: "" }); + }); + + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); + await executeCreateNextShadcn(); + + const claudeCommand = called.find((cmd) => cmd.includes("| claude")); + + assert({ + given: "claude command is executed", + should: "include prompt instructions", + actual: + claudeCommand?.includes("Follow these instructions exactly") || + claudeCommand?.includes("echo"), + expected: true, + }); + }); + + test("should include GitHub prompt URL", async () => { + const called = []; + execAsyncMock.mockImplementation((cmd) => { + called.push(cmd); + return Promise.resolve({ stdout: "OK", stderr: "" }); + }); + + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); + await executeCreateNextShadcn(); + + const claudeCommand = called.find((cmd) => cmd.includes("| claude")); + + assert({ + given: "claude command is executed", + should: "reference the GitHub prompt URL", + actual: + claudeCommand?.includes("new-project-setup-nextjs-shadcn.md") || + claudeCommand?.includes("curl"), + expected: true, + }); + }); +}); From 7fd25baeeb399191be41a9f650592ee39b130f50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 03:31:11 +0000 Subject: [PATCH 3/5] Improve create-next-shadcn to use spawn with inherited stdio Co-authored-by: ericelliott <364727+ericelliott@users.noreply.github.com> --- lib/create-next-shadcn.js | 30 ++++++++++-- lib/create-next-shadcn.test.js | 89 ++++++++++++++++++++++++++-------- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/lib/create-next-shadcn.js b/lib/create-next-shadcn.js index ec6aa25..3240534 100644 --- a/lib/create-next-shadcn.js +++ b/lib/create-next-shadcn.js @@ -1,5 +1,6 @@ import { exec } from "child_process"; import { promisify } from "util"; +import { spawn } from "child_process"; const execAsync = promisify(exec); @@ -23,12 +24,33 @@ export const executeCreateNextShadcn = async () => { } try { - // Execute claude with piped prompt instructions - const command = `{ echo "Follow these instructions exactly. Stop when complete." && echo && curl -fsSL "${PROMPT_URL}"; } | claude`; + // Execute claude with piped prompt instructions using spawn for better output handling + return new Promise((resolve, reject) => { + const command = `{ echo "Follow these instructions exactly. Stop when complete." && echo && curl -fsSL "${PROMPT_URL}"; } | claude`; - await execAsync(command); + const child = spawn("bash", ["-c", command], { + stdio: "inherit", // Inherit stdio so user sees Claude's output + shell: true, + }); - return { success: true }; + child.on("close", (code) => { + if (code === 0) { + resolve({ success: true }); + } else { + resolve({ + success: false, + error: `Claude CLI exited with code ${code}`, + }); + } + }); + + child.on("error", (error) => { + resolve({ + success: false, + error: `Failed to execute claude: ${error.message}`, + }); + }); + }); } catch (error) { return { success: false, diff --git a/lib/create-next-shadcn.test.js b/lib/create-next-shadcn.test.js index 98a3391..3ed88df 100644 --- a/lib/create-next-shadcn.test.js +++ b/lib/create-next-shadcn.test.js @@ -9,10 +9,13 @@ vi.mock("util"); describe("executeCreateNextShadcn", () => { let execAsyncMock; + let spawnMock; beforeEach(async () => { execAsyncMock = vi.fn(); + spawnMock = vi.fn(); vi.mocked(util.promisify).mockReturnValue(execAsyncMock); + vi.mocked(childProcess.spawn).mockImplementation(spawnMock); }); afterEach(() => { @@ -33,6 +36,15 @@ describe("executeCreateNextShadcn", () => { return Promise.resolve({ stdout: "OK", stderr: "" }); }); + // Mock spawn to immediately call close with success + spawnMock.mockReturnValue({ + on: (event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 0); + } + }, + }); + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); await executeCreateNextShadcn(); @@ -71,65 +83,100 @@ describe("executeCreateNextShadcn", () => { }); test("should execute claude with correct prompt URL", async () => { - const called = []; - execAsyncMock.mockImplementation((cmd) => { - called.push(cmd); - return Promise.resolve({ stdout: "OK", stderr: "" }); + const spawnArgs = []; + execAsyncMock.mockResolvedValue({ + stdout: "/usr/local/bin/claude\n", + stderr: "", + }); + + spawnMock.mockImplementation((cmd, args) => { + spawnArgs.push({ cmd, args }); + return { + on: (event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 0); + } + }, + }; }); const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); await executeCreateNextShadcn(); - const claudeCommand = called.find((cmd) => cmd.includes("| claude")); + const bashCommand = spawnArgs.find((arg) => arg.cmd === "bash"); assert({ given: "claude CLI exists", - should: "execute claude command", - actual: claudeCommand !== undefined, + should: "execute bash command", + actual: bashCommand !== undefined, expected: true, }); }); test("should pipe prompt instructions to claude", async () => { - const called = []; - execAsyncMock.mockImplementation((cmd) => { - called.push(cmd); - return Promise.resolve({ stdout: "OK", stderr: "" }); + const spawnArgs = []; + execAsyncMock.mockResolvedValue({ + stdout: "/usr/local/bin/claude\n", + stderr: "", + }); + + spawnMock.mockImplementation((cmd, args) => { + spawnArgs.push({ cmd, args }); + return { + on: (event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 0); + } + }, + }; }); const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); await executeCreateNextShadcn(); - const claudeCommand = called.find((cmd) => cmd.includes("| claude")); + const bashCommand = spawnArgs.find((arg) => arg.cmd === "bash"); + const commandString = bashCommand?.args?.join(" "); assert({ given: "claude command is executed", should: "include prompt instructions", actual: - claudeCommand?.includes("Follow these instructions exactly") || - claudeCommand?.includes("echo"), + commandString?.includes("Follow these instructions exactly") || + commandString?.includes("echo"), expected: true, }); }); test("should include GitHub prompt URL", async () => { - const called = []; - execAsyncMock.mockImplementation((cmd) => { - called.push(cmd); - return Promise.resolve({ stdout: "OK", stderr: "" }); + const spawnArgs = []; + execAsyncMock.mockResolvedValue({ + stdout: "/usr/local/bin/claude\n", + stderr: "", + }); + + spawnMock.mockImplementation((cmd, args) => { + spawnArgs.push({ cmd, args }); + return { + on: (event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 0); + } + }, + }; }); const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); await executeCreateNextShadcn(); - const claudeCommand = called.find((cmd) => cmd.includes("| claude")); + const bashCommand = spawnArgs.find((arg) => arg.cmd === "bash"); + const commandString = bashCommand?.args?.join(" "); assert({ given: "claude command is executed", should: "reference the GitHub prompt URL", actual: - claudeCommand?.includes("new-project-setup-nextjs-shadcn.md") || - claudeCommand?.includes("curl"), + commandString?.includes("new-project-setup-nextjs-shadcn.md") || + commandString?.includes("curl"), expected: true, }); }); From 07b47ff53680d0de93fbad60ebd4c1bfb38eace5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 00:48:52 +0000 Subject: [PATCH 4/5] Refactor: CLI automation + separate /scaffold command Co-authored-by: ericelliott <364727+ericelliott@users.noreply.github.com> --- ai/commands/index.md | 6 + ai/commands/scaffold.md | 98 +++++++++++++ bin/aidd.js | 19 +-- bin/create-next-shadcn-e2e.test.js | 10 +- lib/create-next-shadcn.js | 221 +++++++++++++++++++++++------ lib/create-next-shadcn.test.js | 200 ++++++++++++-------------- 6 files changed, 388 insertions(+), 166 deletions(-) create mode 100644 ai/commands/scaffold.md diff --git a/ai/commands/index.md b/ai/commands/index.md index f728833..96dd65a 100644 --- a/ai/commands/index.md +++ b/ai/commands/index.md @@ -52,6 +52,12 @@ This index provides an overview of the contents in this directory. *No description available* +### ๐Ÿ—๏ธ Scaffold + +**File:** `scaffold.md` + +*No description available* + ### task **File:** `task.md` diff --git a/ai/commands/scaffold.md b/ai/commands/scaffold.md new file mode 100644 index 0000000..8ef32fc --- /dev/null +++ b/ai/commands/scaffold.md @@ -0,0 +1,98 @@ +--- +description: Scaffold design system and shadcn components after Phase 1 automation +globs: **/app/**,**/components/**,**/stories/** +alwaysApply: false +--- + +# ๐Ÿ—๏ธ Scaffold + +Use this command after running `npx aidd create-next-shadcn` to complete Phase 2 of the setup: adding shadcn UI and building a baseline design system. + +## Prerequisites + +You should have already run: +```bash +npx aidd create-next-shadcn [project-name] +``` + +This command completes Phase 1 (deterministic setup) and gives you a working Next.js app with: +- TypeScript & ESM +- Vitest + Riteway for unit tests +- Playwright for E2E tests +- Storybook +- AIDD framework installed + +## Phase 2: Design System with shadcn + +Now you'll add shadcn UI and build a baseline design system. + +### Step 1: Install shadcn + +First, search for the latest official shadcn installation instructions for Next.js App Router. + +Then follow the current recommended setup. Document the exact commands you run. + +Typically this looks like: +```bash +npx shadcn@latest init +``` + +### Step 2: Build Baseline Design System + +Create a simple design system story that includes: + +**Components to build:** +- Standard button components (variants, sizes, disabled, loading) +- Toggle switch +- Input elements (text, textarea, select if applicable) +- Semantic colors: error, warning, success, primary +- Focus and hover states for all interactive components + +**Location:** +- Place design system stories under `src/stories/design-system` +- Use Storybook CSF format + +### Step 3: Responsive Primary Actions + +Implement primary actions in two responsive styles: +- **Mobile**: centered circular primary action +- **Desktop**: primary action button + +### Step 4: TDD Process + +- Use TDD for any custom components you create +- Follow `ai/rules/tdd.mdc` carefully +- Follow JavaScript best practices in `ai/rules/javascript` + +### Step 5: Style Guidance + +Before starting implementation, prompt the user for guidance on the visual look and feel of the application. + +After they respond, continue with the task epic, then present it for feedback. + +### Step 6: Review + +Use your own `/review` of your changes as the approval gate before moving to the next step in the TDD process. + +## Exit Criteria + +Before considering this complete: + +- [ ] Storybook shows all components and states listed above +- [ ] Visual and interaction a11y basics: keyboard nav, visible focus, acceptable contrast +- [ ] All tests pass (`npm test`) +- [ ] E2E tests pass (`npm run test:e2e`) + +## Final Review + +Once all work is complete, `/review` the code: + +- Reduce duplication +- Ensure the TDD process was carefully adhered to, identify and fix any gaps +- Present findings and ask for advice before any further work + +## References + +- Phase 1 setup details: `docs/new-project-setup-nextjs-shadcn.md` +- JavaScript style guide: `ai/rules/javascript` +- TDD guide: `ai/rules/tdd.mdc` diff --git a/bin/aidd.js b/bin/aidd.js index 1e74d0d..3a6647e 100755 --- a/bin/aidd.js +++ b/bin/aidd.js @@ -106,9 +106,9 @@ Install without Cursor integration: npx aidd my-project -Scaffold a new Next.js + shadcn app (requires Claude Code CLI): +Scaffold a new Next.js app with tests and AIDD: - npx aidd create-next-shadcn + npx aidd create-next-shadcn [project-name] `, ) .addHelpText( @@ -213,23 +213,18 @@ https://paralleldrive.com // Add create-next-shadcn command program - .command("create-next-shadcn") + .command("create-next-shadcn [project-name]") .description( - "Scaffold a new Next.js app with AIDD, shadcn, and baseline tests using Claude Code", + "Scaffold a new Next.js app with AIDD, tests, and baseline setup", ) - .action(async () => { - console.log( - chalk.blue("๐Ÿš€ Creating Next.js + shadcn app with Claude Code...\n"), - ); - - const result = await executeCreateNextShadcn(); + .action(async (projectName = "my-app") => { + const result = await executeCreateNextShadcn(projectName); if (!result.success) { - console.error(chalk.red(`โŒ ${result.error}`)); + console.error(chalk.red(`\nโŒ ${result.error}`)); process.exit(1); } - console.log(chalk.green("\nโœ… Project scaffolding complete!")); process.exit(0); }); diff --git a/bin/create-next-shadcn-e2e.test.js b/bin/create-next-shadcn-e2e.test.js index ea544fd..1341603 100644 --- a/bin/create-next-shadcn-e2e.test.js +++ b/bin/create-next-shadcn-e2e.test.js @@ -31,22 +31,20 @@ describe("CLI create-next-shadcn command", () => { given: "create-next-shadcn help is requested", should: "show command description", actual: - stdout.includes("create-next-shadcn") && - stdout.includes("Next.js") && - stdout.includes("shadcn"), + stdout.includes("create-next-shadcn") && stdout.includes("Next.js"), expected: true, }); }); - test("create-next-shadcn mentions Claude Code requirement", async () => { + test("create-next-shadcn accepts project name argument", async () => { const { stdout } = await execAsync( `node ${cliPath} create-next-shadcn --help`, ); assert({ given: "create-next-shadcn help is requested", - should: "mention Claude Code requirement", - actual: stdout.includes("Claude Code") || stdout.includes("Claude"), + should: "show project-name argument", + actual: stdout.includes("[project-name]"), expected: true, }); }); diff --git a/lib/create-next-shadcn.js b/lib/create-next-shadcn.js index 3240534..c38dbf0 100644 --- a/lib/create-next-shadcn.js +++ b/lib/create-next-shadcn.js @@ -1,60 +1,197 @@ -import { exec } from "child_process"; -import { promisify } from "util"; import { spawn } from "child_process"; +import { promisify } from "util"; +import { exec } from "child_process"; +import fs from "fs-extra"; +import path from "path"; +import process from "process"; const execAsync = promisify(exec); -const PROMPT_URL = - "https://raw.githubusercontent.com/paralleldrive/aidd/main/docs/new-project-setup-nextjs-shadcn.md"; +/** + * Run a command and stream output to user + */ +const runCommand = (command, args = [], cwd = process.cwd()) => + new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + }); + + child.on("close", (code) => { + if (code === 0) { + resolve({ success: true }); + } else { + reject(new Error(`Command failed with exit code ${code}`)); + } + }); + + child.on("error", (error) => { + reject(error); + }); + }); /** - * Execute the create-next-shadcn command by piping instructions to Claude CLI + * Execute the create-next-shadcn command by automating Phase 1 setup + * @param {string} projectName - Name of the project to create * @returns {Promise<{success: boolean, error?: string}>} */ -export const executeCreateNextShadcn = async () => { +export const executeCreateNextShadcn = async (projectName = "my-app") => { + const projectPath = path.resolve(process.cwd(), projectName); + try { - // Check if claude CLI is installed - await execAsync("which claude"); - } catch { - return { - success: false, - error: - "Claude Code CLI is not installed. Please install it first: https://docs.claude.ai/cli", + console.log("๐Ÿš€ Creating Next.js app...\n"); + + // Step 1: Create Next.js app with defaults + await runCommand("npx", [ + "create-next-app@latest", + projectName, + "--typescript", + "--eslint", + "--tailwind", + "--app", + "--no-src-dir", + "--import-alias", + "@/*", + "--use-npm", + ]); + + console.log("\nโœ… Next.js app created\n"); + + // Step 2: Install aidd + console.log("๐Ÿ“ฆ Installing aidd framework...\n"); + await runCommand("npx", ["aidd", "--cursor"], projectPath); + await runCommand("npm", ["install", "aidd"], projectPath); + + console.log("\nโœ… AIDD framework installed\n"); + + // Step 3: Install test dependencies + console.log("๐Ÿงช Installing test dependencies...\n"); + await runCommand( + "npm", + ["install", "-D", "vitest", "riteway", "@playwright/test", "@vitest/ui"], + projectPath, + ); + + console.log("\nโœ… Test dependencies installed\n"); + + // Step 4: Install Storybook + console.log("๐Ÿ“š Installing Storybook...\n"); + await runCommand("npx", ["storybook@latest", "init", "--yes"], projectPath); + + console.log("\nโœ… Storybook installed\n"); + + // Step 5: Update package.json scripts + console.log("โš™๏ธ Configuring npm scripts...\n"); + const packageJsonPath = path.join(projectPath, "package.json"); + const packageJson = await fs.readJson(packageJsonPath); + + packageJson.scripts = { + ...packageJson.scripts, + test: "vitest run && npm run -s lint && npm run -s typecheck", + "test:watch": "vitest", + "test:e2e": "playwright test", + "test:ui": "vitest --ui", + typecheck: "tsc --noEmit", }; - } - try { - // Execute claude with piped prompt instructions using spawn for better output handling - return new Promise((resolve, reject) => { - const command = `{ echo "Follow these instructions exactly. Stop when complete." && echo && curl -fsSL "${PROMPT_URL}"; } | claude`; - - const child = spawn("bash", ["-c", command], { - stdio: "inherit", // Inherit stdio so user sees Claude's output - shell: true, - }); - - child.on("close", (code) => { - if (code === 0) { - resolve({ success: true }); - } else { - resolve({ - success: false, - error: `Claude CLI exited with code ${code}`, - }); - } - }); - - child.on("error", (error) => { - resolve({ - success: false, - error: `Failed to execute claude: ${error.message}`, - }); - }); + packageJson.type = "module"; + + await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); + + console.log("โœ… Package.json configured\n"); + + // Step 6: Create baseline test file + console.log("๐Ÿ“ Creating baseline tests...\n"); + const testDir = path.join(projectPath, "app"); + const testFile = path.join(testDir, "page.test.js"); + + const testContent = `import { assert } from "riteway"; +import { describe, test } from "vitest"; + +describe("Home page", () => { + test("baseline test", () => { + assert({ + given: "a baseline test", + should: "pass", + actual: true, + expected: true, }); + }); +}); +`; + + await fs.writeFile(testFile, testContent); + + console.log("โœ… Baseline test created\n"); + + // Step 7: Create playwright config + console.log("๐ŸŽญ Configuring Playwright...\n"); + const playwrightConfig = `import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'npm run build && npm start', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, +}); +`; + + await fs.writeFile( + path.join(projectPath, "playwright.config.js"), + playwrightConfig, + ); + + // Create e2e test directory and sample test + const e2eDir = path.join(projectPath, "e2e"); + await fs.ensureDir(e2eDir); + + const e2eTest = `import { test, expect } from '@playwright/test'; + +test('home page loads', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveTitle(/Next/); +}); +`; + + await fs.writeFile(path.join(e2eDir, "home.spec.js"), e2eTest); + + console.log("โœ… Playwright configured\n"); + + console.log("๐ŸŽ‰ Phase 1 setup complete!\n"); + console.log(`๐Ÿ“ Project created at: ${projectPath}\n`); + console.log("Next steps:"); + console.log(` cd ${projectName}`); + console.log(" npm test # Run unit tests"); + console.log(" npm run test:e2e # Run E2E tests"); + console.log(" npm run storybook # Start Storybook"); + console.log(" npm run dev # Start dev server\n"); + console.log( + "๐Ÿ’ก For Phase 2 (design system), use the /scaffold command with your preferred LLM", + ); + + return { success: true }; } catch (error) { return { success: false, - error: `Failed to execute claude: ${error.message}`, + error: `Setup failed: ${error.message}`, }; } }; diff --git a/lib/create-next-shadcn.test.js b/lib/create-next-shadcn.test.js index 3ed88df..c3eb8eb 100644 --- a/lib/create-next-shadcn.test.js +++ b/lib/create-next-shadcn.test.js @@ -1,42 +1,55 @@ import { assert } from "riteway/vitest"; import { describe, test, vi, beforeEach, afterEach } from "vitest"; -import * as childProcess from "child_process"; -import * as util from "util"; -// Mock child_process before importing the module -vi.mock("child_process"); -vi.mock("util"); +// Mock modules before importing +vi.mock("child_process", () => ({ + spawn: vi.fn(), + exec: vi.fn(), +})); + +vi.mock("fs-extra", () => ({ + default: { + readJson: vi.fn(), + writeJson: vi.fn(), + writeFile: vi.fn(), + ensureDir: vi.fn(), + }, + readJson: vi.fn(), + writeJson: vi.fn(), + writeFile: vi.fn(), + ensureDir: vi.fn(), +})); + +vi.mock("util", () => ({ + promisify: vi.fn(), +})); describe("executeCreateNextShadcn", () => { - let execAsyncMock; + let childProcess; + let fs; let spawnMock; + let readJsonMock; + let writeJsonMock; + let writeFileMock; + let ensureDirMock; beforeEach(async () => { - execAsyncMock = vi.fn(); - spawnMock = vi.fn(); - vi.mocked(util.promisify).mockReturnValue(execAsyncMock); - vi.mocked(childProcess.spawn).mockImplementation(spawnMock); - }); - - afterEach(() => { - vi.clearAllMocks(); - vi.resetModules(); - }); + childProcess = await import("child_process"); + fs = await import("fs-extra"); - test("should check for claude CLI", async () => { - const called = []; - execAsyncMock.mockImplementation((cmd) => { - called.push(cmd); - if (cmd === "which claude") { - return Promise.resolve({ - stdout: "/usr/local/bin/claude\n", - stderr: "", - }); - } - return Promise.resolve({ stdout: "OK", stderr: "" }); - }); - - // Mock spawn to immediately call close with success + spawnMock = vi.fn(); + readJsonMock = vi.fn(); + writeJsonMock = vi.fn(); + writeFileMock = vi.fn(); + ensureDirMock = vi.fn(); + + childProcess.spawn.mockImplementation(spawnMock); + fs.readJson.mockImplementation(readJsonMock); + fs.writeJson.mockImplementation(writeJsonMock); + fs.writeFile.mockImplementation(writeFileMock); + fs.ensureDir.mockImplementation(ensureDirMock); + + // Default successful spawn implementation spawnMock.mockReturnValue({ on: (event, callback) => { if (event === "close") { @@ -45,52 +58,26 @@ describe("executeCreateNextShadcn", () => { }, }); - const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); - await executeCreateNextShadcn(); - - assert({ - given: "executeCreateNextShadcn is called", - should: "check if claude CLI exists", - actual: called.some((cmd) => cmd === "which claude"), - expected: true, - }); - }); - - test("should return error when claude CLI is not found", async () => { - execAsyncMock.mockImplementation((cmd) => { - if (cmd === "which claude") { - return Promise.reject(new Error("not found")); - } - return Promise.resolve({ stdout: "OK", stderr: "" }); - }); - - const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); - const result = await executeCreateNextShadcn(); - - assert({ - given: "claude CLI is not installed", - should: "return error with installation instructions", - actual: result.success, - expected: false, + // Default package.json + readJsonMock.mockResolvedValue({ + name: "test-app", + scripts: {}, }); - assert({ - given: "claude CLI is not installed", - should: "include error message about claude installation", - actual: result.error?.includes("Claude Code CLI"), - expected: true, - }); + writeJsonMock.mockResolvedValue(undefined); + writeFileMock.mockResolvedValue(undefined); + ensureDirMock.mockResolvedValue(undefined); }); - test("should execute claude with correct prompt URL", async () => { - const spawnArgs = []; - execAsyncMock.mockResolvedValue({ - stdout: "/usr/local/bin/claude\n", - stderr: "", - }); + afterEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + }); + test("should run create-next-app with correct flags", async () => { + const spawnCalls = []; spawnMock.mockImplementation((cmd, args) => { - spawnArgs.push({ cmd, args }); + spawnCalls.push({ cmd, args }); return { on: (event, callback) => { if (event === "close") { @@ -101,27 +88,33 @@ describe("executeCreateNextShadcn", () => { }); const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); - await executeCreateNextShadcn(); + await executeCreateNextShadcn("test-project"); - const bashCommand = spawnArgs.find((arg) => arg.cmd === "bash"); + const createNextCall = spawnCalls.find( + (call) => + call.cmd === "npx" && + call.args?.some((arg) => arg === "create-next-app@latest"), + ); assert({ - given: "claude CLI exists", - should: "execute bash command", - actual: bashCommand !== undefined, + given: "executeCreateNextShadcn is called", + should: "run create-next-app", + actual: createNextCall !== undefined, expected: true, }); - }); - test("should pipe prompt instructions to claude", async () => { - const spawnArgs = []; - execAsyncMock.mockResolvedValue({ - stdout: "/usr/local/bin/claude\n", - stderr: "", + assert({ + given: "create-next-app command", + should: "include TypeScript flag", + actual: createNextCall?.args?.includes("--typescript"), + expected: true, }); + }); + test("should install aidd framework", async () => { + const spawnCalls = []; spawnMock.mockImplementation((cmd, args) => { - spawnArgs.push({ cmd, args }); + spawnCalls.push({ cmd, args }); return { on: (event, callback) => { if (event === "close") { @@ -132,30 +125,24 @@ describe("executeCreateNextShadcn", () => { }); const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); - await executeCreateNextShadcn(); + await executeCreateNextShadcn("test-project"); - const bashCommand = spawnArgs.find((arg) => arg.cmd === "bash"); - const commandString = bashCommand?.args?.join(" "); + const aiddInstall = spawnCalls.find( + (call) => call.cmd === "npx" && call.args?.includes("aidd"), + ); assert({ - given: "claude command is executed", - should: "include prompt instructions", - actual: - commandString?.includes("Follow these instructions exactly") || - commandString?.includes("echo"), + given: "executeCreateNextShadcn is called", + should: "install aidd framework", + actual: aiddInstall !== undefined, expected: true, }); }); - test("should include GitHub prompt URL", async () => { - const spawnArgs = []; - execAsyncMock.mockResolvedValue({ - stdout: "/usr/local/bin/claude\n", - stderr: "", - }); - + test("should install test dependencies", async () => { + const spawnCalls = []; spawnMock.mockImplementation((cmd, args) => { - spawnArgs.push({ cmd, args }); + spawnCalls.push({ cmd, args }); return { on: (event, callback) => { if (event === "close") { @@ -166,17 +153,18 @@ describe("executeCreateNextShadcn", () => { }); const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); - await executeCreateNextShadcn(); + await executeCreateNextShadcn("test-project"); - const bashCommand = spawnArgs.find((arg) => arg.cmd === "bash"); - const commandString = bashCommand?.args?.join(" "); + const testDepsInstall = spawnCalls.find( + (call) => + call.cmd === "npm" && + call.args?.some((arg) => arg === "vitest" || arg === "riteway"), + ); assert({ - given: "claude command is executed", - should: "reference the GitHub prompt URL", - actual: - commandString?.includes("new-project-setup-nextjs-shadcn.md") || - commandString?.includes("curl"), + given: "executeCreateNextShadcn is called", + should: "install vitest and riteway", + actual: testDepsInstall !== undefined, expected: true, }); }); From 3482a29ac28b6ac2ed753a39f438926e27ca0f60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 00:50:24 +0000 Subject: [PATCH 5/5] Remove unused execAsync import Co-authored-by: ericelliott <364727+ericelliott@users.noreply.github.com> --- lib/create-next-shadcn.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/create-next-shadcn.js b/lib/create-next-shadcn.js index c38dbf0..62dfbee 100644 --- a/lib/create-next-shadcn.js +++ b/lib/create-next-shadcn.js @@ -1,12 +1,8 @@ import { spawn } from "child_process"; -import { promisify } from "util"; -import { exec } from "child_process"; import fs from "fs-extra"; import path from "path"; import process from "process"; -const execAsync = promisify(exec); - /** * Run a command and stream output to user */