explorbot 0.3.5 → 0.4.1
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 +23 -16
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +12 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/cli.ts +6 -1
- package/boat/doc-collector/src/docbot.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +23 -8
- package/boat/prima/src/envelope.ts +35 -9
- package/boat/prima/src/prima.ts +23 -10
- package/dist/bin/explorbot-cli.js +24 -16
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +12 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/cli.js +6 -1
- package/dist/boat/doc-collector/src/docbot.js +4 -2
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +22 -8
- package/dist/boat/prima/src/envelope.js +24 -6
- package/dist/boat/prima/src/prima.js +23 -11
- package/dist/package.json +2 -2
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +9 -1
- package/dist/src/action-result.js +57 -18
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +87 -12
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/driller.d.ts +0 -1
- package/dist/src/ai/driller.js +8 -20
- package/dist/src/ai/fisherman-tools.d.ts +9 -0
- package/dist/src/ai/fisherman-tools.js +52 -6
- package/dist/src/ai/fisherman.d.ts +4 -2
- package/dist/src/ai/fisherman.js +48 -27
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.d.ts +1 -0
- package/dist/src/ai/pilot.js +13 -1
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +20 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -3
- package/dist/src/ai/researcher.js +5 -3
- package/dist/src/ai/rules.js +3 -3
- package/dist/src/ai/tester.d.ts +3 -0
- package/dist/src/ai/tester.js +40 -3
- package/dist/src/ai/tools.d.ts +1 -0
- package/dist/src/ai/tools.js +13 -6
- package/dist/src/api/request-result.d.ts +2 -0
- package/dist/src/api/request-result.js +8 -2
- package/dist/src/api/request-store.d.ts +3 -2
- package/dist/src/api/request-store.js +66 -14
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.d.ts +6 -0
- package/dist/src/commands/explore-command.js +39 -3
- package/dist/src/commands/freesail-command.js +10 -1
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.js +6 -6
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.js +5 -0
- package/dist/src/explorbot.d.ts +0 -1
- package/dist/src/explorbot.js +24 -37
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +5 -1
- package/dist/src/state-manager.js +10 -7
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +27 -0
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +6 -42
- package/dist/src/utils/html-diff.d.ts +4 -0
- package/dist/src/utils/html-diff.js +62 -7
- package/dist/src/utils/html.d.ts +5 -15
- package/dist/src/utils/html.js +14 -85
- package/dist/src/utils/overlay.d.ts +56 -11
- package/dist/src/utils/overlay.js +191 -21
- package/dist/src/utils/request-map.d.ts +7 -0
- package/dist/src/utils/request-map.js +16 -0
- package/dist/src/utils/url-matcher.js +4 -2
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/reference/commands.md +32 -5
- package/docs/reference/websocket.md +1 -0
- package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
- package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
- package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
- package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
- package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
- package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
- package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
- package/docs/workflow/agentic-usage.md +12 -2
- package/docs/workflow/ci.md +1 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +2 -2
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +61 -22
- package/src/action.ts +87 -14
- package/src/ai/captain.ts +1 -1
- package/src/ai/driller.ts +7 -39
- package/src/ai/fisherman-tools.ts +56 -7
- package/src/ai/fisherman.ts +48 -28
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +9 -1
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +21 -3
- package/src/ai/researcher/deep-analysis.ts +1 -2
- package/src/ai/researcher.ts +4 -3
- package/src/ai/rules.ts +3 -3
- package/src/ai/tester.ts +40 -3
- package/src/ai/tools.ts +17 -9
- package/src/api/request-result.ts +10 -2
- package/src/api/request-store.ts +60 -13
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +37 -3
- package/src/commands/freesail-command.ts +7 -1
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +6 -6
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +5 -1
- package/src/explorbot.ts +21 -37
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +13 -7
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +7 -44
- package/src/utils/html-diff.ts +62 -7
- package/src/utils/html.ts +14 -91
- package/src/utils/overlay.ts +226 -23
- package/src/utils/request-map.ts +19 -0
- package/src/utils/url-matcher.ts +3 -2
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
export const STATUS_FILE = 'status.json';
|
|
5
|
+
|
|
6
|
+
const ARTIFACT_FILES = { aria: 'aria.yml', html: 'page.html', screenshot: 'page.png', network: 'network.jsonl' };
|
|
7
|
+
|
|
4
8
|
const EXPECTATION_LABELS = {
|
|
5
9
|
passed: 'PASSED ',
|
|
6
10
|
failed: 'FAILED ',
|
|
@@ -33,7 +37,7 @@ export interface EnvelopeData {
|
|
|
33
37
|
failure?: { error: string; compactAria?: string };
|
|
34
38
|
instance: InstanceInfo;
|
|
35
39
|
status?: string;
|
|
36
|
-
artifacts?:
|
|
40
|
+
artifacts?: ArtifactPaths;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export function renderEnvelope(data: EnvelopeData): string {
|
|
@@ -41,27 +45,40 @@ export function renderEnvelope(data: EnvelopeData): string {
|
|
|
41
45
|
return sections.filter((section) => section).join('\n\n');
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; screenshot?: Buffer; requests: unknown[] }):
|
|
48
|
+
export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; screenshot?: Buffer; requests: unknown[] }): ArtifactPaths {
|
|
45
49
|
mkdirSync(dir, { recursive: true });
|
|
46
|
-
const paths:
|
|
47
|
-
aria: path.resolve(dir,
|
|
48
|
-
html: path.resolve(dir,
|
|
50
|
+
const paths: ArtifactPaths = {
|
|
51
|
+
aria: path.resolve(dir, ARTIFACT_FILES.aria),
|
|
52
|
+
html: path.resolve(dir, ARTIFACT_FILES.html),
|
|
49
53
|
};
|
|
50
54
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
51
55
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
52
56
|
|
|
53
57
|
if (snapshot.screenshot) {
|
|
54
|
-
paths.screenshot = path.resolve(dir,
|
|
58
|
+
paths.screenshot = path.resolve(dir, ARTIFACT_FILES.screenshot);
|
|
55
59
|
writeFileSync(paths.screenshot, snapshot.screenshot);
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
if (!snapshot.requests.length) return paths;
|
|
59
63
|
|
|
60
|
-
paths.network = path.resolve(dir,
|
|
64
|
+
paths.network = path.resolve(dir, ARTIFACT_FILES.network);
|
|
61
65
|
writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
|
|
62
66
|
return paths;
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
export function readArtifacts(dir: string): ArtifactPaths {
|
|
70
|
+
const paths: ArtifactPaths = { aria: path.resolve(dir, ARTIFACT_FILES.aria), html: path.resolve(dir, ARTIFACT_FILES.html) };
|
|
71
|
+
if (existsSync(path.resolve(dir, ARTIFACT_FILES.screenshot))) paths.screenshot = path.resolve(dir, ARTIFACT_FILES.screenshot);
|
|
72
|
+
if (existsSync(path.resolve(dir, ARTIFACT_FILES.network))) paths.network = path.resolve(dir, ARTIFACT_FILES.network);
|
|
73
|
+
|
|
74
|
+
const recorded = [...Object.values(ARTIFACT_FILES), STATUS_FILE];
|
|
75
|
+
const files = readdirSync(dir)
|
|
76
|
+
.filter((entry) => !recorded.includes(entry))
|
|
77
|
+
.sort();
|
|
78
|
+
if (files.length) paths.files = files;
|
|
79
|
+
return paths;
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
function renderResult(data: EnvelopeData): string {
|
|
66
83
|
const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
|
|
67
84
|
if (data.used?.length) lines.push(`used: ${data.used.join('; ')}`);
|
|
@@ -100,7 +117,7 @@ function renderSteps(data: EnvelopeData): string | null {
|
|
|
100
117
|
lines.push(`${index + 1}. ${mark} ${step.label}`);
|
|
101
118
|
for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
|
|
102
119
|
});
|
|
103
|
-
if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}`);
|
|
120
|
+
if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}/<n>-<step>.{aria.yaml,html,diff.yaml}`);
|
|
104
121
|
return section('Steps', lines.join('\n'));
|
|
105
122
|
}
|
|
106
123
|
|
|
@@ -178,6 +195,7 @@ export function renderArtifacts(data: EnvelopeData): string | null {
|
|
|
178
195
|
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
|
|
179
196
|
if (data.artifacts.screenshot) lines.push(`screenshot: ${data.artifacts.screenshot}`);
|
|
180
197
|
if (data.artifacts.network) lines.push(`network: ${data.artifacts.network}`);
|
|
198
|
+
if (data.artifacts.files?.length) lines.push(`also here: ${data.artifacts.files.join(', ')}`);
|
|
181
199
|
return section('Artifacts', lines.join('\n'));
|
|
182
200
|
}
|
|
183
201
|
|
|
@@ -189,3 +207,11 @@ function align(label: string, marker: string, width: number): string {
|
|
|
189
207
|
function section(title: string, body: string): string {
|
|
190
208
|
return `### ${title}\n${body}`;
|
|
191
209
|
}
|
|
210
|
+
|
|
211
|
+
export interface ArtifactPaths {
|
|
212
|
+
aria: string;
|
|
213
|
+
html: string;
|
|
214
|
+
screenshot?: string;
|
|
215
|
+
network?: string;
|
|
216
|
+
files?: string[];
|
|
217
|
+
}
|
package/boat/prima/src/prima.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
|
25
25
|
import { pluralize } from '../../../src/utils/logger.ts';
|
|
26
26
|
import { mdq } from '../../../src/utils/markdown-query.ts';
|
|
27
27
|
import { safeFilename } from '../../../src/utils/strings.ts';
|
|
28
|
-
import { type EnvelopeData, type InstanceInfo, writeArtifacts } from './envelope.ts';
|
|
28
|
+
import { type EnvelopeData, type InstanceInfo, STATUS_FILE, readArtifacts, writeArtifacts } from './envelope.ts';
|
|
29
29
|
import { isFunctionExpression, takePwValue, toCodeceptWrapper } from './pw-parser.ts';
|
|
30
30
|
import { type PwServerDescriptor, readDescriptors, selectDescriptor } from './pw-registry.ts';
|
|
31
31
|
import { type SessionRun, latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from './session-log.ts';
|
|
@@ -376,7 +376,6 @@ export class Prima {
|
|
|
376
376
|
|
|
377
377
|
const problems = [...unreached.map((expectation) => `not reached: ${expectation.text}`), ...contradicted.map((expectation) => `the picture and the run disagree about: ${expectation.text}`)];
|
|
378
378
|
if (problems.length) envelope.failure = { error: problems.join('\n') };
|
|
379
|
-
if (contradicted.length) envelope.artifacts = this.artifacts;
|
|
380
379
|
|
|
381
380
|
if (!test.hasFinished || test.isSkipped) {
|
|
382
381
|
envelope.ok = false;
|
|
@@ -982,14 +981,17 @@ export class Prima {
|
|
|
982
981
|
}
|
|
983
982
|
|
|
984
983
|
private async successEnvelope(command: string, used: string[], result: ActionResult, previousState: WebPageState | null): Promise<EnvelopeData> {
|
|
984
|
+
const changes = await this.pageChanges(result, previousState, used[0]);
|
|
985
|
+
const status = await this.saveStatus(result);
|
|
985
986
|
return {
|
|
986
987
|
ok: true,
|
|
987
988
|
command,
|
|
988
989
|
used,
|
|
989
990
|
page: this.pageBlock(result, previousState),
|
|
990
|
-
changes
|
|
991
|
+
changes,
|
|
991
992
|
instance: await this.instanceInfo(),
|
|
992
|
-
status
|
|
993
|
+
status,
|
|
994
|
+
artifacts: this.artifacts,
|
|
993
995
|
};
|
|
994
996
|
}
|
|
995
997
|
|
|
@@ -998,24 +1000,28 @@ export class Prima {
|
|
|
998
1000
|
const failure: EnvelopeData['failure'] = { error: browserErrorMessage(error) };
|
|
999
1001
|
if (result.ariaSnapshot) failure.compactAria = compactAriaSnapshot(result.ariaSnapshot, true);
|
|
1000
1002
|
|
|
1003
|
+
const status = await this.saveStatus(result);
|
|
1001
1004
|
return {
|
|
1002
1005
|
ok: false,
|
|
1003
1006
|
command,
|
|
1004
1007
|
page: this.pageBlock(result, previousState),
|
|
1005
1008
|
failure,
|
|
1006
1009
|
instance: await this.instanceInfo(),
|
|
1007
|
-
status
|
|
1010
|
+
status,
|
|
1011
|
+
artifacts: this.artifacts,
|
|
1008
1012
|
};
|
|
1009
1013
|
}
|
|
1010
1014
|
|
|
1011
1015
|
private async reportEnvelope(command: string, result: ActionResult, previousState: WebPageState | null, outcome: Partial<EnvelopeData>): Promise<EnvelopeData> {
|
|
1016
|
+
const status = await this.saveStatus(result);
|
|
1012
1017
|
return {
|
|
1013
1018
|
ok: true,
|
|
1014
1019
|
command,
|
|
1015
1020
|
page: this.pageBlock(result, previousState),
|
|
1016
1021
|
...outcome,
|
|
1017
1022
|
instance: await this.instanceInfo(),
|
|
1018
|
-
status
|
|
1023
|
+
status,
|
|
1024
|
+
artifacts: this.artifacts,
|
|
1019
1025
|
};
|
|
1020
1026
|
}
|
|
1021
1027
|
|
|
@@ -1060,9 +1066,16 @@ export class Prima {
|
|
|
1060
1066
|
}
|
|
1061
1067
|
|
|
1062
1068
|
async status(hash: string): Promise<EnvelopeData> {
|
|
1069
|
+
if (!this.artifactsDir) {
|
|
1070
|
+
const sites = listSites();
|
|
1071
|
+
const site = sites.find((candidate) => existsSync(path.join(candidate.dir, 'output', 'prima', hash))) || sites[0];
|
|
1072
|
+
if (site && !this.configBaseUrl()) this.sessionUrl = site.url;
|
|
1073
|
+
await this.loadConfig();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1063
1076
|
const dir = this.statusDir(hash);
|
|
1064
|
-
const statusFile = path.join(dir,
|
|
1065
|
-
if (!existsSync(statusFile)) return this.toolFailureEnvelope(`status ${hash}`, `No command was recorded under ${
|
|
1077
|
+
const statusFile = path.join(dir, STATUS_FILE);
|
|
1078
|
+
if (!existsSync(statusFile)) return this.toolFailureEnvelope(`status ${hash}`, `No command was recorded under ${dir}. Every envelope prints its own hash on the Instance line.`);
|
|
1066
1079
|
|
|
1067
1080
|
const saved = JSON.parse(readFileSync(statusFile, 'utf-8'));
|
|
1068
1081
|
return {
|
|
@@ -1070,14 +1083,14 @@ export class Prima {
|
|
|
1070
1083
|
command: `status ${hash}`,
|
|
1071
1084
|
page: saved.page,
|
|
1072
1085
|
instance: await this.instanceInfo(),
|
|
1073
|
-
artifacts:
|
|
1086
|
+
artifacts: readArtifacts(dir),
|
|
1074
1087
|
};
|
|
1075
1088
|
}
|
|
1076
1089
|
|
|
1077
1090
|
private async saveStatus(result: ActionResult): Promise<string> {
|
|
1078
1091
|
const hash = this.statusHash();
|
|
1079
1092
|
await this.writeSnapshot(result);
|
|
1080
|
-
writeFileSync(path.join(this.statusDir(hash),
|
|
1093
|
+
writeFileSync(path.join(this.statusDir(hash), STATUS_FILE), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
|
|
1081
1094
|
return hash;
|
|
1082
1095
|
}
|
|
1083
1096
|
|
|
@@ -10,6 +10,7 @@ import React from 'react';
|
|
|
10
10
|
import { flushTelemetry } from '../src/ai/provider.js';
|
|
11
11
|
import { App } from '../src/components/App.js';
|
|
12
12
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
13
|
+
import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
13
14
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
14
15
|
import { ExplorBot } from '../src/explorbot.js';
|
|
15
16
|
import { remote } from '../src/remote.js';
|
|
@@ -25,7 +26,8 @@ const cli = getCliName();
|
|
|
25
26
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
26
27
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
27
28
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
28
|
-
|
|
29
|
+
wsOption.register(program);
|
|
30
|
+
knowledgeOption.register(program);
|
|
29
31
|
process.on('uncaughtException', async (error) => {
|
|
30
32
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
31
33
|
await flushTelemetry();
|
|
@@ -114,6 +116,7 @@ addCommonOptions(program
|
|
|
114
116
|
.command('explore <path>')
|
|
115
117
|
.description('Explore a page autonomously and run invented scenarios')
|
|
116
118
|
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
119
|
+
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run; wraps up before the limit is hit')
|
|
117
120
|
.option('--focus <feature>', 'Focus area for exploration')
|
|
118
121
|
.option('--configure <spec>', 'Reuse spec: keys new|from|style|subpages|pick_by|priority, e.g. "new:25%;pick_by=random;priority=critical,high"')
|
|
119
122
|
.option('--dry-run', 'Mark picked tests as skipped without executing or generating new ones')).action(async (explorePath, options) => {
|
|
@@ -125,6 +128,10 @@ addCommonOptions(program
|
|
|
125
128
|
const cmd = new ExploreCommand(explorBot);
|
|
126
129
|
if (options.maxTests)
|
|
127
130
|
cmd.maxTests = Number.parseInt(options.maxTests, 10);
|
|
131
|
+
if (options.maxDuration)
|
|
132
|
+
cmd.maxDurationMinutes = Number.parseInt(options.maxDuration, 10);
|
|
133
|
+
else if (process.env.EXPLORBOT_MAX_DURATION)
|
|
134
|
+
cmd.maxDurationMinutes = Number.parseInt(process.env.EXPLORBOT_MAX_DURATION, 10);
|
|
128
135
|
if (options.dryRun)
|
|
129
136
|
cmd.dryRun = true;
|
|
130
137
|
const execArgs = [];
|
|
@@ -219,13 +226,12 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
219
226
|
});
|
|
220
227
|
addCommonOptions(program.command('plan:load <planfile> [index]').description('Load a plan file and display its tests. Pass index to see test details.')).action(async (planfile, index) => {
|
|
221
228
|
try {
|
|
222
|
-
const
|
|
223
|
-
if (!
|
|
224
|
-
console.error(`Plan file not found: ${
|
|
229
|
+
const plan = Plan.loadFromFile(planfile);
|
|
230
|
+
if (!plan?.filePath) {
|
|
231
|
+
console.error(`Plan file not found: ${planfile}`);
|
|
225
232
|
process.exit(1);
|
|
226
233
|
}
|
|
227
|
-
const
|
|
228
|
-
const planFile = path.basename(resolvedPath);
|
|
234
|
+
const planFile = path.basename(plan.filePath);
|
|
229
235
|
if (index) {
|
|
230
236
|
const idx = Number.parseInt(index, 10);
|
|
231
237
|
if (Number.isNaN(idx) || idx < 1 || idx > plan.tests.length) {
|
|
@@ -236,7 +242,7 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
236
242
|
const lines = [];
|
|
237
243
|
lines.push(`## #${idx} ${test.scenario}\n`);
|
|
238
244
|
lines.push(`**Priority:** ${test.priority}`);
|
|
239
|
-
const planUrl = plan.
|
|
245
|
+
const planUrl = plan.startUrl;
|
|
240
246
|
if (planUrl)
|
|
241
247
|
lines.push(`**Plan URL:** ${planUrl}`);
|
|
242
248
|
if (test.startUrl && test.startUrl !== planUrl)
|
|
@@ -256,7 +262,7 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
256
262
|
console.log(parseMarkdownToTerminal(lines.join('\n')));
|
|
257
263
|
return;
|
|
258
264
|
}
|
|
259
|
-
const planUrl = plan.
|
|
265
|
+
const planUrl = plan.startUrl;
|
|
260
266
|
const lines = [`**${plan.title}** (${plan.tests.length} tests)\n`];
|
|
261
267
|
if (planUrl) {
|
|
262
268
|
lines.push(`URL: ${planUrl}\n`);
|
|
@@ -284,24 +290,25 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
284
290
|
});
|
|
285
291
|
addCommonOptions(program.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1,3, 1-5, *, all').option('--grep <pattern>', 'Run tests matching pattern').option('--from-plan <file>', 'Load plan file when the first argument is a test index')).action(async (planfile, index, options) => {
|
|
286
292
|
try {
|
|
287
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
288
|
-
await explorBot.start();
|
|
289
293
|
let planfileArg = planfile;
|
|
290
294
|
let indexArg = index;
|
|
291
295
|
if (options.fromPlan) {
|
|
292
296
|
planfileArg = options.fromPlan;
|
|
293
297
|
indexArg = planfile;
|
|
294
298
|
}
|
|
299
|
+
const planTarget = Plan.loadFromFile(planfileArg)?.startUrl;
|
|
300
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(planTarget, options));
|
|
301
|
+
await explorBot.start();
|
|
295
302
|
const plan = explorBot.loadPlan(planfileArg);
|
|
296
303
|
const pending = plan.getPendingTests();
|
|
297
304
|
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
298
|
-
const startUrl = plan.
|
|
305
|
+
const startUrl = plan.startUrl;
|
|
299
306
|
if (!startUrl) {
|
|
300
307
|
throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
|
|
301
308
|
}
|
|
302
309
|
log(`Navigating to ${startUrl}`);
|
|
303
310
|
await explorBot.visit(startUrl);
|
|
304
|
-
let args = '';
|
|
311
|
+
let args = '*';
|
|
305
312
|
if (indexArg)
|
|
306
313
|
args = indexArg;
|
|
307
314
|
else if (options.grep)
|
|
@@ -387,10 +394,11 @@ addCommonOptions(program
|
|
|
387
394
|
.option('--deep', 'Depth-first: prioritize newly discovered pages')
|
|
388
395
|
.option('--shallow', 'Breadth-first: pick globally least-visited page')
|
|
389
396
|
.option('--scope <prefix>', 'Restrict navigation to URL prefix')
|
|
390
|
-
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
397
|
+
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
398
|
+
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')).action(async (startUrl, options) => {
|
|
391
399
|
const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
|
|
392
400
|
await explorBot.start();
|
|
393
|
-
const args = [options.deep && '--deep', options.shallow && '--shallow', options.scope && `--scope ${options.scope}`, options.maxTests && `--max-tests ${options.maxTests}`].filter(Boolean).join(' ');
|
|
401
|
+
const args = [options.deep && '--deep', options.shallow && '--shallow', options.scope && `--scope ${options.scope}`, options.maxTests && `--max-tests ${options.maxTests}`, options.maxDuration && `--max-duration ${options.maxDuration}`].filter(Boolean).join(' ');
|
|
394
402
|
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
395
403
|
const cmd = new FreesailCommand(explorBot);
|
|
396
404
|
await cmd.execute(args);
|
|
@@ -638,13 +646,13 @@ addCommonOptions(program.command('navigate <url>').description('Navigate to a UR
|
|
|
638
646
|
await showStatsAndExit(1);
|
|
639
647
|
}
|
|
640
648
|
});
|
|
641
|
-
addCommonOptions(program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')).action(async (url, options) => {
|
|
649
|
+
addCommonOptions(program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--save-knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')).action(async (url, options) => {
|
|
642
650
|
try {
|
|
643
651
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
644
652
|
await explorBot.start();
|
|
645
653
|
await explorBot.visit(url);
|
|
646
654
|
const plan = await explorBot.agentDriller().drill({
|
|
647
|
-
knowledgePath: options.
|
|
655
|
+
knowledgePath: options.saveKnowledge,
|
|
648
656
|
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
649
657
|
interactive: false,
|
|
650
658
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { knowledgeOption, wsOption } from "../../../src/commands/options/index.js";
|
|
3
3
|
import { createApiCommands } from "../src/cli.js";
|
|
4
4
|
const program = createApiCommands('apibot');
|
|
5
|
-
|
|
5
|
+
wsOption.register(program);
|
|
6
|
+
knowledgeOption.register(program);
|
|
6
7
|
program.parse();
|
|
@@ -24,15 +24,17 @@ export class Chief extends ChiefBase {
|
|
|
24
24
|
provider;
|
|
25
25
|
config;
|
|
26
26
|
apiClient;
|
|
27
|
+
knowledgeTracker;
|
|
27
28
|
currentPlan = null;
|
|
28
29
|
lastStyleName = '';
|
|
29
30
|
MIN_TASKS = 3;
|
|
30
31
|
MAX_TASKS = 10;
|
|
31
|
-
constructor(provider, config, apiClient) {
|
|
32
|
+
constructor(provider, config, apiClient, knowledgeTracker) {
|
|
32
33
|
super();
|
|
33
34
|
this.provider = provider;
|
|
34
35
|
this.config = config;
|
|
35
36
|
this.apiClient = apiClient || null;
|
|
37
|
+
this.knowledgeTracker = knowledgeTracker;
|
|
36
38
|
}
|
|
37
39
|
async plan(endpoint, opts) {
|
|
38
40
|
tag('info').log(`Planning API tests for ${endpoint}`);
|
|
@@ -42,6 +44,9 @@ export class Chief extends ChiefBase {
|
|
|
42
44
|
await Observability.run(`chief: ${endpoint}`, { tags: ['chief'], sessionId: endpoint }, async () => {
|
|
43
45
|
const sampleData = await this.collectSampleData(endpoint);
|
|
44
46
|
const conversation = this.buildConversation(endpoint, opts?.style, sampleData);
|
|
47
|
+
const knowledge = this.knowledgeTracker?.renderEndpointKnowledge(endpoint);
|
|
48
|
+
if (knowledge)
|
|
49
|
+
conversation.addUserText(knowledge);
|
|
45
50
|
if (opts?.specDefinition) {
|
|
46
51
|
conversation.addUserText(dedent `
|
|
47
52
|
<api_spec>
|
|
@@ -11,11 +11,13 @@ export class Curler {
|
|
|
11
11
|
apiClient;
|
|
12
12
|
requestState;
|
|
13
13
|
reporter;
|
|
14
|
-
|
|
14
|
+
knowledgeTracker;
|
|
15
|
+
constructor(provider, apiClient, requestState, reporter, knowledgeTracker) {
|
|
15
16
|
this.provider = provider;
|
|
16
17
|
this.apiClient = apiClient;
|
|
17
18
|
this.requestState = requestState;
|
|
18
19
|
this.reporter = reporter;
|
|
20
|
+
this.knowledgeTracker = knowledgeTracker;
|
|
19
21
|
}
|
|
20
22
|
async test(test, opts) {
|
|
21
23
|
tag('info').log(`Testing: ${test.scenario}`);
|
|
@@ -25,6 +27,9 @@ export class Curler {
|
|
|
25
27
|
await this.reporter.reportTestStart(test);
|
|
26
28
|
const conversation = this.provider.startConversation(this.buildSystemPrompt(), 'curler', this.provider.getAgenticModel('curler'));
|
|
27
29
|
const tools = createCurlerTools(this.apiClient, this.requestState, test, opts?.searchSpec);
|
|
30
|
+
const knowledge = test.startUrl && this.knowledgeTracker?.renderEndpointKnowledge(test.startUrl);
|
|
31
|
+
if (knowledge)
|
|
32
|
+
conversation.addUserText(knowledge);
|
|
28
33
|
const initialPrompt = this.buildTestPrompt(test, opts?.specDefinition, opts?.baseEndpoint);
|
|
29
34
|
conversation.addUserText(initialPrompt);
|
|
30
35
|
await loop(async ({ stop, iteration }) => {
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { AIProvider } from "../../../src/ai/provider.js";
|
|
4
4
|
import { RequestStore } from "../../../src/api/request-store.js";
|
|
5
5
|
import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from "../../../src/api/spec-reader.js";
|
|
6
|
+
import { KnowledgeTracker } from "../../../src/knowledge-tracker.js";
|
|
6
7
|
import { Reporter } from "../../../src/reporter.js";
|
|
7
8
|
import { Plan } from "../../../src/test-plan.js";
|
|
8
9
|
import { setVerboseMode, tag } from "../../../src/utils/logger.js";
|
|
@@ -19,6 +20,7 @@ export class ApiBot {
|
|
|
19
20
|
apiClient;
|
|
20
21
|
requestState;
|
|
21
22
|
reporter;
|
|
23
|
+
knowledgeTracker;
|
|
22
24
|
options;
|
|
23
25
|
apiSpec;
|
|
24
26
|
constructor(options = {}) {
|
|
@@ -30,7 +32,7 @@ export class ApiBot {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
async start() {
|
|
33
|
-
this.config = await this.configParser.loadConfig(
|
|
35
|
+
this.config = await this.configParser.loadConfig(this.options);
|
|
34
36
|
this.provider = new AIProvider(this.config.ai);
|
|
35
37
|
await this.provider.validateConnection();
|
|
36
38
|
this.apiClient = new ApiClient(this.config.api.baseEndpoint, this.config.api.headers || {}, {
|
|
@@ -42,6 +44,7 @@ export class ApiBot {
|
|
|
42
44
|
this.configParser.ensureDirectory(outputDir);
|
|
43
45
|
this.requestState = new RequestStore(outputDir);
|
|
44
46
|
this.reporter = new Reporter(this.config.reporter);
|
|
47
|
+
this.knowledgeTracker = new KnowledgeTracker({ knowledgeDir: this.configParser.getKnowledgeDir() });
|
|
45
48
|
validateSpecs(this.config.api.spec);
|
|
46
49
|
this.apiSpec = await loadSpec(this.config.api.spec, outputDir);
|
|
47
50
|
tag('info').log('OpenAPI spec loaded');
|
|
@@ -78,13 +81,14 @@ export class ApiBot {
|
|
|
78
81
|
config: this.config,
|
|
79
82
|
apiClient: this.apiClient,
|
|
80
83
|
requestState: this.requestState,
|
|
84
|
+
knowledge: this.knowledgeTracker,
|
|
81
85
|
});
|
|
82
86
|
}
|
|
83
87
|
agentChief() {
|
|
84
|
-
return (this.agents.chief ||= this.createAgent(({ ai, config, apiClient }) => new Chief(ai, config, apiClient)));
|
|
88
|
+
return (this.agents.chief ||= this.createAgent(({ ai, config, apiClient, knowledge }) => new Chief(ai, config, apiClient, knowledge)));
|
|
85
89
|
}
|
|
86
90
|
agentCurler() {
|
|
87
|
-
return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState }) => new Curler(ai, apiClient, requestState, this.reporter)));
|
|
91
|
+
return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState, knowledge }) => new Curler(ai, apiClient, requestState, this.reporter, knowledge)));
|
|
88
92
|
}
|
|
89
93
|
async plan(target, opts = {}) {
|
|
90
94
|
if (opts.fresh) {
|
|
@@ -12,10 +12,18 @@ function buildOptions(options) {
|
|
|
12
12
|
verbose: options.verbose || options.debug,
|
|
13
13
|
config: options.config,
|
|
14
14
|
path: options.path,
|
|
15
|
+
baseEndpoint: options.endpoint,
|
|
16
|
+
spec: options.spec,
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
function addCommonOptions(cmd) {
|
|
18
|
-
return cmd
|
|
20
|
+
return cmd
|
|
21
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
22
|
+
.option('--debug', 'Enable debug logging')
|
|
23
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
24
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
25
|
+
.option('--endpoint <url>', 'Base API endpoint to test (env: EXPLORBOT_URL)')
|
|
26
|
+
.option('--spec <path>', 'OpenAPI spec file or URL (env: EXPLORBOT_API_SPEC)');
|
|
19
27
|
}
|
|
20
28
|
function selectTests(tests, index) {
|
|
21
29
|
if (!index || index === '*' || index === 'all') {
|
|
@@ -76,8 +84,10 @@ export function createApiCommands(name = 'api') {
|
|
|
76
84
|
.action(async (endpoint, options) => {
|
|
77
85
|
const parser = ApibotConfigParser.getInstance();
|
|
78
86
|
const [site] = listSites();
|
|
87
|
+
const runOptions = buildOptions(options);
|
|
88
|
+
runOptions.endpoint = endpoint || site?.url;
|
|
79
89
|
try {
|
|
80
|
-
const config = await parser.loadConfig(
|
|
90
|
+
const config = await parser.loadConfig(runOptions);
|
|
81
91
|
console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
|
|
82
92
|
}
|
|
83
93
|
catch (error) {
|
|
@@ -39,6 +39,7 @@ export class ApibotConfigParser {
|
|
|
39
39
|
}
|
|
40
40
|
ApibotConfigParser.loadEnv(globalEnvPath());
|
|
41
41
|
ApibotConfigParser.loadEnv('.env');
|
|
42
|
+
this.applyRunOptions(options);
|
|
42
43
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
43
44
|
if (!resolvedPath) {
|
|
44
45
|
try {
|
|
@@ -63,6 +64,9 @@ export class ApibotConfigParser {
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
this.config = this.mergeWithDefaults(loadedConfig);
|
|
67
|
+
this.applyEnvSpec(this.config.api);
|
|
68
|
+
if (options?.baseEndpoint)
|
|
69
|
+
this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
|
|
66
70
|
await resolveConfigModels(this.config.ai);
|
|
67
71
|
this.configPath = resolvedPath;
|
|
68
72
|
this.site = null;
|
|
@@ -104,6 +108,13 @@ export class ApibotConfigParser {
|
|
|
104
108
|
const resolved = resolveSiteTarget(endpoint, this.site.url);
|
|
105
109
|
if (resolved.baseUrl !== this.site.url)
|
|
106
110
|
return endpoint;
|
|
111
|
+
const basePath = new URL(this.getConfig().api.baseEndpoint).pathname.replace(/\/$/, '');
|
|
112
|
+
if (!basePath)
|
|
113
|
+
return resolved.path;
|
|
114
|
+
if (resolved.path === basePath)
|
|
115
|
+
return '/';
|
|
116
|
+
if (resolved.path.startsWith(`${basePath}/`))
|
|
117
|
+
return resolved.path.slice(basePath.length);
|
|
107
118
|
return resolved.path;
|
|
108
119
|
}
|
|
109
120
|
getPlansDir() {
|
|
@@ -121,13 +132,27 @@ export class ApibotConfigParser {
|
|
|
121
132
|
mkdirSync(dirPath, { recursive: true });
|
|
122
133
|
}
|
|
123
134
|
}
|
|
135
|
+
applyRunOptions(options) {
|
|
136
|
+
if (options?.baseEndpoint)
|
|
137
|
+
process.env.EXPLORBOT_URL = options.baseEndpoint;
|
|
138
|
+
if (options?.spec)
|
|
139
|
+
process.env.EXPLORBOT_API_SPEC = options.spec;
|
|
140
|
+
}
|
|
141
|
+
applyEnvSpec(api) {
|
|
142
|
+
if (!process.env.EXPLORBOT_API_SPEC)
|
|
143
|
+
return;
|
|
144
|
+
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
145
|
+
}
|
|
124
146
|
enterGlobalMode(config, endpoint) {
|
|
125
147
|
const site = resolveSiteTarget(endpoint);
|
|
126
148
|
this.site = registerSite(site.baseUrl);
|
|
149
|
+
let baseEndpoint = site.baseUrl;
|
|
150
|
+
const envUrl = process.env.EXPLORBOT_URL;
|
|
151
|
+
if (envUrl && URL.parse(envUrl)?.origin === site.baseUrl)
|
|
152
|
+
baseEndpoint = envUrl.replace(/\/$/, '');
|
|
127
153
|
config.dirs = { output: 'output', knowledge: 'knowledge' };
|
|
128
|
-
config.api = { ...config.api, baseEndpoint
|
|
129
|
-
|
|
130
|
-
config.api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
154
|
+
config.api = { ...config.api, baseEndpoint };
|
|
155
|
+
materializeKnowledge(this.site.dir);
|
|
131
156
|
}
|
|
132
157
|
async loadEnvConfig() {
|
|
133
158
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
@@ -140,14 +165,12 @@ export class ApibotConfigParser {
|
|
|
140
165
|
}
|
|
141
166
|
const baseEndpoint = process.env.EXPLORBOT_URL;
|
|
142
167
|
if (!baseEndpoint) {
|
|
143
|
-
throw new Error('No API endpoint to test.
|
|
168
|
+
throw new Error('No API endpoint to test. Pass --endpoint or set EXPLORBOT_URL to the API base endpoint');
|
|
144
169
|
}
|
|
145
170
|
const outputRoot = resolveOutputRoot();
|
|
146
171
|
materializeKnowledge(outputRoot);
|
|
147
172
|
const api = { baseEndpoint };
|
|
148
|
-
|
|
149
|
-
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
150
|
-
}
|
|
173
|
+
this.applyEnvSpec(api);
|
|
151
174
|
let model;
|
|
152
175
|
if (provider && modelSpec)
|
|
153
176
|
model = await createModel(provider, modelSpec);
|
|
@@ -210,7 +233,7 @@ export class ApibotConfigParser {
|
|
|
210
233
|
}
|
|
211
234
|
}
|
|
212
235
|
mergeWithDefaults(config) {
|
|
213
|
-
return this.deepMerge({ dirs: { output: 'output' } }, config);
|
|
236
|
+
return this.deepMerge({ dirs: { output: 'output' }, api: {} }, config);
|
|
214
237
|
}
|
|
215
238
|
deepMerge(target, source) {
|
|
216
239
|
const result = { ...target };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { knowledgeOption, wsOption } from "../../../src/commands/options/index.js";
|
|
3
3
|
import { createDocsCommands } from "../src/cli.js";
|
|
4
4
|
const program = createDocsCommands('doc-collector');
|
|
5
|
-
|
|
5
|
+
wsOption.register(program);
|
|
6
|
+
knowledgeOption.register(program);
|
|
6
7
|
program.parse();
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
5
|
+
import { remote } from "../../../src/remote.js";
|
|
5
6
|
import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from "../../../src/utils/logger.js";
|
|
6
7
|
import { DocBot } from "./docbot.js";
|
|
7
8
|
function buildOptions(options) {
|
|
@@ -14,6 +15,7 @@ function buildOptions(options) {
|
|
|
14
15
|
incognito: options.incognito,
|
|
15
16
|
session: options.session,
|
|
16
17
|
docsConfig: options.docsConfig,
|
|
18
|
+
baseUrl: options.url,
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
21
|
function addCommonOptions(cmd) {
|
|
@@ -23,6 +25,7 @@ function addCommonOptions(cmd) {
|
|
|
23
25
|
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
24
26
|
.option('--docs-config <path>', 'Path to doc collector configuration file')
|
|
25
27
|
.option('-p, --path <path>', 'Working directory path')
|
|
28
|
+
.option('--url <url>', 'Base URL of the site, when the path argument is relative (env: EXPLORBOT_URL)')
|
|
26
29
|
.option('-s, --show', 'Show browser window')
|
|
27
30
|
.option('--headless', 'Run browser in headless mode')
|
|
28
31
|
.option('--incognito', 'Run without recording experiences')
|
|
@@ -50,10 +53,12 @@ export function createDocsCommands(name = 'docs') {
|
|
|
50
53
|
console.log(`Pages dir: ${path.join(result.outputDir, 'pages')}`);
|
|
51
54
|
console.log(`Use in Explorbot: npx explorbot start ${startPath} --spec "${result.outputDir}"`);
|
|
52
55
|
await bot.stop();
|
|
56
|
+
await remote.close(0);
|
|
53
57
|
process.exit(0);
|
|
54
58
|
}
|
|
55
59
|
catch (error) {
|
|
56
60
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
61
|
+
await remote.close(1);
|
|
57
62
|
process.exit(1);
|
|
58
63
|
}
|
|
59
64
|
});
|
|
@@ -62,7 +67,7 @@ export function createDocsCommands(name = 'docs') {
|
|
|
62
67
|
.action(async (url, options) => {
|
|
63
68
|
setQuietMode(!isVerboseMode());
|
|
64
69
|
try {
|
|
65
|
-
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
70
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url: url || options.url, json: options.json }));
|
|
66
71
|
}
|
|
67
72
|
catch (error) {
|
|
68
73
|
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
@@ -20,7 +20,7 @@ class DocBot {
|
|
|
20
20
|
scopeRoot = '/';
|
|
21
21
|
constructor(options = {}) {
|
|
22
22
|
this.options = options;
|
|
23
|
-
const baseUrl = this.extractAbsoluteBaseUrl(options.startUrl || '/');
|
|
23
|
+
const baseUrl = this.extractAbsoluteBaseUrl(options.startUrl || '/') || options.baseUrl;
|
|
24
24
|
this.explorBot = new ExplorBot({
|
|
25
25
|
baseUrl,
|
|
26
26
|
verbose: options.verbose,
|
|
@@ -409,7 +409,9 @@ class DocBot {
|
|
|
409
409
|
saveIndex(startPath, pages, skipped, maxPages) {
|
|
410
410
|
const outputDir = this.configParser.getOutputDir();
|
|
411
411
|
const indexPath = path.join(outputDir, 'index.md');
|
|
412
|
-
|
|
412
|
+
const index = renderSpecIndex(outputDir, startPath, pages, skipped, maxPages);
|
|
413
|
+
writeFileSync(indexPath, index, 'utf8');
|
|
414
|
+
tag('data').log('docs', { path: indexPath, content: index });
|
|
413
415
|
const diagramPath = path.join(outputDir, 'state-diagram.mmd');
|
|
414
416
|
writeFileSync(diagramPath, renderMermaidBody(outputDir, pages), 'utf8');
|
|
415
417
|
return { indexPath, diagramPath };
|