reviewer 0.1.5 → 1.0.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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/main.yml +79 -11
- data/.github/workflows/release.yml +98 -0
- data/.gitignore +1 -1
- data/.inch.yml +3 -1
- data/.reek.yml +175 -0
- data/.reviewer.example.yml +7 -2
- data/.reviewer.yml +166 -40
- data/.rubocop.yml +34 -2
- data/CHANGELOG.md +42 -2
- data/Gemfile +39 -1
- data/Gemfile.lock +291 -70
- data/LICENSE.txt +20 -4
- data/README.md +310 -21
- data/RELEASING.md +190 -0
- data/Rakefile +117 -0
- data/dependency_decisions.yml +61 -0
- data/exe/fmt +1 -1
- data/exe/rvw +1 -1
- data/lib/reviewer/arguments/files.rb +47 -20
- data/lib/reviewer/arguments/keywords.rb +34 -41
- data/lib/reviewer/arguments/tags.rb +11 -11
- data/lib/reviewer/arguments.rb +100 -29
- data/lib/reviewer/batch/formatter.rb +87 -0
- data/lib/reviewer/batch.rb +32 -48
- data/lib/reviewer/capabilities.rb +81 -0
- data/lib/reviewer/command/string/env.rb +12 -6
- data/lib/reviewer/command/string/flags.rb +2 -4
- data/lib/reviewer/command/string.rb +47 -12
- data/lib/reviewer/command.rb +65 -10
- data/lib/reviewer/configuration/loader.rb +70 -0
- data/lib/reviewer/configuration.rb +6 -3
- data/lib/reviewer/context.rb +15 -0
- data/lib/reviewer/doctor/config_check.rb +46 -0
- data/lib/reviewer/doctor/environment_check.rb +58 -0
- data/lib/reviewer/doctor/formatter.rb +75 -0
- data/lib/reviewer/doctor/keyword_check.rb +85 -0
- data/lib/reviewer/doctor/opportunity_check.rb +88 -0
- data/lib/reviewer/doctor/report.rb +63 -0
- data/lib/reviewer/doctor/tool_inventory.rb +41 -0
- data/lib/reviewer/doctor.rb +28 -0
- data/lib/reviewer/history.rb +10 -17
- data/lib/reviewer/output/formatting.rb +40 -0
- data/lib/reviewer/output/printer.rb +70 -9
- data/lib/reviewer/output.rb +37 -78
- data/lib/reviewer/prompt.rb +38 -0
- data/lib/reviewer/report/formatter.rb +124 -0
- data/lib/reviewer/report.rb +100 -0
- data/lib/reviewer/runner/failed_files.rb +66 -0
- data/lib/reviewer/runner/formatter.rb +103 -0
- data/lib/reviewer/runner/guidance.rb +79 -0
- data/lib/reviewer/runner/result.rb +150 -0
- data/lib/reviewer/runner/strategies/captured.rb +98 -23
- data/lib/reviewer/runner/strategies/passthrough.rb +2 -11
- data/lib/reviewer/runner.rb +126 -40
- data/lib/reviewer/session/formatter.rb +87 -0
- data/lib/reviewer/session.rb +208 -0
- data/lib/reviewer/setup/catalog.rb +233 -0
- data/lib/reviewer/setup/detector.rb +61 -0
- data/lib/reviewer/setup/formatter.rb +94 -0
- data/lib/reviewer/setup/gemfile_lock.rb +55 -0
- data/lib/reviewer/setup/generator.rb +54 -0
- data/lib/reviewer/setup/tool_block.rb +112 -0
- data/lib/reviewer/setup.rb +41 -0
- data/lib/reviewer/shell/result.rb +14 -15
- data/lib/reviewer/shell/timer.rb +40 -35
- data/lib/reviewer/shell.rb +41 -12
- data/lib/reviewer/tool/conversions.rb +20 -0
- data/lib/reviewer/tool/file_resolver.rb +54 -0
- data/lib/reviewer/tool/settings.rb +88 -44
- data/lib/reviewer/tool/test_file_mapper.rb +73 -0
- data/lib/reviewer/tool/timing.rb +78 -0
- data/lib/reviewer/tool.rb +88 -69
- data/lib/reviewer/tools.rb +47 -33
- data/lib/reviewer/version.rb +1 -1
- data/lib/reviewer.rb +109 -50
- data/reviewer.gemspec +16 -19
- metadata +101 -142
- data/lib/reviewer/conversions.rb +0 -16
- data/lib/reviewer/guidance.rb +0 -77
- data/lib/reviewer/keywords/git/staged.rb +0 -64
- data/lib/reviewer/keywords/git.rb +0 -14
- data/lib/reviewer/keywords.rb +0 -9
- data/lib/reviewer/loader.rb +0 -59
- data/lib/reviewer/output/scrubber.rb +0 -48
- data/lib/reviewer/output/token.rb +0 -85
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
module Doctor
|
|
7
|
+
# Checks environment prerequisites (git, Ruby version)
|
|
8
|
+
class EnvironmentCheck
|
|
9
|
+
attr_reader :report
|
|
10
|
+
|
|
11
|
+
# Creates an environment checker for Ruby and git availability
|
|
12
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
13
|
+
#
|
|
14
|
+
# @return [EnvironmentCheck]
|
|
15
|
+
def initialize(report)
|
|
16
|
+
@report = report
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Checks Ruby version and git availability
|
|
20
|
+
def check
|
|
21
|
+
check_ruby_version
|
|
22
|
+
check_git
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def check_ruby_version
|
|
28
|
+
report.add(:environment, status: :ok, message: "Ruby #{RUBY_VERSION}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check_git
|
|
32
|
+
stdout, _stderr, status = Open3.capture3('git --version')
|
|
33
|
+
|
|
34
|
+
unless status.success?
|
|
35
|
+
report.add(:environment, status: :warning,
|
|
36
|
+
message: 'Git not available',
|
|
37
|
+
detail: 'Git keywords (staged, modified, etc.) require git')
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
report.add(:environment, status: :ok, message: stdout.strip)
|
|
42
|
+
check_git_repo
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def check_git_repo
|
|
46
|
+
_stdout, _stderr, status = Open3.capture3('git rev-parse --git-dir')
|
|
47
|
+
|
|
48
|
+
if status.success?
|
|
49
|
+
report.add(:environment, status: :ok, message: 'Inside a git repository')
|
|
50
|
+
else
|
|
51
|
+
report.add(:environment, status: :warning,
|
|
52
|
+
message: 'Not inside a git repository',
|
|
53
|
+
detail: 'Git keywords (staged, modified, etc.) will not work')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../output/formatting'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
module Doctor
|
|
7
|
+
# Display logic for diagnostic reports
|
|
8
|
+
class Formatter
|
|
9
|
+
include Output::Formatting
|
|
10
|
+
|
|
11
|
+
attr_reader :output, :printer
|
|
12
|
+
private :output, :printer
|
|
13
|
+
|
|
14
|
+
SYMBOLS = { ok: "\u2713", warning: '!', error: "\u2717", info: "\u00b7", muted: "\u00b7" }.freeze
|
|
15
|
+
STYLES = { ok: :success, warning: :warning, error: :failure, info: :muted, muted: :muted }.freeze
|
|
16
|
+
|
|
17
|
+
SECTION_LABELS = {
|
|
18
|
+
configuration: 'Configuration',
|
|
19
|
+
tools: 'Tools',
|
|
20
|
+
opportunities: 'Opportunities',
|
|
21
|
+
environment: 'Environment'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Creates a formatter for diagnostic report display
|
|
25
|
+
# @param output [Output] the console output handler
|
|
26
|
+
#
|
|
27
|
+
# @return [Formatter]
|
|
28
|
+
def initialize(output)
|
|
29
|
+
@output = output
|
|
30
|
+
@printer = output.printer
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Renders a full diagnostic report
|
|
34
|
+
# @param report [Doctor::Report] the report to display
|
|
35
|
+
def print(report)
|
|
36
|
+
output.newline
|
|
37
|
+
Doctor::Report::SECTIONS.each do |section|
|
|
38
|
+
findings = report.section(section)
|
|
39
|
+
print_section(section, findings) if findings.any?
|
|
40
|
+
end
|
|
41
|
+
print_summary(report)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def print_section(section, findings)
|
|
47
|
+
printer.puts(:bold, SECTION_LABELS.fetch(section) { section.to_s.capitalize })
|
|
48
|
+
findings.each { |finding| print_finding(finding) }
|
|
49
|
+
output.newline
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def print_finding(finding)
|
|
53
|
+
status = finding.status
|
|
54
|
+
message = finding.message
|
|
55
|
+
detail = finding.detail
|
|
56
|
+
|
|
57
|
+
symbol = SYMBOLS.fetch(status) { ' ' }
|
|
58
|
+
style = STYLES.fetch(status) { :default }
|
|
59
|
+
|
|
60
|
+
printer.print(style, " #{symbol} ")
|
|
61
|
+
printer.puts(:default, message)
|
|
62
|
+
printer.puts(:muted, " #{detail}") if detail
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def print_summary(report)
|
|
66
|
+
if report.ok?
|
|
67
|
+
printer.puts(:success, 'No issues found')
|
|
68
|
+
else
|
|
69
|
+
printer.puts(:failure, pluralize(report.errors.size, 'issue found', 'issues found'))
|
|
70
|
+
end
|
|
71
|
+
output.newline
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Detects conflicts between configured tool names, tags, and reserved keywords
|
|
6
|
+
class KeywordCheck
|
|
7
|
+
attr_reader :report
|
|
8
|
+
|
|
9
|
+
RESERVED = Arguments::Keywords::RESERVED
|
|
10
|
+
|
|
11
|
+
# Creates a keyword check that scans for keyword conflicts in configuration
|
|
12
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
13
|
+
# @param configuration [Configuration] the configuration to check
|
|
14
|
+
# @param tools [Tools] the tools collection to analyze
|
|
15
|
+
#
|
|
16
|
+
# @return [KeywordCheck]
|
|
17
|
+
def initialize(report, configuration:, tools:)
|
|
18
|
+
@report = report
|
|
19
|
+
@configuration = configuration
|
|
20
|
+
@tools = tools
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Checks for keyword conflicts between tool names, tags, and reserved keywords
|
|
24
|
+
def check
|
|
25
|
+
return unless @configuration.file.exist?
|
|
26
|
+
|
|
27
|
+
check_tool_names_vs_reserved
|
|
28
|
+
check_tags_vs_reserved
|
|
29
|
+
check_tool_names_vs_tags
|
|
30
|
+
rescue Configuration::Loader::MissingConfigurationError
|
|
31
|
+
# Tools may reference a stale config path — nothing to check
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def all_tools = @all_tools ||= @tools.all
|
|
37
|
+
|
|
38
|
+
def tool_names = all_tools.map { |tool| tool.key.to_s }
|
|
39
|
+
|
|
40
|
+
def all_tags = all_tools.flat_map(&:tags).uniq
|
|
41
|
+
|
|
42
|
+
# Names of tools that use a given tag
|
|
43
|
+
def tools_with_tag(tag)
|
|
44
|
+
all_tools.select { |tool| tool.tags.include?(tag) }.map(&:name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Tags that belong to tools OTHER than the named tool
|
|
48
|
+
def tags_from_other_tools(tool_name)
|
|
49
|
+
all_tools.reject { |tool| tool.key.to_s == tool_name }.flat_map(&:tags).uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def check_tool_names_vs_reserved
|
|
53
|
+
(tool_names & RESERVED).each do |name|
|
|
54
|
+
report.add(:configuration,
|
|
55
|
+
status: :warning,
|
|
56
|
+
message: "Tool name '#{name}' shadows reserved keyword '#{name}'",
|
|
57
|
+
detail: "Reserved keywords (#{RESERVED.join(', ')}) trigger special behavior. " \
|
|
58
|
+
'Rename this tool to avoid unexpected results.')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_tags_vs_reserved
|
|
63
|
+
(all_tags & RESERVED).each do |tag|
|
|
64
|
+
report.add(:configuration,
|
|
65
|
+
status: :warning,
|
|
66
|
+
message: "Tag '#{tag}' shadows reserved keyword '#{tag}' (used by #{tools_with_tag(tag).join(', ')})",
|
|
67
|
+
detail: "Reserved keywords (#{RESERVED.join(', ')}) trigger special behavior. " \
|
|
68
|
+
'Rename this tag or use -t to pass tags explicitly.')
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check_tool_names_vs_tags
|
|
73
|
+
tool_names.each do |name|
|
|
74
|
+
next unless tags_from_other_tools(name).include?(name)
|
|
75
|
+
|
|
76
|
+
report.add(:configuration,
|
|
77
|
+
status: :warning,
|
|
78
|
+
message: "Tool name '#{name}' is also a tag on: #{tools_with_tag(name).join(', ')}",
|
|
79
|
+
detail: "'rvw #{name}' will run both the '#{name}' tool and tagged tools. " \
|
|
80
|
+
'Use -t to target tags explicitly if this is unintended.')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Suggests improvements based on current configuration and project state
|
|
6
|
+
class OpportunityCheck
|
|
7
|
+
attr_reader :report, :project_dir
|
|
8
|
+
|
|
9
|
+
# Creates an opportunity checker that scans for unconfigured tools and missing features
|
|
10
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
11
|
+
# @param project_dir [Pathname] the project root for tool detection
|
|
12
|
+
# @param configuration [Configuration] the configuration to check
|
|
13
|
+
# @param tools [Tools] the tools collection to analyze
|
|
14
|
+
#
|
|
15
|
+
# @return [OpportunityCheck]
|
|
16
|
+
def initialize(report, project_dir, configuration:, tools:)
|
|
17
|
+
@report = report
|
|
18
|
+
@project_dir = project_dir
|
|
19
|
+
@configuration = configuration
|
|
20
|
+
@tools = tools
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Checks for unconfigured tools, missing file targeting, and missing format commands
|
|
24
|
+
def check
|
|
25
|
+
return unless @configuration.file.exist?
|
|
26
|
+
|
|
27
|
+
check_unconfigured_tools
|
|
28
|
+
check_missing_files_config
|
|
29
|
+
check_missing_format_command
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def check_unconfigured_tools
|
|
35
|
+
detected = Setup::Detector.new(project_dir).detect
|
|
36
|
+
configured_keys = @tools.all.map(&:key)
|
|
37
|
+
|
|
38
|
+
detected.each do |result|
|
|
39
|
+
next if configured_keys.include?(result.key)
|
|
40
|
+
|
|
41
|
+
report.add(:opportunities, status: :info,
|
|
42
|
+
message: "#{result.name} detected but not configured",
|
|
43
|
+
detail: result.reasons.join(', '))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_missing_files_config
|
|
48
|
+
@tools.all.each do |tool|
|
|
49
|
+
next if tool.skip_in_batch?
|
|
50
|
+
next if tool.supports_files?
|
|
51
|
+
next unless catalog_supports?(tool.key, :files)
|
|
52
|
+
|
|
53
|
+
report.add(:opportunities, status: :info,
|
|
54
|
+
message: "#{tool.name} has no file targeting configured",
|
|
55
|
+
detail: 'Add a `files` section to enable staged/modified file scoping')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_missing_format_command
|
|
60
|
+
@tools.all.each do |tool|
|
|
61
|
+
next if tool.skip_in_batch?
|
|
62
|
+
next if tool.formattable?
|
|
63
|
+
next unless catalog_supports?(tool.key, :format)
|
|
64
|
+
|
|
65
|
+
report.add(:opportunities, status: :info,
|
|
66
|
+
message: "#{tool.name} has no format command",
|
|
67
|
+
detail: 'Add a `format` command to enable `fmt` support')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns true only if the catalog knows this tool AND the catalog entry
|
|
72
|
+
# includes the given capability (:files or :format command)
|
|
73
|
+
def catalog_supports?(key, capability)
|
|
74
|
+
entry = Setup::Catalog::TOOLS[key]
|
|
75
|
+
return false unless entry
|
|
76
|
+
|
|
77
|
+
case capability
|
|
78
|
+
when :files
|
|
79
|
+
entry.key?(:files)
|
|
80
|
+
when :format
|
|
81
|
+
entry.dig(:commands, :format) ? true : false
|
|
82
|
+
else
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Structured container for diagnostic findings organized by section
|
|
6
|
+
class Report
|
|
7
|
+
# A single diagnostic finding with status, message, and optional detail.
|
|
8
|
+
# @!attribute status [rw]
|
|
9
|
+
# @return [Symbol] the severity (:ok, :warning, :error, or :info)
|
|
10
|
+
# @!attribute message [rw]
|
|
11
|
+
# @return [String] the finding summary
|
|
12
|
+
# @!attribute detail [rw]
|
|
13
|
+
# @return [String, nil] optional detail or guidance text
|
|
14
|
+
Finding = Struct.new(:status, :message, :detail, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
# Ordered list of report sections
|
|
17
|
+
SECTIONS = %i[configuration tools opportunities environment].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :findings
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@findings = Hash.new { |hash, key| hash[key] = [] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Adds a finding to a section
|
|
26
|
+
# @param section [Symbol] one of SECTIONS
|
|
27
|
+
# @param status [Symbol] :ok, :warning, :error, or :info
|
|
28
|
+
# @param message [String] the finding summary
|
|
29
|
+
# @param detail [String, nil] optional detail text
|
|
30
|
+
def add(section, status:, message:, detail: nil)
|
|
31
|
+
findings[section] << Finding.new(status: status, message: message, detail: detail)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether all findings are free of errors
|
|
35
|
+
def ok?
|
|
36
|
+
all_findings.none? { |finding| finding.status == :error }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# All error findings across sections
|
|
40
|
+
def errors
|
|
41
|
+
all_findings.select { |finding| finding.status == :error }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# All warning findings across sections
|
|
45
|
+
def warnings
|
|
46
|
+
all_findings.select { |finding| finding.status == :warning }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Findings for a specific section
|
|
50
|
+
# @param name [Symbol] the section name
|
|
51
|
+
# @return [Array<Finding>] findings for that section
|
|
52
|
+
def section(name)
|
|
53
|
+
findings[name]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def all_findings
|
|
59
|
+
findings.values.flatten
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Reports the status of each configured tool
|
|
6
|
+
class ToolInventory
|
|
7
|
+
attr_reader :report
|
|
8
|
+
|
|
9
|
+
# Creates a tool inventory check that reports batch/skip status for each tool
|
|
10
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
11
|
+
# @param configuration [Configuration] the configuration to check
|
|
12
|
+
# @param tools [Tools] the tools collection to report on
|
|
13
|
+
#
|
|
14
|
+
# @return [ToolInventory]
|
|
15
|
+
def initialize(report, configuration:, tools:)
|
|
16
|
+
@report = report
|
|
17
|
+
@configuration = configuration
|
|
18
|
+
@tools = tools
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Reports batch/skip status and available commands for each configured tool
|
|
22
|
+
def check
|
|
23
|
+
return unless @configuration.file.exist?
|
|
24
|
+
|
|
25
|
+
@tools.all.each do |tool|
|
|
26
|
+
skipped = tool.skip_in_batch?
|
|
27
|
+
|
|
28
|
+
report.add(:tools,
|
|
29
|
+
status: skipped ? :muted : :ok,
|
|
30
|
+
message: "#{tool.name} (#{tool.key}) — #{command_summary(tool)}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def command_summary(tool)
|
|
37
|
+
%i[review format install prepare].select { |cmd| tool.command?(cmd) }.join(', ')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'doctor/formatter'
|
|
4
|
+
require_relative 'doctor/report'
|
|
5
|
+
require_relative 'doctor/config_check'
|
|
6
|
+
require_relative 'doctor/keyword_check'
|
|
7
|
+
require_relative 'doctor/tool_inventory'
|
|
8
|
+
require_relative 'doctor/opportunity_check'
|
|
9
|
+
require_relative 'doctor/environment_check'
|
|
10
|
+
|
|
11
|
+
module Reviewer
|
|
12
|
+
# Diagnostic module for checking configuration, tools, and environment health
|
|
13
|
+
module Doctor
|
|
14
|
+
# Runs all diagnostic checks and returns a structured report
|
|
15
|
+
# @param project_dir [Pathname] the project root to scan
|
|
16
|
+
#
|
|
17
|
+
# @return [Doctor::Report] the complete diagnostic report
|
|
18
|
+
def self.run(configuration:, tools:, project_dir: Pathname.pwd)
|
|
19
|
+
report = Report.new
|
|
20
|
+
ConfigCheck.new(report, configuration: configuration).check
|
|
21
|
+
KeywordCheck.new(report, configuration: configuration, tools: tools).check
|
|
22
|
+
ToolInventory.new(report, configuration: configuration, tools: tools).check
|
|
23
|
+
OpportunityCheck.new(report, project_dir, configuration: configuration, tools: tools).check
|
|
24
|
+
EnvironmentCheck.new(report).check
|
|
25
|
+
report
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/reviewer/history.rb
CHANGED
|
@@ -17,11 +17,11 @@ module Reviewer
|
|
|
17
17
|
class History
|
|
18
18
|
attr_reader :file, :store
|
|
19
19
|
|
|
20
|
-
# Creates an instance of a YAML::Store-backed history file
|
|
21
|
-
# @param file
|
|
20
|
+
# Creates an instance of a YAML::Store-backed history file
|
|
21
|
+
# @param file [Pathname] the history file to store data
|
|
22
22
|
#
|
|
23
|
-
# @return [History]
|
|
24
|
-
def initialize(file
|
|
23
|
+
# @return [History]
|
|
24
|
+
def initialize(file:)
|
|
25
25
|
@file = file
|
|
26
26
|
@store = YAML::Store.new(file)
|
|
27
27
|
end
|
|
@@ -33,9 +33,9 @@ module Reviewer
|
|
|
33
33
|
#
|
|
34
34
|
# @return [Primitive] the value being stored
|
|
35
35
|
def set(group, attribute, value)
|
|
36
|
-
store.transaction do
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
store.transaction do
|
|
37
|
+
store[group] ||= {}
|
|
38
|
+
store[group][attribute] = value
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -45,25 +45,18 @@ module Reviewer
|
|
|
45
45
|
#
|
|
46
46
|
# @return [Primitive] the value being stored
|
|
47
47
|
def get(group, attribute)
|
|
48
|
-
store.transaction do
|
|
49
|
-
|
|
48
|
+
store.transaction(true) do
|
|
49
|
+
store[group]&.[](attribute)
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Removes the existing history file.
|
|
54
54
|
#
|
|
55
55
|
# @return [void]
|
|
56
|
-
def
|
|
56
|
+
def clear
|
|
57
57
|
return unless File.exist?(file)
|
|
58
58
|
|
|
59
59
|
FileUtils.rm(file)
|
|
60
60
|
end
|
|
61
|
-
|
|
62
|
-
# Convenience class method for removing the history file.
|
|
63
|
-
#
|
|
64
|
-
# @return [void]
|
|
65
|
-
def self.reset!
|
|
66
|
-
new.reset!
|
|
67
|
-
end
|
|
68
61
|
end
|
|
69
62
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Output
|
|
5
|
+
# Shared display vocabulary included by all domain formatters.
|
|
6
|
+
# Provides common constants and helper methods for formatting output.
|
|
7
|
+
module Formatting
|
|
8
|
+
CHECKMARK = "\u2713"
|
|
9
|
+
XMARK = "\u2717"
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Formats a duration in seconds for display
|
|
14
|
+
# @param seconds [Float, nil] the duration to format
|
|
15
|
+
# @return [String] formatted duration (e.g., "1.23s")
|
|
16
|
+
def format_duration(seconds)
|
|
17
|
+
"#{seconds.to_f.round(2)}s"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the appropriate status mark
|
|
21
|
+
# @param success [Boolean] whether the operation succeeded
|
|
22
|
+
# @return [String] checkmark or x mark
|
|
23
|
+
def status_mark(success) = success ? CHECKMARK : XMARK
|
|
24
|
+
|
|
25
|
+
# Returns the appropriate style key for a status
|
|
26
|
+
# @param success [Boolean] whether the operation succeeded
|
|
27
|
+
# @return [Symbol] :success or :failure
|
|
28
|
+
def status_style(success) = success ? :success : :failure
|
|
29
|
+
|
|
30
|
+
# Pluralizes a word based on count
|
|
31
|
+
# @param count [Integer] the count
|
|
32
|
+
# @param singular [String] the singular form
|
|
33
|
+
# @param plural [String] the plural form (defaults to singular + "s")
|
|
34
|
+
# @return [String] formatted count with word (e.g., "1 issue" or "3 issues")
|
|
35
|
+
def pluralize(count, singular, plural = "#{singular}s")
|
|
36
|
+
count == 1 ? "1 #{singular}" : "#{count} #{plural}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -4,37 +4,98 @@ require 'io/console' # For determining console width/height
|
|
|
4
4
|
|
|
5
5
|
module Reviewer
|
|
6
6
|
class Output
|
|
7
|
-
#
|
|
7
|
+
# ANSI terminal escape sequences for styled console output.
|
|
8
|
+
# Extracted from Printer so style definitions are separated from printing mechanics.
|
|
9
|
+
module AnsiStyles
|
|
10
|
+
ESC = "\e["
|
|
11
|
+
RESET = "#{ESC}0m".freeze
|
|
12
|
+
|
|
13
|
+
# Weight codes
|
|
14
|
+
WEIGHTS = { default: 0, bold: 1, light: 2, italic: 3 }.freeze
|
|
15
|
+
|
|
16
|
+
# Color codes
|
|
17
|
+
COLORS = {
|
|
18
|
+
black: 30, red: 31, green: 32, yellow: 33,
|
|
19
|
+
blue: 34, magenta: 35, cyan: 36, gray: 37, default: 39
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Style definitions: [weight, color]
|
|
23
|
+
STYLE_DEFS = {
|
|
24
|
+
success_bold: %i[bold green],
|
|
25
|
+
success: %i[default green],
|
|
26
|
+
success_light: %i[light green],
|
|
27
|
+
error: %i[bold red],
|
|
28
|
+
failure: %i[default red],
|
|
29
|
+
warning: %i[bold yellow],
|
|
30
|
+
warning_light: %i[light yellow],
|
|
31
|
+
source: %i[italic default],
|
|
32
|
+
bold: %i[default default],
|
|
33
|
+
default: %i[default default],
|
|
34
|
+
muted: %i[light gray]
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Pre-computed ANSI escape strings for each style
|
|
38
|
+
STYLES = STYLE_DEFS.transform_values do |weight_key, color_key|
|
|
39
|
+
"#{ESC}#{WEIGHTS.fetch(weight_key)};#{COLORS.fetch(color_key)}m"
|
|
40
|
+
end.freeze
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Wrapper to encapsulate some lower-level details of printing to $stdout.
|
|
44
|
+
# Handles ANSI styling via the pre-computed AnsiStyles::STYLES constant.
|
|
8
45
|
class Printer
|
|
46
|
+
include AnsiStyles
|
|
47
|
+
|
|
9
48
|
attr_reader :stream
|
|
10
49
|
|
|
11
|
-
# Creates
|
|
50
|
+
# Creates a printer for styled console output
|
|
51
|
+
# @param stream [IO] the output stream to write to
|
|
52
|
+
#
|
|
53
|
+
# @return [Printer]
|
|
12
54
|
def initialize(stream = $stdout)
|
|
13
|
-
@stream = stream
|
|
14
|
-
|
|
15
|
-
str.sync = str.respond_to?(:sync=)
|
|
16
|
-
end
|
|
55
|
+
@stream = stream
|
|
56
|
+
@stream.sync = true if @stream.respond_to?(:sync=)
|
|
17
57
|
end
|
|
18
58
|
|
|
59
|
+
# Prints styled content without a newline
|
|
60
|
+
# @param style [Symbol] the style key for color and weight
|
|
61
|
+
# @param content [String] the text to print
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
19
64
|
def print(style, content)
|
|
20
65
|
text(style, content)
|
|
21
66
|
end
|
|
22
67
|
|
|
68
|
+
# Prints styled content followed by a newline
|
|
69
|
+
# @param style [Symbol] the style key for color and weight
|
|
70
|
+
# @param content [String] the text to print
|
|
71
|
+
#
|
|
72
|
+
# @return [void]
|
|
23
73
|
def puts(style, content)
|
|
24
74
|
text(style, content)
|
|
25
75
|
stream.puts
|
|
26
76
|
end
|
|
27
77
|
|
|
28
|
-
|
|
29
|
-
|
|
78
|
+
# Writes content directly to the stream without styling.
|
|
79
|
+
# Skips if content is nil or blank.
|
|
80
|
+
#
|
|
81
|
+
# @param content [String, nil] the raw text to write
|
|
82
|
+
# @return [void]
|
|
83
|
+
def write_raw(content)
|
|
84
|
+
return if content.to_s.strip.empty?
|
|
85
|
+
|
|
86
|
+
stream << content
|
|
30
87
|
end
|
|
88
|
+
|
|
89
|
+
# Whether the output stream is a TTY (interactive terminal)
|
|
90
|
+
# @return [Boolean] true if the stream supports ANSI styling
|
|
91
|
+
def tty? = stream.tty?
|
|
31
92
|
alias style_enabled? tty?
|
|
32
93
|
|
|
33
94
|
private
|
|
34
95
|
|
|
35
96
|
def text(style, content)
|
|
36
97
|
if style_enabled?
|
|
37
|
-
stream.print
|
|
98
|
+
stream.print "#{STYLES.fetch(style)}#{content}#{RESET}"
|
|
38
99
|
else
|
|
39
100
|
stream.print content
|
|
40
101
|
end
|