numux 1.21.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/README.md CHANGED
@@ -164,6 +164,7 @@ Template properties (color, env, dependsOn, etc.) are inherited by all matched p
164
164
  | `--exclude <a,b,...>` | Exclude these processes |
165
165
  | `--kill-others` | Kill all processes when any exits |
166
166
  | `--no-restart` | Disable auto-restart for crashed processes |
167
+ | `-s, --sort <mode>` | Tab display order: `config` (default), `alphabetical`, `topological` |
167
168
  | `--no-watch` | Disable file watching even if config has `watch` patterns |
168
169
  | `-t, --timestamps` | Add `[HH:MM:SS]` timestamps to prefixed output |
169
170
  | `--log-dir <path>` | Write per-process output to `<path>/<name>.log` |
@@ -198,6 +199,7 @@ Top-level options apply to all processes (process-level settings override):
198
199
  | `stopSignal` | `'SIGTERM' \| 'SIGINT' \| 'SIGHUP'` | Stop signal for all processes (default: `'SIGTERM'`) |
199
200
  | `errorMatcher` | `boolean \| string` | Error detection for all processes (`true` = ANSI red, string = regex) |
200
201
  | `watch` | `string \| string[]` | Watch patterns for all processes (process `watch` replaces if set) |
202
+ | `sort` | `'config' \| 'alphabetical' \| 'topological'` | Tab display order (default: `'config'` — definition order) |
201
203
 
