spaps 0.7.6 → 0.7.8

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
@@ -2,24 +2,30 @@ const chalk = require('chalk');
2
2
  const fs = require('fs');
3
3
  const { DEFAULT_PORT } = require('./config');
4
4
  const { handleError } = require('./error-handler');
5
- const { showInteractiveHelp, showQuickHelp } = require('./help-system');
6
- const { showInteractiveDocs, showQuickReference, searchDocs } = require('./docs-system');
7
- const { getQuickStartInstructions, getServerStatus, runQuickTest } = require('./ai-helper');
8
- const { buildToolSpec } = require('./ai-tool-spec');
9
- const { runDoctor } = require('./doctor');
10
- const {
11
- applyFixtures,
12
- exportStorageState,
13
- initFixtureKernel,
14
- resetFixtures,
15
- } = require('./fixture-kernel');
16
- const { createProjectStarter } = require('./project-scaffolder');
17
- const {
18
- loginHandler,
19
- logoutHandler,
20
- whoamiHandler,
21
- tokenHandler,
22
- } = require('./auth/handlers');
5
+ const { hasInteractiveTerminal } = require('./auth/env');
6
+ const { showQuickHelp } = require('./help-quick');
7
+ const { showQuickReference } = require('./docs-quick');
8
+
9
+ function lazy(factory) {
10
+ let value;
11
+ return () => {
12
+ if (value === undefined) {
13
+ value = factory();
14
+ }
15
+ return value;
16
+ };
17
+ }
18
+
19
+ const loadHelpSystem = lazy(() => require('./help-system'));
20
+ const loadDocsSystem = lazy(() => require('./docs-system'));
21
+ const loadAiHelper = lazy(() => require('./ai-helper'));
22
+ const loadAiToolSpec = lazy(() => require('./ai-tool-spec'));
23
+ const loadDoctor = lazy(() => require('./doctor'));
24
+ const loadHomeView = lazy(() => require('./home-view'));
25
+ const loadFixtureKernel = lazy(() => require('./fixture-kernel'));
26
+ const loadProjectScaffolder = lazy(() => require('./project-scaffolder'));
27
+ const loadAuthCommandHandlers = lazy(() => require('./auth/handlers'));
28
+ const loadDomainCli = lazy(() => require('./domain-cli'));
23
29
 
