aircana 0.1.0 → 1.1.0.rc1
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/.rspec_status +164 -104
- data/.rubocop.yml +34 -0
- data/CLAUDE.md +54 -9
- data/README.md +31 -19
- data/lib/aircana/cli/app.rb +93 -26
- data/lib/aircana/cli/commands/agents.rb +75 -4
- data/lib/aircana/cli/commands/files.rb +65 -0
- data/lib/aircana/cli/commands/generate.rb +19 -0
- data/lib/aircana/cli/commands/hooks.rb +276 -0
- data/lib/aircana/cli/commands/install.rb +113 -0
- data/lib/aircana/cli/commands/project.rb +156 -0
- data/lib/aircana/cli/help_formatter.rb +90 -0
- data/lib/aircana/cli/subcommand.rb +2 -1
- data/lib/aircana/configuration.rb +2 -1
- data/lib/aircana/contexts/confluence.rb +76 -3
- data/lib/aircana/contexts/manifest.rb +148 -0
- data/lib/aircana/generators/agents_generator.rb +1 -1
- data/lib/aircana/generators/hooks_generator.rb +82 -0
- data/lib/aircana/generators/project_config_generator.rb +54 -0
- data/lib/aircana/symlink_manager.rb +158 -0
- data/lib/aircana/system_checker.rb +10 -0
- data/lib/aircana/templates/hooks/bundle_install.erb +63 -0
- data/lib/aircana/templates/hooks/post_tool_use.erb +49 -0
- data/lib/aircana/templates/hooks/pre_tool_use.erb +43 -0
- data/lib/aircana/templates/hooks/rspec_test.erb +72 -0
- data/lib/aircana/templates/hooks/rubocop_pre_commit.erb +55 -0
- data/lib/aircana/templates/hooks/session_start.erb +127 -0
- data/lib/aircana/templates/hooks/user_prompt_submit.erb +46 -0
- data/lib/aircana/version.rb +1 -1
- metadata +19 -5
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_generator"
|
4
|
+
|
5
|
+
module Aircana
|
6
|
+
module Generators
|
7
|
+
class HooksGenerator < BaseGenerator
|
8
|
+
# All available hook types (for manual creation)
|
9
|
+
ALL_HOOK_TYPES = %w[
|
10
|
+
pre_tool_use
|
11
|
+
post_tool_use
|
12
|
+
user_prompt_submit
|
13
|
+
session_start
|
14
|
+
rubocop_pre_commit
|
15
|
+
rspec_test
|
16
|
+
bundle_install
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
# Default hooks that are auto-installed
|
20
|
+
DEFAULT_HOOK_TYPES = %w[
|
21
|
+
session_start
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def available_default_hooks
|
26
|
+
DEFAULT_HOOK_TYPES
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_available_hooks
|
30
|
+
ALL_HOOK_TYPES
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_default_hook(hook_name)
|
34
|
+
return unless all_available_hooks.include?(hook_name)
|
35
|
+
|
36
|
+
template_path = File.join(File.dirname(__FILE__), "..", "templates", "hooks", "#{hook_name}.erb")
|
37
|
+
output_path = File.join(Aircana.configuration.hooks_dir, "#{hook_name}.sh")
|
38
|
+
|
39
|
+
generator = new(file_in: template_path, file_out: output_path)
|
40
|
+
generator.generate
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_all_default_hooks
|
44
|
+
available_default_hooks.each { |hook_name| create_default_hook(hook_name) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(hook_name: nil, **)
|
49
|
+
@hook_name = hook_name
|
50
|
+
|
51
|
+
if hook_name
|
52
|
+
template_path = File.join(File.dirname(__FILE__), "..", "templates", "hooks", "#{hook_name}.erb")
|
53
|
+
output_path = File.join(Aircana.configuration.hooks_dir, "#{hook_name}.sh")
|
54
|
+
super(file_in: template_path, file_out: output_path)
|
55
|
+
else
|
56
|
+
super(**)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def generate
|
61
|
+
result = super
|
62
|
+
make_executable if File.exist?(file_out)
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def locals
|
69
|
+
super.merge(
|
70
|
+
hook_name: @hook_name,
|
71
|
+
project_root: Dir.pwd
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def make_executable
|
78
|
+
File.chmod(0o755, file_out)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_generator"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Aircana
|
7
|
+
module Generators
|
8
|
+
class ProjectConfigGenerator < BaseGenerator
|
9
|
+
def initialize(file_in: nil, file_out: nil)
|
10
|
+
super(
|
11
|
+
file_in: file_in || default_template_path,
|
12
|
+
file_out: file_out || default_output_path
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate
|
17
|
+
# Create the project.json with default content
|
18
|
+
project_config = default_project_config
|
19
|
+
|
20
|
+
Aircana.create_dir_if_needed(File.dirname(file_out))
|
21
|
+
File.write(file_out, JSON.pretty_generate(project_config))
|
22
|
+
|
23
|
+
Aircana.human_logger.success "Generated project.json at #{file_out}"
|
24
|
+
file_out
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def default_template_path
|
30
|
+
# We don't use a template for this, generate directly
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_output_path
|
35
|
+
File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_project_config
|
39
|
+
{
|
40
|
+
"folders" => [],
|
41
|
+
"_comment" => [
|
42
|
+
"Add folders to include agents from sub-projects",
|
43
|
+
"Example:",
|
44
|
+
" 'folders': [",
|
45
|
+
" { 'path': 'frontend' },",
|
46
|
+
" { 'path': 'backend' },",
|
47
|
+
" { 'path': 'shared/utils' }",
|
48
|
+
" ]"
|
49
|
+
]
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Aircana
|
7
|
+
class SymlinkManager
|
8
|
+
class << self
|
9
|
+
def sync_multi_root_agents
|
10
|
+
project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
|
11
|
+
|
12
|
+
unless File.exist?(project_json_path)
|
13
|
+
Aircana.human_logger.info "No project.json found, skipping multi-root sync"
|
14
|
+
return { agents: 0, knowledge: 0 }
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
config = JSON.parse(File.read(project_json_path))
|
19
|
+
folders = config["folders"] || []
|
20
|
+
|
21
|
+
if folders.empty?
|
22
|
+
Aircana.human_logger.info "No folders configured in project.json"
|
23
|
+
return { agents: 0, knowledge: 0 }
|
24
|
+
end
|
25
|
+
|
26
|
+
cleanup_broken_symlinks
|
27
|
+
create_symlinks_for_folders(folders)
|
28
|
+
rescue JSON::ParserError => e
|
29
|
+
Aircana.human_logger.error "Invalid JSON in project.json: #{e.message}"
|
30
|
+
{ agents: 0, knowledge: 0 }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def cleanup_broken_symlinks
|
35
|
+
claude_agents_dir = File.join(Aircana.configuration.project_dir, ".claude", "agents")
|
36
|
+
aircana_agents_dir = File.join(Aircana.configuration.project_dir, ".aircana", "agents")
|
37
|
+
|
38
|
+
[claude_agents_dir, aircana_agents_dir].each do |dir|
|
39
|
+
next unless Dir.exist?(dir)
|
40
|
+
|
41
|
+
Dir.glob("#{dir}/*").each do |path|
|
42
|
+
if File.symlink?(path) && !File.exist?(path)
|
43
|
+
File.delete(path)
|
44
|
+
Aircana.human_logger.info "Removed broken symlink: #{path}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_symlinks_for_folders(folders)
|
51
|
+
stats = { agents: 0, knowledge: 0 }
|
52
|
+
|
53
|
+
folders.each do |folder_config|
|
54
|
+
folder_path = folder_config["path"]
|
55
|
+
next unless folder_path_valid?(folder_path)
|
56
|
+
|
57
|
+
prefix = folder_path.tr("/", "_")
|
58
|
+
|
59
|
+
stats[:agents] += link_agents(folder_path, prefix)
|
60
|
+
stats[:knowledge] += link_knowledge(folder_path, prefix)
|
61
|
+
end
|
62
|
+
|
63
|
+
Aircana.human_logger.success "Linked #{stats[:agents]} agents and #{stats[:knowledge]} knowledge bases"
|
64
|
+
stats
|
65
|
+
end
|
66
|
+
|
67
|
+
def link_agents(folder_path, prefix)
|
68
|
+
source_dir = File.join(folder_path, ".claude", "agents")
|
69
|
+
target_dir = File.join(Aircana.configuration.project_dir, ".claude", "agents")
|
70
|
+
|
71
|
+
return 0 unless Dir.exist?(source_dir)
|
72
|
+
|
73
|
+
FileUtils.mkdir_p(target_dir)
|
74
|
+
linked = 0
|
75
|
+
|
76
|
+
Dir.glob("#{source_dir}/*.md").each do |agent_file|
|
77
|
+
agent_name = File.basename(agent_file, ".md")
|
78
|
+
link_name = "#{prefix}_#{agent_name}.md"
|
79
|
+
target_path = File.join(target_dir, link_name)
|
80
|
+
|
81
|
+
# Use relative paths for symlinks
|
82
|
+
relative_path = calculate_relative_path(target_dir, agent_file)
|
83
|
+
|
84
|
+
File.symlink(relative_path, target_path) unless File.exist?(target_path)
|
85
|
+
linked += 1
|
86
|
+
Aircana.human_logger.info "Linked agent: #{link_name}"
|
87
|
+
end
|
88
|
+
|
89
|
+
linked
|
90
|
+
end
|
91
|
+
|
92
|
+
def link_knowledge(folder_path, prefix)
|
93
|
+
source_dir = File.join(folder_path, ".aircana", "agents")
|
94
|
+
target_dir = File.join(Aircana.configuration.project_dir, ".aircana", "agents")
|
95
|
+
|
96
|
+
return 0 unless Dir.exist?(source_dir)
|
97
|
+
|
98
|
+
FileUtils.mkdir_p(target_dir)
|
99
|
+
linked = 0
|
100
|
+
|
101
|
+
Dir.glob("#{source_dir}/*").each do |agent_dir|
|
102
|
+
next unless File.directory?(agent_dir)
|
103
|
+
|
104
|
+
agent_name = File.basename(agent_dir)
|
105
|
+
link_name = "#{prefix}_#{agent_name}"
|
106
|
+
target_path = File.join(target_dir, link_name)
|
107
|
+
|
108
|
+
# Use relative paths for symlinks
|
109
|
+
relative_path = calculate_relative_path(target_dir, agent_dir)
|
110
|
+
|
111
|
+
File.symlink(relative_path, target_path) unless File.exist?(target_path)
|
112
|
+
linked += 1
|
113
|
+
Aircana.human_logger.info "Linked knowledge: #{link_name}"
|
114
|
+
end
|
115
|
+
|
116
|
+
linked
|
117
|
+
end
|
118
|
+
|
119
|
+
def folder_path_valid?(folder_path)
|
120
|
+
full_path = File.join(Aircana.configuration.project_dir, folder_path)
|
121
|
+
|
122
|
+
unless Dir.exist?(full_path)
|
123
|
+
Aircana.human_logger.warn "Folder not found: #{folder_path}"
|
124
|
+
return false
|
125
|
+
end
|
126
|
+
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
def calculate_relative_path(from_dir, to_path)
|
131
|
+
# Calculate the relative path from one directory to another
|
132
|
+
from = Pathname.new(File.expand_path(from_dir))
|
133
|
+
to = Pathname.new(File.expand_path(to_path))
|
134
|
+
to.relative_path_from(from).to_s
|
135
|
+
end
|
136
|
+
|
137
|
+
# Helper methods for resolving symlinked agent paths
|
138
|
+
def resolve_agent_path(agent_name)
|
139
|
+
agent_path = File.join(Aircana.configuration.agent_knowledge_dir, agent_name)
|
140
|
+
|
141
|
+
if File.symlink?(agent_path)
|
142
|
+
File.readlink(agent_path)
|
143
|
+
else
|
144
|
+
agent_path
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def agent_is_symlinked?(agent_name)
|
149
|
+
agent_path = File.join(Aircana.configuration.agent_knowledge_dir, agent_name)
|
150
|
+
File.symlink?(agent_path)
|
151
|
+
end
|
152
|
+
|
153
|
+
def resolve_symlinked_path(path)
|
154
|
+
File.symlink?(path) ? File.readlink(path) : path
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -23,6 +23,16 @@ module Aircana
|
|
23
23
|
"Arch" => "pacman -S git",
|
24
24
|
"Other" => "https://git-scm.com/downloads"
|
25
25
|
}
|
26
|
+
},
|
27
|
+
"jq" => {
|
28
|
+
purpose: "JSON parsing for multi-root configuration",
|
29
|
+
install: {
|
30
|
+
"macOS" => "brew install jq",
|
31
|
+
"Ubuntu/Debian" => "apt install jq",
|
32
|
+
"Fedora/CentOS" => "dnf install jq",
|
33
|
+
"Arch" => "pacman -S jq",
|
34
|
+
"Other" => "https://jqlang.github.io/jq/download/"
|
35
|
+
}
|
26
36
|
}
|
27
37
|
}.freeze
|
28
38
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# Bundle install hook generated by Aircana
|
3
|
+
# This hook runs bundle install when Gemfile changes are detected
|
4
|
+
|
5
|
+
# Get the tool details
|
6
|
+
TOOL_NAME="$1"
|
7
|
+
TOOL_PARAMS="$2"
|
8
|
+
|
9
|
+
# Only act on file modification tools
|
10
|
+
case "$TOOL_NAME" in
|
11
|
+
"Edit"|"Write"|"MultiEdit")
|
12
|
+
;;
|
13
|
+
*)
|
14
|
+
exit 0
|
15
|
+
;;
|
16
|
+
esac
|
17
|
+
|
18
|
+
# Check if Gemfile was modified
|
19
|
+
if ! echo "$TOOL_PARAMS" | grep -q "Gemfile"; then
|
20
|
+
exit 0
|
21
|
+
fi
|
22
|
+
|
23
|
+
# Check if this is a Ruby project
|
24
|
+
if [ ! -f "Gemfile" ]; then
|
25
|
+
exit 0
|
26
|
+
fi
|
27
|
+
|
28
|
+
echo "Gemfile modified, running bundle install..."
|
29
|
+
|
30
|
+
# Run bundle install
|
31
|
+
if command -v bundle >/dev/null 2>&1; then
|
32
|
+
bundle install
|
33
|
+
BUNDLE_EXIT_CODE=$?
|
34
|
+
|
35
|
+
if [ $BUNDLE_EXIT_CODE -eq 0 ]; then
|
36
|
+
cat << EOF
|
37
|
+
{
|
38
|
+
"type": "advanced",
|
39
|
+
"decision": "allow",
|
40
|
+
"message": "Bundle install completed successfully after Gemfile modification."
|
41
|
+
}
|
42
|
+
EOF
|
43
|
+
else
|
44
|
+
cat << EOF
|
45
|
+
{
|
46
|
+
"type": "advanced",
|
47
|
+
"decision": "allow",
|
48
|
+
"message": "Bundle install failed. You may need to run 'bundle install' manually.\n\nError code: $BUNDLE_EXIT_CODE"
|
49
|
+
}
|
50
|
+
EOF
|
51
|
+
fi
|
52
|
+
else
|
53
|
+
cat << EOF
|
54
|
+
{
|
55
|
+
"type": "advanced",
|
56
|
+
"decision": "allow",
|
57
|
+
"message": "Bundler not found. Please install bundler and run 'bundle install' manually."
|
58
|
+
}
|
59
|
+
EOF
|
60
|
+
fi
|
61
|
+
|
62
|
+
# Always allow the modification to proceed
|
63
|
+
exit 0
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# Post-tool-use hook generated by Aircana
|
3
|
+
# This hook runs after a tool has been executed
|
4
|
+
|
5
|
+
# Get tool execution details
|
6
|
+
TOOL_NAME="$1"
|
7
|
+
TOOL_PARAMS="$2"
|
8
|
+
TOOL_RESULT="$3"
|
9
|
+
EXIT_CODE="$4"
|
10
|
+
|
11
|
+
# Log the tool execution
|
12
|
+
echo "$(date): Tool '$TOOL_NAME' completed with exit code: $EXIT_CODE" >> ~/.aircana/hooks.log
|
13
|
+
|
14
|
+
# Initialize context for response
|
15
|
+
ADDITIONAL_CONTEXT=""
|
16
|
+
|
17
|
+
# Handle specific tools
|
18
|
+
case "$TOOL_NAME" in
|
19
|
+
"Edit"|"Write")
|
20
|
+
# Auto-update relevant files context if code files were modified
|
21
|
+
if echo "$TOOL_PARAMS" | grep -E "\.(rb|js|py|ts)$" && [ "$EXIT_CODE" -eq 0 ]; then
|
22
|
+
ADDITIONAL_CONTEXT="Code file modified - consider updating relevant files context."
|
23
|
+
fi
|
24
|
+
;;
|
25
|
+
"Bash")
|
26
|
+
# Handle bash command results
|
27
|
+
if [ "$EXIT_CODE" -ne 0 ]; then
|
28
|
+
echo "Bash command failed: $TOOL_PARAMS" >> ~/.aircana/hooks.log
|
29
|
+
ADDITIONAL_CONTEXT="Previous bash command failed with exit code $EXIT_CODE."
|
30
|
+
fi
|
31
|
+
;;
|
32
|
+
esac
|
33
|
+
|
34
|
+
# Output appropriate JSON response
|
35
|
+
if [ -n "$ADDITIONAL_CONTEXT" ]; then
|
36
|
+
# Escape context for JSON
|
37
|
+
ESCAPED_CONTEXT=$(echo -n "$ADDITIONAL_CONTEXT" | sed 's/"/\\"/g')
|
38
|
+
cat << EOF
|
39
|
+
{
|
40
|
+
"hookSpecificOutput": {
|
41
|
+
"hookEventName": "PostToolUse",
|
42
|
+
"additionalContext": "$ESCAPED_CONTEXT"
|
43
|
+
}
|
44
|
+
}
|
45
|
+
EOF
|
46
|
+
else
|
47
|
+
# No additional context needed
|
48
|
+
echo "{}"
|
49
|
+
fi
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# Pre-tool-use hook generated by Aircana
|
3
|
+
# This hook runs before any tool is executed
|
4
|
+
|
5
|
+
# Get the tool name and parameters
|
6
|
+
TOOL_NAME="$1"
|
7
|
+
TOOL_PARAMS="$2"
|
8
|
+
|
9
|
+
# Log the tool usage (optional)
|
10
|
+
echo "$(date): About to use tool: $TOOL_NAME" >> ~/.aircana/hooks.log
|
11
|
+
|
12
|
+
# Basic validation example
|
13
|
+
case "$TOOL_NAME" in
|
14
|
+
"Bash")
|
15
|
+
# Add validation for bash commands
|
16
|
+
if echo "$TOOL_PARAMS" | grep -q "rm -rf /"; then
|
17
|
+
cat << EOF
|
18
|
+
{
|
19
|
+
"hookSpecificOutput": {
|
20
|
+
"hookEventName": "PreToolUse",
|
21
|
+
"permissionDecision": "deny",
|
22
|
+
"permissionDecisionReason": "Dangerous command detected: rm -rf /"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
EOF
|
26
|
+
exit 0
|
27
|
+
fi
|
28
|
+
;;
|
29
|
+
"Edit"|"Write")
|
30
|
+
# Validate file operations
|
31
|
+
# Add custom validation logic here
|
32
|
+
;;
|
33
|
+
esac
|
34
|
+
|
35
|
+
# Allow the tool to proceed
|
36
|
+
cat << EOF
|
37
|
+
{
|
38
|
+
"hookSpecificOutput": {
|
39
|
+
"hookEventName": "PreToolUse",
|
40
|
+
"permissionDecision": "allow"
|
41
|
+
}
|
42
|
+
}
|
43
|
+
EOF
|
@@ -0,0 +1,72 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# RSpec test hook generated by Aircana
|
3
|
+
# This hook runs relevant tests when Ruby files are modified
|
4
|
+
|
5
|
+
# Get the tool details
|
6
|
+
TOOL_NAME="$1"
|
7
|
+
TOOL_PARAMS="$2"
|
8
|
+
|
9
|
+
# Only act on file modification tools
|
10
|
+
case "$TOOL_NAME" in
|
11
|
+
"Edit"|"Write"|"MultiEdit")
|
12
|
+
;;
|
13
|
+
*)
|
14
|
+
exit 0
|
15
|
+
;;
|
16
|
+
esac
|
17
|
+
|
18
|
+
# Check if this is a Ruby project with RSpec
|
19
|
+
if [ ! -f "Gemfile" ] || ! grep -q "rspec" Gemfile 2>/dev/null; then
|
20
|
+
exit 0
|
21
|
+
fi
|
22
|
+
|
23
|
+
# Extract file path from tool parameters (basic extraction)
|
24
|
+
MODIFIED_FILE=$(echo "$TOOL_PARAMS" | grep -o '"[^"]*\.rb"' | head -1 | tr -d '"')
|
25
|
+
|
26
|
+
if [ -z "$MODIFIED_FILE" ]; then
|
27
|
+
exit 0
|
28
|
+
fi
|
29
|
+
|
30
|
+
# Determine which specs to run based on the modified file
|
31
|
+
SPEC_FILE=""
|
32
|
+
|
33
|
+
# Check for corresponding spec file
|
34
|
+
if echo "$MODIFIED_FILE" | grep -q "^lib/"; then
|
35
|
+
# For lib files, look for spec file
|
36
|
+
SPEC_FILE="spec/$(echo "$MODIFIED_FILE" | sed 's|^lib/||' | sed 's|\.rb$|_spec.rb|')"
|
37
|
+
elif echo "$MODIFIED_FILE" | grep -q "^app/"; then
|
38
|
+
# For Rails app files
|
39
|
+
SPEC_FILE="spec/$(echo "$MODIFIED_FILE" | sed 's|^app/||' | sed 's|\.rb$|_spec.rb|')"
|
40
|
+
elif echo "$MODIFIED_FILE" | grep -q "_spec\.rb$"; then
|
41
|
+
# If the modified file is already a spec file
|
42
|
+
SPEC_FILE="$MODIFIED_FILE"
|
43
|
+
fi
|
44
|
+
|
45
|
+
# Check if spec file exists
|
46
|
+
if [ -n "$SPEC_FILE" ] && [ -f "$SPEC_FILE" ]; then
|
47
|
+
echo "Running tests for modified file: $MODIFIED_FILE"
|
48
|
+
|
49
|
+
# Run the specific test
|
50
|
+
if command -v bundle >/dev/null 2>&1; then
|
51
|
+
bundle exec rspec "$SPEC_FILE" --format progress
|
52
|
+
else
|
53
|
+
rspec "$SPEC_FILE" --format progress
|
54
|
+
fi
|
55
|
+
|
56
|
+
RSPEC_EXIT_CODE=$?
|
57
|
+
|
58
|
+
if [ $RSPEC_EXIT_CODE -ne 0 ]; then
|
59
|
+
cat << EOF
|
60
|
+
{
|
61
|
+
"type": "advanced",
|
62
|
+
"decision": "allow",
|
63
|
+
"message": "Tests failed for $SPEC_FILE. Consider fixing the failing tests.\n\nYou can run: bundle exec rspec $SPEC_FILE"
|
64
|
+
}
|
65
|
+
EOF
|
66
|
+
else
|
67
|
+
echo "Tests passed for $SPEC_FILE"
|
68
|
+
fi
|
69
|
+
fi
|
70
|
+
|
71
|
+
# Always allow the modification to proceed
|
72
|
+
exit 0
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# RuboCop pre-commit hook generated by Aircana
|
3
|
+
# This hook runs RuboCop before allowing commits
|
4
|
+
|
5
|
+
# Get the tool name and parameters
|
6
|
+
TOOL_NAME="$1"
|
7
|
+
TOOL_PARAMS="$2"
|
8
|
+
|
9
|
+
# Only act on Bash commands that look like git commits
|
10
|
+
if [ "$TOOL_NAME" != "Bash" ]; then
|
11
|
+
exit 0
|
12
|
+
fi
|
13
|
+
|
14
|
+
# Check if this is a git commit command
|
15
|
+
if ! echo "$TOOL_PARAMS" | grep -q "git commit"; then
|
16
|
+
exit 0
|
17
|
+
fi
|
18
|
+
|
19
|
+
# Check if we're in a Ruby project
|
20
|
+
if [ ! -f "Gemfile" ] && [ ! -f ".rubocop.yml" ]; then
|
21
|
+
exit 0
|
22
|
+
fi
|
23
|
+
|
24
|
+
# Get list of staged Ruby files
|
25
|
+
RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(rb)$' | tr '\n' ' ')
|
26
|
+
|
27
|
+
if [ -z "$RUBY_FILES" ]; then
|
28
|
+
# No Ruby files to check
|
29
|
+
exit 0
|
30
|
+
fi
|
31
|
+
|
32
|
+
echo "Running RuboCop on staged Ruby files..."
|
33
|
+
|
34
|
+
# Run RuboCop on staged files
|
35
|
+
if command -v bundle >/dev/null 2>&1 && [ -f "Gemfile" ]; then
|
36
|
+
bundle exec rubocop $RUBY_FILES
|
37
|
+
else
|
38
|
+
rubocop $RUBY_FILES
|
39
|
+
fi
|
40
|
+
|
41
|
+
RUBOCOP_EXIT_CODE=$?
|
42
|
+
|
43
|
+
if [ $RUBOCOP_EXIT_CODE -ne 0 ]; then
|
44
|
+
cat << EOF
|
45
|
+
{
|
46
|
+
"type": "advanced",
|
47
|
+
"decision": "deny",
|
48
|
+
"message": "RuboCop found style violations. Please fix them before committing.\n\nYou can run: bundle exec rubocop $RUBY_FILES --auto-correct\n\nOr fix them manually and try again."
|
49
|
+
}
|
50
|
+
EOF
|
51
|
+
exit 1
|
52
|
+
fi
|
53
|
+
|
54
|
+
# RuboCop passed, allow the commit
|
55
|
+
exit 0
|