@ralphkrauss/codex-account-switcher 0.1.4 → 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/CHANGELOG.md +20 -0
- package/README.md +39 -18
- package/dist/cli.js +166 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/remote.d.ts +19 -0
- package/dist/remote.js +181 -12
- package/dist/remote.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +187 -12
- package/src/index.ts +6 -0
- package/src/remote.ts +222 -11
package/src/remote.ts
CHANGED
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
getCodexPaths,
|
|
18
18
|
listAccountNames,
|
|
19
19
|
resolveExecutable,
|
|
20
|
+
useAccount,
|
|
20
21
|
validateAccountName,
|
|
22
|
+
writebackCurrentAccount,
|
|
21
23
|
type CodexPaths,
|
|
22
24
|
} from './accounts.js';
|
|
23
25
|
|
|
@@ -112,6 +114,23 @@ export interface SyncStatus {
|
|
|
112
114
|
readonly accounts: readonly SyncStatusAccount[];
|
|
113
115
|
}
|
|
114
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
|
+
|
|
115
134
|
interface OpResult {
|
|
116
135
|
readonly exitCode: number;
|
|
117
136
|
readonly stdout: string;
|
|
@@ -414,6 +433,68 @@ function itemTitle(config: RemoteConfig, account: string): string {
|
|
|
414
433
|
return `${config.itemPrefix}${account}`;
|
|
415
434
|
}
|
|
416
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
|
+
|
|
417
498
|
function validateRemoteSyncAccountName(account: string): string {
|
|
418
499
|
const safeAccount = validateAccountName(account);
|
|
419
500
|
if (safeAccount === 'default') {
|
|
@@ -518,12 +599,64 @@ function decodeAuthJsonField(stdout: string, item: string): string {
|
|
|
518
599
|
}
|
|
519
600
|
}
|
|
520
601
|
|
|
602
|
+
function decodeAuthJsonFieldFromItemJson(stdout: string, item: string): string {
|
|
603
|
+
let parsed: unknown;
|
|
604
|
+
try {
|
|
605
|
+
parsed = JSON.parse(stdout) as unknown;
|
|
606
|
+
} catch (error) {
|
|
607
|
+
throw new CxError(`1Password item '${item}' JSON could not be parsed: ${errorMessage(error)}`, 1);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.fields)) {
|
|
611
|
+
throw new CxError(`1Password item '${item}' does not contain fields metadata`, 1);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const field = parsed.fields.find((candidate: unknown): candidate is Record<string, unknown> => (
|
|
615
|
+
isRecord(candidate) && candidate.label === ONEPASSWORD_AUTH_FIELD
|
|
616
|
+
));
|
|
617
|
+
if (!field) {
|
|
618
|
+
throw new CxError(`1Password item '${item}' does not contain a revealable '${ONEPASSWORD_AUTH_FIELD}' field`, 1);
|
|
619
|
+
}
|
|
620
|
+
if (typeof field.value !== 'string') {
|
|
621
|
+
throw new CxError(`1Password item '${item}' field '${ONEPASSWORD_AUTH_FIELD}' did not contain a string value`, 1);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
parseAuthJsonString(field.value, `auth_json field in 1Password item '${item}'`);
|
|
625
|
+
return field.value;
|
|
626
|
+
}
|
|
627
|
+
|
|
521
628
|
async function readOnePasswordAuthJson(
|
|
522
629
|
config: RemoteConfig,
|
|
523
630
|
item: string,
|
|
524
631
|
env: NodeJS.ProcessEnv,
|
|
525
632
|
): Promise<string> {
|
|
526
|
-
const
|
|
633
|
+
const jsonResult = await runOpRaw([
|
|
634
|
+
'item',
|
|
635
|
+
'get',
|
|
636
|
+
item,
|
|
637
|
+
'--vault',
|
|
638
|
+
config.vault,
|
|
639
|
+
'--format',
|
|
640
|
+
'json',
|
|
641
|
+
], env);
|
|
642
|
+
|
|
643
|
+
if (jsonResult.exitCode === 0) {
|
|
644
|
+
try {
|
|
645
|
+
return decodeAuthJsonFieldFromItemJson(jsonResult.stdout, item);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (!errorMessage(error).includes(`'${ONEPASSWORD_AUTH_FIELD}'`)) {
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
// Fall through to the field-specific command for older op/item shapes.
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
if (looksLikeMissingItem(jsonResult)) {
|
|
654
|
+
throw new CxError(`remote account item '${item}' was not found in 1Password vault '${config.vault}'`, 1);
|
|
655
|
+
}
|
|
656
|
+
throwOpFailure(`reading 1Password item '${item}'`, jsonResult);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const fieldResult = await runOpRaw([
|
|
527
660
|
'item',
|
|
528
661
|
'get',
|
|
529
662
|
item,
|
|
@@ -534,16 +667,16 @@ async function readOnePasswordAuthJson(
|
|
|
534
667
|
'--reveal',
|
|
535
668
|
], env);
|
|
536
669
|
|
|
537
|
-
if (
|
|
538
|
-
return decodeAuthJsonField(
|
|
670
|
+
if (fieldResult.exitCode === 0) {
|
|
671
|
+
return decodeAuthJsonField(fieldResult.stdout, item);
|
|
539
672
|
}
|
|
540
|
-
if (looksLikeMissingField(
|
|
673
|
+
if (looksLikeMissingField(fieldResult)) {
|
|
541
674
|
throw new CxError(`1Password item '${item}' does not contain a revealable '${ONEPASSWORD_AUTH_FIELD}' field`, 1);
|
|
542
675
|
}
|
|
543
|
-
if (looksLikeMissingItem(
|
|
676
|
+
if (looksLikeMissingItem(fieldResult)) {
|
|
544
677
|
throw new CxError(`remote account item '${item}' was not found in 1Password vault '${config.vault}'`, 1);
|
|
545
678
|
}
|
|
546
|
-
throwOpFailure(`reading '${ONEPASSWORD_AUTH_FIELD}' from 1Password item '${item}'`,
|
|
679
|
+
throwOpFailure(`reading '${ONEPASSWORD_AUTH_FIELD}' from 1Password item '${item}'`, fieldResult);
|
|
547
680
|
}
|
|
548
681
|
|
|
549
682
|
async function readLocalAccountAuthJson(
|
|
@@ -590,10 +723,13 @@ export async function syncPushAccount(
|
|
|
590
723
|
): Promise<SyncPushResult> {
|
|
591
724
|
const env = options.env ?? process.env;
|
|
592
725
|
const paths = remotePaths(options);
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
726
|
+
const config = await requireRemoteConfig({ paths });
|
|
727
|
+
const safeAccount = validateRemoteSyncAccountName(account);
|
|
728
|
+
const writeback = await writebackCurrentAccount({ paths });
|
|
729
|
+
if (writeback.performed === true && writeback.account !== safeAccount) {
|
|
730
|
+
throw new CxError(`unexpected writeback account '${writeback.account}' while syncing '${safeAccount}'`, 1);
|
|
731
|
+
}
|
|
732
|
+
const local = await readLocalAccountAuthJson(safeAccount, paths);
|
|
597
733
|
const item = itemTitle(config, local.account);
|
|
598
734
|
const operation = await upsertOnePasswordAuthJson(config, item, local.authJson, env);
|
|
599
735
|
|
|
@@ -629,6 +765,73 @@ export async function syncPullAccount(
|
|
|
629
765
|
};
|
|
630
766
|
}
|
|
631
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
|
+
|
|
632
835
|
export async function inspectSyncStatus(
|
|
633
836
|
account?: string,
|
|
634
837
|
options: RemoteCliOptions = {},
|
|
@@ -637,9 +840,17 @@ export async function inspectSyncStatus(
|
|
|
637
840
|
const paths = remotePaths(options);
|
|
638
841
|
const config = await readRemoteConfig({ paths });
|
|
639
842
|
const opPath = await resolveExecutable('op', env);
|
|
640
|
-
|
|
843
|
+
let accounts = account ? [validateRemoteSyncAccountName(account)] : await listRemoteSyncAccountNames(paths);
|
|
641
844
|
const statuses: SyncStatusAccount[] = [];
|
|
642
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
|
+
|
|
643
854
|
for (const accountName of accounts) {
|
|
644
855
|
const accountFile = accountPathForName(paths, accountName);
|
|
645
856
|
let presence: RemotePresence = 'unknown';
|