Move the four providers onto managed_runtime.go and shell_command.go; keep the Auggie wrappers, Cline metadata wrappers, and the Copilot and Droid precondition handling provider-local. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
256 lines
7.0 KiB
Go
256 lines
7.0 KiB
Go
package plugins
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type copilotSettingsLayer struct {
|
|
filePath string
|
|
label string
|
|
jsonc bool
|
|
}
|
|
|
|
type copilotDirectoryLocation struct {
|
|
path string
|
|
label string
|
|
}
|
|
|
|
type copilotPaths struct {
|
|
copilotHome string
|
|
userHooksDirectory string
|
|
userHookPath string
|
|
legacyHookPath string
|
|
settingsLayers []copilotSettingsLayer
|
|
directories []copilotDirectoryLocation
|
|
}
|
|
|
|
type parsedCopilotSettings struct {
|
|
layer copilotSettingsLayer
|
|
disabled *bool
|
|
snapshot *managedFileSnapshot
|
|
}
|
|
|
|
func resolveCopilotPaths() (*copilotPaths, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve home directory: %w", err)
|
|
}
|
|
override := os.Getenv("COPILOT_HOME")
|
|
copilotHome := override
|
|
if strings.TrimSpace(override) == "" {
|
|
copilotHome = filepath.Join(home, ".copilot")
|
|
} else if !filepath.IsAbs(copilotHome) {
|
|
copilotHome, err = filepath.Abs(copilotHome)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve COPILOT_HOME at %s: %w", override, err)
|
|
}
|
|
}
|
|
project, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve working directory: %w", err)
|
|
}
|
|
github := filepath.Join(project, ".github")
|
|
githubCopilot := filepath.Join(github, "copilot")
|
|
githubHooks := filepath.Join(github, "hooks")
|
|
claude := filepath.Join(project, ".claude")
|
|
userHooks := filepath.Join(copilotHome, "hooks")
|
|
return &copilotPaths{
|
|
copilotHome: copilotHome,
|
|
userHooksDirectory: userHooks,
|
|
userHookPath: filepath.Join(userHooks, copilotConfigFile),
|
|
legacyHookPath: filepath.Join(githubHooks, "hooks.json"),
|
|
settingsLayers: []copilotSettingsLayer{
|
|
{filepath.Join(copilotHome, "config.json"), "legacy Copilot user config", false},
|
|
{filepath.Join(copilotHome, "settings.json"), "Copilot user settings", true},
|
|
{filepath.Join(claude, "settings.json"), "Claude repository settings", true},
|
|
{filepath.Join(claude, "settings.local.json"), "Claude local settings", true},
|
|
{filepath.Join(githubCopilot, "settings.json"), "Copilot repository settings", true},
|
|
{filepath.Join(githubCopilot, "settings.local.json"), "Copilot local settings", true},
|
|
},
|
|
directories: []copilotDirectoryLocation{
|
|
{project, "Copilot working directory"},
|
|
{copilotHome, "COPILOT_HOME"},
|
|
{userHooks, "Copilot user hooks directory"},
|
|
{github, "GitHub configuration directory"},
|
|
{githubHooks, "GitHub repository hooks directory"},
|
|
{githubCopilot, "Copilot repository settings directory"},
|
|
{claude, "Claude repository settings directory"},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func inspectCopilotDirectories(locations []copilotDirectoryLocation) error {
|
|
for _, location := range locations {
|
|
if _, err := managedPhysicalDirectoryExists(location.path, location.label); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readCopilotDocument(filePath, label string) (*copilotDocument, error) {
|
|
snapshot, err := readManagedFile(filePath, label, maxManagedSourceBytes)
|
|
if err != nil || snapshot == nil {
|
|
return nil, err
|
|
}
|
|
return parseCopilotDocument(filePath, snapshot, label)
|
|
}
|
|
|
|
func parseCopilotSettings(
|
|
snapshot *managedFileSnapshot,
|
|
layer copilotSettingsLayer,
|
|
) (map[string]any, error) {
|
|
if len(strings.TrimSpace(string(snapshot.contents))) == 0 {
|
|
return map[string]any{}, nil
|
|
}
|
|
label := fmt.Sprintf("%s at %s", layer.label, layer.filePath)
|
|
if layer.jsonc {
|
|
return decodeJSONCObject(snapshot.contents, label, true)
|
|
}
|
|
return decodeStrictJSONObject(snapshot.contents, label)
|
|
}
|
|
|
|
func readCopilotSettingsLayer(
|
|
layer copilotSettingsLayer,
|
|
) (parsedCopilotSettings, error) {
|
|
snapshot, err := readManagedFile(
|
|
layer.filePath,
|
|
layer.label,
|
|
maxManagedSourceBytes,
|
|
)
|
|
if err != nil || snapshot == nil {
|
|
return parsedCopilotSettings{layer: layer}, err
|
|
}
|
|
root, err := parseCopilotSettings(snapshot, layer)
|
|
if err != nil {
|
|
return parsedCopilotSettings{}, err
|
|
}
|
|
result := parsedCopilotSettings{layer: layer, snapshot: snapshot}
|
|
if value, exists := root["disableAllHooks"]; exists {
|
|
disabled, ok := value.(bool)
|
|
if !ok {
|
|
return parsedCopilotSettings{}, fmt.Errorf(
|
|
`%s at %s field "disableAllHooks" must be a boolean`,
|
|
layer.label,
|
|
layer.filePath,
|
|
)
|
|
}
|
|
result.disabled = &disabled
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func effectiveCopilotDisabledSource(layers []parsedCopilotSettings) string {
|
|
disabledBy := ""
|
|
for _, layer := range layers {
|
|
if layer.disabled == nil {
|
|
continue
|
|
}
|
|
if *layer.disabled {
|
|
disabledBy = fmt.Sprintf("%s at %s", layer.layer.label, layer.layer.filePath)
|
|
} else {
|
|
disabledBy = ""
|
|
}
|
|
}
|
|
return disabledBy
|
|
}
|
|
|
|
func readCopilotSources() (*copilotSources, *copilotPaths, error) {
|
|
paths, err := resolveCopilotPaths()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := inspectCopilotDirectories(paths.directories); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
user, err := readCopilotDocument(paths.userHookPath, "GitHub Copilot user hooks")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
legacy, err := readCopilotDocument(
|
|
paths.legacyHookPath,
|
|
"GitHub Copilot legacy project hooks",
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
settings := make([]parsedCopilotSettings, 0, len(paths.settingsLayers))
|
|
for _, layer := range paths.settingsLayers {
|
|
parsed, readErr := readCopilotSettingsLayer(layer)
|
|
if readErr != nil {
|
|
return nil, nil, readErr
|
|
}
|
|
settings = append(settings, parsed)
|
|
}
|
|
if user == nil {
|
|
user = createCopilotDocument(paths.userHookPath)
|
|
}
|
|
disabledBy := effectiveCopilotDisabledSource(settings)
|
|
if user.hooksDisabled {
|
|
disabledBy = fmt.Sprintf("GitHub Copilot user hooks at %s", paths.userHookPath)
|
|
}
|
|
preconditions := make([]copilotSourcePrecondition, 0, len(settings))
|
|
for _, item := range settings {
|
|
preconditions = append(preconditions, copilotSourcePrecondition{
|
|
filePath: item.layer.filePath,
|
|
label: item.layer.label,
|
|
snapshot: item.snapshot,
|
|
})
|
|
}
|
|
return &copilotSources{
|
|
user: user, legacy: legacy, disabledBy: disabledBy,
|
|
settingsPreconditions: preconditions,
|
|
}, paths, nil
|
|
}
|
|
|
|
func requireCopilotHooksEnabled(sources *copilotSources) error {
|
|
if sources.disabledBy == "" {
|
|
return nil
|
|
}
|
|
return fmt.Errorf(
|
|
"GitHub Copilot hooks are disabled by %s; set disableAllHooks to false before installation",
|
|
sources.disabledBy,
|
|
)
|
|
}
|
|
|
|
func prepareRenderedCopilotChange(
|
|
rendered *copilotRenderedDocument,
|
|
) (*fileChange, error) {
|
|
if rendered == nil || !rendered.changed {
|
|
return nil, nil
|
|
}
|
|
return prepareSnapshotSourceChange(
|
|
rendered.document.filePath,
|
|
"GitHub Copilot hook source",
|
|
rendered.document.snapshot,
|
|
rendered.next,
|
|
0600,
|
|
rendered.remove,
|
|
)
|
|
}
|
|
|
|
func writeCopilotChanges(
|
|
changes []*fileChange,
|
|
label string,
|
|
rename renameFunc,
|
|
runtimeRoot string,
|
|
agentDirectory string,
|
|
preconditions []filePrecondition,
|
|
) error {
|
|
if !hasFileChanges(changes) {
|
|
return writeChanges(changes, label, rename, preconditions...)
|
|
}
|
|
if agentDirectory != "" {
|
|
if err := EnsurePrivateDirectory(runtimeRoot); err != nil {
|
|
return err
|
|
}
|
|
if err := EnsurePrivateDirectory(agentDirectory); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return writeChanges(changes, label, rename, preconditions...)
|
|
}
|