lili-solana-cli
Version:
Production-ready CLI tool for Solana developers - Build, Deploy, and Manage Solana programs
2 lines • 254 kB
JavaScript
#!/usr/bin/env node
import chalk from"chalk";import inquirer from"inquirer";import ora from"ora";import figlet from"figlet";import{Connection,Keypair,LAMPORTS_PER_SOL,PublicKey,SystemProgram,Transaction,sendAndConfirmTransaction,ComputeBudgetProgram}from"@solana/web3.js";import*as splToken from"@solana/spl-token";import{withCreateRealm,withCreateTokenOwnerRecord,withDepositGoverningTokens,withCreateGovernance,withCreateNativeTreasury,GovernanceConfig,GoverningTokenConfigArgs,GoverningTokenType,MintMaxVoteWeightSource,VoteThreshold,VoteThresholdType,VoteTipping,getTokenOwnerRecordAddress,getTokenHoldingAddress,getNativeTreasuryAddress}from"@solana/spl-governance";import{createUmi}from"@metaplex-foundation/umi-bundle-defaults";import{Metaplex,keypairIdentity,bundlrStorage,toMetaplexFile}from"@metaplex-foundation/js";import*as sns from"@bonfida/spl-name-service";import{createSignerFromKeypair,publicKey as umiPublicKey,signerIdentity,generateSigner}from"@metaplex-foundation/umi";import{fromWeb3JsKeypair}from"@metaplex-foundation/umi-web3js-adapters";import fs from"fs-extra";import{exec,spawn}from"child_process";import{promisify}from"util";import path from"path";import os from"os";import{fileURLToPath}from"url";import BN from"bn.js";const execAsync=promisify(exec);const __filename=fileURLToPath(import.meta.url);const __dirname=path.dirname(__filename);const stripAnsi=str=>str.replace(/\x1b\[[0-9;]*m/g,"");function centerBlock(block){const cols=process.stdout.columns||80;const lines=block.split("\n");const maxLen=Math.max(...lines.map(l=>stripAnsi(l).length));const left=Math.max(0,Math.floor((cols-maxLen)/2));const pad=" ".repeat(left);return lines.map(l=>pad+l).join("\n")}function makeBox(lines,width=77){const top="╔"+"═".repeat(width-2)+"╗";const bottom="╚"+"═".repeat(width-2)+"╝";const empty="║"+" ".repeat(width-2)+"║";const content=lines.map(txt=>{const s=txt.trim();const inner=width-2;const pad=Math.max(0,inner-s.length);const left=Math.floor(pad/2),right=pad-left;return"║"+" ".repeat(left)+s+" ".repeat(right)+"║"});return[top,empty,...content,empty,bottom].join("\n")}function centeredBox(lines,width=77){const inner=width-2;const top="╔"+"═".repeat(inner)+"╗";const bottom="╚"+"═".repeat(inner)+"╝";const empty="║"+" ".repeat(inner)+"║";const content=lines.map(line=>{if(!line.trim())return empty;const value=line.trim();const plain=stripAnsi(value);const pad=Math.max(0,inner-plain.length);const left=Math.floor(pad/2);const right=pad-left;return"║"+" ".repeat(left)+value+" ".repeat(right)+"║"});return[top,...content,bottom].join("\n")}const colorize=(color,text)=>{const map={magenta:"#8B5CF6",magentaBright:"#8B5CF6"};if(map[color])return chalk.hex(map[color])(text);const fn=chalk[color];if(typeof fn==="function")return fn(text);try{return chalk.hex(color)(text)}catch{return chalk.white(text)}};const PROGRESS_BAR_WIDTH=50;function renderStepProgress(step,progress){const prefix=colorize(step.color,step.prefix.padEnd(10));const filled=Math.max(0,Math.min(PROGRESS_BAR_WIDTH,Math.round(PROGRESS_BAR_WIDTH*progress)));const empty=PROGRESS_BAR_WIDTH-filled;const bar=colorize(step.color,step.icon.repeat(filled))+chalk.gray("░".repeat(empty));const percent=String(Math.round(progress*100)).padStart(3)+"%";return chalk.gray("[")+prefix+chalk.gray("] ")+chalk.white(step.text.padEnd(40))+bar+chalk.gray(` ${percent}`)}function renderStepComplete(step){const prefix=colorize(step.color,step.prefix.padEnd(10));const bar=colorize(step.color,step.icon.repeat(PROGRESS_BAR_WIDTH));return chalk.gray("[")+prefix+chalk.gray("] ")+chalk.gray(step.text.padEnd(40))+bar+chalk.yellow(" READY")}async function runWithLoadingBar(stepOptions,action){const step={text:stepOptions.text||"Processing",color:stepOptions.color||"cyan",prefix:stepOptions.prefix||"TASK",icon:stepOptions.icon||"▓"};const durationMs=Math.max(1e3,stepOptions.durationMs||1e4);const spinner=ora({text:renderStepProgress(step,0),spinner:"dots8"}).start();const start=Date.now();let done=false;const tick=async()=>{while(!done){const elapsed=Date.now()-start;const p=Math.min(.9,elapsed/durationMs);spinner.text=renderStepProgress(step,p);await new Promise(r=>setTimeout(r,100))}};const anim=tick();try{const result=await action();done=true;await anim;spinner.succeed(renderStepComplete(step));return result}catch(e){done=true;await anim;spinner.fail(chalk.red(`[${step.prefix}] ${step.text} FAILED: ${e.message}`));throw e}}function formatSol(amount,decimals=4){if(amount===null||amount===undefined||Number.isNaN(amount)){return"0"}const fixed=Number.parseFloat(amount.toFixed(decimals));return fixed%1===0?fixed.toFixed(0):fixed.toString()}(()=>{const origWrite=process.stdout.write.bind(process.stdout);process.stdout.write=(chunk,encoding,cb)=>{try{let s=Buffer.isBuffer(chunk)?chunk.toString("utf8"):String(chunk??"");const cols=process.stdout.columns||80;const out=s.split("\n").map(line=>{if(/^\s/.test(line))return line;const plain=stripAnsi(line);if(/^(╔|╗|╚|╝|║|═|│|┌|┐|└|┘|─)/.test(plain)){const pad=Math.max(0,Math.floor((cols-plain.length)/2));return" ".repeat(pad)+line}return line}).join("\n");return origWrite(out,encoding,cb)}catch{return origWrite(chunk,encoding,cb)}}})();const CONFIG_DIR=path.join(os.homedir(),".lili-cli");const CONFIG_FILE=path.join(CONFIG_DIR,"config.json");const WALLETS_DIR=path.join(CONFIG_DIR,"wallets");const resolveTemplatesDir=()=>{const candidates=[path.join(__dirname,"templates"),path.join(__dirname,"..","templates"),path.join(process.cwd(),"templates")];for(const candidate of candidates){try{if(fs.existsSync(path.join(candidate,"manifest.json"))){return candidate}}catch{}}return path.join(__dirname,"templates")};const PACKAGE_MANAGER_INSTALL={npm:"npm install",pnpm:"pnpm install",yarn:"yarn install",bun:"bun install"};const PACKAGE_MANAGER_DEV={npm:"npm run dev",pnpm:"pnpm dev",yarn:"yarn dev",bun:"bun dev"};const resolveBootstrapCwd=(base,subdir)=>subdir?path.join(base,subdir):base;async function loadPackageJsonIfExists(dir){const file=path.join(dir,"package.json");if(!await fs.pathExists(file))return null;return fs.readJSON(file).catch(()=>null)}async function detectPackageManager(dir,preferred){if(preferred)return preferred;const candidates=[{file:"pnpm-lock.yaml",value:"pnpm"},{file:"yarn.lock",value:"yarn"},{file:"bun.lockb",value:"bun"},{file:"package-lock.json",value:"npm"}];for(const candidate of candidates){if(await fs.pathExists(path.join(dir,candidate.file))){return candidate.value}}return await fs.pathExists(path.join(dir,"package.json"))?"npm":null}function formatStepName(defaultLabel,command){return defaultLabel||command}async function bootstrapTemplateDestination(template,destination){const bootstrap=template.bootstrap||{};const packageJson=await loadPackageJsonIfExists(destination);const packageManager=await detectPackageManager(destination,bootstrap.packageManager);const steps=[];const liliDeploy=bootstrap.liliDeploy||null;let deployContext=null;let deployInfo=null;if(liliDeploy){try{deployContext=await prepareLiliDeployContext(destination,liliDeploy);console.log(chalk.gray(`\nUsing wallet ${deployContext.walletInfo.publicKey} on ${deployContext.config.network} via ${deployContext.config.rpcUrl}`));if(Array.isArray(liliDeploy.prompts)&&liliDeploy.prompts.length){const responses=await runAdditionalPrompts(liliDeploy.prompts);deployContext.promptResponses=responses;injectPromptResponsesIntoEnv(deployContext,responses)}if(liliDeploy.tokenSetup){await runTokenSetup(deployContext,liliDeploy.tokenSetup)}}catch(error){console.log(chalk.yellow(`\nSkipping automated deploy integration: ${error.message}`));deployContext=null}}let installCommand=bootstrap.installCommand||null;if(!installCommand&&packageJson&&packageManager){installCommand=PACKAGE_MANAGER_INSTALL[packageManager]||null}if(installCommand){steps.push({name:formatStepName("Install dependencies",installCommand),command:installCommand,cwd:resolveBootstrapCwd(destination,bootstrap.installCwd),env:bootstrap.installEnv||null})}const deployCommands=Array.isArray(bootstrap.deployCommands)?bootstrap.deployCommands:[];for(const entry of deployCommands){if(!entry)continue;if(typeof entry==="string"){steps.push({name:formatStepName("Run deployment command",entry),command:entry,cwd:destination,env:null});continue}steps.push({name:formatStepName(entry.name||null,entry.command),command:entry.command,cwd:resolveBootstrapCwd(destination,entry.cwd),env:entry.env||null})}if(steps.length){console.log(chalk.cyan("\nBootstrapping workspace..."))}for(const step of steps){console.log(chalk.whiteBright(`\n→ ${step.name}`));try{const envContext={...deployContext?deployContext.env:{},...step.env||{}};await runShellCommand(step.command,{cwd:step.cwd,env:envContext,stdio:"inherit"})}catch(error){throw new Error(`Step "${step.name}" failed: ${error.message}`)}}if(steps.length){console.log(chalk.green("\nBootstrap steps completed."))}if(deployContext){try{deployInfo=await finalizeLiliDeploy(destination,deployContext)}catch(error){console.log(chalk.yellow(`Deployment integration skipped: ${error.message}`))}}let devCommand=bootstrap.devCommand||null;if(!devCommand&&packageJson&&packageManager&&packageJson.scripts&&packageJson.scripts.dev){devCommand=PACKAGE_MANAGER_DEV[packageManager]||null}return{devCommand:devCommand,devCwd:resolveBootstrapCwd(destination,bootstrap.devCwd),devEnv:bootstrap.devEnv||null,deployInfo:deployInfo}}async function prepareLiliDeployContext(destination,deployConfig={}){await initConfig();const config=await loadConfig();const walletSelection=await selectWalletForDeploy(deployConfig.wallet||config.defaultWallet);const network=(config.network||"devnet").toLowerCase();const rpcUrl=config.rpcUrl||"https://api.devnet.solana.com";const workspace=resolveBootstrapCwd(destination,deployConfig.workspace||".");const baseEnv={SOLANA_NETWORK:network,SOLANA_CLUSTER:network,SOLANA_RPC_URL:rpcUrl,SOLANA_URL:rpcUrl,ANCHOR_PROVIDER_URL:rpcUrl,ANCHOR_WALLET:walletSelection.walletPath,LILI_WALLET_PUBLIC_KEY:walletSelection.publicKey,LILI_WALLET_PATH:walletSelection.walletPath,WALLET_PUBLIC_KEY:walletSelection.publicKey,WALLET_PATH:walletSelection.walletPath};const mergedEnv={...baseEnv,...deployConfig.env||{}};return{config:{network:network,rpcUrl:rpcUrl,raw:config},walletInfo:walletSelection,env:mergedEnv,workspace:workspace,deployConfig:deployConfig}}async function selectWalletForDeploy(preferredName){const hasWalletDir=await fs.pathExists(WALLETS_DIR);const walletFiles=hasWalletDir?(await fs.readdir(WALLETS_DIR)).filter(name=>name.endsWith(".json")):[];if(!walletFiles.length){throw new Error("No wallets available. Create or import one from the wallet menu first.")}let selectedFile=null;if(preferredName){const candidate=preferredName.endsWith(".json")?preferredName:`${preferredName}.json`;if(walletFiles.includes(candidate)){selectedFile=candidate}}if(!selectedFile){if(walletFiles.length===1){selectedFile=walletFiles[0]}else{const{walletChoice:walletChoice}=await inquirer.prompt([{type:"list",name:"walletChoice",message:chalk.yellow.bold("Select wallet for deployment"),choices:walletFiles.map(file=>({name:file.replace(/\.json$/,""),value:file})),pageSize:Math.min(walletFiles.length,10)}]);selectedFile=walletChoice}}const walletPath=path.join(WALLETS_DIR,selectedFile);const secretKey=await fs.readJSON(walletPath);const keypair=Keypair.fromSecretKey(new Uint8Array(secretKey));return{walletName:selectedFile.replace(/\.json$/,""),walletPath:walletPath,keypair:keypair,publicKey:keypair.publicKey.toBase58()}}async function finalizeLiliDeploy(destination,deployContext){const type=(deployContext.deployConfig?.type||"anchor").toLowerCase();const baseContext=buildDeployEnvContext(deployContext);if(type==="anchor"){return finalizeAnchorDeploy(destination,deployContext,baseContext)}const envTargets=Array.isArray(deployContext.deployConfig?.envTargets)?deployContext.deployConfig.envTargets:[];const updatedFiles=await applyEnvTargets(destination,envTargets,baseContext);if(updatedFiles.length){console.log(chalk.green("Environment updated:"));for(const file of updatedFiles){console.log(chalk.gray(` - ${file}`))}}return{...baseContext,envFiles:updatedFiles,tokenSelections:deployContext.tokenSelections||null,promptResponses:deployContext.promptResponses||null}}function buildDeployEnvContext(deployContext){const context={rpcUrl:deployContext.config.rpcUrl,network:deployContext.config.network,walletPath:deployContext.walletInfo.walletPath,walletPublicKey:deployContext.walletInfo.publicKey,walletName:deployContext.walletInfo.walletName,workspace:deployContext.workspace};if(deployContext.env){for(const[key,value]of Object.entries(deployContext.env)){if(value===undefined||value===null)continue;if(typeof value==="object")continue;context[key]=String(value)}}if(deployContext.promptResponses){for(const[key,value]of Object.entries(deployContext.promptResponses)){const upper=String(key||"").toUpperCase();context[`PROMPT_${upper}`]=String(value??"")}}if(deployContext.tokenSelections){for(const[id,token]of Object.entries(deployContext.tokenSelections)){const upperId=String(id||"").toUpperCase();if(token.mint)context[`TOKEN_${upperId}_MINT`]=token.mint;if(token.decimals!==undefined)context[`TOKEN_${upperId}_DECIMALS`]=String(token.decimals);if(token.symbol)context[`TOKEN_${upperId}_SYMBOL`]=token.symbol;if(token.supplyUi!==undefined)context[`TOKEN_${upperId}_SUPPLY_UI`]=String(token.supplyUi);if(token.supplyBase!==undefined)context[`TOKEN_${upperId}_SUPPLY_BASE`]=String(token.supplyBase);if(token.requiredAmountUi!==undefined)context[`TOKEN_${upperId}_REQUIRED_AMOUNT_UI`]=String(token.requiredAmountUi);if(token.requiredAmountBase!==undefined)context[`TOKEN_${upperId}_REQUIRED_AMOUNT_BASE`]=String(token.requiredAmountBase);if(token.balanceUi!==undefined)context[`TOKEN_${upperId}_BALANCE_UI`]=String(token.balanceUi)}}return context}async function finalizeAnchorDeploy(destination,deployContext,baseContext){const programInfo=await collectAnchorProgramInfo(deployContext);if(!programInfo.programId){console.log(chalk.yellow("Could not detect program id from deployment output."));return null}const context={...baseContext,programId:programInfo.programId,programName:programInfo.programName,programKeypairPath:programInfo.programKeypairPath,programJsonPath:programInfo.programJsonPath,idlPath:programInfo.idlPath,programKeypairWorkspaceRelative:programInfo.programKeypairPath?toPosixPath(path.relative(deployContext.workspace,programInfo.programKeypairPath)):null,programJsonWorkspaceRelative:programInfo.programJsonPath?toPosixPath(path.relative(deployContext.workspace,programInfo.programJsonPath)):null,idlWorkspaceRelative:programInfo.idlPath?toPosixPath(path.relative(deployContext.workspace,programInfo.idlPath)):null};const envTargets=Array.isArray(deployContext.deployConfig?.envTargets)?deployContext.deployConfig.envTargets:[];const updatedFiles=await applyEnvTargets(destination,envTargets,context);if(updatedFiles.length){console.log(chalk.green("Environment updated:"));for(const file of updatedFiles){console.log(chalk.gray(` - ${file}`))}}return{...context,envFiles:updatedFiles,tokenSelections:deployContext.tokenSelections||null,promptResponses:deployContext.promptResponses||null}}async function collectAnchorProgramInfo(deployContext){const workspace=deployContext.workspace;const deployConfig=deployContext.deployConfig||{};const programName=deployConfig.programName||await inferAnchorProgramName(workspace);const targetDeployDir=path.join(workspace,"target","deploy");let programId=null;let programJsonPath=null;let programKeypairPath=null;const readProgramMetadata=async metadataPath=>{const data=await fs.readJSON(metadataPath).catch(()=>null);if(!data)return null;const id=data.programId||data.program_id||null;let keypairPath=data.path||data.deployPath||null;if(keypairPath&&!path.isAbsolute(keypairPath)){keypairPath=path.resolve(path.dirname(metadataPath),keypairPath)}return{id:id,keypairPath:keypairPath}};if(programName){const metadataPath=path.join(targetDeployDir,`${programName}.json`);if(await fs.pathExists(metadataPath)){const details=await readProgramMetadata(metadataPath);if(details?.id){programId=details.id;programJsonPath=metadataPath;if(details.keypairPath&&await fs.pathExists(details.keypairPath)){programKeypairPath=details.keypairPath}else{const fallback=path.join(targetDeployDir,`${programName}-keypair.json`);if(await fs.pathExists(fallback)){programKeypairPath=fallback}}}}}if(!programId&&await fs.pathExists(targetDeployDir)){const entries=await fs.readdir(targetDeployDir);for(const file of entries){if(!file.endsWith(".json"))continue;const metadataPath=path.join(targetDeployDir,file);const details=await readProgramMetadata(metadataPath);if(details?.id){programId=details.id;programJsonPath=metadataPath;if(details.keypairPath&&await fs.pathExists(details.keypairPath)){programKeypairPath=details.keypairPath}else{const base=file.replace(/\.json$/,"");const fallback=path.join(targetDeployDir,`${base}-keypair.json`);if(await fs.pathExists(fallback)){programKeypairPath=fallback}}break}}}let idlPath=null;if(deployConfig.idlPath){const candidate=path.join(workspace,deployConfig.idlPath);if(await fs.pathExists(candidate)){idlPath=candidate}}else if(programName){const candidate=path.join(workspace,"target","idl",`${programName}.json`);if(await fs.pathExists(candidate)){idlPath=candidate}}return{programName:programName,programId:programId,programJsonPath:programJsonPath,programKeypairPath:programKeypairPath,idlPath:idlPath}}async function inferAnchorProgramName(workspace){const anchorTomlPath=path.join(workspace,"Anchor.toml");if(!await fs.pathExists(anchorTomlPath)){return null}const content=await fs.readFile(anchorTomlPath,"utf8");const lines=content.split(/\r?\n/);let inProgramsBlock=false;for(const rawLine of lines){const line=rawLine.trim();if(!line)continue;if(line.startsWith("[")){inProgramsBlock=/^\[programs\./i.test(line);continue}if(!inProgramsBlock)continue;if(line.startsWith("#"))continue;const eqIndex=line.indexOf("=");if(eqIndex===-1)continue;const name=line.slice(0,eqIndex).trim();if(name){return name}}return null}async function applyEnvTargets(destination,envTargets,baseContext){if(!envTargets.length)return[];const updated=[];for(const target of envTargets){if(!target||!target.file||!target.entries)continue;const envPath=path.join(destination,target.file);const envDir=path.dirname(envPath);const context={...baseContext};context.envFile=envPath;context.envDir=envDir;context.projectRelativePath=path.relative(destination,envPath);if(baseContext.programKeypairPath){context.programKeypairRelative=toPosixPath(path.relative(envDir,baseContext.programKeypairPath))}if(baseContext.programJsonPath){context.programJsonRelative=toPosixPath(path.relative(envDir,baseContext.programJsonPath))}if(baseContext.idlPath){context.idlRelative=toPosixPath(path.relative(envDir,baseContext.idlPath))}const updates={};for(const[key,rawValue]of Object.entries(target.entries)){const resolved=resolveEnvPlaceholder(rawValue,context);if(resolved===undefined||resolved===null)continue;if(resolved==="")continue;updates[key]=resolved}if(!Object.keys(updates).length)continue;await writeEnvFile(envPath,updates);updated.push(path.relative(destination,envPath))}return updated}function resolveEnvPlaceholder(value,context){if(value===undefined||value===null)return"";if(typeof value!=="string"){return String(value)}return value.replace(/\{([a-zA-Z0-9_]+)\}/g,(_,key)=>{const replacement=context[key];return replacement===undefined||replacement===null?"":String(replacement)})}async function ensureEnvFileBaseline(filePath){if(await fs.pathExists(filePath))return;const dir=path.dirname(filePath);const base=path.basename(filePath);const candidates=[];if(base===".env"){candidates.push(".env.example")}candidates.push(`${base}.example`,`${base}.sample`,`${base}.template`);for(const candidate of candidates){if(!candidate)continue;const candidatePath=path.join(dir,candidate);if(await fs.pathExists(candidatePath)){await fs.copy(candidatePath,filePath);return}}await fs.ensureDir(dir);await fs.writeFile(filePath,"")}async function writeEnvFile(filePath,updates){await ensureEnvFileBaseline(filePath);let content="";try{content=await fs.readFile(filePath,"utf8")}catch{content=""}const lines=content.split(/\r?\n/);const index=new Map;lines.forEach((line,idx)=>{if(!line||/^\s*#/.test(line))return;const eqIdx=line.indexOf("=");if(eqIdx===-1)return;const key=line.slice(0,eqIdx).trim();if(!key||index.has(key))return;index.set(key,idx)});for(const[key,value]of Object.entries(updates)){const formatted=`${key}=${value}`;if(index.has(key)){lines[index.get(key)]=formatted}else{lines.push(formatted)}}while(lines.length&&lines[lines.length-1]===""){lines.pop()}lines.push("");await fs.writeFile(filePath,lines.join("\n"))}function toPosixPath(value){if(value===null||value===undefined)return value;return String(value).split(path.sep).join("/")}async function launchDevServer(command,options={}){if(!command)return;console.log(chalk.cyan(`\nStarting development server with "${command}"`));console.log(chalk.gray("Press Ctrl+C to stop the server when you are finished."));await runShellCommand(command,{cwd:options.cwd,env:options.env||undefined,stdio:"inherit"})}const TEMPLATES_DIR=resolveTemplatesDir();const COLLECTIONS_FILE=path.join(CONFIG_DIR,"collections.json");const TEMPLATE_CACHE_DIR=path.join(CONFIG_DIR,"templates");const TEMPLATE_MANIFEST_FILE=path.join(TEMPLATES_DIR,"manifest.json");const USER_TEMPLATE_MANIFEST=path.join(CONFIG_DIR,"templates-manifest.json");const DOCTOR_HISTORY_FILE=path.join(CONFIG_DIR,"doctor-history.json");const TOKENS_FILE=path.join(CONFIG_DIR,"tokens.json");const DEFAULT_CONFIG={network:"devnet",rpcUrl:"https://api.devnet.solana.com",defaultWallet:null,lastUsed:null};let viewSplTokensFlow;let sendSplTokenFlow;async function initConfig(){try{await fs.ensureDir(CONFIG_DIR);await fs.ensureDir(WALLETS_DIR);await fs.ensureDir(TEMPLATES_DIR);await fs.ensureDir(TEMPLATE_CACHE_DIR);if(!await fs.pathExists(COLLECTIONS_FILE)){await fs.writeJSON(COLLECTIONS_FILE,{collections:[]},{spaces:2})}if(!await fs.pathExists(USER_TEMPLATE_MANIFEST)){if(await fs.pathExists(TEMPLATE_MANIFEST_FILE)){await fs.copy(TEMPLATE_MANIFEST_FILE,USER_TEMPLATE_MANIFEST)}else{await fs.writeJSON(USER_TEMPLATE_MANIFEST,{version:1,templates:[]},{spaces:2})}}await reconcileTemplateManifestWithDefaults();if(!await fs.pathExists(DOCTOR_HISTORY_FILE)){await fs.writeJSON(DOCTOR_HISTORY_FILE,[],{spaces:2})}if(!await fs.pathExists(TOKENS_FILE)){await fs.writeJSON(TOKENS_FILE,[],{spaces:2})}viewSplTokensFlow=async function(){const config=await loadConfig();const walletFiles=await fs.pathExists(WALLETS_DIR)?await fs.readdir(WALLETS_DIR):[];const wallets=walletFiles.filter(f=>f.endsWith(".json"));if(wallets.length===0){console.log(chalk.red("\nNo wallets found. Create/import one first."));await inquirer.prompt([{type:"input",name:"continue",message:chalk.gray("Press Enter to return")}]);return}let defaultChoice=config.defaultWallet?`${config.defaultWallet}.json`:null;if(defaultChoice&&!wallets.includes(defaultChoice))defaultChoice=null;const choices=[...defaultChoice?[{name:`${config.defaultWallet} (default)`,value:defaultChoice},new inquirer.Separator]:[],...wallets.map(w=>({name:w.replace(".json",""),value:w}))];const{walletFile:walletFile}=await inquirer.prompt([{type:"list",name:"walletFile",message:chalk.yellow.bold("Select wallet to inspect"),choices:choices}]);const secretKey=await fs.readJSON(path.join(WALLETS_DIR,walletFile));const keypair=Keypair.fromSecretKey(new Uint8Array(secretKey));const connection=new Connection(config.rpcUrl,"confirmed");const spinner=ora({text:chalk.white("Fetching token accounts..."),spinner:"dots2"}).start();try{const accounts=await connection.getParsedTokenAccountsByOwner(keypair.publicKey,{programId:splToken.TOKEN_PROGRAM_ID});const adminsExplain=centeredBox(["","Admin wallets can be preconfigured for the web app.","We will store them in the .env as NEXT_PUBLIC_ADMIN_WALLETS (comma-separated).",""]);console.log(centerBlock(chalk.white(adminsExplain)));spinner.stop();if(!accounts.value.length){console.log(chalk.yellow("\nNo SPL token accounts found for this wallet."));await inquirer.prompt([{type:"input",name:"continue",message:chalk.gray("Press Enter to return")}]);return}console.log();console.log(chalk.hex("#8B5CF6")("Token Accounts for:"),chalk.yellow(keypair.publicKey.toBase58()));for(const acct of accounts.value){const info=acct.account.data.parsed.info;const mint=info.mint;const amount=BigInt(info.tokenAmount.amount);const decimals=info.tokenAmount.decimals;const ui=Number(amount)/10**decimals;console.log("- Mint:",mint,"| Balance:",ui)}}catch(e){spinner.fail(chalk.red("Failed to fetch tokens"));console.log(chalk.red(e.message))}console.log();await inquirer.prompt([{type:"input",name:"continue",message:chalk.gray("Press Enter to return")}])};sendSplTokenFlow=async function(){const config=await loadConfig();const walletFiles=await fs.pathExists(WALLETS_DIR)?await fs.readdir(WALLETS_DIR):[];const wallets=walletFiles.filter(f=>f.endsWith(".json"));if(wallets.length===0){console.log(chalk.red("\nNo wallets found. Create/import one first."));await new Promise(r=>setTimeout(r,1500));return}let defaultChoice=config.defaultWallet?`${config.defaultWallet}.json`:null;if(defaultChoice&&!wallets.includes(defaultChoice))defaultChoice=null;const choices=[...defaultChoice?[{name:`${config.defaultWallet} (default)`,value:defaultChoice},new inquirer.Separator]:[],...wallets.map(w=>({name:w.replace(".json",""),value:w}))];const{walletFile:walletFile}=await inquirer.prompt([{type:"list",name:"walletFile",message:chalk.yellow.bold("Select wallet to send from"),choices:choices}]);const secretKey=await fs.readJSON(path.join(WALLETS_DIR,walletFile));const keypair=Keypair.fromSecretKey(new Uint8Array(secretKey));const connection=new Connection(config.rpcUrl,"confirmed");const spinner=ora({text:chalk.white("Loading token accounts..."),spinner:"dots2"}).start();let accounts;try{accounts=await connection.getParsedTokenAccountsByOwner(keypair.publicKey,{programId:splToken.TOKEN_PROGRAM_ID});spinner.stop()}catch(e){spinner.fail(chalk.red("Failed to load token accounts"));console.log(chalk.red(e.message));return}if(!accounts.value.length){console.log(chalk.yellow("\nNo SPL tokens to send from this wallet."));await new Promise(r=>setTimeout(r,1500));return}const tokenChoices=accounts.value.map(a=>{const info=a.account.data.parsed.info;const mint=info.mint;const decimals=info.tokenAmount.decimals;const amount=BigInt(info.tokenAmount.amount);const ui=Number(amount)/10**decimals;return{name:`${mint} — balance: ${ui}`,value:JSON.stringify({mint:mint,decimals:decimals})}});const{mintSel:mintSel}=await inquirer.prompt([{type:"list",name:"mintSel",message:chalk.yellow.bold("Select SPL token to send"),choices:tokenChoices}]);const{mint:mint,decimals:decimals}=JSON.parse(mintSel);const{recipientInput:recipientInput}=await inquirer.prompt([{type:"input",name:"recipientInput",message:chalk.yellow.bold("Recipient address"),validate:v=>{try{new PublicKey(v);return true}catch{return"Enter a valid public key"}}}]);const recipient=new PublicKey(recipientInput);const{amountInput:amountInput}=await inquirer.prompt([{type:"input",name:"amountInput",message:chalk.yellow.bold("Amount to send"),validate:v=>{const n=Number(v);return n>0&&Number.isFinite(n)?true:"Enter a positive number"}}]);const amountUi=Number(amountInput);const amountBase=BigInt(Math.round(amountUi*10**decimals));const confirmMsg=`Send ${amountUi} tokens of ${mint} to ${recipient.toBase58()}?`;const{confirmSend:confirmSend}=await inquirer.prompt([{type:"confirm",name:"confirmSend",message:chalk.yellow.bold(confirmMsg),default:true}]);if(!confirmSend){console.log(chalk.gray("\nTransfer cancelled"));return}const txSpinner=ora({text:chalk.white("Submitting token transfer..."),spinner:"dots2"}).start();try{const mintPk=new PublicKey(mint);const fromAta=await splToken.getOrCreateAssociatedTokenAccount(connection,keypair,mintPk,keypair.publicKey);const toAta=await splToken.getOrCreateAssociatedTokenAccount(connection,keypair,mintPk,recipient);const sig=await splToken.transfer(connection,keypair,fromAta.address,toAta.address,keypair.publicKey,amountBase);txSpinner.succeed(chalk.yellow("Token transfer complete"));console.log(chalk.white("Signature:"),sig)}catch(e){txSpinner.fail(chalk.red("Token transfer failed"));console.log(chalk.red(e.message))}console.log();await new Promise(r=>setTimeout(r,1500))};if(!await fs.pathExists(CONFIG_FILE)){await fs.writeJSON(CONFIG_FILE,DEFAULT_CONFIG,{spaces:2})}}catch(error){console.error(chalk.red("ERROR: Failed to initialize configuration:"),error.message)}}async function loadConfig(){try{return await fs.readJSON(CONFIG_FILE)}catch(error){return DEFAULT_CONFIG}}async function saveConfig(config){try{await fs.writeJSON(CONFIG_FILE,config,{spaces:2})}catch(error){console.error(chalk.red("ERROR: Failed to save configuration:"),error.message)}}async function loadTemplateManifest(){try{const manifest=await fs.readJSON(USER_TEMPLATE_MANIFEST);if(!Array.isArray(manifest.templates)){manifest.templates=[]}manifest.version=manifest.version??1;return manifest}catch{return{version:1,templates:[]}}}async function saveTemplateManifest(manifest){const payload={version:manifest?.version??1,templates:Array.isArray(manifest?.templates)?manifest.templates:[]};await fs.writeJSON(USER_TEMPLATE_MANIFEST,payload,{spaces:2})}async function reconcileTemplateManifestWithDefaults(){try{if(!await fs.pathExists(USER_TEMPLATE_MANIFEST))return;if(!await fs.pathExists(TEMPLATE_MANIFEST_FILE))return;const base=await fs.readJSON(TEMPLATE_MANIFEST_FILE).catch(()=>null);if(!base||!Array.isArray(base.templates))return;const user=await loadTemplateManifest();const merged=Array.isArray(user.templates)?[...user.templates]:[];const index=new Map(merged.map((tpl,idx)=>[tpl.name,idx]));let changed=false;for(const tpl of base.templates){if(!tpl||!tpl.name)continue;if(index.has(tpl.name)){const existing=merged[index.get(tpl.name)];const keys=["title","description","source","repo","ref","subdir","localPath","featured","bootstrap"];for(const key of keys){if(tpl[key]===undefined)continue;const same=key==="bootstrap"?JSON.stringify(existing[key]??null)===JSON.stringify(tpl[key]??null):existing[key]===tpl[key];if(same)continue;existing[key]=tpl[key];changed=true}}else{merged.push(tpl);index.set(tpl.name,merged.length-1);changed=true}}const targetVersion=Math.max(base.version??1,user.version??1);if((user.version??1)!==targetVersion){changed=true}if(!changed)return;merged.sort((a,b)=>(a.title||a.name).localeCompare(b.title||b.name));await saveTemplateManifest({version:targetVersion,templates:merged})}catch{}}async function listCachedTemplates(){if(!await fs.pathExists(TEMPLATE_CACHE_DIR))return[];const entries=await fs.readdir(TEMPLATE_CACHE_DIR);const result=[];for(const name of entries){const absolute=path.join(TEMPLATE_CACHE_DIR,name);const stat=await fs.stat(absolute).catch(()=>null);if(!stat||!stat.isDirectory())continue;const metaFile=path.join(absolute,".lili-template.json");let meta={};if(await fs.pathExists(metaFile)){meta=await fs.readJSON(metaFile).catch(()=>({}))}result.push({name:name,path:absolute,meta:meta})}return result}async function writeTemplateMetadata(targetDir,template){const meta={name:template.name,title:template.title||template.displayName||template.name,source:template.source||"github",repo:template.repo||null,ref:template.ref||null,subdir:template.subdir||null,localPath:template.localPath||template.path||null,cachePath:template.cachePath||null,description:template.description||null,bootstrap:template.bootstrap||null,updatedAt:(new Date).toISOString()};await fs.writeJSON(path.join(targetDir,".lili-template.json"),meta,{spaces:2})}async function copyLocalTemplate(template,destination){const localPath=template.localPath||template.path||template.name;const sourceDir=path.join(TEMPLATES_DIR,localPath);if(!await fs.pathExists(sourceDir)){throw new Error(`Local template not found at ${localPath}`)}await fs.copy(sourceDir,destination,{overwrite:true,recursive:true});await writeTemplateMetadata(destination,{...template,source:"local"})}async function loadKnownTokens(){try{const tokens=await fs.readJSON(TOKENS_FILE);return Array.isArray(tokens)?tokens:[]}catch{return[]}}async function saveKnownTokens(tokens){try{await fs.writeJSON(TOKENS_FILE,tokens,{spaces:2})}catch{}}async function rememberKnownToken(record={}){const mint=record?.mint;if(!mint)return;const normalizedMint=(()=>{try{return new PublicKey(mint).toBase58()}catch{return String(mint)}})();const tokens=await loadKnownTokens();const network=(record.network||"unknown").toLowerCase();const existing=tokens.find(t=>t.mint===normalizedMint&&(t.network||"unknown").toLowerCase()===network);const baseRecord={mint:normalizedMint,decimals:record.decimals,symbol:record.symbol||null,name:record.name||null,network:network,wallet:record.wallet||null,supplyUi:record.supplyUi||null,supplyBase:record.supplyBase||null,source:record.source||"custom",notedAt:(new Date).toISOString()};if(existing){Object.assign(existing,baseRecord)}else{tokens.unshift(baseRecord)}const trimmed=tokens.slice(0,50);await saveKnownTokens(trimmed)}async function listWalletSplTokens(connection,owner){const results=new Map;try{const accounts=await connection.getParsedTokenAccountsByOwner(owner,{programId:splToken.TOKEN_PROGRAM_ID});for(const entry of accounts.value){const info=entry.account?.data?.parsed?.info;if(!info?.mint)continue;const mint=info.mint;const decimals=Number(info.tokenAmount?.decimals??0);const amount=info.tokenAmount?.uiAmount;const base=info.tokenAmount?.amount;const key=`${mint}:${decimals}`;if(!results.has(key)){results.set(key,{mint:mint,decimals:decimals,balanceUi:Number(amount??0),balanceBase:BigInt(base||"0"),accounts:[entry.pubkey]})}else{const existing=results.get(key);existing.balanceUi+=Number(amount??0);existing.balanceBase+=BigInt(base||"0");existing.accounts.push(entry.pubkey)}}}catch{return[]}return Array.from(results.values()).map(record=>({mint:record.mint,decimals:record.decimals,balanceUi:Number(record.balanceUi??0),balanceBase:record.balanceBase.toString(),accounts:record.accounts})).sort((a,b)=>b.balanceUi-a.balanceUi)}function normalizeUiAmountInput(value){if(value===undefined||value===null)return"";return String(value).trim().replace(/,/g,"").replace(/_/g,"")}function uiAmountToBaseUnits(amount,decimals){const sanitized=normalizeUiAmountInput(amount);if(!sanitized)return 0n;if(!/^[0-9]*\.?[0-9]*$/.test(sanitized)){throw new Error("Enter a numeric amount")}const[whole,frac=""]=sanitized.split(".");if(!whole&&!frac)return 0n;if(frac.length>decimals){throw new Error(`Too many decimal places (max ${decimals})`)}const wholePart=whole?BigInt(whole):0n;const factor=10n**BigInt(decimals);const fracPart=frac?BigInt(frac.padEnd(decimals,"0")):0n;return wholePart*factor+fracPart}function baseUnitsToUiString(amountBase,decimals){try{const amount=BigInt(amountBase);const factor=10n**BigInt(decimals);const whole=amount/factor;const remainder=amount%factor;if(remainder===0n){return whole.toString()}const remainderStr=remainder.toString().padStart(decimals,"0").replace(/0+$/,"");return`${whole.toString()}.${remainderStr}`}catch{return String(amountBase)}}async function runTokenSetup(deployContext,setup={}){const tokens=Array.isArray(setup.tokens)?setup.tokens:[];if(!tokens.length)return{};const connection=new Connection(deployContext.config.rpcUrl,"confirmed");const walletInfo=deployContext.walletInfo;if(!walletInfo?.keypair){throw new Error("Wallet missing for token setup")}const knownTokens=await loadKnownTokens();const walletTokens=await listWalletSplTokens(connection,walletInfo.keypair.publicKey);const selections={};for(const requirement of tokens){const selection=await promptForTokenRequirement({deployContext:deployContext,requirement:requirement,connection:connection,walletTokens:walletTokens,knownTokens:knownTokens});selections[requirement.id]=selection;if(selection?.mint){const tokenRecord={mint:selection.mint,decimals:selection.decimals,balanceUi:Number(selection.balanceUi??selection.supplyUi??0),balanceBase:selection.balanceBase??selection.supplyBase??"0",accounts:[]};if(selection.source==="created"&&selection.supplyUi!==undefined){tokenRecord.balanceUi=Number(selection.supplyUi||0);tokenRecord.balanceBase=selection.supplyBase??tokenRecord.balanceBase}walletTokens.push(tokenRecord);knownTokens.unshift({mint:selection.mint,decimals:selection.decimals,symbol:selection.symbol||null,network:(deployContext.config.network||"unknown").toLowerCase()})}}deployContext.tokenSelections=selections;injectTokenSelectionsIntoEnv(deployContext,selections);return selections}async function promptForTokenRequirement({deployContext:deployContext,requirement:requirement,connection:connection,walletTokens:walletTokens,knownTokens:knownTokens}){const label=requirement?.label||requirement?.id||"token";console.log();console.log(chalk.hex("#8B5CF6")(`► ${label}`));if(requirement?.purpose){console.log(chalk.gray(requirement.purpose))}const allowWallet=requirement?.allowWallet!==false;const allowCustom=requirement?.allowCustom!==false;const allowCreate=requirement?.allowCreate!==false;const allowSaved=requirement?.allowSaved!==false;const network=deployContext.config.network||"unknown";const savedTokens=allowSaved?knownTokens.filter(token=>(token.network||"unknown")===network.toLowerCase()):[];const options=[];if(allowWallet&&walletTokens.length){options.push({name:"Use a token from my wallet",value:"wallet"})}if(savedTokens.length){options.push({name:"Use a previously saved token",value:"saved"})}if(allowCustom){options.push({name:"Enter a custom SPL token mint",value:"custom"})}if(allowCreate){options.push({name:"Create a new SPL token now",value:"create"})}if(!options.length){throw new Error("No token selection options are available for this template")}const{source:source}=await inquirer.prompt([{type:"list",name:"source",message:chalk.yellow.bold("Select token source"),choices:options}]);if(source==="wallet"){const choices=walletTokens.map(token=>({name:`${token.mint} — balance: ${token.balanceUi}`,value:JSON.stringify(token)}));const{picked:picked}=await inquirer.prompt([{type:"list",name:"picked",message:chalk.yellow.bold("Choose SPL token"),choices:choices}]);const parsed=JSON.parse(picked);const mint=new PublicKey(parsed.mint);const onChain=await splToken.getMint(connection,mint);const selection={id:requirement.id,source:"wallet",mint:mint.toBase58(),decimals:onChain.decimals,balanceUi:parsed.balanceUi,balanceBase:parsed.balanceBase?.toString?.()??null};await maybeRememberSelection(selection,deployContext,requirement);await applyAmountPrompt(selection,requirement);return selection}if(source==="saved"){const choices=savedTokens.map(token=>({name:token.symbol?`${token.symbol} — ${token.mint}`:token.mint,value:token.mint}));const{savedMint:savedMint}=await inquirer.prompt([{type:"list",name:"savedMint",message:chalk.yellow.bold("Choose saved token"),choices:choices}]);const mint=new PublicKey(savedMint);const onChain=await splToken.getMint(connection,mint);const selection={id:requirement.id,source:"saved",mint:mint.toBase58(),decimals:onChain.decimals,symbol:savedTokens.find(t=>t.mint===savedMint)?.symbol||null};await applyAmountPrompt(selection,requirement);return selection}if(source==="custom"){const{customMint:customMint}=await inquirer.prompt([{type:"input",name:"customMint",message:chalk.yellow.bold("Enter SPL token mint address"),validate:value=>{try{new PublicKey(value);return true}catch{return"Enter a valid public key"}}}]);const mint=new PublicKey(customMint);const onChain=await splToken.getMint(connection,mint);const selection={id:requirement.id,source:"custom",mint:mint.toBase58(),decimals:onChain.decimals};await maybeRememberSelection(selection,deployContext,requirement);await applyAmountPrompt(selection,requirement);return selection}const created=await createNewTokenMint({deployContext:deployContext,requirement:requirement,connection:connection});await applyAmountPrompt(created,requirement);return created}async function maybeRememberSelection(selection,deployContext,requirement){if(selection.source==="custom"||selection.source==="wallet"){const{remember:remember}=await inquirer.prompt([{type:"confirm",name:"remember",message:chalk.yellow.bold("Save this mint for future runs?"),default:requirement?.rememberByDefault??selection.source==="custom"}]);if(remember){await rememberKnownToken({mint:selection.mint,decimals:selection.decimals,wallet:deployContext.walletInfo?.publicKey,network:deployContext.config.network,source:selection.source})}}}async function applyAmountPrompt(selection,requirement){if(!requirement?.amountPrompt)return;const prompt=requirement.amountPrompt;const{amount:amount}=await inquirer.prompt([{type:"input",name:"amount",message:chalk.yellow.bold(prompt.message||"Required balance"),default:prompt.default??"1",validate:value=>{try{uiAmountToBaseUnits(value,selection.decimals??0);return true}catch(error){return error instanceof Error?error.message:"Enter a number"}}}]);const normalized=normalizeUiAmountInput(amount||"0");const baseUnits=uiAmountToBaseUnits(normalized,selection.decimals??0);selection.requiredAmountUi=normalized||"0";selection.requiredAmountBase=baseUnits.toString()}async function createNewTokenMint({deployContext:deployContext,requirement:requirement,connection:connection}){const walletInfo=deployContext.walletInfo;const keypair=walletInfo.keypair;const answers=await inquirer.prompt([{type:"input",name:"symbol",message:chalk.yellow.bold("Token symbol (optional)"),default:requirement?.defaultSymbol||"TOKEN"},{type:"input",name:"decimals",message:chalk.yellow.bold("Decimals (0-9)"),default:"6",validate:value=>{const n=Number(value);return Number.isInteger(n)&&n>=0&&n<=9?true:"Enter 0-9"}},{type:"input",name:"initialSupply",message:chalk.yellow.bold("Initial supply to mint (UI units)"),default:requirement?.defaultSupply??"1_000_000",validate:(value,data)=>{try{const decimals=Number(data?.decimals??0);uiAmountToBaseUnits(value,decimals);return true}catch(error){return error instanceof Error?error.message:"Enter a number"}}},{type:"confirm",name:"lockMint",message:chalk.yellow.bold("Lock mint authority after minting?"),default:requirement?.lockMintDefault??true},{type:"confirm",name:"removeFreeze",message:chalk.yellow.bold("Remove freeze authority?"),default:requirement?.removeFreezeDefault??true}]);const decimals=Number(answers.decimals);const initialBase=uiAmountToBaseUnits(answers.initialSupply,decimals);try{const balance=await connection.getBalance(keypair.publicKey);if(balance<.05*LAMPORTS_PER_SOL&&["devnet","testnet"].includes((deployContext.config.network||"").toLowerCase())){console.log(chalk.gray("Requesting SOL airdrop for token creation..."));await requestAirdrop(keypair.publicKey)}}catch{}const spinner=ora({text:chalk.white("Creating SPL token mint"),spinner:"dots2"}).start();try{const mintKey=await splToken.createMint(connection,keypair,keypair.publicKey,keypair.publicKey,decimals);const ata=await splToken.getOrCreateAssociatedTokenAccount(connection,keypair,mintKey,keypair.publicKey);if(initialBase>0n){await splToken.mintTo(connection,keypair,mintKey,ata.address,keypair,initialBase)}if(answers.lockMint){await splToken.setAuthority(connection,keypair,mintKey,keypair,splToken.AuthorityType.MintTokens,null)}if(answers.removeFreeze){try{await splToken.setAuthority(connection,keypair,mintKey,keypair,splToken.AuthorityType.FreezeAccount,null)}catch{}}spinner.succeed(chalk.yellow(`Created mint ${mintKey.toBase58()}`));const selection={id:requirement.id,source:"created",mint:mintKey.toBase58(),decimals:decimals,symbol:answers.symbol?.trim()||null,supplyUi:normalizeUiAmountInput(answers.initialSupply||"0")||"0",supplyBase:initialBase.toString()};await rememberKnownToken({mint:selection.mint,decimals:selection.decimals,symbol:selection.symbol,supplyUi:selection.supplyUi,supplyBase:selection.supplyBase,wallet:deployContext.walletInfo?.publicKey,network:deployContext.config.network,source:"created"});return selection}catch(error){spinner.fail(chalk.red("Failed to create token mint"));throw error}}function injectTokenSelectionsIntoEnv(deployContext,selections){if(!deployContext)return;const env=deployContext.env||{};const entries=Object.entries(selections||{});entries.forEach(([id,token],index)=>{const upperId=String(id).toUpperCase();env[`TOKEN_${upperId}_MINT`]=token.mint;if(token.decimals!==undefined)env[`TOKEN_${upperId}_DECIMALS`]=String(token.decimals);if(token.symbol)env[`TOKEN_${upperId}_SYMBOL`]=token.symbol;if(token.supplyUi!==undefined)env[`TOKEN_${upperId}_SUPPLY_UI`]=String(token.supplyUi);if(token.supplyBase!==undefined)env[`TOKEN_${upperId}_SUPPLY_BASE`]=String(token.supplyBase);if(token.requiredAmountUi!==undefined)env[`TOKEN_${upperId}_REQUIRED_AMOUNT_UI`]=String(token.requiredAmountUi);if(token.requiredAmountBase!==undefined)env[`TOKEN_${upperId}_REQUIRED_AMOUNT_BASE`]=String(token.requiredAmountBase);if(token.balanceUi!==undefined)env[`TOKEN_${upperId}_BALANCE_UI`]=String(token.balanceUi);if(index===0){env.TOKEN_PRIMARY_MINT=token.mint;if(token.decimals!==undefined)env.TOKEN_PRIMARY_DECIMALS=String(token.decimals)}});deployContext.env=env}async function runAdditionalPrompts(prompts=[]){if(!Array.isArray(prompts)||!prompts.length)return{};const responses={};for(const prompt of prompts){const type=prompt.type||"input";if(type==="confirm"){const{value:value}=await inquirer.prompt([{type:"confirm",name:"value",message:chalk.yellow.bold(prompt.message||"Confirm?"),default:prompt.default??false}]);responses[prompt.key]=value?"true":"false";continue}const{value:value}=await inquirer.prompt([{type:"input",name:"value",message:chalk.yellow.bold(prompt.message||prompt.key||"Value"),default:prompt.default??"",validate:input=>{if(!prompt.validate)return true;if(prompt.validate==="publicKey"){if(!input&&prompt.optional)return true;try{new PublicKey(input);return true}catch{return"Enter a valid public key"}}if(prompt.validate==="number"){return/^-?\d+(\.\d+)?$/.test(String(input).trim())?true:"Enter a number"}return true}}]);responses[prompt.key]=String(value??"")}return responses}function injectPromptResponsesIntoEnv(deployContext,responses){if(!deployContext||!responses)return;const env=deployContext.env||{};for(const[key,value]of Object.entries(responses)){const upper=String(key||"").toUpperCase();env[`PROMPT_${upper}`]=String(value??"")}deployContext.env=env}async function cloneTemplateFromGitHub(template,destination){if(!template.repo||!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(template.repo)){throw new Error("Invalid GitHub repository identifier")}const branch=template.ref||"main";const tmpRoot=await fs.mkdtemp(path.join(os.tmpdir(),"lili-template-"));const gitUrl=`https://github.com/${template.repo}.git`;const cloneCmd=`git clone --depth=1 --branch ${branch} ${gitUrl} "${tmpRoot}"`;try{await execAsync(cloneCmd);const sourceDir=template.subdir?path.join(tmpRoot,template.subdir):tmpRoot;if(!await fs.pathExists(sourceDir)){throw new Error("Subdirectory not found in cloned repository")}await fs.copy(sourceDir,destination,{overwrite:true,recursive:true})}catch(error){throw new Error(`Failed to download template from GitHub: ${error.message}`)}finally{await fs.remove(tmpRoot).catch(()=>null)}await writeTemplateMetadata(destination,template)}async function ensureTemplateCache(template,options={}){const targetDir=path.join(TEMPLATE_CACHE_DIR,template.name);const force=options.force??false;if(!force&&await fs.pathExists(targetDir)){return targetDir}await fs.remove(targetDir).catch(()=>null);await fs.ensureDir(path.dirname(targetDir));if((template.source||"").toLowerCase()==="local"){await copyLocalTemplate(template,targetDir)}else{await cloneTemplateFromGitHub(template,targetDir)}return targetDir}async function refreshAllTemplates(options={}){const manifest=await loadTemplateManifest();const results=[];for(const template of manifest.templates){try{const pathResult=await ensureTemplateCache(template,options);results.push({name:template.name,status:"ok",path:pathResult})}catch(error){results.push({name:template.name,status:"error",error:error.message})}}return results}async function loadDoctorHistory(){try{const history=await fs.readJSON(DOCTOR_HISTORY_FILE);return Array.isArray(history)?history:[]}catch{return[]}}async function recordDoctorEvent(event){try{const history=await loadDoctorHistory();history.unshift({...event,timestamp:(new Date).toISOString()});const trimmed=history.slice(0,25);await fs.writeJSON(DOCTOR_HISTORY_FILE,trimmed,{spaces:2})}catch{}}async function runShellCommand(command,options={}){const isWindows=process.platform==="win32";const shell=options.shell||(isWindows?"powershell.exe":"zsh");const shellArgs=isWindows?["-NoLogo","-NoProfile","-Command",command]:["-lc",command];return new Promise((resolve,reject)=>{const child=spawn(shell,shellArgs,{stdio:options.stdio||"inherit",env:{...process.env,...options.env},cwd:options.cwd||process.cwd()});child.on("error",reject);child.on("exit",code=>{if(code===0){resolve()}else{reject(new Error(`Command failed (${command}) with exit code ${code}`))}})})}function displayTitle(){console.clear();const title=centeredBox(["","██╗ ██╗██╗ ██╗ ██████╗██╗ ██╗","██║ ██║██║ ██║ ██╔════╝██║ ██║","██║ ██║██║ ██║ ██║ ██║ ██║","██║ ██║██║ ██║ ██║ ██║ ██║","███████╗██║███████╗██║ ╚██████╗███████╗██║","╚══════╝╚═╝╚══════╝╚═╝ ╚═════╝╚══════╝╚═╝","","Solana Development Infrastructure",""]);console.log(centerBlock(chalk.hex("#8B5CF6")(title)));console.log()}async function bootSequence(){console.clear();const bootHeader=centeredBox(["","LILI CLI SYSTEM","[ VERSION 0.0.4 ]",""]);console.log(centerBlock(chalk.hex("#8B5CF6"