create-vista-app 0.2.16 → 0.3.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/bin/cli.js CHANGED
@@ -1,511 +1,535 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { execSync } = require('child_process');
6
- const prompts = require('prompts');
7
-
8
- const usageCommand = 'npx create-vista-app@latest <project-name>';
9
- const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
10
-
11
- // Detect which package manager invoked us (npm, pnpm, yarn, bun)
12
- function normalizePackageManager(value) {
13
- const normalized = String(value || '').trim().toLowerCase();
14
- return SUPPORTED_PACKAGE_MANAGERS.includes(normalized) ? normalized : undefined;
15
- }
16
-
17
- function detectPackageManager(userAgent = process.env.npm_config_user_agent || '') {
18
- const ua = String(userAgent || '');
19
- if (ua.startsWith('pnpm')) return 'pnpm';
20
- if (ua.startsWith('yarn')) return 'yarn';
21
- if (ua.startsWith('bun')) return 'bun';
22
- return 'npm';
23
- }
24
-
25
- function getExplicitPackageManagerFromArgs(args) {
26
- const explicitValue = normalizePackageManager(getFlagValue('--package-manager'));
27
- const explicitFlags = SUPPORTED_PACKAGE_MANAGERS.filter((manager) => args.includes(`--${manager}`));
28
-
29
- if (explicitFlags.length > 1) {
30
- console.error('Error: use only one package manager flag: --npm, --pnpm, --yarn, or --bun.');
31
- process.exit(1);
32
- }
33
-
34
- if (getFlagValue('--package-manager') && !explicitValue) {
35
- console.error(
36
- `Error: unsupported package manager "${getFlagValue('--package-manager')}". Use npm, pnpm, yarn, or bun.`
37
- );
38
- process.exit(1);
39
- }
40
-
41
- if (explicitValue && explicitFlags.length > 0 && explicitFlags[0] !== explicitValue) {
42
- console.error('Error: package manager flags conflict. Use only one package manager selector.');
43
- process.exit(1);
44
- }
45
-
46
- return explicitValue || explicitFlags[0];
47
- }
48
-
49
- const rawArgs = process.argv.slice(2);
50
- const useTypedApiStarter = rawArgs.includes('--typed-api') || rawArgs.includes('--typed');
51
- const skipInstall = rawArgs.includes('--skip-install');
52
- const skipGit = rawArgs.includes('--no-git');
53
- const assumeYes = rawArgs.includes('--yes') || rawArgs.includes('-y');
54
- const canPrompt = !!(process.stdin.isTTY && process.stdout.isTTY);
55
- const detectedPackageManager = detectPackageManager();
56
-
57
- function getFlagValue(flag) {
58
- const index = rawArgs.indexOf(flag);
59
- if (index !== -1) {
60
- const next = rawArgs[index + 1];
61
- if (next && !next.startsWith('-')) return next;
62
- }
63
- const inline = rawArgs.find((arg) => arg.startsWith(`${flag}=`));
64
- if (inline) return inline.slice(flag.length + 1);
65
- return undefined;
66
- }
67
-
68
- const explicitFlashpack = rawArgs.includes('--flashpack');
69
- const explicitDefaultEngine = rawArgs.includes('--default-engine');
70
- const explicitEngine = getFlagValue('--engine');
71
- const explicitPackageManager = getExplicitPackageManagerFromArgs(rawArgs);
72
-
73
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
74
- console.log(`
75
- Usage:
76
- ${usageCommand} [--typed-api] [--skip-install] [--no-git] [--yes] [--engine <default|flashpack>] [--flashpack] [--default-engine] [--package-manager <npm|pnpm|yarn|bun>] [--npm|--pnpm|--yarn|--bun]
77
-
78
- Example:
79
- npx create-vista-app@latest my-vista-app
80
- npx create-vista-app@latest
81
- npx create-vista-app@latest my-vista-app --typed-api
82
- npx create-vista-app@latest my-vista-app --flashpack
83
- npx create-vista-app@latest my-vista-app --package-manager pnpm
84
- `);
85
- process.exit(0);
86
- }
87
-
88
- if (explicitFlashpack && explicitDefaultEngine) {
89
- console.error('Error: use only one of --flashpack or --default-engine.');
90
- process.exit(1);
91
- }
92
-
93
- if (explicitEngine && !['default', 'flashpack'].includes(explicitEngine)) {
94
- console.error(`Error: unsupported engine "${explicitEngine}". Use "default" or "flashpack".`);
95
- process.exit(1);
96
- }
97
-
98
- async function resolveProjectName() {
99
- const args = rawArgs.filter((arg) => !arg.startsWith('-'));
100
- if (args[0]) return args[0];
101
-
102
- if (!canPrompt) {
103
- return 'my-vista-app';
104
- }
105
-
106
- const response = await prompts({
107
- type: 'text',
108
- name: 'projectName',
109
- message: 'Project name?',
110
- initial: 'my-vista-app',
111
- validate: (value) => {
112
- const trimmed = String(value || '').trim();
113
- if (!trimmed) return 'Project name is required.';
114
- if (/[<>:"/\\|?*\x00-\x1F]/.test(trimmed)) return 'Use a valid folder name.';
115
- return true;
116
- },
117
- });
118
-
119
- const value = String(response.projectName || '').trim();
120
- if (!value) {
121
- console.log('Aborted.');
122
- process.exit(0);
123
- }
124
- return value;
125
- }
126
- async function confirmProceed(projectName, projectDir, engine, packageManager) {
127
- if (assumeYes || !canPrompt) return true;
128
- const response = await prompts({
129
- type: 'confirm',
130
- name: 'proceed',
131
- message: `Create Vista app "${projectName}" in ${projectDir} (engine: ${engine}, package manager: ${packageManager})?`,
132
- initial: true,
133
- });
134
- return response.proceed !== false;
135
- }
136
-
137
- async function resolveEngineChoice() {
138
- if (explicitEngine) return explicitEngine;
139
- if (explicitFlashpack) return 'flashpack';
140
- if (explicitDefaultEngine) return 'default';
141
- if (assumeYes || !canPrompt) return 'default';
142
-
143
- const response = await prompts({
144
- type: 'select',
145
- name: 'engine',
146
- message: 'Select engine',
147
- choices: [
148
- {
149
- title: 'default (recommended)',
150
- value: 'default',
151
- description: 'Stable webpack-first path',
152
- },
153
- {
154
- title: 'flashpack',
155
- value: 'flashpack',
156
- description: 'Rust-first engine path',
157
- },
158
- ],
159
- initial: 0,
160
- });
161
-
162
- const value = String(response.engine || '').trim();
163
- if (!value) {
164
- console.log('Aborted.');
165
- process.exit(0);
166
- }
167
- return value;
168
- }
169
-
170
- async function resolvePackageManagerChoice() {
171
- if (explicitPackageManager) return explicitPackageManager;
172
- if (assumeYes || !canPrompt) return detectedPackageManager;
173
-
174
- const response = await prompts({
175
- type: 'select',
176
- name: 'packageManager',
177
- message: 'Select package manager',
178
- choices: [
179
- {
180
- title: 'npm',
181
- value: 'npm',
182
- description: 'Widely available default',
183
- },
184
- {
185
- title: 'pnpm',
186
- value: 'pnpm',
187
- description: 'Fast installs with shared store',
188
- },
189
- {
190
- title: 'yarn',
191
- value: 'yarn',
192
- description: 'Classic Yarn workflow',
193
- },
194
- {
195
- title: 'bun',
196
- value: 'bun',
197
- description: 'Fast Bun-based install/runtime',
198
- },
199
- ],
200
- initial: Math.max(SUPPORTED_PACKAGE_MANAGERS.indexOf(detectedPackageManager), 0),
201
- });
202
-
203
- const value = normalizePackageManager(response.packageManager);
204
- if (!value) {
205
- console.log('Aborted.');
206
- process.exit(0);
207
- }
208
-
209
- return value;
210
- }
211
-
212
- function getInstallCommand(packageManager) {
213
- if (packageManager === 'yarn') return 'yarn';
214
- if (packageManager === 'bun') return 'bun install';
215
- return `${packageManager} install`;
216
- }
217
-
218
- function getRunCommand(packageManager) {
219
- return packageManager === 'npm' ? 'npm run' : packageManager;
220
- }
221
-
222
- function getCreateCommand(packageManager) {
223
- if (packageManager === 'pnpm') return 'pnpm create vista-app';
224
- if (packageManager === 'yarn') return 'yarn create vista-app';
225
- if (packageManager === 'bun') return 'bun create vista-app';
226
- return 'npx create-vista-app@latest';
227
- }
228
-
229
- function copyRecursiveSync(src, dest) {
230
- const exists = fs.existsSync(src);
231
- const stats = exists && fs.statSync(src);
232
- const isDirectory = exists && stats.isDirectory();
233
- if (isDirectory) {
234
- fs.mkdirSync(dest, { recursive: true });
235
- fs.readdirSync(src).forEach((childItemName) => {
236
- copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName));
237
- });
238
- } else {
239
- fs.copyFileSync(src, dest);
240
- }
241
- }
242
-
243
- function injectEngineBlock(source, selectedEngine) {
244
- // Prefer preserving existing formatting when an engine block already exists.
245
- if (/\bengine\s*:\s*\{[\s\S]*?\bvariant\s*:\s*['"][^'"]+['"]/m.test(source)) {
246
- return source.replace(
247
- /(\bengine\s*:\s*\{[\s\S]*?\bvariant\s*:\s*['"])([^'"]+)(['"])/m,
248
- `$1${selectedEngine}$3`
249
- );
250
- }
251
-
252
- // Replace existing scalar engine config
253
- if (/\bengine\s*:\s*['"][^'"]+['"],?/m.test(source)) {
254
- return source.replace(/\bengine\s*:\s*['"][^'"]+['"],?/m, `engine: '${selectedEngine}',`);
255
- }
256
-
257
- // Insert right after "const config = {"
258
- const engineBlock = ` engine: {\n variant: '${selectedEngine}',\n },`;
259
- const marker = 'const config = {';
260
- const markerIndex = source.indexOf(marker);
261
- if (markerIndex !== -1) {
262
- const insertAt = markerIndex + marker.length;
263
- return `${source.slice(0, insertAt)}\n${engineBlock}${source.slice(insertAt)}`;
264
- }
265
-
266
- // Fallback to a minimal config when template structure is unexpected
267
- return `const config = {\n${engineBlock}\n};\n\nexport default config;\n`;
268
- }
269
-
270
- function applyEngineToVistaConfig(projectDir, selectedEngine) {
271
- const configPath = path.join(projectDir, 'vista.config.ts');
272
- if (!fs.existsSync(configPath)) return;
273
-
274
- const source = fs.readFileSync(configPath, 'utf8');
275
- const patched = injectEngineBlock(source, selectedEngine);
276
- fs.writeFileSync(configPath, patched);
277
- }
278
-
279
- function applyReadmeSelections(projectDir, selectedEngine, useTypedApi) {
280
- const readmePath = path.join(projectDir, 'README.md');
281
- if (!fs.existsSync(readmePath)) return;
282
-
283
- const source = fs.readFileSync(readmePath, 'utf8');
284
- const patched = source
285
- .replace(/__VISTA_ENGINE__/g, selectedEngine)
286
- .replace(/__VISTA_TYPED_API__/g, useTypedApi ? 'enabled' : 'disabled');
287
- fs.writeFileSync(readmePath, patched);
288
- }
289
-
290
- function applyFlashpackStarterTheme(projectDir) {
291
- const flashTemplateDir = path.join(__dirname, 'flash-template');
292
- if (fs.existsSync(flashTemplateDir)) {
293
- copyRecursiveSync(flashTemplateDir, projectDir);
294
- }
295
- }
296
-
297
- async function main() {
298
- const useLocal = rawArgs.includes('--local');
299
- const currentDir = process.cwd();
300
- const projectName = await resolveProjectName();
301
- const selectedEngine = await resolveEngineChoice();
302
- const selectedPackageManager = await resolvePackageManagerChoice();
303
- const projectDir = path.join(currentDir, projectName);
304
-
305
- const proceed = await confirmProceed(
306
- projectName,
307
- projectDir,
308
- selectedEngine,
309
- selectedPackageManager
310
- );
311
- if (!proceed) {
312
- console.log('Aborted.');
313
- process.exit(0);
314
- }
315
-
316
- console.log(`Creating a new Vista app in ${projectDir}...`);
317
-
318
- // 1. Create Directory
319
- if (fs.existsSync(projectDir)) {
320
- console.error(`Error: Directory ${projectName} already exists.`);
321
- process.exit(1);
322
- }
323
- fs.mkdirSync(projectDir);
324
-
325
- // 2. Copy Template
326
- const templateDir = path.join(__dirname, '../template');
327
- copyRecursiveSync(templateDir, projectDir);
328
-
329
- if (useTypedApiStarter) {
330
- const typedTemplateDir = path.join(__dirname, '../template-typed');
331
- copyRecursiveSync(typedTemplateDir, projectDir);
332
- console.log('Added typed API starter files.');
333
- }
334
-
335
- applyEngineToVistaConfig(projectDir, selectedEngine);
336
- applyReadmeSelections(projectDir, selectedEngine, useTypedApiStarter);
337
- if (selectedEngine === 'flashpack') {
338
- applyFlashpackStarterTheme(projectDir);
339
- }
340
-
341
- console.log('Scaffolding complete.');
342
-
343
- // 3. Setup Dependencies (production-ready)
344
- const packageJson = {
345
- name: projectName,
346
- version: '0.1.0',
347
- scripts: {
348
- dev: 'vista dev',
349
- build: 'vista build',
350
- start: 'vista start',
351
- },
352
- dependencies: {
353
- // Runtime dependencies
354
- react: '^19.0.0',
355
- 'react-dom': '^19.0.0',
356
- 'react-server-dom-webpack': '^19.0.0',
357
- vista: useLocal ? 'file:../packages/vista' : 'npm:@vistagenic/vista@latest',
358
- // CSS build (needed in production for vista build)
359
- postcss: '^8.0.0',
360
- 'postcss-cli': '^11.0.0',
361
- tailwindcss: '^4.0.0',
362
- '@tailwindcss/postcss': '^4.0.0',
363
- webpack: '^5.90.0',
364
- // Node 20+ SSR compatibility
365
- '@swc-node/register': '^1.9.0',
366
- '@swc/core': '^1.4.0',
367
- tsx: '^4.7.0',
368
- },
369
- devDependencies: {
370
- typescript: '^5.0.0',
371
- '@types/react': '^19.0.0',
372
- '@types/react-dom': '^19.0.0',
373
- },
374
- };
375
-
376
- fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
377
-
378
- // 4. Create .gitignore
379
- const gitignoreContent = `# Dependencies
380
- node_modules/
381
- .pnpm-store/
382
-
383
- # Build outputs
384
- dist/
385
- .vista/
386
- .flash/
387
- out/
388
-
389
- # Rust artifacts
390
- target/
391
- *.node
392
-
393
- # IDE
394
- .idea/
395
- .vscode/
396
- *.swp
397
- *.swo
398
-
399
- # Environment
400
- .env
401
- .env.local
402
- .env.development.local
403
- .env.test.local
404
- .env.production.local
405
-
406
- # Logs
407
- npm-debug.log*
408
- yarn-debug.log*
409
- yarn-error.log*
410
- pnpm-debug.log*
411
-
412
- # OS files
413
- .DS_Store
414
- Thumbs.db
415
-
416
- # TypeScript
417
- *.tsbuildinfo
418
-
419
- # Testing
420
- coverage/
421
-
422
- # Misc
423
- *.log
424
- `;
425
-
426
- fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignoreContent);
427
-
428
- console.log('Created .gitignore');
429
-
430
- // 5. Initialize Git Repository
431
- if (!skipGit) {
432
- try {
433
- execSync('git init', { cwd: projectDir, stdio: 'pipe' });
434
- execSync('git add .', { cwd: projectDir, stdio: 'pipe' });
435
- execSync('git commit -m "Initial commit from create-vista-app"', {
436
- cwd: projectDir,
437
- stdio: 'pipe',
438
- env: {
439
- ...process.env,
440
- GIT_AUTHOR_NAME: 'Vista',
441
- GIT_AUTHOR_EMAIL: 'vista@example.com',
442
- GIT_COMMITTER_NAME: 'Vista',
443
- GIT_COMMITTER_EMAIL: 'vista@example.com',
444
- },
445
- });
446
- console.log('Initialized git repository with initial commit');
447
- } catch (e) {
448
- // Git might not be installed, that's okay
449
- console.log('Note: Could not initialize git repository. You can do this manually with: git init');
450
- }
451
- } else {
452
- console.log('Skipped git initialization (--no-git).');
453
- }
454
-
455
- // 6. Install Dependencies
456
- const installCmd = getInstallCommand(selectedPackageManager);
457
- if (!skipInstall) {
458
- console.log(
459
- `\nInstalling dependencies with ${selectedPackageManager}... This may take a moment.\n`
460
- );
461
- try {
462
- execSync(installCmd, { cwd: projectDir, stdio: 'inherit' });
463
- console.log(`\n✓ Dependencies installed successfully!`);
464
- } catch (e) {
465
- console.log(
466
- `\nNote: Could not install dependencies automatically. Run "${installCmd}" manually.`
467
- );
468
- }
469
- } else {
470
- console.log('\nSkipped dependency installation (--skip-install).');
471
- }
472
-
473
- const runCmd = getRunCommand(selectedPackageManager);
474
- const createCmd = getCreateCommand(selectedPackageManager);
475
-
476
- console.log(`
477
- ✨ Success! Created ${projectName} at ${projectDir}
478
- Engine: ${selectedEngine}
479
- Package manager: ${selectedPackageManager}
480
-
481
- Get started by running:
482
-
483
- cd ${projectName}
484
- ${runCmd} dev
485
-
486
- Create another app anytime with:
487
- ${createCmd} <project-name>
488
-
489
- Happy Hacking! 🚀
490
- `);
491
- }
492
-
493
- module.exports = {
494
- main,
495
- detectPackageManager,
496
- normalizePackageManager,
497
- getInstallCommand,
498
- getRunCommand,
499
- getCreateCommand,
500
- injectEngineBlock,
501
- applyEngineToVistaConfig,
502
- applyReadmeSelections,
503
- applyFlashpackStarterTheme,
504
- };
505
-
506
- if (require.main === module) {
507
- main().catch((error) => {
508
- console.error('create-vista-app failed:', error);
509
- process.exit(1);
510
- });
511
- }
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+ const prompts = require('prompts');
7
+
8
+ const usageCommand = 'npx create-vista-app@latest <project-name>';
9
+ const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
10
+
11
+ // Detect which package manager invoked us (npm, pnpm, yarn, bun)
12
+ function normalizePackageManager(value) {
13
+ const normalized = String(value || '').trim().toLowerCase();
14
+ return SUPPORTED_PACKAGE_MANAGERS.includes(normalized) ? normalized : undefined;
15
+ }
16
+
17
+ function detectPackageManager(userAgent = process.env.npm_config_user_agent || '') {
18
+ const ua = String(userAgent || '');
19
+ if (ua.startsWith('pnpm')) return 'pnpm';
20
+ if (ua.startsWith('yarn')) return 'yarn';
21
+ if (ua.startsWith('bun')) return 'bun';
22
+ return 'npm';
23
+ }
24
+
25
+ function getExplicitPackageManagerFromArgs(args) {
26
+ const explicitValue = normalizePackageManager(getFlagValue('--package-manager'));
27
+ const explicitFlags = SUPPORTED_PACKAGE_MANAGERS.filter((manager) => args.includes(`--${manager}`));
28
+
29
+ if (explicitFlags.length > 1) {
30
+ console.error('Error: use only one package manager flag: --npm, --pnpm, --yarn, or --bun.');
31
+ process.exit(1);
32
+ }
33
+
34
+ if (getFlagValue('--package-manager') && !explicitValue) {
35
+ console.error(
36
+ `Error: unsupported package manager "${getFlagValue('--package-manager')}". Use npm, pnpm, yarn, or bun.`
37
+ );
38
+ process.exit(1);
39
+ }
40
+
41
+ if (explicitValue && explicitFlags.length > 0 && explicitFlags[0] !== explicitValue) {
42
+ console.error('Error: package manager flags conflict. Use only one package manager selector.');
43
+ process.exit(1);
44
+ }
45
+
46
+ return explicitValue || explicitFlags[0];
47
+ }
48
+
49
+ const rawArgs = process.argv.slice(2);
50
+ const useTypedApiStarter = rawArgs.includes('--typed-api') || rawArgs.includes('--typed');
51
+ const skipInstall = rawArgs.includes('--skip-install');
52
+ const skipGit = rawArgs.includes('--no-git');
53
+ const includeDeployTemplates = !rawArgs.includes('--no-deploy-templates');
54
+ const assumeYes = rawArgs.includes('--yes') || rawArgs.includes('-y');
55
+ const canPrompt = !!(process.stdin.isTTY && process.stdout.isTTY);
56
+ const detectedPackageManager = detectPackageManager();
57
+
58
+ function getFlagValue(flag) {
59
+ const index = rawArgs.indexOf(flag);
60
+ if (index !== -1) {
61
+ const next = rawArgs[index + 1];
62
+ if (next && !next.startsWith('-')) return next;
63
+ }
64
+ const inline = rawArgs.find((arg) => arg.startsWith(`${flag}=`));
65
+ if (inline) return inline.slice(flag.length + 1);
66
+ return undefined;
67
+ }
68
+
69
+ const explicitFlashpack = rawArgs.includes('--flashpack');
70
+ const explicitDefaultEngine = rawArgs.includes('--default-engine');
71
+ const explicitEngine = getFlagValue('--engine');
72
+ const explicitPackageManager = getExplicitPackageManagerFromArgs(rawArgs);
73
+
74
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
75
+ console.log(`
76
+ Usage:
77
+ ${usageCommand} [--typed-api] [--skip-install] [--no-git] [--yes] [--no-deploy-templates] [--engine <default|flashpack>] [--flashpack] [--default-engine] [--package-manager <npm|pnpm|yarn|bun>] [--npm|--pnpm|--yarn|--bun]
78
+
79
+ Example:
80
+ npx create-vista-app@latest my-vista-app
81
+ npx create-vista-app@latest
82
+ npx create-vista-app@latest my-vista-app --typed-api
83
+ npx create-vista-app@latest my-vista-app --flashpack
84
+ npx create-vista-app@latest my-vista-app --package-manager pnpm
85
+ `);
86
+ process.exit(0);
87
+ }
88
+
89
+ if (explicitFlashpack && explicitDefaultEngine) {
90
+ console.error('Error: use only one of --flashpack or --default-engine.');
91
+ process.exit(1);
92
+ }
93
+
94
+ if (explicitEngine && !['default', 'flashpack'].includes(explicitEngine)) {
95
+ console.error(`Error: unsupported engine "${explicitEngine}". Use "default" or "flashpack".`);
96
+ process.exit(1);
97
+ }
98
+
99
+ async function resolveProjectName() {
100
+ const args = rawArgs.filter((arg) => !arg.startsWith('-'));
101
+ if (args[0]) return args[0];
102
+
103
+ if (!canPrompt) {
104
+ return 'my-vista-app';
105
+ }
106
+
107
+ const response = await prompts({
108
+ type: 'text',
109
+ name: 'projectName',
110
+ message: 'Project name?',
111
+ initial: 'my-vista-app',
112
+ validate: (value) => {
113
+ const trimmed = String(value || '').trim();
114
+ if (!trimmed) return 'Project name is required.';
115
+ if (/[<>:"/\\|?*\x00-\x1F]/.test(trimmed)) return 'Use a valid folder name.';
116
+ return true;
117
+ },
118
+ });
119
+
120
+ const value = String(response.projectName || '').trim();
121
+ if (!value) {
122
+ console.log('Aborted.');
123
+ process.exit(0);
124
+ }
125
+ return value;
126
+ }
127
+ async function confirmProceed(projectName, projectDir, engine, packageManager) {
128
+ if (assumeYes || !canPrompt) return true;
129
+ const response = await prompts({
130
+ type: 'confirm',
131
+ name: 'proceed',
132
+ message: `Create Vista app "${projectName}" in ${projectDir} (engine: ${engine}, package manager: ${packageManager})?`,
133
+ initial: true,
134
+ });
135
+ return response.proceed !== false;
136
+ }
137
+
138
+ async function resolveEngineChoice() {
139
+ if (explicitEngine) return explicitEngine;
140
+ if (explicitFlashpack) return 'flashpack';
141
+ if (explicitDefaultEngine) return 'default';
142
+ if (assumeYes || !canPrompt) return 'default';
143
+
144
+ const response = await prompts({
145
+ type: 'select',
146
+ name: 'engine',
147
+ message: 'Select engine',
148
+ choices: [
149
+ {
150
+ title: 'default (recommended)',
151
+ value: 'default',
152
+ description: 'Stable webpack-first path',
153
+ },
154
+ {
155
+ title: 'flashpack',
156
+ value: 'flashpack',
157
+ description: 'Rust-first engine path',
158
+ },
159
+ ],
160
+ initial: 0,
161
+ });
162
+
163
+ const value = String(response.engine || '').trim();
164
+ if (!value) {
165
+ console.log('Aborted.');
166
+ process.exit(0);
167
+ }
168
+ return value;
169
+ }
170
+
171
+ async function resolvePackageManagerChoice() {
172
+ if (explicitPackageManager) return explicitPackageManager;
173
+ if (assumeYes || !canPrompt) return detectedPackageManager;
174
+
175
+ const response = await prompts({
176
+ type: 'select',
177
+ name: 'packageManager',
178
+ message: 'Select package manager',
179
+ choices: [
180
+ {
181
+ title: 'npm',
182
+ value: 'npm',
183
+ description: 'Widely available default',
184
+ },
185
+ {
186
+ title: 'pnpm',
187
+ value: 'pnpm',
188
+ description: 'Fast installs with shared store',
189
+ },
190
+ {
191
+ title: 'yarn',
192
+ value: 'yarn',
193
+ description: 'Classic Yarn workflow',
194
+ },
195
+ {
196
+ title: 'bun',
197
+ value: 'bun',
198
+ description: 'Fast Bun-based install/runtime',
199
+ },
200
+ ],
201
+ initial: Math.max(SUPPORTED_PACKAGE_MANAGERS.indexOf(detectedPackageManager), 0),
202
+ });
203
+
204
+ const value = normalizePackageManager(response.packageManager);
205
+ if (!value) {
206
+ console.log('Aborted.');
207
+ process.exit(0);
208
+ }
209
+
210
+ return value;
211
+ }
212
+
213
+ function getInstallCommand(packageManager) {
214
+ if (packageManager === 'yarn') return 'yarn';
215
+ if (packageManager === 'bun') return 'bun install';
216
+ return `${packageManager} install`;
217
+ }
218
+
219
+ function getRunCommand(packageManager) {
220
+ return packageManager === 'npm' ? 'npm run' : packageManager;
221
+ }
222
+
223
+ function getCreateCommand(packageManager) {
224
+ if (packageManager === 'pnpm') return 'pnpm create vista-app';
225
+ if (packageManager === 'yarn') return 'yarn create vista-app';
226
+ if (packageManager === 'bun') return 'bun create vista-app';
227
+ return 'npx create-vista-app@latest';
228
+ }
229
+
230
+ function copyRecursiveSync(src, dest) {
231
+ const exists = fs.existsSync(src);
232
+ const stats = exists && fs.statSync(src);
233
+ const isDirectory = exists && stats.isDirectory();
234
+ if (isDirectory) {
235
+ fs.mkdirSync(dest, { recursive: true });
236
+ fs.readdirSync(src).forEach((childItemName) => {
237
+ copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName));
238
+ });
239
+ } else {
240
+ fs.copyFileSync(src, dest);
241
+ }
242
+ }
243
+
244
+ function injectEngineBlock(source, selectedEngine) {
245
+ // Prefer preserving existing formatting when an engine block already exists.
246
+ if (/\bengine\s*:\s*\{[\s\S]*?\bvariant\s*:\s*['"][^'"]+['"]/m.test(source)) {
247
+ return source.replace(
248
+ /(\bengine\s*:\s*\{[\s\S]*?\bvariant\s*:\s*['"])([^'"]+)(['"])/m,
249
+ `$1${selectedEngine}$3`
250
+ );
251
+ }
252
+
253
+ // Replace existing scalar engine config
254
+ if (/\bengine\s*:\s*['"][^'"]+['"],?/m.test(source)) {
255
+ return source.replace(/\bengine\s*:\s*['"][^'"]+['"],?/m, `engine: '${selectedEngine}',`);
256
+ }
257
+
258
+ // Insert right after "const config = {"
259
+ const engineBlock = ` engine: {\n variant: '${selectedEngine}',\n },`;
260
+ const marker = 'const config = {';
261
+ const markerIndex = source.indexOf(marker);
262
+ if (markerIndex !== -1) {
263
+ const insertAt = markerIndex + marker.length;
264
+ return `${source.slice(0, insertAt)}\n${engineBlock}${source.slice(insertAt)}`;
265
+ }
266
+
267
+ // Fallback to a minimal config when template structure is unexpected
268
+ return `const config = {\n${engineBlock}\n};\n\nexport default config;\n`;
269
+ }
270
+
271
+ function applyEngineToVistaConfig(projectDir, selectedEngine) {
272
+ const configPath = path.join(projectDir, 'vista.config.ts');
273
+ if (!fs.existsSync(configPath)) return;
274
+
275
+ const source = fs.readFileSync(configPath, 'utf8');
276
+ const patched = injectEngineBlock(source, selectedEngine);
277
+ fs.writeFileSync(configPath, patched);
278
+ }
279
+
280
+ function applyReadmeSelections(projectDir, selectedEngine, useTypedApi) {
281
+ const readmePath = path.join(projectDir, 'README.md');
282
+ if (!fs.existsSync(readmePath)) return;
283
+
284
+ const source = fs.readFileSync(readmePath, 'utf8');
285
+ const patched = source
286
+ .replace(/__VISTA_ENGINE__/g, selectedEngine)
287
+ .replace(/__VISTA_TYPED_API__/g, useTypedApi ? 'enabled' : 'disabled');
288
+ fs.writeFileSync(readmePath, patched);
289
+ }
290
+
291
+ function applyFlashpackStarterTheme(projectDir) {
292
+ const flashTemplateDir = path.join(__dirname, 'flash-template');
293
+ if (fs.existsSync(flashTemplateDir)) {
294
+ copyRecursiveSync(flashTemplateDir, projectDir);
295
+ }
296
+ }
297
+
298
+ function applyDeployTemplates(projectDir, options = {}) {
299
+ const deployTemplateDir = path.join(__dirname, '../template/deploy');
300
+ if (!fs.existsSync(deployTemplateDir)) return;
301
+
302
+ const includeAll = Boolean(options.all);
303
+ const defaultFiles = ['render.yaml', 'Dockerfile', '.dockerignore'];
304
+ const optionalFiles = ['wrangler.toml', 'netlify.toml', 'vercel.json'];
305
+ const filesToCopy = includeAll ? [...defaultFiles, ...optionalFiles] : defaultFiles;
306
+
307
+ for (const fileName of filesToCopy) {
308
+ const source = path.join(deployTemplateDir, fileName);
309
+ const target = path.join(projectDir, fileName);
310
+ if (fs.existsSync(source) && !fs.existsSync(target)) {
311
+ fs.copyFileSync(source, target);
312
+ }
313
+ }
314
+ }
315
+
316
+ async function main() {
317
+ const useLocal = rawArgs.includes('--local');
318
+ const currentDir = process.cwd();
319
+ const projectName = await resolveProjectName();
320
+ const selectedEngine = await resolveEngineChoice();
321
+ const selectedPackageManager = await resolvePackageManagerChoice();
322
+ const projectDir = path.join(currentDir, projectName);
323
+
324
+ const proceed = await confirmProceed(
325
+ projectName,
326
+ projectDir,
327
+ selectedEngine,
328
+ selectedPackageManager
329
+ );
330
+ if (!proceed) {
331
+ console.log('Aborted.');
332
+ process.exit(0);
333
+ }
334
+
335
+ console.log(`Creating a new Vista app in ${projectDir}...`);
336
+
337
+ // 1. Create Directory
338
+ if (fs.existsSync(projectDir)) {
339
+ console.error(`Error: Directory ${projectName} already exists.`);
340
+ process.exit(1);
341
+ }
342
+ fs.mkdirSync(projectDir);
343
+
344
+ // 2. Copy Template
345
+ const templateDir = path.join(__dirname, '../template');
346
+ copyRecursiveSync(templateDir, projectDir);
347
+
348
+ if (useTypedApiStarter) {
349
+ const typedTemplateDir = path.join(__dirname, '../template-typed');
350
+ copyRecursiveSync(typedTemplateDir, projectDir);
351
+ console.log('Added typed API starter files.');
352
+ }
353
+
354
+ applyEngineToVistaConfig(projectDir, selectedEngine);
355
+ applyReadmeSelections(projectDir, selectedEngine, useTypedApiStarter);
356
+ if (selectedEngine === 'flashpack') {
357
+ applyFlashpackStarterTheme(projectDir);
358
+ }
359
+ if (includeDeployTemplates) {
360
+ applyDeployTemplates(projectDir, { all: rawArgs.includes('--deploy-templates-all') });
361
+ console.log('Added deployment templates (render.yaml, Dockerfile).');
362
+ }
363
+
364
+ console.log('Scaffolding complete.');
365
+
366
+ // 3. Setup Dependencies (production-ready)
367
+ const packageJson = {
368
+ name: projectName,
369
+ version: '0.1.0',
370
+ scripts: {
371
+ dev: 'vista dev',
372
+ build: 'vista build',
373
+ start: 'vista start',
374
+ deploy: 'vista deploy',
375
+ },
376
+ dependencies: {
377
+ // Runtime dependencies
378
+ react: '^19.0.0',
379
+ 'react-dom': '^19.0.0',
380
+ 'react-server-dom-webpack': '^19.0.0',
381
+ vista: useLocal ? 'file:../packages/vista' : 'npm:@vistagenic/vista@latest',
382
+ 'lucide-react': '^0.468.0',
383
+ // CSS build (needed in production for vista build)
384
+ postcss: '^8.0.0',
385
+ tailwindcss: '^4.0.0',
386
+ '@tailwindcss/postcss': '^4.0.0',
387
+ '@swc-node/register': '^1.9.0',
388
+ '@swc/core': '^1.4.0',
389
+ },
390
+ devDependencies: {
391
+ typescript: '^5.0.0',
392
+ '@types/react': '^19.0.0',
393
+ '@types/react-dom': '^19.0.0',
394
+ webpack: '^5.90.0',
395
+ 'postcss-cli': '^11.0.0',
396
+ tsx: '^4.7.0',
397
+ },
398
+ };
399
+
400
+ fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
401
+
402
+ // 4. Create .gitignore
403
+ const gitignoreContent = `# Dependencies
404
+ node_modules/
405
+ .pnpm-store/
406
+
407
+ # Build outputs
408
+ dist/
409
+ .vista/
410
+ .flash/
411
+ out/
412
+
413
+ # Rust artifacts
414
+ target/
415
+ *.node
416
+
417
+ # IDE
418
+ .idea/
419
+ .vscode/
420
+ *.swp
421
+ *.swo
422
+
423
+ # Environment
424
+ .env
425
+ .env.local
426
+ .env.development.local
427
+ .env.test.local
428
+ .env.production.local
429
+
430
+ # Logs
431
+ npm-debug.log*
432
+ yarn-debug.log*
433
+ yarn-error.log*
434
+ pnpm-debug.log*
435
+
436
+ # OS files
437
+ .DS_Store
438
+ Thumbs.db
439
+
440
+ # TypeScript
441
+ *.tsbuildinfo
442
+
443
+ # Testing
444
+ coverage/
445
+
446
+ # Misc
447
+ *.log
448
+ `;
449
+
450
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignoreContent);
451
+
452
+ console.log('Created .gitignore');
453
+
454
+ // 5. Initialize Git Repository
455
+ if (!skipGit) {
456
+ try {
457
+ execSync('git init', { cwd: projectDir, stdio: 'pipe' });
458
+ execSync('git add .', { cwd: projectDir, stdio: 'pipe' });
459
+ execSync('git commit -m "Initial commit from create-vista-app"', {
460
+ cwd: projectDir,
461
+ stdio: 'pipe',
462
+ env: {
463
+ ...process.env,
464
+ GIT_AUTHOR_NAME: 'Vista',
465
+ GIT_AUTHOR_EMAIL: 'vista@example.com',
466
+ GIT_COMMITTER_NAME: 'Vista',
467
+ GIT_COMMITTER_EMAIL: 'vista@example.com',
468
+ },
469
+ });
470
+ console.log('Initialized git repository with initial commit');
471
+ } catch (e) {
472
+ // Git might not be installed, that's okay
473
+ console.log('Note: Could not initialize git repository. You can do this manually with: git init');
474
+ }
475
+ } else {
476
+ console.log('Skipped git initialization (--no-git).');
477
+ }
478
+
479
+ // 6. Install Dependencies
480
+ const installCmd = getInstallCommand(selectedPackageManager);
481
+ if (!skipInstall) {
482
+ console.log(
483
+ `\nInstalling dependencies with ${selectedPackageManager}... This may take a moment.\n`
484
+ );
485
+ try {
486
+ execSync(installCmd, { cwd: projectDir, stdio: 'inherit' });
487
+ console.log(`\n✓ Dependencies installed successfully!`);
488
+ } catch (e) {
489
+ console.log(
490
+ `\nNote: Could not install dependencies automatically. Run "${installCmd}" manually.`
491
+ );
492
+ }
493
+ } else {
494
+ console.log('\nSkipped dependency installation (--skip-install).');
495
+ }
496
+
497
+ const runCmd = getRunCommand(selectedPackageManager);
498
+ const createCmd = getCreateCommand(selectedPackageManager);
499
+
500
+ console.log(`
501
+ ✨ Success! Created ${projectName} at ${projectDir}
502
+ Engine: ${selectedEngine}
503
+ Package manager: ${selectedPackageManager}
504
+
505
+ Get started by running:
506
+
507
+ cd ${projectName}
508
+ ${runCmd} dev
509
+
510
+ Create another app anytime with:
511
+ ${createCmd} <project-name>
512
+
513
+ Happy Hacking! 🚀
514
+ `);
515
+ }
516
+
517
+ module.exports = {
518
+ main,
519
+ detectPackageManager,
520
+ normalizePackageManager,
521
+ getInstallCommand,
522
+ getRunCommand,
523
+ getCreateCommand,
524
+ injectEngineBlock,
525
+ applyEngineToVistaConfig,
526
+ applyReadmeSelections,
527
+ applyFlashpackStarterTheme,
528
+ };
529
+
530
+ if (require.main === module) {
531
+ main().catch((error) => {
532
+ console.error('create-vista-app failed:', error);
533
+ process.exit(1);
534
+ });
535
+ }