spaps 0.7.6 → 0.7.7

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.
@@ -44,6 +44,38 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
44
44
  };
45
45
  }
46
46
 
47
+ function allowDryRun(command) {
48
+ if (!dryRun) return command;
49
+ command.allowUnknownOption(true);
50
+ if (typeof command.allowExcessArguments === 'function') {
51
+ command.allowExcessArguments(true);
52
+ }
53
+ return command;
54
+ }
55
+
56
+ function addRemoteCommandOptions(command) {
57
+ return command
58
+ .option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
59
+ .option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
60
+ .option('--json', 'Output in JSON format');
61
+ }
62
+
63
+ // spaps home
64
+ const cmdHome = program
65
+ .command('home')
66
+ .description('Show operator, app, and runtime state')
67
+ .option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
68
+ .option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
69
+ .option('--json', 'Output in JSON format')
70
+ .action(
71
+ makeAction('home', (opts, _cmd, isJson) => ({
72
+ port: Number(opts.port) || DEFAULT_PORT,
73
+ serverUrl: opts.serverUrl || null,
74
+ json: isJson,
75
+ }))
76
+ );
77
+ allowDryRun(cmdHome);
78
+
47
79
  // spaps local
48
80
  const cmdLocal = program
49
81
  .command('local [subcommand]')
@@ -58,7 +90,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
58
90
  )
59
91
  .option('-d, --detach', 'Run in background (don\'t tail logs)', false)
60
92
  .option('--fresh', 'Fresh start: tear down and rebuild from scratch', false)
61
- .option('--from-backup <path>', 'Load from Supabase backup file', null)
93
+ .option('--from-backup <path>', 'Load from database dump file', null)
62
94
  .option('-o, --open', 'Open browser automatically', false)
63
95
  .option('--json', 'Output in JSON format')
64
96
  .action(
@@ -80,12 +112,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
80
112
  return out;
81
113
  })
82
114
  );
83
- if (dryRun) {
84
- cmdLocal.allowUnknownOption(true);
85
- if (typeof cmdLocal.allowExcessArguments === 'function') {
86
- cmdLocal.allowExcessArguments(true);
87
- }
88
- }
115
+ allowDryRun(cmdLocal);
89
116
 
90
117
  // spaps quickstart
91
118
  const cmdQuick = program
@@ -94,12 +121,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
94
121
  .option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
95
122
  .option('--json', 'Output in JSON format')
96
123
  .action(makeAction('quickstart', (opts, _cmd, isJson) => ({ port: Number(opts.port), json: isJson })));
97
- if (dryRun) {
98
- cmdQuick.allowUnknownOption(true);
99
- if (typeof cmdQuick.allowExcessArguments === 'function') {
100
- cmdQuick.allowExcessArguments(true);
101
- }
102
- }
124
+ allowDryRun(cmdQuick);
103
125
 
104
126
  // spaps status
105
127
  const cmdStatus = program
@@ -108,12 +130,24 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
108
130
  .option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
109
131
  .option('--json', 'Output in JSON format')
110
132
  .action(makeAction('status', (opts, _cmd, isJson) => ({ port: Number(opts.port), json: isJson })));
111
- if (dryRun) {
112
- cmdStatus.allowUnknownOption(true);
113
- if (typeof cmdStatus.allowExcessArguments === 'function') {
114
- cmdStatus.allowExcessArguments(true);
115
- }
116
- }
133
+ allowDryRun(cmdStatus);
134
+
135
+ // spaps verify
136
+ const cmdVerify = program
137
+ .command('verify')
138
+ .alias('test')
139
+ .description('Run a quick SPAPS verification')
140
+ .option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
141
+ .option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
142
+ .option('--json', 'Output in JSON format')
143
+ .action(
144
+ makeAction('verify', (opts, _cmd, isJson) => ({
145
+ port: Number(opts.port) || DEFAULT_PORT,
146
+ serverUrl: opts.serverUrl || null,
147
+ json: isJson,
148
+ }))
149
+ );
150
+ allowDryRun(cmdVerify);
117
151
 
