numux 1.4.0 → 1.5.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 +27 -17
- package/dist/bin.js +2611 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +7 -0
- package/dist/types.d.ts +53 -0
- package/package.json +13 -9
- package/src/cli.ts +0 -217
- package/src/completions.ts +0 -121
- package/src/config/expand-scripts.ts +0 -96
- package/src/config/interpolate.ts +0 -50
- package/src/config/loader.ts +0 -76
- package/src/config/resolver.ts +0 -67
- package/src/config/validator.ts +0 -150
- package/src/config.ts +0 -8
- package/src/index.ts +0 -258
- package/src/process/manager.ts +0 -379
- package/src/process/ready.ts +0 -45
- package/src/process/runner.ts +0 -243
- package/src/types.ts +0 -57
- package/src/ui/app.ts +0 -454
- package/src/ui/pane.ts +0 -125
- package/src/ui/prefix.ts +0 -207
- package/src/ui/status-bar.ts +0 -58
- package/src/ui/tabs.ts +0 -246
- package/src/utils/color.ts +0 -93
- package/src/utils/env-file.ts +0 -58
- package/src/utils/log-writer.ts +0 -48
- package/src/utils/logger.ts +0 -32
- package/src/utils/shutdown.ts +0 -39
- package/src/utils/watcher.ts +0 -53
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface NumuxProcessConfig {
|
|
2
|
+
command: string;
|
|
3
|
+
cwd?: string;
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
envFile?: string | string[];
|
|
6
|
+
dependsOn?: string[];
|
|
7
|
+
readyPattern?: string;
|
|
8
|
+
persistent?: boolean;
|
|
9
|
+
maxRestarts?: number;
|
|
10
|
+
readyTimeout?: number;
|
|
11
|
+
delay?: number;
|
|
12
|
+
condition?: string;
|
|
13
|
+
stopSignal?: 'SIGTERM' | 'SIGINT' | 'SIGHUP';
|
|
14
|
+
color?: string | string[];
|
|
15
|
+
watch?: string | string[];
|
|
16
|
+
interactive?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Config for npm: wildcard entries — command is derived from package.json scripts */
|
|
19
|
+
export type NumuxScriptPattern = Omit<NumuxProcessConfig, 'command'> & {
|
|
20
|
+
command?: never;
|
|
21
|
+
};
|
|
22
|
+
/** Raw config as authored — processes can be string shorthand, full objects, or wildcard patterns */
|
|
23
|
+
export interface NumuxConfig {
|
|
24
|
+
cwd?: string;
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
envFile?: string | string[];
|
|
27
|
+
processes: Record<string, NumuxProcessConfig | NumuxScriptPattern | string>;
|
|
28
|
+
}
|
|
29
|
+
/** Validated config with all shorthand expanded to full objects */
|
|
30
|
+
export interface ResolvedNumuxConfig {
|
|
31
|
+
processes: Record<string, NumuxProcessConfig>;
|
|
32
|
+
}
|
|
33
|
+
export type ProcessStatus = 'pending' | 'starting' | 'ready' | 'running' | 'stopping' | 'stopped' | 'finished' | 'failed' | 'skipped';
|
|
34
|
+
export interface ProcessState {
|
|
35
|
+
name: string;
|
|
36
|
+
config: NumuxProcessConfig;
|
|
37
|
+
status: ProcessStatus;
|
|
38
|
+
exitCode: number | null;
|
|
39
|
+
restartCount: number;
|
|
40
|
+
}
|
|
41
|
+
export type ProcessEvent = {
|
|
42
|
+
type: 'status';
|
|
43
|
+
name: string;
|
|
44
|
+
status: ProcessStatus;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'output';
|
|
47
|
+
name: string;
|
|
48
|
+
data: Uint8Array;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'exit';
|
|
51
|
+
name: string;
|
|
52
|
+
code: number | null;
|
|
53
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "numux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Terminal multiplexer with dependency orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,29 +18,33 @@
|
|
|
18
18
|
"orchestration"
|
|
19
19
|
],
|
|
20
20
|
"engines": {
|
|
21
|
-
"bun": ">=1.0"
|
|
21
|
+
"bun": ">=1.0",
|
|
22
|
+
"node": ">=18"
|
|
22
23
|
},
|
|
23
24
|
"bin": {
|
|
24
|
-
"numux": "
|
|
25
|
+
"numux": "dist/bin.js"
|
|
25
26
|
},
|
|
26
27
|
"exports": {
|
|
27
|
-
".":
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/config.d.ts",
|
|
30
|
+
"import": "./dist/config.js"
|
|
31
|
+
}
|
|
28
32
|
},
|
|
29
33
|
"scripts": {
|
|
30
|
-
"
|
|
34
|
+
"build": "bun build src/index.ts --outfile dist/bin.js --target bun --packages external && bun build src/config.ts --outfile dist/config.js --packages external && bunx tsc src/config.ts src/types.ts --emitDeclarationOnly --declaration --outDir dist --target ESNext --module ESNext --moduleResolution bundler",
|
|
35
|
+
"prepublishOnly": "bun run build",
|
|
36
|
+
"dev": "cd example && bun run dev --debug",
|
|
31
37
|
"test": "bun test",
|
|
32
38
|
"typecheck": "bunx tsc --noEmit",
|
|
33
39
|
"lint": "biome check .",
|
|
34
40
|
"fix": "biome check . --fix --unsafe"
|
|
35
41
|
},
|
|
36
42
|
"files": [
|
|
37
|
-
"
|
|
38
|
-
"!src/**/*.test.ts"
|
|
43
|
+
"dist/"
|
|
39
44
|
],
|
|
40
45
|
"dependencies": {
|
|
41
46
|
"@opentui/core": "^0.1.81",
|
|
42
|
-
"ghostty-opentui": "^1.4.3"
|
|
43
|
-
"yaml": "^2.8.2"
|
|
47
|
+
"ghostty-opentui": "^1.4.3"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
50
|
"@biomejs/biome": "^2.4.4",
|
package/src/cli.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
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
|
-
colors?: string[]
|
|
23
|
-
commands: string[]
|
|
24
|
-
named: Array<{ name: string; command: string }>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function parseArgs(argv: string[]): ParsedArgs {
|
|
28
|
-
const result: ParsedArgs = {
|
|
29
|
-
help: false,
|
|
30
|
-
version: false,
|
|
31
|
-
debug: false,
|
|
32
|
-
init: false,
|
|
33
|
-
validate: false,
|
|
34
|
-
exec: false,
|
|
35
|
-
prefix: false,
|
|
36
|
-
killOthers: false,
|
|
37
|
-
timestamps: false,
|
|
38
|
-
noRestart: false,
|
|
39
|
-
noWatch: false,
|
|
40
|
-
configPath: undefined,
|
|
41
|
-
commands: [],
|
|
42
|
-
named: []
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const args = argv.slice(2) // skip bun + script
|
|
46
|
-
let i = 0
|
|
47
|
-
|
|
48
|
-
/** Consume the next argument as a value for the given flag, erroring if missing */
|
|
49
|
-
const consumeValue = (flag: string): string => {
|
|
50
|
-
const next = args[++i]
|
|
51
|
-
if (next === undefined) {
|
|
52
|
-
throw new Error(`Missing value for ${flag}`)
|
|
53
|
-
}
|
|
54
|
-
return next
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
while (i < args.length) {
|
|
58
|
-
const arg = args[i]
|
|
59
|
-
|
|
60
|
-
if (arg === '-h' || arg === '--help') {
|
|
61
|
-
result.help = true
|
|
62
|
-
} else if (arg === '-v' || arg === '--version') {
|
|
63
|
-
result.version = true
|
|
64
|
-
} else if (arg === '--debug') {
|
|
65
|
-
result.debug = true
|
|
66
|
-
} else if (arg === '-p' || arg === '--prefix') {
|
|
67
|
-
result.prefix = true
|
|
68
|
-
} else if (arg === '--kill-others') {
|
|
69
|
-
result.killOthers = true
|
|
70
|
-
} else if (arg === '-t' || arg === '--timestamps') {
|
|
71
|
-
result.timestamps = true
|
|
72
|
-
} else if (arg === '--no-restart') {
|
|
73
|
-
result.noRestart = true
|
|
74
|
-
} else if (arg === '--no-watch') {
|
|
75
|
-
result.noWatch = true
|
|
76
|
-
} else if (arg === '--config') {
|
|
77
|
-
result.configPath = consumeValue(arg)
|
|
78
|
-
} else if (arg === '-c' || arg === '--color') {
|
|
79
|
-
result.colors = consumeValue(arg)
|
|
80
|
-
.split(',')
|
|
81
|
-
.map(s => s.trim())
|
|
82
|
-
.filter(Boolean)
|
|
83
|
-
} else if (arg === '--log-dir') {
|
|
84
|
-
result.logDir = consumeValue(arg)
|
|
85
|
-
} else if (arg === '--only') {
|
|
86
|
-
result.only = consumeValue(arg)
|
|
87
|
-
.split(',')
|
|
88
|
-
.map(s => s.trim())
|
|
89
|
-
.filter(Boolean)
|
|
90
|
-
} else if (arg === '--exclude') {
|
|
91
|
-
result.exclude = consumeValue(arg)
|
|
92
|
-
.split(',')
|
|
93
|
-
.map(s => s.trim())
|
|
94
|
-
.filter(Boolean)
|
|
95
|
-
} else if (arg === '-n' || arg === '--name') {
|
|
96
|
-
const value = consumeValue(arg)
|
|
97
|
-
const eq = value.indexOf('=')
|
|
98
|
-
if (eq < 1) {
|
|
99
|
-
throw new Error(`Invalid --name value: expected "name=command", got "${value}"`)
|
|
100
|
-
}
|
|
101
|
-
result.named.push({
|
|
102
|
-
name: value.slice(0, eq),
|
|
103
|
-
command: value.slice(eq + 1)
|
|
104
|
-
})
|
|
105
|
-
} else if (arg === 'init' && result.commands.length === 0) {
|
|
106
|
-
result.init = true
|
|
107
|
-
} else if (arg === 'validate' && result.commands.length === 0) {
|
|
108
|
-
result.validate = true
|
|
109
|
-
} else if (arg === 'exec' && result.commands.length === 0) {
|
|
110
|
-
result.exec = true
|
|
111
|
-
const name = args[++i]
|
|
112
|
-
if (!name) throw new Error('exec requires a process name')
|
|
113
|
-
result.execName = name
|
|
114
|
-
// Skip optional --
|
|
115
|
-
if (args[i + 1] === '--') i++
|
|
116
|
-
const rest = args.slice(i + 1)
|
|
117
|
-
if (rest.length === 0) throw new Error('exec requires a command to run')
|
|
118
|
-
result.execCommand = rest.join(' ')
|
|
119
|
-
break
|
|
120
|
-
} else if (arg === 'completions' && result.commands.length === 0) {
|
|
121
|
-
result.completions = consumeValue(arg)
|
|
122
|
-
} else if (!arg.startsWith('-')) {
|
|
123
|
-
result.commands.push(arg)
|
|
124
|
-
} else {
|
|
125
|
-
throw new Error(`Unknown option: ${arg}`)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
i++
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return result
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function buildConfigFromArgs(
|
|
135
|
-
commands: string[],
|
|
136
|
-
named: Array<{ name: string; command: string }>,
|
|
137
|
-
options?: { noRestart?: boolean; colors?: string[] }
|
|
138
|
-
): ResolvedNumuxConfig {
|
|
139
|
-
const processes: ResolvedNumuxConfig['processes'] = {}
|
|
140
|
-
const maxRestarts = options?.noRestart ? 0 : undefined
|
|
141
|
-
const colors = options?.colors
|
|
142
|
-
let colorIndex = 0
|
|
143
|
-
|
|
144
|
-
for (const { name, command } of named) {
|
|
145
|
-
const color = colors?.[colorIndex++ % colors.length]
|
|
146
|
-
processes[name] = { command, persistent: true, maxRestarts, ...(color ? { color } : {}) }
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
for (let i = 0; i < commands.length; i++) {
|
|
150
|
-
const cmd = commands[i]
|
|
151
|
-
// Derive name from command: first word, deduplicated
|
|
152
|
-
let name = cmd.split(/\s+/)[0].split('/').pop()!
|
|
153
|
-
if (processes[name]) {
|
|
154
|
-
name = `${name}-${i}`
|
|
155
|
-
}
|
|
156
|
-
const color = colors?.[colorIndex++ % colors.length]
|
|
157
|
-
processes[name] = { command: cmd, persistent: true, maxRestarts, ...(color ? { color } : {}) }
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return { processes }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Filter a config to include/exclude specific processes. --only also pulls in transitive dependencies. */
|
|
164
|
-
export function filterConfig(config: ResolvedNumuxConfig, only?: string[], exclude?: string[]): ResolvedNumuxConfig {
|
|
165
|
-
const allNames = Object.keys(config.processes)
|
|
166
|
-
|
|
167
|
-
let selected: Set<string>
|
|
168
|
-
|
|
169
|
-
if (only && only.length > 0) {
|
|
170
|
-
// Validate names exist
|
|
171
|
-
for (const name of only) {
|
|
172
|
-
if (!allNames.includes(name)) {
|
|
173
|
-
throw new Error(`--only: unknown process "${name}"`)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// Collect transitive dependencies
|
|
177
|
-
selected = new Set<string>()
|
|
178
|
-
const queue = [...only]
|
|
179
|
-
while (queue.length > 0) {
|
|
180
|
-
const name = queue.pop()!
|
|
181
|
-
if (selected.has(name)) continue
|
|
182
|
-
selected.add(name)
|
|
183
|
-
const deps = config.processes[name].dependsOn ?? []
|
|
184
|
-
for (const dep of deps) {
|
|
185
|
-
if (!selected.has(dep)) queue.push(dep)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
selected = new Set(allNames)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (exclude && exclude.length > 0) {
|
|
193
|
-
for (const name of exclude) {
|
|
194
|
-
if (!allNames.includes(name)) {
|
|
195
|
-
throw new Error(`--exclude: unknown process "${name}"`)
|
|
196
|
-
}
|
|
197
|
-
selected.delete(name)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (selected.size === 0) {
|
|
202
|
-
throw new Error('No processes left after filtering')
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const processes: ResolvedNumuxConfig['processes'] = {}
|
|
206
|
-
for (const name of selected) {
|
|
207
|
-
const proc = { ...config.processes[name] }
|
|
208
|
-
// Remove deps that were filtered out
|
|
209
|
-
if (proc.dependsOn) {
|
|
210
|
-
proc.dependsOn = proc.dependsOn.filter(d => selected.has(d))
|
|
211
|
-
if (proc.dependsOn.length === 0) proc.dependsOn = undefined
|
|
212
|
-
}
|
|
213
|
-
processes[name] = proc
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return { processes }
|
|
217
|
-
}
|
package/src/completions.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
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
|
-
--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 --color --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 --color)'{-c,--color}'[Comma-separated colors for processes]' \\
|
|
67
|
-
'--config[Config file path]:file:_files' \\
|
|
68
|
-
'(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
|
|
69
|
-
'(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
|
|
70
|
-
'--only[Only run these processes]:processes' \\
|
|
71
|
-
'--exclude[Exclude these processes]:processes' \\
|
|
72
|
-
'--kill-others[Kill all when any exits]' \\
|
|
73
|
-
'--no-restart[Disable auto-restart]' \\
|
|
74
|
-
'--no-watch[Disable file watching]' \\
|
|
75
|
-
'(-t --timestamps)'{-t,--timestamps}'[Add timestamps to output]' \\
|
|
76
|
-
'--log-dir[Log directory]:directory:_directories' \\
|
|
77
|
-
'--debug[Enable debug logging]' \\
|
|
78
|
-
'1:subcommand:->subcmd' \\
|
|
79
|
-
'*:command' \\
|
|
80
|
-
&& return
|
|
81
|
-
|
|
82
|
-
case "$state" in
|
|
83
|
-
subcmd)
|
|
84
|
-
_describe 'subcommand' subcmds
|
|
85
|
-
;;
|
|
86
|
-
esac
|
|
87
|
-
}
|
|
88
|
-
_numux`
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function fishCompletions(): string {
|
|
92
|
-
return `# numux fish completions
|
|
93
|
-
# Add to fish: numux completions fish | source
|
|
94
|
-
# Or save to: ~/.config/fish/completions/numux.fish
|
|
95
|
-
complete -c numux -f
|
|
96
|
-
|
|
97
|
-
# Subcommands
|
|
98
|
-
complete -c numux -n __fish_use_subcommand -a init -d 'Create a starter config file'
|
|
99
|
-
complete -c numux -n __fish_use_subcommand -a validate -d 'Validate config and show process graph'
|
|
100
|
-
complete -c numux -n __fish_use_subcommand -a exec -d 'Run a command in a process environment'
|
|
101
|
-
complete -c numux -n __fish_use_subcommand -a completions -d 'Generate shell completions'
|
|
102
|
-
|
|
103
|
-
# Completions subcommand
|
|
104
|
-
complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'
|
|
105
|
-
|
|
106
|
-
# Options
|
|
107
|
-
complete -c numux -s h -l help -d 'Show help'
|
|
108
|
-
complete -c numux -s v -l version -d 'Show version'
|
|
109
|
-
complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
|
|
110
|
-
complete -c numux -l config -rF -d 'Config file path'
|
|
111
|
-
complete -c numux -s n -l name -r -d 'Named process (name=command)'
|
|
112
|
-
complete -c numux -s p -l prefix -d 'Prefixed output mode'
|
|
113
|
-
complete -c numux -l only -r -d 'Only run these processes'
|
|
114
|
-
complete -c numux -l exclude -r -d 'Exclude these processes'
|
|
115
|
-
complete -c numux -l kill-others -d 'Kill all when any exits'
|
|
116
|
-
complete -c numux -l no-restart -d 'Disable auto-restart'
|
|
117
|
-
complete -c numux -l no-watch -d 'Disable file watching'
|
|
118
|
-
complete -c numux -s t -l timestamps -d 'Add timestamps to output'
|
|
119
|
-
complete -c numux -l log-dir -ra '(__fish_complete_directories)' -d 'Log directory'
|
|
120
|
-
complete -c numux -l debug -d 'Enable debug logging'`
|
|
121
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import type { NumuxConfig, NumuxProcessConfig } from '../types'
|
|
4
|
-
|
|
5
|
-
type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun'
|
|
6
|
-
|
|
7
|
-
const LOCKFILE_PM: [string, PackageManager][] = [
|
|
8
|
-
['bun.lockb', 'bun'],
|
|
9
|
-
['bun.lock', 'bun'],
|
|
10
|
-
['yarn.lock', 'yarn'],
|
|
11
|
-
['pnpm-lock.yaml', 'pnpm'],
|
|
12
|
-
['package-lock.json', 'npm']
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
export function detectPackageManager(pkgJson: Record<string, unknown>, cwd: string): PackageManager {
|
|
16
|
-
const field = pkgJson.packageManager
|
|
17
|
-
if (typeof field === 'string') {
|
|
18
|
-
const name = field.split('@')[0] as PackageManager
|
|
19
|
-
if (['npm', 'yarn', 'pnpm', 'bun'].includes(name)) return name
|
|
20
|
-
}
|
|
21
|
-
for (const [file, pm] of LOCKFILE_PM) {
|
|
22
|
-
if (existsSync(resolve(cwd, file))) return pm
|
|
23
|
-
}
|
|
24
|
-
return 'npm'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function expandScriptPatterns(config: NumuxConfig, cwd?: string): NumuxConfig {
|
|
28
|
-
const entries = Object.entries(config.processes)
|
|
29
|
-
const hasWildcard = entries.some(([name]) => name.startsWith('npm:'))
|
|
30
|
-
if (!hasWildcard) return config
|
|
31
|
-
|
|
32
|
-
const dir = config.cwd ?? cwd ?? process.cwd()
|
|
33
|
-
const pkgPath = resolve(dir, 'package.json')
|
|
34
|
-
|
|
35
|
-
if (!existsSync(pkgPath)) {
|
|
36
|
-
throw new Error(`npm: patterns require a package.json (looked in ${dir})`)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>
|
|
40
|
-
const scripts = pkgJson.scripts as Record<string, string> | undefined
|
|
41
|
-
if (!scripts || typeof scripts !== 'object') {
|
|
42
|
-
throw new Error('package.json has no "scripts" field')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const scriptNames = Object.keys(scripts)
|
|
46
|
-
const pm = detectPackageManager(pkgJson, dir)
|
|
47
|
-
|
|
48
|
-
const expanded: Record<string, NumuxProcessConfig | string> = {}
|
|
49
|
-
|
|
50
|
-
for (const [name, value] of entries) {
|
|
51
|
-
if (!name.startsWith('npm:')) {
|
|
52
|
-
expanded[name] = value as NumuxProcessConfig | string
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const pattern = name.slice(4) // strip "npm:"
|
|
57
|
-
const template = (value ?? {}) as Partial<NumuxProcessConfig>
|
|
58
|
-
|
|
59
|
-
if (template.command) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
`"${name}": wildcard processes cannot have a "command" field (commands come from package.json scripts)`
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const glob = new Bun.Glob(pattern)
|
|
66
|
-
const matches = scriptNames.filter(s => glob.match(s))
|
|
67
|
-
|
|
68
|
-
if (matches.length === 0) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`"${name}": no scripts matched pattern "${pattern}". Available scripts: ${scriptNames.join(', ')}`
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const colors = Array.isArray(template.color) ? template.color : undefined
|
|
75
|
-
const singleColor = typeof template.color === 'string' ? template.color : undefined
|
|
76
|
-
|
|
77
|
-
for (let i = 0; i < matches.length; i++) {
|
|
78
|
-
const scriptName = matches[i]
|
|
79
|
-
|
|
80
|
-
if (expanded[scriptName]) {
|
|
81
|
-
throw new Error(`"${name}": expanded script "${scriptName}" collides with an existing process name`)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const color = colors ? colors[i % colors.length] : singleColor
|
|
85
|
-
|
|
86
|
-
const { color: _color, ...rest } = template
|
|
87
|
-
expanded[scriptName] = {
|
|
88
|
-
...rest,
|
|
89
|
-
command: `${pm} run ${scriptName}`,
|
|
90
|
-
...(color ? { color } : {})
|
|
91
|
-
} as NumuxProcessConfig
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { ...config, processes: expanded }
|
|
96
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Environment variable interpolation for config values.
|
|
3
|
-
* Supports ${VAR}, ${VAR:-default}, and ${VAR:?error} syntax.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Matches ${VAR}, ${VAR:-default}, ${VAR:?error message}
|
|
7
|
-
const VAR_RE = /\$\{([^}:]+)(?::([-?])([^}]*))?\}/g
|
|
8
|
-
|
|
9
|
-
/** Recursively interpolate environment variables in all string values of a config object */
|
|
10
|
-
export function interpolateConfig<T>(config: T): T {
|
|
11
|
-
return interpolateValue(config) as T
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function interpolateValue(value: unknown): unknown {
|
|
15
|
-
if (typeof value === 'string') {
|
|
16
|
-
return interpolateString(value)
|
|
17
|
-
}
|
|
18
|
-
if (Array.isArray(value)) {
|
|
19
|
-
return value.map(interpolateValue)
|
|
20
|
-
}
|
|
21
|
-
if (value && typeof value === 'object') {
|
|
22
|
-
const result: Record<string, unknown> = {}
|
|
23
|
-
for (const [k, v] of Object.entries(value)) {
|
|
24
|
-
result[k] = interpolateValue(v)
|
|
25
|
-
}
|
|
26
|
-
return result
|
|
27
|
-
}
|
|
28
|
-
return value
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function interpolateString(str: string): string {
|
|
32
|
-
return str.replace(VAR_RE, (_match, name: string, operator?: string, operand?: string) => {
|
|
33
|
-
const value = process.env[name]
|
|
34
|
-
|
|
35
|
-
if (value !== undefined && value !== '') {
|
|
36
|
-
return value
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (operator === '-') {
|
|
40
|
-
return operand ?? ''
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (operator === '?') {
|
|
44
|
-
throw new Error(operand || `Required variable ${name} is not set`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Unset with no operator → empty string
|
|
48
|
-
return ''
|
|
49
|
-
})
|
|
50
|
-
}
|
package/src/config/loader.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { extname, resolve } from 'node:path'
|
|
3
|
-
import { parse as parseYaml } from 'yaml'
|
|
4
|
-
import type { NumuxConfig } from '../types'
|
|
5
|
-
import { log } from '../utils/logger'
|
|
6
|
-
import { interpolateConfig } from './interpolate'
|
|
7
|
-
|
|
8
|
-
const CONFIG_FILES = [
|
|
9
|
-
'numux.config.ts',
|
|
10
|
-
'numux.config.js',
|
|
11
|
-
'numux.config.yaml',
|
|
12
|
-
'numux.config.yml',
|
|
13
|
-
'numux.config.json'
|
|
14
|
-
] as const
|
|
15
|
-
|
|
16
|
-
export async function loadConfig(configPath?: string, cwd?: string): Promise<NumuxConfig> {
|
|
17
|
-
if (configPath) {
|
|
18
|
-
return loadExplicitConfig(configPath)
|
|
19
|
-
}
|
|
20
|
-
return autoDetectConfig(cwd ?? process.cwd())
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function loadFile(path: string): Promise<NumuxConfig> {
|
|
24
|
-
const ext = extname(path)
|
|
25
|
-
let config: NumuxConfig
|
|
26
|
-
if (ext === '.yaml' || ext === '.yml') {
|
|
27
|
-
const content = readFileSync(path, 'utf-8')
|
|
28
|
-
try {
|
|
29
|
-
config = parseYaml(content) as NumuxConfig
|
|
30
|
-
} catch (err) {
|
|
31
|
-
throw new Error(`Failed to parse ${path}: ${err instanceof Error ? err.message : err}`, { cause: err })
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
try {
|
|
35
|
-
const mod = await import(path)
|
|
36
|
-
config = mod.default ?? mod
|
|
37
|
-
} catch (err) {
|
|
38
|
-
throw new Error(`Failed to load ${path}: ${err instanceof Error ? err.message : err}`, { cause: err })
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return interpolateConfig(config)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function loadExplicitConfig(configPath: string): Promise<NumuxConfig> {
|
|
45
|
-
const path = resolve(configPath)
|
|
46
|
-
if (!existsSync(path)) {
|
|
47
|
-
throw new Error(`Config file not found: ${path}`)
|
|
48
|
-
}
|
|
49
|
-
log(`Loading explicit config: ${path}`)
|
|
50
|
-
return loadFile(path)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function autoDetectConfig(cwd: string): Promise<NumuxConfig> {
|
|
54
|
-
for (const file of CONFIG_FILES) {
|
|
55
|
-
const path = resolve(cwd, file)
|
|
56
|
-
if (existsSync(path)) {
|
|
57
|
-
log(`Found config file: ${path}`)
|
|
58
|
-
return loadFile(path)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Try package.json "numux" key
|
|
63
|
-
const pkgPath = resolve(cwd, 'package.json')
|
|
64
|
-
if (existsSync(pkgPath)) {
|
|
65
|
-
const pkg = await import(pkgPath)
|
|
66
|
-
const config = (pkg.default ?? pkg).numux
|
|
67
|
-
if (config) {
|
|
68
|
-
log('Found config in package.json "numux" key')
|
|
69
|
-
return interpolateConfig(config as NumuxConfig)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
throw new Error(
|
|
74
|
-
`No numux config found. Create one of: ${CONFIG_FILES.join(', ')} or add a "numux" key to package.json`
|
|
75
|
-
)
|
|
76
|
-
}
|