551 lines
19 KiB
TypeScript
551 lines
19 KiB
TypeScript
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 <sub-command>
|
|
|
|
${bold("Sub-commands:")}
|
|
- import <template-file> ${dim("Import a template from a JSON, JS, or TS 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")}
|
|
- export <template-identifier> [output-file] ${dim("Export a template to stdout or a file")}
|
|
|
|
${bold("Options:")}
|
|
-o --output <output-filename> ${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 <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>> => {
|
|
// 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<string, string>,
|
|
): 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<string, string>,
|
|
): 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}`,
|
|
);
|
|
}
|
|
};
|