create-atsdc-stack 1.0.1 → 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 -0
  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 -1138
  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,1138 +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 { 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);
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);