@nexusbloom/cli 0.9.7 → 0.9.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusbloom/cli",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/pipe.js CHANGED
@@ -295,9 +295,23 @@ function splitSteps(text) {
295
295
  return chunks;
296
296
  }
297
297
 
298
+ /**
299
+ * Is this pipe the start of a transform chain rather than the next step?
300
+ *
301
+ * The word after the pipe has to be a *whole* transform name, optionally
302
+ * followed by an argument list. Without the terminator check, a tool whose slug
303
+ * merely starts with a transform name — `base64-tool`, `split-words` — was read
304
+ * as the transform `base64` plus a stray slug, and the pipeline failed to parse
305
+ * with a confusing "Expected key=value" pointing at the pipe.
306
+ */
298
307
  function isKnownTransformWord(rest) {
299
- const word = (rest.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)/) || [])[1];
300
- return word !== undefined && Object.hasOwn(TRANSFORMS, word);
308
+ const m = /^\s*([a-zA-Z_][a-zA-Z0-9_]*)/.exec(rest);
309
+ if (!m) return false;
310
+ if (!Object.hasOwn(TRANSFORMS, m[1])) return false;
311
+ // Whatever follows the name decides it: `len()`, `len` at the end of a step,
312
+ // then whitespace, or a pipe. A hyphen or dot means this is a slug.
313
+ const after = rest.slice(m.index + m[1].length);
314
+ return after === "" || /^\s*[(\s|]/.test(after);
301
315
  }
302
316
 
303
317
  /**
package/src/values.js CHANGED
@@ -274,8 +274,22 @@ export const TRANSFORMS = {
274
274
  const nums = v.filter((n) => typeof n === "number");
275
275
  return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : MISSING;
276
276
  },
277
- min: (v) => (Array.isArray(v) && v.length ? Math.min(...v.filter((n) => typeof n === "number")) : v),
278
- max: (v) => (Array.isArray(v) && v.length ? Math.max(...v.filter((n) => typeof n === "number")) : v),
277
+ // These collapse an *array*; they do not compare against a bound. `clamp(lo,hi)`
278
+ // is the one that takes bounds. Ignoring a second argument silently was the
279
+ // reason `min(120, 60)` returned 120 instead of erroring, so it now returns the
280
+ // value untouched rather than pretending to have clamped anything.
281
+ min: (v, ...ignored) =>
282
+ ignored.length
283
+ ? v
284
+ : Array.isArray(v) && v.length
285
+ ? Math.min(...v.filter((n) => typeof n === "number"))
286
+ : v,
287
+ max: (v, ...ignored) =>
288
+ ignored.length
289
+ ? v
290
+ : Array.isArray(v) && v.length
291
+ ? Math.max(...v.filter((n) => typeof n === "number"))
292
+ : v,
279
293
  // Sum one field across an array of objects — the common "total of the column".
280
294
  sumBy: (v, key) => {
281
295
  if (!Array.isArray(v)) return v;
@@ -319,6 +333,11 @@ export const TRANSFORMS = {
319
333
  (Array.isArray(v) && v.length === 0) ||
320
334
  (typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0),
321
335
 
336
+ // Builds one string out of several values. Without this a report can only ever
337
+ // be as long as whatever single tool produced it: `join` needs an array that
338
+ // already exists, and there is no way to place an upstream value inside text.
339
+ concat: (v, ...parts) => [v, ...parts].map((p) => (p === MISSING || p === undefined || p === null ? "" : String(p))).join(""),
340
+
322
341
  // — branching —
323
342
  //
324
343
  // `then(cond, whenTrue, whenFalse)` is what a linear pipeline was missing: pick
@@ -471,6 +490,11 @@ export const TRANSFORM_ARITY = {
471
490
  // understate it, so the builder asks for one and a written pipeline may list
472
491
  // as many as it likes.
473
492
  coalesce: 2,
493
+ // Arity is the *minimum* argument count the parser enforces, so a variadic
494
+ // transform declares 1 — `concat()` with nothing after it is legal and just
495
+ // stringifies the piped value. `coalesce` above keeps its 2 because a coalesce
496
+ // with no fallback does nothing.
497
+ concat: 1,
474
498
  pluck: 2, chunk: 2, without: 2, where: 4,
475
499
  truncate: 3, repeat: 2, mask: 2,
476
500
  };
@@ -535,8 +559,8 @@ export const TRANSFORM_DOCS = {
535
559
  clamp: "keep a number between a low and a high",
536
560
  sum: "add up every number in an array",
537
561
  avg: "average of every number in an array",
538
- min: "smallest number in an array",
539
- max: "largest number in an array",
562
+ min: "smallest number in an array (to bound a single number use clamp)",
563
+ max: "largest number in an array (to bound a single number use clamp)",
540
564
  sumBy: "add up one field across an array of objects",
541
565
  eq: "is it equal to a value",
542
566
  ne: "is it different from a value",
@@ -551,6 +575,7 @@ export const TRANSFORM_DOCS = {
551
575
  matches: "does a string match a regular expression",
552
576
  isEmpty: "is the value empty — blank, [], or {}",
553
577
  then: "choose between two values on a condition",
578
+ concat: "combine several values into one string",
554
579
  coalesce: "first value that is not empty",
555
580
  pluck: "pull one field out of every object in an array",
556
581
  unique: "remove duplicates from an array",
@@ -607,6 +632,8 @@ export const TRANSFORM_GROUPS = [
607
632
  names: ["json", "unjson"] },
608
633
  { label: "default(v) / coalesce(a,b,…) / then(cond,a,b)", doc: "supply a value when one is missing, or choose on a condition",
609
634
  names: ["default", "coalesce", "then"] },
635
+ { label: "concat(a,b,…)", doc: "combine several values into one string",
636
+ names: ["concat"] },
610
637
  { label: "eq / ne / gt / gte / lt / lte / between(a,b)", doc: "compare — the result is true or false",
611
638
  names: ["eq", "ne", "gt", "gte", "lt", "lte", "between"] },
612
639
  { label: "includes / startsWith / endsWith / matches(re) / isEmpty", doc: "test text and arrays",