spaps 0.7.2 → 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 +92 -115
- 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
|
@@ -2,185 +2,161 @@
|
|
|
2
2
|
|
|
3
3
|
CLI and middleware package for local SPAPS workflows.
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Why Use `spaps`?
|
|
12
|
-
|
|
13
|
-
| Feature | What It Does |
|
|
14
|
-
| --- | --- |
|
|
15
|
-
| Local server workflow | Start, stop, inspect, and diagnose a local SPAPS server from the CLI |
|
|
16
|
-
| Project bootstrap | Create a starter `.env.local` for SPAPS-aware projects |
|
|
17
|
-
| AI tooling output | Emit the local OpenAI-style tool spec with `spaps tools` |
|
|
18
|
-
| Middleware exports | Reuse admin-permission helpers from the package API |
|
|
19
|
-
|
|
20
|
-
## Installation
|
|
21
|
-
|
|
22
|
-
### Run Without Installing
|
|
9
|
+
Run it without installing:
|
|
23
10
|
|
|
24
11
|
```bash
|
|
25
12
|
npx spaps help
|
|
26
13
|
```
|
|
27
14
|
|
|
28
|
-
|
|
15
|
+
Install globally:
|
|
29
16
|
|
|
30
17
|
```bash
|
|
31
18
|
npm install -g spaps
|
|
32
19
|
```
|
|
33
20
|
|
|
34
|
-
|
|
21
|
+
Add it to a project:
|
|
35
22
|
|
|
36
23
|
```bash
|
|
37
24
|
npm install spaps
|
|
38
25
|
```
|
|
39
26
|
|
|
40
|
-
|
|
27
|
+
This package targets `Node.js >=22`.
|
|
28
|
+
|
|
29
|
+
## When It Fits
|
|
30
|
+
|
|
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 |
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
41
39
|
|
|
42
40
|
```bash
|
|
43
41
|
# Start the local server
|
|
44
42
|
npx spaps local
|
|
45
43
|
|
|
46
|
-
#
|
|
44
|
+
# Confirm it is healthy
|
|
47
45
|
npx spaps status
|
|
48
46
|
|
|
49
|
-
#
|
|
47
|
+
# Create a starter .env.local in the current project
|
|
50
48
|
npx spaps init
|
|
51
49
|
|
|
52
50
|
# Emit the local tool spec as JSON
|
|
53
51
|
npx spaps tools --json
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
## CLI
|
|
54
|
+
## CLI Surface
|
|
57
55
|
|
|
58
|
-
|
|
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` |
|
|
59
66
|
|
|
60
|
-
|
|
67
|
+
Example command usage:
|
|
61
68
|
|
|
62
69
|
```bash
|
|
63
|
-
spaps local
|
|
64
|
-
spaps local --port 3400
|
|
65
|
-
spaps local --detach
|
|
66
|
-
spaps local --open
|
|
67
|
-
spaps local stop
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Supported flags:
|
|
71
|
-
|
|
72
|
-
- `--port <port>`
|
|
73
|
-
- `--detach`
|
|
74
|
-
- `--fresh`
|
|
75
|
-
- `--from-backup <path>`
|
|
76
|
-
- `--open`
|
|
77
|
-
- `--json`
|
|
78
|
-
|
|
79
|
-
### `spaps status`
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
spaps status
|
|
83
|
-
spaps status --port 3400
|
|
70
|
+
spaps local --port 3400 --detach
|
|
84
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
|
|
85
76
|
```
|
|
86
77
|
|
|
87
|
-
|
|
78
|
+
Still reserved and not finished:
|
|
88
79
|
|
|
89
|
-
|
|
90
|
-
spaps quickstart
|
|
91
|
-
spaps quickstart --port 3400
|
|
92
|
-
spaps quickstart --json
|
|
93
|
-
```
|
|
80
|
+
- `spaps types`
|
|
94
81
|
|
|
95
|
-
|
|
82
|
+
## Create A Starter Project
|
|
96
83
|
|
|
97
|
-
|
|
98
|
-
spaps init
|
|
99
|
-
spaps init --json
|
|
100
|
-
```
|
|
84
|
+
`spaps create` ships a local-first starter kit rather than a full framework generator. It creates a new directory with:
|
|
101
85
|
|
|
102
|
-
|
|
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
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
spaps docs
|
|
106
|
-
spaps docs --interactive
|
|
107
|
-
spaps docs --search secure-messages
|
|
108
|
-
spaps docs --json
|
|
109
|
-
```
|
|
91
|
+
Supported templates:
|
|
110
92
|
|
|
111
|
-
|
|
93
|
+
- `nextjs`
|
|
94
|
+
- `react`
|
|
95
|
+
- `node`
|
|
96
|
+
- `vanilla`
|
|
112
97
|
|
|
113
|
-
|
|
114
|
-
spaps tools
|
|
115
|
-
spaps tools --port 3400
|
|
116
|
-
spaps tools --format openai
|
|
117
|
-
spaps tools --json
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### `spaps doctor`
|
|
98
|
+
Example:
|
|
121
99
|
|
|
122
100
|
```bash
|
|
123
|
-
spaps
|
|
124
|
-
spaps
|
|
125
|
-
spaps doctor --stripe mock
|
|
126
|
-
spaps doctor --json
|
|
101
|
+
npx spaps create my-app --template react
|
|
102
|
+
npx spaps create my-api --template node --dir ./services/my-api
|
|
127
103
|
```
|
|
128
104
|
|
|
129
|
-
### Placeholder Commands
|
|
130
|
-
|
|
131
|
-
The CLI currently includes placeholders for:
|
|
132
|
-
|
|
133
|
-
- `spaps create <name>`
|
|
134
|
-
- `spaps types`
|
|
135
|
-
|
|
136
|
-
Treat them as reserved surfaces rather than stable workflows.
|
|
137
|
-
|
|
138
105
|
## Middleware Example
|
|
139
106
|
|
|
140
|
-
The
|
|
107
|
+
The main module exports admin and permission helpers for Express-style apps.
|
|
141
108
|
|
|
142
|
-
```
|
|
143
|
-
const express = require(
|
|
144
|
-
const { requireAdmin, requirePermission } = require(
|
|
109
|
+
```js
|
|
110
|
+
const express = require("express");
|
|
111
|
+
const { requireAdmin, requirePermission } = require("spaps");
|
|
145
112
|
|
|
146
113
|
const app = express();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
114
|
+
const customAdmins = ["admin@example.com"];
|
|
115
|
+
|
|
116
|
+
app.get(
|
|
117
|
+
"/admin",
|
|
118
|
+
requireAdmin({ customAdmins }),
|
|
119
|
+
(_req, res) => {
|
|
120
|
+
res.json({ ok: true });
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
app.post(
|
|
125
|
+
"/admin/products",
|
|
126
|
+
requirePermission("manage_products", { customAdmins }),
|
|
127
|
+
(_req, res) => {
|
|
128
|
+
res.json({ created: true });
|
|
129
|
+
},
|
|
130
|
+
);
|
|
155
131
|
```
|
|
156
132
|
|
|
157
133
|
## What `spaps init` Writes
|
|
158
134
|
|
|
159
|
-
`spaps init` creates a `.env.local` with a local API URL starter and leaves existing
|
|
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.
|
|
160
136
|
|
|
161
137
|
## Troubleshooting
|
|
162
138
|
|
|
163
139
|
### Port already in use
|
|
164
140
|
|
|
165
|
-
Start on
|
|
141
|
+
Start on another port:
|
|
166
142
|
|
|
167
143
|
```bash
|
|
168
144
|
npx spaps local --port 3400
|
|
169
145
|
```
|
|
170
146
|
|
|
171
|
-
###
|
|
147
|
+
### I need machine-readable output
|
|
172
148
|
|
|
173
|
-
Use `--json` on
|
|
149
|
+
Use `--json` on commands that support it, including `local`, `status`, `quickstart`, `init`, `docs`, `tools`, and `doctor`.
|
|
174
150
|
|
|
175
|
-
###
|
|
151
|
+
### The local server is not responding
|
|
176
152
|
|
|
177
|
-
Check
|
|
153
|
+
Check current status first:
|
|
178
154
|
|
|
179
155
|
```bash
|
|
180
156
|
npx spaps status
|
|
181
157
|
```
|
|
182
158
|
|
|
183
|
-
|
|
159
|
+
If needed, restart with a clean boot:
|
|
184
160
|
|
|
185
161
|
```bash
|
|
186
162
|
npx spaps local --fresh
|
|
@@ -188,21 +164,22 @@ npx spaps local --fresh
|
|
|
188
164
|
|
|
189
165
|
## Limitations
|
|
190
166
|
|
|
191
|
-
- The
|
|
192
|
-
- `create`
|
|
193
|
-
-
|
|
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.
|
|
194
171
|
|
|
195
172
|
## FAQ
|
|
196
173
|
|
|
197
|
-
### Does this package
|
|
174
|
+
### Does this package include the TypeScript SDK?
|
|
198
175
|
|
|
199
|
-
No. The SDK is
|
|
176
|
+
No. The SDK is published separately as `spaps-sdk`.
|
|
200
177
|
|
|
201
|
-
### Can I use the CLI without
|
|
178
|
+
### Can I use the CLI without a global install?
|
|
202
179
|
|
|
203
180
|
Yes. Use `npx spaps ...`.
|
|
204
181
|
|
|
205
|
-
### Does `spaps init` overwrite
|
|
182
|
+
### Does `spaps init` overwrite an existing env file?
|
|
206
183
|
|
|
207
184
|
No. It skips `.env.local` if the file already exists.
|
|
208
185
|
|
|
@@ -210,14 +187,14 @@ No. It skips `.env.local` if the file already exists.
|
|
|
210
187
|
|
|
211
188
|
An OpenAI-style tool spec for the local SPAPS surface.
|
|
212
189
|
|
|
213
|
-
### Is the
|
|
190
|
+
### Is the middleware available from a subpath?
|
|
214
191
|
|
|
215
|
-
Yes.
|
|
192
|
+
Yes. You can import from the main module or `spaps/middleware`.
|
|
216
193
|
|
|
217
194
|
## About Contributions
|
|
218
195
|
|
|
219
|
-
*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.
|
|
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.
|
|
220
197
|
|
|
221
198
|
## License
|
|
222
199
|
|
|
223
|
-
|
|
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
|
+
};
|