@huui/cdx-switcher 1.8.7 → 1.9.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/api.d.mts ADDED
@@ -0,0 +1,129 @@
1
+ //#region api.d.ts
2
+ /** API 调用失败时使用的稳定错误代码,调用方应优先根据此字段处理。 */
3
+ type CdxApiErrorCode = "CONFIGURATION_ERROR" | "CURRENT_ACCOUNT_UNAVAILABLE" | "ACCOUNT_LABEL_REQUIRED" | "ACCOUNT_LABEL_NOT_FOUND" | "ACCOUNT_LABEL_DUPLICATED" | "SECRET_STORE_UNAVAILABLE" | "CREDENTIAL_UNAVAILABLE" | "AUTH_FAILED" | "USAGE_UNAVAILABLE" | "NETWORK_ERROR";
4
+ /** API 失败时的错误信息;不会包含访问令牌、刷新令牌或账号邮箱。 */
5
+ interface CdxApiError {
6
+ /** 供程序稳定判断的错误代码。 */
7
+ code: CdxApiErrorCode;
8
+ /** 供日志或人工排查使用的可读错误说明。 */
9
+ message: string;
10
+ /** 为 true 时,调用方可以在退避后重试当前请求。 */
11
+ retryable: boolean;
12
+ }
13
+ /** 所有公开 API 统一使用的成功或失败返回结构。 */
14
+ type CdxApiResult<T> = {
15
+ /** 调用是否成功。 */ok: true; /** 成功时返回的业务数据。 */
16
+ data: T;
17
+ } | {
18
+ /** 调用是否成功。 */ok: false; /** 失败时返回的结构化错误信息。 */
19
+ error: CdxApiError;
20
+ };
21
+ /** 不暴露内部账号 ID 与邮箱的账号安全摘要。 */
22
+ interface AccountSummary {
23
+ /** 用户通过 `cdx label` 设置的账号标签;未设置时为 null。 */
24
+ label: string | null;
25
+ /** 用于界面或日志展示的安全名称;未设置标签时显示“未命名账号 N”。 */
26
+ displayName: string;
27
+ /** 此账号是否为当前已写入开发工具认证文件的账号。 */
28
+ isCurrent: boolean;
29
+ }
30
+ /** `listAccounts` 成功时返回的数据。 */
31
+ interface AccountListData {
32
+ /** 已配置账号的安全摘要列表,按 cdx 配置中的顺序排列。 */
33
+ accounts: AccountSummary[];
34
+ }
35
+ /** `getCurrentAccount` 成功时返回的数据。 */
36
+ interface CurrentAccountData {
37
+ /** 当前启用账号的安全摘要。 */
38
+ account: AccountSummary;
39
+ }
40
+ /** 单个限额周期的规范化数据。 */
41
+ interface UsageWindowData {
42
+ /** 限额周期类别;primary 为主周期,secondary 为辅助周期。 */
43
+ kind: "primary" | "secondary";
44
+ /** 当前周期已经使用的百分比,范围通常为 0 到 100。 */
45
+ usedPercent: number;
46
+ /** 当前周期剩余的百分比,按 100 - usedPercent 计算。 */
47
+ remainingPercent: number;
48
+ /** 此限额周期的总时长,单位为秒。 */
49
+ limitWindowSeconds: number;
50
+ /** 限额下次重置的 ISO 8601 时间字符串。 */
51
+ resetsAt: string;
52
+ /** 限额下次重置的 Unix 时间戳,单位为毫秒。 */
53
+ resetsAtUnixMs: number;
54
+ /** 相对于 checkedAt 的剩余时间,单位为毫秒;过期时为 0。 */
55
+ resetsInMs: number;
56
+ }
57
+ /** 账号额度附加信息。 */
58
+ interface CreditsData {
59
+ /** 账号是否拥有可用额度。 */
60
+ hasCredits: boolean;
61
+ /** 额度是否不受余额限制。 */
62
+ unlimited: boolean;
63
+ /** 可用余额;上游未提供时为 null。 */
64
+ balance: number | null;
65
+ }
66
+ /** `getCurrentAccountUsage` 成功时返回的数据。 */
67
+ interface CurrentAccountUsageData {
68
+ /** 被查询的当前启用账号。 */
69
+ account: AccountSummary;
70
+ /** 本次用量数据查询完成时的 ISO 8601 时间。 */
71
+ checkedAt: string;
72
+ /** 账号套餐类型;上游未提供时为 null。 */
73
+ planType: string | null;
74
+ /** 主周期和辅助周期的规范化限额数据。 */
75
+ windows: UsageWindowData[];
76
+ /** 账号额度信息;上游未提供时为 null。 */
77
+ credits: CreditsData | null;
78
+ }
79
+ /** 指定账号切换时的输入参数。 */
80
+ interface SwitchToAccountInput {
81
+ /** 目标账号的唯一标签;标签重复或不存在时会返回结构化错误。 */
82
+ label: string;
83
+ }
84
+ /** 认证文件更新后的写入状态。 */
85
+ interface AuthTargetWriteData {
86
+ /** OpenCode 认证文件的写入状态。 */
87
+ openCode: "written";
88
+ /** Codex CLI 认证文件的写入、清理或跳过状态。 */
89
+ codex: "written" | "cleared" | "skipped";
90
+ /** Pi Agent 认证文件的写入状态。 */
91
+ pi: "written";
92
+ }
93
+ /** `switchNextAccount` 与 `switchToAccount` 成功时返回的数据。 */
94
+ interface AccountSwitchData {
95
+ /** 切换前当前启用账号的安全摘要。 */
96
+ previousAccount: AccountSummary;
97
+ /** 切换后当前启用账号的安全摘要。 */
98
+ currentAccount: AccountSummary;
99
+ /** 是否实际切换到了另一个账号;只有一个账号或指定当前账号时为 false。 */
100
+ changed: boolean;
101
+ /** 切换完成时的 ISO 8601 时间。 */
102
+ switchedAt: string;
103
+ /** 本次切换写入各开发工具认证文件的结果。 */
104
+ targets: AuthTargetWriteData;
105
+ }
106
+ /**
107
+ * 返回所有已配置账号的安全摘要。
108
+ *
109
+ * 不会返回账号邮箱、内部账号 ID 或任何认证凭据。
110
+ */
111
+ declare const listAccounts: () => Promise<CdxApiResult<AccountListData>>;
112
+ /** 返回当前已经写入开发工具认证文件的账号信息。 */
113
+ declare const getCurrentAccount: () => Promise<CdxApiResult<CurrentAccountData>>;
114
+ /**
115
+ * 查询当前启用账号的周期用量和下一次重置时间。
116
+ *
117
+ * 此方法不会输出 CLI 文本,也不会返回令牌或原始上游响应。
118
+ */
119
+ declare const getCurrentAccountUsage: () => Promise<CdxApiResult<CurrentAccountUsageData>>;
120
+ /** 按配置顺序切换到下一个账号。 */
121
+ declare const switchNextAccount: () => Promise<CdxApiResult<AccountSwitchData>>;
122
+ /**
123
+ * 按用户设置的唯一标签切换账号。
124
+ *
125
+ * 不接受账号邮箱或内部账号 ID;请先通过 CLI 的 `cdx label` 为账号设置标签。
126
+ */
127
+ declare const switchToAccount: (input: SwitchToAccountInput) => Promise<CdxApiResult<AccountSwitchData>>;
128
+ //#endregion
129
+ export { AccountListData, AccountSummary, AccountSwitchData, AuthTargetWriteData, CdxApiError, CdxApiErrorCode, CdxApiResult, CreditsData, CurrentAccountData, CurrentAccountUsageData, SwitchToAccountInput, UsageWindowData, getCurrentAccount, getCurrentAccountUsage, listAccounts, switchNextAccount, switchToAccount };
package/api.mjs ADDED
@@ -0,0 +1,164 @@
1
+ import { D as loadConfig, O as loadConfiguredSecretStoreSelection, m as createSecretStoreAdapterFromSelection, n as fetchUsage, t as switchAccountAtIndex } from "./account-switch-BDEmlyn5.mjs";
2
+ //#region api.ts
3
+ const success = (data) => ({
4
+ ok: true,
5
+ data
6
+ });
7
+ const failure = (code, message, retryable) => ({
8
+ ok: false,
9
+ error: {
10
+ code,
11
+ message,
12
+ retryable
13
+ }
14
+ });
15
+ const formatAccount = (account, index, currentIndex) => {
16
+ const label = account.label?.trim() || null;
17
+ return {
18
+ label,
19
+ displayName: label ?? `未命名账号 ${index + 1}`,
20
+ isCurrent: index === currentIndex
21
+ };
22
+ };
23
+ const loadApiConfig = async () => {
24
+ try {
25
+ return success(await loadConfig());
26
+ } catch {
27
+ return failure("CONFIGURATION_ERROR", "无法读取 cdx 账号配置。", false);
28
+ }
29
+ };
30
+ const resolveCurrentAccount = (config) => {
31
+ const account = config.accounts[config.current];
32
+ if (!account?.accountId) return failure("CURRENT_ACCOUNT_UNAVAILABLE", "当前启用账号不存在或账号配置无效。", false);
33
+ return success({
34
+ account,
35
+ index: config.current
36
+ });
37
+ };
38
+ const createConfiguredSecretStore = async () => {
39
+ try {
40
+ return success(createSecretStoreAdapterFromSelection(await loadConfiguredSecretStoreSelection() ?? "auto"));
41
+ } catch {
42
+ return failure("SECRET_STORE_UNAVAILABLE", "无法初始化系统凭据库。", false);
43
+ }
44
+ };
45
+ const mapUsageFailure = (result) => {
46
+ if (result.error.type === "auth_failed") return failure("AUTH_FAILED", "当前账号凭据不可用,请使用 CLI 完成登录或重新登录。", false);
47
+ if (result.error.type === "network_error") return failure("NETWORK_ERROR", "查询账号用量时网络请求失败。", true);
48
+ return failure("USAGE_UNAVAILABLE", "当前无法获取账号用量。", true);
49
+ };
50
+ const mapUsageWindow = (kind, snapshot, checkedAtMs) => {
51
+ const resetsAtUnixMs = snapshot.reset_at * 1e3;
52
+ return {
53
+ kind,
54
+ usedPercent: snapshot.used_percent,
55
+ remainingPercent: Math.max(0, 100 - snapshot.used_percent),
56
+ limitWindowSeconds: snapshot.limit_window_seconds,
57
+ resetsAt: new Date(resetsAtUnixMs).toISOString(),
58
+ resetsAtUnixMs,
59
+ resetsInMs: Math.max(0, resetsAtUnixMs - checkedAtMs)
60
+ };
61
+ };
62
+ const mapAuthTargets = (authResult) => ({
63
+ openCode: "written",
64
+ pi: "written",
65
+ codex: authResult.codexWritten ? "written" : authResult.codexCleared ? "cleared" : "skipped"
66
+ });
67
+ /**
68
+ * 返回所有已配置账号的安全摘要。
69
+ *
70
+ * 不会返回账号邮箱、内部账号 ID 或任何认证凭据。
71
+ */
72
+ const listAccounts = async () => {
73
+ const configResult = await loadApiConfig();
74
+ if (!configResult.ok) return configResult;
75
+ return success({ accounts: configResult.data.accounts.map((account, index) => formatAccount(account, index, configResult.data.current)) });
76
+ };
77
+ /** 返回当前已经写入开发工具认证文件的账号信息。 */
78
+ const getCurrentAccount = async () => {
79
+ const configResult = await loadApiConfig();
80
+ if (!configResult.ok) return configResult;
81
+ const currentResult = resolveCurrentAccount(configResult.data);
82
+ if (!currentResult.ok) return currentResult;
83
+ return success({ account: formatAccount(currentResult.data.account, currentResult.data.index, configResult.data.current) });
84
+ };
85
+ /**
86
+ * 查询当前启用账号的周期用量和下一次重置时间。
87
+ *
88
+ * 此方法不会输出 CLI 文本,也不会返回令牌或原始上游响应。
89
+ */
90
+ const getCurrentAccountUsage = async () => {
91
+ const configResult = await loadApiConfig();
92
+ if (!configResult.ok) return configResult;
93
+ const currentResult = resolveCurrentAccount(configResult.data);
94
+ if (!currentResult.ok) return currentResult;
95
+ const secretStoreResult = await createConfiguredSecretStore();
96
+ if (!secretStoreResult.ok) return secretStoreResult;
97
+ const usageResult = await fetchUsage(currentResult.data.account.accountId, secretStoreResult.data);
98
+ if (!usageResult.ok) return mapUsageFailure(usageResult);
99
+ try {
100
+ const checkedAtMs = Date.now();
101
+ const windows = [];
102
+ if (usageResult.data.rate_limit?.primary_window) windows.push(mapUsageWindow("primary", usageResult.data.rate_limit.primary_window, checkedAtMs));
103
+ if (usageResult.data.rate_limit?.secondary_window) windows.push(mapUsageWindow("secondary", usageResult.data.rate_limit.secondary_window, checkedAtMs));
104
+ const credits = usageResult.data.credits ? {
105
+ hasCredits: usageResult.data.credits.has_credits,
106
+ unlimited: usageResult.data.credits.unlimited,
107
+ balance: usageResult.data.credits.balance ?? null
108
+ } : null;
109
+ return success({
110
+ account: formatAccount(currentResult.data.account, currentResult.data.index, configResult.data.current),
111
+ checkedAt: new Date(checkedAtMs).toISOString(),
112
+ planType: usageResult.data.plan_type ?? null,
113
+ windows,
114
+ credits
115
+ });
116
+ } catch {
117
+ return failure("USAGE_UNAVAILABLE", "上游用量数据格式异常,暂时无法解析。", true);
118
+ }
119
+ };
120
+ const switchToIndex = async (config, targetIndex) => {
121
+ const currentResult = resolveCurrentAccount(config);
122
+ if (!currentResult.ok) return currentResult;
123
+ const secretStoreResult = await createConfiguredSecretStore();
124
+ if (!secretStoreResult.ok) return secretStoreResult;
125
+ try {
126
+ const result = await switchAccountAtIndex(config, targetIndex, secretStoreResult.data);
127
+ return success({
128
+ previousAccount: formatAccount(currentResult.data.account, currentResult.data.index, currentResult.data.index),
129
+ currentAccount: formatAccount(result.account, result.currentIndex, result.currentIndex),
130
+ changed: result.previousIndex !== result.currentIndex,
131
+ switchedAt: (/* @__PURE__ */ new Date()).toISOString(),
132
+ targets: mapAuthTargets(result.authResult)
133
+ });
134
+ } catch {
135
+ return failure("CREDENTIAL_UNAVAILABLE", "无法读取目标账号凭据或更新认证文件。", true);
136
+ }
137
+ };
138
+ /** 按配置顺序切换到下一个账号。 */
139
+ const switchNextAccount = async () => {
140
+ const configResult = await loadApiConfig();
141
+ if (!configResult.ok) return configResult;
142
+ const nextIndex = (configResult.data.current + 1) % configResult.data.accounts.length;
143
+ return switchToIndex(configResult.data, nextIndex);
144
+ };
145
+ /**
146
+ * 按用户设置的唯一标签切换账号。
147
+ *
148
+ * 不接受账号邮箱或内部账号 ID;请先通过 CLI 的 `cdx label` 为账号设置标签。
149
+ */
150
+ const switchToAccount = async (input) => {
151
+ const label = typeof input?.label === "string" ? input.label.trim() : "";
152
+ if (!label) return failure("ACCOUNT_LABEL_REQUIRED", "切换账号时必须提供非空标签。", false);
153
+ const configResult = await loadApiConfig();
154
+ if (!configResult.ok) return configResult;
155
+ const matchingIndices = configResult.data.accounts.map((account, index) => ({
156
+ account,
157
+ index
158
+ })).filter(({ account }) => account.label === label).map(({ index }) => index);
159
+ if (matchingIndices.length === 0) return failure("ACCOUNT_LABEL_NOT_FOUND", `未找到标签为“${label}”的账号。`, false);
160
+ if (matchingIndices.length > 1) return failure("ACCOUNT_LABEL_DUPLICATED", `账号标签“${label}”重复,无法确定切换目标。`, false);
161
+ return switchToIndex(configResult.data, matchingIndices[0]);
162
+ };
163
+ //#endregion
164
+ export { getCurrentAccount, getCurrentAccountUsage, listAccounts, switchNextAccount, switchToAccount };
package/cdx.d.mts ADDED
@@ -0,0 +1,105 @@
1
+ import { Command } from "commander";
2
+
3
+ //#region lib/oauth/login.d.ts
4
+ type AuthFlowMode = "auto" | "device";
5
+ type PerformLoginOptions = {
6
+ authFlow?: AuthFlowMode;
7
+ };
8
+ declare const performLogin: (options?: PerformLoginOptions) => Promise<{
9
+ accountId: string;
10
+ } | null>;
11
+ //#endregion
12
+ //#region lib/commands/login.d.ts
13
+ type LoginDeps = {
14
+ performLogin?: typeof performLogin;
15
+ };
16
+ //#endregion
17
+ //#region lib/commands/switch.d.ts
18
+ declare const switchNext: () => Promise<void>;
19
+ declare const switchToAccount: (identifier: string) => Promise<void>;
20
+ //#endregion
21
+ //#region lib/types.d.ts
22
+ type SecretStoreSelection = "auto" | "legacy-keychain";
23
+ type AccountRecord = {
24
+ accountId: string;
25
+ keychainService: string;
26
+ label?: string;
27
+ };
28
+ type Config = {
29
+ current: number;
30
+ accounts: AccountRecord[];
31
+ secretStore?: SecretStoreSelection;
32
+ };
33
+ type OAuthPayload = {
34
+ refresh: string;
35
+ access: string;
36
+ expires: number;
37
+ accountId: string;
38
+ idToken?: string;
39
+ };
40
+ //#endregion
41
+ //#region lib/secrets/macos-cross-keychain.d.ts
42
+ type BackendId = "native-macos" | "macos";
43
+ type MacOSCrossKeychainBackendId = BackendId;
44
+ //#endregion
45
+ //#region lib/secrets/store.d.ts
46
+ type SecretStoreCapability = {
47
+ available: boolean;
48
+ reason?: string;
49
+ };
50
+ type SecretStoreAdapter = {
51
+ id: string;
52
+ label: string;
53
+ getServiceName(accountId: string): string;
54
+ save(accountId: string, payload: OAuthPayload): Promise<void>;
55
+ load(accountId: string): Promise<OAuthPayload>;
56
+ delete(accountId: string): Promise<void>;
57
+ exists(accountId: string): Promise<boolean>;
58
+ listAccountIds(): Promise<string[]>;
59
+ getCapability(): SecretStoreCapability;
60
+ };
61
+ declare const createRuntimeSecretStoreAdapter: (platform?: NodeJS.Platform) => SecretStoreAdapter;
62
+ declare const createSecretStoreAdapterFromSelection: (selection?: SecretStoreSelection, platform?: NodeJS.Platform) => SecretStoreAdapter;
63
+ declare const resolveMacOSCrossKeychainBackendId: (platform?: NodeJS.Platform) => Promise<MacOSCrossKeychainBackendId | null>;
64
+ declare const getSecretStoreAdapter: () => SecretStoreAdapter;
65
+ declare const setSecretStoreAdapter: (adapter: SecretStoreAdapter) => void;
66
+ declare const resetSecretStoreAdapter: () => void;
67
+ //#endregion
68
+ //#region lib/config.d.ts
69
+ declare const loadConfig: () => Promise<Config>;
70
+ declare const saveConfig: (config: Config) => Promise<void>;
71
+ //#endregion
72
+ //#region lib/auth.d.ts
73
+ declare const writeAuthFile: (payload: OAuthPayload) => Promise<void>;
74
+ declare const writeCodexAuthFile: (payload: OAuthPayload) => Promise<void>;
75
+ declare const writePiAuthFile: (payload: OAuthPayload) => Promise<void>;
76
+ type WriteAuthResult = {
77
+ piWritten: boolean;
78
+ codexWritten: boolean;
79
+ codexMissingIdToken: boolean;
80
+ codexCleared: boolean;
81
+ };
82
+ declare const writeAllAuthFiles: (payload: OAuthPayload) => Promise<WriteAuthResult>;
83
+ //#endregion
84
+ //#region lib/paths.d.ts
85
+ type PathConfig = {
86
+ configDir: string;
87
+ configPath: string;
88
+ authPath: string;
89
+ codexAuthPath: string;
90
+ piAuthPath: string;
91
+ };
92
+ declare const getPaths: () => PathConfig;
93
+ declare const setPaths: (paths: Partial<PathConfig>) => void;
94
+ declare const resetPaths: () => void;
95
+ declare const createTestPaths: (testDir: string) => PathConfig;
96
+ //#endregion
97
+ //#region lib/interactive.d.ts
98
+ declare const runInteractiveMode: () => Promise<void>;
99
+ //#endregion
100
+ //#region cdx.d.ts
101
+ declare const interactiveMode: () => Promise<void>;
102
+ declare const getMacOSKeychainPromptWarning: (selection: SecretStoreSelection, platform?: NodeJS.Platform, backendId?: MacOSCrossKeychainBackendId | null) => string | null;
103
+ declare const createProgram: (deps?: LoginDeps) => Command;
104
+ //#endregion
105
+ export { type AccountRecord, type Config, type MacOSCrossKeychainBackendId, type OAuthPayload, type SecretStoreAdapter, type SecretStoreSelection, createProgram, createRuntimeSecretStoreAdapter, createSecretStoreAdapterFromSelection, createTestPaths, getMacOSKeychainPromptWarning, getPaths, getSecretStoreAdapter, interactiveMode, loadConfig, resetPaths, resetSecretStoreAdapter, resolveMacOSCrossKeychainBackendId, runInteractiveMode, saveConfig, setPaths, setSecretStoreAdapter, switchNext, switchToAccount, writeAllAuthFiles, writeAuthFile, writeCodexAuthFile, writePiAuthFile };