gem-why 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 427248f88372106860306d895e33b9323628582c6bee05b5b83fed9ce5c1b416
4
+ data.tar.gz: 4e6fec4a6b52226de621ea6d3486e79780c68e3a6e2ba4bcc6e6e843c9bcd0de
5
+ SHA512:
6
+ metadata.gz: 8c61e858acfd0fa46812f0eb8548e4710137b07fd9d551ad2dc7a75fe456655bb6c45817a2799e8770d2a4fcc1e59f1bb026ed9ebaa814a090411611d67929cd
7
+ data.tar.gz: bfc45293182646801fc2d3ab7ea7b5f4b464c21c844fd3a7f18743b716e4b4d76b6b0e9e885e036c00774940da1ef6b285b124e10243c4dab296791e4094ec90
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.0.1] - 2025-10-01
9
+
10
+ ### Added
11
+
12
+ - Initial release of `gem-why` plugin
13
+ - `gem why GEMNAME` command to show which installed gems depend on a specific gem
14
+ - Display gem names, versions, and version requirements for dependencies
15
+ - Alphabetically sorted output for easy scanning
16
+ - Clear messaging when no dependencies are found
17
+ - **Deep dependency tracking** - traverse the full dependency tree to find transitive dependencies (DEFAULT)
18
+ - **Tree visualization** with `--tree` option - display dependencies as a visual tree with proper indentation
19
+ - `--direct` option to show only direct dependencies (for quick lookups)
20
+ - Comprehensive test suite with 16 test cases
21
+ - RuboCop compliance with default settings
22
+ - Complete documentation in README
23
+
24
+ [0.0.1]: https://github.com/vitaly/gem-why/releases/tag/v0.0.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Vitaly Slobodin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # gem-why
2
+
3
+ A RubyGems plugin that shows which installed gems depend on a specific gem. Similar to `yarn why` or `npm why`.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install the plugin
9
+ gem install gem-why
10
+
11
+ # Find all dependency chains to rake (default - shows transitive dependencies)
12
+ gem why rake
13
+
14
+ # Show only direct dependencies
15
+ gem why rake --direct
16
+
17
+ # Visualize dependencies as a tree
18
+ gem why rake --tree
19
+ ```
20
+
21
+ ## Why Deep by Default?
22
+
23
+ Unlike most dependency tools that show only direct dependencies by default, `gem why` defaults to **deep dependency analysis** because:
24
+
25
+ - **Most real-world scenarios** involve transitive dependencies - you typically want to know "why is this gem even installed?" not just "what directly depends on it?"
26
+ - **Debugging is easier** when you see the full chain immediately - you can trace exactly how a problematic gem entered your dependency tree
27
+ - **Mirrors `yarn why`** behavior - the inspiration for this tool also defaults to showing full dependency chains
28
+ - **Direct lookups are still fast** - use `--direct` when you specifically need just immediate dependents
29
+
30
+ ## Installation
31
+
32
+ Install the gem by executing:
33
+
34
+ ```bash
35
+ gem install gem-why
36
+ ```
37
+
38
+ The plugin will automatically be available as a `gem` command after installation.
39
+
40
+ ## Feature Comparison
41
+
42
+ | Mode | Command | Shows | Best For |
43
+ |------|---------|-------|----------|
44
+ | **Deep** (default) | `gem why GEMNAME` | Full dependency chains with paths | Understanding transitive dependencies |
45
+ | **Direct** | `gem why GEMNAME --direct` | Only direct dependencies | Quick lookup of immediate dependents |
46
+ | **Tree** | `gem why GEMNAME --tree` | Visual tree structure | Complex dependency visualization |
47
+
48
+ ## Usage
49
+
50
+ To see which gems depend on a specific gem:
51
+
52
+ ```bash
53
+ gem why GEMNAME [options]
54
+ ```
55
+
56
+ ### Examples
57
+
58
+ #### Default Behavior - Deep Dependencies
59
+
60
+ Show all dependency chains leading to a gem (including transitive dependencies):
61
+
62
+ ```bash
63
+ $ gem why concurrent-ruby
64
+ Dependency chains leading to concurrent-ruby:
65
+
66
+ rails => activesupport => concurrent-ruby
67
+ └─ rails (8.0.3) requires activesupport = 8.0.3
68
+ └─ activesupport (8.0.3) requires concurrent-ruby ~> 1.0, >= 1.3.1
69
+
70
+ actionpack => activesupport => concurrent-ruby
71
+ └─ actionpack (8.0.3) requires activesupport = 8.0.3
72
+ └─ activesupport (8.0.3) requires concurrent-ruby ~> 1.0, >= 1.3.1
73
+
74
+ Total: 10 root gem(s) depend on concurrent-ruby
75
+ Found 25 dependency chain(s)
76
+
77
+ Tip: Use --direct for direct dependencies only or --tree for a visual tree
78
+ ```
79
+
80
+ #### Direct Dependencies Only
81
+
82
+ Check which gems directly depend on `rake`:
83
+
84
+ ```bash
85
+ $ gem why rake --direct
86
+ Gems that depend on rake:
87
+
88
+ ast (2.4.3) requires rake ~> 13.2
89
+ minitest (5.16.0) requires rake >= 0
90
+ rubocop (1.21.0) requires rake >= 0
91
+ ...
92
+
93
+ Total: 15 gem(s)
94
+ ```
95
+
96
+ #### Tree Visualization
97
+
98
+ Display dependencies as a visual tree:
99
+
100
+ ```bash
101
+ $ gem why concurrent-ruby --tree
102
+ Dependency tree for concurrent-ruby:
103
+
104
+ rails (8.0.3)
105
+ ├── activesupport = 8.0.3
106
+ │ └── activesupport (8.0.3) requires concurrent-ruby ~> 1.0, >= 1.3.1
107
+ │ └── concurrent-ruby ✓
108
+
109
+ actionpack (8.0.3)
110
+ ├── activesupport = 8.0.3
111
+ │ └── activesupport (8.0.3) requires concurrent-ruby ~> 1.0, >= 1.3.1
112
+ │ └── concurrent-ruby ✓
113
+
114
+ Total: 10 root gem(s) depend on concurrent-ruby
115
+ ```
116
+
117
+ If no gems depend on the specified gem:
118
+
119
+ ```bash
120
+ $ gem why nonexistent-gem
121
+ No gems depend on nonexistent-gem
122
+ ```
123
+
124
+ ### Options
125
+
126
+ | Option | Short | Description |
127
+ |--------|-------|-------------|
128
+ | (none) | | Show full dependency chains (default) |
129
+ | `--direct` | `-d` | Show only direct dependencies |
130
+ | `--tree` | `-t` | Display as a visual tree |
131
+ | `--help` | `-h` | Show help message |
132
+ | `--verbose` | `-V` | Verbose output |
133
+
134
+ ## How It Works
135
+
136
+ ### Default Mode (Deep Dependencies)
137
+
138
+ 1. Builds a complete dependency graph of all installed gems
139
+ 2. Finds all paths (dependency chains) leading to the target gem
140
+ 3. Shows transitive dependencies (A depends on B, B depends on target)
141
+ 4. Groups results by root gems
142
+ 5. Displays the full chain: `root => intermediate => target`
143
+
144
+ ### Direct Mode (`--direct`)
145
+
146
+ 1. Scans all installed gems on your system
147
+ 2. Checks both runtime and development dependencies
148
+ 3. Finds which gems **directly** depend on the specified gem
149
+ 4. Displays results with gem names, versions, and requirements
150
+ 5. Results are sorted alphabetically for easy reading
151
+
152
+ ### Tree Mode (`--tree`)
153
+
154
+ 1. Performs deep dependency analysis
155
+ 2. Builds a hierarchical tree structure
156
+ 3. Visualizes dependencies using tree characters (├──, └──, │)
157
+ 4. Shows the complete dependency path from root to target
158
+ 5. Makes it easy to understand complex dependency relationships
159
+
160
+ ## License
161
+
162
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemWhy
4
+ # Analyzes gem dependencies to find dependents and dependency chains
5
+ class Analyzer
6
+ # Finds all gems that directly depend on the target gem
7
+ # @param target_gem_name [String] the gem to find dependents for
8
+ # @return [Array<Array(String, String)>] array of [gem_name, requirement] pairs
9
+ def find_direct_dependents(target_gem_name)
10
+ dependents = []
11
+ normalized_target = target_gem_name.downcase
12
+
13
+ Gem::Specification.each do |spec|
14
+ spec.dependencies.each do |dep|
15
+ dependents << [spec.name, dep.requirement.to_s] if dep.name.downcase == normalized_target
16
+ end
17
+ end
18
+
19
+ dependents.sort_by(&:first)
20
+ end
21
+
22
+ # Finds all dependency chains leading to the target gem
23
+ # @param target_gem_name [String] the gem to find chains for
24
+ # @return [Array<Array<Hash>>] array of dependency chains
25
+ def find_dependency_chains(target_gem_name)
26
+ chains = []
27
+ normalized_target = target_gem_name.downcase
28
+
29
+ Gem::Specification.each do |spec|
30
+ paths = find_paths_to_target(spec.name, normalized_target, [])
31
+ chains.concat(paths)
32
+ end
33
+
34
+ chains.uniq.sort_by { |chain| chain.first[:name] }
35
+ end
36
+
37
+ private
38
+
39
+ # Recursively finds all paths from current gem to target gem
40
+ # @param current_gem [String] the current gem being explored
41
+ # @param target_gem [String] the gem we're searching for
42
+ # @param path [Array<Hash>] the current dependency path
43
+ # @param visited [Set<String>] set of already visited gems (prevents cycles)
44
+ # @return [Array<Array<Hash>>] array of paths to the target
45
+ def find_paths_to_target(current_gem, target_gem, path, visited = Set.new)
46
+ return [] if visited.include?(current_gem)
47
+
48
+ visited = visited.dup
49
+ visited.add(current_gem)
50
+ spec = load_gem_spec(current_gem)
51
+ return [] unless spec
52
+
53
+ collect_dependency_paths(spec, target_gem, path, visited)
54
+ end
55
+
56
+ def collect_dependency_paths(spec, target_gem, path, visited)
57
+ paths = []
58
+
59
+ spec.dependencies.each do |dep|
60
+ new_node = build_dependency_node(spec, dep)
61
+ paths.concat(process_dependency(dep, target_gem, path, new_node, visited))
62
+ end
63
+
64
+ paths
65
+ end
66
+
67
+ def process_dependency(dep, target_gem, path, new_node, visited)
68
+ if dep.name.downcase == target_gem.downcase
69
+ [path + [new_node]]
70
+ else
71
+ find_paths_to_target(dep.name, target_gem, path + [new_node], visited)
72
+ end
73
+ end
74
+
75
+ def load_gem_spec(gem_name)
76
+ Gem::Specification.find_by_name(gem_name)
77
+ rescue Gem::MissingSpecError
78
+ nil
79
+ end
80
+
81
+ def build_dependency_node(spec, dep)
82
+ name = spec.name
83
+ version = spec.version.to_s
84
+ dependency = dep.name
85
+ requirement = dep.requirement.to_s
86
+ { name:, version:, dependency:, requirement: }
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rainbow"
4
+
5
+ module GemWhy
6
+ module Formatters
7
+ # Base formatter with colorization support
8
+ class BaseFormatter
9
+ attr_reader :command
10
+
11
+ def initialize(command)
12
+ @command = command
13
+ end
14
+
15
+ private
16
+
17
+ # Determines if output should be colorized
18
+ # @return [Boolean] true if colors should be used
19
+ def colorize?
20
+ !command.options[:no_color] && $stdout.tty?
21
+ end
22
+
23
+ # Colorizes text if appropriate
24
+ # @param text [String] the text to colorize
25
+ # @param color [Symbol] the color to apply
26
+ # @return [String] the colorized or original text
27
+ def colorize(text, color)
28
+ colorize? ? Rainbow(text).color(color) : text
29
+ end
30
+
31
+ # Delegates to command's say method
32
+ def say(message)
33
+ command.say(message)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_formatter"
4
+
5
+ module GemWhy
6
+ module Formatters
7
+ # Formats deep dependency chains output
8
+ class DeepFormatter < BaseFormatter
9
+ # Formats and displays deep dependency chains
10
+ # @param gem_name [String] the target gem name
11
+ # @param chains [Array<Array<Hash>>] the dependency chains
12
+ # @return [void]
13
+ def format(gem_name, chains)
14
+ return say "No gems depend on #{colorize(gem_name, :yellow)}" if chains.empty?
15
+
16
+ say "Dependency chains leading to #{colorize(gem_name, :cyan)}:\n\n"
17
+ print_dependency_chains(chains, gem_name)
18
+ print_deep_summary(chains, gem_name)
19
+ end
20
+
21
+ private
22
+
23
+ def print_dependency_chains(chains, gem_name)
24
+ chains_by_root = chains.group_by { |chain| chain.first[:name] }
25
+
26
+ chains_by_root.each_value do |root_chains|
27
+ root_chains.each do |chain|
28
+ display_chain(chain, gem_name)
29
+ say ""
30
+ end
31
+ end
32
+ end
33
+
34
+ def print_deep_summary(chains, gem_name)
35
+ chains_by_root = chains.group_by { |chain| chain.first[:name] }
36
+ root_gems = chains_by_root.keys
37
+
38
+ say "#{colorize("Total:", :green)} #{root_gems.size} root gem(s) depend on #{gem_name}"
39
+ say "Found #{chains.size} dependency chain(s)"
40
+ say "\n#{colorize("Tip:", :yellow)} Use --direct for direct dependencies only or --tree for a visual tree"
41
+ end
42
+
43
+ def display_chain(chain, gem_name)
44
+ path_str = chain.map { |node| colorize(node[:name], :blue) }.join(" #{colorize("=>", :white)} ")
45
+ path_str += " #{colorize("=>", :white)} #{colorize(gem_name, :cyan)}"
46
+ say " #{path_str}"
47
+
48
+ chain.each_with_index do |node, idx|
49
+ display_chain_node(node, idx)
50
+ end
51
+ end
52
+
53
+ def display_chain_node(node, idx)
54
+ indent = " " * (idx + 1)
55
+ requirement = "#{node[:dependency]} #{node[:requirement]}"
56
+ say "#{indent}└─ #{node[:name]} (#{node[:version]}) requires #{requirement}"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_formatter"
4
+
5
+ module GemWhy
6
+ module Formatters
7
+ # Formats direct dependencies output
8
+ class DirectFormatter < BaseFormatter
9
+ # Formats and displays direct dependencies
10
+ # @param gem_name [String] the target gem name
11
+ # @param dependents [Array<Array(String, String)>] the dependent gems
12
+ # @return [void]
13
+ def format(gem_name, dependents)
14
+ return say "No gems depend on #{colorize(gem_name, :yellow)}" if dependents.empty?
15
+
16
+ say "Gems that depend on #{colorize(gem_name, :cyan)}:\n\n"
17
+ print_direct_dependents(dependents, gem_name)
18
+ say "\n#{colorize("Total:", :green)} #{dependents.size} gem(s)"
19
+ end
20
+
21
+ private
22
+
23
+ def print_direct_dependents(dependents, gem_name)
24
+ dependents.each do |dependent_name, requirement|
25
+ spec = Gem::Specification.find_by_name(dependent_name)
26
+ say " #{colorize(dependent_name, :blue)} (#{spec.version}) requires #{gem_name} #{requirement}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_formatter"
4
+
5
+ module GemWhy
6
+ module Formatters
7
+ # Formats tree visualization output
8
+ class TreeFormatter < BaseFormatter
9
+ attr_reader :tree_builder
10
+
11
+ def initialize(command, tree_builder)
12
+ super(command)
13
+ @tree_builder = tree_builder
14
+ end
15
+
16
+ # Formats and displays dependency tree
17
+ # @param gem_name [String] the target gem name
18
+ # @param chains [Array<Array<Hash>>] the dependency chains
19
+ # @return [void]
20
+ def format(gem_name, chains)
21
+ return say "No gems depend on #{colorize(gem_name, :yellow)}" if chains.empty?
22
+
23
+ say "Dependency tree for #{colorize(gem_name, :cyan)}:\n\n"
24
+ print_dependency_tree(chains, gem_name)
25
+ print_tree_summary(chains, gem_name)
26
+ end
27
+
28
+ private
29
+
30
+ def print_dependency_tree(chains, gem_name)
31
+ chains_by_root = chains.group_by { |chain| chain.first[:name] }
32
+
33
+ chains_by_root.each do |root_name, root_chains|
34
+ print_root_tree(root_name, root_chains, gem_name)
35
+ end
36
+ end
37
+
38
+ def print_root_tree(root_name, root_chains, gem_name)
39
+ root_spec = Gem::Specification.find_by_name(root_name)
40
+ say "#{colorize(root_name, :blue)} (#{root_spec.version})"
41
+
42
+ tree = tree_builder.build_tree_structure(root_chains)
43
+ display_tree_node(tree, "", gem_name)
44
+
45
+ say ""
46
+ end
47
+
48
+ def print_tree_summary(chains, gem_name)
49
+ chains_by_root = chains.group_by { |chain| chain.first[:name] }
50
+ root_gems = chains_by_root.keys
51
+ say "#{colorize("Total:", :green)} #{root_gems.size} root gem(s) depend on #{gem_name}"
52
+ end
53
+
54
+ def display_tree_node(tree, prefix, target_gem, depth = 0)
55
+ return if tree.empty?
56
+
57
+ tree.each_with_index do |(key, value), index|
58
+ is_last = index == tree.size - 1
59
+ display_node_line(key, value, prefix, is_last, depth)
60
+ display_children_or_target(value, prefix, is_last, target_gem, depth)
61
+ end
62
+ end
63
+
64
+ def display_node_line(key, value, prefix, is_last, depth)
65
+ connector = is_last ? "└──" : "├──"
66
+ line = build_node_line(key, value, depth)
67
+ say "#{prefix}#{connector} #{line}"
68
+ end
69
+
70
+ def build_node_line(key, value, depth)
71
+ if depth.zero?
72
+ "#{value[:dependency]} #{value[:requirement]}"
73
+ else
74
+ "#{key} requires #{value[:dependency]} #{value[:requirement]}"
75
+ end
76
+ end
77
+
78
+ def display_children_or_target(value, prefix, is_last, target_gem, depth)
79
+ new_prefix = prefix + (is_last ? " " : "│ ")
80
+
81
+ if value[:children].empty?
82
+ say "#{new_prefix}└── #{colorize(target_gem, :cyan)} #{colorize("✓", :green)}"
83
+ else
84
+ display_tree_node(value[:children], new_prefix, target_gem, depth + 1)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module GemWhy
6
+ # Handles JSON output for all display modes
7
+ class JSONOutputter
8
+ attr_reader :command, :tree_builder
9
+
10
+ def initialize(command, tree_builder)
11
+ @command = command
12
+ @tree_builder = tree_builder
13
+ end
14
+
15
+ # Outputs direct dependencies as JSON
16
+ # @param gem_name [String] the target gem name
17
+ # @param dependents [Array<Array(String, String)>] the dependent gems
18
+ # @return [void]
19
+ def output_direct(gem_name, dependents)
20
+ target = gem_name
21
+ mode = "direct"
22
+ total = dependents.size
23
+ dependents_data = dependents.map do |name, requirement|
24
+ spec = Gem::Specification.find_by_name(name)
25
+ version = spec.version.to_s
26
+ { name:, version:, requirement: }
27
+ end
28
+ output = { target:, mode:, dependents: dependents_data, total: }
29
+ say JSON.pretty_generate(output)
30
+ end
31
+
32
+ # Outputs deep dependency chains as JSON
33
+ # @param gem_name [String] the target gem name
34
+ # @param chains [Array<Array<Hash>>] the dependency chains
35
+ # @return [void]
36
+ def output_deep(gem_name, chains)
37
+ chains_by_root = chains.group_by { |chain| chain.first[:name] }
38
+ target = gem_name
39
+ mode = "deep"
40
+ chains_data = chains.map { |chain| chain.map { |node| node.slice(:name, :version, :dependency, :requirement) } }
41
+ root_gems = chains_by_root.keys.size
42
+ total_chains = chains.size
43
+ output = { target:, mode:, chains: chains_data, root_gems:, total_chains: }
44
+ say JSON.pretty_generate(output)
45
+ end
46
+
47
+ # Outputs dependency tree as JSON
48
+ # @param gem_name [String] the target gem name
49
+ # @param chains [Array<Array<Hash>>] the dependency chains
50
+ # @return [void]
51
+ def output_tree(gem_name, chains)
52
+ chains_by_root = chains.group_by { |chain| chain.first[:name] }
53
+ target = gem_name
54
+ mode = "tree"
55
+ roots = build_json_roots(chains_by_root)
56
+ total_roots = chains_by_root.keys.size
57
+ output = { target:, mode:, roots:, total_roots: }
58
+ say JSON.pretty_generate(output)
59
+ end
60
+
61
+ private
62
+
63
+ def build_json_roots(chains_by_root)
64
+ chains_by_root.map do |root_name, root_chains|
65
+ root_spec = Gem::Specification.find_by_name(root_name)
66
+ name = root_name
67
+ version = root_spec.version.to_s
68
+ tree = tree_builder.build_tree_structure(root_chains)
69
+ { name:, version:, tree: }
70
+ end
71
+ end
72
+
73
+ def say(message)
74
+ command.say(message)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemWhy
4
+ # Builds tree structures from dependency chains
5
+ class TreeBuilder
6
+ # Builds a tree structure from dependency chains
7
+ # @param chains [Array<Array<Hash>>] the dependency chains
8
+ # @return [Hash] hierarchical tree structure
9
+ def build_tree_structure(chains)
10
+ tree = {}
11
+
12
+ chains.each do |chain|
13
+ add_chain_to_tree(tree, chain)
14
+ end
15
+
16
+ tree
17
+ end
18
+
19
+ private
20
+
21
+ def add_chain_to_tree(tree, chain)
22
+ current_level = tree
23
+
24
+ chain.each do |node|
25
+ key = "#{node[:name]} (#{node[:version]})"
26
+ current_level[key] ||= create_tree_node(node)
27
+ current_level = current_level[key][:children]
28
+ end
29
+ end
30
+
31
+ def create_tree_node(node)
32
+ dependency = node[:dependency]
33
+ requirement = node[:requirement]
34
+ children = {}
35
+ { dependency:, requirement:, children: }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemWhy
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/command"
4
+ require_relative "../../gem_why/version"
5
+ require_relative "../../gem_why/analyzer"
6
+ require_relative "../../gem_why/tree_builder"
7
+ require_relative "../../gem_why/json_outputter"
8
+ require_relative "../../gem_why/formatters/direct_formatter"
9
+ require_relative "../../gem_why/formatters/deep_formatter"
10
+ require_relative "../../gem_why/formatters/tree_formatter"
11
+
12
+ module Gem
13
+ module Commands
14
+ # Command to show which gems depend on a specific gem
15
+ #
16
+ # This command helps identify dependency relationships by showing:
17
+ # - Direct dependencies (--direct): immediate dependents only
18
+ # - Deep dependencies (default): full dependency chains
19
+ # - Tree visualization (--tree): hierarchical view
20
+ #
21
+ # @example Show all dependency chains
22
+ # gem why concurrent-ruby
23
+ #
24
+ # @example Show only direct dependencies
25
+ # gem why rake --direct
26
+ #
27
+ # @example Show tree visualization
28
+ # gem why concurrent-ruby --tree
29
+ #
30
+ # @example Output as JSON
31
+ # gem why rake --json
32
+ class WhyCommand < Gem::Command
33
+ # Initializes the why command with options
34
+ def initialize
35
+ super("why", "Show which gems depend on a specific gem")
36
+ setup_options
37
+ initialize_dependencies
38
+ end
39
+
40
+ # @return [String] long description of the command
41
+ def description
42
+ "Show which installed gems depend on a specific gem, including dependency chains"
43
+ end
44
+
45
+ # @return [String] usage string for the command
46
+ def usage
47
+ "#{program_name} GEMNAME [options]"
48
+ end
49
+
50
+ # Executes the command with the provided arguments
51
+ # @return [void]
52
+ def execute
53
+ gem_name = validate_gem_name
54
+ route_to_display_mode(gem_name)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :analyzer, :tree_builder, :json_outputter, :direct_formatter, :deep_formatter, :tree_formatter
60
+
61
+ def setup_options
62
+ setup_tree_option
63
+ setup_direct_option
64
+ setup_version_option
65
+ setup_no_color_option
66
+ setup_json_option
67
+ end
68
+
69
+ def setup_tree_option
70
+ add_option("-t", "--tree", "Display dependencies as a tree") do |value, options|
71
+ options[:tree] = value
72
+ end
73
+ end
74
+
75
+ def setup_direct_option
76
+ add_option("-d", "--direct", "Show only direct dependencies") do |value, options|
77
+ options[:direct] = value
78
+ end
79
+ end
80
+
81
+ def setup_version_option
82
+ add_option("-v", "--version", "Show gem-why version") do |_value, _options|
83
+ say "gem-why version #{GemWhy::VERSION}"
84
+ terminate_interaction
85
+ end
86
+ end
87
+
88
+ def setup_no_color_option
89
+ add_option("--no-color", "Disable colored output") do |_value, options|
90
+ options[:no_color] = true
91
+ end
92
+ end
93
+
94
+ def setup_json_option
95
+ add_option("--json", "Output in JSON format") do |_value, options|
96
+ options[:json] = true
97
+ end
98
+ end
99
+
100
+ def initialize_dependencies
101
+ @analyzer = GemWhy::Analyzer.new
102
+ @tree_builder = GemWhy::TreeBuilder.new
103
+ @json_outputter = GemWhy::JSONOutputter.new(self, tree_builder)
104
+ @direct_formatter = GemWhy::Formatters::DirectFormatter.new(self)
105
+ @deep_formatter = GemWhy::Formatters::DeepFormatter.new(self)
106
+ @tree_formatter = GemWhy::Formatters::TreeFormatter.new(self, tree_builder)
107
+ end
108
+
109
+ # Validates and normalizes the gem name argument
110
+ # @return [String] the normalized gem name
111
+ # @raise [Gem::CommandLineError] if no gem name provided
112
+ def validate_gem_name
113
+ gem_name = get_one_optional_argument || raise(
114
+ Gem::CommandLineError,
115
+ "Please specify a gem name (e.g. gem why rake)"
116
+ )
117
+ gem_name.downcase
118
+ end
119
+
120
+ # Routes execution to the appropriate display mode
121
+ # @param gem_name [String] the target gem name
122
+ # @return [void]
123
+ def route_to_display_mode(gem_name)
124
+ if options[:direct]
125
+ show_direct_dependencies(gem_name)
126
+ elsif options[:tree]
127
+ show_tree_visualization(gem_name)
128
+ else
129
+ show_deep_dependencies(gem_name)
130
+ end
131
+ end
132
+
133
+ # Shows direct dependencies only
134
+ # @param gem_name [String] the target gem name
135
+ # @return [void]
136
+ def show_direct_dependencies(gem_name)
137
+ dependents = analyzer.find_direct_dependents(gem_name)
138
+
139
+ if options[:json]
140
+ json_outputter.output_direct(gem_name, dependents)
141
+ else
142
+ direct_formatter.format(gem_name, dependents)
143
+ end
144
+ end
145
+
146
+ # Shows deep dependencies (full chains)
147
+ # @param gem_name [String] the target gem name
148
+ # @return [void]
149
+ def show_deep_dependencies(gem_name)
150
+ chains = analyzer.find_dependency_chains(gem_name)
151
+
152
+ if options[:json]
153
+ json_outputter.output_deep(gem_name, chains)
154
+ else
155
+ deep_formatter.format(gem_name, chains)
156
+ end
157
+ end
158
+
159
+ # Shows tree visualization
160
+ # @param gem_name [String] the target gem name
161
+ # @return [void]
162
+ def show_tree_visualization(gem_name)
163
+ chains = analyzer.find_dependency_chains(gem_name)
164
+
165
+ if options[:json]
166
+ json_outputter.output_tree(gem_name, chains)
167
+ else
168
+ tree_formatter.format(gem_name, chains)
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/command_manager"
4
+
5
+ Gem::CommandManager.instance.register_command :why
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gem-why
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Vitaly Slobodin
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rainbow
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.1'
26
+ description: A RubyGems plugin that shows which gems depend on a specific gem, including
27
+ full dependency chains and tree visualization.
28
+ email:
29
+ - vitaliy.slobodin@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE.txt
36
+ - README.md
37
+ - Rakefile
38
+ - lib/gem_why/analyzer.rb
39
+ - lib/gem_why/formatters/base_formatter.rb
40
+ - lib/gem_why/formatters/deep_formatter.rb
41
+ - lib/gem_why/formatters/direct_formatter.rb
42
+ - lib/gem_why/formatters/tree_formatter.rb
43
+ - lib/gem_why/json_outputter.rb
44
+ - lib/gem_why/tree_builder.rb
45
+ - lib/gem_why/version.rb
46
+ - lib/rubygems/commands/why_command.rb
47
+ - lib/rubygems_plugin.rb
48
+ homepage: https://github.com/vitallium/gem-why
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/vitallium/gem-why
53
+ source_code_uri: https://github.com/vitallium/gem-why
54
+ changelog_uri: https://github.com/vitallium/gem-why/blob/main/CHANGELOG.md
55
+ rubygems_mfa_required: 'true'
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.2.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.6.9
71
+ specification_version: 4
72
+ summary: A RubyGems plugin to show which gems depend on a specific gem
73
+ test_files: []