envspec 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8d59afdbad3765ce2c0516b2cfd710b877f3da5b123ff52a8c2423259ea1825
4
- data.tar.gz: ddece928a9d0b0d20d3a096f7d6a51d36422ebd9300d86c2b80d1102b45734b5
3
+ metadata.gz: 876d54d300e49212e28fe8ff57ed3b05fc96ef168a2b668ac71ba6f2c9421945
4
+ data.tar.gz: d7d25d84e2fc60d898070bd13ce0cfebe891612035975695e16c31b10eedd319
5
5
  SHA512:
6
- metadata.gz: d0ec195a2b9eb390aaad8fc547371b3ed8251f599c8916e535fbcac5448cec6dd85ca28a86e537b01484cbd52766315eff2235d003f21c73ec111ba6acf33750
7
- data.tar.gz: 3e77c5274e45e61cde1f1e8792a934efe7b3aaf3914ea7da4e1715750e9c1737a93c26f8899a4ac54b88ffa2fd97cb4b3428b5f8d6e665d76c1413dee8a153c0
6
+ metadata.gz: 7a4582455ef7d02a5d190d8bb16173ddda9e67aacbe701fe29b16879df6e404dad3d6106ff792913527677253511c1c0428e3cfb20dccc41b348f36125a06234
7
+ data.tar.gz: af136637aa5cd6f4e35129dbfe787ce59ec245901b5980b0a6b4df06a8790885ce1e59426ace7ececb9f095bca1d94aafc00cf85b7386a07ccae2054830bfea3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## 0.1.1 — 2026-05-04
4
12
 
5
13
  - Refactor CLI into `EnvSpec::CLI` with proper subcommand structure (per-subcommand `OptionParser` with banners, options, and `-h` help). Still pure stdlib.
@@ -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 CHANGED
@@ -1,8 +1,20 @@
1
- require "optparse"
1
+ require_relative "cli/command"
2
+ require_relative "cli/lint"
3
+ require_relative "cli/check"
4
+ require_relative "cli/init"
2
5
 
3
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/.
4
12
  class CLI
5
- SUBCOMMANDS = %w[lint check init help version].freeze
13
+ COMMANDS = {
14
+ "lint" => Lint,
15
+ "check" => Check,
16
+ "init" => Init,
17
+ }.freeze
6
18
 
7
19
  def self.run(argv)
8
20
  new.run(argv)
@@ -15,22 +27,21 @@ class EnvSpec
15
27
  case cmd
16
28
  when nil, "help", "-h", "--help"
17
29
  print_help(argv.first)
18
- 0
30
+ return 0
19
31
  when "version", "-v", "--version"
20
32
  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
33
+ return 0
34
+ end
35
+
36
+ klass = COMMANDS[cmd]
37
+ unless klass
29
38
  warn "envspec: unknown command '#{cmd}'"
30
39
  warn ""
31
40
  print_help
32
- 2
41
+ return 2
33
42
  end
43
+
44
+ klass.new.run(argv)
34
45
  rescue OptionParser::ParseError => e
35
46
  warn "envspec: #{e.message}"
36
47
  2
@@ -41,139 +52,29 @@ class EnvSpec
41
52
 
42
53
  private
43
54
 
44
- # ── help ──────────────────────────────────────────────────────────
45
-
46
55
  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
56
+ if topic && (klass = COMMANDS[topic])
57
+ puts klass.new.help
58
+ return
52
59
  end
60
+ puts global_help
53
61
  end
54
62
 
55
63
  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
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")
177
78
  end
178
79
  end
179
80
  end
@@ -1,3 +1,3 @@
1
1
  class EnvSpec
2
- VERSION = "0.1.1".freeze
2
+ VERSION = "0.1.2".freeze
3
3
  end
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.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leadfy
@@ -68,6 +68,10 @@ files:
68
68
  - exe/envspec
69
69
  - lib/envspec.rb
70
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
71
75
  - lib/envspec/entry.rb
72
76
  - lib/envspec/errors.rb
73
77
  - lib/envspec/heuristics.rb