numux 2.11.0 → 2.13.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
@@ -89,13 +89,6 @@ numux exec api -- npx prisma migrate
89
89
  numux exec web npm run build
90
90
  ```
91
91
 
92
- `logs` prints the log directory path, or a specific process's log contents:
93
-
94
- ```sh
95
- numux logs # Print log directory path
96
- numux logs api # Print the api process log
97
- ```
98
-
99
92
  Set up completions for your shell:
100
93
 
101
94
  ```sh
@@ -227,8 +220,8 @@ export default defineConfig({
227
220
  | `-e,` `--env-file` `<path|false>` | Env file path, or "false" to disable env file loading |
228
221
  | `--config` `<path>` | Config file path (default: auto-detect) |
229
222
  | `-p,` `--prefix` | Prefixed output mode (no TUI, for CI/scripts) |
230
- | `--only` `<a,b,...>` | Only run these processes (+ their dependencies) |
231
- | `--exclude` `<a,b,...>` | Exclude these processes |
223
+ | `-o,` `--only` `<a,b,...>` | Only run these processes (+ their dependencies) |
224
+ | `-x,` `--exclude` `<a,b,...>` | Exclude these processes |
232
225
  | `--kill-others` | Kill all processes when any exits (regardless of exit code) |
233
226
  | `--kill-others-on-fail` | Kill all processes when any exits with non-zero code |
234
227
  | `--max-restarts` `<n>` | Max auto-restarts for crashed processes |
@@ -250,6 +243,20 @@ numux --prefix
250
243
 
251
244
  Auto-exits when all processes finish. Exit code 1 if any process failed.
252
245
 
246
+ ## Logging
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.
249
+
250
+ <!-- generated:logging-usage -->
251
+ ```sh
252
+ numux logs # Print log directory path
253
+ numux logs api # Pipe the api process log to stdout
254
+ numux logs api | grep "ERROR" # Search process logs
255
+ numux logs api | tail -f # Follow process log output
256
+ ```
257
+ <!-- /generated:logging-usage -->
258
+
259
+
253
260
  ## Config reference
254
261
 
255
262
  ### Global options
package/dist/man/numux.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "NUMUX" "1" "April 2026" "2.11.0" "numux manual"
1
+ .TH "NUMUX" "1" "April 2026" "2.13.0" "numux manual"
2
2
  .SH "NAME"
3
3
  \fBnumux\fR
4
4
  .P
@@ -97,14 +97,6 @@ numux exec web npm run build
97
97
  .fi
98
98
  .RE
99
99
  .P
100
- \fBlogs\fP prints the log directory path, or a specific process's log contents:
101
- .RS 2
102
- .nf
103
- numux logs # Print log directory path
104
- numux logs api # Print the api process log
105
- .fi
106
- .RE
107
- .P
108
100
  Set up completions for your shell:
109
101
  .RS 2
110
102
  .nf
@@ -309,13 +301,13 @@ Prefixed output mode (no TUI, for CI/scripts)
309
301
  T}
310
302
  _
311
303
  T{
312
- \fB\-\-only\fP \fB<a,b,\.\.\.>\fP
304
+ \fB\-o,\fP \fB\-\-only\fP \fB<a,b,\.\.\.>\fP
313
305
  T}|T{
314
306
  Only run these processes (+ their dependencies)
315
307
  T}
316
308
  _
317
309
  T{
318
- \fB\-\-exclude\fP \fB<a,b,\.\.\.>\fP
310
+ \fB\-x,\fP \fB\-\-exclude\fP \fB<a,b,\.\.\.>\fP
319
311
  T}|T{
320
312
  Exclude these processes
321
313
  T}
@@ -386,6 +378,21 @@ numux \-\-prefix
386
378
  .RE
387
379
  .P
388
380
  Auto\-exits when all processes finish\. Exit code 1 if any process failed\.
381
+ .SH Logging
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\.
384
+ <!\-\- generated:logging\-usage \-\->
385
+ .RS 2
386
+ .nf
387
+ numux logs # Print log directory path
388
+ numux logs api # Pipe the api process log to stdout
389
+ numux logs api | grep "ERROR" # Search process logs
390
+ numux logs api | tail \-f # Follow process log output
391
+ .fi
392
+ .RE
393
+ <!\-\- /generated:logging\-usage \-\->
394
+
395
+
389
396
  .SH Config reference
390
397
  .SS Global options
391
398
  .P
package/dist/numux.js CHANGED
@@ -127,13 +127,6 @@ numux exec api -- npx prisma migrate
127
127
  numux exec web npm run build
128
128
  \`\`\`
129
129
 
130
- \`logs\` prints the log directory path, or a specific process's log contents:
131
-
132
- \`\`\`sh
133
- numux logs # Print log directory path
134
- numux logs api # Print the api process log
135
- \`\`\`
136
-
137
130
  Set up completions for your shell:
138
131
 
139
132
  \`\`\`sh
@@ -245,8 +238,8 @@ export default defineConfig({
245
238
  | \`-e,\` \`--env-file\` \`<path|false>\` | Env file path, or "false" to disable env file loading |
246
239
  | \`--config\` \`<path>\` | Config file path (default: auto-detect) |
247
240
  | \`-p,\` \`--prefix\` | Prefixed output mode (no TUI, for CI/scripts) |
248
- | \`--only\` \`<a,b,...>\` | Only run these processes (+ their dependencies) |
249
- | \`--exclude\` \`<a,b,...>\` | Exclude these processes |
241
+ | \`-o,\` \`--only\` \`<a,b,...>\` | Only run these processes (+ their dependencies) |
242
+ | \`-x,\` \`--exclude\` \`<a,b,...>\` | Exclude these processes |
250
243
  | \`--kill-others\` | Kill all processes when any exits (regardless of exit code) |
251
244
  | \`--kill-others-on-fail\` | Kill all processes when any exits with non-zero code |
252
245
  | \`--max-restarts\` \`<n>\` | Max auto-restarts for crashed processes |
@@ -264,6 +257,16 @@ numux --prefix
264
257
  \`\`\`
265
258
 
266
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.
261
+
262
+ <!-- generated:logging-usage -->
263
+ \`\`\`sh
264
+ numux logs # Print log directory path
265
+ numux logs api # Pipe the api process log to stdout
266
+ numux logs api | grep "ERROR" # Search process logs
267
+ numux logs api | tail -f # Follow process log output
268
+ \`\`\`
269
+ <!-- /generated:logging-usage -->` },
267
270
  "global-options": { title: "Global options", body: `Top-level options apply to all processes (process-level settings override):
268
271
 
269
272
  <!-- generated:config-global -->
@@ -538,7 +541,7 @@ var init_help = __esm(() => {
538
541
  var require_package = __commonJS((exports, module) => {
539
542
  module.exports = {
540
543
  name: "numux",
541
- version: "2.11.0",
544
+ version: "2.13.0",
542
545
  description: "Terminal multiplexer with dependency orchestration",
543
546
  type: "module",
544
547
  license: "MIT",
@@ -800,6 +803,7 @@ var FLAGS = [
800
803
  {
801
804
  type: "value",
802
805
  long: "--only",
806
+ short: "-o",
803
807
  key: "only",
804
808
  description: "Only run these processes (+ their dependencies)",
805
809
  valueName: "<a,b,...>",
@@ -809,6 +813,7 @@ var FLAGS = [
809
813
  {
810
814
  type: "value",
811
815
  long: "--exclude",
816
+ short: "-x",
812
817
  key: "exclude",
813
818
  description: "Exclude these processes",
814
819
  valueName: "<a,b,...>",
@@ -925,6 +930,14 @@ var SUBCOMMANDS = [
925
930
  name: "logs",
926
931
  description: "Open the log directory or a specific process log",
927
932
  usage: "logs [name]",
933
+ examples: [
934
+ ["numux logs", "Print log directory path"],
935
+ ["numux logs api", "Pipe the api process log to stdout"],
936
+ ['numux logs api | grep "ERROR"', "Search process logs"],
937
+ ["numux logs api | tail -f", "Follow process log output"]
938
+ ],
939
+ completionArgs: "dynamic",
940
+ completionScript: "numux logs 2>/dev/null",
928
941
  parse: (args, i, result) => {
929
942
  result.logs = true;
930
943
  const next = args[i + 1];
@@ -939,6 +952,7 @@ var SUBCOMMANDS = [
939
952
  name: "completions",
940
953
  description: "Generate shell completions (bash, zsh, fish)",
941
954
  usage: "completions <shell>",
955
+ completionArgs: ["bash", "zsh", "fish"],
942
956
  parse: (args, i, result) => {
943
957
  const next = args[++i];
944
958
  if (next === undefined)
@@ -951,6 +965,7 @@ var SUBCOMMANDS = [
951
965
  name: "help",
952
966
  description: "Show help for a topic",
953
967
  usage: "help [topic]",
968
+ completionArgs: "dynamic",
954
969
  parse: (args, i, result) => {
955
970
  result.help = true;
956
971
  const next = args[i + 1];
@@ -1048,7 +1063,10 @@ function parseArgs(argv) {
1048
1063
  const value = flag.parse ? flag.parse(next, arg) : next;
1049
1064
  const current = result[flag.key];
1050
1065
  if (Array.isArray(current)) {
1051
- current.push(value);
1066
+ if (Array.isArray(value))
1067
+ current.push(...value);
1068
+ else
1069
+ current.push(value);
1052
1070
  } else {
1053
1071
  result[flag.key] = value;
1054
1072
  }
@@ -1151,6 +1169,13 @@ function filterConfig(config, only, exclude) {
1151
1169
  init_help_topics();
1152
1170
  var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
1153
1171
  var HELP_TOPIC_NAMES = [...Object.keys(HELP_TOPICS), ...Object.keys(TOPIC_ALIASES)];
1172
+ function resolveArgs(sub) {
1173
+ if (!sub.completionArgs)
1174
+ return;
1175
+ if (sub.completionArgs === "dynamic" && sub.name === "help")
1176
+ return HELP_TOPIC_NAMES;
1177
+ return sub.completionArgs;
1178
+ }
1154
1179
  function generateCompletions(shell) {
1155
1180
  switch (shell) {
1156
1181
  case "bash":
@@ -1188,12 +1213,30 @@ function bashCompletions() {
1188
1213
  return ;;`);
1189
1214
  }
1190
1215
  }
1191
- caseEntries.push(` completions)
1192
- COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
1216
+ for (const sub of SUBCOMMANDS) {
1217
+ const args = resolveArgs(sub);
1218
+ if (!args)
1219
+ continue;
1220
+ if (args === "dynamic") {
1221
+ const script = sub.completionScript;
1222
+ if (!script)
1223
+ continue;
1224
+ const dir = `$(${script})`;
1225
+ caseEntries.push(` ${sub.name})
1226
+ local logdir
1227
+ logdir="${dir}"
1228
+ if [ -n "$logdir" ] && [ -d "$logdir" ]; then
1229
+ local names
1230
+ names="$(ls "$logdir"/*.log 2>/dev/null | xargs -I{} basename {} .log)"
1231
+ COMPREPLY=( $(compgen -W "$names" -- "$cur") )
1232
+ fi
1193
1233
  return ;;`);
1194
- caseEntries.push(` help)
1195
- COMPREPLY=( $(compgen -W "${HELP_TOPIC_NAMES.join(" ")}" -- "$cur") )
1234
+ } else {
1235
+ caseEntries.push(` ${sub.name})
1236
+ COMPREPLY=( $(compgen -W "${args.join(" ")}" -- "$cur") )
1196
1237
  return ;;`);
1238
+ }
1239
+ }
1197
1240
  const allFlags = FLAGS.flatMap((f) => f.short ? [f.short, f.long] : [f.long]);