118
152
  // spaps init
119
153
  const cmdInit = program
@@ -121,12 +155,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
121
155
  .description('Initialize SPAPS in current project')
122
156
  .option('--json', 'Output in JSON format')
123
157
  .action(makeAction('init', (_opts, _cmd, isJson) => ({ json: isJson })));
124
- if (dryRun) {
125
- cmdInit.allowUnknownOption(true);
126
- if (typeof cmdInit.allowExcessArguments === 'function') {
127
- cmdInit.allowExcessArguments(true);
128
- }
129
- }
158
+ allowDryRun(cmdInit);
130
159
 
131
160
  // spaps create <name>
132
161
  const cmdCreate = program
@@ -147,24 +176,14 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
147
176
  json: isJson,
148
177
  }))
149
178
  );
150
- if (dryRun) {
151
- cmdCreate.allowUnknownOption(true);
152
- if (typeof cmdCreate.allowExcessArguments === 'function') {
153
- cmdCreate.allowExcessArguments(true);
154
- }
155
- }
179
+ allowDryRun(cmdCreate);
156
180
 
157
181
  // spaps types
158
182
  const cmdTypes = program
159
183
  .command('types')
160
184
  .description('Generate TypeScript types (coming soon)')
161
185
  .action(makeAction('types', () => ({})));
162
- if (dryRun) {
163
- cmdTypes.allowUnknownOption(true);
164
- if (typeof cmdTypes.allowExcessArguments === 'function') {
165
- cmdTypes.allowExcessArguments(true);
166
- }
167
- }
186
+ allowDryRun(cmdTypes);
168
187
 
169
188
  // spaps help
170
189
  const cmdHelp = program
@@ -175,12 +194,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
175
194
  .action(
176
195
  makeAction('help', (opts) => ({ interactive: Boolean(opts.interactive), quick: Boolean(opts.quick) }))
177
196
  );
178
- if (dryRun) {
179
- cmdHelp.allowUnknownOption(true);
180
- if (typeof cmdHelp.allowExcessArguments === 'function') {
181
- cmdHelp.allowExcessArguments(true);
182
- }
183
- }
197
+ allowDryRun(cmdHelp);
184
198
 
185
199
  // spaps docs
186
200
  const cmdDocs = program
@@ -192,12 +206,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
192
206
  .action(
193
207
  makeAction('docs', (opts, _cmd, isJson) => ({ interactive: Boolean(opts.interactive), search: opts.search || null, json: isJson }))
194
208
  );
195
- if (dryRun) {
196
- cmdDocs.allowUnknownOption(true);
197
- if (typeof cmdDocs.allowExcessArguments === 'function') {
198
- cmdDocs.allowExcessArguments(true);
199
- }
200
- }
209
+ allowDryRun(cmdDocs);
201
210
 
202
211
  // spaps tools
203
212
  const cmdTools = program
@@ -209,12 +218,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
209
218
  .action(
210
219
  makeAction('tools', (opts, _cmd, isJson) => ({ port: Number(opts.port), format: String(opts.format || 'openai'), json: isJson }))
211
220
  );
212
- if (dryRun) {
213
- cmdTools.allowUnknownOption(true);
214
- if (typeof cmdTools.allowExcessArguments === 'function') {
215
- cmdTools.allowExcessArguments(true);
216
- }
217
- }
221
+ allowDryRun(cmdTools);
218
222
 
219
223
  // spaps fixtures
220
224
  const cmdFixtures = program
@@ -223,7 +227,8 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
223
227
  .option('--dir <dir>', 'Target repo directory (defaults to current working directory)')
224
228
  .option('-p, --port <port>', 'Port to inspect for SPAPS runtime hints', String(DEFAULT_PORT))
225
229
  .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')
230
+ .option('--persona <persona>', 'Persona code to target for apply or storage-state export')
231
+ .option('--seed', 'Run persona-declared seed requests against the local SPAPS server', false)
227
232
  .option('-f, --format <format>', 'Artifact format (playwright)', 'playwright')
228
233
  .option('--force', 'Overwrite fixture files during init', false)
