dsh-qoder-connect 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -13,14 +13,16 @@ Configure only what you use: a variant with no saved PAT shows no model group an
13
13
 
14
14
  ## Features
15
15
 
16
- - **PAT: save it and it works** — paste a PAT into the card and press *Save*; the token is validated live against that variant's region before anything is written, and the model group appears immediately — no DSH restart. Once signed in, *Replace PAT* (the new token overwrites only after it validates, so a mistyped paste can never strand a working credential) and *Clear PAT* are one click away.
16
+ - **Multiple PATs, switched by hand** — each variant keeps a list of saved PATs and the card offers a select to choose the active one. Switching only changes the signing token: model toggles, context-window preferences, check-in and sidebar-quota settings are one shared configuration per variant and never follow the account. *Add PAT*, *Remove current* and *Clear all* manage the list; saving validates first, and removing or switching never touches another account's saved catalog or probe records.
17
+ - **PAT: save it and it works** — paste a PAT into the card and press *Save*; the token is validated live against that variant's region before anything is written, and the model group appears immediately — no DSH restart. A mistyped paste can never strand a working credential.
17
18
  - **Two independent variants** — `qoder` (china) and `qoder-global` (global) keep separate credential files, routes and saved catalogs: a token minted on qoder.com does not work against the China deployment and vice versa.
