Files
xo-cli/src/cli/commands/template.ts

354 lines
12 KiB
TypeScript

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 <sub-command>
${bold("Sub-commands:")}
- import <template-file> ${dim("Import a template from a file")}
- list ${dim("List all templates")}
- list <category> <identifier> ${dim("List all options of the field type in a template")}
- inspect <category> <identifier> <field> ${dim("Inspect a field in a template")}
- set-default <template-file> <output-identifier> <role-identifier> ${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 <category> <identifier> <field>
${bold("Arguments:")}
<category> ${dim("The category of the template to inspect")}
<identifier> ${dim("The identifier of the template to inspect")}
<field> ${dim("The field of the template to inspect")}
${bold("Categories:")}
- action <action-identifier> ${dim("Inspect an action")}
- transaction <transaction-identifier> ${dim("Inspect a transaction")}
- output <output-identifier> ${dim("Inspect an output")}
- lockingscript <lockingscript-identifier> ${dim("Inspect a lockingscript")}
- variable <variable-identifier> ${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<Record<string, never>> => {
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<string, string>,
): 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}`,
);
}
};