create-rudder-app 0.9.2 → 0.10.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/dist/index.js CHANGED
@@ -1,21 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import { intro, outro, text, select, multiselect, groupMultiselect, confirm, spinner, log, isCancel, cancel, } from '@clack/prompts';
3
3
  import fs from 'node:fs/promises';
4
+ import os from 'node:os';
4
5
  import path from 'node:path';
5
6
  import { spawn } from 'node:child_process';
6
7
  import { randomBytes } from 'node:crypto';
7
8
  import { createRequire } from 'node:module';
8
9
  import { getTemplates, detectPackageManager, pmExec, pmRun, pmInstall } from './templates.js';
9
10
  import { availableDemos } from './templates/demos/registry.js';
10
- async function main() {
11
- const argName = process.argv[2];
12
- const pm = detectPackageManager();
13
- console.log();
14
- intro(' create-rudder-app ');
15
- // ── Project name ───────────────────────────────────────
16
- let name;
17
- if (argName) {
18
- name = argName;
11
+ import { detectAgent } from './agent-detect.js';
12
+ import { parseFlags, validateJsonMode, resolveJsonAnswers, packagesFromList, FlagError, DB_GATED, } from './cli-flags.js';
13
+ // ──────────────────────────────────────────────────────────────
14
+ // Interactive prompt flow — only prompts for what's missing
15
+ // ──────────────────────────────────────────────────────────────
16
+ async function gatherInteractive(name, p) {
17
+ let resolvedName;
18
+ if (p.name)
19
+ resolvedName = p.name;
20
+ else if (name) {
21
+ resolvedName = name;
19
22
  console.log(` Project name: ${name}`);
20
23
  }
21
24
  else {
@@ -28,25 +31,28 @@ async function main() {
28
31
  cancel('Cancelled.');
29
32
  process.exit(0);
30
33
  }
31
- name = answer.trim();
34
+ resolvedName = answer.trim();
32
35
  }
33
- // ── Database ORM ─────────────────────────────────────────
34
- const ormAnswer = await select({
35
- message: 'Database ORM',
36
- options: [
37
- { value: 'prisma', label: 'Prisma' },
38
- { value: 'drizzle', label: 'Drizzle' },
39
- { value: 'none', label: 'None', hint: 'no database' },
40
- ],
41
- });
42
- if (isCancel(ormAnswer)) {
43
- cancel('Cancelled.');
44
- process.exit(0);
36
+ let orm;
37
+ if (p.orm !== undefined)
38
+ orm = p.orm;
39
+ else {
40
+ const ormAnswer = await select({
41
+ message: 'Database ORM',
42
+ options: [
43
+ { value: 'prisma', label: 'Prisma' },
44
+ { value: 'drizzle', label: 'Drizzle' },
45
+ { value: 'none', label: 'None', hint: 'no database' },
46
+ ],
47
+ });
48
+ if (isCancel(ormAnswer)) {
49
+ cancel('Cancelled.');
50
+ process.exit(0);
51
+ }
52
+ orm = ormAnswer === 'none' ? false : ormAnswer;
45
53
  }
46
- const orm = ormAnswer === 'none' ? false : ormAnswer;
47
- // ── Database driver (only if ORM selected) ───────────────
48
- let db = 'sqlite';
49
- if (orm) {
54
+ let db = p.db ?? 'sqlite';
55
+ if (orm && p.db === undefined) {
50
56
  const dbAnswer = await select({
51
57
  message: 'Database driver',
52
58
  options: [
@@ -61,133 +67,105 @@ async function main() {
61
67
  }
62
68
  db = dbAnswer;
63
69
  }
64
- // ── Package checklist ────────────────────────────────────
65
- //
66
- // Categorized via clack's groupMultiselect — one section per concern,
67
- // group headers are non-selectable (selectableGroups: false).
68
- // ORM=none filters out DB-dependent packages (auth/sanctum/passport)
69
- // before the prompt renders, so the user never sees rows they can't use.
70
- // Database-dependent packages hidden when ORM=none.
71
- const DB_GATED = new Set(['auth', 'sanctum', 'passport']);
72
- const PACKAGE_GROUPS = {
73
- 'Auth & Security': [
74
- { value: 'auth', label: 'Authentication', hint: 'login, register, sessions' },
75
- { value: 'sanctum', label: 'Sanctum', hint: 'API tokens (SHA-256 + abilities)' },
76
- { value: 'passport', label: 'Passport', hint: 'OAuth2 server — requires Auth + Prisma' },
77
- { value: 'socialite', label: 'Socialite', hint: 'social login: GitHub, Google, Facebook, Apple' },
78
- { value: 'crypt', label: 'Crypt', hint: 'AES-256-CBC + HMAC encryption' },
79
- ],
80
- 'Infrastructure': [
81
- { value: 'queue', label: 'Queue', hint: 'background jobs' },
82
- { value: 'storage', label: 'Storage', hint: 'file uploads (local + S3)' },
83
- { value: 'scheduler', label: 'Scheduler', hint: 'cron-like task scheduling' },
84
- ],
85
- 'Communication': [
86
- { value: 'mail', label: 'Mail', hint: 'SMTP + log driver' },
87
- { value: 'notifications', label: 'Notifications', hint: 'multi-channel notifications' },
88
- { value: 'broadcast', label: 'WebSocket / Broadcast', hint: 'real-time channels' },
89
- { value: 'sync', label: 'Sync (Yjs CRDT)', hint: 'collaborative documents' },
90
- ],
91
- 'Internationalization': [
92
- { value: 'localization', label: 'Localization', hint: 'i18ntrans(), setLocale()' },
93
- ],
94
- 'Developer Experience': [
95
- { value: 'pennant', label: 'Pennant', hint: 'feature flags' },
96
- { value: 'http', label: 'HTTP', hint: 'fluent fetch client — retries, timeouts, pools' },
97
- { value: 'process', label: 'Process', hint: 'shell execution — run, pool, pipe' },
98
- { value: 'concurrency', label: 'Concurrency', hint: 'parallel execution via worker threads' },
99
- ],
100
- 'Media': [
101
- { value: 'image', label: 'Image', hint: 'resize, crop, convert (sharp wrapper)' },
102
- ],
103
- 'Observability': [
104
- { value: 'telescope', label: 'Telescope', hint: 'debug dashboard — requests, queries, jobs, exceptions' },
105
- { value: 'pulse', label: 'Pulse', hint: 'metrics dashboard — throughput, latency, hit rates' },
106
- { value: 'horizon', label: 'Horizon', hint: 'queue monitoring lifecycle, workers, retry/delete' },
107
- ],
108
- 'AI & Tooling': [
109
- { value: 'ai', label: 'AI', hint: 'LLM providers (Anthropic, OpenAI, Google, Ollama)' },
110
- { value: 'mcp', label: 'MCP', hint: 'Model Context Protocol — expose tools/resources to LLMs' },
111
- { value: 'boost', label: 'Boost', hint: 'AI coding DX (Claude Code/Cursor/Copilot)' },
112
- ],
113
- };
114
- if (orm === false) {
115
- log.info('Database not selected auth, sanctum, and passport options are hidden.');
116
- }
117
- const groupedOptions = {};
118
- for (const [group, pkgs] of Object.entries(PACKAGE_GROUPS)) {
119
- const visible = orm === false ? pkgs.filter(p => !DB_GATED.has(p.value)) : pkgs;
120
- if (visible.length > 0)
121
- groupedOptions[group] = visible;
122
- }
123
- const packageAnswer = await groupMultiselect({
124
- message: 'Select packages',
125
- options: groupedOptions,
126
- initialValues: orm === false ? [] : ['auth'],
127
- required: false,
128
- selectableGroups: false,
129
- });
130
- if (isCancel(packageAnswer)) {
131
- cancel('Cancelled.');
132
- process.exit(0);
70
+ let packages;
71
+ if (p.packages !== undefined)
72
+ packages = p.packages;
73
+ else {
74
+ const PACKAGE_GROUPS = {
75
+ 'Auth & Security': [
76
+ { value: 'auth', label: 'Authentication', hint: 'login, register, sessions' },
77
+ { value: 'sanctum', label: 'Sanctum', hint: 'API tokens (SHA-256 + abilities)' },
78
+ { value: 'passport', label: 'Passport', hint: 'OAuth2 server — requires Auth + Prisma' },
79
+ { value: 'socialite', label: 'Socialite', hint: 'social login: GitHub, Google, Facebook, Apple' },
80
+ { value: 'crypt', label: 'Crypt', hint: 'AES-256-CBC + HMAC encryption' },
81
+ ],
82
+ 'Infrastructure': [
83
+ { value: 'queue', label: 'Queue', hint: 'background jobs' },
84
+ { value: 'storage', label: 'Storage', hint: 'file uploads (local + S3)' },
85
+ { value: 'scheduler', label: 'Scheduler', hint: 'cron-like task scheduling' },
86
+ ],
87
+ 'Communication': [
88
+ { value: 'mail', label: 'Mail', hint: 'SMTP + log driver' },
89
+ { value: 'notifications', label: 'Notifications', hint: 'multi-channel notifications' },
90
+ { value: 'broadcast', label: 'WebSocket / Broadcast', hint: 'real-time channels' },
91
+ { value: 'sync', label: 'Sync (Yjs CRDT)', hint: 'collaborative documents' },
92
+ ],
93
+ 'Internationalization': [
94
+ { value: 'localization', label: 'Localization', hint: 'i18n — trans(), setLocale()' },
95
+ ],
96
+ 'Developer Experience': [
97
+ { value: 'pennant', label: 'Pennant', hint: 'feature flags' },
98
+ { value: 'http', label: 'HTTP', hint: 'fluent fetch client retries, timeouts, pools' },
99
+ { value: 'process', label: 'Process', hint: 'shell execution — run, pool, pipe' },
100
+ { value: 'concurrency', label: 'Concurrency', hint: 'parallel execution via worker threads' },
101
+ { value: 'terminal', label: 'Terminal', hint: 'rich terminal UIs from CLI commands (Ink)' },
102
+ ],
103
+ 'Media': [
104
+ { value: 'image', label: 'Image', hint: 'resize, crop, convert (sharp wrapper)' },
105
+ ],
106
+ 'Observability': [
107
+ { value: 'telescope', label: 'Telescope', hint: 'debug dashboard — requests, queries, jobs, exceptions' },
108
+ { value: 'pulse', label: 'Pulse', hint: 'metrics dashboard — throughput, latency, hit rates' },
109
+ { value: 'horizon', label: 'Horizon', hint: 'queue monitoring — lifecycle, workers, retry/delete' },
110
+ ],
111
+ 'AI & Tooling': [
112
+ { value: 'ai', label: 'AI', hint: 'LLM providers (Anthropic, OpenAI, Google, Ollama)' },
113
+ { value: 'mcp', label: 'MCP', hint: 'Model Context Protocol — expose tools/resources to LLMs' },
114
+ { value: 'boost', label: 'Boost', hint: 'AI coding DX (Claude Code/Cursor/Copilot)' },
115
+ ],
116
+ };
117
+ if (orm === false)
118
+ log.info('Database not selected — auth, sanctum, and passport options are hidden.');
119
+ const groupedOptions = {};
120
+ for (const [group, pkgs] of Object.entries(PACKAGE_GROUPS)) {
121
+ const visible = orm === false ? pkgs.filter(p => !DB_GATED.has(p.value)) : pkgs;
122
+ if (visible.length > 0)
123
+ groupedOptions[group] = visible;
124
+ }
125
+ const packageAnswer = await groupMultiselect({
126
+ message: 'Select packages',
127
+ options: groupedOptions,
128
+ initialValues: orm === false ? [] : ['auth'],
129
+ required: false,
130
+ selectableGroups: false,
131
+ });
132
+ if (isCancel(packageAnswer)) {
133
+ cancel('Cancelled.');
134
+ process.exit(0);
135
+ }
136
+ packages = packagesFromList(packageAnswer, orm);
133
137
  }
134
- const selectedPackages = packageAnswer;
135
- const packages = {
136
- auth: selectedPackages.includes('auth'),
137
- sanctum: selectedPackages.includes('sanctum'),
138
- passport: selectedPackages.includes('passport'),
139
- socialite: selectedPackages.includes('socialite'),
140
- queue: selectedPackages.includes('queue'),
141
- storage: selectedPackages.includes('storage'),
142
- scheduler: selectedPackages.includes('scheduler'),
143
- image: selectedPackages.includes('image'),
144
- mail: selectedPackages.includes('mail'),
145
- notifications: selectedPackages.includes('notifications'),
146
- broadcast: selectedPackages.includes('broadcast'),
147
- sync: selectedPackages.includes('sync'),
148
- ai: selectedPackages.includes('ai'),
149
- mcp: selectedPackages.includes('mcp'),
150
- boost: selectedPackages.includes('boost'),
151
- localization: selectedPackages.includes('localization'),
152
- pennant: selectedPackages.includes('pennant'),
153
- telescope: selectedPackages.includes('telescope'),
154
- pulse: selectedPackages.includes('pulse'),
155
- horizon: selectedPackages.includes('horizon'),
156
- crypt: selectedPackages.includes('crypt'),
157
- http: selectedPackages.includes('http'),
158
- process: selectedPackages.includes('process'),
159
- concurrency: selectedPackages.includes('concurrency'),
160
- };
161
- // Passport requires auth + prisma at runtime. Warn and drop silently if missing.
162
138
  if (packages.passport && (!packages.auth || orm !== 'prisma')) {
163
139
  cancel('Passport requires Auth + Prisma. Re-run and select both, or drop Passport.');
164
140
  process.exit(1);
165
141
  }
166
- // ── Frontend frameworks ────────────────────────────────
167
- const frameworksAnswer = await multiselect({
168
- message: 'Frontend frameworks',
169
- options: [
170
- { value: 'react', label: 'React' },
171
- { value: 'vue', label: 'Vue' },
172
- { value: 'solid', label: 'Solid' },
173
- ],
174
- initialValues: ['react'],
175
- required: true,
176
- });
177
- if (isCancel(frameworksAnswer)) {
178
- cancel('Cancelled.');
179
- process.exit(0);
142
+ let frameworks;
143
+ if (p.frameworks)
144
+ frameworks = p.frameworks;
145
+ else {
146
+ const frameworksAnswer = await multiselect({
147
+ message: 'Frontend frameworks',
148
+ options: [
149
+ { value: 'react', label: 'React' },
150
+ { value: 'vue', label: 'Vue' },
151
+ { value: 'solid', label: 'Solid' },
152
+ ],
153
+ initialValues: ['react'],
154
+ required: true,
155
+ });
156
+ if (isCancel(frameworksAnswer)) {
157
+ cancel('Cancelled.');
158
+ process.exit(0);
159
+ }
160
+ frameworks = frameworksAnswer;
180
161
  }
181
- const frameworks = frameworksAnswer;
182
- // ── Primary framework (only when >1 selected) ──────────
183
162
  let primary;
184
- if (frameworks.length > 1) {
163
+ if (p.primary)
164
+ primary = p.primary;
165
+ else if (frameworks.length > 1) {
185
166
  const primaryAnswer = await select({
186
167
  message: 'Primary framework (drives main pages)',
187
- options: frameworks.map(f => ({
188
- value: f,
189
- label: f.charAt(0).toUpperCase() + f.slice(1),
190
- })),
168
+ options: frameworks.map(f => ({ value: f, label: f.charAt(0).toUpperCase() + f.slice(1) })),
191
169
  });
192
170
  if (isCancel(primaryAnswer)) {
193
171
  cancel('Cancelled.');
@@ -198,36 +176,28 @@ async function main() {
198
176
  else {
199
177
  primary = frameworks[0];
200
178
  }
201
- // ── Tailwind CSS ───────────────────────────────────────
202
- const tailwindAnswer = await confirm({
203
- message: 'Add Tailwind CSS?',
204
- initialValue: true,
205
- });
206
- if (isCancel(tailwindAnswer)) {
207
- cancel('Cancelled.');
208
- process.exit(0);
179
+ let tailwind;
180
+ if (p.tailwind !== undefined)
181
+ tailwind = p.tailwind;
182
+ else {
183
+ const tailwindAnswer = await confirm({ message: 'Add Tailwind CSS?', initialValue: true });
184
+ if (isCancel(tailwindAnswer)) {
185
+ cancel('Cancelled.');
186
+ process.exit(0);
187
+ }
188
+ tailwind = tailwindAnswer;
209
189
  }
210
- const tailwind = tailwindAnswer;
211
- // ── shadcn/ui ──────────────────────────────────────────
212
- let shadcn = false;
213
- if (frameworks.includes('react') && tailwind) {
214
- const shadcnAnswer = await confirm({
215
- message: 'Add shadcn/ui?',
216
- initialValue: true,
217
- });
190
+ let shadcn = p.shadcn ?? false;
191
+ if (frameworks.includes('react') && tailwind && p.shadcn === undefined) {
192
+ const shadcnAnswer = await confirm({ message: 'Add shadcn/ui?', initialValue: true });
218
193
  if (isCancel(shadcnAnswer)) {
219
194
  cancel('Cancelled.');
220
195
  process.exit(0);
221
196
  }
222
197
  shadcn = shadcnAnswer;
223
198
  }
224
- // ── Demos ──────────────────────────────────────────────
225
- // Demos are only scaffolded when React is the primary framework — vue/solid
226
- // variants aren't written yet. Each demo is gated by package selection
227
- // (e.g. WebSocket chat needs Broadcast). Rows that fail their gate are
228
- // filtered out before the prompt renders so the user only sees what's available.
229
- let demos = [];
230
- if (primary === 'react') {
199
+ let demos = p.demos ?? [];
200
+ if (primary === 'react' && p.demos === undefined) {
231
201
  const demoOptions = availableDemos(orm, packages);
232
202
  if (demoOptions.length > 0) {
233
203
  const demoAnswer = await multiselect({
@@ -247,106 +217,245 @@ async function main() {
247
217
  demos = demoAnswer;
248
218
  }
249
219
  }
250
- // ── Install dependencies ───────────────────────────────
251
- const installAnswer = await confirm({
252
- message: `Install dependencies?`,
253
- initialValue: true,
254
- });
255
- if (isCancel(installAnswer)) {
256
- cancel('Cancelled.');
257
- process.exit(0);
220
+ else if (demos.includes('*')) {
221
+ demos = primary === 'react' ? availableDemos(orm, packages).map(d => d.value) : [];
258
222
  }
259
- const install = installAnswer;
260
- // ── Generate ───────────────────────────────────────────
261
- const target = path.resolve(process.cwd(), name);
223
+ let install;
224
+ if (p.install !== undefined)
225
+ install = p.install;
226
+ else {
227
+ const installAnswer = await confirm({ message: 'Install dependencies?', initialValue: true });
228
+ if (isCancel(installAnswer)) {
229
+ cancel('Cancelled.');
230
+ process.exit(0);
231
+ }
232
+ install = installAnswer;
233
+ }
234
+ return { name: resolvedName, orm, db, packages, frameworks, primary, tailwind, shadcn, demos, install };
235
+ }
236
+ async function scaffold(answers, opts) {
237
+ const { pm, quiet, logFile } = opts;
238
+ const target = path.resolve(process.cwd(), answers.name);
262
239
  const authSecret = randomBytes(32).toString('hex');
263
240
  const appKey = randomBytes(32).toString('base64');
264
241
  // Make sure target directory doesn't exist
265
242
  try {
266
243
  await fs.access(target);
267
- cancel(`Directory "${name}" already exists. Choose a different name.`);
268
- process.exit(1);
244
+ throw new ScaffoldError(`Directory "${answers.name}" already exists.`);
269
245
  }
270
- catch {
271
- // Good directory doesn't exist
246
+ catch (e) {
247
+ if (e instanceof ScaffoldError)
248
+ throw e;
249
+ // ENOENT — good, directory doesn't exist
272
250
  }
273
- const s = spinner();
274
- s.start('Scaffolding project files...');
275
- const templates = getTemplates({ name, db, orm, authSecret, appKey, frameworks, primary, tailwind, shadcn, pm, packages, demos });
251
+ const s = quiet ? null : spinner();
252
+ s?.start('Scaffolding project files...');
253
+ const templates = getTemplates({
254
+ name: answers.name, db: answers.db, orm: answers.orm,
255
+ authSecret, appKey,
256
+ frameworks: answers.frameworks, primary: answers.primary,
257
+ tailwind: answers.tailwind, shadcn: answers.shadcn,
258
+ pm, packages: answers.packages, demos: answers.demos,
259
+ });
276
260
  for (const [filePath, content] of Object.entries(templates)) {
277
261
  const abs = path.join(target, filePath);
278
262
  await fs.mkdir(path.dirname(abs), { recursive: true });
279
263
  await fs.writeFile(abs, content, 'utf8');
280
264
  }
281
- // Copy auth views from installer's own @rudderjs/auth dependency.
282
- // Views are consumed via `registerAuthRoutes(Route)` from @rudderjs/auth/routes
283
- // — the generated routes/web.ts wires this automatically.
284
265
  let authViewsCopied = true;
285
- if (packages.auth) {
266
+ if (answers.packages.auth) {
286
267
  try {
287
268
  const require = createRequire(import.meta.url);
288
269
  const authPkgPath = require.resolve('@rudderjs/auth/package.json');
289
- const authViewsDir = path.join(path.dirname(authPkgPath), 'views', primary);
270
+ const authViewsDir = path.join(path.dirname(authPkgPath), 'views', answers.primary);
290
271
  await fs.cp(authViewsDir, path.join(target, 'app', 'Views', 'Auth'), { recursive: true });
291
272
  }
292
273
  catch {
293
274
  authViewsCopied = false;
294
275
  }
295
276
  }
296
- s.stop(`${Object.keys(templates).length} files written`);
297
- if (packages.auth && !authViewsCopied) {
298
- console.warn(` ⚠ Auth views could not be vendored from @rudderjs/auth.\n` +
299
- ` After install, run: ${pmRun(pm, 'rudder')} vendor:publish --tag=auth-views-${primary}`);
300
- }
301
- // ── Install ────────────────────────────────────────────
302
- if (install) {
303
- const s2 = spinner();
304
- s2.start(`Installing dependencies with ${pm}...`);
277
+ s?.stop(`${Object.keys(templates).length} files written`);
278
+ let installAttempted = false, installOk = false, discoverOk = false;
279
+ if (answers.install) {
280
+ installAttempted = true;
281
+ const s2 = quiet ? null : spinner();
282
+ s2?.start(`Installing dependencies with ${pm}...`);
305
283
  const [cmd, ...args] = pmInstall(pm).split(' ');
306
- const ok = await new Promise((resolve) => {
307
- const child = spawn(cmd, args, { cwd: target, stdio: 'pipe' });
308
- child.on('close', (code) => resolve(code === 0));
309
- child.on('error', () => resolve(false));
310
- });
311
- s2.stop(ok ? 'Dependencies installed' : `${pmInstall(pm)} failed — run it manually`);
312
- // Generate the provider manifest so the app boots on first `dev`
313
- if (ok) {
314
- const s3 = spinner();
315
- s3.start('Discovering framework providers...');
284
+ installOk = await runChild(cmd, args, target, logFile);
285
+ s2?.stop(installOk ? 'Dependencies installed' : `${pmInstall(pm)} failed run it manually`);
286
+ if (installOk) {
287
+ const s3 = quiet ? null : spinner();
288
+ s3?.start('Discovering framework providers...');
316
289
  const [rcmd, ...rargs] = `${pmRun(pm, 'rudder')} providers:discover`.split(' ');
317
- const discovered = await new Promise((resolve) => {
318
- const child = spawn(rcmd, rargs, { cwd: target, stdio: 'pipe' });
319
- child.on('close', (code) => resolve(code === 0));
320
- child.on('error', () => resolve(false));
321
- });
322
- s3.stop(discovered
290
+ discoverOk = await runChild(rcmd, rargs, target, logFile);
291
+ s3?.stop(discoverOk
323
292
  ? 'Provider manifest generated'
324
293
  : `providers:discover failed — run \`${pmRun(pm, 'rudder')} providers:discover\` manually`);
325
294
  }
326
295
  }
327
- // ── Done ───────────────────────────────────────────────
296
+ return {
297
+ target,
298
+ filesWritten: Object.keys(templates).length,
299
+ authViewsCopied,
300
+ installAttempted, installOk, discoverOk,
301
+ };
302
+ }
303
+ class ScaffoldError extends Error {
304
+ }
305
+ function runChild(cmd, args, cwd, logFile) {
306
+ return new Promise((resolve) => {
307
+ const child = spawn(cmd, args, { cwd, stdio: 'pipe' });
308
+ if (logFile) {
309
+ child.stdout?.on('data', (b) => { void fs.appendFile(logFile, b); });
310
+ child.stderr?.on('data', (b) => { void fs.appendFile(logFile, b); });
311
+ }
312
+ child.on('close', (code) => resolve(code === 0));
313
+ child.on('error', () => resolve(false));
314
+ });
315
+ }
316
+ async function readLogTail(logFile, lines = 40) {
317
+ try {
318
+ const text = await fs.readFile(logFile, 'utf8');
319
+ return text.split('\n').slice(-lines).join('\n');
320
+ }
321
+ catch {
322
+ return '';
323
+ }
324
+ }
325
+ // ──────────────────────────────────────────────────────────────
326
+ // Main
327
+ // ──────────────────────────────────────────────────────────────
328
+ async function main() {
329
+ const argv = process.argv.slice(2);
330
+ const pm = detectPackageManager();
331
+ let parsed;
332
+ try {
333
+ parsed = parseFlags(argv);
334
+ }
335
+ catch (err) {
336
+ if (err instanceof FlagError) {
337
+ // Always emit JSON for flag errors when an agent is detected; otherwise
338
+ // print a friendly message and exit 1.
339
+ const agent = detectAgent();
340
+ if (agent.detected) {
341
+ process.stdout.write(JSON.stringify({
342
+ success: false,
343
+ error: err.message,
344
+ ...(agent.name !== undefined ? { agent: agent.name } : {}),
345
+ }) + '\n');
346
+ process.exit(1);
347
+ }
348
+ console.error(`\n ${err.message}\n`);
349
+ process.exit(1);
350
+ }
351
+ throw err;
352
+ }
353
+ const agent = detectAgent();
354
+ const jsonMode = !parsed.forceInteractive && (parsed.jsonRequested || agent.detected);
355
+ if (jsonMode) {
356
+ const missing = validateJsonMode(parsed.name, parsed.partial);
357
+ if (missing.length > 0) {
358
+ process.stdout.write(JSON.stringify({
359
+ success: false,
360
+ error: `Missing required flags for non-interactive mode: ${missing.join(', ')}`,
361
+ requiredFlags: missing,
362
+ ...(agent.name !== undefined ? { agent: agent.name } : {}),
363
+ }) + '\n');
364
+ process.exit(1);
365
+ }
366
+ const answers = resolveJsonAnswers(parsed.name, parsed.partial);
367
+ if (answers.packages.passport && (!answers.packages.auth || answers.orm !== 'prisma')) {
368
+ process.stdout.write(JSON.stringify({
369
+ success: false,
370
+ error: 'Passport requires --packages to include auth and --orm=prisma.',
371
+ ...(agent.name !== undefined ? { agent: agent.name } : {}),
372
+ }) + '\n');
373
+ process.exit(1);
374
+ }
375
+ const logFile = path.join(os.tmpdir(), `create-rudder-app-${Date.now()}.log`);
376
+ await fs.writeFile(logFile, '');
377
+ try {
378
+ const result = await scaffold(answers, { pm, quiet: true, logFile });
379
+ const payload = {
380
+ success: true,
381
+ name: answers.name,
382
+ directory: result.target,
383
+ files: result.filesWritten,
384
+ };
385
+ if (agent.name)
386
+ payload['agent'] = agent.name;
387
+ if (result.installAttempted) {
388
+ payload['installed'] = result.installOk;
389
+ payload['providersDiscovered'] = result.discoverOk;
390
+ }
391
+ if (answers.packages.auth && !result.authViewsCopied) {
392
+ payload['warning'] = `Auth views could not be vendored. Run: ${pmRun(pm, 'rudder')} vendor:publish --tag=auth-views-${answers.primary}`;
393
+ }
394
+ process.stdout.write(JSON.stringify(payload) + '\n');
395
+ try {
396
+ await fs.unlink(logFile);
397
+ }
398
+ catch { /* ignore */ }
399
+ process.exit(0);
400
+ }
401
+ catch (err) {
402
+ const message = err instanceof Error ? err.message : String(err);
403
+ const tail = await readLogTail(logFile);
404
+ process.stdout.write(JSON.stringify({
405
+ success: false,
406
+ error: message,
407
+ logFile,
408
+ logTail: tail,
409
+ ...(agent.name !== undefined ? { agent: agent.name } : {}),
410
+ }) + '\n');
411
+ process.exit(1);
412
+ }
413
+ }
414
+ // ── Interactive flow ────────────────────────────────────
415
+ console.log();
416
+ intro(' create-rudder-app ');
417
+ const answers = await gatherInteractive(parsed.name, parsed.partial);
418
+ let result;
419
+ try {
420
+ result = await scaffold(answers, { pm, quiet: false });
421
+ }
422
+ catch (err) {
423
+ if (err instanceof ScaffoldError) {
424
+ cancel(err.message);
425
+ process.exit(1);
426
+ }
427
+ throw err;
428
+ }
429
+ if (answers.packages.auth && !result.authViewsCopied) {
430
+ console.warn(` ⚠ Auth views could not be vendored from @rudderjs/auth.\n` +
431
+ ` After install, run: ${pmRun(pm, 'rudder')} vendor:publish --tag=auth-views-${answers.primary}`);
432
+ }
328
433
  const nextSteps = [
329
- ` cd ${name}`,
330
- ...(!install ? [` ${pmInstall(pm)}`, ` ${pmRun(pm, 'rudder')} providers:discover`] : []),
331
- ...(orm === 'prisma' ? [
434
+ ` cd ${answers.name}`,
435
+ ...(!answers.install ? [` ${pmInstall(pm)}`, ` ${pmRun(pm, 'rudder')} providers:discover`] : []),
436
+ ...(answers.orm === 'prisma' ? [
332
437
  ` ${pmExec(pm, 'prisma generate')}`,
333
438
  ` ${pmExec(pm, 'prisma db push')}`,
334
439
  ] : []),
335
- ...(!install && packages.auth ? [` ${pmRun(pm, 'rudder')} vendor:publish --tag=auth-views-${primary}`] : []),
336
- ...(packages.passport ? [` ${pmRun(pm, 'rudder')} passport:keys`] : []),
440
+ ...(!answers.install && answers.packages.auth
441
+ ? [` ${pmRun(pm, 'rudder')} vendor:publish --tag=auth-views-${answers.primary}`]
442
+ : []),
443
+ ...(answers.packages.passport ? [` ${pmRun(pm, 'rudder')} passport:keys`] : []),
337
444
  ` ${pmRun(pm, 'dev')}`,
338
445
  ];
339
446
  const hints = [];
340
- if (packages.ai)
447
+ if (answers.packages.ai)
341
448
  hints.push(' AI chat: /ai-chat (set ANTHROPIC_API_KEY in .env)');
342
- if (packages.mcp)
449
+ if (answers.packages.mcp)
343
450
  hints.push(' MCP echo: POST /mcp/echo (see app/Mcp/EchoServer.ts)');
344
- if (packages.passport)
451
+ if (answers.packages.passport)
345
452
  hints.push(' OAuth2: /oauth/authorize, /oauth/token (run `rudder passport:client <name>` first)');
346
- if (packages.telescope)
453
+ if (answers.packages.telescope)
347
454
  hints.push(' Telescope: /telescope (debug dashboard — requests, queries, jobs, AI, mail)');
348
- if (packages.boost)
455
+ if (answers.packages.boost)
349
456
  hints.push(` Boost: ${pmRun(pm, 'rudder')} boost:install (wire your AI coding assistant)`);
457
+ if (answers.packages.terminal)
458
+ hints.push(` Terminal: ${pmRun(pm, 'rudder')} make:terminal <Name> (scaffold a terminal view)`);
350
459
  const hintsStr = hints.length > 0 ? '\n\n' + hints.join('\n') : '';
351
460
  outro(`Done! Get started:\n\n` +
352
461
  nextSteps.join('\n') +