spaps 0.7.5 → 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.
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 { buildHomeView, renderHomeView } = require('./home-view');
10
11
  const {
11
12
  applyFixtures,
12
13
  exportStorageState,
@@ -20,6 +21,7 @@ const {
20
21
  whoamiHandler,
21
22
  tokenHandler,
22
23
  } = require('./auth/handlers');
24
+ const { callEndpoint, emit, emitAuthError } = require('./domain-cli');
23
25
 
24
26
  function createHandlers(version, logo) {
25
27
  function invalidArgument(message) {
@@ -28,7 +30,54 @@ function createHandlers(version, logo) {
28
30
  return error;
29
31
  }
30
32
 
33
+ const verifyHandler = async ({ options }) => {
34
+ const result = await runQuickTest({
35
+ port: options.port,
36
+ serverUrl: options.serverUrl,
37
+ cwd: process.cwd(),
38
+ });
39
+
40
+ if (options.json) {
41
+ console.log(JSON.stringify(result, null, 2));
42
+ return;
43
+ }
44
+
45
+ console.log(chalk.yellow('\nšŸ  SPAPS Verify\n'));
46
+ console.log(result.success ? chalk.green(result.summary) : chalk.yellow(result.summary));
47
+ console.log();
48
+ (result.results || []).forEach((entry) => {
49
+ const mark = entry.success ? chalk.green('āœ“') : chalk.red('āœ—');
50
+ const message = entry.message ? `: ${entry.message}` : '';
51
+ console.log(` ${mark} ${entry.test}${message}`);
52
+ if (!entry.success && entry.fix) {
53
+ console.log(chalk.gray(` fix: ${entry.fix}`));
54
+ }
55
+ });
56
+ if (Array.isArray(result.next_steps) && result.next_steps.length > 0) {
57
+ console.log();
58
+ console.log(chalk.bold('Next'));
59
+ result.next_steps.forEach((step) => {
60
+ console.log(chalk.cyan(` ${step}`));
61
+ });
62
+ }
63
+ console.log();
64
+ };
65
+
31
66
  return {
67
+ home: async ({ options }) => {
68
+ const view = await buildHomeView({
69
+ port: options.port,
70
+ serverUrl: options.serverUrl,
71
+ cwd: process.cwd(),
72
+ });
73
+
74
+ if (options.json) {
75
+ console.log(JSON.stringify(view, null, 2));
76
+ return;
77
+ }
78
+
79
+ renderHomeView(view, { logo });
80
+ },
32
81
  local: async ({ options }) => {
33
82
  const isJson = options.json;
34
83
  if (!isJson) console.log(logo);
@@ -127,10 +176,7 @@ function createHandlers(version, logo) {
127
176
  }
128
177
  }
129
178
  },
130
- test: async ({ options }) => {
131
- const result = await runQuickTest(options.port);
132
- console.log(JSON.stringify(result, null, 2));
133
- },
179
+ verify: verifyHandler,
134
180
  init: async ({ options }) => {
135
181
  const isJson = options.json;
136
182
  const envContent = `# SPAPS Local Development\nSPAPS_API_URL=http://localhost:${DEFAULT_PORT}\n# SPAPS_API_KEY=your-api-key-here\n`;
@@ -305,6 +351,8 @@ function createHandlers(version, logo) {
305
351
  port: options.port,
306
352
  baseUrl: options.baseUrl,
307
353
  version,
354
+ persona: options.persona,
355
+ seed: options.seed,
308
356
  });
309
357
  break;
310
358
  case 'reset':
@@ -313,6 +361,7 @@ function createHandlers(version, logo) {
313
361
  port: options.port,
314
362
  baseUrl: options.baseUrl,
315
363
  version,
364
+ seed: options.seed,
316
365
  });
317
366
  break;
318
367
  case 'storage-state':
@@ -325,6 +374,7 @@ function createHandlers(version, logo) {
325
374
  baseUrl: options.baseUrl,
326
375
  version,
327
376
  persona: options.persona,
377
+ seed: options.seed,
328
378
  });
329
379
  break;
330
380
  default:
@@ -362,6 +412,13 @@ function createHandlers(version, logo) {
362
412
  });
363
413
  }
364
414
 
