numux 1.8.0 → 1.10.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 +46 -16
- package/dist/numux.js +199 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,22 @@ numux completions fish | source
|
|
|
98
98
|
numux completions fish > ~/.config/fish/completions/numux.fish
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Workspaces
|
|
102
|
+
|
|
103
|
+
Run a `package.json` script across all workspaces in a monorepo:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
numux -w dev
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Reads the `workspaces` field from your root `package.json`, finds which workspaces have the given script, and spawns `<pm> run <script>` in each. The package manager is auto-detected from `packageManager` field or lockfiles.
|
|
110
|
+
|
|
111
|
+
Composes with other flags:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
numux -w dev -n redis="redis-server" --colors
|
|
115
|
+
```
|
|
116
|
+
|
|
101
117
|
### Ad-hoc commands
|
|
102
118
|
|
|
103
119
|
```sh
|
|
@@ -108,10 +124,39 @@ numux "bun dev:api" "bun dev:web"
|
|
|
108
124
|
numux -n api="bun dev:api" -n web="bun dev:web"
|
|
109
125
|
```
|
|
110
126
|
|
|
127
|
+
### Script patterns
|
|
128
|
+
|
|
129
|
+
Run multiple package.json scripts matching a glob pattern:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
numux 'dev:*' # all scripts matching dev:*
|
|
133
|
+
numux 'npm:*:dev' # explicit npm: prefix (same behavior)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Extra arguments after the pattern are forwarded to each matched command:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
numux 'lint:* --fix' # → bun run lint:js --fix, bun run lint:ts --fix
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In a config file, use the pattern as the process name:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
export default defineConfig({
|
|
146
|
+
processes: {
|
|
147
|
+
'dev:*': { color: ['green', 'cyan'] },
|
|
148
|
+
'lint:* --fix': {},
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Template properties (color, env, dependsOn, etc.) are inherited by all matched processes. Colors given as an array are distributed round-robin.
|
|
154
|
+
|
|
111
155
|
### Options
|
|
112
156
|
|
|
113
157
|
| Flag | Description |
|
|
114
158
|
|------|-------------|
|
|
159
|
+
| `-w, --workspace <script>` | Run a script across all workspaces |
|
|
115
160
|
| `-c, --config <path>` | Explicit config file path |
|
|
116
161
|
| `-n, --name <name=cmd>` | Add a named process (repeatable) |
|
|
117
162
|
| `-p, --prefix` | Prefixed output mode (no TUI, for CI/scripts) |
|
|
@@ -265,22 +310,7 @@ Persistent processes that crash are auto-restarted with exponential backoff (1s
|
|
|
265
310
|
|
|
266
311
|
## Keybindings
|
|
267
312
|
|
|
268
|
-
|
|
269
|
-
|-----|--------|
|
|
270
|
-
| `Ctrl+C` | Quit (graceful shutdown) |
|
|
271
|
-
| `R` | Restart active process |
|
|
272
|
-
| `Shift+R` | Restart all processes |
|
|
273
|
-
| `S` | Stop/start active process |
|
|
274
|
-
| `L` | Clear active pane output |
|
|
275
|
-
| `F` | Search in active pane output |
|
|
276
|
-
| `1`–`9` | Jump to tab |
|
|
277
|
-
| `Left/Right` | Cycle tabs |
|
|
278
|
-
| `PageUp/PageDown` | Scroll output by page |
|
|
279
|
-
| `Home/End` | Scroll to top/bottom |
|
|
280
|
-
|
|
281
|
-
While searching: type to filter, `Enter`/`Shift+Enter` to navigate matches, `Escape` to close.
|
|
282
|
-
|
|
283
|
-
Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
|
|
313
|
+
Keybindings are shown in the status bar at the bottom of the app. Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
|
|
284
314
|
|
|
285
315
|
## Tab icons
|
|
286
316
|
|
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.10.0",
|
|
26
26
|
description: "Terminal multiplexer with dependency orchestration",
|
|
27
27
|
type: "module",
|
|
28
28
|
license: "MIT",
|
|
@@ -75,8 +75,8 @@ var require_package = __commonJS((exports, module) => {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
// src/index.ts
|
|
78
|
-
import { existsSync as
|
|
79
|
-
import { resolve as
|
|
78
|
+
import { existsSync as existsSync5, writeFileSync } from "fs";
|
|
79
|
+
import { resolve as resolve8 } from "path";
|
|
80
80
|
|
|
81
81
|
// src/cli.ts
|
|
82
82
|
function parseArgs(argv) {
|
|
@@ -122,6 +122,8 @@ function parseArgs(argv) {
|
|
|
122
122
|
result.timestamps = true;
|
|
123
123
|
} else if (arg === "--no-restart") {
|
|
124
124
|
result.noRestart = true;
|
|
125
|
+
} else if (arg === "-w" || arg === "--workspace") {
|
|
126
|
+
result.workspace = consumeValue(arg);
|
|
125
127
|
} else if (arg === "--no-watch") {
|
|
126
128
|
result.noWatch = true;
|
|
127
129
|
} else if (arg === "--colors") {
|
|
@@ -274,7 +276,7 @@ _numux() {
|
|
|
274
276
|
return ;;
|
|
275
277
|
--only|--exclude)
|
|
276
278
|
return ;;
|
|
277
|
-
-n|--name)
|
|
279
|
+
-n|--name|-w|--workspace)
|
|
278
280
|
return ;;
|
|
279
281
|
completions)
|
|
280
282
|
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
@@ -282,7 +284,7 @@ _numux() {
|
|
|
282
284
|
esac
|
|
283
285
|
|
|
284
286
|
if [[ "$cur" == -* ]]; then
|
|
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") )
|
|
287
|
+
COMPREPLY=( $(compgen -W "-h --help -v --version -w --workspace -c --color --colors --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
|
|
286
288
|
else
|
|
287
289
|
local subcmds="init validate exec completions"
|
|
288
290
|
COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
|
|
@@ -306,6 +308,7 @@ _numux() {
|
|
|
306
308
|
_arguments -s \\
|
|
307
309
|
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
308
310
|
'(-v --version)'{-v,--version}'[Show version]' \\
|
|
311
|
+
'(-w --workspace)'{-w,--workspace}'[Run script across all workspaces]:script' \\
|
|
309
312
|
'(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
|
|
310
313
|
'--colors[Auto-assign colors based on process name]' \\
|
|
311
314
|
'--config[Config file path]:file:_files' \\
|
|
@@ -352,6 +355,7 @@ complete -c numux -s v -l version -d 'Show version'
|
|
|
352
355
|
complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
|
|
353
356
|
complete -c numux -l colors -d 'Auto-assign colors based on process name'
|
|
354
357
|
complete -c numux -l config -rF -d 'Config file path'
|
|
358
|
+
complete -c numux -s w -l workspace -r -d 'Run script across all workspaces'
|
|
355
359
|
complete -c numux -s n -l name -r -d 'Named process (name=command)'
|
|
356
360
|
complete -c numux -s p -l prefix -d 'Prefixed output mode'
|
|
357
361
|
complete -c numux -l only -r -d 'Only run these processes'
|
|
@@ -390,6 +394,12 @@ function detectPackageManager(pkgJson, cwd) {
|
|
|
390
394
|
function isGlobPattern(name) {
|
|
391
395
|
return /[*?[]/.test(name);
|
|
392
396
|
}
|
|
397
|
+
function splitPatternArgs(raw) {
|
|
398
|
+
const i = raw.indexOf(" ");
|
|
399
|
+
if (i === -1)
|
|
400
|
+
return { glob: raw, extraArgs: "" };
|
|
401
|
+
return { glob: raw.slice(0, i), extraArgs: raw.slice(i) };
|
|
402
|
+
}
|
|
393
403
|
function expandScriptPatterns(config, cwd) {
|
|
394
404
|
const entries = Object.entries(config.processes);
|
|
395
405
|
const hasWildcard = entries.some(([name]) => name.startsWith("npm:") || isGlobPattern(name));
|
|
@@ -413,15 +423,16 @@ function expandScriptPatterns(config, cwd) {
|
|
|
413
423
|
expanded[name] = value;
|
|
414
424
|
continue;
|
|
415
425
|
}
|
|
416
|
-
const
|
|
426
|
+
const rawPattern = name.startsWith("npm:") ? name.slice(4) : name;
|
|
427
|
+
const { glob: globPattern, extraArgs } = splitPatternArgs(rawPattern);
|
|
417
428
|
const template = value ?? {};
|
|
418
429
|
if (template.command) {
|
|
419
430
|
throw new Error(`"${name}": wildcard processes cannot have a "command" field (commands come from package.json scripts)`);
|
|
420
431
|
}
|
|
421
|
-
const glob = new Bun.Glob(
|
|
432
|
+
const glob = new Bun.Glob(globPattern);
|
|
422
433
|
const matches = scriptNames.filter((s) => glob.match(s));
|
|
423
434
|
if (matches.length === 0) {
|
|
424
|
-
throw new Error(`"${name}": no scripts matched pattern "${
|
|
435
|
+
throw new Error(`"${name}": no scripts matched pattern "${globPattern}". Available scripts: ${scriptNames.join(", ")}`);
|
|
425
436
|
}
|
|
426
437
|
const colors = Array.isArray(template.color) ? template.color : undefined;
|
|
427
438
|
const singleColor = typeof template.color === "string" ? template.color : undefined;
|
|
@@ -434,7 +445,7 @@ function expandScriptPatterns(config, cwd) {
|
|
|
434
445
|
const { color: _color, ...rest } = template;
|
|
435
446
|
expanded[scriptName] = {
|
|
436
447
|
...rest,
|
|
437
|
-
command: `${pm} run ${scriptName}`,
|
|
448
|
+
command: `${pm} run ${scriptName}${extraArgs}`,
|
|
438
449
|
...color ? { color } : {}
|
|
439
450
|
};
|
|
440
451
|
}
|
|
@@ -847,8 +858,71 @@ function validateStopSignal(value) {
|
|
|
847
858
|
return;
|
|
848
859
|
}
|
|
849
860
|
|
|
861
|
+
// src/config/workspaces.ts
|
|
862
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
863
|
+
import { basename, resolve as resolve4 } from "path";
|
|
864
|
+
function resolveWorkspaceProcesses(script, cwd) {
|
|
865
|
+
const pkgPath = resolve4(cwd, "package.json");
|
|
866
|
+
if (!existsSync4(pkgPath)) {
|
|
867
|
+
throw new Error(`No package.json found in ${cwd}`);
|
|
868
|
+
}
|
|
869
|
+
const pkgJson = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
870
|
+
const pm = detectPackageManager(pkgJson, cwd);
|
|
871
|
+
const raw = pkgJson.workspaces;
|
|
872
|
+
let patterns;
|
|
873
|
+
if (Array.isArray(raw)) {
|
|
874
|
+
patterns = raw;
|
|
875
|
+
} else if (raw && typeof raw === "object" && Array.isArray(raw.packages)) {
|
|
876
|
+
patterns = raw.packages;
|
|
877
|
+
} else {
|
|
878
|
+
throw new Error('No "workspaces" field found in package.json');
|
|
879
|
+
}
|
|
880
|
+
const dirs = [];
|
|
881
|
+
for (const pattern of patterns) {
|
|
882
|
+
const glob = new Bun.Glob(pattern);
|
|
883
|
+
for (const match of glob.scanSync({ cwd, onlyFiles: false })) {
|
|
884
|
+
const abs = resolve4(cwd, match);
|
|
885
|
+
const wsPkgPath = resolve4(abs, "package.json");
|
|
886
|
+
if (existsSync4(wsPkgPath)) {
|
|
887
|
+
dirs.push(abs);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const processes = {};
|
|
892
|
+
const usedNames = new Set;
|
|
893
|
+
for (const dir of dirs) {
|
|
894
|
+
const wsPkgPath = resolve4(dir, "package.json");
|
|
895
|
+
const wsPkg = JSON.parse(readFileSync2(wsPkgPath, "utf-8"));
|
|
896
|
+
const scripts = wsPkg.scripts;
|
|
897
|
+
if (!scripts?.[script])
|
|
898
|
+
continue;
|
|
899
|
+
let name;
|
|
900
|
+
if (typeof wsPkg.name === "string" && wsPkg.name) {
|
|
901
|
+
name = wsPkg.name.replace(/^@[^/]+\//, "");
|
|
902
|
+
} else {
|
|
903
|
+
name = basename(dir);
|
|
904
|
+
}
|
|
905
|
+
if (usedNames.has(name)) {
|
|
906
|
+
let suffix = 1;
|
|
907
|
+
while (usedNames.has(`${name}-${suffix}`))
|
|
908
|
+
suffix++;
|
|
909
|
+
name = `${name}-${suffix}`;
|
|
910
|
+
}
|
|
911
|
+
usedNames.add(name);
|
|
912
|
+
processes[name] = {
|
|
913
|
+
command: `${pm} run ${script}`,
|
|
914
|
+
cwd: dir,
|
|
915
|
+
persistent: true
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (Object.keys(processes).length === 0) {
|
|
919
|
+
throw new Error(`No workspaces have a "${script}" script`);
|
|
920
|
+
}
|
|
921
|
+
return processes;
|
|
922
|
+
}
|
|
923
|
+
|
|
850
924
|
// src/process/manager.ts
|
|
851
|
-
import { resolve as
|
|
925
|
+
import { resolve as resolve7 } from "path";
|
|
852
926
|
|
|
853
927
|
// src/utils/watcher.ts
|
|
854
928
|
import { watch } from "fs";
|
|
@@ -896,11 +970,11 @@ class FileWatcher {
|
|
|
896
970
|
}
|
|
897
971
|
|
|
898
972
|
// src/process/runner.ts
|
|
899
|
-
import { resolve as
|
|
973
|
+
import { resolve as resolve6 } from "path";
|
|
900
974
|
|
|
901
975
|
// src/utils/env-file.ts
|
|
902
|
-
import { readFileSync as
|
|
903
|
-
import { resolve as
|
|
976
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
977
|
+
import { resolve as resolve5 } from "path";
|
|
904
978
|
function parseEnvFile(content) {
|
|
905
979
|
const vars = {};
|
|
906
980
|
for (const line of content.split(/\r?\n/)) {
|
|
@@ -928,10 +1002,10 @@ function loadEnvFiles(envFile, cwd) {
|
|
|
928
1002
|
const files = Array.isArray(envFile) ? envFile : [envFile];
|
|
929
1003
|
const merged = {};
|
|
930
1004
|
for (const file of files) {
|
|
931
|
-
const path =
|
|
1005
|
+
const path = resolve5(cwd, file);
|
|
932
1006
|
let content;
|
|
933
1007
|
try {
|
|
934
|
-
content =
|
|
1008
|
+
content = readFileSync3(path, "utf-8");
|
|
935
1009
|
} catch (err) {
|
|
936
1010
|
const code = err.code;
|
|
937
1011
|
if (code === "ENOENT") {
|
|
@@ -1046,7 +1120,7 @@ class ProcessRunner {
|
|
|
1046
1120
|
this.stopping = false;
|
|
1047
1121
|
log(`[${this.name}] Starting (gen ${gen}): ${this.config.command}`);
|
|
1048
1122
|
this.handler.onStatus("starting");
|
|
1049
|
-
const cwd = this.config.cwd ?
|
|
1123
|
+
const cwd = this.config.cwd ? resolve6(this.config.cwd) : process.cwd();
|
|
1050
1124
|
try {
|
|
1051
1125
|
const envFromFile = this.config.envFile ? loadEnvFiles(this.config.envFile, cwd) : {};
|
|
1052
1126
|
const noColor = "NO_COLOR" in process.env;
|
|
@@ -1308,12 +1382,12 @@ class ProcessManager {
|
|
|
1308
1382
|
this.updateStatus(name, "skipped");
|
|
1309
1383
|
continue;
|
|
1310
1384
|
}
|
|
1311
|
-
const { promise, resolve:
|
|
1385
|
+
const { promise, resolve: resolve8 } = Promise.withResolvers();
|
|
1312
1386
|
readyPromises.push(promise);
|
|
1313
|
-
this.pendingReadyResolvers.set(name,
|
|
1387
|
+
this.pendingReadyResolvers.set(name, resolve8);
|
|
1314
1388
|
this.createRunner(name, () => {
|
|
1315
1389
|
this.pendingReadyResolvers.delete(name);
|
|
1316
|
-
|
|
1390
|
+
resolve8();
|
|
1317
1391
|
});
|
|
1318
1392
|
this.startProcess(name, cols, rows);
|
|
1319
1393
|
}
|
|
@@ -1421,7 +1495,7 @@ class ProcessManager {
|
|
|
1421
1495
|
if (!this.fileWatcher)
|
|
1422
1496
|
this.fileWatcher = new FileWatcher;
|
|
1423
1497
|
const patterns = Array.isArray(proc.watch) ? proc.watch : [proc.watch];
|
|
1424
|
-
const cwd = proc.cwd ?
|
|
1498
|
+
const cwd = proc.cwd ? resolve7(proc.cwd) : process.cwd();
|
|
1425
1499
|
this.fileWatcher.watch(name, patterns, cwd, (changedFile) => {
|
|
1426
1500
|
const state = this.states.get(name);
|
|
1427
1501
|
if (!state)
|
|
@@ -1527,8 +1601,8 @@ class ProcessManager {
|
|
|
1527
1601
|
clearTimeout(timer);
|
|
1528
1602
|
}
|
|
1529
1603
|
this.restartTimers.clear();
|
|
1530
|
-
for (const
|
|
1531
|
-
|
|
1604
|
+
for (const resolve8 of this.pendingReadyResolvers.values()) {
|
|
1605
|
+
resolve8();
|
|
1532
1606
|
}
|
|
1533
1607
|
this.pendingReadyResolvers.clear();
|
|
1534
1608
|
const reversed = [...this.tiers].reverse();
|
|
@@ -1549,6 +1623,26 @@ function evaluateCondition(condition) {
|
|
|
1549
1623
|
// src/ui/app.ts
|
|
1550
1624
|
import { BoxRenderable, createCliRenderer } from "@opentui/core";
|
|
1551
1625
|
|
|
1626
|
+
// src/ui/keybindings.ts
|
|
1627
|
+
var SHORTCUTS = {
|
|
1628
|
+
restartAll: { key: "r", label: "Shift+R", description: "restart all", shift: true },
|
|
1629
|
+
copy: { key: "y", label: "Y", description: "copy" },
|
|
1630
|
+
search: { key: "f", label: "F", description: "search" },
|
|
1631
|
+
restart: { key: "r", label: "R", description: "restart" },
|
|
1632
|
+
stopStart: { key: "s", label: "S", description: "stop/start" },
|
|
1633
|
+
clear: { key: "l", label: "L", description: "clear" }
|
|
1634
|
+
};
|
|
1635
|
+
var STATUS_HINTS = [
|
|
1636
|
+
["\u2190\u2192/1-9", "tabs"],
|
|
1637
|
+
[SHORTCUTS.restart.label, SHORTCUTS.restart.description],
|
|
1638
|
+
[SHORTCUTS.stopStart.label, SHORTCUTS.stopStart.description],
|
|
1639
|
+
[SHORTCUTS.search.label, SHORTCUTS.search.description],
|
|
1640
|
+
[SHORTCUTS.copy.label, SHORTCUTS.copy.description],
|
|
1641
|
+
[SHORTCUTS.clear.label, SHORTCUTS.clear.description],
|
|
1642
|
+
["Ctrl+C", "quit"]
|
|
1643
|
+
];
|
|
1644
|
+
var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
|
|
1645
|
+
|
|
1552
1646
|
// src/ui/pane.ts
|
|
1553
1647
|
import { ScrollBoxRenderable } from "@opentui/core";
|
|
1554
1648
|
import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
|
|
@@ -1558,6 +1652,7 @@ class Pane {
|
|
|
1558
1652
|
terminal;
|
|
1559
1653
|
decoder = new TextDecoder;
|
|
1560
1654
|
_onScroll = null;
|
|
1655
|
+
_onCopy = null;
|
|
1561
1656
|
constructor(renderer, name, cols, rows, interactive = false) {
|
|
1562
1657
|
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
1563
1658
|
id: `pane-${name}`,
|
|
@@ -1576,6 +1671,18 @@ class Pane {
|
|
|
1576
1671
|
showCursor: interactive,
|
|
1577
1672
|
trimEnd: true
|
|
1578
1673
|
});
|
|
1674
|
+
const origOnSelectionChanged = this.terminal.onSelectionChanged.bind(this.terminal);
|
|
1675
|
+
this.terminal.onSelectionChanged = (selection) => {
|
|
1676
|
+
const result = origOnSelectionChanged(selection);
|
|
1677
|
+
if (selection?.isActive && !selection.isDragging) {
|
|
1678
|
+
const text = selection.getSelectedText();
|
|
1679
|
+
if (text) {
|
|
1680
|
+
renderer.copyToClipboardOSC52(text);
|
|
1681
|
+
this._onCopy?.(text);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return result;
|
|
1685
|
+
};
|
|
1579
1686
|
this.scrollBox.add(this.terminal);
|
|
1580
1687
|
}
|
|
1581
1688
|
feed(data) {
|
|
@@ -1604,6 +1711,9 @@ class Pane {
|
|
|
1604
1711
|
onScroll(handler) {
|
|
1605
1712
|
this._onScroll = handler;
|
|
1606
1713
|
}
|
|
1714
|
+
onCopy(handler) {
|
|
1715
|
+
this._onCopy = handler;
|
|
1716
|
+
}
|
|
1607
1717
|
show() {
|
|
1608
1718
|
this.scrollBox.visible = true;
|
|
1609
1719
|
}
|
|
@@ -1667,6 +1777,8 @@ class StatusBar {
|
|
|
1667
1777
|
_searchQuery = "";
|
|
1668
1778
|
_searchMatchCount = 0;
|
|
1669
1779
|
_searchCurrentIndex = -1;
|
|
1780
|
+
_tempMessage = null;
|
|
1781
|
+
_tempTimer = null;
|
|
1670
1782
|
constructor(renderer) {
|
|
1671
1783
|
this.renderable = new TextRenderable(renderer, {
|
|
1672
1784
|
id: "status-bar",
|
|
@@ -1684,13 +1796,25 @@ class StatusBar {
|
|
|
1684
1796
|
this._searchCurrentIndex = currentIndex;
|
|
1685
1797
|
this.renderable.content = this.buildContent();
|
|
1686
1798
|
}
|
|
1799
|
+
showTemporaryMessage(message, duration = 2000) {
|
|
1800
|
+
if (this._tempTimer)
|
|
1801
|
+
clearTimeout(this._tempTimer);
|
|
1802
|
+
this._tempMessage = message;
|
|
1803
|
+
this.renderable.content = this.buildContent();
|
|
1804
|
+
this._tempTimer = setTimeout(() => {
|
|
1805
|
+
this._tempMessage = null;
|
|
1806
|
+
this._tempTimer = null;
|
|
1807
|
+
this.renderable.content = this.buildContent();
|
|
1808
|
+
}, duration);
|
|
1809
|
+
}
|
|
1687
1810
|
buildContent() {
|
|
1811
|
+
if (this._tempMessage) {
|
|
1812
|
+
return new StyledText([cyan(this._tempMessage)]);
|
|
1813
|
+
}
|
|
1688
1814
|
if (this._searchMode) {
|
|
1689
1815
|
return this.buildSearchContent();
|
|
1690
1816
|
}
|
|
1691
|
-
return new StyledText([
|
|
1692
|
-
plain("\u2190\u2192/1-9: tabs R: restart S: stop/start F: search L: clear Ctrl+C: quit")
|
|
1693
|
-
]);
|
|
1817
|
+
return new StyledText([plain(STATUS_BAR_TEXT)]);
|
|
1694
1818
|
}
|
|
1695
1819
|
buildSearchContent() {
|
|
1696
1820
|
const chunks = [];
|
|
@@ -1997,6 +2121,7 @@ class App {
|
|
|
1997
2121
|
for (const name of this.names) {
|
|
1998
2122
|
const interactive = this.config.processes[name].interactive === true;
|
|
1999
2123
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
2124
|
+
pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
|
|
2000
2125
|
this.panes.set(name, pane);
|
|
2001
2126
|
paneContainer.add(pane.scrollBox);
|
|
2002
2127
|
}
|
|
@@ -2060,19 +2185,23 @@ class App {
|
|
|
2060
2185
|
const isInteractive = this.config.processes[this.activePane]?.interactive === true;
|
|
2061
2186
|
if (!isInteractive) {
|
|
2062
2187
|
const name = key.name.toLowerCase();
|
|
2063
|
-
if (key.shift && name ===
|
|
2188
|
+
if (key.shift && name === SHORTCUTS.restartAll.key) {
|
|
2064
2189
|
this.manager.restartAll(this.termCols, this.termRows);
|
|
2065
2190
|
return;
|
|
2066
2191
|
}
|
|
2067
|
-
if (name ===
|
|
2192
|
+
if (name === SHORTCUTS.copy.key) {
|
|
2193
|
+
this.copySelection();
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (name === SHORTCUTS.search.key) {
|
|
2068
2197
|
this.enterSearch();
|
|
2069
2198
|
return;
|
|
2070
2199
|
}
|
|
2071
|
-
if (name ===
|
|
2200
|
+
if (name === SHORTCUTS.restart.key) {
|
|
2072
2201
|
this.manager.restart(this.activePane, this.termCols, this.termRows);
|
|
2073
2202
|
return;
|
|
2074
2203
|
}
|
|
2075
|
-
if (name ===
|
|
2204
|
+
if (name === SHORTCUTS.stopStart.key) {
|
|
2076
2205
|
const state = this.manager.getState(this.activePane);
|
|
2077
2206
|
if (state?.status === "stopped" || state?.status === "finished" || state?.status === "failed") {
|
|
2078
2207
|
this.manager.start(this.activePane, this.termCols, this.termRows);
|
|
@@ -2081,7 +2210,7 @@ class App {
|
|
|
2081
2210
|
}
|
|
2082
2211
|
return;
|
|
2083
2212
|
}
|
|
2084
|
-
if (name ===
|
|
2213
|
+
if (name === SHORTCUTS.clear.key) {
|
|
2085
2214
|
this.panes.get(this.activePane)?.clear();
|
|
2086
2215
|
return;
|
|
2087
2216
|
}
|
|
@@ -2169,6 +2298,18 @@ class App {
|
|
|
2169
2298
|
this.tabBar.setInputWaiting(name, false);
|
|
2170
2299
|
}
|
|
2171
2300
|
}
|
|
2301
|
+
copySelection() {
|
|
2302
|
+
const selection = this.renderer.getSelection();
|
|
2303
|
+
if (!selection?.isActive)
|
|
2304
|
+
return false;
|
|
2305
|
+
const text = selection.getSelectedText();
|
|
2306
|
+
if (!text)
|
|
2307
|
+
return false;
|
|
2308
|
+
this.renderer.copyToClipboardOSC52(text);
|
|
2309
|
+
this.renderer.clearSelection();
|
|
2310
|
+
this.statusBar.showTemporaryMessage("Copied!");
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2172
2313
|
enterSearch() {
|
|
2173
2314
|
this.searchMode = true;
|
|
2174
2315
|
this.searchQuery = "";
|
|
@@ -2285,7 +2426,6 @@ class PrefixDisplay {
|
|
|
2285
2426
|
noColor;
|
|
2286
2427
|
decoders = new Map;
|
|
2287
2428
|
buffers = new Map;
|
|
2288
|
-
maxNameLen;
|
|
2289
2429
|
logWriter;
|
|
2290
2430
|
killOthers;
|
|
2291
2431
|
timestamps;
|
|
@@ -2297,7 +2437,6 @@ class PrefixDisplay {
|
|
|
2297
2437
|
this.timestamps = options.timestamps ?? false;
|
|
2298
2438
|
this.noColor = "NO_COLOR" in process.env;
|
|
2299
2439
|
const names = manager.getProcessNames();
|
|
2300
|
-
this.maxNameLen = Math.max(...names.map((n) => n.length));
|
|
2301
2440
|
this.colors = buildProcessColorMap(names, config);
|
|
2302
2441
|
for (const name of names) {
|
|
2303
2442
|
this.decoders.set(name, new TextDecoder("utf-8", { fatal: false }));
|
|
@@ -2371,15 +2510,14 @@ class PrefixDisplay {
|
|
|
2371
2510
|
return `${h}:${m}:${s}`;
|
|
2372
2511
|
}
|
|
2373
2512
|
printLine(name, line) {
|
|
2374
|
-
const padded = name.padEnd(this.maxNameLen);
|
|
2375
2513
|
const ts = this.timestamps ? `${DIM}[${this.formatTimestamp()}]${RESET} ` : "";
|
|
2376
2514
|
const tsPlain = this.timestamps ? `[${this.formatTimestamp()}] ` : "";
|
|
2377
2515
|
if (this.noColor) {
|
|
2378
|
-
process.stdout.write(`${tsPlain}[${
|
|
2516
|
+
process.stdout.write(`${tsPlain}[${name}] ${stripAnsi(line)}
|
|
2379
2517
|
`);
|
|
2380
2518
|
} else {
|
|
2381
2519
|
const color = this.colors.get(name) ?? "";
|
|
2382
|
-
process.stdout.write(`${ts}${color}[${
|
|
2520
|
+
process.stdout.write(`${ts}${color}[${name}]${RESET} ${line}
|
|
2383
2521
|
`);
|
|
2384
2522
|
}
|
|
2385
2523
|
}
|
|
@@ -2533,12 +2671,14 @@ Usage:
|
|
|
2533
2671
|
numux Run processes from config file
|
|
2534
2672
|
numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel
|
|
2535
2673
|
numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands
|
|
2674
|
+
numux -w <script> Run a script across all workspaces
|
|
2536
2675
|
numux init Create a starter config file
|
|
2537
2676
|
numux validate Validate config and show process graph
|
|
2538
2677
|
numux exec <name> [--] <cmd> Run a command in a process's environment
|
|
2539
2678
|
numux completions <shell> Generate shell completions (bash, zsh, fish)
|
|
2540
2679
|
|
|
2541
2680
|
Options:
|
|
2681
|
+
-w, --workspace <script> Run a package.json script across all workspaces
|
|
2542
2682
|
-n, --name <name=command> Add a named process
|
|
2543
2683
|
-c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
|
|
2544
2684
|
--colors Auto-assign colors to processes based on their name
|
|
@@ -2591,8 +2731,8 @@ async function main() {
|
|
|
2591
2731
|
process.exit(0);
|
|
2592
2732
|
}
|
|
2593
2733
|
if (parsed.init) {
|
|
2594
|
-
const target =
|
|
2595
|
-
if (
|
|
2734
|
+
const target = resolve8("numux.config.ts");
|
|
2735
|
+
if (existsSync5(target)) {
|
|
2596
2736
|
console.error(`Already exists: ${target}`);
|
|
2597
2737
|
process.exit(1);
|
|
2598
2738
|
}
|
|
@@ -2650,7 +2790,7 @@ async function main() {
|
|
|
2650
2790
|
const names = Object.keys(config2.processes);
|
|
2651
2791
|
throw new Error(`Unknown process "${parsed.execName}". Available: ${names.join(", ")}`);
|
|
2652
2792
|
}
|
|
2653
|
-
const cwd = proc.cwd ?
|
|
2793
|
+
const cwd = proc.cwd ? resolve8(proc.cwd) : process.cwd();
|
|
2654
2794
|
const envFromFile = proc.envFile ? loadEnvFiles(proc.envFile, cwd) : {};
|
|
2655
2795
|
const env = {
|
|
2656
2796
|
...process.env,
|
|
@@ -2671,7 +2811,7 @@ async function main() {
|
|
|
2671
2811
|
}
|
|
2672
2812
|
let config;
|
|
2673
2813
|
const warnings = [];
|
|
2674
|
-
if (parsed.commands.length > 0 || parsed.named.length > 0) {
|
|
2814
|
+
if (parsed.commands.length > 0 || parsed.named.length > 0 || parsed.workspace) {
|
|
2675
2815
|
const isScriptPattern = (c) => c.startsWith("npm:") || /[*?[]/.test(c);
|
|
2676
2816
|
const hasNpmPatterns = parsed.commands.some(isScriptPattern);
|
|
2677
2817
|
if (hasNpmPatterns) {
|
|
@@ -2702,6 +2842,21 @@ async function main() {
|
|
|
2702
2842
|
colors: parsed.colors
|
|
2703
2843
|
});
|
|
2704
2844
|
}
|
|
2845
|
+
if (parsed.workspace) {
|
|
2846
|
+
const wsProcesses = resolveWorkspaceProcesses(parsed.workspace, process.cwd());
|
|
2847
|
+
for (const [name, proc] of Object.entries(wsProcesses)) {
|
|
2848
|
+
let finalName = name;
|
|
2849
|
+
if (config.processes[finalName]) {
|
|
2850
|
+
let suffix = 1;
|
|
2851
|
+
while (config.processes[`${finalName}-${suffix}`])
|
|
2852
|
+
suffix++;
|
|
2853
|
+
finalName = `${finalName}-${suffix}`;
|
|
2854
|
+
}
|
|
2855
|
+
if (parsed.noRestart)
|
|
2856
|
+
proc.maxRestarts = 0;
|
|
2857
|
+
config.processes[finalName] = proc;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2705
2860
|
} else {
|
|
2706
2861
|
const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
|
|
2707
2862
|
config = validateConfig(raw, warnings);
|
|
@@ -2733,6 +2888,11 @@ async function main() {
|
|
|
2733
2888
|
}
|
|
2734
2889
|
printWarnings(warnings);
|
|
2735
2890
|
if (parsed.prefix) {
|
|
2891
|
+
if (!parsed.noRestart) {
|
|
2892
|
+
for (const proc of Object.values(config.processes)) {
|
|
2893
|
+
proc.maxRestarts ??= 0;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2736
2896
|
const display = new PrefixDisplay(manager, config, {
|
|
2737
2897
|
logWriter,
|
|
2738
2898
|
killOthers: parsed.killOthers,
|