envdoctor 0.1.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.
- checksums.yaml +7 -0
- data/README.md +33 -0
- data/exe/envdoctor +5 -0
- data/lib/envdoctor/cli.rb +48 -0
- data/lib/envdoctor/scanner.rb +98 -0
- data/lib/envdoctor.rb +8 -0
- metadata +51 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ec3513e2cfdb6870cd1608bf7d5a3cf2c831113187ff3ca34ed6fe9539a231d5
|
|
4
|
+
data.tar.gz: 76b4d2234288c38d2c20c27ee39db004a69da9adfe945a7ef6a856a5e56f7f5b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fc16e6e1ca03e96d1e72a88a05391370afc810790dae1d338a77759d9152b3cdb0ad6ecfd86d51faf093196144eeb2ba66ba352822c3859718114c5a0e0a9fc0
|
|
7
|
+
data.tar.gz: fada228584d093fb6fc71ecc9dbda5cd2d7fe20b66c646e490383c981e87d01db9b2d3e56fb79ca0a5ac8d831e9fcfd569d7d42e837aca2af1bff8513d9a7e47
|
data/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# envdoctor (Ruby)
|
|
2
|
+
|
|
3
|
+
Native Ruby port of [envdoctor](https://github.com/arun-skg/envdoctor) — a
|
|
4
|
+
local-first environment-variable consistency checker, packaged as a gem.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
gem install envdoctor
|
|
8
|
+
envdoctor scan --dir .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
Reconciles variables **used** in Ruby source (`ENV["X"]`, `ENV['X']`,
|
|
14
|
+
`ENV.fetch("X")`) against those **defined** in `.env` files:
|
|
15
|
+
|
|
16
|
+
| Rule | Severity | Meaning |
|
|
17
|
+
|------|----------|---------|
|
|
18
|
+
| `undefined-in-source` | error | Used in code but not defined in any `.env` file |
|
|
19
|
+
| `unused` | warning | Defined in `.env` but never referenced in source |
|
|
20
|
+
|
|
21
|
+
Comments and `=begin/=end` blocks are stripped before scanning. `scan` exits
|
|
22
|
+
`1` on errors (or warnings with `--strict`). Values are never printed.
|
|
23
|
+
|
|
24
|
+
## Development
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd ruby
|
|
28
|
+
ruby -Ilib test/test_scanner.rb
|
|
29
|
+
gem build envdoctor.gemspec
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
One of several native, per-ecosystem ports; the reference implementation lives
|
|
33
|
+
in the [main repository](https://github.com/arun-skg/envdoctor).
|
data/exe/envdoctor
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "scanner"
|
|
5
|
+
|
|
6
|
+
module Envdoctor
|
|
7
|
+
# Command-line entry point.
|
|
8
|
+
module CLI
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def run(argv)
|
|
12
|
+
dir = "."
|
|
13
|
+
strict = false
|
|
14
|
+
parser = OptionParser.new do |o|
|
|
15
|
+
o.banner = "Usage: envdoctor scan [options]"
|
|
16
|
+
o.on("-d", "--dir DIR", "Project root (default: cwd)") { |v| dir = v }
|
|
17
|
+
o.on("--strict", "Treat warnings as errors") { strict = true }
|
|
18
|
+
end
|
|
19
|
+
args = argv.dup
|
|
20
|
+
args.shift if args.first == "scan"
|
|
21
|
+
parser.parse!(args)
|
|
22
|
+
|
|
23
|
+
root = File.expand_path(dir)
|
|
24
|
+
findings = Scanner.scan(root)
|
|
25
|
+
errors = findings.select { |f| f.severity == "error" }
|
|
26
|
+
warnings = findings.select { |f| f.severity == "warning" }
|
|
27
|
+
|
|
28
|
+
puts "ENVIRONMENT AUDIT"
|
|
29
|
+
puts "=" * 40
|
|
30
|
+
if findings.empty?
|
|
31
|
+
puts "\nNo issues found."
|
|
32
|
+
return 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless errors.empty?
|
|
36
|
+
puts "\nErrors"
|
|
37
|
+
errors.each { |f| puts " x #{f.name} #{f.origin.file}:#{f.origin.line} #{f.message}" }
|
|
38
|
+
end
|
|
39
|
+
unless warnings.empty?
|
|
40
|
+
puts "\nWarnings"
|
|
41
|
+
warnings.each { |f| puts " ! #{f.name} #{f.origin.file}:#{f.origin.line} #{f.message}" }
|
|
42
|
+
end
|
|
43
|
+
puts "\nSummary: #{errors.length} error(s), #{warnings.length} warning(s)"
|
|
44
|
+
|
|
45
|
+
(!errors.empty? || (strict && !warnings.empty?)) ? 1 : 0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envdoctor
|
|
4
|
+
# Core scanner: reconcile ENV usage in Ruby source against .env definitions.
|
|
5
|
+
# Local-first — no network, values never printed.
|
|
6
|
+
module Scanner
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
USAGE_PATTERNS = [
|
|
10
|
+
/\bENV\[\s*["']([A-Za-z_]\w*)["']\s*\]/,
|
|
11
|
+
/\bENV\.fetch\(\s*["']([A-Za-z_]\w*)["']/
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
ENV_LINE = /\A\s*(?:export\s+)?([A-Za-z_]\w*)\s*=/.freeze
|
|
15
|
+
|
|
16
|
+
Origin = Struct.new(:file, :line)
|
|
17
|
+
Finding = Struct.new(:rule, :severity, :name, :message, :origin)
|
|
18
|
+
|
|
19
|
+
# Blank comments and =begin/=end blocks, preserving line structure.
|
|
20
|
+
def strip_noise(code)
|
|
21
|
+
code = code.gsub(/^=begin\b.*?^=end\b[^\n]*/m) { |m| m.gsub(/[^\n]/, " ") }
|
|
22
|
+
code.gsub(/#[^\n]*/) { |m| " " * m.length }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def scan_source(path, content)
|
|
26
|
+
text = strip_noise(content)
|
|
27
|
+
used = {}
|
|
28
|
+
USAGE_PATTERNS.each do |re|
|
|
29
|
+
text.to_enum(:scan, re).each do
|
|
30
|
+
match = Regexp.last_match
|
|
31
|
+
name = match[1]
|
|
32
|
+
next if used.key?(name)
|
|
33
|
+
|
|
34
|
+
line = text[0...match.begin(0)].count("\n") + 1
|
|
35
|
+
used[name] = Origin.new(path, line)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
used
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_env(path, content)
|
|
42
|
+
defined = {}
|
|
43
|
+
content.split("\n").each_with_index do |raw, i|
|
|
44
|
+
stripped = raw.strip
|
|
45
|
+
next if stripped.empty? || stripped.start_with?("#")
|
|
46
|
+
|
|
47
|
+
if (m = raw.match(ENV_LINE))
|
|
48
|
+
defined[m[1]] ||= Origin.new(path, i + 1)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
defined
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def discover_env_files(root)
|
|
55
|
+
files = Dir.glob(File.join(root, ".env"))
|
|
56
|
+
files += Dir.glob(File.join(root, ".env.*")).reject { |f| f.end_with?(".example") }
|
|
57
|
+
files.sort
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def discover_source_files(root)
|
|
61
|
+
Dir.glob(File.join(root, "**", "*.rb")).reject do |p|
|
|
62
|
+
p.split(File::SEPARATOR).any? { |part| %w[.git vendor node_modules].include?(part) }
|
|
63
|
+
end.sort
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def scan(root)
|
|
67
|
+
defined = {}
|
|
68
|
+
discover_env_files(root).each do |f|
|
|
69
|
+
parse_env(relative(root, f), File.read(f)).each { |k, v| defined[k] ||= v }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
used = {}
|
|
73
|
+
discover_source_files(root).each do |f|
|
|
74
|
+
scan_source(relative(root, f), File.read(f)).each { |k, v| used[k] ||= v }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
findings = []
|
|
78
|
+
used.keys.sort.each do |name|
|
|
79
|
+
next if defined.key?(name)
|
|
80
|
+
|
|
81
|
+
findings << Finding.new("undefined-in-source", "error", name,
|
|
82
|
+
"used in source code but not defined in any environment file",
|
|
83
|
+
used[name])
|
|
84
|
+
end
|
|
85
|
+
defined.keys.sort.each do |name|
|
|
86
|
+
next if used.key?(name)
|
|
87
|
+
|
|
88
|
+
findings << Finding.new("unused", "warning", name,
|
|
89
|
+
"defined but never referenced in source", defined[name])
|
|
90
|
+
end
|
|
91
|
+
findings
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def relative(root, path)
|
|
95
|
+
path.sub(/\A#{Regexp.escape(root)}#{Regexp.escape(File::SEPARATOR)}?/, "")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/envdoctor.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: envdoctor
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Arun Natesan
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'Reconciles ENV usage in Ruby source against .env files: reports undefined-in-source
|
|
14
|
+
(error) and unused (warning). Local-first, no network.'
|
|
15
|
+
email:
|
|
16
|
+
executables:
|
|
17
|
+
- envdoctor
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- README.md
|
|
22
|
+
- exe/envdoctor
|
|
23
|
+
- lib/envdoctor.rb
|
|
24
|
+
- lib/envdoctor/cli.rb
|
|
25
|
+
- lib/envdoctor/scanner.rb
|
|
26
|
+
homepage: https://github.com/arun-skg/envdoctor
|
|
27
|
+
licenses:
|
|
28
|
+
- MIT
|
|
29
|
+
metadata:
|
|
30
|
+
source_code_uri: https://github.com/arun-skg/envdoctor
|
|
31
|
+
rubygems_mfa_required: 'true'
|
|
32
|
+
post_install_message:
|
|
33
|
+
rdoc_options: []
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.6'
|
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
requirements: []
|
|
47
|
+
rubygems_version: 3.5.22
|
|
48
|
+
signing_key:
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Local-first consistency checker for environment variables (native Ruby port)
|
|
51
|
+
test_files: []
|