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
package/boat/prima/src/cli.ts
CHANGED
|
@@ -2,83 +2,81 @@ import { Command } from 'commander';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { keepServerRunning } from '../../../src/browser-server.ts';
|
|
4
4
|
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
5
|
-
import {
|
|
5
|
+
import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
|
|
6
|
+
import { clearActivityLine, trackActivityLine } from './activity-line.ts';
|
|
6
7
|
import { type EnvelopeData, renderEnvelope } from './envelope.ts';
|
|
7
8
|
import { Prima, type PrimaOptions } from './prima.ts';
|
|
8
9
|
|
|
9
10
|
const helpContract = dedent`
|
|
10
|
-
Prima
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
on the tabs it already has open; driving the same session from both tools is the
|
|
54
|
-
intended usage.
|
|
55
|
-
playwright-cli open <url> the session prima attaches to
|
|
56
|
-
--pw-session <title> which playwright-cli session, when several are open
|
|
57
|
-
--endpoint <ep> attach to a browser server endpoint directly
|
|
58
|
-
prima browser start a prima-owned browser instead, when no session is open
|
|
59
|
-
--instance <name> which prima-owned browser you talk to; parallel work
|
|
60
|
-
needs one each
|
|
61
|
-
--session [file] cookies and storage persisted across processes; ignored
|
|
62
|
-
while attached, the attached session keeps its own
|
|
63
|
-
Prima never launches a browser implicitly and never closes an attached one - it
|
|
64
|
-
disconnects. browser list shows both kinds; ### Instance names the one you are on.
|
|
65
|
-
Every browser is reached over a Playwright browser-server endpoint, which needs the
|
|
66
|
-
Node build - run prima as "npx explorbot prima ..." or through the published prima
|
|
67
|
-
bin; from source under Bun the connection does not open.
|
|
68
|
-
When no AI model is usable pw still works; for everything else drive
|
|
69
|
-
playwright-cli directly.
|
|
70
|
-
Parsed but not active yet: --framework, so reported code is CodeceptJS whatever
|
|
71
|
-
you pass.
|
|
11
|
+
Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
|
|
12
|
+
|
|
13
|
+
playwright-cli open <url> starts the session
|
|
14
|
+
prima <command> ... drives it
|
|
15
|
+
playwright-cli close ends it
|
|
16
|
+
|
|
17
|
+
One call takes a whole job:
|
|
18
|
+
|
|
19
|
+
prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
|
|
20
|
+
prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
|
|
21
|
+
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const checkHelp = dedent`
|
|
25
|
+
check takes an outcome rather than a click path, and works out how to reach it. It runs
|
|
26
|
+
on the page you are already on and never reloads it, so an open dialog survives the check.
|
|
27
|
+
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
28
|
+
scenario text is the single expected outcome. Each comes back under
|
|
29
|
+
### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
|
|
30
|
+
"not verified" means the run never checked it, which is not the same
|
|
31
|
+
as false.
|
|
32
|
+
Outcomes are settled against a screenshot of the whole page: what a user can see is
|
|
33
|
+
the proof, and the run log only says what was done. CONTRADICTION means the two
|
|
34
|
+
disagree - reported with both sides rather than settled one way, and ### Artifacts
|
|
35
|
+
then names the html, aria and screenshot on disk so you can judge it yourself. Not
|
|
36
|
+
finding something in the picture is not enough on its own; that is "not verified".
|
|
37
|
+
ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
|
|
38
|
+
could not complete, which is reported as such rather than as an app failure.
|
|
39
|
+
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const doHelp = dedent`
|
|
43
|
+
Each instruction is numbered and accounted for: ### Steps reports each as ok, FAIL or ??.
|
|
44
|
+
?? means the action ran but the run ended without confirming that instruction - read the
|
|
45
|
+
steps above it. Only FAIL and an instruction the page could not carry out fail the command.
|
|
46
|
+
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
47
|
+
what makes this tier cheap.
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const verifyHelp = dedent`
|
|
51
|
+
Reports each assertion it could express as PASSED or FAILED with its playwright form,
|
|
52
|
+
and gives no overall verdict - read the lines and decide. "none ran" means the claim
|
|
53
|
+
could not be expressed, which is not the same as false.
|
|
72
54
|
`;
|
|
73
55
|
|
|
74
|
-
|
|
56
|
+
const reportHelp = dedent`
|
|
57
|
+
Commands are logged as they run, so the report needs no browser and outlives the session.
|
|
58
|
+
The most recent session is reported unless --pw-session names another.
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const sessionHelp = dedent`
|
|
62
|
+
--endpoint <ep> attach to a browser server endpoint directly, skipping discovery
|
|
63
|
+
--instance <name> which prima-owned browser you talk to; parallel work needs one each
|
|
64
|
+
--session [file] cookies and storage persisted across processes; ignored while
|
|
65
|
+
attached, since the attached session keeps its own
|
|
66
|
+
--framework parsed but not active yet; reported code is CodeceptJS either way
|
|
67
|
+
DEBUG='explorbot:*' in front of a command prints the log of everything it does.
|
|
68
|
+
When no AI model is usable pw still works; for everything else drive playwright-cli.
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
let rootOptions: () => any = () => ({});
|
|
72
|
+
|
|
73
|
+
function buildOptions(subcommand: any): PrimaOptions {
|
|
74
|
+
const options = { ...rootOptions(), ...stripEmpty(subcommand) };
|
|
75
75
|
return {
|
|
76
|
-
verbose: options.verbose || options.debug,
|
|
77
76
|
config: options.config,
|
|
78
77
|
path: options.path,
|
|
79
78
|
instance: options.instance,
|
|
80
79
|
session: options.session,
|
|
81
|
-
heal: options.heal,
|
|
82
80
|
ephemeral: options.ephemeral,
|
|
83
81
|
framework: options.framework,
|
|
84
82
|
noVision: options.vision === false,
|
|
@@ -91,30 +89,39 @@ function buildOptions(options: any): PrimaOptions {
|
|
|
91
89
|
};
|
|
92
90
|
}
|
|
93
91
|
|
|
92
|
+
function stripEmpty(options: any): any {
|
|
93
|
+
const present: any = {};
|
|
94
|
+
for (const [key, value] of Object.entries(options || {})) {
|
|
95
|
+
if (value === undefined) continue;
|
|
96
|
+
present[key] = value;
|
|
97
|
+
}
|
|
98
|
+
return present;
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
function addCommonOptions(cmd: Command): Command {
|
|
95
102
|
return cmd
|
|
96
|
-
.option('-v, --verbose', 'Enable verbose logging')
|
|
97
|
-
.option('--debug', 'Enable debug logging (same as --verbose)')
|
|
98
103
|
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
99
104
|
.option('-p, --path <path>', 'Working directory path')
|
|
100
105
|
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
101
106
|
.option('--session [file]', 'Persist cookies and storage to a session file')
|
|
102
|
-
.option('--no-heal', 'Fail immediately instead of letting AI retry a failed action')
|
|
103
107
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
104
108
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
105
109
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
106
110
|
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
107
|
-
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
111
|
+
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
112
|
+
.addHelpText('after', `\n${sessionHelp}`);
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
function primaFor(options: any): Prima {
|
|
111
|
-
setPreserveConsoleLogs(true);
|
|
112
116
|
if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
113
117
|
return new Prima(buildOptions(options));
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData
|
|
120
|
+
async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>, record = true): Promise<void> {
|
|
121
|
+
setQuietMode(!isVerboseMode());
|
|
122
|
+
trackActivityLine();
|
|
117
123
|
const prima = primaFor(options);
|
|
124
|
+
const startedAt = Date.now();
|
|
118
125
|
|
|
119
126
|
let envelope: EnvelopeData;
|
|
120
127
|
try {
|
|
@@ -124,6 +131,8 @@ async function runPrima(options: any, command: string, run: (prima: Prima) => Pr
|
|
|
124
131
|
envelope = await prima.toolFailureEnvelope(command, error);
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
if (record) prima.record(envelope, Date.now() - startedAt);
|
|
135
|
+
clearActivityLine();
|
|
127
136
|
console.log(renderEnvelope(envelope));
|
|
128
137
|
await prima.stop().catch(() => {});
|
|
129
138
|
process.exit(envelope.ok ? 0 : 1);
|
|
@@ -143,32 +152,38 @@ async function runBrowser(options: any, run: (prima: Prima) => Promise<boolean>)
|
|
|
143
152
|
|
|
144
153
|
export function createPrimaCommands(name = 'prima'): Command {
|
|
145
154
|
const cmd = new Command(name);
|
|
146
|
-
cmd.description('
|
|
155
|
+
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');
|
|
156
|
+
cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
|
|
157
|
+
cmd.option('--url <url>', 'Page to open when the session has no page yet');
|
|
147
158
|
cmd.addHelpText('after', `\n${helpContract}`);
|
|
159
|
+
rootOptions = () => cmd.opts();
|
|
148
160
|
|
|
149
161
|
addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
|
|
150
162
|
await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
|
|
151
163
|
});
|
|
152
164
|
|
|
153
|
-
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await runPrima(options, `click ${target}`, (prima) => prima.click(target));
|
|
159
|
-
});
|
|
165
|
+
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
166
|
+
.addHelpText('after', `\n${doHelp}`)
|
|
167
|
+
.action(async (instructions, options) => {
|
|
168
|
+
await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
|
|
169
|
+
});
|
|
160
170
|
|
|
161
|
-
addCommonOptions(cmd.command('
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
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'))
|
|
172
|
+
.option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value: string, all: string[]) => [...all, value], [])
|
|
173
|
+
.addHelpText('after', `\n${checkHelp}`)
|
|
174
|
+
.action(async (scenario, options) => {
|
|
175
|
+
await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
|
|
176
|
+
});
|
|
164
177
|
|
|
165
178
|
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) => {
|
|
166
179
|
await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
|
|
167
180
|
});
|
|
168
181
|
|
|
169
|
-
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
170
|
-
|
|
171
|
-
|
|
182
|
+
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
183
|
+
.addHelpText('after', `\n${verifyHelp}`)
|
|
184
|
+
.action(async (assertion, options) => {
|
|
185
|
+
await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
|
|
186
|
+
});
|
|
172
187
|
|
|
173
188
|
addCommonOptions(
|
|
174
189
|
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')
|
|
@@ -181,6 +196,32 @@ export function createPrimaCommands(name = 'prima'): Command {
|
|
|
181
196
|
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
182
197
|
});
|
|
183
198
|
|
|
199
|
+
addCommonOptions(cmd.command('config').description('Show models, config file and paths used by this run'))
|
|
200
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
201
|
+
.action(async (options) => {
|
|
202
|
+
setQuietMode(!isVerboseMode());
|
|
203
|
+
const prima = primaFor(options);
|
|
204
|
+
console.log(await prima.config(options.json).catch((error: unknown) => browserErrorMessage(error)));
|
|
205
|
+
await prima.stop().catch(() => {});
|
|
206
|
+
process.exit(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
|
|
210
|
+
await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
|
|
214
|
+
.addHelpText('after', `\n${reportHelp}`)
|
|
215
|
+
.action(async (options) => {
|
|
216
|
+
setQuietMode(!isVerboseMode());
|
|
217
|
+
console.log(
|
|
218
|
+
await primaFor(options)
|
|
219
|
+
.report()
|
|
220
|
+
.catch((error: unknown) => browserErrorMessage(error))
|
|
221
|
+
);
|
|
222
|
+
process.exit(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
184
225
|
const browser = cmd.command('browser').description('Manage the browsers prima drives');
|
|
185
226
|
|
|
186
227
|
addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
const EXPECTATION_LABELS = {
|
|
5
|
+
passed: 'PASSED ',
|
|
6
|
+
failed: 'FAILED ',
|
|
7
|
+
unverified: 'not verified ',
|
|
8
|
+
contradiction: 'CONTRADICTION',
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
export interface InstanceInfo {
|
|
5
12
|
name: string;
|
|
6
13
|
tabs: number;
|
|
@@ -9,59 +16,58 @@ export interface InstanceInfo {
|
|
|
9
16
|
others: Array<{ name: string; tabs: number }>;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
export interface HealAttempt {
|
|
13
|
-
code: string;
|
|
14
|
-
outcome: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
19
|
export interface EnvelopeData {
|
|
18
20
|
ok: boolean;
|
|
19
21
|
command: string;
|
|
20
|
-
healed?: boolean;
|
|
21
|
-
healNote?: string;
|
|
22
22
|
used?: string[];
|
|
23
23
|
page: { url: string; previousUrl?: string; title: string; state: string; visits: number };
|
|
24
24
|
changes?: string | null;
|
|
25
|
+
steps?: Array<{ label: string; ok: boolean; unconfirmed?: boolean; proof: string }>;
|
|
26
|
+
expectations?: Array<{ text: string; status: 'passed' | 'failed' | 'unverified' | 'contradiction'; evidence?: string }>;
|
|
27
|
+
warning?: string;
|
|
28
|
+
stepFiles?: string;
|
|
29
|
+
value?: string;
|
|
25
30
|
answer?: string;
|
|
26
31
|
research?: string;
|
|
27
|
-
|
|
28
|
-
failure?: { error: string;
|
|
32
|
+
assertions?: Array<{ code: string; passed: boolean; proof: string[] }>;
|
|
33
|
+
failure?: { error: string; compactAria?: string };
|
|
29
34
|
instance: InstanceInfo;
|
|
30
|
-
|
|
35
|
+
status?: string;
|
|
36
|
+
artifacts?: { aria: string; html: string; screenshot?: string; network?: string };
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export function renderEnvelope(data: EnvelopeData): string {
|
|
34
|
-
const sections = [renderResult(data), renderPage(data), renderOutcome(data), ...renderFailure(data), renderInstance(data
|
|
40
|
+
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderWarning(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
35
41
|
return sections.filter((section) => section).join('\n\n');
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; requests: unknown[] }): { aria: string; html: string; network
|
|
44
|
+
export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; screenshot?: Buffer; requests: unknown[] }): { aria: string; html: string; screenshot?: string; network?: string } {
|
|
39
45
|
mkdirSync(dir, { recursive: true });
|
|
40
|
-
const paths = {
|
|
46
|
+
const paths: { aria: string; html: string; screenshot?: string; network?: string } = {
|
|
41
47
|
aria: path.resolve(dir, 'aria.yml'),
|
|
42
48
|
html: path.resolve(dir, 'page.html'),
|
|
43
|
-
network: path.resolve(dir, 'network.jsonl'),
|
|
44
49
|
};
|
|
45
50
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
46
51
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
52
|
+
|
|
53
|
+
if (snapshot.screenshot) {
|
|
54
|
+
paths.screenshot = path.resolve(dir, 'page.png');
|
|
55
|
+
writeFileSync(paths.screenshot, snapshot.screenshot);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!snapshot.requests.length) return paths;
|
|
59
|
+
|
|
60
|
+
paths.network = path.resolve(dir, 'network.jsonl');
|
|
47
61
|
writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
|
|
48
62
|
return paths;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
function renderResult(data: EnvelopeData): string {
|
|
52
66
|
const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
|
|
53
|
-
const healed = renderHealed(data);
|
|
54
|
-
if (healed) lines.push(healed);
|
|
55
67
|
if (data.used?.length) lines.push(`used: ${data.used.join('; ')}`);
|
|
56
68
|
return section('Result', lines.join('\n'));
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
function renderHealed(data: EnvelopeData): string | null {
|
|
60
|
-
if (data.healed === undefined) return null;
|
|
61
|
-
if (data.healNote) return `healed: ${data.healed} (${data.healNote})`;
|
|
62
|
-
return `healed: ${data.healed}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
71
|
function renderPage(data: EnvelopeData): string {
|
|
66
72
|
const { url, previousUrl, title, state, visits } = data.page;
|
|
67
73
|
const urlLabel = `url: ${url}`;
|
|
@@ -73,28 +79,73 @@ function renderPage(data: EnvelopeData): string {
|
|
|
73
79
|
return section('Page', lines.join('\n'));
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
function renderValue(data: EnvelopeData): string | null {
|
|
83
|
+
if (data.value === undefined) return null;
|
|
84
|
+
return section('Value', data.value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderChanges(data: EnvelopeData): string | null {
|
|
88
|
+
if (data.changes === undefined || data.changes === null) return null;
|
|
89
|
+
return section('Changes', data.changes);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderSteps(data: EnvelopeData): string | null {
|
|
93
|
+
if (!data.steps?.length) return null;
|
|
94
|
+
|
|
95
|
+
const lines: string[] = [];
|
|
96
|
+
data.steps.forEach((step, index) => {
|
|
97
|
+
let mark = 'FAIL';
|
|
98
|
+
if (step.ok) mark = 'ok ';
|
|
99
|
+
if (step.unconfirmed) mark = '?? ';
|
|
100
|
+
lines.push(`${index + 1}. ${mark} ${step.label}`);
|
|
101
|
+
for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
|
|
102
|
+
});
|
|
103
|
+
if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}`);
|
|
104
|
+
return section('Steps', lines.join('\n'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderExpectations(data: EnvelopeData): string | null {
|
|
108
|
+
if (!data.expectations?.length) return null;
|
|
109
|
+
|
|
110
|
+
const lines: string[] = [];
|
|
111
|
+
data.expectations.forEach((expectation, index) => {
|
|
112
|
+
lines.push(`${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
|
|
113
|
+
if (expectation.status !== 'contradiction' && expectation.status !== 'failed') return;
|
|
114
|
+
for (const line of (expectation.evidence || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
|
|
115
|
+
});
|
|
116
|
+
return section('Expected outcomes', lines.join('\n'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderWarning(data: EnvelopeData): string | null {
|
|
120
|
+
if (!data.warning) return null;
|
|
121
|
+
return section('Warning', data.warning);
|
|
122
|
+
}
|
|
123
|
+
|
|
76
124
|
function renderOutcome(data: EnvelopeData): string | null {
|
|
77
|
-
if (data.changes) return section('Changes', data.changes);
|
|
78
125
|
if (data.answer) return section('Answer', data.answer);
|
|
79
126
|
if (data.research) return section('Research', data.research);
|
|
80
|
-
if (!data.
|
|
81
|
-
|
|
82
|
-
return section('
|
|
127
|
+
if (!data.assertions) return null;
|
|
128
|
+
|
|
129
|
+
if (!data.assertions.length) return section('Assertions', 'none ran — no assertion could express this claim, so nothing was checked against the page');
|
|
130
|
+
|
|
131
|
+
const lines = data.assertions.map((assertion) => {
|
|
132
|
+
const code = assertion.code
|
|
133
|
+
.split('\n')
|
|
134
|
+
.map((line) => line.trim())
|
|
135
|
+
.filter((line) => line && !line.startsWith('//'))
|
|
136
|
+
.join(' ');
|
|
137
|
+
return `${code} => ${assertion.passed ? 'PASSED' : 'FAILED'}`;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const proof = data.assertions.flatMap((assertion) => assertion.proof);
|
|
141
|
+
if (proof.length) lines.push('', 'playwright:', ...proof);
|
|
142
|
+
|
|
143
|
+
return section('Assertions', lines.join('\n'));
|
|
83
144
|
}
|
|
84
145
|
|
|
85
146
|
function renderFailure(data: EnvelopeData): Array<string | null> {
|
|
86
147
|
if (!data.failure) return [];
|
|
87
|
-
|
|
88
|
-
if (data.failure.reasoning) lines.push(`reasoning: ${data.failure.reasoning}`);
|
|
89
|
-
return [section('Failure', lines.join('\n')), renderAttempts(data.failure.attempts), renderCompactAria(data.failure.compactAria)];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function renderAttempts(attempts: HealAttempt[]): string | null {
|
|
93
|
-
if (!attempts?.length) return null;
|
|
94
|
-
const labels = attempts.map((attempt, index) => `${index + 1}. ${attempt.code}`);
|
|
95
|
-
const width = Math.max(...labels.map((label) => label.length)) + 3;
|
|
96
|
-
const lines = labels.map((label, index) => align(label, `→ ${attempts[index].outcome}`, width));
|
|
97
|
-
return section(`Healing attempts (${attempts.length})`, lines.join('\n'));
|
|
148
|
+
return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
|
|
98
149
|
}
|
|
99
150
|
|
|
100
151
|
function renderCompactAria(compactAria?: string): string | null {
|
|
@@ -102,22 +153,19 @@ function renderCompactAria(compactAria?: string): string | null {
|
|
|
102
153
|
return section('Current page (compact ARIA)', compactAria);
|
|
103
154
|
}
|
|
104
155
|
|
|
105
|
-
function renderInstance(
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function otherInstances(others: string[]): string {
|
|
112
|
-
if (!others.length) return 'none';
|
|
113
|
-
return others.join(', ');
|
|
156
|
+
function renderInstance(data: EnvelopeData): string | null {
|
|
157
|
+
const instance = data.instance;
|
|
158
|
+
const parts = [`${instance.name} (${tabsLabel(instance.tabs)})`, browserLine(instance)];
|
|
159
|
+
if (instance.others.length) parts.push(`other instances: ${instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`).join(', ')}`);
|
|
160
|
+
if (data.status) parts.push(`details: prima status ${data.status}`);
|
|
161
|
+
return section('Instance', parts.join(' | '));
|
|
114
162
|
}
|
|
115
163
|
|
|
116
164
|
function browserLine(instance: InstanceInfo): string {
|
|
117
|
-
if (instance.attached) return `
|
|
118
|
-
if (instance.startedAgo) return `
|
|
119
|
-
if (instance.tabs > 0) return '
|
|
120
|
-
return '
|
|
165
|
+
if (instance.attached) return `attached to ${instance.attached}`;
|
|
166
|
+
if (instance.startedAgo) return `running, started ${instance.startedAgo} ago`;
|
|
167
|
+
if (instance.tabs > 0) return 'running';
|
|
168
|
+
return 'not running';
|
|
121
169
|
}
|
|
122
170
|
|
|
123
171
|
function tabsLabel(tabs: number): string {
|
|
@@ -125,9 +173,11 @@ function tabsLabel(tabs: number): string {
|
|
|
125
173
|
return `${tabs} tabs`;
|
|
126
174
|
}
|
|
127
175
|
|
|
128
|
-
function renderArtifacts(data: EnvelopeData): string | null {
|
|
176
|
+
export function renderArtifacts(data: EnvelopeData): string | null {
|
|
129
177
|
if (!data.artifacts) return null;
|
|
130
|
-
const lines = [`aria:
|
|
178
|
+
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
|
|
179
|
+
if (data.artifacts.screenshot) lines.push(`screenshot: ${data.artifacts.screenshot}`);
|
|
180
|
+
if (data.artifacts.network) lines.push(`network: ${data.artifacts.network}`);
|
|
131
181
|
return section('Artifacts', lines.join('\n'));
|
|
132
182
|
}
|
|
133
183
|
|