diff --git a/README.md b/README.md index 19d915f..ec17c65 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ENV3=THE FISH ```json { "scripts": { - "test": "env-cmd mocha -R spec" + "test": "env-cmd -- mocha -R spec" } } ``` @@ -38,7 +38,7 @@ ENV3=THE FISH **Terminal** ```sh -./node_modules/.bin/env-cmd node index.js +./node_modules/.bin/env-cmd -- node index.js ``` ### Using custom env file path @@ -48,13 +48,13 @@ To use a custom env filename or path, pass the `-f` flag. This is a major breaki **Terminal** ```sh -./node_modules/.bin/env-cmd -f ./custom/path/.env node index.js +./node_modules/.bin/env-cmd -f ./custom/path/.env -- node index.js ``` ## 📜 Help ```text -Usage: _ [options] [...args] +Usage: env-cmd [options] -- [...args] Options: -v, --version output the version number @@ -101,10 +101,10 @@ are found. **Terminal** ```sh -./node_modules/.bin/env-cmd -e production node index.js +./node_modules/.bin/env-cmd -e production -- node index.js # Or for multiple environments (where `production` vars override `test` vars, # but both are included) -./node_modules/.bin/env-cmd -e test,production node index.js +./node_modules/.bin/env-cmd -e test,production -- node index.js ``` ### `--no-override` option @@ -125,7 +125,7 @@ commands together that share the same environment variables. **Terminal** ```sh -./node_modules/.bin/env-cmd -f ./test/.env --use-shell "npm run lint && npm test" +./node_modules/.bin/env-cmd -f ./test/.env --use-shell -- "npm run lint && npm test" ``` ### Asynchronous env file support @@ -138,7 +138,7 @@ commands together that share the same environment variables. **Terminal** ```sh - ./node_modules/.bin/env-cmd -f ./async-file.js node index.js + ./node_modules/.bin/env-cmd -f ./async-file.js -- node index.js ``` ### `-x` expands vars in arguments @@ -152,14 +152,14 @@ to provide arguments to a command that are based on environment variable values ```sh # $VAR will be expanded into the env value it contains at runtime -./node_modules/.bin/env-cmd -x node index.js --arg=\$VAR +./node_modules/.bin/env-cmd -x -- node index.js --arg=\$VAR ``` or in `package.json` (use `\\` to insert a literal backslash) ```json { "script": { - "start": "env-cmd -x node index.js --arg=\\$VAR" + "start": "env-cmd -x -- node index.js --arg=\\$VAR" } } ``` @@ -252,11 +252,6 @@ usually just easier to have a file with all the vars in them, especially for dev [`cross-env`](https://github.com/kentcdodds/cross-env) - Cross platform setting of environment scripts -## 🎊 Special Thanks - -Special thanks to [`cross-env`](https://github.com/kentcdodds/cross-env) for inspiration (uses the -same `cross-spawn` lib underneath too). - ## 📋 Contributing Guide I welcome all pull requests. Please make sure you add appropriate test cases for any features diff --git a/dist/parse-args.js b/dist/parse-args.js index 4494073..873f783 100644 --- a/dist/parse-args.js +++ b/dist/parse-args.js @@ -1,4 +1,4 @@ -import * as commander from 'commander'; +import { Command } from '@commander-js/extra-typings'; import { parseArgList } from './utils.js'; import { default as packageJson } from '../package.json' with { type: 'json' }; /** @@ -7,48 +7,55 @@ import { default as packageJson } from '../package.json' with { type: 'json' }; export function parseArgs(args) { // Run the initial arguments through commander in order to determine // which value in the args array is the `command` to execute - let program = parseArgsUsingCommander(args); + const program = parseArgsUsingCommander(args); const command = program.args[0]; // Grab all arguments after the `command` in the args array const commandArgs = args.splice(args.indexOf(command) + 1); // Reprocess the args with the command and command arguments removed - program = parseArgsUsingCommander(args.slice(0, args.indexOf(command))); + // program = parseArgsUsingCommander(args.slice(0, args.indexOf(command))) + const parsedCmdOptions = program.opts(); // Set values for provided options let noOverride = false; // In commander `no-` negates the original value `override` - if (program.override === false) { + if (parsedCmdOptions.override === false) { noOverride = true; } let useShell = false; - if (program.useShell === true) { + if (parsedCmdOptions.useShell === true) { useShell = true; } let expandEnvs = false; - if (program.expandEnvs === true) { + if (parsedCmdOptions.expandEnvs === true) { expandEnvs = true; } let verbose = false; - if (program.verbose === true) { + if (parsedCmdOptions.verbose === true) { verbose = true; } let silent = false; - if (program.silent === true) { + if (parsedCmdOptions.silent === true) { silent = true; } let rc; - if (program.environments !== undefined - && Array.isArray(program.environments) - && program.environments.length !== 0) { + if (parsedCmdOptions.environments !== undefined + && Array.isArray(parsedCmdOptions.environments) + && parsedCmdOptions.environments.length !== 0) { rc = { - environments: program.environments, - filePath: program.rcFile, + environments: parsedCmdOptions.environments, + // if we get a boolean value assume not defined + filePath: parsedCmdOptions.rcFile === true ? + undefined : + parsedCmdOptions.rcFile, }; } let envFile; - if (program.file !== undefined) { + if (parsedCmdOptions.file !== undefined) { envFile = { - filePath: program.file, - fallback: program.fallback, + // if we get a boolean value assume not defined + filePath: parsedCmdOptions.file === true ? + undefined : + parsedCmdOptions.file, + fallback: parsedCmdOptions.fallback, }; } const options = { @@ -70,19 +77,19 @@ export function parseArgs(args) { return options; } export function parseArgsUsingCommander(args) { - const program = new commander.Command(); - return program + return new Command('env-cmd') + .description('CLI for executing commands using an environment from an env file.') .version(packageJson.version, '-v, --version') - .usage('[options] [...args]') - .option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', parseArgList) + .usage('[options] -- [...args]') + .option('-e, --environments [envs...]', 'The rc file environment(s) to use', parseArgList) .option('-f, --file [path]', 'Custom env file path (default path: ./.env)') + .option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc.(js|cjs|mjs|json)') + .option('-x, --expand-envs', 'Replace $var in args and command with environment variables') .option('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--no-override', 'Do not override existing environment variables') - .option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc(|.js|.json)') .option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.') .option('--use-shell', 'Execute the command in a new shell with the given environment') .option('--verbose', 'Print helpful debugging information') - .option('-x, --expand-envs', 'Replace $var in args and command with environment variables') .allowUnknownOption(true) - .parse(['_', '_', ...args]); + .parse(['_', '_', ...args], { from: 'node' }); } diff --git a/dist/types.d.ts b/dist/types.d.ts index ca2795d..92f1ba3 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -1,17 +1,17 @@ -import { Command } from 'commander'; +import type { Command } from '@commander-js/extra-typings'; export type Environment = Partial>; export type RCEnvironment = Partial>; -export interface CommanderOptions extends Command { - override?: boolean; - useShell?: boolean; +export type CommanderOptions = Command<[], { + environments?: true | string[]; expandEnvs?: boolean; - verbose?: boolean; - silent?: boolean; fallback?: boolean; - environments?: string[]; - rcFile?: string; - file?: string; -} + file?: true | string; + override?: boolean; + rcFile?: true | string; + silent?: boolean; + useShell?: boolean; + verbose?: boolean; +}>; export interface RCFileOptions { environments: string[]; filePath?: string; diff --git a/package.json b/package.json index c5e4514..ce83127 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "homepage": "https://github.com/toddbluhm/env-cmd#readme", "dependencies": { + "@commander-js/extra-typings": "^12.1.0", "commander": "^12.1.0", "cross-spawn": "^7.0.6" }, diff --git a/src/parse-args.ts b/src/parse-args.ts index 4bbe764..981c7bc 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -1,4 +1,4 @@ -import * as commander from 'commander' +import { Command } from '@commander-js/extra-typings' import type { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types.ts' import { parseArgList } from './utils.js' import { default as packageJson } from '../package.json' with { type: 'json' }; @@ -9,54 +9,62 @@ import { default as packageJson } from '../package.json' with { type: 'json' }; export function parseArgs(args: string[]): EnvCmdOptions { // Run the initial arguments through commander in order to determine // which value in the args array is the `command` to execute - let program = parseArgsUsingCommander(args) + const program = parseArgsUsingCommander(args) + const command = program.args[0] // Grab all arguments after the `command` in the args array const commandArgs = args.splice(args.indexOf(command) + 1) // Reprocess the args with the command and command arguments removed - program = parseArgsUsingCommander(args.slice(0, args.indexOf(command))) + // program = parseArgsUsingCommander(args.slice(0, args.indexOf(command))) + const parsedCmdOptions = program.opts() // Set values for provided options let noOverride = false // In commander `no-` negates the original value `override` - if (program.override === false) { + if (parsedCmdOptions.override === false) { noOverride = true } let useShell = false - if (program.useShell === true) { + if (parsedCmdOptions.useShell === true) { useShell = true } let expandEnvs = false - if (program.expandEnvs === true) { + if (parsedCmdOptions.expandEnvs === true) { expandEnvs = true } let verbose = false - if (program.verbose === true) { + if (parsedCmdOptions.verbose === true) { verbose = true } let silent = false - if (program.silent === true) { + if (parsedCmdOptions.silent === true) { silent = true } let rc: RCFileOptions | undefined if ( - program.environments !== undefined - && Array.isArray(program.environments) - && program.environments.length !== 0 + parsedCmdOptions.environments !== undefined + && Array.isArray(parsedCmdOptions.environments) + && parsedCmdOptions.environments.length !== 0 ) { rc = { - environments: program.environments, - filePath: program.rcFile, + environments: parsedCmdOptions.environments, + // if we get a boolean value assume not defined + filePath: parsedCmdOptions.rcFile === true ? + undefined : + parsedCmdOptions.rcFile, } } let envFile: EnvFileOptions | undefined - if (program.file !== undefined) { + if (parsedCmdOptions.file !== undefined) { envFile = { - filePath: program.file, - fallback: program.fallback, + // if we get a boolean value assume not defined + filePath: parsedCmdOptions.file === true ? + undefined : + parsedCmdOptions.file, + fallback: parsedCmdOptions.fallback, } } @@ -80,19 +88,19 @@ export function parseArgs(args: string[]): EnvCmdOptions { } export function parseArgsUsingCommander(args: string[]): CommanderOptions { - const program = new commander.Command() as CommanderOptions - return program + return new Command('env-cmd') + .description('CLI for executing commands using an environment from an env file.') .version(packageJson.version, '-v, --version') - .usage('[options] [...args]') - .option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', parseArgList) + .usage('[options] -- [...args]') + .option('-e, --environments [envs...]', 'The rc file environment(s) to use', parseArgList) .option('-f, --file [path]', 'Custom env file path (default path: ./.env)') + .option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc.(js|cjs|mjs|json)') + .option('-x, --expand-envs', 'Replace $var in args and command with environment variables') .option('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--no-override', 'Do not override existing environment variables') - .option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc(|.js|.json)') .option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.') .option('--use-shell', 'Execute the command in a new shell with the given environment') .option('--verbose', 'Print helpful debugging information') - .option('-x, --expand-envs', 'Replace $var in args and command with environment variables') .allowUnknownOption(true) - .parse(['_', '_', ...args]) + .parse(['_', '_', ...args], { from: 'node' }) } diff --git a/src/types.ts b/src/types.ts index 092c637..93d40a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,21 +1,21 @@ -import { Command } from 'commander' +import type { Command } from '@commander-js/extra-typings' // Define an export type export type Environment = Partial> export type RCEnvironment = Partial> -export interface CommanderOptions extends Command { - override?: boolean // Default: false - useShell?: boolean // Default: false +export type CommanderOptions = Command<[], { + environments?: true | string[] expandEnvs?: boolean // Default: false - verbose?: boolean // Default: false - silent?: boolean // Default: false fallback?: boolean // Default false - environments?: string[] - rcFile?: string - file?: string -} + file?: true | string + override?: boolean // Default: false + rcFile?: true | string + silent?: boolean // Default: false + useShell?: boolean // Default: false + verbose?: boolean // Default: false +}> export interface RCFileOptions { environments: string[] diff --git a/test/parse-args.spec.ts b/test/parse-args.spec.ts index bed6479..364502d 100644 --- a/test/parse-args.spec.ts +++ b/test/parse-args.spec.ts @@ -25,31 +25,31 @@ describe('parseArgs', (): void => { }) it('should parse environment value', (): void => { - const res = parseArgs(['-e', environments[0], command]) + const res = parseArgs(['-e', environments[0], '--', command]) assert.exists(res.rc) assert.sameOrderedMembers(res.rc.environments, [environments[0]]) }) it('should parse multiple environment values', (): void => { - const res = parseArgs(['-e', environments.join(','), command]) + const res = parseArgs(['-e', environments.join(','), '--', command]) assert.exists(res.rc) assert.sameOrderedMembers(res.rc.environments, environments) }) it('should parse command value', (): void => { - const res = parseArgs(['-e', environments[0], command]) + const res = parseArgs(['-e', environments[0], '--', command]) assert.equal(res.command, command) }) it('should parse multiple command arguments', (): void => { - const res = parseArgs(['-e', environments[0], command, ...commandArgs]) + const res = parseArgs(['-e', environments[0], '--', command, ...commandArgs]) assert.sameOrderedMembers(res.commandArgs, commandArgs) }) it('should parse multiple command arguments even if they use the same options flags as env-cmd', (): void => { const commandFlags = ['-f', './other-file', '--use-shell', '-r'] - const res = parseArgs(['-e', environments[0], command, ...commandFlags]) + const res = parseArgs(['-e', environments[0], '--', command, ...commandFlags]) assert.sameOrderedMembers(res.commandArgs, commandFlags) assert.notOk(res.options!.useShell) assert.notOk(res.envFile) @@ -57,49 +57,49 @@ describe('parseArgs', (): void => { ) it('should parse override option', (): void => { - const res = parseArgs(['-e', environments[0], '--no-override', command, ...commandArgs]) + const res = parseArgs(['-e', environments[0], '--no-override', '--', command, ...commandArgs]) assert.exists(res.options) assert.isTrue(res.options.noOverride) }) it('should parse use shell option', (): void => { - const res = parseArgs(['-e', environments[0], '--use-shell', command, ...commandArgs]) + const res = parseArgs(['-e', environments[0], '--use-shell', '--', command, ...commandArgs]) assert.exists(res.options) assert.isTrue(res.options.useShell) }) it('should parse rc file path', (): void => { - const res = parseArgs(['-e', environments[0], '-r', rcFilePath, command, ...commandArgs]) + const res = parseArgs(['-e', environments[0], '-r', rcFilePath, '--', command, ...commandArgs]) assert.exists(res.rc) assert.equal(res.rc.filePath, rcFilePath) }) it('should parse env file path', (): void => { - const res = parseArgs(['-f', envFilePath, command, ...commandArgs]) + const res = parseArgs(['-f', envFilePath, '--', command, ...commandArgs]) assert.exists(res.envFile) assert.equal(res.envFile.filePath, envFilePath) }) it('should parse fallback option', (): void => { - const res = parseArgs(['-f', envFilePath, '--fallback', command, ...commandArgs]) + const res = parseArgs(['-f', envFilePath, '--fallback', '--', command, ...commandArgs]) assert.exists(res.envFile) assert.isTrue(res.envFile.fallback) }) it('should print to console.info if --verbose flag is passed', (): void => { - const res = parseArgs(['-f', envFilePath, '--verbose', command, ...commandArgs]) + const res = parseArgs(['-f', envFilePath, '--verbose', '--', command, ...commandArgs]) assert.exists(res.options!.verbose) assert.equal(logInfoStub.callCount, 1) }) it('should parse expandEnvs option', (): void => { - const res = parseArgs(['-f', envFilePath, '-x', command, ...commandArgs]) + const res = parseArgs(['-f', envFilePath, '-x', '--', command, ...commandArgs]) assert.exists(res.envFile) assert.isTrue(res.options!.expandEnvs) }) it('should parse silent option', (): void => { - const res = parseArgs(['-f', envFilePath, '--silent', command, ...commandArgs]) + const res = parseArgs(['-f', envFilePath, '--silent', '--', command, ...commandArgs]) assert.exists(res.envFile) assert.isTrue(res.options!.silent) }) diff --git a/test/test-files/.rc-test-async.cjs b/test/test-files/.rc-test-async.cjs index 7baa971..356914f 100644 --- a/test/test-files/.rc-test-async.cjs +++ b/test/test-files/.rc-test-async.cjs @@ -1,6 +1,5 @@ module.exports = new Promise((resolve) => { setTimeout(() => { - console.log('resolved') resolve({ development: { THANKS: 'FOR ALL THE FISH',