Skip to content

Commit eb13031

Browse files
RyanLee-Devclaude
andcommitted
refactor: api key from file only, interactive setup, dedup utilities
Auth: - Remove env var fallback from resolver; file is sole persistent source - Add ensureApiKey() interactive setup: file → env confirm → prompt input - Add maskToken() utility, replacing 5 divergent inline implementations - Show config file path and key/region source in status bar Dedup: - Extract renderQuotaTable() shared by quota show and auth status - Extract saveAudioOutput() shared by speech/synthesize and music/generate - Extract readTextFromPathOrStdin() shared by speech and music - auth status now uses the same rich quota table as quota show Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6f08b75 commit eb13031

17 files changed

Lines changed: 313 additions & 402 deletions

File tree

src/auth/resolver.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,9 @@ export async function resolveCredential(config: Config): Promise<ResolvedCredent
2323
return { token: config.fileApiKey, method: 'api-key', source: 'config.json' };
2424
}
2525

26-
// 4. Environment variable
27-
if (config.envApiKey) {
28-
return { token: config.envApiKey, method: 'api-key', source: 'env' };
29-
}
30-
3126
throw new CLIError(
3227
'No credentials found.',
3328
ExitCode.AUTH,
34-
'Log in: minimax auth login\nSet env var: export MINIMAX_API_KEY=sk-xxxxx\nPass directly: --api-key sk-xxxxx',
29+
'Log in: minimax auth login\nPass directly: --api-key sk-xxxxx',
3530
);
3631
}

src/auth/setup.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Config } from '../config/schema';
2+
import { readConfigFile, writeConfigFile } from '../config/loader';
3+
import { promptText, promptConfirm } from '../utils/prompt';
4+
import { isInteractive } from '../utils/env';
5+
import { maskToken } from '../utils/token';
6+
import { CLIError } from '../errors/base';
7+
import { ExitCode } from '../errors/codes';
8+
9+
export async function ensureApiKey(config: Config): Promise<void> {
10+
if (config.apiKey || config.fileApiKey) return;
11+
12+
const envKey = process.env.MINIMAX_API_KEY;
13+
let key: string | undefined;
14+
15+
if (envKey) {
16+
if (!isInteractive({ nonInteractive: config.nonInteractive })) {
17+
key = envKey;
18+
} else {
19+
const use = await promptConfirm({
20+
message: `Found MINIMAX_API_KEY in environment (${maskToken(envKey)}). Save it to config file?`,
21+
});
22+
if (use) key = envKey;
23+
}
24+
}
25+
26+
if (!key) {
27+
if (!isInteractive({ nonInteractive: config.nonInteractive })) {
28+
throw new CLIError(
29+
'No API key found.',
30+
ExitCode.AUTH,
31+
'Set env var: export MINIMAX_API_KEY=sk-xxxxx\nPass directly: --api-key sk-xxxxx',
32+
);
33+
}
34+
const input = await promptText({ message: 'Enter your MiniMax API key:' });
35+
if (!input) throw new CLIError('API key is required.', ExitCode.AUTH);
36+
key = input;
37+
}
38+
39+
const data = { ...(readConfigFile() as Record<string, unknown>), api_key: key };
40+
await writeConfigFile(data);
41+
config.fileApiKey = key;
42+
43+
const path = config.configPath ?? '~/.minimax/config.json';
44+
process.stderr.write(`API key saved to ${path}\n`);
45+
}

