numux 1.16.1 → 1.18.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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/numux.js +112 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -311,7 +311,7 @@ Persistent processes that crash are auto-restarted with exponential backoff (1s
311
311
 
312
312
  ### Dependency output capture
313
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:
314
+ When `readyPattern` is a `RegExp` (not a string), capture groups are extracted on match and expanded into dependent process `command` and `env` values using `$process.group` syntax:
315
315
 
316
316
  ```ts
317
317
  export default defineConfig({
@@ -323,6 +323,7 @@ export default defineConfig({
323
323
  api: {
324
324
  command: 'node server.js --db-port $db.port',
325
325
  dependsOn: ['db'],
326
+ env: { DB_PORT: '$db.port' },
326
327
  },
327
328
  },
328
329
  })
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.16.1",
39
+ version: "1.18.0",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -1419,6 +1419,7 @@ class ProcessRunner {
1419
1419
  restarting = false;
1420
1420
  readyTimedOut = false;
1421
1421
  commandOverride;
1422
+ envOverride;
1422
1423
  constructor(name, config, handler) {
1423
1424
  this.name = name;
1424
1425
  this.config = config;
@@ -1432,9 +1433,11 @@ class ProcessRunner {
1432
1433
  get signal() {
1433
1434
  return this.config.stopSignal ?? "SIGTERM";
1434
1435
  }
1435
- start(cols, rows, commandOverride) {
1436
+ start(cols, rows, commandOverride, envOverride) {
1436
1437
  if (commandOverride !== undefined)
1437
1438
  this.commandOverride = commandOverride;
1439
+ if (envOverride !== undefined)
1440
+ this.envOverride = envOverride;
1438
1441
  const command = this.commandOverride ?? this.config.command;
1439
1442
  const gen = ++this.generation;
1440
1443
  this.stopping = false;
@@ -1449,7 +1452,7 @@ class ProcessRunner {
1449
1452
  ...noColor ? {} : { FORCE_COLOR: "1" },
1450
1453
  TERM: "xterm-256color",
1451
1454
  ...envFromFile,
1452
- ...this.config.env
1455
+ ...this.envOverride ?? this.config.env
1453
1456
  };
1454
1457
  this.proc = Bun.spawn(["sh", "-c", command], {
1455
1458
  cwd,
@@ -1567,7 +1570,7 @@ class ProcessRunner {
1567
1570
  this.handler.onStatus("ready");
1568
1571
  this.handler.onReady(this.readiness.captures);
1569
1572
  }
1570
- async restart(cols, rows, commandOverride) {
1573
+ async restart(cols, rows, commandOverride, envOverride) {
1571
1574
  if (this.restarting)
1572
1575
  return;
1573
1576
  this.restarting = true;
@@ -1592,7 +1595,7 @@ class ProcessRunner {
1592
1595
  this.readyTimedOut = false;
1593
1596
  this.readiness = createReadinessChecker(this.config);
1594
1597
  this.errorChecker = createErrorChecker(this.config);
1595
- this.start(cols, rows, commandOverride);
1598
+ this.start(cols, rows, commandOverride, envOverride);
1596
1599
  }
1597
1600
  async stop(timeoutMs = 5000) {
1598
1601
  if (!this.proc)
@@ -1737,7 +1740,7 @@ class ProcessManager {
1737
1740
  this.setupWatchers();
1738
1741
  }
1739
1742
  startProcess(name, cols, rows) {
1740
- const commandOverride = this.expandDependencyCaptures(name);
1743
+ const { command, env } = this.expandDependencyCaptures(name);
1741
1744
  const delay = this.config.processes[name].delay;
1742
1745
  if (delay) {
1743
1746
  log(`[${name}] Delaying start by ${delay}ms`);
@@ -1746,12 +1749,12 @@ class ProcessManager {
1746
1749
  if (this.stopping)
1747
1750
  return;
1748
1751
  this.startTimes.set(name, Date.now());
1749
- this.runners.get(name).start(cols, rows, commandOverride);
1752
+ this.runners.get(name).start(cols, rows, command, env);
1750
1753
  }, delay);
1751
1754
  this.restartTimers.set(name, timer);
1752
1755
  } else {
1753
1756
  this.startTimes.set(name, Date.now());
1754
- this.runners.get(name).start(cols, rows, commandOverride);
1757
+ this.runners.get(name).start(cols, rows, command, env);
1755
1758
  }
1756
1759
  }
1757
1760
  createRunner(name, onInitialReady) {
@@ -1860,7 +1863,7 @@ class ProcessManager {
1860
1863
  const proc = this.config.processes[name];
1861
1864
  const deps = proc.dependsOn;
1862
1865
  if (!deps?.length)
1863
- return;
1866
+ return {};
1864
1867
  const allCaptures = new Map;
1865
1868
  for (const dep of deps) {
1866
1869
  const captures = this.readyCaptures.get(dep);
@@ -1868,19 +1871,33 @@ class ProcessManager {
1868
1871
  allCaptures.set(dep, captures);
1869
1872
  }
1870
1873
  if (allCaptures.size === 0)
1871
- return;
1874
+ return {};
1872
1875
  const depNames = [...allCaptures.keys()].map((n) => escapeRegExp(n)).join("|");
1873
1876
  const refPattern = new RegExp(`\\$(${depNames})\\.(\\w+)`, "g");
1874
- let hadReplacement = false;
1875
- const expanded = proc.command.replace(refPattern, (match, dep, key) => {
1877
+ const replacer = (match, dep, key) => {
1876
1878
  const captures = allCaptures.get(dep);
1877
- if (captures && key in captures) {
1878
- hadReplacement = true;
1879
+ if (captures && key in captures)
1879
1880
  return captures[key];
1880
- }
1881
1881
  return match;
1882
- });
1883
- return hadReplacement ? expanded : undefined;
1882
+ };
1883
+ let command;
1884
+ const expandedCmd = proc.command.replace(refPattern, replacer);
1885
+ if (expandedCmd !== proc.command)
1886
+ command = expandedCmd;
1887
+ let env;
1888
+ if (proc.env) {
1889
+ const expandedEnv = {};
1890
+ let hadReplacement = false;
1891
+ for (const [k, v] of Object.entries(proc.env)) {
1892
+ const expanded = v.replace(refPattern, replacer);
1893
+ expandedEnv[k] = expanded;
1894
+ if (expanded !== v)
1895
+ hadReplacement = true;
1896
+ }
1897
+ if (hadReplacement)
1898
+ env = expandedEnv;
1899
+ }
1900
+ return { command, env };
1884
1901
  }
1885
1902
  updateStatus(name, status) {
1886
1903
  const state = this.states.get(name);
@@ -1908,7 +1925,8 @@ class ProcessManager {
1908
1925
  state.exitCode = null;
1909
1926
  state.restartCount++;
1910
1927
  this.startTimes.set(name, Date.now());
1911
- runner.restart(cols, rows, this.expandDependencyCaptures(name));
1928
+ const { command, env } = this.expandDependencyCaptures(name);
1929
+ runner.restart(cols, rows, command, env);
1912
1930
  }
1913
1931
  async stop(name) {
1914
1932
  const state = this.states.get(name);
@@ -1946,7 +1964,8 @@ class ProcessManager {
1946
1964
  state.exitCode = null;
1947
1965
  state.restartCount++;
1948
1966
  this.startTimes.set(name, Date.now());
1949
- this.runners.get(name)?.restart(cols, rows, this.expandDependencyCaptures(name));
1967
+ const { command, env } = this.expandDependencyCaptures(name);
1968
+ this.runners.get(name)?.restart(cols, rows, command, env);
1950
1969
  }
1951
1970
  restartAll(cols, rows) {
1952
1971
  log("Restarting all processes");
@@ -2019,6 +2038,7 @@ var STATUS_HINTS = [
2019
2038
  [SHORTCUTS.search.label, SHORTCUTS.search.description],
2020
2039
  [SHORTCUTS.copy.label, SHORTCUTS.copy.description],
2021
2040
  [SHORTCUTS.clear.label, SHORTCUTS.clear.description],
2041
+ ["Ctrl+Click", "open link"],
2022
2042
  ["Ctrl+C", "quit"]
2023
2043
  ];
2024
2044
  var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
@@ -2027,12 +2047,57 @@ var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
2027
2047
  import { ScrollBoxRenderable } from "@opentui/core";
2028
2048
  import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
2029
2049
 
2050
+ // src/ui/url-handler.ts
2051
+ var URL_RE = /https?:\/\/[^\s<>"'`)\]},;]+/g;
2052
+ var FILE_PATH_RE = /(?:\.\.?\/|\/)[^\s:]+(?::(\d+)(?::(\d+))?)?/g;
2053
+ var TRAILING_PUNCT = /[.,;:!?)>\]'"]+$/;
2054
+ function findLinksInLine(line) {
2055
+ const links = [];
2056
+ for (const m of line.matchAll(URL_RE)) {
2057
+ const url = m[0].replace(TRAILING_PUNCT, "");
2058
+ links.push({
2059
+ url,
2060
+ start: m.index,
2061
+ end: m.index + url.length,
2062
+ type: "url"
2063
+ });
2064
+ }
2065
+ for (const m of line.matchAll(FILE_PATH_RE)) {
2066
+ const start = m.index;
2067
+ const end = m.index + m[0].length;
2068
+ if (links.some((l) => start < l.end && end > l.start))
2069
+ continue;
2070
+ links.push({
2071
+ url: m[0],
2072
+ start,
2073
+ end,
2074
+ type: "file"
2075
+ });
2076
+ }
2077
+ return links.sort((a, b) => a.start - b.start);
2078
+ }
2079
+ function findLinkAtPosition(line, col) {
2080
+ const links = findLinksInLine(line);
2081
+ return links.find((l) => col >= l.start && col < l.end) ?? null;
2082
+ }
2083
+ function openLink(link) {
2084
+ const opener = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
2085
+ if (!opener)
2086
+ return;
2087
+ try {
2088
+ const proc = Bun.spawn([opener, link.url]);
2089
+ proc.exited.catch(() => {});
2090
+ } catch {}
2091
+ }
2092
+
2093
+ // src/ui/pane.ts
2030
2094
  class Pane {
2031
2095
  scrollBox;
2032
2096
  terminal;
2033
2097
  decoder = new TextDecoder;
2034
2098
  _onScroll = null;
2035
2099
  _onCopy = null;
2100
+ _onLinkClick = null;
2036
2101
  _textLines = null;
2037
2102
  _textLinesLower = null;
2038
2103
  constructor(renderer, name, cols, rows, interactive = false) {
@@ -2067,6 +2132,15 @@ class Pane {
2067
2132
  }
2068
2133
  return result;
2069
2134
  };
2135
+ this.terminal.onMouseDown = (event) => {
2136
+ if (event.modifiers.ctrl && event.button === 0) {
2137
+ const link = this.getLinkAtMouse(event.x, event.y);
2138
+ if (link) {
2139
+ event.stopPropagation();
2140
+ this._onLinkClick?.(link);
2141
+ }
2142
+ }
2143
+ };
2070
2144
  this.scrollBox.add(this.terminal);
2071
2145
  }
