explorbot 0.5.10 → 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/bin/explorbot-cli.ts +494 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/dist/bin/explorbot-cli.js +455 -444
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- 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 +4 -4
- package/dist/src/commands/learn-command.js +0 -2
- 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 +2 -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 +6 -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/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 +5 -3
- package/src/commands/learn-command.ts +0 -2
- 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 +4 -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,13 +3,16 @@ 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
18
|
import { authOption, decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.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,11 +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
|
-
|
|
39
|
-
|
|
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);
|
|
40
71
|
|
|
41
72
|
process.on('uncaughtException', async (error) => {
|
|
42
73
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
@@ -64,10 +95,11 @@ interface CLIOptions {
|
|
|
64
95
|
headless?: boolean;
|
|
65
96
|
incognito?: boolean;
|
|
66
97
|
session?: string | boolean;
|
|
67
|
-
spec?: string;
|
|
98
|
+
spec?: string | string[];
|
|
68
99
|
}
|
|
69
100
|
|
|
70
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;
|
|
71
103
|
return {
|
|
72
104
|
from,
|
|
73
105
|
baseUrl: options.baseUrl,
|
|
@@ -78,7 +110,7 @@ function buildExplorBotOptions(from: string | undefined, options: CLIOptions): E
|
|
|
78
110
|
headless: options.headless,
|
|
79
111
|
incognito: options.incognito,
|
|
80
112
|
session: options.session,
|
|
81
|
-
applicationSpec:
|
|
113
|
+
applicationSpec: spec,
|
|
82
114
|
} as ExplorBotOptions;
|
|
83
115
|
}
|
|
84
116
|
|
|
@@ -92,7 +124,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
92
124
|
.option('-s, --show', 'Show browser window')
|
|
93
125
|
.option('--headless', 'Run browser in headless mode')
|
|
94
126
|
.option('--incognito', 'Run without recording experiences')
|
|
95
|
-
.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])
|
|
96
128
|
.option('--session [file]', 'Save/restore browser session from file');
|
|
97
129
|
}
|
|
98
130
|
|
|
@@ -159,7 +191,9 @@ async function showStatsAndExit(code: number): Promise<never> {
|
|
|
159
191
|
process.exit(code);
|
|
160
192
|
}
|
|
161
193
|
|
|
162
|
-
|
|
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) => {
|
|
163
197
|
setPreserveConsoleLogs(false);
|
|
164
198
|
const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
|
|
165
199
|
await explorBot.start();
|
|
@@ -169,7 +203,7 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
|
|
|
169
203
|
addCommonOptions(
|
|
170
204
|
program
|
|
171
205
|
.command('explore [path]')
|
|
172
|
-
.description('
|
|
206
|
+
.description('Invent test scenarios for a page and run them')
|
|
173
207
|
.option('--sitemap <source>', 'Explore every page listed in a sitemap: file path, URL, or - for stdin (a piped stdin is read automatically)')
|
|
174
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"')
|
|
175
209
|
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
@@ -207,10 +241,65 @@ addCommonOptions(
|
|
|
207
241
|
}
|
|
208
242
|
});
|
|
209
243
|
|
|
210
|
-
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'))
|
|
211
297
|
.option('-a, --append', 'Add tests to existing plan file')
|
|
212
298
|
.option('--style <style>', 'Planning style: normal, curious, psycho')
|
|
213
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')
|
|
214
303
|
.action(async (planPath, options) => {
|
|
215
304
|
try {
|
|
216
305
|
const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
|
|
@@ -218,70 +307,159 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
218
307
|
|
|
219
308
|
await explorBot.visit(planPath);
|
|
220
309
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
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(' '));
|
|
228
318
|
|
|
229
|
-
await explorBot.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
});
|
|
233
326
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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;
|
|
239
335
|
}
|
|
240
336
|
|
|
241
|
-
const
|
|
242
|
-
if (suite && suite.automatedTestCount > 0) {
|
|
243
|
-
const names = suite.getAutomatedTestNames();
|
|
244
|
-
console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
|
|
245
|
-
for (let i = 0; i < names.length; i++) {
|
|
246
|
-
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
337
|
+
const peeked = Plan.loadFromFile(planfileArg);
|
|
249
338
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
for (let i = 0; i < plan.tests.length; i++) {
|
|
253
|
-
const t = plan.tests[i];
|
|
254
|
-
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
339
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
340
|
+
await explorBot.start();
|
|
257
341
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const relPlan = relativeToCwd(savedPath);
|
|
266
|
-
sections.push({
|
|
267
|
-
label: 'Plan',
|
|
268
|
-
path: savedPath,
|
|
269
|
-
commands: [
|
|
270
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1${cliSuffix}` },
|
|
271
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *${cliSuffix}` },
|
|
272
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3${cliSuffix}` },
|
|
273
|
-
],
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
const automatedFiles = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
|
|
277
|
-
if (automatedFiles.length > 0) {
|
|
278
|
-
const commands = automatedFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
|
|
279
|
-
sections.push({
|
|
280
|
-
label: `Automated tests (${automatedFiles.length})`,
|
|
281
|
-
commands,
|
|
282
|
-
});
|
|
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.');
|
|
283
349
|
}
|
|
284
|
-
|
|
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(' '));
|
|
285
463
|
|
|
286
464
|
await explorBot.stop();
|
|
287
465
|
await showStatsAndExit(0);
|
|
@@ -291,7 +469,50 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
291
469
|
}
|
|
292
470
|
});
|
|
293
471
|
|
|
294
|
-
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) => {
|
|
295
516
|
try {
|
|
296
517
|
const plan = Plan.loadFromFile(planfile);
|
|
297
518
|
if (!plan?.filePath) {
|
|
@@ -357,59 +578,12 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
357
578
|
}
|
|
358
579
|
});
|
|
359
580
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
planfileArg = options.fromPlan;
|
|
367
|
-
indexArg = planfile;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const peeked = Plan.loadFromFile(planfileArg);
|
|
371
|
-
|
|
372
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
373
|
-
await explorBot.start();
|
|
374
|
-
|
|
375
|
-
let plan = peeked;
|
|
376
|
-
if (plan) explorBot.setCurrentPlan(plan);
|
|
377
|
-
if (!plan) plan = explorBot.loadPlan(planfileArg);
|
|
378
|
-
const pending = plan.getPendingTests();
|
|
379
|
-
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
380
|
-
|
|
381
|
-
const startUrl = plan.startUrl;
|
|
382
|
-
if (!startUrl) {
|
|
383
|
-
throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
log(`Navigating to ${startUrl}`);
|
|
387
|
-
await explorBot.visit(startUrl);
|
|
388
|
-
|
|
389
|
-
let args = '*';
|
|
390
|
-
if (indexArg) args = indexArg;
|
|
391
|
-
else if (options.grep) args = options.grep;
|
|
392
|
-
|
|
393
|
-
const { TestCommand } = await import('../src/commands/test-command.js');
|
|
394
|
-
const cmd = new TestCommand(explorBot);
|
|
395
|
-
await cmd.execute(args);
|
|
396
|
-
|
|
397
|
-
await explorBot.printSessionAnalysis();
|
|
398
|
-
await explorBot.stop();
|
|
399
|
-
await showStatsAndExit(0);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
402
|
-
await showStatsAndExit(1);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
program
|
|
408
|
-
.command('runs [file]')
|
|
409
|
-
.description('List generated test files, or show steps for a specific file')
|
|
410
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
411
|
-
.option('-c, --config <path>', 'Path to configuration file')
|
|
412
|
-
.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) => {
|
|
413
587
|
try {
|
|
414
588
|
await ConfigParser.getInstance().loadConfig({
|
|
415
589
|
config: options.config,
|
|
@@ -425,19 +599,27 @@ program
|
|
|
425
599
|
});
|
|
426
600
|
|
|
427
601
|
program
|
|
428
|
-
.command('
|
|
429
|
-
.description('List
|
|
602
|
+
.command('experience [filter] [index]')
|
|
603
|
+
.description('List stored experience by URL; pass a tag (A.1) or index to expand one')
|
|
430
604
|
.option('-p, --path <path>', 'Working directory path')
|
|
431
605
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
432
|
-
.
|
|
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) => {
|
|
433
609
|
try {
|
|
434
610
|
await ConfigParser.getInstance().loadConfig({
|
|
435
611
|
config: options.config,
|
|
436
612
|
path: options.path || process.cwd(),
|
|
437
613
|
});
|
|
614
|
+
const { ExperienceCommand } = await import('../src/commands/experience-command.js');
|
|
438
615
|
const explorBot = new ExplorBot({ path: options.path });
|
|
439
|
-
const
|
|
440
|
-
|
|
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();
|
|
441
623
|
} catch (error) {
|
|
442
624
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
443
625
|
process.exit(1);
|
|
@@ -445,169 +627,29 @@ program
|
|
|
445
627
|
});
|
|
446
628
|
|
|
447
629
|
program
|
|
448
|
-
.command('
|
|
449
|
-
.description('List
|
|
450
|
-
.action(async () => {
|
|
451
|
-
const { SitesCommand } = await import('../src/commands/sites-command.js');
|
|
452
|
-
await new SitesCommand(new ExplorBot()).execute('');
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
|
|
456
|
-
try {
|
|
457
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
458
|
-
await explorBot.start();
|
|
459
|
-
const { RerunCommand } = await import('../src/commands/rerun-command.js');
|
|
460
|
-
const cmd = new RerunCommand(explorBot);
|
|
461
|
-
const args = index ? `${filename} ${index}` : filename;
|
|
462
|
-
await cmd.execute(args);
|
|
463
|
-
await explorBot.stop();
|
|
464
|
-
await showStatsAndExit(0);
|
|
465
|
-
} catch (error) {
|
|
466
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
467
|
-
await showStatsAndExit(1);
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
addCommonOptions(
|
|
472
|
-
program
|
|
473
|
-
.command('freesail [startUrl]')
|
|
474
|
-
.description('Continuously explore and navigate to new pages autonomously')
|
|
475
|
-
.option('--deep', 'Depth-first: prioritize newly discovered pages')
|
|
476
|
-
.option('--shallow', 'Breadth-first: pick globally least-visited page')
|
|
477
|
-
.option('--scope <prefix>', 'Restrict navigation to URL prefix')
|
|
478
|
-
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
479
|
-
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
|
|
480
|
-
.option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')
|
|
481
|
-
).action(async (startUrl, options) => {
|
|
482
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
|
|
483
|
-
await explorBot.start();
|
|
484
|
-
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}"`]
|
|
485
|
-
.filter(Boolean)
|
|
486
|
-
.join(' ');
|
|
487
|
-
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
488
|
-
const cmd = new FreesailCommand(explorBot);
|
|
489
|
-
try {
|
|
490
|
-
await cmd.execute(args);
|
|
491
|
-
} catch (error) {
|
|
492
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
493
|
-
printPageContext(explorBot);
|
|
494
|
-
await showStatsAndExit(1);
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
program
|
|
499
|
-
.command('config [url]')
|
|
500
|
-
.description('Show models, config file and paths used by this run')
|
|
501
|
-
.option('-c, --config <path>', 'Path to configuration file')
|
|
630
|
+
.command('knows [url]')
|
|
631
|
+
.description('List knowledge URLs, or the knowledge matching a URL')
|
|
502
632
|
.option('-p, --path <path>', 'Working directory path')
|
|
503
|
-
.option('--json', 'Print the resolved config as JSON')
|
|
504
633
|
.action(async (url, options) => {
|
|
505
|
-
setQuietMode(!isVerboseMode());
|
|
506
|
-
const { ConfigCommand } = await import('../src/commands/config-command.js');
|
|
507
634
|
try {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
511
|
-
process.exit(1);
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
RecommendedModelsCommand.register(program);
|
|
516
|
-
|
|
517
|
-
program
|
|
518
|
-
.command('init')
|
|
519
|
-
.description('Initialize configuration for a project or for this machine')
|
|
520
|
-
.option('-c, --config-path <path>', 'Path for the config file')
|
|
521
|
-
.option('-f, --force', 'Overwrite existing config file')
|
|
522
|
-
.option('-p, --path <path>', 'Working directory for initialization')
|
|
523
|
-
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
524
|
-
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
525
|
-
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
526
|
-
.option('--url <url>', 'Web application URL written to web.url in the local config')
|
|
527
|
-
.action(async (options) => {
|
|
528
|
-
try {
|
|
529
|
-
const { runInit } = await import('../src/commands/init-command.js');
|
|
530
|
-
await runInit({
|
|
531
|
-
configPath: options.configPath,
|
|
532
|
-
force: options.force,
|
|
533
|
-
path: options.path,
|
|
534
|
-
global: options.global,
|
|
535
|
-
provider: options.provider,
|
|
536
|
-
apiKey: options.apiKey,
|
|
537
|
-
url: options.url,
|
|
635
|
+
await ConfigParser.getInstance().loadConfig({
|
|
636
|
+
path: options.path || process.cwd(),
|
|
538
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 || '');
|
|
539
642
|
} catch (error) {
|
|
540
643
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
541
644
|
process.exit(1);
|
|
542
645
|
}
|
|
543
646
|
});
|
|
544
647
|
|
|
545
|
-
program
|
|
546
|
-
.command('clean [target]')
|
|
547
|
-
.description('Clean files: states, research, plans, tests, experiences, output (default: output + experiences)')
|
|
548
|
-
.option('-p, --path <path>', 'Custom path to clean')
|
|
549
|
-
.action(async (target, options) => {
|
|
550
|
-
const customPath = options.path;
|
|
551
|
-
const originalCwd = process.cwd();
|
|
552
|
-
const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
|
|
553
|
-
|
|
554
|
-
const targets: Record<string, { description: string; dir: string }> = {
|
|
555
|
-
states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
|
|
556
|
-
research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
|
|
557
|
-
plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
|
|
558
|
-
tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
|
|
559
|
-
experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
|
|
560
|
-
output: { description: 'all output files', dir: path.join(basePath, 'output') },
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
function cleanDirectoryContents(dirPath: string): number {
|
|
564
|
-
let count = 0;
|
|
565
|
-
for (const item of fs.readdirSync(dirPath)) {
|
|
566
|
-
const itemPath = path.join(dirPath, item);
|
|
567
|
-
if (fs.statSync(itemPath).isDirectory()) {
|
|
568
|
-
count += cleanDirectoryContents(itemPath);
|
|
569
|
-
fs.rmSync(itemPath, { recursive: true });
|
|
570
|
-
} else {
|
|
571
|
-
fs.unlinkSync(itemPath);
|
|
572
|
-
count++;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return count;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function cleanTarget(name: string): void {
|
|
579
|
-
const t = targets[name];
|
|
580
|
-
if (!fs.existsSync(t.dir)) {
|
|
581
|
-
console.log(`${name}: nothing to clean (${t.dir} not found)`);
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
const count = cleanDirectoryContents(t.dir);
|
|
585
|
-
console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
try {
|
|
589
|
-
if (target && !targets[target]) {
|
|
590
|
-
console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
|
|
591
|
-
process.exit(1);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (!target) {
|
|
595
|
-
cleanTarget('output');
|
|
596
|
-
cleanTarget('experiences');
|
|
597
|
-
} else {
|
|
598
|
-
cleanTarget(target);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
console.log('Cleanup completed successfully!');
|
|
602
|
-
} catch (error) {
|
|
603
|
-
console.error('Failed to clean:', error);
|
|
604
|
-
process.exit(1);
|
|
605
|
-
}
|
|
606
|
-
});
|
|
648
|
+
program.commandsGroup(GROUPS.knowledge);
|
|
607
649
|
|
|
608
650
|
program
|
|
609
651
|
.command('learn [url] [description]')
|
|
610
|
-
.description('Add knowledge for
|
|
652
|
+
.description('Add knowledge for a URL')
|
|
611
653
|
.option('-p, --path <path>', 'Working directory path')
|
|
612
654
|
.option('--replace', 'Replace existing knowledge for this URL instead of appending')
|
|
613
655
|
.action(async (url, description, options) => {
|
|
@@ -637,28 +679,9 @@ program
|
|
|
637
679
|
}
|
|
638
680
|
});
|
|
639
681
|
|
|
640
|
-
program
|
|
641
|
-
.command('knows [url]')
|
|
642
|
-
.description('List all knowledge URLs or show matching knowledge for a URL')
|
|
643
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
644
|
-
.action(async (url, options) => {
|
|
645
|
-
try {
|
|
646
|
-
await ConfigParser.getInstance().loadConfig({
|
|
647
|
-
path: options.path || process.cwd(),
|
|
648
|
-
});
|
|
649
|
-
const { KnowsCommand } = await import('../src/commands/knows-command.js');
|
|
650
|
-
const explorBot = new ExplorBot({ path: options.path });
|
|
651
|
-
const command = new KnowsCommand(explorBot);
|
|
652
|
-
await command.execute(url || '');
|
|
653
|
-
} catch (error) {
|
|
654
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
655
|
-
process.exit(1);
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
|
|
659
682
|
program
|
|
660
683
|
.command('compact [target]')
|
|
661
|
-
.description('Compact
|
|
684
|
+
.description('Compact experience files; pass a filename or URL substring to narrow')
|
|
662
685
|
.option('--dry-run', 'Preview without running AI or writing files')
|
|
663
686
|
.option('--no-merge', 'Skip the cross-URL merge step when compacting all')
|
|
664
687
|
.option('-p, --path <path>', 'Working directory path')
|
|
@@ -687,162 +710,112 @@ program
|
|
|
687
710
|
});
|
|
688
711
|
|
|
689
712
|
program
|
|
690
|
-
.command('
|
|
691
|
-
.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')
|
|
692
716
|
.option('-p, --path <path>', 'Working directory path')
|
|
693
|
-
.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
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,
|
|
701
724
|
});
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
+
}
|
|
711
755
|
} catch (error) {
|
|
712
756
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
713
757
|
process.exit(1);
|
|
714
758
|
}
|
|
715
759
|
});
|
|
716
760
|
|
|
717
|
-
|
|
718
|
-
async (url, options) => {
|
|
719
|
-
try {
|
|
720
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
721
|
-
await explorBot.start();
|
|
722
|
-
|
|
723
|
-
const argParts: string[] = [url];
|
|
724
|
-
if (options.data) argParts.push('--data');
|
|
725
|
-
if (options.deep) argParts.push('--deep');
|
|
726
|
-
if (options.fix === false) argParts.push('--no-fix');
|
|
727
|
-
|
|
728
|
-
const { ResearchCommand } = await import('../src/commands/research-command.js');
|
|
729
|
-
await new ResearchCommand(explorBot).execute(argParts.join(' '));
|
|
761
|
+
program.commandsGroup(GROUPS.setup);
|
|
730
762
|
|
|
731
|
-
|
|
732
|
-
|
|
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
|
+
});
|
|
733
785
|
} catch (error) {
|
|
734
786
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
735
|
-
|
|
787
|
+
process.exit(1);
|
|
736
788
|
}
|
|
737
|
-
}
|
|
738
|
-
);
|
|
739
|
-
|
|
740
|
-
addCommonOptions(program.command('navigate <url>').description('Navigate to a URL using the AI Navigator. Exits 0 if reachable, 1 otherwise.')).action(async (url, options) => {
|
|
741
|
-
try {
|
|
742
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
743
|
-
await explorBot.start();
|
|
744
|
-
|
|
745
|
-
const { NavigateCommand } = await import('../src/commands/navigate-command.js');
|
|
746
|
-
await new NavigateCommand(explorBot).execute(url);
|
|
747
|
-
|
|
748
|
-
await explorBot.stop();
|
|
749
|
-
await showStatsAndExit(0);
|
|
750
|
-
} catch (error) {
|
|
751
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
752
|
-
await showStatsAndExit(1);
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
addCommonOptions(
|
|
757
|
-
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')
|
|
758
|
-
).action(async (url, options) => {
|
|
759
|
-
try {
|
|
760
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
761
|
-
await explorBot.start();
|
|
762
|
-
|
|
763
|
-
await explorBot.visit(url);
|
|
764
|
-
|
|
765
|
-
const plan = await explorBot.agentDriller().drill({
|
|
766
|
-
knowledgePath: options.saveKnowledge,
|
|
767
|
-
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
768
|
-
interactive: false,
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
console.log(`\nDrill completed: ${plan.tests.length} components`);
|
|
772
|
-
console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
|
|
773
|
-
console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
|
|
774
|
-
|
|
775
|
-
await explorBot.stop();
|
|
776
|
-
await showStatsAndExit(0);
|
|
777
|
-
} catch (error) {
|
|
778
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
779
|
-
await showStatsAndExit(1);
|
|
780
|
-
}
|
|
781
|
-
});
|
|
789
|
+
});
|
|
782
790
|
|
|
783
791
|
program
|
|
784
|
-
.command('
|
|
785
|
-
.description('
|
|
786
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
792
|
+
.command('config [url]')
|
|
793
|
+
.description('Show models, config file and paths used by this run')
|
|
787
794
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
788
|
-
.option('--
|
|
789
|
-
.option('--
|
|
790
|
-
.option('--compact', 'Compact view with summaries')
|
|
791
|
-
.option('--attached', 'Only auto-attached sections (default)')
|
|
792
|
-
.option('--visual', 'Annotate elements on screenshot and print screenshot path')
|
|
793
|
-
.option('--screenshot', 'Alias for --visual')
|
|
795
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
796
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
794
797
|
.action(async (url, options) => {
|
|
798
|
+
setQuietMode(!isVerboseMode());
|
|
799
|
+
const { ConfigCommand } = await import('../src/commands/config-command.js');
|
|
795
800
|
try {
|
|
796
|
-
|
|
797
|
-
path: options.path,
|
|
798
|
-
config: options.config,
|
|
799
|
-
headless: true,
|
|
800
|
-
session: options.session,
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
const explorBot = new ExplorBot(mainOptions);
|
|
804
|
-
await explorBot.start();
|
|
805
|
-
|
|
806
|
-
await explorBot.visit(url);
|
|
807
|
-
|
|
808
|
-
const { ContextCommand } = await import('../src/commands/context-command.js');
|
|
809
|
-
const argParts: string[] = [];
|
|
810
|
-
if (options.full) argParts.push('--full');
|
|
811
|
-
else if (options.compact) argParts.push('--compact');
|
|
812
|
-
else argParts.push('--attached');
|
|
813
|
-
if (options.visual || options.screenshot) argParts.push('--visual');
|
|
814
|
-
await new ContextCommand(explorBot).execute(argParts.join(' '));
|
|
815
|
-
|
|
816
|
-
await explorBot.stop();
|
|
817
|
-
await showStatsAndExit(0);
|
|
801
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
818
802
|
} catch (error) {
|
|
819
|
-
console.error(
|
|
820
|
-
|
|
803
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
804
|
+
process.exit(1);
|
|
821
805
|
}
|
|
822
806
|
});
|
|
823
807
|
|
|
824
|
-
|
|
825
|
-
try {
|
|
826
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
827
|
-
await explorBot.start();
|
|
828
|
-
await explorBot.visit(url);
|
|
829
|
-
|
|
830
|
-
const action = explorBot.getExplorer().action();
|
|
831
|
-
await action.execute(command);
|
|
832
|
-
|
|
833
|
-
log('Command executed successfully');
|
|
834
|
-
const state = explorBot.stateManager().getCurrentState();
|
|
835
|
-
if (state) log(`URL: ${state.url}`);
|
|
808
|
+
RecommendedModelsCommand.register(program);
|
|
836
809
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
await
|
|
842
|
-
|
|
843
|
-
});
|
|
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
|
+
});
|
|
844
817
|
|
|
845
|
-
const browserCmd = program.command('browser').description('
|
|
818
|
+
const browserCmd = program.command('browser').description('Start, stop or check a persistent browser server');
|
|
846
819
|
|
|
847
820
|
browserCmd
|
|
848
821
|
.command('start')
|
|
@@ -921,80 +894,119 @@ browserCmd
|
|
|
921
894
|
});
|
|
922
895
|
|
|
923
896
|
program
|
|
924
|
-
.command('
|
|
925
|
-
.description('
|
|
926
|
-
.option('-
|
|
927
|
-
.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
|
+
|
|
928
939
|
try {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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');
|
|
934
948
|
} else {
|
|
935
|
-
|
|
949
|
+
cleanTarget(target);
|
|
936
950
|
}
|
|
951
|
+
|
|
952
|
+
console.log('Cleanup completed successfully!');
|
|
937
953
|
} catch (error) {
|
|
938
|
-
console.error('Failed
|
|
954
|
+
console.error('Failed to clean:', error);
|
|
939
955
|
process.exit(1);
|
|
940
956
|
}
|
|
941
957
|
});
|
|
942
958
|
|
|
943
|
-
program
|
|
944
|
-
.command('add-rule [agent] [name]')
|
|
945
|
-
.description('Create a rule file for an agent')
|
|
946
|
-
.option('--url <pattern>', 'URL pattern for this rule')
|
|
947
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
948
|
-
.action(async (agent, name, options) => {
|
|
949
|
-
if (options.path) process.chdir(path.resolve(options.path));
|
|
950
|
-
|
|
951
|
-
if (agent && name) {
|
|
952
|
-
const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
|
|
953
|
-
const result = AddRuleCommand.createRuleFile(agent, name, {
|
|
954
|
-
urlPattern: options.url,
|
|
955
|
-
});
|
|
956
|
-
process.exit(result ? 0 : 1);
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
const AddRule = (await import('../src/components/AddRule.js')).default;
|
|
960
|
-
render(
|
|
961
|
-
React.createElement(AddRule, {
|
|
962
|
-
initialAgent: agent || '',
|
|
963
|
-
initialName: name || '',
|
|
964
|
-
}),
|
|
965
|
-
{
|
|
966
|
-
exitOnCtrlC: false,
|
|
967
|
-
patchConsole: false,
|
|
968
|
-
}
|
|
969
|
-
);
|
|
970
|
-
});
|
|
959
|
+
program.commandsGroup(GROUPS.tools);
|
|
971
960
|
|
|
972
|
-
import { createApiCommands } from '../boat/api-tester/src/cli.ts';
|
|
973
|
-
import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
|
|
974
|
-
import { createPrimaCommands } from '../boat/prima/src/cli.ts';
|
|
975
961
|
program.addCommand(createApiCommands('api'));
|
|
976
962
|
program.addCommand(createDocsCommands('docs'));
|
|
977
963
|
program.addCommand(createPrimaCommands('prima'));
|
|
978
964
|
|
|
979
965
|
const envHelp = () => {
|
|
980
966
|
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
981
|
-
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');
|
|
982
968
|
|
|
983
969
|
return `
|
|
984
|
-
Environment variables
|
|
970
|
+
${chalk.yellow.bold('Environment variables:')}
|
|
985
971
|
Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
|
|
986
972
|
|
|
987
973
|
${rows}
|
|
988
974
|
|
|
989
975
|
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
976
|
+
|
|
977
|
+
${chalk.yellow.bold('Example:')}
|
|
978
|
+
EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
|
|
979
|
+
${cli} explore /login --max-tests 3
|
|
993
980
|
`;
|
|
994
981
|
};
|
|
995
982
|
|
|
996
|
-
program
|
|
997
|
-
|
|
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);
|
|
998
1010
|
HelpJsonCommand.register(program);
|
|
999
1011
|
|
|
1000
1012
|
program.parse();
|