@rootnode-app/cli 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,43 @@
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
+ Uploads go to the app's target for that build's platform (`ios` / `android`)
22
+ and to its `react-native` target, whichever exist. Never fails the build
23
+ unless you pass `--strict`.
24
+
25
+ ## Any build
26
+
27
+ ```bash
28
+ export ROOTNODE_KEY=rn_…
29
+ npx @rootnode-app/cli upload-symbols --target ios ./build/App.xcarchive
30
+ npx @rootnode-app/cli upload-symbols --target android app/build/outputs/mapping/release/mapping.txt
31
+ npx @rootnode-app/cli upload-sourcemaps --target react-native \
32
+ --bundle index.android.bundle --map index.android.bundle.map \
33
+ --release com.acme.shop@2.1.0+47 --dist 47
34
+ ```
35
+
36
+ `--target`: which target of the app the crashes come from (`react-native`
37
+ default, `ios`, `android`, …). `--host` for self-hosted Rootnode.
38
+
39
+ Symbols apply to crashes received **after** the upload — upload during the
40
+ build, not after the first crash. Check the result in the panel: Setup →
41
+ target → Symbols.
42
+
43
+ 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,116 @@
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 lookupTarget(o, name) {
8
+ const res = await fetch(`${o.host}/api/0/rootnode/targets/${encodeURIComponent(name)}`, {
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.status === 404)
15
+ return null;
16
+ if (!res.ok || !body.organization || !body.project)
17
+ throw new Error(body.detail ?? `target lookup failed (http ${res.status})`);
18
+ return { organization: body.organization, project: body.project };
19
+ }
20
+ async function resolveTarget(o) {
21
+ const t = await lookupTarget(o, o.target);
22
+ if (!t)
23
+ throw new Error(`the app has no "${o.target}" target — pass --target with one of its targets (see Rootnode → Setup)`);
24
+ return t;
25
+ }
26
+ function uploaderPath() {
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ return require('@sentry/cli').getPath();
29
+ }
30
+ /** The uploader talks about itself; our users only ever deal with Rootnode. */
31
+ const rebrand = (text) => text.replace(/\bsentry-cli\b/gi, 'rootnode').replace(/\bSentry\b/g, 'Rootnode');
32
+ function execute(runs, env) {
33
+ let ok = true;
34
+ for (const r of runs) {
35
+ console.log(`rootnode: uploading ${r.label}…`);
36
+ const p = (0, child_process_1.spawnSync)(uploaderPath(), r.args, { stdio: ['inherit', 'pipe', 'pipe'], env: { ...process.env, ...env }, encoding: 'utf8' });
37
+ process.stdout.write(rebrand(p.stdout ?? ''));
38
+ process.stderr.write(rebrand(p.stderr ?? ''));
39
+ if (p.status !== 0) {
40
+ ok = false;
41
+ console.error(`rootnode: ${r.label} failed (exit ${p.status ?? p.signal})`);
42
+ }
43
+ }
44
+ return ok;
45
+ }
46
+ function runsFor(out) {
47
+ const runs = (0, plan_1.symbolRuns)(out.debugPaths);
48
+ if (out.bundle && out.map && out.release)
49
+ runs.push((0, plan_1.sourcemapRun)(out.bundle, out.map, out.release, out.dist));
50
+ return runs;
51
+ }
52
+ async function main(argv) {
53
+ const cmd = (0, plan_1.parseArgs)(argv, process.env);
54
+ const o = cmd.options;
55
+ if (cmd.name === 'help') {
56
+ console.log(plan_1.HELP);
57
+ return 0;
58
+ }
59
+ if (!o.key)
60
+ throw new Error('no key — pass --key rn_… or set ROOTNODE_KEY (EXPO_PUBLIC_ROOTNODE_KEY works too)');
61
+ if (cmd.name === 'eas') {
62
+ // EAS runs this hook once per platform; outside EAS both are tried.
63
+ const platform = process.env.EAS_BUILD_PLATFORM;
64
+ const root = process.cwd();
65
+ const outs = [platform !== 'android' && (0, discover_1.discoverIos)(root), platform !== 'ios' && (0, discover_1.discoverAndroid)(root)].filter(Boolean);
66
+ for (const out of outs)
67
+ for (const n of out.notes)
68
+ console.log(`rootnode: ${out.platform}: ${n}`);
69
+ if (!outs.some((out) => runsFor(out).length)) {
70
+ console.log('rootnode: nothing to upload');
71
+ return 0;
72
+ }
73
+ try {
74
+ let ok = true;
75
+ for (const out of outs) {
76
+ const runs = runsFor(out);
77
+ if (!runs.length)
78
+ continue;
79
+ const names = (0, plan_1.easTargetCandidates)(out.platform, o);
80
+ const found = [];
81
+ for (const name of names) {
82
+ const t = await lookupTarget(o, name);
83
+ if (t)
84
+ found.push({ name, t });
85
+ }
86
+ if (!found.length) {
87
+ console.error(`rootnode: ${out.platform}: the app has none of these targets: ${names.join(', ')} — add one in Rootnode → Setup or pass --target`);
88
+ ok = false;
89
+ continue;
90
+ }
91
+ for (const { name, t } of found) {
92
+ console.log(`rootnode: ${out.platform} → target ${name}`);
93
+ ok = execute(runs, (0, plan_1.uploaderEnv)(o, t)) && ok;
94
+ }
95
+ }
96
+ return ok || !o.strict ? 0 : 1;
97
+ }
98
+ catch (e) {
99
+ console.error(`rootnode: ${e.message}`);
100
+ return o.strict ? 1 : 0; // an upload problem must not fail a good build
101
+ }
102
+ }
103
+ const target = await resolveTarget(o);
104
+ if (cmd.name === 'upload-symbols') {
105
+ if (!o.paths.length)
106
+ throw new Error('give the dSYM / .xcarchive / .so folder / mapping.txt paths');
107
+ return execute((0, plan_1.symbolRuns)(o.paths), (0, plan_1.uploaderEnv)(o, target)) ? 0 : 1;
108
+ }
109
+ if (!o.bundle || !o.map || !o.release)
110
+ throw new Error('upload-sourcemaps needs --bundle, --map and --release');
111
+ return execute([(0, plan_1.sourcemapRun)(o.bundle, o.map, o.release, o.dist)], (0, plan_1.uploaderEnv)(o, target)) ? 0 : 1;
112
+ }
113
+ main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
114
+ console.error(`rootnode: ${e.message}`);
115
+ process.exit(1);
116
+ });
@@ -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,94 @@
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
+ // The archive's own Info.plist usually carries ApplicationProperties; the app bundle's always does.
55
+ const complete = (i) => (i?.CFBundleIdentifier && i?.CFBundleShortVersionString && i?.CFBundleVersion ? i : null);
56
+ const info = complete(plist((0, path_1.join)(arch, 'Info.plist'))?.ApplicationProperties) ?? (appName ? complete(plist((0, path_1.join)(apps, appName, 'Info.plist'))) : null);
57
+ if (info) {
58
+ out.release = (0, plan_1.releaseName)(info.CFBundleIdentifier, info.CFBundleShortVersionString, String(info.CFBundleVersion));
59
+ out.dist = String(info.CFBundleVersion);
60
+ }
61
+ else
62
+ out.notes.push('could not read the version from the archive or app Info.plist — dSYMs still upload, the JS source map is skipped');
63
+ return out;
64
+ }
65
+ /** Android: R8 mapping, the release bundle + its (Hermes-composed) source map, version from build.gradle. */
66
+ function discoverAndroid(root) {
67
+ const out = { platform: 'android', debugPaths: [], bundle: null, map: null, release: null, dist: null, notes: [] };
68
+ const app = (0, path_1.join)(root, 'android', 'app');
69
+ const b = (0, path_1.join)(app, 'build');
70
+ if (!(0, fs_1.existsSync)(b)) {
71
+ out.notes.push('no android/app/build — nothing to upload for Android');
72
+ return out;
73
+ }
74
+ const mapping = first((0, path_1.join)(b, 'outputs', 'mapping', 'release', 'mapping.txt'));
75
+ if (mapping)
76
+ out.debugPaths.push(mapping);
77
+ const nativeLibs = first((0, path_1.join)(b, 'intermediates', 'merged_native_libs', 'release'));
78
+ if (nativeLibs)
79
+ out.debugPaths.push(nativeLibs);
80
+ 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'));
81
+ 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'));
82
+ const gradle = first((0, path_1.join)(app, 'build.gradle'), (0, path_1.join)(app, 'build.gradle.kts'));
83
+ const text = gradle ? (0, fs_1.readFileSync)(gradle, 'utf8') : '';
84
+ const id = text.match(/applicationId\s*=?\s*["']([^"']+)["']/)?.[1];
85
+ const code = text.match(/versionCode\s*=?\s*(\d+)/)?.[1];
86
+ const name = text.match(/versionName\s*=?\s*["']([^"']+)["']/)?.[1];
87
+ if (id && code && name) {
88
+ out.release = (0, plan_1.releaseName)(id, name, code);
89
+ out.dist = code;
90
+ }
91
+ else
92
+ out.notes.push('could not read applicationId / versionName / versionCode from android/app/build.gradle');
93
+ return out;
94
+ }
package/dist/plan.d.ts ADDED
@@ -0,0 +1,47 @@
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
+ /** --target or ROOTNODE_TARGET was given — no automatic target choice. */
11
+ targetExplicit: boolean;
12
+ release: string | null;
13
+ dist: string | null;
14
+ bundle: string | null;
15
+ map: string | null;
16
+ strict: boolean;
17
+ paths: string[];
18
+ }
19
+ export type Command = {
20
+ name: 'upload-symbols' | 'upload-sourcemaps' | 'eas' | 'help';
21
+ options: Options;
22
+ };
23
+ export interface UploadTarget {
24
+ organization: string;
25
+ project: string;
26
+ }
27
+ export interface Run {
28
+ label: string;
29
+ args: string[];
30
+ }
31
+ export declare const DEFAULT_HOST = "https://rootnode.app";
32
+ export declare function parseArgs(argv: string[], env: Record<string, string | undefined>): Command;
33
+ /** Environment for the uploader: our host, the app's analytics key, the resolved org/project. */
34
+ export declare function uploaderEnv(o: Options, t: UploadTarget): Record<string, string>;
35
+ /** ProGuard/R8 mapping files go through their own upload; everything else is a debug file (dSYM, .so, archive). */
36
+ export declare function symbolRuns(paths: string[]): Run[];
37
+ /** A Hermes/JS bundle with its source map, tied to the release the SDK reports. */
38
+ export declare function sourcemapRun(bundle: string, map: string, release: string, dist: string | null): Run;
39
+ /**
40
+ * Which app targets one EAS build uploads to. A React Native app may report
41
+ * crashes through its per-platform targets (ios / android DSNs) or one
42
+ * react-native target — symbols go to every one of these that exists.
43
+ */
44
+ export declare function easTargetCandidates(platform: 'ios' | 'android', o: Options): string[];
45
+ /** The release name the React Native SDK reports: bundleId@version+build. */
46
+ export declare const releaseName: (id: string, version: string, build: string) => string;
47
+ 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\": \"npx @rootnode-app/cli 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; eas picks ios/android + react-native), 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,117 @@
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.easTargetCandidates = easTargetCandidates;
14
+ exports.DEFAULT_HOST = 'https://rootnode.app';
15
+ function parseArgs(argv, env) {
16
+ const [name = 'help', ...rest] = argv;
17
+ const o = {
18
+ key: env.ROOTNODE_KEY ?? env.EXPO_PUBLIC_ROOTNODE_KEY ?? null,
19
+ host: (env.ROOTNODE_HOST ?? exports.DEFAULT_HOST).replace(/\/$/, ''),
20
+ target: env.ROOTNODE_TARGET ?? 'react-native',
21
+ targetExplicit: !!env.ROOTNODE_TARGET,
22
+ release: null,
23
+ dist: null,
24
+ bundle: null,
25
+ map: null,
26
+ strict: false,
27
+ paths: [],
28
+ };
29
+ const flags = {
30
+ '--key': (v) => (o.key = v),
31
+ '--host': (v) => (o.host = v.replace(/\/$/, '')),
32
+ '--target': (v) => {
33
+ o.target = v;
34
+ o.targetExplicit = true;
35
+ },
36
+ '--release': (v) => (o.release = v),
37
+ '--dist': (v) => (o.dist = v),
38
+ '--bundle': (v) => (o.bundle = v),
39
+ '--map': (v) => (o.map = v),
40
+ };
41
+ for (let i = 0; i < rest.length; i++) {
42
+ const a = rest[i];
43
+ const [flag, inline] = a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, undefined];
44
+ if (flag === '--strict')
45
+ o.strict = true;
46
+ else if (flags[flag]) {
47
+ const v = inline ?? rest[++i];
48
+ if (v === undefined)
49
+ throw new Error(`${flag} needs a value`);
50
+ flags[flag](v);
51
+ }
52
+ else if (a.startsWith('--'))
53
+ throw new Error(`unknown option ${a}`);
54
+ else
55
+ o.paths.push(a);
56
+ }
57
+ const known = ['upload-symbols', 'upload-sourcemaps', 'eas', 'help', '--help', '-h'];
58
+ if (!known.includes(name))
59
+ throw new Error(`unknown command "${name}" — run: rootnode help`);
60
+ return { name: name.startsWith('-') ? 'help' : name, options: o };
61
+ }
62
+ /** Environment for the uploader: our host, the app's analytics key, the resolved org/project. */
63
+ function uploaderEnv(o, t) {
64
+ return {
65
+ SENTRY_URL: o.host,
66
+ SENTRY_AUTH_TOKEN: o.key ?? '',
67
+ SENTRY_ORG: t.organization,
68
+ SENTRY_PROJECT: t.project,
69
+ SENTRY_DISABLE_UPDATE_CHECK: 'true',
70
+ };
71
+ }
72
+ /** ProGuard/R8 mapping files go through their own upload; everything else is a debug file (dSYM, .so, archive). */
73
+ function symbolRuns(paths) {
74
+ const mappings = paths.filter((p) => /mapping\.txt$/i.test(p));
75
+ const debug = paths.filter((p) => !mappings.includes(p));
76
+ const runs = [];
77
+ if (debug.length)
78
+ runs.push({ label: `debug files (${debug.length})`, args: ['debug-files', 'upload', ...debug] });
79
+ for (const m of mappings)
80
+ runs.push({ label: 'ProGuard mapping', args: ['upload-proguard', m] });
81
+ return runs;
82
+ }
83
+ /** A Hermes/JS bundle with its source map, tied to the release the SDK reports. */
84
+ function sourcemapRun(bundle, map, release, dist) {
85
+ return {
86
+ label: `source map (${release}${dist ? `, build ${dist}` : ''})`,
87
+ args: ['sourcemaps', 'upload', '--release', release, ...(dist ? ['--dist', dist] : []), '--bundle', bundle, '--bundle-sourcemap', map],
88
+ };
89
+ }
90
+ /**
91
+ * Which app targets one EAS build uploads to. A React Native app may report
92
+ * crashes through its per-platform targets (ios / android DSNs) or one
93
+ * react-native target — symbols go to every one of these that exists.
94
+ */
95
+ function easTargetCandidates(platform, o) {
96
+ return o.targetExplicit ? [o.target] : [platform, 'react-native'];
97
+ }
98
+ /** The release name the React Native SDK reports: bundleId@version+build. */
99
+ const releaseName = (id, version, build) => `${id}@${version}+${build}`;
100
+ exports.releaseName = releaseName;
101
+ exports.HELP = `rootnode — upload symbols so release crashes show real code lines
102
+
103
+ rootnode eas
104
+ After an EAS build (package.json "eas-build-on-success": "npx @rootnode-app/cli eas"):
105
+ finds this build's dSYMs, ProGuard mapping and JS bundle + source map and
106
+ uploads them. Never fails the build unless --strict.
107
+
108
+ rootnode upload-symbols <path...>
109
+ dSYM folders, .xcarchive, unstripped .so folders, mapping.txt.
110
+
111
+ rootnode upload-sourcemaps --bundle <file> --map <file> --release <name> [--dist <build>]
112
+
113
+ Options
114
+ --key <rn_…> the app's analytics key (default: ROOTNODE_KEY or EXPO_PUBLIC_ROOTNODE_KEY)
115
+ --target <name> which target of the app: react-native (default; eas picks ios/android + react-native), ios, android…
116
+ --host <url> default ${exports.DEFAULT_HOST}
117
+ --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.1",
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
+ }