@ralphkrauss/codex-account-switcher 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/accounts.ts CHANGED
@@ -34,6 +34,7 @@ export interface CodexPaths {
34
34
 
35
35
  export interface OperationOptions {
36
36
  readonly paths?: CodexPaths;
37
+ readonly skipWriteback?: boolean;
37
38
  }
38
39
 
39
40
  export interface ForceOptions extends OperationOptions {
@@ -354,7 +355,15 @@ export async function writebackCurrentAccount(options: OperationOptions = {}): P
354
355
  return { performed: false, reason: 'live auth.json is missing or too small', account: current.name };
355
356
  }
356
357
 
357
- await copyFilePrivate(paths.authFile, slot);
358
+ let liveAuthJson: string;
359
+ try {
360
+ liveAuthJson = await readFile(paths.authFile, 'utf8');
361
+ JSON.parse(liveAuthJson) as unknown;
362
+ } catch {
363
+ return { performed: false, reason: 'live auth.json is not valid JSON', account: current.name };
364
+ }
365
+
366
+ await writeFilePrivate(slot, liveAuthJson);
358
367
  return { performed: true, account: current.name };
359
368
  }
360
369
 
@@ -384,7 +393,9 @@ export async function useAccount(name: string, options: OperationOptions = {}):
384
393
  throw new CxError(`no account '${safeName}'`, 1);
385
394
  }
386
395
 
387
- const writeback = await writebackCurrentAccount({ paths });
396
+ const writeback = options.skipWriteback === true
397
+ ? { performed: false, reason: 'writeback skipped by caller' }
398
+ : await writebackCurrentAccount({ paths });
388
399
  await copyFilePrivate(source, paths.authFile);
389
400
  await writeCurrentMarker(paths, safeName);
390
401
  return writeback;