229
234
  .option('--json', 'Output in JSON format')
@@ -235,18 +240,14 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
235
240
  port: Number(opts.port) || DEFAULT_PORT,
236
241
  baseUrl: opts.baseUrl || null,
237
242
  persona: opts.persona || null,
243
+ seed: Boolean(opts.seed),
238
244
  format: String(opts.format || 'playwright'),
239
245
  force: Boolean(opts.force),
240
246
  json: isJson,
241
247
  };
242
248
  })
243
249
  );
244
- if (dryRun) {
245
- cmdFixtures.allowUnknownOption(true);
246
- if (typeof cmdFixtures.allowExcessArguments === 'function') {
247
- cmdFixtures.allowExcessArguments(true);
248
- }
249
- }
250
+ allowDryRun(cmdFixtures);
250
251
 
251
252
  // spaps doctor
252
253
  const cmdDoctor = program
@@ -258,16 +259,12 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
258
259
  .action(
259
260
  makeAction('doctor', (opts, _cmd, isJson) => ({ port: Number(opts.port), stripe: opts.stripe || null, json: isJson }))
260
261
  );
261
- if (dryRun) {
262
- cmdDoctor.allowUnknownOption(true);
263
- if (typeof cmdDoctor.allowExcessArguments === 'function') {
264
- cmdDoctor.allowExcessArguments(true);
265
- }
266
- }
262
+ allowDryRun(cmdDoctor);
267
263
 
268
264
  // spaps login
269
265
  const cmdLogin = program
270
266
  .command('login')
267
+ .alias('connect')
271
268
  .description('Authenticate with a SPAPS server (RFC 8628 device flow)')
272
269
  .option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
273
270
  .option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
@@ -281,12 +278,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
281
278
  json: isJson,
282
279
  }))
283
280
  );
284
- if (dryRun) {
285
- cmdLogin.allowUnknownOption(true);
286
- if (typeof cmdLogin.allowExcessArguments === 'function') {
287
- cmdLogin.allowExcessArguments(true);
288
- }
289
- }
281
+ allowDryRun(cmdLogin);
290
282
 
291
283
  // spaps logout
292
284
  const cmdLogout = program
@@ -302,12 +294,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
302
294
  json: isJson,
303
295
  }))
304
296
  );
305
- if (dryRun) {
306
- cmdLogout.allowUnknownOption(true);
307
- if (typeof cmdLogout.allowExcessArguments === 'function') {
308
- cmdLogout.allowExcessArguments(true);
309
- }
310
- }
297
+ allowDryRun(cmdLogout);
311
298
 
312
299
  // spaps whoami
313
300
  const cmdWhoami = program
@@ -323,12 +310,7 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
323
310
  json: isJson,
324
311
  }))
325
312
  );
326
- if (dryRun) {
327
- cmdWhoami.allowUnknownOption(true);
328
- if (typeof cmdWhoami.allowExcessArguments === 'function') {
329
- cmdWhoami.allowExcessArguments(true);
330
- }
331
- }
313
+ allowDryRun(cmdWhoami);
332
314
 
333
315
  // spaps token (print access token for piping to curl or env vars)
334
316
  const cmdToken = program
@@ -344,12 +326,304 @@ function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo
344
326
  json: isJson,
345
327
  }))
346
328
  );
