From e2c5c4aa1df6e564c336290182ce1429e08b6a01 Mon Sep 17 00:00:00 2001 From: Yoftahe Date: Mon, 23 Feb 2026 17:30:54 +0300 Subject: [PATCH] chore: Detect interactive mod and View only TUI for basic onboard --- cmd/picoclaw/cmd_onboard.go | 51 ++++++------ cmd/picoclaw/main.go | 2 +- pkg/setup/setup.go | 150 ++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 23 deletions(-) diff --git a/cmd/picoclaw/cmd_onboard.go b/cmd/picoclaw/cmd_onboard.go index 8d02df613..70a9db40a 100644 --- a/cmd/picoclaw/cmd_onboard.go +++ b/cmd/picoclaw/cmd_onboard.go @@ -18,23 +18,37 @@ import ( //go:embed workspace var embeddedFiles embed.FS -func onboard() { +func onboard(args []string) { configPath := getConfigPath() - // Use the new setup package to run interactive setup and ask for missing values. + interactive := false + for _, arg := range args { + if arg == "--interactive" || arg == "-i" { + interactive = true + break + } + } + s, err := setup.NewSetup(configPath) if err != nil { fmt.Printf("Error initializing setup: %v\n", err) os.Exit(1) } - if err := s.Run(); err != nil { - fmt.Printf("TUI error: %v\n", err) - } + if interactive { + if err := s.Run(); err != nil { + fmt.Printf("TUI error: %v\n", err) + } - if err := s.AskMissing(); err != nil { - fmt.Printf("Error updating config: %v\n", err) - os.Exit(1) + if err := s.AskMissing(); err != nil { + fmt.Printf("Error updating config: %v\n", err) + os.Exit(1) + } + } else { + if err := s.RunNonInteractive(); err != nil { + fmt.Printf("Error running non-interactive setup: %v\n", err) + os.Exit(1) + } } cfg := s.Cfg @@ -47,44 +61,37 @@ func onboard() { } func copyEmbeddedToTarget(targetDir string) error { - // Ensure target directory exists if err := os.MkdirAll(targetDir, 0o755); err != nil { - return fmt.Errorf("Failed to create target directory: %w", err) + return fmt.Errorf("failed to create target directory: %w", err) } - // Walk through all files in embed.FS err := fs.WalkDir(embeddedFiles, "workspace", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } - // Skip directories if d.IsDir() { return nil } - // Read embedded file data, err := embeddedFiles.ReadFile(path) if err != nil { - return fmt.Errorf("Failed to read embedded file %s: %w", path, err) + return fmt.Errorf("failed to read embedded file %s: %w", path, err) } - new_path, err := filepath.Rel("workspace", path) + newPath, err := filepath.Rel("workspace", path) if err != nil { - return fmt.Errorf("Failed to get relative path for %s: %v\n", path, err) + return fmt.Errorf("failed to get relative path for %s: %v\n", path, err) } - // Build target file path - targetPath := filepath.Join(targetDir, new_path) + targetPath := filepath.Join(targetDir, newPath) - // Ensure target file's directory exists if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { - return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err) + return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(targetPath), err) } - // Write file if err := os.WriteFile(targetPath, data, 0o644); err != nil { - return fmt.Errorf("Failed to write file %s: %w", targetPath, err) + return fmt.Errorf("failed to write file %s: %w", targetPath, err) } return nil diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 1e4b393f8..0cf8a9622 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -102,7 +102,7 @@ func main() { switch command { case "onboard": - onboard() + onboard(os.Args[2:]) case "agent": agentCmd() case "gateway": diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go index ca18681eb..ab4a8e0f0 100644 --- a/pkg/setup/setup.go +++ b/pkg/setup/setup.go @@ -100,6 +100,156 @@ func (s *Setup) AskMissing() error { return nil } +func (s *Setup) RunNonInteractive() error { + cfg := s.Cfg + + if cfg.Agents.Defaults.Workspace == "" { + cfg.Agents.Defaults.Workspace = "~/.picoclaw/workspace" + } + + workspace := cfg.WorkspacePath() + + titleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true) + successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("82")).Bold(true) + dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("244")) + highlightStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) + sectionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true) + + var b strings.Builder + + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(titleStyle.Render("╔═══════════════════════════════════════════════════════╗"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(titleStyle.Render("║ PicoClaw Setup Complete ║"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(titleStyle.Render("╚═══════════════════════════════════════════════════════╝"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(successStyle.Render("✓ Workspace config created and is working!"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(sectionStyle.Render("📁 Workspace:"))) + b.WriteString(" ") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(workspace))) + b.WriteString("\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(sectionStyle.Render("⚙ Config:"))) + b.WriteString(" ") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(s.ConfigPath))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(sectionStyle.Render("📋 IMPORTANT COMMANDS"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(highlightStyle.Render(" picoclaw agent - Start chatting with the agent"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" picoclaw onboard --interactive - Full interactive setup"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" picoclaw auth login - Login to providers (openai, anthropic)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" picoclaw skills list - List available skills"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" picoclaw status - Show picoclaw status"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" picoclaw gateway - Start the gateway server"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(sectionStyle.Render("🔌 PROVIDERS (in config.json)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(highlightStyle.Render(" openai : OpenAI API key"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" anthropic : Anthropic/Claude API key"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" openrouter : OpenRouter (unified API)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" groq : Groq (fast inference)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" deepseek : DeepSeek API"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" ollama : Local models (http://localhost:11434)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" vllm : Local vLLM server"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" + more: gemini, nvidia, moonshot, qwen, etc."))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(sectionStyle.Render("💬 CHANNELS (in config.json)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(highlightStyle.Render(" telegram : Telegram bot (token from @BotFather)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" slack : Slack bot (bot_token, app_token)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" discord : Discord bot (token from dev portal)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" whatsapp : WhatsApp bridge (bridge_url)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" feishu : Feishu/Lark (app_id, app_secret)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" dingtalk : DingTalk (client_id, client_secret)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" line : LINE (channel_access_token)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" qq : QQ (app_id)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" onebot : OneBot (ws_url, access_token)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" wecom : WeCom (token, encoding_aes_key)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" wecom_app : WeCom App (corp_id, agent_id)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" maixcam : MaixCam device"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(sectionStyle.Render("🤖 MODELS (in model_list)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" Format: {\"model\": \"provider/model-id\", \"api_base\": \"url\"}"))) + b.WriteString("\n\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" Examples:"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" {\"model\": \"openai/gpt-4o\", \"api_base\": \"https://api.openai.com/v1\"}"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" {\"model\": \"anthropic/claude-sonnet-4-20250514\", \"api_base\": \"https://api.anthropic.com/v1\"}"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" {\"model\": \"deepseek/deepseek-chat\", \"api_base\": \"https://api.deepseek.com/v1\"}"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" {\"model\": \"ollama/llama3\", \"api_base\": \"http://localhost:11434/v1\"}"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(sectionStyle.Render("🚀 NEXT STEPS"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(highlightStyle.Render(" 1. Edit config.json to add your provider API key"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" 2. Enable and configure your desired channel(s)"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" 3. Run 'picoclaw auth login' for OAuth providers"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(successStyle.Render(" 4. Run 'picoclaw agent' to start chatting!"))) + b.WriteString("\n\n") + + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))) + b.WriteString("\n") + b.WriteString(lipgloss.NewStyle().PaddingLeft(4).Render(dimStyle.Render(" Run 'picoclaw onboard --interactive' for guided setup"))) + b.WriteString("\n\n") + + fmt.Println(b.String()) + + return nil +} + func saveConfigPath(path string, cfg *config.Config) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil {