aircana 0.1.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 +7 -0
- data/.devcontainer/devcontainer.json +36 -0
- data/.dockerignore +14 -0
- data/.rspec_status +106 -0
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +19 -0
- data/CLAUDE.md +58 -0
- data/Dockerfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +251 -0
- data/Rakefile +12 -0
- data/SECURITY.md +15 -0
- data/compose.yml +13 -0
- data/exe/aircana +13 -0
- data/lib/aircana/cli/app.rb +93 -0
- data/lib/aircana/cli/commands/add_directory.rb +148 -0
- data/lib/aircana/cli/commands/add_files.rb +26 -0
- data/lib/aircana/cli/commands/agents.rb +152 -0
- data/lib/aircana/cli/commands/clear_files.rb +16 -0
- data/lib/aircana/cli/commands/doctor.rb +85 -0
- data/lib/aircana/cli/commands/doctor_checks.rb +131 -0
- data/lib/aircana/cli/commands/doctor_helpers.rb +119 -0
- data/lib/aircana/cli/commands/dump_context.rb +23 -0
- data/lib/aircana/cli/commands/generate.rb +34 -0
- data/lib/aircana/cli/commands/install.rb +67 -0
- data/lib/aircana/cli/commands/plan.rb +69 -0
- data/lib/aircana/cli/commands/work.rb +69 -0
- data/lib/aircana/cli/shell_command.rb +13 -0
- data/lib/aircana/cli/subcommand.rb +19 -0
- data/lib/aircana/cli.rb +8 -0
- data/lib/aircana/configuration.rb +41 -0
- data/lib/aircana/contexts/confluence.rb +141 -0
- data/lib/aircana/contexts/confluence_content.rb +36 -0
- data/lib/aircana/contexts/confluence_http.rb +41 -0
- data/lib/aircana/contexts/confluence_logging.rb +71 -0
- data/lib/aircana/contexts/confluence_setup.rb +15 -0
- data/lib/aircana/contexts/local.rb +47 -0
- data/lib/aircana/contexts/relevant_files.rb +78 -0
- data/lib/aircana/fzf_helper.rb +117 -0
- data/lib/aircana/generators/agents_generator.rb +75 -0
- data/lib/aircana/generators/base_generator.rb +61 -0
- data/lib/aircana/generators/helpers.rb +16 -0
- data/lib/aircana/generators/relevant_files_command_generator.rb +36 -0
- data/lib/aircana/generators/relevant_files_verbose_results_generator.rb +34 -0
- data/lib/aircana/generators.rb +10 -0
- data/lib/aircana/human_logger.rb +143 -0
- data/lib/aircana/initializers.rb +8 -0
- data/lib/aircana/llm/claude_client.rb +86 -0
- data/lib/aircana/progress_tracker.rb +55 -0
- data/lib/aircana/system_checker.rb +177 -0
- data/lib/aircana/templates/agents/base_agent.erb +30 -0
- data/lib/aircana/templates/agents/defaults/planner.erb +126 -0
- data/lib/aircana/templates/agents/defaults/worker.erb +185 -0
- data/lib/aircana/templates/commands/add_relevant_files.erb +3 -0
- data/lib/aircana/templates/relevant_files_verbose_results.erb +18 -0
- data/lib/aircana/version.rb +5 -0
- data/lib/aircana.rb +53 -0
- data/sig/aircana.rbs +4 -0
- metadata +189 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aircana
|
4
|
+
module Contexts
|
5
|
+
module ConfluenceLogging
|
6
|
+
private
|
7
|
+
|
8
|
+
def log_request(method, path, query_params = nil, pagination: false)
|
9
|
+
config = Aircana.configuration
|
10
|
+
full_url = "#{config.confluence_base_url}#{path}"
|
11
|
+
|
12
|
+
log_parts = build_request_log_parts(method, full_url, query_params, config)
|
13
|
+
log_message = log_parts.join(" | ")
|
14
|
+
|
15
|
+
output_request_log(log_message, pagination)
|
16
|
+
end
|
17
|
+
|
18
|
+
def log_response(response, context = nil, pagination: false)
|
19
|
+
return if pagination
|
20
|
+
|
21
|
+
status_color = response_status_color(response)
|
22
|
+
status_text = response.success? ? "✓" : "✗"
|
23
|
+
|
24
|
+
log_parts = build_response_log_parts(response, context, status_color, status_text)
|
25
|
+
|
26
|
+
Aircana.human_logger.info "#{log_parts.join(" | ")}\e[0m"
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_api_error(operation, error, message)
|
30
|
+
Aircana.human_logger.error "Failed to #{operation}: #{error.message}"
|
31
|
+
raise Error, "#{message}: #{error.message}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_request_log_parts(method, full_url, query_params, config)
|
35
|
+
log_parts = ["#{method.upcase} #{full_url}"]
|
36
|
+
|
37
|
+
if query_params && !query_params.empty?
|
38
|
+
query_string = query_params.map { |k, v| "#{k}=#{v}" }.join("&")
|
39
|
+
log_parts << "Query: #{query_string}"
|
40
|
+
end
|
41
|
+
|
42
|
+
log_parts << "Auth: Basic #{config.confluence_username}:***"
|
43
|
+
log_parts
|
44
|
+
end
|
45
|
+
|
46
|
+
def output_request_log(log_message, pagination)
|
47
|
+
if pagination
|
48
|
+
print "\r\e[36m🌐 #{log_message}\e[0m\e[K"
|
49
|
+
else
|
50
|
+
Aircana.human_logger.info log_message
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def response_status_color(response)
|
55
|
+
response.success? ? "32" : "31"
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_response_log_parts(response, context, status_color, status_text)
|
59
|
+
log_parts = ["\e[#{status_color}m#{status_text} Response: #{response.code}"]
|
60
|
+
log_parts << context if context
|
61
|
+
|
62
|
+
if response.body && !response.body.empty?
|
63
|
+
body_preview = response.body.length > 200 ? "#{response.body[0..200]}..." : response.body
|
64
|
+
log_parts << "Body: #{body_preview}"
|
65
|
+
end
|
66
|
+
|
67
|
+
log_parts
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aircana
|
4
|
+
module Contexts
|
5
|
+
module ConfluenceSetup
|
6
|
+
def setup_httparty
|
7
|
+
config = Aircana.configuration
|
8
|
+
|
9
|
+
self.class.base_uri config.confluence_base_url
|
10
|
+
self.class.basic_auth config.confluence_username, config.confluence_api_token
|
11
|
+
self.class.headers "Content-Type" => "application/json"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module Aircana
|
6
|
+
module Contexts
|
7
|
+
class Local
|
8
|
+
def store_content(title:, content:, agent:)
|
9
|
+
agent_dir = create_agent_knowledge_dir(agent)
|
10
|
+
filename = sanitize_filename(title)
|
11
|
+
filepath = File.join(agent_dir, "#{filename}.md")
|
12
|
+
|
13
|
+
File.write(filepath, content)
|
14
|
+
Aircana.human_logger.success "Stored '#{title}' for agent '#{agent}' at #{filepath}"
|
15
|
+
|
16
|
+
filepath
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_agent_knowledge_dir(agent)
|
22
|
+
config = Aircana.configuration
|
23
|
+
agent_dir = File.join(config.agent_knowledge_dir, agent, "knowledge")
|
24
|
+
|
25
|
+
FileUtils.mkdir_p(agent_dir)
|
26
|
+
|
27
|
+
agent_dir
|
28
|
+
end
|
29
|
+
|
30
|
+
def sanitize_filename(title)
|
31
|
+
# Replace invalid characters with safe alternatives
|
32
|
+
# Remove or replace characters that are problematic in filenames
|
33
|
+
sanitized = title.strip
|
34
|
+
.gsub(%r{[<>:"/\\|?*]}, "-") # Replace invalid chars with hyphens
|
35
|
+
.gsub(/\s+/, "-") # Replace spaces with hyphens
|
36
|
+
.gsub(/-+/, "-") # Collapse multiple hyphens
|
37
|
+
.gsub(/^-|-$/, "") # Remove leading/trailing hyphens
|
38
|
+
|
39
|
+
# Ensure the filename isn't empty and isn't too long
|
40
|
+
sanitized = "untitled" if sanitized.empty?
|
41
|
+
sanitized = sanitized[0, 200] if sanitized.length > 200 # Limit to 200 chars
|
42
|
+
|
43
|
+
sanitized
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aircana
|
4
|
+
module Contexts
|
5
|
+
class RelevantFiles
|
6
|
+
class << self
|
7
|
+
# TODO: Honor the provided verbose flag
|
8
|
+
def print(_verbose: false)
|
9
|
+
verbose_generator(default_stream: true).generate
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(files)
|
13
|
+
files = Array(files)
|
14
|
+
|
15
|
+
return if files.empty?
|
16
|
+
|
17
|
+
Aircana.create_dir_if_needed(Aircana.configuration.relevant_project_files_dir)
|
18
|
+
|
19
|
+
files.each do |file|
|
20
|
+
absolute_file_path = File.expand_path(file)
|
21
|
+
link_path = "#{Aircana.configuration.relevant_project_files_dir}/#{File.basename(file)}"
|
22
|
+
|
23
|
+
FileUtils.rm_f(link_path)
|
24
|
+
File.symlink(absolute_file_path, link_path)
|
25
|
+
end
|
26
|
+
|
27
|
+
rewrite_verbose_file
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove(files)
|
31
|
+
files = Array(files)
|
32
|
+
|
33
|
+
return if files.empty?
|
34
|
+
|
35
|
+
Aircana.create_dir_if_needed(Aircana.configuration.relevant_project_files_dir)
|
36
|
+
|
37
|
+
files.each do |file|
|
38
|
+
link_path = "#{Aircana.configuration.relevant_project_files_dir}/#{File.basename(file)}"
|
39
|
+
FileUtils.rm_f(link_path)
|
40
|
+
end
|
41
|
+
|
42
|
+
rewrite_verbose_file
|
43
|
+
end
|
44
|
+
|
45
|
+
def remove_all
|
46
|
+
return unless directory_exists?
|
47
|
+
|
48
|
+
Dir.glob("#{Aircana.configuration.relevant_project_files_dir}/*").each do |file|
|
49
|
+
FileUtils.rm_f(file)
|
50
|
+
end
|
51
|
+
|
52
|
+
return unless Dir.empty?(Aircana.configuration.relevant_project_files_dir)
|
53
|
+
|
54
|
+
Dir.rmdir(Aircana.configuration.relevant_project_files_dir)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def rewrite_verbose_file
|
60
|
+
verbose_generator.generate
|
61
|
+
|
62
|
+
# TODO: If the verbose file uses too many tokens, warn and instead use only
|
63
|
+
# the summary generatior or do something smart like summarize file contents
|
64
|
+
end
|
65
|
+
|
66
|
+
def verbose_generator(default_stream: false)
|
67
|
+
Generators::RelevantFilesVerboseResultsGenerator.new(
|
68
|
+
file_out: default_stream ? Aircana.configuration.stream : nil
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def directory_exists?
|
73
|
+
Dir.exist?(Aircana.configuration.relevant_project_files_dir)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aircana
|
4
|
+
class FzfHelper
|
5
|
+
class << self
|
6
|
+
def select_files_interactively(header: "Select files", multi: true)
|
7
|
+
return [] unless fzf_available?
|
8
|
+
|
9
|
+
execute_fzf_selection(header: header, multi: multi)
|
10
|
+
rescue StandardError => e
|
11
|
+
Aircana.human_logger.error "File selection failed: #{e.message}"
|
12
|
+
[]
|
13
|
+
end
|
14
|
+
|
15
|
+
def fzf_available?
|
16
|
+
return true if command_available?("fzf")
|
17
|
+
|
18
|
+
handle_missing_dependency
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute_fzf_selection(header:, multi:)
|
23
|
+
command = build_fzf_command(header: header, multi: multi)
|
24
|
+
result = `#{command}`.strip
|
25
|
+
return [] if result.empty?
|
26
|
+
|
27
|
+
result.split("\n").map(&:strip).reject(&:empty?)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def command_available?(command)
|
33
|
+
system("which #{command}", out: File::NULL, err: File::NULL)
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle_missing_dependency
|
37
|
+
Aircana.human_logger.error "fzf is required but not installed"
|
38
|
+
Aircana.human_logger.info "To install fzf:"
|
39
|
+
Aircana.human_logger.info " • macOS: brew install fzf"
|
40
|
+
Aircana.human_logger.info " • Ubuntu/Debian: apt install fzf"
|
41
|
+
Aircana.human_logger.info " • Other: https://github.com/junegunn/fzf#installation"
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_fzf_command(header:, multi:)
|
45
|
+
options = base_fzf_options(header: header, multi: multi)
|
46
|
+
preview_options = preview_command_options
|
47
|
+
key_bindings = key_binding_options
|
48
|
+
|
49
|
+
"#{generate_file_list_command} | fzf #{options} #{preview_options} #{key_bindings}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def base_fzf_options(header:, multi:)
|
53
|
+
options = build_fzf_option_list(multi: multi)
|
54
|
+
options += build_fzf_display_options(header: header)
|
55
|
+
options.join(" ")
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_fzf_option_list(multi:)
|
59
|
+
options = ["--ansi", "--border", "--height=80%", "--layout=reverse", "--info=inline"]
|
60
|
+
options << "--multi" if multi
|
61
|
+
options
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_fzf_display_options(header:)
|
65
|
+
[
|
66
|
+
"--header='#{header}'",
|
67
|
+
"--header-lines=0",
|
68
|
+
"--prompt='❯ '",
|
69
|
+
"--pointer='▶'",
|
70
|
+
"--marker='✓'"
|
71
|
+
]
|
72
|
+
end
|
73
|
+
|
74
|
+
def preview_command_options
|
75
|
+
preview_cmd = preview_command
|
76
|
+
return "" if preview_cmd.nil?
|
77
|
+
|
78
|
+
[
|
79
|
+
"--preview='#{preview_cmd}'",
|
80
|
+
"--preview-window='right:60%:wrap'",
|
81
|
+
"--preview-label='Preview'"
|
82
|
+
].join(" ")
|
83
|
+
end
|
84
|
+
|
85
|
+
def preview_command
|
86
|
+
# Try to use bat for syntax highlighting, fall back to head/cat
|
87
|
+
if command_available?("bat")
|
88
|
+
"bat --color=always --style=header,grid --line-range :50 {}"
|
89
|
+
elsif command_available?("head")
|
90
|
+
"head -50 {}"
|
91
|
+
else
|
92
|
+
"cat {}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def key_binding_options
|
97
|
+
[
|
98
|
+
"--bind='ctrl-a:select-all'",
|
99
|
+
"--bind='ctrl-d:deselect-all'",
|
100
|
+
"--bind='ctrl-/:toggle-preview'",
|
101
|
+
"--bind='ctrl-u:preview-page-up'",
|
102
|
+
"--bind='ctrl-n:preview-page-down'",
|
103
|
+
"--bind='?:toggle-preview'"
|
104
|
+
].join(" ")
|
105
|
+
end
|
106
|
+
|
107
|
+
def generate_file_list_command
|
108
|
+
# Use fd if available for better performance and .gitignore respect
|
109
|
+
if command_available?("fd")
|
110
|
+
"fd --type f --hidden --exclude .git"
|
111
|
+
else
|
112
|
+
"find . -type f -not -path '*/\\.git/*' -not -path '*/node_modules/*' -not -path '*/\\.vscode/*'"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../generators"
|
4
|
+
|
5
|
+
module Aircana
|
6
|
+
module Generators
|
7
|
+
class AgentsGenerator < BaseGenerator
|
8
|
+
attr_reader :agent_name, :short_description, :description, :model, :color, :default_agent
|
9
|
+
|
10
|
+
AVAILABLE_DEFAULT_AGENTS = %w[planner worker].freeze
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def create_default_agent(agent_name)
|
14
|
+
unless AVAILABLE_DEFAULT_AGENTS.include?(agent_name)
|
15
|
+
raise ArgumentError, "Unknown default agent: #{agent_name}"
|
16
|
+
end
|
17
|
+
|
18
|
+
new(agent_name: agent_name, default_agent: true).generate
|
19
|
+
end
|
20
|
+
|
21
|
+
def available_default_agents
|
22
|
+
AVAILABLE_DEFAULT_AGENTS
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
27
|
+
agent_name:, short_description: nil, description: nil, model: nil, color: nil,
|
28
|
+
file_in: nil, file_out: nil, default_agent: false
|
29
|
+
)
|
30
|
+
@agent_name = agent_name
|
31
|
+
@short_description = short_description
|
32
|
+
@description = description
|
33
|
+
@model = model
|
34
|
+
@color = color
|
35
|
+
@default_agent = default_agent
|
36
|
+
|
37
|
+
super(
|
38
|
+
file_in: file_in || default_template_path,
|
39
|
+
file_out: file_out || default_output_path
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def locals
|
46
|
+
super.merge({
|
47
|
+
relevant_project_files_path:, agent_name:, short_description:, description:,
|
48
|
+
model:, color:, knowledge_path:
|
49
|
+
})
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def default_template_path
|
55
|
+
if default_agent
|
56
|
+
File.join(File.dirname(__FILE__), "..", "templates", "agents", "defaults", "#{agent_name}.erb")
|
57
|
+
else
|
58
|
+
File.join(File.dirname(__FILE__), "..", "templates", "agents", "base_agent.erb")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_output_path
|
63
|
+
File.join(Aircana.configuration.claude_code_project_config_path, "agents", "#{agent_name}.md")
|
64
|
+
end
|
65
|
+
|
66
|
+
def relevant_project_files_path
|
67
|
+
File.join(Aircana.configuration.relevant_project_files_dir, "relevant_files.md")
|
68
|
+
end
|
69
|
+
|
70
|
+
def knowledge_path
|
71
|
+
".aircana/agents/#{agent_name}/knowledge"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
require "fileutils"
|
5
|
+
require_relative "helpers"
|
6
|
+
|
7
|
+
module Aircana
|
8
|
+
module Generators
|
9
|
+
class BaseGenerator
|
10
|
+
attr_reader :file_in, :file_out
|
11
|
+
|
12
|
+
def initialize(file_in: nil, file_out: nil)
|
13
|
+
@file_in = file_in
|
14
|
+
@file_out = file_out
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate
|
18
|
+
prepare_output_directory
|
19
|
+
content = generate_content
|
20
|
+
write_content(content)
|
21
|
+
|
22
|
+
file_out
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def prepare_output_directory
|
28
|
+
return unless file_out.is_a?(String)
|
29
|
+
|
30
|
+
FileUtils.mkdir_p(File.dirname(file_out))
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_content
|
34
|
+
erb = ERB.new(template)
|
35
|
+
Aircana.human_logger.info "Generating #{file_out} from #{file_in}"
|
36
|
+
# Debug info removed for cleaner output
|
37
|
+
erb.result_with_hash(locals)
|
38
|
+
end
|
39
|
+
|
40
|
+
def write_content(content)
|
41
|
+
if file_out.respond_to?(:write)
|
42
|
+
file_out.write(content)
|
43
|
+
else
|
44
|
+
File.write(file_out, content)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def locals
|
51
|
+
{ helpers: Helpers }
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def template
|
57
|
+
File.read(file_in)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aircana
|
4
|
+
module Generators
|
5
|
+
module Helpers
|
6
|
+
class << self
|
7
|
+
def model_instructions(instructions, important: false)
|
8
|
+
<<~INSTRUCTIONS
|
9
|
+
INSTRUCTIONS #{"IMPORTANT" if important}:
|
10
|
+
#{instructions}
|
11
|
+
INSTRUCTIONS
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../generators"
|
4
|
+
|
5
|
+
module Aircana
|
6
|
+
module Generators
|
7
|
+
class RelevantFilesCommandGenerator < BaseGenerator
|
8
|
+
def initialize(file_in: nil, file_out: nil)
|
9
|
+
super(
|
10
|
+
file_in: file_in || default_template_path,
|
11
|
+
file_out: file_out || default_output_path
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def locals
|
18
|
+
super.merge({ relevant_project_files_path: })
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def default_template_path
|
24
|
+
File.join(File.dirname(__FILE__), "..", "templates", "commands", "add_relevant_files.erb")
|
25
|
+
end
|
26
|
+
|
27
|
+
def default_output_path
|
28
|
+
File.join(Aircana.configuration.output_dir, "commands", "ac-add-relevant-files.md")
|
29
|
+
end
|
30
|
+
|
31
|
+
def relevant_project_files_path
|
32
|
+
File.join(Aircana.configuration.relevant_project_files_dir, "relevant_files.md")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../generators"
|
4
|
+
|
5
|
+
module Aircana
|
6
|
+
module Generators
|
7
|
+
class RelevantFilesVerboseResultsGenerator < BaseGenerator
|
8
|
+
def initialize(file_in: nil, file_out: nil)
|
9
|
+
super(
|
10
|
+
file_in: file_in || default_template_path,
|
11
|
+
file_out: file_out || default_output_path
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def locals
|
18
|
+
super.merge({ relevant_files: })
|
19
|
+
end
|
20
|
+
|
21
|
+
def relevant_files
|
22
|
+
Dir.glob("#{Aircana.configuration.relevant_project_files_dir}/*")
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_template_path
|
26
|
+
File.join(File.dirname(__FILE__), "..", "templates", "relevant_files_verbose_results.erb")
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_output_path
|
30
|
+
File.join(Aircana.configuration.relevant_project_files_dir, "relevant_files.md")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "generators/base_generator"
|
4
|
+
require_relative "generators/relevant_files_command_generator"
|
5
|
+
require_relative "generators/relevant_files_verbose_results_generator"
|
6
|
+
|
7
|
+
module Aircana
|
8
|
+
module Generators
|
9
|
+
end
|
10
|
+
end
|