spaps 0.7.2 → 0.7.4
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/AI_TOOLS.json +10 -11
- package/README.md +267 -110
- package/assets/local-runtime/Dockerfile +28 -0
- package/assets/local-runtime/alembic/env.py +101 -0
- package/assets/local-runtime/alembic/path_bootstrap.py +71 -0
- package/assets/local-runtime/alembic/versions/000000000001_baseline_consolidated_schema.py +1076 -0
- package/assets/local-runtime/alembic/versions/000000000002_fix_column_types_to_match_prod.py +83 -0
- package/assets/local-runtime/alembic/versions/000000000003_fix_email_template_key_uniqueness.py +49 -0
- package/assets/local-runtime/alembic/versions/000000000004_add_hold_duration_minutes_to_dayrate_config.py +30 -0
- package/assets/local-runtime/alembic/versions/000000000005_resource_scoped_entitlements.py +77 -0
- package/assets/local-runtime/alembic/versions/000000000006_cfo_rbac_add_is_admin.py +37 -0
- package/assets/local-runtime/alembic/versions/000000000007_agent_approvals.py +158 -0
- package/assets/local-runtime/alembic/versions/000000000008_add_company_id_to_cfo_connections.py +35 -0
- package/assets/local-runtime/alembic/versions/000000000009_tx_signing.py +62 -0
- package/assets/local-runtime/alembic/versions/000000000010_affiliate_referrals.py +235 -0
- package/assets/local-runtime/alembic/versions/000000000011_checkin_call_booking.py +137 -0
- package/assets/local-runtime/alembic/versions/000000000012_subscription_application_scoping.py +55 -0
- package/assets/local-runtime/alembic/versions/000000000013_refresh_token_anomaly_context.py +61 -0
- package/assets/local-runtime/alembic/versions/000000000014_buildooor_dayrate_hire_schedule.py +39 -0
- package/assets/local-runtime/alembic/versions/000000000015_support_telemetry_platform.py +112 -0
- package/assets/local-runtime/alembic/versions/000000000016_issue_reporting_platform.py +54 -0
- package/assets/local-runtime/alembic/versions/000000000017_issue_reporting_platform_import_tracking.py +44 -0
- package/assets/local-runtime/alembic/versions/000000000018_authorization_policy_engine.py +76 -0
- package/assets/local-runtime/alembic.ini +47 -0
- package/assets/local-runtime/docker-compose.yml +61 -0
- package/assets/local-runtime/manifest.json +8 -0
- package/assets/local-runtime/scripts/container-entrypoint.sh +13 -0
- package/assets/local-runtime/scripts/fetch-prod-db.sh +112 -0
- package/assets/local-runtime/scripts/run-migrations.sh +96 -0
- package/package.json +5 -4
- package/src/ai-helper.js +176 -234
- package/src/ai-tool-spec.js +52 -20
- package/src/auth/api-key.js +119 -0
- package/src/auth/client-id.js +136 -0
- package/src/auth/client.js +169 -0
- package/src/auth/credentials.js +110 -0
- package/src/auth/device-flow.js +159 -0
- package/src/auth/env.js +57 -0
- package/src/auth/handlers.js +462 -0
- package/src/auth/http.js +74 -0
- package/src/cli-dispatcher.js +155 -24
- package/src/docs-system.js +7 -7
- package/src/error-handler.js +42 -0
- package/src/fixture-kernel.js +1143 -0
- package/src/handlers.js +252 -15
- package/src/help-system.js +3 -1
- package/src/local-runtime.js +258 -0
- package/src/local-server.js +597 -199
- package/src/project-scaffolder.js +441 -0
package/src/cli-dispatcher.js
CHANGED
|
@@ -28,10 +28,11 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
28
28
|
// For commands with args, it passes (arg1, arg2, ..., options, command)
|
|
29
29
|
const cmd = args[args.length - 1];
|
|
30
30
|
const options = args[args.length - 2] || {};
|
|
31
|
+
const positionals = args.slice(0, -2);
|
|
31
32
|
const parentJson = program.opts().json;
|
|
32
33
|
const isJson = Boolean(options.json || parentJson);
|
|
33
34
|
|
|
34
|
-
const intent = { name, options: { ...shape(options, cmd, isJson) } };
|
|
35
|
+
const intent = { name, options: { ...shape(options, cmd, isJson, positionals) } };
|
|
35
36
|
intents.push(intent);
|
|
36
37
|
|
|
37
38
|
if (dryRun) return intent;
|
|
@@ -48,34 +49,26 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
48
49
|
.command('local [subcommand]')
|
|
49
50
|
.description('Start local SPAPS server via Docker Compose (subcommand: stop)')
|
|
50
51
|
.option('-p, --port <port>', 'Port to check (default: 3301)', String(DEFAULT_PORT))
|
|
52
|
+
.option('--runtime-dir <path>', 'Portable runtime directory (defaults to ~/.cache/spaps/local-<port>)')
|
|
53
|
+
.option('--runtime-source <source>', 'Runtime source: auto|repo|bundle', 'auto')
|
|
54
|
+
.option(
|
|
55
|
+
'--data-source <source>',
|
|
56
|
+
'Base data source: empty|prod-cache|prod-fresh (use --from-backup for an explicit dump file)',
|
|
57
|
+
'empty'
|
|
58
|
+
)
|
|
51
59
|
.option('-d, --detach', 'Run in background (don\'t tail logs)', false)
|
|
52
60
|
.option('--fresh', 'Fresh start: tear down and rebuild from scratch', false)
|
|
53
61
|
.option('--from-backup <path>', 'Load from Supabase backup file', null)
|
|
54
62
|
.option('-o, --open', 'Open browser automatically', false)
|
|
55
63
|
.option('--json', 'Output in JSON format')
|
|
56
64
|
.action(
|
|
57
|
-
makeAction('local', (
|
|
58
|
-
|
|
59
|
-
let subcommand = null;
|
|
60
|
-
let opts = null;
|
|
61
|
-
let cmd = null;
|
|
62
|
-
let isJson = false;
|
|
63
|
-
|
|
64
|
-
if (typeof subcommandOrOpts === 'string') {
|
|
65
|
-
// 'local stop' case
|
|
66
|
-
subcommand = subcommandOrOpts;
|
|
67
|
-
opts = cmdOrOpts;
|
|
68
|
-
cmd = isJsonOrCmd;
|
|
69
|
-
isJson = Boolean(opts.json || (cmd && cmd.parent && cmd.parent.opts().json));
|
|
70
|
-
} else {
|
|
71
|
-
// 'local' case (no subcommand)
|
|
72
|
-
opts = subcommandOrOpts;
|
|
73
|
-
cmd = cmdOrOpts;
|
|
74
|
-
isJson = Boolean(opts.json || (cmd && cmd.parent && cmd.parent.opts().json));
|
|
75
|
-
}
|
|
76
|
-
|
|
65
|
+
makeAction('local', (opts, _cmd, isJson, positionals) => {
|
|
66
|
+
const subcommand = typeof positionals[0] === 'string' ? positionals[0] : null;
|
|
77
67
|
const out = {
|
|
78
68
|
port: Number(opts.port) || 3301,
|
|
69
|
+
runtimeDir: opts.runtimeDir || null,
|
|
70
|
+
runtimeSource: String(opts.runtimeSource || 'auto'),
|
|
71
|
+
dataSource: String(opts.dataSource || 'empty'),
|
|
79
72
|
open: Boolean(opts.open),
|
|
80
73
|
detach: Boolean(opts.detach),
|
|
81
74
|
fresh: Boolean(opts.fresh),
|
|
@@ -138,8 +131,22 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
138
131
|
// spaps create <name>
|
|
139
132
|
const cmdCreate = program
|
|
140
133
|
.command('create <name>')
|
|
141
|
-
.description('Create a
|
|
142
|
-
.
|
|
134
|
+
.description('Create a starter project directory wired for SPAPS')
|
|
135
|
+
.option('-t, --template <template>', 'Starter template: nextjs|react|node|vanilla')
|
|
136
|
+
.option('--dir <dir>', 'Target directory (defaults to ./<name>)')
|
|
137
|
+
.option('-p, --port <port>', 'Local SPAPS port to provision against', String(DEFAULT_PORT))
|
|
138
|
+
.option('-f, --force', 'Allow writing into a non-empty directory', false)
|
|
139
|
+
.option('--json', 'Output in JSON format')
|
|
140
|
+
.action(
|
|
141
|
+
makeAction('create', (opts, cmd, isJson) => ({
|
|
142
|
+
name: cmd.args[0],
|
|
143
|
+
template: opts.template || null,
|
|
144
|
+
dir: opts.dir || null,
|
|
145
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
146
|
+
force: Boolean(opts.force),
|
|
147
|
+
json: isJson,
|
|
148
|
+
}))
|
|
149
|
+
);
|
|
143
150
|
if (dryRun) {
|
|
144
151
|
cmdCreate.allowUnknownOption(true);
|
|
145
152
|
if (typeof cmdCreate.allowExcessArguments === 'function') {
|
|
@@ -209,6 +216,38 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
209
216
|
}
|
|
210
217
|
}
|
|
211
218
|
|
|
219
|
+
// spaps fixtures
|
|
220
|
+
const cmdFixtures = program
|
|
221
|
+
.command('fixtures <subcommand>')
|
|
222
|
+
.description('Manage repo-local .spaps auth fixtures (init|apply|reset|storage-state)')
|
|
223
|
+
.option('--dir <dir>', 'Target repo directory (defaults to current working directory)')
|
|
224
|
+
.option('-p, --port <port>', 'Port to inspect for SPAPS runtime hints', String(DEFAULT_PORT))
|
|
225
|
+
.option('--base-url <url>', 'Browser app base URL for generated storage-state files')
|
|
226
|
+
.option('--persona <persona>', 'Persona code to target for storage-state export')
|
|
227
|
+
.option('-f, --format <format>', 'Artifact format (playwright)', 'playwright')
|
|
228
|
+
.option('--force', 'Overwrite fixture files during init', false)
|
|
229
|
+
.option('--json', 'Output in JSON format')
|
|
230
|
+
.action(
|
|
231
|
+
makeAction('fixtures', (opts, cmd, isJson) => {
|
|
232
|
+
return {
|
|
233
|
+
subcommand: cmd.args[0],
|
|
234
|
+
dir: opts.dir || null,
|
|
235
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
236
|
+
baseUrl: opts.baseUrl || null,
|
|
237
|
+
persona: opts.persona || null,
|
|
238
|
+
format: String(opts.format || 'playwright'),
|
|
239
|
+
force: Boolean(opts.force),
|
|
240
|
+
json: isJson,
|
|
241
|
+
};
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
if (dryRun) {
|
|
245
|
+
cmdFixtures.allowUnknownOption(true);
|
|
246
|
+
if (typeof cmdFixtures.allowExcessArguments === 'function') {
|
|
247
|
+
cmdFixtures.allowExcessArguments(true);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
212
251
|
// spaps doctor
|
|
213
252
|
const cmdDoctor = program
|
|
214
253
|
.command('doctor')
|
|
@@ -226,6 +265,92 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
|
|
|
226
265
|
}
|
|
227
266
|
}
|
|
228
267
|
|
|
268
|
+
// spaps login
|
|
269
|
+
const cmdLogin = program
|
|
270
|
+
.command('login')
|
|
271
|
+
.description('Authenticate with a SPAPS server (RFC 8628 device flow)')
|
|
272
|
+
.option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
|
|
273
|
+
.option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
|
|
274
|
+
.option('--client-id <id>', 'Application slug to authorize as')
|
|
275
|
+
.option('--json', 'Output in JSON format')
|
|
276
|
+
.action(
|
|
277
|
+
makeAction('login', (opts, _cmd, isJson) => ({
|
|
278
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
279
|
+
serverUrl: opts.serverUrl || null,
|
|
280
|
+
clientId: opts.clientId || null,
|
|
281
|
+
json: isJson,
|
|
282
|
+
}))
|
|
283
|
+
);
|
|
284
|
+
if (dryRun) {
|
|
285
|
+
cmdLogin.allowUnknownOption(true);
|
|
286
|
+
if (typeof cmdLogin.allowExcessArguments === 'function') {
|
|
287
|
+
cmdLogin.allowExcessArguments(true);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// spaps logout
|
|
292
|
+
const cmdLogout = program
|
|
293
|
+
.command('logout')
|
|
294
|
+
.description('Revoke and clear stored SPAPS credentials')
|
|
295
|
+
.option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
|
|
296
|
+
.option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
|
|
297
|
+
.option('--json', 'Output in JSON format')
|
|
298
|
+
.action(
|
|
299
|
+
makeAction('logout', (opts, _cmd, isJson) => ({
|
|
300
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
301
|
+
serverUrl: opts.serverUrl || null,
|
|
302
|
+
json: isJson,
|
|
303
|
+
}))
|
|
304
|
+
);
|
|
305
|
+
if (dryRun) {
|
|
306
|
+
cmdLogout.allowUnknownOption(true);
|
|
307
|
+
if (typeof cmdLogout.allowExcessArguments === 'function') {
|
|
308
|
+
cmdLogout.allowExcessArguments(true);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// spaps whoami
|
|
313
|
+
const cmdWhoami = program
|
|
314
|
+
.command('whoami')
|
|
315
|
+
.description('Show the currently authenticated user')
|
|
316
|
+
.option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
|
|
317
|
+
.option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
|
|
318
|
+
.option('--json', 'Output in JSON format')
|
|
319
|
+
.action(
|
|
320
|
+
makeAction('whoami', (opts, _cmd, isJson) => ({
|
|
321
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
322
|
+
serverUrl: opts.serverUrl || null,
|
|
323
|
+
json: isJson,
|
|
324
|
+
}))
|
|
325
|
+
);
|
|
326
|
+
if (dryRun) {
|
|
327
|
+
cmdWhoami.allowUnknownOption(true);
|
|
328
|
+
if (typeof cmdWhoami.allowExcessArguments === 'function') {
|
|
329
|
+
cmdWhoami.allowExcessArguments(true);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// spaps token (print access token for piping to curl or env vars)
|
|
334
|
+
const cmdToken = program
|
|
335
|
+
.command('token')
|
|
336
|
+
.description('Print the current access token (for piping to tools like curl)')
|
|
337
|
+
.option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
|
|
338
|
+
.option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
|
|
339
|
+
.option('--json', 'Output JSON with metadata instead of bare token')
|
|
340
|
+
.action(
|
|
341
|
+
makeAction('token', (opts, _cmd, isJson) => ({
|
|
342
|
+
port: Number(opts.port) || DEFAULT_PORT,
|
|
343
|
+
serverUrl: opts.serverUrl || null,
|
|
344
|
+
json: isJson,
|
|
345
|
+
}))
|
|
346
|
+
);
|
|
347
|
+
if (dryRun) {
|
|
348
|
+
cmdToken.allowUnknownOption(true);
|
|
349
|
+
if (typeof cmdToken.allowExcessArguments === 'function') {
|
|
350
|
+
cmdToken.allowExcessArguments(true);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
229
354
|
return { program, getIntents: () => intents };
|
|
230
355
|
}
|
|
231
356
|
|
|
@@ -236,8 +361,14 @@ function buildProgram(config = {}) {
|
|
|
236
361
|
function parseArgv(argv, config = {}) {
|
|
237
362
|
const { program, getIntents } = defineProgram({ ...config, dryRun: true });
|
|
238
363
|
program.exitOverride(() => { /* swallow exit in dry-run */ });
|
|
364
|
+
const normalizedArgv = Array.isArray(argv) &&
|
|
365
|
+
argv.length >= 2 &&
|
|
366
|
+
/(^|[\\/])node(\.exe)?$/.test(String(argv[0])) &&
|
|
367
|
+
/spaps(?:\.js)?$/.test(String(argv[1]))
|
|
368
|
+
? argv.slice(2)
|
|
369
|
+
: argv;
|
|
239
370
|
try {
|
|
240
|
-
program.parse(
|
|
371
|
+
program.parse(normalizedArgv, { from: 'user' });
|
|
241
372
|
} catch (err) {
|
|
242
373
|
// Commander throws for help/version; we ignore in parse mode
|
|
243
374
|
}
|
package/src/docs-system.js
CHANGED
|
@@ -22,13 +22,13 @@ ${chalk.green('Basic Usage:')}
|
|
|
22
22
|
${chalk.gray('// CommonJS')}
|
|
23
23
|
const { SweetPotatoSDK } = require('spaps-sdk')
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
${chalk.gray('// Create client for local or provisioned SPAPS')}
|
|
26
26
|
const sdk = new SweetPotatoSDK({
|
|
27
27
|
apiUrl: process.env.SPAPS_API_URL || 'http://localhost:3301',
|
|
28
|
-
apiKey: process.env.SPAPS_API_KEY, ${chalk.gray('//
|
|
28
|
+
apiKey: process.env.SPAPS_API_KEY, ${chalk.gray('// Required unless /health/local-mode says otherwise')}
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
${chalk.gray('// Sign in with email/password
|
|
31
|
+
${chalk.gray('// Sign in with email/password')}
|
|
32
32
|
const auth = await sdk.auth.signInWithPassword({ email: 'user@example.com', password: 'password' })
|
|
33
33
|
console.log('User:', auth.user)
|
|
34
34
|
`
|
|
@@ -108,18 +108,18 @@ ${chalk.green('Environment Variables:')}
|
|
|
108
108
|
${chalk.green('Configuration Options:')}
|
|
109
109
|
const sdk = new SweetPotatoSDK({
|
|
110
110
|
apiUrl: process.env.SPAPS_API_URL || 'http://localhost:3301',
|
|
111
|
-
apiKey: process.env.SPAPS_API_KEY, ${chalk.gray('// Omit
|
|
111
|
+
apiKey: process.env.SPAPS_API_KEY, ${chalk.gray('// Omit only when /health/local-mode reports local_mode_active: true')}
|
|
112
112
|
})
|
|
113
113
|
|
|
114
114
|
${chalk.green('Local Mode Detection:')}
|
|
115
|
-
${chalk.gray('//
|
|
115
|
+
${chalk.gray('// Localhost URLs still need the running server to advertise local mode:')}
|
|
116
116
|
- URL contains 'localhost'
|
|
117
117
|
- URL contains '127.0.0.1'
|
|
118
118
|
- No API URL is provided
|
|
119
119
|
|
|
120
|
-
${chalk.gray('//
|
|
120
|
+
${chalk.gray('// Verify against the server before assuming no key is required')}
|
|
121
121
|
if (sdk.isLocalMode) {
|
|
122
|
-
console.log('
|
|
122
|
+
console.log('Check /health/local-mode before skipping API keys.')
|
|
123
123
|
}
|
|
124
124
|
`
|
|
125
125
|
},
|
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',
|