@dmh2002/dsh-connect-qoder 0.0.0-stage → 0.4.0

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/lib/bin.js ADDED
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ import { S as deriveFingerprint, _ as piAiRuntimeInfo, d as qoderAccountsPath, f as QODER_PAT_ENV_VARS, h as patFromEnv, i as readHostHeartbeat, n as isHeartbeatProcessAlive, o as qoderPoolStorePath, r as qoderHostHeartbeatPath, u as QoderAccountStore, v as QODER_REGIONS, x as isQoderUpstreamError, y as QoderUpstreamClient } from "./host-heartbeat-BNuj6Lt_.js";
3
+ import { rm } from "node:fs/promises";
4
+ //#region src/bin.ts
5
+ /**
6
+ * Standalone status/diagnostics CLI for the dsh-connect-qoder bundle.
7
+ *
8
+ * 参考:dsh-connect-workbuddy(MIT,Copyright (c) 2026 LaoDing)——三个子命令
9
+ * 的骨架(`doctor` / `status` / `logout`)、`--json` 输出、`safeMessage` 脱敏与
10
+ * 「凭据来源 + 上游连通性」诊断的结构由该项目设计。
11
+ * 改动(按 Qoder 上游的现实):
12
+ * - 凭据只读环境变量 + 本地 vault(CLI 运行于宿主之外,无法访问设置服务);
13
+ * - `doctor` 增加 cosy 指纹派生自检、pi-ai 代际、宿主心跳存活、vault 账号计数;
14
+ * - `status` 汇报每区域凭据来源 + 上游可达性 + 当前剩余积分(总额);
15
+ * - `logout` 清除本区域的 vault 凭据与账号池测量副本(可 `--region` 指定,
16
+ * 否则两区域都清)。
17
+ *
18
+ * @module dsh-connect-qoder/bin
19
+ */
20
+ const JSON_SCHEMA_VERSION = 2;
21
+ const REGION_KEYS = ["cn", "global"];
22
+ /** Remove token-like strings from an unexpected diagnostic message. */
23
+ function safeMessage(error) {
24
+ return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]");
25
+ }
26
+ /** One region's total remaining credits from a usage read (secret-free). */
27
+ function creditsTotal(usage) {
28
+ if (usage === void 0) return void 0;
29
+ let total = 0;
30
+ let seen = false;
31
+ const userQuota = usage.userQuota;
32
+ if (userQuota !== void 0 && typeof userQuota.total === "number") {
33
+ total += Math.max(userQuota.total - (userQuota.used ?? 0), 0);
34
+ seen = true;
35
+ }
36
+ const addOnQuota = usage.addOnQuota;
37
+ if (addOnQuota !== void 0 && typeof addOnQuota.total === "number") {
38
+ total += Math.max(addOnQuota.total - (addOnQuota.used ?? 0), 0);
39
+ seen = true;
40
+ }
41
+ const org = usage.orgResourcePackage;
42
+ if (org !== void 0 && typeof org.cap === "number") {
43
+ total += Math.max(org.cap - (org.used ?? 0), 0);
44
+ seen = true;
45
+ }
46
+ return seen ? total : void 0;
47
+ }
48
+ function printHelp() {
49
+ process.stdout.write([
50
+ "Usage: dsh-connect-qoder <doctor|status|logout> [--json] [--region cn|global]",
51
+ "",
52
+ " doctor PAT source, cosy fingerprint, pi-ai generation, heartbeat, vault (no network)",
53
+ " status per-region PAT presence, upstream model-list reachability, credits",
54
+ " logout clear the local vault accounts + pool measurements for a region",
55
+ " --json emit one secret-free JSON document",
56
+ "",
57
+ "Credentials come from the environment:",
58
+ ` ${QODER_PAT_ENV_VARS.global} (global, qoder.sh)`,
59
+ ` ${QODER_PAT_ENV_VARS.cn} (CN, qoder.com.cn)`,
60
+ "or from the local vault ($DSH_HOME/.qoder-accounts.<region>.json).",
61
+ ""
62
+ ].join("\n"));
63
+ }
64
+ function printJson(value) {
65
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
66
+ }
67
+ function regionArg() {
68
+ const index = process.argv.indexOf("--region");
69
+ if (index !== -1) {
70
+ const value = process.argv[index + 1];
71
+ if (value === "cn" || value === "global") return [value];
72
+ }
73
+ return REGION_KEYS;
74
+ }
75
+ /**
76
+ * 从 argv 里取出**命令词**:剥掉 flag 与区域实参,剩下的第一个就是。
77
+ *
78
+ * 必须是「保留**不是** flag、**不是**区域名的实参」。写反成
79
+ * `REGION_KEYS.includes(arg)`(只留 cn/global)会让命令词自己被过滤掉,
80
+ * 于是三条子命令全部只会打印 usage。抽成纯函数就是为了能对它断言。
81
+ */
82
+ function commandArgOf(argv) {
83
+ return argv.filter((arg) => arg !== "--json" && arg !== "--region" && !REGION_KEYS.includes(arg))[0];
84
+ }
85
+ async function doctor() {
86
+ const piAi = piAiRuntimeInfo();
87
+ const heartbeat = await readHostHeartbeat();
88
+ const entries = [];
89
+ for (const region of REGION_KEYS) {
90
+ const fingerprint = deriveFingerprint("", `doctor:${region}`);
91
+ const vault = await new QoderAccountStore(region).vaultAccounts();
92
+ entries.push({
93
+ region,
94
+ gateway: QODER_REGIONS[region].gateway,
95
+ patEnv: QODER_PAT_ENV_VARS[region],
96
+ patPresent: patFromEnv(region) !== void 0,
97
+ vaultAccounts: vault.length,
98
+ fingerprint: {
99
+ machineId: fingerprint.machineId,
100
+ machineType: fingerprint.machineType,
101
+ machineToken: fingerprint.machineToken
102
+ }
103
+ });
104
+ }
105
+ const report = {
106
+ schemaVersion: JSON_SCHEMA_VERSION,
107
+ piAi: {
108
+ generation: piAi?.generation ?? "unknown",
109
+ version: piAi?.version ?? "unknown"
110
+ },
111
+ heartbeat: {
112
+ present: heartbeat !== void 0,
113
+ alive: heartbeat !== void 0 && isHeartbeatProcessAlive(heartbeat),
114
+ ...heartbeat?.pid === void 0 ? {} : { pid: heartbeat.pid }
115
+ },
116
+ entries
117
+ };
118
+ if (process.argv.includes("--json")) {
119
+ printJson(report);
120
+ return report;
121
+ }
122
+ process.stdout.write(`pi-ai: ${report.piAi.generation} (${report.piAi.version})\n`);
123
+ process.stdout.write(`heartbeat: ${report.heartbeat.present ? report.heartbeat.alive ? `alive (pid ${report.heartbeat.pid ?? "?"})` : "stale" : "absent"} — ${qoderHostHeartbeatPath()}\n`);
124
+ for (const entry of entries) {
125
+ process.stdout.write(`[${entry.region}] ${entry.gateway}\n`);
126
+ process.stdout.write(` PAT ${entry.patEnv}: ${entry.patPresent ? "present" : "missing"}\n`);
127
+ process.stdout.write(` vault: ${entry.vaultAccounts} account(s) — ${qoderAccountsPath(entry.region)}\n`);
128
+ process.stdout.write(` fingerprint: machineId=${entry.fingerprint.machineId} type=${entry.fingerprint.machineType}\n`);
129
+ }
130
+ const missing = entries.filter((entry) => !entry.patPresent && entry.vaultAccounts === 0);
131
+ if (missing.length > 0) {
132
+ process.stdout.write(`\nNo PAT in the environment or vault for: ${missing.map((entry) => entry.region).join(", ")}\n`);
133
+ process.stdout.write("Set the variable above (or add an account in the plugin card) before using that region.\n");
134
+ }
135
+ return report;
136
+ }
137
+ async function status() {
138
+ const entries = [];
139
+ for (const region of REGION_KEYS) {
140
+ const envPat = patFromEnv(region);
141
+ const vault = await new QoderAccountStore(region).vaultAccounts();
142
+ const pat = envPat ?? vault[0]?.pat;
143
+ const patSource = envPat !== void 0 ? "env" : vault.length > 0 ? "vault" : "none";
144
+ const entry = {
145
+ region,
146
+ gateway: QODER_REGIONS[region].gateway,
147
+ ...patSource === void 0 ? {} : { patSource },
148
+ models: 0
149
+ };
150
+ if (pat === void 0) {
151
+ entries.push(entry);
152
+ continue;
153
+ }
154
+ try {
155
+ const client = new QoderUpstreamClient(region);
156
+ entry.models = (await client.fetchModels(pat)).length;
157
+ try {
158
+ const credits = creditsTotal(await client.fetchUsage(pat));
159
+ if (credits !== void 0) entry.creditsTotal = credits;
160
+ } catch {}
161
+ } catch (error) {
162
+ if (isQoderUpstreamError(error)) entry.error = `${error.kind} (HTTP ${error.status}): ${safeMessage(error.message)}`;
163
+ else entry.error = safeMessage(error);
164
+ }
165
+ entries.push(entry);
166
+ }
167
+ if (process.argv.includes("--json")) {
168
+ printJson({
169
+ schemaVersion: JSON_SCHEMA_VERSION,
170
+ entries
171
+ });
172
+ return;
173
+ }
174
+ for (const entry of entries) {
175
+ process.stdout.write(`[${entry.region}] ${entry.gateway}\n`);
176
+ process.stdout.write(` PAT: ${entry.patSource ?? "none"}\n`);
177
+ if (entry.error !== void 0) process.stdout.write(` upstream: FAILED (${entry.error})\n`);
178
+ else if (entry.patSource !== "none") {
179
+ const credits = entry.creditsTotal === void 0 ? "" : `, credits ${entry.creditsTotal}`;
180
+ process.stdout.write(` upstream: OK, ${entry.models} models${credits}\n`);
181
+ }
182
+ }
183
+ }
184
+ async function logout() {
185
+ const regions = regionArg();
186
+ const cleared = [];
187
+ for (const region of regions) for (const path of [qoderAccountsPath(region), qoderPoolStorePath(region)]) try {
188
+ await rm(path, { force: true });
189
+ cleared.push(path);
190
+ } catch {}
191
+ if (process.argv.includes("--json")) {
192
+ printJson({
193
+ schemaVersion: JSON_SCHEMA_VERSION,
194
+ cleared
195
+ });
196
+ return;
197
+ }
198
+ process.stdout.write(cleared.length === 0 ? "Nothing to clear (no vault files present).\n" : `Cleared:\n${cleared.map((path) => ` ${path}\n`).join("")}`);
199
+ }
200
+ async function main() {
201
+ const command = commandArgOf(process.argv.slice(2));
202
+ if (command === "doctor") {
203
+ await doctor();
204
+ return;
205
+ }
206
+ if (command === "status") {
207
+ await status();
208
+ return;
209
+ }
210
+ if (command === "logout") {
211
+ await logout();
212
+ return;
213
+ }
214
+ printHelp();
215
+ process.exitCode = command === void 0 ? 0 : 2;
216
+ }
217
+ main();
218
+ //#endregion
219
+ export { commandArgOf };