157 lines
4.0 KiB
Go
157 lines
4.0 KiB
Go
package plugins
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
func kimiHomeDirectory() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve home directory: %w", err)
|
|
}
|
|
return home, nil
|
|
}
|
|
|
|
func isMissingKimiPath(err error) bool {
|
|
return os.IsNotExist(err) || errors.Is(err, syscall.ENOTDIR)
|
|
}
|
|
|
|
func kimiPathExists(path, label string) (bool, error) {
|
|
// #nosec G703 -- KIMI_CODE_HOME is an explicit local-user configuration path.
|
|
_, err := os.Stat(path)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if isMissingKimiPath(err) {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("inspect %s at %s: %w", label, path, err)
|
|
}
|
|
|
|
func legacyKimiCLIOnPath() (bool, error) {
|
|
_, err := exec.LookPath("kimi-cli")
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("inspect kimi-cli executable on PATH: %w", err)
|
|
}
|
|
|
|
func resolveKimiContracts() ([]kimiContract, error) {
|
|
home, err := kimiHomeDirectory()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
explicitHome := os.Getenv("KIMI_CODE_HOME")
|
|
kimiHome := explicitHome
|
|
if kimiHome == "" {
|
|
kimiHome = filepath.Join(home, ".kimi-code")
|
|
}
|
|
modern := kimiContract{
|
|
runtimeName: "Kimi Code",
|
|
label: "Kimi Code hooks config",
|
|
configPath: filepath.Join(kimiHome, "config.toml"),
|
|
events: kimiModernEvents,
|
|
}
|
|
legacy := kimiContract{
|
|
runtimeName: "kimi-cli",
|
|
label: "kimi-cli legacy hooks config",
|
|
configPath: filepath.Join(home, ".kimi", "config.toml"),
|
|
events: kimiSharedEvents,
|
|
}
|
|
modernDetected := explicitHome != ""
|
|
if !modernDetected {
|
|
modernDetected, err = kimiPathExists(kimiHome, "Kimi Code home")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
legacyDetected, err := kimiPathExists(legacy.configPath, legacy.label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !legacyDetected {
|
|
legacyDetected, err = legacyKimiCLIOnPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if legacyDetected && !modernDetected {
|
|
return []kimiContract{legacy}, nil
|
|
}
|
|
if legacyDetected {
|
|
return []kimiContract{modern, legacy}, nil
|
|
}
|
|
return []kimiContract{modern}, nil
|
|
}
|
|
|
|
func readKimiConfig(contract kimiContract) (kimiDocument, error) {
|
|
raw, err := os.ReadFile(contract.configPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return parseKimiDocument(contract, nil, false)
|
|
}
|
|
return kimiDocument{}, fmt.Errorf("read %s at %s: %w", contract.label, contract.configPath, err)
|
|
}
|
|
return parseKimiDocument(contract, raw, true)
|
|
}
|
|
|
|
func readAllKimiConfigs() ([]kimiDocument, error) {
|
|
contracts, err := resolveKimiContracts()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
documents := make([]kimiDocument, 0, len(contracts))
|
|
for _, contract := range contracts {
|
|
document, err := readKimiConfig(contract)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
documents = append(documents, document)
|
|
}
|
|
return documents, nil
|
|
}
|
|
|
|
func writeKimiConfig(document kimiDocument, raw []byte) error {
|
|
if err := writeHookFileAtomic(document.contract.configPath, raw, 0600); err != nil {
|
|
return fmt.Errorf("write %s at %s: %w", document.contract.label, document.contract.configPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func removeKimiConfig(document kimiDocument) error {
|
|
if err := removeHookFile(document.contract.configPath, document.contract.label); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildKimiCommand(runtimePath, scriptPath string) string {
|
|
if runtime.GOOS == "windows" {
|
|
return quoteWindowsArgument(runtimePath) + " " + quoteWindowsArgument(scriptPath)
|
|
}
|
|
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath)
|
|
}
|
|
|
|
func buildKimiHook(event, command string) kimiHook {
|
|
timeout := kimiHookTimeoutSeconds
|
|
return kimiHook{event: event, command: command, timeout: &timeout}
|
|
}
|
|
|
|
func kimiRuntimeNames(documents []kimiDocument) string {
|
|
names := make([]string, 0, len(documents))
|
|
for _, document := range documents {
|
|
names = append(names, document.contract.runtimeName)
|
|
}
|
|
return strings.Join(names, " and ")
|
|
}
|