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/agent-detect.d.ts +21 -0
- package/dist/agent-detect.d.ts.map +1 -0
- package/dist/agent-detect.js +36 -0
- package/dist/agent-detect.js.map +1 -0
- package/dist/cli-flags.d.ts +33 -0
- package/dist/cli-flags.d.ts.map +1 -0
- package/dist/cli-flags.js +164 -0
- package/dist/cli-flags.js.map +1 -0
- package/dist/index.js +335 -226
- package/dist/index.js.map +1 -1
- package/dist/templates/app/terminal-dashboard.d.ts +2 -0
- package/dist/templates/app/terminal-dashboard.d.ts.map +1 -0
- package/dist/templates/app/terminal-dashboard.js +22 -0
- package/dist/templates/app/terminal-dashboard.js.map +1 -0
- package/dist/templates/package-json.d.ts.map +1 -1
- package/dist/templates/package-json.js +4 -0
- package/dist/templates/package-json.js.map +1 -1
- package/dist/templates/routes/console.d.ts +2 -1
- package/dist/templates/routes/console.d.ts.map +1 -1
- package/dist/templates/routes/console.js +28 -20
- package/dist/templates/routes/console.js.map +1 -1
- package/dist/templates.d.ts +1 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +5 -1
- package/dist/templates.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let
|
|
17
|
-
if (
|
|
18
|
-
|
|
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
|
-
|
|
34
|
+
resolvedName = answer.trim();
|
|
32
35
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
groupedOptions
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 (
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
268
|
-
process.exit(1);
|
|
244
|
+
throw new ScaffoldError(`Directory "${answers.name}" already exists.`);
|
|
269
245
|
}
|
|
270
|
-
catch {
|
|
271
|
-
|
|
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
|
|
275
|
-
const templates = getTemplates({
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
|
336
|
-
|
|
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') +
|