@dotenvx/dotenvx 1.57.5 → 1.59.0

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,7 +2,20 @@
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/v1.57.5...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.59.0...main)
6
+
7
+ ## [1.59.0](https://github.com/dotenvx/dotenvx/compare/v1.58.0...v1.59.0) (2026-03-28)
8
+
9
+ ### Changed
10
+
11
+ * `encrypt` and `set` now create a `.env` file if one does not exist ([#771](https://github.com/dotenvx/dotenvx/pull/771))
12
+ * pass `--no-create` to prevent file creation
13
+
14
+ ## [1.58.0](https://github.com/dotenvx/dotenvx/compare/v1.57.5...v1.58.0) (2026-03-27)
15
+
16
+ ### Changed
17
+
18
+ * Changed runtime injection message to format `⟐ injecting env (N) from FILE · dotenvx@VERSION` ([#770](https://github.com/dotenvx/dotenvx/pull/770))
6
19
 
7
20
  ## [1.57.5](https://github.com/dotenvx/dotenvx/compare/v1.57.4...v1.57.5) (2026-03-26)
8
21
 
@@ -35,6 +48,7 @@ All notable changes to this project will be documented in this file. See [standa
35
48
  * improved error logs and compacted most to a single line ([#755](https://github.com/dotenvx/dotenvx/pull/755))
36
49
  * introduced leading log glyphs as a visual status language:
37
50
 
51
+ * `⟐` success action (injected)
38
52
  * `◈` success action (encrypted)
39
53
  * `◇` success action (set plain value, decrypted)
40
54
  * `⟳` success action (rotated)
package/README.md CHANGED
@@ -34,7 +34,7 @@ or install globally - *unlocks dotenv for any language, framework, or platform!*
34
34
 
35
35
  ```sh
36
36
  npm i -g @dotenvx/dotenvx
37
- dotenvx help
37
+ dotenvx encrypt
38
38
  ```
39
39
 
40
40
  [![npm installs](https://img.shields.io/npm/dt/@dotenvx/dotenvx)](https://npmjs.com/@dotenvx/dotenvx)
@@ -46,7 +46,7 @@ dotenvx help
46
46
 
47
47
  ```sh
48
48
  curl -sfS https://dotenvx.sh | sh
49
- dotenvx help
49
+ dotenvx encrypt
50
50
  ```
51
51
 
52
52
  [![curl installs](https://img.shields.io/endpoint?url=https://dotenvx.sh/stats/curl&label=curl%20installs)](https://github.com/dotenvx/dotenvx.sh/blob/main/install.sh)
@@ -59,7 +59,7 @@ dotenvx help
59
59
 
60
60
  ```sh
61
61
  brew install dotenvx/brew/dotenvx
62
- dotenvx help
62
+ dotenvx encrypt
63
63
  ```
64
64
 
65
65
  [![brew installs](https://img.shields.io/github/downloads/dotenvx/dotenvx/total?label=brew%20installs)](https://github.com/dotenvx/homebrew-brew/blob/main/Formula/dotenvx.rb)
@@ -71,7 +71,7 @@ dotenvx help
71
71
  <details><summary>with docker 🐳</summary><br>
72
72
 
73
73
  ```sh
74
- docker run -it --rm -v $(pwd):/app dotenv/dotenvx help
74
+ docker run -it --rm -v $(pwd):/app dotenv/dotenvx encrypt
75
75
  ```
76
76
 
77
77
  [![docker pulls](https://img.shields.io/docker/pulls/dotenv/dotenvx)](https://hub.docker.com/r/dotenv/dotenvx)
@@ -85,7 +85,7 @@ docker run -it --rm -v $(pwd):/app dotenv/dotenvx help
85
85
  ```sh
86
86
  curl -L -o dotenvx.tar.gz "https://github.com/dotenvx/dotenvx/releases/latest/download/dotenvx-$(uname -s)-$(uname -m).tar.gz"
87
87
  tar -xzf dotenvx.tar.gz
88
- ./dotenvx help
88
+ ./dotenvx encrypt
89
89
  ```
90
90
 
91
91
  [![github releases](https://img.shields.io/github/downloads/dotenvx/dotenvx/total)](https://github.com/dotenvx/dotenvx/releases)
@@ -99,7 +99,7 @@ tar -xzf dotenvx.tar.gz
99
99
 
100
100
  ```sh
101
101
  winget install dotenvx
102
- dotenvx help
102
+ dotenvx encrypt
103
103
  ```
104
104
 
105
105
  </details>
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.57.5",
2
+ "version": "1.59.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -12,12 +12,13 @@ function encrypt () {
12
12
 
13
13
  const envs = this.envs
14
14
  const opsOn = options.opsOff !== true
15
+ const noCreate = options.create === false
15
16
 
16
17
  // stdout - should not have a try so that exit codes can surface to stdout
17
18
  if (options.stdout) {
18
19
  const {
19
20
  processedEnvs
20
- } = new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile, opsOn).run()
21
+ } = new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile, opsOn, noCreate).run()
21
22
 
22
23
  for (const processedEnv of processedEnvs) {
23
24
  console.log(processedEnv.envSrc)
@@ -29,7 +30,7 @@ function encrypt () {
29
30
  processedEnvs,
30
31
  changedFilepaths,
31
32
  unchangedFilepaths
32
- } = new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile, opsOn).run()
33
+ } = new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile, opsOn, noCreate).run()
33
34
 
34
35
  for (const processedEnv of processedEnvs) {
35
36
  logger.verbose(`encrypting ${processedEnv.envFilepath} (${processedEnv.filepath})`)
@@ -23,12 +23,13 @@ function set (key, value) {
23
23
  const envs = this.envs
24
24
  const envKeysFilepath = options.envKeysFile
25
25
  const opsOn = options.opsOff !== true
26
+ const noCreate = options.create === false
26
27
 
27
28
  const {
28
29
  processedEnvs,
29
30
  changedFilepaths,
30
31
  unchangedFilepaths
31
- } = new Sets(key, value, envs, encrypt, envKeysFilepath, opsOn).run()
32
+ } = new Sets(key, value, envs, encrypt, envKeysFilepath, opsOn, noCreate).run()
32
33
 
33
34
  let withEncryption = ''
34
35
 
@@ -116,6 +116,7 @@ program.command('set')
116
116
  .option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
117
117
  .option('-c, --encrypt', 'encrypt value', true)
118
118
  .option('-p, --plain', 'store value as plain text', false)
119
+ .option('--no-create', 'do not create .env file(s) when missing')
119
120
  .option('--ops-off', 'disable dotenvx-ops features', sesh.opsOff())
120
121
  .action(function (...args) {
121
122
  this.envs = envs
@@ -130,6 +131,7 @@ program.command('encrypt')
130
131
  .option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
131
132
  .option('-k, --key <keys...>', 'keys(s) to encrypt (default: all keys in file)')
132
133
  .option('-ek, --exclude-key <excludeKeys...>', 'keys(s) to exclude from encryption (default: none)')
134
+ .option('--no-create', 'do not create .env file(s) when missing')
133
135
  .option('--ops-off', 'disable dotenvx-ops features', sesh.opsOff())
134
136
  .option('--stdout', 'send to stdout')
135
137
  .action(function (...args) {
@@ -0,0 +1,33 @@
1
+ const SAMPLE_ENV_KIT = `
2
+ # ── Database ─────────────────────────────────────
3
+ DATABASE_URL="postgresql://postgres:pass@db.ref.supabase.co:5432/postgres"
4
+
5
+ # ── Auth ─────────────────────────────────────────
6
+ AUTH0_CLIENT_ID="xxxx"
7
+ AUTH0_CLIENT_SECRET="xxxx"
8
+
9
+ # ── AI / LLM ────────────────────────────────────
10
+ OPENAI_API_KEY="sk-xxxx"
11
+ ANTHROPIC_API_KEY="sk-ant-xxxx"
12
+
13
+ # ── Email ────────────────────────────────────────
14
+ RESEND_API_KEY="re_xxxx"
15
+
16
+ # ── Cloud Storage ────────────────────────────────
17
+ AWS_ACCESS_KEY_ID="xxxx"
18
+ AWS_SECRET_ACCESS_KEY="xxxx"
19
+
20
+ # ── Analytics / Monitoring ───────────────────────
21
+ SENTRY_DSN="https://hex@o1234.ingest.us.sentry.io/1234567"
22
+
23
+ # ── Payments ─────────────────────────────────────
24
+ STRIPE_API_KEY="sk_test_xxxx"
25
+
26
+ # ── Feature Flags ────────────────────────────────
27
+ FLAGSMITH_ENV_ID="xxxx"
28
+
29
+ # ── CI/CD / Deployment ──────────────────────────
30
+ VERCEL_TOKEN="vcp_xxxx"
31
+ `.trimStart()
32
+
33
+ module.exports = SAMPLE_ENV_KIT
@@ -26,14 +26,16 @@ const {
26
26
  const replace = require('./../helpers/replace')
27
27
  const dotenvParse = require('./../helpers/dotenvParse')
28
28
  const detectEncoding = require('./../helpers/detectEncoding')
29
+ const SAMPLE_ENV_KIT = require('./../helpers/kits/sample')
29
30
 
30
31
  class Encrypt {
31
- constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null, opsOn = false) {
32
+ constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null, opsOn = false, noCreate = false) {
32
33
  this.envs = determine(envs, process.env)
33
34
  this.key = key
34
35
  this.excludeKey = excludeKey
35
36
  this.envKeysFilepath = envKeysFilepath
36
37
  this.opsOn = opsOn
38
+ this.noCreate = noCreate
37
39
 
38
40
  this.processedEnvs = []
39
41
  this.changedFilepaths = new Set()
@@ -75,8 +77,17 @@ class Encrypt {
75
77
  row.envFilepath = envFilepath
76
78
 
77
79
  try {
80
+ // if noCreate is on then detectEncoding will throw and we'll halt the calls
81
+ // but if noCreate is false then create the file if it doesn't exist
82
+ if (!fsx.existsSync(filepath) && !this.noCreate) {
83
+ fsx.writeFileX(filepath, SAMPLE_ENV_KIT)
84
+ }
78
85
  const encoding = detectEncoding(filepath)
79
86
  let envSrc = fsx.readFileX(filepath, { encoding })
87
+ if (envSrc.trim().length === 0) {
88
+ envSrc = SAMPLE_ENV_KIT
89
+ row.kitCreated = 'sample'
90
+ }
80
91
  const envParsed = dotenvParse(envSrc)
81
92
 
82
93
  let publicKey
@@ -27,13 +27,14 @@ const dotenvParse = require('./../helpers/dotenvParse')
27
27
  const detectEncoding = require('./../helpers/detectEncoding')
28
28
 
29
29
  class Sets {
30
- constructor (key, value, envs = [], encrypt = true, envKeysFilepath = null, opsOn = false) {
30
+ constructor (key, value, envs = [], encrypt = true, envKeysFilepath = null, opsOn = false, noCreate = false) {
31
31
  this.envs = determine(envs, process.env)
32
32
  this.key = key
33
33
  this.value = value
34
34
  this.encrypt = encrypt
35
35
  this.envKeysFilepath = envKeysFilepath
36
36
  this.opsOn = opsOn
37
+ this.noCreate = noCreate
37
38
 
38
39
  this.processedEnvs = []
39
40
  this.changedFilepaths = new Set()
@@ -72,10 +73,30 @@ class Sets {
72
73
  row.changed = false
73
74
 
74
75
  try {
76
+ let seededWithInitialKey = false
77
+
78
+ if (!fsx.existsSync(filepath)) {
79
+ if (this.noCreate) {
80
+ detectEncoding(filepath) // throws ENOENT
81
+ } else {
82
+ fsx.writeFileX(filepath, '')
83
+ }
84
+ }
85
+
75
86
  const encoding = detectEncoding(filepath)
76
87
  let envSrc = fsx.readFileX(filepath, { encoding })
88
+
89
+ // blank files seeded by `set` should contain only the key being set
90
+ if (row.key && envSrc.trim().length === 0) {
91
+ envSrc = `${row.key}="${this.value}"\n`
92
+ seededWithInitialKey = true
93
+ }
94
+
77
95
  const envParsed = dotenvParse(envSrc)
78
96
  row.originalValue = envParsed[row.key] || null
97
+ if (seededWithInitialKey) {
98
+ row.originalValue = null
99
+ }
79
100
  const wasPlainText = !isEncrypted(row.originalValue)
80
101
  this.readableFilepaths.add(envFilepath)
81
102
 
@@ -119,7 +140,13 @@ class Sets {
119
140
 
120
141
  const goingFromPlainTextToEncrypted = wasPlainText && this.encrypt
121
142
  const valueChanged = this.value !== row.originalValue
122
- if (goingFromPlainTextToEncrypted || valueChanged) {
143
+ const shouldPersistSeededPlainValue = seededWithInitialKey && !this.encrypt
144
+
145
+ if (shouldPersistSeededPlainValue) {
146
+ row.envSrc = envSrc
147
+ this.changedFilepaths.add(envFilepath)
148
+ row.changed = true
149
+ } else if (goingFromPlainTextToEncrypted || valueChanged) {
123
150
  row.envSrc = replace(envSrc, this.key, row.encryptedValue || this.value)
124
151
  this.changedFilepaths.add(envFilepath)
125
152
  row.changed = true
@@ -17,7 +17,7 @@ const levels = {
17
17
  const error = (m) => bold(getColor('red')(`☠ ${m}`))
18
18
  const warn = (m) => getColor('orangered')(`⚠ ${m}`)
19
19
  const success = getColor('amber')
20
- const successv = getColor('amber')
20
+ const successv = (m) => getColor('amber')(`⟐ ${m}`)
21
21
  const info = getColor('gray')
22
22
  const help = getColor('dodgerblue')
23
23
  const verbose = getColor('plum')
@@ -57,7 +57,7 @@ function formatMessage (level, message) {
57
57
  case 'success':
58
58
  return success(formattedMessage)
59
59
  case 'successv': // success with 'version'
60
- return successv(`[${currentName}@${currentVersion}] ${formattedMessage}`)
60
+ return successv(`${formattedMessage} · ${currentName}@${currentVersion}`)
61
61
  // info
62
62
  case 'info':
63
63
  return info(formattedMessage)