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'); + }); +});