explorbot 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/explorbot-cli.ts +19 -7
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/prima/README.md +96 -0
- package/boat/prima/package.json +14 -10
- package/boat/prima/src/cli.ts +29 -12
- package/boat/prima/src/envelope.ts +35 -13
- package/boat/prima/src/prima.ts +78 -45
- package/dist/bin/explorbot-cli.js +19 -7
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/prima/src/cli.js +26 -7
- package/dist/boat/prima/src/envelope.js +32 -8
- package/dist/boat/prima/src/prima.js +75 -43
- package/dist/models.json +4 -4
- package/dist/package.json +6 -2
- package/dist/src/action-result.d.ts +13 -0
- package/dist/src/action-result.js +46 -15
- package/dist/src/action.d.ts +5 -2
- package/dist/src/action.js +53 -18
- package/dist/src/ai/captain/web-mode.js +1 -2
- package/dist/src/ai/captain.d.ts +20 -0
- package/dist/src/ai/captain.js +10 -1
- package/dist/src/ai/driller.js +6 -2
- package/dist/src/ai/fisherman-tools.d.ts +40 -1
- package/dist/src/ai/fisherman-tools.js +39 -0
- package/dist/src/ai/fisherman.js +2 -1
- package/dist/src/ai/navigator.d.ts +28 -0
- package/dist/src/ai/navigator.js +223 -175
- package/dist/src/ai/pilot.d.ts +7 -4
- package/dist/src/ai/pilot.js +89 -30
- package/dist/src/ai/planner/subpages.js +2 -16
- package/dist/src/ai/planner.js +1 -1
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +28 -22
- package/dist/src/ai/researcher/cache.d.ts +10 -3
- package/dist/src/ai/researcher/cache.js +23 -10
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
- package/dist/src/ai/researcher.js +6 -4
- package/dist/src/ai/rules.js +1 -5
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/tester.d.ts +6 -3
- package/dist/src/ai/tester.js +30 -35
- package/dist/src/ai/tools.d.ts +8 -5
- package/dist/src/ai/tools.js +83 -57
- 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/commands/init-command.js +13 -20
- package/dist/src/config.d.ts +8 -1
- package/dist/src/config.js +43 -1
- package/dist/src/experience-tracker.d.ts +2 -0
- package/dist/src/experience-tracker.js +12 -0
- package/dist/src/explorbot.js +5 -2
- package/dist/src/playwright-recorder.js +6 -12
- package/dist/src/remote.d.ts +3 -2
- package/dist/src/remote.js +8 -2
- package/dist/src/state-manager.d.ts +1 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/test-plan.d.ts +9 -0
- package/dist/src/test-plan.js +30 -0
- package/dist/src/utils/html-diff.d.ts +5 -0
- package/dist/src/utils/html-diff.js +65 -6
- package/dist/src/utils/logger.d.ts +1 -1
- package/dist/src/utils/logger.js +8 -0
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +32 -0
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +31 -2
- package/docs/basics/getting-started.md +33 -10
- package/docs/basics/providers.md +6 -4
- package/docs/contributing/npm-package.md +73 -4
- package/docs/index.json +2 -1
- package/docs/reference/commands.md +3 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/models.json +4 -4
- package/package.json +6 -2
- package/src/action-result.ts +61 -16
- package/src/action.ts +56 -18
- package/src/ai/captain/web-mode.ts +1 -2
- package/src/ai/captain.ts +9 -1
- package/src/ai/driller.ts +6 -2
- package/src/ai/fisherman-tools.ts +35 -0
- package/src/ai/fisherman.ts +2 -1
- package/src/ai/navigator.ts +238 -179
- package/src/ai/pilot.ts +104 -36
- package/src/ai/planner/subpages.ts +2 -13
- package/src/ai/planner.ts +1 -1
- package/src/ai/provider.ts +29 -21
- package/src/ai/researcher/cache.ts +29 -11
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +23 -5
- package/src/ai/researcher.ts +6 -4
- package/src/ai/rules.ts +1 -5
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/tester.ts +33 -34
- package/src/ai/tools.ts +88 -61
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +14 -20
- package/src/config.ts +47 -2
- package/src/experience-tracker.ts +13 -0
- package/src/explorbot.ts +4 -2
- package/src/playwright-recorder.ts +6 -11
- package/src/remote.ts +8 -2
- package/src/state-manager.ts +5 -2
- package/src/test-plan.ts +38 -0
- package/src/utils/html-diff.ts +72 -7
- package/src/utils/logger.ts +9 -1
- package/src/utils/strings.ts +36 -0
- package/src/utils/url-matcher.ts +27 -2
package/bin/explorbot-cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { remote } from '../src/remote.js';
|
|
|
15
15
|
import { Stats } from '../src/stats.js';
|
|
16
16
|
import { Plan } from '../src/test-plan.js';
|
|
17
17
|
import { getCliName } from '../src/utils/cli-name.ts';
|
|
18
|
-
import { log, setPreserveConsoleLogs } from '../src/utils/logger.js';
|
|
18
|
+
import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode } from '../src/utils/logger.js';
|
|
19
19
|
import { jsonToTable } from '../src/utils/markdown-parser.js';
|
|
20
20
|
import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
|
|
21
21
|
import { type NextStepSection, printNextSteps, relativeToCwd } from '../src/utils/next-steps.ts';
|
|
@@ -436,6 +436,23 @@ addCommonOptions(
|
|
|
436
436
|
await cmd.execute(args);
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
+
program
|
|
440
|
+
.command('config [url]')
|
|
441
|
+
.description('Show models, config file and paths used by this run')
|
|
442
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
443
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
444
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
445
|
+
.action(async (url, options) => {
|
|
446
|
+
setQuietMode(!isVerboseMode());
|
|
447
|
+
const { ConfigCommand } = await import('../src/commands/config-command.js');
|
|
448
|
+
try {
|
|
449
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
439
456
|
program
|
|
440
457
|
.command('init')
|
|
441
458
|
.description('Initialize configuration for a project or for this machine')
|
|
@@ -912,11 +929,6 @@ ${rows}
|
|
|
912
929
|
`;
|
|
913
930
|
};
|
|
914
931
|
|
|
915
|
-
|
|
916
|
-
cmd.addHelpText('after', envHelp);
|
|
917
|
-
for (const sub of cmd.commands) addEnvHelp(sub);
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
addEnvHelp(program);
|
|
932
|
+
program.addHelpText('after', envHelp);
|
|
921
933
|
|
|
922
934
|
program.parse();
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
+
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
5
|
+
import { listSites } from '../../../src/global-config.ts';
|
|
4
6
|
import { setPreserveConsoleLogs } from '../../../src/utils/logger.ts';
|
|
5
7
|
import { getStyles } from './ai/chief/styles.ts';
|
|
6
8
|
import { ApiBot, type ApibotOptions } from './apibot.ts';
|
|
9
|
+
import { ApibotConfigParser } from './config.ts';
|
|
7
10
|
|
|
8
11
|
function buildOptions(options: any): ApibotOptions {
|
|
9
12
|
return {
|
|
@@ -82,6 +85,20 @@ export function createApiCommands(name = 'api'): Command {
|
|
|
82
85
|
}
|
|
83
86
|
});
|
|
84
87
|
|
|
88
|
+
addCommonOptions(cmd.command('config [endpoint]').description('Show models, config file and paths used by this run'))
|
|
89
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
90
|
+
.action(async (endpoint, options) => {
|
|
91
|
+
const parser = ApibotConfigParser.getInstance();
|
|
92
|
+
const [site] = listSites();
|
|
93
|
+
try {
|
|
94
|
+
const config = await parser.loadConfig({ config: options.config, path: options.path, endpoint: endpoint || site?.url });
|
|
95
|
+
console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
85
102
|
addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
|
|
86
103
|
setPreserveConsoleLogs(true);
|
|
87
104
|
try {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import {
|
|
4
|
+
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
5
|
+
import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from '../../../src/utils/logger.ts';
|
|
5
6
|
import { DocBot, type DocbotOptions } from './docbot.ts';
|
|
6
7
|
|
|
7
8
|
function buildOptions(options: any): DocbotOptions {
|
|
@@ -65,6 +66,18 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
addCommonOptions(cmd.command('config [url]').description('Show models, config file and paths used by this run'))
|
|
70
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
71
|
+
.action(async (url, options) => {
|
|
72
|
+
setQuietMode(!isVerboseMode());
|
|
73
|
+
try {
|
|
74
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
68
81
|
cmd
|
|
69
82
|
.command('init')
|
|
70
83
|
.description('Initialize doc collector configuration')
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# prima-cli
|
|
2
|
+
|
|
3
|
+
Prima is a high-level AI browser driver. You describe the behaviour you want checked, and it works out how to reach it — instead of writing selectors and step-by-step click paths.
|
|
4
|
+
|
|
5
|
+
It drives a browser opened by [playwright-cli](https://www.npmjs.com/package/playwright-cli), or one of its own.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx prima-cli check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx prima-cli --help # no install
|
|
15
|
+
npm install -g prima-cli # or install it
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Prima also ships inside [explorbot](https://www.npmjs.com/package/explorbot), so `npx explorbot prima <command>` runs the same tool if you already have it.
|
|
19
|
+
|
|
20
|
+
Requires Node.js 24+. Playwright browsers come from `npx playwright install chromium`.
|
|
21
|
+
|
|
22
|
+
## Session
|
|
23
|
+
|
|
24
|
+
Prima needs a browser to drive. Either attach to a `playwright-cli` session:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
playwright-cli open https://app.example.com
|
|
28
|
+
prima-cli check "the settings page saves a changed theme"
|
|
29
|
+
playwright-cli close
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or let prima own the browser:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
prima-cli browser start --url https://app.example.com
|
|
36
|
+
prima-cli do "open the account menu" "choose the settings entry"
|
|
37
|
+
prima-cli browser stop
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | What it does |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `check <scenario>` | Run a scenario end to end as a test, verify it, and report the steps it took |
|
|
45
|
+
| `do <instructions...>` | Run high-level instructions tester-style, one argument per instruction |
|
|
46
|
+
| `verify <assertion>` | Assert a statement about the current page |
|
|
47
|
+
| `ask <question>` | Answer a question about the current page |
|
|
48
|
+
| `research` | Map the current page and return verified locators |
|
|
49
|
+
| `go <target>` | Navigate to a url, a path, or a page described in plain words |
|
|
50
|
+
| `pw <fn>` | Run a Playwright function expression against the open page |
|
|
51
|
+
| `status <hash>` | Show the artifacts and page detail recorded for an earlier command |
|
|
52
|
+
| `report` | Turn every command of a session into one html and markdown report |
|
|
53
|
+
| `browser start\|stop\|status\|list` | Manage the browsers prima drives |
|
|
54
|
+
| `config` | Show models, config file and paths used by this run |
|
|
55
|
+
|
|
56
|
+
`check` takes an outcome rather than a click path. It runs on the page you are already on and never reloads it, so an open dialog survives the check. Each `--expected` outcome comes back as PASSED, FAILED, CONTRADICTION or not verified — settled against a screenshot of the whole page, because what a user can see is the proof.
|
|
57
|
+
|
|
58
|
+
`do` accounts for every instruction you gave: each is reported as ok, FAIL or ??. Nothing runs past the last instruction.
|
|
59
|
+
|
|
60
|
+
## AI model
|
|
61
|
+
|
|
62
|
+
Prima needs a model, and takes it from the environment. There is no `init` to run and nothing is written for you.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export PRIMA_CLI_AI_MODEL=openrouter/openai/gpt-oss-120b
|
|
66
|
+
export OPENROUTER_API_KEY=your-key
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The provider comes from the model name, so that is the whole setup. Fish uses `set -gx` instead of `export`. Since prima is usually run by a coding agent, the better home is the agent's own config — in `~/.claude/settings.json`, an `env` block reaches every command the agent runs:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"env": {
|
|
74
|
+
"PRIMA_CLI_AI_MODEL": "openrouter/openai/gpt-oss-120b",
|
|
75
|
+
"OPENROUTER_API_KEY": "your-key"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Screenshot analysis needs its own model — one is never guessed from the main model. Set `PRIMA_CLI_VISION_MODEL`, or pass `--vision-model` for a single run, as `--model` does for the main one. Without it prima still runs, but settles `check` outcomes from the run log rather than from the page as seen.
|
|
81
|
+
|
|
82
|
+
Every `PRIMA_CLI_*` variable mirrors the `EXPLORBOT_*` one of the same name and wins over it, so prima can be pointed at a different provider, model or URL without disturbing an explorbot setup on the same machine. `PRIMA_CLI_AI_MODEL`, `PRIMA_CLI_URL`, `PRIMA_CLI_VISION_MODEL` and the rest all work this way.
|
|
83
|
+
|
|
84
|
+
Where [explorbot](https://www.npmjs.com/package/explorbot) is already configured, prima reads its config too — a project's `explorbot.config.js` first, then `~/.explorbot/config.js`. Variables win over both. `prima config` prints what a directory resolves to, and `--ephemeral` keeps no state between runs, writing output to a temp directory.
|
|
85
|
+
|
|
86
|
+
Providers: openai, anthropic, google, groq, mistral, openrouter, sambanova.
|
|
87
|
+
|
|
88
|
+
Without a model `pw` still works, since it runs Playwright directly.
|
|
89
|
+
|
|
90
|
+
## Agents
|
|
91
|
+
|
|
92
|
+
Prima is built to be called by a coding agent: one call takes a whole job, and the reply is a compact report rather than a browser transcript. Point your agent at `prima-cli --help` and it will find its way.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
Elastic-2.0. Part of [explorbot](https://github.com/testomatio/explorbot).
|
package/boat/prima/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "prima",
|
|
3
|
-
"version": "
|
|
2
|
+
"name": "prima-cli",
|
|
3
|
+
"version": "0.0.0",
|
|
4
4
|
"description": "High-level browser driver CLI for orchestrating agents",
|
|
5
|
+
"license": "Elastic-2.0",
|
|
5
6
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
"format": "biome format --write .",
|
|
9
|
-
"lint:fix": "biome lint --write .",
|
|
10
|
-
"check:fix": "biome check --write ."
|
|
7
|
+
"bin": {
|
|
8
|
+
"prima-cli": "./dist/boat/prima/bin/prima-cli.js"
|
|
11
9
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
"files": ["dist/"],
|
|
11
|
+
"keywords": ["cli", "playwright", "browser", "ai", "agent", "testing"],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/testomatio/explorbot",
|
|
15
|
+
"directory": "boat/prima"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=24.0.0"
|
|
15
19
|
}
|
|
16
20
|
}
|
package/boat/prima/src/cli.ts
CHANGED
|
@@ -22,17 +22,27 @@ const helpContract = dedent`
|
|
|
22
22
|
`;
|
|
23
23
|
|
|
24
24
|
const checkHelp = dedent`
|
|
25
|
-
check takes an outcome rather than a click path, and works out how to reach it.
|
|
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.
|
|
26
27
|
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
27
28
|
scenario text is the single expected outcome. Each comes back under
|
|
28
|
-
### Expected outcomes as PASSED, FAILED or not verified
|
|
29
|
-
means the run never checked it, which is not the same
|
|
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.
|
|
30
39
|
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
31
40
|
`;
|
|
32
41
|
|
|
33
42
|
const doHelp = dedent`
|
|
34
|
-
Each instruction is numbered and accounted for: ### Steps reports each as ok or
|
|
35
|
-
|
|
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.
|
|
36
46
|
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
37
47
|
what makes this tier cheap.
|
|
38
48
|
`;
|
|
@@ -94,6 +104,8 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
94
104
|
.option('-p, --path <path>', 'Working directory path')
|
|
95
105
|
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
96
106
|
.option('--session [file]', 'Persist cookies and storage to a session file')
|
|
107
|
+
.option('--model <model>', 'Main model, as provider/model-id')
|
|
108
|
+
.option('--vision-model <model>', 'Model for screenshot analysis, as provider/model-id')
|
|
97
109
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
98
110
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
99
111
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
@@ -103,7 +115,10 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
function primaFor(options: any): Prima {
|
|
118
|
+
Prima.applyEnv();
|
|
106
119
|
if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
120
|
+
if (options.model) process.env.EXPLORBOT_AI_MODEL = options.model;
|
|
121
|
+
if (options.visionModel) process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
|
|
107
122
|
return new Prima(buildOptions(options));
|
|
108
123
|
}
|
|
109
124
|
|
|
@@ -186,13 +201,15 @@ export function createPrimaCommands(name = 'prima'): Command {
|
|
|
186
201
|
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
187
202
|
});
|
|
188
203
|
|
|
189
|
-
addCommonOptions(cmd.command('config').description('Show
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
addCommonOptions(cmd.command('config').description('Show models, config file and paths used by this run'))
|
|
205
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
206
|
+
.action(async (options) => {
|
|
207
|
+
setQuietMode(!isVerboseMode());
|
|
208
|
+
const prima = primaFor(options);
|
|
209
|
+
console.log(await prima.config(options.json).catch((error: unknown) => browserErrorMessage(error)));
|
|
210
|
+
await prima.stop().catch(() => {});
|
|
211
|
+
process.exit(0);
|
|
212
|
+
});
|
|
196
213
|
|
|
197
214
|
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
|
|
198
215
|
await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
|
|
@@ -2,9 +2,10 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
const EXPECTATION_LABELS = {
|
|
5
|
-
passed: 'PASSED
|
|
6
|
-
failed: 'FAILED
|
|
7
|
-
unverified: 'not verified',
|
|
5
|
+
passed: 'PASSED ',
|
|
6
|
+
failed: 'FAILED ',
|
|
7
|
+
unverified: 'not verified ',
|
|
8
|
+
contradiction: 'CONTRADICTION',
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export interface InstanceInfo {
|
|
@@ -21,8 +22,9 @@ export interface EnvelopeData {
|
|
|
21
22
|
used?: string[];
|
|
22
23
|
page: { url: string; previousUrl?: string; title: string; state: string; visits: number };
|
|
23
24
|
changes?: string | null;
|
|
24
|
-
steps?: Array<{ label: string; ok: boolean; proof: string }>;
|
|
25
|
-
expectations?: Array<{ text: string; status: 'passed' | 'failed' | 'unverified' }>;
|
|
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;
|
|
26
28
|
stepFiles?: string;
|
|
27
29
|
value?: string;
|
|
28
30
|
answer?: string;
|
|
@@ -31,23 +33,28 @@ export interface EnvelopeData {
|
|
|
31
33
|
failure?: { error: string; compactAria?: string };
|
|
32
34
|
instance: InstanceInfo;
|
|
33
35
|
status?: string;
|
|
34
|
-
artifacts?: { aria: string; html: string; network?: string };
|
|
36
|
+
artifacts?: { aria: string; html: string; screenshot?: string; network?: string };
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export function renderEnvelope(data: EnvelopeData): string {
|
|
38
|
-
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(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)];
|
|
39
41
|
return sections.filter((section) => section).join('\n\n');
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; requests: unknown[] }): { aria: string; html: string; network?: string } {
|
|
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 } {
|
|
43
45
|
mkdirSync(dir, { recursive: true });
|
|
44
|
-
const paths: { aria: string; html: string; network?: string } = {
|
|
46
|
+
const paths: { aria: string; html: string; screenshot?: string; network?: string } = {
|
|
45
47
|
aria: path.resolve(dir, 'aria.yml'),
|
|
46
48
|
html: path.resolve(dir, 'page.html'),
|
|
47
49
|
};
|
|
48
50
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
49
51
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
50
52
|
|
|
53
|
+
if (snapshot.screenshot) {
|
|
54
|
+
paths.screenshot = path.resolve(dir, 'page.png');
|
|
55
|
+
writeFileSync(paths.screenshot, snapshot.screenshot);
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
if (!snapshot.requests.length) return paths;
|
|
52
59
|
|
|
53
60
|
paths.network = path.resolve(dir, 'network.jsonl');
|
|
@@ -87,7 +94,10 @@ function renderSteps(data: EnvelopeData): string | null {
|
|
|
87
94
|
|
|
88
95
|
const lines: string[] = [];
|
|
89
96
|
data.steps.forEach((step, index) => {
|
|
90
|
-
|
|
97
|
+
let mark = 'FAIL';
|
|
98
|
+
if (step.ok) mark = 'ok ';
|
|
99
|
+
if (step.unconfirmed) mark = '?? ';
|
|
100
|
+
lines.push(`${index + 1}. ${mark} ${step.label}`);
|
|
91
101
|
for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
|
|
92
102
|
});
|
|
93
103
|
if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}`);
|
|
@@ -96,10 +106,21 @@ function renderSteps(data: EnvelopeData): string | null {
|
|
|
96
106
|
|
|
97
107
|
function renderExpectations(data: EnvelopeData): string | null {
|
|
98
108
|
if (!data.expectations?.length) return null;
|
|
99
|
-
|
|
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
|
+
});
|
|
100
116
|
return section('Expected outcomes', lines.join('\n'));
|
|
101
117
|
}
|
|
102
118
|
|
|
119
|
+
function renderWarning(data: EnvelopeData): string | null {
|
|
120
|
+
if (!data.warning) return null;
|
|
121
|
+
return section('Warning', data.warning);
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
function renderOutcome(data: EnvelopeData): string | null {
|
|
104
125
|
if (data.answer) return section('Answer', data.answer);
|
|
105
126
|
if (data.research) return section('Research', data.research);
|
|
@@ -154,8 +175,9 @@ function tabsLabel(tabs: number): string {
|
|
|
154
175
|
|
|
155
176
|
export function renderArtifacts(data: EnvelopeData): string | null {
|
|
156
177
|
if (!data.artifacts) return null;
|
|
157
|
-
const lines = [`aria:
|
|
158
|
-
if (data.artifacts.
|
|
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}`);
|
|
159
181
|
return section('Artifacts', lines.join('\n'));
|
|
160
182
|
}
|
|
161
183
|
|