24
30
  function createHandlers(version, logo) {
25
31
  function invalidArgument(message) {
@@ -28,7 +34,62 @@ function createHandlers(version, logo) {
28
34
  return error;
29
35
  }
30
36
 
37
+ function emitInteractiveFallback(message) {
38
+ console.log(
39
+ chalk.yellow(`\n⚠️ ${message}. Showing the quick reference instead.\n`)
40
+ );
41
+ }
42
+
43
+ const verifyHandler = async ({ options }) => {
44
+ const { runQuickTest } = loadAiHelper();
45
+ const result = await runQuickTest({
46
+ port: options.port,
47
+ serverUrl: options.serverUrl,
48
+ cwd: process.cwd(),
49
+ });
50
+
51
+ if (options.json) {
52
+ console.log(JSON.stringify(result, null, 2));
53
+ return;
54
+ }
55
+
56
+ console.log(chalk.yellow('\n🍠 SPAPS Verify\n'));
57
+ console.log(result.success ? chalk.green(result.summary) : chalk.yellow(result.summary));
58
+ console.log();
59
+ (result.results || []).forEach((entry) => {
60
+ const mark = entry.success ? chalk.green('✓') : chalk.red('✗');
61
+ const message = entry.message ? `: ${entry.message}` : '';
62
+ console.log(` ${mark} ${entry.test}${message}`);
63
+ if (!entry.success && entry.fix) {
64
+ console.log(chalk.gray(` fix: ${entry.fix}`));
65
+ }
66
+ });
67
+ if (Array.isArray(result.next_steps) && result.next_steps.length > 0) {
68
+ console.log();
69
+ console.log(chalk.bold('Next'));
70
+ result.next_steps.forEach((step) => {
71
+ console.log(chalk.cyan(` ${step}`));
72
+ });
73
+ }
74
+ console.log();
75
+ };
76
+
31
77
  return {
78
+ home: async ({ options }) => {
79
+ const { buildHomeView, renderHomeView } = loadHomeView();
80
+ const view = await buildHomeView({
81
+ port: options.port,
82
+ serverUrl: options.serverUrl,
83
+ cwd: process.cwd(),
84
+ });
85
+
86
+ if (options.json) {
87
+ console.log(JSON.stringify(view, null, 2));
88
+ return;
89
+ }
90
+
91
+ renderHomeView(view, { logo });
92
+ },
32
93
  local: async ({ options }) => {
33
94
  const isJson = options.json;
34
95
  if (!isJson) console.log(logo);
@@ -86,6 +147,7 @@ function createHandlers(version, logo) {
86
147
  }
87
148
  },
88
149
  quickstart: async ({ options }) => {
150
+ const { getQuickStartInstructions } = loadAiHelper();
89
151
  const instructions = await getQuickStartInstructions(options.port);
90
152
  if (options.json) {
91
153
  console.log(JSON.stringify(instructions, null, 2));
@@ -104,6 +166,7 @@ function createHandlers(version, logo) {
104
166
  }
105
167
  },
106
168
  status: async ({ options }) => {
169
+ const { getServerStatus } = loadAiHelper();
107
170
  const status = await getServerStatus(options.port);
108
171
  if (options.json) {
109
172
  console.log(JSON.stringify(status));
@@ -127,10 +190,7 @@ function createHandlers(version, logo) {
127
190
  }
128
191
  }
129
192
  },
130
- test: async ({ options }) => {
131
- const result = await runQuickTest(options.port);
132
- console.log(JSON.stringify(result, null, 2));
133
- },
193
+ verify: verifyHandler,
134
194
  init: async ({ options }) => {
135
195
  const isJson = options.json;
136
196
  const envContent = `# SPAPS Local Development\nSPAPS_API_URL=http://localhost:${DEFAULT_PORT}\n# SPAPS_API_KEY=your-api-key-here\n`;
@@ -158,6 +218,7 @@ function createHandlers(version, logo) {
158
218
  },
159
219
  create: async ({ options }) => {
160
220
  const isJson = options.json;
221
+ const { createProjectStarter } = loadProjectScaffolder();
161
222
 
162
223
  try {
163
224
  const result = await createProjectStarter({
@@ -223,7 +284,12 @@ function createHandlers(version, logo) {
223
284
  },
224
285
  help: async ({ options }) => {
225
286
  if (options.interactive) {
226
- await showInteractiveHelp();
287
+ if (!hasInteractiveTerminal()) {
288
+ emitInteractiveFallback('Interactive help requires a TTY');
289
+ showQuickHelp();
290
+ return;
291
+ }
292
+ await loadHelpSystem().showInteractiveHelp();
227
293
  } else if (options.quick) {
228
294
  showQuickHelp();
229
295
  } else {
@@ -232,7 +298,7 @@ function createHandlers(version, logo) {
232
298
  },
233
299
  docs: async ({ options }) => {
234
300
  if (options.search) {
235
- const results = searchDocs(options.search);
301
+ const results = loadDocsSystem().searchDocs(options.search);
236
302
  if (options.json) {
237
303
  console.log(JSON.stringify({ results }, null, 2));
238
304
  } else {
@@ -250,12 +316,18 @@ function createHandlers(version, logo) {
250
316
  console.log(chalk.blue(' to browse full documentation\n'));
251
317
  }
252
318
  } else if (options.interactive) {
253
- await showInteractiveDocs();
319
+ if (!hasInteractiveTerminal()) {
320
+ emitInteractiveFallback('Interactive docs require a TTY');
321
+ showQuickReference();
322
+ return;
323
+ }
324
+ await loadDocsSystem().showInteractiveDocs();
254
325
  } else {
255
326
  showQuickReference();
256
327
  }
257
328
  },
258
329
  tools: async ({ options }) => {
330
+ const { buildToolSpec } = loadAiToolSpec();
259
331
  const spec = await buildToolSpec({
260
332
  format: options.format || 'openai',
261
333
  port: options.port,
@@ -282,6 +354,12 @@ function createHandlers(version, logo) {
282
354
  },
283
355
  fixtures: async ({ options }) => {
284
356
  const isJson = options.json;
357
+ const {
358
+ applyFixtures,
359
+ exportStorageState,
360
+ initFixtureKernel,
361
+ resetFixtures,
362
+ } = loadFixtureKernel();
285
363
 
286
364
  try {
287
365
  if (options.format && options.format !== 'playwright') {
@@ -305,6 +383,8 @@ function createHandlers(version, logo) {
305
383
  port: options.port,
306
384
  baseUrl: options.baseUrl,
307
385
  version,
386
+ persona: options.persona,
387
+ seed: options.seed,
308
388
  });
309
389
  break;
310
390
  case 'reset':
@@ -313,6 +393,7 @@ function createHandlers(version, logo) {
313
393
  port: options.port,
314
394
  baseUrl: options.baseUrl,
315
395
  version,
396
+ seed: options.seed,
316
397
  });
317
398
  break;
318
399
  case 'storage-state':
@@ -325,6 +406,7 @@ function createHandlers(version, logo) {
325
406
  baseUrl: options.baseUrl,
326
407
  version,
327
408
  persona: options.persona,
409
+ seed: options.seed,
328
410
  });
329
411
  break;
330
412
  default:
@@ -362,6 +444,13 @@ function createHandlers(version, logo) {
362
444
  });
363
445
  }
364
446
 
447
+ if (Array.isArray(result.seeding?.personas) && result.seeding.personas.length > 0) {
448
+ console.log(chalk.green('\nSeeded personas:'));
449
+ result.seeding.personas.forEach((entry) => {
450
+ console.log(chalk.gray(` • ${entry.persona} (${entry.request_count} requests)`));
451
+ });
452
+ }
453
+
365
454
  if (result.generated?.personas?.length) {
366
455
  console.log(chalk.green('\nGenerated personas:'));
367
456
  result.generated.personas.forEach((entry) => {
@@ -408,13 +497,430 @@ function createHandlers(version, logo) {
408
497
  }
409
498
  },
410
499
  doctor: async ({ options }) => {
500
+ const { runDoctor } = loadDoctor();
411
501
  await runDoctor({ port: options.port || DEFAULT_PORT, stripe: options.stripe || null, json: options.json });
412
502
  },
413
- login: loginHandler,
414
- logout: logoutHandler,
415
- whoami: whoamiHandler,
416
- token: tokenHandler,
503
+ test: verifyHandler,
504
+ login: async (...args) => loadAuthCommandHandlers().loginHandler(...args),
505
+ logout: async (...args) => loadAuthCommandHandlers().logoutHandler(...args),
506
+ whoami: async (...args) => loadAuthCommandHandlers().whoamiHandler(...args),
507
+ token: async (...args) => loadAuthCommandHandlers().tokenHandler(...args),
508
+ dayrate: dayrateHandler,
509
+ email: emailHandler,
510
+ policy: policyHandler,
511
+ webhook: webhookHandler,
512
+ 'issue-reports': issueReportsHandler,
417
513
  };
418
514
  }
419
515
 
516
+ function emitText({ intent, result, isJson, successMessage = null }) {
517
+ const { emit } = loadDomainCli();
518
+ if (isJson) {
519
+ emit({ intent, result, isJson, successMessage });
520
+ return;
521
+ }
522
+ if (!result.ok) {
523
+ emit({ intent, result, isJson, successMessage });
524
+ return;
525
+ }
526
+ if (successMessage) {
527
+ console.log(successMessage);
528
+ }
529
+ const output = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
530
+ process.stdout.write(output);
531
+ if (!output.endsWith('\n')) process.stdout.write('\n');
532
+ }
533
+
534
+ function parseJsonFlag(raw, label) {
535
+ if (raw === null || raw === undefined) {
536
+ return { ok: true, value: null };
537
+ }
538
+ try {
539
+ return { ok: true, value: JSON.parse(raw) };
540
+ } catch {
541
+ console.error(`${label}: must be valid JSON`);
542
+ process.exitCode = 2;
543
+ return { ok: false, value: null };
544
+ }
545
+ }
546
+
547
+ function parseBooleanFlag(raw, label) {
548
+ if (raw === null || raw === undefined) {
549
+ return { ok: true, value: null };
550
+ }
551
+
552
+ if (typeof raw === 'boolean') {
553
+ return { ok: true, value: raw };
554
+ }
555
+
556
+ const normalized = String(raw).trim().toLowerCase();
557
+ if (['true', '1', 'yes', 'y'].includes(normalized)) {
558
+ return { ok: true, value: true };
559
+ }
560
+ if (['false', '0', 'no', 'n'].includes(normalized)) {
561
+ return { ok: true, value: false };
562
+ }
563
+
564
+ console.error(`${label}: must be one of true|false|1|0|yes|no`);
565
+ process.exitCode = 2;
566
+ return { ok: false, value: null };
567
+ }
568
+
569
+ function requireOption(value, message) {
570
+ if (value !== null && value !== undefined && value !== '') {
571
+ return true;
572
+ }
573
+ console.error(message);
574
+ process.exitCode = 2;
575
+ return false;
576
+ }
577
+
578
+ function assignIfDefined(target, key, value) {
579
+ if (value !== null && value !== undefined) {
580
+ target[key] = value;
581
+ }
582
+ }
583
+
584
+ async function dayrateHandler({ options }) {
585
+ const { callEndpoint, emit, emitAuthError } = loadDomainCli();
586
+ const isJson = Boolean(options.json);
587
+ const sub = options.subcommand;
588
+ if (sub !== 'config') {
589
+ console.error('dayrate: unknown subcommand. Supported: config');
590
+ process.exitCode = 2;
591
+ return;
592
+ }
593
+ try {
594
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/dayrate/admin/config' });
595
+ emit({ intent: 'dayrate.config', result, isJson });
596
+ if (!result.ok) process.exitCode = 1;
597
+ } catch (err) {
598
+ emitAuthError('dayrate.config', err, isJson);
599
+ process.exitCode = 1;
600
+ }
601
+ }
602
+
603
+ async function emailHandler({ options }) {
604
+ const { callEndpoint, emit, emitAuthError } = loadDomainCli();
605
+ const isJson = Boolean(options.json);
606
+ const sub = options.subcommand;
607
+
608
+ try {
609
+ if (sub === 'send') {
610
+ if (!requireOption(options.templateKey, 'email send: --template-key is required')) return;
611
+ if (!requireOption(options.to, 'email send: --to is required')) return;
612
+
613
+ const context = parseJsonFlag(options.context, 'email send: --context');
614
+ if (!context.ok) return;
615
+
616
+ const body = {
617
+ template_key: options.templateKey,
618
+ to: options.to,
619
+ };
620
+ assignIfDefined(body, 'context', context.value);
621
+ assignIfDefined(body, 'user_id', options.userId);
622
+ assignIfDefined(body, 'owner_id', options.ownerId);
623
+ assignIfDefined(body, 'subject_override', options.subjectOverride);
624
+ assignIfDefined(body, 'body_override', options.bodyOverride);
625
+
626
+ const result = await callEndpoint({ options, method: 'POST', path: '/api/email/send', body });
627
+ emit({ intent: 'email.send', result, isJson });
628
+ if (!result.ok) process.exitCode = 1;
629
+ return;
630
+ }
631
+
632
+ if (sub === 'get-template') {
633
+ if (!requireOption(options.templateKey, 'email get-template: --template-key is required')) return;
634
+ const result = await callEndpoint({
635
+ options,
636
+ method: 'GET',
637
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}`,
638
+ });
639
+ emit({ intent: 'email.get-template', result, isJson });
640
+ if (!result.ok) process.exitCode = 1;
641
+ return;
642
+ }
643
+
644
+ if (sub === 'preview') {
645
+ if (!requireOption(options.templateKey, 'email preview: --template-key is required')) return;
646
+
647
+ const context = parseJsonFlag(options.context, 'email preview: --context');
648
+ if (!context.ok) return;
649
+
650
+ const path = `/api/email/templates/${encodeURIComponent(options.templateKey)}/preview`;
651
+ const result = context.value === null
652
+ ? await callEndpoint({ options, method: 'GET', path })
653
+ : await callEndpoint({
654
+ options,
655
+ method: 'POST',
656
+ path,
657
+ body: { context: context.value },
658
+ });
659
+
660
+ emitText({ intent: 'email.preview', result, isJson });
661
+ if (!result.ok) process.exitCode = 1;
662
+ return;
663
+ }
664
+
665
+ if (sub === 'logs') {
666
+ const query = {};
667
+ if (options.ownerId) query.owner_id = options.ownerId;
668
+ if (options.userId) query.user_id = options.userId;
669
+ if (options.limit) query.limit = options.limit;
670
+ if (options.offset) query.offset = options.offset;
671
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/email/logs', query });
672
+ emit({ intent: 'email.logs', result, isJson });
673
+ if (!result.ok) process.exitCode = 1;
674
+ return;
675
+ }
676
+
677
+ if (sub === 'list-templates') {
678
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/email/templates' });
679
+ emit({ intent: 'email.list-templates', result, isJson });
680
+ if (!result.ok) process.exitCode = 1;
681
+ return;
682
+ }
683
+
684
+ if (sub === 'create-template') {
685
+ if (!requireOption(options.templateKey, 'email create-template: --template-key is required')) return;
686
+ if (!requireOption(options.name, 'email create-template: --name is required')) return;
687
+ if (!requireOption(options.subject, 'email create-template: --subject is required')) return;
688
+ if (!requireOption(options.htmlBody, 'email create-template: --html-body is required')) return;
689
+
690
+ const variables = parseJsonFlag(options.variables, 'email create-template: --variables');
691
+ if (!variables.ok) return;
692
+ const sampleContext = parseJsonFlag(options.sampleContext, 'email create-template: --sample-context');
693
+ if (!sampleContext.ok) return;
694
+ const isActive = parseBooleanFlag(options.isActive, 'email create-template: --is-active');
695
+ if (!isActive.ok) return;
696
+
697
+ const body = {
698
+ template_key: options.templateKey,
699
+ name: options.name,
700
+ subject: options.subject,
701
+ html_body: options.htmlBody,
702
+ };
703
+ assignIfDefined(body, 'description', options.description);
704
+ assignIfDefined(body, 'text_body', options.textBody);
705
+ assignIfDefined(body, 'from_email', options.fromEmail);
706
+ assignIfDefined(body, 'from_name', options.fromName);
707
+ assignIfDefined(body, 'reply_to', options.replyTo);
708
+ assignIfDefined(body, 'variables', variables.value);
709
+ assignIfDefined(body, 'sample_context', sampleContext.value);
710
+ assignIfDefined(body, 'is_active', isActive.value);
711
+ assignIfDefined(body, 'category', options.category);
712
+
713
+ const result = await callEndpoint({ options, method: 'POST', path: '/api/email/templates', body });
714
+ emit({ intent: 'email.create-template', result, isJson });
715
+ if (!result.ok) process.exitCode = 1;
716
+ return;
717
+ }
718
+
719
+ if (sub === 'update-template') {
720
+ if (!requireOption(options.templateKey, 'email update-template: --template-key is required')) return;
721
+
722
+ const variables = parseJsonFlag(options.variables, 'email update-template: --variables');
723
+ if (!variables.ok) return;
724
+ const sampleContext = parseJsonFlag(options.sampleContext, 'email update-template: --sample-context');
725
+ if (!sampleContext.ok) return;
726
+ const isActive = parseBooleanFlag(options.isActive, 'email update-template: --is-active');
727
+ if (!isActive.ok) return;
728
+
729
+ const body = {};
730
+ assignIfDefined(body, 'name', options.name);
731
+ assignIfDefined(body, 'description', options.description);
732
+ assignIfDefined(body, 'subject', options.subject);
733
+ assignIfDefined(body, 'html_body', options.htmlBody);
734
+ assignIfDefined(body, 'text_body', options.textBody);
735
+ assignIfDefined(body, 'from_email', options.fromEmail);
736
+ assignIfDefined(body, 'from_name', options.fromName);
737
+ assignIfDefined(body, 'reply_to', options.replyTo);
738
+ assignIfDefined(body, 'variables', variables.value);
739
+ assignIfDefined(body, 'sample_context', sampleContext.value);
740
+ assignIfDefined(body, 'is_active', isActive.value);
741
+ assignIfDefined(body, 'category', options.category);
742
+
743
+ if (Object.keys(body).length === 0) {
744
+ console.error('email update-template: provide at least one field to update');
745
+ process.exitCode = 2;
746
+ return;
747
+ }
748
+
749
+ const result = await callEndpoint({
750
+ options,
751
+ method: 'PUT',
752
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}`,
753
+ body,
754
+ });
755
+ emit({ intent: 'email.update-template', result, isJson });
756
+ if (!result.ok) process.exitCode = 1;
757
+ return;
758
+ }
759
+
760
+ if (sub === 'get-override') {
761
+ if (!requireOption(options.templateKey, 'email get-override: --template-key is required')) return;
762
+ const result = await callEndpoint({
763
+ options,
764
+ method: 'GET',
765
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
766
+ });
767
+ emit({ intent: 'email.get-override', result, isJson });
768
+ if (!result.ok) process.exitCode = 1;
769
+ return;
770
+ }
771
+
772
+ if (sub === 'set-override') {
773
+ if (!requireOption(options.templateKey, 'email set-override: --template-key is required')) return;
774
+ const body = {};
775
+ assignIfDefined(body, 'subject_override', options.subjectOverride);
776
+ assignIfDefined(body, 'body_override', options.bodyOverride);
777
+ if (Object.keys(body).length === 0) {
778
+ console.error('email set-override: provide --subject-override and/or --body-override');
779
+ process.exitCode = 2;
780
+ return;
781
+ }
782
+ const result = await callEndpoint({
783
+ options,
784
+ method: 'PUT',
785
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
786
+ body,
787
+ });
788
+ emit({ intent: 'email.set-override', result, isJson });
789
+ if (!result.ok) process.exitCode = 1;
790
+ return;
791
+ }
792
+
793
+ if (sub === 'clear-override') {
794
+ if (!requireOption(options.templateKey, 'email clear-override: --template-key is required')) return;
795
+ const result = await callEndpoint({
796
+ options,
797
+ method: 'DELETE',
798
+ path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
799
+ });
800
+ emit({ intent: 'email.clear-override', result, isJson });
801
+ if (!result.ok) process.exitCode = 1;
802
+ return;
803
+ }
804
+
805
+ console.error(
806
+ 'email: unknown subcommand. Supported: send, get-template, preview, logs, list-templates, create-template, update-template, get-override, set-override, clear-override'
807
+ );
808
+ process.exitCode = 2;
809
+ } catch (err) {
810
+ emitAuthError(`email.${sub || 'unknown'}`, err, isJson);
811
+ process.exitCode = 1;
812
+ }
813
+ }
814
+
815
+ async function policyHandler({ options }) {
816
+ const { callEndpoint, emit, emitAuthError } = loadDomainCli();
817
+ const isJson = Boolean(options.json);
818
+ const sub = options.subcommand;
819
+ try {
820
+ if (sub === 'list') {
821
+ const query = {};
822
+ if (options.isActive !== null && options.isActive !== undefined) query.is_active = options.isActive;
823
+ if (options.limit) query.limit = options.limit;
824
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/policies', query });
825
+ emit({ intent: 'policy.list', result, isJson });
826
+ if (!result.ok) process.exitCode = 1;
827
+ return;
828
+ }
829
+ if (sub === 'create') {
830
+ if (!options.name || !options.effect) {
831
+ console.error('policy create: --name and --effect are required');
832
+ process.exitCode = 2;
833
+ return;
834
+ }
835
+ let conditions = {};
836
+ if (options.conditions) {
837
+ try { conditions = JSON.parse(options.conditions); }
838
+ catch { console.error('policy create: --conditions must be valid JSON'); process.exitCode = 2; return; }
839
+ }
840
+ const body = {
841
+ name: options.name,
842
+ effect: options.effect,
843
+ conditions,
844
+ description: options.description || null,
845
+ priority: options.priority || 0,
846
+ };
847
+ const result = await callEndpoint({ options, method: 'POST', path: '/api/policies', body });
848
+ emit({ intent: 'policy.create', result, isJson });
849
+ if (!result.ok) process.exitCode = 1;
850
+ return;
851
+ }
852
+ if (sub === 'delete') {
853
+ if (!options.id) {
854
+ console.error('policy delete: --id is required');
855
+ process.exitCode = 2;
856
+ return;
857
+ }
858
+ const result = await callEndpoint({ options, method: 'DELETE', path: `/api/policies/${encodeURIComponent(options.id)}` });
859
+ emit({ intent: 'policy.delete', result, isJson });
860
+ if (!result.ok) process.exitCode = 1;
861
+ return;
862
+ }
863
+ console.error('policy: unknown subcommand. Supported: list, create, delete');
864
+ process.exitCode = 2;
865
+ } catch (err) {
866
+ emitAuthError(`policy.${sub || 'unknown'}`, err, isJson);
867
+ process.exitCode = 1;
868
+ }
869
+ }
870
+
871
+ async function webhookHandler({ options }) {
872
+ const { callEndpoint, emit, emitAuthError } = loadDomainCli();
873
+ const isJson = Boolean(options.json);
874
+ const sub = options.subcommand;
875
+ try {
876
+ if (sub === 'list') {
877
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/webhooks' });
878
+ emit({ intent: 'webhook.list', result, isJson });
879
+ if (!result.ok) process.exitCode = 1;
880
+ return;
881
+ }
882
+ if (sub === 'register') {
883
+ if (!options.url || !options.events) {
884
+ console.error('webhook register: --url and --events (comma-separated) are required');
885
+ process.exitCode = 2;
886
+ return;
887
+ }
888
+ const events = String(options.events).split(',').map((e) => e.trim()).filter(Boolean);
889
+ const body = { url: options.url, events };
890
+ const result = await callEndpoint({ options, method: 'POST', path: '/api/webhooks', body });
891
+ emit({ intent: 'webhook.register', result, isJson });
892
+ if (!result.ok) process.exitCode = 1;
893
+ return;
894
+ }
895
+ console.error('webhook: unknown subcommand. Supported: list, register');
896
+ process.exitCode = 2;
897
+ } catch (err) {
898
+ emitAuthError(`webhook.${sub || 'unknown'}`, err, isJson);
899
+ process.exitCode = 1;
900
+ }
901
+ }
902
+
903
+ async function issueReportsHandler({ options }) {
904
+ const { callEndpoint, emit, emitAuthError } = loadDomainCli();
905
+ const isJson = Boolean(options.json);
906
+ const sub = options.subcommand;
907
+ if (sub !== 'list-mine') {
908
+ console.error('issue-reports: unknown subcommand. Supported: list-mine');
909
+ process.exitCode = 2;
910
+ return;
911
+ }
912
+ try {
913
+ const query = {};
914
+ if (options.status) query.status = options.status;
915
+ if (options.limit) query.limit = options.limit;
916
+ if (options.offset) query.offset = options.offset;
917
+ const result = await callEndpoint({ options, method: 'GET', path: '/api/v1/issue-reports', query });
918
+ emit({ intent: 'issue-reports.list-mine', result, isJson });
919
+ if (!result.ok) process.exitCode = 1;
920
+ } catch (err) {
921
+ emitAuthError('issue-reports.list-mine', err, isJson);
922
+ process.exitCode = 1;
923
+ }
924
+ }
925
+
420
926
  module.exports = { createHandlers };