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 +127 -15
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
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.
|
|
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/
|
|
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 >
|
|
932
|
-
outputBuffer = outputBuffer.slice(-
|
|
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
|
-
|
|
1697
|
-
|
|
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
|
|
1700
|
-
fb.drawText(optName.slice(2),
|
|
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
|
|
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
|
};
|