reactoradar 1.2.3

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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/app.js +2450 -0
  4. package/assets/icon.svg +54 -0
  5. package/bin/cli.js +79 -0
  6. package/bin/open-debugger.sh +9 -0
  7. package/bin/setup.js +473 -0
  8. package/index.html +82 -0
  9. package/main.js +528 -0
  10. package/package.json +76 -0
  11. package/preload.js +31 -0
  12. package/sdk/RNDebugSDK.js +540 -0
  13. package/src/main/main.js +396 -0
  14. package/src/main/preload.js +28 -0
  15. package/src/renderer/app.js +221 -0
  16. package/src/renderer/components/object-tree.js +245 -0
  17. package/src/renderer/index.html +111 -0
  18. package/src/renderer/panels/console.js +248 -0
  19. package/src/renderer/panels/memory.js +60 -0
  20. package/src/renderer/panels/network.js +559 -0
  21. package/src/renderer/panels/performance.js +144 -0
  22. package/src/renderer/panels/react.js +31 -0
  23. package/src/renderer/panels/redux.js +159 -0
  24. package/src/renderer/panels/settings.js +93 -0
  25. package/src/renderer/panels/sources.js +189 -0
  26. package/src/renderer/panels/storage.js +134 -0
  27. package/src/renderer/state.js +132 -0
  28. package/src/renderer/styles/components.css +145 -0
  29. package/src/renderer/styles/console.css +73 -0
  30. package/src/renderer/styles/main.css +229 -0
  31. package/src/renderer/styles/network.css +242 -0
  32. package/src/renderer/styles/performance.css +45 -0
  33. package/src/renderer/styles/redux.css +77 -0
  34. package/src/renderer/styles/settings.css +63 -0
  35. package/src/renderer/styles/sources.css +48 -0
  36. package/src/renderer/styles/storage.css +28 -0
  37. package/src/renderer/styles/theme-light.css +57 -0
  38. package/styles.css +1308 -0
