numux 1.8.0 → 1.10.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.
Files changed (3) hide show
  1. package/README.md +46 -16
  2. package/dist/numux.js +199 -39
  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) |
@@ -265,22 +310,7 @@ Persistent processes that crash are auto-restarted with exponential backoff (1s
265
310
 
266
311
  ## Keybindings
267
312
 
268
- | Key | Action |
269
- |-----|--------|
270
- | `Ctrl+C` | Quit (graceful shutdown) |
271
- | `R` | Restart active process |
272
- | `Shift+R` | Restart all processes |
273
- | `S` | Stop/start active process |
274
- | `L` | Clear active pane output |
275
- | `F` | Search in active pane output |
276
- | `1`–`9` | Jump to tab |
277
- | `Left/Right` | Cycle tabs |
278
- | `PageUp/PageDown` | Scroll output by page |
279
- | `Home/End` | Scroll to top/bottom |
280
-
281
- While searching: type to filter, `Enter`/`Shift+Enter` to navigate matches, `Escape` to close.
282
-
283
- Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
313
+ Keybindings are shown in the status bar at the bottom of the app. Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
284
314
 
285
315
  ## Tab icons
286
316
 
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.8.0",
25
+ version: "1.10.0",
26
26
  description: "Terminal multiplexer with dependency orchestration",
27
27
  type: "module",
28
28
  license: "MIT",
@@ -75,8 +75,8 @@ 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
80
 
81
81
  // src/cli.ts
82
82
  function parseArgs(argv) {
@@ -122,6 +122,8 @@ function parseArgs(argv) {
122
122
  result.timestamps = true;
123
123
  } else if (arg === "--no-restart") {
124
124
  result.noRestart = true;
125
+ } else if (arg === "-w" || arg === "--workspace") {
126
+ result.workspace = consumeValue(arg);
125
127
  } else if (arg === "--no-watch") {
126
128
  result.noWatch = true;
127
129
  } else if (arg === "--colors") {
@@ -274,7 +276,7 @@ _numux() {
274
276
  return ;;
275
277
  --only|--exclude)
276
278
  return ;;
277
- -n|--name)
279
+ -n|--name|-w|--workspace)
278
280
  return ;;
279
281
  completions)
280
282
  COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
@@ -282,7 +284,7 @@ _numux() {
282
284
  esac
283
285
 
284
286
  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") )
287
+ COMPREPLY=( $(compgen -W "-h --help -v --version -w --workspace -c --color --colors --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
286
288
  else
287
289
  local subcmds="init validate exec completions"
288
290
  COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
@@ -306,6 +308,7 @@ _numux() {
306
308
  _arguments -s \\
307
309
  '(-h --help)'{-h,--help}'[Show help]' \\
308
310
  '(-v --version)'{-v,--version}'[Show version]' \\
311
+ '(-w --workspace)'{-w,--workspace}'[Run script across all workspaces]:script' \\
309
312
  '(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
310
313
  '--colors[Auto-assign colors based on process name]' \\
311
314
  '--config[Config file path]:file:_files' \\
@@ -352,6 +355,7 @@ complete -c numux -s v -l version -d 'Show version'
352
355
  complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
353
356
  complete -c numux -l colors -d 'Auto-assign colors based on process name'
354
357
  complete -c numux -l config -rF -d 'Config file path'
358
+ complete -c numux -s w -l workspace -r -d 'Run script across all workspaces'
355
359
  complete -c numux -s n -l name -r -d 'Named process (name=command)'
356
360
  complete -c numux -s p -l prefix -d 'Prefixed output mode'
357
361
  complete -c numux -l only -r -d 'Only run these processes'
@@ -390,6 +394,12 @@ function detectPackageManager(pkgJson, cwd) {
390
394
  function isGlobPattern(name) {
391
395
  return /[*?[]/.test(name);
392
396
  }
397
+ function splitPatternArgs(raw) {
398
+ const i = raw.indexOf(" ");
399
+ if (i === -1)
400
+ return { glob: raw, extraArgs: "" };
401
+ return { glob: raw.slice(0, i), extraArgs: raw.slice(i) };
402
+ }
393
403
  function expandScriptPatterns(config, cwd) {
394
404
  const entries = Object.entries(config.processes);
395
405
  const hasWildcard = entries.some(([name]) => name.startsWith("npm:") || isGlobPattern(name));
@@ -413,15 +423,16 @@ function expandScriptPatterns(config, cwd) {
413
423
  expanded[name] = value;
414
424
  continue;
415
425
  }
416
- const pattern = name.startsWith("npm:") ? name.slice(4) : name;
426
+ const rawPattern = name.startsWith("npm:") ? name.slice(4) : name;
427
+ const { glob: globPattern, extraArgs } = splitPatternArgs(rawPattern);
417
428
  const template = value ?? {};
418
429
  if (template.command) {
419
430
  throw new Error(`"${name}": wildcard processes cannot have a "command" field (commands come from package.json scripts)`);
420
431
  }
421
- const glob = new Bun.Glob(pattern);
432
+ const glob = new Bun.Glob(globPattern);
422
433
  const matches = scriptNames.filter((s) => glob.match(s));
423
434
  if (matches.length === 0) {
424
- throw new Error(`"${name}": no scripts matched pattern "${pattern}". Available scripts: ${scriptNames.join(", ")}`);
435
+ throw new Error(`"${name}": no scripts matched pattern "${globPattern}". Available scripts: ${scriptNames.join(", ")}`);
425
436
  }
426
437
  const colors = Array.isArray(template.color) ? template.color : undefined;
427
438
  const singleColor = typeof template.color === "string" ? template.color : undefined;
@@ -434,7 +445,7 @@ function expandScriptPatterns(config, cwd) {
434
445
  const { color: _color, ...rest } = template;
435
446
  expanded[scriptName] = {
436
447
  ...rest,
437
- command: `${pm} run ${scriptName}`,
448
+ command: `${pm} run ${scriptName}${extraArgs}`,
438
449
  ...color ? { color } : {}
439
450
  };
440
451
  }
@@ -847,8 +858,71 @@ function validateStopSignal(value) {
847
858
  return;
848
859
  }
849
860
 
861
+ // src/config/workspaces.ts
862
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
863
+ import { basename, resolve as resolve4 } from "path";
864
+ function resolveWorkspaceProcesses(script, cwd) {
865
+ const pkgPath = resolve4(cwd, "package.json");
866
+ if (!existsSync4(pkgPath)) {
867
+ throw new Error(`No package.json found in ${cwd}`);
868
+ }
869
+ const pkgJson = JSON.parse(readFileSync2(pkgPath, "utf-8"));
870
+ const pm = detectPackageManager(pkgJson, cwd);
871
+ const raw = pkgJson.workspaces;
872
+ let patterns;
873
+ if (Array.isArray(raw)) {
874
+ patterns = raw;
875
+ } else if (raw && typeof raw === "object" && Array.isArray(raw.packages)) {
876
+ patterns = raw.packages;
877
+ } else {
878
+ throw new Error('No "workspaces" field found in package.json');
879
+ }
880
+ const dirs = [];
881
+ for (const pattern of patterns) {
882
+ const glob = new Bun.Glob(pattern);
883
+ for (const match of glob.scanSync({ cwd, onlyFiles: false })) {
884
+ const abs = resolve4(cwd, match);
885
+ const wsPkgPath = resolve4(abs, "package.json");
886
+ if (existsSync4(wsPkgPath)) {
887
+ dirs.push(abs);
888
+ }
889
+ }
890
+ }
891
+ const processes = {};
892
+ const usedNames = new Set;
893
+ for (const dir of dirs) {
894
+ const wsPkgPath = resolve4(dir, "package.json");
895
+ const wsPkg = JSON.parse(readFileSync2(wsPkgPath, "utf-8"));
896
+ const scripts = wsPkg.scripts;
897
+ if (!scripts?.[script])
898
+ continue;
899
+ let name;
900
+ if (typeof wsPkg.name === "string" && wsPkg.name) {
901
+ name = wsPkg.name.replace(/^@[^/]+\//, "");
902
+ } else {
903
+ name = basename(dir);
904
+ }
905
+ if (usedNames.has(name)) {
906
+ let suffix = 1;
907
+ while (usedNames.has(`${name}-${suffix}`))
908
+ suffix++;
909
+ name = `${name}-${suffix}`;
910
+ }
911
+ usedNames.add(name);
912
+ processes[name] = {
913
+ command: `${pm} run ${script}`,
914
+ cwd: dir,
915
+ persistent: true
916
+ };
917
+ }
918
+ if (Object.keys(processes).length === 0) {
919
+ throw new Error(`No workspaces have a "${script}" script`);
920
+ }
921
+ return processes;
922
+ }
923
+
850
924
  // src/process/manager.ts
851
- import { resolve as resolve6 } from "path";
925
+ import { resolve as resolve7 } from "path";
852
926
 
853
927
  // src/utils/watcher.ts
854
928
  import { watch } from "fs";
@@ -896,11 +970,11 @@ class FileWatcher {
896
970
  }
897
971
 
898
972
  // src/process/runner.ts
899
- import { resolve as resolve5 } from "path";
973
+ import { resolve as resolve6 } from "path";
900
974
 
901
975
  // src/utils/env-file.ts
902
- import { readFileSync as readFileSync2 } from "fs";
903
- import { resolve as resolve4 } from "path";
976
+ import { readFileSync as readFileSync3 } from "fs";
977
+ import { resolve as resolve5 } from "path";
904
978
  function parseEnvFile(content) {
905
979
  const vars = {};
906
980
  for (const line of content.split(/\r?\n/)) {
@@ -928,10 +1002,10 @@ function loadEnvFiles(envFile, cwd) {
928
1002
  const files = Array.isArray(envFile) ? envFile : [envFile];
929
1003
  const merged = {};
930
1004
  for (const file of files) {
931
- const path = resolve4(cwd, file);
1005
+ const path = resolve5(cwd, file);
932
1006
  let content;
933
1007
  try {
934
- content = readFileSync2(path, "utf-8");
1008
+ content = readFileSync3(path, "utf-8");
935
1009
  } catch (err) {
936
1010
  const code = err.code;
937
1011
  if (code === "ENOENT") {
@@ -1046,7 +1120,7 @@ class ProcessRunner {
1046
1120
  this.stopping = false;
1047
1121
  log(`[${this.name}] Starting (gen ${gen}): ${this.config.command}`);
1048
1122
  this.handler.onStatus("starting");
1049
- const cwd = this.config.cwd ? resolve5(this.config.cwd) : process.cwd();
1123
+ const cwd = this.config.cwd ? resolve6(this.config.cwd) : process.cwd();
1050
1124
  try {
1051
1125
  const envFromFile = this.config.envFile ? loadEnvFiles(this.config.envFile, cwd) : {};
1052
1126
  const noColor = "NO_COLOR" in process.env;
@@ -1308,12 +1382,12 @@ class ProcessManager {
1308
1382
  this.updateStatus(name, "skipped");
1309
1383
  continue;
1310
1384
  }
1311
- const { promise, resolve: resolve7 } = Promise.withResolvers();
1385
+ const { promise, resolve: resolve8 } = Promise.withResolvers();
1312
1386
  readyPromises.push(promise);
1313
- this.pendingReadyResolvers.set(name, resolve7);
1387
+ this.pendingReadyResolvers.set(name, resolve8);
1314
1388
  this.createRunner(name, () => {
1315
1389
  this.pendingReadyResolvers.delete(name);
1316
- resolve7();
1390
+ resolve8();
1317
1391
  });
1318
1392
  this.startProcess(name, cols, rows);
1319
1393
  }
@@ -1421,7 +1495,7 @@ class ProcessManager {
1421
1495
  if (!this.fileWatcher)
1422
1496
  this.fileWatcher = new FileWatcher;
1423
1497
  const patterns = Array.isArray(proc.watch) ? proc.watch : [proc.watch];
1424
- const cwd = proc.cwd ? resolve6(proc.cwd) : process.cwd();
1498
+ const cwd = proc.cwd ? resolve7(proc.cwd) : process.cwd();
1425
1499
  this.fileWatcher.watch(name, patterns, cwd, (changedFile) => {
1426
1500
  const state = this.states.get(name);
1427
1501
  if (!state)
@@ -1527,8 +1601,8 @@ class ProcessManager {
1527
1601
  clearTimeout(timer);
1528
1602
  }
1529
1603
  this.restartTimers.clear();
1530
- for (const resolve7 of this.pendingReadyResolvers.values()) {
1531
- resolve7();
1604
+ for (const resolve8 of this.pendingReadyResolvers.values()) {
1605
+ resolve8();
1532
1606
  }
1533
1607
  this.pendingReadyResolvers.clear();
1534
1608
  const reversed = [...this.tiers].reverse();
@@ -1549,6 +1623,26 @@ function evaluateCondition(condition) {
1549
1623
  // src/ui/app.ts
1550
1624
  import { BoxRenderable, createCliRenderer } from "@opentui/core";
1551
1625
 
1626
+ // src/ui/keybindings.ts
1627
+ var SHORTCUTS = {
1628
+ restartAll: { key: "r", label: "Shift+R", description: "restart all", shift: true },
1629
+ copy: { key: "y", label: "Y", description: "copy" },
1630
+ search: { key: "f", label: "F", description: "search" },
1631
+ restart: { key: "r", label: "R", description: "restart" },
1632
+ stopStart: { key: "s", label: "S", description: "stop/start" },
1633
+ clear: { key: "l", label: "L", description: "clear" }
1634
+ };
1635
+ var STATUS_HINTS = [
1636
+ ["\u2190\u2192/1-9", "tabs"],
1637
+ [SHORTCUTS.restart.label, SHORTCUTS.restart.description],
1638
+ [SHORTCUTS.stopStart.label, SHORTCUTS.stopStart.description],
1639
+ [SHORTCUTS.search.label, SHORTCUTS.search.description],
1640
+ [SHORTCUTS.copy.label, SHORTCUTS.copy.description],
1641
+ [SHORTCUTS.clear.label, SHORTCUTS.clear.description],
1642
+ ["Ctrl+C", "quit"]
1643
+ ];
1644
+ var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
1645
+
1552
1646
  // src/ui/pane.ts
1553
1647
  import { ScrollBoxRenderable } from "@opentui/core";
1554
1648
  import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
@@ -1558,6 +1652,7 @@ class Pane {
1558
1652
  terminal;
1559
1653
  decoder = new TextDecoder;
1560
1654
  _onScroll = null;
1655
+ _onCopy = null;
1561
1656
  constructor(renderer, name, cols, rows, interactive = false) {
1562
1657
  this.scrollBox = new ScrollBoxRenderable(renderer, {
1563
1658
  id: `pane-${name}`,
@@ -1576,6 +1671,18 @@ class Pane {
1576
1671
  showCursor: interactive,
1577
1672
  trimEnd: true
1578
1673
  });
1674
+ const origOnSelectionChanged = this.terminal.onSelectionChanged.bind(this.terminal);
1675
+ this.terminal.onSelectionChanged = (selection) => {
1676
+ const result = origOnSelectionChanged(selection);
1677
+ if (selection?.isActive && !selection.isDragging) {
1678
+ const text = selection.getSelectedText();
1679
+ if (text) {
1680
+ renderer.copyToClipboardOSC52(text);
1681
+ this._onCopy?.(text);
1682
+ }
1683
+ }
1684
+ return result;
1685
+ };
1579
1686
  this.scrollBox.add(this.terminal);
1580
1687
  }
1581
1688
  feed(data) {
@@ -1604,6 +1711,9 @@ class Pane {
1604
1711
  onScroll(handler) {
1605
1712
  this._onScroll = handler;
1606
1713
  }
1714
+ onCopy(handler) {
1715
+ this._onCopy = handler;
1716
+ }
1607
1717
  show() {
1608
1718
  this.scrollBox.visible = true;
1609
1719
  }
@@ -1667,6 +1777,8 @@ class StatusBar {
1667
1777
  _searchQuery = "";
1668
1778
  _searchMatchCount = 0;
1669
1779
  _searchCurrentIndex = -1;
1780
+ _tempMessage = null;
1781
+ _tempTimer = null;
1670
1782
  constructor(renderer) {
1671
1783
  this.renderable = new TextRenderable(renderer, {
1672
1784
  id: "status-bar",
@@ -1684,13 +1796,25 @@ class StatusBar {
1684
1796
  this._searchCurrentIndex = currentIndex;
1685
1797
  this.renderable.content = this.buildContent();
1686
1798
  }
1799
+ showTemporaryMessage(message, duration = 2000) {
1800
+ if (this._tempTimer)
1801
+ clearTimeout(this._tempTimer);
1802
+ this._tempMessage = message;
1803
+ this.renderable.content = this.buildContent();
1804
+ this._tempTimer = setTimeout(() => {
1805
+ this._tempMessage = null;
1806
+ this._tempTimer = null;
1807
+ this.renderable.content = this.buildContent();
1808
+ }, duration);
1809
+ }
1687
1810
  buildContent() {
1811
+ if (this._tempMessage) {
1812
+ return new StyledText([cyan(this._tempMessage)]);
1813
+ }
1688
1814
  if (this._searchMode) {
1689
1815
  return this.buildSearchContent();
1690
1816
  }
1691
- return new StyledText([
1692
- plain("\u2190\u2192/1-9: tabs R: restart S: stop/start F: search L: clear Ctrl+C: quit")
1693
- ]);
1817
+ return new StyledText([plain(STATUS_BAR_TEXT)]);
1694
1818
  }
1695
1819
  buildSearchContent() {
1696
1820
  const chunks = [];
@@ -1997,6 +2121,7 @@ class App {
1997
2121
  for (const name of this.names) {
1998
2122
  const interactive = this.config.processes[name].interactive === true;
1999
2123
  const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
2124
+ pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
2000
2125
  this.panes.set(name, pane);
2001
2126
  paneContainer.add(pane.scrollBox);
2002
2127
  }
@@ -2060,19 +2185,23 @@ class App {
2060
2185
  const isInteractive = this.config.processes[this.activePane]?.interactive === true;
2061
2186
  if (!isInteractive) {
2062
2187
  const name = key.name.toLowerCase();
2063
- if (key.shift && name === "r") {
2188
+ if (key.shift && name === SHORTCUTS.restartAll.key) {
2064
2189
  this.manager.restartAll(this.termCols, this.termRows);
2065
2190
  return;
2066
2191
  }
2067
- if (name === "f") {
2192
+ if (name === SHORTCUTS.copy.key) {
2193
+ this.copySelection();
2194
+ return;
2195
+ }
2196
+ if (name === SHORTCUTS.search.key) {
2068
2197
  this.enterSearch();
2069
2198
  return;
2070
2199
  }
2071
- if (name === "r") {
2200
+ if (name === SHORTCUTS.restart.key) {
2072
2201
  this.manager.restart(this.activePane, this.termCols, this.termRows);
2073
2202
  return;
2074
2203
  }
2075
- if (name === "s") {
2204
+ if (name === SHORTCUTS.stopStart.key) {
2076
2205
  const state = this.manager.getState(this.activePane);
2077
2206
  if (state?.status === "stopped" || state?.status === "finished" || state?.status === "failed") {
2078
2207
  this.manager.start(this.activePane, this.termCols, this.termRows);
@@ -2081,7 +2210,7 @@ class App {
2081
2210
  }
2082
2211
  return;
2083
2212
  }
2084
- if (name === "l") {
2213
+ if (name === SHORTCUTS.clear.key) {
2085
2214
  this.panes.get(this.activePane)?.clear();
2086
2215
  return;
2087
2216
  }
@@ -2169,6 +2298,18 @@ class App {
2169
2298
  this.tabBar.setInputWaiting(name, false);
2170
2299
  }
2171
2300
  }
2301
+ copySelection() {
2302
+ const selection = this.renderer.getSelection();
2303
+ if (!selection?.isActive)
2304
+ return false;
2305
+ const text = selection.getSelectedText();
2306
+ if (!text)
2307
+ return false;
2308
+ this.renderer.copyToClipboardOSC52(text);
2309
+ this.renderer.clearSelection();
2310
+ this.statusBar.showTemporaryMessage("Copied!");
2311
+ return true;
2312
+ }
2172
2313
  enterSearch() {
2173
2314
  this.searchMode = true;
2174
2315
  this.searchQuery = "";
@@ -2285,7 +2426,6 @@ class PrefixDisplay {
2285
2426
  noColor;
2286
2427
  decoders = new Map;
2287
2428
  buffers = new Map;
2288
- maxNameLen;
2289
2429
  logWriter;
2290
2430
  killOthers;
2291
2431
  timestamps;
@@ -2297,7 +2437,6 @@ class PrefixDisplay {
2297
2437
  this.timestamps = options.timestamps ?? false;
2298
2438
  this.noColor = "NO_COLOR" in process.env;
2299
2439
  const names = manager.getProcessNames();
2300
- this.maxNameLen = Math.max(...names.map((n) => n.length));
2301
2440
  this.colors = buildProcessColorMap(names, config);
2302
2441
  for (const name of names) {
2303
2442
  this.decoders.set(name, new TextDecoder("utf-8", { fatal: false }));
@@ -2371,15 +2510,14 @@ class PrefixDisplay {
2371
2510
  return `${h}:${m}:${s}`;
2372
2511
  }
2373
2512
  printLine(name, line) {
2374
- const padded = name.padEnd(this.maxNameLen);
2375
2513
  const ts = this.timestamps ? `${DIM}[${this.formatTimestamp()}]${RESET} ` : "";
2376
2514
  const tsPlain = this.timestamps ? `[${this.formatTimestamp()}] ` : "";
2377
2515
  if (this.noColor) {
2378
- process.stdout.write(`${tsPlain}[${padded}] ${stripAnsi(line)}
2516
+ process.stdout.write(`${tsPlain}[${name}] ${stripAnsi(line)}
2379
2517
  `);
2380
2518
  } else {
2381
2519
  const color = this.colors.get(name) ?? "";
2382
- process.stdout.write(`${ts}${color}[${padded}]${RESET} ${line}
2520
+ process.stdout.write(`${ts}${color}[${name}]${RESET} ${line}
2383
2521
  `);
2384
2522
  }
2385
2523
  }
@@ -2533,12 +2671,14 @@ Usage:
2533
2671
  numux Run processes from config file
2534
2672
  numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel
2535
2673
  numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands
2674
+ numux -w <script> Run a script across all workspaces
2536
2675
  numux init Create a starter config file
2537
2676
  numux validate Validate config and show process graph
2538
2677
  numux exec <name> [--] <cmd> Run a command in a process's environment
2539
2678
  numux completions <shell> Generate shell completions (bash, zsh, fish)
2540
2679
 
2541
2680
  Options:
2681
+ -w, --workspace <script> Run a package.json script across all workspaces
2542
2682
  -n, --name <name=command> Add a named process
2543
2683
  -c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
2544
2684
  --colors Auto-assign colors to processes based on their name
@@ -2591,8 +2731,8 @@ async function main() {
2591
2731
  process.exit(0);
2592
2732
  }
2593
2733
  if (parsed.init) {
2594
- const target = resolve7("numux.config.ts");
2595
- if (existsSync4(target)) {
2734
+ const target = resolve8("numux.config.ts");
2735
+ if (existsSync5(target)) {
2596
2736
  console.error(`Already exists: ${target}`);
2597
2737
  process.exit(1);
2598
2738
  }
@@ -2650,7 +2790,7 @@ async function main() {
2650
2790
  const names = Object.keys(config2.processes);
2651
2791
  throw new Error(`Unknown process "${parsed.execName}". Available: ${names.join(", ")}`);
2652
2792
  }
2653
- const cwd = proc.cwd ? resolve7(proc.cwd) : process.cwd();
2793
+ const cwd = proc.cwd ? resolve8(proc.cwd) : process.cwd();
2654
2794
  const envFromFile = proc.envFile ? loadEnvFiles(proc.envFile, cwd) : {};
2655
2795
  const env = {
2656
2796
  ...process.env,
@@ -2671,7 +2811,7 @@ async function main() {
2671
2811
  }
2672
2812
  let config;
2673
2813
  const warnings = [];
2674
- if (parsed.commands.length > 0 || parsed.named.length > 0) {
2814
+ if (parsed.commands.length > 0 || parsed.named.length > 0 || parsed.workspace) {
2675
2815
  const isScriptPattern = (c) => c.startsWith("npm:") || /[*?[]/.test(c);
2676
2816
  const hasNpmPatterns = parsed.commands.some(isScriptPattern);
2677
2817
  if (hasNpmPatterns) {
@@ -2702,6 +2842,21 @@ async function main() {
2702
2842
  colors: parsed.colors
2703
2843
  });
2704
2844
  }
2845
+ if (parsed.workspace) {
2846
+ const wsProcesses = resolveWorkspaceProcesses(parsed.workspace, process.cwd());
2847
+ for (const [name, proc] of Object.entries(wsProcesses)) {
2848
+ let finalName = name;
2849
+ if (config.processes[finalName]) {
2850
+ let suffix = 1;
2851
+ while (config.processes[`${finalName}-${suffix}`])
2852
+ suffix++;
2853
+ finalName = `${finalName}-${suffix}`;
2854
+ }
2855
+ if (parsed.noRestart)
2856
+ proc.maxRestarts = 0;
2857
+ config.processes[finalName] = proc;
2858
+ }
2859
+ }
2705
2860
  } else {
2706
2861
  const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
2707
2862
  config = validateConfig(raw, warnings);
@@ -2733,6 +2888,11 @@ async function main() {
2733
2888
  }
2734
2889
  printWarnings(warnings);
2735
2890
  if (parsed.prefix) {
2891
+ if (!parsed.noRestart) {
2892
+ for (const proc of Object.values(config.processes)) {
2893
+ proc.maxRestarts ??= 0;
2894
+ }
2895
+ }
2736
2896
  const display = new PrefixDisplay(manager, config, {
2737
2897
  logWriter,
2738
2898
  killOthers: parsed.killOthers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",