Skip to content
Merged
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
18 changes: 16 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@ on:
tags: ['v*']

jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run typecheck
- run: bun test

release:
needs: test
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
Expand All @@ -17,5 +29,7 @@ jobs:
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: dist/minimax-*
files: |
dist/minimax-*
dist/manifest.json
generate_release_notes: true
39 changes: 28 additions & 11 deletions build.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
import { $ } from 'bun';
import { createHash } from 'crypto';
import { readFileSync, writeFileSync } from 'fs';

const VERSION = process.env.VERSION ?? 'dev';

const targets = [
{ target: 'bun-linux-x64', output: 'minimax-linux-x64' },
{ target: 'bun-linux-arm64', output: 'minimax-linux-arm64' },
{ target: 'bun-darwin-x64', output: 'minimax-darwin-x64' },
{ target: 'bun-darwin-arm64', output: 'minimax-darwin-arm64' },
{ target: 'bun-windows-x64', output: 'minimax-windows-x64.exe' },
{ bunTarget: 'bun-linux-x64', platform: 'linux-x64', output: 'minimax-linux-x64' },
{ bunTarget: 'bun-linux-x64-musl', platform: 'linux-x64-musl', output: 'minimax-linux-x64-musl' },
{ bunTarget: 'bun-linux-arm64', platform: 'linux-arm64', output: 'minimax-linux-arm64' },
{ bunTarget: 'bun-linux-arm64-musl', platform: 'linux-arm64-musl', output: 'minimax-linux-arm64-musl' },
{ bunTarget: 'bun-darwin-x64', platform: 'darwin-x64', output: 'minimax-darwin-x64' },
{ bunTarget: 'bun-darwin-arm64', platform: 'darwin-arm64', output: 'minimax-darwin-arm64' },
{ bunTarget: 'bun-windows-x64', platform: 'windows-x64', output: 'minimax-windows-x64.exe' },
];

function sha256(path: string): string {
return createHash('sha256').update(readFileSync(path)).digest('hex');
}

console.log(`Building minimax-cli ${VERSION}...\n`);

for (const { target, output } of targets) {
console.log(` Building ${output}...`);
const manifest: {
version: string;
platforms: Record<string, { checksum: string }>;
} = { version: VERSION, platforms: {} };

for (const { bunTarget, platform, output } of targets) {
const outPath = `dist/${output}`;
process.stdout.write(` ${output}...`);
await $`bun build src/main.ts \
--compile \
--target ${target} \
--outfile dist/${output} \
--target ${bunTarget} \
--outfile ${outPath} \
--define "process.env.CLI_VERSION='${VERSION}'"`.quiet();
console.log(` ✓ dist/${output}`);
manifest.platforms[platform] = { checksum: sha256(outPath) };
console.log(' ✓');
}

console.log('\nDone. Binaries are in dist/');
writeFileSync('dist/manifest.json', JSON.stringify(manifest, null, 2));
console.log(' manifest.json ✓');
console.log(`\nDone. ${targets.length} binaries in dist/`);
137 changes: 122 additions & 15 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,22 +1,129 @@
#!/bin/sh
set -e

# Usage: install.sh [stable|latest|VERSION]
CHANNEL="${1:-stable}"

case "$CHANNEL" in
stable|latest) ;;
v*|[0-9]*) ;;
*)
echo "Usage: $0 [stable|latest|VERSION]" >&2
exit 1
;;
esac

REPO="MiniMax-AI-Dev/minimax-cli"
INSTALL_DIR="${MINIMAX_INSTALL_DIR:-/usr/local/bin}"

OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="x64" ;;
aarch64|arm64) ARCH="arm64" ;;
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;
INSTALL_DIR="${MINIMAX_INSTALL_DIR:-$HOME/.local/bin}"

# Dependency check: curl or wget
if command -v curl >/dev/null 2>&1; then
download() { curl -fsSL "$1"; }
download_to() { curl -fsSL -o "$2" "$1"; }
elif command -v wget >/dev/null 2>&1; then
download() { wget -qO- "$1"; }
download_to() { wget -qO "$2" "$1"; }
else
echo "curl or wget is required." >&2; exit 1
fi

# Detect OS
case "$(uname -s)" in
Darwin) OS="darwin" ;;
Linux) OS="linux" ;;
*) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac

# Detect architecture
case "$(uname -m)" in
x86_64|amd64) ARCH="x64" ;;
arm64|aarch64) ARCH="arm64" ;;
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

