From af65c72d0f1c6c4692dde636031173b9bf1e1ef4 Mon Sep 17 00:00:00 2001 From: cvince Date: Thu, 25 Jun 2026 01:22:00 -0700 Subject: [PATCH] feat(cli): add --json to status, branch, users --- src/commands/statusCommand.ts | 28 +++++++++++++++++++++++++--- src/commands/usersCommand.ts | 33 ++++++++++++++++++++++++++++++++- src/index-dev.ts | 32 ++++++++++++++++++++++++++++---- src/index.ts | 32 ++++++++++++++++++++++++++++---- 4 files changed, 113 insertions(+), 12 deletions(-) diff --git a/src/commands/statusCommand.ts b/src/commands/statusCommand.ts index 8df18ba..9fa6cda 100644 --- a/src/commands/statusCommand.ts +++ b/src/commands/statusCommand.ts @@ -129,9 +129,9 @@ export class StatusCommand { this.serviceClient.setTokenProvider(() => this.authService.getValidToken()); } - async execute(): Promise { + async execute(opts: { json?: boolean } = {}): Promise { try { - await this._execute(); + await this._execute(opts); } catch { // Exit silently on any error (auth, network, etc.) // Hooks must never block git operations @@ -139,7 +139,7 @@ export class StatusCommand { } } - private async _execute(): Promise { + private async _execute(opts: { json?: boolean } = {}): Promise { const projectState = await this.projectManager.detectProjectState(); if (!projectState.initialized) { if (this.terse) return; @@ -247,6 +247,28 @@ export class StatusCommand { : undefined; const { diffs, showLocal, showRemote } = compareSecrets(pinned, localHashes, remoteHashes); + if (opts.json) { + // diffs carry value HASHES only (sha256 prefix), never plaintext. + const totalSecrets = new Set([...Object.keys(pinned), ...Object.keys(localHashes)]).size; + console.log( + JSON.stringify( + { + projectName: keep.project_name, + branch, + totalSecrets, + inSync: diffs.length === 0, + localMatchesPinned: !showLocal, + remoteMatchesPinned: !showRemote, + remoteFailure: remoteFailure ?? null, + diffs, + }, + null, + 2, + ), + ); + return; + } + if (this.terse) { // Terse mode for git hooks if (diffs.length === 0) return; // Silent when synced diff --git a/src/commands/usersCommand.ts b/src/commands/usersCommand.ts index 11b159e..e900f14 100644 --- a/src/commands/usersCommand.ts +++ b/src/commands/usersCommand.ts @@ -94,7 +94,7 @@ export class UsersCommand { return { projectId: project.id, branchId: (branch as any).id, userId: member.userId }; } - async execute(): Promise { + async execute(opts: { json?: boolean } = {}): Promise { const pm = new ProjectManager(); const projectState = await pm.detectProjectState(); @@ -141,6 +141,37 @@ export class UsersCommand { process.exit(1); } + if (opts.json) { + console.log( + JSON.stringify( + { + members: members.map((m: any) => ({ + membershipId: m.membershipId, + userId: m.userId, + email: m.email, + role: m.role, + status: m.status, + joinedAt: m.createdAt, + projects: (m.projects || []).map((p: any) => ({ + id: p.id, + name: p.name, + role: p.role ?? null, + branches: (p.branches || []).map((b: any) => ({ + id: b.id, + name: b.name, + isProtected: b.isProtected, + hasAccess: b.hasAccess, + })), + })), + })), + }, + null, + 2, + ), + ); + return; + } + if (members.length === 0) { console.log('\n No members found.\n'); return; diff --git a/src/index-dev.ts b/src/index-dev.ts index e25d944..50d9e59 100644 --- a/src/index-dev.ts +++ b/src/index-dev.ts @@ -119,6 +119,7 @@ program .command('branch') .description('List secret branches') .option('-D ', 'Delete a branch') + .option('--json', 'emit machine-readable JSON instead of the human UI') .action(async (options) => { const { AuthService } = await import('./auth/authService'); const { ServiceClient } = await import('./service/serviceClient'); @@ -181,6 +182,27 @@ program const activeBranch = projectState.activeBranch; const projectName = projectState.projectName || 'project'; + if (options.json) { + console.log( + JSON.stringify( + { + projectName, + activeBranch, + branches: branches.map((b) => ({ + id: b.id, + name: b.name, + isProtected: b.is_protected, + createdAt: b.created_at ?? null, + isCurrent: b.name === activeBranch, + })), + }, + null, + 2, + ), + ); + return; + } + // Tree view console.log(''); console.log(`Project "${projectName}"`); @@ -250,10 +272,11 @@ program program .command('status') .description('Show secret drift between local, pinned, and remote') - .action(async () => { + .option('--json', 'emit machine-readable JSON instead of the human UI') + .action(async (options) => { const { StatusCommand } = await import('./commands/statusCommand'); const cmd = new StatusCommand(false, true); - await cmd.execute(); + await cmd.execute({ json: options.json }); }); program @@ -547,10 +570,11 @@ program program .command('users') .description('List organization members and their project access') - .action(async () => { + .option('--json', 'emit machine-readable JSON instead of the human UI') + .action(async (options) => { const { UsersCommand } = await import('./commands/usersCommand'); const cmd = new UsersCommand(process.env.CAPY_API_URL, true); - await cmd.execute(); + await cmd.execute({ json: options.json }); }); program diff --git a/src/index.ts b/src/index.ts index 924a302..49bfc89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -107,10 +107,11 @@ program program .command('status') .description('Show secret drift between local, pinned, and remote') - .action(async () => { + .option('--json', 'emit machine-readable JSON instead of the human UI') + .action(async (options) => { const { StatusCommand } = await import('./commands/statusCommand'); const cmd = new StatusCommand(); - await cmd.execute(); + await cmd.execute({ json: options.json }); }); program @@ -126,6 +127,7 @@ program .command('branch') .description('List secret branches') .option('-D ', 'Delete a branch') + .option('--json', 'emit machine-readable JSON instead of the human UI') .action(async (options) => { assertNotLocalOnly('branch'); const { AuthService } = await import('./auth/authService'); @@ -186,6 +188,27 @@ program const activeBranch = projectState.activeBranch; const projectName = projectState.projectName || 'project'; + if (options.json) { + console.log( + JSON.stringify( + { + projectName, + activeBranch, + branches: branches.map((b) => ({ + id: b.id, + name: b.name, + isProtected: b.is_protected, + createdAt: b.created_at ?? null, + isCurrent: b.name === activeBranch, + })), + }, + null, + 2, + ), + ); + return; + } + // Tree view console.log(''); console.log(` Project "${projectName}"`); @@ -556,11 +579,12 @@ program program .command('users') .description('List organization members and their project access') - .action(async () => { + .option('--json', 'emit machine-readable JSON instead of the human UI') + .action(async (options) => { assertNotLocalOnly('users'); const { UsersCommand } = await import('./commands/usersCommand'); const cmd = new UsersCommand(); - await cmd.execute(); + await cmd.execute({ json: options.json }); }); program