numux 1.9.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 +46 -0
- package/dist/numux.js +522 -203
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,22 @@ numux completions fish | source
|
|
|
98
98
|
numux completions fish > ~/.config/fish/completions/numux.fish
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Workspaces
|
|
102
|
+
|
|
103
|
+
Run a `package.json` script across all workspaces in a monorepo:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
numux -w dev
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Reads the `workspaces` field from your root `package.json`, finds which workspaces have the given script, and spawns `<pm> run <script>` in each. The package manager is auto-detected from `packageManager` field or lockfiles.
|
|
110
|
+
|
|
111
|
+
Composes with other flags:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
numux -w dev -n redis="redis-server" --colors
|
|
115
|
+
```
|
|
116
|
+
|
|
101
117
|
### Ad-hoc commands
|
|
102
118
|
|
|
103
119
|
```sh
|
|
@@ -108,10 +124,39 @@ numux "bun dev:api" "bun dev:web"
|
|
|
108
124
|
numux -n api="bun dev:api" -n web="bun dev:web"
|
|
109
125
|
```
|
|
110
126
|
|
|
127
|
+
### Script patterns
|
|
128
|
+
|
|
129
|
+
Run multiple package.json scripts matching a glob pattern:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
numux 'dev:*' # all scripts matching dev:*
|
|
133
|
+
numux 'npm:*:dev' # explicit npm: prefix (same behavior)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Extra arguments after the pattern are forwarded to each matched command:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
numux 'lint:* --fix' # → bun run lint:js --fix, bun run lint:ts --fix
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In a config file, use the pattern as the process name:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
export default defineConfig({
|
|
146
|
+
processes: {
|
|
147
|
+
'dev:*': { color: ['green', 'cyan'] },
|
|
148
|
+
'lint:* --fix': {},
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Template properties (color, env, dependsOn, etc.) are inherited by all matched processes. Colors given as an array are distributed round-robin.
|
|
154
|
+
|
|
111
155
|
### Options
|
|
112
156
|
|
|
113
157
|
| Flag | Description |
|
|
114
158
|
|------|-------------|
|
|
159
|
+
| `-w, --workspace <script>` | Run a script across all workspaces |
|
|
115
160
|
| `-c, --config <path>` | Explicit config file path |
|
|
116
161
|
| `-n, --name <name=cmd>` | Add a named process (repeatable) |
|
|
117
162
|
| `-p, --prefix` | Prefixed output mode (no TUI, for CI/scripts) |
|
|
@@ -181,6 +226,7 @@ Each process accepts:
|
|
|
181
226
|
| `color` | `string \| string[]` | auto | Hex (e.g. `"#ff6600"`) or basic name: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple |
|
|
182
227
|
| `watch` | `string \| string[]` | — | Glob patterns — restart process when matching files change |
|
|
183
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 |
|
|
184
230
|
|
|
185
231
|
### File watching
|
|
186
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.
|
|
25
|
+
version: "1.10.1",
|
|
26
26
|
description: "Terminal multiplexer with dependency orchestration",
|
|
27
27
|
type: "module",
|
|
28
28
|
license: "MIT",
|
|
@@ -75,10 +75,230 @@ var require_package = __commonJS((exports, module) => {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
// src/index.ts
|
|
78
|
-
import { existsSync as
|
|
79
|
-
import { resolve as
|
|
78
|
+
import { existsSync as existsSync5, writeFileSync } from "fs";
|
|
79
|
+
import { resolve as resolve8 } from "path";
|
|
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
|
+
}
|
|
80
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,74 +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 === "--colors") {
|
|
128
|
-
result.autoColors = true;
|
|
129
|
-
} else if (arg === "--config") {
|
|
130
|
-
result.configPath = consumeValue(arg);
|
|
131
|
-
} else if (arg === "-c" || arg === "--color") {
|
|
132
|
-
result.colors = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
|
|
133
|
-
} else if (arg === "--log-dir") {
|
|
134
|
-
result.logDir = consumeValue(arg);
|
|
135
|
-
} else if (arg === "--only") {
|
|
136
|
-
result.only = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
|
|
137
|
-
} else if (arg === "--exclude") {
|
|
138
|
-
result.exclude = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
|
|
139
|
-
} else if (arg === "-n" || arg === "--name") {
|
|
140
|
-
const value = consumeValue(arg);
|
|
141
|
-
const eq = value.indexOf("=");
|
|
142
|
-
if (eq < 1) {
|
|
143
|
-
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
|
+
}
|
|
144
340
|
}
|
|
145
|
-
result.named.push({
|
|
146
|
-
name: value.slice(0, eq),
|
|
147
|
-
command: value.slice(eq + 1)
|
|
148
|
-
});
|
|
149
|
-
} else if (arg === "init" && result.commands.length === 0) {
|
|
150
|
-
result.init = true;
|
|
151
|
-
} else if (arg === "validate" && result.commands.length === 0) {
|
|
152
|
-
result.validate = true;
|
|
153
|
-
} else if (arg === "exec" && result.commands.length === 0) {
|
|
154
|
-
result.exec = true;
|
|
155
|
-
const name = args[++i];
|
|
156
|
-
if (!name)
|
|
157
|
-
throw new Error("exec requires a process name");
|
|
158
|
-
result.execName = name;
|
|
159
|
-
if (args[i + 1] === "--")
|
|
160
|
-
i++;
|
|
161
|
-
const rest = args.slice(i + 1);
|
|
162
|
-
if (rest.length === 0)
|
|
163
|
-
throw new Error("exec requires a command to run");
|
|
164
|
-
result.execCommand = rest.join(" ");
|
|
165
|
-
break;
|
|
166
|
-
} else if (arg === "completions" && result.commands.length === 0) {
|
|
167
|
-
result.completions = consumeValue(arg);
|
|
168
341
|
} else if (!arg.startsWith("-")) {
|
|
169
|
-
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
|
+
}
|
|
170
351
|
} else {
|
|
171
352
|
throw new Error(`Unknown option: ${arg}`);
|
|
172
353
|
}
|
|
@@ -257,7 +438,36 @@ function generateCompletions(shell) {
|
|
|
257
438
|
throw new Error(`Unknown shell: "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
|
|
258
439
|
}
|
|
259
440
|
}
|
|
441
|
+
function longName(f) {
|
|
442
|
+
return f.long.replace(/^-+/, "");
|
|
443
|
+
}
|
|
444
|
+
function sq(s) {
|
|
445
|
+
return s.replace(/'/g, "'\\''");
|
|
446
|
+
}
|
|
260
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);
|
|
261
471
|
return `# numux bash completions
|
|
262
472
|
# Add to ~/.bashrc: eval "$(numux completions bash)"
|
|
263
473
|
_numux() {
|
|
@@ -266,59 +476,66 @@ _numux() {
|
|
|
266
476
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
267
477
|
|
|
268
478
|
case "$prev" in
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return ;;
|
|
272
|
-
--log-dir)
|
|
273
|
-
COMPREPLY=( $(compgen -d -- "$cur") )
|
|
274
|
-
return ;;
|
|
275
|
-
--only|--exclude)
|
|
276
|
-
return ;;
|
|
277
|
-
-n|--name)
|
|
278
|
-
return ;;
|
|
279
|
-
completions)
|
|
280
|
-
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
281
|
-
return ;;
|
|
479
|
+
${caseEntries.join(`
|
|
480
|
+
`)}
|
|
282
481
|
esac
|
|
283
482
|
|
|
284
483
|
if [[ "$cur" == -* ]]; then
|
|
285
|
-
COMPREPLY=( $(compgen -W "
|
|
484
|
+
COMPREPLY=( $(compgen -W "${allFlags.join(" ")}" -- "$cur") )
|
|
286
485
|
else
|
|
287
|
-
local subcmds="
|
|
486
|
+
local subcmds="${subcmds.join(" ")}"
|
|
288
487
|
COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
|
|
289
488
|
fi
|
|
290
489
|
}
|
|
291
490
|
complete -F _numux numux`;
|
|
292
491
|
}
|
|
293
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
|
+
`);
|
|
294
528
|
return `#compdef numux
|
|
295
529
|
# numux zsh completions
|
|
296
530
|
# Add to ~/.zshrc: eval "$(numux completions zsh)"
|
|
297
531
|
_numux() {
|
|
298
532
|
local -a subcmds
|
|
299
533
|
subcmds=(
|
|
300
|
-
|
|
301
|
-
'validate:Validate config and show process graph'
|
|
302
|
-
'exec:Run a command in a process environment'
|
|
303
|
-
'completions:Generate shell completions'
|
|
534
|
+
${subcmdLines}
|
|
304
535
|
)
|
|
305
536
|
|
|
306
537
|
_arguments -s \\
|
|
307
|
-
|
|
308
|
-
'(-v --version)'{-v,--version}'[Show version]' \\
|
|
309
|
-
'(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
|
|
310
|
-
'--colors[Auto-assign colors based on process name]' \\
|
|
311
|
-
'--config[Config file path]:file:_files' \\
|
|
312
|
-
'(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
|
|
313
|
-
'(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
|
|
314
|
-
'--only[Only run these processes]:processes' \\
|
|
315
|
-
'--exclude[Exclude these processes]:processes' \\
|
|
316
|
-
'--kill-others[Kill all when any exits]' \\
|
|
317
|
-
'--no-restart[Disable auto-restart]' \\
|
|
318
|
-
'--no-watch[Disable file watching]' \\
|
|
319
|
-
'(-t --timestamps)'{-t,--timestamps}'[Add timestamps to output]' \\
|
|
320
|
-
'--log-dir[Log directory]:directory:_directories' \\
|
|
321
|
-
'--debug[Enable debug logging]' \\
|
|
538
|
+
${argsBlock}
|
|
322
539
|
'1:subcommand:->subcmd' \\
|
|
323
540
|
'*:command' \\
|
|
324
541
|
&& return
|
|
@@ -332,36 +549,37 @@ _numux() {
|
|
|
332
549
|
_numux`;
|
|
333
550
|
}
|
|
334
551
|
function fishCompletions() {
|
|
335
|
-
|
|
336
|
-
#
|
|
337
|
-
#
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
complete -c numux -n __fish_use_subcommand -a
|
|
345
|
-
|
|
346
|
-
# Completions subcommand
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
`);
|
|
365
583
|
}
|
|
366
584
|
|
|
367
585
|
// src/config/expand-scripts.ts
|
|
@@ -390,6 +608,12 @@ function detectPackageManager(pkgJson, cwd) {
|
|
|
390
608
|
function isGlobPattern(name) {
|
|
391
609
|
return /[*?[]/.test(name);
|
|
392
610
|
}
|
|
611
|
+
function splitPatternArgs(raw) {
|
|
612
|
+
const i = raw.indexOf(" ");
|
|
613
|
+
if (i === -1)
|
|
614
|
+
return { glob: raw, extraArgs: "" };
|
|
615
|
+
return { glob: raw.slice(0, i), extraArgs: raw.slice(i) };
|
|
616
|
+
}
|
|
393
617
|
function expandScriptPatterns(config, cwd) {
|
|
394
618
|
const entries = Object.entries(config.processes);
|
|
395
619
|
const hasWildcard = entries.some(([name]) => name.startsWith("npm:") || isGlobPattern(name));
|
|
@@ -413,15 +637,16 @@ function expandScriptPatterns(config, cwd) {
|
|
|
413
637
|
expanded[name] = value;
|
|
414
638
|
continue;
|
|
415
639
|
}
|
|
416
|
-
const
|
|
640
|
+
const rawPattern = name.startsWith("npm:") ? name.slice(4) : name;
|
|
641
|
+
const { glob: globPattern, extraArgs } = splitPatternArgs(rawPattern);
|
|
417
642
|
const template = value ?? {};
|
|
418
643
|
if (template.command) {
|
|
419
644
|
throw new Error(`"${name}": wildcard processes cannot have a "command" field (commands come from package.json scripts)`);
|
|
420
645
|
}
|
|
421
|
-
const glob = new Bun.Glob(
|
|
646
|
+
const glob = new Bun.Glob(globPattern);
|
|
422
647
|
const matches = scriptNames.filter((s) => glob.match(s));
|
|
423
648
|
if (matches.length === 0) {
|
|
424
|
-
throw new Error(`"${name}": no scripts matched pattern "${
|
|
649
|
+
throw new Error(`"${name}": no scripts matched pattern "${globPattern}". Available scripts: ${scriptNames.join(", ")}`);
|
|
425
650
|
}
|
|
426
651
|
const colors = Array.isArray(template.color) ? template.color : undefined;
|
|
427
652
|
const singleColor = typeof template.color === "string" ? template.color : undefined;
|
|
@@ -434,7 +659,7 @@ function expandScriptPatterns(config, cwd) {
|
|
|
434
659
|
const { color: _color, ...rest } = template;
|
|
435
660
|
expanded[scriptName] = {
|
|
436
661
|
...rest,
|
|
437
|
-
command: `${pm} run ${scriptName}`,
|
|
662
|
+
command: `${pm} run ${scriptName}${extraArgs}`,
|
|
438
663
|
...color ? { color } : {}
|
|
439
664
|
};
|
|
440
665
|
}
|
|
@@ -847,8 +1072,71 @@ function validateStopSignal(value) {
|
|
|
847
1072
|
return;
|
|
848
1073
|
}
|
|
849
1074
|
|
|
1075
|
+
// src/config/workspaces.ts
|
|
1076
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
1077
|
+
import { basename, resolve as resolve4 } from "path";
|
|
1078
|
+
function resolveWorkspaceProcesses(script, cwd) {
|
|
1079
|
+
const pkgPath = resolve4(cwd, "package.json");
|
|
1080
|
+
if (!existsSync4(pkgPath)) {
|
|
1081
|
+
throw new Error(`No package.json found in ${cwd}`);
|
|
1082
|
+
}
|
|
1083
|
+
const pkgJson = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
1084
|
+
const pm = detectPackageManager(pkgJson, cwd);
|
|
1085
|
+
const raw = pkgJson.workspaces;
|
|
1086
|
+
let patterns;
|
|
1087
|
+
if (Array.isArray(raw)) {
|
|
1088
|
+
patterns = raw;
|
|
1089
|
+
} else if (raw && typeof raw === "object" && Array.isArray(raw.packages)) {
|
|
1090
|
+
patterns = raw.packages;
|
|
1091
|
+
} else {
|
|
1092
|
+
throw new Error('No "workspaces" field found in package.json');
|
|
1093
|
+
}
|
|
1094
|
+
const dirs = [];
|
|
1095
|
+
for (const pattern of patterns) {
|
|
1096
|
+
const glob = new Bun.Glob(pattern);
|
|
1097
|
+
for (const match of glob.scanSync({ cwd, onlyFiles: false })) {
|
|
1098
|
+
const abs = resolve4(cwd, match);
|
|
1099
|
+
const wsPkgPath = resolve4(abs, "package.json");
|
|
1100
|
+
if (existsSync4(wsPkgPath)) {
|
|
1101
|
+
dirs.push(abs);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const processes = {};
|
|
1106
|
+
const usedNames = new Set;
|
|
1107
|
+
for (const dir of dirs) {
|
|
1108
|
+
const wsPkgPath = resolve4(dir, "package.json");
|
|
1109
|
+
const wsPkg = JSON.parse(readFileSync2(wsPkgPath, "utf-8"));
|
|
1110
|
+
const scripts = wsPkg.scripts;
|
|
1111
|
+
if (!scripts?.[script])
|
|
1112
|
+
continue;
|
|
1113
|
+
let name;
|
|
1114
|
+
if (typeof wsPkg.name === "string" && wsPkg.name) {
|
|
1115
|
+
name = wsPkg.name.replace(/^@[^/]+\//, "");
|
|
1116
|
+
} else {
|
|
1117
|
+
name = basename(dir);
|
|
1118
|
+
}
|
|
1119
|
+
if (usedNames.has(name)) {
|
|
1120
|
+
let suffix = 1;
|
|
1121
|
+
while (usedNames.has(`${name}-${suffix}`))
|
|
1122
|
+
suffix++;
|
|
1123
|
+
name = `${name}-${suffix}`;
|
|
1124
|
+
}
|
|
1125
|
+
usedNames.add(name);
|
|
1126
|
+
processes[name] = {
|
|
1127
|
+
command: `${pm} run ${script}`,
|
|
1128
|
+
cwd: dir,
|
|
1129
|
+
persistent: true
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
if (Object.keys(processes).length === 0) {
|
|
1133
|
+
throw new Error(`No workspaces have a "${script}" script`);
|
|
1134
|
+
}
|
|
1135
|
+
return processes;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
850
1138
|
// src/process/manager.ts
|
|
851
|
-
import { resolve as
|
|
1139
|
+
import { resolve as resolve7 } from "path";
|
|
852
1140
|
|
|
853
1141
|
// src/utils/watcher.ts
|
|
854
1142
|
import { watch } from "fs";
|
|
@@ -896,11 +1184,11 @@ class FileWatcher {
|
|
|
896
1184
|
}
|
|
897
1185
|
|
|
898
1186
|
// src/process/runner.ts
|
|
899
|
-
import { resolve as
|
|
1187
|
+
import { resolve as resolve6 } from "path";
|
|
900
1188
|
|
|
901
1189
|
// src/utils/env-file.ts
|
|
902
|
-
import { readFileSync as
|
|
903
|
-
import { resolve as
|
|
1190
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1191
|
+
import { resolve as resolve5 } from "path";
|
|
904
1192
|
function parseEnvFile(content) {
|
|
905
1193
|
const vars = {};
|
|
906
1194
|
for (const line of content.split(/\r?\n/)) {
|
|
@@ -928,10 +1216,10 @@ function loadEnvFiles(envFile, cwd) {
|
|
|
928
1216
|
const files = Array.isArray(envFile) ? envFile : [envFile];
|
|
929
1217
|
const merged = {};
|
|
930
1218
|
for (const file of files) {
|
|
931
|
-
const path =
|
|
1219
|
+
const path = resolve5(cwd, file);
|
|
932
1220
|
let content;
|
|
933
1221
|
try {
|
|
934
|
-
content =
|
|
1222
|
+
content = readFileSync3(path, "utf-8");
|
|
935
1223
|
} catch (err) {
|
|
936
1224
|
const code = err.code;
|
|
937
1225
|
if (code === "ENOENT") {
|
|
@@ -1046,7 +1334,7 @@ class ProcessRunner {
|
|
|
1046
1334
|
this.stopping = false;
|
|
1047
1335
|
log(`[${this.name}] Starting (gen ${gen}): ${this.config.command}`);
|
|
1048
1336
|
this.handler.onStatus("starting");
|
|
1049
|
-
const cwd = this.config.cwd ?
|
|
1337
|
+
const cwd = this.config.cwd ? resolve6(this.config.cwd) : process.cwd();
|
|
1050
1338
|
try {
|
|
1051
1339
|
const envFromFile = this.config.envFile ? loadEnvFiles(this.config.envFile, cwd) : {};
|
|
1052
1340
|
const noColor = "NO_COLOR" in process.env;
|
|
@@ -1308,12 +1596,12 @@ class ProcessManager {
|
|
|
1308
1596
|
this.updateStatus(name, "skipped");
|
|
1309
1597
|
continue;
|
|
1310
1598
|
}
|
|
1311
|
-
const { promise, resolve:
|
|
1599
|
+
const { promise, resolve: resolve8 } = Promise.withResolvers();
|
|
1312
1600
|
readyPromises.push(promise);
|
|
1313
|
-
this.pendingReadyResolvers.set(name,
|
|
1601
|
+
this.pendingReadyResolvers.set(name, resolve8);
|
|
1314
1602
|
this.createRunner(name, () => {
|
|
1315
1603
|
this.pendingReadyResolvers.delete(name);
|
|
1316
|
-
|
|
1604
|
+
resolve8();
|
|
1317
1605
|
});
|
|
1318
1606
|
this.startProcess(name, cols, rows);
|
|
1319
1607
|
}
|
|
@@ -1421,7 +1709,7 @@ class ProcessManager {
|
|
|
1421
1709
|
if (!this.fileWatcher)
|
|
1422
1710
|
this.fileWatcher = new FileWatcher;
|
|
1423
1711
|
const patterns = Array.isArray(proc.watch) ? proc.watch : [proc.watch];
|
|
1424
|
-
const cwd = proc.cwd ?
|
|
1712
|
+
const cwd = proc.cwd ? resolve7(proc.cwd) : process.cwd();
|
|
1425
1713
|
this.fileWatcher.watch(name, patterns, cwd, (changedFile) => {
|
|
1426
1714
|
const state = this.states.get(name);
|
|
1427
1715
|
if (!state)
|
|
@@ -1527,8 +1815,8 @@ class ProcessManager {
|
|
|
1527
1815
|
clearTimeout(timer);
|
|
1528
1816
|
}
|
|
1529
1817
|
this.restartTimers.clear();
|
|
1530
|
-
for (const
|
|
1531
|
-
|
|
1818
|
+
for (const resolve8 of this.pendingReadyResolvers.values()) {
|
|
1819
|
+
resolve8();
|
|
1532
1820
|
}
|
|
1533
1821
|
this.pendingReadyResolvers.clear();
|
|
1534
1822
|
const reversed = [...this.tiers].reverse();
|
|
@@ -1579,6 +1867,8 @@ class Pane {
|
|
|
1579
1867
|
decoder = new TextDecoder;
|
|
1580
1868
|
_onScroll = null;
|
|
1581
1869
|
_onCopy = null;
|
|
1870
|
+
_textLines = null;
|
|
1871
|
+
_textLinesLower = null;
|
|
1582
1872
|
constructor(renderer, name, cols, rows, interactive = false) {
|
|
1583
1873
|
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
1584
1874
|
id: `pane-${name}`,
|
|
@@ -1614,10 +1904,14 @@ class Pane {
|
|
|
1614
1904
|
feed(data) {
|
|
1615
1905
|
const text = this.decoder.decode(data, { stream: true });
|
|
1616
1906
|
this.terminal.feed(text);
|
|
1907
|
+
this._textLines = null;
|
|
1908
|
+
this._textLinesLower = null;
|
|
1617
1909
|
}
|
|
1618
1910
|
resize(cols, rows) {
|
|
1619
1911
|
this.terminal.cols = cols;
|
|
1620
1912
|
this.terminal.rows = rows;
|
|
1913
|
+
this._textLines = null;
|
|
1914
|
+
this._textLinesLower = null;
|
|
1621
1915
|
}
|
|
1622
1916
|
get isAtBottom() {
|
|
1623
1917
|
const { scrollTop, scrollHeight, viewport } = this.scrollBox;
|
|
@@ -1649,16 +1943,19 @@ class Pane {
|
|
|
1649
1943
|
search(query) {
|
|
1650
1944
|
if (!query)
|
|
1651
1945
|
return [];
|
|
1652
|
-
|
|
1653
|
-
|
|
1946
|
+
if (!this._textLines) {
|
|
1947
|
+
const text = this.terminal.getText();
|
|
1948
|
+
this._textLines = text.split(`
|
|
1654
1949
|
`);
|
|
1950
|
+
this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
|
|
1951
|
+
}
|
|
1952
|
+
const lines = this._textLinesLower;
|
|
1655
1953
|
const matches = [];
|
|
1656
1954
|
const lowerQuery = query.toLowerCase();
|
|
1657
1955
|
for (let line = 0;line < lines.length; line++) {
|
|
1658
|
-
const lowerLine = lines[line].toLowerCase();
|
|
1659
1956
|
let pos = 0;
|
|
1660
1957
|
while (true) {
|
|
1661
|
-
const idx =
|
|
1958
|
+
const idx = lines[line].indexOf(lowerQuery, pos);
|
|
1662
1959
|
if (idx === -1)
|
|
1663
1960
|
break;
|
|
1664
1961
|
matches.push({ line, start: idx, end: idx + query.length });
|
|
@@ -1668,12 +1965,22 @@ class Pane {
|
|
|
1668
1965
|
return matches;
|
|
1669
1966
|
}
|
|
1670
1967
|
setHighlights(matches, currentIndex) {
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
+
}
|
|
1677
1984
|
this.terminal.highlights = regions;
|
|
1678
1985
|
}
|
|
1679
1986
|
clearHighlights() {
|
|
@@ -1685,6 +1992,8 @@ class Pane {
|
|
|
1685
1992
|
}
|
|
1686
1993
|
clear() {
|
|
1687
1994
|
this.terminal.reset();
|
|
1995
|
+
this._textLines = null;
|
|
1996
|
+
this._textLinesLower = null;
|
|
1688
1997
|
}
|
|
1689
1998
|
destroy() {
|
|
1690
1999
|
this.terminal.destroy();
|
|
@@ -1823,6 +2132,7 @@ class ColoredSelectRenderable extends SelectRenderable {
|
|
|
1823
2132
|
const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
|
|
1824
2133
|
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
|
|
1825
2134
|
const selectedTextColor = this._selectedTextColor;
|
|
2135
|
+
const lineWidth = fb.width;
|
|
1826
2136
|
for (let i = 0;i < visibleCount; i++) {
|
|
1827
2137
|
const actualIndex = scrollOffset + i;
|
|
1828
2138
|
const itemY = i * linesPerItem;
|
|
@@ -1830,13 +2140,11 @@ class ColoredSelectRenderable extends SelectRenderable {
|
|
|
1830
2140
|
const isSelected = actualIndex === selectedIndex;
|
|
1831
2141
|
const defaultColor = isSelected ? selectedTextColor : baseTextColor;
|
|
1832
2142
|
const colors = this._optionColors[actualIndex];
|
|
1833
|
-
|
|
2143
|
+
const textColor = colors?.name ?? defaultColor;
|
|
2144
|
+
fb.drawText(optName.padEnd(lineWidth), 1, itemY, textColor);
|
|
1834
2145
|
if (colors?.icon) {
|
|
1835
2146
|
fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
|
|
1836
2147
|
}
|
|
1837
|
-
if (colors?.name) {
|
|
1838
|
-
fb.drawText(optName.slice(2), 3, itemY, colors.name);
|
|
1839
|
-
}
|
|
1840
2148
|
}
|
|
1841
2149
|
}
|
|
1842
2150
|
}
|
|
@@ -1993,6 +2301,7 @@ class App {
|
|
|
1993
2301
|
sidebarWidth = 20;
|
|
1994
2302
|
config;
|
|
1995
2303
|
resizeTimer = null;
|
|
2304
|
+
searchTimer = null;
|
|
1996
2305
|
searchMode = false;
|
|
1997
2306
|
searchQuery = "";
|
|
1998
2307
|
searchMatches = [];
|
|
@@ -2048,6 +2357,11 @@ class App {
|
|
|
2048
2357
|
const interactive = this.config.processes[name].interactive === true;
|
|
2049
2358
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
2050
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
|
+
});
|
|
2051
2365
|
this.panes.set(name, pane);
|
|
2052
2366
|
paneContainer.add(pane.scrollBox);
|
|
2053
2367
|
}
|
|
@@ -2248,6 +2562,10 @@ class App {
|
|
|
2248
2562
|
this.searchQuery = "";
|
|
2249
2563
|
this.searchMatches = [];
|
|
2250
2564
|
this.searchIndex = -1;
|
|
2565
|
+
if (this.searchTimer) {
|
|
2566
|
+
clearTimeout(this.searchTimer);
|
|
2567
|
+
this.searchTimer = null;
|
|
2568
|
+
}
|
|
2251
2569
|
if (this.activePane) {
|
|
2252
2570
|
this.panes.get(this.activePane)?.clearHighlights();
|
|
2253
2571
|
}
|
|
@@ -2273,15 +2591,24 @@ class App {
|
|
|
2273
2591
|
if (key.name === "backspace") {
|
|
2274
2592
|
if (this.searchQuery.length > 0) {
|
|
2275
2593
|
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
2276
|
-
this.
|
|
2594
|
+
this.scheduleSearch();
|
|
2277
2595
|
}
|
|
2278
2596
|
return;
|
|
2279
2597
|
}
|
|
2280
2598
|
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2281
2599
|
this.searchQuery += key.sequence;
|
|
2282
|
-
this.
|
|
2600
|
+
this.scheduleSearch();
|
|
2283
2601
|
}
|
|
2284
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
|
+
}
|
|
2285
2612
|
runSearch() {
|
|
2286
2613
|
if (!this.activePane)
|
|
2287
2614
|
return;
|
|
@@ -2325,6 +2652,10 @@ class App {
|
|
|
2325
2652
|
clearTimeout(this.resizeTimer);
|
|
2326
2653
|
this.resizeTimer = null;
|
|
2327
2654
|
}
|
|
2655
|
+
if (this.searchTimer) {
|
|
2656
|
+
clearTimeout(this.searchTimer);
|
|
2657
|
+
this.searchTimer = null;
|
|
2658
|
+
}
|
|
2328
2659
|
for (const timer of this.inputWaitTimers.values()) {
|
|
2329
2660
|
clearTimeout(timer);
|
|
2330
2661
|
}
|
|
@@ -2352,7 +2683,6 @@ class PrefixDisplay {
|
|
|
2352
2683
|
noColor;
|
|
2353
2684
|
decoders = new Map;
|
|
2354
2685
|
buffers = new Map;
|
|
2355
|
-
maxNameLen;
|
|
2356
2686
|
logWriter;
|
|
2357
2687
|
killOthers;
|
|
2358
2688
|
timestamps;
|
|
@@ -2364,7 +2694,6 @@ class PrefixDisplay {
|
|
|
2364
2694
|
this.timestamps = options.timestamps ?? false;
|
|
2365
2695
|
this.noColor = "NO_COLOR" in process.env;
|
|
2366
2696
|
const names = manager.getProcessNames();
|
|
2367
|
-
this.maxNameLen = Math.max(...names.map((n) => n.length));
|
|
2368
2697
|
this.colors = buildProcessColorMap(names, config);
|
|
2369
2698
|
for (const name of names) {
|
|
2370
2699
|
this.decoders.set(name, new TextDecoder("utf-8", { fatal: false }));
|
|
@@ -2438,15 +2767,14 @@ class PrefixDisplay {
|
|
|
2438
2767
|
return `${h}:${m}:${s}`;
|
|
2439
2768
|
}
|
|
2440
2769
|
printLine(name, line) {
|
|
2441
|
-
const padded = name.padEnd(this.maxNameLen);
|
|
2442
2770
|
const ts = this.timestamps ? `${DIM}[${this.formatTimestamp()}]${RESET} ` : "";
|
|
2443
2771
|
const tsPlain = this.timestamps ? `[${this.formatTimestamp()}] ` : "";
|
|
2444
2772
|
if (this.noColor) {
|
|
2445
|
-
process.stdout.write(`${tsPlain}[${
|
|
2773
|
+
process.stdout.write(`${tsPlain}[${name}] ${stripAnsi(line)}
|
|
2446
2774
|
`);
|
|
2447
2775
|
} else {
|
|
2448
2776
|
const color = this.colors.get(name) ?? "";
|
|
2449
|
-
process.stdout.write(`${ts}${color}[${
|
|
2777
|
+
process.stdout.write(`${ts}${color}[${name}]${RESET} ${line}
|
|
2450
2778
|
`);
|
|
2451
2779
|
}
|
|
2452
2780
|
}
|
|
@@ -2594,36 +2922,7 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
2594
2922
|
}
|
|
2595
2923
|
|
|
2596
2924
|
// src/index.ts
|
|
2597
|
-
var HELP =
|
|
2598
|
-
|
|
2599
|
-
Usage:
|
|
2600
|
-
numux Run processes from config file
|
|
2601
|
-
numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel
|
|
2602
|
-
numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands
|
|
2603
|
-
numux init Create a starter config file
|
|
2604
|
-
numux validate Validate config and show process graph
|
|
2605
|
-
numux exec <name> [--] <cmd> Run a command in a process's environment
|
|
2606
|
-
numux completions <shell> Generate shell completions (bash, zsh, fish)
|
|
2607
|
-
|
|
2608
|
-
Options:
|
|
2609
|
-
-n, --name <name=command> Add a named process
|
|
2610
|
-
-c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
|
|
2611
|
-
--colors Auto-assign colors to processes based on their name
|
|
2612
|
-
--config <path> Config file path (default: auto-detect)
|
|
2613
|
-
-p, --prefix Prefixed output mode (no TUI, for CI/scripts)
|
|
2614
|
-
--only <a,b,...> Only run these processes (+ their dependencies)
|
|
2615
|
-
--exclude <a,b,...> Exclude these processes
|
|
2616
|
-
--kill-others Kill all processes when any exits
|
|
2617
|
-
--no-restart Disable auto-restart for crashed processes
|
|
2618
|
-
--no-watch Disable file watching even if config has watch patterns
|
|
2619
|
-
-t, --timestamps Add timestamps to prefixed output lines
|
|
2620
|
-
--log-dir <path> Write per-process logs to directory
|
|
2621
|
-
--debug Enable debug logging to .numux/debug.log
|
|
2622
|
-
-h, --help Show this help
|
|
2623
|
-
-v, --version Show version
|
|
2624
|
-
|
|
2625
|
-
Config files (auto-detected):
|
|
2626
|
-
numux.config.ts, numux.config.js`;
|
|
2925
|
+
var HELP = generateHelp();
|
|
2627
2926
|
var INIT_TEMPLATE = `import { defineConfig } from 'numux'
|
|
2628
2927
|
|
|
2629
2928
|
export default defineConfig({
|
|
@@ -2658,8 +2957,8 @@ async function main() {
|
|
|
2658
2957
|
process.exit(0);
|
|
2659
2958
|
}
|
|
2660
2959
|
if (parsed.init) {
|
|
2661
|
-
const target =
|
|
2662
|
-
if (
|
|
2960
|
+
const target = resolve8("numux.config.ts");
|
|
2961
|
+
if (existsSync5(target)) {
|
|
2663
2962
|
console.error(`Already exists: ${target}`);
|
|
2664
2963
|
process.exit(1);
|
|
2665
2964
|
}
|
|
@@ -2717,7 +3016,7 @@ async function main() {
|
|
|
2717
3016
|
const names = Object.keys(config2.processes);
|
|
2718
3017
|
throw new Error(`Unknown process "${parsed.execName}". Available: ${names.join(", ")}`);
|
|
2719
3018
|
}
|
|
2720
|
-
const cwd = proc.cwd ?
|
|
3019
|
+
const cwd = proc.cwd ? resolve8(proc.cwd) : process.cwd();
|
|
2721
3020
|
const envFromFile = proc.envFile ? loadEnvFiles(proc.envFile, cwd) : {};
|
|
2722
3021
|
const env = {
|
|
2723
3022
|
...process.env,
|
|
@@ -2738,7 +3037,7 @@ async function main() {
|
|
|
2738
3037
|
}
|
|
2739
3038
|
let config;
|
|
2740
3039
|
const warnings = [];
|
|
2741
|
-
if (parsed.commands.length > 0 || parsed.named.length > 0) {
|
|
3040
|
+
if (parsed.commands.length > 0 || parsed.named.length > 0 || parsed.workspace) {
|
|
2742
3041
|
const isScriptPattern = (c) => c.startsWith("npm:") || /[*?[]/.test(c);
|
|
2743
3042
|
const hasNpmPatterns = parsed.commands.some(isScriptPattern);
|
|
2744
3043
|
if (hasNpmPatterns) {
|
|
@@ -2769,6 +3068,21 @@ async function main() {
|
|
|
2769
3068
|
colors: parsed.colors
|
|
2770
3069
|
});
|
|
2771
3070
|
}
|
|
3071
|
+
if (parsed.workspace) {
|
|
3072
|
+
const wsProcesses = resolveWorkspaceProcesses(parsed.workspace, process.cwd());
|
|
3073
|
+
for (const [name, proc] of Object.entries(wsProcesses)) {
|
|
3074
|
+
let finalName = name;
|
|
3075
|
+
if (config.processes[finalName]) {
|
|
3076
|
+
let suffix = 1;
|
|
3077
|
+
while (config.processes[`${finalName}-${suffix}`])
|
|
3078
|
+
suffix++;
|
|
3079
|
+
finalName = `${finalName}-${suffix}`;
|
|
3080
|
+
}
|
|
3081
|
+
if (parsed.noRestart)
|
|
3082
|
+
proc.maxRestarts = 0;
|
|
3083
|
+
config.processes[finalName] = proc;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
2772
3086
|
} else {
|
|
2773
3087
|
const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
|
|
2774
3088
|
config = validateConfig(raw, warnings);
|
|
@@ -2800,6 +3114,11 @@ async function main() {
|
|
|
2800
3114
|
}
|
|
2801
3115
|
printWarnings(warnings);
|
|
2802
3116
|
if (parsed.prefix) {
|
|
3117
|
+
if (!parsed.noRestart) {
|
|
3118
|
+
for (const proc of Object.values(config.processes)) {
|
|
3119
|
+
proc.maxRestarts ??= 0;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
2803
3122
|
const display = new PrefixDisplay(manager, config, {
|
|
2804
3123
|
logWriter,
|
|
2805
3124
|
killOthers: parsed.killOthers,
|