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 +117 -355
- package/package.json +4 -4
- package/src/cli-dispatcher.js +21 -3
- package/src/error-handler.js +42 -0
- package/src/handlers.js +52 -6
- package/src/help-system.js +1 -1
- package/src/project-scaffolder.js +301 -0
package/README.md
CHANGED
|
@@ -1,438 +1,200 @@
|
|
|
1
|
-
#
|
|
1
|
+
# spaps
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> Zero‑config local development server with real Stripe integration
|
|
3
|
+
CLI and middleware package for local SPAPS workflows.
|
|
5
4
|
|
|
6
|
-
|
|
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
|
-
|
|
7
|
+
## Install
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-

|
|
12
|
-
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
## Quick Start
|
|
115
39
|
|
|
116
40
|
```bash
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
44
|
+
# Confirm it is healthy
|
|
45
|
+
npx spaps status
|
|
124
46
|
|
|
125
|
-
|
|
47
|
+
# Create a starter .env.local in the current project
|
|
48
|
+
npx spaps init
|
|
126
49
|
|
|
127
|
-
|
|
128
|
-
spaps
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
Example command usage:
|
|
143
68
|
|
|
144
69
|
```bash
|
|
145
|
-
|
|
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
|
-
|
|
78
|
+
Still reserved and not finished:
|
|
149
79
|
|
|
150
|
-
|
|
151
|
-
npx spaps tools --json > spaps-tools.json
|
|
152
|
-
```
|
|
80
|
+
- `spaps types`
|
|
153
81
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
npx spaps doctor --json
|
|
82
|
+
## Create A Starter Project
|
|
158
83
|
|
|
159
|
-
|
|
84
|
+
`spaps create` ships a local-first starter kit rather than a full framework generator. It creates a new directory with:
|
|
160
85
|
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
91
|
+
Supported templates:
|
|
167
92
|
|
|
168
|
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
93
|
+
- `nextjs`
|
|
94
|
+
- `react`
|
|
95
|
+
- `node`
|
|
96
|
+
- `vanilla`
|
|
172
97
|
|
|
173
|
-
|
|
174
|
-
Switch between user roles instantly (local server only):
|
|
98
|
+
Example:
|
|
175
99
|
|
|
176
100
|
```bash
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
The main module exports admin and permission helpers for Express-style apps.
|
|
243
108
|
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
|
|
109
|
+
```js
|
|
110
|
+
const express = require("express");
|
|
111
|
+
const { requireAdmin, requirePermission } = require("spaps");
|
|
247
112
|
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
116
|
+
app.get(
|
|
117
|
+
"/admin",
|
|
118
|
+
requireAdmin({ customAdmins }),
|
|
119
|
+
(_req, res) => {
|
|
120
|
+
res.json({ ok: true });
|
|
121
|
+
},
|
|
122
|
+
);
|
|
259
123
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
##
|
|
133
|
+
## What `spaps init` Writes
|
|
268
134
|
|
|
269
|
-
|
|
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
|
-
##
|
|
137
|
+
## Troubleshooting
|
|
276
138
|
|
|
277
|
-
|
|
139
|
+
### Port already in use
|
|
278
140
|
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
+
Check current status first:
|
|
314
154
|
|
|
315
155
|
```bash
|
|
316
|
-
|
|
317
|
-
|
|
156
|
+
npx spaps status
|
|
157
|
+
```
|
|
318
158
|
|
|
319
|
-
|
|
320
|
-
npm install -g spaps
|
|
159
|
+
If needed, restart with a clean boot:
|
|
321
160
|
|
|
322
|
-
|
|
323
|
-
|
|
161
|
+
```bash
|
|
162
|
+
npx spaps local --fresh
|
|
324
163
|
```
|
|
325
164
|
|
|
326
|
-
##
|
|
165
|
+
## Limitations
|
|
327
166
|
|
|
328
|
-
-
|
|
329
|
-
-
|
|
330
|
-
-
|
|
331
|
-
-
|
|
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
|
-
##
|
|
172
|
+
## FAQ
|
|
334
173
|
|
|
335
|
-
|
|
174
|
+
### Does this package include the TypeScript SDK?
|
|
336
175
|
|
|
337
|
-
|
|
338
|
-
npm install spaps-sdk
|
|
339
|
-
```
|
|
176
|
+
No. The SDK is published separately as `spaps-sdk`.
|
|
340
177
|
|
|
341
|
-
|
|
342
|
-
import { SPAPSClient } from 'spaps-sdk';
|
|
178
|
+
### Can I use the CLI without a global install?
|
|
343
179
|
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
+
### What does `spaps tools` output?
|
|
415
187
|
|
|
416
|
-
|
|
188
|
+
An OpenAI-style tool spec for the local SPAPS surface.
|
|
417
189
|
|
|
418
|
-
|
|
419
|
-
const { requireAdmin, isAdminAccount } = require('spaps');
|
|
190
|
+
### Is the middleware available from a subpath?
|
|
420
191
|
|
|
421
|
-
|
|
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
|
-
|
|
427
|
-
if (isAdminAccount('buildooor@gmail.com')) {
|
|
428
|
-
// Grant admin access
|
|
429
|
-
}
|
|
430
|
-
```
|
|
194
|
+
## About Contributions
|
|
431
195
|
|
|
432
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
16
|
+
"test": "node --test"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"authentication",
|
|
20
20
|
"payments",
|
|
21
21
|
"stripe",
|
|
22
|
-
"
|
|
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": "
|
|
32
|
+
"license": "MIT",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
35
|
"url": "https://github.com/build000r"
|
package/src/cli-dispatcher.js
CHANGED
|
@@ -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
|
|
142
|
-
.
|
|
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(
|
|
258
|
+
program.parse(normalizedArgv, { from: 'user' });
|
|
241
259
|
} catch (err) {
|
|
242
260
|
// Commander throws for help/version; we ignore in parse mode
|
|
243
261
|
}
|
package/src/error-handler.js
CHANGED
|
@@ -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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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'));
|
package/src/help-system.js
CHANGED
|
@@ -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('#
|
|
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
|
+
};
|