numux 1.10.0 → 1.10.2

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/numux.js +410 -181
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -226,6 +226,7 @@ Each process accepts:
226
226
  | `color` | `string \| string[]` | auto | Hex (e.g. `"#ff6600"`) or basic name: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple |
227
227
  | `watch` | `string \| string[]` | — | Glob patterns — restart process when matching files change |
228
228
  | `interactive` | `boolean` | `false` | When `true`, keyboard input is forwarded to the process |
229
+ | `errorMatcher` | `boolean \| string` | — | `true` detects ANSI red output, string = regex pattern — shows error indicator on tab |
229
230
 
230
231
  ### File watching
231
232
 
package/dist/numux.js CHANGED
@@ -22,7 +22,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
22
22
  var require_package = __commonJS((exports, module) => {
23
23
  module.exports = {
24
24
  name: "numux",
25
- version: "1.10.0",
25
+ version: "1.10.2",
26
26
  description: "Terminal multiplexer with dependency orchestration",
27
27
  type: "module",
28
28
  license: "MIT",
@@ -78,7 +78,227 @@ var require_package = __commonJS((exports, module) => {
78
78
  import { existsSync as existsSync5, writeFileSync } from "fs";
79
79
  import { resolve as resolve8 } from "path";
80
80
 
81
+ // src/cli-flags.ts
82
+ var commaSplit = (raw) => raw.split(",").map((s) => s.trim()).filter(Boolean);
83
+ var FLAGS = [
84
+ {
85
+ type: "value",
86
+ long: "--workspace",
87
+ short: "-w",
88
+ key: "workspace",
89
+ description: "Run a package.json script across all workspaces",
90
+ valueName: "<script>",
91
+ completionHint: "none"
92
+ },
93
+ {
94
+ type: "value",
95
+ long: "--name",
96
+ short: "-n",
97
+ key: "named",
98
+ description: "Add a named process",
99
+ valueName: "<name=command>",
100
+ completionHint: "none",
101
+ parse(raw) {
102
+ const eq = raw.indexOf("=");
103
+ if (eq < 1) {
104
+ throw new Error(`Invalid --name value: expected "name=command", got "${raw}"`);
105
+ }
106
+ return { name: raw.slice(0, eq), command: raw.slice(eq + 1) };
107
+ }
108
+ },
109
+ {
110
+ type: "value",
111
+ long: "--color",
112
+ short: "-c",
113
+ key: "colors",
114
+ description: "Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)",
115
+ valueName: "<colors>",
116
+ completionHint: "none",
117
+ parse: commaSplit
118
+ },
119
+ {
120
+ type: "boolean",
121
+ long: "--colors",
122
+ key: "autoColors",
123
+ description: "Auto-assign colors to processes based on their name"
124
+ },
125
+ {
126
+ type: "value",
127
+ long: "--config",
128
+ key: "configPath",
129
+ description: "Config file path (default: auto-detect)",
130
+ valueName: "<path>",
131
+ completionHint: "file"
132
+ },
133
+ {
134
+ type: "boolean",
135
+ long: "--prefix",
136
+ short: "-p",
137
+ key: "prefix",
138
+ description: "Prefixed output mode (no TUI, for CI/scripts)"
139
+ },
140
+ {
141
+ type: "value",
142
+ long: "--only",
143
+ key: "only",
144
+ description: "Only run these processes (+ their dependencies)",
145
+ valueName: "<a,b,...>",
146
+ completionHint: "none",
147
+ parse: commaSplit
148
+ },
149
+ {
150
+ type: "value",
151
+ long: "--exclude",
152
+ key: "exclude",
153
+ description: "Exclude these processes",
154
+ valueName: "<a,b,...>",
155
+ completionHint: "none",
156
+ parse: commaSplit
157
+ },
158
+ {
159
+ type: "boolean",
160
+ long: "--kill-others",
161
+ key: "killOthers",
162
+ description: "Kill all processes when any exits"
163
+ },
164
+ {
165
+ type: "boolean",
166
+ long: "--no-restart",
167
+ key: "noRestart",
168
+ description: "Disable auto-restart for crashed processes"
169
+ },
170
+ {
171
+ type: "boolean",
172
+ long: "--no-watch",
173
+ key: "noWatch",
174
+ description: "Disable file watching even if config has watch patterns"
175
+ },
176
+ {
177
+ type: "boolean",
178
+ long: "--timestamps",
179
+ short: "-t",
180
+ key: "timestamps",
181
+ description: "Add timestamps to prefixed output lines"
182
+ },
183
+ {
184
+ type: "value",
185
+ long: "--log-dir",
186
+ key: "logDir",
187
+ description: "Write per-process logs to directory",
188
+ valueName: "<path>",
189
+ completionHint: "directory"
190
+ },
191
+ {
192
+ type: "boolean",
193
+ long: "--debug",
194
+ key: "debug",
195
+ description: "Enable debug logging to .numux/debug.log"
196
+ },
197
+ {
198
+ type: "boolean",
199
+ long: "--help",
200
+ short: "-h",
201
+ key: "help",
202
+ description: "Show this help"
203
+ },
204
+ {
205
+ type: "boolean",
206
+ long: "--version",
207
+ short: "-v",
208
+ key: "version",
209
+ description: "Show version"
210
+ }
211
+ ];
212
+ var SUBCOMMANDS = [
213
+ {
214
+ name: "init",
215
+ description: "Create a starter config file",
216
+ parse: (_args, i, result) => {
217
+ result.init = true;
218
+ return i;
219
+ }
220
+ },
221
+ {
222
+ name: "validate",
223
+ description: "Validate config and show process graph",
224
+ parse: (_args, i, result) => {
225
+ result.validate = true;
226
+ return i;
227
+ }
228
+ },
229
+ {
230
+ name: "exec",
231
+ description: "Run a command in a process's environment",
232
+ usage: "exec <name> [--] <cmd>",
233
+ parse: (args, i, result) => {
234
+ result.exec = true;
235
+ const name = args[++i];
236
+ if (!name)
237
+ throw new Error("exec requires a process name");
238
+ result.execName = name;
239
+ if (args[i + 1] === "--")
240
+ i++;
241
+ const rest = args.slice(i + 1);
242
+ if (rest.length === 0)
243
+ throw new Error("exec requires a command to run");
244
+ result.execCommand = rest.join(" ");
245
+ return "break";
246
+ }
247
+ },
248
+ {
249
+ name: "completions",
250
+ description: "Generate shell completions (bash, zsh, fish)",
251
+ usage: "completions <shell>",
252
+ parse: (args, i, result) => {
253
+ const next = args[++i];
254
+ if (next === undefined)
255
+ throw new Error("Missing value for completions");
256
+ result.completions = next;
257
+ return i;
258
+ }
259
+ }
260
+ ];
261
+ function generateHelp() {
262
+ const lines = [
263
+ "numux \u2014 terminal multiplexer with dependency orchestration",
264
+ "",
265
+ "Usage:",
266
+ " numux Run processes from config file",
267
+ " numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel",
268
+ " numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands",
269
+ " numux -w <script> Run a script across all workspaces"
270
+ ];
271
+ for (const sub of SUBCOMMANDS) {
272
+ const label = ` numux ${sub.usage ?? sub.name}`;
273
+ lines.push(`${label.padEnd(33)}${sub.description}`);
274
+ }
275
+ lines.push("", "Options:");
276
+ for (const f of FLAGS) {
277
+ const parts = [];
278
+ if (f.short)
279
+ parts.push(`${f.short},`);
280
+ parts.push(f.long);
281
+ if (f.type === "value")
282
+ parts.push(f.valueName);
283
+ const left = ` ${parts.join(" ")}`;
284
+ lines.push(`${left.padEnd(29)}${f.description}`);
285
+ }
286
+ lines.push("", "Config files (auto-detected):", " numux.config.ts, numux.config.js");
287
+ return lines.join(`
288
+ `);
289
+ }
290
+
81
291
  // src/cli.ts
292
+ var flagByName = new Map;
293
+ for (const f of FLAGS) {
294
+ flagByName.set(f.long, f);
295
+ if (f.short)
296
+ flagByName.set(f.short, f);
297
+ }
298
+ var subcommandByName = new Map;
299
+ for (const s of SUBCOMMANDS) {
300
+ subcommandByName.set(s.name, s);
301
+ }
82
302
  function parseArgs(argv) {
83
303
  const result = {
84
304
  help: false,
@@ -99,76 +319,35 @@ function parseArgs(argv) {
99
319
  };
100
320
  const args = argv.slice(2);
101
321
  let i = 0;
102
- const consumeValue = (flag) => {
103
- const next = args[++i];
104
- if (next === undefined) {
105
- throw new Error(`Missing value for ${flag}`);
106
- }
107
- return next;
108
- };
109
322
  while (i < args.length) {
110
323
  const arg = args[i];
111
- if (arg === "-h" || arg === "--help") {
112
- result.help = true;
113
- } else if (arg === "-v" || arg === "--version") {
114
- result.version = true;
115
- } else if (arg === "--debug") {
116
- result.debug = true;
117
- } else if (arg === "-p" || arg === "--prefix") {
118
- result.prefix = true;
119
- } else if (arg === "--kill-others") {
120
- result.killOthers = true;
121
- } else if (arg === "-t" || arg === "--timestamps") {
122
- result.timestamps = true;
123
- } else if (arg === "--no-restart") {
124
- result.noRestart = true;
125
- } else if (arg === "-w" || arg === "--workspace") {
126
- result.workspace = consumeValue(arg);
127
- } else if (arg === "--no-watch") {
128
- result.noWatch = true;
129
- } else if (arg === "--colors") {
130
- result.autoColors = true;
131
- } else if (arg === "--config") {
132
- result.configPath = consumeValue(arg);
133
- } else if (arg === "-c" || arg === "--color") {
134
- result.colors = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
135
- } else if (arg === "--log-dir") {
136
- result.logDir = consumeValue(arg);
137
- } else if (arg === "--only") {
138
- result.only = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
139
- } else if (arg === "--exclude") {
140
- result.exclude = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
141
- } else if (arg === "-n" || arg === "--name") {
142
- const value = consumeValue(arg);
143
- const eq = value.indexOf("=");
144
- if (eq < 1) {
145
- throw new Error(`Invalid --name value: expected "name=command", got "${value}"`);
324
+ const flag = flagByName.get(arg);
325
+ if (flag) {
326
+ if (flag.type === "boolean") {
327
+ result[flag.key] = true;
328
+ } else {
329
+ const next = args[++i];
330
+ if (next === undefined) {
331
+ throw new Error(`Missing value for ${arg}`);
332
+ }
333
+ const value = flag.parse ? flag.parse(next, arg) : next;
334
+ const current = result[flag.key];
335
+ if (Array.isArray(current)) {
336
+ current.push(value);
337
+ } else {
338
+ result[flag.key] = value;
339
+ }
146
340
  }
147
- result.named.push({
148
- name: value.slice(0, eq),
149
- command: value.slice(eq + 1)
150
- });
151
- } else if (arg === "init" && result.commands.length === 0) {
152
- result.init = true;
153
- } else if (arg === "validate" && result.commands.length === 0) {
154
- result.validate = true;
155
- } else if (arg === "exec" && result.commands.length === 0) {
156
- result.exec = true;
157
- const name = args[++i];
158
- if (!name)
159
- throw new Error("exec requires a process name");
160
- result.execName = name;
161
- if (args[i + 1] === "--")
162
- i++;
163
- const rest = args.slice(i + 1);
164
- if (rest.length === 0)
165
- throw new Error("exec requires a command to run");
166
- result.execCommand = rest.join(" ");
167
- break;
168
- } else if (arg === "completions" && result.commands.length === 0) {
169
- result.completions = consumeValue(arg);
170
341
  } else if (!arg.startsWith("-")) {
171
- result.commands.push(arg);
342
+ const sub = result.commands.length === 0 ? subcommandByName.get(arg) : undefined;
343
+ if (sub) {
344
+ const ret = sub.parse(args, i, result);
345
+ if (ret === "break")
346
+ break;
347
+ i = ret;
348
+ } else {
349
+ result.commands.push(arg);
350
+ }
172
351
  } else {
173
352
  throw new Error(`Unknown option: ${arg}`);
174
353
  }
@@ -259,7 +438,36 @@ function generateCompletions(shell) {
259
438
  throw new Error(`Unknown shell: "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
260
439
  }
261
440
  }
441
+ function longName(f) {
442
+ return f.long.replace(/^-+/, "");
443
+ }
444
+ function sq(s) {
445
+ return s.replace(/'/g, "'\\''");
446
+ }
262
447
  function bashCompletions() {
448
+ const caseEntries = [];
449
+ for (const f of FLAGS) {
450
+ if (f.type !== "value")
451
+ continue;
452
+ const names = f.short ? `${f.short}|${f.long}` : f.long;
453
+ if (f.completionHint === "file") {
454
+ caseEntries.push(` ${names})
455
+ COMPREPLY=( $(compgen -f -- "$cur") )
456
+ return ;;`);
457
+ } else if (f.completionHint === "directory") {
458
+ caseEntries.push(` ${names})
459
+ COMPREPLY=( $(compgen -d -- "$cur") )
460
+ return ;;`);
461
+ } else {
462
+ caseEntries.push(` ${names})
463
+ return ;;`);
464
+ }
465
+ }
466
+ caseEntries.push(` completions)
467
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
468
+ return ;;`);
469
+ const allFlags = FLAGS.flatMap((f) => f.short ? [f.short, f.long] : [f.long]);
470
+ const subcmds = SUBCOMMANDS.map((s) => s.name);
263
471
  return `# numux bash completions
264
472
  # Add to ~/.bashrc: eval "$(numux completions bash)"
265
473
  _numux() {
@@ -268,60 +476,66 @@ _numux() {
268
476
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
269
477
 
270
478
  case "$prev" in
271
- --config)
272
- COMPREPLY=( $(compgen -f -- "$cur") )
273
- return ;;
274
- --log-dir)
275
- COMPREPLY=( $(compgen -d -- "$cur") )
276
- return ;;
277
- --only|--exclude)
278
- return ;;
279
- -n|--name|-w|--workspace)
280
- return ;;
281
- completions)
282
- COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
283
- return ;;
479
+ ${caseEntries.join(`
480
+ `)}
284
481
  esac
285
482
 
286
483
  if [[ "$cur" == -* ]]; then
287
- COMPREPLY=( $(compgen -W "-h --help -v --version -w --workspace -c --color --colors --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
484
+ COMPREPLY=( $(compgen -W "${allFlags.join(" ")}" -- "$cur") )
288
485
  else
289
- local subcmds="init validate exec completions"
486
+ local subcmds="${subcmds.join(" ")}"
290
487
  COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
291
488
  fi
292
489
  }
293
490
  complete -F _numux numux`;
294
491
  }
295
492
  function zshCompletions() {
493
+ const subcmdLines = SUBCOMMANDS.map((s) => ` '${s.name}:${sq(s.description)}'`).join(`
494
+ `);
495
+ const argLines = [];
496
+ for (const f of FLAGS) {
497
+ const desc = sq(f.description);
498
+ if (f.short) {
499
+ if (f.type === "value") {
500
+ let suffix = "";
501
+ if (f.completionHint === "file")
502
+ suffix = ":file:_files";
503
+ else if (f.completionHint === "directory")
504
+ suffix = ":directory:_directories";
505
+ else
506
+ suffix = `:${longName(f)}`;
507
+ argLines.push(` '(${f.short} ${f.long})'{${f.short},${f.long}}'[${desc}]${suffix}'`);
508
+ } else {
509
+ argLines.push(` '(${f.short} ${f.long})'{${f.short},${f.long}}'[${desc}]'`);
510
+ }
511
+ } else {
512
+ if (f.type === "value") {
513
+ let suffix = "";
514
+ if (f.completionHint === "file")
515
+ suffix = ":file:_files";
516
+ else if (f.completionHint === "directory")
517
+ suffix = ":directory:_directories";
518
+ else
519
+ suffix = `:${longName(f)}`;
520
+ argLines.push(` '${f.long}[${desc}]${suffix}'`);
521
+ } else {
522
+ argLines.push(` '${f.long}[${desc}]'`);
523
+ }
524
+ }
525
+ }
526
+ const argsBlock = argLines.map((l) => `${l} \\`).join(`
527
+ `);
296
528
  return `#compdef numux
297
529
  # numux zsh completions
298
530
  # Add to ~/.zshrc: eval "$(numux completions zsh)"
299
531
  _numux() {
300
532
  local -a subcmds
301
533
  subcmds=(
302
- 'init:Create a starter config file'
303
- 'validate:Validate config and show process graph'
304
- 'exec:Run a command in a process environment'
305
- 'completions:Generate shell completions'
534
+ ${subcmdLines}
306
535
  )
307
536
 
308
537
  _arguments -s \\
309
- '(-h --help)'{-h,--help}'[Show help]' \\
310
- '(-v --version)'{-v,--version}'[Show version]' \\
311
- '(-w --workspace)'{-w,--workspace}'[Run script across all workspaces]:script' \\
312
- '(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
313
- '--colors[Auto-assign colors based on process name]' \\
314
- '--config[Config file path]:file:_files' \\
315
- '(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
316
- '(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
317
- '--only[Only run these processes]:processes' \\
318
- '--exclude[Exclude these processes]:processes' \\
319
- '--kill-others[Kill all when any exits]' \\
320
- '--no-restart[Disable auto-restart]' \\
321
- '--no-watch[Disable file watching]' \\
322
- '(-t --timestamps)'{-t,--timestamps}'[Add timestamps to output]' \\
323
- '--log-dir[Log directory]:directory:_directories' \\
324
- '--debug[Enable debug logging]' \\
538
+ ${argsBlock}
325
539
  '1:subcommand:->subcmd' \\
326
540
  '*:command' \\
327
541
  && return
@@ -335,37 +549,37 @@ _numux() {
335
549
  _numux`;
336
550
  }
337
551
  function fishCompletions() {
338
- return `# numux fish completions
339
- # Add to fish: numux completions fish | source
340
- # Or save to: ~/.config/fish/completions/numux.fish
341
- complete -c numux -f
342
-
343
- # Subcommands
344
- complete -c numux -n __fish_use_subcommand -a init -d 'Create a starter config file'
345
- complete -c numux -n __fish_use_subcommand -a validate -d 'Validate config and show process graph'
346
- complete -c numux -n __fish_use_subcommand -a exec -d 'Run a command in a process environment'
347
- complete -c numux -n __fish_use_subcommand -a completions -d 'Generate shell completions'
348
-
349
- # Completions subcommand
350
- complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'
351
-
352
- # Options
353
- complete -c numux -s h -l help -d 'Show help'
354
- complete -c numux -s v -l version -d 'Show version'
355
- complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
356
- complete -c numux -l colors -d 'Auto-assign colors based on process name'
357
- complete -c numux -l config -rF -d 'Config file path'
358
- complete -c numux -s w -l workspace -r -d 'Run script across all workspaces'
359
- complete -c numux -s n -l name -r -d 'Named process (name=command)'
360
- complete -c numux -s p -l prefix -d 'Prefixed output mode'
361
- complete -c numux -l only -r -d 'Only run these processes'
362
- complete -c numux -l exclude -r -d 'Exclude these processes'
363
- complete -c numux -l kill-others -d 'Kill all when any exits'
364
- complete -c numux -l no-restart -d 'Disable auto-restart'
365
- complete -c numux -l no-watch -d 'Disable file watching'
366
- complete -c numux -s t -l timestamps -d 'Add timestamps to output'
367
- complete -c numux -l log-dir -ra '(__fish_complete_directories)' -d 'Log directory'
368
- complete -c numux -l debug -d 'Enable debug logging'`;
552
+ const lines = [
553
+ "# numux fish completions",
554
+ "# Add to fish: numux completions fish | source",
555
+ "# Or save to: ~/.config/fish/completions/numux.fish",
556
+ "complete -c numux -f",
557
+ "",
558
+ "# Subcommands"
559
+ ];
560
+ for (const s of SUBCOMMANDS) {
561
+ lines.push(`complete -c numux -n __fish_use_subcommand -a ${s.name} -d '${sq(s.description)}'`);
562
+ }
563
+ lines.push("", "# Completions subcommand", "complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'", "", "# Options");
564
+ for (const f of FLAGS) {
565
+ const parts = ["complete -c numux"];
566
+ if (f.short)
567
+ parts.push(`-s ${f.short.replace("-", "")}`);
568
+ parts.push(`-l ${longName(f)}`);
569
+ if (f.type === "value") {
570
+ if (f.completionHint === "file") {
571
+ parts.push("-rF");
572
+ } else if (f.completionHint === "directory") {
573
+ parts.push("-ra '(__fish_complete_directories)'");
574
+ } else {
575
+ parts.push("-r");
576
+ }
577
+ }
578
+ parts.push(`-d '${sq(f.description)}'`);
579
+ lines.push(parts.join(" "));
580
+ }
581
+ return lines.join(`
582
+ `);
369
583
  }
370
584
 
371
585
  // src/config/expand-scripts.ts
@@ -1514,6 +1728,9 @@ class ProcessManager {
1514
1728
  updateStatus(name, status) {
1515
1729
  const state = this.states.get(name);
1516
1730
  state.status = status;
1731
+ if (status === "ready" && this.config.processes[name].readyPattern) {
1732
+ this.restartAttempts.set(name, 0);
1733
+ }
1517
1734
  this.emit({ type: "status", name, status });
1518
1735
  }
1519
1736
  restart(name, cols, rows) {
@@ -1653,6 +1870,8 @@ class Pane {
1653
1870
  decoder = new TextDecoder;
1654
1871
  _onScroll = null;
1655
1872
  _onCopy = null;
1873
+ _textLines = null;
1874
+ _textLinesLower = null;
1656
1875
  constructor(renderer, name, cols, rows, interactive = false) {
1657
1876
  this.scrollBox = new ScrollBoxRenderable(renderer, {
1658
1877
  id: `pane-${name}`,
@@ -1688,10 +1907,14 @@ class Pane {
1688
1907
  feed(data) {
1689
1908
  const text = this.decoder.decode(data, { stream: true });
1690
1909
  this.terminal.feed(text);
1910
+ this._textLines = null;
1911
+ this._textLinesLower = null;
1691
1912
  }
1692
1913
  resize(cols, rows) {
1693
1914
  this.terminal.cols = cols;
1694
1915
  this.terminal.rows = rows;
1916
+ this._textLines = null;
1917
+ this._textLinesLower = null;
1695
1918
  }
1696
1919
  get isAtBottom() {
1697
1920
  const { scrollTop, scrollHeight, viewport } = this.scrollBox;
@@ -1723,16 +1946,19 @@ class Pane {
1723
1946
  search(query) {
1724
1947
  if (!query)
1725
1948
  return [];
1726
- const text = this.terminal.getText();
1727
- const lines = text.split(`
1949
+ if (!this._textLines) {
1950
+ const text = this.terminal.getText();
1951
+ this._textLines = text.split(`
1728
1952
  `);
1953
+ this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
1954
+ }
1955
+ const lines = this._textLinesLower;
1729
1956
  const matches = [];
1730
1957
  const lowerQuery = query.toLowerCase();
1731
1958
  for (let line = 0;line < lines.length; line++) {
1732
- const lowerLine = lines[line].toLowerCase();
1733
1959
  let pos = 0;
1734
1960
  while (true) {
1735
- const idx = lowerLine.indexOf(lowerQuery, pos);
1961
+ const idx = lines[line].indexOf(lowerQuery, pos);
1736
1962
  if (idx === -1)
1737
1963
  break;
1738
1964
  matches.push({ line, start: idx, end: idx + query.length });
@@ -1742,12 +1968,22 @@ class Pane {
1742
1968
  return matches;
1743
1969
  }
1744
1970
  setHighlights(matches, currentIndex) {
1745
- const regions = matches.map((m, i) => ({
1746
- line: m.line,
1747
- start: m.start,
1748
- end: m.end,
1749
- backgroundColor: i === currentIndex ? "#b58900" : "#073642"
1750
- }));
1971
+ const firstVisible = Math.max(0, Math.floor(this.scrollBox.scrollTop) - 2);
1972
+ const lastVisible = Math.ceil(this.scrollBox.scrollTop + this.scrollBox.viewport.height) + 2;
1973
+ const regions = [];
1974
+ for (let i = 0;i < matches.length; i++) {
1975
+ const m = matches[i];
1976
+ if (m.line < firstVisible || m.line > lastVisible) {
1977
+ if (i !== currentIndex)
1978
+ continue;
1979
+ }
1980
+ regions.push({
1981
+ line: m.line,
1982
+ start: m.start,
1983
+ end: m.end,
1984
+ backgroundColor: i === currentIndex ? "#b58900" : "#073642"
1985
+ });
1986
+ }
1751
1987
  this.terminal.highlights = regions;
1752
1988
  }
1753
1989
  clearHighlights() {
@@ -1759,6 +1995,8 @@ class Pane {
1759
1995
  }
1760
1996
  clear() {
1761
1997
  this.terminal.reset();
1998
+ this._textLines = null;
1999
+ this._textLinesLower = null;
1762
2000
  }
1763
2001
  destroy() {
1764
2002
  this.terminal.destroy();
@@ -1897,6 +2135,7 @@ class ColoredSelectRenderable extends SelectRenderable {
1897
2135
  const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
1898
2136
  const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
1899
2137
  const selectedTextColor = this._selectedTextColor;
2138
+ const lineWidth = fb.width;
1900
2139
  for (let i = 0;i < visibleCount; i++) {
1901
2140
  const actualIndex = scrollOffset + i;
1902
2141
  const itemY = i * linesPerItem;
@@ -1904,13 +2143,11 @@ class ColoredSelectRenderable extends SelectRenderable {
1904
2143
  const isSelected = actualIndex === selectedIndex;
1905
2144
  const defaultColor = isSelected ? selectedTextColor : baseTextColor;
1906
2145
  const colors = this._optionColors[actualIndex];
1907
- fb.drawText(`${optName} `, 1, itemY, defaultColor);
2146
+ const textColor = colors?.name ?? defaultColor;
2147
+ fb.drawText(optName.padEnd(lineWidth), 1, itemY, textColor);
1908
2148
  if (colors?.icon) {
1909
2149
  fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
1910
2150
  }
1911
- if (colors?.name) {
1912
- fb.drawText(optName.slice(2), 3, itemY, colors.name);
1913
- }
1914
2151
  }
1915
2152
  }
1916
2153
  }
@@ -2067,6 +2304,7 @@ class App {
2067
2304
  sidebarWidth = 20;
2068
2305
  config;
2069
2306
  resizeTimer = null;
2307
+ searchTimer = null;
2070
2308
  searchMode = false;
2071
2309
  searchQuery = "";
2072
2310
  searchMatches = [];
@@ -2122,6 +2360,11 @@ class App {
2122
2360
  const interactive = this.config.processes[name].interactive === true;
2123
2361
  const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
2124
2362
  pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
2363
+ pane.onScroll(() => {
2364
+ if (this.searchMode && this.searchMatches.length > 0 && this.activePane === name) {
2365
+ this.updateSearchHighlights();
2366
+ }
2367
+ });
2125
2368
  this.panes.set(name, pane);
2126
2369
  paneContainer.add(pane.scrollBox);
2127
2370
  }
@@ -2322,6 +2565,10 @@ class App {
2322
2565
  this.searchQuery = "";
2323
2566
  this.searchMatches = [];
2324
2567
  this.searchIndex = -1;
2568
+ if (this.searchTimer) {
2569
+ clearTimeout(this.searchTimer);
2570
+ this.searchTimer = null;
2571
+ }
2325
2572
  if (this.activePane) {
2326
2573
  this.panes.get(this.activePane)?.clearHighlights();
2327
2574
  }
@@ -2347,15 +2594,24 @@ class App {
2347
2594
  if (key.name === "backspace") {
2348
2595
  if (this.searchQuery.length > 0) {
2349
2596
  this.searchQuery = this.searchQuery.slice(0, -1);
2350
- this.runSearch();
2597
+ this.scheduleSearch();
2351
2598
  }
2352
2599
  return;
2353
2600
  }
2354
2601
  if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
2355
2602
  this.searchQuery += key.sequence;
2356
- this.runSearch();
2603
+ this.scheduleSearch();
2357
2604
  }
2358
2605
  }
2606
+ scheduleSearch() {
2607
+ this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
2608
+ if (this.searchTimer)
2609
+ clearTimeout(this.searchTimer);
2610
+ this.searchTimer = setTimeout(() => {
2611
+ this.searchTimer = null;
2612
+ this.runSearch();
2613
+ }, 100);
2614
+ }
2359
2615
  runSearch() {
2360
2616
  if (!this.activePane)
2361
2617
  return;
@@ -2399,6 +2655,10 @@ class App {
2399
2655
  clearTimeout(this.resizeTimer);
2400
2656
  this.resizeTimer = null;
2401
2657
  }
2658
+ if (this.searchTimer) {
2659
+ clearTimeout(this.searchTimer);
2660
+ this.searchTimer = null;
2661
+ }
2402
2662
  for (const timer of this.inputWaitTimers.values()) {
2403
2663
  clearTimeout(timer);
2404
2664
  }
@@ -2665,38 +2925,7 @@ function setupShutdownHandlers(app, logWriter) {
2665
2925
  }
2666
2926
 
2667
2927
  // src/index.ts
2668
- var HELP = `numux \u2014 terminal multiplexer with dependency orchestration
2669
-
2670
- Usage:
2671
- numux Run processes from config file
2672
- numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel
2673
- numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands
2674
- numux -w <script> Run a script across all workspaces
2675
- numux init Create a starter config file
2676
- numux validate Validate config and show process graph
2677
- numux exec <name> [--] <cmd> Run a command in a process's environment
2678
- numux completions <shell> Generate shell completions (bash, zsh, fish)
2679
-
2680
- Options:
2681
- -w, --workspace <script> Run a package.json script across all workspaces
2682
- -n, --name <name=command> Add a named process
2683
- -c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
2684
- --colors Auto-assign colors to processes based on their name
2685
- --config <path> Config file path (default: auto-detect)
2686
- -p, --prefix Prefixed output mode (no TUI, for CI/scripts)
2687
- --only <a,b,...> Only run these processes (+ their dependencies)
2688
- --exclude <a,b,...> Exclude these processes
2689
- --kill-others Kill all processes when any exits
2690
- --no-restart Disable auto-restart for crashed processes
2691
- --no-watch Disable file watching even if config has watch patterns
2692
- -t, --timestamps Add timestamps to prefixed output lines
2693
- --log-dir <path> Write per-process logs to directory
2694
- --debug Enable debug logging to .numux/debug.log
2695
- -h, --help Show this help
2696
- -v, --version Show version
2697
-
2698
- Config files (auto-detected):
2699
- numux.config.ts, numux.config.js`;
2928
+ var HELP = generateHelp();
2700
2929
  var INIT_TEMPLATE = `import { defineConfig } from 'numux'
2701
2930
 
2702
2931
  export default defineConfig({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",