src/commands/auth/login.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { quotaEndpoint } from '../../client/endpoints';
99
import { getConfigPath } from '../../config/paths';
1010
import { readConfigFile, writeConfigFile } from '../../config/loader';
1111
import { isInteractive } from '../../utils/env';
12+
import { maskToken } from '../../utils/token';
1213
import type { Config } from '../../config/schema';
1314
import type { GlobalFlags } from '../../types/flags';
1415
import type { CredentialFile } from '../../auth/types';
@@ -32,7 +33,7 @@ export default defineCommand({
3233
async run(config: Config, flags: GlobalFlags) {
3334
const envKey = process.env.MINIMAX_API_KEY;
3435
if (envKey) {
35-
const maskedEnvKey = envKey.length > 8 ? `${envKey.slice(0, 4)}...${envKey.slice(-4)}` : '***';
36+
const maskedEnvKey = maskToken(envKey);
3637
if (isInteractive({ nonInteractive: config.nonInteractive })) {
3738
const { confirm } = await import('@clack/prompts');
3839
const proceed = await confirm({

src/commands/auth/status.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { loadCredentials } from '../../auth/credentials';
44
import { formatOutput, detectOutputFormat } from '../../output/formatter';
55
import { requestJson } from '../../client/http';
66
import { quotaEndpoint } from '../../client/endpoints';
7+
import { renderQuotaTable } from '../../output/quota-table';
8+
import { maskToken } from '../../utils/token';
79
import type { Config } from '../../config/schema';
810
import type { GlobalFlags } from '../../types/flags';
911
import type { QuotaResponse } from '../../types/api';
@@ -33,7 +35,7 @@ export default defineCommand({
3335
if (creds.account) result.account = creds.account;
3436
}
3537
} else {
36-
result.key = credential.token.slice(0, 6) + '...' + credential.token.slice(-4);
38+
result.key = maskToken(credential.token);
3739
}
3840
console.log(formatOutput(result, format));
3941
return;
@@ -45,8 +47,7 @@ export default defineCommand({
4547
console.log(` Source: ${credential.source}`);
4648

4749
const token = credential.token;
48-
const maskedToken = token.length > 8 ? `${token.slice(0, 4)}...${token.slice(-4)}` : '***';
49-
console.log(` Key: ${maskedToken}`);
50+
console.log(` Key: ${maskToken(token)}`);
5051

5152
if (credential.method === 'oauth') {
5253
const creds = await loadCredentials();
@@ -59,32 +60,14 @@ export default defineCommand({
5960
}
6061

6162
// Fetch quota snapshot
62-
console.log('');
6363
process.stderr.write('Fetching quota snapshot...\n');
6464
try {
6565
const url = quotaEndpoint(config.baseUrl);
6666
const quota = await requestJson<QuotaResponse>(config, { url, method: 'GET' });
67-
const models = quota.model_remains || [];
68-
if (models.length > 0) {
69-
console.log('Available Quotas:');
70-
for (const m of models.slice(0, 5)) {
71-
const remaining = m.current_interval_total_count - m.current_interval_usage_count;
72-
const pct = m.current_interval_total_count > 0
73-
? Math.round((remaining / m.current_interval_total_count) * 100)
74-
: 0;
75-
console.log(` ${m.model_name.padEnd(24)} ${String(remaining).padStart(6)} / ${m.current_interval_total_count} (${pct}%)`);
76-
}
77-
if (models.length > 5) {
78-
console.log(` ... and ${models.length - 5} more (run 'minimax quota show' for full details)`);
79-
}
80-
} else {
81-
console.log(' No quota data available.');
82-
}
67+
renderQuotaTable(quota.model_remains || [], config);
8368
} catch (e) {
8469
console.log(` Quota fetch failed: ${(e as Error).message}`);
8570
}
86-
console.log('');
87-
console.log("Run 'minimax quota show' for full details.");
8871

8972
} catch {
9073
const format = detectOutputFormat(config.output);

src/commands/config/show.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defineCommand } from '../../command';
22
import { readConfigFile as loadConfigFile } from '../../config/loader';
33
import { getConfigPath } from '../../config/paths';
44
import { formatOutput, detectOutputFormat } from '../../output/formatter';
5+
import { maskToken } from '../../utils/token';
56
import type { Config } from '../../config/schema';
67
import type { GlobalFlags } from '../../types/flags';
78

@@ -27,7 +28,7 @@ export default defineCommand({
2728

2829
// Mask API key if present
2930
if (file.api_key) {
30-
result.api_key = file.api_key.slice(0, 6) + '...' + file.api_key.slice(-4);
31+
result.api_key = maskToken(file.api_key);
3132
}
3233

3334
console.log(formatOutput(result, format));

src/commands/music/generate.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { ExitCode } from '../../errors/codes';
44
import { request, requestJson } from '../../client/http';
55
import { musicEndpoint } from '../../client/endpoints';
66
import { formatOutput, detectOutputFormat } from '../../output/formatter';
7+
import { saveAudioOutput } from '../../output/audio';
8+
import { readTextFromPathOrStdin } from '../../utils/fs';
79
import type { Config } from '../../config/schema';
810
import type { GlobalFlags } from '../../types/flags';
911
import type { MusicRequest, MusicResponse } from '../../types/api';
10-
import { readFileSync, writeFileSync } from 'fs';
1112

1213
export default defineCommand({
1314
name: 'music generate',
@@ -33,10 +34,7 @@ export default defineCommand({
3334
let lyrics = flags.lyrics as string | undefined;
3435

3536
if (flags.lyricsFile) {
36-
const path = flags.lyricsFile as string;
37-
lyrics = path === '-'
38-
? readFileSync('/dev/stdin', 'utf-8')
39-
: readFileSync(path, 'utf-8');
37+
lyrics = readTextFromPathOrStdin(flags.lyricsFile as string);
4038
}
4139

4240
if (!prompt && !lyrics) {
@@ -94,35 +92,7 @@ export default defineCommand({
9492
body,
9593
});
9694

97-
if (!config.quiet) {
98-
process.stderr.write('[Model: music-2.5]\n');
99-
}
100-
101-
if (outPath) {
102-
const audioBuffer = Buffer.from(response.data.audio!, 'hex');
103-
writeFileSync(outPath, audioBuffer);
104-
105-
if (config.quiet) {
106-
console.log(outPath);
107-
} else {
108-
console.log(formatOutput({
109-
saved: outPath,
110-
duration_ms: response.extra_info?.audio_length,
111-
size_bytes: response.extra_info?.audio_size,
112-
sample_rate: response.extra_info?.audio_sample_rate,
113-
}, format));
114-
}
115-
} else {
116-
const audioUrl = response.data.audio_url ?? response.data.audio;
117-
if (config.quiet) {
118-
console.log(audioUrl);
119-
} else {
120-
console.log(formatOutput({
121-
url: audioUrl,
122-
duration_ms: response.extra_info?.audio_length,
123-
size_bytes: response.extra_info?.audio_size,
124-
}, format));
125-
}
126-
}
95+
if (!config.quiet) process.stderr.write('[Model: music-2.5]\n');
96+
saveAudioOutput(response, outPath, format, config.quiet);
12797
},
12898
});

0 commit comments

Comments
 (0)