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 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/user/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.
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.14.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 pattern = config.readyPattern ? new RegExp(config.readyPattern) : null;
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
- return pattern.test(outputBuffer);
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}): ${this.config.command}`);
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", this.config.command], {
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$ ${this.config.command}\x1B[0m\r
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
@@ -4,7 +4,7 @@ export interface NumuxProcessConfig {
4
4
  env?: Record<string, string>;
5
5
  envFile?: string | string[] | false;
6
6
  dependsOn?: string[];
7
- readyPattern?: string;
7
+ readyPattern?: string | RegExp;
8
8
  persistent?: boolean;
9
9
  maxRestarts?: number;
10
10
  readyTimeout?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.14.1",
3
+ "version": "1.15.1",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",