@trawlme/cli 3.7.2 → 3.7.4
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 +2 -1
- package/dist/commands/scraps.js +66 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ trawl ping [--json] Health/version handshake against
|
|
|
79
79
|
### Scrap management
|
|
80
80
|
|
|
81
81
|
```
|
|
82
|
-
trawl scraps create -t <title>
|
|
82
|
+
trawl scraps create -t <title> -u <url> [-r <request>] [-d <description>] [--tier <tier0|tier1|tier2|tier3|tier4>] [--json]
|
|
83
83
|
trawl scraps update <id> [-t <title>] [-u <url>] [-r <request>] [-d <description>] [--cron <expr>|--no-cron] [--alert <email>|--no-alert] [--autofix|--no-autofix] [-p <json>|--params-file <path>] [--tier <tier0|tier1|tier2|tier3|tier4>] [--force-tier <tier0|tier1|tier2|tier3|tier4>] [--json]
|
|
84
84
|
trawl scraps watch <id> [--json]
|
|
85
85
|
trawl scraps doctor <id> [--json] [--autofix]
|
|
@@ -89,6 +89,7 @@ trawl scraps banner <id> -f <file> [--json]
|
|
|
89
89
|
trawl scraps delete <id> [--force] [--json] Alias: rm
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
- `-u/--url` is **the site the scrap targets**, not necessarily the exact URL fetched at run time — a scrap whose real target is computed inside its `-r/--request` script (a templated search query, an interpolated item id, e.g. `https://www.ebay.com/sch/i.html?_nkw=${query}`) should still supply a base/site URL. Every domain-level policy (proxy tier ceiling, provider routing, per-domain memoize) keys off this field, so a blank value silently disables all of it. `create` requires it — validated client-side (must parse as an `http`/`https` URL) so a missing or malformed value fails fast with a readable usage error (exit 2) instead of a raw server 400/422; `update` keeps it optional (omitting it is still a no-op, matching the API) but a value that **is** passed is validated the same way.
|
|
92
93
|
- `--tier` forces a proxy tier; `--force-tier` raises the proxy-tier ceiling past the auto-cap (history-gated: may be refused or cost more). `create --json`/`update --json` print the full scrap object (including the `_tierOverride` outcome) on stdout; a refused tier override exits 1 with a standard `--json` error envelope (`kind:"refused"` — distinct from `"unknown"`, so a script can branch on "the server said no"). When a tier was requested but the server's response carries no `_tierOverride` at all (an older server that can't confirm what actually got applied), a stderr warning is printed either way, and under `--json` the emitted object also carries `"_tierUnconfirmed": true` — the machine-readable counterpart to that warning, since a `--json` caller has no reliable reason to read stderr.
|
|
93
94
|
- `scraps doctor` diagnoses the last run (error, failed selector, block status, page state, autofix outcome); `--autofix` includes the full autofix diff/dry-run/knowledge. A run that is still in flight (`status: null`, server-side) shows a `running` badge, never `failed`; a run whose item count regressed vs baseline (`statusDetail: "regression"`) shows its own amber `regression` badge, never `failed` either.
|
|
94
95
|
- `scraps autofix` shows the last auto-fix attempt on its own (decision, diff, dry-run, knowledge). `--json` on a scrap that has **never run** returns `{"status":"no_runs"}` (exit 0) — distinct from `null`, which means a run exists but had no auto-fix attempt.
|
package/dist/commands/scraps.js
CHANGED
|
@@ -5,7 +5,7 @@ import { api, LONG_RUN_TIMEOUT_MS } from '../lib/api.js';
|
|
|
5
5
|
import { table, json, formatDate } from '../lib/format.js';
|
|
6
6
|
import { parseServerJson } from '../lib/json.js';
|
|
7
7
|
import { promptPassword } from '../lib/prompt.js';
|
|
8
|
-
import { validateObjectId } from '../lib/validate.js';
|
|
8
|
+
import { validateObjectId, requireUrl } from '../lib/validate.js';
|
|
9
9
|
import { classifyError, reportError, UsageError, RefusalError } from '../lib/errors.js';
|
|
10
10
|
import { confirmDestructive, isInteractive } from '../lib/confirm.js';
|
|
11
11
|
import { formatDoctor, formatAutofix, fetchRunAndFix, pickRun, pickFix } from './doctor.js';
|
|
@@ -22,6 +22,31 @@ import { maybeShowReferralTip } from '../lib/tips.js';
|
|
|
22
22
|
function usageError(message, opts = {}) {
|
|
23
23
|
process.exitCode = reportError(new UsageError(message), { json: opts.json });
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* #163 — client-side URL validation shared by `create`/`update`, mirroring
|
|
27
|
+
* the server's now-required-and-parseable `url` field (trawl_node#1823,
|
|
28
|
+
* PR #1828: `z.string().trim().default('')` -> `z.string().trim().url()`).
|
|
29
|
+
* Gives a readable message BEFORE the request goes out instead of a raw
|
|
30
|
+
* 400/422 from the server. Reuses the exact same `requireUrl` helper
|
|
31
|
+
* `trawl create`/`trawl login --url` already validate against (lib/validate.js),
|
|
32
|
+
* just routed through this file's own return-style `usageError()` convention
|
|
33
|
+
* (never throw — every other validation in this file, tier/limit/page/params/
|
|
34
|
+
* status, follows the same pattern) instead of the throw-style those two
|
|
35
|
+
* commands use. Returns `undefined` (never throws) on a bad value — the
|
|
36
|
+
* caller must check for that and `return` without calling the API.
|
|
37
|
+
*/
|
|
38
|
+
function tryValidateUrl(value, opts) {
|
|
39
|
+
try {
|
|
40
|
+
return requireUrl(value, '--url');
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof UsageError) {
|
|
44
|
+
usageError(err.message, { json: opts.json });
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
25
50
|
function lastStatus(scrap) {
|
|
26
51
|
const last = scrap.history?.[0];
|
|
27
52
|
if (!last)
|
|
@@ -565,7 +590,7 @@ scraps
|
|
|
565
590
|
.command('create')
|
|
566
591
|
.description('Create a new scrap')
|
|
567
592
|
.requiredOption('-t, --title <title>', 'Scrap title')
|
|
568
|
-
.option('-u, --url <url>', 'Target URL')
|
|
593
|
+
.option('-u, --url <url>', 'Target URL — the site the scrap targets, not necessarily the exact URL fetched at run time (a request script can compute that, e.g. a templated search query). Required.')
|
|
569
594
|
.option('-r, --request <request>', 'Request/query')
|
|
570
595
|
.option('-d, --description <text>', 'Scrap description')
|
|
571
596
|
.option('--tier <tier>', `Force proxy tier (${VALID_TIERS.join('|')})`)
|
|
@@ -575,9 +600,16 @@ scraps
|
|
|
575
600
|
usageError(`Invalid --tier "${opts.tier}" (allowed: ${VALID_TIERS.join(', ')})`, { json: opts.json });
|
|
576
601
|
return;
|
|
577
602
|
}
|
|
603
|
+
// #163 — the API now requires a parseable `url` on create (trawl_node
|
|
604
|
+
// #1823/#1828); validate client-side BEFORE the request goes out so a
|
|
605
|
+
// missing/unparseable value surfaces as a readable usage error instead
|
|
606
|
+
// of a raw server 400/422.
|
|
607
|
+
const url = tryValidateUrl(opts.url, opts);
|
|
608
|
+
if (url === undefined)
|
|
609
|
+
return;
|
|
578
610
|
const data = await spin(() => api.post('/api/scraps', {
|
|
579
611
|
title: opts.title,
|
|
580
|
-
|
|
612
|
+
url,
|
|
581
613
|
request: opts.request || '',
|
|
582
614
|
...(opts.description !== undefined && { description: opts.description }),
|
|
583
615
|
...(opts.tier !== undefined && { proxyTier: opts.tier }),
|
|
@@ -606,7 +638,7 @@ scraps
|
|
|
606
638
|
.command('update <id>')
|
|
607
639
|
.description('Update an existing scrap')
|
|
608
640
|
.option('-t, --title <title>', 'New title')
|
|
609
|
-
.option('-u, --url <url>', 'New target URL')
|
|
641
|
+
.option('-u, --url <url>', 'New target URL — the site the scrap targets. Optional: omitting it leaves the current value unchanged (a no-op, matching the API); when passed it must still be a parseable URL.')
|
|
610
642
|
.option('-r, --request <request>', 'New request')
|
|
611
643
|
.option('-d, --description <text>', 'New description')
|
|
612
644
|
.option('--cron <expression>', 'Cron expression (empty string to disable)')
|
|
@@ -633,8 +665,15 @@ scraps
|
|
|
633
665
|
const body = {};
|
|
634
666
|
if (opts.title !== undefined)
|
|
635
667
|
body.title = opts.title;
|
|
636
|
-
|
|
637
|
-
|
|
668
|
+
// #163 — omitting --url stays a no-op (the API keeps that allowed on
|
|
669
|
+
// update), but a value that IS supplied must still be a parseable URL —
|
|
670
|
+
// validate it client-side before the request goes out, same as create.
|
|
671
|
+
if (opts.url !== undefined) {
|
|
672
|
+
const url = tryValidateUrl(opts.url, opts);
|
|
673
|
+
if (url === undefined)
|
|
674
|
+
return;
|
|
675
|
+
body.url = url;
|
|
676
|
+
}
|
|
638
677
|
if (opts.request !== undefined)
|
|
639
678
|
body.request = opts.request;
|
|
640
679
|
if (opts.description !== undefined)
|
|
@@ -1167,8 +1206,28 @@ export function attachTriggerCommand(parent, attachOpts = {}) {
|
|
|
1167
1206
|
text: opts.wait ? 'Running worker…' : 'Triggering worker…',
|
|
1168
1207
|
successText: opts.wait ? 'Worker run complete' : 'Worker triggered',
|
|
1169
1208
|
});
|
|
1170
|
-
if (opts.json)
|
|
1209
|
+
if (opts.json) {
|
|
1171
1210
|
json(data);
|
|
1211
|
+
}
|
|
1212
|
+
else if (!process.stdout.isTTY) {
|
|
1213
|
+
// #160 — spin() (lib/spinner.ts, #119) intentionally emits NOTHING
|
|
1214
|
+
// when stderr isn't a TTY, so a piped/redirected `trigger` wrote zero
|
|
1215
|
+
// bytes to both stdout AND stderr while exiting 0: a caller driving
|
|
1216
|
+
// this from a script has no way to tell success from a no-op. Gated
|
|
1217
|
+
// on STDOUT specifically — that's the stream a script actually reads
|
|
1218
|
+
// (`$(trawl trigger …)`, `> out.txt`, `| jq`), independent of
|
|
1219
|
+
// whatever spin() decides about stderr — so an interactive TTY
|
|
1220
|
+
// session, where the ora spinner already confirmed this on stderr,
|
|
1221
|
+
// never sees a duplicate line here. Keeps the exact "Worker
|
|
1222
|
+
// triggered"/"Worker run complete" substrings the trawl-internal QA
|
|
1223
|
+
// runbook asserts on, plus the scrap id so a script has something to
|
|
1224
|
+
// parse. Known residual: stdout attached to a real terminal while
|
|
1225
|
+
// stderr is separately redirected (rare) still prints nothing on
|
|
1226
|
+
// either stream, same as pre-#160 — not the reported/common shape
|
|
1227
|
+
// (full redirection or stdout-only capture), left alone to keep this
|
|
1228
|
+
// fix narrowly scoped to the stream it actually touches.
|
|
1229
|
+
console.log(opts.wait ? `Worker run complete for scrap ${id}` : `Worker triggered for scrap ${id}`);
|
|
1230
|
+
}
|
|
1172
1231
|
// #107 — see the matching comment on `run`'s --watch call above (review
|
|
1173
1232
|
// F1): honest final NDJSON line + exit code under --json, human mode
|
|
1174
1233
|
// gets the same honest exit code too.
|