numux 1.10.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/numux.js +407 -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.1",
|
|
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
|
|
@@ -1653,6 +1867,8 @@ class Pane {
|
|
|
1653
1867
|
decoder = new TextDecoder;
|
|
1654
1868
|
_onScroll = null;
|
|
1655
1869
|
_onCopy = null;
|
|
1870
|
+
_textLines = null;
|
|
1871
|
+
_textLinesLower = null;
|
|
1656
1872
|
constructor(renderer, name, cols, rows, interactive = false) {
|
|
1657
1873
|
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
1658
1874
|
id: `pane-${name}`,
|
|
@@ -1688,10 +1904,14 @@ class Pane {
|
|
|
1688
1904
|
feed(data) {
|
|
1689
1905
|
const text = this.decoder.decode(data, { stream: true });
|
|
1690
1906
|
this.terminal.feed(text);
|
|
1907
|
+
this._textLines = null;
|
|
1908
|
+
this._textLinesLower = null;
|
|
1691
1909
|
}
|
|
1692
1910
|
resize(cols, rows) {
|
|
1693
1911
|
this.terminal.cols = cols;
|
|
1694
1912
|
this.terminal.rows = rows;
|
|
1913
|
+
this._textLines = null;
|
|
1914
|
+
this._textLinesLower = null;
|
|
1695
1915
|
}
|
|
1696
1916
|
get isAtBottom() {
|
|
1697
1917
|
const { scrollTop, scrollHeight, viewport } = this.scrollBox;
|
|
@@ -1723,16 +1943,19 @@ class Pane {
|
|
|
1723
1943
|
search(query) {
|
|
1724
1944
|
if (!query)
|
|
1725
1945
|
return [];
|
|
1726
|
-
|
|
1727
|
-
|
|
1946
|
+
if (!this._textLines) {
|
|
1947
|
+
const text = this.terminal.getText();
|
|
1948
|
+
this._textLines = text.split(`
|
|
1728
1949
|
`);
|
|
1950
|
+
this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
|
|
1951
|
+
}
|
|
1952
|
+
const lines = this._textLinesLower;
|
|
1729
1953
|
const matches = [];
|
|
1730
1954
|
const lowerQuery = query.toLowerCase();
|
|
1731
1955
|
for (let line = 0;line < lines.length; line++) {
|
|
1732
|
-
const lowerLine = lines[line].toLowerCase();
|
|
1733
1956
|
let pos = 0;
|
|
1734
1957
|
while (true) {
|
|
1735
|
-
const idx =
|
|
1958
|
+
const idx = lines[line].indexOf(lowerQuery, pos);
|
|
1736
1959
|
if (idx === -1)
|
|
1737
1960
|
break;
|
|
1738
1961
|
matches.push({ line, start: idx, end: idx + query.length });
|
|
@@ -1742,12 +1965,22 @@ class Pane {
|
|
|
1742
1965
|
return matches;
|
|
1743
1966
|
}
|
|
1744
1967
|
setHighlights(matches, currentIndex) {
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1968
|
+
const firstVisible = Math.max(0, Math.floor(this.scrollBox.scrollTop) - 2);
|
|
1969
|
+
const lastVisible = Math.ceil(this.scrollBox.scrollTop + this.scrollBox.viewport.height) + 2;
|
|
1970
|
+
const regions = [];
|
|
1971
|
+
for (let i = 0;i < matches.length; i++) {
|
|
1972
|
+
const m = matches[i];
|
|
1973
|
+
if (m.line < firstVisible || m.line > lastVisible) {
|
|
1974
|
+
if (i !== currentIndex)
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
regions.push({
|
|
1978
|
+
line: m.line,
|
|
1979
|
+
start: m.start,
|
|
1980
|
+
end: m.end,
|
|
1981
|
+
backgroundColor: i === currentIndex ? "#b58900" : "#073642"
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1751
1984
|
this.terminal.highlights = regions;
|
|
1752
1985
|
}
|
|
1753
1986
|
clearHighlights() {
|
|
@@ -1759,6 +1992,8 @@ class Pane {
|
|
|
1759
1992
|
}
|
|
1760
1993
|
clear() {
|
|
1761
1994
|
this.terminal.reset();
|
|
1995
|
+
this._textLines = null;
|
|
1996
|
+
this._textLinesLower = null;
|
|
1762
1997
|
}
|
|
1763
1998
|
destroy() {
|
|
1764
1999
|
this.terminal.destroy();
|
|
@@ -1897,6 +2132,7 @@ class ColoredSelectRenderable extends SelectRenderable {
|
|
|
1897
2132
|
const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
|
|
1898
2133
|
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
|
|
1899
2134
|
const selectedTextColor = this._selectedTextColor;
|
|
2135
|
+
const lineWidth = fb.width;
|
|
1900
2136
|
for (let i = 0;i < visibleCount; i++) {
|
|
1901
2137
|
const actualIndex = scrollOffset + i;
|
|
1902
2138
|
const itemY = i * linesPerItem;
|
|
@@ -1904,13 +2140,11 @@ class ColoredSelectRenderable extends SelectRenderable {
|
|
|
1904
2140
|
const isSelected = actualIndex === selectedIndex;
|
|
1905
2141
|
const defaultColor = isSelected ? selectedTextColor : baseTextColor;
|
|
1906
2142
|
const colors = this._optionColors[actualIndex];
|
|
1907
|
-
|
|
2143
|
+
const textColor = colors?.name ?? defaultColor;
|
|
2144
|
+
fb.drawText(optName.padEnd(lineWidth), 1, itemY, textColor);
|
|
1908
2145
|
if (colors?.icon) {
|
|
1909
2146
|
fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
|
|
1910
2147
|
}
|
|
1911
|
-
if (colors?.name) {
|
|
1912
|
-
fb.drawText(optName.slice(2), 3, itemY, colors.name);
|
|
1913
|
-
}
|
|
1914
2148
|
}
|
|
1915
2149
|
}
|
|
1916
2150
|
}
|
|
@@ -2067,6 +2301,7 @@ class App {
|
|
|
2067
2301
|
sidebarWidth = 20;
|
|
2068
2302
|
config;
|
|
2069
2303
|
resizeTimer = null;
|
|
2304
|
+
searchTimer = null;
|
|
2070
2305
|
searchMode = false;
|
|
2071
2306
|
searchQuery = "";
|
|
2072
2307
|
searchMatches = [];
|
|
@@ -2122,6 +2357,11 @@ class App {
|
|
|
2122
2357
|
const interactive = this.config.processes[name].interactive === true;
|
|
2123
2358
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
2124
2359
|
pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
|
|
2360
|
+
pane.onScroll(() => {
|
|
2361
|
+
if (this.searchMode && this.searchMatches.length > 0 && this.activePane === name) {
|
|
2362
|
+
this.updateSearchHighlights();
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2125
2365
|
this.panes.set(name, pane);
|
|
2126
2366
|
paneContainer.add(pane.scrollBox);
|
|
2127
2367
|
}
|
|
@@ -2322,6 +2562,10 @@ class App {
|
|
|
2322
2562
|
this.searchQuery = "";
|
|
2323
2563
|
this.searchMatches = [];
|
|
2324
2564
|
this.searchIndex = -1;
|
|
2565
|
+
if (this.searchTimer) {
|
|
2566
|
+
clearTimeout(this.searchTimer);
|
|
2567
|
+
this.searchTimer = null;
|
|
2568
|
+
}
|
|
2325
2569
|
if (this.activePane) {
|
|
2326
2570
|
this.panes.get(this.activePane)?.clearHighlights();
|
|
2327
2571
|
}
|
|
@@ -2347,15 +2591,24 @@ class App {
|
|
|
2347
2591
|
if (key.name === "backspace") {
|
|
2348
2592
|
if (this.searchQuery.length > 0) {
|
|
2349
2593
|
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
2350
|
-
this.
|
|
2594
|
+
this.scheduleSearch();
|
|
2351
2595
|
}
|
|
2352
2596
|
return;
|
|
2353
2597
|
}
|
|
2354
2598
|
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2355
2599
|
this.searchQuery += key.sequence;
|
|
2356
|
-
this.
|
|
2600
|
+
this.scheduleSearch();
|
|
2357
2601
|
}
|
|
2358
2602
|
}
|
|
2603
|
+
scheduleSearch() {
|
|
2604
|
+
this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
|
|
2605
|
+
if (this.searchTimer)
|
|
2606
|
+
clearTimeout(this.searchTimer);
|
|
2607
|
+
this.searchTimer = setTimeout(() => {
|
|
2608
|
+
this.searchTimer = null;
|
|
2609
|
+
this.runSearch();
|
|
2610
|
+
}, 100);
|
|
2611
|
+
}
|
|
2359
2612
|
runSearch() {
|
|
2360
2613
|
if (!this.activePane)
|
|
2361
2614
|
return;
|
|
@@ -2399,6 +2652,10 @@ class App {
|
|
|
2399
2652
|
clearTimeout(this.resizeTimer);
|
|
2400
2653
|
this.resizeTimer = null;
|
|
2401
2654
|
}
|
|
2655
|
+
if (this.searchTimer) {
|
|
2656
|
+
clearTimeout(this.searchTimer);
|
|
2657
|
+
this.searchTimer = null;
|
|
2658
|
+
}
|
|
2402
2659
|
for (const timer of this.inputWaitTimers.values()) {
|
|
2403
2660
|
clearTimeout(timer);
|
|
2404
2661
|
}
|
|
@@ -2665,38 +2922,7 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
2665
2922
|
}
|
|
2666
2923
|
|
|
2667
2924
|
// 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`;
|
|
2925
|
+
var HELP = generateHelp();
|
|
2700
2926
|
var INIT_TEMPLATE = `import { defineConfig } from 'numux'
|
|
2701
2927
|
|
|
2702
2928
|
export default defineConfig({
|