@dotenvx/dotenvx 1.65.3 → 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,13 @@
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.3...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))
6
12
 
7
13
  ## [1.65.3](https://github.com/dotenvx/dotenvx/compare/v1.65.2...v1.65.3) (2026-05-13)
8
14
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.65.3",
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
@@ -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')
package/src/lib/main.d.ts CHANGED
@@ -363,6 +363,23 @@ export function ls(
363
363
  excludeEnvFile: string | string[]
364
364
  ): string[];
365
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
+
366
383
  export type GenExampleOutput = {
367
384
  envExampleFile: string;
368
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')
@@ -298,6 +299,10 @@ const ls = function (directory, envFile, excludeEnvFile) {
298
299
  return new Ls(directory, envFile, excludeEnvFile).run()
299
300
  }
300
301
 
302
+ const doctor = function (directory) {
303
+ return new Doctor(directory).run()
304
+ }
305
+
301
306
  /** @type {import('./main').genexample} */
302
307
  const genexample = function (directory, envFile) {
303
308
  return new Genexample(directory, envFile).run()
@@ -326,6 +331,7 @@ module.exports = {
326
331
  set,
327
332
  get,
328
333
  ls,
334
+ doctor,
329
335
  keypair,
330
336
  genexample,
331
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