@goliapkg/sentori-cli 0.5.3 → 1.0.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 +723 -0
- package/lib/index.js.map +1 -0
- package/lib/issue.d.ts +26 -0
- package/lib/issue.d.ts.map +1 -0
- package/lib/issue.js +50 -0
- package/lib/issue.js.map +1 -0
- package/lib/lenient.d.ts +13 -0
- package/lib/lenient.d.ts.map +1 -0
- package/lib/lenient.js +25 -0
- package/lib/lenient.js.map +1 -0
- package/lib/mcp.d.ts +16 -0
- package/lib/mcp.d.ts.map +1 -0
- package/lib/mcp.js +194 -0
- package/lib/mcp.js.map +1 -0
- package/lib/native-artifacts.d.ts +42 -0
- package/lib/native-artifacts.d.ts.map +1 -0
- package/lib/native-artifacts.js +137 -0
- package/lib/native-artifacts.js.map +1 -0
- package/lib/probes.d.ts +10 -0
- package/lib/probes.d.ts.map +1 -0
- package/lib/probes.js +75 -0
- package/lib/probes.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 +82 -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 +13 -0
- package/lib/upload.d.ts.map +1 -0
- package/lib/upload.js +28 -0
- package/lib/upload.js.map +1 -0
- package/package.json +23 -13
- package/src/__tests__/lenient.test.ts +32 -0
- package/src/__tests__/mcp.test.ts +32 -0
- package/src/__tests__/native-artifacts.test.ts +125 -0
- package/src/__tests__/probes.test.ts +41 -0
- package/src/__tests__/react-native.test.ts +37 -0
- package/src/index.ts +727 -0
- package/src/issue.ts +82 -0
- package/src/lenient.ts +39 -0
- package/src/mcp.ts +230 -0
- package/src/native-artifacts.ts +175 -0
- package/src/probes.ts +75 -0
- package/src/push.ts +174 -0
- package/src/react-native.ts +94 -0
- package/src/upload.ts +44 -0
- package/bin/sentori-cli.js +0 -32
- package/scripts/postinstall.js +0 -90
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sentori CLI — upload source maps
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sentori CLI — upload source maps so dashboard stack traces resolve to your original source.",
|
|
5
|
+
"license": "Apache-2.0 OR MIT",
|
|
6
|
+
"author": "GOLIA K.K. <takagi@golia.jp> (https://golia.jp)",
|
|
6
7
|
"homepage": "https://sentori.golia.jp",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
10
|
"url": "git+https://github.com/goliajp/sentori.git",
|
|
10
|
-
"directory": "cli
|
|
11
|
+
"directory": "sdk/cli"
|
|
11
12
|
},
|
|
12
13
|
"bugs": {
|
|
13
14
|
"url": "https://github.com/goliajp/sentori/issues"
|
|
@@ -15,25 +16,34 @@
|
|
|
15
16
|
"keywords": [
|
|
16
17
|
"sentori",
|
|
17
18
|
"cli",
|
|
18
|
-
"
|
|
19
|
+
"sourcemaps",
|
|
19
20
|
"error-tracking"
|
|
20
21
|
],
|
|
22
|
+
"type": "module",
|
|
21
23
|
"bin": {
|
|
22
|
-
"sentori-cli": "
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"postinstall": "node scripts/postinstall.js || echo 'sentori-cli postinstall failed — run `npx @goliapkg/sentori-cli` once you have network'"
|
|
24
|
+
"sentori-cli": "./lib/index.js"
|
|
26
25
|
},
|
|
26
|
+
"main": "./lib/index.js",
|
|
27
|
+
"types": "./lib/index.d.ts",
|
|
27
28
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"
|
|
29
|
+
"lib/",
|
|
30
|
+
"src/",
|
|
30
31
|
"README.md"
|
|
31
32
|
],
|
|
32
33
|
"engines": {
|
|
33
34
|
"node": ">=18"
|
|
34
35
|
},
|
|
35
|
-
"
|
|
36
|
-
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"prepack": "bun run build"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/bun": "latest",
|
|
44
|
+
"@types/node": "*",
|
|
45
|
+
"typescript": "^7"
|
|
46
|
+
},
|
|
37
47
|
"publishConfig": {
|
|
38
48
|
"access": "public"
|
|
39
49
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Build-time iron rule: upload failure exits 0 with a friendly,
|
|
2
|
+
// actionable note; --strict opts into a real non-zero.
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from 'bun:test'
|
|
5
|
+
|
|
6
|
+
import { isStrict, lenientFail, stripStrict } from '../lenient.js'
|
|
7
|
+
|
|
8
|
+
describe('lenient upload contract', () => {
|
|
9
|
+
test('default: failure exits 0', () => {
|
|
10
|
+
const code = lenientFail(false, {
|
|
11
|
+
failure: 'network down',
|
|
12
|
+
impact: 'stacks stay minified',
|
|
13
|
+
retry: 'sentori-cli upload sourcemap ...',
|
|
14
|
+
})
|
|
15
|
+
expect(code).toBe(0)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('--strict: failure exits 1', () => {
|
|
19
|
+
const code = lenientFail(true, {
|
|
20
|
+
failure: 'network down',
|
|
21
|
+
impact: 'stacks stay minified',
|
|
22
|
+
retry: 'retry-cmd',
|
|
23
|
+
})
|
|
24
|
+
expect(code).toBe(1)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('strict flag parses and strips', () => {
|
|
28
|
+
expect(isStrict(['--release', 'r', '--strict'])).toBe(true)
|
|
29
|
+
expect(isStrict(['--release', 'r'])).toBe(false)
|
|
30
|
+
expect(stripStrict(['--strict', 'a'])).toEqual(['a'])
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The MCP tool surface mirrors the /api closed loop 1:1.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test'
|
|
4
|
+
|
|
5
|
+
import { buildTools } from '../mcp.js'
|
|
6
|
+
|
|
7
|
+
describe('mcp tools', () => {
|
|
8
|
+
const tools = buildTools()
|
|
9
|
+
|
|
10
|
+
test('exactly the four closed-loop tools exist', () => {
|
|
11
|
+
expect(tools.map((t) => t.name).sort()).toEqual([
|
|
12
|
+
'sentori_issue_bundle',
|
|
13
|
+
'sentori_issue_list',
|
|
14
|
+
'sentori_issue_note',
|
|
15
|
+
'sentori_issue_resolve',
|
|
16
|
+
])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('every tool has a description and an object schema', () => {
|
|
20
|
+
for (const t of tools) {
|
|
21
|
+
expect(t.description.length).toBeGreaterThan(20)
|
|
22
|
+
expect((t.inputSchema as { type?: string }).type).toBe('object')
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('bundle/note/resolve require issueId', () => {
|
|
27
|
+
for (const name of ['sentori_issue_bundle', 'sentori_issue_note', 'sentori_issue_resolve']) {
|
|
28
|
+
const t = tools.find((x) => x.name === name)!
|
|
29
|
+
expect((t.inputSchema as { required?: string[] }).required).toContain('issueId')
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { mkdir, 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 {
|
|
8
|
+
dsymSlicesFromDwarfdump,
|
|
9
|
+
dwarfBinariesIn,
|
|
10
|
+
uploadDsym,
|
|
11
|
+
uploadMapping,
|
|
12
|
+
} from '../native-artifacts.js'
|
|
13
|
+
|
|
14
|
+
const ADMIN = {
|
|
15
|
+
apiUrl: 'https://api.example.com/',
|
|
16
|
+
token: 'sk_test',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let dir = ''
|
|
20
|
+
const origFetch = globalThis.fetch
|
|
21
|
+
let calls: { body: Buffer | null; headers: Headers; method: string; url: string }[]
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
dir = await mkdtemp(join(tmpdir(), 'sentori-cli-native-'))
|
|
24
|
+
calls = []
|
|
25
|
+
})
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
globalThis.fetch = origFetch
|
|
28
|
+
await rm(dir, { force: true, recursive: true })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function mockFetch(status = 200, respBody = ''): void {
|
|
32
|
+
globalThis.fetch = (async (url: Request | string | URL, init?: RequestInit) => {
|
|
33
|
+
const body = init?.body
|
|
34
|
+
calls.push({
|
|
35
|
+
body: body instanceof Uint8Array ? Buffer.from(body) : null,
|
|
36
|
+
headers: new Headers(init?.headers),
|
|
37
|
+
method: init?.method ?? 'GET',
|
|
38
|
+
url: String(url),
|
|
39
|
+
})
|
|
40
|
+
return new Response(respBody, { status })
|
|
41
|
+
}) as typeof fetch
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('uploadMapping', () => {
|
|
45
|
+
test('POSTs raw bytes to /mappings with Bearer + release query', async () => {
|
|
46
|
+
mockFetch()
|
|
47
|
+
const path = join(dir, 'mapping.txt')
|
|
48
|
+
await writeFile(path, '# pg_map_id: abc\nfoo.bar -> a.b:\n')
|
|
49
|
+
await uploadMapping({ ...ADMIN, path, release: 'app@1.0.0+1' })
|
|
50
|
+
expect(calls).toHaveLength(1)
|
|
51
|
+
expect(calls[0]?.method).toBe('POST')
|
|
52
|
+
expect(calls[0]?.url).toBe(
|
|
53
|
+
'https://api.example.com/v1/releases/app%401.0.0%2B1/artifacts',
|
|
54
|
+
)
|
|
55
|
+
expect(calls[0]?.headers.get('authorization')).toBe('Bearer sk_test')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('sets x-sentori-debug-id when given', async () => {
|
|
59
|
+
mockFetch()
|
|
60
|
+
const path = join(dir, 'mapping.txt')
|
|
61
|
+
await writeFile(path, 'x')
|
|
62
|
+
await uploadMapping({ ...ADMIN, debugId: '1234-uuid', path, release: 'r' })
|
|
63
|
+
expect(calls[0]?.url).toContain('/v1/releases/r/artifacts')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('throws on empty file', async () => {
|
|
67
|
+
const path = join(dir, 'empty.txt')
|
|
68
|
+
await writeFile(path, '')
|
|
69
|
+
await expect(uploadMapping({ ...ADMIN, path })).rejects.toThrow('empty mapping file')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('throws on non-2xx with server detail', async () => {
|
|
73
|
+
mockFetch(403, 'forbidden')
|
|
74
|
+
const path = join(dir, 'mapping.txt')
|
|
75
|
+
await writeFile(path, 'x')
|
|
76
|
+
await expect(uploadMapping({ ...ADMIN, path })).rejects.toThrow('403')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('uploadDsym (explicit single-slice)', () => {
|
|
81
|
+
test('POSTs to /dsyms with x-sentori-debug-id, x-sentori-arch, release + objectName', async () => {
|
|
82
|
+
mockFetch()
|
|
83
|
+
const path = join(dir, 'Foo.dSYM/Contents/Resources/DWARF/Foo')
|
|
84
|
+
await mkdir(join(dir, 'Foo.dSYM/Contents/Resources/DWARF'), { recursive: true })
|
|
85
|
+
await writeFile(path, 'macho-bytes')
|
|
86
|
+
const r = await uploadDsym({
|
|
87
|
+
...ADMIN,
|
|
88
|
+
arch: 'arm64',
|
|
89
|
+
debugId: '1234abcd-1234-1234-1234-1234567890ab',
|
|
90
|
+
objectName: 'Foo',
|
|
91
|
+
path,
|
|
92
|
+
release: 'app@1.0.0+1',
|
|
93
|
+
})
|
|
94
|
+
expect(calls).toHaveLength(1)
|
|
95
|
+
expect(calls[0]?.method).toBe('POST')
|
|
96
|
+
expect(calls[0]?.url).toContain('/v1/releases/app%401.0.0%2B1/artifacts')
|
|
97
|
+
expect(r.slices).toEqual([{ arch: 'arm64', debugId: '1234ABCD-1234-1234-1234-1234567890AB' }])
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('helpers', () => {
|
|
102
|
+
test('dwarfBinariesIn walks a .dSYM bundle', async () => {
|
|
103
|
+
const bundle = join(dir, 'Foo.dSYM')
|
|
104
|
+
const dwarfDir = join(bundle, 'Contents/Resources/DWARF')
|
|
105
|
+
await mkdir(dwarfDir, { recursive: true })
|
|
106
|
+
await writeFile(join(dwarfDir, 'Foo'), 'x')
|
|
107
|
+
await writeFile(join(dwarfDir, '.DS_Store'), 'noise')
|
|
108
|
+
const found = dwarfBinariesIn(bundle)
|
|
109
|
+
expect(found.map((p) => p.replace(bundle + '/', ''))).toEqual([
|
|
110
|
+
'Contents/Resources/DWARF/Foo',
|
|
111
|
+
])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('dwarfBinariesIn on a plain file returns just that file', async () => {
|
|
115
|
+
const f = join(dir, 'binary')
|
|
116
|
+
await writeFile(f, 'x')
|
|
117
|
+
expect(dwarfBinariesIn(f)).toEqual([f])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('dsymSlicesFromDwarfdump returns [] when dwarfdump fails (or is absent)', () => {
|
|
121
|
+
// pass a path that doesn't exist; dwarfdump exits non-zero (or isn't installed)
|
|
122
|
+
const slices = dsymSlicesFromDwarfdump(join(dir, 'nope.dSYM'))
|
|
123
|
+
expect(slices).toEqual([])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The probe scanner: finds sentori.probe('REF') call sites, skips
|
|
2
|
+
// junk dirs, dedupes.
|
|
3
|
+
|
|
4
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
9
|
+
|
|
10
|
+
import { scanProbes } from '../probes.js'
|
|
11
|
+
|
|
12
|
+
let dir = ''
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
dir = await mkdtemp(join(tmpdir(), 'sentori-probes-'))
|
|
15
|
+
})
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await rm(dir, { force: true, recursive: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('scanProbes', () => {
|
|
21
|
+
test('finds refs across quote styles and dedupes', async () => {
|
|
22
|
+
await writeFile(
|
|
23
|
+
join(dir, 'a.ts'),
|
|
24
|
+
`sentori.probe('SENT-1'); probe("SENT-2", {x:1}); sentori.probe(\`SENT-1\`)`,
|
|
25
|
+
)
|
|
26
|
+
await writeFile(join(dir, 'b.tsx'), `if (bad) { sentori.probe('SENT-3') }`)
|
|
27
|
+
expect(scanProbes(dir)).toEqual(['SENT-1', 'SENT-2', 'SENT-3'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('skips node_modules and non-source files', async () => {
|
|
31
|
+
await mkdir(join(dir, 'node_modules'), { recursive: true })
|
|
32
|
+
await writeFile(join(dir, 'node_modules', 'x.ts'), `probe('NOPE')`)
|
|
33
|
+
await writeFile(join(dir, 'notes.md'), `probe('ALSO-NOPE')`)
|
|
34
|
+
expect(scanProbes(dir)).toEqual([])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('dynamic refs are invisible (documented limitation)', async () => {
|
|
38
|
+
await writeFile(join(dir, 'c.ts'), 'sentori.probe(ref)')
|
|
39
|
+
expect(scanProbes(dir)).toEqual([])
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
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 { composeSourceMaps, resolveComposeScript } from '../react-native.js'
|
|
8
|
+
|
|
9
|
+
let dir = ''
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
dir = await mkdtemp(join(tmpdir(), 'sentori-cli-rn-'))
|
|
12
|
+
})
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await rm(dir, { force: true, recursive: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('react-native helpers', () => {
|
|
18
|
+
test('resolveComposeScript returns null when react-native is not installed', () => {
|
|
19
|
+
// This package doesn't depend on react-native, so from sdk/cli's
|
|
20
|
+
// own node_modules there's nothing to find.
|
|
21
|
+
expect(resolveComposeScript()).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('composeSourceMaps throws "no such file" for a missing input', () => {
|
|
25
|
+
expect(() => composeSourceMaps(join(dir, 'nope.map'), join(dir, 'nope2.map'))).toThrow(
|
|
26
|
+
'no such file',
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('composeSourceMaps throws a helpful message when react-native is absent', async () => {
|
|
31
|
+
await writeFile(join(dir, 'a.packager.map'), '{"version":3}')
|
|
32
|
+
await writeFile(join(dir, 'a.hbc.map'), '{"version":3}')
|
|
33
|
+
expect(() =>
|
|
34
|
+
composeSourceMaps(join(dir, 'a.packager.map'), join(dir, 'a.hbc.map')),
|
|
35
|
+
).toThrow(/compose-source-maps\.js/)
|
|
36
|
+
})
|
|
37
|
+
})
|