numux 1.26.0 → 2.0.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
@@ -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.0",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -201,10 +201,18 @@ var FLAGS = [
201
201
  description: "Kill all processes when any exits with non-zero code"
202
202
  },
203
203
  {
204
- type: "boolean",
205
- long: "--no-restart",
206
- key: "noRestart",
207
- description: "Disable auto-restart for crashed processes"
204
+ type: "value",
205
+ long: "--max-restarts",
206
+ key: "maxRestarts",
207
+ description: "Max auto-restarts for crashed processes",
208
+ valueName: "<n>",
209
+ completionHint: "none",
210
+ parse(raw, flag) {
211
+ const n = Number(raw);
212
+ if (!Number.isInteger(n) || n < 0)
213
+ throw new Error(`${flag} must be a non-negative integer, got "${raw}"`);
214
+ return n;
215
+ }
208
216
  },
209
217
  {
210
218
  type: "boolean",
@@ -350,7 +358,6 @@ function parseArgs(argv) {
350
358
  killOthers: false,
351
359
  killOthersOnFail: false,
352
360
  timestamps: false,
353
- noRestart: false,
354
361
  noWatch: false,
355
362
  autoColors: false,
356
363
  configPath: undefined,
@@ -397,12 +404,11 @@ function parseArgs(argv) {
397
404
  }
398
405
  function buildConfigFromArgs(commands, named, options) {
399
406
  const processes = {};
400
- const maxRestarts = options?.noRestart ? 0 : undefined;
401
407
  const colors = options?.colors;
402
408
  let colorIndex = 0;
403
409
  for (const { name, command } of named) {
404
410
  const color = colors?.[colorIndex++ % colors.length];
405
- processes[name] = { command, persistent: true, maxRestarts, ...color ? { color } : {} };
411
+ processes[name] = { command, ...color ? { color } : {} };
406
412
  }
407
413
  for (let i = 0;i < commands.length; i++) {
408
414
  const cmd = commands[i];
@@ -411,7 +417,7 @@ function buildConfigFromArgs(commands, named, options) {
411
417
  name = `${name}-${i}`;
412
418
  }
413
419
  const color = colors?.[colorIndex++ % colors.length];
414
- processes[name] = { command: cmd, persistent: true, maxRestarts, ...color ? { color } : {} };
420
+ processes[name] = { command: cmd, ...color ? { color } : {} };
415
421
  }
416
422
  return { processes };
417
423
  }
@@ -1035,7 +1041,7 @@ function buildProcessHexColorMap(names, config) {
1035
1041
  }
1036
1042
 
1037
1043
  // src/config/validator.ts
1038
- function validateConfig(raw, warnings) {
1044
+ function validateConfig(raw, _warnings) {
1039
1045
  if (!raw || typeof raw !== "object") {
1040
1046
  throw new Error("Config must be an object");
1041
1047
  }
@@ -1053,7 +1059,6 @@ function validateConfig(raw, warnings) {
1053
1059
  const globalEnvFile = validateEnvFile(config.envFile);
1054
1060
  const globalMaxRestarts = typeof config.maxRestarts === "number" && config.maxRestarts >= 0 ? config.maxRestarts : undefined;
1055
1061
  const globalReadyTimeout = typeof config.readyTimeout === "number" && config.readyTimeout > 0 ? config.readyTimeout : undefined;
1056
- const globalPersistent = typeof config.persistent === "boolean" ? config.persistent : undefined;
1057
1062
  const globalStopSignal = validateStopSignal(config.stopSignal);
1058
1063
  const globalErrorMatcher = validateErrorMatcher("(global)", config.errorMatcher);
1059
1064
  const globalWatch = validateStringOrStringArray(config.watch);
@@ -1115,7 +1120,6 @@ function validateConfig(raw, warnings) {
1115
1120
  }
1116
1121
  }
1117
1122
  }
1118
- const persistent = typeof p.persistent === "boolean" ? p.persistent : globalPersistent ?? true;
1119
1123
  const readyPattern = p.readyPattern instanceof RegExp ? p.readyPattern : typeof p.readyPattern === "string" ? p.readyPattern : undefined;
1120
1124
  if (typeof readyPattern === "string") {
1121
1125
  try {
@@ -1126,12 +1130,6 @@ function validateConfig(raw, warnings) {
1126
1130
  });
1127
1131
  }
1128
1132
  }
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
1133
  if (p.env && typeof p.env === "object") {
1136
1134
  for (const [k, v] of Object.entries(p.env)) {
1137
1135
  if (typeof v !== "string") {
@@ -1156,8 +1154,7 @@ function validateConfig(raw, warnings) {
1156
1154
  envFile: processEnvFile ?? globalEnvFile,
1157
1155
  dependsOn: Array.isArray(p.dependsOn) ? p.dependsOn : undefined,
1158
1156
  readyPattern,
1159
- persistent,
1160
- maxRestarts: processMaxRestarts ?? globalMaxRestarts,
1157
+ maxRestarts: processMaxRestarts ?? globalMaxRestarts ?? 0,
1161
1158
  readyTimeout: processReadyTimeout ?? globalReadyTimeout,
1162
1159
  delay: typeof p.delay === "number" && p.delay > 0 ? p.delay : undefined,
1163
1160
  condition: typeof p.condition === "string" && p.condition.trim() ? p.condition.trim() : undefined,
@@ -1293,8 +1290,7 @@ function resolveWorkspaceProcesses(script, cwd) {
1293
1290
  usedNames.add(name);
1294
1291
  processes[name] = {
1295
1292
  command: `${pm} run ${script}`,
1296
- cwd: dir,
1297
- persistent: true
1293
+ cwd: dir
1298
1294
  };
1299
1295
  }
1300
1296
  if (Object.keys(processes).length === 0) {
@@ -1467,12 +1463,11 @@ function extractCaptures(match) {
1467
1463
  function createReadinessChecker(config) {
1468
1464
  const shouldCapture = config.readyPattern instanceof RegExp;
1469
1465
  const pattern = config.readyPattern ? config.readyPattern instanceof RegExp ? config.readyPattern : new RegExp(config.readyPattern) : null;
1470
- const persistent = config.persistent !== false;
1471
1466
  let outputBuffer = "";
1472
1467
  let _captures = null;
1473
1468
  return {
1474
1469
  feedOutput(data) {
1475
- if (!(persistent && pattern))
1470
+ if (!pattern)
1476
1471
  return false;
1477
1472
  outputBuffer += data;
1478
1473
  if (outputBuffer.length > BUFFER_CAP2) {
@@ -1491,11 +1486,8 @@ function createReadinessChecker(config) {
1491
1486
  get captures() {
1492
1487
  return _captures;
1493
1488
  },
1494
- get isImmediatelyReady() {
1495
- return persistent && !pattern;
1496
- },
1497
1489
  get dependsOnExit() {
1498
- return !persistent;
1490
+ return !pattern;
1499
1491
  }
1500
1492
  };
1501
1493
  }
@@ -1585,10 +1577,7 @@ class ProcessRunner {
1585
1577
  `;
1586
1578
  this.handler.onOutput(encoder.encode(msg));
1587
1579
  }
1588
- this.handler.onStatus(this.config.persistent !== false ? "running" : "starting");
1589
- if (this.readiness.isImmediatelyReady) {
1590
- this.markReady();
1591
- }
1580
+ this.handler.onStatus(this.config.readyPattern ? "running" : "starting");
1592
1581
  this.startReadyTimeout(gen);
1593
1582
  this.proc.exited.then((code) => {
1594
1583
  if (this.generation !== gen)
@@ -1636,7 +1625,7 @@ class ProcessRunner {
1636
1625
  }
1637
1626
  startReadyTimeout(gen) {
1638
1627
  const timeout = this.config.readyTimeout;
1639
- if (!(timeout && this.config.readyPattern) || this.config.persistent === false)
1628
+ if (!(timeout && this.config.readyPattern))
1640
1629
  return;
1641
1630
  this.readyTimer = setTimeout(() => {
1642
1631
  this.readyTimer = null;
@@ -1894,8 +1883,6 @@ class ProcessManager {
1894
1883
  if (this.stopping)
1895
1884
  return;
1896
1885
  const proc = this.config.processes[name];
1897
- if (proc.persistent === false)
1898
- return;
1899
1886
  if (exitCode === 0)
1900
1887
  return;
1901
1888
  if (exitCode === null)
@@ -1906,8 +1893,8 @@ class ProcessManager {
1906
1893
  this.restartAttempts.set(name, 0);
1907
1894
  }
1908
1895
  const attempt = this.restartAttempts.get(name) ?? 0;
1909
- const maxRestarts = proc.maxRestarts;
1910
- if (maxRestarts !== undefined && attempt >= maxRestarts) {
1896
+ const maxRestarts = proc.maxRestarts ?? 0;
1897
+ if (attempt >= maxRestarts) {
1911
1898
  log(`[${name}] Reached maxRestarts limit (${maxRestarts}), not restarting`);
1912
1899
  if (maxRestarts > 0) {
1913
1900
  const encoder2 = new TextEncoder;
@@ -1922,7 +1909,7 @@ class ProcessManager {
1922
1909
  this.restartAttempts.set(name, attempt + 1);
1923
1910
  const encoder = new TextEncoder;
1924
1911
  const msg = `\r
1925
- \x1B[33m[numux] restarting in ${(delay / 1000).toFixed(0)}s (attempt ${attempt + 1}${maxRestarts !== undefined ? `/${maxRestarts}` : ""})...\x1B[0m\r
1912
+ \x1B[33m[numux] restarting in ${(delay / 1000).toFixed(0)}s (attempt ${attempt + 1}${Number.isFinite(maxRestarts) ? `/${maxRestarts}` : ""})...\x1B[0m\r
1926
1913
  `;
1927
1914
  this.emit({ type: "output", name, data: encoder.encode(msg) });
1928
1915
  const timer = setTimeout(() => {
@@ -1953,7 +1940,7 @@ class ProcessManager {
1953
1940
  const state = this.states.get(name);
1954
1941
  if (!state)
1955
1942
  return;
1956
- if (state.status === "pending" || state.status === "stopped" || state.status === "finished" || state.status === "stopping" || state.status === "skipped")
1943
+ if (state.status === "pending" || state.status === "stopped" || state.status === "stopping" || state.status === "skipped")
1957
1944
  return;
1958
1945
  log(`[${name}] File changed: ${changedFile}, restarting`);
1959
1946
  const msg = `\r
@@ -2196,10 +2183,14 @@ function openLink(link) {
2196
2183
  }
2197
2184
 
2198
2185
  // src/ui/pane.ts
2186
+ var MAX_SCROLLBACK_LINES = 50000;
2187
+ var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
2188
+
2199
2189
  class Pane {
2200
2190
  scrollBox;
2201
2191
  terminal;
2202
2192
  decoder = new TextDecoder;
2193
+ bytesFed = 0;
2203
2194
  _onScroll = null;
2204
2195
  _onCopy = null;
2205
2196
  _onLinkClick = null;
@@ -2247,6 +2238,11 @@ class Pane {
2247
2238
  this.scrollBox.add(this.terminal);
2248
2239
  }
2249
2240
  feed(data) {
2241
+ this.bytesFed += data.length;
2242
+ if (this.terminal.lineCount > MAX_SCROLLBACK_LINES || this.bytesFed > MAX_BUFFER_BYTES) {
2243
+ this.terminal.reset();
2244
+ this.bytesFed = 0;
2245
+ }
2250
2246
  const text = this.decoder.decode(data, { stream: true });
2251
2247
  this.terminal.feed(text);
2252
2248
  }
@@ -2324,6 +2320,7 @@ class Pane {
2324
2320
  }
2325
2321
  clear() {
2326
2322
  this.terminal.reset();
2323
+ this.bytesFed = 0;
2327
2324
  }
2328
2325
  destroy() {
2329
2326
  this.terminal.destroy();
@@ -3674,7 +3671,7 @@ async function main() {
3674
3671
  flags.push(`depends on: ${proc.dependsOn.join(", ")}`);
3675
3672
  if (proc.readyPattern)
3676
3673
  flags.push(`ready: /${proc.readyPattern}/`);
3677
- if (proc.persistent === false)
3674
+ if (!proc.readyPattern)
3678
3675
  flags.push("one-shot");
3679
3676
  if (proc.delay)
3680
3677
  flags.push(`delay: ${proc.delay}ms`);
@@ -3751,7 +3748,6 @@ async function main() {
3751
3748
  config = validateConfig(expanded, warnings);
3752
3749
  } else {
3753
3750
  config = buildConfigFromArgs(parsed.commands, parsed.named, {
3754
- noRestart: parsed.noRestart,
3755
3751
  colors: parsed.colors
3756
3752
  });
3757
3753
  }
@@ -3765,8 +3761,6 @@ async function main() {
3765
3761
  suffix++;
3766
3762
  finalName = `${finalName}-${suffix}`;
3767
3763
  }
3768
- if (parsed.noRestart)
3769
- proc.maxRestarts = 0;
3770
3764
  config.processes[finalName] = proc;
3771
3765
  }
3772
3766
  }
@@ -3774,11 +3768,6 @@ async function main() {
3774
3768
  const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
3775
3769
  config = validateConfig(raw, warnings);
3776
3770
  config = filterByPlatform(config);
3777
- if (parsed.noRestart) {
3778
- for (const proc of Object.values(config.processes)) {
3779
- proc.maxRestarts = 0;
3780
- }
3781
- }
3782
3771
  }
3783
3772
  if (parsed.sort) {
3784
3773
  config.sort = parsed.sort;
@@ -3788,6 +3777,11 @@ async function main() {
3788
3777
  proc.envFile = parsed.envFile;
3789
3778
  }
3790
3779
  }
3780
+ if (parsed.maxRestarts !== undefined) {
3781
+ for (const proc of Object.values(config.processes)) {
3782
+ proc.maxRestarts = parsed.maxRestarts;
3783
+ }
3784
+ }
3791
3785
  if (parsed.noWatch || config.noWatch) {
3792
3786
  for (const proc of Object.values(config.processes)) {
3793
3787
  delete proc.watch;
@@ -3809,11 +3803,6 @@ async function main() {
3809
3803
  printWarnings(warnings);
3810
3804
  const usePrefix = parsed.prefix || config.prefix;
3811
3805
  if (usePrefix) {
3812
- if (!parsed.noRestart) {
3813
- for (const proc of Object.values(config.processes)) {
3814
- proc.maxRestarts ??= 0;
3815
- }
3816
- }
3817
3806
  const display = new PrefixDisplay(manager, config, {
3818
3807
  logWriter,
3819
3808
  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.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",