Files
Elydora-Go-SDK/cmd/elydora/plugins/cline_io.go
T

212 lines
6.0 KiB
Go

package plugins
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
type clinePendingWrite struct {
state clineHookFile
source string
}
type clineStagedFile struct {
state clineHookFile
temporaryPath string
}
type clineRenameFunc func(source, destination string) error
func readClineHookFile(filePath string) (clineHookFile, error) {
// #nosec G304 -- filePath is resolved from Cline's native hook directory.
raw, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
return clineHookFile{filePath: filePath}, nil
}
return clineHookFile{}, fmt.Errorf("read Cline hook at %s: %w", filePath, err)
}
source := string(raw)
metadata, err := parseClineMetadata(filePath, source)
if err != nil {
return clineHookFile{}, err
}
return clineHookFile{
exists: true,
filePath: filePath,
source: source,
metadata: metadata,
}, nil
}
func requireAvailableClineHook(file clineHookFile) error {
if file.exists && file.metadata == nil {
return fmt.Errorf(
"Cline hook at %s already exists and is owned by another integration",
file.filePath,
)
}
return nil
}
func removeClineTemporary(path string) error {
if path == "" {
return nil
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove temporary Cline hook at %s: %w", path, err)
}
return nil
}
func failClineStage(temporary *os.File, path string, cause error) error {
closeErr := temporary.Close()
removeErr := removeClineTemporary(path)
return errors.Join(cause, closeErr, removeErr)
}
func stageClineHook(write clinePendingWrite) (clineStagedFile, error) {
directory := filepath.Dir(write.state.filePath)
if err := os.MkdirAll(directory, 0700); err != nil {
return clineStagedFile{}, fmt.Errorf("create Cline hooks directory at %s: %w", directory, err)
}
temporary, err := os.CreateTemp(directory, "."+filepath.Base(write.state.filePath)+".*.tmp")
if err != nil {
return clineStagedFile{}, fmt.Errorf("stage Cline hook at %s: %w", write.state.filePath, err)
}
temporaryPath := temporary.Name()
if err := temporary.Chmod(0700); err != nil {
return clineStagedFile{}, failClineStage(
temporary,
temporaryPath,
fmt.Errorf("set permissions for %s: %w", temporaryPath, err),
)
}
written, err := temporary.WriteString(write.source)
if err != nil {
return clineStagedFile{}, failClineStage(
temporary,
temporaryPath,
fmt.Errorf("write temporary Cline hook for %s: %w", write.state.filePath, err),
)
}
if written != len(write.source) {
return clineStagedFile{}, failClineStage(
temporary,
temporaryPath,
fmt.Errorf("write temporary Cline hook for %s: %w", write.state.filePath, io.ErrShortWrite),
)
}
if err := temporary.Sync(); err != nil {
return clineStagedFile{}, failClineStage(
temporary,
temporaryPath,
fmt.Errorf("sync temporary Cline hook for %s: %w", write.state.filePath, err),
)
}
if err := temporary.Close(); err != nil {
return clineStagedFile{}, errors.Join(
fmt.Errorf("close temporary Cline hook for %s: %w", write.state.filePath, err),
removeClineTemporary(temporaryPath),
)
}
return clineStagedFile{state: write.state, temporaryPath: temporaryPath}, nil
}
func rollbackClineHook(state clineHookFile) error {
if state.exists {
return writeHookFileAtomic(state.filePath, []byte(state.source), 0700)
}
return removeHookFile(state.filePath, "Cline hook rollback")
}
func writeClineHookPair(guard, audit clinePendingWrite) error {
return writeClineHookPairWithRename(guard, audit, os.Rename)
}
func writeClineHookPairWithRename(
guard, audit clinePendingWrite,
rename clineRenameFunc,
) error {
writes := []clinePendingWrite{guard, audit}
staged := make([]clineStagedFile, 0, len(writes))
for _, write := range writes {
file, err := stageClineHook(write)
if err != nil {
failures := []error{fmt.Errorf("stage Cline hook pair: %w", err)}
for _, item := range staged {
if cleanupErr := removeClineTemporary(item.temporaryPath); cleanupErr != nil {
failures = append(failures, cleanupErr)
}
}
return errors.Join(failures...)
}
staged = append(staged, file)
}
committed := 0
for index, item := range staged {
if err := rename(item.temporaryPath, item.state.filePath); err != nil {
failures := []error{
fmt.Errorf("write Cline hook pair at %s: %w", item.state.filePath, err),
}
for rollbackIndex := committed - 1; rollbackIndex >= 0; rollbackIndex-- {
if rollbackErr := rollbackClineHook(staged[rollbackIndex].state); rollbackErr != nil {
failures = append(failures, fmt.Errorf(
"restore Cline hook at %s: %w",
staged[rollbackIndex].state.filePath,
rollbackErr,
))
}
}
for cleanupIndex := index; cleanupIndex < len(staged); cleanupIndex++ {
if cleanupErr := removeClineTemporary(staged[cleanupIndex].temporaryPath); cleanupErr != nil {
failures = append(failures, cleanupErr)
}
}
return errors.Join(failures...)
}
committed++
}
return nil
}
func removeOwnedClineHooks(files []clineHookFile, agentID string) error {
for _, file := range files {
if file.metadata == nil {
continue
}
if agentID != "" && !sameClineAgentID(file.metadata.AgentID, agentID) {
continue
}
if err := removeHookFile(file.filePath, "Cline hook"); err != nil {
return err
}
}
return nil
}
func clineRuntimeFilesExist(contract *clineRuntimeContract) (bool, error) {
configPath := filepath.Join(contract.agentDirectory, "config.json")
config, exists, err := readHookJSONObject(configPath, "Elydora runtime config")
if err != nil || !exists {
return false, err
}
agentID, ok := config["agent_id"].(string)
if config["agent_name"] != clineAgentKey || !ok || !sameClineAgentID(agentID, contract.agentID) {
return false, nil
}
guardExists, err := regularFileExists(contract.guardPath, "Elydora guard runtime")
if err != nil {
return false, err
}
auditExists, err := regularFileExists(contract.auditPath, "Elydora audit runtime")
if err != nil {
return false, err
}
return guardExists && auditExists, nil
}