@trawlme/cli 1.18.7 → 1.19.0

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.
@@ -7,6 +7,7 @@ import { promptPassword } from '../lib/prompt.js';
7
7
  import { validateObjectId } from '../lib/validate.js';
8
8
  import { classifyError, reportError, UsageError } from '../lib/errors.js';
9
9
  import { formatDoctor, formatAutofix, fetchRunAndFix, pickRun, pickFix } from './doctor.js';
10
+ import { renderPinch, pinchEnabled } from '../lib/pinch.js';
10
11
  /**
11
12
  * Print a usage/validation error consistently: human text to stderr, or a
12
13
  * machine envelope on stdout under --json (never both — reportError is the
@@ -242,6 +243,14 @@ export async function pollRunProgress(id, before, opts = {}) {
242
243
  if (last.status !== null) {
243
244
  const outcome = last.statusDetail ?? (last.status ? 'success' : 'failure');
244
245
  console.log(chalk.dim(`Run finished: ${outcome}`));
246
+ // Pinch celebrates a clean run finish (#94) — mirrors doctor.ts's own
247
+ // `status === true` success definition (regardless of statusDetail),
248
+ // never for a failed/regression run. Neither `run --watch` nor
249
+ // `trigger --watch` (the only two callers) has a --json flag, so this
250
+ // is always safe to print. Guarded by pinchEnabled() (NO_COLOR/non-TTY).
251
+ if (last.status === true && pinchEnabled()) {
252
+ console.log(renderPinch('celebrating'));
253
+ }
245
254
  return;
246
255
  }
247
256
  }
package/dist/index.d.ts CHANGED
@@ -36,6 +36,15 @@ export declare function isEntryPoint(argv1: string | undefined, moduleUrl: strin
36
36
  * subcommand cases)
37
37
  */
38
38
  export declare function isHelpOrVersion(argv: string[]): boolean;