18
- - **Three-tier model catalog (live → saved → fallback)** — the card states where the list on screen came from: *“Model list updated …”* (fetched now), *“Showing the saved model list from …”* (this account's last successful fetch, restored after a restart or a failed fetch), or *“Showing the built-in model list (not yet updated from Qoder)”* (the roster compiled into the plugin). The reason of the most recent failed fetch is shown too, and a *Refresh model list* button sits right there.
19
+ - **Three-tier model catalog (live → saved → fallback)** — the card states where the list on screen came from: *“Model list updated …”* (fetched now), *“Showing the saved model list from …”* (this account's last successful fetch, restored after a restart or a failed fetch), or *“Showing the built-in model list (not yet updated from Qoder)”* (the roster compiled into the plugin). The reason of the most recent failed fetch is shown too, and the card's *Refresh* action re-reads the status and re-fetches the model list.
19
20
  - **One-toggle context window** — each variant card's *Context window* tab lists every model's capacity (the default and the largest declared window) and carries its own *“Use the largest declared context window”* preference (on by default): on, requests declare the maximum (e.g. Qwen3.8-Max at 1M); off, the default (200K). The two variants keep independent toggle state.
20
21
  - **Model visibility toggles & batch controls** — each variant card provides a dedicated *Model Toggles* tab (between *Context window* and *Credit details*) to freely enable or hide individual models; disabled models are hidden from the DSH model picker, keeping it tidy; includes instant search by name/ID and one-click *Enable all* / *Disable all* batch operations.
21
22
  - **Daily auto check-in & logs** — claims the daily 100 Credits automatically, **at a time you can set per variant on the card** (10:00 UTC+8 by default, the upstream's reset moment), with independent toggles and startup catch-up protection. The card features a dedicated *Check-in log* panel with audit trails, *Check in now*, *Refresh*, and *Clear logs* quick actions.
22
23
  - **Sidebar quota card + credit details** — the *“Qoder sidebar display”* card toggles a per-variant sidebar quota widget (off by default; each toggle needs its variant's PAT saved) with one shared refresh interval (default 5 min, minimum 1 min). Clicking the sidebar widget opens the *Qoder quota* panel: one row per credit package — *“Remaining / Total + bar | Expires”* — where a campaign grant (the international edition’s *SOTA Exclusive Credits*, for one) keeps its own upstream-given title and **its own expiry**. When the account reports no overall deadline (the upstream’s year-9999 sentinel), the panel states when the PAT was authorized rather than a fabricated date. Click the same widget again to close; click the other to switch variants.
23
24
  - **Rate display `x<priceFactor>`** — each model name is suffixed with the upstream-reported price multiplier (e.g. `Some Model · x0.79`, free is `x0`), spelled `x<n>` from the catalog's `price_factor`. Display only — it never changes the request; models whose rate the upstream did not report simply show no suffix.
25
+ - **Upstream queue hints are honored** — when Qoder answers a queued or throttled request (401/403/429 body) with `retryAfterSeconds`, the plugin turns it into a local `Retry-After` header and hands it to DSH as `failure.providerRetryAfterMs`; the host retry engine can then wait until the moment the upstream asked for, instead of hammering the same still-closed window with its local backoff.
24
26
 
25
27
  ## Screenshots
26
28
 
@@ -80,7 +82,7 @@ Enable daily automatic check-in in the settings card and set each variant's own
80
82
 
81
83
  Prerequisites:
82
84
 
83
- - DSH core `0.1.7-rc.1` or newer (this plugin peers on the `@deepseek-ai/*` `>=0.1.7-rc.1` line);
85
+ - DSH core `0.2.1-rc.1` or newer (this plugin peers on the `@deepseek-ai/*` `>=0.2.1-rc.1` line);
84
86
  - Node.js `^22.19.0 || >=24.0.0` (per `package.json` engines);
85
87
  - your own Qoder account and at least one Personal Access Token.
86
88
 
@@ -93,6 +95,15 @@ dsh plugin --profile web add dsh-qoder-connect
93
95
 
94
96
  Swap the `--profile` value for the profile you use (`web` / `desktop` / `tui`, e.g. `dsh plugin --profile desktop add dsh-qoder-connect`) — data stays inside that profile, so web / desktop / tui never collide. On a terminal-only profile (TUI) there is no browser card: manage the PAT and check status through the bundled CLI instead; the settings card and sidebar widgets require Web or Desktop.
95
97
 
98
+ > **DSH older than `0.2.1-rc.1`?** Use **[v0.2.5](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.2.5)** instead — the last release on the 0.1.7 line (DSH ≥ `0.1.7-rc.1`):
99
+ >
100
+ > ```sh
101
+ > # From GitHub (recommended, pin the tag)
102
+ > dsh plugin --profile web add github:masknull/dsh-qoder-connect#v0.2.5
103
+ > # Or from npm
104
+ > dsh plugin --profile web add dsh-qoder-connect@0.2.5
105
+ > ```
106
+ >
96
107
  > **DSH older than `0.1.7-rc.1`?** Use **[v0.2.0](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.2.0)** instead — the last release supporting the 0.1.5 line (DSH ≥ `0.1.5-rc.2`):
97
108
  >
98
109
  > ```sh
@@ -102,7 +113,7 @@ Swap the `--profile` value for the profile you use (`web` / `desktop` / `tui`, e
102
113
  > dsh plugin --profile web add dsh-qoder-connect@0.2.0
103
114
  > ```
104
115
  >
105
- > Upgrading from 0.1.5? This release removed the automatic migration of the old host's `settings.yaml`. To carry your old configuration over, migrate through **[v0.1.9](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.1.9)** (the first release with the data migration) first, then upgrade to this release.
116
+ > Upgrading from 0.1.5? Newer releases removed the automatic migration of the old host's `settings.yaml`. To carry your old configuration over, migrate through **[v0.1.9](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.1.9)** (the first release with the data migration) first, then upgrade.
106
117
 
107
118
  ## Configuration
108
119
 
@@ -113,9 +124,23 @@ Swap the `--profile` value for the profile you use (`web` / `desktop` / `tui`, e
113
124
 
114
125
  (The card repeats the same guidance: *“Generate a PAT in your qoder.com account settings (Account → Personal Access Token), then paste it here.”*)
115
126
 
116
- ### Save it on the card
127
+ ### Save and switch on the card
117
128
 
118
- DSH → Settings → Plugin Settings (插件设置) → the Qoder card: paste the PAT into the password field and press *Save* (*“Validating and saving…”* while it runs). The token is validated against that variant's region first — a refusal saves nothing and the card says *“That PAT was rejected — generate a new one in your account settings and try again.”*. Do this on both cards to run both groups side by side.
129
+ DSH → Settings → Plugin Settings (插件设置) → the Qoder card: paste the PAT into the password field and press *Save* (*“Validating and saving…”* while it runs). The token is validated against that variant's region first — a refusal saves nothing and the card says *“That PAT was rejected — generate a new one in your account settings and try again.”*
130
+
131
+ Each variant can hold several PATs: once a second one is saved, the account row shows a select — choosing an entry switches to it. *Add PAT* appends another (with an optional local-only label), *Remove current* deletes only the active one, and *Clear all* empties the list. **Configuration is shared**: model toggles, context-window preferences, check-in time and sidebar-quota settings belong to the variant, not to the account, and switching never changes them. Quota and check-in act on the active account. Check-in history is kept per variant, so switching does not retroactively check the new account in for today — use *Check in now* when needed. Do this on both cards to run both groups side by side.
132
+
133
+ ### Command line (optional)
134
+
135
+ On a terminal-only profile the CLI manages the same list (`--provider` defaults to `qoder`):
136
+
137
+ ```bash
138
+ dsh-qoder-connect pat set <token> --provider qoder # validate, save, and activate
139
+ dsh-qoder-connect pat list --provider qoder # list saved tokens (* = active)
140
+ dsh-qoder-connect pat use <id> --provider qoder # switch
141
+ dsh-qoder-connect pat rm <id> --provider qoder # remove
142
+ dsh-qoder-connect pat clear --provider qoder # remove all (logout is an alias)
143
+ ```
119
144
 
120
145
  ### Environment-variable fallback
121
146
 
@@ -136,8 +161,8 @@ Everything the plugin owns lives in one data directory: `<profile>/.dsh-qoder-co
136
161
 
137
162
  ```text
138
163
  <profile>/.dsh-qoder-connect/
139
- ├── .qoder-auth.json # China PAT ({version:2, pat, region, savedAt})
140
- ├── .qoder-global-auth.json # Global PAT
164
+ ├── .qoder-auth.json # China PAT list ({version:3, activeId, credentials:[{id,pat,region,savedAt,label?}]})
165
+ ├── .qoder-global-auth.json # Global PAT list
141
166
  ├── checkin-status.json # check-in status and history logs
142
167
  └── state/
143
168
  ├── .qoder-catalog.json # China per-account saved catalog
package/README.md CHANGED
@@ -13,14 +13,16 @@
13
13
 
14
14
  ## 功能
15
15
 
16
- - **PAT 即存即用**:在卡片上粘贴 PAT 点「保存」,插件先对该变体的区域实时校验,通过才落盘,模型分组立即出现,无需重启 DSH。已登录后可「更换 PAT」(新令牌校验通过才覆盖,粘贴打错不会把可用凭据弄丢)与「清除 PAT」。
16
+ - **多 PAT 保存与手动切换**:每个变体可保存多枚 PAT,卡片上用下拉框切换当前账号(切换只换签名令牌,模型开关、上下文窗口、签到与侧栏额度等配置全部共享,不随账号变化);可「添加 PAT」「删除当前」「清除全部」。保存时先实时校验,通过才落盘;删除或切换不会误删其他账号的已保存模型目录与探测记录。
17
+ - **PAT 即存即用**:保存时先校验,通过才落盘,模型分组立即出现,无需重启 DSH;粘贴打错不会把可用凭据弄丢。
17
18
  - **双变体独立配置**:`qoder`(china)与 `qoder-global`(global)各有自己的凭证文件、路由与已保存目录——qoder.com 生成的令牌对中国区无效,反之亦然,两套永不互串。
18
- - **模型目录三级降级(live → saved → fallback)**:卡片明示当前列表来自哪里——「模型列表更新于 …」(实时拉取)、「当前显示已保存的模型列表,更新于 …」(本账号上次成功目录,重启或断网后顶上来)、「当前显示内置模型列表(尚未从 Qoder 更新)」(编译进插件的兜底名单);最近一次拉取失败时附带原因,并可在卡片上手动「刷新模型列表」。
19
+ - **模型目录三级降级(live → saved → fallback)**:卡片明示当前列表来自哪里——「模型列表更新于 …」(实时拉取)、「当前显示已保存的模型列表,更新于 …」(本账号上次成功目录,重启或断网后顶上来)、「当前显示内置模型列表(尚未从 Qoder 更新)」(编译进插件的兜底名单);最近一次拉取失败时附带原因,并可在卡片上手动「刷新」(同时刷新状态与模型列表)。
19
20
  - **上下文窗口一键切换**:两张变体卡片的「上下文窗口」页列出每个模型的容量(默认值与上游声明的最大值),并各自提供「使用上游声明的最大上下文窗口」开关(默认勾选):勾选按最大窗口(如 Qwen3.8-Max 的 1M)发请求,不勾选按默认窗口(200K),两版开关状态独立保存、互不影响。
20
21
  - **模型自由开关与批量管理**:两张变体卡片均提供「模型开关」页(位于上下文窗口与额度明细之间),可自由开启或关闭指定模型;关闭的模型将自动从 DeepSeek Harness 的模型选择器中隐藏,避免误选与列表冗长;顶部提供按名称/ID的即时搜索框,并支持全选与一键「批量开启」/「批量关闭」。
21
22
  - **每日自动签到与日志明细**:支持每天自动签到领取 100 Credits 算力额度,**签到时刻可在卡片上自定义**(默认 10:00,即上游刷新时间,按 UTC+8),配备双端开关与开机防漏补签机制;卡片常驻「签到日志」面板,提供执行流水记录与「立即签到」、「刷新」、「清空日志」快捷操作。
22
23
  - **侧栏额度展示 + 额度明细**:「Qoder 侧栏展示」卡片可分别为两版开启侧栏底部的额度小卡(默认关,开启前需为该变体保存 PAT),共享一个刷新间隔(默认 5 分钟,最小 1 分钟)。点侧栏小卡在中间面板打开「Qoder 额度」明细:按资源包逐行给出「剩余 / 总量 + 进度条|到期时间」,活动赠送包(如国际版「SOTA 专属积分」)按上游给定的名称与**各自的到期日**单独成行;账号整体无到期(上游回 9999 占位值)时展示 PAT 的授权时间而非假日期;再点同一张卡关闭,点另一张切换到对应版本。
23
24
  - **倍率显示 `x<priceFactor>`**:模型名后直接拼上游报价的价格倍率(如 `某模型 · x0.79`,免费为 `x0`),来自目录接口的 `price_factor` 字段,仅作展示、不影响请求;上游未报倍率的模型不显示后缀,不会凭空补一个数。
25
+ - **尊重上游的排队建议**:Qoder 在排队/限流答复里给出 `retryAfterSeconds` 时(401/403/429 的 body),插件把它转成本机 `Retry-After` 响应头,并作为 `failure.providerRetryAfterMs` 交给 DSH 的重试策略;宿主重试引擎因此会等到上游要求的时刻再发起下一次请求,而不是用本地退避提前撞回同一个未打开的窗口。
24
26
 
25
27
  ## 界面一览
26
28
 
@@ -80,7 +82,7 @@
80
82
 
81
83
  前置条件:
82
84
 
83
- - DSH 核心 `0.1.7-rc.1` 及以上(本插件 peer 依赖 `@deepseek-ai/*` `>=0.1.7-rc.1` 线);
85
+ - DSH 核心 `0.2.1-rc.1` 及以上(本插件 peer 依赖 `@deepseek-ai/*` `>=0.2.1-rc.1` 线);
84
86
  - Node.js `^22.19.0 || >=24.0.0`(`package.json` engines);
85
87
  - 你自己的 Qoder 账号,以及至少一枚个人访问令牌。
86
88
 
@@ -93,6 +95,15 @@ dsh plugin --profile web add dsh-qoder-connect
93
95
 
94
96
  按你使用的 profile 换 `--profile` 的值(`web` / `desktop` / `tui`,如 `dsh plugin --profile desktop add dsh-qoder-connect`)。装到哪个 profile,数据就落在哪个 profile 目录里,web / desktop / tui 各自独立。没有浏览器卡片的终端环境(TUI)下依旧可用 CLI 保存 PAT 并驱动模型;设置卡片与侧栏额度卡需要 Web/Desktop 界面。
95
97
 
98
+ > **DSH 低于 `0.2.1-rc.1`?** 请改用最后一个支持 0.1.7 线的版本 **[v0.2.5](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.2.5)**(要求 DSH ≥ `0.1.7-rc.1`):
99
+ >
100
+ > ```sh
101
+ > # GitHub(推荐,指定 tag)
102
+ > dsh plugin --profile web add github:masknull/dsh-qoder-connect#v0.2.5
103
+ > # 或从 npm 安装
104
+ > dsh plugin --profile web add dsh-qoder-connect@0.2.5
105
+ > ```
106
+ >
96
107
  > **DSH 低于 `0.1.7-rc.1`?** 请改用最后一个支持 0.1.5 线的版本 **[v0.2.0](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.2.0)**(要求 DSH ≥ `0.1.5-rc.2`):
97
108
  >
98
109
  > ```sh
@@ -102,7 +113,7 @@ dsh plugin --profile web add dsh-qoder-connect
102
113
  > dsh plugin --profile web add dsh-qoder-connect@0.2.0
103
114
  > ```
104
115
  >
105
- > 从 0.1.5 升级上来的老用户:本版本已移除旧宿主 `settings.yaml` 的自动迁移;如需保留旧配置,请先经 **[v0.1.9](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.1.9)**(首个带数据迁移的版本)完成迁移后,再升级本版本。
116
+ > 从 0.1.5 升级上来的老用户:新版本已移除旧宿主 `settings.yaml` 的自动迁移;如需保留旧配置,请先经 **[v0.1.9](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.1.9)**(首个带数据迁移的版本)完成迁移后,再升级。
106
117
 
107
118
  ## 配置与使用
108
119
 
@@ -113,9 +124,23 @@ dsh plugin --profile web add dsh-qoder-connect
113
124
 
114
125
  (卡片内也有同样指引:「请先在 qoder.com.cn 的账号设置(账号 → 个人访问令牌)中生成 PAT,再粘贴到下方输入框。」)
115
126
 
116
- ### 在卡片上保存
127
+ ### 在卡片上保存与切换
117
128
 
118
- DSH → 设置 → 插件设置 → Qoder 卡片,展开后把 PAT 粘贴进密码输入框,点「保存」(期间显示「校验并保存中…」)。保存动作会先经 Qoder 校验令牌对该变体的区域有效,失败则不落盘并提示「PAT 无效或已过期 — 请到账号设置重新生成后再试。」两版各填各的卡片即可两组并存。
129
+ DSH → 设置 → 插件设置 → Qoder 卡片,展开后把 PAT 粘贴进密码输入框,点「保存」(期间显示「校验并保存中…」)。保存动作会先经 Qoder 校验令牌对该变体的区域有效,失败则不落盘并提示「PAT 无效或已过期 — 请到账号设置重新生成后再试。」
130
+
131
+ 每个变体可以保存多枚 PAT:保存第二枚后,账号行出现下拉框,选中即切换;「添加 PAT」继续追加(可填一个仅本机显示的备注名),「删除当前」只删当前这一枚,「清除全部」一次清空。**配置永远共享**:模型开关、上下文窗口、签到时刻与侧栏额度设置都属于该变体的全局配置,切换账号不会改变;额度与签到作用于当前激活的账号。签到记录按变体保存,切换后不会自动为新账号补签当天,需要时可点「立即签到」。两版各填各的卡片即可两组并存。
132
+
133
+ ### 命令行(可选)
134
+
135
+ 终端环境可用 CLI 管理同一份多 PAT 列表(`--provider` 缺省为 `qoder`):
136
+
137
+ ```bash
138
+ dsh-qoder-connect pat set <token> --provider qoder # 校验、保存并设为当前
139
+ dsh-qoder-connect pat list --provider qoder # 列出已保存(* = 当前)
140
+ dsh-qoder-connect pat use <id> --provider qoder # 切换
141
+ dsh-qoder-connect pat rm <id> --provider qoder # 删除
142
+ dsh-qoder-connect pat clear --provider qoder # 清除全部(logout 同义)
143
+ ```
119
144
 
120
145
  ### 环境变量兜底
121
146
 
@@ -136,8 +161,8 @@ DSH → 设置 → 插件设置 → Qoder 卡片,展开后把 PAT 粘贴进密
136
161
 
137
162
  ```text
138
163
  <profile>/.dsh-qoder-connect/
139
- ├── .qoder-auth.json # 国内版 PAT({version:2, pat, region, savedAt})
140
- ├── .qoder-global-auth.json # 国际版 PAT
164
+ ├── .qoder-auth.json # 国内版 PAT 列表({version:3, activeId, credentials:[{id,pat,region,savedAt,label?}]})
165
+ ├── .qoder-global-auth.json # 国际版 PAT 列表
141
166
  ├── checkin-status.json # 签到状态与历史日志记录
142
167
  └── state/
143
168
  ├── .qoder-catalog.json # 国内版按账号保存的模型目录
package/assets/3.png CHANGED
Binary file
package/assets/5.png CHANGED
Binary file
package/assets/6.png CHANGED
Binary file
package/assets/7.png CHANGED
Binary file
package/assets/8.png CHANGED
Binary file
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { D as QoderCatalogStore, O as qoderCatalogPath, U as qoderCredentialIdentity, V as QoderCredentialStore, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, k as FALLBACK_QODER_MODELS, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey } from "./variants-COxX6r1j.js";
2
+ import { K as QoderCredentialStore, M as QoderCatalogStore, N as qoderCatalogPath, P as FALLBACK_QODER_MODELS, T as createQoderTransport, Y as qoderCredentialIdentity, _ as qoderProbePath, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderProbeStore, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, w as validateApiKey, y as QoderUpstreamClient } from "./variants-Dl1AbWre.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { realpathSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -16,8 +16,11 @@ function printHelp() {
16
16
  "",
17
17
  " doctor secret-free environment diagnostics",
18
18
  " status credential state and remaining Qoder credit",
19
- " pat set validate and store a Personal Access Token (arg, --file, or stdin)",
20
- " pat clear remove the stored credential",
19
+ " pat set validate, store, and activate a Personal Access Token (arg, --file, or stdin)",
20
+ " pat list list saved tokens and mark the active one",
21
+ " pat use <id> switch to a saved token",
22
+ " pat rm <id> remove a saved token",
23
+ " pat clear remove every saved token",
21
24
  " logout alias for `pat clear`",
22
25
  " catalog refresh re-fetch the model list and save it for this account",
23
26
  "",
@@ -147,8 +150,7 @@ async function status(jsonOutput, variant) {
147
150
  }
148
151
  }
149
152
  /** Read a token from the positional arg, --file, or standard input. */
150
- async function readPat(source, rest) {
151
- const inline = rest.find((argument) => !argument.startsWith("--"));
153
+ async function readPat(source, inline) {
152
154
  const pat = (source === void 0 ? inline ?? await readStdin() : source === "-" ? await readStdin() : await readFile(source, "utf8"))?.trim() ?? "";
153
155
  if (pat === "") throw new Error("no token given: pass it as an argument, via --file <path>, or on standard input");
154
156
  return pat;
@@ -158,9 +160,25 @@ async function readStdin() {
158
160
  for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
159
161
  return Buffer.concat(chunks).toString("utf8");
160
162
  }
161
- async function patSet(variant, file, rest) {
163
+ /**
164
+ * Drop the saved catalog and probe records that belong to one token.
165
+ *
166
+ * The host does this when a saved token is removed from the card; the CLI
167
+ * writes the same files, so it has to leave the same state behind. Leaving them
168
+ * would orphan another account's only copy — or resurrect data for a token the
169
+ * user just deleted.
170
+ */
171
+ function forgetAccount(variant, credential) {
172
+ const identity = qoderCredentialIdentity(credential);
173
+ new QoderCatalogStore({ path: qoderCatalogPath(variant.catalogFilename) }).delete(identity);
174
+ new QoderProbeStore({
175
+ pluginVersion: QODER_CONNECT_VERSION,
176
+ path: qoderProbePath(variant.probeFilename)
177
+ }).clearFor(identity);
178
+ }
179
+ async function patSet(variant, file, inline) {
162
180
  const store = makeStore(variant);
163
- const pat = await readPat(file, rest);
181
+ const pat = await readPat(file, inline);
164
182
  process.stdout.write(`Validating a token against ${variant.displayName} (${variant.region})…\n`);
165
183
  if (!await validateApiKey(pat, variant.region)) {
166
184
  process.stderr.write(`dsh-qoder-connect: the token was refused by ${variant.displayName} (${variant.region}). Nothing was saved.\n`);
@@ -170,17 +188,79 @@ async function patSet(variant, file, rest) {
170
188
  await store.save(pat, "cli");
171
189
  const status = await store.status();
172
190
  process.stdout.write([
173
- `Saved to ${store.ownAuthPath()}${status.pat?.patTail === void 0 ? "" : ` (tail …${status.pat.patTail})`}`,
191
+ `Saved and activated in ${store.ownAuthPath()}${status.pat?.patTail === void 0 ? "" : ` (tail …${status.pat.patTail})`}`,
192
+ `${status.profiles?.length ?? 1} saved token(s); switch with \`dsh-qoder-connect pat use <id>\`.`,
174
193
  "The running plugin picks the token up on its next sweep; no restart needed.",
175
194
  ""
176
195
  ].join("\n"));
177
196
  return 0;
178
197
  }
198
+ /** List the saved tokens, marking the active one; never prints a token. */
199
+ async function patList(jsonOutput, variant) {
200
+ const status = await makeStore(variant).status();
201
+ const profiles = status.profiles ?? [];
202
+ if (jsonOutput) {
203
+ printJson({
204
+ schemaVersion: JSON_SCHEMA_VERSION,
205
+ provider: variant.id,
206
+ activeId: status.activeId ?? null,
207
+ profiles,
208
+ ...status.pat?.source === "env" ? { source: "env" } : {}
209
+ });
210
+ return 0;
211
+ }
212
+ if (profiles.length === 0) {
213
+ process.stdout.write(status.pat?.source === "env" ? `${variant.displayName}: no saved token; the environment token is in use${status.pat.patTail === void 0 ? "" : ` (tail …${status.pat.patTail})`}.\n` : `${variant.displayName}: no saved tokens.\n`);
214
+ return 0;
215
+ }
216
+ const lines = profiles.map((profile) => [
217
+ profile.id === status.activeId ? "*" : " ",
218
+ profile.id,
219
+ profile.label ?? "(no label)",
220
+ profile.patTail === void 0 ? "" : `tail …${profile.patTail}`
221
+ ].filter((part) => part !== "").join(" "));
222
+ process.stdout.write([
223
+ `${variant.displayName} saved tokens (* = active):`,
224
+ ...lines,
225
+ ""
226
+ ].join("\n"));
227
+ return 0;
228
+ }
229
+ /** Make one saved token active. */
230
+ async function patUse(variant, id) {
231
+ const entry = await makeStore(variant).activate(id);
232
+ if (entry === void 0) {
233
+ process.stderr.write(`dsh-qoder-connect: no saved token with id ${JSON.stringify(id)}; run \`pat list\` to see the ids.\n`);
234
+ return 1;
235
+ }
236
+ const tail = entry.pat.slice(-4);
237
+ process.stdout.write(`${variant.displayName}: active token is now ${entry.id}${tail.length === 4 ? ` (tail …${tail})` : ""}.\n`);
238
+ return 0;
239
+ }
240
+ /** Remove one saved token and the account-scoped data that belonged to it. */
241
+ async function patRemove(variant, id) {
242
+ const store = makeStore(variant);
243
+ const removed = await store.remove(id);
244
+ if (removed === void 0) {
245
+ process.stderr.write(`dsh-qoder-connect: no saved token with id ${JSON.stringify(id)}; run \`pat list\` to see the ids.\n`);
246
+ return 1;
247
+ }
248
+ forgetAccount(variant, removed);
249
+ const status = await store.status();
250
+ process.stdout.write([
251
+ `${variant.displayName}: removed ${removed.id}.`,
252
+ status.activeId === void 0 ? "No saved token remains; the plugin is signed out of this variant." : `Active token is now ${status.activeId}.`,
253
+ ""
254
+ ].join("\n"));
255
+ return 0;
256
+ }
179
257
  async function patClear(variant) {
180
258
  const store = makeStore(variant);
259
+ const saved = await store.entries();
260
+ for (const entry of saved) forgetAccount(variant, entry);
181
261
  await store.clear();
182
262
  const status = await store.status();
183
- process.stdout.write(status.state === "configured" ? `${variant.displayName} Connect: removed the stored file, but a token is still active from the environment (${status.pat?.source ?? "env"}).\n` : `${variant.displayName} Connect: signed out; removed ${store.ownAuthPath()}\n`);
263
+ process.stdout.write(status.state === "configured" ? `${variant.displayName} Connect: removed ${saved.length} saved token(s), but a token is still active from the environment (${status.pat?.source ?? "env"}).\n` : `${variant.displayName} Connect: signed out; removed ${store.ownAuthPath()}\n`);
184
264
  return 0;
185
265
  }
186
266
  async function catalogRefresh(variant) {
@@ -249,11 +329,18 @@ async function run(argv) {
249
329
  }
250
330
  let patCommand;
251
331
  let catalogCommand;
332
+ /** The positional a subcommand takes: the token (`set`) or the entry id. */
333
+ let commandArg;
252
334
  if (action === "pat") {
253
335
  const sub = rest.shift();
254
- patCommand = sub === "set" || sub === "clear" ? sub : void 0;
336
+ patCommand = sub === "set" || sub === "list" || sub === "use" || sub === "rm" || sub === "clear" ? sub : void 0;
255
337
  if (patCommand === void 0) {
256
- process.stderr.write(`dsh-qoder-connect: pat needs set or clear; got ${JSON.stringify(sub ?? "")}\n`);
338
+ process.stderr.write(`dsh-qoder-connect: pat needs set, list, use, rm, or clear; got ${JSON.stringify(sub ?? "")}\n`);
339
+ return 1;
340
+ }
341
+ if (patCommand !== "list" && patCommand !== "clear") commandArg = rest.shift();
342
+ if ((patCommand === "use" || patCommand === "rm") && (commandArg === void 0 || commandArg.startsWith("--"))) {
343
+ process.stderr.write(`dsh-qoder-connect: pat ${patCommand} needs an entry id; run \`pat list\` to see them\n`);
257
344
  return 1;
258
345
  }
259
346
  }
@@ -265,7 +352,8 @@ async function run(argv) {
265
352
  return 1;
266
353
  }
267
354
  }
268
- if (rest.filter((flag) => flag !== "--json" && !flag.startsWith("--")).length > 0 || jsonOutput && action !== "doctor" && action !== "status") {
355
+ const patListJson = action === "pat" && patCommand === "list";
356
+ if (rest.filter((flag) => flag !== "--json" && !flag.startsWith("--")).length > 0 || jsonOutput && action !== "doctor" && action !== "status" && !patListJson) {
269
357
  process.stderr.write(`dsh-qoder-connect: invalid options for ${action}: ${flags.join(" ")}\n`);
270
358
  return 1;
271
359
  }
@@ -277,7 +365,15 @@ async function run(argv) {
277
365
  switch (action) {
278
366
  case "doctor": return await doctor(jsonOutput, variant);
279
367
  case "status": return await status(jsonOutput, variant);
280
- case "pat": return patCommand === "set" ? await patSet(variant, file, rest) : await patClear(variant);
368
+ case "pat":
369
+ switch (patCommand) {
370
+ case "set": return await patSet(variant, file, commandArg);
371
+ case "list": return await patList(jsonOutput, variant);
372
+ case "use": return await patUse(variant, commandArg);
373
+ case "rm": return await patRemove(variant, commandArg);
374
+ case "clear": return await patClear(variant);
375
+ }
376
+ return 1;
281
377
  case "logout": return await patClear(variant);
282
378
  case "catalog": return await catalogRefresh(variant);
283
379
  }