202
204
  ```ts
203
205
  export default defineConfig({
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.21.0",
39
+ version: "1.23.0",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -95,6 +95,15 @@ import { resolve as resolve8 } from "path";
95
95
  // src/cli-flags.ts
96
96
  var commaSplit = (raw) => raw.split(",").map((s) => s.trim()).filter(Boolean);
97
97
  var FLAGS = [
98
+ {
99
+ type: "value",
100
+ long: "--sort",
101
+ short: "-s",
102
+ key: "sort",
103
+ description: "Tab display order",
104
+ valueName: "<config|alphabetical|topological>",
105
+ completionHint: "none"
106
+ },
98
107
  {
99
108
  type: "value",
100
109
  long: "--workspace",
@@ -906,12 +915,12 @@ function findCycle(remaining, config) {
906
915
  // src/utils/color.ts
907
916
  var BASIC_COLORS = {
908
917
  black: "#000000",
909
- red: "#ff0000",
910
- green: "#00ff00",
911
- yellow: "#ffff00",
912
- blue: "#0000ff",
913
- magenta: "#ff00ff",
914
- cyan: "#00ffff",
918
+ red: "#ff5555",
919
+ green: "#00cc00",
920
+ yellow: "#cccc00",
921
+ blue: "#0000cc",
922
+ magenta: "#cc00cc",
923
+ cyan: "#00cccc",
915
924
  white: "#ffffff",
916
925
  gray: "#808080",
917
926
  grey: "#808080",
@@ -940,35 +949,35 @@ function hexToAnsi(hex) {
940
949
  }
941
950
  var HEX_COLOR_RE = /^#?[0-9a-fA-F]{6}$/;
942
951
  var STATUS_ANSI = {
943
- ready: "\x1B[32m",
944
- running: "\x1B[36m",
945
- finished: "\x1B[32m",
946
- failed: "\x1B[31m",
947
- stopped: "\x1B[90m",
948
- 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)
949
958
  };
950
959
  var ANSI_RESET = "\x1B[0m";
951
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;
952
961
  function stripAnsi(str) {
953
962
  return str.replace(ANSI_RE, "");
954
963
  }
955
- var DEFAULT_ANSI_COLORS = [
956
- "\x1B[36m",
957
- "\x1B[33m",
958
- "\x1B[35m",
959
- "\x1B[34m",
960
- "\x1B[32m",
961
- "\x1B[91m",
962
- "\x1B[93m",
963
- "\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
964
973
  ];
965
- var DEFAULT_HEX_COLORS = ["#00cccc", "#cccc00", "#cc00cc", "#0000cc", "#00cc00", "#ff5555", "#ffff55", "#ff55ff"];
974
+ var DEFAULT_ANSI_COLORS = DEFAULT_PALETTE.map(hexToAnsi);
966
975
  function colorFromName(name) {
967
976
  let hash = 0;
968
977
  for (let i = 0;i < name.length; i++) {
969
978
  hash = (hash << 5) - hash + name.charCodeAt(i) | 0;
970
979
  }
971
- return DEFAULT_HEX_COLORS[Math.abs(hash) % DEFAULT_HEX_COLORS.length];
980
+ return DEFAULT_PALETTE[Math.abs(hash) % DEFAULT_PALETTE.length];
972
981
  }
973
982
  function resolveColor(color) {
974
983
  if (typeof color === "string")
@@ -1009,9 +1018,9 @@ function buildProcessHexColorMap(names, config) {
1009
1018
  if (hex)
1010
1019
  map.set(name, hex);
1011
1020
  else
1012
- map.set(name, DEFAULT_HEX_COLORS[paletteIndex++ % DEFAULT_HEX_COLORS.length]);
1021
+ map.set(name, DEFAULT_PALETTE[paletteIndex++ % DEFAULT_PALETTE.length]);
1013
1022
  } else {
1014
- map.set(name, DEFAULT_HEX_COLORS[paletteIndex % DEFAULT_HEX_COLORS.length]);
1023
+ map.set(name, DEFAULT_PALETTE[paletteIndex % DEFAULT_PALETTE.length]);
1015
1024
  paletteIndex++;
1016
1025
  }
1017
1026
  }
@@ -1049,6 +1058,7 @@ function validateConfig(raw, warnings) {
1049
1058
  }
1050
1059
  globalEnv = config.env;
1051
1060
  }
1061
+ const sort = validateSort(config.sort);
1052
1062
  const validated = {};
1053
1063
  for (const name of names) {
1054
1064
  let proc = processes[name];
@@ -1146,7 +1156,7 @@ function validateConfig(raw, warnings) {
1146
1156
  showCommand
1147
1157
  };
1148
1158
  }
1149
- return { processes: validated };
1159
+ return { ...sort ? { sort } : {}, processes: validated };
1150
1160
  }
1151
1161
  function validateStringOrStringArray(value) {
1152
1162
  if (typeof value === "string")
@@ -1176,6 +1186,16 @@ function validateErrorMatcher(name, value) {
1176
1186
  }
1177
1187
  return;
1178
1188
  }
1189
+ var VALID_SORT_VALUES = new Set(["config", "alphabetical", "topological"]);
1190
+ function validateSort(value) {
1191
+ if (typeof value === "string") {
1192
+ if (!VALID_SORT_VALUES.has(value)) {
1193
+ throw new Error(`sort must be one of: ${[...VALID_SORT_VALUES].join(", ")}. Got "${value}"`);
1194
+ }
1195
+ return value;
1196
+ }
1197
+ return;
1198
+ }
1179
1199
  var VALID_PLATFORMS = new Set(["aix", "darwin", "freebsd", "linux", "openbsd", "sunos", "win32"]);
1180
1200
  function validatePlatform(name, value) {
1181
1201
  const arr = validateStringOrStringArray(value);
@@ -1743,7 +1763,14 @@ class ProcessManager {
1743
1763
  return [...this.states.values()];
1744
1764
  }
1745
1765
  getProcessNames() {
1746
- return this.tiers.flat();
1766
+ switch (this.config.sort) {
1767
+ case "alphabetical":
1768
+ return Object.keys(this.config.processes).sort();
1769
+ case "topological":
1770
+ return this.tiers.flat();
1771
+ default:
1772
+ return Object.keys(this.config.processes);
1773
+ }
1747
1774
  }
1748
1775
  async startAll(cols, rows) {
1749
1776
  log("Starting all processes");
@@ -2153,8 +2180,6 @@ class Pane {
2153
2180
  _onScroll = null;
2154
2181
  _onCopy = null;
2155
2182
  _onLinkClick = null;
2156
- _textLines = null;
2157
- _textLinesLower = null;
2158
2183
  constructor(renderer, name, cols, rows, interactive = false) {
2159
2184
  this.scrollBox = new ScrollBoxRenderable(renderer, {
2160
2185
  id: `pane-${name}`,
@@ -2201,14 +2226,10 @@ class Pane {
2201
2226
  feed(data) {
2202
2227
  const text = this.decoder.decode(data, { stream: true });
2203
2228
  this.terminal.feed(text);
2204
- this._textLines = null;
2205
- this._textLinesLower = null;
2206
2229
  }
2207
2230
  resize(cols, rows) {
2208
2231
  this.terminal.cols = cols;
2209
2232
  this.terminal.rows = rows;
2210
- this._textLines = null;
2211
- this._textLinesLower = null;
2212
2233
  }
2213
2234
  get isAtBottom() {
2214
2235
  const { scrollTop, scrollHeight, viewport } = this.scrollBox;
@@ -2238,16 +2259,13 @@ class Pane {
2238
2259
  this._onLinkClick = handler;
2239
2260
  }
2240
2261
  getLinkAtMouse(localX, localY) {
2241
- if (!this._textLines) {
2242
- const text = this.terminal.getText();
2243
- this._textLines = text.split(`
2262
+ const text = this.terminal.getText();
2263
+ const lines = text.split(`
2244
2264
  `);
2245
- this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
2246
- }
2247
2265
  const lineIndex = Math.floor(this.scrollBox.scrollTop) + localY;
2248
- if (lineIndex < 0 || lineIndex >= this._textLines.length)
2266
+ if (lineIndex < 0 || lineIndex >= lines.length)
2249
2267
  return null;
2250
- return findLinkAtPosition(this._textLines[lineIndex], localX);
2268
+ return findLinkAtPosition(lines[lineIndex], localX);
2251
2269
  }
2252
2270
  show() {
2253
2271
  this.scrollBox.visible = true;
@@ -2255,30 +2273,6 @@ class Pane {
2255
2273
  hide() {
2256
2274
  this.scrollBox.visible = false;
2257
2275
  }
2258
- search(query) {
2259
- if (!query)
2260
- return [];
2261
- if (!this._textLines) {
2262
- const text = this.terminal.getText();
2263
- this._textLines = text.split(`
2264
- `);
2265
- this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
2266
- }
2267
- const lines = this._textLinesLower;
2268
- const matches = [];
2269
- const lowerQuery = query.toLowerCase();
2270
- for (let line = 0;line < lines.length; line++) {
2271
- let pos = 0;
2272
- while (true) {
2273
- const idx = lines[line].indexOf(lowerQuery, pos);
2274
- if (idx === -1)
2275
- break;
2276
- matches.push({ line, start: idx, end: idx + query.length });
2277
- pos = idx + 1;
2278
- }
2279
- }
2280
- return matches;
2281
- }
2282
2276
  setHighlights(matches, currentIndex) {
2283
2277
  const firstVisible = Math.max(0, Math.floor(this.scrollBox.scrollTop) - 2);
2284
2278
  const lastVisible = Math.ceil(this.scrollBox.scrollTop + this.scrollBox.viewport.height) + 2;
@@ -2307,8 +2301,6 @@ class Pane {
2307
2301
  }
2308
2302
  clear() {
2309
2303
  this.terminal.reset();
2310
- this._textLines = null;
2311
- this._textLinesLower = null;
2312
2304
  }
2313
2305
  destroy() {
2314
2306
  this.terminal.destroy();
@@ -2637,6 +2629,7 @@ class App {
2637
2629
  termRows = 24;
2638
2630
  sidebarWidth = 20;
2639
2631
  config;
2632
+ logWriter;
2640
2633
  resizeTimer = null;
2641
2634
  searchTimer = null;
2642
2635
  searchMode = false;
@@ -2645,9 +2638,10 @@ class App {
2645
2638
  searchIndex = -1;
2646
2639
  inputWaitTimers = new Map;
2647
2640
  awaitingInput = new Set;
2648
- constructor(manager, config) {
2641
+ constructor(manager, config, logWriter) {
2649
2642
  this.manager = manager;
2650
2643
  this.config = config;
2644
+ this.logWriter = logWriter;
2651
2645
  this.names = manager.getProcessNames();
2652
2646
  }
2653
2647
  async start() {
@@ -2805,6 +2799,7 @@ class App {
2805
2799
  }
2806
2800
  if (name === SHORTCUTS.clear.key) {
2807
2801
  this.panes.get(this.activePane)?.clear();
2802
+ this.logWriter.truncate(this.activePane);
2808
2803
  return;
2809
2804
  }
2810
2805
  const num = Number.parseInt(name, 10);
@@ -2977,14 +2972,16 @@ class App {
2977
2972
  this.runSearch();
2978
2973
  }, 100);
2979
2974
  }
2980
- runSearch() {
2975
+ async runSearch() {
2981
2976
  if (!this.activePane)
2982
2977
  return;
2983
- const pane = this.panes.get(this.activePane);
2984
- 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)
2985
2982
  return;
2986
- this.searchMatches = pane.search(this.searchQuery);
2987
- this.searchIndex = this.searchMatches.length > 0 ? 0 : -1;
2983
+ this.searchMatches = matches;
2984
+ this.searchIndex = matches.length > 0 ? 0 : -1;
2988
2985
  this.updateSearchHighlights();
2989
2986
  if (this.searchIndex >= 0) {
2990
2987
  this.scrollToCurrentMatch();
@@ -3152,7 +3149,7 @@ class PrefixDisplay {
3152
3149
  const allDone = states.every((s) => s.status === "stopped" || s.status === "finished" || s.status === "failed" || s.status === "skipped");
3153
3150
  if (allDone) {
3154
3151
  this.printSummary();
3155
- this.logWriter?.close();
3152
+ this.logWriter?.cleanup();
3156
3153
  const anyFailed = states.some((s) => s.status === "failed");
3157
3154
  process.exit(anyFailed ? 1 : 0);
3158
3155
  }
@@ -3168,7 +3165,7 @@ class PrefixDisplay {
3168
3165
  this.flushBuffer(name);
3169
3166
  }
3170
3167
  this.printSummary();
3171
- this.logWriter?.close();
3168
+ this.logWriter?.cleanup();
3172
3169
  process.exit(code === 0 ? 0 : 1);
3173
3170
  });
3174
3171
  }
@@ -3220,24 +3217,31 @@ ${DIM}Done in ${elapsed}${RESET}
3220
3217
  for (const name of this.manager.getProcessNames()) {
3221
3218
  this.flushBuffer(name);
3222
3219
  }
3223
- this.logWriter?.close();
3220
+ this.logWriter?.cleanup();
3224
3221
  const anyFailed = this.manager.getAllStates().some((s) => s.status === "failed");
3225
3222
  process.exit(anyFailed ? 1 : 0);
3226
3223
  }
3227
3224
  }
3228
3225
 
3229
3226
  // src/utils/log-writer.ts
3230
- 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";
3231
3229
  import { join } from "path";
3232
3230
  class LogWriter {
3233
3231
  dir;
3232
+ isTemp;
3234
3233
  files = new Map;
3235
3234
  decoder = new TextDecoder;
3236
3235
  encoder = new TextEncoder;
3237
- constructor(dir) {
3236
+ constructor(dir, isTemp = false) {
3238
3237
  this.dir = dir;
3238
+ this.isTemp = isTemp;
3239
3239
  mkdirSync2(dir, { recursive: true });
3240
3240
  }
3241
+ static createTemp() {
3242
+ const dir = join(tmpdir(), `numux-${process.pid}`);
3243
+ return new LogWriter(dir, true);
3244
+ }
3241
3245
  errored = false;
3242
3246
  handleEvent = (event) => {
3243
3247
  if (event.type !== "output" || this.errored)
@@ -3258,12 +3262,81 @@ class LogWriter {
3258
3262
  `);
3259
3263
  }
3260
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
+ }
3261
3326
  close() {
3262
3327
  for (const fd of this.files.values()) {
3263
3328
  closeSync(fd);
3264
3329
  }
3265
3330
  this.files.clear();
3266
3331
  }
3332
+ cleanup() {
3333
+ this.close();
3334
+ if (this.isTemp) {
3335
+ try {
3336
+ rmSync(this.dir, { recursive: true });
3337
+ } catch {}
3338
+ }
3339
+ }
3267
3340
  }
3268
3341
 
3269
3342
  // src/utils/shutdown.ts
@@ -3275,7 +3348,7 @@ function setupShutdownHandlers(app, logWriter) {
3275
3348
  }
3276
3349
  shuttingDown = true;
3277
3350
  app.shutdown().finally(() => {
3278
- logWriter?.close();
3351
+ logWriter?.cleanup();
3279
3352
  process.exit(app.hasFailures() ? 1 : 0);
3280
3353
  });
3281
3354
  };
@@ -3286,7 +3359,7 @@ function setupShutdownHandlers(app, logWriter) {
3286
3359
  process.stderr.write(`numux: unexpected error: ${err?.stack ?? err}
3287
3360
  `);
3288
3361
  app.shutdown().finally(() => {
3289
- logWriter?.close();
3362
+ logWriter?.cleanup();
3290
3363
  process.exit(1);
3291
3364
  });
3292
3365
  });
@@ -3296,7 +3369,7 @@ function setupShutdownHandlers(app, logWriter) {
3296
3369
  process.stderr.write(`numux: unhandled rejection: ${message}
3297
3370
  `);
3298
3371
  app.shutdown().finally(() => {
3299
- logWriter?.close();
3372
+ logWriter?.cleanup();
3300
3373
  process.exit(1);
3301
3374
  });
3302
3375
  });
@@ -3479,6 +3552,9 @@ async function main() {
3479
3552
  }
3480
3553
  }
3481
3554
  }
3555
+ if (parsed.sort) {
3556
+ config.sort = parsed.sort;
3557
+ }
3482
3558
  if (parsed.envFile !== undefined) {
3483
3559
  for (const proc of Object.values(config.processes)) {
3484
3560
  proc.envFile = parsed.envFile;
@@ -3500,10 +3576,7 @@ async function main() {
3500
3576
  }
3501
3577
  }
3502
3578
  const manager = new ProcessManager(config);
3503
- let logWriter;
3504
- if (parsed.logDir) {
3505
- logWriter = new LogWriter(parsed.logDir);
3506
- }
3579
+ const logWriter = parsed.logDir ? new LogWriter(parsed.logDir) : LogWriter.createTemp();
3507
3580
  printWarnings(warnings);
3508
3581
  if (parsed.prefix) {
3509
3582
  if (!parsed.noRestart) {
@@ -3518,10 +3591,8 @@ async function main() {
3518
3591
  });
3519
3592
  await display.start();
3520
3593
  } else {
3521
- if (logWriter) {
3522
- manager.on(logWriter.handleEvent);
3523
- }
3524
- const app = new App(manager, config);
3594
+ manager.on(logWriter.handleEvent);
3595
+ const app = new App(manager, config, logWriter);
3525
3596
  setupShutdownHandlers(app, logWriter);
3526
3597
  await app.start();
3527
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
  /**
@@ -88,14 +89,22 @@ export interface NumuxConfig<K extends string = string> {
88
89
  errorMatcher?: boolean | string;
89
90
  /** Global watch patterns, inherited by processes without their own watch */
90
91
  watch?: string | string[];
92
+ /**
93
+ * Tab display order. `'config'` preserves definition order (package.json script order for wildcards),
94
+ * `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers.
95
+ * @default 'config'
96
+ */
97
+ sort?: SortOrder;
91
98
  processes: Record<K, NumuxProcessConfig<K> | NumuxScriptPattern<K> | string>;
92
99
  }
100
+ export type SortOrder = 'config' | 'alphabetical' | 'topological';
93
101
  /** Process config after validation — dependsOn is always normalized to an array */
94
102
  export interface ResolvedProcessConfig extends Omit<NumuxProcessConfig, 'dependsOn'> {
95
103
  dependsOn?: string[];
96
104
  }
97
105
  /** Validated config with all shorthand expanded to full objects */
98
106
  export interface ResolvedNumuxConfig {
107
+ sort?: SortOrder;
99
108
  processes: Record<string, ResolvedProcessConfig>;
100
109
  }
101
110
  export type ProcessStatus = 'pending' | 'starting' | 'ready' | 'running' | 'stopping' | 'stopped' | 'finished' | 'failed' | 'skipped';
@@ -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.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",