2026-10-17 11:29:03 +08:00
import { withCommandCancellation } from "./commandCancellation.js" ;
2026-07-18 04:11:29 +00:00
import { z } from "incur" ;
2026-10-11 13:07:54 +08:00
import type { DoctorScope } from "../application/Doctor.js" ;
import { DOCTOR_PROVIDER_IDS } from "../application/DoctorProviderIds.js" ;
2026-07-22 20:38:26 +08:00
import {
2026-10-06 00:50:38 +08:00
isSetupFailure ,
2026-07-22 20:38:26 +08:00
type SetupOptions ,
2026-10-06 05:00:32 +08:00
} from "../application/SetupTypes.js" ;
2026-10-11 13:07:54 +08:00
import { isUninstallFailure } from "../application/UninstallFailure.js" ;
2026-10-09 20:02:52 +08:00
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js" ;
2026-10-26 19:50:12 +08:00
import {
mcpToolFamiliesSchema ,
toolFamiliesConstraint ,
} from "../config/mcpToolFamilies.js" ;
2026-10-11 13:07:54 +08:00
import { isUpdateFailure } from "../application/UpdateFailure.js" ;
2026-07-18 05:18:05 +00:00
import {
confirmInteractiveSetup ,
2026-10-13 03:22:53 +08:00
renderSetupDryRun ,
2026-07-18 05:18:05 +00:00
renderInteractiveSetupResult ,
renderSetupProgress ,
2026-10-06 05:04:25 +08:00
} from "./interactiveSetup.js" ;
2026-07-18 04:11:29 +00:00
import { PRODUCT_IDENTITY } from "../identity.js" ;
import { logCliCommand } from "../cliLogging.js" ;
import { CLI_COMMANDS } from "../cliCommandNames.js" ;
2026-10-09 22:45:18 +08:00
import type { Logger } from "pino" ;
2026-07-18 04:11:29 +00:00
import type { CliInstance } from "./types.js" ;
2026-07-22 20:38:26 +08:00
import { SUPPORTED_CLIENT_DEFINITIONS } from "../application/SupportedClients.js" ;
const supportedClientIds = SUPPORTED_CLIENT_DEFINITIONS . map (({ name }) => name );
2026-10-24 02:24:40 +08:00
const supportedClientSchema = z . enum ( supportedClientIds );
2026-10-11 00:03:21 +08:00
const skillClientSchema = z . union ([ z . literal ( "shared" ), supportedClientSchema ]);
2026-07-18 04:11:29 +00:00
2026-10-05 23:06:19 +08:00
/** Register setup, doctor, uninstall, and update CLI commands. */
2026-07-18 04:11:29 +00:00
export const registerSetupCommands = (
cli : CliInstance ,
logger : Logger ,
2026-10-09 20:02:52 +08:00
environment : Readonly < NodeJS.ProcessEnv >,
2026-07-18 04:11:29 +00:00
) : void => {
2026-10-09 20:02:52 +08:00
registerSetupCommand ( cli , logger , environment );
registerDoctorCommand ( cli , logger , environment );
registerMaintenanceCommands ( cli , logger , environment );
2026-07-22 20:38:26 +08:00
};
2026-10-09 20:02:52 +08:00
const registerSetupCommand = (
cli : CliInstance ,
logger : Logger ,
environment : Readonly < NodeJS.ProcessEnv >,
) : void => {
2026-07-18 04:11:29 +00:00
cli . command ( CLI_COMMANDS . setup , {
2026-10-05 23:06:19 +08:00
description : "Configure agent integrations and optional analysis providers" ,
2026-07-18 05:18:05 +00:00
outputPolicy : "agent-only" ,
2026-07-18 04:11:29 +00:00
options : z.object ({
yes : z
. boolean ()
. default ( false )
. describe ( "Approve user-owned setup actions without prompting" ),
installHopper : z
. boolean ()
. default ( false )
. describe ( "Also approve Hopper installation with --yes" ),
2026-07-18 05:18:05 +00:00
client : z
2026-07-22 20:38:26 +08:00
. array ( supportedClientSchema )
2026-07-18 05:18:05 +00:00
. default ([])
. describe ( "Agent integration to configure; repeat for multiple agents" ),
2026-10-11 00:03:21 +08:00
skillClient : z
. array ( skillClientSchema )
. default ([])
. describe ( "Existing client skill destination to update; repeatable" ),
2026-10-26 19:50:12 +08:00
toolFamilies : z
. string ()
. optional ()
. refine (
( value ) =>
value === undefined ||
mcpToolFamiliesSchema . safeParse ( value ). success ,
{ message : toolFamiliesConstraint },
)
. describe (
"Comma-separated tool families this registration advertises; omit to preserve the saved selection, or advertise every tool if none is saved. To restore every tool, remove REA_MCP_TOOL_FAMILIES from the client's registration and restart the client." ,
),
2026-07-18 05:18:05 +00:00
allDetected : z
. boolean ()
. default ( false )
. describe ( "Configure every detected supported agent" ),
skill : z
. boolean ()
. optional ()
2026-07-20 17:11:06 +08:00
. describe (
"Override the bundled skill included with agent integrations" ,
),
2026-07-18 05:18:05 +00:00
dryRun : z
. boolean ()
. default ( false )
. describe ( "Print the resolved plan without applying it" ),
accessible : z
. boolean ()
. default ( false )
. describe ( "Use sequential accessible setup prompts" ),
2026-07-18 04:11:29 +00:00
}),
2026-07-18 05:18:05 +00:00
alias : { yes : "y" },
2026-07-22 20:38:26 +08:00
run : ({ options , formatExplicit }) =>
2026-10-06 00:50:38 +08:00
logCliCommand (
logger ,
CLI_COMMANDS . setup ,
2026-10-09 20:02:52 +08:00
() => runSetupCommand ({ options , formatExplicit }, environment ),
2026-10-06 00:50:38 +08:00
isSetupFailure ,
2026-07-18 04:11:29 +00:00
),
});
2026-07-22 20:38:26 +08:00
};
2026-10-09 20:02:52 +08:00
const registerDoctorCommand = (
cli : CliInstance ,
logger : Logger ,
environment : Readonly < NodeJS.ProcessEnv >,
) : void => {
2026-07-18 04:11:29 +00:00
cli . command ( CLI_COMMANDS . doctor , {
description : "Check whether REA is ready" ,
options : z.object ({
target : z.string (). optional (). describe ( "Optional app path to check" ),
2026-07-22 20:38:26 +08:00
client : z
. array ( supportedClientSchema )
. default ([])
. describe ( "Agent registration whose readiness is required; repeatable" ),
provider : z
. array ( z . enum ( DOCTOR_PROVIDER_IDS ))
. default ([])
. describe ( "Deep-analysis provider whose readiness is required" ),
skill : z
. boolean ()
. optional ()
. describe ( "Require the installed REA skill identity to be aligned" ),
2026-07-18 04:11:29 +00:00
}),
run : ({ options }) =>
2026-10-18 02:07:41 +08:00
withCommandCancellation (( signal ) =>
logCliCommand ( logger , "doctor" , async () => {
2026-10-17 11:29:03 +08:00
const [{ runDoctorOwned }, { createSystemDoctorHost }] =
await Promise . all ([
import ( "../application/Doctor.js" ),
import ( "../doctorRuntime.js" ),
]);
return runDoctorOwned (
options . target ,
createSystemDoctorHost ( environment , signal ),
doctorScope ( options ),
);
}),
),
2026-07-18 04:11:29 +00:00
});
2026-07-22 20:38:26 +08:00
};
const registerMaintenanceCommands = (
cli : CliInstance ,
logger : Logger ,
2026-10-09 20:02:52 +08:00
environment : Readonly < NodeJS.ProcessEnv >,
2026-07-22 20:38:26 +08:00
) : void => {
2026-07-18 04:11:29 +00:00
cli . command ( CLI_COMMANDS . uninstall , {
description : "Remove REA-owned agent configuration and skill files" ,
options : z.object ({
purgeData : z
. boolean ()
. default ( false )
. describe ( "Also remove REA caches and state" ),
}),
run : ({ options }) =>
2026-10-06 00:50:38 +08:00
logCliCommand (
logger ,
"uninstall" ,
2026-10-11 13:07:54 +08:00
async () => {
const { runUninstall , systemUninstallHost } =
await import ( "../application/Uninstall.js" );
return runUninstall (
2026-10-09 20:02:52 +08:00
options . purgeData ,
systemUninstallHost (
homeDirectoryFromEnvironment ( environment , process . platform ),
undefined ,
environment ,
),
2026-10-11 13:07:54 +08:00
);
},
2026-10-06 00:50:38 +08:00
isUninstallFailure ,
),
2026-07-18 04:11:29 +00:00
});
2026-10-05 23:06:19 +08:00
cli . command ( CLI_COMMANDS . update , {
description :
"Update this REA installation and verify the installed release" ,
2026-07-18 04:11:29 +00:00
run : ({ formatExplicit }) =>
2026-10-06 00:50:38 +08:00
logCliCommand (
logger ,
CLI_COMMANDS . update ,
2026-10-11 13:07:54 +08:00
async () => {
const [{ runUpdate }, { systemUpdateHost }] = await Promise . all ([
import ( "../application/Update.js" ),
import ( "../application/UpdateRuntime.js" ),
]);
return runUpdate (
2026-10-06 00:50:38 +08:00
PRODUCT_IDENTITY . packageVersion ,
2026-10-09 20:02:52 +08:00
systemUpdateHost ( undefined , undefined , environment ),
2026-10-06 00:50:38 +08:00
formatExplicit ? "structured" : "human" ,
2026-10-11 13:07:54 +08:00
);
},
2026-10-06 00:50:38 +08:00
isUpdateFailure ,
2026-07-18 04:11:29 +00:00
),
});
};
2026-07-18 05:18:05 +00:00
interface SetupCommandOptions {
readonly yes : boolean ;
readonly installHopper : boolean ;
readonly client : readonly string [];
2026-10-11 00:03:21 +08:00
readonly skillClient : readonly string [];
2026-10-26 19:50:12 +08:00
readonly toolFamilies? : string | undefined ;
2026-07-18 05:18:05 +00:00
readonly allDetected : boolean ;
readonly skill? : boolean | undefined ;
readonly dryRun : boolean ;
readonly accessible : boolean ;
}
2026-10-09 20:02:52 +08:00
const runSetupCommand = async (
input : {
readonly options : SetupCommandOptions ;
readonly formatExplicit : boolean ;
},
environment : Readonly < NodeJS.ProcessEnv >,
) => {
2026-07-18 05:18:05 +00:00
const { options } = input ;
2026-07-22 20:38:26 +08:00
const interactive = setupIsInteractive ( options , input . formatExplicit );
2026-07-18 05:18:05 +00:00
const hasExplicitScope = setupHasExplicitScope ( options );
2026-10-11 13:07:54 +08:00
const [{ runSetup }, { systemSetupHost }, { createSystemDoctorHost }] =
await Promise . all ([
import ( "../application/Setup.js" ),
import ( "../application/SetupHost.js" ),
import ( "../doctorRuntime.js" ),
]);
2026-07-18 05:18:05 +00:00
const result = await runSetup (
2026-07-22 20:38:26 +08:00
setupRunOptions ( input , interactive , hasExplicitScope ),
2026-10-09 20:02:52 +08:00
systemSetupHost ( createSystemDoctorHost ( environment ), environment ),
2026-07-18 05:18:05 +00:00
interactive
2026-10-05 23:06:19 +08:00
? ( actions , context ) =>
confirmInteractiveSetup ( actions , options . accessible , context )
2026-07-18 05:18:05 +00:00
: undefined ,
);
2026-10-09 20:02:52 +08:00
if ( interactive ) renderInteractiveSetupResult ( result , environment );
2026-10-14 16:09:29 +08:00
else if (
options . dryRun &&
! input . formatExplicit &&
hasInteractiveTerminal ()
) {
if ( result . status === "planned" ) renderSetupDryRun ( result . plannedActions );
else renderInteractiveSetupResult ( result , environment );
}
2026-09-29 07:08:46 +08:00
return result ;
2026-07-18 04:11:29 +00:00
};
2026-07-18 05:18:05 +00:00
2026-07-22 20:38:26 +08:00
const setupRunOptions = (
input : {
readonly options : SetupCommandOptions ;
readonly formatExplicit : boolean ;
},
interactive : boolean ,
hasExplicitScope : boolean ,
) : SetupOptions => {
const { options } = input ;
const agentIntegrationSelected =
2026-10-11 00:03:21 +08:00
options . allDetected ||
options . client . length > 0 ||
options . skillClient . length > 0 ;
const hasSelectedClients =
hasExplicitScope &&
( options . client . length > 0 || options . skillClient . length > 0 );
2026-07-22 20:38:26 +08:00
return {
approved : options.yes && ! options . dryRun ,
installHopper : options.installHopper ,
structured : input.formatExplicit || options . dryRun ,
2026-10-05 23:06:19 +08:00
dryRun : options.dryRun ,
allDetectedClients : options.allDetected ,
proposeHopper : interactive || options . installHopper ,
2026-07-22 20:38:26 +08:00
...( hasSelectedClients ? { clientIds : options.client } : {}),
2026-10-11 00:03:21 +08:00
...( options . skillClient . length > 0
? { skillClientIds : options.skillClient }
: {}),
2026-07-22 20:38:26 +08:00
...( hasExplicitScope
? { installSkill : options.skill ?? agentIntegrationSelected }
: {}),
2026-10-26 19:50:12 +08:00
...( options . toolFamilies === undefined
? {}
: { toolFamilies : options.toolFamilies }),
2026-07-22 20:38:26 +08:00
...( interactive ? { onProgress : renderSetupProgress } : {}),
2026-10-23 02:23:58 +08:00
...( hasSelectedClients && ! options . allDetected
2026-07-22 20:38:26 +08:00
? {
readinessScope : {
clients : options.client ,
2026-10-11 23:52:42 +08:00
...( options . skillClient . length > 0
? { skillClients : options.skillClient }
: {}),
2026-07-22 20:38:26 +08:00
providers : options.installHopper ? [ "hopper" ] : [],
skill : options.skill ?? agentIntegrationSelected ,
},
}
: {}),
};
};
const setupIsInteractive = (
options : SetupCommandOptions ,
formatExplicit : boolean ,
) : boolean =>
! options . yes &&
! options . dryRun &&
! formatExplicit &&
2026-10-13 03:22:53 +08:00
hasInteractiveTerminal ();
const hasInteractiveTerminal = () : boolean =>
2026-07-22 20:38:26 +08:00
process . stdin . isTTY === true &&
2026-10-05 23:06:19 +08:00
process . stdout . isTTY === true &&
2026-07-22 20:38:26 +08:00
process . stderr . isTTY === true ;
2026-07-18 05:18:05 +00:00
const setupHasExplicitScope = ( options : SetupCommandOptions ) : boolean =>
options . allDetected ||
options . client . length > 0 ||
2026-10-11 00:03:21 +08:00
options . skillClient . length > 0 ||
2026-07-18 05:18:05 +00:00
options . skill !== undefined ||
options . installHopper ;
2026-07-22 20:38:26 +08:00
const doctorScope = ( options : {
readonly client : readonly string [];
readonly provider : readonly string [];
readonly skill? : boolean | undefined ;
}) : DoctorScope | undefined =>
options . client . length === 0 &&
options . provider . length === 0 &&
options . skill === undefined
? undefined
: {
clients : options.client ,
providers : options.provider ,
skill : options.skill === true ,
};