numux 1.23.2 → 1.25.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 +21 -1
- package/dist/numux.js +330 -128
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Terminal multiplexer with dependency orchestration. Run multiple processes in a tabbed TUI with a dependency graph controlling startup order.
|
|
4
4
|
|
|
5
|
+
Inspired by `sst dev` and `concurrently`
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
Requires [Bun](https://bun.sh) >= 1.0.
|
|
@@ -167,7 +169,7 @@ Template properties (color, env, dependsOn, etc.) are inherited by all matched p
|
|
|
167
169
|
| `-s, --sort <mode>` | Tab display order: `config` (default), `alphabetical`, `topological` |
|
|
168
170
|
| `--no-watch` | Disable file watching even if config has `watch` patterns |
|
|
169
171
|
| `-t, --timestamps` | Add `[HH:MM:SS]` timestamps to prefixed output |
|
|
170
|
-
| `--log-dir <path>` |
|
|
172
|
+
| `--log-dir <path>` | Persist logs to timestamped subdirs (`<path>/<timestamp>/<name>.log`) with a `latest` symlink. Path is printed on exit |
|
|
171
173
|
| `--debug` | Log to `.numux/debug.log` |
|
|
172
174
|
| `-h, --help` | Show help |
|
|
173
175
|
| `-v, --version` | Show version |
|
|
@@ -194,6 +196,7 @@ Top-level options apply to all processes (process-level settings override):
|
|
|
194
196
|
| `env` | `Record<string, string>` | Environment variables merged into all processes (process `env` overrides per key) |
|
|
195
197
|
| `envFile` | `string \| string[] \| false` | `.env` file(s) for all processes (process `envFile` replaces if set; `false` disables) |
|
|
196
198
|
| `showCommand` | `boolean` | Print the command being run as the first line of output (default: `true`) |
|
|
199
|
+
| `persistent` | `boolean` | Set to `false` to make all processes one-shot by default (default: `true`) |
|
|
197
200
|
| `maxRestarts` | `number` | Restart limit for all processes (default: `Infinity`) |
|
|
198
201
|
| `readyTimeout` | `number` | Ready timeout in ms for all processes |
|
|
199
202
|
| `stopSignal` | `'SIGTERM' \| 'SIGINT' \| 'SIGHUP'` | Stop signal for all processes (default: `'SIGTERM'`) |
|
|
@@ -347,6 +350,23 @@ Unmatched references are left as-is (the shell will expand `$db` as empty + `.po
|
|
|
347
350
|
|
|
348
351
|
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
352
|
|
|
353
|
+
| Key | Action |
|
|
354
|
+
|-----|--------|
|
|
355
|
+
| `←`/`→` or `1`-`9` | Switch tabs |
|
|
356
|
+
| `F` | Search current pane |
|
|
357
|
+
| `Tab` (in search) | Toggle between single-pane and all-process search |
|
|
358
|
+
| `Enter`/`Shift+Enter` | Next/previous match |
|
|
359
|
+
| `Esc` | Exit search |
|
|
360
|
+
| `R` | Restart current process |
|
|
361
|
+
| `Shift+R` | Restart all processes |
|
|
362
|
+
| `S` | Stop/start current process |
|
|
363
|
+
| `Y` | Copy all output |
|
|
364
|
+
| `L` | Clear pane |
|
|
365
|
+
| `G`/`Shift+G` | Scroll to top/bottom |
|
|
366
|
+
| `PageUp`/`PageDown` | Scroll by page |
|
|
367
|
+
| `Ctrl+Click` | Open link |
|
|
368
|
+
| `Ctrl+C` | Quit |
|
|
369
|
+
|
|
350
370
|
## Tab icons
|
|
351
371
|
|
|
352
372
|
| 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.25.0",
|
|
40
40
|
description: "Terminal multiplexer with dependency orchestration",
|
|
41
41
|
type: "module",
|
|
42
42
|
license: "MIT",
|
|
@@ -1046,6 +1046,7 @@ function validateConfig(raw, warnings) {
|
|
|
1046
1046
|
const globalEnvFile = validateEnvFile(config.envFile);
|
|
1047
1047
|
const globalMaxRestarts = typeof config.maxRestarts === "number" && config.maxRestarts >= 0 ? config.maxRestarts : undefined;
|
|
1048
1048
|
const globalReadyTimeout = typeof config.readyTimeout === "number" && config.readyTimeout > 0 ? config.readyTimeout : undefined;
|
|
1049
|
+
const globalPersistent = typeof config.persistent === "boolean" ? config.persistent : undefined;
|
|
1049
1050
|
const globalStopSignal = validateStopSignal(config.stopSignal);
|
|
1050
1051
|
const globalErrorMatcher = validateErrorMatcher("(global)", config.errorMatcher);
|
|
1051
1052
|
const globalWatch = validateStringOrStringArray(config.watch);
|
|
@@ -1106,7 +1107,7 @@ function validateConfig(raw, warnings) {
|
|
|
1106
1107
|
}
|
|
1107
1108
|
}
|
|
1108
1109
|
}
|
|
1109
|
-
const persistent = typeof p.persistent === "boolean" ? p.persistent : true;
|
|
1110
|
+
const persistent = typeof p.persistent === "boolean" ? p.persistent : globalPersistent ?? true;
|
|
1110
1111
|
const readyPattern = p.readyPattern instanceof RegExp ? p.readyPattern : typeof p.readyPattern === "string" ? p.readyPattern : undefined;
|
|
1111
1112
|
if (typeof readyPattern === "string") {
|
|
1112
1113
|
try {
|
|
@@ -2320,6 +2321,197 @@ class Pane {
|
|
|
2320
2321
|
}
|
|
2321
2322
|
}
|
|
2322
2323
|
|
|
2324
|
+
// src/ui/search.ts
|
|
2325
|
+
class SearchController {
|
|
2326
|
+
mode = false;
|
|
2327
|
+
allMode = false;
|
|
2328
|
+
query = "";
|
|
2329
|
+
matches = [];
|
|
2330
|
+
index = -1;
|
|
2331
|
+
crossMatches = [];
|
|
2332
|
+
crossCounts = new Map;
|
|
2333
|
+
timer = null;
|
|
2334
|
+
logWriter;
|
|
2335
|
+
statusBar;
|
|
2336
|
+
tabBar;
|
|
2337
|
+
getActivePane;
|
|
2338
|
+
getPane;
|
|
2339
|
+
constructor(opts) {
|
|
2340
|
+
this.logWriter = opts.logWriter;
|
|
2341
|
+
this.statusBar = opts.statusBar;
|
|
2342
|
+
this.tabBar = opts.tabBar;
|
|
2343
|
+
this.getActivePane = opts.getActivePane;
|
|
2344
|
+
this.getPane = opts.getPane;
|
|
2345
|
+
}
|
|
2346
|
+
get isActive() {
|
|
2347
|
+
return this.mode;
|
|
2348
|
+
}
|
|
2349
|
+
get isAllMode() {
|
|
2350
|
+
return this.allMode;
|
|
2351
|
+
}
|
|
2352
|
+
get currentMatches() {
|
|
2353
|
+
return this.matches;
|
|
2354
|
+
}
|
|
2355
|
+
enter() {
|
|
2356
|
+
this.mode = true;
|
|
2357
|
+
this.query = "";
|
|
2358
|
+
this.matches = [];
|
|
2359
|
+
this.index = -1;
|
|
2360
|
+
this.allMode = false;
|
|
2361
|
+
this.crossMatches = [];
|
|
2362
|
+
this.crossCounts.clear();
|
|
2363
|
+
this.statusBar.setSearchMode(true);
|
|
2364
|
+
}
|
|
2365
|
+
exit() {
|
|
2366
|
+
this.mode = false;
|
|
2367
|
+
this.query = "";
|
|
2368
|
+
this.matches = [];
|
|
2369
|
+
this.index = -1;
|
|
2370
|
+
this.allMode = false;
|
|
2371
|
+
this.crossMatches = [];
|
|
2372
|
+
this.crossCounts.clear();
|
|
2373
|
+
if (this.timer) {
|
|
2374
|
+
clearTimeout(this.timer);
|
|
2375
|
+
this.timer = null;
|
|
2376
|
+
}
|
|
2377
|
+
const active = this.getActivePane();
|
|
2378
|
+
if (active) {
|
|
2379
|
+
this.getPane(active)?.clearHighlights();
|
|
2380
|
+
}
|
|
2381
|
+
this.tabBar.clearSearchMatches();
|
|
2382
|
+
this.statusBar.setSearchMode(false);
|
|
2383
|
+
}
|
|
2384
|
+
onPaneSwitch() {
|
|
2385
|
+
if (this.mode && this.allMode) {
|
|
2386
|
+
this.applyPaneMatches();
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
refreshHighlights() {
|
|
2390
|
+
if (this.matches.length > 0) {
|
|
2391
|
+
this.updateHighlights();
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
handleInput(key) {
|
|
2395
|
+
if (key.name === "escape") {
|
|
2396
|
+
this.exit();
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (key.name === "tab") {
|
|
2400
|
+
this.allMode = !this.allMode;
|
|
2401
|
+
if (!this.allMode) {
|
|
2402
|
+
this.crossMatches = [];
|
|
2403
|
+
this.crossCounts.clear();
|
|
2404
|
+
this.tabBar.clearSearchMatches();
|
|
2405
|
+
}
|
|
2406
|
+
this.scheduleSearch();
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
if (key.name === "return") {
|
|
2410
|
+
if (this.matches.length === 0)
|
|
2411
|
+
return;
|
|
2412
|
+
if (key.shift) {
|
|
2413
|
+
this.index = (this.index - 1 + this.matches.length) % this.matches.length;
|
|
2414
|
+
} else {
|
|
2415
|
+
this.index = (this.index + 1) % this.matches.length;
|
|
2416
|
+
}
|
|
2417
|
+
this.scrollToMatch();
|
|
2418
|
+
this.updateHighlights();
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
if (key.name === "backspace") {
|
|
2422
|
+
if (this.query.length > 0) {
|
|
2423
|
+
this.query = this.query.slice(0, -1);
|
|
2424
|
+
this.scheduleSearch();
|
|
2425
|
+
}
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2429
|
+
this.query += key.sequence;
|
|
2430
|
+
this.scheduleSearch();
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
dispose() {
|
|
2434
|
+
if (this.timer) {
|
|
2435
|
+
clearTimeout(this.timer);
|
|
2436
|
+
this.timer = null;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
scheduleSearch() {
|
|
2440
|
+
this.statusBar.setSearchMode(true, this.query, this.matches.length, this.index, this.crossInfo());
|
|
2441
|
+
if (this.timer)
|
|
2442
|
+
clearTimeout(this.timer);
|
|
2443
|
+
this.timer = setTimeout(() => {
|
|
2444
|
+
this.timer = null;
|
|
2445
|
+
this.runSearch();
|
|
2446
|
+
}, 100);
|
|
2447
|
+
}
|
|
2448
|
+
async runSearch() {
|
|
2449
|
+
const active = this.getActivePane();
|
|
2450
|
+
if (!active)
|
|
2451
|
+
return;
|
|
2452
|
+
const query = this.query;
|
|
2453
|
+
const allMode = this.allMode;
|
|
2454
|
+
if (allMode) {
|
|
2455
|
+
const allMatches = await this.logWriter.searchAll(query);
|
|
2456
|
+
if (!this.mode || this.query !== query || !this.allMode)
|
|
2457
|
+
return;
|
|
2458
|
+
this.crossMatches = allMatches;
|
|
2459
|
+
this.crossCounts.clear();
|
|
2460
|
+
for (const m of allMatches) {
|
|
2461
|
+
this.crossCounts.set(m.process, (this.crossCounts.get(m.process) ?? 0) + 1);
|
|
2462
|
+
}
|
|
2463
|
+
this.tabBar.setSearchMatches(this.crossCounts);
|
|
2464
|
+
this.applyPaneMatches();
|
|
2465
|
+
} else {
|
|
2466
|
+
const matches = await this.logWriter.search(active, query);
|
|
2467
|
+
if (!this.mode || this.query !== query || this.getActivePane() !== active)
|
|
2468
|
+
return;
|
|
2469
|
+
this.matches = matches;
|
|
2470
|
+
this.index = matches.length > 0 ? 0 : -1;
|
|
2471
|
+
this.updateHighlights();
|
|
2472
|
+
if (this.index >= 0)
|
|
2473
|
+
this.scrollToMatch();
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
applyPaneMatches() {
|
|
2477
|
+
const active = this.getActivePane();
|
|
2478
|
+
if (!active)
|
|
2479
|
+
return;
|
|
2480
|
+
const paneMatches = this.crossMatches.filter((m) => m.process === active).map(({ line, start, end }) => ({ line, start, end }));
|
|
2481
|
+
this.matches = paneMatches;
|
|
2482
|
+
this.index = paneMatches.length > 0 ? 0 : -1;
|
|
2483
|
+
this.updateHighlights();
|
|
2484
|
+
if (this.index >= 0)
|
|
2485
|
+
this.scrollToMatch();
|
|
2486
|
+
}
|
|
2487
|
+
updateHighlights() {
|
|
2488
|
+
const active = this.getActivePane();
|
|
2489
|
+
if (!active)
|
|
2490
|
+
return;
|
|
2491
|
+
const pane = this.getPane(active);
|
|
2492
|
+
if (!pane)
|
|
2493
|
+
return;
|
|
2494
|
+
if (this.matches.length > 0) {
|
|
2495
|
+
pane.setHighlights(this.matches, this.index);
|
|
2496
|
+
} else {
|
|
2497
|
+
pane.clearHighlights();
|
|
2498
|
+
}
|
|
2499
|
+
this.statusBar.setSearchMode(true, this.query, this.matches.length, this.index, this.crossInfo());
|
|
2500
|
+
}
|
|
2501
|
+
scrollToMatch() {
|
|
2502
|
+
const active = this.getActivePane();
|
|
2503
|
+
if (!active || this.index < 0)
|
|
2504
|
+
return;
|
|
2505
|
+
const pane = this.getPane(active);
|
|
2506
|
+
if (!pane)
|
|
2507
|
+
return;
|
|
2508
|
+
pane.scrollToLine(this.matches[this.index].line);
|
|
2509
|
+
}
|
|
2510
|
+
crossInfo() {
|
|
2511
|
+
return this.allMode ? { totalMatches: this.crossMatches.length, processCount: this.crossCounts.size } : undefined;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2323
2515
|
// src/ui/status-bar.ts
|
|
2324
2516
|
import { cyan, red, reverse, StyledText, TextRenderable, yellow } from "@opentui/core";
|
|
2325
2517
|
function plain(text) {
|
|
@@ -2332,6 +2524,7 @@ class StatusBar {
|
|
|
2332
2524
|
_searchQuery = "";
|
|
2333
2525
|
_searchMatchCount = 0;
|
|
2334
2526
|
_searchCurrentIndex = -1;
|
|
2527
|
+
_crossProcessInfo;
|
|
2335
2528
|
_tempMessage = null;
|
|
2336
2529
|
_tempTimer = null;
|
|
2337
2530
|
constructor(renderer) {
|
|
@@ -2344,11 +2537,12 @@ class StatusBar {
|
|
|
2344
2537
|
paddingX: 1
|
|
2345
2538
|
});
|
|
2346
2539
|
}
|
|
2347
|
-
setSearchMode(active, query = "", matchCount = 0, currentIndex = -1) {
|
|
2540
|
+
setSearchMode(active, query = "", matchCount = 0, currentIndex = -1, crossProcessInfo) {
|
|
2348
2541
|
this._searchMode = active;
|
|
2349
2542
|
this._searchQuery = query;
|
|
2350
2543
|
this._searchMatchCount = matchCount;
|
|
2351
2544
|
this._searchCurrentIndex = currentIndex;
|
|
2545
|
+
this._crossProcessInfo = crossProcessInfo;
|
|
2352
2546
|
this.renderable.content = this.buildContent();
|
|
2353
2547
|
}
|
|
2354
2548
|
showTemporaryMessage(message, duration = 2000) {
|
|
@@ -2373,6 +2567,7 @@ class StatusBar {
|
|
|
2373
2567
|
}
|
|
2374
2568
|
buildSearchContent() {
|
|
2375
2569
|
const chunks = [];
|
|
2570
|
+
const isAllMode = !!this._crossProcessInfo;
|
|
2376
2571
|
chunks.push(yellow("/"));
|
|
2377
2572
|
if (this._searchQuery)
|
|
2378
2573
|
chunks.push(plain(this._searchQuery));
|
|
@@ -2380,14 +2575,20 @@ class StatusBar {
|
|
|
2380
2575
|
if (this._searchMatchCount === 0 && this._searchQuery) {
|
|
2381
2576
|
chunks.push(plain(" "));
|
|
2382
2577
|
chunks.push(red("no matches"));
|
|
2383
|
-
chunks.push(plain(" Esc: close"));
|
|
2384
2578
|
} else if (this._searchMatchCount > 0) {
|
|
2385
2579
|
chunks.push(plain(" "));
|
|
2386
|
-
|
|
2387
|
-
|
|
2580
|
+
if (isAllMode) {
|
|
2581
|
+
const { totalMatches, processCount } = this._crossProcessInfo;
|
|
2582
|
+
chunks.push(cyan(`${this._searchCurrentIndex + 1}/${this._searchMatchCount}`));
|
|
2583
|
+
chunks.push(plain(` ${totalMatches} in ${processCount} process${processCount === 1 ? "" : "es"}`));
|
|
2584
|
+
} else {
|
|
2585
|
+
chunks.push(cyan(`${this._searchCurrentIndex + 1}/${this._searchMatchCount}`));
|
|
2586
|
+
}
|
|
2587
|
+
chunks.push(plain(" Enter: next"));
|
|
2388
2588
|
} else {
|
|
2389
|
-
chunks.push(plain(" Enter: next
|
|
2589
|
+
chunks.push(plain(" Enter: next"));
|
|
2390
2590
|
}
|
|
2591
|
+
chunks.push(plain(` Tab: ${isAllMode ? "single" : "all"} Esc: close`));
|
|
2391
2592
|
return new StyledText(chunks);
|
|
2392
2593
|
}
|
|
2393
2594
|
}
|
|
@@ -2435,12 +2636,13 @@ function getDisplayOrder(originalNames, statuses) {
|
|
|
2435
2636
|
const terminal = originalNames.filter((n) => TERMINAL_STATUSES.has(statuses.get(n)));
|
|
2436
2637
|
return [...active, ...terminal];
|
|
2437
2638
|
}
|
|
2438
|
-
function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses) {
|
|
2639
|
+
function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses, searchMatchProcesses) {
|
|
2439
2640
|
return names.map((name) => {
|
|
2440
2641
|
const status = statuses.get(name);
|
|
2441
2642
|
const waiting = inputWaiting.has(name);
|
|
2442
2643
|
const errored = erroredProcesses.has(name);
|
|
2443
|
-
const
|
|
2644
|
+
const hasSearchMatch = searchMatchProcesses?.has(name);
|
|
2645
|
+
const statusHex = hasSearchMatch ? "#b58900" : waiting ? "#ffaa00" : errored ? "#ff5555" : STATUS_ICON_HEX[status];
|
|
2444
2646
|
const processHex = processColors.get(name);
|
|
2445
2647
|
return {
|
|
2446
2648
|
iconHex: statusHex ?? processHex ?? "#888888",
|
|
@@ -2525,6 +2727,7 @@ class TabBar {
|
|
|
2525
2727
|
processColors;
|
|
2526
2728
|
inputWaiting = new Set;
|
|
2527
2729
|
erroredProcesses = new Set;
|
|
2730
|
+
searchMatchCounts = new Map;
|
|
2528
2731
|
constructor(renderer, names, colors) {
|
|
2529
2732
|
this.originalNames = names;
|
|
2530
2733
|
this.names = [...names];
|
|
@@ -2582,6 +2785,14 @@ class TabBar {
|
|
|
2582
2785
|
this.erroredProcesses.delete(name);
|
|
2583
2786
|
this.refreshOptions();
|
|
2584
2787
|
}
|
|
2788
|
+
setSearchMatches(counts) {
|
|
2789
|
+
this.searchMatchCounts = counts;
|
|
2790
|
+
this.refreshOptions();
|
|
2791
|
+
}
|
|
2792
|
+
clearSearchMatches() {
|
|
2793
|
+
this.searchMatchCounts.clear();
|
|
2794
|
+
this.refreshOptions();
|
|
2795
|
+
}
|
|
2585
2796
|
getNameAtIndex(index) {
|
|
2586
2797
|
return this.names[index];
|
|
2587
2798
|
}
|
|
@@ -2603,6 +2814,10 @@ class TabBar {
|
|
|
2603
2814
|
this.updateOptionColors();
|
|
2604
2815
|
}
|
|
2605
2816
|
getDescription(name) {
|
|
2817
|
+
const matchCount = this.searchMatchCounts.get(name);
|
|
2818
|
+
if (matchCount != null && matchCount > 0) {
|
|
2819
|
+
return `${matchCount} match${matchCount === 1 ? "" : "es"}`;
|
|
2820
|
+
}
|
|
2606
2821
|
if (this.inputWaiting.has(name))
|
|
2607
2822
|
return "awaiting input";
|
|
2608
2823
|
if (this.erroredProcesses.has(name))
|
|
@@ -2610,7 +2825,8 @@ class TabBar {
|
|
|
2610
2825
|
return this.baseDescriptions.get(name) ?? "pending";
|
|
2611
2826
|
}
|
|
2612
2827
|
updateOptionColors() {
|
|
2613
|
-
const
|
|
2828
|
+
const searchProcesses = this.searchMatchCounts.size > 0 ? new Set(this.searchMatchCounts.keys()) : undefined;
|
|
2829
|
+
const resolved = resolveOptionColors(this.names, this.statuses, this.processColors, this.inputWaiting, this.erroredProcesses, searchProcesses);
|
|
2614
2830
|
const colors = resolved.map((c) => ({
|
|
2615
2831
|
icon: parseColor(c.iconHex),
|
|
2616
2832
|
name: c.nameHex ? parseColor(c.nameHex) : null
|
|
@@ -2635,6 +2851,7 @@ class App {
|
|
|
2635
2851
|
panes = new Map;
|
|
2636
2852
|
tabBar;
|
|
2637
2853
|
statusBar;
|
|
2854
|
+
search;
|
|
2638
2855
|
activePane = null;
|
|
2639
2856
|
destroyed = false;
|
|
2640
2857
|
names;
|
|
@@ -2644,11 +2861,6 @@ class App {
|
|
|
2644
2861
|
config;
|
|
2645
2862
|
logWriter;
|
|
2646
2863
|
resizeTimer = null;
|
|
2647
|
-
searchTimer = null;
|
|
2648
|
-
searchMode = false;
|
|
2649
|
-
searchQuery = "";
|
|
2650
|
-
searchMatches = [];
|
|
2651
|
-
searchIndex = -1;
|
|
2652
2864
|
inputWaitTimers = new Map;
|
|
2653
2865
|
awaitingInput = new Set;
|
|
2654
2866
|
constructor(manager, config, logWriter) {
|
|
@@ -2698,6 +2910,14 @@ class App {
|
|
|
2698
2910
|
flexGrow: 1,
|
|
2699
2911
|
border: false
|
|
2700
2912
|
});
|
|
2913
|
+
this.statusBar = new StatusBar(this.renderer);
|
|
2914
|
+
this.search = new SearchController({
|
|
2915
|
+
logWriter: this.logWriter,
|
|
2916
|
+
statusBar: this.statusBar,
|
|
2917
|
+
tabBar: this.tabBar,
|
|
2918
|
+
getActivePane: () => this.activePane,
|
|
2919
|
+
getPane: (name) => this.panes.get(name)
|
|
2920
|
+
});
|
|
2701
2921
|
for (const name of this.names) {
|
|
2702
2922
|
const interactive = this.config.processes[name].interactive === true;
|
|
2703
2923
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
@@ -2710,14 +2930,13 @@ class App {
|
|
|
2710
2930
|
this.statusBar.showTemporaryMessage(`Opening ${link.url}`);
|
|
2711
2931
|
});
|
|
2712
2932
|
pane.onScroll(() => {
|
|
2713
|
-
if (this.
|
|
2714
|
-
this.
|
|
2933
|
+
if (this.search.isActive && this.search.currentMatches.length > 0 && this.activePane === name) {
|
|
2934
|
+
this.search.refreshHighlights();
|
|
2715
2935
|
}
|
|
2716
2936
|
});
|
|
2717
2937
|
this.panes.set(name, pane);
|
|
2718
2938
|
paneContainer.add(pane.scrollBox);
|
|
2719
2939
|
}
|
|
2720
|
-
this.statusBar = new StatusBar(this.renderer);
|
|
2721
2940
|
contentRow.add(sidebar);
|
|
2722
2941
|
contentRow.add(paneContainer);
|
|
2723
2942
|
layout.add(contentRow);
|
|
@@ -2759,8 +2978,8 @@ class App {
|
|
|
2759
2978
|
this.renderer.keyInput.on("keypress", (key) => {
|
|
2760
2979
|
log(key);
|
|
2761
2980
|
if (key.ctrl && key.name === "c") {
|
|
2762
|
-
if (this.
|
|
2763
|
-
this.
|
|
2981
|
+
if (this.search.isActive) {
|
|
2982
|
+
this.search.exit();
|
|
2764
2983
|
return;
|
|
2765
2984
|
}
|
|
2766
2985
|
this.shutdown().then(() => {
|
|
@@ -2768,8 +2987,8 @@ class App {
|
|
|
2768
2987
|
});
|
|
2769
2988
|
return;
|
|
2770
2989
|
}
|
|
2771
|
-
if (this.
|
|
2772
|
-
this.
|
|
2990
|
+
if (this.search.isActive) {
|
|
2991
|
+
this.search.handleInput(key);
|
|
2773
2992
|
return;
|
|
2774
2993
|
}
|
|
2775
2994
|
if (!this.activePane)
|
|
@@ -2794,7 +3013,7 @@ class App {
|
|
|
2794
3013
|
return;
|
|
2795
3014
|
}
|
|
2796
3015
|
if (name === SHORTCUTS.search.key) {
|
|
2797
|
-
this.
|
|
3016
|
+
this.search.enter();
|
|
2798
3017
|
return;
|
|
2799
3018
|
}
|
|
2800
3019
|
if (name === SHORTCUTS.restart.key) {
|
|
@@ -2858,14 +3077,16 @@ class App {
|
|
|
2858
3077
|
switchPane(name) {
|
|
2859
3078
|
if (this.activePane === name)
|
|
2860
3079
|
return;
|
|
2861
|
-
if (this.
|
|
2862
|
-
this.
|
|
3080
|
+
if (this.search.isActive && !this.search.isAllMode) {
|
|
3081
|
+
this.search.exit();
|
|
2863
3082
|
}
|
|
2864
3083
|
if (this.activePane) {
|
|
3084
|
+
this.panes.get(this.activePane)?.clearHighlights();
|
|
2865
3085
|
this.panes.get(this.activePane)?.hide();
|
|
2866
3086
|
}
|
|
2867
3087
|
this.activePane = name;
|
|
2868
3088
|
this.panes.get(name)?.show();
|
|
3089
|
+
this.search.onPaneSwitch();
|
|
2869
3090
|
}
|
|
2870
3091
|
checkInputWaiting(name, data) {
|
|
2871
3092
|
const existing = this.inputWaitTimers.get(name);
|
|
@@ -2926,102 +3147,6 @@ class App {
|
|
|
2926
3147
|
this.copyToClipboard(text);
|
|
2927
3148
|
this.statusBar.showTemporaryMessage("Copied all output!");
|
|
2928
3149
|
}
|
|
2929
|
-
enterSearch() {
|
|
2930
|
-
this.searchMode = true;
|
|
2931
|
-
this.searchQuery = "";
|
|
2932
|
-
this.searchMatches = [];
|
|
2933
|
-
this.searchIndex = -1;
|
|
2934
|
-
this.statusBar.setSearchMode(true);
|
|
2935
|
-
}
|
|
2936
|
-
exitSearch() {
|
|
2937
|
-
this.searchMode = false;
|
|
2938
|
-
this.searchQuery = "";
|
|
2939
|
-
this.searchMatches = [];
|
|
2940
|
-
this.searchIndex = -1;
|
|
2941
|
-
if (this.searchTimer) {
|
|
2942
|
-
clearTimeout(this.searchTimer);
|
|
2943
|
-
this.searchTimer = null;
|
|
2944
|
-
}
|
|
2945
|
-
if (this.activePane) {
|
|
2946
|
-
this.panes.get(this.activePane)?.clearHighlights();
|
|
2947
|
-
}
|
|
2948
|
-
this.statusBar.setSearchMode(false);
|
|
2949
|
-
}
|
|
2950
|
-
handleSearchInput(key) {
|
|
2951
|
-
if (key.name === "escape") {
|
|
2952
|
-
this.exitSearch();
|
|
2953
|
-
return;
|
|
2954
|
-
}
|
|
2955
|
-
if (key.name === "return") {
|
|
2956
|
-
if (this.searchMatches.length === 0)
|
|
2957
|
-
return;
|
|
2958
|
-
if (key.shift) {
|
|
2959
|
-
this.searchIndex = (this.searchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
|
|
2960
|
-
} else {
|
|
2961
|
-
this.searchIndex = (this.searchIndex + 1) % this.searchMatches.length;
|
|
2962
|
-
}
|
|
2963
|
-
this.scrollToCurrentMatch();
|
|
2964
|
-
this.updateSearchHighlights();
|
|
2965
|
-
return;
|
|
2966
|
-
}
|
|
2967
|
-
if (key.name === "backspace") {
|
|
2968
|
-
if (this.searchQuery.length > 0) {
|
|
2969
|
-
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
2970
|
-
this.scheduleSearch();
|
|
2971
|
-
}
|
|
2972
|
-
return;
|
|
2973
|
-
}
|
|
2974
|
-
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2975
|
-
this.searchQuery += key.sequence;
|
|
2976
|
-
this.scheduleSearch();
|
|
2977
|
-
}
|
|
2978
|
-
}
|
|
2979
|
-
scheduleSearch() {
|
|
2980
|
-
this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
|
|
2981
|
-
if (this.searchTimer)
|
|
2982
|
-
clearTimeout(this.searchTimer);
|
|
2983
|
-
this.searchTimer = setTimeout(() => {
|
|
2984
|
-
this.searchTimer = null;
|
|
2985
|
-
this.runSearch();
|
|
2986
|
-
}, 100);
|
|
2987
|
-
}
|
|
2988
|
-
async runSearch() {
|
|
2989
|
-
if (!this.activePane)
|
|
2990
|
-
return;
|
|
2991
|
-
const query = this.searchQuery;
|
|
2992
|
-
const activeName = this.activePane;
|
|
2993
|
-
const matches = await this.logWriter.search(activeName, query);
|
|
2994
|
-
if (!this.searchMode || this.searchQuery !== query || this.activePane !== activeName)
|
|
2995
|
-
return;
|
|
2996
|
-
this.searchMatches = matches;
|
|
2997
|
-
this.searchIndex = matches.length > 0 ? 0 : -1;
|
|
2998
|
-
this.updateSearchHighlights();
|
|
2999
|
-
if (this.searchIndex >= 0) {
|
|
3000
|
-
this.scrollToCurrentMatch();
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
updateSearchHighlights() {
|
|
3004
|
-
if (!this.activePane)
|
|
3005
|
-
return;
|
|
3006
|
-
const pane = this.panes.get(this.activePane);
|
|
3007
|
-
if (!pane)
|
|
3008
|
-
return;
|
|
3009
|
-
if (this.searchMatches.length > 0) {
|
|
3010
|
-
pane.setHighlights(this.searchMatches, this.searchIndex);
|
|
3011
|
-
} else {
|
|
3012
|
-
pane.clearHighlights();
|
|
3013
|
-
}
|
|
3014
|
-
this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
|
|
3015
|
-
}
|
|
3016
|
-
scrollToCurrentMatch() {
|
|
3017
|
-
if (!this.activePane || this.searchIndex < 0)
|
|
3018
|
-
return;
|
|
3019
|
-
const pane = this.panes.get(this.activePane);
|
|
3020
|
-
if (!pane)
|
|
3021
|
-
return;
|
|
3022
|
-
const match = this.searchMatches[this.searchIndex];
|
|
3023
|
-
pane.scrollToLine(match.line);
|
|
3024
|
-
}
|
|
3025
3150
|
async shutdown() {
|
|
3026
3151
|
if (this.destroyed)
|
|
3027
3152
|
return;
|
|
@@ -3030,10 +3155,7 @@ class App {
|
|
|
3030
3155
|
clearTimeout(this.resizeTimer);
|
|
3031
3156
|
this.resizeTimer = null;
|
|
3032
3157
|
}
|
|
3033
|
-
|
|
3034
|
-
clearTimeout(this.searchTimer);
|
|
3035
|
-
this.searchTimer = null;
|
|
3036
|
-
}
|
|
3158
|
+
this.search.dispose();
|
|
3037
3159
|
for (const timer of this.inputWaitTimers.values()) {
|
|
3038
3160
|
clearTimeout(timer);
|
|
3039
3161
|
}
|
|
@@ -3237,9 +3359,9 @@ ${DIM}Done in ${elapsed}${RESET}
|
|
|
3237
3359
|
}
|
|
3238
3360
|
|
|
3239
3361
|
// src/utils/log-writer.ts
|
|
3240
|
-
import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, writeSync } from "fs";
|
|
3362
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
|
|
3241
3363
|
import { tmpdir } from "os";
|
|
3242
|
-
import { join } from "path";
|
|
3364
|
+
import { basename as basename2, join } from "path";
|
|
3243
3365
|
class LogWriter {
|
|
3244
3366
|
dir;
|
|
3245
3367
|
isTemp;
|
|
@@ -3255,6 +3377,30 @@ class LogWriter {
|
|
|
3255
3377
|
const dir = join(tmpdir(), `numux-${process.pid}`);
|
|
3256
3378
|
return new LogWriter(dir, true);
|
|
3257
3379
|
}
|
|
3380
|
+
static createPersistent(baseDir) {
|
|
3381
|
+
mkdirSync2(baseDir, { recursive: true });
|
|
3382
|
+
const now = new Date;
|
|
3383
|
+
const ts = now.toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
3384
|
+
const sessionDir = join(baseDir, ts);
|
|
3385
|
+
mkdirSync2(sessionDir, { recursive: true });
|
|
3386
|
+
const latestLink = join(baseDir, "latest");
|
|
3387
|
+
try {
|
|
3388
|
+
unlinkSync(latestLink);
|
|
3389
|
+
} catch {}
|
|
3390
|
+
try {
|
|
3391
|
+
symlinkSync(sessionDir, latestLink);
|
|
3392
|
+
} catch {}
|
|
3393
|
+
return new LogWriter(sessionDir, false);
|
|
3394
|
+
}
|
|
3395
|
+
get isTemporary() {
|
|
3396
|
+
return this.isTemp;
|
|
3397
|
+
}
|
|
3398
|
+
getDirectory() {
|
|
3399
|
+
return this.dir;
|
|
3400
|
+
}
|
|
3401
|
+
getProcessNames() {
|
|
3402
|
+
return [...this.files.keys()];
|
|
3403
|
+
}
|
|
3258
3404
|
errored = false;
|
|
3259
3405
|
handleEvent = (event) => {
|
|
3260
3406
|
if (event.type !== "output" || this.errored)
|
|
@@ -3326,6 +3472,58 @@ class LogWriter {
|
|
|
3326
3472
|
return [];
|
|
3327
3473
|
}
|
|
3328
3474
|
}
|
|
3475
|
+
async searchAll(query) {
|
|
3476
|
+
if (!query)
|
|
3477
|
+
return [];
|
|
3478
|
+
const paths = [...this.files.keys()].map((name) => join(this.dir, `${name}.log`));
|
|
3479
|
+
if (paths.length === 0)
|
|
3480
|
+
return [];
|
|
3481
|
+
try {
|
|
3482
|
+
const proc = Bun.spawn(["grep", "-inFH", query, ...paths], {
|
|
3483
|
+
stdout: "pipe",
|
|
3484
|
+
stderr: "ignore"
|
|
3485
|
+
});
|
|
3486
|
+
const output = await new Response(proc.stdout).text();
|
|
3487
|
+
await proc.exited;
|
|
3488
|
+
const matches = [];
|
|
3489
|
+
const lowerQuery = query.toLowerCase();
|
|
3490
|
+
for (const line of output.split(`
|
|
3491
|
+
`)) {
|
|
3492
|
+
if (!line)
|
|
3493
|
+
continue;
|
|
3494
|
+
const firstColon = line.indexOf(":");
|
|
3495
|
+
if (firstColon === -1)
|
|
3496
|
+
continue;
|
|
3497
|
+
const filePath = line.slice(0, firstColon);
|
|
3498
|
+
const rest = line.slice(firstColon + 1);
|
|
3499
|
+
const secondColon = rest.indexOf(":");
|
|
3500
|
+
if (secondColon === -1)
|
|
3501
|
+
continue;
|
|
3502
|
+
const lineNumber = Number.parseInt(rest.slice(0, secondColon), 10);
|
|
3503
|
+
if (Number.isNaN(lineNumber))
|
|
3504
|
+
continue;
|
|
3505
|
+
const fileName = basename2(filePath);
|
|
3506
|
+
const processName = fileName.replace(/\.log$/, "");
|
|
3507
|
+
const lineText = rest.slice(secondColon + 1).toLowerCase();
|
|
3508
|
+
let pos = 0;
|
|
3509
|
+
while (true) {
|
|
3510
|
+
const idx = lineText.indexOf(lowerQuery, pos);
|
|
3511
|
+
if (idx === -1)
|
|
3512
|
+
break;
|
|
3513
|
+
matches.push({
|
|
3514
|
+
process: processName,
|
|
3515
|
+
line: lineNumber - 1,
|
|
3516
|
+
start: idx,
|
|
3517
|
+
end: idx + query.length
|
|
3518
|
+
});
|
|
3519
|
+
pos = idx + 1;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
return matches;
|
|
3523
|
+
} catch {
|
|
3524
|
+
return [];
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3329
3527
|
truncate(name) {
|
|
3330
3528
|
const fd = this.files.get(name);
|
|
3331
3529
|
if (fd === undefined)
|
|
@@ -3362,6 +3560,10 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
3362
3560
|
}
|
|
3363
3561
|
shuttingDown = true;
|
|
3364
3562
|
app.shutdown().finally(() => {
|
|
3563
|
+
if (logWriter && !logWriter.isTemporary) {
|
|
3564
|
+
process.stderr.write(`Logs saved to: ${logWriter.getDirectory()}
|
|
3565
|
+
`);
|
|
3566
|
+
}
|
|
3365
3567
|
logWriter?.cleanup();
|
|
3366
3568
|
process.exit(app.hasFailures() ? 1 : 0);
|
|
3367
3569
|
});
|
|
@@ -3591,7 +3793,7 @@ async function main() {
|
|
|
3591
3793
|
}
|
|
3592
3794
|
const manager = new ProcessManager(config);
|
|
3593
3795
|
const logDir = parsed.logDir ?? config.logDir;
|
|
3594
|
-
const logWriter = logDir ?
|
|
3796
|
+
const logWriter = logDir ? LogWriter.createPersistent(logDir) : LogWriter.createTemp();
|
|
3595
3797
|
printWarnings(warnings);
|
|
3596
3798
|
const usePrefix = parsed.prefix || config.prefix;
|
|
3597
3799
|
if (usePrefix) {
|
package/dist/types.d.ts
CHANGED
|
@@ -80,6 +80,12 @@ export interface NumuxConfig<K extends string = string> {
|
|
|
80
80
|
maxRestarts?: number;
|
|
81
81
|
/** Global ready timeout (ms), inherited by all processes */
|
|
82
82
|
readyTimeout?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Set to `false` to make all processes non-persistent (one-shot) by default.
|
|
85
|
+
* Individual processes can still override with their own `persistent` value.
|
|
86
|
+
* @default true
|
|
87
|
+
*/
|
|
88
|
+
persistent?: boolean;
|
|
83
89
|
/**
|
|
84
90
|
* Global stop signal, inherited by all processes
|
|
85
91
|
* @default 'SIGTERM'
|
|
@@ -155,3 +161,10 @@ export type ProcessEvent = {
|
|
|
155
161
|
type: 'error';
|
|
156
162
|
name: string;
|
|
157
163
|
};
|
|
164
|
+
export interface KeyEvent {
|
|
165
|
+
ctrl: boolean;
|
|
166
|
+
shift: boolean;
|
|
167
|
+
meta: boolean;
|
|
168
|
+
name: string;
|
|
169
|
+
sequence: string;
|
|
170
|
+
}
|