1198
1241
  const subcmds = SUBCOMMANDS.map((s) => s.name);
1199
1242
  return `# numux bash completions
@@ -1253,6 +1296,31 @@ function zshCompletions() {
1253
1296
  }
1254
1297
  const argsBlock = argLines.map((l) => `${l} \\`).join(`
1255
1298
  `);
1299
+ const subcmdCases = [];
1300
+ for (const sub of SUBCOMMANDS) {
1301
+ const args = resolveArgs(sub);
1302
+ if (!args)
1303
+ continue;
1304
+ if (args === "dynamic") {
1305
+ const script = sub.completionScript;
1306
+ if (!script)
1307
+ continue;
1308
+ subcmdCases.push(` ${sub.name})
1309
+ local logdir names
1310
+ logdir="\\$(${script})"
1311
+ if [[ -n "\\$logdir" ]] && [[ -d "\\$logdir" ]]; then
1312
+ names=( \\$(ls "\\$logdir"/*.log 2>/dev/null | xargs -I{} basename {} .log) )
1313
+ _describe 'process' names
1314
+ fi
1315
+ ;;`);
1316
+ } else {
1317
+ subcmdCases.push(` ${sub.name})
1318
+ local -a args
1319
+ args=(${args.map((a) => `'${a}'`).join(" ")})
1320
+ _describe '${sub.name}' args
1321
+ ;;`);
1322
+ }
1323
+ }
1256
1324
  return `#compdef numux
1257
1325
  # numux zsh completions
1258
1326
  # Add to ~/.zshrc: eval "$(numux completions zsh)"
@@ -1275,14 +1343,8 @@ ${argsBlock}
1275
1343
  esac
1276
1344
 
1277
1345
  case "\${words[2]}" in
1278
- help)
1279
- local -a topics
1280
- topics=(${HELP_TOPIC_NAMES.map((t) => `'${t}'`).join(" ")})
1281
- _describe 'topic' topics
1282
- ;;
1283
- completions)
1284
- _values 'shell' bash zsh fish
1285
- ;;
1346
+ ${subcmdCases.join(`
1347
+ `)}
1286
1348
  esac
1287
1349
  }
1288
1350
  _numux`;
@@ -1299,7 +1361,22 @@ function fishCompletions() {
1299
1361
  for (const s of SUBCOMMANDS) {
1300
1362
  lines.push(`complete -c numux -n __fish_use_subcommand -a ${s.name} -d '${sq(s.description)}'`);
1301
1363
  }
1302
- lines.push("", "# Completions subcommand", "complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'", "", "# Help subcommand", `complete -c numux -n '__fish_seen_subcommand_from help' -a '${HELP_TOPIC_NAMES.join(" ")}'`, "", "# Options");
1364
+ for (const sub of SUBCOMMANDS) {
1365
+ const args = resolveArgs(sub);
1366
+ if (!args)
1367
+ continue;
1368
+ lines.push("");
1369
+ lines.push(`# ${sub.name} subcommand`);
1370
+ if (args === "dynamic") {
1371
+ const script = sub.completionScript;
1372
+ if (!script)
1373
+ continue;
1374
+ lines.push(`complete -c numux -n '__fish_seen_subcommand_from ${sub.name}' -a '(set -l d (${script}); and ls $d/*.log 2>/dev/null | xargs -I{} basename {} .log)'`);
1375
+ } else {
1376
+ lines.push(`complete -c numux -n '__fish_seen_subcommand_from ${sub.name}' -a '${args.join(" ")}'`);
1377
+ }
1378
+ }
1379
+ lines.push("", "# Options");
1303
1380
  for (const f of FLAGS) {
1304
1381
  const parts = ["complete -c numux"];
1305
1382
  if (f.short)
@@ -4258,7 +4335,7 @@ class App {
4258
4335
  }
4259
4336
  if (name === SHORTCUTS.clear.key) {
4260
4337
  this.panes.get(this.activePane)?.clear();
4261
- this.logWriter.truncate(this.activePane);
4338
+ this.logWriter.markCopyStart(this.activePane);
4262
4339
  return;
4263
4340
  }
4264
4341
  if (name === SHORTCUTS.timestamps.key) {
@@ -4389,10 +4466,7 @@ class App {
4389
4466
  copyAllText() {
4390
4467
  if (!this.activePane)
4391
4468
  return;
4392
- const pane = this.panes.get(this.activePane);
4393
- if (!pane)
4394
- return;
4395
- const text = pane.getText();
4469
+ const text = this.logWriter.readLog(this.activePane);
4396
4470
  if (!text) {
4397
4471
  this.statusBar.showTemporaryMessage("No output to copy");
4398
4472
  return;
@@ -4647,13 +4721,14 @@ ${DIM}Done in ${elapsed}${RESET}
4647
4721
  }
4648
4722
 
4649
4723
  // src/utils/log-writer.ts
4650
- import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
4724
+ import { closeSync, mkdirSync as mkdirSync2, openSync, readSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
4651
4725
  import { tmpdir } from "os";
4652
4726
  import { basename as basename2, join } from "path";
4653
4727
  class LogWriter {
4654
4728
  dir;
4655
4729
  isTemp;
4656
4730
  files = new Map;
4731
+ copyOffsets = new Map;
4657
4732
  decoder = new TextDecoder;
4658
4733
  encoder = new TextEncoder;
4659
4734
  constructor(dir, isTemp = false) {
@@ -4709,6 +4784,35 @@ class LogWriter {
4709
4784
  `);
4710
4785
  }
4711
4786
  };
4787
+ markCopyStart(name) {
4788
+ const path = this.getLogPath(name);
4789
+ if (!path)
4790
+ return;
4791
+ try {
4792
+ this.copyOffsets.set(name, Bun.file(path).size);
4793
+ } catch {}
4794
+ }
4795
+ readLog(name) {
4796
+ const path = this.getLogPath(name);
4797
+ if (!path)
4798
+ return;
4799
+ try {
4800
+ const size = Bun.file(path).size;
4801
+ const offset = this.copyOffsets.get(name) ?? 0;
4802
+ if (size <= offset)
4803
+ return;
4804
+ const fd = openSync(path, "r");
4805
+ try {
4806
+ const buf = Buffer.alloc(size - offset);
4807
+ readSync(fd, buf, 0, buf.length, offset);
4808
+ return buf.toString("utf-8");
4809
+ } finally {
4810
+ closeSync(fd);
4811
+ }
4812
+ } catch {
4813
+ return;
4814
+ }
4815
+ }
4712
4816
  getLogPath(name) {
4713
4817
  if (this.files.has(name)) {
4714
4818
  return join(this.dir, `${name}.log`);
@@ -4812,17 +4916,6 @@ class LogWriter {
4812
4916
  return [];
4813
4917
  }
4814
4918
  }
4815
- truncate(name) {
4816
- const fd = this.files.get(name);
4817
- if (fd === undefined)
4818
- return;
4819
- try {
4820
- closeSync(fd);
4821
- const path = join(this.dir, `${name}.log`);
4822
- const newFd = openSync(path, "w");
4823
- this.files.set(name, newFd);
4824
- } catch {}
4825
- }
4826
4919
  close() {
4827
4920
  for (const fd of this.files.values()) {
4828
4921
  closeSync(fd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "2.11.0",
3
+ "version": "2.13.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",