From ae8d3345c8dfa1acfceb8d1a19fecc87da5729d7 Mon Sep 17 00:00:00 2001 From: Scott Lovegrove Date: Thu, 16 Apr 2026 17:01:38 +0100 Subject: [PATCH] feat(user): add --json and --full support to tw user Brings `tw user` in line with `tw users` and `tw away status` for machine-readable output, enabling scripting use cases like piping the current user's id or email into other commands. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/twist-cli/SKILL.md | 2 ++ src/__tests__/user.test.ts | 60 +++++++++++++++++++++++++++++++++++++- src/commands/user.ts | 12 ++++++-- src/lib/skills/content.ts | 2 ++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/skills/twist-cli/SKILL.md b/skills/twist-cli/SKILL.md index dffbf19..5c458f9 100644 --- a/skills/twist-cli/SKILL.md +++ b/skills/twist-cli/SKILL.md @@ -177,6 +177,8 @@ tw search "query" --cursor # Pagination cursor ```bash tw user # Show current user info +tw user --json # JSON output +tw user --json --full # Include all fields in JSON output tw users # List workspace users tw users --search # Filter by name/email tw channels # List active joined workspace channels diff --git a/src/__tests__/user.test.ts b/src/__tests__/user.test.ts index 8228d00..cff0648 100644 --- a/src/__tests__/user.test.ts +++ b/src/__tests__/user.test.ts @@ -1,12 +1,18 @@ import { Command } from 'commander' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('../lib/api.js', () => ({ +const apiMocks = vi.hoisted(() => ({ getCurrentWorkspaceId: vi.fn().mockResolvedValue(1), getSessionUser: vi.fn(), getWorkspaceUsers: vi.fn(), })) +vi.mock('../lib/api.js', () => ({ + getCurrentWorkspaceId: apiMocks.getCurrentWorkspaceId, + getSessionUser: apiMocks.getSessionUser, + getWorkspaceUsers: apiMocks.getWorkspaceUsers, +})) + vi.mock('../lib/refs.js', () => ({ resolveWorkspaceRef: vi.fn(), })) @@ -35,3 +41,55 @@ describe('users --workspace conflict', () => { ).rejects.toThrow('Cannot specify workspace both as argument and --workspace flag') }) }) + +describe('user --json', () => { + const sampleUser = { + id: 42, + name: 'Jane Smith', + email: 'jane@example.com', + timezone: 'America/New_York', + userType: 'regular', + awayMode: null, + defaultWorkspace: 1, + lang: 'en', + shortName: 'Jane', + } + + beforeEach(() => { + vi.clearAllMocks() + apiMocks.getSessionUser.mockResolvedValue(sampleUser) + }) + + it('outputs essential user fields as JSON', async () => { + const program = createProgram() + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await program.parseAsync(['node', 'tw', 'user', '--json']) + + expect(consoleSpy).toHaveBeenCalledTimes(1) + const jsonOutput = JSON.parse(consoleSpy.mock.calls[0][0]) + expect(jsonOutput.id).toBe(42) + expect(jsonOutput.name).toBe('Jane Smith') + expect(jsonOutput.email).toBe('jane@example.com') + expect(jsonOutput.timezone).toBe('America/New_York') + expect(jsonOutput).not.toHaveProperty('lang') + expect(jsonOutput).not.toHaveProperty('shortName') + + consoleSpy.mockRestore() + }) + + it('outputs full user fields with --full', async () => { + const program = createProgram() + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await program.parseAsync(['node', 'tw', 'user', '--json', '--full']) + + expect(consoleSpy).toHaveBeenCalledTimes(1) + const jsonOutput = JSON.parse(consoleSpy.mock.calls[0][0]) + expect(jsonOutput).toHaveProperty('lang', 'en') + expect(jsonOutput).toHaveProperty('shortName', 'Jane') + expect(jsonOutput).toHaveProperty('defaultWorkspace', 1) + + consoleSpy.mockRestore() + }) +}) diff --git a/src/commands/user.ts b/src/commands/user.ts index eb49adb..ef2cdc6 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -8,9 +8,14 @@ import { resolveWorkspaceRef } from '../lib/refs.js' type UsersOptions = ViewOptions & { workspace?: string; search?: string } -async function showCurrentUser(): Promise { +async function showCurrentUser(options: ViewOptions): Promise { const user = await getSessionUser() + if (options.json) { + console.log(formatJson(user, 'user', options.full)) + return + } + console.log(chalk.bold(user.name)) console.log('') console.log(`ID: ${user.id}`) @@ -77,11 +82,14 @@ export function registerUserCommand(program: Command): void { program .command('user') .description('Show current user info') + .option('--json', 'Output as JSON') + .option('--full', 'Include all fields in JSON output') .addHelpText( 'after', ` Examples: - tw user`, + tw user + tw user --json`, ) .action(showCurrentUser) diff --git a/src/lib/skills/content.ts b/src/lib/skills/content.ts index dbe7969..595f7c0 100644 --- a/src/lib/skills/content.ts +++ b/src/lib/skills/content.ts @@ -176,6 +176,8 @@ tw search "query" --cursor # Pagination cursor \`\`\`bash tw user # Show current user info +tw user --json # JSON output +tw user --json --full # Include all fields in JSON output tw users # List workspace users tw users --search # Filter by name/email tw channels # List active joined workspace channels