create-atsdc-stack 1.0.0 → 1.1.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.md +215 -0
- package/bin/cli.js +57 -44
- package/package.json +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
The ATSDC Stack is a full-stack web application framework built with Astro, TypeScript, SCSS, Drizzle ORM, and Clerk. It's designed as both a production-ready template and a CLI tool for scaffolding new projects.
|
|
8
|
+
|
|
9
|
+
This is a monorepo with two main parts:
|
|
10
|
+
- **Root**: CLI tool for scaffolding new projects (`create-atsdc-stack`)
|
|
11
|
+
- **app/**: The actual Astro application template
|
|
12
|
+
|
|
13
|
+
## Development Commands
|
|
14
|
+
|
|
15
|
+
Run these commands from the **root directory** (they delegate to the app workspace):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Development
|
|
19
|
+
npm run dev # Start dev server at http://localhost:4321
|
|
20
|
+
|
|
21
|
+
# Build & Preview
|
|
22
|
+
npm run build # Type-check and build for production
|
|
23
|
+
npm run preview # Preview production build locally
|
|
24
|
+
|
|
25
|
+
# Database Operations
|
|
26
|
+
npm run db:push # Push schema changes to database (no migrations)
|
|
27
|
+
npm run db:generate # Generate migration files from schema
|
|
28
|
+
npm run db:migrate # Apply pending migrations
|
|
29
|
+
npm run db:studio # Open Drizzle Studio GUI for database
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Architecture Overview
|
|
33
|
+
|
|
34
|
+
### Database Layer (Drizzle ORM)
|
|
35
|
+
|
|
36
|
+
**Key files:**
|
|
37
|
+
- `app/src/db/schema.ts` - Table definitions using Drizzle ORM
|
|
38
|
+
- `app/src/db/validations.ts` - Zod schemas for runtime validation
|
|
39
|
+
- `app/src/db/initialize.ts` - Database client initialization and utilities
|
|
40
|
+
- `app/drizzle.config.ts` - Drizzle Kit configuration
|
|
41
|
+
|
|
42
|
+
**Important patterns:**
|
|
43
|
+
- Uses **NanoID** (21 chars) for all primary keys, not UUIDs or auto-increment
|
|
44
|
+
- All IDs are `varchar(21)` with `.$defaultFn(() => nanoid())`
|
|
45
|
+
- TypeScript types are inferred: `typeof posts.$inferSelect` and `typeof posts.$inferInsert`
|
|
46
|
+
- Zod validation schemas mirror database schemas but add runtime validation
|
|
47
|
+
- Use `@vercel/postgres` for connection pooling, wrapped by Drizzle
|
|
48
|
+
|
|
49
|
+
### API Routes
|
|
50
|
+
|
|
51
|
+
**Location:** `app/src/pages/api/`
|
|
52
|
+
|
|
53
|
+
**Pattern:** Each file exports HTTP methods as named exports:
|
|
54
|
+
```typescript
|
|
55
|
+
export const GET: APIRoute = async ({ request, url }) => { ... }
|
|
56
|
+
export const POST: APIRoute = async ({ request }) => { ... }
|
|
57
|
+
export const PUT: APIRoute = async ({ request }) => { ... }
|
|
58
|
+
export const DELETE: APIRoute = async ({ request, url }) => { ... }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Key conventions:**
|
|
62
|
+
1. Always validate inputs with Zod schemas from `validations.ts`
|
|
63
|
+
2. Return JSON responses with proper status codes (200, 201, 400, 404, 500)
|
|
64
|
+
3. Handle `ZodError` separately from generic errors
|
|
65
|
+
4. Use Drizzle query builder, not raw SQL
|
|
66
|
+
5. For query params, use `url.searchParams.get()` and validate with Zod
|
|
67
|
+
|
|
68
|
+
**Example:** See `app/src/pages/api/posts.ts` for complete CRUD implementation
|
|
69
|
+
|
|
70
|
+
### AI Integration (Vercel AI SDK v5+)
|
|
71
|
+
|
|
72
|
+
**Location:** `app/src/pages/api/chat.ts`
|
|
73
|
+
|
|
74
|
+
**Key pattern:** Uses AI Gateway - no provider-specific packages needed!
|
|
75
|
+
```typescript
|
|
76
|
+
import { streamText } from 'ai';
|
|
77
|
+
|
|
78
|
+
const result = streamText({
|
|
79
|
+
model: 'openai/gpt-4o', // or 'anthropic/claude-3-5-sonnet-20241022'
|
|
80
|
+
messages: validatedData.messages,
|
|
81
|
+
apiKey: process.env.OPENAI_API_KEY, // Pass the appropriate API key
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return result.toDataStreamResponse();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Supported formats:** `openai/`, `anthropic/`, `google/`, etc.
|
|
88
|
+
|
|
89
|
+
### SCSS Architecture
|
|
90
|
+
|
|
91
|
+
**Critical rules:**
|
|
92
|
+
1. **NO inline `<style>` tags** in `.astro` files (except truly standalone components)
|
|
93
|
+
2. **NO utility classes** - use semantic class names (`.btn`, `.card`, not `.px-4`)
|
|
94
|
+
3. All styles in external `.scss` files under `app/src/styles/`
|
|
95
|
+
4. Component styles: `app/src/styles/components/`
|
|
96
|
+
5. Page styles: `app/src/styles/pages/`
|
|
97
|
+
6. Global variables auto-imported via Vite config: `@use "@/styles/variables/globals.scss" as *;`
|
|
98
|
+
|
|
99
|
+
**Import pattern in .astro files:**
|
|
100
|
+
```astro
|
|
101
|
+
---
|
|
102
|
+
import '@/styles/components/button.scss';
|
|
103
|
+
import '@/styles/pages/example.scss';
|
|
104
|
+
---
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Styling modifiers (in order of preference):**
|
|
108
|
+
1. Data attributes: `<button class="btn" data-variant="primary" data-size="lg">`
|
|
109
|
+
2. Class chaining: `<button class="btn primary lg">`
|
|
110
|
+
|
|
111
|
+
**SCSS organization:**
|
|
112
|
+
- `variables/globals.scss` - Colors, spacing, typography
|
|
113
|
+
- `variables/mixins.scss` - Reusable mixins like `@include flex-center`
|
|
114
|
+
- `reset.scss` - CSS reset
|
|
115
|
+
- `global.scss` - Global base styles
|
|
116
|
+
|
|
117
|
+
### TypeScript Path Aliases
|
|
118
|
+
|
|
119
|
+
Configured in `app/tsconfig.json`:
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"@/*": ["src/*"],
|
|
123
|
+
"@db/*": ["src/db/*"],
|
|
124
|
+
"@styles/*": ["src/styles/*"],
|
|
125
|
+
"@components/*": ["src/components/*"]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Usage:**
|
|
130
|
+
```typescript
|
|
131
|
+
import { db } from '@/db/initialize';
|
|
132
|
+
import { posts } from '@/db/schema';
|
|
133
|
+
import '@/styles/components/card.scss';
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Authentication (Clerk)
|
|
137
|
+
|
|
138
|
+
- Pre-configured in `app/astro.config.mjs`
|
|
139
|
+
- Middleware: Create `app/src/middleware.ts` with `clerkMiddleware()` to protect routes
|
|
140
|
+
- User IDs stored as `authorId` in database (varchar 255)
|
|
141
|
+
- React components available via `@clerk/clerk-react`
|
|
142
|
+
|
|
143
|
+
### Progressive Web App (PWA)
|
|
144
|
+
|
|
145
|
+
- Configured via `vite-plugin-pwa` in `astro.config.mjs`
|
|
146
|
+
- Auto-updates enabled (`registerType: 'autoUpdate'`)
|
|
147
|
+
- Manifest and service worker auto-generated
|
|
148
|
+
- Assets should be in `app/public/` (pwa-192x192.png, pwa-512x512.png)
|
|
149
|
+
|
|
150
|
+
## Environment Variables
|
|
151
|
+
|
|
152
|
+
**Required:**
|
|
153
|
+
- `DATABASE_URL` - PostgreSQL connection string
|
|
154
|
+
- `PUBLIC_CLERK_PUBLISHABLE_KEY` - Clerk publishable key
|
|
155
|
+
- `CLERK_SECRET_KEY` - Clerk secret key
|
|
156
|
+
- `OPENAI_API_KEY` - OpenAI API key (for AI features)
|
|
157
|
+
|
|
158
|
+
**Setup:** Copy `.env.example` to `.env` and fill in values
|
|
159
|
+
|
|
160
|
+
## Key Design Decisions
|
|
161
|
+
|
|
162
|
+
1. **NanoID over UUID/auto-increment:** URL-safe, shorter, equally collision-resistant
|
|
163
|
+
2. **Vercel Postgres over node-postgres:** Better connection pooling for serverless
|
|
164
|
+
3. **Drizzle over Prisma:** Closer to SQL, better TypeScript inference, lighter weight
|
|
165
|
+
4. **Zod validation separate from schema:** Allows different validation rules for create/update operations
|
|
166
|
+
5. **SCSS over Tailwind:** Enforces semantic naming, better for large teams and maintainability
|
|
167
|
+
6. **Astro server mode:** Enables API routes and dynamic rendering with Vercel adapter
|
|
168
|
+
|
|
169
|
+
## Common Patterns
|
|
170
|
+
|
|
171
|
+
### Creating a new database table
|
|
172
|
+
|
|
173
|
+
1. Define schema in `app/src/db/schema.ts` using NanoID for primary key
|
|
174
|
+
2. Create Zod schemas in `app/src/db/validations.ts` for create/update operations
|
|
175
|
+
3. Export TypeScript types: `export type MyModel = typeof myTable.$inferSelect`
|
|
176
|
+
4. Push to database: `npm run db:push` (dev) or `npm run db:generate && npm run db:migrate` (prod)
|
|
177
|
+
|
|
178
|
+
### Creating a new API route
|
|
179
|
+
|
|
180
|
+
1. Create file in `app/src/pages/api/[name].ts`
|
|
181
|
+
2. Export named HTTP method handlers: `GET`, `POST`, `PUT`, `DELETE`
|
|
182
|
+
3. Validate inputs with Zod schemas
|
|
183
|
+
4. Use Drizzle ORM for database operations
|
|
184
|
+
5. Return JSON responses with proper error handling
|
|
185
|
+
|
|
186
|
+
### Adding new styles
|
|
187
|
+
|
|
188
|
+
1. Create `.scss` file in appropriate location (`components/` or `pages/`)
|
|
189
|
+
2. Import in `.astro` component: `import '@/styles/components/mycomponent.scss'`
|
|
190
|
+
3. Use semantic class names with data attributes for modifiers
|
|
191
|
+
4. Access global variables/mixins automatically (via Vite config)
|
|
192
|
+
|
|
193
|
+
## Testing Database Connection
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { getDatabaseHealth } from '@/db/initialize';
|
|
197
|
+
|
|
198
|
+
const health = await getDatabaseHealth();
|
|
199
|
+
// Returns: { connected: boolean, tablesExist: boolean, timestamp: Date }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Deployment
|
|
203
|
+
|
|
204
|
+
Configured for **Vercel** deployment with:
|
|
205
|
+
- Adapter: `@astrojs/vercel` (serverless mode)
|
|
206
|
+
- Build command: `npm run build`
|
|
207
|
+
- Output directory: `app/dist/`
|
|
208
|
+
- Environment variables must be set in Vercel project settings
|
|
209
|
+
|
|
210
|
+
## Workspace Structure
|
|
211
|
+
|
|
212
|
+
This is an npm workspace:
|
|
213
|
+
- Root `package.json` contains CLI tooling and workspace configuration
|
|
214
|
+
- `app/package.json` contains the Astro application dependencies
|
|
215
|
+
- Commands run from root are proxied to the app workspace via `--workspace=app`
|
package/bin/cli.js
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { dirname, join } from 'node:path';
|
|
9
|
+
import { basename, dirname, join } from 'node:path';
|
|
10
10
|
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { execSync } from 'node:child_process';
|
|
13
|
-
import
|
|
13
|
+
import promptSyncModule from 'prompt-sync';
|
|
14
|
+
|
|
15
|
+
const prompt = promptSyncModule();
|
|
14
16
|
|
|
15
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
18
|
const __dirname = dirname(__filename);
|
|
@@ -46,23 +48,14 @@ function logWarning(message) {
|
|
|
46
48
|
console.warn(`${colors.yellow}⚠${colors.reset} ${message}`);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
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
|
-
}
|
|
51
|
+
function promptUser(question) {
|
|
52
|
+
const answer = prompt(`${colors.cyan}${question}${colors.reset} `);
|
|
53
|
+
return answer ? answer.trim() : '';
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
|
|
56
|
+
function promptYesNo(question, defaultValue = false) {
|
|
64
57
|
const defaultText = defaultValue ? 'Y/n' : 'y/N';
|
|
65
|
-
const answer =
|
|
58
|
+
const answer = promptUser(`${question} (${defaultText}):`);
|
|
66
59
|
|
|
67
60
|
if (!answer) {
|
|
68
61
|
return defaultValue;
|
|
@@ -592,23 +585,34 @@ async function setupDatabase(projectDir) {
|
|
|
592
585
|
}
|
|
593
586
|
|
|
594
587
|
async function createProject(projectName, options = {}) {
|
|
595
|
-
|
|
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;
|
|
596
592
|
|
|
597
593
|
try {
|
|
598
|
-
// Step 1: Check if directory exists
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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');
|
|
603
602
|
}
|
|
604
603
|
|
|
605
|
-
// Step 2: Create project directory
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
+
}
|
|
609
612
|
|
|
610
613
|
// Step 3: Copy template files
|
|
611
|
-
|
|
614
|
+
const stepOffset = isCurrentDir ? 1 : 2;
|
|
615
|
+
logStep(stepOffset + 1, 'Copying template files...');
|
|
612
616
|
|
|
613
617
|
const appDir = join(templateDir, 'app');
|
|
614
618
|
|
|
@@ -651,10 +655,12 @@ async function createProject(projectName, options = {}) {
|
|
|
651
655
|
logSuccess('Template files copied');
|
|
652
656
|
|
|
653
657
|
// Step 4: Update package.json with project name, adapter, and integrations
|
|
654
|
-
logStep(
|
|
658
|
+
logStep(stepOffset + 2, 'Updating package.json...');
|
|
655
659
|
const packageJsonPath = join(targetDir, 'package.json');
|
|
656
660
|
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
|
|
657
|
-
|
|
661
|
+
// Use current directory name if no project name provided
|
|
662
|
+
const finalProjectName = isCurrentDir ? basename(targetDir) : projectName;
|
|
663
|
+
packageJson.name = finalProjectName;
|
|
658
664
|
|
|
659
665
|
// Initialize dependencies if not present
|
|
660
666
|
if (!packageJson.dependencies) {
|
|
@@ -716,7 +722,7 @@ async function createProject(projectName, options = {}) {
|
|
|
716
722
|
logSuccess('package.json updated');
|
|
717
723
|
|
|
718
724
|
// Step 5: Create .env from .env.example
|
|
719
|
-
logStep(
|
|
725
|
+
logStep(stepOffset + 3, 'Creating environment file...');
|
|
720
726
|
const envExamplePath = join(targetDir, '.env.example');
|
|
721
727
|
const envPath = join(targetDir, '.env');
|
|
722
728
|
if (existsSync(envExamplePath)) {
|
|
@@ -728,7 +734,7 @@ async function createProject(projectName, options = {}) {
|
|
|
728
734
|
|
|
729
735
|
// Step 6: Install dependencies if requested
|
|
730
736
|
if (options.install) {
|
|
731
|
-
logStep(
|
|
737
|
+
logStep(stepOffset + 4, 'Installing dependencies...');
|
|
732
738
|
try {
|
|
733
739
|
execSync('npm install', {
|
|
734
740
|
cwd: targetDir,
|
|
@@ -752,7 +758,9 @@ async function createProject(projectName, options = {}) {
|
|
|
752
758
|
|
|
753
759
|
console.log('\nNext steps:');
|
|
754
760
|
let step = 1;
|
|
755
|
-
|
|
761
|
+
if (!isCurrentDir) {
|
|
762
|
+
console.log(` ${step++}. ${colors.cyan}cd ${projectName}${colors.reset}`);
|
|
763
|
+
}
|
|
756
764
|
|
|
757
765
|
if (!options.install) {
|
|
758
766
|
console.log(` ${step++}. ${colors.cyan}npm install${colors.reset}`);
|
|
@@ -837,7 +845,7 @@ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
|
837
845
|
console.log(`
|
|
838
846
|
${colors.bright}${colors.cyan}╔════════════════════════════════════════════════════════════════════╗
|
|
839
847
|
║ ATSDC Stack CLI v1.0 ║
|
|
840
|
-
║ Production-Ready Full-Stack Application Generator
|
|
848
|
+
║ Production-Ready Full-Stack Application Generator ║
|
|
841
849
|
╚════════════════════════════════════════════════════════════════════╝${colors.reset}
|
|
842
850
|
|
|
843
851
|
${colors.bright}${colors.green}USAGE${colors.reset}
|
|
@@ -856,10 +864,11 @@ ${colors.bright}${colors.green}DESCRIPTION${colors.reset}
|
|
|
856
864
|
${colors.bright}${colors.green}ARGUMENTS${colors.reset}
|
|
857
865
|
${colors.cyan}project-name${colors.reset}
|
|
858
866
|
The name of your new project directory. If omitted, you will be
|
|
859
|
-
prompted to enter it interactively.
|
|
867
|
+
prompted to enter it interactively. Press Enter without typing
|
|
868
|
+
a name (or use ".") to scaffold in the current directory.
|
|
860
869
|
|
|
861
870
|
${colors.yellow}Validation:${colors.reset} Only letters, numbers, hyphens, and underscores
|
|
862
|
-
${colors.yellow}Example:${colors.reset} my-app, my_blog, myapp123
|
|
871
|
+
${colors.yellow}Example:${colors.reset} my-app, my_blog, myapp123, . (current directory)
|
|
863
872
|
|
|
864
873
|
${colors.bright}${colors.green}OPTIONS${colors.reset}
|
|
865
874
|
${colors.cyan}--install, -i${colors.reset}
|
|
@@ -911,6 +920,9 @@ ${colors.bright}${colors.green}EXAMPLES${colors.reset}
|
|
|
911
920
|
${colors.yellow}# Fully interactive - prompts for everything${colors.reset}
|
|
912
921
|
npx create-atsdc-stack
|
|
913
922
|
|
|
923
|
+
${colors.yellow}# Scaffold in current directory${colors.reset}
|
|
924
|
+
npx create-atsdc-stack .
|
|
925
|
+
|
|
914
926
|
${colors.yellow}# Provide name, get prompted for install/setup options${colors.reset}
|
|
915
927
|
npx create-atsdc-stack my-awesome-app
|
|
916
928
|
|
|
@@ -1062,17 +1074,18 @@ if (needsInteractive && !projectName) {
|
|
|
1062
1074
|
|
|
1063
1075
|
// Prompt for project name if not provided
|
|
1064
1076
|
if (!projectName) {
|
|
1065
|
-
projectName = await promptUser('What would you like to name your project?');
|
|
1077
|
+
projectName = await promptUser('What would you like to name your project? (Press Enter to use current directory)');
|
|
1066
1078
|
|
|
1079
|
+
// If empty, use current directory
|
|
1067
1080
|
if (!projectName) {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
+
}
|
|
1076
1089
|
}
|
|
1077
1090
|
console.log();
|
|
1078
1091
|
}
|