Skip to content

Commit 1485bc7

Browse files
committed
Route desktop through clovapi CLI and unify Codex detection.
Electron calls top-level profiles, switch, auth, and proxy commands instead of bespoke bridges; add CODEX_HOME-aware config and broader Codex install discovery for CLI and app.
1 parent 4a2079c commit 1485bc7

58 files changed

Lines changed: 2398 additions & 1290 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.en.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ clovapi --help
2424

2525
| Command | Description |
2626
|---------|-------------|
27-
| `clovapi list` | Show profiles and CLI matrix (aliases `profiles` / `ls`) |
28-
| `clovapi add --name NAME` | Save upstream profile and probe ( `set` / `new`) |
29-
| `clovapi switch [--cli KIND]` | Apply profile to one CLI (`use`) |
30-
| `clovapi proxy` | Built-in local proxy (`start` / `status` / `config`) |
27+
| `clovapi list` | Show profiles and active CLI bindings (alias `ls`) |
28+
| `clovapi profiles` | JSON API for load/save/test/catalog (`--json`; see `core/README.md`) |
29+
| `clovapi add --name NAME` | Save upstream profile and probe (`set` / `new`) |
30+
| `clovapi switch [--cli KIND]` | Apply binding to one CLI (`use`; add `--json` for scripts) |
31+
| `clovapi auth` | Subscription OAuth (`status` / `login` / `logout`; `--json`) |
32+
| `clovapi proxy` | Built-in local proxy (`start` / `stop` / `status` / `health`; `--json` where supported) |
3133
| `clovapi reset` | Clear all profiles and bindings (`--yes`) |
34+
35+
Details: [core/README.md](core/README.md).

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ clovapi --help
2424

