import chalk from 'chalk'; import * as fs from 'node:fs'; import { parseArgs } from 'node:util'; import prompts from 'prompts'; import { Language } from './getLanguage'; import { kebabCase } from './stringHelpers'; import { toMinorSolanaVersion } from './solanaCli'; export const allClients = ['js', 'rust'] as const; export type Client = (typeof allClients)[number]; export type Inputs = { jsClient: boolean; jsClientPackageName: string; organizationName: string; programAddress?: string; programCrateName: string; programFramework: 'shank' | 'anchor'; programName: string; rustClient: boolean; rustClientCrateName: string; shouldOverride: boolean; solanaVersion?: string; targetDirectoryName: string; useDefaults: boolean; }; export async function getInputs(language: Language): Promise { const argInputs = getInputsFromArgs(language); const defaultInputs = getDefaultInputs(argInputs); if (argInputs.useDefaults) { return defaultInputs; } return getInputsFromPrompts(language, argInputs); } async function getInputsFromPrompts( language: Language, argInputs: Partial ): Promise { type PromptInputs = { programName?: string; shouldOverride?: boolean; organizationName?: string; programCrateName?: string; programFramework?: 'shank' | 'anchor'; clients?: Array<'js' | 'rust'>; jsClientPackageName?: string; rustClientCrateName?: string; }; let defaultInputs = getDefaultInputs(argInputs); function parsePromptInputs(promptInputs: PromptInputs): Inputs { const inputs = {} as Partial; if (promptInputs.programName) inputs.programName = promptInputs.programName; if (promptInputs.shouldOverride !== undefined) inputs.shouldOverride = promptInputs.shouldOverride; if (promptInputs.organizationName) inputs.organizationName = promptInputs.organizationName; if (promptInputs.programCrateName) inputs.programCrateName = promptInputs.programCrateName; if (promptInputs.programFramework) inputs.programFramework = promptInputs.programFramework; if (promptInputs.clients !== undefined) { inputs.jsClient = promptInputs.clients.includes('js'); inputs.rustClient = promptInputs.clients.includes('rust'); } if (promptInputs.jsClientPackageName) inputs.jsClientPackageName = promptInputs.jsClientPackageName; if (promptInputs.rustClientCrateName) inputs.rustClientCrateName = promptInputs.rustClientCrateName; return getDefaultInputs({ ...argInputs, ...inputs }); } try { const promptInputs: PromptInputs = await prompts( [ { name: 'programName', type: argInputs.programName ? null : 'text', message: language.programName.message, initial: () => defaultInputs.programName, }, { name: 'shouldOverride', type: (_, values) => { if (argInputs.shouldOverride) return null; defaultInputs = parsePromptInputs(values); return canSkipEmptying(defaultInputs.targetDirectoryName) ? null : 'toggle'; }, message: () => { const dirForPrompt = defaultInputs.targetDirectoryName === '.' ? language.shouldOverride.dirForPrompts!.current : `${language.shouldOverride.dirForPrompts!.target} "${defaultInputs.targetDirectoryName}"`; return `${dirForPrompt} ${language.shouldOverride.message}`; }, initial: false, active: language.defaultToggleOptions.active, inactive: language.defaultToggleOptions.inactive, }, { name: 'overwriteChecker', type: (_, values) => { if (values.shouldOverride === false) { throw new Error( chalk.red('✖') + ` ${language.errors.operationCancelled}` ); } return null; }, }, { name: 'organizationName', type: argInputs.organizationName ? null : 'text', message: language.organizationName.message, initial: () => defaultInputs.organizationName, }, { name: 'programCrateName', type: argInputs.programCrateName ? null : 'text', message: language.programCrateName.message, initial: (_, values) => { defaultInputs = parsePromptInputs(values); return defaultInputs.programCrateName; }, }, { name: 'programFramework', type: argInputs.programFramework ? null : 'select', message: language.programFramework.message, hint: language.instructions.select, initial: 0, choices: [ { title: language.programFramework.selectOptions!.shank.title, description: language.programFramework.selectOptions!.shank.desc, value: 'shank', }, { title: language.programFramework.selectOptions!.anchor.title, description: language.programFramework.selectOptions!.anchor.desc, value: 'anchor', }, ], }, { name: 'clients', type: () => { const hasSelectedClients = [ argInputs.jsClient, argInputs.rustClient, ].every((client) => typeof client === 'boolean'); return hasSelectedClients ? null : 'multiselect'; }, message: language.clients.message, hint: language.clients.hint, instructions: language.instructions.multiselect, choices: allClients.map((client) => ({ title: language.clients.selectOptions![client].title, description: language.clients.selectOptions![client].desc, value: client, selected: true, })), }, { name: 'jsClientPackageName', type: (_, values) => { if (argInputs.jsClientPackageName) return null; defaultInputs = parsePromptInputs(values); return defaultInputs.jsClient ? 'text' : null; }, message: language.jsClientPackageName.message, initial: () => defaultInputs.jsClientPackageName, }, { name: 'rustClientCrateName', type: (_, values) => { if (argInputs.rustClientCrateName) return null; defaultInputs = parsePromptInputs(values); return defaultInputs.rustClient ? 'text' : null; }, message: language.rustClientCrateName.message, initial: () => defaultInputs.rustClientCrateName, }, ], { onCancel: () => { throw new Error( chalk.red('✖') + ` ${language.errors.operationCancelled}` ); }, } ); // Add a line break after the prompts console.log(''); return parsePromptInputs(promptInputs); } catch (cancelled) { console.log((cancelled as Error).message); process.exit(1); } } function getInputsFromArgs(language: Language): Partial { type ArgInputs = { address?: string; anchorProgram: boolean; clients: Array<'js' | 'rust'>; force: boolean; noClients: boolean; organizationName?: string; programName?: string; shankProgram: boolean; solanaVersion?: string; useDefaults: boolean; targetDirectoryName?: string; }; function parseArgInputs(argInputs: ArgInputs): Partial { const inputs = {} as Partial; if (argInputs.address) inputs.programAddress = argInputs.address; if (argInputs.organizationName) inputs.organizationName = kebabCase(argInputs.organizationName); if (argInputs.programName) inputs.programName = kebabCase(argInputs.programName); if (argInputs.solanaVersion) inputs.solanaVersion = toMinorSolanaVersion( language, argInputs.solanaVersion ); if (argInputs.targetDirectoryName) inputs.targetDirectoryName = argInputs.targetDirectoryName; if (argInputs.force) inputs.shouldOverride = true; if (argInputs.useDefaults) inputs.useDefaults = true; if (argInputs.anchorProgram) { inputs.programFramework = 'anchor'; } else if (argInputs.shankProgram) { inputs.programFramework = 'shank'; } if (argInputs.noClients) { inputs.jsClient = false; inputs.rustClient = false; } else if (argInputs.clients) { inputs.jsClient = argInputs.clients.includes('js'); inputs.rustClient = argInputs.clients.includes('rust'); } return inputs; } const args = process.argv.slice(2); const { values: options, positionals } = parseArgs({ args, options: { address: { type: 'string' }, anchor: { type: 'boolean' }, client: { type: 'string', multiple: true }, default: { type: 'boolean', short: 'd' }, force: { type: 'boolean' }, 'no-clients': { type: 'boolean' }, org: { type: 'string' }, shank: { type: 'boolean' }, solana: { type: 'string' }, }, strict: false, }); return parseArgInputs({ address: options.address, anchorProgram: options.anchor ?? false, clients: options.client, force: options.force ?? false, noClients: options['no-clients'] ?? false, organizationName: options.org, programName: positionals[1], shankProgram: options.shank ?? false, solanaVersion: options.solana, useDefaults: options.default ?? false, targetDirectoryName: positionals[0], } as ArgInputs); } export function getDefaultInputs(partialInputs: Partial): Inputs { const organizationName = kebabCase( partialInputs.organizationName ?? 'solana-program' ); const parsedTargetDirectoryName = partialInputs.targetDirectoryName ? partialInputs.targetDirectoryName.split('/').pop() : ''; const programName = kebabCase( partialInputs.programName ?? (parsedTargetDirectoryName || 'my-program') ); const programCrateName = partialInputs.programCrateName ?? `${organizationName}-${programName}`; return { jsClient: true, jsClientPackageName: `@${organizationName}/${programName}`, organizationName, programCrateName, programFramework: 'shank', programName, rustClient: true, rustClientCrateName: `${programCrateName}-client`, shouldOverride: false, targetDirectoryName: programName, useDefaults: false, ...partialInputs, }; } function canSkipEmptying(dir: fs.PathLike) { if (!fs.existsSync(dir)) { return true; } const files = fs.readdirSync(dir); if (files.length === 0) { return true; } if (files.length === 1 && files[0] === '.git') { return true; } return false; }