347
- if (dryRun) {
348
- cmdToken.allowUnknownOption(true);
349
- if (typeof cmdToken.allowExcessArguments === 'function') {
350
- cmdToken.allowExcessArguments(true);
351
- }
329
+ allowDryRun(cmdToken);
330
+
331
+ // spaps dayrate <subcommand>
332
+ const cmdDayrate = program
333
+ .command('dayrate <subcommand>')
334
+ .description('Dayrate domain commands (subcommand: config)')
335
+ .option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
336
+ .option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
337
+ .option('--json', 'Output in JSON format')
338
+ .action(
339
+ makeAction('dayrate', (opts, cmd, isJson) => ({
340
+ subcommand: cmd.args[0],
341
+ port: Number(opts.port) || DEFAULT_PORT,
342
+ serverUrl: opts.serverUrl || null,
343
+ json: isJson,
344
+ }))
345
+ );
346
+ allowDryRun(cmdDayrate);
347
+
348
+ // spaps email
349
+ const cmdEmail = program
350
+ .command('email')
351
+ .description('Email domain commands')
352
+ .showHelpAfterError()
353
+ .showSuggestionAfterError()
354
+ .addHelpText('after', '\nUse `spaps email <verb> --help` for verb-specific flags.\n');
355
+ if (!dryRun) {
356
+ cmdEmail.action(() => {
357
+ cmdEmail.outputHelp();
358
+ });
352
359
  }
360
+ allowDryRun(cmdEmail);
361
+
362
+ function addEmailSubcommand(name, description, addOptions, shape) {
363
+ const command = cmdEmail.command(name).description(description);
364
+ if (typeof addOptions === 'function') addOptions(command);
365
+ addRemoteCommandOptions(command);
366
+ command.action(
367
+ makeAction('email', (opts, _cmd, isJson) => ({
368
+ subcommand: name,
369
+ port: Number(opts.port) || DEFAULT_PORT,
370
+ serverUrl: opts.serverUrl || null,
371
+ ...shape(opts),
372
+ json: isJson,
373
+ }))
374
+ );
375
+ allowDryRun(command);
376
+ return command;
377
+ }
378
+
379
+ addEmailSubcommand(
380
+ 'send',
381
+ 'Send a transactional email by template key',
382
+ (command) =>
383
+ command
384
+ .requiredOption('--template-key <key>', 'Template key')
385
+ .requiredOption('--to <email>', 'Recipient email address')
386
+ .option('--context <json>', 'Template context as JSON')
387
+ .option('--user-id <id>', 'User id for log attribution')
388
+ .option('--owner-id <id>', 'Owner id for log attribution')
389
+ .option('--subject-override <text>', 'Subject override')
390
+ .option('--body-override <text>', 'Body override'),
391
+ (opts) => ({
392
+ templateKey: opts.templateKey || null,
393
+ to: opts.to || null,
394
+ context: opts.context || null,
395
+ userId: opts.userId || null,
396
+ ownerId: opts.ownerId || null,
397
+ subjectOverride: opts.subjectOverride || null,
398
+ bodyOverride: opts.bodyOverride || null,
399
+ })
400
+ );
401
+
402
+ addEmailSubcommand(
403
+ 'get-template',
404
+ 'Fetch one email template by key',
405
+ (command) => command.requiredOption('--template-key <key>', 'Template key'),
406
+ (opts) => ({
407
+ templateKey: opts.templateKey || null,
408
+ })
409
+ );
410
+
411
+ addEmailSubcommand(
412
+ 'preview',
413
+ 'Render a template preview using sample or custom context',
414
+ (command) =>
415
+ command
416
+ .requiredOption('--template-key <key>', 'Template key')
417
+ .option('--context <json>', 'Template context as JSON'),
418
+ (opts) => ({
419
+ templateKey: opts.templateKey || null,
420
+ context: opts.context || null,
421
+ })
422
+ );
423
+
424
+ addEmailSubcommand(
425
+ 'logs',
426
+ 'List email logs filtered by owner or user',
427
+ (command) =>
428
+ command
429
+ .option('--owner-id <id>', 'Owner id for log attribution')
430
+ .option('--user-id <id>', 'User id for log attribution')
431
+ .option('--limit <n>', 'Pagination limit')
432
+ .option('--offset <n>', 'Pagination offset'),
433
+ (opts) => ({
434
+ ownerId: opts.ownerId || null,
435
+ userId: opts.userId || null,
436
+ limit: opts.limit ? Number(opts.limit) : null,
437
+ offset: opts.offset ? Number(opts.offset) : null,
438
+ })
439
+ );
440
+
441
+ addEmailSubcommand(
442
+ 'list-templates',
443
+ 'List all templates for the active application',
444
+ null,
445
+ () => ({})
446
+ );
447
+
448
+ addEmailSubcommand(
449
+ 'create-template',
450
+ 'Create a new template',
451
+ (command) =>
452
+ command
453
+ .requiredOption('--template-key <key>', 'Template key')
454
+ .requiredOption('--name <name>', 'Template display name')
455
+ .requiredOption('--subject <text>', 'Template subject')
456
+ .requiredOption('--html-body <html>', 'Template HTML body')
457
+ .option('--text-body <text>', 'Template text body')
458
+ .option('--description <text>', 'Template description')
459
+ .option('--from-email <email>', 'From email address')
460
+ .option('--from-name <name>', 'From display name')
461
+ .option('--reply-to <email>', 'Reply-to address')
462
+ .option('--variables <json>', 'Template variables JSON')
463
+ .option('--sample-context <json>', 'Sample context JSON')
464
+ .option('--is-active <bool>', 'Template active state')
465
+ .option('--category <category>', 'Template category'),
466
+ (opts) => ({
467
+ templateKey: opts.templateKey || null,
468
+ name: opts.name || null,
469
+ subject: opts.subject || null,
470
+ htmlBody: opts.htmlBody || null,
471
+ textBody: opts.textBody || null,
472
+ description: opts.description || null,
473
+ fromEmail: opts.fromEmail || null,
474
+ fromName: opts.fromName || null,
475
+ replyTo: opts.replyTo || null,
476
+ variables: opts.variables || null,
477
+ sampleContext: opts.sampleContext || null,
478
+ isActive: opts.isActive === undefined ? null : opts.isActive,
479
+ category: opts.category || null,
480
+ })
481
+ );
482
+
483
+ addEmailSubcommand(
484
+ 'update-template',
485
+ 'Update an existing template',
486
+ (command) =>
487
+ command
488
+ .requiredOption('--template-key <key>', 'Template key')
489
+ .option('--name <name>', 'Template display name')
490
+ .option('--subject <text>', 'Template subject')
491
+ .option('--html-body <html>', 'Template HTML body')
492
+ .option('--text-body <text>', 'Template text body')
493
+ .option('--description <text>', 'Template description')
494
+ .option('--from-email <email>', 'From email address')
495
+ .option('--from-name <name>', 'From display name')
496
+ .option('--reply-to <email>', 'Reply-to address')
497
+ .option('--variables <json>', 'Template variables JSON')
498
+ .option('--sample-context <json>', 'Sample context JSON')
499
+ .option('--is-active <bool>', 'Template active state')
500
+ .option('--category <category>', 'Template category'),
501
+ (opts) => ({
502
+ templateKey: opts.templateKey || null,
503
+ name: opts.name || null,
504
+ subject: opts.subject || null,
505
+ htmlBody: opts.htmlBody || null,
506
+ textBody: opts.textBody || null,
507
+ description: opts.description || null,
508
+ fromEmail: opts.fromEmail || null,
509
+ fromName: opts.fromName || null,
510
+ replyTo: opts.replyTo || null,
511
+ variables: opts.variables || null,
512
+ sampleContext: opts.sampleContext || null,
513
+ isActive: opts.isActive === undefined ? null : opts.isActive,
514
+ category: opts.category || null,
515
+ })
516
+ );
517
+
518
+ addEmailSubcommand(
519
+ 'get-override',
520
+ 'Fetch the current override for a template',
521
+ (command) => command.requiredOption('--template-key <key>', 'Template key'),
522
+ (opts) => ({
523
+ templateKey: opts.templateKey || null,
524
+ })
525
+ );
526
+
527
+ addEmailSubcommand(
528
+ 'set-override',
529
+ 'Create or update an override for a template',
530
+ (command) =>
531
+ command
532
+ .requiredOption('--template-key <key>', 'Template key')
533
+ .option('--subject-override <text>', 'Subject override')
534
+ .option('--body-override <text>', 'Body override'),
535
+ (opts) => ({
536
+ templateKey: opts.templateKey || null,
537
+ subjectOverride: opts.subjectOverride || null,
538
+ bodyOverride: opts.bodyOverride || null,
539
+ })
540
+ );
541
+
542
+ addEmailSubcommand(
543
+ 'clear-override',
544
+ 'Delete an override for a template',
545
+ (command) => command.requiredOption('--template-key <key>', 'Template key'),
546
+ (opts) => ({
547
+ templateKey: opts.templateKey || null,
548
+ })
549
+ );
550
+
551
+ // spaps policy <subcommand>
552
+ const cmdPolicy = program
553
+ .command('policy <subcommand>')
554
+ .description('Policies domain commands (subcommand: list|create|delete)')
555
+ .option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
556
+ .option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
557
+ .option('--name <name>', 'Policy name (create)')
558
+ .option('--effect <effect>', 'Policy effect: allow|deny (create)')
559
+ .option('--conditions <json>', 'Policy conditions as JSON (create)')
560
+ .option('--description <text>', 'Policy description (create)')
561
+ .option('--priority <n>', 'Policy priority (create)')
562
+ .option('--id <id>', 'Policy id (delete)')
563
+ .option('--is-active <bool>', 'Filter by is_active (list)')
564
+ .option('--limit <n>', 'Limit (list)')
565
+ .option('--json', 'Output in JSON format')
566
+ .action(
567
+ makeAction('policy', (opts, cmd, isJson) => ({
568
+ subcommand: cmd.args[0],
569
+ port: Number(opts.port) || DEFAULT_PORT,
570
+ serverUrl: opts.serverUrl || null,
571
+ name: opts.name || null,
572
+ effect: opts.effect || null,
573
+ conditions: opts.conditions || null,
574
+ description: opts.description || null,
575
+ priority: opts.priority ? Number(opts.priority) : 0,
576
+ id: opts.id || null,
577
+ isActive: opts.isActive === undefined ? null : opts.isActive,
578
+ limit: opts.limit ? Number(opts.limit) : null,
579
+ json: isJson,
580
+ }))
581
+ );
582
+ allowDryRun(cmdPolicy);
583
+
584
+ // spaps webhook <subcommand>
585
+ const cmdWebhook = program
586
+ .command('webhook <subcommand>')
587
+ .description('Webhooks domain commands (subcommand: list|register)')
588
+ .option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
589
+ .option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
590
+ .option('--url <url>', 'Destination URL (register)')
591
+ .option('--events <csv>', 'Comma-separated event keys (register)')
592
+ .option('--json', 'Output in JSON format')
593
+ .action(
594
+ makeAction('webhook', (opts, cmd, isJson) => ({
595
+ subcommand: cmd.args[0],
596
+ port: Number(opts.port) || DEFAULT_PORT,
597
+ serverUrl: opts.serverUrl || null,
598
+ url: opts.url || null,
599
+ events: opts.events || null,
600
+ json: isJson,
601
+ }))
602
+ );
603
+ allowDryRun(cmdWebhook);
604
+
605
+ // spaps issue-reports <subcommand>
606
+ const cmdIssueReports = program
607
+ .command('issue-reports <subcommand>')
608
+ .description('Issue reporting commands (subcommand: list-mine)')
609
+ .option('-p, --port <port>', 'Port (default: 3301)', String(DEFAULT_PORT))
610
+ .option('--server-url <url>', 'Full server URL (overrides --port and SPAPS_API_URL)')
611
+ .option('--status <status>', 'Filter by status')
612
+ .option('--limit <n>', 'Pagination limit')
613
+ .option('--offset <n>', 'Pagination offset')
614
+ .option('--json', 'Output in JSON format')
615
+ .action(
616
+ makeAction('issue-reports', (opts, cmd, isJson) => ({
617
+ subcommand: cmd.args[0],
618
+ port: Number(opts.port) || DEFAULT_PORT,
619
+ serverUrl: opts.serverUrl || null,
620
+ status: opts.status || null,
621
+ limit: opts.limit ? Number(opts.limit) : null,
622
+ offset: opts.offset ? Number(opts.offset) : null,
623
+ json: isJson,
624
+ }))
625
+ );
626
+ allowDryRun(cmdIssueReports);
353
627
 
354
628
  return { program, getIntents: () => intents };
355
629
  }