2525
| 命令 | 说明 |
2626
|------|------|
27-
| `clovapi list` | 展示 profiles 与 CLI 矩阵(别名 `profiles` / `ls`|
27+
| `clovapi list` | 展示 profiles 与各 CLI 的 active 绑定(别名 `ls`|
28+
| `clovapi profiles` | 加载/保存/测试 profiles 的 JSON API(`--json`;详见 `core/README.zh.md`|
2829
| `clovapi add --name NAME` | 保存上游 profile 并测连通(`set` / `new`|
29-
| `clovapi switch [--cli KIND]` | 将 profile 应用到某一 CLI(`use`|
30-
| `clovapi proxy` | 内置本地代理(`start` / `status` / `config`|
31-
| `clovapi reset` | 清空全部 profile 与绑定(`--yes`|
30+
| `clovapi switch [--cli KIND]` | 将绑定应用到某一 CLI(`use`;脚本可加 `--json`|
31+
| `clovapi auth` | 订阅 OAuth(`status` / `login` / `logout`;支持 `--json`|
32+
| `clovapi proxy` | 内置本地代理(`start` / `stop` / `status` / `health` 等) |
33+
| `clovapi reset` | 清空全部 profile 与绑定(`--yes`|
34+
35+
详细说明见 [core/README.zh.md](core/README.zh.md)

core/README.md

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,40 @@ For **Claude Code**, env wiring matches community **cc-switch** / **ccswitch**;
1616

1717
| Command | Description |
1818
|--------|-------------|
19-
| `clovapi list` | Show saved profiles, CLI ↔ API-style matrix, and last-applied CLIs (aliases: **`profiles`**, **`ls`**) |
19+
| `clovapi list` | Show saved profiles and active CLI bindings (alias: **`ls`**) |
20+
| `clovapi profiles` | Load/save/test profiles and vendor helpers for scripts and the desktop UI (see below) |
2021
| `clovapi add --name NAME` | Save one upstream profile (CLI is chosen at switch time); connectivity test before persist (`--name` required, aliases: **`set`**, **`new`**) |
2122
| `clovapi remove <name>` | Remove one saved profile (aliases: **`rm`**, **`delete`**) |
22-
| `clovapi switch [--cli KIND] [VENDOR/MODEL]` | Apply a vendor model binding to one CLI (`--vendor` + `--model`, or interactive vendor → model). Alias **`use`** |
23-
| `clovapi proxy` | Run and inspect the built-in local proxy core (`start`, `status`, `config`) |
23+
| `clovapi switch [--cli KIND] [VENDOR/MODEL]` | Apply a provider/model binding to one CLI (`--provider` + `--model`, `--vendor` + `--model`, or interactive). Alias **`use`**. Add **`--json`** for machine-readable output |
24+
| `clovapi auth` | Claude/Codex subscription OAuth (`status`, `login`, `logout`; **`--json`** on each) |
25+
| `clovapi proxy` | Run and inspect the built-in local proxy (`start`, `stop`, `status`, `health`, `config`, `logs`, …; **`--json`** on `status` / `health`) |
2426
| `clovapi reset` | Clear all saved profiles and bindings (`--yes` / `-y` skips prompt) |
2527

28+
### `clovapi profiles` (JSON API)
29+
30+
Subcommands mirror what the desktop app and Electron shell call. Pass **`--json`** for structured stdout (stdin JSON for `save`):
31+
32+
| Subcommand | Description |
33+
|------------|-------------|
34+
| `load --json` | Read normalized `profiles.json` (vendors, `active`, proxy bind) |
35+
| `save --json` | Merge payload from stdin and persist |
36+
| `test --json` | Probe a provider/model via the local proxy (`--provider`, `--model`, optional `--cli`, `--port`) |
37+
| `list-models --vendor NAME --json` | Fetch and merge models for one vendor |
38+
| `usage --vendor NAME --json` | Query upstream quota/balance for one API vendor |
39+
| `catalog --json` | Fixed provider registry and model-adapter catalog |
40+
41+
Legacy **`clovapi desktop profiles|vendor|auth …`** remains for older scripts; prefer the top-level commands above.
42+
43+
Script examples:
44+
45+
```bash
46+
clovapi profiles load --json
47+
clovapi profiles save --json < payload.json
48+
clovapi switch --cli opencode --provider custom-api --model my-model --json
49+
clovapi auth status --json
50+
clovapi proxy status --json
51+
```
52+
2653
## Build
2754

2855
```bash
@@ -59,10 +86,9 @@ cd core && go test ./...
5986
- **Unix** (macOS/Linux): `$XDG_CONFIG_HOME/clovapi` or `~/.config/clovapi`
6087
- **Windows**: `%APPDATA%\clovapi`
6188

62-
State file: `profiles.json` (0600). It stores **`profiles`** (all saved rows) and **`active`** (last applied profile name per CLI).
63-
64-
`clovapi switch` always targets a single CLI. Use `--cli` for non-interactive scripts. Each CLI adapter picks the appropriate upstream API style internally — you do not need to match styles manually.
89+
State file: `profiles.json` (0600). It stores **`profiles`** (vendor rows and models), **`active`** (per CLI: `provider_id` + `model_id`), and **`proxy`** (local bind host/port).
6590

91+
`clovapi switch` always targets a single CLI. Use `--cli` for non-interactive scripts. Prefer **`--provider`** + **`--model`** (or **`--vendor`** + **`--model`**) in automation; add **`--json`** when a caller needs structured success/error output.
6692
## Apply behavior (summary)
6793

6894
- **claude-code** + `claude`: writes `~/.claude/settings.json` with **`env.ANTHROPIC_AUTH_TOKEN`** and **`ANTHROPIC_BASE_URL`** (same pattern as ccswitch). **`ANTHROPIC_API_KEY` is removed** from `env` so Claude Code does not show “Auth conflict: Both a token and an API key are set”.
@@ -86,11 +112,13 @@ Paths expand correctly on Windows (user profile / AppData).
86112

87113
| clovapi | Alias(es) | Similar to |
88114
|---------|-----------|------------|
89-
| `list` | `profiles`, `ls` | `cc-switch list`, `ccswitch list` |
115+
| `list` | `ls` | `cc-switch list`, `ccswitch list` |
116+
| `profiles` | _(command group, not a list alias)_ | Desktop/script JSON API for `profiles.json` |
90117
| `add` | `set`, `new` | One-shot save of upstream binding |
91-
| `switch [--cli …]` | `use …` | Apply one saved profile into one tool config |
118+
| `switch [--cli …]` | `use …` | Apply one binding into one tool config |
92119
| `remove NAME` | `rm`, `delete` | Delete one saved profile |
93120

121+
**Note:** Older docs used `clovapi profiles` as an alias for `list`. That alias was removed when **`profiles`** became its own subcommand group (`load`, `save`, …). Use **`clovapi list`** or **`clovapi ls`** for the human table.
94122
### Where state lives
95123

96124
| Tool | Stored profiles |

core/README.zh.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,40 @@
1616

1717
| 命令 | 说明 |
1818
|------|------|
19-
| `clovapi list` | 展示已保存的 profiles、CLI ↔ API 形态矩阵、上次下发的 CLI(别名:**`profiles`****`ls`**|
19+
| `clovapi list` | 展示已保存的 profiles 与各 CLI 的 active 绑定(别名:**`ls`**|
20+
| `clovapi profiles` | 为脚本与桌面 UI 加载/保存/测试 profiles(见下文) |
2021
| `clovapi add --name NAME` | 保存一个上游 profile(CLI 在 switch 时再选择);持久化前先测连通(`--name` 必填,别名:**`set`****`new`**|
2122
| `clovapi remove <name>` | 删除一条已保存 profile(别名:**`rm`****`delete`**|
22-
| `switch [--cli KIND] [VENDOR/MODEL]` | 将某个 vendor 下的模型绑定应用到某一 CLI(`--vendor` + `--model`,或交互式选 vendor → model)。别名 **`use`** |
23-
| `clovapi proxy` | 运行并查看内置本地代理核心(`start``status``config`|
23+
| `clovapi switch [--cli KIND] [VENDOR/MODEL]` | 将 provider/model 绑定应用到某一 CLI(`--provider` + `--model``--vendor` + `--model`,或交互选择)。别名 **`use`**。脚本可加 **`--json`** |
24+
| `clovapi auth` | Claude/Codex 订阅 OAuth(`status``login``logout`;均可 **`--json`**|
25+
| `clovapi proxy` | 运行并查看内置本地代理(`start``stop``status``health``config``logs` 等;`status` / `health` 支持 **`--json`**|
2426
| `clovapi reset` | 清空所有 profile 与绑定记录(**`--yes`** / **`-y`** 跳过确认) |
2527

28+
### `clovapi profiles`(JSON API)
29+
30+
子命令与桌面端 / Electron 调用一致。加 **`--json`** 输出结构化 JSON(`save` 从 stdin 读 JSON):
31+
32+
| 子命令 | 说明 |
33+
|--------|------|
34+
| `load --json` | 读取规范化后的 `profiles.json`(vendors、`active`、proxy 绑定) |
35+
| `save --json` | 合并 stdin 载荷并写盘 |
36+
| `test --json` | 经本地代理探测 provider/model(`--provider``--model`,可选 `--cli``--port`|
37+
| `list-models --vendor NAME --json` | 拉取并合并某一 vendor 的模型列表 |
38+
| `usage --vendor NAME --json` | 查询某一 API vendor 的额度/余额 |
39+
| `catalog --json` | 固定 provider 注册表与 model adapter 目录 |
40+
41+
旧版 **`clovapi desktop profiles|vendor|auth …`** 仍可用;新脚本请优先用上述顶层命令。
42+
43+
示例:
44+
45+
```bash
46+
clovapi profiles load --json
47+
clovapi profiles save --json < payload.json
48+
clovapi switch --cli opencode --provider custom-api --model my-model --json
49+
clovapi auth status --json
50+
clovapi proxy status --json
51+
```
52+
2653
## 构建
2754

2855
```bash
@@ -59,16 +86,16 @@ cd core && go test ./...
5986
- **Unix**(macOS/Linux):`$XDG_CONFIG_HOME/clovapi``~/.config/clovapi`
6087
- **Windows**`%APPDATA%\clovapi`
6188

62-
状态文件:`profiles.json`(权限 0600)。内含 **`profiles`**(vendor 与模型列表)**`active`**(每个 CLI 的 `@model:Vendor/model-id` 绑定)。
89+
状态文件:`profiles.json`(权限 0600)。内含 **`profiles`**(vendor 与模型列表)**`active`**(每个 CLI 的 `provider_id` + `model_id`),以及 **`proxy`**(本地监听 host/port)。
6390

6491
`clovapi switch` 始终只针对单一 CLI。交互流程:**选 CLI → 选 vendor → 选 model**。脚本示例:
6592

6693
```bash
94+
clovapi switch --cli codex --provider codex --model gpt-5.5 --json
6795
clovapi switch --cli codex --vendor "Codex Subscription" --model gpt-5.5
6896
clovapi switch --cli codex "Codex Subscription/gpt-5.5"
6997
clovapi switch --cli codex # 无参数时复用 active 绑定,或进入交互选择
7098
```
71-
7299
## 下发行为摘要
73100

74101
- **claude-code** + `claude`:写入 `~/.claude/settings.json`,设置 **`env.ANTHROPIC_AUTH_TOKEN`****`ANTHROPIC_BASE_URL`**(与 ccswitch 同款)。从 `env`**移除 `ANTHROPIC_API_KEY`**,避免 Claude Code 提示「Auth conflict: Both a token and an API key are set」。
@@ -92,11 +119,13 @@ clovapi switch --cli codex # 无参数时复用 active 绑定,或进入交
92119

93120
| clovapi | 别名 | 类似 |
94121
|---------|------|------|
95-
| `list` | `profiles``ls` | `cc-switch list``ccswitch list` |
122+
| `list` | `ls` | `cc-switch list``ccswitch list` |
123+
| `profiles` | _(子命令组,不是 list 的别名)_ | 桌面/脚本的 `profiles.json` JSON API |
96124
| `add` | `set``new` | 一次性保存上游绑定 |
97-
| `switch [--cli …]` | `use …` | 将单个 profile 推入单一工具 |
125+
| `switch [--cli …]` | `use …` | 将单个绑定推入单一工具 |
98126
| `remove NAME` | `rm``delete` | 删除单条 profile |
99127

128+
**说明:** 旧文档曾把 `clovapi profiles` 当作 `list` 的别名;现 **`profiles`** 为独立子命令组(`load``save` 等)。查看人类可读表格请用 **`clovapi list`****`clovapi ls`**
100129
### 状态存放位置
101130

102131
| 工具 | 配置存放 |

core/cmd/auth_cmd.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/clovapi/switcher/internal/desktop"
10+
)
11+
12+
func cmdAuth() *cobra.Command {
13+
c := &cobra.Command{
14+
Use: "auth",
15+
Short: "Claude/Codex subscription OAuth login",
16+
}
17+
c.AddCommand(cmdAuthStatus(), cmdAuthLogin(), cmdAuthLogout())
18+
return c
19+
}
20+
21+
func cmdAuthStatus() *cobra.Command {
22+
var jsonFlag bool
23+
c := &cobra.Command{
24+
Use: "status",
25+
Short: "Report Claude/Codex subscription login status",
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
result := desktop.AuthStatus()
28+
if jsonFlag {
29+
return writeDesktopJSON(result)
30+
}
31+
return printAuthStatus(result)
32+
},
33+
}
34+
c.Flags().BoolVar(&jsonFlag, "json", false, "Return JSON")
35+
return c
36+
}
37+
38+
func cmdAuthLogin() *cobra.Command {
39+
var providerID string
40+
var jsonFlag bool
41+
c := &cobra.Command{
42+
Use: "login",
43+
Short: "Run subscription OAuth login in the browser",
44+
RunE: func(cmd *cobra.Command, args []string) error {
45+
result := desktop.AuthLogin(cmd.Context(), providerID)
46+
if jsonFlag {
47+
return writeDesktopJSON(result)
48+
}
49+
return printAuthLoginResult(result)
50+
},
51+
}
52+
c.Flags().StringVar(&providerID, "provider", "", "Provider id: claude-code|codex")
53+
c.Flags().BoolVar(&jsonFlag, "json", false, "Return JSON")
54+
return c
55+
}
56+
57+
func cmdAuthLogout() *cobra.Command {
58+
var providerID string
59+
var jsonFlag bool
60+
c := &cobra.Command{
61+
Use: "logout",
62+
Short: "Remove subscription OAuth credentials",
63+
RunE: func(cmd *cobra.Command, args []string) error {
64+
result := desktop.AuthLogout(providerID)
65+
if jsonFlag {
66+
return writeDesktopJSON(result)
67+
}
68+
return printAuthLogoutResult(result)
69+
},
70+
}
71+
c.Flags().StringVar(&providerID, "provider", "", "Provider id: claude-code|codex")
72+
c.Flags().BoolVar(&jsonFlag, "json", false, "Return JSON")
73+
return c
74+
}
75+
76+
func printAuthStatus(result desktop.AuthStatusResult) error {
77+
if !result.OK {
78+
if msg := strings.TrimSpace(result.Error); msg != "" {
79+
return fmt.Errorf("%s", msg)
80+
}
81+
return fmt.Errorf("failed to read subscription status")
82+
}
83+
for _, item := range result.Items {
84+
state := "not logged in"
85+
if item.LoggedIn {
86+
state = "logged in"
87+
}
88+
summary := strings.TrimSpace(item.Summary)
89+
if summary != "" {
90+
fmt.Printf("%s (%s): %s — %s\n", item.Label, item.ID, state, summary)
91+
} else {
92+
fmt.Printf("%s (%s): %s\n", item.Label, item.ID, state)
93+
}
94+
}
95+
return nil
96+
}
97+
98+
func printAuthLoginResult(result desktop.AuthLoginResult) error {
99+
if result.OK {
100+
fmt.Println("Subscription login succeeded.")
101+
if url := strings.TrimSpace(result.AuthorizeURL); url != "" {
102+
fmt.Println("If the browser did not open, visit:")
103+
fmt.Println(url)
104+
}
105+
return nil
106+
}
107+
if msg := strings.TrimSpace(result.Error); msg != "" {
108+
return fmt.Errorf("%s", msg)
109+
}
110+
return fmt.Errorf("subscription login failed")
111+
}
112+
113+
func printAuthLogoutResult(result desktop.AuthLogoutResult) error {
114+
if result.OK {
115+
fmt.Println("Subscription logout succeeded.")
116+
return nil
117+
}
118+
if msg := strings.TrimSpace(result.Error); msg != "" {
119+
return fmt.Errorf("%s", msg)
120+
}
121+
return fmt.Errorf("subscription logout failed")
122+
}

0 commit comments

Comments
 (0)