fix(setup): preserve onboarding after refactor

This commit is contained in:
morluto 2026-07-18 05:18:05 +00:00
parent db1efef9d2
commit da48033c19
4 changed files with 474 additions and 267 deletions

View File

@ -23,16 +23,14 @@ import {
canonicalSkillNeedsInstall,
installCanonicalSkill,
} from "./SetupSkill.js";
import { setupPlan } from "./SetupPlan.js";
import { discoverSetupActions } from "./SetupPlan.js";
import {
clientConfigurationAligned,
configureJsonClient,
configureTomlClient,
inspectClientConfiguration,
} from "./SetupClientConfiguration.js";
export {
configureJsonClient,
configureTomlClient,
} from "./SetupClientConfiguration.js";
export { configureJsonClient, configureTomlClient };
import {
setupInstallFailure,
type SetupFailureCode,
@ -43,10 +41,7 @@ export type { SetupHopperInstallResult } from "./SetupInstallFailure.js";
/** Exact non-secret provider variables propagated into managed registrations. */
export type SetupProviderEnvironment = Readonly<Record<string, string>>;
export {
canonicalSkillNeedsInstall,
installCanonicalSkill,
} from "./SetupSkill.js";
export { canonicalSkillNeedsInstall, installCanonicalSkill };
const registrationCommand = (): readonly string[] =>
process.env.npm_command === "exec"
@ -75,11 +70,7 @@ export type ClientConfigurationInspection =
readonly status: "invalid";
readonly remediation: string;
};
/**
* Effects required by the idempotent setup workflow.
* Implementations report interrupted installers as values: setup must not wait
* indefinitely for stdin or a GUI response in an unattended agent invocation.
*/
/** Effects required by the idempotent, non-blocking setup workflow. */
export interface SetupHost {
readonly platform: NodeJS.Platform;
readonly nodeVersion: string;
@ -108,11 +99,7 @@ export interface SetupHost {
installSkill(): Promise<"installed" | "unchanged" | "failed">;
doctor(): Promise<Awaited<ReturnType<typeof runDoctor>>>;
}
/**
* Structured setup outcome intended to travel back through an agent to a human.
* `needs_human` carries remediation text instead of initiating an interactive
* prompt, avoiding deadlocks in stdio and other unattended environments.
*/
/** Structured setup outcome carrying remediation instead of prompting. */
export interface SetupResult {
readonly status: "planned" | "needs_confirmation" | "ready" | "needs_human";
readonly plannedActions: readonly SetupAction[];
@ -168,11 +155,7 @@ export type SetupConfirmation = (
actions: readonly SetupAction[],
) => Promise<boolean | SetupConfirmationDecision>;
/**
* Install prerequisites and configure detected clients idempotently.
* Discovery always precedes mutation. Interactive confirmation or explicit
* unattended options authorize only the actions represented in the result.
*/
/** Discover, approve, and apply setup actions idempotently. */
export const runSetup = async (
options: SetupOptions,
host: SetupHost = systemSetupHost(),
@ -190,15 +173,86 @@ export const runSetup = async (
const discovery = await discoverSetupActions({
host,
providerEnvironment,
command: registrationCommand(),
forceHopperInstall: options.installHopper,
proposeHopper: options.proposeHopper ?? true,
selectedClientIds: options.clientIds,
installSkillSelection: options.installSkill,
});
if (discovery.blocker !== undefined) return fail(discovery.blocker);
let { installHopper, installSkill } = discovery;
let detectedClients: readonly SetupClient[] = discovery.detectedClients;
plannedActions = discovery.plannedActions;
const selection = await resolveSetupSelection(options, discovery, confirm);
plannedActions = selection.plannedActions;
if (!selection.approved)
return {
status: confirm === undefined ? "needs_confirmation" : "planned",
plannedActions,
appliedActions,
clients,
doctor: await host.doctor(),
remediation:
"Review the setup plan, then rerun interactively or with --yes.",
};
let detectedClients: readonly SetupClient[] = selection.detectedClients;
if (
selection.installHopper &&
(selection.interactiveApproval || options.installHopper)
) {
const install = await installHopperAction({
host,
options,
plannedActions,
appliedActions,
clients,
detectedClients,
providerEnvironment,
});
if ("failure" in install) return install.failure;
({ hopperPath, providerEnvironment, detectedClients } = install);
}
const clientFailure = await configureDetectedClients({
host,
detectedClients,
providerEnvironment,
command: registrationCommand(),
clients,
appliedActions,
...(options.onProgress === undefined
? {}
: { onProgress: options.onProgress }),
});
if (clientFailure !== undefined) return fail(clientFailure);
if (
selection.installSkill &&
!(await installSkillAction(host, options, appliedActions))
)
return fail(
"REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.",
);
const doctor = await host.doctor();
const remediation = finalSetupRemediation(
host.platform,
appliedActions.includes("installed_hopper"),
doctor.healthy,
hopperPath,
);
return {
status: remediation === undefined ? "ready" : "needs_human",
plannedActions,
appliedActions,
clients,
doctor,
...(remediation === undefined ? {} : { remediation }),
};
};
const resolveSetupSelection = async (
options: SetupOptions,
discovery: Awaited<ReturnType<typeof discoverSetupActions>>,
confirm: SetupConfirmation | undefined,
) => {
let { installHopper, installSkill, detectedClients, plannedActions } =
discovery;
let approved = options.approved || plannedActions.length === 0;
let interactiveApproval = false;
if (!approved && confirm !== undefined && !options.structured) {
@ -222,120 +276,104 @@ export const runSetup = async (
approved = decision.approved || plannedActions.length === 0;
}
}
if (!approved)
return {
status: confirm === undefined ? "needs_confirmation" : "planned",
plannedActions,
appliedActions,
clients,
doctor: await host.doctor(),
remediation:
"Review the setup plan, then rerun interactively or with --yes.",
};
return {
approved,
interactiveApproval,
installHopper,
installSkill,
detectedClients,
plannedActions,
};
};
if (installHopper && (interactiveApproval || options.installHopper)) {
emitProgress(
options,
"install_hopper",
"Hopper deep-analysis provider",
"started",
);
const installed = await host.installHopper(options.installHopper);
if (installed.status === "failed") {
emitProgress(
options,
"install_hopper",
"Hopper deep-analysis provider",
"failed",
installed.remediation,
);
return {
status: "needs_human",
plannedActions,
appliedActions,
clients,
doctor: await host.doctor(),
const installHopperAction = async (input: {
readonly host: SetupHost;
readonly options: SetupOptions;
readonly plannedActions: readonly SetupAction[];
readonly appliedActions: string[];
readonly clients: Readonly<Record<string, ClientConfigurationResult>>;
readonly detectedClients: readonly SetupClient[];
readonly providerEnvironment: SetupProviderEnvironment;
}) => {
const label = "Hopper deep-analysis provider";
emitProgress(input.options, {
actionId: "install_hopper",
label,
state: "started",
});
const installed = await input.host.installHopper(input.options.installHopper);
if (installed.status === "failed") {
emitProgress(input.options, {
actionId: "install_hopper",
label,
state: "failed",
detail: installed.remediation,
});
return {
failure: {
status: "needs_human" as const,
plannedActions: input.plannedActions,
appliedActions: input.appliedActions,
clients: input.clients,
doctor: await input.host.doctor(),
code: installed.code,
remediation: installed.remediation,
};
}
hopperPath = installed.launcherPath;
providerEnvironment = {
...providerEnvironment,
HOPPER_LAUNCHER_PATH: installed.launcherPath,
},
};
appliedActions.push("installed_hopper");
emitProgress(
options,
"install_hopper",
"Hopper deep-analysis provider",
"completed",
installed.launcherPath,
);
detectedClients = await filterClientsNeedingConfigure(
host,
detectedClients,
}
input.appliedActions.push("installed_hopper");
emitProgress(input.options, {
actionId: "install_hopper",
label,
state: "completed",
detail: installed.launcherPath,
});
const providerEnvironment = {
...input.providerEnvironment,
HOPPER_LAUNCHER_PATH: installed.launcherPath,
};
return {
hopperPath: installed.launcherPath,
providerEnvironment,
detectedClients: await filterClientsNeedingConfigure(
input.host,
input.detectedClients,
providerEnvironment,
registrationCommand(),
);
}
const clientFailure = await configureDetectedClients({
host,
detectedClients,
providerEnvironment,
command: registrationCommand(),
clients,
appliedActions,
...(options.onProgress === undefined
? {}
: { onProgress: options.onProgress }),
});
if (clientFailure !== undefined) return fail(clientFailure);
if (installSkill) {
emitProgress(
options,
"install_skill",
"REA reverse-engineering skill",
"started",
);
const skill = await host.installSkill();
if (skill === "failed") {
emitProgress(
options,
"install_skill",
"REA reverse-engineering skill",
"failed",
);
return fail(
"REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.",
);
}
if (skill === "installed") appliedActions.push("installed_skill");
emitProgress(
options,
"install_skill",
"REA reverse-engineering skill",
skill === "installed" ? "completed" : "warning",
skill === "unchanged" ? "Already current" : undefined,
);
}
const doctor = await host.doctor();
const remediation = finalSetupRemediation(
host.platform,
appliedActions.includes("installed_hopper"),
doctor.healthy,
hopperPath,
);
return {
status: remediation === undefined ? "ready" : "needs_human",
plannedActions,
appliedActions,
clients,
doctor,
...(remediation === undefined ? {} : { remediation }),
),
};
};
const installSkillAction = async (
host: SetupHost,
options: SetupOptions,
appliedActions: string[],
): Promise<boolean> => {
const label = "REA reverse-engineering skill";
emitProgress(options, {
actionId: "install_skill",
label,
state: "started",
});
const skill = await host.installSkill();
if (skill === "failed") {
emitProgress(options, {
actionId: "install_skill",
label,
state: "failed",
});
return false;
}
if (skill === "installed") appliedActions.push("installed_skill");
emitProgress(options, {
actionId: "install_skill",
label,
state: skill === "installed" ? "completed" : "warning",
...(skill === "unchanged" ? { detail: "Already current" } : {}),
});
return true;
};
const setupFailure = async (
remediation: string,
[host, plannedActions, appliedActions, clients]: readonly [
@ -353,92 +391,8 @@ const setupFailure = async (
remediation,
});
const discoverSetupActions = async (
host: SetupHost,
providerEnvironment: SetupProviderEnvironment,
forceHopperInstall: boolean,
) => {
const initialDoctor = await host.doctor();
const linuxHopperRepairNeeded = initialDoctor.checks.some(
({ name, ok, detail }) =>
!ok &&
(name === "hopper-demo-runtime" ||
(name === "hopper-version" && detail === "/opt/hopper/bin/Hopper")),
);
const installHopper =
input.forceHopperInstall ||
(input.proposeHopper &&
(Object.keys(providerEnvironment).length === 0 ||
linuxHopperRepairNeeded));
const [allDetectedClients, skillNeedsInstall] = await Promise.all([
host.detectedClients(),
host.skillNeedsInstall(),
]);
const selectedClientIdSet =
input.selectedClientIds === undefined
? undefined
: new Set(input.selectedClientIds);
const detectedClients = allDetectedClients.filter(
({ name }) =>
selectedClientIdSet === undefined || selectedClientIdSet.has(name),
);
const installSkill =
input.installSkillSelection === false ? false : skillNeedsInstall;
const command = registrationCommand();
const clients = installHopper
? detectedClients
: await filterClientsNeedingConfigure(
host,
detectedClients,
providerEnvironment,
command,
);
const plannedActions = setupPlan(host.platform, {
installHopper,
installSkill,
clients,
providerEnvironment,
});
return {
installHopper,
installSkill,
detectedClients: clientPlans.map(({ client }) => client),
plannedActions,
blocker,
};
};
const inspectClients = async (
host: SetupHost,
clients: readonly SetupClient[],
providerEnvironment: SetupProviderEnvironment,
command: readonly string[],
) =>
Promise.all(
clients.map(async (client) => ({
client,
inspection:
(await host.inspectClientConfiguration?.(
client,
providerEnvironment,
command,
)) ?? ({ status: "update" } as const),
})),
);
const emitProgress = (
options: SetupOptions,
actionId: string,
label: string,
state: SetupProgressEvent["state"],
detail?: string,
): void =>
options.onProgress?.({
actionId,
label,
state,
...(detail === undefined ? {} : { detail }),
});
const emitProgress = (options: SetupOptions, event: SetupProgressEvent): void =>
options.onProgress?.(event);
const filterClientsNeedingConfigure = async (
host: SetupHost,

View File

@ -8,6 +8,7 @@ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
import { PRODUCT_IDENTITY } from "../identity.js";
import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
import type {
ClientConfigurationInspection,
ClientConfigurationResult,
SetupProviderEnvironment,
} from "./Setup.js";
@ -187,6 +188,57 @@ export const clientConfigurationAligned = async (
}
};
/** Preflight one client configuration before setup presents any mutations. */
export const inspectClientConfiguration = async (
client: SetupClient,
providerEnvironment: SetupProviderEnvironment,
command: readonly string[],
): Promise<ClientConfigurationInspection> => {
if (client.format === "unsupported") return { status: "already_current" };
const transactionPath = await resolveClientConfigTransactionPath(
client.configPath,
);
if (transactionPath === undefined)
return {
status: "invalid",
remediation:
"The configuration path is unsafe or unresolved. Check ownership and symbolic links before rerunning setup.",
};
let original: string;
try {
original = await readFile(transactionPath, "utf8");
} catch (cause: unknown) {
if (isMissing(cause)) return { status: "create" };
return {
status: "invalid",
remediation:
"The configuration file could not be read. Check its permissions before rerunning setup.",
};
}
const desired = clientConfigurationDesired(providerEnvironment, command);
try {
const document =
client.format === "toml"
? objectSchema.parse(parseToml(original))
: parseObject(original);
const servers = parseOptionalObject(
client.format === "toml" ? document.mcp_servers : document.mcpServers,
);
if (sameConfiguration(servers[PRODUCT_IDENTITY.mcpServerKey], desired))
return { status: "already_current" };
} catch {
return {
status: "invalid",
remediation:
"The existing configuration is malformed. Repair it before setup applies any other changes.",
};
}
return {
status: "update",
backupPath: `${client.configPath}.rea.backup`,
};
};
const objectSchema = z.record(z.string(), z.unknown());
const parseOptionalObject = (value: unknown): Record<string, unknown> =>
value === undefined ? {} : objectSchema.parse(value);

View File

@ -1,24 +1,39 @@
import { homedir } from "node:os";
import { join } from "node:path";
import { PRODUCT_IDENTITY } from "../identity.js";
import {
linuxHopperInstallDisclosure,
type LinuxPackageFamily,
} from "./LinuxHopper.js";
import { macHopperInstallDisclosure } from "./MacHopper.js";
import type {
SetupAction,
SetupClient,
SetupHost,
SetupProviderEnvironment,
} from "./Setup.js";
/** Preflighted client mutation admitted into the setup plan. */
export type SetupClientPlan = Readonly<{
client: SetupClient;
operation: "create" | "update";
backupPath?: string;
}>;
/** Inputs used to build the setup mutation plan. */
export interface SetupPlanOptions {
readonly platform: NodeJS.Platform;
readonly installHopper: boolean;
readonly installSkill: boolean;
readonly clients: readonly SetupClient[];
readonly clients: readonly SetupClientPlan[];
readonly providerEnvironment: SetupProviderEnvironment;
readonly linuxPackageFamily?: LinuxPackageFamily;
}
/** Build the complete setup mutation plan before approval. */
export const setupPlan = (
platform: NodeJS.Platform,
options: SetupPlanOptions,
): readonly SetupAction[] => [
...(options.installHopper
export const setupPlan = (input: SetupPlanOptions): readonly SetupAction[] => [
...(input.installHopper
? [
{
id: "install_hopper",
@ -45,8 +60,8 @@ export const setupPlan = (
},
]
: []),
...options.clients
.filter(({ format }) => format !== "unsupported")
...input.clients
.filter(({ client }) => client.format !== "unsupported")
.map(
({ client, operation, backupPath }): SetupAction => ({
id: `configure_client:${client.name}`,
@ -54,15 +69,15 @@ export const setupPlan = (
label: client.displayName ?? client.name,
target: client.configPath,
detail: clientConfigurationDetail(
client.name,
options.providerEnvironment,
client.displayName ?? client.name,
input.providerEnvironment,
),
external: false,
operation,
...(backupPath === undefined ? {} : { backupPath }),
}),
),
...(options.installSkill
...(input.installSkill
? [
{
id: "install_skill",
@ -83,6 +98,139 @@ export const setupPlan = (
: []),
];
/** Discover and preflight the complete set of selectable setup actions. */
export const discoverSetupActions = async (input: {
readonly host: SetupHost;
readonly providerEnvironment: SetupProviderEnvironment;
readonly command: readonly string[];
readonly forceHopperInstall: boolean;
readonly proposeHopper: boolean;
readonly selectedClientIds: readonly string[] | undefined;
readonly installSkillSelection: boolean | undefined;
}) => {
const { host, providerEnvironment, command } = input;
const initialDoctor = await host.doctor();
const linuxHopperRepairNeeded = initialDoctor.checks.some(
({ name, ok, detail }) =>
!ok &&
(name === "hopper-demo-runtime" ||
(name === "hopper-version" && detail === "/opt/hopper/bin/Hopper")),
);
const installHopper =
input.forceHopperInstall ||
(input.proposeHopper &&
(Object.keys(providerEnvironment).length === 0 ||
linuxHopperRepairNeeded));
const [allDetectedClients, skillNeedsInstall] = await Promise.all([
host.detectedClients(),
host.skillNeedsInstall(),
]);
const selectedClientIdSet =
input.selectedClientIds === undefined
? undefined
: new Set(input.selectedClientIds);
const detectedClients = allDetectedClients.filter(
({ name }) =>
selectedClientIdSet === undefined || selectedClientIdSet.has(name),
);
const installSkill =
input.installSkillSelection === false ? false : skillNeedsInstall;
const clients = installHopper
? detectedClients
: await filterClientsNeedingConfigure({
host,
clients: detectedClients,
providerEnvironment,
command,
});
const inspections = await inspectClients({
host,
clients,
providerEnvironment,
command,
});
const invalid = inspections.find(
({ inspection }) => inspection.status === "invalid",
);
const blocker =
invalid?.inspection.status === "invalid"
? `${invalid.client.displayName ?? invalid.client.name}: ${invalid.inspection.remediation}`
: undefined;
const clientPlans = inspections.flatMap(({ client, inspection }) =>
inspection.status === "create" ||
inspection.status === "update" ||
(installHopper && inspection.status === "already_current")
? [
{
client,
operation:
inspection.status === "already_current"
? ("update" as const)
: inspection.status,
...(inspection.status !== "already_current" &&
inspection.backupPath !== undefined
? { backupPath: inspection.backupPath }
: {}),
},
]
: [],
);
const linuxDistribution =
host.platform === "linux" ? await host.linuxDistribution() : undefined;
return {
installHopper,
installSkill,
detectedClients: clientPlans.map(({ client }) => client),
plannedActions: setupPlan({
platform: host.platform,
installHopper,
installSkill,
clients: clientPlans,
providerEnvironment,
...(linuxDistribution?.packageFamily === undefined
? {}
: { linuxPackageFamily: linuxDistribution.packageFamily }),
}),
blocker,
};
};
const inspectClients = async (input: {
readonly host: SetupHost;
readonly clients: readonly SetupClient[];
readonly providerEnvironment: SetupProviderEnvironment;
readonly command: readonly string[];
}) =>
Promise.all(
input.clients.map(async (client) => ({
client,
inspection:
(await input.host.inspectClientConfiguration?.(
client,
input.providerEnvironment,
input.command,
)) ?? ({ status: "update" } as const),
})),
);
const filterClientsNeedingConfigure = async (input: {
readonly host: SetupHost;
readonly clients: readonly SetupClient[];
readonly providerEnvironment: SetupProviderEnvironment;
readonly command: readonly string[];
}): Promise<readonly SetupClient[]> => {
const needs = await Promise.all(
input.clients.map((client) =>
input.host.clientNeedsConfigure(
client,
input.providerEnvironment,
input.command,
),
),
);
return input.clients.filter((_, index) => needs[index]);
};
const linuxDisclosure = (family: LinuxPackageFamily) => {
const disclosure = linuxHopperInstallDisclosure(
family,

View File

@ -1,14 +1,15 @@
import { z } from "incur";
import { createInterface } from "node:readline/promises";
import { runDoctor } from "../application/Doctor.js";
import {
runSetup,
systemSetupHost,
type SetupAction,
} from "../application/Setup.js";
import { runSetup, systemSetupHost } from "../application/Setup.js";
import { runUninstall } from "../application/Uninstall.js";
import { runUpgrade, systemUpgradeHost } from "../application/Upgrade.js";
import {
conciseSetupResult,
confirmInteractiveSetup,
renderInteractiveSetupResult,
renderSetupProgress,
} from "../cliSetup.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { logCliCommand } from "../cliLogging.js";
import { createSystemDoctorHost } from "../doctorRuntime.js";
@ -22,6 +23,7 @@ export const registerSetupCommands = (
): void => {
cli.command(CLI_COMMANDS.setup, {
description: "Install requirements and configure agents",
outputPolicy: "agent-only",
options: z.object({
yes: z
.boolean()
@ -31,21 +33,40 @@ export const registerSetupCommands = (
.boolean()
.default(false)
.describe("Also approve Hopper installation with --yes"),
client: z
.array(
z.enum([
"claude_code",
"claude_desktop",
"codex",
"cursor",
"gemini_cli",
"windsurf",
]),
)
.default([])
.describe("Agent integration to configure; repeat for multiple agents"),
allDetected: z
.boolean()
.default(false)
.describe("Configure every detected supported agent"),
skill: z
.boolean()
.optional()
.describe("Install or skip the bundled REA skill"),
dryRun: z
.boolean()
.default(false)
.describe("Print the resolved plan without applying it"),
accessible: z
.boolean()
.default(false)
.describe("Use sequential accessible setup prompts"),
}),
alias: { yes: "y", installHopper: "install-hopper" },
run: ({ options, formatExplicit }) =>
alias: { yes: "y" },
run: ({ options, formatExplicit, agent }) =>
logCliCommand(logger, "setup", () =>
runSetup(
{
approved: options.yes,
installHopper: options.installHopper,
structured: formatExplicit,
},
systemSetupHost(createSystemDoctorHost()),
options.yes || formatExplicit || process.stdin.isTTY !== true
? undefined
: confirmSetup,
),
runSetupCommand({ options, formatExplicit, agent }),
),
});
cli.command(CLI_COMMANDS.doctor, {
@ -83,24 +104,56 @@ export const registerSetupCommands = (
});
};
const confirmSetup = async (
actions: readonly SetupAction[],
): Promise<boolean> => {
process.stdout.write("\nREA setup plan\n");
for (const action of actions)
process.stdout.write(
` - ${action.detail}\n ${action.target}${action.external ? " (external software)" : ""}\n`,
interface SetupCommandOptions {
readonly yes: boolean;
readonly installHopper: boolean;
readonly client: readonly string[];
readonly allDetected: boolean;
readonly skill?: boolean | undefined;
readonly dryRun: boolean;
readonly accessible: boolean;
}
const runSetupCommand = async (input: {
readonly options: SetupCommandOptions;
readonly formatExplicit: boolean;
readonly agent: boolean;
}) => {
const { options } = input;
const interactive =
!options.yes &&
!options.dryRun &&
!input.formatExplicit &&
process.stdin.isTTY === true &&
process.stderr.isTTY === true;
const hasExplicitScope = setupHasExplicitScope(options);
if (options.yes && !hasExplicitScope)
process.stderr.write(
"! Implicit `rea setup --yes` scope is deprecated; use `--all-detected --skill` to retain it.\n",
);
const prompt = createInterface({
input: process.stdin,
output: process.stdout,
});
try {
const answer = (await prompt.question("Continue? [Y/n] "))
.trim()
.toLowerCase();
return answer === "" || answer === "y" || answer === "yes";
} finally {
prompt.close();
}
const result = await runSetup(
{
approved: options.yes && !options.dryRun,
installHopper: options.installHopper,
structured: input.formatExplicit || options.dryRun,
proposeHopper: !hasExplicitScope || options.installHopper,
...(!hasExplicitScope || options.allDetected
? {}
: { clientIds: options.client }),
...(!hasExplicitScope ? {} : { installSkill: options.skill ?? false }),
...(interactive ? { onProgress: renderSetupProgress } : {}),
},
systemSetupHost(createSystemDoctorHost()),
interactive
? (actions) => confirmInteractiveSetup(actions, options.accessible)
: undefined,
);
if (interactive) renderInteractiveSetupResult(result);
return input.formatExplicit ? result : conciseSetupResult(result);
};
const setupHasExplicitScope = (options: SetupCommandOptions): boolean =>
options.allDetected ||
options.client.length > 0 ||
options.skill !== undefined ||
options.installHopper;