numux 2.2.1 → 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 +135 -17
- 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,
|
|
@@ -2273,9 +2309,25 @@ class Pane {
|
|
|
2273
2309
|
if (this.terminal.lineCount > MAX_SCROLLBACK_LINES || this.bytesFed > MAX_BUFFER_BYTES) {
|
|
2274
2310
|
this.terminal.reset();
|
|
2275
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
|
+
}
|
|
2276
2325
|
}
|
|
2277
2326
|
const text = this.decoder.decode(data, { stream: true });
|
|
2278
2327
|
this.terminal.feed(text);
|
|
2328
|
+
if (this._timestampFormat) {
|
|
2329
|
+
this.updateTimestampSigns();
|
|
2330
|
+
}
|
|
2279
2331
|
}
|
|
2280
2332
|
resize(cols, rows) {
|
|
2281
2333
|
this.terminal.cols = cols;
|
|
@@ -2352,6 +2404,60 @@ class Pane {
|
|
|
2352
2404
|
clear() {
|
|
2353
2405
|
this.terminal.reset();
|
|
2354
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);
|
|
2355
2461
|
}
|
|
2356
2462
|
destroy() {
|
|
2357
2463
|
this.terminal.destroy();
|
|
@@ -2959,6 +3065,9 @@ class App {
|
|
|
2959
3065
|
for (const name of this.names) {
|
|
2960
3066
|
const interactive = this.config.processes[name].interactive === true;
|
|
2961
3067
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
3068
|
+
if (this.config.timestamps) {
|
|
3069
|
+
pane.setTimestamps(this.config.timestamps);
|
|
3070
|
+
}
|
|
2962
3071
|
pane.onCopy((text) => {
|
|
2963
3072
|
this.copyToClipboard(text);
|
|
2964
3073
|
this.statusBar.showTemporaryMessage("Copied!");
|
|
@@ -3072,6 +3181,18 @@ class App {
|
|
|
3072
3181
|
this.logWriter.truncate(this.activePane);
|
|
3073
3182
|
return;
|
|
3074
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
|
+
}
|
|
3075
3196
|
const num = Number.parseInt(name, 10);
|
|
3076
3197
|
if (num >= 1 && num <= 9 && num <= this.tabBar.count) {
|
|
3077
3198
|
this.tabBar.setSelectedIndex(num - 1);
|
|
@@ -3229,7 +3350,7 @@ class PrefixDisplay {
|
|
|
3229
3350
|
logWriter;
|
|
3230
3351
|
killOthers;
|
|
3231
3352
|
killOthersOnFail;
|
|
3232
|
-
|
|
3353
|
+
timestampFormat;
|
|
3233
3354
|
stopping = false;
|
|
3234
3355
|
startTime = 0;
|
|
3235
3356
|
constructor(manager, config, options = {}) {
|
|
@@ -3237,7 +3358,7 @@ class PrefixDisplay {
|
|
|
3237
3358
|
this.logWriter = options.logWriter;
|
|
3238
3359
|
this.killOthers = options.killOthers ?? false;
|
|
3239
3360
|
this.killOthersOnFail = options.killOthersOnFail ?? false;
|
|
3240
|
-
this.
|
|
3361
|
+
this.timestampFormat = resolveTimestampFormat(options.timestamps);
|
|
3241
3362
|
this.noColor = "NO_COLOR" in process.env;
|
|
3242
3363
|
const names = manager.getProcessNames();
|
|
3243
3364
|
this.colors = buildProcessColorMap(names, config);
|
|
@@ -3306,16 +3427,10 @@ class PrefixDisplay {
|
|
|
3306
3427
|
}
|
|
3307
3428
|
}
|
|
3308
3429
|
handleStatus(_name, _status) {}
|
|
3309
|
-
formatTimestamp() {
|
|
3310
|
-
const now = new Date;
|
|
3311
|
-
const h = String(now.getHours()).padStart(2, "0");
|
|
3312
|
-
const m = String(now.getMinutes()).padStart(2, "0");
|
|
3313
|
-
const s = String(now.getSeconds()).padStart(2, "0");
|
|
3314
|
-
return `${h}:${m}:${s}`;
|
|
3315
|
-
}
|
|
3316
3430
|
printLine(name, line) {
|
|
3317
|
-
const
|
|
3318
|
-
const
|
|
3431
|
+
const fmt = this.timestampFormat;
|
|
3432
|
+
const ts = fmt ? `${DIM}[${formatTimestamp(new Date, fmt)}]${RESET} ` : "";
|
|
3433
|
+
const tsPlain = fmt ? `[${formatTimestamp(new Date, fmt)}] ` : "";
|
|
3319
3434
|
if (this.noColor) {
|
|
3320
3435
|
process.stdout.write(`${tsPlain}[${name}] ${stripAnsi(line)}
|
|
3321
3436
|
`);
|
|
@@ -3852,16 +3967,19 @@ async function main() {
|
|
|
3852
3967
|
const logDir = parsed.logDir ?? config.logDir;
|
|
3853
3968
|
const logWriter = logDir ? LogWriter.createPersistent(logDir) : LogWriter.createTemp();
|
|
3854
3969
|
printWarnings(warnings);
|
|
3970
|
+
const timestamps = parsed.timestamps || config.timestamps;
|
|
3855
3971
|
const usePrefix = parsed.prefix || config.prefix;
|
|
3856
3972
|
if (usePrefix) {
|
|
3857
3973
|
const display = new PrefixDisplay(manager, config, {
|
|
3858
3974
|
logWriter,
|
|
3859
3975
|
killOthers: parsed.killOthers || config.killOthers,
|
|
3860
3976
|
killOthersOnFail: parsed.killOthersOnFail || config.killOthersOnFail,
|
|
3861
|
-
timestamps
|
|
3977
|
+
timestamps
|
|
3862
3978
|
});
|
|
3863
3979
|
await display.start();
|
|
3864
3980
|
} else {
|
|
3981
|
+
if (timestamps)
|
|
3982
|
+
config.timestamps = timestamps;
|
|
3865
3983
|
manager.on(logWriter.handleEvent);
|
|
3866
3984
|
const app = new App(manager, config, logWriter);
|
|
3867
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;
|