docktor_rails 0.1.0

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.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_check"
4
+
5
+ module DocktorRails
6
+ module Checks
7
+ class ShellScriptsCrlf < BaseCheck
8
+ DEFAULT_IGNORES = [
9
+ "/.git/",
10
+ "/node_modules/",
11
+ "/vendor/",
12
+ "/tmp/",
13
+ "/log/"
14
+ ].freeze
15
+
16
+ def id
17
+ "fs.shell_crlf"
18
+ end
19
+
20
+ def run(ctx)
21
+ root = ctx.fetch(:root)
22
+ offenders = []
23
+
24
+ Dir.glob(File.join(root, "**", "*.sh"), File::FNM_DOTMATCH).each do |path|
25
+ next unless File.file?(path)
26
+ next if ignored?(path)
27
+
28
+ offenders << rel(path, root) if File.binread(path).include?("\r\n")
29
+ end
30
+
31
+ if offenders.empty?
32
+ pass("No CRLF line endings in .sh files")
33
+ else
34
+ fail(
35
+ "CRLF line endings found in shell scripts",
36
+ files: offenders.sort,
37
+ hint: "Convert to LF (e.g. with git) and enforce via .gitattributes: *.sh text eol=lf"
38
+ )
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def ignored?(path)
45
+ DEFAULT_IGNORES.any? { |seg| path.include?(seg) }
46
+ end
47
+
48
+ def rel(path, root)
49
+ path.delete_prefix(root + File::SEPARATOR)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ require_relative "version"
6
+ require_relative "preflight/runner"
7
+ require_relative "reporters/text_reporter"
8
+ require_relative "reporters/json_reporter"
9
+
10
+ module DocktorRails
11
+ class CLI < Thor
12
+ class_option :root, type: :string, desc: "Project root to scan (default: current directory)"
13
+ class_option :format, type: :string, default: "text", desc: "Output format: text|json"
14
+ class_option :output, type: :string, desc: "Write output to a file (useful with --format json)"
15
+ class_option :verbose, type: :boolean, default: false, desc: "Show passing checks"
16
+ class_option :quiet, type: :boolean, default: false, desc: "Only show errors/warnings and the summary"
17
+ class_option :no_color, type: :boolean, default: false, desc: "Disable colored output"
18
+
19
+ desc "diagnose", "Run preflight checks (read-only)"
20
+ def diagnose
21
+ report = Preflight::Runner.new(root: root_dir).run
22
+
23
+ io = output_io
24
+ render_report(report, io)
25
+
26
+ exit(report.fetch(:status) == :fail ? 1 : 0)
27
+ rescue StandardError => e
28
+ $stderr.puts("docktor_rails error: #{e.class}: #{e.message}")
29
+ exit(2)
30
+ ensure
31
+ io&.close if io && io != $stdout
32
+ end
33
+
34
+ desc "up", "Run preflight checks, then print the suggested docker compose commands"
35
+ def up
36
+ report = Preflight::Runner.new(root: root_dir).run
37
+
38
+ Reporters::TextReporter.new(
39
+ color: color_enabled?,
40
+ verbose: options[:verbose],
41
+ quiet: options[:quiet]
42
+ ).render(report)
43
+ exit(1) if report.fetch(:status) == :fail
44
+
45
+ puts
46
+ puts "Preflight passed. Next:"
47
+ puts " docker compose up"
48
+ rescue StandardError => e
49
+ $stderr.puts("docktor_rails error: #{e.class}: #{e.message}")
50
+ exit(2)
51
+ end
52
+
53
+ desc "version", "Print version"
54
+ def version
55
+ puts DocktorRails::VERSION
56
+ end
57
+
58
+ private
59
+
60
+ def output_io
61
+ path = options[:output]
62
+ return $stdout unless path
63
+
64
+ File.open(path, "w")
65
+ end
66
+
67
+ def render_report(report, io)
68
+ case options[:format]
69
+ when "json"
70
+ Reporters::JsonReporter.new(io: io).render(report, tool_version: DocktorRails::VERSION)
71
+ else
72
+ Reporters::TextReporter.new(
73
+ io: io,
74
+ color: color_enabled?,
75
+ verbose: options[:verbose],
76
+ quiet: options[:quiet]
77
+ ).render(report)
78
+ end
79
+ end
80
+
81
+ def color_enabled?
82
+ return false if options[:no_color]
83
+
84
+ $stdout.tty?
85
+ end
86
+
87
+ def root_dir
88
+ options[:root] ? File.expand_path(options[:root]) : Dir.pwd
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module DocktorRails
6
+ module Compose
7
+ CANDIDATES = ["compose.yml", "docker-compose.yml"].freeze
8
+
9
+ def self.find_file(root)
10
+ CANDIDATES.map { |f| File.join(root, f) }.find { |p| File.file?(p) }
11
+ end
12
+
13
+ def self.load_file(path)
14
+ data = YAML.safe_load(File.read(path), aliases: true)
15
+ data.is_a?(Hash) ? data : {}
16
+ rescue Psych::Exception => e
17
+ raise DocktorRails::Error, "Invalid compose YAML in #{File.basename(path)}: #{e.message}"
18
+ end
19
+
20
+ def self.services(doc)
21
+ doc.fetch("services", {}) || {}
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocktorRails
4
+ class Dotenv
5
+ def self.parse_file(path)
6
+ return {} unless File.file?(path)
7
+
8
+ parse(File.read(path))
9
+ end
10
+
11
+ def self.parse(content)
12
+ env = {}
13
+ content.each_line do |line|
14
+ line = line.strip
15
+ next if line.empty? || line.start_with?("#")
16
+
17
+ key, value = line.split("=", 2)
18
+ next if key.nil? || key.empty?
19
+
20
+ value = "" if value.nil?
21
+ value = value.strip
22
+ value = value[1..-2] if (value.start_with?("\"") && value.end_with?("\"")) || (value.start_with?("'") && value.end_with?("'"))
23
+ env[key] = value
24
+ end
25
+ env
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocktorRails
4
+ module Guidance
5
+ def self.hint_for(result, platform)
6
+ return result.hint if result.hint
7
+
8
+ id = result.id
9
+ status = result.status
10
+ return nil if status == :pass
11
+
12
+ case id
13
+ when "fs.shell_crlf", "compose.entrypoint_sanity"
14
+ crlf_hint(platform)
15
+ when "compose.healthchecks"
16
+ "Add compose healthchecks so readiness is deterministic across machines (then you can wait on health status instead of sleeps)."
17
+ when "env.rails_master_key"
18
+ "Provide config/master.key (local dev), or set RAILS_MASTER_KEY (CI/prod), or add it to .env.docker."
19
+ when "compose.env_file_present"
20
+ "Ensure each env_file referenced in compose exists (or remove the reference)."
21
+ when "compose.build_paths"
22
+ "Fix compose build paths: ensure build.context directories exist and build.dockerfile paths resolve correctly."
23
+ when "compose.entrypoint_exec"
24
+ entrypoint_exec_hint(platform)
25
+ when "compose.platform_risk"
26
+ "If teammates use different CPU architectures (arm64 vs amd64), consider multi-arch images or removing hard platform pins unless necessary."
27
+ else
28
+ nil
29
+ end
30
+ end
31
+
32
+ def self.crlf_hint(platform)
33
+ if platform&.windows?
34
+ "CRLF usually comes from Windows checkouts. Add .gitattributes: '*.sh text eol=lf', then run: git add --renormalize ."
35
+ else
36
+ "Convert scripts to LF and enforce via .gitattributes: '*.sh text eol=lf'"
37
+ end
38
+ end
39
+
40
+ def self.entrypoint_exec_hint(platform)
41
+ if platform&.windows?
42
+ "Executable bit/shebang checks are not meaningful on Windows hosts. Prefer running via WSL2 for Linux-like behavior."
43
+ else
44
+ "Ensure entrypoint scripts are executable (chmod +x) and start with a shebang (e.g. #!/usr/bin/env bash)."
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module DocktorRails
6
+ class Platform
7
+ attr_reader :os, :arch
8
+
9
+ def initialize(os:, arch:)
10
+ @os = os
11
+ @arch = arch
12
+ end
13
+
14
+ def self.detect
15
+ new(os: detect_os, arch: detect_arch)
16
+ end
17
+
18
+ def windows?
19
+ os == :windows
20
+ end
21
+
22
+ def macos?
23
+ os == :macos
24
+ end
25
+
26
+ def linux?
27
+ os == :linux
28
+ end
29
+
30
+ def arm64?
31
+ arch == :arm64
32
+ end
33
+
34
+ def amd64?
35
+ arch == :amd64
36
+ end
37
+
38
+ def to_h
39
+ { os: os.to_s, arch: arch.to_s }
40
+ end
41
+
42
+ def label
43
+ "#{os} #{arch}"
44
+ end
45
+
46
+ def self.detect_os
47
+ p = RUBY_PLATFORM
48
+ return :windows if p.match?(/mswin|mingw|cygwin/i)
49
+ return :macos if p.match?(/darwin/i)
50
+ return :linux if p.match?(/linux/i)
51
+
52
+ :unknown
53
+ end
54
+
55
+ def self.detect_arch
56
+ cpu = RbConfig::CONFIG["host_cpu"].to_s.downcase
57
+ return :arm64 if cpu.match?(/arm64|aarch64/)
58
+ return :amd64 if cpu.match?(/x86_64|amd64/)
59
+
60
+ :unknown
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../platform"
4
+
5
+ require_relative "../checks/compose_file_present"
6
+ require_relative "../checks/compose_build_paths"
7
+ require_relative "../checks/compose_env_file_present"
8
+ require_relative "../checks/compose_entrypoint_sanity"
9
+ require_relative "../checks/compose_entrypoint_exec"
10
+ require_relative "../checks/shell_scripts_crlf"
11
+ require_relative "../checks/compose_healthchecks"
12
+ require_relative "../checks/compose_platform_risk"
13
+ require_relative "../checks/rails_master_key_required"
14
+
15
+ module DocktorRails
16
+ module Preflight
17
+ class Runner
18
+ DEFAULT_CHECKS = [
19
+ Checks::ComposeFilePresent,
20
+ Checks::ComposeBuildPaths,
21
+ Checks::ComposeEnvFilePresent,
22
+ Checks::ComposeEntrypointSanity,
23
+ Checks::ComposeEntrypointExec,
24
+ Checks::ShellScriptsCrlf,
25
+ Checks::ComposeHealthchecks,
26
+ Checks::ComposePlatformRisk,
27
+ Checks::RailsMasterKeyRequired
28
+ ].freeze
29
+
30
+ def initialize(root: Dir.pwd, checks: DEFAULT_CHECKS)
31
+ @root = root
32
+ @checks = checks
33
+ end
34
+
35
+ def run
36
+ platform = Platform.detect
37
+ ctx = { root: @root, platform: platform }
38
+
39
+ results = @checks.map do |check_class|
40
+ check_class.new.run(ctx)
41
+ end
42
+
43
+ summary = {
44
+ pass: results.count(&:pass?),
45
+ warn: results.count(&:warn?),
46
+ fail: results.count(&:fail?)
47
+ }
48
+
49
+ status = if summary[:fail].positive?
50
+ :fail
51
+ elsif summary[:warn].positive?
52
+ :warn
53
+ else
54
+ :pass
55
+ end
56
+
57
+ {
58
+ root: @root,
59
+ platform: platform,
60
+ status: status,
61
+ summary: summary,
62
+ checks: results
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../guidance"
6
+
7
+ module DocktorRails
8
+ module Reporters
9
+ class JsonReporter
10
+ SCHEMA_VERSION = 1
11
+
12
+ def initialize(io: $stdout)
13
+ @io = io
14
+ end
15
+
16
+ def render(report, tool_version:)
17
+ platform = report[:platform]
18
+
19
+ checks = report.fetch(:checks).map do |r|
20
+ h = r.to_h
21
+ hint = Guidance.hint_for(r, platform)
22
+ h[:hint] = hint if hint && !h.key?(:hint)
23
+ h
24
+ end
25
+
26
+ payload = {
27
+ schema_version: SCHEMA_VERSION,
28
+ tool: { name: "docktor_rails", version: tool_version },
29
+ context: {
30
+ root: report[:root],
31
+ host: platform&.to_h
32
+ },
33
+ status: report.fetch(:status).to_s,
34
+ summary: report.fetch(:summary),
35
+ checks: checks
36
+ }
37
+
38
+ @io.write(JSON.pretty_generate(payload))
39
+ @io.write("\n")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ require_relative "../guidance"
6
+
7
+ module DocktorRails
8
+ module Reporters
9
+ class TextReporter
10
+ def initialize(io: $stdout, color: true, verbose: false, quiet: false)
11
+ @io = io
12
+ @pastel = Pastel.new(enabled: color)
13
+ @verbose = verbose
14
+ @quiet = quiet
15
+ end
16
+
17
+ def render(report)
18
+ @root = report[:root]
19
+ @platform = report[:platform]
20
+
21
+ unless @quiet
22
+ @io.puts "docktor_rails — preflight check"
23
+ @io.puts @pastel.dim("host: #{@platform&.label || "unknown"}")
24
+ @io.puts @pastel.dim("root: #{@root}") if @root
25
+ @io.puts "—" * 46
26
+ end
27
+
28
+ checks = report.fetch(:checks)
29
+ render_group("Errors", checks.select(&:fail?))
30
+ render_group("Warnings", checks.select(&:warn?))
31
+ render_group("Passed", checks.select(&:pass?)) if @verbose && !@quiet
32
+
33
+ @io.puts "—" * 46 unless @quiet
34
+ s = report.fetch(:summary)
35
+ @io.puts "#{s.fetch(:fail)} errors · #{s.fetch(:warn)} warnings · #{s.fetch(:pass)} passed"
36
+ end
37
+
38
+ private
39
+
40
+ def render_group(title, results)
41
+ return if results.empty?
42
+
43
+ @io.puts @pastel.bold(title) unless @quiet
44
+ results.each do |result|
45
+ @io.puts format_line(result)
46
+ render_details(result)
47
+ end
48
+ @io.puts unless @quiet
49
+ end
50
+
51
+ def format_line(result)
52
+ case result.status
53
+ when :pass
54
+ @pastel.green("✅ #{result.message}")
55
+ when :warn
56
+ @pastel.yellow("⚠️ #{result.message}")
57
+ else
58
+ @pastel.red("❌ #{result.message}")
59
+ end
60
+ end
61
+
62
+ def render_details(result)
63
+ files = Array(result.files).compact
64
+ hint = Guidance.hint_for(result, @platform)
65
+
66
+ return if files.empty? && hint.nil?
67
+
68
+ if files.any?
69
+ rendered = files.first(8).join(", ")
70
+ rendered += " …" if files.length > 8
71
+ @io.puts @pastel.dim(" files: #{rendered}")
72
+ end
73
+
74
+ @io.puts @pastel.dim(" hint: #{hint}") if hint
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocktorRails
4
+ Result = Struct.new(:id, :status, :message, :files, :hint, keyword_init: true) do
5
+ def to_h
6
+ h = {
7
+ id: id,
8
+ status: status.to_s,
9
+ message: message,
10
+ files: Array(files).compact
11
+ }
12
+ h[:hint] = hint if hint
13
+ h.delete(:files) if h[:files].empty?
14
+ h
15
+ end
16
+
17
+ def pass?
18
+ status == :pass
19
+ end
20
+
21
+ def warn?
22
+ status == :warn
23
+ end
24
+
25
+ def fail?
26
+ status == :fail
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocktorRails
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "docktor_rails/version"
4
+ require_relative "docktor_rails/platform"
5
+ require_relative "docktor_rails/guidance"
6
+ require_relative "docktor_rails/cli"
7
+
8
+ module DocktorRails
9
+ class Error < StandardError; end
10
+ end
@@ -0,0 +1,4 @@
1
+ module DocktorRails
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: docktor_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - FAllS
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-06-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pastel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: fakefs
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Diagnose common cross-machine Docker/Compose issues in Rails apps (CRLF,
70
+ missing files/env, healthcheck/readiness risks) with CI-friendly output.
71
+ email:
72
+ - abdullah_alavi@hotmail.com
73
+ executables:
74
+ - docktor_rails
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".rspec"
79
+ - ".rubocop.yml"
80
+ - CHANGELOG.md
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - exe/docktor_rails
85
+ - lib/docktor_rails.rb
86
+ - lib/docktor_rails/checks/base_check.rb
87
+ - lib/docktor_rails/checks/compose_build_paths.rb
88
+ - lib/docktor_rails/checks/compose_entrypoint_exec.rb
89
+ - lib/docktor_rails/checks/compose_entrypoint_sanity.rb
90
+ - lib/docktor_rails/checks/compose_env_file_present.rb
91
+ - lib/docktor_rails/checks/compose_file_present.rb
92
+ - lib/docktor_rails/checks/compose_healthchecks.rb
93
+ - lib/docktor_rails/checks/compose_platform_risk.rb
94
+ - lib/docktor_rails/checks/rails_master_key_required.rb
95
+ - lib/docktor_rails/checks/shell_scripts_crlf.rb
96
+ - lib/docktor_rails/cli.rb
97
+ - lib/docktor_rails/compose.rb
98
+ - lib/docktor_rails/dotenv.rb
99
+ - lib/docktor_rails/guidance.rb
100
+ - lib/docktor_rails/platform.rb
101
+ - lib/docktor_rails/preflight/runner.rb
102
+ - lib/docktor_rails/reporters/json_reporter.rb
103
+ - lib/docktor_rails/reporters/text_reporter.rb
104
+ - lib/docktor_rails/result.rb
105
+ - lib/docktor_rails/version.rb
106
+ - sig/docktor_rails.rbs
107
+ homepage: https://github.com/fallS/docktor_rails
108
+ licenses:
109
+ - MIT
110
+ metadata:
111
+ homepage_uri: https://github.com/fallS/docktor_rails
112
+ source_code_uri: https://github.com/fallS/docktor_rails
113
+ changelog_uri: https://github.com/fallS/docktor_rails/blob/main/CHANGELOG.md
114
+ rubygems_mfa_required: 'true'
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: 3.0.0
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubygems_version: 3.3.7
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: Preflight checks for Rails Docker/Compose dev environments
134
+ test_files: []