numux 1.10.0 → 1.10.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.
- package/README.md +1 -0
- package/dist/numux.js +410 -181
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -226,6 +226,7 @@ Each process accepts:
|
|
|
226
226
|
| `color` | `string \| string[]` | auto | Hex (e.g. `"#ff6600"`) or basic name: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple |
|
|
227
227
|
| `watch` | `string \| string[]` | — | Glob patterns — restart process when matching files change |
|
|
228
228
|
| `interactive` | `boolean` | `false` | When `true`, keyboard input is forwarded to the process |
|
|
229
|
+
| `errorMatcher` | `boolean \| string` | — | `true` detects ANSI red output, string = regex pattern — shows error indicator on tab |
|
|
229
230
|
|
|
230
231
|
### File watching
|
|
231
232
|
|
package/dist/numux.js
CHANGED
|
@@ -22,7 +22,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
|
|
|
22
22
|
var require_package = __commonJS((exports, module) => {
|
|
23
23
|
module.exports = {
|
|
24
24
|
name: "numux",
|
|
25
|
-
version: "1.10.
|
|
25
|
+
version: "1.10.2",
|
|
26
26
|
description: "Terminal multiplexer with dependency orchestration",
|
|
27
27
|
type: "module",
|
|
28
28
|
license: "MIT",
|
|
@@ -78,7 +78,227 @@ var require_package = __commonJS((exports, module) => {
|
|
|
78
78
|
import { existsSync as existsSync5, writeFileSync } from "fs";
|
|
79
79
|
import { resolve as resolve8 } from "path";
|
|
80
80
|
|
|
81
|
+
// src/cli-flags.ts
|
|
82
|
+
var commaSplit = (raw) => raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
83
|
+
var FLAGS = [
|
|
84
|
+
{
|
|
85
|
+
type: "value",
|
|
86
|
+
long: "--workspace",
|
|
87
|
+
short: "-w",
|
|
88
|
+
key: "workspace",
|
|
89
|
+
description: "Run a package.json script across all workspaces",
|
|
90
|
+
valueName: "<script>",
|
|
91
|
+
completionHint: "none"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: "value",
|
|
95
|
+
long: "--name",
|
|
96
|
+
short: "-n",
|
|
97
|
+
key: "named",
|
|
98
|
+
description: "Add a named process",
|
|
99
|
+
valueName: "<name=command>",
|
|
100
|
+
completionHint: "none",
|
|
101
|
+
parse(raw) {
|
|
102
|
+
const eq = raw.indexOf("=");
|
|
103
|
+
if (eq < 1) {
|
|
104
|
+
throw new Error(`Invalid --name value: expected "name=command", got "${raw}"`);
|
|
105
|
+
}
|
|
106
|
+
return { name: raw.slice(0, eq), command: raw.slice(eq + 1) };
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: "value",
|
|
111
|
+
long: "--color",
|
|
112
|
+
short: "-c",
|
|
113
|
+
key: "colors",
|
|
114
|
+
description: "Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)",
|
|
115
|
+
valueName: "<colors>",
|
|
116
|
+
completionHint: "none",
|
|
117
|
+
parse: commaSplit
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: "boolean",
|
|
121
|
+
long: "--colors",
|
|
122
|
+
key: "autoColors",
|
|
123
|
+
description: "Auto-assign colors to processes based on their name"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: "value",
|
|
127
|
+
long: "--config",
|
|
128
|
+
key: "configPath",
|
|
129
|
+
description: "Config file path (default: auto-detect)",
|
|
130
|
+
valueName: "<path>",
|
|
131
|
+
completionHint: "file"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: "boolean",
|
|
135
|
+
long: "--prefix",
|
|
136
|
+
short: "-p",
|
|
137
|
+
key: "prefix",
|
|
138
|
+
description: "Prefixed output mode (no TUI, for CI/scripts)"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: "value",
|
|
142
|
+
long: "--only",
|
|
143
|
+
key: "only",
|
|
144
|
+
description: "Only run these processes (+ their dependencies)",
|
|
145
|
+
valueName: "<a,b,...>",
|
|
146
|
+
completionHint: "none",
|
|
147
|
+
parse: commaSplit
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: "value",
|
|
151
|
+
long: "--exclude",
|
|
152
|
+
key: "exclude",
|
|
153
|
+
description: "Exclude these processes",
|
|
154
|
+
valueName: "<a,b,...>",
|
|
155
|
+
completionHint: "none",
|
|
156
|
+
parse: commaSplit
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: "boolean",
|
|
160
|
+
long: "--kill-others",
|
|
161
|
+
key: "killOthers",
|
|
162
|
+
description: "Kill all processes when any exits"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: "boolean",
|
|
166
|
+
long: "--no-restart",
|
|
167
|
+
key: "noRestart",
|
|
168
|
+
description: "Disable auto-restart for crashed processes"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: "boolean",
|
|
172
|
+
long: "--no-watch",
|
|
173
|
+
key: "noWatch",
|
|
174
|
+
description: "Disable file watching even if config has watch patterns"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
type: "boolean",
|
|
178
|
+
long: "--timestamps",
|
|
179
|
+
short: "-t",
|
|
180
|
+
key: "timestamps",
|
|
181
|
+
description: "Add timestamps to prefixed output lines"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: "value",
|
|
185
|
+
long: "--log-dir",
|
|
186
|
+
key: "logDir",
|
|
187
|
+
description: "Write per-process logs to directory",
|
|
188
|
+
valueName: "<path>",
|
|
189
|
+
completionHint: "directory"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
type: "boolean",
|
|
193
|
+
long: "--debug",
|
|
194
|
+
key: "debug",
|
|
195
|
+
description: "Enable debug logging to .numux/debug.log"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: "boolean",
|
|
199
|
+
long: "--help",
|
|
200
|
+
short: "-h",
|
|
201
|
+
key: "help",
|
|
202
|
+
description: "Show this help"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
type: "boolean",
|
|
206
|
+
long: "--version",
|
|
207
|
+
short: "-v",
|
|
208
|
+
key: "version",
|
|
209
|
+
description: "Show version"
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
var SUBCOMMANDS = [
|
|
213
|
+
{
|
|
214
|
+
name: "init",
|
|
215
|
+
description: "Create a starter config file",
|
|
216
|
+
parse: (_args, i, result) => {
|
|
217
|
+
result.init = true;
|
|
218
|
+
return i;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "validate",
|
|
223
|
+
description: "Validate config and show process graph",
|
|
224
|
+
parse: (_args, i, result) => {
|
|
225
|
+
result.validate = true;
|
|
226
|
+
return i;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "exec",
|
|
231
|
+
description: "Run a command in a process's environment",
|
|
232
|
+
usage: "exec <name> [--] <cmd>",
|
|
233
|
+
parse: (args, i, result) => {
|
|
234
|
+
result.exec = true;
|
|
235
|
+
const name = args[++i];
|
|
236
|
+
if (!name)
|
|
237
|
+
throw new Error("exec requires a process name");
|
|
238
|
+
result.execName = name;
|
|
239
|
+
if (args[i + 1] === "--")
|
|
240
|
+
i++;
|
|
241
|
+
const rest = args.slice(i + 1);
|
|
242
|
+
if (rest.length === 0)
|
|
243
|
+
throw new Error("exec requires a command to run");
|
|
244
|
+
result.execCommand = rest.join(" ");
|
|
245
|
+
return "break";
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "completions",
|
|
250
|
+
description: "Generate shell completions (bash, zsh, fish)",
|
|
251
|
+
usage: "completions <shell>",
|
|
252
|
+
parse: (args, i, result) => {
|
|
253
|
+
const next = args[++i];
|
|
254
|
+
if (next === undefined)
|
|
255
|
+
throw new Error("Missing value for completions");
|
|
256
|
+
result.completions = next;
|
|
257
|
+
return i;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
];
|
|
261
|
+
function generateHelp() {
|
|
262
|
+
const lines = [
|
|
263
|
+
"numux \u2014 terminal multiplexer with dependency orchestration",
|
|
264
|
+
"",
|
|
265
|
+
"Usage:",
|
|
266
|
+
" numux Run processes from config file",
|
|
267
|
+
" numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel",
|
|
268
|
+
" numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands",
|
|
269
|
+
" numux -w <script> Run a script across all workspaces"
|
|
270
|
+
];
|
|
271
|
+
for (const sub of SUBCOMMANDS) {
|
|
272
|
+
const label = ` numux ${sub.usage ?? sub.name}`;
|
|
273
|
+
lines.push(`${label.padEnd(33)}${sub.description}`);
|
|
274
|
+
}
|
|
275
|
+
lines.push("", "Options:");
|
|
276
|
+
for (const f of FLAGS) {
|
|
277
|
+
const parts = [];
|
|
278
|
+
if (f.short)
|
|
279
|
+
parts.push(`${f.short},`);
|
|
280
|
+
parts.push(f.long);
|
|
281
|
+
if (f.type === "value")
|
|
282
|
+
parts.push(f.valueName);
|
|
283
|
+
const left = ` ${parts.join(" ")}`;
|
|
284
|
+
lines.push(`${left.padEnd(29)}${f.description}`);
|
|
285
|
+
}
|
|
286
|
+
lines.push("", "Config files (auto-detected):", " numux.config.ts, numux.config.js");
|
|
287
|
+
return lines.join(`
|
|
288
|
+
`);
|
|
289
|
+
}
|
|
290
|
+
|
|
81
291
|
// src/cli.ts
|
|
292
|
+
var flagByName = new Map;
|
|
293
|
+
for (const f of FLAGS) {
|
|
294
|
+
flagByName.set(f.long, f);
|
|
295
|
+
if (f.short)
|
|
296
|
+
flagByName.set(f.short, f);
|
|
297
|
+
}
|
|
298
|
+
var subcommandByName = new Map;
|
|
299
|
+
for (const s of SUBCOMMANDS) {
|
|
300
|
+
subcommandByName.set(s.name, s);
|
|
301
|
+
}
|
|
82
302
|
function parseArgs(argv) {
|
|
83
303
|
const result = {
|
|
84
304
|
help: false,
|
|
@@ -99,76 +319,35 @@ function parseArgs(argv) {
|
|
|
99
319
|
};
|
|
100
320
|
const args = argv.slice(2);
|
|
101
321
|
let i = 0;
|
|
102
|
-
const consumeValue = (flag) => {
|
|
103
|
-
const next = args[++i];
|
|
104
|
-
if (next === undefined) {
|
|
105
|
-
throw new Error(`Missing value for ${flag}`);
|
|
106
|
-
}
|
|
107
|
-
return next;
|
|
108
|
-
};
|
|
109
322
|
while (i < args.length) {
|
|
110
323
|
const arg = args[i];
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} else if (arg === "--no-watch") {
|
|
128
|
-
result.noWatch = true;
|
|
129
|
-
} else if (arg === "--colors") {
|
|
130
|
-
result.autoColors = true;
|
|
131
|
-
} else if (arg === "--config") {
|
|
132
|
-
result.configPath = consumeValue(arg);
|
|
133
|
-
} else if (arg === "-c" || arg === "--color") {
|
|
134
|
-
result.colors = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
|
|
135
|
-
} else if (arg === "--log-dir") {
|
|
136
|
-
result.logDir = consumeValue(arg);
|
|
137
|
-
} else if (arg === "--only") {
|
|
138
|
-
result.only = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
|
|
139
|
-
} else if (arg === "--exclude") {
|
|
140
|
-
result.exclude = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
|
|
141
|
-
} else if (arg === "-n" || arg === "--name") {
|
|
142
|
-
const value = consumeValue(arg);
|
|
143
|
-
const eq = value.indexOf("=");
|
|
144
|
-
if (eq < 1) {
|
|
145
|
-
throw new Error(`Invalid --name value: expected "name=command", got "${value}"`);
|
|
324
|
+
const flag = flagByName.get(arg);
|
|
325
|
+
if (flag) {
|
|
326
|
+
if (flag.type === "boolean") {
|
|
327
|
+
result[flag.key] = true;
|
|
328
|
+
} else {
|
|
329
|
+
const next = args[++i];
|
|
330
|
+
if (next === undefined) {
|
|
331
|
+
throw new Error(`Missing value for ${arg}`);
|
|
332
|
+
}
|
|
333
|
+
const value = flag.parse ? flag.parse(next, arg) : next;
|
|
334
|
+
const current = result[flag.key];
|
|
335
|
+
if (Array.isArray(current)) {
|
|
336
|
+
current.push(value);
|
|
337
|
+
} else {
|
|
338
|
+
result[flag.key] = value;
|
|
339
|
+
}
|
|
146
340
|
}
|
|
147
|
-
result.named.push({
|
|
148
|
-
name: value.slice(0, eq),
|
|
149
|
-
command: value.slice(eq + 1)
|
|
150
|
-
});
|
|
151
|
-
} else if (arg === "init" && result.commands.length === 0) {
|
|
152
|
-
result.init = true;
|
|
153
|
-
} else if (arg === "validate" && result.commands.length === 0) {
|
|
154
|
-
result.validate = true;
|
|
155
|
-
} else if (arg === "exec" && result.commands.length === 0) {
|
|
156
|
-
result.exec = true;
|
|
157
|
-
const name = args[++i];
|
|
158
|
-
if (!name)
|
|
159
|
-
throw new Error("exec requires a process name");
|
|
160
|
-
result.execName = name;
|
|
161
|
-
if (args[i + 1] === "--")
|
|
162
|
-
i++;
|
|
163
|
-
const rest = args.slice(i + 1);
|
|
164
|
-
if (rest.length === 0)
|
|
165
|
-
throw new Error("exec requires a command to run");
|
|
166
|
-
result.execCommand = rest.join(" ");
|
|
167
|
-
break;
|
|
168
|
-
} else if (arg === "completions" && result.commands.length === 0) {
|
|
169
|
-
result.completions = consumeValue(arg);
|
|
170
341
|
} else if (!arg.startsWith("-")) {
|
|
171
|
-
result.commands.
|
|
342
|
+
const sub = result.commands.length === 0 ? subcommandByName.get(arg) : undefined;
|
|
343
|
+
if (sub) {
|
|
344
|
+
const ret = sub.parse(args, i, result);
|
|
345
|
+
if (ret === "break")
|
|
346
|
+
break;
|
|
347
|
+
i = ret;
|
|
348
|
+
} else {
|
|
349
|
+
result.commands.push(arg);
|
|
350
|
+
}
|
|
172
351
|
} else {
|
|
173
352
|
throw new Error(`Unknown option: ${arg}`);
|
|
174
353
|
}
|
|
@@ -259,7 +438,36 @@ function generateCompletions(shell) {
|
|
|
259
438
|
throw new Error(`Unknown shell: "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
|
|
260
439
|
}
|
|
261
440
|
}
|
|
441
|
+
function longName(f) {
|
|
442
|
+
return f.long.replace(/^-+/, "");
|
|
443
|
+
}
|
|
444
|
+
function sq(s) {
|
|
445
|
+
return s.replace(/'/g, "'\\''");
|
|
446
|
+
}
|
|
262
447
|
function bashCompletions() {
|
|
448
|
+
const caseEntries = [];
|
|
449
|
+
for (const f of FLAGS) {
|
|
450
|
+
if (f.type !== "value")
|
|
451
|
+
continue;
|
|
452
|
+
const names = f.short ? `${f.short}|${f.long}` : f.long;
|
|
453
|
+
if (f.completionHint === "file") {
|
|
454
|
+
caseEntries.push(` ${names})
|
|
455
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
456
|
+
return ;;`);
|
|
457
|
+
} else if (f.completionHint === "directory") {
|
|
458
|
+
caseEntries.push(` ${names})
|
|
459
|
+
COMPREPLY=( $(compgen -d -- "$cur") )
|
|
460
|
+
return ;;`);
|
|
461
|
+
} else {
|
|
462
|
+
caseEntries.push(` ${names})
|
|
463
|
+
return ;;`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
caseEntries.push(` completions)
|
|
467
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
468
|
+
return ;;`);
|
|
469
|
+
const allFlags = FLAGS.flatMap((f) => f.short ? [f.short, f.long] : [f.long]);
|
|
470
|
+
const subcmds = SUBCOMMANDS.map((s) => s.name);
|
|
263
471
|
return `# numux bash completions
|
|
264
472
|
# Add to ~/.bashrc: eval "$(numux completions bash)"
|
|
265
473
|
_numux() {
|
|
@@ -268,60 +476,66 @@ _numux() {
|
|
|
268
476
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
269
477
|
|
|
270
478
|
case "$prev" in
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return ;;
|
|
274
|
-
--log-dir)
|
|
275
|
-
COMPREPLY=( $(compgen -d -- "$cur") )
|
|
276
|
-
return ;;
|
|
277
|
-
--only|--exclude)
|
|
278
|
-
return ;;
|
|
279
|
-
-n|--name|-w|--workspace)
|
|
280
|
-
return ;;
|
|
281
|
-
completions)
|
|
282
|
-
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
283
|
-
return ;;
|
|
479
|
+
${caseEntries.join(`
|
|
480
|
+
`)}
|
|
284
481
|
esac
|
|
285
482
|
|
|
286
483
|
if [[ "$cur" == -* ]]; then
|
|
287
|
-
COMPREPLY=( $(compgen -W "
|
|
484
|
+
COMPREPLY=( $(compgen -W "${allFlags.join(" ")}" -- "$cur") )
|
|
288
485
|
else
|
|
289
|
-
local subcmds="
|
|
486
|
+
local subcmds="${subcmds.join(" ")}"
|
|
290
487
|
COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
|
|
291
488
|
fi
|
|
292
489
|
}
|
|
293
490
|
complete -F _numux numux`;
|
|
294
491
|
}
|
|
295
492
|
function zshCompletions() {
|
|
493
|
+
const subcmdLines = SUBCOMMANDS.map((s) => ` '${s.name}:${sq(s.description)}'`).join(`
|
|
494
|
+
`);
|
|
495
|
+
const argLines = [];
|
|
496
|
+
for (const f of FLAGS) {
|
|
497
|
+
const desc = sq(f.description);
|
|
498
|
+
if (f.short) {
|
|
499
|
+
if (f.type === "value") {
|
|
500
|
+
let suffix = "";
|
|
501
|
+
if (f.completionHint === "file")
|
|
502
|
+
suffix = ":file:_files";
|
|
503
|
+
else if (f.completionHint === "directory")
|
|
504
|
+
suffix = ":directory:_directories";
|
|
505
|
+
else
|
|
506
|
+
suffix = `:${longName(f)}`;
|
|
507
|
+
argLines.push(` '(${f.short} ${f.long})'{${f.short},${f.long}}'[${desc}]${suffix}'`);
|
|
508
|
+
} else {
|
|
509
|
+
argLines.push(` '(${f.short} ${f.long})'{${f.short},${f.long}}'[${desc}]'`);
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
if (f.type === "value") {
|
|
513
|
+
let suffix = "";
|
|
514
|
+
if (f.completionHint === "file")
|
|
515
|
+
suffix = ":file:_files";
|
|
516
|
+
else if (f.completionHint === "directory")
|
|
517
|
+
suffix = ":directory:_directories";
|
|
518
|
+
else
|
|
519
|
+
suffix = `:${longName(f)}`;
|
|
520
|
+
argLines.push(` '${f.long}[${desc}]${suffix}'`);
|
|
521
|
+
} else {
|
|
522
|
+
argLines.push(` '${f.long}[${desc}]'`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const argsBlock = argLines.map((l) => `${l} \\`).join(`
|
|
527
|
+
`);
|
|
296
528
|
return `#compdef numux
|
|
297
529
|
# numux zsh completions
|
|
298
530
|
# Add to ~/.zshrc: eval "$(numux completions zsh)"
|
|
299
531
|
_numux() {
|
|
300
532
|
local -a subcmds
|
|
301
533
|
subcmds=(
|
|
302
|
-
|
|
303
|
-
'validate:Validate config and show process graph'
|
|
304
|
-
'exec:Run a command in a process environment'
|
|
305
|
-
'completions:Generate shell completions'
|
|
534
|
+
${subcmdLines}
|
|
306
535
|
)
|
|
307
536
|
|
|
308
537
|
_arguments -s \\
|
|
309
|
-
|
|
310
|
-
'(-v --version)'{-v,--version}'[Show version]' \\
|
|
311
|
-
'(-w --workspace)'{-w,--workspace}'[Run script across all workspaces]:script' \\
|
|
312
|
-
'(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
|
|
313
|
-
'--colors[Auto-assign colors based on process name]' \\
|
|
314
|
-
'--config[Config file path]:file:_files' \\
|
|
315
|
-
'(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
|
|
316
|
-
'(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
|
|
317
|
-
'--only[Only run these processes]:processes' \\
|
|
318
|
-
'--exclude[Exclude these processes]:processes' \\
|
|
319
|
-
'--kill-others[Kill all when any exits]' \\
|
|
320
|
-
'--no-restart[Disable auto-restart]' \\
|
|
321
|
-
'--no-watch[Disable file watching]' \\
|
|
322
|
-
'(-t --timestamps)'{-t,--timestamps}'[Add timestamps to output]' \\
|
|
323
|
-
'--log-dir[Log directory]:directory:_directories' \\
|
|
324
|
-
'--debug[Enable debug logging]' \\
|
|
538
|
+
${argsBlock}
|
|
325
539
|
'1:subcommand:->subcmd' \\
|
|
326
540
|
'*:command' \\
|
|
327
541
|
&& return
|
|
@@ -335,37 +549,37 @@ _numux() {
|
|
|
335
549
|
_numux`;
|
|
336
550
|
}
|
|
337
551
|
function fishCompletions() {
|
|
338
|
-
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
complete -c numux -n __fish_use_subcommand -a
|
|
348
|
-
|
|
349
|
-
# Completions subcommand
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
552
|
+
const lines = [
|
|
553
|
+
"# numux fish completions",
|
|
554
|
+
"# Add to fish: numux completions fish | source",
|
|
555
|
+
"# Or save to: ~/.config/fish/completions/numux.fish",
|
|
556
|
+
"complete -c numux -f",
|
|
557
|
+
"",
|
|
558
|
+
"# Subcommands"
|
|
559
|
+
];
|
|
560
|
+
for (const s of SUBCOMMANDS) {
|
|
561
|
+
lines.push(`complete -c numux -n __fish_use_subcommand -a ${s.name} -d '${sq(s.description)}'`);
|
|
562
|
+
}
|
|
563
|
+
lines.push("", "# Completions subcommand", "complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'", "", "# Options");
|
|
564
|
+
for (const f of FLAGS) {
|
|
565
|
+
const parts = ["complete -c numux"];
|
|
566
|
+
if (f.short)
|
|
567
|
+
parts.push(`-s ${f.short.replace("-", "")}`);
|
|
568
|
+
parts.push(`-l ${longName(f)}`);
|
|
569
|
+
if (f.type === "value") {
|
|
570
|
+
if (f.completionHint === "file") {
|
|
571
|
+
parts.push("-rF");
|
|
572
|
+
} else if (f.completionHint === "directory") {
|
|
573
|
+
parts.push("-ra '(__fish_complete_directories)'");
|
|
574
|
+
} else {
|
|
575
|
+
parts.push("-r");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
parts.push(`-d '${sq(f.description)}'`);
|
|
579
|
+
lines.push(parts.join(" "));
|
|
580
|
+
}
|
|
581
|
+
return lines.join(`
|
|
582
|
+
`);
|
|
369
583
|
}
|
|
370
584
|
|
|
371
585
|
// src/config/expand-scripts.ts
|
|
@@ -1514,6 +1728,9 @@ class ProcessManager {
|
|
|
1514
1728
|
updateStatus(name, status) {
|
|
1515
1729
|
const state = this.states.get(name);
|
|
1516
1730
|
state.status = status;
|
|
1731
|
+
if (status === "ready" && this.config.processes[name].readyPattern) {
|
|
1732
|
+
this.restartAttempts.set(name, 0);
|
|
1733
|
+
}
|
|
1517
1734
|
this.emit({ type: "status", name, status });
|
|
1518
1735
|
}
|
|
1519
1736
|
restart(name, cols, rows) {
|
|
@@ -1653,6 +1870,8 @@ class Pane {
|
|
|
1653
1870
|
decoder = new TextDecoder;
|
|
1654
1871
|
_onScroll = null;
|
|
1655
1872
|
_onCopy = null;
|
|
1873
|
+
_textLines = null;
|
|
1874
|
+
_textLinesLower = null;
|
|
1656
1875
|
constructor(renderer, name, cols, rows, interactive = false) {
|
|
1657
1876
|
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
1658
1877
|
id: `pane-${name}`,
|
|
@@ -1688,10 +1907,14 @@ class Pane {
|
|
|
1688
1907
|
feed(data) {
|
|
1689
1908
|
const text = this.decoder.decode(data, { stream: true });
|
|
1690
1909
|
this.terminal.feed(text);
|
|
1910
|
+
this._textLines = null;
|
|
1911
|
+
this._textLinesLower = null;
|
|
1691
1912
|
}
|
|
1692
1913
|
resize(cols, rows) {
|
|
1693
1914
|
this.terminal.cols = cols;
|
|
1694
1915
|
this.terminal.rows = rows;
|
|
1916
|
+
this._textLines = null;
|
|
1917
|
+
this._textLinesLower = null;
|
|
1695
1918
|
}
|
|
1696
1919
|
get isAtBottom() {
|
|
1697
1920
|
const { scrollTop, scrollHeight, viewport } = this.scrollBox;
|
|
@@ -1723,16 +1946,19 @@ class Pane {
|
|
|
1723
1946
|
search(query) {
|
|
1724
1947
|
if (!query)
|
|
1725
1948
|
return [];
|
|
1726
|
-
|
|
1727
|
-
|
|
1949
|
+
if (!this._textLines) {
|
|
1950
|
+
const text = this.terminal.getText();
|
|
1951
|
+
this._textLines = text.split(`
|
|
1728
1952
|
`);
|
|
1953
|
+
this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
|
|
1954
|
+
}
|
|
1955
|
+
const lines = this._textLinesLower;
|
|
1729
1956
|
const matches = [];
|
|
1730
1957
|
const lowerQuery = query.toLowerCase();
|
|
1731
1958
|
for (let line = 0;line < lines.length; line++) {
|
|
1732
|
-
const lowerLine = lines[line].toLowerCase();
|
|
1733
1959
|
let pos = 0;
|
|
1734
1960
|
while (true) {
|
|
1735
|
-
const idx =
|
|
1961
|
+
const idx = lines[line].indexOf(lowerQuery, pos);
|
|
1736
1962
|
if (idx === -1)
|
|
1737
1963
|
break;
|
|
1738
1964
|
matches.push({ line, start: idx, end: idx + query.length });
|
|
@@ -1742,12 +1968,22 @@ class Pane {
|
|
|
1742
1968
|
return matches;
|
|
1743
1969
|
}
|
|
1744
1970
|
setHighlights(matches, currentIndex) {
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1971
|
+
const firstVisible = Math.max(0, Math.floor(this.scrollBox.scrollTop) - 2);
|
|
1972
|
+
const lastVisible = Math.ceil(this.scrollBox.scrollTop + this.scrollBox.viewport.height) + 2;
|
|
1973
|
+
const regions = [];
|
|
1974
|
+
for (let i = 0;i < matches.length; i++) {
|
|
1975
|
+
const m = matches[i];
|
|
1976
|
+
if (m.line < firstVisible || m.line > lastVisible) {
|
|
1977
|
+
if (i !== currentIndex)
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
regions.push({
|
|
1981
|
+
line: m.line,
|
|
1982
|
+
start: m.start,
|
|
1983
|
+
end: m.end,
|
|
1984
|
+
backgroundColor: i === currentIndex ? "#b58900" : "#073642"
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1751
1987
|
this.terminal.highlights = regions;
|
|
1752
1988
|
}
|
|
1753
1989
|
clearHighlights() {
|
|
@@ -1759,6 +1995,8 @@ class Pane {
|
|
|
1759
1995
|
}
|
|
1760
1996
|
clear() {
|
|
1761
1997
|
this.terminal.reset();
|
|
1998
|
+
this._textLines = null;
|
|
1999
|
+
this._textLinesLower = null;
|
|
1762
2000
|
}
|
|
1763
2001
|
destroy() {
|
|
1764
2002
|
this.terminal.destroy();
|
|
@@ -1897,6 +2135,7 @@ class ColoredSelectRenderable extends SelectRenderable {
|
|
|
1897
2135
|
const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
|
|
1898
2136
|
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
|
|
1899
2137
|
const selectedTextColor = this._selectedTextColor;
|
|
2138
|
+
const lineWidth = fb.width;
|
|
1900
2139
|
for (let i = 0;i < visibleCount; i++) {
|
|
1901
2140
|
const actualIndex = scrollOffset + i;
|
|
1902
2141
|
const itemY = i * linesPerItem;
|
|
@@ -1904,13 +2143,11 @@ class ColoredSelectRenderable extends SelectRenderable {
|
|
|
1904
2143
|
const isSelected = actualIndex === selectedIndex;
|
|
1905
2144
|
const defaultColor = isSelected ? selectedTextColor : baseTextColor;
|
|
1906
2145
|
const colors = this._optionColors[actualIndex];
|
|
1907
|
-
|
|
2146
|
+
const textColor = colors?.name ?? defaultColor;
|
|
2147
|
+
fb.drawText(optName.padEnd(lineWidth), 1, itemY, textColor);
|
|
1908
2148
|
if (colors?.icon) {
|
|
1909
2149
|
fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
|
|
1910
2150
|
}
|
|
1911
|
-
if (colors?.name) {
|
|
1912
|
-
fb.drawText(optName.slice(2), 3, itemY, colors.name);
|
|
1913
|
-
}
|
|
1914
2151
|
}
|
|
1915
2152
|
}
|
|
1916
2153
|
}
|
|
@@ -2067,6 +2304,7 @@ class App {
|
|
|
2067
2304
|
sidebarWidth = 20;
|
|
2068
2305
|
config;
|
|
2069
2306
|
resizeTimer = null;
|
|
2307
|
+
searchTimer = null;
|
|
2070
2308
|
searchMode = false;
|
|
2071
2309
|
searchQuery = "";
|
|
2072
2310
|
searchMatches = [];
|
|
@@ -2122,6 +2360,11 @@ class App {
|
|
|
2122
2360
|
const interactive = this.config.processes[name].interactive === true;
|
|
2123
2361
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
2124
2362
|
pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
|
|
2363
|
+
pane.onScroll(() => {
|
|
2364
|
+
if (this.searchMode && this.searchMatches.length > 0 && this.activePane === name) {
|
|
2365
|
+
this.updateSearchHighlights();
|
|
2366
|
+
}
|
|
2367
|
+
});
|
|
2125
2368
|
this.panes.set(name, pane);
|
|
2126
2369
|
paneContainer.add(pane.scrollBox);
|
|
2127
2370
|
}
|
|
@@ -2322,6 +2565,10 @@ class App {
|
|
|
2322
2565
|
this.searchQuery = "";
|
|
2323
2566
|
this.searchMatches = [];
|
|
2324
2567
|
this.searchIndex = -1;
|
|
2568
|
+
if (this.searchTimer) {
|
|
2569
|
+
clearTimeout(this.searchTimer);
|
|
2570
|
+
this.searchTimer = null;
|
|
2571
|
+
}
|
|
2325
2572
|
if (this.activePane) {
|
|
2326
2573
|
this.panes.get(this.activePane)?.clearHighlights();
|
|
2327
2574
|
}
|
|
@@ -2347,15 +2594,24 @@ class App {
|
|
|
2347
2594
|
if (key.name === "backspace") {
|
|
2348
2595
|
if (this.searchQuery.length > 0) {
|
|
2349
2596
|
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
2350
|
-
this.
|
|
2597
|
+
this.scheduleSearch();
|
|
2351
2598
|
}
|
|
2352
2599
|
return;
|
|
2353
2600
|
}
|
|
2354
2601
|
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2355
2602
|
this.searchQuery += key.sequence;
|
|
2356
|
-
this.
|
|
2603
|
+
this.scheduleSearch();
|
|
2357
2604
|
}
|
|
2358
2605
|
}
|
|
2606
|
+
scheduleSearch() {
|
|
2607
|
+
this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
|
|
2608
|
+
if (this.searchTimer)
|
|
2609
|
+
clearTimeout(this.searchTimer);
|
|
2610
|
+
this.searchTimer = setTimeout(() => {
|
|
2611
|
+
this.searchTimer = null;
|
|
2612
|
+
this.runSearch();
|
|
2613
|
+
}, 100);
|
|
2614
|
+
}
|
|
2359
2615
|
runSearch() {
|
|
2360
2616
|
if (!this.activePane)
|
|
2361
2617
|
return;
|
|
@@ -2399,6 +2655,10 @@ class App {
|
|
|
2399
2655
|
clearTimeout(this.resizeTimer);
|
|
2400
2656
|
this.resizeTimer = null;
|
|
2401
2657
|
}
|
|
2658
|
+
if (this.searchTimer) {
|
|
2659
|
+
clearTimeout(this.searchTimer);
|
|
2660
|
+
this.searchTimer = null;
|
|
2661
|
+
}
|
|
2402
2662
|
for (const timer of this.inputWaitTimers.values()) {
|
|
2403
2663
|
clearTimeout(timer);
|
|
2404
2664
|
}
|
|
@@ -2665,38 +2925,7 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
2665
2925
|
}
|
|
2666
2926
|
|
|
2667
2927
|
// src/index.ts
|
|
2668
|
-
var HELP =
|
|
2669
|
-
|
|
2670
|
-
Usage:
|
|
2671
|
-
numux Run processes from config file
|
|
2672
|
-
numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel
|
|
2673
|
-
numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands
|
|
2674
|
-
numux -w <script> Run a script across all workspaces
|
|
2675
|
-
numux init Create a starter config file
|
|
2676
|
-
numux validate Validate config and show process graph
|
|
2677
|
-
numux exec <name> [--] <cmd> Run a command in a process's environment
|
|
2678
|
-
numux completions <shell> Generate shell completions (bash, zsh, fish)
|
|
2679
|
-
|
|
2680
|
-
Options:
|
|
2681
|
-
-w, --workspace <script> Run a package.json script across all workspaces
|
|
2682
|
-
-n, --name <name=command> Add a named process
|
|
2683
|
-
-c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
|
|
2684
|
-
--colors Auto-assign colors to processes based on their name
|
|
2685
|
-
--config <path> Config file path (default: auto-detect)
|
|
2686
|
-
-p, --prefix Prefixed output mode (no TUI, for CI/scripts)
|
|
2687
|
-
--only <a,b,...> Only run these processes (+ their dependencies)
|
|
2688
|
-
--exclude <a,b,...> Exclude these processes
|
|
2689
|
-
--kill-others Kill all processes when any exits
|
|
2690
|
-
--no-restart Disable auto-restart for crashed processes
|
|
2691
|
-
--no-watch Disable file watching even if config has watch patterns
|
|
2692
|
-
-t, --timestamps Add timestamps to prefixed output lines
|
|
2693
|
-
--log-dir <path> Write per-process logs to directory
|
|
2694
|
-
--debug Enable debug logging to .numux/debug.log
|
|
2695
|
-
-h, --help Show this help
|
|
2696
|
-
-v, --version Show version
|
|
2697
|
-
|
|
2698
|
-
Config files (auto-detected):
|
|
2699
|
-
numux.config.ts, numux.config.js`;
|
|
2928
|
+
var HELP = generateHelp();
|
|
2700
2929
|
var INIT_TEMPLATE = `import { defineConfig } from 'numux'
|
|
2701
2930
|
|
|
2702
2931
|
export default defineConfig({
|