@@ -0,0 +1,54 @@
1
+ <svg width="100%" viewBox="0 0 680 680" role="img" xmlns="http://www.w3.org/2000/svg" style="">
2
+ <title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">RN Debugger icon — version 2</title>
3
+ <desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Bold minimal macOS icon: deep navy rounded square, cyan React atom overlaid with a sharp red breakpoint dot. Clean and readable at any size.</desc>
4
+
5
+ <defs>
6
+ <linearGradient id="bg2" x1="0" y1="0" x2="1" y2="1">
7
+ <stop offset="0%" stop-color="#1a2340"/>
8
+ <stop offset="100%" stop-color="#0d1120"/>
9
+ </linearGradient>
10
+ <linearGradient id="atomShine" x1="0" y1="0" x2="0" y2="1">
11
+ <stop offset="0%" stop-color="#6fc8ff"/>
12
+ <stop offset="100%" stop-color="#2e90d4"/>
13
+ </linearGradient>
14
+ <linearGradient id="bpShine" x1="0" y1="0" x2="0" y2="1">
15
+ <stop offset="0%" stop-color="#ff7b8a"/>
16
+ <stop offset="100%" stop-color="#e0283e"/>
17
+ </linearGradient>
18
+ </defs>
19
+
20
+ <!-- Icon background -->
21
+ <rect x="40" y="40" width="600" height="600" rx="130" fill="url(#bg2)" style="stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
22
+
23
+ <!-- Subtle inner top shine -->
24
+ <rect x="40" y="40" width="600" height="200" rx="130" fill="#ffffff" opacity="0.03" style="fill:rgb(255, 255, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.03;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
25
+
26
+ <!-- ── REACT ATOM (the main mark) ── -->
27
+ <g transform="translate(340, 320)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
28
+
29
+ <!-- Orbit 1 — horizontal -->
30
+ <ellipse cx="0" cy="0" rx="188" ry="66" fill="none" stroke="url(#atomShine)" stroke-width="14" opacity="0.9" style="fill:none;color:rgb(0, 0, 0);stroke-width:14px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.9;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
31
+
32
+ <!-- Orbit 2 — rotated 60° -->
33
+ <ellipse cx="0" cy="0" rx="188" ry="66" fill="none" stroke="url(#atomShine)" stroke-width="14" opacity="0.9" transform="rotate(60)" style="fill:none;color:rgb(0, 0, 0);stroke-width:14px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.9;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
34
+
35
+ <!-- Orbit 3 — rotated 120° -->
36
+ <ellipse cx="0" cy="0" rx="188" ry="66" fill="none" stroke="url(#atomShine)" stroke-width="14" opacity="0.9" transform="rotate(120)" style="fill:none;color:rgb(0, 0, 0);stroke-width:14px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.9;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
37
+
38
+ <!-- Nucleus -->
39
+ <circle cx="0" cy="0" r="32" fill="url(#atomShine)" style="stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
40
+ <circle cx="0" cy="0" r="18" fill="#0d1120" style="fill:rgb(13, 17, 32);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
41
+
42
+ </g>
43
+
44
+ <!-- ── BREAKPOINT DOT (debugger mark) ── -->
45
+ <!-- Positioned top-right, overlapping atom — the "debugger" signal -->
46
+ <circle cx="478" cy="202" r="52" fill="#0d1120" style="fill:rgb(13, 17, 32);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
47
+ <circle cx="478" cy="202" r="42" fill="url(#bpShine)" style="stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
48
+ <!-- inner hollow — classic breakpoint ring style -->
49
+ <circle cx="478" cy="202" r="22" fill="#0d1120" style="fill:rgb(13, 17, 32);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
50
+
51
+ <!-- Icon border -->
52
+ <rect x="40" y="40" width="600" height="600" rx="130" fill="none" stroke="#4facff" stroke-width="1.5" opacity="0.15" style="fill:none;stroke:rgb(79, 172, 255);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.15;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
53
+
54
+ </svg>
package/bin/cli.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { execSync, spawn } = require('child_process');
6
+
7
+ const C = {
8
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
9
+ green: '\x1b[32m', cyan: '\x1b[36m', yellow: '\x1b[33m', magenta: '\x1b[35m',
10
+ };
11
+
12
+ const args = process.argv.slice(2);
13
+ const command = args[0] || 'start';
14
+ const appDir = path.resolve(__dirname, '..');
15
+
16
+ function printHelp() {
17
+ console.log();
18
+ console.log(C.bold + C.magenta + ' ReactoRadar' + C.reset + ' — React Native debugging tool');
19
+ console.log();
20
+ console.log(' Usage:');
21
+ console.log(` ${C.cyan}npx rn-debugger${C.reset} Launch the debugger app`);
22
+ console.log(` ${C.cyan}npx rn-debugger setup${C.reset} Install SDK into current RN project`);
23
+ console.log(` ${C.cyan}npx rn-debugger remove${C.reset} Remove SDK from current RN project`);
24
+ console.log(` ${C.cyan}npx rn-debugger help${C.reset} Show this help`);
25
+ console.log();
26
+ console.log(' Or add to your RN project\'s package.json:');
27
+ console.log(` ${C.dim}"debug:setup": "npx rn-debugger setup"${C.reset}`);
28
+ console.log(` ${C.dim}"debug:start": "npx rn-debugger"${C.reset}`);
29
+ console.log(` ${C.dim}"debug:remove": "npx rn-debugger remove"${C.reset}`);
30
+ console.log();
31
+ }
32
+
33
+ switch (command) {
34
+ case 'start':
35
+ case 'launch':
36
+ case 'open': {
37
+ // Ensure electron is installed
38
+ try {
39
+ require.resolve('electron');
40
+ } catch {
41
+ console.log(C.yellow + ' Installing electron...' + C.reset);
42
+ execSync('npm install', { cwd: appDir, stdio: 'inherit' });
43
+ }
44
+ console.log(C.green + ' Launching ReactoRadar...' + C.reset);
45
+ const env = { ...process.env };
46
+ delete env.ELECTRON_RUN_AS_NODE;
47
+ const electronPath = path.join(appDir, 'node_modules', '.bin', 'electron');
48
+ const child = spawn(electronPath, [appDir], { env, stdio: 'inherit', detached: true });
49
+ child.unref();
50
+ break;
51
+ }
52
+
53
+ case 'setup':
54
+ case 'init':
55
+ case 'install': {
56
+ const setupScript = path.join(appDir, 'bin', 'setup.js');
57
+ const projectPath = args[1] || process.cwd();
58
+ // Set argv BEFORE requiring setup.js so it reads the correct path
59
+ process.argv = [process.argv[0], setupScript, projectPath];
60
+ require(setupScript);
61
+ break;
62
+ }
63
+
64
+ case 'remove':
65
+ case 'uninstall': {
66
+ const setupScript = path.join(appDir, 'bin', 'setup.js');
67
+ const projectPath = args[1] || process.cwd();
68
+ process.argv = [process.argv[0], setupScript, projectPath, '--uninstall'];
69
+ require(setupScript);
70
+ break;
71
+ }
72
+
73
+ case 'help':
74
+ case '--help':
75
+ case '-h':
76
+ default:
77
+ printHelp();
78
+ break;
79
+ }
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # This script is used as BROWSER= for Metro to redirect "Open DevTools" to ReactoRadar app
3
+ # Instead of opening Chrome, it opens our Electron app
4
+
5
+ if [ -d "/Applications/ReactoRadar.app" ]; then
6
+ open "/Applications/ReactoRadar.app"
7
+ else
8
+ echo "[ReactoRadar] App not installed. Install from: https://github.com/sharanagouda/react-native-debugger/releases"
9
+ fi
package/bin/setup.js ADDED
@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * ReactoRadar — Auto Setup
6
+ *
7
+ * Usage:
8
+ * node bin/setup.js <path-to-rn-project> # install into RN project
9
+ * node bin/setup.js <path-to-rn-project> --uninstall # remove from RN project
10
+ *
11
+ * What it does (install):
12
+ * 1. Validates the target is a React Native project
13
+ * 2. Copies RNDebugSDK.js into the project
14
+ * 3. Detects platform (iOS sim / Android emu / device) and sets HOST
15
+ * 4. Patches the entry file (index.js / index.tsx) to load the SDK
16
+ * 5. Detects Redux (@reduxjs/toolkit or redux) and patches the store
17
+ * 6. Runs adb reverse for Android
18
+ * 7. Prints summary
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { execSync } = require('child_process');
24
+ const os = require('os');
25
+ const readline = require('readline');
26
+
27
+ // ─── Colors ──────────────────────────────────────────────────────────────────
28
+ const C = {
29
+ reset: '\x1b[0m',
30
+ bold: '\x1b[1m',
31
+ dim: '\x1b[2m',
32
+ green: '\x1b[32m',
33
+ yellow:'\x1b[33m',
34
+ red: '\x1b[31m',
35
+ cyan: '\x1b[36m',
36
+ magenta:'\x1b[35m',
37
+ };
38
+ const log = (...a) => console.log(C.green + ' ✓' + C.reset, ...a);
39
+ const warn = (...a) => console.log(C.yellow + ' ⚠' + C.reset, ...a);
40
+ const err = (...a) => console.log(C.red + ' ✗' + C.reset, ...a);
41
+ const info = (...a) => console.log(C.cyan + ' →' + C.reset, ...a);
42
+ const title = (t) => console.log('\n' + C.bold + C.magenta + ' ' + t + C.reset);
43
+
44
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
45
+ function fileExists(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
46
+ function dirExists(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
47
+ function readJSON(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } }
48
+
49
+ function ask(question) {
50
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
51
+ return new Promise(resolve => {
52
+ rl.question(C.cyan + ' ? ' + C.reset + question + ' ', answer => {
53
+ rl.close();
54
+ resolve(answer.trim());
55
+ });
56
+ });
57
+ }
58
+
59
+ function getMacLanIP() {
60
+ const nets = os.networkInterfaces();
61
+ for (const name of Object.keys(nets)) {
62
+ for (const net of nets[name]) {
63
+ if (net.family === 'IPv4' && !net.internal) return net.address;
64
+ }
65
+ }
66
+ return '192.168.1.100';
67
+ }
68
+
69
+ function tryExec(cmd) {
70
+ try { return execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim(); } catch { return ''; }
71
+ }
72
+
73
+ // ─── SDK Marker ──────────────────────────────────────────────────────────────
74
+ const SDK_MARKER_START = '// ── RNDebugSDK setup ──';
75
+ const SDK_MARKER_END = '// ── /RNDebugSDK setup ──';
76
+
77
+ // ─── Detect platform ────────────────────────────────────────────────────────
78
+ function detectPlatform() {
79
+ // Check if Android emulator is running
80
+ const adbDevices = tryExec('adb devices 2>/dev/null');
81
+ const hasAndroidEmu = adbDevices.includes('emulator');
82
+ const hasAndroidDevice = /\b[A-Z0-9]{6,}\s+device\b/i.test(adbDevices) && !hasAndroidEmu;
83
+
84
+ // Check if iOS simulator is running
85
+ const xcrun = tryExec('xcrun simctl list devices booted 2>/dev/null');
86
+ const hasIOSSim = xcrun.includes('Booted');
87
+
88
+ return { hasAndroidEmu, hasAndroidDevice, hasIOSSim };
89
+ }
90
+
91
+ function pickHost(platform) {
92
+ if (platform.hasIOSSim && !platform.hasAndroidEmu && !platform.hasAndroidDevice) {
93
+ return { host: '127.0.0.1', reason: 'iOS Simulator detected' };
94
+ }
95
+ if (platform.hasAndroidEmu && !platform.hasIOSSim) {
96
+ return { host: '10.0.2.2', reason: 'Android Emulator detected' };
97
+ }
98
+ if (platform.hasAndroidDevice) {
99
+ return { host: '10.0.2.2', reason: 'Android device detected (using adb reverse)' };
100
+ }
101
+ if (platform.hasIOSSim && platform.hasAndroidEmu) {
102
+ return { host: '127.0.0.1', reason: 'Both iOS Sim + Android Emu detected (defaulting to iOS, Android uses adb reverse)' };
103
+ }
104
+ // Nothing running — default to localhost
105
+ return { host: '127.0.0.1', reason: 'No running devices detected (default)' };
106
+ }
107
+
108
+ // ─── Find entry file ─────────────────────────────────────────────────────────
109
+ function findEntryFile(projectDir) {
110
+ const candidates = ['index.js', 'index.tsx', 'index.ts'];
111
+ for (const f of candidates) {
112
+ if (fileExists(path.join(projectDir, f))) return f;
113
+ }
114
+ return null;
115
+ }
116
+
117
+ // ─── Find Redux store file ───────────────────────────────────────────────────
118
+ function findStoreFile(projectDir) {
119
+ const searchDirs = ['src', 'app', 'store', 'redux', 'src/store', 'src/redux', 'app/store', 'app/redux', 'src/app/store'];
120
+ const storeNames = ['store.ts', 'store.js', 'store.tsx', 'index.ts', 'index.js'];
121
+
122
+ for (const dir of searchDirs) {
123
+ for (const name of storeNames) {
124
+ const p = path.join(projectDir, dir, name);
125
+ if (fileExists(p)) {
126
+ const content = fs.readFileSync(p, 'utf8');
127
+ if (content.includes('configureStore') || content.includes('createStore')) {
128
+ return path.join(dir, name);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
136
+ // ─── Install ─────────────────────────────────────────────────────────────────
137
+ async function install(projectDir) {
138
+ const debuggerDir = path.resolve(__dirname, '..');
139
+
140
+ title('ReactoRadar — Auto Setup');
141
+ console.log();
142
+
143
+ // 1. Validate RN project
144
+ info('Validating React Native project...');
145
+ const pkg = readJSON(path.join(projectDir, 'package.json'));
146
+ if (!pkg) {
147
+ err('No package.json found at ' + projectDir);
148
+ process.exit(1);
149
+ }
150
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
151
+ if (!allDeps['react-native']) {
152
+ err('Not a React Native project (no react-native dependency)');
153
+ process.exit(1);
154
+ }
155
+ log('React Native project found:', C.bold + pkg.name + C.reset, '(' + (allDeps['react-native']) + ')');
156
+
157
+ // 2. Detect platform and set HOST
158
+ info('Detecting running devices...');
159
+ const platform = detectPlatform();
160
+ const { host, reason } = pickHost(platform);
161
+ log('HOST =', C.bold + host + C.reset, C.dim + '(' + reason + ')' + C.reset);
162
+
163
+ // 3. Copy SDK
164
+ info('Installing RNDebugSDK...');
165
+ const sdkSrc = path.join(debuggerDir, 'sdk', 'RNDebugSDK.js');
166
+ const sdkDestDir = path.join(projectDir, 'src', 'debug');
167
+ const sdkDest = path.join(sdkDestDir, 'RNDebugSDK.js');
168
+
169
+ if (!dirExists(path.join(projectDir, 'src'))) {
170
+ fs.mkdirSync(path.join(projectDir, 'src'), { recursive: true });
171
+ }
172
+ fs.mkdirSync(sdkDestDir, { recursive: true });
173
+
174
+ // Read SDK, patch HOST
175
+ let sdkContent = fs.readFileSync(sdkSrc, 'utf8');
176
+ sdkContent = sdkContent.replace(
177
+ /const HOST = '[^']+';/,
178
+ `const HOST = '${host}';`
179
+ );
180
+ fs.writeFileSync(sdkDest, sdkContent);
181
+ log('Copied RNDebugSDK.js →', C.dim + 'src/debug/RNDebugSDK.js' + C.reset);
182
+
183
+ // 4. Patch entry file
184
+ info('Patching entry file...');
185
+ const entryFile = findEntryFile(projectDir);
186
+ if (!entryFile) {
187
+ warn('No index.js/tsx found — you\'ll need to add the SDK import manually');
188
+ console.log(C.dim + ' Add to your entry file:' + C.reset);
189
+ console.log(C.dim + ' if (__DEV__) { require("./src/debug/RNDebugSDK"); }' + C.reset);
190
+ } else {
191
+ const entryPath = path.join(projectDir, entryFile);
192
+ let entryContent = fs.readFileSync(entryPath, 'utf8');
193
+
194
+ if (entryContent.includes('RNDebugSDK')) {
195
+ log('Entry file already has RNDebugSDK import — skipping');
196
+ } else {
197
+ const sdkImport = `${SDK_MARKER_START}
198
+ if (__DEV__) {
199
+ const { watchAsyncStorage } = require('./src/debug/RNDebugSDK');
200
+ watchAsyncStorage();
201
+ }
202
+ ${SDK_MARKER_END}
203
+ `;
204
+ entryContent = sdkImport + entryContent;
205
+ fs.writeFileSync(entryPath, entryContent);
206
+ log('Patched', C.bold + entryFile + C.reset, '— SDK loads automatically in dev mode');
207
+ }
208
+ }
209
+
210
+ // 5. Detect and wire Redux
211
+ info('Checking for Redux...');
212
+ const hasRedux = allDeps['@reduxjs/toolkit'] || allDeps['redux'];
213
+ if (hasRedux) {
214
+ const storeFile = findStoreFile(projectDir);
215
+ if (storeFile) {
216
+ const storePath = path.join(projectDir, storeFile);
217
+ const storeContent = fs.readFileSync(storePath, 'utf8');
218
+
219
+ if (storeContent.includes('RNDebugSDK')) {
220
+ log('Redux store already has RNDebugSDK wired — skipping');
221
+ } else if (storeContent.includes('configureStore')) {
222
+ // RTK configureStore
223
+ const patchedStore = `${SDK_MARKER_START}\nimport { reduxMiddleware } from '../debug/RNDebugSDK';\n${SDK_MARKER_END}\n` + storeContent;
224
+
225
+ // Try to add middleware to configureStore
226
+ if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
227
+ warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
228
+ console.log(C.dim + ' Add manually to your middleware:' + C.reset);
229
+ console.log(C.dim + ' import { reduxMiddleware } from \'./src/debug/RNDebugSDK\';' + C.reset);
230
+ console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
231
+ console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
232
+ console.log(C.dim + ' : getDefault(),' + C.reset);
233
+ } else {
234
+ // Add middleware field to configureStore
235
+ const patched = storeContent.replace(
236
+ /(configureStore\s*\(\s*\{)/,
237
+ `$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('./src/debug/RNDebugSDK').reduxMiddleware)\n : getDefaultMiddleware(),`
238
+ );
239
+ if (patched !== storeContent) {
240
+ fs.writeFileSync(storePath, patched);
241
+ log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired');
242
+ } else {
243
+ warn('Could not auto-patch', storeFile, '— wire Redux manually');
244
+ }
245
+ }
246
+ } else if (storeContent.includes('createStore')) {
247
+ warn('Legacy createStore found at', C.bold + storeFile + C.reset);
248
+ console.log(C.dim + ' Add manually:' + C.reset);
249
+ console.log(C.dim + ' import { reduxEnhancer } from \'./src/debug/RNDebugSDK\';' + C.reset);
250
+ console.log(C.dim + ' const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);' + C.reset);
251
+ }
252
+ } else {
253
+ warn('Redux detected but store file not found automatically');
254
+ console.log(C.dim + ' Add to your store setup:' + C.reset);
255
+ console.log(C.dim + ' import { reduxMiddleware } from \'./src/debug/RNDebugSDK\';' + C.reset);
256
+ }
257
+ } else {
258
+ log('No Redux detected — skipping');
259
+ }
260
+
261
+ // 6. adb reverse for Android
262
+ if (platform.hasAndroidEmu || platform.hasAndroidDevice) {
263
+ info('Setting up adb reverse...');
264
+ const ports = [9090, 9091, 9092, 8097];
265
+ let allGood = true;
266
+ for (const port of ports) {
267
+ const result = tryExec(`adb reverse tcp:${port} tcp:${port} 2>&1`);
268
+ if (result.includes('error')) {
269
+ warn(`adb reverse tcp:${port} failed`);
270
+ allGood = false;
271
+ }
272
+ }
273
+ if (allGood) {
274
+ log('adb reverse set for ports 9090, 9091, 9092, 8097');
275
+ }
276
+ }
277
+
278
+ // 7. Add debug scripts to package.json
279
+ info('Adding debug scripts to package.json...');
280
+ const projPkg = readJSON(path.join(projectDir, 'package.json'));
281
+ if (projPkg && projPkg.scripts) {
282
+ let changed = false;
283
+ if (!projPkg.scripts['debug:start']) {
284
+ projPkg.scripts['debug:start'] = 'npx reactoradar';
285
+ changed = true;
286
+ }
287
+ if (!projPkg.scripts['debug:metro']) {
288
+ projPkg.scripts['debug:metro'] = 'BROWSER=' + path.join(debuggerDir, 'bin/open-debugger.sh') + ' npx react-native start --reset-cache';
289
+ changed = true;
290
+ }
291
+ if (!projPkg.scripts['debug:remove']) {
292
+ projPkg.scripts['debug:remove'] = 'npx reactoradar remove';
293
+ changed = true;
294
+ }
295
+ if (changed) {
296
+ fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(projPkg, null, 2) + '\n');
297
+ log('Added scripts: debug:start, debug:metro, debug:remove');
298
+ } else {
299
+ log('Debug scripts already present');
300
+ }
301
+ }
302
+
303
+ // 8. Add .gitignore entry if not present
304
+ const gitignorePath = path.join(projectDir, '.gitignore');
305
+ if (fileExists(gitignorePath)) {
306
+ const gitignore = fs.readFileSync(gitignorePath, 'utf8');
307
+ if (!gitignore.includes('RNDebugSDK')) {
308
+ fs.appendFileSync(gitignorePath, '\n# ReactoRadar SDK (dev only)\nsrc/debug/RNDebugSDK.js\n');
309
+ log('Added RNDebugSDK.js to .gitignore');
310
+ }
311
+ }
312
+
313
+ // Summary
314
+ title('Setup Complete!');
315
+ console.log();
316
+ console.log(C.dim + ' Files modified:' + C.reset);
317
+ console.log(C.dim + ' + src/debug/RNDebugSDK.js (SDK)' + C.reset);
318
+ if (entryFile) console.log(C.dim + ' ~ ' + entryFile + ' (entry patched)' + C.reset);
319
+ console.log();
320
+ // Check if .dmg app is installed
321
+ const dmgInstalled = require('fs').existsSync('/Applications/ReactoRadar.app');
322
+
323
+ console.log(C.bold + ' Next steps:' + C.reset);
324
+ if (dmgInstalled) {
325
+ console.log(' 1. Start the debugger: ' + C.cyan + 'open "/Applications/ReactoRadar.app"' + C.reset);
326
+ } else {
327
+ console.log(' 1. Start the debugger: ' + C.cyan + 'npx reactoradar' + C.reset);
328
+ console.log(' ' + C.dim + '(or download .dmg from https://github.com/sharanagouda/react-native-debugger/releases)' + C.reset);
329
+ }
330
+ console.log(' 2. Run your RN app: ' + C.cyan + 'npx react-native start --reset-cache' + C.reset);
331
+ console.log(' 3. Console, Network, Storage auto-connect');
332
+ console.log();
333
+ console.log(C.bold + ' Tip: Open DevTools from simulator → launches ReactoRadar (not Chrome):' + C.reset);
334
+ console.log(' ' + C.cyan + 'BROWSER=' + path.join(debuggerDir, 'bin/open-debugger.sh') + ' npx react-native start' + C.reset);
335
+ console.log();
336
+ if (platform.hasAndroidDevice || platform.hasAndroidEmu) {
337
+ console.log(C.dim + ' Tip: If adb reverse drops, re-run:' + C.reset);
338
+ console.log(C.dim + ' adb reverse tcp:9090 tcp:9090 && adb reverse tcp:9091 tcp:9091 && adb reverse tcp:9092 tcp:9092' + C.reset);
339
+ console.log();
340
+ }
341
+ console.log(C.dim + ' To remove: npx reactoradar remove' + C.reset);
342
+ console.log();
343
+ }
344
+
345
+ // ─── Uninstall ───────────────────────────────────────────────────────────────
346
+ function uninstall(projectDir) {
347
+ title('ReactoRadar — Uninstall');
348
+ console.log();
349
+
350
+ // Remove SDK file
351
+ const sdkPath = path.join(projectDir, 'src', 'debug', 'RNDebugSDK.js');
352
+ if (fileExists(sdkPath)) {
353
+ fs.unlinkSync(sdkPath);
354
+ log('Removed src/debug/RNDebugSDK.js');
355
+ // Remove debug dir if empty
356
+ const debugDir = path.join(projectDir, 'src', 'debug');
357
+ try {
358
+ const remaining = fs.readdirSync(debugDir);
359
+ if (remaining.length === 0) {
360
+ fs.rmdirSync(debugDir);
361
+ log('Removed empty src/debug/ directory');
362
+ }
363
+ } catch {}
364
+ } else {
365
+ warn('SDK file not found — may already be removed');
366
+ }
367
+
368
+ // Remove SDK import from entry file
369
+ const entryFile = findEntryFile(projectDir);
370
+ if (entryFile) {
371
+ const entryPath = path.join(projectDir, entryFile);
372
+ let content = fs.readFileSync(entryPath, 'utf8');
373
+ const markerRe = new RegExp(
374
+ escapeRegExp(SDK_MARKER_START) + '[\\s\\S]*?' + escapeRegExp(SDK_MARKER_END) + '\\n?',
375
+ 'g'
376
+ );
377
+ const cleaned = content.replace(markerRe, '');
378
+ if (cleaned !== content) {
379
+ fs.writeFileSync(entryPath, cleaned);
380
+ log('Removed SDK import from', C.bold + entryFile + C.reset);
381
+ }
382
+ }
383
+
384
+ // Remove from store files
385
+ const storeFile = findStoreFile(projectDir);
386
+ if (storeFile) {
387
+ const storePath = path.join(projectDir, storeFile);
388
+ let content = fs.readFileSync(storePath, 'utf8');
389
+
390
+ // Remove marker blocks
391
+ const markerRe = new RegExp(
392
+ escapeRegExp(SDK_MARKER_START) + '[\\s\\S]*?' + escapeRegExp(SDK_MARKER_END) + '\\n?',
393
+ 'g'
394
+ );
395
+ let cleaned = content.replace(markerRe, '');
396
+
397
+ // Remove inline require of RNDebugSDK in configureStore
398
+ cleaned = cleaned.replace(
399
+ /\s*middleware:\s*\(getDefaultMiddleware\)\s*=>\s*\n?\s*__DEV__\s*\n?\s*\?\s*getDefaultMiddleware\(\)\.concat\(require\(['"]\.\/src\/debug\/RNDebugSDK['"]\)\.reduxMiddleware\)\s*\n?\s*:\s*getDefaultMiddleware\(\),?\n?/g,
400
+ '\n'
401
+ );
402
+ // Clean up double newlines left behind
403
+ cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
404
+
405
+ if (cleaned !== content) {
406
+ fs.writeFileSync(storePath, cleaned);
407
+ log('Removed SDK from', C.bold + storeFile + C.reset);
408
+ }
409
+ }
410
+
411
+ // Remove .gitignore entry
412
+ const gitignorePath = path.join(projectDir, '.gitignore');
413
+ if (fileExists(gitignorePath)) {
414
+ let gitignore = fs.readFileSync(gitignorePath, 'utf8');
415
+ const cleaned = gitignore.replace(/\n# ReactoRadar SDK \(dev only\)\nsrc\/debug\/RNDebugSDK\.js\n?/g, '');
416
+ if (cleaned !== gitignore) {
417
+ fs.writeFileSync(gitignorePath, cleaned);
418
+ log('Removed from .gitignore');
419
+ }
420
+ }
421
+
422
+ title('Uninstall Complete');
423
+ console.log();
424
+ }
425
+
426
+ function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
427
+
428
+ // ─── Main ────────────────────────────────────────────────────────────────────
429
+ async function main() {
430
+ const args = process.argv.slice(2);
431
+ const isUninstall = args.includes('--uninstall') || args.includes('--remove');
432
+ let projectArg = args.find(a => !a.startsWith('--'));
433
+
434
+ // If no path given, check if current directory is an RN project
435
+ if (!projectArg) {
436
+ const cwd = process.cwd();
437
+ const cwdPkg = readJSON(path.join(cwd, 'package.json'));
438
+ const cwdDeps = { ...cwdPkg?.dependencies, ...cwdPkg?.devDependencies };
439
+ if (cwdPkg && cwdDeps['react-native']) {
440
+ // Running from inside an RN project — use current directory
441
+ projectArg = cwd;
442
+ info('Detected RN project in current directory:', C.bold + cwdPkg.name + C.reset);
443
+ } else {
444
+ // Not in an RN project — show help
445
+ console.log();
446
+ console.log(C.bold + ' ReactoRadar — Setup CLI' + C.reset);
447
+ console.log();
448
+ console.log(' Run from inside your React Native project:');
449
+ console.log(' ' + C.cyan + 'node /path/to/rn-debug-app/bin/setup.js' + C.reset);
450
+ console.log();
451
+ console.log(' Or specify the path:');
452
+ console.log(' ' + C.cyan + 'node /path/to/rn-debug-app/bin/setup.js /path/to/rn-project' + C.reset);
453
+ console.log(' ' + C.cyan + 'node /path/to/rn-debug-app/bin/setup.js /path/to/rn-project --uninstall' + C.reset);
454
+ console.log();
455
+ process.exit(0);
456
+ }
457
+ }
458
+
459
+ const projectDir = path.resolve(projectArg);
460
+
461
+ if (!dirExists(projectDir)) {
462
+ err('Directory not found:', projectDir);
463
+ process.exit(1);
464
+ }
465
+
466
+ if (isUninstall) {
467
+ uninstall(projectDir);
468
+ } else {
469
+ await install(projectDir);
470
+ }
471
+ }
472
+
473
+ main().catch(e => { err(e.message); process.exit(1); });