Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ERRORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/auth/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export async function resolveCredential(config: Config): Promise<ResolvedCredent
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',
);
}
4 changes: 3 additions & 1 deletion src/auth/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export async function ensureAuth(config: Config): Promise<void> {
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',
);
}

Expand Down
47 changes: 47 additions & 0 deletions test/auth/resolver.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
}
});
});