@jackwener/opencli 1.7.14 → 1.7.16
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 +9 -6
- package/README.zh-CN.md +9 -6
- package/cli-manifest.json +374 -74
- package/clis/bilibili/subtitle.js +1 -1
- package/clis/chatgpt/ask.js +2 -1
- package/clis/chatgpt/detail.js +6 -1
- package/clis/chatgpt/read.js +2 -1
- package/clis/chatgpt/send.js +2 -1
- package/clis/chatgpt/utils.js +54 -12
- package/clis/chatgpt/utils.test.js +36 -1
- package/clis/claude/ask.js +22 -7
- package/clis/claude/detail.js +9 -2
- package/clis/claude/new.js +8 -2
- package/clis/claude/read.js +2 -1
- package/clis/claude/send.js +8 -3
- package/clis/claude/utils.js +27 -4
- package/clis/deepseek/ask.js +21 -8
- package/clis/deepseek/detail.js +9 -1
- package/clis/deepseek/new.js +13 -2
- package/clis/deepseek/read.js +2 -1
- package/clis/deepseek/utils.js +8 -1
- package/clis/dianping/cityResolver.js +185 -0
- package/clis/dianping/dianping.test.js +154 -0
- package/clis/dianping/search.js +6 -3
- package/clis/douyin/_shared/browser-fetch.js +14 -2
- package/clis/douyin/_shared/browser-fetch.test.js +13 -0
- package/clis/douyin/stats.js +1 -1
- package/clis/douyin/update.js +1 -1
- package/clis/jike/search.js +1 -1
- package/clis/linkedin/search.js +8 -11
- package/clis/maimai/search-talents.js +10 -6
- package/clis/openreview/author.js +58 -0
- package/clis/openreview/openreview.test.js +83 -1
- package/clis/openreview/utils.js +14 -0
- package/clis/reddit/comment.js +1 -0
- package/clis/reddit/frontpage.js +1 -0
- package/clis/reddit/popular.js +1 -0
- package/clis/reddit/read.js +2 -0
- package/clis/reddit/read.test.js +4 -0
- package/clis/reddit/save.js +1 -0
- package/clis/reddit/saved.js +1 -0
- package/clis/reddit/search.js +2 -1
- package/clis/reddit/subreddit.js +2 -1
- package/clis/reddit/subscribe.js +1 -0
- package/clis/reddit/upvote.js +1 -0
- package/clis/reddit/upvoted.js +1 -0
- package/clis/reddit/user-comments.js +2 -1
- package/clis/reddit/user-posts.js +2 -1
- package/clis/reddit/user.js +2 -1
- package/clis/twitter/article.js +9 -5
- package/clis/twitter/bookmark-folder.js +187 -0
- package/clis/twitter/bookmark-folder.test.js +337 -0
- package/clis/twitter/bookmark-folders.js +115 -0
- package/clis/twitter/bookmark-folders.test.js +152 -0
- package/clis/twitter/bookmark.js +15 -6
- package/clis/twitter/bookmark.test.js +74 -0
- package/clis/twitter/bookmarks.js +10 -10
- package/clis/twitter/delete.js +11 -35
- package/clis/twitter/delete.test.js +21 -9
- package/clis/twitter/download.js +6 -5
- package/clis/twitter/followers.js +10 -3
- package/clis/twitter/following.js +14 -11
- package/clis/twitter/following.test.js +2 -1
- package/clis/twitter/hide-reply.js +24 -5
- package/clis/twitter/hide-reply.test.js +76 -0
- package/clis/twitter/like.js +21 -11
- package/clis/twitter/like.test.js +73 -0
- package/clis/twitter/likes.js +11 -11
- package/clis/twitter/list-add.js +8 -7
- package/clis/twitter/list-add.test.js +23 -1
- package/clis/twitter/list-remove.js +8 -7
- package/clis/twitter/list-remove.test.js +23 -1
- package/clis/twitter/list-tweets.js +9 -9
- package/clis/twitter/lists.js +6 -8
- package/clis/twitter/notifications.js +3 -2
- package/clis/twitter/profile.js +11 -7
- package/clis/twitter/quote.js +60 -32
- package/clis/twitter/quote.test.js +96 -8
- package/clis/twitter/reply.js +24 -178
- package/clis/twitter/reply.test.js +29 -11
- package/clis/twitter/retweet.js +9 -14
- package/clis/twitter/retweet.test.js +5 -1
- package/clis/twitter/search.js +176 -23
- package/clis/twitter/search.test.js +266 -1
- package/clis/twitter/shared.js +43 -0
- package/clis/twitter/shared.test.js +107 -1
- package/clis/twitter/thread.js +11 -11
- package/clis/twitter/timeline.js +13 -13
- package/clis/twitter/trending.js +4 -4
- package/clis/twitter/tweets.js +8 -9
- package/clis/twitter/unbookmark.js +13 -6
- package/clis/twitter/unbookmark.test.js +73 -0
- package/clis/twitter/unlike.js +6 -13
- package/clis/twitter/unlike.test.js +5 -2
- package/clis/twitter/unretweet.js +9 -14
- package/clis/twitter/unretweet.test.js +5 -1
- package/clis/twitter/utils.js +286 -0
- package/clis/twitter/utils.test.js +169 -0
- package/clis/youtube/like.js +6 -2
- package/clis/youtube/subscribe.js +6 -2
- package/clis/youtube/unlike.js +6 -2
- package/clis/youtube/unsubscribe.js +6 -2
- package/clis/youtube/utils.js +19 -13
- package/clis/youtube/utils.test.js +17 -1
- package/dist/src/browser/ax-snapshot.d.ts +37 -0
- package/dist/src/browser/ax-snapshot.js +217 -0
- package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
- package/dist/src/browser/ax-snapshot.test.js +91 -0
- package/dist/src/browser/base-page.d.ts +51 -0
- package/dist/src/browser/base-page.js +545 -2
- package/dist/src/browser/base-page.test.js +520 -4
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +1 -1
- package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
- package/dist/src/browser/cdp-click-fixture.test.js +87 -0
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +5 -0
- package/dist/src/browser/cdp.test.js +1 -0
- package/dist/src/browser/daemon-client.d.ts +5 -3
- package/dist/src/browser/daemon-client.js +6 -3
- package/dist/src/browser/daemon-client.test.js +10 -0
- package/dist/src/browser/find.d.ts +9 -1
- package/dist/src/browser/find.js +219 -0
- package/dist/src/browser/find.test.js +61 -1
- package/dist/src/browser/page.d.ts +4 -2
- package/dist/src/browser/page.js +18 -1
- package/dist/src/browser/page.test.js +28 -0
- package/dist/src/browser/target-errors.d.ts +3 -1
- package/dist/src/browser/target-errors.js +2 -0
- package/dist/src/browser/target-resolver.d.ts +14 -0
- package/dist/src/browser/target-resolver.js +28 -0
- package/dist/src/browser/visual-refs.d.ts +11 -0
- package/dist/src/browser/visual-refs.js +108 -0
- package/dist/src/build-manifest.d.ts +23 -0
- package/dist/src/build-manifest.js +34 -0
- package/dist/src/build-manifest.test.js +108 -1
- package/dist/src/cli.js +630 -60
- package/dist/src/cli.test.js +731 -1
- package/dist/src/commanderAdapter.js +7 -0
- package/dist/src/doctor.js +2 -2
- package/dist/src/doctor.test.js +4 -4
- package/dist/src/execution.d.ts +2 -0
- package/dist/src/execution.js +31 -6
- package/dist/src/execution.test.js +43 -16
- package/dist/src/external-clis.yaml +24 -0
- package/dist/src/help.d.ts +33 -0
- package/dist/src/help.js +174 -0
- package/dist/src/main.js +4 -14
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +83 -1
- package/package.json +1 -1
- package/scripts/typed-error-lint-baseline.json +18 -18
package/dist/src/cli.js
CHANGED
|
@@ -18,11 +18,11 @@ import { PKG_VERSION } from './version.js';
|
|
|
18
18
|
import { printCompletionScript } from './completion.js';
|
|
19
19
|
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
|
|
20
20
|
import { registerAllCommands } from './commanderAdapter.js';
|
|
21
|
-
import { classifyAdapter, formatRootAdapterHelpText, installStructuredHelp, rootHelpData } from './help.js';
|
|
21
|
+
import { classifyAdapter, formatRootAdapterHelpText, installCommanderNamespaceStructuredHelp, installStructuredHelp, rootHelpData } from './help.js';
|
|
22
22
|
import { EXIT_CODES, getErrorMessage, BrowserConnectError } from './errors.js';
|
|
23
23
|
import { TargetError } from './browser/target-errors.js';
|
|
24
24
|
import { resolveTargetJs, getTextResolvedJs, getValueResolvedJs, getAttributesResolvedJs, selectResolvedJs, isAutocompleteResolvedJs } from './browser/target-resolver.js';
|
|
25
|
-
import { buildFindJs, isFindError } from './browser/find.js';
|
|
25
|
+
import { buildFindJs, buildSemanticFindJs, isFindError } from './browser/find.js';
|
|
26
26
|
import { inferShape } from './browser/shape.js';
|
|
27
27
|
import { assignKeys } from './browser/network-key.js';
|
|
28
28
|
import { DEFAULT_TTL_MS, findEntry, loadNetworkCache, saveNetworkCache } from './browser/network-cache.js';
|
|
@@ -313,7 +313,7 @@ async function resolveStoredBrowserTarget(page, scope = DEFAULT_BROWSER_WORKSPAC
|
|
|
313
313
|
return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
|
|
314
314
|
}
|
|
315
315
|
/** Create a browser page for browser commands. Uses a dedicated browser workspace for session persistence. */
|
|
316
|
-
async function getBrowserPage(targetPage, workspace = DEFAULT_BROWSER_WORKSPACE, contextId) {
|
|
316
|
+
async function getBrowserPage(targetPage, workspace = DEFAULT_BROWSER_WORKSPACE, contextId, opts = {}) {
|
|
317
317
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
318
318
|
const bridge = new BrowserBridge();
|
|
319
319
|
// Idle timeout: how long the browser workspace lease stays alive between commands
|
|
@@ -325,6 +325,7 @@ async function getBrowserPage(targetPage, workspace = DEFAULT_BROWSER_WORKSPACE,
|
|
|
325
325
|
workspace,
|
|
326
326
|
...(contextId && { contextId }),
|
|
327
327
|
...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
|
|
328
|
+
windowMode: opts.windowMode ?? getBrowserWindowMode(undefined, 'foreground'),
|
|
328
329
|
});
|
|
329
330
|
const targetScope = getBrowserScope(workspace, contextId);
|
|
330
331
|
const resolvedTargetPage = targetPage
|
|
@@ -338,6 +339,43 @@ async function getBrowserPage(targetPage, workspace = DEFAULT_BROWSER_WORKSPACE,
|
|
|
338
339
|
}
|
|
339
340
|
return page;
|
|
340
341
|
}
|
|
342
|
+
function getBrowserWindowMode(command, defaultMode) {
|
|
343
|
+
const optionRaw = getCommandOption(command, 'window');
|
|
344
|
+
if (optionRaw !== undefined && optionRaw !== '') {
|
|
345
|
+
if (optionRaw === 'foreground' || optionRaw === 'background')
|
|
346
|
+
return optionRaw;
|
|
347
|
+
throw new Error(`--window must be one of: foreground, background. Received: "${String(optionRaw)}"`);
|
|
348
|
+
}
|
|
349
|
+
const envRaw = process.env.OPENCLI_WINDOW;
|
|
350
|
+
if (envRaw !== undefined && envRaw !== '') {
|
|
351
|
+
if (envRaw === 'foreground' || envRaw === 'background')
|
|
352
|
+
return envRaw;
|
|
353
|
+
throw new Error(`OPENCLI_WINDOW must be one of: foreground, background. Received: "${envRaw}"`);
|
|
354
|
+
}
|
|
355
|
+
return defaultMode;
|
|
356
|
+
}
|
|
357
|
+
function parseBrowserBoolean(name, raw) {
|
|
358
|
+
if (raw === undefined || raw === '')
|
|
359
|
+
return undefined;
|
|
360
|
+
if (raw === 'true')
|
|
361
|
+
return true;
|
|
362
|
+
if (raw === 'false')
|
|
363
|
+
return false;
|
|
364
|
+
throw new Error(`${name} must be one of: true, false. Received: "${String(raw)}"`);
|
|
365
|
+
}
|
|
366
|
+
function getBrowserKeepTab(command, defaultValue) {
|
|
367
|
+
return parseBrowserBoolean('--keep-tab', getCommandOption(command, 'keepTab'))
|
|
368
|
+
?? parseBrowserBoolean('OPENCLI_KEEP_TAB', process.env.OPENCLI_KEEP_TAB)
|
|
369
|
+
?? defaultValue;
|
|
370
|
+
}
|
|
371
|
+
function hasExplicitBrowserWindowOption(command) {
|
|
372
|
+
const raw = getCommandOption(command, 'window');
|
|
373
|
+
return raw !== undefined && raw !== '';
|
|
374
|
+
}
|
|
375
|
+
function hasExplicitBrowserKeepTabOption(command) {
|
|
376
|
+
const raw = getCommandOption(command, 'keepTab');
|
|
377
|
+
return raw !== undefined && raw !== '';
|
|
378
|
+
}
|
|
341
379
|
function addBrowserTabOption(command) {
|
|
342
380
|
return command.option('--tab <targetId>', BROWSER_TAB_OPTION_DESCRIPTION);
|
|
343
381
|
}
|
|
@@ -373,6 +411,41 @@ function getPageScope(page) {
|
|
|
373
411
|
const contextId = page.contextId;
|
|
374
412
|
return getBrowserScope(getPageWorkspace(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
|
|
375
413
|
}
|
|
414
|
+
function snapshotMetricText(snapshot) {
|
|
415
|
+
return typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2);
|
|
416
|
+
}
|
|
417
|
+
function snapshotMetrics(snapshot, elapsedMs) {
|
|
418
|
+
const text = snapshotMetricText(snapshot);
|
|
419
|
+
const interactiveMatch = text.match(/^interactive:\s*(\d+)\s*$/m);
|
|
420
|
+
return {
|
|
421
|
+
ok: true,
|
|
422
|
+
chars: text.length,
|
|
423
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
424
|
+
lines: text ? text.split(/\r?\n/).length : 0,
|
|
425
|
+
approx_tokens: Math.ceil(text.length / 4),
|
|
426
|
+
refs: (text.match(/(^|\n)\s*\[\d+\]/g) ?? []).length,
|
|
427
|
+
frame_sections: (text.match(/(^|\n)frame /g) ?? []).length,
|
|
428
|
+
...(interactiveMatch ? { interactive: Number(interactiveMatch[1]) } : {}),
|
|
429
|
+
elapsed_ms: elapsedMs,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
async function snapshotSourceMetrics(page, source) {
|
|
433
|
+
const started = Date.now();
|
|
434
|
+
try {
|
|
435
|
+
const snapshot = await page.snapshot({ viewportExpand: 2000, source });
|
|
436
|
+
return snapshotMetrics(snapshot, Date.now() - started);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
elapsed_ms: Date.now() - started,
|
|
442
|
+
error: {
|
|
443
|
+
...(err instanceof Error && 'code' in err ? { code: String(err.code) } : {}),
|
|
444
|
+
message: err instanceof Error ? err.message : String(err),
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
376
449
|
function resolveBrowserTabTarget(targetId, opts) {
|
|
377
450
|
if (typeof targetId === 'string' && targetId.trim())
|
|
378
451
|
return targetId.trim();
|
|
@@ -548,7 +621,10 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
548
621
|
const browser = program
|
|
549
622
|
.command('browser')
|
|
550
623
|
.option('--workspace <name>', 'Browser workspace to use (default: browser:default; bound tabs use bound:<name>)')
|
|
624
|
+
.option('--window <mode>', 'Browser window mode: foreground or background')
|
|
625
|
+
.option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes')
|
|
551
626
|
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)');
|
|
627
|
+
const originalBrowserDescription = browser.description();
|
|
552
628
|
/**
|
|
553
629
|
* Resolve a `<target>` (numeric ref or CSS selector) via the unified resolver.
|
|
554
630
|
* Returns the CSS match count so callers can propagate `matches_n` into the
|
|
@@ -610,12 +686,20 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
610
686
|
/** Wrap browser actions with error handling and optional --json output */
|
|
611
687
|
function browserAction(fn) {
|
|
612
688
|
return async (...args) => {
|
|
689
|
+
let page = null;
|
|
690
|
+
let shouldReleasePage = false;
|
|
613
691
|
try {
|
|
614
692
|
const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
|
|
615
693
|
const targetPage = getBrowserTargetId(command);
|
|
616
694
|
const workspace = getBrowserWorkspace(command);
|
|
617
695
|
const contextId = getBrowserContextId(command);
|
|
618
|
-
const
|
|
696
|
+
const windowMode = getBrowserWindowMode(command, 'foreground');
|
|
697
|
+
const keepTab = getBrowserKeepTab(command, true);
|
|
698
|
+
shouldReleasePage = !keepTab && !workspace.startsWith('bound:');
|
|
699
|
+
if (workspace.startsWith('bound:') && (hasExplicitBrowserWindowOption(command) || hasExplicitBrowserKeepTabOption(command))) {
|
|
700
|
+
log.warn('--window/--keep-tab ignored for bound:* workspaces; bound tabs are user-owned.');
|
|
701
|
+
}
|
|
702
|
+
page = await getBrowserPage(targetPage, workspace, contextId, { windowMode });
|
|
619
703
|
await fn(page, ...args);
|
|
620
704
|
}
|
|
621
705
|
catch (err) {
|
|
@@ -663,6 +747,14 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
663
747
|
}
|
|
664
748
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
665
749
|
}
|
|
750
|
+
finally {
|
|
751
|
+
if (shouldReleasePage && page?.closeWindow) {
|
|
752
|
+
await page.closeWindow().catch((err) => {
|
|
753
|
+
if (process.env.OPENCLI_VERBOSE)
|
|
754
|
+
log.warn(`[browser] Failed to release tab lease: ${getErrorMessage(err)}`);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
666
758
|
};
|
|
667
759
|
}
|
|
668
760
|
browser.command('bind')
|
|
@@ -887,9 +979,33 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
887
979
|
console.log(`Scrolled ${direction}`);
|
|
888
980
|
}));
|
|
889
981
|
// ── Inspect ──
|
|
890
|
-
addBrowserTabOption(browser.command('state').description('Page state: URL, title, interactive elements with [N] indices')
|
|
891
|
-
.
|
|
892
|
-
|
|
982
|
+
addBrowserTabOption(browser.command('state').description('Page state: URL, title, interactive elements with [N] indices')
|
|
983
|
+
.option('--source <source>', 'Snapshot backend: dom (default) or ax prototype', 'dom')
|
|
984
|
+
.option('--compare-sources', 'Print DOM vs AX snapshot metrics for observation promotion decisions', false))
|
|
985
|
+
.action(browserAction(async (page, opts) => {
|
|
986
|
+
if (opts.compareSources === true) {
|
|
987
|
+
const [dom, ax] = await Promise.all([
|
|
988
|
+
snapshotSourceMetrics(page, 'dom'),
|
|
989
|
+
snapshotSourceMetrics(page, 'ax'),
|
|
990
|
+
]);
|
|
991
|
+
console.log(JSON.stringify({
|
|
992
|
+
url: await page.getCurrentUrl?.() ?? '',
|
|
993
|
+
sources: { dom, ax },
|
|
994
|
+
}, null, 2));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const source = String(opts.source ?? 'dom').toLowerCase();
|
|
998
|
+
if (source !== 'dom' && source !== 'ax') {
|
|
999
|
+
console.log(JSON.stringify({
|
|
1000
|
+
error: {
|
|
1001
|
+
code: 'invalid_source',
|
|
1002
|
+
message: `--source must be "dom" or "ax", got "${opts.source}"`,
|
|
1003
|
+
},
|
|
1004
|
+
}, null, 2));
|
|
1005
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const snapshot = await page.snapshot({ viewportExpand: 2000, source: source });
|
|
893
1009
|
const url = await page.getCurrentUrl?.() ?? '';
|
|
894
1010
|
console.log(`URL: ${url}\n`);
|
|
895
1011
|
console.log(typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2));
|
|
@@ -901,21 +1017,26 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
901
1017
|
}));
|
|
902
1018
|
addBrowserTabOption(browser.command('screenshot').argument('[path]', 'Save to file (base64 if omitted)'))
|
|
903
1019
|
.option('--full-page', 'Capture the full scrollable page, not just the viewport', false)
|
|
1020
|
+
.option('--annotate', 'Overlay visible browser state ref labels on the screenshot', false)
|
|
904
1021
|
.option('--width <n>', 'Override viewport width in CSS pixels for this screenshot only', (v) => parseScreenshotDim(v, 'width'))
|
|
905
1022
|
.option('--height <n>', 'Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)', (v) => parseScreenshotDim(v, 'height'))
|
|
906
1023
|
.description('Take screenshot')
|
|
907
1024
|
.action(browserAction(async (page, path, opts) => {
|
|
908
1025
|
const shotOpts = {
|
|
909
1026
|
fullPage: opts.fullPage === true,
|
|
1027
|
+
annotate: opts.annotate === true,
|
|
910
1028
|
width: opts.width,
|
|
911
1029
|
height: opts.height,
|
|
912
1030
|
};
|
|
1031
|
+
const capture = opts.annotate === true
|
|
1032
|
+
? (page.annotatedScreenshot ?? page.screenshot).bind(page)
|
|
1033
|
+
: page.screenshot.bind(page);
|
|
913
1034
|
if (path) {
|
|
914
|
-
await
|
|
1035
|
+
await capture({ ...shotOpts, path });
|
|
915
1036
|
console.log(`Screenshot saved to: ${path}`);
|
|
916
1037
|
}
|
|
917
1038
|
else {
|
|
918
|
-
console.log(await
|
|
1039
|
+
console.log(await capture({ ...shotOpts, format: 'png' }));
|
|
919
1040
|
}
|
|
920
1041
|
}));
|
|
921
1042
|
addBrowserTabOption(browser.command('console'))
|
|
@@ -1053,18 +1174,200 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1053
1174
|
// `browser find --css <sel>` lets agents jump straight from a semantic
|
|
1054
1175
|
// selector to a JSON list of matching elements, without having to parse
|
|
1055
1176
|
// the free-text state snapshot to recover indices.
|
|
1056
|
-
|
|
1177
|
+
const addSemanticLocatorOptions = (cmd) => cmd
|
|
1178
|
+
.option('--role <role>', 'Semantic role (button, link, textbox, option, etc.)')
|
|
1179
|
+
.option('--name <text>', 'Accessible name contains text (aria-label, label, title, placeholder, or visible text)')
|
|
1180
|
+
.option('--label <text>', 'Associated label contains text')
|
|
1181
|
+
.option('--text <text>', 'Visible text contains text')
|
|
1182
|
+
.option('--testid <id>', 'data-testid / data-test / test-id contains id');
|
|
1183
|
+
const addPrefixedSemanticLocatorOptions = (cmd, prefix) => cmd
|
|
1184
|
+
.option(`--${prefix}-role <role>`, `${prefix} semantic role`)
|
|
1185
|
+
.option(`--${prefix}-name <text>`, `${prefix} accessible name contains text`)
|
|
1186
|
+
.option(`--${prefix}-label <text>`, `${prefix} associated label contains text`)
|
|
1187
|
+
.option(`--${prefix}-text <text>`, `${prefix} visible text contains text`)
|
|
1188
|
+
.option(`--${prefix}-testid <id>`, `${prefix} data-testid / data-test / test-id contains id`);
|
|
1189
|
+
const semanticLocatorFromOptions = (opts) => {
|
|
1190
|
+
const locator = {};
|
|
1191
|
+
for (const key of ['role', 'name', 'label', 'text', 'testid']) {
|
|
1192
|
+
const value = opts[key];
|
|
1193
|
+
if (typeof value === 'string' && value.trim())
|
|
1194
|
+
locator[key] = value.trim();
|
|
1195
|
+
}
|
|
1196
|
+
return Object.keys(locator).length > 0 ? locator : null;
|
|
1197
|
+
};
|
|
1198
|
+
const prefixedSemanticLocatorFromOptions = (opts, prefix) => {
|
|
1199
|
+
const locator = {};
|
|
1200
|
+
const map = {
|
|
1201
|
+
role: `${prefix}Role`,
|
|
1202
|
+
name: `${prefix}Name`,
|
|
1203
|
+
label: `${prefix}Label`,
|
|
1204
|
+
text: `${prefix}Text`,
|
|
1205
|
+
testid: `${prefix}Testid`,
|
|
1206
|
+
};
|
|
1207
|
+
for (const key of ['role', 'name', 'label', 'text', 'testid']) {
|
|
1208
|
+
const value = opts[map[key]];
|
|
1209
|
+
if (typeof value === 'string' && value.trim())
|
|
1210
|
+
locator[key] = value.trim();
|
|
1211
|
+
}
|
|
1212
|
+
return Object.keys(locator).length > 0 ? locator : null;
|
|
1213
|
+
};
|
|
1214
|
+
const semanticTargetFromLocator = async (page, locator, mode) => {
|
|
1215
|
+
const result = await page.evaluate(buildSemanticFindJs({ ...locator, limit: 6 }));
|
|
1216
|
+
if (isFindError(result))
|
|
1217
|
+
return result;
|
|
1218
|
+
if (mode === 'write' && result.matches_n !== 1) {
|
|
1219
|
+
return {
|
|
1220
|
+
error: {
|
|
1221
|
+
code: 'semantic_ambiguous',
|
|
1222
|
+
message: `Semantic locator matched ${result.matches_n} elements; write actions require a unique target.`,
|
|
1223
|
+
hint: 'Add --name/--label/--text/--testid or use browser find with a narrower locator.',
|
|
1224
|
+
matches_n: result.matches_n,
|
|
1225
|
+
entries: result.entries,
|
|
1226
|
+
},
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
const first = result.entries[0];
|
|
1230
|
+
if (!first) {
|
|
1231
|
+
return {
|
|
1232
|
+
error: {
|
|
1233
|
+
code: 'semantic_not_found',
|
|
1234
|
+
message: 'Semantic locator matched 0 elements',
|
|
1235
|
+
hint: 'Try browser state, --source ax, or relax the semantic locator.',
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
const target = String(first.ref);
|
|
1240
|
+
if (mode === 'read') {
|
|
1241
|
+
return {
|
|
1242
|
+
target,
|
|
1243
|
+
...(result.matches_n > 1 ? { total_matches: result.matches_n } : {}),
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
return target;
|
|
1247
|
+
};
|
|
1248
|
+
const semanticTargetFromOptions = async (page, opts, mode) => {
|
|
1249
|
+
const locator = semanticLocatorFromOptions(opts);
|
|
1250
|
+
if (!locator)
|
|
1251
|
+
return null;
|
|
1252
|
+
return semanticTargetFromLocator(page, locator, mode);
|
|
1253
|
+
};
|
|
1254
|
+
const resolveExplicitOrSemanticTarget = async (page, target, opts, mode) => {
|
|
1255
|
+
const explicit = typeof target === 'string' && target.trim() ? target.trim() : '';
|
|
1256
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts);
|
|
1257
|
+
if (explicit && hasSemantic) {
|
|
1258
|
+
return {
|
|
1259
|
+
error: {
|
|
1260
|
+
code: 'usage_error',
|
|
1261
|
+
message: 'Pass either <target> or semantic locator flags, not both.',
|
|
1262
|
+
},
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
if (explicit)
|
|
1266
|
+
return explicit;
|
|
1267
|
+
const semantic = await semanticTargetFromOptions(page, opts, mode);
|
|
1268
|
+
if (semantic)
|
|
1269
|
+
return semantic;
|
|
1270
|
+
return {
|
|
1271
|
+
error: {
|
|
1272
|
+
code: 'usage_error',
|
|
1273
|
+
message: 'Missing target. Pass a numeric ref/CSS selector, or semantic flags like --role button --name Submit.',
|
|
1274
|
+
},
|
|
1275
|
+
};
|
|
1276
|
+
};
|
|
1277
|
+
const printTargetResolutionError = (resolved) => {
|
|
1278
|
+
console.log(JSON.stringify(resolved, null, 2));
|
|
1279
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1280
|
+
};
|
|
1281
|
+
const resolveWriteTargetOrPrint = async (page, target, opts) => {
|
|
1282
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts, 'write');
|
|
1283
|
+
if (typeof resolvedTarget === 'string')
|
|
1284
|
+
return resolvedTarget;
|
|
1285
|
+
if ('error' in resolvedTarget)
|
|
1286
|
+
printTargetResolutionError(resolvedTarget);
|
|
1287
|
+
return null;
|
|
1288
|
+
};
|
|
1289
|
+
const resolveWriteTargetAndValueOrPrint = async (page, targetOrValue, value, opts, valueLabel) => {
|
|
1290
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts);
|
|
1291
|
+
if (hasSemantic && value !== undefined) {
|
|
1292
|
+
printTargetResolutionError({
|
|
1293
|
+
error: {
|
|
1294
|
+
code: 'usage_error',
|
|
1295
|
+
message: `When using semantic locator flags, pass only <${valueLabel}> as the positional argument.`,
|
|
1296
|
+
},
|
|
1297
|
+
});
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
const resolvedValue = hasSemantic ? targetOrValue : value;
|
|
1301
|
+
if (resolvedValue === undefined) {
|
|
1302
|
+
printTargetResolutionError({
|
|
1303
|
+
error: {
|
|
1304
|
+
code: 'usage_error',
|
|
1305
|
+
message: `Missing ${valueLabel}.`,
|
|
1306
|
+
hint: hasSemantic
|
|
1307
|
+
? `With semantic locator flags, pass the ${valueLabel} as the only positional argument.`
|
|
1308
|
+
: `Pass both a target and ${valueLabel}.`,
|
|
1309
|
+
},
|
|
1310
|
+
});
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, hasSemantic ? undefined : targetOrValue, opts);
|
|
1314
|
+
if (!resolvedTarget)
|
|
1315
|
+
return null;
|
|
1316
|
+
return { target: resolvedTarget, value: String(resolvedValue) };
|
|
1317
|
+
};
|
|
1318
|
+
const resolvePrefixedWriteTargetOrPrint = async (page, target, opts, prefix, label) => {
|
|
1319
|
+
const explicit = typeof target === 'string' && target.trim() ? target.trim() : '';
|
|
1320
|
+
const locator = prefixedSemanticLocatorFromOptions(opts, prefix);
|
|
1321
|
+
if (explicit && locator) {
|
|
1322
|
+
printTargetResolutionError({
|
|
1323
|
+
error: {
|
|
1324
|
+
code: 'usage_error',
|
|
1325
|
+
message: `Pass either <${label}> or --${prefix}-* semantic locator flags, not both.`,
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
if (explicit)
|
|
1331
|
+
return explicit;
|
|
1332
|
+
if (locator) {
|
|
1333
|
+
const resolved = await semanticTargetFromLocator(page, locator, 'write');
|
|
1334
|
+
if (typeof resolved === 'string')
|
|
1335
|
+
return resolved;
|
|
1336
|
+
if ('error' in resolved)
|
|
1337
|
+
printTargetResolutionError(resolved);
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
printTargetResolutionError({
|
|
1341
|
+
error: {
|
|
1342
|
+
code: 'usage_error',
|
|
1343
|
+
message: `Missing ${label}. Pass a numeric ref/CSS selector, or --${prefix}-role/--${prefix}-name semantic flags.`,
|
|
1344
|
+
},
|
|
1345
|
+
});
|
|
1346
|
+
return null;
|
|
1347
|
+
};
|
|
1348
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('find'))
|
|
1057
1349
|
.option('--css <selector>', 'CSS selector (required)')
|
|
1058
1350
|
.option('--limit <n>', 'Max entries returned', '50')
|
|
1059
1351
|
.option('--text-max <n>', 'Max chars of trimmed text per entry', '120')
|
|
1060
|
-
.description('Find DOM elements by CSS
|
|
1352
|
+
.description('Find DOM elements by CSS or semantic locator — returns JSON {matches_n, entries[]}'))
|
|
1061
1353
|
.action(browserAction(async (page, opts) => {
|
|
1062
|
-
|
|
1354
|
+
const locator = semanticLocatorFromOptions(opts);
|
|
1355
|
+
if ((!opts.css || typeof opts.css !== 'string') && !locator) {
|
|
1356
|
+
console.log(JSON.stringify({
|
|
1357
|
+
error: {
|
|
1358
|
+
code: 'usage_error',
|
|
1359
|
+
message: '--css <selector> or a semantic locator flag is required',
|
|
1360
|
+
hint: 'Examples: opencli browser find --css ".btn.primary"; opencli browser find --role button --name Save',
|
|
1361
|
+
},
|
|
1362
|
+
}, null, 2));
|
|
1363
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (opts.css && locator) {
|
|
1063
1367
|
console.log(JSON.stringify({
|
|
1064
1368
|
error: {
|
|
1065
1369
|
code: 'usage_error',
|
|
1066
|
-
message: '--css
|
|
1067
|
-
hint: 'Example: opencli browser find --css ".btn.primary"',
|
|
1370
|
+
message: 'Pass either --css or semantic locator flags, not both.',
|
|
1068
1371
|
},
|
|
1069
1372
|
}, null, 2));
|
|
1070
1373
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
@@ -1082,10 +1385,16 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1082
1385
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1083
1386
|
return;
|
|
1084
1387
|
}
|
|
1085
|
-
const result = await page.evaluate(
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1388
|
+
const result = await page.evaluate(locator
|
|
1389
|
+
? buildSemanticFindJs({
|
|
1390
|
+
...locator,
|
|
1391
|
+
limit: limit ?? undefined,
|
|
1392
|
+
textMax: textMax ?? undefined,
|
|
1393
|
+
})
|
|
1394
|
+
: buildFindJs(opts.css, {
|
|
1395
|
+
limit: limit ?? undefined,
|
|
1396
|
+
textMax: textMax ?? undefined,
|
|
1397
|
+
}));
|
|
1089
1398
|
if (isFindError(result)) {
|
|
1090
1399
|
console.log(JSON.stringify(result, null, 2));
|
|
1091
1400
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
@@ -1112,13 +1421,21 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1112
1421
|
// or a CSS selector. On multi-match CSS, the first element wins and the real
|
|
1113
1422
|
// match count is exposed via `matches_n`; `--nth <n>` picks a specific one.
|
|
1114
1423
|
const runGetCommand = async (page, target, opts, evalJs, field) => {
|
|
1424
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts, 'read');
|
|
1425
|
+
if (typeof resolvedTarget !== 'string' && 'error' in resolvedTarget) {
|
|
1426
|
+
console.log(JSON.stringify(resolvedTarget, null, 2));
|
|
1427
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const targetRef = typeof resolvedTarget === 'string' ? resolvedTarget : resolvedTarget.target;
|
|
1431
|
+
const totalMatches = typeof resolvedTarget === 'string' ? undefined : resolvedTarget.total_matches;
|
|
1115
1432
|
const nth = parseNthFlag(opts.nth);
|
|
1116
1433
|
if (nth && typeof nth === 'object' && 'error' in nth) {
|
|
1117
1434
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: nth.error } }, null, 2));
|
|
1118
1435
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1119
1436
|
return;
|
|
1120
1437
|
}
|
|
1121
|
-
const { matches_n, match_level } = await resolveRef(page,
|
|
1438
|
+
const { matches_n, match_level } = await resolveRef(page, targetRef, {
|
|
1122
1439
|
firstOnMulti: nth === null,
|
|
1123
1440
|
...(typeof nth === 'number' ? { nth } : {}),
|
|
1124
1441
|
});
|
|
@@ -1137,18 +1454,23 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1137
1454
|
else {
|
|
1138
1455
|
value = raw ?? null;
|
|
1139
1456
|
}
|
|
1140
|
-
console.log(JSON.stringify({
|
|
1457
|
+
console.log(JSON.stringify({
|
|
1458
|
+
value,
|
|
1459
|
+
matches_n,
|
|
1460
|
+
match_level,
|
|
1461
|
+
...(totalMatches && totalMatches > 1 ? { total_matches: totalMatches } : {}),
|
|
1462
|
+
}, null, 2));
|
|
1141
1463
|
};
|
|
1142
|
-
addBrowserTabOption(get.command('text')
|
|
1143
|
-
.argument('
|
|
1464
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('text'))
|
|
1465
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1144
1466
|
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1145
1467
|
.description('Element text content — JSON envelope {value, matches_n}'))
|
|
1146
|
-
.action(browserAction(async (page, target, opts) => runGetCommand(page,
|
|
1147
|
-
addBrowserTabOption(get.command('value')
|
|
1148
|
-
.argument('
|
|
1468
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getTextResolvedJs(), 'text')));
|
|
1469
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('value'))
|
|
1470
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1149
1471
|
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1150
1472
|
.description('Input/textarea value — JSON envelope {value, matches_n}'))
|
|
1151
|
-
.action(browserAction(async (page, target, opts) => runGetCommand(page,
|
|
1473
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getValueResolvedJs(), 'value')));
|
|
1152
1474
|
addBrowserTabOption(get.command('html')
|
|
1153
1475
|
.option('--selector <css>', 'CSS selector scope (first match)')
|
|
1154
1476
|
.option('--as <format>', 'Output format: "html" (default) or "json" for structured tree', 'html')
|
|
@@ -1263,11 +1585,11 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1263
1585
|
}
|
|
1264
1586
|
console.log(html);
|
|
1265
1587
|
}));
|
|
1266
|
-
addBrowserTabOption(get.command('attributes')
|
|
1267
|
-
.argument('
|
|
1588
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('attributes'))
|
|
1589
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1268
1590
|
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1269
1591
|
.description('Element attributes — JSON envelope {value, matches_n}'))
|
|
1270
|
-
.action(browserAction(async (page, target, opts) => runGetCommand(page,
|
|
1592
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getAttributesResolvedJs(), 'attributes')));
|
|
1271
1593
|
// ── Interact ──
|
|
1272
1594
|
//
|
|
1273
1595
|
// Write commands (`click/type/select`) share the same `<target>` contract
|
|
@@ -1290,26 +1612,73 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1290
1612
|
return { opts: { nth: parsed } };
|
|
1291
1613
|
return { opts: {} };
|
|
1292
1614
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1615
|
+
function resolveUploadFilePaths(rawFiles) {
|
|
1616
|
+
const inputs = Array.isArray(rawFiles) ? rawFiles : [];
|
|
1617
|
+
if (inputs.length === 0) {
|
|
1618
|
+
return {
|
|
1619
|
+
error: {
|
|
1620
|
+
code: 'usage_error',
|
|
1621
|
+
message: 'At least one file path is required.',
|
|
1622
|
+
hint: 'Example: opencli browser upload "input[type=file]" ./receipt.pdf',
|
|
1623
|
+
},
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
const files = [];
|
|
1627
|
+
for (const input of inputs) {
|
|
1628
|
+
const raw = String(input);
|
|
1629
|
+
const expanded = raw === '~' || raw.startsWith(`~${path.sep}`)
|
|
1630
|
+
? path.join(os.homedir(), raw.slice(2))
|
|
1631
|
+
: raw;
|
|
1632
|
+
const resolved = path.resolve(expanded);
|
|
1633
|
+
if (!fs.existsSync(resolved)) {
|
|
1634
|
+
return { error: { code: 'file_not_found', message: `File not found: ${resolved}` } };
|
|
1635
|
+
}
|
|
1636
|
+
const stat = fs.statSync(resolved);
|
|
1637
|
+
if (!stat.isFile()) {
|
|
1638
|
+
return { error: { code: 'not_a_file', message: `Not a regular file: ${resolved}` } };
|
|
1639
|
+
}
|
|
1640
|
+
files.push(resolved);
|
|
1641
|
+
}
|
|
1642
|
+
return { files };
|
|
1643
|
+
}
|
|
1644
|
+
function parseResolveFlag(raw, flag) {
|
|
1645
|
+
const parsed = parseNthFlag(raw);
|
|
1646
|
+
if (parsed && typeof parsed === 'object' && 'error' in parsed) {
|
|
1647
|
+
return { error: parsed.error.replace('--nth', flag) };
|
|
1648
|
+
}
|
|
1649
|
+
if (typeof parsed === 'number')
|
|
1650
|
+
return { opts: { nth: parsed } };
|
|
1651
|
+
return { opts: {} };
|
|
1652
|
+
}
|
|
1653
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('click'))
|
|
1654
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1295
1655
|
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1296
1656
|
.description('Click element — JSON envelope {clicked, target, matches_n}'))
|
|
1297
1657
|
.action(browserAction(async (page, target, opts) => {
|
|
1658
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts ?? {}, 'write');
|
|
1659
|
+
if (typeof resolvedTarget !== 'string') {
|
|
1660
|
+
console.log(JSON.stringify(resolvedTarget, null, 2));
|
|
1661
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1298
1664
|
const parsed = nthToResolveOpts(opts?.nth);
|
|
1299
1665
|
if ('error' in parsed) {
|
|
1300
1666
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1301
1667
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1302
1668
|
return;
|
|
1303
1669
|
}
|
|
1304
|
-
const { matches_n, match_level } = await page.click(
|
|
1305
|
-
console.log(JSON.stringify({ clicked: true, target:
|
|
1670
|
+
const { matches_n, match_level } = await page.click(resolvedTarget, parsed.opts);
|
|
1671
|
+
console.log(JSON.stringify({ clicked: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1306
1672
|
}));
|
|
1307
|
-
addBrowserTabOption(browser.command('type')
|
|
1308
|
-
.argument('
|
|
1309
|
-
.argument('
|
|
1673
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('type'))
|
|
1674
|
+
.argument('[targetOrText]', 'Numeric ref/CSS target, or text when using --role/--name/etc.')
|
|
1675
|
+
.argument('[text]', 'Text to type')
|
|
1310
1676
|
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1311
1677
|
.description('Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}'))
|
|
1312
|
-
.action(browserAction(async (page,
|
|
1678
|
+
.action(browserAction(async (page, targetOrText, text, opts) => {
|
|
1679
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrText, text, opts ?? {}, 'text');
|
|
1680
|
+
if (!resolved)
|
|
1681
|
+
return;
|
|
1313
1682
|
const parsed = nthToResolveOpts(opts?.nth);
|
|
1314
1683
|
if ('error' in parsed) {
|
|
1315
1684
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
@@ -1317,42 +1686,199 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1317
1686
|
return;
|
|
1318
1687
|
}
|
|
1319
1688
|
// Click first (focuses the field), wait briefly, then type.
|
|
1320
|
-
await page.click(
|
|
1689
|
+
await page.click(resolved.target, parsed.opts);
|
|
1321
1690
|
await page.wait(0.3);
|
|
1322
|
-
const { matches_n, match_level } = await page.typeText(
|
|
1691
|
+
const { matches_n, match_level } = await page.typeText(resolved.target, resolved.value, parsed.opts);
|
|
1323
1692
|
// __resolved is already set by the resolver call inside page.typeText
|
|
1324
1693
|
const isAutocomplete = await page.evaluate(isAutocompleteResolvedJs());
|
|
1325
1694
|
if (isAutocomplete)
|
|
1326
1695
|
await page.wait(0.4);
|
|
1327
1696
|
console.log(JSON.stringify({
|
|
1328
1697
|
typed: true,
|
|
1329
|
-
text:
|
|
1330
|
-
target:
|
|
1698
|
+
text: resolved.value,
|
|
1699
|
+
target: resolved.target,
|
|
1331
1700
|
matches_n,
|
|
1332
1701
|
match_level,
|
|
1333
1702
|
autocomplete: !!isAutocomplete,
|
|
1334
1703
|
}, null, 2));
|
|
1335
1704
|
}));
|
|
1336
|
-
addBrowserTabOption(browser.command('
|
|
1337
|
-
.argument('
|
|
1338
|
-
.
|
|
1705
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('hover'))
|
|
1706
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1707
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1708
|
+
.description('Move the mouse over an element — JSON envelope {hovered, target, matches_n}'))
|
|
1709
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1710
|
+
if (typeof page.hover !== 'function')
|
|
1711
|
+
throw new Error('browser hover is not supported by this browser backend');
|
|
1712
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1713
|
+
if (!resolvedTarget)
|
|
1714
|
+
return;
|
|
1715
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1716
|
+
if ('error' in parsed) {
|
|
1717
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1718
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const { matches_n, match_level } = await page.hover(resolvedTarget, parsed.opts);
|
|
1722
|
+
console.log(JSON.stringify({ hovered: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1723
|
+
}));
|
|
1724
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('focus'))
|
|
1725
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1726
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1727
|
+
.description('Focus an element — JSON envelope {focused, target, matches_n}'))
|
|
1728
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1729
|
+
if (typeof page.focus !== 'function')
|
|
1730
|
+
throw new Error('browser focus is not supported by this browser backend');
|
|
1731
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1732
|
+
if (!resolvedTarget)
|
|
1733
|
+
return;
|
|
1734
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1735
|
+
if ('error' in parsed) {
|
|
1736
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1737
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
const { focused, matches_n, match_level } = await page.focus(resolvedTarget, parsed.opts);
|
|
1741
|
+
console.log(JSON.stringify({ focused, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1742
|
+
}));
|
|
1743
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('dblclick'))
|
|
1744
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1745
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1746
|
+
.description('Double-click element — JSON envelope {dblclicked, target, matches_n}'))
|
|
1747
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1748
|
+
if (typeof page.dblClick !== 'function')
|
|
1749
|
+
throw new Error('browser dblclick is not supported by this browser backend');
|
|
1750
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1751
|
+
if (!resolvedTarget)
|
|
1752
|
+
return;
|
|
1753
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1754
|
+
if ('error' in parsed) {
|
|
1755
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1756
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const { matches_n, match_level } = await page.dblClick(resolvedTarget, parsed.opts);
|
|
1760
|
+
console.log(JSON.stringify({ dblclicked: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1761
|
+
}));
|
|
1762
|
+
const runCheckCommand = async (page, target, opts, checked) => {
|
|
1763
|
+
if (typeof page.setChecked !== 'function')
|
|
1764
|
+
throw new Error(`browser ${checked ? 'check' : 'uncheck'} is not supported by this browser backend`);
|
|
1765
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts);
|
|
1766
|
+
if (!resolvedTarget)
|
|
1767
|
+
return;
|
|
1768
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1769
|
+
if ('error' in parsed) {
|
|
1770
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1771
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const result = await page.setChecked(resolvedTarget, checked, parsed.opts);
|
|
1775
|
+
console.log(JSON.stringify({
|
|
1776
|
+
checked: result.checked,
|
|
1777
|
+
changed: result.changed,
|
|
1778
|
+
target: resolvedTarget,
|
|
1779
|
+
matches_n: result.matches_n,
|
|
1780
|
+
match_level: result.match_level,
|
|
1781
|
+
...(result.kind ? { kind: result.kind } : {}),
|
|
1782
|
+
}, null, 2));
|
|
1783
|
+
};
|
|
1784
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('check'))
|
|
1785
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1786
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1787
|
+
.description('Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}'))
|
|
1788
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1789
|
+
await runCheckCommand(page, target, opts ?? {}, true);
|
|
1790
|
+
}));
|
|
1791
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('uncheck'))
|
|
1792
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1793
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1794
|
+
.description('Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}'))
|
|
1795
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1796
|
+
await runCheckCommand(page, target, opts ?? {}, false);
|
|
1797
|
+
}));
|
|
1798
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('upload'))
|
|
1799
|
+
.argument('[targetOrFile]', 'Numeric ref/CSS target, or first file when using --role/--name/etc.')
|
|
1800
|
+
.argument('[files...]', 'Local file path(s) to attach')
|
|
1801
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1802
|
+
.description('Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}'))
|
|
1803
|
+
.action(browserAction(async (page, targetOrFile, files, opts) => {
|
|
1804
|
+
if (typeof page.uploadFiles !== 'function')
|
|
1805
|
+
throw new Error('browser upload is not supported by this browser backend');
|
|
1806
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts ?? {});
|
|
1807
|
+
const target = hasSemantic ? undefined : targetOrFile;
|
|
1808
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1809
|
+
if (!resolvedTarget)
|
|
1810
|
+
return;
|
|
1811
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1812
|
+
if ('error' in parsed) {
|
|
1813
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1814
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const rawFiles = hasSemantic
|
|
1818
|
+
? [targetOrFile, ...(Array.isArray(files) ? files : [])].filter((value) => value !== undefined)
|
|
1819
|
+
: files;
|
|
1820
|
+
const resolvedFiles = resolveUploadFilePaths(rawFiles);
|
|
1821
|
+
if ('error' in resolvedFiles) {
|
|
1822
|
+
console.log(JSON.stringify({ error: resolvedFiles.error }, null, 2));
|
|
1823
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const result = await page.uploadFiles(resolvedTarget, resolvedFiles.files, parsed.opts);
|
|
1827
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1828
|
+
}));
|
|
1829
|
+
addBrowserTabOption(addPrefixedSemanticLocatorOptions(addPrefixedSemanticLocatorOptions(browser.command('drag'), 'from'), 'to')
|
|
1830
|
+
.argument('[source]', 'Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.')
|
|
1831
|
+
.argument('[target]', 'Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc.')
|
|
1832
|
+
.option('--from-nth <n>', 'When <source> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1833
|
+
.option('--to-nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1834
|
+
.description('Drag one element to another — JSON envelope {dragged, source, target, source_matches_n, target_matches_n}'))
|
|
1835
|
+
.action(browserAction(async (page, source, target, opts) => {
|
|
1836
|
+
if (typeof page.drag !== 'function')
|
|
1837
|
+
throw new Error('browser drag is not supported by this browser backend');
|
|
1838
|
+
const resolvedSource = await resolvePrefixedWriteTargetOrPrint(page, source, opts ?? {}, 'from', 'source');
|
|
1839
|
+
if (!resolvedSource)
|
|
1840
|
+
return;
|
|
1841
|
+
const resolvedTarget = await resolvePrefixedWriteTargetOrPrint(page, target, opts ?? {}, 'to', 'target');
|
|
1842
|
+
if (!resolvedTarget)
|
|
1843
|
+
return;
|
|
1844
|
+
const from = parseResolveFlag(opts?.fromNth, '--from-nth');
|
|
1845
|
+
if ('error' in from) {
|
|
1846
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: from.error } }, null, 2));
|
|
1847
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const to = parseResolveFlag(opts?.toNth, '--to-nth');
|
|
1851
|
+
if ('error' in to) {
|
|
1852
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: to.error } }, null, 2));
|
|
1853
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const result = await page.drag(resolvedSource, resolvedTarget, { from: from.opts, to: to.opts });
|
|
1857
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1858
|
+
}));
|
|
1859
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('fill'))
|
|
1860
|
+
.argument('[targetOrText]', 'Numeric ref/CSS target, or text when using --role/--name/etc.')
|
|
1861
|
+
.argument('[text]', 'Text to set exactly')
|
|
1339
1862
|
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1340
1863
|
.description('Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}'))
|
|
1341
|
-
.action(browserAction(async (page,
|
|
1864
|
+
.action(browserAction(async (page, targetOrText, text, opts) => {
|
|
1865
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrText, text, opts ?? {}, 'text');
|
|
1866
|
+
if (!resolved)
|
|
1867
|
+
return;
|
|
1342
1868
|
const parsed = nthToResolveOpts(opts?.nth);
|
|
1343
1869
|
if ('error' in parsed) {
|
|
1344
1870
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1345
1871
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1346
1872
|
return;
|
|
1347
1873
|
}
|
|
1348
|
-
const result = await page.fillText(
|
|
1874
|
+
const result = await page.fillText(resolved.target, resolved.value, parsed.opts);
|
|
1349
1875
|
if (!result.verified)
|
|
1350
1876
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
1351
1877
|
console.log(JSON.stringify({
|
|
1352
1878
|
filled: result.filled,
|
|
1353
1879
|
verified: result.verified,
|
|
1354
|
-
target:
|
|
1355
|
-
text:
|
|
1880
|
+
target: resolved.target,
|
|
1881
|
+
text: resolved.value,
|
|
1356
1882
|
actual: result.actual,
|
|
1357
1883
|
length: result.length,
|
|
1358
1884
|
matches_n: result.matches_n,
|
|
@@ -1360,20 +1886,23 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1360
1886
|
...(result.mode ? { mode: result.mode } : {}),
|
|
1361
1887
|
}, null, 2));
|
|
1362
1888
|
}));
|
|
1363
|
-
addBrowserTabOption(browser.command('select')
|
|
1364
|
-
.argument('
|
|
1365
|
-
.argument('
|
|
1889
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('select'))
|
|
1890
|
+
.argument('[targetOrOption]', 'Numeric ref/CSS target, or option text when using --role/--name/etc.')
|
|
1891
|
+
.argument('[option]', 'Option text (or value) to select')
|
|
1366
1892
|
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1367
1893
|
.description('Select dropdown option — JSON envelope {selected, target, matches_n}'))
|
|
1368
|
-
.action(browserAction(async (page,
|
|
1894
|
+
.action(browserAction(async (page, targetOrOption, option, opts) => {
|
|
1895
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrOption, option, opts ?? {}, 'option');
|
|
1896
|
+
if (!resolved)
|
|
1897
|
+
return;
|
|
1369
1898
|
const parsed = nthToResolveOpts(opts?.nth);
|
|
1370
1899
|
if ('error' in parsed) {
|
|
1371
1900
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1372
1901
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1373
1902
|
return;
|
|
1374
1903
|
}
|
|
1375
|
-
const { matches_n, match_level } = await resolveRef(page,
|
|
1376
|
-
const result = await page.evaluate(selectResolvedJs(
|
|
1904
|
+
const { matches_n, match_level } = await resolveRef(page, resolved.target, parsed.opts);
|
|
1905
|
+
const result = await page.evaluate(selectResolvedJs(resolved.value));
|
|
1377
1906
|
if (result?.error) {
|
|
1378
1907
|
// The select-specific "Not a <select>" / "Option not found" errors
|
|
1379
1908
|
// are domain-level failures — emit a structured envelope so agents
|
|
@@ -1390,8 +1919,8 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1390
1919
|
return;
|
|
1391
1920
|
}
|
|
1392
1921
|
console.log(JSON.stringify({
|
|
1393
|
-
selected: result?.selected ??
|
|
1394
|
-
target:
|
|
1922
|
+
selected: result?.selected ?? resolved.value,
|
|
1923
|
+
target: resolved.target,
|
|
1395
1924
|
matches_n,
|
|
1396
1925
|
match_level,
|
|
1397
1926
|
}, null, 2));
|
|
@@ -1458,10 +1987,10 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1458
1987
|
}));
|
|
1459
1988
|
// ── Wait commands ──
|
|
1460
1989
|
addBrowserTabOption(browser.command('wait'))
|
|
1461
|
-
.argument('<type>', 'selector, text, time, or
|
|
1462
|
-
.argument('[value]', 'CSS selector, text string, seconds, or
|
|
1990
|
+
.argument('<type>', 'selector, text, time, xhr, or download')
|
|
1991
|
+
.argument('[value]', 'CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern')
|
|
1463
1992
|
.option('--timeout <ms>', 'Timeout in milliseconds', '10000')
|
|
1464
|
-
.description('Wait for selector, text, time,
|
|
1993
|
+
.description('Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)')
|
|
1465
1994
|
.action(browserAction(async (page, type, value, opts) => {
|
|
1466
1995
|
const timeout = parseInt(opts.timeout, 10);
|
|
1467
1996
|
if (type === 'time') {
|
|
@@ -1538,8 +2067,36 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1538
2067
|
matched: { url: matched.url, status: matched.status, contentType: matched.ct },
|
|
1539
2068
|
}, null, 2));
|
|
1540
2069
|
}
|
|
2070
|
+
else if (type === 'download') {
|
|
2071
|
+
if (typeof page.waitForDownload !== 'function') {
|
|
2072
|
+
console.log(JSON.stringify({
|
|
2073
|
+
error: {
|
|
2074
|
+
code: 'download_wait_unavailable',
|
|
2075
|
+
message: 'The active browser backend does not support download lifecycle waits.',
|
|
2076
|
+
hint: 'Use the Browser Bridge extension version 1.0.8 or newer, then retry the command.',
|
|
2077
|
+
},
|
|
2078
|
+
}, null, 2));
|
|
2079
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const result = await page.waitForDownload(String(value ?? ''), timeout);
|
|
2083
|
+
if (!result.downloaded) {
|
|
2084
|
+
const code = result.state === 'interrupted' && result.id !== undefined ? 'download_failed' : 'download_not_seen';
|
|
2085
|
+
console.log(JSON.stringify({
|
|
2086
|
+
error: {
|
|
2087
|
+
code,
|
|
2088
|
+
message: result.error ?? `No download matched "${value ?? '*'}" within ${timeout}ms`,
|
|
2089
|
+
hint: 'Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly.',
|
|
2090
|
+
},
|
|
2091
|
+
download: result,
|
|
2092
|
+
}, null, 2));
|
|
2093
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2097
|
+
}
|
|
1541
2098
|
else {
|
|
1542
|
-
console.error(`Unknown wait type "${type}". Use: selector, text, time, or
|
|
2099
|
+
console.error(`Unknown wait type "${type}". Use: selector, text, time, xhr, or download`);
|
|
1543
2100
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1544
2101
|
}
|
|
1545
2102
|
}));
|
|
@@ -2110,6 +2667,8 @@ cli({
|
|
|
2110
2667
|
});
|
|
2111
2668
|
// ── Plugin management ──────────────────────────────────────────────────────
|
|
2112
2669
|
const pluginCmd = program.command('plugin').description('Manage opencli plugins');
|
|
2670
|
+
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
|
|
2671
|
+
const originalPluginDescription = pluginCmd.description();
|
|
2113
2672
|
pluginCmd
|
|
2114
2673
|
.command('install')
|
|
2115
2674
|
.description('Install a plugin from a git repository')
|
|
@@ -2296,6 +2855,8 @@ cli({
|
|
|
2296
2855
|
});
|
|
2297
2856
|
// ── Built-in: adapter management ─────────────────────────────────────────
|
|
2298
2857
|
const adapterCmd = program.command('adapter').description('Manage CLI adapters');
|
|
2858
|
+
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
|
|
2859
|
+
const originalAdapterDescription = adapterCmd.description();
|
|
2299
2860
|
adapterCmd
|
|
2300
2861
|
.command('status')
|
|
2301
2862
|
.description('Show which sites have local overrides vs using official baseline')
|
|
@@ -2404,6 +2965,8 @@ cli({
|
|
|
2404
2965
|
});
|
|
2405
2966
|
// ── Built-in: browser profile selection ──────────────────────────────────
|
|
2406
2967
|
const profileCmd = program.command('profile').description('Manage Browser Bridge Chrome profiles');
|
|
2968
|
+
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
|
|
2969
|
+
const originalProfileDescription = profileCmd.description();
|
|
2407
2970
|
profileCmd
|
|
2408
2971
|
.command('list')
|
|
2409
2972
|
.description('List Chrome profiles connected through the Browser Bridge extension')
|
|
@@ -2481,6 +3044,8 @@ cli({
|
|
|
2481
3044
|
});
|
|
2482
3045
|
// ── Built-in: daemon ──────────────────────────────────────────────────────
|
|
2483
3046
|
const daemonCmd = program.command('daemon').description('Manage the opencli daemon');
|
|
3047
|
+
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
|
|
3048
|
+
const originalDaemonDescription = daemonCmd.description();
|
|
2484
3049
|
daemonCmd
|
|
2485
3050
|
.command('status')
|
|
2486
3051
|
.description('Show daemon status')
|
|
@@ -2605,6 +3170,11 @@ cli({
|
|
|
2605
3170
|
}
|
|
2606
3171
|
const adapterGroups = { external: externalNames, apps, sites };
|
|
2607
3172
|
const adapterNameSet = new Set([...externalNames, ...siteNames]);
|
|
3173
|
+
installCommanderNamespaceStructuredHelp(browser, { globalCommand: program, description: originalBrowserDescription });
|
|
3174
|
+
installCommanderNamespaceStructuredHelp(daemonCmd, { globalCommand: program, description: originalDaemonDescription });
|
|
3175
|
+
installCommanderNamespaceStructuredHelp(pluginCmd, { globalCommand: program, description: originalPluginDescription });
|
|
3176
|
+
installCommanderNamespaceStructuredHelp(adapterCmd, { globalCommand: program, description: originalAdapterDescription });
|
|
3177
|
+
installCommanderNamespaceStructuredHelp(profileCmd, { globalCommand: program, description: originalProfileDescription });
|
|
2608
3178
|
program.configureHelp({
|
|
2609
3179
|
visibleCommands: (command) => command.commands.filter(child => command !== program || !adapterNameSet.has(child.name())),
|
|
2610
3180
|
});
|