numux 1.22.0 → 1.23.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/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.22.0",
39
+ version: "1.23.0",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -915,12 +915,12 @@ function findCycle(remaining, config) {
915
915
  // src/utils/color.ts
916
916
  var BASIC_COLORS = {
917
917
  black: "#000000",
918
- red: "#ff0000",
919
- green: "#00ff00",
920
- yellow: "#ffff00",
921
- blue: "#0000ff",
922
- magenta: "#ff00ff",
923
- cyan: "#00ffff",
918
+ red: "#ff5555",
919
+ green: "#00cc00",
920
+ yellow: "#cccc00",
921
+ blue: "#0000cc",
922
+ magenta: "#cc00cc",
923
+ cyan: "#00cccc",
924
924
  white: "#ffffff",
925
925
  gray: "#808080",
926
926
  grey: "#808080",
@@ -949,35 +949,35 @@ function hexToAnsi(hex) {
949
949
  }
950
950
  var HEX_COLOR_RE = /^#?[0-9a-fA-F]{6}$/;
951
951
  var STATUS_ANSI = {
952
- ready: "\x1B[32m",
953
- running: "\x1B[36m",
954
- finished: "\x1B[32m",
955
- failed: "\x1B[31m",
956
- stopped: "\x1B[90m",
957
- skipped: "\x1B[90m"
952
+ ready: hexToAnsi(BASIC_COLORS.green),
953
+ running: hexToAnsi(BASIC_COLORS.cyan),
954
+ finished: hexToAnsi(BASIC_COLORS.green),
955
+ failed: hexToAnsi(BASIC_COLORS.red),
956
+ stopped: hexToAnsi(BASIC_COLORS.gray),
957
+ skipped: hexToAnsi(BASIC_COLORS.gray)
958
958
  };
959
959
  var ANSI_RESET = "\x1B[0m";
960
960
  var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()#][0-9A-Za-z]|\x1b[A-Za-z><=]/g;
961
961
  function stripAnsi(str) {
962
962
  return str.replace(ANSI_RE, "");
963
963
  }
964
- var DEFAULT_ANSI_COLORS = [
965
- "\x1B[36m",
966
- "\x1B[33m",
967
- "\x1B[35m",
968
- "\x1B[34m",
969
- "\x1B[32m",
970
- "\x1B[91m",
971
- "\x1B[93m",
972
- "\x1B[95m"
964
+ var DEFAULT_PALETTE = [
965
+ BASIC_COLORS.cyan,
966
+ BASIC_COLORS.yellow,
967
+ BASIC_COLORS.magenta,
968
+ BASIC_COLORS.blue,
969
+ BASIC_COLORS.green,
970
+ BASIC_COLORS.red,
971
+ BASIC_COLORS.orange,
972
+ BASIC_COLORS.purple
973
973
  ];
974
- var DEFAULT_HEX_COLORS = ["#00cccc", "#cccc00", "#cc00cc", "#0000cc", "#00cc00", "#ff5555", "#ffff55", "#ff55ff"];
974
+ var DEFAULT_ANSI_COLORS = DEFAULT_PALETTE.map(hexToAnsi);
975
975
  function colorFromName(name) {
976
976
  let hash = 0;
977
977
  for (let i = 0;i < name.length; i++) {
978
978
  hash = (hash << 5) - hash + name.charCodeAt(i) | 0;
979
979
  }
980
- return DEFAULT_HEX_COLORS[Math.abs(hash) % DEFAULT_HEX_COLORS.length];
980
+ return DEFAULT_PALETTE[Math.abs(hash) % DEFAULT_PALETTE.length];
981
981
  }
982
982
  function resolveColor(color) {
983
983
  if (typeof color === "string")
@@ -1018,9 +1018,9 @@ function buildProcessHexColorMap(names, config) {
1018
1018
  if (hex)
1019
1019
  map.set(name, hex);
1020
1020
  else
1021
- map.set(name, DEFAULT_HEX_COLORS[paletteIndex++ % DEFAULT_HEX_COLORS.length]);
1021
+ map.set(name, DEFAULT_PALETTE[paletteIndex++ % DEFAULT_PALETTE.length]);
1022
1022
  } else {
1023
- map.set(name, DEFAULT_HEX_COLORS[paletteIndex % DEFAULT_HEX_COLORS.length]);
1023
+ map.set(name, DEFAULT_PALETTE[paletteIndex % DEFAULT_PALETTE.length]);
1024
1024
  paletteIndex++;
1025
1025
  }
1026
1026
  }
@@ -2180,8 +2180,6 @@ class Pane {
2180
2180
  _onScroll = null;
2181
2181
  _onCopy = null;
2182
2182
  _onLinkClick = null;
2183
- _textLines = null;
2184
- _textLinesLower = null;
2185
2183
  constructor(renderer, name, cols, rows, interactive = false) {
2186
2184
  this.scrollBox = new ScrollBoxRenderable(renderer, {
2187
2185
  id: `pane-${name}`,
@@ -2228,14 +2226,10 @@ class Pane {
2228
2226
  feed(data) {
2229
2227
  const text = this.decoder.decode(data, { stream: true });
2230
2228
  this.terminal.feed(text);
2231
- this._textLines = null;
2232
- this._textLinesLower = null;
2233
2229
  }
2234
2230
  resize(cols, rows) {
2235
2231
  this.terminal.cols = cols;
2236
2232
  this.terminal.rows = rows;
2237
- this._textLines = null;
2238
- this._textLinesLower = null;
2239
2233
  }
2240
2234
  get isAtBottom() {
2241
2235
  const { scrollTop, scrollHeight, viewport } = this.scrollBox;
@@ -2265,16 +2259,13 @@ class Pane {
2265
2259
  this._onLinkClick = handler;
2266
2260
  }
2267
2261
  getLinkAtMouse(localX, localY) {
2268
- if (!this._textLines) {
2269
- const text = this.terminal.getText();
2270
- this._textLines = text.split(`
2262
+ const text = this.terminal.getText();
2263
+ const lines = text.split(`
2271
2264
  `);
2272
- this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
2273
- }
2274
2265
  const lineIndex = Math.floor(this.scrollBox.scrollTop) + localY;
2275
- if (lineIndex < 0 || lineIndex >= this._textLines.length)
2266
+ if (lineIndex < 0 || lineIndex >= lines.length)
2276
2267
  return null;
2277
- return findLinkAtPosition(this._textLines[lineIndex], localX);
2268
+ return findLinkAtPosition(lines[lineIndex], localX);
2278
2269
  }
2279
2270
  show() {
2280
2271
  this.scrollBox.visible = true;
@@ -2282,30 +2273,6 @@ class Pane {
2282
2273
  hide() {
2283
2274
  this.scrollBox.visible = false;
2284
2275
  }
2285
- search(query) {
2286
- if (!query)
2287
- return [];
2288
- if (!this._textLines) {
2289
- const text = this.terminal.getText();
2290
- this._textLines = text.split(`
2291
- `);
2292
- this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
2293
- }
2294
- const lines = this._textLinesLower;
2295
- const matches = [];
2296
- const lowerQuery = query.toLowerCase();
2297
- for (let line = 0;line < lines.length; line++) {
2298
- let pos = 0;
2299
- while (true) {
2300
- const idx = lines[line].indexOf(lowerQuery, pos);
2301
- if (idx === -1)
2302
- break;
2303
- matches.push({ line, start: idx, end: idx + query.length });
2304
- pos = idx + 1;
2305
- }
2306
- }
2307
- return matches;
2308
- }
2309
2276
  setHighlights(matches, currentIndex) {
2310
2277
  const firstVisible = Math.max(0, Math.floor(this.scrollBox.scrollTop) - 2);
2311
2278
  const lastVisible = Math.ceil(this.scrollBox.scrollTop + this.scrollBox.viewport.height) + 2;
@@ -2334,8 +2301,6 @@ class Pane {
2334
2301
  }
2335
2302
  clear() {
2336
2303
  this.terminal.reset();
2337
- this._textLines = null;
2338
- this._textLinesLower = null;
2339
2304
  }
2340
2305
  destroy() {
2341
2306
  this.terminal.destroy();
@@ -2664,6 +2629,7 @@ class App {
2664
2629
  termRows = 24;
2665
2630
  sidebarWidth = 20;
2666
2631
  config;
2632
+ logWriter;
2667
2633
  resizeTimer = null;
2668
2634
  searchTimer = null;
2669
2635
  searchMode = false;
@@ -2672,9 +2638,10 @@ class App {
2672
2638
  searchIndex = -1;
2673
2639
  inputWaitTimers = new Map;
2674
2640
  awaitingInput = new Set;
2675
- constructor(manager, config) {
2641
+ constructor(manager, config, logWriter) {
2676
2642
  this.manager = manager;
2677
2643
  this.config = config;
2644
+ this.logWriter = logWriter;
2678
2645
  this.names = manager.getProcessNames();
2679
2646
  }
2680
2647
  async start() {
@@ -2832,6 +2799,7 @@ class App {
2832
2799
  }
2833
2800
  if (name === SHORTCUTS.clear.key) {
2834
2801
  this.panes.get(this.activePane)?.clear();
2802
+ this.logWriter.truncate(this.activePane);
2835
2803
  return;
2836
2804
  }
2837
2805
  const num = Number.parseInt(name, 10);
@@ -3004,14 +2972,16 @@ class App {
3004
2972
  this.runSearch();
3005
2973
  }, 100);
3006
2974
  }
3007
- runSearch() {
2975
+ async runSearch() {
3008
2976
  if (!this.activePane)
3009
2977
  return;
3010
- const pane = this.panes.get(this.activePane);
3011
- if (!pane)
2978
+ const query = this.searchQuery;
2979
+ const activeName = this.activePane;
2980
+ const matches = await this.logWriter.search(activeName, query);
2981
+ if (!this.searchMode || this.searchQuery !== query || this.activePane !== activeName)
3012
2982
  return;
3013
- this.searchMatches = pane.search(this.searchQuery);
3014
- this.searchIndex = this.searchMatches.length > 0 ? 0 : -1;
2983
+ this.searchMatches = matches;
2984
+ this.searchIndex = matches.length > 0 ? 0 : -1;
3015
2985
  this.updateSearchHighlights();
3016
2986
  if (this.searchIndex >= 0) {
3017
2987
  this.scrollToCurrentMatch();
@@ -3179,7 +3149,7 @@ class PrefixDisplay {
3179
3149
  const allDone = states.every((s) => s.status === "stopped" || s.status === "finished" || s.status === "failed" || s.status === "skipped");
3180
3150
  if (allDone) {
3181
3151
  this.printSummary();
3182
- this.logWriter?.close();
3152
+ this.logWriter?.cleanup();
3183
3153
  const anyFailed = states.some((s) => s.status === "failed");
3184
3154
  process.exit(anyFailed ? 1 : 0);
3185
3155
  }
@@ -3195,7 +3165,7 @@ class PrefixDisplay {
3195
3165
  this.flushBuffer(name);
3196
3166
  }
3197
3167
  this.printSummary();
3198
- this.logWriter?.close();
3168
+ this.logWriter?.cleanup();
3199
3169
  process.exit(code === 0 ? 0 : 1);
3200
3170
  });
3201
3171
  }
@@ -3247,24 +3217,31 @@ ${DIM}Done in ${elapsed}${RESET}
3247
3217
  for (const name of this.manager.getProcessNames()) {
3248
3218
  this.flushBuffer(name);
3249
3219
  }
3250
- this.logWriter?.close();
3220
+ this.logWriter?.cleanup();
3251
3221
  const anyFailed = this.manager.getAllStates().some((s) => s.status === "failed");
3252
3222
  process.exit(anyFailed ? 1 : 0);
3253
3223
  }
3254
3224
  }
3255
3225
 
3256
3226
  // src/utils/log-writer.ts
3257
- import { closeSync, mkdirSync as mkdirSync2, openSync, writeSync } from "fs";
3227
+ import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, writeSync } from "fs";
3228
+ import { tmpdir } from "os";
3258
3229
  import { join } from "path";
3259
3230
  class LogWriter {
3260
3231
  dir;
3232
+ isTemp;
3261
3233
  files = new Map;
3262
3234
  decoder = new TextDecoder;
3263
3235
  encoder = new TextEncoder;
3264
- constructor(dir) {
3236
+ constructor(dir, isTemp = false) {
3265
3237
  this.dir = dir;
3238
+ this.isTemp = isTemp;
3266
3239
  mkdirSync2(dir, { recursive: true });
3267
3240
  }
3241
+ static createTemp() {
3242
+ const dir = join(tmpdir(), `numux-${process.pid}`);
3243
+ return new LogWriter(dir, true);
3244
+ }
3268
3245
  errored = false;
3269
3246
  handleEvent = (event) => {
3270
3247
  if (event.type !== "output" || this.errored)
@@ -3285,12 +3262,81 @@ class LogWriter {
3285
3262
  `);
3286
3263
  }
3287
3264
  };
3265
+ getLogPath(name) {
3266
+ if (this.files.has(name)) {
3267
+ return join(this.dir, `${name}.log`);
3268
+ }
3269
+ return;
3270
+ }
3271
+ async search(name, query) {
3272
+ if (!query)
3273
+ return [];
3274
+ const path = this.getLogPath(name);
3275
+ if (!path)
3276
+ return [];
3277
+ try {
3278
+ const proc = Bun.spawn(["grep", "-inF", query, path], {
3279
+ stdout: "pipe",
3280
+ stderr: "ignore"
3281
+ });
3282
+ const output = await new Response(proc.stdout).text();
3283
+ await proc.exited;
3284
+ const matches = [];
3285
+ const lowerQuery = query.toLowerCase();
3286
+ for (const line of output.split(`
3287
+ `)) {
3288
+ if (!line)
3289
+ continue;
3290
+ const colonIdx = line.indexOf(":");
3291
+ if (colonIdx === -1)
3292
+ continue;
3293
+ const lineNumber = Number.parseInt(line.slice(0, colonIdx), 10);
3294
+ if (Number.isNaN(lineNumber))
3295
+ continue;
3296
+ const lineText = line.slice(colonIdx + 1).toLowerCase();
3297
+ let pos = 0;
3298
+ while (true) {
3299
+ const idx = lineText.indexOf(lowerQuery, pos);
3300
+ if (idx === -1)
3301
+ break;
3302
+ matches.push({
3303
+ line: lineNumber - 1,
3304
+ start: idx,
3305
+ end: idx + query.length
3306
+ });
3307
+ pos = idx + 1;
3308
+ }
3309
+ }
3310
+ return matches;
3311
+ } catch {
3312
+ return [];
3313
+ }
3314
+ }
3315
+ truncate(name) {
3316
+ const fd = this.files.get(name);
3317
+ if (fd === undefined)
3318
+ return;
3319
+ try {
3320
+ closeSync(fd);
3321
+ const path = join(this.dir, `${name}.log`);
3322
+ const newFd = openSync(path, "w");
3323
+ this.files.set(name, newFd);
3324
+ } catch {}
3325
+ }
3288
3326
  close() {
3289
3327
  for (const fd of this.files.values()) {
3290
3328
  closeSync(fd);
3291
3329
  }
3292
3330
  this.files.clear();
3293
3331
  }
3332
+ cleanup() {
3333
+ this.close();
3334
+ if (this.isTemp) {
3335
+ try {
3336
+ rmSync(this.dir, { recursive: true });
3337
+ } catch {}
3338
+ }
3339
+ }
3294
3340
  }
3295
3341
 
3296
3342
  // src/utils/shutdown.ts
@@ -3302,7 +3348,7 @@ function setupShutdownHandlers(app, logWriter) {
3302
3348
  }
3303
3349
  shuttingDown = true;
3304
3350
  app.shutdown().finally(() => {
3305
- logWriter?.close();
3351
+ logWriter?.cleanup();
3306
3352
  process.exit(app.hasFailures() ? 1 : 0);
3307
3353
  });
3308
3354
  };
@@ -3313,7 +3359,7 @@ function setupShutdownHandlers(app, logWriter) {
3313
3359
  process.stderr.write(`numux: unexpected error: ${err?.stack ?? err}
3314
3360
  `);
3315
3361
  app.shutdown().finally(() => {
3316
- logWriter?.close();
3362
+ logWriter?.cleanup();
3317
3363
  process.exit(1);
3318
3364
  });
3319
3365
  });
@@ -3323,7 +3369,7 @@ function setupShutdownHandlers(app, logWriter) {
3323
3369
  process.stderr.write(`numux: unhandled rejection: ${message}
3324
3370
  `);
3325
3371
  app.shutdown().finally(() => {
3326
- logWriter?.close();
3372
+ logWriter?.cleanup();
3327
3373
  process.exit(1);
3328
3374
  });
3329
3375
  });
@@ -3530,10 +3576,7 @@ async function main() {
3530
3576
  }
3531
3577
  }
3532
3578
  const manager = new ProcessManager(config);
3533
- let logWriter;
3534
- if (parsed.logDir) {
3535
- logWriter = new LogWriter(parsed.logDir);
3536
- }
3579
+ const logWriter = parsed.logDir ? new LogWriter(parsed.logDir) : LogWriter.createTemp();
3537
3580
  printWarnings(warnings);
3538
3581
  if (parsed.prefix) {
3539
3582
  if (!parsed.noRestart) {
@@ -3548,10 +3591,8 @@ async function main() {
3548
3591
  });
3549
3592
  await display.start();
3550
3593
  } else {
3551
- if (logWriter) {
3552
- manager.on(logWriter.handleEvent);
3553
- }
3554
- const app = new App(manager, config);
3594
+ manager.on(logWriter.handleEvent);
3595
+ const app = new App(manager, config, logWriter);
3555
3596
  setupShutdownHandlers(app, logWriter);
3556
3597
  await app.start();
3557
3598
  }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Color } from './utils/color';
1
2
  export interface NumuxProcessConfig<K extends string = string> {
2
3
  /** Shell command to run. Supports `$dep.group` references from dependency capture groups */
3
4
  command: string;
@@ -39,7 +40,7 @@ export interface NumuxProcessConfig<K extends string = string> {
39
40
  */
40
41
  stopSignal?: 'SIGTERM' | 'SIGINT' | 'SIGHUP';
41
42
  /** Hex color (e.g. `"#ff6600"`) or color name. Array for round-robin in script patterns */
42
- color?: string | string[];
43
+ color?: Color | Color[];
43
44
  /** Glob patterns — restart process when matching files change */
44
45
  watch?: string | string[];
45
46
  /**
@@ -0,0 +1,40 @@
1
+ /** Basic color names mapped to muted hex tones (lowercase keys) */
2
+ export type BasicColor = keyof typeof BASIC_COLORS;
3
+ export type Color = `#${string}` | BasicColor;
4
+ export declare const BASIC_COLORS: {
5
+ readonly black: "#000000";
6
+ readonly red: "#ff5555";
7
+ readonly green: "#00cc00";
8
+ readonly yellow: "#cccc00";
9
+ readonly blue: "#0000cc";
10
+ readonly magenta: "#cc00cc";
11
+ readonly cyan: "#00cccc";
12
+ readonly white: "#ffffff";
13
+ readonly gray: "#808080";
14
+ readonly grey: "#808080";
15
+ readonly orange: "#ffa500";
16
+ readonly purple: "#800080";
17
+ };
18
+ /** Check if a string is a valid color (hex or basic name) */
19
+ export declare function isValidColor(color: string): boolean;
20
+ /** Resolve any color (hex or basic name) to normalized hex (#rrggbb) */
21
+ export declare function resolveToHex(color: string): string;
22
+ /**
23
+ * Convert a hex color string (e.g. "#ff8800") to an ANSI true-color escape sequence.
24
+ * Returns an empty string if the hex is malformed.
25
+ */
26
+ export declare function hexToAnsi(hex: string): string;
27
+ /** Regex matching a valid 6-digit hex color (with or without leading #) */
28
+ export declare const HEX_COLOR_RE: RegExp;
29
+ import type { ProcessStatus, ResolvedNumuxConfig } from '../types';
30
+ /** ANSI color codes for process statuses */
31
+ export declare const STATUS_ANSI: Partial<Record<ProcessStatus, string>>;
32
+ export declare const ANSI_RESET = "\u001B[0m";
33
+ /** Strip ANSI escape sequences from text */
34
+ export declare function stripAnsi(str: string): string;
35
+ /** Pick a deterministic color from the default palette based on the process name */
36
+ export declare function colorFromName(name: string): Color;
37
+ /** Build a map of process names to ANSI color codes, using explicit config colors or a default palette. */
38
+ export declare function buildProcessColorMap(names: string[], config: ResolvedNumuxConfig): Map<string, string>;
39
+ /** Build a map of process names to hex color strings (for StyledText rendering). */
40
+ export declare function buildProcessHexColorMap(names: string[], config: ResolvedNumuxConfig): Map<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",