opencode-provider-switch/internal/cli/opencode.go
apale7 cf3dcec399 refactor(cli): rename tool to ocswitch
Avoid collision with the existing Opswitch product while keeping the repository name as opencode-provider-switch.

This aligns the CLI, provider key, config paths, examples, and Trellis history under one public-facing name.
2026-04-17 21:43:21 +08:00

121 lines
4.0 KiB
Go

package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/Apale7/opencode-provider-switch/internal/opencode"
)
func newOpencodeCmd() *cobra.Command {
c := &cobra.Command{
Use: "opencode",
Short: "OpenCode integration commands",
Long: `OpenCode commands manage the narrow integration boundary between ocswitch and
OpenCode config.
These commands do not attempt full OpenCode config takeover. They are limited to
the provider.ocswitch sync path and optional top-level model fields when you opt in
explicitly.
Common workflow: validate with doctor first, inspect sync help, then run
opencode sync.`,
Example: ` ocswitch opencode sync --dry-run
ocswitch opencode sync --set-model ocswitch/gpt-5.4`,
}
c.AddCommand(newOpencodeSyncCmd())
return c
}
func newOpencodeSyncCmd() *cobra.Command {
var target string
var setModel string
var setSmallModel string
var dryRun bool
cmd := &cobra.Command{
Use: "sync",
Short: "Update provider.ocswitch in the global OpenCode config to match current aliases",
Long: `ocswitch opencode sync writes provider.ocswitch into the target OpenCode config.
By default it targets the global user config (~/.config/opencode), picking the
existing file in precedence order opencode.jsonc > opencode.json > config.json,
or creating opencode.jsonc if none exists. It does NOT touch the top-level
"model" or "small_model" unless --set-model / --set-small-model are given.
The default target scope is only the global user config path; it does not follow
OPENCODE_CONFIG_DIR unless you pass --target yourself. The command writes alias
exposure into provider.ocswitch.models using only aliases that are currently
routable.
Use --dry-run to preview the resolved target file without writing it. Typical
workflow: run ocswitch doctor first, then sync, then start or restart ocswitch serve if
needed.`,
Example: ` ocswitch opencode sync
ocswitch opencode sync --dry-run
ocswitch opencode sync --set-model ocswitch/gpt-5.4
ocswitch opencode sync --set-model ocswitch/gpt-5.4 --set-small-model ocswitch/gpt-5.4-mini
ocswitch opencode sync --target /path/to/opencode.jsonc`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadCfg()
if err != nil {
return err
}
if errs := cfg.Validate(); len(errs) > 0 {
for _, e := range errs {
cmd.PrintErrln("config error:", e)
}
return errs[0]
}
path := target
if path == "" {
p, _ := opencode.ResolveGlobalConfigPath()
path = p
}
raw, err := opencode.Load(path)
if err != nil {
return err
}
aliasNames := cfg.AvailableAliasNames()
baseURL := fmt.Sprintf("http://%s:%d/v1", cfg.Server.Host, cfg.Server.Port)
changed := opencode.EnsureOcswitchProvider(raw, baseURL, cfg.Server.APIKey, aliasNames)
if setModel != "" {
if raw["model"] != setModel {
raw["model"] = setModel
changed = true
}
}
if setSmallModel != "" {
if raw["small_model"] != setSmallModel {
raw["small_model"] = setSmallModel
changed = true
}
}
if !changed {
fmt.Fprintf(cmd.OutOrStdout(), "✓ no changes required at %s\n", path)
return nil
}
if dryRun {
fmt.Fprintf(cmd.OutOrStdout(), "would write %s (dry-run)\n", path)
return nil
}
if err := opencode.Save(path, raw); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "synced provider.ocswitch into %s (%d alias(es))\n", path, len(aliasNames))
if setModel != "" {
fmt.Fprintf(cmd.OutOrStdout(), " model = %s\n", setModel)
}
if setSmallModel != "" {
fmt.Fprintf(cmd.OutOrStdout(), " small_model = %s\n", setSmallModel)
}
return nil
},
}
cmd.Flags().StringVar(&target, "target", "", "explicit opencode config file to write (default: global)")
cmd.Flags().StringVar(&setModel, "set-model", "", "also set top-level model (opt-in only)")
cmd.Flags().StringVar(&setSmallModel, "set-small-model", "", "also set top-level small_model (opt-in only)")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "do not write, just report intent")
return cmd
}