envspec 0.1.0 → 0.1.2
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 +14 -0
- data/exe/envspec +1 -104
- data/lib/envspec/cli/check.rb +33 -0
- data/lib/envspec/cli/command.rb +97 -0
- data/lib/envspec/cli/init.rb +46 -0
- data/lib/envspec/cli/lint.rb +19 -0
- data/lib/envspec/cli.rb +80 -0
- data/lib/envspec/version.rb +1 -1
- data/lib/envspec.rb +1 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 876d54d300e49212e28fe8ff57ed3b05fc96ef168a2b668ac71ba6f2c9421945
|
|
4
|
+
data.tar.gz: d7d25d84e2fc60d898070bd13ce0cfebe891612035975695e16c31b10eedd319
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a4582455ef7d02a5d190d8bb16173ddda9e67aacbe701fe29b16879df6e404dad3d6106ff792913527677253511c1c0428e3cfb20dccc41b348f36125a06234
|
|
7
|
+
data.tar.gz: af136637aa5cd6f4e35129dbfe787ce59ec245901b5980b0a6b4df06a8790885ce1e59426ace7ececb9f095bca1d94aafc00cf85b7386a07ccae2054830bfea3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 — 2026-05-04
|
|
4
|
+
|
|
5
|
+
- Split CLI into one file per subcommand under `lib/envspec/cli/`:
|
|
6
|
+
`command.rb` (base class), `lint.rb`, `check.rb`, `init.rb`. Adding a new
|
|
7
|
+
subcommand is now: write a class inheriting from `EnvSpec::CLI::Command`,
|
|
8
|
+
register it in `EnvSpec::CLI::COMMANDS`.
|
|
9
|
+
- Aligned formatting in global help.
|
|
10
|
+
|
|
11
|
+
## 0.1.1 — 2026-05-04
|
|
12
|
+
|
|
13
|
+
- Refactor CLI into `EnvSpec::CLI` with proper subcommand structure (per-subcommand `OptionParser` with banners, options, and `-h` help). Still pure stdlib.
|
|
14
|
+
- `envspec help <command>` and `envspec <command> -h` now show subcommand-specific usage.
|
|
15
|
+
- Better exit codes (2 for usage errors, 1 for parse/validation errors, 0 for success).
|
|
16
|
+
|
|
3
17
|
## 0.1.0 — 2026-05-04
|
|
4
18
|
|
|
5
19
|
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)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require_relative "command"
|
|
2
|
+
|
|
3
|
+
class EnvSpec
|
|
4
|
+
class CLI
|
|
5
|
+
class Check < Command
|
|
6
|
+
name "check"
|
|
7
|
+
summary "Check current ENV against an env.spec"
|
|
8
|
+
banner "Usage: envspec check <file> [options]"
|
|
9
|
+
description <<~DESC
|
|
10
|
+
Validates the current process ENV against an env.spec file.
|
|
11
|
+
Checks presence of required keys and that values match declared types.
|
|
12
|
+
DESC
|
|
13
|
+
|
|
14
|
+
def default_opts
|
|
15
|
+
{ env: "*", strict: false }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure(o, opts)
|
|
19
|
+
o.on("--env=NAME", "Env scope to validate against (default: shared/*)") { |v| opts[:env] = v }
|
|
20
|
+
o.on("--strict", "Treat unknown env vars as problems") { opts[:strict] = true }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(rest, opts)
|
|
24
|
+
file = require_file(rest.shift)
|
|
25
|
+
spec = EnvSpec.parse_file(file)
|
|
26
|
+
spec.validate!(ENV.to_h, env: opts[:env], strict: opts[:strict])
|
|
27
|
+
label = opts[:env] == "*" ? "shared" : opts[:env]
|
|
28
|
+
puts "✓ ENV satisfies #{file} (env: #{label})"
|
|
29
|
+
0
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
class EnvSpec
|
|
4
|
+
class CLI
|
|
5
|
+
# Base class for a single CLI subcommand.
|
|
6
|
+
#
|
|
7
|
+
# Subclasses declare:
|
|
8
|
+
# - name (string, e.g. "lint")
|
|
9
|
+
# - summary (one-line description for global help)
|
|
10
|
+
# - configure(parser, opts) — define options on the OptionParser
|
|
11
|
+
# - call(argv, opts) — execute; return exit code
|
|
12
|
+
class Command
|
|
13
|
+
class << self
|
|
14
|
+
def name(value = nil)
|
|
15
|
+
@name = value if value
|
|
16
|
+
@name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def summary(value = nil)
|
|
20
|
+
@summary = value if value
|
|
21
|
+
@summary
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def banner(value = nil)
|
|
25
|
+
@banner = value if value
|
|
26
|
+
@banner
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def description(value = nil)
|
|
30
|
+
@description = value if value
|
|
31
|
+
@description
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@opts = default_opts
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def default_opts
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parser
|
|
44
|
+
@parser ||= build_parser
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_parser
|
|
48
|
+
OptionParser.new do |o|
|
|
49
|
+
o.banner = self.class.banner || "Usage: envspec #{self.class.name}"
|
|
50
|
+
if self.class.description
|
|
51
|
+
o.separator ""
|
|
52
|
+
self.class.description.each_line { |ln| o.separator ln.chomp }
|
|
53
|
+
end
|
|
54
|
+
o.separator ""
|
|
55
|
+
o.separator "Options:"
|
|
56
|
+
configure(o, @opts)
|
|
57
|
+
o.on("-h", "--help", "Show help for this command") { puts o; exit 0 }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def configure(_parser, _opts)
|
|
62
|
+
# subclass override
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def help
|
|
66
|
+
parser.help
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Subclasses override `call(argv, opts)`. The dispatcher invokes `run(argv)`.
|
|
70
|
+
def run(argv)
|
|
71
|
+
rest = parser.parse(argv)
|
|
72
|
+
call(rest, @opts)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def call(_rest, _opts)
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
protected
|
|
80
|
+
|
|
81
|
+
def die(msg, code = 1)
|
|
82
|
+
warn "envspec: #{msg}"
|
|
83
|
+
code
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def require_file(file)
|
|
87
|
+
return file if file && File.exist?(file)
|
|
88
|
+
if file.nil?
|
|
89
|
+
warn parser.help
|
|
90
|
+
exit 2
|
|
91
|
+
end
|
|
92
|
+
warn "envspec: file not found: #{file}"
|
|
93
|
+
exit 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative "command"
|
|
2
|
+
|
|
3
|
+
class EnvSpec
|
|
4
|
+
class CLI
|
|
5
|
+
class Init < Command
|
|
6
|
+
name "init"
|
|
7
|
+
summary "Scan repo for env vars and generate env.spec"
|
|
8
|
+
banner "Usage: envspec init [options]"
|
|
9
|
+
description <<~DESC
|
|
10
|
+
Scans the current directory tree for env var usages across
|
|
11
|
+
Ruby/Python/JS/Go/Shell sources and writes a starter env.spec.
|
|
12
|
+
DESC
|
|
13
|
+
|
|
14
|
+
def default_opts
|
|
15
|
+
{ force: false, output: "env.spec", root: Dir.pwd }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure(o, opts)
|
|
19
|
+
o.on("--force", "Overwrite an existing env.spec") { opts[:force] = true }
|
|
20
|
+
o.on("--output=PATH", "Output file (default: env.spec)") { |v| opts[:output] = v }
|
|
21
|
+
o.on("--root=PATH", "Directory to scan (default: cwd)") { |v| opts[:root] = v }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(_rest, opts)
|
|
25
|
+
result = EnvSpec::Init.run(root: opts[:root], force: opts[:force], output: opts[:output])
|
|
26
|
+
|
|
27
|
+
unless result[:ok]
|
|
28
|
+
if result[:reason] == :exists
|
|
29
|
+
return die("#{opts[:output]} already exists (use --force to overwrite)", 1)
|
|
30
|
+
end
|
|
31
|
+
return die("init failed: #{result[:reason]}", 1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
puts "✓ Scanned in #{format('%.2fs', result[:elapsed])}"
|
|
35
|
+
puts "✓ Found #{result[:keys]} env vars across #{result[:files]} files"
|
|
36
|
+
puts "✓ Wrote #{result[:path]}"
|
|
37
|
+
puts ""
|
|
38
|
+
puts "Next steps:"
|
|
39
|
+
puts " 1. Edit #{opts[:output]} — review heuristic suggestions, classify secrets"
|
|
40
|
+
puts " 2. Run `envspec lint #{opts[:output]}` to validate"
|
|
41
|
+
puts " 3. Run `envspec check #{opts[:output]}` to test against your current ENV"
|
|
42
|
+
0
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative "command"
|
|
2
|
+
|
|
3
|
+
class EnvSpec
|
|
4
|
+
class CLI
|
|
5
|
+
class Lint < Command
|
|
6
|
+
name "lint"
|
|
7
|
+
summary "Parse and validate syntax of an env.spec file"
|
|
8
|
+
banner "Usage: envspec lint <file>"
|
|
9
|
+
description "Parses an env.spec file and reports any syntax errors."
|
|
10
|
+
|
|
11
|
+
def call(rest, _opts)
|
|
12
|
+
file = require_file(rest.shift)
|
|
13
|
+
EnvSpec.parse_file(file)
|
|
14
|
+
puts "✓ #{file} is valid"
|
|
15
|
+
0
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/envspec/cli.rb
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require_relative "cli/command"
|
|
2
|
+
require_relative "cli/lint"
|
|
3
|
+
require_relative "cli/check"
|
|
4
|
+
require_relative "cli/init"
|
|
5
|
+
|
|
6
|
+
class EnvSpec
|
|
7
|
+
# Dispatcher for `envspec` subcommands.
|
|
8
|
+
#
|
|
9
|
+
# Each subcommand is a class under EnvSpec::CLI inheriting from
|
|
10
|
+
# EnvSpec::CLI::Command. Register new commands by adding them to COMMANDS
|
|
11
|
+
# below and creating a file under lib/envspec/cli/.
|
|
12
|
+
class CLI
|
|
13
|
+
COMMANDS = {
|
|
14
|
+
"lint" => Lint,
|
|
15
|
+
"check" => Check,
|
|
16
|
+
"init" => Init,
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def self.run(argv)
|
|
20
|
+
new.run(argv)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(argv)
|
|
24
|
+
argv = argv.dup
|
|
25
|
+
cmd = argv.shift
|
|
26
|
+
|
|
27
|
+
case cmd
|
|
28
|
+
when nil, "help", "-h", "--help"
|
|
29
|
+
print_help(argv.first)
|
|
30
|
+
return 0
|
|
31
|
+
when "version", "-v", "--version"
|
|
32
|
+
puts EnvSpec::VERSION
|
|
33
|
+
return 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
klass = COMMANDS[cmd]
|
|
37
|
+
unless klass
|
|
38
|
+
warn "envspec: unknown command '#{cmd}'"
|
|
39
|
+
warn ""
|
|
40
|
+
print_help
|
|
41
|
+
return 2
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
klass.new.run(argv)
|
|
45
|
+
rescue OptionParser::ParseError => e
|
|
46
|
+
warn "envspec: #{e.message}"
|
|
47
|
+
2
|
|
48
|
+
rescue EnvSpec::ParseError, EnvSpec::ValidationError => e
|
|
49
|
+
warn "envspec: #{e.message}"
|
|
50
|
+
1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def print_help(topic = nil)
|
|
56
|
+
if topic && (klass = COMMANDS[topic])
|
|
57
|
+
puts klass.new.help
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
puts global_help
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def global_help
|
|
64
|
+
rows = COMMANDS.map { |n, k| [n, k.summary] }
|
|
65
|
+
rows << ["help", "Show help for a command"]
|
|
66
|
+
rows << ["version", "Show version"]
|
|
67
|
+
width = rows.map { |r| r[0].length }.max
|
|
68
|
+
|
|
69
|
+
lines = []
|
|
70
|
+
lines << "Usage: envspec <command> [options]"
|
|
71
|
+
lines << ""
|
|
72
|
+
lines << "Commands:"
|
|
73
|
+
rows.each { |name, summary| lines << format(" %-#{width}s %s", name, summary) }
|
|
74
|
+
lines << ""
|
|
75
|
+
lines << "Run `envspec help <command>` (or `envspec <command> -h`) for command-specific options."
|
|
76
|
+
lines << "Docs: https://github.com/repleadfy/envspec"
|
|
77
|
+
lines.join("\n")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leadfy
|
|
@@ -67,6 +67,11 @@ files:
|
|
|
67
67
|
- README.md
|
|
68
68
|
- exe/envspec
|
|
69
69
|
- lib/envspec.rb
|
|
70
|
+
- lib/envspec/cli.rb
|
|
71
|
+
- lib/envspec/cli/check.rb
|
|
72
|
+
- lib/envspec/cli/command.rb
|
|
73
|
+
- lib/envspec/cli/init.rb
|
|
74
|
+
- lib/envspec/cli/lint.rb
|
|
70
75
|
- lib/envspec/entry.rb
|
|
71
76
|
- lib/envspec/errors.rb
|
|
72
77
|
- lib/envspec/heuristics.rb
|