@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 @@
|
|
|
1
|
+
{"version":3,"file":"source-bundle.js","sourceRoot":"","sources":["../src/source-bundle.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,EAAE;AACF,sEAAsE;AACtE,2CAA2C;AAC3C,iEAAiE;AACjE,oEAAoE;AACpE,qDAAqD;AACrD,EAAE;AACF,iEAAiE;AACjE,wDAAwD;AACxD,mDAAmD;AACnD,EAAE;AACF,wCAAwC;AACxC,wDAAwD;AACxD,oEAAoE;AAEpE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC9E,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,OAAO,EAAE,MAAM,WAAW,CAAA;AAI1D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAA;AAE7C;;;;+BAI+B;AAC/B,MAAM,UAAU,GAAwC;IACtD,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC;IACzB,GAAG,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC;CAC3C,CAAA;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,MAAM;IACN,cAAc;IACd,MAAM;IACN,OAAO;IACP,aAAa;IACb,SAAS;IACT,QAAQ;IACR,QAAQ;CACT,CAAC,CAAA;AAmBF,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAA+B;IAE/B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,+CAA+C,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAA;IACnF,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IACpE,CAAC;IAED,mEAAmE;IACnE,8DAA8D;IAC9D,IAAI,WAAW,GAAG,IAAI,CAAC,IAAI,CAAA;IAC3B,IAAI,OAA0C,CAAA;IAC9C,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAChC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;YACrB,MAAM,KAAK,GAAG,MAAM,wBAAwB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;YACtE,WAAW,GAAG,KAAK,CAAC,IAAI,CAAA;YACxB,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;QACzB,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,CAAC,CAAA;QAC3D,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IACxD,CAAC;IAED,IAAI,CAAC;QACH,OAAO,MAAM,mBAAmB,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;IAClE,CAAC;YAAS,CAAC;QACT,IAAI,OAAO;YAAE,MAAM,OAAO,EAAE,CAAA;IAC9B,CAAC;AACH,CAAC;AAED,KAAK,UAAU,mBAAmB,CAChC,IAA+B;IAE/B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACtC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IACrE,gEAAgE;IAChE,kEAAkE;IAClE,iBAAiB;IACjB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,IAAI,6DAA6D;YACvE,iEAAiE,CACpE,CAAA;IACH,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IACrF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAC5C,MAAM,CAAC,GAAG,IAAI,eAAe,EAAE,CAAA;IAC/B,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;IAC9B,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;IAChC,IAAI,IAAI,CAAC,MAAM;QAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IAC7C,MAAM,GAAG,GAAG,GAAG,IAAI,uBAAuB,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAA;IAE7G,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC5B,IAAI;QACJ,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE;YACrC,cAAc,EAAE,kBAAkB;SACnC;QACD,MAAM,EAAE,MAAM;KACf,CAAC,CAAA;IACF,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,IAAI,MAAM,GAAG,EAAE,CAAA;QACf,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CACjF,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GAAY,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;IACzC,IACE,CAAC,MAAM;QACP,OAAO,MAAM,KAAK,QAAQ;QAC1B,OAAQ,MAAoC,CAAC,WAAW,KAAK,QAAQ,EACrE,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACzE,CAAC;IACD,MAAM,CAAC,GAAG,MAAkE,CAAA;IAC5E,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAA;AAC7E,CAAC;AAED;;;;;gEAKgE;AAChE,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,GAAW,EACX,QAA2B;IAE3B,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAA;IACjC,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;IACjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,MAAM,QAAQ,kBAAkB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,KAAK;YACtE,kEAAkE,CACrE,CAAA;IACH,CAAC;IACD,KAAK,CAAC,IAAI,EAAE,CAAA;IAEZ,mEAAmE;IACnE,8DAA8D;IAC9D,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAA;IAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAA;IACvC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,gBAAgB,CAAC,CAAA;IAC1D,MAAM,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3C,MAAM,CAAC,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAA;IAC5E,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CACb,sBAAsB,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAChF,CAAA;IACH,CAAC;IACD,OAAO;QACL,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;oBACxC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;wBACxB,MAAM,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAA;oBACpC,CAAC;gBACH,CAAC,CAAC,CAAA;gBACF,MAAM,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;YACjC,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;QACH,CAAC;QACD,IAAI,EAAE,WAAW;KAClB,CAAA;AACH,CAAC;AAED,KAAK,UAAU,IAAI,CACjB,IAAY,EACZ,GAAW,EACX,IAAc,EACd,GAAa;IAEb,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3D,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YAChE,6DAA6D;YAC7D,gEAAgE;YAChE,uCAAuC;YACvC,SAAQ;QACV,CAAC;QACD,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,SAAQ;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;QAC9B,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACpB,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;QACnC,CAAC;aAAM,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YACtB,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAA;YAClC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC5C,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;YACzD,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,KAAK,GAAG,KAAK;IACvD,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAA;IAC/C,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAA;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;AACH,CAAC"}
|
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://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,oEAAoE;IACpE,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,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sentori CLI — upload source maps
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.6.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": "^6"
|
|
46
|
+
},
|
|
37
47
|
"publishConfig": {
|
|
38
48
|
"access": "public"
|
|
39
49
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { formatIssueLine, issueList, issuePatch } from '../issue.js'
|
|
4
|
+
|
|
5
|
+
const cfg = {
|
|
6
|
+
apiUrl: 'https://api.example.com/',
|
|
7
|
+
projectId: 'proj-1',
|
|
8
|
+
token: 'sk_test',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const sample = {
|
|
12
|
+
errorType: 'TypeError',
|
|
13
|
+
eventCount: 17,
|
|
14
|
+
id: 'issue-1',
|
|
15
|
+
lastSeen: '2026-05-13T00:00:00Z',
|
|
16
|
+
messageSample: 'undefined is not an object',
|
|
17
|
+
status: 'active' as const,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const origFetch = globalThis.fetch
|
|
21
|
+
let calls: { body: string | undefined; headers: Headers; method: string; url: string }[]
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
calls = []
|
|
24
|
+
})
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
globalThis.fetch = origFetch
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
function mockFetch(respBody: string, init: ResponseInit = { status: 200 }): void {
|
|
30
|
+
globalThis.fetch = (async (url: Request | string | URL, opts?: RequestInit) => {
|
|
31
|
+
calls.push({
|
|
32
|
+
body: typeof opts?.body === 'string' ? opts.body : undefined,
|
|
33
|
+
headers: new Headers(opts?.headers),
|
|
34
|
+
method: opts?.method ?? 'GET',
|
|
35
|
+
url: String(url),
|
|
36
|
+
})
|
|
37
|
+
return new Response(respBody, {
|
|
38
|
+
headers: { 'content-type': 'application/json' },
|
|
39
|
+
...init,
|
|
40
|
+
})
|
|
41
|
+
}) as typeof fetch
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('issueList', () => {
|
|
45
|
+
test('GETs /admin/api/projects/<id>/issues with query params + Bearer', async () => {
|
|
46
|
+
mockFetch(JSON.stringify([sample]))
|
|
47
|
+
const rows = await issueList({ config: cfg, limit: 20, status: 'active' })
|
|
48
|
+
expect(calls).toHaveLength(1)
|
|
49
|
+
expect(calls[0]?.method).toBe('GET')
|
|
50
|
+
expect(calls[0]?.url).toBe(
|
|
51
|
+
'https://api.example.com/admin/api/projects/proj-1/issues?status=active&limit=20',
|
|
52
|
+
)
|
|
53
|
+
expect(calls[0]?.headers.get('authorization')).toBe('Bearer sk_test')
|
|
54
|
+
expect(rows).toHaveLength(1)
|
|
55
|
+
expect(rows[0]?.id).toBe('issue-1')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('omits empty query params', async () => {
|
|
59
|
+
mockFetch(JSON.stringify([]))
|
|
60
|
+
await issueList({ config: cfg })
|
|
61
|
+
expect(calls[0]?.url).toBe('https://api.example.com/admin/api/projects/proj-1/issues')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('throws with server detail on non-2xx', async () => {
|
|
65
|
+
mockFetch('forbidden', { status: 403, statusText: 'Forbidden' })
|
|
66
|
+
await expect(issueList({ config: cfg })).rejects.toThrow('403 Forbidden — forbidden')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('issuePatch', () => {
|
|
71
|
+
test('PATCHes /issues/<id> with the body', async () => {
|
|
72
|
+
mockFetch(JSON.stringify({ ...sample, status: 'resolved' }))
|
|
73
|
+
const updated = await issuePatch(cfg, 'issue-1', {
|
|
74
|
+
resolvedInRelease: 'app@1.2.4+457',
|
|
75
|
+
status: 'resolved',
|
|
76
|
+
})
|
|
77
|
+
expect(calls[0]?.method).toBe('PATCH')
|
|
78
|
+
expect(calls[0]?.url).toBe('https://api.example.com/admin/api/projects/proj-1/issues/issue-1')
|
|
79
|
+
expect(JSON.parse(calls[0]?.body ?? '{}')).toEqual({
|
|
80
|
+
resolvedInRelease: 'app@1.2.4+457',
|
|
81
|
+
status: 'resolved',
|
|
82
|
+
})
|
|
83
|
+
expect(updated.status).toBe('resolved')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('formatIssueLine', () => {
|
|
88
|
+
test('renders id + status + title + event count on one line', () => {
|
|
89
|
+
const line = formatIssueLine(sample)
|
|
90
|
+
expect(line).toContain('issue-1')
|
|
91
|
+
expect(line).toContain('active')
|
|
92
|
+
expect(line).toContain('TypeError: undefined is not an object')
|
|
93
|
+
expect(line).toContain('17×')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { buildTools } from '../mcp.js'
|
|
4
|
+
|
|
5
|
+
const ADMIN = {
|
|
6
|
+
apiUrl: 'https://api.example.com/',
|
|
7
|
+
projectId: 'proj-uuid',
|
|
8
|
+
token: 'sk_test',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const origFetch = globalThis.fetch
|
|
12
|
+
let calls: { body: string | null; headers: Headers; method: string; url: string }[]
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
calls = []
|
|
16
|
+
})
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
globalThis.fetch = origFetch
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
function mockFetch(status = 200, body: unknown = {}): void {
|
|
22
|
+
globalThis.fetch = (async (url: Request | string | URL, init?: RequestInit) => {
|
|
23
|
+
const b = init?.body
|
|
24
|
+
calls.push({
|
|
25
|
+
body: typeof b === 'string' ? b : null,
|
|
26
|
+
headers: new Headers(init?.headers),
|
|
27
|
+
method: init?.method ?? 'GET',
|
|
28
|
+
url: String(url),
|
|
29
|
+
})
|
|
30
|
+
return new Response(status === 204 ? '' : JSON.stringify(body), { status })
|
|
31
|
+
}) as typeof fetch
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('buildTools', () => {
|
|
35
|
+
test('returns the expected set of tools with required input schema fields', () => {
|
|
36
|
+
const tools = buildTools()
|
|
37
|
+
const names = new Set(tools.map((t) => t.name))
|
|
38
|
+
for (const expected of [
|
|
39
|
+
'sentori_issue_list',
|
|
40
|
+
'sentori_issue_get',
|
|
41
|
+
'sentori_issue_comment',
|
|
42
|
+
'sentori_issue_transition',
|
|
43
|
+
'sentori_issue_assign',
|
|
44
|
+
'sentori_issue_set_priority',
|
|
45
|
+
'sentori_issue_set_labels',
|
|
46
|
+
'sentori_issue_watch',
|
|
47
|
+
]) {
|
|
48
|
+
expect(names.has(expected)).toBe(true)
|
|
49
|
+
}
|
|
50
|
+
for (const t of tools) {
|
|
51
|
+
// Every tool advertises a description + an input schema.
|
|
52
|
+
expect(typeof t.description).toBe('string')
|
|
53
|
+
expect(t.inputSchema).toBeDefined()
|
|
54
|
+
expect((t.inputSchema as { type?: string }).type).toBe('object')
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('tool handlers proxy to admin API', () => {
|
|
60
|
+
test('sentori_issue_list calls GET .../issues with filters', async () => {
|
|
61
|
+
const tools = buildTools()
|
|
62
|
+
const list = tools.find((t) => t.name === 'sentori_issue_list')!
|
|
63
|
+
mockFetch(200, [])
|
|
64
|
+
await list.handler(
|
|
65
|
+
{ limit: 25, priority: 'p0', projectId: 'proj-uuid', status: 'active' },
|
|
66
|
+
ADMIN,
|
|
67
|
+
)
|
|
68
|
+
expect(calls.length).toBe(1)
|
|
69
|
+
expect(calls[0]!.url).toContain('/admin/api/projects/proj-uuid/issues?')
|
|
70
|
+
expect(calls[0]!.url).toContain('status=active')
|
|
71
|
+
expect(calls[0]!.url).toContain('priority=p0')
|
|
72
|
+
expect(calls[0]!.url).toContain('limit=25')
|
|
73
|
+
expect(calls[0]!.method).toBe('GET')
|
|
74
|
+
expect(calls[0]!.headers.get('authorization')).toBe('Bearer sk_test')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('sentori_issue_get fetches issue + activity in parallel', async () => {
|
|
78
|
+
const tools = buildTools()
|
|
79
|
+
const get = tools.find((t) => t.name === 'sentori_issue_get')!
|
|
80
|
+
mockFetch(200, { stub: true })
|
|
81
|
+
await get.handler({ issueId: 'iss-uuid', projectId: 'proj-uuid' }, ADMIN)
|
|
82
|
+
expect(calls.length).toBe(2)
|
|
83
|
+
const paths = calls.map((c) => c.url).sort()
|
|
84
|
+
expect(paths[0]).toContain('/admin/api/projects/proj-uuid/issues/iss-uuid')
|
|
85
|
+
expect(paths[1]).toContain('/admin/api/projects/proj-uuid/issues/iss-uuid/activity')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('sentori_issue_transition PATCHes status', async () => {
|
|
89
|
+
const tools = buildTools()
|
|
90
|
+
const t = tools.find((x) => x.name === 'sentori_issue_transition')!
|
|
91
|
+
mockFetch(200, { id: 'x' })
|
|
92
|
+
await t.handler({ issueId: 'iss', projectId: 'proj', status: 'resolved' }, ADMIN)
|
|
93
|
+
expect(calls[0]!.method).toBe('PATCH')
|
|
94
|
+
expect(JSON.parse(calls[0]!.body!).status).toBe('resolved')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('sentori_issue_set_labels rejects non-string array entries', async () => {
|
|
98
|
+
const tools = buildTools()
|
|
99
|
+
const t = tools.find((x) => x.name === 'sentori_issue_set_labels')!
|
|
100
|
+
await expect(
|
|
101
|
+
t.handler({ issueId: 'iss', labels: ['ok', 42], projectId: 'proj' }, ADMIN),
|
|
102
|
+
).rejects.toThrow(/each label/)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('sentori_issue_watch=true → PUT, watch=false → DELETE', async () => {
|
|
106
|
+
const tools = buildTools()
|
|
107
|
+
const t = tools.find((x) => x.name === 'sentori_issue_watch')!
|
|
108
|
+
mockFetch(204)
|
|
109
|
+
await t.handler({ issueId: 'iss', projectId: 'proj', watch: true }, ADMIN)
|
|
110
|
+
expect(calls[0]!.method).toBe('PUT')
|
|
111
|
+
calls.length = 0
|
|
112
|
+
await t.handler({ issueId: 'iss', projectId: 'proj', watch: false }, ADMIN)
|
|
113
|
+
expect(calls[0]!.method).toBe('DELETE')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('handler surfaces a non-2xx as an Error', async () => {
|
|
117
|
+
const tools = buildTools()
|
|
118
|
+
const t = tools.find((x) => x.name === 'sentori_issue_comment')!
|
|
119
|
+
mockFetch(500, { error: 'boom' })
|
|
120
|
+
await expect(
|
|
121
|
+
t.handler({ body: 'hello', issueId: 'iss', projectId: 'proj' }, ADMIN),
|
|
122
|
+
).rejects.toThrow(/500/)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
projectId: 'proj-1',
|
|
17
|
+
token: 'sk_test',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let dir = ''
|
|
21
|
+
const origFetch = globalThis.fetch
|
|
22
|
+
let calls: { body: Buffer | null; headers: Headers; method: string; url: string }[]
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
dir = await mkdtemp(join(tmpdir(), 'sentori-cli-native-'))
|
|
25
|
+
calls = []
|
|
26
|
+
})
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
globalThis.fetch = origFetch
|
|
29
|
+
await rm(dir, { force: true, recursive: true })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function mockFetch(status = 200, respBody = ''): void {
|
|
33
|
+
globalThis.fetch = (async (url: Request | string | URL, init?: RequestInit) => {
|
|
34
|
+
const body = init?.body
|
|
35
|
+
calls.push({
|
|
36
|
+
body: body instanceof Uint8Array ? Buffer.from(body) : null,
|
|
37
|
+
headers: new Headers(init?.headers),
|
|
38
|
+
method: init?.method ?? 'GET',
|
|
39
|
+
url: String(url),
|
|
40
|
+
})
|
|
41
|
+
return new Response(respBody, { status })
|
|
42
|
+
}) as typeof fetch
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('uploadMapping', () => {
|
|
46
|
+
test('POSTs raw bytes to /mappings with Bearer + release query', async () => {
|
|
47
|
+
mockFetch()
|
|
48
|
+
const path = join(dir, 'mapping.txt')
|
|
49
|
+
await writeFile(path, '# pg_map_id: abc\nfoo.bar -> a.b:\n')
|
|
50
|
+
await uploadMapping({ ...ADMIN, path, release: 'app@1.0.0+1' })
|
|
51
|
+
expect(calls).toHaveLength(1)
|
|
52
|
+
expect(calls[0]?.method).toBe('POST')
|
|
53
|
+
expect(calls[0]?.url).toBe(
|
|
54
|
+
'https://api.example.com/admin/api/projects/proj-1/mappings?release=app%401.0.0%2B1',
|
|
55
|
+
)
|
|
56
|
+
expect(calls[0]?.headers.get('authorization')).toBe('Bearer sk_test')
|
|
57
|
+
expect(calls[0]?.headers.get('content-type')).toBe('application/octet-stream')
|
|
58
|
+
expect(calls[0]?.body?.toString()).toContain('pg_map_id')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('sets x-sentori-debug-id when given', async () => {
|
|
62
|
+
mockFetch()
|
|
63
|
+
const path = join(dir, 'mapping.txt')
|
|
64
|
+
await writeFile(path, 'x')
|
|
65
|
+
await uploadMapping({ ...ADMIN, debugId: '1234-uuid', path })
|
|
66
|
+
expect(calls[0]?.headers.get('x-sentori-debug-id')).toBe('1234-uuid')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('throws on empty file', async () => {
|
|
70
|
+
const path = join(dir, 'empty.txt')
|
|
71
|
+
await writeFile(path, '')
|
|
72
|
+
await expect(uploadMapping({ ...ADMIN, path })).rejects.toThrow('empty mapping file')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('throws on non-2xx with server detail', async () => {
|
|
76
|
+
mockFetch(403, 'forbidden')
|
|
77
|
+
const path = join(dir, 'mapping.txt')
|
|
78
|
+
await writeFile(path, 'x')
|
|
79
|
+
await expect(uploadMapping({ ...ADMIN, path })).rejects.toThrow('403')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('uploadDsym (explicit single-slice)', () => {
|
|
84
|
+
test('POSTs to /dsyms with x-sentori-debug-id, x-sentori-arch, release + objectName', async () => {
|
|
85
|
+
mockFetch()
|
|
86
|
+
const path = join(dir, 'Foo.dSYM/Contents/Resources/DWARF/Foo')
|
|
87
|
+
await mkdir(join(dir, 'Foo.dSYM/Contents/Resources/DWARF'), { recursive: true })
|
|
88
|
+
await writeFile(path, 'macho-bytes')
|
|
89
|
+
const r = await uploadDsym({
|
|
90
|
+
...ADMIN,
|
|
91
|
+
arch: 'arm64',
|
|
92
|
+
debugId: '1234abcd-1234-1234-1234-1234567890ab',
|
|
93
|
+
objectName: 'Foo',
|
|
94
|
+
path,
|
|
95
|
+
release: 'app@1.0.0+1',
|
|
96
|
+
})
|
|
97
|
+
expect(calls).toHaveLength(1)
|
|
98
|
+
expect(calls[0]?.method).toBe('POST')
|
|
99
|
+
expect(calls[0]?.url).toContain('/admin/api/projects/proj-1/dsyms')
|
|
100
|
+
expect(calls[0]?.url).toContain('release=app%401.0.0%2B1')
|
|
101
|
+
expect(calls[0]?.url).toContain('objectName=Foo')
|
|
102
|
+
expect(calls[0]?.headers.get('x-sentori-debug-id')).toBe('1234ABCD-1234-1234-1234-1234567890AB')
|
|
103
|
+
expect(calls[0]?.headers.get('x-sentori-arch')).toBe('arm64')
|
|
104
|
+
expect(r.slices).toEqual([{ arch: 'arm64', debugId: '1234ABCD-1234-1234-1234-1234567890AB' }])
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('helpers', () => {
|
|
109
|
+
test('dwarfBinariesIn walks a .dSYM bundle', async () => {
|
|
110
|
+
const bundle = join(dir, 'Foo.dSYM')
|
|
111
|
+
const dwarfDir = join(bundle, 'Contents/Resources/DWARF')
|
|
112
|
+
await mkdir(dwarfDir, { recursive: true })
|
|
113
|
+
await writeFile(join(dwarfDir, 'Foo'), 'x')
|
|
114
|
+
await writeFile(join(dwarfDir, '.DS_Store'), 'noise')
|
|
115
|
+
const found = dwarfBinariesIn(bundle)
|
|
116
|
+
expect(found.map((p) => p.replace(bundle + '/', ''))).toEqual([
|
|
117
|
+
'Contents/Resources/DWARF/Foo',
|
|
118
|
+
])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('dwarfBinariesIn on a plain file returns just that file', async () => {
|
|
122
|
+
const f = join(dir, 'binary')
|
|
123
|
+
await writeFile(f, 'x')
|
|
124
|
+
expect(dwarfBinariesIn(f)).toEqual([f])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('dsymSlicesFromDwarfdump returns [] when dwarfdump fails (or is absent)', () => {
|
|
128
|
+
// pass a path that doesn't exist; dwarfdump exits non-zero (or isn't installed)
|
|
129
|
+
const slices = dsymSlicesFromDwarfdump(join(dir, 'nope.dSYM'))
|
|
130
|
+
expect(slices).toEqual([])
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
7
|
+
|
|
8
|
+
import { buildSourceBundleFromDir } from '../source-bundle.js'
|
|
9
|
+
|
|
10
|
+
let dir = ''
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
dir = await mkdtemp(join(tmpdir(), 'sentori-cli-fromdir-'))
|
|
13
|
+
await mkdir(join(dir, 'Sources', 'MyApp'), { recursive: true })
|
|
14
|
+
await writeFile(join(dir, 'Sources', 'MyApp', 'View.swift'), '// swift\n')
|
|
15
|
+
await writeFile(join(dir, 'Sources', 'MyApp', 'Util.m'), '// objc\n')
|
|
16
|
+
await mkdir(join(dir, 'android', 'app', 'src'), { recursive: true })
|
|
17
|
+
await writeFile(join(dir, 'android', 'app', 'src', 'MainActivity.kt'), '// kotlin\n')
|
|
18
|
+
// Skips: hidden + skip-list + non-matching ext.
|
|
19
|
+
await mkdir(join(dir, 'node_modules', 'foo'), { recursive: true })
|
|
20
|
+
await writeFile(join(dir, 'node_modules', 'foo', 'index.swift'), '// should skip\n')
|
|
21
|
+
await mkdir(join(dir, 'Pods'), { recursive: true })
|
|
22
|
+
await writeFile(join(dir, 'Pods', 'a.swift'), '// should skip\n')
|
|
23
|
+
await mkdir(join(dir, '.git'), { recursive: true })
|
|
24
|
+
await writeFile(join(dir, '.git', 'config'), '\n')
|
|
25
|
+
await writeFile(join(dir, 'README.md'), '# readme\n')
|
|
26
|
+
})
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await rm(dir, { force: true, recursive: true })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function listEntries(tarPath: string): string[] {
|
|
32
|
+
const r = spawnSync('tar', ['-tzf', tarPath])
|
|
33
|
+
if (r.status !== 0) throw new Error(`tar -t failed: ${r.stderr?.toString()}`)
|
|
34
|
+
return r.stdout
|
|
35
|
+
.toString()
|
|
36
|
+
.split('\n')
|
|
37
|
+
.map((s) => s.trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('buildSourceBundleFromDir', () => {
|
|
42
|
+
test('ios bundle includes .swift + .m + .h, excludes android + node_modules + Pods', async () => {
|
|
43
|
+
const { cleanup, path } = await buildSourceBundleFromDir(dir, 'ios')
|
|
44
|
+
try {
|
|
45
|
+
const entries = listEntries(path)
|
|
46
|
+
expect(entries).toContain('Sources/MyApp/View.swift')
|
|
47
|
+
expect(entries).toContain('Sources/MyApp/Util.m')
|
|
48
|
+
// No .kt
|
|
49
|
+
expect(entries.find((e) => e.endsWith('.kt'))).toBeUndefined()
|
|
50
|
+
// No node_modules
|
|
51
|
+
expect(entries.find((e) => e.startsWith('node_modules/'))).toBeUndefined()
|
|
52
|
+
// No Pods
|
|
53
|
+
expect(entries.find((e) => e.startsWith('Pods/'))).toBeUndefined()
|
|
54
|
+
// No .git
|
|
55
|
+
expect(entries.find((e) => e.startsWith('.git/'))).toBeUndefined()
|
|
56
|
+
// No README
|
|
57
|
+
expect(entries.find((e) => e.endsWith('.md'))).toBeUndefined()
|
|
58
|
+
} finally {
|
|
59
|
+
await cleanup()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('android bundle picks .kt + .java only', async () => {
|
|
64
|
+
const { cleanup, path } = await buildSourceBundleFromDir(dir, 'android')
|
|
65
|
+
try {
|
|
66
|
+
const entries = listEntries(path)
|
|
67
|
+
expect(entries).toContain('android/app/src/MainActivity.kt')
|
|
68
|
+
expect(entries.find((e) => e.endsWith('.swift'))).toBeUndefined()
|
|
69
|
+
expect(entries.find((e) => e.endsWith('.m'))).toBeUndefined()
|
|
70
|
+
} finally {
|
|
71
|
+
await cleanup()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('empty directory of the right ext throws a clear error', async () => {
|
|
76
|
+
const empty = await mkdtemp(join(tmpdir(), 'sentori-cli-empty-'))
|
|
77
|
+
try {
|
|
78
|
+
await expect(buildSourceBundleFromDir(empty, 'ios')).rejects.toThrow(
|
|
79
|
+
/no ios source files/,
|
|
80
|
+
)
|
|
81
|
+
} finally {
|
|
82
|
+
await rm(empty, { force: true, recursive: true })
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
})
|