From ea6fadb953e935c1df93f3a957dea0988f358dbf Mon Sep 17 00:00:00 2001 From: andershsueh Date: Sat, 1 Aug 2026 12:48:22 +0800 Subject: [PATCH] fix(config): drop --base-url flag and MINIMAX_BASE_URL env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit baseUrl is now sourced from config.json's base_url field (or, after OAuth login, the oauth.resource_url field stored alongside it). The --base-url global flag and the MINIMAX_BASE_URL env var are no longer honored. Background: an external tool setting MINIMAX_BASE_URL was getting intercepted by mmx and breaking its own routing. base_url is also fully redundant with mmx config set --key base_url, so the explicit overrides are removed. - src/command.ts: drop --base-url from GLOBAL_OPTIONS - src/registry.ts: drop --base-url line from help text - src/types/flags.ts: drop baseUrl? field from GlobalFlags - src/config/loader.ts: shorten baseUrl chain to file.base_url || file.oauth?.resource_url || REGIONS[region] || REGIONS.global - src/args.ts: explicitly reject --base-url with a CLIError(USAGE) pointing at `mmx config set --key base_url`, so users no longer see their input silently dropped - src/errors/handler.ts: refresh stale MINIMAX_BASE_URL example in the "fetch failed" branch comment - test/config/loader.test.ts: add 7 cases for the baseUrl source chain (file wins, region fallback, env ignored, malformed URL fallback). Uses MMX_CONFIG_DIR for cross-platform isolation. - test/args.test.ts: add 3 cases for the --base-url rejection (space form, = form, and a happy-path smoke test) Verified: 358 non-SDK tests pass; 14 pre-existing SDK failures are unrelated (network-dependent integration tests). bun run lint and bun run typecheck both clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/args.ts | 13 +++++++ src/command.ts | 1 - src/config/loader.ts | 5 ++- src/errors/handler.ts | 2 +- src/registry.ts | 3 +- src/types/flags.ts | 1 - test/args.test.ts | 28 +++++++++++++++ test/config/loader.test.ts | 70 +++++++++++++++++++++++++++++++++++++- 8 files changed, 114 insertions(+), 9 deletions(-) diff --git a/src/args.ts b/src/args.ts index 0fe2f343..fd8d7221 100644 --- a/src/args.ts +++ b/src/args.ts @@ -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']); @@ -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 `.', + ); + } + 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 diff --git a/src/command.ts b/src/command.ts index 8d89da84..c1a3b85d 100644 --- a/src/command.ts +++ b/src/command.ts @@ -44,7 +44,6 @@ export function defineCommand(spec: CommandSpec): Command { export const GLOBAL_OPTIONS: OptionDef[] = [ { flag: '--api-key ', description: 'API key' }, { flag: '--region ', description: 'API region: global, cn' }, - { flag: '--base-url ', description: 'API base URL' }, { flag: '--output ', description: 'Output format: text, json' }, { flag: '--timeout ', description: 'Request timeout', type: 'number' }, { flag: '--quiet', description: 'Suppress non-essential output' }, diff --git a/src/config/loader.ts b/src/config/loader.ts index 77680441..e63fd6af 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -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; diff --git a/src/errors/handler.ts b/src/errors/handler.ts index 871e553c..dd2ae31b 100644 --- a/src/errors/handler.ts +++ b/src/errors/handler.ts @@ -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.", diff --git a/src/registry.ts b/src/registry.ts index feca2332..2b302af8 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -218,8 +218,7 @@ ${b('Resources:')} ${b('Global Flags:')} ${a('--api-key ')} ${d('API key (overrides all other auth)')} - ${a('--region ')} ${d('API region: global (default), cn')} - ${a('--base-url ')} ${d('API base URL (overrides region)')} + ${a('--region ')} ${d('API region: global (default), cn)')} ${a('--output ')} ${d('Output format: text, json')} ${a('--quiet')} ${d('Suppress non-essential output')} ${a('--verbose')} ${d('Print HTTP request/response details')} diff --git a/src/types/flags.ts b/src/types/flags.ts index 4f29f851..f7dbfbf6 100644 --- a/src/types/flags.ts +++ b/src/types/flags.ts @@ -1,6 +1,5 @@ export interface GlobalFlags { apiKey?: string; - baseUrl?: string; output?: string; quiet: boolean; verbose: boolean; diff --git a/test/args.test.ts b/test/args.test.ts index e196d3e2..492b7c30 100644 --- a/test/args.test.ts +++ b/test/args.test.ts @@ -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 ', description: 'Request timeout', type: 'number' }, @@ -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); + }); +}); diff --git a/test/config/loader.test.ts b/test/config/loader.test.ts index 1fcd8158..afc67c73 100644 --- a/test/config/loader.test.ts +++ b/test/config/loader.test.ts @@ -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'; @@ -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'); + }); +});