@magic/cli 0.0.46 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +17 -7
  2. package/package.json +17 -12
  3. package/src/{exec.mjs → exec.js} +7 -0
  4. package/src/{execFile.mjs → execFile.js} +8 -1
  5. package/src/help/{argToHelp.mjs → argToHelp.js} +11 -1
  6. package/src/help/{envToHelp.mjs → envToHelp.js} +13 -1
  7. package/src/help/findLongestString.js +19 -0
  8. package/src/help/index.js +100 -0
  9. package/src/index.js +75 -0
  10. package/src/parse/argv.js +125 -0
  11. package/src/parse/{clean.mjs → clean.js} +14 -1
  12. package/src/parse/{commands.mjs → commands.js} +14 -2
  13. package/src/parse/env.js +37 -0
  14. package/src/parse/{index.mjs → index.js} +12 -6
  15. package/src/parse/required.js +56 -0
  16. package/src/{prompt.mjs → prompt.js} +11 -2
  17. package/src/spawn.js +20 -0
  18. package/types/exec.d.ts +6 -0
  19. package/types/execFile.d.ts +5 -0
  20. package/types/help/argToHelp.d.ts +5 -0
  21. package/types/help/envToHelp.d.ts +1 -0
  22. package/types/help/findLongestString.d.ts +1 -0
  23. package/types/help/index.d.ts +67 -0
  24. package/types/index.d.ts +57 -0
  25. package/types/parse/argv.d.ts +35 -0
  26. package/types/parse/clean.d.ts +14 -0
  27. package/types/parse/commands.d.ts +11 -0
  28. package/types/parse/env.d.ts +15 -0
  29. package/types/parse/index.d.ts +1 -0
  30. package/types/parse/required.d.ts +16 -0
  31. package/types/prompt.d.ts +8 -0
  32. package/types/spawn.d.ts +5 -0
  33. package/src/help/findLongestString.mjs +0 -15
  34. package/src/help/index.mjs +0 -64
  35. package/src/index.mjs +0 -46
  36. package/src/parse/argv.mjs +0 -118
  37. package/src/parse/env.mjs +0 -22
  38. package/src/parse/required.mjs +0 -32
  39. package/src/spawn.mjs +0 -12
package/README.md CHANGED
@@ -90,7 +90,7 @@ _those issues might get addressed in the future._
90
90
  first, define the cli file
91
91
 
92
92
  ```javascript
93
- // ./bin.mjs
93
+ // ./bin.js
94
94
  import { cli } from '@magic/cli'
95
95
 
96
96
  const res = cli({
@@ -120,7 +120,7 @@ argv mappings handle options and option aliases
120
120
  using the cli file above
121
121
 
122
122
  ```bash
123
- ./bin.mjs -f1 arg1 arg2 -f2
123
+ ./bin.js -f1 arg1 arg2 -f2
124
124
  ```
125
125
 
126
126
  resulting process.argv:
@@ -128,7 +128,7 @@ resulting process.argv:
128
128
  ```javascript
