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 859801d..3a6647e 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 app with tests and AIDD: + + npx aidd create-next-shadcn [project-name] `, ) .addHelpText( @@ -205,6 +210,25 @@ https://paralleldrive.com process.exit(result.success ? 0 : 1); }, ); + + // Add create-next-shadcn command + program + .command("create-next-shadcn [project-name]") + .description( + "Scaffold a new Next.js app with AIDD, tests, and baseline setup", + ) + .action(async (projectName = "my-app") => { + const result = await executeCreateNextShadcn(projectName); + + if (!result.success) { + console.error(chalk.red(`\nโŒ ${result.error}`)); + process.exit(1); + } + + 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..1341603 --- /dev/null +++ b/bin/create-next-shadcn-e2e.test.js @@ -0,0 +1,51 @@ +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"), + expected: true, + }); + }); + + 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: "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 new file mode 100644 index 0000000..62dfbee --- /dev/null +++ b/lib/create-next-shadcn.js @@ -0,0 +1,193 @@ +import { spawn } from "child_process"; +import fs from "fs-extra"; +import path from "path"; +import process from "process"; + +/** + * 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 automating Phase 1 setup + * @param {string} projectName - Name of the project to create + * @returns {Promise<{success: boolean, error?: string}>} + */ +export const executeCreateNextShadcn = async (projectName = "my-app") => { + const projectPath = path.resolve(process.cwd(), projectName); + + try { + 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", + }; + + 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: `Setup failed: ${error.message}`, + }; + } +}; diff --git a/lib/create-next-shadcn.test.js b/lib/create-next-shadcn.test.js new file mode 100644 index 0000000..c3eb8eb --- /dev/null +++ b/lib/create-next-shadcn.test.js @@ -0,0 +1,171 @@ +import { assert } from "riteway/vitest"; +import { describe, test, vi, beforeEach, afterEach } from "vitest"; + +// 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 childProcess; + let fs; + let spawnMock; + let readJsonMock; + let writeJsonMock; + let writeFileMock; + let ensureDirMock; + + beforeEach(async () => { + childProcess = await import("child_process"); + fs = await import("fs-extra"); + + 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") { + setTimeout(() => callback(0), 0); + } + }, + }); + + // Default package.json + readJsonMock.mockResolvedValue({ + name: "test-app", + scripts: {}, + }); + + writeJsonMock.mockResolvedValue(undefined); + writeFileMock.mockResolvedValue(undefined); + ensureDirMock.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + }); + + test("should run create-next-app with correct flags", async () => { + const spawnCalls = []; + spawnMock.mockImplementation((cmd, args) => { + spawnCalls.push({ cmd, args }); + return { + on: (event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 0); + } + }, + }; + }); + + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); + await executeCreateNextShadcn("test-project"); + + const createNextCall = spawnCalls.find( + (call) => + call.cmd === "npx" && + call.args?.some((arg) => arg === "create-next-app@latest"), + ); + + assert({ + given: "executeCreateNextShadcn is called", + should: "run create-next-app", + actual: createNextCall !== undefined, + expected: true, + }); + + 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) => { + spawnCalls.push({ cmd, args }); + return { + on: (event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 0); + } + }, + }; + }); + + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); + await executeCreateNextShadcn("test-project"); + + const aiddInstall = spawnCalls.find( + (call) => call.cmd === "npx" && call.args?.includes("aidd"), + ); + + assert({ + given: "executeCreateNextShadcn is called", + should: "install aidd framework", + actual: aiddInstall !== undefined, + expected: true, + }); + }); + + test("should install test dependencies", async () => { + const spawnCalls = []; + spawnMock.mockImplementation((cmd, args) => { + spawnCalls.push({ cmd, args }); + return { + on: (event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 0); + } + }, + }; + }); + + const { executeCreateNextShadcn } = await import("./create-next-shadcn.js"); + await executeCreateNextShadcn("test-project"); + + const testDepsInstall = spawnCalls.find( + (call) => + call.cmd === "npm" && + call.args?.some((arg) => arg === "vitest" || arg === "riteway"), + ); + + assert({ + given: "executeCreateNextShadcn is called", + should: "install vitest and riteway", + actual: testDepsInstall !== undefined, + expected: true, + }); + }); +});