explorbot 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/explorbot-cli.ts +52 -37
- package/boat/api-tester/src/apibot.ts +4 -2
- package/boat/api-tester/src/cli.ts +2 -2
- package/boat/api-tester/src/config.ts +39 -8
- package/boat/doc-collector/src/cli.ts +1 -0
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- package/boat/doc-collector/src/state-diagram.ts +61 -14
- package/boat/prima/bin/prima-cli.ts +5 -0
- package/boat/prima/package.json +16 -0
- package/boat/prima/src/cli.ts +222 -0
- package/boat/prima/src/envelope.ts +141 -0
- package/boat/prima/src/prima.ts +705 -0
- package/boat/prima/src/pw-parser.ts +17 -0
- package/boat/prima/src/pw-registry.ts +75 -0
- package/dist/bin/explorbot-cli.js +44 -31
- package/dist/boat/api-tester/src/apibot.js +3 -2
- package/dist/boat/api-tester/src/cli.js +2 -2
- package/dist/boat/api-tester/src/config.js +36 -8
- package/dist/boat/doc-collector/src/cli.js +1 -0
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- package/dist/boat/doc-collector/src/state-diagram.js +57 -13
- package/dist/boat/prima/bin/prima-cli.js +4 -0
- package/dist/boat/prima/src/cli.js +200 -0
- package/dist/boat/prima/src/envelope.js +116 -0
- package/dist/boat/prima/src/prima.js +635 -0
- package/dist/boat/prima/src/pw-parser.js +18 -0
- package/dist/boat/prima/src/pw-registry.js +66 -0
- package/dist/models.json +3 -0
- package/dist/package.json +6 -2
- package/dist/src/action.d.ts +5 -2
- package/dist/src/action.js +5 -5
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +1 -1
- package/dist/src/ai/navigator.d.ts +4 -0
- package/dist/src/ai/navigator.js +11 -6
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/task-agent.js +1 -1
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +13 -0
- package/dist/src/application-spec-contract.d.ts +8 -0
- package/dist/src/application-spec-contract.js +8 -0
- package/dist/src/application-spec.d.ts +15 -0
- package/dist/src/application-spec.js +71 -0
- package/dist/src/browser-server.d.ts +12 -6
- package/dist/src/browser-server.js +74 -19
- package/dist/src/commands/clean-command.js +2 -7
- package/dist/src/commands/init-command.d.ts +5 -0
- package/dist/src/commands/init-command.js +119 -1
- package/dist/src/commands/navigate-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/sites-command.d.ts +6 -0
- package/dist/src/commands/sites-command.js +23 -0
- package/dist/src/components/InitWizard.d.ts +10 -0
- package/dist/src/components/InitWizard.js +133 -0
- package/dist/src/components/InputReadline.d.ts +1 -0
- package/dist/src/components/InputReadline.js +7 -4
- package/dist/src/config.d.ts +24 -5
- package/dist/src/config.js +146 -37
- package/dist/src/explorbot.d.ts +9 -0
- package/dist/src/explorbot.js +24 -5
- package/dist/src/explorer.d.ts +4 -1
- package/dist/src/explorer.js +40 -6
- package/dist/src/global-config.d.ts +22 -0
- package/dist/src/global-config.js +117 -0
- package/dist/src/knowledge-tracker.d.ts +5 -1
- package/dist/src/knowledge-tracker.js +14 -1
- package/dist/src/utils/cli-name.js +6 -2
- package/dist/src/utils/test-files.js +1 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +9 -0
- package/models.json +3 -0
- package/package.json +6 -2
- package/src/action.ts +9 -5
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +1 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner.ts +7 -0
- package/src/ai/researcher.ts +1 -1
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +15 -0
- package/src/application-spec-contract.ts +10 -0
- package/src/application-spec.ts +87 -0
- package/src/browser-server.ts +74 -19
- package/src/commands/clean-command.ts +1 -6
- package/src/commands/init-command.ts +146 -1
- package/src/commands/navigate-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/sites-command.ts +27 -0
- package/src/components/InitWizard.tsx +166 -0
- package/src/components/InputReadline.tsx +8 -4
- package/src/config.ts +162 -39
- package/src/explorbot.ts +30 -5
- package/src/explorer.ts +45 -7
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- package/src/utils/cli-name.ts +5 -2
- package/src/utils/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +10 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import dedent from 'dedent';
|
|
3
|
+
import { keepServerRunning } from "../../../src/browser-server.js";
|
|
4
|
+
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
5
|
+
import { setPreserveConsoleLogs } from "../../../src/utils/logger.js";
|
|
6
|
+
import { renderEnvelope } from "./envelope.js";
|
|
7
|
+
import { Prima } from "./prima.js";
|
|
8
|
+
const helpContract = dedent `
|
|
9
|
+
Prima drives a browser that is already open. One command per process; every command
|
|
10
|
+
prints a plain-text envelope on stdout and exits 0 when ok, 1 when not.
|
|
11
|
+
|
|
12
|
+
TIERS - choose by what you hold, not by how hard the step looks
|
|
13
|
+
pw <fn> Precise. A Playwright function expression built from a locator you
|
|
14
|
+
already verified. No AI on the happy path.
|
|
15
|
+
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
16
|
+
click / fill One action described in words; AI resolves it on the current page.
|
|
17
|
+
prima click "the primary action button in the header"
|
|
18
|
+
prima fill "the search box" "a search term"
|
|
19
|
+
do <steps...> Several described steps, run tester-style in one process.
|
|
20
|
+
prima do "open the account menu" "choose the settings entry"
|
|
21
|
+
Never pass a locator or a function expression to click/fill/do - describe the target.
|
|
22
|
+
Never pass a description to pw - it takes executable code only.
|
|
23
|
+
|
|
24
|
+
LOOP
|
|
25
|
+
prima go <url|path|words> reach the page you want to work on
|
|
26
|
+
prima research once per new page; returns verified locators
|
|
27
|
+
prima pw "..." drive the page with those locators
|
|
28
|
+
prima verify "..." assert the outcome (prima ask "..." to inspect instead)
|
|
29
|
+
Fall back to click/fill/do whenever research left you no locator to hold.
|
|
30
|
+
|
|
31
|
+
ENVELOPE
|
|
32
|
+
### Result ok, command, healed, used
|
|
33
|
+
### Page url, title, state hash, visit count
|
|
34
|
+
### Changes what the accessibility tree gained or lost
|
|
35
|
+
### Answer | ### Research | ### Verdict output of ask, research, verify
|
|
36
|
+
### Failure error, reasoning, healing attempts, compact ARIA of the page
|
|
37
|
+
### Instance the browser you are on and the other instances running
|
|
38
|
+
### Artifacts paths to the full aria.yml, page.html and network.jsonl
|
|
39
|
+
used: is code that already executed - CodeceptJS steps to copy as they are, except
|
|
40
|
+
for pw, whose Playwright expression a test needs inside I.usePlaywrightTo(...).
|
|
41
|
+
Log lines can precede the envelope; start parsing at the first ### line.
|
|
42
|
+
|
|
43
|
+
HEALING AND FAILURE
|
|
44
|
+
A failed action is retried by AI along a different route; healed: true means the
|
|
45
|
+
outcome was reached another way and used: holds the code that worked.
|
|
46
|
+
--no-heal skips that and fails fast.
|
|
47
|
+
Failures print compact ARIA inline, so retarget from the envelope itself and open
|
|
48
|
+
the artifact files only when the inline snapshot is not enough.
|
|
49
|
+
|
|
50
|
+
SESSIONS
|
|
51
|
+
By default prima attaches to the playwright-cli browser of this workspace and works
|
|
52
|
+
on the tabs it already has open; driving the same session from both tools is the
|
|
53
|
+
intended usage.
|
|
54
|
+
playwright-cli open <url> the session prima attaches to
|
|
55
|
+
--pw-session <title> which playwright-cli session, when several are open
|
|
56
|
+
--endpoint <ep> attach to a browser server endpoint directly
|
|
57
|
+
prima browser start a prima-owned browser instead, when no session is open
|
|
58
|
+
--instance <name> which prima-owned browser you talk to; parallel work
|
|
59
|
+
needs one each
|
|
60
|
+
--session [file] cookies and storage persisted across processes; ignored
|
|
61
|
+
while attached, the attached session keeps its own
|
|
62
|
+
Prima never launches a browser implicitly and never closes an attached one - it
|
|
63
|
+
disconnects. browser list shows both kinds; ### Instance names the one you are on.
|
|
64
|
+
Every browser is reached over a Playwright browser-server endpoint, which needs the
|
|
65
|
+
Node build - run prima as "npx explorbot prima ..." or through the published prima
|
|
66
|
+
bin; from source under Bun the connection does not open.
|
|
67
|
+
When no AI model is usable pw still works; for everything else drive
|
|
68
|
+
playwright-cli directly.
|
|
69
|
+
Parsed but not active yet: --framework, so reported code is CodeceptJS whatever
|
|
70
|
+
you pass.
|
|
71
|
+
`;
|
|
72
|
+
function buildOptions(options) {
|
|
73
|
+
return {
|
|
74
|
+
verbose: options.verbose || options.debug,
|
|
75
|
+
config: options.config,
|
|
76
|
+
path: options.path,
|
|
77
|
+
instance: options.instance,
|
|
78
|
+
session: options.session,
|
|
79
|
+
heal: options.heal,
|
|
80
|
+
ephemeral: options.ephemeral,
|
|
81
|
+
framework: options.framework,
|
|
82
|
+
noVision: options.vision === false,
|
|
83
|
+
url: options.url,
|
|
84
|
+
baseUrl: options.baseUrl,
|
|
85
|
+
show: options.show,
|
|
86
|
+
headless: options.headless,
|
|
87
|
+
endpoint: options.endpoint,
|
|
88
|
+
pwSession: options.pwSession,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function addCommonOptions(cmd) {
|
|
92
|
+
return cmd
|
|
93
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
94
|
+
.option('--debug', 'Enable debug logging (same as --verbose)')
|
|
95
|
+
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
96
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
97
|
+
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
98
|
+
.option('--session [file]', 'Persist cookies and storage to a session file')
|
|
99
|
+
.option('--no-heal', 'Fail immediately instead of letting AI retry a failed action')
|
|
100
|
+
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
101
|
+
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
102
|
+
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
103
|
+
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
104
|
+
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
|
|
105
|
+
}
|
|
106
|
+
function primaFor(options) {
|
|
107
|
+
setPreserveConsoleLogs(true);
|
|
108
|
+
if (options.ephemeral)
|
|
109
|
+
process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
110
|
+
return new Prima(buildOptions(options));
|
|
111
|
+
}
|
|
112
|
+
async function runPrima(options, command, run) {
|
|
113
|
+
const prima = primaFor(options);
|
|
114
|
+
let envelope;
|
|
115
|
+
try {
|
|
116
|
+
await prima.start();
|
|
117
|
+
envelope = await run(prima);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
envelope = await prima.toolFailureEnvelope(command, error);
|
|
121
|
+
}
|
|
122
|
+
console.log(renderEnvelope(envelope));
|
|
123
|
+
await prima.stop().catch(() => { });
|
|
124
|
+
process.exit(envelope.ok ? 0 : 1);
|
|
125
|
+
}
|
|
126
|
+
async function runBrowser(options, run) {
|
|
127
|
+
let ok = false;
|
|
128
|
+
try {
|
|
129
|
+
ok = await run(primaFor(options));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error(browserErrorMessage(error));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
process.exit(ok ? 0 : 1);
|
|
136
|
+
}
|
|
137
|
+
export function createPrimaCommands(name = 'prima') {
|
|
138
|
+
const cmd = new Command(name);
|
|
139
|
+
cmd.description('Drive an already-open browser one command at a time and report back in a plain-text envelope');
|
|
140
|
+
cmd.addHelpText('after', `\n${helpContract}`);
|
|
141
|
+
addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
|
|
142
|
+
await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
|
|
143
|
+
});
|
|
144
|
+
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction')).action(async (instructions, options) => {
|
|
145
|
+
await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
|
|
146
|
+
});
|
|
147
|
+
addCommonOptions(cmd.command('click <target>').description('Click an element described in plain words')).action(async (target, options) => {
|
|
148
|
+
await runPrima(options, `click ${target}`, (prima) => prima.click(target));
|
|
149
|
+
});
|
|
150
|
+
addCommonOptions(cmd.command('fill <field> <value>').description('Fill a field described in plain words')).action(async (field, value, options) => {
|
|
151
|
+
await runPrima(options, `fill ${field} ${value}`, (prima) => prima.fill(field, value));
|
|
152
|
+
});
|
|
153
|
+
addCommonOptions(cmd.command('ask <question>').description('Answer a question about the current page').option('--no-vision', 'Answer from page structure only, without a screenshot')).action(async (question, options) => {
|
|
154
|
+
await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
|
|
155
|
+
});
|
|
156
|
+
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page')).action(async (assertion, options) => {
|
|
157
|
+
await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
|
|
158
|
+
});
|
|
159
|
+
addCommonOptions(cmd.command('research').description('Map the current page and return verified locators').option('--data', 'Include data extraction in the map').option('--deep', 'Expand hidden elements for a deeper map').option('--fresh', 'Ignore the cached map and research the page again')).action(async (options) => {
|
|
160
|
+
await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
|
|
161
|
+
});
|
|
162
|
+
addCommonOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
|
|
163
|
+
if (URL.canParse(target))
|
|
164
|
+
options.baseUrl = target;
|
|
165
|
+
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
166
|
+
});
|
|
167
|
+
const browser = cmd.command('browser').description('Manage the browsers prima drives');
|
|
168
|
+
addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
|
|
169
|
+
.option('-s, --show', 'Launch the browser in a visible window')
|
|
170
|
+
.option('--headless', 'Launch the browser without a window')
|
|
171
|
+
.action(async (options) => {
|
|
172
|
+
await runBrowser(options, async (prima) => {
|
|
173
|
+
await prima.browserStart();
|
|
174
|
+
console.log(await prima.browserStatus());
|
|
175
|
+
return keepServerRunning(() => prima.browserStop());
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
addCommonOptions(browser.command('stop').description('Stop the browser of this instance'))
|
|
179
|
+
.option('--all', 'Stop every running instance')
|
|
180
|
+
.action(async (options) => {
|
|
181
|
+
await runBrowser(options, async (prima) => {
|
|
182
|
+
const stopped = await prima.browserStop(options.all);
|
|
183
|
+
console.log(await prima.browserStatus());
|
|
184
|
+
return stopped;
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
addCommonOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
|
|
188
|
+
await runBrowser(options, async (prima) => {
|
|
189
|
+
console.log(await prima.browserStatus());
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
addCommonOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
|
|
194
|
+
await runBrowser(options, async (prima) => {
|
|
195
|
+
console.log(await prima.browserList());
|
|
196
|
+
return true;
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
return cmd;
|
|
200
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function renderEnvelope(data) {
|
|
4
|
+
const sections = [renderResult(data), renderPage(data), renderOutcome(data), ...renderFailure(data), renderInstance(data.instance), renderArtifacts(data)];
|
|
5
|
+
return sections.filter((section) => section).join('\n\n');
|
|
6
|
+
}
|
|
7
|
+
export function writeArtifacts(dir, snapshot) {
|
|
8
|
+
mkdirSync(dir, { recursive: true });
|
|
9
|
+
const paths = {
|
|
10
|
+
aria: path.resolve(dir, 'aria.yml'),
|
|
11
|
+
html: path.resolve(dir, 'page.html'),
|
|
12
|
+
network: path.resolve(dir, 'network.jsonl'),
|
|
13
|
+
};
|
|
14
|
+
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
15
|
+
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
16
|
+
writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
|
|
17
|
+
return paths;
|
|
18
|
+
}
|
|
19
|
+
function renderResult(data) {
|
|
20
|
+
const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
|
|
21
|
+
const healed = renderHealed(data);
|
|
22
|
+
if (healed)
|
|
23
|
+
lines.push(healed);
|
|
24
|
+
if (data.used?.length)
|
|
25
|
+
lines.push(`used: ${data.used.join('; ')}`);
|
|
26
|
+
return section('Result', lines.join('\n'));
|
|
27
|
+
}
|
|
28
|
+
function renderHealed(data) {
|
|
29
|
+
if (data.healed === undefined)
|
|
30
|
+
return null;
|
|
31
|
+
if (data.healNote)
|
|
32
|
+
return `healed: ${data.healed} (${data.healNote})`;
|
|
33
|
+
return `healed: ${data.healed}`;
|
|
34
|
+
}
|
|
35
|
+
function renderPage(data) {
|
|
36
|
+
const { url, previousUrl, title, state, visits } = data.page;
|
|
37
|
+
const urlLabel = `url: ${url}`;
|
|
38
|
+
const stateLabel = `state: ${state}`;
|
|
39
|
+
const width = Math.max(urlLabel.length, stateLabel.length) + 3;
|
|
40
|
+
let changedMarker = '';
|
|
41
|
+
if (previousUrl && previousUrl !== url)
|
|
42
|
+
changedMarker = `(changed: ${previousUrl} → ${url})`;
|
|
43
|
+
const lines = [align(urlLabel, changedMarker, width), `title: ${title}`, align(stateLabel, `(visit #${visits})`, width)];
|
|
44
|
+
return section('Page', lines.join('\n'));
|
|
45
|
+
}
|
|
46
|
+
function renderOutcome(data) {
|
|
47
|
+
if (data.changes)
|
|
48
|
+
return section('Changes', data.changes);
|
|
49
|
+
if (data.answer)
|
|
50
|
+
return section('Answer', data.answer);
|
|
51
|
+
if (data.research)
|
|
52
|
+
return section('Research', data.research);
|
|
53
|
+
if (!data.verdict)
|
|
54
|
+
return null;
|
|
55
|
+
const lines = [`passed: ${data.verdict.passed}`, `evidence: ${data.verdict.evidence}`, `code: ${data.verdict.code}`];
|
|
56
|
+
return section('Verdict', lines.join('\n'));
|
|
57
|
+
}
|
|
58
|
+
function renderFailure(data) {
|
|
59
|
+
if (!data.failure)
|
|
60
|
+
return [];
|
|
61
|
+
const lines = [`error: ${data.failure.error}`];
|
|
62
|
+
if (data.failure.reasoning)
|
|
63
|
+
lines.push(`reasoning: ${data.failure.reasoning}`);
|
|
64
|
+
return [section('Failure', lines.join('\n')), renderAttempts(data.failure.attempts), renderCompactAria(data.failure.compactAria)];
|
|
65
|
+
}
|
|
66
|
+
function renderAttempts(attempts) {
|
|
67
|
+
if (!attempts?.length)
|
|
68
|
+
return null;
|
|
69
|
+
const labels = attempts.map((attempt, index) => `${index + 1}. ${attempt.code}`);
|
|
70
|
+
const width = Math.max(...labels.map((label) => label.length)) + 3;
|
|
71
|
+
const lines = labels.map((label, index) => align(label, `→ ${attempts[index].outcome}`, width));
|
|
72
|
+
return section(`Healing attempts (${attempts.length})`, lines.join('\n'));
|
|
73
|
+
}
|
|
74
|
+
function renderCompactAria(compactAria) {
|
|
75
|
+
if (!compactAria)
|
|
76
|
+
return null;
|
|
77
|
+
return section('Current page (compact ARIA)', compactAria);
|
|
78
|
+
}
|
|
79
|
+
function renderInstance(instance) {
|
|
80
|
+
const others = instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`);
|
|
81
|
+
const lines = [`instance: ${instance.name} (${tabsLabel(instance.tabs)}) | other instances: ${otherInstances(others)}`, browserLine(instance)];
|
|
82
|
+
return section('Instance', lines.join('\n'));
|
|
83
|
+
}
|
|
84
|
+
function otherInstances(others) {
|
|
85
|
+
if (!others.length)
|
|
86
|
+
return 'none';
|
|
87
|
+
return others.join(', ');
|
|
88
|
+
}
|
|
89
|
+
function browserLine(instance) {
|
|
90
|
+
if (instance.attached)
|
|
91
|
+
return `browser: attached (${instance.attached})`;
|
|
92
|
+
if (instance.startedAgo)
|
|
93
|
+
return `browser: running, started ${instance.startedAgo} ago`;
|
|
94
|
+
if (instance.tabs > 0)
|
|
95
|
+
return 'browser: running';
|
|
96
|
+
return 'browser: not running';
|
|
97
|
+
}
|
|
98
|
+
function tabsLabel(tabs) {
|
|
99
|
+
if (tabs === 1)
|
|
100
|
+
return '1 tab';
|
|
101
|
+
return `${tabs} tabs`;
|
|
102
|
+
}
|
|
103
|
+
function renderArtifacts(data) {
|
|
104
|
+
if (!data.artifacts)
|
|
105
|
+
return null;
|
|
106
|
+
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`, `network: ${data.artifacts.network}`];
|
|
107
|
+
return section('Artifacts', lines.join('\n'));
|
|
108
|
+
}
|
|
109
|
+
function align(label, marker, width) {
|
|
110
|
+
if (!marker)
|
|
111
|
+
return label;
|
|
112
|
+
return `${label.padEnd(width)}${marker}`;
|
|
113
|
+
}
|
|
114
|
+
function section(title, body) {
|
|
115
|
+
return `### ${title}\n${body}`;
|
|
116
|
+
}
|