@opencraw/cli 0.1.8 → 0.1.10
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 +14 -9
- package/dist/index.esm.js +20 -7
- package/dist/src/index.d.ts +1 -1
- package/dist/src/probe/index.d.ts +1 -1
- package/dist/src/probe/probe-page.use-case.d.ts +13 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/russoedu/open.craw/main/docs/assets/opencraw-logo.svg" alt="OpenCraw" width="360">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# @opencraw/cli
|
|
2
6
|
|
|
3
|
-
Command-line tools for [`@opencraw/core`](
|
|
7
|
+
Command-line tools for [`@opencraw/core`](https://github.com/russoedu/open.craw/blob/main/packages/core): probe a page for scrapable data, validate recipes,
|
|
4
8
|
and run a crawl from the terminal.
|
|
5
9
|
|
|
6
10
|
## Install
|
|
7
11
|
|
|
8
12
|
```sh
|
|
9
13
|
npm install -g @opencraw/cli
|
|
10
|
-
# or, inside this workspace: node packages/cli/bin/opencraw.mjs <command>
|
|
14
|
+
# or, inside this workspace, after `npm run cli:build`: node packages/cli/bin/opencraw.mjs <command>
|
|
11
15
|
```
|
|
12
16
|
|
|
13
17
|
## Commands
|
|
@@ -16,6 +20,7 @@ npm install -g @opencraw/cli
|
|
|
16
20
|
opencraw validate <recipe files or directories...>
|
|
17
21
|
opencraw run <recipe files or directories...> [options]
|
|
18
22
|
opencraw probe <url> [options]
|
|
23
|
+
opencraw diff <previous.jsonl> <current.jsonl> [--key a,b] [--ignore a,b] [--changes <file>]
|
|
19
24
|
```
|
|
20
25
|
|
|
21
26
|
### `validate`
|
|
@@ -43,11 +48,11 @@ input recipes) and reports what happened.
|
|
|
43
48
|
| `--dry-run` | One record per input recipe, printed with the scope it was mapped from — for checking a recipe under construction without a full run. |
|
|
44
49
|
| `--trace` | Print the crawl trace to stderr. |
|
|
45
50
|
| `--headed` | Show the browser instead of running headless. |
|
|
46
|
-
| `--parallel <n>` | Run this many input recipes at once (default 1). Iterations inside a recipe follow its `limits.concurrency`. See [§3.9 of the authoring guide](
|
|
47
|
-
| `--retries <n>` | Tries per request that fails in passing (a dropped connection, a timeout, a 429, a 5xx), for recipes whose `limits.retry` says nothing. Default 3; `1` turns retrying off. See [§7 of the authoring guide](
|
|
48
|
-
| `--profiles <dir>` | Where the persistent browser profiles of `session.browserProfile` live (or `OPENCRAW_PROFILES`; default `.opencraw/profiles`). See [access.md](
|
|
49
|
-
| `--host-delay <ms>`, `--host-concurrency <n>` | Per-site politeness across every recipe: at least `ms` between two requests to one site, at most `n` in flight. They override the access config's `throttle` defaults. See [access.md](
|
|
50
|
-
| `--plugins <file>` (or `--hooks`) | A plugins module: named exports `hooks` (the recipes' hook steps and transforms), `accessPlugins` (for `{ kind: "plugin" }` access profiles) and `captchaSolvers`. A module whose default export is `{ name: function }` is read as hooks alone. `OPENCRAW_PLUGINS` / `OPENCRAW_HOOKS` when not given; `probe` reads it too. It runs as your code; load only files you trust. See [§6 of the authoring guide](
|
|
51
|
+
| `--parallel <n>` | Run this many input recipes at once (default 1). Iterations inside a recipe follow its `limits.concurrency`. See [§3.9 of the authoring guide](https://github.com/russoedu/open.craw/blob/main/docs/recipes/authoring.md#39-concurrency). |
|
|
52
|
+
| `--retries <n>` | Tries per request that fails in passing (a dropped connection, a timeout, a 429, a 5xx), for recipes whose `limits.retry` says nothing. Default 3; `1` turns retrying off. See [§7 of the authoring guide](https://github.com/russoedu/open.craw/blob/main/docs/recipes/authoring.md#7-policies-what-happens-when-something-is-missing-or-fails). |
|
|
53
|
+
| `--profiles <dir>` | Where the persistent browser profiles of `session.browserProfile` live (or `OPENCRAW_PROFILES`; default `.opencraw/profiles`). See [access.md](https://github.com/russoedu/open.craw/blob/main/docs/recipes/access.md#persistent-browser-profiles). |
|
|
54
|
+
| `--host-delay <ms>`, `--host-concurrency <n>` | Per-site politeness across every recipe: at least `ms` between two requests to one site, at most `n` in flight. They override the access config's `throttle` defaults. See [access.md](https://github.com/russoedu/open.craw/blob/main/docs/recipes/access.md#throttling-per-site). |
|
|
55
|
+
| `--plugins <file>` (or `--hooks`) | A plugins module: named exports `hooks` (the recipes' hook steps and transforms), `accessPlugins` (for `{ kind: "plugin" }` access profiles) and `captchaSolvers`. A module whose default export is `{ name: function }` is read as hooks alone. `OPENCRAW_PLUGINS` / `OPENCRAW_HOOKS` when not given; `probe` reads it too. It runs as your code; load only files you trust. See [§6 of the authoring guide](https://github.com/russoedu/open.craw/blob/main/docs/recipes/authoring.md#6-hooks). |
|
|
51
56
|
|
|
52
57
|
```sh
|
|
53
58
|
opencraw run recipes/ --out out/products.jsonl --trace
|
|
@@ -113,7 +118,7 @@ opencraw probe ./decks/incentivi.pptx
|
|
|
113
118
|
```
|
|
114
119
|
|
|
115
120
|
Use it before writing an input recipe, to find the shape a site's data actually takes (§9 of
|
|
116
|
-
[the authoring guide](
|
|
121
|
+
[the authoring guide](https://github.com/russoedu/open.craw/blob/main/docs/recipes/authoring.md)).
|
|
117
122
|
|
|
118
123
|
### Options common to `run` and `probe`
|
|
119
124
|
|
|
@@ -121,7 +126,7 @@ Use it before writing an input recipe, to find the shape a site's data actually
|
|
|
121
126
|
|---|---|
|
|
122
127
|
| `--browser-path <path>` | A browser binary other than the one Playwright installed (or `OPENCRAW_CHROMIUM`). |
|
|
123
128
|
| `--insecure-tls` | Accept an intercepting proxy's certificate (or `OPENCRAW_INSECURE_TLS=1`). |
|
|
124
|
-
| `--access <file>` | An access config: proxy profiles, with credentials as `{{env.NAME}}` (or `OPENCRAW_ACCESS`). See [access.md](
|
|
129
|
+
| `--access <file>` | An access config: proxy profiles, with credentials as `{{env.NAME}}` (or `OPENCRAW_ACCESS`). See [access.md](https://github.com/russoedu/open.craw/blob/main/docs/recipes/access.md). |
|
|
125
130
|
| `--access-profile <name>` | The profile used by recipes that name none, overriding the config's `default`. `probe` uses it for its fetch. |
|
|
126
131
|
| `--user-agent <ua>` | The user agent to send. |
|
|
127
132
|
|
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { writeFile } from 'node:fs/promises';
|
|
3
|
-
import { readableKey, readRecordsFile, diffRecords, loadAccessConfig, htmlTableSheets, AccessBroker, HttpClient, parseXml, BrowserClient, readRecipeSource, parseOutputRecipe, parseInputRecipe, bindRecipeSet, memorySink, jsonLinesSink, createCrawler, RecipeSet, variantLabel, diffOptionsFor, RecipeValidationError, RecipeBindingError, traceLine } from '@opencraw/core';
|
|
3
|
+
import { readableKey, readRecordsFile, diffRecords, loadAccessConfig, htmlTableSheets, HostAllowlist, AccessBroker, HttpClient, parseXml, BrowserClient, readRecipeSource, parseOutputRecipe, parseInputRecipe, bindRecipeSet, memorySink, jsonLinesSink, createCrawler, RecipeSet, variantLabel, diffOptionsFor, RecipeValidationError, RecipeBindingError, traceLine } from '@opencraw/core';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
@@ -1046,8 +1046,10 @@ const OBSERVE_MS = 4000;
|
|
|
1046
1046
|
* @throws Error when the fetch itself fails.
|
|
1047
1047
|
*/
|
|
1048
1048
|
async function probeUrl(url, options) {
|
|
1049
|
-
const
|
|
1050
|
-
const
|
|
1049
|
+
const allowedHosts = HostAllowlist.of(options.allowedHosts);
|
|
1050
|
+
const plugins = options.accessPlugins === undefined && options.plugins !== undefined ? await loadPlugins(options.plugins) : undefined;
|
|
1051
|
+
const access = options.accessConfig === undefined ? await resolveAccess(options) : withDefault(options.accessConfig, options.accessProfile);
|
|
1052
|
+
const lease = await new AccessBroker(access, options.accessPlugins ?? plugins?.accessPlugins).lease({
|
|
1051
1053
|
recipeId: 'probe'
|
|
1052
1054
|
});
|
|
1053
1055
|
if (lease.cdp !== undefined) throw new Error(`access profile "${lease.profile}" is a remote browser; probe fetches over HTTP and needs a proxy profile`);
|
|
@@ -1055,7 +1057,8 @@ async function probeUrl(url, options) {
|
|
|
1055
1057
|
userAgent: options.userAgent ?? BROWSER_USER_AGENT,
|
|
1056
1058
|
ignoreHTTPSErrors: options.insecureTls || lease.ignoreHTTPSErrors === true,
|
|
1057
1059
|
proxy: lease.proxy,
|
|
1058
|
-
headers: lease.headers
|
|
1060
|
+
headers: lease.headers,
|
|
1061
|
+
allowedHosts
|
|
1059
1062
|
});
|
|
1060
1063
|
try {
|
|
1061
1064
|
const target = /^[a-z][\w+.-]+:/i.test(url) ? url : pathToFileURL(resolve(url)).href;
|
|
@@ -1106,7 +1109,7 @@ async function probeUrl(url, options) {
|
|
|
1106
1109
|
xml: describeXml(parseXml(body.xml, response.url))
|
|
1107
1110
|
};
|
|
1108
1111
|
const text = body.kind === 'html' ? body.html : body.text;
|
|
1109
|
-
const observed = options.browser && !target.startsWith('file:') ? await observeBrowserJson(url, options, lease) : [];
|
|
1112
|
+
const observed = options.browser && !target.startsWith('file:') ? await observeBrowserJson(url, options, lease, allowedHosts) : [];
|
|
1110
1113
|
const html = body.kind === 'html' ? {
|
|
1111
1114
|
html: describeHtml(body.html, response.format === 'markdown' || response.format === 'docx')
|
|
1112
1115
|
} : {};
|
|
@@ -1139,7 +1142,7 @@ async function probePage(url, options, terminal) {
|
|
|
1139
1142
|
return 1;
|
|
1140
1143
|
}
|
|
1141
1144
|
}
|
|
1142
|
-
async function observeBrowserJson(url, options, lease) {
|
|
1145
|
+
async function observeBrowserJson(url, options, lease, allowedHosts) {
|
|
1143
1146
|
const browser = await BrowserClient.launch({
|
|
1144
1147
|
executablePath: options.browserPath,
|
|
1145
1148
|
ignoreHTTPSErrors: options.insecureTls
|
|
@@ -1149,7 +1152,8 @@ async function observeBrowserJson(url, options, lease) {
|
|
|
1149
1152
|
userAgent: options.userAgent,
|
|
1150
1153
|
proxy: lease.proxy,
|
|
1151
1154
|
headers: lease.headers,
|
|
1152
|
-
ignoreHTTPSErrors: lease.ignoreHTTPSErrors
|
|
1155
|
+
ignoreHTTPSErrors: lease.ignoreHTTPSErrors,
|
|
1156
|
+
allowedHosts
|
|
1153
1157
|
});
|
|
1154
1158
|
const seen = [];
|
|
1155
1159
|
session.page.on('response', response => {
|
|
@@ -1169,6 +1173,15 @@ async function observeBrowserJson(url, options, lease) {
|
|
|
1169
1173
|
await browser.close();
|
|
1170
1174
|
}
|
|
1171
1175
|
}
|
|
1176
|
+
/** An access config with its default profile overridden, when one is named. */
|
|
1177
|
+
function withDefault(config, profile) {
|
|
1178
|
+
if (profile === undefined) return config;
|
|
1179
|
+
if (!Object.hasOwn(config.profiles, profile)) throw new Error(`access profile "${profile}" is not in the access config (profiles: ${Object.keys(config.profiles).join(', ') || 'none'})`);
|
|
1180
|
+
return {
|
|
1181
|
+
...config,
|
|
1182
|
+
default: profile
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1172
1185
|
function reportOf(result) {
|
|
1173
1186
|
if (result.pdf !== undefined) return pdfReport(result.url, result.pdf);
|
|
1174
1187
|
if (result.workbook !== undefined) return workbookReport(result.url, result.workbook);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { Command } from './arguments/index.js';
|
|
|
4
4
|
export { processTerminal } from './terminal/index.js';
|
|
5
5
|
export type { Terminal } from './terminal/index.js';
|
|
6
6
|
export { findData, probeReport, probeUrl } from './probe/index.js';
|
|
7
|
-
export type { ProbeFindings, ProbeResult } from './probe/index.js';
|
|
7
|
+
export type { ProbeFindings, ProbeResult, ProbeOptions } from './probe/index.js';
|
|
8
8
|
export { readRecipeFiles } from './recipe-files/index.js';
|
|
9
9
|
export type { RecipeFiles } from './recipe-files/index.js';
|
|
10
10
|
export { loadForRun } from './run/index.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { probePage, probeUrl } from './probe-page.use-case.js';
|
|
2
|
-
export type { ProbeResult } from './probe-page.use-case.js';
|
|
2
|
+
export type { ProbeResult, ProbeOptions } from './probe-page.use-case.js';
|
|
3
3
|
export { findData } from './find-data.algorithm.js';
|
|
4
4
|
export type { ProbeFindings, JsonLdFinding, InlineJsonFinding } from './find-data.algorithm.js';
|
|
5
5
|
export { probeReport, pdfReport, workbookReport, deckReport, jsonReport, xmlReport } from './probe-report.mapper.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AccessConfig, AccessPlugin } from '@opencraw/core';
|
|
1
2
|
import type { CommonOptions } from '../arguments/index.js';
|
|
2
3
|
import type { Terminal } from '../terminal/index.js';
|
|
3
4
|
import { findData } from './find-data.algorithm.js';
|
|
@@ -27,6 +28,17 @@ export interface ProbeResult {
|
|
|
27
28
|
/** Present for XML: its namespaces, repeated elements and structure. */
|
|
28
29
|
xml?: XmlFindings;
|
|
29
30
|
}
|
|
31
|
+
/** How to probe: the cli's options, plus what a service probing for others sets. */
|
|
32
|
+
export interface ProbeOptions extends CommonOptions {
|
|
33
|
+
/** Also render the page and list the JSON it fetches. */
|
|
34
|
+
browser: boolean;
|
|
35
|
+
/** The only hosts the probe may reach, as `CrawlOptions.allowedHosts`; local paths and `file:` are refused. */
|
|
36
|
+
allowedHosts?: string[];
|
|
37
|
+
/** An access config already loaded, instead of `access` (a file). `accessProfile` still picks the default. */
|
|
38
|
+
accessConfig?: AccessConfig;
|
|
39
|
+
/** Access plugins already loaded, instead of those in `plugins` (a module). */
|
|
40
|
+
accessPlugins?: AccessPlugin[];
|
|
41
|
+
}
|
|
30
42
|
/**
|
|
31
43
|
* Fetches a page and finds where its data lives, through the access profile the
|
|
32
44
|
* options name (direct without one). With `browser: true` it also
|
|
@@ -43,9 +55,7 @@ export interface ProbeResult {
|
|
|
43
55
|
* @returns What was found.
|
|
44
56
|
* @throws Error when the fetch itself fails.
|
|
45
57
|
*/
|
|
46
|
-
export declare function probeUrl(url: string, options:
|
|
47
|
-
browser: boolean;
|
|
48
|
-
} & CommonOptions): Promise<ProbeResult>;
|
|
58
|
+
export declare function probeUrl(url: string, options: ProbeOptions): Promise<ProbeResult>;
|
|
49
59
|
/**
|
|
50
60
|
* Fetches a page and reports where its data lives, to a `Terminal`.
|
|
51
61
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencraw/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.esm.js",
|
|
6
6
|
"module": "./dist/index.esm.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"!**/*.js.map"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@opencraw/core": "^0.1.
|
|
27
|
+
"@opencraw/core": "^0.1.10",
|
|
28
28
|
"playwright": "^1.63.0"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|