package/src/doctor.js CHANGED
@@ -6,6 +6,7 @@ const chalk = require('chalk');
6
6
 
7
7
  const { getServerStatus } = require('./ai-helper');
8
8
  const { DEFAULT_PORT } = require('./config');
9
+ const { listDomains } = require('./domains');
9
10
 
10
11
  function checkNodeVersion() {
11
12
  const version = process.versions.node || '0.0.0';
@@ -169,6 +170,60 @@ async function checkWebhook(port) {
169
170
  }
170
171
  }
171
172
 
173
+ async function probeDomain(port, domain) {
174
+ const httpMod = require('http');
175
+ const { method, path: probePath, ok_on_404 = false, ok_on_auth_error = false } = domain.probe;
176
+ return new Promise((resolve) => {
177
+ const req = httpMod.request(
178
+ { hostname: 'localhost', port, path: probePath, method, timeout: 2000 },
179
+ (res) => {
180
+ // Drain so the socket can close cleanly.
181
+ res.on('data', () => {});
182
+ res.on('end', () => {
183
+ const code = res.statusCode || 0;
184
+ const is2xx = code >= 200 && code < 300;
185
+ const is404 = code === 404;
186
+ const isAuth = code === 401 || code === 403;
187
+ const mounted = is2xx || (ok_on_404 && is404) || (ok_on_auth_error && isAuth);
188
+ resolve({
189
+ check: `domain_${domain.key}_mounted`,
190
+ success: mounted,
191
+ details: { probe: `${method} ${probePath}`, status: code, domain: domain.key },
192
+ fix: mounted
193
+ ? null
194
+ : `Probe ${method} ${probePath} returned ${code}; confirm the ${domain.label} router is mounted on the running server.`,
195
+ });
196
+ });
197
+ }
198
+ );
199
+ req.on('error', (err) => {
200
+ resolve({
201
+ check: `domain_${domain.key}_mounted`,
202
+ success: false,
203
+ details: { probe: `${method} ${probePath}`, error: err.message, domain: domain.key },
204
+ fix: `Server unreachable while probing ${method} ${probePath}. Start the local stack and retry.`,
205
+ });
206
+ });
207
+ req.on('timeout', () => {
208
+ req.destroy(new Error('probe timeout'));
209
+ });
210
+ req.end();
211
+ });
212
+ }
213
+
214
+ async function checkDomainMounts(port) {
215
+ const status = await getServerStatus(port);
216
+ if (!status.running) {
217
+ return listDomains().map((d) => ({
218
+ check: `domain_${d.key}_mounted`,
219
+ success: false,
220
+ details: { probe: `${d.probe.method} ${d.probe.path}`, running: false, domain: d.key },
221
+ fix: `Start server: npx spaps local --port ${port}`,
222
+ }));
223
+ }
224
+ return Promise.all(listDomains().map((d) => probeDomain(port, d)));
225
+ }
226
+
172
227
  function formatHuman(results) {
173
228
  const ok = results.every(r => r.success);
174
229
  console.log(chalk.yellow('\nšŸ  SPAPS Doctor\n'));
@@ -203,6 +258,8 @@ async function runDoctor({ port = DEFAULT_PORT, stripe = null, json = false } =
203
258
  results.push(checkEnvTest());
204
259
  results.push(await checkNextJsPort());
205
260
  results.push(await checkWebhook(port));
261
+ const domainResults = await checkDomainMounts(port);
262
+ results.push(...domainResults);
206
263
 
207
264
  const ok = results.every(r => r.success);
208
265
  const payload = { success: ok, results, next_steps: ok ? [] : ['Apply suggested fixes and re-run: npx spaps doctor --json'] };
@@ -214,4 +271,4 @@ async function runDoctor({ port = DEFAULT_PORT, stripe = null, json = false } =
214
271
  return payload;
215
272
  }
216
273
 
217
- module.exports = { runDoctor };
274
+ module.exports = { runDoctor, checkDomainMounts };