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.
- package/README.md +2 -1
- package/dist/numux.js +112 -19
- 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
|
|
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.
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|