@goliapkg/sentori-cli 0.2.0 → 0.2.1
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/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +118 -0
- package/lib/index.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 +21 -12
- package/src/__tests__/upload.test.ts +121 -0
- package/src/index.ts +125 -0
- package/src/upload.ts +85 -0
- package/README.md +0 -51
- package/bin/sentori-cli.js +0 -32
- package/scripts/postinstall.js +0 -90
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { uploadSourcemaps } from './upload.js';
|
|
4
|
+
const HELP = `sentori-cli — upload release artifacts to Sentori
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
sentori-cli upload sourcemap [options] <path...>
|
|
8
|
+
|
|
9
|
+
<path> one or more files or directories. A directory is scanned
|
|
10
|
+
(one level) for *.map and *.js / *.bundle / *.hbc files.
|
|
11
|
+
A file given explicitly is uploaded as-is.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--release <r> release identifier — MUST equal the value the SDK
|
|
15
|
+
reports via init({ release }). Required. A mismatch
|
|
16
|
+
means the dashboard silently can't symbolicate.
|
|
17
|
+
--token <t> Sentori token (or set $SENTORI_TOKEN).
|
|
18
|
+
--api-url <url> Sentori API base (default https://api.sentori.golia.jp,
|
|
19
|
+
or $SENTORI_API_URL). For a self-hosted instance, your
|
|
20
|
+
host. (Accepts --ingest-url as an alias.)
|
|
21
|
+
--dry-run list what would be uploaded; don't upload.
|
|
22
|
+
-h, --help show this help.
|
|
23
|
+
|
|
24
|
+
React Native / Hermes: in a release build the *minified* map (Metro)
|
|
25
|
+
and the *bytecode* map (Hermes) must be composed first —
|
|
26
|
+
npx react-native bundle --platform ios --dev false \\
|
|
27
|
+
--entry-file index.js --bundle-output main.jsbundle \\
|
|
28
|
+
--sourcemap-output main.jsbundle.packager.map
|
|
29
|
+
node node_modules/react-native/scripts/compose-source-maps.js \\
|
|
30
|
+
main.jsbundle.packager.map main.jsbundle.hbc.map -o main.jsbundle.map
|
|
31
|
+
then \`sentori-cli upload sourcemap --release "<app>@<version>+<build>" main.jsbundle.map main.jsbundle\`.
|
|
32
|
+
(Web bundlers emit a usable .map directly — just point at the build dir.)
|
|
33
|
+
`;
|
|
34
|
+
async function main(argv) {
|
|
35
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
36
|
+
console.log(HELP);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
if (argv[0] !== 'upload' || argv[1] !== 'sourcemap') {
|
|
40
|
+
console.error(`unknown command: ${argv.slice(0, 2).join(' ') || '(none)'}\n`);
|
|
41
|
+
console.error(HELP);
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = parseArgs({
|
|
47
|
+
allowPositionals: true,
|
|
48
|
+
args: argv.slice(2),
|
|
49
|
+
options: {
|
|
50
|
+
'api-url': { type: 'string' },
|
|
51
|
+
'dry-run': { type: 'boolean' },
|
|
52
|
+
help: { short: 'h', type: 'boolean' },
|
|
53
|
+
'ingest-url': { type: 'string' }, // alias, kept for older docs
|
|
54
|
+
release: { type: 'string' },
|
|
55
|
+
token: { type: 'string' },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
console.error(`error: ${e.message}\n`);
|
|
61
|
+
console.error(HELP);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
const { positionals, values } = parsed;
|
|
65
|
+
if (values.help) {
|
|
66
|
+
console.log(HELP);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
const release = values.release;
|
|
70
|
+
if (!release) {
|
|
71
|
+
console.error('error: --release is required (must match the SDK’s init({ release }))');
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
const dryRun = !!values['dry-run'];
|
|
75
|
+
const token = values.token ?? process.env.SENTORI_TOKEN;
|
|
76
|
+
if (!token && !dryRun) {
|
|
77
|
+
console.error('error: --token (or $SENTORI_TOKEN) is required');
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
const apiUrl = values['api-url'] ??
|
|
81
|
+
values['ingest-url'] ??
|
|
82
|
+
process.env.SENTORI_API_URL ??
|
|
83
|
+
'https://api.sentori.golia.jp';
|
|
84
|
+
if (positionals.length === 0) {
|
|
85
|
+
console.error('error: at least one path (file or directory) is required');
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const result = await uploadSourcemaps({
|
|
90
|
+
apiUrl,
|
|
91
|
+
dryRun,
|
|
92
|
+
paths: positionals,
|
|
93
|
+
release,
|
|
94
|
+
token: token ?? '',
|
|
95
|
+
});
|
|
96
|
+
if (dryRun) {
|
|
97
|
+
console.log(`would upload ${result.files.length} file(s) to ${apiUrl.replace(/\/+$/, '')}/admin/api/releases/${encodeURIComponent(release)}/sourcemaps:`);
|
|
98
|
+
for (const f of result.files)
|
|
99
|
+
console.log(` ${f}`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log(`uploaded ${result.uploaded} file(s) for release "${release}"`);
|
|
103
|
+
for (const a of result.artifacts ?? [])
|
|
104
|
+
console.log(` ${a.name} [${a.kind}]`);
|
|
105
|
+
console.log('done — minified stack traces on this release will now resolve to source.');
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
console.error(`upload failed: ${e.message}`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
|
|
115
|
+
console.error(`fatal: ${e.message}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/upload.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type UploadOptions = {
|
|
2
|
+
release: string;
|
|
3
|
+
token: string;
|
|
4
|
+
/** Sentori API base, e.g. https://api.sentori.golia.jp or your host. */
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
/** Files or directories. Directories are scanned one level deep. */
|
|
7
|
+
paths: string[];
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type UploadResult = {
|
|
11
|
+
files: string[];
|
|
12
|
+
uploaded?: number;
|
|
13
|
+
artifacts?: {
|
|
14
|
+
kind: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}[];
|
|
17
|
+
};
|
|
18
|
+
/** Resolve `paths` (files or dirs) to a deduped list of files to upload.
|
|
19
|
+
* A directory contributes its top-level `.map` / `.js` / `.bundle` /
|
|
20
|
+
* `.hbc` files; a file given explicitly is taken as-is (even if its
|
|
21
|
+
* extension isn't in the list — the caller asked for it). */
|
|
22
|
+
export declare function collectFiles(paths: string[]): Promise<string[]>;
|
|
23
|
+
export declare function uploadSourcemaps(opts: UploadOptions): Promise<UploadResult>;
|
|
24
|
+
//# sourceMappingURL=upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC7C,CAAA;AAED;;;8DAG8D;AAC9D,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBrE;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAiCjF"}
|
package/lib/upload.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
// Files worth uploading: source maps, and the bundle they map (so the
|
|
4
|
+
// dashboard can show the minified line too if a frame falls outside
|
|
5
|
+
// the map). `.jsbundle` (iOS) and `.bundle` (Android) are RN's bundle
|
|
6
|
+
// names; `.hbc` is the Hermes bytecode bundle.
|
|
7
|
+
const UPLOADABLE = /\.(map|js|jsbundle|bundle|hbc)$/i;
|
|
8
|
+
/** Resolve `paths` (files or dirs) to a deduped list of files to upload.
|
|
9
|
+
* A directory contributes its top-level `.map` / `.js` / `.bundle` /
|
|
10
|
+
* `.hbc` files; a file given explicitly is taken as-is (even if its
|
|
11
|
+
* extension isn't in the list — the caller asked for it). */
|
|
12
|
+
export async function collectFiles(paths) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const p of paths) {
|
|
15
|
+
const s = await stat(p).catch(() => null);
|
|
16
|
+
if (!s)
|
|
17
|
+
throw new Error(`no such file or directory: ${p}`);
|
|
18
|
+
if (s.isDirectory()) {
|
|
19
|
+
for (const entry of await readdir(p)) {
|
|
20
|
+
const full = join(p, entry);
|
|
21
|
+
const es = await stat(full).catch(() => null);
|
|
22
|
+
if (es?.isFile() && UPLOADABLE.test(entry))
|
|
23
|
+
out.push(full);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
out.push(p);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const deduped = [...new Set(out)];
|
|
31
|
+
if (deduped.length === 0) {
|
|
32
|
+
throw new Error('no .map / .js / .bundle files found in the given path(s)');
|
|
33
|
+
}
|
|
34
|
+
return deduped;
|
|
35
|
+
}
|
|
36
|
+
export async function uploadSourcemaps(opts) {
|
|
37
|
+
const files = await collectFiles(opts.paths);
|
|
38
|
+
if (opts.dryRun)
|
|
39
|
+
return { files };
|
|
40
|
+
const form = new FormData();
|
|
41
|
+
for (const f of files) {
|
|
42
|
+
const buf = await readFile(f);
|
|
43
|
+
form.append('file', new Blob([buf]), basename(f));
|
|
44
|
+
}
|
|
45
|
+
const base = opts.apiUrl.replace(/\/+$/, '');
|
|
46
|
+
const url = `${base}/admin/api/releases/${encodeURIComponent(opts.release)}/sourcemaps`;
|
|
47
|
+
const resp = await fetch(url, {
|
|
48
|
+
body: form,
|
|
49
|
+
headers: { Authorization: `Bearer ${opts.token}` },
|
|
50
|
+
method: 'POST',
|
|
51
|
+
});
|
|
52
|
+
if (!resp.ok) {
|
|
53
|
+
let detail = '';
|
|
54
|
+
try {
|
|
55
|
+
detail = await resp.text();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`${resp.status} ${resp.statusText}${detail ? ` — ${detail.slice(0, 300)}` : ''}`);
|
|
61
|
+
}
|
|
62
|
+
const body = (await resp.json().catch(() => ({})));
|
|
63
|
+
return { artifacts: body.artifacts, files, uploaded: body.uploaded ?? files.length };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Sentori CLI
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Sentori CLI \u2014 upload source maps so dashboard stack traces resolve to your original source.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://sentori.golia.jp",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/goliajp/sentori.git",
|
|
10
|
-
"directory": "cli
|
|
10
|
+
"directory": "sdk/cli"
|
|
11
11
|
},
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/goliajp/sentori/issues"
|
|
@@ -15,25 +15,34 @@
|
|
|
15
15
|
"keywords": [
|
|
16
16
|
"sentori",
|
|
17
17
|
"cli",
|
|
18
|
-
"
|
|
18
|
+
"sourcemaps",
|
|
19
19
|
"error-tracking"
|
|
20
20
|
],
|
|
21
|
+
"type": "module",
|
|
21
22
|
"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'"
|
|
23
|
+
"sentori-cli": "./lib/index.js"
|
|
26
24
|
},
|
|
25
|
+
"main": "./lib/index.js",
|
|
26
|
+
"types": "./lib/index.d.ts",
|
|
27
27
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"lib/",
|
|
29
|
+
"src/",
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=18"
|
|
34
34
|
},
|
|
35
|
-
"
|
|
36
|
-
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"prepack": "bun run build"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"@types/node": "*",
|
|
44
|
+
"typescript": "^5"
|
|
45
|
+
},
|
|
37
46
|
"publishConfig": {
|
|
38
47
|
"access": "public"
|
|
39
48
|
}
|
|
@@ -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
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
|
|
4
|
+
import { uploadSourcemaps } from './upload.js'
|
|
5
|
+
|
|
6
|
+
const HELP = `sentori-cli — upload release artifacts to Sentori
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
sentori-cli upload sourcemap [options] <path...>
|
|
10
|
+
|
|
11
|
+
<path> one or more files or directories. A directory is scanned
|
|
12
|
+
(one level) for *.map and *.js / *.bundle / *.hbc files.
|
|
13
|
+
A file given explicitly is uploaded as-is.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--release <r> release identifier — MUST equal the value the SDK
|
|
17
|
+
reports via init({ release }). Required. A mismatch
|
|
18
|
+
means the dashboard silently can't symbolicate.
|
|
19
|
+
--token <t> Sentori token (or set $SENTORI_TOKEN).
|
|
20
|
+
--api-url <url> Sentori API base (default https://api.sentori.golia.jp,
|
|
21
|
+
or $SENTORI_API_URL). For a self-hosted instance, your
|
|
22
|
+
host. (Accepts --ingest-url as an alias.)
|
|
23
|
+
--dry-run list what would be uploaded; don't upload.
|
|
24
|
+
-h, --help show this help.
|
|
25
|
+
|
|
26
|
+
React Native / Hermes: in a release build the *minified* map (Metro)
|
|
27
|
+
and the *bytecode* map (Hermes) must be composed first —
|
|
28
|
+
npx react-native bundle --platform ios --dev false \\
|
|
29
|
+
--entry-file index.js --bundle-output main.jsbundle \\
|
|
30
|
+
--sourcemap-output main.jsbundle.packager.map
|
|
31
|
+
node node_modules/react-native/scripts/compose-source-maps.js \\
|
|
32
|
+
main.jsbundle.packager.map main.jsbundle.hbc.map -o main.jsbundle.map
|
|
33
|
+
then \`sentori-cli upload sourcemap --release "<app>@<version>+<build>" main.jsbundle.map main.jsbundle\`.
|
|
34
|
+
(Web bundlers emit a usable .map directly — just point at the build dir.)
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
async function main(argv: string[]): Promise<number> {
|
|
38
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
39
|
+
console.log(HELP)
|
|
40
|
+
return 0
|
|
41
|
+
}
|
|
42
|
+
if (argv[0] !== 'upload' || argv[1] !== 'sourcemap') {
|
|
43
|
+
console.error(`unknown command: ${argv.slice(0, 2).join(' ') || '(none)'}\n`)
|
|
44
|
+
console.error(HELP)
|
|
45
|
+
return 2
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let parsed
|
|
49
|
+
try {
|
|
50
|
+
parsed = parseArgs({
|
|
51
|
+
allowPositionals: true,
|
|
52
|
+
args: argv.slice(2),
|
|
53
|
+
options: {
|
|
54
|
+
'api-url': { type: 'string' },
|
|
55
|
+
'dry-run': { type: 'boolean' },
|
|
56
|
+
help: { short: 'h', type: 'boolean' },
|
|
57
|
+
'ingest-url': { type: 'string' }, // alias, kept for older docs
|
|
58
|
+
release: { type: 'string' },
|
|
59
|
+
token: { type: 'string' },
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(`error: ${(e as Error).message}\n`)
|
|
64
|
+
console.error(HELP)
|
|
65
|
+
return 2
|
|
66
|
+
}
|
|
67
|
+
const { positionals, values } = parsed
|
|
68
|
+
|
|
69
|
+
if (values.help) {
|
|
70
|
+
console.log(HELP)
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
const release = values.release
|
|
74
|
+
if (!release) {
|
|
75
|
+
console.error('error: --release is required (must match the SDK’s init({ release }))')
|
|
76
|
+
return 2
|
|
77
|
+
}
|
|
78
|
+
const dryRun = !!values['dry-run']
|
|
79
|
+
const token = values.token ?? process.env.SENTORI_TOKEN
|
|
80
|
+
if (!token && !dryRun) {
|
|
81
|
+
console.error('error: --token (or $SENTORI_TOKEN) is required')
|
|
82
|
+
return 2
|
|
83
|
+
}
|
|
84
|
+
const apiUrl =
|
|
85
|
+
values['api-url'] ??
|
|
86
|
+
values['ingest-url'] ??
|
|
87
|
+
process.env.SENTORI_API_URL ??
|
|
88
|
+
'https://api.sentori.golia.jp'
|
|
89
|
+
if (positionals.length === 0) {
|
|
90
|
+
console.error('error: at least one path (file or directory) is required')
|
|
91
|
+
return 2
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const result = await uploadSourcemaps({
|
|
96
|
+
apiUrl,
|
|
97
|
+
dryRun,
|
|
98
|
+
paths: positionals,
|
|
99
|
+
release,
|
|
100
|
+
token: token ?? '',
|
|
101
|
+
})
|
|
102
|
+
if (dryRun) {
|
|
103
|
+
console.log(
|
|
104
|
+
`would upload ${result.files.length} file(s) to ${apiUrl.replace(/\/+$/, '')}/admin/api/releases/${encodeURIComponent(release)}/sourcemaps:`,
|
|
105
|
+
)
|
|
106
|
+
for (const f of result.files) console.log(` ${f}`)
|
|
107
|
+
} else {
|
|
108
|
+
console.log(`uploaded ${result.uploaded} file(s) for release "${release}"`)
|
|
109
|
+
for (const a of result.artifacts ?? []) console.log(` ${a.name} [${a.kind}]`)
|
|
110
|
+
console.log('done — minified stack traces on this release will now resolve to source.')
|
|
111
|
+
}
|
|
112
|
+
return 0
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(`upload failed: ${(e as Error).message}`)
|
|
115
|
+
return 1
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main(process.argv.slice(2)).then(
|
|
120
|
+
(code) => process.exit(code),
|
|
121
|
+
(e: unknown) => {
|
|
122
|
+
console.error(`fatal: ${(e as Error).message}`)
|
|
123
|
+
process.exit(1)
|
|
124
|
+
},
|
|
125
|
+
)
|
package/src/upload.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
// Files worth uploading: source maps, and the bundle they map (so the
|
|
5
|
+
// dashboard can show the minified line too if a frame falls outside
|
|
6
|
+
// the map). `.jsbundle` (iOS) and `.bundle` (Android) are RN's bundle
|
|
7
|
+
// names; `.hbc` is the Hermes bytecode bundle.
|
|
8
|
+
const UPLOADABLE = /\.(map|js|jsbundle|bundle|hbc)$/i
|
|
9
|
+
|
|
10
|
+
export type UploadOptions = {
|
|
11
|
+
release: string
|
|
12
|
+
token: string
|
|
13
|
+
/** Sentori API base, e.g. https://api.sentori.golia.jp or your host. */
|
|
14
|
+
apiUrl: string
|
|
15
|
+
/** Files or directories. Directories are scanned one level deep. */
|
|
16
|
+
paths: string[]
|
|
17
|
+
dryRun?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type UploadResult = {
|
|
21
|
+
files: string[]
|
|
22
|
+
uploaded?: number
|
|
23
|
+
artifacts?: { kind: string; name: string }[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolve `paths` (files or dirs) to a deduped list of files to upload.
|
|
27
|
+
* A directory contributes its top-level `.map` / `.js` / `.bundle` /
|
|
28
|
+
* `.hbc` files; a file given explicitly is taken as-is (even if its
|
|
29
|
+
* extension isn't in the list — the caller asked for it). */
|
|
30
|
+
export async function collectFiles(paths: string[]): Promise<string[]> {
|
|
31
|
+
const out: string[] = []
|
|
32
|
+
for (const p of paths) {
|
|
33
|
+
const s = await stat(p).catch(() => null)
|
|
34
|
+
if (!s) throw new Error(`no such file or directory: ${p}`)
|
|
35
|
+
if (s.isDirectory()) {
|
|
36
|
+
for (const entry of await readdir(p)) {
|
|
37
|
+
const full = join(p, entry)
|
|
38
|
+
const es = await stat(full).catch(() => null)
|
|
39
|
+
if (es?.isFile() && UPLOADABLE.test(entry)) out.push(full)
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
out.push(p)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const deduped = [...new Set(out)]
|
|
46
|
+
if (deduped.length === 0) {
|
|
47
|
+
throw new Error('no .map / .js / .bundle files found in the given path(s)')
|
|
48
|
+
}
|
|
49
|
+
return deduped
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function uploadSourcemaps(opts: UploadOptions): Promise<UploadResult> {
|
|
53
|
+
const files = await collectFiles(opts.paths)
|
|
54
|
+
if (opts.dryRun) return { files }
|
|
55
|
+
|
|
56
|
+
const form = new FormData()
|
|
57
|
+
for (const f of files) {
|
|
58
|
+
const buf = await readFile(f)
|
|
59
|
+
form.append('file', new Blob([buf]), basename(f))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const base = opts.apiUrl.replace(/\/+$/, '')
|
|
63
|
+
const url = `${base}/admin/api/releases/${encodeURIComponent(opts.release)}/sourcemaps`
|
|
64
|
+
const resp = await fetch(url, {
|
|
65
|
+
body: form,
|
|
66
|
+
headers: { Authorization: `Bearer ${opts.token}` },
|
|
67
|
+
method: 'POST',
|
|
68
|
+
})
|
|
69
|
+
if (!resp.ok) {
|
|
70
|
+
let detail = ''
|
|
71
|
+
try {
|
|
72
|
+
detail = await resp.text()
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore
|
|
75
|
+
}
|
|
76
|
+
throw new Error(
|
|
77
|
+
`${resp.status} ${resp.statusText}${detail ? ` — ${detail.slice(0, 300)}` : ''}`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
const body = (await resp.json().catch(() => ({}))) as {
|
|
81
|
+
artifacts?: { kind: string; name: string }[]
|
|
82
|
+
uploaded?: number
|
|
83
|
+
}
|
|
84
|
+
return { artifacts: body.artifacts, files, uploaded: body.uploaded ?? files.length }
|
|
85
|
+
}
|
package/README.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# @goliapkg/sentori-cli
|
|
2
|
-
|
|
3
|
-
Thin npm wrapper around the [Sentori](https://sentori.golia.jp) Rust CLI. The package downloads the prebuilt binary for your platform on `npm install` (postinstall hook) so `npx @goliapkg/sentori-cli` works the moment you have it in `package.json`.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
npm install -D @goliapkg/sentori-cli
|
|
9
|
-
# or pnpm / yarn
|
|
10
|
-
|
|
11
|
-
npx @goliapkg/sentori-cli upload sourcemap \
|
|
12
|
-
--release myapp@1.2.3+456 \
|
|
13
|
-
--token st_pk_... \
|
|
14
|
-
--ingest-url https://ingest.sentori.your-host.com \
|
|
15
|
-
dist/bundle.js.map
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### Using bun
|
|
19
|
-
|
|
20
|
-
Bun blocks postinstall scripts by default. After `bun add`, run:
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
bun pm trust @goliapkg/sentori-cli
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
once to allow the binary download, or use `bun add --trust @goliapkg/sentori-cli` from the start.
|
|
27
|
-
|
|
28
|
-
Supported platforms:
|
|
29
|
-
|
|
30
|
-
| OS | Arch |
|
|
31
|
-
|----|------|
|
|
32
|
-
| Linux | x64, arm64 |
|
|
33
|
-
| macOS | x64 (Intel), arm64 (Apple Silicon) |
|
|
34
|
-
|
|
35
|
-
If your platform isn't covered, the postinstall hook will print a soft warning and you can fall back to:
|
|
36
|
-
|
|
37
|
-
```sh
|
|
38
|
-
cargo install --git https://github.com/goliajp/sentori --bin sentori-cli
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Skipping postinstall
|
|
42
|
-
|
|
43
|
-
`SENTORI_SKIP_DOWNLOAD=1` skips the binary download — useful in monorepo bootstrap or sandbox CI environments where the binary will be vendored separately.
|
|
44
|
-
|
|
45
|
-
## Verifying the binary
|
|
46
|
-
|
|
47
|
-
Each release artifact ships with a `.sha256` sidecar published next to the `.tar.gz` on the GitHub Release page; the postinstall script downloads only the tarball but you can manually verify:
|
|
48
|
-
|
|
49
|
-
```sh
|
|
50
|
-
curl -L https://github.com/goliajp/sentori/releases/download/cli-v<VERSION>/sentori-cli-cli-v<VERSION>-darwin-arm64.tar.gz.sha256
|
|
51
|
-
```
|
package/bin/sentori-cli.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Phase 17 sub-B: Node entry point for `npx @goliapkg/sentori-cli ...`.
|
|
3
|
-
// Forwards argv + stdio to the prebuilt Rust binary that postinstall
|
|
4
|
-
// dropped at ../vendor/sentori-cli.
|
|
5
|
-
|
|
6
|
-
'use strict'
|
|
7
|
-
|
|
8
|
-
const path = require('node:path')
|
|
9
|
-
const fs = require('node:fs')
|
|
10
|
-
const { spawn } = require('node:child_process')
|
|
11
|
-
|
|
12
|
-
const bin = path.join(__dirname, '..', 'vendor', 'sentori-cli')
|
|
13
|
-
if (!fs.existsSync(bin)) {
|
|
14
|
-
console.error(
|
|
15
|
-
'sentori-cli binary missing — postinstall did not complete. ' +
|
|
16
|
-
'Try: `npm rebuild @goliapkg/sentori-cli` or set SENTORI_SKIP_DOWNLOAD=0 then reinstall.'
|
|
17
|
-
)
|
|
18
|
-
process.exit(127)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit' })
|
|
22
|
-
child.on('exit', (code, signal) => {
|
|
23
|
-
if (signal) {
|
|
24
|
-
process.kill(process.pid, signal)
|
|
25
|
-
} else {
|
|
26
|
-
process.exit(code ?? 1)
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
child.on('error', (e) => {
|
|
30
|
-
console.error('failed to run sentori-cli:', e.message)
|
|
31
|
-
process.exit(1)
|
|
32
|
-
})
|
package/scripts/postinstall.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Phase 17 sub-B: download the prebuilt sentori-cli binary that
|
|
3
|
-
// matches the current platform / arch and place it at ../vendor/.
|
|
4
|
-
//
|
|
5
|
-
// Skips if SENTORI_SKIP_DOWNLOAD=1 (CI, monorepo bootstrap, etc.).
|
|
6
|
-
|
|
7
|
-
'use strict'
|
|
8
|
-
|
|
9
|
-
const fs = require('node:fs')
|
|
10
|
-
const https = require('node:https')
|
|
11
|
-
const path = require('node:path')
|
|
12
|
-
const { spawnSync } = require('node:child_process')
|
|
13
|
-
|
|
14
|
-
const pkg = require(path.join(__dirname, '..', 'package.json'))
|
|
15
|
-
const TAG = `cli-v${pkg.version}`
|
|
16
|
-
|
|
17
|
-
const SUPPORTED = {
|
|
18
|
-
'linux-x64': 'linux-x64',
|
|
19
|
-
'linux-arm64': 'linux-arm64',
|
|
20
|
-
'darwin-x64': 'darwin-x64',
|
|
21
|
-
'darwin-arm64': 'darwin-arm64',
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function detect() {
|
|
25
|
-
return `${process.platform}-${process.arch}`
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function fetch(url, destStream) {
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
const handle = (u) => {
|
|
31
|
-
https
|
|
32
|
-
.get(u, { headers: { 'user-agent': `sentori-cli-installer/${pkg.version}` } }, (res) => {
|
|
33
|
-
const code = res.statusCode || 0
|
|
34
|
-
if (code >= 300 && code < 400 && res.headers.location) {
|
|
35
|
-
return handle(res.headers.location)
|
|
36
|
-
}
|
|
37
|
-
if (code !== 200) {
|
|
38
|
-
return reject(new Error(`HTTP ${code} from ${u}`))
|
|
39
|
-
}
|
|
40
|
-
res.pipe(destStream)
|
|
41
|
-
destStream.on('finish', resolve)
|
|
42
|
-
destStream.on('error', reject)
|
|
43
|
-
})
|
|
44
|
-
.on('error', reject)
|
|
45
|
-
}
|
|
46
|
-
handle(url)
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function main() {
|
|
51
|
-
if (process.env.SENTORI_SKIP_DOWNLOAD === '1') {
|
|
52
|
-
console.log('SENTORI_SKIP_DOWNLOAD=1; skipping sentori-cli binary download')
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const key = detect()
|
|
57
|
-
const slug = SUPPORTED[key]
|
|
58
|
-
if (!slug) {
|
|
59
|
-
console.error(
|
|
60
|
-
`sentori-cli has no prebuilt binary for ${key}. ` +
|
|
61
|
-
`Cargo install instead: cargo install --git https://github.com/goliajp/sentori --bin sentori-cli`
|
|
62
|
-
)
|
|
63
|
-
process.exit(0) // soft fail — user can still cargo install
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const filename = `sentori-cli-${TAG}-${slug}.tar.gz`
|
|
67
|
-
const url = `https://github.com/goliajp/sentori/releases/download/${TAG}/${filename}`
|
|
68
|
-
const vendorDir = path.join(__dirname, '..', 'vendor')
|
|
69
|
-
fs.mkdirSync(vendorDir, { recursive: true })
|
|
70
|
-
const tarball = path.join(vendorDir, 'sentori-cli.tar.gz')
|
|
71
|
-
|
|
72
|
-
console.log(`sentori-cli ${pkg.version}: downloading ${slug} from GitHub Release`)
|
|
73
|
-
await fetch(url, fs.createWriteStream(tarball))
|
|
74
|
-
|
|
75
|
-
const tar = spawnSync('tar', ['-xzf', tarball, '-C', vendorDir], { stdio: 'inherit' })
|
|
76
|
-
if (tar.status !== 0) {
|
|
77
|
-
console.error('tar -xzf failed; is `tar` on PATH?')
|
|
78
|
-
process.exit(1)
|
|
79
|
-
}
|
|
80
|
-
fs.unlinkSync(tarball)
|
|
81
|
-
|
|
82
|
-
const bin = path.join(vendorDir, 'sentori-cli')
|
|
83
|
-
fs.chmodSync(bin, 0o755)
|
|
84
|
-
console.log(`✓ sentori-cli ${pkg.version} ready (${slug})`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
main().catch((e) => {
|
|
88
|
-
console.error('sentori-cli install failed:', e.message)
|
|
89
|
-
process.exit(1)
|
|
90
|
-
})
|