166 lines
4.6 KiB
Go
166 lines
4.6 KiB
Go
package plugins
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// CopilotPlugin manages GitHub Copilot CLI native user hooks.
|
|
type CopilotPlugin struct {
|
|
rename renameFunc
|
|
}
|
|
|
|
func (p *CopilotPlugin) Install(config InstallConfig) error {
|
|
if config.AgentID == "" {
|
|
return fmt.Errorf("agent ID is required")
|
|
}
|
|
sources, err := readCopilotSources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sources.user.disabled {
|
|
return fmt.Errorf(
|
|
"GitHub Copilot user hooks are disabled by disableAllHooks at %s",
|
|
sources.user.filePath,
|
|
)
|
|
}
|
|
runtimeRoot, err := AgentRuntimeRoot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
expectedGuard := filepath.Join(agentDirectory, copilotGuardScript)
|
|
if !sameCopilotPath(config.GuardScriptPath, expectedGuard) {
|
|
return fmt.Errorf("Elydora guard runtime must use the managed agent directory: %s", expectedGuard)
|
|
}
|
|
if err := requireCopilotRuntime(expectedGuard, "Elydora guard runtime"); err != nil {
|
|
return err
|
|
}
|
|
auditPath := filepath.Join(agentDirectory, copilotAuditScript)
|
|
if config.HookScript != "" && !sameCopilotPath(config.HookScript, auditPath) {
|
|
return fmt.Errorf("Elydora audit runtime must use the managed agent directory: %s", auditPath)
|
|
}
|
|
nodePath, err := resolveNodeRuntime()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userHooks := removeManagedCopilotHooks(sources.user.hooks, runtimeRoot, "")
|
|
userHooks["preToolUse"] = append(
|
|
userHooks["preToolUse"],
|
|
buildCopilotHandler(nodePath, expectedGuard),
|
|
)
|
|
userHooks["postToolUse"] = append(
|
|
userHooks["postToolUse"],
|
|
buildCopilotHandler(nodePath, auditPath),
|
|
)
|
|
userRendered, err := renderCopilotDocument(sources.user, userHooks)
|
|
if err != nil {
|
|
return fmt.Errorf("render GitHub Copilot user hooks: %w", err)
|
|
}
|
|
rendered := []*copilotRenderedDocument{userRendered}
|
|
if sources.legacy != nil {
|
|
legacyHooks := removeManagedCopilotHooks(sources.legacy.hooks, runtimeRoot, "")
|
|
legacyRendered, renderErr := renderCopilotDocument(sources.legacy, legacyHooks)
|
|
if renderErr != nil {
|
|
return fmt.Errorf("render GitHub Copilot project hooks: %w", renderErr)
|
|
}
|
|
rendered = append(rendered, legacyRendered)
|
|
}
|
|
changes, err := prepareCopilotInstallationChanges(
|
|
config,
|
|
agentDirectory,
|
|
auditPath,
|
|
rendered,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writeChanges(changes, "Install GitHub Copilot hooks", p.rename); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf(" GitHub Copilot CLI hooks: %s\n", sources.user.filePath)
|
|
return nil
|
|
}
|
|
|
|
func (p *CopilotPlugin) Uninstall(agentID string) error {
|
|
sources, err := readCopilotSources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtimeRoot, err := AgentRuntimeRoot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
documents := []*copilotDocument{sources.user}
|
|
if sources.legacy != nil {
|
|
documents = append(documents, sources.legacy)
|
|
}
|
|
changes := make([]*fileChange, 0, len(documents))
|
|
for _, document := range documents {
|
|
hooks := removeManagedCopilotHooks(document.hooks, runtimeRoot, agentID)
|
|
rendered, renderErr := renderCopilotDocument(document, hooks)
|
|
if renderErr != nil {
|
|
return fmt.Errorf("render GitHub Copilot hook source: %w", renderErr)
|
|
}
|
|
change, changeErr := prepareRenderedCopilotChange(rendered)
|
|
if changeErr != nil {
|
|
return changeErr
|
|
}
|
|
changes = append(changes, change)
|
|
}
|
|
return writeChanges(changes, "Uninstall GitHub Copilot hooks", p.rename)
|
|
}
|
|
|
|
func (p *CopilotPlugin) Status() (PluginStatus, error) {
|
|
userPath, _, pathErr := copilotConfigPaths()
|
|
entry := SupportedAgents[copilotAgentKey]
|
|
status := PluginStatus{
|
|
AgentName: copilotAgentKey, DisplayName: entry.Name, ConfigPath: userPath,
|
|
}
|
|
if pathErr != nil {
|
|
return status, pathErr
|
|
}
|
|
sources, err := readCopilotSources()
|
|
if err != nil {
|
|
return status, err
|
|
}
|
|
runtimeRoot, err := AgentRuntimeRoot()
|
|
if err != nil {
|
|
return status, err
|
|
}
|
|
contracts := activeCopilotContracts(sources, runtimeRoot)
|
|
status.ConfigPath = configuredCopilotPath(sources, runtimeRoot)
|
|
status.HookConfigured = len(contracts) > 0
|
|
if !status.HookConfigured {
|
|
return status, nil
|
|
}
|
|
status.HookScriptExists, err = copilotRuntimeFilesExist(contracts)
|
|
if err != nil {
|
|
return status, err
|
|
}
|
|
status.Installed = status.HookScriptExists
|
|
return status, nil
|
|
}
|
|
|
|
func requireCopilotRuntime(path, label string) error {
|
|
if path == "" {
|
|
return fmt.Errorf("%s path is required", label)
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("%s is missing: %s", label, path)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("read %s at %s: %w", label, path, err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return fmt.Errorf("%s path is not a physical file: %s", label, path)
|
|
}
|
|
return nil
|
|
}
|