diff --git a/package-lock.json b/package-lock.json index 092190a..a2efb7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,30 @@ { "name": "mmx-cli", - "version": "1.0.5", + "version": "1.0.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mmx-cli", - "version": "1.0.5", + "version": "1.0.13", "dependencies": { "@clack/prompts": "^0.7.0", - "yaml": "^2.7.1" + "bun-plugin-dts": "^0.4.0", + "es-toolkit": "^1.46.1", + "undici": "^6.21.1" }, "bin": { "mmx": "dist/mmx.mjs" }, "devDependencies": { + "@eslint/js": "^9.0.0", "@types/bun": "latest", "eslint": "^9.24.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "typescript-eslint": "^8.58.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/@clack/core": { @@ -286,6 +293,288 @@ "undici-types": "~7.18.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/type-utils": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://npmmirror.xaminim.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://npmmirror.xaminim.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://npmmirror.xaminim.com/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://npmmirror.xaminim.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://npmmirror.xaminim.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", @@ -326,11 +615,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://npmmirror.xaminim.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -367,6 +664,17 @@ "concat-map": "0.0.1" } }, + "node_modules/bun-plugin-dts": { + "version": "0.4.0", + "resolved": "https://npmmirror.xaminim.com/bun-plugin-dts/-/bun-plugin-dts-0.4.0.tgz", + "integrity": "sha512-g/pHy9SuhnUw+E+bHnJvADOnnZlEIci3nvZY8EuQEMwkpC4V4Kmoa2nG9nfda4jmjj+0POlCRCjdqXrL9gjYtA==", + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "dts-bundle-generator": "^9.5.1", + "get-tsconfig": "^4.13.6" + } + }, "node_modules/bun-types": { "version": "1.3.11", "resolved": "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.11.tgz", @@ -404,11 +712,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://npmmirror.xaminim.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -421,9 +742,14 @@ "version": "1.1.4", "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://npmmirror.xaminim.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "license": "ISC" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", @@ -471,6 +797,47 @@ "dev": true, "license": "MIT" }, + "node_modules/dts-bundle-generator": { + "version": "9.5.1", + "resolved": "https://npmmirror.xaminim.com/dts-bundle-generator/-/dts-bundle-generator-9.5.1.tgz", + "integrity": "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA==", + "license": "MIT", + "dependencies": { + "typescript": ">=5.0.2", + "yargs": "^17.6.0" + }, + "bin": { + "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://npmmirror.xaminim.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://npmmirror.xaminim.com/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://npmmirror.xaminim.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -659,6 +1026,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://npmmirror.xaminim.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -710,6 +1095,27 @@ "dev": true, "license": "ISC" }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://npmmirror.xaminim.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://npmmirror.xaminim.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", @@ -793,6 +1199,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://npmmirror.xaminim.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", @@ -1010,6 +1425,19 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://npmmirror.xaminim.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1030,6 +1458,15 @@ "node": ">=6" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://npmmirror.xaminim.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1040,6 +1477,28 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://npmmirror.xaminim.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://npmmirror.xaminim.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1069,6 +1528,32 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://npmmirror.xaminim.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://npmmirror.xaminim.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -1095,6 +1580,36 @@ "node": ">=8" } }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://npmmirror.xaminim.com/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://npmmirror.xaminim.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", @@ -1112,7 +1627,6 @@ "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -1122,6 +1636,39 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz", + "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.2", + "@typescript-eslint/parser": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "6.25.0", + "resolved": "https://npmmirror.xaminim.com/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", @@ -1165,19 +1712,57 @@ "node": ">=0.10.0" } }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://npmmirror.xaminim.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 14.6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/eemeli" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://npmmirror.xaminim.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://npmmirror.xaminim.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://npmmirror.xaminim.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" } }, "node_modules/yocto-queue": { diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 530f03e..842e469 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -2,27 +2,17 @@ import type { OAuthTokens } from './types'; import { CLIError } from '../errors/base'; import { ExitCode } from '../errors/codes'; -// OAuth configuration — exact endpoints TBD pending MiniMax OAuth docs +// OAuth configuration export interface OAuthConfig { clientId: string; - authorizationUrl: string; + clientName: string; tokenUrl: string; deviceCodeUrl: string; scopes: string[]; - callbackPort: number; } -const DEFAULT_OAUTH_CONFIG: OAuthConfig = { - clientId: 'mmx-cli', - authorizationUrl: 'https://platform.minimax.io/oauth/authorize', - tokenUrl: 'https://api.minimax.io/v1/oauth/token', - deviceCodeUrl: 'https://api.minimax.io/v1/oauth/device/code', - scopes: ['api'], - callbackPort: 18991, -}; - -export async function startBrowserFlow( - config: OAuthConfig = DEFAULT_OAUTH_CONFIG, +export async function startDeviceCodeFlow( + config: OAuthConfig, ): Promise { const { randomBytes, createHash } = await import('crypto'); const codeVerifier = randomBytes(32).toString('base64url'); @@ -30,175 +20,104 @@ export async function startBrowserFlow( .update(codeVerifier) .digest('base64url'); - const state = randomBytes(16).toString('hex'); - - const params = new URLSearchParams({ - client_id: config.clientId, - response_type: 'code', - redirect_uri: `http://localhost:${config.callbackPort}/callback`, - scope: config.scopes.join(' '), - state, - code_challenge: codeChallenge, - code_challenge_method: 'S256', - }); - - const authUrl = `${config.authorizationUrl}?${params}`; - - // Open browser using execFile/spawn instead of exec to prevent shell injection. - // exec() passes the string to a shell, so a crafted authUrl containing shell - // metacharacters (e.g. from a malicious authorization server redirect) could - // execute arbitrary commands. execFile/spawn bypass the shell entirely. (#79) - const { execFile, spawn } = await import('child_process'); - const platform = process.platform; - - if (platform === 'darwin') { - execFile('open', [authUrl]); - } else if (platform === 'win32') { - // On Windows, 'start' is a shell built-in — use cmd.exe /c start explicitly. - spawn('cmd.exe', ['/c', 'start', '', authUrl], { shell: false, detached: true }); - } else { - execFile('xdg-open', [authUrl]); - } - process.stderr.write('Opening browser to authenticate with MiniMax...\n'); - - // Start local server to receive callback - const code = await waitForCallback(config.callbackPort, state); - - // Exchange code for tokens - const tokenRes = await fetch(config.tokenUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - code, - client_id: config.clientId, - redirect_uri: `http://localhost:${config.callbackPort}/callback`, - code_verifier: codeVerifier, - }), - }); - - if (!tokenRes.ok) { - const body = await tokenRes.text(); - throw new CLIError( - `OAuth token exchange failed: ${body}`, - ExitCode.AUTH, - ); - } - - return (await tokenRes.json()) as OAuthTokens; -} + const state = randomBytes(16).toString('base64url'); -async function waitForCallback(port: number, expectedState: string): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - server.stop(); - reject(new CLIError('OAuth callback timed out.', ExitCode.TIMEOUT)); - }, 120_000); - - const server = Bun.serve({ - port, - fetch(req) { - const url = new URL(req.url); - if (url.pathname !== '/callback') { - return new Response('Not found', { status: 404 }); - } - - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const error = url.searchParams.get('error'); - - if (error) { - clearTimeout(timeout); - server.stop(); - reject(new CLIError(`OAuth error: ${error}`, ExitCode.AUTH)); - return new Response( - '

Authentication Failed

You can close this tab.

', - { headers: { 'Content-Type': 'text/html' } }, - ); - } - - if (!code || state !== expectedState) { - clearTimeout(timeout); - server.stop(); - reject(new CLIError('Invalid OAuth callback.', ExitCode.AUTH)); - return new Response('Invalid callback', { status: 400 }); - } - - clearTimeout(timeout); - server.stop(); - resolve(code); - return new Response( - '

Authentication Successful

You can close this tab.

', - { headers: { 'Content-Type': 'text/html' } }, - ); - }, - }); - }); -} + const lane = process.env.BEDROCK_LANE; + const extraHeaders: Record = lane ? { bedrock_lane: lane } : {}; + if (process.env.X_USER_PRE) extraHeaders['X-User-Pre'] = 'true'; -export async function startDeviceCodeFlow( - config: OAuthConfig = DEFAULT_OAUTH_CONFIG, -): Promise { - // Request device code + // Request device code with PKCE const codeRes = await fetch(config.deviceCodeUrl, { method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...extraHeaders }, body: new URLSearchParams({ client_id: config.clientId, scope: config.scopes.join(' '), + code_challenge: codeChallenge, + code_challenge_method: 'S256', + state, }), }); if (!codeRes.ok) { + const body = await codeRes.text().catch(() => ''); throw new CLIError( - 'Failed to start device code flow.', + `Failed to start device code flow: HTTP ${codeRes.status} ${body}`, ExitCode.AUTH, + `URL: ${config.deviceCodeUrl}`, ); } - const { device_code, user_code, verification_uri, interval, expires_in } = - (await codeRes.json()) as { - device_code: string; - user_code: string; - verification_uri: string; - interval: number; - expires_in: number; - }; + const data = (await codeRes.json()) as { + user_code: string; + verification_uri: string; + expired_in: number; // Unix timestamp (ms) + interval: number; // milliseconds + state: string; + }; + + if (data.state !== state) { + throw new CLIError('OAuth state mismatch: possible CSRF attack.', ExitCode.AUTH); + } + + const url = data.verification_uri; - process.stderr.write(`\nVisit: ${verification_uri}\n`); - process.stderr.write(`Enter code: ${user_code}\n`); + const { exec } = await import('child_process'); + const openCmd = process.platform === 'darwin' ? 'open' : + process.platform === 'win32' ? 'start' : 'xdg-open'; + exec(`${openCmd} "${url}"`); + + process.stderr.write(`\nOpened: ${url}\n`); + process.stderr.write(`Enter code: ${data.user_code}\n`); process.stderr.write('Waiting for authorization...\n'); - // Poll for authorization - const deadline = Date.now() + expires_in * 1000; - const pollInterval = (interval || 5) * 1000; + // Poll for authorization (expired_in is Unix timestamp in ms) + const deadline = data.expired_in; + const pollInterval = data.interval || 5000; while (Date.now() < deadline) { await new Promise(r => setTimeout(r, pollInterval)); const tokenRes = await fetch(config.tokenUrl, { method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...extraHeaders }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', - device_code, client_id: config.clientId, + user_code: data.user_code, + code_verifier: codeVerifier, }), }); - if (tokenRes.ok) { - return (await tokenRes.json()) as OAuthTokens; + if (!tokenRes.ok) { + throw new CLIError( + `Device code authorization failed: HTTP ${tokenRes.status}`, + ExitCode.AUTH, + ); } - const err = (await tokenRes.json()) as { error: string }; - if (err.error === 'authorization_pending') continue; - if (err.error === 'slow_down') { - await new Promise(r => setTimeout(r, 5000)); - continue; + const tokenData = (await tokenRes.json()) as { + status: string; + access_token?: string; + refresh_token?: string; + expired_in?: number; + resource_url?: string; + }; + + if (tokenData.status === 'success' && tokenData.access_token) { + return { + access_token: tokenData.access_token, + refresh_token: tokenData.refresh_token ?? '', + expired_in: tokenData.expired_in ?? 0, + token_type: 'Bearer', + resource_url: tokenData.resource_url, + }; } + if (tokenData.status === 'pending') continue; + throw new CLIError( - `Device code authorization failed: ${err.error}`, + `Device code authorization failed: ${tokenData.status}`, ExitCode.AUTH, ); } diff --git a/src/auth/refresh.ts b/src/auth/refresh.ts index e544dea..b0eb594 100644 --- a/src/auth/refresh.ts +++ b/src/auth/refresh.ts @@ -3,31 +3,37 @@ import { saveCredentials } from "./credentials"; import { CLIError } from "../errors/base"; import { ExitCode } from "../errors/codes"; -// OAuth config — endpoints TBD pending MiniMax OAuth documentation -const TOKEN_URL = "https://api.minimax.io/v1/oauth/token"; +const DEFAULT_TOKEN_URL = 'https://account.minimax.io/oauth2/token'; +const DEFAULT_CLIENT_ID = '659cf4c1-615c-45f6-a5f6-4bf15eb476e5'; const MAX_REFRESH_RETRIES = 2; const RETRY_DELAY_MS = 500; export async function refreshAccessToken( refreshToken: string, + tokenUrl: string = DEFAULT_TOKEN_URL, + clientId: string = DEFAULT_CLIENT_ID, ): Promise { + const lane = process.env.BEDROCK_LANE; + const extraHeaders: Record = lane ? { bedrock_lane: lane } : {}; + if (process.env.X_USER_PRE) extraHeaders['X-User-Pre'] = 'true'; + let lastErr: Error | null = null; for (let attempt = 0; attempt <= MAX_REFRESH_RETRIES; attempt++) { if (attempt > 0) { - // Exponential backoff before retry await new Promise(r => setTimeout(r, RETRY_DELAY_MS * attempt)); } let res: Response; try { - res = await fetch(TOKEN_URL, { + res = await fetch(tokenUrl, { method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, + headers: { "Content-Type": "application/x-www-form-urlencoded", ...extraHeaders }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, + client_id: clientId, }), signal: AbortSignal.timeout(10_000), }); @@ -42,11 +48,10 @@ export async function refreshAccessToken( ? "Token refresh timed out — auth server did not respond within 10 s." : `Token refresh failed: ${err instanceof Error ? err.message : String(err)}`, ); - continue; // retry + continue; } if (!res.ok) { - // 4xx errors won't recover with retry if (res.status >= 400 && res.status < 500) { throw new CLIError( "OAuth session expired and could not be refreshed.", @@ -55,14 +60,34 @@ export async function refreshAccessToken( ); } lastErr = new Error(`Token refresh failed: HTTP ${res.status}`); - continue; // retry 5xx errors + continue; + } + + const body = (await res.json()) as { + status: string; + access_token?: string; + refresh_token?: string; + expired_in?: number; + resource_url?: string; + }; + + if (body.status !== 'success' || !body.access_token) { + throw new CLIError( + 'OAuth refresh failed.', + ExitCode.AUTH, + 'Re-authenticate: mmx auth login', + ); } - const data = (await res.json()) as OAuthTokens; - return data; + return { + access_token: body.access_token, + refresh_token: body.refresh_token ?? refreshToken, + expired_in: body.expired_in ?? 0, + token_type: 'Bearer', + resource_url: body.resource_url, + }; } - // All retries exhausted throw new CLIError( `Token refresh failed after ${MAX_REFRESH_RETRIES + 1} attempts: ${lastErr?.message}`, ExitCode.AUTH, @@ -70,22 +95,26 @@ export async function refreshAccessToken( ); } -export async function ensureFreshToken(creds: CredentialFile): Promise { +export async function ensureFreshToken( + creds: CredentialFile, + tokenUrl?: string, + clientId?: string, +): Promise { const expiresAt = new Date(creds.expires_at).getTime(); - const bufferMs = 5 * 60 * 1000; // 5 minutes + const bufferMs = 5 * 60 * 1000; if (Date.now() < expiresAt - bufferMs) { return creds.access_token; } - // Token expired or about to expire — refresh - const tokens = await refreshAccessToken(creds.refresh_token); + const tokens = await refreshAccessToken(creds.refresh_token, tokenUrl, clientId); const updated: CredentialFile = { access_token: tokens.access_token, refresh_token: tokens.refresh_token, - expires_at: new Date(Date.now() + tokens.expires_in * 1000).toISOString(), - token_type: "Bearer", + expires_at: new Date(tokens.expired_in).toISOString(), // expired_in is Unix timestamp (ms) + token_type: 'Bearer', + resource_url: tokens.resource_url ?? creds.resource_url, account: creds.account, }; diff --git a/src/auth/resolver.ts b/src/auth/resolver.ts index eb9a11a..d28cef8 100644 --- a/src/auth/resolver.ts +++ b/src/auth/resolver.ts @@ -14,7 +14,7 @@ export async function resolveCredential(config: Config): Promise { +export async function ensureAuth(config: Config): Promise { if (config.apiKey || config.fileApiKey) return; + // Check existing OAuth credentials + const existingOAuth = await loadCredentials(); + if (existingOAuth) return; + const envKey = process.env.MINIMAX_API_KEY; let key: string | undefined; @@ -26,11 +33,48 @@ export async function ensureApiKey(config: Config): Promise { if (!key) { if (!isInteractive({ nonInteractive: config.nonInteractive })) { throw new CLIError( - 'No API key found.', + 'No credentials found.', ExitCode.AUTH, - 'Set env var: export MINIMAX_API_KEY=sk-xxxxx\nPass directly: --api-key sk-xxxxx', + 'Log in: mmx auth login\nPass directly: --api-key sk-xxxxx', ); } + + const { select } = await import('@clack/prompts'); + const method = await select({ + message: 'How would you like to authenticate?', + options: [ + { value: 'oauth', label: 'Log in with MiniMax account (OAuth)' }, + { value: 'api-key', label: 'Enter API key manually' }, + ], + }); + + if (typeof method === 'symbol') { + // User pressed Ctrl+C + throw new CLIError('Authentication cancelled.', ExitCode.AUTH); + } + + if (method === 'oauth') { + const oauthConfig: OAuthConfig = { + clientId: '659cf4c1-615c-45f6-a5f6-4bf15eb476e5', + clientName: 'MiniMax CLI', + tokenUrl: `${config.oauthApiHost}/oauth2/token`, + deviceCodeUrl: `${config.oauthApiHost}/oauth2/device/code`, + scopes: ['openid', 'profile', 'coding_plan'], + }; + const tokens = await startDeviceCodeFlow(oauthConfig); + const creds: CredentialFile = { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + expires_at: new Date(tokens.expired_in).toISOString(), + token_type: 'Bearer', + resource_url: tokens.resource_url, + }; + await saveCredentials(creds); + process.stderr.write('Logged in successfully.\n'); + return; + } + + // api-key method const input = await promptText({ message: 'Enter your MiniMax API key:' }); if (!input) throw new CLIError('API key is required.', ExitCode.AUTH); key = input; diff --git a/src/auth/types.ts b/src/auth/types.ts index 0f39fbe..4b87289 100644 --- a/src/auth/types.ts +++ b/src/auth/types.ts @@ -3,8 +3,9 @@ export type AuthMethod = 'api-key' | 'oauth'; export interface OAuthTokens { access_token: string; refresh_token: string; - expires_in: number; + expired_in: number; // milliseconds token_type: 'Bearer'; + resource_url?: string; } export interface CredentialFile { @@ -12,6 +13,7 @@ export interface CredentialFile { refresh_token: string; expires_at: string; // ISO 8601 token_type: 'Bearer'; + resource_url?: string; account?: string; } diff --git a/src/client/http.ts b/src/client/http.ts index c77ba79..eb2f2a2 100644 --- a/src/client/http.ts +++ b/src/client/http.ts @@ -26,6 +26,14 @@ export async function request(config: Config, opts: RequestOpts): Promise', description: 'Auth method: oauth (default), api-key' }, { flag: '--api-key ', description: 'API key to store' }, - { flag: '--no-browser', description: 'Use device-code flow instead of browser' }, ], examples: [ 'mmx auth login', - 'mmx auth login --no-browser', 'mmx auth login --api-key sk-xxxxx', 'mmx auth login --method api-key --api-key sk-xxxxx', ], @@ -112,18 +111,45 @@ export default defineCommand({ return; } - let tokens; - if (flags.noBrowser) { - tokens = await startDeviceCodeFlow(); - } else { - tokens = await startBrowserFlow(); + // If no region was explicitly specified via flag or env, let the user choose interactively + let region = config.region; + if (!flags.region && !process.env.MINIMAX_REGION) { + if (isInteractive({ nonInteractive: config.nonInteractive })) { + const { select } = await import('@clack/prompts'); + const chosen = await select({ + message: 'Select your region', + options: [ + { value: 'cn', label: 'minimax.com (China)' }, + { value: 'global', label: 'minimax.io (Global)' }, + ], + }); + if (typeof chosen === 'string') { + region = chosen as Region; + const existing = readConfigFile() as Record; + existing.region = region; + await writeConfigFile(existing); + } + } } + const oauthApiHost = process.env.MINIMAX_AUTH_URL || OAUTH_API_HOSTS[region]; + + const oauthConfig: OAuthConfig = { + clientId: '659cf4c1-615c-45f6-a5f6-4bf15eb476e5', + clientName: 'MiniMax CLI', + tokenUrl: `${oauthApiHost}/oauth2/token`, + deviceCodeUrl: `${oauthApiHost}/oauth2/device/code`, + scopes: ['openid', 'profile', 'coding_plan'], + }; + + const tokens = await startDeviceCodeFlow(oauthConfig); + const creds: CredentialFile = { access_token: tokens.access_token, refresh_token: tokens.refresh_token, - expires_at: new Date(Date.now() + tokens.expires_in * 1000).toISOString(), + expires_at: new Date(tokens.expired_in).toISOString(), // expired_in is Unix timestamp (ms) token_type: 'Bearer', + resource_url: tokens.resource_url, }; await saveCredentials(creds); diff --git a/src/commands/auth/refresh.ts b/src/commands/auth/refresh.ts index 2328062..db8dd18 100644 --- a/src/commands/auth/refresh.ts +++ b/src/commands/auth/refresh.ts @@ -31,13 +31,17 @@ export default defineCommand({ return; } - const tokens = await refreshAccessToken(creds.refresh_token); + const tokens = await refreshAccessToken( + creds.refresh_token, + `${config.oauthApiHost}/oauth2/token`, + ); const updated: CredentialFile = { access_token: tokens.access_token, refresh_token: tokens.refresh_token, - expires_at: new Date(Date.now() + tokens.expires_in * 1000).toISOString(), + expires_at: new Date(tokens.expired_in).toISOString(), // expired_in is Unix timestamp (ms) token_type: 'Bearer', + resource_url: tokens.resource_url ?? creds.resource_url, account: creds.account, }; diff --git a/src/config/loader.ts b/src/config/loader.ts index 5d40ec9..74aedd8 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -1,6 +1,6 @@ import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs'; -import { parseConfigFile, REGIONS, type Config, type ConfigFile, type Region } from './schema'; -import { ensureConfigDir, getConfigPath } from './paths'; +import { parseConfigFile, REGIONS, OAUTH_API_HOSTS, type Config, type ConfigFile, type Region } from './schema'; +import { ensureConfigDir, getConfigPath, getCredentialsPath } from './paths'; import { detectOutputFormat, type OutputFormat } from '../output/formatter'; import { CLIError } from '../errors/base'; import { ExitCode } from '../errors/codes'; @@ -28,6 +28,17 @@ export async function writeConfigFile(data: Record): Promise = { + cn: 'https://account.minimaxi.com', + global: 'https://account.minimax.io', +}; export interface ConfigFile { api_key?: string; region?: Region; @@ -52,6 +56,7 @@ export interface Config { configPath?: string; region: Region; baseUrl: string; + oauthApiHost: string; output: 'text' | 'json'; timeout: number; defaultTextModel?: string; diff --git a/src/main.ts b/src/main.ts index 0ad712f..b1dff49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,7 @@ import { detectRegion, saveDetectedRegion } from './config/detect-region'; import { REGIONS, type Region } from './config/schema'; import { checkForUpdate, getPendingUpdateNotification } from './update/checker'; import { loadCredentials } from './auth/credentials'; -import { ensureApiKey } from './auth/setup'; +import { ensureAuth } from './auth/setup'; import { CLI_VERSION } from './version'; import { ProxyAgent, setGlobalDispatcher } from 'undici'; @@ -75,7 +75,8 @@ async function main() { await quotaCmd.execute(config, flags); } else { process.stderr.write(' Not logged in.\n'); - process.stderr.write(' mmx auth login --api-key sk-xxxxx\n\n'); + process.stderr.write(' mmx auth login Log in with MiniMax account by OAuth\n'); + process.stderr.write(' mmx auth login --api-key Log in with API key\n\n'); } process.exit(0); } @@ -91,7 +92,7 @@ async function main() { (cmd) => cmd.every((c, i) => commandPath[i] === c), ); if (needsAuthSetup) { - await ensureApiKey(config); + await ensureAuth(config); } if (config.needsRegionDetection) { diff --git a/test/auth/resolver.test.ts b/test/auth/resolver.test.ts index f967803..8187498 100644 --- a/test/auth/resolver.test.ts +++ b/test/auth/resolver.test.ts @@ -5,6 +5,7 @@ import type { Config } from '../../src/config/schema'; function makeConfig(overrides: Partial = {}): Config { return { region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text', timeout: 300, diff --git a/test/client/http.test.ts b/test/client/http.test.ts index 07c0aeb..31c948d 100644 --- a/test/client/http.test.ts +++ b/test/client/http.test.ts @@ -8,6 +8,7 @@ function makeConfig(baseUrl: string): Config { return { apiKey: 'test-api-key', region: 'global', + oauthApiHost: 'https://account.minimax.io', baseUrl, output: 'text', timeout: 10, diff --git a/test/commands/auth/login.test.ts b/test/commands/auth/login.test.ts index 8095acc..4b4ce77 100644 --- a/test/commands/auth/login.test.ts +++ b/test/commands/auth/login.test.ts @@ -10,6 +10,7 @@ describe('auth login command', () => { it('requires api key when method is api-key', async () => { const config = { region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/auth/logout.test.ts b/test/commands/auth/logout.test.ts index 32bfdbf..d577025 100644 --- a/test/commands/auth/logout.test.ts +++ b/test/commands/auth/logout.test.ts @@ -9,6 +9,7 @@ describe('auth logout command', () => { it('handles dry run', async () => { const config = { region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/auth/refresh.test.ts b/test/commands/auth/refresh.test.ts index 56d8fa6..4a6890e 100644 --- a/test/commands/auth/refresh.test.ts +++ b/test/commands/auth/refresh.test.ts @@ -9,6 +9,7 @@ describe('auth refresh command', () => { it('errors when not using OAuth', async () => { const config = { region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/auth/status.test.ts b/test/commands/auth/status.test.ts index ae93f2f..bcc41e2 100644 --- a/test/commands/auth/status.test.ts +++ b/test/commands/auth/status.test.ts @@ -9,6 +9,7 @@ describe('auth status command', () => { it('shows not authenticated when no credentials', async () => { const config = { region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'json' as const, timeout: 10, diff --git a/test/commands/config/set.test.ts b/test/commands/config/set.test.ts index 53f085e..a5439f9 100644 --- a/test/commands/config/set.test.ts +++ b/test/commands/config/set.test.ts @@ -15,6 +15,7 @@ describe('config set command', () => { it('requires key and value', async () => { const config = { region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -44,6 +45,7 @@ describe('config set command', () => { it('validates config key', async () => { const config = { region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/config/show.test.ts b/test/commands/config/show.test.ts index e063b7c..6576883 100644 --- a/test/commands/config/show.test.ts +++ b/test/commands/config/show.test.ts @@ -21,6 +21,7 @@ describe('config show command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'json' as const, timeout: 300, diff --git a/test/commands/file/upload.test.ts b/test/commands/file/upload.test.ts index c48994d..d51e344 100644 --- a/test/commands/file/upload.test.ts +++ b/test/commands/file/upload.test.ts @@ -10,6 +10,7 @@ describe('file upload command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/image/generate.test.ts b/test/commands/image/generate.test.ts index fb29b6a..f776df9 100644 --- a/test/commands/image/generate.test.ts +++ b/test/commands/image/generate.test.ts @@ -10,6 +10,7 @@ describe('image generate command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/music/generate.test.ts b/test/commands/music/generate.test.ts index e5d0b16..de3825c 100644 --- a/test/commands/music/generate.test.ts +++ b/test/commands/music/generate.test.ts @@ -4,6 +4,7 @@ import { default as generateCommand } from '../../../src/commands/music/generate const baseConfig = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/quota/show.test.ts b/test/commands/quota/show.test.ts index ebb88fd..9f7fdec 100644 --- a/test/commands/quota/show.test.ts +++ b/test/commands/quota/show.test.ts @@ -10,6 +10,7 @@ describe('quota show command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/search/query.test.ts b/test/commands/search/query.test.ts index 6784be1..44fa942 100644 --- a/test/commands/search/query.test.ts +++ b/test/commands/search/query.test.ts @@ -10,6 +10,7 @@ describe('search query command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/speech/synthesize.test.ts b/test/commands/speech/synthesize.test.ts index 8a49063..304a11d 100644 --- a/test/commands/speech/synthesize.test.ts +++ b/test/commands/speech/synthesize.test.ts @@ -10,6 +10,7 @@ describe('speech synthesize command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -40,6 +41,7 @@ describe('speech synthesize command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'json' as const, timeout: 10, diff --git a/test/commands/text/chat.test.ts b/test/commands/text/chat.test.ts index 37ad015..e5751ca 100644 --- a/test/commands/text/chat.test.ts +++ b/test/commands/text/chat.test.ts @@ -23,6 +23,7 @@ describe('text chat command', () => { const config: Config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: server.url, output: 'json', timeout: 10, @@ -66,6 +67,7 @@ describe('text chat command', () => { const config: Config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'json', timeout: 10, diff --git a/test/commands/video/download.test.ts b/test/commands/video/download.test.ts index dc8b14e..aad6464 100644 --- a/test/commands/video/download.test.ts +++ b/test/commands/video/download.test.ts @@ -10,6 +10,7 @@ describe('video download command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -40,6 +41,7 @@ describe('video download command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/video/generate.test.ts b/test/commands/video/generate.test.ts index 9845a09..7c564d8 100644 --- a/test/commands/video/generate.test.ts +++ b/test/commands/video/generate.test.ts @@ -10,6 +10,7 @@ describe('video generate command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/video/task-get.test.ts b/test/commands/video/task-get.test.ts index 29046d8..114a58f 100644 --- a/test/commands/video/task-get.test.ts +++ b/test/commands/video/task-get.test.ts @@ -18,6 +18,7 @@ describe('video task get command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -54,6 +55,7 @@ describe('video task get command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: server.url, output: 'json' as const, timeout: 10, diff --git a/test/commands/vision/describe.test.ts b/test/commands/vision/describe.test.ts index 77db40f..1dfb2da 100644 --- a/test/commands/vision/describe.test.ts +++ b/test/commands/vision/describe.test.ts @@ -10,6 +10,7 @@ describe('vision describe command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -40,6 +41,7 @@ describe('vision describe command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10,