fix(update): share parsed client activation with maintenance
This commit is contained in:
parent
bb540826d6
commit
b045df100e
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue