@@ -19,6 +19,7 @@ import { MANUAL_COMMAND_ALLOWLIST, type ManualCommandKey } from './lib/manual-co
1919import { validateOperationResponseData } from './lib/operation-args' ;
2020import { runInstall } from './commands/install' ;
2121import { runUninstall } from './commands/uninstall' ;
22+ import { withStateDirOverride } from './lib/context' ;
2223import {
2324 CLI_COMMAND_SPECS ,
2425 CLI_COMMAND_KEYS ,
@@ -60,6 +61,7 @@ export type InvokeCommandOptions = {
6061 ioOverrides ?: Partial < CliIO > ;
6162 executionMode ?: ExecutionMode ;
6263 collabSessionPool ? : CommandContext [ 'collabSessionPool' ] ;
64+ stateDir ? : string ;
6365} ;
6466
6567const MANUAL_COMMANDS = {
@@ -262,13 +264,16 @@ async function executeParsedInvocation(
262264export async function invokeCommand ( argv : string [ ] , options : InvokeCommandOptions = { } ) : Promise < InvokeCommandResult > {
263265 const io = mergeIo ( options . ioOverrides ) ;
264266 const startedAt = io . now ( ) ;
265- const parsed = parseInvocation ( argv ) ;
266- const output = await executeParsedInvocation (
267- parsed ,
268- io ,
269- options . executionMode ?? 'oneshot' ,
270- options . collabSessionPool ,
271- ) ;
267+ const { parsed, output } = await withStateDirOverride ( options . stateDir , async ( ) => {
268+ const parsedInvocation = parseInvocation ( argv ) ;
269+ const commandOutput = await executeParsedInvocation (
270+ parsedInvocation ,
271+ io ,
272+ options . executionMode ?? 'oneshot' ,
273+ options . collabSessionPool ,
274+ ) ;
275+ return { parsed : parsedInvocation , output : commandOutput } ;
276+ } ) ;
272277
273278 return {
274279 globals : parsed . globals ,
@@ -289,60 +294,67 @@ async function runHostCommand(tokens: string[], io: CliIO): Promise<number> {
289294 *
290295 * @param argv - Raw process arguments (after stripping the binary path)
291296 * @param ioOverrides - Optional overrides for stdout, stderr, stdin, and clock
297+ * @param options - Optional runtime overrides such as test-scoped state directory
292298 * @returns Process exit code (0 on success, non-zero on error)
293299 */
294- export async function run ( argv : string [ ] , ioOverrides ?: Partial < CliIO > ) : Promise < number > {
300+ export async function run (
301+ argv : string [ ] ,
302+ ioOverrides ?: Partial < CliIO > ,
303+ options : Pick < InvokeCommandOptions , 'stateDir '> = { } ,
304+ ) : Promise < number > {
295305 const io = mergeIo ( ioOverrides ) ;
296306 const startedAt = io . now ( ) ;
297307 let outputMode : OutputMode = 'json' ;
298308
299- try {
300- const parsed = parseInvocation ( argv ) ;
301- outputMode = parsed . globals . output ;
309+ return withStateDirOverride ( options . stateDir , async ( ) => {
310+ try {
311+ const parsed = parseInvocation ( argv ) ;
312+ outputMode = parsed . globals . output ;
302313
303- if ( parsed . rest [ 0 ] === 'host' ) {
304- const hostTokens = parsed . rest . slice ( 1 ) ;
305- if ( parsed . globals . help ) hostTokens . push ( '--help' ) ;
306- return await runHostCommand ( hostTokens , io ) ;
307- }
314+ if ( parsed . rest [ 0 ] === 'host' ) {
315+ const hostTokens = parsed . rest . slice ( 1 ) ;
316+ if ( parsed . globals . help ) hostTokens . push ( '--help' ) ;
317+ return await runHostCommand ( hostTokens , io ) ;
318+ }
308319
309- if ( parsed . rest [ 0 ] === 'install' && ! parsed . globals . help ) {
310- return await runInstall ( parsed . rest . slice ( 1 ) , io ) ;
311- }
320+ if ( parsed . rest [ 0 ] === 'install' && ! parsed . globals . help ) {
321+ return await runInstall ( parsed . rest . slice ( 1 ) , io ) ;
322+ }
312323
313- if ( parsed . rest [ 0 ] === 'uninstall' && ! parsed . globals . help ) {
314- return await runUninstall ( parsed . rest . slice ( 1 ) , io ) ;
315- }
324+ if ( parsed . rest [ 0 ] === 'uninstall' && ! parsed . globals . help ) {
325+ return await runUninstall ( parsed . rest . slice ( 1 ) , io ) ;
326+ }
316327
317- if ( parsed . rest [ 0 ] === 'call' && outputMode !== 'json' ) {
318- throw new CliError ( 'INVALID_ARGUMENT' , 'call: only --output json is supported.' ) ;
319- }
328+ if ( parsed . rest [ 0 ] === 'call' && outputMode !== 'json' ) {
329+ throw new CliError ( 'INVALID_ARGUMENT' , 'call: only --output json is supported.' ) ;
330+ }
320331
321- if ( ! parsed . globals . help ) {
322- const legacyCompat = await tryRunLegacyCompatCommand ( argv , parsed . rest , io ) ;
323- if ( legacyCompat . handled ) {
324- return legacyCompat . exitCode ;
332+ if ( ! parsed . globals . help ) {
333+ const legacyCompat = await tryRunLegacyCompatCommand ( argv , parsed . rest , io ) ;
334+ if ( legacyCompat . handled ) {
335+ return legacyCompat . exitCode ;
336+ }
337+ }
338+
339+ const output = await executeParsedInvocation ( parsed , io , 'oneshot' ) ;
340+ if ( output . helpText ) {
341+ io . stdout ( output . helpText ) ;
342+ return 0 ;
343+ }
344+ if ( ! output . execution ) {
345+ throw new CliError ( 'COMMAND_FAILED' , 'Command produced no execution result and no help text.' ) ;
325346 }
326- }
327347
328- const output = await executeParsedInvocation ( parsed , io , 'oneshot' ) ;
329- if ( output . helpText ) {
330- io . stdout ( output . helpText ) ;
348+ const elapsedMs = io . now ( ) - startedAt ;
349+ writeSuccess ( io , outputMode , output . execution , elapsedMs ) ;
331350 return 0 ;
351+ } catch ( error ) {
352+ const cliError = toCliError ( error ) ;
353+ const elapsedMs = io . now ( ) - startedAt ;
354+ writeFailure ( io , outputMode , cliError , elapsedMs ) ;
355+ return cliError . exitCode ;
332356 }
333- if ( ! output . execution ) {
334- throw new CliError ( 'COMMAND_FAILED' , 'Command produced no execution result and no help text.' ) ;
335- }
336-
337- const elapsedMs = io . now ( ) - startedAt ;
338- writeSuccess ( io , outputMode , output . execution , elapsedMs ) ;
339- return 0 ;
340- } catch ( error ) {
341- const cliError = toCliError ( error ) ;
342- const elapsedMs = io . now ( ) - startedAt ;
343- writeFailure ( io , outputMode , cliError , elapsedMs ) ;
344- return cliError . exitCode ;
345- }
357+ } ) ;
346358}
347359
348360if ( import . meta. main ) {
0 commit comments