reviewer 0.1.4 → 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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.alexignore +1 -0
  3. data/.github/FUNDING.yml +3 -0
  4. data/.github/workflows/main.yml +81 -11
  5. data/.github/workflows/release.yml +98 -0
  6. data/.gitignore +1 -1
  7. data/.inch.yml +3 -1
  8. data/.reek.yml +175 -0
  9. data/.reviewer.example.yml +27 -12
  10. data/.reviewer.future.yml +221 -0
  11. data/.reviewer.yml +191 -28
  12. data/.reviewer_stdout +0 -0
  13. data/.rubocop.yml +34 -1
  14. data/CHANGELOG.md +42 -2
  15. data/Gemfile +39 -1
  16. data/Gemfile.lock +294 -72
  17. data/README.md +315 -7
  18. data/RELEASING.md +190 -0
  19. data/Rakefile +117 -0
  20. data/dependency_decisions.yml +61 -0
  21. data/exe/fmt +1 -1
  22. data/exe/rvw +1 -1
  23. data/lib/reviewer/arguments/files.rb +60 -27
  24. data/lib/reviewer/arguments/keywords.rb +39 -43
  25. data/lib/reviewer/arguments/tags.rb +21 -14
  26. data/lib/reviewer/arguments.rb +107 -29
  27. data/lib/reviewer/batch/formatter.rb +87 -0
  28. data/lib/reviewer/batch.rb +46 -35
  29. data/lib/reviewer/capabilities.rb +81 -0
  30. data/lib/reviewer/command/string/env.rb +16 -6
  31. data/lib/reviewer/command/string/flags.rb +14 -5
  32. data/lib/reviewer/command/string.rb +53 -24
  33. data/lib/reviewer/command.rb +69 -39
  34. data/lib/reviewer/configuration/loader.rb +70 -0
  35. data/lib/reviewer/configuration.rb +14 -4
  36. data/lib/reviewer/context.rb +15 -0
  37. data/lib/reviewer/doctor/config_check.rb +46 -0
  38. data/lib/reviewer/doctor/environment_check.rb +58 -0
  39. data/lib/reviewer/doctor/formatter.rb +75 -0
  40. data/lib/reviewer/doctor/keyword_check.rb +85 -0
  41. data/lib/reviewer/doctor/opportunity_check.rb +88 -0
  42. data/lib/reviewer/doctor/report.rb +63 -0
  43. data/lib/reviewer/doctor/tool_inventory.rb +41 -0
  44. data/lib/reviewer/doctor.rb +28 -0
  45. data/lib/reviewer/history.rb +36 -12
  46. data/lib/reviewer/output/formatting.rb +40 -0
  47. data/lib/reviewer/output/printer.rb +105 -0
  48. data/lib/reviewer/output.rb +54 -65
  49. data/lib/reviewer/prompt.rb +38 -0
  50. data/lib/reviewer/report/formatter.rb +124 -0
  51. data/lib/reviewer/report.rb +100 -0
  52. data/lib/reviewer/runner/failed_files.rb +66 -0
  53. data/lib/reviewer/runner/formatter.rb +103 -0
  54. data/lib/reviewer/runner/guidance.rb +79 -0
  55. data/lib/reviewer/runner/result.rb +150 -0
  56. data/lib/reviewer/runner/strategies/captured.rb +232 -0
  57. data/lib/reviewer/runner/strategies/{verbose.rb → passthrough.rb} +15 -24
  58. data/lib/reviewer/runner.rb +179 -35
  59. data/lib/reviewer/session/formatter.rb +87 -0
  60. data/lib/reviewer/session.rb +208 -0
  61. data/lib/reviewer/setup/catalog.rb +233 -0
  62. data/lib/reviewer/setup/detector.rb +61 -0
  63. data/lib/reviewer/setup/formatter.rb +94 -0
  64. data/lib/reviewer/setup/gemfile_lock.rb +55 -0
  65. data/lib/reviewer/setup/generator.rb +54 -0
  66. data/lib/reviewer/setup/tool_block.rb +112 -0
  67. data/lib/reviewer/setup.rb +41 -0
  68. data/lib/reviewer/shell/result.rb +25 -11
  69. data/lib/reviewer/shell/timer.rb +47 -27
  70. data/lib/reviewer/shell.rb +46 -21
  71. data/lib/reviewer/tool/conversions.rb +20 -0
  72. data/lib/reviewer/tool/file_resolver.rb +54 -0
  73. data/lib/reviewer/tool/settings.rb +107 -56
  74. data/lib/reviewer/tool/test_file_mapper.rb +73 -0
  75. data/lib/reviewer/tool/timing.rb +78 -0
  76. data/lib/reviewer/tool.rb +88 -47
  77. data/lib/reviewer/tools.rb +47 -33
  78. data/lib/reviewer/version.rb +1 -1
  79. data/lib/reviewer.rb +114 -54
  80. data/reviewer.gemspec +21 -20
  81. data/structure.svg +1 -0
  82. metadata +113 -148
  83. data/.ruby-version +0 -1
  84. data/lib/reviewer/command/string/verbosity.rb +0 -51
  85. data/lib/reviewer/command/verbosity.rb +0 -65
  86. data/lib/reviewer/conversions.rb +0 -27
  87. data/lib/reviewer/guidance.rb +0 -73
  88. data/lib/reviewer/keywords/git/staged.rb +0 -48
  89. data/lib/reviewer/keywords/git.rb +0 -14
  90. data/lib/reviewer/keywords.rb +0 -9
  91. data/lib/reviewer/loader.rb +0 -59
  92. data/lib/reviewer/printer.rb +0 -25
  93. data/lib/reviewer/runner/strategies/quiet.rb +0 -90
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ module Setup
5
+ # Frozen hash of known tool definitions with detection signals.
6
+ # Each entry contains the config structure needed for .reviewer.yml
7
+ # plus a :detect key with signals for auto-detection.
8
+ module Catalog
9
+ # Known tool definitions with detection signals and default configuration
10
+ TOOLS = {
11
+ bundle_audit: {
12
+ name: 'Bundle Audit',
13
+ description: 'Review gem dependencies for security issues',
14
+ tags: %w[security dependencies ruby],
15
+ commands: {
16
+ install: 'bundle exec gem install bundler-audit',
17
+ prepare: 'bundle exec bundle-audit update',
18
+ review: 'bundle exec bundle-audit check --no-update'
19
+ },
20
+ detect: {
21
+ gems: %w[bundler-audit]
22
+ }
23
+ },
24
+ rubocop: {
25
+ name: 'RuboCop',
26
+ description: 'Review Ruby syntax and formatting for consistency',
27
+ tags: %w[ruby syntax],
28
+ commands: {
29
+ install: 'bundle exec gem install rubocop',
30
+ review: 'bundle exec rubocop --parallel',
31
+ format: 'bundle exec rubocop --auto-correct'
32
+ },
33
+ files: { flag: '', separator: ' ', pattern: '*.rb' },
34
+ detect: {
35
+ gems: %w[rubocop],
36
+ files: %w[.rubocop.yml]
37
+ }
38
+ },
39
+ standard: {
40
+ name: 'Standard',
41
+ description: 'Zero-configuration Ruby linter',
42
+ tags: %w[ruby syntax],
43
+ commands: {
44
+ review: 'bundle exec standardrb',
45
+ format: 'bundle exec standardrb --fix'
46
+ },
47
+ detect: {
48
+ gems: %w[standard]
49
+ }
50
+ },
51
+ reek: {
52
+ name: 'Reek',
53
+ description: 'Examine Ruby classes for code smells',
54
+ tags: %w[ruby quality],
55
+ commands: {
56
+ install: 'bundle exec gem install reek',
57
+ review: 'bundle exec reek'
58
+ },
59
+ files: { flag: '', separator: ' ', pattern: '*.rb' },
60
+ detect: {
61
+ gems: %w[reek],
62
+ files: %w[.reek.yml]
63
+ }
64
+ },
65
+ flog: {
66
+ name: 'Flog',
67
+ description: 'Reports the most tortured Ruby code in a pain report',
68
+ tags: %w[ruby quality],
69
+ commands: {
70
+ install: 'bundle exec gem install flog',
71
+ review: 'bundle exec flog -g lib'
72
+ },
73
+ detect: {
74
+ gems: %w[flog]
75
+ }
76
+ },
77
+ flay: {
78
+ name: 'Flay',
79
+ description: 'Review Ruby code for structural similarities',
80
+ tags: %w[ruby quality],
81
+ commands: {
82
+ install: 'bundle exec gem install flay',
83
+ review: 'bundle exec flay ./lib'
84
+ },
85
+ detect: {
86
+ gems: %w[flay]
87
+ }
88
+ },
89
+ brakeman: {
90
+ name: 'Brakeman',
91
+ description: 'Static analysis security scanner for Rails',
92
+ tags: %w[security ruby],
93
+ commands: {
94
+ install: 'bundle exec gem install brakeman',
95
+ review: 'bundle exec brakeman --no-pager -q'
96
+ },
97
+ detect: {
98
+ gems: %w[brakeman],
99
+ directories: %w[app/controllers]
100
+ }
101
+ },
102
+ fasterer: {
103
+ name: 'Fasterer',
104
+ description: 'Suggest performance improvements for Ruby code',
105
+ tags: %w[ruby quality performance],
106
+ commands: {
107
+ install: 'bundle exec gem install fasterer',
108
+ review: 'bundle exec fasterer'
109
+ },
110
+ files: { flag: '', separator: ' ', pattern: '*.rb' },
111
+ detect: {
112
+ gems: %w[fasterer]
113
+ }
114
+ },
115
+ tests: {
116
+ name: 'Minitest',
117
+ description: 'Unit tests and coverage',
118
+ tags: %w[ruby tests],
119
+ commands: {
120
+ review: 'bundle exec rake test'
121
+ },
122
+ files: { review: 'bundle exec ruby -Itest', pattern: '*_test.rb', map_to_tests: 'minitest' },
123
+ detect: {
124
+ gems: %w[minitest],
125
+ directories: %w[test]
126
+ }
127
+ },
128
+ specs: {
129
+ name: 'RSpec',
130
+ description: 'Behavior-driven tests and coverage',
131
+ tags: %w[ruby tests],
132
+ commands: {
133
+ review: 'bundle exec rspec'
134
+ },
135
+ files: { flag: '', separator: ' ', pattern: '*_spec.rb', map_to_tests: 'rspec' },
136
+ detect: {
137
+ gems: %w[rspec],
138
+ directories: %w[spec]
139
+ }
140
+ },
141
+ eslint: {
142
+ name: 'ESLint',
143
+ description: 'Lint JavaScript and TypeScript code',
144
+ tags: %w[javascript linting syntax],
145
+ commands: {
146
+ review: 'npx eslint .',
147
+ format: 'npx eslint . --fix'
148
+ },
149
+ files: { flag: '', separator: ' ', pattern: '*.js' },
150
+ detect: {
151
+ files: %w[.eslintrc .eslintrc.js .eslintrc.json .eslintrc.yml eslint.config.js eslint.config.mjs]
152
+ }
153
+ },
154
+ prettier: {
155
+ name: 'Prettier',
156
+ description: 'Check code formatting consistency',
157
+ tags: %w[javascript formatting],
158
+ commands: {
159
+ review: 'npx prettier --check .',
160
+ format: 'npx prettier --write .'
161
+ },
162
+ detect: {
163
+ files: %w[.prettierrc .prettierrc.js .prettierrc.json .prettierrc.yml .prettierrc.yaml]
164
+ }
165
+ },
166
+ stylelint: {
167
+ name: 'Stylelint',
168
+ description: 'Lint CSS and SCSS for errors and consistency',
169
+ tags: %w[css linting],
170
+ commands: {
171
+ review: 'npx stylelint "**/*.css"',
172
+ format: 'npx stylelint "**/*.css" --fix'
173
+ },
174
+ files: { flag: '', separator: ' ', pattern: '*.css' },
175
+ detect: {
176
+ files: %w[.stylelintrc .stylelintrc.js .stylelintrc.json .stylelintrc.yml]
177
+ }
178
+ },
179
+ typescript: {
180
+ name: 'TypeScript',
181
+ description: 'Type-check TypeScript code',
182
+ tags: %w[javascript typescript],
183
+ commands: {
184
+ review: 'npx tsc --noEmit'
185
+ },
186
+ detect: {
187
+ files: %w[tsconfig.json]
188
+ }
189
+ },
190
+ biome: {
191
+ name: 'Biome',
192
+ description: 'Lint and format JavaScript and TypeScript',
193
+ tags: %w[javascript linting formatting],
194
+ commands: {
195
+ review: 'npx @biomejs/biome check .',
196
+ format: 'npx @biomejs/biome check . --fix'
197
+ },
198
+ files: { flag: '', separator: ' ', pattern: '*.js' },
199
+ detect: {
200
+ files: %w[biome.json biome.jsonc]
201
+ }
202
+ }
203
+ }.freeze
204
+
205
+ # Returns the full catalog of known tools
206
+ #
207
+ # @return [Hash] frozen hash of tool definitions
208
+ def self.all = TOOLS
209
+
210
+ # Returns the config for a tool key without the :detect key
211
+ #
212
+ # @param key [Symbol] the tool key
213
+ # @return [Hash, nil] config hash without :detect, or nil if not found
214
+ def self.config_for(key)
215
+ definition = TOOLS[key]
216
+ return nil unless definition
217
+
218
+ definition.except(:detect)
219
+ end
220
+
221
+ # Returns the detection signals for a tool key
222
+ #
223
+ # @param key [Symbol] the tool key
224
+ # @return [Hash, nil] detect hash, or nil if not found
225
+ def self.detect_for(key)
226
+ definition = TOOLS[key]
227
+ return nil unless definition
228
+
229
+ definition[:detect]
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ module Setup
5
+ # Scans a project directory to detect which review tools are applicable
6
+ # based on Gemfile.lock contents, config files, and directory structure.
7
+ class Detector
8
+ # Value object for a single detection result (tool key + evidence).
9
+ # @!attribute key [rw]
10
+ # @return [Symbol] the tool identifier from the catalog
11
+ # @!attribute reasons [rw]
12
+ # @return [Array<String>] evidence strings explaining why the tool was detected
13
+ Result = Struct.new(:key, :reasons, keyword_init: true) do
14
+ # @return [String] human-readable tool name from the catalog, or the key as fallback
15
+ def name = Catalog.config_for(key)&.dig(:name) || key.to_s
16
+ # @return [String] formatted line for display (name + reasons)
17
+ def summary = " #{name.ljust(22)}#{reasons.join(', ')}"
18
+ end
19
+
20
+ attr_reader :project_dir
21
+
22
+ # Creates a detector for scanning a project directory for supported tools
23
+ # @param project_dir [Pathname, String] the project root to scan
24
+ #
25
+ # @return [Detector]
26
+ def initialize(project_dir = Pathname.pwd)
27
+ @project_dir = Pathname(project_dir)
28
+ end
29
+
30
+ # Scans the project and returns detection results for matching tools
31
+ #
32
+ # @return [Array<Result>] detected tools with evidence
33
+ def detect
34
+ gems = GemfileLock.new(project_dir.join('Gemfile.lock')).gem_names
35
+
36
+ Catalog.all.filter_map do |key, definition|
37
+ reasons = reasons_for(definition[:detect], gems)
38
+ Result.new(key: key, reasons: reasons) if reasons.any?
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def reasons_for(detect, gems)
45
+ gem_reasons(detect, gems) + file_reasons(detect) + directory_reasons(detect)
46
+ end
47
+
48
+ def gem_reasons(detect, gems)
49
+ Array(detect[:gems]).select { |name| gems.include?(name) }.map { |name| "#{name} in Gemfile.lock" }
50
+ end
51
+
52
+ def file_reasons(detect)
53
+ Array(detect[:files]).select { |name| project_dir.join(name).exist? }
54
+ end
55
+
56
+ def directory_reasons(detect)
57
+ Array(detect[:directories]).select { |name| project_dir.join(name).directory? }.map { |name| "#{name}/ directory" }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../output/formatting'
4
+
5
+ module Reviewer
6
+ module Setup
7
+ # Display logic for the first-run setup flow
8
+ class Formatter
9
+ include Output::Formatting
10
+
11
+ attr_reader :output, :printer
12
+ private :output, :printer
13
+
14
+ # Creates a formatter for setup flow display
15
+ # @param output [Output] the console output handler
16
+ #
17
+ # @return [Formatter]
18
+ def initialize(output)
19
+ @output = output
20
+ @printer = output.printer
21
+ end
22
+
23
+ # Displays the welcome message when Reviewer has no configuration file
24
+ #
25
+ # @return [void]
26
+ def first_run_greeting
27
+ output.newline
28
+ printer.puts(:bold, "It looks like you're setting up Reviewer for the first time on this project.")
29
+ output.newline
30
+ printer.puts(:muted, 'This will auto-detect your tools and generate a .reviewer.yml configuration file.')
31
+ output.newline
32
+ end
33
+
34
+ # Displays a hint about `rvw init` when the user declines initial setup
35
+ #
36
+ # @return [void]
37
+ def first_run_skip
38
+ printer.puts(:muted, 'You can run `rvw init` any time to auto-detect and configure your tools.')
39
+ output.newline
40
+ end
41
+
42
+ # Displays a notice when `rvw init` is run but .reviewer.yml already exists
43
+ # @param config_file [Pathname] the existing configuration file path
44
+ #
45
+ # @return [void]
46
+ def setup_already_exists(config_file)
47
+ output.newline
48
+ printer.puts(:bold, "Configuration already exists: #{config_file.basename}")
49
+ output.newline
50
+ printer.puts(:muted, 'Run `rvw doctor` for a diagnostic report.')
51
+ printer.puts(:muted, 'To regenerate, remove the file and run `rvw init` again.')
52
+ output.newline
53
+ end
54
+
55
+ # Displays a message when auto-detection finds no supported tools in the project
56
+ #
57
+ # @return [void]
58
+ def setup_no_tools_detected
59
+ output.newline
60
+ printer.puts(:bold, 'No supported tools detected.')
61
+ output.newline
62
+ printer.puts(:muted, 'Create .reviewer.yml manually:')
63
+ printer.puts(:muted, " #{Setup::CONFIG_URL}")
64
+ output.newline
65
+ end
66
+
67
+ # Displays the results of a successful setup with the detected tools
68
+ # @param results [Array<Detector::Result>] the tools that were detected and configured
69
+ #
70
+ # @return [void]
71
+ def setup_success(results)
72
+ output.newline
73
+ printer.puts(:success, 'Created .reviewer.yml')
74
+ print_detected_tools(results)
75
+ print_setup_footer
76
+ end
77
+
78
+ private
79
+
80
+ def print_detected_tools(results)
81
+ output.newline
82
+ printer.puts(:bold, 'Detected tools:')
83
+ results.each { |result| printer.puts(:default, result.summary) }
84
+ end
85
+
86
+ def print_setup_footer
87
+ output.newline
88
+ printer.puts(:muted, "Configure further: #{Setup::CONFIG_URL}")
89
+ printer.puts(:muted, 'Run `rvw` to review your code.')
90
+ output.newline
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ module Setup
5
+ # Parses a Gemfile.lock to extract gem names from the specs section
6
+ class GemfileLock
7
+ # Spec lines are indented with 4 spaces: " gem-name (version)"
8
+ SPEC_LINE = /\A {4}(\S+)\s/
9
+
10
+ attr_reader :path
11
+
12
+ # Creates a parser for extracting gem names from a Gemfile.lock
13
+ # @param path [Pathname] the path to the Gemfile.lock file
14
+ #
15
+ # @return [GemfileLock]
16
+ def initialize(path)
17
+ @path = path
18
+ end
19
+
20
+ # Returns the set of gem names found in the specs section
21
+ #
22
+ # @return [Set<String>] gem names
23
+ def gem_names
24
+ return Set.new unless path.exist?
25
+
26
+ parse_specs
27
+ end
28
+
29
+ private
30
+
31
+ def parse_specs
32
+ in_specs = false
33
+ gems = Set.new
34
+
35
+ path.each_line do |line|
36
+ in_specs, gem_name = process_line(line, in_specs)
37
+ gems.add(gem_name) if gem_name
38
+ end
39
+
40
+ gems
41
+ end
42
+
43
+ def process_line(line, in_specs)
44
+ return [true, nil] if line.strip == 'specs:'
45
+ return [in_specs, nil] unless in_specs
46
+
47
+ match = line.match(SPEC_LINE)
48
+ return [true, match[1]] if match
49
+
50
+ still_in_specs = line.start_with?(' ') || line.strip.empty?
51
+ [still_in_specs, nil]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ require_relative 'tool_block'
6
+
7
+ module Reviewer
8
+ module Setup
9
+ # Produces .reviewer.yml YAML content from a list of detected tool keys.
10
+ # Orchestrates which tools to include; delegates per-tool rendering to ToolBlock.
11
+ class Generator
12
+ attr_reader :tool_keys, :project_dir
13
+
14
+ # Creates a generator for producing .reviewer.yml configuration
15
+ # @param tool_keys [Array<Symbol>] catalog tool keys to include in config
16
+ # @param project_dir [Pathname] the project root (used for package manager detection)
17
+ #
18
+ # @return [Generator]
19
+ def initialize(tool_keys, project_dir: Pathname.pwd)
20
+ @tool_keys = tool_keys
21
+ @project_dir = Pathname(project_dir)
22
+ end
23
+
24
+ # Generates YAML configuration string for the detected tools
25
+ #
26
+ # @return [String] valid YAML for .reviewer.yml
27
+ def generate
28
+ return "--- {}\n" if tool_keys.empty?
29
+
30
+ blocks = tool_keys.filter_map do |key|
31
+ definition = Catalog.config_for(key)
32
+ next unless definition
33
+
34
+ ToolBlock.new(key, definition, js_runner: js_runner).to_s
35
+ end
36
+
37
+ "---\n#{blocks.join("\n")}"
38
+ end
39
+
40
+ private
41
+
42
+ # Detects the JS package manager based on lockfile presence
43
+ def js_runner
44
+ @js_runner ||= if project_dir.join('yarn.lock').exist?
45
+ 'yarn'
46
+ elsif project_dir.join('pnpm-lock.yaml').exist?
47
+ 'pnpm exec'
48
+ else
49
+ 'npx'
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ module Setup
5
+ # Renders the YAML configuration block for a single tool definition.
6
+ # Owns the definition data so rendering methods reference self's state
7
+ # rather than reaching into parameters.
8
+ class ToolBlock
9
+ YAML_BARE_WORDS = %w[true false yes no on off null ~].freeze
10
+
11
+ # Creates a renderer for a single tool's YAML configuration block
12
+ # @param key [Symbol] the tool key (e.g., :rubocop)
13
+ # @param definition [Hash] the catalog definition for this tool
14
+ # @param js_runner [String] the JS package runner to substitute for npx
15
+ #
16
+ # @return [ToolBlock]
17
+ def initialize(key, definition, js_runner:)
18
+ @key = key
19
+ @definition = definition
20
+ @js_runner = js_runner
21
+ end
22
+
23
+ # Renders the full YAML block for this tool
24
+ #
25
+ # @return [String] the YAML text for this tool's configuration
26
+ def to_s
27
+ lines = header_lines
28
+ lines.concat(commands_block)
29
+ lines.concat(files_block) if @definition[:files]
30
+ lines << ''
31
+ lines.join("\n")
32
+ end
33
+
34
+ private
35
+
36
+ def header_lines
37
+ lines = []
38
+ lines << "# #{@definition[:description]}"
39
+ lines << "#{@key}:"
40
+ lines << " name: #{quote(@definition[:name])}"
41
+ lines << " description: #{quote(@definition[:description])}"
42
+ lines << tags_line if @definition[:tags]&.any?
43
+ lines
44
+ end
45
+
46
+ def tags_line
47
+ " tags: [#{@definition[:tags].join(', ')}]"
48
+ end
49
+
50
+ def commands_block
51
+ commands = @definition[:commands]
52
+ lines = [' commands:']
53
+ %i[install prepare review format].each do |type|
54
+ next unless commands[type]
55
+
56
+ lines << " #{type}: #{quote(apply_js_runner(commands[type].to_s))}"
57
+ end
58
+ lines
59
+ end
60
+
61
+ def files_block
62
+ lines = [' files:']
63
+ lines.concat(files_command_lines)
64
+ lines.concat(files_targeting_lines)
65
+ lines
66
+ end
67
+
68
+ def files_command_lines
69
+ %i[review format].filter_map do |type|
70
+ " #{type}: #{quote(apply_js_runner(tool_files[type].to_s))}" if tool_files[type]
71
+ end
72
+ end
73
+
74
+ def files_targeting_lines
75
+ [
76
+ (file_setting_line(:flag) if tool_files.key?(:flag)),
77
+ (file_setting_line(:separator) if tool_files.key?(:separator)),
78
+ (file_setting_line(:pattern) if tool_files[:pattern]),
79
+ (" map_to_tests: #{tool_files[:map_to_tests]}" if tool_files[:map_to_tests])
80
+ ].compact
81
+ end
82
+
83
+ def file_setting_line(key)
84
+ " #{key}: #{quote(tool_files[key])}"
85
+ end
86
+
87
+ def tool_files
88
+ @definition[:files]
89
+ end
90
+
91
+ def apply_js_runner(command)
92
+ return command unless command.start_with?('npx ')
93
+
94
+ command.sub('npx ', "#{@js_runner} ")
95
+ end
96
+
97
+ def quote(value)
98
+ str = value.to_s
99
+ return "''" if str.empty?
100
+
101
+ needs_quoting?(str) ? "'#{str.gsub("'", "''")}'" : str
102
+ end
103
+
104
+ def needs_quoting?(str)
105
+ str.match?(/[:#\[\]{}&*!|>'"@`,]/) ||
106
+ str.strip != str ||
107
+ str.empty? ||
108
+ YAML_BARE_WORDS.include?(str.downcase)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'setup/catalog'
4
+ require_relative 'setup/detector'
5
+ require_relative 'setup/formatter'
6
+ require_relative 'setup/gemfile_lock'
7
+ require_relative 'setup/generator'
8
+
9
+ module Reviewer
10
+ # Handles first-run setup: detecting tools and generating .reviewer.yml
11
+ module Setup
12
+ # URL to the configuration documentation for setup output messages
13
+ CONFIG_URL = 'https://github.com/garrettdimon/reviewer#configuration'
14
+
15
+ # Runs the full setup flow: detect tools, generate config, display results
16
+ # @param project_dir [Pathname, String] the project root to scan (defaults to pwd)
17
+ # @param output [Output] the console output handler
18
+ #
19
+ # @return [void]
20
+ def self.run(configuration:, project_dir: Pathname.pwd, output: Output.new)
21
+ config_file = configuration.file
22
+ formatter = Formatter.new(output)
23
+
24
+ if config_file.exist?
25
+ formatter.setup_already_exists(config_file)
26
+ return
27
+ end
28
+
29
+ results = Detector.new(project_dir).detect
30
+
31
+ if results.empty?
32
+ formatter.setup_no_tools_detected
33
+ return
34
+ end
35
+
36
+ yaml = Generator.new(results.map(&:key), project_dir: project_dir).generate
37
+ config_file.write(yaml)
38
+ formatter.setup_success(results)
39
+ end
40
+ end
41
+ end