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
|
@@ -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.js";
|
|
10
|
+
import { createDocsCommands } from "../boat/doc-collector/src/cli.js";
|
|
11
|
+
import { createPrimaCommands } from "../boat/prima/src/cli.js";
|
|
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 } from '../src/explorbot.js';
|
|
18
21
|
import { remote } from '../src/remote.js';
|
|
@@ -22,17 +25,46 @@ import { getCliName } from "../src/utils/cli-name.js";
|
|
|
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 { printNextSteps, relativeToCwd } from "../src/utils/next-steps.js";
|
|
26
28
|
import { readCliSitemap } from "../src/utils/sitemap.js";
|
|
27
29
|
import { UrlFilter } from "../src/utils/url-filter.js";
|
|
28
30
|
const program = new Command();
|
|
29
31
|
const cli = getCliName();
|
|
30
32
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
31
33
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const GROUPS = {
|
|
35
|
+
explore: 'Explore:',
|
|
36
|
+
steps: 'Steps:',
|
|
37
|
+
results: 'Results:',
|
|
38
|
+
knowledge: 'Knowledge & rules:',
|
|
39
|
+
setup: 'Setup:',
|
|
40
|
+
tools: 'More tools:',
|
|
41
|
+
help: 'Help:',
|
|
42
|
+
};
|
|
43
|
+
program
|
|
44
|
+
.name(cli)
|
|
45
|
+
.description('AI-powered exploratory testing for web apps')
|
|
46
|
+
.version(pkgVersion, '-V, --version')
|
|
47
|
+
.configureHelp({
|
|
48
|
+
styleTitle: (str) => chalk.yellow.bold(str),
|
|
49
|
+
styleCommandText: (str) => chalk.bold(str),
|
|
50
|
+
styleSubcommandText: (str) => chalk.cyan(str),
|
|
51
|
+
styleOptionText: (str) => chalk.green(str),
|
|
52
|
+
styleArgumentText: (str) => chalk.magenta(str),
|
|
53
|
+
showGlobalOptions: true,
|
|
54
|
+
visibleOptions(cmd) {
|
|
55
|
+
const options = Help.prototype.visibleOptions.call(this, cmd);
|
|
56
|
+
if (cmd.parent)
|
|
57
|
+
return options;
|
|
58
|
+
return options.filter((option) => !isGlobalOption(option.flags));
|
|
59
|
+
},
|
|
60
|
+
visibleGlobalOptions(cmd) {
|
|
61
|
+
return Help.prototype.visibleGlobalOptions.call(this, cmd).filter((option) => isGlobalOption(option.flags));
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const globalOptions = [knowledgeOption, authOption, decisionModelOption, wsOption];
|
|
65
|
+
for (const option of globalOptions)
|
|
66
|
+
option.register(program);
|
|
67
|
+
const isGlobalOption = (flags) => globalOptions.some((option) => option.flags === flags);
|
|
36
68
|
process.on('uncaughtException', async (error) => {
|
|
37
69
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
38
70
|
await flushTelemetry();
|
|
@@ -46,6 +78,7 @@ if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima') && !prin
|
|
|
46
78
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
47
79
|
}
|
|
48
80
|
function buildExplorBotOptions(from, options) {
|
|
81
|
+
const spec = Array.isArray(options.spec) ? (options.spec.length > 0 ? options.spec : undefined) : options.spec;
|
|
49
82
|
return {
|
|
50
83
|
from,
|
|
51
84
|
baseUrl: options.baseUrl,
|
|
@@ -56,7 +89,7 @@ function buildExplorBotOptions(from, options) {
|
|
|
56
89
|
headless: options.headless,
|
|
57
90
|
incognito: options.incognito,
|
|
58
91
|
session: options.session,
|
|
59
|
-
applicationSpec:
|
|
92
|
+
applicationSpec: spec,
|
|
60
93
|
};
|
|
61
94
|
}
|
|
62
95
|
function addCommonOptions(cmd) {
|
|
@@ -69,7 +102,7 @@ function addCommonOptions(cmd) {
|
|
|
69
102
|
.option('-s, --show', 'Show browser window')
|
|
70
103
|
.option('--headless', 'Run browser in headless mode')
|
|
71
104
|
.option('--incognito', 'Run without recording experiences')
|
|
72
|
-
.option('--spec <path>', 'Use a Docbot application spec directory or index.md')
|
|
105
|
+
.option('--spec <path>', 'Use a Docbot application spec directory or index.md (repeatable)', (val, acc) => [...(acc || []), val])
|
|
73
106
|
.option('--session [file]', 'Save/restore browser session from file');
|
|
74
107
|
}
|
|
75
108
|
async function startTUI(explorBot) {
|
|
@@ -131,7 +164,8 @@ async function showStatsAndExit(code) {
|
|
|
131
164
|
await flushTelemetry();
|
|
132
165
|
process.exit(code);
|
|
133
166
|
}
|
|
134
|
-
|
|
167
|
+
program.commandsGroup(GROUPS.explore);
|
|
168
|
+
addCommonOptions(program.command('start [path]').description('Open the interactive TUI and explore with commands')).action(async (startPath, options) => {
|
|
135
169
|
setPreserveConsoleLogs(false);
|
|
136
170
|
const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
|
|
137
171
|
await explorBot.start();
|
|
@@ -139,7 +173,7 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
|
|
|
139
173
|
});
|
|
140
174
|
addCommonOptions(program
|
|
141
175
|
.command('explore [path]')
|
|
142
|
-
.description('
|
|
176
|
+
.description('Invent test scenarios for a page and run them')
|
|
143
177
|
.option('--sitemap <source>', 'Explore every page listed in a sitemap: file path, URL, or - for stdin (a piped stdin is read automatically)')
|
|
144
178
|
.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"')
|
|
145
179
|
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
@@ -184,72 +218,227 @@ addCommonOptions(program
|
|
|
184
218
|
await showStatsAndExit(1);
|
|
185
219
|
}
|
|
186
220
|
});
|
|
187
|
-
addCommonOptions(program
|
|
221
|
+
addCommonOptions(program
|
|
222
|
+
.command('freesail [startUrl]')
|
|
223
|
+
.description('Roam the app on its own, moving on to new pages')
|
|
224
|
+
.option('--deep', 'Depth-first: prioritize newly discovered pages')
|
|
225
|
+
.option('--shallow', 'Breadth-first: pick globally least-visited page')
|
|
226
|
+
.option('--scope <prefix>', 'Restrict navigation to URL prefix')
|
|
227
|
+
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
228
|
+
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
|
|
229
|
+
.option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')).action(async (startUrl, options) => {
|
|
230
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
|
|
231
|
+
await explorBot.start();
|
|
232
|
+
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}"`]
|
|
233
|
+
.filter(Boolean)
|
|
234
|
+
.join(' ');
|
|
235
|
+
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
236
|
+
const cmd = new FreesailCommand(explorBot);
|
|
237
|
+
try {
|
|
238
|
+
await cmd.execute(args);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
242
|
+
printPageContext(explorBot);
|
|
243
|
+
await showStatsAndExit(1);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
program.commandsGroup(GROUPS.steps);
|
|
247
|
+
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(async (url, options) => {
|
|
248
|
+
try {
|
|
249
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
250
|
+
await explorBot.start();
|
|
251
|
+
const argParts = [url];
|
|
252
|
+
if (options.data)
|
|
253
|
+
argParts.push('--data');
|
|
254
|
+
if (options.deep)
|
|
255
|
+
argParts.push('--deep');
|
|
256
|
+
if (options.fix === false)
|
|
257
|
+
argParts.push('--no-fix');
|
|
258
|
+
const { ResearchCommand } = await import('../src/commands/research-command.js');
|
|
259
|
+
await new ResearchCommand(explorBot).execute(argParts.join(' '));
|
|
260
|
+
await explorBot.stop();
|
|
261
|
+
await showStatsAndExit(0);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
265
|
+
await showStatsAndExit(1);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
addCommonOptions(program.command('plan <path>').description('Write a test plan for a page'))
|
|
188
269
|
.option('-a, --append', 'Add tests to existing plan file')
|
|
189
270
|
.option('--style <style>', 'Planning style: normal, curious, psycho')
|
|
190
271
|
.option('--focus <feature>', 'Focus area for test planning')
|
|
272
|
+
.option('--tests <count>', 'Exact number of tests to generate')
|
|
273
|
+
.option('--min-tests <count>', 'Minimum number of tests to generate')
|
|
274
|
+
.option('--max-tests <count>', 'Maximum number of tests to generate')
|
|
191
275
|
.action(async (planPath, options) => {
|
|
192
276
|
try {
|
|
193
277
|
const explorBot = new ExplorBot(buildExplorBotOptions(planPath, options));
|
|
194
278
|
await explorBot.start();
|
|
195
279
|
await explorBot.visit(planPath);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const savedPath = explorBot.savePlan();
|
|
229
|
-
const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
|
|
230
|
-
const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
|
|
231
|
-
const sections = [];
|
|
232
|
-
if (savedPath) {
|
|
233
|
-
const relPlan = relativeToCwd(savedPath);
|
|
234
|
-
sections.push({
|
|
235
|
-
label: 'Plan',
|
|
236
|
-
path: savedPath,
|
|
237
|
-
commands: [
|
|
238
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1${cliSuffix}` },
|
|
239
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *${cliSuffix}` },
|
|
240
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3${cliSuffix}` },
|
|
241
|
-
],
|
|
242
|
-
});
|
|
280
|
+
const args = ['--fresh'];
|
|
281
|
+
if (options.append)
|
|
282
|
+
args[0] = '--append';
|
|
283
|
+
if (options.style)
|
|
284
|
+
args.push(`--style ${options.style}`);
|
|
285
|
+
if (options.focus)
|
|
286
|
+
args.push(`--focus "${options.focus}"`);
|
|
287
|
+
if (options.tests)
|
|
288
|
+
args.push(`--tests ${options.tests}`);
|
|
289
|
+
if (options.minTests)
|
|
290
|
+
args.push(`--min-tests ${options.minTests}`);
|
|
291
|
+
if (options.maxTests)
|
|
292
|
+
args.push(`--max-tests ${options.maxTests}`);
|
|
293
|
+
await new PlanCommand(explorBot).execute(args.join(' '));
|
|
294
|
+
await explorBot.stop();
|
|
295
|
+
await showStatsAndExit(0);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
299
|
+
await showStatsAndExit(1);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
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(async (planfile, index, options) => {
|
|
303
|
+
try {
|
|
304
|
+
let planfileArg = planfile;
|
|
305
|
+
let indexArg = index;
|
|
306
|
+
if (options.fromPlan) {
|
|
307
|
+
planfileArg = options.fromPlan;
|
|
308
|
+
indexArg = planfile;
|
|
243
309
|
}
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
310
|
+
const peeked = Plan.loadFromFile(planfileArg);
|
|
311
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
312
|
+
await explorBot.start();
|
|
313
|
+
const plan = explorBot.loadPlan(planfileArg);
|
|
314
|
+
const pending = plan.getPendingTests();
|
|
315
|
+
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
316
|
+
const startUrl = plan.startUrl;
|
|
317
|
+
if (!startUrl) {
|
|
318
|
+
throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
|
|
251
319
|
}
|
|
252
|
-
|
|
320
|
+
log(`Navigating to ${startUrl}`);
|
|
321
|
+
await explorBot.visit(startUrl);
|
|
322
|
+
let args = '*';
|
|
323
|
+
if (indexArg)
|
|
324
|
+
args = indexArg;
|
|
325
|
+
else if (options.grep)
|
|
326
|
+
args = options.grep;
|
|
327
|
+
const { TestCommand } = await import('../src/commands/test-command.js');
|
|
328
|
+
const cmd = new TestCommand(explorBot);
|
|
329
|
+
await cmd.execute(args);
|
|
330
|
+
await explorBot.printSessionAnalysis();
|
|
331
|
+
await explorBot.stop();
|
|
332
|
+
await showStatsAndExit(0);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
336
|
+
await showStatsAndExit(1);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests, healing failures with AI')).action(async (filename, index, options) => {
|
|
340
|
+
try {
|
|
341
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
342
|
+
await explorBot.start();
|
|
343
|
+
const { RerunCommand } = await import('../src/commands/rerun-command.js');
|
|
344
|
+
const cmd = new RerunCommand(explorBot);
|
|
345
|
+
const args = index ? `${filename} ${index}` : filename;
|
|
346
|
+
await cmd.execute(args);
|
|
347
|
+
await explorBot.stop();
|
|
348
|
+
await showStatsAndExit(0);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
352
|
+
await showStatsAndExit(1);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
addCommonOptions(program.command('navigate <url>').description('Reach a URL with the AI Navigator; exits 1 if unreachable')).action(async (url, options) => {
|
|
356
|
+
try {
|
|
357
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
358
|
+
await explorBot.start();
|
|
359
|
+
const { NavigateCommand } = await import('../src/commands/navigate-command.js');
|
|
360
|
+
await new NavigateCommand(explorBot).execute(url);
|
|
361
|
+
await explorBot.stop();
|
|
362
|
+
await showStatsAndExit(0);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
366
|
+
await showStatsAndExit(1);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
addCommonOptions(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')).action(async (url, options) => {
|
|
370
|
+
try {
|
|
371
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
372
|
+
await explorBot.start();
|
|
373
|
+
await explorBot.visit(url);
|
|
374
|
+
const plan = await explorBot.agentDriller().drill({
|
|
375
|
+
knowledgePath: options.saveKnowledge,
|
|
376
|
+
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
377
|
+
interactive: false,
|
|
378
|
+
});
|
|
379
|
+
console.log(`\nDrill completed: ${plan.tests.length} components`);
|
|
380
|
+
console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
|
|
381
|
+
console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
|
|
382
|
+
await explorBot.stop();
|
|
383
|
+
await showStatsAndExit(0);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
387
|
+
await showStatsAndExit(1);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
program
|
|
391
|
+
.command('context <url>')
|
|
392
|
+
.description('Print what agents see on a page: headings, knowledge, experience, elements')
|
|
393
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
394
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
395
|
+
.option('--session [file]', 'Save/restore browser session from file')
|
|
396
|
+
.option('--full', 'Include HTML and all data')
|
|
397
|
+
.option('--compact', 'Compact view with summaries')
|
|
398
|
+
.option('--attached', 'Only auto-attached sections (default)')
|
|
399
|
+
.option('--visual', 'Annotate elements on screenshot and print screenshot path')
|
|
400
|
+
.option('--screenshot', 'Alias for --visual')
|
|
401
|
+
.action(async (url, options) => {
|
|
402
|
+
try {
|
|
403
|
+
const mainOptions = {
|
|
404
|
+
path: options.path,
|
|
405
|
+
config: options.config,
|
|
406
|
+
headless: true,
|
|
407
|
+
session: options.session,
|
|
408
|
+
};
|
|
409
|
+
const explorBot = new ExplorBot(mainOptions);
|
|
410
|
+
await explorBot.start();
|
|
411
|
+
await explorBot.visit(url);
|
|
412
|
+
const { ContextCommand } = await import('../src/commands/context-command.js');
|
|
413
|
+
const argParts = [];
|
|
414
|
+
if (options.full)
|
|
415
|
+
argParts.push('--full');
|
|
416
|
+
else if (options.compact)
|
|
417
|
+
argParts.push('--compact');
|
|
418
|
+
else
|
|
419
|
+
argParts.push('--attached');
|
|
420
|
+
if (options.visual || options.screenshot)
|
|
421
|
+
argParts.push('--visual');
|
|
422
|
+
await new ContextCommand(explorBot).execute(argParts.join(' '));
|
|
423
|
+
await explorBot.stop();
|
|
424
|
+
await showStatsAndExit(0);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
428
|
+
await showStatsAndExit(1);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
addCommonOptions(program.command('shell <url> <command>').description('Run one CodeceptJS command on a page')).action(async (url, command, options) => {
|
|
432
|
+
try {
|
|
433
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
434
|
+
await explorBot.start();
|
|
435
|
+
await explorBot.visit(url);
|
|
436
|
+
const action = explorBot.getExplorer().action();
|
|
437
|
+
await action.execute(command);
|
|
438
|
+
log('Command executed successfully');
|
|
439
|
+
const state = explorBot.stateManager().getCurrentState();
|
|
440
|
+
if (state)
|
|
441
|
+
log(`URL: ${state.url}`);
|
|
253
442
|
await explorBot.stop();
|
|
254
443
|
await showStatsAndExit(0);
|
|
255
444
|
}
|
|
@@ -258,7 +447,28 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
258
447
|
await showStatsAndExit(1);
|
|
259
448
|
}
|
|
260
449
|
});
|
|
261
|
-
|
|
450
|
+
program.commandsGroup(GROUPS.results);
|
|
451
|
+
program
|
|
452
|
+
.command('plans [plan]')
|
|
453
|
+
.description('List saved plans, or the tests of one plan')
|
|
454
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
455
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
456
|
+
.action(async (plan, options) => {
|
|
457
|
+
try {
|
|
458
|
+
await ConfigParser.getInstance().loadConfig({
|
|
459
|
+
config: options.config,
|
|
460
|
+
path: options.path || process.cwd(),
|
|
461
|
+
});
|
|
462
|
+
const explorBot = new ExplorBot({ path: options.path });
|
|
463
|
+
const { PlansCommand } = await import('../src/commands/plans-command.js');
|
|
464
|
+
await new PlansCommand(explorBot).execute(plan || '');
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
addCommonOptions(program.command('plan:load <planfile> [index]').description('Show the tests of a plan file, or one test in detail')).action(async (planfile, index) => {
|
|
262
472
|
try {
|
|
263
473
|
const plan = Plan.loadFromFile(planfile);
|
|
264
474
|
if (!plan?.filePath) {
|
|
@@ -322,50 +532,9 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
322
532
|
process.exit(1);
|
|
323
533
|
}
|
|
324
534
|
});
|
|
325
|
-
addCommonOptions(program.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1,3, 1-5, *, all').option('--grep <pattern>', 'Run tests matching pattern').option('--from-plan <file>', 'Load plan file when the first argument is a test index')).action(async (planfile, index, options) => {
|
|
326
|
-
try {
|
|
327
|
-
let planfileArg = planfile;
|
|
328
|
-
let indexArg = index;
|
|
329
|
-
if (options.fromPlan) {
|
|
330
|
-
planfileArg = options.fromPlan;
|
|
331
|
-
indexArg = planfile;
|
|
332
|
-
}
|
|
333
|
-
const peeked = Plan.loadFromFile(planfileArg);
|
|
334
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
335
|
-
await explorBot.start();
|
|
336
|
-
let plan = peeked;
|
|
337
|
-
if (plan)
|
|
338
|
-
explorBot.setCurrentPlan(plan);
|
|
339
|
-
if (!plan)
|
|
340
|
-
plan = explorBot.loadPlan(planfileArg);
|
|
341
|
-
const pending = plan.getPendingTests();
|
|
342
|
-
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
343
|
-
const startUrl = plan.startUrl;
|
|
344
|
-
if (!startUrl) {
|
|
345
|
-
throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
|
|
346
|
-
}
|
|
347
|
-
log(`Navigating to ${startUrl}`);
|
|
348
|
-
await explorBot.visit(startUrl);
|
|
349
|
-
let args = '*';
|
|
350
|
-
if (indexArg)
|
|
351
|
-
args = indexArg;
|
|
352
|
-
else if (options.grep)
|
|
353
|
-
args = options.grep;
|
|
354
|
-
const { TestCommand } = await import('../src/commands/test-command.js');
|
|
355
|
-
const cmd = new TestCommand(explorBot);
|
|
356
|
-
await cmd.execute(args);
|
|
357
|
-
await explorBot.printSessionAnalysis();
|
|
358
|
-
await explorBot.stop();
|
|
359
|
-
await showStatsAndExit(0);
|
|
360
|
-
}
|
|
361
|
-
catch (error) {
|
|
362
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
363
|
-
await showStatsAndExit(1);
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
535
|
program
|
|
367
536
|
.command('runs [file]')
|
|
368
|
-
.description('List generated test files, or
|
|
537
|
+
.description('List generated test files, or the steps of one file')
|
|
369
538
|
.option('-p, --path <path>', 'Working directory path')
|
|
370
539
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
371
540
|
.action(async (file, options) => {
|
|
@@ -384,19 +553,29 @@ program
|
|
|
384
553
|
}
|
|
385
554
|
});
|
|
386
555
|
program
|
|
387
|
-
.command('
|
|
388
|
-
.description('List
|
|
556
|
+
.command('experience [filter] [index]')
|
|
557
|
+
.description('List stored experience by URL; pass a tag (A.1) or index to expand one')
|
|
389
558
|
.option('-p, --path <path>', 'Working directory path')
|
|
390
559
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
391
|
-
.
|
|
560
|
+
.option('--recent', 'Only files modified within the last 30 days')
|
|
561
|
+
.option('--old', 'Only files modified more than 30 days ago')
|
|
562
|
+
.action(async (filter, index, options) => {
|
|
392
563
|
try {
|
|
393
564
|
await ConfigParser.getInstance().loadConfig({
|
|
394
565
|
config: options.config,
|
|
395
566
|
path: options.path || process.cwd(),
|
|
396
567
|
});
|
|
568
|
+
const { ExperienceCommand } = await import('../src/commands/experience-command.js');
|
|
397
569
|
const explorBot = new ExplorBot({ path: options.path });
|
|
398
|
-
const
|
|
399
|
-
|
|
570
|
+
const command = new ExperienceCommand(explorBot);
|
|
571
|
+
const flags = [];
|
|
572
|
+
if (options.recent)
|
|
573
|
+
flags.push('--recent');
|
|
574
|
+
if (options.old)
|
|
575
|
+
flags.push('--old');
|
|
576
|
+
const args = [...flags, filter, index].filter(Boolean).join(' ');
|
|
577
|
+
await command.execute(args);
|
|
578
|
+
command.printSuggestions();
|
|
400
579
|
}
|
|
401
580
|
catch (error) {
|
|
402
581
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
@@ -404,161 +583,28 @@ program
|
|
|
404
583
|
}
|
|
405
584
|
});
|
|
406
585
|
program
|
|
407
|
-
.command('
|
|
408
|
-
.description('List
|
|
409
|
-
.action(async () => {
|
|
410
|
-
const { SitesCommand } = await import('../src/commands/sites-command.js');
|
|
411
|
-
await new SitesCommand(new ExplorBot()).execute('');
|
|
412
|
-
});
|
|
413
|
-
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
|
|
414
|
-
try {
|
|
415
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
416
|
-
await explorBot.start();
|
|
417
|
-
const { RerunCommand } = await import('../src/commands/rerun-command.js');
|
|
418
|
-
const cmd = new RerunCommand(explorBot);
|
|
419
|
-
const args = index ? `${filename} ${index}` : filename;
|
|
420
|
-
await cmd.execute(args);
|
|
421
|
-
await explorBot.stop();
|
|
422
|
-
await showStatsAndExit(0);
|
|
423
|
-
}
|
|
424
|
-
catch (error) {
|
|
425
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
426
|
-
await showStatsAndExit(1);
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
addCommonOptions(program
|
|
430
|
-
.command('freesail [startUrl]')
|
|
431
|
-
.description('Continuously explore and navigate to new pages autonomously')
|
|
432
|
-
.option('--deep', 'Depth-first: prioritize newly discovered pages')
|
|
433
|
-
.option('--shallow', 'Breadth-first: pick globally least-visited page')
|
|
434
|
-
.option('--scope <prefix>', 'Restrict navigation to URL prefix')
|
|
435
|
-
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
436
|
-
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
|
|
437
|
-
.option('--url-filter <spec>', 'Pick pages to explore: keys filter|exclude|limit, e.g. "filter:/admin/*;exclude:/admin/logs/*;limit:20"')).action(async (startUrl, options) => {
|
|
438
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
|
|
439
|
-
await explorBot.start();
|
|
440
|
-
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}"`]
|
|
441
|
-
.filter(Boolean)
|
|
442
|
-
.join(' ');
|
|
443
|
-
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
444
|
-
const cmd = new FreesailCommand(explorBot);
|
|
445
|
-
try {
|
|
446
|
-
await cmd.execute(args);
|
|
447
|
-
}
|
|
448
|
-
catch (error) {
|
|
449
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
450
|
-
printPageContext(explorBot);
|
|
451
|
-
await showStatsAndExit(1);
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
program
|
|
455
|
-
.command('config [url]')
|
|
456
|
-
.description('Show models, config file and paths used by this run')
|
|
457
|
-
.option('-c, --config <path>', 'Path to configuration file')
|
|
586
|
+
.command('knows [url]')
|
|
587
|
+
.description('List knowledge URLs, or the knowledge matching a URL')
|
|
458
588
|
.option('-p, --path <path>', 'Working directory path')
|
|
459
|
-
.option('--json', 'Print the resolved config as JSON')
|
|
460
589
|
.action(async (url, options) => {
|
|
461
|
-
setQuietMode(!isVerboseMode());
|
|
462
|
-
const { ConfigCommand } = await import('../src/commands/config-command.js');
|
|
463
|
-
try {
|
|
464
|
-
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
468
|
-
process.exit(1);
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
RecommendedModelsCommand.register(program);
|
|
472
|
-
program
|
|
473
|
-
.command('init')
|
|
474
|
-
.description('Initialize configuration for a project or for this machine')
|
|
475
|
-
.option('-c, --config-path <path>', 'Path for the config file')
|
|
476
|
-
.option('-f, --force', 'Overwrite existing config file')
|
|
477
|
-
.option('-p, --path <path>', 'Working directory for initialization')
|
|
478
|
-
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
479
|
-
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
480
|
-
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
481
|
-
.option('--url <url>', 'Web application URL written to web.url in the local config')
|
|
482
|
-
.action(async (options) => {
|
|
483
590
|
try {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
configPath: options.configPath,
|
|
487
|
-
force: options.force,
|
|
488
|
-
path: options.path,
|
|
489
|
-
global: options.global,
|
|
490
|
-
provider: options.provider,
|
|
491
|
-
apiKey: options.apiKey,
|
|
492
|
-
url: options.url,
|
|
591
|
+
await ConfigParser.getInstance().loadConfig({
|
|
592
|
+
path: options.path || process.cwd(),
|
|
493
593
|
});
|
|
594
|
+
const { KnowsCommand } = await import('../src/commands/knows-command.js');
|
|
595
|
+
const explorBot = new ExplorBot({ path: options.path });
|
|
596
|
+
const command = new KnowsCommand(explorBot);
|
|
597
|
+
await command.execute(url || '');
|
|
494
598
|
}
|
|
495
599
|
catch (error) {
|
|
496
600
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
497
601
|
process.exit(1);
|
|
498
602
|
}
|
|
499
603
|
});
|
|
500
|
-
program
|
|
501
|
-
.command('clean [target]')
|
|
502
|
-
.description('Clean files: states, research, plans, tests, experiences, output (default: output + experiences)')
|
|
503
|
-
.option('-p, --path <path>', 'Custom path to clean')
|
|
504
|
-
.action(async (target, options) => {
|
|
505
|
-
const customPath = options.path;
|
|
506
|
-
const originalCwd = process.cwd();
|
|
507
|
-
const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
|
|
508
|
-
const targets = {
|
|
509
|
-
states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
|
|
510
|
-
research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
|
|
511
|
-
plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
|
|
512
|
-
tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
|
|
513
|
-
experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
|
|
514
|
-
output: { description: 'all output files', dir: path.join(basePath, 'output') },
|
|
515
|
-
};
|
|
516
|
-
function cleanDirectoryContents(dirPath) {
|
|
517
|
-
let count = 0;
|
|
518
|
-
for (const item of fs.readdirSync(dirPath)) {
|
|
519
|
-
const itemPath = path.join(dirPath, item);
|
|
520
|
-
if (fs.statSync(itemPath).isDirectory()) {
|
|
521
|
-
count += cleanDirectoryContents(itemPath);
|
|
522
|
-
fs.rmSync(itemPath, { recursive: true });
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
fs.unlinkSync(itemPath);
|
|
526
|
-
count++;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return count;
|
|
530
|
-
}
|
|
531
|
-
function cleanTarget(name) {
|
|
532
|
-
const t = targets[name];
|
|
533
|
-
if (!fs.existsSync(t.dir)) {
|
|
534
|
-
console.log(`${name}: nothing to clean (${t.dir} not found)`);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
const count = cleanDirectoryContents(t.dir);
|
|
538
|
-
console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
|
|
539
|
-
}
|
|
540
|
-
try {
|
|
541
|
-
if (target && !targets[target]) {
|
|
542
|
-
console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
|
|
543
|
-
process.exit(1);
|
|
544
|
-
}
|
|
545
|
-
if (!target) {
|
|
546
|
-
cleanTarget('output');
|
|
547
|
-
cleanTarget('experiences');
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
cleanTarget(target);
|
|
551
|
-
}
|
|
552
|
-
console.log('Cleanup completed successfully!');
|
|
553
|
-
}
|
|
554
|
-
catch (error) {
|
|
555
|
-
console.error('Failed to clean:', error);
|
|
556
|
-
process.exit(1);
|
|
557
|
-
}
|
|
558
|
-
});
|
|
604
|
+
program.commandsGroup(GROUPS.knowledge);
|
|
559
605
|
program
|
|
560
606
|
.command('learn [url] [description]')
|
|
561
|
-
.description('Add knowledge for
|
|
607
|
+
.description('Add knowledge for a URL')
|
|
562
608
|
.option('-p, --path <path>', 'Working directory path')
|
|
563
609
|
.option('--replace', 'Replace existing knowledge for this URL instead of appending')
|
|
564
610
|
.action(async (url, description, options) => {
|
|
@@ -585,28 +631,9 @@ program
|
|
|
585
631
|
process.exit(1);
|
|
586
632
|
}
|
|
587
633
|
});
|
|
588
|
-
program
|
|
589
|
-
.command('knows [url]')
|
|
590
|
-
.description('List all knowledge URLs or show matching knowledge for a URL')
|
|
591
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
592
|
-
.action(async (url, options) => {
|
|
593
|
-
try {
|
|
594
|
-
await ConfigParser.getInstance().loadConfig({
|
|
595
|
-
path: options.path || process.cwd(),
|
|
596
|
-
});
|
|
597
|
-
const { KnowsCommand } = await import('../src/commands/knows-command.js');
|
|
598
|
-
const explorBot = new ExplorBot({ path: options.path });
|
|
599
|
-
const command = new KnowsCommand(explorBot);
|
|
600
|
-
await command.execute(url || '');
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
604
|
-
process.exit(1);
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
634
|
program
|
|
608
635
|
.command('compact [target]')
|
|
609
|
-
.description('Compact
|
|
636
|
+
.description('Compact experience files; pass a filename or URL substring to narrow')
|
|
610
637
|
.option('--dry-run', 'Preview without running AI or writing files')
|
|
611
638
|
.option('--no-merge', 'Skip the cross-URL merge step when compacting all')
|
|
612
639
|
.option('-p, --path <path>', 'Working directory path')
|
|
@@ -633,152 +660,105 @@ program
|
|
|
633
660
|
}
|
|
634
661
|
});
|
|
635
662
|
program
|
|
636
|
-
.command('
|
|
637
|
-
.description('
|
|
663
|
+
.command('add-rule [agent] [name]')
|
|
664
|
+
.description('Create a rule file for an agent')
|
|
665
|
+
.option('--url <pattern>', 'URL pattern for this rule')
|
|
638
666
|
.option('-p, --path <path>', 'Working directory path')
|
|
639
|
-
.
|
|
640
|
-
.
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
path: options.path || process.cwd(),
|
|
667
|
+
.action(async (agent, name, options) => {
|
|
668
|
+
if (options.path)
|
|
669
|
+
process.chdir(path.resolve(options.path));
|
|
670
|
+
if (agent && name) {
|
|
671
|
+
const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
|
|
672
|
+
const result = AddRuleCommand.createRuleFile(agent, name, {
|
|
673
|
+
urlPattern: options.url,
|
|
647
674
|
});
|
|
648
|
-
|
|
649
|
-
const explorBot = new ExplorBot({ path: options.path });
|
|
650
|
-
const command = new ExperienceCommand(explorBot);
|
|
651
|
-
const flags = [];
|
|
652
|
-
if (options.recent)
|
|
653
|
-
flags.push('--recent');
|
|
654
|
-
if (options.old)
|
|
655
|
-
flags.push('--old');
|
|
656
|
-
const args = [...flags, filter, index].filter(Boolean).join(' ');
|
|
657
|
-
await command.execute(args);
|
|
658
|
-
command.printSuggestions();
|
|
659
|
-
}
|
|
660
|
-
catch (error) {
|
|
661
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
662
|
-
process.exit(1);
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
addCommonOptions(program.command('research <url>').description('Research a page and print UI analysis').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(async (url, options) => {
|
|
666
|
-
try {
|
|
667
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
668
|
-
await explorBot.start();
|
|
669
|
-
const argParts = [url];
|
|
670
|
-
if (options.data)
|
|
671
|
-
argParts.push('--data');
|
|
672
|
-
if (options.deep)
|
|
673
|
-
argParts.push('--deep');
|
|
674
|
-
if (options.fix === false)
|
|
675
|
-
argParts.push('--no-fix');
|
|
676
|
-
const { ResearchCommand } = await import('../src/commands/research-command.js');
|
|
677
|
-
await new ResearchCommand(explorBot).execute(argParts.join(' '));
|
|
678
|
-
await explorBot.stop();
|
|
679
|
-
await showStatsAndExit(0);
|
|
680
|
-
}
|
|
681
|
-
catch (error) {
|
|
682
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
683
|
-
await showStatsAndExit(1);
|
|
675
|
+
process.exit(result ? 0 : 1);
|
|
684
676
|
}
|
|
677
|
+
const AddRule = (await import('../src/components/AddRule.js')).default;
|
|
678
|
+
render(React.createElement(AddRule, {
|
|
679
|
+
initialAgent: agent || '',
|
|
680
|
+
initialName: name || '',
|
|
681
|
+
}), {
|
|
682
|
+
exitOnCtrlC: false,
|
|
683
|
+
patchConsole: false,
|
|
684
|
+
});
|
|
685
685
|
});
|
|
686
|
-
|
|
686
|
+
program
|
|
687
|
+
.command('extract-rules <agent>')
|
|
688
|
+
.description("Copy an agent's built-in rules and planning styles out to customize")
|
|
689
|
+
.option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
|
|
690
|
+
.action(async (agent, options) => {
|
|
687
691
|
try {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
692
|
+
const { RulesLoader } = await import('../src/utils/rules-loader.js');
|
|
693
|
+
const targetDir = options.dir || path.resolve(`./rules/${agent}`);
|
|
694
|
+
const extracted = RulesLoader.extractRules(agent, targetDir);
|
|
695
|
+
if (extracted.length === 0) {
|
|
696
|
+
console.log('All rule files already exist in target directory.');
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
|
|
700
|
+
}
|
|
694
701
|
}
|
|
695
702
|
catch (error) {
|
|
696
703
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
697
|
-
|
|
704
|
+
process.exit(1);
|
|
698
705
|
}
|
|
699
706
|
});
|
|
700
|
-
|
|
707
|
+
program.commandsGroup(GROUPS.setup);
|
|
708
|
+
program
|
|
709
|
+
.command('init')
|
|
710
|
+
.description('Initialize configuration for a project or for this machine')
|
|
711
|
+
.option('-c, --config-path <path>', 'Path for the config file')
|
|
712
|
+
.option('-f, --force', 'Overwrite existing config file')
|
|
713
|
+
.option('-p, --path <path>', 'Working directory for initialization')
|
|
714
|
+
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
715
|
+
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
716
|
+
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
717
|
+
.option('--url <url>', 'Web application URL written to web.url in the local config')
|
|
718
|
+
.action(async (options) => {
|
|
701
719
|
try {
|
|
702
|
-
const
|
|
703
|
-
await
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
720
|
+
const { runInit } = await import('../src/commands/init-command.js');
|
|
721
|
+
await runInit({
|
|
722
|
+
configPath: options.configPath,
|
|
723
|
+
force: options.force,
|
|
724
|
+
path: options.path,
|
|
725
|
+
global: options.global,
|
|
726
|
+
provider: options.provider,
|
|
727
|
+
apiKey: options.apiKey,
|
|
728
|
+
url: options.url,
|
|
709
729
|
});
|
|
710
|
-
console.log(`\nDrill completed: ${plan.tests.length} components`);
|
|
711
|
-
console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
|
|
712
|
-
console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
|
|
713
|
-
await explorBot.stop();
|
|
714
|
-
await showStatsAndExit(0);
|
|
715
730
|
}
|
|
716
731
|
catch (error) {
|
|
717
732
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
718
|
-
|
|
733
|
+
process.exit(1);
|
|
719
734
|
}
|
|
720
735
|
});
|
|
721
736
|
program
|
|
722
|
-
.command('
|
|
723
|
-
.description('
|
|
724
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
737
|
+
.command('config [url]')
|
|
738
|
+
.description('Show models, config file and paths used by this run')
|
|
725
739
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
726
|
-
.option('--
|
|
727
|
-
.option('--
|
|
728
|
-
.option('--compact', 'Compact view with summaries')
|
|
729
|
-
.option('--attached', 'Only auto-attached sections (default)')
|
|
730
|
-
.option('--visual', 'Annotate elements on screenshot and print screenshot path')
|
|
731
|
-
.option('--screenshot', 'Alias for --visual')
|
|
740
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
741
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
732
742
|
.action(async (url, options) => {
|
|
743
|
+
setQuietMode(!isVerboseMode());
|
|
744
|
+
const { ConfigCommand } = await import('../src/commands/config-command.js');
|
|
733
745
|
try {
|
|
734
|
-
|
|
735
|
-
path: options.path,
|
|
736
|
-
config: options.config,
|
|
737
|
-
headless: true,
|
|
738
|
-
session: options.session,
|
|
739
|
-
};
|
|
740
|
-
const explorBot = new ExplorBot(mainOptions);
|
|
741
|
-
await explorBot.start();
|
|
742
|
-
await explorBot.visit(url);
|
|
743
|
-
const { ContextCommand } = await import('../src/commands/context-command.js');
|
|
744
|
-
const argParts = [];
|
|
745
|
-
if (options.full)
|
|
746
|
-
argParts.push('--full');
|
|
747
|
-
else if (options.compact)
|
|
748
|
-
argParts.push('--compact');
|
|
749
|
-
else
|
|
750
|
-
argParts.push('--attached');
|
|
751
|
-
if (options.visual || options.screenshot)
|
|
752
|
-
argParts.push('--visual');
|
|
753
|
-
await new ContextCommand(explorBot).execute(argParts.join(' '));
|
|
754
|
-
await explorBot.stop();
|
|
755
|
-
await showStatsAndExit(0);
|
|
746
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
756
747
|
}
|
|
757
748
|
catch (error) {
|
|
758
|
-
console.error(
|
|
759
|
-
|
|
749
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
750
|
+
process.exit(1);
|
|
760
751
|
}
|
|
761
752
|
});
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
log('Command executed successfully');
|
|
770
|
-
const state = explorBot.stateManager().getCurrentState();
|
|
771
|
-
if (state)
|
|
772
|
-
log(`URL: ${state.url}`);
|
|
773
|
-
await explorBot.stop();
|
|
774
|
-
await showStatsAndExit(0);
|
|
775
|
-
}
|
|
776
|
-
catch (error) {
|
|
777
|
-
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
778
|
-
await showStatsAndExit(1);
|
|
779
|
-
}
|
|
753
|
+
RecommendedModelsCommand.register(program);
|
|
754
|
+
program
|
|
755
|
+
.command('sites')
|
|
756
|
+
.description('List sites registered in the global installation')
|
|
757
|
+
.action(async () => {
|
|
758
|
+
const { SitesCommand } = await import('../src/commands/sites-command.js');
|
|
759
|
+
await new SitesCommand(new ExplorBot()).execute('');
|
|
780
760
|
});
|
|
781
|
-
const browserCmd = program.command('browser').description('
|
|
761
|
+
const browserCmd = program.command('browser').description('Start, stop or check a persistent browser server');
|
|
782
762
|
browserCmd
|
|
783
763
|
.command('start')
|
|
784
764
|
.description('Launch a persistent browser server')
|
|
@@ -847,72 +827,104 @@ browserCmd
|
|
|
847
827
|
}
|
|
848
828
|
});
|
|
849
829
|
program
|
|
850
|
-
.command('
|
|
851
|
-
.description('
|
|
852
|
-
.option('-
|
|
853
|
-
.action(async (
|
|
830
|
+
.command('clean [target]')
|
|
831
|
+
.description('Delete states, research, plans, tests, experiences or output (default: output + experiences)')
|
|
832
|
+
.option('-p, --path <path>', 'Custom path to clean')
|
|
833
|
+
.action(async (target, options) => {
|
|
834
|
+
const customPath = options.path;
|
|
835
|
+
const originalCwd = process.cwd();
|
|
836
|
+
const basePath = customPath ? path.resolve(originalCwd, customPath) : process.cwd();
|
|
837
|
+
const targets = {
|
|
838
|
+
states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
|
|
839
|
+
research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
|
|
840
|
+
plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
|
|
841
|
+
tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
|
|
842
|
+
experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
|
|
843
|
+
output: { description: 'all output files', dir: path.join(basePath, 'output') },
|
|
844
|
+
};
|
|
845
|
+
function cleanDirectoryContents(dirPath) {
|
|
846
|
+
let count = 0;
|
|
847
|
+
for (const item of fs.readdirSync(dirPath)) {
|
|
848
|
+
const itemPath = path.join(dirPath, item);
|
|
849
|
+
if (fs.statSync(itemPath).isDirectory()) {
|
|
850
|
+
count += cleanDirectoryContents(itemPath);
|
|
851
|
+
fs.rmSync(itemPath, { recursive: true });
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
fs.unlinkSync(itemPath);
|
|
855
|
+
count++;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return count;
|
|
859
|
+
}
|
|
860
|
+
function cleanTarget(name) {
|
|
861
|
+
const t = targets[name];
|
|
862
|
+
if (!fs.existsSync(t.dir)) {
|
|
863
|
+
console.log(`${name}: nothing to clean (${t.dir} not found)`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const count = cleanDirectoryContents(t.dir);
|
|
867
|
+
console.log(`Cleaned ${count} ${t.description} files from ${t.dir}`);
|
|
868
|
+
}
|
|
854
869
|
try {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
870
|
+
if (target && !targets[target]) {
|
|
871
|
+
console.error(`Unknown target: ${target}. Available: ${Object.keys(targets).join(', ')}`);
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
if (!target) {
|
|
875
|
+
cleanTarget('output');
|
|
876
|
+
cleanTarget('experiences');
|
|
860
877
|
}
|
|
861
878
|
else {
|
|
862
|
-
|
|
879
|
+
cleanTarget(target);
|
|
863
880
|
}
|
|
881
|
+
console.log('Cleanup completed successfully!');
|
|
864
882
|
}
|
|
865
883
|
catch (error) {
|
|
866
|
-
console.error('Failed
|
|
884
|
+
console.error('Failed to clean:', error);
|
|
867
885
|
process.exit(1);
|
|
868
886
|
}
|
|
869
887
|
});
|
|
870
|
-
program
|
|
871
|
-
.command('add-rule [agent] [name]')
|
|
872
|
-
.description('Create a rule file for an agent')
|
|
873
|
-
.option('--url <pattern>', 'URL pattern for this rule')
|
|
874
|
-
.option('-p, --path <path>', 'Working directory path')
|
|
875
|
-
.action(async (agent, name, options) => {
|
|
876
|
-
if (options.path)
|
|
877
|
-
process.chdir(path.resolve(options.path));
|
|
878
|
-
if (agent && name) {
|
|
879
|
-
const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
|
|
880
|
-
const result = AddRuleCommand.createRuleFile(agent, name, {
|
|
881
|
-
urlPattern: options.url,
|
|
882
|
-
});
|
|
883
|
-
process.exit(result ? 0 : 1);
|
|
884
|
-
}
|
|
885
|
-
const AddRule = (await import('../src/components/AddRule.js')).default;
|
|
886
|
-
render(React.createElement(AddRule, {
|
|
887
|
-
initialAgent: agent || '',
|
|
888
|
-
initialName: name || '',
|
|
889
|
-
}), {
|
|
890
|
-
exitOnCtrlC: false,
|
|
891
|
-
patchConsole: false,
|
|
892
|
-
});
|
|
893
|
-
});
|
|
894
|
-
import { createApiCommands } from "../boat/api-tester/src/cli.js";
|
|
895
|
-
import { createDocsCommands } from "../boat/doc-collector/src/cli.js";
|
|
896
|
-
import { createPrimaCommands } from "../boat/prima/src/cli.js";
|
|
888
|
+
program.commandsGroup(GROUPS.tools);
|
|
897
889
|
program.addCommand(createApiCommands('api'));
|
|
898
890
|
program.addCommand(createDocsCommands('docs'));
|
|
899
891
|
program.addCommand(createPrimaCommands('prima'));
|
|
900
892
|
const envHelp = () => {
|
|
901
893
|
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
902
|
-
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
|
|
894
|
+
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${chalk.green(v.name.padEnd(width))} ${v.description}`).join('\n');
|
|
903
895
|
return `
|
|
904
|
-
Environment variables
|
|
896
|
+
${chalk.yellow.bold('Environment variables:')}
|
|
905
897
|
Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
|
|
906
898
|
|
|
907
899
|
${rows}
|
|
908
900
|
|
|
909
901
|
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
902
|
+
|
|
903
|
+
${chalk.yellow.bold('Example:')}
|
|
904
|
+
EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
|
|
905
|
+
${cli} explore /login --max-tests 3
|
|
913
906
|
`;
|
|
914
907
|
};
|
|
915
|
-
program
|
|
916
|
-
|
|
908
|
+
program
|
|
909
|
+
.command('env')
|
|
910
|
+
.description('Show environment variables for running without a config file')
|
|
911
|
+
.helpGroup(GROUPS.setup)
|
|
912
|
+
.addHelpText('after', envHelp)
|
|
913
|
+
.action(() => console.log(envHelp()));
|
|
914
|
+
const examples = [
|
|
915
|
+
[`${cli} start`, 'interactive TUI, drive it with /commands'],
|
|
916
|
+
[`${cli} explore /login`, 'invent and run tests for one page'],
|
|
917
|
+
[`${cli} freesail`, 'roam the whole app on its own'],
|
|
918
|
+
[`${cli} plan /users`, 'write a test plan, then run it with test'],
|
|
919
|
+
];
|
|
920
|
+
const exampleWidth = Math.max(...examples.map(([command]) => command.length));
|
|
921
|
+
program.addHelpText('after', `
|
|
922
|
+
${chalk.yellow.bold('Examples:')}
|
|
923
|
+
${examples.map(([command, note]) => ` ${chalk.cyan(command.padEnd(exampleWidth))} ${chalk.dim(note)}`).join('\n')}
|
|
924
|
+
|
|
925
|
+
${chalk.dim(`Environment variables: ${cli} help env · JSON definitions for agents: ${cli} help-json [command...]`)}
|
|
926
|
+
`);
|
|
927
|
+
program.commandsGroup(GROUPS.help);
|
|
928
|
+
program.helpCommand(true);
|
|
917
929
|
HelpJsonCommand.register(program);
|
|
918
930
|
program.parse();
|