@goliapkg/sentori-cli 0.5.3 → 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.
Files changed (51) hide show
  1. package/README.md +27 -32
  2. package/lib/index.d.ts +3 -0
  3. package/lib/index.d.ts.map +1 -0
  4. package/lib/index.js +795 -0
  5. package/lib/index.js.map +1 -0
  6. package/lib/issue.d.ts +28 -0
  7. package/lib/issue.d.ts.map +1 -0
  8. package/lib/issue.js +53 -0
  9. package/lib/issue.js.map +1 -0
  10. package/lib/mcp.d.ts +14 -0
  11. package/lib/mcp.d.ts.map +1 -0
  12. package/lib/mcp.js +371 -0
  13. package/lib/mcp.js.map +1 -0
  14. package/lib/native-artifacts.d.ts +43 -0
  15. package/lib/native-artifacts.d.ts.map +1 -0
  16. package/lib/native-artifacts.js +148 -0
  17. package/lib/native-artifacts.js.map +1 -0
  18. package/lib/push.d.ts +42 -0
  19. package/lib/push.d.ts.map +1 -0
  20. package/lib/push.js +92 -0
  21. package/lib/push.js.map +1 -0
  22. package/lib/react-native.d.ts +25 -0
  23. package/lib/react-native.d.ts.map +1 -0
  24. package/lib/react-native.js +74 -0
  25. package/lib/react-native.js.map +1 -0
  26. package/lib/source-bundle.d.ts +28 -0
  27. package/lib/source-bundle.d.ts.map +1 -0
  28. package/lib/source-bundle.js +193 -0
  29. package/lib/source-bundle.js.map +1 -0
  30. package/lib/upload.d.ts +24 -0
  31. package/lib/upload.d.ts.map +1 -0
  32. package/lib/upload.js +65 -0
  33. package/lib/upload.js.map +1 -0
  34. package/package.json +23 -13
  35. package/src/__tests__/issue.test.ts +95 -0
  36. package/src/__tests__/mcp.test.ts +124 -0
  37. package/src/__tests__/native-artifacts.test.ts +132 -0
  38. package/src/__tests__/react-native.test.ts +37 -0
  39. package/src/__tests__/source-bundle-from-dir.test.ts +85 -0
  40. package/src/__tests__/source-bundle.test.ts +105 -0
  41. package/src/__tests__/upload.test.ts +121 -0
  42. package/src/index.ts +799 -0
  43. package/src/issue.ts +81 -0
  44. package/src/mcp.ts +399 -0
  45. package/src/native-artifacts.ts +182 -0
  46. package/src/push.ts +174 -0
  47. package/src/react-native.ts +87 -0
  48. package/src/source-bundle.ts +234 -0
  49. package/src/upload.ts +85 -0
  50. package/bin/sentori-cli.js +0 -32
  51. package/scripts/postinstall.js +0 -90
