@goliapkg/sentori-cli 0.5.2 → 0.6.0

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/index.ts CHANGED
@@ -2,8 +2,18 @@
2
2
  import { parseArgs } from 'node:util'
3
3
 
4
4
  import { formatIssueLine, issueList, issuePatch } from './issue.js'
5
+ import { runMcpServer } from './mcp.js'
5
6
  import { uploadDsym, uploadMapping } from './native-artifacts.js'
7
+ import {
8
+ parseJsonArg,
9
+ pushCredsDelete,
10
+ pushCredsList,
11
+ pushCredsSet,
12
+ pushReceipt,
13
+ pushSend,
14
+ } from './push.js'
6
15
  import { reactNativeUpload } from './react-native.js'
16
+ import { uploadSourceBundle } from './source-bundle.js'
7
17
  import { uploadSourcemaps } from './upload.js'
8
18
 
9
19
  const HELP = `sentori-cli — Sentori command-line interface
@@ -43,16 +53,33 @@ Native artifacts (project-scoped, need --project + admin token):
43
53
  with a "# pg_map_id:" line the server sniffs the debug-id from
44
54
  it; otherwise you can pass it explicitly.
45
55
 
56
+ sentori-cli upload source-bundle --project <uuid> --release <r> --platform ios|android [--module <label>] <archive.tar.gz>
57
+ Upload a pre-built tar.gz of your project's source so the
58
+ dashboard can render inline source for native (Swift / Kotlin /
59
+ Objective-C) frames the way it already does for JS via source
60
+ maps. Build the archive yourself:
61
+ tar -czf ios-source.tar.gz Sources/
62
+ Pass --module to upload multiple bundles per (release, platform)
63
+ — e.g. \`--module main\`, \`--module watch-ext\`. Omitting --module
64
+ reuses the v1.3 single-bundle slot (re-uploading replaces it).
65
+
46
66
  CI triage:
47
67
  sentori-cli issue list --project <uuid> [--status active|silenced|resolved|closed] [--limit N] [--error-type <t>]
48
68
  sentori-cli issue resolve <issue-uuid> --project <uuid> [--in-release <r>]
49
69
  sentori-cli issue silence <issue-uuid> --project <uuid>
50
70
 
71
+ LLM agents (MCP):
72
+ sentori-cli mcp serve --project <uuid> [--token <t>] [--api-url <url>]
73
+ Run a stdio MCP server. Connect from Claude Code / any MCP
74
+ client by pointing at \`sentori-cli mcp serve …\` as the command.
75
+ Exposes sentori_issue_list / _get / _comment / _transition /
76
+ _assign / _set_priority / _set_labels / _watch tools.
77
+
51
78
  Options (upload commands):
52
79
  --release <r> release identifier — MUST equal the value the SDK
53
80
  reports via init({ release }). Required.
54
81
  --token <t> Sentori token (or set $SENTORI_TOKEN).
55
- --api-url <url> Sentori API base (default https://api.sentori.golia.jp,
82
+ --api-url <url> Sentori API base (default https://sentori.golia.jp,
56
83
  or $SENTORI_API_URL). For a self-hosted instance, your
57
84
  host. (Accepts --ingest-url as an alias.)
58
85
  --dry-run describe what would be uploaded; don't upload.
@@ -110,7 +137,7 @@ function parseCommon(values: Record<string, unknown>): Common | null {
110
137
  (typeof values['api-url'] === 'string' ? values['api-url'] : undefined) ??
111
138
  (typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
112
139
  process.env.SENTORI_API_URL ??
113
- 'https://api.sentori.golia.jp'
140
+ 'https://sentori.golia.jp'
114
141
  return { apiUrl, dryRun, release, token: token ?? '' }
115
142
  }
116
143
 
@@ -254,7 +281,7 @@ function parseAdminCfg(values: Record<string, unknown>): AdminCfg | null {
254
281
  (typeof values['api-url'] === 'string' ? values['api-url'] : undefined) ??
255
282
  (typeof values['ingest-url'] === 'string' ? values['ingest-url'] : undefined) ??
256
283
  process.env.SENTORI_API_URL ??
257
- 'https://api.sentori.golia.jp'
284
+ 'https://sentori.golia.jp'
258
285
  return { apiUrl, projectId, token }
259
286
  }
260
287
 
@@ -468,6 +495,101 @@ async function cmdUploadMapping(argv: string[]): Promise<number> {
468
495
  }
469
496
  }
470
497
 
498
+ async function cmdUploadSourceBundle(argv: string[]): Promise<number> {
499
+ let parsed
500
+ try {
501
+ parsed = parseArgs({
502
+ allowPositionals: true,
503
+ args: argv,
504
+ options: {
505
+ 'api-url': { type: 'string' },
506
+ help: { short: 'h', type: 'boolean' },
507
+ 'ingest-url': { type: 'string' },
508
+ // v1.4 W26 — optional module label so polyrepo apps can
509
+ // upload multiple bundles per (release, platform) without
510
+ // clobbering each other (main vs watch-ext vs share-ext…).
511
+ module: { type: 'string' },
512
+ platform: { type: 'string' },
513
+ project: { type: 'string' },
514
+ release: { type: 'string' },
515
+ token: { type: 'string' },
516
+ },
517
+ })
518
+ } catch (e) {
519
+ console.error(`error: ${(e as Error).message}\n${HELP}`)
520
+ return 2
521
+ }
522
+ if (parsed.values.help) {
523
+ console.log(HELP)
524
+ return 0
525
+ }
526
+ const cfg = parseAdminCfg(parsed.values)
527
+ if (!cfg) return 2
528
+ const path = parsed.positionals[0]
529
+ if (!path) {
530
+ console.error('error: a path to a tar.gz archive is required')
531
+ return 2
532
+ }
533
+ const platform = parsed.values.platform
534
+ if (platform !== 'ios' && platform !== 'android') {
535
+ console.error('error: --platform must be ios or android')
536
+ return 2
537
+ }
538
+ const release = typeof parsed.values.release === 'string' ? parsed.values.release : undefined
539
+ if (!release) {
540
+ console.error('error: --release is required for source-bundle uploads')
541
+ return 2
542
+ }
543
+ try {
544
+ const r = await uploadSourceBundle({
545
+ apiUrl: cfg.apiUrl,
546
+ module: typeof parsed.values.module === 'string' ? parsed.values.module : undefined,
547
+ path,
548
+ platform,
549
+ projectId: cfg.projectId,
550
+ release,
551
+ token: cfg.token,
552
+ })
553
+ console.log(`uploaded ${r.kind} (${r.sizeBytes} bytes, sha256:${r.contentHash.slice(0, 12)}…)`)
554
+ return 0
555
+ } catch (e) {
556
+ console.error(`source-bundle upload failed: ${(e as Error).message}`)
557
+ return 1
558
+ }
559
+ }
560
+
561
+ async function cmdMcpServe(argv: string[]): Promise<number> {
562
+ let parsed
563
+ try {
564
+ parsed = parseArgs({
565
+ args: argv,
566
+ options: {
567
+ 'api-url': { type: 'string' },
568
+ help: { short: 'h', type: 'boolean' },
569
+ 'ingest-url': { type: 'string' },
570
+ project: { type: 'string' },
571
+ token: { type: 'string' },
572
+ },
573
+ })
574
+ } catch (e) {
575
+ console.error(`error: ${(e as Error).message}\n${HELP}`)
576
+ return 2
577
+ }
578
+ if (parsed.values.help) {
579
+ console.log(HELP)
580
+ return 0
581
+ }
582
+ const cfg = parseAdminCfg(parsed.values)
583
+ if (!cfg) return 2
584
+ try {
585
+ await runMcpServer({ apiUrl: cfg.apiUrl, projectId: cfg.projectId, token: cfg.token })
586
+ return 0
587
+ } catch (e) {
588
+ console.error(`mcp serve failed: ${(e as Error).message}`)
589
+ return 1
590
+ }
591
+ }
592
+
471
593
  async function main(argv: string[]): Promise<number> {
472
594
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
473
595
  console.log(HELP)
@@ -477,16 +599,197 @@ async function main(argv: string[]): Promise<number> {
477
599
  if (a === 'upload' && b === 'sourcemap') return cmdUploadSourcemap(rest)
478
600
  if (a === 'upload' && b === 'dsym') return cmdUploadDsym(rest)
479
601
  if (a === 'upload' && b === 'mapping') return cmdUploadMapping(rest)
602
+ if (a === 'upload' && b === 'source-bundle') return cmdUploadSourceBundle(rest)
603
+ if (a === 'mcp' && b === 'serve') return cmdMcpServe(rest)
480
604
  if (a === 'react-native' && b === 'upload') return cmdReactNativeUpload(rest)
481
605
  if (a === 'issue' && b === 'list') return cmdIssueList(rest)
482
606
  if (a === 'issue' && b === 'resolve') return cmdIssuePatch(rest, { status: 'resolved' }, 'resolved')
483
607
  if (a === 'issue' && b === 'silence') return cmdIssuePatch(rest, { status: 'silenced' }, 'silenced')
484
608
  if (a === 'issue' && b === 'close') return cmdIssuePatch(rest, { status: 'closed' }, 'closed')
609
+ if (a === 'push' && b === 'send') return cmdPushSend(rest)
610
+ if (a === 'push' && b === 'receipt') return cmdPushReceipt(rest)
611
+ if (a === 'push' && b === 'creds') {
612
+ const [c, ...rest2] = rest
613
+ if (c === 'list') return cmdPushCredsList(rest2)
614
+ if (c === 'set') return cmdPushCredsSet(rest2)
615
+ if (c === 'delete') return cmdPushCredsDelete(rest2)
616
+ }
485
617
  console.error(`unknown command: ${[a, b].filter(Boolean).join(' ') || '(none)'}\n`)
486
618
  console.error(HELP)
487
619
  return 2
488
620
  }
489
621
 
622
+ // ── push commands (v2.12) ─────────────────────────────────────────
623
+
624
+ async function cmdPushSend(argv: string[]): Promise<number> {
625
+ const parsed = parseArgs({
626
+ args: argv,
627
+ options: {
628
+ 'api-url': { type: 'string' },
629
+ body: { type: 'string' },
630
+ data: { type: 'string' },
631
+ 'idempotency-key': { type: 'string' },
632
+ 'ingest-url': { type: 'string' },
633
+ priority: { type: 'string' },
634
+ project: { type: 'string' },
635
+ title: { type: 'string' },
636
+ to: { type: 'string' },
637
+ token: { type: 'string' },
638
+ ttl: { type: 'string' },
639
+ },
640
+ strict: true,
641
+ })
642
+ const cfg = parseAdminCfg(parsed.values)
643
+ if (!cfg) return 2
644
+ const to = parsed.values.to as string | undefined
645
+ if (!to) {
646
+ console.error('error: --to <ipt_handle> is required')
647
+ return 2
648
+ }
649
+ try {
650
+ const data = parsed.values.data ? (parseJsonArg(parsed.values.data as string, '--data') as Record<string, unknown>) : undefined
651
+ const priority = parsed.values.priority as 'high' | 'normal' | undefined
652
+ const ticket = await pushSend(cfg, {
653
+ to,
654
+ title: parsed.values.title as string | undefined,
655
+ body: parsed.values.body as string | undefined,
656
+ data,
657
+ priority,
658
+ ttl: parsed.values.ttl ? Number(parsed.values.ttl) : undefined,
659
+ idempotencyKey: parsed.values['idempotency-key'] as string | undefined,
660
+ })
661
+ console.log(`${ticket.id} ${ticket.status}`)
662
+ return 0
663
+ } catch (e) {
664
+ console.error(`push send failed: ${(e as Error).message}`)
665
+ return 1
666
+ }
667
+ }
668
+
669
+ async function cmdPushReceipt(argv: string[]): Promise<number> {
670
+ const parsed = parseArgs({
671
+ args: argv,
672
+ options: {
673
+ 'api-url': { type: 'string' },
674
+ 'ingest-url': { type: 'string' },
675
+ project: { type: 'string' },
676
+ token: { type: 'string' },
677
+ },
678
+ allowPositionals: true,
679
+ strict: true,
680
+ })
681
+ const sendId = parsed.positionals[0]
682
+ if (!sendId) {
683
+ console.error('error: <send-id> positional is required')
684
+ return 2
685
+ }
686
+ const cfg = parseAdminCfg(parsed.values)
687
+ if (!cfg) return 2
688
+ try {
689
+ const r = await pushReceipt(cfg, sendId)
690
+ console.log(`${r.ticket.id} ${r.ticket.status}${r.ticket.providerOutcome ? ` (${r.ticket.providerOutcome})` : ''}${r.ticket.error ? ` — ${r.ticket.error}` : ''}`)
691
+ return 0
692
+ } catch (e) {
693
+ console.error(`push receipt failed: ${(e as Error).message}`)
694
+ return 1
695
+ }
696
+ }
697
+
698
+ async function cmdPushCredsList(argv: string[]): Promise<number> {
699
+ const parsed = parseArgs({
700
+ args: argv,
701
+ options: {
702
+ 'api-url': { type: 'string' },
703
+ 'ingest-url': { type: 'string' },
704
+ project: { type: 'string' },
705
+ token: { type: 'string' },
706
+ },
707
+ strict: true,
708
+ })
709
+ const cfg = parseAdminCfg(parsed.values)
710
+ if (!cfg) return 2
711
+ try {
712
+ const rows = await pushCredsList(cfg)
713
+ if (rows.length === 0) {
714
+ console.log('(no providers configured)')
715
+ return 0
716
+ }
717
+ for (const r of rows) {
718
+ console.log(`${r.provider}\t${r.updatedAt}\t${JSON.stringify(r.config)}`)
719
+ }
720
+ return 0
721
+ } catch (e) {
722
+ console.error(`push creds list failed: ${(e as Error).message}`)
723
+ return 1
724
+ }
725
+ }
726
+
727
+ async function cmdPushCredsSet(argv: string[]): Promise<number> {
728
+ const parsed = parseArgs({
729
+ args: argv,
730
+ options: {
731
+ 'api-url': { type: 'string' },
732
+ config: { type: 'string' },
733
+ 'ingest-url': { type: 'string' },
734
+ project: { type: 'string' },
735
+ secret: { type: 'string' },
736
+ token: { type: 'string' },
737
+ },
738
+ allowPositionals: true,
739
+ strict: true,
740
+ })
741
+ const provider = parsed.positionals[0]
742
+ if (!provider) {
743
+ console.error('error: <provider> positional (apns/fcm/webpush/hcm/mipush) is required')
744
+ return 2
745
+ }
746
+ const cfg = parseAdminCfg(parsed.values)
747
+ if (!cfg) return 2
748
+ if (!parsed.values.config || !parsed.values.secret) {
749
+ console.error('error: --config @file.json and --secret @file.json are both required')
750
+ return 2
751
+ }
752
+ try {
753
+ const config = parseJsonArg(parsed.values.config as string, '--config')
754
+ const secret = parseJsonArg(parsed.values.secret as string, '--secret')
755
+ await pushCredsSet(cfg, provider, config, secret)
756
+ console.log(`${provider} ✓ saved`)
757
+ return 0
758
+ } catch (e) {
759
+ console.error(`push creds set failed: ${(e as Error).message}`)
760
+ return 1
761
+ }
762
+ }
763
+
764
+ async function cmdPushCredsDelete(argv: string[]): Promise<number> {
765
+ const parsed = parseArgs({
766
+ args: argv,
767
+ options: {
768
+ 'api-url': { type: 'string' },
769
+ 'ingest-url': { type: 'string' },
770
+ project: { type: 'string' },
771
+ token: { type: 'string' },
772
+ },
773
+ allowPositionals: true,
774
+ strict: true,
775
+ })
776
+ const provider = parsed.positionals[0]
777
+ if (!provider) {
778
+ console.error('error: <provider> positional is required')
779
+ return 2
780
+ }
781
+ const cfg = parseAdminCfg(parsed.values)
782
+ if (!cfg) return 2
783
+ try {
784
+ await pushCredsDelete(cfg, provider)
785
+ console.log(`${provider} ✓ deleted`)
786
+ return 0
787
+ } catch (e) {
788
+ console.error(`push creds delete failed: ${(e as Error).message}`)
789
+ return 1
790
+ }
791
+ }
792
+
490
793
  main(process.argv.slice(2)).then(
491
794
  (code) => process.exit(code),
492
795
  (e: unknown) => {