fix(copilot): make Node hook installation transactional
This commit is contained in:
1 parent
cf77d49a3c
commit
2f988e72ba
14 files changed
+1722
-647
No files matched your search
+68
-49
@@ -1,81 +1,100 @@
|
||||
import type { AgentPlugin, InstallConfig, PluginStatus } from './base.js';
|
||||
import {
|
||||
AGENT_KEY,
|
||||
AUDIT_SCRIPT,
|
||||
GUARD_SCRIPT,
|
||||
buildHandler,
|
||||
removeManagedHooks,
|
||||
renderDocument,
|
||||
runtimeContracts,
|
||||
type CopilotSources,
|
||||
type RenderedDocument,
|
||||
} from './copilot-contract.js';
|
||||
import {
|
||||
readSources,
|
||||
requireRuntime,
|
||||
runtimeFilesExist,
|
||||
writeDocuments,
|
||||
} from './copilot-io.js';
|
||||
commitCopilotInstallation,
|
||||
commitCopilotUninstall,
|
||||
preflightCopilotInstallation,
|
||||
prepareCopilotInstallation,
|
||||
prepareCopilotUninstall,
|
||||
} from './copilot-installation.js';
|
||||
import { readSources, runtimeFilesExist } from './copilot-io.js';
|
||||
import { SUPPORTED_AGENTS } from './registry.js';
|
||||
|
||||
const entry = SUPPORTED_AGENTS.get(AGENT_KEY)!;
|
||||
|
||||
function mergedContracts(
|
||||
userHooks: Parameters<typeof runtimeContracts>[0],
|
||||
legacyHooks?: Parameters<typeof runtimeContracts>[0],
|
||||
): ReturnType<typeof runtimeContracts> {
|
||||
const contracts = [
|
||||
...runtimeContracts(userHooks),
|
||||
...runtimeContracts(legacyHooks ?? {}),
|
||||
function renderInstallation(
|
||||
sources: CopilotSources,
|
||||
guardPath: string,
|
||||
auditPath: string,
|
||||
): RenderedDocument[] {
|
||||
const userHooks = removeManagedHooks(sources.user.hooks);
|
||||
userHooks.preToolUse = [...(userHooks.preToolUse ?? []), buildHandler(guardPath)];
|
||||
userHooks.postToolUse = [...(userHooks.postToolUse ?? []), buildHandler(auditPath)];
|
||||
userHooks.postToolUseFailure = [
|
||||
...(userHooks.postToolUseFailure ?? []),
|
||||
buildHandler(auditPath),
|
||||
];
|
||||
const unique = new Map(contracts.map((contract) => [contract.agentId, contract]));
|
||||
const rendered = [renderDocument(sources.user, userHooks)];
|
||||
if (sources.legacy) {
|
||||
rendered.push(renderDocument(
|
||||
sources.legacy,
|
||||
removeManagedHooks(sources.legacy.hooks),
|
||||
));
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
function renderUninstall(sources: CopilotSources, agentId?: string): RenderedDocument[] {
|
||||
const rendered = [
|
||||
renderDocument(sources.user, removeManagedHooks(sources.user.hooks, agentId)),
|
||||
];
|
||||
if (sources.legacy) {
|
||||
rendered.push(renderDocument(
|
||||
sources.legacy,
|
||||
removeManagedHooks(sources.legacy.hooks, agentId),
|
||||
));
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
function mergedContracts(sources: CopilotSources) {
|
||||
const contracts = [
|
||||
...runtimeContracts(sources.user.hooks),
|
||||
...runtimeContracts(sources.legacy?.hooks ?? {}),
|
||||
];
|
||||
const unique = new Map(contracts.map((contract) => [
|
||||
process.platform === 'win32' ? contract.agentId.toLowerCase() : contract.agentId,
|
||||
contract,
|
||||
]));
|
||||
return [...unique.values()];
|
||||
}
|
||||
|
||||
export const copilotPlugin: AgentPlugin = {
|
||||
async install(config: InstallConfig): Promise<void> {
|
||||
if (!config.agentId) throw new Error('agentId is required');
|
||||
managesRuntime: true,
|
||||
|
||||
async preflightInstall(config: InstallConfig): Promise<void> {
|
||||
const sources = await readSources();
|
||||
await requireRuntime(config.guardScriptPath, 'Elydora guard runtime');
|
||||
await requireRuntime(config.hookScriptPath, 'Elydora audit runtime');
|
||||
await preflightCopilotInstallation(config, sources);
|
||||
},
|
||||
|
||||
const userHooks = removeManagedHooks(sources.user.hooks);
|
||||
userHooks.preToolUse = [
|
||||
...(userHooks.preToolUse ?? []),
|
||||
buildHandler(config.guardScriptPath),
|
||||
];
|
||||
userHooks.postToolUse = [
|
||||
...(userHooks.postToolUse ?? []),
|
||||
buildHandler(config.hookScriptPath),
|
||||
];
|
||||
|
||||
const rendered = [renderDocument(sources.user, userHooks)];
|
||||
if (sources.legacy) {
|
||||
rendered.push(renderDocument(
|
||||
sources.legacy,
|
||||
removeManagedHooks(sources.legacy.hooks),
|
||||
));
|
||||
}
|
||||
await writeDocuments(rendered);
|
||||
async install(config: InstallConfig): Promise<void> {
|
||||
const sources = await readSources();
|
||||
const paths = await preflightCopilotInstallation(config, sources);
|
||||
const rendered = renderInstallation(sources, paths.guardPath, paths.auditPath);
|
||||
const prepared = await prepareCopilotInstallation(config, sources, rendered);
|
||||
await commitCopilotInstallation(prepared);
|
||||
console.log(` GitHub Copilot CLI hooks: ${sources.user.filePath}`);
|
||||
console.log(' GitHub Copilot CLI: restart active sessions to load the updated hooks.');
|
||||
},
|
||||
|
||||
async uninstall(agentId?: string): Promise<void> {
|
||||
const sources = await readSources();
|
||||
const rendered = [
|
||||
renderDocument(sources.user, removeManagedHooks(sources.user.hooks, agentId)),
|
||||
];
|
||||
if (sources.legacy) {
|
||||
rendered.push(renderDocument(
|
||||
sources.legacy,
|
||||
removeManagedHooks(sources.legacy.hooks, agentId),
|
||||
));
|
||||
}
|
||||
await writeDocuments(rendered);
|
||||
const prepared = await prepareCopilotUninstall(renderUninstall(sources, agentId));
|
||||
await commitCopilotUninstall(prepared);
|
||||
},
|
||||
|
||||
async status(): Promise<PluginStatus> {
|
||||
const sources = await readSources();
|
||||
const contracts = mergedContracts(sources.user.hooks, sources.legacy?.hooks);
|
||||
const hookConfigured = contracts.length > 0;
|
||||
const contracts = mergedContracts(sources);
|
||||
const hookConfigured = sources.disabledBy === undefined && contracts.length > 0;
|
||||
const hookScriptExists = hookConfigured && await runtimeFilesExist(contracts);
|
||||
return {
|
||||
installed: hookConfigured && hookScriptExists,
|
||||
|
||||
Reference in new issue
Block a user