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