@jackwener/opencli 1.7.14 → 1.7.15
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/cli-manifest.json +215 -45
- package/clis/bilibili/subtitle.js +1 -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/reddit/search.js +1 -1
- package/clis/reddit/subreddit.js +1 -1
- package/clis/reddit/user-comments.js +1 -1
- package/clis/reddit/user-posts.js +1 -1
- package/clis/reddit/user.js +1 -1
- package/clis/twitter/article.js +2 -1
- package/clis/twitter/bookmark-folder.js +189 -0
- package/clis/twitter/bookmark-folder.test.js +334 -0
- package/clis/twitter/bookmark-folders.js +117 -0
- package/clis/twitter/bookmark-folders.test.js +150 -0
- package/clis/twitter/bookmark.js +15 -6
- package/clis/twitter/bookmark.test.js +74 -0
- package/clis/twitter/bookmarks.js +7 -5
- package/clis/twitter/delete.js +11 -35
- package/clis/twitter/delete.test.js +21 -9
- package/clis/twitter/download.js +5 -5
- package/clis/twitter/followers.js +9 -3
- package/clis/twitter/following.js +11 -5
- 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 +8 -6
- package/clis/twitter/list-add.js +4 -4
- package/clis/twitter/list-remove.js +4 -4
- package/clis/twitter/list-tweets.js +6 -4
- package/clis/twitter/lists.js +3 -3
- package/clis/twitter/notifications.js +2 -2
- package/clis/twitter/profile.js +4 -3
- 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 +175 -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 +6 -4
- package/clis/twitter/timeline.js +8 -6
- package/clis/twitter/tweets.js +5 -3
- 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/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/cdp-click-fixture.test.d.ts +1 -0
- package/dist/src/browser/cdp-click-fixture.test.js +87 -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 +3 -1
- 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 +2 -1
- package/dist/src/browser/page.js +13 -0
- 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 +560 -58
- package/dist/src/cli.test.js +598 -0
- package/dist/src/help.d.ts +32 -0
- package/dist/src/help.js +145 -0
- package/dist/src/types.d.ts +82 -0
- 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';
|
|
@@ -373,6 +373,41 @@ function getPageScope(page) {
|
|
|
373
373
|
const contextId = page.contextId;
|
|
374
374
|
return getBrowserScope(getPageWorkspace(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
|
|
375
375
|
}
|
|
376
|
+
function snapshotMetricText(snapshot) {
|
|
377
|
+
return typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2);
|
|
378
|
+
}
|
|
379
|
+
function snapshotMetrics(snapshot, elapsedMs) {
|
|
380
|
+
const text = snapshotMetricText(snapshot);
|
|
381
|
+
const interactiveMatch = text.match(/^interactive:\s*(\d+)\s*$/m);
|
|
382
|
+
return {
|
|
383
|
+
ok: true,
|
|
384
|
+
chars: text.length,
|
|
385
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
386
|
+
lines: text ? text.split(/\r?\n/).length : 0,
|
|
387
|
+
approx_tokens: Math.ceil(text.length / 4),
|
|
388
|
+
refs: (text.match(/(^|\n)\s*\[\d+\]/g) ?? []).length,
|
|
389
|
+
frame_sections: (text.match(/(^|\n)frame /g) ?? []).length,
|
|
390
|
+
...(interactiveMatch ? { interactive: Number(interactiveMatch[1]) } : {}),
|
|
391
|
+
elapsed_ms: elapsedMs,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
async function snapshotSourceMetrics(page, source) {
|
|
395
|
+
const started = Date.now();
|
|
396
|
+
try {
|
|
397
|
+
const snapshot = await page.snapshot({ viewportExpand: 2000, source });
|
|
398
|
+
return snapshotMetrics(snapshot, Date.now() - started);
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
elapsed_ms: Date.now() - started,
|
|
404
|
+
error: {
|
|
405
|
+
...(err instanceof Error && 'code' in err ? { code: String(err.code) } : {}),
|
|
406
|
+
message: err instanceof Error ? err.message : String(err),
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
376
411
|
function resolveBrowserTabTarget(targetId, opts) {
|
|
377
412
|
if (typeof targetId === 'string' && targetId.trim())
|
|
378
413
|
return targetId.trim();
|
|
@@ -549,6 +584,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
549
584
|
.command('browser')
|
|
550
585
|
.option('--workspace <name>', 'Browser workspace to use (default: browser:default; bound tabs use bound:<name>)')
|
|
551
586
|
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)');
|
|
587
|
+
const originalBrowserDescription = browser.description();
|
|
552
588
|
/**
|
|
553
589
|
* Resolve a `<target>` (numeric ref or CSS selector) via the unified resolver.
|
|
554
590
|
* Returns the CSS match count so callers can propagate `matches_n` into the
|
|
@@ -887,9 +923,33 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
887
923
|
console.log(`Scrolled ${direction}`);
|
|
888
924
|
}));
|
|
889
925
|
// ── Inspect ──
|
|
890
|
-
addBrowserTabOption(browser.command('state').description('Page state: URL, title, interactive elements with [N] indices')
|
|
891
|
-
.
|
|
892
|
-
|
|
926
|
+
addBrowserTabOption(browser.command('state').description('Page state: URL, title, interactive elements with [N] indices')
|
|
927
|
+
.option('--source <source>', 'Snapshot backend: dom (default) or ax prototype', 'dom')
|
|
928
|
+
.option('--compare-sources', 'Print DOM vs AX snapshot metrics for observation promotion decisions', false))
|
|
929
|
+
.action(browserAction(async (page, opts) => {
|
|
930
|
+
if (opts.compareSources === true) {
|
|
931
|
+
const [dom, ax] = await Promise.all([
|
|
932
|
+
snapshotSourceMetrics(page, 'dom'),
|
|
933
|
+
snapshotSourceMetrics(page, 'ax'),
|
|
934
|
+
]);
|
|
935
|
+
console.log(JSON.stringify({
|
|
936
|
+
url: await page.getCurrentUrl?.() ?? '',
|
|
937
|
+
sources: { dom, ax },
|
|
938
|
+
}, null, 2));
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const source = String(opts.source ?? 'dom').toLowerCase();
|
|
942
|
+
if (source !== 'dom' && source !== 'ax') {
|
|
943
|
+
console.log(JSON.stringify({
|
|
944
|
+
error: {
|
|
945
|
+
code: 'invalid_source',
|
|
946
|
+
message: `--source must be "dom" or "ax", got "${opts.source}"`,
|
|
947
|
+
},
|
|
948
|
+
}, null, 2));
|
|
949
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const snapshot = await page.snapshot({ viewportExpand: 2000, source: source });
|
|
893
953
|
const url = await page.getCurrentUrl?.() ?? '';
|
|
894
954
|
console.log(`URL: ${url}\n`);
|
|
895
955
|
console.log(typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2));
|
|
@@ -901,21 +961,26 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
901
961
|
}));
|
|
902
962
|
addBrowserTabOption(browser.command('screenshot').argument('[path]', 'Save to file (base64 if omitted)'))
|
|
903
963
|
.option('--full-page', 'Capture the full scrollable page, not just the viewport', false)
|
|
964
|
+
.option('--annotate', 'Overlay visible browser state ref labels on the screenshot', false)
|
|
904
965
|
.option('--width <n>', 'Override viewport width in CSS pixels for this screenshot only', (v) => parseScreenshotDim(v, 'width'))
|
|
905
966
|
.option('--height <n>', 'Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)', (v) => parseScreenshotDim(v, 'height'))
|
|
906
967
|
.description('Take screenshot')
|
|
907
968
|
.action(browserAction(async (page, path, opts) => {
|
|
908
969
|
const shotOpts = {
|
|
909
970
|
fullPage: opts.fullPage === true,
|
|
971
|
+
annotate: opts.annotate === true,
|
|
910
972
|
width: opts.width,
|
|
911
973
|
height: opts.height,
|
|
912
974
|
};
|
|
975
|
+
const capture = opts.annotate === true
|
|
976
|
+
? (page.annotatedScreenshot ?? page.screenshot).bind(page)
|
|
977
|
+
: page.screenshot.bind(page);
|
|
913
978
|
if (path) {
|
|
914
|
-
await
|
|
979
|
+
await capture({ ...shotOpts, path });
|
|
915
980
|
console.log(`Screenshot saved to: ${path}`);
|
|
916
981
|
}
|
|
917
982
|
else {
|
|
918
|
-
console.log(await
|
|
983
|
+
console.log(await capture({ ...shotOpts, format: 'png' }));
|
|
919
984
|
}
|
|
920
985
|
}));
|
|
921
986
|
addBrowserTabOption(browser.command('console'))
|
|
@@ -1053,18 +1118,200 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1053
1118
|
// `browser find --css <sel>` lets agents jump straight from a semantic
|
|
1054
1119
|
// selector to a JSON list of matching elements, without having to parse
|
|
1055
1120
|
// the free-text state snapshot to recover indices.
|
|
1056
|
-
|
|
1121
|
+
const addSemanticLocatorOptions = (cmd) => cmd
|
|
1122
|
+
.option('--role <role>', 'Semantic role (button, link, textbox, option, etc.)')
|
|
1123
|
+
.option('--name <text>', 'Accessible name contains text (aria-label, label, title, placeholder, or visible text)')
|
|
1124
|
+
.option('--label <text>', 'Associated label contains text')
|
|
1125
|
+
.option('--text <text>', 'Visible text contains text')
|
|
1126
|
+
.option('--testid <id>', 'data-testid / data-test / test-id contains id');
|
|
1127
|
+
const addPrefixedSemanticLocatorOptions = (cmd, prefix) => cmd
|
|
1128
|
+
.option(`--${prefix}-role <role>`, `${prefix} semantic role`)
|
|
1129
|
+
.option(`--${prefix}-name <text>`, `${prefix} accessible name contains text`)
|
|
1130
|
+
.option(`--${prefix}-label <text>`, `${prefix} associated label contains text`)
|
|
1131
|
+
.option(`--${prefix}-text <text>`, `${prefix} visible text contains text`)
|
|
1132
|
+
.option(`--${prefix}-testid <id>`, `${prefix} data-testid / data-test / test-id contains id`);
|
|
1133
|
+
const semanticLocatorFromOptions = (opts) => {
|
|
1134
|
+
const locator = {};
|
|
1135
|
+
for (const key of ['role', 'name', 'label', 'text', 'testid']) {
|
|
1136
|
+
const value = opts[key];
|
|
1137
|
+
if (typeof value === 'string' && value.trim())
|
|
1138
|
+
locator[key] = value.trim();
|
|
1139
|
+
}
|
|
1140
|
+
return Object.keys(locator).length > 0 ? locator : null;
|
|
1141
|
+
};
|
|
1142
|
+
const prefixedSemanticLocatorFromOptions = (opts, prefix) => {
|
|
1143
|
+
const locator = {};
|
|
1144
|
+
const map = {
|
|
1145
|
+
role: `${prefix}Role`,
|
|
1146
|
+
name: `${prefix}Name`,
|
|
1147
|
+
label: `${prefix}Label`,
|
|
1148
|
+
text: `${prefix}Text`,
|
|
1149
|
+
testid: `${prefix}Testid`,
|
|
1150
|
+
};
|
|
1151
|
+
for (const key of ['role', 'name', 'label', 'text', 'testid']) {
|
|
1152
|
+
const value = opts[map[key]];
|
|
1153
|
+
if (typeof value === 'string' && value.trim())
|
|
1154
|
+
locator[key] = value.trim();
|
|
1155
|
+
}
|
|
1156
|
+
return Object.keys(locator).length > 0 ? locator : null;
|
|
1157
|
+
};
|
|
1158
|
+
const semanticTargetFromLocator = async (page, locator, mode) => {
|
|
1159
|
+
const result = await page.evaluate(buildSemanticFindJs({ ...locator, limit: 6 }));
|
|
1160
|
+
if (isFindError(result))
|
|
1161
|
+
return result;
|
|
1162
|
+
if (mode === 'write' && result.matches_n !== 1) {
|
|
1163
|
+
return {
|
|
1164
|
+
error: {
|
|
1165
|
+
code: 'semantic_ambiguous',
|
|
1166
|
+
message: `Semantic locator matched ${result.matches_n} elements; write actions require a unique target.`,
|
|
1167
|
+
hint: 'Add --name/--label/--text/--testid or use browser find with a narrower locator.',
|
|
1168
|
+
matches_n: result.matches_n,
|
|
1169
|
+
entries: result.entries,
|
|
1170
|
+
},
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
const first = result.entries[0];
|
|
1174
|
+
if (!first) {
|
|
1175
|
+
return {
|
|
1176
|
+
error: {
|
|
1177
|
+
code: 'semantic_not_found',
|
|
1178
|
+
message: 'Semantic locator matched 0 elements',
|
|
1179
|
+
hint: 'Try browser state, --source ax, or relax the semantic locator.',
|
|
1180
|
+
},
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
const target = String(first.ref);
|
|
1184
|
+
if (mode === 'read') {
|
|
1185
|
+
return {
|
|
1186
|
+
target,
|
|
1187
|
+
...(result.matches_n > 1 ? { total_matches: result.matches_n } : {}),
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
return target;
|
|
1191
|
+
};
|
|
1192
|
+
const semanticTargetFromOptions = async (page, opts, mode) => {
|
|
1193
|
+
const locator = semanticLocatorFromOptions(opts);
|
|
1194
|
+
if (!locator)
|
|
1195
|
+
return null;
|
|
1196
|
+
return semanticTargetFromLocator(page, locator, mode);
|
|
1197
|
+
};
|
|
1198
|
+
const resolveExplicitOrSemanticTarget = async (page, target, opts, mode) => {
|
|
1199
|
+
const explicit = typeof target === 'string' && target.trim() ? target.trim() : '';
|
|
1200
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts);
|
|
1201
|
+
if (explicit && hasSemantic) {
|
|
1202
|
+
return {
|
|
1203
|
+
error: {
|
|
1204
|
+
code: 'usage_error',
|
|
1205
|
+
message: 'Pass either <target> or semantic locator flags, not both.',
|
|
1206
|
+
},
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
if (explicit)
|
|
1210
|
+
return explicit;
|
|
1211
|
+
const semantic = await semanticTargetFromOptions(page, opts, mode);
|
|
1212
|
+
if (semantic)
|
|
1213
|
+
return semantic;
|
|
1214
|
+
return {
|
|
1215
|
+
error: {
|
|
1216
|
+
code: 'usage_error',
|
|
1217
|
+
message: 'Missing target. Pass a numeric ref/CSS selector, or semantic flags like --role button --name Submit.',
|
|
1218
|
+
},
|
|
1219
|
+
};
|
|
1220
|
+
};
|
|
1221
|
+
const printTargetResolutionError = (resolved) => {
|
|
1222
|
+
console.log(JSON.stringify(resolved, null, 2));
|
|
1223
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1224
|
+
};
|
|
1225
|
+
const resolveWriteTargetOrPrint = async (page, target, opts) => {
|
|
1226
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts, 'write');
|
|
1227
|
+
if (typeof resolvedTarget === 'string')
|
|
1228
|
+
return resolvedTarget;
|
|
1229
|
+
if ('error' in resolvedTarget)
|
|
1230
|
+
printTargetResolutionError(resolvedTarget);
|
|
1231
|
+
return null;
|
|
1232
|
+
};
|
|
1233
|
+
const resolveWriteTargetAndValueOrPrint = async (page, targetOrValue, value, opts, valueLabel) => {
|
|
1234
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts);
|
|
1235
|
+
if (hasSemantic && value !== undefined) {
|
|
1236
|
+
printTargetResolutionError({
|
|
1237
|
+
error: {
|
|
1238
|
+
code: 'usage_error',
|
|
1239
|
+
message: `When using semantic locator flags, pass only <${valueLabel}> as the positional argument.`,
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
const resolvedValue = hasSemantic ? targetOrValue : value;
|
|
1245
|
+
if (resolvedValue === undefined) {
|
|
1246
|
+
printTargetResolutionError({
|
|
1247
|
+
error: {
|
|
1248
|
+
code: 'usage_error',
|
|
1249
|
+
message: `Missing ${valueLabel}.`,
|
|
1250
|
+
hint: hasSemantic
|
|
1251
|
+
? `With semantic locator flags, pass the ${valueLabel} as the only positional argument.`
|
|
1252
|
+
: `Pass both a target and ${valueLabel}.`,
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, hasSemantic ? undefined : targetOrValue, opts);
|
|
1258
|
+
if (!resolvedTarget)
|
|
1259
|
+
return null;
|
|
1260
|
+
return { target: resolvedTarget, value: String(resolvedValue) };
|
|
1261
|
+
};
|
|
1262
|
+
const resolvePrefixedWriteTargetOrPrint = async (page, target, opts, prefix, label) => {
|
|
1263
|
+
const explicit = typeof target === 'string' && target.trim() ? target.trim() : '';
|
|
1264
|
+
const locator = prefixedSemanticLocatorFromOptions(opts, prefix);
|
|
1265
|
+
if (explicit && locator) {
|
|
1266
|
+
printTargetResolutionError({
|
|
1267
|
+
error: {
|
|
1268
|
+
code: 'usage_error',
|
|
1269
|
+
message: `Pass either <${label}> or --${prefix}-* semantic locator flags, not both.`,
|
|
1270
|
+
},
|
|
1271
|
+
});
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
if (explicit)
|
|
1275
|
+
return explicit;
|
|
1276
|
+
if (locator) {
|
|
1277
|
+
const resolved = await semanticTargetFromLocator(page, locator, 'write');
|
|
1278
|
+
if (typeof resolved === 'string')
|
|
1279
|
+
return resolved;
|
|
1280
|
+
if ('error' in resolved)
|
|
1281
|
+
printTargetResolutionError(resolved);
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
printTargetResolutionError({
|
|
1285
|
+
error: {
|
|
1286
|
+
code: 'usage_error',
|
|
1287
|
+
message: `Missing ${label}. Pass a numeric ref/CSS selector, or --${prefix}-role/--${prefix}-name semantic flags.`,
|
|
1288
|
+
},
|
|
1289
|
+
});
|
|
1290
|
+
return null;
|
|
1291
|
+
};
|
|
1292
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('find'))
|
|
1057
1293
|
.option('--css <selector>', 'CSS selector (required)')
|
|
1058
1294
|
.option('--limit <n>', 'Max entries returned', '50')
|
|
1059
1295
|
.option('--text-max <n>', 'Max chars of trimmed text per entry', '120')
|
|
1060
|
-
.description('Find DOM elements by CSS
|
|
1296
|
+
.description('Find DOM elements by CSS or semantic locator — returns JSON {matches_n, entries[]}'))
|
|
1061
1297
|
.action(browserAction(async (page, opts) => {
|
|
1062
|
-
|
|
1298
|
+
const locator = semanticLocatorFromOptions(opts);
|
|
1299
|
+
if ((!opts.css || typeof opts.css !== 'string') && !locator) {
|
|
1300
|
+
console.log(JSON.stringify({
|
|
1301
|
+
error: {
|
|
1302
|
+
code: 'usage_error',
|
|
1303
|
+
message: '--css <selector> or a semantic locator flag is required',
|
|
1304
|
+
hint: 'Examples: opencli browser find --css ".btn.primary"; opencli browser find --role button --name Save',
|
|
1305
|
+
},
|
|
1306
|
+
}, null, 2));
|
|
1307
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (opts.css && locator) {
|
|
1063
1311
|
console.log(JSON.stringify({
|
|
1064
1312
|
error: {
|
|
1065
1313
|
code: 'usage_error',
|
|
1066
|
-
message: '--css
|
|
1067
|
-
hint: 'Example: opencli browser find --css ".btn.primary"',
|
|
1314
|
+
message: 'Pass either --css or semantic locator flags, not both.',
|
|
1068
1315
|
},
|
|
1069
1316
|
}, null, 2));
|
|
1070
1317
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
@@ -1082,10 +1329,16 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1082
1329
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1083
1330
|
return;
|
|
1084
1331
|
}
|
|
1085
|
-
const result = await page.evaluate(
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1332
|
+
const result = await page.evaluate(locator
|
|
1333
|
+
? buildSemanticFindJs({
|
|
1334
|
+
...locator,
|
|
1335
|
+
limit: limit ?? undefined,
|
|
1336
|
+
textMax: textMax ?? undefined,
|
|
1337
|
+
})
|
|
1338
|
+
: buildFindJs(opts.css, {
|
|
1339
|
+
limit: limit ?? undefined,
|
|
1340
|
+
textMax: textMax ?? undefined,
|
|
1341
|
+
}));
|
|
1089
1342
|
if (isFindError(result)) {
|
|
1090
1343
|
console.log(JSON.stringify(result, null, 2));
|
|
1091
1344
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
@@ -1112,13 +1365,21 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1112
1365
|
// or a CSS selector. On multi-match CSS, the first element wins and the real
|
|
1113
1366
|
// match count is exposed via `matches_n`; `--nth <n>` picks a specific one.
|
|
1114
1367
|
const runGetCommand = async (page, target, opts, evalJs, field) => {
|
|
1368
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts, 'read');
|
|
1369
|
+
if (typeof resolvedTarget !== 'string' && 'error' in resolvedTarget) {
|
|
1370
|
+
console.log(JSON.stringify(resolvedTarget, null, 2));
|
|
1371
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const targetRef = typeof resolvedTarget === 'string' ? resolvedTarget : resolvedTarget.target;
|
|
1375
|
+
const totalMatches = typeof resolvedTarget === 'string' ? undefined : resolvedTarget.total_matches;
|
|
1115
1376
|
const nth = parseNthFlag(opts.nth);
|
|
1116
1377
|
if (nth && typeof nth === 'object' && 'error' in nth) {
|
|
1117
1378
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: nth.error } }, null, 2));
|
|
1118
1379
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1119
1380
|
return;
|
|
1120
1381
|
}
|
|
1121
|
-
const { matches_n, match_level } = await resolveRef(page,
|
|
1382
|
+
const { matches_n, match_level } = await resolveRef(page, targetRef, {
|
|
1122
1383
|
firstOnMulti: nth === null,
|
|
1123
1384
|
...(typeof nth === 'number' ? { nth } : {}),
|
|
1124
1385
|
});
|
|
@@ -1137,18 +1398,23 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1137
1398
|
else {
|
|
1138
1399
|
value = raw ?? null;
|
|
1139
1400
|
}
|
|
1140
|
-
console.log(JSON.stringify({
|
|
1401
|
+
console.log(JSON.stringify({
|
|
1402
|
+
value,
|
|
1403
|
+
matches_n,
|
|
1404
|
+
match_level,
|
|
1405
|
+
...(totalMatches && totalMatches > 1 ? { total_matches: totalMatches } : {}),
|
|
1406
|
+
}, null, 2));
|
|
1141
1407
|
};
|
|
1142
|
-
addBrowserTabOption(get.command('text')
|
|
1143
|
-
.argument('
|
|
1408
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('text'))
|
|
1409
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1144
1410
|
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1145
1411
|
.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('
|
|
1412
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getTextResolvedJs(), 'text')));
|
|
1413
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('value'))
|
|
1414
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1149
1415
|
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1150
1416
|
.description('Input/textarea value — JSON envelope {value, matches_n}'))
|
|
1151
|
-
.action(browserAction(async (page, target, opts) => runGetCommand(page,
|
|
1417
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getValueResolvedJs(), 'value')));
|
|
1152
1418
|
addBrowserTabOption(get.command('html')
|
|
1153
1419
|
.option('--selector <css>', 'CSS selector scope (first match)')
|
|
1154
1420
|
.option('--as <format>', 'Output format: "html" (default) or "json" for structured tree', 'html')
|
|
@@ -1263,11 +1529,11 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1263
1529
|
}
|
|
1264
1530
|
console.log(html);
|
|
1265
1531
|
}));
|
|
1266
|
-
addBrowserTabOption(get.command('attributes')
|
|
1267
|
-
.argument('
|
|
1532
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('attributes'))
|
|
1533
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1268
1534
|
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1269
1535
|
.description('Element attributes — JSON envelope {value, matches_n}'))
|
|
1270
|
-
.action(browserAction(async (page, target, opts) => runGetCommand(page,
|
|
1536
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getAttributesResolvedJs(), 'attributes')));
|
|
1271
1537
|
// ── Interact ──
|
|
1272
1538
|
//
|
|
1273
1539
|
// Write commands (`click/type/select`) share the same `<target>` contract
|
|
@@ -1290,26 +1556,73 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1290
1556
|
return { opts: { nth: parsed } };
|
|
1291
1557
|
return { opts: {} };
|
|
1292
1558
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1559
|
+
function resolveUploadFilePaths(rawFiles) {
|
|
1560
|
+
const inputs = Array.isArray(rawFiles) ? rawFiles : [];
|
|
1561
|
+
if (inputs.length === 0) {
|
|
1562
|
+
return {
|
|
1563
|
+
error: {
|
|
1564
|
+
code: 'usage_error',
|
|
1565
|
+
message: 'At least one file path is required.',
|
|
1566
|
+
hint: 'Example: opencli browser upload "input[type=file]" ./receipt.pdf',
|
|
1567
|
+
},
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const files = [];
|
|
1571
|
+
for (const input of inputs) {
|
|
1572
|
+
const raw = String(input);
|
|
1573
|
+
const expanded = raw === '~' || raw.startsWith(`~${path.sep}`)
|
|
1574
|
+
? path.join(os.homedir(), raw.slice(2))
|
|
1575
|
+
: raw;
|
|
1576
|
+
const resolved = path.resolve(expanded);
|
|
1577
|
+
if (!fs.existsSync(resolved)) {
|
|
1578
|
+
return { error: { code: 'file_not_found', message: `File not found: ${resolved}` } };
|
|
1579
|
+
}
|
|
1580
|
+
const stat = fs.statSync(resolved);
|
|
1581
|
+
if (!stat.isFile()) {
|
|
1582
|
+
return { error: { code: 'not_a_file', message: `Not a regular file: ${resolved}` } };
|
|
1583
|
+
}
|
|
1584
|
+
files.push(resolved);
|
|
1585
|
+
}
|
|
1586
|
+
return { files };
|
|
1587
|
+
}
|
|
1588
|
+
function parseResolveFlag(raw, flag) {
|
|
1589
|
+
const parsed = parseNthFlag(raw);
|
|
1590
|
+
if (parsed && typeof parsed === 'object' && 'error' in parsed) {
|
|
1591
|
+
return { error: parsed.error.replace('--nth', flag) };
|
|
1592
|
+
}
|
|
1593
|
+
if (typeof parsed === 'number')
|
|
1594
|
+
return { opts: { nth: parsed } };
|
|
1595
|
+
return { opts: {} };
|
|
1596
|
+
}
|
|
1597
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('click'))
|
|
1598
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1295
1599
|
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1296
1600
|
.description('Click element — JSON envelope {clicked, target, matches_n}'))
|
|
1297
1601
|
.action(browserAction(async (page, target, opts) => {
|
|
1602
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts ?? {}, 'write');
|
|
1603
|
+
if (typeof resolvedTarget !== 'string') {
|
|
1604
|
+
console.log(JSON.stringify(resolvedTarget, null, 2));
|
|
1605
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1298
1608
|
const parsed = nthToResolveOpts(opts?.nth);
|
|
1299
1609
|
if ('error' in parsed) {
|
|
1300
1610
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1301
1611
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1302
1612
|
return;
|
|
1303
1613
|
}
|
|
1304
|
-
const { matches_n, match_level } = await page.click(
|
|
1305
|
-
console.log(JSON.stringify({ clicked: true, target:
|
|
1614
|
+
const { matches_n, match_level } = await page.click(resolvedTarget, parsed.opts);
|
|
1615
|
+
console.log(JSON.stringify({ clicked: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1306
1616
|
}));
|
|
1307
|
-
addBrowserTabOption(browser.command('type')
|
|
1308
|
-
.argument('
|
|
1309
|
-
.argument('
|
|
1617
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('type'))
|
|
1618
|
+
.argument('[targetOrText]', 'Numeric ref/CSS target, or text when using --role/--name/etc.')
|
|
1619
|
+
.argument('[text]', 'Text to type')
|
|
1310
1620
|
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1311
1621
|
.description('Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}'))
|
|
1312
|
-
.action(browserAction(async (page,
|
|
1622
|
+
.action(browserAction(async (page, targetOrText, text, opts) => {
|
|
1623
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrText, text, opts ?? {}, 'text');
|
|
1624
|
+
if (!resolved)
|
|
1625
|
+
return;
|
|
1313
1626
|
const parsed = nthToResolveOpts(opts?.nth);
|
|
1314
1627
|
if ('error' in parsed) {
|
|
1315
1628
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
@@ -1317,42 +1630,199 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1317
1630
|
return;
|
|
1318
1631
|
}
|
|
1319
1632
|
// Click first (focuses the field), wait briefly, then type.
|
|
1320
|
-
await page.click(
|
|
1633
|
+
await page.click(resolved.target, parsed.opts);
|
|
1321
1634
|
await page.wait(0.3);
|
|
1322
|
-
const { matches_n, match_level } = await page.typeText(
|
|
1635
|
+
const { matches_n, match_level } = await page.typeText(resolved.target, resolved.value, parsed.opts);
|
|
1323
1636
|
// __resolved is already set by the resolver call inside page.typeText
|
|
1324
1637
|
const isAutocomplete = await page.evaluate(isAutocompleteResolvedJs());
|
|
1325
1638
|
if (isAutocomplete)
|
|
1326
1639
|
await page.wait(0.4);
|
|
1327
1640
|
console.log(JSON.stringify({
|
|
1328
1641
|
typed: true,
|
|
1329
|
-
text:
|
|
1330
|
-
target:
|
|
1642
|
+
text: resolved.value,
|
|
1643
|
+
target: resolved.target,
|
|
1331
1644
|
matches_n,
|
|
1332
1645
|
match_level,
|
|
1333
1646
|
autocomplete: !!isAutocomplete,
|
|
1334
1647
|
}, null, 2));
|
|
1335
1648
|
}));
|
|
1336
|
-
addBrowserTabOption(browser.command('
|
|
1337
|
-
.argument('
|
|
1338
|
-
.
|
|
1649
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('hover'))
|
|
1650
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1651
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1652
|
+
.description('Move the mouse over an element — JSON envelope {hovered, target, matches_n}'))
|
|
1653
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1654
|
+
if (typeof page.hover !== 'function')
|
|
1655
|
+
throw new Error('browser hover is not supported by this browser backend');
|
|
1656
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1657
|
+
if (!resolvedTarget)
|
|
1658
|
+
return;
|
|
1659
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1660
|
+
if ('error' in parsed) {
|
|
1661
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1662
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const { matches_n, match_level } = await page.hover(resolvedTarget, parsed.opts);
|
|
1666
|
+
console.log(JSON.stringify({ hovered: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1667
|
+
}));
|
|
1668
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('focus'))
|
|
1669
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1670
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1671
|
+
.description('Focus an element — JSON envelope {focused, target, matches_n}'))
|
|
1672
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1673
|
+
if (typeof page.focus !== 'function')
|
|
1674
|
+
throw new Error('browser focus is not supported by this browser backend');
|
|
1675
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1676
|
+
if (!resolvedTarget)
|
|
1677
|
+
return;
|
|
1678
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1679
|
+
if ('error' in parsed) {
|
|
1680
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1681
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const { focused, matches_n, match_level } = await page.focus(resolvedTarget, parsed.opts);
|
|
1685
|
+
console.log(JSON.stringify({ focused, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1686
|
+
}));
|
|
1687
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('dblclick'))
|
|
1688
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1689
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1690
|
+
.description('Double-click element — JSON envelope {dblclicked, target, matches_n}'))
|
|
1691
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1692
|
+
if (typeof page.dblClick !== 'function')
|
|
1693
|
+
throw new Error('browser dblclick is not supported by this browser backend');
|
|
1694
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1695
|
+
if (!resolvedTarget)
|
|
1696
|
+
return;
|
|
1697
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1698
|
+
if ('error' in parsed) {
|
|
1699
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1700
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const { matches_n, match_level } = await page.dblClick(resolvedTarget, parsed.opts);
|
|
1704
|
+
console.log(JSON.stringify({ dblclicked: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1705
|
+
}));
|
|
1706
|
+
const runCheckCommand = async (page, target, opts, checked) => {
|
|
1707
|
+
if (typeof page.setChecked !== 'function')
|
|
1708
|
+
throw new Error(`browser ${checked ? 'check' : 'uncheck'} is not supported by this browser backend`);
|
|
1709
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts);
|
|
1710
|
+
if (!resolvedTarget)
|
|
1711
|
+
return;
|
|
1712
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1713
|
+
if ('error' in parsed) {
|
|
1714
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1715
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const result = await page.setChecked(resolvedTarget, checked, parsed.opts);
|
|
1719
|
+
console.log(JSON.stringify({
|
|
1720
|
+
checked: result.checked,
|
|
1721
|
+
changed: result.changed,
|
|
1722
|
+
target: resolvedTarget,
|
|
1723
|
+
matches_n: result.matches_n,
|
|
1724
|
+
match_level: result.match_level,
|
|
1725
|
+
...(result.kind ? { kind: result.kind } : {}),
|
|
1726
|
+
}, null, 2));
|
|
1727
|
+
};
|
|
1728
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('check'))
|
|
1729
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1730
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1731
|
+
.description('Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}'))
|
|
1732
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1733
|
+
await runCheckCommand(page, target, opts ?? {}, true);
|
|
1734
|
+
}));
|
|
1735
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('uncheck'))
|
|
1736
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1737
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1738
|
+
.description('Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}'))
|
|
1739
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1740
|
+
await runCheckCommand(page, target, opts ?? {}, false);
|
|
1741
|
+
}));
|
|
1742
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('upload'))
|
|
1743
|
+
.argument('[targetOrFile]', 'Numeric ref/CSS target, or first file when using --role/--name/etc.')
|
|
1744
|
+
.argument('[files...]', 'Local file path(s) to attach')
|
|
1745
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1746
|
+
.description('Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}'))
|
|
1747
|
+
.action(browserAction(async (page, targetOrFile, files, opts) => {
|
|
1748
|
+
if (typeof page.uploadFiles !== 'function')
|
|
1749
|
+
throw new Error('browser upload is not supported by this browser backend');
|
|
1750
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts ?? {});
|
|
1751
|
+
const target = hasSemantic ? undefined : targetOrFile;
|
|
1752
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1753
|
+
if (!resolvedTarget)
|
|
1754
|
+
return;
|
|
1755
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1756
|
+
if ('error' in parsed) {
|
|
1757
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1758
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
const rawFiles = hasSemantic
|
|
1762
|
+
? [targetOrFile, ...(Array.isArray(files) ? files : [])].filter((value) => value !== undefined)
|
|
1763
|
+
: files;
|
|
1764
|
+
const resolvedFiles = resolveUploadFilePaths(rawFiles);
|
|
1765
|
+
if ('error' in resolvedFiles) {
|
|
1766
|
+
console.log(JSON.stringify({ error: resolvedFiles.error }, null, 2));
|
|
1767
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const result = await page.uploadFiles(resolvedTarget, resolvedFiles.files, parsed.opts);
|
|
1771
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1772
|
+
}));
|
|
1773
|
+
addBrowserTabOption(addPrefixedSemanticLocatorOptions(addPrefixedSemanticLocatorOptions(browser.command('drag'), 'from'), 'to')
|
|
1774
|
+
.argument('[source]', 'Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.')
|
|
1775
|
+
.argument('[target]', 'Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc.')
|
|
1776
|
+
.option('--from-nth <n>', 'When <source> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1777
|
+
.option('--to-nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1778
|
+
.description('Drag one element to another — JSON envelope {dragged, source, target, source_matches_n, target_matches_n}'))
|
|
1779
|
+
.action(browserAction(async (page, source, target, opts) => {
|
|
1780
|
+
if (typeof page.drag !== 'function')
|
|
1781
|
+
throw new Error('browser drag is not supported by this browser backend');
|
|
1782
|
+
const resolvedSource = await resolvePrefixedWriteTargetOrPrint(page, source, opts ?? {}, 'from', 'source');
|
|
1783
|
+
if (!resolvedSource)
|
|
1784
|
+
return;
|
|
1785
|
+
const resolvedTarget = await resolvePrefixedWriteTargetOrPrint(page, target, opts ?? {}, 'to', 'target');
|
|
1786
|
+
if (!resolvedTarget)
|
|
1787
|
+
return;
|
|
1788
|
+
const from = parseResolveFlag(opts?.fromNth, '--from-nth');
|
|
1789
|
+
if ('error' in from) {
|
|
1790
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: from.error } }, null, 2));
|
|
1791
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const to = parseResolveFlag(opts?.toNth, '--to-nth');
|
|
1795
|
+
if ('error' in to) {
|
|
1796
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: to.error } }, null, 2));
|
|
1797
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const result = await page.drag(resolvedSource, resolvedTarget, { from: from.opts, to: to.opts });
|
|
1801
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1802
|
+
}));
|
|
1803
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('fill'))
|
|
1804
|
+
.argument('[targetOrText]', 'Numeric ref/CSS target, or text when using --role/--name/etc.')
|
|
1805
|
+
.argument('[text]', 'Text to set exactly')
|
|
1339
1806
|
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1340
1807
|
.description('Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}'))
|
|
1341
|
-
.action(browserAction(async (page,
|
|
1808
|
+
.action(browserAction(async (page, targetOrText, text, opts) => {
|
|
1809
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrText, text, opts ?? {}, 'text');
|
|
1810
|
+
if (!resolved)
|
|
1811
|
+
return;
|
|
1342
1812
|
const parsed = nthToResolveOpts(opts?.nth);
|
|
1343
1813
|
if ('error' in parsed) {
|
|
1344
1814
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1345
1815
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1346
1816
|
return;
|
|
1347
1817
|
}
|
|
1348
|
-
const result = await page.fillText(
|
|
1818
|
+
const result = await page.fillText(resolved.target, resolved.value, parsed.opts);
|
|
1349
1819
|
if (!result.verified)
|
|
1350
1820
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
1351
1821
|
console.log(JSON.stringify({
|
|
1352
1822
|
filled: result.filled,
|
|
1353
1823
|
verified: result.verified,
|
|
1354
|
-
target:
|
|
1355
|
-
text:
|
|
1824
|
+
target: resolved.target,
|
|
1825
|
+
text: resolved.value,
|
|
1356
1826
|
actual: result.actual,
|
|
1357
1827
|
length: result.length,
|
|
1358
1828
|
matches_n: result.matches_n,
|
|
@@ -1360,20 +1830,23 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1360
1830
|
...(result.mode ? { mode: result.mode } : {}),
|
|
1361
1831
|
}, null, 2));
|
|
1362
1832
|
}));
|
|
1363
|
-
addBrowserTabOption(browser.command('select')
|
|
1364
|
-
.argument('
|
|
1365
|
-
.argument('
|
|
1833
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('select'))
|
|
1834
|
+
.argument('[targetOrOption]', 'Numeric ref/CSS target, or option text when using --role/--name/etc.')
|
|
1835
|
+
.argument('[option]', 'Option text (or value) to select')
|
|
1366
1836
|
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1367
1837
|
.description('Select dropdown option — JSON envelope {selected, target, matches_n}'))
|
|
1368
|
-
.action(browserAction(async (page,
|
|
1838
|
+
.action(browserAction(async (page, targetOrOption, option, opts) => {
|
|
1839
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrOption, option, opts ?? {}, 'option');
|
|
1840
|
+
if (!resolved)
|
|
1841
|
+
return;
|
|
1369
1842
|
const parsed = nthToResolveOpts(opts?.nth);
|
|
1370
1843
|
if ('error' in parsed) {
|
|
1371
1844
|
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1372
1845
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1373
1846
|
return;
|
|
1374
1847
|
}
|
|
1375
|
-
const { matches_n, match_level } = await resolveRef(page,
|
|
1376
|
-
const result = await page.evaluate(selectResolvedJs(
|
|
1848
|
+
const { matches_n, match_level } = await resolveRef(page, resolved.target, parsed.opts);
|
|
1849
|
+
const result = await page.evaluate(selectResolvedJs(resolved.value));
|
|
1377
1850
|
if (result?.error) {
|
|
1378
1851
|
// The select-specific "Not a <select>" / "Option not found" errors
|
|
1379
1852
|
// are domain-level failures — emit a structured envelope so agents
|
|
@@ -1390,8 +1863,8 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1390
1863
|
return;
|
|
1391
1864
|
}
|
|
1392
1865
|
console.log(JSON.stringify({
|
|
1393
|
-
selected: result?.selected ??
|
|
1394
|
-
target:
|
|
1866
|
+
selected: result?.selected ?? resolved.value,
|
|
1867
|
+
target: resolved.target,
|
|
1395
1868
|
matches_n,
|
|
1396
1869
|
match_level,
|
|
1397
1870
|
}, null, 2));
|
|
@@ -1458,10 +1931,10 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1458
1931
|
}));
|
|
1459
1932
|
// ── Wait commands ──
|
|
1460
1933
|
addBrowserTabOption(browser.command('wait'))
|
|
1461
|
-
.argument('<type>', 'selector, text, time, or
|
|
1462
|
-
.argument('[value]', 'CSS selector, text string, seconds, or
|
|
1934
|
+
.argument('<type>', 'selector, text, time, xhr, or download')
|
|
1935
|
+
.argument('[value]', 'CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern')
|
|
1463
1936
|
.option('--timeout <ms>', 'Timeout in milliseconds', '10000')
|
|
1464
|
-
.description('Wait for selector, text, time,
|
|
1937
|
+
.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
1938
|
.action(browserAction(async (page, type, value, opts) => {
|
|
1466
1939
|
const timeout = parseInt(opts.timeout, 10);
|
|
1467
1940
|
if (type === 'time') {
|
|
@@ -1538,8 +2011,36 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1538
2011
|
matched: { url: matched.url, status: matched.status, contentType: matched.ct },
|
|
1539
2012
|
}, null, 2));
|
|
1540
2013
|
}
|
|
2014
|
+
else if (type === 'download') {
|
|
2015
|
+
if (typeof page.waitForDownload !== 'function') {
|
|
2016
|
+
console.log(JSON.stringify({
|
|
2017
|
+
error: {
|
|
2018
|
+
code: 'download_wait_unavailable',
|
|
2019
|
+
message: 'The active browser backend does not support download lifecycle waits.',
|
|
2020
|
+
hint: 'Use the Browser Bridge extension version 1.0.8 or newer, then retry the command.',
|
|
2021
|
+
},
|
|
2022
|
+
}, null, 2));
|
|
2023
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const result = await page.waitForDownload(String(value ?? ''), timeout);
|
|
2027
|
+
if (!result.downloaded) {
|
|
2028
|
+
const code = result.state === 'interrupted' && result.id !== undefined ? 'download_failed' : 'download_not_seen';
|
|
2029
|
+
console.log(JSON.stringify({
|
|
2030
|
+
error: {
|
|
2031
|
+
code,
|
|
2032
|
+
message: result.error ?? `No download matched "${value ?? '*'}" within ${timeout}ms`,
|
|
2033
|
+
hint: 'Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly.',
|
|
2034
|
+
},
|
|
2035
|
+
download: result,
|
|
2036
|
+
}, null, 2));
|
|
2037
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2041
|
+
}
|
|
1541
2042
|
else {
|
|
1542
|
-
console.error(`Unknown wait type "${type}". Use: selector, text, time, or
|
|
2043
|
+
console.error(`Unknown wait type "${type}". Use: selector, text, time, xhr, or download`);
|
|
1543
2044
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1544
2045
|
}
|
|
1545
2046
|
}));
|
|
@@ -2605,6 +3106,7 @@ cli({
|
|
|
2605
3106
|
}
|
|
2606
3107
|
const adapterGroups = { external: externalNames, apps, sites };
|
|
2607
3108
|
const adapterNameSet = new Set([...externalNames, ...siteNames]);
|
|
3109
|
+
installCommanderNamespaceStructuredHelp(browser, { globalCommand: program, description: originalBrowserDescription });
|
|
2608
3110
|
program.configureHelp({
|
|
2609
3111
|
visibleCommands: (command) => command.commands.filter(child => command !== program || !adapterNameSet.has(child.name())),
|
|
2610
3112
|
});
|