explorbot 0.2.3 → 0.2.5
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 +26 -8
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/api-tester/src/config.ts +4 -2
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +127 -86
- package/boat/prima/src/envelope.ts +102 -52
- package/boat/prima/src/prima.ts +567 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +26 -8
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +109 -77
- package/dist/boat/prima/src/envelope.js +94 -44
- package/dist/boat/prima/src/prima.js +533 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +2 -0
- package/dist/src/action.js +41 -2
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +34 -0
- package/dist/src/ai/navigator.js +237 -181
- package/dist/src/ai/pilot.d.ts +7 -0
- package/dist/src/ai/pilot.js +90 -2
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +14 -23
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher/cache.d.ts +2 -0
- package/dist/src/ai/researcher/cache.js +10 -2
- package/dist/src/ai/researcher.js +3 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +6 -3
- package/dist/src/ai/tester.js +50 -46
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +117 -37
- package/dist/src/commands/config-command.d.ts +51 -0
- package/dist/src/commands/config-command.js +117 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/config.d.ts +9 -1
- package/dist/src/config.js +53 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +7 -2
- package/dist/src/explorer.js +2 -3
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +55 -0
- package/dist/src/remote.js +235 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +26 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +7 -1
- package/dist/src/utils/logger.js +32 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/url-matcher.js +3 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +87 -0
- package/docs/reference/commands.md +997 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +43 -3
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +255 -186
- package/src/ai/pilot.ts +104 -2
- package/src/ai/provider.ts +14 -24
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher/cache.ts +12 -2
- package/src/ai/researcher.ts +3 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +52 -45
- package/src/ai/tools.ts +136 -37
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/config.ts +60 -5
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +7 -3
- package/src/explorer.ts +2 -2
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +244 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +6 -2
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +33 -2
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
|
@@ -2,81 +2,73 @@ import { Command } from 'commander';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { keepServerRunning } from "../../../src/browser-server.js";
|
|
4
4
|
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isVerboseMode, setQuietMode } from "../../../src/utils/logger.js";
|
|
6
|
+
import { clearActivityLine, trackActivityLine } from "./activity-line.js";
|
|
6
7
|
import { renderEnvelope } from "./envelope.js";
|
|
7
8
|
import { Prima } from "./prima.js";
|
|
8
9
|
const helpContract = dedent `
|
|
9
|
-
Prima
|
|
10
|
-
prints a plain-text envelope on stdout and exits 0 when ok, 1 when not.
|
|
10
|
+
Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
12
|
+
playwright-cli open <url> starts the session
|
|
13
|
+
prima <command> ... drives it
|
|
14
|
+
playwright-cli close ends it
|
|
23
15
|
|
|
24
|
-
|
|
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.
|
|
16
|
+
One call takes a whole job:
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
18
|
+
prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
|
|
19
|
+
prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
|
|
20
|
+
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
21
|
+
`;
|
|
22
|
+
const checkHelp = dedent `
|
|
23
|
+
check takes an outcome rather than a click path, and works out how to reach it. It runs
|
|
24
|
+
on the page you are already on and never reloads it, so an open dialog survives the check.
|
|
25
|
+
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
26
|
+
scenario text is the single expected outcome. Each comes back under
|
|
27
|
+
### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
|
|
28
|
+
"not verified" means the run never checked it, which is not the same
|
|
29
|
+
as false.
|
|
30
|
+
Outcomes are settled against a screenshot of the whole page: what a user can see is
|
|
31
|
+
the proof, and the run log only says what was done. CONTRADICTION means the two
|
|
32
|
+
disagree - reported with both sides rather than settled one way, and ### Artifacts
|
|
33
|
+
then names the html, aria and screenshot on disk so you can judge it yourself. Not
|
|
34
|
+
finding something in the picture is not enough on its own; that is "not verified".
|
|
35
|
+
ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
|
|
36
|
+
could not complete, which is reported as such rather than as an app failure.
|
|
37
|
+
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
38
|
+
`;
|
|
39
|
+
const doHelp = dedent `
|
|
40
|
+
Each instruction is numbered and accounted for: ### Steps reports each as ok, FAIL or ??.
|
|
41
|
+
?? means the action ran but the run ended without confirming that instruction - read the
|
|
42
|
+
steps above it. Only FAIL and an instruction the page could not carry out fail the command.
|
|
43
|
+
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
44
|
+
what makes this tier cheap.
|
|
71
45
|
`;
|
|
72
|
-
|
|
46
|
+
const verifyHelp = dedent `
|
|
47
|
+
Reports each assertion it could express as PASSED or FAILED with its playwright form,
|
|
48
|
+
and gives no overall verdict - read the lines and decide. "none ran" means the claim
|
|
49
|
+
could not be expressed, which is not the same as false.
|
|
50
|
+
`;
|
|
51
|
+
const reportHelp = dedent `
|
|
52
|
+
Commands are logged as they run, so the report needs no browser and outlives the session.
|
|
53
|
+
The most recent session is reported unless --pw-session names another.
|
|
54
|
+
`;
|
|
55
|
+
const sessionHelp = dedent `
|
|
56
|
+
--endpoint <ep> attach to a browser server endpoint directly, skipping discovery
|
|
57
|
+
--instance <name> which prima-owned browser you talk to; parallel work needs one each
|
|
58
|
+
--session [file] cookies and storage persisted across processes; ignored while
|
|
59
|
+
attached, since the attached session keeps its own
|
|
60
|
+
--framework parsed but not active yet; reported code is CodeceptJS either way
|
|
61
|
+
DEBUG='explorbot:*' in front of a command prints the log of everything it does.
|
|
62
|
+
When no AI model is usable pw still works; for everything else drive playwright-cli.
|
|
63
|
+
`;
|
|
64
|
+
let rootOptions = () => ({});
|
|
65
|
+
function buildOptions(subcommand) {
|
|
66
|
+
const options = { ...rootOptions(), ...stripEmpty(subcommand) };
|
|
73
67
|
return {
|
|
74
|
-
verbose: options.verbose || options.debug,
|
|
75
68
|
config: options.config,
|
|
76
69
|
path: options.path,
|
|
77
70
|
instance: options.instance,
|
|
78
71
|
session: options.session,
|
|
79
|
-
heal: options.heal,
|
|
80
72
|
ephemeral: options.ephemeral,
|
|
81
73
|
framework: options.framework,
|
|
82
74
|
noVision: options.vision === false,
|
|
@@ -88,29 +80,38 @@ function buildOptions(options) {
|
|
|
88
80
|
pwSession: options.pwSession,
|
|
89
81
|
};
|
|
90
82
|
}
|
|
83
|
+
function stripEmpty(options) {
|
|
84
|
+
const present = {};
|
|
85
|
+
for (const [key, value] of Object.entries(options || {})) {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
continue;
|
|
88
|
+
present[key] = value;
|
|
89
|
+
}
|
|
90
|
+
return present;
|
|
91
|
+
}
|
|
91
92
|
function addCommonOptions(cmd) {
|
|
92
93
|
return cmd
|
|
93
|
-
.option('-v, --verbose', 'Enable verbose logging')
|
|
94
|
-
.option('--debug', 'Enable debug logging (same as --verbose)')
|
|
95
94
|
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
96
95
|
.option('-p, --path <path>', 'Working directory path')
|
|
97
96
|
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
98
97
|
.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
98
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
101
99
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
102
100
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
103
101
|
.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')
|
|
102
|
+
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
103
|
+
.addHelpText('after', `\n${sessionHelp}`);
|
|
105
104
|
}
|
|
106
105
|
function primaFor(options) {
|
|
107
|
-
setPreserveConsoleLogs(true);
|
|
108
106
|
if (options.ephemeral)
|
|
109
107
|
process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
110
108
|
return new Prima(buildOptions(options));
|
|
111
109
|
}
|
|
112
|
-
async function runPrima(options, command, run) {
|
|
110
|
+
async function runPrima(options, command, run, record = true) {
|
|
111
|
+
setQuietMode(!isVerboseMode());
|
|
112
|
+
trackActivityLine();
|
|
113
113
|
const prima = primaFor(options);
|
|
114
|
+
const startedAt = Date.now();
|
|
114
115
|
let envelope;
|
|
115
116
|
try {
|
|
116
117
|
await prima.start();
|
|
@@ -119,6 +120,9 @@ async function runPrima(options, command, run) {
|
|
|
119
120
|
catch (error) {
|
|
120
121
|
envelope = await prima.toolFailureEnvelope(command, error);
|
|
121
122
|
}
|
|
123
|
+
if (record)
|
|
124
|
+
prima.record(envelope, Date.now() - startedAt);
|
|
125
|
+
clearActivityLine();
|
|
122
126
|
console.log(renderEnvelope(envelope));
|
|
123
127
|
await prima.stop().catch(() => { });
|
|
124
128
|
process.exit(envelope.ok ? 0 : 1);
|
|
@@ -136,24 +140,31 @@ async function runBrowser(options, run) {
|
|
|
136
140
|
}
|
|
137
141
|
export function createPrimaCommands(name = 'prima') {
|
|
138
142
|
const cmd = new Command(name);
|
|
139
|
-
cmd.description('
|
|
143
|
+
cmd.description('Tests and drives a web app through described behaviour instead of locators: one command carries a whole scenario, verifies it, and reports the proof');
|
|
144
|
+
cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
|
|
145
|
+
cmd.option('--url <url>', 'Page to open when the session has no page yet');
|
|
140
146
|
cmd.addHelpText('after', `\n${helpContract}`);
|
|
147
|
+
rootOptions = () => cmd.opts();
|
|
141
148
|
addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
|
|
142
149
|
await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
|
|
143
150
|
});
|
|
144
|
-
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
151
|
+
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
152
|
+
.addHelpText('after', `\n${doHelp}`)
|
|
153
|
+
.action(async (instructions, options) => {
|
|
145
154
|
await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
|
|
146
155
|
});
|
|
147
|
-
addCommonOptions(cmd.command('
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await runPrima(options, `
|
|
156
|
+
addCommonOptions(cmd.command('check <scenario>').description('Run a scenario end to end as a test, with its own verification, and report the steps it took'))
|
|
157
|
+
.option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value, all) => [...all, value], [])
|
|
158
|
+
.addHelpText('after', `\n${checkHelp}`)
|
|
159
|
+
.action(async (scenario, options) => {
|
|
160
|
+
await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
|
|
152
161
|
});
|
|
153
162
|
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
163
|
await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
|
|
155
164
|
});
|
|
156
|
-
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
165
|
+
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
166
|
+
.addHelpText('after', `\n${verifyHelp}`)
|
|
167
|
+
.action(async (assertion, options) => {
|
|
157
168
|
await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
|
|
158
169
|
});
|
|
159
170
|
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) => {
|
|
@@ -164,6 +175,27 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
164
175
|
options.baseUrl = target;
|
|
165
176
|
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
166
177
|
});
|
|
178
|
+
addCommonOptions(cmd.command('config').description('Show models, config file and paths used by this run'))
|
|
179
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
180
|
+
.action(async (options) => {
|
|
181
|
+
setQuietMode(!isVerboseMode());
|
|
182
|
+
const prima = primaFor(options);
|
|
183
|
+
console.log(await prima.config(options.json).catch((error) => browserErrorMessage(error)));
|
|
184
|
+
await prima.stop().catch(() => { });
|
|
185
|
+
process.exit(0);
|
|
186
|
+
});
|
|
187
|
+
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
|
|
188
|
+
await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
|
|
189
|
+
});
|
|
190
|
+
addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
|
|
191
|
+
.addHelpText('after', `\n${reportHelp}`)
|
|
192
|
+
.action(async (options) => {
|
|
193
|
+
setQuietMode(!isVerboseMode());
|
|
194
|
+
console.log(await primaFor(options)
|
|
195
|
+
.report()
|
|
196
|
+
.catch((error) => browserErrorMessage(error)));
|
|
197
|
+
process.exit(0);
|
|
198
|
+
});
|
|
167
199
|
const browser = cmd.command('browser').description('Manage the browsers prima drives');
|
|
168
200
|
addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
|
|
169
201
|
.option('-s, --show', 'Launch the browser in a visible window')
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
const EXPECTATION_LABELS = {
|
|
4
|
+
passed: 'PASSED ',
|
|
5
|
+
failed: 'FAILED ',
|
|
6
|
+
unverified: 'not verified ',
|
|
7
|
+
contradiction: 'CONTRADICTION',
|
|
8
|
+
};
|
|
3
9
|
export function renderEnvelope(data) {
|
|
4
|
-
const sections = [renderResult(data), renderPage(data), renderOutcome(data), ...renderFailure(data), renderInstance(data
|
|
10
|
+
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderWarning(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
5
11
|
return sections.filter((section) => section).join('\n\n');
|
|
6
12
|
}
|
|
7
13
|
export function writeArtifacts(dir, snapshot) {
|
|
@@ -9,29 +15,25 @@ export function writeArtifacts(dir, snapshot) {
|
|
|
9
15
|
const paths = {
|
|
10
16
|
aria: path.resolve(dir, 'aria.yml'),
|
|
11
17
|
html: path.resolve(dir, 'page.html'),
|
|
12
|
-
network: path.resolve(dir, 'network.jsonl'),
|
|
13
18
|
};
|
|
14
19
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
15
20
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
21
|
+
if (snapshot.screenshot) {
|
|
22
|
+
paths.screenshot = path.resolve(dir, 'page.png');
|
|
23
|
+
writeFileSync(paths.screenshot, snapshot.screenshot);
|
|
24
|
+
}
|
|
25
|
+
if (!snapshot.requests.length)
|
|
26
|
+
return paths;
|
|
27
|
+
paths.network = path.resolve(dir, 'network.jsonl');
|
|
16
28
|
writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
|
|
17
29
|
return paths;
|
|
18
30
|
}
|
|
19
31
|
function renderResult(data) {
|
|
20
32
|
const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
|
|
21
|
-
const healed = renderHealed(data);
|
|
22
|
-
if (healed)
|
|
23
|
-
lines.push(healed);
|
|
24
33
|
if (data.used?.length)
|
|
25
34
|
lines.push(`used: ${data.used.join('; ')}`);
|
|
26
35
|
return section('Result', lines.join('\n'));
|
|
27
36
|
}
|
|
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
37
|
function renderPage(data) {
|
|
36
38
|
const { url, previousUrl, title, state, visits } = data.page;
|
|
37
39
|
const urlLabel = `url: ${url}`;
|
|
@@ -43,67 +45,115 @@ function renderPage(data) {
|
|
|
43
45
|
const lines = [align(urlLabel, changedMarker, width), `title: ${title}`, align(stateLabel, `(visit #${visits})`, width)];
|
|
44
46
|
return section('Page', lines.join('\n'));
|
|
45
47
|
}
|
|
48
|
+
function renderValue(data) {
|
|
49
|
+
if (data.value === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
return section('Value', data.value);
|
|
52
|
+
}
|
|
53
|
+
function renderChanges(data) {
|
|
54
|
+
if (data.changes === undefined || data.changes === null)
|
|
55
|
+
return null;
|
|
56
|
+
return section('Changes', data.changes);
|
|
57
|
+
}
|
|
58
|
+
function renderSteps(data) {
|
|
59
|
+
if (!data.steps?.length)
|
|
60
|
+
return null;
|
|
61
|
+
const lines = [];
|
|
62
|
+
data.steps.forEach((step, index) => {
|
|
63
|
+
let mark = 'FAIL';
|
|
64
|
+
if (step.ok)
|
|
65
|
+
mark = 'ok ';
|
|
66
|
+
if (step.unconfirmed)
|
|
67
|
+
mark = '?? ';
|
|
68
|
+
lines.push(`${index + 1}. ${mark} ${step.label}`);
|
|
69
|
+
for (const line of (step.proof || '').split('\n').filter(Boolean))
|
|
70
|
+
lines.push(` ${line}`);
|
|
71
|
+
});
|
|
72
|
+
if (data.stepFiles)
|
|
73
|
+
lines.push('', `page after each step: ${data.stepFiles}`);
|
|
74
|
+
return section('Steps', lines.join('\n'));
|
|
75
|
+
}
|
|
76
|
+
function renderExpectations(data) {
|
|
77
|
+
if (!data.expectations?.length)
|
|
78
|
+
return null;
|
|
79
|
+
const lines = [];
|
|
80
|
+
data.expectations.forEach((expectation, index) => {
|
|
81
|
+
lines.push(`${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
|
|
82
|
+
if (expectation.status !== 'contradiction' && expectation.status !== 'failed')
|
|
83
|
+
return;
|
|
84
|
+
for (const line of (expectation.evidence || '').split('\n').filter(Boolean))
|
|
85
|
+
lines.push(` ${line}`);
|
|
86
|
+
});
|
|
87
|
+
return section('Expected outcomes', lines.join('\n'));
|
|
88
|
+
}
|
|
89
|
+
function renderWarning(data) {
|
|
90
|
+
if (!data.warning)
|
|
91
|
+
return null;
|
|
92
|
+
return section('Warning', data.warning);
|
|
93
|
+
}
|
|
46
94
|
function renderOutcome(data) {
|
|
47
|
-
if (data.changes)
|
|
48
|
-
return section('Changes', data.changes);
|
|
49
95
|
if (data.answer)
|
|
50
96
|
return section('Answer', data.answer);
|
|
51
97
|
if (data.research)
|
|
52
98
|
return section('Research', data.research);
|
|
53
|
-
if (!data.
|
|
99
|
+
if (!data.assertions)
|
|
54
100
|
return null;
|
|
55
|
-
|
|
56
|
-
|
|
101
|
+
if (!data.assertions.length)
|
|
102
|
+
return section('Assertions', 'none ran — no assertion could express this claim, so nothing was checked against the page');
|
|
103
|
+
const lines = data.assertions.map((assertion) => {
|
|
104
|
+
const code = assertion.code
|
|
105
|
+
.split('\n')
|
|
106
|
+
.map((line) => line.trim())
|
|
107
|
+
.filter((line) => line && !line.startsWith('//'))
|
|
108
|
+
.join(' ');
|
|
109
|
+
return `${code} => ${assertion.passed ? 'PASSED' : 'FAILED'}`;
|
|
110
|
+
});
|
|
111
|
+
const proof = data.assertions.flatMap((assertion) => assertion.proof);
|
|
112
|
+
if (proof.length)
|
|
113
|
+
lines.push('', 'playwright:', ...proof);
|
|
114
|
+
return section('Assertions', lines.join('\n'));
|
|
57
115
|
}
|
|
58
116
|
function renderFailure(data) {
|
|
59
117
|
if (!data.failure)
|
|
60
118
|
return [];
|
|
61
|
-
|
|
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'));
|
|
119
|
+
return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
|
|
73
120
|
}
|
|
74
121
|
function renderCompactAria(compactAria) {
|
|
75
122
|
if (!compactAria)
|
|
76
123
|
return null;
|
|
77
124
|
return section('Current page (compact ARIA)', compactAria);
|
|
78
125
|
}
|
|
79
|
-
function renderInstance(
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return others.join(', ');
|
|
126
|
+
function renderInstance(data) {
|
|
127
|
+
const instance = data.instance;
|
|
128
|
+
const parts = [`${instance.name} (${tabsLabel(instance.tabs)})`, browserLine(instance)];
|
|
129
|
+
if (instance.others.length)
|
|
130
|
+
parts.push(`other instances: ${instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`).join(', ')}`);
|
|
131
|
+
if (data.status)
|
|
132
|
+
parts.push(`details: prima status ${data.status}`);
|
|
133
|
+
return section('Instance', parts.join(' | '));
|
|
88
134
|
}
|
|
89
135
|
function browserLine(instance) {
|
|
90
136
|
if (instance.attached)
|
|
91
|
-
return `
|
|
137
|
+
return `attached to ${instance.attached}`;
|
|
92
138
|
if (instance.startedAgo)
|
|
93
|
-
return `
|
|
139
|
+
return `running, started ${instance.startedAgo} ago`;
|
|
94
140
|
if (instance.tabs > 0)
|
|
95
|
-
return '
|
|
96
|
-
return '
|
|
141
|
+
return 'running';
|
|
142
|
+
return 'not running';
|
|
97
143
|
}
|
|
98
144
|
function tabsLabel(tabs) {
|
|
99
145
|
if (tabs === 1)
|
|
100
146
|
return '1 tab';
|
|
101
147
|
return `${tabs} tabs`;
|
|
102
148
|
}
|
|
103
|
-
function renderArtifacts(data) {
|
|
149
|
+
export function renderArtifacts(data) {
|
|
104
150
|
if (!data.artifacts)
|
|
105
151
|
return null;
|
|
106
|
-
const lines = [`aria:
|
|
152
|
+
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
|
|
153
|
+
if (data.artifacts.screenshot)
|
|
154
|
+
lines.push(`screenshot: ${data.artifacts.screenshot}`);
|
|
155
|
+
if (data.artifacts.network)
|
|
156
|
+
lines.push(`network: ${data.artifacts.network}`);
|
|
107
157
|
return section('Artifacts', lines.join('\n'));
|
|
108
158
|
}
|
|
109
159
|
function align(label, marker, width) {
|