@@ -0,0 +1,105 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
6
+
7
+ import { uploadSourceBundle } from '../source-bundle.js'
8
+
9
+ const ADMIN = {
10
+ apiUrl: 'https://api.example.com/',
11
+ projectId: 'proj-1',
12
+ release: 'myapp@1.0.0',
13
+ token: 'sk_test',
14
+ }
15
+
16
+ let dir = ''
17
+ const origFetch = globalThis.fetch
18
+ let calls: { body: Buffer | null; headers: Headers; url: string }[]
19
+
20
+ beforeEach(async () => {
21
+ dir = await mkdtemp(join(tmpdir(), 'sentori-cli-srcbun-'))
22
+ calls = []
23
+ })
24
+ afterEach(async () => {
25
+ globalThis.fetch = origFetch
26
+ await rm(dir, { force: true, recursive: true })
27
+ })
28
+
29
+ function mockFetch(status = 201, body: object = { contentHash: 'h', kind: 'source_bundle_ios', sizeBytes: 12 }): void {
30
+ globalThis.fetch = (async (url: Request | string | URL, init?: RequestInit) => {
31
+ const b = init?.body
32
+ calls.push({
33
+ body: b instanceof Uint8Array ? Buffer.from(b) : null,
34
+ headers: new Headers(init?.headers),
35
+ url: String(url),
36
+ })
37
+ return new Response(JSON.stringify(body), { status })
38
+ }) as typeof fetch
39
+ }
40
+
41
+ describe('uploadSourceBundle', () => {
42
+ test('rejects unknown platform', async () => {
43
+ const path = join(dir, 'a.tar.gz')
44
+ await writeFile(path, Buffer.from([0x1f, 0x8b, 0x08, 0x00]))
45
+ mockFetch()
46
+ await expect(
47
+ uploadSourceBundle({ ...ADMIN, path, platform: 'windows' as unknown as 'ios' }),
48
+ ).rejects.toThrow(/platform/)
49
+ })
50
+
51
+ test('rejects non-gzip body before hitting network', async () => {
52
+ const path = join(dir, 'not-gzip.tar.gz')
53
+ await writeFile(path, Buffer.from('plain text not a gzip stream'))
54
+ mockFetch()
55
+ await expect(
56
+ uploadSourceBundle({ ...ADMIN, path, platform: 'ios' }),
57
+ ).rejects.toThrow(/gzip|1f 8b/i)
58
+ expect(calls.length).toBe(0)
59
+ })
60
+
61
+ test('rejects empty body', async () => {
62
+ const path = join(dir, 'empty.tar.gz')
63
+ await writeFile(path, Buffer.alloc(0))
64
+ mockFetch()
65
+ await expect(uploadSourceBundle({ ...ADMIN, path, platform: 'ios' })).rejects.toThrow(
66
+ /empty/,
67
+ )
68
+ })
69
+
70
+ test('posts to /admin/api/projects/<id>/source-bundles with platform + release query', async () => {
71
+ const path = join(dir, 'src.tar.gz')
72
+ // gzip magic + minimal payload — server-side validation passes.
73
+ await writeFile(path, Buffer.from([0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00]))
74
+ mockFetch(201, { contentHash: 'abc123', kind: 'source_bundle_ios', sizeBytes: 6 })
75
+
76
+ const r = await uploadSourceBundle({ ...ADMIN, path, platform: 'ios' })
77
+
78
+ expect(calls.length).toBe(1)
79
+ expect(calls[0]!.url).toContain('/admin/api/projects/proj-1/source-bundles?')
80
+ expect(calls[0]!.url).toContain('platform=ios')
81
+ expect(calls[0]!.url).toContain('release=myapp%401.0.0')
82
+ expect(calls[0]!.headers.get('authorization')).toBe('Bearer sk_test')
83
+ expect(calls[0]!.headers.get('content-type')).toBe('application/gzip')
84
+ expect(r.kind).toBe('source_bundle_ios')
85
+ expect(r.contentHash).toBe('abc123')
86
+ })
87
+
88
+ test('android platform routes the same shape', async () => {
89
+ const path = join(dir, 'src.tar.gz')
90
+ await writeFile(path, Buffer.from([0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00]))
91
+ mockFetch(201, { contentHash: 'def', kind: 'source_bundle_android', sizeBytes: 6 })
92
+ const r = await uploadSourceBundle({ ...ADMIN, path, platform: 'android' })
93
+ expect(calls[0]!.url).toContain('platform=android')
94
+ expect(r.kind).toBe('source_bundle_android')
95
+ })
96
+
97
+ test('surfaces server 4xx/5xx as Error with snippet', async () => {
98
+ const path = join(dir, 'src.tar.gz')
99
+ await writeFile(path, Buffer.from([0x1f, 0x8b]))
100
+ mockFetch(500, { error: { code: 'bad', message: 'boom on server' } })
101
+ await expect(uploadSourceBundle({ ...ADMIN, path, platform: 'ios' })).rejects.toThrow(
102
+ /500/,
103
+ )
104
+ })
105
+ })
@@ -0,0 +1,121 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
6
+
7
+ import { collectFiles, uploadSourcemaps } from '../upload.js'
8
+
9
+ let dir = ''
10
+ beforeEach(async () => {
11
+ dir = await mkdtemp(join(tmpdir(), 'sentori-cli-test-'))
12
+ await writeFile(join(dir, 'main.jsbundle'), '/*bundle*/')
13
+ await writeFile(join(dir, 'main.jsbundle.map'), '{"version":3}')
14
+ await writeFile(join(dir, 'app.js'), 'console.log(1)')
15
+ await writeFile(join(dir, 'app.js.map'), '{"version":3}')
16
+ await writeFile(join(dir, 'README.txt'), 'not a build artifact')
17
+ })
18
+ afterEach(async () => {
19
+ await rm(dir, { force: true, recursive: true })
20
+ })
21
+
22
+ describe('collectFiles', () => {
23
+ test('a directory yields its .map / .js / .bundle files, not others', async () => {
24
+ const files = await collectFiles([dir])
25
+ const names = files.map((f) => f.replace(`${dir}/`, '')).sort()
26
+ expect(names).toEqual(['app.js', 'app.js.map', 'main.jsbundle', 'main.jsbundle.map'])
27
+ })
28
+
29
+ test('an explicit file is taken as-is regardless of extension', async () => {
30
+ const files = await collectFiles([join(dir, 'README.txt')])
31
+ expect(files).toEqual([join(dir, 'README.txt')])
32
+ })
33
+
34
+ test('dedupes when a file is named both directly and via its dir', async () => {
35
+ const files = await collectFiles([dir, join(dir, 'app.js.map')])
36
+ expect(files.filter((f) => f.endsWith('app.js.map'))).toHaveLength(1)
37
+ })
38
+
39
+ test('throws on a nonexistent path', async () => {
40
+ await expect(collectFiles([join(dir, 'nope')])).rejects.toThrow('no such file or directory')
41
+ })
42
+
43
+ test('throws when a directory has no uploadable files', async () => {
44
+ const empty = await mkdtemp(join(tmpdir(), 'sentori-cli-empty-'))
45
+ try {
46
+ await expect(collectFiles([empty])).rejects.toThrow('no .map')
47
+ } finally {
48
+ await rm(empty, { force: true, recursive: true })
49
+ }
50
+ })
51
+ })
52
+
53
+ describe('uploadSourcemaps', () => {
54
+ test('dryRun returns the file list, makes no request', async () => {
55
+ let called = false
56
+ const orig = globalThis.fetch
57
+ globalThis.fetch = (async () => {
58
+ called = true
59
+ return new Response()
60
+ }) as typeof fetch
61
+ try {
62
+ const r = await uploadSourcemaps({
63
+ apiUrl: 'https://api.example.com',
64
+ dryRun: true,
65
+ paths: [dir],
66
+ release: 'app@1.0.0+1',
67
+ token: 'st_pk_x',
68
+ })
69
+ expect(r.files).toHaveLength(4)
70
+ expect(called).toBe(false)
71
+ } finally {
72
+ globalThis.fetch = orig
73
+ }
74
+ })
75
+
76
+ test('POSTs multipart to /admin/api/releases/<release>/sourcemaps with the token', async () => {
77
+ const calls: { headers: Headers; url: string }[] = []
78
+ const orig = globalThis.fetch
79
+ globalThis.fetch = (async (url: Request | string | URL, init?: RequestInit) => {
80
+ calls.push({ headers: new Headers(init?.headers), url: String(url) })
81
+ return new Response(JSON.stringify({ artifacts: [], uploaded: 4 }), {
82
+ headers: { 'content-type': 'application/json' },
83
+ status: 200,
84
+ })
85
+ }) as typeof fetch
86
+ try {
87
+ const r = await uploadSourcemaps({
88
+ apiUrl: 'https://api.example.com/',
89
+ paths: [dir],
90
+ release: 'my app@1.0.0+1',
91
+ token: 'st_pk_secret',
92
+ })
93
+ expect(calls).toHaveLength(1)
94
+ expect(calls[0]?.url).toBe(
95
+ 'https://api.example.com/admin/api/releases/my%20app%401.0.0%2B1/sourcemaps',
96
+ )
97
+ expect(calls[0]?.headers.get('authorization')).toBe('Bearer st_pk_secret')
98
+ expect(r.uploaded).toBe(4)
99
+ } finally {
100
+ globalThis.fetch = orig
101
+ }
102
+ })
103
+
104
+ test('throws with the server detail on a non-2xx response', async () => {
105
+ const orig = globalThis.fetch
106
+ globalThis.fetch = (async () =>
107
+ new Response('release frozen', { status: 403, statusText: 'Forbidden' })) as typeof fetch
108
+ try {
109
+ await expect(
110
+ uploadSourcemaps({
111
+ apiUrl: 'https://api.example.com',
112
+ paths: [dir],
113
+ release: 'app@1.0.0+1',
114
+ token: 'st_pk_x',
115
+ }),
116
+ ).rejects.toThrow('403 Forbidden — release frozen')
117
+ } finally {
118
+ globalThis.fetch = orig
119
+ }
120
+ })
121
+ })