From b045df100e1489e08a36d6ff04cea7feecf1332c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:46:05 +0800 Subject: [PATCH] fix(update): share parsed client activation with maintenance --- src/application/ClientRegistrationStatus.ts | 106 +++++++++++------- src/application/UpdateMaintenance.ts | 51 ++------- .../process-global/updateMaintenance.test.ts | 35 ++++++ 3 files changed, 112 insertions(+), 80 deletions(-) diff --git a/src/application/ClientRegistrationStatus.ts b/src/application/ClientRegistrationStatus.ts index e2696e088..caeba4fb6 100644 --- a/src/application/ClientRegistrationStatus.ts +++ b/src/application/ClientRegistrationStatus.ts @@ -7,6 +7,7 @@ import { clientServerForcedEnabled, clientServerListedDisabled, effectiveClientServer, + type ClientConfigurationDocument, } from "./ClientConfigurationDocument.js"; import { access } from "node:fs/promises"; import { resolve } from "node:path"; @@ -74,24 +75,48 @@ const registrationSchema = z }) .passthrough(); -/** Inspect supported client registrations without reading their environment. */ +interface ClientRegistrationReadOptions { + readonly platform?: NodeJS.Platform; + readonly environment?: NodeJS.ProcessEnv; + readonly resources?: ArtifactResourceScope; +} + +/** Registration facts shared by diagnostics and update maintenance. */ +export interface ClientRegistrationInspection { + readonly status: ClientRegistrationStatus; + readonly activation: "enabled" | "disabled" | "unknown"; +} + +/** Project diagnostics from the same inspection used to select maintenance. */ export const readClientRegistrationStatuses = async ( home: string, currentCommandPath: string = resolve(process.argv[1] ?? "unknown"), - options: { - readonly platform?: NodeJS.Platform; - readonly environment?: NodeJS.ProcessEnv; - readonly resources?: ArtifactResourceScope; - } = {}, -): Promise => { - const statuses: ClientRegistrationStatus[] = []; + options: ClientRegistrationReadOptions = {}, +): Promise => + ( + await readClientRegistrationInspections(home, currentCommandPath, options) + ).map(({ status }) => status); + +/** Parse effective registrations and their client-specific activation once. */ +export const readClientRegistrationInspections = async ( + home: string, + currentCommandPath: string = resolve(process.argv[1] ?? "unknown"), + options: ClientRegistrationReadOptions = {}, +): Promise => { + const inspections: ClientRegistrationInspection[] = []; + const record = ( + status: ClientRegistrationStatus, + activation: ClientRegistrationInspection["activation"] = "unknown", + ): void => { + inspections.push({ status, activation }); + }; for (const client of supportedClients( home, options.platform, options.environment, )) { if (client.configPathError !== undefined) { - statuses.push({ + record({ client: client.name, config_path: client.configPath, command: [], @@ -104,7 +129,7 @@ export const readClientRegistrationStatuses = async ( const manualRemediation = manualRegistrationRemediation(client.name); if (client.format === "unsupported") { if (manualRemediation !== undefined) - statuses.push({ + record({ client: client.name, config_path: client.markerPath ?? client.configPath, command: [], @@ -119,7 +144,7 @@ export const readClientRegistrationStatuses = async ( options.resources, ); if (!files.ok) { - statuses.push({ + record({ client: client.name, config_path: client.configPath, command: [], @@ -130,9 +155,7 @@ export const readClientRegistrationStatuses = async ( } const parsed = effectiveClientConfiguration(files.value); if (parsed === undefined) { - statuses.push( - unavailableStatus(client.name, client.configPath, "missing"), - ); + record(unavailableStatus(client.name, client.configPath, "missing")); continue; } const policy = await readClientPolicyBlock( @@ -142,7 +165,7 @@ export const readClientRegistrationStatuses = async ( options.resources, ); if (!policy.ok) { - statuses.push({ + record({ client: client.name, config_path: client.configPath, command: [], @@ -159,7 +182,7 @@ export const readClientRegistrationStatuses = async ( ? dshDocument(parsed.document).editabilityError : undefined; if (editabilityError !== undefined) { - statuses.push({ + record({ client: client.name, config_path: client.configPath, command: [], @@ -168,9 +191,7 @@ export const readClientRegistrationStatuses = async ( }); continue; } - statuses.push( - unavailableStatus(client.name, client.configPath, "missing"), - ); + record(unavailableStatus(client.name, client.configPath, "missing")); continue; } const registration = parseRegistration(raw, client); @@ -178,21 +199,18 @@ export const readClientRegistrationStatuses = async ( registration.command, ...registration.args, ]; - statuses.push( + const activation = registrationActivation(registration, client, parsed); + record( configuredStatus( client.name, client.configPath, command, - registrationAligned( - registration, - client, - currentCommandPath, - options.platform ?? process.platform, - clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey), - ) && - !clientServerListedDisabled( - parsed, - PRODUCT_IDENTITY.mcpServerKey, + activation === "enabled" && + registrationAligned( + registration, + client, + currentCommandPath, + options.platform ?? process.platform, ) && (parsed.dialect !== "dsh" || dshDocument(parsed.document).ready) && policyBlock === undefined @@ -200,9 +218,10 @@ export const readClientRegistrationStatuses = async ( : "stale", policyBlock, ), + activation, ); } catch (cause: unknown) { - statuses.push( + record( unavailableStatus( client.name, client.configPath, @@ -211,26 +230,31 @@ export const readClientRegistrationStatuses = async ( ); } } - return statuses.sort((left, right) => - compareUnicodeCodePoints(left.client, right.client), + return inspections.sort((left, right) => + compareUnicodeCodePoints(left.status.client, right.status.client), ); }; +const registrationActivation = ( + registration: z.output, + client: SetupClient, + parsed: ClientConfigurationDocument, +): "enabled" | "disabled" => + (client.format !== "grok" && registration.disabled === true) || + (registration.enabled === false && + !clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey)) || + (client.format === "zcode" && registration.enable === false) || + clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey) + ? "disabled" + : "enabled"; + const registrationAligned = ( registration: z.output, client: SetupClient, currentCommandPath: string, platform: NodeJS.Platform, - forcedEnabled = false, ): boolean => { const command = [registration.command, ...registration.args]; - if ( - (client.format !== "grok" && registration.disabled === true) || - (registration.enabled === false && !forcedEnabled) || - // ZCode's native flag is `enable`; other dialects leave that field opaque. - (client.format === "zcode" && registration.enable === false) - ) - return false; if (!isOwnedClientRegistrationCommand(command, currentCommandPath)) return false; if ( diff --git a/src/application/UpdateMaintenance.ts b/src/application/UpdateMaintenance.ts index 06ac1b512..fd55c8f23 100644 --- a/src/application/UpdateMaintenance.ts +++ b/src/application/UpdateMaintenance.ts @@ -1,19 +1,10 @@ import { z } from "zod"; -import { PRODUCT_IDENTITY } from "../identity.js"; import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.js"; -import { readClientRegistrationStatuses } from "./ClientRegistrationStatus.js"; -import { - clientServerForcedEnabled, - clientServerListedDisabled, - effectiveClientServer, - parseClientConfiguration, -} from "./ClientConfigurationDocument.js"; -import { supportedClients } from "./SupportedClients.js"; +import { readClientRegistrationInspections } from "./ClientRegistrationStatus.js"; import { existingSkillDestinations } from "./SetupSkill.js"; import type { SetupAction } from "./SetupTypes.js"; import type { Result } from "../domain/result.js"; -import { readRegularFileText } from "./RegularFileRead.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; /** Existing REA integrations selected for maintenance, without discovering new targets. */ @@ -61,41 +52,23 @@ export const existingMaintenanceScope = async ( environment: Readonly, resources?: ArtifactResourceScope, ): Promise => { - const registrations = await readClientRegistrationStatuses(home, entryPoint, { - environment, - ...(resources === undefined ? {} : { resources }), - }); - const supported = supportedClients(home, process.platform, environment); + const registrations = await readClientRegistrationInspections( + home, + entryPoint, + { + environment, + ...(resources === undefined ? {} : { resources }), + }, + ); const clients: string[] = []; - for (const registration of registrations) { + for (const { status: registration, activation } of registrations) { if ( + activation !== "enabled" || (registration.state !== "aligned" && registration.state !== "stale") || !isOwnedClientRegistrationCommand(registration.command, entryPoint) ) continue; - const client = supported.find(({ name }) => name === registration.client); - if (client === undefined) continue; - const parsed = parseClientConfiguration( - await readRegularFileText( - client.configPath, - resources === undefined ? {} : { resources }, - ), - client.format, - ); - const enabled = z - .object({ - enabled: z.boolean().optional(), - disabled: z.boolean().optional(), - }) - .parse(effectiveClientServer(parsed, PRODUCT_IDENTITY.mcpServerKey)); - // Root disable lists override enabled entries and OMP's force-on allowlist. - if ( - (enabled.enabled !== false || - clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey)) && - enabled.disabled !== true && - !clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey) - ) - clients.push(client.name); + clients.push(registration.client); } const skillDestinations = await existingSkillDestinations( home, diff --git a/tests/process-global/updateMaintenance.test.ts b/tests/process-global/updateMaintenance.test.ts index a60a2e213..ae057bab2 100644 --- a/tests/process-global/updateMaintenance.test.ts +++ b/tests/process-global/updateMaintenance.test.ts @@ -126,6 +126,41 @@ describe("OMP integration maintenance", () => { }); }); +describe("maintenance activation and layered configuration", () => { + it("leaves a natively disabled ZCode registration outside maintenance", async () => { + const configPath = await writeClient( + "zcode", + JSON.stringify({ + mcp: { + servers: { + rea: { command: entryPoint, args: ["mcp"], enable: false }, + }, + }, + }), + ); + const original = await readFile(configPath, "utf8"); + await expect( + existingMaintenanceScope(home, entryPoint, process.env), + ).resolves.toMatchObject({ clients: [] }); + expect(await readFile(configPath, "utf8")).toBe(original); + }); + + it("selects an enabled registration inherited through layered OpenCode files", async () => { + const directory = join(home, ".config", "opencode"); + await mkdir(directory, { recursive: true }); + await writeFile( + join(directory, "opencode.json"), + JSON.stringify({ + mcp: { rea: { type: "local", command: [entryPoint, "mcp"] } }, + }), + ); + await writeClient("opencode", JSON.stringify({ theme: "user-selected" })); + await expect( + existingMaintenanceScope(home, entryPoint, process.env), + ).resolves.toMatchObject({ clients: ["opencode"] }); + }); +}); + describe("existing REA integration maintenance", () => { it("selects owned registrations while preserving unconfigured, foreign, and disabled choices", async () => { await writeClient("codex", staleCodex);