numux 1.9.0 → 1.10.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.
Files changed (3) hide show
  1. package/README.md +46 -0
  2. package/dist/numux.js +522 -203
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -98,6 +98,22 @@ numux completions fish | source
98
98
  numux completions fish > ~/.config/fish/completions/numux.fish
99
99
  ```
100
100
 
101
+ ### Workspaces
102
+
103
+ Run a `package.json` script across all workspaces in a monorepo:
104
+
105
+ ```sh
106
+ numux -w dev
107
+ ```
108
+
109
+ Reads the `workspaces` field from your root `package.json`, finds which workspaces have the given script, and spawns `<pm> run <script>` in each. The package manager is auto-detected from `packageManager` field or lockfiles.
110
+
111
+ Composes with other flags:
112
+
113
+ ```sh
114
+ numux -w dev -n redis="redis-server" --colors
115
+ ```
116
+
101
117
  ### Ad-hoc commands
102
118
 
103
119
  ```sh
@@ -108,10 +124,39 @@ numux "bun dev:api" "bun dev:web"
108
124
  numux -n api="bun dev:api" -n web="bun dev:web"
109
125
  ```
110
126
 
127
+ ### Script patterns
128
+
129
+ Run multiple package.json scripts matching a glob pattern:
130
+
131
+ ```sh
132
+ numux 'dev:*' # all scripts matching dev:*
133
+ numux 'npm:*:dev' # explicit npm: prefix (same behavior)
134
+ ```
135
+
136
+ Extra arguments after the pattern are forwarded to each matched command:
137
+
138
+ ```sh
139
+ numux 'lint:* --fix' # → bun run lint:js --fix, bun run lint:ts --fix
140
+ ```
141
+
142
+ In a config file, use the pattern as the process name:
143
+
144
+ ```ts
145
+ export default defineConfig({
146
+ processes: {
147
+ 'dev:*': { color: ['green', 'cyan'] },
148
+ 'lint:* --fix': {},
149
+ },
150
+ })
151
+ ```
152
+
153
+ Template properties (color, env, dependsOn, etc.) are inherited by all matched processes. Colors given as an array are distributed round-robin.
154
+
111
155
  ### Options
112
156
 
113
157
  | Flag | Description |
114
158
  |------|-------------|
159
+ | `-w, --workspace <script>` | Run a script across all workspaces |
115
160
  | `-c, --config <path>` | Explicit config file path |
116
161
  | `-n, --name <name=cmd>` | Add a named process (repeatable) |
117
162
  | `-p, --prefix` | Prefixed output mode (no TUI, for CI/scripts) |
@@ -181,6 +226,7 @@ Each process accepts:
181
226
  | `color` | `string \| string[]` | auto | Hex (e.g. `"#ff6600"`) or basic name: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple |
182
227
  | `watch` | `string \| string[]` | — | Glob patterns — restart process when matching files change |
183
228
  | `interactive` | `boolean` | `false` | When `true`, keyboard input is forwarded to the process |
229
+ | `errorMatcher` | `boolean \| string` | — | `true` detects ANSI red output, string = regex pattern — shows error indicator on tab |
184
230
 
185
231
  ### File watching
186
232
 
package/dist/numux.js CHANGED
@@ -22,7 +22,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
22
22
  var require_package = __commonJS((exports, module) => {
23
23
  module.exports = {
24
24
  name: "numux",
25
- version: "1.9.0",
25
+ version: "1.10.1",
26
26
  description: "Terminal multiplexer with dependency orchestration",
27
27
  type: "module",
28
28
  license: "MIT",
@@ -75,10 +75,230 @@ var require_package = __commonJS((exports, module) => {
75
75
  });
76
76
 
77
77
  // src/index.ts
78
- import { existsSync as existsSync4, writeFileSync } from "fs";
79
- import { resolve as resolve7 } from "path";
78
+ import { existsSync as existsSync5, writeFileSync } from "fs";
79
+ import { resolve as resolve8 } from "path";
80
+
81
+ // src/cli-flags.ts
82
+ var commaSplit = (raw) => raw.split(",").map((s) => s.trim()).filter(Boolean);
83
+ var FLAGS = [
84
+ {
85
+ type: "value",
86
+ long: "--workspace",
87
+ short: "-w",
88
+ key: "workspace",
89
+ description: "Run a package.json script across all workspaces",
90
+ valueName: "<script>",
91
+ completionHint: "none"
92
+ },
93
+ {
94
+ type: "value",
95
+ long: "--name",
96
+ short: "-n",
97
+ key: "named",
98
+ description: "Add a named process",
99
+ valueName: "<name=command>",
100
+ completionHint: "none",
101
+ parse(raw) {
102
+ const eq = raw.indexOf("=");
103
+ if (eq < 1) {
104
+ throw new Error(`Invalid --name value: expected "name=command", got "${raw}"`);
105
+ }
106
+ return { name: raw.slice(0, eq), command: raw.slice(eq + 1) };
107
+ }
108
+ },
109
+ {
110
+ type: "value",
111
+ long: "--color",
112
+ short: "-c",
113
+ key: "colors",
114
+ description: "Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)",
115
+ valueName: "<colors>",
116
+ completionHint: "none",
117
+ parse: commaSplit
118
+ },
119
+ {
120
+ type: "boolean",
121
+ long: "--colors",
122
+ key: "autoColors",
123
+ description: "Auto-assign colors to processes based on their name"
124
+ },
125
+ {
126
+ type: "value",
127
+ long: "--config",
128
+ key: "configPath",
129
+ description: "Config file path (default: auto-detect)",
130
+ valueName: "<path>",
131
+ completionHint: "file"
132
+ },
133
+ {
134
+ type: "boolean",
135
+ long: "--prefix",
136
+ short: "-p",
137
+ key: "prefix",
138
+ description: "Prefixed output mode (no TUI, for CI/scripts)"
139
+ },
140
+ {
141
+ type: "value",
142
+ long: "--only",
143
+ key: "only",
144
+ description: "Only run these processes (+ their dependencies)",
145
+ valueName: "<a,b,...>",
146
+ completionHint: "none",
147
+ parse: commaSplit
148
+ },
149
+ {
150
+ type: "value",
151
+ long: "--exclude",
152
+ key: "exclude",
153
+ description: "Exclude these processes",
154
+ valueName: "<a,b,...>",
155
+ completionHint: "none",
156
+ parse: commaSplit
157
+ },
158
+ {
159
+ type: "boolean",
160
+ long: "--kill-others",
161
+ key: "killOthers",
162
+ description: "Kill all processes when any exits"
163
+ },
164
+ {
165
+ type: "boolean",
166
+ long: "--no-restart",
167
+ key: "noRestart",
168
+ description: "Disable auto-restart for crashed processes"
169
+ },
170
+ {
171
+ type: "boolean",
172
+ long: "--no-watch",
173
+ key: "noWatch",
174
+ description: "Disable file watching even if config has watch patterns"
175
+ },
176
+ {
177
+ type: "boolean",
178
+ long: "--timestamps",
179
+ short: "-t",
180
+ key: "timestamps",
181
+ description: "Add timestamps to prefixed output lines"
182
+ },
183
+ {
184
+ type: "value",
185
+ long: "--log-dir",
186
+ key: "logDir",
187
+ description: "Write per-process logs to directory",
188
+ valueName: "<path>",
189
+ completionHint: "directory"
190
+ },
191
+ {
192
+ type: "boolean",
193
+ long: "--debug",
194
+ key: "debug",
195
+ description: "Enable debug logging to .numux/debug.log"
196
+ },
197
+ {
198
+ type: "boolean",
199
+ long: "--help",
200
+ short: "-h",
201
+ key: "help",
202
+ description: "Show this help"
203
+ },
204
+ {
205
+ type: "boolean",
206
+ long: "--version",
207
+ short: "-v",
208
+ key: "version",
209
+ description: "Show version"
210
+ }
211
+ ];
212
+ var SUBCOMMANDS = [
213
+ {
214
+ name: "init",
215
+ description: "Create a starter config file",
216
+ parse: (_args, i, result) => {
217
+ result.init = true;
218
+ return i;
219
+ }
220
+ },
221
+ {
222
+ name: "validate",
223
+ description: "Validate config and show process graph",
224
+ parse: (_args, i, result) => {
225
+ result.validate = true;
226
+ return i;
227
+ }
228
+ },
229
+ {
230
+ name: "exec",
231
+ description: "Run a command in a process's environment",
232
+ usage: "exec <name> [--] <cmd>",
233
+ parse: (args, i, result) => {
234
+ result.exec = true;
235
+ const name = args[++i];
236
+ if (!name)
237
+ throw new Error("exec requires a process name");
238
+ result.execName = name;
239
+ if (args[i + 1] === "--")
240
+ i++;
241
+ const rest = args.slice(i + 1);
242
+ if (rest.length === 0)
243
+ throw new Error("exec requires a command to run");
244
+ result.execCommand = rest.join(" ");
245
+ return "break";
246
+ }
247
+ },
248
+ {
249
+ name: "completions",
250
+ description: "Generate shell completions (bash, zsh, fish)",
251
+ usage: "completions <shell>",
252
+ parse: (args, i, result) => {
253
+ const next = args[++i];
254
+ if (next === undefined)
255
+ throw new Error("Missing value for completions");
256
+ result.completions = next;
257
+ return i;
258
+ }
259
+ }
260
+ ];
261
+ function generateHelp() {
262
+ const lines = [
263
+ "numux \u2014 terminal multiplexer with dependency orchestration",
264
+ "",
265
+ "Usage:",
266
+ " numux Run processes from config file",
267
+ " numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel",
268
+ " numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands",
269
+ " numux -w <script> Run a script across all workspaces"
270
+ ];
271
+ for (const sub of SUBCOMMANDS) {
272
+ const label = ` numux ${sub.usage ?? sub.name}`;
273
+ lines.push(`${label.padEnd(33)}${sub.description}`);
274
+ }
275
+ lines.push("", "Options:");
276
+ for (const f of FLAGS) {
277
+ const parts = [];
278
+ if (f.short)
279
+ parts.push(`${f.short},`);
280
+ parts.push(f.long);
281
+ if (f.type === "value")
282
+ parts.push(f.valueName);
283
+ const left = ` ${parts.join(" ")}`;
284
+ lines.push(`${left.padEnd(29)}${f.description}`);
285
+ }
286
+ lines.push("", "Config files (auto-detected):", " numux.config.ts, numux.config.js");
287
+ return lines.join(`
288
+ `);
289
+ }
80
290
 
81
291
  // src/cli.ts
292
+ var flagByName = new Map;
293
+ for (const f of FLAGS) {
294
+ flagByName.set(f.long, f);
295
+ if (f.short)
296
+ flagByName.set(f.short, f);
297
+ }
298
+ var subcommandByName = new Map;
299
+ for (const s of SUBCOMMANDS) {
300
+ subcommandByName.set(s.name, s);
301
+ }
82
302
  function parseArgs(argv) {
83
303
  const result = {
84
304
  help: false,
@@ -99,74 +319,35 @@ function parseArgs(argv) {
99
319
  };
100
320
  const args = argv.slice(2);
101
321
  let i = 0;
102
- const consumeValue = (flag) => {
103
- const next = args[++i];
104
- if (next === undefined) {
105
- throw new Error(`Missing value for ${flag}`);
106
- }
107
- return next;
108
- };
109
322
  while (i < args.length) {
110
323
  const arg = args[i];
111
- if (arg === "-h" || arg === "--help") {
112
- result.help = true;
113
- } else if (arg === "-v" || arg === "--version") {
114
- result.version = true;
115
- } else if (arg === "--debug") {
116
- result.debug = true;
117
- } else if (arg === "-p" || arg === "--prefix") {
118
- result.prefix = true;
119
- } else if (arg === "--kill-others") {
120
- result.killOthers = true;
121
- } else if (arg === "-t" || arg === "--timestamps") {
122
- result.timestamps = true;
123
- } else if (arg === "--no-restart") {
124
- result.noRestart = true;
125
- } else if (arg === "--no-watch") {
126
- result.noWatch = true;
127
- } else if (arg === "--colors") {
128
- result.autoColors = true;
129
- } else if (arg === "--config") {
130
- result.configPath = consumeValue(arg);
131
- } else if (arg === "-c" || arg === "--color") {
132
- result.colors = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
133
- } else if (arg === "--log-dir") {
134
- result.logDir = consumeValue(arg);
135
- } else if (arg === "--only") {
136
- result.only = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
137
- } else if (arg === "--exclude") {
138
- result.exclude = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
139
- } else if (arg === "-n" || arg === "--name") {
140
- const value = consumeValue(arg);
141
- const eq = value.indexOf("=");
142
- if (eq < 1) {
143
- throw new Error(`Invalid --name value: expected "name=command", got "${value}"`);
324
+ const flag = flagByName.get(arg);
325
+ if (flag) {
326
+ if (flag.type === "boolean") {
327
+ result[flag.key] = true;
328
+ } else {
329
+ const next = args[++i];
330
+ if (next === undefined) {
331
+ throw new Error(`Missing value for ${arg}`);
332
+ }
333
+ const value = flag.parse ? flag.parse(next, arg) : next;
334
+ const current = result[flag.key];
335
+ if (Array.isArray(current)) {
336
+ current.push(value);
337
+ } else {
338
+ result[flag.key] = value;
339
+ }
144
340
  }
145
- result.named.push({
146
- name: value.slice(0, eq),
147
- command: value.slice(eq + 1)
148
- });
149
- } else if (arg === "init" && result.commands.length === 0) {
150
- result.init = true;
151
- } else if (arg === "validate" && result.commands.length === 0) {
152
- result.validate = true;
153
- } else if (arg === "exec" && result.commands.length === 0) {
154
- result.exec = true;
155
- const name = args[++i];
156
- if (!name)
157
- throw new Error("exec requires a process name");
158
- result.execName = name;
159
- if (args[i + 1] === "--")
160
- i++;
161
- const rest = args.slice(i + 1);
162
- if (rest.length === 0)
163
- throw new Error("exec requires a command to run");
164
- result.execCommand = rest.join(" ");
165
- break;
166
- } else if (arg === "completions" && result.commands.length === 0) {
167
- result.completions = consumeValue(arg);
168
341
  } else if (!arg.startsWith("-")) {
169
- result.commands.push(arg);
342
+ const sub = result.commands.length === 0 ? subcommandByName.get(arg) : undefined;
343
+ if (sub) {
344
+ const ret = sub.parse(args, i, result);
345
+ if (ret === "break")
346
+ break;
347
+ i = ret;
348
+ } else {
349
+ result.commands.push(arg);
350
+ }
170
351
  } else {
171
352
  throw new Error(`Unknown option: ${arg}`);
172
353
  }
@@ -257,7 +438,36 @@ function generateCompletions(shell) {
257
438
  throw new Error(`Unknown shell: "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
