import { existsSync, readFileSync } from "fs"; import path from "path"; import { generateTemplateIdentifier } from "@xo-cash/engine"; import type { XOTemplate } from "@xo-cash/types"; import { bold, dim, formatObject } from "../utils.js"; import { resolveTemplateReferences } from "../../utils/templates.js"; import type { CommandDependencies, CommandIO } from "./types.js"; import { CommandError } from "./types.js"; import { resolveTemplate } from "../utils.js"; /** * Prints the help message for the template command */ export const printTemplateHelp = (io: CommandIO): void => { io.out( ` ${bold("Usage:")} xo-cli template ${bold("Sub-commands:")} - import ${dim("Import a template from a file")} - list ${dim("List all templates")} - list ${dim("List all options of the field type in a template")} - inspect ${dim("Inspect a field in a template")} - set-default ${dim("Set the default template")} `, ); }; /** * Handles the template list command. * Throws CommandError on failure, returns result data on success. * @param deps - The command dependencies. * @param args - Positional args after the command name, e.g. ["list", "action"] or ["list", "action", "1234567890"]. */ export const handleTemplateListCommand = async ( deps: CommandDependencies, args: string[], ): Promise<{ count?: number }> => { const templateCategory = args[0]; deps.io.verbose(`Template list category: ${templateCategory}`); if (!templateCategory) { const templates = await deps.app.engine.listImportedTemplates(); const formattedTemplates = templates.map( (template: XOTemplate) => `${bold(generateTemplateIdentifier(template))} - ${dim(template.name)} ${dim(template.description)}`, ); deps.io.out(formattedTemplates.join("\n")); return { count: templates.length }; } const templateIdentifier = args[1]; deps.io.verbose(`Template identifier: ${templateIdentifier}`); if (!templateIdentifier) { deps.io.err("No template identifier provided"); throw new CommandError( "template.list.identifier_missing", "No template identifier provided", ); } const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier); if (!rawTemplate) { deps.io.err(`No template found: ${templateIdentifier}`); throw new CommandError( "template.list.not_found", `No template found: ${templateIdentifier}`, ); } const template = await resolveTemplateReferences(rawTemplate); deps.io.verbose(`Template: ${formatObject(template)}`); switch (templateCategory) { case "action": { const actions = template.actions; const formattedActions = Object.entries(actions).map( ([actionIdentifier, action]) => `${bold(actionIdentifier)} ${dim(action.name)} ${dim(action.description)}`, ); deps.io.out(formattedActions.join("\n")); return {}; } case "transaction": { const transactions = template.transactions; const formattedTransactions = Object.entries(transactions).map( ([transactionIdentifier, transaction]) => `${bold(transactionIdentifier)} ${dim(transaction.name)} ${dim(transaction.description)}`, ); deps.io.out(formattedTransactions.join("\n")); return {}; } case "output": { const outputs = template.outputs; const formattedOutputs = Object.entries(outputs).map( ([outputIdentifier, output]) => `${bold(outputIdentifier)} ${dim(output.name)} ${dim(output.description)}`, ); deps.io.out(formattedOutputs.join("\n")); return {}; } case "lockingscript": { const lockingscripts = template.lockingScripts; const formattedLockingscripts = Object.entries(lockingscripts).map( ([lockingScriptIdentifier, lockingScript]) => `${bold(lockingScriptIdentifier)} ${dim(lockingScript.name)} ${dim(lockingScript.description)}`, ); deps.io.out(formattedLockingscripts.join("\n")); return {}; } case "variable": { const variables = template.variables || {}; const formattedVariables = Object.entries(variables).map( ([variableIdentifier, variable]) => `${bold(variableIdentifier)} ${dim(variable.name)} ${dim(variable.description)}`, ); deps.io.out(formattedVariables.join("\n")); return {}; } default: { deps.io.verbose(`Unknown template category: ${templateCategory}`); throw new CommandError( "template.list.category_unknown", `Unknown template category: ${templateCategory}`, ); } } }; /** * Prints the help message for the template inspect command */ export const printTemplateInspectHelp = (io: CommandIO): void => { io.out( ` ${bold("Usage:")} xo-cli template inspect ${bold("Arguments:")} ${dim("The category of the template to inspect")} ${dim("The identifier of the template to inspect")} ${dim("The field of the template to inspect")} ${bold("Categories:")} - action ${dim("Inspect an action")} - transaction ${dim("Inspect a transaction")} - output ${dim("Inspect an output")} - lockingscript ${dim("Inspect a lockingscript")} - variable ${dim("Inspect a variable")} `, ); }; /** * Handles the template inspect command. * Throws CommandError on failure, returns empty object on success. * @param deps - The command dependencies. * @param args - Positional args after the command name, e.g. ["inspect", "transaction", "1234567890"]. */ export const handleTemplateInspectCommand = async ( deps: CommandDependencies, args: string[], ): Promise> => { const templateCategory = args[0]; const templateQuery = args[1]; const templateField = args[2]; deps.io.verbose( `Template inspect args - category: ${templateCategory}, identifier: ${templateQuery}, field: ${templateField}`, ); if (!templateCategory || !templateQuery || !templateField) { deps.io.err("No template category, identifier, or field provided"); printTemplateInspectHelp(deps.io); throw new CommandError( "template.inspect.arguments_missing", "No template category, identifier, or field provided", ); } const originalTemplate = await resolveTemplate(deps, templateQuery); deps.io.verbose(`Original Template: ${formatObject(originalTemplate)}`); const template = await resolveTemplateReferences(originalTemplate); deps.io.verbose(`Extended Template: ${formatObject(template)}`); switch (templateCategory) { case "action": { const action = template.actions[templateField]; if (!action) { deps.io.err(`No action found: ${templateField}`); throw new CommandError( "template.inspect.action_missing", `No action found: ${templateField}`, ); } deps.io.out(formatObject(action)); return {}; } case "transaction": { const transaction = template.transactions?.[templateField]; if (!transaction) { deps.io.err(`No transaction found: ${templateField}`); throw new CommandError( "template.inspect.transaction_missing", `No transaction found: ${templateField}`, ); } deps.io.out(formatObject(transaction)); return {}; } case "output": { const output = template.outputs[templateField]; if (!output) { deps.io.err(`No output found: ${templateField}`); throw new CommandError( "template.inspect.output_missing", `No output found: ${templateField}`, ); } deps.io.out(formatObject(output)); return {}; } case "lockingscript": { const lockingscript = template.lockingScripts[templateField]; if (!lockingscript) { deps.io.err(`No lockingscript found: ${templateField}`); throw new CommandError( "template.inspect.lockingscript_missing", `No lockingscript found: ${templateField}`, ); } deps.io.out(formatObject(lockingscript)); return {}; } case "variable": { const variable = template.variables?.[templateField]; if (!variable) { deps.io.err(`No variable found: ${templateField}`); throw new CommandError( "template.inspect.variable_missing", `No variable found: ${templateField}`, ); } deps.io.out(formatObject(variable)); return {}; } default: { deps.io.verbose(`Unknown template category: ${templateCategory}`); throw new CommandError( "template.inspect.category_unknown", `Unknown template category: ${templateCategory}`, ); } } }; /** * Handles the template command. * Throws CommandError on failure, returns result data on success. * @param deps - The command dependencies. * @param args - Positional args after the command name, e.g. ["import", "template.json"] or ["set-default", "tpl", "out", "role"]. * @param options - Parsed option flags. */ export const handleTemplateCommand = async ( deps: CommandDependencies, args: string[], _options: Record, ): Promise<{ templateFile?: string; count?: number }> => { const subCommand = args[0]; if (!subCommand) { deps.io.verbose("No sub-command provided"); printTemplateHelp(deps.io); throw new CommandError( "template.subcommand.missing", "No sub-command provided", ); } switch (subCommand) { case "import": { const templateFile = args[1]; deps.io.verbose(`Template file: ${templateFile}`); if (!templateFile) { deps.io.verbose("No template file provided"); printTemplateHelp(deps.io); throw new CommandError( "template.import.file_missing", "No template file provided", ); } const templatePath = path.resolve(`${process.cwd()}/${templateFile}`); deps.io.verbose(`Template path: ${templatePath}`); if (!existsSync(templatePath)) { deps.io.err(`Template file does not exist: ${templatePath}`); printTemplateHelp(deps.io); throw new CommandError( "template.import.file_not_found", `Template file does not exist: ${templatePath}`, ); } const template = await readFileSync(templatePath, "utf8"); deps.io.verbose(`Importing template: ${templateFile}`); await deps.app.engine.importTemplate(template); deps.io.verbose(`Template imported: ${templateFile}`); return { templateFile }; } case "list": { return handleTemplateListCommand(deps, args.slice(1)); } case "inspect": { return handleTemplateInspectCommand(deps, args.slice(1)); } case "set-default": { const templateFile = args[1]; const outputIdentifier = args[2]; const roleIdentifier = args[3]; if (!templateFile || !outputIdentifier || !roleIdentifier) { deps.io.verbose( "No template file, output identifier, or role identifier provided", ); printTemplateHelp(deps.io); throw new CommandError( "template.default.arguments_missing", "No template file, output identifier, or role identifier provided", ); } deps.io.verbose( `Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`, ); await deps.app.engine.setDefaultLockingParameters( templateFile, outputIdentifier, roleIdentifier, ); return {}; } default: deps.io.verbose(`Unknown template sub-command: ${subCommand}`); printTemplateHelp(deps.io); throw new CommandError( "template.subcommand.unknown", `Unknown template sub-command: ${subCommand}`, ); } };