Tests. Autocomplete. Few Fixes. Mocks for Electrum Service. Template-to-Json parser. Fix global paths. Use IO Dependency injection for logging from cli. Additional commands in CLI.

This commit is contained in:
2026-04-20 10:30:38 +00:00
parent df4f438f6d
commit ff2fe126c6
44 changed files with 8220 additions and 1503 deletions

View File

@@ -3,15 +3,17 @@ import path from "path";
import { generateTemplateIdentifier } from "@xo-cash/engine";
import type { XOTemplate } from "@xo-cash/types";
import { bold, dim, formatObject, objectPrint } from "../cli-utils.js";
import { bold, dim, formatObject } from "../cli-utils.js";
import { resolveTemplateReferences } from "../../utils/templates.js";
import type { CommandDependencies } from "./types.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 = () => {
console.log(
export const printTemplateHelp = (io: CommandIO): void => {
io.out(
`
${bold("Usage:")} xo-cli template <sub-command>
@@ -26,76 +28,75 @@ ${bold("Sub-commands:")}
/**
* 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<void> => {
export const handleTemplateListCommand = async (
deps: CommandDependencies,
args: string[],
): Promise<{ count?: number }> => {
const templateCategory = args[0];
deps.verboseLogger(`Template list category: ${templateCategory}`);
deps.io.verbose(`Template list category: ${templateCategory}`);
// If no category was provided to list, we assume its listing out the templates
if (!templateCategory) {
const templates = await deps.app.engine.listImportedTemplates();
const formattedTemplates = templates.map((template: XOTemplate) => `${bold(generateTemplateIdentifier(template))} - ${dim(template.name)} ${dim(template.description)}`);
console.log(formattedTemplates.join('\n'));
return;
deps.io.out(formattedTemplates.join('\n'));
return { count: templates.length };
}
// Extract the template identifier from the positional args
const templateIdentifier = args[1];
deps.verboseLogger(`Template identifier: ${templateIdentifier}`);
deps.io.verbose(`Template identifier: ${templateIdentifier}`);
if (!templateIdentifier) {
console.error("No template identifier provided");
return;
deps.io.err("No template identifier provided");
throw new CommandError("template.list.identifier_missing", "No template identifier provided");
}
// Get the template from the engine
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
if (!rawTemplate) {
console.error(`No template found: ${templateIdentifier}`);
return;
deps.io.err(`No template found: ${templateIdentifier}`);
throw new CommandError("template.list.not_found", `No template found: ${templateIdentifier}`);
}
// Resolve the template references
const template = await resolveTemplateReferences(rawTemplate);
deps.verboseLogger(`Template: ${formatObject(template)}`);
deps.io.verbose(`Template: ${formatObject(template)}`);
// List the templates in the category
switch (templateCategory) {
case "action": {
const actions = template.actions;
const formattedActions = Object.entries(actions).map(([actionIdentifier, action]) => `${bold(actionIdentifier)} ${dim(action.name)} ${dim(action.description)}`);
console.log(formattedActions.join('\n'));
break;
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)}`);
console.log(formattedTransactions.join('\n'));
break;
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)}`);
console.log(formattedOutputs.join('\n'));
break;
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)}`);
console.log(formattedLockingscripts.join('\n'));
break;
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)}`);
console.log(formattedVariables.join('\n'));
break;
deps.io.out(formattedVariables.join('\n'));
return {};
}
default: {
deps.verboseLogger(`Unknown template category: ${templateCategory}`);
return;
deps.io.verbose(`Unknown template category: ${templateCategory}`);
throw new CommandError("template.list.category_unknown", `Unknown template category: ${templateCategory}`);
}
}
}
@@ -103,8 +104,8 @@ export const handleTemplateListCommand = async (deps: CommandDependencies, args:
/**
* Prints the help message for the template inspect command
*/
export const printTemplateInspectHelp = () => {
console.log(
export const printTemplateInspectHelp = (io: CommandIO): void => {
io.out(
`
${bold("Usage:")} xo-cli template inspect <category> <identifier> <field>
@@ -124,153 +125,151 @@ ${bold("Categories:")}
/**
* 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<void> => {
export const handleTemplateInspectCommand = async (
deps: CommandDependencies,
args: string[],
): Promise<Record<string, never>> => {
const templateCategory = args[0];
const templateIdentifier = args[1];
const templateQuery = args[1];
const templateField = args[2];
deps.verboseLogger(`Template inspect args - category: ${templateCategory}, identifier: ${templateIdentifier}, field: ${templateField}`);
deps.io.verbose(`Template inspect args - category: ${templateCategory}, identifier: ${templateQuery}, field: ${templateField}`);
if (!templateCategory || !templateIdentifier || !templateField) {
console.log("No template category, identifier, or field provided");
printTemplateInspectHelp();
return;
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");
}
// Get the template from the engine
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
if (!rawTemplate) {
console.error(`No template found: ${templateIdentifier}`);
return;
}
const template = await resolveTemplate(deps, templateQuery);
deps.io.verbose(`Template: ${formatObject(template)}`);
// Resolve the template references
const template = await resolveTemplateReferences(rawTemplate);
deps.verboseLogger(`Template: ${formatObject(template)}`);
// Inspect the template in the category
switch (templateCategory) {
case "action": {
const action = template.actions[templateField];
if (!action) {
console.error(`No action found: ${templateField}`);
return;
deps.io.err(`No action found: ${templateField}`);
throw new CommandError("template.inspect.action_missing", `No action found: ${templateField}`);
}
objectPrint(action);
break;
deps.io.out(formatObject(action));
return {};
}
case "transaction": {
const transaction = template.transactions[templateField];
const transaction = template.transactions?.[templateField];
if (!transaction) {
console.error(`No transaction found: ${templateField}`);
return;
deps.io.err(`No transaction found: ${templateField}`);
throw new CommandError("template.inspect.transaction_missing", `No transaction found: ${templateField}`);
}
objectPrint(transaction);
break;
deps.io.out(formatObject(transaction));
return {};
}
case "output": {
const output = template.outputs[templateField];
if (!output) {
console.error(`No output found: ${templateField}`);
return;
deps.io.err(`No output found: ${templateField}`);
throw new CommandError("template.inspect.output_missing", `No output found: ${templateField}`);
}
objectPrint(output);
break;
deps.io.out(formatObject(output));
return {};
}
case "lockingscript": {
const lockingscript = template.lockingScripts[templateField];
if (!lockingscript) {
console.error(`No lockingscript found: ${templateField}`);
return;
deps.io.err(`No lockingscript found: ${templateField}`);
throw new CommandError("template.inspect.lockingscript_missing", `No lockingscript found: ${templateField}`);
}
objectPrint(lockingscript);
break;
deps.io.out(formatObject(lockingscript));
return {};
}
case "variable": {
const variable = template.variables?.[templateField];
if (!variable) {
console.error(`No variable found: ${templateField}`);
return;
deps.io.err(`No variable found: ${templateField}`);
throw new CommandError("template.inspect.variable_missing", `No variable found: ${templateField}`);
}
objectPrint(variable);
break;
deps.io.out(formatObject(variable));
return {};
}
default: {
deps.verboseLogger(`Unknown template category: ${templateCategory}`);
return;
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<void> => {
export const handleTemplateCommand = async (
deps: CommandDependencies,
args: string[],
_options: Record<string, string>,
): Promise<{ templateFile?: string; count?: number }> => {
const subCommand = args[0];
if (!subCommand) {
deps.verboseLogger("No sub-command provided");
printTemplateHelp();
return;
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.verboseLogger(`Template file: ${templateFile}`);
deps.io.verbose(`Template file: ${templateFile}`);
if (!templateFile) {
deps.verboseLogger("No template file provided");
printTemplateHelp();
return;
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.verboseLogger(`Template path: ${templatePath}`);
deps.io.verbose(`Template path: ${templatePath}`);
if (!existsSync(templatePath)) {
console.error(`Template file does not exist: ${templatePath}`);
printTemplateHelp();
return;
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.verboseLogger(`Importing template: ${templateFile}`);
deps.io.verbose(`Importing template: ${templateFile}`);
await deps.app.engine.importTemplate(template);
deps.verboseLogger(`Template imported: ${templateFile}`);
break;
deps.io.verbose(`Template imported: ${templateFile}`);
return { templateFile };
}
case "list": {
await handleTemplateListCommand(deps, args.slice(1));
break;
return handleTemplateListCommand(deps, args.slice(1));
}
case "inspect": {
await handleTemplateInspectCommand(deps, args.slice(1));
break;
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.verboseLogger("No template file, output identifier, or role identifier provided");
printTemplateHelp();
return;
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.verboseLogger(`Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`);
deps.io.verbose(`Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`);
await deps.app.engine.setDefaultLockingParameters(templateFile, outputIdentifier, roleIdentifier);
break;
return {};
}
default:
deps.verboseLogger(`Unknown template sub-command: ${subCommand}`);
printTemplateHelp();
return;
deps.io.verbose(`Unknown template sub-command: ${subCommand}`);
printTemplateHelp(deps.io);
throw new CommandError("template.subcommand.unknown", `Unknown template sub-command: ${subCommand}`);
}
};