@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 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> [-u <url>] [-r <request>] [-d <description>] [--tier <tier0|tier1|tier2|tier3|tier4>] [--json]
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.
@@ -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
- ...(opts.url && { url: opts.url }),
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
- if (opts.url !== undefined)
637
- body.url = opts.url;
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "3.7.2",
3
+ "version": "3.7.4",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {