numux 2.15.0 → 2.16.1

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
@@ -212,7 +212,7 @@ export default defineConfig({
212
212
  <!-- generated:options -->
213
213
  | Flag | Description |
214
214
  |------|-------------|
215
- | `-s,` `--sort` `<config|alphabetical|topological>` | Tab display order |
215
+ | `-s,` `--sort` `<config|alphabetical|topological|status>` | Tab display order |
216
216
  | `-w,` `--workspace` `<script>` | Run a package.json script across all workspaces |
217
217
  | `-n,` `--name` `<name=command>` | Add a named process |
218
218
  | `-c,` `--color` `<colors>` | Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple) |
@@ -245,7 +245,9 @@ Auto-exits when all processes finish. Exit code 1 if any process failed.
245
245
 
246
246
  ## Logging
247
247
 
248
- numux writes per-process log files (ANSI-stripped) when `--log-dir` is set or `logDir` is configured. Each session creates a timestamped subdirectory with a `latest` symlink pointing to the most recent run.
248
+ numux writes per-process log files (ANSI-stripped) for every run. By default they go to `<tmpdir>/numux/<project-name>/`, or to `--log-dir` / the `logDir` config when set. Each session creates a timestamped subdirectory with a `latest` symlink pointing to the most recent run.
249
+
250
+ The TUI prints `Logs saved to: <session-dir>` on exit (Ctrl+C or signal). Multiple numux instances in the same project share the default base dir, so each session has its own timestamped folder but the `latest` symlink will point at whichever started most recently. If `numux logs` falls back to the default and a `latest` symlink is in use, it prints a warning. To avoid the collision, set `logDir` per config or pass `--log-dir`.
249
251
 
250
252
  <!-- generated:logging-usage -->
251
253
  ```sh
@@ -275,7 +277,7 @@ Top-level options apply to all processes (process-level settings override):
275
277
  | `stopSignal` | `'SIGTERM' \| 'SIGINT' \| 'SIGHUP'` | Global stop signal, inherited by all processes |
276
278
  | `errorMatcher` | `boolean \| string` | Global error matcher, inherited by all processes. `true` = detect ANSI red output, string = regex |
277
279
  | `watch` | `string \| string[]` | Global watch patterns, inherited by processes without their own watch |
278
- | `sort` | `'config' \| 'alphabetical' \| 'topological'` | Tab display order. `'config'` preserves definition order (package.json script order for wildcards), `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers. |
280
+ | `sort` | `'config' \| 'alphabetical' \| 'topological' \| 'status'` | Tab display order. `'config'` preserves definition order (package.json script order for wildcards), `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers, `'status'` uses config order but moves finished/stopped/failed/skipped tabs to the bottom. |
279
281
  | `prefix` | `boolean` | Use prefixed output mode instead of TUI (for CI/scripts) |
280
282
  | `timestamps` | `boolean \| string` | Add timestamps to output lines. `true` uses default `HH:mm:ss.SSS` format, or pass a format string (e.g. `"HH:mm:ss"`) |
281
283
  | `killOthers` | `boolean` | Kill all processes when any one exits (regardless of exit code) |
package/dist/man/numux.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "NUMUX" "1" "April 2026" "2.15.0" "numux manual"
1
+ .TH "NUMUX" "1" "May 2026" "2.16.1" "numux manual"
2
2
  .SH "NAME"
3
3
  \fBnumux\fR
4
4
  .P
@@ -380,7 +380,9 @@ numux \-\-prefix
380
380
  Auto\-exits when all processes finish\. Exit code 1 if any process failed\.
381
381
  .SH Logging
382
382
  .P
383
- numux writes per\-process log files (ANSI\-stripped) when \fB\-\-log\-dir\fP is set or \fBlogDir\fP is configured\. Each session creates a timestamped subdirectory with a \fBlatest\fP symlink pointing to the most recent run\.
383
+ numux writes per\-process log files (ANSI\-stripped) for every run\. By default they go to \fB<tmpdir>/numux/<project\-name>/\fP, or to \fB\-\-log\-dir\fP / the \fBlogDir\fP config when set\. Each session creates a timestamped subdirectory with a \fBlatest\fP symlink pointing to the most recent run\.
384
+ .P
385
+ The TUI prints \fBLogs saved to: <session\-dir>\fP on exit (Ctrl+C or signal)\. Multiple numux instances in the same project share the default base dir, so each session has its own timestamped folder but the \fBlatest\fP symlink will point at whichever started most recently\. If \fBnumux logs\fP falls back to the default and a \fBlatest\fP symlink is in use, it prints a warning\. To avoid the collision, set \fBlogDir\fP per config or pass \fB\-\-log\-dir\fP\|\.
384
386
  <!\-\- generated:logging\-usage \-\->
385
387
  .RS 2
386
388
  .nf
@@ -484,9 +486,9 @@ _
484
486
  T{
485
487
  \fBsort\fP
486
488
  T}|T{
487
- \fB&#39;config&#39; | &#39;alphabetical&#39; | &#39;topological&#39;\fP
489
+ \fB&#39;config&#39; | &#39;alphabetical&#39; | &#39;topological&#39; | &#39;status&#39;\fP
488
490
  T}|T{
489
- Tab display order\. \fB&#39;config&#39;\fP preserves definition order (package\.json script order for wildcards), \fB&#39;alphabetical&#39;\fP sorts by process name, \fB&#39;topological&#39;\fP sorts by dependency tiers\.
491
+ Tab display order\. \fB&#39;config&#39;\fP preserves definition order (package\.json script order for wildcards), \fB&#39;alphabetical&#39;\fP sorts by process name, \fB&#39;topological&#39;\fP sorts by dependency tiers, \fB&#39;status&#39;\fP uses config order but moves finished/stopped/failed/skipped tabs to the bottom\.
490
492
  T}
491
493
  _
492
494
  T{
package/dist/numux.js CHANGED
@@ -230,7 +230,7 @@ export default defineConfig({
230
230
  options: { title: "Options", body: `<!-- generated:options -->
231
231
  | Flag | Description |
232
232
  |------|-------------|
233
- | \`-s,\` \`--sort\` \`<config|alphabetical|topological>\` | Tab display order |
233
+ | \`-s,\` \`--sort\` \`<config|alphabetical|topological|status>\` | Tab display order |
234
234
  | \`-w,\` \`--workspace\` \`<script>\` | Run a package.json script across all workspaces |
235
235
  | \`-n,\` \`--name\` \`<name=command>\` | Add a named process |
236
236
  | \`-c,\` \`--color\` \`<colors>\` | Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple) |
@@ -257,7 +257,9 @@ numux --prefix
257
257
  \`\`\`
258
258
 
259
259
  Auto-exits when all processes finish. Exit code 1 if any process failed.` },
260
- logging: { title: "Logging", body: `numux writes per-process log files (ANSI-stripped) when \`--log-dir\` is set or \`logDir\` is configured. Each session creates a timestamped subdirectory with a \`latest\` symlink pointing to the most recent run.
260
+ logging: { title: "Logging", body: `numux writes per-process log files (ANSI-stripped) for every run. By default they go to \`<tmpdir>/numux/<project-name>/\`, or to \`--log-dir\` / the \`logDir\` config when set. Each session creates a timestamped subdirectory with a \`latest\` symlink pointing to the most recent run.
261
+
262
+ The TUI prints \`Logs saved to: <session-dir>\` on exit (Ctrl+C or signal). Multiple numux instances in the same project share the default base dir, so each session has its own timestamped folder but the \`latest\` symlink will point at whichever started most recently. If \`numux logs\` falls back to the default and a \`latest\` symlink is in use, it prints a warning. To avoid the collision, set \`logDir\` per config or pass \`--log-dir\`.
261
263
 
262
264
  <!-- generated:logging-usage -->
263
265
  \`\`\`sh
@@ -281,7 +283,7 @@ numux logs api | tail -f # Follow process log output
281
283
  | \`stopSignal\` | \`'SIGTERM' \\| 'SIGINT' \\| 'SIGHUP'\` | Global stop signal, inherited by all processes |
282
284
  | \`errorMatcher\` | \`boolean \\| string\` | Global error matcher, inherited by all processes. \`true\` = detect ANSI red output, string = regex |
283
285
  | \`watch\` | \`string \\| string[]\` | Global watch patterns, inherited by processes without their own watch |
284
- | \`sort\` | \`'config' \\| 'alphabetical' \\| 'topological'\` | Tab display order. \`'config'\` preserves definition order (package.json script order for wildcards), \`'alphabetical'\` sorts by process name, \`'topological'\` sorts by dependency tiers. |
286
+ | \`sort\` | \`'config' \\| 'alphabetical' \\| 'topological' \\| 'status'\` | Tab display order. \`'config'\` preserves definition order (package.json script order for wildcards), \`'alphabetical'\` sorts by process name, \`'topological'\` sorts by dependency tiers, \`'status'\` uses config order but moves finished/stopped/failed/skipped tabs to the bottom. |
285
287
  | \`prefix\` | \`boolean\` | Use prefixed output mode instead of TUI (for CI/scripts) |
286
288
  | \`timestamps\` | \`boolean \\| string\` | Add timestamps to output lines. \`true\` uses default \`HH:mm:ss.SSS\` format, or pass a format string (e.g. \`"HH:mm:ss"\`) |
287
289
  | \`killOthers\` | \`boolean\` | Kill all processes when any one exits (regardless of exit code) |
@@ -546,7 +548,7 @@ var init_help = __esm(() => {
546
548
  var require_package = __commonJS((exports, module) => {
547
549
  module.exports = {
548
550
  name: "numux",
549
- version: "2.15.0",
551
+ version: "2.16.1",
550
552
  description: "Terminal multiplexer with dependency orchestration",
551
553
  type: "module",
552
554
  license: "MIT",
@@ -736,7 +738,7 @@ var FLAGS = [
736
738
  short: "-s",
737
739
  key: "sort",
738
740
  description: "Tab display order",
739
- valueName: "<config|alphabetical|topological>",
741
+ valueName: "<config|alphabetical|topological|status>",
740
742
  completionHint: "none"
741
743
  },
742
744
  {
@@ -1910,7 +1912,7 @@ function validateErrorMatcher(name, value) {
1910
1912
  }
1911
1913
  return;
1912
1914
  }
1913
- var VALID_SORT_VALUES = new Set(["config", "alphabetical", "topological"]);
1915
+ var VALID_SORT_VALUES = new Set(["config", "alphabetical", "topological", "status"]);
1914
1916
  function validateSort(value) {
1915
1917
  if (typeof value === "string") {
1916
1918
  if (!VALID_SORT_VALUES.has(value)) {
@@ -2900,6 +2902,53 @@ function evaluateCondition(condition) {
2900
2902
  // src/ui/app.ts
2901
2903
  import { BoxRenderable as BoxRenderable3, createCliRenderer } from "@opentui/core";
2902
2904
 
2905
+ // src/utils/shutdown.ts
2906
+ var finalized = false;
2907
+ function finalizeShutdown(logWriter, exitCode) {
2908
+ if (finalized)
2909
+ process.exit(exitCode);
2910
+ finalized = true;
2911
+ if (logWriter && !logWriter.isTemporary) {
2912
+ process.stderr.write(`Logs saved to: ${logWriter.getDirectory()}
2913
+ `);
2914
+ }
2915
+ logWriter?.cleanup();
2916
+ process.exit(exitCode);
2917
+ }
2918
+ function setupShutdownHandlers(app, logWriter) {
2919
+ let shuttingDown = false;
2920
+ const shutdown = () => {
2921
+ if (shuttingDown) {
2922
+ process.exit(1);
2923
+ }
2924
+ shuttingDown = true;
2925
+ app.shutdown().finally(() => {
2926
+ finalizeShutdown(logWriter, app.hasFailures() ? 1 : 0);
2927
+ });
2928
+ };
2929
+ process.on("SIGINT", shutdown);
2930
+ process.on("SIGTERM", shutdown);
2931
+ process.on("uncaughtException", (err) => {
2932
+ log("Uncaught exception:", err?.message ?? err);
2933
+ app.shutdown().finally(() => {
2934
+ process.stderr.write(`numux: unexpected error: ${err?.stack ?? err}
2935
+ `);
2936
+ logWriter?.cleanup();
2937
+ process.exit(1);
2938
+ });
2939
+ });
2940
+ process.on("unhandledRejection", (reason) => {
2941
+ const stack = reason instanceof Error ? reason.stack : String(reason);
2942
+ log("Unhandled rejection:", stack);
2943
+ app.shutdown().finally(() => {
2944
+ process.stderr.write(`numux: unhandled rejection: ${stack}
2945
+ `);
2946
+ logWriter?.cleanup();
2947
+ process.exit(1);
2948
+ });
2949
+ });
2950
+ }
2951
+
2903
2952
  // src/ui/help-overlay.ts
2904
2953
  import { BoxRenderable, TextRenderable } from "@opentui/core";
2905
2954
 
@@ -4053,6 +4102,11 @@ var STATUS_ICON_HEX = {
4053
4102
  skipped: "#888888"
4054
4103
  };
4055
4104
  var TERMINAL_STATUSES = new Set(["finished", "stopped", "failed", "skipped"]);
4105
+ function getDisplayOrder(originalNames, statuses) {
4106
+ const active = originalNames.filter((n) => !TERMINAL_STATUSES.has(statuses.get(n)));
4107
+ const terminal = originalNames.filter((n) => TERMINAL_STATUSES.has(statuses.get(n)));
4108
+ return [...active, ...terminal];
4109
+ }
4056
4110
  function formatTab(name, status) {
4057
4111
  return `${STATUS_ICONS[status]} ${name}`;
4058
4112
  }
@@ -4066,11 +4120,6 @@ function formatDescription(status, exitCode, restartCount) {
4066
4120
  }
4067
4121
  return desc;
4068
4122
  }
4069
- function getDisplayOrder(originalNames, statuses) {
4070
- const active = originalNames.filter((n) => !TERMINAL_STATUSES.has(statuses.get(n)));
4071
- const terminal = originalNames.filter((n) => TERMINAL_STATUSES.has(statuses.get(n)));
4072
- return [...active, ...terminal];
4073
- }
4074
4123
  function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses, searchMatchProcesses) {
4075
4124
  return names.map((name) => {
4076
4125
  const status = statuses.get(name);
@@ -4160,12 +4209,14 @@ class TabBar {
4160
4209
  statuses;
4161
4210
  baseDescriptions;
4162
4211
  processColors;
4212
+ reorderByStatus;
4163
4213
  inputWaiting = new Set;
4164
4214
  erroredProcesses = new Set;
4165
4215
  searchMatchCounts = new Map;
4166
- constructor(renderer, names, colors) {
4216
+ constructor(renderer, names, colors, reorderByStatus = false) {
4167
4217
  this.originalNames = names;
4168
4218
  this.names = [...names];
4219
+ this.reorderByStatus = reorderByStatus;
4169
4220
  this.statuses = new Map(names.map((n) => [n, "pending"]));
4170
4221
  this.baseDescriptions = new Map(names.map((n) => [n, "pending"]));
4171
4222
  this.processColors = colors ?? new Map;
@@ -4235,17 +4286,19 @@ class TabBar {
4235
4286
  return this.names.length;
4236
4287
  }
4237
4288
  refreshOptions() {
4238
- const currentIdx = this.renderable.getSelectedIndex();
4239
- const currentName = this.names[currentIdx];
4240
- this.names = getDisplayOrder(this.originalNames, this.statuses);
4289
+ if (this.reorderByStatus) {
4290
+ const currentIdx = this.renderable.getSelectedIndex();
4291
+ const currentName = this.names[currentIdx];
4292
+ this.names = getDisplayOrder(this.originalNames, this.statuses);
4293
+ const newIdx = this.names.indexOf(currentName);
4294
+ if (newIdx >= 0 && newIdx !== currentIdx) {
4295
+ this.renderable.setSelectedIndex(newIdx);
4296
+ }
4297
+ }
4241
4298
  this.renderable.options = this.names.map((n) => ({
4242
4299
  name: formatTab(n, this.statuses.get(n)),
4243
4300
  description: this.getDescription(n)
4244
4301
  }));
4245
- const newIdx = this.names.indexOf(currentName);
4246
- if (newIdx >= 0 && newIdx !== currentIdx) {
4247
- this.renderable.setSelectedIndex(newIdx);
4248
- }
4249
4302
  this.updateOptionColors();
4250
4303
  }
4251
4304
  getDescription(name) {
@@ -4274,9 +4327,6 @@ class TabBar {
4274
4327
  setSelectedIndex(index) {
4275
4328
  this.renderable.setSelectedIndex(index);
4276
4329
  }
4277
- focus() {
4278
- this.renderable.focus();
4279
- }
4280
4330
  }
4281
4331
 
4282
4332
  // src/ui/app.ts
@@ -4326,7 +4376,7 @@ class App {
4326
4376
  border: false
4327
4377
  });
4328
4378
  const processHexColors = buildProcessHexColorMap(this.names, this.config);
4329
- this.tabBar = new TabBar(this.renderer, this.names, processHexColors);
4379
+ this.tabBar = new TabBar(this.renderer, this.names, processHexColors, this.config.sort === "status");
4330
4380
  const contentRow = new BoxRenderable3(this.renderer, {
4331
4381
  id: "content-row",
4332
4382
  flexDirection: "row",
@@ -4433,7 +4483,7 @@ class App {
4433
4483
  return;
4434
4484
  }
4435
4485
  this.shutdown().then(() => {
4436
- process.exit(this.hasFailures() ? 1 : 0);
4486
+ finalizeShutdown(this.logWriter, this.hasFailures() ? 1 : 0);
4437
4487
  });
4438
4488
  return;
4439
4489
  }
@@ -4569,7 +4619,6 @@ class App {
4569
4619
  });
4570
4620
  if (this.names.length > 0) {
4571
4621
  this.switchPane(this.names[0]);
4572
- this.tabBar.focus();
4573
4622
  }
4574
4623
  await this.manager.startAll(termCols, termRows);
4575
4624
  }
@@ -5155,46 +5204,6 @@ function defaultLogDir(cwd) {
5155
5204
  return join2(tmpdir2(), "numux", resolveProjectName(cwd));
5156
5205
  }
5157
5206
 
5158
- // src/utils/shutdown.ts
5159
- function setupShutdownHandlers(app, logWriter) {
5160
- let shuttingDown = false;
5161
- const shutdown = () => {
5162
- if (shuttingDown) {
5163
- process.exit(1);
5164
- }
5165
- shuttingDown = true;
5166
- app.shutdown().finally(() => {
5167
- if (logWriter && !logWriter.isTemporary) {
5168
- process.stderr.write(`Logs saved to: ${logWriter.getDirectory()}
5169
- `);
5170
- }
5171
- logWriter?.cleanup();
5172
- process.exit(app.hasFailures() ? 1 : 0);
5173
- });
5174
- };
5175
- process.on("SIGINT", shutdown);
5176
- process.on("SIGTERM", shutdown);
5177
- process.on("uncaughtException", (err) => {
5178
- log("Uncaught exception:", err?.message ?? err);
5179
- app.shutdown().finally(() => {
5180
- process.stderr.write(`numux: unexpected error: ${err?.stack ?? err}
5181
- `);
5182
- logWriter?.cleanup();
5183
- process.exit(1);
5184
- });
5185
- });
5186
- process.on("unhandledRejection", (reason) => {
5187
- const stack = reason instanceof Error ? reason.stack : String(reason);
5188
- log("Unhandled rejection:", stack);
5189
- app.shutdown().finally(() => {
5190
- process.stderr.write(`numux: unhandled rejection: ${stack}
5191
- `);
5192
- logWriter?.cleanup();
5193
- process.exit(1);
5194
- });
5195
- });
5196
- }
5197
-
5198
5207
  // src/index.ts
5199
5208
  var HELP = generateHelp();
5200
5209
  var INIT_TEMPLATE = `import { defineConfig } from 'numux'
@@ -5250,9 +5259,14 @@ async function main() {
5250
5259
  process.exit(0);
5251
5260
  }
5252
5261
  if (parsed.logs) {
5253
- const logDir2 = parsed.logDir ?? await resolveLogDir(parsed.configPath);
5262
+ const resolved = parsed.logDir ? { dir: parsed.logDir, explicit: true } : await resolveLogDir(parsed.configPath);
5263
+ const logDir2 = resolved.dir;
5254
5264
  const latestDir = resolve8(logDir2, "latest");
5255
- const target = existsSync6(latestDir) ? latestDir : logDir2;
5265
+ const usingLatest = existsSync6(latestDir);
5266
+ const target = usingLatest ? latestDir : logDir2;
5267
+ if (!resolved.explicit && usingLatest) {
5268
+ console.warn('Warning: using default log directory; "latest" may have been overwritten by another numux instance in this project.');
5269
+ }
5256
5270
  if (parsed.logsProcess) {
5257
5271
  const logFile2 = resolve8(target, `${parsed.logsProcess}.log`);
5258
5272
  if (!existsSync6(logFile2)) {
@@ -5451,10 +5465,10 @@ async function resolveLogDir(configPath) {
5451
5465
  try {
5452
5466
  const raw = await loadConfig(configPath);
5453
5467
  if (typeof raw.logDir === "string" && raw.logDir.trim()) {
5454
- return resolve8(raw.logDir.trim());
5468
+ return { dir: resolve8(raw.logDir.trim()), explicit: true };
5455
5469
  }
5456
5470
  } catch {}
5457
- return defaultLogDir(process.cwd());
5471
+ return { dir: defaultLogDir(process.cwd()), explicit: false };
5458
5472
  }
5459
5473
  main().catch((err) => {
5460
5474
  console.error(err instanceof Error ? err.message : err);
package/dist/types.d.ts CHANGED
@@ -93,7 +93,8 @@ export interface NumuxConfig<K extends string = string> {
93
93
  watch?: string | string[];
94
94
  /**
95
95
  * Tab display order. `'config'` preserves definition order (package.json script order for wildcards),
96
- * `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers.
96
+ * `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers,
97
+ * `'status'` uses config order but moves finished/stopped/failed/skipped tabs to the bottom.
97
98
  * @default 'config'
98
99
  */
99
100
  sort?: SortOrder;
@@ -123,7 +124,7 @@ export interface NumuxConfig<K extends string = string> {
123
124
  logDir?: string;
124
125
  processes: Record<K, NumuxProcessConfig<K> | NumuxScriptPattern<K> | string | true>;
125
126
  }
126
- export type SortOrder = 'config' | 'alphabetical' | 'topological';
127
+ export type SortOrder = 'config' | 'alphabetical' | 'topological' | 'status';
127
128
  /** Process config after validation — dependsOn is always normalized to an array */
128
129
  export interface ResolvedProcessConfig extends Omit<NumuxProcessConfig, 'dependsOn' | 'workspaces'> {
129
130
  dependsOn?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",