simplecov-mcp 1.0.1 → 2.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/README.md +98 -50
- data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
- data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
- data/docs/dev/README.md +10 -0
- data/docs/dev/RELEASING.md +146 -0
- data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
- data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
- data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
- data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
- data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
- data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
- data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
- data/docs/fixtures/demo_project/README.md +9 -0
- data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
- data/docs/user/CLI_USAGE.md +750 -0
- data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
- data/docs/user/EXAMPLES.md +588 -0
- data/docs/user/INSTALLATION.md +130 -0
- data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
- data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
- data/docs/user/README.md +14 -0
- data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
- data/docs/user/V2-BREAKING-CHANGES.md +472 -0
- data/exe/simplecov-mcp +1 -1
- data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
- data/lib/simplecov_mcp/app_context.rb +1 -1
- data/lib/simplecov_mcp/base_tool.rb +66 -38
- data/lib/simplecov_mcp/cli.rb +67 -123
- data/lib/simplecov_mcp/commands/base_command.rb +16 -27
- data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
- data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
- data/lib/simplecov_mcp/commands/list_command.rb +1 -1
- data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
- data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
- data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
- data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
- data/lib/simplecov_mcp/commands/version_command.rb +19 -4
- data/lib/simplecov_mcp/config_parser.rb +32 -0
- data/lib/simplecov_mcp/constants.rb +3 -3
- data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
- data/lib/simplecov_mcp/error_handler.rb +81 -40
- data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
- data/lib/simplecov_mcp/errors.rb +12 -19
- data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
- data/lib/simplecov_mcp/formatters.rb +51 -0
- data/lib/simplecov_mcp/mcp_server.rb +9 -7
- data/lib/simplecov_mcp/mode_detector.rb +6 -5
- data/lib/simplecov_mcp/model.rb +122 -88
- data/lib/simplecov_mcp/option_normalizers.rb +39 -18
- data/lib/simplecov_mcp/option_parser_builder.rb +82 -65
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
- data/lib/simplecov_mcp/path_relativizer.rb +17 -14
- data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
- data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
- data/lib/simplecov_mcp/resultset_loader.rb +20 -25
- data/lib/simplecov_mcp/staleness_checker.rb +50 -46
- data/lib/simplecov_mcp/table_formatter.rb +64 -0
- data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
- data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
- data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
- data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
- data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
- data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
- data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
- data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
- data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
- data/lib/simplecov_mcp/util.rb +18 -12
- data/lib/simplecov_mcp/version.rb +1 -1
- data/lib/simplecov_mcp.rb +23 -29
- data/spec/all_files_coverage_tool_spec.rb +4 -3
- data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
- data/spec/base_tool_spec.rb +17 -14
- data/spec/cli/show_default_report_spec.rb +2 -2
- data/spec/cli_enumerated_options_spec.rb +31 -9
- data/spec/cli_error_spec.rb +46 -23
- data/spec/cli_format_spec.rb +123 -0
- data/spec/cli_json_options_spec.rb +50 -0
- data/spec/cli_source_spec.rb +11 -63
- data/spec/cli_spec.rb +82 -97
- data/spec/cli_usage_spec.rb +15 -15
- data/spec/commands/base_command_spec.rb +12 -92
- data/spec/commands/command_factory_spec.rb +7 -3
- data/spec/commands/detailed_command_spec.rb +10 -24
- data/spec/commands/list_command_spec.rb +28 -0
- data/spec/commands/raw_command_spec.rb +43 -20
- data/spec/commands/summary_command_spec.rb +10 -23
- data/spec/commands/totals_command_spec.rb +34 -0
- data/spec/commands/uncovered_command_spec.rb +29 -23
- data/spec/commands/validate_command_spec.rb +213 -0
- data/spec/commands/version_command_spec.rb +38 -0
- data/spec/constants_spec.rb +3 -3
- data/spec/coverage_reporter_spec.rb +102 -0
- data/spec/coverage_table_tool_spec.rb +21 -10
- data/spec/coverage_totals_tool_spec.rb +37 -0
- data/spec/error_handler_spec.rb +120 -4
- data/spec/error_mode_spec.rb +18 -22
- data/spec/errors_edge_cases_spec.rb +101 -28
- data/spec/errors_stale_spec.rb +34 -0
- data/spec/file_based_mcp_tools_spec.rb +6 -6
- data/spec/fixtures/project1/lib/bar.rb +2 -0
- data/spec/fixtures/project1/lib/foo.rb +2 -0
- data/spec/help_tool_spec.rb +2 -18
- data/spec/integration_spec.rb +103 -161
- data/spec/logging_fallback_spec.rb +3 -3
- data/spec/mcp_server_integration_spec.rb +1 -1
- data/spec/mcp_server_spec.rb +70 -53
- data/spec/mode_detector_spec.rb +46 -41
- data/spec/model_error_handling_spec.rb +139 -78
- data/spec/model_staleness_spec.rb +13 -13
- data/spec/option_normalizers_spec.rb +111 -112
- data/spec/option_parsers/env_options_parser_spec.rb +25 -37
- data/spec/option_parsers/error_helper_spec.rb +56 -56
- data/spec/path_relativizer_spec.rb +15 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
- data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
- data/spec/presenters/project_totals_presenter_spec.rb +144 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
- data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
- data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
- data/spec/shared_examples/formatted_command_examples.rb +64 -0
- data/spec/simple_cov_mcp_module_spec.rb +24 -3
- data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
- data/spec/simplecov_mcp/formatters_spec.rb +76 -0
- data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
- data/spec/simplecov_mcp_model_spec.rb +97 -47
- data/spec/simplecov_mcp_opts_spec.rb +42 -39
- data/spec/spec_helper.rb +27 -92
- data/spec/staleness_checker_spec.rb +10 -9
- data/spec/staleness_more_spec.rb +4 -4
- data/spec/support/cli_helpers.rb +22 -0
- data/spec/support/control_flow_helpers.rb +20 -0
- data/spec/support/fake_mcp.rb +40 -0
- data/spec/support/io_helpers.rb +29 -0
- data/spec/support/mcp_helpers.rb +35 -0
- data/spec/support/mcp_runner.rb +10 -8
- data/spec/support/mocking_helpers.rb +30 -0
- data/spec/table_format_spec.rb +70 -0
- data/spec/tools/validate_tool_spec.rb +132 -0
- data/spec/tools_error_handling_spec.rb +34 -48
- data/spec/util_spec.rb +5 -4
- data/spec/version_spec.rb +7 -7
- data/spec/version_tool_spec.rb +20 -22
- metadata +90 -23
- data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
- data/docs/CLI_USAGE.md +0 -637
- data/docs/EXAMPLES.md +0 -430
- data/docs/INSTALLATION.md +0 -352
- data/spec/cli_success_predicate_spec.rb +0 -141
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCovMcp
|
|
4
|
-
# Configuration container for CLI
|
|
4
|
+
# Configuration container for application options (used by both CLI and MCP modes)
|
|
5
5
|
# Uses Struct for simplicity and built-in functionality
|
|
6
|
-
|
|
6
|
+
AppConfig = Struct.new(
|
|
7
7
|
:root,
|
|
8
8
|
:resultset,
|
|
9
|
-
:
|
|
9
|
+
:format,
|
|
10
10
|
:sort_order,
|
|
11
11
|
:source_mode,
|
|
12
12
|
:source_context,
|
|
13
13
|
:color,
|
|
14
14
|
:error_mode,
|
|
15
|
-
:
|
|
15
|
+
:staleness,
|
|
16
16
|
:tracked_globs,
|
|
17
17
|
:log_file,
|
|
18
|
-
:
|
|
18
|
+
:show_version,
|
|
19
19
|
keyword_init: true
|
|
20
20
|
) do
|
|
21
21
|
# Set sensible defaults - ALL SYMBOLS FOR ENUMS
|
|
22
22
|
def initialize(
|
|
23
23
|
root: '.',
|
|
24
24
|
resultset: nil,
|
|
25
|
-
|
|
26
|
-
sort_order: :
|
|
25
|
+
format: :table,
|
|
26
|
+
sort_order: :descending,
|
|
27
27
|
source_mode: nil,
|
|
28
28
|
source_context: 2,
|
|
29
|
-
color:
|
|
30
|
-
error_mode: :
|
|
31
|
-
|
|
29
|
+
color: $stdout.tty?,
|
|
30
|
+
error_mode: :log,
|
|
31
|
+
staleness: :off,
|
|
32
32
|
tracked_globs: nil,
|
|
33
33
|
log_file: nil,
|
|
34
|
-
|
|
34
|
+
show_version: false
|
|
35
35
|
)
|
|
36
36
|
super
|
|
37
37
|
end
|
|
@@ -41,7 +41,7 @@ module SimpleCovMcp
|
|
|
41
41
|
{
|
|
42
42
|
root: root,
|
|
43
43
|
resultset: resultset,
|
|
44
|
-
staleness:
|
|
44
|
+
staleness: staleness,
|
|
45
45
|
tracked_globs: tracked_globs
|
|
46
46
|
}
|
|
47
47
|
end
|
|
@@ -7,46 +7,75 @@ require_relative 'error_handler'
|
|
|
7
7
|
|
|
8
8
|
module SimpleCovMcp
|
|
9
9
|
class BaseTool < ::MCP::Tool
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
description: 'Repo-relative or absolute path to the file whose coverage data you need.',
|
|
17
|
-
examples: ['lib/simple_cov_mcp/model.rb']
|
|
18
|
-
},
|
|
19
|
-
root: {
|
|
20
|
-
type: 'string',
|
|
21
|
-
description: 'Project root used to resolve relative paths ' \
|
|
22
|
-
'(defaults to current workspace).',
|
|
23
|
-
default: '.'
|
|
24
|
-
},
|
|
25
|
-
resultset: {
|
|
26
|
-
type: 'string',
|
|
27
|
-
description: 'Path to the SimpleCov .resultset.json file (absolute or relative to root).'
|
|
28
|
-
},
|
|
29
|
-
stale: {
|
|
30
|
-
type: 'string',
|
|
31
|
-
description: 'How to handle missing/outdated coverage data.' \
|
|
32
|
-
"'off' skips checks; 'error' raises.",
|
|
33
|
-
enum: %w[off error],
|
|
34
|
-
default: 'off'
|
|
35
|
-
},
|
|
36
|
-
error_mode: {
|
|
37
|
-
type: 'string',
|
|
38
|
-
description: "Error handling mode: 'off' (silent), 'on' (log errors), 'trace' (verbose).",
|
|
39
|
-
enum: %w[off on trace],
|
|
40
|
-
default: 'on'
|
|
41
|
-
}
|
|
10
|
+
COMMON_PROPERTIES = {
|
|
11
|
+
root: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Project root used to resolve relative paths ' \
|
|
14
|
+
'(defaults to current workspace).',
|
|
15
|
+
default: '.'
|
|
42
16
|
},
|
|
17
|
+
resultset: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Path to the SimpleCov .resultset.json file (absolute or relative to root).'
|
|
20
|
+
},
|
|
21
|
+
staleness: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'How to handle missing/outdated coverage data. ' \
|
|
24
|
+
"'off' skips checks; 'error' raises.",
|
|
25
|
+
enum: [:off, :error],
|
|
26
|
+
default: :off
|
|
27
|
+
},
|
|
28
|
+
error_mode: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: "Error handling mode: 'off' (silent), 'log' (log errors), " \
|
|
31
|
+
"'debug' (verbose with backtraces).",
|
|
32
|
+
enum: %w[off log debug],
|
|
33
|
+
default: 'log'
|
|
34
|
+
}
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
ERROR_MODE_PROPERTY = COMMON_PROPERTIES[:error_mode].freeze
|
|
38
|
+
|
|
39
|
+
TRACKED_GLOBS_PROPERTY = {
|
|
40
|
+
type: 'array',
|
|
41
|
+
description: 'Glob patterns for files that should exist in the coverage report' \
|
|
42
|
+
'(helps flag new files).',
|
|
43
|
+
items: { type: 'string' }
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
PATH_PROPERTY = {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Repo-relative or absolute path to the file whose coverage data you need.',
|
|
49
|
+
examples: ['lib/simple_cov_mcp/model.rb']
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
def self.coverage_schema(additional_properties: {}, required: [])
|
|
53
|
+
{
|
|
54
|
+
type: 'object',
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
properties: COMMON_PROPERTIES.merge(additional_properties),
|
|
57
|
+
required: required
|
|
58
|
+
}.freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
FILE_INPUT_SCHEMA = coverage_schema(
|
|
62
|
+
additional_properties: { path: PATH_PROPERTY },
|
|
43
63
|
required: ['path']
|
|
44
|
-
|
|
45
|
-
def self.input_schema_def =
|
|
64
|
+
)
|
|
65
|
+
def self.input_schema_def = FILE_INPUT_SCHEMA
|
|
66
|
+
|
|
67
|
+
# Wrap tool execution with consistent error handling.
|
|
68
|
+
# Yields to the block and rescues any error, delegating to handle_mcp_error.
|
|
69
|
+
# This eliminates duplicate rescue blocks across all tools.
|
|
70
|
+
def self.with_error_handling(tool_name, error_mode:)
|
|
71
|
+
yield
|
|
72
|
+
rescue => e
|
|
73
|
+
handle_mcp_error(e, tool_name, error_mode: error_mode)
|
|
74
|
+
end
|
|
46
75
|
|
|
47
76
|
# Handle errors consistently across all MCP tools
|
|
48
77
|
# Returns an MCP::Tool::Response with appropriate error message
|
|
49
|
-
def self.handle_mcp_error(error, tool_name, error_mode: :
|
|
78
|
+
def self.handle_mcp_error(error, tool_name, error_mode: :log)
|
|
50
79
|
# Create error handler with the specified mode
|
|
51
80
|
error_handler = ErrorHandlerFactory.for_mcp_server(error_mode: error_mode.to_sym)
|
|
52
81
|
|
|
@@ -54,7 +83,7 @@ module SimpleCovMcp
|
|
|
54
83
|
normalized = error.is_a?(SimpleCovMcp::Error) \
|
|
55
84
|
? error : error_handler.convert_standard_error(error)
|
|
56
85
|
log_mcp_error(normalized, tool_name, error_handler)
|
|
57
|
-
::MCP::Tool::Response.new([{ type
|
|
86
|
+
::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => "Error: #{normalized.user_friendly_message}" }])
|
|
58
87
|
end
|
|
59
88
|
|
|
60
89
|
# Respond with JSON as a resource to avoid clients mutating content types.
|
|
@@ -64,11 +93,10 @@ module SimpleCovMcp
|
|
|
64
93
|
::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => json }])
|
|
65
94
|
end
|
|
66
95
|
|
|
67
|
-
private
|
|
68
|
-
|
|
69
96
|
def self.log_mcp_error(error, tool_name, error_handler)
|
|
70
97
|
# Use the provided error handler for logging
|
|
71
98
|
error_handler.send(:log_error, error, tool_name)
|
|
72
99
|
end
|
|
100
|
+
private_class_method :log_mcp_error
|
|
73
101
|
end
|
|
74
102
|
end
|
data/lib/simplecov_mcp/cli.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'app_config'
|
|
5
5
|
require_relative 'option_parser_builder'
|
|
6
6
|
require_relative 'commands/command_factory'
|
|
7
7
|
require_relative 'option_parsers/error_helper'
|
|
@@ -11,7 +11,7 @@ require_relative 'presenters/project_coverage_presenter'
|
|
|
11
11
|
|
|
12
12
|
module SimpleCovMcp
|
|
13
13
|
class CoverageCLI
|
|
14
|
-
SUBCOMMANDS = %w[list summary raw uncovered detailed version].freeze
|
|
14
|
+
SUBCOMMANDS = %w[list summary raw uncovered detailed totals validate version].freeze
|
|
15
15
|
HORIZONTAL_RULE = '-' * 79
|
|
16
16
|
|
|
17
17
|
# Reference shared constant to avoid duplication with ModeDetector
|
|
@@ -22,7 +22,7 @@ module SimpleCovMcp
|
|
|
22
22
|
# Initialize CLI for pure CLI usage only.
|
|
23
23
|
# Always runs as CLI, no mode detection needed.
|
|
24
24
|
def initialize(error_handler: nil)
|
|
25
|
-
@config =
|
|
25
|
+
@config = AppConfig.new
|
|
26
26
|
@cmd = nil
|
|
27
27
|
@cmd_args = []
|
|
28
28
|
@custom_error_handler = error_handler # Store custom handler if provided
|
|
@@ -31,28 +31,19 @@ module SimpleCovMcp
|
|
|
31
31
|
|
|
32
32
|
def run(argv)
|
|
33
33
|
context = nil
|
|
34
|
-
#
|
|
35
|
-
full_argv = parse_env_opts + argv
|
|
34
|
+
# argv should already include environment options (merged by caller)
|
|
36
35
|
# Pre-scan for error-mode to ensure early errors are logged with correct verbosity
|
|
37
|
-
pre_scan_error_mode(
|
|
38
|
-
parse_options!(
|
|
39
|
-
|
|
40
|
-
# Create error handler AFTER parsing options to respect user's --error-mode choice
|
|
41
|
-
ensure_error_handler
|
|
36
|
+
pre_scan_error_mode(argv)
|
|
37
|
+
parse_options!(argv)
|
|
38
|
+
enforce_version_subcommand_if_requested
|
|
42
39
|
|
|
43
40
|
context = SimpleCovMcp.create_context(
|
|
44
|
-
error_handler:
|
|
41
|
+
error_handler: error_handler, # construct after options to respect --error-mode
|
|
45
42
|
log_target: config.log_file.nil? ? SimpleCovMcp.context.log_target : config.log_file,
|
|
46
43
|
mode: :cli
|
|
47
44
|
)
|
|
48
45
|
|
|
49
46
|
SimpleCovMcp.with_context(context) do
|
|
50
|
-
# If success predicate specified, run it and exit
|
|
51
|
-
if config.success_predicate
|
|
52
|
-
run_success_predicate
|
|
53
|
-
next
|
|
54
|
-
end
|
|
55
|
-
|
|
56
47
|
if @cmd
|
|
57
48
|
run_subcommand(@cmd, @cmd_args)
|
|
58
49
|
else
|
|
@@ -62,22 +53,23 @@ module SimpleCovMcp
|
|
|
62
53
|
rescue OptionParser::ParseError => e
|
|
63
54
|
# Handle any option parsing errors (invalid option/argument) without relying on
|
|
64
55
|
# @error_handler, which is not guaranteed to be initialized yet.
|
|
65
|
-
with_context_if_available(context) { handle_option_parser_error(e, argv:
|
|
56
|
+
with_context_if_available(context) { handle_option_parser_error(e, argv: argv) }
|
|
66
57
|
rescue SimpleCovMcp::Error => e
|
|
67
58
|
with_context_if_available(context) { handle_user_facing_error(e) }
|
|
68
59
|
end
|
|
69
60
|
|
|
70
|
-
def show_default_report(sort_order: :
|
|
61
|
+
def show_default_report(sort_order: :descending, output: $stdout)
|
|
71
62
|
model = CoverageModel.new(**config.model_options)
|
|
72
63
|
presenter = Presenters::ProjectCoveragePresenter.new(
|
|
73
64
|
model: model,
|
|
74
65
|
sort_order: sort_order,
|
|
75
|
-
check_stale: (config.
|
|
66
|
+
check_stale: (config.staleness == :error),
|
|
76
67
|
tracked_globs: config.tracked_globs
|
|
77
68
|
)
|
|
78
69
|
|
|
79
|
-
if config.
|
|
80
|
-
|
|
70
|
+
if config.format != :table
|
|
71
|
+
require_relative 'formatters'
|
|
72
|
+
output.puts Formatters.format(presenter.relativized_payload, config.format)
|
|
81
73
|
return
|
|
82
74
|
end
|
|
83
75
|
|
|
@@ -85,149 +77,101 @@ module SimpleCovMcp
|
|
|
85
77
|
output.puts model.format_table(
|
|
86
78
|
file_summaries,
|
|
87
79
|
sort_order: sort_order,
|
|
88
|
-
check_stale: (config.
|
|
80
|
+
check_stale: (config.staleness == :error),
|
|
89
81
|
tracked_globs: nil
|
|
90
82
|
)
|
|
91
83
|
end
|
|
92
84
|
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
def parse_options!(argv)
|
|
85
|
+
private def parse_options!(argv)
|
|
96
86
|
require 'optparse'
|
|
97
|
-
extract_subcommand!(argv)
|
|
98
87
|
parser = build_option_parser
|
|
99
|
-
parser.parse!(argv)
|
|
100
|
-
@cmd_args = argv
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def extract_subcommand!(argv)
|
|
104
|
-
# Environment options (e.g., from SIMPLECOV_MCP_OPTS) may precede the subcommand.
|
|
105
|
-
# Walk the array so we can skip over any option/argument pairs before
|
|
106
|
-
# we decide what the first meaningful token is.
|
|
107
|
-
return if argv.empty?
|
|
108
88
|
|
|
109
|
-
|
|
110
|
-
|
|
89
|
+
# order! parses global options (updating config) and removes them from argv.
|
|
90
|
+
# It stops cleanly at the first subcommand (e.g., 'list', 'summary') or unknown option.
|
|
91
|
+
# If it stops at an unknown option, it raises OptionParser::InvalidOption.
|
|
92
|
+
parser.order!(argv)
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if pending_option
|
|
115
|
-
pending_option = nil
|
|
116
|
-
next
|
|
117
|
-
end
|
|
94
|
+
# The first remaining argument is the subcommand
|
|
95
|
+
@cmd = argv.shift
|
|
118
96
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# that value on the next iteration.
|
|
123
|
-
pending_option = expects_argument?(token) && !token.include?('=') ? token : nil
|
|
124
|
-
next
|
|
125
|
-
elsif SUBCOMMANDS.include?(token)
|
|
126
|
-
# Found the real subcommand; pluck it out so option parsing sees the
|
|
127
|
-
# remaining args in their original order.
|
|
128
|
-
@cmd = token
|
|
129
|
-
argv.delete_at(index)
|
|
130
|
-
return
|
|
131
|
-
else
|
|
132
|
-
first_unknown ||= token
|
|
133
|
-
end
|
|
97
|
+
# Verify it's a valid subcommand if present
|
|
98
|
+
if @cmd && !SUBCOMMANDS.include?(@cmd)
|
|
99
|
+
raise UsageError, "Unknown subcommand: '#{@cmd}'. Valid subcommands: #{SUBCOMMANDS.join(', ')}"
|
|
134
100
|
end
|
|
135
101
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def expects_argument?(option)
|
|
142
|
-
OPTIONS_EXPECTING_ARGUMENT.include?(option)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def ensure_error_handler
|
|
146
|
-
@error_handler ||=
|
|
147
|
-
@custom_error_handler || ErrorHandlerFactory.for_cli(error_mode: config.error_mode)
|
|
102
|
+
# Any remaining arguments belong to the subcommand
|
|
103
|
+
@cmd_args = argv
|
|
148
104
|
end
|
|
149
105
|
|
|
150
|
-
def
|
|
151
|
-
@
|
|
152
|
-
|
|
106
|
+
private def error_handler
|
|
107
|
+
@error_handler ||= @custom_error_handler ||
|
|
108
|
+
ErrorHandlerFactory.for_cli(error_mode: config.error_mode)
|
|
153
109
|
end
|
|
154
110
|
|
|
155
|
-
def pre_scan_error_mode(argv)
|
|
156
|
-
|
|
157
|
-
config.error_mode =
|
|
111
|
+
private def pre_scan_error_mode(argv)
|
|
112
|
+
env_parser = OptionParsers::EnvOptionsParser.new
|
|
113
|
+
config.error_mode = env_parser.pre_scan_error_mode(argv) || :log
|
|
158
114
|
end
|
|
159
115
|
|
|
160
|
-
def build_option_parser
|
|
116
|
+
private def build_option_parser
|
|
161
117
|
builder = OptionParserBuilder.new(config)
|
|
162
118
|
builder.build_option_parser
|
|
163
119
|
end
|
|
164
120
|
|
|
165
|
-
|
|
121
|
+
# Converts the -v/--version flags into the version subcommand.
|
|
122
|
+
# When the user passes -v or --version, config.show_version is set to true during option parsing.
|
|
123
|
+
# This method intercepts that flag and redirects execution to the 'version' subcommand,
|
|
124
|
+
# ensuring consistent version display regardless of whether the user runs
|
|
125
|
+
# `simplecov-mcp -v`, `simplecov-mcp --version`, or `simplecov-mcp version`.
|
|
126
|
+
private def enforce_version_subcommand_if_requested
|
|
127
|
+
@cmd = 'version' if config.show_version
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private def with_context_if_available(ctx, &block)
|
|
166
131
|
if ctx
|
|
167
|
-
SimpleCovMcp.with_context(ctx)
|
|
132
|
+
SimpleCovMcp.with_context(ctx, &block)
|
|
168
133
|
else
|
|
169
|
-
|
|
134
|
+
block.call
|
|
170
135
|
end
|
|
171
136
|
end
|
|
172
137
|
|
|
173
|
-
def run_subcommand(cmd, args)
|
|
138
|
+
private def run_subcommand(cmd, args)
|
|
139
|
+
# Check if user mistakenly placed global options after the subcommand
|
|
140
|
+
check_for_misplaced_global_options(cmd, args)
|
|
141
|
+
|
|
174
142
|
command = Commands::CommandFactory.create(cmd, self)
|
|
175
143
|
command.execute(args)
|
|
176
144
|
rescue SimpleCovMcp::Error => e
|
|
177
145
|
handle_user_facing_error(e)
|
|
178
146
|
rescue => e
|
|
179
|
-
|
|
147
|
+
error_handler.handle_error(e, context: "subcommand '#{cmd}'")
|
|
180
148
|
end
|
|
181
149
|
|
|
182
|
-
def handle_option_parser_error(error, argv: [])
|
|
150
|
+
private def handle_option_parser_error(error, argv: [])
|
|
183
151
|
@error_helper ||= OptionParsers::ErrorHelper.new(SUBCOMMANDS)
|
|
184
152
|
@error_helper.handle_option_parser_error(error, argv: argv)
|
|
185
153
|
end
|
|
186
154
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
exit(result ? 0 : 1)
|
|
193
|
-
rescue => e
|
|
194
|
-
warn "Success predicate error: #{e.message}"
|
|
195
|
-
warn e.backtrace.first(5).join("\n") if config.error_mode == :trace
|
|
196
|
-
exit 2 # Exit code 2 for predicate errors
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def load_success_predicate(path)
|
|
200
|
-
unless File.exist?(path)
|
|
201
|
-
raise "Success predicate file not found: #{path}"
|
|
202
|
-
end
|
|
155
|
+
private def check_for_misplaced_global_options(cmd, args)
|
|
156
|
+
# Global options that users commonly place after subcommands by mistake
|
|
157
|
+
global_options = %w[-r --resultset -R --root -f --format -o --sort-order -s --source
|
|
158
|
+
-c --context-lines -S --staleness -g --tracked-globs
|
|
159
|
+
-l --log-file --error-mode --color --no-color]
|
|
203
160
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
# WARNING: The predicate code executes with full Ruby privileges.
|
|
207
|
-
# It has unrestricted access to the file system, network, and system commands.
|
|
208
|
-
# Only use predicate files from trusted sources.
|
|
209
|
-
#
|
|
210
|
-
# We evaluate in a fresh Object context to prevent accidental access to
|
|
211
|
-
# CLI internals, but this provides NO security isolation.
|
|
212
|
-
evaluation_context = Object.new
|
|
213
|
-
predicate = evaluation_context.instance_eval(content, path, 1)
|
|
214
|
-
|
|
215
|
-
unless predicate.respond_to?(:call)
|
|
216
|
-
raise 'Success predicate must be callable (lambda, proc, or object with #call method)'
|
|
217
|
-
end
|
|
161
|
+
misplaced = args.select { |arg| global_options.include?(arg) }
|
|
162
|
+
return if misplaced.empty?
|
|
218
163
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
164
|
+
option_list = misplaced.join(', ')
|
|
165
|
+
raise UsageError, "Global option(s) must come BEFORE the subcommand.\n" \
|
|
166
|
+
"You used: #{cmd} #{option_list}\n" \
|
|
167
|
+
"Correct: #{option_list} #{cmd}\n\n" \
|
|
168
|
+
"Example: simplecov-mcp --format json #{cmd}"
|
|
222
169
|
end
|
|
223
170
|
|
|
224
|
-
def handle_user_facing_error(error)
|
|
225
|
-
|
|
226
|
-
ensure_error_handler
|
|
227
|
-
# Log the error if error_mode allows it
|
|
228
|
-
@error_handler.handle_error(error, context: 'CLI', reraise: false)
|
|
229
|
-
# Show user-friendly message
|
|
171
|
+
private def handle_user_facing_error(error)
|
|
172
|
+
error_handler.handle_error(error, context: 'CLI', reraise: false)
|
|
230
173
|
warn error.user_friendly_message
|
|
174
|
+
warn error.backtrace.first(5).join("\n") if config.error_mode == :debug && error.backtrace
|
|
231
175
|
exit 1
|
|
232
176
|
end
|
|
233
177
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require_relative '../formatters'
|
|
4
5
|
require_relative '../formatters/source_formatter'
|
|
5
6
|
require_relative '../model'
|
|
6
7
|
require_relative '../errors'
|
|
@@ -16,59 +17,47 @@ module SimpleCovMcp
|
|
|
16
17
|
)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
protected
|
|
20
|
-
|
|
21
20
|
attr_reader :cli, :config, :source_formatter
|
|
22
21
|
|
|
23
|
-
def model
|
|
22
|
+
protected def model
|
|
24
23
|
@model ||= CoverageModel.new(**config.model_options)
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
def handle_with_path(args, name)
|
|
26
|
+
protected def handle_with_path(args, name)
|
|
28
27
|
path = args.shift or raise UsageError.for_subcommand("#{name} <path>")
|
|
29
28
|
yield(path)
|
|
30
|
-
rescue Errno::ENOENT
|
|
31
|
-
raise FileNotFoundError
|
|
32
|
-
rescue Errno::EACCES
|
|
33
|
-
raise FilePermissionError
|
|
29
|
+
rescue Errno::ENOENT
|
|
30
|
+
raise FileNotFoundError, "File not found: #{path}"
|
|
31
|
+
rescue Errno::EACCES
|
|
32
|
+
raise FilePermissionError, "Permission denied: #{path}"
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
def
|
|
37
|
-
return false
|
|
35
|
+
protected def maybe_output_structured_format?(obj, model)
|
|
36
|
+
return false if config.format == :table
|
|
38
37
|
|
|
39
|
-
puts
|
|
38
|
+
puts SimpleCovMcp::Formatters.format(model.relativize(obj), config.format)
|
|
40
39
|
true
|
|
41
40
|
end
|
|
42
41
|
|
|
43
|
-
def
|
|
44
|
-
return false
|
|
42
|
+
protected def emit_structured_format_with_optional_source?(data, model, path)
|
|
43
|
+
return false if config.format == :table
|
|
45
44
|
|
|
46
45
|
relativized = model.relativize(data)
|
|
47
46
|
if config.source_mode
|
|
48
47
|
payload = relativized.merge('source' => build_source_payload(model, path))
|
|
49
|
-
puts
|
|
48
|
+
puts SimpleCovMcp::Formatters.format(payload, config.format)
|
|
50
49
|
else
|
|
51
|
-
puts
|
|
50
|
+
puts SimpleCovMcp::Formatters.format(relativized, config.format)
|
|
52
51
|
end
|
|
53
52
|
true
|
|
54
53
|
end
|
|
55
54
|
|
|
56
|
-
def build_source_payload(model, path)
|
|
55
|
+
protected def build_source_payload(model, path)
|
|
57
56
|
source_formatter.build_source_payload(model, path, mode: config.source_mode,
|
|
58
57
|
context: config.source_context)
|
|
59
58
|
end
|
|
60
59
|
|
|
61
|
-
def
|
|
62
|
-
@raw_cache ||= {}
|
|
63
|
-
return @raw_cache[path] if @raw_cache.key?(path)
|
|
64
|
-
|
|
65
|
-
raw = model.raw_for(path)
|
|
66
|
-
@raw_cache[path] = raw
|
|
67
|
-
rescue StandardError
|
|
68
|
-
nil
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def print_source_for(model, path)
|
|
60
|
+
protected def print_source_for(model, path)
|
|
72
61
|
formatted = source_formatter.format_source_for(model, path, mode: config.source_mode,
|
|
73
62
|
context: config.source_context)
|
|
74
63
|
puts formatted
|
|
@@ -7,6 +7,8 @@ require_relative 'summary_command'
|
|
|
7
7
|
require_relative 'raw_command'
|
|
8
8
|
require_relative 'uncovered_command'
|
|
9
9
|
require_relative 'detailed_command'
|
|
10
|
+
require_relative 'totals_command'
|
|
11
|
+
require_relative 'validate_command'
|
|
10
12
|
|
|
11
13
|
module SimpleCovMcp
|
|
12
14
|
module Commands
|
|
@@ -17,14 +19,18 @@ module SimpleCovMcp
|
|
|
17
19
|
'summary' => SummaryCommand,
|
|
18
20
|
'raw' => RawCommand,
|
|
19
21
|
'uncovered' => UncoveredCommand,
|
|
20
|
-
'detailed' => DetailedCommand
|
|
22
|
+
'detailed' => DetailedCommand,
|
|
23
|
+
'totals' => TotalsCommand,
|
|
24
|
+
'total' => TotalsCommand, # Alias for backward compatibility
|
|
25
|
+
'validate' => ValidateCommand
|
|
21
26
|
}.freeze
|
|
22
27
|
|
|
23
28
|
def self.create(command_name, cli_context)
|
|
24
29
|
command_class = COMMAND_MAP[command_name]
|
|
25
30
|
unless command_class
|
|
26
31
|
raise UsageError.for_subcommand(
|
|
27
|
-
'list | summary <path> | raw <path> | uncovered <path> | detailed <path>
|
|
32
|
+
'list | summary <path> | raw <path> | uncovered <path> | detailed <path> ' \
|
|
33
|
+
'| totals | validate <file> | validate -i <code> | version'
|
|
28
34
|
)
|
|
29
35
|
end
|
|
30
36
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'base_command'
|
|
4
4
|
require_relative '../formatters/source_formatter'
|
|
5
5
|
require_relative '../presenters/coverage_detailed_presenter'
|
|
6
|
+
require_relative '../table_formatter'
|
|
6
7
|
|
|
7
8
|
module SimpleCovMcp
|
|
8
9
|
module Commands
|
|
@@ -11,11 +12,24 @@ module SimpleCovMcp
|
|
|
11
12
|
handle_with_path(args, 'detailed') do |path|
|
|
12
13
|
presenter = Presenters::CoverageDetailedPresenter.new(model: model, path: path)
|
|
13
14
|
data = presenter.absolute_payload
|
|
14
|
-
break if
|
|
15
|
+
break if emit_structured_format_with_optional_source?(data, model, path)
|
|
15
16
|
|
|
16
17
|
relative_path = presenter.relative_path
|
|
17
18
|
puts "File: #{relative_path}"
|
|
18
|
-
puts
|
|
19
|
+
puts
|
|
20
|
+
|
|
21
|
+
# Table format with box-drawing
|
|
22
|
+
headers = ['Line', 'Hits', 'Covered']
|
|
23
|
+
rows = data['lines'].map do |r|
|
|
24
|
+
[r['line'].to_s, r['hits'].to_s, r['covered'] ? 'yes' : 'no']
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
puts TableFormatter.format(
|
|
28
|
+
headers: headers,
|
|
29
|
+
rows: rows,
|
|
30
|
+
alignments: [:right, :right, :center]
|
|
31
|
+
)
|
|
32
|
+
|
|
19
33
|
print_source_for(model, path) if config.source_mode
|
|
20
34
|
end
|
|
21
35
|
end
|