Files
xo-cli/src/cli/index.ts

244 lines
7.9 KiB
JavaScript

#!/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 <template-file>
* xo-cli template list
* xo-cli template set-default <template-file> <output-identifier> <role-identifier>
*
* xo-cli invitation list
* xo-cli invitation create <template-file> <action-id> [-o Output file, var-${action-variable-name}=${value}, role=${value}]
* xo-cli invitation import <invitation-file>
* xo-cli invitation sign <invitation-file>
* xo-cli invitation broadcast <invitation-file>
*
* xo-cli resource list
*
* universal Args:
* -h --help
* -m --mnemonic-file <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<void> {
// 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 <mnemonic-seed> or use one of the following files: \n${listGlobalMnemonicFiles().join("\n")}`);
io.out(`\nTip: pass -m <file> 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<string, string>,
): Promise<Record<string, never>> => {
deps.io.out(
`${bold("XO-CLI Help:")}
${bold("Usage:")} xo-cli <command> [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 <mnemonic-file> ${dim("Use a specific mnemonic file")}
-v, --verbose ${dim("Show verbose output")}`
);
return {};
};
main().catch((error) => {
console.error(error);
process.exit(1);
});