feat: client sync core — config, pull, inventory, manifest, OpenCode target
- admin-protected client config (flag > AGENT_LIB_CONFIG > platform default), token used for basic auth only, redacted from all errors and files - work-repo pull via go-git (bare cache clone + fetch, remote re-pointed on config change), clean failure keeps previous state - inventory: own items + external areas from lockfile; MCP inventoried but never deployed - manifest v1 records name/type/origin/revision/content-hash per item, deterministic bytes; folder hashes over sorted relpath+filehash lines so tree and disk hashes agree - shared deploy package (atomic temp+rename, exec-bit preserving) now backs both curator materialization and client deployment - sync validates and hashes everything before the first mutation; second run is a byte-level no-op
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
// Package client implements the employee side: config loading, work-repo
|
||||
// pull, inventory, manifest and deployment into the OpenCode target.
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// ClientConfig is the admin-protected fleet configuration for sync.
|
||||
type ClientConfig struct {
|
||||
RepoURL string `json:"repo_url"`
|
||||
Ref string `json:"ref,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
TokenUser string `json:"token_user,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigEnv names the override environment variables.
|
||||
const (
|
||||
ConfigEnv = "AGENT_LIB_CONFIG"
|
||||
StateDirEnv = "AGENT_LIB_STATE_DIR"
|
||||
defaultConfig = `{
|
||||
"repo_url": "",
|
||||
"ref": "",
|
||||
"token": "",
|
||||
"token_user": ""
|
||||
}`
|
||||
)
|
||||
|
||||
// DefaultConfigPath returns the admin-protected config location per platform:
|
||||
// ProgramData on Windows, /etc on Linux/macOS.
|
||||
func DefaultConfigPath() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
if pd := os.Getenv("ProgramData"); pd != "" {
|
||||
return filepath.Join(pd, "agent-lib", "config.json")
|
||||
}
|
||||
return `C:\ProgramData\agent-lib\config.json`
|
||||
}
|
||||
return "/etc/agent-lib/config.json"
|
||||
}
|
||||
|
||||
// LoadClientConfig reads the config from flagPath, the override env, or the
|
||||
// platform default — in that order.
|
||||
func LoadClientConfig(flagPath string) (*ClientConfig, error) {
|
||||
path := flagPath
|
||||
if path == "" {
|
||||
path = os.Getenv(ConfigEnv)
|
||||
}
|
||||
if path == "" {
|
||||
path = DefaultConfigPath()
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading client config %s: %w", path, err)
|
||||
}
|
||||
var cfg ClientConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("client config %s is not valid JSON: %w", path, err)
|
||||
}
|
||||
if cfg.RepoURL == "" {
|
||||
return nil, fmt.Errorf("client config %s: repo_url is required", path)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// StateDir returns the client state directory: the override env, then the
|
||||
// platform default (LocalAppData on Windows, XDG state elsewhere).
|
||||
func StateDir() (string, error) {
|
||||
if d := os.Getenv(StateDirEnv); d != "" {
|
||||
return d, nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
base := os.Getenv("LocalAppData")
|
||||
if base == "" {
|
||||
return "", fmt.Errorf("LocalAppData is not set")
|
||||
}
|
||||
return filepath.Join(base, "agent-lib"), nil
|
||||
}
|
||||
if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" {
|
||||
return filepath.Join(xdg, "agent-lib"), nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, ".local", "state", "agent-lib"), nil
|
||||
}
|
||||
|
||||
// StatePaths bundles the well-known files inside the state dir.
|
||||
type StatePaths struct {
|
||||
Root string
|
||||
CacheDir string
|
||||
Manifest string
|
||||
LogFile string
|
||||
}
|
||||
|
||||
func ResolveStatePaths() (StatePaths, error) {
|
||||
root, err := StateDir()
|
||||
if err != nil {
|
||||
return StatePaths{}, err
|
||||
}
|
||||
return StatePaths{
|
||||
Root: root,
|
||||
CacheDir: filepath.Join(root, "work-repo"),
|
||||
Manifest: filepath.Join(root, "manifest.json"),
|
||||
LogFile: filepath.Join(root, "sync.log"),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user