diff --git a/src/commands/infoCommand.ts b/src/commands/infoCommand.ts index 335015d..8699b6b 100644 --- a/src/commands/infoCommand.ts +++ b/src/commands/infoCommand.ts @@ -24,7 +24,7 @@ export class InfoCommand { this.devMode = devMode; } - async execute(): Promise { + async execute(opts: { json?: boolean } = {}): Promise { const pm = new ProjectManager(); const projectState = await pm.detectProjectState(); const hasKeep = projectState.initialized && !!projectState.organizationId; @@ -58,6 +58,7 @@ export class InfoCommand { // Resolve the user's role in the active org (best effort). let roleLabel = '—'; + let roleSlug: string | null = null; if (activeOrgId && authService.getToken() && authResult.user_id) { try { const serviceClient = new ServiceClient(this.apiUrl, this.devMode); @@ -65,12 +66,33 @@ export class InfoCommand { const { members } = await serviceClient.listMembers(activeOrgId); const me = members.find((m: any) => m.userId === authResult.user_id); const slug = me?.role?.slug; - if (slug) roleLabel = ROLE_LABELS[slug] || slug; + if (slug) { + roleLabel = ROLE_LABELS[slug] || slug; + roleSlug = slug; + } } catch { // leave as — } } + if (opts.json) { + console.log( + JSON.stringify( + { + user: { email: authResult.user_email ?? null, userId: authResult.user_id ?? null }, + org: { id: activeOrgId ?? null, name: activeOrgName ?? null, workosOrgId: workosOrgId ?? null, fromKeepLock: hasKeep }, + role: roleSlug, + project: { name: projectState.projectName ?? null, id: projectState.projectId ?? null }, + branch: hasKeep ? branch : null, + memberships: allOrgs.map((o) => ({ id: o.id, name: o.name ?? null, workosOrgId: o.workos_org_id ?? null })), + }, + null, + 2, + ), + ); + return; + } + const activeOrgLabel = hasKeep ? `${activeOrgName || '—'} ${DIM}(from keep.lock)${RESET}` : activeOrgId diff --git a/src/commands/listCommand.ts b/src/commands/listCommand.ts new file mode 100644 index 0000000..e3b11b1 --- /dev/null +++ b/src/commands/listCommand.ts @@ -0,0 +1,63 @@ +import { ProjectManager } from '../core/projectManager'; +import { listAllVarsOnBranch, listManagedKeys, findManagedConnector } from './connectors/shared'; + +const B = (s: string) => `\x1b[1m${s}\x1b[0m`; +const DIM = '\x1b[90m'; +const RESET = '\x1b[0m'; + +/** + * `capy list` — variable NAMES + connector metadata for the active branch. + * Reads keep.lock only: no auth, no network, no decryption. Never emits values. + */ +export class ListCommand { + constructor(private readonly devMode: boolean = false) {} + + async execute(opts: { json?: boolean } = {}): Promise { + const pm = new ProjectManager(); + const projectState = await pm.detectProjectState(); + if (!projectState.initialized) { + console.error(`No keep.lock found. Run ${B('capy')} to initialize.`); + process.exit(1); + } + const keep = pm.readKeepFile(); + if (!keep) { + console.error('Could not read keep.lock'); + process.exit(1); + } + const branch = projectState.activeBranch; + + const managed = new Set(listManagedKeys(keep, branch).map((m) => m.varName)); + const variables = listAllVarsOnBranch(keep, branch).map((name) => { + const c = managed.has(name) ? findManagedConnector(keep, name, branch) : undefined; + return { + name, + managed: !!c, + connector: c + ? { + provider: c.provider, + source: c.source, + mode: c.mode ?? null, + accountId: c.account_id ?? null, + fingerprint: c.fingerprint, + createdAt: new Date(c.created_at * 1000).toISOString(), + rotatedAt: c.rotated_at ? new Date(c.rotated_at * 1000).toISOString() : null, + expiresAt: c.expires_at ? new Date(c.expires_at * 1000).toISOString() : null, + } + : null, + }; + }); + + if (opts.json) { + console.log(JSON.stringify({ projectName: keep.project_name, branch, variables }, null, 2)); + return; + } + + console.log(''); + console.log(` ${keep.project_name} ${DIM}·${RESET} ${branch} ${DIM}(${variables.length})${RESET}`); + for (const v of variables) { + const tag = v.connector ? ` ${DIM}(${v.connector.provider})${RESET}` : ''; + console.log(` ${v.name}${tag}`); + } + console.log(''); + } +} diff --git a/src/index-dev.ts b/src/index-dev.ts index bf2a879..e25d944 100644 --- a/src/index-dev.ts +++ b/src/index-dev.ts @@ -527,10 +527,21 @@ program program .command('info') .description('Show current session info') - .action(async () => { + .option('--json', 'emit machine-readable JSON instead of the human UI') + .action(async (options) => { const { InfoCommand } = await import('./commands/infoCommand'); const cmd = new InfoCommand(process.env.CAPY_API_URL, true); - await cmd.execute(); + await cmd.execute({ json: options.json }); + }); + +program + .command('list') + .description('List variable names + connector metadata for the active branch (no values)') + .option('--json', 'emit machine-readable JSON instead of the human UI') + .action(async (options) => { + const { ListCommand } = await import('./commands/listCommand'); + const cmd = new ListCommand(true); + await cmd.execute({ json: options.json }); }); program diff --git a/src/index.ts b/src/index.ts index e4f48bb..924a302 100644 --- a/src/index.ts +++ b/src/index.ts @@ -534,11 +534,23 @@ program program .command('info') .description('Show current session info') - .action(async () => { + .option('--json', 'emit machine-readable JSON instead of the human UI') + .action(async (options) => { assertNotLocalOnly('info'); const { InfoCommand } = await import('./commands/infoCommand'); const cmd = new InfoCommand(); - await cmd.execute(); + await cmd.execute({ json: options.json }); + }); + +program + .command('list') + .description('List variable names + connector metadata for the active branch (no values)') + .option('--json', 'emit machine-readable JSON instead of the human UI') + .action(async (options) => { + assertNotLocalOnly('list'); + const { ListCommand } = await import('./commands/listCommand'); + const cmd = new ListCommand(); + await cmd.execute({ json: options.json }); }); program