sidekick-shared 0.26.5 → 0.26.6
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/CHANGELOG.md +35 -0
- package/README.md +56 -51
- package/dist/accountChanges.js +21 -2
- package/dist/accountHealth.d.ts +72 -0
- package/dist/accountHealth.js +286 -0
- package/dist/accountLaunch.d.ts +30 -0
- package/dist/accountLaunch.js +124 -0
- package/dist/accountManager.d.ts +33 -8
- package/dist/accountManager.js +183 -50
- package/dist/accountRegistry.d.ts +10 -0
- package/dist/accountRegistry.js +42 -22
- package/dist/accountSwitch.d.ts +43 -0
- package/dist/accountSwitch.js +115 -0
- package/dist/accountSync.d.ts +30 -0
- package/dist/accountSync.js +180 -0
- package/dist/accountSyncTypes.d.ts +25 -0
- package/dist/accountSyncTypes.js +7 -0
- package/dist/accounts.d.ts +58 -6
- package/dist/accounts.js +408 -189
- package/dist/claudeCredentials.d.ts +38 -0
- package/dist/claudeCredentials.js +74 -0
- package/dist/claudeProfiles.d.ts +40 -0
- package/dist/claudeProfiles.js +102 -10
- package/dist/codexAuth.d.ts +32 -0
- package/dist/codexAuth.js +127 -0
- package/dist/codexPaths.d.ts +21 -0
- package/dist/codexPaths.js +117 -0
- package/dist/codexProfiles.d.ts +58 -5
- package/dist/codexProfiles.js +574 -289
- package/dist/credentialIO.d.ts +12 -5
- package/dist/credentialIO.js +63 -29
- package/dist/credentials.d.ts +8 -0
- package/dist/credentials.js +28 -32
- package/dist/ensureDefaultAccounts.d.ts +8 -0
- package/dist/ensureDefaultAccounts.js +42 -82
- package/dist/index.d.ts +27 -10
- package/dist/index.js +61 -9
- package/dist/keepAlive.d.ts +44 -0
- package/dist/keepAlive.js +106 -0
- package/dist/processDetection.d.ts +32 -0
- package/dist/processDetection.js +210 -0
- package/dist/schemas/accountManager.d.ts +283 -1
- package/dist/schemas/accountManager.js +122 -1
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/schemas/index.d.ts +6 -1
- package/dist/schemas/index.js +14 -1
- package/dist/schemas/observedSessionV1.d.ts +21 -21
- package/dist/schemas/quota.d.ts +18 -18
- package/dist/schemas/quotaHistory.d.ts +1 -1
- package/dist/schemas/stateFile.d.ts +5 -5
- package/dist/terminalSync.d.ts +5 -5
- package/dist/terminalSync.js +7 -112
- package/dist/writers/atomic.d.ts +2 -0
- package/dist/writers/atomic.js +49 -2
- package/package.json +1 -1
package/dist/accountManager.js
CHANGED
|
@@ -42,6 +42,8 @@ exports.finalizeAccountLoginAsync = finalizeAccountLoginAsync;
|
|
|
42
42
|
exports.spawnAccountLogin = spawnAccountLogin;
|
|
43
43
|
exports.switchAccount = switchAccount;
|
|
44
44
|
exports.switchAccountAsync = switchAccountAsync;
|
|
45
|
+
exports.getLastSwitch = getLastSwitch;
|
|
46
|
+
exports.undoLastSwitch = undoLastSwitch;
|
|
45
47
|
exports.listAllAccounts = listAllAccounts;
|
|
46
48
|
const child_process_1 = require("child_process");
|
|
47
49
|
const crypto_1 = require("crypto");
|
|
@@ -53,11 +55,9 @@ const accounts_1 = require("./accounts");
|
|
|
53
55
|
const claudeProfiles_1 = require("./claudeProfiles");
|
|
54
56
|
const credentialIO_1 = require("./credentialIO");
|
|
55
57
|
const codexProfiles_1 = require("./codexProfiles");
|
|
56
|
-
|
|
57
|
-
return path.dirname((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
|
|
58
|
-
}
|
|
58
|
+
const accountSwitch_1 = require("./accountSwitch");
|
|
59
59
|
function getPendingClaudeProfilePath(loginId) {
|
|
60
|
-
return path.join(getClaudeProfileDir(loginId), 'profile.json');
|
|
60
|
+
return path.join((0, claudeProfiles_1.getClaudeProfileDir)(loginId), 'profile.json');
|
|
61
61
|
}
|
|
62
62
|
function readPendingClaudeProfile(loginId) {
|
|
63
63
|
try {
|
|
@@ -79,11 +79,21 @@ function removePendingClaudeProfile(loginId) {
|
|
|
79
79
|
/* best effort */
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
/**
|
|
83
|
+
* A fresh `CLAUDE_CONFIG_DIR` puts the CLI through first-run onboarding before
|
|
84
|
+
* the login prompt. Seeding the onboarding flag keeps the isolated login on the
|
|
85
|
+
* sign-in flow only; `oauthAccount` is written by the CLI itself.
|
|
86
|
+
*/
|
|
87
|
+
function seedClaudeLoginHome(home) {
|
|
88
|
+
const configPath = path.join(home, '.claude.json');
|
|
89
|
+
if (fs.existsSync(configPath))
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
(0, atomic_1.atomicWriteJsonSync)(configPath, { hasCompletedOnboarding: true });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* the CLI creates the file itself */
|
|
96
|
+
}
|
|
87
97
|
}
|
|
88
98
|
function readClaudeOauthAccount(home) {
|
|
89
99
|
try {
|
|
@@ -94,7 +104,7 @@ function readClaudeOauthAccount(home) {
|
|
|
94
104
|
return null;
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
|
-
function copyClaudeProfileToCanonicalHome(sourceHome, accountUuid, credentials) {
|
|
107
|
+
function copyClaudeProfileToCanonicalHome(sourceHome, accountUuid, credentials, oauthAccount) {
|
|
98
108
|
const canonicalHome = (0, claudeProfiles_1.getClaudeProfileHome)(accountUuid);
|
|
99
109
|
fs.mkdirSync(canonicalHome, { recursive: true, mode: 0o700 });
|
|
100
110
|
if (path.resolve(sourceHome) !== path.resolve(canonicalHome)) {
|
|
@@ -105,27 +115,40 @@ function copyClaudeProfileToCanonicalHome(sourceHome, accountUuid, credentials)
|
|
|
105
115
|
// The credential and identity files are re-written below when possible.
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
fs.copyFileSync(sourceConfig, path.join(canonicalHome, '.claude.json'));
|
|
111
|
-
}
|
|
112
|
-
(0, credentialIO_1.writeActiveCredentials)(credentials, canonicalHome);
|
|
118
|
+
(0, accounts_1.storeClaudeProfileIdentity)(accountUuid, oauthAccount);
|
|
119
|
+
(0, accounts_1.storeClaudeProfileCredentials)(accountUuid, credentials, 'login');
|
|
113
120
|
}
|
|
114
121
|
function parseClaudeLoginArgs(raw) {
|
|
115
122
|
const trimmed = raw?.trim();
|
|
116
123
|
return trimmed ? trimmed.split(/\s+/) : null;
|
|
117
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* `claude auth login` signs in without the interactive first-run flow the bare
|
|
127
|
+
* `claude /login` form triggers in a fresh config directory. Override with
|
|
128
|
+
* `SIDEKICK_CLAUDE_LOGIN_ARGS` (space separated) for older CLIs.
|
|
129
|
+
*/
|
|
118
130
|
function resolveClaudeLoginCommand(opts = {}) {
|
|
119
131
|
if (opts.loginCommand)
|
|
120
132
|
return opts.loginCommand;
|
|
121
133
|
return {
|
|
122
134
|
command: 'claude',
|
|
123
|
-
args: parseClaudeLoginArgs(process.env.SIDEKICK_CLAUDE_LOGIN_ARGS) ?? ['
|
|
135
|
+
args: parseClaudeLoginArgs(process.env.SIDEKICK_CLAUDE_LOGIN_ARGS) ?? ['auth', 'login'],
|
|
124
136
|
};
|
|
125
137
|
}
|
|
126
138
|
function beginAccountLogin(provider, label, opts = {}) {
|
|
127
139
|
if (provider === 'codex') {
|
|
128
|
-
const
|
|
140
|
+
const existing = opts.existingAccountId
|
|
141
|
+
? (0, codexProfiles_1.listCodexAccounts)().find((account) => account.id === opts.existingAccountId)
|
|
142
|
+
: undefined;
|
|
143
|
+
if (opts.existingAccountId && !existing) {
|
|
144
|
+
return { success: false, error: `Codex account ${opts.existingAccountId} not found.` };
|
|
145
|
+
}
|
|
146
|
+
// A re-login uses a throwaway label: finalize folds the fresh credentials
|
|
147
|
+
// into the existing profile by identity and keeps its label.
|
|
148
|
+
const effectiveLabel = existing
|
|
149
|
+
? `${existing.label ?? existing.email ?? 'codex'} (re-login ${(0, crypto_1.randomUUID)().slice(0, 8)})`
|
|
150
|
+
: label.trim() || `Codex ${(0, codexProfiles_1.listCodexAccounts)().length + 1}`;
|
|
151
|
+
const prepared = (0, codexProfiles_1.prepareCodexAccount)(effectiveLabel);
|
|
129
152
|
if (!prepared.success || !prepared.profileId || !prepared.codexHome) {
|
|
130
153
|
return { success: false, error: prepared.error ?? 'Could not prepare Codex account login.' };
|
|
131
154
|
}
|
|
@@ -135,6 +158,7 @@ function beginAccountLogin(provider, label, opts = {}) {
|
|
|
135
158
|
loginId: prepared.profileId,
|
|
136
159
|
alreadyComplete: true,
|
|
137
160
|
configDir: prepared.codexHome,
|
|
161
|
+
existingAccountId: existing?.id,
|
|
138
162
|
};
|
|
139
163
|
}
|
|
140
164
|
const loginCommand = opts.loginCommand ?? { command: 'codex', args: ['login'] };
|
|
@@ -145,18 +169,23 @@ function beginAccountLogin(provider, label, opts = {}) {
|
|
|
145
169
|
args: loginCommand.args,
|
|
146
170
|
env: { CODEX_HOME: prepared.codexHome },
|
|
147
171
|
configDir: prepared.codexHome,
|
|
172
|
+
existingAccountId: existing?.id,
|
|
148
173
|
};
|
|
149
174
|
}
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
const existing = opts.existingAccountId
|
|
176
|
+
? (0, accountRegistry_1.listSavedAccountProfiles)('claude-code').find((account) => account.id === opts.existingAccountId)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (opts.existingAccountId && !existing) {
|
|
179
|
+
return { success: false, error: `Claude account ${opts.existingAccountId} not found.` };
|
|
153
180
|
}
|
|
154
181
|
const loginId = (0, crypto_1.randomUUID)();
|
|
155
182
|
const home = (0, claudeProfiles_1.getClaudeProfileHome)(loginId);
|
|
156
183
|
writePendingClaudeProfile(loginId, {
|
|
157
|
-
label:
|
|
158
|
-
addedAt: new Date().toISOString(),
|
|
184
|
+
label: existing?.label ?? label.trim(),
|
|
185
|
+
addedAt: existing?.addedAt ?? new Date().toISOString(),
|
|
186
|
+
existingAccountId: existing?.id,
|
|
159
187
|
});
|
|
188
|
+
seedClaudeLoginHome(home);
|
|
160
189
|
const loginCommand = resolveClaudeLoginCommand(opts);
|
|
161
190
|
return {
|
|
162
191
|
success: true,
|
|
@@ -164,7 +193,9 @@ function beginAccountLogin(provider, label, opts = {}) {
|
|
|
164
193
|
command: loginCommand.command,
|
|
165
194
|
args: loginCommand.args,
|
|
166
195
|
env: { CLAUDE_CONFIG_DIR: home },
|
|
196
|
+
envUnset: [claudeProfiles_1.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV],
|
|
167
197
|
configDir: home,
|
|
198
|
+
existingAccountId: existing?.id,
|
|
168
199
|
};
|
|
169
200
|
}
|
|
170
201
|
function getClaudeAccountLoginStatus(loginId) {
|
|
@@ -235,28 +266,51 @@ function finalizeClaudeAccountLogin(loginId, opts = {}) {
|
|
|
235
266
|
return { success: false, error: 'Could not read Claude profile config.' };
|
|
236
267
|
}
|
|
237
268
|
const pending = readPendingClaudeProfile(loginId);
|
|
238
|
-
(0,
|
|
239
|
-
(
|
|
240
|
-
|
|
269
|
+
const existing = (0, accountRegistry_1.listSavedAccountProfiles)('claude-code').find((account) => (account.providerAccountId ?? account.id) === identity.uuid);
|
|
270
|
+
copyClaudeProfileToCanonicalHome(home, identity.uuid, credentials, oauthAccount);
|
|
271
|
+
const warnings = [];
|
|
272
|
+
if (pending?.existingAccountId && pending.existingAccountId !== identity.uuid) {
|
|
273
|
+
warnings.push(`You signed in as ${identity.email}, which is a different account from the one you chose to re-authenticate; it was saved separately.`);
|
|
274
|
+
}
|
|
241
275
|
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
242
|
-
id: identity.uuid,
|
|
276
|
+
id: existing?.id ?? identity.uuid,
|
|
243
277
|
providerId: 'claude-code',
|
|
244
278
|
providerAccountId: identity.uuid,
|
|
245
279
|
email: identity.email,
|
|
246
|
-
label: pending?.label,
|
|
247
|
-
addedAt: pending?.addedAt ?? new Date().toISOString(),
|
|
280
|
+
label: existing?.label ?? (pending?.label || identity.email),
|
|
281
|
+
addedAt: existing?.addedAt ?? pending?.addedAt ?? new Date().toISOString(),
|
|
248
282
|
metadata: {
|
|
283
|
+
...existing?.metadata,
|
|
249
284
|
email: identity.email,
|
|
285
|
+
origin: 'login',
|
|
250
286
|
},
|
|
251
287
|
});
|
|
252
288
|
removePendingClaudeProfile(loginId);
|
|
253
289
|
if (loginId !== identity.uuid) {
|
|
254
290
|
removePendingClaudeProfile(identity.uuid);
|
|
291
|
+
// The temporary home's keychain item holds live tokens; nothing references it now.
|
|
292
|
+
(0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
|
|
293
|
+
try {
|
|
294
|
+
fs.rmSync((0, claudeProfiles_1.getClaudeProfileDir)(loginId), { recursive: true, force: true });
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
/* the temporary login home is disposable */
|
|
298
|
+
}
|
|
255
299
|
}
|
|
256
300
|
if (opts.activate === false) {
|
|
257
|
-
return { success: true };
|
|
301
|
+
return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
|
|
302
|
+
}
|
|
303
|
+
if ((0, accounts_1.readActiveClaudeAccount)()?.uuid === identity.uuid) {
|
|
304
|
+
// Re-login of the account that is already live: the new token replaces
|
|
305
|
+
// the live one (which may be the dead credential that prompted the login).
|
|
306
|
+
const applied = (0, accounts_1.applyClaudeProfileToLiveHome)(identity.uuid);
|
|
307
|
+
if (!applied.success)
|
|
308
|
+
return applied;
|
|
258
309
|
}
|
|
259
|
-
|
|
310
|
+
const switched = (0, accounts_1.switchToAccount)(identity.uuid);
|
|
311
|
+
return warnings.length
|
|
312
|
+
? { ...switched, warning: [switched.warning, ...warnings].filter(Boolean).join(' ') }
|
|
313
|
+
: switched;
|
|
260
314
|
}
|
|
261
315
|
function emitStatus(opts, status) {
|
|
262
316
|
opts.onStatus?.(status);
|
|
@@ -283,11 +337,48 @@ function waitForNextPoll(ms, signal) {
|
|
|
283
337
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
284
338
|
});
|
|
285
339
|
}
|
|
340
|
+
/** Most recent modification inside an isolated login home (the CLI writes as the user progresses). */
|
|
341
|
+
function latestHomeActivity(home) {
|
|
342
|
+
if (!home)
|
|
343
|
+
return 0;
|
|
344
|
+
let latest = 0;
|
|
345
|
+
const candidates = [
|
|
346
|
+
home,
|
|
347
|
+
...['.claude.json', '.credentials.json', 'auth.json', 'config.toml'].map((f) => path.join(home, f)),
|
|
348
|
+
];
|
|
349
|
+
for (const candidate of candidates) {
|
|
350
|
+
try {
|
|
351
|
+
latest = Math.max(latest, fs.statSync(candidate).mtimeMs);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
/* absent */
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return latest;
|
|
358
|
+
}
|
|
359
|
+
function discardPendingLogin(provider, loginId) {
|
|
360
|
+
try {
|
|
361
|
+
const dir = provider === 'codex'
|
|
362
|
+
? path.dirname((0, codexProfiles_1.getCodexProfileHome)(loginId))
|
|
363
|
+
: (0, claudeProfiles_1.getClaudeProfileDir)(loginId);
|
|
364
|
+
if (!dir.startsWith((0, accountRegistry_1.getAccountsDir)()))
|
|
365
|
+
return;
|
|
366
|
+
if (provider === 'claude-code')
|
|
367
|
+
(0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
|
|
368
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
/* best effort */
|
|
372
|
+
}
|
|
373
|
+
}
|
|
286
374
|
async function spawnAccountLogin(provider, label, opts = {}) {
|
|
287
375
|
if (opts.signal?.aborted) {
|
|
288
376
|
return { success: false, error: 'Account login aborted.' };
|
|
289
377
|
}
|
|
290
|
-
const begin = beginAccountLogin(provider, label, {
|
|
378
|
+
const begin = beginAccountLogin(provider, label, {
|
|
379
|
+
loginCommand: opts.loginCommand,
|
|
380
|
+
existingAccountId: opts.existingAccountId,
|
|
381
|
+
});
|
|
291
382
|
if (!begin.success)
|
|
292
383
|
return { success: false, error: begin.error };
|
|
293
384
|
if (begin.alreadyComplete) {
|
|
@@ -299,16 +390,25 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
299
390
|
let childExited = false;
|
|
300
391
|
let childExitCode = null;
|
|
301
392
|
const childState = {};
|
|
302
|
-
const
|
|
303
|
-
const
|
|
393
|
+
const inactivityMs = opts.timeoutMs ?? 180_000;
|
|
394
|
+
const maxMs = opts.maxTimeoutMs ?? 900_000;
|
|
395
|
+
const startedAt = Date.now();
|
|
396
|
+
let lastActivity = startedAt;
|
|
397
|
+
let lastSeenMtime = latestHomeActivity(begin.configDir);
|
|
398
|
+
const env = provider === 'codex'
|
|
399
|
+
? { ...process.env, ...(begin.env ?? {}) }
|
|
400
|
+
: (0, claudeProfiles_1.buildClaudeChildEnv)(begin.configDir ?? '', process.env);
|
|
401
|
+
for (const name of begin.envUnset ?? [])
|
|
402
|
+
delete env[name];
|
|
304
403
|
let child;
|
|
305
404
|
try {
|
|
306
405
|
child = (0, child_process_1.spawn)(begin.command, begin.args ?? [], {
|
|
307
|
-
env
|
|
406
|
+
env,
|
|
308
407
|
stdio: opts.stdio ?? 'inherit',
|
|
309
408
|
});
|
|
310
409
|
}
|
|
311
410
|
catch (err) {
|
|
411
|
+
discardPendingLogin(provider, begin.loginId);
|
|
312
412
|
return { success: false, error: `Could not spawn account login: ${err}` };
|
|
313
413
|
}
|
|
314
414
|
child.on('exit', (code) => {
|
|
@@ -319,12 +419,15 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
319
419
|
childExited = true;
|
|
320
420
|
childState.spawnError = error;
|
|
321
421
|
});
|
|
422
|
+
const abort = () => {
|
|
423
|
+
child.kill();
|
|
424
|
+
discardPendingLogin(provider, begin.loginId);
|
|
425
|
+
emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
|
|
426
|
+
return { success: false, error: 'Account login aborted.' };
|
|
427
|
+
};
|
|
322
428
|
while (true) {
|
|
323
|
-
if (opts.signal?.aborted)
|
|
324
|
-
|
|
325
|
-
emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
|
|
326
|
-
return { success: false, error: 'Account login aborted.' };
|
|
327
|
-
}
|
|
429
|
+
if (opts.signal?.aborted)
|
|
430
|
+
return abort();
|
|
328
431
|
const status = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
|
|
329
432
|
if (status.state === 'authenticated') {
|
|
330
433
|
return finalizeAccountLoginAsync(provider, begin.loginId, {
|
|
@@ -334,6 +437,7 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
334
437
|
if (childExited) {
|
|
335
438
|
if (childState.spawnError) {
|
|
336
439
|
const error = `Could not spawn account login: ${childState.spawnError.message}`;
|
|
440
|
+
discardPendingLogin(provider, begin.loginId);
|
|
337
441
|
emitStatus(opts, { state: 'failed', error });
|
|
338
442
|
return { success: false, error };
|
|
339
443
|
}
|
|
@@ -343,34 +447,63 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
343
447
|
activate: opts.activate ?? true,
|
|
344
448
|
});
|
|
345
449
|
}
|
|
450
|
+
discardPendingLogin(provider, begin.loginId);
|
|
346
451
|
emitStatus(opts, {
|
|
347
452
|
state: 'failed',
|
|
348
453
|
error: `Account login exited before authentication completed${childExitCode === null ? '.' : ` (exit ${childExitCode}).`}`,
|
|
349
454
|
});
|
|
350
455
|
return { success: false, error: 'Account login did not complete.' };
|
|
351
456
|
}
|
|
352
|
-
const
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
const mtime = latestHomeActivity(begin.configDir);
|
|
459
|
+
if (mtime > lastSeenMtime) {
|
|
460
|
+
lastSeenMtime = mtime;
|
|
461
|
+
lastActivity = now;
|
|
462
|
+
}
|
|
463
|
+
const remainingMs = Math.min(inactivityMs - (now - lastActivity), maxMs - (now - startedAt));
|
|
353
464
|
if (remainingMs <= 0) {
|
|
465
|
+
child.kill();
|
|
466
|
+
discardPendingLogin(provider, begin.loginId);
|
|
354
467
|
emitStatus(opts, { state: 'failed', error: 'Account login timed out.' });
|
|
355
468
|
return { success: false, error: 'Account login timed out.' };
|
|
356
469
|
}
|
|
357
470
|
const shouldContinue = await waitForNextPoll(Math.min(2_000, remainingMs), opts.signal);
|
|
358
|
-
if (!shouldContinue)
|
|
359
|
-
|
|
360
|
-
emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
|
|
361
|
-
return { success: false, error: 'Account login aborted.' };
|
|
362
|
-
}
|
|
471
|
+
if (!shouldContinue)
|
|
472
|
+
return abort();
|
|
363
473
|
}
|
|
364
474
|
}
|
|
365
|
-
function switchAccount(provider, id) {
|
|
366
|
-
return provider === 'codex' ? (0, codexProfiles_1.switchToCodexAccount)(id) : (0, accounts_1.switchToAccount)(id);
|
|
475
|
+
function switchAccount(provider, id, options = {}) {
|
|
476
|
+
return provider === 'codex' ? (0, codexProfiles_1.switchToCodexAccount)(id, options) : (0, accounts_1.switchToAccount)(id, options);
|
|
367
477
|
}
|
|
368
478
|
/**
|
|
369
|
-
* {@link switchAccount} without blocking the event loop on
|
|
479
|
+
* {@link switchAccount} without blocking the event loop on CLI/process probes.
|
|
370
480
|
* The Claude path still performs bounded synchronous keychain reads on macOS.
|
|
371
481
|
*/
|
|
372
|
-
async function switchAccountAsync(provider, id) {
|
|
373
|
-
return provider === 'codex'
|
|
482
|
+
async function switchAccountAsync(provider, id, options = {}) {
|
|
483
|
+
return provider === 'codex'
|
|
484
|
+
? (0, codexProfiles_1.switchToCodexAccountAsync)(id, options)
|
|
485
|
+
: (0, accounts_1.switchToAccountAsync)(id, options);
|
|
486
|
+
}
|
|
487
|
+
function getLastSwitch(provider) {
|
|
488
|
+
return (0, accountSwitch_1.readLastSwitch)(provider);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Switch back to the account that was live before the last recorded switch.
|
|
492
|
+
* With `token`, the record must match (a stale Undo button never reverts a
|
|
493
|
+
* newer switch).
|
|
494
|
+
*/
|
|
495
|
+
async function undoLastSwitch(provider, token) {
|
|
496
|
+
const record = (0, accountSwitch_1.readLastSwitch)(provider);
|
|
497
|
+
if (!record) {
|
|
498
|
+
return (0, accountSwitch_1.switchFailure)(provider, '', 'There is no account switch to undo.');
|
|
499
|
+
}
|
|
500
|
+
if (token && token !== record.token) {
|
|
501
|
+
return (0, accountSwitch_1.switchFailure)(provider, record.from ?? '', 'A newer account switch has happened since; nothing was undone.');
|
|
502
|
+
}
|
|
503
|
+
if (!record.from) {
|
|
504
|
+
return (0, accountSwitch_1.switchFailure)(provider, '', 'The previous state had no saved account to return to.');
|
|
505
|
+
}
|
|
506
|
+
return switchAccountAsync(provider, record.from);
|
|
374
507
|
}
|
|
375
508
|
function listAllAccounts() {
|
|
376
509
|
return {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { AccountProviderId } from './providerIds';
|
|
2
2
|
export { ACCOUNT_PROVIDER_IDS } from './providerIds';
|
|
3
3
|
export type { AccountProviderId } from './providerIds';
|
|
4
|
+
/** How a saved profile came to exist. */
|
|
5
|
+
export type AccountProfileOrigin = 'login' | 'manual' | 'live-sync' | 'migration';
|
|
4
6
|
export interface AccountIdentityMetadata {
|
|
5
7
|
email?: string;
|
|
6
8
|
workspaceId?: string;
|
|
7
9
|
planType?: string;
|
|
8
10
|
authMode?: 'chatgpt' | 'api-key' | 'unknown';
|
|
11
|
+
/** `live-sync` marks accounts sidekick registered from a login it observed. */
|
|
12
|
+
origin?: AccountProfileOrigin;
|
|
9
13
|
}
|
|
10
14
|
export interface SavedAccountProfile {
|
|
11
15
|
id: string;
|
|
@@ -63,6 +67,10 @@ export declare function listSavedAccountProfiles(providerId?: AccountProviderId)
|
|
|
63
67
|
export declare function getActiveSavedAccount(providerId: AccountProviderId): SavedAccountProfile | null;
|
|
64
68
|
export declare function upsertSavedAccountProfile(profile: SavedAccountProfile): SavedAccountRegistry;
|
|
65
69
|
export declare function setActiveSavedAccount(providerId: AccountProviderId, accountId: string | null, options?: RegistryWriteOptions): SavedAccountRegistry;
|
|
70
|
+
/** {@link upsertSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
71
|
+
export declare function upsertSavedAccountProfileUnlocked(profile: SavedAccountProfile): SavedAccountRegistry;
|
|
72
|
+
/** {@link setActiveSavedAccount} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
73
|
+
export declare function setActiveSavedAccountUnlocked(providerId: AccountProviderId, accountId: string | null, options?: RegistryWriteOptions): SavedAccountRegistry;
|
|
66
74
|
export declare function replaceSavedAccountProfiles(providerId: AccountProviderId, accounts: SavedAccountProfile[], activeId: string | null): SavedAccountRegistry;
|
|
67
75
|
/**
|
|
68
76
|
* Same as {@link replaceSavedAccountProfiles} without taking the registry
|
|
@@ -70,3 +78,5 @@ export declare function replaceSavedAccountProfiles(providerId: AccountProviderI
|
|
|
70
78
|
*/
|
|
71
79
|
export declare function replaceSavedAccountProfilesUnlocked(providerId: AccountProviderId, accounts: SavedAccountProfile[], activeId: string | null): SavedAccountRegistry;
|
|
72
80
|
export declare function removeSavedAccountProfile(providerId: AccountProviderId, accountId: string): SavedAccountProfile | null;
|
|
81
|
+
/** {@link removeSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
82
|
+
export declare function removeSavedAccountProfileUnlocked(providerId: AccountProviderId, accountId: string): SavedAccountProfile | null;
|
package/dist/accountRegistry.js
CHANGED
|
@@ -42,9 +42,12 @@ exports.listSavedAccountProfiles = listSavedAccountProfiles;
|
|
|
42
42
|
exports.getActiveSavedAccount = getActiveSavedAccount;
|
|
43
43
|
exports.upsertSavedAccountProfile = upsertSavedAccountProfile;
|
|
44
44
|
exports.setActiveSavedAccount = setActiveSavedAccount;
|
|
45
|
+
exports.upsertSavedAccountProfileUnlocked = upsertSavedAccountProfileUnlocked;
|
|
46
|
+
exports.setActiveSavedAccountUnlocked = setActiveSavedAccountUnlocked;
|
|
45
47
|
exports.replaceSavedAccountProfiles = replaceSavedAccountProfiles;
|
|
46
48
|
exports.replaceSavedAccountProfilesUnlocked = replaceSavedAccountProfilesUnlocked;
|
|
47
49
|
exports.removeSavedAccountProfile = removeSavedAccountProfile;
|
|
50
|
+
exports.removeSavedAccountProfileUnlocked = removeSavedAccountProfileUnlocked;
|
|
48
51
|
const fs = __importStar(require("fs"));
|
|
49
52
|
const path = __importStar(require("path"));
|
|
50
53
|
const paths_1 = require("./paths");
|
|
@@ -125,6 +128,7 @@ function normalizeRegistry(registry) {
|
|
|
125
128
|
workspaceId: account.metadata.workspaceId || undefined,
|
|
126
129
|
planType: account.metadata.planType || undefined,
|
|
127
130
|
authMode: account.metadata.authMode || undefined,
|
|
131
|
+
origin: account.metadata.origin || undefined,
|
|
128
132
|
}
|
|
129
133
|
: undefined,
|
|
130
134
|
}))
|
|
@@ -189,7 +193,19 @@ function getActiveSavedAccount(providerId) {
|
|
|
189
193
|
return (registry.accounts.find((account) => account.providerId === providerId && account.id === activeId) ?? null);
|
|
190
194
|
}
|
|
191
195
|
function upsertSavedAccountProfile(profile) {
|
|
192
|
-
return mutateSavedAccountRegistry((
|
|
196
|
+
return mutateSavedAccountRegistry(upsertProfile(profile));
|
|
197
|
+
}
|
|
198
|
+
function setActiveSavedAccount(providerId, accountId, options = {}) {
|
|
199
|
+
return mutateSavedAccountRegistry(setActive(providerId, accountId), options);
|
|
200
|
+
}
|
|
201
|
+
function setActive(providerId, accountId) {
|
|
202
|
+
return (registry) => {
|
|
203
|
+
registry.activeByProvider[providerId] = accountId;
|
|
204
|
+
return registry;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function upsertProfile(profile) {
|
|
208
|
+
return (registry) => {
|
|
193
209
|
const index = registry.accounts.findIndex((account) => account.providerId === profile.providerId && account.id === profile.id);
|
|
194
210
|
if (index >= 0) {
|
|
195
211
|
registry.accounts[index] = profile;
|
|
@@ -198,13 +214,15 @@ function upsertSavedAccountProfile(profile) {
|
|
|
198
214
|
registry.accounts.push(profile);
|
|
199
215
|
}
|
|
200
216
|
return registry;
|
|
201
|
-
}
|
|
217
|
+
};
|
|
202
218
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
219
|
+
/** {@link upsertSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
220
|
+
function upsertSavedAccountProfileUnlocked(profile) {
|
|
221
|
+
return mutateSavedAccountRegistryUnlocked(upsertProfile(profile));
|
|
222
|
+
}
|
|
223
|
+
/** {@link setActiveSavedAccount} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
224
|
+
function setActiveSavedAccountUnlocked(providerId, accountId, options = {}) {
|
|
225
|
+
return mutateSavedAccountRegistryUnlocked(setActive(providerId, accountId), options);
|
|
208
226
|
}
|
|
209
227
|
function replaceProfiles(providerId, accounts, activeId) {
|
|
210
228
|
return (registry) => {
|
|
@@ -227,19 +245,21 @@ function replaceSavedAccountProfilesUnlocked(providerId, accounts, activeId) {
|
|
|
227
245
|
}
|
|
228
246
|
function removeSavedAccountProfile(providerId, accountId) {
|
|
229
247
|
ensureAccountsDir();
|
|
230
|
-
return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), () =>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
248
|
+
return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), () => removeSavedAccountProfileUnlocked(providerId, accountId));
|
|
249
|
+
}
|
|
250
|
+
/** {@link removeSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
251
|
+
function removeSavedAccountProfileUnlocked(providerId, accountId) {
|
|
252
|
+
const registry = readSavedAccountRegistry();
|
|
253
|
+
if (!registry)
|
|
254
|
+
return null;
|
|
255
|
+
const index = registry.accounts.findIndex((account) => account.providerId === providerId && account.id === accountId);
|
|
256
|
+
if (index === -1)
|
|
257
|
+
return null;
|
|
258
|
+
const [removed] = registry.accounts.splice(index, 1);
|
|
259
|
+
if (registry.activeByProvider[providerId] === accountId) {
|
|
260
|
+
registry.activeByProvider[providerId] =
|
|
261
|
+
registry.accounts.find((account) => account.providerId === providerId)?.id ?? null;
|
|
262
|
+
}
|
|
263
|
+
writeRegistryUnlocked(registry);
|
|
264
|
+
return removed;
|
|
245
265
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type AccountProviderId } from './accountRegistry';
|
|
2
|
+
import type { AccountHealth } from './accountHealth';
|
|
3
|
+
import type { AccountManagerResult } from './accounts';
|
|
4
|
+
import type { RunningAccountConsumer } from './processDetection';
|
|
5
|
+
export type SwitchVerification = 'store' | 'cli' | 'none' | 'failed';
|
|
6
|
+
export interface SwitchAccountResult extends AccountManagerResult {
|
|
7
|
+
provider: AccountProviderId;
|
|
8
|
+
accountId: string;
|
|
9
|
+
previousAccountId: string | null;
|
|
10
|
+
/** True when the live store was re-read after the write and matched the target. */
|
|
11
|
+
verified: boolean;
|
|
12
|
+
verification: SwitchVerification;
|
|
13
|
+
health?: AccountHealth;
|
|
14
|
+
warnings: string[];
|
|
15
|
+
hints: string[];
|
|
16
|
+
runningConsumers: RunningAccountConsumer[];
|
|
17
|
+
/** Present after a successful switch that changed the live login. */
|
|
18
|
+
undoToken?: string;
|
|
19
|
+
/** True when the target was already the live login and nothing was written. */
|
|
20
|
+
alreadyActive?: boolean;
|
|
21
|
+
/** Email of the account now live, when known. */
|
|
22
|
+
email?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SwitchAccountOptions {
|
|
25
|
+
/** Also confirm through the provider CLI after the store check (async paths only). */
|
|
26
|
+
verifyWithCli?: boolean;
|
|
27
|
+
/** Switch even when the stored credential is reported expired. */
|
|
28
|
+
force?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface LastSwitchRecord {
|
|
31
|
+
token: string;
|
|
32
|
+
provider: AccountProviderId;
|
|
33
|
+
from: string | null;
|
|
34
|
+
to: string;
|
|
35
|
+
at: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function getLastSwitchPath(provider: AccountProviderId): string;
|
|
38
|
+
export declare function readLastSwitch(provider: AccountProviderId): LastSwitchRecord | null;
|
|
39
|
+
export declare function writeLastSwitch(provider: AccountProviderId, from: string | null, to: string): LastSwitchRecord;
|
|
40
|
+
export declare function clearLastSwitch(provider: AccountProviderId): void;
|
|
41
|
+
export declare function switchFailure(provider: AccountProviderId, accountId: string, error: string, extra?: Partial<SwitchAccountResult>): SwitchAccountResult;
|
|
42
|
+
/** Fold per-consumer warnings into a result; keeps `warning` (legacy) in sync. */
|
|
43
|
+
export declare function finishSwitchResult(result: SwitchAccountResult): SwitchAccountResult;
|