numux 1.21.0 → 1.23.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 +2 -0
- package/dist/numux.js +160 -89
- package/dist/types.d.ts +10 -1
- package/dist/utils/color.d.ts +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,6 +164,7 @@ Template properties (color, env, dependsOn, etc.) are inherited by all matched p
|
|
|
164
164
|
| `--exclude <a,b,...>` | Exclude these processes |
|
|
165
165
|
| `--kill-others` | Kill all processes when any exits |
|
|
166
166
|
| `--no-restart` | Disable auto-restart for crashed processes |
|
|
167
|
+
| `-s, --sort <mode>` | Tab display order: `config` (default), `alphabetical`, `topological` |
|
|
167
168
|
| `--no-watch` | Disable file watching even if config has `watch` patterns |
|
|
168
169
|
| `-t, --timestamps` | Add `[HH:MM:SS]` timestamps to prefixed output |
|
|
169
170
|
| `--log-dir <path>` | Write per-process output to `<path>/<name>.log` |
|
|
@@ -198,6 +199,7 @@ Top-level options apply to all processes (process-level settings override):
|
|
|
198
199
|
| `stopSignal` | `'SIGTERM' \| 'SIGINT' \| 'SIGHUP'` | Stop signal for all processes (default: `'SIGTERM'`) |
|
|
199
200
|
| `errorMatcher` | `boolean \| string` | Error detection for all processes (`true` = ANSI red, string = regex) |
|
|
200
201
|
| `watch` | `string \| string[]` | Watch patterns for all processes (process `watch` replaces if set) |
|
|
202
|
+
| `sort` | `'config' \| 'alphabetical' \| 'topological'` | Tab display order (default: `'config'` — definition order) |
|
|
201
203
|
|
|
202
204
|
```ts
|
|
203
205
|
export default defineConfig({
|
package/dist/numux.js
CHANGED
|
@@ -36,7 +36,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
|
|
|
36
36
|
var require_package = __commonJS((exports, module) => {
|
|
37
37
|
module.exports = {
|
|
38
38
|
name: "numux",
|
|
39
|
-
version: "1.
|
|
39
|
+
version: "1.23.0",
|
|
40
40
|
description: "Terminal multiplexer with dependency orchestration",
|
|
41
41
|
type: "module",
|
|
42
42
|
license: "MIT",
|
|
@@ -95,6 +95,15 @@ import { resolve as resolve8 } from "path";
|
|
|
95
95
|
// src/cli-flags.ts
|
|
96
96
|
var commaSplit = (raw) => raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
97
97
|
var FLAGS = [
|
|
98
|
+
{
|
|
99
|
+
type: "value",
|
|
100
|
+
long: "--sort",
|
|
101
|
+
short: "-s",
|
|
102
|
+
key: "sort",
|
|
103
|
+
description: "Tab display order",
|
|
104
|
+
valueName: "<config|alphabetical|topological>",
|
|
105
|
+
completionHint: "none"
|
|
106
|
+
},
|
|
98
107
|
{
|
|
99
108
|
type: "value",
|
|
100
109
|
long: "--workspace",
|
|
@@ -906,12 +915,12 @@ function findCycle(remaining, config) {
|
|
|
906
915
|
// src/utils/color.ts
|
|
907
916
|
var BASIC_COLORS = {
|
|
908
917
|
black: "#000000",
|
|
909
|
-
red: "#
|
|
910
|
-
green: "#
|
|
911
|
-
yellow: "#
|
|
912
|
-
blue: "#
|
|
913
|
-
magenta: "#
|
|
914
|
-
cyan: "#
|
|
918
|
+
red: "#ff5555",
|
|
919
|
+
green: "#00cc00",
|
|
920
|
+
yellow: "#cccc00",
|
|
921
|
+
blue: "#0000cc",
|
|
922
|
+
magenta: "#cc00cc",
|
|
923
|
+
cyan: "#00cccc",
|
|
915
924
|
white: "#ffffff",
|
|
916
925
|
gray: "#808080",
|
|
917
926
|
grey: "#808080",
|
|
@@ -940,35 +949,35 @@ function hexToAnsi(hex) {
|
|
|
940
949
|
}
|
|
941
950
|
var HEX_COLOR_RE = /^#?[0-9a-fA-F]{6}$/;
|
|
942
951
|
var STATUS_ANSI = {
|
|
943
|
-
ready:
|
|
944
|
-
running:
|
|
945
|
-
finished:
|
|
946
|
-
failed:
|
|
947
|
-
stopped:
|
|
948
|
-
skipped:
|
|
952
|
+
ready: hexToAnsi(BASIC_COLORS.green),
|
|
953
|
+
running: hexToAnsi(BASIC_COLORS.cyan),
|
|
954
|
+
finished: hexToAnsi(BASIC_COLORS.green),
|
|
955
|
+
failed: hexToAnsi(BASIC_COLORS.red),
|
|
956
|
+
stopped: hexToAnsi(BASIC_COLORS.gray),
|
|
957
|
+
skipped: hexToAnsi(BASIC_COLORS.gray)
|
|
949
958
|
};
|
|
950
959
|
var ANSI_RESET = "\x1B[0m";
|
|
951
960
|
var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()#][0-9A-Za-z]|\x1b[A-Za-z><=]/g;
|
|
952
961
|
function stripAnsi(str) {
|
|
953
962
|
return str.replace(ANSI_RE, "");
|
|
954
963
|
}
|
|
955
|
-
var
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
+
var DEFAULT_PALETTE = [
|
|
965
|
+
BASIC_COLORS.cyan,
|
|
966
|
+
BASIC_COLORS.yellow,
|
|
967
|
+
BASIC_COLORS.magenta,
|
|
968
|
+
BASIC_COLORS.blue,
|
|
969
|
+
BASIC_COLORS.green,
|
|
970
|
+
BASIC_COLORS.red,
|
|
971
|
+
BASIC_COLORS.orange,
|
|
972
|
+
BASIC_COLORS.purple
|
|
964
973
|
];
|
|
965
|
-
var
|
|
974
|
+
var DEFAULT_ANSI_COLORS = DEFAULT_PALETTE.map(hexToAnsi);
|
|
966
975
|
function colorFromName(name) {
|
|
967
976
|
let hash = 0;
|
|
968
977
|
for (let i = 0;i < name.length; i++) {
|
|
969
978
|
hash = (hash << 5) - hash + name.charCodeAt(i) | 0;
|
|
970
979
|
}
|
|
971
|
-
return
|
|
980
|
+
return DEFAULT_PALETTE[Math.abs(hash) % DEFAULT_PALETTE.length];
|
|
972
981
|
}
|
|
973
982
|
function resolveColor(color) {
|
|
974
983
|
if (typeof color === "string")
|
|
@@ -1009,9 +1018,9 @@ function buildProcessHexColorMap(names, config) {
|
|
|
1009
1018
|
if (hex)
|
|
1010
1019
|
map.set(name, hex);
|
|
1011
1020
|
else
|
|
1012
|
-
map.set(name,
|
|
1021
|
+
map.set(name, DEFAULT_PALETTE[paletteIndex++ % DEFAULT_PALETTE.length]);
|
|
1013
1022
|
} else {
|
|
1014
|
-
map.set(name,
|
|
1023
|
+
map.set(name, DEFAULT_PALETTE[paletteIndex % DEFAULT_PALETTE.length]);
|
|
1015
1024
|
paletteIndex++;
|
|
1016
1025
|
}
|
|
1017
1026
|
}
|
|
@@ -1049,6 +1058,7 @@ function validateConfig(raw, warnings) {
|
|
|
1049
1058
|
}
|
|
1050
1059
|
globalEnv = config.env;
|
|
1051
1060
|
}
|
|
1061
|
+
const sort = validateSort(config.sort);
|
|
1052
1062
|
const validated = {};
|
|
1053
1063
|
for (const name of names) {
|
|
1054
1064
|
let proc = processes[name];
|
|
@@ -1146,7 +1156,7 @@ function validateConfig(raw, warnings) {
|
|
|
1146
1156
|
showCommand
|
|
1147
1157
|
};
|
|
1148
1158
|
}
|
|
1149
|
-
return { processes: validated };
|
|
1159
|
+
return { ...sort ? { sort } : {}, processes: validated };
|
|
1150
1160
|
}
|
|
1151
1161
|
function validateStringOrStringArray(value) {
|
|
1152
1162
|
if (typeof value === "string")
|
|
@@ -1176,6 +1186,16 @@ function validateErrorMatcher(name, value) {
|
|
|
1176
1186
|
}
|
|
1177
1187
|
return;
|
|
1178
1188
|
}
|
|
1189
|
+
var VALID_SORT_VALUES = new Set(["config", "alphabetical", "topological"]);
|
|
1190
|
+
function validateSort(value) {
|
|
1191
|
+
if (typeof value === "string") {
|
|
1192
|
+
if (!VALID_SORT_VALUES.has(value)) {
|
|
1193
|
+
throw new Error(`sort must be one of: ${[...VALID_SORT_VALUES].join(", ")}. Got "${value}"`);
|
|
1194
|
+
}
|
|
1195
|
+
return value;
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1179
1199
|
var VALID_PLATFORMS = new Set(["aix", "darwin", "freebsd", "linux", "openbsd", "sunos", "win32"]);
|
|
1180
1200
|
function validatePlatform(name, value) {
|
|
1181
1201
|
const arr = validateStringOrStringArray(value);
|
|
@@ -1743,7 +1763,14 @@ class ProcessManager {
|
|
|
1743
1763
|
return [...this.states.values()];
|
|
1744
1764
|
}
|
|
1745
1765
|
getProcessNames() {
|
|
1746
|
-
|
|
1766
|
+
switch (this.config.sort) {
|
|
1767
|
+
case "alphabetical":
|
|
1768
|
+
return Object.keys(this.config.processes).sort();
|
|
1769
|
+
case "topological":
|
|
1770
|
+
return this.tiers.flat();
|
|
1771
|
+
default:
|
|
1772
|
+
return Object.keys(this.config.processes);
|
|
1773
|
+
}
|
|
1747
1774
|
}
|
|
1748
1775
|
async startAll(cols, rows) {
|
|
1749
1776
|
log("Starting all processes");
|
|
@@ -2153,8 +2180,6 @@ class Pane {
|
|
|
2153
2180
|
_onScroll = null;
|
|
2154
2181
|
_onCopy = null;
|
|
2155
2182
|
_onLinkClick = null;
|
|
2156
|
-
_textLines = null;
|
|
2157
|
-
_textLinesLower = null;
|
|
2158
2183
|
constructor(renderer, name, cols, rows, interactive = false) {
|
|
2159
2184
|
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
2160
2185
|
id: `pane-${name}`,
|
|
@@ -2201,14 +2226,10 @@ class Pane {
|
|
|
2201
2226
|
feed(data) {
|
|
2202
2227
|
const text = this.decoder.decode(data, { stream: true });
|
|
2203
2228
|
this.terminal.feed(text);
|
|
2204
|
-
this._textLines = null;
|
|
2205
|
-
this._textLinesLower = null;
|
|
2206
2229
|
}
|
|
2207
2230
|
resize(cols, rows) {
|
|
2208
2231
|
this.terminal.cols = cols;
|
|
2209
2232
|
this.terminal.rows = rows;
|
|
2210
|
-
this._textLines = null;
|
|
2211
|
-
this._textLinesLower = null;
|
|
2212
2233
|
}
|
|
2213
2234
|
get isAtBottom() {
|
|
2214
2235
|
const { scrollTop, scrollHeight, viewport } = this.scrollBox;
|
|
@@ -2238,16 +2259,13 @@ class Pane {
|
|
|
2238
2259
|
this._onLinkClick = handler;
|
|
2239
2260
|
}
|
|
2240
2261
|
getLinkAtMouse(localX, localY) {
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
this._textLines = text.split(`
|
|
2262
|
+
const text = this.terminal.getText();
|
|
2263
|
+
const lines = text.split(`
|
|
2244
2264
|
`);
|
|
2245
|
-
this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
|
|
2246
|
-
}
|
|
2247
2265
|
const lineIndex = Math.floor(this.scrollBox.scrollTop) + localY;
|
|
2248
|
-
if (lineIndex < 0 || lineIndex >=
|
|
2266
|
+
if (lineIndex < 0 || lineIndex >= lines.length)
|
|
2249
2267
|
return null;
|
|
2250
|
-
return findLinkAtPosition(
|
|
2268
|
+
return findLinkAtPosition(lines[lineIndex], localX);
|
|
2251
2269
|
}
|
|
2252
2270
|
show() {
|
|
2253
2271
|
this.scrollBox.visible = true;
|
|
@@ -2255,30 +2273,6 @@ class Pane {
|
|
|
2255
2273
|
hide() {
|
|
2256
2274
|
this.scrollBox.visible = false;
|
|
2257
2275
|
}
|
|
2258
|
-
search(query) {
|
|
2259
|
-
if (!query)
|
|
2260
|
-
return [];
|
|
2261
|
-
if (!this._textLines) {
|
|
2262
|
-
const text = this.terminal.getText();
|
|
2263
|
-
this._textLines = text.split(`
|
|
2264
|
-
`);
|
|
2265
|
-
this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
|
|
2266
|
-
}
|
|
2267
|
-
const lines = this._textLinesLower;
|
|
2268
|
-
const matches = [];
|
|
2269
|
-
const lowerQuery = query.toLowerCase();
|
|
2270
|
-
for (let line = 0;line < lines.length; line++) {
|
|
2271
|
-
let pos = 0;
|
|
2272
|
-
while (true) {
|
|
2273
|
-
const idx = lines[line].indexOf(lowerQuery, pos);
|
|
2274
|
-
if (idx === -1)
|
|
2275
|
-
break;
|
|
2276
|
-
matches.push({ line, start: idx, end: idx + query.length });
|
|
2277
|
-
pos = idx + 1;
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
return matches;
|
|
2281
|
-
}
|
|
2282
2276
|
setHighlights(matches, currentIndex) {
|
|
2283
2277
|
const firstVisible = Math.max(0, Math.floor(this.scrollBox.scrollTop) - 2);
|
|
2284
2278
|
const lastVisible = Math.ceil(this.scrollBox.scrollTop + this.scrollBox.viewport.height) + 2;
|
|
@@ -2307,8 +2301,6 @@ class Pane {
|
|
|
2307
2301
|
}
|
|
2308
2302
|
clear() {
|
|
2309
2303
|
this.terminal.reset();
|
|
2310
|
-
this._textLines = null;
|
|
2311
|
-
this._textLinesLower = null;
|
|
2312
2304
|
}
|
|
2313
2305
|
destroy() {
|
|
2314
2306
|
this.terminal.destroy();
|
|
@@ -2637,6 +2629,7 @@ class App {
|
|
|
2637
2629
|
termRows = 24;
|
|
2638
2630
|
sidebarWidth = 20;
|
|
2639
2631
|
config;
|
|
2632
|
+
logWriter;
|
|
2640
2633
|
resizeTimer = null;
|
|
2641
2634
|
searchTimer = null;
|
|
2642
2635
|
searchMode = false;
|
|
@@ -2645,9 +2638,10 @@ class App {
|
|
|
2645
2638
|
searchIndex = -1;
|
|
2646
2639
|
inputWaitTimers = new Map;
|
|
2647
2640
|
awaitingInput = new Set;
|
|
2648
|
-
constructor(manager, config) {
|
|
2641
|
+
constructor(manager, config, logWriter) {
|
|
2649
2642
|
this.manager = manager;
|
|
2650
2643
|
this.config = config;
|
|
2644
|
+
this.logWriter = logWriter;
|
|
2651
2645
|
this.names = manager.getProcessNames();
|
|
2652
2646
|
}
|
|
2653
2647
|
async start() {
|
|
@@ -2805,6 +2799,7 @@ class App {
|
|
|
2805
2799
|
}
|
|
2806
2800
|
if (name === SHORTCUTS.clear.key) {
|
|
2807
2801
|
this.panes.get(this.activePane)?.clear();
|
|
2802
|
+
this.logWriter.truncate(this.activePane);
|
|
2808
2803
|
return;
|
|
2809
2804
|
}
|
|
2810
2805
|
const num = Number.parseInt(name, 10);
|
|
@@ -2977,14 +2972,16 @@ class App {
|
|
|
2977
2972
|
this.runSearch();
|
|
2978
2973
|
}, 100);
|
|
2979
2974
|
}
|
|
2980
|
-
runSearch() {
|
|
2975
|
+
async runSearch() {
|
|
2981
2976
|
if (!this.activePane)
|
|
2982
2977
|
return;
|
|
2983
|
-
const
|
|
2984
|
-
|
|
2978
|
+
const query = this.searchQuery;
|
|
2979
|
+
const activeName = this.activePane;
|
|
2980
|
+
const matches = await this.logWriter.search(activeName, query);
|
|
2981
|
+
if (!this.searchMode || this.searchQuery !== query || this.activePane !== activeName)
|
|
2985
2982
|
return;
|
|
2986
|
-
this.searchMatches =
|
|
2987
|
-
this.searchIndex =
|
|
2983
|
+
this.searchMatches = matches;
|
|
2984
|
+
this.searchIndex = matches.length > 0 ? 0 : -1;
|
|
2988
2985
|
this.updateSearchHighlights();
|
|
2989
2986
|
if (this.searchIndex >= 0) {
|
|
2990
2987
|
this.scrollToCurrentMatch();
|
|
@@ -3152,7 +3149,7 @@ class PrefixDisplay {
|
|
|
3152
3149
|
const allDone = states.every((s) => s.status === "stopped" || s.status === "finished" || s.status === "failed" || s.status === "skipped");
|
|
3153
3150
|
if (allDone) {
|
|
3154
3151
|
this.printSummary();
|
|
3155
|
-
this.logWriter?.
|
|
3152
|
+
this.logWriter?.cleanup();
|
|
3156
3153
|
const anyFailed = states.some((s) => s.status === "failed");
|
|
3157
3154
|
process.exit(anyFailed ? 1 : 0);
|
|
3158
3155
|
}
|
|
@@ -3168,7 +3165,7 @@ class PrefixDisplay {
|
|
|
3168
3165
|
this.flushBuffer(name);
|
|
3169
3166
|
}
|
|
3170
3167
|
this.printSummary();
|
|
3171
|
-
this.logWriter?.
|
|
3168
|
+
this.logWriter?.cleanup();
|
|
3172
3169
|
process.exit(code === 0 ? 0 : 1);
|
|
3173
3170
|
});
|
|
3174
3171
|
}
|
|
@@ -3220,24 +3217,31 @@ ${DIM}Done in ${elapsed}${RESET}
|
|
|
3220
3217
|
for (const name of this.manager.getProcessNames()) {
|
|
3221
3218
|
this.flushBuffer(name);
|
|
3222
3219
|
}
|
|
3223
|
-
this.logWriter?.
|
|
3220
|
+
this.logWriter?.cleanup();
|
|
3224
3221
|
const anyFailed = this.manager.getAllStates().some((s) => s.status === "failed");
|
|
3225
3222
|
process.exit(anyFailed ? 1 : 0);
|
|
3226
3223
|
}
|
|
3227
3224
|
}
|
|
3228
3225
|
|
|
3229
3226
|
// src/utils/log-writer.ts
|
|
3230
|
-
import { closeSync, mkdirSync as mkdirSync2, openSync, writeSync } from "fs";
|
|
3227
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, writeSync } from "fs";
|
|
3228
|
+
import { tmpdir } from "os";
|
|
3231
3229
|
import { join } from "path";
|
|
3232
3230
|
class LogWriter {
|
|
3233
3231
|
dir;
|
|
3232
|
+
isTemp;
|
|
3234
3233
|
files = new Map;
|
|
3235
3234
|
decoder = new TextDecoder;
|
|
3236
3235
|
encoder = new TextEncoder;
|
|
3237
|
-
constructor(dir) {
|
|
3236
|
+
constructor(dir, isTemp = false) {
|
|
3238
3237
|
this.dir = dir;
|
|
3238
|
+
this.isTemp = isTemp;
|
|
3239
3239
|
mkdirSync2(dir, { recursive: true });
|
|
3240
3240
|
}
|
|
3241
|
+
static createTemp() {
|
|
3242
|
+
const dir = join(tmpdir(), `numux-${process.pid}`);
|
|
3243
|
+
return new LogWriter(dir, true);
|
|
3244
|
+
}
|
|
3241
3245
|
errored = false;
|
|
3242
3246
|
handleEvent = (event) => {
|
|
3243
3247
|
if (event.type !== "output" || this.errored)
|
|
@@ -3258,12 +3262,81 @@ class LogWriter {
|
|
|
3258
3262
|
`);
|
|
3259
3263
|
}
|
|
3260
3264
|
};
|
|
3265
|
+
getLogPath(name) {
|
|
3266
|
+
if (this.files.has(name)) {
|
|
3267
|
+
return join(this.dir, `${name}.log`);
|
|
3268
|
+
}
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
async search(name, query) {
|
|
3272
|
+
if (!query)
|
|
3273
|
+
return [];
|
|
3274
|
+
const path = this.getLogPath(name);
|
|
3275
|
+
if (!path)
|
|
3276
|
+
return [];
|
|
3277
|
+
try {
|
|
3278
|
+
const proc = Bun.spawn(["grep", "-inF", query, path], {
|
|
3279
|
+
stdout: "pipe",
|
|
3280
|
+
stderr: "ignore"
|
|
3281
|
+
});
|
|
3282
|
+
const output = await new Response(proc.stdout).text();
|
|
3283
|
+
await proc.exited;
|
|
3284
|
+
const matches = [];
|
|
3285
|
+
const lowerQuery = query.toLowerCase();
|
|
3286
|
+
for (const line of output.split(`
|
|
3287
|
+
`)) {
|
|
3288
|
+
if (!line)
|
|
3289
|
+
continue;
|
|
3290
|
+
const colonIdx = line.indexOf(":");
|
|
3291
|
+
if (colonIdx === -1)
|
|
3292
|
+
continue;
|
|
3293
|
+
const lineNumber = Number.parseInt(line.slice(0, colonIdx), 10);
|
|
3294
|
+
if (Number.isNaN(lineNumber))
|
|
3295
|
+
continue;
|
|
3296
|
+
const lineText = line.slice(colonIdx + 1).toLowerCase();
|
|
3297
|
+
let pos = 0;
|
|
3298
|
+
while (true) {
|
|
3299
|
+
const idx = lineText.indexOf(lowerQuery, pos);
|
|
3300
|
+
if (idx === -1)
|
|
3301
|
+
break;
|
|
3302
|
+
matches.push({
|
|
3303
|
+
line: lineNumber - 1,
|
|
3304
|
+
start: idx,
|
|
3305
|
+
end: idx + query.length
|
|
3306
|
+
});
|
|
3307
|
+
pos = idx + 1;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
return matches;
|
|
3311
|
+
} catch {
|
|
3312
|
+
return [];
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
truncate(name) {
|
|
3316
|
+
const fd = this.files.get(name);
|
|
3317
|
+
if (fd === undefined)
|
|
3318
|
+
return;
|
|
3319
|
+
try {
|
|
3320
|
+
closeSync(fd);
|
|
3321
|
+
const path = join(this.dir, `${name}.log`);
|
|
3322
|
+
const newFd = openSync(path, "w");
|
|
3323
|
+
this.files.set(name, newFd);
|
|
3324
|
+
} catch {}
|
|
3325
|
+
}
|
|
3261
3326
|
close() {
|
|
3262
3327
|
for (const fd of this.files.values()) {
|
|
3263
3328
|
closeSync(fd);
|
|
3264
3329
|
}
|
|
3265
3330
|
this.files.clear();
|
|
3266
3331
|
}
|
|
3332
|
+
cleanup() {
|
|
3333
|
+
this.close();
|
|
3334
|
+
if (this.isTemp) {
|
|
3335
|
+
try {
|
|
3336
|
+
rmSync(this.dir, { recursive: true });
|
|
3337
|
+
} catch {}
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3267
3340
|
}
|
|
3268
3341
|
|
|
3269
3342
|
// src/utils/shutdown.ts
|
|
@@ -3275,7 +3348,7 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
3275
3348
|
}
|
|
3276
3349
|
shuttingDown = true;
|
|
3277
3350
|
app.shutdown().finally(() => {
|
|
3278
|
-
logWriter?.
|
|
3351
|
+
logWriter?.cleanup();
|
|
3279
3352
|
process.exit(app.hasFailures() ? 1 : 0);
|
|
3280
3353
|
});
|
|
3281
3354
|
};
|
|
@@ -3286,7 +3359,7 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
3286
3359
|
process.stderr.write(`numux: unexpected error: ${err?.stack ?? err}
|
|
3287
3360
|
`);
|
|
3288
3361
|
app.shutdown().finally(() => {
|
|
3289
|
-
logWriter?.
|
|
3362
|
+
logWriter?.cleanup();
|
|
3290
3363
|
process.exit(1);
|
|
3291
3364
|
});
|
|
3292
3365
|
});
|
|
@@ -3296,7 +3369,7 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
3296
3369
|
process.stderr.write(`numux: unhandled rejection: ${message}
|
|
3297
3370
|
`);
|
|
3298
3371
|
app.shutdown().finally(() => {
|
|
3299
|
-
logWriter?.
|
|
3372
|
+
logWriter?.cleanup();
|
|
3300
3373
|
process.exit(1);
|
|
3301
3374
|
});
|
|
3302
3375
|
});
|
|
@@ -3479,6 +3552,9 @@ async function main() {
|
|
|
3479
3552
|
}
|
|
3480
3553
|
}
|
|
3481
3554
|
}
|
|
3555
|
+
if (parsed.sort) {
|
|
3556
|
+
config.sort = parsed.sort;
|
|
3557
|
+
}
|
|
3482
3558
|
if (parsed.envFile !== undefined) {
|
|
3483
3559
|
for (const proc of Object.values(config.processes)) {
|
|
3484
3560
|
proc.envFile = parsed.envFile;
|
|
@@ -3500,10 +3576,7 @@ async function main() {
|
|
|
3500
3576
|
}
|
|
3501
3577
|
}
|
|
3502
3578
|
const manager = new ProcessManager(config);
|
|
3503
|
-
|
|
3504
|
-
if (parsed.logDir) {
|
|
3505
|
-
logWriter = new LogWriter(parsed.logDir);
|
|
3506
|
-
}
|
|
3579
|
+
const logWriter = parsed.logDir ? new LogWriter(parsed.logDir) : LogWriter.createTemp();
|
|
3507
3580
|
printWarnings(warnings);
|
|
3508
3581
|
if (parsed.prefix) {
|
|
3509
3582
|
if (!parsed.noRestart) {
|
|
@@ -3518,10 +3591,8 @@ async function main() {
|
|
|
3518
3591
|
});
|
|
3519
3592
|
await display.start();
|
|
3520
3593
|
} else {
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
}
|
|
3524
|
-
const app = new App(manager, config);
|
|
3594
|
+
manager.on(logWriter.handleEvent);
|
|
3595
|
+
const app = new App(manager, config, logWriter);
|
|
3525
3596
|
setupShutdownHandlers(app, logWriter);
|
|
3526
3597
|
await app.start();
|
|
3527
3598
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Color } from './utils/color';
|
|
1
2
|
export interface NumuxProcessConfig<K extends string = string> {
|
|
2
3
|
/** Shell command to run. Supports `$dep.group` references from dependency capture groups */
|
|
3
4
|
command: string;
|
|
@@ -39,7 +40,7 @@ export interface NumuxProcessConfig<K extends string = string> {
|
|
|
39
40
|
*/
|
|
40
41
|
stopSignal?: 'SIGTERM' | 'SIGINT' | 'SIGHUP';
|
|
41
42
|
/** Hex color (e.g. `"#ff6600"`) or color name. Array for round-robin in script patterns */
|
|
42
|
-
color?:
|
|
43
|
+
color?: Color | Color[];
|
|
43
44
|
/** Glob patterns — restart process when matching files change */
|
|
44
45
|
watch?: string | string[];
|
|
45
46
|
/**
|
|
@@ -88,14 +89,22 @@ export interface NumuxConfig<K extends string = string> {
|
|
|
88
89
|
errorMatcher?: boolean | string;
|
|
89
90
|
/** Global watch patterns, inherited by processes without their own watch */
|
|
90
91
|
watch?: string | string[];
|
|
92
|
+
/**
|
|
93
|
+
* Tab display order. `'config'` preserves definition order (package.json script order for wildcards),
|
|
94
|
+
* `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers.
|
|
95
|
+
* @default 'config'
|
|
96
|
+
*/
|
|
97
|
+
sort?: SortOrder;
|
|
91
98
|
processes: Record<K, NumuxProcessConfig<K> | NumuxScriptPattern<K> | string>;
|
|
92
99
|
}
|
|
100
|
+
export type SortOrder = 'config' | 'alphabetical' | 'topological';
|
|
93
101
|
/** Process config after validation — dependsOn is always normalized to an array */
|
|
94
102
|
export interface ResolvedProcessConfig extends Omit<NumuxProcessConfig, 'dependsOn'> {
|
|
95
103
|
dependsOn?: string[];
|
|
96
104
|
}
|
|
97
105
|
/** Validated config with all shorthand expanded to full objects */
|
|
98
106
|
export interface ResolvedNumuxConfig {
|
|
107
|
+
sort?: SortOrder;
|
|
99
108
|
processes: Record<string, ResolvedProcessConfig>;
|
|
100
109
|
}
|
|
101
110
|
export type ProcessStatus = 'pending' | 'starting' | 'ready' | 'running' | 'stopping' | 'stopped' | 'finished' | 'failed' | 'skipped';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Basic color names mapped to muted hex tones (lowercase keys) */
|
|
2
|
+
export type BasicColor = keyof typeof BASIC_COLORS;
|
|
3
|
+
export type Color = `#${string}` | BasicColor;
|
|
4
|
+
export declare const BASIC_COLORS: {
|
|
5
|
+
readonly black: "#000000";
|
|
6
|
+
readonly red: "#ff5555";
|
|
7
|
+
readonly green: "#00cc00";
|
|
8
|
+
readonly yellow: "#cccc00";
|
|
9
|
+
readonly blue: "#0000cc";
|
|
10
|
+
readonly magenta: "#cc00cc";
|
|
11
|
+
readonly cyan: "#00cccc";
|
|
12
|
+
readonly white: "#ffffff";
|
|
13
|
+
readonly gray: "#808080";
|
|
14
|
+
readonly grey: "#808080";
|
|
15
|
+
readonly orange: "#ffa500";
|
|
16
|
+
readonly purple: "#800080";
|
|
17
|
+
};
|
|
18
|
+
/** Check if a string is a valid color (hex or basic name) */
|
|
19
|
+
export declare function isValidColor(color: string): boolean;
|
|
20
|
+
/** Resolve any color (hex or basic name) to normalized hex (#rrggbb) */
|
|
21
|
+
export declare function resolveToHex(color: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Convert a hex color string (e.g. "#ff8800") to an ANSI true-color escape sequence.
|
|
24
|
+
* Returns an empty string if the hex is malformed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hexToAnsi(hex: string): string;
|
|
27
|
+
/** Regex matching a valid 6-digit hex color (with or without leading #) */
|
|
28
|
+
export declare const HEX_COLOR_RE: RegExp;
|
|
29
|
+
import type { ProcessStatus, ResolvedNumuxConfig } from '../types';
|
|
30
|
+
/** ANSI color codes for process statuses */
|
|
31
|
+
export declare const STATUS_ANSI: Partial<Record<ProcessStatus, string>>;
|
|
32
|
+
export declare const ANSI_RESET = "\u001B[0m";
|
|
33
|
+
/** Strip ANSI escape sequences from text */
|
|
34
|
+
export declare function stripAnsi(str: string): string;
|
|
35
|
+
/** Pick a deterministic color from the default palette based on the process name */
|
|
36
|
+
export declare function colorFromName(name: string): Color;
|
|
37
|
+
/** Build a map of process names to ANSI color codes, using explicit config colors or a default palette. */
|
|
38
|
+
export declare function buildProcessColorMap(names: string[], config: ResolvedNumuxConfig): Map<string, string>;
|
|
39
|
+
/** Build a map of process names to hex color strings (for StyledText rendering). */
|
|
40
|
+
export declare function buildProcessHexColorMap(names: string[], config: ResolvedNumuxConfig): Map<string, string>;
|