diff --git a/build.ts b/build.ts index 49b691e..6e5b189 100644 --- a/build.ts +++ b/build.ts @@ -13,6 +13,7 @@ await Bun.build({ naming: 'mmx.mjs', target: 'node', minify: !DEV_BUILD, + external: ['undici'], define: { 'process.env.CLI_VERSION': JSON.stringify(VERSION) }, }); diff --git a/bun.lock b/bun.lock index 7d732be..3435bcf 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@clack/prompts": "^0.7.0", "bun-plugin-dts": "^0.4.0", "es-toolkit": "^1.46.1", + "undici": "^6.21.1", }, "devDependencies": { "@eslint/js": "^9.0.0", @@ -257,6 +258,8 @@ "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="], + "undici": ["undici@6.25.0", "", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], diff --git a/package.json b/package.json index a6155f6..a6d58e1 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "dependencies": { "@clack/prompts": "^0.7.0", "bun-plugin-dts": "^0.4.0", - "es-toolkit": "^1.46.1" + "es-toolkit": "^1.46.1", + "undici": "^6.21.1" }, "devDependencies": { "@eslint/js": "^9.0.0", diff --git a/src/commands/config/set.ts b/src/commands/config/set.ts index 39f7ef9..3a511c9 100644 --- a/src/commands/config/set.ts +++ b/src/commands/config/set.ts @@ -6,7 +6,7 @@ import { readConfigFile, writeConfigFile } from '../../config/loader'; import type { Config } from '../../config/schema'; import type { GlobalFlags } from '../../types/flags'; -const VALID_KEYS = ['region', 'base_url', 'output', 'timeout', 'api_key', 'default_text_model', 'default_speech_model', 'default_video_model', 'default_music_model']; +const VALID_KEYS = ['region', 'base_url', 'output', 'timeout', 'api_key', 'proxy', 'default_text_model', 'default_speech_model', 'default_video_model', 'default_music_model']; // Allow hyphen-style keys (e.g. default-text-model → default_text_model) const KEY_ALIASES: Record = { @@ -21,12 +21,13 @@ export default defineCommand({ description: 'Set a config value', usage: 'mmx config set --key --value ', options: [ - { flag: '--key ', description: 'Config key (region, base_url, output, timeout, api_key, default_text_model, default_speech_model, default_video_model, default_music_model)' }, + { flag: '--key ', description: 'Config key (region, base_url, output, timeout, api_key, proxy, default_text_model, default_speech_model, default_video_model, default_music_model)' }, { flag: '--value ', description: 'Value to set' }, ], examples: [ 'mmx config set --key output --value json', 'mmx config set --key timeout --value 600', + 'mmx config set --key proxy --value http://127.0.0.1:7890', 'mmx config set --key base_url --value https://api-uw.minimax.io', ], async run(config: Config, flags: GlobalFlags) { @@ -67,6 +68,20 @@ export default defineCommand({ ); } + if (resolvedKey === 'base_url' && !value.startsWith('http')) { + throw new CLIError( + `Invalid base_url "${value}". Must start with http.`, + ExitCode.USAGE, + ); + } + + if (resolvedKey === 'proxy' && !value.startsWith('http')) { + throw new CLIError( + `Invalid proxy "${value}". Must be a URL starting with http (e.g. http://127.0.0.1:7890).`, + ExitCode.USAGE, + ); + } + if (resolvedKey === 'timeout') { const num = Number(value); if (isNaN(num) || num <= 0) { diff --git a/src/commands/config/show.ts b/src/commands/config/show.ts index c431f09..a241173 100644 --- a/src/commands/config/show.ts +++ b/src/commands/config/show.ts @@ -36,6 +36,7 @@ export default defineCommand({ if (file.default_speech_model) result.default_speech_model = file.default_speech_model; if (file.default_video_model) result.default_video_model = file.default_video_model; if (file.default_music_model) result.default_music_model = file.default_music_model; + if (file.proxy) result.proxy = file.proxy; console.log(formatOutput(result, format)); }, diff --git a/src/config/schema.ts b/src/config/schema.ts index eb075be..38f3445 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -16,6 +16,7 @@ export interface ConfigFile { base_url?: string; output?: 'text' | 'json'; timeout?: number; + proxy?: string; default_text_model?: string; default_speech_model?: string; default_video_model?: string; @@ -35,6 +36,7 @@ export function parseConfigFile(raw: unknown): ConfigFile { if (typeof obj.base_url === 'string' && obj.base_url.startsWith('http')) out.base_url = obj.base_url; if (typeof obj.output === 'string' && VALID_OUTPUTS.has(obj.output)) out.output = obj.output as ConfigFile['output']; if (typeof obj.timeout === 'number' && obj.timeout > 0) out.timeout = obj.timeout; + if (typeof obj.proxy === 'string' && obj.proxy.startsWith('http')) out.proxy = obj.proxy; if (typeof obj.default_text_model === 'string' && obj.default_text_model.length > 0) out.default_text_model = obj.default_text_model; if (typeof obj.default_speech_model === 'string' && obj.default_speech_model.length > 0) out.default_speech_model = obj.default_speech_model; if (typeof obj.default_video_model === 'string' && obj.default_video_model.length > 0) out.default_video_model = obj.default_video_model; diff --git a/src/errors/handler.ts b/src/errors/handler.ts index acf978a..871e553 100644 --- a/src/errors/handler.ts +++ b/src/errors/handler.ts @@ -40,7 +40,8 @@ export function handleError(err: unknown): never { const networkErr = new CLIError( "Network request failed.", ExitCode.NETWORK, - "Check your network connection and proxy settings. Also verify MINIMAX_BASE_URL is a valid URL.", + "Check your network connection.\n" + + "To use a proxy: set HTTPS_PROXY env var, or run: mmx config set --key proxy --value http://HOST:PORT", ); return handleError(networkErr); } @@ -63,10 +64,13 @@ export function handleError(err: unknown): never { msg.includes("eai_AGAIN"); if (isNetworkError) { - let hint = "Check your network connection and proxy settings."; + let hint = + "Check your network connection.\n" + + "To use a proxy: set HTTPS_PROXY env var, or run: mmx config set --key proxy --value http://HOST:PORT"; if (msg.includes("proxy")) { hint = - "Proxy error — check HTTP_PROXY / HTTPS_PROXY environment variables and proxy authentication."; + "Proxy connection failed — verify your proxy URL and authentication.\n" + + "Check: HTTPS_PROXY / HTTP_PROXY env vars, or mmx config show for configured proxy."; } const networkErr = new CLIError( "Network request failed.", diff --git a/src/main.ts b/src/main.ts index 67bd2c3..0ad712f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,7 @@ import { checkForUpdate, getPendingUpdateNotification } from './update/checker'; import { loadCredentials } from './auth/credentials'; import { ensureApiKey } from './auth/setup'; import { CLI_VERSION } from './version'; +import { ProxyAgent, setGlobalDispatcher } from 'undici'; // Handle Ctrl+C gracefully process.on('SIGINT', () => { @@ -42,9 +43,19 @@ async function main() { const commandPath = scanCommandPath(argv, GLOBAL_OPTIONS); + // Proxy: env vars take precedence over config file + const rawConfig = readConfigFile(); + const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy + || process.env.HTTP_PROXY || process.env.http_proxy + || process.env.ALL_PROXY || process.env.all_proxy + || rawConfig.proxy; + if (proxyUrl) { + setGlobalDispatcher(new ProxyAgent(proxyUrl)); + } + if (argv.includes('--help') || argv.includes('-h')) { const ri = argv.indexOf('--region'); - const region = ((ri >= 0 && argv[ri + 1]) || process.env.MINIMAX_REGION || readConfigFile().region || 'global') as Region; + const region = ((ri >= 0 && argv[ri + 1]) || process.env.MINIMAX_REGION || rawConfig.region || 'global') as Region; registry.printHelp(commandPath, process.stderr, region); process.exit(0); }