diff --git a/src/auth/setup.ts b/src/auth/setup.ts index 5720f4d..98b6e8a 100644 --- a/src/auth/setup.ts +++ b/src/auth/setup.ts @@ -1,5 +1,6 @@ import type { Config } from '../config/schema'; import { readConfigFile, writeConfigFile } from '../config/loader'; +import { loadCredentials } from './credentials'; import { promptText, promptConfirm } from '../utils/prompt'; import { isInteractive } from '../utils/env'; import { maskToken } from '../utils/token'; @@ -8,6 +9,7 @@ import { ExitCode } from '../errors/codes'; export async function ensureApiKey(config: Config): Promise { if (config.apiKey || config.fileApiKey) return; + if (await loadCredentials()) return; const envKey = process.env.MINIMAX_API_KEY; let key: string | undefined; diff --git a/src/main.ts b/src/main.ts index cd7f630..d9c9757 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,6 +26,8 @@ process.stdout.on('error', (e: NodeJS.ErrnoException) => { // Commands that manage their own auth or need no key const NO_AUTH_SETUP = [ ['auth', 'login'], + ['auth', 'refresh'], + ['auth', 'status'], ['auth', 'logout'], ['config', 'show'], ['config', 'set'], @@ -80,7 +82,7 @@ async function main() { const needsAuthSetup = !NO_AUTH_SETUP.some( (cmd) => cmd.every((c, i) => commandPath[i] === c), ); - if (needsAuthSetup) { + if (needsAuthSetup && !config.dryRun) { await ensureApiKey(config); } diff --git a/test/main.test.ts b/test/main.test.ts new file mode 100644 index 0000000..ca6e8fc --- /dev/null +++ b/test/main.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { spawn, spawnSync } from 'child_process'; + +function runCli(args: string[], home: string) { + const env: NodeJS.ProcessEnv = { + ...process.env, + HOME: home, + CI: '1', + NO_COLOR: '1', + }; + delete env.MINIMAX_API_KEY; + + return spawnSync(process.execPath, ['src/main.ts', ...args], { + cwd: process.cwd(), + env, + encoding: 'utf-8', + }); +} + +function runCliAsync(args: string[], home: string): Promise<{ status: number | null; stdout: string; stderr: string }> { + const env: NodeJS.ProcessEnv = { + ...process.env, + HOME: home, + CI: '1', + NO_COLOR: '1', + }; + delete env.MINIMAX_API_KEY; + + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, ['src/main.ts', ...args], { + cwd: process.cwd(), + env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + const timer = setTimeout(() => { + child.kill(); + reject(new Error('CLI process timed out')); + }, 5000); + + child.stdout.setEncoding('utf-8'); + child.stderr.setEncoding('utf-8'); + child.stdout.on('data', (chunk: string) => { + stdout += chunk; + }); + child.stderr.on('data', (chunk: string) => { + stderr += chunk; + }); + child.on('error', (err) => { + clearTimeout(timer); + reject(err); + }); + child.on('close', (status) => { + clearTimeout(timer); + resolve({ status, stdout, stderr }); + }); + }); +} + +describe('main CLI auth setup', () => { + let home: string; + + beforeEach(() => { + home = mkdtempSync(join(tmpdir(), 'mmx-main-test-')); + }); + + afterEach(() => { + rmSync(home, { recursive: true, force: true }); + }); + + it('allows dry-run commands without configured credentials', () => { + const result = runCli( + ['text', 'chat', '--message', 'hello', '--dry-run', '--output', 'json'], + home, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('"request"'); + expect(result.stdout).toContain('"hello"'); + expect(result.stderr).not.toContain('No API key found'); + }); + + it('lets auth status report unauthenticated state without prompting for setup', () => { + const result = runCli(['auth', 'status', '--output', 'json'], home); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('"authenticated": false'); + expect(result.stdout).toContain('Not authenticated'); + expect(result.stderr).not.toContain('No API key found'); + }); + + it('allows OAuth credentials to satisfy auth setup for API commands', async () => { + mkdirSync(join(home, '.mmx'), { recursive: true }); + writeFileSync( + join(home, '.mmx', 'credentials.json'), + JSON.stringify({ + access_token: 'oauth-access-token', + refresh_token: 'oauth-refresh-token', + expires_at: new Date(Date.now() + 3600000).toISOString(), + token_type: 'Bearer', + }), + { mode: 0o600 }, + ); + + const server = Bun.serve({ + port: 0, + fetch() { + return Response.json({ + id: 'msg_1', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'ok' }], + model: 'MiniMax-M2.7', + stop_reason: 'end_turn', + usage: { input_tokens: 1, output_tokens: 1 }, + }); + }, + }); + + try { + const result = await runCliAsync( + [ + 'text', + 'chat', + '--message', + 'hello', + '--base-url', + `http://127.0.0.1:${server.port}`, + '--output', + 'json', + ], + home, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('"id": "msg_1"'); + expect(result.stderr).not.toContain('No API key found'); + } finally { + server.stop(true); + } + }); +});