tui-devtools 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 +98 -0
- package/dist/bin/tui-devtools.js +409 -0
- package/dist/bin/tui-devtools.js.map +1 -0
- package/dist/chunk-UV6LYAWB.js +925 -0
- package/dist/chunk-UV6LYAWB.js.map +1 -0
- package/dist/daemon-27VEN2X5.js +18 -0
- package/dist/daemon-27VEN2X5.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/daemon.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import net from "net";
|
|
7
|
+
|
|
8
|
+
// src/server.ts
|
|
9
|
+
import { WebSocketServer } from "ws";
|
|
10
|
+
|
|
11
|
+
// src/store.ts
|
|
12
|
+
var MAX_LOGS = 500;
|
|
13
|
+
var DevToolsStore = class {
|
|
14
|
+
roots = /* @__PURE__ */ new Map();
|
|
15
|
+
nodes = /* @__PURE__ */ new Map();
|
|
16
|
+
logs = [];
|
|
17
|
+
connected = false;
|
|
18
|
+
appName = null;
|
|
19
|
+
rendererIds = [];
|
|
20
|
+
// Pending inspection responses keyed by request ID
|
|
21
|
+
inspectCallbacks = /* @__PURE__ */ new Map();
|
|
22
|
+
nextRequestId = 1;
|
|
23
|
+
addNode(node) {
|
|
24
|
+
this.nodes.set(node.id, node);
|
|
25
|
+
if (node.parentId === null || node.parentId === 0) {
|
|
26
|
+
this.roots.set(node.id, node);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
removeNode(id) {
|
|
30
|
+
const node = this.nodes.get(id);
|
|
31
|
+
if (node) {
|
|
32
|
+
if (node.parentId !== null) {
|
|
33
|
+
const parent = this.nodes.get(node.parentId);
|
|
34
|
+
if (parent) {
|
|
35
|
+
parent.childIds = parent.childIds.filter((cid) => cid !== id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const childId of node.childIds) {
|
|
39
|
+
this.removeNode(childId);
|
|
40
|
+
}
|
|
41
|
+
this.nodes.delete(id);
|
|
42
|
+
this.roots.delete(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
addLog(entry) {
|
|
46
|
+
this.logs.push(entry);
|
|
47
|
+
if (this.logs.length > MAX_LOGS) {
|
|
48
|
+
this.logs = this.logs.slice(-MAX_LOGS);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
findByName(name) {
|
|
52
|
+
for (const node of this.nodes.values()) {
|
|
53
|
+
if (node.displayName === name) return node;
|
|
54
|
+
}
|
|
55
|
+
for (const node of this.nodes.values()) {
|
|
56
|
+
if (node.displayName?.includes(name)) return node;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
findAllByName(name) {
|
|
61
|
+
const results = [];
|
|
62
|
+
for (const node of this.nodes.values()) {
|
|
63
|
+
if (node.displayName?.includes(name)) results.push(node);
|
|
64
|
+
}
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
getTree(rootId, maxDepth = 20) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
const roots = rootId ? [this.nodes.get(rootId)].filter(Boolean) : [...this.roots.values()];
|
|
70
|
+
for (const root of roots) {
|
|
71
|
+
this.printNode(root, 0, maxDepth, lines);
|
|
72
|
+
}
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
getTreeJson(rootId, maxDepth = 20) {
|
|
76
|
+
const roots = rootId ? [this.nodes.get(rootId)].filter(Boolean) : [...this.roots.values()];
|
|
77
|
+
return roots.map((r) => this.nodeToJson(r, 0, maxDepth));
|
|
78
|
+
}
|
|
79
|
+
printNode(node, depth, maxDepth, lines) {
|
|
80
|
+
if (depth > maxDepth) return;
|
|
81
|
+
const indent = " ".repeat(depth);
|
|
82
|
+
const name = node.displayName || `<${node.type}>`;
|
|
83
|
+
const keyStr = node.key ? ` key="${node.key}"` : "";
|
|
84
|
+
lines.push(`${indent}${name}${keyStr} [${node.id}]`);
|
|
85
|
+
for (const childId of node.childIds) {
|
|
86
|
+
const child = this.nodes.get(childId);
|
|
87
|
+
if (child) this.printNode(child, depth + 1, maxDepth, lines);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
nodeToJson(node, depth, maxDepth) {
|
|
91
|
+
const children = depth < maxDepth ? node.childIds.map((id) => this.nodes.get(id)).filter(Boolean).map((child) => this.nodeToJson(child, depth + 1, maxDepth)) : [];
|
|
92
|
+
return {
|
|
93
|
+
id: node.id,
|
|
94
|
+
name: node.displayName,
|
|
95
|
+
type: node.type,
|
|
96
|
+
key: node.key,
|
|
97
|
+
children
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
registerInspectCallback(callback) {
|
|
101
|
+
const id = this.nextRequestId++;
|
|
102
|
+
this.inspectCallbacks.set(id, callback);
|
|
103
|
+
return id;
|
|
104
|
+
}
|
|
105
|
+
resolveInspectCallback(requestId, data) {
|
|
106
|
+
const cb = this.inspectCallbacks.get(requestId);
|
|
107
|
+
if (cb) {
|
|
108
|
+
this.inspectCallbacks.delete(requestId);
|
|
109
|
+
cb(data);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
clear() {
|
|
113
|
+
this.roots.clear();
|
|
114
|
+
this.nodes.clear();
|
|
115
|
+
this.logs = [];
|
|
116
|
+
this.connected = false;
|
|
117
|
+
this.appName = null;
|
|
118
|
+
this.rendererIds = [];
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/types.ts
|
|
123
|
+
var TREE_OPERATION_ADD = 1;
|
|
124
|
+
var TREE_OPERATION_REMOVE = 2;
|
|
125
|
+
var TREE_OPERATION_REORDER_CHILDREN = 3;
|
|
126
|
+
var TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
|
|
127
|
+
var TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
|
|
128
|
+
var TREE_OPERATION_REMOVE_ROOT = 6;
|
|
129
|
+
var TREE_OPERATION_SET_SUBTREE_MODE = 7;
|
|
130
|
+
var ElementTypeClass = 1;
|
|
131
|
+
var ElementTypeFunction = 2;
|
|
132
|
+
var ElementTypeHostComponent = 5;
|
|
133
|
+
var ElementTypeRoot = 9;
|
|
134
|
+
|
|
135
|
+
// src/bridge.ts
|
|
136
|
+
var DevToolsBridge = class {
|
|
137
|
+
ws = null;
|
|
138
|
+
store;
|
|
139
|
+
onLog;
|
|
140
|
+
debugLog;
|
|
141
|
+
constructor(store, onLog, debugLog) {
|
|
142
|
+
this.store = store;
|
|
143
|
+
this.onLog = onLog;
|
|
144
|
+
this.debugLog = debugLog;
|
|
145
|
+
}
|
|
146
|
+
attach(ws) {
|
|
147
|
+
this.ws = ws;
|
|
148
|
+
this.store.connected = true;
|
|
149
|
+
ws.on("message", (data) => {
|
|
150
|
+
try {
|
|
151
|
+
const raw = typeof data === "string" ? data : data.toString();
|
|
152
|
+
const msg = JSON.parse(raw);
|
|
153
|
+
this.debugLog?.(`[MSG] event=${msg.event} payload_type=${typeof msg.payload} payload_keys=${msg.payload && typeof msg.payload === "object" ? Object.keys(msg.payload).join(",") : Array.isArray(msg.payload) ? `array[${msg.payload.length}]` : String(msg.payload)?.slice(0, 80)}`);
|
|
154
|
+
this.handleMessage(msg);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
this.debugLog?.(`[PARSE_ERR] ${e.message} raw=${(typeof data === "string" ? data : data.toString()).slice(0, 200)}`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
ws.on("close", () => {
|
|
160
|
+
this.store.connected = false;
|
|
161
|
+
this.ws = null;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
handleMessage(msg) {
|
|
165
|
+
switch (msg.event) {
|
|
166
|
+
case "operations": {
|
|
167
|
+
let ops;
|
|
168
|
+
if (Array.isArray(msg.payload)) {
|
|
169
|
+
ops = msg.payload;
|
|
170
|
+
} else if (msg.payload && typeof msg.payload === "object") {
|
|
171
|
+
const obj = msg.payload;
|
|
172
|
+
const keys = Object.keys(obj).map(Number).sort((a, b) => a - b);
|
|
173
|
+
ops = keys.map((k) => obj[String(k)]);
|
|
174
|
+
} else {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
this.debugLog?.(`[OPS] parsed ${ops.length} entries: [${ops.join(",")}]`);
|
|
178
|
+
this.handleOperations(ops);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "inspectedElement":
|
|
182
|
+
this.handleInspectedElement(msg.payload);
|
|
183
|
+
break;
|
|
184
|
+
case "shutdown":
|
|
185
|
+
this.store.connected = false;
|
|
186
|
+
break;
|
|
187
|
+
// Console capture — DevTools patches console methods and sends logs
|
|
188
|
+
case "console-log":
|
|
189
|
+
case "console-warn":
|
|
190
|
+
case "console-error":
|
|
191
|
+
case "console-info":
|
|
192
|
+
case "console-debug": {
|
|
193
|
+
const level = msg.event.replace("console-", "");
|
|
194
|
+
const entry = {
|
|
195
|
+
level,
|
|
196
|
+
args: Array.isArray(msg.payload) ? msg.payload : [msg.payload],
|
|
197
|
+
timestamp: Date.now()
|
|
198
|
+
};
|
|
199
|
+
this.store.addLog(entry);
|
|
200
|
+
this.onLog?.(entry);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
// DevTools backend sends renderer info
|
|
204
|
+
case "renderer": {
|
|
205
|
+
const payload = msg.payload;
|
|
206
|
+
if (payload?.id != null) {
|
|
207
|
+
this.store.rendererIds.push(payload.id);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case "renderer-attached": {
|
|
212
|
+
const payload = msg.payload;
|
|
213
|
+
if (payload?.id != null && !this.store.rendererIds.includes(payload.id)) {
|
|
214
|
+
this.store.rendererIds.push(payload.id);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
handleOperations(ops) {
|
|
221
|
+
if (!ops || ops.length === 0) return;
|
|
222
|
+
let i = 0;
|
|
223
|
+
const rendererId = ops[i++];
|
|
224
|
+
const rootId = ops[i++];
|
|
225
|
+
if (!this.store.rendererIds.includes(rendererId)) {
|
|
226
|
+
this.store.rendererIds.push(rendererId);
|
|
227
|
+
}
|
|
228
|
+
const stringTableSize = ops[i++];
|
|
229
|
+
const stringTable = [""];
|
|
230
|
+
const stringTableEnd = i + stringTableSize;
|
|
231
|
+
while (i < stringTableEnd) {
|
|
232
|
+
const len = ops[i++];
|
|
233
|
+
const chars = [];
|
|
234
|
+
for (let j = 0; j < len; j++) {
|
|
235
|
+
chars.push(String.fromCharCode(ops[i++]));
|
|
236
|
+
}
|
|
237
|
+
stringTable.push(chars.join(""));
|
|
238
|
+
}
|
|
239
|
+
this.debugLog?.(`[OPS] stringTable=[${stringTable.join(",")}] remaining=${ops.length - i} ops`);
|
|
240
|
+
while (i < ops.length) {
|
|
241
|
+
const op = ops[i++];
|
|
242
|
+
switch (op) {
|
|
243
|
+
case TREE_OPERATION_ADD: {
|
|
244
|
+
const id = ops[i++];
|
|
245
|
+
const elementType = ops[i++];
|
|
246
|
+
const parentId = ops[i++];
|
|
247
|
+
const ownerID = ops[i++];
|
|
248
|
+
void ownerID;
|
|
249
|
+
const displayNameIndex = ops[i++];
|
|
250
|
+
const keyIndex = ops[i++];
|
|
251
|
+
const displayName = stringTable[displayNameIndex] || null;
|
|
252
|
+
const key = stringTable[keyIndex] || null;
|
|
253
|
+
let type = "other";
|
|
254
|
+
if (elementType === ElementTypeFunction) type = "function";
|
|
255
|
+
else if (elementType === ElementTypeClass) type = "class";
|
|
256
|
+
else if (elementType === ElementTypeHostComponent) type = "host";
|
|
257
|
+
else if (elementType === ElementTypeRoot) type = "other";
|
|
258
|
+
const resolvedParentId = parentId === 0 ? null : parentId;
|
|
259
|
+
const node = {
|
|
260
|
+
id,
|
|
261
|
+
displayName,
|
|
262
|
+
type,
|
|
263
|
+
parentId: resolvedParentId,
|
|
264
|
+
childIds: [],
|
|
265
|
+
key
|
|
266
|
+
};
|
|
267
|
+
this.debugLog?.(`[ADD] id=${id} type=${type} parent=${resolvedParentId} name=${displayName}`);
|
|
268
|
+
this.store.addNode(node);
|
|
269
|
+
if (resolvedParentId != null) {
|
|
270
|
+
const parent = this.store.nodes.get(resolvedParentId);
|
|
271
|
+
if (parent && !parent.childIds.includes(id)) {
|
|
272
|
+
parent.childIds.push(id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case TREE_OPERATION_REMOVE: {
|
|
278
|
+
const removeCount = ops[i++];
|
|
279
|
+
for (let j = 0; j < removeCount; j++) {
|
|
280
|
+
const id = ops[i++];
|
|
281
|
+
this.store.removeNode(id);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case TREE_OPERATION_REORDER_CHILDREN: {
|
|
286
|
+
const id = ops[i++];
|
|
287
|
+
const childCount = ops[i++];
|
|
288
|
+
const newChildIds = [];
|
|
289
|
+
for (let j = 0; j < childCount; j++) {
|
|
290
|
+
newChildIds.push(ops[i++]);
|
|
291
|
+
}
|
|
292
|
+
const node = this.store.nodes.get(id);
|
|
293
|
+
if (node) node.childIds = newChildIds;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: {
|
|
297
|
+
i++;
|
|
298
|
+
i++;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
|
|
302
|
+
i++;
|
|
303
|
+
i++;
|
|
304
|
+
i++;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case TREE_OPERATION_REMOVE_ROOT: {
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case TREE_OPERATION_SET_SUBTREE_MODE: {
|
|
311
|
+
i++;
|
|
312
|
+
i++;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
default:
|
|
316
|
+
this.debugLog?.(`[OPS] unknown op=${op} at index=${i - 1}`);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
handleInspectedElement(payload) {
|
|
322
|
+
const p = payload;
|
|
323
|
+
if (p?.responseID != null) {
|
|
324
|
+
this.store.resolveInspectCallback(p.responseID, p.value);
|
|
325
|
+
}
|
|
326
|
+
if (p?.id != null && p?.value) {
|
|
327
|
+
const node = this.store.nodes.get(p.id);
|
|
328
|
+
if (node) {
|
|
329
|
+
if (p.value.props) node.props = p.value.props;
|
|
330
|
+
if (p.value.state) node.state = p.value.state;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Request element inspection from the DevTools backend */
|
|
335
|
+
inspectElement(id) {
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
if (!this.ws) {
|
|
338
|
+
reject(new Error("Not connected"));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const requestId = this.store.registerInspectCallback(resolve);
|
|
342
|
+
const rendererId = this.store.rendererIds[0] ?? 1;
|
|
343
|
+
this.ws.send(JSON.stringify({
|
|
344
|
+
event: "inspectElement",
|
|
345
|
+
payload: {
|
|
346
|
+
id,
|
|
347
|
+
rendererID: rendererId,
|
|
348
|
+
requestID: requestId,
|
|
349
|
+
forceFullData: true,
|
|
350
|
+
path: null
|
|
351
|
+
}
|
|
352
|
+
}));
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
this.store.resolveInspectCallback(requestId, null);
|
|
355
|
+
}, 5e3);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
detach() {
|
|
359
|
+
this.ws?.close();
|
|
360
|
+
this.ws = null;
|
|
361
|
+
this.store.connected = false;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// src/server.ts
|
|
366
|
+
var DevToolsServer = class {
|
|
367
|
+
wss = null;
|
|
368
|
+
store;
|
|
369
|
+
bridge;
|
|
370
|
+
options;
|
|
371
|
+
constructor(options) {
|
|
372
|
+
this.options = options;
|
|
373
|
+
this.store = new DevToolsStore();
|
|
374
|
+
this.bridge = new DevToolsBridge(this.store, options.onLog, options.debugLog);
|
|
375
|
+
}
|
|
376
|
+
start() {
|
|
377
|
+
return new Promise((resolve, reject) => {
|
|
378
|
+
const host = this.options.host ?? "127.0.0.1";
|
|
379
|
+
this.wss = new WebSocketServer({ port: this.options.port, host });
|
|
380
|
+
this.wss.on("listening", () => {
|
|
381
|
+
resolve();
|
|
382
|
+
});
|
|
383
|
+
this.wss.on("error", (err) => {
|
|
384
|
+
reject(err);
|
|
385
|
+
});
|
|
386
|
+
this.wss.on("connection", (ws) => {
|
|
387
|
+
this.bridge.attach(ws);
|
|
388
|
+
this.options.onConnect?.();
|
|
389
|
+
ws.on("close", () => {
|
|
390
|
+
this.options.onDisconnect?.();
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
stop() {
|
|
396
|
+
this.bridge.detach();
|
|
397
|
+
this.wss?.close();
|
|
398
|
+
this.wss = null;
|
|
399
|
+
}
|
|
400
|
+
getStore() {
|
|
401
|
+
return this.store;
|
|
402
|
+
}
|
|
403
|
+
getBridge() {
|
|
404
|
+
return this.bridge;
|
|
405
|
+
}
|
|
406
|
+
isConnected() {
|
|
407
|
+
return this.store.connected;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/pty-session.ts
|
|
412
|
+
import * as pty from "node-pty";
|
|
413
|
+
import xtermPkg from "@xterm/headless";
|
|
414
|
+
import unicode11Pkg from "@xterm/addon-unicode11";
|
|
415
|
+
var { Terminal } = xtermPkg;
|
|
416
|
+
var { Unicode11Addon } = unicode11Pkg;
|
|
417
|
+
var PtySession = class {
|
|
418
|
+
id;
|
|
419
|
+
command;
|
|
420
|
+
ptyProcess;
|
|
421
|
+
terminal;
|
|
422
|
+
_running = true;
|
|
423
|
+
exitCode = null;
|
|
424
|
+
cols;
|
|
425
|
+
rows;
|
|
426
|
+
constructor(id, options) {
|
|
427
|
+
this.id = id;
|
|
428
|
+
this.command = options.command;
|
|
429
|
+
this.cols = options.cols ?? 120;
|
|
430
|
+
this.rows = options.rows ?? 40;
|
|
431
|
+
this.terminal = new Terminal({
|
|
432
|
+
cols: this.cols,
|
|
433
|
+
rows: this.rows,
|
|
434
|
+
allowProposedApi: true
|
|
435
|
+
});
|
|
436
|
+
const unicode11 = new Unicode11Addon();
|
|
437
|
+
this.terminal.loadAddon(unicode11);
|
|
438
|
+
this.terminal.unicode.activeVersion = "11";
|
|
439
|
+
const env = { ...process.env, ...options.env };
|
|
440
|
+
if (!env.TERM) env.TERM = "xterm-256color";
|
|
441
|
+
if (!env.FORCE_COLOR) env.FORCE_COLOR = "1";
|
|
442
|
+
env.COLUMNS = String(this.cols);
|
|
443
|
+
env.LINES = String(this.rows);
|
|
444
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
445
|
+
const shellArgs = ["-c", options.command];
|
|
446
|
+
this.ptyProcess = pty.spawn(shell, shellArgs, {
|
|
447
|
+
name: "xterm-256color",
|
|
448
|
+
cols: this.cols,
|
|
449
|
+
rows: this.rows,
|
|
450
|
+
cwd: options.cwd ?? process.cwd(),
|
|
451
|
+
env
|
|
452
|
+
});
|
|
453
|
+
this.ptyProcess.onData((data) => {
|
|
454
|
+
this.terminal.write(data);
|
|
455
|
+
});
|
|
456
|
+
this.ptyProcess.onExit(({ exitCode }) => {
|
|
457
|
+
this._running = false;
|
|
458
|
+
this.exitCode = exitCode;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/** Simple shell-like string splitting (handles quotes) */
|
|
462
|
+
shellSplit(cmd) {
|
|
463
|
+
const parts = [];
|
|
464
|
+
let current = "";
|
|
465
|
+
let inSingle = false;
|
|
466
|
+
let inDouble = false;
|
|
467
|
+
for (const ch of cmd) {
|
|
468
|
+
if (ch === "'" && !inDouble) {
|
|
469
|
+
inSingle = !inSingle;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (ch === '"' && !inSingle) {
|
|
473
|
+
inDouble = !inDouble;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (ch === " " && !inSingle && !inDouble) {
|
|
477
|
+
if (current) parts.push(current);
|
|
478
|
+
current = "";
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
current += ch;
|
|
482
|
+
}
|
|
483
|
+
if (current) parts.push(current);
|
|
484
|
+
return parts;
|
|
485
|
+
}
|
|
486
|
+
get pid() {
|
|
487
|
+
return this.ptyProcess.pid;
|
|
488
|
+
}
|
|
489
|
+
get running() {
|
|
490
|
+
return this._running;
|
|
491
|
+
}
|
|
492
|
+
get info() {
|
|
493
|
+
return {
|
|
494
|
+
id: this.id,
|
|
495
|
+
command: this.command,
|
|
496
|
+
pid: this.ptyProcess.pid,
|
|
497
|
+
cols: this.cols,
|
|
498
|
+
rows: this.rows,
|
|
499
|
+
running: this._running
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** Capture current screen content as text */
|
|
503
|
+
screenshot(options) {
|
|
504
|
+
const buffer = this.terminal.buffer.active;
|
|
505
|
+
const lines = [];
|
|
506
|
+
for (let i = 0; i < this.rows; i++) {
|
|
507
|
+
const line = buffer.getLine(i);
|
|
508
|
+
if (line) {
|
|
509
|
+
lines.push(line.translateToString(true));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
513
|
+
lines.pop();
|
|
514
|
+
}
|
|
515
|
+
let text = lines.join("\n");
|
|
516
|
+
if (options?.stripAnsi) {
|
|
517
|
+
text = text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
518
|
+
}
|
|
519
|
+
return text;
|
|
520
|
+
}
|
|
521
|
+
/** Send key press to the PTY */
|
|
522
|
+
press(key) {
|
|
523
|
+
const seq = this.keyToSequence(key);
|
|
524
|
+
this.ptyProcess.write(seq);
|
|
525
|
+
}
|
|
526
|
+
/** Type text character by character */
|
|
527
|
+
type(text) {
|
|
528
|
+
this.ptyProcess.write(text);
|
|
529
|
+
}
|
|
530
|
+
/** Scroll the terminal viewport */
|
|
531
|
+
scroll(direction, amount = 1) {
|
|
532
|
+
for (let i = 0; i < amount; i++) {
|
|
533
|
+
if (direction === "up") {
|
|
534
|
+
this.ptyProcess.write("\x1B[5~");
|
|
535
|
+
} else {
|
|
536
|
+
this.ptyProcess.write("\x1B[6~");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/** Resize the terminal */
|
|
541
|
+
resize(cols, rows) {
|
|
542
|
+
this.ptyProcess.resize(cols, rows);
|
|
543
|
+
this.terminal.resize(cols, rows);
|
|
544
|
+
}
|
|
545
|
+
/** Wait for text to appear on screen */
|
|
546
|
+
async wait(text, timeoutMs = 3e4) {
|
|
547
|
+
const start = Date.now();
|
|
548
|
+
while (Date.now() - start < timeoutMs) {
|
|
549
|
+
const screen = this.screenshot();
|
|
550
|
+
if (screen.includes(text)) return true;
|
|
551
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
/** Kill the PTY process */
|
|
556
|
+
kill(signal = "SIGTERM") {
|
|
557
|
+
try {
|
|
558
|
+
this.ptyProcess.kill(signal);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
this._running = false;
|
|
562
|
+
}
|
|
563
|
+
/** Clean up resources */
|
|
564
|
+
dispose() {
|
|
565
|
+
this.kill();
|
|
566
|
+
this.terminal.dispose();
|
|
567
|
+
}
|
|
568
|
+
/** Convert key name to terminal escape sequence */
|
|
569
|
+
keyToSequence(key) {
|
|
570
|
+
const keyMap = {
|
|
571
|
+
"Enter": "\r",
|
|
572
|
+
"Return": "\r",
|
|
573
|
+
"Tab": " ",
|
|
574
|
+
"Escape": "\x1B",
|
|
575
|
+
"Backspace": "\x7F",
|
|
576
|
+
"Delete": "\x1B[3~",
|
|
577
|
+
"Space": " ",
|
|
578
|
+
"ArrowUp": "\x1B[A",
|
|
579
|
+
"ArrowDown": "\x1B[B",
|
|
580
|
+
"ArrowRight": "\x1B[C",
|
|
581
|
+
"ArrowLeft": "\x1B[D",
|
|
582
|
+
"Home": "\x1B[H",
|
|
583
|
+
"End": "\x1B[F",
|
|
584
|
+
"PageUp": "\x1B[5~",
|
|
585
|
+
"PageDown": "\x1B[6~",
|
|
586
|
+
"Insert": "\x1B[2~",
|
|
587
|
+
"F1": "\x1BOP",
|
|
588
|
+
"F2": "\x1BOQ",
|
|
589
|
+
"F3": "\x1BOR",
|
|
590
|
+
"F4": "\x1BOS",
|
|
591
|
+
"F5": "\x1B[15~",
|
|
592
|
+
"F6": "\x1B[17~",
|
|
593
|
+
"F7": "\x1B[18~",
|
|
594
|
+
"F8": "\x1B[19~",
|
|
595
|
+
"F9": "\x1B[20~",
|
|
596
|
+
"F10": "\x1B[21~",
|
|
597
|
+
"F11": "\x1B[23~",
|
|
598
|
+
"F12": "\x1B[24~",
|
|
599
|
+
"Ctrl-c": "",
|
|
600
|
+
"Ctrl-d": "",
|
|
601
|
+
"Ctrl-z": "",
|
|
602
|
+
"Ctrl-l": "\f",
|
|
603
|
+
"Ctrl-a": "",
|
|
604
|
+
"Ctrl-e": "",
|
|
605
|
+
"Ctrl-k": "\v",
|
|
606
|
+
"Ctrl-u": ""
|
|
607
|
+
};
|
|
608
|
+
return keyMap[key] ?? key;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// src/session-manager.ts
|
|
613
|
+
var SessionManager = class {
|
|
614
|
+
sessions = /* @__PURE__ */ new Map();
|
|
615
|
+
create(id, options) {
|
|
616
|
+
if (this.sessions.has(id)) {
|
|
617
|
+
this.sessions.get(id).dispose();
|
|
618
|
+
}
|
|
619
|
+
const session = new PtySession(id, options);
|
|
620
|
+
this.sessions.set(id, session);
|
|
621
|
+
return session.info;
|
|
622
|
+
}
|
|
623
|
+
get(id) {
|
|
624
|
+
return this.sessions.get(id);
|
|
625
|
+
}
|
|
626
|
+
list() {
|
|
627
|
+
return [...this.sessions.values()].map((s) => s.info);
|
|
628
|
+
}
|
|
629
|
+
kill(id) {
|
|
630
|
+
const session = this.sessions.get(id);
|
|
631
|
+
if (!session) return false;
|
|
632
|
+
session.dispose();
|
|
633
|
+
this.sessions.delete(id);
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
killAll() {
|
|
637
|
+
for (const session of this.sessions.values()) {
|
|
638
|
+
session.dispose();
|
|
639
|
+
}
|
|
640
|
+
this.sessions.clear();
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// src/daemon.ts
|
|
645
|
+
var BASE_DIR = path.join(process.env.HOME ?? "/tmp", ".tui-devtools");
|
|
646
|
+
function ensureBaseDir() {
|
|
647
|
+
fs.mkdirSync(BASE_DIR, { recursive: true });
|
|
648
|
+
}
|
|
649
|
+
function getSocketPath(session) {
|
|
650
|
+
return path.join(BASE_DIR, `${session}.sock`);
|
|
651
|
+
}
|
|
652
|
+
function getPidPath(session) {
|
|
653
|
+
return path.join(BASE_DIR, `${session}.pid`);
|
|
654
|
+
}
|
|
655
|
+
function getLogPath(session) {
|
|
656
|
+
return path.join(BASE_DIR, `${session}.log`);
|
|
657
|
+
}
|
|
658
|
+
function isDaemonRunning(session) {
|
|
659
|
+
const pidPath = getPidPath(session);
|
|
660
|
+
try {
|
|
661
|
+
const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim(), 10);
|
|
662
|
+
process.kill(pid, 0);
|
|
663
|
+
return true;
|
|
664
|
+
} catch {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function sendIpcRequest(session, request) {
|
|
669
|
+
return new Promise((resolve, reject) => {
|
|
670
|
+
const socketPath = getSocketPath(session);
|
|
671
|
+
const client = net.createConnection(socketPath);
|
|
672
|
+
let buffer = "";
|
|
673
|
+
client.on("connect", () => {
|
|
674
|
+
client.write(JSON.stringify(request) + "\n");
|
|
675
|
+
});
|
|
676
|
+
client.on("data", (data) => {
|
|
677
|
+
buffer += data.toString();
|
|
678
|
+
const lines = buffer.split("\n");
|
|
679
|
+
if (lines.length > 1) {
|
|
680
|
+
try {
|
|
681
|
+
const response = JSON.parse(lines[0]);
|
|
682
|
+
client.end();
|
|
683
|
+
resolve(response);
|
|
684
|
+
} catch (e) {
|
|
685
|
+
reject(new Error(`Invalid IPC response: ${lines[0]}`));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
client.on("error", (err) => {
|
|
690
|
+
reject(new Error(`Cannot connect to daemon (session: ${session}): ${err.message}`));
|
|
691
|
+
});
|
|
692
|
+
setTimeout(() => {
|
|
693
|
+
client.destroy();
|
|
694
|
+
reject(new Error("IPC timeout"));
|
|
695
|
+
}, 1e4);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
async function startDaemon(session, port) {
|
|
699
|
+
ensureBaseDir();
|
|
700
|
+
const socketPath = getSocketPath(session);
|
|
701
|
+
try {
|
|
702
|
+
fs.unlinkSync(socketPath);
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
fs.writeFileSync(getPidPath(session), String(process.pid));
|
|
706
|
+
const logStream = fs.createWriteStream(getLogPath(session), { flags: "a" });
|
|
707
|
+
const log = (msg) => {
|
|
708
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
709
|
+
logStream.write(`[${ts}] ${msg}
|
|
710
|
+
`);
|
|
711
|
+
};
|
|
712
|
+
const sessionMgr = new SessionManager();
|
|
713
|
+
const devtools = new DevToolsServer({
|
|
714
|
+
port,
|
|
715
|
+
onLog: (entry) => {
|
|
716
|
+
log(`[${entry.level}] ${entry.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}`);
|
|
717
|
+
},
|
|
718
|
+
onConnect: () => log("App connected"),
|
|
719
|
+
onDisconnect: () => log("App disconnected"),
|
|
720
|
+
debugLog: (msg) => log(msg)
|
|
721
|
+
});
|
|
722
|
+
await devtools.start();
|
|
723
|
+
log(`DevTools server listening on port ${port}`);
|
|
724
|
+
const ipcServer = net.createServer((conn) => {
|
|
725
|
+
let buffer = "";
|
|
726
|
+
conn.on("data", async (data) => {
|
|
727
|
+
buffer += data.toString();
|
|
728
|
+
const lines = buffer.split("\n");
|
|
729
|
+
if (lines.length <= 1) return;
|
|
730
|
+
buffer = lines.slice(1).join("\n");
|
|
731
|
+
try {
|
|
732
|
+
const req = JSON.parse(lines[0]);
|
|
733
|
+
const res = await handleIpcRequest(req, devtools, sessionMgr, log);
|
|
734
|
+
conn.write(JSON.stringify(res) + "\n");
|
|
735
|
+
} catch (e) {
|
|
736
|
+
conn.write(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
ipcServer.listen(socketPath);
|
|
741
|
+
log(`IPC server listening on ${socketPath}`);
|
|
742
|
+
const cleanup = () => {
|
|
743
|
+
log("Shutting down...");
|
|
744
|
+
sessionMgr.killAll();
|
|
745
|
+
devtools.stop();
|
|
746
|
+
ipcServer.close();
|
|
747
|
+
try {
|
|
748
|
+
fs.unlinkSync(socketPath);
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
fs.unlinkSync(getPidPath(session));
|
|
753
|
+
} catch {
|
|
754
|
+
}
|
|
755
|
+
logStream.end();
|
|
756
|
+
process.exit(0);
|
|
757
|
+
};
|
|
758
|
+
process.on("SIGTERM", cleanup);
|
|
759
|
+
process.on("SIGINT", cleanup);
|
|
760
|
+
}
|
|
761
|
+
async function handleIpcRequest(req, devtools, sessionMgr, log) {
|
|
762
|
+
const store = devtools.getStore();
|
|
763
|
+
const bridge = devtools.getBridge();
|
|
764
|
+
switch (req.command) {
|
|
765
|
+
// ─── DevTools commands ───
|
|
766
|
+
case "status":
|
|
767
|
+
return {
|
|
768
|
+
ok: true,
|
|
769
|
+
data: {
|
|
770
|
+
connected: store.connected,
|
|
771
|
+
appName: store.appName,
|
|
772
|
+
nodeCount: store.nodes.size,
|
|
773
|
+
rootCount: store.roots.size,
|
|
774
|
+
logCount: store.logs.length,
|
|
775
|
+
rendererIds: store.rendererIds,
|
|
776
|
+
sessions: sessionMgr.list()
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
case "tree": {
|
|
780
|
+
const depth = req.args?.depth ?? 20;
|
|
781
|
+
const json = req.args?.json;
|
|
782
|
+
if (json) {
|
|
783
|
+
return { ok: true, data: store.getTreeJson(void 0, depth) };
|
|
784
|
+
}
|
|
785
|
+
return { ok: true, data: store.getTree(void 0, depth) };
|
|
786
|
+
}
|
|
787
|
+
case "inspect": {
|
|
788
|
+
const name = req.args?.name;
|
|
789
|
+
const id = req.args?.id;
|
|
790
|
+
let node = id != null ? store.nodes.get(id) : null;
|
|
791
|
+
if (!node && name) {
|
|
792
|
+
node = store.findByName(name);
|
|
793
|
+
}
|
|
794
|
+
if (!node) {
|
|
795
|
+
return { ok: false, error: `Component not found: ${name ?? id}` };
|
|
796
|
+
}
|
|
797
|
+
const inspectData = await bridge.inspectElement(node.id);
|
|
798
|
+
return {
|
|
799
|
+
ok: true,
|
|
800
|
+
data: {
|
|
801
|
+
id: node.id,
|
|
802
|
+
displayName: node.displayName,
|
|
803
|
+
type: node.type,
|
|
804
|
+
key: node.key,
|
|
805
|
+
parentId: node.parentId,
|
|
806
|
+
childIds: node.childIds,
|
|
807
|
+
props: node.props,
|
|
808
|
+
state: node.state,
|
|
809
|
+
inspectData
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
case "logs": {
|
|
814
|
+
const level = req.args?.level;
|
|
815
|
+
const tail = req.args?.tail ?? 50;
|
|
816
|
+
let logs = store.logs;
|
|
817
|
+
if (level) {
|
|
818
|
+
logs = logs.filter((l) => l.level === level);
|
|
819
|
+
}
|
|
820
|
+
logs = logs.slice(-tail);
|
|
821
|
+
return { ok: true, data: logs };
|
|
822
|
+
}
|
|
823
|
+
case "find": {
|
|
824
|
+
const name = req.args?.name;
|
|
825
|
+
if (!name) return { ok: false, error: "name is required" };
|
|
826
|
+
const nodes = store.findAllByName(name);
|
|
827
|
+
return {
|
|
828
|
+
ok: true,
|
|
829
|
+
data: nodes.map((n) => ({
|
|
830
|
+
id: n.id,
|
|
831
|
+
displayName: n.displayName,
|
|
832
|
+
type: n.type,
|
|
833
|
+
key: n.key
|
|
834
|
+
}))
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// ─── PTY automation commands ───
|
|
838
|
+
case "run": {
|
|
839
|
+
const command = req.args?.command;
|
|
840
|
+
const sessionId = req.args?.sessionId ?? "default";
|
|
841
|
+
const cwd = req.args?.cwd;
|
|
842
|
+
const cols = req.args?.cols;
|
|
843
|
+
const rows = req.args?.rows;
|
|
844
|
+
const env = req.args?.env;
|
|
845
|
+
if (!command) return { ok: false, error: "command is required" };
|
|
846
|
+
log(`[PTY] run session=${sessionId} command=${command}`);
|
|
847
|
+
const info = sessionMgr.create(sessionId, { command, cwd, cols, rows, env });
|
|
848
|
+
return { ok: true, data: info };
|
|
849
|
+
}
|
|
850
|
+
case "screenshot": {
|
|
851
|
+
const sessionId = req.args?.sessionId ?? "default";
|
|
852
|
+
const stripAnsi = req.args?.stripAnsi;
|
|
853
|
+
const session = sessionMgr.get(sessionId);
|
|
854
|
+
if (!session) return { ok: false, error: `Session not found: ${sessionId}` };
|
|
855
|
+
const text = session.screenshot({ stripAnsi });
|
|
856
|
+
return { ok: true, data: { screenshot: text, running: session.running } };
|
|
857
|
+
}
|
|
858
|
+
case "press": {
|
|
859
|
+
const sessionId = req.args?.sessionId ?? "default";
|
|
860
|
+
const keys = req.args?.keys;
|
|
861
|
+
const session = sessionMgr.get(sessionId);
|
|
862
|
+
if (!session) return { ok: false, error: `Session not found: ${sessionId}` };
|
|
863
|
+
for (const key of keys ?? []) {
|
|
864
|
+
session.press(key);
|
|
865
|
+
}
|
|
866
|
+
return { ok: true };
|
|
867
|
+
}
|
|
868
|
+
case "type": {
|
|
869
|
+
const sessionId = req.args?.sessionId ?? "default";
|
|
870
|
+
const text = req.args?.text;
|
|
871
|
+
const session = sessionMgr.get(sessionId);
|
|
872
|
+
if (!session) return { ok: false, error: `Session not found: ${sessionId}` };
|
|
873
|
+
if (!text) return { ok: false, error: "text is required" };
|
|
874
|
+
session.type(text);
|
|
875
|
+
return { ok: true };
|
|
876
|
+
}
|
|
877
|
+
case "scroll": {
|
|
878
|
+
const sessionId = req.args?.sessionId ?? "default";
|
|
879
|
+
const direction = req.args?.direction ?? "down";
|
|
880
|
+
const amount = req.args?.amount ?? 1;
|
|
881
|
+
const session = sessionMgr.get(sessionId);
|
|
882
|
+
if (!session) return { ok: false, error: `Session not found: ${sessionId}` };
|
|
883
|
+
session.scroll(direction, amount);
|
|
884
|
+
return { ok: true };
|
|
885
|
+
}
|
|
886
|
+
case "wait": {
|
|
887
|
+
const sessionId = req.args?.sessionId ?? "default";
|
|
888
|
+
const text = req.args?.text;
|
|
889
|
+
const timeout = req.args?.timeout ?? 3e4;
|
|
890
|
+
const session = sessionMgr.get(sessionId);
|
|
891
|
+
if (!session) return { ok: false, error: `Session not found: ${sessionId}` };
|
|
892
|
+
if (!text) return { ok: false, error: "text is required" };
|
|
893
|
+
const found = await session.wait(text, timeout);
|
|
894
|
+
return { ok: true, data: { found, screenshot: session.screenshot() } };
|
|
895
|
+
}
|
|
896
|
+
case "resize": {
|
|
897
|
+
const sessionId = req.args?.sessionId ?? "default";
|
|
898
|
+
const cols = req.args?.cols ?? 120;
|
|
899
|
+
const rows = req.args?.rows ?? 40;
|
|
900
|
+
const session = sessionMgr.get(sessionId);
|
|
901
|
+
if (!session) return { ok: false, error: `Session not found: ${sessionId}` };
|
|
902
|
+
session.resize(cols, rows);
|
|
903
|
+
return { ok: true };
|
|
904
|
+
}
|
|
905
|
+
case "kill-session": {
|
|
906
|
+
const sessionId = req.args?.sessionId ?? "default";
|
|
907
|
+
const killed = sessionMgr.kill(sessionId);
|
|
908
|
+
return { ok: killed, error: killed ? void 0 : `Session not found: ${sessionId}` };
|
|
909
|
+
}
|
|
910
|
+
case "sessions":
|
|
911
|
+
return { ok: true, data: sessionMgr.list() };
|
|
912
|
+
default:
|
|
913
|
+
return { ok: false, error: `Unknown command: ${req.command}` };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export {
|
|
918
|
+
getSocketPath,
|
|
919
|
+
getPidPath,
|
|
920
|
+
getLogPath,
|
|
921
|
+
isDaemonRunning,
|
|
922
|
+
sendIpcRequest,
|
|
923
|
+
startDaemon
|
|
924
|
+
};
|
|
925
|
+
//# sourceMappingURL=chunk-UV6LYAWB.js.map
|