@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.
- package/README.md +27 -32
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +795 -0
- package/lib/index.js.map +1 -0
- package/lib/issue.d.ts +28 -0
- package/lib/issue.d.ts.map +1 -0
- package/lib/issue.js +53 -0
- package/lib/issue.js.map +1 -0
- package/lib/mcp.d.ts +14 -0
- package/lib/mcp.d.ts.map +1 -0
- package/lib/mcp.js +371 -0
- package/lib/mcp.js.map +1 -0
- package/lib/native-artifacts.d.ts +43 -0
- package/lib/native-artifacts.d.ts.map +1 -0
- package/lib/native-artifacts.js +148 -0
- package/lib/native-artifacts.js.map +1 -0
- package/lib/push.d.ts +42 -0
- package/lib/push.d.ts.map +1 -0
- package/lib/push.js +92 -0
- package/lib/push.js.map +1 -0
- package/lib/react-native.d.ts +25 -0
- package/lib/react-native.d.ts.map +1 -0
- package/lib/react-native.js +74 -0
- package/lib/react-native.js.map +1 -0
- package/lib/source-bundle.d.ts +28 -0
- package/lib/source-bundle.d.ts.map +1 -0
- package/lib/source-bundle.js +193 -0
- package/lib/source-bundle.js.map +1 -0
- package/lib/upload.d.ts +24 -0
- package/lib/upload.d.ts.map +1 -0
- package/lib/upload.js +65 -0
- package/lib/upload.js.map +1 -0
- package/package.json +23 -13
- package/src/__tests__/issue.test.ts +95 -0
- package/src/__tests__/mcp.test.ts +124 -0
- package/src/__tests__/native-artifacts.test.ts +132 -0
- package/src/__tests__/react-native.test.ts +37 -0
- package/src/__tests__/source-bundle-from-dir.test.ts +85 -0
- package/src/__tests__/source-bundle.test.ts +105 -0
- package/src/__tests__/upload.test.ts +121 -0
- package/src/index.ts +799 -0
- package/src/issue.ts +81 -0
- package/src/mcp.ts +399 -0
- package/src/native-artifacts.ts +182 -0
- package/src/push.ts +174 -0
- package/src/react-native.ts +87 -0
- package/src/source-bundle.ts +234 -0
- package/src/upload.ts +85 -0
- package/bin/sentori-cli.js +0 -32
- 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
|
+
})
|