# Rosetta 2: x64 shell on ARM Mac → use native arm64 binary
if [ "$OS" = "darwin" ] && [ "$ARCH" = "x64" ]; then
if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then
ARCH="arm64"
fi
fi

# musl detection on Linux
PLATFORM="${OS}-${ARCH}"
if [ "$OS" = "linux" ]; then
if [ -f /lib/libc.musl-x86_64.so.1 ] || \
[ -f /lib/libc.musl-aarch64.so.1 ] || \
ldd /bin/ls 2>&1 | grep -q musl; then
PLATFORM="${OS}-${ARCH}-musl"
fi
fi

# Resolve version from channel
GH_API="https://api.github.com/repos/${REPO}"
case "$CHANNEL" in
stable)
VERSION=$(download "${GH_API}/releases/latest" \
| grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
;;
latest)
VERSION=$(download "${GH_API}/releases?per_page=1" \
| grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
;;
*)
case "$CHANNEL" in v*) VERSION="$CHANNEL" ;; *) VERSION="v${CHANNEL}" ;; esac
;;
esac

BINARY="minimax-${OS}-${ARCH}"
URL="https://github.com/${REPO}/releases/latest/download/${BINARY}"
if [ -z "$VERSION" ]; then
echo "Failed to resolve version." >&2; exit 1
fi

echo "Downloading ${BINARY}..."
curl -fsSL "$URL" -o "${INSTALL_DIR}/minimax"
chmod +x "${INSTALL_DIR}/minimax"
echo "Installed minimax to ${INSTALL_DIR}/minimax"
"${INSTALL_DIR}/minimax" --version
echo "Installing minimax ${VERSION} for ${PLATFORM}..."

# Fetch manifest and extract SHA256 (pure sh, no jq required)
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
MANIFEST=$(download "${BASE_URL}/manifest.json") || {
echo "Failed to fetch manifest.json" >&2; exit 1
}
CHECKSUM=$(printf '%s' "$MANIFEST" | tr -d '\n' | \
sed "s/.*\"${PLATFORM}\"[^}]*\"checksum\" *: *\"\([a-f0-9]*\)\".*/\1/")

if [ -z "$CHECKSUM" ] || [ "${#CHECKSUM}" -ne 64 ]; then
echo "Platform '${PLATFORM}' not found in manifest." >&2; exit 1
fi

# Download binary to temp file
TMP=$(mktemp)
trap 'rm -f "$TMP"' EXIT

download_to "${BASE_URL}/minimax-${PLATFORM}" "$TMP" || {
echo "Download failed." >&2; exit 1
}

# Verify SHA256
if command -v shasum >/dev/null 2>&1; then
ACTUAL=$(shasum -a 256 "$TMP" | cut -d' ' -f1)
elif command -v sha256sum >/dev/null 2>&1; then
ACTUAL=$(sha256sum "$TMP" | cut -d' ' -f1)
else
echo "shasum or sha256sum is required." >&2; exit 1
fi

if [ "$ACTUAL" != "$CHECKSUM" ]; then
echo "Checksum verification failed." >&2; exit 1
fi

chmod +x "$TMP"
mkdir -p "$INSTALL_DIR"
mv "$TMP" "${INSTALL_DIR}/minimax"

echo "Installed minimax ${VERSION} to ${INSTALL_DIR}/minimax"

