create-atsdc-stack 1.0.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 ADDED
@@ -0,0 +1,1138 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ATSDC Stack CLI
5
+ * Command-line utility for scaffolding new projects with the ATSDC Stack
6
+ */
7
+
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname, join } from 'node:path';
10
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
11
+ import { existsSync } from 'node:fs';
12
+ import { execSync } from 'node:child_process';
13
+ import * as readline from 'node:readline/promises';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ const templateDir = join(__dirname, '..');
18
+
19
+ // ANSI color codes for terminal output
20
+ const colors = {
21
+ reset: '\x1b[0m',
22
+ bright: '\x1b[1m',
23
+ cyan: '\x1b[36m',
24
+ green: '\x1b[32m',
25
+ yellow: '\x1b[33m',
26
+ red: '\x1b[31m',
27
+ };
28
+
29
+ function log(message, color = 'reset') {
30
+ console.log(`${colors[color]}${message}${colors.reset}`);
31
+ }
32
+
33
+ function logStep(step, message) {
34
+ console.log(`${colors.cyan}[${step}]${colors.reset} ${message}`);
35
+ }
36
+
37
+ function logSuccess(message) {
38
+ console.log(`${colors.green}✓${colors.reset} ${message}`);
39
+ }
40
+
41
+ function logError(message) {
42
+ console.error(`${colors.red}✗${colors.reset} ${message}`);
43
+ }
44
+
45
+ function logWarning(message) {
46
+ console.warn(`${colors.yellow}⚠${colors.reset} ${message}`);
47
+ }
48
+
49
+ async function promptUser(question) {
50
+ const rl = readline.createInterface({
51
+ input: process.stdin,
52
+ output: process.stdout,
53
+ });
54
+
55
+ try {
56
+ const answer = await rl.question(`${colors.cyan}${question}${colors.reset} `);
57
+ return answer.trim();
58
+ } finally {
59
+ rl.close();
60
+ }
61
+ }
62
+
63
+ async function promptYesNo(question, defaultValue = false) {
64
+ const defaultText = defaultValue ? 'Y/n' : 'y/N';
65
+ const answer = await promptUser(`${question} (${defaultText}):`);
66
+
67
+ if (!answer) {
68
+ return defaultValue;
69
+ }
70
+
71
+ const normalized = answer.toLowerCase();
72
+ return normalized === 'y' || normalized === 'yes';
73
+ }
74
+
75
+ function createAdapter(name, value = null, pkg = undefined) {
76
+ // Generate value if not provided
77
+ const adapterValue = value || name.toLowerCase().split(' ')[0];
78
+
79
+ // Generate pkg if not provided
80
+ let adapterPkg;
81
+ if (pkg !== undefined) {
82
+ adapterPkg = pkg; // Use explicit value (including null)
83
+ } else {
84
+ adapterPkg = adapterValue === 'static' ? null : `@astrojs/${adapterValue}`;
85
+ }
86
+
87
+ return { name, value: adapterValue, pkg: adapterPkg };
88
+ }
89
+
90
+ async function promptAdapter() {
91
+ const adapters = {
92
+ '1': createAdapter('Vercel'),
93
+ '2': createAdapter('Netlify'),
94
+ '3': createAdapter('Cloudflare'),
95
+ '4': createAdapter('Node'),
96
+ '5': createAdapter('Static (no adapter)'),
97
+ };
98
+
99
+ console.log(`\n${colors.cyan}Select deployment adapter:${colors.reset}`);
100
+ console.log(` ${colors.green}1${colors.reset}. Vercel (default)`);
101
+ console.log(` 2. Netlify`);
102
+ console.log(` 3. Cloudflare`);
103
+ console.log(` 4. Node`);
104
+ console.log(` 5. Static (no adapter)`);
105
+
106
+ const answer = await promptUser('Enter your choice (1-5):');
107
+ const choice = answer || '1'; // Default to Vercel
108
+
109
+ const selected = adapters[choice];
110
+ if (!selected) {
111
+ logWarning(`Invalid choice, defaulting to Vercel`);
112
+ return adapters['1'];
113
+ }
114
+
115
+ return selected;
116
+ }
117
+
118
+ async function promptIntegrations() {
119
+ const integrations = {
120
+ react: { name: 'React', pkg: '@astrojs/react', deps: ['react', 'react-dom', '@types/react', '@types/react-dom'] },
121
+ vue: { name: 'Vue', pkg: '@astrojs/vue', deps: ['vue'] },
122
+ svelte: { name: 'Svelte', pkg: '@astrojs/svelte', deps: ['svelte'] },
123
+ solid: { name: 'Solid', pkg: '@astrojs/solid-js', deps: ['solid-js'] },
124
+ };
125
+
126
+ console.log(`\n${colors.cyan}Select other UI framework integrations (space to select, enter when done):${colors.reset}`);
127
+ console.log(` ${colors.green}1${colors.reset}. React (default)`);
128
+ console.log(` 2. Vue`);
129
+ console.log(` 3. Svelte`);
130
+ console.log(` 4. Solid`);
131
+ console.log(` ${colors.yellow}0${colors.reset}. None (no other UI framework)`);
132
+
133
+ const answer = await promptUser('Enter numbers separated by spaces (e.g., "1 2" for React and Vue):');
134
+ const choices = answer ? answer.split(/\s+/).filter(Boolean) : ['1']; // Default to React
135
+
136
+ const selected = [];
137
+ const choiceMap = {
138
+ '1': 'react',
139
+ '2': 'vue',
140
+ '3': 'svelte',
141
+ '4': 'solid',
142
+ };
143
+
144
+ if (!choices.includes('0')) {
145
+ for (const choice of choices) {
146
+ const key = choiceMap[choice];
147
+ if (key && integrations[key]) {
148
+ selected.push({ key, ...integrations[key] });
149
+ }
150
+ }
151
+ }
152
+
153
+ // Default to React if no valid selections
154
+ if (selected.length === 0 && !choices.includes('0')) {
155
+ logWarning('No valid integrations selected, defaulting to React');
156
+ selected.push({ key: 'react', ...integrations.react });
157
+ }
158
+
159
+ return selected;
160
+ }
161
+
162
+ function generateAstroConfig(adapter, integrations = []) {
163
+ // Generate integration imports
164
+ const integrationImports = integrations
165
+ .map(int => `import ${int.key} from '${int.pkg}';`)
166
+ .join('\n');
167
+
168
+ // Generate integration calls
169
+ const integrationCalls = integrations
170
+ .map(int => ` ${int.key}(),`)
171
+ .join('\n');
172
+
173
+ const configs = {
174
+ vercel: `import { defineConfig } from 'astro/config';
175
+ ${integrationImports}
176
+ import vercel from '@astrojs/vercel';
177
+ import clerk from '@clerk/astro';
178
+ import { VitePWA } from 'vite-plugin-pwa';
179
+
180
+ export default defineConfig({
181
+ output: 'server',
182
+ adapter: vercel({
183
+ imageService: true,
184
+ }),
185
+ image: {
186
+ service: {
187
+ entrypoint: 'astro/assets/services/noop',
188
+ },
189
+ },
190
+ integrations: [
191
+ ${integrationCalls}
192
+ clerk({
193
+ afterSignInUrl: '/',
194
+ afterSignUpUrl: '/',
195
+ }),
196
+ ],
197
+ vite: {
198
+ plugins: [
199
+ VitePWA({
200
+ registerType: 'autoUpdate',
201
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
202
+ manifest: {
203
+ name: 'ATSDC Stack App',
204
+ short_name: 'ATSDC',
205
+ description: 'Progressive Web App built with the ATSDC Stack',
206
+ theme_color: '#ffffff',
207
+ background_color: '#ffffff',
208
+ display: 'standalone',
209
+ icons: [
210
+ {
211
+ src: 'pwa-192x192.png',
212
+ sizes: '192x192',
213
+ type: 'image/png',
214
+ },
215
+ {
216
+ src: 'pwa-512x512.png',
217
+ sizes: '512x512',
218
+ type: 'image/png',
219
+ },
220
+ {
221
+ src: 'pwa-512x512.png',
222
+ sizes: '512x512',
223
+ type: 'image/png',
224
+ purpose: 'any maskable',
225
+ },
226
+ ],
227
+ },
228
+ workbox: {
229
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
230
+ runtimeCaching: [
231
+ {
232
+ urlPattern: /^https:\\/\\/api\\./i,
233
+ handler: 'NetworkFirst',
234
+ options: {
235
+ cacheName: 'api-cache',
236
+ expiration: {
237
+ maxEntries: 50,
238
+ maxAgeSeconds: 60 * 60 * 24,
239
+ },
240
+ },
241
+ },
242
+ ],
243
+ },
244
+ }),
245
+ ],
246
+ css: {
247
+ preprocessorOptions: {
248
+ scss: {
249
+ api: 'modern-compiler',
250
+ additionalData: \`@use "@/styles/variables/globals.scss" as *;\`,
251
+ },
252
+ },
253
+ },
254
+ },
255
+ });
256
+ `,
257
+ netlify: `import { defineConfig } from 'astro/config';
258
+ ${integrationImports}
259
+ import netlify from '@astrojs/netlify';
260
+ import clerk from '@clerk/astro';
261
+ import { VitePWA } from 'vite-plugin-pwa';
262
+
263
+ export default defineConfig({
264
+ output: 'server',
265
+ adapter: netlify(),
266
+ integrations: [
267
+ ${integrationCalls}
268
+ clerk({
269
+ afterSignInUrl: '/',
270
+ afterSignUpUrl: '/',
271
+ }),
272
+ ],
273
+ vite: {
274
+ plugins: [
275
+ VitePWA({
276
+ registerType: 'autoUpdate',
277
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
278
+ manifest: {
279
+ name: 'ATSDC Stack App',
280
+ short_name: 'ATSDC',
281
+ description: 'Progressive Web App built with the ATSDC Stack',
282
+ theme_color: '#ffffff',
283
+ background_color: '#ffffff',
284
+ display: 'standalone',
285
+ icons: [
286
+ {
287
+ src: 'pwa-192x192.png',
288
+ sizes: '192x192',
289
+ type: 'image/png',
290
+ },
291
+ {
292
+ src: 'pwa-512x512.png',
293
+ sizes: '512x512',
294
+ type: 'image/png',
295
+ },
296
+ {
297
+ src: 'pwa-512x512.png',
298
+ sizes: '512x512',
299
+ type: 'image/png',
300
+ purpose: 'any maskable',
301
+ },
302
+ ],
303
+ },
304
+ workbox: {
305
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
306
+ runtimeCaching: [
307
+ {
308
+ urlPattern: /^https:\\/\\/api\\./i,
309
+ handler: 'NetworkFirst',
310
+ options: {
311
+ cacheName: 'api-cache',
312
+ expiration: {
313
+ maxEntries: 50,
314
+ maxAgeSeconds: 60 * 60 * 24,
315
+ },
316
+ },
317
+ },
318
+ ],
319
+ },
320
+ }),
321
+ ],
322
+ css: {
323
+ preprocessorOptions: {
324
+ scss: {
325
+ api: 'modern-compiler',
326
+ additionalData: \`@use "@/styles/variables/globals.scss" as *;\`,
327
+ },
328
+ },
329
+ },
330
+ },
331
+ });
332
+ `,
333
+ cloudflare: `import { defineConfig} from 'astro/config';
334
+ ${integrationImports}
335
+ import cloudflare from '@astrojs/cloudflare';
336
+ import clerk from '@clerk/astro';
337
+ import { VitePWA } from 'vite-plugin-pwa';
338
+
339
+ export default defineConfig({
340
+ output: 'server',
341
+ adapter: cloudflare(),
342
+ integrations: [
343
+ ${integrationCalls}
344
+ clerk({
345
+ afterSignInUrl: '/',
346
+ afterSignUpUrl: '/',
347
+ }),
348
+ ],
349
+ vite: {
350
+ plugins: [
351
+ VitePWA({
352
+ registerType: 'autoUpdate',
353
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
354
+ manifest: {
355
+ name: 'ATSDC Stack App',
356
+ short_name: 'ATSDC',
357
+ description: 'Progressive Web App built with the ATSDC Stack',
358
+ theme_color: '#ffffff',
359
+ background_color: '#ffffff',
360
+ display: 'standalone',
361
+ icons: [
362
+ {
363
+ src: 'pwa-192x192.png',
364
+ sizes: '192x192',
365
+ type: 'image/png',
366
+ },
367
+ {
368
+ src: 'pwa-512x512.png',
369
+ sizes: '512x512',
370
+ type: 'image/png',
371
+ },
372
+ {
373
+ src: 'pwa-512x512.png',
374
+ sizes: '512x512',
375
+ type: 'image/png',
376
+ purpose: 'any maskable',
377
+ },
378
+ ],
379
+ },
380
+ workbox: {
381
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
382
+ runtimeCaching: [
383
+ {
384
+ urlPattern: /^https:\\/\\/api\\./i,
385
+ handler: 'NetworkFirst',
386
+ options: {
387
+ cacheName: 'api-cache',
388
+ expiration: {
389
+ maxEntries: 50,
390
+ maxAgeSeconds: 60 * 60 * 24,
391
+ },
392
+ },
393
+ },
394
+ ],
395
+ },
396
+ }),
397
+ ],
398
+ css: {
399
+ preprocessorOptions: {
400
+ scss: {
401
+ api: 'modern-compiler',
402
+ additionalData: \`@use "@/styles/variables/globals.scss" as *;\`,
403
+ },
404
+ },
405
+ },
406
+ },
407
+ });
408
+ `,
409
+ node: `import { defineConfig } from 'astro/config';
410
+ ${integrationImports}
411
+ import node from '@astrojs/node';
412
+ import clerk from '@clerk/astro';
413
+ import { VitePWA } from 'vite-plugin-pwa';
414
+
415
+ export default defineConfig({
416
+ output: 'server',
417
+ adapter: node({
418
+ mode: 'standalone'
419
+ }),
420
+ integrations: [
421
+ ${integrationCalls}
422
+ clerk({
423
+ afterSignInUrl: '/',
424
+ afterSignUpUrl: '/',
425
+ }),
426
+ ],
427
+ vite: {
428
+ plugins: [
429
+ VitePWA({
430
+ registerType: 'autoUpdate',
431
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
432
+ manifest: {
433
+ name: 'ATSDC Stack App',
434
+ short_name: 'ATSDC',
435
+ description: 'Progressive Web App built with the ATSDC Stack',
436
+ theme_color: '#ffffff',
437
+ background_color: '#ffffff',
438
+ display: 'standalone',
439
+ icons: [
440
+ {
441
+ src: 'pwa-192x192.png',
442
+ sizes: '192x192',
443
+ type: 'image/png',
444
+ },
445
+ {
446
+ src: 'pwa-512x512.png',
447
+ sizes: '512x512',
448
+ type: 'image/png',
449
+ },
450
+ {
451
+ src: 'pwa-512x512.png',
452
+ sizes: '512x512',
453
+ type: 'image/png',
454
+ purpose: 'any maskable',
455
+ },
456
+ ],
457
+ },
458
+ workbox: {
459
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
460
+ runtimeCaching: [
461
+ {
462
+ urlPattern: /^https:\\/\\/api\\./i,
463
+ handler: 'NetworkFirst',
464
+ options: {
465
+ cacheName: 'api-cache',
466
+ expiration: {
467
+ maxEntries: 50,
468
+ maxAgeSeconds: 60 * 60 * 24,
469
+ },
470
+ },
471
+ },
472
+ ],
473
+ },
474
+ }),
475
+ ],
476
+ css: {
477
+ preprocessorOptions: {
478
+ scss: {
479
+ api: 'modern-compiler',
480
+ additionalData: \`@use "@/styles/variables/globals.scss" as *;\`,
481
+ },
482
+ },
483
+ },
484
+ },
485
+ });
486
+ `,
487
+ static: `import { defineConfig } from 'astro/config';
488
+ ${integrationImports}
489
+ import clerk from '@clerk/astro';
490
+ import { VitePWA } from 'vite-plugin-pwa';
491
+
492
+ export default defineConfig({
493
+ output: 'static',
494
+ integrations: [
495
+ ${integrationCalls}
496
+ clerk({
497
+ afterSignInUrl: '/',
498
+ afterSignUpUrl: '/',
499
+ }),
500
+ ],
501
+ vite: {
502
+ plugins: [
503
+ VitePWA({
504
+ registerType: 'autoUpdate',
505
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
506
+ manifest: {
507
+ name: 'ATSDC Stack App',
508
+ short_name: 'ATSDC',
509
+ description: 'Progressive Web App built with the ATSDC Stack',
510
+ theme_color: '#ffffff',
511
+ background_color: '#ffffff',
512
+ display: 'standalone',
513
+ icons: [
514
+ {
515
+ src: 'pwa-192x192.png',
516
+ sizes: '192x192',
517
+ type: 'image/png',
518
+ },
519
+ {
520
+ src: 'pwa-512x512.png',
521
+ sizes: '512x512',
522
+ type: 'image/png',
523
+ },
524
+ {
525
+ src: 'pwa-512x512.png',
526
+ sizes: '512x512',
527
+ type: 'image/png',
528
+ purpose: 'any maskable',
529
+ },
530
+ ],
531
+ },
532
+ workbox: {
533
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
534
+ runtimeCaching: [
535
+ {
536
+ urlPattern: /^https:\\/\\/api\\./i,
537
+ handler: 'NetworkFirst',
538
+ options: {
539
+ cacheName: 'api-cache',
540
+ expiration: {
541
+ maxEntries: 50,
542
+ maxAgeSeconds: 60 * 60 * 24,
543
+ },
544
+ },
545
+ },
546
+ ],
547
+ },
548
+ }),
549
+ ],
550
+ css: {
551
+ preprocessorOptions: {
552
+ scss: {
553
+ api: 'modern-compiler',
554
+ additionalData: \`@use "@/styles/variables/globals.scss" as *;\`,
555
+ },
556
+ },
557
+ },
558
+ },
559
+ });
560
+ `,
561
+ };
562
+
563
+ return configs[adapter] || configs.vercel;
564
+ }
565
+
566
+ async function setupDatabase(projectDir) {
567
+ try {
568
+ logStep('DB', 'Setting up database...');
569
+
570
+ // Check if .env file exists
571
+ const envPath = join(projectDir, '.env');
572
+ if (!existsSync(envPath)) {
573
+ logWarning('No .env file found. Skipping database setup.');
574
+ logWarning('Please copy .env.example to .env and configure your DATABASE_URL');
575
+ return false;
576
+ }
577
+
578
+ // Try to run drizzle-kit push
579
+ execSync('npm run db:push', {
580
+ cwd: projectDir,
581
+ stdio: 'inherit',
582
+ });
583
+
584
+ logSuccess('Database schema pushed successfully');
585
+ return true;
586
+ } catch (error) {
587
+ logError('Failed to push database schema');
588
+ logWarning('Please configure your DATABASE_URL in .env and run: npm run db:push');
589
+ console.log(`\nError details: ${error.message}`);
590
+ return false;
591
+ }
592
+ }
593
+
594
+ async function createProject(projectName, options = {}) {
595
+ const targetDir = join(process.cwd(), projectName);
596
+
597
+ try {
598
+ // Step 1: Check if directory exists
599
+ logStep(1, 'Checking project directory...');
600
+ if (existsSync(targetDir)) {
601
+ logError(`Directory "${projectName}" already exists!`);
602
+ process.exit(1);
603
+ }
604
+
605
+ // Step 2: Create project directory
606
+ logStep(2, `Creating project directory: ${projectName}`);
607
+ await mkdir(targetDir, { recursive: true });
608
+ logSuccess('Directory created');
609
+
610
+ // Step 3: Copy template files
611
+ logStep(3, 'Copying template files...');
612
+
613
+ const appDir = join(templateDir, 'app');
614
+
615
+ // Copy files from app directory (excluding astro.config.mjs - we'll generate it)
616
+ const filesToCopy = [
617
+ 'package.json',
618
+ 'tsconfig.json',
619
+ 'drizzle.config.ts',
620
+ '.env.example',
621
+ '.gitignore',
622
+ 'README.md',
623
+ ];
624
+
625
+ for (const file of filesToCopy) {
626
+ const srcPath = join(appDir, file);
627
+ const destPath = join(targetDir, file);
628
+
629
+ if (existsSync(srcPath)) {
630
+ await copyFile(srcPath, destPath);
631
+ }
632
+ }
633
+
634
+ // Generate astro.config.mjs based on adapter and integrations
635
+ const astroConfigPath = join(targetDir, 'astro.config.mjs');
636
+ const astroConfig = generateAstroConfig(options.adapter || 'vercel', options.integrations || []);
637
+ await writeFile(astroConfigPath, astroConfig);
638
+ logSuccess(`Generated astro.config.mjs with ${options.adapter || 'vercel'} adapter`);
639
+
640
+ // Copy directory structures from app
641
+ const dirsToCopy = ['src', 'public'];
642
+ for (const dir of dirsToCopy) {
643
+ const srcPath = join(appDir, dir);
644
+ const destPath = join(targetDir, dir);
645
+
646
+ if (existsSync(srcPath)) {
647
+ await copyDirectory(srcPath, destPath);
648
+ }
649
+ }
650
+
651
+ logSuccess('Template files copied');
652
+
653
+ // Step 4: Update package.json with project name, adapter, and integrations
654
+ logStep(4, 'Updating package.json...');
655
+ const packageJsonPath = join(targetDir, 'package.json');
656
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
657
+ packageJson.name = projectName;
658
+
659
+ // Initialize dependencies if not present
660
+ if (!packageJson.dependencies) {
661
+ packageJson.dependencies = {};
662
+ }
663
+ if (!packageJson.devDependencies) {
664
+ packageJson.devDependencies = {};
665
+ }
666
+
667
+ // Add adapter dependency if not static
668
+ const adapterInfo = options.adapterInfo;
669
+ if (adapterInfo && adapterInfo.pkg) {
670
+ packageJson.dependencies[adapterInfo.pkg] = packageJson.dependencies['@astrojs/vercel'] || '^7.8.1';
671
+ // Remove vercel adapter if using a different one
672
+ if (adapterInfo.value !== 'vercel' && packageJson.dependencies['@astrojs/vercel']) {
673
+ delete packageJson.dependencies['@astrojs/vercel'];
674
+ }
675
+ } else if (options.adapter === 'static') {
676
+ // Remove all adapters for static build
677
+ if (packageJson.dependencies['@astrojs/vercel']) {
678
+ delete packageJson.dependencies['@astrojs/vercel'];
679
+ }
680
+ }
681
+
682
+ // Remove React by default (will be added back if selected)
683
+ if (packageJson.dependencies['@astrojs/react']) {
684
+ delete packageJson.dependencies['@astrojs/react'];
685
+ }
686
+ if (packageJson.dependencies['react']) {
687
+ delete packageJson.dependencies['react'];
688
+ }
689
+ if (packageJson.dependencies['react-dom']) {
690
+ delete packageJson.dependencies['react-dom'];
691
+ }
692
+ if (packageJson.devDependencies['@types/react']) {
693
+ delete packageJson.devDependencies['@types/react'];
694
+ }
695
+ if (packageJson.devDependencies['@types/react-dom']) {
696
+ delete packageJson.devDependencies['@types/react-dom'];
697
+ }
698
+
699
+ // Add selected integrations and their dependencies
700
+ const integrations = options.integrations || [];
701
+ for (const integration of integrations) {
702
+ // Add the integration package
703
+ packageJson.dependencies[integration.pkg] = '^latest';
704
+
705
+ // Add framework dependencies
706
+ for (const dep of integration.deps) {
707
+ if (dep.startsWith('@types/')) {
708
+ packageJson.devDependencies[dep] = '^latest';
709
+ } else {
710
+ packageJson.dependencies[dep] = '^latest';
711
+ }
712
+ }
713
+ }
714
+
715
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 4));
716
+ logSuccess('package.json updated');
717
+
718
+ // Step 5: Create .env from .env.example
719
+ logStep(5, 'Creating environment file...');
720
+ const envExamplePath = join(targetDir, '.env.example');
721
+ const envPath = join(targetDir, '.env');
722
+ if (existsSync(envExamplePath)) {
723
+ await copyFile(envExamplePath, envPath);
724
+ logSuccess('.env created from .env.example');
725
+ } else {
726
+ logWarning('No .env.example found, skipping .env creation');
727
+ }
728
+
729
+ // Step 6: Install dependencies if requested
730
+ if (options.install) {
731
+ logStep(6, 'Installing dependencies...');
732
+ try {
733
+ execSync('npm install', {
734
+ cwd: targetDir,
735
+ stdio: 'inherit',
736
+ });
737
+ logSuccess('Dependencies installed');
738
+ } catch (error) {
739
+ logWarning('Failed to install dependencies. You can run npm install manually.');
740
+ }
741
+ }
742
+
743
+ // Step 7: Setup database if requested
744
+ if (options.setupDb && options.install) {
745
+ await setupDatabase(targetDir);
746
+ }
747
+
748
+ // Display next steps
749
+ log('\n' + '='.repeat(60), 'bright');
750
+ log('🎉 Project created successfully!', 'green');
751
+ log('='.repeat(60), 'bright');
752
+
753
+ console.log('\nNext steps:');
754
+ let step = 1;
755
+ console.log(` ${step++}. ${colors.cyan}cd ${projectName}${colors.reset}`);
756
+
757
+ if (!options.install) {
758
+ console.log(` ${step++}. ${colors.cyan}npm install${colors.reset}`);
759
+ }
760
+
761
+ console.log(` ${step++}. Edit ${colors.yellow}.env${colors.reset} and fill in your database credentials and API keys`);
762
+
763
+ if (!options.setupDb) {
764
+ console.log(` ${step++}. ${colors.cyan}npm run db:push${colors.reset} - Push database schema`);
765
+ }
766
+
767
+ console.log(` ${step++}. ${colors.cyan}npm run dev${colors.reset} - Start development server`);
768
+
769
+ console.log('\nDocumentation:');
770
+ console.log(` • Astro: ${colors.cyan}https://astro.build${colors.reset}`);
771
+ console.log(` • Drizzle ORM: ${colors.cyan}https://orm.drizzle.team${colors.reset}`);
772
+ console.log(` • Clerk: ${colors.cyan}https://clerk.com/docs${colors.reset}`);
773
+ console.log(` • Vercel AI SDK: ${colors.cyan}https://sdk.vercel.ai${colors.reset}`);
774
+ console.log(` • Exa Search: ${colors.cyan}https://docs.exa.ai${colors.reset}`);
775
+
776
+ console.log('\nNew utilities added:');
777
+ console.log(` • Cheerio - DOM manipulation in ${colors.cyan}src/lib/dom-utils.ts${colors.reset}`);
778
+ console.log(` • Marked/Turndown - Content conversion in ${colors.cyan}src/lib/content-converter.ts${colors.reset}`);
779
+ console.log(` • Exa - AI search in ${colors.cyan}src/lib/exa-search.ts${colors.reset}`);
780
+
781
+ log('\n' + '='.repeat(60), 'bright');
782
+
783
+ // Step 8: Vercel CLI login (if dependencies were installed) - at the very end
784
+ if (options.install) {
785
+ const shouldLoginVercel = await promptYesNo(
786
+ '\nLogin to Vercel now?',
787
+ true
788
+ );
789
+
790
+ if (shouldLoginVercel) {
791
+ try {
792
+ log('\n' + '='.repeat(60), 'bright');
793
+ logStep('Vercel', 'Launching Vercel CLI login...');
794
+ execSync('npx vercel login', {
795
+ cwd: targetDir,
796
+ stdio: 'inherit',
797
+ });
798
+ logSuccess('Vercel login completed');
799
+ log('='.repeat(60), 'bright');
800
+ } catch (error) {
801
+ logWarning('Vercel login skipped or failed. You can login later with: npx vercel login');
802
+ }
803
+ } else {
804
+ console.log(`\n ${colors.yellow}ℹ${colors.reset} You can login to Vercel later with: ${colors.cyan}npx vercel login${colors.reset}`);
805
+ }
806
+ }
807
+
808
+ } catch (error) {
809
+ logError(`Failed to create project: ${error.message}`);
810
+ process.exit(1);
811
+ }
812
+ }
813
+
814
+ async function copyDirectory(src, dest) {
815
+ const { readdir, stat } = await import('node:fs/promises');
816
+
817
+ await mkdir(dest, { recursive: true });
818
+ const entries = await readdir(src, { withFileTypes: true });
819
+
820
+ for (const entry of entries) {
821
+ const srcPath = join(src, entry.name);
822
+ const destPath = join(dest, entry.name);
823
+
824
+ if (entry.isDirectory()) {
825
+ await copyDirectory(srcPath, destPath);
826
+ } else {
827
+ await copyFile(srcPath, destPath);
828
+ }
829
+ }
830
+ }
831
+
832
+ // Main CLI logic
833
+ const args = process.argv.slice(2);
834
+
835
+ // Check for help or version flags first
836
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
837
+ console.log(`
838
+ ${colors.bright}${colors.cyan}╔════════════════════════════════════════════════════════════════════╗
839
+ ║ ATSDC Stack CLI v1.0 ║
840
+ ║ Production-Ready Full-Stack Application Generator ║
841
+ ╚════════════════════════════════════════════════════════════════════╝${colors.reset}
842
+
843
+ ${colors.bright}${colors.green}USAGE${colors.reset}
844
+ ${colors.cyan}npx create-atsdc-stack${colors.reset} ${colors.yellow}[project-name]${colors.reset} ${colors.yellow}[options]${colors.reset}
845
+
846
+ ${colors.bright}${colors.green}DESCRIPTION${colors.reset}
847
+ The ATSDC Stack CLI scaffolds production-ready full-stack applications
848
+ with best-in-class technologies. Create modern web apps with type safety,
849
+ authentication, database operations, and AI integration out of the box.
850
+
851
+ ${colors.yellow}🤖 Interactive Mode:${colors.reset}
852
+ This CLI features intelligent interactive prompts. Any option not provided
853
+ as a command-line argument will trigger an interactive prompt, making it
854
+ easy for both beginners and power users.
855
+
856
+ ${colors.bright}${colors.green}ARGUMENTS${colors.reset}
857
+ ${colors.cyan}project-name${colors.reset}
858
+ The name of your new project directory. If omitted, you will be
859
+ prompted to enter it interactively.
860
+
861
+ ${colors.yellow}Validation:${colors.reset} Only letters, numbers, hyphens, and underscores
862
+ ${colors.yellow}Example:${colors.reset} my-app, my_blog, myapp123
863
+
864
+ ${colors.bright}${colors.green}OPTIONS${colors.reset}
865
+ ${colors.cyan}--install, -i${colors.reset}
866
+ Automatically install npm dependencies after creating the project.
867
+ If omitted, you will be prompted (default: ${colors.green}Yes${colors.reset})
868
+
869
+ ${colors.yellow}What it does:${colors.reset}
870
+ • Runs 'npm install' in the project directory
871
+ • Installs all dependencies from package.json
872
+ • Required for --setup-db to work
873
+
874
+ ${colors.cyan}--setup-db, --db${colors.reset}
875
+ Set up the database schema after installation.
876
+ If omitted, you will be prompted (default: ${colors.red}No${colors.reset})
877
+ ${colors.yellow}Requires: --install${colors.reset}
878
+
879
+ ${colors.yellow}What it does:${colors.reset}
880
+ • Runs 'npm run db:push' to sync schema with database
881
+ • Requires DATABASE_URL to be configured in .env
882
+ • Creates tables defined in src/db/schema.ts
883
+
884
+ ${colors.red}Note:${colors.reset} You must configure your DATABASE_URL in .env before
885
+ this will work. If not configured, this step will be skipped with
886
+ a warning.
887
+
888
+ ${colors.cyan}--adapter, -a <adapter>${colors.reset}
889
+ Choose deployment adapter for your Astro application.
890
+ If omitted, you will be prompted (default: ${colors.green}vercel${colors.reset})
891
+
892
+ ${colors.yellow}Available adapters:${colors.reset}
893
+ • ${colors.green}vercel${colors.reset} - Deploy to Vercel (serverless)
894
+ • netlify - Deploy to Netlify
895
+ • cloudflare - Deploy to Cloudflare Pages
896
+ • node - Deploy to Node.js server
897
+ • static - Static site generation (no adapter)
898
+
899
+ ${colors.yellow}Examples:${colors.reset}
900
+ --adapter vercel
901
+ --adapter static
902
+ -a netlify
903
+
904
+ ${colors.cyan}--help, -h${colors.reset}
905
+ Display this help message and exit.
906
+
907
+ ${colors.cyan}--version, -v${colors.reset}
908
+ Display the CLI version number and exit.
909
+
910
+ ${colors.bright}${colors.green}EXAMPLES${colors.reset}
911
+ ${colors.yellow}# Fully interactive - prompts for everything${colors.reset}
912
+ npx create-atsdc-stack
913
+
914
+ ${colors.yellow}# Provide name, get prompted for install/setup options${colors.reset}
915
+ npx create-atsdc-stack my-awesome-app
916
+
917
+ ${colors.yellow}# Auto-install dependencies, prompt for database setup${colors.reset}
918
+ npx create-atsdc-stack my-blog --install
919
+
920
+ ${colors.yellow}# Full automatic setup (recommended for experienced users)${colors.reset}
921
+ npx create-atsdc-stack my-app --install --setup-db
922
+
923
+ ${colors.yellow}# Short flags work too${colors.reset}
924
+ npx create-atsdc-stack my-app -i --db
925
+
926
+ ${colors.yellow}# Specify deployment adapter${colors.reset}
927
+ npx create-atsdc-stack my-app --adapter netlify
928
+ npx create-atsdc-stack my-app --adapter static --install
929
+
930
+ ${colors.yellow}# Complete setup with all options${colors.reset}
931
+ npx create-atsdc-stack my-app -i --db -a vercel
932
+
933
+ ${colors.bright}${colors.green}WHAT GETS CREATED${colors.reset}
934
+ ${colors.cyan}Project Structure:${colors.reset}
935
+ • src/ - Source code directory
936
+ ├── components/ - Reusable Astro components
937
+ ├── db/ - Database schema and client
938
+ ├── layouts/ - Page layouts
939
+ ├── lib/ - Utility libraries
940
+ ├── pages/ - Routes and API endpoints
941
+ └── styles/ - SCSS stylesheets
942
+ • public/ - Static assets
943
+ • .env - Environment variables (with examples)
944
+ • package.json - Dependencies and scripts
945
+ • astro.config.mjs - Astro configuration
946
+ • drizzle.config.ts - Database ORM configuration
947
+ • tsconfig.json - TypeScript configuration
948
+
949
+ ${colors.bright}${colors.green}TECHNOLOGY STACK${colors.reset}
950
+ ${colors.bright}Core Framework:${colors.reset}
951
+ ${colors.cyan}• Astro 4.x${colors.reset} - Modern web framework with zero-JS by default
952
+ Perfect for content sites and dynamic apps
953
+
954
+ ${colors.bright}Type Safety & Validation:${colors.reset}
955
+ ${colors.cyan}• TypeScript 5.x${colors.reset} - Full type safety across your entire stack
956
+ ${colors.cyan}• Zod 3.x${colors.reset} - Runtime validation with TypeScript integration
957
+
958
+ ${colors.bright}Database:${colors.reset}
959
+ ${colors.cyan}• Drizzle ORM${colors.reset} - Type-safe database operations
960
+ ${colors.cyan}• PostgreSQL${colors.reset} - Powerful relational database (via Vercel/Neon)
961
+ ${colors.cyan}• NanoID${colors.reset} - Secure unique ID generation for records
962
+
963
+ ${colors.bright}Authentication:${colors.reset}
964
+ ${colors.cyan}• Clerk${colors.reset} - Complete user management and authentication
965
+ Includes social logins, 2FA, user profiles
966
+
967
+ ${colors.bright}Styling:${colors.reset}
968
+ ${colors.cyan}• SCSS${colors.reset} - Advanced CSS with variables, mixins, nesting
969
+ Data attributes preferred over BEM classes
970
+
971
+ ${colors.bright}AI & Content:${colors.reset}
972
+ ${colors.cyan}• Vercel AI SDK${colors.reset} - Seamless LLM integration (OpenAI, Anthropic, etc.)
973
+ ${colors.cyan}• Exa${colors.reset} - AI-powered semantic search
974
+ ${colors.cyan}• Cheerio${colors.reset} - Server-side DOM manipulation
975
+ ${colors.cyan}• Marked${colors.reset} - Markdown to HTML conversion
976
+ ${colors.cyan}• Turndown${colors.reset} - HTML to Markdown conversion
977
+
978
+ ${colors.bright}Progressive Web App:${colors.reset}
979
+ ${colors.cyan}• Vite PWA${colors.reset} - Offline support, installable apps, service workers
980
+
981
+ ${colors.bright}${colors.green}NEXT STEPS AFTER CREATION${colors.reset}
982
+ ${colors.yellow}1.${colors.reset} ${colors.cyan}cd your-project-name${colors.reset}
983
+
984
+ ${colors.yellow}2.${colors.reset} Configure environment variables in ${colors.cyan}.env${colors.reset}:
985
+ • DATABASE_URL - PostgreSQL connection string
986
+ • PUBLIC_CLERK_PUBLISHABLE_KEY - Get from clerk.com
987
+ • CLERK_SECRET_KEY - Get from clerk.com
988
+ • OPENAI_API_KEY - Get from platform.openai.com
989
+ • EXA_API_KEY - Get from exa.ai (optional)
990
+
991
+ ${colors.yellow}3.${colors.reset} Push database schema: ${colors.cyan}npm run db:push${colors.reset}
992
+
993
+ ${colors.yellow}4.${colors.reset} Start development server: ${colors.cyan}npm run dev${colors.reset}
994
+
995
+ ${colors.yellow}5.${colors.reset} Open ${colors.cyan}http://localhost:4321${colors.reset}
996
+
997
+ ${colors.bright}${colors.green}AVAILABLE SCRIPTS${colors.reset}
998
+ ${colors.cyan}npm run dev${colors.reset} - Start development server (port 4321)
999
+ ${colors.cyan}npm run build${colors.reset} - Build for production
1000
+ ${colors.cyan}npm run preview${colors.reset} - Preview production build locally
1001
+ ${colors.cyan}npm run db:push${colors.reset} - Push schema changes to database
1002
+ ${colors.cyan}npm run db:generate${colors.reset} - Generate migration files
1003
+ ${colors.cyan}npm run db:studio${colors.reset} - Open Drizzle Studio (database GUI)
1004
+
1005
+ ${colors.bright}${colors.green}RESOURCES${colors.reset}
1006
+ ${colors.cyan}Documentation:${colors.reset}
1007
+ • Astro: https://docs.astro.build
1008
+ • Drizzle ORM: https://orm.drizzle.team
1009
+ • Clerk: https://clerk.com/docs
1010
+ • Zod: https://zod.dev
1011
+ • Vercel AI: https://sdk.vercel.ai/docs
1012
+ • Exa Search: https://docs.exa.ai
1013
+
1014
+ ${colors.cyan}GitHub:${colors.reset}
1015
+ • Repository: https://github.com/yourusername/atsdc-stack
1016
+ • Issues: https://github.com/yourusername/atsdc-stack/issues
1017
+
1018
+ ${colors.bright}${colors.green}TIPS${colors.reset}
1019
+ ${colors.yellow}•${colors.reset} Use ${colors.cyan}--install${colors.reset} flag to save time on dependency installation
1020
+ ${colors.yellow}•${colors.reset} Set up database credentials before using ${colors.cyan}--setup-db${colors.reset}
1021
+ ${colors.yellow}•${colors.reset} Check ${colors.cyan}.env.example${colors.reset} for all required environment variables
1022
+ ${colors.yellow}•${colors.reset} Use ${colors.cyan}npm run db:studio${colors.reset} to visually manage your database
1023
+ ${colors.yellow}•${colors.reset} Data attributes are preferred over BEM for SCSS modifiers
1024
+
1025
+ ${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════════════${colors.reset}
1026
+ `);
1027
+ process.exit(0);
1028
+ }
1029
+
1030
+ if (args.includes('--version') || args.includes('-v')) {
1031
+ const packageJson = JSON.parse(
1032
+ await readFile(join(templateDir, 'package.json'), 'utf-8')
1033
+ );
1034
+ console.log(packageJson.version);
1035
+ process.exit(0);
1036
+ }
1037
+
1038
+ // Check if flags were explicitly passed
1039
+ const installFlagPassed = args.includes('--install') || args.includes('-i');
1040
+ const setupDbFlagPassed = args.includes('--setup-db') || args.includes('--db');
1041
+ const adapterFlagIndex = args.findIndex(arg => arg === '--adapter' || arg === '-a');
1042
+ const adapterFlagPassed = adapterFlagIndex !== -1;
1043
+
1044
+ // Get adapter value from flag if provided
1045
+ let adapterValue = null;
1046
+ if (adapterFlagPassed && args[adapterFlagIndex + 1]) {
1047
+ adapterValue = args[adapterFlagIndex + 1].toLowerCase();
1048
+ }
1049
+
1050
+ // Get project name (first argument that's not a flag)
1051
+ let projectName = args.find(arg => !arg.startsWith('-') && arg !== adapterValue);
1052
+
1053
+ // Interactive mode setup
1054
+ const needsInteractive = !projectName || !installFlagPassed || (!setupDbFlagPassed && installFlagPassed);
1055
+
1056
+ if (needsInteractive && !projectName) {
1057
+ log('\n' + '='.repeat(60), 'bright');
1058
+ log('Welcome to ATSDC Stack!', 'cyan');
1059
+ log('='.repeat(60), 'bright');
1060
+ console.log();
1061
+ }
1062
+
1063
+ // Prompt for project name if not provided
1064
+ if (!projectName) {
1065
+ projectName = await promptUser('What would you like to name your project?');
1066
+
1067
+ if (!projectName) {
1068
+ logError('Project name is required');
1069
+ process.exit(1);
1070
+ }
1071
+
1072
+ // Validate project name (basic validation)
1073
+ if (!/^[a-z0-9-_]+$/i.test(projectName)) {
1074
+ logError('Project name can only contain letters, numbers, hyphens, and underscores');
1075
+ process.exit(1);
1076
+ }
1077
+ console.log();
1078
+ }
1079
+
1080
+ // Prompt for install flag if not provided
1081
+ let shouldInstall = installFlagPassed;
1082
+ if (!installFlagPassed) {
1083
+ shouldInstall = await promptYesNo('Install dependencies now?', true);
1084
+ console.log();
1085
+ }
1086
+
1087
+ // Prompt for setup-db flag if not provided (only if installing)
1088
+ let shouldSetupDb = setupDbFlagPassed;
1089
+ if (shouldInstall && !setupDbFlagPassed) {
1090
+ shouldSetupDb = await promptYesNo('Set up the database now?', false);
1091
+ console.log();
1092
+ }
1093
+
1094
+ // Prompt for adapter if not provided
1095
+ let selectedAdapter;
1096
+ if (adapterFlagPassed && adapterValue) {
1097
+ // Validate adapter value
1098
+ const validAdapters = { vercel: true, netlify: true, cloudflare: true, node: true, static: true };
1099
+ if (validAdapters[adapterValue]) {
1100
+ const adapterMap = {
1101
+ vercel: createAdapter('Vercel'),
1102
+ netlify: createAdapter('Netlify'),
1103
+ cloudflare: createAdapter('Cloudflare'),
1104
+ node: createAdapter('Node'),
1105
+ static: createAdapter('Static (no adapter)'),
1106
+ };
1107
+ selectedAdapter = adapterMap[adapterValue];
1108
+ logSuccess(`Using ${selectedAdapter.name} adapter`);
1109
+ } else {
1110
+ logWarning(`Invalid adapter '${adapterValue}', using default`);
1111
+ selectedAdapter = createAdapter('Vercel');
1112
+ }
1113
+ } else {
1114
+ selectedAdapter = await promptAdapter();
1115
+ console.log();
1116
+ }
1117
+
1118
+ // Prompt for integrations
1119
+ const selectedIntegrations = await promptIntegrations();
1120
+ if (selectedIntegrations.length > 0) {
1121
+ const integrationNames = selectedIntegrations.map(int => int.name).join(', ');
1122
+ logSuccess(`Selected integrations: ${integrationNames}`);
1123
+ } else {
1124
+ logSuccess('No UI framework integrations selected');
1125
+ }
1126
+ console.log();
1127
+
1128
+ // Build flags object
1129
+ const flags = {
1130
+ install: shouldInstall,
1131
+ setupDb: shouldSetupDb,
1132
+ adapter: selectedAdapter.value,
1133
+ adapterInfo: selectedAdapter,
1134
+ integrations: selectedIntegrations,
1135
+ };
1136
+
1137
+ log(`\n${colors.bright}${colors.cyan}Creating ATSDC Stack project...${colors.reset}\n`);
1138
+ await createProject(projectName, flags);