numux 1.14.1 → 1.15.1
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 +25 -2
- package/dist/numux.js +98 -16
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -216,7 +216,7 @@ Each process accepts:
|
|
|
216
216
|
| `env` | `Record<string, string>` | — | Extra environment variables |
|
|
217
217
|
| `envFile` | `string \| string[]` | — | `.env` file path(s) to load (relative to `cwd`) |
|
|
218
218
|
| `dependsOn` | `string[]` | — | Processes that must be ready first |
|
|
219
|
-
| `readyPattern` | `string` | — | Regex matched against stdout to signal readiness |
|
|
219
|
+
| `readyPattern` | `string \| RegExp` | — | Regex matched against stdout to signal readiness. Use `RegExp` to capture groups (see below) |
|
|
220
220
|
| `readyTimeout` | `number` | — | Milliseconds to wait for `readyPattern` before failing |
|
|
221
221
|
| `persistent` | `boolean` | `true` | `false` for one-shot commands (exit 0 = ready) |
|
|
222
222
|
| `maxRestarts` | `number` | `Infinity` | Max auto-restart attempts before giving up |
|
|
@@ -309,6 +309,29 @@ A process becomes **ready** when:
|
|
|
309
309
|
|
|
310
310
|
Persistent processes that crash are auto-restarted with exponential backoff (1s–30s). Backoff resets after 10s of uptime.
|
|
311
311
|
|
|
312
|
+
### Dependency output capture
|
|
313
|
+
|
|
314
|
+
When `readyPattern` is a `RegExp` (not a string), capture groups are extracted on match and expanded into dependent process commands using `$process.group` syntax:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
export default defineConfig({
|
|
318
|
+
processes: {
|
|
319
|
+
db: {
|
|
320
|
+
command: 'docker compose up postgres',
|
|
321
|
+
readyPattern: /ready to accept connections on port (?<port>\d+)/,
|
|
322
|
+
},
|
|
323
|
+
api: {
|
|
324
|
+
command: 'node server.js --db-port $db.port',
|
|
325
|
+
dependsOn: ['db'],
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Both named (`$db.port`) and positional (`$db.1`) references work. Named groups also populate positional slots, so `$db.port` and `$db.1` both resolve to the same value above.
|
|
332
|
+
|
|
333
|
+
Unmatched references are left as-is (the shell will expand `$db` as empty + `.port` literal, making the issue visible). String `readyPattern` values work as before — readiness detection only, no capture extraction.
|
|
334
|
+
|
|
312
335
|
## Keybindings
|
|
313
336
|
|
|
314
337
|
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.).
|
|
@@ -330,7 +353,7 @@ Keybindings are shown in the status bar at the bottom of the app. Panes are read
|
|
|
330
353
|
|
|
331
354
|
### ghostty-opentui
|
|
332
355
|
|
|
333
|
-
Despite the name, [`ghostty-opentui`](https://github.com/
|
|
356
|
+
Despite the name, [`ghostty-opentui`](https://github.com/remorses/ghostty-opentui) is **not** a compatibility layer for the [Ghostty](https://ghostty.org) terminal. It uses Ghostty's Zig-based VT parser as the ANSI terminal emulation engine for OpenTUI's terminal renderable. It works in any terminal emulator (iTerm, Kitty, Alacritty, WezTerm, etc.) and adds ~8MB to install size due to native binaries.
|
|
334
357
|
|
|
335
358
|
## License
|
|
336
359
|
|
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.15.1",
|
|
40
40
|
description: "Terminal multiplexer with dependency orchestration",
|
|
41
41
|
type: "module",
|
|
42
42
|
license: "MIT",
|
|
@@ -755,6 +755,9 @@ function interpolateValue(value) {
|
|
|
755
755
|
if (Array.isArray(value)) {
|
|
756
756
|
return value.map(interpolateValue);
|
|
757
757
|
}
|
|
758
|
+
if (value instanceof RegExp) {
|
|
759
|
+
return value;
|
|
760
|
+
}
|
|
758
761
|
if (value && typeof value === "object") {
|
|
759
762
|
const result = {};
|
|
760
763
|
for (const [k, v] of Object.entries(value)) {
|
|
@@ -1046,7 +1049,16 @@ function validateConfig(raw, warnings) {
|
|
|
1046
1049
|
}
|
|
1047
1050
|
}
|
|
1048
1051
|
const persistent = typeof p.persistent === "boolean" ? p.persistent : true;
|
|
1049
|
-
const readyPattern = typeof p.readyPattern === "string" ? p.readyPattern : undefined;
|
|
1052
|
+
const readyPattern = p.readyPattern instanceof RegExp ? p.readyPattern : typeof p.readyPattern === "string" ? p.readyPattern : undefined;
|
|
1053
|
+
if (typeof readyPattern === "string") {
|
|
1054
|
+
try {
|
|
1055
|
+
new RegExp(readyPattern);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
throw new Error(`Process "${name}".readyPattern is not a valid regex: "${readyPattern}"`, {
|
|
1058
|
+
cause: err
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1050
1062
|
if (readyPattern && !persistent) {
|
|
1051
1063
|
warnings?.push({
|
|
1052
1064
|
process: name,
|
|
@@ -1327,10 +1339,31 @@ function createErrorChecker(config) {
|
|
|
1327
1339
|
|
|
1328
1340
|
// src/process/ready.ts
|
|
1329
1341
|
var BUFFER_CAP2 = 65536;
|
|
1342
|
+
function extractCaptures(match) {
|
|
1343
|
+
const result = {};
|
|
1344
|
+
let hasCaptures = false;
|
|
1345
|
+
if (match.groups) {
|
|
1346
|
+
for (const [key, value] of Object.entries(match.groups)) {
|
|
1347
|
+
if (value !== undefined) {
|
|
1348
|
+
result[key] = value;
|
|
1349
|
+
hasCaptures = true;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
for (let i = 1;i < match.length; i++) {
|
|
1354
|
+
if (match[i] !== undefined) {
|
|
1355
|
+
result[String(i)] = match[i];
|
|
1356
|
+
hasCaptures = true;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return hasCaptures ? result : null;
|
|
1360
|
+
}
|
|
1330
1361
|
function createReadinessChecker(config) {
|
|
1331
|
-
const
|
|
1362
|
+
const shouldCapture = config.readyPattern instanceof RegExp;
|
|
1363
|
+
const pattern = config.readyPattern ? config.readyPattern instanceof RegExp ? config.readyPattern : new RegExp(config.readyPattern) : null;
|
|
1332
1364
|
const persistent = config.persistent !== false;
|
|
1333
1365
|
let outputBuffer = "";
|
|
1366
|
+
let _captures = null;
|
|
1334
1367
|
return {
|
|
1335
1368
|
feedOutput(data) {
|
|
1336
1369
|
if (!(persistent && pattern))
|
|
@@ -1339,7 +1372,18 @@ function createReadinessChecker(config) {
|
|
|
1339
1372
|
if (outputBuffer.length > BUFFER_CAP2) {
|
|
1340
1373
|
outputBuffer = outputBuffer.slice(-BUFFER_CAP2);
|
|
1341
1374
|
}
|
|
1342
|
-
|
|
1375
|
+
const clean = stripAnsi(outputBuffer);
|
|
1376
|
+
if (!shouldCapture)
|
|
1377
|
+
return pattern.test(clean);
|
|
1378
|
+
const match = pattern.exec(clean);
|
|
1379
|
+
if (match) {
|
|
1380
|
+
_captures = extractCaptures(match);
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
return false;
|
|
1384
|
+
},
|
|
1385
|
+
get captures() {
|
|
1386
|
+
return _captures;
|
|
1343
1387
|
},
|
|
1344
1388
|
get isImmediatelyReady() {
|
|
1345
1389
|
return persistent && !pattern;
|
|
@@ -1366,6 +1410,7 @@ class ProcessRunner {
|
|
|
1366
1410
|
readyTimer = null;
|
|
1367
1411
|
restarting = false;
|
|
1368
1412
|
readyTimedOut = false;
|
|
1413
|
+
commandOverride;
|
|
1369
1414
|
constructor(name, config, handler) {
|
|
1370
1415
|
this.name = name;
|
|
1371
1416
|
this.config = config;
|
|
@@ -1379,10 +1424,13 @@ class ProcessRunner {
|
|
|
1379
1424
|
get signal() {
|
|
1380
1425
|
return this.config.stopSignal ?? "SIGTERM";
|
|
1381
1426
|
}
|
|
1382
|
-
start(cols, rows) {
|
|
1427
|
+
start(cols, rows, commandOverride) {
|
|
1428
|
+
if (commandOverride !== undefined)
|
|
1429
|
+
this.commandOverride = commandOverride;
|
|
1430
|
+
const command = this.commandOverride ?? this.config.command;
|
|
1383
1431
|
const gen = ++this.generation;
|
|
1384
1432
|
this.stopping = false;
|
|
1385
|
-
log(`[${this.name}] Starting (gen ${gen}): ${
|
|
1433
|
+
log(`[${this.name}] Starting (gen ${gen}): ${command}`);
|
|
1386
1434
|
this.handler.onStatus("starting");
|
|
1387
1435
|
const cwd = this.config.cwd ? resolve6(this.config.cwd) : process.cwd();
|
|
1388
1436
|
try {
|
|
@@ -1395,7 +1443,7 @@ class ProcessRunner {
|
|
|
1395
1443
|
...envFromFile,
|
|
1396
1444
|
...this.config.env
|
|
1397
1445
|
};
|
|
1398
|
-
this.proc = Bun.spawn(["sh", "-c",
|
|
1446
|
+
this.proc = Bun.spawn(["sh", "-c", command], {
|
|
1399
1447
|
cwd,
|
|
1400
1448
|
env,
|
|
1401
1449
|
terminal: {
|
|
@@ -1423,7 +1471,7 @@ class ProcessRunner {
|
|
|
1423
1471
|
}
|
|
1424
1472
|
if (this.config.showCommand !== false) {
|
|
1425
1473
|
const encoder = new TextEncoder;
|
|
1426
|
-
const msg = `\x1B[2m$ ${
|
|
1474
|
+
const msg = `\x1B[2m$ ${command}\x1B[0m\r
|
|
1427
1475
|
\r
|
|
1428
1476
|
`;
|
|
1429
1477
|
this.handler.onOutput(encoder.encode(msg));
|
|
@@ -1509,9 +1557,9 @@ class ProcessRunner {
|
|
|
1509
1557
|
this.clearReadyTimeout();
|
|
1510
1558
|
log(`[${this.name}] Ready`);
|
|
1511
1559
|
this.handler.onStatus("ready");
|
|
1512
|
-
this.handler.onReady();
|
|
1560
|
+
this.handler.onReady(this.readiness.captures);
|
|
1513
1561
|
}
|
|
1514
|
-
async restart(cols, rows) {
|
|
1562
|
+
async restart(cols, rows, commandOverride) {
|
|
1515
1563
|
if (this.restarting)
|
|
1516
1564
|
return;
|
|
1517
1565
|
this.restarting = true;
|
|
@@ -1536,7 +1584,7 @@ class ProcessRunner {
|
|
|
1536
1584
|
this.readyTimedOut = false;
|
|
1537
1585
|
this.readiness = createReadinessChecker(this.config);
|
|
1538
1586
|
this.errorChecker = createErrorChecker(this.config);
|
|
1539
|
-
this.start(cols, rows);
|
|
1587
|
+
this.start(cols, rows, commandOverride);
|
|
1540
1588
|
}
|
|
1541
1589
|
async stop(timeoutMs = 5000) {
|
|
1542
1590
|
if (!this.proc)
|
|
@@ -1598,6 +1646,7 @@ class ProcessManager {
|
|
|
1598
1646
|
restartTimers = new Map;
|
|
1599
1647
|
startTimes = new Map;
|
|
1600
1648
|
pendingReadyResolvers = new Map;
|
|
1649
|
+
readyCaptures = new Map;
|
|
1601
1650
|
fileWatcher;
|
|
1602
1651
|
constructor(config) {
|
|
1603
1652
|
this.config = config;
|
|
@@ -1669,6 +1718,7 @@ class ProcessManager {
|
|
|
1669
1718
|
this.setupWatchers();
|
|
1670
1719
|
}
|
|
1671
1720
|
startProcess(name, cols, rows) {
|
|
1721
|
+
const commandOverride = this.expandDependencyCaptures(name);
|
|
1672
1722
|
const delay = this.config.processes[name].delay;
|
|
1673
1723
|
if (delay) {
|
|
1674
1724
|
log(`[${name}] Delaying start by ${delay}ms`);
|
|
@@ -1677,12 +1727,12 @@ class ProcessManager {
|
|
|
1677
1727
|
if (this.stopping)
|
|
1678
1728
|
return;
|
|
1679
1729
|
this.startTimes.set(name, Date.now());
|
|
1680
|
-
this.runners.get(name).start(cols, rows);
|
|
1730
|
+
this.runners.get(name).start(cols, rows, commandOverride);
|
|
1681
1731
|
}, delay);
|
|
1682
1732
|
this.restartTimers.set(name, timer);
|
|
1683
1733
|
} else {
|
|
1684
1734
|
this.startTimes.set(name, Date.now());
|
|
1685
|
-
this.runners.get(name).start(cols, rows);
|
|
1735
|
+
this.runners.get(name).start(cols, rows, commandOverride);
|
|
1686
1736
|
}
|
|
1687
1737
|
}
|
|
1688
1738
|
createRunner(name, onInitialReady) {
|
|
@@ -1700,7 +1750,10 @@ class ProcessManager {
|
|
|
1700
1750
|
}
|
|
1701
1751
|
this.scheduleAutoRestart(name, code);
|
|
1702
1752
|
},
|
|
1703
|
-
onReady: () => {
|
|
1753
|
+
onReady: (captures) => {
|
|
1754
|
+
if (captures) {
|
|
1755
|
+
this.readyCaptures.set(name, captures);
|
|
1756
|
+
}
|
|
1704
1757
|
if (!readyResolved) {
|
|
1705
1758
|
readyResolved = true;
|
|
1706
1759
|
onInitialReady();
|
|
@@ -1784,6 +1837,32 @@ class ProcessManager {
|
|
|
1784
1837
|
});
|
|
1785
1838
|
}
|
|
1786
1839
|
}
|
|
1840
|
+
expandDependencyCaptures(name) {
|
|
1841
|
+
const proc = this.config.processes[name];
|
|
1842
|
+
const deps = proc.dependsOn;
|
|
1843
|
+
if (!deps?.length)
|
|
1844
|
+
return;
|
|
1845
|
+
const allCaptures = new Map;
|
|
1846
|
+
for (const dep of deps) {
|
|
1847
|
+
const captures = this.readyCaptures.get(dep);
|
|
1848
|
+
if (captures)
|
|
1849
|
+
allCaptures.set(dep, captures);
|
|
1850
|
+
}
|
|
1851
|
+
if (allCaptures.size === 0)
|
|
1852
|
+
return;
|
|
1853
|
+
const depNames = [...allCaptures.keys()].map((n) => escapeRegExp(n)).join("|");
|
|
1854
|
+
const refPattern = new RegExp(`\\$(${depNames})\\.(\\w+)`, "g");
|
|
1855
|
+
let hadReplacement = false;
|
|
1856
|
+
const expanded = proc.command.replace(refPattern, (match, dep, key) => {
|
|
1857
|
+
const captures = allCaptures.get(dep);
|
|
1858
|
+
if (captures && key in captures) {
|
|
1859
|
+
hadReplacement = true;
|
|
1860
|
+
return captures[key];
|
|
1861
|
+
}
|
|
1862
|
+
return match;
|
|
1863
|
+
});
|
|
1864
|
+
return hadReplacement ? expanded : undefined;
|
|
1865
|
+
}
|
|
1787
1866
|
updateStatus(name, status) {
|
|
1788
1867
|
const state = this.states.get(name);
|
|
1789
1868
|
state.status = status;
|
|
@@ -1810,7 +1889,7 @@ class ProcessManager {
|
|
|
1810
1889
|
state.exitCode = null;
|
|
1811
1890
|
state.restartCount++;
|
|
1812
1891
|
this.startTimes.set(name, Date.now());
|
|
1813
|
-
runner.restart(cols, rows);
|
|
1892
|
+
runner.restart(cols, rows, this.expandDependencyCaptures(name));
|
|
1814
1893
|
}
|
|
1815
1894
|
async stop(name) {
|
|
1816
1895
|
const state = this.states.get(name);
|
|
@@ -1848,7 +1927,7 @@ class ProcessManager {
|
|
|
1848
1927
|
state.exitCode = null;
|
|
1849
1928
|
state.restartCount++;
|
|
1850
1929
|
this.startTimes.set(name, Date.now());
|
|
1851
|
-
this.runners.get(name)?.restart(cols, rows);
|
|
1930
|
+
this.runners.get(name)?.restart(cols, rows, this.expandDependencyCaptures(name));
|
|
1852
1931
|
}
|
|
1853
1932
|
restartAll(cols, rows) {
|
|
1854
1933
|
log("Restarting all processes");
|
|
@@ -1887,6 +1966,9 @@ class ProcessManager {
|
|
|
1887
1966
|
}
|
|
1888
1967
|
}
|
|
1889
1968
|
}
|
|
1969
|
+
function escapeRegExp(str) {
|
|
1970
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1971
|
+
}
|
|
1890
1972
|
var FALSY_VALUES = new Set(["", "0", "false", "no", "off"]);
|
|
1891
1973
|
function evaluateCondition(condition) {
|
|
1892
1974
|
const negated = condition.startsWith("!");
|
package/dist/types.d.ts
CHANGED