Module: Kettle::Jem::CLI
- Defined in:
- lib/kettle/jem/cli.rb
Constant Summary collapse
- USAGE =
<<~USAGE Usage: kettle-jem [PROJECT_ROOT] [--accept-config] [--bootstrap-mode] [--changelog|--no-changelog] [--quiet|--verbose] kettle-jem setup [PROJECT_ROOT] [--accept-config] [--bootstrap-mode] [--changelog|--no-changelog] [--quiet|--verbose] kettle-jem prepare [PROJECT_ROOT] [--json|--events[=TYPE,...]] [--report PATH] [--accept|--force] [--changelog|--no-changelog] [--quiet|--verbose] kettle-jem plan [PROJECT_ROOT] [--json|--events[=TYPE,...]] [--report PATH] [--accept|--force|--interactive] [--failure-mode MODE] [--prompt-answer ID=ACTION] kettle-jem apply [PROJECT_ROOT] [--json|--events[=TYPE,...]] [--report PATH] [--accept|--force|--interactive] [--failure-mode MODE] [--prompt-answer ID=ACTION] [--changelog|--no-changelog] kettle-jem template [PROJECT_ROOT] [--json|--events[=TYPE,...]] [--report PATH] [--accept|--force|--interactive] [--failure-mode MODE] [--prompt-answer ID=ACTION] [--changelog|--no-changelog] kettle-jem install [PROJECT_ROOT] [--json|--events[=TYPE,...]] [--report PATH] [--accept|--force|--interactive] [--failure-mode MODE] [--prompt-answer ID=ACTION] [--changelog|--no-changelog] kettle-jem manifest [PROJECT_ROOT] [--json] kettle-jem selftest [PROJECT_ROOT] [--json] [--report PATH] [--destination PATH] [--template-root PATH] [--selftest-output PATH] kettle-jem version USAGE
Class Method Summary collapse
- .canonical_path(path) ⇒ Object
- .command_allowed?(command) ⇒ Boolean
- .debug_enabled?(env) ⇒ Boolean
- .ensure_not_running_from_own_context!(command, project_root) ⇒ Object
- .env_true?(value) ⇒ Boolean
- .env_value(env, key) ⇒ Object
- .execute(command, project_root:, env:, options:) ⇒ Object
- .normalize_command(argv) ⇒ Object
- .parse_options(args) ⇒ Object
- .print_debug_snapshot(command, project_root:, env:, err:) ⇒ Object
- .print_help(out) ⇒ Object
- .print_result(command, result, options:, out:) ⇒ Object
- .print_version(out) ⇒ Object
- .run(argv = ARGV, env: ENV, out: $stdout, err: $stderr) ⇒ Object
- .scoped_template_run?(run_options) ⇒ Boolean
- .write_report(path, result) ⇒ Object
Class Method Details
.canonical_path(path) ⇒ Object
263 264 265 266 267 |
# File 'lib/kettle/jem/cli.rb', line 263 def canonical_path(path) File.realpath(path) rescue Errno::ENOENT File.(path) end |
.command_allowed?(command) ⇒ Boolean
66 67 68 |
# File 'lib/kettle/jem/cli.rb', line 66 def command_allowed?(command) %w[setup prepare plan apply template install manifest selftest help version].include?(command) end |
.debug_enabled?(env) ⇒ Boolean
320 321 322 |
# File 'lib/kettle/jem/cli.rb', line 320 def debug_enabled?(env) %w[DEBUG KETTLE_JEM_DEBUG KETTLE_DEV_DEBUG].any? { |key| env_true?(env_value(env, key)) } end |
.ensure_not_running_from_own_context!(command, project_root) ⇒ Object
250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/kettle/jem/cli.rb', line 250 def ensure_not_running_from_own_context!(command, project_root) return if %w[help version].include?(command) current_root = canonical_path(Dir.pwd) own_root = canonical_path(File.("../../..", __dir__)) target_root = canonical_path(project_root) return unless current_root == own_root && target_root != own_root raise ArgumentError, "Refusing to run kettle-jem from its own project root against #{project_root}; " \ "run from the destination repository so its environment is loaded, or target kettle-jem itself." end |
.env_true?(value) ⇒ Boolean
324 325 326 |
# File 'lib/kettle/jem/cli.rb', line 324 def env_true?(value) /\A(?:true|t|yes|y|on|1)\z/i.match?(value.to_s.strip) end |
.env_value(env, key) ⇒ Object
328 329 330 331 332 |
# File 'lib/kettle/jem/cli.rb', line 328 def env_value(env, key) env.fetch(key, nil) rescue KeyError nil end |
.execute(command, project_root:, env:, options:) ⇒ Object
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/kettle/jem/cli.rb', line 197 def execute(command, project_root:, env:, options:) = .fetch(:run_options) if [:events] [:event_stream] = Kettle::Jem.event_stream( .fetch(:event_io), types: [:event_types] ) end result = case command when "setup" Kettle::Jem.setup_project(project_root, env: env, run_options: ) when "prepare" Kettle::Jem::Tasks::PrepareTask.run(project_root: project_root, env: env, run_options: ) when "plan" Kettle::Jem.plan_project(project_root, env: env, run_options: ) when "apply" Kettle::Jem.apply_project(project_root, env: env, run_options: ) when "template" if scoped_template_run?() Kettle::Jem::Tasks::TemplateTask.run(project_root: project_root, env: env, run_options: ) else Kettle::Jem::Tasks::InstallTask.run(project_root: project_root, env: env, run_options: ) end when "install" Kettle::Jem::Tasks::InstallTask.run(project_root: project_root, env: env, run_options: ) when "manifest" Kettle::Jem.template_manifest(project_root: project_root) when "selftest" Kettle::Jem::Tasks::SelfTestTask.run( project_root: project_root, destination_root: [:destination_root] || project_root, template_root: [:template_root], output_root: [:selftest_output_root], min_divergence_threshold: [:min_divergence_threshold] ) else raise ArgumentError, "Unsupported kettle-jem command #{command.inspect}" end return result unless %w[setup prepare apply template install].include?(command) Kettle::Jem::MaintenanceChangelog.record_template_run( project_root: project_root, report: result, run_options: , label: (command == "prepare") ? "Prepare project for kettle-jem templates" : "Apply kettle-jem templates" ) end |
.normalize_command(argv) ⇒ Object
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/kettle/jem/cli.rb', line 50 def normalize_command(argv) args = Array(argv).dup command = args.shift return ["help", []] if command == "help" || command == "--help" || command == "-h" return ["setup", []] if command.nil? unless command_allowed?(command) args.unshift(command) command = "setup" end command = "version" if command == "version" || command == "--version" || command == "-v" raise ArgumentError, "Unsupported kettle-jem command #{command.inspect}" unless command_allowed?(command) [command, args] end |
.parse_options(args) ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/kettle/jem/cli.rb', line 70 def (args) = { json: false, events: false, run_options: {} } parser = OptionParser.new do |opts| opts. = USAGE opts.on("--json", "Print the full machine-readable result as JSON.") { [:json] = true } opts.on("--events[=TYPES]", "Print newline-delimited JSON progress events. Optional comma-separated TYPES filter.") do |value| [:events] = true [:event_types] = value if value end opts.on("--report PATH", "Write the full machine-readable result to PATH as JSON.") do |path| [:report_path] = path end opts.on("--accept", "Use non-interactive default decisions.") { [:run_options][:accept] = true } opts.on("--force", "Alias for --accept.") { [:run_options][:force] = true } opts.on("--interactive", "Use interactive decision mode when supported.") do [:run_options][:interactive] = true end opts.on("--quiet", "Suppress normal text output.") do [:run_options][:quiet] = true end opts.on("--verbose", "Request verbose diagnostics where supported.") do [:run_options][:verbose] = true end opts.on("--failure-mode MODE", "Set the template failure mode.") do |mode| [:run_options][:failure_mode] = mode end opts.on("--prompt-answer ID=ACTION", "Answer an interactive decision prompt.") do |value| id, action = value.to_s.split("=", 2) raise OptionParser::InvalidArgument, "--prompt-answer must use ID=ACTION" if id.to_s.empty? || action.to_s.empty? ([:run_options][:prompt_answers] ||= {})[id] = action [:run_options][:interactive] = true end opts.on("--allowed VALUE", "Set the env-file change policy.") do |value| [:run_options][:allowed] = value end opts.on("--hook-templates VALUE", "Set hook template handling.") do |value| [:run_options][:hook_templates] = value end opts.on("--hook_templates VALUE", "Alias for --hook-templates.") do |value| [:run_options][:hook_templates] = value end opts.on("--git-drivers VALUE", "Set Git diff/merge driver setup handling.") do |value| [:run_options][:git_drivers] = value end opts.on("--git_drivers VALUE", "Alias for --git-drivers.") do |value| [:run_options][:git_drivers] = value end opts.on("--dry-run", "Plan supported post-template actions without applying them.") do [:run_options][:dry_run] = true end opts.on("--check", "Verify supported post-template setup without changing files.") do [:run_options][:git_drivers] = "check" end opts.on("--undo", "Undo supported post-template setup.") do [:run_options][:git_drivers] = "undo" end opts.on("--only PATHS", "Restrict templating to comma-separated paths or patterns.") do |value| ([:run_options][:only] ||= []) << value end opts.on("--include PATHS", "Include comma-separated paths or patterns.") do |value| ([:run_options][:include] ||= []) << value end opts.on("--skip-commit", "Skip bootstrap commit behavior.") do [:run_options][:skip_commit] = true end opts.on("--skip-drift-check", "Skip post-apply duplicate drift checking.") do [:run_options][:skip_drift_check] = true end opts.on("--skip-duplicate-drift", "Alias for --skip-drift-check.") do [:run_options][:skip_drift_check] = true end opts.on("--skip-rubocop-gradual", "Skip post-template RuboCop Gradual autocorrect.") do [:run_options][:skip_rubocop_gradual] = true end opts.on("--[no-]changelog", "Add a Changed entry summarizing actual template changes (default).") do |value| [:run_options][:skip_changelog] = !value end opts.on("--skip-binstubs", "Skip post-template curated Bundler binstub generation.") do [:run_options][:skip_binstubs] = true end opts.on("--checksums VALUE", "Set checksum skip mode: template, dest, ignore-template, ignore-dest, or off.") do |value| [:run_options][:checksums] = value end opts.on("--ignore-checksums", "Alias for --checksums off.") do [:run_options][:checksums] = "off" end opts.on("--accept-config", "Accept first-run template config bootstrap.") do [:run_options][:accept_config] = true end opts.on("--bootstrap-mode", "Force first-run bootstrap mode.") do [:run_options][:bootstrap_mode] = true end opts.on("--template-profile PROFILE", "Use a packaged template profile.") do |value| [:run_options][:template_profile] = value end opts.on("--shimmed-gem GEM", "Runtime gem required by the shim profile.") do |value| [:run_options][:shimmed_gem] = value end opts.on("--shimmed-require REQUIRE", "Require path loaded by the shim profile.") do |value| [:run_options][:shimmed_require] = value end opts.on("--destination PATH", "Selftest destination root.") do |path| [:destination_root] = path end opts.on("--template-root PATH", "Selftest template root.") do |path| [:template_root] = path end opts.on("--selftest-output PATH", "Selftest output root.") do |path| [:selftest_output_root] = path end opts.on("--min-divergence-threshold PERCENT", "Fail selftest when divergence exceeds PERCENT.") do |value| [:min_divergence_threshold] = Float(value) end end remaining = parser.parse(args) raise ArgumentError, "Expected at most one PROJECT_ROOT" if remaining.length > 1 raise OptionParser::InvalidOption, "--events cannot be combined with --json" if [:events] && [:json] [:project_root] = remaining.first end |
.print_debug_snapshot(command, project_root:, env:, err:) ⇒ Object
311 312 313 314 315 316 317 318 |
# File 'lib/kettle/jem/cli.rb', line 311 def print_debug_snapshot(command, project_root:, env:, err:) err.puts("[kettle-jem] DEBUG: early environment snapshot") err.puts(" command=#{command.inspect}") err.puts(" project_root=#{project_root.inspect}") %w[DEBUG KETTLE_JEM_DEBUG KETTLE_DEV_DEBUG KETTLE_DEV_DEV BUNDLE_GEMFILE BUNDLE_PATH GEM_HOME GEM_PATH RUBYOPT RUBYLIB PWD].each do |key| err.puts(" #{key}=#{env_value(env, key).inspect}") end end |
.print_help(out) ⇒ Object
334 335 336 337 |
# File 'lib/kettle/jem/cli.rb', line 334 def print_help(out) out.puts(USAGE) 0 end |
.print_result(command, result, options:, out:) ⇒ Object
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/kettle/jem/cli.rb', line 269 def print_result(command, result, options:, out:) return if [:events] return if .fetch(:run_options, {})[:quiet] && ![:json] if [:json] out.puts(JSON.pretty_generate(result)) return end case command when "setup" out.puts("setup: #{result.fetch(:setup_status)}") result.fetch(:changed_files, []).each { |path| out.puts(" #{path}") } result.fetch(:diagnostics, []).each do |diagnostic| = diagnostic.is_a?(Hash) ? diagnostic[:message] || diagnostic["message"] : diagnostic out.puts(" #{}") unless .to_s.empty? end when "manifest" entries = result.fetch(:entries, []) out.puts("template manifest: #{entries.length} entr#{(entries.length == 1) ? "y" : "ies"}") when "selftest" comparison = result.fetch(:comparison, {}) divergent = comparison.fetch(:changed, []).size + comparison.fetch(:added, []).size + comparison.fetch(:removed, []).size out.puts("selftest: #{divergent} divergent file#{"s" unless divergent == 1}") out.puts(" report: #{result.fetch(:report_path)}") if result[:report_path] else changed_files = result.fetch(:changed_files, []) out.puts("#{result.fetch(:mode)}: #{changed_files.length} changed file#{"s" unless changed_files.length == 1}") changed_files.each { |path| out.puts(" #{path}") } Array(result[:warnings]).map(&:to_s).reject(&:empty?).uniq.each do |warning| out.puts(" warning: #{warning}") end end end |
.print_version(out) ⇒ Object
339 340 341 342 |
# File 'lib/kettle/jem/cli.rb', line 339 def print_version(out) out.puts(Kettle::Jem::Version::VERSION) 0 end |
.run(argv = ARGV, env: ENV, out: $stdout, err: $stderr) ⇒ Object
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/kettle/jem/cli.rb', line 26 def run(argv = ARGV, env: ENV, out: $stdout, err: $stderr) command, args = normalize_command(argv) return print_help(out) if command == "help" return print_version(out) if command == "version" = (args) project_root = File.(.fetch(:project_root) || Dir.pwd) ensure_not_running_from_own_context!(command, project_root) print_debug_snapshot(command, project_root: project_root, env: env, err: err) if debug_enabled?(env) [:event_io] = out result = execute(command, project_root: project_root, env: env, options: ) write_report([:report_path], result) if [:report_path] print_result(command, result, options: , out: out) 0 rescue OptionParser::ParseError, ArgumentError => error err.puts(error.) err.puts(USAGE) 2 rescue => error err.puts("#{error.class}: #{error.}") err.puts(error.backtrace.join("\n")) if debug_enabled?(env) 1 end |
.scoped_template_run?(run_options) ⇒ Boolean
245 246 247 248 |
# File 'lib/kettle/jem/cli.rb', line 245 def scoped_template_run?() = .to_h .key?(:only) || .key?(:include) end |
.write_report(path, result) ⇒ Object
306 307 308 309 |
# File 'lib/kettle/jem/cli.rb', line 306 def write_report(path, result) FileUtils.mkdir_p(File.dirname(File.(path))) File.write(path, "#{JSON.pretty_generate(result)}\n") end |