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
13 changes: 13 additions & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { GlobalFlags } from './types/flags';
import type { OptionDef } from './command';
import { CLIError } from './errors/base';
import { ExitCode } from './errors/codes';

/** Recognised spellings for an explicit boolean flag value, e.g. `--flag=false`. */
const BOOLEAN_TRUE_VALUES = new Set(['true', '1', 'yes', 'on']);
Expand Down Expand Up @@ -117,6 +119,17 @@ export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags {

const camelKey = kebabToCamel(key);

// Flags removed in newer versions — give users a clear pointer to the
// current way of doing the same thing instead of silently dropping
// their input.
if (key === 'base-url') {
throw new CLIError(
'Flag --base-url was removed.',
ExitCode.USAGE,
'Set base_url via `mmx config set --key base_url --value <url>`.',
);
}

if (schema.booleans.has(camelKey)) {
// A bare boolean flag (`--flag`) is true. Honour an explicit value
// (`--flag=false`, `--flag=0`, ...) and reject an unrecognised one so a
Expand Down
1 change: 0 additions & 1 deletion src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export function defineCommand(spec: CommandSpec): Command {
export const GLOBAL_OPTIONS: OptionDef[] = [
{ flag: '--api-key <key>', description: 'API key' },
{ flag: '--region <region>', description: 'API region: global, cn' },
{ flag: '--base-url <url>', description: 'API base URL' },
{ flag: '--output <format>', description: 'Output format: text, json' },
{ flag: '--timeout <seconds>', description: 'Request timeout', type: 'number' },
{ flag: '--quiet', description: 'Suppress non-essential output' },
Expand Down
5 changes: 2 additions & 3 deletions src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,8 @@ export function loadConfig(flags: GlobalFlags): Config {
const needsRegionDetection = !explicitRegion
&& (!cachedRegion || (activeKey !== undefined && activeKey !== file.api_key));

const baseUrl = flags.baseUrl
|| process.env.MINIMAX_BASE_URL
|| file.base_url
// env/flag removed intentionally — see PR for context.
const baseUrl = file.base_url
|| file.oauth?.resource_url
|| REGIONS[region]
|| REGIONS.global;
Expand Down
2 changes: 1 addition & 1 deletion src/errors/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function handleError(err: unknown): never {
return handleError(timeout);
}

// Detect TypeError from fetch with invalid URL (e.g., malformed MINIMAX_BASE_URL)
// Detect TypeError from fetch with invalid URL (e.g., malformed base_url in config.json)
if (err instanceof TypeError && err.message === "fetch failed") {
const networkErr = new CLIError(
"Network request failed.",
Expand Down
3 changes: 1 addition & 2 deletions src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,7 @@ ${b('Resources:')}

${b('Global Flags:')}
${a('--api-key <key>')} ${d('API key (overrides all other auth)')}
${a('--region <region>')} ${d('API region: global (default), cn')}
${a('--base-url <url>')} ${d('API base URL (overrides region)')}
${a('--region <region>')} ${d('API region: global (default), cn)')}
${a('--output <format>')} ${d('Output format: text, json')}
${a('--quiet')} ${d('Suppress non-essential output')}
${a('--verbose')} ${d('Print HTTP request/response details')}
Expand Down
1 change: 0 additions & 1 deletion src/types/flags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export interface GlobalFlags {
apiKey?: string;
baseUrl?: string;
output?: string;
quiet: boolean;
verbose: boolean;
Expand Down
28 changes: 28 additions & 0 deletions test/args.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, it, expect } from 'bun:test';
import { parseFlags } from '../src/args';
import type { OptionDef } from '../src/command';
import { GLOBAL_OPTIONS } from '../src/command';
import { CLIError } from '../src/errors/base';

const OPTIONS: OptionDef[] = [
{ flag: '--timeout <seconds>', description: 'Request timeout', type: 'number' },
Expand Down Expand Up @@ -53,3 +55,29 @@ describe('parseFlags', () => {
);
});
});

describe('parseFlags — removed flags', () => {
it('rejects --base-url (space form) as a CLIError with a migration hint', () => {
let caught: unknown;
try {
parseFlags(['--base-url', 'https://api.example', 'search', 'query'], GLOBAL_OPTIONS);
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(CLIError);
expect((caught as CLIError).message).toBe('Flag --base-url was removed.');
expect((caught as CLIError).hint).toMatch(/mmx config set.*base_url/);
});

it('rejects --base-url=... (= form) as a CLIError', () => {
expect(() =>
parseFlags(['--base-url=https://api.example', 'search', 'query'], GLOBAL_OPTIONS),
).toThrow(/--base-url was removed/);
});

it('still parses unrelated flags normally', () => {
const flags = parseFlags(['--region', 'cn', '--quiet'], GLOBAL_OPTIONS);
expect(flags.region).toBe('cn');
expect(flags.quiet).toBe(true);
});
});
70 changes: 69 additions & 1 deletion test/config/loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { copyFileSync, mkdirSync, readFileSync, rmSync, unlinkSync } from 'fs';
import { copyFileSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { loadConfig, renameWithCrossDeviceFallback, writeConfigFile } from '../../src/config/loader';
Expand Down Expand Up @@ -156,3 +156,71 @@ describe('writeConfigFile', () => {
expect(JSON.parse(readFileSync(configPath, 'utf-8')).output).toBe('json');
});
});

describe('loadConfig — baseUrl source chain', () => {
const testDir = join(tmpdir(), `mmx-baseurl-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
const configPath = join(testDir, 'config.json');
const originalConfigDir = process.env.MMX_CONFIG_DIR;

beforeEach(() => {
process.env.MMX_CONFIG_DIR = testDir;
delete process.env.MINIMAX_BASE_URL;
delete process.env.MINIMAX_REGION;
});

afterEach(() => {
if (originalConfigDir === undefined) delete process.env.MMX_CONFIG_DIR;
else process.env.MMX_CONFIG_DIR = originalConfigDir;
rmSync(testDir, { recursive: true, force: true });
});

function writeConfig(body: object): void {
mkdirSync(testDir, { recursive: true });
writeFileSync(configPath, JSON.stringify(body));
}

it('uses file.base_url when set, with no flag or env', () => {
writeConfig({ base_url: 'https://api.from-file.example' });
const cfg = loadConfig(baseFlags);
expect(cfg.baseUrl).toBe('https://api.from-file.example');
});

it('falls back to REGIONS.global when nothing is set', () => {
writeConfig({});
const cfg = loadConfig(baseFlags);
expect(cfg.baseUrl).toBe('https://api.minimax.io');
});

it('falls back to REGIONS[region] when region is set in config file', () => {
writeConfig({ region: 'cn' });
const cfg = loadConfig(baseFlags);
expect(cfg.baseUrl).toBe('https://api.minimaxi.com');
});

it('ignores MINIMAX_BASE_URL env even when file.base_url is missing', () => {
writeConfig({});
process.env.MINIMAX_BASE_URL = 'https://api.from-env.example';
const cfg = loadConfig(baseFlags);
expect(cfg.baseUrl).toBe('https://api.minimax.io');
});

it('file.base_url wins over MINIMAX_BASE_URL env', () => {
writeConfig({ base_url: 'https://api.from-file.example' });
process.env.MINIMAX_BASE_URL = 'https://api.from-env.example';
const cfg = loadConfig(baseFlags);
expect(cfg.baseUrl).toBe('https://api.from-file.example');
});

it('falls back to REGIONS[region] when region is set via env', () => {
writeConfig({});
process.env.MINIMAX_REGION = 'cn';
const cfg = loadConfig(baseFlags);
expect(cfg.baseUrl).toBe('https://api.minimaxi.com');
});

it('ignores malformed file.base_url (not starting with http) and falls back to regions', () => {
writeConfig({ base_url: 'not-a-url' });
const cfg = loadConfig(baseFlags);
expect(cfg.baseUrl).toBe('https://api.minimax.io');
});
});