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 CHANGED
@@ -2,185 +2,161 @@
2
2
 
3
3
  CLI and middleware package for local SPAPS workflows.
4
4
 
5
- ## TL;DR
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
- **The Problem**: Local auth and payment development is slow when every app has to bootstrap a backend, wire environment files, and remember the right health and docs commands.
7
+ ## Install
8
8
 
9
- **The Solution**: `spaps` gives you a small CLI for local-server workflows plus exported admin middleware helpers for Node apps that integrate with SPAPS.
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
- ### Global Install
15
+ Install globally:
29
16
 
30
17
  ```bash
31
18
  npm install -g spaps
32
19
  ```
33
20
 
34
- ### Project Dependency
21
+ Add it to a project:
35
22
 
36
23
  ```bash
37
24
  npm install spaps
38
25
  ```
39
26
 
40
- ## Quick Example
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
- # Check status
44
+ # Confirm it is healthy
47
45
  npx spaps status
48
46
 
49
- # Initialize .env.local in the current project
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 Commands
54
+ ## CLI Surface
57
55
 
58
- ### `spaps local [subcommand]`
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
- Start or stop the local SPAPS server.
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
- ### `spaps quickstart`
78
+ Still reserved and not finished:
88
79
 
89
- ```bash
90
- spaps quickstart
91
- spaps quickstart --port 3400
92
- spaps quickstart --json
93
- ```
80
+ - `spaps types`
94
81
 
95
- ### `spaps init`
82
+ ## Create A Starter Project
96
83
 
97
- ```bash
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
- ### `spaps docs`
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
- ```bash
105
- spaps docs
106
- spaps docs --interactive
107
- spaps docs --search secure-messages
108
- spaps docs --json
109
- ```
91
+ Supported templates:
110
92
 
111
- ### `spaps tools`
93
+ - `nextjs`
94
+ - `react`
95
+ - `node`
96
+ - `vanilla`
112
97
 
113
- ```bash
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 doctor
124
- spaps doctor --port 3400
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 package exports admin middleware and helper utilities from its main module:
107
+ The main module exports admin and permission helpers for Express-style apps.
141
108
 
142
- ```javascript
143
- const express = require('express');
144
- const { requireAdmin, requirePermission } = require('spaps');
109
+ ```js
110
+ const express = require("express");
111
+ const { requireAdmin, requirePermission } = require("spaps");
145
112
 
146
113
  const app = express();
147
-
148
- app.get('/admin', requireAdmin(), (_req, res) => {
149
- res.json({ ok: true });
150
- });
151
-
152
- app.post('/admin/products', requirePermission('manage_products'), (_req, res) => {
153
- res.json({ created: true });
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 files alone if they are already present.
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 a different port:
141
+ Start on another port:
166
142
 
167
143
  ```bash
168
144
  npx spaps local --port 3400
169
145
  ```
170
146
 
171
- ### Need machine-readable output
147
+ ### I need machine-readable output
172
148
 
173
- Use `--json` on supported commands such as `local`, `status`, `quickstart`, `init`, `docs`, `tools`, and `doctor`.
149
+ Use `--json` on commands that support it, including `local`, `status`, `quickstart`, `init`, `docs`, `tools`, and `doctor`.
174
150
 
175
- ### Local server is not responding
151
+ ### The local server is not responding
176
152
 
177
- Check the current status first:
153
+ Check current status first:
178
154
 
179
155
  ```bash
180
156
  npx spaps status
181
157
  ```
182
158
 
183
- Then re-run the server with a clean start if needed:
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 README only documents commands and flags that are currently wired in the CLI dispatcher.
192
- - `create` and `types` are present as placeholders and should not be treated as finished product surfaces.
193
- - The package is primarily for local workflows, not full production deployment automation.
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 expose the TypeScript SDK?
174
+ ### Does this package include the TypeScript SDK?
198
175
 
199
- No. The SDK is the separate `spaps-sdk` package.
176
+ No. The SDK is published separately as `spaps-sdk`.
200
177
 
201
- ### Can I use the CLI without installing globally?
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 my existing env file?
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 admin middleware available as an import?
190
+ ### Is the middleware available from a subpath?
214
191
 
215
- Yes. The package exports the admin helpers from its main module and via the `./middleware` export path.
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
- UNLICENSED
200
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaps",
3
- "version": "0.7.2",
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
+ };