# Warn if install dir is not in PATH
case ":${PATH}:" in
*":${INSTALL_DIR}:"*) ;;
*)
printf '\nNote: %s is not in PATH. Add to your shell profile:\n' "$INSTALL_DIR"
printf ' export PATH="%s:$PATH"\n\n' "$INSTALL_DIR"
;;
esac
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
"prepublishOnly": "bun run build:npm"
},
"dependencies": {
"yaml": "^2.7.1",
"zod": "^3.24.4"
"yaml": "^2.7.1"
},
"devDependencies": {
"typescript": "^5.8.3",
Expand Down
18 changes: 5 additions & 13 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import { startBrowserFlow, startDeviceCodeFlow } from '../../auth/oauth';
import { requestJson } from '../../client/http';
import { quotaEndpoint } from '../../client/endpoints';
import { formatOutput } from '../../output/formatter';
import { ensureConfigDir, getConfigPath } from '../../config/paths';
import { getConfigPath } from '../../config/paths';
import { readConfigFile, writeConfigFile } from '../../config/loader';
import type { Config } from '../../config/schema';
import type { GlobalFlags } from '../../types/flags';
import type { CredentialFile } from '../../auth/types';
import type { QuotaResponse } from '../../types/api';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { parse as parseYaml, stringify as yamlStringify } from 'yaml';

export default defineCommand({
name: 'auth login',
Expand Down Expand Up @@ -60,17 +59,10 @@ export default defineCommand({
}

// Store key in config.yaml
await ensureConfigDir();
const configPath = getConfigPath();
let existing: Record<string, unknown> = {};
if (existsSync(configPath)) {
try {
existing = parseYaml(readFileSync(configPath, 'utf-8')) || {};
} catch { /* ignore */ }
}
const existing = readConfigFile() as Record<string, unknown>;
existing.api_key = key;
writeFileSync(configPath, yamlStringify(existing), { mode: 0o600 });
process.stderr.write(`API key saved to ${configPath}\n`);
await writeConfigFile(existing);
process.stderr.write(`API key saved to ${getConfigPath()}\n`);
} else {
console.log('Would validate and save API key.');
}
Expand Down
20 changes: 6 additions & 14 deletions src/commands/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { defineCommand } from '../../command';
import { clearCredentials, loadCredentials } from '../../auth/credentials';
import { getConfigPath } from '../../config/paths';
import { readConfigFile, writeConfigFile } from '../../config/loader';
import type { Config } from '../../config/schema';
import type { GlobalFlags } from '../../types/flags';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { parse as parseYaml, stringify as yamlStringify } from 'yaml';

export default defineCommand({
name: 'auth logout',
Expand All @@ -17,13 +15,8 @@ export default defineCommand({
],
async run(config: Config, flags: GlobalFlags) {
const creds = await loadCredentials();
const configPath = getConfigPath();
const hasConfigKey = existsSync(configPath) && (() => {
try {
const parsed = parseYaml(readFileSync(configPath, 'utf-8'));
return parsed?.api_key;
} catch { return false; }
})();
const fileConfig = readConfigFile();
const hasConfigKey = !!fileConfig.api_key;

if (config.dryRun) {
if (creds) console.log('Would remove ~/.minimax/credentials.json');
Expand All @@ -40,10 +33,9 @@ export default defineCommand({

if (hasConfigKey) {
try {
const raw = readFileSync(configPath, 'utf-8');
const parsed = parseYaml(raw) || {};
delete parsed.api_key;
writeFileSync(configPath, yamlStringify(parsed), { mode: 0o600 });
const updated = fileConfig as Record<string, unknown>;
delete updated.api_key;
await writeConfigFile(updated);
process.stderr.write('Cleared api_key from ~/.minimax/config.yaml\n');
} catch { /* ignore */ }
}
Expand Down
25 changes: 4 additions & 21 deletions src/commands/config/set.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { defineCommand } from '../../command';
import { CLIError } from '../../errors/base';
import { ExitCode } from '../../errors/codes';
import { ensureConfigDir, getConfigPath } from '../../config/paths';
import { formatOutput, detectOutputFormat } from '../../output/formatter';
import { readConfigFile, writeConfigFile } from '../../config/loader';
import type { Config } from '../../config/schema';
import type { GlobalFlags } from '../../types/flags';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { parse as parseYaml, stringify as yamlStringify } from 'yaml';

const VALID_KEYS = ['region', 'base_url', 'output', 'timeout', 'api_key'];

Expand Down Expand Up @@ -74,24 +72,9 @@ export default defineCommand({
return;
}

await ensureConfigDir();
const configPath = getConfigPath();

let existing: Record<string, unknown> = {};
if (existsSync(configPath)) {
try {
existing = parseYaml(readFileSync(configPath, 'utf-8')) || {};
} catch { /* ignore */ }
}

// Convert numeric values
if (key === 'timeout') {
existing[key] = Number(value);
} else {
existing[key] = value;
}

writeFileSync(configPath, yamlStringify(existing), { mode: 0o600 });
const existing = readConfigFile() as Record<string, unknown>;
existing[key] = key === 'timeout' ? Number(value) : value;
await writeConfigFile(existing);

if (!config.quiet) {
console.log(formatOutput({ [key]: existing[key] }, format));
Expand Down
2 changes: 1 addition & 1 deletion src/commands/config/show.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineCommand } from '../../command';
import { loadConfigFile } from '../../config/loader';
import { readConfigFile as loadConfigFile } from '../../config/loader';
import { getConfigPath } from '../../config/paths';
import { formatOutput, detectOutputFormat } from '../../output/formatter';
import type { Config } from '../../config/schema';
Expand Down
Loading
Loading