package/src/cli.ts CHANGED
@@ -5,6 +5,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import {
6
6
  CxError,
7
7
  authFileExists,
8
+ autoPullAccountForUse,
9
+ autoPushAccountIfChanged,
8
10
  configureOnePasswordRemote,
9
11
  getCodexPaths,
10
12
  inspectHermesStatus,
@@ -13,6 +15,7 @@ import {
13
15
  inspectSyncStatus,
14
16
  listAccounts,
15
17
  loginAccount,
18
+ readCurrentMarker,
16
19
  removeAccount,
17
20
  renameAccount,
18
21
  runCodex,
@@ -26,6 +29,8 @@ import {
26
29
  useAccount,
27
30
  useHermesAccount,
28
31
  validateAccountName,
32
+ writebackCurrentAccount,
33
+ writebackAndAutoPushCurrentAccount,
29
34
  type AccountList,
30
35
  type DoctorReport,
31
36
  type HermesStatus,
@@ -547,7 +552,10 @@ function formatSyncStatus(status: SyncStatus): string {
547
552
 
548
553
  lines.push('accounts:');
549
554
  for (const account of status.accounts) {
550
- lines.push(` - ${account.account}: local=${account.local.exists ? 'present' : 'missing'}, remote=${remotePresenceText(account)}`);
555
+ lines.push(` - ${account.account}: local=${account.local.exists ? 'present' : 'missing'}, remote=${remotePresenceText(account)}, sync=${account.sync.state}`);
556
+ if (account.sync.error) {
557
+ lines.push(` sync error: ${account.sync.error}`);
558
+ }
551
559
  lines.push(` file: ${account.local.file}`);
552
560
  lines.push(` item: ${account.item ?? '(unknown)'}`);
553
561
  }
@@ -558,23 +566,59 @@ async function printList(io: CliIo, env: NodeJS.ProcessEnv): Promise<void> {
558
566
  write(io.stdout, formatAccounts(await listAccounts(getCodexPaths(env))));
559
567
  }
560
568
 
561
- async function useAccountWithRemoteFallback(name: string, env: NodeJS.ProcessEnv, io: CliIo): Promise<void> {
569
+ async function maybeAutoPushCurrent(env: NodeJS.ProcessEnv, io: CliIo): Promise<void> {
570
+ const result = await writebackAndAutoPushCurrentAccount({ env, paths: getCodexPaths(env) });
571
+ if (result?.action === 'pushed') {
572
+ write(io.stdout, `auto-pushed profile '${result.account}'`);
573
+ }
574
+ }
575
+
576
+ async function maybeAutoPullCurrent(env: NodeJS.ProcessEnv, io: CliIo): Promise<void> {
562
577
  const paths = getCodexPaths(env);
563
- try {
564
- await useAccount(name, { paths });
578
+ const current = await readCurrentMarker(paths);
579
+ if (current.state !== 'valid') {
565
580
  return;
566
- } catch (error) {
567
- if (!(error instanceof CxError) || !error.message.includes(`no account '${name}'`)) {
568
- throw error;
569
- }
581
+ }
582
+ await writebackCurrentAccount({ paths });
583
+ const pull = await autoPullAccountForUse(current.name, { env, paths });
584
+ if (pull.action === 'pulled') {
585
+ await useAccount(current.name, { paths, skipWriteback: true });
586
+ write(io.stdout, `auto-pulled ${displayBackendName(pull.backend)}-backed profile '${pull.account}'`);
587
+ }
588
+ }
570
589
 
571
- try {
572
- const pulled = await syncPullAccount(name, { env, paths });
573
- write(io.stdout, `pulled 1Password-backed profile '${pulled.account}'`);
574
- await useAccount(name, { paths });
575
- } catch {
576
- throw error;
577
- }
590
+ async function runCodexAndAutoPush(args: readonly string[], env: NodeJS.ProcessEnv, io: CliIo): Promise<number> {
591
+ await maybeAutoPullCurrent(env, io);
592
+ const exitCode = await runCodex(args, { env });
593
+ await maybeAutoPushCurrent(env, io);
594
+ return exitCode;
595
+ }
596
+
597
+ async function autoPushNamed(name: string, env: NodeJS.ProcessEnv, io: CliIo): Promise<void> {
598
+ const result = await autoPushAccountIfChanged(name, { env, paths: getCodexPaths(env) });
599
+ if (result.action === 'pushed') {
600
+ write(io.stdout, `auto-pushed profile '${result.account}'`);
601
+ }
602
+ }
603
+
604
+ function displayBackendName(backend: string | undefined): string {
605
+ return backend === '1password' ? '1Password' : (backend ?? 'remote');
606
+ }
607
+
608
+ async function useAccountWithRemoteFallback(name: string, env: NodeJS.ProcessEnv, io: CliIo): Promise<void> {
609
+ const paths = getCodexPaths(env);
610
+ const current = await readCurrentMarker(paths);
611
+ const wasActive = current.state === 'valid' && current.name === name;
612
+ if (wasActive) {
613
+ await writebackCurrentAccount({ paths });
614
+ }
615
+ const pull = await autoPullAccountForUse(name, { env, paths });
616
+ if (pull.action === 'pulled') {
617
+ write(io.stdout, `auto-pulled ${displayBackendName(pull.backend)}-backed profile '${pull.account}'`);
618
+ }
619
+ const writeback = await useAccount(name, { paths, skipWriteback: wasActive && pull.action === 'pulled' });
620
+ if (writeback.performed && writeback.account) {
621
+ await autoPushNamed(writeback.account, env, io);
578
622
  }
579
623
  }
580
624
 
@@ -836,7 +880,7 @@ export async function main(
836
880
  if (!first) {
837
881
  const paths = getCodexPaths(env);
838
882
  if (await authFileExists(paths)) {
839
- return await runCodex([], { env });
883
+ return await runCodexAndAutoPush([], env, io);
840
884
  }
841
885
 
842
886
  write(io.stdout, helpText(metadata));
@@ -856,7 +900,7 @@ export async function main(
856
900
  validateAccountName(first);
857
901
  await useAccountWithRemoteFallback(first, env, io);
858
902
  write(io.stdout, `→ codex on '${first}'`);
859
- return await runCodex(rest, { env });
903
+ return await runCodexAndAutoPush(rest, env, io);
860
904
  }
861
905
 
862
906
  switch (first) {
@@ -874,6 +918,7 @@ export async function main(
874
918
  const name = parsed.positionals[0] ?? '';
875
919
  await saveAccount(name, { force: parsed.force, paths: getCodexPaths(env) });
876
920
  write(io.stdout, `saved current login as '${name}'`);
921
+ await autoPushNamed(name, env, io);
877
922
  return 0;
878
923
  }
879
924
 
@@ -887,13 +932,17 @@ export async function main(
887
932
 
888
933
  case 'login': {
889
934
  const parsed = parseLoginArgs(rest);
890
- await loginAccount(parsed.name, {
935
+ const writeback = await loginAccount(parsed.name, {
891
936
  force: parsed.force,
892
937
  loginArgs: parsed.loginArgs,
893
938
  env,
894
939
  paths: getCodexPaths(env),
895
940
  });
896
941
  write(io.stdout, `logged in and saved as '${parsed.name}'`);
942
+ if (writeback.performed && writeback.account) {
943
+ await autoPushNamed(writeback.account, env, io);
944
+ }
945
+ await autoPushNamed(parsed.name, env, io);
897
946
  return 0;
898
947
  }
899
948
 
@@ -924,7 +973,7 @@ export async function main(
924
973
  await useAccountWithRemoteFallback(parsed.account, env, io);
925
974
  write(io.stdout, `→ codex on '${parsed.account}'`);
926
975
  }
927
- return await runCodex(parsed.codexArgs, { env });
976
+ return await runCodexAndAutoPush(parsed.codexArgs, env, io);
928
977
  }
929
978
 
930
979
  case 'doctor': {
package/src/index.ts CHANGED
@@ -37,6 +37,9 @@ export {
37
37
  ONEPASSWORD_AUTH_FIELD,
38
38
  ONEPASSWORD_BACKEND,
39
39
  REMOTE_CONFIG_VERSION,
40
+ REMOTE_METADATA_FIELD,
41
+ autoPullAccountForUse,
42
+ autoPushAccountIfChanged,
40
43
  configureOnePasswordRemote,
41
44
  getRemoteConfigPath,
42
45
  inspectRemoteStatus,
@@ -48,6 +51,7 @@ export {
48
51
  syncPullAccount,
49
52
  syncPushAllAccounts,
50
53
  syncPushAccount,
54
+ writebackAndAutoPushCurrentAccount,
51
55
  } from './remote.js';
52
56
 
53
57
  export type {
@@ -88,6 +92,7 @@ export type {
88
92
  SetupOnePasswordProfilesResult,
89
93
  SyncPullResult,
90
94
  SyncPushResult,
95
+ SyncState,
91
96
  SyncStatus,
92
97
  SyncStatusAccount,
93
98
  } from './remote.js';