numux 1.14.0 → 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 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.14.0",
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 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,17 @@ 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
+ 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}): ${this.config.command}`);
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", this.config.command], {
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$ ${this.config.command}\x1B[0m\r
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("!");
@@ -1902,7 +1983,7 @@ import { BoxRenderable, createCliRenderer } from "@opentui/core";
1902
1983
  // src/ui/keybindings.ts
1903
1984
  var SHORTCUTS = {
1904
1985
  restartAll: { key: "r", label: "Shift+R", description: "restart all", shift: true },
1905
- copy: { key: "y", label: "Y/\u2318C", description: "copy" },
1986
+ copy: { key: "y", label: "Y", description: "copy all" },
1906
1987
  search: { key: "f", label: "F", description: "search" },
1907
1988
  restart: { key: "r", label: "R", description: "restart" },
1908
1989
  stopStart: { key: "s", label: "S", description: "stop/start" },
@@ -1957,9 +2038,11 @@ class Pane {
1957
2038
  this.terminal.onSelectionChanged = (selection) => {
1958
2039
  const result = origOnSelectionChanged(selection);
1959
2040
  if (selection?.isActive && !selection.isDragging) {
1960
- const text = selection.getSelectedText();
2041
+ const text = this.terminal.getSelectedText();
1961
2042
  if (text) {
1962
2043
  this._onCopy?.(text);
2044
+ } else {
2045
+ queueMicrotask(() => renderer.clearSelection());
1963
2046
  }
1964
2047
  }
1965
2048
  return result;
@@ -1996,6 +2079,9 @@ class Pane {
1996
2079
  onScroll(handler) {
1997
2080
  this._onScroll = handler;
1998
2081
  }
2082
+ getText() {
2083
+ return this.terminal.getText();
2084
+ }
1999
2085
  onCopy(handler) {
2000
2086
  this._onCopy = handler;
2001
2087
  }
@@ -2497,10 +2583,6 @@ class App {
2497
2583
  });
2498
2584
  this.renderer.keyInput.on("keypress", (key) => {
2499
2585
  log(key);
2500
- if (key.super && key.name === "c") {
2501
- this.copySelection();
2502
- return;
2503
- }
2504
2586
  if (key.ctrl && key.name === "c") {
2505
2587
  if (this.searchMode) {
2506
2588
  this.exitSearch();
@@ -2533,7 +2615,7 @@ class App {
2533
2615
  return;
2534
2616
  }
2535
2617
  if (name === SHORTCUTS.copy.key) {
2536
- this.copySelection();
2618
+ this.copyAllText();
2537
2619
  return;
2538
2620
  }
2539
2621
  if (name === SHORTCUTS.search.key) {
@@ -2646,22 +2728,27 @@ class App {
2646
2728
  const cmd = process.platform === "darwin" ? "pbcopy" : process.platform === "linux" ? "xclip -selection clipboard" : null;
2647
2729
  if (cmd) {
2648
2730
  const [bin, ...args] = cmd.split(" ");
2649
- const proc = Bun.spawn([bin, ...args], { stdin: "pipe" });
2650
- proc.stdin.write(text);
2651
- proc.stdin.end();
2731
+ try {
2732
+ const proc = Bun.spawn([bin, ...args], { stdin: "pipe" });
2733
+ proc.stdin.write(text);
2734
+ proc.stdin.end();
2735
+ proc.exited.catch(() => {});
2736
+ } catch {}
2652
2737
  }
2653
2738
  }
2654
- copySelection() {
2655
- const selection = this.renderer.getSelection();
2656
- if (!selection?.isActive)
2657
- return false;
2658
- const text = selection.getSelectedText();
2659
- if (!text)
2660
- return false;
2739
+ copyAllText() {
2740
+ if (!this.activePane)
2741
+ return;
2742
+ const pane = this.panes.get(this.activePane);
2743
+ if (!pane)
2744
+ return;
2745
+ const text = pane.getText();
2746
+ if (!text) {
2747
+ this.statusBar.showTemporaryMessage("No output to copy");
2748
+ return;
2749
+ }
2661
2750
  this.copyToClipboard(text);
2662
- this.renderer.clearSelection();
2663
- this.statusBar.showTemporaryMessage("Copied!");
2664
- return true;
2751
+ this.statusBar.showTemporaryMessage("Copied all output!");
2665
2752
  }
2666
2753
  enterSearch() {
2667
2754
  this.searchMode = true;
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.0",
3
+ "version": "1.15.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",