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.
@@ -0,0 +1,4 @@
1
+ import type { NumuxConfig } from './types';
2
+ export type { NumuxConfig, NumuxProcessConfig } from './types';
3
+ /** Type-safe helper for numux.config.ts files. */
4
+ export declare function defineConfig(config: NumuxConfig): NumuxConfig;
package/dist/config.js ADDED
@@ -0,0 +1,7 @@
1
+ // src/config.ts
2
+ function defineConfig(config) {
3
+ return config;
4
+ }
5
+ export {
6
+ defineConfig
7
+ };
@@ -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.4.0",
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": "src/index.ts"
25
+ "numux": "dist/bin.js"
25
26
  },
26
27
  "exports": {
27
- ".": "./src/config.ts"
28
+ ".": {
29
+ "types": "./dist/config.d.ts",
30
+ "import": "./dist/config.js"
31
+ }
28
32
  },
29
33
  "scripts": {
30
- "dev": "bun run src/index.ts",
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
- "src/**/*.ts",
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
- }
@@ -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
- }
@@ -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
- }