#!/usr/bin/env node /** * CLI entry point. * * TODO: Decide the best way to handle CLI arguments. We have the option of: * - Handling it in the `bin` folder * - Switch / if statements in here * - Dedicated command parser * - Separate files? * * What kind of commands do we want to support? * Worth noting that we shouldn't need to list invitations? Maybe we will though? If we do, then we will need to reuse the storage + xo-invitations.db file. I think this is fine to do though? * Nah, lets use the storage + xo-invitations.db file. Will allow us to persist invitations. * How do we want to import invitations though? Should we just take in the ID still? Probably makes more sense to allow for reading from a file though... * But thats an entirely different flow to what we have already. And how would we handle writing the invitation? Do we just overwrite the file? Probably... Just take in an -o option; default to overwrite? * * Commands: * xo-cli mnemonic create [mnemonic seed] * xo-cli mnemonic list * * xo-cli template import * xo-cli template list * xo-cli template set-default * * xo-cli invitation list * xo-cli invitation create [-o Output file, var-${action-variable-name}=${value}, role=${value}] * xo-cli invitation import * xo-cli invitation sign * xo-cli invitation broadcast * * xo-cli resource list * * universal Args: * -h --help * -m --mnemonic-file */ import { existsSync, readFileSync, writeFileSync } from "fs"; import { join } from "path"; import { AppService } from "../services/app.js"; import { convertArgsToObject } from "./arguments.js"; import { bold, dim, formatObject } from "./cli-utils.js"; import { listGlobalMnemonicFiles, loadMnemonic } from "./mnemonic.js"; import { getDataDir, getMnemonicsDir, getWalletConfigPath } from "../utils/paths.js"; import { type CommandDependencies, type CommandIO, type CommandPaths, CommandError, handleMnemonicCommand, handleTemplateCommand, handleInvitationCommand, handleReceiveCommand, handleResourceCommand, } from "./commands/index.js"; import { handleCompletionsCommand } from "./autocomplete/completions.js"; const createCommandIO = (verbose: boolean): CommandIO => ({ out: (message: string) => { console.log(message); }, err: (message: string) => { console.error(message); }, verbose: (message: string) => { if (verbose) console.log(message); }, }); /** * Main entry point. * We will: * - Initialize the app service? * - Extract the command being called * - Extract CLI Args (Depends on the command being called. Eww. But we can probably use Zod to validate the args in a decent way?) * - Execute the command * - Export if configured? * - Exit with the appropriate code */ async function main(): Promise { // Initialize the app service // NOTE: We are going to assume that they are using a mnemonic file for now const { args, options } = convertArgsToObject(process.argv.slice(2)); // Create a verbose logger if the user set the verbose flag const io = createCommandIO(options["verbose"] === "true"); // Log the parsed app args io.verbose(`Parsed args: ${formatObject(args)}`); io.verbose(`Parsed options: ${formatObject(options)}`); // Handle the command const command = args[0]; io.verbose(`Command: ${command}`); if (!command) { // TODO: Print help, probably... io.err("No command provided"); process.exit(1); } // Positional args after the command name (sub-command, files, etc.) const subArgs = args.slice(1); // Build paths object from global path functions const paths: CommandPaths = { mnemonicsDir: getMnemonicsDir(), dataDir: getDataDir(), walletConfigPath: getWalletConfigPath(), workingDir: process.cwd(), }; // Early handling for completions command if (command === "completions") { handleCompletionsCommand(subArgs, options); process.exit(0); } if (command === "mnemonic") { try { await handleMnemonicCommand({ io, paths }, subArgs, options); process.exit(0); } catch (error) { if (error instanceof CommandError) { process.exit(error.code); } throw error; } } // Resolve mnemonic file: explicit flag > persisted config > error. let mnemonicFile = options["mnemonicFile"]; if (!mnemonicFile && existsSync(paths.walletConfigPath)) { mnemonicFile = readFileSync(paths.walletConfigPath, "utf8").trim(); io.verbose(`Using persisted wallet: ${mnemonicFile}`); } if (!mnemonicFile) { io.err("No mnemonic file provided"); io.out(`You can create a mnemonic file with the following command: xo-cli mnemonic create or use one of the following files: \n${listGlobalMnemonicFiles().join("\n")}`); io.out(`\nTip: pass -m once and it will be remembered in ${paths.walletConfigPath}`); process.exit(1); } // Persist the choice so subsequent commands can omit -m. writeFileSync(paths.walletConfigPath, mnemonicFile); const mnemonic = loadMnemonic(paths.mnemonicsDir, mnemonicFile); io.verbose(`Loaded mnemonic from file: ${mnemonicFile} ${mnemonic}`); // Create an App instance io.verbose("Creating app instance..."); const app = await AppService.create(mnemonic, { syncServerUrl: options["syncServerUrl"] ?? "http://localhost:3000", engineConfig: { databasePath: options["databasePath"] ?? paths.dataDir, databaseFilename: options["databaseFilename"] ?? "xo-wallet.db", }, invitationStoragePath: options["invitationStoragePath"] ?? join(paths.dataDir, "xo-invitations.db"), }); io.verbose("App instance created"); // Start the app // TODO: Rethink this. Do we really want to start the app here? It just slows it down if we dont actually have to have it started for the command io.verbose("Starting app..."); await app.start(); io.verbose("App started"); const commandDependencies: CommandDependencies = { io, paths, app, }; // Handle the command try { let result: unknown; switch (command) { case "template": result = await handleTemplateCommand(commandDependencies, subArgs, options); break; case "invitation": result = await handleInvitationCommand(commandDependencies, subArgs, options); break; case "receive": result = await handleReceiveCommand(commandDependencies, subArgs, options); break; case "resource": result = await handleResourceCommand(commandDependencies, subArgs, options); break; case "help": result = await handleHelpCommand(commandDependencies, subArgs, options); break; default: io.err(`Unknown command: ${command}`); throw new CommandError("cli.command.unknown", `Unknown command: ${command}`); } // console.log(result); // objectPrint(result); process.exit(0); } catch (error) { if (error instanceof CommandError) { io.err(error.message); process.exit(error.code); } throw error; } } const handleHelpCommand = async ( deps: CommandDependencies, _args: string[], _options: Record, ): Promise> => { deps.io.out( `${bold("XO-CLI Help:")} ${bold("Usage:")} xo-cli [options] Commands: mnemonic ${dim("Manage mnemonic files")} template ${dim("Manage templates")} invitation ${dim("Manage invitations")} receive ${dim("Generate a single-use receiving address")} resource ${dim("Manage resources")} completions ${dim("Generate shell completion scripts (bash, zsh, fish)")} Options: -h, --help ${dim("Show this help message")} -m, --mnemonic-file ${dim("Use a specific mnemonic file")} -v, --verbose ${dim("Show verbose output")}` ); return {}; }; main().catch((error) => { console.error(error); process.exit(1); });