numux 1.14.1 → 1.15.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 +24 -1
- package/dist/numux.js +97 -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.).
|
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.0",
|
|
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,17 @@ function createReadinessChecker(config) {
|
|
|
1339
1372
|
if (outputBuffer.length > BUFFER_CAP2) {
|
|
1340
1373
|
outputBuffer = outputBuffer.slice(-BUFFER_CAP2);
|
|
1341
1374
|
}
|
|
1342
|
-
|
|
1375
|
+
if (!shouldCapture)
|
|
1376
|
+
return pattern.test(outputBuffer);
|
|
1377
|
+
const match = pattern.exec(outputBuffer);
|
|
1378
|
+
if (match) {
|
|
1379
|
+
_captures = extractCaptures(match);
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
return false;
|
|
1383
|
+
},
|
|
1384
|
+
get captures() {
|
|
1385
|
+
return _captures;
|
|
1343
1386
|
},
|
|
1344
1387
|
get isImmediatelyReady() {
|
|
1345
1388
|
return persistent && !pattern;
|
|
@@ -1366,6 +1409,7 @@ class ProcessRunner {
|
|
|
1366
1409
|
readyTimer = null;
|
|
1367
1410
|
restarting = false;
|
|
1368
1411
|
readyTimedOut = false;
|
|
1412
|
+
commandOverride;
|
|
1369
1413
|
constructor(name, config, handler) {
|
|
1370
1414
|
this.name = name;
|
|
1371
1415
|
this.config = config;
|
|
@@ -1379,10 +1423,13 @@ class ProcessRunner {
|
|
|
1379
1423
|
get signal() {
|
|
1380
1424
|
return this.config.stopSignal ?? "SIGTERM";
|
|
1381
1425
|
}
|
|
1382
|
-
start(cols, rows) {
|
|
1426
|
+
start(cols, rows, commandOverride) {
|
|
1427
|
+
if (commandOverride !== undefined)
|
|
1428
|
+
this.commandOverride = commandOverride;
|
|
1429
|
+
const command = this.commandOverride ?? this.config.command;
|
|
1383
1430
|
const gen = ++this.generation;
|
|
1384
1431
|
this.stopping = false;
|
|
1385
|
-
log(`[${this.name}] Starting (gen ${gen}): ${
|
|
1432
|
+
log(`[${this.name}] Starting (gen ${gen}): ${command}`);
|
|
1386
1433
|
this.handler.onStatus("starting");
|
|
1387
1434
|
const cwd = this.config.cwd ? resolve6(this.config.cwd) : process.cwd();
|
|
1388
1435
|
try {
|
|
@@ -1395,7 +1442,7 @@ class ProcessRunner {
|
|
|
1395
1442
|
...envFromFile,
|
|
1396
1443
|
...this.config.env
|
|
1397
1444
|
};
|
|
1398
|
-
this.proc = Bun.spawn(["sh", "-c",
|
|
1445
|
+
this.proc = Bun.spawn(["sh", "-c", command], {
|
|
1399
1446
|
cwd,
|
|
1400
1447
|
env,
|
|
1401
1448
|
terminal: {
|
|
@@ -1423,7 +1470,7 @@ class ProcessRunner {
|
|
|
1423
1470
|
}
|
|
1424
1471
|
if (this.config.showCommand !== false) {
|
|
1425
1472
|
const encoder = new TextEncoder;
|
|
1426
|
-
const msg = `\x1B[2m$ ${
|
|
1473
|
+
const msg = `\x1B[2m$ ${command}\x1B[0m\r
|
|
1427
1474
|
\r
|
|
1428
1475
|
`;
|
|
1429
1476
|
this.handler.onOutput(encoder.encode(msg));
|
|
@@ -1509,9 +1556,9 @@ class ProcessRunner {
|
|
|
1509
1556
|
this.clearReadyTimeout();
|
|
1510
1557
|
log(`[${this.name}] Ready`);
|
|
1511
1558
|
this.handler.onStatus("ready");
|
|
1512
|
-
this.handler.onReady();
|
|
1559
|
+
this.handler.onReady(this.readiness.captures);
|
|
1513
1560
|
}
|
|
1514
|
-
async restart(cols, rows) {
|
|
1561
|
+
async restart(cols, rows, commandOverride) {
|
|
1515
1562
|
if (this.restarting)
|
|
1516
1563
|
return;
|
|
1517
1564
|
this.restarting = true;
|
|
@@ -1536,7 +1583,7 @@ class ProcessRunner {
|
|
|
1536
1583
|
this.readyTimedOut = false;
|
|
1537
1584
|
this.readiness = createReadinessChecker(this.config);
|
|
1538
1585
|
this.errorChecker = createErrorChecker(this.config);
|
|
1539
|
-
this.start(cols, rows);
|
|
1586
|
+
this.start(cols, rows, commandOverride);
|
|
1540
1587
|
}
|
|
1541
1588
|
async stop(timeoutMs = 5000) {
|
|
1542
1589
|
if (!this.proc)
|
|
@@ -1598,6 +1645,7 @@ class ProcessManager {
|
|
|
1598
1645
|
restartTimers = new Map;
|
|
1599
1646
|
startTimes = new Map;
|
|
1600
1647
|
pendingReadyResolvers = new Map;
|
|
1648
|
+
readyCaptures = new Map;
|
|
1601
1649
|
fileWatcher;
|
|
1602
1650
|
constructor(config) {
|
|
1603
1651
|
this.config = config;
|
|
@@ -1669,6 +1717,7 @@ class ProcessManager {
|
|
|
1669
1717
|
this.setupWatchers();
|
|
1670
1718
|
}
|
|
1671
1719
|
startProcess(name, cols, rows) {
|
|
1720
|
+
const commandOverride = this.expandDependencyCaptures(name);
|
|
1672
1721
|
const delay = this.config.processes[name].delay;
|
|
1673
1722
|
if (delay) {
|
|
1674
1723
|
log(`[${name}] Delaying start by ${delay}ms`);
|
|
@@ -1677,12 +1726,12 @@ class ProcessManager {
|
|
|
1677
1726
|
if (this.stopping)
|
|
1678
1727
|
return;
|
|
1679
1728
|
this.startTimes.set(name, Date.now());
|
|
1680
|
-
this.runners.get(name).start(cols, rows);
|
|
1729
|
+
this.runners.get(name).start(cols, rows, commandOverride);
|
|
1681
1730
|
}, delay);
|
|
1682
1731
|
this.restartTimers.set(name, timer);
|
|
1683
1732
|
} else {
|
|
1684
1733
|
this.startTimes.set(name, Date.now());
|
|
1685
|
-
this.runners.get(name).start(cols, rows);
|
|
1734
|
+
this.runners.get(name).start(cols, rows, commandOverride);
|
|
1686
1735
|
}
|
|
1687
1736
|
}
|
|
1688
1737
|
createRunner(name, onInitialReady) {
|
|
@@ -1700,7 +1749,10 @@ class ProcessManager {
|
|
|
1700
1749
|
}
|
|
1701
1750
|
this.scheduleAutoRestart(name, code);
|
|
1702
1751
|
},
|
|
1703
|
-
onReady: () => {
|
|
1752
|
+
onReady: (captures) => {
|
|
1753
|
+
if (captures) {
|
|
1754
|
+
this.readyCaptures.set(name, captures);
|
|
1755
|
+
}
|
|
1704
1756
|
if (!readyResolved) {
|
|
1705
1757
|
readyResolved = true;
|
|
1706
1758
|
onInitialReady();
|
|
@@ -1784,6 +1836,32 @@ class ProcessManager {
|
|
|
1784
1836
|
});
|
|
1785
1837
|
}
|
|
1786
1838
|
}
|
|
1839
|
+
expandDependencyCaptures(name) {
|
|
1840
|
+
const proc = this.config.processes[name];
|
|
1841
|
+
const deps = proc.dependsOn;
|
|
1842
|
+
if (!deps?.length)
|
|
1843
|
+
return;
|
|
1844
|
+
const allCaptures = new Map;
|
|
1845
|
+
for (const dep of deps) {
|
|
1846
|
+
const captures = this.readyCaptures.get(dep);
|
|
1847
|
+
if (captures)
|
|
1848
|
+
allCaptures.set(dep, captures);
|
|
1849
|
+
}
|
|
1850
|
+
if (allCaptures.size === 0)
|
|
1851
|
+
return;
|
|
1852
|
+
const depNames = [...allCaptures.keys()].map((n) => escapeRegExp(n)).join("|");
|
|
1853
|
+
const refPattern = new RegExp(`\\$(${depNames})\\.(\\w+)`, "g");
|
|
1854
|
+
let hadReplacement = false;
|
|
1855
|
+
const expanded = proc.command.replace(refPattern, (match, dep, key) => {
|
|
1856
|
+
const captures = allCaptures.get(dep);
|
|
1857
|
+
if (captures && key in captures) {
|
|
1858
|
+
hadReplacement = true;
|
|
1859
|
+
return captures[key];
|
|
1860
|
+
}
|
|
1861
|
+
return match;
|
|
1862
|
+
});
|
|
1863
|
+
return hadReplacement ? expanded : undefined;
|
|
1864
|
+
}
|
|
1787
1865
|
updateStatus(name, status) {
|
|
1788
1866
|
const state = this.states.get(name);
|
|
1789
1867
|
state.status = status;
|
|
@@ -1810,7 +1888,7 @@ class ProcessManager {
|
|
|
1810
1888
|
state.exitCode = null;
|
|
1811
1889
|
state.restartCount++;
|
|
1812
1890
|
this.startTimes.set(name, Date.now());
|
|
1813
|
-
runner.restart(cols, rows);
|
|
1891
|
+
runner.restart(cols, rows, this.expandDependencyCaptures(name));
|
|
1814
1892
|
}
|
|
1815
1893
|
async stop(name) {
|
|
1816
1894
|
const state = this.states.get(name);
|
|
@@ -1848,7 +1926,7 @@ class ProcessManager {
|
|
|
1848
1926
|
state.exitCode = null;
|
|
1849
1927
|
state.restartCount++;
|
|
1850
1928
|
this.startTimes.set(name, Date.now());
|
|
1851
|
-
this.runners.get(name)?.restart(cols, rows);
|
|
1929
|
+
this.runners.get(name)?.restart(cols, rows, this.expandDependencyCaptures(name));
|
|
1852
1930
|
}
|
|
1853
1931
|
restartAll(cols, rows) {
|
|
1854
1932
|
log("Restarting all processes");
|
|
@@ -1887,6 +1965,9 @@ class ProcessManager {
|
|
|
1887
1965
|
}
|
|
1888
1966
|
}
|
|
1889
1967
|
}
|
|
1968
|
+
function escapeRegExp(str) {
|
|
1969
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1970
|
+
}
|
|
1890
1971
|
var FALSY_VALUES = new Set(["", "0", "false", "no", "off"]);
|
|
1891
1972
|
function evaluateCondition(condition) {
|
|
1892
1973
|
const negated = condition.startsWith("!");
|
package/dist/types.d.ts
CHANGED