numux 1.6.0 → 1.8.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.
package/README.md CHANGED
@@ -178,7 +178,7 @@ Each process accepts:
178
178
  | `delay` | `number` | — | Milliseconds to wait before starting the process |
179
179
  | `condition` | `string` | — | Env var name; process skipped if falsy. Prefix with `!` to negate |
180
180
  | `stopSignal` | `string` | `SIGTERM` | Signal for graceful stop (`SIGTERM`, `SIGINT`, or `SIGHUP`) |
181
- | `color` | `string` | auto | Hex color for tab icon and status bar (e.g. `"#ff6600"`) |
181
+ | `color` | `string \| string[]` | auto | Hex (e.g. `"#ff6600"`) or basic name: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple |
182
182
  | `watch` | `string \| string[]` | — | Glob patterns — restart process when matching files change |
183
183
  | `interactive` | `boolean` | `false` | When `true`, keyboard input is forwarded to the process |
184
184
 
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.6.0",
25
+ version: "1.8.0",
26
26
  description: "Terminal multiplexer with dependency orchestration",
27
27
  type: "module",
28
28
  license: "MIT",
@@ -92,6 +92,7 @@ function parseArgs(argv) {
92
92
  timestamps: false,
93
93
  noRestart: false,
94
94
  noWatch: false,
95
+ autoColors: false,
95
96
  configPath: undefined,
96
97
  commands: [],
97
98
  named: []
@@ -123,6 +124,8 @@ function parseArgs(argv) {
123
124
  result.noRestart = true;
124
125
  } else if (arg === "--no-watch") {
125
126
  result.noWatch = true;
127
+ } else if (arg === "--colors") {
128
+ result.autoColors = true;
126
129
  } else if (arg === "--config") {
127
130
  result.configPath = consumeValue(arg);
128
131
  } else if (arg === "-c" || arg === "--color") {
@@ -279,7 +282,7 @@ _numux() {
279
282
  esac
280
283
 
281
284
  if [[ "$cur" == -* ]]; then
282
- COMPREPLY=( $(compgen -W "-h --help -v --version -c --color --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
285
+ COMPREPLY=( $(compgen -W "-h --help -v --version -c --color --colors --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
283
286
  else
284
287
  local subcmds="init validate exec completions"
285
288
  COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
@@ -304,6 +307,7 @@ _numux() {
304
307
  '(-h --help)'{-h,--help}'[Show help]' \\
305
308
  '(-v --version)'{-v,--version}'[Show version]' \\
306
309
  '(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
310
+ '--colors[Auto-assign colors based on process name]' \\
307
311
  '--config[Config file path]:file:_files' \\
308
312
  '(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
309
313
  '(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
@@ -346,6 +350,7 @@ complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish
346
350
  complete -c numux -s h -l help -d 'Show help'
347
351
  complete -c numux -s v -l version -d 'Show version'
348
352
  complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
353
+ complete -c numux -l colors -d 'Auto-assign colors based on process name'
349
354
  complete -c numux -l config -rF -d 'Config file path'
350
355
  complete -c numux -s n -l name -r -d 'Named process (name=command)'
351
356
  complete -c numux -s p -l prefix -d 'Prefixed output mode'
@@ -592,6 +597,31 @@ function findCycle(remaining, config) {
592
597
  }
593
598
 
594
599
  // src/utils/color.ts
600
+ var BASIC_COLORS = {
601
+ black: "#000000",
602
+ red: "#ff0000",
603
+ green: "#00ff00",
604
+ yellow: "#ffff00",
605
+ blue: "#0000ff",
606
+ magenta: "#ff00ff",
607
+ cyan: "#00ffff",
608
+ white: "#ffffff",
609
+ gray: "#808080",
610
+ grey: "#808080",
611
+ orange: "#ffa500",
612
+ purple: "#800080"
613
+ };
614
+ function isValidColor(color) {
615
+ if (HEX_COLOR_RE.test(color))
616
+ return true;
617
+ return color.toLowerCase() in BASIC_COLORS;
618
+ }
619
+ function resolveToHex(color) {
620
+ if (HEX_COLOR_RE.test(color))
621
+ return color.startsWith("#") ? color : `#${color}`;
622
+ const hex = BASIC_COLORS[color.toLowerCase()];
623
+ return hex ?? "";
624
+ }
595
625
  function hexToAnsi(hex) {
596
626
  const h = hex.replace("#", "");
597
627
  const r = Number.parseInt(h.slice(0, 2), 16);
@@ -626,6 +656,13 @@ var DEFAULT_ANSI_COLORS = [
626
656
  "\x1B[95m"
627
657
  ];
628
658
  var DEFAULT_HEX_COLORS = ["#00cccc", "#cccc00", "#cc00cc", "#0000cc", "#00cc00", "#ff5555", "#ffff55", "#ff55ff"];
659
+ function colorFromName(name) {
660
+ let hash = 0;
661
+ for (let i = 0;i < name.length; i++) {
662
+ hash = (hash << 5) - hash + name.charCodeAt(i) | 0;
663
+ }
664
+ return DEFAULT_HEX_COLORS[Math.abs(hash) % DEFAULT_HEX_COLORS.length];
665
+ }
629
666
  function resolveColor(color) {
630
667
  if (typeof color === "string")
631
668
  return color;
@@ -641,7 +678,11 @@ function buildProcessColorMap(names, config) {
641
678
  for (const name of names) {
642
679
  const explicit = resolveColor(config.processes[name]?.color);
643
680
  if (explicit) {
644
- map.set(name, hexToAnsi(explicit));
681
+ const hex = resolveToHex(explicit);
682
+ if (hex)
683
+ map.set(name, hexToAnsi(hex));
684
+ else
685
+ map.set(name, DEFAULT_ANSI_COLORS[paletteIndex++ % DEFAULT_ANSI_COLORS.length]);
645
686
  } else {
646
687
  map.set(name, DEFAULT_ANSI_COLORS[paletteIndex % DEFAULT_ANSI_COLORS.length]);
647
688
  paletteIndex++;
@@ -657,7 +698,11 @@ function buildProcessHexColorMap(names, config) {
657
698
  for (const name of names) {
658
699
  const explicit = resolveColor(config.processes[name]?.color);
659
700
  if (explicit) {
660
- map.set(name, explicit.startsWith("#") ? explicit : `#${explicit}`);
701
+ const hex = resolveToHex(explicit);
702
+ if (hex)
703
+ map.set(name, hex);
704
+ else
705
+ map.set(name, DEFAULT_HEX_COLORS[paletteIndex++ % DEFAULT_HEX_COLORS.length]);
661
706
  } else {
662
707
  map.set(name, DEFAULT_HEX_COLORS[paletteIndex % DEFAULT_HEX_COLORS.length]);
663
708
  paletteIndex++;
@@ -721,13 +766,13 @@ function validateConfig(raw, warnings) {
721
766
  }
722
767
  }
723
768
  if (typeof p.color === "string") {
724
- if (!HEX_COLOR_RE.test(p.color)) {
725
- throw new Error(`Process "${name}".color must be a valid hex color (e.g. "#ff8800"), got "${p.color}"`);
769
+ if (!isValidColor(p.color)) {
770
+ throw new Error(`Process "${name}".color must be a hex color (e.g. "#ff8800") or basic name (black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple), got "${p.color}"`);
726
771
  }
727
772
  } else if (Array.isArray(p.color)) {
728
773
  for (const c of p.color) {
729
- if (typeof c !== "string" || !HEX_COLOR_RE.test(c)) {
730
- throw new Error(`Process "${name}".color entries must be valid hex colors (e.g. "#ff8800"), got "${c}"`);
774
+ if (typeof c !== "string" || !isValidColor(c)) {
775
+ throw new Error(`Process "${name}".color entries must be hex or basic names (black, red, green, yellow, blue, magenta, cyan, white, gray, orange), got "${c}"`);
731
776
  }
732
777
  }
733
778
  }
@@ -764,7 +809,8 @@ function validateConfig(raw, warnings) {
764
809
  stopSignal: validateStopSignal(p.stopSignal),
765
810
  color: typeof p.color === "string" ? p.color : Array.isArray(p.color) ? p.color : undefined,
766
811
  watch: validateStringOrStringArray(p.watch),
767
- interactive: typeof p.interactive === "boolean" ? p.interactive : false
812
+ interactive: typeof p.interactive === "boolean" ? p.interactive : false,
813
+ errorMatcher: validateErrorMatcher(name, p.errorMatcher)
768
814
  };
769
815
  }
770
816
  return { processes: validated };
@@ -777,6 +823,22 @@ function validateStringOrStringArray(value) {
777
823
  return;
778
824
  }
779
825
  var validateEnvFile = validateStringOrStringArray;
826
+ function validateErrorMatcher(name, value) {
827
+ if (value === true)
828
+ return true;
829
+ if (typeof value === "string" && value.trim()) {
830
+ try {
831
+ new RegExp(value);
832
+ } catch (err) {
833
+ throw new Error(`Process "${name}".errorMatcher is not a valid regex: "${value}"`, { cause: err });
834
+ }
835
+ return value;
836
+ }
837
+ if (value !== undefined && value !== false) {
838
+ throw new Error(`Process "${name}".errorMatcher must be true or a regex string`);
839
+ }
840
+ return;
841
+ }
780
842
  var VALID_STOP_SIGNALS = new Set(["SIGTERM", "SIGINT", "SIGHUP"]);
781
843
  function validateStopSignal(value) {
782
844
  if (typeof value === "string" && VALID_STOP_SIGNALS.has(value)) {
@@ -884,8 +946,49 @@ function loadEnvFiles(envFile, cwd) {
884
946
  return merged;
885
947
  }
886
948
 
887
- // src/process/ready.ts
949
+ // src/process/error.ts
888
950
  var BUFFER_CAP = 65536;
951
+ var SGR_RE = /\x1b\[([0-9;]*)m/g;
952
+ function hasAnsiRed(text) {
953
+ SGR_RE.lastIndex = 0;
954
+ for (let match = SGR_RE.exec(text);match !== null; match = SGR_RE.exec(text)) {
955
+ const params = match[1].split(";");
956
+ if (params.includes("31") || params.includes("91"))
957
+ return true;
958
+ }
959
+ return false;
960
+ }
961
+ function createErrorChecker(config) {
962
+ const matcher = config.errorMatcher;
963
+ if (!matcher)
964
+ return null;
965
+ const pattern = typeof matcher === "string" ? new RegExp(matcher) : null;
966
+ let buffer = "";
967
+ let triggered = false;
968
+ return {
969
+ feedOutput(data) {
970
+ if (triggered)
971
+ return false;
972
+ buffer += data;
973
+ if (buffer.length > BUFFER_CAP) {
974
+ buffer = buffer.slice(-BUFFER_CAP);
975
+ }
976
+ if (pattern) {
977
+ if (pattern.test(stripAnsi(buffer))) {
978
+ triggered = true;
979
+ return true;
980
+ }
981
+ } else if (hasAnsiRed(buffer)) {
982
+ triggered = true;
983
+ return true;
984
+ }
985
+ return false;
986
+ }
987
+ };
988
+ }
989
+
990
+ // src/process/ready.ts
991
+ var BUFFER_CAP2 = 65536;
889
992
  function createReadinessChecker(config) {
890
993
  const pattern = config.readyPattern ? new RegExp(config.readyPattern) : null;
891
994
  const persistent = config.persistent !== false;
@@ -895,8 +998,8 @@ function createReadinessChecker(config) {
895
998
  if (!(persistent && pattern))
896
999
  return false;
897
1000
  outputBuffer += data;
898
- if (outputBuffer.length > BUFFER_CAP) {
899
- outputBuffer = outputBuffer.slice(-BUFFER_CAP);
1001
+ if (outputBuffer.length > BUFFER_CAP2) {
1002
+ outputBuffer = outputBuffer.slice(-BUFFER_CAP2);
900
1003
  }
901
1004
  return pattern.test(outputBuffer);
902
1005
  },
@@ -916,9 +1019,11 @@ class ProcessRunner {
916
1019
  handler;
917
1020
  proc = null;
918
1021
  readiness;
1022
+ errorChecker;
919
1023
  _ready = false;
920
1024
  stopping = false;
921
1025
  decoder = new TextDecoder;
1026
+ errorDecoder = new TextDecoder;
922
1027
  generation = 0;
923
1028
  readyTimer = null;
924
1029
  restarting = false;
@@ -928,6 +1033,7 @@ class ProcessRunner {
928
1033
  this.config = config;
929
1034
  this.handler = handler;
930
1035
  this.readiness = createReadinessChecker(config);
1036
+ this.errorChecker = createErrorChecker(config);
931
1037
  }
932
1038
  get isReady() {
933
1039
  return this._ready;
@@ -962,6 +1068,7 @@ class ProcessRunner {
962
1068
  return;
963
1069
  this.handler.onOutput(data);
964
1070
  this.checkReadiness(data);
1071
+ this.checkError(data);
965
1072
  }
966
1073
  }
967
1074
  });
@@ -1017,6 +1124,14 @@ class ProcessRunner {
1017
1124
  this.markReady();
1018
1125
  }
1019
1126
  }
1127
+ checkError(data) {
1128
+ if (!this.errorChecker)
1129
+ return;
1130
+ const text = this.errorDecoder.decode(data, { stream: true });
1131
+ if (this.errorChecker.feedOutput(text)) {
1132
+ this.handler.onError();
1133
+ }
1134
+ }
1020
1135
  startReadyTimeout(gen) {
1021
1136
  const timeout = this.config.readyTimeout;
1022
1137
  if (!(timeout && this.config.readyPattern) || this.config.persistent === false)
@@ -1075,6 +1190,7 @@ class ProcessRunner {
1075
1190
  this.restarting = false;
1076
1191
  this.readyTimedOut = false;
1077
1192
  this.readiness = createReadinessChecker(this.config);
1193
+ this.errorChecker = createErrorChecker(this.config);
1078
1194
  this.start(cols, rows);
1079
1195
  }
1080
1196
  async stop(timeoutMs = 5000) {
@@ -1244,7 +1360,8 @@ class ProcessManager {
1244
1360
  readyResolved = true;
1245
1361
  onInitialReady();
1246
1362
  }
1247
- }
1363
+ },
1364
+ onError: () => this.emit({ type: "error", name })
1248
1365
  });
1249
1366
  this.runners.set(name, runner);
1250
1367
  }
@@ -1651,20 +1768,24 @@ class ColoredSelectRenderable extends SelectRenderable {
1651
1768
  const scrollOffset = this.scrollOffset;
1652
1769
  const maxVisibleItems = this.maxVisibleItems;
1653
1770
  const linesPerItem = this.linesPerItem;
1771
+ const selectedIndex = this.getSelectedIndex();
1654
1772
  const options = this.options;
1655
1773
  const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
1774
+ const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
1775
+ const selectedTextColor = this._selectedTextColor;
1656
1776
  for (let i = 0;i < visibleCount; i++) {
1657
1777
  const actualIndex = scrollOffset + i;
1658
- const colors = this._optionColors[actualIndex];
1659
- if (!colors)
1660
- continue;
1661
1778
  const itemY = i * linesPerItem;
1662
1779
  const optName = options[actualIndex].name;
1663
- if (colors.icon) {
1664
- fb.drawText(optName.charAt(0), 3, itemY, colors.icon);
1780
+ const isSelected = actualIndex === selectedIndex;
1781
+ const defaultColor = isSelected ? selectedTextColor : baseTextColor;
1782
+ const colors = this._optionColors[actualIndex];
1783
+ fb.drawText(`${optName} `, 1, itemY, defaultColor);
1784
+ if (colors?.icon) {
1785
+ fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
1665
1786
  }
1666
- if (colors.name) {
1667
- fb.drawText(optName.slice(2), 5, itemY, colors.name);
1787
+ if (colors?.name) {
1788
+ fb.drawText(optName.slice(2), 3, itemY, colors.name);
1668
1789
  }
1669
1790
  }
1670
1791
  }
@@ -1678,6 +1799,7 @@ class TabBar {
1678
1799
  baseDescriptions;
1679
1800
  processColors;
1680
1801
  inputWaiting = new Set;
1802
+ erroredProcesses = new Set;
1681
1803
  constructor(renderer, names, colors) {
1682
1804
  this.originalNames = names;
1683
1805
  this.names = [...names];
@@ -1716,6 +1838,9 @@ class TabBar {
1716
1838
  if (TERMINAL_STATUSES.has(status) || status === "stopping") {
1717
1839
  this.inputWaiting.delete(name);
1718
1840
  }
1841
+ if (status === "starting") {
1842
+ this.erroredProcesses.delete(name);
1843
+ }
1719
1844
  this.refreshOptions();
1720
1845
  }
1721
1846
  setInputWaiting(name, waiting) {
@@ -1725,6 +1850,13 @@ class TabBar {
1725
1850
  this.inputWaiting.delete(name);
1726
1851
  this.refreshOptions();
1727
1852
  }
1853
+ setError(name, hasError) {
1854
+ if (hasError)
1855
+ this.erroredProcesses.add(name);
1856
+ else
1857
+ this.erroredProcesses.delete(name);
1858
+ this.refreshOptions();
1859
+ }
1728
1860
  getNameAtIndex(index) {
1729
1861
  return this.names[index];
1730
1862
  }
@@ -1753,13 +1885,16 @@ class TabBar {
1753
1885
  getDescription(name) {
1754
1886
  if (this.inputWaiting.has(name))
1755
1887
  return "awaiting input";
1888
+ if (this.erroredProcesses.has(name))
1889
+ return "error detected";
1756
1890
  return this.baseDescriptions.get(name) ?? "pending";
1757
1891
  }
1758
1892
  updateOptionColors() {
1759
1893
  const colors = this.names.map((name) => {
1760
1894
  const status = this.statuses.get(name);
1761
1895
  const waiting = this.inputWaiting.has(name);
1762
- const statusHex = waiting ? "#ffaa00" : STATUS_ICON_HEX[status];
1896
+ const errored = this.erroredProcesses.has(name);
1897
+ const statusHex = waiting ? "#ffaa00" : errored ? "#ff5555" : STATUS_ICON_HEX[status];
1763
1898
  const processHex = this.processColors.get(name);
1764
1899
  return {
1765
1900
  icon: parseColor(statusHex ?? processHex ?? "#888888"),
@@ -1881,6 +2016,8 @@ class App {
1881
2016
  if (this.config.processes[event.name]?.interactive) {
1882
2017
  this.checkInputWaiting(event.name, event.data);
1883
2018
  }
2019
+ } else if (event.type === "error") {
2020
+ this.tabBar.setError(event.name, true);
1884
2021
  } else if (event.type === "status") {
1885
2022
  const state = this.manager.getState(event.name);
1886
2023
  this.tabBar.updateStatus(event.name, event.status, state?.exitCode, state?.restartCount);
@@ -2403,7 +2540,8 @@ Usage:
2403
2540
 
2404
2541
  Options:
2405
2542
  -n, --name <name=command> Add a named process
2406
- -c, --color <colors> Comma-separated colors for processes (hex, e.g. #ff0,#0f0)
2543
+ -c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
2544
+ --colors Auto-assign colors to processes based on their name
2407
2545
  --config <path> Config file path (default: auto-detect)
2408
2546
  -p, --prefix Prefixed output mode (no TUI, for CI/scripts)
2409
2547
  --only <a,b,...> Only run these processes (+ their dependencies)
@@ -2581,6 +2719,13 @@ async function main() {
2581
2719
  if (parsed.only || parsed.exclude) {
2582
2720
  config = filterConfig(config, parsed.only, parsed.exclude);
2583
2721
  }
2722
+ if (parsed.autoColors) {
2723
+ for (const [name, proc] of Object.entries(config.processes)) {
2724
+ if (!proc.color) {
2725
+ proc.color = colorFromName(name);
2726
+ }
2727
+ }
2728
+ }
2584
2729
  const manager = new ProcessManager(config);
2585
2730
  let logWriter;
2586
2731
  if (parsed.logDir) {
package/dist/types.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface NumuxProcessConfig {
14
14
  color?: string | string[];
15
15
  watch?: string | string[];
16
16
  interactive?: boolean;
17
+ errorMatcher?: boolean | string;
17
18
  }
18
19
  /** Config for npm: wildcard entries — command is derived from package.json scripts */
19
20
  export type NumuxScriptPattern = Omit<NumuxProcessConfig, 'command'> & {
@@ -50,4 +51,7 @@ export type ProcessEvent = {
50
51
  type: 'exit';
51
52
  name: string;
52
53
  code: number | null;
54
+ } | {
55
+ type: 'error';
56
+ name: string;
53
57
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",