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

@@ -1,10 +1,11 @@
import { existsSync, readFileSync, writeFileSync } from "fs";
import { readFileSync, writeFileSync } from "fs";
import path from "path";
import { generateTemplateIdentifier } from "@xo-cash/engine";
import { binToHex, hexToBin } from "@bitauth/libauth";
import { bold, dim, formatObject } from "../cli-utils.js";
import type { CommandDependencies } from "./types.js";
import type { CommandDependencies, CommandIO } from "./types.js";
import { CommandError } from "./types.js";
import type { Invitation } from "../../services/invitation.js";
import {
resolveProvidedLockingBytecodeHex,
@@ -12,6 +13,7 @@ import {
autoSelectGreedyUtxos,
} from "../../utils/invitation-flow.js";
import { encodeExtendedJson } from "../../utils/ext-json.js";
import { resolveTemplate } from "../utils.js";
const DEFAULT_FEE = 500n;
const DUST_THRESHOLD = 546n;
@@ -89,10 +91,10 @@ async function buildAppendParams(
}));
if (inputs.length === 0) {
console.error("No suitable UTXOs found for auto-input selection.");
deps.io.err("No suitable UTXOs found for auto-input selection.");
return null;
}
deps.verboseLogger(`Auto-selected ${inputs.length} input(s)`);
deps.io.verbose(`Auto-selected ${inputs.length} input(s)`);
} else if (options["addInput"]) {
inputs = options["addInput"].split(",").map((entry) => {
const separatorIndex = entry.lastIndexOf(":");
@@ -110,7 +112,7 @@ async function buildAppendParams(
};
});
}
deps.verboseLogger(`Inputs: ${formatObject(inputs.map(i => ({ txHash: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })))}`);
deps.io.verbose(`Inputs: ${formatObject(inputs.map(i => ({ txHash: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })))}`);
// --- Outputs ---
// When --add-output is provided, use those identifiers explicitly.
@@ -133,7 +135,7 @@ async function buildAppendParams(
}
outputIdentifiers = [...discovered];
if (outputIdentifiers.length > 0) {
deps.verboseLogger(`Auto-discovered output(s) from template: ${outputIdentifiers.join(", ")}`);
deps.io.verbose(`Auto-discovered output(s) from template: ${outputIdentifiers.join(", ")}`);
}
}
@@ -161,14 +163,14 @@ async function buildAppendParams(
const lockingBytecodeHex = providedHex
?? await invitation.generateLockingBytecode(outputId, roleIdentifier);
deps.verboseLogger(`Locking bytecode for output "${outputId}": ${lockingBytecodeHex}`);
deps.io.verbose(`Locking bytecode for output "${outputId}": ${lockingBytecodeHex}`);
return {
outputIdentifier: outputId,
lockingBytecode: new Uint8Array(Buffer.from(lockingBytecodeHex, "hex")),
};
}),
);
deps.verboseLogger(`Outputs: ${formatObject(outputs.map(o => o.outputIdentifier))}`);
deps.io.verbose(`Outputs: ${formatObject(outputs.map(o => o.outputIdentifier))}`);
// --- Auto change output ---
// When inputs are provided, look up each UTXO's value, compute the
@@ -182,29 +184,29 @@ async function buildAppendParams(
const txHashHex = binToHex(input.outpointTransactionHash);
const utxo = utxoMap.get(`${txHashHex}:${input.outpointIndex}`);
if (!utxo) {
console.error(`UTXO not found: ${txHashHex}:${input.outpointIndex}. Make sure it exists in your wallet.`);
deps.io.err(`UTXO not found: ${txHashHex}:${input.outpointIndex}. Make sure it exists in your wallet.`);
return null;
}
totalInputSats += BigInt(utxo.valueSatoshis);
}
deps.verboseLogger(`Total input value: ${totalInputSats} satoshis`);
deps.io.verbose(`Total input value: ${totalInputSats} satoshis`);
const requiredSats = await invitation.getSatsOut();
deps.verboseLogger(`Required output value: ${requiredSats} satoshis`);
deps.io.verbose(`Required output value: ${requiredSats} satoshis`);
const changeAmount = totalInputSats - requiredSats - DEFAULT_FEE;
deps.verboseLogger(`Change amount: ${changeAmount} satoshis (fee: ${DEFAULT_FEE})`);
deps.io.verbose(`Change amount: ${changeAmount} satoshis (fee: ${DEFAULT_FEE})`);
if (changeAmount < 0n) {
console.error(`Insufficient funds. Inputs total ${totalInputSats} sats, but need ${requiredSats + DEFAULT_FEE} sats (${requiredSats} required + ${DEFAULT_FEE} fee).`);
deps.io.err(`Insufficient funds. Inputs total ${totalInputSats} sats, but need ${requiredSats + DEFAULT_FEE} sats (${requiredSats} required + ${DEFAULT_FEE} fee).`);
return null;
}
if (changeAmount >= DUST_THRESHOLD) {
outputs.push({ valueSatoshis: changeAmount });
console.log(`Auto-adding change output: ${changeAmount} satoshis`);
deps.io.out(`Auto-adding change output: ${changeAmount} satoshis`);
} else if (changeAmount > 0n) {
console.log(`Change ${changeAmount} sats is below dust threshold (${DUST_THRESHOLD} sats), donating to miners as fee.`);
deps.io.out(`Change ${changeAmount} sats is below dust threshold (${DUST_THRESHOLD} sats), donating to miners as fee.`);
}
}
@@ -214,8 +216,8 @@ async function buildAppendParams(
/**
* Prints the help message for the invitation command
*/
export const printInvitationHelp = () => {
console.log(
export const printInvitationHelp = (io: CommandIO): void => {
io.out(
`
${bold("Usage:")} xo-cli invitation <sub-command>
@@ -242,82 +244,87 @@ ${bold("Create / Append options:")}
`);
};
/**
* Result data returned by invitation commands on success.
*/
export type InvitationCommandResult = {
invitationIdentifier?: string;
txHash?: string;
count?: number;
templateName?: string;
actionIdentifier?: string;
status?: string;
entities?: { entityIdentifier: string; roles: (string | undefined)[] }[];
inputs?: unknown[];
outputs?: unknown[];
variables?: unknown[];
};
/**
* Handles the invitation 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. ["create", "template.json", "action-id"].
* @param options - Parsed option flags, e.g. { varRequestedSatohis: "1000", role: "receiver" }.
*/
export const handleInvitationCommand = async (deps: CommandDependencies, args: string[], options: Record<string, string>): Promise<void> => {
export const handleInvitationCommand = async (
deps: CommandDependencies,
args: string[],
options: Record<string, string>,
): Promise<InvitationCommandResult> => {
const subCommand = args[0];
deps.verboseLogger(`Invitation sub-command: ${subCommand}`);
deps.io.verbose(`Invitation sub-command: ${subCommand}`);
if (!subCommand) {
deps.verboseLogger("No sub-command provided");
printInvitationHelp();
return;
deps.io.verbose("No sub-command provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.subcommand.missing", "No sub-command provided");
}
switch (subCommand) {
case "create": {
const templateFile = args[1];
const templateQuery = args[1];
const actionIdentifier = args[2];
deps.verboseLogger(`Template file: ${templateFile}, action identifier: ${actionIdentifier}`);
deps.io.verbose(`Template query: ${templateQuery}, action identifier: ${actionIdentifier}`);
if (!templateFile || !actionIdentifier) {
deps.verboseLogger("No template file or action identifier provided");
printInvitationHelp();
return;
if (!templateQuery || !actionIdentifier) {
deps.io.verbose("No template file or action identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.create.arguments_missing", "No template file or action identifier provided");
}
// Resolve and validate the template file path
const templatePath = path.resolve(`${process.cwd()}/${templateFile}`);
deps.verboseLogger(`Template path: ${templatePath}`);
if (!existsSync(templatePath)) {
console.error(`Template file does not exist: ${templatePath}`);
printInvitationHelp();
return;
}
const template = await readFileSync(templatePath, "utf8");
const templateIdentifier = generateTemplateIdentifier(JSON.parse(template));
// Create the base invitation via the engine
const template = await resolveTemplate(deps, templateQuery);
const templateIdentifier = generateTemplateIdentifier(template);
const rawInvitation = await deps.app.engine.createInvitation({
templateIdentifier: templateIdentifier,
actionIdentifier: actionIdentifier,
templateIdentifier,
actionIdentifier,
});
deps.verboseLogger(`XOInvitation created: ${formatObject(rawInvitation)}`);
deps.io.verbose(`XOInvitation created: ${formatObject(rawInvitation)}`);
const invitationInstance = await deps.app.createInvitation(rawInvitation);
deps.verboseLogger(`Invitation created: ${formatObject(invitationInstance.data)}`);
deps.io.verbose(`Invitation created: ${formatObject(invitationInstance.data)}`);
// Commit variables first so getSatsOut can resolve them for change calc
// and resolveProvidedLockingBytecodeHex can read them from commits.
const variables = parseVariablesFromOptions(options);
deps.verboseLogger(`Variables: ${formatObject(variables)}`);
deps.io.verbose(`Variables: ${formatObject(variables)}`);
if (variables.length > 0) {
await invitationInstance.addVariables(variables);
}
// Parse inputs/outputs and calculate change (variables are now committed)
const params = await buildAppendParams(deps, invitationInstance, options);
if (!params) return;
if (!params) {
throw new CommandError("invitation.create.append_params_failed", "Failed to build append parameters");
}
const { inputs, outputs } = params;
if (inputs.length > 0 || outputs.length > 0) {
await invitationInstance.append({ inputs, outputs });
}
// Save the invitation to a file
const invitationFilePath = `${process.cwd()}/inv-${invitationInstance.data.invitationIdentifier}.json`;
deps.verboseLogger(`Invitation file path: ${invitationFilePath}`);
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitationInstance.data.invitationIdentifier}.json`;
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
writeFileSync(invitationFilePath, encodeExtendedJson(invitationInstance.data, 2));
console.log(`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`);
deps.io.out(`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`);
// Check remaining requirements
const missingRequirements = await invitationInstance.getMissingRequirements();
const hasMissing =
(missingRequirements.variables?.length ?? 0) > 0 ||
@@ -326,76 +333,74 @@ export const handleInvitationCommand = async (deps: CommandDependencies, args: s
(missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0);
if (hasMissing) {
console.log(`\n${bold("Remaining requirements:")}`);
console.log(formatObject(missingRequirements));
deps.io.out(`\n${bold("Remaining requirements:")}`);
deps.io.out(formatObject(missingRequirements));
} else {
// --broadcast implies --sign
const shouldSign = options["sign"] === "true" || options["broadcast"] === "true";
const shouldBroadcast = options["broadcast"] === "true";
if (shouldSign) {
await invitationInstance.sign();
console.log(`Invitation signed: ${invitationInstance.data.invitationIdentifier}`);
deps.io.out(`Invitation signed: ${invitationInstance.data.invitationIdentifier}`);
}
if (shouldBroadcast) {
const txHash = await invitationInstance.broadcast();
console.log(`Transaction broadcast: ${bold(txHash)}`);
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
} else if (!shouldSign) {
console.log(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationInstance.data.invitationIdentifier}`);
deps.io.out(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationInstance.data.invitationIdentifier}`);
}
}
break;
return { invitationIdentifier: invitationInstance.data.invitationIdentifier };
}
case "append": {
const invitationIdentifier = args[1];
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
if (!invitationIdentifier) {
deps.verboseLogger("No invitation identifier provided");
printInvitationHelp();
return;
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.append.identifier_missing", "No invitation identifier provided");
}
// Find the invitation by identifier
const invitation = deps.app.invitations.find(inv => inv.data.invitationIdentifier === invitationIdentifier);
const invitation = deps.app.invitations.find(
(inv) => inv.data.invitationIdentifier === invitationIdentifier,
);
if (!invitation) {
console.error(`Invitation not found: ${invitationIdentifier}`);
return;
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError("invitation.append.not_found", `Invitation not found: ${invitationIdentifier}`);
}
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
// Commit variables first so getSatsOut can resolve them for change calc
const variables = parseVariablesFromOptions(options);
deps.verboseLogger(`Variables to append: ${formatObject(variables)}`);
deps.io.verbose(`Variables to append: ${formatObject(variables)}`);
if (variables.length > 0) {
await invitation.addVariables(variables);
}
// Parse inputs/outputs and calculate change (variables are now committed)
const params = await buildAppendParams(deps, invitation, options);
if (!params) return;
if (!params) {
throw new CommandError("invitation.append.params_failed", "Failed to build append parameters");
}
const { inputs, outputs } = params;
if (variables.length === 0 && inputs.length === 0 && outputs.length === 0) {
console.error("Nothing to append. Provide variables (-var-<name> <value>), inputs (--add-input <txhash>:<vout>), or outputs (--add-output <identifier>).");
return;
const error = "Nothing to append. Provide variables (-var-<name> <value>), inputs (--add-input <txhash>:<vout>), or outputs (--add-output <identifier>).";
deps.io.err(error);
throw new CommandError("invitation.append.empty", error);
}
if (inputs.length > 0 || outputs.length > 0) {
await invitation.append({ inputs, outputs });
}
deps.verboseLogger(`Invitation appended: ${formatObject(invitation.data)}`);
console.log(`Invitation appended: ${invitationIdentifier}`);
deps.io.verbose(`Invitation appended: ${formatObject(invitation.data)}`);
deps.io.out(`Invitation appended: ${invitationIdentifier}`);
// Save the updated invitation to a file
const invitationFilePath = `${process.cwd()}/inv-${invitation.data.invitationIdentifier}.json`;
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitation.data.invitationIdentifier}.json`;
writeFileSync(invitationFilePath, encodeExtendedJson(invitation.data, 2));
console.log(`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`);
deps.io.out(`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`);
// Check remaining requirements
const missingRequirements = await invitation.getMissingRequirements();
const hasMissing =
(missingRequirements.variables?.length ?? 0) > 0 ||
@@ -404,159 +409,212 @@ export const handleInvitationCommand = async (deps: CommandDependencies, args: s
(missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0);
if (hasMissing) {
console.log(`\n${bold("Remaining requirements:")}`);
console.log(formatObject(missingRequirements));
deps.io.out(`\n${bold("Remaining requirements:")}`);
deps.io.out(formatObject(missingRequirements));
} else {
const shouldSign = options["sign"] === "true" || options["broadcast"] === "true";
const shouldBroadcast = options["broadcast"] === "true";
if (shouldSign) {
await invitation.sign();
console.log(`Invitation signed: ${invitationIdentifier}`);
deps.io.out(`Invitation signed: ${invitationIdentifier}`);
}
if (shouldBroadcast) {
const txHash = await invitation.broadcast();
console.log(`Transaction broadcast: ${bold(txHash)}`);
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
} else if (!shouldSign) {
console.log(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationIdentifier}`);
deps.io.out(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationIdentifier}`);
}
}
break;
return { invitationIdentifier };
}
case "sign": {
const invitationIdentifier = args[1];
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
// Check if the invitation identifier is provided
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
if (!invitationIdentifier) {
deps.verboseLogger("No invitation identifier provided");
printInvitationHelp();
return;
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.sign.identifier_missing", "No invitation identifier provided");
}
// Find the invitation by identifier
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
const invitation = deps.app.invitations.find(
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
);
if (!invitation) {
console.error(`Invitation not found: ${invitationIdentifier}`);
return;
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError("invitation.sign.not_found", `Invitation not found: ${invitationIdentifier}`);
}
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
// Sign the invitation
await invitation.sign();
deps.verboseLogger(`Invitation signed: ${formatObject(invitation.data)}`);
console.log(`Invitation signed: ${invitationIdentifier}`);
break;
deps.io.verbose(`Invitation signed: ${formatObject(invitation.data)}`);
deps.io.out(`Invitation signed: ${invitationIdentifier}`);
return { invitationIdentifier };
}
case "broadcast": {
const invitationIdentifier = args[1];
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
// Check if the invitation identifier is provided
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
if (!invitationIdentifier) {
deps.verboseLogger("No invitation identifier provided");
printInvitationHelp();
return;
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.broadcast.identifier_missing", "No invitation identifier provided");
}
// Find the invitation by identifier
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
const invitation = deps.app.invitations.find(
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
);
if (!invitation) {
console.error(`Invitation not found: ${invitationIdentifier}`);
return;
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError("invitation.broadcast.not_found", `Invitation not found: ${invitationIdentifier}`);
}
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
// Broadcast the invitation
const txHash = await invitation.broadcast();
deps.verboseLogger(`Invitation broadcasted: ${formatObject(invitation.data)}`);
console.log(`Transaction broadcast: ${bold(txHash)}`);
break;
deps.io.verbose(`Invitation broadcasted: ${formatObject(invitation.data)}`);
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
return { invitationIdentifier, txHash };
}
case "requirements": {
const invitationIdentifier = args[1];
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
// Check if the invitation identifier is provided
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
if (!invitationIdentifier) {
deps.verboseLogger("No invitation identifier provided");
printInvitationHelp();
return;
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.requirements.identifier_missing", "No invitation identifier provided");
}
// Find the invitation by identifier
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
const invitation = deps.app.invitations.find(
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
);
if (!invitation) {
console.error(`Invitation not found: ${invitationIdentifier}`);
return;
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError("invitation.requirements.not_found", `Invitation not found: ${invitationIdentifier}`);
}
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
// List the requirements for the invitation
const requirements = await deps.app.engine.listRequirements(invitation.data);
deps.verboseLogger(`Requirements: ${formatObject(requirements)}`);
console.log(formatObject(requirements));
break;
deps.io.verbose(`Requirements: ${formatObject(requirements)}`);
deps.io.out(formatObject(requirements));
return { invitationIdentifier };
}
case "inspect": {
const invitationFilePath = args[1];
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
if (!invitationFilePath) {
deps.io.verbose("No invitation file provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.inspect.file_missing", "No invitation file provided");
}
const invitationFile = await readFileSync(invitationFilePath, "utf8");
deps.io.verbose(`Invitation file: ${invitationFile}`);
const invitation = JSON.parse(invitationFile);
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
const invitationInstance = await deps.app.createInvitation(invitation);
deps.io.verbose(`Invitation created: ${formatObject(invitationInstance.data)}`);
const template = await deps.app.engine.getTemplate(invitationInstance.data.templateIdentifier);
const action = template?.actions[invitationInstance.data.actionIdentifier];
deps.io.verbose(`Action: ${formatObject(action)}`);
if (!action) {
deps.io.err(`Action not found: ${invitationInstance.data.actionIdentifier}`);
throw new CommandError("invitation.inspect.action_not_found", `Action not found: ${invitationInstance.data.actionIdentifier}`);
}
const status = invitationInstance.status;
deps.io.verbose(`Status: ${status}`);
const entities = Array.from(new Set(invitationInstance.data.commits.map((commit) => commit.entityIdentifier)));
deps.io.verbose(`Entities: ${formatObject(entities)}`);
const entitiesWithRoles = entities.map((entity) => {
return {
entityIdentifier: entity,
roles: invitationInstance.data.commits.filter((commit) => commit.entityIdentifier === entity).map((commit) => {
return [
...(commit.data.inputs?.map((input) => input.roleIdentifier) ?? []),
...(commit.data.outputs?.map((output) => output.roleIdentifier) ?? []),
...(commit.data.variables?.map((variable) => variable.roleIdentifier) ?? []),
];
}).flat().filter((role) => role !== undefined)
};
});
const inputs = invitationInstance.data.commits.flatMap((commit) => commit.data.inputs ?? []);
deps.io.verbose(`Inputs: ${formatObject(inputs)}`);
const outputs = invitationInstance.data.commits.flatMap((commit) => commit.data.outputs ?? []);
deps.io.verbose(`Outputs: ${formatObject(outputs)}`);
const variables = invitationInstance.data.commits.flatMap((commit) => commit.data.variables ?? []);
deps.io.verbose(`Variables: ${formatObject(variables)}`);
return {
templateName: template?.name ?? "Unknown",
actionIdentifier: invitationInstance.data.actionIdentifier,
status: status,
entities: entitiesWithRoles,
inputs: inputs,
outputs: outputs,
variables: variables,
};
}
case "import": {
// Check if the invitation file path is provided
const invitationFilePath = args[1];
deps.verboseLogger(`Invitation file path: ${invitationFilePath}`);
// Check if the invitation file path is provided
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
if (!invitationFilePath) {
deps.verboseLogger("No invitation file provided");
printInvitationHelp();
return;
deps.io.verbose("No invitation file provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.import.file_missing", "No invitation file provided");
}
// Read the invitation file
const invitationFile = await readFileSync(invitationFilePath, "utf8");
deps.verboseLogger(`Invitation file: ${invitationFile}`);
// Parse the invitation file
deps.io.verbose(`Invitation file: ${invitationFile}`);
const invitation = JSON.parse(invitationFile);
deps.verboseLogger(`Invitation: ${formatObject(invitation)}`);
// Create the invitation (internal to XO Engine)
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
const xoInvitation = await deps.app.engine.createInvitation(invitation);
// Create the invitation instance
const invitationInstance = await deps.app.createInvitation(xoInvitation);
deps.verboseLogger(`Invitation created: ${formatObject(invitationInstance.data)}`);
break;
deps.io.verbose(`Invitation created: ${formatObject(invitationInstance.data)}`);
return { invitationIdentifier: invitationInstance.data.invitationIdentifier };
}
case "list": {
// List all invitations
const invitations = await Promise.all(deps.app.invitations.map(async invitation => {
// Get the template for the invitation so we can display the name of it
const template = await deps.app.engine.getTemplate(invitation.data.templateIdentifier);
return {
invitationIdentifier: invitation.data.invitationIdentifier,
templateIdentifier: invitation.data.templateIdentifier,
actionIdentifier: invitation.data.actionIdentifier,
templateName: template?.name ?? "Unknown",
status: invitation.status,
roleIdentifier: 'TODO: Get role identifier',
};
}));
deps.verboseLogger(`Invitations: ${formatObject(invitations)}`);
// Format the invitations for display and print it
const formattedInvitations = invitations.map(invitation => `${bold(invitation.templateName)} ${dim(invitation.status)} ${dim(invitation.invitationIdentifier)} ${dim(invitation.actionIdentifier)} (${dim(invitation.roleIdentifier)})`);
console.log(formattedInvitations.join('\n'));
break;
const invitations = await Promise.all(
deps.app.invitations.map(async (invitation) => {
const template = await deps.app.engine.getTemplate(invitation.data.templateIdentifier);
return {
invitationIdentifier: invitation.data.invitationIdentifier,
templateIdentifier: invitation.data.templateIdentifier,
actionIdentifier: invitation.data.actionIdentifier,
templateName: template?.name ?? "Unknown",
status: invitation.status,
roleIdentifier: "TODO: Get role identifier",
};
}),
);
deps.io.verbose(`Invitations: ${formatObject(invitations)}`);
const formattedInvitations = invitations.map(
(invitation) =>
`${bold(invitation.templateName)} ${dim(invitation.status)} ${dim(invitation.invitationIdentifier)} ${dim(invitation.actionIdentifier)} (${dim(invitation.roleIdentifier)})`,
);
deps.io.out(formattedInvitations.join("\n"));
return { count: invitations.length };
}
default:
deps.verboseLogger(`Unknown invitation sub-command: ${subCommand}`);
printInvitationHelp();
return;
deps.io.verbose(`Unknown invitation sub-command: ${subCommand}`);
printInvitationHelp(deps.io);
throw new CommandError("invitation.subcommand.unknown", `Unknown invitation sub-command: ${subCommand}`);
}
};