sidekick-shared 0.27.5 → 0.27.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 +30 -0
- package/README.md +35 -34
- package/dist/accountHealth.d.ts +14 -0
- package/dist/accountHealth.js +61 -14
- package/dist/accountLaunch.d.ts +6 -0
- package/dist/accountLaunch.js +61 -4
- package/dist/accountLease.d.ts +5 -0
- package/dist/accountLease.js +98 -0
- package/dist/accountManager.d.ts +7 -0
- package/dist/accountManager.js +226 -89
- package/dist/accountSync.d.ts +1 -5
- package/dist/accountSync.js +22 -6
- package/dist/accountSyncTypes.d.ts +9 -0
- package/dist/accounts.d.ts +75 -18
- package/dist/accounts.js +712 -189
- package/dist/claudeCredentialDoctor.d.ts +50 -0
- package/dist/claudeCredentialDoctor.js +277 -0
- package/dist/claudeCredentialLedger.d.ts +15 -0
- package/dist/claudeCredentialLedger.js +130 -0
- package/dist/claudeCredentials.d.ts +20 -3
- package/dist/claudeCredentials.js +50 -3
- package/dist/claudeLiveGuard.d.ts +33 -0
- package/dist/claudeLiveGuard.js +46 -0
- package/dist/claudeProfiles.d.ts +14 -0
- package/dist/claudeProfiles.js +22 -0
- package/dist/claudeRefreshLock.d.ts +15 -0
- package/dist/claudeRefreshLock.js +145 -0
- package/dist/codexAuth.d.ts +4 -0
- package/dist/codexAuth.js +18 -0
- package/dist/codexCredentialDoctor.d.ts +33 -0
- package/dist/codexCredentialDoctor.js +302 -0
- package/dist/codexCredentialStore.d.ts +29 -0
- package/dist/codexCredentialStore.js +213 -0
- package/dist/codexKeepAlive.d.ts +2 -0
- package/dist/codexKeepAlive.js +116 -0
- package/dist/codexPaths.d.ts +8 -2
- package/dist/codexPaths.js +51 -7
- package/dist/codexProfiles.d.ts +37 -31
- package/dist/codexProfiles.js +842 -671
- package/dist/credentialIO.d.ts +67 -7
- package/dist/credentialIO.js +211 -47
- package/dist/ensureDefaultAccounts.js +1 -8
- package/dist/index.d.ts +14 -4
- package/dist/index.js +29 -6
- package/dist/keepAlive.d.ts +2 -1
- package/dist/keepAlive.js +116 -29
- package/dist/liveCredentialGuard.d.ts +28 -0
- package/dist/liveCredentialGuard.js +36 -0
- package/dist/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- package/dist/schemas/accountManager.d.ts +22 -0
- package/dist/schemas/accountManager.js +4 -0
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -5,10 +5,17 @@
|
|
|
5
5
|
* Handles saving, listing, switching, syncing, and removing Claude accounts.
|
|
6
6
|
* Both the VS Code extension and CLI consume this module.
|
|
7
7
|
*
|
|
8
|
-
* Model:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Model: Claude Code rotates the refresh token on every refresh, so every
|
|
9
|
+
* refresh-token chain may have exactly one copy a `claude` process can use.
|
|
10
|
+
*
|
|
11
|
+
* - The live home (`CLAUDE_CONFIG_DIR` or `~/.claude`) is the only CLI-usable
|
|
12
|
+
* copy of the account that is logged in right now. Its profile home holds
|
|
13
|
+
* identity and health metadata but never a credential; Sidekick mirrors the
|
|
14
|
+
* live credential into a private backup Claude Code never reads.
|
|
15
|
+
* - An inactive account's profile home holds its single CLI-usable copy (the
|
|
16
|
+
* Claude login only; MCP tokens follow the live store).
|
|
17
|
+
* - Writes into the live store go through one guarded installer that refuses
|
|
18
|
+
* downgrades, and a switch saves the outgoing login before replacing it.
|
|
12
19
|
*/
|
|
13
20
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
21
|
if (k2 === undefined) k2 = k;
|
|
@@ -45,6 +52,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
52
|
})();
|
|
46
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
54
|
exports.storeClaudeProfileIdentity = storeClaudeProfileIdentity;
|
|
55
|
+
exports.storeClaudeProfileCredentialsUnlocked = storeClaudeProfileCredentialsUnlocked;
|
|
48
56
|
exports.storeClaudeProfileCredentials = storeClaudeProfileCredentials;
|
|
49
57
|
exports.readStoredClaudeAccount = readStoredClaudeAccount;
|
|
50
58
|
exports.readAccountRegistry = readAccountRegistry;
|
|
@@ -55,6 +63,10 @@ exports.withClaudeAuthSwapLock = withClaudeAuthSwapLock;
|
|
|
55
63
|
exports.syncClaudeLiveStateUnlocked = syncClaudeLiveStateUnlocked;
|
|
56
64
|
exports.syncClaudeLiveState = syncClaudeLiveState;
|
|
57
65
|
exports.cleanupAbandonedClaudeLogins = cleanupAbandonedClaudeLogins;
|
|
66
|
+
exports.getClaudeKeepAliveBlocker = getClaudeKeepAliveBlocker;
|
|
67
|
+
exports.beginClaudeKeepAlive = beginClaudeKeepAlive;
|
|
68
|
+
exports.finishClaudeKeepAlive = finishClaudeKeepAlive;
|
|
69
|
+
exports.restoreClaudeProfileFromBackup = restoreClaudeProfileFromBackup;
|
|
58
70
|
exports.switchToAccount = switchToAccount;
|
|
59
71
|
exports.switchToAccountAsync = switchToAccountAsync;
|
|
60
72
|
exports.applyClaudeProfileToLiveHome = applyClaudeProfileToLiveHome;
|
|
@@ -73,6 +85,9 @@ const atomic_1 = require("./writers/atomic");
|
|
|
73
85
|
const accountRegistry_1 = require("./accountRegistry");
|
|
74
86
|
const claudeProfiles_1 = require("./claudeProfiles");
|
|
75
87
|
const claudeCredentials_1 = require("./claudeCredentials");
|
|
88
|
+
const claudeLiveGuard_1 = require("./claudeLiveGuard");
|
|
89
|
+
const claudeRefreshLock_1 = require("./claudeRefreshLock");
|
|
90
|
+
const accountLease_1 = require("./accountLease");
|
|
76
91
|
const accountHealth_1 = require("./accountHealth");
|
|
77
92
|
const accountSwitch_1 = require("./accountSwitch");
|
|
78
93
|
const accountSyncTypes_1 = require("./accountSyncTypes");
|
|
@@ -87,6 +102,9 @@ function getConfigsDir() {
|
|
|
87
102
|
function getClaudeMigrationMarkerPath() {
|
|
88
103
|
return path.join((0, accountRegistry_1.getAccountsDir)(), 'claude', '.profiles-migrated-v1');
|
|
89
104
|
}
|
|
105
|
+
function getSwitchJournalPath() {
|
|
106
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'claude', 'switch-journal.json');
|
|
107
|
+
}
|
|
90
108
|
// ── Directory bootstrap ──────────────────────────────────────────────────
|
|
91
109
|
function ensureDirs() {
|
|
92
110
|
for (const dir of [(0, accountRegistry_1.getAccountsDir)(), getCredentialsDir(), getConfigsDir()]) {
|
|
@@ -105,6 +123,9 @@ function readOauthAccountFromConfigPath(filePath) {
|
|
|
105
123
|
const parsed = readJsonOrNull(filePath);
|
|
106
124
|
return parsed?.oauthAccount ?? null;
|
|
107
125
|
}
|
|
126
|
+
function isLiveClaudeAccount(uuid) {
|
|
127
|
+
return (0, claudeProfiles_1.readLiveClaudeIdentity)()?.uuid === uuid;
|
|
128
|
+
}
|
|
108
129
|
/** Store a profile's identity (`oauthAccount`) in its home and the flat backup. */
|
|
109
130
|
function storeClaudeProfileIdentity(uuid, oauthAccount) {
|
|
110
131
|
(0, atomic_1.atomicWriteJsonSync)(path.join((0, claudeProfiles_1.getClaudeProfileHome)(uuid), '.claude.json'), { oauthAccount });
|
|
@@ -115,27 +136,53 @@ function storeClaudeProfileIdentity(uuid, oauthAccount) {
|
|
|
115
136
|
/* the profile home is canonical; the flat copy is a fallback */
|
|
116
137
|
}
|
|
117
138
|
}
|
|
139
|
+
// ── Private credential backups ───────────────────────────────────────────
|
|
140
|
+
function readClaudeBackup(uuid) {
|
|
141
|
+
return readJsonOrNull((0, claudeProfiles_1.getClaudeCredentialBackupPath)(uuid));
|
|
142
|
+
}
|
|
143
|
+
/** Write and verify the private backup. Throws: for the live account it is the only saved copy. */
|
|
144
|
+
function writeClaudeBackup(uuid, credentials) {
|
|
145
|
+
const target = (0, claudeProfiles_1.getClaudeCredentialBackupPath)(uuid);
|
|
146
|
+
(0, atomic_1.atomicWriteJsonSync)(target, credentials);
|
|
147
|
+
if (JSON.stringify(readJsonOrNull(target)) !== JSON.stringify(credentials)) {
|
|
148
|
+
throw new Error(`The credential backup for ${uuid} did not verify after writing.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function fresherOf(a, b) {
|
|
152
|
+
const aCreds = (0, claudeCredentials_1.parseClaudeCredentialBlob)(a);
|
|
153
|
+
const bCreds = (0, claudeCredentials_1.parseClaudeCredentialBlob)(b);
|
|
154
|
+
if (!aCreds)
|
|
155
|
+
return bCreds ? b : null;
|
|
156
|
+
if (!bCreds)
|
|
157
|
+
return a;
|
|
158
|
+
return (0, claudeCredentials_1.claudeCredentialFreshness)(bCreds) > (0, claudeCredentials_1.claudeCredentialFreshness)(aCreds) ? b : a;
|
|
159
|
+
}
|
|
118
160
|
/**
|
|
119
|
-
* The single chokepoint for storing a Claude credential in a profile
|
|
120
|
-
*
|
|
121
|
-
*
|
|
161
|
+
* The single chokepoint for storing a Claude credential in a profile. Always
|
|
162
|
+
* writes the private backup and the secret-free health sidecar. Writes the
|
|
163
|
+
* profile-home store (suffixed Keychain item on macOS, file elsewhere) only
|
|
164
|
+
* when the account is NOT the live login: the live store is that account's one
|
|
165
|
+
* CLI-usable copy, and a second one would fight it over the refresh token.
|
|
166
|
+
* Caller holds the swap lock.
|
|
122
167
|
*/
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
function storeClaudeProfileCredentialsUnlocked(uuid, credentials, source) {
|
|
169
|
+
const login = (0, claudeCredentials_1.claudeLoginOnly)(credentials);
|
|
170
|
+
if (!login || !(0, claudeCredentials_1.parseClaudeCredentialBlob)(login)) {
|
|
171
|
+
throw new Error('Refusing to store something that is not a Claude login.');
|
|
127
172
|
}
|
|
128
|
-
|
|
129
|
-
|
|
173
|
+
writeClaudeBackup(uuid, credentials);
|
|
174
|
+
if (!isLiveClaudeAccount(uuid)) {
|
|
175
|
+
(0, credentialIO_1.writeActiveCredentials)(login, (0, claudeProfiles_1.getClaudeProfileHome)(uuid));
|
|
130
176
|
}
|
|
131
177
|
(0, accountHealth_1.writeClaudeHealthSidecar)(uuid, credentials, source);
|
|
132
178
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
179
|
+
/** {@link storeClaudeProfileCredentialsUnlocked} under the swap lock. */
|
|
180
|
+
function storeClaudeProfileCredentials(uuid, credentials, source) {
|
|
181
|
+
withClaudeAuthSwapLock(() => storeClaudeProfileCredentialsUnlocked(uuid, credentials, source));
|
|
136
182
|
}
|
|
137
|
-
function
|
|
138
|
-
|
|
183
|
+
function writeClaudeProfileMirrorUnlocked(uuid, credentials, oauthAccount, source) {
|
|
184
|
+
storeClaudeProfileIdentity(uuid, oauthAccount);
|
|
185
|
+
storeClaudeProfileCredentialsUnlocked(uuid, credentials, source);
|
|
139
186
|
}
|
|
140
187
|
function readFlatClaudeOauthAccount(uuid) {
|
|
141
188
|
return readJsonOrNull(path.join(getConfigsDir(), `${uuid}.config.json`));
|
|
@@ -143,24 +190,16 @@ function readFlatClaudeOauthAccount(uuid) {
|
|
|
143
190
|
function readProfileClaudeOauthAccount(uuid) {
|
|
144
191
|
return readOauthAccountFromConfigPath(path.join((0, claudeProfiles_1.getClaudeProfileHome)(uuid), '.claude.json'));
|
|
145
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* The saved credential and identity of a profile: the fresher of its
|
|
195
|
+
* profile-home store and its private backup. Read-only — it never
|
|
196
|
+
* re-materialises a backup into a CLI-visible place.
|
|
197
|
+
*/
|
|
146
198
|
function readStoredClaudeAccount(uuid) {
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
const flatCredentials = readFlatClaudeCredentials(uuid);
|
|
153
|
-
const flatOauthAccount = readFlatClaudeOauthAccount(uuid);
|
|
154
|
-
if (flatCredentials && flatOauthAccount) {
|
|
155
|
-
try {
|
|
156
|
-
writeClaudeProfileMirror(uuid, flatCredentials, flatOauthAccount, 'migration');
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Flat backups remain the fallback when profile materialization fails.
|
|
160
|
-
}
|
|
161
|
-
return { credentials: flatCredentials, oauthAccount: flatOauthAccount };
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
199
|
+
const store = (0, credentialIO_1.readClaudeCredentialStore)((0, claudeProfiles_1.getClaudeProfileHome)(uuid));
|
|
200
|
+
const credentials = fresherOf(store.status === 'ok' ? store.blob : null, readClaudeBackup(uuid));
|
|
201
|
+
const oauthAccount = readProfileClaudeOauthAccount(uuid) ?? readFlatClaudeOauthAccount(uuid);
|
|
202
|
+
return credentials && oauthAccount ? { credentials, oauthAccount } : null;
|
|
164
203
|
}
|
|
165
204
|
function readCurrentLiveOauthAccount() {
|
|
166
205
|
return readOauthAccountFromConfigPath((0, claudeProfiles_1.getClaudeConfigPath)());
|
|
@@ -177,6 +216,40 @@ function mergeOauthAccountIntoLiveConfig(oauthAccount) {
|
|
|
177
216
|
configObj.oauthAccount = oauthAccount;
|
|
178
217
|
(0, atomic_1.atomicWriteJsonSync)(configPath, configObj);
|
|
179
218
|
}
|
|
219
|
+
// ── The guarded live installer ───────────────────────────────────────────
|
|
220
|
+
function liveStateFrom(read) {
|
|
221
|
+
return read.status === 'ok' ? { status: 'ok', blob: read.blob } : { status: read.status };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The only writer of the live Claude credential store. Asks the guard, merges
|
|
225
|
+
* (the live MCP tokens stay; the candidate's Claude login replaces live's),
|
|
226
|
+
* writes, and verifies. `rollback` restores `candidate` verbatim.
|
|
227
|
+
*/
|
|
228
|
+
function installIntoLiveClaudeStore(candidate, candidateAccountUuid, intent, context) {
|
|
229
|
+
const live = liveStateFrom(context.live);
|
|
230
|
+
const decision = (0, claudeLiveGuard_1.decideLiveClaudeWrite)({
|
|
231
|
+
intent,
|
|
232
|
+
live,
|
|
233
|
+
liveAccountUuid: context.liveAccountUuid,
|
|
234
|
+
candidate,
|
|
235
|
+
candidateAccountUuid,
|
|
236
|
+
capturedOutgoing: context.capturedOutgoing,
|
|
237
|
+
written: context.written,
|
|
238
|
+
});
|
|
239
|
+
if (!decision.allow) {
|
|
240
|
+
return { ok: false, code: decision.code, reason: decision.reason, wrote: false };
|
|
241
|
+
}
|
|
242
|
+
const blob = intent === 'rollback'
|
|
243
|
+
? candidate
|
|
244
|
+
: (0, claudeCredentials_1.mergeClaudeLoginIntoLive)(live.status === 'ok' ? live.blob : null, candidate);
|
|
245
|
+
try {
|
|
246
|
+
(0, credentialIO_1.writeLiveClaudeCredentialStoreUnguarded)(blob);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
return { ok: false, code: 'write-failed', reason: `${err}`, wrote: true, attempted: blob };
|
|
250
|
+
}
|
|
251
|
+
return { ok: true, installed: blob };
|
|
252
|
+
}
|
|
180
253
|
// ── Registry operations (legacy v1 view over the shared v2 registry) ───────
|
|
181
254
|
function readAccountRegistry() {
|
|
182
255
|
const registry = (0, accountRegistry_1.readSavedAccountRegistry)();
|
|
@@ -230,6 +303,9 @@ function findClaudeProfileForIdentity(identity) {
|
|
|
230
303
|
profiles.find((p) => (p.email ?? p.metadata?.email) === identity.email) ??
|
|
231
304
|
null);
|
|
232
305
|
}
|
|
306
|
+
function profileKey(profile) {
|
|
307
|
+
return profile.providerAccountId ?? profile.id;
|
|
308
|
+
}
|
|
233
309
|
// ── Add current account ──────────────────────────────────────────────────
|
|
234
310
|
function addCurrentAccount(label) {
|
|
235
311
|
const active = readActiveClaudeAccount();
|
|
@@ -249,7 +325,8 @@ function addCurrentAccount(label) {
|
|
|
249
325
|
}
|
|
250
326
|
ensureDirs();
|
|
251
327
|
try {
|
|
252
|
-
|
|
328
|
+
// The live account: backup and metadata only (see the chokepoint).
|
|
329
|
+
withClaudeAuthSwapLock(() => writeClaudeProfileMirrorUnlocked(active.uuid, credBlob, configBlob, 'login'));
|
|
253
330
|
}
|
|
254
331
|
catch (err) {
|
|
255
332
|
return { success: false, error: `Failed to back up Claude credentials: ${err}` };
|
|
@@ -279,25 +356,180 @@ function addCurrentAccount(label) {
|
|
|
279
356
|
// ── Live-state sync ──────────────────────────────────────────────────────
|
|
280
357
|
/**
|
|
281
358
|
* Serializes live Claude credential swaps across processes so two concurrent
|
|
282
|
-
* switchers cannot interleave their backup/write/rollback sequences.
|
|
283
|
-
*
|
|
284
|
-
* ordering: this lock first, the
|
|
285
|
-
*
|
|
359
|
+
* switchers cannot interleave their backup/write/rollback sequences. Claude
|
|
360
|
+
* Code's own refreshes are kept out separately by borrowing its refresh lock
|
|
361
|
+
* (claudeRefreshLock.ts). Lock ordering: this lock first, then the refresh
|
|
362
|
+
* locks, the registry lock (inside setActiveSavedAccount), and the ledger
|
|
363
|
+
* lock — never the reverse. Never spawn a `claude` process while holding it.
|
|
286
364
|
*/
|
|
287
365
|
function withClaudeAuthSwapLock(operation) {
|
|
288
366
|
const lockDir = path.join((0, accountRegistry_1.getAccountsDir)(), 'claude');
|
|
289
367
|
fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
|
|
290
368
|
return (0, atomic_1.withFileLockSync)(path.join(lockDir, 'auth-swap.lock'), operation);
|
|
291
369
|
}
|
|
370
|
+
function readSwitchJournal() {
|
|
371
|
+
const raw = readJsonOrNull(getSwitchJournalPath());
|
|
372
|
+
return raw && typeof raw.to === 'string' ? raw : null;
|
|
373
|
+
}
|
|
374
|
+
function writeSwitchJournal(journal) {
|
|
375
|
+
(0, atomic_1.atomicWriteJsonSync)(getSwitchJournalPath(), journal);
|
|
376
|
+
}
|
|
377
|
+
function clearSwitchJournal() {
|
|
378
|
+
try {
|
|
379
|
+
fs.rmSync(getSwitchJournalPath(), { force: true });
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
/* the next sync clears it */
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/** Give a switched-away account its single CLI-usable copy (it is inactive now). */
|
|
386
|
+
function materializeInactiveProfile(uuid, credentials) {
|
|
387
|
+
const login = (0, claudeCredentials_1.claudeLoginOnly)(credentials);
|
|
388
|
+
if (!login || isLiveClaudeAccount(uuid))
|
|
389
|
+
return;
|
|
390
|
+
(0, credentialIO_1.writeActiveCredentials)(login, (0, claudeProfiles_1.getClaudeProfileHome)(uuid));
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* A switch that died mid-way left its journal behind (sync runs under the swap
|
|
394
|
+
* lock, so a journal it can see belongs to no running switch). Finish or roll
|
|
395
|
+
* back from the live fingerprint — never from `.claude.json` alone, which may
|
|
396
|
+
* still name the other account.
|
|
397
|
+
*/
|
|
398
|
+
function resolveSwitchJournalUnlocked(report) {
|
|
399
|
+
const journal = readSwitchJournal();
|
|
400
|
+
if (!journal)
|
|
401
|
+
return;
|
|
402
|
+
const live = (0, credentialIO_1.readClaudeCredentialStore)();
|
|
403
|
+
if (live.status === 'error') {
|
|
404
|
+
report.warnings.push('An interrupted account switch is pending; the live login could not be read yet.');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const liveFp = live.status === 'ok' ? (0, claudeCredentials_1.claudeRefreshFingerprint)((0, claudeCredentials_1.parseClaudeCredentialBlob)(live.blob)) : null;
|
|
408
|
+
if (liveFp !== null && liveFp === journal.toFp) {
|
|
409
|
+
// The target was installed: finish the commit.
|
|
410
|
+
const toOauth = readProfileClaudeOauthAccount(journal.to) ?? readFlatClaudeOauthAccount(journal.to);
|
|
411
|
+
try {
|
|
412
|
+
if (toOauth && (0, claudeProfiles_1.readLiveClaudeIdentity)()?.uuid !== journal.to) {
|
|
413
|
+
mergeOauthAccountIntoLiveConfig(toOauth);
|
|
414
|
+
}
|
|
415
|
+
if (journal.from) {
|
|
416
|
+
const backup = readClaudeBackup(journal.from);
|
|
417
|
+
if ((0, claudeCredentials_1.claudeRefreshFingerprint)((0, claudeCredentials_1.parseClaudeCredentialBlob)(backup)) === journal.fromFp) {
|
|
418
|
+
materializeInactiveProfile(journal.from, backup);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
(0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(journal.to));
|
|
422
|
+
const target = findClaudeProfile(journal.to);
|
|
423
|
+
if (target)
|
|
424
|
+
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', target.id, { silent: true });
|
|
425
|
+
report.journal = 'finished';
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
report.warnings.push(`Could not finish an interrupted account switch: ${err}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else if (liveFp === journal.fromFp) {
|
|
433
|
+
// Nothing was installed (or it was rolled back): the outgoing login is live.
|
|
434
|
+
report.journal = 'rolled-back';
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
report.journal = 'cleared';
|
|
438
|
+
report.warnings.push('An interrupted account switch was found, but the live login matches neither side; it was left as is.');
|
|
439
|
+
}
|
|
440
|
+
clearSwitchJournal();
|
|
441
|
+
}
|
|
442
|
+
/** The profile whose recorded refresh-token fingerprint is `fp`, other than `exceptUuid`. */
|
|
443
|
+
function findProfileHoldingFingerprint(fp, exceptUuid) {
|
|
444
|
+
return ((0, accountRegistry_1.listSavedAccountProfiles)('claude-code').find((profile) => {
|
|
445
|
+
const uuid = profileKey(profile);
|
|
446
|
+
return uuid !== exceptUuid && (0, accountHealth_1.readHealthSidecar)('claude-code', profile.id)?.refreshFp === fp;
|
|
447
|
+
}) ?? null);
|
|
448
|
+
}
|
|
292
449
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
450
|
+
* Enforce "the live account keeps no CLI-usable copy": if its profile home
|
|
451
|
+
* still holds a credential (older builds mirrored every fold there), remove
|
|
452
|
+
* it. When that copy is strictly newer than live, live is the dying link of
|
|
453
|
+
* the chain, so the copy is installed first (guarded `repair`). The fresher
|
|
454
|
+
* of the two stays in the private backup.
|
|
455
|
+
*/
|
|
456
|
+
function demoteLiveDuplicateUnlocked(uuid, report) {
|
|
457
|
+
const home = (0, claudeProfiles_1.getClaudeProfileHome)(uuid);
|
|
458
|
+
const probe = (0, credentialIO_1.readClaudeCredentialStore)(home);
|
|
459
|
+
if (probe.status === 'absent')
|
|
460
|
+
return;
|
|
461
|
+
if (probe.status === 'error') {
|
|
462
|
+
report.warnings.push("The live account's profile copy could not be read; it was left in place.");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
(0, claudeRefreshLock_1.withClaudeRefreshLocksSync)([(0, claudeProfiles_1.getLiveClaudeHome)(), home], () => {
|
|
467
|
+
const live = (0, credentialIO_1.readClaudeCredentialStore)();
|
|
468
|
+
const duplicate = (0, credentialIO_1.readClaudeCredentialStore)(home);
|
|
469
|
+
if (live.status === 'error' || duplicate.status === 'error') {
|
|
470
|
+
report.warnings.push('A credential store could not be read; the profile copy was left in place.');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (duplicate.status === 'absent')
|
|
474
|
+
return;
|
|
475
|
+
const liveCreds = live.status === 'ok' ? (0, claudeCredentials_1.parseClaudeCredentialBlob)(live.blob) : null;
|
|
476
|
+
const dupBlob = duplicate.status === 'ok' ? duplicate.blob : null;
|
|
477
|
+
const dupCreds = (0, claudeCredentials_1.parseClaudeCredentialBlob)(dupBlob);
|
|
478
|
+
if (dupCreds &&
|
|
479
|
+
(0, claudeCredentials_1.claudeRefreshFingerprint)(dupCreds) !== (0, claudeCredentials_1.claudeRefreshFingerprint)(liveCreds) &&
|
|
480
|
+
(0, claudeCredentials_1.claudeCredentialFreshness)(dupCreds) > (0, claudeCredentials_1.claudeCredentialFreshness)(liveCreds)) {
|
|
481
|
+
const install = installIntoLiveClaudeStore(dupBlob, uuid, 'repair', {
|
|
482
|
+
live,
|
|
483
|
+
liveAccountUuid: uuid,
|
|
484
|
+
});
|
|
485
|
+
if (install.ok) {
|
|
486
|
+
writeClaudeBackup(uuid, install.installed);
|
|
487
|
+
(0, accountHealth_1.writeClaudeHealthSidecar)(uuid, install.installed, 'live-sync');
|
|
488
|
+
report.repaired = uuid;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Keep the newer copy somewhere Claude Code cannot use it.
|
|
492
|
+
writeClaudeBackup(uuid, fresherOf(readClaudeBackup(uuid), dupBlob));
|
|
493
|
+
report.warnings.push(`The live Claude login looks older than its saved copy and could not be repaired (${install.reason}).`);
|
|
494
|
+
if (install.wrote)
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else if (liveCreds && live.status === 'ok') {
|
|
499
|
+
writeClaudeBackup(uuid, fresherOf(readClaudeBackup(uuid), live.blob));
|
|
500
|
+
}
|
|
501
|
+
if ((0, credentialIO_1.deleteStoredCredentials)(home))
|
|
502
|
+
report.demoted = uuid;
|
|
503
|
+
else
|
|
504
|
+
report.warnings.push("The live account's profile copy could not be removed.");
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
if (err instanceof claudeRefreshLock_1.ClaudeRefreshLockBusyError) {
|
|
509
|
+
report.warnings.push("Claude Code was refreshing; the live account's profile copy is removed on the next sync.");
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
report.warnings.push(`Could not remove the live account's profile copy: ${err}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Reconcile the live login with the saved profiles: finish an interrupted
|
|
518
|
+
* switch, register an unknown login (label = email), refresh the matching
|
|
519
|
+
* profile's backup when the live credential is newer, remove any CLI-usable
|
|
520
|
+
* copy of the live account from its profile home, and re-point the active
|
|
521
|
+
* pointer. Caller holds the swap lock. Never throws; problems land in
|
|
522
|
+
* `warnings`.
|
|
297
523
|
*/
|
|
298
524
|
function syncClaudeLiveStateUnlocked(source = 'live-sync') {
|
|
299
525
|
const report = (0, accountSyncTypes_1.emptyProviderSyncReport)();
|
|
526
|
+
if ((0, claudeProfiles_1.isLiveClaudeHomeSidekickProfile)()) {
|
|
527
|
+
report.skipped = 'managed-home';
|
|
528
|
+
report.warnings.push('Unset CLAUDE_CONFIG_DIR before syncing; it points at a sidekick profile home.');
|
|
529
|
+
return report;
|
|
530
|
+
}
|
|
300
531
|
try {
|
|
532
|
+
resolveSwitchJournalUnlocked(report);
|
|
301
533
|
const identity = readActiveClaudeAccount();
|
|
302
534
|
if (!identity) {
|
|
303
535
|
report.skipped = 'logged-out';
|
|
@@ -326,33 +558,48 @@ function syncClaudeLiveStateUnlocked(source = 'live-sync') {
|
|
|
326
558
|
};
|
|
327
559
|
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
328
560
|
}
|
|
329
|
-
const uuid = profile
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
561
|
+
const uuid = profileKey(profile);
|
|
562
|
+
const live = (0, credentialIO_1.readClaudeCredentialStore)();
|
|
563
|
+
if (live.status === 'error') {
|
|
564
|
+
report.warnings.push('The live Claude credential store could not be read.');
|
|
565
|
+
}
|
|
566
|
+
else if (live.status === 'corrupt') {
|
|
567
|
+
report.warnings.push('The live Claude credential store is unreadable (not valid JSON); sign in again with `claude`.');
|
|
568
|
+
}
|
|
569
|
+
else if (live.status === 'ok') {
|
|
570
|
+
const liveCreds = (0, claudeCredentials_1.parseClaudeCredentialBlob)(live.blob);
|
|
571
|
+
const liveFp = (0, claudeCredentials_1.claudeRefreshFingerprint)(liveCreds);
|
|
572
|
+
const owner = liveFp ? findProfileHoldingFingerprint(liveFp, uuid) : null;
|
|
573
|
+
if (owner) {
|
|
574
|
+
// `.claude.json` names one account but the credential is another's
|
|
575
|
+
// (a switch or login mid-way): folding would file the chain under
|
|
576
|
+
// the wrong account, and demoting could delete the right one.
|
|
577
|
+
report.warnings.push(`The live credential belongs to ${owner.label ?? owner.email ?? owner.id}, not ${identity.email}; nothing was folded.`);
|
|
578
|
+
}
|
|
579
|
+
else if (liveCreds) {
|
|
580
|
+
const backupCreds = (0, claudeCredentials_1.parseClaudeCredentialBlob)(readClaudeBackup(uuid));
|
|
581
|
+
const liveFreshness = (0, claudeCredentials_1.claudeCredentialFreshness)(liveCreds);
|
|
582
|
+
const backupFreshness = (0, claudeCredentials_1.claudeCredentialFreshness)(backupCreds);
|
|
583
|
+
const needsFold = !backupCreds ||
|
|
584
|
+
liveFreshness > backupFreshness ||
|
|
585
|
+
(liveFreshness === backupFreshness &&
|
|
586
|
+
((0, claudeCredentials_1.claudeRefreshFingerprint)(backupCreds) !== liveFp ||
|
|
587
|
+
!fs.existsSync(path.join((0, claudeProfiles_1.getClaudeProfileHome)(uuid), '.claude.json'))));
|
|
588
|
+
if (needsFold) {
|
|
589
|
+
try {
|
|
590
|
+
writeClaudeProfileMirrorUnlocked(uuid, live.blob, readCurrentLiveOauthAccount() ?? {
|
|
591
|
+
emailAddress: identity.email,
|
|
592
|
+
accountUuid: identity.uuid,
|
|
593
|
+
}, source);
|
|
594
|
+
report.folded = profile.id;
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
report.warnings.push(`Could not back up the live Claude credentials: ${err}`);
|
|
598
|
+
}
|
|
350
599
|
}
|
|
600
|
+
demoteLiveDuplicateUnlocked(uuid, report);
|
|
351
601
|
}
|
|
352
602
|
}
|
|
353
|
-
else if (liveBlob === null) {
|
|
354
|
-
report.warnings.push('The live Claude credential store could not be read.');
|
|
355
|
-
}
|
|
356
603
|
const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
|
|
357
604
|
if (!active || active.id !== profile.id) {
|
|
358
605
|
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', profile.id, { silent: true });
|
|
@@ -373,9 +620,13 @@ function syncClaudeLiveState(source = 'live-sync') {
|
|
|
373
620
|
return { warnings: [`Could not acquire the account-switch lock: ${err}`] };
|
|
374
621
|
}
|
|
375
622
|
}
|
|
623
|
+
/** Unregistered login homes that authenticated are kept this long for a late finalize. */
|
|
624
|
+
const AUTHENTICATED_PENDING_LOGIN_TTL_MS = 24 * 60 * 60 * 1000;
|
|
376
625
|
/**
|
|
377
|
-
* Remove isolated login homes that never
|
|
378
|
-
* browser, the login timed out) once they
|
|
626
|
+
* Remove isolated login homes that never finished: ones that never
|
|
627
|
+
* authenticated (the user closed the browser, the login timed out) once they
|
|
628
|
+
* are older than `olderThanMs`, and ones that authenticated but were never
|
|
629
|
+
* finalized (the host quit) after a day. Their Keychain item goes with them.
|
|
379
630
|
* Returns the removed login ids.
|
|
380
631
|
*/
|
|
381
632
|
function cleanupAbandonedClaudeLogins(olderThanMs = 6 * 60 * 60 * 1000, now = Date.now()) {
|
|
@@ -397,11 +648,12 @@ function cleanupAbandonedClaudeLogins(olderThanMs = 6 * 60 * 60 * 1000, now = Da
|
|
|
397
648
|
if (!pending)
|
|
398
649
|
continue;
|
|
399
650
|
const home = (0, claudeProfiles_1.getClaudeProfileHome)(loginId);
|
|
400
|
-
|
|
401
|
-
|
|
651
|
+
const threshold = (0, claudeProfiles_1.isClaudeProfileAuthenticated)(home)
|
|
652
|
+
? Math.max(olderThanMs, AUTHENTICATED_PENDING_LOGIN_TTL_MS)
|
|
653
|
+
: olderThanMs;
|
|
402
654
|
const addedAt = pending.addedAt ? Date.parse(pending.addedAt) : NaN;
|
|
403
655
|
const age = Number.isNaN(addedAt) ? Number.POSITIVE_INFINITY : now - addedAt;
|
|
404
|
-
if (age <
|
|
656
|
+
if (age < threshold)
|
|
405
657
|
continue;
|
|
406
658
|
try {
|
|
407
659
|
(0, credentialIO_1.deleteStoredCredentials)(home);
|
|
@@ -414,6 +666,136 @@ function cleanupAbandonedClaudeLogins(olderThanMs = 6 * 60 * 60 * 1000, now = Da
|
|
|
414
666
|
}
|
|
415
667
|
return removed;
|
|
416
668
|
}
|
|
669
|
+
// ── Keep-alive and launch support ────────────────────────────────────────
|
|
670
|
+
/**
|
|
671
|
+
* Why keep-alive must not run the CLI for this Claude profile, or null when it
|
|
672
|
+
* may. A refresh inside a profile home rotates that home's refresh token, so
|
|
673
|
+
* the home must hold the only live copy of its chain. Caller holds the swap lock.
|
|
674
|
+
*/
|
|
675
|
+
function claudeKeepAliveBlockerUnlocked(profile) {
|
|
676
|
+
const uuid = profileKey(profile);
|
|
677
|
+
if (isLiveClaudeAccount(uuid))
|
|
678
|
+
return 'live account; the CLI refreshes it in normal use';
|
|
679
|
+
const store = (0, credentialIO_1.readClaudeCredentialStore)((0, claudeProfiles_1.getClaudeProfileHome)(uuid));
|
|
680
|
+
if (store.status === 'error')
|
|
681
|
+
return 'its saved login could not be read';
|
|
682
|
+
const storeCreds = store.status === 'ok' ? (0, claudeCredentials_1.parseClaudeCredentialBlob)(store.blob) : null;
|
|
683
|
+
if (!storeCreds)
|
|
684
|
+
return 'no profile copy to refresh (keep-alive never restores backups)';
|
|
685
|
+
const live = (0, credentialIO_1.readClaudeCredentialStore)();
|
|
686
|
+
if (live.status === 'error')
|
|
687
|
+
return 'the live login could not be read';
|
|
688
|
+
const liveCreds = live.status === 'ok' ? (0, claudeCredentials_1.parseClaudeCredentialBlob)(live.blob) : null;
|
|
689
|
+
if (liveCreds && (0, claudeCredentials_1.claudeRefreshFingerprint)(liveCreds) === (0, claudeCredentials_1.claudeRefreshFingerprint)(storeCreds)) {
|
|
690
|
+
return 'same refresh token as the live login';
|
|
691
|
+
}
|
|
692
|
+
if ((0, accountHealth_1.readHealthSidecar)('claude-code', profile.id)?.source === 'live-sync') {
|
|
693
|
+
return 'copied from the live login; waits for a switch away from it';
|
|
694
|
+
}
|
|
695
|
+
const backupCreds = (0, claudeCredentials_1.parseClaudeCredentialBlob)(readClaudeBackup(uuid));
|
|
696
|
+
if ((0, claudeCredentials_1.claudeCredentialFreshness)(backupCreds) > (0, claudeCredentials_1.claudeCredentialFreshness)(storeCreds)) {
|
|
697
|
+
return 'its saved backup is newer than the profile copy';
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
function findClaudeProfileById(accountId) {
|
|
702
|
+
return (0, accountRegistry_1.listSavedAccountProfiles)('claude-code').find((p) => p.id === accountId) ?? null;
|
|
703
|
+
}
|
|
704
|
+
/** {@link beginClaudeKeepAlive} without taking the lease (dry runs). */
|
|
705
|
+
function getClaudeKeepAliveBlocker(accountId) {
|
|
706
|
+
try {
|
|
707
|
+
return withClaudeAuthSwapLock(() => {
|
|
708
|
+
const profile = findClaudeProfileById(accountId);
|
|
709
|
+
return profile ? claudeKeepAliveBlockerUnlocked(profile) : 'account not found';
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
return `could not acquire the account-switch lock: ${err}`;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Check a Claude profile for keep-alive and take its lease, both under the
|
|
718
|
+
* swap lock, so no switch can make it live (or copy it into live) while the
|
|
719
|
+
* CLI refreshes it. Pair with {@link finishClaudeKeepAlive}.
|
|
720
|
+
*/
|
|
721
|
+
function beginClaudeKeepAlive(accountId, leaseMs) {
|
|
722
|
+
try {
|
|
723
|
+
return withClaudeAuthSwapLock(() => {
|
|
724
|
+
const profile = findClaudeProfileById(accountId);
|
|
725
|
+
if (!profile)
|
|
726
|
+
return { ok: false, reason: 'account not found' };
|
|
727
|
+
const blocker = claudeKeepAliveBlockerUnlocked(profile);
|
|
728
|
+
if (blocker)
|
|
729
|
+
return { ok: false, reason: blocker };
|
|
730
|
+
if (!(0, accountLease_1.acquireAccountLease)('claude-code', accountId, leaseMs)) {
|
|
731
|
+
return { ok: false, reason: 'another keep-alive run holds this account' };
|
|
732
|
+
}
|
|
733
|
+
return { ok: true };
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
return { ok: false, reason: `could not acquire the account-switch lock: ${err}` };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/** Record what the CLI refreshed (backup + sidecar) and release the lease. */
|
|
741
|
+
function finishClaudeKeepAlive(accountId) {
|
|
742
|
+
try {
|
|
743
|
+
withClaudeAuthSwapLock(() => {
|
|
744
|
+
const profile = findClaudeProfileById(accountId);
|
|
745
|
+
if (!profile)
|
|
746
|
+
return;
|
|
747
|
+
const uuid = profileKey(profile);
|
|
748
|
+
if (isLiveClaudeAccount(uuid))
|
|
749
|
+
return;
|
|
750
|
+
const store = (0, credentialIO_1.readClaudeCredentialStore)((0, claudeProfiles_1.getClaudeProfileHome)(uuid));
|
|
751
|
+
if (store.status !== 'ok')
|
|
752
|
+
return;
|
|
753
|
+
const backup = readClaudeBackup(uuid);
|
|
754
|
+
const storeCreds = (0, claudeCredentials_1.parseClaudeCredentialBlob)(store.blob);
|
|
755
|
+
if (storeCreds &&
|
|
756
|
+
(0, claudeCredentials_1.claudeCredentialFreshness)(storeCreds) >
|
|
757
|
+
(0, claudeCredentials_1.claudeCredentialFreshness)((0, claudeCredentials_1.parseClaudeCredentialBlob)(backup))) {
|
|
758
|
+
const updated = (0, claudeCredentials_1.mergeClaudeLoginIntoLive)(backup, store.blob);
|
|
759
|
+
writeClaudeBackup(uuid, updated);
|
|
760
|
+
(0, accountHealth_1.writeClaudeHealthSidecar)(profile.id, updated, 'keepalive');
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
/* the next sync or probe records the refresh */
|
|
766
|
+
}
|
|
767
|
+
finally {
|
|
768
|
+
(0, accountLease_1.releaseAccountLease)('claude-code', accountId);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Give an inactive profile a CLI-usable copy from its backup when its profile
|
|
773
|
+
* home has none (it was live when it was last seen, or a switch could not
|
|
774
|
+
* write it). User-initiated launches only; keep-alive never does this.
|
|
775
|
+
*/
|
|
776
|
+
function restoreClaudeProfileFromBackup(accountId) {
|
|
777
|
+
try {
|
|
778
|
+
return withClaudeAuthSwapLock(() => {
|
|
779
|
+
const profile = findClaudeProfileById(accountId);
|
|
780
|
+
if (!profile)
|
|
781
|
+
return 'none';
|
|
782
|
+
const uuid = profileKey(profile);
|
|
783
|
+
if (isLiveClaudeAccount(uuid))
|
|
784
|
+
return 'live';
|
|
785
|
+
const store = (0, credentialIO_1.readClaudeCredentialStore)((0, claudeProfiles_1.getClaudeProfileHome)(uuid));
|
|
786
|
+
if (store.status !== 'absent')
|
|
787
|
+
return store.status === 'ok' ? 'present' : 'none';
|
|
788
|
+
const backup = readClaudeBackup(uuid);
|
|
789
|
+
if (!(0, claudeCredentials_1.parseClaudeCredentialBlob)(backup))
|
|
790
|
+
return 'none';
|
|
791
|
+
materializeInactiveProfile(uuid, backup);
|
|
792
|
+
return 'restored';
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
return 'none';
|
|
797
|
+
}
|
|
798
|
+
}
|
|
417
799
|
// ── Switch to account ────────────────────────────────────────────────────
|
|
418
800
|
function switchToAccount(uuid, options = {}) {
|
|
419
801
|
const consumers = (0, processDetection_1.detectRunningAccountConsumersSync)('claude-code');
|
|
@@ -441,15 +823,13 @@ function switchToAccountUnlocked(uuid, options, consumers) {
|
|
|
441
823
|
const targetName = target.label ?? target.email ?? uuid;
|
|
442
824
|
// Refuse to treat one of our own profile homes as the live home: a shell
|
|
443
825
|
// that eval'd `sidekick accounts env` would otherwise overwrite that profile.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
// account known: it is whatever was really live, not the stale pointer.
|
|
452
|
-
const sync = syncClaudeLiveStateUnlocked('switch');
|
|
826
|
+
if ((0, claudeProfiles_1.isLiveClaudeHomeSidekickProfile)()) {
|
|
827
|
+
return (0, accountSwitch_1.switchFailure)('claude-code', uuid, `CLAUDE_CONFIG_DIR points at a sidekick profile home (${path.resolve((0, claudeProfiles_1.getLiveClaudeHome)())}); unset it before switching accounts.`);
|
|
828
|
+
}
|
|
829
|
+
// Phase 1: register the outgoing login if sidekick has never seen it, finish
|
|
830
|
+
// any interrupted switch, and enforce the one-copy rule. Only then is the
|
|
831
|
+
// "previous" account known: it is whatever was really live.
|
|
832
|
+
const sync = syncClaudeLiveStateUnlocked('live-sync');
|
|
453
833
|
const previousAccountId = (0, accountRegistry_1.getActiveSavedAccount)('claude-code')?.id ?? null;
|
|
454
834
|
const base = {
|
|
455
835
|
success: true,
|
|
@@ -475,8 +855,6 @@ function switchToAccountUnlocked(uuid, options, consumers) {
|
|
|
475
855
|
hints: [`${targetName} is already the active Claude account.`],
|
|
476
856
|
});
|
|
477
857
|
}
|
|
478
|
-
// Materialise pre-migration flat backups into the profile home first so the
|
|
479
|
-
// health probe (and the install) read one canonical store.
|
|
480
858
|
const stored = readStoredClaudeAccount(uuid);
|
|
481
859
|
if (!stored) {
|
|
482
860
|
return (0, accountSwitch_1.finishSwitchResult)({
|
|
@@ -486,7 +864,8 @@ function switchToAccountUnlocked(uuid, options, consumers) {
|
|
|
486
864
|
error: `Stored credentials for "${targetName}" not found; sign in again.`,
|
|
487
865
|
});
|
|
488
866
|
}
|
|
489
|
-
// Preflight
|
|
867
|
+
// Preflight on the credential that would be installed: never install one we
|
|
868
|
+
// already know is dead.
|
|
490
869
|
const health = (0, accountHealth_1.getAccountHealth)('claude-code', target.id, { probe: 'store' });
|
|
491
870
|
base.health = health;
|
|
492
871
|
if ((health.state === 'expired' || health.state === 'missing') && !options.force) {
|
|
@@ -500,8 +879,123 @@ function switchToAccountUnlocked(uuid, options, consumers) {
|
|
|
500
879
|
if (health.state === 'expiring' && health.reason) {
|
|
501
880
|
base.warnings.push(`"${targetName}": ${health.reason}`);
|
|
502
881
|
}
|
|
503
|
-
|
|
504
|
-
|
|
882
|
+
if (!options.force && (0, accountLease_1.isAccountLeaseHeld)('claude-code', target.id)) {
|
|
883
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
884
|
+
...base,
|
|
885
|
+
success: false,
|
|
886
|
+
error: `Keep-alive is refreshing "${targetName}" right now; try again in a few seconds.`,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
return (0, claudeRefreshLock_1.withClaudeRefreshLocksSync)([(0, claudeProfiles_1.getLiveClaudeHome)(), (0, claudeProfiles_1.getClaudeProfileHome)(uuid)], () => installSwitchTargetUnlocked(target, targetName, base, previousAccountId));
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
894
|
+
...base,
|
|
895
|
+
success: false,
|
|
896
|
+
error: err instanceof claudeRefreshLock_1.ClaudeRefreshLockBusyError
|
|
897
|
+
? `${err.message} Nothing was changed.`
|
|
898
|
+
: `The switch failed before anything was changed: ${err}`,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Capture → journal → install → verify → commit, holding the swap lock and
|
|
904
|
+
* Claude Code's refresh locks for the live home and the target's home.
|
|
905
|
+
*/
|
|
906
|
+
function installSwitchTargetUnlocked(target, targetName, base, previousAccountId) {
|
|
907
|
+
const uuid = profileKey(target);
|
|
908
|
+
const fail = (error, extra = {}) => (0, accountSwitch_1.finishSwitchResult)({ ...base, success: false, error, ...extra });
|
|
909
|
+
// Re-read inside the refresh locks: a keep-alive or CLI may have moved either store.
|
|
910
|
+
const stored = readStoredClaudeAccount(uuid);
|
|
911
|
+
const targetCreds = (0, claudeCredentials_1.parseClaudeCredentialBlob)(stored?.credentials);
|
|
912
|
+
if (!stored || !targetCreds) {
|
|
913
|
+
return fail(`Stored credentials for "${targetName}" not found; sign in again.`, {
|
|
914
|
+
needsLogin: true,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
let live = (0, credentialIO_1.readClaudeCredentialStore)();
|
|
918
|
+
if (live.status === 'error') {
|
|
919
|
+
return fail('The live Claude login could not be read, so nothing was changed. Try again.');
|
|
920
|
+
}
|
|
921
|
+
const liveIdentity = readActiveClaudeAccount();
|
|
922
|
+
const outgoing = liveIdentity ? findClaudeProfileForIdentity(liveIdentity) : null;
|
|
923
|
+
const outgoingUuid = outgoing ? profileKey(outgoing) : null;
|
|
924
|
+
let liveCreds = live.status === 'ok' ? (0, claudeCredentials_1.parseClaudeCredentialBlob)(live.blob) : null;
|
|
925
|
+
if (outgoingUuid === uuid) {
|
|
926
|
+
// The live login is this profile under a rotated account id (matched by
|
|
927
|
+
// email): installing its saved copy would be a downgrade, not a switch.
|
|
928
|
+
return (0, accountSwitch_1.finishSwitchResult)({
|
|
929
|
+
...base,
|
|
930
|
+
alreadyActive: true,
|
|
931
|
+
verified: true,
|
|
932
|
+
verification: 'store',
|
|
933
|
+
health: (0, accountHealth_1.getAccountHealth)('claude-code', target.id),
|
|
934
|
+
hints: [`${targetName} is already the active Claude account.`],
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const liveFp = (0, claudeCredentials_1.claudeRefreshFingerprint)(liveCreds);
|
|
938
|
+
const liveOwner = liveFp && outgoingUuid ? findProfileHoldingFingerprint(liveFp, outgoingUuid) : null;
|
|
939
|
+
if (liveOwner) {
|
|
940
|
+
// Capturing would file that chain under the wrong account.
|
|
941
|
+
return fail(`The live credential belongs to ${liveOwner.label ?? liveOwner.email ?? liveOwner.id}, but the live identity says ${liveIdentity?.email}; run \`claude\` once (or sign in again) and retry. Nothing was changed.`);
|
|
942
|
+
}
|
|
943
|
+
// Capture the outgoing login, verified, before anything replaces it. This
|
|
944
|
+
// is mandatory even with `force`: losing it would lose that account's chain.
|
|
945
|
+
let captured = false;
|
|
946
|
+
const capture = (blob) => {
|
|
947
|
+
if (!outgoingUuid) {
|
|
948
|
+
return 'The live login has no account identity to save it under; run `claude` once and retry.';
|
|
949
|
+
}
|
|
950
|
+
try {
|
|
951
|
+
writeClaudeBackup(outgoingUuid, blob);
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
return `The outgoing login could not be saved, so nothing was changed: ${err}`;
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
if (liveCreds && live.status === 'ok') {
|
|
959
|
+
const error = capture(live.blob);
|
|
960
|
+
if (error)
|
|
961
|
+
return fail(error);
|
|
962
|
+
captured = true;
|
|
963
|
+
}
|
|
964
|
+
const fromFp = (0, claudeCredentials_1.claudeRefreshFingerprint)(liveCreds);
|
|
965
|
+
const toFp = (0, claudeCredentials_1.claudeRefreshFingerprint)(targetCreds);
|
|
966
|
+
writeSwitchJournal({
|
|
967
|
+
from: outgoingUuid,
|
|
968
|
+
to: uuid,
|
|
969
|
+
fromFp,
|
|
970
|
+
toFp,
|
|
971
|
+
at: new Date().toISOString(),
|
|
972
|
+
});
|
|
973
|
+
// The live store must not have moved since the capture.
|
|
974
|
+
const recheck = (0, credentialIO_1.readClaudeCredentialStore)();
|
|
975
|
+
if (recheck.status === 'error') {
|
|
976
|
+
clearSwitchJournal();
|
|
977
|
+
return fail('The live Claude login could not be re-read, so nothing was changed. Try again.');
|
|
978
|
+
}
|
|
979
|
+
const recheckCreds = recheck.status === 'ok' ? (0, claudeCredentials_1.parseClaudeCredentialBlob)(recheck.blob) : null;
|
|
980
|
+
if ((0, claudeCredentials_1.claudeRefreshFingerprint)(recheckCreds) !== fromFp &&
|
|
981
|
+
recheck.status === 'ok' &&
|
|
982
|
+
recheckCreds) {
|
|
983
|
+
const error = capture(recheck.blob);
|
|
984
|
+
if (error) {
|
|
985
|
+
clearSwitchJournal();
|
|
986
|
+
return fail(error);
|
|
987
|
+
}
|
|
988
|
+
captured = true;
|
|
989
|
+
live = recheck;
|
|
990
|
+
liveCreds = recheckCreds;
|
|
991
|
+
writeSwitchJournal({
|
|
992
|
+
from: outgoingUuid,
|
|
993
|
+
to: uuid,
|
|
994
|
+
fromFp: (0, claudeCredentials_1.claudeRefreshFingerprint)(liveCreds),
|
|
995
|
+
toFp,
|
|
996
|
+
at: new Date().toISOString(),
|
|
997
|
+
});
|
|
998
|
+
}
|
|
505
999
|
let originalConfig = null;
|
|
506
1000
|
try {
|
|
507
1001
|
originalConfig = fs.readFileSync((0, claudeProfiles_1.getClaudeConfigPath)(), 'utf8');
|
|
@@ -509,13 +1003,25 @@ function switchToAccountUnlocked(uuid, options, consumers) {
|
|
|
509
1003
|
catch {
|
|
510
1004
|
/* no existing config to back up */
|
|
511
1005
|
}
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1006
|
+
const install = installIntoLiveClaudeStore(stored.credentials, uuid, 'switch', {
|
|
1007
|
+
live,
|
|
1008
|
+
liveAccountUuid: liveIdentity?.uuid ?? null,
|
|
1009
|
+
capturedOutgoing: captured,
|
|
1010
|
+
});
|
|
1011
|
+
const rollback = (written) => {
|
|
1012
|
+
// Nothing usable was displaced when live held no login: there is nothing
|
|
1013
|
+
// to put back, and the result must not claim otherwise.
|
|
1014
|
+
let restored = live.status === 'ok';
|
|
1015
|
+
if (live.status === 'ok') {
|
|
1016
|
+
const current = (0, credentialIO_1.readClaudeCredentialStore)();
|
|
1017
|
+
const untouched = current.status === 'ok' && JSON.stringify(current.blob) === JSON.stringify(live.blob);
|
|
1018
|
+
if (!untouched) {
|
|
1019
|
+
const undo = installIntoLiveClaudeStore(live.blob, outgoingUuid ?? uuid, 'rollback', {
|
|
1020
|
+
live: current,
|
|
1021
|
+
liveAccountUuid: readActiveClaudeAccount()?.uuid ?? null,
|
|
1022
|
+
written,
|
|
1023
|
+
});
|
|
1024
|
+
restored = undo.ok;
|
|
519
1025
|
}
|
|
520
1026
|
}
|
|
521
1027
|
if (originalConfig !== null) {
|
|
@@ -523,7 +1029,7 @@ function switchToAccountUnlocked(uuid, options, consumers) {
|
|
|
523
1029
|
fs.writeFileSync((0, claudeProfiles_1.getClaudeConfigPath)(), originalConfig);
|
|
524
1030
|
}
|
|
525
1031
|
catch {
|
|
526
|
-
|
|
1032
|
+
restored = false;
|
|
527
1033
|
}
|
|
528
1034
|
}
|
|
529
1035
|
try {
|
|
@@ -532,50 +1038,69 @@ function switchToAccountUnlocked(uuid, options, consumers) {
|
|
|
532
1038
|
catch {
|
|
533
1039
|
/* rollback failed */
|
|
534
1040
|
}
|
|
1041
|
+
clearSwitchJournal();
|
|
1042
|
+
return restored;
|
|
535
1043
|
};
|
|
536
|
-
|
|
1044
|
+
const restoredSuffix = (restored) => restored
|
|
1045
|
+
? 'the previous login was restored.'
|
|
1046
|
+
: captured
|
|
1047
|
+
? 'the previous login could not be restored automatically; it is saved, run `sidekick accounts doctor`.'
|
|
1048
|
+
: 'the live store held no usable login before the switch; run `sidekick accounts doctor`.';
|
|
1049
|
+
if (!install.ok) {
|
|
1050
|
+
if (!install.wrote) {
|
|
1051
|
+
clearSwitchJournal();
|
|
1052
|
+
return fail(install.reason);
|
|
1053
|
+
}
|
|
1054
|
+
const restored = rollback(install.attempted);
|
|
1055
|
+
return fail(`Failed to write Claude credentials (${install.reason}); ${restoredSuffix(restored)}`);
|
|
1056
|
+
}
|
|
537
1057
|
try {
|
|
538
|
-
(0, credentialIO_1.writeActiveCredentials)(stored.credentials);
|
|
539
1058
|
mergeOauthAccountIntoLiveConfig(stored.oauthAccount);
|
|
540
1059
|
}
|
|
541
1060
|
catch (err) {
|
|
542
|
-
rollback();
|
|
543
|
-
return (
|
|
544
|
-
...base,
|
|
545
|
-
success: false,
|
|
546
|
-
error: `Failed to write Claude credentials: ${err}`,
|
|
547
|
-
});
|
|
1061
|
+
const restored = rollback(install.installed);
|
|
1062
|
+
return fail(`Failed to write the Claude identity (${err}); ${restoredSuffix(restored)}`);
|
|
548
1063
|
}
|
|
549
1064
|
// Verify by re-reading the live store.
|
|
550
|
-
const installed = (0,
|
|
551
|
-
const
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
installed.accessToken === expected.accessToken &&
|
|
556
|
-
liveIdentity?.uuid === uuid;
|
|
1065
|
+
const installed = (0, credentialIO_1.readClaudeCredentialStore)(undefined, { keychainOnly: true });
|
|
1066
|
+
const installedCreds = installed.status === 'ok' ? (0, claudeCredentials_1.parseClaudeCredentialBlob)(installed.blob) : null;
|
|
1067
|
+
const verified = installedCreds !== null &&
|
|
1068
|
+
installedCreds.accessToken === targetCreds.accessToken &&
|
|
1069
|
+
readActiveClaudeAccount()?.uuid === uuid;
|
|
557
1070
|
if (!verified) {
|
|
558
|
-
rollback();
|
|
559
|
-
return (
|
|
560
|
-
...base,
|
|
561
|
-
success: false,
|
|
562
|
-
verification: 'failed',
|
|
563
|
-
error: `The live Claude credential store did not reflect the switch to "${targetName}"; the previous login was restored.`,
|
|
564
|
-
});
|
|
1071
|
+
const restored = rollback(install.installed);
|
|
1072
|
+
return fail(`The live Claude credential store did not reflect the switch to "${targetName}"; ${restoredSuffix(restored)}`, { verification: 'failed' });
|
|
565
1073
|
}
|
|
566
|
-
// Pointer
|
|
567
|
-
// previous account, which the next sync repairs from the live identity.
|
|
1074
|
+
// Pointer before the rest of the commit: from here on the switch happened.
|
|
568
1075
|
try {
|
|
569
1076
|
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', target.id);
|
|
570
1077
|
}
|
|
571
1078
|
catch (err) {
|
|
572
|
-
rollback();
|
|
573
|
-
return (
|
|
574
|
-
...base,
|
|
575
|
-
success: false,
|
|
576
|
-
error: `Failed to update account registry: ${err}`,
|
|
577
|
-
});
|
|
1079
|
+
const restored = rollback(install.installed);
|
|
1080
|
+
return fail(`Failed to update account registry (${err}); ${restoredSuffix(restored)}`);
|
|
578
1081
|
}
|
|
1082
|
+
// Commit: the outgoing account becomes inactive and gets its single CLI copy;
|
|
1083
|
+
// the target is live and keeps none.
|
|
1084
|
+
if (outgoingUuid && captured && live.status === 'ok') {
|
|
1085
|
+
try {
|
|
1086
|
+
materializeInactiveProfile(outgoingUuid, live.blob);
|
|
1087
|
+
(0, accountHealth_1.writeClaudeHealthSidecar)(outgoingUuid, live.blob, 'switch');
|
|
1088
|
+
}
|
|
1089
|
+
catch (err) {
|
|
1090
|
+
base.warnings.push(`The previous login is saved, but its profile copy could not be written yet (${err}).`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
writeClaudeBackup(uuid, install.installed);
|
|
1095
|
+
(0, accountHealth_1.writeClaudeHealthSidecar)(uuid, install.installed, 'switch');
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
/* the next sync refreshes the live account's backup */
|
|
1099
|
+
}
|
|
1100
|
+
if (!(0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(uuid))) {
|
|
1101
|
+
base.warnings.push(`"${targetName}"'s profile copy could not be removed; the next sync retries.`);
|
|
1102
|
+
}
|
|
1103
|
+
clearSwitchJournal();
|
|
579
1104
|
let undoToken;
|
|
580
1105
|
try {
|
|
581
1106
|
undoToken = (0, accountSwitch_1.writeLastSwitch)('claude-code', previousAccountId, target.id).token;
|
|
@@ -594,94 +1119,89 @@ function switchToAccountUnlocked(uuid, options, consumers) {
|
|
|
594
1119
|
/**
|
|
595
1120
|
* Install a saved profile's credentials into the live home without changing
|
|
596
1121
|
* which account is active: used right after a re-login of the live account,
|
|
597
|
-
* whose freshly minted token must replace the (possibly dead) live one.
|
|
1122
|
+
* whose freshly minted token must replace the (possibly dead) live one. The
|
|
1123
|
+
* guard only lets a strictly fresher credential of the same account through;
|
|
1124
|
+
* when live is already at least as new, nothing is written.
|
|
598
1125
|
*/
|
|
599
1126
|
function applyClaudeProfileToLiveHome(uuid) {
|
|
1127
|
+
if ((0, claudeProfiles_1.isLiveClaudeHomeSidekickProfile)()) {
|
|
1128
|
+
return {
|
|
1129
|
+
success: false,
|
|
1130
|
+
error: `CLAUDE_CONFIG_DIR points at a sidekick profile home (${path.resolve((0, claudeProfiles_1.getLiveClaudeHome)())}); the login was saved, but unset it before making the account live.`,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
600
1133
|
try {
|
|
601
1134
|
return withClaudeAuthSwapLock(() => {
|
|
602
1135
|
const stored = readStoredClaudeAccount(uuid);
|
|
603
1136
|
if (!stored) {
|
|
604
1137
|
return { success: false, error: `Stored credentials for ${uuid} not found.` };
|
|
605
1138
|
}
|
|
606
|
-
(0,
|
|
607
|
-
|
|
608
|
-
|
|
1139
|
+
const profileHome = (0, claudeProfiles_1.getClaudeProfileHome)(uuid);
|
|
1140
|
+
return (0, claudeRefreshLock_1.withClaudeRefreshLocksSync)([(0, claudeProfiles_1.getLiveClaudeHome)(), profileHome], () => {
|
|
1141
|
+
const live = (0, credentialIO_1.readClaudeCredentialStore)();
|
|
1142
|
+
const liveUuid = readActiveClaudeAccount()?.uuid ?? null;
|
|
1143
|
+
const install = installIntoLiveClaudeStore(stored.credentials, uuid, 'relogin', {
|
|
1144
|
+
live,
|
|
1145
|
+
liveAccountUuid: liveUuid,
|
|
1146
|
+
});
|
|
1147
|
+
if (!install.ok) {
|
|
1148
|
+
if (install.code === 'not-newer') {
|
|
1149
|
+
return { success: true, warning: install.reason };
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
success: false,
|
|
1153
|
+
error: `Could not install the refreshed credentials: ${install.reason}`,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
mergeOauthAccountIntoLiveConfig(stored.oauthAccount);
|
|
1157
|
+
writeClaudeBackup(uuid, install.installed);
|
|
1158
|
+
(0, accountHealth_1.writeClaudeHealthSidecar)(uuid, install.installed, 'login');
|
|
1159
|
+
// The account is live now: its profile home keeps no CLI copy.
|
|
1160
|
+
(0, credentialIO_1.deleteStoredCredentials)(profileHome);
|
|
1161
|
+
return { success: true };
|
|
1162
|
+
});
|
|
609
1163
|
});
|
|
610
1164
|
}
|
|
611
1165
|
catch (err) {
|
|
612
1166
|
return { success: false, error: `Could not install the refreshed credentials: ${err}` };
|
|
613
1167
|
}
|
|
614
1168
|
}
|
|
1169
|
+
/**
|
|
1170
|
+
* @deprecated The home `claude` should use for the active account. The live
|
|
1171
|
+
* account's profile home holds no credential, so this is the live home for the
|
|
1172
|
+
* live account; kept for API compatibility.
|
|
1173
|
+
*/
|
|
615
1174
|
function resolveActiveClaudeHome() {
|
|
616
1175
|
const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
|
|
617
1176
|
if (!active)
|
|
618
1177
|
return (0, claudeProfiles_1.getLiveClaudeHome)();
|
|
619
|
-
|
|
1178
|
+
const uuid = active.providerAccountId ?? active.id;
|
|
1179
|
+
return isLiveClaudeAccount(uuid) ? (0, claudeProfiles_1.getLiveClaudeHome)() : (0, claudeProfiles_1.getClaudeProfileHome)(uuid);
|
|
620
1180
|
}
|
|
1181
|
+
/**
|
|
1182
|
+
* @deprecated Re-install the active profile's credential into the live home.
|
|
1183
|
+
* It never follows the registry pointer across accounts (after a native
|
|
1184
|
+
* `claude /login` the pointer can be stale): when the live login is another
|
|
1185
|
+
* account it refuses — use {@link switchToAccount}. For the same account it is
|
|
1186
|
+
* a guarded re-login that writes only a strictly fresher credential.
|
|
1187
|
+
*/
|
|
621
1188
|
function applyActiveClaudeToLiveHome() {
|
|
622
|
-
try {
|
|
623
|
-
// Keep the non-throwing result contract when the lock cannot be acquired.
|
|
624
|
-
return withClaudeAuthSwapLock(applyActiveClaudeToLiveHomeUnlocked);
|
|
625
|
-
}
|
|
626
|
-
catch (err) {
|
|
627
|
-
return { success: false, error: `Could not acquire the account-switch lock: ${err}` };
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
function applyActiveClaudeToLiveHomeUnlocked() {
|
|
631
1189
|
const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
|
|
632
1190
|
if (!active) {
|
|
633
1191
|
return { success: false, error: 'No active Claude account found.' };
|
|
634
1192
|
}
|
|
635
1193
|
const uuid = active.providerAccountId ?? active.id;
|
|
636
|
-
const
|
|
637
|
-
if (
|
|
638
|
-
return {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
try {
|
|
643
|
-
originalConfig = fs.readFileSync((0, claudeProfiles_1.getClaudeConfigPath)(), 'utf8');
|
|
644
|
-
}
|
|
645
|
-
catch {
|
|
646
|
-
/* no existing config to back up */
|
|
647
|
-
}
|
|
648
|
-
try {
|
|
649
|
-
(0, credentialIO_1.writeActiveCredentials)(stored.credentials);
|
|
650
|
-
}
|
|
651
|
-
catch (err) {
|
|
652
|
-
if (originalCreds) {
|
|
653
|
-
try {
|
|
654
|
-
(0, credentialIO_1.writeActiveCredentials)(originalCreds);
|
|
655
|
-
}
|
|
656
|
-
catch {
|
|
657
|
-
/* rollback failed */
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
return { success: false, error: `Failed to write credentials: ${err}` };
|
|
661
|
-
}
|
|
662
|
-
try {
|
|
663
|
-
mergeOauthAccountIntoLiveConfig(stored.oauthAccount);
|
|
1194
|
+
const liveUuid = readActiveClaudeAccount()?.uuid;
|
|
1195
|
+
if (liveUuid && liveUuid !== uuid) {
|
|
1196
|
+
return {
|
|
1197
|
+
success: false,
|
|
1198
|
+
error: 'The live Claude login belongs to another account; use switchToAccount() to change accounts.',
|
|
1199
|
+
};
|
|
664
1200
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
try {
|
|
668
|
-
(0, credentialIO_1.writeActiveCredentials)(originalCreds);
|
|
669
|
-
}
|
|
670
|
-
catch {
|
|
671
|
-
/* rollback failed */
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
if (originalConfig) {
|
|
675
|
-
try {
|
|
676
|
-
fs.writeFileSync((0, claudeProfiles_1.getClaudeConfigPath)(), originalConfig);
|
|
677
|
-
}
|
|
678
|
-
catch {
|
|
679
|
-
/* rollback failed */
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
return { success: false, error: `Failed to write config: ${err}` };
|
|
1201
|
+
if (!readStoredClaudeAccount(uuid)) {
|
|
1202
|
+
return { success: false, error: `Stored credentials for ${active.email ?? uuid} not found.` };
|
|
683
1203
|
}
|
|
684
|
-
return
|
|
1204
|
+
return applyClaudeProfileToLiveHome(uuid);
|
|
685
1205
|
}
|
|
686
1206
|
function reconcileClaudeAuthState() {
|
|
687
1207
|
try {
|
|
@@ -693,15 +1213,18 @@ function reconcileClaudeAuthState() {
|
|
|
693
1213
|
for (const profile of registry.accounts.filter((account) => account.providerId === 'claude-code')) {
|
|
694
1214
|
try {
|
|
695
1215
|
const uuid = profile.providerAccountId ?? profile.id;
|
|
1216
|
+
// The live account keeps no profile-home copy; nothing to migrate.
|
|
1217
|
+
if (isLiveClaudeAccount(uuid))
|
|
1218
|
+
continue;
|
|
696
1219
|
if ((0, credentialIO_1.readActiveCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(uuid)) &&
|
|
697
1220
|
readProfileClaudeOauthAccount(uuid)) {
|
|
698
1221
|
continue;
|
|
699
1222
|
}
|
|
700
|
-
const credentials =
|
|
1223
|
+
const credentials = readClaudeBackup(uuid);
|
|
701
1224
|
const oauthAccount = readFlatClaudeOauthAccount(uuid);
|
|
702
1225
|
if (!credentials || !oauthAccount)
|
|
703
1226
|
continue;
|
|
704
|
-
|
|
1227
|
+
withClaudeAuthSwapLock(() => writeClaudeProfileMirrorUnlocked(uuid, credentials, oauthAccount, 'migration'));
|
|
705
1228
|
}
|
|
706
1229
|
catch {
|
|
707
1230
|
// Migration is best-effort per account.
|
|
@@ -728,7 +1251,7 @@ function removeAccount(uuid) {
|
|
|
728
1251
|
}
|
|
729
1252
|
// Remove backed-up files and the profile home (credential store included).
|
|
730
1253
|
for (const filePath of [
|
|
731
|
-
|
|
1254
|
+
(0, claudeProfiles_1.getClaudeCredentialBackupPath)(uuid),
|
|
732
1255
|
path.join(getConfigsDir(), `${uuid}.config.json`),
|
|
733
1256
|
]) {
|
|
734
1257
|
try {
|