envspec 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/exe/envspec +1 -104
- data/lib/envspec/cli.rb +179 -0
- data/lib/envspec/version.rb +1 -1
- data/lib/envspec.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8d59afdbad3765ce2c0516b2cfd710b877f3da5b123ff52a8c2423259ea1825
|
|
4
|
+
data.tar.gz: ddece928a9d0b0d20d3a096f7d6a51d36422ebd9300d86c2b80d1102b45734b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0ec195a2b9eb390aaad8fc547371b3ed8251f599c8916e535fbcac5448cec6dd85ca28a86e537b01484cbd52766315eff2235d003f21c73ec111ba6acf33750
|
|
7
|
+
data.tar.gz: 3e77c5274e45e61cde1f1e8792a934efe7b3aaf3914ea7da4e1715750e9c1737a93c26f8899a4ac54b88ffa2fd97cb4b3428b5f8d6e665d76c1413dee8a153c0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1 — 2026-05-04
|
|
4
|
+
|
|
5
|
+
- Refactor CLI into `EnvSpec::CLI` with proper subcommand structure (per-subcommand `OptionParser` with banners, options, and `-h` help). Still pure stdlib.
|
|
6
|
+
- `envspec help <command>` and `envspec <command> -h` now show subcommand-specific usage.
|
|
7
|
+
- Better exit codes (2 for usage errors, 1 for parse/validation errors, 0 for success).
|
|
8
|
+
|
|
3
9
|
## 0.1.0 — 2026-05-04
|
|
4
10
|
|
|
5
11
|
Initial release.
|
data/exe/envspec
CHANGED
|
@@ -1,106 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
require "optparse"
|
|
3
2
|
require "envspec"
|
|
4
|
-
|
|
5
|
-
def die(msg, code = 1)
|
|
6
|
-
warn "envspec: #{msg}"
|
|
7
|
-
exit code
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def usage
|
|
11
|
-
<<~USAGE
|
|
12
|
-
Usage: envspec <command> [options]
|
|
13
|
-
|
|
14
|
-
Commands:
|
|
15
|
-
lint <file> Parse and validate syntax of an env.spec file
|
|
16
|
-
check <file> [--env=NAME] Check current ENV against the spec
|
|
17
|
-
(presence + type validation)
|
|
18
|
-
init [--force] [--output=PATH]
|
|
19
|
-
Scan repo for env var usages and generate env.spec
|
|
20
|
-
|
|
21
|
-
Options:
|
|
22
|
-
-h, --help Show this help
|
|
23
|
-
-v, --version Show version
|
|
24
|
-
|
|
25
|
-
USAGE
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
cmd = ARGV.shift
|
|
29
|
-
|
|
30
|
-
case cmd
|
|
31
|
-
when nil, "-h", "--help", "help"
|
|
32
|
-
puts usage
|
|
33
|
-
exit 0
|
|
34
|
-
|
|
35
|
-
when "-v", "--version", "version"
|
|
36
|
-
puts EnvSpec::VERSION
|
|
37
|
-
exit 0
|
|
38
|
-
|
|
39
|
-
when "lint"
|
|
40
|
-
file = ARGV.shift or die "lint: missing file argument\n\n#{usage}"
|
|
41
|
-
die "file not found: #{file}" unless File.exist?(file)
|
|
42
|
-
begin
|
|
43
|
-
EnvSpec.parse_file(file)
|
|
44
|
-
puts "✓ #{file} is valid"
|
|
45
|
-
rescue EnvSpec::ParseError => e
|
|
46
|
-
die e.message
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
when "check"
|
|
50
|
-
file = nil
|
|
51
|
-
env = "*"
|
|
52
|
-
strict = false
|
|
53
|
-
|
|
54
|
-
parser = OptionParser.new do |o|
|
|
55
|
-
o.on("--env=NAME") { |v| env = v }
|
|
56
|
-
o.on("--strict") { strict = true }
|
|
57
|
-
end
|
|
58
|
-
rest = parser.parse(ARGV)
|
|
59
|
-
file = rest.shift or die "check: missing file argument\n\n#{usage}"
|
|
60
|
-
die "file not found: #{file}" unless File.exist?(file)
|
|
61
|
-
|
|
62
|
-
spec = begin
|
|
63
|
-
EnvSpec.parse_file(file)
|
|
64
|
-
rescue EnvSpec::ParseError => e
|
|
65
|
-
die e.message
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
begin
|
|
69
|
-
spec.validate!(ENV.to_h, env: env, strict: strict)
|
|
70
|
-
scope_label = env == "*" ? "shared" : env
|
|
71
|
-
puts "✓ ENV satisfies #{file} (env: #{scope_label})"
|
|
72
|
-
rescue EnvSpec::ValidationError => e
|
|
73
|
-
die e.message
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
when "init"
|
|
77
|
-
force = false
|
|
78
|
-
output = "env.spec"
|
|
79
|
-
root = Dir.pwd
|
|
80
|
-
|
|
81
|
-
parser = OptionParser.new do |o|
|
|
82
|
-
o.on("--force") { force = true }
|
|
83
|
-
o.on("--output=PATH") { |v| output = v }
|
|
84
|
-
o.on("--root=PATH") { |v| root = v }
|
|
85
|
-
end
|
|
86
|
-
parser.parse(ARGV)
|
|
87
|
-
|
|
88
|
-
result = EnvSpec::Init.run(root: root, force: force, output: output)
|
|
89
|
-
|
|
90
|
-
unless result[:ok]
|
|
91
|
-
die "#{output} already exists (use --force to overwrite)" if result[:reason] == :exists
|
|
92
|
-
die "init failed: #{result[:reason]}"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
puts "✓ Scanned in #{format('%.2fs', result[:elapsed])}"
|
|
96
|
-
puts "✓ Found #{result[:keys]} env vars across #{result[:files]} files"
|
|
97
|
-
puts "✓ Wrote #{result[:path]}"
|
|
98
|
-
puts ""
|
|
99
|
-
puts "Next steps:"
|
|
100
|
-
puts " 1. Edit #{output} — review heuristic suggestions, classify secrets"
|
|
101
|
-
puts " 2. Run `envspec lint #{output}` to validate"
|
|
102
|
-
puts " 3. Run `envspec check #{output}` to test against your current ENV"
|
|
103
|
-
|
|
104
|
-
else
|
|
105
|
-
die "unknown command: #{cmd}\n\n#{usage}"
|
|
106
|
-
end
|
|
3
|
+
exit EnvSpec::CLI.run(ARGV)
|
data/lib/envspec/cli.rb
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
class EnvSpec
|
|
4
|
+
class CLI
|
|
5
|
+
SUBCOMMANDS = %w[lint check init help version].freeze
|
|
6
|
+
|
|
7
|
+
def self.run(argv)
|
|
8
|
+
new.run(argv)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(argv)
|
|
12
|
+
argv = argv.dup
|
|
13
|
+
cmd = argv.shift
|
|
14
|
+
|
|
15
|
+
case cmd
|
|
16
|
+
when nil, "help", "-h", "--help"
|
|
17
|
+
print_help(argv.first)
|
|
18
|
+
0
|
|
19
|
+
when "version", "-v", "--version"
|
|
20
|
+
puts EnvSpec::VERSION
|
|
21
|
+
0
|
|
22
|
+
when "lint"
|
|
23
|
+
cmd_lint(argv)
|
|
24
|
+
when "check"
|
|
25
|
+
cmd_check(argv)
|
|
26
|
+
when "init"
|
|
27
|
+
cmd_init(argv)
|
|
28
|
+
else
|
|
29
|
+
warn "envspec: unknown command '#{cmd}'"
|
|
30
|
+
warn ""
|
|
31
|
+
print_help
|
|
32
|
+
2
|
|
33
|
+
end
|
|
34
|
+
rescue OptionParser::ParseError => e
|
|
35
|
+
warn "envspec: #{e.message}"
|
|
36
|
+
2
|
|
37
|
+
rescue EnvSpec::ParseError, EnvSpec::ValidationError => e
|
|
38
|
+
warn "envspec: #{e.message}"
|
|
39
|
+
1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# ── help ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def print_help(topic = nil)
|
|
47
|
+
case topic
|
|
48
|
+
when "lint" then puts lint_parser.help
|
|
49
|
+
when "check" then puts check_parser({}).help
|
|
50
|
+
when "init" then puts init_parser({}).help
|
|
51
|
+
else puts global_help
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def global_help
|
|
56
|
+
<<~HELP
|
|
57
|
+
Usage: envspec <command> [options]
|
|
58
|
+
|
|
59
|
+
Commands:
|
|
60
|
+
lint <file> Parse and validate syntax of an env.spec file
|
|
61
|
+
check <file> Check current ENV against the spec
|
|
62
|
+
init Scan repo for env vars and generate env.spec
|
|
63
|
+
help [command] Show help for a command
|
|
64
|
+
version Show version
|
|
65
|
+
|
|
66
|
+
Run `envspec help <command>` for command-specific options.
|
|
67
|
+
Docs: https://github.com/repleadfy/envspec
|
|
68
|
+
HELP
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ── lint ──────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def lint_parser
|
|
74
|
+
OptionParser.new do |o|
|
|
75
|
+
o.banner = "Usage: envspec lint <file>"
|
|
76
|
+
o.separator ""
|
|
77
|
+
o.separator "Parses an env.spec file and reports any syntax errors."
|
|
78
|
+
o.separator ""
|
|
79
|
+
o.separator "Options:"
|
|
80
|
+
o.on("-h", "--help") { puts o; exit 0 }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def cmd_lint(argv)
|
|
85
|
+
rest = lint_parser.parse(argv)
|
|
86
|
+
file = rest.shift or (warn lint_parser.help; return 2)
|
|
87
|
+
die_unless_file(file)
|
|
88
|
+
|
|
89
|
+
EnvSpec.parse_file(file)
|
|
90
|
+
puts "✓ #{file} is valid"
|
|
91
|
+
0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ── check ─────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
def check_parser(opts)
|
|
97
|
+
OptionParser.new do |o|
|
|
98
|
+
o.banner = "Usage: envspec check <file> [options]"
|
|
99
|
+
o.separator ""
|
|
100
|
+
o.separator "Validates the current process ENV against an env.spec file."
|
|
101
|
+
o.separator "Checks presence of required keys and that values match declared types."
|
|
102
|
+
o.separator ""
|
|
103
|
+
o.separator "Options:"
|
|
104
|
+
o.on("--env=NAME", "Env scope to validate against (default: shared/*)") do |v|
|
|
105
|
+
opts[:env] = v
|
|
106
|
+
end
|
|
107
|
+
o.on("--strict", "Treat unknown env vars as problems") do
|
|
108
|
+
opts[:strict] = true
|
|
109
|
+
end
|
|
110
|
+
o.on("-h", "--help") { puts o; exit 0 }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def cmd_check(argv)
|
|
115
|
+
opts = { env: "*", strict: false }
|
|
116
|
+
rest = check_parser(opts).parse(argv)
|
|
117
|
+
file = rest.shift or (warn check_parser(opts).help; return 2)
|
|
118
|
+
die_unless_file(file)
|
|
119
|
+
|
|
120
|
+
spec = EnvSpec.parse_file(file)
|
|
121
|
+
spec.validate!(ENV.to_h, env: opts[:env], strict: opts[:strict])
|
|
122
|
+
|
|
123
|
+
label = opts[:env] == "*" ? "shared" : opts[:env]
|
|
124
|
+
puts "✓ ENV satisfies #{file} (env: #{label})"
|
|
125
|
+
0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ── init ──────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
def init_parser(opts)
|
|
131
|
+
OptionParser.new do |o|
|
|
132
|
+
o.banner = "Usage: envspec init [options]"
|
|
133
|
+
o.separator ""
|
|
134
|
+
o.separator "Scans the current directory tree for env var usages across"
|
|
135
|
+
o.separator "Ruby/Python/JS/Go/Shell sources and writes a starter env.spec."
|
|
136
|
+
o.separator ""
|
|
137
|
+
o.separator "Options:"
|
|
138
|
+
o.on("--force", "Overwrite an existing env.spec") { opts[:force] = true }
|
|
139
|
+
o.on("--output=PATH", "Output file (default: env.spec)") { |v| opts[:output] = v }
|
|
140
|
+
o.on("--root=PATH", "Directory to scan (default: cwd)") { |v| opts[:root] = v }
|
|
141
|
+
o.on("-h", "--help") { puts o; exit 0 }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def cmd_init(argv)
|
|
146
|
+
opts = { force: false, output: "env.spec", root: Dir.pwd }
|
|
147
|
+
init_parser(opts).parse(argv)
|
|
148
|
+
|
|
149
|
+
result = EnvSpec::Init.run(root: opts[:root], force: opts[:force], output: opts[:output])
|
|
150
|
+
|
|
151
|
+
unless result[:ok]
|
|
152
|
+
if result[:reason] == :exists
|
|
153
|
+
warn "envspec: #{opts[:output]} already exists (use --force to overwrite)"
|
|
154
|
+
return 1
|
|
155
|
+
end
|
|
156
|
+
warn "envspec: init failed: #{result[:reason]}"
|
|
157
|
+
return 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
puts "✓ Scanned in #{format('%.2fs', result[:elapsed])}"
|
|
161
|
+
puts "✓ Found #{result[:keys]} env vars across #{result[:files]} files"
|
|
162
|
+
puts "✓ Wrote #{result[:path]}"
|
|
163
|
+
puts ""
|
|
164
|
+
puts "Next steps:"
|
|
165
|
+
puts " 1. Edit #{opts[:output]} — review heuristic suggestions, classify secrets"
|
|
166
|
+
puts " 2. Run `envspec lint #{opts[:output]}` to validate"
|
|
167
|
+
puts " 3. Run `envspec check #{opts[:output]}` to test against your current ENV"
|
|
168
|
+
0
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# ── helpers ───────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def die_unless_file(file)
|
|
174
|
+
return if File.exist?(file)
|
|
175
|
+
warn "envspec: file not found: #{file}"
|
|
176
|
+
exit 1
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
data/lib/envspec/version.rb
CHANGED
data/lib/envspec.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: envspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leadfy
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- README.md
|
|
68
68
|
- exe/envspec
|
|
69
69
|
- lib/envspec.rb
|
|
70
|
+
- lib/envspec/cli.rb
|
|
70
71
|
- lib/envspec/entry.rb
|
|
71
72
|
- lib/envspec/errors.rb
|
|
72
73
|
- lib/envspec/heuristics.rb
|