create-reactor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/build.mjs ADDED
@@ -0,0 +1,434 @@
1
+ // Turns a config object into a concrete plan: files to write + deps to install.
2
+ import * as base from "./templates/base.mjs";
3
+ import * as app from "./templates/app.mjs";
4
+ import * as be from "./templates/backend.mjs";
5
+ import * as ex from "./templates/extras.mjs";
6
+ import * as feat from "./templates/features.mjs";
7
+ import * as quality from "./templates/quality.mjs";
8
+ import * as srv from "./templates/server.mjs";
9
+ import { statePlan } from "./templates/state.mjs";
10
+ import { securityFile } from "./templates/security.mjs";
11
+ import { agentsMd, claudeMd } from "./templates/ai-docs.mjs";
12
+ import { biomeJson, linterDevDeps } from "./templates/biome.mjs";
13
+ import { projectReadme } from "./templates/readme.mjs";
14
+ import { PM } from "./pm.mjs";
15
+
16
+ /** Linter / formatter options (single choice). */
17
+ export const LINTER_OPTIONS = [
18
+ { value: "eslint", label: "ESLint (+ Prettier)", hint: "the established standard" },
19
+ { value: "biome", label: "Biome", hint: "Rust-based, lint + format in one fast tool" },
20
+ ];
21
+
22
+ /** State management options (single choice). */
23
+ export const STATE_OPTIONS = [
24
+ { value: "zustand", label: "Zustand", hint: "lightweight hooks-based store (most popular)" },
25
+ { value: "jotai", label: "Jotai", hint: "atomic, fine-grained state" },
26
+ { value: "redux", label: "Redux Toolkit", hint: "the enterprise standard" },
27
+ { value: "none", label: "None", hint: "add state management later" },
28
+ ];
29
+
30
+ /** All valid extras, grouped for the prompts. */
31
+ export const EXTRAS = {
32
+ libraries: [
33
+ { value: "query", label: "TanStack Query", hint: "server-state / data fetching" },
34
+ { value: "table", label: "TanStack Table", hint: "headless data tables + demo" },
35
+ { value: "forms", label: "React Hook Form + Zod", hint: "forms + validation" },
36
+ { value: "charts", label: "Recharts", hint: "charts + demo" },
37
+ { value: "motion", label: "Motion (Framer Motion)", hint: "declarative animations" },
38
+ { value: "gsap", label: "GSAP", hint: "timeline animations + demo" },
39
+ { value: "editor", label: "Tiptap", hint: "rich text editor + demo" },
40
+ { value: "maps", label: "Leaflet", hint: "interactive maps + demo (no API key)" },
41
+ { value: "dates", label: "date-fns", hint: "date utilities" },
42
+ { value: "nuqs", label: "nuqs", hint: "type-safe URL search-param state" },
43
+ { value: "redis", label: "Upstash Redis", hint: "rate limiting / caching (server-side)" },
44
+ ],
45
+ features: [
46
+ { value: "stripe", label: "Stripe", hint: "payments (client SDK + env wiring)" },
47
+ { value: "resend", label: "Resend + React Email", hint: "transactional email templates" },
48
+ { value: "posthog", label: "PostHog", hint: "product analytics + session replay" },
49
+ { value: "i18n", label: "react-i18next", hint: "internationalization" },
50
+ { value: "pwa", label: "PWA", hint: "installable app + offline (vite-plugin-pwa)" },
51
+ { value: "deploy", label: "Deploy configs", hint: "Dockerfile + Vercel + Netlify" },
52
+ ],
53
+ tooling: [
54
+ { value: "testing", label: "Vitest + Testing Library", hint: "unit/component tests" },
55
+ { value: "e2e", label: "Playwright", hint: "end-to-end browser tests" },
56
+ { value: "msw", label: "MSW", hint: "API mocking for tests (needs Vitest)" },
57
+ { value: "storybook", label: "Storybook", hint: "component workshop (heavy install)" },
58
+ { value: "prettier", label: "Prettier", hint: "+ Tailwind class sorting (ESLint only)" },
59
+ { value: "husky", label: "Husky + lint-staged", hint: "lint/format on git commit" },
60
+ { value: "ci", label: "GitHub Actions CI", hint: "lint + build + test workflow" },
61
+ { value: "sentry", label: "Sentry", hint: "error monitoring" },
62
+ { value: "fallow", label: "Fallow", hint: "dead code + duplication + complexity + PR audit (Rust)" },
63
+ { value: "knip", label: "Knip", hint: "unused files/exports/deps (lighter alternative to Fallow)" },
64
+ ],
65
+ };
66
+
67
+ /** Database provider options (asked when an ORM is selected). */
68
+ export const DB_PROVIDERS = {
69
+ drizzle: ["neon", "docker", "turso", "supabase", "other"],
70
+ prisma: ["neon", "docker", "supabase", "other"],
71
+ };
72
+
73
+ export const ALL_EXTRAS = [...EXTRAS.libraries, ...EXTRAS.features, ...EXTRAS.tooling].map(
74
+ (e) => e.value,
75
+ );
76
+
77
+ /** Always installed so the starter UI works out of the box. */
78
+ export const ESSENTIAL_COMPONENTS = ["button", "card", "input", "badge"];
79
+
80
+ /** Offered as optional picks in the prompt. */
81
+ export const OPTIONAL_COMPONENTS = [
82
+ "label",
83
+ "dialog",
84
+ "dropdown-menu",
85
+ "sonner",
86
+ "tabs",
87
+ "avatar",
88
+ "skeleton",
89
+ "select",
90
+ "checkbox",
91
+ "switch",
92
+ "textarea",
93
+ "tooltip",
94
+ "separator",
95
+ "sheet",
96
+ "table",
97
+ ];
98
+
99
+ /** Add derived helpers (command labels for the chosen package manager). */
100
+ export function enrichConfig(raw) {
101
+ const pm = PM[raw.pm];
102
+ return {
103
+ ...raw,
104
+ pmRunLabel: (s) => pm.runLabel(s),
105
+ pmDlxLabel: (p) => pm.dlxLabel(p),
106
+ pmInstallLabel: `${pm.install[0]} ${pm.install[1].join(" ")}`,
107
+ };
108
+ }
109
+
110
+ /** Validate cross-option constraints. Returns an error string or null. */
111
+ export function validateConfig(c) {
112
+ if (c.auth === "convex-auth" && c.backend !== "convex") {
113
+ return "Convex Auth requires the Convex backend (--backend convex).";
114
+ }
115
+ if (c.auth === "better-auth" && c.backend !== "convex") {
116
+ return "Better Auth is currently wired through the Convex backend (--backend convex).";
117
+ }
118
+ if (c.auth === "supabase-auth" && c.backend !== "supabase") {
119
+ return "Supabase Auth requires the Supabase backend (--backend supabase).";
120
+ }
121
+ if (c.orm !== "none" && c.backend === "convex") {
122
+ return "Convex has its own database — Drizzle/Prisma only apply to other backends.";
123
+ }
124
+ if (c.dbProvider === "turso" && c.orm !== "drizzle") {
125
+ return "Turso requires Drizzle (--orm drizzle).";
126
+ }
127
+ if (c.dbProvider === "supabase" && c.backend !== "supabase") {
128
+ return "The Supabase database provider requires the Supabase backend.";
129
+ }
130
+ if (c.extras.includes("msw") && !c.extras.includes("testing")) {
131
+ return "MSW requires Vitest (add 'testing' to your extras).";
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /** Build the full plan: { files: Map<relPath, content>, deps: string[], devDeps: string[] }. */
137
+ export function buildPlan(c) {
138
+ const files = new Map();
139
+ const deps = ["react", "react-dom"];
140
+ const devDeps = [
141
+ "typescript",
142
+ "vite",
143
+ "@vitejs/plugin-react",
144
+ "@types/react",
145
+ "@types/react-dom",
146
+ "@types/node",
147
+ "tailwindcss",
148
+ "@tailwindcss/vite",
149
+ "tw-animate-css",
150
+ // linter / formatter (ESLint stack or Biome)
151
+ ...linterDevDeps(c),
152
+ ];
153
+
154
+ // shadcn/ui core runtime deps (components themselves are added via the shadcn CLI)
155
+ deps.push("clsx", "tailwind-merge", "class-variance-authority", "lucide-react");
156
+
157
+ // ---- base files -----------------------------------------------------------
158
+ files.set("package.json", base.pkgJson(c));
159
+ files.set("index.html", base.indexHtml(c));
160
+ files.set("public/favicon.svg", base.favicon());
161
+ files.set("vite.config.ts", base.viteConfig(c));
162
+ files.set("tsconfig.json", base.tsconfig());
163
+ files.set("tsconfig.app.json", base.tsconfigApp(c));
164
+ files.set("tsconfig.node.json", base.tsconfigNode(c));
165
+ if (c.linter === "biome") {
166
+ files.set("biome.json", biomeJson(c));
167
+ } else {
168
+ files.set("eslint.config.js", base.eslintConfig(c));
169
+ }
170
+ files.set(".gitignore", base.gitignore(c));
171
+ // AI-assistant instructions (Claude Code, Cursor, Copilot, ...)
172
+ files.set("AGENTS.md", agentsMd(c));
173
+ files.set("CLAUDE.md", claudeMd());
174
+ files.set("src/index.css", base.indexCss());
175
+ files.set("components.json", base.componentsJson());
176
+ files.set("src/lib/utils.ts", base.libUtils());
177
+ files.set("src/vite-env.d.ts", base.viteEnvDts(c));
178
+ files.set("README.md", projectReadme(c));
179
+
180
+ const envLocalContent = base.envLocal(c);
181
+ if (envLocalContent) files.set(".env.local", envLocalContent);
182
+ const envExampleContent = base.envExample(c);
183
+ if (envExampleContent) files.set(".env.example", envExampleContent);
184
+ const needsServerEnv =
185
+ c.orm !== "none" || ["redis", "stripe", "resend"].some((e) => c.extras.includes(e));
186
+ if (needsServerEnv) files.set(".env", base.envDatabase(c));
187
+
188
+ // Supply-chain protection: 7-day minimum package release age
189
+ if (c.secure) {
190
+ const sec = securityFile(c.pm);
191
+ if (sec) files.set(sec.filename, sec.content);
192
+ }
193
+
194
+ // ---- app source files -----------------------------------------------------
195
+ files.set("src/main.tsx", app.mainTsx(c));
196
+ files.set("src/components/header.tsx", app.header(c));
197
+ files.set("src/components/theme-toggle.tsx", app.themeToggle());
198
+ files.set("src/components/home.tsx", app.home(c));
199
+
200
+ // ---- routing --------------------------------------------------------------
201
+ if (c.router === "tanstack") {
202
+ deps.push("@tanstack/react-router", "@tanstack/react-router-devtools");
203
+ devDeps.push("@tanstack/router-plugin");
204
+ files.set("src/routes/__root.tsx", app.rootRoute());
205
+ files.set("src/routes/index.tsx", app.indexRoute());
206
+ files.set("src/routes/about.tsx", app.aboutRoute());
207
+ } else {
208
+ files.set("src/App.tsx", app.appTsx(c));
209
+ if (c.router === "react-router") deps.push("react-router");
210
+ }
211
+
212
+ // ---- backend ---------------------------------------------------------------
213
+ if (c.backend === "convex") {
214
+ deps.push("convex");
215
+ devDeps.push("npm-run-all2");
216
+ files.set("convex/tsconfig.json", be.convexTsconfig());
217
+ files.set("convex/schema.ts", be.convexSchema(c));
218
+ files.set("convex/tasks.ts", be.convexTasks());
219
+ files.set("sampleData.jsonl", be.convexSampleData());
220
+ files.set("convex/_generated/api.d.ts", be.convexGeneratedApiDts(c));
221
+ files.set("convex/_generated/api.js", be.convexGeneratedApiJs());
222
+ files.set("convex/_generated/dataModel.d.ts", be.convexGeneratedDataModelDts());
223
+ files.set("convex/_generated/server.d.ts", be.convexGeneratedServerDts());
224
+ files.set("convex/_generated/server.js", be.convexGeneratedServerJs());
225
+ files.set("src/components/tasks-demo.tsx", app.tasksDemo(c));
226
+ const httpTs = be.convexHttp(c);
227
+ if (httpTs) files.set("convex/http.ts", httpTs);
228
+ const authConfig = be.convexAuthConfig(c);
229
+ if (authConfig) files.set("convex/auth.config.ts", authConfig);
230
+ }
231
+ if (c.backend === "supabase") {
232
+ deps.push("@supabase/supabase-js");
233
+ files.set("src/lib/supabase.ts", be.supabaseClient());
234
+ }
235
+ if (c.backend === "hono") {
236
+ deps.push(
237
+ "hono",
238
+ "@hono/node-server",
239
+ "@hono/trpc-server",
240
+ "@trpc/server",
241
+ "@trpc/client",
242
+ "@trpc/tanstack-react-query",
243
+ "@tanstack/react-query",
244
+ "zod",
245
+ );
246
+ devDeps.push("tsx", "npm-run-all2");
247
+ files.set("server/router.ts", srv.trpcRouter());
248
+ files.set("server/index.ts", srv.honoServer());
249
+ files.set("src/lib/trpc.ts", srv.trpcClient());
250
+ files.set("src/components/trpc-demo.tsx", srv.trpcDemo(c));
251
+ }
252
+
253
+ // ---- auth ------------------------------------------------------------------
254
+ if (c.auth !== "none") {
255
+ files.set("src/components/auth-buttons.tsx", app.authButtons(c));
256
+ }
257
+ if (c.auth === "clerk") {
258
+ deps.push("@clerk/clerk-react");
259
+ }
260
+ if (c.auth === "convex-auth") {
261
+ deps.push("@convex-dev/auth", "@auth/core");
262
+ files.set("convex/auth.ts", be.convexAuthTs());
263
+ files.set("src/components/sign-in-form.tsx", app.signInForm());
264
+ }
265
+ if (c.auth === "better-auth") {
266
+ deps.push("@convex-dev/better-auth", "better-auth");
267
+ files.set("convex/convex.config.ts", be.convexConfigBetterAuth());
268
+ files.set("convex/auth.ts", be.convexBetterAuthTs());
269
+ files.set("src/lib/auth-client.ts", be.betterAuthClient());
270
+ files.set("src/components/sign-in-form.tsx", app.signInFormBetterAuth());
271
+ }
272
+ if (c.auth === "supabase-auth") {
273
+ files.set("src/hooks/use-session.ts", app.useSessionHook());
274
+ }
275
+
276
+ // ---- ORM + database provider ------------------------------------------------
277
+ if (c.orm === "drizzle") {
278
+ deps.push(...be.drizzleDeps(c));
279
+ // dotenv is only used by drizzle.config.ts (a dev-time tool), so it's a devDep
280
+ devDeps.push("drizzle-kit", "dotenv");
281
+ files.set("drizzle.config.ts", be.drizzleConfig(c));
282
+ files.set("db/schema.ts", be.drizzleSchema(c));
283
+ files.set("db/index.ts", be.drizzleIndex(c));
284
+ }
285
+ if (c.orm === "prisma") {
286
+ deps.push("@prisma/client");
287
+ devDeps.push("prisma");
288
+ // prisma/schema.prisma is created by `prisma init` post-step (or fallback file)
289
+ }
290
+ if (c.dbProvider === "docker") {
291
+ files.set("docker-compose.yml", be.dockerCompose(c));
292
+ }
293
+
294
+ // ---- AI SDK ----------------------------------------------------------------
295
+ if (c.ai !== "none") {
296
+ const ai = be.AI_MODELS[c.ai];
297
+ deps.push("ai", "@ai-sdk/react", ai.pkg);
298
+ if (c.backend === "convex") {
299
+ files.set("convex/chat.ts", be.convexChat(c));
300
+ files.set("src/components/chat-demo.tsx", app.chatDemo(c));
301
+ } else {
302
+ devDeps.push("tsx");
303
+ files.set("examples/ai.ts", be.aiExample(c));
304
+ }
305
+ }
306
+
307
+ // ---- state management --------------------------------------------------------
308
+ const state = statePlan(c.state ?? "none");
309
+ deps.push(...state.deps);
310
+ for (const [rel, content] of Object.entries(state.files)) files.set(rel, content);
311
+
312
+ // ---- library extras -----------------------------------------------------------
313
+ if (c.extras.includes("query") && c.backend !== "hono") {
314
+ // (hono backend already includes @tanstack/react-query for tRPC)
315
+ deps.push("@tanstack/react-query");
316
+ }
317
+ if (c.extras.includes("table")) {
318
+ deps.push("@tanstack/react-table");
319
+ files.set("src/components/data-table-demo.tsx", ex.dataTableDemo());
320
+ }
321
+ if (c.extras.includes("forms")) {
322
+ deps.push("react-hook-form", "zod", "@hookform/resolvers");
323
+ }
324
+ if (c.extras.includes("charts")) {
325
+ deps.push("recharts");
326
+ files.set("src/components/chart-demo.tsx", feat.chartDemo());
327
+ }
328
+ if (c.extras.includes("motion")) {
329
+ deps.push("motion");
330
+ files.set("src/components/fade-in.tsx", ex.fadeIn());
331
+ }
332
+ if (c.extras.includes("gsap")) {
333
+ deps.push("gsap", "@gsap/react");
334
+ files.set("src/components/gsap-demo.tsx", feat.gsapDemo());
335
+ }
336
+ if (c.extras.includes("editor")) {
337
+ deps.push("@tiptap/react", "@tiptap/pm", "@tiptap/starter-kit");
338
+ files.set("src/components/editor-demo.tsx", feat.editorDemo());
339
+ }
340
+ if (c.extras.includes("maps")) {
341
+ deps.push("leaflet", "react-leaflet");
342
+ devDeps.push("@types/leaflet");
343
+ files.set("src/components/map-demo.tsx", feat.mapDemo());
344
+ }
345
+ if (c.extras.includes("dates")) {
346
+ deps.push("date-fns");
347
+ }
348
+ if (c.extras.includes("nuqs")) {
349
+ deps.push("nuqs");
350
+ }
351
+ if (c.extras.includes("redis")) {
352
+ deps.push("@upstash/redis", "@upstash/ratelimit");
353
+ files.set("db/redis.ts", be.upstashRedis());
354
+ files.set("db/ratelimit.ts", be.upstashRatelimit());
355
+ }
356
+
357
+ // ---- feature extras -----------------------------------------------------------
358
+ if (c.extras.includes("stripe")) {
359
+ deps.push("@stripe/stripe-js");
360
+ files.set("src/lib/stripe.ts", feat.stripeLib());
361
+ }
362
+ if (c.extras.includes("resend")) {
363
+ deps.push("resend", "@react-email/components");
364
+ files.set("src/emails/welcome.tsx", feat.welcomeEmail(c));
365
+ }
366
+ if (c.extras.includes("posthog")) {
367
+ deps.push("posthog-js");
368
+ files.set("src/lib/posthog.ts", feat.posthogInit());
369
+ }
370
+ if (c.extras.includes("i18n")) {
371
+ deps.push("i18next", "react-i18next");
372
+ files.set("src/lib/i18n.ts", feat.i18nSetup());
373
+ }
374
+ if (c.extras.includes("pwa")) {
375
+ devDeps.push("vite-plugin-pwa");
376
+ }
377
+ if (c.extras.includes("deploy")) {
378
+ files.set("Dockerfile", feat.dockerfile(c));
379
+ files.set("nginx.conf", feat.nginxConf());
380
+ files.set(".dockerignore", feat.dockerignore());
381
+ files.set("vercel.json", feat.vercelJson());
382
+ files.set("netlify.toml", feat.netlifyToml());
383
+ }
384
+
385
+ // ---- tooling extras -----------------------------------------------------------
386
+ if (c.extras.includes("testing")) {
387
+ devDeps.push(
388
+ "vitest",
389
+ "jsdom",
390
+ "@testing-library/react",
391
+ "@testing-library/jest-dom",
392
+ "@testing-library/user-event",
393
+ );
394
+ files.set("src/test/setup.ts", quality.testSetup(c));
395
+ files.set("src/test/button.test.tsx", ex.sampleTest());
396
+ files.set("src/lib/utils.test.ts", ex.utilsTest());
397
+ }
398
+ if (c.extras.includes("msw")) {
399
+ devDeps.push("msw");
400
+ files.set("src/test/mocks/handlers.ts", quality.mswHandlers());
401
+ files.set("src/test/mocks/server.ts", quality.mswServer());
402
+ }
403
+ if (c.extras.includes("e2e")) {
404
+ devDeps.push("@playwright/test");
405
+ files.set("playwright.config.ts", quality.playwrightConfig(c));
406
+ files.set("e2e/home.spec.ts", quality.playwrightExampleTest(c));
407
+ }
408
+ // (storybook has no files here — it's set up by `storybook init` as a post-step)
409
+ if (c.extras.includes("prettier") && c.linter !== "biome") {
410
+ // (deps are added by linterDevDeps; Biome formats on its own, so Prettier is skipped)
411
+ files.set(".prettierrc", base.prettierrc());
412
+ files.set(".prettierignore", base.prettierignore());
413
+ }
414
+ if (c.extras.includes("knip")) {
415
+ devDeps.push("knip");
416
+ }
417
+ if (c.extras.includes("fallow")) {
418
+ devDeps.push("fallow");
419
+ files.set(".fallowrc.json", quality.fallowConfig(c));
420
+ }
421
+ if (c.extras.includes("husky")) {
422
+ devDeps.push("husky", "lint-staged");
423
+ files.set(".husky/pre-commit", ex.huskyPreCommit(c));
424
+ }
425
+ if (c.extras.includes("ci")) {
426
+ files.set(".github/workflows/ci.yml", ex.githubCi(c));
427
+ }
428
+ if (c.extras.includes("sentry")) {
429
+ deps.push("@sentry/react");
430
+ files.set("src/lib/sentry.ts", ex.sentryInit());
431
+ }
432
+
433
+ return { files, deps, devDeps };
434
+ }
package/lib/pm.mjs ADDED
@@ -0,0 +1,85 @@
1
+ // Package manager detection, command mapping, and process execution helpers.
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ const IS_WIN = process.platform === "win32";
5
+
6
+ /** Quote an argument for cmd.exe (only when needed). */
7
+ function quoteArg(a) {
8
+ if (a === "") return '""';
9
+ if (/^[A-Za-z0-9_\-./@:=+,]+$/.test(a)) return a;
10
+ return '"' + a.replace(/"/g, '""') + '"';
11
+ }
12
+
13
+ /** Run a command, return { ok, stdout, stderr, status }. Never throws. */
14
+ export function run(cmd, args, opts = {}) {
15
+ // On Windows we need a shell to resolve .cmd shims (npx, pnpm). Joining the
16
+ // command into a single pre-quoted string avoids Node's DEP0190 warning.
17
+ const spawnCmd = IS_WIN ? [cmd, ...args].map(quoteArg).join(" ") : cmd;
18
+ const spawnArgs = IS_WIN ? undefined : args;
19
+ const res = spawnSync(spawnCmd, spawnArgs, {
20
+ cwd: opts.cwd,
21
+ shell: IS_WIN,
22
+ stdio: opts.interactive ? "inherit" : "pipe",
23
+ encoding: "utf8",
24
+ env: { ...process.env, FORCE_COLOR: "0", CI: "true", ...opts.env },
25
+ timeout: opts.timeout ?? 10 * 60 * 1000,
26
+ });
27
+ return {
28
+ ok: res.status === 0,
29
+ status: res.status,
30
+ stdout: res.stdout ?? "",
31
+ stderr: res.stderr ?? "",
32
+ error: res.error,
33
+ };
34
+ }
35
+
36
+ /** Check whether a CLI tool exists on PATH. */
37
+ export function hasCommand(cmd) {
38
+ const res = run(cmd, ["--version"], { timeout: 15_000 });
39
+ return res.ok;
40
+ }
41
+
42
+ /** Detect which package managers are installed. */
43
+ export function detectPackageManagers() {
44
+ const found = [];
45
+ for (const pm of ["bun", "pnpm", "npm"]) {
46
+ if (hasCommand(pm)) found.push(pm);
47
+ }
48
+ return found;
49
+ }
50
+
51
+ /** Command fragments per package manager. */
52
+ export const PM = {
53
+ bun: {
54
+ install: ["bun", ["install"]],
55
+ add: (pkgs) => ["bun", ["add", ...pkgs]],
56
+ addDev: (pkgs) => ["bun", ["add", "-d", ...pkgs]],
57
+ // `bun x` rather than `bunx`: the bunx shim doesn't exist in every bun
58
+ // install (e.g. chocolatey on Windows), but `bun x` always works.
59
+ dlx: (pkg, args) => ["bun", ["x", "--bun", pkg, ...args]],
60
+ run: (script) => ["bun", ["run", script]],
61
+ execName: "bun x",
62
+ runLabel: (script) => `bun run ${script}`,
63
+ dlxLabel: (pkg) => `bun x ${pkg}`,
64
+ },
65
+ pnpm: {
66
+ install: ["pnpm", ["install"]],
67
+ add: (pkgs) => ["pnpm", ["add", ...pkgs]],
68
+ addDev: (pkgs) => ["pnpm", ["add", "-D", ...pkgs]],
69
+ dlx: (pkg, args) => ["pnpm", ["dlx", pkg, ...args]],
70
+ run: (script) => ["pnpm", ["run", script]],
71
+ execName: "pnpm dlx",
72
+ runLabel: (script) => `pnpm ${script}`,
73
+ dlxLabel: (pkg) => `pnpm dlx ${pkg}`,
74
+ },
75
+ npm: {
76
+ install: ["npm", ["install"]],
77
+ add: (pkgs) => ["npm", ["install", ...pkgs]],
78
+ addDev: (pkgs) => ["npm", ["install", "-D", ...pkgs]],
79
+ dlx: (pkg, args) => ["npx", ["-y", pkg, ...args]],
80
+ run: (script) => ["npm", ["run", script]],
81
+ execName: "npx",
82
+ runLabel: (script) => `npm run ${script}`,
83
+ dlxLabel: (pkg) => `npx ${pkg}`,
84
+ },
85
+ };
@@ -0,0 +1,122 @@
1
+ // Presets: answer one question instead of twelve.
2
+ // Each preset is a complete config (minus name/pm/git/install which are always asked).
3
+
4
+ export const ESSENTIAL_COMPONENTS_LIST = ["button", "card", "input", "badge"];
5
+
6
+ export const PRESETS = {
7
+ minimal: {
8
+ label: "Minimal",
9
+ hint: "Vite + TS + Tailwind + shadcn + TanStack Router — nothing else",
10
+ config: {
11
+ backend: "none",
12
+ orm: "none",
13
+ dbProvider: "none",
14
+ auth: "none",
15
+ router: "tanstack",
16
+ ai: "none",
17
+ state: "none",
18
+ components: ["label"],
19
+ extras: ["prettier"],
20
+ },
21
+ },
22
+ saas: {
23
+ label: "SaaS",
24
+ hint: "Convex + Clerk + Stripe + Resend + PostHog + Sentry + tests + CI",
25
+ config: {
26
+ backend: "convex",
27
+ orm: "none",
28
+ dbProvider: "none",
29
+ auth: "clerk",
30
+ router: "tanstack",
31
+ ai: "none",
32
+ state: "zustand",
33
+ components: ["label", "dialog", "sonner", "dropdown-menu", "avatar", "tabs", "table", "skeleton"],
34
+ extras: [
35
+ "query",
36
+ "table",
37
+ "forms",
38
+ "charts",
39
+ "stripe",
40
+ "resend",
41
+ "posthog",
42
+ "sentry",
43
+ "testing",
44
+ "e2e",
45
+ "prettier",
46
+ "husky",
47
+ "ci",
48
+ "deploy",
49
+ "fallow",
50
+ ],
51
+ },
52
+ },
53
+ fullstack: {
54
+ label: "Full-stack API",
55
+ hint: "Hono + tRPC + Drizzle + Neon + Clerk + Biome + tests + CI",
56
+ config: {
57
+ backend: "hono",
58
+ orm: "drizzle",
59
+ dbProvider: "neon",
60
+ auth: "clerk",
61
+ router: "tanstack",
62
+ ai: "none",
63
+ state: "zustand",
64
+ linter: "biome",
65
+ components: ["label", "dialog", "sonner", "table"],
66
+ extras: ["forms", "table", "testing", "husky", "ci", "deploy", "fallow"],
67
+ },
68
+ },
69
+ ai: {
70
+ label: "AI app",
71
+ hint: "Convex + Clerk + AI SDK (Claude) streaming chat",
72
+ config: {
73
+ backend: "convex",
74
+ orm: "none",
75
+ dbProvider: "none",
76
+ auth: "clerk",
77
+ router: "tanstack",
78
+ ai: "anthropic",
79
+ state: "none",
80
+ components: ["label", "dialog", "sonner", "skeleton", "textarea"],
81
+ extras: ["query", "testing", "prettier", "ci"],
82
+ },
83
+ },
84
+ everything: {
85
+ label: "Everything",
86
+ hint: "Every feature and every extra — the kitchen sink",
87
+ config: {
88
+ backend: "convex",
89
+ orm: "none",
90
+ dbProvider: "none",
91
+ auth: "clerk",
92
+ router: "tanstack",
93
+ ai: "anthropic",
94
+ state: "zustand",
95
+ components: [
96
+ "label",
97
+ "dialog",
98
+ "dropdown-menu",
99
+ "sonner",
100
+ "tabs",
101
+ "avatar",
102
+ "skeleton",
103
+ "select",
104
+ "checkbox",
105
+ "switch",
106
+ "textarea",
107
+ "tooltip",
108
+ "separator",
109
+ "table",
110
+ ],
111
+ // extras filled in dynamically with ALL_EXTRAS by the caller
112
+ extras: "all",
113
+ },
114
+ },
115
+ custom: {
116
+ label: "Custom",
117
+ hint: "choose every option yourself",
118
+ config: null,
119
+ },
120
+ };
121
+
122
+ export const PRESET_NAMES = Object.keys(PRESETS);