39
+ /**
40
+ * True for a bare `trawl` invocation — no subcommand, no flags at all. This
41
+ * is the CLI's "first thing a new user sees" moment (commander prints its
42
+ * own top-level help right after), distinct from `isHelpOrVersion` which is
43
+ * intentionally broader (also matches `--help`/`--version`/`trawl help`
44
+ * anywhere in argv) — the Pinch wave banner (#94) only wants the narrowest
45
+ * case so it never shows up ahead of e.g. `trawl scraps --help`.
46
+ */
47
+ export declare function isBareInvocation(argv: string[]): boolean;
39
48
  /** Best-effort scan for a `--json` flag in raw argv, used only when parsing
40
49
  * itself failed before any command's own `.opts()` could be resolved (a
41
50
  * commander usage error — unknown option/command, missing required arg). Same
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { token } from './commands/token.js';
11
11
  import { autoUpdateInstalledSkills } from './lib/skills.js';
12
12
  import { initPostHog, captureCommand, shutdown, registerAllowedCommands } from './lib/posthog.js';
13
13
  import { classifyError, reportError } from './lib/errors.js';
14
+ import { renderPinch, pinchEnabled } from './lib/pinch.js';
14
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
16
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
16
17
  /**
@@ -96,6 +97,17 @@ export function isHelpOrVersion(argv) {
96
97
  return true;
97
98
  return false;
98
99
  }
100
+ /**
101
+ * True for a bare `trawl` invocation — no subcommand, no flags at all. This
102
+ * is the CLI's "first thing a new user sees" moment (commander prints its
103
+ * own top-level help right after), distinct from `isHelpOrVersion` which is
104
+ * intentionally broader (also matches `--help`/`--version`/`trawl help`
105
+ * anywhere in argv) — the Pinch wave banner (#94) only wants the narrowest
106
+ * case so it never shows up ahead of e.g. `trawl scraps --help`.
107
+ */
108
+ export function isBareInvocation(argv) {
109
+ return argv.slice(2).length === 0;
110
+ }
99
111
  /** Best-effort scan for a `--json` flag in raw argv, used only when parsing
100
112
  * itself failed before any command's own `.opts()` could be resolved (a
101
113
  * commander usage error — unknown option/command, missing required arg). Same
@@ -171,6 +183,12 @@ export function bestEffortCommandName(argv, allowedNames) {
171
183
  export async function runCli(argv = process.argv) {
172
184
  if (!isHelpOrVersion(argv))
173
185
  autoUpdateInstalledSkills();
186
+ // Pinch wave banner (#94) — bare `trawl` only, guarded so it never shows
187
+ // under NO_COLOR/non-TTY/piped output (pinchEnabled() covers all three).
188
+ if (isBareInvocation(argv) && pinchEnabled()) {
189
+ console.log(renderPinch('wave'));
190
+ console.log();
191
+ }
174
192
  initPostHog();
175
193
  const program = createProgram();
176
194
  // Must run before parseAsync — installs on every node in the tree,
@@ -270,6 +288,12 @@ export async function runCli(argv = process.argv) {
270
288
  // (stderr), never both; `quiet` skips the human line when the raw
271
289
  // stack was already dumped above under --debug.
272
290
  process.exitCode = reportError(err, { json: wantsJson, quiet: isDebug });
291
+ // Pinch confused frame (#94) — never under --json (stdout must stay
292
+ // pure JSON) and guarded by pinchEnabled() (NO_COLOR/non-TTY/piped).
293
+ // Printed to stderr, alongside reportError's own human line above.
294
+ if (!wantsJson && pinchEnabled()) {
295
+ console.error(renderPinch('confused'));
296
+ }
273
297
  }
274
298
  }
275
299
  finally {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Pinch — Trawl's voxel lobster mascot, rendered as 24-bit ANSI half-block
3
+ * art. Distinct from Clawd's 8-bit lane: Pinch is drawn with full 24-bit
4
+ * (`\x1b[38;2;r;g;bm` / `\x1b[48;2;r;g;bm`) color blocks, not a fixed palette.
5
+ *
6
+ * The 12×9 cube-grid (AVGRID proto v11.4, locked 2026-07-16) is packed two
7
+ * grid rows into one terminal row: the upper row's color becomes the
8
+ * half-block's foreground, the lower row's becomes its background, using the
9
+ * upper-half-block glyph '▀' (or '▄' when only the lower half is filled). A
10
+ * 9-row grid therefore renders in 5 terminal rows, 12 columns wide. '.' cells
11
+ * are transparent — no color escape is emitted for that half, so the
12
+ * terminal's own background shows through.
13
+ *
14
+ * See comes-io/trawl_cli#94.
15
+ */
16
+ export type PinchState = 'wave' | 'thinking' | 'celebrating' | 'confused';
17
+ /**
18
+ * Render Pinch as 24-bit ANSI half-block art for the given state, plus a
19
+ * one-line caption. Pure — never touches process.env/stdout; callers must
20
+ * gate on `pinchEnabled()` before printing the result.
21
+ *
22
+ * `frame` only affects 'thinking' (2-frame spinner alternation); every other
23
+ * state ignores it.
24
+ */
25
+ export declare function renderPinch(state: PinchState, frame?: number): string;
26
+ /**
27
+ * True when it's safe to print Pinch art: a real color-capable interactive
28
+ * terminal. False under NO_COLOR (https://no-color.org — presence, not
29
+ * value, disables color output), a non-TTY stdout (piped/redirected output —
30
+ * covers --json/--quiet/CI log capture), or TERM=dumb.
31
+ */
32
+ export declare function pinchEnabled(): boolean;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Pinch — Trawl's voxel lobster mascot, rendered as 24-bit ANSI half-block
3
+ * art. Distinct from Clawd's 8-bit lane: Pinch is drawn with full 24-bit
4
+ * (`\x1b[38;2;r;g;bm` / `\x1b[48;2;r;g;bm`) color blocks, not a fixed palette.
5
+ *
6
+ * The 12×9 cube-grid (AVGRID proto v11.4, locked 2026-07-16) is packed two
7
+ * grid rows into one terminal row: the upper row's color becomes the
8
+ * half-block's foreground, the lower row's becomes its background, using the
9
+ * upper-half-block glyph '▀' (or '▄' when only the lower half is filled). A
10
+ * 9-row grid therefore renders in 5 terminal rows, 12 columns wide. '.' cells
11
+ * are transparent — no color escape is emitted for that half, so the
12
+ * terminal's own background shows through.
13
+ *
14
+ * See comes-io/trawl_cli#94.
15
+ */
16
+ /** Grid-char → RGB. 'C' (cyan) is a "thinking" blip, not present in BASE_GRID. */
17
+ const PALETTE = {
18
+ B: [0x3b, 0x82, 0xf6], // blue — shell
19
+ O: [0xee, 0x90, 0x18], // orange — antennae / claws
20
+ W: [0xff, 0xff, 0xff], // white — eye whites
21
+ K: [0x12, 0x30, 0x33], // navy — pupils / mouth
22
+ P: [0xf9, 0xa8, 0xd4], // pink — claw tips
23
+ C: [0x08, 0x91, 0xb2], // cyan — thinking-spinner antenna blip
24
+ };
25
+ const TRANSPARENT = '.';
26
+ /** Base 12×9 grid — Pinch waving (also the 'wave' state, unmodified). */
27
+ const BASE_GRID = [
28
+ '...O....O...',
29
+ '...O....O...',
30
+ '.BBBBBBBBBB.',
31
+ '.BWWBBBBWWB.',
32
+ '.BWKBBBBKWB.',
33
+ '.PBBBBBBBBP.',
34
+ '.BBBKBBKBBB.',
35
+ '.BBBBKKBBBB.',
36
+ 'OO........OO',
37
+ ];
38
+ const CAPTIONS = {
39
+ wave: 'Pinch says hi.',
40
+ thinking: 'Pinch is thinking…',
41
+ celebrating: 'Pinch is celebrating!',
42
+ confused: 'Pinch looks confused.',
43
+ };
44
+ /** Replace the chars at `indices` in `row` with `ch` — never mutates `row`. */
45
+ function setCells(row, indices, ch) {
46
+ const chars = row.split('');
47
+ for (const i of indices)
48
+ chars[i] = ch;
49
+ return chars.join('');
50
+ }
51
+ /** Derive the per-state grid from BASE_GRID (which is never mutated). */
52
+ function gridForState(state, frame) {
53
+ const rows = [...BASE_GRID];
54
+ switch (state) {
55
+ case 'thinking':
56
+ // Odd frames: both antenna tips (row 0, cols 3 & 8) blip cyan — a
57
+ // 2-frame spinner alternation with no layout shift.
58
+ if (frame % 2 === 1) {
59
+ rows[0] = setCells(rows[0], [3, 8], 'C');
60
+ }
61
+ return rows;
62
+ case 'celebrating':
63
+ // Fists up: the row-8 corner claws move up to row 7's corners; row 8's
64
+ // corners go transparent (arms raised, no longer at the sides).
65
+ rows[7] = setCells(rows[7], [0, 11], 'O');
66
+ rows[8] = setCells(rows[8], [0, 1, 10, 11], TRANSPARENT);
67
+ return rows;
68
+ case 'confused':
69
+ // Pupils removed (row 4, cols 3 & 8) — blank white eyes.
70
+ rows[4] = setCells(rows[4], [3, 8], 'W');
71
+ return rows;
72
+ case 'wave':
73
+ default:
74
+ return rows;
75
+ }
76
+ }
77
+ const RESET = '\x1b[0m';
78
+ const fgCode = ([r, g, b]) => `\x1b[38;2;${r};${g};${b}m`;
79
+ const bgCode = ([r, g, b]) => `\x1b[48;2;${r};${g};${b}m`;
80
+ /**
81
+ * One terminal column: combine an upper (fg-half) + lower (bg-half) grid
82
+ * cell into a single half-block character. Either half may be transparent
83
+ * independently — no bg/fg escape is emitted for a transparent half, so the
84
+ * terminal's own background/foreground shows through it.
85
+ */
86
+ function renderCell(upper, lower) {
87
+ const upperColor = upper === TRANSPARENT ? null : PALETTE[upper];
88
+ const lowerColor = lower === TRANSPARENT ? null : PALETTE[lower];
89
+ if (!upperColor && !lowerColor)
90
+ return ' ';
91
+ // Lower half transparent — draw the upper half-block with fg only, no bg
92
+ // (the glyph's own unpainted lower half already shows the terminal bg).
93
+ if (!lowerColor)
94
+ return `${fgCode(upperColor)}▀${RESET}`;
95
+ // Upper half transparent — draw the LOWER half-block glyph instead, with
96
+ // fg set to the lower color and no bg, so the painted half is the bottom.
97
+ if (!upperColor)
98
+ return `${fgCode(lowerColor)}▄${RESET}`;
99
+ return `${fgCode(upperColor)}${bgCode(lowerColor)}▀${RESET}`;
100
+ }
101
+ /** Pack a 9-row grid into 5 terminal lines (rows 0-1, 2-3, 4-5, 6-7, 8-blank). */
102
+ function renderGrid(rows) {
103
+ const lines = [];
104
+ for (let i = 0; i < rows.length; i += 2) {
105
+ const upperRow = rows[i];
106
+ const lowerRow = rows[i + 1] ?? TRANSPARENT.repeat(upperRow.length);
107
+ let line = '';
108
+ for (let c = 0; c < upperRow.length; c++) {
109
+ line += renderCell(upperRow[c], lowerRow[c]);
110
+ }
111
+ lines.push(line);
112
+ }
113
+ return lines;
114
+ }
115
+ /**
116
+ * Render Pinch as 24-bit ANSI half-block art for the given state, plus a
117
+ * one-line caption. Pure — never touches process.env/stdout; callers must
118
+ * gate on `pinchEnabled()` before printing the result.
119
+ *
120
+ * `frame` only affects 'thinking' (2-frame spinner alternation); every other
121
+ * state ignores it.
122
+ */
123
+ export function renderPinch(state, frame = 0) {
124
+ const grid = gridForState(state, frame);
125
+ const lines = renderGrid(grid);
126
+ if (state === 'confused') {
127
+ // "beside the art" — a bold '?' to the right of the eye row (grid rows
128
+ // 4-5 pack into terminal line index 2).
129
+ lines[2] = `${lines[2]} \x1b[1m?${RESET}`;
130
+ }
131
+ return [...lines, CAPTIONS[state]].join('\n');
132
+ }
133
+ /**
134
+ * True when it's safe to print Pinch art: a real color-capable interactive
135
+ * terminal. False under NO_COLOR (https://no-color.org — presence, not
136
+ * value, disables color output), a non-TTY stdout (piped/redirected output —
137
+ * covers --json/--quiet/CI log capture), or TERM=dumb.
138
+ */
139
+ export function pinchEnabled() {
140
+ if (process.env['NO_COLOR'] !== undefined)
141
+ return false;
142
+ if (!process.stdout.isTTY)
143
+ return false;
144
+ if (process.env['TERM'] === 'dumb')
145
+ return false;
146
+ return true;
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "1.18.7",
3
+ "version": "1.19.0",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {