explorbot 0.5.9 → 0.6.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/README.md +1 -1
- package/bin/explorbot-cli.ts +495 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/api-tester/src/commands/init-command.ts +2 -0
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/dist/bin/explorbot-cli.js +456 -444
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/boat/api-tester/src/commands/init-command.js +2 -0
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
- package/dist/boat/prima/bin/prima-cli.js +2 -1
- package/dist/models.json +1 -1
- package/dist/package.json +2 -1
- package/dist/rules/navigator/output.md +2 -8
- package/dist/rules/navigator/verification-actions.md +6 -2
- package/dist/rules/planner/ignored-pages.md +1 -0
- package/dist/src/action-result.js +1 -1
- package/dist/src/action.d.ts +0 -1
- package/dist/src/action.js +0 -4
- package/dist/src/ai/captain/idle-mode.js +42 -26
- package/dist/src/ai/captain/mixin.d.ts +2 -0
- package/dist/src/ai/captain/web-mode.js +11 -0
- package/dist/src/ai/captain.d.ts +6 -4
- package/dist/src/ai/captain.js +25 -1
- package/dist/src/ai/driller.js +1 -1
- package/dist/src/ai/experience-compactor.js +2 -2
- package/dist/src/ai/judge.d.ts +7 -0
- package/dist/src/ai/judge.js +8 -1
- package/dist/src/ai/navigator.d.ts +1 -0
- package/dist/src/ai/navigator.js +11 -7
- package/dist/src/ai/planner/subpages.js +1 -1
- package/dist/src/ai/planner.d.ts +12 -5
- package/dist/src/ai/planner.js +138 -86
- package/dist/src/ai/provider.d.ts +1 -1
- package/dist/src/ai/provider.js +16 -5
- package/dist/src/ai/rerunner.js +1 -1
- package/dist/src/ai/researcher/cache.js +14 -6
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -2
- package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
- package/dist/src/ai/researcher.js +44 -24
- package/dist/src/ai/rules.js +6 -7
- package/dist/src/ai/scout.d.ts +12 -2
- package/dist/src/ai/scout.js +103 -8
- package/dist/src/ai/tester.d.ts +9 -5
- package/dist/src/ai/tester.js +22 -42
- package/dist/src/ai/tools.js +10 -8
- package/dist/src/application-spec.d.ts +2 -0
- package/dist/src/application-spec.js +17 -12
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/explore-command.d.ts +3 -2
- package/dist/src/commands/explore-command.js +35 -23
- package/dist/src/commands/help-json-command.js +3 -1
- package/dist/src/commands/init-command.js +8 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/options/auth-option.d.ts +6 -0
- package/dist/src/commands/options/auth-option.js +14 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -0
- package/dist/src/commands/plan-command.d.ts +3 -0
- package/dist/src/commands/plan-command.js +43 -17
- package/dist/src/commands/plan-save-command.js +6 -17
- package/dist/src/commands/plans-command.js +1 -1
- package/dist/src/commands/test-command.js +12 -2
- package/dist/src/components/App.d.ts +1 -2
- package/dist/src/components/App.js +1 -3
- package/dist/src/components/InputPane.js +0 -5
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +3 -12
- package/dist/src/explorbot.d.ts +3 -2
- package/dist/src/explorbot.js +32 -10
- package/dist/src/explorer.js +6 -7
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +18 -6
- package/dist/src/playwright-recorder.d.ts +0 -1
- package/dist/src/playwright-recorder.js +0 -2
- package/dist/src/reporter.d.ts +1 -1
- package/dist/src/reporter.js +3 -3
- package/dist/src/test-plan.js +1 -1
- package/dist/src/utils/error-page.d.ts +17 -0
- package/dist/src/utils/error-page.js +53 -2
- package/dist/src/utils/html-diff.d.ts +1 -2
- package/dist/src/utils/html-diff.js +4 -44
- package/dist/src/utils/logger.js +27 -10
- package/dist/src/utils/markdown-terminal.js +1 -2
- package/dist/src/utils/next-steps.js +20 -16
- package/dist/src/utils/page-readiness.d.ts +2 -1
- package/dist/src/utils/page-readiness.js +24 -5
- package/dist/src/utils/test-plan-markdown.d.ts +3 -0
- package/dist/src/utils/test-plan-markdown.js +35 -4
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +5 -0
- package/docs/basics/providers.md +1 -1
- package/docs/contributing/regression-tests.md +19 -4
- package/docs/reference/commands.md +18 -2
- package/docs/reference/configuration.md +1 -1
- package/docs/web-testing/agents.md +8 -0
- package/docs/web-testing/customization.md +2 -0
- package/docs/web-testing/researcher.md +1 -1
- package/docs/workflow/agentic-usage.md +7 -0
- package/docs/workflow/knowledge.md +6 -0
- package/models.json +1 -1
- package/package.json +2 -1
- package/rules/navigator/output.md +2 -8
- package/rules/navigator/verification-actions.md +6 -2
- package/rules/planner/ignored-pages.md +1 -0
- package/src/action-result.ts +1 -2
- package/src/action.ts +0 -5
- package/src/ai/captain/idle-mode.ts +38 -26
- package/src/ai/captain/mixin.ts +2 -1
- package/src/ai/captain/web-mode.ts +12 -0
- package/src/ai/captain.ts +25 -7
- package/src/ai/driller.ts +1 -2
- package/src/ai/experience-compactor.ts +2 -2
- package/src/ai/judge.ts +10 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner/subpages.ts +1 -2
- package/src/ai/planner.ts +140 -92
- package/src/ai/provider.ts +15 -6
- package/src/ai/rerunner.ts +1 -2
- package/src/ai/researcher/cache.ts +16 -7
- package/src/ai/researcher/deep-analysis.ts +7 -2
- package/src/ai/researcher/fingerprint-worker.ts +5 -5
- package/src/ai/researcher.ts +44 -28
- package/src/ai/rules.ts +6 -7
- package/src/ai/scout.ts +98 -10
- package/src/ai/tester.ts +29 -47
- package/src/ai/tools.ts +11 -9
- package/src/application-spec.ts +19 -13
- package/src/commands/clean-command.ts +1 -1
- package/src/commands/context-command.ts +0 -1
- package/src/commands/context-experience-command.ts +0 -1
- package/src/commands/context-knowledge-command.ts +0 -1
- package/src/commands/explore-command.ts +36 -23
- package/src/commands/help-json-command.ts +8 -6
- package/src/commands/init-command.ts +10 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/options/auth-option.ts +15 -0
- package/src/commands/options/index.ts +2 -0
- package/src/commands/plan-command.ts +44 -17
- package/src/commands/plan-save-command.ts +6 -17
- package/src/commands/plans-command.ts +1 -1
- package/src/commands/test-command.ts +11 -2
- package/src/components/App.tsx +2 -6
- package/src/components/InputPane.tsx +0 -5
- package/src/config.ts +5 -16
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +31 -11
- package/src/explorer.ts +6 -7
- package/src/knowledge-tracker.ts +18 -7
- package/src/playwright-recorder.ts +0 -2
- package/src/reporter.ts +3 -4
- package/src/test-plan.ts +1 -2
- package/src/utils/error-page.ts +64 -2
- package/src/utils/html-diff.ts +4 -56
- package/src/utils/html.ts +0 -1
- package/src/utils/logger.ts +30 -12
- package/src/utils/markdown-terminal.ts +1 -2
- package/src/utils/next-steps.ts +19 -17
- package/src/utils/page-readiness.ts +33 -7
- package/src/utils/test-plan-markdown.ts +36 -4
- package/src/utils/url-matcher.ts +6 -0
package/bin/explorbot-cli.ts
CHANGED
|
@@ -3,16 +3,19 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { Command } from 'commander';
|
|
7
|
-
import figureSet from 'figures';
|
|
6
|
+
import { Command, Help } from 'commander';
|
|
8
7
|
import { render } from 'ink';
|
|
9
8
|
import React from 'react';
|
|
9
|
+
import { createApiCommands } from '../boat/api-tester/src/cli.ts';
|
|
10
|
+
import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
|
|
11
|
+
import { createPrimaCommands } from '../boat/prima/src/cli.ts';
|
|
10
12
|
import { flushTelemetry } from '../src/ai/provider.js';
|
|
11
13
|
import { HelpJsonCommand } from '../src/commands/help-json-command.js';
|
|
12
14
|
import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
|
|
15
|
+
import { PlanCommand } from '../src/commands/plan-command.js';
|
|
13
16
|
import { App } from '../src/components/App.js';
|
|
14
17
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
15
|
-
import { decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
18
|
+
import { authOption, decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
16
19
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS, outputPath } from '../src/config.js';
|
|
17
20
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
18
21
|
import { remote } from '../src/remote.js';
|
|
@@ -22,7 +25,6 @@ import { getCliName } from '../src/utils/cli-name.ts';
|
|
|
22
25
|
import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode, tag } from '../src/utils/logger.js';
|
|
23
26
|
import { jsonToTable } from '../src/utils/markdown-parser.js';
|
|
24
27
|
import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
|
|
25
|
-
import { type NextStepSection, printNextSteps, relativeToCwd } from '../src/utils/next-steps.ts';
|
|
26
28
|
import { readCliSitemap } from '../src/utils/sitemap.ts';
|
|
27
29
|
import { UrlFilter } from '../src/utils/url-filter.ts';
|
|
28
30
|
|
|
@@ -32,10 +34,40 @@ const cli = getCliName();
|
|
|
32
34
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
33
35
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const GROUPS = {
|
|
38
|
+
explore: 'Explore:',
|
|
39
|
+
steps: 'Steps:',
|
|
40
|
+
results: 'Results:',
|
|
41
|
+
knowledge: 'Knowledge & rules:',
|
|
42
|
+
setup: 'Setup:',
|
|
43
|
+
tools: 'More tools:',
|
|
44
|
+
help: 'Help:',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
program
|
|
48
|
+
.name(cli)
|
|
49
|
+
.description('AI-powered exploratory testing for web apps')
|
|
50
|
+
.version(pkgVersion, '-V, --version')
|
|
51
|
+
.configureHelp({
|
|
52
|
+
styleTitle: (str) => chalk.yellow.bold(str),
|
|
53
|
+
styleCommandText: (str) => chalk.bold(str),
|
|
54
|
+
styleSubcommandText: (str) => chalk.cyan(str),
|
|
55
|
+
styleOptionText: (str) => chalk.green(str),
|
|
56
|
+
styleArgumentText: (str) => chalk.magenta(str),
|
|
57
|
+
showGlobalOptions: true,
|
|
58
|
+
visibleOptions(cmd) {
|
|
59
|
+
const options = Help.prototype.visibleOptions.call(this, cmd);
|
|
60
|
+
if (cmd.parent) return options;
|
|
61
|
+
return options.filter((option) => !isGlobalOption(option.flags));
|
|
62
|
+
},
|
|
63
|
+
visibleGlobalOptions(cmd) {
|
|
64
|
+
return Help.prototype.visibleGlobalOptions.call(this, cmd).filter((option) => isGlobalOption(option.flags));
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const globalOptions = [knowledgeOption, authOption, decisionModelOption, wsOption];
|
|
69
|
+
for (const option of globalOptions) option.register(program);
|
|
70
|
+
const isGlobalOption = (flags: string) => globalOptions.some((option) => option.flags === flags);
|
|
39
71
|
|
|
40
72
|
process.on('uncaughtException', async (error) => {
|
|
41
73
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
@@ -63,10 +95,11 @@ interface CLIOptions {
|
|
|
63
95
|
headless?: boolean;
|
|
64
96
|
incognito?: boolean;
|
|
65
97
|
session?: string | boolean;
|
|
66
|
-
spec?: string;
|
|
98
|
+
spec?: string | string[];
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
function buildExplorBotOptions(from: string | undefined, options: CLIOptions): ExplorBotOptions {
|
|
102
|
+
const spec = Array.isArray(options.spec) ? (options.spec.length > 0 ? options.spec : undefined) : options.spec;
|
|
70
103
|
return {
|
|
71
104
|
from,
|
|
72
105
|
baseUrl: options.baseUrl,
|
|
@@ -77,7 +110,7 @@ function buildExplorBotOptions(from: string | undefined, options: CLIOptions): E
|
|
|
77
110
|
headless: options.headless,
|
|
78
111
|
incognito: options.incognito,
|
|
79
112
|
session: options.session,
|
|
80
|
-
applicationSpec:
|
|
113
|
+
applicationSpec: spec,
|
|
81
114
|
} as ExplorBotOptions;
|
|
82
115
|
}
|
|
83
116
|
|
|
@@ -91,7 +124,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
91
124
|
.option('-s, --show', 'Show browser window')
|
|
92
125
|
.option('--headless', 'Run browser in headless mode')
|
|
93
126
|
.option('--incognito', 'Run without recording experiences')
|
|
94
|
-
.option('--spec <path>', 'Use a Docbot application spec directory or index.md')
|
|
127
|
+
.option('--spec <path>', 'Use a Docbot application spec directory or index.md (repeatable)', (val: string, acc: string[]) => [...(acc || []), val])
|
|
95
128
|
.option('--session [file]', 'Save/restore browser session from file');
|
|
96
129
|
}
|
|
97
130
|
|
|
@@ -158,7 +191,9 @@ async function showStatsAndExit(code: number): Promise<never> {
|
|
|
158
191
|
process.exit(code);
|
|
159
192
|
}
|
|
160
193
|
|
|
161
|
-
|
|
194
|
+
program.commandsGroup(GROUPS.explore);
|
|
195
|
+
|
|
196
|
+
addCommonOptions(program.command('start [path]').description('Open the interactive TUI and explore with commands')).action(async (startPath, options) => {
|
|
162
197
|
setPreserveConsoleLogs(false);
|
|
163
198
|
const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
|
|
164
199
|
await explorBot.start();
|
|
@@ -168,7 +203,7 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
|
|
|
168
203
|
addCommonOptions(
|
|
169
204
|
program
|
|
170
205
|
.command('explore [path]')
|
|
171
|
-
.description('
|
|
206
|
+
.description('Invent test scenarios for a page and run them')
|
|
172
207
|
.option('--sitemap <source>', 'Explore every page listed in a sitemap: file path, URL, or - for stdin (a piped stdin is read automatically)')
|
|
173
208
|
.option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, plus sort:priority|lastmod for a sitemap, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')
|
|
174
209
|
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
@@ -206,10 +241,65 @@ addCommonOptions(
|
|
|
206
241
|
}
|
|
207
242
|
});
|
|
208
243
|
|
|
209
|
-
addCommonOptions(
|
|
244
|
+
addCommonOptions(
|
|
245
|
+
program
|
|
246
|
+
.command('freesail [startUrl]')
|
|
247
|
+
.description('Roam the app on its own, moving on to new pages')
|
|
248
|
+
.option('--deep', 'Depth-first: prioritize newly discovered pages')
|
|
249
|
+
.option('--shallow', 'Breadth-first: pick globally least-visited page')
|
|
250
|
+
.option('--scope <prefix>', 'Restrict navigation to URL prefix')
|
|
251
|
+
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
252
|
+
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
|
|
253
|
+
.option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')
|
|
254
|
+
).action(async (startUrl, options) => {
|
|
255
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
|
|
256
|
+
await explorBot.start();
|
|
257
|
+
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}`, options.urlFilter && `--url-filter "${options.urlFilter}"`]
|
|
258
|
+
.filter(Boolean)
|
|
259
|
+
.join(' ');
|
|
260
|
+
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
261
|
+
const cmd = new FreesailCommand(explorBot);
|
|
262
|
+
try {
|
|
263
|
+
await cmd.execute(args);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
266
|
+
printPageContext(explorBot);
|
|
267
|
+
await showStatsAndExit(1);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
program.commandsGroup(GROUPS.steps);
|
|
272
|
+
|
|
273
|
+
addCommonOptions(program.command('research <url>').description('Research a page and print its UI map').option('--data', 'Include data extraction in research').option('--deep', 'Enable deep analysis (expand hidden elements)').option('--no-fix', 'Skip locator fix cycle (for debugging)')).action(
|
|
274
|
+
async (url, options) => {
|
|
275
|
+
try {
|
|
276
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
277
|
+
await explorBot.start();
|
|
278
|
+
|
|
279
|
+
const argParts: string[] = [url];
|
|
280
|
+
if (options.data) argParts.push('--data');
|
|
281
|
+
if (options.deep) argParts.push('--deep');
|
|
282
|
+
if (options.fix === false) argParts.push('--no-fix');
|
|
283
|
+
|
|
284
|
+
const { ResearchCommand } = await import('../src/commands/research-command.js');
|
|
285
|
+
await new ResearchCommand(explorBot).execute(argParts.join(' '));
|
|
286
|
+
|
|
287
|
+
await explorBot.stop();
|
|
288
|
+
await showStatsAndExit(0);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
291
|
+
await showStatsAndExit(1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
addCommonOptions(program.command('plan <path>').description('Write a test plan for a page'))
|
|
210
297
|
.option('-a, --append', 'Add tests to existing plan file')
|
|
211
298
|
.option('--style <style>', 'Planning style: normal, curious, psycho')
|
|
212
299
|
.option('--focus <feature>', 'Focus area for test planning')
|
|
300
|
+
.option('--tests <count>', 'Exact number of tests to generate')
|
|
301
|
+
.option('--min-tests <count>', 'Minimum number of tests to generate')
|
|
302
|
+
.option('--max-tests <count>', 'Maximum number of tests to generate')
|
|
213
303
|
.action(async (planPath, options) => {
|
|
214
304
|
try {
|
|
215
305
|
const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
|
|
@@ -217,70 +307,159 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
217
307
|
|
|
218
308
|
await explorBot.visit(planPath);
|
|
219
309
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
310
|
+
const args = ['--fresh'];
|
|
311
|
+
if (options.append) args[0] = '--append';
|
|
312
|
+
if (options.style) args.push(`--style ${options.style}`);
|
|
313
|
+
if (options.focus) args.push(`--focus "${options.focus}"`);
|
|
314
|
+
if (options.tests) args.push(`--tests ${options.tests}`);
|
|
315
|
+
if (options.minTests) args.push(`--min-tests ${options.minTests}`);
|
|
316
|
+
if (options.maxTests) args.push(`--max-tests ${options.maxTests}`);
|
|
317
|
+
await new PlanCommand(explorBot).execute(args.join(' '));
|
|
227
318
|
|
|
228
|
-
await explorBot.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
319
|
+
await explorBot.stop();
|
|
320
|
+
await showStatsAndExit(0);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
323
|
+
await showStatsAndExit(1);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
232
326
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
327
|
+
addCommonOptions(program.command('test <planfile> [index]').description('Run tests from a plan file; index: 1, 1,3, 1-5, *').option('--grep <pattern>', 'Run tests matching pattern').option('--from-plan <file>', 'Load plan file when the first argument is a test index')).action(
|
|
328
|
+
async (planfile, index, options) => {
|
|
329
|
+
try {
|
|
330
|
+
let planfileArg = planfile;
|
|
331
|
+
let indexArg = index;
|
|
332
|
+
if (options.fromPlan) {
|
|
333
|
+
planfileArg = options.fromPlan;
|
|
334
|
+
indexArg = planfile;
|
|
238
335
|
}
|
|
239
336
|
|
|
240
|
-
const
|
|
241
|
-
if (suite && suite.automatedTestCount > 0) {
|
|
242
|
-
const names = suite.getAutomatedTestNames();
|
|
243
|
-
console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
|
|
244
|
-
for (let i = 0; i < names.length; i++) {
|
|
245
|
-
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
337
|
+
const peeked = Plan.loadFromFile(planfileArg);
|
|
248
338
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (let i = 0; i < plan.tests.length; i++) {
|
|
252
|
-
const t = plan.tests[i];
|
|
253
|
-
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
339
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
340
|
+
await explorBot.start();
|
|
256
341
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const relPlan = relativeToCwd(savedPath);
|
|
265
|
-
sections.push({
|
|
266
|
-
label: 'Plan',
|
|
267
|
-
path: savedPath,
|
|
268
|
-
commands: [
|
|
269
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1${cliSuffix}` },
|
|
270
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *${cliSuffix}` },
|
|
271
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3${cliSuffix}` },
|
|
272
|
-
],
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
const automatedFiles = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
|
|
276
|
-
if (automatedFiles.length > 0) {
|
|
277
|
-
const commands = automatedFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
|
|
278
|
-
sections.push({
|
|
279
|
-
label: `Automated tests (${automatedFiles.length})`,
|
|
280
|
-
commands,
|
|
281
|
-
});
|
|
342
|
+
const plan = explorBot.loadPlan(planfileArg);
|
|
343
|
+
const pending = plan.getPendingTests();
|
|
344
|
+
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
345
|
+
|
|
346
|
+
const startUrl = plan.startUrl;
|
|
347
|
+
if (!startUrl) {
|
|
348
|
+
throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
|
|
282
349
|
}
|
|
283
|
-
|
|
350
|
+
|
|
351
|
+
log(`Navigating to ${startUrl}`);
|
|
352
|
+
await explorBot.visit(startUrl);
|
|
353
|
+
|
|
354
|
+
let args = '*';
|
|
355
|
+
if (indexArg) args = indexArg;
|
|
356
|
+
else if (options.grep) args = options.grep;
|
|
357
|
+
|
|
358
|
+
const { TestCommand } = await import('../src/commands/test-command.js');
|
|
359
|
+
const cmd = new TestCommand(explorBot);
|
|
360
|
+
await cmd.execute(args);
|
|
361
|
+
|
|
362
|
+
await explorBot.printSessionAnalysis();
|
|
363
|
+
await explorBot.stop();
|
|
364
|
+
await showStatsAndExit(0);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
367
|
+
await showStatsAndExit(1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests, healing failures with AI')).action(async (filename, index, options) => {
|
|
373
|
+
try {
|
|
374
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
375
|
+
await explorBot.start();
|
|
376
|
+
const { RerunCommand } = await import('../src/commands/rerun-command.js');
|
|
377
|
+
const cmd = new RerunCommand(explorBot);
|
|
378
|
+
const args = index ? `${filename} ${index}` : filename;
|
|
379
|
+
await cmd.execute(args);
|
|
380
|
+
await explorBot.stop();
|
|
381
|
+
await showStatsAndExit(0);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
384
|
+
await showStatsAndExit(1);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
addCommonOptions(program.command('navigate <url>').description('Reach a URL with the AI Navigator; exits 1 if unreachable')).action(async (url, options) => {
|
|
389
|
+
try {
|
|
390
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
391
|
+
await explorBot.start();
|
|
392
|
+
|
|
393
|
+
const { NavigateCommand } = await import('../src/commands/navigate-command.js');
|
|
394
|
+
await new NavigateCommand(explorBot).execute(url);
|
|
395
|
+
|
|
396
|
+
await explorBot.stop();
|
|
397
|
+
await showStatsAndExit(0);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
400
|
+
await showStatsAndExit(1);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
addCommonOptions(
|
|
405
|
+
program.command('drill <url>').alias('driller').description('Try every component on a page to learn how it works').option('--save-knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')
|
|
406
|
+
).action(async (url, options) => {
|
|
407
|
+
try {
|
|
408
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
409
|
+
await explorBot.start();
|
|
410
|
+
|
|
411
|
+
await explorBot.visit(url);
|
|
412
|
+
|
|
413
|
+
const plan = await explorBot.agentDriller().drill({
|
|
414
|
+
knowledgePath: options.saveKnowledge,
|
|
415
|
+
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
416
|
+
interactive: false,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
console.log(`\nDrill completed: ${plan.tests.length} components`);
|
|
420
|
+
console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
|
|
421
|
+
console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
|
|
422
|
+
|
|
423
|
+
await explorBot.stop();
|
|
424
|
+
await showStatsAndExit(0);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
427
|
+
await showStatsAndExit(1);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
program
|
|
432
|
+
.command('context <url>')
|
|
433
|
+
.description('Print what agents see on a page: headings, knowledge, experience, elements')
|
|
434
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
435
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
436
|
+
.option('--session [file]', 'Save/restore browser session from file')
|
|
437
|
+
.option('--full', 'Include HTML and all data')
|
|
438
|
+
.option('--compact', 'Compact view with summaries')
|
|
439
|
+
.option('--attached', 'Only auto-attached sections (default)')
|
|
440
|
+
.option('--visual', 'Annotate elements on screenshot and print screenshot path')
|
|
441
|
+
.option('--screenshot', 'Alias for --visual')
|
|
442
|
+
.action(async (url, options) => {
|
|
443
|
+
try {
|
|
444
|
+
const mainOptions: ExplorBotOptions = {
|
|
445
|
+
path: options.path,
|
|
446
|
+
config: options.config,
|
|
447
|
+
headless: true,
|
|
448
|
+
session: options.session,
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const explorBot = new ExplorBot(mainOptions);
|
|
452
|
+
await explorBot.start();
|
|
453
|
+
|
|
454
|
+
await explorBot.visit(url);
|
|
455
|
+
|
|
456
|
+
const { ContextCommand } = await import('../src/commands/context-command.js');
|
|
457
|
+
const argParts: string[] = [];
|
|
458
|
+
if (options.full) argParts.push('--full');
|
|
459
|
+
else if (options.compact) argParts.push('--compact');
|
|
460
|
+
else argParts.push('--attached');
|
|
461
|
+
if (options.visual || options.screenshot) argParts.push('--visual');
|
|
462
|
+
await new ContextCommand(explorBot).execute(argParts.join(' '));
|
|
284
463
|
|
|
285
464
|
await explorBot.stop();
|
|
286
465
|
await showStatsAndExit(0);
|
|
@@ -290,7 +469,50 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
290
469
|
}
|
|
291
470
|
});
|
|
292
471
|
|
|
293
|
-
addCommonOptions(program.command('
|
|
472
|
+
addCommonOptions(program.command('shell <url> <command>').description('Run one CodeceptJS command on a page')).action(async (url, command, options) => {
|
|
473
|
+
try {
|
|
474
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
475
|
+
await explorBot.start();
|
|
476
|
+
await explorBot.visit(url);
|
|
477
|
+
|
|
478
|
+
const action = explorBot.getExplorer().action();
|
|
479
|
+
await action.execute(command);
|
|
480
|
+
|
|
481
|
+
log('Command executed successfully');
|
|
482
|
+
const state = explorBot.stateManager().getCurrentState();
|
|
483
|
+
if (state) log(`URL: ${state.url}`);
|
|
484
|
+
|
|
485
|
+
await explorBot.stop();
|
|
486
|
+
await showStatsAndExit(0);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
489
|
+
await showStatsAndExit(1);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
program.commandsGroup(GROUPS.results);
|
|
494
|
+
|
|
495
|
+
program
|
|
496
|
+
.command('plans [plan]')
|
|
497
|
+
.description('List saved plans, or the tests of one plan')
|
|
498
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
499
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
500
|
+
.action(async (plan, options) => {
|
|
501
|
+
try {
|
|
502
|
+
await ConfigParser.getInstance().loadConfig({
|
|
503
|
+
config: options.config,
|
|
504
|
+
path: options.path || process.cwd(),
|
|
505
|
+
});
|
|
506
|
+
const explorBot = new ExplorBot({ path: options.path });
|
|
507
|
+
const { PlansCommand } = await import('../src/commands/plans-command.js');
|
|
508
|
+
await new PlansCommand(explorBot).execute(plan || '');
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
addCommonOptions(program.command('plan:load <planfile> [index]').description('Show the tests of a plan file, or one test in detail')).action(async (planfile: string, index: string | undefined) => {
|
|
294
516
|
try {
|
|
295
517
|
const plan = Plan.loadFromFile(planfile);
|
|
296
518
|
if (!plan?.filePath) {
|
|
@@ -356,59 +578,12 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
356
578
|
}
|
|
357
579
|
});
|
|
358
580
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
planfileArg = options.fromPlan;
|
|
366
|
-
indexArg = planfile;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const peeked = Plan.loadFromFile(planfileArg);
|
|
370
|
-
|
|
371
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
372
|
-
await explorBot.start();
|
|
373
|
-
|
|
374
|
-
let plan = peeked;
|
|
375
|
-
if (plan) explorBot.setCurrentPlan(plan);
|
|
376
|
-
if (!plan) plan = explorBot.loadPlan(planfileArg);
|
|
377
|
-
const pending = plan.getPendingTests();
|
|
378
|
-
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
379
|
-
|
|
380
|
-
const startUrl = plan.startUrl;
|
|
381
|
-
if (!startUrl) {
|
|
382
|
-
throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
log(`Navigating to ${startUrl}`);
|
|
386
|
-
await explorBot.visit(startUrl);
|
|
387
|
-
|
|
388
|
-
let args = '*';
|
|
389
|
-
if (indexArg) args = indexArg;
|
|
390
|
-
else if (options.grep) args = options.grep;
|
|
391
|
-
|
|
392
|
-
const { TestCommand } = await import('../src/commands/test-command.js');
|
|
393
|
-
const cmd = new TestCommand(explorBot);
|
|
394
|
-
await cmd.execute(args);
|
|
395
|
-
|
|
396
|
-
await explorBot.printSessionAnalysis();
|
|
397
|
-
await explorBot.stop();
|
|
398
|
-
await showStatsAndExit(0);
|
|
399
|
-
} catch (error) {
|
|
400
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
401
|
-
await showStatsAndExit(1);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
program
|
|
407
|
-
.command('runs [file]')
|
|
408
|
-
.description('List generated test files, or show steps for a specific file')
|
|
409
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
410
|
-
.option('-c, --config <path>', 'Path to configuration file')
|
|
411
|
-
.action(async (file, options) => {
|
|
581
|
+
program
|
|
582
|
+
.command('runs [file]')
|
|
583
|
+
.description('List generated test files, or the steps of one file')
|
|
584
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
585
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
586
|
+
.action(async (file, options) => {
|
|
412
587
|
try {
|
|
413
588
|
await ConfigParser.getInstance().loadConfig({
|
|
414
589
|
config: options.config,
|
|
@@ -424,19 +599,27 @@ program
|
|
|
424
599
|
});
|
|
425
600
|
|
|
426
601
|
program
|
|
427
|
-
.command('
|
|
428
|
-
.description('List
|
|
602
|
+
.command('experience [filter] [index]')
|
|
603
|
+
.description('List stored experience by URL; pass a tag (A.1) or index to expand one')
|
|
429
604
|
.option('-p, --path <path>', 'Working directory path')
|
|
430
605
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
431
|
-
.
|
|
606
|
+
.option('--recent', 'Only files modified within the last 30 days')
|
|
607
|
+
.option('--old', 'Only files modified more than 30 days ago')
|
|
608
|
+
.action(async (filter, index, options) => {
|
|
432
609
|
try {
|
|
433
610
|
await ConfigParser.getInstance().loadConfig({
|
|
434
611
|
config: options.config,
|
|
435
612
|
path: options.path || process.cwd(),
|
|
436
613
|
});
|
|
614
|
+
const { ExperienceCommand } = await import('../src/commands/experience-command.js');
|
|
437
615
|
const explorBot = new ExplorBot({ path: options.path });
|
|
438
|
-
const
|
|
439
|
-
|
|
616
|
+
const command = new ExperienceCommand(explorBot);
|
|
617
|
+
const flags: string[] = [];
|
|
618
|
+
if (options.recent) flags.push('--recent');
|
|
619
|
+
if (options.old) flags.push('--old');
|
|
620
|
+
const args = [...flags, filter, index].filter(Boolean).join(' ');
|
|
621
|
+
await command.execute(args);
|
|
622
|
+
command.printSuggestions();
|
|
440
623
|
} catch (error) {
|
|
441
624
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
442
625
|
process.exit(1);
|
|
@@ -444,169 +627,29 @@ program
|
|
|
444
627
|
});
|
|
445
628
|
|
|
446
629
|
program
|
|
447
|
-
.command('
|
|
448
|
-
.description('List
|
|
449
|
-
.action(async () => {
|
|
450
|
-
const { SitesCommand } = await import('../src/commands/sites-command.js');
|
|
451
|
-
await new SitesCommand(new ExplorBot()).execute('');
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
|
|
455
|
-
try {
|
|
456
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
457
|
-
await explorBot.start();
|
|
458
|
-
const { RerunCommand } = await import('../src/commands/rerun-command.js');
|
|
459
|
-
const cmd = new RerunCommand(explorBot);
|
|
460
|
-
const args = index ? `${filename} ${index}` : filename;
|
|
461
|
-
await cmd.execute(args);
|
|
462
|
-
await explorBot.stop();
|
|
463
|
-
await showStatsAndExit(0);
|
|
464
|
-
} catch (error) {
|
|
465
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
466
|
-
await showStatsAndExit(1);
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
addCommonOptions(
|
|
471
|
-
program
|
|
472
|
-
.command('freesail [startUrl]')
|
|
473
|
-
.description('Continuously explore and navigate to new pages autonomously')
|
|
474
|
-
.option('--deep', 'Depth-first: prioritize newly discovered pages')
|
|
475
|
-
.option('--shallow', 'Breadth-first: pick globally least-visited page')
|
|
476
|
-
.option('--scope <prefix>', 'Restrict navigation to URL prefix')
|
|
477
|
-
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
478
|
-
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
|
|
479
|
-
.option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')
|
|
480
|
-
).action(async (startUrl, options) => {
|
|
481
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
|
|
482
|
-
await explorBot.start();
|
|
483
|
-
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}`, options.urlFilter && `--url-filter "${options.urlFilter}"`]
|
|
484
|
-
.filter(Boolean)
|
|
485
|
-
.join(' ');
|
|
486
|
-
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
487
|
-
const cmd = new FreesailCommand(explorBot);
|
|
488
|
-
try {
|
|
489
|
-
await cmd.execute(args);
|
|
490
|
-
} catch (error) {
|
|
491
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
492
|
-
printPageContext(explorBot);
|
|
493
|
-
await showStatsAndExit(1);
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
program
|
|
498
|
-
.command('config [url]')
|
|
499
|
-
.description('Show models, config file and paths used by this run')
|
|
500
|
-
.option('-c, --config <path>', 'Path to configuration file')
|
|
630
|
+
.command('knows [url]')
|
|
631
|
+
.description('List knowledge URLs, or the knowledge matching a URL')
|
|
501
632
|
.option('-p, --path <path>', 'Working directory path')
|
|
502
|
-
.option('--json', 'Print the resolved config as JSON')
|
|
503
633
|
.action(async (url, options) => {
|
|
504
|
-
setQuietMode(!isVerboseMode());
|
|
505
|
-
const { ConfigCommand } = await import('../src/commands/config-command.js');
|
|
506
634
|
try {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
510
|
-
process.exit(1);
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
RecommendedModelsCommand.register(program);
|
|
515
|
-
|
|
516
|
-
program
|
|
517
|
-
.command('init')
|
|
518
|
-
.description('Initialize configuration for a project or for this machine')
|
|
519
|
-
.option('-c, --config-path <path>', 'Path for the config file')
|
|
520
|
-
.option('-f, --force', 'Overwrite existing config file')
|
|
521
|
-
.option('-p, --path <path>', 'Working directory for initialization')
|
|
522
|
-
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
523
|
-
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
524
|
-
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
525
|
-
.option('--url <url>', 'Web application URL written to web.url in the local config')
|
|
526
|
-
.action(async (options) => {
|
|
527
|
-
try {
|
|
528
|
-
const { runInit } = await import('../src/commands/init-command.js');
|
|
529
|
-
await runInit({
|
|
530
|
-
configPath: options.configPath,
|
|
531
|
-
force: options.force,
|
|
532
|
-
path: options.path,
|
|
533
|
-
global: options.global,
|
|
534
|
-
provider: options.provider,
|
|
535
|
-
apiKey: options.apiKey,
|
|
536
|
-
url: options.url,
|
|
635
|
+
await ConfigParser.getInstance().loadConfig({
|
|
636
|
+
path: options.path || process.cwd(),
|
|
537
637
|
});
|
|
638
|
+
const { KnowsCommand } = await import('../src/commands/knows-command.js');
|
|
639
|
+
const explorBot = new ExplorBot({ path: options.path });
|
|
640
|
+
const command = new KnowsCommand(explorBot);
|
|
641
|
+
await command.execute(url || '');
|
|
538
642
|
} catch (error) {
|
|
539
643
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
540
644
|
process.exit(1);
|
|
541
645
|
}
|
|
542
646
|
});
|
|
543
647
|
|
|
544
|
-
program
|
|
545
|
-
.command('clean [target]')
|
|
546
|
-
.description('Clean files: states, research, plans, tests, experiences, output (default: output + experiences)')
|
|
547
|
-
.option('-p, --path <path>', 'Custom path to clean')
|
|
548
|
-
.action(async (target, options) => {
|
|
549
|
-
const customPath = options.path;
|
|
550
|
-
const originalCwd = process.cwd();
|
|
551
|
-
const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
|
|
552
|
-
|
|
553
|
-
const targets: Record<string, { description: string; dir: string }> = {
|
|
554
|
-
states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
|
|
555
|
-
research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
|
|
556
|
-
plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
|
|
557
|
-
tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
|
|
558
|
-
experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
|
|
559
|
-
output: { description: 'all output files', dir: path.join(basePath, 'output') },
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
function cleanDirectoryContents(dirPath: string): number {
|
|
563
|
-
let count = 0;
|
|
564
|
-
for (const item of fs.readdirSync(dirPath)) {
|
|
565
|
-
const itemPath = path.join(dirPath, item);
|
|
566
|
-
if (fs.statSync(itemPath).isDirectory()) {
|
|
567
|
-
count += cleanDirectoryContents(itemPath);
|
|
568
|
-
fs.rmSync(itemPath, { recursive: true });
|
|
569
|
-
} else {
|
|
570
|
-
fs.unlinkSync(itemPath);
|
|
571
|
-
count++;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return count;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function cleanTarget(name: string): void {
|
|
578
|
-
const t = targets[name];
|
|
579
|
-
if (!fs.existsSync(t.dir)) {
|
|
580
|
-
console.log(`${name}: nothing to clean (${t.dir} not found)`);
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
const count = cleanDirectoryContents(t.dir);
|
|
584
|
-
console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
try {
|
|
588
|
-
if (target && !targets[target]) {
|
|
589
|
-
console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
|
|
590
|
-
process.exit(1);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (!target) {
|
|
594
|
-
cleanTarget('output');
|
|
595
|
-
cleanTarget('experiences');
|
|
596
|
-
} else {
|
|
597
|
-
cleanTarget(target);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
console.log('Cleanup completed successfully!');
|
|
601
|
-
} catch (error) {
|
|
602
|
-
console.error('Failed to clean:', error);
|
|
603
|
-
process.exit(1);
|
|
604
|
-
}
|
|
605
|
-
});
|
|
648
|
+
program.commandsGroup(GROUPS.knowledge);
|
|
606
649
|
|
|
607
650
|
program
|
|
608
651
|
.command('learn [url] [description]')
|
|
609
|
-
.description('Add knowledge for
|
|
652
|
+
.description('Add knowledge for a URL')
|
|
610
653
|
.option('-p, --path <path>', 'Working directory path')
|
|
611
654
|
.option('--replace', 'Replace existing knowledge for this URL instead of appending')
|
|
612
655
|
.action(async (url, description, options) => {
|
|
@@ -636,28 +679,9 @@ program
|
|
|
636
679
|
}
|
|
637
680
|
});
|
|
638
681
|
|
|
639
|
-
program
|
|
640
|
-
.command('knows [url]')
|
|
641
|
-
.description('List all knowledge URLs or show matching knowledge for a URL')
|
|
642
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
643
|
-
.action(async (url, options) => {
|
|
644
|
-
try {
|
|
645
|
-
await ConfigParser.getInstance().loadConfig({
|
|
646
|
-
path: options.path || process.cwd(),
|
|
647
|
-
});
|
|
648
|
-
const { KnowsCommand } = await import('../src/commands/knows-command.js');
|
|
649
|
-
const explorBot = new ExplorBot({ path: options.path });
|
|
650
|
-
const command = new KnowsCommand(explorBot);
|
|
651
|
-
await command.execute(url || '');
|
|
652
|
-
} catch (error) {
|
|
653
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
654
|
-
process.exit(1);
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
|
|
658
682
|
program
|
|
659
683
|
.command('compact [target]')
|
|
660
|
-
.description('Compact
|
|
684
|
+
.description('Compact experience files; pass a filename or URL substring to narrow')
|
|
661
685
|
.option('--dry-run', 'Preview without running AI or writing files')
|
|
662
686
|
.option('--no-merge', 'Skip the cross-URL merge step when compacting all')
|
|
663
687
|
.option('-p, --path <path>', 'Working directory path')
|
|
@@ -686,162 +710,112 @@ program
|
|
|
686
710
|
});
|
|
687
711
|
|
|
688
712
|
program
|
|
689
|
-
.command('
|
|
690
|
-
.description('
|
|
713
|
+
.command('add-rule [agent] [name]')
|
|
714
|
+
.description('Create a rule file for an agent')
|
|
715
|
+
.option('--url <pattern>', 'URL pattern for this rule')
|
|
691
716
|
.option('-p, --path <path>', 'Working directory path')
|
|
692
|
-
.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
path: options.path || process.cwd(),
|
|
717
|
+
.action(async (agent, name, options) => {
|
|
718
|
+
if (options.path) process.chdir(path.resolve(options.path));
|
|
719
|
+
|
|
720
|
+
if (agent && name) {
|
|
721
|
+
const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
|
|
722
|
+
const result = AddRuleCommand.createRuleFile(agent, name, {
|
|
723
|
+
urlPattern: options.url,
|
|
700
724
|
});
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
725
|
+
process.exit(result ? 0 : 1);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const AddRule = (await import('../src/components/AddRule.js')).default;
|
|
729
|
+
render(
|
|
730
|
+
React.createElement(AddRule, {
|
|
731
|
+
initialAgent: agent || '',
|
|
732
|
+
initialName: name || '',
|
|
733
|
+
}),
|
|
734
|
+
{
|
|
735
|
+
exitOnCtrlC: false,
|
|
736
|
+
patchConsole: false,
|
|
737
|
+
}
|
|
738
|
+
);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
program
|
|
742
|
+
.command('extract-rules <agent>')
|
|
743
|
+
.description("Copy an agent's built-in rules and planning styles out to customize")
|
|
744
|
+
.option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
|
|
745
|
+
.action(async (agent, options) => {
|
|
746
|
+
try {
|
|
747
|
+
const { RulesLoader } = await import('../src/utils/rules-loader.js');
|
|
748
|
+
const targetDir = options.dir || path.resolve(`./rules/${agent}`);
|
|
749
|
+
const extracted = RulesLoader.extractRules(agent, targetDir);
|
|
750
|
+
if (extracted.length === 0) {
|
|
751
|
+
console.log('All rule files already exist in target directory.');
|
|
752
|
+
} else {
|
|
753
|
+
console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
|
|
754
|
+
}
|
|
710
755
|
} catch (error) {
|
|
711
756
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
712
757
|
process.exit(1);
|
|
713
758
|
}
|
|
714
759
|
});
|
|
715
760
|
|
|
716
|
-
|
|
717
|
-
async (url, options) => {
|
|
718
|
-
try {
|
|
719
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
720
|
-
await explorBot.start();
|
|
721
|
-
|
|
722
|
-
const argParts: string[] = [url];
|
|
723
|
-
if (options.data) argParts.push('--data');
|
|
724
|
-
if (options.deep) argParts.push('--deep');
|
|
725
|
-
if (options.fix === false) argParts.push('--no-fix');
|
|
726
|
-
|
|
727
|
-
const { ResearchCommand } = await import('../src/commands/research-command.js');
|
|
728
|
-
await new ResearchCommand(explorBot).execute(argParts.join(' '));
|
|
761
|
+
program.commandsGroup(GROUPS.setup);
|
|
729
762
|
|
|
730
|
-
|
|
731
|
-
|
|
763
|
+
program
|
|
764
|
+
.command('init')
|
|
765
|
+
.description('Initialize configuration for a project or for this machine')
|
|
766
|
+
.option('-c, --config-path <path>', 'Path for the config file')
|
|
767
|
+
.option('-f, --force', 'Overwrite existing config file')
|
|
768
|
+
.option('-p, --path <path>', 'Working directory for initialization')
|
|
769
|
+
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
770
|
+
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
771
|
+
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
772
|
+
.option('--url <url>', 'Web application URL written to web.url in the local config')
|
|
773
|
+
.action(async (options) => {
|
|
774
|
+
try {
|
|
775
|
+
const { runInit } = await import('../src/commands/init-command.js');
|
|
776
|
+
await runInit({
|
|
777
|
+
configPath: options.configPath,
|
|
778
|
+
force: options.force,
|
|
779
|
+
path: options.path,
|
|
780
|
+
global: options.global,
|
|
781
|
+
provider: options.provider,
|
|
782
|
+
apiKey: options.apiKey,
|
|
783
|
+
url: options.url,
|
|
784
|
+
});
|
|
732
785
|
} catch (error) {
|
|
733
786
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
734
|
-
|
|
787
|
+
process.exit(1);
|
|
735
788
|
}
|
|
736
|
-
}
|
|
737
|
-
);
|
|
738
|
-
|
|
739
|
-
addCommonOptions(program.command('navigate <url>').description('Navigate to a URL using the AI Navigator. Exits 0 if reachable, 1 otherwise.')).action(async (url, options) => {
|
|
740
|
-
try {
|
|
741
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
742
|
-
await explorBot.start();
|
|
743
|
-
|
|
744
|
-
const { NavigateCommand } = await import('../src/commands/navigate-command.js');
|
|
745
|
-
await new NavigateCommand(explorBot).execute(url);
|
|
746
|
-
|
|
747
|
-
await explorBot.stop();
|
|
748
|
-
await showStatsAndExit(0);
|
|
749
|
-
} catch (error) {
|
|
750
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
751
|
-
await showStatsAndExit(1);
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
addCommonOptions(
|
|
756
|
-
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')
|
|
757
|
-
).action(async (url, options) => {
|
|
758
|
-
try {
|
|
759
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
760
|
-
await explorBot.start();
|
|
761
|
-
|
|
762
|
-
await explorBot.visit(url);
|
|
763
|
-
|
|
764
|
-
const plan = await explorBot.agentDriller().drill({
|
|
765
|
-
knowledgePath: options.saveKnowledge,
|
|
766
|
-
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
767
|
-
interactive: false,
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
console.log(`\nDrill completed: ${plan.tests.length} components`);
|
|
771
|
-
console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
|
|
772
|
-
console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
|
|
773
|
-
|
|
774
|
-
await explorBot.stop();
|
|
775
|
-
await showStatsAndExit(0);
|
|
776
|
-
} catch (error) {
|
|
777
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
778
|
-
await showStatsAndExit(1);
|
|
779
|
-
}
|
|
780
|
-
});
|
|
789
|
+
});
|
|
781
790
|
|
|
782
791
|
program
|
|
783
|
-
.command('
|
|
784
|
-
.description('
|
|
785
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
792
|
+
.command('config [url]')
|
|
793
|
+
.description('Show models, config file and paths used by this run')
|
|
786
794
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
787
|
-
.option('--
|
|
788
|
-
.option('--
|
|
789
|
-
.option('--compact', 'Compact view with summaries')
|
|
790
|
-
.option('--attached', 'Only auto-attached sections (default)')
|
|
791
|
-
.option('--visual', 'Annotate elements on screenshot and print screenshot path')
|
|
792
|
-
.option('--screenshot', 'Alias for --visual')
|
|
795
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
796
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
793
797
|
.action(async (url, options) => {
|
|
798
|
+
setQuietMode(!isVerboseMode());
|
|
799
|
+
const { ConfigCommand } = await import('../src/commands/config-command.js');
|
|
794
800
|
try {
|
|
795
|
-
|
|
796
|
-
path: options.path,
|
|
797
|
-
config: options.config,
|
|
798
|
-
headless: true,
|
|
799
|
-
session: options.session,
|
|
800
|
-
};
|
|
801
|
-
|
|
802
|
-
const explorBot = new ExplorBot(mainOptions);
|
|
803
|
-
await explorBot.start();
|
|
804
|
-
|
|
805
|
-
await explorBot.visit(url);
|
|
806
|
-
|
|
807
|
-
const { ContextCommand } = await import('../src/commands/context-command.js');
|
|
808
|
-
const argParts: string[] = [];
|
|
809
|
-
if (options.full) argParts.push('--full');
|
|
810
|
-
else if (options.compact) argParts.push('--compact');
|
|
811
|
-
else argParts.push('--attached');
|
|
812
|
-
if (options.visual || options.screenshot) argParts.push('--visual');
|
|
813
|
-
await new ContextCommand(explorBot).execute(argParts.join(' '));
|
|
814
|
-
|
|
815
|
-
await explorBot.stop();
|
|
816
|
-
await showStatsAndExit(0);
|
|
801
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
817
802
|
} catch (error) {
|
|
818
|
-
console.error(
|
|
819
|
-
|
|
803
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
804
|
+
process.exit(1);
|
|
820
805
|
}
|
|
821
806
|
});
|
|
822
807
|
|
|
823
|
-
|
|
824
|
-
try {
|
|
825
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
826
|
-
await explorBot.start();
|
|
827
|
-
await explorBot.visit(url);
|
|
828
|
-
|
|
829
|
-
const action = explorBot.getExplorer().action();
|
|
830
|
-
await action.execute(command);
|
|
831
|
-
|
|
832
|
-
log('Command executed successfully');
|
|
833
|
-
const state = explorBot.stateManager().getCurrentState();
|
|
834
|
-
if (state) log(`URL: ${state.url}`);
|
|
808
|
+
RecommendedModelsCommand.register(program);
|
|
835
809
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
await
|
|
841
|
-
|
|
842
|
-
});
|
|
810
|
+
program
|
|
811
|
+
.command('sites')
|
|
812
|
+
.description('List sites registered in the global installation')
|
|
813
|
+
.action(async () => {
|
|
814
|
+
const { SitesCommand } = await import('../src/commands/sites-command.js');
|
|
815
|
+
await new SitesCommand(new ExplorBot()).execute('');
|
|
816
|
+
});
|
|
843
817
|
|
|
844
|
-
const browserCmd = program.command('browser').description('
|
|
818
|
+
const browserCmd = program.command('browser').description('Start, stop or check a persistent browser server');
|
|
845
819
|
|
|
846
820
|
browserCmd
|
|
847
821
|
.command('start')
|
|
@@ -920,80 +894,119 @@ browserCmd
|
|
|
920
894
|
});
|
|
921
895
|
|
|
922
896
|
program
|
|
923
|
-
.command('
|
|
924
|
-
.description('
|
|
925
|
-
.option('-
|
|
926
|
-
.action(async (
|
|
897
|
+
.command('clean [target]')
|
|
898
|
+
.description('Delete states, research, plans, tests, experiences or output (default: output + experiences)')
|
|
899
|
+
.option('-p, --path <path>', 'Custom path to clean')
|
|
900
|
+
.action(async (target, options) => {
|
|
901
|
+
const customPath = options.path;
|
|
902
|
+
const originalCwd = process.cwd();
|
|
903
|
+
const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
|
|
904
|
+
|
|
905
|
+
const targets: Record<string, { description: string; dir: string }> = {
|
|
906
|
+
states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
|
|
907
|
+
research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
|
|
908
|
+
plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
|
|
909
|
+
tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
|
|
910
|
+
experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
|
|
911
|
+
output: { description: 'all output files', dir: path.join(basePath, 'output') },
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
function cleanDirectoryContents(dirPath: string): number {
|
|
915
|
+
let count = 0;
|
|
916
|
+
for (const item of fs.readdirSync(dirPath)) {
|
|
917
|
+
const itemPath = path.join(dirPath, item);
|
|
918
|
+
if (fs.statSync(itemPath).isDirectory()) {
|
|
919
|
+
count += cleanDirectoryContents(itemPath);
|
|
920
|
+
fs.rmSync(itemPath, { recursive: true });
|
|
921
|
+
} else {
|
|
922
|
+
fs.unlinkSync(itemPath);
|
|
923
|
+
count++;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return count;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function cleanTarget(name: string): void {
|
|
930
|
+
const t = targets[name];
|
|
931
|
+
if (!fs.existsSync(t.dir)) {
|
|
932
|
+
console.log(`${name}: nothing to clean (${t.dir} not found)`);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const count = cleanDirectoryContents(t.dir);
|
|
936
|
+
console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
|
|
937
|
+
}
|
|
938
|
+
|
|
927
939
|
try {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
940
|
+
if (target && !targets[target]) {
|
|
941
|
+
console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (!target) {
|
|
946
|
+
cleanTarget('output');
|
|
947
|
+
cleanTarget('experiences');
|
|
933
948
|
} else {
|
|
934
|
-
|
|
949
|
+
cleanTarget(target);
|
|
935
950
|
}
|
|
951
|
+
|
|
952
|
+
console.log('Cleanup completed successfully!');
|
|
936
953
|
} catch (error) {
|
|
937
|
-
console.error('Failed
|
|
954
|
+
console.error('Failed to clean:', error);
|
|
938
955
|
process.exit(1);
|
|
939
956
|
}
|
|
940
957
|
});
|
|
941
958
|
|
|
942
|
-
program
|
|
943
|
-
.command('add-rule [agent] [name]')
|
|
944
|
-
.description('Create a rule file for an agent')
|
|
945
|
-
.option('--url <pattern>', 'URL pattern for this rule')
|
|
946
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
947
|
-
.action(async (agent, name, options) => {
|
|
948
|
-
if (options.path) process.chdir(path.resolve(options.path));
|
|
949
|
-
|
|
950
|
-
if (agent && name) {
|
|
951
|
-
const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
|
|
952
|
-
const result = AddRuleCommand.createRuleFile(agent, name, {
|
|
953
|
-
urlPattern: options.url,
|
|
954
|
-
});
|
|
955
|
-
process.exit(result ? 0 : 1);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const AddRule = (await import('../src/components/AddRule.js')).default;
|
|
959
|
-
render(
|
|
960
|
-
React.createElement(AddRule, {
|
|
961
|
-
initialAgent: agent || '',
|
|
962
|
-
initialName: name || '',
|
|
963
|
-
}),
|
|
964
|
-
{
|
|
965
|
-
exitOnCtrlC: false,
|
|
966
|
-
patchConsole: false,
|
|
967
|
-
}
|
|
968
|
-
);
|
|
969
|
-
});
|
|
959
|
+
program.commandsGroup(GROUPS.tools);
|
|
970
960
|
|
|
971
|
-
import { createApiCommands } from '../boat/api-tester/src/cli.ts';
|
|
972
|
-
import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
|
|
973
|
-
import { createPrimaCommands } from '../boat/prima/src/cli.ts';
|
|
974
961
|
program.addCommand(createApiCommands('api'));
|
|
975
962
|
program.addCommand(createDocsCommands('docs'));
|
|
976
963
|
program.addCommand(createPrimaCommands('prima'));
|
|
977
964
|
|
|
978
965
|
const envHelp = () => {
|
|
979
966
|
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
980
|
-
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
|
|
967
|
+
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${chalk.green(v.name.padEnd(width))} ${v.description}`).join('\n');
|
|
981
968
|
|
|
982
969
|
return `
|
|
983
|
-
Environment variables
|
|
970
|
+
${chalk.yellow.bold('Environment variables:')}
|
|
984
971
|
Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
|
|
985
972
|
|
|
986
973
|
${rows}
|
|
987
974
|
|
|
988
975
|
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
976
|
+
|
|
977
|
+
${chalk.yellow.bold('Example:')}
|
|
978
|
+
EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
|
|
979
|
+
${cli} explore /login --max-tests 3
|
|
992
980
|
`;
|
|
993
981
|
};
|
|
994
982
|
|
|
995
|
-
program
|
|
996
|
-
|
|
983
|
+
program
|
|
984
|
+
.command('env')
|
|
985
|
+
.description('Show environment variables for running without a config file')
|
|
986
|
+
.helpGroup(GROUPS.setup)
|
|
987
|
+
.addHelpText('after', envHelp)
|
|
988
|
+
.action(() => console.log(envHelp()));
|
|
989
|
+
|
|
990
|
+
const examples = [
|
|
991
|
+
[`${cli} start`, 'interactive TUI, drive it with /commands'],
|
|
992
|
+
[`${cli} explore /login`, 'invent and run tests for one page'],
|
|
993
|
+
[`${cli} freesail`, 'roam the whole app on its own'],
|
|
994
|
+
[`${cli} plan /users`, 'write a test plan, then run it with test'],
|
|
995
|
+
];
|
|
996
|
+
const exampleWidth = Math.max(...examples.map(([command]) => command.length));
|
|
997
|
+
|
|
998
|
+
program.addHelpText(
|
|
999
|
+
'after',
|
|
1000
|
+
`
|
|
1001
|
+
${chalk.yellow.bold('Examples:')}
|
|
1002
|
+
${examples.map(([command, note]) => ` ${chalk.cyan(command.padEnd(exampleWidth))} ${chalk.dim(note)}`).join('\n')}
|
|
1003
|
+
|
|
1004
|
+
${chalk.dim(`Environment variables: ${cli} help env · JSON definitions for agents: ${cli} help-json [command...]`)}
|
|
1005
|
+
`
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
program.commandsGroup(GROUPS.help);
|
|
1009
|
+
program.helpCommand(true);
|
|
997
1010
|
HelpJsonCommand.register(program);
|
|
998
1011
|
|
|
999
1012
|
program.parse();
|