nterminal 1.3.7 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/client/assets/MarkdownPreview-D3Oa7_1H.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-CWvFo7OT.js → TranscriptMarkdownBody-Dd6cp7t2.js} +1 -1
- package/dist/client/assets/index-BOvp6zV9.js +58 -0
- package/dist/client/assets/index-CWRDvfyP.css +1 -0
- package/dist/client/assets/{index-Cc-QI7Ho.js → index-DJwOCZv_.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/accounts/accountService.d.ts +5 -2
- package/dist/server/accounts/accountService.js +172 -49
- package/dist/server/accounts/accountService.js.map +1 -1
- package/dist/server/agent/agentRoutes.js +2 -2
- package/dist/server/agent/agentRoutes.js.map +1 -1
- package/dist/server/routes/agentManagementRoutes.d.ts +2 -0
- package/dist/server/routes/agentManagementRoutes.js +2 -2
- package/dist/server/routes/agentManagementRoutes.js.map +1 -1
- package/dist/server/system/stats.d.ts +4 -1
- package/dist/server/system/stats.js +6 -0
- package/dist/server/system/stats.js.map +1 -1
- package/dist/shared/accountRelogin.d.ts +3 -0
- package/dist/shared/accountRelogin.js +21 -0
- package/dist/shared/accountRelogin.js.map +1 -0
- package/dist/shared/protocol.d.ts +7 -0
- package/dist/shared/protocol.js.map +1 -1
- package/docs/features.md +3 -2
- package/package.json +1 -1
- package/dist/client/assets/MarkdownPreview-Bx3a_UAM.js +0 -1
- package/dist/client/assets/index-BdHPdvab.js +0 -58
- package/dist/client/assets/index-W-zGfY1a.css +0 -1
|
@@ -4,6 +4,7 @@ import { execFile } from 'node:child_process';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { accountReloginReason } from '../../shared/accountRelogin.js';
|
|
7
8
|
import { ProviderRequestError, requestAccountUsage, requestTokenRefresh, unavailableUsage } from './providerRequests.js';
|
|
8
9
|
const VAULT_DIRECTORY = 'credential-vault';
|
|
9
10
|
const VAULT_INDEX = 'index.json';
|
|
@@ -210,7 +211,12 @@ export class AccountService {
|
|
|
210
211
|
await Promise.all(['codex', 'claude'].map(async (provider) => {
|
|
211
212
|
try {
|
|
212
213
|
const representedPanes = new Set(running.filter((pane) => pane.provider === provider).map((pane) => pane.terminalId));
|
|
213
|
-
|
|
214
|
+
const listed = await this.processLister(provider);
|
|
215
|
+
const daemonHelpers = daemonDescendants(listed);
|
|
216
|
+
processes.set(provider, listed
|
|
217
|
+
// Helpers the shared daemon spawns (e.g. codex-code-mode-host) are part
|
|
218
|
+
// of the daemon entry, not separate sessions.
|
|
219
|
+
.filter((entry) => !daemonHelpers.has(entry.pid))
|
|
214
220
|
.filter((entry) => isCodexDaemon(entry) || !representedPanes.has(paneProcesses.get(entry.pid) ?? ''))
|
|
215
221
|
.map((entry) => ({ pid: entry.pid, provider, command: entry.command, ...(isCodexDaemon(entry) ? { role: 'daemon' } : {}) })));
|
|
216
222
|
}
|
|
@@ -227,9 +233,15 @@ export class AccountService {
|
|
|
227
233
|
providers: ['codex', 'claude'].map((provider) => this.providerStatus(refreshed, provider, running, processes.get(provider) ?? [], processCheckUnavailable, pendingReady.get(provider) === true))
|
|
228
234
|
};
|
|
229
235
|
}
|
|
236
|
+
// Providers with at least one saved login that needs a new sign-in. Cheap
|
|
237
|
+
// (index metadata only), so the server list can poll it with system stats.
|
|
238
|
+
async reloginRequired() {
|
|
239
|
+
const index = await this.readIndex();
|
|
240
|
+
const now = this.now().getTime();
|
|
241
|
+
return ['codex', 'claude'].filter((provider) => index.profiles.some((profile) => profile.provider === provider && accountReloginReason(profile, now) !== null));
|
|
242
|
+
}
|
|
230
243
|
async selectUnlocked(provider, profileId) {
|
|
231
244
|
const index = await this.readIndex();
|
|
232
|
-
await this.assertNoRunningPanes(provider, { allowTerminationAcknowledgement: true });
|
|
233
245
|
if (index.pending[provider])
|
|
234
246
|
throw new AccountServiceError('account_login_already_pending');
|
|
235
247
|
await this.captureActiveCredentials(index, provider);
|
|
@@ -237,6 +249,8 @@ export class AccountService {
|
|
|
237
249
|
if (!profile)
|
|
238
250
|
throw new AccountServiceError('account_not_found');
|
|
239
251
|
if (index.active[provider] === profile.id) {
|
|
252
|
+
if (clearExternalSwitch(index, provider))
|
|
253
|
+
await this.writeIndex(index);
|
|
240
254
|
this.clearTerminationAcknowledgement(provider);
|
|
241
255
|
return this.status();
|
|
242
256
|
}
|
|
@@ -246,6 +260,7 @@ export class AccountService {
|
|
|
246
260
|
try {
|
|
247
261
|
await this.writeCredentialFile(provider, credentials);
|
|
248
262
|
index.active[provider] = profile.id;
|
|
263
|
+
clearExternalSwitch(index, provider);
|
|
249
264
|
await this.writeIndex(index);
|
|
250
265
|
}
|
|
251
266
|
catch (error) {
|
|
@@ -253,11 +268,10 @@ export class AccountService {
|
|
|
253
268
|
await this.writeCredentialFile(provider, current);
|
|
254
269
|
else
|
|
255
270
|
await this.removeCredentialFile(provider);
|
|
256
|
-
if (previousId)
|
|
257
|
-
await rm(this.profilePath(previousId), { force: true });
|
|
258
271
|
throw error;
|
|
259
272
|
}
|
|
260
|
-
|
|
273
|
+
// The selected profile's vault copy is exactly what was just published, so
|
|
274
|
+
// it already is the active backup. The previous account kept its archive.
|
|
261
275
|
this.clearTerminationAcknowledgement(provider);
|
|
262
276
|
return this.status();
|
|
263
277
|
}
|
|
@@ -273,6 +287,8 @@ export class AccountService {
|
|
|
273
287
|
throw new AccountServiceError('cannot_remove_active_account');
|
|
274
288
|
}
|
|
275
289
|
index.profiles = index.profiles.filter((candidate) => candidate.id !== profile.id);
|
|
290
|
+
if (index.externalSwitch?.[provider]?.fromProfileId === profile.id)
|
|
291
|
+
clearExternalSwitch(index, provider);
|
|
276
292
|
await this.writeIndex(index);
|
|
277
293
|
await rm(this.profilePath(profile.id), { force: true });
|
|
278
294
|
this.retryAfter.delete(profile.id);
|
|
@@ -282,7 +298,6 @@ export class AccountService {
|
|
|
282
298
|
}
|
|
283
299
|
async beginLoginUnlocked(provider) {
|
|
284
300
|
const index = await this.readIndex();
|
|
285
|
-
await this.assertNoRunningPanes(provider, { allowTerminationAcknowledgement: true });
|
|
286
301
|
if (index.pending[provider])
|
|
287
302
|
throw new AccountServiceError('account_login_already_pending');
|
|
288
303
|
await this.captureActiveCredentials(index, provider);
|
|
@@ -319,9 +334,6 @@ export class AccountService {
|
|
|
319
334
|
}
|
|
320
335
|
delete index.pending[provider];
|
|
321
336
|
await this.writeIndex(index);
|
|
322
|
-
const restoredId = index.active[provider];
|
|
323
|
-
if (restoredId)
|
|
324
|
-
await rm(this.profilePath(restoredId), { force: true });
|
|
325
337
|
throw error;
|
|
326
338
|
}
|
|
327
339
|
}
|
|
@@ -330,15 +342,16 @@ export class AccountService {
|
|
|
330
342
|
const pending = index.pending[provider];
|
|
331
343
|
if (!pending)
|
|
332
344
|
throw new AccountServiceError('account_login_not_pending');
|
|
333
|
-
await this.assertNoRunningPanes(provider, { allowTerminationAcknowledgement: true });
|
|
334
345
|
const credentials = await this.readCredentialFile(provider);
|
|
335
346
|
if (!credentials)
|
|
336
347
|
throw new AccountServiceError('account_login_not_complete');
|
|
337
348
|
const profile = await this.storeProfile(index, provider, credentials, label);
|
|
349
|
+
// Back up the new active login before committing it as active.
|
|
350
|
+
await this.writeVaultCredentials(profile.id, credentials);
|
|
338
351
|
index.active[provider] = profile.id;
|
|
339
352
|
delete index.pending[provider];
|
|
353
|
+
clearExternalSwitch(index, provider);
|
|
340
354
|
await this.writeIndex(index);
|
|
341
|
-
await rm(this.profilePath(profile.id), { force: true });
|
|
342
355
|
this.clearTerminationAcknowledgement(provider);
|
|
343
356
|
return this.status();
|
|
344
357
|
}
|
|
@@ -347,7 +360,6 @@ export class AccountService {
|
|
|
347
360
|
const pending = index.pending[provider];
|
|
348
361
|
if (!pending)
|
|
349
362
|
return this.status();
|
|
350
|
-
await this.assertNoRunningPanes(provider, { allowTerminationAcknowledgement: true });
|
|
351
363
|
if (pending.previousProfileId) {
|
|
352
364
|
const credentials = await this.readVaultCredentials(pending.previousProfileId);
|
|
353
365
|
await this.writeCredentialFile(provider, credentials);
|
|
@@ -358,9 +370,6 @@ export class AccountService {
|
|
|
358
370
|
}
|
|
359
371
|
delete index.pending[provider];
|
|
360
372
|
await this.writeIndex(index);
|
|
361
|
-
const restoredId = index.active[provider];
|
|
362
|
-
if (restoredId)
|
|
363
|
-
await rm(this.profilePath(restoredId), { force: true });
|
|
364
373
|
this.clearTerminationAcknowledgement(provider);
|
|
365
374
|
return this.status();
|
|
366
375
|
}
|
|
@@ -487,6 +496,7 @@ export class AccountService {
|
|
|
487
496
|
processCheckUnavailable,
|
|
488
497
|
terminationAcknowledged: this.hasTerminationAcknowledgement(provider),
|
|
489
498
|
pendingLogin: pending ? { startedAt: pending.startedAt, ready: pendingReady, terminalId: pending.terminalId ?? null } : null,
|
|
499
|
+
lastExternalSwitch: index.externalSwitch?.[provider] ?? null,
|
|
490
500
|
credentialPresent: Boolean(profile || pending || false)
|
|
491
501
|
};
|
|
492
502
|
}
|
|
@@ -498,12 +508,10 @@ export class AccountService {
|
|
|
498
508
|
return provider ? [{ terminalId: terminal.id, provider, title: terminal.title, cwd: terminal.cwd }] : [];
|
|
499
509
|
});
|
|
500
510
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
}
|
|
511
|
+
// Running provider sessions are advisory for account changes: the dialog lists
|
|
512
|
+
// them, and they keep the account they started with until restarted. Only
|
|
513
|
+
// refreshing the ACTIVE account's credential stays guarded, because rotating
|
|
514
|
+
// its refresh token would invalidate the running CLI's copy.
|
|
507
515
|
async terminateProviderSessionsUnlocked(provider) {
|
|
508
516
|
const processes = await this.processLister(provider);
|
|
509
517
|
this.clearTerminationAcknowledgement(provider);
|
|
@@ -559,51 +567,102 @@ export class AccountService {
|
|
|
559
567
|
}
|
|
560
568
|
if (JSON.stringify(index) !== before)
|
|
561
569
|
await this.writeIndex(index);
|
|
562
|
-
// Only metadata is retained for active accounts. A duplicate can exist
|
|
563
|
-
// briefly during a switch, until the source file and index are committed.
|
|
564
|
-
for (const provider of providers) {
|
|
565
|
-
const id = index.active[provider];
|
|
566
|
-
if (id)
|
|
567
|
-
await rm(this.profilePath(id), { force: true });
|
|
568
|
-
}
|
|
569
570
|
}
|
|
571
|
+
// The live CLI file is the source of truth for the active account. Every
|
|
572
|
+
// newer live credential NTerminal observes is also copied into the active
|
|
573
|
+
// profile's encrypted vault backup, so an external overwrite (another login,
|
|
574
|
+
// or a stale CLI writing its old account back) never loses the account that
|
|
575
|
+
// was active: it stays in the vault and can be selected again.
|
|
570
576
|
async captureActiveCredential(index, provider) {
|
|
571
577
|
const credentials = await this.readCredentialFile(provider);
|
|
572
578
|
const previousId = index.active[provider];
|
|
573
579
|
if (!credentials) {
|
|
580
|
+
if (!previousId)
|
|
581
|
+
return;
|
|
574
582
|
delete index.active[provider];
|
|
575
|
-
await this.
|
|
583
|
+
await this.deactivateProfile(index, provider, previousId, null);
|
|
576
584
|
return;
|
|
577
585
|
}
|
|
578
586
|
const fingerprint = fingerprintFor(provider, credentials);
|
|
579
587
|
const credentialHash = hashCredential(credentials);
|
|
580
588
|
const existing = index.profiles.find((profile) => profile.provider === provider && (profile.fingerprint === fingerprint || profile.credentialHash === credentialHash));
|
|
589
|
+
if (existing && existing.id === previousId) {
|
|
590
|
+
if (existing.credentialHash !== credentialHash) {
|
|
591
|
+
// The active account's CLI rotated or re-wrote it: the live file wins.
|
|
592
|
+
await this.updateProfile(existing, provider, credentials);
|
|
593
|
+
await this.backupLiveCredential(existing, credentials, false);
|
|
594
|
+
}
|
|
595
|
+
else if (!await this.hasVaultCredentials(existing.id)) {
|
|
596
|
+
// Vaults written before active backups existed: create it once.
|
|
597
|
+
await this.writeVaultCredentials(existing.id, credentials);
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
// The live file now holds a different account than the index says.
|
|
602
|
+
let profile;
|
|
581
603
|
if (existing) {
|
|
582
604
|
if (existing.credentialHash !== credentialHash)
|
|
583
605
|
await this.updateProfile(existing, provider, credentials);
|
|
584
|
-
|
|
585
|
-
if (previousId !== existing.id)
|
|
586
|
-
await this.removeUnavailableProfile(index, previousId);
|
|
587
|
-
return;
|
|
606
|
+
profile = existing;
|
|
588
607
|
}
|
|
589
|
-
|
|
608
|
+
else {
|
|
609
|
+
profile = await this.storeProfile(index, provider, credentials);
|
|
610
|
+
}
|
|
611
|
+
// A stale CLI may write back tokens older than this profile's vault copy
|
|
612
|
+
// (renewed while it was inactive); keep the newer copy for a later Use.
|
|
613
|
+
await this.backupLiveCredential(profile, credentials, true);
|
|
590
614
|
index.active[provider] = profile.id;
|
|
591
|
-
if (previousId
|
|
592
|
-
await this.
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
615
|
+
if (previousId)
|
|
616
|
+
await this.deactivateProfile(index, provider, previousId, profile.id);
|
|
617
|
+
}
|
|
618
|
+
// Copies a live credential into the profile's vault backup. With
|
|
619
|
+
// keepNewerBackup, a backup that holds a strictly newer credential for the
|
|
620
|
+
// same account is kept instead. The live file itself is never changed here.
|
|
621
|
+
async backupLiveCredential(profile, live, keepNewerBackup) {
|
|
622
|
+
const backup = await this.readVaultCredentials(profile.id).catch(() => null);
|
|
623
|
+
if (backup && keepNewerBackup && stableIdentity(profile.provider, backup) === stableIdentity(profile.provider, live) && isOlderCredential(profile.provider, live, backup)) {
|
|
624
|
+
return backup;
|
|
625
|
+
}
|
|
626
|
+
if (!backup || hashCredential(backup) !== hashCredential(live))
|
|
627
|
+
await this.writeVaultCredentials(profile.id, live);
|
|
628
|
+
return live;
|
|
629
|
+
}
|
|
630
|
+
// A profile stops being active because the live file changed outside
|
|
631
|
+
// NTerminal. Its backup becomes its saved credential. Profiles without a
|
|
632
|
+
// backup (vaults from before active backups) cannot be restored and are
|
|
633
|
+
// dropped, as before.
|
|
634
|
+
async deactivateProfile(index, provider, id, toProfileId) {
|
|
635
|
+
const profile = index.profiles.find((entry) => entry.id === id);
|
|
636
|
+
if (!profile)
|
|
596
637
|
return;
|
|
638
|
+
let backup;
|
|
597
639
|
try {
|
|
598
|
-
await
|
|
640
|
+
backup = await this.readVaultCredentials(id);
|
|
599
641
|
}
|
|
600
642
|
catch (error) {
|
|
601
|
-
if (!
|
|
643
|
+
if (!(error instanceof AccountServiceError) || await this.hasVaultCredentials(id))
|
|
602
644
|
throw error;
|
|
603
|
-
index.profiles = index.profiles.filter((
|
|
645
|
+
index.profiles = index.profiles.filter((entry) => entry.id !== id);
|
|
604
646
|
this.retryAfter.delete(id);
|
|
647
|
+
return;
|
|
605
648
|
}
|
|
649
|
+
if (hashCredential(backup) !== profile.credentialHash)
|
|
650
|
+
await this.updateProfile(profile, provider, backup);
|
|
651
|
+
index.externalSwitch = { ...index.externalSwitch, [provider]: { at: this.now().toISOString(), fromProfileId: id, toProfileId } };
|
|
606
652
|
}
|
|
653
|
+
async hasVaultCredentials(id) {
|
|
654
|
+
try {
|
|
655
|
+
await stat(this.profilePath(id));
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
if (isNotFound(error))
|
|
660
|
+
return false;
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Before NTerminal itself replaces the live file (switch or new login), make
|
|
665
|
+
// sure the outgoing account's vault copy holds its newest credential.
|
|
607
666
|
async archiveActiveCredential(index, provider) {
|
|
608
667
|
const credentials = await this.readCredentialFile(provider);
|
|
609
668
|
const profile = index.profiles.find((entry) => entry.id === index.active[provider]);
|
|
@@ -611,7 +670,12 @@ export class AccountService {
|
|
|
611
670
|
return null;
|
|
612
671
|
if (!profile || profile.credentialHash !== hashCredential(credentials))
|
|
613
672
|
throw new AccountServiceError('account_credential_changed');
|
|
614
|
-
|
|
673
|
+
// The live hash matches the last synchronization, so the backup can only
|
|
674
|
+
// differ if it was kept as newer when this account became live again.
|
|
675
|
+
const kept = await this.backupLiveCredential(profile, credentials, true);
|
|
676
|
+
// Inactive profiles describe their vault copy.
|
|
677
|
+
if (kept !== credentials)
|
|
678
|
+
await this.updateProfile(profile, provider, kept);
|
|
615
679
|
return credentials;
|
|
616
680
|
}
|
|
617
681
|
// A blank label only names new profiles; re-authenticating an existing
|
|
@@ -665,7 +729,11 @@ export class AccountService {
|
|
|
665
729
|
}
|
|
666
730
|
// Idle describes a conversation, not the CLI's background auth work. Until
|
|
667
731
|
// we can delegate to its native auth owner, never compete with a live CLI.
|
|
668
|
-
|
|
732
|
+
// The same applies to an account an external write just displaced: CLIs
|
|
733
|
+
// started while it was active still hold its refresh token. The active
|
|
734
|
+
// account is always refreshed from the live file, never from its backup.
|
|
735
|
+
const displaced = index.externalSwitch?.[profile.provider]?.fromProfileId === profile.id;
|
|
736
|
+
if (active || displaced)
|
|
669
737
|
await this.assertNoRefreshOwner(profile.provider);
|
|
670
738
|
let refreshResponse;
|
|
671
739
|
try {
|
|
@@ -706,8 +774,11 @@ export class AccountService {
|
|
|
706
774
|
}
|
|
707
775
|
// The live credential is shared by every pane for this OS user. The
|
|
708
776
|
// compare above preserves an external CLI refresh or login that won the
|
|
709
|
-
// race; otherwise atomically publish the one rotated credential
|
|
777
|
+
// race; otherwise atomically publish the one rotated credential, then
|
|
778
|
+
// mirror it into the active backup. If the backup write fails, the next
|
|
779
|
+
// synchronization copies the newer live file.
|
|
710
780
|
await this.writeCredentialFile(profile.provider, next);
|
|
781
|
+
await this.writeVaultCredentials(profile.id, next);
|
|
711
782
|
}
|
|
712
783
|
else {
|
|
713
784
|
await this.writeVaultCredentials(profile.id, next);
|
|
@@ -735,7 +806,8 @@ export class AccountService {
|
|
|
735
806
|
version: 1,
|
|
736
807
|
profiles: Array.isArray(parsed.profiles) ? parsed.profiles.filter(isVaultProfile) : [],
|
|
737
808
|
active: isRecord(parsed.active) ? parsed.active : {},
|
|
738
|
-
pending: isRecord(parsed.pending) ? parsed.pending : {}
|
|
809
|
+
pending: isRecord(parsed.pending) ? parsed.pending : {},
|
|
810
|
+
...(isRecord(parsed.externalSwitch) ? { externalSwitch: parseExternalSwitches(parsed.externalSwitch) } : {})
|
|
739
811
|
};
|
|
740
812
|
}
|
|
741
813
|
catch (error) {
|
|
@@ -929,14 +1001,22 @@ async function listUserProcesses() {
|
|
|
929
1001
|
const getuid = process.getuid;
|
|
930
1002
|
if (!getuid)
|
|
931
1003
|
throw new Error('unsupported');
|
|
932
|
-
const { stdout } = await execFileAsync('ps', ['-u', String(getuid()), '-o', 'pid=,ppid=,comm=,args='], {
|
|
1004
|
+
const { stdout } = await execFileAsync('ps', ['-u', String(getuid()), '-o', 'pid=,ppid=,stat=,comm=,args='], {
|
|
933
1005
|
timeout: 3_000, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8'
|
|
934
1006
|
});
|
|
1007
|
+
return parseProcessSnapshot(stdout);
|
|
1008
|
+
}
|
|
1009
|
+
// Zombie (Z) and dead (X) entries have exited and cannot be signalled; only
|
|
1010
|
+
// their parent can reap them (e.g. an old Codex daemon under its updater).
|
|
1011
|
+
// They are not running providers.
|
|
1012
|
+
export function parseProcessSnapshot(stdout) {
|
|
935
1013
|
return stdout.split('\n').flatMap((line) => {
|
|
936
|
-
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
|
|
1014
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s*(.*)$/);
|
|
937
1015
|
if (!match)
|
|
938
1016
|
return [];
|
|
939
|
-
const [, pidText, ppidText, command, args] = match;
|
|
1017
|
+
const [, pidText, ppidText, stat, command, args] = match;
|
|
1018
|
+
if (/^[ZX]/.test(stat))
|
|
1019
|
+
return [];
|
|
940
1020
|
const pid = Number.parseInt(pidText, 10);
|
|
941
1021
|
const ppid = Number.parseInt(ppidText, 10);
|
|
942
1022
|
return Number.isSafeInteger(pid) && Number.isSafeInteger(ppid) ? [{ pid, ppid, command: command, args: args }] : [];
|
|
@@ -1005,6 +1085,24 @@ async function stopCodexDaemon(daemons) {
|
|
|
1005
1085
|
}
|
|
1006
1086
|
throw new AccountServiceError('codex_daemon_stop_failed');
|
|
1007
1087
|
}
|
|
1088
|
+
function daemonDescendants(entries) {
|
|
1089
|
+
const parents = new Map(entries.map((entry) => [entry.pid, entry.ppid]));
|
|
1090
|
+
const daemons = new Set(entries.filter(isCodexDaemon).map((entry) => entry.pid));
|
|
1091
|
+
const out = new Set();
|
|
1092
|
+
for (const entry of entries) {
|
|
1093
|
+
if (daemons.has(entry.pid))
|
|
1094
|
+
continue;
|
|
1095
|
+
const seen = new Set();
|
|
1096
|
+
for (let pid = entry.ppid; pid > 0 && !seen.has(pid); pid = parents.get(pid) ?? 0) {
|
|
1097
|
+
if (daemons.has(pid)) {
|
|
1098
|
+
out.add(entry.pid);
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
seen.add(pid);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1008
1106
|
function isCodexDaemon(entry) {
|
|
1009
1107
|
const args = entry.args.trim().split(/\s+/);
|
|
1010
1108
|
return path.basename(args[0] ?? '') === 'codex' && args[1] === 'app-server'
|
|
@@ -1044,6 +1142,31 @@ function signalProcess(pid, signal) {
|
|
|
1044
1142
|
function sleep(milliseconds) {
|
|
1045
1143
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1046
1144
|
}
|
|
1145
|
+
function clearExternalSwitch(index, provider) {
|
|
1146
|
+
if (!index.externalSwitch?.[provider])
|
|
1147
|
+
return false;
|
|
1148
|
+
const { [provider]: _cleared, ...rest } = index.externalSwitch;
|
|
1149
|
+
index.externalSwitch = rest;
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
function parseExternalSwitches(value) {
|
|
1153
|
+
const result = {};
|
|
1154
|
+
for (const provider of ['codex', 'claude']) {
|
|
1155
|
+
const entry = value[provider];
|
|
1156
|
+
if (isRecord(entry) && typeof entry.at === 'string' && typeof entry.fromProfileId === 'string' && (entry.toProfileId === null || typeof entry.toProfileId === 'string')) {
|
|
1157
|
+
result[provider] = { at: entry.at, fromProfileId: entry.fromProfileId, toProfileId: entry.toProfileId };
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return result;
|
|
1161
|
+
}
|
|
1162
|
+
// True when `candidate` is provably older than `reference`: both carry an
|
|
1163
|
+
// access-token expiry and the candidate's is earlier. Unknown timing is never
|
|
1164
|
+
// treated as older, so the live file wins by default.
|
|
1165
|
+
function isOlderCredential(provider, candidate, reference) {
|
|
1166
|
+
const a = Date.parse(credentialTiming(provider, candidate).expiresAt ?? '');
|
|
1167
|
+
const b = Date.parse(credentialTiming(provider, reference).expiresAt ?? '');
|
|
1168
|
+
return Number.isFinite(a) && Number.isFinite(b) && a < b;
|
|
1169
|
+
}
|
|
1047
1170
|
function toPublicProfile(profile) {
|
|
1048
1171
|
return { id: profile.id, provider: profile.provider, label: profile.label, createdAt: profile.createdAt, updatedAt: profile.updatedAt, expiresAt: profile.expiresAt, refreshTokenExpiresAt: profile.refreshTokenExpiresAt, ...(profile.refresh ? { refresh: profile.refresh } : {}) };
|
|
1049
1172
|
}
|