contextizer 0.1.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 +7 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +153 -0
- data/Rakefile +9 -0
- data/config/default.yml +53 -0
- data/contextizer_2025-09-01_2223.md +2538 -0
- data/exe/contextizer +7 -0
- data/lib/contextizer/analyzer.rb +33 -0
- data/lib/contextizer/analyzers/base.rb +34 -0
- data/lib/contextizer/analyzers/java_script_analyzer.rb +30 -0
- data/lib/contextizer/analyzers/python_analyzer.rb +39 -0
- data/lib/contextizer/analyzers/ruby_analyzer.rb +40 -0
- data/lib/contextizer/cli.rb +70 -0
- data/lib/contextizer/collector.rb +57 -0
- data/lib/contextizer/configuration.rb +61 -0
- data/lib/contextizer/context.rb +22 -0
- data/lib/contextizer/providers/base/file_system.rb +60 -0
- data/lib/contextizer/providers/base/git.rb +25 -0
- data/lib/contextizer/providers/base/project_name.rb +33 -0
- data/lib/contextizer/providers/base_provider.rb +13 -0
- data/lib/contextizer/providers/javascript/packages.rb +33 -0
- data/lib/contextizer/providers/ruby/gems.rb +29 -0
- data/lib/contextizer/providers/ruby/project_info.rb +45 -0
- data/lib/contextizer/remote_repo_handler.rb +23 -0
- data/lib/contextizer/renderers/base.rb +12 -0
- data/lib/contextizer/renderers/markdown.rb +118 -0
- data/lib/contextizer/version.rb +5 -0
- data/lib/contextizer/writer.rb +17 -0
- data/lib/contextizer.rb +18 -0
- metadata +121 -0
data/exe/contextizer
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
class Analyzer
|
|
5
|
+
SPECIALISTS = Analyzers.constants.map do |const|
|
|
6
|
+
Analyzers.const_get(const)
|
|
7
|
+
end.select { |const| const.is_a?(Class) && const < Analyzers::Base }
|
|
8
|
+
|
|
9
|
+
def self.call(target_path:)
|
|
10
|
+
new(target_path: target_path).analyze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(target_path:)
|
|
14
|
+
@target_path = target_path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def analyze
|
|
18
|
+
results = SPECIALISTS.map do |specialist_class|
|
|
19
|
+
specialist_class.call(target_path: @target_path)
|
|
20
|
+
end.compact
|
|
21
|
+
|
|
22
|
+
return { language: :unknown, framework: nil, scores: {} } if results.empty?
|
|
23
|
+
|
|
24
|
+
best_result = results.max_by { |result| result[:score] }
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
language: best_result[:language],
|
|
28
|
+
framework: best_result[:framework],
|
|
29
|
+
scores: results.map { |r| [r[:language], r[:score]] }.to_h
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Analyzers
|
|
5
|
+
class Base
|
|
6
|
+
def self.call(target_path:)
|
|
7
|
+
new(target_path: target_path).analyze
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(target_path:)
|
|
11
|
+
@target_path = Pathname.new(target_path)
|
|
12
|
+
@score = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def analyze
|
|
16
|
+
raise NotImplementedError, "#{self.class.name} must implement #analyze"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def check_signal(signal)
|
|
22
|
+
path = @target_path.join(signal[:path])
|
|
23
|
+
case signal[:type]
|
|
24
|
+
when :file
|
|
25
|
+
@target_path.glob(signal[:path]).any?
|
|
26
|
+
when :dir
|
|
27
|
+
path.directory?
|
|
28
|
+
else
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Analyzers
|
|
5
|
+
class JavaScriptAnalyzer < Base
|
|
6
|
+
LANGUAGE = :javascript
|
|
7
|
+
|
|
8
|
+
SIGNALS = [
|
|
9
|
+
{ type: :file, path: "package.json", weight: 5 },
|
|
10
|
+
{ type: :dir, path: "node_modules", weight: 10 },
|
|
11
|
+
{ type: :file, path: "webpack.config.js", weight: 5 },
|
|
12
|
+
{ type: :file, path: "vite.config.js", weight: 5 }
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
FRAMEWORK_SIGNALS = {}.freeze
|
|
16
|
+
|
|
17
|
+
def analyze
|
|
18
|
+
SIGNALS.each { |signal| @score += signal[:weight] if check_signal(signal) }
|
|
19
|
+
|
|
20
|
+
return nil if @score.zero?
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
language: LANGUAGE,
|
|
24
|
+
framework: nil,
|
|
25
|
+
score: @score
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Analyzers
|
|
5
|
+
class PythonAnalyzer < Base
|
|
6
|
+
LANGUAGE = :python
|
|
7
|
+
|
|
8
|
+
SIGNALS = [
|
|
9
|
+
{ type: :file, path: "requirements.txt", weight: 10 },
|
|
10
|
+
{ type: :file, path: "pyproject.toml", weight: 10 },
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
FRAMEWORK_SIGNALS = {
|
|
14
|
+
# rails: [{ type: :file, path: "bin/rails", weight: 15 }]
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def analyze
|
|
18
|
+
SIGNALS.each { |signal| @score += signal[:weight] if check_signal(signal) }
|
|
19
|
+
|
|
20
|
+
return nil if @score.zero?
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
language: LANGUAGE,
|
|
24
|
+
framework: detect_framework,
|
|
25
|
+
score: @score
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def detect_framework
|
|
32
|
+
(FRAMEWORK_SIGNALS || {}).each do |fw, signals|
|
|
33
|
+
return fw if signals.any? { |signal| check_signal(signal) }
|
|
34
|
+
end
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Analyzers
|
|
5
|
+
class RubyAnalyzer < Base
|
|
6
|
+
LANGUAGE = :ruby
|
|
7
|
+
|
|
8
|
+
SIGNALS = [
|
|
9
|
+
{ type: :file, path: "Gemfile", weight: 10 },
|
|
10
|
+
{ type: :file, path: "*.gemspec", weight: 20 },
|
|
11
|
+
{ type: :dir, path: "app/controllers", weight: 5 }
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
FRAMEWORK_SIGNALS = {
|
|
15
|
+
rails: [{ type: :file, path: "bin/rails", weight: 15 }]
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def analyze
|
|
19
|
+
SIGNALS.each { |signal| @score += signal[:weight] if check_signal(signal) }
|
|
20
|
+
|
|
21
|
+
return nil if @score.zero?
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
language: LANGUAGE,
|
|
25
|
+
framework: detect_framework,
|
|
26
|
+
score: @score
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def detect_framework
|
|
33
|
+
(FRAMEWORK_SIGNALS || {}).each do |fw, signals|
|
|
34
|
+
return fw if signals.any? { |signal| check_signal(signal) }
|
|
35
|
+
end
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Contextizer
|
|
7
|
+
class CLI < Thor
|
|
8
|
+
desc "extract [TARGET_PATH]", "Extracts project context into a single file."
|
|
9
|
+
option :git_url,
|
|
10
|
+
type: :string,
|
|
11
|
+
desc: "URL of a remote git repository to analyze instead of a local path."
|
|
12
|
+
option :output,
|
|
13
|
+
aliases: "-o",
|
|
14
|
+
type: :string,
|
|
15
|
+
desc: "Output file path (overrides config)."
|
|
16
|
+
option :format,
|
|
17
|
+
aliases: "-f",
|
|
18
|
+
type: :string,
|
|
19
|
+
desc: "Output format (e.g., markdown, json)."
|
|
20
|
+
|
|
21
|
+
RENDERER_MAPPING = {
|
|
22
|
+
"markdown" => Renderers::Markdown
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def extract(target_path = ".")
|
|
26
|
+
if options[:git_url]
|
|
27
|
+
RemoteRepoHandler.handle(options[:git_url]) do |remote_path|
|
|
28
|
+
run_extraction(remote_path)
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
run_extraction(target_path)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def run_extraction(path)
|
|
38
|
+
cli_options = options.transform_keys(&:to_s).compact
|
|
39
|
+
config = Configuration.load(cli_options)
|
|
40
|
+
|
|
41
|
+
command_string = "contextizer #{ARGV.join(" ")}"
|
|
42
|
+
context = Collector.call(
|
|
43
|
+
config: config,
|
|
44
|
+
target_path: path,
|
|
45
|
+
command: command_string
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
renderer = RENDERER_MAPPING[config.format]
|
|
49
|
+
raise Error, "Unsupported format: '#{config.format}'" unless renderer
|
|
50
|
+
|
|
51
|
+
rendered_output = renderer.call(context: context)
|
|
52
|
+
|
|
53
|
+
destination_path = resolve_output_path(config.output, context)
|
|
54
|
+
Writer.call(content: rendered_output, destination: destination_path)
|
|
55
|
+
|
|
56
|
+
puts "\nContextizer: Extraction complete! ✨"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_output_path(path_template, context)
|
|
60
|
+
timestamp = Time.now.strftime("%Y-%m-%d_%H%M")
|
|
61
|
+
project_name = context.metadata.dig(:project, :type) == "Gem" ? context.project_name : "project"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
path_template
|
|
65
|
+
.gsub("{profile}", "default")
|
|
66
|
+
.gsub("{timestamp}", timestamp)
|
|
67
|
+
.gsub("{project_name}", project_name)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
class Collector
|
|
5
|
+
BASE_PROVIDERS = [
|
|
6
|
+
Providers::Base::ProjectName,
|
|
7
|
+
Providers::Base::Git,
|
|
8
|
+
Providers::Base::FileSystem
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
LANGUAGE_MODULES = {
|
|
12
|
+
ruby: Providers::Ruby,
|
|
13
|
+
javascript: Providers::JavaScript
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def self.call(config:, target_path:, command:)
|
|
17
|
+
new(config: config, target_path: target_path, command: command).call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(config:, target_path:, command:)
|
|
21
|
+
@config = config
|
|
22
|
+
@target_path = target_path
|
|
23
|
+
@command = command
|
|
24
|
+
@context = Context.new(
|
|
25
|
+
target_path: target_path,
|
|
26
|
+
command: @command
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call
|
|
31
|
+
stack = Analyzer.call(target_path: @target_path)
|
|
32
|
+
@context.metadata[:stack] = stack
|
|
33
|
+
|
|
34
|
+
BASE_PROVIDERS.each do |provider_class|
|
|
35
|
+
provider_class.call(context: @context, config: @config)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
language_module = LANGUAGE_MODULES[stack[:language]]
|
|
39
|
+
run_language_providers(language_module) if language_module
|
|
40
|
+
|
|
41
|
+
puts "Collector: Collection complete. Found #{@context.files.count} files."
|
|
42
|
+
@context
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def run_language_providers(language_module)
|
|
48
|
+
puts "Collector: Running '#{language_module.name.split('::').last}' providers..."
|
|
49
|
+
language_module.constants.each do |const_name|
|
|
50
|
+
provider_class = language_module.const_get(const_name)
|
|
51
|
+
if provider_class.is_a?(Class) && provider_class < Providers::BaseProvider
|
|
52
|
+
provider_class.call(context: @context, config: @config)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Contextizer
|
|
7
|
+
class Configuration
|
|
8
|
+
# CLI options > Project Config > Default Config
|
|
9
|
+
def self.load(cli_options = {})
|
|
10
|
+
default_config_path = Pathname.new(__dir__).join("../../config/default.yml")
|
|
11
|
+
default_config = YAML.load_file(default_config_path)
|
|
12
|
+
|
|
13
|
+
project_config_path = find_project_config
|
|
14
|
+
project_config = project_config_path ? YAML.load_file(project_config_path) : {}
|
|
15
|
+
|
|
16
|
+
merged_config = deep_merge(default_config, project_config)
|
|
17
|
+
merged_config = deep_merge(merged_config, cli_options)
|
|
18
|
+
|
|
19
|
+
new(merged_config)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(options)
|
|
23
|
+
@options = options
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def method_missing(name, *args, &block)
|
|
27
|
+
key = name.to_s
|
|
28
|
+
if @options.key?(key)
|
|
29
|
+
@options[key]
|
|
30
|
+
else
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def respond_to_missing?(name, include_private = false)
|
|
36
|
+
@options.key?(name.to_s) || super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.find_project_config(path = Dir.pwd)
|
|
40
|
+
path = Pathname.new(path)
|
|
41
|
+
loop do
|
|
42
|
+
config_file = path.join(".contextizer.yml")
|
|
43
|
+
return config_file if config_file.exist?
|
|
44
|
+
break if path.root?
|
|
45
|
+
|
|
46
|
+
path = path.parent
|
|
47
|
+
end
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.deep_merge(hash1, hash2)
|
|
52
|
+
hash1.merge(hash2) do |key, old_val, new_val|
|
|
53
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
54
|
+
deep_merge(old_val, new_val)
|
|
55
|
+
else
|
|
56
|
+
new_val
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
# A value object that holds all the collected information about a project.
|
|
5
|
+
# This object is the result of the Collector phase and the input for the Renderer phase.
|
|
6
|
+
Context = Struct.new(
|
|
7
|
+
:project_name,
|
|
8
|
+
:target_path,
|
|
9
|
+
:timestamp,
|
|
10
|
+
:command,
|
|
11
|
+
:metadata, # Hash for data from providers like Git, Gems, etc.
|
|
12
|
+
:files, # Array of file objects { path:, content: }
|
|
13
|
+
keyword_init: true
|
|
14
|
+
) do
|
|
15
|
+
def initialize(*)
|
|
16
|
+
super
|
|
17
|
+
self.metadata ||= {}
|
|
18
|
+
self.files ||= []
|
|
19
|
+
self.timestamp ||= Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Contextizer
|
|
6
|
+
module Providers
|
|
7
|
+
module Base
|
|
8
|
+
class FileSystem < BaseProvider
|
|
9
|
+
def self.call(context:, config:)
|
|
10
|
+
new(context: context, config: config).collect_files
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(context:, config:)
|
|
14
|
+
@context = context
|
|
15
|
+
@config = config.settings["filesystem"]
|
|
16
|
+
@target_path = Pathname.new(context.target_path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def collect_files
|
|
20
|
+
file_paths = find_matching_files
|
|
21
|
+
|
|
22
|
+
file_paths.each do |path|
|
|
23
|
+
content = read_file_content(path)
|
|
24
|
+
next unless content
|
|
25
|
+
|
|
26
|
+
@context.files << {
|
|
27
|
+
path: path.relative_path_from(@target_path).to_s,
|
|
28
|
+
content: content
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def find_matching_files
|
|
36
|
+
patterns = @config["components"].values.flatten.map do |pattern|
|
|
37
|
+
(@target_path / pattern).to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
all_files = Dir.glob(patterns)
|
|
41
|
+
|
|
42
|
+
exclude_patterns = @config["exclude"].map do |pattern|
|
|
43
|
+
(@target_path / pattern).to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
all_files.reject do |file|
|
|
47
|
+
exclude_patterns.any? { |pattern| File.fnmatch?(pattern, file) }
|
|
48
|
+
end.map { |file| Pathname.new(file) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def read_file_content(path)
|
|
52
|
+
path.read
|
|
53
|
+
rescue Errno::ENOENT, IOError => e
|
|
54
|
+
warn "Warning: Could not read file #{path}: #{e.message}"
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Providers
|
|
5
|
+
module Base
|
|
6
|
+
class Git < BaseProvider
|
|
7
|
+
def self.call(context:, config:)
|
|
8
|
+
context.metadata[:git] = fetch_git_info(context.target_path)
|
|
9
|
+
@config = config
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.fetch_git_info(path)
|
|
13
|
+
Dir.chdir(path) do
|
|
14
|
+
{
|
|
15
|
+
branch: `git rev-parse --abbrev-ref HEAD`.strip,
|
|
16
|
+
commit: `git rev-parse HEAD`.strip[0, 8]
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
rescue StandardError
|
|
20
|
+
{ branch: "N/A", commit: "N/A" }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Providers
|
|
5
|
+
module Base
|
|
6
|
+
class ProjectName < BaseProvider
|
|
7
|
+
def self.call(context:, config:)
|
|
8
|
+
context.project_name = detect_project_name(context.target_path)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def self.detect_project_name(path)
|
|
14
|
+
git_name = name_from_git_remote(path)
|
|
15
|
+
return git_name if git_name
|
|
16
|
+
|
|
17
|
+
File.basename(path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.name_from_git_remote(path)
|
|
21
|
+
return nil unless Dir.exist?(File.join(path, ".git"))
|
|
22
|
+
|
|
23
|
+
Dir.chdir(path) do
|
|
24
|
+
remote_url = `git remote get-url origin`.strip
|
|
25
|
+
remote_url.split("/").last.sub(/\.git$/, "")
|
|
26
|
+
end
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Providers
|
|
5
|
+
class BaseProvider
|
|
6
|
+
# @param context [Contextizer::Context] The context object to be populated.
|
|
7
|
+
# @param config [Contextizer::Configuration] The overall configuration.
|
|
8
|
+
def self.call(context:, config:)
|
|
9
|
+
raise NotImplementedError, "#{self.name} must implement the .call method"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Contextizer
|
|
5
|
+
module Providers
|
|
6
|
+
module JavaScript
|
|
7
|
+
class Packages < BaseProvider
|
|
8
|
+
def self.call(context:, config:)
|
|
9
|
+
package_json_path = File.join(context.target_path, "package.json")
|
|
10
|
+
return unless File.exist?(package_json_path)
|
|
11
|
+
|
|
12
|
+
context.metadata[:packages] = parse_packages(package_json_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def self.parse_packages(path)
|
|
18
|
+
begin
|
|
19
|
+
file = File.read(path)
|
|
20
|
+
data = JSON.parse(file)
|
|
21
|
+
{
|
|
22
|
+
dependencies: data["dependencies"] || {},
|
|
23
|
+
dev_dependencies: data["devDependencies"] || {}
|
|
24
|
+
}
|
|
25
|
+
rescue JSON::ParserError => e
|
|
26
|
+
puts "Warning: Could not parse package.json: #{e.message}"
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Providers
|
|
5
|
+
module Ruby
|
|
6
|
+
class Gems < BaseProvider
|
|
7
|
+
def self.call(context:, config:)
|
|
8
|
+
key_gems = config.settings.dig("gems", "key_gems")
|
|
9
|
+
return if key_gems.nil? || key_gems.empty?
|
|
10
|
+
|
|
11
|
+
gemfile_lock = File.join(context.target_path, "Gemfile.lock")
|
|
12
|
+
return unless File.exist?(gemfile_lock)
|
|
13
|
+
|
|
14
|
+
context.metadata[:gems] = parse_gemfile_lock(gemfile_lock, key_gems)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.parse_gemfile_lock(path, key_gems)
|
|
18
|
+
found_gems = {}
|
|
19
|
+
content = File.read(path)
|
|
20
|
+
key_gems.each do |gem_name|
|
|
21
|
+
match = content.match(/^\s{4}#{gem_name}\s\((.+?)\)/)
|
|
22
|
+
found_gems[gem_name] = match[1] if match
|
|
23
|
+
end
|
|
24
|
+
found_gems
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Providers
|
|
5
|
+
module Ruby
|
|
6
|
+
class ProjectInfo < BaseProvider
|
|
7
|
+
def self.call(context:, config:)
|
|
8
|
+
project_info = detect_project_info(context.target_path)
|
|
9
|
+
context.metadata[:project] = project_info
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def self.detect_project_info(path)
|
|
14
|
+
if Dir.glob(File.join(path, "*.gemspec")).any?
|
|
15
|
+
return { type: "Gem", version: detect_gem_version(path) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if File.exist?(File.join(path, "bin", "rails"))
|
|
19
|
+
return { type: "Rails", version: detect_rails_version(path) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
{ type: "Directory", version: "N/A" }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.detect_gem_version(path)
|
|
26
|
+
version_file = Dir.glob(File.join(path, "lib", "**", "version.rb")).first
|
|
27
|
+
return "N/A" unless version_file
|
|
28
|
+
|
|
29
|
+
content = File.read(version_file)
|
|
30
|
+
match = content.match(/VERSION\s*=\s*["'](.+?)["']/)
|
|
31
|
+
match ? match[1] : "N/A"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.detect_rails_version(path)
|
|
35
|
+
gemfile_lock = File.join(path, "Gemfile.lock")
|
|
36
|
+
return "N/A" unless File.exist?(gemfile_lock)
|
|
37
|
+
|
|
38
|
+
content = File.read(gemfile_lock)
|
|
39
|
+
match = content.match(/^\s{4}rails\s\((.+?)\)/)
|
|
40
|
+
match ? match[1] : "N/A"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Contextizer
|
|
7
|
+
class RemoteRepoHandler
|
|
8
|
+
def self.handle(url)
|
|
9
|
+
Dir.mktmpdir("contextizer-clone-") do |temp_path|
|
|
10
|
+
puts "Cloning #{url} into temporary directory..."
|
|
11
|
+
|
|
12
|
+
success = system("git clone --depth 1 #{url} #{temp_path}")
|
|
13
|
+
|
|
14
|
+
if success
|
|
15
|
+
puts "Cloning successful."
|
|
16
|
+
yield(temp_path)
|
|
17
|
+
else
|
|
18
|
+
puts "Error: Failed to clone repository."
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contextizer
|
|
4
|
+
module Renderers
|
|
5
|
+
class Base
|
|
6
|
+
# @param context [Contextizer::Context] The context object to be rendered.
|
|
7
|
+
def self.call(context:)
|
|
8
|
+
raise NotImplementedError, "#{self.name} must implement the .call method"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|