numux 2.11.0 → 2.12.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
@@ -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.12.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
@@ -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
@@ -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.12.0",
542
545
  description: "Terminal multiplexer with dependency orchestration",
543
546
  type: "module",
544
547
  license: "MIT",
@@ -925,6 +928,14 @@ var SUBCOMMANDS = [
925
928
  name: "logs",
926
929
  description: "Open the log directory or a specific process log",
927
930
  usage: "logs [name]",
931
+ examples: [
932
+ ["numux logs", "Print log directory path"],
933
+ ["numux logs api", "Pipe the api process log to stdout"],
934
+ ['numux logs api | grep "ERROR"', "Search process logs"],
935
+ ["numux logs api | tail -f", "Follow process log output"]
936
+ ],
937
+ completionArgs: "dynamic",
938
+ completionScript: "numux logs 2>/dev/null",
928
939
  parse: (args, i, result) => {
929
940
  result.logs = true;
930
941
  const next = args[i + 1];
@@ -939,6 +950,7 @@ var SUBCOMMANDS = [
939
950
  name: "completions",
940
951
  description: "Generate shell completions (bash, zsh, fish)",
941
952
  usage: "completions <shell>",
953
+ completionArgs: ["bash", "zsh", "fish"],
942
954
  parse: (args, i, result) => {
943
955
  const next = args[++i];
944
956
  if (next === undefined)
@@ -951,6 +963,7 @@ var SUBCOMMANDS = [
951
963
  name: "help",
952
964
  description: "Show help for a topic",
953
965
  usage: "help [topic]",
966
+ completionArgs: "dynamic",
954
967
  parse: (args, i, result) => {
955
968
  result.help = true;
956
969
  const next = args[i + 1];
@@ -1151,6 +1164,13 @@ function filterConfig(config, only, exclude) {
1151
1164
  init_help_topics();
1152
1165
  var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
1153
1166
  var HELP_TOPIC_NAMES = [...Object.keys(HELP_TOPICS), ...Object.keys(TOPIC_ALIASES)];
1167
+ function resolveArgs(sub) {
1168
+ if (!sub.completionArgs)
1169
+ return;
1170
+ if (sub.completionArgs === "dynamic" && sub.name === "help")
1171
+ return HELP_TOPIC_NAMES;
1172
+ return sub.completionArgs;
1173
+ }
1154
1174
  function generateCompletions(shell) {
1155
1175
  switch (shell) {
1156
1176
  case "bash":
@@ -1188,12 +1208,30 @@ function bashCompletions() {
1188
1208
  return ;;`);
1189
1209
  }
1190
1210
  }
1191
- caseEntries.push(` completions)
1192
- COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
1211
+ for (const sub of SUBCOMMANDS) {
1212
+ const args = resolveArgs(sub);
1213
+ if (!args)
1214
+ continue;
1215
+ if (args === "dynamic") {
1216
+ const script = sub.completionScript;
1217
+ if (!script)
1218
+ continue;
1219
+ const dir = `$(${script})`;
1220
+ caseEntries.push(` ${sub.name})
1221
+ local logdir
1222
+ logdir="${dir}"
1223
+ if [ -n "$logdir" ] && [ -d "$logdir" ]; then
1224
+ local names
1225
+ names="$(ls "$logdir"/*.log 2>/dev/null | xargs -I{} basename {} .log)"
1226
+ COMPREPLY=( $(compgen -W "$names" -- "$cur") )
1227
+ fi
1193
1228
  return ;;`);
1194
- caseEntries.push(` help)
1195
- COMPREPLY=( $(compgen -W "${HELP_TOPIC_NAMES.join(" ")}" -- "$cur") )
1229
+ } else {
1230
+ caseEntries.push(` ${sub.name})
1231
+ COMPREPLY=( $(compgen -W "${args.join(" ")}" -- "$cur") )
1196
1232
  return ;;`);
1233
+ }
1234
+ }
1197
1235
  const allFlags = FLAGS.flatMap((f) => f.short ? [f.short, f.long] : [f.long]);
1198
1236
  const subcmds = SUBCOMMANDS.map((s) => s.name);
1199
1237
  return `# numux bash completions
@@ -1253,6 +1291,31 @@ function zshCompletions() {
1253
1291
  }
1254
1292
  const argsBlock = argLines.map((l) => `${l} \\`).join(`
1255
1293
  `);
1294
+ const subcmdCases = [];
1295
+ for (const sub of SUBCOMMANDS) {
1296
+ const args = resolveArgs(sub);
1297
+ if (!args)
1298
+ continue;
1299
+ if (args === "dynamic") {
1300
+ const script = sub.completionScript;
1301
+ if (!script)
1302
+ continue;
1303
+ subcmdCases.push(` ${sub.name})
1304
+ local logdir names
1305
+ logdir="\\$(${script})"
1306
+ if [[ -n "\\$logdir" ]] && [[ -d "\\$logdir" ]]; then
1307
+ names=( \\$(ls "\\$logdir"/*.log 2>/dev/null | xargs -I{} basename {} .log) )
1308
+ _describe 'process' names
1309
+ fi
1310
+ ;;`);
1311
+ } else {
1312
+ subcmdCases.push(` ${sub.name})
1313
+ local -a args
1314
+ args=(${args.map((a) => `'${a}'`).join(" ")})
1315
+ _describe '${sub.name}' args
1316
+ ;;`);
1317
+ }
1318
+ }
1256
1319
  return `#compdef numux
1257
1320
  # numux zsh completions
1258
1321
  # Add to ~/.zshrc: eval "$(numux completions zsh)"
@@ -1275,14 +1338,8 @@ ${argsBlock}
1275
1338
  esac
1276
1339
 
1277
1340
  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
- ;;
1341
+ ${subcmdCases.join(`
1342
+ `)}
1286
1343
  esac
1287
1344
  }
1288
1345
  _numux`;
@@ -1299,7 +1356,22 @@ function fishCompletions() {
1299
1356
  for (const s of SUBCOMMANDS) {
1300
1357
  lines.push(`complete -c numux -n __fish_use_subcommand -a ${s.name} -d '${sq(s.description)}'`);
1301
1358
  }
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");
1359
+ for (const sub of SUBCOMMANDS) {
1360
+ const args = resolveArgs(sub);
1361
+ if (!args)
1362
+ continue;
1363
+ lines.push("");
1364
+ lines.push(`# ${sub.name} subcommand`);
1365
+ if (args === "dynamic") {
1366
+ const script = sub.completionScript;
1367
+ if (!script)
1368
+ continue;
1369
+ 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)'`);
1370
+ } else {
1371
+ lines.push(`complete -c numux -n '__fish_seen_subcommand_from ${sub.name}' -a '${args.join(" ")}'`);
1372
+ }
1373
+ }
1374
+ lines.push("", "# Options");
1303
1375
  for (const f of FLAGS) {
1304
1376
  const parts = ["complete -c numux"];
1305
1377
  if (f.short)
@@ -4258,7 +4330,7 @@ class App {
4258
4330
  }
4259
4331
  if (name === SHORTCUTS.clear.key) {
4260
4332
  this.panes.get(this.activePane)?.clear();
4261
- this.logWriter.truncate(this.activePane);
4333
+ this.logWriter.markCopyStart(this.activePane);
4262
4334
  return;
4263
4335
  }
4264
4336
  if (name === SHORTCUTS.timestamps.key) {
@@ -4389,10 +4461,7 @@ class App {
4389
4461
  copyAllText() {
4390
4462
  if (!this.activePane)
4391
4463
  return;
4392
- const pane = this.panes.get(this.activePane);
4393
- if (!pane)
4394
- return;
4395
- const text = pane.getText();
4464
+ const text = this.logWriter.readLog(this.activePane);
4396
4465
  if (!text) {
4397
4466
  this.statusBar.showTemporaryMessage("No output to copy");
4398
4467
  return;
@@ -4647,13 +4716,14 @@ ${DIM}Done in ${elapsed}${RESET}
4647
4716
  }
4648
4717
 
4649
4718
  // src/utils/log-writer.ts
4650
- import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
4719
+ import { closeSync, mkdirSync as mkdirSync2, openSync, readSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
4651
4720
  import { tmpdir } from "os";
4652
4721
  import { basename as basename2, join } from "path";
4653
4722
  class LogWriter {
4654
4723
  dir;
4655
4724
  isTemp;
4656
4725
  files = new Map;
4726
+ copyOffsets = new Map;
4657
4727
  decoder = new TextDecoder;
4658
4728
  encoder = new TextEncoder;
4659
4729
  constructor(dir, isTemp = false) {
@@ -4709,6 +4779,35 @@ class LogWriter {
4709
4779
  `);
4710
4780
  }
4711
4781
  };
4782
+ markCopyStart(name) {
4783
+ const path = this.getLogPath(name);
4784
+ if (!path)
4785
+ return;
4786
+ try {
4787
+ this.copyOffsets.set(name, Bun.file(path).size);
4788
+ } catch {}
4789
+ }
4790
+ readLog(name) {
4791
+ const path = this.getLogPath(name);
4792
+ if (!path)
4793
+ return;
4794
+ try {
4795
+ const size = Bun.file(path).size;
4796
+ const offset = this.copyOffsets.get(name) ?? 0;
4797
+ if (size <= offset)
4798
+ return;
4799
+ const fd = openSync(path, "r");
4800
+ try {
4801
+ const buf = Buffer.alloc(size - offset);
4802
+ readSync(fd, buf, 0, buf.length, offset);
4803
+ return buf.toString("utf-8");
4804
+ } finally {
4805
+ closeSync(fd);
4806
+ }
4807
+ } catch {
4808
+ return;
4809
+ }
4810
+ }
4712
4811
  getLogPath(name) {
4713
4812
  if (this.files.has(name)) {
4714
4813
  return join(this.dir, `${name}.log`);
@@ -4812,17 +4911,6 @@ class LogWriter {
4812
4911
  return [];
4813
4912
  }
4814
4913
  }
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
4914
  close() {
4827
4915
  for (const fd of this.files.values()) {
4828
4916
  closeSync(fd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",