415
+ if (Array.isArray(result.seeding?.personas) && result.seeding.personas.length > 0) {
416
+ console.log(chalk.green('\nSeeded personas:'));
417
+ result.seeding.personas.forEach((entry) => {
418
+ console.log(chalk.gray(` • ${entry.persona} (${entry.request_count} requests)`));
419
+ });
420
+ }
421
+
365
422
  if (result.generated?.personas?.length) {
366
423
  console.log(chalk.green('\nGenerated personas:'));
367
424
  result.generated.personas.forEach((entry) => {
@@ -410,11 +467,421 @@ function createHandlers(version, logo) {
410
467
  doctor: async ({ options }) => {
411
468
  await runDoctor({ port: options.port || DEFAULT_PORT, stripe: options.stripe || null, json: options.json });
412
469
  },
470
+ test: verifyHandler,
413
471
  login: loginHandler,
414
472
  logout: logoutHandler,
415
473
  whoami: whoamiHandler,
416
474
  token: tokenHandler,
475
+ dayrate: dayrateHandler,
476
+ email: emailHandler,
477
+ policy: policyHandler,
478
+ webhook: webhookHandler,
479
+ 'issue-reports': issueReportsHandler,
417
480
  };
418
481
  }
419
482
 
483
+ function emitText({ intent, result, isJson, successMessage = null }) {
484
+ if (isJson) {
485
+ emit({ intent, result, isJson, successMessage });
486
+ return;
487
+ }
488
+ if (!result.ok) {
489
+ emit({ intent, result, isJson, successMessage });
490
+ return;
491
+ }
492
+ if (successMessage) {
493
+ console.log(successMessage);
494
+ }
495
+ const output = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
496
+ process.stdout.write(output);
497
+ if (!output.endsWith('\n')) process.stdout.write('\n');
498
+ }
499
+
500
+ function parseJsonFlag(raw, label) {
501
+ if (raw === null || raw === undefined) {
502
+ return { ok: true, value: null };
503
+ }
504
+ try {
505
+ return { ok: true, value: JSON.parse(raw) };
506
+ } catch {
507
+ console.error(`${label}: must be valid JSON`);
508
+ process.exitCode = 2;
509
+ return { ok: false, value: null };
510
+ }
511
+ }
512
+
513
+ function parseBooleanFlag(raw, label) {
514
+ if (raw === null || raw === undefined) {
515
+ return { ok: true, value: null };
516
+ }
517
+
518
+ if (typeof raw === 'boolean') {
519
+ return { ok: true, value: raw };
520
+ }
521
+
522
+ const normalized = String(raw).trim().toLowerCase();
523
+ if (['true', '1', 'yes', 'y'].includes(normalized)) {
524
+ return { ok: true, value: true };
525
+ }
526
+ if (['false', '0', 'no', 'n'].includes(normalized)) {
527
+ return { ok: true, value: false };
528
+ }
529
+
530
+ console.error(`${label}: must be one of true|false|1|0|yes|no`);
531
+ process.exitCode = 2;
532
+ return { ok: false, value: null };
533
+ }
534
+
535
+ function requireOption(value, message) {
536
+ if (value !== null && value !== undefined && value !== '') {
537
+ return true;
538
+ }
539
+ console.error(message);
540
+ process.exitCode = 2;
541
+ return false;
542
+ }
543
+
544
+ function assignIfDefined(target, key, value) {
545
+ if (value !== null && value !== undefined) {
546
+ target[key] = value;
547
+ }
548
+ }
549
+
550
+ async function dayrateHandler({ options }) {
551
+ const isJson = Boolean(options.json);
552
+ const sub = options.subcommand;
553
+ if (sub !== 'config') {
554
+ console.error('dayrate: unknown subcommand. Supported: config');
555
+ process.exitCode = 2;
556
+ return;
557
+ }
558
+ try {
559
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/dayrate/admin/config' });
560
+ emit({ intent: 'dayrate.config', result, isJson });
561
+ if (!result.ok) process.exitCode = 1;
562
+ } catch (err) {
563
+ emitAuthError('dayrate.config', err, isJson);
564
+ process.exitCode = 1;
565
+ }
566
+ }
567
+
568
+ async function emailHandler({ options }) {
569
+ const isJson = Boolean(options.json);
570
+ const sub = options.subcommand;
571
+
572
+ try {
573
+ if (sub === 'send') {
574
+ if (!requireOption(options.templateKey, 'email send: --template-key is required')) return;
575
+ if (!requireOption(options.to, 'email send: --to is required')) return;
576
+
577
+ const context = parseJsonFlag(options.context, 'email send: --context');
578
+ if (!context.ok) return;
579
+
580
+ const body = {
581
+ template_key: options.templateKey,
582
+ to: options.to,
583
+ };
584
+ assignIfDefined(body, 'context', context.value);
585
+ assignIfDefined(body, 'user_id', options.userId);
586
+ assignIfDefined(body, 'owner_id', options.ownerId);
587
+ assignIfDefined(body, 'subject_override', options.subjectOverride);
588
+ assignIfDefined(body, 'body_override', options.bodyOverride);
589
+
590
+ const result = await callEndpoint({ options, method: 'POST', path: '/api/email/send', body });
591
+ emit({ intent: 'email.send', result, isJson });
592
+ if (!result.ok) process.exitCode = 1;
593
+ return;
594
+ }
595
+
596
+ if (sub === 'get-template') {
597
+ if (!requireOption(options.templateKey, 'email get-template: --template-key is required')) return;
598
+ const result = await callEndpoint({
599
+ options,
600
+ method: 'GET',
601
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}`,
602
+ });
603
+ emit({ intent: 'email.get-template', result, isJson });
604
+ if (!result.ok) process.exitCode = 1;
605
+ return;
606
+ }
607
+
608
+ if (sub === 'preview') {
609
+ if (!requireOption(options.templateKey, 'email preview: --template-key is required')) return;
610
+
611
+ const context = parseJsonFlag(options.context, 'email preview: --context');
612
+ if (!context.ok) return;
613
+
614
+ const path = `/api/email/templates/${encodeURIComponent(options.templateKey)}/preview`;
615
+ const result = context.value === null
616
+ ? await callEndpoint({ options, method: 'GET', path })
617
+ : await callEndpoint({
618
+ options,
619
+ method: 'POST',
620
+ path,
621
+ body: { context: context.value },
622
+ });
623
+
624
+ emitText({ intent: 'email.preview', result, isJson });
625
+ if (!result.ok) process.exitCode = 1;
626
+ return;
627
+ }
628
+
629
+ if (sub === 'logs') {
630
+ const query = {};
631
+ if (options.ownerId) query.owner_id = options.ownerId;
632
+ if (options.userId) query.user_id = options.userId;
633
+ if (options.limit) query.limit = options.limit;
634
+ if (options.offset) query.offset = options.offset;
635
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/email/logs', query });
636
+ emit({ intent: 'email.logs', result, isJson });
637
+ if (!result.ok) process.exitCode = 1;
638
+ return;
639
+ }
640
+
641
+ if (sub === 'list-templates') {
642
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/email/templates' });
643
+ emit({ intent: 'email.list-templates', result, isJson });
644
+ if (!result.ok) process.exitCode = 1;
645
+ return;
646
+ }
647
+
648
+ if (sub === 'create-template') {
649
+ if (!requireOption(options.templateKey, 'email create-template: --template-key is required')) return;
650
+ if (!requireOption(options.name, 'email create-template: --name is required')) return;
651
+ if (!requireOption(options.subject, 'email create-template: --subject is required')) return;
652
+ if (!requireOption(options.htmlBody, 'email create-template: --html-body is required')) return;
653
+
654
+ const variables = parseJsonFlag(options.variables, 'email create-template: --variables');
655
+ if (!variables.ok) return;
656
+ const sampleContext = parseJsonFlag(options.sampleContext, 'email create-template: --sample-context');
657
+ if (!sampleContext.ok) return;
658
+ const isActive = parseBooleanFlag(options.isActive, 'email create-template: --is-active');
659
+ if (!isActive.ok) return;
660
+
661
+ const body = {
662
+ template_key: options.templateKey,
663
+ name: options.name,
664
+ subject: options.subject,
665
+ html_body: options.htmlBody,
666
+ };
667
+ assignIfDefined(body, 'description', options.description);
668
+ assignIfDefined(body, 'text_body', options.textBody);
669
+ assignIfDefined(body, 'from_email', options.fromEmail);
670
+ assignIfDefined(body, 'from_name', options.fromName);
671
+ assignIfDefined(body, 'reply_to', options.replyTo);
672
+ assignIfDefined(body, 'variables', variables.value);
673
+ assignIfDefined(body, 'sample_context', sampleContext.value);
674
+ assignIfDefined(body, 'is_active', isActive.value);
675
+ assignIfDefined(body, 'category', options.category);
676
+
677
+ const result = await callEndpoint({ options, method: 'POST', path: '/api/email/templates', body });
678
+ emit({ intent: 'email.create-template', result, isJson });
679
+ if (!result.ok) process.exitCode = 1;
680
+ return;
681
+ }
682
+
683
+ if (sub === 'update-template') {
684
+ if (!requireOption(options.templateKey, 'email update-template: --template-key is required')) return;
685
+
686
+ const variables = parseJsonFlag(options.variables, 'email update-template: --variables');
687
+ if (!variables.ok) return;
688
+ const sampleContext = parseJsonFlag(options.sampleContext, 'email update-template: --sample-context');
689
+ if (!sampleContext.ok) return;
690
+ const isActive = parseBooleanFlag(options.isActive, 'email update-template: --is-active');
691
+ if (!isActive.ok) return;
692
+
693
+ const body = {};
694
+ assignIfDefined(body, 'name', options.name);
695
+ assignIfDefined(body, 'description', options.description);
696
+ assignIfDefined(body, 'subject', options.subject);
697
+ assignIfDefined(body, 'html_body', options.htmlBody);
698
+ assignIfDefined(body, 'text_body', options.textBody);
699
+ assignIfDefined(body, 'from_email', options.fromEmail);
700
+ assignIfDefined(body, 'from_name', options.fromName);
701
+ assignIfDefined(body, 'reply_to', options.replyTo);
702
+ assignIfDefined(body, 'variables', variables.value);
703
+ assignIfDefined(body, 'sample_context', sampleContext.value);
704
+ assignIfDefined(body, 'is_active', isActive.value);
705
+ assignIfDefined(body, 'category', options.category);
706
+
707
+ if (Object.keys(body).length === 0) {
708
+ console.error('email update-template: provide at least one field to update');
709
+ process.exitCode = 2;
710
+ return;
711
+ }
712
+
713
+ const result = await callEndpoint({
714
+ options,
715
+ method: 'PUT',
716
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}`,
717
+ body,
718
+ });
719
+ emit({ intent: 'email.update-template', result, isJson });
720
+ if (!result.ok) process.exitCode = 1;
721
+ return;
722
+ }
723
+
724
+ if (sub === 'get-override') {
725
+ if (!requireOption(options.templateKey, 'email get-override: --template-key is required')) return;
726
+ const result = await callEndpoint({
727
+ options,
728
+ method: 'GET',
729
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
730
+ });
731
+ emit({ intent: 'email.get-override', result, isJson });
732
+ if (!result.ok) process.exitCode = 1;
733
+ return;
734
+ }
735
+
736
+ if (sub === 'set-override') {
737
+ if (!requireOption(options.templateKey, 'email set-override: --template-key is required')) return;
738
+ const body = {};
739
+ assignIfDefined(body, 'subject_override', options.subjectOverride);
740
+ assignIfDefined(body, 'body_override', options.bodyOverride);
741
+ if (Object.keys(body).length === 0) {
742
+ console.error('email set-override: provide --subject-override and/or --body-override');
743
+ process.exitCode = 2;
744
+ return;
745
+ }
746
+ const result = await callEndpoint({
747
+ options,
748
+ method: 'PUT',
749
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
750
+ body,
751
+ });
752
+ emit({ intent: 'email.set-override', result, isJson });
753
+ if (!result.ok) process.exitCode = 1;
754
+ return;
755
+ }
756
+
757
+ if (sub === 'clear-override') {
758
+ if (!requireOption(options.templateKey, 'email clear-override: --template-key is required')) return;
759
+ const result = await callEndpoint({
760
+ options,
761
+ method: 'DELETE',
762
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
763
+ });
764
+ emit({ intent: 'email.clear-override', result, isJson });
765
+ if (!result.ok) process.exitCode = 1;
766
+ return;
767
+ }
768
+
769
+ console.error(
770
+ 'email: unknown subcommand. Supported: send, get-template, preview, logs, list-templates, create-template, update-template, get-override, set-override, clear-override'
771
+ );
772
+ process.exitCode = 2;
773
+ } catch (err) {
774
+ emitAuthError(`email.${sub || 'unknown'}`, err, isJson);
775
+ process.exitCode = 1;
776
+ }
777
+ }
778
+
779
+ async function policyHandler({ options }) {
780
+ const isJson = Boolean(options.json);
781
+ const sub = options.subcommand;
782
+ try {
783
+ if (sub === 'list') {
784
+ const query = {};
785
+ if (options.isActive !== null && options.isActive !== undefined) query.is_active = options.isActive;
786
+ if (options.limit) query.limit = options.limit;
787
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/policies', query });
788
+ emit({ intent: 'policy.list', result, isJson });
789
+ if (!result.ok) process.exitCode = 1;
790
+ return;
791
+ }
792
+ if (sub === 'create') {
793
+ if (!options.name || !options.effect) {
794
+ console.error('policy create: --name and --effect are required');
795
+ process.exitCode = 2;
796
+ return;
797
+ }
798
+ let conditions = {};
799
+ if (options.conditions) {
800
+ try { conditions = JSON.parse(options.conditions); }
801
+ catch { console.error('policy create: --conditions must be valid JSON'); process.exitCode = 2; return; }
802
+ }
803
+ const body = {
804
+ name: options.name,
805
+ effect: options.effect,
806
+ conditions,
807
+ description: options.description || null,
808
+ priority: options.priority || 0,
809
+ };
810
+ const result = await callEndpoint({ options, method: 'POST', path: '/api/policies', body });
811
+ emit({ intent: 'policy.create', result, isJson });
812
+ if (!result.ok) process.exitCode = 1;
813
+ return;
814
+ }
815
+ if (sub === 'delete') {
816
+ if (!options.id) {
817
+ console.error('policy delete: --id is required');
818
+ process.exitCode = 2;
819
+ return;
820
+ }
821
+ const result = await callEndpoint({ options, method: 'DELETE', path: `/api/policies/${encodeURIComponent(options.id)}` });
822
+ emit({ intent: 'policy.delete', result, isJson });
823
+ if (!result.ok) process.exitCode = 1;
824
+ return;
825
+ }
826
+ console.error('policy: unknown subcommand. Supported: list, create, delete');
827
+ process.exitCode = 2;
828
+ } catch (err) {
829
+ emitAuthError(`policy.${sub || 'unknown'}`, err, isJson);
830
+ process.exitCode = 1;
831
+ }
832
+ }
833
+
834
+ async function webhookHandler({ options }) {
835
+ const isJson = Boolean(options.json);
836
+ const sub = options.subcommand;
837
+ try {
838
+ if (sub === 'list') {
839
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/webhooks' });
840
+ emit({ intent: 'webhook.list', result, isJson });
841
+ if (!result.ok) process.exitCode = 1;
842
+ return;
843
+ }
844
+ if (sub === 'register') {
845
+ if (!options.url || !options.events) {
846
+ console.error('webhook register: --url and --events (comma-separated) are required');
847
+ process.exitCode = 2;
848
+ return;
849
+ }
850
+ const events = String(options.events).split(',').map((e) => e.trim()).filter(Boolean);
851
+ const body = { url: options.url, events };
852
+ const result = await callEndpoint({ options, method: 'POST', path: '/api/webhooks', body });
853
+ emit({ intent: 'webhook.register', result, isJson });
854
+ if (!result.ok) process.exitCode = 1;
855
+ return;
856
+ }
857
+ console.error('webhook: unknown subcommand. Supported: list, register');
858
+ process.exitCode = 2;
859
+ } catch (err) {
860
+ emitAuthError(`webhook.${sub || 'unknown'}`, err, isJson);
861
+ process.exitCode = 1;
862
+ }
863
+ }
864
+
865
+ async function issueReportsHandler({ options }) {
866
+ const isJson = Boolean(options.json);
867
+ const sub = options.subcommand;
868
+ if (sub !== 'list-mine') {
869
+ console.error('issue-reports: unknown subcommand. Supported: list-mine');
870
+ process.exitCode = 2;
871
+ return;
872
+ }
873
+ try {
874
+ const query = {};
875
+ if (options.status) query.status = options.status;
876
+ if (options.limit) query.limit = options.limit;
877
+ if (options.offset) query.offset = options.offset;
878
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/v1/issue-reports', query });
879
+ emit({ intent: 'issue-reports.list-mine', result, isJson });
880
+ if (!result.ok) process.exitCode = 1;
881
+ } catch (err) {
882
+ emitAuthError('issue-reports.list-mine', err, isJson);
883
+ process.exitCode = 1;
884
+ }
885
+ }
886
+
420
887
  module.exports = { createHandlers };