@tuidom/inspector 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/dist/InspectorCore.d.ts +33 -0
- package/dist/InspectorCore.js +144 -0
- package/dist/InspectorDriver.d.ts +35 -0
- package/dist/InspectorDriver.js +1 -0
- package/dist/InspectorServer.d.ts +23 -0
- package/dist/InspectorServer.js +89 -0
- package/dist/attachInspector.d.ts +18 -0
- package/dist/attachInspector.js +26 -0
- package/dist/idleWaiter.d.ts +34 -0
- package/dist/idleWaiter.js +45 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/protocol.d.ts +133 -0
- package/dist/protocol.js +24 -0
- package/dist/serializeTree.d.ts +7 -0
- package/dist/serializeTree.js +58 -0
- package/dist/ws/frame.d.ts +20 -0
- package/dist/ws/frame.js +78 -0
- package/dist/ws/handshake.d.ts +2 -0
- package/dist/ws/handshake.js +9 -0
- package/package.json +35 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
2
|
+
import type { InspectorDriver } from "./InspectorDriver.js";
|
|
3
|
+
import { type InspectorRequest, type InspectorResponse } from "./protocol.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read-only view of the inspected application. Decouples the core from
|
|
6
|
+
* `TuiApplication` so the same core can be driven by a WS server, an in-process
|
|
7
|
+
* inspector widget, or a test.
|
|
8
|
+
*/
|
|
9
|
+
export interface InspectorTarget {
|
|
10
|
+
getRoot(): TUIElement | null;
|
|
11
|
+
getFocused(): TUIElement | null;
|
|
12
|
+
}
|
|
13
|
+
type MethodHandler = (params: unknown) => unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Transport-agnostic inspector. Holds a read-only reference to the inspected
|
|
16
|
+
* app via `InspectorTarget` and answers protocol methods. Methods are a
|
|
17
|
+
* registry, so new ones extend the inspector without touching `dispatch`.
|
|
18
|
+
*
|
|
19
|
+
* When constructed with an optional {@link InspectorDriver}, it additionally
|
|
20
|
+
* registers the write/capture methods (`sendKey`, `sendText`, `resize`,
|
|
21
|
+
* `captureFrame`, `shutdown`) — used by `--headless` mode. Without a driver the
|
|
22
|
+
* inspector stays read-only.
|
|
23
|
+
*/
|
|
24
|
+
export declare class InspectorCore {
|
|
25
|
+
private readonly target;
|
|
26
|
+
private readonly methods;
|
|
27
|
+
constructor(target: InspectorTarget, driver?: InspectorDriver);
|
|
28
|
+
register(method: string, handler: MethodHandler): void;
|
|
29
|
+
dispatch(request: InspectorRequest): Promise<InspectorResponse>;
|
|
30
|
+
private getDocument;
|
|
31
|
+
private registerDriverMethods;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { InspectorMethod, } from "./protocol.js";
|
|
2
|
+
import { serializeTree } from "./serializeTree.js";
|
|
3
|
+
/**
|
|
4
|
+
* Transport-agnostic inspector. Holds a read-only reference to the inspected
|
|
5
|
+
* app via `InspectorTarget` and answers protocol methods. Methods are a
|
|
6
|
+
* registry, so new ones extend the inspector without touching `dispatch`.
|
|
7
|
+
*
|
|
8
|
+
* When constructed with an optional {@link InspectorDriver}, it additionally
|
|
9
|
+
* registers the write/capture methods (`sendKey`, `sendText`, `resize`,
|
|
10
|
+
* `captureFrame`, `shutdown`) — used by `--headless` mode. Without a driver the
|
|
11
|
+
* inspector stays read-only.
|
|
12
|
+
*/
|
|
13
|
+
export class InspectorCore {
|
|
14
|
+
target;
|
|
15
|
+
methods = new Map();
|
|
16
|
+
constructor(target, driver) {
|
|
17
|
+
this.target = target;
|
|
18
|
+
this.register(InspectorMethod.getDocument, () => this.getDocument());
|
|
19
|
+
if (driver !== undefined)
|
|
20
|
+
this.registerDriverMethods(driver);
|
|
21
|
+
}
|
|
22
|
+
register(method, handler) {
|
|
23
|
+
this.methods.set(method, handler);
|
|
24
|
+
}
|
|
25
|
+
async dispatch(request) {
|
|
26
|
+
const handler = this.methods.get(request.method);
|
|
27
|
+
if (handler === undefined) {
|
|
28
|
+
return { id: request.id, error: { message: `Unknown method: ${request.method}` } };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return { id: request.id, result: await handler(request.params) };
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return { id: request.id, error: { message: err instanceof Error ? err.message : String(err) } };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
getDocument() {
|
|
38
|
+
return { root: serializeTree(this.target.getRoot(), this.target.getFocused()) };
|
|
39
|
+
}
|
|
40
|
+
registerDriverMethods(driver) {
|
|
41
|
+
this.register(InspectorMethod.sendKey, (params) => {
|
|
42
|
+
driver.sendKey(asSendKeyParams(params).name);
|
|
43
|
+
return {};
|
|
44
|
+
});
|
|
45
|
+
this.register(InspectorMethod.sendText, (params) => {
|
|
46
|
+
driver.sendText(asSendTextParams(params).text);
|
|
47
|
+
return {};
|
|
48
|
+
});
|
|
49
|
+
this.register(InspectorMethod.sendMouse, (params) => {
|
|
50
|
+
driver.sendMouse(asSendMouseParams(params));
|
|
51
|
+
return {};
|
|
52
|
+
});
|
|
53
|
+
this.register(InspectorMethod.resize, (params) => {
|
|
54
|
+
const { cols, rows } = asResizeParams(params);
|
|
55
|
+
driver.resize(cols, rows);
|
|
56
|
+
return {};
|
|
57
|
+
});
|
|
58
|
+
this.register(InspectorMethod.captureFrame, async () => {
|
|
59
|
+
return { frame: await driver.captureFrame() };
|
|
60
|
+
});
|
|
61
|
+
this.register(InspectorMethod.waitForIdle, (params) => {
|
|
62
|
+
return driver.waitForIdle(asWaitForIdleParams(params));
|
|
63
|
+
});
|
|
64
|
+
this.register(InspectorMethod.shutdown, () => {
|
|
65
|
+
driver.shutdown();
|
|
66
|
+
return {};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function asRecord(params) {
|
|
71
|
+
if (typeof params !== "object" || params === null) {
|
|
72
|
+
throw new Error("Expected object params");
|
|
73
|
+
}
|
|
74
|
+
return params;
|
|
75
|
+
}
|
|
76
|
+
function asSendKeyParams(params) {
|
|
77
|
+
const { name } = asRecord(params);
|
|
78
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
79
|
+
throw new Error("sendKey requires a non-empty string 'name'");
|
|
80
|
+
}
|
|
81
|
+
return { name };
|
|
82
|
+
}
|
|
83
|
+
function asSendTextParams(params) {
|
|
84
|
+
const { text } = asRecord(params);
|
|
85
|
+
if (typeof text !== "string") {
|
|
86
|
+
throw new Error("sendText requires a string 'text'");
|
|
87
|
+
}
|
|
88
|
+
return { text };
|
|
89
|
+
}
|
|
90
|
+
const MOUSE_ACTIONS = new Set([
|
|
91
|
+
"press",
|
|
92
|
+
"release",
|
|
93
|
+
"move",
|
|
94
|
+
"scroll-up",
|
|
95
|
+
"scroll-down",
|
|
96
|
+
"scroll-left",
|
|
97
|
+
"scroll-right",
|
|
98
|
+
]);
|
|
99
|
+
const MOUSE_BUTTONS = new Set(["left", "middle", "right", "none"]);
|
|
100
|
+
function asSendMouseParams(params) {
|
|
101
|
+
const { action, button, x, y, shiftKey, altKey, ctrlKey } = asRecord(params);
|
|
102
|
+
if (typeof action !== "string" || !MOUSE_ACTIONS.has(action)) {
|
|
103
|
+
throw new Error(`sendMouse requires 'action' one of: ${[...MOUSE_ACTIONS].join(", ")}`);
|
|
104
|
+
}
|
|
105
|
+
if (button !== undefined && (typeof button !== "string" || !MOUSE_BUTTONS.has(button))) {
|
|
106
|
+
throw new Error(`sendMouse 'button' must be one of: ${[...MOUSE_BUTTONS].join(", ")}`);
|
|
107
|
+
}
|
|
108
|
+
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0) {
|
|
109
|
+
throw new Error("sendMouse requires non-negative integer 'x' and 'y'");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
action: action,
|
|
113
|
+
...(button === undefined ? {} : { button: button }),
|
|
114
|
+
x: x,
|
|
115
|
+
y: y,
|
|
116
|
+
shiftKey: shiftKey === true,
|
|
117
|
+
altKey: altKey === true,
|
|
118
|
+
ctrlKey: ctrlKey === true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function asWaitForIdleParams(params) {
|
|
122
|
+
if (params === undefined || params === null)
|
|
123
|
+
return {};
|
|
124
|
+
const { quietMs, timeoutMs } = asRecord(params);
|
|
125
|
+
const out = {};
|
|
126
|
+
if (quietMs !== undefined) {
|
|
127
|
+
if (!Number.isFinite(quietMs) || quietMs < 0)
|
|
128
|
+
throw new Error("waitForIdle 'quietMs' must be a non-negative number");
|
|
129
|
+
out.quietMs = quietMs;
|
|
130
|
+
}
|
|
131
|
+
if (timeoutMs !== undefined) {
|
|
132
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
133
|
+
throw new Error("waitForIdle 'timeoutMs' must be a non-negative number");
|
|
134
|
+
out.timeoutMs = timeoutMs;
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function asResizeParams(params) {
|
|
139
|
+
const { cols, rows } = asRecord(params);
|
|
140
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols <= 0 || rows <= 0) {
|
|
141
|
+
throw new Error("resize requires positive integer 'cols' and 'rows'");
|
|
142
|
+
}
|
|
143
|
+
return { cols: cols, rows: rows };
|
|
144
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { GridSnapshot } from "@tuidom/core/rendering/gridSnapshot";
|
|
2
|
+
import type { SendMouseParams, WaitForIdleParams, WaitForIdleResult } from "./protocol.js";
|
|
3
|
+
/**
|
|
4
|
+
* Write/capture side of the inspector — the counterpart to the read-only
|
|
5
|
+
* {@link import("./InspectorCore.js").InspectorTarget}. Present only when the app
|
|
6
|
+
* runs in a drivable mode (headless capture); a normal terminal session attaches
|
|
7
|
+
* the inspector without a driver and stays read-only.
|
|
8
|
+
*
|
|
9
|
+
* The App layer supplies the concrete adapter (over `HeadlessCaptureBackend`), so
|
|
10
|
+
* the Inspector layer never imports Backend — it only depends on this port and on
|
|
11
|
+
* the {@link GridSnapshot} data type.
|
|
12
|
+
*/
|
|
13
|
+
export interface InspectorDriver {
|
|
14
|
+
/** Inject a key by DSL name (`"a"`, `"Enter"`, `"Ctrl+P"`). */
|
|
15
|
+
sendKey(name: string): void;
|
|
16
|
+
/** Inject literal text as a single bracketed paste. */
|
|
17
|
+
sendText(text: string): void;
|
|
18
|
+
/** Inject a mouse event at 0-based screen coordinates (see {@link SendMouseParams}). */
|
|
19
|
+
sendMouse(params: SendMouseParams): void;
|
|
20
|
+
/** Resize the virtual terminal. */
|
|
21
|
+
resize(cols: number, rows: number): void;
|
|
22
|
+
/**
|
|
23
|
+
* Capture the current screen. Async so the adapter can first drain any
|
|
24
|
+
* deferred render (the app schedules some renders on `setImmediate`).
|
|
25
|
+
*/
|
|
26
|
+
captureFrame(): Promise<GridSnapshot>;
|
|
27
|
+
/**
|
|
28
|
+
* Wait until the app stops repainting (frame counter stable + no scheduled
|
|
29
|
+
* render). Lets a client settle after injecting input without guessing a
|
|
30
|
+
* `sleep`. Resolves `idle: false` on timeout rather than rejecting.
|
|
31
|
+
*/
|
|
32
|
+
waitForIdle(params: WaitForIdleParams): Promise<WaitForIdleResult>;
|
|
33
|
+
/** Tear down the session and exit the process. */
|
|
34
|
+
shutdown(): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { InspectorCore } from "./InspectorCore.js";
|
|
2
|
+
export interface InspectorServerOptions {
|
|
3
|
+
host?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* WebSocket transport for the inspector: a hand-written RFC6455 server over
|
|
8
|
+
* node:http (zero runtime deps). Routes each inbound JSON message to
|
|
9
|
+
* `InspectorCore.dispatch` and writes back the response frame.
|
|
10
|
+
*/
|
|
11
|
+
export declare class InspectorServer {
|
|
12
|
+
private readonly core;
|
|
13
|
+
private readonly httpServer;
|
|
14
|
+
private readonly sockets;
|
|
15
|
+
constructor(core: InspectorCore);
|
|
16
|
+
listen(options?: InspectorServerOptions): Promise<{
|
|
17
|
+
port: number;
|
|
18
|
+
}>;
|
|
19
|
+
dispose(): void;
|
|
20
|
+
private handleUpgrade;
|
|
21
|
+
private handleFrame;
|
|
22
|
+
private dispatchAndReply;
|
|
23
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { decodeFrames, encodeFrame, encodeTextFrame, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG, OPCODE_TEXT, } from "./ws/frame.js";
|
|
3
|
+
import { computeAcceptKey } from "./ws/handshake.js";
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket transport for the inspector: a hand-written RFC6455 server over
|
|
6
|
+
* node:http (zero runtime deps). Routes each inbound JSON message to
|
|
7
|
+
* `InspectorCore.dispatch` and writes back the response frame.
|
|
8
|
+
*/
|
|
9
|
+
export class InspectorServer {
|
|
10
|
+
core;
|
|
11
|
+
httpServer;
|
|
12
|
+
sockets = new Set();
|
|
13
|
+
constructor(core) {
|
|
14
|
+
this.core = core;
|
|
15
|
+
this.httpServer = createServer((_req, res) => {
|
|
16
|
+
res.writeHead(426); // Upgrade Required — this endpoint is WebSocket only
|
|
17
|
+
res.end("WebSocket only");
|
|
18
|
+
});
|
|
19
|
+
this.httpServer.on("upgrade", (req, socket) => {
|
|
20
|
+
this.handleUpgrade(req, socket);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
listen(options = {}) {
|
|
24
|
+
const host = options.host ?? "127.0.0.1";
|
|
25
|
+
const port = options.port ?? 0;
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
this.httpServer.listen(port, host, () => {
|
|
28
|
+
const addr = this.httpServer.address();
|
|
29
|
+
const boundPort = typeof addr === "object" && addr !== null ? addr.port : port;
|
|
30
|
+
resolve({ port: boundPort });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
dispose() {
|
|
35
|
+
for (const socket of this.sockets)
|
|
36
|
+
socket.destroy();
|
|
37
|
+
this.sockets.clear();
|
|
38
|
+
this.httpServer.close();
|
|
39
|
+
}
|
|
40
|
+
handleUpgrade(req, socket) {
|
|
41
|
+
const key = req.headers["sec-websocket-key"];
|
|
42
|
+
if (typeof key !== "string") {
|
|
43
|
+
socket.destroy();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
socket.write("HTTP/1.1 101 Switching Protocols\r\n" +
|
|
47
|
+
"Upgrade: websocket\r\n" +
|
|
48
|
+
"Connection: Upgrade\r\n" +
|
|
49
|
+
`Sec-WebSocket-Accept: ${computeAcceptKey(key)}\r\n\r\n`);
|
|
50
|
+
this.sockets.add(socket);
|
|
51
|
+
let buffer = Buffer.alloc(0);
|
|
52
|
+
socket.on("data", (chunk) => {
|
|
53
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
54
|
+
const { frames, rest } = decodeFrames(buffer);
|
|
55
|
+
buffer = rest;
|
|
56
|
+
for (const frame of frames) {
|
|
57
|
+
this.handleFrame(socket, frame.opcode, frame.payload);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
socket.on("close", () => this.sockets.delete(socket));
|
|
61
|
+
socket.on("error", () => this.sockets.delete(socket));
|
|
62
|
+
}
|
|
63
|
+
handleFrame(socket, opcode, payload) {
|
|
64
|
+
if (opcode === OPCODE_CLOSE) {
|
|
65
|
+
socket.end();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (opcode === OPCODE_PING) {
|
|
69
|
+
socket.write(encodeFrame(OPCODE_PONG, Buffer.from(payload, "utf8")));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (opcode !== OPCODE_TEXT)
|
|
73
|
+
return;
|
|
74
|
+
let request;
|
|
75
|
+
try {
|
|
76
|
+
request = JSON.parse(payload);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return; // ignore non-JSON frames
|
|
80
|
+
}
|
|
81
|
+
void this.dispatchAndReply(socket, request);
|
|
82
|
+
}
|
|
83
|
+
async dispatchAndReply(socket, request) {
|
|
84
|
+
const response = await this.core.dispatch(request);
|
|
85
|
+
if (socket.destroyed)
|
|
86
|
+
return;
|
|
87
|
+
socket.write(encodeTextFrame(JSON.stringify(response)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TuiApplication } from "@tuidom/core/dom/tuiApplication";
|
|
2
|
+
import { InspectorCore } from "./InspectorCore.js";
|
|
3
|
+
import type { InspectorDriver } from "./InspectorDriver.js";
|
|
4
|
+
import { InspectorServer, type InspectorServerOptions } from "./InspectorServer.js";
|
|
5
|
+
export interface AttachedInspector {
|
|
6
|
+
core: InspectorCore;
|
|
7
|
+
server: InspectorServer;
|
|
8
|
+
port: number;
|
|
9
|
+
dispose(): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Attach an inspector to a running `TuiApplication` over WebSocket. Reads the
|
|
13
|
+
* app read-only (root/focus). When a {@link InspectorDriver} is supplied (headless
|
|
14
|
+
* mode) the inspector also exposes input-injection and frame-capture methods;
|
|
15
|
+
* without it the inspector stays read-only. For in-process use (e.g. a
|
|
16
|
+
* split-screen inspector widget) build an `InspectorCore` directly instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare function attachInspector(app: TuiApplication, options?: InspectorServerOptions, driver?: InspectorDriver): Promise<AttachedInspector>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { InspectorCore } from "./InspectorCore.js";
|
|
2
|
+
import { InspectorServer } from "./InspectorServer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Attach an inspector to a running `TuiApplication` over WebSocket. Reads the
|
|
5
|
+
* app read-only (root/focus). When a {@link InspectorDriver} is supplied (headless
|
|
6
|
+
* mode) the inspector also exposes input-injection and frame-capture methods;
|
|
7
|
+
* without it the inspector stays read-only. For in-process use (e.g. a
|
|
8
|
+
* split-screen inspector widget) build an `InspectorCore` directly instead.
|
|
9
|
+
*/
|
|
10
|
+
export async function attachInspector(app, options = {}, driver) {
|
|
11
|
+
const target = {
|
|
12
|
+
getRoot: () => app.root,
|
|
13
|
+
getFocused: () => app.focusManager?.activeElement ?? null,
|
|
14
|
+
};
|
|
15
|
+
const core = new InspectorCore(target, driver);
|
|
16
|
+
const server = new InspectorServer(core);
|
|
17
|
+
const { port } = await server.listen(options);
|
|
18
|
+
return {
|
|
19
|
+
core,
|
|
20
|
+
server,
|
|
21
|
+
port,
|
|
22
|
+
dispose: () => {
|
|
23
|
+
server.dispose();
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Наблюдаемое состояние отрисовки — то, что даёт `TuiApplication`. */
|
|
2
|
+
export interface IdleSource {
|
|
3
|
+
/** Монотонный счётчик отрисованных кадров. */
|
|
4
|
+
frameCount(): number;
|
|
5
|
+
/** Есть ли ещё не выполненный отложенный рендер. */
|
|
6
|
+
isRenderScheduled(): boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Часы: вынесены параметром ради детерминированных юнит-тестов. */
|
|
9
|
+
export interface IdleClock {
|
|
10
|
+
now(): number;
|
|
11
|
+
sleep(ms: number): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface WaitForIdleOptions {
|
|
14
|
+
/** Сколько кадр должен простоять неизменным, чтобы считаться устоявшимся. */
|
|
15
|
+
quietMs?: number;
|
|
16
|
+
/** Предел ожидания; по истечении возвращаем `idle: false` (не бросаем). */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
/** Период опроса источника. */
|
|
19
|
+
pollMs?: number;
|
|
20
|
+
/** Подменяемые часы (по умолчанию — реальные). */
|
|
21
|
+
clock?: IdleClock;
|
|
22
|
+
}
|
|
23
|
+
export interface WaitForIdleResult {
|
|
24
|
+
/** `true` — дождались тишины; `false` — упёрлись в `timeoutMs`. */
|
|
25
|
+
idle: boolean;
|
|
26
|
+
/** `frameCount` на момент возврата. */
|
|
27
|
+
frames: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ждёт, пока `source` перестанет перерисовываться. Возвращает `{ idle, frames }`;
|
|
31
|
+
* по таймауту — `idle: false` (решение, ждать ли дальше, за вызывающим). Никогда
|
|
32
|
+
* не бросает.
|
|
33
|
+
*/
|
|
34
|
+
export declare function waitForIdle(source: IdleSource, options?: WaitForIdleOptions): Promise<WaitForIdleResult>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Ожидание «рендер устоялся» для инспектора. Клиент (e2e) после инъекции ввода
|
|
2
|
+
// хочет дождаться, пока приложение перестанет перерисовываться, — не угадывая
|
|
3
|
+
// паузу `sleep`, а наблюдая за реальным счётчиком кадров. «Устоялся» = frameCount
|
|
4
|
+
// не менялся в течение `quietMs` И нет отложенного (setImmediate) рендера.
|
|
5
|
+
//
|
|
6
|
+
// Важно: это «рендер устоялся», а НЕ «все эффекты завершились». Асинхронные
|
|
7
|
+
// хвосты (ответ ext-host'а, дебаунс StateService) idle не ловит — под них клиент
|
|
8
|
+
// использует предикат по дереву/кадру, а не увеличенный quietMs.
|
|
9
|
+
const DEFAULT_QUIET_MS = 40;
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
11
|
+
const DEFAULT_POLL_MS = 10;
|
|
12
|
+
const realClock = {
|
|
13
|
+
now: () => Date.now(),
|
|
14
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Ждёт, пока `source` перестанет перерисовываться. Возвращает `{ idle, frames }`;
|
|
18
|
+
* по таймауту — `idle: false` (решение, ждать ли дальше, за вызывающим). Никогда
|
|
19
|
+
* не бросает.
|
|
20
|
+
*/
|
|
21
|
+
export async function waitForIdle(source, options = {}) {
|
|
22
|
+
const quietMs = options.quietMs ?? DEFAULT_QUIET_MS;
|
|
23
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
24
|
+
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
25
|
+
const clock = options.clock ?? realClock;
|
|
26
|
+
const start = clock.now();
|
|
27
|
+
let lastFrame = source.frameCount();
|
|
28
|
+
let lastChange = start;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const now = clock.now();
|
|
31
|
+
const frame = source.frameCount();
|
|
32
|
+
const busy = frame !== lastFrame || source.isRenderScheduled();
|
|
33
|
+
if (busy) {
|
|
34
|
+
lastFrame = frame;
|
|
35
|
+
lastChange = now;
|
|
36
|
+
}
|
|
37
|
+
else if (now - lastChange >= quietMs) {
|
|
38
|
+
return { idle: true, frames: frame };
|
|
39
|
+
}
|
|
40
|
+
if (now - start >= timeoutMs) {
|
|
41
|
+
return { idle: false, frames: frame };
|
|
42
|
+
}
|
|
43
|
+
await clock.sleep(pollMs);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { type AttachedInspector, attachInspector } from "./attachInspector.js";
|
|
2
|
+
export { InspectorCore, type InspectorTarget } from "./InspectorCore.js";
|
|
3
|
+
export type { InspectorDriver } from "./InspectorDriver.js";
|
|
4
|
+
export { InspectorServer, type InspectorServerOptions } from "./InspectorServer.js";
|
|
5
|
+
export * from "./protocol.js";
|
|
6
|
+
export { serializeTree } from "./serializeTree.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { MouseAction, MouseButton } from "@tuidom/core/input/rawTerminalToken";
|
|
2
|
+
import type { GridSnapshot } from "@tuidom/core/rendering/gridSnapshot";
|
|
3
|
+
/** Method names (namespace `TUIDom.*`). */
|
|
4
|
+
export declare const InspectorMethod: {
|
|
5
|
+
readonly getDocument: "TUIDom.getDocument";
|
|
6
|
+
/** Inject a key by DSL name. Requires a driver (headless mode). */
|
|
7
|
+
readonly sendKey: "TUIDom.sendKey";
|
|
8
|
+
/** Inject literal text as a paste. Requires a driver. */
|
|
9
|
+
readonly sendText: "TUIDom.sendText";
|
|
10
|
+
/** Inject a mouse event at screen coordinates. Requires a driver. */
|
|
11
|
+
readonly sendMouse: "TUIDom.sendMouse";
|
|
12
|
+
/** Resize the virtual terminal. Requires a driver. */
|
|
13
|
+
readonly resize: "TUIDom.resize";
|
|
14
|
+
/** Capture the current screen as a {@link GridSnapshot}. Requires a driver. */
|
|
15
|
+
readonly captureFrame: "TUIDom.captureFrame";
|
|
16
|
+
/** Wait until the app stops repainting (render settles). Requires a driver. */
|
|
17
|
+
readonly waitForIdle: "TUIDom.waitForIdle";
|
|
18
|
+
/** Tear down the session and exit. Requires a driver. */
|
|
19
|
+
readonly shutdown: "TUIDom.shutdown";
|
|
20
|
+
};
|
|
21
|
+
/** Params for `TUIDom.sendKey`. */
|
|
22
|
+
export interface SendKeyParams {
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
/** Params for `TUIDom.sendText`. */
|
|
26
|
+
export interface SendTextParams {
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Params for `TUIDom.sendMouse`.
|
|
31
|
+
*
|
|
32
|
+
* Coordinates are **0-based screen cells**, the same frame of reference as
|
|
33
|
+
* {@link NodeSnapshot.box} — so a client can read the document, take a node's
|
|
34
|
+
* `box` and click into it without any coordinate arithmetic.
|
|
35
|
+
*
|
|
36
|
+
* A click is two calls (`press` then `release`) — the same pair a terminal sends;
|
|
37
|
+
* the DOM's `click`/`dblclick` events are synthesized from it downstream.
|
|
38
|
+
*/
|
|
39
|
+
export interface SendMouseParams {
|
|
40
|
+
action: MouseAction;
|
|
41
|
+
/** Defaults to `"none"` — the only meaningful button for wheel actions. */
|
|
42
|
+
button?: MouseButton;
|
|
43
|
+
/** 0-based column */
|
|
44
|
+
x: number;
|
|
45
|
+
/** 0-based row */
|
|
46
|
+
y: number;
|
|
47
|
+
shiftKey?: boolean;
|
|
48
|
+
altKey?: boolean;
|
|
49
|
+
ctrlKey?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Params for `TUIDom.resize`. */
|
|
52
|
+
export interface ResizeParams {
|
|
53
|
+
cols: number;
|
|
54
|
+
rows: number;
|
|
55
|
+
}
|
|
56
|
+
/** Result of `TUIDom.captureFrame`. */
|
|
57
|
+
export interface CaptureFrameResult {
|
|
58
|
+
frame: GridSnapshot;
|
|
59
|
+
}
|
|
60
|
+
/** Params for `TUIDom.waitForIdle`. */
|
|
61
|
+
export interface WaitForIdleParams {
|
|
62
|
+
/** How long a frame must stay unchanged to count as settled (ms). */
|
|
63
|
+
quietMs?: number;
|
|
64
|
+
/** Give up after this long and report `idle: false` (ms). */
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
}
|
|
67
|
+
/** Result of `TUIDom.waitForIdle`. */
|
|
68
|
+
export interface WaitForIdleResult {
|
|
69
|
+
/** `true` — render settled; `false` — timed out while still repainting. */
|
|
70
|
+
idle: boolean;
|
|
71
|
+
/** Frame counter at return. */
|
|
72
|
+
frames: number;
|
|
73
|
+
}
|
|
74
|
+
/** Serialized tree node. */
|
|
75
|
+
export interface NodeSnapshot {
|
|
76
|
+
/** Sequential id within a single snapshot (pre-order). Not stable across snapshots yet. */
|
|
77
|
+
nodeId: number;
|
|
78
|
+
/** Runtime class name (e.g. "BodyElement"). */
|
|
79
|
+
type: string;
|
|
80
|
+
id?: string;
|
|
81
|
+
role?: string;
|
|
82
|
+
focusable?: true;
|
|
83
|
+
box: {
|
|
84
|
+
x: number;
|
|
85
|
+
y: number;
|
|
86
|
+
width: number;
|
|
87
|
+
height: number;
|
|
88
|
+
};
|
|
89
|
+
style: {
|
|
90
|
+
fg: number;
|
|
91
|
+
bg: number;
|
|
92
|
+
};
|
|
93
|
+
/** Активные состояния стиля (hover/focus/произвольные) — если есть. */
|
|
94
|
+
styleStates?: string[];
|
|
95
|
+
/** Исходные имена токенов рядом с резолвленными числами style — если стиль ссылается на токены. */
|
|
96
|
+
styleTokens?: {
|
|
97
|
+
fg?: string;
|
|
98
|
+
bg?: string;
|
|
99
|
+
};
|
|
100
|
+
/** Элемент владеет заливкой фона (bg задан собственным стилем). */
|
|
101
|
+
ownBackground?: true;
|
|
102
|
+
focused: boolean;
|
|
103
|
+
text?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Observable widget state, self-described by the element's `inspectState()`
|
|
106
|
+
* (cursor/selection/readonly for an editor, active tab for a panel, …).
|
|
107
|
+
* Absent when the element exposes none. Lets e2e assert on data instead of
|
|
108
|
+
* guessing from rendered cells.
|
|
109
|
+
*/
|
|
110
|
+
state?: Record<string, unknown>;
|
|
111
|
+
children: NodeSnapshot[];
|
|
112
|
+
}
|
|
113
|
+
/** Result of `TUIDom.getDocument`. */
|
|
114
|
+
export interface GetDocumentResult {
|
|
115
|
+
root: NodeSnapshot | null;
|
|
116
|
+
}
|
|
117
|
+
/** Inbound request envelope. */
|
|
118
|
+
export interface InspectorRequest {
|
|
119
|
+
id: number;
|
|
120
|
+
method: string;
|
|
121
|
+
params?: unknown;
|
|
122
|
+
}
|
|
123
|
+
export interface InspectorSuccessResponse {
|
|
124
|
+
id: number;
|
|
125
|
+
result: unknown;
|
|
126
|
+
}
|
|
127
|
+
export interface InspectorErrorResponse {
|
|
128
|
+
id: number;
|
|
129
|
+
error: {
|
|
130
|
+
message: string;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export type InspectorResponse = InspectorSuccessResponse | InspectorErrorResponse;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// TUIDom inspector protocol — own, CDP-shaped (not CDP-compatible).
|
|
2
|
+
//
|
|
3
|
+
// Wire form mirrors Chrome DevTools Protocol in shape only: a request carries
|
|
4
|
+
// `{ id, method, params? }`, a reply is `{ id, result }` or `{ id, error }`.
|
|
5
|
+
// Methods live under the `TUIDom.*` namespace. The protocol is transport-
|
|
6
|
+
// agnostic — InspectorCore speaks it directly (in-process) or over WebSocket.
|
|
7
|
+
/** Method names (namespace `TUIDom.*`). */
|
|
8
|
+
export const InspectorMethod = {
|
|
9
|
+
getDocument: "TUIDom.getDocument",
|
|
10
|
+
/** Inject a key by DSL name. Requires a driver (headless mode). */
|
|
11
|
+
sendKey: "TUIDom.sendKey",
|
|
12
|
+
/** Inject literal text as a paste. Requires a driver. */
|
|
13
|
+
sendText: "TUIDom.sendText",
|
|
14
|
+
/** Inject a mouse event at screen coordinates. Requires a driver. */
|
|
15
|
+
sendMouse: "TUIDom.sendMouse",
|
|
16
|
+
/** Resize the virtual terminal. Requires a driver. */
|
|
17
|
+
resize: "TUIDom.resize",
|
|
18
|
+
/** Capture the current screen as a {@link GridSnapshot}. Requires a driver. */
|
|
19
|
+
captureFrame: "TUIDom.captureFrame",
|
|
20
|
+
/** Wait until the app stops repainting (render settles). Requires a driver. */
|
|
21
|
+
waitForIdle: "TUIDom.waitForIdle",
|
|
22
|
+
/** Tear down the session and exit. Requires a driver. */
|
|
23
|
+
shutdown: "TUIDom.shutdown",
|
|
24
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
2
|
+
import type { NodeSnapshot } from "./protocol.js";
|
|
3
|
+
/**
|
|
4
|
+
* Serialize a TUIDom subtree into a plain JSON snapshot, reading only public
|
|
5
|
+
* accessors of TUIElement (no widget changes). `nodeId` is assigned pre-order.
|
|
6
|
+
*/
|
|
7
|
+
export declare function serializeTree(root: TUIElement | null, focused: TUIElement | null): NodeSnapshot | null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TextLabelElement } from "@tuidom/elements/text/textLabelElement";
|
|
2
|
+
/**
|
|
3
|
+
* Serialize a TUIDom subtree into a plain JSON snapshot, reading only public
|
|
4
|
+
* accessors of TUIElement (no widget changes). `nodeId` is assigned pre-order.
|
|
5
|
+
*/
|
|
6
|
+
export function serializeTree(root, focused) {
|
|
7
|
+
if (root === null)
|
|
8
|
+
return null;
|
|
9
|
+
return serializeNode(root, focused, { next: 0 });
|
|
10
|
+
}
|
|
11
|
+
function serializeNode(element, focused, counter) {
|
|
12
|
+
const nodeId = counter.next++;
|
|
13
|
+
const pos = element.globalPosition;
|
|
14
|
+
const size = element.layoutSize;
|
|
15
|
+
const style = element.resolvedStyle;
|
|
16
|
+
const snapshot = {
|
|
17
|
+
nodeId,
|
|
18
|
+
type: element.constructor.name,
|
|
19
|
+
box: { x: pos.x, y: pos.y, width: size.width, height: size.height },
|
|
20
|
+
style: { fg: style.fg, bg: style.bg },
|
|
21
|
+
focused: element === focused,
|
|
22
|
+
children: [],
|
|
23
|
+
};
|
|
24
|
+
if (element.id !== undefined)
|
|
25
|
+
snapshot.id = element.id;
|
|
26
|
+
if (element.role !== undefined)
|
|
27
|
+
snapshot.role = element.role;
|
|
28
|
+
if (element.focusable)
|
|
29
|
+
snapshot.focusable = true;
|
|
30
|
+
const states = element.activeStyleStates;
|
|
31
|
+
if (states.length > 0)
|
|
32
|
+
snapshot.styleStates = [...states];
|
|
33
|
+
const applied = element.appliedStyle;
|
|
34
|
+
if (typeof applied.fg === "string" || typeof applied.bg === "string") {
|
|
35
|
+
snapshot.styleTokens = {};
|
|
36
|
+
if (typeof applied.fg === "string")
|
|
37
|
+
snapshot.styleTokens.fg = applied.fg;
|
|
38
|
+
if (typeof applied.bg === "string")
|
|
39
|
+
snapshot.styleTokens.bg = applied.bg;
|
|
40
|
+
}
|
|
41
|
+
if (element.hasOwnBackground)
|
|
42
|
+
snapshot.ownBackground = true;
|
|
43
|
+
if (element instanceof TextLabelElement)
|
|
44
|
+
snapshot.text = element.getText();
|
|
45
|
+
const state = element.inspectState();
|
|
46
|
+
if (state !== undefined)
|
|
47
|
+
snapshot.state = state;
|
|
48
|
+
// Скрытые (hidden) поддеревья не сериализуются: инспектор — «глаз
|
|
49
|
+
// пользователя», а скрытое не рисуется, не кликается и не участвует в
|
|
50
|
+
// Tab-обходе. Структурно такие элементы остаются в дереве (стили/root
|
|
51
|
+
// доходят) — но e2e-тест, ждущий исчезновения виджета со сцены
|
|
52
|
+
// (waitForNoNode), должен видеть ровно то же, что видит пользователь.
|
|
53
|
+
snapshot.children = element
|
|
54
|
+
.getChildren()
|
|
55
|
+
.filter((child) => !child.hidden)
|
|
56
|
+
.map((child) => serializeNode(child, focused, counter));
|
|
57
|
+
return snapshot;
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const OPCODE_TEXT = 1;
|
|
2
|
+
export declare const OPCODE_CLOSE = 8;
|
|
3
|
+
export declare const OPCODE_PING = 9;
|
|
4
|
+
export declare const OPCODE_PONG = 10;
|
|
5
|
+
export interface DecodedFrame {
|
|
6
|
+
opcode: number;
|
|
7
|
+
payload: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Extract every complete frame buffered so far. Returns the decoded frames and
|
|
11
|
+
* the unconsumed tail (a partial frame awaiting more bytes).
|
|
12
|
+
*/
|
|
13
|
+
export declare function decodeFrames(buffer: Buffer): {
|
|
14
|
+
frames: DecodedFrame[];
|
|
15
|
+
rest: Buffer;
|
|
16
|
+
};
|
|
17
|
+
/** Encode a server→client frame (unmasked, single fragment). */
|
|
18
|
+
export declare function encodeFrame(opcode: number, payload: Buffer): Buffer;
|
|
19
|
+
/** Encode a server→client text frame. */
|
|
20
|
+
export declare function encodeTextFrame(text: string): Buffer;
|
package/dist/ws/frame.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Minimal RFC6455 codec. Scope: single-fragment frames (FIN=1), client→server
|
|
2
|
+
// masked, payloads up to 64 KiB practical. No continuation frames — inspector
|
|
3
|
+
// messages are small JSON. Enough for the inspector transport.
|
|
4
|
+
export const OPCODE_TEXT = 0x1;
|
|
5
|
+
export const OPCODE_CLOSE = 0x8;
|
|
6
|
+
export const OPCODE_PING = 0x9;
|
|
7
|
+
export const OPCODE_PONG = 0xa;
|
|
8
|
+
/**
|
|
9
|
+
* Extract every complete frame buffered so far. Returns the decoded frames and
|
|
10
|
+
* the unconsumed tail (a partial frame awaiting more bytes).
|
|
11
|
+
*/
|
|
12
|
+
export function decodeFrames(buffer) {
|
|
13
|
+
const frames = [];
|
|
14
|
+
let offset = 0;
|
|
15
|
+
while (offset + 2 <= buffer.length) {
|
|
16
|
+
const b1 = buffer[offset + 1];
|
|
17
|
+
const opcode = buffer[offset] & 0x0f;
|
|
18
|
+
const masked = (b1 & 0x80) !== 0;
|
|
19
|
+
let len = b1 & 0x7f;
|
|
20
|
+
let pos = offset + 2;
|
|
21
|
+
if (len === 126) {
|
|
22
|
+
if (pos + 2 > buffer.length)
|
|
23
|
+
break;
|
|
24
|
+
len = buffer.readUInt16BE(pos);
|
|
25
|
+
pos += 2;
|
|
26
|
+
}
|
|
27
|
+
else if (len === 127) {
|
|
28
|
+
if (pos + 8 > buffer.length)
|
|
29
|
+
break;
|
|
30
|
+
len = Number(buffer.readBigUInt64BE(pos));
|
|
31
|
+
pos += 8;
|
|
32
|
+
}
|
|
33
|
+
const maskLen = masked ? 4 : 0;
|
|
34
|
+
if (pos + maskLen + len > buffer.length)
|
|
35
|
+
break; // frame not fully arrived
|
|
36
|
+
let payload;
|
|
37
|
+
if (masked) {
|
|
38
|
+
const mask = buffer.subarray(pos, pos + 4);
|
|
39
|
+
pos += 4;
|
|
40
|
+
payload = Buffer.allocUnsafe(len);
|
|
41
|
+
for (let i = 0; i < len; i++) {
|
|
42
|
+
payload[i] = buffer[pos + i] ^ mask[i & 3];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
payload = buffer.subarray(pos, pos + len);
|
|
47
|
+
}
|
|
48
|
+
pos += len;
|
|
49
|
+
frames.push({ opcode, payload: payload.toString("utf8") });
|
|
50
|
+
offset = pos;
|
|
51
|
+
}
|
|
52
|
+
return { frames, rest: buffer.subarray(offset) };
|
|
53
|
+
}
|
|
54
|
+
/** Encode a server→client frame (unmasked, single fragment). */
|
|
55
|
+
export function encodeFrame(opcode, payload) {
|
|
56
|
+
const len = payload.length;
|
|
57
|
+
let header;
|
|
58
|
+
if (len < 126) {
|
|
59
|
+
header = Buffer.from([0x80 | opcode, len]);
|
|
60
|
+
}
|
|
61
|
+
else if (len < 65536) {
|
|
62
|
+
header = Buffer.allocUnsafe(4);
|
|
63
|
+
header[0] = 0x80 | opcode;
|
|
64
|
+
header[1] = 126;
|
|
65
|
+
header.writeUInt16BE(len, 2);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
header = Buffer.allocUnsafe(10);
|
|
69
|
+
header[0] = 0x80 | opcode;
|
|
70
|
+
header[1] = 127;
|
|
71
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat([header, payload]);
|
|
74
|
+
}
|
|
75
|
+
/** Encode a server→client text frame. */
|
|
76
|
+
export function encodeTextFrame(text) {
|
|
77
|
+
return encodeFrame(OPCODE_TEXT, Buffer.from(text, "utf8"));
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
// RFC6455 magic GUID appended to Sec-WebSocket-Key before hashing.
|
|
3
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
4
|
+
/** Compute the Sec-WebSocket-Accept value for a client's Sec-WebSocket-Key. */
|
|
5
|
+
export function computeAcceptKey(secWebSocketKey) {
|
|
6
|
+
return createHash("sha1")
|
|
7
|
+
.update(secWebSocketKey + WS_GUID)
|
|
8
|
+
.digest("base64");
|
|
9
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tuidom/inspector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Experimental terminal UI DOM — WebSocket inspector for devtools/e2e (hand-written RFC6455, zero-dep). API is unstable.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/tuidom/tuidom.git",
|
|
10
|
+
"directory": "packages/inspector"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24.0.0"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"tui",
|
|
17
|
+
"terminal",
|
|
18
|
+
"ui",
|
|
19
|
+
"dom",
|
|
20
|
+
"inspector",
|
|
21
|
+
"devtools"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
"./*.js": "./dist/*.js",
|
|
29
|
+
"./*": "./dist/*.js"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@tuidom/core": "0.1.0",
|
|
33
|
+
"@tuidom/elements": "0.1.0"
|
|
34
|
+
}
|
|
35
|
+
}
|