@ralphkrauss/codex-account-switcher 0.1.1 → 0.1.2
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 +15 -0
- package/README.md +79 -1
- package/dist/cli.js +342 -1
- package/dist/cli.js.map +1 -1
- package/dist/hermes.d.ts +56 -0
- package/dist/hermes.js +638 -0
- package/dist/hermes.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/remote.d.ts +85 -0
- package/dist/remote.js +468 -0
- package/dist/remote.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +421 -0
- package/src/hermes.ts +794 -0
- package/src/index.ts +49 -0
- package/src/remote.ts +670 -0
package/src/cli.ts
CHANGED
|
@@ -5,30 +5,44 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
5
5
|
import {
|
|
6
6
|
CxError,
|
|
7
7
|
authFileExists,
|
|
8
|
+
configureOnePasswordRemote,
|
|
8
9
|
getCodexPaths,
|
|
10
|
+
inspectHermesStatus,
|
|
9
11
|
inspectDoctor,
|
|
12
|
+
inspectRemoteStatus,
|
|
13
|
+
inspectSyncStatus,
|
|
10
14
|
listAccounts,
|
|
11
15
|
loginAccount,
|
|
12
16
|
removeAccount,
|
|
13
17
|
renameAccount,
|
|
14
18
|
runCodex,
|
|
15
19
|
saveAccount,
|
|
20
|
+
syncHermesAccount,
|
|
21
|
+
syncPullAccount,
|
|
22
|
+
syncPushAccount,
|
|
16
23
|
useAccount,
|
|
24
|
+
useHermesAccount,
|
|
17
25
|
validateAccountName,
|
|
18
26
|
type AccountList,
|
|
19
27
|
type DoctorReport,
|
|
28
|
+
type HermesStatus,
|
|
29
|
+
type RemoteStatus,
|
|
30
|
+
type SyncStatus,
|
|
20
31
|
} from './index.js';
|
|
21
32
|
|
|
22
33
|
const PACKAGE_NAME = '@ralphkrauss/codex-account-switcher';
|
|
23
34
|
const SUBCOMMANDS = new Set([
|
|
24
35
|
'doctor',
|
|
36
|
+
'hermes',
|
|
25
37
|
'help',
|
|
26
38
|
'login',
|
|
27
39
|
'ls',
|
|
28
40
|
'rename',
|
|
41
|
+
'remote',
|
|
29
42
|
'rm',
|
|
30
43
|
'run',
|
|
31
44
|
'save',
|
|
45
|
+
'sync',
|
|
32
46
|
'use',
|
|
33
47
|
]);
|
|
34
48
|
|
|
@@ -48,6 +62,23 @@ interface ParsedLoginArgs {
|
|
|
48
62
|
readonly loginArgs: readonly string[];
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
interface ParsedHermesArgs {
|
|
66
|
+
readonly json: boolean;
|
|
67
|
+
readonly noConfig: boolean;
|
|
68
|
+
readonly profile?: string;
|
|
69
|
+
readonly positionals: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ParsedJsonArgs {
|
|
73
|
+
readonly json: boolean;
|
|
74
|
+
readonly positionals: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ParsedRemoteConfigureArgs {
|
|
78
|
+
readonly vault: string;
|
|
79
|
+
readonly itemPrefix?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
function write(stream: NodeJS.WritableStream, text: string): void {
|
|
52
83
|
stream.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
53
84
|
}
|
|
@@ -78,6 +109,14 @@ Usage:
|
|
|
78
109
|
cx rename <old> <new> [--force]
|
|
79
110
|
cx rm <name>
|
|
80
111
|
cx run [name] -- [codex args...]
|
|
112
|
+
cx hermes use <account> [--profile <name>] [--no-config]
|
|
113
|
+
cx hermes sync <account> [--profile <name>]
|
|
114
|
+
cx hermes status [--profile <name>] [--json]
|
|
115
|
+
cx remote configure 1password --vault <vault> [--item-prefix <prefix>]
|
|
116
|
+
cx remote status [--json]
|
|
117
|
+
cx sync push <account>
|
|
118
|
+
cx sync pull <account> [--force]
|
|
119
|
+
cx sync status [account] [--json]
|
|
81
120
|
cx doctor [--json]
|
|
82
121
|
cx --help
|
|
83
122
|
cx --version
|
|
@@ -90,10 +129,154 @@ Data layout:
|
|
|
90
129
|
Uses CODEX_HOME when set, otherwise ~/.codex.
|
|
91
130
|
Accounts are stored as CODEX_HOME/accounts/<name>.json.
|
|
92
131
|
The active account marker is CODEX_HOME/.current-account.
|
|
132
|
+
Remote sync config is stored as CODEX_HOME/remote.json.
|
|
93
133
|
|
|
94
134
|
Account names may contain letters, numbers, dot, underscore, and dash only.`;
|
|
95
135
|
}
|
|
96
136
|
|
|
137
|
+
function hermesHelpText(): string {
|
|
138
|
+
return `Usage:
|
|
139
|
+
cx hermes use <account> [--profile <name>] [--no-config]
|
|
140
|
+
cx hermes sync <account> [--profile <name>]
|
|
141
|
+
cx hermes status [--profile <name>] [--json]
|
|
142
|
+
|
|
143
|
+
Commands:
|
|
144
|
+
use Import CODEX_HOME/accounts/<account>.json into Hermes openai-codex auth.
|
|
145
|
+
Also sets model.provider=openai-codex unless --no-config is passed.
|
|
146
|
+
sync Copy Hermes openai-codex tokens back to the cx account slot.
|
|
147
|
+
status Show the selected Hermes home, auth/config state, and linked cx account.
|
|
148
|
+
|
|
149
|
+
Paths:
|
|
150
|
+
Default Hermes home uses HERMES_HOME when set, otherwise ~/.hermes.
|
|
151
|
+
--profile <name> explicitly targets ~/.hermes/profiles/<name>.`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function remoteHelpText(): string {
|
|
155
|
+
return `Usage:
|
|
156
|
+
cx remote configure 1password --vault <vault> [--item-prefix <prefix>]
|
|
157
|
+
cx remote status [--json]
|
|
158
|
+
|
|
159
|
+
Commands:
|
|
160
|
+
configure Store remote sync settings in CODEX_HOME/remote.json.
|
|
161
|
+
status Show configured backend/vault/prefix and whether the op CLI is available.
|
|
162
|
+
|
|
163
|
+
Notes:
|
|
164
|
+
The default 1Password item prefix is cx-. Token contents are never printed.`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function syncHelpText(): string {
|
|
168
|
+
return `Usage:
|
|
169
|
+
cx sync push <account>
|
|
170
|
+
cx sync pull <account> [--force]
|
|
171
|
+
cx sync status [account] [--json]
|
|
172
|
+
|
|
173
|
+
Commands:
|
|
174
|
+
push Upsert CODEX_HOME/accounts/<account>.json into the configured 1Password item.
|
|
175
|
+
pull Read the configured 1Password item into CODEX_HOME/accounts/<account>.json.
|
|
176
|
+
Refuses to overwrite unless --force is passed.
|
|
177
|
+
status Compare local account-file presence with remote item presence without printing tokens.`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseHermesArgs(
|
|
181
|
+
command: string,
|
|
182
|
+
args: readonly string[],
|
|
183
|
+
allowed: { readonly json?: boolean; readonly noConfig?: boolean },
|
|
184
|
+
): ParsedHermesArgs {
|
|
185
|
+
let json = false;
|
|
186
|
+
let noConfig = false;
|
|
187
|
+
let profile: string | undefined;
|
|
188
|
+
const positionals: string[] = [];
|
|
189
|
+
|
|
190
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
191
|
+
const arg = args[index] ?? '';
|
|
192
|
+
if (arg === '--profile') {
|
|
193
|
+
const value = args[index + 1];
|
|
194
|
+
if (!value || value.startsWith('-')) {
|
|
195
|
+
throw new CxError(`usage: cx hermes ${command}`, 2);
|
|
196
|
+
}
|
|
197
|
+
profile = value;
|
|
198
|
+
index += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (arg === '--json') {
|
|
202
|
+
if (allowed.json !== true) {
|
|
203
|
+
throw new CxError(`unknown option '${arg}'`, 2);
|
|
204
|
+
}
|
|
205
|
+
json = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (arg === '--no-config') {
|
|
209
|
+
if (allowed.noConfig !== true) {
|
|
210
|
+
throw new CxError(`unknown option '${arg}'`, 2);
|
|
211
|
+
}
|
|
212
|
+
noConfig = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (arg.startsWith('--')) {
|
|
216
|
+
throw new CxError(`unknown option '${arg}'`, 2);
|
|
217
|
+
}
|
|
218
|
+
positionals.push(arg);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { json, noConfig, ...(profile ? { profile } : {}), positionals };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseJsonArgs(args: readonly string[]): ParsedJsonArgs {
|
|
225
|
+
let json = false;
|
|
226
|
+
const positionals: string[] = [];
|
|
227
|
+
for (const arg of args) {
|
|
228
|
+
if (arg === '--json') {
|
|
229
|
+
json = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (arg.startsWith('--')) {
|
|
233
|
+
throw new CxError(`unknown option '${arg}'`, 2);
|
|
234
|
+
}
|
|
235
|
+
positionals.push(arg);
|
|
236
|
+
}
|
|
237
|
+
return { json, positionals };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseRemoteConfigureArgs(args: readonly string[]): ParsedRemoteConfigureArgs {
|
|
241
|
+
const [backend, ...rest] = args;
|
|
242
|
+
if (backend !== '1password') {
|
|
243
|
+
throw new CxError('usage: cx remote configure 1password --vault <vault> [--item-prefix <prefix>]', 2);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let vault: string | undefined;
|
|
247
|
+
let itemPrefix: string | undefined;
|
|
248
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
249
|
+
const arg = rest[index] ?? '';
|
|
250
|
+
if (arg === '--vault') {
|
|
251
|
+
const value = rest[index + 1];
|
|
252
|
+
if (!value || value.startsWith('-')) {
|
|
253
|
+
throw new CxError('usage: cx remote configure 1password --vault <vault> [--item-prefix <prefix>]', 2);
|
|
254
|
+
}
|
|
255
|
+
vault = value;
|
|
256
|
+
index += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (arg === '--item-prefix') {
|
|
260
|
+
const value = rest[index + 1];
|
|
261
|
+
if (!value || value.startsWith('-')) {
|
|
262
|
+
throw new CxError('usage: cx remote configure 1password --vault <vault> [--item-prefix <prefix>]', 2);
|
|
263
|
+
}
|
|
264
|
+
itemPrefix = value;
|
|
265
|
+
index += 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (arg.startsWith('--')) {
|
|
269
|
+
throw new CxError(`unknown option '${arg}'`, 2);
|
|
270
|
+
}
|
|
271
|
+
throw new CxError('usage: cx remote configure 1password --vault <vault> [--item-prefix <prefix>]', 2);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!vault) {
|
|
275
|
+
throw new CxError('usage: cx remote configure 1password --vault <vault> [--item-prefix <prefix>]', 2);
|
|
276
|
+
}
|
|
277
|
+
return { vault, ...(itemPrefix ? { itemPrefix } : {}) };
|
|
278
|
+
}
|
|
279
|
+
|
|
97
280
|
function parseLoginArgs(args: readonly string[]): ParsedLoginArgs {
|
|
98
281
|
let force = false;
|
|
99
282
|
let name: string | null = null;
|
|
@@ -204,6 +387,88 @@ function formatDoctor(report: DoctorReport): string {
|
|
|
204
387
|
return lines.join('\n');
|
|
205
388
|
}
|
|
206
389
|
|
|
390
|
+
function yesNo(value: boolean): string {
|
|
391
|
+
return value ? 'yes' : 'no';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function formatHermesStatus(status: HermesStatus): string {
|
|
395
|
+
const tokenBits = status.hasTokens
|
|
396
|
+
? `access=${yesNo(status.hasAccessToken)}, refresh=${yesNo(status.hasRefreshToken)}`
|
|
397
|
+
: 'missing';
|
|
398
|
+
const linked = status.linkedAccounts.length === 0
|
|
399
|
+
? '(none detected)'
|
|
400
|
+
: status.linkedAccounts.join(', ');
|
|
401
|
+
const lines = [
|
|
402
|
+
'Hermes Codex integration status',
|
|
403
|
+
`profile: ${status.profile ?? 'default'}`,
|
|
404
|
+
`hermes home: ${status.hermesHome}`,
|
|
405
|
+
`auth.json: ${status.authExists ? 'present' : 'missing'}`,
|
|
406
|
+
`auth readable: ${yesNo(status.authReadable)}`,
|
|
407
|
+
`openai-codex auth: ${status.openaiCodexAuthExists ? 'present' : 'missing'}`,
|
|
408
|
+
`tokens: ${tokenBits}`,
|
|
409
|
+
`last refresh: ${status.lastRefresh ?? '(unknown)'}`,
|
|
410
|
+
`auth mode: ${status.authMode ?? '(unknown)'}`,
|
|
411
|
+
`credential pool openai-codex entries: ${status.poolEntryCount}`,
|
|
412
|
+
`linked cx account: ${linked}`,
|
|
413
|
+
`configured provider: ${status.configuredProvider ?? '(not set)'}`,
|
|
414
|
+
`config.yaml: ${status.configExists ? 'present' : 'missing'}`,
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
if (status.authError) {
|
|
418
|
+
lines.push(`auth warning: ${status.authError}`);
|
|
419
|
+
}
|
|
420
|
+
if (status.configError) {
|
|
421
|
+
lines.push(`config warning: ${status.configError}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return lines.join('\n');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function formatRemoteStatus(status: RemoteStatus): string {
|
|
428
|
+
const lines = [
|
|
429
|
+
'Remote sync status',
|
|
430
|
+
`config: ${status.configPath}`,
|
|
431
|
+
`configured: ${yesNo(status.configured)}`,
|
|
432
|
+
`backend: ${status.backend ?? '(not configured)'}`,
|
|
433
|
+
`vault: ${status.vault ?? '(not configured)'}`,
|
|
434
|
+
`item prefix: ${status.itemPrefix ?? '(not configured)'}`,
|
|
435
|
+
`op CLI: ${status.opAvailable ? `available (${status.opPath ?? 'op'})` : 'not found'}`,
|
|
436
|
+
];
|
|
437
|
+
return lines.join('\n');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function remotePresenceText(status: SyncStatus['accounts'][number]): string {
|
|
441
|
+
if (status.remote.presence === 'unknown') {
|
|
442
|
+
return status.remote.error ? `unknown (${status.remote.error})` : 'unknown';
|
|
443
|
+
}
|
|
444
|
+
return status.remote.presence;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function formatSyncStatus(status: SyncStatus): string {
|
|
448
|
+
const lines = [
|
|
449
|
+
'Remote sync status',
|
|
450
|
+
`config: ${status.configPath}`,
|
|
451
|
+
`configured: ${yesNo(status.configured)}`,
|
|
452
|
+
`backend: ${status.backend ?? '(not configured)'}`,
|
|
453
|
+
`vault: ${status.vault ?? '(not configured)'}`,
|
|
454
|
+
`item prefix: ${status.itemPrefix ?? '(not configured)'}`,
|
|
455
|
+
`op CLI: ${status.opAvailable ? `available (${status.opPath ?? 'op'})` : 'not found'}`,
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
if (status.accounts.length === 0) {
|
|
459
|
+
lines.push('accounts: (none)');
|
|
460
|
+
return lines.join('\n');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
lines.push('accounts:');
|
|
464
|
+
for (const account of status.accounts) {
|
|
465
|
+
lines.push(` - ${account.account}: local=${account.local.exists ? 'present' : 'missing'}, remote=${remotePresenceText(account)}`);
|
|
466
|
+
lines.push(` file: ${account.local.file}`);
|
|
467
|
+
lines.push(` item: ${account.item ?? '(unknown)'}`);
|
|
468
|
+
}
|
|
469
|
+
return lines.join('\n');
|
|
470
|
+
}
|
|
471
|
+
|
|
207
472
|
async function printList(io: CliIo, env: NodeJS.ProcessEnv): Promise<void> {
|
|
208
473
|
write(io.stdout, formatAccounts(await listAccounts(getCodexPaths(env))));
|
|
209
474
|
}
|
|
@@ -231,6 +496,153 @@ function parseRunArgs(args: readonly string[]): { account: string | null; codexA
|
|
|
231
496
|
return { account, codexArgs };
|
|
232
497
|
}
|
|
233
498
|
|
|
499
|
+
async function handleHermesCommand(
|
|
500
|
+
args: readonly string[],
|
|
501
|
+
env: NodeJS.ProcessEnv,
|
|
502
|
+
io: CliIo,
|
|
503
|
+
): Promise<number> {
|
|
504
|
+
const [command, ...rest] = args;
|
|
505
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
506
|
+
write(io.stdout, hermesHelpText());
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
switch (command) {
|
|
511
|
+
case 'use': {
|
|
512
|
+
const parsed = parseHermesArgs('use <account> [--profile <name>] [--no-config]', rest, { noConfig: true });
|
|
513
|
+
requireArity('hermes use <account> [--profile <name>] [--no-config]', parsed.positionals, 1);
|
|
514
|
+
const account = parsed.positionals[0] ?? '';
|
|
515
|
+
const result = await useHermesAccount(account, {
|
|
516
|
+
env,
|
|
517
|
+
...(parsed.profile ? { profile: parsed.profile } : {}),
|
|
518
|
+
updateConfig: !parsed.noConfig,
|
|
519
|
+
});
|
|
520
|
+
write(io.stdout, `Hermes openai-codex auth now uses cx account '${result.account}'`);
|
|
521
|
+
write(io.stdout, `hermes home: ${result.hermesHome}`);
|
|
522
|
+
write(io.stdout, `auth.json: ${result.hermesAuthFile}`);
|
|
523
|
+
write(io.stdout, result.hermesConfigFile
|
|
524
|
+
? `config.yaml: ${result.hermesConfigFile} (model.provider=openai-codex)`
|
|
525
|
+
: 'config.yaml: skipped (--no-config)');
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
case 'sync': {
|
|
530
|
+
const parsed = parseHermesArgs('sync <account> [--profile <name>]', rest, {});
|
|
531
|
+
requireArity('hermes sync <account> [--profile <name>]', parsed.positionals, 1);
|
|
532
|
+
const account = parsed.positionals[0] ?? '';
|
|
533
|
+
const result = await syncHermesAccount(account, {
|
|
534
|
+
env,
|
|
535
|
+
...(parsed.profile ? { profile: parsed.profile } : {}),
|
|
536
|
+
});
|
|
537
|
+
write(io.stdout, `synced Hermes openai-codex tokens to cx account '${result.account}'`);
|
|
538
|
+
write(io.stdout, `cx account file: ${result.codexAccountFile}`);
|
|
539
|
+
write(io.stdout, `hermes home: ${result.hermesHome}`);
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
case 'status': {
|
|
544
|
+
const parsed = parseHermesArgs('status [--profile <name>] [--json]', rest, { json: true });
|
|
545
|
+
requireArity('hermes status [--profile <name>] [--json]', parsed.positionals, 0);
|
|
546
|
+
const status = await inspectHermesStatus({
|
|
547
|
+
env,
|
|
548
|
+
...(parsed.profile ? { profile: parsed.profile } : {}),
|
|
549
|
+
});
|
|
550
|
+
write(io.stdout, parsed.json ? JSON.stringify(status, null, 2) : formatHermesStatus(status));
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
default:
|
|
555
|
+
throw new CxError(`unknown hermes command '${command}'`, 2);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function handleRemoteCommand(
|
|
560
|
+
args: readonly string[],
|
|
561
|
+
env: NodeJS.ProcessEnv,
|
|
562
|
+
io: CliIo,
|
|
563
|
+
): Promise<number> {
|
|
564
|
+
const [command, ...rest] = args;
|
|
565
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
566
|
+
write(io.stdout, remoteHelpText());
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
switch (command) {
|
|
571
|
+
case 'configure': {
|
|
572
|
+
const parsed = parseRemoteConfigureArgs(rest);
|
|
573
|
+
const result = await configureOnePasswordRemote(parsed, { paths: getCodexPaths(env) });
|
|
574
|
+
write(io.stdout, 'configured remote backend: 1password');
|
|
575
|
+
write(io.stdout, `config: ${result.configPath}`);
|
|
576
|
+
write(io.stdout, `vault: ${result.config.vault}`);
|
|
577
|
+
write(io.stdout, `item prefix: ${result.config.itemPrefix}`);
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
case 'status': {
|
|
582
|
+
const parsed = parseJsonArgs(rest);
|
|
583
|
+
requireArity('remote status [--json]', parsed.positionals, 0);
|
|
584
|
+
const status = await inspectRemoteStatus({ env, paths: getCodexPaths(env) });
|
|
585
|
+
write(io.stdout, parsed.json ? JSON.stringify(status, null, 2) : formatRemoteStatus(status));
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
default:
|
|
590
|
+
throw new CxError(`unknown remote command '${command}'`, 2);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function handleSyncCommand(
|
|
595
|
+
args: readonly string[],
|
|
596
|
+
env: NodeJS.ProcessEnv,
|
|
597
|
+
io: CliIo,
|
|
598
|
+
): Promise<number> {
|
|
599
|
+
const [command, ...rest] = args;
|
|
600
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
601
|
+
write(io.stdout, syncHelpText());
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
switch (command) {
|
|
606
|
+
case 'push': {
|
|
607
|
+
requireArity('sync push <account>', rest, 1);
|
|
608
|
+
const account = rest[0] ?? '';
|
|
609
|
+
const result = await syncPushAccount(account, { env, paths: getCodexPaths(env) });
|
|
610
|
+
write(io.stdout, `pushed account '${result.account}' to 1Password item '${result.item}'`);
|
|
611
|
+
write(io.stdout, `vault: ${result.vault}`);
|
|
612
|
+
write(io.stdout, `operation: ${result.operation}`);
|
|
613
|
+
return 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
case 'pull': {
|
|
617
|
+
const parsed = parseForceArgs(rest);
|
|
618
|
+
requireArity('sync pull <account> [--force]', parsed.positionals, 1);
|
|
619
|
+
const account = parsed.positionals[0] ?? '';
|
|
620
|
+
const result = await syncPullAccount(account, {
|
|
621
|
+
env,
|
|
622
|
+
paths: getCodexPaths(env),
|
|
623
|
+
force: parsed.force,
|
|
624
|
+
});
|
|
625
|
+
write(io.stdout, `pulled 1Password item '${result.item}' into account '${result.account}'`);
|
|
626
|
+
write(io.stdout, `account file: ${result.accountFile}`);
|
|
627
|
+
write(io.stdout, `overwrote local account: ${yesNo(result.overwritten)}`);
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
case 'status': {
|
|
632
|
+
const parsed = parseJsonArgs(rest);
|
|
633
|
+
if (parsed.positionals.length > 1) {
|
|
634
|
+
throw new CxError('usage: cx sync status [account] [--json]', 2);
|
|
635
|
+
}
|
|
636
|
+
const status = await inspectSyncStatus(parsed.positionals[0], { env, paths: getCodexPaths(env) });
|
|
637
|
+
write(io.stdout, parsed.json ? JSON.stringify(status, null, 2) : formatSyncStatus(status));
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
default:
|
|
642
|
+
throw new CxError(`unknown sync command '${command}'`, 2);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
234
646
|
export async function main(
|
|
235
647
|
argv: readonly string[] = process.argv.slice(2),
|
|
236
648
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -353,6 +765,15 @@ export async function main(
|
|
|
353
765
|
return 0;
|
|
354
766
|
}
|
|
355
767
|
|
|
768
|
+
case 'hermes':
|
|
769
|
+
return await handleHermesCommand(rest, env, io);
|
|
770
|
+
|
|
771
|
+
case 'remote':
|
|
772
|
+
return await handleRemoteCommand(rest, env, io);
|
|
773
|
+
|
|
774
|
+
case 'sync':
|
|
775
|
+
return await handleSyncCommand(rest, env, io);
|
|
776
|
+
|
|
356
777
|
default:
|
|
357
778
|
throw new CxError(`unknown command '${first}'`, 2);
|
|
358
779
|
}
|