Skip to content

Commit 5a18dde

Browse files
hyldmoclaude
andauthored
feat(config): auto-resolve command from package.json scripts (#88)
* feat(config): auto-resolve command from package.json scripts When a process has no command and its name matches a package.json script, the command is automatically set to `<pm> run <name>`. This eliminates the redundancy of `'lint': { command: 'npm:lint' }` — now just `'lint': {}` works. Co-Authored-By: Claude Opus 4.6 <[email protected]> * docs: document auto-resolve of commands from package.json scripts Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat(config): allow `true` as process shorthand for script auto-resolve `lint: true` is now equivalent to `lint: {}` — both auto-resolve to `<pm> run lint` when the name matches a package.json script. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent d208654 commit 5a18dde

File tree

5 files changed

+62
-10
lines changed

5 files changed

+62
-10
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export default defineConfig({
5959

6060
The `defineConfig()` helper is optional — it provides type checking for your config.
6161

62-
Processes can be a string (shorthand for `{ command: "..." }`) or a full config object.
62+
Processes can be a string (shorthand for `{ command: "..." }`), `true` or `{}` (auto-resolves to a matching `package.json` script), or a full config object.
6363

6464
Then run:
6565

@@ -159,6 +159,18 @@ export default defineConfig({
159159

160160
Template properties (color, env, dependsOn, etc.) are inherited by all matched processes. Colors given as an array are distributed round-robin.
161161

162+
When a process has no command and its name matches a `package.json` script, the command is auto-resolved:
163+
164+
```ts
165+
export default defineConfig({
166+
processes: {
167+
lint: true, // → bun run lint
168+
typecheck: { dependsOn: ['db'] }, // → bun run typecheck (with dependency)
169+
db: 'docker compose up postgres', // explicit command, not resolved
170+
},
171+
})
172+
```
173+
162174
### Options
163175

164176
| Flag | Description |

src/config/expand-scripts.test.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -353,11 +353,38 @@ describe('expandScriptPatterns', () => {
353353
)
354354
})
355355

356-
test('non-glob names without command are not expanded', () => {
357-
// Names like "web" that don't contain glob chars should NOT be treated as patterns
358-
const config: NumuxConfig = { processes: { web: { env: { FOO: 'bar' } } } }
359-
// This should passthrough, not try to expand
360-
expect(expandScriptPatterns(config)).toBe(config)
356+
test('commandless entry auto-resolves when name matches a script', () => {
357+
const dir = setupDir('auto-resolve', {
358+
'package.json': pkgJson({ lint: 'eslint .', dev: 'next dev' })
359+
})
360+
const result = expandScriptPatterns({ processes: { lint: {}, dev: { env: { PORT: '3000' } } } }, dir)
361+
expect(proc(result, 'lint').command).toBe('npm run lint')
362+
expect(proc(result, 'dev').command).toBe('npm run dev')
363+
expect(proc(result, 'dev').env).toEqual({ PORT: '3000' })
364+
})
365+
366+
test('true value auto-resolves when name matches a script', () => {
367+
const dir = setupDir('auto-resolve-true', {
368+
'package.json': pkgJson({ lint: 'eslint .', dev: 'next dev' })
369+
})
370+
const result = expandScriptPatterns({ processes: { lint: true as any, dev: true as any } }, dir)
371+
expect(result.processes.lint).toBe('npm run lint')
372+
expect(result.processes.dev).toBe('npm run dev')
373+
})
374+
375+
test('commandless entry not matching a script passes through unchanged', () => {
376+
const dir = setupDir('auto-resolve-miss', {
377+
'package.json': pkgJson({ dev: 'next dev' })
378+
})
379+
const result = expandScriptPatterns({ processes: { web: { env: { FOO: 'bar' } } } }, dir)
380+
// Not a script match — passes through as-is (validator will catch missing command)
381+
expect(proc(result, 'web').command).toBeUndefined()
382+
})
383+
384+
test('no package.json — commandless entries pass through', () => {
385+
const dir = setupDir('auto-resolve-no-pkg', {})
386+
const result = expandScriptPatterns({ processes: { web: {} } }, dir)
387+
expect(proc(result, 'web').command).toBeUndefined()
361388
})
362389

363390
test('extra args are forwarded to expanded commands', () => {

src/config/expand-scripts.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ export function expandScriptPatterns(config: NumuxConfig, cwd?: string): NumuxCo
8787
const c = cmd(v)
8888
return typeof c === 'string' && c.startsWith('npm:')
8989
})
90-
if (!(hasScriptRef || hasNpmCommand)) return config
90+
const hasCommandlessEntry = entries.some(([, v]) => {
91+
if (v == null || v === true) return true
92+
if (typeof v === 'object' && !('command' in v)) return true
93+
return false
94+
})
95+
if (!(hasScriptRef || hasNpmCommand || hasCommandlessEntry)) return config
9196

9297
const dir = config.cwd ?? cwd ?? process.cwd()
9398
const pkgPath = resolve(dir, 'package.json')
@@ -103,11 +108,19 @@ export function expandScriptPatterns(config: NumuxConfig, cwd?: string): NumuxCo
103108

104109
for (const [name, value] of entries) {
105110
if (!isScriptReference(name, value)) {
111+
// true/null shorthand: resolve from scripts or pass through for validator to catch
112+
if (value === true || value == null) {
113+
expanded[name] = scriptNames.includes(name) ? expandScriptCommand(name, pm) : (value as any)
114+
continue
115+
}
106116
let proc = value as NumuxProcessConfig | string
107117
const c = cmd(proc)
108118
if (typeof c === 'string' && c.startsWith('npm:')) {
109119
const expandedCmd = expandScriptCommand(c.slice(4), pm)
110120
proc = typeof proc === 'string' ? expandedCmd : { ...proc, command: expandedCmd }
121+
} else if (!c && scriptNames.includes(name)) {
122+
// Auto-resolve: process name matches a package.json script
123+
proc = { ...(proc as NumuxProcessConfig), command: expandScriptCommand(name, pm) }
111124
}
112125
expanded[name] = proc
113126
continue

src/config/workspaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ export function expandWorkspaces(config: NumuxConfig): NumuxConfig {
8080
let discoveredWorkspaces: WorkspaceInfo[] | null = null
8181

8282
for (const [name, entry] of Object.entries(config.processes)) {
83-
// Pass through string shorthand and non-workspace entries
84-
if (typeof entry === 'string' || !entry.workspaces) {
83+
// Pass through string shorthand, true shorthand, and non-workspace entries
84+
if (typeof entry === 'string' || entry === true || !entry.workspaces) {
8585
newProcesses[name] = entry
8686
continue
8787
}

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export interface NumuxConfig<K extends string = string> {
120120
noWatch?: boolean
121121
/** Directory to write per-process log files */
122122
logDir?: string
123-
processes: Record<K, NumuxProcessConfig<K> | NumuxScriptPattern<K> | string>
123+
processes: Record<K, NumuxProcessConfig<K> | NumuxScriptPattern<K> | string | true>
124124
}
125125

126126
export type SortOrder = 'config' | 'alphabetical' | 'topological'

0 commit comments

Comments
 (0)