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:
164
src/cli/index.ts
164
src/cli/index.ts
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI entry point.
|
||||
*
|
||||
@@ -35,17 +36,19 @@
|
||||
*/
|
||||
|
||||
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 { listMnemonicFiles, loadMnemonic } from "./mnemonic.js";
|
||||
|
||||
/** File that remembers the last-used mnemonic so `-m` can be omitted. */
|
||||
const WALLET_CONFIG_FILE = ".xo-cli-wallet";
|
||||
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,
|
||||
@@ -53,15 +56,19 @@ import {
|
||||
handleResourceCommand,
|
||||
} from "./commands/index.js";
|
||||
|
||||
import { handleCompletionsCommand } from "./completions.js";
|
||||
import { handleCompletionsCommand } from "./autocomplete/completions.js";
|
||||
|
||||
const createConditionalLogger = (verbose: boolean) => {
|
||||
return (message: string) => {
|
||||
if (verbose) {
|
||||
console.log(message);
|
||||
}
|
||||
};
|
||||
};
|
||||
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.
|
||||
@@ -79,106 +86,138 @@ async function main(): Promise<void> {
|
||||
const { args, options } = convertArgsToObject(process.argv.slice(2));
|
||||
|
||||
// Create a verbose logger if the user set the verbose flag
|
||||
const verboseLogger = createConditionalLogger(options["verbose"] === "true");
|
||||
const io = createCommandIO(options["verbose"] === "true");
|
||||
|
||||
// Log the parsed app args
|
||||
verboseLogger(`Parsed args: ${formatObject(args)}`);
|
||||
verboseLogger(`Parsed options: ${formatObject(options)}`);
|
||||
io.verbose(`Parsed args: ${formatObject(args)}`);
|
||||
io.verbose(`Parsed options: ${formatObject(options)}`);
|
||||
|
||||
// Handle the command
|
||||
const command = args[0];
|
||||
verboseLogger(`Command: ${command}`);
|
||||
io.verbose(`Command: ${command}`);
|
||||
if (!command) {
|
||||
// TODO: Print help, probably...
|
||||
console.error("No command provided");
|
||||
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 if we are calling the mnemonic command
|
||||
// TODO: This is ugly. I would like to find a nicer way of doing this.
|
||||
if (command === "completions") {
|
||||
handleCompletionsCommand(subArgs);
|
||||
return;
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (command === "mnemonic") {
|
||||
await handleMnemonicCommand({ verboseLogger }, subArgs, options);
|
||||
return;
|
||||
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(WALLET_CONFIG_FILE)) {
|
||||
mnemonicFile = readFileSync(WALLET_CONFIG_FILE, "utf8").trim();
|
||||
verboseLogger(`Using persisted wallet: ${mnemonicFile}`);
|
||||
if (!mnemonicFile && existsSync(paths.walletConfigPath)) {
|
||||
mnemonicFile = readFileSync(paths.walletConfigPath, "utf8").trim();
|
||||
io.verbose(`Using persisted wallet: ${mnemonicFile}`);
|
||||
}
|
||||
if (!mnemonicFile) {
|
||||
console.error("No mnemonic file provided");
|
||||
console.log(`You can create a mnemonic file with the following command: xo-cli mnemonic create <mnemonic-seed> or use one of the following files: \n${listMnemonicFiles().join("\n")}`);
|
||||
console.log(`\nTip: pass -m <file> once and it will be remembered in ${WALLET_CONFIG_FILE}`);
|
||||
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(WALLET_CONFIG_FILE, mnemonicFile);
|
||||
writeFileSync(paths.walletConfigPath, mnemonicFile);
|
||||
|
||||
const mnemonic = await loadMnemonic(mnemonicFile);
|
||||
verboseLogger(`Loaded mnemonic from file: ${mnemonicFile} ${mnemonic}`);
|
||||
const mnemonic = loadMnemonic(paths.mnemonicsDir, mnemonicFile);
|
||||
io.verbose(`Loaded mnemonic from file: ${mnemonicFile} ${mnemonic}`);
|
||||
|
||||
// Create an App instance
|
||||
verboseLogger("Creating app instance...");
|
||||
io.verbose("Creating app instance...");
|
||||
const app = await AppService.create(mnemonic, {
|
||||
syncServerUrl: options["syncServerUrl"] ?? "http://localhost:3000",
|
||||
engineConfig: {
|
||||
databasePath: options["databasePath"] ?? "./",
|
||||
databaseFilename: options["databaseFilename"] ?? 'xo-wallet.db',
|
||||
databasePath: options["databasePath"] ?? paths.dataDir,
|
||||
databaseFilename: options["databaseFilename"] ?? "xo-wallet.db",
|
||||
},
|
||||
invitationStoragePath: options["invitationStoragePath"] ?? "./xo-invitations.db",
|
||||
invitationStoragePath:
|
||||
options["invitationStoragePath"] ?? join(paths.dataDir, "xo-invitations.db"),
|
||||
});
|
||||
verboseLogger("App instance created");
|
||||
io.verbose("App instance created");
|
||||
|
||||
// Start the app
|
||||
verboseLogger("Starting 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();
|
||||
verboseLogger("App started");
|
||||
io.verbose("App started");
|
||||
|
||||
const commandDependencies: CommandDependencies = {
|
||||
verboseLogger,
|
||||
io,
|
||||
paths,
|
||||
app,
|
||||
};
|
||||
|
||||
// Handle the command
|
||||
switch (command) {
|
||||
case "template":
|
||||
await handleTemplateCommand(commandDependencies, subArgs, options);
|
||||
break;
|
||||
case "invitation":
|
||||
await handleInvitationCommand(commandDependencies, subArgs, options);
|
||||
break;
|
||||
case "receive":
|
||||
await handleReceiveCommand(commandDependencies, subArgs, options);
|
||||
break;
|
||||
case "resource":
|
||||
await handleResourceCommand(commandDependencies, subArgs, options);
|
||||
break;
|
||||
case "help":
|
||||
await handleHelpCommand(commandDependencies, subArgs, options);
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
process.exit(1);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
|
||||
// Exit the process
|
||||
process.exit(0);
|
||||
// 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<void> => {
|
||||
// Im sorry about the formatting here. I'm not sure how to handle this better.
|
||||
console.log(
|
||||
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]
|
||||
@@ -196,6 +235,7 @@ Options:
|
||||
-m, --mnemonic-file <mnemonic-file> ${dim("Use a specific mnemonic file")}
|
||||
-v, --verbose ${dim("Show verbose output")}`
|
||||
);
|
||||
return {};
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
Reference in New Issue
Block a user