fix(update): share parsed client activation with maintenance

This commit is contained in:
morluto 2026-10-13 12:46:05 +08:00
parent bb540826d6
commit b045df100e
3 changed files with 112 additions and 80 deletions

View File

@ -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<readonly ClientRegistrationStatus[]> => {
const statuses: ClientRegistrationStatus[] = [];
options: ClientRegistrationReadOptions = {},
): Promise<readonly ClientRegistrationStatus[]> =>
(
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<readonly ClientRegistrationInspection[]> => {
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<typeof registrationSchema>,
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<typeof registrationSchema>,
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 (

View File

@ -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<NodeJS.ProcessEnv>,
resources?: ArtifactResourceScope,
): Promise<MaintenanceScope> => {
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,

View File

@ -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);