numux 1.7.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/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.7.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'
@@ -651,6 +656,13 @@ var DEFAULT_ANSI_COLORS = [
651
656
  "\x1B[95m"
652
657
  ];
653
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
+ }
654
666
  function resolveColor(color) {
655
667
  if (typeof color === "string")
656
668
  return color;
@@ -797,7 +809,8 @@ function validateConfig(raw, warnings) {
797
809
  stopSignal: validateStopSignal(p.stopSignal),
798
810
  color: typeof p.color === "string" ? p.color : Array.isArray(p.color) ? p.color : undefined,
799
811
  watch: validateStringOrStringArray(p.watch),
800
- interactive: typeof p.interactive === "boolean" ? p.interactive : false
812
+ interactive: typeof p.interactive === "boolean" ? p.interactive : false,
813
+ errorMatcher: validateErrorMatcher(name, p.errorMatcher)
801
814
  };
802
815
  }
803
816
  return { processes: validated };
@@ -810,6 +823,22 @@ function validateStringOrStringArray(value) {
810
823
  return;
811
824
  }
812
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
+ }
813
842
  var VALID_STOP_SIGNALS = new Set(["SIGTERM", "SIGINT", "SIGHUP"]);
814
843
  function validateStopSignal(value) {
815
844
  if (typeof value === "string" && VALID_STOP_SIGNALS.has(value)) {
@@ -917,8 +946,49 @@ function loadEnvFiles(envFile, cwd) {
917
946
  return merged;
918
947
  }
919
948
 
920
- // src/process/ready.ts
949
+ // src/process/error.ts
921
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;
922
992
  function createReadinessChecker(config) {
923
993
  const pattern = config.readyPattern ? new RegExp(config.readyPattern) : null;
924
994
  const persistent = config.persistent !== false;
@@ -928,8 +998,8 @@ function createReadinessChecker(config) {
928
998
  if (!(persistent && pattern))
929
999
  return false;
930
1000
  outputBuffer += data;
931
- if (outputBuffer.length > BUFFER_CAP) {
932
- outputBuffer = outputBuffer.slice(-BUFFER_CAP);
1001
+ if (outputBuffer.length > BUFFER_CAP2) {
1002
+ outputBuffer = outputBuffer.slice(-BUFFER_CAP2);
933
1003
  }
934
1004
  return pattern.test(outputBuffer);
935
1005
  },
@@ -949,9 +1019,11 @@ class ProcessRunner {
949
1019
  handler;
950
1020
  proc = null;
951
1021
  readiness;
1022
+ errorChecker;
952
1023
  _ready = false;
953
1024
  stopping = false;
954
1025
  decoder = new TextDecoder;
1026
+ errorDecoder = new TextDecoder;
955
1027
  generation = 0;
956
1028
  readyTimer = null;
957
1029
  restarting = false;
@@ -961,6 +1033,7 @@ class ProcessRunner {
961
1033
  this.config = config;
962
1034
  this.handler = handler;
963
1035
  this.readiness = createReadinessChecker(config);
1036
+ this.errorChecker = createErrorChecker(config);
964
1037
  }
965
1038
  get isReady() {
966
1039
  return this._ready;
@@ -995,6 +1068,7 @@ class ProcessRunner {
995
1068
  return;
996
1069
  this.handler.onOutput(data);
997
1070
  this.checkReadiness(data);
1071
+ this.checkError(data);
998
1072
  }
999
1073
  }
1000
1074
  });
@@ -1050,6 +1124,14 @@ class ProcessRunner {
1050
1124
  this.markReady();
1051
1125
  }
1052
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
+ }
1053
1135
  startReadyTimeout(gen) {
1054
1136
  const timeout = this.config.readyTimeout;
1055
1137
  if (!(timeout && this.config.readyPattern) || this.config.persistent === false)
@@ -1108,6 +1190,7 @@ class ProcessRunner {
1108
1190
  this.restarting = false;
1109
1191
  this.readyTimedOut = false;
1110
1192
  this.readiness = createReadinessChecker(this.config);
1193
+ this.errorChecker = createErrorChecker(this.config);
1111
1194
  this.start(cols, rows);
1112
1195
  }
1113
1196
  async stop(timeoutMs = 5000) {
@@ -1277,7 +1360,8 @@ class ProcessManager {
1277
1360
  readyResolved = true;
1278
1361
  onInitialReady();
1279
1362
  }
1280
- }
1363
+ },
1364
+ onError: () => this.emit({ type: "error", name })
1281
1365
  });
1282
1366
  this.runners.set(name, runner);
1283
1367
  }
