Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ create-faster/
- `hono`/`express`: Dedicated backend in `{appName}-api/`
- `undefined`: No backend (only for frameworks without built-in backend)

5. **Context-Aware Filtering** (NEW)
- Category-level `requires`: Dependencies between categories (e.g., `orm.requires = ['database']`)
- Stack-level `requires`: Dependencies for specific stacks (e.g., `husky.requires = ['git']`)
- Progressive context building: Each prompt updates shared context for dynamic filtering
- Automatic skip: Prompts auto-skip when requirements not met with informative logs

### CLI Application Flow

```
Expand Down Expand Up @@ -117,11 +123,13 @@ create-faster/
## Core Files & Responsibilities

### [apps/cli/src/__meta__.ts](apps/cli/src/__meta__.ts)
**Single source of truth** for all available stacks (85 lines)
**Single source of truth** for all available stacks (88 lines)
- Defines labels, hints, dependencies, and capabilities for each framework/tool
- Category-level `requires`: Dependencies between entire categories (e.g., `orm.requires = ['database']`)
- Stack-level `requires`: Dependencies for individual stacks (e.g., `husky.requires = ['git']`)
- Types: `StackMeta`, `CategoryMeta`, `Meta`
- To add new framework: add entry here first
- Categories: web, api, mobile, orm, database, extras
- Categories: web, api, mobile, database, orm, git, extras

### [apps/cli/src/types.ts](apps/cli/src/types.ts)
Core type definitions (44 lines)
Expand All @@ -133,11 +141,13 @@ Core type definitions (44 lines)
- `TemplateContext`: Runtime context for template resolution

### [apps/cli/src/index.ts](apps/cli/src/index.ts)
Main CLI entry point (170 lines)
- Orchestrates interactive prompt flow
Main CLI entry point (167 lines)
- Orchestrates interactive prompt flow with progressive context building
- Handles multi-app configuration
- Conditional database/ORM selection
- Extras multi-selection (biome, git, husky)
- Context-aware database → ORM selection (ORM requires database)
- Git confirmation with boolean context
- Extras multi-selection with automatic filtering (husky requires git)
- Progressive `ctx` object updated after each prompt for dependency chain
- Currently ends at `getAllTemplatesForContext()` call

### [apps/cli/src/lib/schema.ts](apps/cli/src/lib/schema.ts)
Expand All @@ -147,10 +157,12 @@ Zod validation schemas (24 lines)
- Type-safe runtime validation

### [apps/cli/src/lib/prompts.ts](apps/cli/src/lib/prompts.ts)
Reusable prompt wrappers (68 lines)
Context-aware prompt wrappers (124 lines)
- `filterOptionsByContext()`: Central filtering logic for category + stack requires
- `promptText()`: Open text input with validation
- `promptSelect()`: Single selection from options
- `promptMultiselect()`: Multiple selection from options
- `promptSelect()`: Single selection with auto-skip based on context
- `promptMultiselect()`: Multiple selection with auto-filter based on context
- `promptConfirm()`: Boolean confirmation prompt
- Unified cancellation/error handling
- Built on @clack/prompts

Expand Down Expand Up @@ -180,13 +192,14 @@ Template discovery and path resolution (99 lines)
- **Mobile**: Expo (React Native)

### Database & ORM
- **Database**: PostgreSQL
- **Database**: PostgreSQL, MySQL
- **ORM**: Prisma (type-safe), Drizzle (lightweight SQL-like)
- **Dependency Chain**: Database → ORM (ORM requires database selection first)

### Extras
- **Biome**: Linter + formatter (replaces Prettier + ESLint)
- **Git**: Configuration files
- **Husky**: Git hooks management
- **Git**: Configuration files (confirm prompt, boolean in context)
- **Husky**: Git hooks management (requires git = true, auto-filtered)

### Repository Configuration
- **Turborepo**: Auto-enabled for multi-app projects
Expand All @@ -197,11 +210,18 @@ Template discovery and path resolution (99 lines)
- Interactive CLI with beautiful prompts (@clack/prompts)
- Multi-app configuration support (unlimited apps)
- Platform/framework/backend selection per app
- Database/ORM conditional selection
- Extras multi-selection
- **Context-aware filtering system**:
- Category-level `requires` (database → ORM dependency chain)
- Stack-level `requires` (husky → git filtering)
- Progressive context building (`ctx` updated after each prompt)
- Auto-skip prompts with informative logging
- Central `filterOptionsByContext()` for all filtering logic
- Database → ORM selection with automatic skip
- Git confirmation (boolean) feeding into extras filtering
- Extras multi-selection with auto-filter
- Template resolution engine (path mapping complete)
- Scope-aware path mapping (app/package/root)
- Zod validation for all inputs
- Unified prompt API (promptText, promptSelect, promptMultiselect, promptConfirm)
- Meta-driven stack system (easy to extend)
- Auto-detection of single-file vs. monorepo structure

Expand Down
31 changes: 17 additions & 14 deletions apps/cli/src/__meta__.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,37 @@ export const META: Meta = {
},
},
},
database: {
scope: 'root',
stacks: {
postgres: {
label: 'PostgreSQL',
hint: 'Relational database',
},
mysql: {
label: 'MySQL',
hint: 'Relational database',
},
},
},
orm: {
scope: 'package',
packageName: 'db',
requires: ['database'],
stacks: {
prisma: {
label: 'Prisma',
hint: 'Type-safe ORM with migrations',
requires: ['database'],
},
drizzle: {
label: 'Drizzle',
hint: 'Lightweight TypeScript ORM',
requires: ['database'],
},
},
},
database: {
git: {
scope: 'root',
stacks: {
postgres: {
label: 'PostgreSQL',
hint: 'Relational database',
requires: ['orm'],
},
},
stacks: {},
},
extras: {
scope: 'root',
Expand All @@ -71,11 +77,8 @@ export const META: Meta = {
label: 'Biome',
hint: 'Fast linter & formatter',
},
git: {
label: 'Git',
hint: 'Version control config',
},
husky: {
requires: ['git'],
label: 'Husky',
hint: 'Git hooks for quality checks',
},
Expand Down
112 changes: 60 additions & 52 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
/** biome-ignore-all lint/style/noNonNullAssertion: nope */

import * as p from '@clack/prompts';
import { cancel, intro, isCancel, type Option, outro, select } from '@clack/prompts';
import { META } from '@/__meta__';
import type { Config } from '@/lib/schema';
import { promptConfirm, promptMultiselect, promptSelect, promptText } from '@/lib/prompts';
import { getAllTemplatesForContext } from '@/lib/template-resolver';
import type { App, Platform, TemplateContext } from '@/types';
import { promptMultiselect, promptSelect, promptText } from './lib/prompts';
import type { App, Backend, Framework, Platform, TemplateContext } from '@/types';

async function promptPlatform(message: string): Promise<Platform> {
const result = await p.select({
const result = await select({
message,
options: [
{ value: 'web' as Platform, label: 'Web', hint: 'Next.js, Astro...' },
{ value: 'api' as Platform, label: 'API/Server', hint: 'Hono, Express...' },
{ value: 'mobile' as Platform, label: 'Mobile', hint: 'Expo, React Native' },
{ value: 'web', label: 'Web', hint: 'Next.js, Astro...' },
{ value: 'api', label: 'API/Server', hint: 'Hono, Express...' },
{ value: 'mobile', label: 'Mobile', hint: 'Expo, React Native' },
],
});

if (p.isCancel(result)) {
p.cancel('Operation cancelled');
if (isCancel(result)) {
cancel('Operation cancelled');
process.exit(0);
}

return result as Platform;
return result;
}

async function promptFrameworkForPlatform(platform: Platform, message: string): Promise<string | undefined> {
async function promptFrameworkForPlatform(platform: Platform, message: string): Promise<Framework> {
const stacks = META[platform].stacks;

const options = Object.entries(stacks).map(([value, meta]) => ({
Expand All @@ -34,26 +33,25 @@ async function promptFrameworkForPlatform(platform: Platform, message: string):
hint: meta.hint,
}));

const result = await p.select({ message, options });
const result = await select({ message, options });

if (p.isCancel(result)) {
p.cancel('Operation cancelled');
if (isCancel(result)) {
cancel('Operation cancelled');
process.exit(0);
}

return result as string;
return result;
}

async function promptBackendForApp(
appName: string,
platform: Platform,
framework: string,
): Promise<string | undefined> {
framework: Framework,
): Promise<Backend | undefined> {
const frameworkMeta = META[platform].stacks[framework];

const options: p.Option<string | undefined>[] = [];
const options: Option<Backend | undefined>[] = [];

// Si framework a backend intégré
if (frameworkMeta?.hasBackend) {
options.push({
value: 'builtin',
Expand All @@ -62,35 +60,29 @@ async function promptBackendForApp(
});
}

// Toujours proposer backends dédiés
Object.entries(META.api.stacks).forEach(([key, meta]) => {
options.push({
value: key,
label: meta.label,
hint: meta.hint,
});
options.push({ value: key, label: meta.label, hint: meta.hint });
});

// None seulement si le framework n'a pas de backend intégré
if (!frameworkMeta?.hasBackend) {
options.push({ value: undefined, label: 'None' });
}

const result = await p.select({
const result = await select({
message: `${appName} - Backend?`,
options,
});

if (p.isCancel(result)) {
p.cancel('Operation cancelled');
if (isCancel(result)) {
cancel('Operation cancelled');
process.exit(0);
}

return result as string | undefined;
return result;
}

async function promptApp(index: number): Promise<App> {
const name = await promptText(`App ${index} - Name? (folder name)`, {
const appName = await promptText(`App ${index} - Name? (folder name)`, {
defaultValue: `app-${index}`,
placeholder: `app-${index}`,
validate: (value) => {
Expand All @@ -100,45 +92,61 @@ async function promptApp(index: number): Promise<App> {

const platform = await promptPlatform(`App ${index} - Platform?`);
const framework = await promptFrameworkForPlatform(platform, `App ${index} - Framework?`);
const backend = platform !== 'api' ? await promptBackendForApp(name || 'app', platform, framework || '') : undefined;
const backend = platform !== 'api' ? await promptBackendForApp(appName!, platform, framework!) : undefined;

return { name: name!, platform, framework: framework!, backend };
return { appName: appName!, platform, framework: framework!, backend };
}

async function cli(): Promise<Config> {
p.intro('create-faster');
async function cli(): Promise<Omit<TemplateContext, 'repo'>> {
intro('create-faster');
const ctx: Omit<TemplateContext, 'repo'> = {
apps: [],
git: false,
projectName: '',
};

const name = await promptText('Project name?', {
const projectName = await promptText('Project name?', {
placeholder: 'my-app',
initialValue: 'my-app',
validate: (value) => {
if (!value || value.trim() === '') return 'Project name is required';
},
});

const appCount = await promptText<number>('How many apps?', {
initialValue: '1',
placeholder: '1',
validate: (value) => {
const num = Number(value);
if (Number.isNaN(num) || num < 1) return 'Must be a number >= 1';
ctx.projectName = projectName!;

const appCount = await promptText<number>(
'How many apps? Turborepo mode will be used if you have more than one app',
{
initialValue: '1',
placeholder: '1',
validate: (value) => {
const num = Number(value);
if (Number.isNaN(num) || num < 1) return 'Must be a number >= 1';
},
},
});

const apps: App[] = [];
);

for (let i = 0; i < Number(appCount); i++) {
const app = await promptApp(i + 1);
apps.push(app);
ctx.apps.push(app);
}

const database = await promptSelect('database', 'Database?', { allowNone: true });
const orm = await promptSelect('orm', 'ORM?', { allowNone: true, skip: !database });
const extras = await promptMultiselect('extras', 'Extras?');
const database = await promptSelect('database', 'Database?', ctx, { allowNone: true });
ctx.database = database;

const orm = await promptSelect('orm', 'ORM?', ctx, { allowNone: true });
ctx.orm = orm;

const git = await promptConfirm('Do you want to configure Git?', { initialValue: true });
ctx.git = git;

const extras = await promptMultiselect('extras', 'Extras?', ctx, { required: false });
ctx.extras = extras;

p.outro('Configuration complete!');
outro('Configuration complete!');

return { name: name!, apps, orm, database, extras };
return ctx;
}

async function main() {
Expand Down
Loading