diff --git a/.gitignore b/.gitignore index c6e3351..ed9621a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,11 @@ scripts/test-commands.sh # CodeGraph .codegraph/ + +# GitNexus (code intelligence index) +.gitnexus/ + +# Claude/Cursor AI config (not Kiro) +.claude/ +CLAUDE.md +AGENTS.md diff --git a/apps/token-cli/src/payment-methods/add.ts b/apps/token-cli/src/payment-methods/add.ts index 4f0833f..93a11f5 100644 --- a/apps/token-cli/src/payment-methods/add.ts +++ b/apps/token-cli/src/payment-methods/add.ts @@ -76,6 +76,10 @@ export function registerAddCommand(parent: Command, deps: AddDeps): void { .option( '--idempotency-key ', 'Idempotency key forwarded verbatim as the Idempotency-Key header (manual mode only)', + ) + .option( + '--no-poll', + 'Dropin mode: mint the session, print it, and exit immediately without polling verification status (for server/SDK-driven flows where the front-end completes the binding)', ); cmd.action(async () => { @@ -309,6 +313,13 @@ async function handleDropinMode( 'Use the Session ID to add the payment method in the browser via the Drop-in SDK', ); + // --no-poll: server/SDK-driven flows finish the binding in the front-end, so + // the CLI mints + prints the session and exits immediately. The session id is + // already on stdout (clean JSON in --format json), so the caller can parse it. + if (opts.poll === false) { + return; + } + // 3) Poll verification/status (same endpoint manual mode uses) until the PM // reaches a terminal status or we time out at 30 minutes. const finalPm = await pollVerificationStatus(deps.apiClient, apiKey, pmId, { diff --git a/apps/token-cli/tests/payment-methods.test.ts b/apps/token-cli/tests/payment-methods.test.ts index 4a2c14d..76b3de4 100644 --- a/apps/token-cli/tests/payment-methods.test.ts +++ b/apps/token-cli/tests/payment-methods.test.ts @@ -3,7 +3,7 @@ import { registerListCommand } from '../src/payment-methods/list.js'; import { registerGetCommand } from '../src/payment-methods/get.js'; import { registerDisableCommand } from '../src/payment-methods/disable.js'; import { registerAddCommand } from '../src/payment-methods/add.js'; -import { buildProgram, captureStdout, captureStderr, mockApiClient } from './helpers.js'; +import { buildProgram, captureStdout, captureStderr, mockApiClient, parseJsonOutput } from './helpers.js'; afterEach(() => { vi.restoreAllMocks(); @@ -543,4 +543,64 @@ describe('payment-methods add --mode dropin', () => { expect(out.text()).toContain('pm_dropin'); expect(process.exitCode).toBe(1); }); + + it('--no-poll: mints + prints the session and exits immediately without polling', async () => { + const apiClient = dropinClient({ id: 'pm_dropin', status: 'PENDING' }); + + const program = buildProgram(); + const cmd = program.command('payment-methods'); + registerAddCommand(cmd, { apiClient } as any); + + const out = captureStdout(); + const err = captureStderr(); + + await program.parseAsync([ + 'node', 'cli', 'payment-methods', 'add', + '--mode', 'dropin', + '--no-poll', + '--api-key', 'sk_key', + '--email', 'user@example.com', + ]); + + // Session minted and printed + expect(apiClient.post).toHaveBeenCalledWith( + '/payment-methods/dropin/create', + { type: 'api-key', key: 'sk_key' }, + { email: 'user@example.com' }, + ); + expect(out.text()).toContain('sess_abc123'); + expect(err.text()).toContain('Drop-in session created'); + + // Crucially: no polling of verification/status + expect(apiClient.get).not.toHaveBeenCalled(); + + // Clean exit + expect(process.exitCode === 0 || process.exitCode === undefined).toBe(true); + }); + + it('--no-poll --format json: stdout is clean parseable JSON with session_id', async () => { + const apiClient = dropinClient({ id: 'pm_dropin', status: 'PENDING' }); + + const program = buildProgram(); + const cmd = program.command('payment-methods'); + registerAddCommand(cmd, { apiClient } as any); + + const out = captureStdout(); + captureStderr(); + + await program.parseAsync([ + 'node', 'cli', '--format', 'json', 'payment-methods', 'add', + '--mode', 'dropin', + '--no-poll', + '--api-key', 'sk_key', + '--email', 'user@example.com', + ]); + + // stdout must be parseable JSON carrying session_id (orchestrator parses it) + const parsed = parseJsonOutput(out.text()) as Record; + expect(parsed.session_id).toBe('sess_abc123'); + + // no polling happened + expect(apiClient.get).not.toHaveBeenCalled(); + }); });