258
439
  }
259
440
  }
441
+ function longName(f) {
442
+ return f.long.replace(/^-+/, "");
443
+ }
444
+ function sq(s) {
445
+ return s.replace(/'/g, "'\\''");
446
+ }
260
447
  function bashCompletions() {
448
+ const caseEntries = [];
449
+ for (const f of FLAGS) {
450
+ if (f.type !== "value")
451
+ continue;
452
+ const names = f.short ? `${f.short}|${f.long}` : f.long;
453
+ if (f.completionHint === "file") {
454
+ caseEntries.push(` ${names})
455
+ COMPREPLY=( $(compgen -f -- "$cur") )
456
+ return ;;`);
457
+ } else if (f.completionHint === "directory") {
458
+ caseEntries.push(` ${names})
459
+ COMPREPLY=( $(compgen -d -- "$cur") )
460
+ return ;;`);
461
+ } else {
462
+ caseEntries.push(` ${names})
463
+ return ;;`);
464
+ }
465
+ }
466
+ caseEntries.push(` completions)
467
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
468
+ return ;;`);
469
+ const allFlags = FLAGS.flatMap((f) => f.short ? [f.short, f.long] : [f.long]);
470
+ const subcmds = SUBCOMMANDS.map((s) => s.name);
261
471
  return `# numux bash completions
262
472
  # Add to ~/.bashrc: eval "$(numux completions bash)"
263
473
  _numux() {
@@ -266,59 +476,66 @@ _numux() {
266
476
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
267
477
 
268
478
  case "$prev" in
269
- --config)
270
- COMPREPLY=( $(compgen -f -- "$cur") )
271
- return ;;
272
- --log-dir)
273
- COMPREPLY=( $(compgen -d -- "$cur") )
274
- return ;;
275
- --only|--exclude)
276
- return ;;
277
- -n|--name)
278
- return ;;
279
- completions)
280
- COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
281
- return ;;
479
+ ${caseEntries.join(`
480
+ `)}
282
481
  esac
283
482
 
284
483
  if [[ "$cur" == -* ]]; then
285
- COMPREPLY=( $(compgen -W "-h --help -v --version -c --color --colors --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
484
+ COMPREPLY=( $(compgen -W "${allFlags.join(" ")}" -- "$cur") )
286
485
  else
287
- local subcmds="init validate exec completions"
486
+ local subcmds="${subcmds.join(" ")}"
288
487
  COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
289
488
  fi
290
489
  }
291
490
  complete -F _numux numux`;
292
491
  }
293
492
  function zshCompletions() {
493
+ const subcmdLines = SUBCOMMANDS.map((s) => ` '${s.name}:${sq(s.description)}'`).join(`
494
+ `);
495
+ const argLines = [];
496
+ for (const f of FLAGS) {
497
+ const desc = sq(f.description);
498
+ if (f.short) {
499
+ if (f.type === "value") {
500
+ let suffix = "";
501
+ if (f.completionHint === "file")
502
+ suffix = ":file:_files";
503
+ else if (f.completionHint === "directory")
504
+ suffix = ":directory:_directories";
505
+ else
506
+ suffix = `:${longName(f)}`;
507
+ argLines.push(` '(${f.short} ${f.long})'{${f.short},${f.long}}'[${desc}]${suffix}'`);
508
+ } else {
509
+ argLines.push(` '(${f.short} ${f.long})'{${f.short},${f.long}}'[${desc}]'`);
510
+ }
511
+ } else {
512
+ if (f.type === "value") {
513
+ let suffix = "";
514
+ if (f.completionHint === "file")
515
+ suffix = ":file:_files";
516
+ else if (f.completionHint === "directory")
517
+ suffix = ":directory:_directories";
518
+ else
519
+ suffix = `:${longName(f)}`;
520
+ argLines.push(` '${f.long}[${desc}]${suffix}'`);
521
+ } else {
522
+ argLines.push(` '${f.long}[${desc}]'`);
523
+ }
524
+ }
525
+ }
526
+ const argsBlock = argLines.map((l) => `${l} \\`).join(`
527
+ `);
294
528
  return `#compdef numux
295
529
  # numux zsh completions
296
530
  # Add to ~/.zshrc: eval "$(numux completions zsh)"
297
531
  _numux() {
298
532
  local -a subcmds
299
533
  subcmds=(
300
- 'init:Create a starter config file'
301
- 'validate:Validate config and show process graph'
302
- 'exec:Run a command in a process environment'
303
- 'completions:Generate shell completions'
534
+ ${subcmdLines}
304
535
  )
305
536
 
306
537
  _arguments -s \\
307
- '(-h --help)'{-h,--help}'[Show help]' \\
308
- '(-v --version)'{-v,--version}'[Show version]' \\
309
- '(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
310
- '--colors[Auto-assign colors based on process name]' \\
311
- '--config[Config file path]:file:_files' \\
312
- '(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
313
- '(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
314
- '--only[Only run these processes]:processes' \\
315
- '--exclude[Exclude these processes]:processes' \\
316
- '--kill-others[Kill all when any exits]' \\
317
- '--no-restart[Disable auto-restart]' \\
318
- '--no-watch[Disable file watching]' \\
319
- '(-t --timestamps)'{-t,--timestamps}'[Add timestamps to output]' \\
320
- '--log-dir[Log directory]:directory:_directories' \\
321
- '--debug[Enable debug logging]' \\
538
+ ${argsBlock}
322
539
  '1:subcommand:->subcmd' \\
323
540
  '*:command' \\
324
541
  && return
@@ -332,36 +549,37 @@ _numux() {
332
549
  _numux`;
333
550
  }
334
551
  function fishCompletions() {
335
- return `# numux fish completions
336
- # Add to fish: numux completions fish | source
337
- # Or save to: ~/.config/fish/completions/numux.fish
338
- complete -c numux -f
339
-
340
- # Subcommands
341
- complete -c numux -n __fish_use_subcommand -a init -d 'Create a starter config file'
342
- complete -c numux -n __fish_use_subcommand -a validate -d 'Validate config and show process graph'
343
- complete -c numux -n __fish_use_subcommand -a exec -d 'Run a command in a process environment'
344
- complete -c numux -n __fish_use_subcommand -a completions -d 'Generate shell completions'
345
-
346
- # Completions subcommand
347
- complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'
348
-
349
- # Options
350
- complete -c numux -s h -l help -d 'Show help'
351
- complete -c numux -s v -l version -d 'Show version'
352
- complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
353
- complete -c numux -l colors -d 'Auto-assign colors based on process name'
354
- complete -c numux -l config -rF -d 'Config file path'
355
- complete -c numux -s n -l name -r -d 'Named process (name=command)'
356
- complete -c numux -s p -l prefix -d 'Prefixed output mode'
357
- complete -c numux -l only -r -d 'Only run these processes'
358
- complete -c numux -l exclude -r -d 'Exclude these processes'
359
- complete -c numux -l kill-others -d 'Kill all when any exits'
360
- complete -c numux -l no-restart -d 'Disable auto-restart'
361
- complete -c numux -l no-watch -d 'Disable file watching'
362
- complete -c numux -s t -l timestamps -d 'Add timestamps to output'
363
- complete -c numux -l log-dir -ra '(__fish_complete_directories)' -d 'Log directory'
364
- complete -c numux -l debug -d 'Enable debug logging'`;
552
+ const lines = [
553
+ "# numux fish completions",
554
+ "# Add to fish: numux completions fish | source",
555
+ "# Or save to: ~/.config/fish/completions/numux.fish",
556
+ "complete -c numux -f",
557
+ "",
558
+ "# Subcommands"
559
+ ];
560
+ for (const s of SUBCOMMANDS) {
561
+ lines.push(`complete -c numux -n __fish_use_subcommand -a ${s.name} -d '${sq(s.description)}'`);
562
+ }
563
+ lines.push("", "# Completions subcommand", "complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'", "", "# Options");
564
+ for (const f of FLAGS) {
565
+ const parts = ["complete -c numux"];
566
+ if (f.short)
567
+ parts.push(`-s ${f.short.replace("-", "")}`);
568
+ parts.push(`-l ${longName(f)}`);
569
+ if (f.type === "value") {
570
+ if (f.completionHint === "file") {
571
+ parts.push("-rF");
572
+ } else if (f.completionHint === "directory") {
573
+ parts.push("-ra '(__fish_complete_directories)'");
574
+ } else {
575
+ parts.push("-r");
576
+ }
577
+ }
578
+ parts.push(`-d '${sq(f.description)}'`);
579
+ lines.push(parts.join(" "));
580
+ }
581
+ return lines.join(`
582
+ `);
365
583
  }
366
584
 
367
585
  // src/config/expand-scripts.ts
@@ -390,6 +608,12 @@ function detectPackageManager(pkgJson, cwd) {
390
608
  function isGlobPattern(name) {
391
609
  return /[*?[]/.test(name);
392
610
  }
611
+ function splitPatternArgs(raw) {
612
+ const i = raw.indexOf(" ");
613
+ if (i === -1)
614
+ return { glob: raw, extraArgs: "" };
615
+ return { glob: raw.slice(0, i), extraArgs: raw.slice(i) };
616
+ }
393
617
  function expandScriptPatterns(config, cwd) {
394
618
  const entries = Object.entries(config.processes);
395
619
  const hasWildcard = entries.some(([name]) => name.startsWith("npm:") || isGlobPattern(name));
@@ -413,15 +637,16 @@ function expandScriptPatterns(config, cwd) {
413
637
  expanded[name] = value;
414
638
  continue;
415
639
  }
416
- const pattern = name.startsWith("npm:") ? name.slice(4) : name;
640
+ const rawPattern = name.startsWith("npm:") ? name.slice(4) : name;
641
+ const { glob: globPattern, extraArgs } = splitPatternArgs(rawPattern);
417
642
  const template = value ?? {};
418
643
  if (template.command) {
419
644
  throw new Error(`"${name}": wildcard processes cannot have a "command" field (commands come from package.json scripts)`);
420
645
  }
421
- const glob = new Bun.Glob(pattern);
646
+ const glob = new Bun.Glob(globPattern);
422
647
  const matches = scriptNames.filter((s) => glob.match(s));
423
648
  if (matches.length === 0) {
424
- throw new Error(`"${name}": no scripts matched pattern "${pattern}". Available scripts: ${scriptNames.join(", ")}`);
649
+ throw new Error(`"${name}": no scripts matched pattern "${globPattern}". Available scripts: ${scriptNames.join(", ")}`);
425
650
  }
426
651
  const colors = Array.isArray(template.color) ? template.color : undefined;
427
652
  const singleColor = typeof template.color === "string" ? template.color : undefined;
@@ -434,7 +659,7 @@ function expandScriptPatterns(config, cwd) {
434
659
  const { color: _color, ...rest } = template;
435
660
  expanded[scriptName] = {
436
661
  ...rest,
437
- command: `${pm} run ${scriptName}`,
662
+ command: `${pm} run ${scriptName}${extraArgs}`,
438
663
  ...color ? { color } : {}
439
664
  };
440
665
  }
@@ -847,8 +1072,71 @@ function validateStopSignal(value) {
847
1072
  return;
848
1073
  }
849
1074
 
1075
+ // src/config/workspaces.ts
1076
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1077
+ import { basename, resolve as resolve4 } from "path";
1078
+ function resolveWorkspaceProcesses(script, cwd) {
1079
+ const pkgPath = resolve4(cwd, "package.json");
1080
+ if (!existsSync4(pkgPath)) {
1081
+ throw new Error(`No package.json found in ${cwd}`);
1082
+ }
1083
+ const pkgJson = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1084
+ const pm = detectPackageManager(pkgJson, cwd);
1085
+ const raw = pkgJson.workspaces;
1086
+ let patterns;
1087
+ if (Array.isArray(raw)) {
1088
+ patterns = raw;
1089
+ } else if (raw && typeof raw === "object" && Array.isArray(raw.packages)) {
1090
+ patterns = raw.packages;
1091
+ } else {
1092
+ throw new Error('No "workspaces" field found in package.json');
1093
+ }
1094
+ const dirs = [];
1095
+ for (const pattern of patterns) {
1096
+ const glob = new Bun.Glob(pattern);
1097
+ for (const match of glob.scanSync({ cwd, onlyFiles: false })) {
1098
+ const abs = resolve4(cwd, match);
1099
+ const wsPkgPath = resolve4(abs, "package.json");
1100
+ if (existsSync4(wsPkgPath)) {
1101
+ dirs.push(abs);
1102
+ }
1103
+ }
1104
+ }
1105
+ const processes = {};
1106
+ const usedNames = new Set;
1107
+ for (const dir of dirs) {
1108
+ const wsPkgPath = resolve4(dir, "package.json");
1109
+ const wsPkg = JSON.parse(readFileSync2(wsPkgPath, "utf-8"));
1110
+ const scripts = wsPkg.scripts;
1111
+ if (!scripts?.[script])
1112
+ continue;
1113
+ let name;
1114
+ if (typeof wsPkg.name === "string" && wsPkg.name) {
1115
+ name = wsPkg.name.replace(/^@[^/]+\//, "");
1116
+ } else {
1117
+ name = basename(dir);
1118
+ }
1119
+ if (usedNames.has(name)) {
1120
+ let suffix = 1;
1121
+ while (usedNames.has(`${name}-${suffix}`))
1122
+ suffix++;
1123
+ name = `${name}-${suffix}`;
1124
+ }
1125
+ usedNames.add(name);
1126
+ processes[name] = {
1127
+ command: `${pm} run ${script}`,
1128
+ cwd: dir,
1129
+ persistent: true
1130
+ };
1131
+ }
1132
+ if (Object.keys(processes).length === 0) {
1133
+ throw new Error(`No workspaces have a "${script}" script`);
1134
+ }
1135
+ return processes;
1136
+ }
1137
+
850
1138
  // src/process/manager.ts
851
- import { resolve as resolve6 } from "path";
1139
+ import { resolve as resolve7 } from "path";
852
1140
 
853
1141
  // src/utils/watcher.ts
854
1142
  import { watch } from "fs";
@@ -896,11 +1184,11 @@ class FileWatcher {
896
1184
  }
897
1185
 
898
1186
  // src/process/runner.ts
899
- import { resolve as resolve5 } from "path";
1187
+ import { resolve as resolve6 } from "path";
900
1188
 
901
1189
  // src/utils/env-file.ts
902
- import { readFileSync as readFileSync2 } from "fs";
903
- import { resolve as resolve4 } from "path";
1190
+ import { readFileSync as readFileSync3 } from "fs";
1191
+ import { resolve as resolve5 } from "path";
904
1192
  function parseEnvFile(content) {
905
1193
  const vars = {};
906
1194
  for (const line of content.split(/\r?\n/)) {
@@ -928,10 +1216,10 @@ function loadEnvFiles(envFile, cwd) {
928
1216
  const files = Array.isArray(envFile) ? envFile : [envFile];
929
1217
  const merged = {};
930
1218
  for (const file of files) {
931
- const path = resolve4(cwd, file);
1219
+ const path = resolve5(cwd, file);
932
1220
  let content;
933
1221
  try {
934
- content = readFileSync2(path, "utf-8");
1222
+ content = readFileSync3(path, "utf-8");
935
1223
  } catch (err) {
936
1224
  const code = err.code;
937
1225
  if (code === "ENOENT") {
@@ -1046,7 +1334,7 @@ class ProcessRunner {
1046
1334
  this.stopping = false;
1047
1335
  log(`[${this.name}] Starting (gen ${gen}): ${this.config.command}`);
1048
1336
  this.handler.onStatus("starting");
1049
- const cwd = this.config.cwd ? resolve5(this.config.cwd) : process.cwd();
1337
+ const cwd = this.config.cwd ? resolve6(this.config.cwd) : process.cwd();
1050
1338
  try {
1051
1339
  const envFromFile = this.config.envFile ? loadEnvFiles(this.config.envFile, cwd) : {};
1052
1340
  const noColor = "NO_COLOR" in process.env;
@@ -1308,12 +1596,12 @@ class ProcessManager {
1308
1596
  this.updateStatus(name, "skipped");
1309
1597
  continue;
1310
1598
  }
1311
- const { promise, resolve: resolve7 } = Promise.withResolvers();
1599
+ const { promise, resolve: resolve8 } = Promise.withResolvers();
1312
1600
  readyPromises.push(promise);
1313
- this.pendingReadyResolvers.set(name, resolve7);
1601
+ this.pendingReadyResolvers.set(name, resolve8);
1314
1602
  this.createRunner(name, () => {
1315
1603
  this.pendingReadyResolvers.delete(name);
1316
- resolve7();
1604
+ resolve8();
1317
1605
  });
1318
1606
  this.startProcess(name, cols, rows);
1319
1607
  }
@@ -1421,7 +1709,7 @@ class ProcessManager {
1421
1709
  if (!this.fileWatcher)
1422
1710
  this.fileWatcher = new FileWatcher;
1423
1711
  const patterns = Array.isArray(proc.watch) ? proc.watch : [proc.watch];
1424
- const cwd = proc.cwd ? resolve6(proc.cwd) : process.cwd();
1712
+ const cwd = proc.cwd ? resolve7(proc.cwd) : process.cwd();
1425
1713
  this.fileWatcher.watch(name, patterns, cwd, (changedFile) => {
1426
1714
  const state = this.states.get(name);
1427
1715
  if (!state)
@@ -1527,8 +1815,8 @@ class ProcessManager {
1527
1815
  clearTimeout(timer);
1528
1816
  }
1529
1817
  this.restartTimers.clear();
1530
- for (const resolve7 of this.pendingReadyResolvers.values()) {
1531
- resolve7();
1818
+ for (const resolve8 of this.pendingReadyResolvers.values()) {
1819
+ resolve8();
1532
1820
  }
1533
1821
  this.pendingReadyResolvers.clear();
1534
1822
  const reversed = [...this.tiers].reverse();
@@ -1579,6 +1867,8 @@ class Pane {
1579
1867
  decoder = new TextDecoder;
1580
1868
  _onScroll = null;
1581
1869
  _onCopy = null;
1870
+ _textLines = null;
1871
+ _textLinesLower = null;
1582
1872
  constructor(renderer, name, cols, rows, interactive = false) {
1583
1873
  this.scrollBox = new ScrollBoxRenderable(renderer, {
1584
1874
  id: `pane-${name}`,
@@ -1614,10 +1904,14 @@ class Pane {
1614
1904
  feed(data) {
1615
1905
  const text = this.decoder.decode(data, { stream: true });
1616
1906
  this.terminal.feed(text);
1907
+ this._textLines = null;
1908
+ this._textLinesLower = null;
1617
1909
  }
1618
1910
  resize(cols, rows) {
1619
1911
  this.terminal.cols = cols;
1620
1912
  this.terminal.rows = rows;
1913
+ this._textLines = null;
1914
+ this._textLinesLower = null;
1621
1915
  }
1622
1916
  get isAtBottom() {
1623
1917
  const { scrollTop, scrollHeight, viewport } = this.scrollBox;
@@ -1649,16 +1943,19 @@ class Pane {
1649
1943
  search(query) {
1650
1944
  if (!query)
1651
1945
  return [];
1652
- const text = this.terminal.getText();
1653
- const lines = text.split(`
1946
+ if (!this._textLines) {
1947
+ const text = this.terminal.getText();
1948
+ this._textLines = text.split(`
1654
1949
  `);
1950
+ this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
1951
+ }
1952
+ const lines = this._textLinesLower;
1655
1953
  const matches = [];
1656
1954
  const lowerQuery = query.toLowerCase();
1657
1955
  for (let line = 0;line < lines.length; line++) {
1658
- const lowerLine = lines[line].toLowerCase();
1659
1956
  let pos = 0;
1660
1957
  while (true) {
1661
- const idx = lowerLine.indexOf(lowerQuery, pos);
1958
+ const idx = lines[line].indexOf(lowerQuery, pos);
1662
1959
  if (idx === -1)
1663
1960
  break;
1664
1961
  matches.push({ line, start: idx, end: idx + query.length });
@@ -1668,12 +1965,22 @@ class Pane {
1668
1965
  return matches;
1669
1966
  }
1670
1967
  setHighlights(matches, currentIndex) {
1671
- const regions = matches.map((m, i) => ({
1672
- line: m.line,
1673
- start: m.start,
1674
- end: m.end,
1675
- backgroundColor: i === currentIndex ? "#b58900" : "#073642"
1676
- }));
1968
+ const firstVisible = Math.max(0, Math.floor(this.scrollBox.scrollTop) - 2);
1969
+ const lastVisible = Math.ceil(this.scrollBox.scrollTop + this.scrollBox.viewport.height) + 2;
1970
+ const regions = [];
1971
+ for (let i = 0;i < matches.length; i++) {
1972
+ const m = matches[i];
1973
+ if (m.line < firstVisible || m.line > lastVisible) {
1974
+ if (i !== currentIndex)
1975
+ continue;
1976
+ }
1977
+ regions.push({
1978
+ line: m.line,
1979
+ start: m.start,
1980
+ end: m.end,
1981
+ backgroundColor: i === currentIndex ? "#b58900" : "#073642"
1982
+ });
1983
+ }
1677
1984
  this.terminal.highlights = regions;
1678
1985
  }
1679
1986
  clearHighlights() {
@@ -1685,6 +1992,8 @@ class Pane {
1685
1992
  }
1686
1993
  clear() {
1687
1994
  this.terminal.reset();
1995
+ this._textLines = null;
1996
+ this._textLinesLower = null;
1688
1997
  }
1689
1998
  destroy() {
1690
1999
  this.terminal.destroy();
@@ -1823,6 +2132,7 @@ class ColoredSelectRenderable extends SelectRenderable {
1823
2132
  const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
1824
2133
  const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
1825
2134
  const selectedTextColor = this._selectedTextColor;
2135
+ const lineWidth = fb.width;
1826
2136
  for (let i = 0;i < visibleCount; i++) {
1827
2137
  const actualIndex = scrollOffset + i;
1828
2138
  const itemY = i * linesPerItem;
@@ -1830,13 +2140,11 @@ class ColoredSelectRenderable extends SelectRenderable {
1830
2140
  const isSelected = actualIndex === selectedIndex;
1831
2141
  const defaultColor = isSelected ? selectedTextColor : baseTextColor;
1832
2142
  const colors = this._optionColors[actualIndex];
1833
- fb.drawText(`${optName} `, 1, itemY, defaultColor);
2143
+ const textColor = colors?.name ?? defaultColor;
2144
+ fb.drawText(optName.padEnd(lineWidth), 1, itemY, textColor);
1834
2145
  if (colors?.icon) {
1835
2146
  fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
1836
2147
  }
1837
- if (colors?.name) {
1838
- fb.drawText(optName.slice(2), 3, itemY, colors.name);
1839
- }
1840
2148
  }
1841
2149
  }
1842
2150
  }
@@ -1993,6 +2301,7 @@ class App {
1993
2301
  sidebarWidth = 20;
1994
2302
  config;
1995
2303
  resizeTimer = null;
2304
+ searchTimer = null;
1996
2305
  searchMode = false;
1997
2306
  searchQuery = "";
1998
2307
  searchMatches = [];
@@ -2048,6 +2357,11 @@ class App {
2048
2357
  const interactive = this.config.processes[name].interactive === true;
2049
2358
  const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
2050
2359
  pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
2360
+ pane.onScroll(() => {
2361
+ if (this.searchMode && this.searchMatches.length > 0 && this.activePane === name) {
2362
+ this.updateSearchHighlights();
2363
+ }
2364
+ });
2051
2365
  this.panes.set(name, pane);
2052
2366
  paneContainer.add(pane.scrollBox);
2053
2367
  }
@@ -2248,6 +2562,10 @@ class App {
2248
2562
  this.searchQuery = "";
2249
2563
  this.searchMatches = [];
2250
2564
  this.searchIndex = -1;
2565
+ if (this.searchTimer) {
2566
+ clearTimeout(this.searchTimer);
2567
+ this.searchTimer = null;
2568
+ }
2251
2569
  if (this.activePane) {
2252
2570
  this.panes.get(this.activePane)?.clearHighlights();
2253
2571
  }
@@ -2273,15 +2591,24 @@ class App {
2273
2591
  if (key.name === "backspace") {
2274
2592
  if (this.searchQuery.length > 0) {
2275
2593
  this.searchQuery = this.searchQuery.slice(0, -1);
2276
- this.runSearch();
2594
+ this.scheduleSearch();
2277
2595
  }
2278
2596
  return;
2279
2597
  }
2280
2598
  if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
2281
2599
  this.searchQuery += key.sequence;
2282
- this.runSearch();
2600
+ this.scheduleSearch();
2283
2601
  }
2284
2602
  }
2603
+ scheduleSearch() {
2604
+ this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
2605
+ if (this.searchTimer)
2606
+ clearTimeout(this.searchTimer);
2607
+ this.searchTimer = setTimeout(() => {
2608
+ this.searchTimer = null;
2609
+ this.runSearch();
2610
+ }, 100);
2611
+ }
2285
2612
  runSearch() {
2286
2613
  if (!this.activePane)
2287
2614
  return;
@@ -2325,6 +2652,10 @@ class App {
2325
2652
  clearTimeout(this.resizeTimer);
2326
2653
  this.resizeTimer = null;
2327
2654
  }
2655
+ if (this.searchTimer) {
2656
+ clearTimeout(this.searchTimer);
2657
+ this.searchTimer = null;
2658
+ }
2328
2659
  for (const timer of this.inputWaitTimers.values()) {
2329
2660
  clearTimeout(timer);
2330
2661
  }
@@ -2352,7 +2683,6 @@ class PrefixDisplay {
2352
2683
  noColor;
2353
2684
  decoders = new Map;
2354
2685
  buffers = new Map;
2355
- maxNameLen;
2356
2686
  logWriter;
2357
2687
  killOthers;
2358
2688
  timestamps;
@@ -2364,7 +2694,6 @@ class PrefixDisplay {
2364
2694
  this.timestamps = options.timestamps ?? false;
2365
2695
  this.noColor = "NO_COLOR" in process.env;
2366
2696
  const names = manager.getProcessNames();
2367
- this.maxNameLen = Math.max(...names.map((n) => n.length));
2368
2697
  this.colors = buildProcessColorMap(names, config);
2369
2698
  for (const name of names) {
2370
2699
  this.decoders.set(name, new TextDecoder("utf-8", { fatal: false }));
@@ -2438,15 +2767,14 @@ class PrefixDisplay {
2438
2767
  return `${h}:${m}:${s}`;
2439
2768
  }
2440
2769
  printLine(name, line) {
2441
- const padded = name.padEnd(this.maxNameLen);
2442
2770
  const ts = this.timestamps ? `${DIM}[${this.formatTimestamp()}]${RESET} ` : "";
2443
2771
  const tsPlain = this.timestamps ? `[${this.formatTimestamp()}] ` : "";
2444
2772
  if (this.noColor) {
2445
- process.stdout.write(`${tsPlain}[${padded}] ${stripAnsi(line)}
2773
+ process.stdout.write(`${tsPlain}[${name}] ${stripAnsi(line)}
2446
2774
  `);
2447
2775
  } else {
2448
2776
  const color = this.colors.get(name) ?? "";
2449
- process.stdout.write(`${ts}${color}[${padded}]${RESET} ${line}
2777
+ process.stdout.write(`${ts}${color}[${name}]${RESET} ${line}
2450
2778
  `);
2451
2779
  }
2452
2780
  }
@@ -2594,36 +2922,7 @@ function setupShutdownHandlers(app, logWriter) {
2594
2922
  }
2595
2923
 
2596
2924
  // src/index.ts
2597
- var HELP = `numux \u2014 terminal multiplexer with dependency orchestration
2598
-
2599
- Usage:
2600
- numux Run processes from config file
2601
- numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel
2602
- numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands
2603
- numux init Create a starter config file
2604
- numux validate Validate config and show process graph
2605
- numux exec <name> [--] <cmd> Run a command in a process's environment
2606
- numux completions <shell> Generate shell completions (bash, zsh, fish)
2607
-
2608
- Options:
2609
- -n, --name <name=command> Add a named process
2610
- -c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
2611
- --colors Auto-assign colors to processes based on their name
2612
- --config <path> Config file path (default: auto-detect)
2613
- -p, --prefix Prefixed output mode (no TUI, for CI/scripts)
2614
- --only <a,b,...> Only run these processes (+ their dependencies)
2615
- --exclude <a,b,...> Exclude these processes
2616
- --kill-others Kill all processes when any exits
2617
- --no-restart Disable auto-restart for crashed processes
2618
- --no-watch Disable file watching even if config has watch patterns
2619
- -t, --timestamps Add timestamps to prefixed output lines
2620
- --log-dir <path> Write per-process logs to directory
2621
- --debug Enable debug logging to .numux/debug.log
2622
- -h, --help Show this help
2623
- -v, --version Show version
2624
-
2625
- Config files (auto-detected):
2626
- numux.config.ts, numux.config.js`;
2925
+ var HELP = generateHelp();
2627
2926
  var INIT_TEMPLATE = `import { defineConfig } from 'numux'
2628
2927
 
2629
2928
  export default defineConfig({
@@ -2658,8 +2957,8 @@ async function main() {
2658
2957
  process.exit(0);
2659
2958
  }
2660
2959
  if (parsed.init) {
2661
- const target = resolve7("numux.config.ts");
2662
- if (existsSync4(target)) {
2960
+ const target = resolve8("numux.config.ts");
2961
+ if (existsSync5(target)) {
2663
2962
  console.error(`Already exists: ${target}`);
2664
2963
  process.exit(1);
2665
2964
  }
@@ -2717,7 +3016,7 @@ async function main() {
2717
3016
  const names = Object.keys(config2.processes);
2718
3017
  throw new Error(`Unknown process "${parsed.execName}". Available: ${names.join(", ")}`);
2719
3018
  }
2720
- const cwd = proc.cwd ? resolve7(proc.cwd) : process.cwd();
3019
+ const cwd = proc.cwd ? resolve8(proc.cwd) : process.cwd();
2721
3020
  const envFromFile = proc.envFile ? loadEnvFiles(proc.envFile, cwd) : {};
2722
3021
  const env = {
2723
3022
  ...process.env,
@@ -2738,7 +3037,7 @@ async function main() {
2738
3037
  }
2739
3038
  let config;
2740
3039
  const warnings = [];
2741
- if (parsed.commands.length > 0 || parsed.named.length > 0) {
3040
+ if (parsed.commands.length > 0 || parsed.named.length > 0 || parsed.workspace) {
2742
3041
  const isScriptPattern = (c) => c.startsWith("npm:") || /[*?[]/.test(c);
2743
3042
  const hasNpmPatterns = parsed.commands.some(isScriptPattern);
2744
3043
  if (hasNpmPatterns) {
@@ -2769,6 +3068,21 @@ async function main() {
2769
3068
  colors: parsed.colors
2770
3069
  });
2771
3070
  }
3071
+ if (parsed.workspace) {
3072
+ const wsProcesses = resolveWorkspaceProcesses(parsed.workspace, process.cwd());
3073
+ for (const [name, proc] of Object.entries(wsProcesses)) {
3074
+ let finalName = name;
3075
+ if (config.processes[finalName]) {
3076
+ let suffix = 1;
3077
+ while (config.processes[`${finalName}-${suffix}`])
3078
+ suffix++;
3079
+ finalName = `${finalName}-${suffix}`;
3080
+ }
3081
+ if (parsed.noRestart)
3082
+ proc.maxRestarts = 0;
3083
+ config.processes[finalName] = proc;
3084
+ }
3085
+ }
2772
3086
  } else {
2773
3087
  const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
2774
3088
  config = validateConfig(raw, warnings);
@@ -2800,6 +3114,11 @@ async function main() {
2800
3114
  }
2801
3115
  printWarnings(warnings);
2802
3116
  if (parsed.prefix) {
3117
+ if (!parsed.noRestart) {
3118
+ for (const proc of Object.values(config.processes)) {
3119
+ proc.maxRestarts ??= 0;
3120
+ }
3121
+ }
2803
3122
  const display = new PrefixDisplay(manager, config, {
2804
3123
  logWriter,
2805
3124
  killOthers: parsed.killOthers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.9.0",
3
+ "version": "1.10.1",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",