diff --git a/src/cli.ts b/src/cli.ts index 6eb5d8e6..846e1d62 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,10 +1,11 @@ #! /usr/bin/env node /* NOTE: calling this file entropy.ts helps commander parse process.argv */ -import { Command, Option } from 'commander' +import { Command, /* Option */ } from 'commander' import { EntropyTuiOptions } from './types' import { accountOption, endpointOption, loadEntropy } from './common/utils-cli' +import * as config from './config' import launchTui from './tui' import { entropyAccountCommand } from './account/command' @@ -21,15 +22,15 @@ program .description('CLI interface for interacting with entropy.xyz. Running without commands starts an interactive ui') .addOption(accountOption()) .addOption(endpointOption()) - // NOTE: I think this is currently unused - .addOption( - new Option( - '-d, --dev', - 'Runs entropy in a developer mode uses the dev endpoint as the main endpoint and allows for faucet option to be available in the main menu' - ) - .env('DEV_MODE') - .hideHelp() - ) + // NOTE: currently unused + // .addOption( + // new Option( + // '-d, --dev', + // 'Runs entropy in a developer mode uses the dev endpoint as the main endpoint and allows for faucet option to be available in the main menu' + // ) + // .env('DEV_MODE') + // .hideHelp() + // ) .addCommand(entropyBalanceCommand()) .addCommand(entropyAccountCommand()) .addCommand(entropyTransferCommand()) @@ -43,5 +44,9 @@ program // NOTE: on initial startup you have no account launchTui(entropy, opts) }) + .hook('preAction', async () => { + // set up config file, run migrations + return config.init() + }) -program.parseAsync().then(() => {}) +program.parseAsync() diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts index 84e7838d..3aec350b 100644 --- a/src/common/utils-cli.ts +++ b/src/common/utils-cli.ts @@ -9,6 +9,15 @@ export function cliWrite (result) { process.stdout.write(prettyResult) } +function getConfigOrNull () { + try { + return config.getSync() + } catch (err) { + if (config.isDangerousReadError(err)) throw err + return null + } +} + export function endpointOption () { return new Option( '-e, --endpoint ', @@ -23,8 +32,8 @@ export function endpointOption () { if (aliasOrEndpoint.match(/^wss?:\/\//)) return aliasOrEndpoint /* look up endpoint-alias */ - const storedConfig = config.getSync() - const endpoint = storedConfig.endpoints[aliasOrEndpoint] + const storedConfig = getConfigOrNull() + const endpoint = storedConfig?.endpoints?.[aliasOrEndpoint] if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) return endpoint @@ -41,7 +50,7 @@ export function passwordOption (description?: string) { } export function accountOption () { - const storedConfig = config.getSync() + const storedConfig = getConfigOrNull() return new Option( '-a, --account ', @@ -52,24 +61,25 @@ export function accountOption () { ) .env('ENTROPY_ACCOUNT') .argParser(async (account) => { - if (account === storedConfig.selectedAccount) return account - // Updated selected account in config with new address from this option - await config.set({ - ...storedConfig, - selectedAccount: account - }) + if (storedConfig && storedConfig.selectedAccount !== account) { + // Updated selected account in config with new address from this option + await config.set({ + ...storedConfig, + selectedAccount: account + }) + } return account }) - .default(storedConfig.selectedAccount) + .default(storedConfig?.selectedAccount) // TODO: display the *name* not address // TODO: standardise whether selectedAccount is name or address. } export async function loadEntropy (addressOrName: string, endpoint: string, password?: string): Promise { - const storedConfig = config.getSync() - const selectedAccount = findAccountByAddressOrName(storedConfig.accounts, addressOrName) - if (!selectedAccount) throw new Error(`AddressError: No account with name or address "${addressOrName}"`) + const accounts = getConfigOrNull()?.accounts || [] + const selectedAccount = findAccountByAddressOrName(accounts, addressOrName) + if (!selectedAccount) throw new Error(`No account with name or address: "${addressOrName}"`) // check if data is encrypted + we have a password if (typeof selectedAccount.data === 'string' && !password) { diff --git a/src/config/index.ts b/src/config/index.ts index 73c91e27..049b6775 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,5 +1,5 @@ import { readFile, writeFile, rm } from 'node:fs/promises' -import { readFileSync, writeFileSync } from 'node:fs' +import { readFileSync } from 'node:fs' import { mkdirp } from 'mkdirp' import { join, dirname } from 'path' import envPaths from 'env-paths' @@ -35,9 +35,10 @@ function hasRunMigration (config: any, version: number) { export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG_PATH) { const currentConfig = await get(configPath) - .catch(async (err) => { - if (err.code !== 'ENOENT') throw err + .catch(async (err ) => { + if (isDangerousReadError(err)) throw err + // If there is no current config, try loading the old one const oldConfig = await get(oldConfigPath).catch(noop) // drop errors if (oldConfig) { // move the config @@ -58,33 +59,30 @@ export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG export async function get (configPath = CONFIG_PATH) { return readFile(configPath, 'utf-8') .then(deserialize) - .catch(makeGetErrorHandler(configPath)) } -export function getSync (configPath = CONFIG_PATH): EntropyConfig { - try { - const configBuffer = readFileSync(configPath, 'utf8') - return deserialize(configBuffer) - } catch (err) { - return makeGetErrorHandler(configPath)(err) - } +export function getSync (configPath = CONFIG_PATH) { + const configStr = readFileSync(configPath, 'utf8') + return deserialize(configStr) } export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { + assertConfigPath(configPath) + await mkdirp(dirname(configPath)) await writeFile(configPath, serialize(config)) } /* util */ function noop () {} - -function makeGetErrorHandler (configPath) { - return function getErrorHandler (err) { - if (err.code !== 'ENOENT') throw err - - const newConfig = migrateData(allMigrations, {}) - mkdirp.sync(dirname(configPath)) - writeFileSync(configPath, serialize(newConfig)) - return newConfig +function assertConfigPath (configPath) { + if (!configPath.endsWith('.json')) { + throw Error(`configPath must be of form *.json, got ${configPath}`) } } +export function isDangerousReadError (err) { + // file not found: + if (err.code === 'ENOENT') return false + + return true +} diff --git a/tests/config.test.ts b/tests/config.test.ts index 8f28396b..5e2d0215 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -81,10 +81,11 @@ test('config - get', async t => { const result = await get(configPath) t.deepEqual(result, config, 'get works') + const MSG = 'path that does not exist fails' await get('/tmp/junk') - .then(() => t.fail('bad path should fail')) + .then(() => t.fail(MSG)) .catch(err => { - t.match(err.message, /no such file/, 'bad path should fail') + t.match(err.message, /ENOENT/, MSG) }) }) @@ -95,6 +96,7 @@ test('config - set', async t => { dog: true, secretKey: makeKey() } + // @ts-expect-error : this is a breaking test await set(config, configPath) const actual = await get(configPath)