Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/skills/lynx-devtool/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ This skill allows you to interact with Lynx applications running on connected de

## Usage

### CLI

The CLI is located at `<path_to_the_skill>/scripts/index.mjs` relative to this skill's directory. You can run it using `node`.

In the skill directory, use:
Expand All @@ -17,6 +19,8 @@ In the skill directory, use:
node <path_to_the_skill>/scripts/index.mjs <command>
```

When installed as a package, the CLI is also exposed as `lynx-devtool`.

**Note:** All command outputs are multi-line JSON. You can use `jq` or Node.js to process the data.

### Global Options
Expand All @@ -33,6 +37,33 @@ node <path_to_the_skill>/scripts/index.mjs <command>
- [Get Sources](examples/get-sources.md): List parsed scripts for later debugger operations.
- [Take Screenshot](examples/take-screenshot.md): Capture the current page as a screenshot.

### Node SDK

Other Node.js projects can import the same core capability used by the CLI:

```ts
import { createDevtoolClient } from '@lynx-js/skill-lynx-devtool/sdk';

const client = createDevtoolClient();

try {
const clients = await client.listClients();
const sessions = await client.listSessions();
const result = await client.cdp({
method: 'Runtime.evaluate',
params: {
expression: '2 + 2',
returnByValue: true,
},
});

console.log(result);
console.log({ clients, sessions });
} finally {
await client.close();
}
```

## Example List

- [Inspecting the DOM](examples/inspecting-the-dom.md): Find a session, fetch the document tree, and inspect a specific node.
Expand Down
7 changes: 7 additions & 0 deletions packages/skills/lynx-devtool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
"./sdk": {
"import": "./scripts/sdk.mjs",
"types": "./scripts/sdk.d.ts",
"default": "./scripts/sdk.mjs"
}
},
"files": [
"SKILL.md",
"scripts",
Expand Down
24 changes: 24 additions & 0 deletions packages/skills/lynx-devtool/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ export default defineConfig({
format: 'esm',
syntax: 'es2022',
dts: false,
source: {
entry: {
index: './src/index.ts',
},
},
output: {
filename: {
js: '[name].mjs',
},
distPath: './scripts',
},
autoExtension: false,
},
{
format: 'esm',
syntax: 'es2022',
dts: {
bundle: true,
},
source: {
entry: {
sdk: './src/sdk.ts',
},
},
output: {
filename: {
js: '[name].mjs',
Expand Down
17 changes: 7 additions & 10 deletions packages/skills/lynx-devtool/src/commands/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

import type { Connector } from '@lynx-js/devtool-connector';
import type { Command } from 'commander';
import { getFirstClient } from './utils.ts';
import type { DevtoolClient } from '../sdk.ts';

export function registerAppCommand(program: Command, connector: Connector) {
export function registerAppCommand(program: Command, client: DevtoolClient) {
program
.command('app')
.description('Send an App request')
Expand All @@ -18,14 +17,12 @@ export function registerAppCommand(program: Command, connector: Connector) {
.argument('[params]', 'JSON string of parameters')
.action(async (paramsStr, options) => {
const { method } = options;
let { client: clientId } = options;

if (!clientId) {
clientId = await getFirstClient(connector);
}

const params = paramsStr ? JSON.parse(paramsStr) : {};
const result = await connector.sendAppMessage(clientId, method, params);
const result = await client.app({
clientId: options.client,
method,
params,
});
console.log(JSON.stringify(result, null, 2));
});
}
24 changes: 6 additions & 18 deletions packages/skills/lynx-devtool/src/commands/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

import type { Connector } from '@lynx-js/devtool-connector';
import type { Command } from 'commander';
import { getFirstClient, getLatestSession } from './utils.ts';
import type { DevtoolClient } from '../sdk.ts';

export function registerCdpCommand(program: Command, connector: Connector) {
export function registerCdpCommand(program: Command, client: DevtoolClient) {
program
.command('cdp')
.description('Send a CDP request')
Expand All @@ -25,24 +24,13 @@ export function registerCdpCommand(program: Command, connector: Connector) {
.argument('[params]', 'JSON string of parameters')
.action(async (paramsStr, options) => {
const { method } = options;
let { client: clientId, session: sessionId } = options;

if (!clientId) {
clientId = await getFirstClient(connector);
}

if (!sessionId) {
sessionId = await getLatestSession(connector, clientId);
}

const params = paramsStr ? JSON.parse(paramsStr) : {};

const result = await connector.sendCDPMessage(
clientId,
Number(sessionId),
const result = await client.cdp({
clientId: options.client,
sessionId: options.session,
method,
params,
);
});

console.log(JSON.stringify(result, null, 2));
});
Expand Down
172 changes: 37 additions & 135 deletions packages/skills/lynx-devtool/src/commands/get-console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,12 @@
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

import { ReadableStream } from 'node:stream/web';
import { setTimeout } from 'node:timers/promises';
import type { Connector } from '@lynx-js/devtool-connector';
import type { Command } from 'commander';
import { getFirstClient, getLatestSession } from './utils.ts';

interface ConsoleCallFrame {
url: string;
lineNumber: number;
columnNumber: number;
}

interface ConsoleStackTrace {
callFrames: ConsoleCallFrame[];
}

interface ConsoleArg {
type: string;
value?: unknown;
className?: string;
description?: string;
objectId?: string;
subtype?: string;
}

interface ConsoleMessage {
type: string;
args: ConsoleArg[];
stackTrace?: ConsoleStackTrace;
url?: string;
}
import type { ConsoleMessage, DevtoolClient } from '../sdk.ts';

export function registerGetConsoleCommand(
program: Command,
connector: Connector,
client: DevtoolClient,
) {
program
.command('get-console')
Expand Down Expand Up @@ -69,110 +40,41 @@ export function registerGetConsoleCommand(
(value) => value.split(',').map((s) => s.trim()),
)
.action(async (options) => {
let { client: clientId, session: sessionId, limit } = options;
const { offset = 0, includeStackTraces, level } = options;

if (limit) {
limit = Math.max(1, Math.min(100, limit));
}

if (!clientId) {
clientId = await getFirstClient(connector);
}

if (!sessionId) {
sessionId = await getLatestSession(connector, clientId);
}

const numericSessionId = Number(sessionId);

await using stream = await connector.sendCDPStream(
clientId,
ReadableStream.from([
{
sessionId: numericSessionId,
method: 'Page.enable',
},
{
sessionId: numericSessionId,
method: 'Runtime.enable',
},
]),
);

const messages: ConsoleMessage[] = [];
const defaultLevels = ['info', 'log', 'warning', 'error'];
const allowedLevels = level || defaultLevels;
let skipped = 0;

const reader = stream.getReader();
const IDLE_TIMEOUT = 500;
const MAX_TOTAL_TIME = 5000;
const startTime = Date.now();

try {
while (Date.now() - startTime < MAX_TOTAL_TIME) {
const result = await Promise.race([
reader.read(),
setTimeout(IDLE_TIMEOUT, 'timeout' as const),
]);
if (result === 'timeout') {
await reader.cancel();
break;
}

const { done, value } = result;
if (done) break;

if (value.method === 'Runtime.consoleAPICalled') {
const params = value.params as ConsoleMessage;
if (allowedLevels.includes(params.type)) {
if (skipped < offset) {
skipped++;
continue;
}

if (!includeStackTraces && params.type !== 'error') {
delete params.stackTrace;
}

messages.push(params);
const messages = await client.getConsole({
clientId: options.client,
sessionId: options.session,
offset: options.offset,
limit: options.limit,
includeStackTraces: options.includeStackTraces,
level: options.level,
});

console.log(formatConsoleMessages(messages));
});
}

if (limit && messages.length >= limit) {
await reader.cancel();
break;
}
function formatConsoleMessages(messages: ConsoleMessage[]): string {
return messages
.map(
({ type, args, stackTrace }) =>
`- [${type}]: ${args
.map((arg) => {
if (arg.objectId) {
return `<${arg.description || arg.className || 'Object'} (objectId:${arg.objectId})>`;
}
}
}
} finally {
reader.releaseLock();
}

console.log(
messages
.map(
({ type, args, stackTrace }) =>
`- [${type}]: ${args
.map((arg) => {
if (arg.objectId) {
return `<${arg.description || arg.className || 'Object'} (objectId:${arg.objectId})>`;
}
return arg.value;
})
.join(' ')}${
stackTrace
? '\n' +
stackTrace.callFrames
.map(
({ url, lineNumber, columnNumber }) =>
` at ${url}:${lineNumber}:${columnNumber}`,
)
.join('\n')
: ''
}`,
)
.join('\n'),
);
});
return arg.value;
})
.join(' ')}${
stackTrace
? '\n' +
stackTrace.callFrames
.map(
({ url, lineNumber, columnNumber }) =>
` at ${url}:${lineNumber}:${columnNumber}`,
)
.join('\n')
: ''
}`,
)
.join('\n');
}
Loading