Repository navigation
Initial scaffold CLI project and templates - #1
Conversation
Bootstrap the scaffold CLI project: add CLI entry, command handlers, generators, TypeScript types, utility helpers, and a comprehensive set of Handlebars templates for React, Next.js, Angular, Express, and FastAPI. Include project configuration and developer tooling (ESLint, Prettier, tsup, vitest, tsconfig), docs (CONTRIBUTING, PLUGIN_GUIDE, TEMPLATE_GUIDE), tests, and GitHub metadata (issue/PR templates and workflows for CI, release, and template testing). Also update README with usage and features and add a basic changesets config and package.json to support publishing.
There was a problem hiding this comment.
Pull request overview
This PR scaffolds an initial scaffold CLI project generator, including core CLI commands, generator framework, built-in templates (React/Next.js/Angular/Express/FastAPI + variants), and a test/docs/CI baseline.
Changes:
- Added CLI commands (
create,list,doctor) plus prompt flows and generator implementations. - Added a large set of Handlebars templates for multiple frameworks, shared configs, and optional features (CI/Docker/etc.).
- Added Vitest-based unit/integration tests and GitHub Actions workflows (CI, release, template smoke tests), plus contributor docs.
Reviewed changes
Copilot reviewed 103 out of 105 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Adds Vitest configuration for the repo test suite. |
| tsup.config.ts | Adds tsup build configuration for bundling the CLI. |
| tsconfig.json | Adds TypeScript compiler configuration for the CLI source. |
| tests/unit/prompts.test.ts | Unit tests for validation utilities. |
| tests/unit/plugins.test.ts | Unit tests for plugin registry behavior. |
| tests/unit/generators.test.ts | Unit tests for generator selection + file emission checks. |
| tests/integration/scaffold.test.ts | Integration tests executing the built CLI. |
| tests/fixtures/test-config.json | Fixture config for “create from config” scenarios. |
| src/utils/validator.ts | Project name, directory, and Node version validation utilities. |
| src/utils/logger.ts | Logging/spinner helpers for CLI UX. |
| src/utils/index.ts | Barrel exports for utility modules. |
| src/utils/file.ts | File IO + template rendering + templates directory resolution. |
| src/types/plugin.ts | Plugin interface/types. |
| src/types/index.ts | Barrel exports for type definitions and defaults. |
| src/types/generator.ts | Generator result type definition. |
| src/types/config.ts | Config model/types and default config/feature flags. |
| src/templates/shared/tsconfig.base.json.hbs | Shared base TS config template. |
| src/templates/shared/readme.md.hbs | Shared README template for generated projects. |
| src/templates/shared/prettier.config.js.hbs | Shared Prettier config template. |
| src/templates/shared/license.hbs | Shared LICENSE template. |
| src/templates/shared/gitignore.hbs | Shared .gitignore template. |
| src/templates/shared/github-ci.yml.hbs | Shared GitHub Actions CI template for generated projects. |
| src/templates/shared/eslint.config.js.hbs | Shared ESLint flat config template. |
| src/templates/shared/docker/docker-compose.yml.hbs | Shared docker-compose template for generated projects. |
| src/templates/shared/docker/Dockerfile.hbs | Shared Dockerfile template for generated projects. |
| src/templates/react/with-tailwind/tailwind.config.ts.hbs | React Tailwind variant config template. |
| src/templates/react/with-tailwind/src/index.css.hbs | React Tailwind variant CSS template. |
| src/templates/react/with-tailwind/postcss.config.js.hbs | React Tailwind variant PostCSS config template. |
| src/templates/react/with-shadcn/src/lib/utils.ts.hbs | React shadcn variant utility template. |
| src/templates/react/with-shadcn/components.json.hbs | React shadcn variant components config template. |
| src/templates/react/base/vite.config.ts.hbs | React base Vite config template (optionally with Vitest). |
| src/templates/react/base/tsconfig.json.hbs | React base TS config template. |
| src/templates/react/base/tests/App.test.tsx.hbs | React base test template. |
| src/templates/react/base/src/main.tsx.hbs | React base entrypoint template. |
| src/templates/react/base/src/index.css.hbs | React base CSS template. |
| src/templates/react/base/src/App.tsx.hbs | React base App template. |
| src/templates/react/base/package.json.hbs | React base package.json template. |
| src/templates/react/base/index.html.hbs | React base HTML template. |
| src/templates/nextjs/with-prisma/src/lib/db.ts.hbs | Next.js Prisma variant DB client template. |
| src/templates/nextjs/with-prisma/prisma/seed.ts.hbs | Next.js Prisma seed template. |
| src/templates/nextjs/with-prisma/prisma/schema.prisma.hbs | Next.js Prisma schema template. |
| src/templates/nextjs/with-auth/src/lib/auth.ts.hbs | Next.js NextAuth variant auth config template. |
| src/templates/nextjs/with-auth/src/app/api/auth/[...nextauth]/route.ts.hbs | Next.js NextAuth route handler template. |
| src/templates/nextjs/base/tsconfig.json.hbs | Next.js base TS config template. |
| src/templates/nextjs/base/src/app/page.tsx.hbs | Next.js base page template. |
| src/templates/nextjs/base/src/app/layout.tsx.hbs | Next.js base layout template. |
| src/templates/nextjs/base/src/app/globals.css.hbs | Next.js base global styles template (optionally Tailwind). |
| src/templates/nextjs/base/package.json.hbs | Next.js base package.json template. |
| src/templates/nextjs/base/next.config.ts.hbs | Next.js base config template. |
| src/templates/fastapi/base/requirements.txt.hbs | FastAPI requirements template. |
| src/templates/fastapi/base/pyproject.toml.hbs | FastAPI pyproject template. |
| src/templates/fastapi/base/app/routes/init.py.hbs | FastAPI routes package marker. |
| src/templates/fastapi/base/app/models/init.py.hbs | FastAPI models package marker. |
| src/templates/fastapi/base/app/main.py.hbs | FastAPI app template. |
| src/templates/express/with-prisma/src/lib/db.ts.hbs | Express Prisma variant DB client template. |
| src/templates/express/with-prisma/prisma/seed.ts.hbs | Express Prisma seed template. |
| src/templates/express/with-prisma/prisma/schema.prisma.hbs | Express Prisma schema template. |
| src/templates/express/base/tsconfig.json.hbs | Express base TS config template. |
| src/templates/express/base/src/routes/index.ts.hbs | Express base routes template. |
| src/templates/express/base/src/middleware/errorHandler.ts.hbs | Express base error middleware template. |
| src/templates/express/base/src/index.ts.hbs | Express base server entrypoint template. |
| src/templates/express/base/package.json.hbs | Express base package.json template. |
| src/templates/angular/base/tsconfig.json.hbs | Angular base TS config template. |
| src/templates/angular/base/src/main.ts.hbs | Angular base bootstrap template. |
| src/templates/angular/base/src/app/app.config.ts.hbs | Angular base application config template. |
| src/templates/angular/base/src/app/app.component.ts.hbs | Angular base component template. |
| src/templates/angular/base/package.json.hbs | Angular base package.json template. |
| src/templates/angular/base/angular.json.hbs | Angular base angular.json template. |
| src/prompts/index.ts | Orchestrates interactive prompts into a ProjectConfig. |
| src/prompts/framework.ts | Prompts for project type + framework selection. |
| src/prompts/features.ts | Prompts for feature flag selection. |
| src/prompts/config.ts | Prompts for variant/package manager/git init. |
| src/plugins/types.ts | Re-exports plugin types for plugin API consumers. |
| src/plugins/index.ts | Plugin registry + plugin loading helpers. |
| src/index.ts | CLI entrypoint that parses Commander program. |
| src/generators/react.ts | React generator implementation. |
| src/generators/nextjs.ts | Next.js generator implementation. |
| src/generators/index.ts | Generator factory + base export. |
| src/generators/fullstack.ts | Full-stack monorepo generator implementation. |
| src/generators/fastapi.ts | FastAPI generator implementation (custom shared config behavior). |
| src/generators/express.ts | Express generator implementation. |
| src/generators/base.ts | Base generator pipeline (base/variant/shared/features/git). |
| src/generators/angular.ts | Angular generator implementation. |
| src/constants.ts | Framework metadata + variant display names. |
| src/commands/list.ts | scaffold list command implementation. |
| src/commands/index.ts | Commands barrel export. |
| src/commands/doctor.ts | scaffold doctor dependency check command. |
| src/commands/create.ts | scaffold create command implementation. |
| src/cli.ts | Commander program wiring and global flags. |
| package.json | Defines CLI package metadata, scripts, deps, and engine constraints. |
| eslint.config.js | ESLint flat config for the repo. |
| docs/TEMPLATE_GUIDE.md | Documentation for adding templates/variants. |
| docs/PLUGIN_GUIDE.md | Documentation for plugin system usage/authoring. |
| docs/CONTRIBUTING.md | Contributor setup + workflow documentation. |
| README.md | Project README with usage and supported templates overview. |
| .prettierrc | Repo Prettier configuration. |
| .github/workflows/template-test.yml | Scheduled workflow to generate templates and smoke-check them. |
| .github/workflows/release.yml | Tag-based npm publish + GitHub release workflow. |
| .github/workflows/ci.yml | CI workflow (typecheck/lint/format/build/test). |
| .github/PULL_REQUEST_TEMPLATE.md | PR template for contributors. |
| .github/ISSUE_TEMPLATE/new_template.yml | Issue template for new template requests. |
| .github/ISSUE_TEMPLATE/feature_request.yml | Issue template for feature requests. |
| .github/ISSUE_TEMPLATE/bug_report.yml | Issue template for bug reports. |
| .changeset/config.json | Changesets configuration for versioning/releases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const choices: FeatureChoice[] = [ | ||
| { | ||
| name: 'TypeScript', | ||
| value: 'typescript', | ||
| checked: true, | ||
| disabled: isPython ? 'N/A for Python' : false, | ||
| }, | ||
| { name: 'ESLint + Prettier', value: 'eslint', checked: !isPython, disabled: isPython ? 'N/A for Python' : false }, | ||
| { name: 'Tailwind CSS', value: 'tailwind', checked: false }, | ||
| { name: 'shadcn/ui components', value: 'shadcn', checked: false }, | ||
| { name: isPython ? 'Pytest testing' : 'Vitest testing', value: 'testing', checked: true }, | ||
| { name: 'GitHub Actions CI/CD', value: 'githubActions', checked: false }, | ||
| { name: 'Docker setup', value: 'docker', checked: false }, | ||
| { name: 'Husky pre-commit hooks', value: 'husky', checked: false, disabled: isPython ? 'N/A for Python' : false }, | ||
| { name: '.env example file', value: 'envExample', checked: false }, | ||
| ]; | ||
|
|
||
| // Filter out frontend-only features for backend projects | ||
| const filteredChoices = | ||
| projectType === 'backend' | ||
| ? choices.filter((c) => !['tailwind', 'shadcn'].includes(c.value)) | ||
| : choices; | ||
|
|
There was a problem hiding this comment.
tailwind/shadcn are being collected as feature flags here, but the actual file overlays live under template variants (with-tailwind, with-shadcn) and are selected separately via promptVariant(). This can easily generate broken projects (e.g. base variant + features.tailwind=true => Tailwind deps but no config/CSS; or with-tailwind variant + features.tailwind=false => config files but missing deps). Consider removing these from the feature checklist and treating them purely as variants, or automatically derive features.tailwind/features.shadcn from the chosen variant (and/or constrain available variants based on chosen features).
| {{else}} | ||
| FROM node:20-alpine AS base | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY package*.json ./ | ||
| RUN npm ci | ||
|
|
||
| COPY . . | ||
|
|
||
| {{#if (eq framework "nextjs")}} | ||
| FROM base AS builder | ||
| RUN npm run build | ||
|
|
||
| FROM node:20-alpine AS runner | ||
| WORKDIR /app | ||
| COPY --from=builder /app/.next/standalone ./ | ||
| COPY --from=builder /app/.next/static ./.next/static | ||
| COPY --from=builder /app/public ./public | ||
|
|
||
| EXPOSE 3000 | ||
| CMD ["node", "server.js"] | ||
| {{else}} | ||
| RUN npm run build | ||
|
|
||
| EXPOSE 3000 | ||
| CMD ["node", "dist/index.js"] | ||
| {{/if}} |
There was a problem hiding this comment.
The non-FastAPI/non-Next.js branch assumes every Node template builds a runnable dist/index.js (CMD ["node", "dist/index.js"]). That’s not true for frontend templates like React+Vite (output is static assets) or Angular, so enabling the docker feature will generate a Dockerfile that can’t start. Consider making Docker templates framework-specific (React/Angular should serve built static files) or restricting docker to frameworks where this Dockerfile is valid (e.g., Express).
| const frontendConfig: ProjectConfig = { | ||
| ...this.config, | ||
| type: 'frontend', | ||
| gitInit: false, | ||
| }; | ||
|
|
||
| const frontendGen = this.createFrontendGenerator(frontendConfig, webDir); | ||
| await frontendGen.generate(); | ||
|
|
||
| // Generate backend in apps/api | ||
| const backendConfig: ProjectConfig = { | ||
| ...this.config, | ||
| type: 'backend', | ||
| framework: 'express', | ||
| gitInit: false, | ||
| }; | ||
|
|
||
| const backendGen = new ExpressGenerator(backendConfig, apiDir); | ||
| await backendGen.generate(); | ||
|
|
||
| // Write root package.json for workspaces | ||
| const rootPkgPath = path.join(this.targetDir, 'package.json'); |
There was a problem hiding this comment.
FullstackGenerator calls generateBase() directly on the nested frontend/backend generators. That bypasses their generate() pipeline (variant overlay, shared configs like README/.gitignore/LICENSE, feature application like GitHub Actions/Docker, and git init). The resulting monorepo apps will be missing those expected files/configs. Consider invoking generate() for each sub-generator (with gitInit: false) or refactoring shared steps so fullstack generation applies the same stages.
| ## Loading Local Plugins | ||
|
|
||
| Use the `--plugin` flag to load a plugin from a local path: | ||
|
|
||
| ```bash | ||
| scaffold create my-app --plugin ./my-local-plugin.js | ||
| ``` |
There was a problem hiding this comment.
This guide documents a --plugin flag for scaffold create, but the current CLI implementation does not define or handle any --plugin option, and plugin loading isn’t invoked from the command flow. Either implement the flag + plugin loading path, or update the docs to match the actual supported behavior.
| if (!VALID_PROJECT_NAME.test(name)) { | ||
| return 'Project name must be a valid npm package name (lowercase, no spaces, can use hyphens and dots).'; | ||
| } |
There was a problem hiding this comment.
The error message says project names "can use hyphens and dots", but VALID_PROJECT_NAME also allows _ and ~ (and scoped names). Either tighten the regex to match the documented rules, or update the message to reflect what’s actually accepted so users aren’t misled.
| import { type ClassValue, clsx } from 'clsx'; | ||
| import { twMerge } from 'tailwind-merge'; | ||
|
|
||
| export function cn(...inputs: ClassValue[]) { | ||
| return twMerge(clsx(inputs)); | ||
| } |
There was a problem hiding this comment.
This shadcn variant introduces imports from clsx and tailwind-merge, but the React base package.json.hbs does not include these dependencies under any feature/variant condition. The generated project will fail to compile. Add the required deps (and ensure Tailwind is enabled/installed for this variant) via a package.json.hbs overlay or conditional dependencies based on variant/feature flags.
| import { spinner, success, createDir, writeFile } from '../utils/index.js'; | ||
| import type { ProjectConfig } from '../types/index.js'; | ||
|
|
There was a problem hiding this comment.
There are unused imports here (renderAndWrite, getTemplatesDir) which will be flagged by TypeScript/ESLint depending on settings and add noise. Remove them or use them as intended.
| export async function loadPluginsFromNodeModules(): Promise<void> { | ||
| // Scan node_modules for scaffold-plugin-* packages | ||
| try { | ||
| const fs = await import('fs-extra'); | ||
| const path = await import('node:path'); | ||
| const nodeModulesDir = path.join(process.cwd(), 'node_modules'); | ||
|
|
||
| if (!(await fs.pathExists(nodeModulesDir))) return; | ||
|
|
||
| const entries = await fs.readdir(nodeModulesDir); | ||
| const pluginDirs = entries.filter((e) => e.startsWith('scaffold-plugin-')); | ||
|
|
||
| for (const dir of pluginDirs) { | ||
| try { | ||
| const pluginPath = path.join(nodeModulesDir, dir); | ||
| const pluginModule = await import(pluginPath); | ||
| const plugin: ScaffoldPlugin = pluginModule.default || pluginModule; | ||
| registerPlugin(plugin); |
There was a problem hiding this comment.
loadPluginsFromNodeModules() builds an absolute directory path (.../node_modules/<pkg>) and passes it to import(). In Node ESM, importing a directory path typically throws ERR_UNSUPPORTED_DIR_IMPORT unless you resolve to a file (or use the package specifier). Additionally, this scan misses scoped packages (node_modules/@scope/scaffold-plugin-*). Prefer await import(dir) using the package name, and/or handle @scope/* directories when scanning.
| } catch { | ||
| s.stop(); | ||
| debug('Git initialization skipped (git not available)'); |
There was a problem hiding this comment.
The catch here treats any git failure as “git not available”. In practice git commit commonly fails due to missing user.name/user.email or an empty index, and silently skipping leaves users thinking git init worked. Consider catching errors separately (or logging the actual error in verbose mode) and adding a warning to result.warnings when git init/commit fails.
| } catch { | |
| s.stop(); | |
| debug('Git initialization skipped (git not available)'); | |
| } catch (error: unknown) { | |
| s.stop(); | |
| const message = | |
| error instanceof Error && error.message | |
| ? error.message | |
| : String(error); | |
| debug(`Git initialization failed: ${message}`); | |
| this.result.warnings.push( | |
| 'Git initialization failed. The repository may not have been fully initialized. Please ensure git is installed and configured (e.g., user.name and user.email) and try again.', | |
| ); |
| import type { NextAuthOptions } from 'next-auth'; | ||
| import GithubProvider from 'next-auth/providers/github'; | ||
|
|
||
| export const authOptions: NextAuthOptions = { | ||
| providers: [ | ||
| GithubProvider({ | ||
| clientId: process.env.GITHUB_ID ?? '', | ||
| clientSecret: process.env.GITHUB_SECRET ?? '', | ||
| }), |
There was a problem hiding this comment.
This template imports next-auth, but the generated project’s package.json (base) does not add next-auth to dependencies/devDependencies when the with-auth variant is selected. As-is, the generated Next.js project will fail to install/build. Add a package.json.hbs overlay in this variant (or conditionalize base package.json.hbs on variant) to include the required dependencies and any required scripts/env docs.
No description provided.