244 lines
7.9 KiB
JavaScript
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);
|
|
});
|