@dotenvx/dotenvx 1.65.2 → 1.66.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,19 @@
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.65.2...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.66.0...main)
6
+
7
+ ## [1.66.0](https://github.com/dotenvx/dotenvx/compare/v1.65.3...v1.66.0) (2026-05-13)
8
+
9
+ ### Added
10
+
11
+ * Add `dotenvx doctor` ([#815](https://github.com/dotenvx/dotenvx/pull/815))
12
+
13
+ ## [1.65.3](https://github.com/dotenvx/dotenvx/compare/v1.65.2...v1.65.3) (2026-05-13)
14
+
15
+ ### Changed
16
+
17
+ * Improve spinner message blinking with simpler `--no-spinner` flag passed to ops ([#814](https://github.com/dotenvx/dotenvx/pull/814))
6
18
 
7
19
  ## [1.65.2](https://github.com/dotenvx/dotenvx/compare/v1.65.1...v1.65.2) (2026-05-13)
8
20
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.65.2",
2
+ "version": "1.66.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "secrets for agents–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -0,0 +1,22 @@
1
+ const { logger } = require('./../../shared/logger')
2
+ const main = require('./../../lib/main')
3
+
4
+ function doctor (directory = '.') {
5
+ logger.debug(`directory: ${directory}`)
6
+
7
+ const findings = main.doctor(directory)
8
+ logger.debug(`findings: ${JSON.stringify(findings)}`)
9
+
10
+ if (findings.length === 0) {
11
+ logger.info('no dotenv loaders found')
12
+ return
13
+ }
14
+
15
+ logger.warn(`found ${findings.length} possible dotenv loader${findings.length === 1 ? '' : 's'}`)
16
+
17
+ for (const finding of findings) {
18
+ logger.info(`│ ${finding.filepath}:${finding.line}: ${finding.code}`)
19
+ }
20
+ }
21
+
22
+ module.exports = doctor
@@ -55,7 +55,7 @@ async function run () {
55
55
  uniqueInjectedKeys
56
56
  } = await new Run(envs, options.overload, process.env, options.envKeysFile, noOps, {
57
57
  beforeOpsKeypair: () => {
58
- if (spinner) spinner.stop()
58
+ if (spinner) spinner.start('retrieving')
59
59
  },
60
60
  afterOpsKeypair: () => {
61
61
  if (spinner) spinner.start('injecting')
@@ -186,6 +186,14 @@ program.command('ls')
186
186
  return require('./actions/ls').apply(this, args)
187
187
  })
188
188
 
189
+ // dotenvx doctor
190
+ program.command('doctor', { hidden: true })
191
+ .description('scan for dotenv loaders')
192
+ .argument('[directory]', 'directory to scan', '.')
193
+ .action(function (...args) {
194
+ return require('./actions/doctor').apply(this, args)
195
+ })
196
+
189
197
  // dotenvx login
190
198
  program.command('login')
191
199
  .description('log in to unlock ⛨ ARMORED KEYS ✦ BETA')
@@ -32,13 +32,14 @@ class Ops {
32
32
  }
33
33
  }
34
34
 
35
- async keypair (publicKey) {
35
+ async keypair (publicKey, options = {}) {
36
36
  if (this._isForcedOff()) return {}
37
37
 
38
38
  const binary = await this._resolveBinary()
39
39
  if (!binary) return {}
40
40
 
41
41
  const args = ['keypair']
42
+ if (options.noSpinner) args.push('--no-spinner')
42
43
  if (publicKey) args.push(publicKey)
43
44
 
44
45
  try {
@@ -48,13 +49,14 @@ class Ops {
48
49
  }
49
50
  }
50
51
 
51
- keypairSync (publicKey) {
52
+ keypairSync (publicKey, options = {}) {
52
53
  if (this._isForcedOff()) return {}
53
54
 
54
55
  const binary = this._resolveBinarySync()
55
56
  if (!binary) return {}
56
57
 
57
58
  const args = ['keypair']
59
+ if (options.noSpinner) args.push('--no-spinner')
58
60
  if (publicKey) args.push(publicKey)
59
61
 
60
62
  try {
@@ -106,9 +108,10 @@ class Ops {
106
108
 
107
109
  _execInteractive (binary, args) {
108
110
  return new Promise((resolve, reject) => {
109
- const subprocess = childProcess.spawn(binary, args, {
111
+ const spawnOptions = {
110
112
  stdio: ['inherit', 'pipe', 'inherit']
111
- })
113
+ }
114
+ const subprocess = childProcess.spawn(binary, args, spawnOptions)
112
115
  let stdout = ''
113
116
 
114
117
  subprocess.stdout.on('data', (data) => {
@@ -5,7 +5,11 @@ async function opsKeypair (existingPublicKey, options = {}) {
5
5
 
6
6
  let kp
7
7
  try {
8
- kp = await new Ops().keypair(existingPublicKey)
8
+ if (options.beforeOpsKeypair || options.afterOpsKeypair) {
9
+ kp = await new Ops().keypair(existingPublicKey, { noSpinner: true })
10
+ } else {
11
+ kp = await new Ops().keypair(existingPublicKey)
12
+ }
9
13
  } finally {
10
14
  if (options.afterOpsKeypair) await options.afterOpsKeypair()
11
15
  }
@@ -1,7 +1,8 @@
1
1
  const Ops = require('../../extensions/ops')
2
2
 
3
- function opsKeypairSync (existingPublicKey) {
4
- const kp = new Ops().keypairSync(existingPublicKey)
3
+ function opsKeypairSync (existingPublicKey, options = {}) {
4
+ const ops = new Ops()
5
+ const kp = Object.keys(options).length > 0 ? ops.keypairSync(existingPublicKey, options) : ops.keypairSync(existingPublicKey)
5
6
  const publicKey = kp.public_key
6
7
  const privateKey = kp.private_key
7
8
 
@@ -72,7 +72,12 @@ function keyValuesSync (filepath, opts = {}) {
72
72
 
73
73
  // ops
74
74
  if (!noOps && !privateKey && publicKey && publicKey.length > 0) {
75
- const kp = opsKeypairSync(publicKey)
75
+ const opsOptions = {}
76
+ if (opts.noSpinner) {
77
+ opsOptions.noSpinner = true
78
+ }
79
+
80
+ const kp = Object.keys(opsOptions).length > 0 ? opsKeypairSync(publicKey, opsOptions) : opsKeypairSync(publicKey)
76
81
  privateKey = kp.privateKey
77
82
  }
78
83
 
package/src/lib/main.d.ts CHANGED
@@ -146,6 +146,14 @@ export interface DotenvConfigOptions {
146
146
 
147
147
  quiet?: boolean;
148
148
 
149
+ /**
150
+ * Disable spinner output from child Dotenvx Ops commands.
151
+ *
152
+ * @default false
153
+ * @example require('@dotenvx/dotenvx').config({ noSpinner: true })
154
+ */
155
+ noSpinner?: boolean;
156
+
149
157
  logLevel?:
150
158
  | 'error'
151
159
  | 'warn'
@@ -355,6 +363,23 @@ export function ls(
355
363
  excludeEnvFile: string | string[]
356
364
  ): string[];
357
365
 
366
+ export type DoctorFinding = {
367
+ lang: string;
368
+ filepath: string;
369
+ line: number;
370
+ code: string;
371
+ msg: string;
372
+ };
373
+
374
+ /**
375
+ * Scan code for dotenv loaders that can conflict with dotenvx.
376
+ *
377
+ * @param directory - directory to scan
378
+ */
379
+ export function doctor(
380
+ directory: string
381
+ ): DoctorFinding[];
382
+
358
383
  export type GenExampleOutput = {
359
384
  envExampleFile: string;
360
385
  envFile: string | string[];
package/src/lib/main.js CHANGED
@@ -7,6 +7,7 @@ const { getColor, bold } = require('./../shared/colors')
7
7
 
8
8
  // services
9
9
  const Ls = require('./services/ls')
10
+ const Doctor = require('./services/doctor')
10
11
  const Run = require('./services/run')
11
12
  const Sets = require('./services/sets')
12
13
  const Get = require('./services/get')
@@ -59,7 +60,9 @@ const config = function (options = {}) {
59
60
  processedEnvs,
60
61
  readableFilepaths,
61
62
  uniqueInjectedKeys
62
- } = new Run(envs, overload, processEnv, envKeysFile, noOps).runSync()
63
+ } = new Run(envs, overload, processEnv, envKeysFile, noOps, {
64
+ noSpinner: options.noSpinner
65
+ }).runSync()
63
66
 
64
67
  let lastError
65
68
  /** @type {Record<string, string>} */
@@ -296,6 +299,10 @@ const ls = function (directory, envFile, excludeEnvFile) {
296
299
  return new Ls(directory, envFile, excludeEnvFile).run()
297
300
  }
298
301
 
302
+ const doctor = function (directory) {
303
+ return new Doctor(directory).run()
304
+ }
305
+
299
306
  /** @type {import('./main').genexample} */
300
307
  const genexample = function (directory, envFile) {
301
308
  return new Genexample(directory, envFile).run()
@@ -324,6 +331,7 @@ module.exports = {
324
331
  set,
325
332
  get,
326
333
  ls,
334
+ doctor,
327
335
  keypair,
328
336
  genexample,
329
337
  // expose for libs depending on @dotenvx/dotenvx - like dotenvx-ops
@@ -0,0 +1,93 @@
1
+ const { fdir: Fdir } = require('fdir')
2
+ const path = require('path')
3
+ const picomatch = require('picomatch')
4
+ const fsx = require('./../helpers/fsx')
5
+
6
+ const patterns = [
7
+ { lang: 'Python', file: /\.py$/, match: /load_dotenv\s*\([^)]*\)/, msg: 'found python-dotenv load call' },
8
+ { lang: 'Python', file: /\.py$/, match: /dotenv_values\s*\([^)]*\)/, msg: 'found python-dotenv values call' },
9
+
10
+ { lang: 'Node', file: /\.(js|cjs|mjs|ts)$/, match: /require\(['"]dotenv['"]\)\.config\s*\([^)]*\)/, msg: 'found dotenv config require call' },
11
+ { lang: 'Node', file: /\.(js|cjs|mjs|ts)$/, match: /import\s+['"]dotenv\/config['"]/, msg: 'found dotenv/config import' },
12
+ { lang: 'Node', file: /\.(js|cjs|mjs|ts)$/, match: /dotenv\.config\s*\([^)]*\)/, msg: 'found dotenv config call' },
13
+
14
+ { lang: 'Ruby', file: /\.rb$/, match: /Dotenv\.(load|overload)\s*\([^)]*\)/, msg: 'found ruby dotenv load call' },
15
+ { lang: 'Go', file: /\.go$/, match: /godotenv\.(Load|Overload)\s*\([^)]*\)/, msg: 'found godotenv load call' },
16
+ { lang: 'PHP', file: /\.php$/, match: /Dotenv\\Dotenv::create\w*\s*\([^)]*\)/, msg: 'found vlucas/phpdotenv loader' },
17
+
18
+ { lang: 'Rust', file: /\.rs$/, match: /dotenvy?::dotenv\s*\([^)]*\)/, msg: 'found Rust dotenv load call' },
19
+ { lang: 'Java', file: /\.java$/, match: /Dotenv\.(load|configure)\s*\([^)]*\)/, msg: 'found java-dotenv load call' },
20
+ { lang: 'Kotlin', file: /\.kt$/, match: /Dotenv\.(load|configure)\s*\([^)]*\)/, msg: 'found java-dotenv load call' },
21
+ { lang: '.NET', file: /\.(cs|fs|vb)$/, match: /(?:DotNetEnv\.)?Env\.Load\s*\([^)]*\)/, msg: 'found DotNetEnv load call' }
22
+ ]
23
+
24
+ class Doctor {
25
+ constructor (directory = './') {
26
+ this.cwd = path.resolve(directory)
27
+ this.ignore = [
28
+ '**/node_modules/**',
29
+ '**/.git/**',
30
+ '**/vendor/**',
31
+ '**/dist/**',
32
+ '**/build/**',
33
+ '**/coverage/**',
34
+ '**/.next/**'
35
+ ]
36
+ this.exclude = picomatch(this.ignore)
37
+ }
38
+
39
+ run () {
40
+ const findings = []
41
+
42
+ for (const filepath of this._filepaths()) {
43
+ findings.push(...this._scanFile(filepath))
44
+ }
45
+
46
+ return findings
47
+ }
48
+
49
+ _filepaths () {
50
+ return new Fdir()
51
+ .withRelativePaths()
52
+ .exclude((dir, filepath) => this._ignored(filepath))
53
+ .filter((filepath) => this._patternsFor(filepath).length > 0)
54
+ .crawl(this.cwd)
55
+ .sync()
56
+ .sort()
57
+ }
58
+
59
+ _scanFile (relativeFilepath) {
60
+ const src = fsx.readFileXSync(path.join(this.cwd, relativeFilepath))
61
+ const lines = src.split(/\r?\n/)
62
+ const findings = []
63
+
64
+ lines.forEach((line, index) => {
65
+ for (const pattern of this._patternsFor(relativeFilepath)) {
66
+ if (pattern.match.test(line)) {
67
+ const match = line.match(pattern.match)
68
+ findings.push({
69
+ lang: pattern.lang,
70
+ filepath: relativeFilepath,
71
+ line: index + 1,
72
+ code: match[0].trim(),
73
+ msg: pattern.msg
74
+ })
75
+ }
76
+ }
77
+ })
78
+
79
+ return findings
80
+ }
81
+
82
+ _patternsFor (filepath) {
83
+ return patterns.filter(pattern => pattern.file.test(filepath))
84
+ }
85
+
86
+ _ignored (filepath) {
87
+ return this.exclude(filepath)
88
+ }
89
+ }
90
+
91
+ Doctor.patterns = patterns
92
+
93
+ module.exports = Doctor
@@ -23,6 +23,7 @@ class Run {
23
23
  this.processEnv = processEnv
24
24
  this.envKeysFilepath = envKeysFilepath
25
25
  this.noOps = noOps
26
+ this.noSpinner = options.noSpinner
26
27
  this.beforeOpsKeypair = options.beforeOpsKeypair
27
28
  this.afterOpsKeypair = options.afterOpsKeypair
28
29
 
@@ -136,7 +137,11 @@ class Run {
136
137
  this.readableFilepaths.add(envFilepath)
137
138
 
138
139
  const { privateKeyName } = keyNames(filepath)
139
- const { privateKeyValue } = keyValuesSync(filepath, { keysFilepath: this.envKeysFilepath, noOps: this.noOps })
140
+ const { privateKeyValue } = keyValuesSync(filepath, {
141
+ keysFilepath: this.envKeysFilepath,
142
+ noOps: this.noOps,
143
+ noSpinner: this.noSpinner
144
+ })
140
145
 
141
146
  const {
142
147
  parsed,