Skip to content

Commit 20f58ed

Browse files
authored
feat: add onBeforeCommand to run tasks before agent detection (#318)
1 parent 2ee2820 commit 20f58ed

2 files changed

Lines changed: 49 additions & 11 deletions

File tree

src/runner.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,25 @@ export interface ExtendedResolvedCommand extends ResolvedCommand {
2727
cwd?: string
2828
}
2929

30+
interface RunOptions {
31+
/**
32+
* Called before agent detection and command execution.
33+
*
34+
* Useful for performing concrete, agent-agnostic operations.
35+
*/
36+
onBeforeCommand?: (args: string[], ctx: Pick<RunnerContext, 'cwd' | 'programmatic'> & {
37+
/**
38+
* Skips subsequent command execution.
39+
*
40+
* This is useful for operations such as generating shell-completion scripts.
41+
*/
42+
exit: () => void
43+
}) => void | Promise<void>
44+
}
45+
3046
export type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Promise<ExtendedResolvedCommand | undefined> | ExtendedResolvedCommand | undefined
3147

32-
export async function runCli(fn: Runner, options: DetectOptions & { args?: string[] } = {}) {
48+
export async function runCli(fn: Runner, options: DetectOptions & RunOptions & { args?: string[] } = {}) {
3349
options = {
3450
...getEnvironmentOptions(),
3551
...options,
@@ -82,7 +98,7 @@ export async function getCliCommand(
8298
})
8399
}
84100

85-
export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
101+
export async function run(fn: Runner, args: string[], options: DetectOptions & RunOptions = {}) {
86102
const { programmatic, detectVolta = true } = options
87103

88104
const debug = args.includes(DEBUG_SIGN)
@@ -146,6 +162,17 @@ export async function run(fn: Runner, args: string[], options: DetectOptions = {
146162
return
147163
}
148164

165+
if (options.onBeforeCommand) {
166+
let shouldExit = false
167+
await options.onBeforeCommand(args, {
168+
cwd,
169+
programmatic,
170+
exit: () => { shouldExit = true },
171+
})
172+
if (shouldExit)
173+
return
174+
}
175+
149176
const command = await getCliCommand(fn, args, options, cwd)
150177

151178
if (!command)

test/runner/runCli.test.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,20 @@ import { runCli } from '../../src'
55
// Mock detect to see what options are passed to it
66
const mocks = vi.hoisted(() => ({
77
detectSpy: vi.fn(() => Promise.resolve('npm')),
8+
baseRunFnSpy: vi.fn<Runner>(() => Promise.resolve(undefined)),
89
}))
910
vi.mock('../../src/detect', () => ({
1011
detect: mocks.detectSpy,
1112
}))
1213

13-
const baseRunFn: Runner = async () => {
14-
return undefined
15-
}
16-
1714
describe('runCli', () => {
1815
afterEach(() => {
1916
vi.clearAllMocks()
2017
vi.unstubAllEnvs()
2118
})
2219

2320
it('run without errors', async () => {
24-
const result = await runCli(baseRunFn, {})
21+
const result = await runCli(mocks.baseRunFnSpy, {})
2522
expect(result).toBe(undefined)
2623
})
2724

@@ -34,24 +31,38 @@ describe('runCli', () => {
3431
})
3532

3633
it('calls detect with the correct options', async () => {
37-
await runCli(baseRunFn)
34+
await runCli(mocks.baseRunFnSpy)
3835
expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: false, cwd: expect.any(String) })
3936
})
4037

4138
it('detects environment options', async () => {
4239
vi.stubEnv('NI_AUTO_INSTALL', 'true')
43-
await runCli(baseRunFn)
40+
await runCli(mocks.baseRunFnSpy)
4441
expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: true, cwd: expect.any(String) })
4542
})
4643

4744
it('accept options as input', async () => {
48-
await runCli(baseRunFn, { autoInstall: true, programmatic: true })
45+
await runCli(mocks.baseRunFnSpy, { autoInstall: true, programmatic: true })
4946
expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: true, programmatic: true, cwd: expect.any(String) })
5047
})
5148

5249
it('merges inputs and environment prioritizing inputs', async () => {
5350
vi.stubEnv('NI_AUTO_INSTALL', 'true')
54-
await runCli(baseRunFn, { autoInstall: false, programmatic: true })
51+
await runCli(mocks.baseRunFnSpy, { autoInstall: false, programmatic: true })
5552
expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: false, programmatic: true, cwd: expect.any(String) })
5653
})
54+
55+
describe('onBeforeCommand', () => {
56+
it('skips running the command when exit() is called', async () => {
57+
await runCli(mocks.baseRunFnSpy, { onBeforeCommand: (_args, ctx) => ctx.exit() })
58+
expect(mocks.baseRunFnSpy).not.toHaveBeenCalled()
59+
// https://github.com/antfu-collective/ni/issues/308
60+
expect(mocks.detectSpy).not.toHaveBeenCalled()
61+
})
62+
63+
it('continues to run the command when exit() is not called', async () => {
64+
await runCli(mocks.baseRunFnSpy, { onBeforeCommand: () => Promise.resolve() })
65+
expect(mocks.baseRunFnSpy).toHaveBeenCalledOnce()
66+
})
67+
})
5768
})

0 commit comments

Comments
 (0)