From 213ca64780d3d895f777e8ef2b3aa177865e14fa Mon Sep 17 00:00:00 2001 From: Kartik Date: Fri, 10 Jul 2026 13:23:56 +0530 Subject: [PATCH] fix(auth): document MMX_CONFIG_DIR in credential errors Co-authored-by: Cursor --- ERRORS.md | 1 + README.md | 11 +++++++++ src/auth/resolver.ts | 4 +++- src/auth/setup.ts | 4 +++- test/auth/resolver.test.ts | 47 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/ERRORS.md b/ERRORS.md index fb22e4f9..0e97f008 100644 --- a/ERRORS.md +++ b/ERRORS.md @@ -15,6 +15,7 @@ This document lists all error scenarios and the messages users will see. | OAuth error in callback | `OAuth error: ${error}` | | OAuth token exchange failed | `OAuth token exchange failed: ${body}` | | `MINIMAX_API_KEY` already set (non-interactive) | `Warning: MINIMAX_API_KEY is already set in environment.` | +| No credentials found (non-interactive) | `No credentials found.` + hint: `Log in: mmx auth login`, `--api-key sk-xxxxx`, or `MMX_CONFIG_DIR=/path/to/.mmx` | ### `mmx auth logout` diff --git a/README.md b/README.md index 3da5dc41..2082a961 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,17 @@ Useful for CI/CD (`mmx auth login --api-key sk-xxxxx`), or pass per-command via OAuth and API key are mutually exclusive — logging in with one clears the other. Credential priority: `--api-key` flag > OAuth (config) > `api_key` (config). +### Environment variables + +| Variable | Description | +|---|---| +| `MINIMAX_REGION` | `global` or `cn`. | +| `MINIMAX_BASE_URL` | Override the API base URL. | +| `MINIMAX_OUTPUT` | `text` or `json`. | +| `MINIMAX_TIMEOUT` | Request timeout in seconds. | +| `MINIMAX_VERBOSE` | `1` to enable verbose HTTP logging. | +| `MMX_CONFIG_DIR` | Directory containing the `config.json` file (default: `~/.mmx`). Set this when `mmx` runs from a subprocess, service, or CI job whose home directory differs from where you logged in. | + ### `mmx config` · `mmx quota` ```bash diff --git a/src/auth/resolver.ts b/src/auth/resolver.ts index fdbc26ac..de6199f2 100644 --- a/src/auth/resolver.ts +++ b/src/auth/resolver.ts @@ -26,6 +26,8 @@ export async function resolveCredential(config: Config): Promise { throw new CLIError( 'No credentials found.', ExitCode.AUTH, - 'Log in: mmx auth login\nPass directly: --api-key sk-xxxxx', + 'Log in: mmx auth login\n' + + 'Pass per-call: --api-key sk-xxxxx\n' + + 'Or set env var: MMX_CONFIG_DIR=/path/to/.mmx', ); } diff --git a/test/auth/resolver.test.ts b/test/auth/resolver.test.ts index a24a304b..8b80429f 100644 --- a/test/auth/resolver.test.ts +++ b/test/auth/resolver.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { resolveCredential } from '../../src/auth/resolver'; +import { ensureAuth } from '../../src/auth/setup'; +import { CLIError } from '../../src/errors/base'; import type { Config } from '../../src/config/schema'; import { mkdirSync, rmSync } from 'fs'; import { join } from 'path'; @@ -59,9 +61,54 @@ describe('resolveCredential', () => { await expect(resolveCredential(config)).rejects.toThrow('No credentials found'); }); + it('no-credentials hint mentions MMX_CONFIG_DIR and working remediation options', async () => { + const config = makeConfig(); + try { + await resolveCredential(config); + throw new Error('expected resolveCredential to throw'); + } catch (err) { + expect(err).toBeInstanceOf(CLIError); + const hint = (err as CLIError).hint ?? ''; + expect(hint).toContain('mmx auth login'); + expect(hint).toContain('--api-key'); + expect(hint).toContain('MMX_CONFIG_DIR'); + } + }); + it('prefers flag over file api key', async () => { const config = makeConfig({ apiKey: 'sk-flag', fileApiKey: 'sk-file' }); const cred = await resolveCredential(config); expect(cred.token).toBe('sk-flag'); }); }); + +describe('ensureAuth (non-interactive, no credentials)', () => { + const testDir = join(tmpdir(), `mmx-setup-test-${Date.now()}`); + const originalConfigDir = process.env.MMX_CONFIG_DIR; + + beforeEach(() => { + mkdirSync(join(testDir, '.mmx'), { recursive: true }); + process.env.MMX_CONFIG_DIR = join(testDir, '.mmx'); + }); + + afterEach(() => { + if (originalConfigDir) process.env.MMX_CONFIG_DIR = originalConfigDir; + else delete process.env.MMX_CONFIG_DIR; + delete process.env.MINIMAX_API_KEY; + rmSync(testDir, { recursive: true, force: true }); + }); + + it('no-credentials hint mentions MMX_CONFIG_DIR and working remediation options', async () => { + const config = makeConfig({ nonInteractive: true }); + try { + await ensureAuth(config); + throw new Error('expected ensureAuth to throw'); + } catch (err) { + expect(err).toBeInstanceOf(CLIError); + const hint = (err as CLIError).hint ?? ''; + expect(hint).toContain('mmx auth login'); + expect(hint).toContain('--api-key'); + expect(hint).toContain('MMX_CONFIG_DIR'); + } + }); +});