@trawlme/cli 3.8.0 → 3.8.1

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.
@@ -47,6 +47,58 @@ function tryValidateUrl(value, opts) {
47
47
  throw err;
48
48
  }
49
49
  }
50
+ /**
51
+ * One-line rendering of an emptyContext blob for the run-info table. Two
52
+ * buckets are actionable and must stay distinct: selectors that matched zero
53
+ * nodes (the page changed) and selectors the browser rejected outright
54
+ * (`-1`, a bug in the scrap script). Folding the second into "all matched"
55
+ * would report a broken selector as healthy. `--json` still emits the full
56
+ * object, `page.topClasses` / `topAnchorPaths` included.
57
+ */
58
+ function summarizeEmptyContext(ctx) {
59
+ if (!ctx || typeof ctx !== 'object')
60
+ return null;
61
+ const parts = [];
62
+ const selectors = ctx.selectors && typeof ctx.selectors === 'object' ? ctx.selectors : {};
63
+ const names = Object.keys(selectors);
64
+ // Selectors may themselves contain a comma (`div, span`), so quote each one
65
+ // — a raw join renders one grouping selector as if it were two.
66
+ const list = (ns) => `${ns.slice(0, 3).map((s) => `\`${s}\``).join(', ')}${ns.length > 3 ? ` (+${ns.length - 3})` : ''}`;
67
+ // Four disjoint, exhaustive buckets — every selector lands in exactly one, so
68
+ // no count is ever dropped and none is summarised as something it is not.
69
+ // `selectors` comes from a Mongoose Mixed field, so a count may be `null` or
70
+ // a string: that is `unreadable`, which is neither healthy nor zero-match and
71
+ // must be named as its own thing next to the others, not collapsed into them.
72
+ const isCount = (v) => typeof v === 'number' && Number.isFinite(v);
73
+ const zero = names.filter((s) => selectors[s] === 0);
74
+ const invalid = names.filter((s) => selectors[s] === -1);
75
+ const matched = names.filter((s) => isCount(selectors[s]) && selectors[s] > 0);
76
+ const unreadable = names.filter((s) => !zero.includes(s) && !invalid.includes(s) && !matched.includes(s));
77
+ const mixed = zero.length > 0 || invalid.length > 0 || unreadable.length > 0;
78
+ if (zero.length)
79
+ parts.push(`0-match: ${list(zero)}`);
80
+ if (invalid.length)
81
+ parts.push(`invalid: ${list(invalid)}`);
82
+ if (unreadable.length)
83
+ parts.push(`unreadable: ${list(unreadable)}`);
84
+ if (matched.length) {
85
+ // Named even in a mixed set: "3 of the selectors still work" is the
86
+ // difference between a page that moved and a page that vanished.
87
+ parts.push(mixed ? `${matched.length} matched` : `${matched.length} selector(s), all matched`);
88
+ }
89
+ // Same Mixed-field caution on the title: coerce before calling a string
90
+ // method (a numeric title would otherwise crash `run-info` on the very run
91
+ // it exists to explain), and test against null/undefined rather than
92
+ // truthiness so a falsy-but-real title like `0` still shows. Inner double
93
+ // quotes would close the wrapper early and corrupt the line.
94
+ const title = ctx.page?.title;
95
+ if (title !== undefined && title !== null && String(title) !== '') {
96
+ parts.push(`title="${String(title).replace(/"/g, "'")}"`);
97
+ }
98
+ if (typeof ctx.page?.totalAnchors === 'number')
99
+ parts.push(`${ctx.page.totalAnchors} anchors`);
100
+ return parts.length ? parts.join(' | ') : null;
101
+ }
50
102
  function lastStatus(scrap) {
51
103
  const last = scrap.history?.[0];
52
104
  if (!last)
@@ -1118,14 +1170,15 @@ export function attachRunInfoCommand(parent, attachOpts = {}) {
1118
1170
  blockType: h.blockType ?? null,
1119
1171
  errorMessage: h.errorSnapshot?.errorMessage ?? null,
1120
1172
  selector: h.errorSnapshot?.selector ?? null,
1121
- emptyContext: h.errorSnapshot?.emptyContext ?? null,
1173
+ emptyContext: h.emptyContext ?? null,
1122
1174
  createdAt: h.createdAt ?? null,
1123
1175
  };
1124
1176
  if (opts.json) {
1125
1177
  json(info);
1126
1178
  return;
1127
1179
  }
1128
- table([info], ['hid', 'status', 'time', 'tier', 'failureKind', 'blockType', 'errorMessage', 'selector', 'emptyContext', 'createdAt']);
1180
+ // The blob is an object; the table needs one line. --json keeps it whole.
1181
+ table([{ ...info, emptyContext: summarizeEmptyContext(info.emptyContext) }], ['hid', 'status', 'time', 'tier', 'failureKind', 'blockType', 'errorMessage', 'selector', 'emptyContext', 'createdAt']);
1129
1182
  });
1130
1183
  }
1131
1184
  attachRunInfoCommand(scraps, { hidden: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "3.8.0",
3
+ "version": "3.8.1",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {