mirror of
https://github.com/ollama/ollama.git
synced 2026-04-30 07:57:51 -05:00
cmd/config: ollama launch cline CLI (#14294)
This commit is contained in:
123
cmd/config/cline.go
Normal file
123
cmd/config/cline.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cline implements Runner and Editor for the Cline CLI integration
|
||||||
|
type Cline struct{}
|
||||||
|
|
||||||
|
func (c *Cline) String() string { return "Cline" }
|
||||||
|
|
||||||
|
func (c *Cline) Run(model string, args []string) error {
|
||||||
|
if _, err := exec.LookPath("cline"); err != nil {
|
||||||
|
return fmt.Errorf("cline is not installed, install with: npm install -g cline")
|
||||||
|
}
|
||||||
|
|
||||||
|
models := []string{model}
|
||||||
|
if config, err := loadIntegration("cline"); err == nil && len(config.Models) > 0 {
|
||||||
|
models = config.Models
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
models, err = resolveEditorModels("cline", models, func() ([]string, error) {
|
||||||
|
return selectModels(context.Background(), "cline", "")
|
||||||
|
})
|
||||||
|
if errors.Is(err, errCancelled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Edit(models); err != nil {
|
||||||
|
return fmt.Errorf("setup failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("cline", args...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cline) Paths() []string {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p := filepath.Join(home, ".cline", "data", "globalState.json")
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return []string{p}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cline) Edit(models []string) error {
|
||||||
|
if len(models) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(home, ".cline", "data", "globalState.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := make(map[string]any)
|
||||||
|
if data, err := os.ReadFile(configPath); err == nil {
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse config: %w, at: %s", err, configPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Ollama as the provider for both act and plan modes
|
||||||
|
baseURL := envconfig.Host().String()
|
||||||
|
config["ollamaBaseUrl"] = baseURL
|
||||||
|
config["actModeApiProvider"] = "ollama"
|
||||||
|
config["actModeOllamaModelId"] = models[0]
|
||||||
|
config["actModeOllamaBaseUrl"] = baseURL
|
||||||
|
config["planModeApiProvider"] = "ollama"
|
||||||
|
config["planModeOllamaModelId"] = models[0]
|
||||||
|
config["planModeOllamaBaseUrl"] = baseURL
|
||||||
|
|
||||||
|
config["welcomeViewCompleted"] = true
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeWithBackup(configPath, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cline) Models() []string {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := readJSONFile(filepath.Join(home, ".cline", "data", "globalState.json"))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if config["actModeApiProvider"] != "ollama" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
modelID, _ := config["actModeOllamaModelId"].(string)
|
||||||
|
if modelID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{modelID}
|
||||||
|
}
|
||||||
204
cmd/config/cline_test.go
Normal file
204
cmd/config/cline_test.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClineIntegration(t *testing.T) {
|
||||||
|
c := &Cline{}
|
||||||
|
|
||||||
|
t.Run("String", func(t *testing.T) {
|
||||||
|
if got := c.String(); got != "Cline" {
|
||||||
|
t.Errorf("String() = %q, want %q", got, "Cline")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("implements Runner", func(t *testing.T) {
|
||||||
|
var _ Runner = c
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("implements Editor", func(t *testing.T) {
|
||||||
|
var _ Editor = c
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClineEdit(t *testing.T) {
|
||||||
|
c := &Cline{}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
|
||||||
|
configDir := filepath.Join(tmpDir, ".cline", "data")
|
||||||
|
configPath := filepath.Join(configDir, "globalState.json")
|
||||||
|
|
||||||
|
readConfig := func() map[string]any {
|
||||||
|
data, _ := os.ReadFile(configPath)
|
||||||
|
var config map[string]any
|
||||||
|
json.Unmarshal(data, &config)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("creates config from scratch", func(t *testing.T) {
|
||||||
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
||||||
|
|
||||||
|
if err := c.Edit([]string{"kimi-k2.5:cloud"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := readConfig()
|
||||||
|
if config["actModeApiProvider"] != "ollama" {
|
||||||
|
t.Errorf("actModeApiProvider = %v, want ollama", config["actModeApiProvider"])
|
||||||
|
}
|
||||||
|
if config["actModeOllamaModelId"] != "kimi-k2.5:cloud" {
|
||||||
|
t.Errorf("actModeOllamaModelId = %v, want kimi-k2.5:cloud", config["actModeOllamaModelId"])
|
||||||
|
}
|
||||||
|
if config["planModeApiProvider"] != "ollama" {
|
||||||
|
t.Errorf("planModeApiProvider = %v, want ollama", config["planModeApiProvider"])
|
||||||
|
}
|
||||||
|
if config["planModeOllamaModelId"] != "kimi-k2.5:cloud" {
|
||||||
|
t.Errorf("planModeOllamaModelId = %v, want kimi-k2.5:cloud", config["planModeOllamaModelId"])
|
||||||
|
}
|
||||||
|
if config["welcomeViewCompleted"] != true {
|
||||||
|
t.Errorf("welcomeViewCompleted = %v, want true", config["welcomeViewCompleted"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preserves existing fields", func(t *testing.T) {
|
||||||
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
||||||
|
os.MkdirAll(configDir, 0o755)
|
||||||
|
|
||||||
|
existing := map[string]any{
|
||||||
|
"remoteRulesToggles": map[string]any{},
|
||||||
|
"remoteWorkflowToggles": map[string]any{},
|
||||||
|
"customSetting": "keep-me",
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(existing)
|
||||||
|
os.WriteFile(configPath, data, 0o644)
|
||||||
|
|
||||||
|
if err := c.Edit([]string{"glm-5:cloud"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := readConfig()
|
||||||
|
if config["customSetting"] != "keep-me" {
|
||||||
|
t.Errorf("customSetting was not preserved")
|
||||||
|
}
|
||||||
|
if config["actModeOllamaModelId"] != "glm-5:cloud" {
|
||||||
|
t.Errorf("actModeOllamaModelId = %v, want glm-5:cloud", config["actModeOllamaModelId"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates model on re-edit", func(t *testing.T) {
|
||||||
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
||||||
|
|
||||||
|
if err := c.Edit([]string{"kimi-k2.5:cloud"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := c.Edit([]string{"glm-5:cloud"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := readConfig()
|
||||||
|
if config["actModeOllamaModelId"] != "glm-5:cloud" {
|
||||||
|
t.Errorf("actModeOllamaModelId = %v, want glm-5:cloud", config["actModeOllamaModelId"])
|
||||||
|
}
|
||||||
|
if config["planModeOllamaModelId"] != "glm-5:cloud" {
|
||||||
|
t.Errorf("planModeOllamaModelId = %v, want glm-5:cloud", config["planModeOllamaModelId"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty models is no-op", func(t *testing.T) {
|
||||||
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
||||||
|
|
||||||
|
if err := c.Edit(nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
||||||
|
t.Error("expected no config file to be created for empty models")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("uses first model as primary", func(t *testing.T) {
|
||||||
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
||||||
|
|
||||||
|
if err := c.Edit([]string{"kimi-k2.5:cloud", "glm-5:cloud"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := readConfig()
|
||||||
|
if config["actModeOllamaModelId"] != "kimi-k2.5:cloud" {
|
||||||
|
t.Errorf("actModeOllamaModelId = %v, want kimi-k2.5:cloud (first model)", config["actModeOllamaModelId"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClineModels(t *testing.T) {
|
||||||
|
c := &Cline{}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
|
||||||
|
configDir := filepath.Join(tmpDir, ".cline", "data")
|
||||||
|
configPath := filepath.Join(configDir, "globalState.json")
|
||||||
|
|
||||||
|
t.Run("returns nil when no config", func(t *testing.T) {
|
||||||
|
if models := c.Models(); models != nil {
|
||||||
|
t.Errorf("Models() = %v, want nil", models)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns nil when provider is not ollama", func(t *testing.T) {
|
||||||
|
os.MkdirAll(configDir, 0o755)
|
||||||
|
config := map[string]any{
|
||||||
|
"actModeApiProvider": "anthropic",
|
||||||
|
"actModeOllamaModelId": "some-model",
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(config)
|
||||||
|
os.WriteFile(configPath, data, 0o644)
|
||||||
|
|
||||||
|
if models := c.Models(); models != nil {
|
||||||
|
t.Errorf("Models() = %v, want nil", models)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns model when ollama is configured", func(t *testing.T) {
|
||||||
|
os.MkdirAll(configDir, 0o755)
|
||||||
|
config := map[string]any{
|
||||||
|
"actModeApiProvider": "ollama",
|
||||||
|
"actModeOllamaModelId": "kimi-k2.5:cloud",
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(config)
|
||||||
|
os.WriteFile(configPath, data, 0o644)
|
||||||
|
|
||||||
|
models := c.Models()
|
||||||
|
if len(models) != 1 || models[0] != "kimi-k2.5:cloud" {
|
||||||
|
t.Errorf("Models() = %v, want [kimi-k2.5:cloud]", models)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClinePaths(t *testing.T) {
|
||||||
|
c := &Cline{}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setTestHome(t, tmpDir)
|
||||||
|
|
||||||
|
t.Run("returns nil when no config exists", func(t *testing.T) {
|
||||||
|
if paths := c.Paths(); paths != nil {
|
||||||
|
t.Errorf("Paths() = %v, want nil", paths)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns path when config exists", func(t *testing.T) {
|
||||||
|
configDir := filepath.Join(tmpDir, ".cline", "data")
|
||||||
|
os.MkdirAll(configDir, 0o755)
|
||||||
|
configPath := filepath.Join(configDir, "globalState.json")
|
||||||
|
os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||||
|
|
||||||
|
paths := c.Paths()
|
||||||
|
if len(paths) != 1 || paths[0] != configPath {
|
||||||
|
t.Errorf("Paths() = %v, want [%s]", paths, configPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -54,6 +53,7 @@ type AliasConfigurer interface {
|
|||||||
var integrations = map[string]Runner{
|
var integrations = map[string]Runner{
|
||||||
"claude": &Claude{},
|
"claude": &Claude{},
|
||||||
"clawdbot": &Openclaw{},
|
"clawdbot": &Openclaw{},
|
||||||
|
"cline": &Cline{},
|
||||||
"codex": &Codex{},
|
"codex": &Codex{},
|
||||||
"moltbot": &Openclaw{},
|
"moltbot": &Openclaw{},
|
||||||
"droid": &Droid{},
|
"droid": &Droid{},
|
||||||
@@ -102,16 +102,17 @@ var recommendedVRAM = map[string]string{
|
|||||||
var integrationAliases = map[string]bool{
|
var integrationAliases = map[string]bool{
|
||||||
"clawdbot": true,
|
"clawdbot": true,
|
||||||
"moltbot": true,
|
"moltbot": true,
|
||||||
"pi": true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// integrationInstallHints maps integration names to install URLs.
|
// integrationInstallHints maps integration names to install URLs.
|
||||||
var integrationInstallHints = map[string]string{
|
var integrationInstallHints = map[string]string{
|
||||||
"claude": "https://code.claude.com/docs/en/quickstart",
|
"claude": "https://code.claude.com/docs/en/quickstart",
|
||||||
|
"cline": "https://cline.bot/cli",
|
||||||
"openclaw": "https://docs.openclaw.ai",
|
"openclaw": "https://docs.openclaw.ai",
|
||||||
"codex": "https://developers.openai.com/codex/cli/",
|
"codex": "https://developers.openai.com/codex/cli/",
|
||||||
"droid": "https://docs.factory.ai/cli/getting-started/quickstart",
|
"droid": "https://docs.factory.ai/cli/getting-started/quickstart",
|
||||||
"opencode": "https://opencode.ai",
|
"opencode": "https://opencode.ai",
|
||||||
|
"pi": "https://github.com/badlogic/pi-mono",
|
||||||
}
|
}
|
||||||
|
|
||||||
// hyperlink wraps text in an OSC 8 terminal hyperlink so it is cmd+clickable.
|
// hyperlink wraps text in an OSC 8 terminal hyperlink so it is cmd+clickable.
|
||||||
@@ -129,13 +130,21 @@ type IntegrationInfo struct {
|
|||||||
// integrationDescriptions maps integration names to short descriptions.
|
// integrationDescriptions maps integration names to short descriptions.
|
||||||
var integrationDescriptions = map[string]string{
|
var integrationDescriptions = map[string]string{
|
||||||
"claude": "Anthropic's coding tool with subagents",
|
"claude": "Anthropic's coding tool with subagents",
|
||||||
|
"cline": "Autonomous coding agent with parallel execution",
|
||||||
"codex": "OpenAI's open-source coding agent",
|
"codex": "OpenAI's open-source coding agent",
|
||||||
"openclaw": "Personal AI with 100+ skills",
|
"openclaw": "Personal AI with 100+ skills",
|
||||||
"droid": "Factory's coding agent across terminal and IDEs",
|
"droid": "Factory's coding agent across terminal and IDEs",
|
||||||
"opencode": "Anomaly's open-source coding agent",
|
"opencode": "Anomaly's open-source coding agent",
|
||||||
|
"pi": "Minimal AI agent toolkit with plugin support",
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListIntegrationInfos returns all non-alias registered integrations, sorted by name.
|
// integrationOrder defines a custom display order for integrations.
|
||||||
|
// Integrations listed here are placed at the end in the given order;
|
||||||
|
// all others appear first, sorted alphabetically.
|
||||||
|
var integrationOrder = []string{"opencode", "droid", "pi", "cline"}
|
||||||
|
|
||||||
|
// ListIntegrationInfos returns all non-alias registered integrations, sorted by name
|
||||||
|
// with integrationOrder entries placed at the end.
|
||||||
func ListIntegrationInfos() []IntegrationInfo {
|
func ListIntegrationInfos() []IntegrationInfo {
|
||||||
var result []IntegrationInfo
|
var result []IntegrationInfo
|
||||||
for name, r := range integrations {
|
for name, r := range integrations {
|
||||||
@@ -148,7 +157,26 @@ func ListIntegrationInfos() []IntegrationInfo {
|
|||||||
Description: integrationDescriptions[name],
|
Description: integrationDescriptions[name],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderRank := make(map[string]int, len(integrationOrder))
|
||||||
|
for i, name := range integrationOrder {
|
||||||
|
orderRank[name] = i + 1 // 1-indexed so 0 means "not in the list"
|
||||||
|
}
|
||||||
|
|
||||||
slices.SortFunc(result, func(a, b IntegrationInfo) int {
|
slices.SortFunc(result, func(a, b IntegrationInfo) int {
|
||||||
|
aRank, bRank := orderRank[a.Name], orderRank[b.Name]
|
||||||
|
// Both have custom order: sort by their rank
|
||||||
|
if aRank > 0 && bRank > 0 {
|
||||||
|
return aRank - bRank
|
||||||
|
}
|
||||||
|
// Only one has custom order: it goes last
|
||||||
|
if aRank > 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if bRank > 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
// Neither has custom order: alphabetical
|
||||||
return strings.Compare(a.Name, b.Name)
|
return strings.Compare(a.Name, b.Name)
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
@@ -186,9 +214,15 @@ func IsIntegrationInstalled(name string) bool {
|
|||||||
case "droid":
|
case "droid":
|
||||||
_, err := exec.LookPath("droid")
|
_, err := exec.LookPath("droid")
|
||||||
return err == nil
|
return err == nil
|
||||||
|
case "cline":
|
||||||
|
_, err := exec.LookPath("cline")
|
||||||
|
return err == nil
|
||||||
case "opencode":
|
case "opencode":
|
||||||
_, err := exec.LookPath("opencode")
|
_, err := exec.LookPath("opencode")
|
||||||
return err == nil
|
return err == nil
|
||||||
|
case "pi":
|
||||||
|
_, err := exec.LookPath("pi")
|
||||||
|
return err == nil
|
||||||
default:
|
default:
|
||||||
return true // Assume installed for unknown integrations
|
return true // Assume installed for unknown integrations
|
||||||
}
|
}
|
||||||
@@ -367,13 +401,11 @@ func selectIntegration() (string, error) {
|
|||||||
return "", fmt.Errorf("no integrations available")
|
return "", fmt.Errorf("no integrations available")
|
||||||
}
|
}
|
||||||
|
|
||||||
names := slices.Sorted(maps.Keys(integrations))
|
|
||||||
var items []ModelItem
|
var items []ModelItem
|
||||||
for _, name := range names {
|
for name, r := range integrations {
|
||||||
if integrationAliases[name] {
|
if integrationAliases[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
r := integrations[name]
|
|
||||||
description := r.String()
|
description := r.String()
|
||||||
if conn, err := loadIntegration(name); err == nil && len(conn.Models) > 0 {
|
if conn, err := loadIntegration(name); err == nil && len(conn.Models) > 0 {
|
||||||
description = fmt.Sprintf("%s (%s)", r.String(), conn.Models[0])
|
description = fmt.Sprintf("%s (%s)", r.String(), conn.Models[0])
|
||||||
@@ -381,6 +413,24 @@ func selectIntegration() (string, error) {
|
|||||||
items = append(items, ModelItem{Name: name, Description: description})
|
items = append(items, ModelItem{Name: name, Description: description})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderRank := make(map[string]int, len(integrationOrder))
|
||||||
|
for i, name := range integrationOrder {
|
||||||
|
orderRank[name] = i + 1
|
||||||
|
}
|
||||||
|
slices.SortFunc(items, func(a, b ModelItem) int {
|
||||||
|
aRank, bRank := orderRank[a.Name], orderRank[b.Name]
|
||||||
|
if aRank > 0 && bRank > 0 {
|
||||||
|
return aRank - bRank
|
||||||
|
}
|
||||||
|
if aRank > 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if bRank > 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return strings.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
|
||||||
return DefaultSingleSelector("Select integration:", items)
|
return DefaultSingleSelector("Select integration:", items)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,10 +862,12 @@ Without arguments, this is equivalent to running 'ollama' directly.
|
|||||||
|
|
||||||
Supported integrations:
|
Supported integrations:
|
||||||
claude Claude Code
|
claude Claude Code
|
||||||
|
cline Cline
|
||||||
codex Codex
|
codex Codex
|
||||||
droid Droid
|
droid Droid
|
||||||
opencode OpenCode
|
opencode OpenCode
|
||||||
openclaw OpenClaw (aliases: clawdbot, moltbot)
|
openclaw OpenClaw (aliases: clawdbot, moltbot)
|
||||||
|
pi Pi
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
ollama launch
|
ollama launch
|
||||||
|
|||||||
@@ -1248,10 +1248,26 @@ func TestListIntegrationInfos(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("sorted by name", func(t *testing.T) {
|
t.Run("sorted with custom order at end", func(t *testing.T) {
|
||||||
|
// integrationOrder entries (cline, opencode) should appear last, in that order.
|
||||||
|
// All other entries should be sorted alphabetically before them.
|
||||||
|
orderRank := make(map[string]int)
|
||||||
|
for i, name := range integrationOrder {
|
||||||
|
orderRank[name] = i + 1
|
||||||
|
}
|
||||||
for i := 1; i < len(infos); i++ {
|
for i := 1; i < len(infos); i++ {
|
||||||
if infos[i-1].Name >= infos[i].Name {
|
aRank, bRank := orderRank[infos[i-1].Name], orderRank[infos[i].Name]
|
||||||
t.Errorf("not sorted: %q >= %q", infos[i-1].Name, infos[i].Name)
|
switch {
|
||||||
|
case aRank == 0 && bRank == 0:
|
||||||
|
if infos[i-1].Name >= infos[i].Name {
|
||||||
|
t.Errorf("non-ordered items not sorted: %q >= %q", infos[i-1].Name, infos[i].Name)
|
||||||
|
}
|
||||||
|
case aRank > 0 && bRank == 0:
|
||||||
|
t.Errorf("ordered item %q should come after non-ordered %q", infos[i-1].Name, infos[i].Name)
|
||||||
|
case aRank > 0 && bRank > 0:
|
||||||
|
if aRank >= bRank {
|
||||||
|
t.Errorf("ordered items wrong: %q (rank %d) before %q (rank %d)", infos[i-1].Name, aRank, infos[i].Name, bRank)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user