numux 1.26.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,6 @@ export default defineConfig({
40
40
  migrate: {
41
41
  command: 'bun run migrate',
42
42
  dependsOn: ['db'],
43
- persistent: false,
44
43
  },
45
44
  api: {
46
45
  command: 'bun run dev:api',
@@ -53,7 +52,6 @@ export default defineConfig({
53
52
  confirm: {
54
53
  command: 'sh -c "printf \'Deploy to staging? [y/n] \' && read answer && echo $answer"',
55
54
  interactive: true,
56
- persistent: false,
57
55
  },
58
56
  },
59
57
  })
@@ -168,7 +166,7 @@ Template properties (color, env, dependsOn, etc.) are inherited by all matched p
168
166
  | `--exclude <a,b,...>` | Exclude these processes |
169
167
  | `--kill-others` | Kill all processes when any exits (regardless of exit code) |
170
168
  | `--kill-others-on-fail` | Kill all processes when any exits with a non-zero exit code |
171
- | `--no-restart` | Disable auto-restart for crashed processes |
169
+ | `--max-restarts <n>` | Max auto-restarts for crashed processes |
172
170
  | `-s, --sort <mode>` | Tab display order: `config` (default), `alphabetical`, `topological` |
173
171
  | `--no-watch` | Disable file watching even if config has `watch` patterns |
174
172
  | `-t, --timestamps` | Add `[HH:MM:SS]` timestamps to prefixed output |
@@ -199,8 +197,7 @@ Top-level options apply to all processes (process-level settings override):
199
197
  | `env` | `Record<string, string>` | Environment variables merged into all processes (process `env` overrides per key) |
200
198
  | `envFile` | `string \| string[] \| false` | `.env` file(s) for all processes (process `envFile` replaces if set; `false` disables) |
201
199
  | `showCommand` | `boolean` | Print the command being run as the first line of output (default: `true`) |
202
- | `persistent` | `boolean` | Set to `false` to make all processes one-shot by default (default: `true`) |
203
- | `maxRestarts` | `number` | Restart limit for all processes (default: `Infinity`) |
200
+ | `maxRestarts` | `number` | Restart limit for all processes (default: `0`) |
204
201
  | `readyTimeout` | `number` | Ready timeout in ms for all processes |
205
202
  | `stopSignal` | `'SIGTERM' \| 'SIGINT' \| 'SIGHUP'` | Stop signal for all processes (default: `'SIGTERM'`) |
206
203
  | `errorMatcher` | `boolean \| string` | Error detection for all processes (`true` = ANSI red, string = regex) |
@@ -232,8 +229,7 @@ Each process accepts:
232
229
  | `dependsOn` | `string[]` | — | Processes that must be ready first |
233
230
  | `readyPattern` | `string \| RegExp` | — | Regex matched against stdout to signal readiness. Use `RegExp` to capture groups (see below) |
234
231
  | `readyTimeout` | `number` | — | Milliseconds to wait for `readyPattern` before failing |
235
- | `persistent` | `boolean` | `true` | `false` for one-shot commands (exit 0 = ready) |
236
- | `maxRestarts` | `number` | `Infinity` | Max auto-restart attempts before giving up |
232
+ | `maxRestarts` | `number` | `0` | Max auto-restart attempts on non-zero exit (0 = no restarts) |
237
233
  | `delay` | `number` | — | Milliseconds to wait before starting the process |
238
234
  | `condition` | `string` | — | Env var name; process skipped if falsy. Prefix with `!` to negate |
239
235
  | `platform` | `string \| string[]` | — | OS(es) this process runs on (e.g. `'darwin'`, `'linux'`). Non-matching processes are removed; dependents still start |
@@ -301,7 +297,6 @@ export default defineConfig({
301
297
  processes: {
302
298
  seed: {
303
299
  command: 'bun run seed',
304
- persistent: false,
305
300
  condition: 'SEED_DB', // only runs when SEED_DB is set and truthy
306
301
  },
307
302
  storybook: {
@@ -319,11 +314,10 @@ Falsy values: unset, empty string, `"0"`, `"false"`, `"no"`, `"off"` (case-insen
319
314
  Each process starts as soon as its declared `dependsOn` dependencies are ready — it does not wait for unrelated processes. If a process fails, its dependents are skipped.
320
315
 
321
316
  A process becomes **ready** when:
322
- - **persistent + readyPattern** — the pattern matches in stdout
323
- - **persistent + no readyPattern**immediately after spawn
324
- - **non-persistent** — exits with code 0
317
+ - **Has `readyPattern`** — the pattern matches in stdout (long-running server)
318
+ - **No `readyPattern`**exits with code 0 (one-shot task)
325
319
 
326
- Persistent processes that crash are auto-restarted with exponential backoff (1s–30s). Backoff resets after 10s of uptime.
320
+ Processes that crash (non-zero exit) can be auto-restarted by setting `maxRestarts` (default: `0`). Restarts use exponential backoff (1s–30s), which resets after 10s of uptime.
327
321
 
328
322
  ### Dependency output capture
329
323
 
package/dist/numux.js CHANGED
@@ -36,7 +36,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
36
36
  var require_package = __commonJS((exports, module) => {
37
37
  module.exports = {
38
38
  name: "numux",
39
- version: "1.26.0",
39
+ version: "2.0.1",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -72,7 +72,8 @@ var require_package = __commonJS((exports, module) => {
72
72
  test: "bun test",
73
73
  typecheck: "bunx tsc --noEmit",
74
74
  lint: "biome check .",
75
- fix: "biome check . --fix --unsafe"
75
+ fix: "biome check . --fix --unsafe",
76
+ prepare: "git config core.hooksPath .githooks"
76
77
  },
77
78
  files: [
78
79
  "dist/"
@@ -83,6 +84,8 @@ var require_package = __commonJS((exports, module) => {
83
84
  },
84
85
  devDependencies: {
85
86
  "@biomejs/biome": "^2.4.4",
87
+ "@commitlint/cli": "^20.4.2",
88
+ "@commitlint/config-conventional": "^20.4.2",
86
89
  "@types/bun": "^1.3.9"
87
90
  }
88
91
  };
@@ -201,10 +204,18 @@ var FLAGS = [
201
204
  description: "Kill all processes when any exits with non-zero code"
202
205
  },
203
206
  {
204
- type: "boolean",
205
- long: "--no-restart",
206
- key: "noRestart",
207
- description: "Disable auto-restart for crashed processes"
207
+ type: "value",
208
+ long: "--max-restarts",
209
+ key: "maxRestarts",
210
+ description: "Max auto-restarts for crashed processes",
211
+ valueName: "<n>",
212
+ completionHint: "none",
213
+ parse(raw, flag) {
214
+ const n = Number(raw);
215
+ if (!Number.isInteger(n) || n < 0)
216
+ throw new Error(`${flag} must be a non-negative integer, got "${raw}"`);
217
+ return n;
218
+ }
208
219
  },
209
220
  {
210
221
  type: "boolean",
@@ -350,7 +361,6 @@ function parseArgs(argv) {
350
361
  killOthers: false,
351
362
  killOthersOnFail: false,
352
363
  timestamps: false,
353
- noRestart: false,
354
364
  noWatch: false,
355
365
  autoColors: false,
356
366
  configPath: undefined,
@@ -395,23 +405,31 @@ function parseArgs(argv) {
395
405
  }
396
406
  return result;
397
407
  }
408
+ var RUNNERS = new Set(["npm", "npx", "yarn", "pnpm", "bun", "bunx"]);
409
+ function deriveProcessName(cmd) {
410
+ const parts = cmd.split(/\s+/);
411
+ const first = parts[0].split("/").pop();
412
+ if (parts.length > 1 && RUNNERS.has(first)) {
413
+ return parts[parts.length - 1];
414
+ }
415
+ return first;
416
+ }
398
417
  function buildConfigFromArgs(commands, named, options) {
399
418
  const processes = {};
400
- const maxRestarts = options?.noRestart ? 0 : undefined;
401
419
  const colors = options?.colors;
402
420
  let colorIndex = 0;
403
421
  for (const { name, command } of named) {
404
422
  const color = colors?.[colorIndex++ % colors.length];
405
- processes[name] = { command, persistent: true, maxRestarts, ...color ? { color } : {} };
423
+ processes[name] = { command, ...color ? { color } : {} };
406
424
  }
407
425
  for (let i = 0;i < commands.length; i++) {
408
426
  const cmd = commands[i];
409
- let name = cmd.split(/\s+/)[0].split("/").pop();
427
+ let name = deriveProcessName(cmd);
410
428
  if (processes[name]) {
411
429
  name = `${name}-${i}`;
412
430
  }
413
431
  const color = colors?.[colorIndex++ % colors.length];
414
- processes[name] = { command: cmd, persistent: true, maxRestarts, ...color ? { color } : {} };
432
+ processes[name] = { command: cmd, ...color ? { color } : {} };
415
433
  }
416
434
  return { processes };
417
435
  }
@@ -1035,7 +1053,7 @@ function buildProcessHexColorMap(names, config) {
1035
1053
  }
1036
1054
 
1037
1055
  // src/config/validator.ts
1038
- function validateConfig(raw, warnings) {
1056
+ function validateConfig(raw, _warnings) {
1039
1057
  if (!raw || typeof raw !== "object") {
1040
1058
  throw new Error("Config must be an object");
1041
1059
  }
@@ -1053,7 +1071,6 @@ function validateConfig(raw, warnings) {
1053
1071
  const globalEnvFile = validateEnvFile(config.envFile);
1054
1072
  const globalMaxRestarts = typeof config.maxRestarts === "number" && config.maxRestarts >= 0 ? config.maxRestarts : undefined;
1055
1073
  const globalReadyTimeout = typeof config.readyTimeout === "number" && config.readyTimeout > 0 ? config.readyTimeout : undefined;
1056
- const globalPersistent = typeof config.persistent === "boolean" ? config.persistent : undefined;
1057
1074
  const globalStopSignal = validateStopSignal(config.stopSignal);
1058
1075
  const globalErrorMatcher = validateErrorMatcher("(global)", config.errorMatcher);
1059
1076
  const globalWatch = validateStringOrStringArray(config.watch);
@@ -1115,7 +1132,6 @@ function validateConfig(raw, warnings) {
1115
1132
  }
1116
1133
  }
1117
1134
  }
1118
- const persistent = typeof p.persistent === "boolean" ? p.persistent : globalPersistent ?? true;
1119
1135
  const readyPattern = p.readyPattern instanceof RegExp ? p.readyPattern : typeof p.readyPattern === "string" ? p.readyPattern : undefined;
1120
1136
  if (typeof readyPattern === "string") {
1121
1137
  try {
@@ -1126,12 +1142,6 @@ function validateConfig(raw, warnings) {
1126
1142
  });
1127
1143
  }
1128
1144
  }
1129
- if (readyPattern && !persistent) {
1130
- warnings?.push({
1131
- process: name,
1132
- message: "readyPattern is ignored on non-persistent processes (readiness is determined by exit code)"
1133
- });
1134
- }
1135
1145
  if (p.env && typeof p.env === "object") {
1136
1146
  for (const [k, v] of Object.entries(p.env)) {
1137
1147
  if (typeof v !== "string") {
@@ -1156,8 +1166,7 @@ function validateConfig(raw, warnings) {
1156
1166
  envFile: processEnvFile ?? globalEnvFile,
1157
1167
  dependsOn: Array.isArray(p.dependsOn) ? p.dependsOn : undefined,
1158
1168
  readyPattern,
1159
- persistent,
1160
- maxRestarts: processMaxRestarts ?? globalMaxRestarts,
1169
+ maxRestarts: processMaxRestarts ?? globalMaxRestarts ?? 0,
1161
1170
  readyTimeout: processReadyTimeout ?? globalReadyTimeout,
1162
1171
  delay: typeof p.delay === "number" && p.delay > 0 ? p.delay : undefined,
1163
1172
  condition: typeof p.condition === "string" && p.condition.trim() ? p.condition.trim() : undefined,
@@ -1293,8 +1302,7 @@ function resolveWorkspaceProcesses(script, cwd) {
1293
1302
  usedNames.add(name);
1294
1303
  processes[name] = {
1295
1304
  command: `${pm} run ${script}`,
1296
- cwd: dir,
1297
- persistent: true
1305
+ cwd: dir
1298
1306
  };
1299
1307
  }
1300
1308
  if (Object.keys(processes).length === 0) {
@@ -1467,12 +1475,11 @@ function extractCaptures(match) {
1467
1475
  function createReadinessChecker(config) {
1468
1476
  const shouldCapture = config.readyPattern instanceof RegExp;
1469
1477
  const pattern = config.readyPattern ? config.readyPattern instanceof RegExp ? config.readyPattern : new RegExp(config.readyPattern) : null;
1470
- const persistent = config.persistent !== false;
1471
1478
  let outputBuffer = "";
1472
1479
  let _captures = null;
1473
1480
  return {
1474
1481
  feedOutput(data) {
1475
- if (!(persistent && pattern))
1482
+ if (!pattern)
1476
1483
  return false;
1477
1484
  outputBuffer += data;
1478
1485
  if (outputBuffer.length > BUFFER_CAP2) {
@@ -1491,11 +1498,8 @@ function createReadinessChecker(config) {
1491
1498
  get captures() {
1492
1499
  return _captures;
1493
1500
  },
1494
- get isImmediatelyReady() {
1495
- return persistent && !pattern;
1496
- },
1497
1501
  get dependsOnExit() {
1498
- return !persistent;
1502
+ return !pattern;
1499
1503
  }
1500
1504
  };
1501
1505
  }
@@ -1585,10 +1589,7 @@ class ProcessRunner {
1585
1589
  `;
1586
1590
  this.handler.onOutput(encoder.encode(msg));
1587
1591
  }
1588
- this.handler.onStatus(this.config.persistent !== false ? "running" : "starting");
1589
- if (this.readiness.isImmediatelyReady) {
1590
- this.markReady();
1591
- }
1592
+ this.handler.onStatus("running");
1592
1593
  this.startReadyTimeout(gen);
1593
1594
  this.proc.exited.then((code) => {
1594
1595
  if (this.generation !== gen)
@@ -1636,7 +1637,7 @@ class ProcessRunner {
1636
1637
  }
1637
1638
  startReadyTimeout(gen) {
1638
1639
  const timeout = this.config.readyTimeout;
1639
- if (!(timeout && this.config.readyPattern) || this.config.persistent === false)
1640
+ if (!(timeout && this.config.readyPattern))
1640
1641
  return;
1641
1642
  this.readyTimer = setTimeout(() => {
1642
1643
  this.readyTimer = null;
@@ -1894,8 +1895,6 @@ class ProcessManager {
1894
1895
  if (this.stopping)
1895
1896
  return;
1896
1897
  const proc = this.config.processes[name];
1897
- if (proc.persistent === false)
1898
- return;
1899
1898
  if (exitCode === 0)
1900
1899
  return;
1901
1900
  if (exitCode === null)
@@ -1906,8 +1905,8 @@ class ProcessManager {
1906
1905
  this.restartAttempts.set(name, 0);
1907
1906
  }
1908
1907
  const attempt = this.restartAttempts.get(name) ?? 0;
1909
- const maxRestarts = proc.maxRestarts;
1910
- if (maxRestarts !== undefined && attempt >= maxRestarts) {
1908
+ const maxRestarts = proc.maxRestarts ?? 0;
1909
+ if (attempt >= maxRestarts) {
1911
1910
  log(`[${name}] Reached maxRestarts limit (${maxRestarts}), not restarting`);
1912
1911
  if (maxRestarts > 0) {
1913
1912
  const encoder2 = new TextEncoder;
@@ -1922,7 +1921,7 @@ class ProcessManager {
1922
1921
  this.restartAttempts.set(name, attempt + 1);
1923
1922
  const encoder = new TextEncoder;
1924
1923
  const msg = `\r
1925
- \x1B[33m[numux] restarting in ${(delay / 1000).toFixed(0)}s (attempt ${attempt + 1}${maxRestarts !== undefined ? `/${maxRestarts}` : ""})...\x1B[0m\r
1924
+ \x1B[33m[numux] restarting in ${(delay / 1000).toFixed(0)}s (attempt ${attempt + 1}${Number.isFinite(maxRestarts) ? `/${maxRestarts}` : ""})...\x1B[0m\r
1926
1925
  `;
1927
1926
  this.emit({ type: "output", name, data: encoder.encode(msg) });
1928
1927
  const timer = setTimeout(() => {
@@ -1953,7 +1952,7 @@ class ProcessManager {
1953
1952
  const state = this.states.get(name);
1954
1953
  if (!state)
1955
1954
  return;
1956
- if (state.status === "pending" || state.status === "stopped" || state.status === "finished" || state.status === "stopping" || state.status === "skipped")
1955
+ if (state.status === "pending" || state.status === "stopped" || state.status === "stopping" || state.status === "skipped")
1957
1956
  return;
1958
1957
  log(`[${name}] File changed: ${changedFile}, restarting`);
1959
1958
  const msg = `\r
@@ -2196,10 +2195,14 @@ function openLink(link) {
2196
2195
  }
2197
2196
 
2198
2197
  // src/ui/pane.ts
2198
+ var MAX_SCROLLBACK_LINES = 50000;
2199
+ var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
2200
+
2199
2201
  class Pane {
2200
2202
  scrollBox;
2201
2203
  terminal;
2202
2204
  decoder = new TextDecoder;
2205
+ bytesFed = 0;
2203
2206
  _onScroll = null;
2204
2207
  _onCopy = null;
2205
2208
  _onLinkClick = null;
@@ -2247,6 +2250,11 @@ class Pane {
2247
2250
  this.scrollBox.add(this.terminal);
2248
2251
  }
2249
2252
  feed(data) {
2253
+ this.bytesFed += data.length;
2254
+ if (this.terminal.lineCount > MAX_SCROLLBACK_LINES || this.bytesFed > MAX_BUFFER_BYTES) {
2255
+ this.terminal.reset();
2256
+ this.bytesFed = 0;
2257
+ }
2250
2258
  const text = this.decoder.decode(data, { stream: true });
2251
2259
  this.terminal.feed(text);
2252
2260
  }
@@ -2324,6 +2332,7 @@ class Pane {
2324
2332
  }
2325
2333
  clear() {
2326
2334
  this.terminal.reset();
2335
+ this.bytesFed = 0;
2327
2336
  }
2328
2337
  destroy() {
2329
2338
  this.terminal.destroy();
@@ -3674,7 +3683,7 @@ async function main() {
3674
3683
  flags.push(`depends on: ${proc.dependsOn.join(", ")}`);
3675
3684
  if (proc.readyPattern)
3676
3685
  flags.push(`ready: /${proc.readyPattern}/`);
3677
- if (proc.persistent === false)
3686
+ if (!proc.readyPattern)
3678
3687
  flags.push("one-shot");
3679
3688
  if (proc.delay)
3680
3689
  flags.push(`delay: ${proc.delay}ms`);
@@ -3739,7 +3748,7 @@ async function main() {
3739
3748
  }
3740
3749
  for (let i = 0;i < otherCommands.length; i++) {
3741
3750
  const cmd = otherCommands[i];
3742
- let name = cmd.split(/\s+/)[0].split("/").pop();
3751
+ let name = deriveProcessName(cmd);
3743
3752
  if (processes[name])
3744
3753
  name = `${name}-${i}`;
3745
3754
  processes[name] = cmd;
@@ -3751,7 +3760,6 @@ async function main() {
3751
3760
  config = validateConfig(expanded, warnings);
3752
3761
  } else {
3753
3762
  config = buildConfigFromArgs(parsed.commands, parsed.named, {
3754
- noRestart: parsed.noRestart,
3755
3763
  colors: parsed.colors
3756
3764
  });
3757
3765
  }
@@ -3765,8 +3773,6 @@ async function main() {
3765
3773
  suffix++;
3766
3774
  finalName = `${finalName}-${suffix}`;
3767
3775
  }
3768
- if (parsed.noRestart)
3769
- proc.maxRestarts = 0;
3770
3776
  config.processes[finalName] = proc;
3771
3777
  }
3772
3778
  }
@@ -3774,11 +3780,6 @@ async function main() {
3774
3780
  const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
3775
3781
  config = validateConfig(raw, warnings);
3776
3782
  config = filterByPlatform(config);
3777
- if (parsed.noRestart) {
3778
- for (const proc of Object.values(config.processes)) {
3779
- proc.maxRestarts = 0;
3780
- }
3781
- }
3782
3783
  }
3783
3784
  if (parsed.sort) {
3784
3785
  config.sort = parsed.sort;
@@ -3788,6 +3789,11 @@ async function main() {
3788
3789
  proc.envFile = parsed.envFile;
3789
3790
  }
3790
3791
  }
3792
+ if (parsed.maxRestarts !== undefined) {
3793
+ for (const proc of Object.values(config.processes)) {
3794
+ proc.maxRestarts = parsed.maxRestarts;
3795
+ }
3796
+ }
3791
3797
  if (parsed.noWatch || config.noWatch) {
3792
3798
  for (const proc of Object.values(config.processes)) {
3793
3799
  delete proc.watch;
@@ -3809,11 +3815,6 @@ async function main() {
3809
3815
  printWarnings(warnings);
3810
3816
  const usePrefix = parsed.prefix || config.prefix;
3811
3817
  if (usePrefix) {
3812
- if (!parsed.noRestart) {
3813
- for (const proc of Object.values(config.processes)) {
3814
- proc.maxRestarts ??= 0;
3815
- }
3816
- }
3817
3818
  const display = new PrefixDisplay(manager, config, {
3818
3819
  logWriter,
3819
3820
  killOthers: parsed.killOthers || config.killOthers,
package/dist/types.d.ts CHANGED
@@ -17,13 +17,8 @@ export interface NumuxProcessConfig<K extends string = string> {
17
17
  /** Regex matched against stdout to signal readiness. Use `RegExp` to capture groups for `$dep.group` expansion */
18
18
  readyPattern?: string | RegExp;
19
19
  /**
20
- * Set to `false` for one-shot processes
21
- * @default true
22
- */
23
- persistent?: boolean;
24
- /**
25
- * Limit auto-restart attempts
26
- * @default Infinity
20
+ * Limit auto-restart attempts (only restarts on non-zero exit)
21
+ * @default 0
27
22
  */
28
23
  maxRestarts?: number;
29
24
  /** Milliseconds to wait for readyPattern before failing */
@@ -74,18 +69,12 @@ export interface NumuxConfig<K extends string = string> {
74
69
  */
75
70
  showCommand?: boolean;
76
71
  /**
77
- * Global restart limit, inherited by all processes
78
- * @default Infinity
72
+ * Global restart limit, inherited by all processes (only restarts on non-zero exit)
73
+ * @default 0
79
74
  */
80
75
  maxRestarts?: number;
81
76
  /** Global ready timeout (ms), inherited by all processes */
82
77
  readyTimeout?: number;
83
- /**
84
- * Set to `false` to make all processes non-persistent (one-shot) by default.
85
- * Individual processes can still override with their own `persistent` value.
86
- * @default true
87
- */
88
- persistent?: boolean;
89
78
  /**
90
79
  * Global stop signal, inherited by all processes
91
80
  * @default 'SIGTERM'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.26.0",
3
+ "version": "2.0.1",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,8 @@
36
36
  "test": "bun test",
37
37
  "typecheck": "bunx tsc --noEmit",
38
38
  "lint": "biome check .",
39
- "fix": "biome check . --fix --unsafe"
39
+ "fix": "biome check . --fix --unsafe",
40
+ "prepare": "git config core.hooksPath .githooks"
40
41
  },
41
42
  "files": [
42
43
  "dist/"
@@ -47,6 +48,8 @@
47
48
  },
48
49
  "devDependencies": {
49
50
  "@biomejs/biome": "^2.4.4",
51
+ "@commitlint/cli": "^20.4.2",
52
+ "@commitlint/config-conventional": "^20.4.2",
50
53
  "@types/bun": "^1.3.9"
51
54
  }
52
55
  }