diff --git a/cli/.gitignore b/cli/.gitignore index affe73e..8468f41 100644 --- a/cli/.gitignore +++ b/cli/.gitignore @@ -39,4 +39,6 @@ coverage/ *.wasm # wallets -wallet*.yaml \ No newline at end of file +wallet*.yaml + +deweb_cli_config.json \ No newline at end of file diff --git a/cli/src/commands/config.ts b/cli/src/commands/config.ts new file mode 100644 index 0000000..8ed0db2 --- /dev/null +++ b/cli/src/commands/config.ts @@ -0,0 +1,46 @@ +import { PublicApiUrl } from '@massalabs/massa-web3' +import { OptionValues } from 'commander' +import { readFileSync } from 'fs' + +export const DEFAULT_CHUNK_SIZE = 64000 +const DEFAULT_NODE_URL = PublicApiUrl.Buildnet + +interface Config { + wallet_password: string + wallet_path: string + node_url: string + chunk_size: number + secret_key: string +} + +export function parseConfigFile(filePath: string): Config { + const fileContent = readFileSync(filePath, 'utf-8') + try { + return JSON.parse(fileContent) + } catch (error) { + throw new Error(`Failed to parse file: ${error}`) + } +} + +export function mergeConfigAndOptions( + commandOptions: OptionValues, + configOptions: Config +): OptionValues { + if (!configOptions) return commandOptions + + return { + wallet: commandOptions.wallet || configOptions.wallet_path, + password: commandOptions.password || configOptions.wallet_password, + node_url: + commandOptions.node_url || configOptions.node_url || DEFAULT_NODE_URL, + chunk_size: configOptions.chunk_size || DEFAULT_CHUNK_SIZE, + secret_key: configOptions.secret_key || '', + } +} + +export function setDefaultValues(commandOptions: OptionValues): OptionValues { + return { + node_url: commandOptions.node_url || DEFAULT_NODE_URL, + chunk_size: commandOptions.chunk_size || DEFAULT_CHUNK_SIZE, + } +} diff --git a/cli/src/commands/delete.ts b/cli/src/commands/delete.ts index 920e074..29f468c 100644 --- a/cli/src/commands/delete.ts +++ b/cli/src/commands/delete.ts @@ -11,13 +11,7 @@ export const deleteCommand = new Command('delete') .description('Delete the given website from Massa blockchain') .argument('
', 'Address of the website to delete') .action(async (address, _, command) => { - const globalOptions = command.parent?.opts() - - if (!globalOptions) { - throw new Error( - 'Global options are not defined. This should never happen.' - ) - } + const globalOptions = command.optsWithGlobals() const provider = await makeProviderFromNodeURLAndSecret(globalOptions) diff --git a/cli/src/commands/list.ts b/cli/src/commands/list.ts index f8073f9..53f03c0 100644 --- a/cli/src/commands/list.ts +++ b/cli/src/commands/list.ts @@ -8,13 +8,7 @@ export const listFilesCommand = new Command('list') .description('Lists files from the given website on Massa blockchain') .option('-a, --address
', 'Address of the website to list') .action(async (options, command) => { - const globalOptions = command.parent?.opts() - - if (!globalOptions) { - throw new Error( - 'Global options are not defined. This should never happen.' - ) - } + const globalOptions = command.optsWithGlobals() const provider = await makeProviderFromNodeURLAndSecret(globalOptions) diff --git a/cli/src/commands/showFile.ts b/cli/src/commands/showFile.ts index 57bd2b4..d927c77 100644 --- a/cli/src/commands/showFile.ts +++ b/cli/src/commands/showFile.ts @@ -8,13 +8,7 @@ export const showFileCommand = new Command('show') .argument('', 'Path of the file to show') .option('-a, --address
', 'Address of the website to edit') .action(async (filePath, options, command) => { - const globalOptions = command.parent?.opts() - - if (!globalOptions) { - throw new Error( - 'Global options are not defined. This should never happen.' - ) - } + const globalOptions = command.optsWithGlobals() const provider = await makeProviderFromNodeURLAndSecret(globalOptions) diff --git a/cli/src/commands/upload.ts b/cli/src/commands/upload.ts index 785d794..346299b 100644 --- a/cli/src/commands/upload.ts +++ b/cli/src/commands/upload.ts @@ -15,31 +15,22 @@ import { confirmUploadTask, uploadBatchesTask } from '../tasks/upload' import { makeProviderFromNodeURLAndSecret, validateAddress } from './utils' -const DEFAULT_CHUNK_SIZE = 64000n - export const uploadCommand = new Command('upload') .alias('u') .description('Uploads the given website on Massa blockchain') .argument('', 'Path to the website directory to upload') .option('-a, --address
', 'Address of the website to edit') + .option('-s, --chunkSize ', 'Chunk size in bytes') .option('-y, --yes', 'Skip confirmation prompt', false) - .option( - '-c, --chunk-size ', - 'Chunk size in bytes', - DEFAULT_CHUNK_SIZE.toString() - ) .action(async (websiteDirPath, options, command) => { - const globalOptions = command.parent?.opts() - - if (!globalOptions) { - throw new Error( - 'Global options are not defined. This should never happen.' - ) - } + const globalOptions = command.optsWithGlobals() const provider = await makeProviderFromNodeURLAndSecret(globalOptions) - const chunkSize = parseInt(options.chunkSize) + // set chunksize from options or config + const chunkSize = + parseInt(options.chunkSize as string) || + (globalOptions.chunk_size as number) const ctx: UploadCtx = { provider: provider, diff --git a/cli/src/commands/utils.ts b/cli/src/commands/utils.ts index 0a21913..b8ce3f5 100644 --- a/cli/src/commands/utils.ts +++ b/cli/src/commands/utils.ts @@ -11,6 +11,41 @@ import { parse as yamlParse } from 'yaml' const KEY_ENV_NAME = 'SECRET_KEY' +/** + * Load the keypair using environment variables, secret_key, or wallet file + * @param globalOptions - the global options + * @returns the keypair + */ +async function loadKeyPair(globalOptions: OptionValues): Promise { + try { + const envSecretKey = process.env[KEY_ENV_NAME] + + if (envSecretKey) { + return await KeyPair.fromEnv(KEY_ENV_NAME) + } + + if (globalOptions.secret_key) { + return await KeyPair.fromPrivateKey(globalOptions.secret_key) + } + + if ( + globalOptions.wallet && + globalOptions.password && + globalOptions.wallet.endsWith('.yaml') + ) { + return await importFromYamlKeyStore( + globalOptions.wallet, + globalOptions.password + ) + } + + throw new Error('No valid method to load keypair.') + } catch (error) { + console.warn(`Failed to initialize keyPair: ${error}`) + throw error + } +} + /** * Make a provider from the node URL and secret key * @param globalOptions - the global options @@ -23,33 +58,14 @@ export async function makeProviderFromNodeURLAndSecret( throw new Error('node_url is not defined. Please use --node_url to set one') } - var keyPair: KeyPair - if ( - (globalOptions.wallet && !globalOptions.password) || - (!globalOptions.wallet && globalOptions.password) - ) { - throw new Error('Both wallet and password must be provided together.') - } - - if (globalOptions.wallet && globalOptions.password) { - if (!globalOptions.wallet.endsWith('.yaml')) { - throw new Error('Wallet file must be a YAML file') - } + try { + const keyPair = await loadKeyPair(globalOptions) - keyPair = await importFromYamlKeyStore( - globalOptions.wallet, - globalOptions.password - ) - } else { - keyPair = await KeyPair.fromEnv(KEY_ENV_NAME) + return Web3Provider.fromRPCUrl(globalOptions.node_url as string, keyPair) + } catch (error) { + console.error(`Failed to initialize provider: ${error}`) + throw new Error('Failed to initialize provider with any available method') } - - const provider = Web3Provider.fromRPCUrl( - globalOptions.node_url as string, - keyPair - ) - - return provider } /** diff --git a/cli/src/index.ts b/cli/src/index.ts index dcc115a..7c71514 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,14 +1,19 @@ import { Command } from '@commander-js/extra-typings' -import { PublicApiUrl } from '@massalabs/massa-web3' import { deleteCommand } from './commands/delete' import { listFilesCommand } from './commands/list' import { showFileCommand } from './commands/showFile' import { uploadCommand } from './commands/upload' +import { existsSync } from 'fs' +import { + mergeConfigAndOptions, + parseConfigFile, + setDefaultValues, +} from './commands/config' + const version = process.env.VERSION || 'dev' const defaultConfigPath = 'deweb_cli_config.json' -const defaultNodeUrl = PublicApiUrl.Buildnet const program = new Command() @@ -17,7 +22,7 @@ program .description('CLI app for deploying websites') .version(version) .option('-c, --config ', 'Path to the config file', defaultConfigPath) - .option('-n, --node_url ', 'Node URL', defaultNodeUrl) + .option('-n, --node_url ', 'Node URL') .option('-w, --wallet ', 'Path to the wallet file') .option('-p, --password ', 'Password for the wallet file') @@ -26,4 +31,28 @@ program.addCommand(deleteCommand) program.addCommand(listFilesCommand) program.addCommand(showFileCommand) -program.parse(process.argv) +interface OptionValues { + config: string + node_url: string + wallet: string + password: string +} + +const commandOptions: OptionValues = program.opts() as OptionValues + +if (existsSync(commandOptions.config)) { + const configOptions = parseConfigFile(commandOptions.config) + // commandOptions get priority over configOptions + const programOptions = mergeConfigAndOptions(commandOptions, configOptions) + for (const [key, value] of Object.entries(programOptions)) { + program.setOptionValue(key, value) + } +} else { + const defaultValues = setDefaultValues(commandOptions) + + for (const [key, value] of Object.entries(defaultValues)) { + program.setOptionValue(key, value) + } +} + +program.parse()