numux 2.2.0 → 2.3.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 +1 -1
- package/dist/numux.js +144 -19
- package/dist/types.d.ts +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ Template properties (color, env, dependsOn, etc.) are inherited by all matched p
|
|
|
174
174
|
| `--max-restarts <n>` | Max auto-restarts for crashed processes |
|
|
175
175
|
| `-s, --sort <mode>` | Tab display order: `config` (default), `alphabetical`, `topological` |
|
|
176
176
|
| `--no-watch` | Disable file watching even if config has `watch` patterns |
|
|
177
|
-
| `-t, --timestamps` | Add `
|
|
177
|
+
| `-t, --timestamps [format]` | Add timestamps (default `HH:mm:ss`). Works in both prefix and TUI mode. Pass a format string for custom output (e.g. `HH:mm:ss.SSS`). Toggle in TUI with `T` |
|
|
178
178
|
| `--log-dir <path>` | Persist logs to timestamped subdirs (`<path>/<timestamp>/<name>.log`) with a `latest` symlink. Path is printed on exit |
|
|
179
179
|
| `--debug` | Log to `.numux/debug.log` |
|
|
180
180
|
| `-h, --help` | Show help |
|
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: "2.
|
|
39
|
+
version: "2.3.0",
|
|
40
40
|
description: "Terminal multiplexer with dependency orchestration",
|
|
41
41
|
type: "module",
|
|
42
42
|
license: "MIT",
|
|
@@ -224,11 +224,13 @@ var FLAGS = [
|
|
|
224
224
|
description: "Disable file watching even if config has watch patterns"
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
|
-
type: "
|
|
227
|
+
type: "optional-value",
|
|
228
228
|
long: "--timestamps",
|
|
229
229
|
short: "-t",
|
|
230
230
|
key: "timestamps",
|
|
231
|
-
description: "Add timestamps to
|
|
231
|
+
description: "Add timestamps to output (default HH:mm:ss, or pass a format string)",
|
|
232
|
+
valueName: "<format>",
|
|
233
|
+
completionHint: "none"
|
|
232
234
|
},
|
|
233
235
|
{
|
|
234
236
|
type: "value",
|
|
@@ -330,6 +332,8 @@ function generateHelp() {
|
|
|
330
332
|
parts.push(f.long);
|
|
331
333
|
if (f.type === "value")
|
|
332
334
|
parts.push(f.valueName);
|
|
335
|
+
if (f.type === "optional-value")
|
|
336
|
+
parts.push(`[${f.valueName}]`);
|
|
333
337
|
const left = ` ${parts.join(" ")}`;
|
|
334
338
|
lines.push(`${left.padEnd(29)}${f.description}`);
|
|
335
339
|
}
|
|
@@ -375,6 +379,14 @@ function parseArgs(argv) {
|
|
|
375
379
|
if (flag) {
|
|
376
380
|
if (flag.type === "boolean") {
|
|
377
381
|
result[flag.key] = true;
|
|
382
|
+
} else if (flag.type === "optional-value") {
|
|
383
|
+
const next = args[i + 1];
|
|
384
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
385
|
+
result[flag.key] = next;
|
|
386
|
+
i++;
|
|
387
|
+
} else {
|
|
388
|
+
result[flag.key] = true;
|
|
389
|
+
}
|
|
378
390
|
} else {
|
|
379
391
|
const next = args[++i];
|
|
380
392
|
if (next === undefined) {
|
|
@@ -1099,7 +1111,7 @@ function validateConfig(raw, _warnings) {
|
|
|
1099
1111
|
}
|
|
1100
1112
|
const sort = validateSort(config.sort);
|
|
1101
1113
|
const prefix = config.prefix === true ? true : undefined;
|
|
1102
|
-
const timestamps = config.timestamps === true ? true : undefined;
|
|
1114
|
+
const timestamps = config.timestamps === true ? true : typeof config.timestamps === "string" ? config.timestamps : undefined;
|
|
1103
1115
|
const killOthers = config.killOthers === true ? true : undefined;
|
|
1104
1116
|
const killOthersOnFail = config.killOthersOnFail === true ? true : undefined;
|
|
1105
1117
|
const noWatch = config.noWatch === true ? true : undefined;
|
|
@@ -2145,6 +2157,7 @@ var SHORTCUTS = {
|
|
|
2145
2157
|
restart: { key: "r", label: "R", description: "restart" },
|
|
2146
2158
|
stopStart: { key: "s", label: "S", description: "stop/start" },
|
|
2147
2159
|
clear: { key: "l", label: "L", description: "clear" },
|
|
2160
|
+
timestamps: { key: "t", label: "T", description: "timestamps" },
|
|
2148
2161
|
scrollToTop: { key: "g", label: "G", description: "top" },
|
|
2149
2162
|
scrollToBottom: { key: "g", label: "Shift+G", description: "bottom", shift: true }
|
|
2150
2163
|
};
|
|
@@ -2156,15 +2169,32 @@ var STATUS_HINTS = [
|
|
|
2156
2169
|
[SHORTCUTS.search.label, SHORTCUTS.search.description],
|
|
2157
2170
|
[SHORTCUTS.copy.label, SHORTCUTS.copy.description],
|
|
2158
2171
|
[SHORTCUTS.clear.label, SHORTCUTS.clear.description],
|
|
2172
|
+
[SHORTCUTS.timestamps.label, SHORTCUTS.timestamps.description],
|
|
2159
2173
|
["Ctrl+Click", "open link"],
|
|
2160
2174
|
["Ctrl+C", "quit"]
|
|
2161
2175
|
];
|
|
2162
2176
|
var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
|
|
2163
2177
|
|
|
2164
2178
|
// src/ui/pane.ts
|
|
2165
|
-
import {
|
|
2179
|
+
import {
|
|
2180
|
+
LineNumberRenderable,
|
|
2181
|
+
ScrollBoxRenderable
|
|
2182
|
+
} from "@opentui/core";
|
|
2166
2183
|
import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
|
|
2167
2184
|
|
|
2185
|
+
// src/utils/timestamp.ts
|
|
2186
|
+
var DEFAULT_TIMESTAMP_FORMAT = "HH:mm:ss";
|
|
2187
|
+
function formatTimestamp(date, format) {
|
|
2188
|
+
const hours24 = date.getHours();
|
|
2189
|
+
const hours12 = hours24 % 12 || 12;
|
|
2190
|
+
return format.replace("YYYY", date.getFullYear().toString()).replace("MM", (date.getMonth() + 1).toString().padStart(2, "0")).replace("DD", date.getDate().toString().padStart(2, "0")).replace("HH", hours24.toString().padStart(2, "0")).replace("hh", hours12.toString().padStart(2, "0")).replace("mm", date.getMinutes().toString().padStart(2, "0")).replace("SSS", date.getMilliseconds().toString().padStart(3, "0")).replace("ss", date.getSeconds().toString().padStart(2, "0")).replace("A", hours24 < 12 ? "AM" : "PM");
|
|
2191
|
+
}
|
|
2192
|
+
function resolveTimestampFormat(timestamps) {
|
|
2193
|
+
if (!timestamps)
|
|
2194
|
+
return null;
|
|
2195
|
+
return typeof timestamps === "string" ? timestamps : DEFAULT_TIMESTAMP_FORMAT;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2168
2198
|
// src/ui/url-handler.ts
|
|
2169
2199
|
var URL_RE = /https?:\/\/[^\s<>"'`)\]},;]+/g;
|
|
2170
2200
|
var FILE_PATH_RE = /(?:\.\.?\/|\/)[^\s:]+(?::(\d+)(?::(\d+))?)?/g;
|
|
@@ -2217,10 +2247,16 @@ class Pane {
|
|
|
2217
2247
|
terminal;
|
|
2218
2248
|
decoder = new TextDecoder;
|
|
2219
2249
|
bytesFed = 0;
|
|
2250
|
+
renderer;
|
|
2251
|
+
timestampGutter = null;
|
|
2252
|
+
_timestampFormat = null;
|
|
2253
|
+
lineTimestamps = [];
|
|
2254
|
+
lineCounter = 0;
|
|
2220
2255
|
_onScroll = null;
|
|
2221
2256
|
_onCopy = null;
|
|
2222
2257
|
_onLinkClick = null;
|
|
2223
2258
|
constructor(renderer, name, cols, rows, interactive = false) {
|
|
2259
|
+
this.renderer = renderer;
|
|
2224
2260
|
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
2225
2261
|
id: `pane-${name}`,
|
|
2226
2262
|
flexGrow: 1,
|
|
@@ -2247,7 +2283,12 @@ class Pane {
|
|
|
2247
2283
|
if (text) {
|
|
2248
2284
|
this._onCopy?.(text);
|
|
2249
2285
|
} else {
|
|
2250
|
-
|
|
2286
|
+
const stale = renderer.getSelection();
|
|
2287
|
+
queueMicrotask(() => {
|
|
2288
|
+
if (renderer.getSelection() === stale) {
|
|
2289
|
+
renderer.clearSelection();
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2251
2292
|
}
|
|
2252
2293
|
}
|
|
2253
2294
|
return result;
|
|
@@ -2268,9 +2309,25 @@ class Pane {
|
|
|
2268
2309
|
if (this.terminal.lineCount > MAX_SCROLLBACK_LINES || this.bytesFed > MAX_BUFFER_BYTES) {
|
|
2269
2310
|
this.terminal.reset();
|
|
2270
2311
|
this.bytesFed = 0;
|
|
2312
|
+
this.lineTimestamps = [];
|
|
2313
|
+
this.lineCounter = 0;
|
|
2314
|
+
}
|
|
2315
|
+
const now = Date.now();
|
|
2316
|
+
if (this.lineCounter === 0) {
|
|
2317
|
+
this.lineTimestamps.push(now);
|
|
2318
|
+
this.lineCounter = 1;
|
|
2319
|
+
}
|
|
2320
|
+
for (let i = 0;i < data.length; i++) {
|
|
2321
|
+
if (data[i] === 10) {
|
|
2322
|
+
this.lineTimestamps.push(now);
|
|
2323
|
+
this.lineCounter++;
|
|
2324
|
+
}
|
|
2271
2325
|
}
|
|
2272
2326
|
const text = this.decoder.decode(data, { stream: true });
|
|
2273
2327
|
this.terminal.feed(text);
|
|
2328
|
+
if (this._timestampFormat) {
|
|
2329
|
+
this.updateTimestampSigns();
|
|
2330
|
+
}
|
|
2274
2331
|
}
|
|
2275
2332
|
resize(cols, rows) {
|
|
2276
2333
|
this.terminal.cols = cols;
|
|
@@ -2347,6 +2404,60 @@ class Pane {
|
|
|
2347
2404
|
clear() {
|
|
2348
2405
|
this.terminal.reset();
|
|
2349
2406
|
this.bytesFed = 0;
|
|
2407
|
+
this.lineTimestamps = [];
|
|
2408
|
+
this.lineCounter = 0;
|
|
2409
|
+
if (this._timestampFormat) {
|
|
2410
|
+
this.timestampGutter?.clearAllLineSigns();
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
setTimestamps(value) {
|
|
2414
|
+
const newFormat = !value ? null : typeof value === "string" ? value : DEFAULT_TIMESTAMP_FORMAT;
|
|
2415
|
+
const wasEnabled = this._timestampFormat !== null;
|
|
2416
|
+
const isEnabled = newFormat !== null;
|
|
2417
|
+
if (wasEnabled === isEnabled && this._timestampFormat === newFormat)
|
|
2418
|
+
return;
|
|
2419
|
+
this._timestampFormat = newFormat;
|
|
2420
|
+
if (isEnabled && !wasEnabled) {
|
|
2421
|
+
this.scrollBox.remove(this.terminal.id);
|
|
2422
|
+
const gutterWidth = (newFormat?.length ?? 8) + 1;
|
|
2423
|
+
this.timestampGutter = new LineNumberRenderable(this.renderer, {
|
|
2424
|
+
id: `ts-${this.terminal.id}`,
|
|
2425
|
+
target: this.terminal,
|
|
2426
|
+
showLineNumbers: false,
|
|
2427
|
+
minWidth: gutterWidth,
|
|
2428
|
+
paddingRight: 0,
|
|
2429
|
+
fg: "#666666"
|
|
2430
|
+
});
|
|
2431
|
+
this.scrollBox.add(this.timestampGutter);
|
|
2432
|
+
this.updateTimestampSigns();
|
|
2433
|
+
} else if (!isEnabled && wasEnabled) {
|
|
2434
|
+
if (this.timestampGutter) {
|
|
2435
|
+
this.timestampGutter.clearTarget();
|
|
2436
|
+
this.scrollBox.remove(this.timestampGutter.id);
|
|
2437
|
+
this.timestampGutter = null;
|
|
2438
|
+
}
|
|
2439
|
+
this.scrollBox.add(this.terminal);
|
|
2440
|
+
} else if (isEnabled) {
|
|
2441
|
+
this.updateTimestampSigns();
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
get timestampsEnabled() {
|
|
2445
|
+
return this._timestampFormat !== null;
|
|
2446
|
+
}
|
|
2447
|
+
updateTimestampSigns() {
|
|
2448
|
+
if (!(this.timestampGutter && this._timestampFormat))
|
|
2449
|
+
return;
|
|
2450
|
+
const fmt = this._timestampFormat;
|
|
2451
|
+
const signs = new Map;
|
|
2452
|
+
let prevFormatted = "";
|
|
2453
|
+
for (let i = 0;i < this.lineTimestamps.length; i++) {
|
|
2454
|
+
const formatted = formatTimestamp(new Date(this.lineTimestamps[i]), fmt);
|
|
2455
|
+
if (formatted !== prevFormatted) {
|
|
2456
|
+
signs.set(i, { before: formatted });
|
|
2457
|
+
prevFormatted = formatted;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
this.timestampGutter.setLineSigns(signs);
|
|
2350
2461
|
}
|
|
2351
2462
|
destroy() {
|
|
2352
2463
|
this.terminal.destroy();
|
|
@@ -2568,6 +2679,7 @@ class StatusBar {
|
|
|
2568
2679
|
bg: "#1a1a1a",
|
|
2569
2680
|
paddingX: 1
|
|
2570
2681
|
});
|
|
2682
|
+
this.renderable.selectable = false;
|
|
2571
2683
|
}
|
|
2572
2684
|
setSearchMode(active, query = "", matchCount = 0, currentIndex = -1, crossProcessInfo) {
|
|
2573
2685
|
this._searchMode = active;
|
|
@@ -2953,6 +3065,9 @@ class App {
|
|
|
2953
3065
|
for (const name of this.names) {
|
|
2954
3066
|
const interactive = this.config.processes[name].interactive === true;
|
|
2955
3067
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
3068
|
+
if (this.config.timestamps) {
|
|
3069
|
+
pane.setTimestamps(this.config.timestamps);
|
|
3070
|
+
}
|
|
2956
3071
|
pane.onCopy((text) => {
|
|
2957
3072
|
this.copyToClipboard(text);
|
|
2958
3073
|
this.statusBar.showTemporaryMessage("Copied!");
|
|
@@ -3066,6 +3181,18 @@ class App {
|
|
|
3066
3181
|
this.logWriter.truncate(this.activePane);
|
|
3067
3182
|
return;
|
|
3068
3183
|
}
|
|
3184
|
+
if (name === SHORTCUTS.timestamps.key) {
|
|
3185
|
+
const firstPane = this.panes.values().next().value;
|
|
3186
|
+
if (firstPane?.timestampsEnabled) {
|
|
3187
|
+
for (const pane of this.panes.values())
|
|
3188
|
+
pane.setTimestamps(false);
|
|
3189
|
+
} else {
|
|
3190
|
+
const fmt = this.config.timestamps ?? true;
|
|
3191
|
+
for (const pane of this.panes.values())
|
|
3192
|
+
pane.setTimestamps(fmt);
|
|
3193
|
+
}
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3069
3196
|
const num = Number.parseInt(name, 10);
|
|
3070
3197
|
if (num >= 1 && num <= 9 && num <= this.tabBar.count) {
|
|
3071
3198
|
this.tabBar.setSelectedIndex(num - 1);
|
|
@@ -3208,9 +3335,10 @@ class App {
|
|
|
3208
3335
|
// src/ui/prefix.ts
|
|
3209
3336
|
var RESET = ANSI_RESET;
|
|
3210
3337
|
var DIM = "\x1B[90m";
|
|
3338
|
+
var CHA_COL1_RE = /\x1b\[1?G/g;
|
|
3211
3339
|
var CURSOR_SEQ_RE = /\x1b\[[\d;]*[ABCDEFGHJKLMSTdf]/g;
|
|
3212
3340
|
function stripCursorSequences(text) {
|
|
3213
|
-
return text.replace(CURSOR_SEQ_RE, "");
|
|
3341
|
+
return text.replace(CHA_COL1_RE, "\r").replace(CURSOR_SEQ_RE, "");
|
|
3214
3342
|
}
|
|
3215
3343
|
|
|
3216
3344
|
class PrefixDisplay {
|
|
@@ -3222,7 +3350,7 @@ class PrefixDisplay {
|
|
|
3222
3350
|
logWriter;
|
|
3223
3351
|
killOthers;
|
|
3224
3352
|
killOthersOnFail;
|
|
3225
|
-
|
|
3353
|
+
timestampFormat;
|
|
3226
3354
|
stopping = false;
|
|
3227
3355
|
startTime = 0;
|
|
3228
3356
|
constructor(manager, config, options = {}) {
|
|
@@ -3230,7 +3358,7 @@ class PrefixDisplay {
|
|
|
3230
3358
|
this.logWriter = options.logWriter;
|
|
3231
3359
|
this.killOthers = options.killOthers ?? false;
|
|
3232
3360
|
this.killOthersOnFail = options.killOthersOnFail ?? false;
|
|
3233
|
-
this.
|
|
3361
|
+
this.timestampFormat = resolveTimestampFormat(options.timestamps);
|
|
3234
3362
|
this.noColor = "NO_COLOR" in process.env;
|
|
3235
3363
|
const names = manager.getProcessNames();
|
|
3236
3364
|
this.colors = buildProcessColorMap(names, config);
|
|
@@ -3299,16 +3427,10 @@ class PrefixDisplay {
|
|
|
3299
3427
|
}
|
|
3300
3428
|
}
|
|
3301
3429
|
handleStatus(_name, _status) {}
|
|
3302
|
-
formatTimestamp() {
|
|
3303
|
-
const now = new Date;
|
|
3304
|
-
const h = String(now.getHours()).padStart(2, "0");
|
|
3305
|
-
const m = String(now.getMinutes()).padStart(2, "0");
|
|
3306
|
-
const s = String(now.getSeconds()).padStart(2, "0");
|
|
3307
|
-
return `${h}:${m}:${s}`;
|
|
3308
|
-
}
|
|
3309
3430
|
printLine(name, line) {
|
|
3310
|
-
const
|
|
3311
|
-
const
|
|
3431
|
+
const fmt = this.timestampFormat;
|
|
3432
|
+
const ts = fmt ? `${DIM}[${formatTimestamp(new Date, fmt)}]${RESET} ` : "";
|
|
3433
|
+
const tsPlain = fmt ? `[${formatTimestamp(new Date, fmt)}] ` : "";
|
|
3312
3434
|
if (this.noColor) {
|
|
3313
3435
|
process.stdout.write(`${tsPlain}[${name}] ${stripAnsi(line)}
|
|
3314
3436
|
`);
|
|
@@ -3845,16 +3967,19 @@ async function main() {
|
|
|
3845
3967
|
const logDir = parsed.logDir ?? config.logDir;
|
|
3846
3968
|
const logWriter = logDir ? LogWriter.createPersistent(logDir) : LogWriter.createTemp();
|
|
3847
3969
|
printWarnings(warnings);
|
|
3970
|
+
const timestamps = parsed.timestamps || config.timestamps;
|
|
3848
3971
|
const usePrefix = parsed.prefix || config.prefix;
|
|
3849
3972
|
if (usePrefix) {
|
|
3850
3973
|
const display = new PrefixDisplay(manager, config, {
|
|
3851
3974
|
logWriter,
|
|
3852
3975
|
killOthers: parsed.killOthers || config.killOthers,
|
|
3853
3976
|
killOthersOnFail: parsed.killOthersOnFail || config.killOthersOnFail,
|
|
3854
|
-
timestamps
|
|
3977
|
+
timestamps
|
|
3855
3978
|
});
|
|
3856
3979
|
await display.start();
|
|
3857
3980
|
} else {
|
|
3981
|
+
if (timestamps)
|
|
3982
|
+
config.timestamps = timestamps;
|
|
3858
3983
|
manager.on(logWriter.handleEvent);
|
|
3859
3984
|
const app = new App(manager, config, logWriter);
|
|
3860
3985
|
setupShutdownHandlers(app, logWriter);
|
package/dist/types.d.ts
CHANGED
|
@@ -95,8 +95,8 @@ export interface NumuxConfig<K extends string = string> {
|
|
|
95
95
|
* @default false
|
|
96
96
|
*/
|
|
97
97
|
prefix?: boolean;
|
|
98
|
-
/** Add timestamps to
|
|
99
|
-
timestamps?: boolean;
|
|
98
|
+
/** Add timestamps to output lines. `true` uses default `HH:mm:ss` format, or pass a format string (e.g. `"HH:mm:ss.SSS"`) */
|
|
99
|
+
timestamps?: boolean | string;
|
|
100
100
|
/**
|
|
101
101
|
* Kill all processes when any one exits (regardless of exit code)
|
|
102
102
|
* @default false
|
|
@@ -125,7 +125,7 @@ export interface ResolvedProcessConfig extends Omit<NumuxProcessConfig, 'depends
|
|
|
125
125
|
export interface ResolvedNumuxConfig {
|
|
126
126
|
sort?: SortOrder;
|
|
127
127
|
prefix?: boolean;
|
|
128
|
-
timestamps?: boolean;
|
|
128
|
+
timestamps?: boolean | string;
|
|
129
129
|
killOthers?: boolean;
|
|
130
130
|
killOthersOnFail?: boolean;
|
|
131
131
|
noWatch?: boolean;
|