2026-06-30 20:12:11 +08:00
import { config as loadDotenv } from "dotenv" ;
2026-06-24 13:53:48 +08:00
import { Command } from "commander" ;
import * as crypto from "crypto" ;
import * as fs from "fs" ;
import * as path from "path" ;
import * as os from "os" ;
2026-06-24 21:13:11 +08:00
import { execSync } from "child_process" ;
2026-06-24 13:53:48 +08:00
const GMGN_CONFIG_DIR = path . join ( os . homedir (), ".config" , "gmgn" );
const KEYPAIR_FILE = path . join ( GMGN_CONFIG_DIR , "keypair.pem" );
2026-06-24 21:13:11 +08:00
const ENV_FILE = path . join ( GMGN_CONFIG_DIR , ".env" );
2026-06-24 13:53:48 +08:00
const GMGN_API_URL = "https://gmgn.ai/ai/generateapi" ;
2026-06-24 21:13:11 +08:00
type Lang = "zh-CN" | "zh-TW" | "en" ;
function detectLang () : Lang {
const locale =
process . env . LANG ||
process . env . LC_ALL ||
process . env . LC_MESSAGES ||
Intl . DateTimeFormat (). resolvedOptions (). locale ||
"en" ;
const l = locale . toLowerCase ();
if ( l . startsWith ( "zh_tw" ) || l . startsWith ( "zh-tw" ) || l . startsWith ( "zh_hk" ) || l . startsWith ( "zh-hk" )) return "zh-TW" ;
if ( l . startsWith ( "zh" )) return "zh-CN" ;
return "en" ;
}
const MESSAGES = {
linkGuide : {
"zh-CN" : ( link : string ) =>
`请点击下方链接创建你的 GMGN API Key,完成后将 Key 发给我,我来帮你完成配置: \ n ${ link } ` ,
"zh-TW" : ( link : string ) =>
`請點擊下方連結建立你的 GMGN API Key,完成後將 Key 發給我,我來幫你完成配置: \ n ${ link } ` ,
en : ( link : string ) =>
`Please click the link below to create your GMGN API Key. Once created, send me the API Key and I will finish the configuration: \ n ${ link } ` ,
},
verifySuccess : {
"zh-CN" : "配置验证成功,可以开始使用了。" ,
"zh-TW" : "配置驗證成功,可以開始使用了。" ,
en : "Configuration verified successfully. You are ready to use GMGN." ,
},
verifyFail : {
"zh-CN" :
"配置验证失败:API Key 与本地密钥不匹配。\n请确认:\n1. API Key 是否填写正确;\n2. 创建 API Key 时,是否使用的是页面自动填入的公钥。" ,
"zh-TW" :
"配置驗證失敗:API Key 與本地密鑰不匹配。\n請確認:\n1. API Key 是否填寫正確;\n2. 創建 API Key 時,是否使用的是頁面自動填入的公鑰。" ,
en : "Configuration verification failed: API Key does not match your local key pair.\nPlease confirm:\n1. Whether the API Key was entered correctly.\n2. Whether you used the public key that was pre-filled on the page when creating the API Key." ,
},
2026-06-30 21:08:55 +08:00
verifyNetworkFail : {
"zh-CN" : "配置已写入,但验证请求失败(可能是网络问题)。你可以先尝试使用,如遇接口报错再重新配置。" ,
"zh-TW" : "配置已寫入,但驗證請求失敗(可能是網路問題)。你可以先嘗試使用,如遇介面報錯再重新配置。" ,
en : "Configuration saved, but the verification request failed (possibly a network issue). You can try using it now and reconfigure if you encounter API errors." ,
},
2026-06-24 21:13:11 +08:00
};
function getOrCreateKeypair () : string {
fs . mkdirSync ( GMGN_CONFIG_DIR , { recursive : true });
if ( fs . existsSync ( KEYPAIR_FILE )) {
const content = fs . readFileSync ( KEYPAIR_FILE , "utf-8" );
const match = content . match ( /(-----BEGIN PUBLIC KEY-----[\s\S]+?-----END PUBLIC KEY-----)/ );
if ( ! match ) {
console . error ( "Error: keypair.pem exists but public key could not be parsed. Delete ~/.config/gmgn/keypair.pem and try again." );
process . exit ( 1 );
}
return match [ 1 ] + "\n" ;
}
const { privateKey , publicKey } = crypto . generateKeyPairSync ( "ed25519" );
const privatePem = privateKey . export ({ type : "pkcs8" , format : "pem" }) as string ;
const publicPem = publicKey . export ({ type : "spki" , format : "pem" }) as string ;
const entry = `# Private Key \ n ${ privatePem } \ n# Public Key \ n ${ publicPem } \ n` ;
fs . writeFileSync ( KEYPAIR_FILE , entry , { mode : 0o600 });
return publicPem ;
}
function writeEnv ( apiKey : string , privatePem : string ) : void {
const pkOneLine = privatePem . replace ( /\r?\n/g , "\\n" );
let existing = fs . existsSync ( ENV_FILE ) ? fs . readFileSync ( ENV_FILE , "utf-8" ) : "" ;
existing = existing
. split ( "\n" )
. filter (( l ) => ! /^GMGN_API_KEY=|^GMGN_PRIVATE_KEY=/ . test ( l ))
. join ( "\n" )
. trim ();
const content = ( existing ? existing + "\n" : "" ) + `GMGN_API_KEY= ${ apiKey } \ nGMGN_PRIVATE_KEY= ${ pkOneLine } \ n` ;
fs . mkdirSync ( GMGN_CONFIG_DIR , { recursive : true });
fs . writeFileSync ( ENV_FILE , content , { mode : 0o600 });
}
2026-06-30 21:08:55 +08:00
function verify () : "ok" | "auth_fail" | "network_fail" {
2026-06-24 21:13:11 +08:00
try {
execSync ( "gmgn-cli track follow-wallet --chain sol --limit 1" , { stdio : "pipe" });
2026-06-30 21:08:55 +08:00
return "ok" ;
} catch ( e : unknown ) {
const output = (() => {
if ( e && typeof e === "object" && "stderr" in e ) return String (( e as { stderr : unknown }). stderr );
if ( e && typeof e === "object" && "stdout" in e ) return String (( e as { stdout : unknown }). stdout );
return String ( e );
})();
if ( /401|403|unauthorized|forbidden|invalid.*key|key.*invalid|signature/i . test ( output )) {
return "auth_fail" ;
}
return "network_fail" ;
2026-06-24 21:13:11 +08:00
}
}
2026-06-24 13:53:48 +08:00
export function registerConfigCommands ( program : Command ) : void {
2026-06-24 21:13:11 +08:00
const cmd = program
2026-06-24 13:53:48 +08:00
. command ( "config" )
2026-06-24 21:13:11 +08:00
. description ( "Generate an Ed25519 key pair and output a pre-filled GMGN API Key creation link, or apply an API Key" );
2026-06-24 13:53:48 +08:00
2026-06-24 21:13:11 +08:00
cmd
2026-06-26 13:52:02 +08:00
. option ( "--check" , "Check if GMGN_API_KEY is configured (exit 0 = found, exit 1 = not found)" )
2026-06-24 21:13:11 +08:00
. option ( "--apply <api_key>" , "Write API Key + private key to ~/.config/gmgn/.env and verify" )
. action ( async ( opts ) => {
const lang = detectLang ();
2026-06-24 13:53:48 +08:00
2026-06-26 13:52:02 +08:00
if ( opts . check ) {
2026-06-30 20:12:11 +08:00
loadDotenv ({ path : ENV_FILE , override : true });
loadDotenv ();
2026-06-26 13:52:02 +08:00
process . exit ( process . env . GMGN_API_KEY ? 0 : 1 );
}
2026-06-24 21:13:11 +08:00
if ( opts . apply ) {
// --apply: read private key from keypair.pem, write .env, verify
if ( ! fs . existsSync ( KEYPAIR_FILE )) {
console . error ( "Error: ~/.config/gmgn/keypair.pem not found. Run `gmgn-cli config` first to generate a key pair." );
2026-06-24 13:53:48 +08:00
process . exit ( 1 );
}
2026-06-24 21:13:11 +08:00
const content = fs . readFileSync ( KEYPAIR_FILE , "utf-8" );
const match = content . match ( /(-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----)/ );
if ( ! match ) {
console . error ( "Error: keypair.pem exists but private key could not be parsed. Delete ~/.config/gmgn/keypair.pem and run `gmgn-cli config` again." );
process . exit ( 1 );
}
writeEnv ( opts . apply , match [ 1 ]);
2026-06-24 13:53:48 +08:00
2026-06-30 21:08:55 +08:00
const result = verify ();
if ( result === "ok" ) {
2026-06-24 21:13:11 +08:00
console . log ( MESSAGES . verifySuccess [ lang ]);
2026-06-30 21:08:55 +08:00
} else if ( result === "auth_fail" ) {
2026-06-24 21:13:11 +08:00
console . error ( MESSAGES . verifyFail [ lang ]);
process . exit ( 1 );
2026-06-30 21:08:55 +08:00
} else {
console . log ( MESSAGES . verifyNetworkFail [ lang ]);
2026-06-24 21:13:11 +08:00
}
return ;
2026-06-24 13:53:48 +08:00
}
2026-06-24 21:13:11 +08:00
// Default: generate/reuse keypair, output link with guidance
const publicPem = getOrCreateKeypair ();
2026-06-24 13:53:48 +08:00
const link = ` ${ GMGN_API_URL } ?pbk= ${ encodeURIComponent ( publicPem ) } ` ;
2026-06-24 21:13:11 +08:00
console . log ( MESSAGES . linkGuide [ lang ]( link ));
2026-06-24 13:53:48 +08:00
});
}