2072
2146
  feed(data) {
@@ -2105,6 +2179,21 @@ class Pane {
2105
2179
  onCopy(handler) {
2106
2180
  this._onCopy = handler;
2107
2181
  }
2182
+ onLinkClick(handler) {
2183
+ this._onLinkClick = handler;
2184
+ }
2185
+ getLinkAtMouse(localX, localY) {
2186
+ if (!this._textLines) {
2187
+ const text = this.terminal.getText();
2188
+ this._textLines = text.split(`
2189
+ `);
2190
+ this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
2191
+ }
2192
+ const lineIndex = Math.floor(this.scrollBox.scrollTop) + localY;
2193
+ if (lineIndex < 0 || lineIndex >= this._textLines.length)
2194
+ return null;
2195
+ return findLinkAtPosition(this._textLines[lineIndex], localX);
2196
+ }
2108
2197
  show() {
2109
2198
  this.scrollBox.visible = true;
2110
2199
  }
@@ -2554,6 +2643,10 @@ class App {
2554
2643
  this.copyToClipboard(text);
2555
2644
  this.statusBar.showTemporaryMessage("Copied!");
2556
2645
  });
2646
+ pane.onLinkClick((link) => {
2647
+ openLink(link);
2648
+ this.statusBar.showTemporaryMessage(`Opening ${link.url}`);
2649
+ });
2557
2650
  pane.onScroll(() => {
2558
2651
  if (this.searchMode && this.searchMatches.length > 0 && this.activePane === name) {
2559
2652
  this.updateSearchHighlights();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.16.1",
3
+ "version": "1.18.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",