import { existsSync, writeFileSync } 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 { loadTemplateFromFile, TemplateLoadError, } from "../../utils/load-template-from-file.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 JSON, JS, or TS 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")} - export [output-file] ${dim("Export a template to stdout or a file")} ${bold("Options:")} -o --output ${dim("Output filename for the exported 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 }> => { // Get the template category from the arguments - This could be "action", "transaction", "output", "lockingscript", or "variable" const templateCategory = args[0]; deps.io.verbose(`Template list category: ${templateCategory}`); // If no template category is provided, list all the imported templates if (!templateCategory) { // List all the imported templates const templates = await deps.app.engine.listImportedTemplates(); // Format the templates into a list of strings that we can display to the user const formattedTemplates = templates.map( (template: XOTemplate) => `${bold(generateTemplateIdentifier(template))} - ${dim(template.name)} ${dim(template.description)}`, ); // Display the templates to the user deps.io.out(formattedTemplates.join("\n")); // Return the number of templates return { count: templates.length }; } // Get the template identifier from the arguments const templateIdentifier = args[1]; deps.io.verbose(`Template identifier: ${templateIdentifier}`); // If no template identifier is provided, print a message and throw an error if (!templateIdentifier) { deps.io.err("No template identifier provided"); throw new CommandError( "template.list.identifier_missing", "No template identifier provided", ); } // Get the raw template from the engine const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier); // If the raw template is not found, print a message and throw an error if (!rawTemplate) { deps.io.err(`No template found: ${templateIdentifier}`); throw new CommandError( "template.list.not_found", `No template found: ${templateIdentifier}`, ); } // Resolve the template deeply - Deeply nested objects instead of shallow objects referencing keys at the top level. // Reduces the load of having to call multiple lookups just to get some resolved value like the outputIdentifer that comes from calling an action. const template = await resolveTemplateReferences(rawTemplate); deps.io.verbose(`Template: ${formatObject(template)}`); // Handle the template category switch (templateCategory) { case "action": { // Get the actions from the template const actions = template.actions; // Format the actions into a list of strings that we can display to the user const formattedActions = Object.entries(actions).map( ([actionIdentifier, action]) => `${bold(actionIdentifier)} ${dim(action.name)} ${dim(action.description)}`, ); // Display the actions to the user deps.io.out(formattedActions.join("\n")); // Return the number of actions return {}; } case "transaction": { // Get the transactions from the template const transactions = template.transactions; // Format the transactions into a list of strings that we can display to the user const formattedTransactions = Object.entries(transactions).map( ([transactionIdentifier, transaction]) => `${bold(transactionIdentifier)} ${dim(transaction.name)} ${dim(transaction.description)}`, ); // Display the transactions to the user deps.io.out(formattedTransactions.join("\n")); // Return the number of transactions return {}; } case "output": { // Get the outputs from the template const outputs = template.outputs; // Format the outputs into a list of strings that we can display to the user const formattedOutputs = Object.entries(outputs).map( ([outputIdentifier, output]) => `${bold(outputIdentifier)} ${dim(output.name)} ${dim(output.description)}`, ); // Display the outputs to the user deps.io.out(formattedOutputs.join("\n")); // Return the number of outputs return {}; } case "lockingscript": { // Get the lockingscripts from the template const lockingscripts = template.lockingScripts; // Format the lockingscripts into a list of strings that we can display to the user const formattedLockingscripts = Object.entries(lockingscripts).map( ([lockingScriptIdentifier, lockingScript]) => `${bold(lockingScriptIdentifier)} ${dim(lockingScript.name)} ${dim(lockingScript.description)}`, ); // Display the lockingscripts to the user deps.io.out(formattedLockingscripts.join("\n")); // Return the number of lockingscripts return {}; } case "variable": { // Get the variables from the template const variables = template.variables || {}; // Format the variables into a list of strings that we can display to the user const formattedVariables = Object.entries(variables).map( ([variableIdentifier, variable]) => `${bold(variableIdentifier)} ${dim(variable.name)} ${dim(variable.description)}`, ); // Display the variables to the user deps.io.out(formattedVariables.join("\n")); // Return the number of variables 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> => { // Get the template category, identifier, and field from the arguments 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 no template category, identifier, or field is provided, print a message and throw an error 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", ); } // Resolve the template const originalTemplate = await resolveTemplate(deps, templateQuery); deps.io.verbose(`Original Template: ${formatObject(originalTemplate)}`); // Resolve the template references const template = await resolveTemplateReferences(originalTemplate); deps.io.verbose(`Extended Template: ${formatObject(template)}`); // Handle the template category switch (templateCategory) { case "action": { // Get the action from the template const action = template.actions[templateField]; // If the action is not found, print a message and throw an error if (!action) { deps.io.err(`No action found: ${templateField}`); throw new CommandError( "template.inspect.action_missing", `No action found: ${templateField}`, ); } // Display the action to the user deps.io.out(formatObject(action)); return {}; } case "transaction": { // Get the transaction from the template const transaction = template.transactions?.[templateField]; // If the transaction is not found, print a message and throw an error if (!transaction) { deps.io.err(`No transaction found: ${templateField}`); throw new CommandError( "template.inspect.transaction_missing", `No transaction found: ${templateField}`, ); } // Display the transaction to the user deps.io.out(formatObject(transaction)); return {}; } case "output": { // Get the output from the template const output = template.outputs[templateField]; // If the output is not found, print a message and throw an error if (!output) { deps.io.err(`No output found: ${templateField}`); throw new CommandError( "template.inspect.output_missing", `No output found: ${templateField}`, ); } // Display the output to the user deps.io.out(formatObject(output)); return {}; } case "lockingscript": { // Get the lockingscript from the template const lockingscript = template.lockingScripts[templateField]; // If the lockingscript is not found, print a message and throw an error if (!lockingscript) { deps.io.err(`No lockingscript found: ${templateField}`); throw new CommandError( "template.inspect.lockingscript_missing", `No lockingscript found: ${templateField}`, ); } // Display the lockingscript to the user deps.io.out(formatObject(lockingscript)); return {}; } case "variable": { // Get the variable from the template const variable = template.variables?.[templateField]; // If the variable is not found, print a message and throw an error if (!variable) { deps.io.err(`No variable found: ${templateField}`); throw new CommandError( "template.inspect.variable_missing", `No variable found: ${templateField}`, ); } // Display the variable to the user 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 export command. * Throws CommandError on failure, returns result data on success. * @param deps - The command dependencies. * @param args - Positional args after "export", e.g. ["template-id"] or ["template-id", "template.json"]. * @param options - Parsed option flags. */ export const handleTemplateExportCommand = async ( deps: CommandDependencies, args: string[], options: Record, ): Promise<{ outputFile?: string }> => { // Get the template identifier from the arguments const templateIdentifier = args[0]; // If no template identifier is provided, print a message and throw an error if (!templateIdentifier) { deps.io.err("No template identifier provided"); printTemplateHelp(deps.io); throw new CommandError( "template.export.identifier_missing", "No template identifier provided", ); } // Get the raw template from the engine. // Do not resolve references or pretty-print the template. const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier); // If the raw template is not found, print a message and throw an error if (!rawTemplate) { deps.io.err(`No template found: ${templateIdentifier}`); throw new CommandError( "template.export.not_found", `No template found: ${templateIdentifier}`, ); } // Serialize the template without indentation to preserve the engine output shape. const serializedTemplate = JSON.stringify(rawTemplate); // Resolve output file from --output (or -o), then fallback to optional positional output file const outputFile = options["output"] ?? args[1]; // If no output file is provided, print the template to stdout if (!outputFile) { deps.io.out(serializedTemplate); return {}; } // Resolve output file path and write the template to disk const outputPath = path.resolve(process.cwd(), outputFile); try { writeFileSync(outputPath, serializedTemplate); } catch (error) { throw new CommandError( "template.export.write_failed", `Failed to export template to file: ${outputPath} (${error instanceof Error ? error.message : "unknown error"})`, ); } deps.io.out(`Template exported to: ${outputPath}`); return { outputFile: outputPath }; }; /** * 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; outputFile?: string }> => { // Get the sub-command from the arguments const subCommand = args[0]; // If no sub-command is provided, print a message and throw an error if (!subCommand) { deps.io.verbose("No sub-command provided"); printTemplateHelp(deps.io); throw new CommandError( "template.subcommand.missing", "No sub-command provided", ); } // Handle the sub-command switch (subCommand) { case "import": { // Get the template file from the arguments const templateFile = args[1]; // If no template file is provided, print a message and throw an error 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", ); } // Resolve the template path const templatePath = path.resolve(`${process.cwd()}/${templateFile}`); deps.io.verbose(`Template path: ${templatePath}`); // If the template file does not exist, print a message and throw an error 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}`, ); } // Read and load the template file (JSON directly, TS/JS via child process). let templateContents: string; try { templateContents = await loadTemplateFromFile(templatePath); } catch (error) { const message = error instanceof TemplateLoadError ? error.message : error instanceof Error ? error.message : String(error); deps.io.err(message); printTemplateHelp(deps.io); throw new CommandError("template.import.load_failed", message); } deps.io.verbose(`Importing template: ${templateFile}`); // Import the template await deps.app.engine.importTemplate(templateContents); deps.io.verbose(`Template imported: ${templateFile}`); // Return the template file return { templateFile }; } case "list": { // Handle the template list command, We offload here as it has lots of arguments and is quite long return handleTemplateListCommand(deps, args.slice(1)); } case "inspect": { // Handle the template inspect command, We offload here as it has lots of arguments and is quite long return handleTemplateInspectCommand(deps, args.slice(1)); } case "export": { // Handle the template export command return handleTemplateExportCommand(deps, args.slice(1), options); } case "set-default": { // Get the template file, output identifier, and role identifier from the arguments const templateFile = args[1]; const outputIdentifier = args[2]; const roleIdentifier = args[3]; // If no template file, output identifier, or role identifier is provided, print a message and throw an error 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", ); } // Set the default locking parameters deps.io.verbose( `Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`, ); // Set the default locking parameters await deps.app.engine.setDefaultLockingParameters( templateFile, outputIdentifier, roleIdentifier, ); // Return an empty object return {}; } default: // If the sub-command is not found, print a message and throw an error deps.io.verbose(`Unknown template sub-command: ${subCommand}`); printTemplateHelp(deps.io); throw new CommandError( "template.subcommand.unknown", `Unknown template sub-command: ${subCommand}`, ); } };