create-atsdc-stack 1.0.1 → 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.
Files changed (3) hide show
  1. package/CLAUDE.md +215 -0
  2. package/bin/cli.js +56 -43
  3. 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 * as readline from 'node:readline/promises';
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
- 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
- }
51
+ function promptUser(question) {
52
+ const answer = prompt(`${colors.cyan}${question}${colors.reset} `);
53
+ return answer ? answer.trim() : '';
61
54
  }
62
55
 
63
- async function promptYesNo(question, defaultValue = false) {
56
+ function promptYesNo(question, defaultValue = false) {
64
57
  const defaultText = defaultValue ? 'Y/n' : 'y/N';
65
- const answer = await promptUser(`${question} (${defaultText}):`);
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
- const targetDir = join(process.cwd(), projectName);
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
- logStep(1, 'Checking project directory...');
600
- if (existsSync(targetDir)) {
601
- logError(`Directory "${projectName}" already exists!`);
602
- process.exit(1);
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
- logStep(2, `Creating project directory: ${projectName}`);
607
- await mkdir(targetDir, { recursive: true });
608
- logSuccess('Directory created');
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
- logStep(3, 'Copying template files...');
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(4, 'Updating package.json...');
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
- packageJson.name = projectName;
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(5, 'Creating environment file...');
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(6, 'Installing dependencies...');
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
- console.log(` ${step++}. ${colors.cyan}cd ${projectName}${colors.reset}`);
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}`);
@@ -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
- 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);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-atsdc-stack",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "ATSDC Stack - Astro, TypeScript, SCSS, Drizzle, Clerk CLI utility",
5
5
  "type": "module",
6
6
  "bin": {