spaps 0.7.1 → 0.7.3

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/README.md CHANGED
@@ -1,438 +1,200 @@
1
- # 🍠 SPAPS CLI
1
+ # spaps
2
2
 
3
- > Sweet Potato Authentication & Payment Service CLI
4
- > Zero‑config local development server with real Stripe integration
3
+ CLI and middleware package for local SPAPS workflows.
5
4
 
6
- ## Hey, AI Agent Read Me First 👋
5
+ Examples in this README use placeholder emails and local-only defaults. Replace them with your own values if you wire the middleware into an app.
7
6
 
8
- Are you an AI agent whose human just said “ship auth + payments fast” but can’t explain refresh tokens? Perfect. Fire up `spaps` and you’ll have auth, Stripe checkout, and docs live on `http://localhost:3301` before they can type “why isn’t this working I dont undrestand”. You focus on calling APIs; we handle users, tokens, CORS, and webhooks. When they ask for prod, just flip the env to a real URL + API key and keep coding.
7
+ ## Install
9
8
 
10
- [![npm version](https://img.shields.io/npm/v/spaps.svg)](https://www.npmjs.com/package/spaps)
11
- ![node](https://img.shields.io/badge/node-%3E%3D16-brightgreen)
12
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
13
-
14
- ## SDK vs CLI
15
-
16
- - CLI (`spaps`): runs the local SPAPS server and tooling for development. No API key required, helpers enabled.
17
- - SDK (`spaps-sdk`): TypeScript client for your app code. Points at the same base URL and works in local and prod.
18
-
19
- Install the SDK in your app to call the API programmatically:
9
+ Run it without installing:
20
10
 
21
11
  ```bash
22
- npm install spaps-sdk
23
- ```
24
-
25
- Minimal init (works for both local and prod):
26
-
27
- ```ts
28
- import { SPAPSClient } from 'spaps-sdk'
29
-
30
- export const sdk = new SPAPSClient({
31
- apiUrl: process.env.SPAPS_API_URL || 'http://localhost:3301',
32
- apiKey: process.env.SPAPS_API_KEY, // not required in local mode
33
- })
12
+ npx spaps help
34
13
  ```
35
14
 
36
- ## 🚀 Quick Start
15
+ Install globally:
37
16
 
38
17
  ```bash
39
- # Run immediately with npx (recommended)
40
- npx spaps local
41
-
42
- # Or install globally
43
18
  npm install -g spaps
44
- spaps local
45
19
  ```
46
20
 
47
- Your local SPAPS server runs at `http://localhost:3301` 🎉
48
-
49
- - API docs (Swagger UI): `http://localhost:3301/docs`
50
- - OpenAPI JSON: `http://localhost:3301/openapi.json`
51
-
52
- Point your app (via `SPAPS_API_URL`) to that URL and use `spaps-sdk` for calls.
53
-
54
- ## Local → Prod
55
-
56
- - Local (dev):
57
- - `SPAPS_API_URL=http://localhost:3301`
58
- - `SPAPS_LOCAL_MODE=true` (or auto‑detected on localhost)
59
- - API key optional; helpers available (test users, permissive CORS)
60
- - Prod:
61
- - `SPAPS_API_URL=https://api.yourdomain`
62
- - `SPAPS_API_KEY=spaps_…` required
63
- - Local helpers disabled; CORS and rate limits enforced
64
-
65
- Headers policy:
66
- - Local: may send `x-local-mode: true`; role sim via `X-Test-User: admin` (local‑only)
67
- - Prod: must send `X-API-Key: $SPAPS_API_KEY`; do NOT use local‑only headers
68
-
69
- ## ✨ What is SPAPS?
70
-
71
- SPAPS provides a **complete authentication and payment backend** for your applications:
72
-
73
- - 🔐 **Multi-wallet auth** (Solana, Ethereum, Bitcoin, Base)
74
- - 📧 **Traditional auth** (email/password, magic links)
75
- - 💳 **Real Stripe integration** (products, checkout, webhooks)
76
- - 🎭 **Test user switching** (`?_user=admin`)
77
- - 📊 **Admin dashboard** with analytics
78
- - 🌐 **CORS-ready** for any frontend
79
-
80
- Perfect for **rapid prototyping**, **hackathons**, and **local development**.
81
-
82
- ## 📋 Commands
83
-
84
- ### `spaps local` - Development Server
85
-
86
- Start a full-featured local server with zero configuration:
21
+ Add it to a project:
87
22
 
88
23
  ```bash
89
- spaps local # Default: http://localhost:3301
90
- spaps local --port 3000 # Custom port
91
- spaps local --stripe mock|real # Choose Stripe mode (default: mock)
92
- spaps local --seed demo # Seed demo products/customers/orders
93
- spaps local --json # JSON output (CI-friendly)
24
+ npm install spaps
94
25
  ```
95
26
 
96
- Includes:
97
- - ✅ Auto-authentication (no API keys needed)
98
- - ✅ Real Stripe test mode integration
99
- - ✅ Mock payment flows with webhooks
100
- - ✅ Admin dashboard at `/admin`
101
- - ✅ API documentation at `/docs`
102
- - ✅ Test user switching via headers/query params
103
-
104
- Flags:
27
+ This package targets `Node.js >=22`.
105
28
 
106
- - `--port <number>`: Set a custom port (default: 3301)
107
- - `--stripe <mode>`: Stripe mode `mock` (offline, default) or `real` (test API)
108
- - `--seed <preset>`: Seed local data; supported: `demo`
109
- - `--open`: Open docs in your browser after start
110
- - `--json`: JSON machine-readable output (ideal for CI)
29
+ ## When It Fits
111
30
 
112
- ### `spaps init` - Project Setup
31
+ | Need | Package gives you |
32
+ | --- | --- |
33
+ | A local SPAPS control surface | `local`, `status`, `doctor`, and `quickstart` commands |
34
+ | Project bootstrap | `spaps init` creates a starter `.env.local` |
35
+ | AI tooling integration | `spaps tools` emits an OpenAI-style tool spec |
36
+ | Lightweight Node middleware | Admin and permission helpers for Express-style apps |
113
37
 
114
- Initialize SPAPS in an existing project:
38
+ ## Quick Start
115
39
 
116
40
  ```bash
117
- spaps init
118
- # Creates .env.local with Stripe test keys
119
- # ✅ Adds SPAPS client to your package.json
120
- # ✅ Generates basic integration examples
121
- ```
41
+ # Start the local server
42
+ npx spaps local
122
43
 
123
- ### `spaps status` - Health Check
44
+ # Confirm it is healthy
45
+ npx spaps status
124
46
 
125
- Check your local server and Stripe connection:
47
+ # Create a starter .env.local in the current project
48
+ npx spaps init
126
49
 
127
- ```bash
128
- spaps status
129
- # Shows server status, Stripe connectivity, product sync status
50
+ # Emit the local tool spec as JSON
51
+ npx spaps tools --json
130
52
  ```
131
53
 
132
- ### Other Commands
133
-
134
- - `spaps help` — Quick help; `spaps help --interactive` for guided setup
135
- - `spaps docs` — SDK docs; `spaps docs --interactive` or `--search "query"`
136
- - `spaps quickstart` — Minimal SDK usage instructions
137
- - `spaps tools` — Output AI tool spec (use `--json` to save)
138
- - `spaps doctor` — Diagnose local environment and config
54
+ ## CLI Surface
139
55
 
140
- ### JSON Mode (CI)
56
+ | Command | Purpose | Common flags |
57
+ | --- | --- | --- |
58
+ | `spaps local [stop]` | Start or stop the local server workflow | `--port`, `--detach`, `--fresh`, `--from-backup`, `--open`, `--json` |
59
+ | `spaps status` | Check whether the local server is running | `--port`, `--json` |
60
+ | `spaps quickstart` | Print quick-start instructions | `--port`, `--json` |
61
+ | `spaps init` | Create a starter `.env.local` | `--json` |
62
+ | `spaps create <name>` | Scaffold a SPAPS starter project directory | `--template`, `--dir`, `--force`, `--json` |
63
+ | `spaps docs` | Browse or search bundled docs | `--interactive`, `--search`, `--json` |
64
+ | `spaps tools` | Emit the AI tool spec | `--port`, `--format`, `--json` |
65
+ | `spaps doctor` | Diagnose local environment problems | `--port`, `--stripe`, `--json` |
141
66
 
142
- All commands that support `--json` will print machine-readable output. Example:
67
+ Example command usage:
143
68
 
144
69
  ```bash
145
- npx spaps local --port 0 --json | jq '.'
70
+ spaps local --port 3400 --detach
71
+ spaps status --json
72
+ spaps create my-app --template react
73
+ spaps docs --search secure-messages
74
+ spaps doctor --stripe mock
75
+ spaps local stop
146
76
  ```
147
77
 
148
- AI tool spec (OpenAI-style):
78
+ Still reserved and not finished:
149
79
 
150
- ```bash
151
- npx spaps tools --json > spaps-tools.json
152
- ```
80
+ - `spaps types`
153
81
 
154
- Run diagnostics:
155
-
156
- ```bash
157
- npx spaps doctor --json
82
+ ## Create A Starter Project
158
83
 
159
- OpenAPI JSON:
84
+ `spaps create` ships a local-first starter kit rather than a full framework generator. It creates a new directory with:
160
85
 
161
- ```bash
162
- curl http://localhost:3301/openapi.json | jq '.'
163
- ```
164
- ```
86
+ - `spaps.app.json` for the machine-readable SPAPS app contract
87
+ - `.env.local` pointing at local SPAPS
88
+ - `package.json` with `spaps-sdk`
89
+ - a small template-specific integration starter
165
90
 
166
- ## 🎯 Key Features
91
+ Supported templates:
167
92
 
168
- ### 🔧 **Zero Configuration**
169
- - No setup required - works out of the box
170
- - Real Stripe test keys included
171
- - Automatic CORS for any frontend
93
+ - `nextjs`
94
+ - `react`
95
+ - `node`
96
+ - `vanilla`
172
97
 
173
- ### 🎭 Smart Test Users (local‑only)
174
- Switch between user roles instantly (local server only):
98
+ Example:
175
99
 
176
100
  ```bash
177
- # Prefer header (local‑only)
178
- curl -H "X-Test-User: premium" "http://localhost:3301/api/auth/user"
179
-
180
- # Or query param (local‑only convenience)
181
- curl "http://localhost:3301/api/auth/user?_user=admin"
101
+ npx spaps create my-app --template react
102
+ npx spaps create my-api --template node --dir ./services/my-api
182
103
  ```
183
104
 
184
- Available roles: `user`, `admin`, `premium`
185
-
186
- ### 💳 Real Stripe Integration
187
- - **Real API calls** to Stripe test mode
188
- - Create actual checkout sessions
189
- - Receive real webhooks
190
- - Sync products to/from Stripe
191
- - Full webhook testing UI at `/api/stripe/webhooks/test`
192
-
193
- ### 📊 **Admin Dashboard**
194
- Visit `/admin` for a complete management interface:
195
- - Revenue analytics
196
- - Product management
197
- - Order tracking
198
- - Data export/import
199
- - Real-time webhook monitoring
200
-
201
- ## 🔌 API Endpoints
202
-
203
- | Endpoint | Method | SDK Mapping | Description |
204
- |----------|--------|-------------|-------------|
205
- | `/api/auth/login` | POST | `sdk.auth.signInWithPassword` | Email/password authentication |
206
- | `/api/auth/register` | POST | `sdk.auth.register` | Register new user |
207
- | `/api/auth/user` | GET | `sdk.auth.getCurrentUser` | Current authenticated user |
208
- | `/api/auth/wallet-sign-in` | POST | `sdk.auth.signInWithWallet` / `sdk.auth.authenticateWallet` | Wallet signature authentication |
209
- | `/api/auth/refresh` | POST | `sdk.auth.refreshToken` | Refresh access token |
210
- | `/api/auth/logout` | POST | `sdk.auth.logout` | Log out |
211
- | `/api/stripe/products` | GET | `sdk.payments.listProducts` | List Stripe products |
212
- | `/api/stripe/products/:id` | GET | `sdk.payments.getProduct` | Get product (+prices) |
213
- | `/api/stripe/prices` | POST | `sdk.payments.createPrice` | Create price (admin) |
214
- | `/api/stripe/checkout-sessions` | POST | `sdk.payments.createCheckoutSession` | Create checkout session |
215
- | `/api/stripe/checkout-sessions/:id` | GET | `sdk.payments.getCheckoutSession` | Retrieve checkout session |
216
- | `/api/stripe/webhooks` | POST | — | Stripe webhook receiver |
217
- | `/health` | GET | `sdk.healthCheck` | Server health check |
218
- | `/docs` | GET | — | Interactive API documentation |
219
-
220
- ## 💡 Usage Examples
221
-
222
- ### Frontend Integration
223
-
224
- ```javascript
225
- // React/Next.js example
226
- const createCheckout = async () => {
227
- const response = await fetch('http://localhost:3301/api/stripe/checkout-sessions', {
228
- method: 'POST',
229
- headers: { 'Content-Type': 'application/json' },
230
- body: JSON.stringify({
231
- price_id: 'price_1234567890',
232
- success_url: 'http://localhost:3000/success',
233
- cancel_url: 'http://localhost:3000/cancel'
234
- })
235
- });
236
-
237
- const { data } = await response.json();
238
- window.location.href = data.url; // Redirect to Stripe Checkout
239
- };
240
- ```
105
+ ## Middleware Example
241
106
 
242
- ### Test Different User Roles (local‑only)
107
+ The main module exports admin and permission helpers for Express-style apps.
243
108
 
244
- ```javascript
245
- // Test as admin user
246
- fetch('http://localhost:3301/api/auth/user?_user=admin')
109
+ ```js
110
+ const express = require("express");
111
+ const { requireAdmin, requirePermission } = require("spaps");
247
112
 
248
- // Test wallet authentication
249
- fetch('http://localhost:3301/api/auth/wallet-sign-in', {
250
- method: 'POST',
251
- body: JSON.stringify({
252
- wallet_address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
253
- chain_type: 'bitcoin'
254
- })
255
- });
256
- ```
113
+ const app = express();
114
+ const customAdmins = ["admin@example.com"];
257
115
 
258
- ## 🏗️ Development Workflow
116
+ app.get(
117
+ "/admin",
118
+ requireAdmin({ customAdmins }),
119
+ (_req, res) => {
120
+ res.json({ ok: true });
121
+ },
122
+ );
259
123
 
260
- 1. **Start SPAPS**: `npx spaps local`
261
- 2. **Build your frontend** against `http://localhost:3301`
262
- 3. **Test payments** using Stripe's test cards
263
- 4. **Monitor webhooks** at `/api/stripe/webhooks/test`
264
- 5. **Manage data** via `/admin` dashboard
265
- 6. **Export data** when ready for production
124
+ app.post(
125
+ "/admin/products",
126
+ requirePermission("manage_products", { customAdmins }),
127
+ (_req, res) => {
128
+ res.json({ created: true });
129
+ },
130
+ );
131
+ ```
266
132
 
267
- ## 🔒 Environment & Security
133
+ ## What `spaps init` Writes
268
134
 
269
- Local mode safety:
270
- - Only runs on localhost
271
- - Uses Stripe test keys by default
272
- - All data stored locally in `.spaps/` directory
273
- - Responses include local‑mode headers/metadata for visibility
135
+ `spaps init` creates a `.env.local` file with a local API URL starter and leaves an existing file alone if one is already present.
274
136
 
275
- ## Curl Examples (Header‑First)
137
+ ## Troubleshooting
276
138
 
277
- Authenticated (prod/staging):
139
+ ### Port already in use
278
140
 
279
- ```bash
280
- export SPAPS_API_URL=https://api.yourdomain
281
- export SPAPS_API_KEY=spaps_XXXXXXXXXXXXXXXX
282
-
283
- curl -X POST "$SPAPS_API_URL/api/stripe/checkout-sessions" \
284
- -H "Content-Type: application/json" \
285
- -H "X-API-Key: $SPAPS_API_KEY" \
286
- -d '{
287
- "price_id": "price_1234567890",
288
- "success_url": "https://yourapp/success",
289
- "cancel_url": "https://yourapp/cancel"
290
- }'
291
- ```
292
-
293
- Local (no key, role sim via header):
141
+ Start on another port:
294
142
 
295
143
  ```bash
296
- export SPAPS_API_URL=http://localhost:3301
297
-
298
- curl -X GET "$SPAPS_API_URL/api/auth/user" \
299
- -H "X-Test-User: admin" \
300
- -H "x-local-mode: true"
144
+ npx spaps local --port 3400
301
145
  ```
302
146
 
303
- Note: `X-Test-User` and `x-local-mode` are ignored in production.
147
+ ### I need machine-readable output
304
148
 
305
-
149
+ Use `--json` on commands that support it, including `local`, `status`, `quickstart`, `init`, `docs`, `tools`, and `doctor`.
306
150
 
307
- **Stripe Configuration:**
308
- - Real Stripe test API integration
309
- - Webhook signature verification
310
- - Product/price synchronization
311
- - Customer portal simulation
151
+ ### The local server is not responding
312
152
 
313
- ## 📦 Installation Options
153
+ Check current status first:
314
154
 
315
155
  ```bash
316
- # Use immediately (recommended)
317
- npx spaps local
156
+ npx spaps status
157
+ ```
318
158
 
319
- # Install globally
320
- npm install -g spaps
159
+ If needed, restart with a clean boot:
321
160
 
322
- # Add to project dependencies
323
- npm install --save-dev spaps
161
+ ```bash
162
+ npx spaps local --fresh
324
163
  ```
325
164
 
326
- ## 🎓 Next Steps
165
+ ## Limitations
327
166
 
328
- - 📖 **Full Documentation**: [sweetpotato.dev](https://sweetpotato.dev)
329
- - 🔧 **Production Setup**: See deployment guides
330
- - 💬 **Get Help**: [GitHub Issues](https://github.com/buildooor/sweet-potato/issues)
331
- - 🚀 **Examples**: Check `/examples` directory
167
+ - The CLI is aimed at local workflows. It is not a full deployment or hosting tool.
168
+ - `create` does not run `create-next-app`, Vite, or Express generators for you; it scaffolds the SPAPS integration layer.
169
+ - `types` is still a placeholder today.
170
+ - The middleware ships a legacy default admin configuration in code; public-facing apps should pass explicit `customAdmins` instead of relying on that default.
332
171
 
333
- ## 🤝 Pair with the SDK
172
+ ## FAQ
334
173
 
335
- Use the SDK in your app while running the local server:
174
+ ### Does this package include the TypeScript SDK?
336
175
 
337
- ```bash
338
- npm install spaps-sdk
339
- ```
176
+ No. The SDK is published separately as `spaps-sdk`.
340
177
 
341
- ```ts
342
- import { SPAPSClient } from 'spaps-sdk';
178
+ ### Can I use the CLI without a global install?
343
179
 
344
- const spaps = new SPAPSClient(); // auto-detects local mode
345
- const { data } = await spaps.login('user@example.com', 'password');
346
- console.log('User:', data.user);
347
- ```
180
+ Yes. Use `npx spaps ...`.
348
181
 
349
- ## 🚀 Production Deployment
350
-
351
- Ready to go live? SPAPS supports seamless migration from local to production:
352
-
353
- ### Local → Production Workflow
354
-
355
- 1. **Export Local Data**:
356
- ```bash
357
- # Export your products, orders, and customers
358
- curl http://localhost:3301/api/admin/export > spaps-data.json
359
- ```
360
-
361
- 2. **Set Up Production Server**:
362
- ```bash
363
- # Deploy to your server (DigitalOcean, AWS, etc.)
364
- # Example production server: http://104.131.188.214:3000
365
- git clone https://github.com/build000r/sweet-potato
366
- cd sweet-potato
367
- npm install
368
- ```
369
-
370
- 3. **Configure Environment**:
371
- ```bash
372
- # Set production environment variables
373
- SUPABASE_URL=https://your-project.supabase.co
374
- SUPABASE_SERVICE_KEY=eyJhb...your-service-key
375
- STRIPE_SECRET_KEY=<STRIPE_SECRET_KEY> # Your live Stripe key
376
- JWT_SECRET=your-32-char-secure-secret
377
- ```
378
-
379
- 4. **Sync Products to Production Stripe**:
380
- ```bash
381
- # Import your local products to production Stripe
382
- curl -X POST http://104.131.188.214:3000/api/v1/admin/products/sync \
383
- -H "Content-Type: application/json" \
384
- -d @spaps-data.json
385
- ```
386
-
387
- 5. **Update Frontend Config**:
388
- ```javascript
389
- // Change from local to production endpoint
390
- const SPAPS_URL = 'http://104.131.188.214:3000'; // Your production server
391
- ```
392
-
393
- ### Production Features
394
-
395
- The production SPAPS server includes:
396
- - ✅ **Real Supabase integration** with RLS policies
397
- - ✅ **Live Stripe webhooks** with signature verification
398
- - ✅ **Multi-wallet authentication** (Solana, Ethereum, Base, Bitcoin)
399
- - ✅ **JWT authentication** with refresh tokens
400
- - ✅ **Rate limiting** and security middleware
401
- - ✅ **Usage tracking** and analytics
402
- - ✅ **Multi-tenant support** for multiple client apps
403
-
404
- ### Health Check
405
-
406
- Check if your production server is running:
407
- ```bash
408
- curl http://104.131.188.214:3000/health
409
- # Returns: {"status":"healthy","mode":"production"}
410
- ```
182
+ ### Does `spaps init` overwrite an existing env file?
411
183
 
412
- ---
184
+ No. It skips `.env.local` if the file already exists.
413
185
 
414
- ## 🔒 New in v0.5.0: Admin Middleware & Permissions!
186
+ ### What does `spaps tools` output?
415
187
 
416
- Built-in admin middleware and permission utilities for secure Express.js applications:
188
+ An OpenAI-style tool spec for the local SPAPS surface.
417
189
 
418
- ```javascript
419
- const { requireAdmin, isAdminAccount } = require('spaps');
190
+ ### Is the middleware available from a subpath?
420
191
 
421
- // Protect admin routes
422
- app.get('/admin/dashboard', requireAdmin(), (req, res) => {
423
- res.json({ message: 'Admin only!' });
424
- });
192
+ Yes. You can import from the main module or `spaps/middleware`.
425
193
 
426
- // Check admin status
427
- if (isAdminAccount('buildooor@gmail.com')) {
428
- // Grant admin access
429
- }
430
- ```
194
+ ## About Contributions
431
195
 
432
- See [ADMIN_MIDDLEWARE.md](./ADMIN_MIDDLEWARE.md) for complete documentation.
196
+ > *About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
433
197
 
434
- ---
198
+ ## License
435
199
 
436
- **Current Version**: v0.5.0
437
- **License**: MIT
438
- **Node.js**: >=16.0.0 required
200
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaps",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Sweet Potato Authentication & Payment Service CLI - Docker Compose orchestrator for local Python/FastAPI SPAPS server with built-in admin middleware",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -13,13 +13,13 @@
13
13
  "./middleware": "./src/middleware/admin.js"
14
14
  },
15
15
  "scripts": {
16
- "test": "echo \"No tests yet\""
16
+ "test": "node --test"
17
17
  },
18
18
  "keywords": [
19
19
  "authentication",
20
20
  "payments",
21
21
  "stripe",
22
- "supabase",
22
+ "fastapi",
23
23
  "local-development",
24
24
  "cli",
25
25
  "spaps",
@@ -29,7 +29,7 @@
29
29
  "ethereum"
30
30
  ],
31
31
  "author": "buildooor",
32
- "license": "UNLICENSED",
32
+ "license": "MIT",
33
33
  "repository": {
34
34
  "type": "git",
35
35
  "url": "https://github.com/build000r"
@@ -138,8 +138,20 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
138
138
  // spaps create <name>
139
139
  const cmdCreate = program
140
140
  .command('create <name>')
141
- .description('Create a new project with SPAPS (coming soon)')
142
- .action(makeAction('create', (optsOrName, cmd) => ({ name: typeof optsOrName === 'string' ? optsOrName : cmd.args[0] })));
141
+ .description('Create a starter project directory wired for SPAPS')
142
+ .option('-t, --template <template>', 'Starter template: nextjs|react|node|vanilla')
143
+ .option('--dir <dir>', 'Target directory (defaults to ./<name>)')
144
+ .option('-f, --force', 'Allow writing into a non-empty directory', false)
145
+ .option('--json', 'Output in JSON format')
146
+ .action(
147
+ makeAction('create', (opts, cmd, isJson) => ({
148
+ name: cmd.args[0],
149
+ template: opts.template || null,
150
+ dir: opts.dir || null,
151
+ force: Boolean(opts.force),
152
+ json: isJson,
153
+ }))
154
+ );
143
155
  if (dryRun) {
144
156
  cmdCreate.allowUnknownOption(true);
145
157
  if (typeof cmdCreate.allowExcessArguments === 'function') {
@@ -236,8 +248,14 @@ function buildProgram(config = {}) {
236
248
  function parseArgv(argv, config = {}) {
237
249
  const { program, getIntents } = defineProgram({ ...config, dryRun: true });
238
250
  program.exitOverride(() => { /* swallow exit in dry-run */ });
251
+ const normalizedArgv = Array.isArray(argv) &&
252
+ argv.length >= 2 &&
253
+ /(^|[\\/])node(\.exe)?$/.test(String(argv[0])) &&
254
+ /spaps(?:\.js)?$/.test(String(argv[1]))
255
+ ? argv.slice(2)
256
+ : argv;
239
257
  try {
240
- program.parse(argv, { from: 'user' });
258
+ program.parse(normalizedArgv, { from: 'user' });
241
259
  } catch (err) {
242
260
  // Commander throws for help/version; we ignore in parse mode
243
261
  }
@@ -91,6 +91,48 @@ const ERROR_FIXES = {
91
91
  ]
92
92
  }),
93
93
 
94
+ // Invalid arguments
95
+ EINVAL: (error, context = {}) => ({
96
+ title: 'Invalid Command Arguments',
97
+ description: error.message || 'One or more command arguments are invalid',
98
+ causes: [
99
+ 'A required flag was omitted',
100
+ 'An unsupported template or option was supplied',
101
+ 'The command arguments do not match the expected shape'
102
+ ],
103
+ fixes: [
104
+ {
105
+ command: 'npx spaps create my-app --template react',
106
+ description: 'Run create with an explicit supported template'
107
+ },
108
+ {
109
+ command: 'npx spaps help --interactive',
110
+ description: 'Browse supported create templates and usage examples'
111
+ }
112
+ ]
113
+ }),
114
+
115
+ // Existing file system content
116
+ EEXIST: (error, context = {}) => ({
117
+ title: 'Target Directory Already Contains Files',
118
+ description: error.message || 'The target directory is not empty',
119
+ causes: [
120
+ 'You pointed create at an existing project directory',
121
+ 'A previous scaffold already wrote files there',
122
+ 'The directory contains unrelated files that should not be overwritten by default'
123
+ ],
124
+ fixes: [
125
+ {
126
+ command: `npx spaps create ${context.name || 'my-app'} --template ${context.template || 'react'} --dir ${context.dir || './my-app'} --force`,
127
+ description: 'Overwrite the managed starter files explicitly'
128
+ },
129
+ {
130
+ command: `npx spaps create ${context.name || 'my-app'} --template ${context.template || 'react'} --dir ./another-directory`,
131
+ description: 'Choose an empty directory for the new starter'
132
+ }
133
+ ]
134
+ }),
135
+
94
136
  // Network errors
95
137
  ECONNREFUSED: (error, context = {}) => ({
96
138
  title: 'Connection Refused',
package/src/handlers.js CHANGED
@@ -7,6 +7,7 @@ const { showInteractiveDocs, showQuickReference, searchDocs } = require('./docs-
7
7
  const { getQuickStartInstructions, getServerStatus, runQuickTest } = require('./ai-helper');
8
8
  const { buildToolSpec } = require('./ai-tool-spec');
9
9
  const { runDoctor } = require('./doctor');
10
+ const { createProjectStarter } = require('./project-scaffolder');
10
11
 
11
12
  function createHandlers(version, logo) {
12
13
  return {
@@ -114,12 +115,57 @@ function createHandlers(version, logo) {
114
115
  console.log(chalk.cyan(' 3. Start coding!'));
115
116
  }
116
117
  },
117
- create: () => {
118
- console.log(chalk.yellow('🍠 SPAPS'));
119
- console.log(chalk.yellow(`🚧 'spaps create' coming in v0.3.0!`));
120
- console.log();
121
- console.log('For now, check out our examples:');
122
- console.log(chalk.cyan(' https://github.com/yourusername/sweet-potato/tree/main/examples'));
118
+ create: ({ options }) => {
119
+ const isJson = options.json;
120
+
121
+ try {
122
+ const result = createProjectStarter({
123
+ name: options.name,
124
+ template: options.template,
125
+ dir: options.dir,
126
+ force: options.force,
127
+ version,
128
+ });
129
+
130
+ if (isJson) {
131
+ console.log(JSON.stringify(result, null, 2));
132
+ return;
133
+ }
134
+
135
+ console.log(chalk.green(`\n✨ Created ${result.project_name} (${result.template})`));
136
+ console.log(chalk.cyan(` ${result.target_dir}`));
137
+
138
+ if (result.files_created.length > 0) {
139
+ console.log(chalk.green('\nFiles created:'));
140
+ result.files_created.forEach((file) => {
141
+ console.log(chalk.gray(` • ${file}`));
142
+ });
143
+ }
144
+
145
+ if (result.files_overwritten.length > 0) {
146
+ console.log(chalk.yellow('\nFiles overwritten:'));
147
+ result.files_overwritten.forEach((file) => {
148
+ console.log(chalk.gray(` • ${file}`));
149
+ });
150
+ }
151
+
152
+ console.log(chalk.green('\nNext steps:'));
153
+ result.next_steps.forEach((step, index) => {
154
+ console.log(chalk.cyan(` ${index + 1}. ${step}`));
155
+ });
156
+ console.log();
157
+ } catch (error) {
158
+ handleError(
159
+ error,
160
+ {
161
+ command: 'create',
162
+ name: options.name,
163
+ template: options.template,
164
+ dir: options.dir,
165
+ },
166
+ { json: isJson }
167
+ );
168
+ }
123
169
  },
124
170
  types: () => {
125
171
  console.log(chalk.yellow('🍠 SPAPS'));
@@ -454,7 +454,7 @@ function showQuickHelp() {
454
454
  console.log(chalk.green('Common Commands:'));
455
455
  console.log(' npx spaps local ' + chalk.gray('# Start local server'));
456
456
  console.log(' npx spaps init ' + chalk.gray('# Initialize in project'));
457
- console.log(' npx spaps create <name> ' + chalk.gray('# Create new project (v0.3.0)'));
457
+ console.log(' npx spaps create <name> ' + chalk.gray('# Scaffold a SPAPS starter project'));
458
458
  console.log(' npx spaps types ' + chalk.gray('# Generate types (v0.4.0)'));
459
459
  console.log();
460
460
 
@@ -0,0 +1,301 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { DEFAULT_PORT } = require('./config');
5
+
6
+ const SUPPORTED_TEMPLATES = {
7
+ nextjs: {
8
+ label: 'Next.js starter',
9
+ blueprintKey: 'browser_auth',
10
+ allowedOrigins: ['http://localhost:3000'],
11
+ files: ({ apiUrl }) => ({
12
+ 'lib/spaps.ts': `import { SPAPSClient } from 'spaps-sdk';
13
+
14
+ export const spaps = new SPAPSClient({
15
+ apiUrl: process.env.NEXT_PUBLIC_SPAPS_API_URL || '${apiUrl}',
16
+ });
17
+ `,
18
+ 'app/providers.tsx': `'use client';
19
+
20
+ import { PropsWithChildren } from 'react';
21
+ import { spaps } from '../lib/spaps';
22
+
23
+ export function Providers({ children }: PropsWithChildren) {
24
+ void spaps;
25
+ return children;
26
+ }
27
+ `,
28
+ }),
29
+ },
30
+ react: {
31
+ label: 'React + Vite starter',
32
+ blueprintKey: 'browser_auth',
33
+ allowedOrigins: ['http://localhost:5173'],
34
+ files: ({ apiUrl }) => ({
35
+ 'src/lib/spaps.ts': `import { SPAPSClient } from 'spaps-sdk';
36
+
37
+ export const spaps = new SPAPSClient({
38
+ apiUrl: import.meta.env.VITE_SPAPS_API_URL || '${apiUrl}',
39
+ });
40
+ `,
41
+ }),
42
+ },
43
+ node: {
44
+ label: 'Node.js starter',
45
+ blueprintKey: 'default',
46
+ allowedOrigins: [],
47
+ files: ({ apiUrl }) => ({
48
+ 'src/spaps.js': `const { SPAPSClient } = require('spaps-sdk');
49
+
50
+ const spaps = new SPAPSClient({
51
+ apiUrl: process.env.SPAPS_API_URL || '${apiUrl}',
52
+ apiKey: process.env.SPAPS_API_KEY,
53
+ });
54
+
55
+ module.exports = { spaps };
56
+ `,
57
+ }),
58
+ },
59
+ vanilla: {
60
+ label: 'Vanilla JavaScript starter',
61
+ blueprintKey: 'browser_auth',
62
+ allowedOrigins: ['http://localhost:8080'],
63
+ files: ({ apiUrl }) => ({
64
+ 'src/spaps.js': `import { SPAPSClient } from 'spaps-sdk';
65
+
66
+ export const spaps = new SPAPSClient({
67
+ apiUrl: '${apiUrl}',
68
+ });
69
+ `,
70
+ }),
71
+ },
72
+ };
73
+
74
+ function createCliError(code, message) {
75
+ const error = new Error(message);
76
+ error.code = code;
77
+ return error;
78
+ }
79
+
80
+ function slugifyProjectName(name) {
81
+ return String(name)
82
+ .trim()
83
+ .toLowerCase()
84
+ .replace(/[^a-z0-9]+/g, '-')
85
+ .replace(/^-+|-+$/g, '');
86
+ }
87
+
88
+ function ensureSupportedTemplate(template) {
89
+ if (!template) {
90
+ throw createCliError(
91
+ 'EINVAL',
92
+ `The --template flag is required. Supported templates: ${Object.keys(SUPPORTED_TEMPLATES).join(', ')}.`
93
+ );
94
+ }
95
+
96
+ if (!SUPPORTED_TEMPLATES[template]) {
97
+ throw createCliError(
98
+ 'EINVAL',
99
+ `Unsupported template "${template}". Supported templates: ${Object.keys(SUPPORTED_TEMPLATES).join(', ')}.`
100
+ );
101
+ }
102
+ }
103
+
104
+ function ensureWritableTarget(targetDir, force) {
105
+ if (!fs.existsSync(targetDir)) {
106
+ return;
107
+ }
108
+
109
+ const entries = fs.readdirSync(targetDir);
110
+ if (entries.length > 0 && !force) {
111
+ throw createCliError(
112
+ 'EEXIST',
113
+ `Target directory "${targetDir}" is not empty. Re-run with --force to overwrite managed files.`
114
+ );
115
+ }
116
+ }
117
+
118
+ function writeManagedFile(targetDir, relativePath, content, bookkeeping) {
119
+ const fullPath = path.join(targetDir, relativePath);
120
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
121
+
122
+ if (fs.existsSync(fullPath)) {
123
+ bookkeeping.files_overwritten.push(relativePath);
124
+ } else {
125
+ bookkeeping.files_created.push(relativePath);
126
+ }
127
+
128
+ fs.writeFileSync(fullPath, content);
129
+ }
130
+
131
+ function buildPackageJson(name, template) {
132
+ const pkg = {
133
+ name,
134
+ private: true,
135
+ version: '0.0.0',
136
+ description: `SPAPS ${SUPPORTED_TEMPLATES[template].label.toLowerCase()} for ${name}`,
137
+ dependencies: {
138
+ 'spaps-sdk': 'latest',
139
+ },
140
+ };
141
+
142
+ if (template === 'node') {
143
+ pkg.type = 'commonjs';
144
+ }
145
+
146
+ return `${JSON.stringify(pkg, null, 2)}\n`;
147
+ }
148
+
149
+ function buildContract({ name, slug, template, version, apiUrl, docsUrl }) {
150
+ const templateDef = SUPPORTED_TEMPLATES[template];
151
+ const contract = {
152
+ name,
153
+ slug,
154
+ template,
155
+ created_with: `spaps@${version}`,
156
+ spaps: {
157
+ local: {
158
+ api_url: apiUrl,
159
+ docs_url: docsUrl,
160
+ },
161
+ application: {
162
+ slug,
163
+ blueprint_key: templateDef.blueprintKey,
164
+ allowed_origins: templateDef.allowedOrigins,
165
+ provisioning_status: 'local_starter_only',
166
+ },
167
+ },
168
+ };
169
+
170
+ return `${JSON.stringify(contract, null, 2)}\n`;
171
+ }
172
+
173
+ function buildEnvFile(template, apiUrl) {
174
+ const lines = [
175
+ '# SPAPS local development',
176
+ `SPAPS_API_URL=${apiUrl}`,
177
+ '# SPAPS_API_KEY=',
178
+ ];
179
+
180
+ if (template === 'nextjs') {
181
+ lines.push(`NEXT_PUBLIC_SPAPS_API_URL=${apiUrl}`);
182
+ }
183
+
184
+ if (template === 'react') {
185
+ lines.push(`VITE_SPAPS_API_URL=${apiUrl}`);
186
+ }
187
+
188
+ return `${lines.join('\n')}\n`;
189
+ }
190
+
191
+ function buildReadme({ name, template, apiUrl }) {
192
+ const templateDef = SUPPORTED_TEMPLATES[template];
193
+
194
+ return `# ${name}
195
+
196
+ This directory is a SPAPS ${templateDef.label.toLowerCase()}.
197
+
198
+ It gives you three things immediately:
199
+
200
+ - a machine-readable SPAPS app contract in \`spaps.app.json\`
201
+ - local env wiring in \`.env.local\`
202
+ - a small template-specific integration starter you can drop into a real app
203
+
204
+ This is not a full framework generator. It does not run \`create-next-app\`, Vite, or Express setup for you.
205
+
206
+ ## Next Steps
207
+
208
+ 1. Start SPAPS locally with \`npx spaps local\`
209
+ 2. Install dependencies in this project with \`npm install\`
210
+ 3. Copy the generated starter files into your real ${templateDef.label.toLowerCase()} or keep extending this directory
211
+ 4. Point your app at \`${apiUrl}\`
212
+
213
+ ## Generated Files
214
+
215
+ - \`spaps.app.json\`: local-first app contract and blueprint hints
216
+ - \`.env.local\`: SPAPS API URL wiring
217
+ - \`package.json\`: minimal dependency declaration for \`spaps-sdk\`
218
+ `;
219
+ }
220
+
221
+ function createProjectStarter({
222
+ name,
223
+ template,
224
+ dir = null,
225
+ force = false,
226
+ version = '0.0.0',
227
+ port = DEFAULT_PORT,
228
+ }) {
229
+ if (!name || !String(name).trim()) {
230
+ throw createCliError('EINVAL', 'Project name is required.');
231
+ }
232
+
233
+ ensureSupportedTemplate(template);
234
+
235
+ const normalizedName = String(name).trim();
236
+ const slug = slugifyProjectName(normalizedName);
237
+ const targetDir = path.resolve(dir || path.join(process.cwd(), slug));
238
+ const apiUrl = `http://localhost:${port}`;
239
+ const docsUrl = `${apiUrl}/docs`;
240
+ const bookkeeping = {
241
+ files_created: [],
242
+ files_overwritten: [],
243
+ };
244
+
245
+ ensureWritableTarget(targetDir, force);
246
+ fs.mkdirSync(targetDir, { recursive: true });
247
+
248
+ writeManagedFile(
249
+ targetDir,
250
+ 'spaps.app.json',
251
+ buildContract({
252
+ name: normalizedName,
253
+ slug,
254
+ template,
255
+ version,
256
+ apiUrl,
257
+ docsUrl,
258
+ }),
259
+ bookkeeping
260
+ );
261
+ writeManagedFile(targetDir, '.env.local', buildEnvFile(template, apiUrl), bookkeeping);
262
+ writeManagedFile(targetDir, 'README.md', buildReadme({ name: normalizedName, template, apiUrl }), bookkeeping);
263
+ writeManagedFile(targetDir, 'package.json', buildPackageJson(normalizedName, template), bookkeeping);
264
+ writeManagedFile(
265
+ targetDir,
266
+ '.gitignore',
267
+ 'node_modules\n.env\n.env.local\n',
268
+ bookkeeping
269
+ );
270
+
271
+ const templateFiles = SUPPORTED_TEMPLATES[template].files({
272
+ name: normalizedName,
273
+ slug,
274
+ apiUrl,
275
+ });
276
+
277
+ for (const [relativePath, content] of Object.entries(templateFiles)) {
278
+ writeManagedFile(targetDir, relativePath, content, bookkeeping);
279
+ }
280
+
281
+ return {
282
+ success: true,
283
+ command: 'create',
284
+ project_name: normalizedName,
285
+ template,
286
+ target_dir: targetDir,
287
+ contract_path: path.join(targetDir, 'spaps.app.json'),
288
+ files_created: bookkeeping.files_created,
289
+ files_overwritten: bookkeeping.files_overwritten,
290
+ next_steps: [
291
+ `cd ${targetDir}`,
292
+ 'npm install',
293
+ 'npx spaps local',
294
+ ],
295
+ };
296
+ }
297
+
298
+ module.exports = {
299
+ SUPPORTED_TEMPLATES,
300
+ createProjectStarter,
301
+ };