@dotenvx/dotenvx 2.19.2 → 2.20.1

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.
package/CHANGELOG.md CHANGED
@@ -2,13 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
- [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.19.2...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.20.1...main)
6
6
 
7
- ## [2.19.2](https://github.com/dotenvx/dotenvx/compare/v2.19.1...v2.19.2) (2026-08-03)
7
+ ## [2.20.1](https://github.com/dotenvx/dotenvx/compare/v2.20.0...v2.20.1) (2026-08-07)
8
8
 
9
9
  ### Changed
10
10
 
11
- * Example `.env.vault` from precommit ([#935](https://github.com/dotenvx/dotenvx/pull/935))
11
+ * Handle accent marks for `ext precommit` ([#936](https://github.com/dotenvx/dotenvx/pull/936))
12
+
13
+ ## [2.20.0](https://github.com/dotenvx/dotenvx/compare/v2.19.1...v2.20.0) (2026-08-07)
14
+
15
+ ### Added
16
+
17
+ * Add `dotenvx del` command for deleting a key from a .env file ([#939](https://github.com/dotenvx/dotenvx/pull/939))
18
+
19
+ ### Changed
20
+
21
+ * Exempt `.env.vault` from `precommit`/`prebuild` sealed checks (vault-format ciphertext is already safe to commit)
12
22
 
13
23
  ## [2.19.1](https://github.com/dotenvx/dotenvx/compare/v2.19.0...v2.19.1) (2026-07-29)
14
24
 
package/README.md CHANGED
@@ -2211,6 +2211,30 @@ Do not create a missing `.env` file.
2211
2211
  $ dotenvx set HELLO World -f .env.production --no-create
2212
2212
  ```
2213
2213
 
2214
+ </details>
2215
+ <details><summary>`del KEY`</summary><br>
2216
+
2217
+ Delete a key from your `.env` file.
2218
+
2219
+ ```sh
2220
+ $ echo "HELLO=World" > .env
2221
+
2222
+ $ dotenvx del HELLO
2223
+ ◇ removed HELLO (.env)
2224
+ ```
2225
+
2226
+ </details>
2227
+ <details><summary>`del KEY -f`</summary><br>
2228
+
2229
+ Delete a key from another `.env` file.
2230
+
2231
+ ```sh
2232
+ $ echo "HELLO=production" > .env.production
2233
+
2234
+ $ dotenvx del HELLO -f .env.production
2235
+ ◇ removed HELLO (.env.production)
2236
+ ```
2237
+
2214
2238
  </details>
2215
2239
  <details><summary>`encrypt`</summary><br>
2216
2240
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.19.2",
2
+ "version": "2.20.1",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "funding": "https://dotenvx.com",
54
54
  "dependencies": {
55
- "@dotenvx/primitives": "^2.1.1",
55
+ "@dotenvx/primitives": "^2.2.0",
56
56
  "@dotenvx/tooling": "^1.0.3",
57
57
  "yocto-spinner": "^1.2.1"
58
58
  },
@@ -67,7 +67,7 @@
67
67
  "tap": "^21.7.4"
68
68
  },
69
69
  "overrides": {
70
- "brace-expansion": "^5.0.8",
70
+ "brace-expansion": "^5.0.9",
71
71
  "minimatch": "^10.2.5"
72
72
  },
73
73
  "publishConfig": {
@@ -0,0 +1,56 @@
1
+ const fsx = require('./../../lib/helpers/fsx')
2
+ const { logger } = require('./../../shared/logger')
3
+
4
+ const delTransform = require('./../../lib/transforms/del')
5
+
6
+ const catchAndLog = require('../../lib/helpers/catchAndLog')
7
+ const createSpinner = require('../../lib/helpers/createSpinner')
8
+
9
+ async function del (key) {
10
+ const options = this.opts()
11
+ const spinnerOptions = typeof this.optsWithGlobals === 'function' ? this.optsWithGlobals() : options
12
+ const spinner = await createSpinner({ ...spinnerOptions, ...options, text: 'deleting' })
13
+
14
+ logger.debug(`key: ${key}`)
15
+ logger.debug(`options: ${JSON.stringify(options)}`)
16
+
17
+ const envs = this.envs || []
18
+
19
+ let errorCount = 0
20
+
21
+ try {
22
+ const { processedEnvs, changedFilepaths, unchangedFilepaths } = await delTransform({ envs, key })
23
+
24
+ if (spinner) spinner.stop()
25
+ for (const processedEnv of processedEnvs) {
26
+ logger.verbose(`deleting for ${processedEnv.envFilepath}`)
27
+ if (processedEnv.error) {
28
+ errorCount += 1
29
+ logger.error(processedEnv.error.messageWithHelp || processedEnv.error.message)
30
+ } else if (processedEnv.changed) {
31
+ await fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc)
32
+ logger.verbose(`${processedEnv.key} deleted (${processedEnv.envFilepath})`)
33
+ } else {
34
+ logger.verbose(`no change ${processedEnv.envFilepath} (${processedEnv.filepath})`)
35
+ }
36
+ }
37
+
38
+ if (changedFilepaths.length > 0) {
39
+ logger.success(`◇ removed ${key} (${changedFilepaths.join(',')})`)
40
+ } else if (unchangedFilepaths.length > 0) {
41
+ logger.info(`○ no change (${unchangedFilepaths})`)
42
+ } else {
43
+ // do nothing - scenario when no .env files found
44
+ }
45
+
46
+ if (errorCount > 0) {
47
+ process.exit(1)
48
+ }
49
+ } catch (error) {
50
+ if (spinner) spinner.stop()
51
+ catchAndLog(error)
52
+ process.exit(1)
53
+ }
54
+ }
55
+
56
+ module.exports = del
@@ -129,6 +129,18 @@ program.command('set')
129
129
  return require('./actions/set').apply(this, args)
130
130
  })
131
131
 
132
+ // dotenvx del
133
+ program.command('del')
134
+ .usage('<KEY> [options]')
135
+ .description('delete a single environment variable')
136
+ .addHelpText('after', help.del)
137
+ .argument('KEY', 'KEY')
138
+ .option('-f, --env-file <path>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
139
+ .action(function (...args) {
140
+ this.envs = envs
141
+ return require('./actions/del').apply(this, args)
142
+ })
143
+
132
144
  // dotenvx encrypt
133
145
  program.command('encrypt')
134
146
  .description('encrypt .env file(s)')
@@ -317,7 +329,7 @@ program.addHelpText('after', 'Professional Security: ')
317
329
  program.addHelpText('after', ' lock ⊡ lock private keys with a local passphrase')
318
330
  program.addHelpText('after', ' native ⌥ move private keys into your OS secret store')
319
331
  program.addHelpText('after', ' armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
320
- program.addHelpText('after', ' curl ⛨ call authenticated api to Dotenvx Armor [www.dotenvx.com/armor]')
332
+ program.addHelpText('after', ' curl ⛨ call authenticated api Dotenvx Armor [www.dotenvx.com/armor]')
321
333
 
322
334
  // dotenvx native
323
335
  require('./commands/native')(program.command('native', { hidden: true }))
package/src/cli/help.js CHANGED
@@ -90,6 +90,17 @@ Examples:
90
90
  `
91
91
  }
92
92
 
93
+ const del = function () {
94
+ return `
95
+ Examples:
96
+
97
+ \`\`\`
98
+ $ dotenvx del KEY
99
+ $ dotenvx del KEY -f .env.production
100
+ \`\`\`
101
+ `
102
+ }
103
+
93
104
  const curl = function () {
94
105
  return `
95
106
  Endpoints:
@@ -130,5 +141,6 @@ module.exports = {
130
141
  prebuild,
131
142
  gitignore,
132
143
  set,
144
+ del,
133
145
  curl
134
146
  }
@@ -123,8 +123,11 @@ class Precommit {
123
123
  return true
124
124
  }
125
125
 
126
- const output = childProcess.execSync('git diff HEAD --name-only').toString()
127
- const files = output.split('\n')
126
+ // -z separates paths with NUL and prints them verbatim. without it git c-quotes
127
+ // any path holding non-ascii or special characters ("caf\303\251/.env"), which
128
+ // never matches filePath - the file would be skipped and its secrets committed.
129
+ const output = childProcess.execSync('git diff HEAD --name-only -z').toString()
130
+ const files = output.split('\0')
128
131
  return files.includes(filePath)
129
132
  } catch (error) {
130
133
  // consider file to be committed if there is an error (not using git)
@@ -0,0 +1,58 @@
1
+ const fsx = require('./../helpers/fsx')
2
+ const path = require('path')
3
+ const { remove } = require('@dotenvx/primitives')
4
+
5
+ const TYPE_ENV_FILE = 'envFile'
6
+
7
+ const Errors = require('./../helpers/errors')
8
+ const { determine } = require('./../helpers/envResolution')
9
+ const detectEncoding = require('./../helpers/detectEncoding')
10
+
11
+ async function delTransform (options = {}) {
12
+ const envs = options.envs || []
13
+ const key = options.key
14
+
15
+ const processedEnvs = []
16
+ const changedFilepaths = []
17
+ const unchangedFilepaths = []
18
+
19
+ for (const env of determine(envs, process.env)) {
20
+ if (env.type !== TYPE_ENV_FILE) {
21
+ continue
22
+ }
23
+
24
+ const envFilepath = env.envFilepath || env.value
25
+ const filepath = env.filepath || path.resolve(envFilepath)
26
+ const row = { key, type: TYPE_ENV_FILE, filepath, envFilepath, changed: false }
27
+
28
+ try {
29
+ const encoding = await detectEncoding(filepath)
30
+ const before = await fsx.readFileX(filepath, { encoding })
31
+ const after = remove(before, key)
32
+
33
+ row.envSrc = after
34
+ if (after !== before) {
35
+ row.changed = true
36
+ changedFilepaths.push(envFilepath)
37
+ } else {
38
+ unchangedFilepaths.push(envFilepath)
39
+ }
40
+ } catch (error) {
41
+ if (error.code === 'ENOENT') {
42
+ row.error = new Errors({ envFilepath, filepath }).missingEnvFile()
43
+ } else {
44
+ row.error = error
45
+ }
46
+ }
47
+
48
+ processedEnvs.push(row)
49
+ }
50
+
51
+ return {
52
+ processedEnvs,
53
+ changedFilepaths,
54
+ unchangedFilepaths
55
+ }
56
+ }
57
+
58
+ module.exports = delTransform