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:
2026-04-20 10:30:38 +00:00
parent df4f438f6d
commit ff2fe126c6
44 changed files with 8220 additions and 1503 deletions

View File

@@ -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) => {