@rootnode-app/cli 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,41 @@
1
- # Temporary Holding Version
1
+ # @rootnode-app/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Upload the files that turn release crashes into real code lines —
4
+ iOS dSYMs, Android ProGuard/R8 mappings, native debug files and JS source maps —
5
+ to [Rootnode](https://rootnode.app). Authenticates with the app's analytics
6
+ key; nothing else to configure.
7
+
8
+ ## Expo / EAS (automatic)
9
+
10
+ ```jsonc
11
+ // app.json → "expo"
12
+ "plugins": ["@rootnode-app/react-native"]
13
+
14
+ // package.json → "scripts"
15
+ "eas-build-on-success": "npx @rootnode-app/cli eas"
16
+ ```
17
+
18
+ After every successful EAS build it finds that build's dSYMs, mapping, bundle
19
+ and source map and uploads them under the release the SDK reports
20
+ (`bundleId@version+build`). Uses `EXPO_PUBLIC_ROOTNODE_KEY` (or `ROOTNODE_KEY`).
21
+ Never fails the build unless you pass `--strict`.
22
+
23
+ ## Any build
24
+
25
+ ```bash
26
+ export ROOTNODE_KEY=rn_…
27
+ npx @rootnode-app/cli upload-symbols --target ios ./build/App.xcarchive
28
+ npx @rootnode-app/cli upload-symbols --target android app/build/outputs/mapping/release/mapping.txt
29
+ npx @rootnode-app/cli upload-sourcemaps --target react-native \
30
+ --bundle index.android.bundle --map index.android.bundle.map \
31
+ --release com.acme.shop@2.1.0+47 --dist 47
32
+ ```
33
+
34
+ `--target`: which target of the app the crashes come from (`react-native`
35
+ default, `ios`, `android`, …). `--host` for self-hosted Rootnode.
36
+
37
+ Symbols apply to crashes received **after** the upload — upload during the
38
+ build, not after the first crash. Check the result in the panel: Setup →
39
+ target → Symbols.
40
+
41
+ MIT © Doğan Aktı. Bundles the BSD-3-licensed upload engine at a pinned version.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const child_process_1 = require("child_process");
5
+ const discover_1 = require("./discover");
6
+ const plan_1 = require("./plan");
7
+ async function resolveTarget(o) {
8
+ const res = await fetch(`${o.host}/api/0/rootnode/targets/${encodeURIComponent(o.target)}`, {
9
+ headers: { Authorization: `Bearer ${o.key}` },
10
+ });
11
+ const body = (await res.json().catch(() => ({})));
12
+ if (res.status === 401)
13
+ throw new Error('the key was rejected — use the app\'s analytics key (rn_…)');
14
+ if (!res.ok || !body.organization || !body.project)
15
+ throw new Error(body.detail ?? `target lookup failed (http ${res.status})`);
16
+ return { organization: body.organization, project: body.project };
17
+ }
18
+ function uploaderPath() {
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
20
+ return require('@sentry/cli').getPath();
21
+ }
22
+ /** The uploader talks about itself; our users only ever deal with Rootnode. */
23
+ const rebrand = (text) => text.replace(/\bsentry-cli\b/gi, 'rootnode').replace(/\bSentry\b/g, 'Rootnode');
24
+ function execute(runs, env) {
25
+ let ok = true;
26
+ for (const r of runs) {
27
+ console.log(`rootnode: uploading ${r.label}…`);
28
+ const p = (0, child_process_1.spawnSync)(uploaderPath(), r.args, { stdio: ['inherit', 'pipe', 'pipe'], env: { ...process.env, ...env }, encoding: 'utf8' });
29
+ process.stdout.write(rebrand(p.stdout ?? ''));
30
+ process.stderr.write(rebrand(p.stderr ?? ''));
31
+ if (p.status !== 0) {
32
+ ok = false;
33
+ console.error(`rootnode: ${r.label} failed (exit ${p.status ?? p.signal})`);
34
+ }
35
+ }
36
+ return ok;
37
+ }
38
+ function runsFor(out) {
39
+ const runs = (0, plan_1.symbolRuns)(out.debugPaths);
40
+ if (out.bundle && out.map && out.release)
41
+ runs.push((0, plan_1.sourcemapRun)(out.bundle, out.map, out.release, out.dist));
42
+ return runs;
43
+ }
44
+ async function main(argv) {
45
+ const cmd = (0, plan_1.parseArgs)(argv, process.env);
46
+ const o = cmd.options;
47
+ if (cmd.name === 'help') {
48
+ console.log(plan_1.HELP);
49
+ return 0;
50
+ }
51
+ if (!o.key)
52
+ throw new Error('no key — pass --key rn_… or set ROOTNODE_KEY (EXPO_PUBLIC_ROOTNODE_KEY works too)');
53
+ if (cmd.name === 'eas') {
54
+ // EAS runs this hook once per platform; outside EAS both are tried.
55
+ const platform = process.env.EAS_BUILD_PLATFORM;
56
+ const root = process.cwd();
57
+ const outs = [platform !== 'android' && (0, discover_1.discoverIos)(root), platform !== 'ios' && (0, discover_1.discoverAndroid)(root)].filter(Boolean);
58
+ for (const out of outs)
59
+ for (const n of out.notes)
60
+ console.log(`rootnode: ${out.platform}: ${n}`);
61
+ const runs = outs.flatMap(runsFor);
62
+ if (!runs.length) {
63
+ console.log('rootnode: nothing to upload');
64
+ return 0;
65
+ }
66
+ try {
67
+ const ok = execute(runs, (0, plan_1.uploaderEnv)(o, await resolveTarget(o)));
68
+ return ok || !o.strict ? 0 : 1;
69
+ }
70
+ catch (e) {
71
+ console.error(`rootnode: ${e.message}`);
72
+ return o.strict ? 1 : 0; // an upload problem must not fail a good build
73
+ }
74
+ }
75
+ const target = await resolveTarget(o);
76
+ if (cmd.name === 'upload-symbols') {
77
+ if (!o.paths.length)
78
+ throw new Error('give the dSYM / .xcarchive / .so folder / mapping.txt paths');
79
+ return execute((0, plan_1.symbolRuns)(o.paths), (0, plan_1.uploaderEnv)(o, target)) ? 0 : 1;
80
+ }
81
+ if (!o.bundle || !o.map || !o.release)
82
+ throw new Error('upload-sourcemaps needs --bundle, --map and --release');
83
+ return execute([(0, plan_1.sourcemapRun)(o.bundle, o.map, o.release, o.dist)], (0, plan_1.uploaderEnv)(o, target)) ? 0 : 1;
84
+ }
85
+ main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
86
+ console.error(`rootnode: ${e.message}`);
87
+ process.exit(1);
88
+ });
@@ -0,0 +1,19 @@
1
+ /** What one native build left behind that crash readability needs. */
2
+ export interface BuildOutputs {
3
+ platform: 'ios' | 'android';
4
+ debugPaths: string[];
5
+ bundle: string | null;
6
+ map: string | null;
7
+ release: string | null;
8
+ dist: string | null;
9
+ notes: string[];
10
+ }
11
+ export type PlistReader = (file: string) => Record<string, any> | null;
12
+ /** Info.plist may be binary — plutil (present on every macOS build machine) reads both. */
13
+ export declare const readPlist: PlistReader;
14
+ /** iOS: the .xcarchive EAS/xcodebuild produced under ios/build, plus the map our config plugin writes next to it. */
15
+ /** Newest .xcarchive in Xcode's Archives folder (where fastlane/gym archives by default), if made in the last 3 hours. */
16
+ export declare function newestXcodeArchive(home: string, now?: number): string | null;
17
+ export declare function discoverIos(root: string, plist?: PlistReader, home?: string, override?: string | undefined): BuildOutputs;
18
+ /** Android: R8 mapping, the release bundle + its (Hermes-composed) source map, version from build.gradle. */
19
+ export declare function discoverAndroid(root: string): BuildOutputs;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readPlist = void 0;
4
+ exports.newestXcodeArchive = newestXcodeArchive;
5
+ exports.discoverIos = discoverIos;
6
+ exports.discoverAndroid = discoverAndroid;
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = require("fs");
9
+ const os_1 = require("os");
10
+ const path_1 = require("path");
11
+ const plan_1 = require("./plan");
12
+ /** Info.plist may be binary — plutil (present on every macOS build machine) reads both. */
13
+ const readPlist = (file) => {
14
+ try {
15
+ return JSON.parse((0, child_process_1.execFileSync)('plutil', ['-convert', 'json', '-o', '-', file], { encoding: 'utf8' }));
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ };
21
+ exports.readPlist = readPlist;
22
+ const dirs = (p) => ((0, fs_1.existsSync)(p) ? (0, fs_1.readdirSync)(p).filter((n) => (0, fs_1.statSync)((0, path_1.join)(p, n)).isDirectory()) : []);
23
+ const first = (...paths) => paths.find((p) => (0, fs_1.existsSync)(p)) ?? null;
24
+ /** iOS: the .xcarchive EAS/xcodebuild produced under ios/build, plus the map our config plugin writes next to it. */
25
+ /** Newest .xcarchive in Xcode's Archives folder (where fastlane/gym archives by default), if made in the last 3 hours. */
26
+ function newestXcodeArchive(home, now = Date.now()) {
27
+ const base = (0, path_1.join)(home, 'Library', 'Developer', 'Xcode', 'Archives');
28
+ const all = dirs(base).flatMap((day) => dirs((0, path_1.join)(base, day)).filter((n) => n.endsWith('.xcarchive')).map((n) => (0, path_1.join)(base, day, n)));
29
+ const fresh = all.map((p) => ({ p, t: (0, fs_1.statSync)(p).mtimeMs })).filter((x) => now - x.t < 3 * 3600_000);
30
+ return fresh.sort((a, b) => b.t - a.t)[0]?.p ?? null;
31
+ }
32
+ function discoverIos(root, plist = exports.readPlist, home = (0, os_1.homedir)(), override = process.env.ROOTNODE_IOS_ARCHIVE) {
33
+ const out = { platform: 'ios', debugPaths: [], bundle: null, map: null, release: null, dist: null, notes: [] };
34
+ const buildDir = (0, path_1.join)(root, 'ios', 'build');
35
+ const local = dirs(buildDir).find((n) => n.endsWith('.xcarchive'));
36
+ const arch = override || (local ? (0, path_1.join)(buildDir, local) : newestXcodeArchive(home));
37
+ if (!arch || !(0, fs_1.existsSync)(arch)) {
38
+ out.notes.push('no .xcarchive under ios/build or in Xcode Archives — nothing to upload for iOS (set ROOTNODE_IOS_ARCHIVE to point at one)');
39
+ return out;
40
+ }
41
+ const archive = arch.split('/').pop();
42
+ const dsyms = (0, path_1.join)(arch, 'dSYMs');
43
+ if ((0, fs_1.existsSync)(dsyms))
44
+ out.debugPaths.push(dsyms);
45
+ else
46
+ out.notes.push(`${archive} has no dSYMs folder (is DEBUG_INFORMATION_FORMAT dwarf-with-dsym?)`);
47
+ const apps = (0, path_1.join)(arch, 'Products', 'Applications');
48
+ const appName = dirs(apps).find((n) => n.endsWith('.app'));
49
+ if (appName)
50
+ out.bundle = first((0, path_1.join)(apps, appName, 'main.jsbundle'));
51
+ out.map = first((0, path_1.join)(buildDir, 'main.jsbundle.map'));
52
+ if (out.bundle && !out.map)
53
+ out.notes.push('no ios/build/main.jsbundle.map — add the "@rootnode-app/react-native" plugin to app.json so the build writes it');
54
+ const info = plist((0, path_1.join)(arch, 'Info.plist'))?.ApplicationProperties;
55
+ if (info?.CFBundleIdentifier && info?.CFBundleShortVersionString && info?.CFBundleVersion) {
56
+ out.release = (0, plan_1.releaseName)(info.CFBundleIdentifier, info.CFBundleShortVersionString, String(info.CFBundleVersion));
57
+ out.dist = String(info.CFBundleVersion);
58
+ }
59
+ else
60
+ out.notes.push('could not read the version from the archive Info.plist');
61
+ return out;
62
+ }
63
+ /** Android: R8 mapping, the release bundle + its (Hermes-composed) source map, version from build.gradle. */
64
+ function discoverAndroid(root) {
65
+ const out = { platform: 'android', debugPaths: [], bundle: null, map: null, release: null, dist: null, notes: [] };
66
+ const app = (0, path_1.join)(root, 'android', 'app');
67
+ const b = (0, path_1.join)(app, 'build');
68
+ if (!(0, fs_1.existsSync)(b)) {
69
+ out.notes.push('no android/app/build — nothing to upload for Android');
70
+ return out;
71
+ }
72
+ const mapping = first((0, path_1.join)(b, 'outputs', 'mapping', 'release', 'mapping.txt'));
73
+ if (mapping)
74
+ out.debugPaths.push(mapping);
75
+ const nativeLibs = first((0, path_1.join)(b, 'intermediates', 'merged_native_libs', 'release'));
76
+ if (nativeLibs)
77
+ out.debugPaths.push(nativeLibs);
78
+ out.bundle = first((0, path_1.join)(b, 'generated', 'assets', 'react', 'release', 'index.android.bundle'), (0, path_1.join)(b, 'generated', 'assets', 'createBundleReleaseJsAndAssets', 'index.android.bundle'));
79
+ out.map = first((0, path_1.join)(b, 'generated', 'sourcemaps', 'react', 'release', 'index.android.bundle.map'), (0, path_1.join)(b, 'generated', 'sourcemaps', 'react', 'release', 'index.android.bundle.packager.map'));
80
+ const gradle = first((0, path_1.join)(app, 'build.gradle'), (0, path_1.join)(app, 'build.gradle.kts'));
81
+ const text = gradle ? (0, fs_1.readFileSync)(gradle, 'utf8') : '';
82
+ const id = text.match(/applicationId\s*=?\s*["']([^"']+)["']/)?.[1];
83
+ const code = text.match(/versionCode\s*=?\s*(\d+)/)?.[1];
84
+ const name = text.match(/versionName\s*=?\s*["']([^"']+)["']/)?.[1];
85
+ if (id && code && name) {
86
+ out.release = (0, plan_1.releaseName)(id, name, code);
87
+ out.dist = code;
88
+ }
89
+ else
90
+ out.notes.push('could not read applicationId / versionName / versionCode from android/app/build.gradle');
91
+ return out;
92
+ }
package/dist/plan.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure planning for the Rootnode CLI: which upload runs to make, with which
3
+ * arguments. The uploader underneath is sentry-cli 2.58.2 (BSD-3, pinned —
4
+ * later versions are FSL); nothing here touches the network or the disk.
5
+ */
6
+ export interface Options {
7
+ key: string | null;
8
+ host: string;
9
+ target: string;
10
+ release: string | null;
11
+ dist: string | null;
12
+ bundle: string | null;
13
+ map: string | null;
14
+ strict: boolean;
15
+ paths: string[];
16
+ }
17
+ export type Command = {
18
+ name: 'upload-symbols' | 'upload-sourcemaps' | 'eas' | 'help';
19
+ options: Options;
20
+ };
21
+ export interface UploadTarget {
22
+ organization: string;
23
+ project: string;
24
+ }
25
+ export interface Run {
26
+ label: string;
27
+ args: string[];
28
+ }
29
+ export declare const DEFAULT_HOST = "https://rootnode.app";
30
+ export declare function parseArgs(argv: string[], env: Record<string, string | undefined>): Command;
31
+ /** Environment for the uploader: our host, the app's analytics key, the resolved org/project. */
32
+ export declare function uploaderEnv(o: Options, t: UploadTarget): Record<string, string>;
33
+ /** ProGuard/R8 mapping files go through their own upload; everything else is a debug file (dSYM, .so, archive). */
34
+ export declare function symbolRuns(paths: string[]): Run[];
35
+ /** A Hermes/JS bundle with its source map, tied to the release the SDK reports. */
36
+ export declare function sourcemapRun(bundle: string, map: string, release: string, dist: string | null): Run;
37
+ /** The release name the React Native SDK reports: bundleId@version+build. */
38
+ export declare const releaseName: (id: string, version: string, build: string) => string;
39
+ export declare const HELP = "rootnode \u2014 upload symbols so release crashes show real code lines\n\n rootnode eas\n After an EAS build (package.json \"eas-build-on-success\": \"rootnode eas\"):\n finds this build's dSYMs, ProGuard mapping and JS bundle + source map and\n uploads them. Never fails the build unless --strict.\n\n rootnode upload-symbols <path...>\n dSYM folders, .xcarchive, unstripped .so folders, mapping.txt.\n\n rootnode upload-sourcemaps --bundle <file> --map <file> --release <name> [--dist <build>]\n\nOptions\n --key <rn_\u2026> the app's analytics key (default: ROOTNODE_KEY or EXPO_PUBLIC_ROOTNODE_KEY)\n --target <name> which target of the app: react-native (default), ios, android\u2026\n --host <url> default https://rootnode.app\n --strict exit non-zero when an upload fails";
package/dist/plan.js ADDED
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /**
3
+ * Pure planning for the Rootnode CLI: which upload runs to make, with which
4
+ * arguments. The uploader underneath is sentry-cli 2.58.2 (BSD-3, pinned —
5
+ * later versions are FSL); nothing here touches the network or the disk.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.HELP = exports.releaseName = exports.DEFAULT_HOST = void 0;
9
+ exports.parseArgs = parseArgs;
10
+ exports.uploaderEnv = uploaderEnv;
11
+ exports.symbolRuns = symbolRuns;
12
+ exports.sourcemapRun = sourcemapRun;
13
+ exports.DEFAULT_HOST = 'https://rootnode.app';
14
+ function parseArgs(argv, env) {
15
+ const [name = 'help', ...rest] = argv;
16
+ const o = {
17
+ key: env.ROOTNODE_KEY ?? env.EXPO_PUBLIC_ROOTNODE_KEY ?? null,
18
+ host: (env.ROOTNODE_HOST ?? exports.DEFAULT_HOST).replace(/\/$/, ''),
19
+ target: env.ROOTNODE_TARGET ?? 'react-native',
20
+ release: null,
21
+ dist: null,
22
+ bundle: null,
23
+ map: null,
24
+ strict: false,
25
+ paths: [],
26
+ };
27
+ const flags = {
28
+ '--key': (v) => (o.key = v),
29
+ '--host': (v) => (o.host = v.replace(/\/$/, '')),
30
+ '--target': (v) => (o.target = v),
31
+ '--release': (v) => (o.release = v),
32
+ '--dist': (v) => (o.dist = v),
33
+ '--bundle': (v) => (o.bundle = v),
34
+ '--map': (v) => (o.map = v),
35
+ };
36
+ for (let i = 0; i < rest.length; i++) {
37
+ const a = rest[i];
38
+ const [flag, inline] = a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, undefined];
39
+ if (flag === '--strict')
40
+ o.strict = true;
41
+ else if (flags[flag]) {
42
+ const v = inline ?? rest[++i];
43
+ if (v === undefined)
44
+ throw new Error(`${flag} needs a value`);
45
+ flags[flag](v);
46
+ }
47
+ else if (a.startsWith('--'))
48
+ throw new Error(`unknown option ${a}`);
49
+ else
50
+ o.paths.push(a);
51
+ }
52
+ const known = ['upload-symbols', 'upload-sourcemaps', 'eas', 'help', '--help', '-h'];
53
+ if (!known.includes(name))
54
+ throw new Error(`unknown command "${name}" — run: rootnode help`);
55
+ return { name: name.startsWith('-') ? 'help' : name, options: o };
56
+ }
57
+ /** Environment for the uploader: our host, the app's analytics key, the resolved org/project. */
58
+ function uploaderEnv(o, t) {
59
+ return {
60
+ SENTRY_URL: o.host,
61
+ SENTRY_AUTH_TOKEN: o.key ?? '',
62
+ SENTRY_ORG: t.organization,
63
+ SENTRY_PROJECT: t.project,
64
+ SENTRY_DISABLE_UPDATE_CHECK: 'true',
65
+ };
66
+ }
67
+ /** ProGuard/R8 mapping files go through their own upload; everything else is a debug file (dSYM, .so, archive). */
68
+ function symbolRuns(paths) {
69
+ const mappings = paths.filter((p) => /mapping\.txt$/i.test(p));
70
+ const debug = paths.filter((p) => !mappings.includes(p));
71
+ const runs = [];
72
+ if (debug.length)
73
+ runs.push({ label: `debug files (${debug.length})`, args: ['debug-files', 'upload', ...debug] });
74
+ for (const m of mappings)
75
+ runs.push({ label: 'ProGuard mapping', args: ['upload-proguard', m] });
76
+ return runs;
77
+ }
78
+ /** A Hermes/JS bundle with its source map, tied to the release the SDK reports. */
79
+ function sourcemapRun(bundle, map, release, dist) {
80
+ return {
81
+ label: `source map (${release}${dist ? `, build ${dist}` : ''})`,
82
+ args: ['sourcemaps', 'upload', '--release', release, ...(dist ? ['--dist', dist] : []), '--bundle', bundle, '--bundle-sourcemap', map],
83
+ };
84
+ }
85
+ /** The release name the React Native SDK reports: bundleId@version+build. */
86
+ const releaseName = (id, version, build) => `${id}@${version}+${build}`;
87
+ exports.releaseName = releaseName;
88
+ exports.HELP = `rootnode — upload symbols so release crashes show real code lines
89
+
90
+ rootnode eas
91
+ After an EAS build (package.json "eas-build-on-success": "rootnode eas"):
92
+ finds this build's dSYMs, ProGuard mapping and JS bundle + source map and
93
+ uploads them. Never fails the build unless --strict.
94
+
95
+ rootnode upload-symbols <path...>
96
+ dSYM folders, .xcarchive, unstripped .so folders, mapping.txt.
97
+
98
+ rootnode upload-sourcemaps --bundle <file> --map <file> --release <name> [--dist <build>]
99
+
100
+ Options
101
+ --key <rn_…> the app's analytics key (default: ROOTNODE_KEY or EXPO_PUBLIC_ROOTNODE_KEY)
102
+ --target <name> which target of the app: react-native (default), ios, android…
103
+ --host <url> default ${exports.DEFAULT_HOST}
104
+ --strict exit non-zero when an upload fails`;
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@rootnode-app/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Rootnode command line: upload dSYMs, ProGuard mappings, native debug files and source maps so release crashes show real code lines.",
5
+ "license": "MIT",
6
+ "author": "Doğan Aktı",
7
+ "homepage": "https://rootnode.app/docs/integration.md",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/doganakti/rootnode.git",
11
+ "directory": "sdk/cli"
12
+ },
13
+ "bin": {
14
+ "rootnode": "dist/bin.js"
15
+ },
16
+ "main": "dist/plan.js",
17
+ "types": "dist/plan.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "test": "vitest run",
28
+ "prepublishOnly": "npm run test && npm run build"
29
+ },
30
+ "dependencies": {
31
+ "@sentry/cli": "2.58.2"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.20.5",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^2.1.9"
37
+ }
38
+ }