@@ -5,9 +5,158 @@ import { loadConfig } from './config/loader';
55import { detectRegion , saveDetectedRegion } from './config/detect-region' ;
66import { REGIONS } from './config/schema' ;
77import { checkForUpdate , getPendingUpdateNotification } from './update/checker' ;
8+ import { resolveCredential } from './auth/resolver' ;
9+ import { requestJson } from './client/http' ;
10+ import { quotaEndpoint } from './client/endpoints' ;
11+ import { createSpinner } from './output/progress' ;
12+ import type { Config } from './config/schema' ;
13+ import type { QuotaResponse } from './types/api' ;
814
915const CLI_VERSION = process . env . CLI_VERSION ?? '0.3.1' ;
1016
17+ // ── ANSI color constants (MiniMax brand palette) ──
18+ const R = '\x1b[0m' ;
19+ const B = '\x1b[1m' ;
20+ const D = '\x1b[2m' ;
21+ const MM_BLUE = '\x1b[38;2;43;82;255m' ;
22+ const MM_CYAN = '\x1b[38;2;6;184;212m' ;
23+ const FG_GREEN = '\x1b[38;2;74;222;128m' ;
24+ const FG_YELLOW = '\x1b[38;2;250;204;21m' ;
25+ const FG_RED = '\x1b[38;2;248;113;113m' ;
26+ const WHITE = '\x1b[38;2;255;255;255m' ;
27+
28+ function c ( color : string , text : string ) : string {
29+ return `${ color } ${ text } ${ R } ` ;
30+ }
31+
32+ const BANNER = `
33+ ${ c ( B + MM_BLUE , ' __ __ ___ _ _ ___ __ __ _ __ __' ) }
34+ ${ c ( B + MM_BLUE , ' | \\/ |_ _| \\ | |_ _| \\/ | / \\ \\ \\ \\/ /' ) }
35+ ${ c ( B + MM_BLUE , ' | |\\/| || || \\| || || |\\/| | / _ \\ \\ /' ) }
36+ ${ c ( B + MM_BLUE , ' | | | || || |\\ || || | | |/ ___ \\/ \\' ) }
37+ ${ c ( B + MM_BLUE , ' |_| |_|___|_| \\_|___|_| |_/_/ \\_\\_/\\_\\' ) }
38+ ${ c ( D , ` v${ CLI_VERSION } ` ) }
39+ ` ;
40+
41+ const COMMON_COMMANDS = [
42+ [ 'text chat' , 'Chat with a LLM' ] ,
43+ [ 'vision describe' , 'Image understanding' ] ,
44+ [ 'image generate' , 'Generate an image' ] ,
45+ [ 'video generate' , 'Generate a video' ] ,
46+ ] ;
47+
48+ async function printDashboard ( config : Config ) : Promise < void > {
49+ const useColor = process . stdout . isTTY && ! config . noColor ;
50+
51+ // Try to resolve credentials; if not found we're logged-out
52+ let credential : Awaited < ReturnType < typeof resolveCredential > > | null = null ;
53+ try {
54+ credential = await resolveCredential ( config ) ;
55+ } catch {
56+ // Not logged in
57+ }
58+
59+ if ( credential ) {
60+ // ── Logged-in dashboard ──
61+ const maskedKey = credential . token . length > 8
62+ ? `${ credential . token . slice ( 0 , 4 ) } ...${ credential . token . slice ( - 4 ) } `
63+ : '****' ;
64+ const region = config . region ?? 'global' ;
65+ const keySource = credential . source === 'credentials.json'
66+ ? 'OAuth'
67+ : credential . source === 'config.yaml'
68+ ? 'CONFIG'
69+ : credential . source === 'env'
70+ ? 'ENV'
71+ : 'FLAG' ;
72+
73+ process . stdout . write ( BANNER ) ;
74+
75+ const keyRow = ` ${ c ( D , 'Key:' ) } ${ useColor ? c ( B + WHITE , maskedKey ) : maskedKey } ${ c ( D , `(${ keySource } )` ) } ` ;
76+ const regionRow = ` ${ c ( D , 'Region:' ) } ${ useColor ? c ( MM_CYAN , region ) : region } ` ;
77+ process . stdout . write ( keyRow + '\n' ) ;
78+ process . stdout . write ( regionRow + '\n' ) ;
79+
80+ // Fetch quota with spinner
81+ process . stdout . write ( '\n' ) ;
82+ const spinner = createSpinner ( 'Fetching quota...' ) ;
83+ if ( useColor ) spinner . start ( ) ;
84+
85+ let quotaFailed = false ;
86+ let models : QuotaResponse [ 'model_remains' ] = [ ] ;
87+ try {
88+ const url = quotaEndpoint ( config . baseUrl ) ;
89+ const resp = await requestJson < QuotaResponse > ( config , { url } ) ;
90+ models = resp . model_remains ?? [ ] ;
91+ } catch {
92+ quotaFailed = true ;
93+ }
94+ spinner . stop ( ) ;
95+
96+ if ( quotaFailed ) {
97+ process . stdout . write ( ` ${ c ( FG_YELLOW , '⚠ Quota unavailable (network error or timeout)' ) } \n` ) ;
98+ } else if ( models . length > 0 ) {
99+ const limit = models [ 0 ] ! . current_interval_total_count ;
100+ const remaining = models [ 0 ] ! . current_interval_usage_count ; // this IS the remaining count per API
101+ const used = limit - remaining ;
102+ const pct = limit > 0 ? Math . round ( ( used / limit ) * 100 ) : 0 ; // % of quota consumed
103+ const bar = renderMiniBar ( pct , useColor ) ;
104+ process . stdout . write ( ` ${ c ( D , 'Balance:' ) } ${ useColor ? c ( pctColor ( pct ) , `${ remaining . toLocaleString ( ) } ${ c ( D , '/' ) } ${ limit . toLocaleString ( ) } ` ) : `${ remaining } / ${ limit } ` } ${ bar } ${ pct } %\n` ) ;
105+ if ( models . length > 1 ) {
106+ process . stdout . write ( ` ${ c ( D , `+${ models . length - 1 } more models → 'minimax quota show'` ) } \n` ) ;
107+ }
108+ }
109+
110+ process . stdout . write ( '\n' ) ;
111+ process . stdout . write ( c ( D , ' Common commands:\n' ) ) ;
112+ for ( const [ cmd , desc ] of COMMON_COMMANDS ) {
113+ // Left-aligned: 2-space indent, command padded, 2-space gap, description
114+ const label = useColor ? c ( B + WHITE , cmd ) : cmd ;
115+ process . stdout . write ( ` ${ label . padEnd ( 24 ) } ${ c ( D , desc ) } \n` ) ;
116+ }
117+ process . stdout . write ( '\n' ) ;
118+ process . stdout . write ( ` ${ c ( D , "Run 'minimax <command> --help' for details." ) } \n` ) ;
119+ process . stdout . write ( ` ${ c ( D , "Or 'minimax --help' for the full command list." ) } \n` ) ;
120+
121+ } else {
122+ // ── Logged-out dashboard ──
123+ process . stdout . write ( BANNER ) ;
124+ process . stdout . write ( `\n ${ c ( FG_RED , '✗ Not authenticated yet' ) } \n` ) ;
125+ process . stdout . write ( `\n ${ c ( D , "You're not logged in. To get started:" ) } \n` ) ;
126+ process . stdout . write ( `\n ${ c ( MM_CYAN , 'minimax auth login --api-key sk-xxxxx' ) } \n` ) ;
127+ process . stdout . write ( ` ${ c ( D , '← Paste your Token Plan API key (sk-cp-...)' ) } \n` ) ;
128+ process . stdout . write ( `\n ${ c ( D , 'Or set the environment variable:' ) } \n` ) ;
129+ process . stdout . write ( ` ${ c ( D , 'export MINIMAX_API_KEY=sk-xxxxx' ) } \n` ) ;
130+ process . stdout . write ( `\n ${ c ( D , 'What you can do:' ) } \n` ) ;
131+ const capabilities = [
132+ [ 'text chat' , 'Chat with any MiniMax model' ] ,
133+ [ 'vision describe' , 'Image understanding (VLM)' ] ,
134+ [ 'image generate' , 'Image generation (image-01)' ] ,
135+ [ 'video generate' , 'Video generation (Hailuo series)' ] ,
136+ ] ;
137+ for ( const [ cmd , desc ] of capabilities ) {
138+ const label = useColor ? c ( B + WHITE , cmd ) : cmd ;
139+ process . stdout . write ( ` ${ label . padEnd ( 24 ) } ${ c ( D , desc ) } \n` ) ;
140+ }
141+ process . stdout . write ( '\n' ) ;
142+ process . stdout . write ( ` ${ c ( D , "Run 'minimax --help' for the full command list." ) } \n` ) ;
143+ }
144+ }
145+
146+ function renderMiniBar ( pct : number , color : boolean ) : string {
147+ const W = 10 ;
148+ const filled = Math . round ( ( Math . max ( 0 , Math . min ( 100 , pct ) ) / 100 ) * W ) ;
149+ const empty = W - filled ;
150+ if ( ! color ) return `[${ '█' . repeat ( filled ) } ${ '-' . repeat ( empty ) } ]` ;
151+ return `${ c ( FG_GREEN , '█' . repeat ( filled ) ) } ${ c ( D , '-' . repeat ( empty ) ) } ` ;
152+ }
153+
154+ function pctColor ( pct : number ) : string {
155+ if ( pct > 50 ) return FG_GREEN ;
156+ if ( pct > 20 ) return FG_YELLOW ;
157+ return FG_RED ;
158+ }
159+
11160async function main ( ) {
12161 const args = process . argv . slice ( 2 ) ;
13162
@@ -18,7 +167,14 @@ async function main() {
18167
19168 const { commandPath, flags } = parseArgs ( args ) ;
20169
21- if ( flags . help || commandPath . length === 0 ) {
170+ // ── Dashboard: no subcommand given (distinct from --help) ──
171+ if ( commandPath . length === 0 && ! flags . help ) {
172+ const config = loadConfig ( flags ) ;
173+ await printDashboard ( config ) ;
174+ process . exit ( 0 ) ;
175+ }
176+
177+ if ( flags . help ) {
22178 registry . printHelp ( commandPath , process . stderr ) ;
23179 process . exit ( 0 ) ;
24180 }
0 commit comments