129
129
  process.argv = [
130
130
  '/path/to/bin/node',
131
- '/path/to/bin.mjs',
131
+ '/path/to/bin.js',
132
132
  'cmd1',
133
133
  '--flag1'
134
134
  'arg1',
@@ -169,7 +169,7 @@ cli commands will be handled too.
169
169
  #### <a name="help-simple"></a>simple help message
170
170
 
171
171
  ```javascript
172
- // ./bin.mjs
172
+ // ./bin.js
173
173
 
174
174
  import cli from '@magic/cli'
175
175
 
@@ -183,10 +183,10 @@ const args = {
183
183
  const argv = cli(args)
184
184
  ```
185
185
 
186
- then run ./bin.mjs without arguments
186
+ then run ./bin.js without arguments
187
187
 
188
188
  ```bash
189
- ./bin.mjs
189
+ ./bin.js
190
190
 
191
191
  // help output
192
192
  `
@@ -552,6 +552,16 @@ update dependencies
552
552
  - cli.exec allows stderrToStdout redirect config option
553
553
  - update dependencies
554
554
 
555
- ##### 0.0.47 - unreleased
555
+ ##### 0.0.47
556
+
557
+ - rename .mjs to js
558
+ - add more tests
559
+ - update dependencies
560
+
561
+ ##### 0.0.48
562
+
563
+ - types changed to allow correct props when calling /src/index.js cli function
564
+
565
+ ##### 0.0.49 - unreleased
556
566
 
557
567
  - ...
package/package.json CHANGED
@@ -1,21 +1,25 @@
1
1
  {
2
2
  "name": "@magic/cli",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
4
4
  "homepage": "https://magic.github.io/cli",
5
5
  "description": "declarative command line interfaces with aliasing, commands and environment sanitization",
6
6
  "scripts": {
7
7
  "start": "t -p",
8
8
  "format": "f -w --exclude docs",
9
9
  "test": "t --exclude docs example config.js",
10
- "build": "NODE_ENV=production magic build",
10
+ "build": "tsc && npm run format",
11
+ "prepublishOnly": "npm run test && npm run build",
12
+ "build:docs": "NODE_ENV=production magic build",
11
13
  "prod": "NODE_ENV=production magic build serve",
12
14
  "dev": "magic dev",
13
15
  "calls": "calls"
14
16
  },
15
- "main": "src/index.mjs",
17
+ "main": "src/index.js",
18
+ "types": "types/index.d.ts",
16
19
  "type": "module",
17
20
  "files": [
18
- "src"
21
+ "src",
22
+ "types"
19
23
  ],
20
24
  "engines": {
21
25
  "node": ">=14.15.4"
@@ -32,11 +36,11 @@
32
36
  "declarative"
33
37
  ],
34
38
  "dependencies": {
35
- "@magic/cases": "0.0.9",
36
- "@magic/deep": "0.1.16",
37
- "@magic/error": "0.0.17",
38
- "@magic/log": "0.1.18",
39
- "@magic/types": "0.1.23"
39
+ "@magic/cases": "0.0.10",
40
+ "@magic/deep": "0.1.18",
41
+ "@magic/error": "0.0.20",
42
+ "@magic/log": "0.1.20",
43
+ "@magic/types": "0.1.28"
40
44
  },
41
45
  "devDependencies": {
42
46
  "@magic-modules/git-badges": "0.0.12",
@@ -44,9 +48,10 @@
44
48
  "@magic-modules/no-spy": "0.0.9",
45
49
  "@magic-modules/pre": "0.0.12",
46
50
  "@magic-themes/docs": "0.0.15",
47
- "@magic/core": "0.0.153",
48
- "@magic/format": "0.0.51",
49
- "@magic/test": "0.2.17"
51
+ "@magic/core": "0.0.156",
52
+ "@magic/format": "0.0.68",
53
+ "@magic/test": "0.2.20",
54
+ "typescript": "5.9.3"
50
55
  },
51
56
  "author": "Wizards & Witches",
52
57
  "license": "AGPL-3.0",
@@ -4,6 +4,13 @@ import error from '@magic/error'
4
4
 
5
5
  const libName = '@magic/cli.exec'
6
6
 
7
+ /**
8
+ * Executes a shell command using child_process.exec
9
+ * @param {string} cmd - The shell command to execute.
10
+ * @param {object} [options={}] - Execution options.
11
+ * @param {boolean} [options.stderrToStdout=false] - If true, resolves stderr as stdout instead of rejecting.
12
+ * @returns {Promise<string>} Resolves with stdout or stderr (if stderrToStdout = true), rejects with Error.
13
+ */
7
14
  export const exec = (cmd, options = {}) =>
8
15
  new Promise((resolve, reject) => {
9
16
  const { stderrToStdout, ...opts } = options
@@ -4,6 +4,13 @@ import error from '@magic/error'
4
4
 
5
5
  const libName = '@magic/cli.execFile'
6
6
 
7
+ /**
8
+ * Executes a file using child_process.execFile
9
+ * @param {string} p - Path to the executable file.
10
+ * @param {string[]} [args=[]] - Arguments to pass to the executable.
11
+ * @param {import('child_process').ExecFileOptions} [opts={}] - Execution options.
12
+ * @returns {Promise<string | Buffer>} Resolves with stdout, rejects with Error.
13
+ */
7
14
  export const execFile = (p, args = [], opts = {}) =>
8
15
  new Promise((resolve, reject) => {
9
16
  child_process.execFile(
@@ -17,7 +24,7 @@ export const execFile = (p, args = [], opts = {}) =>
17
24
  return
18
25
  }
19
26
  if (stderr) {
20
- const e = error(new Error(`${libName}: ${cmd} error: ${stderr}`), 'E_EXECFILE_STDERR')
27
+ const e = error(new Error(`${libName}: error: ${stderr}`), 'E_EXECFILE_STDERR')
21
28
  reject(e)
22
29
  return
23
30
  }
@@ -2,10 +2,19 @@ import is from '@magic/types'
2
2
  import log from '@magic/log'
3
3
  import error from '@magic/error'
4
4
 
5
- import { findLongestString } from './findLongestString.mjs'
5
+ import { findLongestString } from './findLongestString.js'
6
6
 
7
7
  const lib = '@magic/cli.help.argToHelp'
8
8
 
9
+ /**
10
+ * Converts an array of CLI argument definitions into a formatted help string.
11
+ *
12
+ * @param {(string | string[])[]} arr - Array of argument names or alias arrays.
13
+ * @param {Record<string, string>} [help={}] - Optional help descriptions keyed by argument name.
14
+ * @param {Record<string, any>} [defaults={}] - Default values for each argument.
15
+ * @returns {string} A formatted help text block.
16
+ * @throws {Error} If `arr` is not a valid array.
17
+ */
9
18
  export const argToHelp = (arr, help = {}, defaults = {}) => {
10
19
  if (!is.array(arr)) {
11
20
  throw error(
@@ -19,6 +28,7 @@ export const argToHelp = (arr, help = {}, defaults = {}) => {
19
28
  return arr
20
29
  .map(opt => {
21
30
  let name = opt
31
+ /** @type {string[]} */
22
32
  let aliases = []
23
33
  if (is.array(name)) {
24
34
  const [n, ...al] = opt
@@ -1,8 +1,20 @@
1
1
  import log from '@magic/log'
2
2
 
3
- import { findLongestString } from './findLongestString.mjs'
3
+ import { findLongestString } from './findLongestString.js'
4
+ import is from '@magic/types'
4
5
 
6
+ /**
7
+ * Converts an array of environment variable definitions into formatted help text.
8
+ *
9
+ * @param {Array<[string[], string, string]>} env - Array of tuples:
10
+ * [command aliases, variable name, value description].
11
+ * @returns {string} A formatted help text block for environment variables.
12
+ */
5
13
  export const envToHelp = env => {
14
+ if (!env || !is.array(env)) {
15
+ return ''
16
+ }
17
+
6
18
  const envStrings = env.map(env => env[0])
7
19
 
8
20
  const longest = findLongestString(envStrings)
@@ -0,0 +1,19 @@
1
+ import deep from '@magic/deep'
2
+
3
+ /**
4
+ * Finds the longest string within a (possibly nested) array.
5
+ *
6
+ * @param {Array<string | string[]>} arr - Array that may contain strings or nested arrays of strings.
7
+ * @returns {string} The longest string found in the array.
8
+ */
9
+ export const findLongestString = arr => {
10
+ const sorted = arr.flat(200).sort((a, b) => {
11
+ if (a.length !== b.length) {
12
+ return b.length - a.length
13
+ }
14
+
15
+ return a > b ? 1 : -1
16
+ })
17
+
18
+ return sorted[0]
19
+ }
@@ -0,0 +1,100 @@
1
+ import log from '@magic/log'
2
+ import is from '@magic/types'
3
+
4
+ import { envToHelp } from './envToHelp.js'
5
+ import { argToHelp } from './argToHelp.js'
6
+
7
+ /**
8
+ * @typedef {object} ParsedCLI
9
+ * @property {Record<string, any>} [args] - Parsed CLI flags or arguments.
10
+ * @property {Record<string, any>} [commands] - Parsed subcommands.
11
+ * @property {Array<string|string[]>} [errors] - Validation errors or missing required args.
12
+ */
13
+
14
+ /**
15
+ * @typedef {object} CLIHelpMeta
16
+ * @property {string} [name] - CLI name shown in the help header.
17
+ * @property {string} [text] - General help text or description.
18
+ * @property {string|string[]} [example] - Example usage text or lines.
19
+ * @property {Record<string, string>} [commands] - Help text for commands.
20
+ * @property {Record<string, string>} [options] - Help text for options/flags.
21
+ * @property {Record<string, string>} [env] - Help text for environment variables.
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} CLIArgs
26
+ * @property {ParsedCLI} [parsed] - Parsed CLI data (from argument parser).
27
+ * @property {Array<(string|string[])>} [commands] - CLI commands or argument definitions.
28
+ * @property {Array<(string|string[])>} [options] - CLI option/flag definitions.
29
+ * @property {Array<[string[], string, string]>} [env] - Environment variable mappings.
30
+ * @property {Record<string, any>} [default] - Default values for options or flags.
31
+ * @property {CLIHelpMeta} [help] - Help configuration or raw string help text.
32
+ */
33
+
34
+ /**
35
+ * Builds and returns formatted help text for a CLI command.
36
+ * Returns `false` if help is not requested and no errors occurred.
37
+ *
38
+ * @param {CLIArgs} args - The CLI arguments/config object.
39
+ * @returns {string|false} The generated help text or `false` if not shown.
40
+ */
41
+ export const maybeHelp = args => {
42
+ const { parsed = {} } = args
43
+
44
+ const hasCommands = args.commands && Object.entries(args.commands).length > 0
45
+ const showCommandHelp =
46
+ hasCommands && parsed.commands && Object.keys(parsed.commands).length === 0
47
+
48
+ const helpRequested = parsed.args?.help
49
+
50
+ const showHelp = showCommandHelp || helpRequested || !is.empty(parsed.errors)
51
+
52
+ if (!showHelp) {
53
+ return false
54
+ }
55
+
56
+ // add help text inspection here
57
+ const { commands = [], default: defaults = [], options = [], env = [], help = {} } = args
58
+
59
+ const commandHelp = argToHelp(commands, help.commands)
60
+ const optionHelp = argToHelp(options, help.options, defaults)
61
+ const envHelp = envToHelp(env)
62
+
63
+ const name = help.name || '@magic/cli wrapped cli.'
64
+ const header = is.string(help) ? help : help.text
65
+
66
+ const exampleArray = is.string(help.example) ? help.example.split('\n') : help.example || []
67
+
68
+ const exampleText = exampleArray
69
+ .map(a => {
70
+ if (a.trim().startsWith('#')) {
71
+ return log.color('green', a)
72
+ } else {
73
+ return a.trim()
74
+ }
75
+ })
76
+ .join('\n')
77
+
78
+ const helpArray = [
79
+ log.paint('green', name),
80
+ '\n',
81
+ header && `\n${log.paint('grey', header)}\n\n`,
82
+ commands.length && `${log.paint('grey', 'commands')}:\n${commandHelp}\n\n`,
83
+ options.length && `${log.paint('grey', 'flags')}:\n${optionHelp}\n\n`,
84
+ env.length && `${log.paint('grey', 'environment switches')}:\n${envHelp}\n\n`,
85
+ 'examples:\n',
86
+ exampleText,
87
+ ]
88
+
89
+ const errors = parsed.errors
90
+ ?.map(e => `${log.paint('red', 'Error:')} ${is.arr(e) ? e.join(' or ') : e} is required`)
91
+ .filter(a => a)
92
+ .join('\n')
93
+
94
+ const errorMsg = `\n${errors}`
95
+ if (errors?.length) {
96
+ helpArray.push(errorMsg)
97
+ }
98
+
99
+ return helpArray.filter(a => a).join('')
100
+ }
package/src/index.js ADDED
@@ -0,0 +1,75 @@
1
+ import log from '@magic/log'
2
+
3
+ import { maybeHelp } from './help/index.js'
4
+ import { parse } from './parse/index.js'
5
+
6
+ import { exec as execute } from './exec.js'
7
+ import { execFile as executeFile } from './execFile.js'
8
+ import { spawn as spawner } from './spawn.js'
9
+ import { prompt as promptUser } from './prompt.js'
10
+
11
+ /**
12
+ * @typedef {object} ParseProps
13
+ * @property {Array<string|string[]>} [options]
14
+ * @property {Record<string, any>|Array<any>} [prepend]
15
+ * @property {Record<string, any>|Array<any>} [append]
16
+ * @property {Record<string, any>} [default]
17
+ * @property {boolean} [pure]
18
+ * @property {boolean} [pureEnv]
19
+ * @property {boolean} [pureArgv]
20
+ * @property {boolean} [pureCommands]
21
+ * @property {Array<string|string[]>} [commands]
22
+ * @property {Array<[string[], string, string]>} [env]
23
+ * @property {Array<string|string[]>} [required]
24
+ * @property {object} [help]
25
+ */
26
+
27
+ /**
28
+ * @typedef {object} ParsedCLI
29
+ * @property {Record<string, string>} env
30
+ * @property {Record<string, any>} argv
31
+ * @property {Record<string, any>} args
32
+ * @property {Record<string, boolean>} commands
33
+ * @property {Array<string|string[]>} errors
34
+ */
35
+
36
+ /**
37
+ * Main CLI entry point
38
+ * @param {ParseProps} args - CLI configuration object.
39
+ * @returns {ParsedCLI}
40
+ */
41
+ export const cli = (args = {}) => {
42
+ const { options = [] } = args
43
+
44
+ const hasHelpOption = options.some(option => option.includes('--help'))
45
+ if (!hasHelpOption) {
46
+ options.push(['--help', '-h'])
47
+ }
48
+
49
+ args.options = options
50
+
51
+ const parsed = parse(args)
52
+
53
+ const helpText = maybeHelp({ ...args, parsed })
54
+
55
+ if (helpText) {
56
+ log(helpText)
57
+ process.exit()
58
+ }
59
+
60
+ return parsed
61
+ }
62
+
63
+ export const spawn = spawner
64
+ cli.spawn = spawner
65
+
66
+ export const exec = execute
67
+ cli.exec = execute
68
+
69
+ export const prompt = promptUser
70
+ cli.prompt = promptUser
71
+
72
+ export const execFile = executeFile
73
+ cli.execFile = executeFile
74
+
75
+ export default cli
@@ -0,0 +1,125 @@
1
+ import is from '@magic/types'
2
+ import cases from '@magic/cases'
3
+
4
+ /**
5
+ * @typedef {string | string[]} Option
6
+ */
7
+
8
+ /**
9
+ * @typedef {Record<string, string | string[]>} ArgMap
10
+ */
11
+
12
+ /**
13
+ * @typedef {object} ParseArgvProps
14
+ * @property {Option[]} [options] - CLI option names or aliases.
15
+ * @property {ArgMap | string[]} [prepend] - Arguments to prepend to process.argv.
16
+ * @property {ArgMap | string[]} [append] - Arguments to append to process.argv.
17
+ * @property {ArgMap} [default] - Default values for options.
18
+ * @property {boolean} [pure=false] - If true, do not modify process.argv.
19
+ */
20
+
21
+ /**
22
+ * @typedef {object} ParseArgvResult
23
+ * @property {Record<string, string[]>} argv - Raw argument key/value map.
24
+ * @property {Record<string, string[]>} args - Camel-cased argument key/value map.
25
+ */
26
+
27
+ /**
28
+ * Parses process.argv according to provided options, defaults, prepend/append arguments.
29
+ *
30
+ * @param {ParseArgvProps} props
31
+ * @returns {ParseArgvResult}
32
+ */
33
+ export const parseArgv = (props = {}) => {
34
+ const { options = [], prepend = {}, append = {}, default: def = {}, pure = false } = props || {}
35
+
36
+ /** @type {string|undefined} */
37
+ let lastArg
38
+
39
+ /** @type {Record<string, string[]>} */
40
+ const argv = {}
41
+
42
+ const argvCopy = process.argv.slice()
43
+
44
+ // Handle prepend args first
45
+ if (prepend) {
46
+ if (is.array(prepend)) {
47
+ argvCopy.splice(2, 0, ...prepend.map(String))
48
+ } else {
49
+ Object.entries(prepend).forEach(([k, v]) => {
50
+ argv[k] = is.array(v) ? v.map(String) : [String(v)]
51
+ argvCopy.splice(2, 0, k, ...(is.array(v) ? v.map(String) : [String(v)]))
52
+ })
53
+ }
54
+ }
55
+
56
+ // Append arguments will be added after existing args
57
+ if (append) {
58
+ if (is.array(append)) {
59
+ argvCopy.push(...append.map(String))
60
+ } else {
61
+ Object.entries(append).forEach(([k, v]) => {
62
+ argv[k] = is.array(v) ? v.map(String) : [String(v)]
63
+ argvCopy.push(k, ...(is.array(v) ? v.map(String) : [String(v)]))
64
+ })
65
+ }
66
+ }
67
+
68
+ // Parsing loop
69
+ argvCopy.forEach((arg, i) => {
70
+ if (i <= 1) return
71
+
72
+ if (arg.startsWith('-')) {
73
+ /** @type {string} */
74
+ let argvArg = ''
75
+ options.forEach(option => {
76
+ if (is.array(option)) {
77
+ if (option.some(opt => opt === arg)) {
78
+ argvArg = option[0]
79
+ }
80
+ } else if (option === arg) {
81
+ argvArg = option
82
+ }
83
+ })
84
+
85
+ if (argvArg) {
86
+ lastArg = argvArg
87
+ argv[lastArg] = argv[lastArg] || []
88
+ if (!pure) argvCopy[i] = lastArg
89
+ }
90
+ } else {
91
+ if (lastArg) argv[lastArg].push(arg)
92
+ }
93
+ })
94
+
95
+ const [argv1, argv2, ...argvRest] = argvCopy
96
+
97
+ const entries = Object.entries(def)
98
+
99
+ if (entries.length) {
100
+ entries.forEach(([k, v]) => {
101
+ if (!argv[k] || argv[k].length === 0) {
102
+ argv[k] = is.array(v) ? v.map(String) : [String(v)]
103
+ argvRest.push(k)
104
+ if (!is.array(v)) v = [v]
105
+ v.forEach(vv => argvRest.push(String(vv)))
106
+ }
107
+ })
108
+ }
109
+
110
+ if (!pure) {
111
+ process.argv = [argv1, argv2, ...argvRest]
112
+ }
113
+
114
+ /** @type {Record<string, string[]>} */
115
+ const args = {}
116
+
117
+ Object.entries(argv).forEach(([k, v]) => {
118
+ args[cases.camel(k)] = v
119
+ })
120
+
121
+ return {
122
+ argv,
123
+ args,
124
+ }
125
+ }
@@ -1,12 +1,25 @@
1
1
  import is from '@magic/types'
2
2
  import cases from '@magic/cases'
3
3
 
4
+ /**
5
+ * @typedef {object} CleanProps
6
+ * @property {string[]} [single] - List of single-value argument keys to clean.
7
+ * @property {Record<string, any>} [default] - Default values for arguments.
8
+ */
9
+
10
+ /**
11
+ * Cleans CLI parsed arguments for single-value keys, applying defaults.
12
+ *
13
+ * @param {import('../index.js').ParsedCLI} cli
14
+ * @param {CleanProps} [props={}]
15
+ * @returns {import('../index.js').ParsedCLI}
16
+ */
4
17
  export const clean = (cli, props = {}) => {
5
18
  if (is.empty(props.single)) {
6
19
  return cli
7
20
  }
8
21
 
9
- props.single.map(s => {
22
+ props.single?.map(s => {
10
23
  const c = cases.camel(s)
11
24
  const def = props.default && (props.default[s] || props.default[c])
12
25
 
@@ -1,6 +1,18 @@
1
1
  import log from '@magic/log'
2
2
  import is from '@magic/types'
3
3
 
4
+ /**
5
+ * @typedef {object} ParseCommandsProps
6
+ * @property {Array<string|string[]>} [commands] - Commands or alias arrays to check.
7
+ * @property {boolean} [pure=false] - If true, do not modify process.argv.
8
+ */
9
+
10
+ /**
11
+ * Parses CLI commands from process.argv.
12
+ *
13
+ * @param {ParseCommandsProps} props
14
+ * @returns {Record<string, boolean>} - Commands found in argv keyed by name.
15
+ */
4
16
  export const parseCommands = (props = {}) => {
5
17
  const { commands = [], pure = false } = props
6
18
  const { argv } = process
@@ -38,7 +50,7 @@ export const parseCommands = (props = {}) => {
38
50
  }
39
51
 
40
52
  if (idx > -1) {
41
- if (!pure) {
53
+ if (!pure && !is.undefined(key)) {
42
54
  argv[idx] = key
43
55
  }
44
56
 
@@ -47,7 +59,7 @@ export const parseCommands = (props = {}) => {
47
59
  return [key, true]
48
60
  }
49
61
  })
50
- .filter(a => a),
62
+ .filter(a => !is.undefined(a)),
51
63
  )
52
64
 
53
65
  return cmds
@@ -0,0 +1,37 @@
1
+ import is from '@magic/types'
2
+ /**
3
+ * @typedef {[string|string[], string, string]} EnvTuple
4
+ * Tuple of: [CLI switches], environment variable name, value].
5
+ *
6
+ * @typedef {object} ParseEnvProps
7
+ * @property {EnvTuple[]} env - Environment variables to parse.
8
+ * @property {boolean} [pure=false] - If true, do not modify process.env.
9
+ */
10
+
11
+ /**
12
+ * Parses environment switches from process.argv.
13
+ *
14
+ * @param {ParseEnvProps} props
15
+ * @returns {Record<string, string>} - Environment variables set.
16
+ */
17
+ export const parseEnv = ({ env = [], pure = false }) => {
18
+ /** @type {Record<string, string>} */
19
+ const environment = {}
20
+
21
+ // set env depending on env switches
22
+ env
23
+ .filter(([argv]) => {
24
+ if (!is.array(argv)) {
25
+ argv = [argv]
26
+ }
27
+ return argv.some(a => process.argv.includes(a))
28
+ })
29
+ .map(([_, key, val]) => {
30
+ environment[key] = val
31
+ if (!pure) {
32
+ process.env[key] = val
33
+ }
34
+ })
35
+
36
+ return environment
37
+ }
@@ -1,15 +1,21 @@
1
- import { parseEnv } from './env.mjs'
2
- import { parseArgv } from './argv.mjs'
3
- import { parseCommands } from './commands.mjs'
4
- import { parseRequired } from './required.mjs'
5
- import { clean } from './clean.mjs'
1
+ import { parseEnv } from './env.js'
2
+ import { parseArgv } from './argv.js'
3
+ import { parseCommands } from './commands.js'
4
+ import { parseRequired } from './required.js'
5
+ import { clean } from './clean.js'
6
6
 
7
+ /**
8
+ * Parses CLI arguments, commands, environment variables, and required options.
9
+ *
10
+ * @param {import('../index.js').ParseProps} props
11
+ * @returns {import('../index.js').ParsedCLI}
12
+ */
7
13
  export const parse = props => {
8
14
  const { pure = false } = props
9
15
 
10
16
  const { pureEnv = pure, pureArgv = pure, pureCommands = pure } = props
11
17
 
12
- const env = parseEnv({ ...props, pure: pureEnv })
18
+ const env = parseEnv({ env: props.env || [], pure: pureEnv })
13
19
  const { argv, args } = parseArgv({ ...props, pure: pureArgv })
14
20
  const commands = parseCommands({ ...props, pure: pureCommands })
15
21