agent-react-devtools 0.0.0 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # agent-react-devtools
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d1e02f9: **Daemon, DevTools bridge, and component tree**
8
+
9
+ **Daemon** — Persistent background process with IPC server (Unix socket) that manages connections and dispatches commands.
10
+
11
+ **DevTools Bridge** — WebSocket server implementing the React DevTools "Wall" protocol. Connects to running React apps via `react-devtools-core`.
12
+
13
+ **Component Tree** — Parse and inspect the full React component hierarchy:
14
+
15
+ - View component tree with types, keys, and parent/child relationships
16
+ - Inspect props, state, and hooks of any component
17
+ - Search components by display name (fuzzy or exact match)
18
+ - Count components by type
19
+
20
+ **CLI** — Command-line interface with commands: `start`, `stop`, `status`, `tree`, `find`, `count`, `get component`.
21
+
22
+ **Formatting** — Token-efficient output designed for LLM consumption.
23
+
24
+ - 626a21a: **Profiler**
25
+
26
+ Start and stop profiling sessions to capture render performance data from connected React apps.
27
+
28
+ - **Render reports** — Per-component render duration and count
29
+ - **Slowest components** — Ranked by self render time
30
+ - **Most re-rendered** — Ranked by render count
31
+ - **Commit timeline** — Chronological view of React commits with durations
32
+ - **Commit details** — Per-component breakdown for a specific commit, sorted by self time
33
+
34
+ CLI commands: `profile start`, `profile stop`, `profile report`, `profile slow`, `profile rerenders`, `profile timeline`, `profile commit`.
package/dist/cli.js ADDED
@@ -0,0 +1,584 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/daemon-client.ts
4
+ import net from "net";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { spawn } from "child_process";
8
+ var DEFAULT_STATE_DIR = path.join(
9
+ process.env.HOME || process.env.USERPROFILE || "/tmp",
10
+ ".agent-react-devtools"
11
+ );
12
+ var stateDir = DEFAULT_STATE_DIR;
13
+ function setStateDir(dir) {
14
+ stateDir = dir;
15
+ }
16
+ function getDaemonInfoPath() {
17
+ return path.join(stateDir, "daemon.json");
18
+ }
19
+ function getSocketPath() {
20
+ return path.join(stateDir, "daemon.sock");
21
+ }
22
+ function readDaemonInfo() {
23
+ try {
24
+ const raw = fs.readFileSync(getDaemonInfoPath(), "utf-8");
25
+ return JSON.parse(raw);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ function isDaemonAlive(info) {
31
+ try {
32
+ process.kill(info.pid, 0);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ async function ensureDaemon(port) {
39
+ const info = readDaemonInfo();
40
+ if (info && isDaemonAlive(info)) {
41
+ return;
42
+ }
43
+ try {
44
+ fs.unlinkSync(getDaemonInfoPath());
45
+ } catch {
46
+ }
47
+ try {
48
+ fs.unlinkSync(getSocketPath());
49
+ } catch {
50
+ }
51
+ const daemonScript = path.join(
52
+ path.dirname(new URL(import.meta.url).pathname),
53
+ "daemon.js"
54
+ );
55
+ const args = [];
56
+ if (port) args.push(`--port=${port}`);
57
+ if (stateDir !== DEFAULT_STATE_DIR) args.push(`--state-dir=${stateDir}`);
58
+ const child = spawn(process.execPath, [daemonScript, ...args], {
59
+ detached: true,
60
+ stdio: "ignore"
61
+ });
62
+ child.unref();
63
+ const deadline = Date.now() + 5e3;
64
+ while (Date.now() < deadline) {
65
+ await new Promise((r) => setTimeout(r, 100));
66
+ try {
67
+ await sendCommand({ type: "ping" });
68
+ return;
69
+ } catch {
70
+ }
71
+ }
72
+ throw new Error("Daemon failed to start within 5 seconds");
73
+ }
74
+ function stopDaemon() {
75
+ const info = readDaemonInfo();
76
+ if (!info) return false;
77
+ try {
78
+ process.kill(info.pid, "SIGTERM");
79
+ try {
80
+ fs.unlinkSync(getDaemonInfoPath());
81
+ } catch {
82
+ }
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+ function sendCommand(cmd) {
89
+ return new Promise((resolve, reject) => {
90
+ const socketPath = getSocketPath();
91
+ const conn = net.createConnection(socketPath, () => {
92
+ conn.write(JSON.stringify(cmd) + "\n");
93
+ });
94
+ let buffer = "";
95
+ conn.on("data", (chunk) => {
96
+ buffer += chunk.toString();
97
+ const newlineIdx = buffer.indexOf("\n");
98
+ if (newlineIdx !== -1) {
99
+ const line = buffer.slice(0, newlineIdx);
100
+ conn.end();
101
+ try {
102
+ resolve(JSON.parse(line));
103
+ } catch {
104
+ reject(new Error("Invalid response from daemon"));
105
+ }
106
+ }
107
+ });
108
+ conn.on("error", (err) => {
109
+ reject(new Error(`Cannot connect to daemon: ${err.message}`));
110
+ });
111
+ conn.setTimeout(3e4, () => {
112
+ conn.destroy();
113
+ reject(new Error("Command timed out"));
114
+ });
115
+ });
116
+ }
117
+
118
+ // src/formatters.ts
119
+ var TYPE_ABBREV = {
120
+ function: "fn",
121
+ class: "cls",
122
+ host: "host",
123
+ memo: "memo",
124
+ forwardRef: "fRef",
125
+ profiler: "prof",
126
+ suspense: "susp",
127
+ context: "ctx",
128
+ other: "?"
129
+ };
130
+ function typeTag(type) {
131
+ return TYPE_ABBREV[type] || type;
132
+ }
133
+ var PIPE = "\u2502 ";
134
+ var TEE = "\u251C\u2500 ";
135
+ var ELBOW = "\u2514\u2500 ";
136
+ var SPACE = " ";
137
+ function formatTree(nodes) {
138
+ if (nodes.length === 0) return "No components (is a React app connected?)";
139
+ const childrenMap = /* @__PURE__ */ new Map();
140
+ for (const node of nodes) {
141
+ const parentId = node.parentId;
142
+ let siblings = childrenMap.get(parentId);
143
+ if (!siblings) {
144
+ siblings = [];
145
+ childrenMap.set(parentId, siblings);
146
+ }
147
+ siblings.push(node);
148
+ }
149
+ const lines = [];
150
+ function walk(nodeId, prefix, isLast, isRoot) {
151
+ const node = nodes.find((n) => n.id === nodeId);
152
+ if (!node) return;
153
+ const connector = isRoot ? "" : isLast ? ELBOW : TEE;
154
+ let line = `${node.label} [${typeTag(node.type)}] "${node.displayName}"`;
155
+ if (node.key) line += ` key="${node.key}"`;
156
+ lines.push(`${prefix}${connector}${line}`);
157
+ const children = childrenMap.get(node.id) || [];
158
+ const childPrefix = isRoot ? "" : prefix + (isLast ? SPACE : PIPE);
159
+ for (let i = 0; i < children.length; i++) {
160
+ walk(children[i].id, childPrefix, i === children.length - 1, false);
161
+ }
162
+ }
163
+ const roots = childrenMap.get(null) || [];
164
+ for (let i = 0; i < roots.length; i++) {
165
+ walk(roots[i].id, "", i === roots.length - 1, true);
166
+ }
167
+ return lines.join("\n");
168
+ }
169
+ function formatComponent(element, label) {
170
+ const lines = [];
171
+ const ref = label || `#${element.id}`;
172
+ let header = `${ref} [${typeTag(element.type)}] "${element.displayName}"`;
173
+ if (element.key) header += ` key="${element.key}"`;
174
+ lines.push(header);
175
+ if (element.props && Object.keys(element.props).length > 0) {
176
+ lines.push("props:");
177
+ for (const [key, value] of Object.entries(element.props)) {
178
+ lines.push(` ${key}: ${formatCompactValue(value) ?? "undefined"}`);
179
+ }
180
+ }
181
+ if (element.state && Object.keys(element.state).length > 0) {
182
+ lines.push("state:");
183
+ for (const [key, value] of Object.entries(element.state)) {
184
+ lines.push(` ${key}: ${formatCompactValue(value) ?? "undefined"}`);
185
+ }
186
+ }
187
+ if (element.hooks && element.hooks.length > 0) {
188
+ lines.push("hooks:");
189
+ for (const h of element.hooks) {
190
+ const val = formatCompactValue(h.value);
191
+ lines.push(val !== void 0 ? ` ${h.name}: ${val}` : ` ${h.name}`);
192
+ if (h.subHooks && h.subHooks.length > 0) {
193
+ for (const sh of h.subHooks) {
194
+ const sval = formatCompactValue(sh.value);
195
+ lines.push(sval !== void 0 ? ` ${sh.name}: ${sval}` : ` ${sh.name}`);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ return lines.join("\n");
201
+ }
202
+ function formatSearchResults(results) {
203
+ if (results.length === 0) return "No components found";
204
+ return results.map((n) => {
205
+ let line = `${n.label} [${typeTag(n.type)}] "${n.displayName}"`;
206
+ if (n.key) line += ` key="${n.key}"`;
207
+ return line;
208
+ }).join("\n");
209
+ }
210
+ function formatCount(counts) {
211
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
212
+ const parts = Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([type, count]) => `${typeTag(type)}:${count}`).join(" ");
213
+ return `${total} components (${parts})`;
214
+ }
215
+ function formatStatus(status) {
216
+ const lines = [];
217
+ lines.push(`Daemon: running (port ${status.port})`);
218
+ lines.push(
219
+ `Apps: ${status.connectedApps} connected, ${status.componentCount} components`
220
+ );
221
+ if (status.profilingActive) {
222
+ lines.push("Profiling: active");
223
+ }
224
+ const upSec = Math.round(status.uptime / 1e3);
225
+ lines.push(`Uptime: ${upSec}s`);
226
+ return lines.join("\n");
227
+ }
228
+ function formatProfileSummary(summary) {
229
+ const lines = [];
230
+ const durSec = (summary.duration / 1e3).toFixed(1);
231
+ lines.push(
232
+ `Profile "${summary.name}" (${durSec}s, ${summary.commitCount} commits)`
233
+ );
234
+ if (summary.componentRenderCounts.length > 0) {
235
+ lines.push("");
236
+ lines.push("Top renders:");
237
+ for (const c of summary.componentRenderCounts.slice(0, 10)) {
238
+ const name = c.displayName || `#${c.id}`;
239
+ lines.push(` ${name} ${c.count} renders`);
240
+ }
241
+ }
242
+ return lines.join("\n");
243
+ }
244
+ function formatProfileReport(report, label) {
245
+ const lines = [];
246
+ const ref = label || `#${report.id}`;
247
+ lines.push(`${ref} "${report.displayName}"`);
248
+ lines.push(
249
+ `renders:${report.renderCount} avg:${report.avgDuration.toFixed(1)}ms max:${report.maxDuration.toFixed(1)}ms total:${report.totalDuration.toFixed(1)}ms`
250
+ );
251
+ if (report.causes.length > 0) {
252
+ lines.push(`causes: ${report.causes.join(", ")}`);
253
+ }
254
+ return lines.join("\n");
255
+ }
256
+ function formatSlowest(reports) {
257
+ if (reports.length === 0) return "No profiling data";
258
+ const lines = ["Slowest (by avg render time):"];
259
+ for (const r of reports) {
260
+ const cause = r.causes[0] || "?";
261
+ lines.push(
262
+ ` ${pad(r.displayName, 20)} avg:${r.avgDuration.toFixed(1)}ms max:${r.maxDuration.toFixed(1)}ms renders:${r.renderCount} cause:${cause}`
263
+ );
264
+ }
265
+ return lines.join("\n");
266
+ }
267
+ function formatRerenders(reports) {
268
+ if (reports.length === 0) return "No profiling data";
269
+ const lines = ["Most re-renders:"];
270
+ for (const r of reports) {
271
+ const cause = r.causes[0] || "?";
272
+ lines.push(
273
+ ` ${pad(r.displayName, 20)} ${r.renderCount} renders \u2014 ${cause}`
274
+ );
275
+ }
276
+ return lines.join("\n");
277
+ }
278
+ function formatTimeline(entries) {
279
+ if (entries.length === 0) return "No profiling data";
280
+ const lines = ["Commit timeline:"];
281
+ for (const e of entries) {
282
+ lines.push(
283
+ ` #${e.index} ${e.duration.toFixed(1)}ms ${e.componentCount} components`
284
+ );
285
+ }
286
+ return lines.join("\n");
287
+ }
288
+ function formatCommitDetail(detail) {
289
+ const lines = [];
290
+ lines.push(`Commit #${detail.index} ${detail.duration.toFixed(1)}ms ${detail.totalComponents} components`);
291
+ lines.push("");
292
+ for (const c of detail.components) {
293
+ const causes = c.causes.length > 0 ? c.causes.join(", ") : "?";
294
+ lines.push(` ${pad(c.displayName, 24)} self:${c.selfDuration.toFixed(1)}ms total:${c.actualDuration.toFixed(1)}ms ${causes}`);
295
+ }
296
+ const hidden = detail.totalComponents - detail.components.length;
297
+ if (hidden > 0) {
298
+ lines.push(` ... ${hidden} more (use --limit to show more)`);
299
+ }
300
+ return lines.join("\n");
301
+ }
302
+ function formatCompactValue(val) {
303
+ if (val === void 0) return void 0;
304
+ if (val === null) return "null";
305
+ if (typeof val === "function") return "\u0192";
306
+ if (typeof val === "string") return `"${val}"`;
307
+ if (typeof val === "number" || typeof val === "boolean") return String(val);
308
+ try {
309
+ const s = JSON.stringify(val, replacer, 0);
310
+ if (s && s.length > 60) return s.slice(0, 57) + "...";
311
+ return s || String(val);
312
+ } catch {
313
+ return String(val);
314
+ }
315
+ }
316
+ function replacer(_key, value) {
317
+ if (typeof value === "function") return "\u0192";
318
+ return value;
319
+ }
320
+ function pad(s, len) {
321
+ return s.length >= len ? s : s + " ".repeat(len - s.length);
322
+ }
323
+
324
+ // src/cli.ts
325
+ function usage() {
326
+ return `Usage: devtools <command> [options]
327
+
328
+ Daemon:
329
+ start [--port 8097] Start daemon
330
+ stop Stop daemon
331
+ status Show daemon status
332
+
333
+ Components:
334
+ get tree [--depth N] Component hierarchy
335
+ get component <@c1 | id> Props, state, hooks
336
+ find <name> [--exact] Search by display name
337
+ count Component count by type
338
+
339
+ Profiling:
340
+ profile start [name] Start profiling session
341
+ profile stop Stop profiling, collect data
342
+ profile report <@c1 | id> Render report for component
343
+ profile slow [--limit N] Slowest components (by avg)
344
+ profile rerenders [--limit N] Most re-rendered components
345
+ profile timeline [--limit N] Commit timeline
346
+ profile commit <N | #N> [--limit N] Detail for specific commit`;
347
+ }
348
+ function parseArgs(argv) {
349
+ const command = [];
350
+ const flags = {};
351
+ for (let i = 0; i < argv.length; i++) {
352
+ const arg = argv[i];
353
+ if (arg.startsWith("--")) {
354
+ const key = arg.slice(2);
355
+ const eqIdx = key.indexOf("=");
356
+ if (eqIdx !== -1) {
357
+ flags[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
358
+ } else {
359
+ const next = argv[i + 1];
360
+ if (next && !next.startsWith("--")) {
361
+ flags[key] = next;
362
+ i++;
363
+ } else {
364
+ flags[key] = true;
365
+ }
366
+ }
367
+ } else {
368
+ command.push(arg);
369
+ }
370
+ }
371
+ return { command, flags };
372
+ }
373
+ async function main() {
374
+ const { command, flags } = parseArgs(process.argv.slice(2));
375
+ if (command.length === 0 || flags["help"]) {
376
+ console.log(usage());
377
+ process.exit(0);
378
+ }
379
+ if (typeof flags["state-dir"] === "string") {
380
+ setStateDir(flags["state-dir"]);
381
+ }
382
+ const cmd0 = command[0];
383
+ const cmd1 = command[1];
384
+ try {
385
+ if (cmd0 === "start") {
386
+ const port = flags["port"] ? parseInt(flags["port"], 10) : void 0;
387
+ await ensureDaemon(port);
388
+ const resp = await sendCommand({ type: "status" });
389
+ if (resp.ok) {
390
+ console.log(formatStatus(resp.data));
391
+ }
392
+ return;
393
+ }
394
+ if (cmd0 === "stop") {
395
+ const stopped = stopDaemon();
396
+ console.log(stopped ? "Daemon stopped" : "Daemon is not running");
397
+ return;
398
+ }
399
+ if (cmd0 === "status") {
400
+ const info = readDaemonInfo();
401
+ if (!info) {
402
+ console.log("Daemon is not running");
403
+ process.exit(1);
404
+ }
405
+ try {
406
+ const resp = await sendCommand({ type: "status" });
407
+ if (resp.ok) {
408
+ console.log(formatStatus(resp.data));
409
+ } else {
410
+ console.error(resp.error);
411
+ process.exit(1);
412
+ }
413
+ } catch {
414
+ console.log("Daemon is not running (stale info)");
415
+ process.exit(1);
416
+ }
417
+ return;
418
+ }
419
+ await ensureDaemon();
420
+ if (cmd0 === "get" && cmd1 === "tree") {
421
+ const depth = flags["depth"] ? parseInt(flags["depth"], 10) : void 0;
422
+ const ipcCmd = { type: "get-tree", depth };
423
+ const resp = await sendCommand(ipcCmd);
424
+ if (resp.ok) {
425
+ console.log(formatTree(resp.data));
426
+ } else {
427
+ console.error(resp.error);
428
+ process.exit(1);
429
+ }
430
+ return;
431
+ }
432
+ if (cmd0 === "get" && cmd1 === "component") {
433
+ const raw = command[2];
434
+ if (!raw) {
435
+ console.error("Usage: devtools get component <@c1 | id>");
436
+ process.exit(1);
437
+ }
438
+ const id = raw.startsWith("@") ? raw : parseInt(raw, 10);
439
+ if (typeof id === "number" && isNaN(id)) {
440
+ console.error("Usage: devtools get component <@c1 | id>");
441
+ process.exit(1);
442
+ }
443
+ const resp = await sendCommand({ type: "get-component", id });
444
+ if (resp.ok) {
445
+ console.log(formatComponent(resp.data, resp.label));
446
+ } else {
447
+ console.error(resp.error);
448
+ process.exit(1);
449
+ }
450
+ return;
451
+ }
452
+ if (cmd0 === "find") {
453
+ const name = command[1];
454
+ if (!name) {
455
+ console.error("Usage: devtools find <name> [--exact]");
456
+ process.exit(1);
457
+ }
458
+ const exact = flags["exact"] === true;
459
+ const resp = await sendCommand({ type: "find", name, exact });
460
+ if (resp.ok) {
461
+ console.log(formatSearchResults(resp.data));
462
+ } else {
463
+ console.error(resp.error);
464
+ process.exit(1);
465
+ }
466
+ return;
467
+ }
468
+ if (cmd0 === "count") {
469
+ const resp = await sendCommand({ type: "count" });
470
+ if (resp.ok) {
471
+ console.log(formatCount(resp.data));
472
+ } else {
473
+ console.error(resp.error);
474
+ process.exit(1);
475
+ }
476
+ return;
477
+ }
478
+ if (cmd0 === "profile" && cmd1 === "start") {
479
+ const name = command[2];
480
+ const resp = await sendCommand({ type: "profile-start", name });
481
+ if (resp.ok) {
482
+ console.log(resp.data);
483
+ } else {
484
+ console.error(resp.error);
485
+ process.exit(1);
486
+ }
487
+ return;
488
+ }
489
+ if (cmd0 === "profile" && cmd1 === "stop") {
490
+ const resp = await sendCommand({ type: "profile-stop" });
491
+ if (resp.ok) {
492
+ console.log(formatProfileSummary(resp.data));
493
+ } else {
494
+ console.error(resp.error);
495
+ process.exit(1);
496
+ }
497
+ return;
498
+ }
499
+ if (cmd0 === "profile" && cmd1 === "report") {
500
+ const raw = command[2];
501
+ if (!raw) {
502
+ console.error("Usage: devtools profile report <@c1 | id>");
503
+ process.exit(1);
504
+ }
505
+ const componentId = raw.startsWith("@") ? raw : parseInt(raw, 10);
506
+ if (typeof componentId === "number" && isNaN(componentId)) {
507
+ console.error("Usage: devtools profile report <@c1 | id>");
508
+ process.exit(1);
509
+ }
510
+ const resp = await sendCommand({ type: "profile-report", componentId });
511
+ if (resp.ok) {
512
+ console.log(formatProfileReport(resp.data, resp.label));
513
+ } else {
514
+ console.error(resp.error);
515
+ process.exit(1);
516
+ }
517
+ return;
518
+ }
519
+ if (cmd0 === "profile" && cmd1 === "slow") {
520
+ const limit = flags["limit"] ? parseInt(flags["limit"], 10) : void 0;
521
+ const resp = await sendCommand({ type: "profile-slow", limit });
522
+ if (resp.ok) {
523
+ console.log(formatSlowest(resp.data));
524
+ } else {
525
+ console.error(resp.error);
526
+ process.exit(1);
527
+ }
528
+ return;
529
+ }
530
+ if (cmd0 === "profile" && cmd1 === "rerenders") {
531
+ const limit = flags["limit"] ? parseInt(flags["limit"], 10) : void 0;
532
+ const resp = await sendCommand({ type: "profile-rerenders", limit });
533
+ if (resp.ok) {
534
+ console.log(formatRerenders(resp.data));
535
+ } else {
536
+ console.error(resp.error);
537
+ process.exit(1);
538
+ }
539
+ return;
540
+ }
541
+ if (cmd0 === "profile" && cmd1 === "commit") {
542
+ const raw = command[2];
543
+ if (!raw) {
544
+ console.error("Usage: devtools profile commit <N | #N>");
545
+ process.exit(1);
546
+ }
547
+ const index = parseInt(raw.replace(/^#/, ""), 10);
548
+ if (isNaN(index)) {
549
+ console.error("Usage: devtools profile commit <N | #N>");
550
+ process.exit(1);
551
+ }
552
+ const limit = flags["limit"] ? parseInt(flags["limit"], 10) : void 0;
553
+ const resp = await sendCommand({ type: "profile-commit", index, limit });
554
+ if (resp.ok) {
555
+ console.log(formatCommitDetail(resp.data));
556
+ } else {
557
+ console.error(resp.error);
558
+ process.exit(1);
559
+ }
560
+ return;
561
+ }
562
+ if (cmd0 === "profile" && cmd1 === "timeline") {
563
+ const limit = flags["limit"] ? parseInt(flags["limit"], 10) : void 0;
564
+ const resp = await sendCommand({ type: "profile-timeline", limit });
565
+ if (resp.ok) {
566
+ console.log(formatTimeline(resp.data));
567
+ } else {
568
+ console.error(resp.error);
569
+ process.exit(1);
570
+ }
571
+ return;
572
+ }
573
+ console.error(`Unknown command: ${command.join(" ")}`);
574
+ console.log(usage());
575
+ process.exit(1);
576
+ } catch (err) {
577
+ console.error(
578
+ err instanceof Error ? err.message : String(err)
579
+ );
580
+ process.exit(1);
581
+ }
582
+ }
583
+ main();
584
+ //# sourceMappingURL=cli.js.map