@ralphkrauss/codex-account-switcher 0.1.5 → 0.1.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/src/remote.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  getCodexPaths,
18
18
  listAccountNames,
19
19
  resolveExecutable,
20
+ useAccount,
20
21
  validateAccountName,
21
22
  writebackCurrentAccount,
22
23
  type CodexPaths,
@@ -113,6 +114,23 @@ export interface SyncStatus {
113
114
  readonly accounts: readonly SyncStatusAccount[];
114
115
  }
115
116
 
117
+ export interface SetupOnePasswordProfilesInput extends ConfigureOnePasswordRemoteInput {
118
+ readonly pull?: boolean;
119
+ readonly force?: boolean;
120
+ readonly use?: string;
121
+ }
122
+
123
+ export interface SetupOnePasswordProfilesResult {
124
+ readonly configPath: string;
125
+ readonly remoteConfigured: boolean;
126
+ readonly opAvailable: boolean;
127
+ readonly vault: string;
128
+ readonly itemPrefix: string;
129
+ readonly remoteAccounts: readonly string[];
130
+ readonly pulledAccounts: readonly string[];
131
+ readonly usedAccount: string | null;
132
+ }
133
+
116
134
  interface OpResult {
117
135
  readonly exitCode: number;
118
136
  readonly stdout: string;
@@ -415,6 +433,68 @@ function itemTitle(config: RemoteConfig, account: string): string {
415
433
  return `${config.itemPrefix}${account}`;
416
434
  }
417
435
 
436
+ function accountNameFromItemTitle(config: RemoteConfig, title: string): string | null {
437
+ if (!title.startsWith(config.itemPrefix)) {
438
+ return null;
439
+ }
440
+ const account = title.slice(config.itemPrefix.length);
441
+ try {
442
+ const safeAccount = validateAccountName(account);
443
+ return safeAccount === 'default' ? null : safeAccount;
444
+ } catch {
445
+ return null;
446
+ }
447
+ }
448
+
449
+ function sortedUnique(values: Iterable<string>): string[] {
450
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
451
+ }
452
+
453
+ function parseOnePasswordItemTitles(stdout: string, action: string): string[] {
454
+ let parsed: unknown;
455
+ try {
456
+ parsed = JSON.parse(stdout) as unknown;
457
+ } catch (error) {
458
+ throw new CxError(`1Password item list JSON could not be parsed while ${action}: ${errorMessage(error)}`, 1);
459
+ }
460
+ if (!Array.isArray(parsed)) {
461
+ throw new CxError(`1Password item list did not return an array while ${action}`, 1);
462
+ }
463
+ return parsed
464
+ .map((item) => (isRecord(item) && typeof item.title === 'string' ? item.title : null))
465
+ .filter((title): title is string => title !== null);
466
+ }
467
+
468
+ async function listOnePasswordAccountNames(
469
+ config: RemoteConfig,
470
+ env: NodeJS.ProcessEnv,
471
+ ): Promise<string[]> {
472
+ const result = await runOp([
473
+ 'item',
474
+ 'list',
475
+ '--vault',
476
+ config.vault,
477
+ '--format',
478
+ 'json',
479
+ ], env, `listing 1Password items in vault '${config.vault}'`);
480
+
481
+ return sortedUnique(
482
+ parseOnePasswordItemTitles(result.stdout, `listing 1Password items in vault '${config.vault}'`)
483
+ .map((title) => accountNameFromItemTitle(config, title))
484
+ .filter((account): account is string => account !== null),
485
+ );
486
+ }
487
+
488
+ async function verifyOnePasswordVault(config: RemoteConfig, env: NodeJS.ProcessEnv): Promise<void> {
489
+ await runOp([
490
+ 'vault',
491
+ 'get',
492
+ config.vault,
493
+ '--format',
494
+ 'json',
495
+ ], env, `checking 1Password vault '${config.vault}'`);
496
+ }
497
+
418
498
  function validateRemoteSyncAccountName(account: string): string {
419
499
  const safeAccount = validateAccountName(account);
420
500
  if (safeAccount === 'default') {
@@ -685,6 +765,73 @@ export async function syncPullAccount(
685
765
  };
686
766
  }
687
767
 
768
+ export async function listRemoteAccountNames(options: RemoteCliOptions = {}): Promise<string[]> {
769
+ const env = options.env ?? process.env;
770
+ const paths = remotePaths(options);
771
+ const config = await requireRemoteConfig({ paths });
772
+ return await listOnePasswordAccountNames(config, env);
773
+ }
774
+
775
+ export async function syncPullAllAccounts(options: RemoteForceOptions = {}): Promise<SyncPullResult[]> {
776
+ const paths = remotePaths(options);
777
+ const accounts = await listRemoteAccountNames(options);
778
+ const results: SyncPullResult[] = [];
779
+ for (const account of accounts) {
780
+ const accountFile = accountPathForName(paths, account);
781
+ if (options.force !== true && await pathExists(accountFile)) {
782
+ continue;
783
+ }
784
+ results.push(await syncPullAccount(account, options));
785
+ }
786
+ return results;
787
+ }
788
+
789
+ export async function syncPushAllAccounts(options: RemoteCliOptions = {}): Promise<SyncPushResult[]> {
790
+ const paths = remotePaths(options);
791
+ const accounts = await listRemoteSyncAccountNames(paths);
792
+ const results: SyncPushResult[] = [];
793
+ for (const account of accounts) {
794
+ results.push(await syncPushAccount(account, options));
795
+ }
796
+ return results;
797
+ }
798
+
799
+ export async function setupOnePasswordProfiles(
800
+ input: SetupOnePasswordProfilesInput,
801
+ options: RemoteCliOptions = {},
802
+ ): Promise<SetupOnePasswordProfilesResult> {
803
+ const env = options.env ?? process.env;
804
+ const paths = remotePaths(options);
805
+ const configured = await configureOnePasswordRemote(input, { paths });
806
+ await verifyOnePasswordVault(configured.config, env);
807
+ const remoteAccounts = await listOnePasswordAccountNames(configured.config, env);
808
+ const pulled = input.pull === true
809
+ ? await syncPullAllAccounts({ paths, env, force: input.force })
810
+ : [];
811
+ let usedAccount: string | null = null;
812
+
813
+ if (input.use) {
814
+ const account = validateRemoteSyncAccountName(input.use);
815
+ const accountFile = accountPathForName(paths, account);
816
+ if (!await pathExists(accountFile)) {
817
+ await syncPullAccount(account, { paths, env, force: input.force });
818
+ }
819
+ await useAccount(account, { paths });
820
+ usedAccount = account;
821
+ }
822
+
823
+ return {
824
+ configPath: configured.configPath,
825
+ remoteConfigured: true,
826
+ opAvailable: true,
827
+ vault: configured.config.vault,
828
+ itemPrefix: configured.config.itemPrefix,
829
+ remoteAccounts,
830
+ pulledAccounts: pulled.map((entry) => entry.account),
831
+ usedAccount,
832
+ };
833
+ }
834
+
688
835
  export async function inspectSyncStatus(
689
836
  account?: string,
690
837
  options: RemoteCliOptions = {},
@@ -693,9 +840,17 @@ export async function inspectSyncStatus(
693
840
  const paths = remotePaths(options);
694
841
  const config = await readRemoteConfig({ paths });
695
842
  const opPath = await resolveExecutable('op', env);
696
- const accounts = account ? [validateRemoteSyncAccountName(account)] : await listRemoteSyncAccountNames(paths);
843
+ let accounts = account ? [validateRemoteSyncAccountName(account)] : await listRemoteSyncAccountNames(paths);
697
844
  const statuses: SyncStatusAccount[] = [];
698
845
 
846
+ if (!account && config && opPath) {
847
+ try {
848
+ accounts = sortedUnique([...accounts, ...await listOnePasswordAccountNames(config, env)]);
849
+ } catch {
850
+ // Keep local status useful; per-account remote errors are reported below.
851
+ }
852
+ }
853
+
699
854
  for (const accountName of accounts) {
700
855
  const accountFile = accountPathForName(paths, accountName);
701
856
  let presence: RemotePresence = 'unknown';