fastbrowser_cli 1.0.30 → 1.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/fastbrowser_cli/fastbrowser_cli.js +11 -19
- package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
- package/dist/fastbrowser_cli/libs/query-builder.d.ts +2 -0
- package/dist/fastbrowser_cli/libs/query-builder.d.ts.map +1 -1
- package/dist/fastbrowser_cli/libs/query-builder.js +4 -0
- package/dist/fastbrowser_cli/libs/query-builder.js.map +1 -1
- package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts +2 -0
- package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/fastbrowser_mcp.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js +147 -22
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_my_client.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_my_client.js +8 -0
- package/dist/fastbrowser_mcp/libs/mcp_my_client.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js +15 -2
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/schemas.d.ts +4 -0
- package/dist/fastbrowser_mcp/libs/schemas.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/schemas.js +6 -0
- package/dist/fastbrowser_mcp/libs/schemas.js.map +1 -1
- package/dist/shared/logger.d.ts +86 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +269 -0
- package/dist/shared/logger.js.map +1 -0
- package/docs/brainstorm_scrap_by_ai.md +1 -1
- package/docs/feature_support_cli.md +7 -8
- package/docs/target_tools/target_tools_chrome_devtools.md +963 -0
- package/docs/target_tools/target_tools_playwright.md +763 -0
- package/examples/linkedin_cli/linked_dm.sh +16 -0
- package/examples/linkedin_cli/linked_post.sh +13 -0
- package/examples/linkedin_cli/linkedin.snapshot.txt +1245 -0
- package/examples/todomvc/todomvc.a11y.txt +44 -0
- package/examples/todomvc/todomvc.sh +11 -0
- package/examples/wttj_cli/fastbrowser_helper.ts +39 -0
- package/examples/wttj_cli/wttf_job-original.a11y.txt +652 -0
- package/examples/wttj_cli/wttf_job.a11y.txt +317 -0
- package/examples/{welcometothejungle/wttj-job.ts → wttj_cli/wttj_job copy.ts } +60 -11
- package/examples/wttj_cli/wttj_job.ts +179 -0
- package/examples/wttj_cli/wttj_search.ts +162 -0
- package/package.json +10 -4
- package/skills/fastbrowser/SKILL.md +10 -11
- package/skills/fastbrowser-script/SKILL.md +4 -4
- package/src/fastbrowser_cli/fastbrowser_cli.ts +14 -25
- package/src/fastbrowser_cli/libs/query-builder.ts +6 -0
- package/src/fastbrowser_mcp/fastbrowser_mcp.ts +181 -26
- package/src/fastbrowser_mcp/libs/mcp_my_client.ts +17 -0
- package/src/fastbrowser_mcp/libs/mcp_target_helper.ts +15 -2
- package/src/fastbrowser_mcp/libs/schemas.ts +6 -0
- package/src/shared/logger.ts +317 -0
- package/test.a11y.txt +828 -0
- package/tests/query-builder.test.ts +51 -11
- package/examples/welcometothejungle/fastbrowser_helper.ts +0 -39
- package/examples/welcometothejungle/wttj-search.ts +0 -82
- /package/examples/{post-to-x.sh → twitter_cli/twitter_post.sh} +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
|
|
3
|
+
// npm imports
|
|
4
|
+
import * as Commander from 'commander';
|
|
5
|
+
import { FastBrowserHelper } from './fastbrowser_helper.js';
|
|
6
|
+
import { A11yTree } from 'a11y_parse';
|
|
7
|
+
|
|
8
|
+
type JobSearchResult = {
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
15
|
+
//
|
|
16
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
17
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
class WttjSearch {
|
|
20
|
+
|
|
21
|
+
static async outputJson(jobs: Array<JobSearchResult>): Promise<void> {
|
|
22
|
+
console.log(JSON.stringify(jobs, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async outputMarkdown(jobs: Array<JobSearchResult>): Promise<void> {
|
|
26
|
+
console.log('| # | Title | URL |');
|
|
27
|
+
console.log('|---|-------|-----|');
|
|
28
|
+
for (const [i, job] of jobs.entries()) {
|
|
29
|
+
const url = job.url !== '' ? `[link](${job.url})` : '';
|
|
30
|
+
console.log(`| ${i + 1} | ${job.title} | ${url} |`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
35
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
36
|
+
//
|
|
37
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
38
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
39
|
+
|
|
40
|
+
private static parseJobTitles(output: string): Array<JobSearchResult> {
|
|
41
|
+
const results: Array<JobSearchResult> = [];
|
|
42
|
+
const headingRe = /heading "(.+?)" level/g;
|
|
43
|
+
const urlRe = /url="([^"]+)"/;
|
|
44
|
+
for (const line of output.split('\n')) {
|
|
45
|
+
const headingMatch = headingRe.exec(line);
|
|
46
|
+
if (headingMatch === null) continue;
|
|
47
|
+
const urlMatch = urlRe.exec(line);
|
|
48
|
+
results.push({
|
|
49
|
+
title: headingMatch[1],
|
|
50
|
+
url: urlMatch !== null ? urlMatch[1] : '',
|
|
51
|
+
});
|
|
52
|
+
headingRe.lastIndex = 0;
|
|
53
|
+
}
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static async collectOnePageResults(): Promise<Array<JobSearchResult>> {
|
|
58
|
+
const searchResults: Array<JobSearchResult> = [];
|
|
59
|
+
const a11ySelector = 'link[url^="/fr/companies/"]';
|
|
60
|
+
let outputStr: string = await FastBrowserHelper.querySelectorsAll(a11ySelector, 0);
|
|
61
|
+
// trim output to avoid issues with trailing newlines
|
|
62
|
+
outputStr = outputStr.trim();
|
|
63
|
+
// split output into lines and remove the first line which is a header
|
|
64
|
+
const outputLines = outputStr.split('\n');
|
|
65
|
+
// remove the first line which is a header
|
|
66
|
+
outputLines.shift();
|
|
67
|
+
|
|
68
|
+
// parse each line as an axNode and extract the job title and url
|
|
69
|
+
for (const outputLine of outputLines) {
|
|
70
|
+
const axNode = A11yTree.parse(outputLine);
|
|
71
|
+
// sanity check - the axNode should have a 'name' and a 'url' attribute
|
|
72
|
+
if (axNode.name === undefined) {
|
|
73
|
+
throw new Error(`Expected axNode.name to be defined for selector: ${a11ySelector} line: ${outputLine}`);
|
|
74
|
+
}
|
|
75
|
+
if (axNode.attributes['url'] === undefined) {
|
|
76
|
+
throw new Error(`Expected axNode.url to be defined for selector: ${a11ySelector} line: ${outputLine}`);
|
|
77
|
+
}
|
|
78
|
+
// Build the search result object and add it to the results array
|
|
79
|
+
const searchResult: JobSearchResult = {
|
|
80
|
+
title: axNode.name!,
|
|
81
|
+
url: `https://www.welcometothejungle.com${axNode.attributes['url']!}`,
|
|
82
|
+
};
|
|
83
|
+
searchResults.push(searchResult);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// return the search results
|
|
87
|
+
return searchResults
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static async runSearch(query: string, pageIndexLimit: number = 1, pageIndexOffset: number = 0): Promise<Array<JobSearchResult>> {
|
|
91
|
+
const searchResults: Array<JobSearchResult> = [];
|
|
92
|
+
for (let pageIndex = pageIndexOffset; pageIndex < pageIndexOffset + pageIndexLimit; pageIndex++) {
|
|
93
|
+
// load the page
|
|
94
|
+
const pageUrl = `https://www.welcometothejungle.com/fr/jobs-matches?page=${pageIndex + 1}`;
|
|
95
|
+
FastBrowserHelper.navigatePage(pageUrl);
|
|
96
|
+
|
|
97
|
+
// collect results for the current page
|
|
98
|
+
const newSearchResults = await this.collectOnePageResults();
|
|
99
|
+
searchResults.push(...newSearchResults);
|
|
100
|
+
}
|
|
101
|
+
return searchResults;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
106
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
107
|
+
//
|
|
108
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
109
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
114
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
115
|
+
//
|
|
116
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
117
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
118
|
+
|
|
119
|
+
const program = new Commander.Command();
|
|
120
|
+
program
|
|
121
|
+
.argument('[query]', 'job search query', 'machine learning')
|
|
122
|
+
.option('-l, --page-limit <number>', 'number of pages to fetch', '1')
|
|
123
|
+
.option('-o, --page-offset <number>', 'first page to scrape', '0')
|
|
124
|
+
.addOption(new Commander.Option('-f, --format <format>', 'output format: json or markdown').choices(['json', 'markdown']).default('markdown'))
|
|
125
|
+
.parse();
|
|
126
|
+
|
|
127
|
+
type CliOptions = {
|
|
128
|
+
pageLimit: string;
|
|
129
|
+
pageOffset: string;
|
|
130
|
+
format: 'json' | 'markdown';
|
|
131
|
+
};
|
|
132
|
+
const options: CliOptions = program.opts<CliOptions>();
|
|
133
|
+
|
|
134
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
135
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
136
|
+
//
|
|
137
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
138
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
139
|
+
|
|
140
|
+
// run the search and get results
|
|
141
|
+
const query = program.args[0]
|
|
142
|
+
const limit = Number(options.pageLimit);
|
|
143
|
+
const offset = Number(options.pageOffset);
|
|
144
|
+
const searchResults: Array<JobSearchResult> = await WttjSearch.runSearch(query, limit, offset);
|
|
145
|
+
|
|
146
|
+
// Output results in the specified format
|
|
147
|
+
if (options.format === 'json') {
|
|
148
|
+
await WttjSearch.outputJson(searchResults);
|
|
149
|
+
} else if (options.format === 'markdown') {
|
|
150
|
+
await WttjSearch.outputMarkdown(searchResults);
|
|
151
|
+
} else {
|
|
152
|
+
throw new Error(`Unsupported output format: ${options.format}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
157
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
158
|
+
// Entry point
|
|
159
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
160
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
161
|
+
|
|
162
|
+
void main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastbrowser_cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.33",
|
|
4
4
|
"description": "A CLI tool for FastBrowser, providing commands to interact with the FastBrowser MCP (Model Context Protocol) server and perform various browser automation tasks.",
|
|
5
5
|
"main": "dist/fastbrowser_cli/fastbrowser_cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,14 +12,20 @@
|
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/jeromeetienne/
|
|
15
|
+
"url": "git+https://github.com/jeromeetienne/skillmd_collection.git",
|
|
16
|
+
"directory": "packages/fastbrowser_cli"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/jeromeetienne/skillmd_collection/tree/main/packages/fastbrowser_cli#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/jeromeetienne/skillmd_collection/issues"
|
|
16
21
|
},
|
|
17
|
-
"homepage": "https://github.com/jeromeetienne/skillmd_collection/packages/fastbrowser_cli#readme",
|
|
18
22
|
"type": "module",
|
|
19
23
|
"dependencies": {
|
|
20
24
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
|
+
"chalk": "^5.6.2",
|
|
21
26
|
"commander": "^12.1.0",
|
|
22
27
|
"express": "^4.21.2",
|
|
28
|
+
"minimatch": "^10.2.5",
|
|
23
29
|
"string-argv": "^0.3.2",
|
|
24
30
|
"typescript": "^6.0.3",
|
|
25
31
|
"zod": "^4.3.6",
|
|
@@ -43,7 +49,7 @@
|
|
|
43
49
|
"inspect:playwright:extension": "npx @modelcontextprotocol/inspector npx @playwright/mcp@latest --extension",
|
|
44
50
|
"build": "tsc -p tsconfig.build.json",
|
|
45
51
|
"prepublish:check": "test -z \"$(git status --porcelain)\" || (echo 'Working tree dirty — commit or stash first' && exit 1)",
|
|
46
|
-
"version:bump": "npm version patch --no-git-tag-version && git add package.json && git commit -m \"feat: bump version
|
|
52
|
+
"version:bump": "npm version patch --no-git-tag-version && git add package.json && git commit -m \"feat: bump version in fastbrowser_cli package.json\"",
|
|
47
53
|
"publish:all": "pnpm run prepublish:check && pnpm run build && pnpm run version:bump && pnpm publish --access public",
|
|
48
54
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
49
55
|
"test": "npx tsx --test 'tests/**/*.test.ts'"
|
|
@@ -20,7 +20,7 @@ npx fastbrowser_cli <command> [flags]
|
|
|
20
20
|
|
|
21
21
|
## Typical Workflow
|
|
22
22
|
|
|
23
|
-
1. **Query** the accessibility tree for specific nodes
|
|
23
|
+
1. **Query** the accessibility tree for specific nodes with `query_selectors` (first match per selector by default; pass `-a, --all` for every match).
|
|
24
24
|
2. **Act** on an element by its accessibility selector: `click`, `fill_form`, `press_keys`. The selector can be a direct uid reference (e.g. `#1_42`, fastest path) or any CSS-like selector (e.g. `button[name="Submit"]`), which is resolved to a uid internally.
|
|
25
25
|
|
|
26
26
|
Snapshot output looks like:
|
|
@@ -145,8 +145,8 @@ Example queries on it:
|
|
|
145
145
|
|
|
146
146
|
## Inspection
|
|
147
147
|
|
|
148
|
-
- `query_selectors`
|
|
149
|
-
-
|
|
148
|
+
- `query_selectors` is the most efficient way to get specific elements or data from the page. Use it instead of `take_snapshot` whenever possible.
|
|
149
|
+
- By default, `query_selectors` returns the first match per selector (cheaper, less output). Pass `-a, --all` when you need every match — pair it with `--limit` to cap results per selector.
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
152
|
# Query the accessibility tree returning the FIRST match per selector (--selector is repeatable)
|
|
@@ -159,14 +159,14 @@ npx fastbrowser_cli query_selectors --selector 'heading[level="1"]' --no-with-an
|
|
|
159
159
|
npx fastbrowser_cli query_selectors \
|
|
160
160
|
--selectors-json '[{"selector":"button","withAncestors":true},{"selector":"link","withAncestors":false}]'
|
|
161
161
|
|
|
162
|
-
#
|
|
163
|
-
npx fastbrowser_cli
|
|
162
|
+
# Pass --all to return every match per selector; --limit caps results per selector (0 = unlimited)
|
|
163
|
+
npx fastbrowser_cli query_selectors --all --selector "button" --selector "link" --limit 5
|
|
164
164
|
|
|
165
165
|
# Exclude ancestor nodes from the result
|
|
166
|
-
npx fastbrowser_cli
|
|
166
|
+
npx fastbrowser_cli query_selectors --all --selector 'heading[level="1"]' --no-with-ancestors
|
|
167
167
|
|
|
168
|
-
# Per-selector control over limit / withAncestors via JSON
|
|
169
|
-
npx fastbrowser_cli
|
|
168
|
+
# Per-selector control over limit / withAncestors via JSON (with --all)
|
|
169
|
+
npx fastbrowser_cli query_selectors --all \
|
|
170
170
|
--selectors-json '[{"selector":"button","limit":3,"withAncestors":true},{"selector":"link","limit":0,"withAncestors":false}]'
|
|
171
171
|
|
|
172
172
|
# Take an accessibility-tree full page snapshot of the current page - very expensive, prefer targeted queries when possible
|
|
@@ -233,9 +233,8 @@ press_keys --keys "Tab, Enter"
|
|
|
233
233
|
| `new_page` | Open a new page at a URL | `--url` |
|
|
234
234
|
| `close_page` | Close a page by id | `--page-id` |
|
|
235
235
|
| `navigate_page` | Navigate current page to a URL | `--url` |
|
|
236
|
-
| `take_snapshot` | Dump the accessibility tree of the whole page - very expensive, prefer targeted queries (`query_selectors`
|
|
237
|
-
| `query_selectors` | Query a11y tree by CSS-like selector
|
|
238
|
-
| `query_selectors_all` | Query a11y tree by CSS-like selector, returning every match per selector | `--selector` or `--selectors-json` |
|
|
236
|
+
| `take_snapshot` | Dump the accessibility tree of the whole page - very expensive, prefer targeted queries (`query_selectors`) when possible | — |
|
|
237
|
+
| `query_selectors` | Query a11y tree by CSS-like selector (first match per selector by default; pass `-a, --all` for every match, with optional `--limit`) | `--selector` or `--selectors-json` |
|
|
239
238
|
| `click` | Click an element by accessibility selector | `--selector` / `-s` |
|
|
240
239
|
| `fill_form` | Fill a form field by accessibility selector | `--selector` / `-s`, `--value` |
|
|
241
240
|
| `press_keys` | Press a comma-separated key sequence | `--keys` |
|
|
@@ -30,7 +30,7 @@ Always follow this order — guessing selectors without checking the page wastes
|
|
|
30
30
|
the user wants to parameterize (query, message, page count), and output format (stdout text,
|
|
31
31
|
JSON, Markdown, file).
|
|
32
32
|
2. **Probe the live page.** Open it and find selectors that actually match. Prefer
|
|
33
|
-
`query_selectors`
|
|
33
|
+
`query_selectors` (use `-a, --all` to get every match) over `take_snapshot` (cheaper, less noise).
|
|
34
34
|
```bash
|
|
35
35
|
npx fastbrowser_cli new_page --url https://example.com
|
|
36
36
|
npx fastbrowser_cli query_selectors -s 'searchbox' -s 'button[name*="Search"]'
|
|
@@ -72,12 +72,12 @@ Hard-coded selectors are the fragile part of any scraper — choose stable ones.
|
|
|
72
72
|
- Use **scoping** when a role is ambiguous: `dialog textbox`, `navigation > link`.
|
|
73
73
|
- **Never** use uid selectors (`#1_42`, `#4`) in a script. They are session-local and change on
|
|
74
74
|
every page rebuild. See the hard rule at the top.
|
|
75
|
-
- If you need every match (e.g. a list of results), use `
|
|
76
|
-
reasonable cap;
|
|
75
|
+
- If you need every match (e.g. a list of results), use `query_selectors --all --limit N` with a
|
|
76
|
+
reasonable cap; without `--limit` it returns everything and is expensive.
|
|
77
77
|
|
|
78
78
|
## Output parsing
|
|
79
79
|
|
|
80
|
-
`query_selectors`
|
|
80
|
+
`query_selectors` prints one indented line per node:
|
|
81
81
|
|
|
82
82
|
```
|
|
83
83
|
uid=1_12 heading "Welcome" level="1"
|
|
@@ -299,42 +299,31 @@ async function main(): Promise<void> {
|
|
|
299
299
|
});
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
-
program
|
|
303
|
-
.command('query_selectors_all')
|
|
304
|
-
.description('Query the accessibility tree with CSS-like selectors')
|
|
305
|
-
.option('-s, --selector <selector>', 'CSS-like selector (repeatable)', (value: string, prev: string[] = []) => {
|
|
306
|
-
prev.push(value);
|
|
307
|
-
return prev;
|
|
308
|
-
})
|
|
309
|
-
.option('--limit <number>', 'Max nodes per selector (0 = unlimited)', '0')
|
|
310
|
-
.option('--with-ancestors', 'Include ancestor nodes', false)
|
|
311
|
-
.option('--no-with-ancestors', 'Exclude ancestor nodes')
|
|
312
|
-
.option('--selectors-json <json>', 'JSON array of {selector,limit,withAncestors} for per-selector control')
|
|
313
|
-
.action(async (opts: {
|
|
314
|
-
selector?: string[];
|
|
315
|
-
limit?: string;
|
|
316
|
-
withAncestors?: boolean;
|
|
317
|
-
selectorsJson?: string;
|
|
318
|
-
}, cmd: Command) => {
|
|
319
|
-
const body = QueryBuilder.buildQuerySelectorsBody(opts);
|
|
320
|
-
await MainHelper.runTool(cmd, 'query_selectors_all', body);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
302
|
program
|
|
324
303
|
.command('query_selectors')
|
|
325
|
-
.description('Query the accessibility tree with CSS-like selectors
|
|
304
|
+
.description('Query the accessibility tree with CSS-like selectors. By default returns the first match per selector; use --all to return every match.')
|
|
326
305
|
.option('-s, --selector <selector>', 'CSS-like selector (repeatable)', (value: string, prev: string[] = []) => {
|
|
327
306
|
prev.push(value);
|
|
328
307
|
return prev;
|
|
329
308
|
})
|
|
330
|
-
.option('--
|
|
331
|
-
.option('--
|
|
332
|
-
.option('--
|
|
309
|
+
.option('-a, --all', 'Return all matches per selector (default: first match only)', false)
|
|
310
|
+
.option('--limit <number>', 'Max nodes per selector when --all is set (0 = unlimited)', '0')
|
|
311
|
+
.option('--wa, --with-ancestors', 'Include ancestor nodes', false)
|
|
312
|
+
.option('--wc, --with-children', 'Include descendant nodes (subtree) of each matched node', false)
|
|
313
|
+
.option('--selectors-json <json>', 'JSON array of {selector,limit?,withAncestors,withChildren} for per-selector control')
|
|
333
314
|
.action(async (opts: {
|
|
334
315
|
selector?: string[];
|
|
316
|
+
all?: boolean;
|
|
317
|
+
limit?: string;
|
|
335
318
|
withAncestors?: boolean;
|
|
319
|
+
withChildren?: boolean;
|
|
336
320
|
selectorsJson?: string;
|
|
337
321
|
}, cmd: Command) => {
|
|
322
|
+
if (opts.all === true) {
|
|
323
|
+
const body = QueryBuilder.buildQuerySelectorsBody(opts);
|
|
324
|
+
await MainHelper.runTool(cmd, 'query_selectors_all', body);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
338
327
|
const body = QueryBuilder.buildQuerySelectorFirstBody(opts);
|
|
339
328
|
await MainHelper.runTool(cmd, 'query_selectors', body);
|
|
340
329
|
});
|
|
@@ -16,12 +16,14 @@ export type BuildQuerySelectorsAllOpts = {
|
|
|
16
16
|
selector?: string[];
|
|
17
17
|
limit?: string;
|
|
18
18
|
withAncestors?: boolean;
|
|
19
|
+
withChildren?: boolean;
|
|
19
20
|
selectorsJson?: string;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export type BuildQuerySelectorFirstOpts = {
|
|
23
24
|
selector?: string[];
|
|
24
25
|
withAncestors?: boolean;
|
|
26
|
+
withChildren?: boolean;
|
|
25
27
|
selectorsJson?: string;
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -50,11 +52,13 @@ export class QueryBuilder {
|
|
|
50
52
|
throw new Error(`Invalid --limit: ${opts.limit}`);
|
|
51
53
|
}
|
|
52
54
|
const withAncestors = opts.withAncestors !== false;
|
|
55
|
+
const withChildren = opts.withChildren === true;
|
|
53
56
|
|
|
54
57
|
const selectors: QuerySelectorInput[] = selectorList.map((selector) => ({
|
|
55
58
|
selector,
|
|
56
59
|
limit,
|
|
57
60
|
withAncestors,
|
|
61
|
+
withChildren,
|
|
58
62
|
}));
|
|
59
63
|
return { selectors };
|
|
60
64
|
}
|
|
@@ -79,10 +83,12 @@ export class QueryBuilder {
|
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
const withAncestors = opts.withAncestors !== false;
|
|
86
|
+
const withChildren = opts.withChildren === true;
|
|
82
87
|
|
|
83
88
|
const selectors: QuerySelectorFirstInput[] = selectorList.map((selector) => ({
|
|
84
89
|
selector,
|
|
85
90
|
withAncestors,
|
|
91
|
+
withChildren,
|
|
86
92
|
}));
|
|
87
93
|
return { selectors };
|
|
88
94
|
}
|