package config import ( "flag" "os" "path/filepath" "runtime" "strings" ) type Config struct { Port int AgentDir string RepoRoot string // pi 仓库根目录(pi CLI 子进程的工作目录) DataDir string // sproutclaw-web 自己的数据目录(SQLite 等) PiCmd string // pi CLI 启动命令(完整路径) PiArgs []string // 派生路径(从 AgentDir 推导) SessionsDir string SystemPromptFile string ModelsConfigFile string McpConfigFile string McpCacheFile string SettingsFile string SkillsDir string SkillsDisabledDir string ExtensionsDir string PromptsDir string FrontendDist string // ../frontend/dist 相对于可执行文件 } func New() *Config { port := flag.Int("port", 19133, "HTTP port") agentDir := flag.String("agent-dir", "", "SproutClaw agent directory (e.g. /path/to/.sproutclaw/agent)") repoRoot := flag.String("repo-root", "", "SproutClaw repo root (CLI cwd); defaults to two levels above agent-dir") dataDir := flag.String("data-dir", "", "sproutclaw-web data dir for SQLite (defaults to ./data)") piCmd := flag.String("pi-cmd", "", "pi CLI command (may include args, e.g. \"node /path/tsx.mjs /path/cli.ts\")") frontendDist := flag.String("frontend-dist", "", "path to frontend/dist (defaults to ../frontend/dist relative to binary)") flag.Parse() if *agentDir == "" { cwd, _ := os.Getwd() *agentDir = filepath.Join(cwd, ".sproutclaw", "agent") } cfg := &Config{ Port: *port, AgentDir: *agentDir, RepoRoot: *repoRoot, DataDir: *dataDir, } // repoRoot defaults to the directory two levels above agentDir (.sproutclaw/agent -> repo root) if cfg.RepoRoot == "" { cfg.RepoRoot = filepath.Dir(filepath.Dir(*agentDir)) } // dataDir defaults to ./data relative to cwd (run-backend.bat cd's into backend/) if cfg.DataDir == "" { cwd, _ := os.Getwd() cfg.DataDir = filepath.Join(cwd, "data") } if abs, err := filepath.Abs(cfg.DataDir); err == nil { cfg.DataDir = abs } // --pi-cmd may include arguments separated by spaces, e.g. // "node /path/to/tsx/dist/cli.mjs /path/to/pi/cli.ts" // Split into executable + args so exec.Command receives them correctly. if parts := strings.Fields(*piCmd); len(parts) > 0 { cfg.PiCmd = parts[0] cfg.PiArgs = parts[1:] } cfg.derivePaths() // Override frontend dist if explicitly specified if *frontendDist != "" { cfg.FrontendDist = *frontendDist } return cfg } func (c *Config) derivePaths() { a := c.AgentDir c.SessionsDir = filepath.Join(a, "sessions") c.SystemPromptFile = filepath.Join(a, "AGENTS.md") c.ModelsConfigFile = filepath.Join(a, "models.json") c.McpConfigFile = filepath.Join(a, "mcp.json") c.McpCacheFile = filepath.Join(a, "mcp-cache.json") c.SettingsFile = filepath.Join(a, "settings.json") c.SkillsDir = filepath.Join(a, "skills") c.SkillsDisabledDir = filepath.Join(a, "skills-disabled") c.ExtensionsDir = filepath.Join(a, "extensions") c.PromptsDir = filepath.Join(a, "prompts") // frontend/dist 相对于二进制所在目录的上级 exe, err := os.Executable() if err != nil { exe, _ = os.Getwd() } exeDir := filepath.Dir(exe) c.FrontendDist = filepath.Join(exeDir, "..", "frontend", "dist") } func Platform() string { return runtime.GOOS } func Arch() string { return runtime.GOARCH }