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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28f13d06ae0882152222605d7b631f76f181c63fd91544ace2e53c33cc718ffa
4
- data.tar.gz: 616d302174e177c0cce5864a4a12533710ea4823c528496a0e88a9f1466462be
3
+ metadata.gz: a8d59afdbad3765ce2c0516b2cfd710b877f3da5b123ff52a8c2423259ea1825
4
+ data.tar.gz: ddece928a9d0b0d20d3a096f7d6a51d36422ebd9300d86c2b80d1102b45734b5
5
5
  SHA512:
6
- metadata.gz: 9b57dba76d1e5862c97548c1c0b040e1147ce3abac04e3034d9ae313f46c27fbbdb9a3105d776a7da2d5d066412a05373dfc19c769698222c25406f9d9b651d0
7
- data.tar.gz: a19532006f8fc6822fa3a8b4ed7a17f4ba6485547233819e76c762df9a22b79b09a6ab52adf36401d760bc0ae0265939ad9e848d967ccb0f349b0572ca0db65d
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)
@@ -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
@@ -1,3 +1,3 @@
1
1
  class EnvSpec
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.1.1".freeze
3
3
  end
data/lib/envspec.rb CHANGED
@@ -4,3 +4,4 @@ require_relative "envspec/entry"
4
4
  require_relative "envspec/parser"
5
5
  require_relative "envspec/validator"
6
6
  require_relative "envspec/init"
7
+ require_relative "envspec/cli"
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.0
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