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