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.
Files changed (49) hide show
  1. package/AI_TOOLS.json +10 -11
  2. package/README.md +267 -110
  3. package/assets/local-runtime/Dockerfile +28 -0
  4. package/assets/local-runtime/alembic/env.py +101 -0
  5. package/assets/local-runtime/alembic/path_bootstrap.py +71 -0
  6. package/assets/local-runtime/alembic/versions/000000000001_baseline_consolidated_schema.py +1076 -0
  7. package/assets/local-runtime/alembic/versions/000000000002_fix_column_types_to_match_prod.py +83 -0
  8. package/assets/local-runtime/alembic/versions/000000000003_fix_email_template_key_uniqueness.py +49 -0
  9. package/assets/local-runtime/alembic/versions/000000000004_add_hold_duration_minutes_to_dayrate_config.py +30 -0
  10. package/assets/local-runtime/alembic/versions/000000000005_resource_scoped_entitlements.py +77 -0
  11. package/assets/local-runtime/alembic/versions/000000000006_cfo_rbac_add_is_admin.py +37 -0
  12. package/assets/local-runtime/alembic/versions/000000000007_agent_approvals.py +158 -0
  13. package/assets/local-runtime/alembic/versions/000000000008_add_company_id_to_cfo_connections.py +35 -0
  14. package/assets/local-runtime/alembic/versions/000000000009_tx_signing.py +62 -0
  15. package/assets/local-runtime/alembic/versions/000000000010_affiliate_referrals.py +235 -0
  16. package/assets/local-runtime/alembic/versions/000000000011_checkin_call_booking.py +137 -0
  17. package/assets/local-runtime/alembic/versions/000000000012_subscription_application_scoping.py +55 -0
  18. package/assets/local-runtime/alembic/versions/000000000013_refresh_token_anomaly_context.py +61 -0
  19. package/assets/local-runtime/alembic/versions/000000000014_buildooor_dayrate_hire_schedule.py +39 -0
  20. package/assets/local-runtime/alembic/versions/000000000015_support_telemetry_platform.py +112 -0
  21. package/assets/local-runtime/alembic/versions/000000000016_issue_reporting_platform.py +54 -0
  22. package/assets/local-runtime/alembic/versions/000000000017_issue_reporting_platform_import_tracking.py +44 -0
  23. package/assets/local-runtime/alembic/versions/000000000018_authorization_policy_engine.py +76 -0
  24. package/assets/local-runtime/alembic.ini +47 -0
  25. package/assets/local-runtime/docker-compose.yml +61 -0
  26. package/assets/local-runtime/manifest.json +8 -0
  27. package/assets/local-runtime/scripts/container-entrypoint.sh +13 -0
  28. package/assets/local-runtime/scripts/fetch-prod-db.sh +112 -0
  29. package/assets/local-runtime/scripts/run-migrations.sh +96 -0
  30. package/package.json +5 -4
  31. package/src/ai-helper.js +176 -234
  32. package/src/ai-tool-spec.js +52 -20
  33. package/src/auth/api-key.js +119 -0
  34. package/src/auth/client-id.js +136 -0
  35. package/src/auth/client.js +169 -0
  36. package/src/auth/credentials.js +110 -0
  37. package/src/auth/device-flow.js +159 -0
  38. package/src/auth/env.js +57 -0
  39. package/src/auth/handlers.js +462 -0
  40. package/src/auth/http.js +74 -0
  41. package/src/cli-dispatcher.js +155 -24
  42. package/src/docs-system.js +7 -7
  43. package/src/error-handler.js +42 -0
  44. package/src/fixture-kernel.js +1143 -0
  45. package/src/handlers.js +252 -15
  46. package/src/help-system.js +3 -1
  47. package/src/local-runtime.js +258 -0
  48. package/src/local-server.js +597 -199
  49. package/src/project-scaffolder.js +441 -0
@@ -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', (subcommandOrOpts, cmdOrOpts, isJsonOrCmd) => {
58
- // Handle both 'local' and 'local stop' cases
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 new project with SPAPS (coming soon)')
142
- .action(makeAction('create', (optsOrName, cmd) => ({ name: typeof optsOrName === 'string' ? optsOrName : cmd.args[0] })));
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(argv, { from: 'user' });
371
+ program.parse(normalizedArgv, { from: 'user' });
241
372
  } catch (err) {
242
373
  // Commander throws for help/version; we ignore in parse mode
243
374
  }
@@ -22,13 +22,13 @@ ${chalk.green('Basic Usage:')}
22
22
  ${chalk.gray('// CommonJS')}
23
23
  const { SweetPotatoSDK } = require('spaps-sdk')
24
24
 
25
- ${chalk.gray('// Create client (auto-detects local mode)')}
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('// Not required in local mode')}
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 (local mode accepts any credentials)')}
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 in local dev')}
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('// The SDK automatically detects local mode when:')}
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('// Check if in local mode')}
120
+ ${chalk.gray('// Verify against the server before assuming no key is required')}
121
121
  if (sdk.isLocalMode) {
122
- console.log('Running in local mode - no API key needed!')
122
+ console.log('Check /health/local-mode before skipping API keys.')
123
123
  }
124
124
  `
125
125
  },
@@ -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',