numux 1.7.0 → 1.9.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
@@ -265,22 +265,7 @@ Persistent processes that crash are auto-restarted with exponential backoff (1s
265
265
 
266
266
  ## Keybindings
267
267
 
268
- | Key | Action |
269
- |-----|--------|
270
- | `Ctrl+C` | Quit (graceful shutdown) |
271
- | `R` | Restart active process |
272
- | `Shift+R` | Restart all processes |
273
- | `S` | Stop/start active process |
274
- | `L` | Clear active pane output |
275
- | `F` | Search in active pane output |
276
- | `1`–`9` | Jump to tab |
277
- | `Left/Right` | Cycle tabs |
278
- | `PageUp/PageDown` | Scroll output by page |
279
- | `Home/End` | Scroll to top/bottom |
280
-
281
- While searching: type to filter, `Enter`/`Shift+Enter` to navigate matches, `Escape` to close.
282
-
283
- Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
268
+ Keybindings are shown in the status bar at the bottom of the app. Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
284
269
 
285
270
  ## Tab icons
286
271
 
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.9.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
  }
@@ -1465,6 +1549,26 @@ function evaluateCondition(condition) {
1465
1549
  // src/ui/app.ts
1466
1550
  import { BoxRenderable, createCliRenderer } from "@opentui/core";
1467
1551
 
1552
+ // src/ui/keybindings.ts
1553
+ var SHORTCUTS = {
1554
+ restartAll: { key: "r", label: "Shift+R", description: "restart all", shift: true },
1555
+ copy: { key: "y", label: "Y", description: "copy" },
1556
+ search: { key: "f", label: "F", description: "search" },
1557
+ restart: { key: "r", label: "R", description: "restart" },
1558
+ stopStart: { key: "s", label: "S", description: "stop/start" },
1559
+ clear: { key: "l", label: "L", description: "clear" }
1560
+ };
1561
+ var STATUS_HINTS = [
1562
+ ["\u2190\u2192/1-9", "tabs"],
1563
+ [SHORTCUTS.restart.label, SHORTCUTS.restart.description],
1564
+ [SHORTCUTS.stopStart.label, SHORTCUTS.stopStart.description],
1565
+ [SHORTCUTS.search.label, SHORTCUTS.search.description],
1566
+ [SHORTCUTS.copy.label, SHORTCUTS.copy.description],
1567
+ [SHORTCUTS.clear.label, SHORTCUTS.clear.description],
1568
+ ["Ctrl+C", "quit"]
1569
+ ];
1570
+ var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
1571
+
1468
1572
  // src/ui/pane.ts
1469
1573
  import { ScrollBoxRenderable } from "@opentui/core";
1470
1574
  import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
@@ -1474,6 +1578,7 @@ class Pane {
1474
1578
  terminal;
1475
1579
  decoder = new TextDecoder;
1476
1580
  _onScroll = null;
1581
+ _onCopy = null;
1477
1582
  constructor(renderer, name, cols, rows, interactive = false) {
1478
1583
  this.scrollBox = new ScrollBoxRenderable(renderer, {
1479
1584
  id: `pane-${name}`,
@@ -1492,6 +1597,18 @@ class Pane {
1492
1597
  showCursor: interactive,
1493
1598
  trimEnd: true
1494
1599
  });
1600
+ const origOnSelectionChanged = this.terminal.onSelectionChanged.bind(this.terminal);
1601
+ this.terminal.onSelectionChanged = (selection) => {
1602
+ const result = origOnSelectionChanged(selection);
1603
+ if (selection?.isActive && !selection.isDragging) {
1604
+ const text = selection.getSelectedText();
1605
+ if (text) {
1606
+ renderer.copyToClipboardOSC52(text);
1607
+ this._onCopy?.(text);
1608
+ }
1609
+ }
1610
+ return result;
1611
+ };
1495
1612
  this.scrollBox.add(this.terminal);
1496
1613
  }
1497
1614
  feed(data) {
@@ -1520,6 +1637,9 @@ class Pane {
1520
1637
  onScroll(handler) {
1521
1638
  this._onScroll = handler;
1522
1639
  }
1640
+ onCopy(handler) {
1641
+ this._onCopy = handler;
1642
+ }
1523
1643
  show() {
1524
1644
  this.scrollBox.visible = true;
1525
1645
  }
@@ -1583,6 +1703,8 @@ class StatusBar {
1583
1703
  _searchQuery = "";
1584
1704
  _searchMatchCount = 0;
1585
1705
  _searchCurrentIndex = -1;
1706
+ _tempMessage = null;
1707
+ _tempTimer = null;
1586
1708
  constructor(renderer) {
1587
1709
  this.renderable = new TextRenderable(renderer, {
1588
1710
  id: "status-bar",
@@ -1600,13 +1722,25 @@ class StatusBar {
1600
1722
  this._searchCurrentIndex = currentIndex;
1601
1723
  this.renderable.content = this.buildContent();
1602
1724
  }
1725
+ showTemporaryMessage(message, duration = 2000) {
1726
+ if (this._tempTimer)
1727
+ clearTimeout(this._tempTimer);
1728
+ this._tempMessage = message;
1729
+ this.renderable.content = this.buildContent();
1730
+ this._tempTimer = setTimeout(() => {
1731
+ this._tempMessage = null;
1732
+ this._tempTimer = null;
1733
+ this.renderable.content = this.buildContent();
1734
+ }, duration);
1735
+ }
1603
1736
  buildContent() {
1737
+ if (this._tempMessage) {
1738
+ return new StyledText([cyan(this._tempMessage)]);
1739
+ }
1604
1740
  if (this._searchMode) {
1605
1741
  return this.buildSearchContent();
1606
1742
  }
1607
- return new StyledText([
1608
- plain("\u2190\u2192/1-9: tabs R: restart S: stop/start F: search L: clear Ctrl+C: quit")
1609
- ]);
1743
+ return new StyledText([plain(STATUS_BAR_TEXT)]);
1610
1744
  }
1611
1745
  buildSearchContent() {
1612
1746
  const chunks = [];
@@ -1684,20 +1818,24 @@ class ColoredSelectRenderable extends SelectRenderable {
1684
1818
  const scrollOffset = this.scrollOffset;
1685
1819
  const maxVisibleItems = this.maxVisibleItems;
1686
1820
  const linesPerItem = this.linesPerItem;
1821
+ const selectedIndex = this.getSelectedIndex();
1687
1822
  const options = this.options;
1688
1823
  const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
1824
+ const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
1825
+ const selectedTextColor = this._selectedTextColor;
1689
1826
  for (let i = 0;i < visibleCount; i++) {
1690
1827
  const actualIndex = scrollOffset + i;
1691
- const colors = this._optionColors[actualIndex];
1692
- if (!colors)
1693
- continue;
1694
1828
  const itemY = i * linesPerItem;
1695
1829
  const optName = options[actualIndex].name;
1696
- if (colors.icon) {
1697
- fb.drawText(optName.charAt(0), 3, itemY, colors.icon);
1830
+ const isSelected = actualIndex === selectedIndex;
1831
+ const defaultColor = isSelected ? selectedTextColor : baseTextColor;
1832
+ const colors = this._optionColors[actualIndex];
1833
+ fb.drawText(`${optName} `, 1, itemY, defaultColor);
1834
+ if (colors?.icon) {
1835
+ fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
1698
1836
  }
1699
- if (colors.name) {
1700
- fb.drawText(optName.slice(2), 5, itemY, colors.name);
1837
+ if (colors?.name) {
1838
+ fb.drawText(optName.slice(2), 3, itemY, colors.name);
1701
1839
  }
1702
1840
  }
1703
1841
  }
@@ -1711,6 +1849,7 @@ class TabBar {
1711
1849
  baseDescriptions;
1712
1850
  processColors;
1713
1851
  inputWaiting = new Set;
1852
+ erroredProcesses = new Set;
1714
1853
  constructor(renderer, names, colors) {
1715
1854
  this.originalNames = names;
1716
1855
  this.names = [...names];
@@ -1749,6 +1888,9 @@ class TabBar {
1749
1888
  if (TERMINAL_STATUSES.has(status) || status === "stopping") {
1750
1889
  this.inputWaiting.delete(name);
1751
1890
  }
1891
+ if (status === "starting") {
1892
+ this.erroredProcesses.delete(name);
1893
+ }
1752
1894
  this.refreshOptions();
1753
1895
  }
1754
1896
  setInputWaiting(name, waiting) {
@@ -1758,6 +1900,13 @@ class TabBar {
1758
1900
  this.inputWaiting.delete(name);
1759
1901
  this.refreshOptions();
1760
1902
  }
1903
+ setError(name, hasError) {
1904
+ if (hasError)
1905
+ this.erroredProcesses.add(name);
1906
+ else
1907
+ this.erroredProcesses.delete(name);
1908
+ this.refreshOptions();
1909
+ }
1761
1910
  getNameAtIndex(index) {
1762
1911
  return this.names[index];
1763
1912
  }
@@ -1786,13 +1935,16 @@ class TabBar {
1786
1935
  getDescription(name) {
1787
1936
  if (this.inputWaiting.has(name))
1788
1937
  return "awaiting input";
1938
+ if (this.erroredProcesses.has(name))
1939
+ return "error detected";
1789
1940
  return this.baseDescriptions.get(name) ?? "pending";
1790
1941
  }
1791
1942
  updateOptionColors() {
1792
1943
  const colors = this.names.map((name) => {
1793
1944
  const status = this.statuses.get(name);
1794
1945
  const waiting = this.inputWaiting.has(name);
1795
- const statusHex = waiting ? "#ffaa00" : STATUS_ICON_HEX[status];
1946
+ const errored = this.erroredProcesses.has(name);
1947
+ const statusHex = waiting ? "#ffaa00" : errored ? "#ff5555" : STATUS_ICON_HEX[status];
1796
1948
  const processHex = this.processColors.get(name);
1797
1949
  return {
1798
1950
  icon: parseColor(statusHex ?? processHex ?? "#888888"),
@@ -1895,6 +2047,7 @@ class App {
1895
2047
  for (const name of this.names) {
1896
2048
  const interactive = this.config.processes[name].interactive === true;
1897
2049
  const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
2050
+ pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
1898
2051
  this.panes.set(name, pane);
1899
2052
  paneContainer.add(pane.scrollBox);
1900
2053
  }
@@ -1914,6 +2067,8 @@ class App {
1914
2067
  if (this.config.processes[event.name]?.interactive) {
1915
2068
  this.checkInputWaiting(event.name, event.data);
1916
2069
  }
2070
+ } else if (event.type === "error") {
2071
+ this.tabBar.setError(event.name, true);
1917
2072
  } else if (event.type === "status") {
1918
2073
  const state = this.manager.getState(event.name);
1919
2074
  this.tabBar.updateStatus(event.name, event.status, state?.exitCode, state?.restartCount);
@@ -1956,19 +2111,23 @@ class App {
1956
2111
  const isInteractive = this.config.processes[this.activePane]?.interactive === true;
1957
2112
  if (!isInteractive) {
1958
2113
  const name = key.name.toLowerCase();
1959
- if (key.shift && name === "r") {
2114
+ if (key.shift && name === SHORTCUTS.restartAll.key) {
1960
2115
  this.manager.restartAll(this.termCols, this.termRows);
1961
2116
  return;
1962
2117
  }
1963
- if (name === "f") {
2118
+ if (name === SHORTCUTS.copy.key) {
2119
+ this.copySelection();
2120
+ return;
2121
+ }
2122
+ if (name === SHORTCUTS.search.key) {
1964
2123
  this.enterSearch();
1965
2124
  return;
1966
2125
  }
1967
- if (name === "r") {
2126
+ if (name === SHORTCUTS.restart.key) {
1968
2127
  this.manager.restart(this.activePane, this.termCols, this.termRows);
1969
2128
  return;
1970
2129
  }
1971
- if (name === "s") {
2130
+ if (name === SHORTCUTS.stopStart.key) {
1972
2131
  const state = this.manager.getState(this.activePane);
1973
2132
  if (state?.status === "stopped" || state?.status === "finished" || state?.status === "failed") {
1974
2133
  this.manager.start(this.activePane, this.termCols, this.termRows);
@@ -1977,7 +2136,7 @@ class App {
1977
2136
  }
1978
2137
  return;
1979
2138
  }
1980
- if (name === "l") {
2139
+ if (name === SHORTCUTS.clear.key) {
1981
2140
  this.panes.get(this.activePane)?.clear();
1982
2141
  return;
1983
2142
  }
@@ -2065,6 +2224,18 @@ class App {
2065
2224
  this.tabBar.setInputWaiting(name, false);
2066
2225
  }
2067
2226
  }
2227
+ copySelection() {
2228
+ const selection = this.renderer.getSelection();
2229
+ if (!selection?.isActive)
2230
+ return false;
2231
+ const text = selection.getSelectedText();
2232
+ if (!text)
2233
+ return false;
2234
+ this.renderer.copyToClipboardOSC52(text);
2235
+ this.renderer.clearSelection();
2236
+ this.statusBar.showTemporaryMessage("Copied!");
2237
+ return true;
2238
+ }
2068
2239
  enterSearch() {
2069
2240
  this.searchMode = true;
2070
2241
  this.searchQuery = "";
@@ -2437,6 +2608,7 @@ Usage:
2437
2608
  Options:
2438
2609
  -n, --name <name=command> Add a named process
2439
2610
  -c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
2611
+ --colors Auto-assign colors to processes based on their name
2440
2612
  --config <path> Config file path (default: auto-detect)
2441
2613
  -p, --prefix Prefixed output mode (no TUI, for CI/scripts)
2442
2614
  --only <a,b,...> Only run these processes (+ their dependencies)
@@ -2614,6 +2786,13 @@ async function main() {
2614
2786
  if (parsed.only || parsed.exclude) {
2615
2787
  config = filterConfig(config, parsed.only, parsed.exclude);
2616
2788
  }
2789
+ if (parsed.autoColors) {
2790
+ for (const [name, proc] of Object.entries(config.processes)) {
2791
+ if (!proc.color) {
2792
+ proc.color = colorFromName(name);
2793
+ }
2794
+ }
2795
+ }
2617
2796
  const manager = new ProcessManager(config);
2618
2797
  let logWriter;
2619
2798
  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.9.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",