numux 1.23.1 → 1.24.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 +18 -1
- package/dist/numux.js +348 -132
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -167,7 +167,7 @@ Template properties (color, env, dependsOn, etc.) are inherited by all matched p
|
|
|
167
167
|
| `-s, --sort <mode>` | Tab display order: `config` (default), `alphabetical`, `topological` |
|
|
168
168
|
| `--no-watch` | Disable file watching even if config has `watch` patterns |
|
|
169
169
|
| `-t, --timestamps` | Add `[HH:MM:SS]` timestamps to prefixed output |
|
|
170
|
-
| `--log-dir <path>` |
|
|
170
|
+
| `--log-dir <path>` | Persist logs to timestamped subdirs (`<path>/<timestamp>/<name>.log`) with a `latest` symlink. Path is printed on exit |
|
|
171
171
|
| `--debug` | Log to `.numux/debug.log` |
|
|
172
172
|
| `-h, --help` | Show help |
|
|
173
173
|
| `-v, --version` | Show version |
|
|
@@ -347,6 +347,23 @@ Unmatched references are left as-is (the shell will expand `$db` as empty + `.po
|
|
|
347
347
|
|
|
348
348
|
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.).
|
|
349
349
|
|
|
350
|
+
| Key | Action |
|
|
351
|
+
|-----|--------|
|
|
352
|
+
| `←`/`→` or `1`-`9` | Switch tabs |
|
|
353
|
+
| `F` | Search current pane |
|
|
354
|
+
| `Tab` (in search) | Toggle between single-pane and all-process search |
|
|
355
|
+
| `Enter`/`Shift+Enter` | Next/previous match |
|
|
356
|
+
| `Esc` | Exit search |
|
|
357
|
+
| `R` | Restart current process |
|
|
358
|
+
| `Shift+R` | Restart all processes |
|
|
359
|
+
| `S` | Stop/start current process |
|
|
360
|
+
| `Y` | Copy all output |
|
|
361
|
+
| `L` | Clear pane |
|
|
362
|
+
| `G`/`Shift+G` | Scroll to top/bottom |
|
|
363
|
+
| `PageUp`/`PageDown` | Scroll by page |
|
|
364
|
+
| `Ctrl+Click` | Open link |
|
|
365
|
+
| `Ctrl+C` | Quit |
|
|
366
|
+
|
|
350
367
|
## Tab icons
|
|
351
368
|
|
|
352
369
|
| Icon | Status |
|
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.24.0",
|
|
40
40
|
description: "Terminal multiplexer with dependency orchestration",
|
|
41
41
|
type: "module",
|
|
42
42
|
license: "MIT",
|
|
@@ -1059,6 +1059,11 @@ function validateConfig(raw, warnings) {
|
|
|
1059
1059
|
globalEnv = config.env;
|
|
1060
1060
|
}
|
|
1061
1061
|
const sort = validateSort(config.sort);
|
|
1062
|
+
const prefix = config.prefix === true ? true : undefined;
|
|
1063
|
+
const timestamps = config.timestamps === true ? true : undefined;
|
|
1064
|
+
const killOthers = config.killOthers === true ? true : undefined;
|
|
1065
|
+
const noWatch = config.noWatch === true ? true : undefined;
|
|
1066
|
+
const logDir = typeof config.logDir === "string" && config.logDir.trim() ? config.logDir.trim() : undefined;
|
|
1062
1067
|
const validated = {};
|
|
1063
1068
|
for (const name of names) {
|
|
1064
1069
|
let proc = processes[name];
|
|
@@ -1156,7 +1161,15 @@ function validateConfig(raw, warnings) {
|
|
|
1156
1161
|
showCommand
|
|
1157
1162
|
};
|
|
1158
1163
|
}
|
|
1159
|
-
return {
|
|
1164
|
+
return {
|
|
1165
|
+
...sort ? { sort } : {},
|
|
1166
|
+
...prefix ? { prefix } : {},
|
|
1167
|
+
...timestamps ? { timestamps } : {},
|
|
1168
|
+
...killOthers ? { killOthers } : {},
|
|
1169
|
+
...noWatch ? { noWatch } : {},
|
|
1170
|
+
...logDir ? { logDir } : {},
|
|
1171
|
+
processes: validated
|
|
1172
|
+
};
|
|
1160
1173
|
}
|
|
1161
1174
|
function validateStringOrStringArray(value) {
|
|
1162
1175
|
if (typeof value === "string")
|
|
@@ -2307,6 +2320,197 @@ class Pane {
|
|
|
2307
2320
|
}
|
|
2308
2321
|
}
|
|
2309
2322
|
|
|
2323
|
+
// src/ui/search.ts
|
|
2324
|
+
class SearchController {
|
|
2325
|
+
mode = false;
|
|
2326
|
+
allMode = false;
|
|
2327
|
+
query = "";
|
|
2328
|
+
matches = [];
|
|
2329
|
+
index = -1;
|
|
2330
|
+
crossMatches = [];
|
|
2331
|
+
crossCounts = new Map;
|
|
2332
|
+
timer = null;
|
|
2333
|
+
logWriter;
|
|
2334
|
+
statusBar;
|
|
2335
|
+
tabBar;
|
|
2336
|
+
getActivePane;
|
|
2337
|
+
getPane;
|
|
2338
|
+
constructor(opts) {
|
|
2339
|
+
this.logWriter = opts.logWriter;
|
|
2340
|
+
this.statusBar = opts.statusBar;
|
|
2341
|
+
this.tabBar = opts.tabBar;
|
|
2342
|
+
this.getActivePane = opts.getActivePane;
|
|
2343
|
+
this.getPane = opts.getPane;
|
|
2344
|
+
}
|
|
2345
|
+
get isActive() {
|
|
2346
|
+
return this.mode;
|
|
2347
|
+
}
|
|
2348
|
+
get isAllMode() {
|
|
2349
|
+
return this.allMode;
|
|
2350
|
+
}
|
|
2351
|
+
get currentMatches() {
|
|
2352
|
+
return this.matches;
|
|
2353
|
+
}
|
|
2354
|
+
enter() {
|
|
2355
|
+
this.mode = true;
|
|
2356
|
+
this.query = "";
|
|
2357
|
+
this.matches = [];
|
|
2358
|
+
this.index = -1;
|
|
2359
|
+
this.allMode = false;
|
|
2360
|
+
this.crossMatches = [];
|
|
2361
|
+
this.crossCounts.clear();
|
|
2362
|
+
this.statusBar.setSearchMode(true);
|
|
2363
|
+
}
|
|
2364
|
+
exit() {
|
|
2365
|
+
this.mode = false;
|
|
2366
|
+
this.query = "";
|
|
2367
|
+
this.matches = [];
|
|
2368
|
+
this.index = -1;
|
|
2369
|
+
this.allMode = false;
|
|
2370
|
+
this.crossMatches = [];
|
|
2371
|
+
this.crossCounts.clear();
|
|
2372
|
+
if (this.timer) {
|
|
2373
|
+
clearTimeout(this.timer);
|
|
2374
|
+
this.timer = null;
|
|
2375
|
+
}
|
|
2376
|
+
const active = this.getActivePane();
|
|
2377
|
+
if (active) {
|
|
2378
|
+
this.getPane(active)?.clearHighlights();
|
|
2379
|
+
}
|
|
2380
|
+
this.tabBar.clearSearchMatches();
|
|
2381
|
+
this.statusBar.setSearchMode(false);
|
|
2382
|
+
}
|
|
2383
|
+
onPaneSwitch() {
|
|
2384
|
+
if (this.mode && this.allMode) {
|
|
2385
|
+
this.applyPaneMatches();
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
refreshHighlights() {
|
|
2389
|
+
if (this.matches.length > 0) {
|
|
2390
|
+
this.updateHighlights();
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
handleInput(key) {
|
|
2394
|
+
if (key.name === "escape") {
|
|
2395
|
+
this.exit();
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (key.name === "tab") {
|
|
2399
|
+
this.allMode = !this.allMode;
|
|
2400
|
+
if (!this.allMode) {
|
|
2401
|
+
this.crossMatches = [];
|
|
2402
|
+
this.crossCounts.clear();
|
|
2403
|
+
this.tabBar.clearSearchMatches();
|
|
2404
|
+
}
|
|
2405
|
+
this.scheduleSearch();
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (key.name === "return") {
|
|
2409
|
+
if (this.matches.length === 0)
|
|
2410
|
+
return;
|
|
2411
|
+
if (key.shift) {
|
|
2412
|
+
this.index = (this.index - 1 + this.matches.length) % this.matches.length;
|
|
2413
|
+
} else {
|
|
2414
|
+
this.index = (this.index + 1) % this.matches.length;
|
|
2415
|
+
}
|
|
2416
|
+
this.scrollToMatch();
|
|
2417
|
+
this.updateHighlights();
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (key.name === "backspace") {
|
|
2421
|
+
if (this.query.length > 0) {
|
|
2422
|
+
this.query = this.query.slice(0, -1);
|
|
2423
|
+
this.scheduleSearch();
|
|
2424
|
+
}
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2428
|
+
this.query += key.sequence;
|
|
2429
|
+
this.scheduleSearch();
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
dispose() {
|
|
2433
|
+
if (this.timer) {
|
|
2434
|
+
clearTimeout(this.timer);
|
|
2435
|
+
this.timer = null;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
scheduleSearch() {
|
|
2439
|
+
this.statusBar.setSearchMode(true, this.query, this.matches.length, this.index, this.crossInfo());
|
|
2440
|
+
if (this.timer)
|
|
2441
|
+
clearTimeout(this.timer);
|
|
2442
|
+
this.timer = setTimeout(() => {
|
|
2443
|
+
this.timer = null;
|
|
2444
|
+
this.runSearch();
|
|
2445
|
+
}, 100);
|
|
2446
|
+
}
|
|
2447
|
+
async runSearch() {
|
|
2448
|
+
const active = this.getActivePane();
|
|
2449
|
+
if (!active)
|
|
2450
|
+
return;
|
|
2451
|
+
const query = this.query;
|
|
2452
|
+
const allMode = this.allMode;
|
|
2453
|
+
if (allMode) {
|
|
2454
|
+
const allMatches = await this.logWriter.searchAll(query);
|
|
2455
|
+
if (!this.mode || this.query !== query || !this.allMode)
|
|
2456
|
+
return;
|
|
2457
|
+
this.crossMatches = allMatches;
|
|
2458
|
+
this.crossCounts.clear();
|
|
2459
|
+
for (const m of allMatches) {
|
|
2460
|
+
this.crossCounts.set(m.process, (this.crossCounts.get(m.process) ?? 0) + 1);
|
|
2461
|
+
}
|
|
2462
|
+
this.tabBar.setSearchMatches(this.crossCounts);
|
|
2463
|
+
this.applyPaneMatches();
|
|
2464
|
+
} else {
|
|
2465
|
+
const matches = await this.logWriter.search(active, query);
|
|
2466
|
+
if (!this.mode || this.query !== query || this.getActivePane() !== active)
|
|
2467
|
+
return;
|
|
2468
|
+
this.matches = matches;
|
|
2469
|
+
this.index = matches.length > 0 ? 0 : -1;
|
|
2470
|
+
this.updateHighlights();
|
|
2471
|
+
if (this.index >= 0)
|
|
2472
|
+
this.scrollToMatch();
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
applyPaneMatches() {
|
|
2476
|
+
const active = this.getActivePane();
|
|
2477
|
+
if (!active)
|
|
2478
|
+
return;
|
|
2479
|
+
const paneMatches = this.crossMatches.filter((m) => m.process === active).map(({ line, start, end }) => ({ line, start, end }));
|
|
2480
|
+
this.matches = paneMatches;
|
|
2481
|
+
this.index = paneMatches.length > 0 ? 0 : -1;
|
|
2482
|
+
this.updateHighlights();
|
|
2483
|
+
if (this.index >= 0)
|
|
2484
|
+
this.scrollToMatch();
|
|
2485
|
+
}
|
|
2486
|
+
updateHighlights() {
|
|
2487
|
+
const active = this.getActivePane();
|
|
2488
|
+
if (!active)
|
|
2489
|
+
return;
|
|
2490
|
+
const pane = this.getPane(active);
|
|
2491
|
+
if (!pane)
|
|
2492
|
+
return;
|
|
2493
|
+
if (this.matches.length > 0) {
|
|
2494
|
+
pane.setHighlights(this.matches, this.index);
|
|
2495
|
+
} else {
|
|
2496
|
+
pane.clearHighlights();
|
|
2497
|
+
}
|
|
2498
|
+
this.statusBar.setSearchMode(true, this.query, this.matches.length, this.index, this.crossInfo());
|
|
2499
|
+
}
|
|
2500
|
+
scrollToMatch() {
|
|
2501
|
+
const active = this.getActivePane();
|
|
2502
|
+
if (!active || this.index < 0)
|
|
2503
|
+
return;
|
|
2504
|
+
const pane = this.getPane(active);
|
|
2505
|
+
if (!pane)
|
|
2506
|
+
return;
|
|
2507
|
+
pane.scrollToLine(this.matches[this.index].line);
|
|
2508
|
+
}
|
|
2509
|
+
crossInfo() {
|
|
2510
|
+
return this.allMode ? { totalMatches: this.crossMatches.length, processCount: this.crossCounts.size } : undefined;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2310
2514
|
// src/ui/status-bar.ts
|
|
2311
2515
|
import { cyan, red, reverse, StyledText, TextRenderable, yellow } from "@opentui/core";
|
|
2312
2516
|
function plain(text) {
|
|
@@ -2319,6 +2523,7 @@ class StatusBar {
|
|
|
2319
2523
|
_searchQuery = "";
|
|
2320
2524
|
_searchMatchCount = 0;
|
|
2321
2525
|
_searchCurrentIndex = -1;
|
|
2526
|
+
_crossProcessInfo;
|
|
2322
2527
|
_tempMessage = null;
|
|
2323
2528
|
_tempTimer = null;
|
|
2324
2529
|
constructor(renderer) {
|
|
@@ -2331,11 +2536,12 @@ class StatusBar {
|
|
|
2331
2536
|
paddingX: 1
|
|
2332
2537
|
});
|
|
2333
2538
|
}
|
|
2334
|
-
setSearchMode(active, query = "", matchCount = 0, currentIndex = -1) {
|
|
2539
|
+
setSearchMode(active, query = "", matchCount = 0, currentIndex = -1, crossProcessInfo) {
|
|
2335
2540
|
this._searchMode = active;
|
|
2336
2541
|
this._searchQuery = query;
|
|
2337
2542
|
this._searchMatchCount = matchCount;
|
|
2338
2543
|
this._searchCurrentIndex = currentIndex;
|
|
2544
|
+
this._crossProcessInfo = crossProcessInfo;
|
|
2339
2545
|
this.renderable.content = this.buildContent();
|
|
2340
2546
|
}
|
|
2341
2547
|
showTemporaryMessage(message, duration = 2000) {
|
|
@@ -2360,6 +2566,7 @@ class StatusBar {
|
|
|
2360
2566
|
}
|
|
2361
2567
|
buildSearchContent() {
|
|
2362
2568
|
const chunks = [];
|
|
2569
|
+
const isAllMode = !!this._crossProcessInfo;
|
|
2363
2570
|
chunks.push(yellow("/"));
|
|
2364
2571
|
if (this._searchQuery)
|
|
2365
2572
|
chunks.push(plain(this._searchQuery));
|
|
@@ -2367,14 +2574,20 @@ class StatusBar {
|
|
|
2367
2574
|
if (this._searchMatchCount === 0 && this._searchQuery) {
|
|
2368
2575
|
chunks.push(plain(" "));
|
|
2369
2576
|
chunks.push(red("no matches"));
|
|
2370
|
-
chunks.push(plain(" Esc: close"));
|
|
2371
2577
|
} else if (this._searchMatchCount > 0) {
|
|
2372
2578
|
chunks.push(plain(" "));
|
|
2373
|
-
|
|
2374
|
-
|
|
2579
|
+
if (isAllMode) {
|
|
2580
|
+
const { totalMatches, processCount } = this._crossProcessInfo;
|
|
2581
|
+
chunks.push(cyan(`${this._searchCurrentIndex + 1}/${this._searchMatchCount}`));
|
|
2582
|
+
chunks.push(plain(` ${totalMatches} in ${processCount} process${processCount === 1 ? "" : "es"}`));
|
|
2583
|
+
} else {
|
|
2584
|
+
chunks.push(cyan(`${this._searchCurrentIndex + 1}/${this._searchMatchCount}`));
|
|
2585
|
+
}
|
|
2586
|
+
chunks.push(plain(" Enter: next"));
|
|
2375
2587
|
} else {
|
|
2376
|
-
chunks.push(plain(" Enter: next
|
|
2588
|
+
chunks.push(plain(" Enter: next"));
|
|
2377
2589
|
}
|
|
2590
|
+
chunks.push(plain(` Tab: ${isAllMode ? "single" : "all"} Esc: close`));
|
|
2378
2591
|
return new StyledText(chunks);
|
|
2379
2592
|
}
|
|
2380
2593
|
}
|
|
@@ -2422,12 +2635,13 @@ function getDisplayOrder(originalNames, statuses) {
|
|
|
2422
2635
|
const terminal = originalNames.filter((n) => TERMINAL_STATUSES.has(statuses.get(n)));
|
|
2423
2636
|
return [...active, ...terminal];
|
|
2424
2637
|
}
|
|
2425
|
-
function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses) {
|
|
2638
|
+
function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses, searchMatchProcesses) {
|
|
2426
2639
|
return names.map((name) => {
|
|
2427
2640
|
const status = statuses.get(name);
|
|
2428
2641
|
const waiting = inputWaiting.has(name);
|
|
2429
2642
|
const errored = erroredProcesses.has(name);
|
|
2430
|
-
const
|
|
2643
|
+
const hasSearchMatch = searchMatchProcesses?.has(name);
|
|
2644
|
+
const statusHex = hasSearchMatch ? "#b58900" : waiting ? "#ffaa00" : errored ? "#ff5555" : STATUS_ICON_HEX[status];
|
|
2431
2645
|
const processHex = processColors.get(name);
|
|
2432
2646
|
return {
|
|
2433
2647
|
iconHex: statusHex ?? processHex ?? "#888888",
|
|
@@ -2512,6 +2726,7 @@ class TabBar {
|
|
|
2512
2726
|
processColors;
|
|
2513
2727
|
inputWaiting = new Set;
|
|
2514
2728
|
erroredProcesses = new Set;
|
|
2729
|
+
searchMatchCounts = new Map;
|
|
2515
2730
|
constructor(renderer, names, colors) {
|
|
2516
2731
|
this.originalNames = names;
|
|
2517
2732
|
this.names = [...names];
|
|
@@ -2569,6 +2784,14 @@ class TabBar {
|
|
|
2569
2784
|
this.erroredProcesses.delete(name);
|
|
2570
2785
|
this.refreshOptions();
|
|
2571
2786
|
}
|
|
2787
|
+
setSearchMatches(counts) {
|
|
2788
|
+
this.searchMatchCounts = counts;
|
|
2789
|
+
this.refreshOptions();
|
|
2790
|
+
}
|
|
2791
|
+
clearSearchMatches() {
|
|
2792
|
+
this.searchMatchCounts.clear();
|
|
2793
|
+
this.refreshOptions();
|
|
2794
|
+
}
|
|
2572
2795
|
getNameAtIndex(index) {
|
|
2573
2796
|
return this.names[index];
|
|
2574
2797
|
}
|
|
@@ -2590,6 +2813,10 @@ class TabBar {
|
|
|
2590
2813
|
this.updateOptionColors();
|
|
2591
2814
|
}
|
|
2592
2815
|
getDescription(name) {
|
|
2816
|
+
const matchCount = this.searchMatchCounts.get(name);
|
|
2817
|
+
if (matchCount != null && matchCount > 0) {
|
|
2818
|
+
return `${matchCount} match${matchCount === 1 ? "" : "es"}`;
|
|
2819
|
+
}
|
|
2593
2820
|
if (this.inputWaiting.has(name))
|
|
2594
2821
|
return "awaiting input";
|
|
2595
2822
|
if (this.erroredProcesses.has(name))
|
|
@@ -2597,7 +2824,8 @@ class TabBar {
|
|
|
2597
2824
|
return this.baseDescriptions.get(name) ?? "pending";
|
|
2598
2825
|
}
|
|
2599
2826
|
updateOptionColors() {
|
|
2600
|
-
const
|
|
2827
|
+
const searchProcesses = this.searchMatchCounts.size > 0 ? new Set(this.searchMatchCounts.keys()) : undefined;
|
|
2828
|
+
const resolved = resolveOptionColors(this.names, this.statuses, this.processColors, this.inputWaiting, this.erroredProcesses, searchProcesses);
|
|
2601
2829
|
const colors = resolved.map((c) => ({
|
|
2602
2830
|
icon: parseColor(c.iconHex),
|
|
2603
2831
|
name: c.nameHex ? parseColor(c.nameHex) : null
|
|
@@ -2622,6 +2850,7 @@ class App {
|
|
|
2622
2850
|
panes = new Map;
|
|
2623
2851
|
tabBar;
|
|
2624
2852
|
statusBar;
|
|
2853
|
+
search;
|
|
2625
2854
|
activePane = null;
|
|
2626
2855
|
destroyed = false;
|
|
2627
2856
|
names;
|
|
@@ -2631,11 +2860,6 @@ class App {
|
|
|
2631
2860
|
config;
|
|
2632
2861
|
logWriter;
|
|
2633
2862
|
resizeTimer = null;
|
|
2634
|
-
searchTimer = null;
|
|
2635
|
-
searchMode = false;
|
|
2636
|
-
searchQuery = "";
|
|
2637
|
-
searchMatches = [];
|
|
2638
|
-
searchIndex = -1;
|
|
2639
2863
|
inputWaitTimers = new Map;
|
|
2640
2864
|
awaitingInput = new Set;
|
|
2641
2865
|
constructor(manager, config, logWriter) {
|
|
@@ -2685,6 +2909,14 @@ class App {
|
|
|
2685
2909
|
flexGrow: 1,
|
|
2686
2910
|
border: false
|
|
2687
2911
|
});
|
|
2912
|
+
this.statusBar = new StatusBar(this.renderer);
|
|
2913
|
+
this.search = new SearchController({
|
|
2914
|
+
logWriter: this.logWriter,
|
|
2915
|
+
statusBar: this.statusBar,
|
|
2916
|
+
tabBar: this.tabBar,
|
|
2917
|
+
getActivePane: () => this.activePane,
|
|
2918
|
+
getPane: (name) => this.panes.get(name)
|
|
2919
|
+
});
|
|
2688
2920
|
for (const name of this.names) {
|
|
2689
2921
|
const interactive = this.config.processes[name].interactive === true;
|
|
2690
2922
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
@@ -2697,14 +2929,13 @@ class App {
|
|
|
2697
2929
|
this.statusBar.showTemporaryMessage(`Opening ${link.url}`);
|
|
2698
2930
|
});
|
|
2699
2931
|
pane.onScroll(() => {
|
|
2700
|
-
if (this.
|
|
2701
|
-
this.
|
|
2932
|
+
if (this.search.isActive && this.search.currentMatches.length > 0 && this.activePane === name) {
|
|
2933
|
+
this.search.refreshHighlights();
|
|
2702
2934
|
}
|
|
2703
2935
|
});
|
|
2704
2936
|
this.panes.set(name, pane);
|
|
2705
2937
|
paneContainer.add(pane.scrollBox);
|
|
2706
2938
|
}
|
|
2707
|
-
this.statusBar = new StatusBar(this.renderer);
|
|
2708
2939
|
contentRow.add(sidebar);
|
|
2709
2940
|
contentRow.add(paneContainer);
|
|
2710
2941
|
layout.add(contentRow);
|
|
@@ -2746,8 +2977,8 @@ class App {
|
|
|
2746
2977
|
this.renderer.keyInput.on("keypress", (key) => {
|
|
2747
2978
|
log(key);
|
|
2748
2979
|
if (key.ctrl && key.name === "c") {
|
|
2749
|
-
if (this.
|
|
2750
|
-
this.
|
|
2980
|
+
if (this.search.isActive) {
|
|
2981
|
+
this.search.exit();
|
|
2751
2982
|
return;
|
|
2752
2983
|
}
|
|
2753
2984
|
this.shutdown().then(() => {
|
|
@@ -2755,8 +2986,8 @@ class App {
|
|
|
2755
2986
|
});
|
|
2756
2987
|
return;
|
|
2757
2988
|
}
|
|
2758
|
-
if (this.
|
|
2759
|
-
this.
|
|
2989
|
+
if (this.search.isActive) {
|
|
2990
|
+
this.search.handleInput(key);
|
|
2760
2991
|
return;
|
|
2761
2992
|
}
|
|
2762
2993
|
if (!this.activePane)
|
|
@@ -2781,7 +3012,7 @@ class App {
|
|
|
2781
3012
|
return;
|
|
2782
3013
|
}
|
|
2783
3014
|
if (name === SHORTCUTS.search.key) {
|
|
2784
|
-
this.
|
|
3015
|
+
this.search.enter();
|
|
2785
3016
|
return;
|
|
2786
3017
|
}
|
|
2787
3018
|
if (name === SHORTCUTS.restart.key) {
|
|
@@ -2845,14 +3076,16 @@ class App {
|
|
|
2845
3076
|
switchPane(name) {
|
|
2846
3077
|
if (this.activePane === name)
|
|
2847
3078
|
return;
|
|
2848
|
-
if (this.
|
|
2849
|
-
this.
|
|
3079
|
+
if (this.search.isActive && !this.search.isAllMode) {
|
|
3080
|
+
this.search.exit();
|
|
2850
3081
|
}
|
|
2851
3082
|
if (this.activePane) {
|
|
3083
|
+
this.panes.get(this.activePane)?.clearHighlights();
|
|
2852
3084
|
this.panes.get(this.activePane)?.hide();
|
|
2853
3085
|
}
|
|
2854
3086
|
this.activePane = name;
|
|
2855
3087
|
this.panes.get(name)?.show();
|
|
3088
|
+
this.search.onPaneSwitch();
|
|
2856
3089
|
}
|
|
2857
3090
|
checkInputWaiting(name, data) {
|
|
2858
3091
|
const existing = this.inputWaitTimers.get(name);
|
|
@@ -2913,102 +3146,6 @@ class App {
|
|
|
2913
3146
|
this.copyToClipboard(text);
|
|
2914
3147
|
this.statusBar.showTemporaryMessage("Copied all output!");
|
|
2915
3148
|
}
|
|
2916
|
-
enterSearch() {
|
|
2917
|
-
this.searchMode = true;
|
|
2918
|
-
this.searchQuery = "";
|
|
2919
|
-
this.searchMatches = [];
|
|
2920
|
-
this.searchIndex = -1;
|
|
2921
|
-
this.statusBar.setSearchMode(true);
|
|
2922
|
-
}
|
|
2923
|
-
exitSearch() {
|
|
2924
|
-
this.searchMode = false;
|
|
2925
|
-
this.searchQuery = "";
|
|
2926
|
-
this.searchMatches = [];
|
|
2927
|
-
this.searchIndex = -1;
|
|
2928
|
-
if (this.searchTimer) {
|
|
2929
|
-
clearTimeout(this.searchTimer);
|
|
2930
|
-
this.searchTimer = null;
|
|
2931
|
-
}
|
|
2932
|
-
if (this.activePane) {
|
|
2933
|
-
this.panes.get(this.activePane)?.clearHighlights();
|
|
2934
|
-
}
|
|
2935
|
-
this.statusBar.setSearchMode(false);
|
|
2936
|
-
}
|
|
2937
|
-
handleSearchInput(key) {
|
|
2938
|
-
if (key.name === "escape") {
|
|
2939
|
-
this.exitSearch();
|
|
2940
|
-
return;
|
|
2941
|
-
}
|
|
2942
|
-
if (key.name === "return") {
|
|
2943
|
-
if (this.searchMatches.length === 0)
|
|
2944
|
-
return;
|
|
2945
|
-
if (key.shift) {
|
|
2946
|
-
this.searchIndex = (this.searchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
|
|
2947
|
-
} else {
|
|
2948
|
-
this.searchIndex = (this.searchIndex + 1) % this.searchMatches.length;
|
|
2949
|
-
}
|
|
2950
|
-
this.scrollToCurrentMatch();
|
|
2951
|
-
this.updateSearchHighlights();
|
|
2952
|
-
return;
|
|
2953
|
-
}
|
|
2954
|
-
if (key.name === "backspace") {
|
|
2955
|
-
if (this.searchQuery.length > 0) {
|
|
2956
|
-
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
2957
|
-
this.scheduleSearch();
|
|
2958
|
-
}
|
|
2959
|
-
return;
|
|
2960
|
-
}
|
|
2961
|
-
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2962
|
-
this.searchQuery += key.sequence;
|
|
2963
|
-
this.scheduleSearch();
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
scheduleSearch() {
|
|
2967
|
-
this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
|
|
2968
|
-
if (this.searchTimer)
|
|
2969
|
-
clearTimeout(this.searchTimer);
|
|
2970
|
-
this.searchTimer = setTimeout(() => {
|
|
2971
|
-
this.searchTimer = null;
|
|
2972
|
-
this.runSearch();
|
|
2973
|
-
}, 100);
|
|
2974
|
-
}
|
|
2975
|
-
async runSearch() {
|
|
2976
|
-
if (!this.activePane)
|
|
2977
|
-
return;
|
|
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)
|
|
2982
|
-
return;
|
|
2983
|
-
this.searchMatches = matches;
|
|
2984
|
-
this.searchIndex = matches.length > 0 ? 0 : -1;
|
|
2985
|
-
this.updateSearchHighlights();
|
|
2986
|
-
if (this.searchIndex >= 0) {
|
|
2987
|
-
this.scrollToCurrentMatch();
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
updateSearchHighlights() {
|
|
2991
|
-
if (!this.activePane)
|
|
2992
|
-
return;
|
|
2993
|
-
const pane = this.panes.get(this.activePane);
|
|
2994
|
-
if (!pane)
|
|
2995
|
-
return;
|
|
2996
|
-
if (this.searchMatches.length > 0) {
|
|
2997
|
-
pane.setHighlights(this.searchMatches, this.searchIndex);
|
|
2998
|
-
} else {
|
|
2999
|
-
pane.clearHighlights();
|
|
3000
|
-
}
|
|
3001
|
-
this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
|
|
3002
|
-
}
|
|
3003
|
-
scrollToCurrentMatch() {
|
|
3004
|
-
if (!this.activePane || this.searchIndex < 0)
|
|
3005
|
-
return;
|
|
3006
|
-
const pane = this.panes.get(this.activePane);
|
|
3007
|
-
if (!pane)
|
|
3008
|
-
return;
|
|
3009
|
-
const match = this.searchMatches[this.searchIndex];
|
|
3010
|
-
pane.scrollToLine(match.line);
|
|
3011
|
-
}
|
|
3012
3149
|
async shutdown() {
|
|
3013
3150
|
if (this.destroyed)
|
|
3014
3151
|
return;
|
|
@@ -3017,10 +3154,7 @@ class App {
|
|
|
3017
3154
|
clearTimeout(this.resizeTimer);
|
|
3018
3155
|
this.resizeTimer = null;
|
|
3019
3156
|
}
|
|
3020
|
-
|
|
3021
|
-
clearTimeout(this.searchTimer);
|
|
3022
|
-
this.searchTimer = null;
|
|
3023
|
-
}
|
|
3157
|
+
this.search.dispose();
|
|
3024
3158
|
for (const timer of this.inputWaitTimers.values()) {
|
|
3025
3159
|
clearTimeout(timer);
|
|
3026
3160
|
}
|
|
@@ -3224,9 +3358,9 @@ ${DIM}Done in ${elapsed}${RESET}
|
|
|
3224
3358
|
}
|
|
3225
3359
|
|
|
3226
3360
|
// src/utils/log-writer.ts
|
|
3227
|
-
import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, writeSync } from "fs";
|
|
3361
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
|
|
3228
3362
|
import { tmpdir } from "os";
|
|
3229
|
-
import { join } from "path";
|
|
3363
|
+
import { basename as basename2, join } from "path";
|
|
3230
3364
|
class LogWriter {
|
|
3231
3365
|
dir;
|
|
3232
3366
|
isTemp;
|
|
@@ -3242,6 +3376,30 @@ class LogWriter {
|
|
|
3242
3376
|
const dir = join(tmpdir(), `numux-${process.pid}`);
|
|
3243
3377
|
return new LogWriter(dir, true);
|
|
3244
3378
|
}
|
|
3379
|
+
static createPersistent(baseDir) {
|
|
3380
|
+
mkdirSync2(baseDir, { recursive: true });
|
|
3381
|
+
const now = new Date;
|
|
3382
|
+
const ts = now.toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
3383
|
+
const sessionDir = join(baseDir, ts);
|
|
3384
|
+
mkdirSync2(sessionDir, { recursive: true });
|
|
3385
|
+
const latestLink = join(baseDir, "latest");
|
|
3386
|
+
try {
|
|
3387
|
+
unlinkSync(latestLink);
|
|
3388
|
+
} catch {}
|
|
3389
|
+
try {
|
|
3390
|
+
symlinkSync(sessionDir, latestLink);
|
|
3391
|
+
} catch {}
|
|
3392
|
+
return new LogWriter(sessionDir, false);
|
|
3393
|
+
}
|
|
3394
|
+
get isTemporary() {
|
|
3395
|
+
return this.isTemp;
|
|
3396
|
+
}
|
|
3397
|
+
getDirectory() {
|
|
3398
|
+
return this.dir;
|
|
3399
|
+
}
|
|
3400
|
+
getProcessNames() {
|
|
3401
|
+
return [...this.files.keys()];
|
|
3402
|
+
}
|
|
3245
3403
|
errored = false;
|
|
3246
3404
|
handleEvent = (event) => {
|
|
3247
3405
|
if (event.type !== "output" || this.errored)
|
|
@@ -3313,6 +3471,58 @@ class LogWriter {
|
|
|
3313
3471
|
return [];
|
|
3314
3472
|
}
|
|
3315
3473
|
}
|
|
3474
|
+
async searchAll(query) {
|
|
3475
|
+
if (!query)
|
|
3476
|
+
return [];
|
|
3477
|
+
const paths = [...this.files.keys()].map((name) => join(this.dir, `${name}.log`));
|
|
3478
|
+
if (paths.length === 0)
|
|
3479
|
+
return [];
|
|
3480
|
+
try {
|
|
3481
|
+
const proc = Bun.spawn(["grep", "-inFH", query, ...paths], {
|
|
3482
|
+
stdout: "pipe",
|
|
3483
|
+
stderr: "ignore"
|
|
3484
|
+
});
|
|
3485
|
+
const output = await new Response(proc.stdout).text();
|
|
3486
|
+
await proc.exited;
|
|
3487
|
+
const matches = [];
|
|
3488
|
+
const lowerQuery = query.toLowerCase();
|
|
3489
|
+
for (const line of output.split(`
|
|
3490
|
+
`)) {
|
|
3491
|
+
if (!line)
|
|
3492
|
+
continue;
|
|
3493
|
+
const firstColon = line.indexOf(":");
|
|
3494
|
+
if (firstColon === -1)
|
|
3495
|
+
continue;
|
|
3496
|
+
const filePath = line.slice(0, firstColon);
|
|
3497
|
+
const rest = line.slice(firstColon + 1);
|
|
3498
|
+
const secondColon = rest.indexOf(":");
|
|
3499
|
+
if (secondColon === -1)
|
|
3500
|
+
continue;
|
|
3501
|
+
const lineNumber = Number.parseInt(rest.slice(0, secondColon), 10);
|
|
3502
|
+
if (Number.isNaN(lineNumber))
|
|
3503
|
+
continue;
|
|
3504
|
+
const fileName = basename2(filePath);
|
|
3505
|
+
const processName = fileName.replace(/\.log$/, "");
|
|
3506
|
+
const lineText = rest.slice(secondColon + 1).toLowerCase();
|
|
3507
|
+
let pos = 0;
|
|
3508
|
+
while (true) {
|
|
3509
|
+
const idx = lineText.indexOf(lowerQuery, pos);
|
|
3510
|
+
if (idx === -1)
|
|
3511
|
+
break;
|
|
3512
|
+
matches.push({
|
|
3513
|
+
process: processName,
|
|
3514
|
+
line: lineNumber - 1,
|
|
3515
|
+
start: idx,
|
|
3516
|
+
end: idx + query.length
|
|
3517
|
+
});
|
|
3518
|
+
pos = idx + 1;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
return matches;
|
|
3522
|
+
} catch {
|
|
3523
|
+
return [];
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3316
3526
|
truncate(name) {
|
|
3317
3527
|
const fd = this.files.get(name);
|
|
3318
3528
|
if (fd === undefined)
|
|
@@ -3349,6 +3559,10 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
3349
3559
|
}
|
|
3350
3560
|
shuttingDown = true;
|
|
3351
3561
|
app.shutdown().finally(() => {
|
|
3562
|
+
if (logWriter && !logWriter.isTemporary) {
|
|
3563
|
+
process.stderr.write(`Logs saved to: ${logWriter.getDirectory()}
|
|
3564
|
+
`);
|
|
3565
|
+
}
|
|
3352
3566
|
logWriter?.cleanup();
|
|
3353
3567
|
process.exit(app.hasFailures() ? 1 : 0);
|
|
3354
3568
|
});
|
|
@@ -3561,7 +3775,7 @@ async function main() {
|
|
|
3561
3775
|
proc.envFile = parsed.envFile;
|
|
3562
3776
|
}
|
|
3563
3777
|
}
|
|
3564
|
-
if (parsed.noWatch) {
|
|
3778
|
+
if (parsed.noWatch || config.noWatch) {
|
|
3565
3779
|
for (const proc of Object.values(config.processes)) {
|
|
3566
3780
|
delete proc.watch;
|
|
3567
3781
|
}
|
|
@@ -3577,9 +3791,11 @@ async function main() {
|
|
|
3577
3791
|
}
|
|
3578
3792
|
}
|
|
3579
3793
|
const manager = new ProcessManager(config);
|
|
3580
|
-
const
|
|
3794
|
+
const logDir = parsed.logDir ?? config.logDir;
|
|
3795
|
+
const logWriter = logDir ? LogWriter.createPersistent(logDir) : LogWriter.createTemp();
|
|
3581
3796
|
printWarnings(warnings);
|
|
3582
|
-
|
|
3797
|
+
const usePrefix = parsed.prefix || config.prefix;
|
|
3798
|
+
if (usePrefix) {
|
|
3583
3799
|
if (!parsed.noRestart) {
|
|
3584
3800
|
for (const proc of Object.values(config.processes)) {
|
|
3585
3801
|
proc.maxRestarts ??= 0;
|
|
@@ -3587,8 +3803,8 @@ async function main() {
|
|
|
3587
3803
|
}
|
|
3588
3804
|
const display = new PrefixDisplay(manager, config, {
|
|
3589
3805
|
logWriter,
|
|
3590
|
-
killOthers: parsed.killOthers,
|
|
3591
|
-
timestamps: parsed.timestamps
|
|
3806
|
+
killOthers: parsed.killOthers || config.killOthers,
|
|
3807
|
+
timestamps: parsed.timestamps || config.timestamps
|
|
3592
3808
|
});
|
|
3593
3809
|
await display.start();
|
|
3594
3810
|
} else {
|
package/dist/types.d.ts
CHANGED
|
@@ -95,6 +95,25 @@ export interface NumuxConfig<K extends string = string> {
|
|
|
95
95
|
* @default 'config'
|
|
96
96
|
*/
|
|
97
97
|
sort?: SortOrder;
|
|
98
|
+
/**
|
|
99
|
+
* Use prefixed output mode instead of TUI (for CI/scripts)
|
|
100
|
+
* @default false
|
|
101
|
+
*/
|
|
102
|
+
prefix?: boolean;
|
|
103
|
+
/** Add timestamps to prefixed output lines (only applies when `prefix` is true) */
|
|
104
|
+
timestamps?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Kill all processes when any one exits
|
|
107
|
+
* @default false
|
|
108
|
+
*/
|
|
109
|
+
killOthers?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Disable file watching even if processes have watch patterns
|
|
112
|
+
* @default false
|
|
113
|
+
*/
|
|
114
|
+
noWatch?: boolean;
|
|
115
|
+
/** Directory to write per-process log files */
|
|
116
|
+
logDir?: string;
|
|
98
117
|
processes: Record<K, NumuxProcessConfig<K> | NumuxScriptPattern<K> | string>;
|
|
99
118
|
}
|
|
100
119
|
export type SortOrder = 'config' | 'alphabetical' | 'topological';
|
|
@@ -105,6 +124,11 @@ export interface ResolvedProcessConfig extends Omit<NumuxProcessConfig, 'depends
|
|
|
105
124
|
/** Validated config with all shorthand expanded to full objects */
|
|
106
125
|
export interface ResolvedNumuxConfig {
|
|
107
126
|
sort?: SortOrder;
|
|
127
|
+
prefix?: boolean;
|
|
128
|
+
timestamps?: boolean;
|
|
129
|
+
killOthers?: boolean;
|
|
130
|
+
noWatch?: boolean;
|
|
131
|
+
logDir?: string;
|
|
108
132
|
processes: Record<string, ResolvedProcessConfig>;
|
|
109
133
|
}
|
|
110
134
|
export type ProcessStatus = 'pending' | 'starting' | 'ready' | 'running' | 'stopping' | 'stopped' | 'finished' | 'failed' | 'skipped';
|
|
@@ -131,3 +155,10 @@ export type ProcessEvent = {
|
|
|
131
155
|
type: 'error';
|
|
132
156
|
name: string;
|
|
133
157
|
};
|
|
158
|
+
export interface KeyEvent {
|
|
159
|
+
ctrl: boolean;
|
|
160
|
+
shift: boolean;
|
|
161
|
+
meta: boolean;
|
|
162
|
+
name: string;
|
|
163
|
+
sequence: string;
|
|
164
|
+
}
|