numux 1.5.1 → 1.5.2

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/dist/bin.js +20 -2608
  2. package/dist/numux.js +2610 -0
  3. package/package.json +3 -4
package/dist/bin.js CHANGED
@@ -1,2611 +1,23 @@
1
- #!/usr/bin/env bun
2
- // @bun
3
- var __create = Object.create;
4
- var __getProtoOf = Object.getPrototypeOf;
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __toESM = (mod, isNodeMode, target) => {
9
- target = mod != null ? __create(__getProtoOf(mod)) : {};
10
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
- for (let key of __getOwnPropNames(mod))
12
- if (!__hasOwnProp.call(to, key))
13
- __defProp(to, key, {
14
- get: () => mod[key],
15
- enumerable: true
16
- });
17
- return to;
18
- };
19
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
-
21
- // package.json
22
- var require_package = __commonJS((exports, module) => {
23
- module.exports = {
24
- name: "numux",
25
- version: "1.5.1",
26
- description: "Terminal multiplexer with dependency orchestration",
27
- type: "module",
28
- license: "MIT",
29
- author: "hyldmo",
30
- repository: {
31
- type: "git",
32
- url: "https://github.com/hyldmo/numux.git"
33
- },
34
- keywords: [
35
- "terminal",
36
- "multiplexer",
37
- "process-manager",
38
- "tui",
39
- "dev-tools",
40
- "orchestration"
41
- ],
42
- engines: {
43
- bun: ">=1.0",
44
- node: ">=18"
45
- },
46
- bin: {
47
- numux: "dist/bin.js"
48
- },
49
- exports: {
50
- ".": {
51
- types: "./dist/config.d.ts",
52
- import: "./dist/config.js"
53
- }
54
- },
55
- scripts: {
56
- build: "bun build src/index.ts --outfile dist/bin.js --target bun --packages external && bun build src/config.ts --outfile dist/config.js --packages external && bunx tsc src/config.ts src/types.ts --emitDeclarationOnly --declaration --outDir dist --target ESNext --module ESNext --moduleResolution bundler",
57
- prepublishOnly: "bun run build",
58
- dev: "cd example && bun run dev --debug",
59
- test: "bun test",
60
- typecheck: "bunx tsc --noEmit",
61
- lint: "biome check .",
62
- fix: "biome check . --fix --unsafe"
63
- },
64
- files: [
65
- "dist/"
66
- ],
67
- dependencies: {
68
- "@opentui/core": "^0.1.81",
69
- "ghostty-opentui": "^1.4.3"
70
- },
71
- devDependencies: {
72
- "@biomejs/biome": "^2.4.4",
73
- "@types/bun": "^1.3.9"
74
- }
75
- };
76
- });
77
-
78
- // src/index.ts
79
- import { existsSync as existsSync4, writeFileSync } from "fs";
80
- import { resolve as resolve7 } from "path";
81
-
82
- // src/cli.ts
83
- function parseArgs(argv) {
84
- const result = {
85
- help: false,
86
- version: false,
87
- debug: false,
88
- init: false,
89
- validate: false,
90
- exec: false,
91
- prefix: false,
92
- killOthers: false,
93
- timestamps: false,
94
- noRestart: false,
95
- noWatch: false,
96
- configPath: undefined,
97
- commands: [],
98
- named: []
99
- };
100
- const args = argv.slice(2);
101
- 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
- while (i < args.length) {
110
- 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 === "--config") {
128
- result.configPath = consumeValue(arg);
129
- } else if (arg === "-c" || arg === "--color") {
130
- result.colors = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
131
- } else if (arg === "--log-dir") {
132
- result.logDir = consumeValue(arg);
133
- } else if (arg === "--only") {
134
- result.only = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
135
- } else if (arg === "--exclude") {
136
- result.exclude = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
137
- } else if (arg === "-n" || arg === "--name") {
138
- const value = consumeValue(arg);
139
- const eq = value.indexOf("=");
140
- if (eq < 1) {
141
- throw new Error(`Invalid --name value: expected "name=command", got "${value}"`);
142
- }
143
- result.named.push({
144
- name: value.slice(0, eq),
145
- command: value.slice(eq + 1)
146
- });
147
- } else if (arg === "init" && result.commands.length === 0) {
148
- result.init = true;
149
- } else if (arg === "validate" && result.commands.length === 0) {
150
- result.validate = true;
151
- } else if (arg === "exec" && result.commands.length === 0) {
152
- result.exec = true;
153
- const name = args[++i];
154
- if (!name)
155
- throw new Error("exec requires a process name");
156
- result.execName = name;
157
- if (args[i + 1] === "--")
158
- i++;
159
- const rest = args.slice(i + 1);
160
- if (rest.length === 0)
161
- throw new Error("exec requires a command to run");
162
- result.execCommand = rest.join(" ");
163
- break;
164
- } else if (arg === "completions" && result.commands.length === 0) {
165
- result.completions = consumeValue(arg);
166
- } else if (!arg.startsWith("-")) {
167
- result.commands.push(arg);
168
- } else {
169
- throw new Error(`Unknown option: ${arg}`);
170
- }
171
- i++;
172
- }
173
- return result;
174
- }
175
- function buildConfigFromArgs(commands, named, options) {
176
- const processes = {};
177
- const maxRestarts = options?.noRestart ? 0 : undefined;
178
- const colors = options?.colors;
179
- let colorIndex = 0;
180
- for (const { name, command } of named) {
181
- const color = colors?.[colorIndex++ % colors.length];
182
- processes[name] = { command, persistent: true, maxRestarts, ...color ? { color } : {} };
183
- }
184
- for (let i = 0;i < commands.length; i++) {
185
- const cmd = commands[i];
186
- let name = cmd.split(/\s+/)[0].split("/").pop();
187
- if (processes[name]) {
188
- name = `${name}-${i}`;
189
- }
190
- const color = colors?.[colorIndex++ % colors.length];
191
- processes[name] = { command: cmd, persistent: true, maxRestarts, ...color ? { color } : {} };
192
- }
193
- return { processes };
194
- }
195
- function filterConfig(config, only, exclude) {
196
- const allNames = Object.keys(config.processes);
197
- let selected;
198
- if (only && only.length > 0) {
199
- for (const name of only) {
200
- if (!allNames.includes(name)) {
201
- throw new Error(`--only: unknown process "${name}"`);
202
- }
203
- }
204
- selected = new Set;
205
- const queue = [...only];
206
- while (queue.length > 0) {
207
- const name = queue.pop();
208
- if (selected.has(name))
209
- continue;
210
- selected.add(name);
211
- const deps = config.processes[name].dependsOn ?? [];
212
- for (const dep of deps) {
213
- if (!selected.has(dep))
214
- queue.push(dep);
215
- }
216
- }
217
- } else {
218
- selected = new Set(allNames);
219
- }
220
- if (exclude && exclude.length > 0) {
221
- for (const name of exclude) {
222
- if (!allNames.includes(name)) {
223
- throw new Error(`--exclude: unknown process "${name}"`);
224
- }
225
- selected.delete(name);
226
- }
227
- }
228
- if (selected.size === 0) {
229
- throw new Error("No processes left after filtering");
230
- }
231
- const processes = {};
232
- for (const name of selected) {
233
- const proc = { ...config.processes[name] };
234
- if (proc.dependsOn) {
235
- proc.dependsOn = proc.dependsOn.filter((d) => selected.has(d));
236
- if (proc.dependsOn.length === 0)
237
- proc.dependsOn = undefined;
238
- }
239
- processes[name] = proc;
240
- }
241
- return { processes };
242
- }
243
-
244
- // src/completions.ts
245
- var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
246
- function generateCompletions(shell) {
247
- switch (shell) {
248
- case "bash":
249
- return bashCompletions();
250
- case "zsh":
251
- return zshCompletions();
252
- case "fish":
253
- return fishCompletions();
254
- default:
255
- throw new Error(`Unknown shell: "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
256
- }
257
- }
258
- function bashCompletions() {
259
- return `# numux bash completions
260
- # Add to ~/.bashrc: eval "$(numux completions bash)"
261
- _numux() {
262
- local cur prev
263
- cur="\${COMP_WORDS[COMP_CWORD]}"
264
- prev="\${COMP_WORDS[COMP_CWORD-1]}"
265
-
266
- case "$prev" in
267
- --config)
268
- COMPREPLY=( $(compgen -f -- "$cur") )
269
- return ;;
270
- --log-dir)
271
- COMPREPLY=( $(compgen -d -- "$cur") )
272
- return ;;
273
- --only|--exclude)
274
- return ;;
275
- -n|--name)
276
- return ;;
277
- completions)
278
- COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
279
- return ;;
280
- esac
281
-
282
- if [[ "$cur" == -* ]]; then
283
- COMPREPLY=( $(compgen -W "-h --help -v --version -c --color --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
284
- else
285
- local subcmds="init validate exec completions"
286
- COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
287
- fi
288
- }
289
- complete -F _numux numux`;
290
- }
291
- function zshCompletions() {
292
- return `#compdef numux
293
- # numux zsh completions
294
- # Add to ~/.zshrc: eval "$(numux completions zsh)"
295
- _numux() {
296
- local -a subcmds
297
- subcmds=(
298
- 'init:Create a starter config file'
299
- 'validate:Validate config and show process graph'
300
- 'exec:Run a command in a process environment'
301
- 'completions:Generate shell completions'
302
- )
303
-
304
- _arguments -s \\
305
- '(-h --help)'{-h,--help}'[Show help]' \\
306
- '(-v --version)'{-v,--version}'[Show version]' \\
307
- '(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
308
- '--config[Config file path]:file:_files' \\
309
- '(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
310
- '(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
311
- '--only[Only run these processes]:processes' \\
312
- '--exclude[Exclude these processes]:processes' \\
313
- '--kill-others[Kill all when any exits]' \\
314
- '--no-restart[Disable auto-restart]' \\
315
- '--no-watch[Disable file watching]' \\
316
- '(-t --timestamps)'{-t,--timestamps}'[Add timestamps to output]' \\
317
- '--log-dir[Log directory]:directory:_directories' \\
318
- '--debug[Enable debug logging]' \\
319
- '1:subcommand:->subcmd' \\
320
- '*:command' \\
321
- && return
322
-
323
- case "$state" in
324
- subcmd)
325
- _describe 'subcommand' subcmds
326
- ;;
327
- esac
328
- }
329
- _numux`;
330
- }
331
- function fishCompletions() {
332
- return `# numux fish completions
333
- # Add to fish: numux completions fish | source
334
- # Or save to: ~/.config/fish/completions/numux.fish
335
- complete -c numux -f
336
-
337
- # Subcommands
338
- complete -c numux -n __fish_use_subcommand -a init -d 'Create a starter config file'
339
- complete -c numux -n __fish_use_subcommand -a validate -d 'Validate config and show process graph'
340
- complete -c numux -n __fish_use_subcommand -a exec -d 'Run a command in a process environment'
341
- complete -c numux -n __fish_use_subcommand -a completions -d 'Generate shell completions'
342
-
343
- # Completions subcommand
344
- complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'
345
-
346
- # Options
347
- complete -c numux -s h -l help -d 'Show help'
348
- complete -c numux -s v -l version -d 'Show version'
349
- complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
350
- complete -c numux -l config -rF -d 'Config file path'
351
- complete -c numux -s n -l name -r -d 'Named process (name=command)'
352
- complete -c numux -s p -l prefix -d 'Prefixed output mode'
353
- complete -c numux -l only -r -d 'Only run these processes'
354
- complete -c numux -l exclude -r -d 'Exclude these processes'
355
- complete -c numux -l kill-others -d 'Kill all when any exits'
356
- complete -c numux -l no-restart -d 'Disable auto-restart'
357
- complete -c numux -l no-watch -d 'Disable file watching'
358
- complete -c numux -s t -l timestamps -d 'Add timestamps to output'
359
- complete -c numux -l log-dir -ra '(__fish_complete_directories)' -d 'Log directory'
360
- complete -c numux -l debug -d 'Enable debug logging'`;
361
- }
362
-
363
- // src/config/expand-scripts.ts
364
- import { existsSync, readFileSync } from "fs";
365
- import { resolve } from "path";
366
- var LOCKFILE_PM = [
367
- ["bun.lockb", "bun"],
368
- ["bun.lock", "bun"],
369
- ["yarn.lock", "yarn"],
370
- ["pnpm-lock.yaml", "pnpm"],
371
- ["package-lock.json", "npm"]
372
- ];
373
- function detectPackageManager(pkgJson, cwd) {
374
- const field = pkgJson.packageManager;
375
- if (typeof field === "string") {
376
- const name = field.split("@")[0];
377
- if (["npm", "yarn", "pnpm", "bun"].includes(name))
378
- return name;
379
- }
380
- for (const [file, pm] of LOCKFILE_PM) {
381
- if (existsSync(resolve(cwd, file)))
382
- return pm;
383
- }
384
- return "npm";
385
- }
386
- function expandScriptPatterns(config, cwd) {
387
- const entries = Object.entries(config.processes);
388
- const hasWildcard = entries.some(([name]) => name.startsWith("npm:"));
389
- if (!hasWildcard)
390
- return config;
391
- const dir = config.cwd ?? cwd ?? process.cwd();
392
- const pkgPath = resolve(dir, "package.json");
393
- if (!existsSync(pkgPath)) {
394
- throw new Error(`npm: patterns require a package.json (looked in ${dir})`);
395
- }
396
- const pkgJson = JSON.parse(readFileSync(pkgPath, "utf-8"));
397
- const scripts = pkgJson.scripts;
398
- if (!scripts || typeof scripts !== "object") {
399
- throw new Error('package.json has no "scripts" field');
400
- }
401
- const scriptNames = Object.keys(scripts);
402
- const pm = detectPackageManager(pkgJson, dir);
403
- const expanded = {};
404
- for (const [name, value] of entries) {
405
- if (!name.startsWith("npm:")) {
406
- expanded[name] = value;
407
- continue;
408
- }
409
- const pattern = name.slice(4);
410
- const template = value ?? {};
411
- if (template.command) {
412
- throw new Error(`"${name}": wildcard processes cannot have a "command" field (commands come from package.json scripts)`);
413
- }
414
- const glob = new Bun.Glob(pattern);
415
- const matches = scriptNames.filter((s) => glob.match(s));
416
- if (matches.length === 0) {
417
- throw new Error(`"${name}": no scripts matched pattern "${pattern}". Available scripts: ${scriptNames.join(", ")}`);
418
- }
419
- const colors = Array.isArray(template.color) ? template.color : undefined;
420
- const singleColor = typeof template.color === "string" ? template.color : undefined;
421
- for (let i = 0;i < matches.length; i++) {
422
- const scriptName = matches[i];
423
- if (expanded[scriptName]) {
424
- throw new Error(`"${name}": expanded script "${scriptName}" collides with an existing process name`);
425
- }
426
- const color = colors ? colors[i % colors.length] : singleColor;
427
- const { color: _color, ...rest } = template;
428
- expanded[scriptName] = {
429
- ...rest,
430
- command: `${pm} run ${scriptName}`,
431
- ...color ? { color } : {}
432
- };
433
- }
434
- }
435
- return { ...config, processes: expanded };
436
- }
437
-
438
- // src/config/loader.ts
439
- import { existsSync as existsSync3 } from "fs";
440
- import { resolve as resolve3 } from "path";
441
-
442
- // src/utils/logger.ts
443
- import { appendFileSync, existsSync as existsSync2, mkdirSync } from "fs";
444
- import { resolve as resolve2 } from "path";
445
- var enabled = false;
446
- var logFile = "";
447
- var debugCallback = null;
448
- function enableDebugLog(dir) {
449
- const logDir = dir ?? resolve2(process.cwd(), ".numux");
450
- logFile = resolve2(logDir, "debug.log");
451
- if (!existsSync2(logDir)) {
452
- mkdirSync(logDir, { recursive: true });
453
- }
454
- enabled = true;
455
- }
456
- function log(...args) {
457
- if (!enabled)
458
- return;
459
- try {
460
- const timestamp = new Date().toISOString();
461
- const formatted = args.length > 0 ? `${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}` : "";
462
- const line = `[${timestamp}] ${formatted}`;
463
- appendFileSync(logFile, `${line}
464
- `);
465
- debugCallback?.(line);
466
- } catch {
467
- enabled = false;
468
- }
469
- }
470
-
471
- // src/config/interpolate.ts
472
- var VAR_RE = /\$\{([^}:]+)(?::([-?])([^}]*))?\}/g;
473
- function interpolateConfig(config) {
474
- return interpolateValue(config);
475
- }
476
- function interpolateValue(value) {
477
- if (typeof value === "string") {
478
- return interpolateString(value);
479
- }
480
- if (Array.isArray(value)) {
481
- return value.map(interpolateValue);
482
- }
483
- if (value && typeof value === "object") {
484
- const result = {};
485
- for (const [k, v] of Object.entries(value)) {
486
- result[k] = interpolateValue(v);
487
- }
488
- return result;
489
- }
490
- return value;
491
- }
492
- function interpolateString(str) {
493
- return str.replace(VAR_RE, (_match, name, operator, operand) => {
494
- const value = process.env[name];
495
- if (value !== undefined && value !== "") {
496
- return value;
497
- }
498
- if (operator === "-") {
499
- return operand ?? "";
500
- }
501
- if (operator === "?") {
502
- throw new Error(operand || `Required variable ${name} is not set`);
503
- }
504
- return "";
505
- });
506
- }
507
-
508
- // src/config/loader.ts
509
- var CONFIG_FILES = ["numux.config.ts", "numux.config.js"];
510
- async function loadConfig(configPath, cwd) {
511
- if (configPath) {
512
- return loadExplicitConfig(configPath);
513
- }
514
- return autoDetectConfig(cwd ?? process.cwd());
515
- }
516
- async function loadFile(path) {
517
- try {
518
- const mod = await import(path);
519
- return interpolateConfig(mod.default ?? mod);
520
- } catch (err) {
521
- throw new Error(`Failed to load ${path}: ${err instanceof Error ? err.message : err}`, { cause: err });
522
- }
523
- }
524
- async function loadExplicitConfig(configPath) {
525
- const path = resolve3(configPath);
526
- if (!existsSync3(path)) {
527
- throw new Error(`Config file not found: ${path}`);
528
- }
529
- log(`Loading explicit config: ${path}`);
530
- return loadFile(path);
531
- }
532
- async function autoDetectConfig(cwd) {
533
- for (const file of CONFIG_FILES) {
534
- const path = resolve3(cwd, file);
535
- if (existsSync3(path)) {
536
- log(`Found config file: ${path}`);
537
- return loadFile(path);
538
- }
539
- }
540
- throw new Error(`No numux config found. Create one of: ${CONFIG_FILES.join(", ")}`);
541
- }
542
-
543
- // src/config/resolver.ts
544
- function resolveDependencyTiers(config) {
545
- const names = Object.keys(config.processes);
546
- const inDegree = new Map;
547
- const dependents = new Map;
548
- for (const name of names) {
549
- inDegree.set(name, 0);
550
- dependents.set(name, []);
551
- }
552
- for (const name of names) {
553
- const deps = config.processes[name].dependsOn ?? [];
554
- inDegree.set(name, deps.length);
555
- for (const dep of deps) {
556
- dependents.get(dep).push(name);
557
- }
558
- }
559
- const tiers = [];
560
- const remaining = new Set(names);
561
- while (remaining.size > 0) {
562
- const tier = [...remaining].filter((n) => inDegree.get(n) === 0);
563
- if (tier.length === 0) {
564
- const cycle = findCycle(remaining, config);
565
- throw new Error(`Dependency cycle detected: ${cycle.join(" \u2192 ")} \u2192 ${cycle[0]}`);
566
- }
567
- tiers.push(tier);
568
- for (const name of tier) {
569
- remaining.delete(name);
570
- for (const dep of dependents.get(name)) {
571
- inDegree.set(dep, inDegree.get(dep) - 1);
572
- }
573
- }
574
- }
575
- return tiers;
576
- }
577
- function findCycle(remaining, config) {
578
- const start = remaining.values().next().value;
579
- const visited = new Set;
580
- const path = [];
581
- let current = start;
582
- while (!visited.has(current)) {
583
- visited.add(current);
584
- path.push(current);
585
- const deps = (config.processes[current].dependsOn ?? []).filter((d) => remaining.has(d));
586
- current = deps[0];
587
- }
588
- const cycleStart = path.indexOf(current);
589
- return path.slice(cycleStart);
590
- }
591
-
592
- // src/utils/color.ts
593
- function hexToAnsi(hex) {
594
- const h = hex.replace("#", "");
595
- const r = Number.parseInt(h.slice(0, 2), 16);
596
- const g = Number.parseInt(h.slice(2, 4), 16);
597
- const b = Number.parseInt(h.slice(4, 6), 16);
598
- if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b))
599
- return "";
600
- return `\x1B[38;2;${r};${g};${b}m`;
601
- }
602
- var HEX_COLOR_RE = /^#?[0-9a-fA-F]{6}$/;
603
- var STATUS_ANSI = {
604
- ready: "\x1B[32m",
605
- running: "\x1B[36m",
606
- finished: "\x1B[32m",
607
- failed: "\x1B[31m",
608
- stopped: "\x1B[90m",
609
- skipped: "\x1B[90m"
610
- };
611
- var ANSI_RESET = "\x1B[0m";
612
- var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()#][0-9A-Za-z]|\x1b[A-Za-z><=]/g;
613
- function stripAnsi(str) {
614
- return str.replace(ANSI_RE, "");
615
- }
616
- var DEFAULT_ANSI_COLORS = [
617
- "\x1B[36m",
618
- "\x1B[33m",
619
- "\x1B[35m",
620
- "\x1B[34m",
621
- "\x1B[32m",
622
- "\x1B[91m",
623
- "\x1B[93m",
624
- "\x1B[95m"
625
- ];
626
- var DEFAULT_HEX_COLORS = ["#00cccc", "#cccc00", "#cc00cc", "#0000cc", "#00cc00", "#ff5555", "#ffff55", "#ff55ff"];
627
- function resolveColor(color) {
628
- if (typeof color === "string")
629
- return color;
630
- if (Array.isArray(color) && color.length > 0)
631
- return color[0];
632
- return;
633
- }
634
- function buildProcessColorMap(names, config) {
635
- const map = new Map;
636
- if ("NO_COLOR" in process.env)
637
- return map;
638
- let paletteIndex = 0;
639
- for (const name of names) {
640
- const explicit = resolveColor(config.processes[name]?.color);
641
- if (explicit) {
642
- map.set(name, hexToAnsi(explicit));
643
- } else {
644
- map.set(name, DEFAULT_ANSI_COLORS[paletteIndex % DEFAULT_ANSI_COLORS.length]);
645
- paletteIndex++;
646
- }
647
- }
648
- return map;
649
- }
650
- function buildProcessHexColorMap(names, config) {
651
- const map = new Map;
652
- if ("NO_COLOR" in process.env)
653
- return map;
654
- let paletteIndex = 0;
655
- for (const name of names) {
656
- const explicit = resolveColor(config.processes[name]?.color);
657
- if (explicit) {
658
- map.set(name, explicit.startsWith("#") ? explicit : `#${explicit}`);
659
- } else {
660
- map.set(name, DEFAULT_HEX_COLORS[paletteIndex % DEFAULT_HEX_COLORS.length]);
661
- paletteIndex++;
662
- }
663
- }
664
- return map;
665
- }
666
-
667
- // src/config/validator.ts
668
- function validateConfig(raw, warnings) {
669
- if (!raw || typeof raw !== "object") {
670
- throw new Error("Config must be an object");
671
- }
672
- const config = raw;
673
- if (!config.processes || typeof config.processes !== "object") {
674
- throw new Error('Config must have a "processes" object');
675
- }
676
- const processes = config.processes;
677
- const names = Object.keys(processes);
678
- if (names.length === 0) {
679
- throw new Error("Config must define at least one process");
680
- }
681
- const globalCwd = typeof config.cwd === "string" ? config.cwd : undefined;
682
- const globalEnvFile = validateStringOrStringArray(config.envFile);
683
- let globalEnv;
684
- if (config.env && typeof config.env === "object") {
685
- for (const [k, v] of Object.entries(config.env)) {
686
- if (typeof v !== "string") {
687
- throw new Error(`env.${k} must be a string, got ${typeof v}`);
688
- }
689
- }
690
- globalEnv = config.env;
691
- }
692
- const validated = {};
693
- for (const name of names) {
694
- let proc = processes[name];
695
- if (typeof proc === "string") {
696
- proc = { command: proc };
697
- }
698
- if (!proc || typeof proc !== "object") {
699
- throw new Error(`Process "${name}" must be an object or a command string`);
700
- }
701
- const p = proc;
702
- if (typeof p.command !== "string" || !p.command.trim()) {
703
- throw new Error(`Process "${name}" must have a non-empty "command" string`);
704
- }
705
- if (p.dependsOn !== undefined) {
706
- if (!Array.isArray(p.dependsOn)) {
707
- throw new Error(`Process "${name}".dependsOn must be an array`);
708
- }
709
- for (const dep of p.dependsOn) {
710
- if (typeof dep !== "string") {
711
- throw new Error(`Process "${name}".dependsOn entries must be strings`);
712
- }
713
- if (!names.includes(dep)) {
714
- throw new Error(`Process "${name}" depends on unknown process "${dep}"`);
715
- }
716
- if (dep === name) {
717
- throw new Error(`Process "${name}" cannot depend on itself`);
718
- }
719
- }
720
- }
721
- if (typeof p.color === "string") {
722
- if (!HEX_COLOR_RE.test(p.color)) {
723
- throw new Error(`Process "${name}".color must be a valid hex color (e.g. "#ff8800"), got "${p.color}"`);
724
- }
725
- } else if (Array.isArray(p.color)) {
726
- for (const c of p.color) {
727
- if (typeof c !== "string" || !HEX_COLOR_RE.test(c)) {
728
- throw new Error(`Process "${name}".color entries must be valid hex colors (e.g. "#ff8800"), got "${c}"`);
729
- }
730
- }
731
- }
732
- const persistent = typeof p.persistent === "boolean" ? p.persistent : true;
733
- const readyPattern = typeof p.readyPattern === "string" ? p.readyPattern : undefined;
734
- if (readyPattern && !persistent) {
735
- warnings?.push({
736
- process: name,
737
- message: "readyPattern is ignored on non-persistent processes (readiness is determined by exit code)"
738
- });
739
- }
740
- if (p.env && typeof p.env === "object") {
741
- for (const [k, v] of Object.entries(p.env)) {
742
- if (typeof v !== "string") {
743
- throw new Error(`Process "${name}".env.${k} must be a string, got ${typeof v}`);
744
- }
745
- }
746
- }
747
- const processCwd = typeof p.cwd === "string" ? p.cwd : undefined;
748
- const processEnv = p.env && typeof p.env === "object" ? p.env : undefined;
749
- const processEnvFile = validateEnvFile(p.envFile);
750
- validated[name] = {
751
- command: p.command,
752
- cwd: processCwd ?? globalCwd,
753
- env: globalEnv || processEnv ? { ...globalEnv, ...processEnv } : undefined,
754
- envFile: processEnvFile ?? globalEnvFile,
755
- dependsOn: Array.isArray(p.dependsOn) ? p.dependsOn : undefined,
756
- readyPattern,
757
- persistent,
758
- maxRestarts: typeof p.maxRestarts === "number" && p.maxRestarts >= 0 ? p.maxRestarts : undefined,
759
- readyTimeout: typeof p.readyTimeout === "number" && p.readyTimeout > 0 ? p.readyTimeout : undefined,
760
- delay: typeof p.delay === "number" && p.delay > 0 ? p.delay : undefined,
761
- condition: typeof p.condition === "string" && p.condition.trim() ? p.condition.trim() : undefined,
762
- stopSignal: validateStopSignal(p.stopSignal),
763
- color: typeof p.color === "string" ? p.color : Array.isArray(p.color) ? p.color : undefined,
764
- watch: validateStringOrStringArray(p.watch),
765
- interactive: typeof p.interactive === "boolean" ? p.interactive : false
766
- };
767
- }
768
- return { processes: validated };
769
- }
770
- function validateStringOrStringArray(value) {
771
- if (typeof value === "string")
772
- return value;
773
- if (Array.isArray(value) && value.every((v) => typeof v === "string"))
774
- return value;
775
- return;
776
- }
777
- var validateEnvFile = validateStringOrStringArray;
778
- var VALID_STOP_SIGNALS = new Set(["SIGTERM", "SIGINT", "SIGHUP"]);
779
- function validateStopSignal(value) {
780
- if (typeof value === "string" && VALID_STOP_SIGNALS.has(value)) {
781
- return value;
782
- }
783
- return;
784
- }
785
-
786
- // src/process/manager.ts
787
- import { resolve as resolve6 } from "path";
788
-
789
- // src/utils/watcher.ts
790
- import { watch } from "fs";
791
- var DEBOUNCE_MS = 300;
792
- var IGNORED_SEGMENTS = new Set(["node_modules", ".git"]);
793
-
794
- class FileWatcher {
795
- watchers = [];
796
- debounceTimers = new Map;
797
- watch(name, patterns, cwd, onChanged) {
798
- const globs = patterns.map((p) => new Bun.Glob(p));
799
- try {
800
- const watcher = watch(cwd, { recursive: true }, (_event, filename) => {
801
- if (!filename)
802
- return;
803
- const segments = filename.split("/");
804
- if (segments.some((s) => IGNORED_SEGMENTS.has(s)))
805
- return;
806
- if (!globs.some((g) => g.match(filename)))
807
- return;
808
- const existing = this.debounceTimers.get(name);
809
- if (existing)
810
- clearTimeout(existing);
811
- this.debounceTimers.set(name, setTimeout(() => {
812
- this.debounceTimers.delete(name);
813
- onChanged(filename);
814
- }, DEBOUNCE_MS));
815
- });
816
- this.watchers.push(watcher);
817
- log(`[${name}] Watching: ${patterns.join(", ")}`);
818
- } catch (err) {
819
- log(`[${name}] Failed to set up file watcher: ${err}`);
820
- }
821
- }
822
- close() {
823
- for (const timer of this.debounceTimers.values()) {
824
- clearTimeout(timer);
825
- }
826
- this.debounceTimers.clear();
827
- for (const watcher of this.watchers) {
828
- watcher.close();
829
- }
830
- this.watchers = [];
831
- }
832
- }
833
-
834
- // src/process/runner.ts
835
- import { resolve as resolve5 } from "path";
836
-
837
- // src/utils/env-file.ts
838
- import { readFileSync as readFileSync2 } from "fs";
839
- import { resolve as resolve4 } from "path";
840
- function parseEnvFile(content) {
841
- const vars = {};
842
- for (const line of content.split(/\r?\n/)) {
843
- const trimmed = line.trim();
844
- if (!trimmed || trimmed.startsWith("#"))
845
- continue;
846
- const eqIndex = trimmed.indexOf("=");
847
- if (eqIndex < 1)
848
- continue;
849
- const key = trimmed.slice(0, eqIndex).trim();
850
- let value = trimmed.slice(eqIndex + 1).trim();
851
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
852
- value = value.slice(1, -1);
853
- } else {
854
- const commentIndex = value.indexOf(" #");
855
- if (commentIndex !== -1) {
856
- value = value.slice(0, commentIndex).trimEnd();
857
- }
858
- }
859
- vars[key] = value;
860
- }
861
- return vars;
862
- }
863
- function loadEnvFiles(envFile, cwd) {
864
- const files = Array.isArray(envFile) ? envFile : [envFile];
865
- const merged = {};
866
- for (const file of files) {
867
- const path = resolve4(cwd, file);
868
- let content;
869
- try {
870
- content = readFileSync2(path, "utf-8");
871
- } catch (err) {
872
- const code = err.code;
873
- if (code === "ENOENT") {
874
- throw new Error(`envFile not found: ${path}`, { cause: err });
875
- }
876
- throw new Error(`Failed to read envFile "${path}": ${err instanceof Error ? err.message : err}`, {
877
- cause: err
878
- });
879
- }
880
- Object.assign(merged, parseEnvFile(content));
881
- }
882
- return merged;
883
- }
884
-
885
- // src/process/ready.ts
886
- var BUFFER_CAP = 65536;
887
- function createReadinessChecker(config) {
888
- const pattern = config.readyPattern ? new RegExp(config.readyPattern) : null;
889
- const persistent = config.persistent !== false;
890
- let outputBuffer = "";
891
- return {
892
- feedOutput(data) {
893
- if (!(persistent && pattern))
894
- return false;
895
- outputBuffer += data;
896
- if (outputBuffer.length > BUFFER_CAP) {
897
- outputBuffer = outputBuffer.slice(-BUFFER_CAP);
898
- }
899
- return pattern.test(outputBuffer);
900
- },
901
- get isImmediatelyReady() {
902
- return persistent && !pattern;
903
- },
904
- get dependsOnExit() {
905
- return !persistent;
906
- }
907
- };
908
- }
909
-
910
- // src/process/runner.ts
911
- class ProcessRunner {
912
- name;
913
- config;
914
- handler;
915
- proc = null;
916
- readiness;
917
- _ready = false;
918
- stopping = false;
919
- decoder = new TextDecoder;
920
- generation = 0;
921
- readyTimer = null;
922
- restarting = false;
923
- readyTimedOut = false;
924
- constructor(name, config, handler) {
925
- this.name = name;
926
- this.config = config;
927
- this.handler = handler;
928
- this.readiness = createReadinessChecker(config);
929
- }
930
- get isReady() {
931
- return this._ready;
932
- }
933
- get signal() {
934
- return this.config.stopSignal ?? "SIGTERM";
935
- }
936
- start(cols, rows) {
937
- const gen = ++this.generation;
938
- this.stopping = false;
939
- log(`[${this.name}] Starting (gen ${gen}): ${this.config.command}`);
940
- this.handler.onStatus("starting");
941
- const cwd = this.config.cwd ? resolve5(this.config.cwd) : process.cwd();
942
- try {
943
- const envFromFile = this.config.envFile ? loadEnvFiles(this.config.envFile, cwd) : {};
944
- const noColor = "NO_COLOR" in process.env;
945
- const env = {
946
- ...process.env,
947
- ...noColor ? {} : { FORCE_COLOR: "1" },
948
- TERM: "xterm-256color",
949
- ...envFromFile,
950
- ...this.config.env
951
- };
952
- this.proc = Bun.spawn(["sh", "-c", this.config.command], {
953
- cwd,
954
- env,
955
- terminal: {
956
- cols,
957
- rows,
958
- data: (_terminal, data) => {
959
- if (this.generation !== gen)
960
- return;
961
- this.handler.onOutput(data);
962
- this.checkReadiness(data);
963
- }
964
- }
965
- });
966
- } catch (err) {
967
- log(`[${this.name}] Spawn failed: ${err}`);
968
- const encoder = new TextEncoder;
969
- const msg = `\r
970
- \x1B[31m[numux] failed to start: ${err instanceof Error ? err.message : err}\x1B[0m\r
971
- `;
972
- this.handler.onOutput(encoder.encode(msg));
973
- this.handler.onStatus("failed");
974
- this.handler.onExit(null);
975
- return;
976
- }
977
- this.handler.onStatus(this.config.persistent !== false ? "running" : "starting");
978
- if (this.readiness.isImmediatelyReady) {
979
- this.markReady();
980
- }
981
- this.startReadyTimeout(gen);
982
- this.proc.exited.then((code) => {
983
- if (this.generation !== gen)
984
- return;
985
- log(`[${this.name}] Exited with code ${code}`);
986
- if (this.readiness.dependsOnExit && code === 0) {
987
- this.markReady();
988
- }
989
- if (code === 127 || code === 126) {
990
- const encoder = new TextEncoder;
991
- const hint = code === 127 ? "command not found" : "permission denied";
992
- const msg = `\r
993
- \x1B[31m[numux] exit ${code}: ${hint}\x1B[0m\r
994
- `;
995
- this.handler.onOutput(encoder.encode(msg));
996
- }
997
- if (!this.readyTimedOut) {
998
- const status = this.stopping ? "stopped" : code === 0 ? "finished" : "failed";
999
- this.handler.onStatus(status);
1000
- this.handler.onExit(code);
1001
- }
1002
- }).catch((err) => {
1003
- if (this.generation !== gen)
1004
- return;
1005
- log(`[${this.name}] proc.exited rejected: ${err}`);
1006
- this.handler.onStatus("failed");
1007
- this.handler.onExit(null);
1008
- });
1009
- }
1010
- checkReadiness(data) {
1011
- if (this._ready)
1012
- return;
1013
- const text = this.decoder.decode(data, { stream: true });
1014
- if (this.readiness.feedOutput(text)) {
1015
- this.markReady();
1016
- }
1017
- }
1018
- startReadyTimeout(gen) {
1019
- const timeout = this.config.readyTimeout;
1020
- if (!(timeout && this.config.readyPattern) || this.config.persistent === false)
1021
- return;
1022
- this.readyTimer = setTimeout(() => {
1023
- this.readyTimer = null;
1024
- if (this.generation !== gen || this._ready)
1025
- return;
1026
- this.readyTimedOut = true;
1027
- log(`[${this.name}] Ready timeout after ${timeout}ms`);
1028
- const encoder = new TextEncoder;
1029
- const msg = `\r
1030
- \x1B[31m[numux] readyPattern not matched within ${(timeout / 1000).toFixed(0)}s \u2014 marking as failed\x1B[0m\r
1031
- `;
1032
- this.handler.onOutput(encoder.encode(msg));
1033
- this.handler.onStatus("failed");
1034
- this.handler.onReady();
1035
- }, timeout);
1036
- }
1037
- clearReadyTimeout() {
1038
- if (this.readyTimer) {
1039
- clearTimeout(this.readyTimer);
1040
- this.readyTimer = null;
1041
- }
1042
- }
1043
- markReady() {
1044
- if (this._ready)
1045
- return;
1046
- this._ready = true;
1047
- this.clearReadyTimeout();
1048
- log(`[${this.name}] Ready`);
1049
- this.handler.onStatus("ready");
1050
- this.handler.onReady();
1051
- }
1052
- async restart(cols, rows) {
1053
- if (this.restarting)
1054
- return;
1055
- this.restarting = true;
1056
- log(`[${this.name}] Restarting`);
1057
- this.clearReadyTimeout();
1058
- if (this.proc) {
1059
- this.stopping = true;
1060
- this.handler.onStatus("stopping");
1061
- this.killProcessGroup(this.signal);
1062
- const result = await Promise.race([
1063
- this.proc.exited.then(() => "exited"),
1064
- new Promise((r) => setTimeout(() => r("timeout"), 2000))
1065
- ]);
1066
- if (result === "timeout" && this.proc) {
1067
- this.killProcessGroup("SIGKILL");
1068
- await this.proc.exited;
1069
- }
1070
- }
1071
- this.proc = null;
1072
- this._ready = false;
1073
- this.restarting = false;
1074
- this.readyTimedOut = false;
1075
- this.readiness = createReadinessChecker(this.config);
1076
- this.start(cols, rows);
1077
- }
1078
- async stop(timeoutMs = 5000) {
1079
- if (!this.proc)
1080
- return;
1081
- this.clearReadyTimeout();
1082
- this.stopping = true;
1083
- log(`[${this.name}] Stopping (timeout: ${timeoutMs}ms)`);
1084
- this.handler.onStatus("stopping");
1085
- this.killProcessGroup(this.signal);
1086
- const exited = Promise.race([
1087
- this.proc.exited,
1088
- new Promise((r) => setTimeout(() => r("timeout"), timeoutMs))
1089
- ]);
1090
- const result = await exited;
1091
- if (result === "timeout") {
1092
- this.killProcessGroup("SIGKILL");
1093
- await this.proc.exited;
1094
- }
1095
- this.proc = null;
1096
- }
1097
- killProcessGroup(sig) {
1098
- if (!this.proc)
1099
- return;
1100
- try {
1101
- process.kill(-this.proc.pid, sig);
1102
- } catch {
1103
- try {
1104
- this.proc.kill(sig);
1105
- } catch {}
1106
- }
1107
- }
1108
- resize(cols, rows) {
1109
- if (this.proc?.terminal) {
1110
- this.proc.terminal.resize(cols, rows);
1111
- }
1112
- }
1113
- write(data) {
1114
- if (this.config.interactive && this.proc?.terminal) {
1115
- this.proc.terminal.write(data);
1116
- }
1117
- }
1118
- }
1119
-
1120
- // src/process/manager.ts
1121
- var BACKOFF_BASE_MS = 1000;
1122
- var BACKOFF_MAX_MS = 30000;
1123
- var BACKOFF_RESET_MS = 1e4;
1124
-
1125
- class ProcessManager {
1126
- config;
1127
- runners = new Map;
1128
- states = new Map;
1129
- tiers;
1130
- listeners = [];
1131
- stopping = false;
1132
- lastCols = 80;
1133
- lastRows = 24;
1134
- restartAttempts = new Map;
1135
- restartTimers = new Map;
1136
- startTimes = new Map;
1137
- pendingReadyResolvers = new Map;
1138
- fileWatcher;
1139
- constructor(config) {
1140
- this.config = config;
1141
- this.tiers = resolveDependencyTiers(config);
1142
- log(`Resolved ${this.tiers.length} dependency tiers:`, this.tiers);
1143
- for (const [name, proc] of Object.entries(config.processes)) {
1144
- this.states.set(name, {
1145
- name,
1146
- config: proc,
1147
- status: "pending",
1148
- exitCode: null,
1149
- restartCount: 0
1150
- });
1151
- }
1152
- }
1153
- on(listener) {
1154
- this.listeners.push(listener);
1155
- }
1156
- emit(event) {
1157
- for (const listener of this.listeners) {
1158
- listener(event);
1159
- }
1160
- }
1161
- getState(name) {
1162
- return this.states.get(name);
1163
- }
1164
- getAllStates() {
1165
- return [...this.states.values()];
1166
- }
1167
- getProcessNames() {
1168
- return this.tiers.flat();
1169
- }
1170
- async startAll(cols, rows) {
1171
- log("Starting all processes");
1172
- this.lastCols = cols;
1173
- this.lastRows = rows;
1174
- for (const tier of this.tiers) {
1175
- const readyPromises = [];
1176
- for (const name of tier) {
1177
- const proc = this.config.processes[name];
1178
- if (proc.condition && !evaluateCondition(proc.condition)) {
1179
- log(`Skipping ${name}: condition "${proc.condition}" not met`);
1180
- this.updateStatus(name, "skipped");
1181
- continue;
1182
- }
1183
- const deps = proc.dependsOn ?? [];
1184
- const failedDep = deps.find((d) => {
1185
- const s = this.states.get(d).status;
1186
- return s === "failed" || s === "skipped";
1187
- });
1188
- if (failedDep) {
1189
- log(`Skipping ${name}: dependency ${failedDep} failed`);
1190
- this.updateStatus(name, "skipped");
1191
- continue;
1192
- }
1193
- const { promise, resolve: resolve7 } = Promise.withResolvers();
1194
- readyPromises.push(promise);
1195
- this.pendingReadyResolvers.set(name, resolve7);
1196
- this.createRunner(name, () => {
1197
- this.pendingReadyResolvers.delete(name);
1198
- resolve7();
1199
- });
1200
- this.startProcess(name, cols, rows);
1201
- }
1202
- if (readyPromises.length > 0) {
1203
- await Promise.all(readyPromises);
1204
- }
1205
- }
1206
- this.setupWatchers();
1207
- }
1208
- startProcess(name, cols, rows) {
1209
- const delay = this.config.processes[name].delay;
1210
- if (delay) {
1211
- log(`[${name}] Delaying start by ${delay}ms`);
1212
- const timer = setTimeout(() => {
1213
- this.restartTimers.delete(name);
1214
- if (this.stopping)
1215
- return;
1216
- this.startTimes.set(name, Date.now());
1217
- this.runners.get(name).start(cols, rows);
1218
- }, delay);
1219
- this.restartTimers.set(name, timer);
1220
- } else {
1221
- this.startTimes.set(name, Date.now());
1222
- this.runners.get(name).start(cols, rows);
1223
- }
1224
- }
1225
- createRunner(name, onInitialReady) {
1226
- let readyResolved = !onInitialReady;
1227
- const runner = new ProcessRunner(name, this.config.processes[name], {
1228
- onStatus: (status) => this.updateStatus(name, status),
1229
- onOutput: (data) => this.emit({ type: "output", name, data }),
1230
- onExit: (code) => {
1231
- const state = this.states.get(name);
1232
- state.exitCode = code;
1233
- this.emit({ type: "exit", name, code });
1234
- if (!readyResolved) {
1235
- readyResolved = true;
1236
- onInitialReady();
1237
- }
1238
- this.scheduleAutoRestart(name, code);
1239
- },
1240
- onReady: () => {
1241
- if (!readyResolved) {
1242
- readyResolved = true;
1243
- onInitialReady();
1244
- }
1245
- }
1246
- });
1247
- this.runners.set(name, runner);
1248
- }
1249
- scheduleAutoRestart(name, exitCode) {
1250
- if (this.stopping)
1251
- return;
1252
- const proc = this.config.processes[name];
1253
- if (proc.persistent === false)
1254
- return;
1255
- if (exitCode === 0)
1256
- return;
1257
- if (exitCode === null)
1258
- return;
1259
- log(`Scheduling auto-restart for ${name} (exit code: ${exitCode})`);
1260
- const startTime = this.startTimes.get(name) ?? 0;
1261
- if (Date.now() - startTime > BACKOFF_RESET_MS) {
1262
- this.restartAttempts.set(name, 0);
1263
- }
1264
- const attempt = this.restartAttempts.get(name) ?? 0;
1265
- const maxRestarts = proc.maxRestarts;
1266
- if (maxRestarts !== undefined && attempt >= maxRestarts) {
1267
- log(`[${name}] Reached maxRestarts limit (${maxRestarts}), not restarting`);
1268
- const encoder2 = new TextEncoder;
1269
- const msg2 = `\r
1270
- \x1B[31m[numux] reached restart limit (${maxRestarts}), giving up\x1B[0m\r
1271
- `;
1272
- this.emit({ type: "output", name, data: encoder2.encode(msg2) });
1273
- return;
1274
- }
1275
- const delay = Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_MAX_MS);
1276
- this.restartAttempts.set(name, attempt + 1);
1277
- const encoder = new TextEncoder;
1278
- const msg = `\r
1279
- \x1B[33m[numux] restarting in ${(delay / 1000).toFixed(0)}s (attempt ${attempt + 1}${maxRestarts !== undefined ? `/${maxRestarts}` : ""})...\x1B[0m\r
1280
- `;
1281
- this.emit({ type: "output", name, data: encoder.encode(msg) });
1282
- const timer = setTimeout(() => {
1283
- this.restartTimers.delete(name);
1284
- if (this.stopping)
1285
- return;
1286
- const runner = this.runners.get(name);
1287
- if (!runner)
1288
- return;
1289
- const state = this.states.get(name);
1290
- if (state)
1291
- state.restartCount++;
1292
- this.startTimes.set(name, Date.now());
1293
- runner.restart(this.lastCols, this.lastRows);
1294
- }, delay);
1295
- this.restartTimers.set(name, timer);
1296
- }
1297
- setupWatchers() {
1298
- const encoder = new TextEncoder;
1299
- for (const [name, proc] of Object.entries(this.config.processes)) {
1300
- if (!proc.watch)
1301
- continue;
1302
- if (!this.fileWatcher)
1303
- this.fileWatcher = new FileWatcher;
1304
- const patterns = Array.isArray(proc.watch) ? proc.watch : [proc.watch];
1305
- const cwd = proc.cwd ? resolve6(proc.cwd) : process.cwd();
1306
- this.fileWatcher.watch(name, patterns, cwd, (changedFile) => {
1307
- const state = this.states.get(name);
1308
- if (!state)
1309
- return;
1310
- if (state.status === "pending" || state.status === "stopped" || state.status === "finished" || state.status === "stopping" || state.status === "skipped")
1311
- return;
1312
- log(`[${name}] File changed: ${changedFile}, restarting`);
1313
- const msg = `\r
1314
- \x1B[36m[numux] file changed: ${changedFile}, restarting...\x1B[0m\r
1315
- `;
1316
- this.emit({ type: "output", name, data: encoder.encode(msg) });
1317
- this.restart(name, this.lastCols, this.lastRows);
1318
- });
1319
- }
1320
- }
1321
- updateStatus(name, status) {
1322
- const state = this.states.get(name);
1323
- state.status = status;
1324
- this.emit({ type: "status", name, status });
1325
- }
1326
- restart(name, cols, rows) {
1327
- const state = this.states.get(name);
1328
- if (!state)
1329
- return;
1330
- if (state.status === "pending" || state.status === "stopping" || state.status === "skipped")
1331
- return;
1332
- const runner = this.runners.get(name);
1333
- if (!runner)
1334
- return;
1335
- const timer = this.restartTimers.get(name);
1336
- if (timer) {
1337
- clearTimeout(timer);
1338
- this.restartTimers.delete(name);
1339
- }
1340
- this.restartAttempts.set(name, 0);
1341
- state.exitCode = null;
1342
- state.restartCount++;
1343
- this.startTimes.set(name, Date.now());
1344
- runner.restart(cols, rows);
1345
- }
1346
- async stop(name) {
1347
- const state = this.states.get(name);
1348
- if (!state)
1349
- return;
1350
- if (state.status === "pending" || state.status === "stopped" || state.status === "finished" || state.status === "stopping" || state.status === "skipped")
1351
- return;
1352
- const timer = this.restartTimers.get(name);
1353
- if (timer) {
1354
- clearTimeout(timer);
1355
- this.restartTimers.delete(name);
1356
- }
1357
- const runner = this.runners.get(name);
1358
- if (!runner)
1359
- return;
1360
- if (state.status === "failed") {
1361
- await runner.stop();
1362
- this.updateStatus(name, "stopped");
1363
- return;
1364
- }
1365
- await runner.stop();
1366
- }
1367
- start(name, cols, rows) {
1368
- const state = this.states.get(name);
1369
- if (!state)
1370
- return;
1371
- if (state.status !== "stopped" && state.status !== "finished" && state.status !== "failed")
1372
- return;
1373
- const timer = this.restartTimers.get(name);
1374
- if (timer) {
1375
- clearTimeout(timer);
1376
- this.restartTimers.delete(name);
1377
- }
1378
- this.restartAttempts.set(name, 0);
1379
- state.exitCode = null;
1380
- state.restartCount++;
1381
- this.startTimes.set(name, Date.now());
1382
- this.runners.get(name)?.restart(cols, rows);
1383
- }
1384
- restartAll(cols, rows) {
1385
- log("Restarting all processes");
1386
- for (const name of this.tiers.flat()) {
1387
- this.restart(name, cols, rows);
1388
- }
1389
- }
1390
- resize(name, cols, rows) {
1391
- this.runners.get(name)?.resize(cols, rows);
1392
- }
1393
- resizeAll(cols, rows) {
1394
- this.lastCols = cols;
1395
- this.lastRows = rows;
1396
- for (const runner of this.runners.values()) {
1397
- runner.resize(cols, rows);
1398
- }
1399
- }
1400
- write(name, data) {
1401
- this.runners.get(name)?.write(data);
1402
- }
1403
- async stopAll() {
1404
- log("Stopping all processes");
1405
- this.stopping = true;
1406
- this.fileWatcher?.close();
1407
- for (const timer of this.restartTimers.values()) {
1408
- clearTimeout(timer);
1409
- }
1410
- this.restartTimers.clear();
1411
- for (const resolve7 of this.pendingReadyResolvers.values()) {
1412
- resolve7();
1413
- }
1414
- this.pendingReadyResolvers.clear();
1415
- const reversed = [...this.tiers].reverse();
1416
- for (const tier of reversed) {
1417
- await Promise.allSettled(tier.map((name) => this.runners.get(name)?.stop()).filter(Boolean));
1418
- }
1419
- }
1420
- }
1421
- var FALSY_VALUES = new Set(["", "0", "false", "no", "off"]);
1422
- function evaluateCondition(condition) {
1423
- const negated = condition.startsWith("!");
1424
- const varName = negated ? condition.slice(1) : condition;
1425
- const value = process.env[varName];
1426
- const isTruthy = value !== undefined && !FALSY_VALUES.has(value.toLowerCase());
1427
- return negated ? !isTruthy : isTruthy;
1428
- }
1429
-
1430
- // src/ui/app.ts
1431
- import { BoxRenderable, createCliRenderer } from "@opentui/core";
1432
-
1433
- // src/ui/pane.ts
1434
- import { ScrollBoxRenderable } from "@opentui/core";
1435
- import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
1436
-
1437
- class Pane {
1438
- scrollBox;
1439
- terminal;
1440
- decoder = new TextDecoder;
1441
- _onScroll = null;
1442
- constructor(renderer, name, cols, rows, interactive = false) {
1443
- this.scrollBox = new ScrollBoxRenderable(renderer, {
1444
- id: `pane-${name}`,
1445
- flexGrow: 1,
1446
- width: "100%",
1447
- stickyScroll: true,
1448
- stickyStart: "bottom",
1449
- visible: false,
1450
- onMouseScroll: () => this._onScroll?.()
1451
- });
1452
- this.terminal = new GhosttyTerminalRenderable(renderer, {
1453
- id: `term-${name}`,
1454
- cols,
1455
- rows,
1456
- persistent: true,
1457
- showCursor: interactive,
1458
- trimEnd: true
1459
- });
1460
- this.scrollBox.add(this.terminal);
1461
- }
1462
- feed(data) {
1463
- const text = this.decoder.decode(data, { stream: true });
1464
- this.terminal.feed(text);
1465
- }
1466
- resize(cols, rows) {
1467
- this.terminal.cols = cols;
1468
- this.terminal.rows = rows;
1469
- }
1470
- get isAtBottom() {
1471
- const { scrollTop, scrollHeight, viewport } = this.scrollBox;
1472
- if (scrollHeight <= 0)
1473
- return true;
1474
- return scrollTop >= scrollHeight - viewport.height - 2;
1475
- }
1476
- scrollBy(delta) {
1477
- this.scrollBox.scrollBy(delta);
1478
- }
1479
- scrollToTop() {
1480
- this.scrollBox.scrollTo(0);
1481
- }
1482
- scrollToBottom() {
1483
- this.scrollBox.scrollTo(this.scrollBox.scrollHeight);
1484
- }
1485
- onScroll(handler) {
1486
- this._onScroll = handler;
1487
- }
1488
- show() {
1489
- this.scrollBox.visible = true;
1490
- }
1491
- hide() {
1492
- this.scrollBox.visible = false;
1493
- }
1494
- search(query) {
1495
- if (!query)
1496
- return [];
1497
- const text = this.terminal.getText();
1498
- const lines = text.split(`
1499
- `);
1500
- const matches = [];
1501
- const lowerQuery = query.toLowerCase();
1502
- for (let line = 0;line < lines.length; line++) {
1503
- const lowerLine = lines[line].toLowerCase();
1504
- let pos = 0;
1505
- while (true) {
1506
- const idx = lowerLine.indexOf(lowerQuery, pos);
1507
- if (idx === -1)
1508
- break;
1509
- matches.push({ line, start: idx, end: idx + query.length });
1510
- pos = idx + 1;
1511
- }
1512
- }
1513
- return matches;
1514
- }
1515
- setHighlights(matches, currentIndex) {
1516
- const regions = matches.map((m, i) => ({
1517
- line: m.line,
1518
- start: m.start,
1519
- end: m.end,
1520
- backgroundColor: i === currentIndex ? "#b58900" : "#073642"
1521
- }));
1522
- this.terminal.highlights = regions;
1523
- }
1524
- clearHighlights() {
1525
- this.terminal.highlights = undefined;
1526
- }
1527
- scrollToLine(line) {
1528
- const pos = this.terminal.getScrollPositionForLine(line);
1529
- this.scrollBox.scrollTo(pos);
1530
- }
1531
- clear() {
1532
- this.terminal.reset();
1533
- }
1534
- destroy() {
1535
- this.terminal.destroy();
1536
- }
1537
- }
1538
-
1539
- // src/ui/status-bar.ts
1540
- import { cyan, red, reverse, StyledText, TextRenderable, yellow } from "@opentui/core";
1541
- function plain(text) {
1542
- return { __isChunk: true, text };
1543
- }
1544
-
1545
- class StatusBar {
1546
- renderable;
1547
- _searchMode = false;
1548
- _searchQuery = "";
1549
- _searchMatchCount = 0;
1550
- _searchCurrentIndex = -1;
1551
- constructor(renderer) {
1552
- this.renderable = new TextRenderable(renderer, {
1553
- id: "status-bar",
1554
- width: "100%",
1555
- height: 1,
1556
- content: this.buildContent(),
1557
- bg: "#1a1a1a",
1558
- paddingX: 1
1559
- });
1560
- }
1561
- setSearchMode(active, query = "", matchCount = 0, currentIndex = -1) {
1562
- this._searchMode = active;
1563
- this._searchQuery = query;
1564
- this._searchMatchCount = matchCount;
1565
- this._searchCurrentIndex = currentIndex;
1566
- this.renderable.content = this.buildContent();
1567
- }
1568
- buildContent() {
1569
- if (this._searchMode) {
1570
- return this.buildSearchContent();
1571
- }
1572
- return new StyledText([
1573
- plain("\u2190\u2192/1-9: tabs R: restart S: stop/start F: search L: clear Ctrl+C: quit")
1574
- ]);
1575
- }
1576
- buildSearchContent() {
1577
- const chunks = [];
1578
- chunks.push(yellow("/"));
1579
- if (this._searchQuery)
1580
- chunks.push(plain(this._searchQuery));
1581
- chunks.push(reverse(" "));
1582
- if (this._searchMatchCount === 0 && this._searchQuery) {
1583
- chunks.push(plain(" "));
1584
- chunks.push(red("no matches"));
1585
- chunks.push(plain(" Esc: close"));
1586
- } else if (this._searchMatchCount > 0) {
1587
- chunks.push(plain(" "));
1588
- chunks.push(cyan(`${this._searchCurrentIndex + 1}/${this._searchMatchCount}`));
1589
- chunks.push(plain(" Enter/Shift+Enter: next/prev Esc: close"));
1590
- } else {
1591
- chunks.push(plain(" Enter: next Esc: close"));
1592
- }
1593
- return new StyledText(chunks);
1594
- }
1595
- }
1596
-
1597
- // src/ui/tabs.ts
1598
- import {
1599
- parseColor,
1600
- SelectRenderable,
1601
- SelectRenderableEvents
1602
- } from "@opentui/core";
1603
- var STATUS_ICONS = {
1604
- pending: "\u25CB",
1605
- starting: "\u25D0",
1606
- running: "\u25C9",
1607
- ready: "\u25CF",
1608
- stopping: "\u25D1",
1609
- stopped: "\u25A0",
1610
- finished: "\u2713",
1611
- failed: "\u2716",
1612
- skipped: "\u2298"
1613
- };
1614
- var STATUS_ICON_HEX = {
1615
- ready: "#00cc00",
1616
- finished: "#66aa66",
1617
- failed: "#ff5555",
1618
- stopped: "#888888",
1619
- skipped: "#888888"
1620
- };
1621
- var TERMINAL_STATUSES = new Set(["finished", "stopped", "failed", "skipped"]);
1622
-
1623
- class ColoredSelectRenderable extends SelectRenderable {
1624
- _optionColors = [];
1625
- setOptionColors(colors) {
1626
- this._optionColors = colors;
1627
- this.requestRender();
1628
- }
1629
- renderSelf(buffer, deltaTime) {
1630
- const wasDirty = this.isDirty;
1631
- super.renderSelf(buffer, deltaTime);
1632
- if (wasDirty && this.frameBuffer && this._optionColors.length > 0) {
1633
- this.colorizeOptions();
1634
- }
1635
- }
1636
- onMouseEvent(event) {
1637
- if (event.type === "down") {
1638
- const linesPerItem = this.linesPerItem;
1639
- const scrollOffset = this.scrollOffset;
1640
- const clickedIndex = scrollOffset + Math.floor(event.y / linesPerItem);
1641
- if (clickedIndex >= 0 && clickedIndex < this.options.length) {
1642
- this.setSelectedIndex(clickedIndex);
1643
- this.selectCurrent();
1644
- }
1645
- }
1646
- }
1647
- colorizeOptions() {
1648
- const fb = this.frameBuffer;
1649
- const scrollOffset = this.scrollOffset;
1650
- const maxVisibleItems = this.maxVisibleItems;
1651
- const linesPerItem = this.linesPerItem;
1652
- const options = this.options;
1653
- const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
1654
- for (let i = 0;i < visibleCount; i++) {
1655
- const actualIndex = scrollOffset + i;
1656
- const colors = this._optionColors[actualIndex];
1657
- if (!colors)
1658
- continue;
1659
- const itemY = i * linesPerItem;
1660
- const optName = options[actualIndex].name;
1661
- if (colors.icon) {
1662
- fb.drawText(optName.charAt(0), 3, itemY, colors.icon);
1663
- }
1664
- if (colors.name) {
1665
- fb.drawText(optName.slice(2), 5, itemY, colors.name);
1666
- }
1667
- }
1668
- }
1669
- }
1670
-
1671
- class TabBar {
1672
- renderable;
1673
- originalNames;
1674
- names;
1675
- statuses;
1676
- baseDescriptions;
1677
- processColors;
1678
- inputWaiting = new Set;
1679
- constructor(renderer, names, colors) {
1680
- this.originalNames = names;
1681
- this.names = [...names];
1682
- this.statuses = new Map(names.map((n) => [n, "pending"]));
1683
- this.baseDescriptions = new Map(names.map((n) => [n, "pending"]));
1684
- this.processColors = colors ?? new Map;
1685
- this.renderable = new ColoredSelectRenderable(renderer, {
1686
- id: "tab-bar",
1687
- width: "100%",
1688
- height: "100%",
1689
- options: names.map((n) => ({
1690
- name: this.formatTab(n, "pending"),
1691
- description: "pending"
1692
- })),
1693
- selectedBackgroundColor: "#334455",
1694
- selectedTextColor: "#fff",
1695
- textColor: "#888",
1696
- showDescription: true,
1697
- wrapSelection: true
1698
- });
1699
- this.updateOptionColors();
1700
- }
1701
- onSelect(handler) {
1702
- this.renderable.on(SelectRenderableEvents.ITEM_SELECTED, (index) => {
1703
- handler(index, this.names[index]);
1704
- });
1705
- }
1706
- onSelectionChanged(handler) {
1707
- this.renderable.on(SelectRenderableEvents.SELECTION_CHANGED, (index) => {
1708
- handler(index, this.names[index]);
1709
- });
1710
- }
1711
- updateStatus(name, status, exitCode, restartCount) {
1712
- this.statuses.set(name, status);
1713
- this.baseDescriptions.set(name, this.formatDescription(status, exitCode, restartCount));
1714
- if (TERMINAL_STATUSES.has(status) || status === "stopping") {
1715
- this.inputWaiting.delete(name);
1716
- }
1717
- this.refreshOptions();
1718
- }
1719
- setInputWaiting(name, waiting) {
1720
- if (waiting)
1721
- this.inputWaiting.add(name);
1722
- else
1723
- this.inputWaiting.delete(name);
1724
- this.refreshOptions();
1725
- }
1726
- getNameAtIndex(index) {
1727
- return this.names[index];
1728
- }
1729
- get count() {
1730
- return this.names.length;
1731
- }
1732
- refreshOptions() {
1733
- const currentIdx = this.renderable.getSelectedIndex();
1734
- const currentName = this.names[currentIdx];
1735
- this.names = this.getDisplayOrder();
1736
- this.renderable.options = this.names.map((n) => ({
1737
- name: this.formatTab(n, this.statuses.get(n)),
1738
- description: this.getDescription(n)
1739
- }));
1740
- const newIdx = this.names.indexOf(currentName);
1741
- if (newIdx >= 0 && newIdx !== currentIdx) {
1742
- this.renderable.setSelectedIndex(newIdx);
1743
- }
1744
- this.updateOptionColors();
1745
- }
1746
- getDisplayOrder() {
1747
- const active = this.originalNames.filter((n) => !TERMINAL_STATUSES.has(this.statuses.get(n)));
1748
- const terminal = this.originalNames.filter((n) => TERMINAL_STATUSES.has(this.statuses.get(n)));
1749
- return [...active, ...terminal];
1750
- }
1751
- getDescription(name) {
1752
- if (this.inputWaiting.has(name))
1753
- return "awaiting input";
1754
- return this.baseDescriptions.get(name) ?? "pending";
1755
- }
1756
- updateOptionColors() {
1757
- const colors = this.names.map((name) => {
1758
- const status = this.statuses.get(name);
1759
- const waiting = this.inputWaiting.has(name);
1760
- const statusHex = waiting ? "#ffaa00" : STATUS_ICON_HEX[status];
1761
- const processHex = this.processColors.get(name);
1762
- return {
1763
- icon: parseColor(statusHex ?? processHex ?? "#888888"),
1764
- name: processHex ? parseColor(processHex) : null
1765
- };
1766
- });
1767
- this.renderable.setOptionColors(colors);
1768
- }
1769
- formatDescription(status, exitCode, restartCount) {
1770
- let desc = status;
1771
- if ((status === "failed" || status === "stopped") && exitCode != null && exitCode !== 0) {
1772
- desc = `exit ${exitCode}`;
1773
- }
1774
- if (restartCount && restartCount > 0) {
1775
- desc += ` \xD7${restartCount}`;
1776
- }
1777
- return desc;
1778
- }
1779
- formatTab(name, status) {
1780
- const icon = STATUS_ICONS[status];
1781
- return `${icon} ${name}`;
1782
- }
1783
- getSelectedIndex() {
1784
- return this.renderable.getSelectedIndex();
1785
- }
1786
- setSelectedIndex(index) {
1787
- this.renderable.setSelectedIndex(index);
1788
- }
1789
- focus() {
1790
- this.renderable.focus();
1791
- }
1792
- }
1793
-
1794
- // src/ui/app.ts
1795
- class App {
1796
- renderer;
1797
- manager;
1798
- panes = new Map;
1799
- tabBar;
1800
- statusBar;
1801
- activePane = null;
1802
- destroyed = false;
1803
- names;
1804
- termCols = 80;
1805
- termRows = 24;
1806
- sidebarWidth = 20;
1807
- config;
1808
- resizeTimer = null;
1809
- searchMode = false;
1810
- searchQuery = "";
1811
- searchMatches = [];
1812
- searchIndex = -1;
1813
- inputWaitTimers = new Map;
1814
- awaitingInput = new Set;
1815
- constructor(manager, config) {
1816
- this.manager = manager;
1817
- this.config = config;
1818
- this.names = manager.getProcessNames();
1819
- }
1820
- async start() {
1821
- this.renderer = await createCliRenderer({
1822
- exitOnCtrlC: false,
1823
- useMouse: true
1824
- });
1825
- const { width, height } = this.renderer;
1826
- const maxNameLen = Math.max(...this.names.map((n) => n.length));
1827
- this.sidebarWidth = Math.min(30, Math.max(16, maxNameLen + 5));
1828
- this.termCols = Math.max(40, width - this.sidebarWidth - 2);
1829
- this.termRows = Math.max(5, height - 2);
1830
- const { termCols, termRows } = this;
1831
- const layout = new BoxRenderable(this.renderer, {
1832
- id: "root",
1833
- flexDirection: "column",
1834
- width: "100%",
1835
- height: "100%",
1836
- border: false
1837
- });
1838
- const processHexColors = buildProcessHexColorMap(this.names, this.config);
1839
- this.tabBar = new TabBar(this.renderer, this.names, processHexColors);
1840
- const contentRow = new BoxRenderable(this.renderer, {
1841
- id: "content-row",
1842
- flexDirection: "row",
1843
- flexGrow: 1,
1844
- width: "100%",
1845
- border: false
1846
- });
1847
- const sidebar = new BoxRenderable(this.renderer, {
1848
- id: "sidebar",
1849
- width: this.sidebarWidth,
1850
- height: "100%",
1851
- border: ["right"],
1852
- borderColor: "#444"
1853
- });
1854
- sidebar.add(this.tabBar.renderable);
1855
- const paneContainer = new BoxRenderable(this.renderer, {
1856
- id: "pane-container",
1857
- flexGrow: 1,
1858
- border: false
1859
- });
1860
- for (const name of this.names) {
1861
- const interactive = this.config.processes[name].interactive === true;
1862
- const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
1863
- this.panes.set(name, pane);
1864
- paneContainer.add(pane.scrollBox);
1865
- }
1866
- this.statusBar = new StatusBar(this.renderer);
1867
- contentRow.add(sidebar);
1868
- contentRow.add(paneContainer);
1869
- layout.add(contentRow);
1870
- layout.add(this.statusBar.renderable);
1871
- this.renderer.root.add(layout);
1872
- this.tabBar.onSelect((_index, name) => this.switchPane(name));
1873
- this.tabBar.onSelectionChanged((_index, name) => this.switchPane(name));
1874
- this.manager.on((event) => {
1875
- if (this.destroyed)
1876
- return;
1877
- if (event.type === "output") {
1878
- this.panes.get(event.name)?.feed(event.data);
1879
- if (this.config.processes[event.name]?.interactive) {
1880
- this.checkInputWaiting(event.name, event.data);
1881
- }
1882
- } else if (event.type === "status") {
1883
- const state = this.manager.getState(event.name);
1884
- this.tabBar.updateStatus(event.name, event.status, state?.exitCode, state?.restartCount);
1885
- if (event.status !== "running" && event.status !== "ready") {
1886
- this.clearInputWaiting(event.name);
1887
- }
1888
- }
1889
- });
1890
- this.renderer.on("resize", (w, h) => {
1891
- this.termCols = Math.max(40, w - this.sidebarWidth - 2);
1892
- this.termRows = Math.max(5, h - 2);
1893
- if (this.resizeTimer)
1894
- clearTimeout(this.resizeTimer);
1895
- this.resizeTimer = setTimeout(() => {
1896
- this.resizeTimer = null;
1897
- for (const pane of this.panes.values()) {
1898
- pane.resize(this.termCols, this.termRows);
1899
- }
1900
- this.manager.resizeAll(this.termCols, this.termRows);
1901
- }, 50);
1902
- });
1903
- this.renderer.keyInput.on("keypress", (key) => {
1904
- log(key);
1905
- if (key.ctrl && key.name === "c") {
1906
- if (this.searchMode) {
1907
- this.exitSearch();
1908
- return;
1909
- }
1910
- this.shutdown().then(() => {
1911
- process.exit(this.hasFailures() ? 1 : 0);
1912
- });
1913
- return;
1914
- }
1915
- if (this.searchMode) {
1916
- this.handleSearchInput(key);
1917
- return;
1918
- }
1919
- if (!this.activePane)
1920
- return;
1921
- const isInteractive = this.config.processes[this.activePane]?.interactive === true;
1922
- if (!isInteractive) {
1923
- const name = key.name.toLowerCase();
1924
- if (key.shift && name === "r") {
1925
- this.manager.restartAll(this.termCols, this.termRows);
1926
- return;
1927
- }
1928
- if (name === "f") {
1929
- this.enterSearch();
1930
- return;
1931
- }
1932
- if (name === "r") {
1933
- this.manager.restart(this.activePane, this.termCols, this.termRows);
1934
- return;
1935
- }
1936
- if (name === "s") {
1937
- const state = this.manager.getState(this.activePane);
1938
- if (state?.status === "stopped" || state?.status === "finished" || state?.status === "failed") {
1939
- this.manager.start(this.activePane, this.termCols, this.termRows);
1940
- } else {
1941
- this.manager.stop(this.activePane);
1942
- }
1943
- return;
1944
- }
1945
- if (name === "l") {
1946
- this.panes.get(this.activePane)?.clear();
1947
- return;
1948
- }
1949
- const num = Number.parseInt(name, 10);
1950
- if (num >= 1 && num <= 9 && num <= this.tabBar.count) {
1951
- this.tabBar.setSelectedIndex(num - 1);
1952
- this.switchPane(this.tabBar.getNameAtIndex(num - 1));
1953
- return;
1954
- }
1955
- if (name === "left" || name === "right") {
1956
- const current = this.tabBar.getSelectedIndex();
1957
- const count = this.tabBar.count;
1958
- const next = name === "right" ? (current + 1) % count : (current - 1 + count) % count;
1959
- this.tabBar.setSelectedIndex(next);
1960
- this.switchPane(this.tabBar.getNameAtIndex(next));
1961
- return;
1962
- }
1963
- if (name === "pageup" || name === "pagedown") {
1964
- const pane = this.panes.get(this.activePane);
1965
- const delta = this.termRows - 2;
1966
- pane?.scrollBy(name === "pageup" ? -delta : delta);
1967
- return;
1968
- }
1969
- if (name === "home") {
1970
- this.panes.get(this.activePane)?.scrollToTop();
1971
- return;
1972
- }
1973
- if (name === "end") {
1974
- this.panes.get(this.activePane)?.scrollToBottom();
1975
- return;
1976
- }
1977
- return;
1978
- }
1979
- if (key.sequence) {
1980
- this.manager.write(this.activePane, key.sequence);
1981
- }
1982
- });
1983
- if (this.names.length > 0) {
1984
- this.switchPane(this.names[0]);
1985
- this.tabBar.focus();
1986
- }
1987
- await this.manager.startAll(termCols, termRows);
1988
- }
1989
- switchPane(name) {
1990
- if (this.activePane === name)
1991
- return;
1992
- if (this.searchMode) {
1993
- this.exitSearch();
1994
- }
1995
- if (this.activePane) {
1996
- this.panes.get(this.activePane)?.hide();
1997
- }
1998
- this.activePane = name;
1999
- this.panes.get(name)?.show();
2000
- }
2001
- checkInputWaiting(name, data) {
2002
- const existing = this.inputWaitTimers.get(name);
2003
- if (existing)
2004
- clearTimeout(existing);
2005
- if (this.awaitingInput.has(name)) {
2006
- this.awaitingInput.delete(name);
2007
- this.tabBar.setInputWaiting(name, false);
2008
- }
2009
- const lastByte = data[data.length - 1];
2010
- if (lastByte !== 10 && lastByte !== 13) {
2011
- const timer = setTimeout(() => {
2012
- this.inputWaitTimers.delete(name);
2013
- const state = this.manager.getState(name);
2014
- if (state && (state.status === "running" || state.status === "ready")) {
2015
- this.awaitingInput.add(name);
2016
- this.tabBar.setInputWaiting(name, true);
2017
- }
2018
- }, 200);
2019
- this.inputWaitTimers.set(name, timer);
2020
- }
2021
- }
2022
- clearInputWaiting(name) {
2023
- const timer = this.inputWaitTimers.get(name);
2024
- if (timer) {
2025
- clearTimeout(timer);
2026
- this.inputWaitTimers.delete(name);
2027
- }
2028
- if (this.awaitingInput.has(name)) {
2029
- this.awaitingInput.delete(name);
2030
- this.tabBar.setInputWaiting(name, false);
2031
- }
2032
- }
2033
- enterSearch() {
2034
- this.searchMode = true;
2035
- this.searchQuery = "";
2036
- this.searchMatches = [];
2037
- this.searchIndex = -1;
2038
- this.statusBar.setSearchMode(true);
2039
- }
2040
- exitSearch() {
2041
- this.searchMode = false;
2042
- this.searchQuery = "";
2043
- this.searchMatches = [];
2044
- this.searchIndex = -1;
2045
- if (this.activePane) {
2046
- this.panes.get(this.activePane)?.clearHighlights();
2047
- }
2048
- this.statusBar.setSearchMode(false);
2049
- }
2050
- handleSearchInput(key) {
2051
- if (key.name === "escape") {
2052
- this.exitSearch();
2053
- return;
2054
- }
2055
- if (key.name === "return") {
2056
- if (this.searchMatches.length === 0)
2057
- return;
2058
- if (key.shift) {
2059
- this.searchIndex = (this.searchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
2060
- } else {
2061
- this.searchIndex = (this.searchIndex + 1) % this.searchMatches.length;
2062
- }
2063
- this.scrollToCurrentMatch();
2064
- this.updateSearchHighlights();
2065
- return;
2066
- }
2067
- if (key.name === "backspace") {
2068
- if (this.searchQuery.length > 0) {
2069
- this.searchQuery = this.searchQuery.slice(0, -1);
2070
- this.runSearch();
2071
- }
2072
- return;
2073
- }
2074
- if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
2075
- this.searchQuery += key.sequence;
2076
- this.runSearch();
2077
- }
2078
- }
2079
- runSearch() {
2080
- if (!this.activePane)
2081
- return;
2082
- const pane = this.panes.get(this.activePane);
2083
- if (!pane)
2084
- return;
2085
- this.searchMatches = pane.search(this.searchQuery);
2086
- this.searchIndex = this.searchMatches.length > 0 ? 0 : -1;
2087
- this.updateSearchHighlights();
2088
- if (this.searchIndex >= 0) {
2089
- this.scrollToCurrentMatch();
2090
- }
2091
- }
2092
- updateSearchHighlights() {
2093
- if (!this.activePane)
2094
- return;
2095
- const pane = this.panes.get(this.activePane);
2096
- if (!pane)
2097
- return;
2098
- if (this.searchMatches.length > 0) {
2099
- pane.setHighlights(this.searchMatches, this.searchIndex);
2100
- } else {
2101
- pane.clearHighlights();
2102
- }
2103
- this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
2104
- }
2105
- scrollToCurrentMatch() {
2106
- if (!this.activePane || this.searchIndex < 0)
2107
- return;
2108
- const pane = this.panes.get(this.activePane);
2109
- if (!pane)
2110
- return;
2111
- const match = this.searchMatches[this.searchIndex];
2112
- pane.scrollToLine(match.line);
2113
- }
2114
- async shutdown() {
2115
- if (this.destroyed)
2116
- return;
2117
- this.destroyed = true;
2118
- if (this.resizeTimer) {
2119
- clearTimeout(this.resizeTimer);
2120
- this.resizeTimer = null;
2121
- }
2122
- for (const timer of this.inputWaitTimers.values()) {
2123
- clearTimeout(timer);
2124
- }
2125
- this.inputWaitTimers.clear();
2126
- await this.manager.stopAll();
2127
- for (const pane of this.panes.values()) {
2128
- pane.destroy();
2129
- }
2130
- if (!this.renderer.isDestroyed) {
2131
- this.renderer.destroy();
2132
- }
2133
- }
2134
- hasFailures() {
2135
- return this.manager.getAllStates().some((s) => s.status === "failed");
2136
- }
2137
- }
2138
-
2139
- // src/ui/prefix.ts
2140
- var RESET = ANSI_RESET;
2141
- var DIM = "\x1B[90m";
2142
-
2143
- class PrefixDisplay {
2144
- manager;
2145
- colors;
2146
- noColor;
2147
- decoders = new Map;
2148
- buffers = new Map;
2149
- maxNameLen;
2150
- logWriter;
2151
- killOthers;
2152
- timestamps;
2153
- stopping = false;
2154
- constructor(manager, config, options = {}) {
2155
- this.manager = manager;
2156
- this.logWriter = options.logWriter;
2157
- this.killOthers = options.killOthers ?? false;
2158
- this.timestamps = options.timestamps ?? false;
2159
- this.noColor = "NO_COLOR" in process.env;
2160
- const names = manager.getProcessNames();
2161
- this.maxNameLen = Math.max(...names.map((n) => n.length));
2162
- this.colors = buildProcessColorMap(names, config);
2163
- for (const name of names) {
2164
- this.decoders.set(name, new TextDecoder("utf-8", { fatal: false }));
2165
- this.buffers.set(name, "");
2166
- }
2167
- }
2168
- async start() {
2169
- const handler = (event) => {
2170
- this.logWriter?.handleEvent(event);
2171
- this.handleEvent(event);
2172
- };
2173
- this.manager.on(handler);
2174
- process.on("SIGINT", () => this.shutdown());
2175
- process.on("SIGTERM", () => this.shutdown());
2176
- process.on("uncaughtException", (err) => {
2177
- process.stderr.write(`numux: unexpected error: ${err?.stack ?? err}
2178
- `);
2179
- this.shutdown();
2180
- });
2181
- process.on("unhandledRejection", (reason) => {
2182
- const message = reason instanceof Error ? reason.message : String(reason);
2183
- process.stderr.write(`numux: unhandled rejection: ${message}
2184
- `);
2185
- this.shutdown();
2186
- });
2187
- const cols = process.stdout.columns || 80;
2188
- const rows = process.stdout.rows || 24;
2189
- await this.manager.startAll(cols, rows);
2190
- this.checkAllDone();
2191
- }
2192
- handleEvent(event) {
2193
- if (event.type === "output") {
2194
- this.handleOutput(event.name, event.data);
2195
- } else if (event.type === "status") {
2196
- this.handleStatus(event.name, event.status);
2197
- } else if (event.type === "exit") {
2198
- this.flushBuffer(event.name);
2199
- if (this.killOthers) {
2200
- this.killAllAndExit(event.name);
2201
- } else {
2202
- this.checkAllDone();
2203
- }
2204
- }
2205
- }
2206
- handleOutput(name, data) {
2207
- const decoder = this.decoders.get(name) ?? new TextDecoder;
2208
- const text = decoder.decode(data, { stream: true });
2209
- const buffer = (this.buffers.get(name) ?? "") + text;
2210
- const lines = buffer.split(/\r?\n/);
2211
- this.buffers.set(name, lines.pop() ?? "");
2212
- for (const line of lines) {
2213
- this.printLine(name, line);
2214
- }
2215
- }
2216
- handleStatus(name, status) {
2217
- if (status === "ready" || status === "failed" || status === "finished" || status === "stopped" || status === "skipped") {
2218
- if (this.noColor) {
2219
- this.printLine(name, `\u2192 ${status}`);
2220
- } else {
2221
- const ansi = STATUS_ANSI[status];
2222
- const statusText = ansi ? `${ansi}${status}${RESET}` : status;
2223
- this.printLine(name, `${DIM}\u2192 ${statusText}${DIM}${RESET}`);
2224
- }
2225
- }
2226
- }
2227
- formatTimestamp() {
2228
- const now = new Date;
2229
- const h = String(now.getHours()).padStart(2, "0");
2230
- const m = String(now.getMinutes()).padStart(2, "0");
2231
- const s = String(now.getSeconds()).padStart(2, "0");
2232
- return `${h}:${m}:${s}`;
2233
- }
2234
- printLine(name, line) {
2235
- const padded = name.padEnd(this.maxNameLen);
2236
- const ts = this.timestamps ? `${DIM}[${this.formatTimestamp()}]${RESET} ` : "";
2237
- const tsPlain = this.timestamps ? `[${this.formatTimestamp()}] ` : "";
2238
- if (this.noColor) {
2239
- process.stdout.write(`${tsPlain}[${padded}] ${stripAnsi(line)}
2240
- `);
2241
- } else {
2242
- const color = this.colors.get(name) ?? "";
2243
- process.stdout.write(`${ts}${color}[${padded}]${RESET} ${line}
2244
- `);
2245
- }
2246
- }
2247
- flushBuffer(name) {
2248
- const remaining = this.buffers.get(name) ?? "";
2249
- if (remaining.length > 0) {
2250
- this.printLine(name, remaining);
2251
- this.buffers.set(name, "");
2252
- }
2253
- }
2254
- checkAllDone() {
2255
- if (this.stopping)
2256
- return;
2257
- const states = this.manager.getAllStates();
2258
- const allDone = states.every((s) => s.status === "stopped" || s.status === "finished" || s.status === "failed" || s.status === "skipped");
2259
- if (allDone) {
2260
- this.printSummary();
2261
- this.logWriter?.close();
2262
- const anyFailed = states.some((s) => s.status === "failed");
2263
- process.exit(anyFailed ? 1 : 0);
2264
- }
2265
- }
2266
- killAllAndExit(exitedName) {
2267
- if (this.stopping)
2268
- return;
2269
- this.stopping = true;
2270
- const state = this.manager.getState(exitedName);
2271
- const code = state?.exitCode ?? 1;
2272
- this.manager.stopAll().then(() => {
2273
- for (const name of this.manager.getProcessNames()) {
2274
- this.flushBuffer(name);
2275
- }
2276
- this.printSummary();
2277
- this.logWriter?.close();
2278
- process.exit(code === 0 ? 0 : 1);
2279
- });
2280
- }
2281
- printSummary() {
2282
- const states = this.manager.getAllStates();
2283
- const namePad = Math.max(...states.map((s) => s.name.length));
2284
- process.stdout.write(`
2285
- `);
2286
- for (const s of states) {
2287
- const name = s.name.padEnd(namePad);
2288
- const exitStr = s.exitCode !== null ? `exit ${s.exitCode}` : "";
2289
- if (this.noColor) {
2290
- process.stdout.write(` ${name} ${s.status}${exitStr ? ` (${exitStr})` : ""}
2291
- `);
2292
- } else {
2293
- const ansi = STATUS_ANSI[s.status] ?? "";
2294
- const statusText = ansi ? `${ansi}${s.status}${RESET}` : s.status;
2295
- process.stdout.write(` ${name} ${statusText}${exitStr ? ` ${DIM}(${exitStr})${RESET}` : ""}
2296
- `);
2297
- }
2298
- }
2299
- }
2300
- async shutdown() {
2301
- if (this.stopping)
2302
- return;
2303
- this.stopping = true;
2304
- await this.manager.stopAll();
2305
- for (const name of this.manager.getProcessNames()) {
2306
- this.flushBuffer(name);
2307
- }
2308
- this.logWriter?.close();
2309
- const anyFailed = this.manager.getAllStates().some((s) => s.status === "failed");
2310
- process.exit(anyFailed ? 1 : 0);
2311
- }
2312
- }
2313
-
2314
- // src/utils/log-writer.ts
2315
- import { closeSync, mkdirSync as mkdirSync2, openSync, writeSync } from "fs";
2316
- import { join } from "path";
2317
- class LogWriter {
2318
- dir;
2319
- files = new Map;
2320
- decoder = new TextDecoder;
2321
- encoder = new TextEncoder;
2322
- constructor(dir) {
2323
- this.dir = dir;
2324
- mkdirSync2(dir, { recursive: true });
2325
- }
2326
- errored = false;
2327
- handleEvent = (event) => {
2328
- if (event.type !== "output" || this.errored)
2329
- return;
2330
- try {
2331
- let fd = this.files.get(event.name);
2332
- if (fd === undefined) {
2333
- const path = join(this.dir, `${event.name}.log`);
2334
- fd = openSync(path, "w");
2335
- this.files.set(event.name, fd);
2336
- }
2337
- const text = this.decoder.decode(event.data, { stream: true });
2338
- const clean = stripAnsi(text);
2339
- writeSync(fd, this.encoder.encode(clean));
2340
- } catch {
2341
- this.errored = true;
2342
- process.stderr.write(`numux: log writing failed for ${this.dir}, disabling log output
2343
- `);
2344
- }
2345
- };
2346
- close() {
2347
- for (const fd of this.files.values()) {
2348
- closeSync(fd);
2349
- }
2350
- this.files.clear();
2351
- }
2352
- }
2353
-
2354
- // src/utils/shutdown.ts
2355
- function setupShutdownHandlers(app, logWriter) {
2356
- let shuttingDown = false;
2357
- const shutdown = () => {
2358
- if (shuttingDown) {
2359
- process.exit(1);
2360
- }
2361
- shuttingDown = true;
2362
- app.shutdown().finally(() => {
2363
- logWriter?.close();
2364
- process.exit(app.hasFailures() ? 1 : 0);
2365
- });
2366
- };
2367
- process.on("SIGINT", shutdown);
2368
- process.on("SIGTERM", shutdown);
2369
- process.on("uncaughtException", (err) => {
2370
- log("Uncaught exception:", err?.message ?? err);
2371
- process.stderr.write(`numux: unexpected error: ${err?.stack ?? err}
2372
- `);
2373
- app.shutdown().finally(() => {
2374
- logWriter?.close();
2375
- process.exit(1);
2376
- });
2377
- });
2378
- process.on("unhandledRejection", (reason) => {
2379
- const message = reason instanceof Error ? reason.message : String(reason);
2380
- log("Unhandled rejection:", message);
2381
- process.stderr.write(`numux: unhandled rejection: ${message}
2382
- `);
2383
- app.shutdown().finally(() => {
2384
- logWriter?.close();
2385
- process.exit(1);
2386
- });
2387
- });
2388
- }
2389
-
2390
- // src/index.ts
2391
- var HELP = `numux \u2014 terminal multiplexer with dependency orchestration
2392
-
2393
- Usage:
2394
- numux Run processes from config file
2395
- numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel
2396
- numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands
2397
- numux init Create a starter config file
2398
- numux validate Validate config and show process graph
2399
- numux exec <name> [--] <cmd> Run a command in a process's environment
2400
- numux completions <shell> Generate shell completions (bash, zsh, fish)
2401
-
2402
- Options:
2403
- -n, --name <name=command> Add a named process
2404
- -c, --color <colors> Comma-separated colors for processes (hex, e.g. #ff0,#0f0)
2405
- --config <path> Config file path (default: auto-detect)
2406
- -p, --prefix Prefixed output mode (no TUI, for CI/scripts)
2407
- --only <a,b,...> Only run these processes (+ their dependencies)
2408
- --exclude <a,b,...> Exclude these processes
2409
- --kill-others Kill all processes when any exits
2410
- --no-restart Disable auto-restart for crashed processes
2411
- --no-watch Disable file watching even if config has watch patterns
2412
- -t, --timestamps Add timestamps to prefixed output lines
2413
- --log-dir <path> Write per-process logs to directory
2414
- --debug Enable debug logging to .numux/debug.log
2415
- -h, --help Show this help
2416
- -v, --version Show version
2417
-
2418
- Config files (auto-detected):
2419
- numux.config.ts, numux.config.js`;
2420
- var INIT_TEMPLATE = `import { defineConfig } from 'numux'
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+ const child = spawn('bun', ['run', join(__dirname, 'numux.js'), ...process.argv.slice(2)], {
8
+ stdio: 'inherit'
9
+ })
2421
10
 
2422
- export default defineConfig({
2423
- // Global options (inherited by all processes):
2424
- // cwd: './packages/backend',
2425
- // env: { NODE_ENV: 'development' },
2426
- // envFile: '.env',
11
+ child.on('exit', (code, signal) => {
12
+ if (signal) process.kill(process.pid, signal)
13
+ else process.exit(code ?? 1)
14
+ })
2427
15
 
2428
- processes: {
2429
- // dev: 'npm run dev',
2430
- // api: {
2431
- // command: 'npm run dev:api',
2432
- // readyPattern: 'listening on port',
2433
- // watch: 'src/**/*.ts',
2434
- // },
2435
- // web: {
2436
- // command: 'npm run dev:web',
2437
- // dependsOn: ['api'],
2438
- // },
2439
- },
16
+ child.on('error', err => {
17
+ if (err.code === 'ENOENT') {
18
+ console.error('numux requires bun. Install it: https://bun.sh')
19
+ } else {
20
+ console.error(err.message)
21
+ }
22
+ process.exit(1)
2440
23
  })
2441
- `;
2442
- async function main() {
2443
- const parsed = parseArgs(process.argv);
2444
- if (parsed.help) {
2445
- console.info(HELP);
2446
- process.exit(0);
2447
- }
2448
- if (parsed.version) {
2449
- const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
2450
- console.info(`numux v${(pkg.default ?? pkg).version}`);
2451
- process.exit(0);
2452
- }
2453
- if (parsed.init) {
2454
- const target = resolve7("numux.config.ts");
2455
- if (existsSync4(target)) {
2456
- console.error(`Already exists: ${target}`);
2457
- process.exit(1);
2458
- }
2459
- writeFileSync(target, INIT_TEMPLATE);
2460
- console.info(`Created ${target}`);
2461
- process.exit(0);
2462
- }
2463
- if (parsed.completions) {
2464
- console.info(generateCompletions(parsed.completions));
2465
- process.exit(0);
2466
- }
2467
- if (parsed.validate) {
2468
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
2469
- const warnings2 = [];
2470
- let config2 = validateConfig(raw, warnings2);
2471
- if (parsed.only || parsed.exclude) {
2472
- config2 = filterConfig(config2, parsed.only, parsed.exclude);
2473
- }
2474
- const tiers = resolveDependencyTiers(config2);
2475
- const names = Object.keys(config2.processes);
2476
- const filterNote = parsed.only || parsed.exclude ? " (filtered)" : "";
2477
- console.info(`Config valid: ${names.length} process${names.length === 1 ? "" : "es"}${filterNote}
2478
- `);
2479
- for (let i = 0;i < tiers.length; i++) {
2480
- console.info(`Tier ${i}:`);
2481
- for (const name of tiers[i]) {
2482
- const proc = config2.processes[name];
2483
- const flags = [];
2484
- if (proc.dependsOn?.length)
2485
- flags.push(`depends on: ${proc.dependsOn.join(", ")}`);
2486
- if (proc.readyPattern)
2487
- flags.push(`ready: /${proc.readyPattern}/`);
2488
- if (proc.persistent === false)
2489
- flags.push("one-shot");
2490
- if (proc.delay)
2491
- flags.push(`delay: ${proc.delay}ms`);
2492
- if (proc.condition)
2493
- flags.push(`if: ${proc.condition}`);
2494
- if (proc.watch) {
2495
- const patterns = Array.isArray(proc.watch) ? proc.watch : [proc.watch];
2496
- flags.push(`watch: ${patterns.join(", ")}`);
2497
- }
2498
- const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
2499
- console.info(` ${name}: ${proc.command}${suffix}`);
2500
- }
2501
- }
2502
- printWarnings(warnings2);
2503
- process.exit(0);
2504
- }
2505
- if (parsed.exec) {
2506
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
2507
- const config2 = validateConfig(raw);
2508
- const proc = config2.processes[parsed.execName];
2509
- if (!proc) {
2510
- const names = Object.keys(config2.processes);
2511
- throw new Error(`Unknown process "${parsed.execName}". Available: ${names.join(", ")}`);
2512
- }
2513
- const cwd = proc.cwd ? resolve7(proc.cwd) : process.cwd();
2514
- const envFromFile = proc.envFile ? loadEnvFiles(proc.envFile, cwd) : {};
2515
- const env = {
2516
- ...process.env,
2517
- ...envFromFile,
2518
- ...proc.env
2519
- };
2520
- const child = Bun.spawn(["sh", "-c", parsed.execCommand], {
2521
- cwd,
2522
- env,
2523
- stdout: "inherit",
2524
- stdin: "inherit",
2525
- stderr: "inherit"
2526
- });
2527
- process.exit(await child.exited);
2528
- }
2529
- if (parsed.debug) {
2530
- enableDebugLog();
2531
- }
2532
- let config;
2533
- const warnings = [];
2534
- if (parsed.commands.length > 0 || parsed.named.length > 0) {
2535
- const hasNpmPatterns = parsed.commands.some((c) => c.startsWith("npm:"));
2536
- if (hasNpmPatterns) {
2537
- const npmPatterns = parsed.commands.filter((c) => c.startsWith("npm:"));
2538
- const otherCommands = parsed.commands.filter((c) => !c.startsWith("npm:"));
2539
- const processes = {};
2540
- for (const pattern of npmPatterns) {
2541
- const entry = {};
2542
- if (parsed.colors?.length)
2543
- entry.color = parsed.colors;
2544
- processes[pattern] = entry;
2545
- }
2546
- for (let i = 0;i < otherCommands.length; i++) {
2547
- const cmd = otherCommands[i];
2548
- let name = cmd.split(/\s+/)[0].split("/").pop();
2549
- if (processes[name])
2550
- name = `${name}-${i}`;
2551
- processes[name] = cmd;
2552
- }
2553
- for (const { name, command } of parsed.named) {
2554
- processes[name] = command;
2555
- }
2556
- const expanded = expandScriptPatterns({ processes });
2557
- config = validateConfig(expanded, warnings);
2558
- } else {
2559
- config = buildConfigFromArgs(parsed.commands, parsed.named, {
2560
- noRestart: parsed.noRestart,
2561
- colors: parsed.colors
2562
- });
2563
- }
2564
- } else {
2565
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
2566
- config = validateConfig(raw, warnings);
2567
- if (parsed.noRestart) {
2568
- for (const proc of Object.values(config.processes)) {
2569
- proc.maxRestarts = 0;
2570
- }
2571
- }
2572
- }
2573
- if (parsed.noWatch) {
2574
- for (const proc of Object.values(config.processes)) {
2575
- delete proc.watch;
2576
- }
2577
- }
2578
- if (parsed.only || parsed.exclude) {
2579
- config = filterConfig(config, parsed.only, parsed.exclude);
2580
- }
2581
- const manager = new ProcessManager(config);
2582
- let logWriter;
2583
- if (parsed.logDir) {
2584
- logWriter = new LogWriter(parsed.logDir);
2585
- }
2586
- printWarnings(warnings);
2587
- if (parsed.prefix) {
2588
- const display = new PrefixDisplay(manager, config, {
2589
- logWriter,
2590
- killOthers: parsed.killOthers,
2591
- timestamps: parsed.timestamps
2592
- });
2593
- await display.start();
2594
- } else {
2595
- if (logWriter) {
2596
- manager.on(logWriter.handleEvent);
2597
- }
2598
- const app = new App(manager, config);
2599
- setupShutdownHandlers(app, logWriter);
2600
- await app.start();
2601
- }
2602
- }
2603
- function printWarnings(warnings) {
2604
- for (const w of warnings) {
2605
- console.warn(`Warning: process "${w.process}": ${w.message}`);
2606
- }
2607
- }
2608
- main().catch((err) => {
2609
- console.error(err instanceof Error ? err.message : err);
2610
- process.exit(1);
2611
- });