Huge commit. Multiple fixes. Refactored commands. Invitations, resources, template inspection, mnemonic stuff, cli utils, pretty printing, remove unreserve on start, fix connectino requirement for invitations, format cashAddress to lockingBytecode on send, lots and lots of other stuff.
This commit is contained in:
562
src/cli/commands/invitation.ts
Normal file
562
src/cli/commands/invitation.ts
Normal file
@@ -0,0 +1,562 @@
|
||||
import { existsSync, 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 { Invitation } from "../../services/invitation.js";
|
||||
import {
|
||||
resolveProvidedLockingBytecodeHex,
|
||||
mapUnspentOutputsToSelectable,
|
||||
autoSelectGreedyUtxos,
|
||||
} from "../../utils/invitation-flow.js";
|
||||
import { encodeExtendedJson } from "../../utils/ext-json.js";
|
||||
|
||||
const DEFAULT_FEE = 500n;
|
||||
const DUST_THRESHOLD = 546n;
|
||||
|
||||
/**
|
||||
* Result of parsing CLI options into inputs and outputs for an append call.
|
||||
* A `null` return signals a fatal error that was already logged to stderr.
|
||||
*/
|
||||
interface BuildAppendResult {
|
||||
inputs: { outpointTransactionHash: Uint8Array; outpointIndex: number }[];
|
||||
outputs: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts invitation variables from CLI option flags.
|
||||
* Keys starting with "var" are treated as variables — the prefix is stripped
|
||||
* and the next character is lowercased to reconstruct the camelCase identifier.
|
||||
*
|
||||
* When a `-role` flag is present the role identifier is attached to every
|
||||
* variable so the engine stores them under the correct role-level requirements.
|
||||
*
|
||||
* @example `-var-requested-satoshis 1000 -role sender` → `{ variableIdentifier: "requestedSatoshis", value: "1000", roleIdentifier: "sender" }`
|
||||
*/
|
||||
function parseVariablesFromOptions(
|
||||
options: Record<string, string>,
|
||||
): { variableIdentifier: string; value: string; roleIdentifier?: string }[] {
|
||||
const roleIdentifier = options["role"];
|
||||
|
||||
return Object.entries(options)
|
||||
.filter(([key]) => key.startsWith("var"))
|
||||
.map(([key, value]) => ({
|
||||
variableIdentifier: key.substring(3, 4).toLowerCase() + key.substring(4),
|
||||
value,
|
||||
...(roleIdentifier ? { roleIdentifier } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses CLI options into the inputs and outputs needed for an invitation
|
||||
* append call. Shared by both `create` and `append` so the same flags
|
||||
* (`--add-input`, `--add-output`, `--auto-inputs`, `-role`) work in either.
|
||||
*
|
||||
* Variables should already be committed to the invitation before calling this
|
||||
* so that `getSatsOut()` can resolve variable-dependent output values for the
|
||||
* automatic change calculation.
|
||||
*
|
||||
* @param deps - Command dependencies (engine, logger, etc.)
|
||||
* @param invitation - The invitation instance (variables should already be committed).
|
||||
* @param options - Parsed CLI option flags.
|
||||
* @returns The structured params, or `null` when a fatal error was printed.
|
||||
*/
|
||||
async function buildAppendParams(
|
||||
deps: CommandDependencies,
|
||||
invitation: Invitation,
|
||||
options: Record<string, string>,
|
||||
): Promise<BuildAppendResult | null> {
|
||||
// --- Inputs ---
|
||||
// Accepts comma-separated <txhash>:<vout> pairs via --add-input,
|
||||
// OR automatic selection via --auto-inputs.
|
||||
let inputs: { outpointTransactionHash: Uint8Array; outpointIndex: number }[] = [];
|
||||
|
||||
if (options["autoInputs"] === "true") {
|
||||
// Auto-select UTXOs using the greedy algorithm from invitation-flow.
|
||||
const suitableResources = await invitation.findSuitableResources();
|
||||
const selectable = mapUnspentOutputsToSelectable(suitableResources);
|
||||
|
||||
const requiredWithFee = (await invitation.getSatsOut().catch(() => 0n)) + DEFAULT_FEE;
|
||||
autoSelectGreedyUtxos(selectable, requiredWithFee);
|
||||
|
||||
inputs = selectable
|
||||
.filter((u) => u.selected)
|
||||
.map((u) => ({
|
||||
outpointTransactionHash: hexToBin(u.outpointTransactionHash),
|
||||
outpointIndex: u.outpointIndex,
|
||||
}));
|
||||
|
||||
if (inputs.length === 0) {
|
||||
console.error("No suitable UTXOs found for auto-input selection.");
|
||||
return null;
|
||||
}
|
||||
deps.verboseLogger(`Auto-selected ${inputs.length} input(s)`);
|
||||
} else if (options["addInput"]) {
|
||||
inputs = options["addInput"].split(",").map((entry) => {
|
||||
const separatorIndex = entry.lastIndexOf(":");
|
||||
if (separatorIndex === -1) {
|
||||
throw new Error(`Invalid input format "${entry}". Expected <txhash>:<vout> (e.g. abc123:0)`);
|
||||
}
|
||||
const txHash = entry.substring(0, separatorIndex);
|
||||
const vout = parseInt(entry.substring(separatorIndex + 1), 10);
|
||||
if (!txHash || isNaN(vout)) {
|
||||
throw new Error(`Invalid input format "${entry}". Expected <txhash>:<vout> (e.g. abc123:0)`);
|
||||
}
|
||||
return {
|
||||
outpointTransactionHash: hexToBin(txHash),
|
||||
outpointIndex: vout,
|
||||
};
|
||||
});
|
||||
}
|
||||
deps.verboseLogger(`Inputs: ${formatObject(inputs.map(i => ({ txHash: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })))}`);
|
||||
|
||||
// --- Outputs ---
|
||||
// When --add-output is provided, use those identifiers explicitly.
|
||||
// Otherwise, auto-discover all required outputs from the template so the
|
||||
// user doesn't have to name them manually.
|
||||
const roleIdentifier = options["role"];
|
||||
let outputIdentifiers: string[] = [];
|
||||
|
||||
if (options["addOutput"]) {
|
||||
outputIdentifiers = options["addOutput"].split(",");
|
||||
} else {
|
||||
// Pull every output identifier the template requires (top-level + role-specific).
|
||||
const requirements = await invitation.getRequirements();
|
||||
const discovered = new Set<string>();
|
||||
for (const id of requirements.outputs ?? []) discovered.add(id);
|
||||
if (requirements.roles) {
|
||||
for (const role of Object.values(requirements.roles)) {
|
||||
for (const id of role.outputs ?? []) discovered.add(id);
|
||||
}
|
||||
}
|
||||
outputIdentifiers = [...discovered];
|
||||
if (outputIdentifiers.length > 0) {
|
||||
deps.verboseLogger(`Auto-discovered output(s) from template: ${outputIdentifiers.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Build a variable-values map from all committed variables so
|
||||
// resolveProvidedLockingBytecodeHex can resolve outputs whose locking
|
||||
// script depends on a variable (e.g. <recipientLockingscript>).
|
||||
const variableValuesByIdentifier: Record<string, string> = {};
|
||||
for (const commit of invitation.data.commits) {
|
||||
for (const v of commit.data?.variables ?? []) {
|
||||
if (v.variableIdentifier && typeof v.value === "string") {
|
||||
variableValuesByIdentifier[v.variableIdentifier] = v.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const template = await deps.app.engine.getTemplate(invitation.data.templateIdentifier);
|
||||
|
||||
const outputs: any[] = await Promise.all(
|
||||
outputIdentifiers.map(async (outputId) => {
|
||||
// Try variable-based resolution first (e.g. sendSatoshis → recipientLockingscript)
|
||||
const providedHex = template
|
||||
? resolveProvidedLockingBytecodeHex(template, outputId, variableValuesByIdentifier)
|
||||
: undefined;
|
||||
|
||||
const lockingBytecodeHex = providedHex
|
||||
?? await invitation.generateLockingBytecode(outputId, roleIdentifier);
|
||||
|
||||
deps.verboseLogger(`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))}`);
|
||||
|
||||
// --- Auto change output ---
|
||||
// When inputs are provided, look up each UTXO's value, compute the
|
||||
// required sats, and return the excess minus fees back to the user.
|
||||
if (inputs.length > 0) {
|
||||
const allUtxos = await deps.app.engine.listUnspentOutputsData();
|
||||
const utxoMap = new Map(allUtxos.map(u => [`${u.outpointTransactionHash}:${u.outpointIndex}`, u]));
|
||||
|
||||
let totalInputSats = 0n;
|
||||
for (const input of inputs) {
|
||||
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.`);
|
||||
return null;
|
||||
}
|
||||
totalInputSats += BigInt(utxo.valueSatoshis);
|
||||
}
|
||||
deps.verboseLogger(`Total input value: ${totalInputSats} satoshis`);
|
||||
|
||||
const requiredSats = await invitation.getSatsOut();
|
||||
deps.verboseLogger(`Required output value: ${requiredSats} satoshis`);
|
||||
|
||||
const changeAmount = totalInputSats - requiredSats - DEFAULT_FEE;
|
||||
deps.verboseLogger(`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).`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (changeAmount >= DUST_THRESHOLD) {
|
||||
outputs.push({ valueSatoshis: changeAmount });
|
||||
console.log(`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.`);
|
||||
}
|
||||
}
|
||||
|
||||
return { inputs, outputs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the help message for the invitation command
|
||||
*/
|
||||
export const printInvitationHelp = () => {
|
||||
console.log(
|
||||
`
|
||||
${bold("Usage:")} xo-cli invitation <sub-command>
|
||||
|
||||
${bold("Sub-commands:")}
|
||||
- create <template-file> <action-id> ${dim("Create a new invitation")}
|
||||
- append <invitation-id> ${dim("Add variables/outputs to an invitation")}
|
||||
- sign <invitation-id> ${dim("Sign an invitation")}
|
||||
- broadcast <invitation-id> ${dim("Broadcast an invitation")}
|
||||
- requirements <invitation-id> ${dim("Show requirements for an invitation")}
|
||||
- import <invitation-file> ${dim("Import an invitation from a file")}
|
||||
- list ${dim("List all invitations")}
|
||||
|
||||
${bold("Create / Append options:")}
|
||||
-var-<name> <value> ${dim("Set a variable (e.g. -var-requested-satoshis 1000)")}
|
||||
--add-input <txhash:vout> ${dim("Add UTXO input(s), comma-separated (e.g. abc123:0,def456:1)")}
|
||||
--add-output <id> ${dim("Override output(s) — omit to auto-discover from template")}
|
||||
--auto-inputs ${dim("Automatically select UTXOs as inputs")}
|
||||
-role <role> ${dim("Role for output bytecode generation (fallback)")}
|
||||
--sign ${dim("Auto-sign after all requirements are satisfied")}
|
||||
--broadcast ${dim("Auto-broadcast after signing (implies --sign)")}
|
||||
|
||||
${dim("When inputs are provided, a change output is automatically added if the")}
|
||||
${dim("input total exceeds the required amount + fee.")}
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the invitation command.
|
||||
* @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> => {
|
||||
const subCommand = args[0];
|
||||
deps.verboseLogger(`Invitation sub-command: ${subCommand}`);
|
||||
|
||||
if (!subCommand) {
|
||||
deps.verboseLogger("No sub-command provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (subCommand) {
|
||||
case "create": {
|
||||
const templateFile = args[1];
|
||||
const actionIdentifier = args[2];
|
||||
deps.verboseLogger(`Template file: ${templateFile}, action identifier: ${actionIdentifier}`);
|
||||
|
||||
if (!templateFile || !actionIdentifier) {
|
||||
deps.verboseLogger("No template file or action identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 rawInvitation = await deps.app.engine.createInvitation({
|
||||
templateIdentifier: templateIdentifier,
|
||||
actionIdentifier: actionIdentifier,
|
||||
});
|
||||
deps.verboseLogger(`XOInvitation created: ${formatObject(rawInvitation)}`);
|
||||
|
||||
const invitationInstance = await deps.app.createInvitation(rawInvitation);
|
||||
deps.verboseLogger(`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)}`);
|
||||
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;
|
||||
|
||||
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}`);
|
||||
writeFileSync(invitationFilePath, encodeExtendedJson(invitationInstance.data, 2));
|
||||
console.log(`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`);
|
||||
|
||||
// Check remaining requirements
|
||||
const missingRequirements = await invitationInstance.getMissingRequirements();
|
||||
const hasMissing =
|
||||
(missingRequirements.variables?.length ?? 0) > 0 ||
|
||||
(missingRequirements.inputs?.length ?? 0) > 0 ||
|
||||
(missingRequirements.outputs?.length ?? 0) > 0 ||
|
||||
(missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0);
|
||||
|
||||
if (hasMissing) {
|
||||
console.log(`\n${bold("Remaining requirements:")}`);
|
||||
console.log(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}`);
|
||||
}
|
||||
|
||||
if (shouldBroadcast) {
|
||||
const txHash = await invitationInstance.broadcast();
|
||||
console.log(`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}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "append": {
|
||||
const invitationIdentifier = args[1];
|
||||
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
if (!invitationIdentifier) {
|
||||
deps.verboseLogger("No invitation identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the invitation by identifier
|
||||
const invitation = deps.app.invitations.find(inv => inv.data.invitationIdentifier === invitationIdentifier);
|
||||
if (!invitation) {
|
||||
console.error(`Invitation not found: ${invitationIdentifier}`);
|
||||
return;
|
||||
}
|
||||
deps.verboseLogger(`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)}`);
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (inputs.length > 0 || outputs.length > 0) {
|
||||
await invitation.append({ inputs, outputs });
|
||||
}
|
||||
deps.verboseLogger(`Invitation appended: ${formatObject(invitation.data)}`);
|
||||
console.log(`Invitation appended: ${invitationIdentifier}`);
|
||||
|
||||
// Save the updated invitation to a file
|
||||
const invitationFilePath = `${process.cwd()}/inv-${invitation.data.invitationIdentifier}.json`;
|
||||
writeFileSync(invitationFilePath, encodeExtendedJson(invitation.data, 2));
|
||||
console.log(`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`);
|
||||
|
||||
// Check remaining requirements
|
||||
const missingRequirements = await invitation.getMissingRequirements();
|
||||
const hasMissing =
|
||||
(missingRequirements.variables?.length ?? 0) > 0 ||
|
||||
(missingRequirements.inputs?.length ?? 0) > 0 ||
|
||||
(missingRequirements.outputs?.length ?? 0) > 0 ||
|
||||
(missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0);
|
||||
|
||||
if (hasMissing) {
|
||||
console.log(`\n${bold("Remaining requirements:")}`);
|
||||
console.log(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}`);
|
||||
}
|
||||
|
||||
if (shouldBroadcast) {
|
||||
const txHash = await invitation.broadcast();
|
||||
console.log(`Transaction broadcast: ${bold(txHash)}`);
|
||||
} else if (!shouldSign) {
|
||||
console.log(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationIdentifier}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "sign": {
|
||||
const invitationIdentifier = args[1];
|
||||
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// Check if the invitation identifier is provided
|
||||
if (!invitationIdentifier) {
|
||||
deps.verboseLogger("No invitation identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the invitation by identifier
|
||||
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
|
||||
if (!invitation) {
|
||||
console.error(`Invitation not found: ${invitationIdentifier}`);
|
||||
return;
|
||||
}
|
||||
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// Sign the invitation
|
||||
await invitation.sign();
|
||||
deps.verboseLogger(`Invitation signed: ${formatObject(invitation.data)}`);
|
||||
console.log(`Invitation signed: ${invitationIdentifier}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case "broadcast": {
|
||||
const invitationIdentifier = args[1];
|
||||
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// Check if the invitation identifier is provided
|
||||
if (!invitationIdentifier) {
|
||||
deps.verboseLogger("No invitation identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the invitation by identifier
|
||||
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
|
||||
if (!invitation) {
|
||||
console.error(`Invitation not found: ${invitationIdentifier}`);
|
||||
return;
|
||||
}
|
||||
deps.verboseLogger(`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;
|
||||
}
|
||||
|
||||
case "requirements": {
|
||||
const invitationIdentifier = args[1];
|
||||
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// Check if the invitation identifier is provided
|
||||
if (!invitationIdentifier) {
|
||||
deps.verboseLogger("No invitation identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the invitation by identifier
|
||||
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
|
||||
if (!invitation) {
|
||||
console.error(`Invitation not found: ${invitationIdentifier}`);
|
||||
return;
|
||||
}
|
||||
deps.verboseLogger(`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;
|
||||
}
|
||||
|
||||
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
|
||||
if (!invitationFilePath) {
|
||||
deps.verboseLogger("No invitation file provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the invitation file
|
||||
const invitationFile = await readFileSync(invitationFilePath, "utf8");
|
||||
deps.verboseLogger(`Invitation file: ${invitationFile}`);
|
||||
|
||||
// Parse the invitation file
|
||||
const invitation = JSON.parse(invitationFile);
|
||||
deps.verboseLogger(`Invitation: ${formatObject(invitation)}`);
|
||||
|
||||
// Create the invitation (internal to XO Engine)
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
default:
|
||||
deps.verboseLogger(`Unknown invitation sub-command: ${subCommand}`);
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user