numux 0.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/package.json +42 -4
- package/src/cli.ts +207 -0
- package/src/completions.ts +119 -0
- package/src/config/interpolate.ts +50 -0
- package/src/config/loader.ts +76 -0
- package/src/config/resolver.ts +67 -0
- package/src/config/validator.ts +140 -0
- package/src/config.ts +8 -0
- package/src/index.ts +229 -0
- package/src/process/manager.ts +377 -0
- package/src/process/ready.ts +45 -0
- package/src/process/runner.ts +243 -0
- package/src/types.ts +45 -0
- package/src/ui/app.ts +424 -0
- package/src/ui/pane.ts +125 -0
- package/src/ui/prefix.ts +199 -0
- package/src/ui/status-bar.ts +119 -0
- package/src/ui/tabs.ts +176 -0
- package/src/utils/color.ts +85 -0
- package/src/utils/env-file.ts +58 -0
- package/src/utils/log-writer.ts +48 -0
- package/src/utils/logger.ts +32 -0
- package/src/utils/shutdown.ts +39 -0
- package/src/utils/watcher.ts +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 hyldmo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1 +1,293 @@
|
|
|
1
1
|
# numux
|
|
2
|
+
|
|
3
|
+
Terminal multiplexer with dependency orchestration. Run multiple processes in a tabbed TUI with a dependency graph controlling startup order.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Requires [Bun](https://bun.sh) >= 1.0.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun install -g numux
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Quick start
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
numux init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This creates a starter `numux.config.ts` with commented-out examples. Edit it, then run `numux`.
|
|
22
|
+
|
|
23
|
+
### Config file
|
|
24
|
+
|
|
25
|
+
Create `numux.config.ts` (or `.js`, `.yaml`, `.yml`, `.json`, or a `"numux"` key in `package.json`):
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { defineConfig } from 'numux'
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
processes: {
|
|
32
|
+
db: {
|
|
33
|
+
command: 'docker compose up postgres',
|
|
34
|
+
readyPattern: 'ready to accept connections',
|
|
35
|
+
},
|
|
36
|
+
migrate: {
|
|
37
|
+
command: 'bun run migrate',
|
|
38
|
+
dependsOn: ['db'],
|
|
39
|
+
persistent: false,
|
|
40
|
+
},
|
|
41
|
+
api: {
|
|
42
|
+
command: 'bun run dev:api',
|
|
43
|
+
dependsOn: ['migrate'],
|
|
44
|
+
readyPattern: 'listening on port 3000',
|
|
45
|
+
},
|
|
46
|
+
// String shorthand for simple processes
|
|
47
|
+
web: 'bun run dev:web',
|
|
48
|
+
// Interactive process — keyboard input is forwarded
|
|
49
|
+
confirm: {
|
|
50
|
+
command: 'sh -c "printf \'Deploy to staging? [y/n] \' && read answer && echo $answer"',
|
|
51
|
+
interactive: true,
|
|
52
|
+
persistent: false,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The `defineConfig()` helper is optional — it provides type checking for your config.
|
|
59
|
+
|
|
60
|
+
Processes can be a string (shorthand for `{ command: "..." }`) or a full config object.
|
|
61
|
+
|
|
62
|
+
Then run:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
numux
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Subcommands
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
numux init # Create a starter numux.config.ts
|
|
72
|
+
numux validate # Validate config and show process dependency graph
|
|
73
|
+
numux exec <name> [--] <command> # Run a command in a process's environment
|
|
74
|
+
numux completions <shell> # Generate shell completions (bash, zsh, fish)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`validate` respects `--only`/`--exclude` filters and shows processes grouped by dependency tiers.
|
|
78
|
+
|
|
79
|
+
`exec` runs a one-off command using a process's configured `cwd`, `env`, and `envFile` — useful for migrations, scripts, or any command that needs the same environment:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
numux exec api -- npx prisma migrate
|
|
83
|
+
numux exec web npm run build
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Set up completions for your shell:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
# Bash (add to ~/.bashrc)
|
|
90
|
+
eval "$(numux completions bash)"
|
|
91
|
+
|
|
92
|
+
# Zsh (add to ~/.zshrc)
|
|
93
|
+
eval "$(numux completions zsh)"
|
|
94
|
+
|
|
95
|
+
# Fish
|
|
96
|
+
numux completions fish | source
|
|
97
|
+
# Or save permanently:
|
|
98
|
+
numux completions fish > ~/.config/fish/completions/numux.fish
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Ad-hoc commands
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
# Unnamed (name derived from command)
|
|
105
|
+
numux "bun dev:api" "bun dev:web"
|
|
106
|
+
|
|
107
|
+
# Named
|
|
108
|
+
numux -n api="bun dev:api" -n web="bun dev:web"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Options
|
|
112
|
+
|
|
113
|
+
| Flag | Description |
|
|
114
|
+
|------|-------------|
|
|
115
|
+
| `-c, --config <path>` | Explicit config file path |
|
|
116
|
+
| `-n, --name <name=cmd>` | Add a named process (repeatable) |
|
|
117
|
+
| `-p, --prefix` | Prefixed output mode (no TUI, for CI/scripts) |
|
|
118
|
+
| `--only <a,b,...>` | Only run these processes (+ their dependencies) |
|
|
119
|
+
| `--exclude <a,b,...>` | Exclude these processes |
|
|
120
|
+
| `--kill-others` | Kill all processes when any exits |
|
|
121
|
+
| `--no-restart` | Disable auto-restart for crashed processes |
|
|
122
|
+
| `--no-watch` | Disable file watching even if config has `watch` patterns |
|
|
123
|
+
| `-t, --timestamps` | Add `[HH:MM:SS]` timestamps to prefixed output |
|
|
124
|
+
| `--log-dir <path>` | Write per-process output to `<path>/<name>.log` |
|
|
125
|
+
| `--debug` | Log to `.numux/debug.log` |
|
|
126
|
+
| `-h, --help` | Show help |
|
|
127
|
+
| `-v, --version` | Show version |
|
|
128
|
+
|
|
129
|
+
### Prefix mode
|
|
130
|
+
|
|
131
|
+
Use `--prefix` (`-p`) for CI or headless environments. Output is printed with colored `[name]` prefixes instead of the TUI:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
numux --prefix
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Auto-exits when all processes finish. Exit code 1 if any process failed.
|
|
138
|
+
|
|
139
|
+
## Config reference
|
|
140
|
+
|
|
141
|
+
### Global options
|
|
142
|
+
|
|
143
|
+
Top-level options apply to all processes (process-level settings override):
|
|
144
|
+
|
|
145
|
+
| Field | Type | Description |
|
|
146
|
+
|-------|------|-------------|
|
|
147
|
+
| `cwd` | `string` | Working directory for all processes (process `cwd` overrides) |
|
|
148
|
+
| `env` | `Record<string, string>` | Environment variables merged into all processes (process `env` overrides per key) |
|
|
149
|
+
| `envFile` | `string \| string[]` | `.env` file(s) for all processes (process `envFile` replaces if set) |
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
export default defineConfig({
|
|
153
|
+
cwd: './packages/backend',
|
|
154
|
+
env: { NODE_ENV: 'development' },
|
|
155
|
+
envFile: '.env',
|
|
156
|
+
processes: {
|
|
157
|
+
api: { command: 'node server.js' }, // inherits cwd, env, envFile
|
|
158
|
+
web: { command: 'vite', cwd: './packages/web' }, // overrides cwd
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Process options
|
|
164
|
+
|
|
165
|
+
Each process accepts:
|
|
166
|
+
|
|
167
|
+
| Field | Type | Default | Description |
|
|
168
|
+
|-------|------|---------|-------------|
|
|
169
|
+
| `command` | `string` | *required* | Shell command to run |
|
|
170
|
+
| `cwd` | `string` | `process.cwd()` | Working directory |
|
|
171
|
+
| `env` | `Record<string, string>` | — | Extra environment variables |
|
|
172
|
+
| `envFile` | `string \| string[]` | — | `.env` file path(s) to load (relative to `cwd`) |
|
|
173
|
+
| `dependsOn` | `string[]` | — | Processes that must be ready first |
|
|
174
|
+
| `readyPattern` | `string` | — | Regex matched against stdout to signal readiness |
|
|
175
|
+
| `readyTimeout` | `number` | — | Milliseconds to wait for `readyPattern` before failing |
|
|
176
|
+
| `persistent` | `boolean` | `true` | `false` for one-shot commands (exit 0 = ready) |
|
|
177
|
+
| `maxRestarts` | `number` | `Infinity` | Max auto-restart attempts before giving up |
|
|
178
|
+
| `delay` | `number` | — | Milliseconds to wait before starting the process |
|
|
179
|
+
| `condition` | `string` | — | Env var name; process skipped if falsy. Prefix with `!` to negate |
|
|
180
|
+
| `stopSignal` | `string` | `SIGTERM` | Signal for graceful stop (`SIGTERM`, `SIGINT`, or `SIGHUP`) |
|
|
181
|
+
| `color` | `string` | auto | Hex color for tab icon and status bar (e.g. `"#ff6600"`) |
|
|
182
|
+
| `watch` | `string \| string[]` | — | Glob patterns — restart process when matching files change |
|
|
183
|
+
| `interactive` | `boolean` | `false` | When `true`, keyboard input is forwarded to the process |
|
|
184
|
+
|
|
185
|
+
### File watching
|
|
186
|
+
|
|
187
|
+
Use `watch` to automatically restart a process when source files change:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
export default defineConfig({
|
|
191
|
+
processes: {
|
|
192
|
+
api: {
|
|
193
|
+
command: 'node server.js',
|
|
194
|
+
watch: 'src/**/*.ts',
|
|
195
|
+
},
|
|
196
|
+
styles: {
|
|
197
|
+
command: 'sass --watch src:dist',
|
|
198
|
+
watch: ['src/**/*.scss', 'src/**/*.css'],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Patterns are matched relative to the process's `cwd` (or the project root). Changes in `node_modules` and `.git` are always ignored. Rapid file changes are debounced (300ms) to avoid restart storms.
|
|
205
|
+
|
|
206
|
+
A watched process is only restarted if it's currently running, ready, or failed — manually stopped processes are not affected.
|
|
207
|
+
|
|
208
|
+
### Environment variable interpolation
|
|
209
|
+
|
|
210
|
+
Config values support `${VAR}` syntax for environment variable substitution:
|
|
211
|
+
|
|
212
|
+
```yaml
|
|
213
|
+
processes:
|
|
214
|
+
api:
|
|
215
|
+
command: node server.js --port ${PORT:-3000}
|
|
216
|
+
env:
|
|
217
|
+
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Syntax | Behavior |
|
|
221
|
+
|--------|----------|
|
|
222
|
+
| `${VAR}` | Value of `VAR`, or empty string if unset |
|
|
223
|
+
| `${VAR:-default}` | Value of `VAR`, or `default` if unset |
|
|
224
|
+
| `${VAR:?error}` | Value of `VAR`, or error with message if unset |
|
|
225
|
+
|
|
226
|
+
Interpolation applies to all string values in the config (command, cwd, env, envFile, readyPattern, etc.).
|
|
227
|
+
|
|
228
|
+
### Conditional processes
|
|
229
|
+
|
|
230
|
+
Use `condition` to run a process only when an environment variable is set:
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
processes:
|
|
234
|
+
seed:
|
|
235
|
+
command: bun run seed
|
|
236
|
+
persistent: false
|
|
237
|
+
condition: SEED_DB # only runs when SEED_DB is set and truthy
|
|
238
|
+
storybook:
|
|
239
|
+
command: bun run storybook
|
|
240
|
+
condition: "!CI" # skipped in CI environments
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Falsy values: unset, empty string, `"0"`, `"false"`, `"no"`, `"off"` (case-insensitive). If a conditional process is skipped, its dependents are also skipped.
|
|
244
|
+
|
|
245
|
+
### Dependency orchestration
|
|
246
|
+
|
|
247
|
+
Processes are grouped into tiers by topological sort. Each tier starts after the previous tier is ready. If a process fails, its dependents are skipped.
|
|
248
|
+
|
|
249
|
+
A process becomes **ready** when:
|
|
250
|
+
- **persistent + readyPattern** — the pattern matches in stdout
|
|
251
|
+
- **persistent + no readyPattern** — immediately after spawn
|
|
252
|
+
- **non-persistent** — exits with code 0
|
|
253
|
+
|
|
254
|
+
Persistent processes that crash are auto-restarted with exponential backoff (1s–30s). Backoff resets after 10s of uptime.
|
|
255
|
+
|
|
256
|
+
## Keybindings
|
|
257
|
+
|
|
258
|
+
| Key | Action |
|
|
259
|
+
|-----|--------|
|
|
260
|
+
| `Ctrl+C` | Quit (graceful shutdown) |
|
|
261
|
+
| `Alt+R` | Restart active process |
|
|
262
|
+
| `Alt+Shift+R` | Restart all processes |
|
|
263
|
+
| `Alt+S` | Stop/start active process |
|
|
264
|
+
| `Alt+L` | Clear active pane output |
|
|
265
|
+
| `Alt+1`–`Alt+9` | Jump to tab |
|
|
266
|
+
| `Alt+Left/Right` | Cycle tabs |
|
|
267
|
+
| `Up/Down` | Scroll output 1 line (non-interactive panes) |
|
|
268
|
+
| `PageUp/PageDown` | Scroll output by page (non-interactive panes) |
|
|
269
|
+
| `Home/End` | Scroll to top/bottom (non-interactive panes) |
|
|
270
|
+
| `Alt+PageUp/PageDown` | Scroll output up/down |
|
|
271
|
+
| `Alt+Home/End` | Scroll to top/bottom |
|
|
272
|
+
| `Alt+F` | Search in active pane output |
|
|
273
|
+
|
|
274
|
+
While searching: type to filter, `Enter`/`Shift+Enter` to navigate matches, `Escape` to close.
|
|
275
|
+
|
|
276
|
+
Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
|
|
277
|
+
|
|
278
|
+
## Tab icons
|
|
279
|
+
|
|
280
|
+
| Icon | Status |
|
|
281
|
+
|------|--------|
|
|
282
|
+
| ○ | Pending |
|
|
283
|
+
| ◐ | Starting |
|
|
284
|
+
| ◉ | Running |
|
|
285
|
+
| ● | Ready |
|
|
286
|
+
| ◑ | Stopping |
|
|
287
|
+
| ■ | Stopped |
|
|
288
|
+
| ✖ | Failed |
|
|
289
|
+
| ⊘ | Skipped |
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,11 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "numux",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Terminal multiplexer with dependency orchestration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "hyldmo",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/hyldmo/numux.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"terminal",
|
|
14
|
+
"multiplexer",
|
|
15
|
+
"process-manager",
|
|
16
|
+
"tui",
|
|
17
|
+
"dev-tools",
|
|
18
|
+
"orchestration"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.0"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"numux": "src/index.ts"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/config.ts"
|
|
28
|
+
},
|
|
5
29
|
"scripts": {
|
|
30
|
+
"dev": "bun run src/index.ts",
|
|
31
|
+
"test": "bun test",
|
|
32
|
+
"typecheck": "bunx tsc --noEmit",
|
|
33
|
+
"lint": "biome check .",
|
|
6
34
|
"fix": "biome check . --fix --unsafe"
|
|
7
35
|
},
|
|
8
36
|
"files": [
|
|
9
|
-
"
|
|
10
|
-
|
|
37
|
+
"src/**/*.ts",
|
|
38
|
+
"!src/**/*.test.ts"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@opentui/core": "^0.1.81",
|
|
42
|
+
"ghostty-opentui": "^1.4.3",
|
|
43
|
+
"yaml": "^2.8.2"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@biomejs/biome": "^2.4.4",
|
|
47
|
+
"@types/bun": "^1.3.9"
|
|
48
|
+
}
|
|
11
49
|
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { ResolvedNumuxConfig } from './types'
|
|
2
|
+
|
|
3
|
+
export interface ParsedArgs {
|
|
4
|
+
help: boolean
|
|
5
|
+
version: boolean
|
|
6
|
+
debug: boolean
|
|
7
|
+
init: boolean
|
|
8
|
+
validate: boolean
|
|
9
|
+
exec: boolean
|
|
10
|
+
execName?: string
|
|
11
|
+
execCommand?: string
|
|
12
|
+
completions?: string
|
|
13
|
+
prefix: boolean
|
|
14
|
+
killOthers: boolean
|
|
15
|
+
timestamps: boolean
|
|
16
|
+
noRestart: boolean
|
|
17
|
+
noWatch: boolean
|
|
18
|
+
configPath?: string
|
|
19
|
+
logDir?: string
|
|
20
|
+
only?: string[]
|
|
21
|
+
exclude?: string[]
|
|
22
|
+
commands: string[]
|
|
23
|
+
named: Array<{ name: string; command: string }>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
27
|
+
const result: ParsedArgs = {
|
|
28
|
+
help: false,
|
|
29
|
+
version: false,
|
|
30
|
+
debug: false,
|
|
31
|
+
init: false,
|
|
32
|
+
validate: false,
|
|
33
|
+
exec: false,
|
|
34
|
+
prefix: false,
|
|
35
|
+
killOthers: false,
|
|
36
|
+
timestamps: false,
|
|
37
|
+
noRestart: false,
|
|
38
|
+
noWatch: false,
|
|
39
|
+
configPath: undefined,
|
|
40
|
+
commands: [],
|
|
41
|
+
named: []
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const args = argv.slice(2) // skip bun + script
|
|
45
|
+
let i = 0
|
|
46
|
+
|
|
47
|
+
/** Consume the next argument as a value for the given flag, erroring if missing */
|
|
48
|
+
const consumeValue = (flag: string): string => {
|
|
49
|
+
const next = args[++i]
|
|
50
|
+
if (next === undefined) {
|
|
51
|
+
throw new Error(`Missing value for ${flag}`)
|
|
52
|
+
}
|
|
53
|
+
return next
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
while (i < args.length) {
|
|
57
|
+
const arg = args[i]
|
|
58
|
+
|
|
59
|
+
if (arg === '-h' || arg === '--help') {
|
|
60
|
+
result.help = true
|
|
61
|
+
} else if (arg === '-v' || arg === '--version') {
|
|
62
|
+
result.version = true
|
|
63
|
+
} else if (arg === '--debug') {
|
|
64
|
+
result.debug = true
|
|
65
|
+
} else if (arg === '-p' || arg === '--prefix') {
|
|
66
|
+
result.prefix = true
|
|
67
|
+
} else if (arg === '--kill-others') {
|
|
68
|
+
result.killOthers = true
|
|
69
|
+
} else if (arg === '-t' || arg === '--timestamps') {
|
|
70
|
+
result.timestamps = true
|
|
71
|
+
} else if (arg === '--no-restart') {
|
|
72
|
+
result.noRestart = true
|
|
73
|
+
} else if (arg === '--no-watch') {
|
|
74
|
+
result.noWatch = true
|
|
75
|
+
} else if (arg === '-c' || arg === '--config') {
|
|
76
|
+
result.configPath = consumeValue(arg)
|
|
77
|
+
} else if (arg === '--log-dir') {
|
|
78
|
+
result.logDir = consumeValue(arg)
|
|
79
|
+
} else if (arg === '--only') {
|
|
80
|
+
result.only = consumeValue(arg)
|
|
81
|
+
.split(',')
|
|
82
|
+
.map(s => s.trim())
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
} else if (arg === '--exclude') {
|
|
85
|
+
result.exclude = consumeValue(arg)
|
|
86
|
+
.split(',')
|
|
87
|
+
.map(s => s.trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
} else if (arg === '-n' || arg === '--name') {
|
|
90
|
+
const value = consumeValue(arg)
|
|
91
|
+
const eq = value.indexOf('=')
|
|
92
|
+
if (eq < 1) {
|
|
93
|
+
throw new Error(`Invalid --name value: expected "name=command", got "${value}"`)
|
|
94
|
+
}
|
|
95
|
+
result.named.push({
|
|
96
|
+
name: value.slice(0, eq),
|
|
97
|
+
command: value.slice(eq + 1)
|
|
98
|
+
})
|
|
99
|
+
} else if (arg === 'init' && result.commands.length === 0) {
|
|
100
|
+
result.init = true
|
|
101
|
+
} else if (arg === 'validate' && result.commands.length === 0) {
|
|
102
|
+
result.validate = true
|
|
103
|
+
} else if (arg === 'exec' && result.commands.length === 0) {
|
|
104
|
+
result.exec = true
|
|
105
|
+
const name = args[++i]
|
|
106
|
+
if (!name) throw new Error('exec requires a process name')
|
|
107
|
+
result.execName = name
|
|
108
|
+
// Skip optional --
|
|
109
|
+
if (args[i + 1] === '--') i++
|
|
110
|
+
const rest = args.slice(i + 1)
|
|
111
|
+
if (rest.length === 0) throw new Error('exec requires a command to run')
|
|
112
|
+
result.execCommand = rest.join(' ')
|
|
113
|
+
break
|
|
114
|
+
} else if (arg === 'completions' && result.commands.length === 0) {
|
|
115
|
+
result.completions = consumeValue(arg)
|
|
116
|
+
} else if (!arg.startsWith('-')) {
|
|
117
|
+
result.commands.push(arg)
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error(`Unknown option: ${arg}`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
i++
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildConfigFromArgs(
|
|
129
|
+
commands: string[],
|
|
130
|
+
named: Array<{ name: string; command: string }>,
|
|
131
|
+
options?: { noRestart?: boolean }
|
|
132
|
+
): ResolvedNumuxConfig {
|
|
133
|
+
const processes: ResolvedNumuxConfig['processes'] = {}
|
|
134
|
+
const maxRestarts = options?.noRestart ? 0 : undefined
|
|
135
|
+
|
|
136
|
+
for (const { name, command } of named) {
|
|
137
|
+
processes[name] = { command, persistent: true, maxRestarts }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < commands.length; i++) {
|
|
141
|
+
const cmd = commands[i]
|
|
142
|
+
// Derive name from command: first word, deduplicated
|
|
143
|
+
let name = cmd.split(/\s+/)[0].split('/').pop()!
|
|
144
|
+
if (processes[name]) {
|
|
145
|
+
name = `${name}-${i}`
|
|
146
|
+
}
|
|
147
|
+
processes[name] = { command: cmd, persistent: true, maxRestarts }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { processes }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Filter a config to include/exclude specific processes. --only also pulls in transitive dependencies. */
|
|
154
|
+
export function filterConfig(config: ResolvedNumuxConfig, only?: string[], exclude?: string[]): ResolvedNumuxConfig {
|
|
155
|
+
const allNames = Object.keys(config.processes)
|
|
156
|
+
|
|
157
|
+
let selected: Set<string>
|
|
158
|
+
|
|
159
|
+
if (only && only.length > 0) {
|
|
160
|
+
// Validate names exist
|
|
161
|
+
for (const name of only) {
|
|
162
|
+
if (!allNames.includes(name)) {
|
|
163
|
+
throw new Error(`--only: unknown process "${name}"`)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Collect transitive dependencies
|
|
167
|
+
selected = new Set<string>()
|
|
168
|
+
const queue = [...only]
|
|
169
|
+
while (queue.length > 0) {
|
|
170
|
+
const name = queue.pop()!
|
|
171
|
+
if (selected.has(name)) continue
|
|
172
|
+
selected.add(name)
|
|
173
|
+
const deps = config.processes[name].dependsOn ?? []
|
|
174
|
+
for (const dep of deps) {
|
|
175
|
+
if (!selected.has(dep)) queue.push(dep)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
selected = new Set(allNames)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (exclude && exclude.length > 0) {
|
|
183
|
+
for (const name of exclude) {
|
|
184
|
+
if (!allNames.includes(name)) {
|
|
185
|
+
throw new Error(`--exclude: unknown process "${name}"`)
|
|
186
|
+
}
|
|
187
|
+
selected.delete(name)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (selected.size === 0) {
|
|
192
|
+
throw new Error('No processes left after filtering')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const processes: ResolvedNumuxConfig['processes'] = {}
|
|
196
|
+
for (const name of selected) {
|
|
197
|
+
const proc = { ...config.processes[name] }
|
|
198
|
+
// Remove deps that were filtered out
|
|
199
|
+
if (proc.dependsOn) {
|
|
200
|
+
proc.dependsOn = proc.dependsOn.filter(d => selected.has(d))
|
|
201
|
+
if (proc.dependsOn.length === 0) proc.dependsOn = undefined
|
|
202
|
+
}
|
|
203
|
+
processes[name] = proc
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { processes }
|
|
207
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const SUPPORTED_SHELLS = ['bash', 'zsh', 'fish'] as const
|
|
2
|
+
|
|
3
|
+
export function generateCompletions(shell: string): string {
|
|
4
|
+
switch (shell) {
|
|
5
|
+
case 'bash':
|
|
6
|
+
return bashCompletions()
|
|
7
|
+
case 'zsh':
|
|
8
|
+
return zshCompletions()
|
|
9
|
+
case 'fish':
|
|
10
|
+
return fishCompletions()
|
|
11
|
+
default:
|
|
12
|
+
throw new Error(`Unknown shell: "${shell}". Supported: ${SUPPORTED_SHELLS.join(', ')}`)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function bashCompletions(): string {
|
|
17
|
+
return `# numux bash completions
|
|
18
|
+
# Add to ~/.bashrc: eval "$(numux completions bash)"
|
|
19
|
+
_numux() {
|
|
20
|
+
local cur prev
|
|
21
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
22
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
23
|
+
|
|
24
|
+
case "$prev" in
|
|
25
|
+
-c|--config)
|
|
26
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
27
|
+
return ;;
|
|
28
|
+
--log-dir)
|
|
29
|
+
COMPREPLY=( $(compgen -d -- "$cur") )
|
|
30
|
+
return ;;
|
|
31
|
+
--only|--exclude)
|
|
32
|
+
return ;;
|
|
33
|
+
-n|--name)
|
|
34
|
+
return ;;
|
|
35
|
+
completions)
|
|
36
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
37
|
+
return ;;
|
|
38
|
+
esac
|
|
39
|
+
|
|
40
|
+
if [[ "$cur" == -* ]]; then
|
|
41
|
+
COMPREPLY=( $(compgen -W "-h --help -v --version -c --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
|
|
42
|
+
else
|
|
43
|
+
local subcmds="init validate exec completions"
|
|
44
|
+
COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
complete -F _numux numux`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function zshCompletions(): string {
|
|
51
|
+
return `#compdef numux
|
|
52
|
+
# numux zsh completions
|
|
53
|
+
# Add to ~/.zshrc: eval "$(numux completions zsh)"
|
|
54
|
+
_numux() {
|
|
55
|
+
local -a subcmds
|
|
56
|
+
subcmds=(
|
|
57
|
+
'init:Create a starter config file'
|
|
58
|
+
'validate:Validate config and show process graph'
|
|
59
|
+
'exec:Run a command in a process environment'
|
|
60
|
+
'completions:Generate shell completions'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_arguments -s \\
|
|
64
|
+
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
65
|
+
'(-v --version)'{-v,--version}'[Show version]' \\
|
|
66
|
+
'(-c --config)'{-c,--config}'[Config file path]:file:_files' \\
|
|
67
|
+
'(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
|
|
68
|
+
'(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
|
|
69
|
+
'--only[Only run these processes]:processes' \\
|
|
70
|
+
'--exclude[Exclude these processes]:processes' \\
|
|
71
|
+
'--kill-others[Kill all when any exits]' \\
|
|
72
|
+
'--no-restart[Disable auto-restart]' \\
|
|
73
|
+
'--no-watch[Disable file watching]' \\
|
|
74
|
+
'(-t --timestamps)'{-t,--timestamps}'[Add timestamps to output]' \\
|
|
75
|
+
'--log-dir[Log directory]:directory:_directories' \\
|
|
76
|
+
'--debug[Enable debug logging]' \\
|
|
77
|
+
'1:subcommand:->subcmd' \\
|
|
78
|
+
'*:command' \\
|
|
79
|
+
&& return
|
|
80
|
+
|
|
81
|
+
case "$state" in
|
|
82
|
+
subcmd)
|
|
83
|
+
_describe 'subcommand' subcmds
|
|
84
|
+
;;
|
|
85
|
+
esac
|
|
86
|
+
}
|
|
87
|
+
_numux`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fishCompletions(): string {
|
|
91
|
+
return `# numux fish completions
|
|
92
|
+
# Add to fish: numux completions fish | source
|
|
93
|
+
# Or save to: ~/.config/fish/completions/numux.fish
|
|
94
|
+
complete -c numux -f
|
|
95
|
+
|
|
96
|
+
# Subcommands
|
|
97
|
+
complete -c numux -n __fish_use_subcommand -a init -d 'Create a starter config file'
|
|
98
|
+
complete -c numux -n __fish_use_subcommand -a validate -d 'Validate config and show process graph'
|
|
99
|
+
complete -c numux -n __fish_use_subcommand -a exec -d 'Run a command in a process environment'
|
|
100
|
+
complete -c numux -n __fish_use_subcommand -a completions -d 'Generate shell completions'
|
|
101
|
+
|
|
102
|
+
# Completions subcommand
|
|
103
|
+
complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'
|
|
104
|
+
|
|
105
|
+
# Options
|
|
106
|
+
complete -c numux -s h -l help -d 'Show help'
|
|
107
|
+
complete -c numux -s v -l version -d 'Show version'
|
|
108
|
+
complete -c numux -s c -l config -rF -d 'Config file path'
|
|
109
|
+
complete -c numux -s n -l name -r -d 'Named process (name=command)'
|
|
110
|
+
complete -c numux -s p -l prefix -d 'Prefixed output mode'
|
|
111
|
+
complete -c numux -l only -r -d 'Only run these processes'
|
|
112
|
+
complete -c numux -l exclude -r -d 'Exclude these processes'
|
|
113
|
+
complete -c numux -l kill-others -d 'Kill all when any exits'
|
|
114
|
+
complete -c numux -l no-restart -d 'Disable auto-restart'
|
|
115
|
+
complete -c numux -l no-watch -d 'Disable file watching'
|
|
116
|
+
complete -c numux -s t -l timestamps -d 'Add timestamps to output'
|
|
117
|
+
complete -c numux -l log-dir -ra '(__fish_complete_directories)' -d 'Log directory'
|
|
118
|
+
complete -c numux -l debug -d 'Enable debug logging'`
|
|
119
|
+
}
|