diff --git a/src/cli.test.ts b/src/cli.test.ts index 444ea5f..bf981e4 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -338,6 +338,11 @@ describe('buildConfigFromArgs', () => { expect(config.processes['format:check']).toEqual({ command: 'yarn format:check' }) }) + test('npm run script:name derives process name from script', () => { + const config = buildConfigFromArgs(['npm run studio:dev'], []) + expect(config.processes['studio:dev']).toEqual({ command: 'npm run studio:dev' }) + }) + test('positional commands use first word for non-runner commands', () => { const config = buildConfigFromArgs(['echo hello', '/usr/bin/node server.js'], []) expect(config.processes.echo).toEqual({ command: 'echo hello' }) diff --git a/src/config/expand-scripts.test.ts b/src/config/expand-scripts.test.ts index ed2577a..a5ddb8d 100644 --- a/src/config/expand-scripts.test.ts +++ b/src/config/expand-scripts.test.ts @@ -366,8 +366,8 @@ describe('expandScriptPatterns', () => { }) const result = expandScriptPatterns({ processes: { 'lint:* --fix': {} } }, dir) expect(Object.keys(result.processes).sort()).toEqual(['js', 'ts']) - expect(proc(result, 'js').command).toBe('npm run lint:js --fix') - expect(proc(result, 'ts').command).toBe('npm run lint:ts --fix') + expect(proc(result, 'js').command).toBe('npm run lint:js -- --fix') + expect(proc(result, 'ts').command).toBe('npm run lint:ts -- --fix') }) test('npm: prefix with extra args', () => { @@ -375,8 +375,8 @@ describe('expandScriptPatterns', () => { 'package.json': pkgJson({ 'lint:js': 'eslint', 'lint:ts': 'tsc' }) }) const result = expandScriptPatterns({ processes: { 'npm:lint:* --fix': {} } }, dir) - expect(proc(result, 'js').command).toBe('npm run lint:js --fix') - expect(proc(result, 'ts').command).toBe('npm run lint:ts --fix') + expect(proc(result, 'js').command).toBe('npm run lint:js -- --fix') + expect(proc(result, 'ts').command).toBe('npm run lint:ts -- --fix') }) test('multiple extra args forwarded', () => { @@ -384,7 +384,7 @@ describe('expandScriptPatterns', () => { 'package.json': pkgJson({ 'lint:js': 'eslint' }) }) const result = expandScriptPatterns({ processes: { 'lint:* --fix --quiet': {} } }, dir) - expect(proc(result, 'js').command).toBe('npm run lint:js --fix --quiet') + expect(proc(result, 'js').command).toBe('npm run lint:js -- --fix --quiet') }) test('npm: exact script name with extra args', () => { @@ -392,7 +392,36 @@ describe('expandScriptPatterns', () => { 'package.json': pkgJson({ lint: 'eslint' }) }) const result = expandScriptPatterns({ processes: { 'npm:lint --fix': {} } }, dir) - expect(proc(result, 'lint').command).toBe('npm run lint --fix') + expect(proc(result, 'lint').command).toBe('npm run lint -- --fix') + }) + + test('npm:studio:dev shorthand expands to npm run studio:dev', () => { + const dir = setupDir('studio-dev', { + 'package.json': pkgJson({ 'studio:dev': 'prisma studio' }) + }) + const result = expandScriptPatterns({ processes: { 'npm:studio:dev': { color: '#5A67D8' } } }, dir) + expect(proc(result, 'studio:dev').command).toBe('npm run studio:dev') + expect(proc(result, 'studio:dev').color).toBe('#5A67D8') + }) + + test('command value npm:script shorthand expands to pm run script', () => { + const dir = setupDir('cmd-shorthand', { + 'package.json': pkgJson({ 'studio:dev': 'prisma studio' }) + }) + const result = expandScriptPatterns( + { processes: { prisma: { command: 'npm:studio:dev', color: '#5A67D8' } } }, + dir + ) + expect(proc(result, 'prisma').command).toBe('npm run studio:dev') + expect(proc(result, 'prisma').color).toBe('#5A67D8') + }) + + test('command value npm:script with extra args', () => { + const dir = setupDir('cmd-shorthand-args', { + 'package.json': pkgJson({ lint: 'eslint' }) + }) + const result = expandScriptPatterns({ processes: { lint: { command: 'npm:lint --fix' } } }, dir) + expect(proc(result, 'lint').command).toBe('npm run lint -- --fix') }) test('prefix glob strips common prefix from process names', () => { @@ -493,7 +522,7 @@ describe('expandScriptPatterns', () => { 'package.json': pkgJson({ 'lint:eslint': 'eslint .' }) }) const result = expandScriptPatterns({ processes: { 'lint:eslint --fix': {} } }, dir) - expect(proc(result, 'lint:eslint').command).toBe('npm run lint:eslint --fix') + expect(proc(result, 'lint:eslint').command).toBe('npm run lint:eslint -- --fix') }) test('exact colon name inherits template properties', () => { diff --git a/src/config/expand-scripts.ts b/src/config/expand-scripts.ts index b498acf..a16ba16 100644 --- a/src/config/expand-scripts.ts +++ b/src/config/expand-scripts.ts @@ -71,35 +71,52 @@ function splitPatternArgs(raw: string): { glob: string; extraArgs: string } { return { glob: raw.slice(0, i), extraArgs: raw.slice(i) } } +function expandScriptCommand(raw: string, pm: PackageManager): string { + const { glob: script, extraArgs } = splitPatternArgs(raw) + if (extraArgs) { + return `${pm} run ${script} --${extraArgs}` + } + return `${pm} run ${script}` +} + export function expandScriptPatterns(config: NumuxConfig, cwd?: string): NumuxConfig { const entries = Object.entries(config.processes) + const cmd = (v: unknown) => (typeof v === 'string' ? v : (v as { command?: string })?.command) const hasScriptRef = entries.some(([name, value]) => isScriptReference(name, value)) - if (!hasScriptRef) return config + const hasNpmCommand = entries.some(([, v]) => { + const c = cmd(v) + return typeof c === 'string' && c.startsWith('npm:') + }) + if (!(hasScriptRef || hasNpmCommand)) return config const dir = config.cwd ?? cwd ?? process.cwd() const pkgPath = resolve(dir, 'package.json') - - if (!existsSync(pkgPath)) { + if (!existsSync(pkgPath) && hasScriptRef) { throw new Error(`Wildcard patterns require a package.json (looked in ${dir})`) } - - const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8')) as Record + const pkgJson = existsSync(pkgPath) ? (JSON.parse(readFileSync(pkgPath, 'utf-8')) as Record) : {} const scripts = pkgJson.scripts as Record | undefined - if (!scripts || typeof scripts !== 'object') { - throw new Error('package.json has no "scripts" field') - } - - const scriptNames = Object.keys(scripts) + const scriptNames = scripts && typeof scripts === 'object' ? Object.keys(scripts) : [] const pm = detectPackageManager(pkgJson, dir) const expanded: Record = {} for (const [name, value] of entries) { if (!isScriptReference(name, value)) { - expanded[name] = value as NumuxProcessConfig | string + let proc = value as NumuxProcessConfig | string + const c = cmd(proc) + if (typeof c === 'string' && c.startsWith('npm:')) { + const expandedCmd = expandScriptCommand(c.slice(4), pm) + proc = typeof proc === 'string' ? expandedCmd : { ...proc, command: expandedCmd } + } + expanded[name] = proc continue } + if (!scripts || typeof scripts !== 'object') { + throw new Error('package.json has no "scripts" field') + } + const rawPattern = name.startsWith('npm:') ? name.slice(4) : name const { glob: globPattern, extraArgs } = splitPatternArgs(rawPattern) const template = (value ?? {}) as Partial @@ -143,7 +160,7 @@ export function expandScriptPatterns(config: NumuxConfig, cwd?: string): NumuxCo const { color: _color, ...rest } = template expanded[displayName] = { ...rest, - command: `${pm} run ${scriptName}${extraArgs}`, + command: expandScriptCommand(`${scriptName}${extraArgs}`, pm), ...(color ? { color } : {}) } as NumuxProcessConfig } diff --git a/src/ui/prefix.test.ts b/src/ui/prefix.test.ts index e7f6a02..83128be 100644 --- a/src/ui/prefix.test.ts +++ b/src/ui/prefix.test.ts @@ -27,10 +27,12 @@ async function runPrefix( extraArgs: string[] = [], envOverrides: Record = {} ): Promise<{ stdout: string; exitCode: number }> { + const env: Record = { ...process.env, FORCE_COLOR: '0', ...envOverrides } + if (!('NO_COLOR' in envOverrides)) delete env.NO_COLOR const proc = Bun.spawn(['bun', INDEX, '--prefix', ...extraArgs, '--config', configPath], { stdout: 'pipe', stderr: 'pipe', - env: { ...process.env, FORCE_COLOR: '0', ...envOverrides } + env }) const stdout = await new Response(proc.stdout).text() const exitCode = await proc.exited