@imdeadpool/codex-account-switcher 0.1.5 → 0.1.7
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.md +79 -5
- package/dist/commands/config.d.ts +15 -0
- package/dist/commands/config.js +81 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +39 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +30 -5
- package/dist/commands/login.d.ts +15 -0
- package/dist/commands/login.js +97 -0
- package/dist/commands/remove.d.ts +14 -0
- package/dist/commands/remove.js +104 -0
- package/dist/commands/save.d.ts +4 -1
- package/dist/commands/save.js +24 -6
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +16 -0
- package/dist/lib/accounts/account-service.d.ts +59 -2
- package/dist/lib/accounts/account-service.js +551 -36
- package/dist/lib/accounts/auth-parser.d.ts +3 -0
- package/dist/lib/accounts/auth-parser.js +83 -0
- package/dist/lib/accounts/errors.d.ts +15 -0
- package/dist/lib/accounts/errors.js +34 -2
- package/dist/lib/accounts/index.d.ts +3 -1
- package/dist/lib/accounts/index.js +5 -1
- package/dist/lib/accounts/registry.d.ts +6 -0
- package/dist/lib/accounts/registry.js +166 -0
- package/dist/lib/accounts/service-manager.d.ts +4 -0
- package/dist/lib/accounts/service-manager.js +204 -0
- package/dist/lib/accounts/types.d.ts +71 -0
- package/dist/lib/accounts/types.js +5 -0
- package/dist/lib/accounts/usage.d.ts +10 -0
- package/dist/lib/accounts/usage.js +246 -0
- package/dist/lib/base-command.d.ts +1 -0
- package/dist/lib/base-command.js +4 -0
- package/dist/lib/config/paths.d.ts +6 -0
- package/dist/lib/config/paths.js +46 -5
- package/dist/tests/auth-parser.test.d.ts +1 -0
- package/dist/tests/auth-parser.test.js +65 -0
- package/dist/tests/registry.test.d.ts +1 -0
- package/dist/tests/registry.test.js +37 -0
- package/dist/tests/save-account-safety.test.d.ts +1 -0
- package/dist/tests/save-account-safety.test.js +399 -0
- package/dist/tests/usage.test.d.ts +1 -0
- package/dist/tests/usage.test.js +29 -0
- package/package.json +9 -6
- package/scripts/postinstall-login-hook.cjs +90 -0
|
@@ -9,64 +9,451 @@ const promises_1 = __importDefault(require("node:fs/promises"));
|
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
10
|
const paths_1 = require("../config/paths");
|
|
11
11
|
const errors_1 = require("./errors");
|
|
12
|
-
const
|
|
12
|
+
const auth_parser_1 = require("./auth-parser");
|
|
13
|
+
const registry_1 = require("./registry");
|
|
14
|
+
const usage_1 = require("./usage");
|
|
15
|
+
const service_manager_1 = require("./service-manager");
|
|
16
|
+
const ACCOUNT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._@+-]*$/;
|
|
13
17
|
class AccountService {
|
|
18
|
+
async syncExternalAuthSnapshotIfNeeded() {
|
|
19
|
+
const authPath = (0, paths_1.resolveAuthPath)();
|
|
20
|
+
if (!(await this.pathExists(authPath))) {
|
|
21
|
+
return {
|
|
22
|
+
synchronized: false,
|
|
23
|
+
autoSwitchDisabled: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
await this.materializeAuthSymlink(authPath);
|
|
27
|
+
const incomingSnapshot = await (0, auth_parser_1.parseAuthSnapshotFile)(authPath);
|
|
28
|
+
if (incomingSnapshot.authMode !== "chatgpt") {
|
|
29
|
+
return {
|
|
30
|
+
synchronized: false,
|
|
31
|
+
autoSwitchDisabled: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const activeName = await this.getCurrentAccountName();
|
|
35
|
+
if (activeName) {
|
|
36
|
+
const activeSnapshotPath = this.accountFilePath(activeName);
|
|
37
|
+
if (await this.pathExists(activeSnapshotPath)) {
|
|
38
|
+
const activeSnapshot = await (0, auth_parser_1.parseAuthSnapshotFile)(activeSnapshotPath);
|
|
39
|
+
if (this.snapshotsShareIdentity(activeSnapshot, incomingSnapshot)) {
|
|
40
|
+
return {
|
|
41
|
+
synchronized: false,
|
|
42
|
+
autoSwitchDisabled: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const status = await this.getStatus();
|
|
48
|
+
const autoSwitchDisabled = status.autoSwitchEnabled;
|
|
49
|
+
if (autoSwitchDisabled) {
|
|
50
|
+
await this.setAutoSwitchEnabled(false);
|
|
51
|
+
}
|
|
52
|
+
const resolvedName = await this.resolveLoginAccountNameFromCurrentAuth();
|
|
53
|
+
const savedName = await this.saveAccount(resolvedName.name);
|
|
54
|
+
return {
|
|
55
|
+
synchronized: true,
|
|
56
|
+
savedName,
|
|
57
|
+
autoSwitchDisabled,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
14
60
|
async listAccountNames() {
|
|
15
|
-
|
|
61
|
+
const accountsDir = (0, paths_1.resolveAccountsDir)();
|
|
62
|
+
if (!(await this.pathExists(accountsDir))) {
|
|
16
63
|
return [];
|
|
17
64
|
}
|
|
18
|
-
const entries = await promises_1.default.readdir(
|
|
65
|
+
const entries = await promises_1.default.readdir(accountsDir, { withFileTypes: true });
|
|
19
66
|
return entries
|
|
20
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
67
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "registry.json")
|
|
21
68
|
.map((entry) => entry.name.replace(/\.json$/i, ""))
|
|
22
69
|
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
|
|
23
70
|
}
|
|
71
|
+
async listAccountChoices() {
|
|
72
|
+
const [accounts, current, registry] = await Promise.all([
|
|
73
|
+
this.listAccountNames(),
|
|
74
|
+
this.getCurrentAccountName(),
|
|
75
|
+
this.loadReconciledRegistry(),
|
|
76
|
+
]);
|
|
77
|
+
return accounts.map((name) => {
|
|
78
|
+
var _a;
|
|
79
|
+
return ({
|
|
80
|
+
name,
|
|
81
|
+
email: (_a = registry.accounts[name]) === null || _a === void 0 ? void 0 : _a.email,
|
|
82
|
+
active: current === name,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async listAccountMappings() {
|
|
87
|
+
const [accounts, current, registry] = await Promise.all([
|
|
88
|
+
this.listAccountNames(),
|
|
89
|
+
this.getCurrentAccountName(),
|
|
90
|
+
this.loadReconciledRegistry(),
|
|
91
|
+
]);
|
|
92
|
+
return Promise.all(accounts.map(async (name) => {
|
|
93
|
+
var _a, _b, _c, _d, _e;
|
|
94
|
+
const entry = registry.accounts[name];
|
|
95
|
+
let fallbackSnapshot;
|
|
96
|
+
if (!(entry === null || entry === void 0 ? void 0 : entry.email) || !(entry === null || entry === void 0 ? void 0 : entry.accountId) || !(entry === null || entry === void 0 ? void 0 : entry.userId) || !(entry === null || entry === void 0 ? void 0 : entry.planType)) {
|
|
97
|
+
fallbackSnapshot = await (0, auth_parser_1.parseAuthSnapshotFile)(this.accountFilePath(name));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
name,
|
|
101
|
+
active: current === name,
|
|
102
|
+
email: (_a = entry === null || entry === void 0 ? void 0 : entry.email) !== null && _a !== void 0 ? _a : fallbackSnapshot === null || fallbackSnapshot === void 0 ? void 0 : fallbackSnapshot.email,
|
|
103
|
+
accountId: (_b = entry === null || entry === void 0 ? void 0 : entry.accountId) !== null && _b !== void 0 ? _b : fallbackSnapshot === null || fallbackSnapshot === void 0 ? void 0 : fallbackSnapshot.accountId,
|
|
104
|
+
userId: (_c = entry === null || entry === void 0 ? void 0 : entry.userId) !== null && _c !== void 0 ? _c : fallbackSnapshot === null || fallbackSnapshot === void 0 ? void 0 : fallbackSnapshot.userId,
|
|
105
|
+
planType: (_d = entry === null || entry === void 0 ? void 0 : entry.planType) !== null && _d !== void 0 ? _d : fallbackSnapshot === null || fallbackSnapshot === void 0 ? void 0 : fallbackSnapshot.planType,
|
|
106
|
+
lastUsageAt: entry === null || entry === void 0 ? void 0 : entry.lastUsageAt,
|
|
107
|
+
usageSource: (_e = entry === null || entry === void 0 ? void 0 : entry.lastUsage) === null || _e === void 0 ? void 0 : _e.source,
|
|
108
|
+
};
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
async findMatchingAccounts(query) {
|
|
112
|
+
const normalized = query.trim().toLowerCase();
|
|
113
|
+
if (!normalized)
|
|
114
|
+
return [];
|
|
115
|
+
const choices = await this.listAccountChoices();
|
|
116
|
+
const registry = await this.loadReconciledRegistry();
|
|
117
|
+
return choices.filter((choice) => {
|
|
118
|
+
var _a, _b;
|
|
119
|
+
if (choice.name.toLowerCase().includes(normalized))
|
|
120
|
+
return true;
|
|
121
|
+
if (choice.email && choice.email.toLowerCase().includes(normalized))
|
|
122
|
+
return true;
|
|
123
|
+
const meta = registry.accounts[choice.name];
|
|
124
|
+
if ((_a = meta === null || meta === void 0 ? void 0 : meta.accountId) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(normalized))
|
|
125
|
+
return true;
|
|
126
|
+
if ((_b = meta === null || meta === void 0 ? void 0 : meta.userId) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes(normalized))
|
|
127
|
+
return true;
|
|
128
|
+
return false;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
24
131
|
async getCurrentAccountName() {
|
|
25
|
-
const
|
|
132
|
+
const currentNamePath = (0, paths_1.resolveCurrentNamePath)();
|
|
133
|
+
const currentName = await this.readCurrentNameFile(currentNamePath);
|
|
26
134
|
if (currentName)
|
|
27
135
|
return currentName;
|
|
28
|
-
|
|
136
|
+
const authPath = (0, paths_1.resolveAuthPath)();
|
|
137
|
+
if (!(await this.pathExists(authPath)))
|
|
29
138
|
return null;
|
|
30
|
-
const stat = await promises_1.default.lstat(
|
|
139
|
+
const stat = await promises_1.default.lstat(authPath);
|
|
31
140
|
if (!stat.isSymbolicLink())
|
|
32
141
|
return null;
|
|
33
|
-
const rawTarget = await promises_1.default.readlink(
|
|
34
|
-
const resolvedTarget = node_path_1.default.resolve(node_path_1.default.dirname(
|
|
35
|
-
const accountsRoot = node_path_1.default.resolve(paths_1.
|
|
142
|
+
const rawTarget = await promises_1.default.readlink(authPath);
|
|
143
|
+
const resolvedTarget = node_path_1.default.resolve(node_path_1.default.dirname(authPath), rawTarget);
|
|
144
|
+
const accountsRoot = node_path_1.default.resolve((0, paths_1.resolveAccountsDir)());
|
|
36
145
|
const relative = node_path_1.default.relative(accountsRoot, resolvedTarget);
|
|
37
146
|
if (relative.startsWith(".."))
|
|
38
147
|
return null;
|
|
39
148
|
const base = node_path_1.default.basename(resolvedTarget);
|
|
149
|
+
if (!base.endsWith(".json") || base === "registry.json")
|
|
150
|
+
return null;
|
|
40
151
|
return base.replace(/\.json$/i, "");
|
|
41
152
|
}
|
|
42
|
-
async saveAccount(rawName) {
|
|
153
|
+
async saveAccount(rawName, options) {
|
|
43
154
|
const name = this.normalizeAccountName(rawName);
|
|
44
|
-
|
|
45
|
-
|
|
155
|
+
const authPath = (0, paths_1.resolveAuthPath)();
|
|
156
|
+
const accountsDir = (0, paths_1.resolveAccountsDir)();
|
|
157
|
+
await this.ensureAuthFileExists(authPath);
|
|
158
|
+
await this.ensureDir(accountsDir);
|
|
46
159
|
const destination = this.accountFilePath(name);
|
|
47
|
-
await
|
|
160
|
+
await this.assertSafeSnapshotOverwrite({
|
|
161
|
+
authPath,
|
|
162
|
+
destinationPath: destination,
|
|
163
|
+
accountName: name,
|
|
164
|
+
force: Boolean(options === null || options === void 0 ? void 0 : options.force),
|
|
165
|
+
});
|
|
166
|
+
await promises_1.default.copyFile(authPath, destination);
|
|
48
167
|
await this.writeCurrentName(name);
|
|
168
|
+
const registry = await this.loadReconciledRegistry();
|
|
169
|
+
await this.hydrateSnapshotMetadata(registry, name);
|
|
170
|
+
registry.activeAccountName = name;
|
|
171
|
+
await this.persistRegistry(registry);
|
|
49
172
|
return name;
|
|
50
173
|
}
|
|
174
|
+
async inferAccountNameFromCurrentAuth() {
|
|
175
|
+
var _a;
|
|
176
|
+
const authPath = (0, paths_1.resolveAuthPath)();
|
|
177
|
+
await this.ensureAuthFileExists(authPath);
|
|
178
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(authPath);
|
|
179
|
+
const email = (_a = parsed.email) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
180
|
+
if (!email || !email.includes("@")) {
|
|
181
|
+
throw new errors_1.AccountNameInferenceError();
|
|
182
|
+
}
|
|
183
|
+
const baseCandidate = this.normalizeAccountName(email);
|
|
184
|
+
const uniqueName = await this.resolveUniqueInferredName(baseCandidate, parsed);
|
|
185
|
+
return uniqueName;
|
|
186
|
+
}
|
|
187
|
+
async resolveDefaultAccountNameFromCurrentAuth() {
|
|
188
|
+
const authPath = (0, paths_1.resolveAuthPath)();
|
|
189
|
+
await this.ensureAuthFileExists(authPath);
|
|
190
|
+
const activeName = await this.getCurrentAccountName();
|
|
191
|
+
if (activeName) {
|
|
192
|
+
const activeSnapshotPath = this.accountFilePath(activeName);
|
|
193
|
+
if (await this.pathExists(activeSnapshotPath)) {
|
|
194
|
+
const [activeSnapshot, incomingSnapshot] = await Promise.all([
|
|
195
|
+
(0, auth_parser_1.parseAuthSnapshotFile)(activeSnapshotPath),
|
|
196
|
+
(0, auth_parser_1.parseAuthSnapshotFile)(authPath),
|
|
197
|
+
]);
|
|
198
|
+
if (this.snapshotsShareIdentity(activeSnapshot, incomingSnapshot)) {
|
|
199
|
+
return {
|
|
200
|
+
name: activeName,
|
|
201
|
+
source: "active",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
name: await this.inferAccountNameFromCurrentAuth(),
|
|
208
|
+
source: "inferred",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async resolveLoginAccountNameFromCurrentAuth() {
|
|
212
|
+
return {
|
|
213
|
+
name: await this.inferAccountNameFromCurrentAuth(),
|
|
214
|
+
source: "inferred",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
51
217
|
async useAccount(rawName) {
|
|
52
218
|
const name = this.normalizeAccountName(rawName);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
219
|
+
await this.activateSnapshot(name);
|
|
220
|
+
const registry = await this.loadReconciledRegistry();
|
|
221
|
+
await this.hydrateSnapshotMetadata(registry, name);
|
|
222
|
+
registry.activeAccountName = name;
|
|
223
|
+
await this.persistRegistry(registry);
|
|
224
|
+
return name;
|
|
225
|
+
}
|
|
226
|
+
async removeAccounts(accountNames) {
|
|
227
|
+
const uniqueNames = [...new Set(accountNames.map((name) => this.normalizeAccountName(name)))];
|
|
228
|
+
if (uniqueNames.length === 0) {
|
|
229
|
+
return { removed: [] };
|
|
56
230
|
}
|
|
57
|
-
await this.
|
|
58
|
-
await this.
|
|
59
|
-
|
|
60
|
-
|
|
231
|
+
const current = await this.getCurrentAccountName();
|
|
232
|
+
const registry = await this.loadReconciledRegistry();
|
|
233
|
+
const removed = [];
|
|
234
|
+
for (const name of uniqueNames) {
|
|
235
|
+
const snapshotPath = this.accountFilePath(name);
|
|
236
|
+
if (!(await this.pathExists(snapshotPath))) {
|
|
237
|
+
throw new errors_1.AccountNotFoundError(name);
|
|
238
|
+
}
|
|
239
|
+
await promises_1.default.rm(snapshotPath, { force: true });
|
|
240
|
+
delete registry.accounts[name];
|
|
241
|
+
removed.push(name);
|
|
242
|
+
}
|
|
243
|
+
const removedSet = new Set(removed);
|
|
244
|
+
let activated;
|
|
245
|
+
if (current && removedSet.has(current)) {
|
|
246
|
+
const remaining = (await this.listAccountNames()).filter((name) => !removedSet.has(name));
|
|
247
|
+
if (remaining.length > 0) {
|
|
248
|
+
const best = this.selectBestCandidateFromRegistry(remaining, registry);
|
|
249
|
+
await this.activateSnapshot(best);
|
|
250
|
+
activated = best;
|
|
251
|
+
registry.activeAccountName = best;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
await this.clearActivePointers();
|
|
255
|
+
delete registry.activeAccountName;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (registry.activeAccountName && removedSet.has(registry.activeAccountName)) {
|
|
259
|
+
delete registry.activeAccountName;
|
|
260
|
+
}
|
|
261
|
+
await this.persistRegistry(registry);
|
|
262
|
+
return {
|
|
263
|
+
removed,
|
|
264
|
+
activated,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async removeByQuery(query) {
|
|
268
|
+
const matches = await this.findMatchingAccounts(query);
|
|
269
|
+
if (matches.length === 0) {
|
|
270
|
+
throw new errors_1.AccountNotFoundError(query);
|
|
271
|
+
}
|
|
272
|
+
if (matches.length > 1) {
|
|
273
|
+
throw new errors_1.AmbiguousAccountQueryError(query);
|
|
274
|
+
}
|
|
275
|
+
return this.removeAccounts([matches[0].name]);
|
|
276
|
+
}
|
|
277
|
+
async removeAllAccounts() {
|
|
278
|
+
const all = await this.listAccountNames();
|
|
279
|
+
return this.removeAccounts(all);
|
|
280
|
+
}
|
|
281
|
+
async getStatus() {
|
|
282
|
+
const registry = await this.loadReconciledRegistry();
|
|
283
|
+
return {
|
|
284
|
+
autoSwitchEnabled: registry.autoSwitch.enabled,
|
|
285
|
+
serviceState: (0, service_manager_1.getManagedServiceState)(),
|
|
286
|
+
threshold5hPercent: registry.autoSwitch.threshold5hPercent,
|
|
287
|
+
thresholdWeeklyPercent: registry.autoSwitch.thresholdWeeklyPercent,
|
|
288
|
+
usageMode: registry.api.usage ? "api" : "local",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async setAutoSwitchEnabled(enabled) {
|
|
292
|
+
const registry = await this.loadReconciledRegistry();
|
|
293
|
+
registry.autoSwitch.enabled = enabled;
|
|
294
|
+
if (enabled) {
|
|
295
|
+
try {
|
|
296
|
+
await (0, service_manager_1.enableManagedService)();
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
registry.autoSwitch.enabled = false;
|
|
300
|
+
await this.persistRegistry(registry);
|
|
301
|
+
throw new errors_1.AutoSwitchConfigError(`Failed to enable managed auto-switch service: ${error.message}`);
|
|
302
|
+
}
|
|
61
303
|
}
|
|
62
304
|
else {
|
|
63
|
-
await
|
|
305
|
+
await (0, service_manager_1.disableManagedService)();
|
|
64
306
|
}
|
|
65
|
-
await this.
|
|
66
|
-
return
|
|
307
|
+
await this.persistRegistry(registry);
|
|
308
|
+
return this.getStatus();
|
|
309
|
+
}
|
|
310
|
+
async setApiUsageEnabled(enabled) {
|
|
311
|
+
const registry = await this.loadReconciledRegistry();
|
|
312
|
+
registry.api.usage = enabled;
|
|
313
|
+
await this.persistRegistry(registry);
|
|
314
|
+
return this.getStatus();
|
|
315
|
+
}
|
|
316
|
+
async configureAutoSwitchThresholds(input) {
|
|
317
|
+
const registry = await this.loadReconciledRegistry();
|
|
318
|
+
if (typeof input.threshold5hPercent === "number") {
|
|
319
|
+
if (!this.isValidPercent(input.threshold5hPercent)) {
|
|
320
|
+
throw new errors_1.AutoSwitchConfigError("`--5h` must be an integer from 1 to 100.");
|
|
321
|
+
}
|
|
322
|
+
registry.autoSwitch.threshold5hPercent = Math.round(input.threshold5hPercent);
|
|
323
|
+
}
|
|
324
|
+
if (typeof input.thresholdWeeklyPercent === "number") {
|
|
325
|
+
if (!this.isValidPercent(input.thresholdWeeklyPercent)) {
|
|
326
|
+
throw new errors_1.AutoSwitchConfigError("`--weekly` must be an integer from 1 to 100.");
|
|
327
|
+
}
|
|
328
|
+
registry.autoSwitch.thresholdWeeklyPercent = Math.round(input.thresholdWeeklyPercent);
|
|
329
|
+
}
|
|
330
|
+
await this.persistRegistry(registry);
|
|
331
|
+
return this.getStatus();
|
|
332
|
+
}
|
|
333
|
+
async runAutoSwitchOnce() {
|
|
334
|
+
var _a, _b, _c;
|
|
335
|
+
const registry = await this.loadReconciledRegistry();
|
|
336
|
+
if (!registry.autoSwitch.enabled) {
|
|
337
|
+
return { switched: false, reason: "auto-switch is disabled" };
|
|
338
|
+
}
|
|
339
|
+
const accountNames = await this.listAccountNames();
|
|
340
|
+
if (accountNames.length === 0) {
|
|
341
|
+
return { switched: false, reason: "no saved accounts" };
|
|
342
|
+
}
|
|
343
|
+
const active = (_a = (await this.getCurrentAccountName())) !== null && _a !== void 0 ? _a : registry.activeAccountName;
|
|
344
|
+
if (!active || !accountNames.includes(active)) {
|
|
345
|
+
return { switched: false, reason: "no active account" };
|
|
346
|
+
}
|
|
347
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
348
|
+
const activeUsage = await this.refreshAccountUsage(registry, active, {
|
|
349
|
+
preferApi: registry.api.usage,
|
|
350
|
+
allowLocalFallback: true,
|
|
351
|
+
});
|
|
352
|
+
if (!(0, usage_1.shouldSwitchCurrent)(activeUsage, {
|
|
353
|
+
threshold5hPercent: registry.autoSwitch.threshold5hPercent,
|
|
354
|
+
thresholdWeeklyPercent: registry.autoSwitch.thresholdWeeklyPercent,
|
|
355
|
+
}, nowSeconds)) {
|
|
356
|
+
await this.persistRegistry(registry);
|
|
357
|
+
return { switched: false, reason: "active account is above configured thresholds" };
|
|
358
|
+
}
|
|
359
|
+
const currentScore = (_b = (0, usage_1.usageScore)(activeUsage, nowSeconds)) !== null && _b !== void 0 ? _b : 0;
|
|
360
|
+
let bestCandidate;
|
|
361
|
+
let bestScore = currentScore;
|
|
362
|
+
for (const candidate of accountNames) {
|
|
363
|
+
if (candidate === active)
|
|
364
|
+
continue;
|
|
365
|
+
const usage = await this.refreshAccountUsage(registry, candidate, {
|
|
366
|
+
preferApi: registry.api.usage,
|
|
367
|
+
allowLocalFallback: false,
|
|
368
|
+
});
|
|
369
|
+
const score = (_c = (0, usage_1.usageScore)(usage, nowSeconds)) !== null && _c !== void 0 ? _c : 100;
|
|
370
|
+
if (!bestCandidate || score > bestScore) {
|
|
371
|
+
bestCandidate = candidate;
|
|
372
|
+
bestScore = score;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!bestCandidate || bestScore <= currentScore) {
|
|
376
|
+
await this.persistRegistry(registry);
|
|
377
|
+
return {
|
|
378
|
+
switched: false,
|
|
379
|
+
reason: "no candidate has better remaining quota",
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
await this.activateSnapshot(bestCandidate);
|
|
383
|
+
registry.activeAccountName = bestCandidate;
|
|
384
|
+
await this.hydrateSnapshotMetadata(registry, bestCandidate);
|
|
385
|
+
await this.persistRegistry(registry);
|
|
386
|
+
return {
|
|
387
|
+
switched: true,
|
|
388
|
+
fromAccount: active,
|
|
389
|
+
toAccount: bestCandidate,
|
|
390
|
+
reason: "switched due to low credits on active account",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
async runDaemon(mode) {
|
|
394
|
+
if (mode === "once") {
|
|
395
|
+
await this.runAutoSwitchOnce();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
for (;;) {
|
|
399
|
+
try {
|
|
400
|
+
await this.runAutoSwitchOnce();
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// keep daemon alive
|
|
404
|
+
}
|
|
405
|
+
await new Promise((resolve) => setTimeout(resolve, 30000));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
selectBestCandidateFromRegistry(candidates, registry) {
|
|
409
|
+
var _a, _b, _c, _d;
|
|
410
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
411
|
+
let best = candidates[0];
|
|
412
|
+
let bestScore = (_b = (0, usage_1.usageScore)((_a = registry.accounts[best]) === null || _a === void 0 ? void 0 : _a.lastUsage, nowSeconds)) !== null && _b !== void 0 ? _b : -1;
|
|
413
|
+
for (const candidate of candidates.slice(1)) {
|
|
414
|
+
const score = (_d = (0, usage_1.usageScore)((_c = registry.accounts[candidate]) === null || _c === void 0 ? void 0 : _c.lastUsage, nowSeconds)) !== null && _d !== void 0 ? _d : -1;
|
|
415
|
+
if (score > bestScore) {
|
|
416
|
+
best = candidate;
|
|
417
|
+
bestScore = score;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return best;
|
|
421
|
+
}
|
|
422
|
+
async refreshAccountUsage(registry, accountName, options) {
|
|
423
|
+
var _a;
|
|
424
|
+
const snapshotPath = this.accountFilePath(accountName);
|
|
425
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(snapshotPath);
|
|
426
|
+
const entry = (_a = registry.accounts[accountName]) !== null && _a !== void 0 ? _a : {
|
|
427
|
+
name: accountName,
|
|
428
|
+
createdAt: new Date().toISOString(),
|
|
429
|
+
};
|
|
430
|
+
if (parsed.email)
|
|
431
|
+
entry.email = parsed.email;
|
|
432
|
+
if (parsed.accountId)
|
|
433
|
+
entry.accountId = parsed.accountId;
|
|
434
|
+
if (parsed.userId)
|
|
435
|
+
entry.userId = parsed.userId;
|
|
436
|
+
if (parsed.planType)
|
|
437
|
+
entry.planType = parsed.planType;
|
|
438
|
+
let usage = null;
|
|
439
|
+
if (options.preferApi) {
|
|
440
|
+
usage = await (0, usage_1.fetchUsageFromApi)(parsed);
|
|
441
|
+
}
|
|
442
|
+
if (!usage && options.allowLocalFallback) {
|
|
443
|
+
usage = await (0, usage_1.fetchUsageFromLocal)((0, paths_1.resolveCodexDir)());
|
|
444
|
+
}
|
|
445
|
+
if (usage) {
|
|
446
|
+
entry.lastUsage = usage;
|
|
447
|
+
entry.lastUsageAt = usage.fetchedAt;
|
|
448
|
+
if (usage.planType) {
|
|
449
|
+
entry.planType = usage.planType;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
registry.accounts[accountName] = entry;
|
|
453
|
+
return entry.lastUsage;
|
|
67
454
|
}
|
|
68
455
|
accountFilePath(name) {
|
|
69
|
-
return node_path_1.default.join(paths_1.
|
|
456
|
+
return node_path_1.default.join((0, paths_1.resolveAccountsDir)(), `${name}.json`);
|
|
70
457
|
}
|
|
71
458
|
normalizeAccountName(rawName) {
|
|
72
459
|
if (typeof rawName !== "string") {
|
|
@@ -82,18 +469,47 @@ class AccountService {
|
|
|
82
469
|
}
|
|
83
470
|
return withoutExtension;
|
|
84
471
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
472
|
+
isValidPercent(value) {
|
|
473
|
+
return Number.isFinite(value) && Number.isInteger(value) && value >= 1 && value <= 100;
|
|
474
|
+
}
|
|
475
|
+
async ensureAuthFileExists(authPath) {
|
|
476
|
+
if (!(await this.pathExists(authPath))) {
|
|
477
|
+
throw new errors_1.AuthFileMissingError(authPath);
|
|
88
478
|
}
|
|
89
479
|
}
|
|
90
480
|
async ensureDir(dirPath) {
|
|
91
481
|
await promises_1.default.mkdir(dirPath, { recursive: true });
|
|
92
482
|
}
|
|
93
|
-
async
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
|
|
483
|
+
async materializeAuthSymlink(authPath) {
|
|
484
|
+
const stat = await promises_1.default.lstat(authPath);
|
|
485
|
+
if (!stat.isSymbolicLink()) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const snapshotData = await promises_1.default.readFile(authPath);
|
|
489
|
+
await this.removeIfExists(authPath);
|
|
490
|
+
await promises_1.default.writeFile(authPath, snapshotData);
|
|
491
|
+
}
|
|
492
|
+
async assertSafeSnapshotOverwrite(input) {
|
|
493
|
+
var _a, _b;
|
|
494
|
+
if (input.force || !(await this.pathExists(input.destinationPath))) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const [existingSnapshot, incomingSnapshot] = await Promise.all([
|
|
498
|
+
(0, auth_parser_1.parseAuthSnapshotFile)(input.destinationPath),
|
|
499
|
+
(0, auth_parser_1.parseAuthSnapshotFile)(input.authPath),
|
|
500
|
+
]);
|
|
501
|
+
const existingEmail = (_a = existingSnapshot.email) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
502
|
+
const incomingEmail = (_b = incomingSnapshot.email) === null || _b === void 0 ? void 0 : _b.trim().toLowerCase();
|
|
503
|
+
if (existingEmail && incomingEmail && existingEmail !== incomingEmail) {
|
|
504
|
+
throw new errors_1.SnapshotEmailMismatchError(input.accountName, existingEmail, incomingEmail);
|
|
505
|
+
}
|
|
506
|
+
if (this.snapshotsShareIdentity(existingSnapshot, incomingSnapshot))
|
|
507
|
+
return;
|
|
508
|
+
if (!existingEmail || !incomingEmail)
|
|
509
|
+
return;
|
|
510
|
+
const existingIdentity = this.renderSnapshotIdentity(existingSnapshot, existingEmail);
|
|
511
|
+
const incomingIdentity = this.renderSnapshotIdentity(incomingSnapshot, incomingEmail);
|
|
512
|
+
throw new errors_1.SnapshotEmailMismatchError(input.accountName, existingIdentity, incomingIdentity);
|
|
97
513
|
}
|
|
98
514
|
async removeIfExists(target) {
|
|
99
515
|
try {
|
|
@@ -107,12 +523,13 @@ class AccountService {
|
|
|
107
523
|
}
|
|
108
524
|
}
|
|
109
525
|
async writeCurrentName(name) {
|
|
110
|
-
|
|
111
|
-
await
|
|
526
|
+
const currentNamePath = (0, paths_1.resolveCurrentNamePath)();
|
|
527
|
+
await this.ensureDir(node_path_1.default.dirname(currentNamePath));
|
|
528
|
+
await promises_1.default.writeFile(currentNamePath, `${name}\n`, "utf8");
|
|
112
529
|
}
|
|
113
|
-
async readCurrentNameFile() {
|
|
530
|
+
async readCurrentNameFile(currentNamePath) {
|
|
114
531
|
try {
|
|
115
|
-
const contents = await promises_1.default.readFile(
|
|
532
|
+
const contents = await promises_1.default.readFile(currentNamePath, "utf8");
|
|
116
533
|
const trimmed = contents.trim();
|
|
117
534
|
return trimmed.length ? trimmed : null;
|
|
118
535
|
}
|
|
@@ -133,5 +550,103 @@ class AccountService {
|
|
|
133
550
|
return false;
|
|
134
551
|
}
|
|
135
552
|
}
|
|
553
|
+
async hydrateSnapshotMetadata(registry, accountName) {
|
|
554
|
+
var _a;
|
|
555
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(this.accountFilePath(accountName));
|
|
556
|
+
const entry = (_a = registry.accounts[accountName]) !== null && _a !== void 0 ? _a : {
|
|
557
|
+
name: accountName,
|
|
558
|
+
createdAt: new Date().toISOString(),
|
|
559
|
+
};
|
|
560
|
+
if (parsed.email)
|
|
561
|
+
entry.email = parsed.email;
|
|
562
|
+
if (parsed.accountId)
|
|
563
|
+
entry.accountId = parsed.accountId;
|
|
564
|
+
if (parsed.userId)
|
|
565
|
+
entry.userId = parsed.userId;
|
|
566
|
+
if (parsed.planType)
|
|
567
|
+
entry.planType = parsed.planType;
|
|
568
|
+
registry.accounts[accountName] = entry;
|
|
569
|
+
}
|
|
570
|
+
async resolveUniqueInferredName(baseName, incomingSnapshot) {
|
|
571
|
+
const accountPathFor = (name) => this.accountFilePath(name);
|
|
572
|
+
const hasMatchingIdentity = async (name) => {
|
|
573
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(accountPathFor(name));
|
|
574
|
+
return this.snapshotsShareIdentity(parsed, incomingSnapshot);
|
|
575
|
+
};
|
|
576
|
+
const basePath = accountPathFor(baseName);
|
|
577
|
+
if (!(await this.pathExists(basePath))) {
|
|
578
|
+
return baseName;
|
|
579
|
+
}
|
|
580
|
+
if (await hasMatchingIdentity(baseName)) {
|
|
581
|
+
return baseName;
|
|
582
|
+
}
|
|
583
|
+
for (let i = 2; i <= 99; i += 1) {
|
|
584
|
+
const candidate = this.normalizeAccountName(`${baseName}--dup-${i}`);
|
|
585
|
+
const candidatePath = accountPathFor(candidate);
|
|
586
|
+
if (!(await this.pathExists(candidatePath))) {
|
|
587
|
+
return candidate;
|
|
588
|
+
}
|
|
589
|
+
if (await hasMatchingIdentity(candidate)) {
|
|
590
|
+
return candidate;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
throw new errors_1.AccountNameInferenceError();
|
|
594
|
+
}
|
|
595
|
+
async loadReconciledRegistry() {
|
|
596
|
+
const accountNames = await this.listAccountNames();
|
|
597
|
+
const loaded = await (0, registry_1.loadRegistry)();
|
|
598
|
+
const base = loaded.version === 1 ? loaded : (0, registry_1.createDefaultRegistry)();
|
|
599
|
+
return (0, registry_1.reconcileRegistryWithAccounts)(base, accountNames);
|
|
600
|
+
}
|
|
601
|
+
async persistRegistry(registry) {
|
|
602
|
+
const reconciled = (0, registry_1.reconcileRegistryWithAccounts)(registry, await this.listAccountNames());
|
|
603
|
+
await (0, registry_1.saveRegistry)(reconciled);
|
|
604
|
+
}
|
|
605
|
+
async activateSnapshot(accountName) {
|
|
606
|
+
const name = this.normalizeAccountName(accountName);
|
|
607
|
+
const source = this.accountFilePath(name);
|
|
608
|
+
if (!(await this.pathExists(source))) {
|
|
609
|
+
throw new errors_1.AccountNotFoundError(name);
|
|
610
|
+
}
|
|
611
|
+
const authPath = (0, paths_1.resolveAuthPath)();
|
|
612
|
+
await this.ensureDir(node_path_1.default.dirname(authPath));
|
|
613
|
+
await promises_1.default.copyFile(source, authPath);
|
|
614
|
+
await this.writeCurrentName(name);
|
|
615
|
+
}
|
|
616
|
+
async clearActivePointers() {
|
|
617
|
+
const currentPath = (0, paths_1.resolveCurrentNamePath)();
|
|
618
|
+
const authPath = (0, paths_1.resolveAuthPath)();
|
|
619
|
+
await this.removeIfExists(currentPath);
|
|
620
|
+
await this.removeIfExists(authPath);
|
|
621
|
+
}
|
|
622
|
+
snapshotsShareIdentity(a, b) {
|
|
623
|
+
var _a, _b;
|
|
624
|
+
if (a.authMode !== "chatgpt" || b.authMode !== "chatgpt") {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
if (a.userId && b.userId && a.accountId && b.accountId) {
|
|
628
|
+
return a.userId === b.userId && a.accountId === b.accountId;
|
|
629
|
+
}
|
|
630
|
+
if (a.accountId && b.accountId) {
|
|
631
|
+
return a.accountId === b.accountId;
|
|
632
|
+
}
|
|
633
|
+
if (a.userId && b.userId) {
|
|
634
|
+
return a.userId === b.userId;
|
|
635
|
+
}
|
|
636
|
+
const aEmail = (_a = a.email) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
637
|
+
const bEmail = (_b = b.email) === null || _b === void 0 ? void 0 : _b.trim().toLowerCase();
|
|
638
|
+
if (aEmail && bEmail) {
|
|
639
|
+
return aEmail === bEmail;
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
renderSnapshotIdentity(snapshot, fallbackEmail) {
|
|
644
|
+
const parts = [fallbackEmail];
|
|
645
|
+
if (snapshot.accountId)
|
|
646
|
+
parts.push(`account:${snapshot.accountId}`);
|
|
647
|
+
if (snapshot.userId)
|
|
648
|
+
parts.push(`user:${snapshot.userId}`);
|
|
649
|
+
return parts.join(" | ");
|
|
650
|
+
}
|
|
136
651
|
}
|
|
137
652
|
exports.AccountService = AccountService;
|