@@ -1684,20 +1768,24 @@ class ColoredSelectRenderable extends SelectRenderable {
1684
1768
  const scrollOffset = this.scrollOffset;
1685
1769
  const maxVisibleItems = this.maxVisibleItems;
1686
1770
  const linesPerItem = this.linesPerItem;
1771
+ const selectedIndex = this.getSelectedIndex();
1687
1772
  const options = this.options;
1688
1773
  const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
1774
+ const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
1775
+ const selectedTextColor = this._selectedTextColor;
1689
1776
  for (let i = 0;i < visibleCount; i++) {
1690
1777
  const actualIndex = scrollOffset + i;
1691
- const colors = this._optionColors[actualIndex];
1692
- if (!colors)
1693
- continue;
1694
1778
  const itemY = i * linesPerItem;
1695
1779
  const optName = options[actualIndex].name;
1696
- if (colors.icon) {
1697
- 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);
1698
1786
  }
1699
- if (colors.name) {
1700
- fb.drawText(optName.slice(2), 5, itemY, colors.name);
1787
+ if (colors?.name) {
1788
+ fb.drawText(optName.slice(2), 3, itemY, colors.name);
1701
1789
  }
1702
1790
  }
1703
1791
  }
@@ -1711,6 +1799,7 @@ class TabBar {
1711
1799
  baseDescriptions;
1712
1800
  processColors;
1713
1801
  inputWaiting = new Set;
1802
+ erroredProcesses = new Set;
1714
1803
  constructor(renderer, names, colors) {
1715
1804
  this.originalNames = names;
1716
1805
  this.names = [...names];
@@ -1749,6 +1838,9 @@ class TabBar {
1749
1838
  if (TERMINAL_STATUSES.has(status) || status === "stopping") {
1750
1839
  this.inputWaiting.delete(name);
1751
1840
  }
1841
+ if (status === "starting") {
1842
+ this.erroredProcesses.delete(name);
1843
+ }
1752
1844
  this.refreshOptions();
1753
1845
  }
1754
1846
  setInputWaiting(name, waiting) {
@@ -1758,6 +1850,13 @@ class TabBar {
1758
1850
  this.inputWaiting.delete(name);
1759
1851
  this.refreshOptions();
1760
1852
  }
1853
+ setError(name, hasError) {
1854
+ if (hasError)
1855
+ this.erroredProcesses.add(name);
1856
+ else
1857
+ this.erroredProcesses.delete(name);
1858
+ this.refreshOptions();
1859
+ }
1761
1860
  getNameAtIndex(index) {
1762
1861
  return this.names[index];
1763
1862
  }
@@ -1786,13 +1885,16 @@ class TabBar {
1786
1885
  getDescription(name) {
1787
1886
  if (this.inputWaiting.has(name))
1788
1887
  return "awaiting input";
1888
+ if (this.erroredProcesses.has(name))
1889
+ return "error detected";
1789
1890
  return this.baseDescriptions.get(name) ?? "pending";
1790
1891
  }
1791
1892
  updateOptionColors() {
1792
1893
  const colors = this.names.map((name) => {
1793
1894
  const status = this.statuses.get(name);
1794
1895
  const waiting = this.inputWaiting.has(name);
1795
- 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];
1796
1898
  const processHex = this.processColors.get(name);
1797
1899
  return {
1798
1900
  icon: parseColor(statusHex ?? processHex ?? "#888888"),
@@ -1914,6 +2016,8 @@ class App {
1914
2016
  if (this.config.processes[event.name]?.interactive) {
1915
2017
  this.checkInputWaiting(event.name, event.data);
1916
2018
  }
2019
+ } else if (event.type === "error") {
2020
+ this.tabBar.setError(event.name, true);
1917
2021
  } else if (event.type === "status") {
1918
2022
  const state = this.manager.getState(event.name);
1919
2023
  this.tabBar.updateStatus(event.name, event.status, state?.exitCode, state?.restartCount);
@@ -2437,6 +2541,7 @@ Usage:
2437
2541
  Options:
2438
2542
  -n, --name <name=command> Add a named process
2439
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
2440
2545
  --config <path> Config file path (default: auto-detect)
2441
2546
  -p, --prefix Prefixed output mode (no TUI, for CI/scripts)
2442
2547
  --only <a,b,...> Only run these processes (+ their dependencies)
@@ -2614,6 +2719,13 @@ async function main() {
2614
2719
  if (parsed.only || parsed.exclude) {
2615
2720
  config = filterConfig(config, parsed.only, parsed.exclude);
2616
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
+ }
2617
2729
  const manager = new ProcessManager(config);
2618
2730
  let logWriter;
2619
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.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",