aircana 2.0.0 → 3.0.0.rc2
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/.claude-plugin/plugin.json +7 -0
- data/.rspec_status +184 -187
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +38 -0
- data/CLAUDE.md +51 -20
- data/README.md +132 -63
- data/agents/apply_feedback.md +92 -0
- data/agents/executor.md +85 -0
- data/agents/jira.md +46 -0
- data/agents/planner.md +64 -0
- data/agents/reviewer.md +95 -0
- data/agents/sub-agent-coordinator.md +91 -0
- data/agents/test-agent/manifest.json +15 -0
- data/commands/air-apply-feedback.md +15 -0
- data/commands/air-ask-expert.md +42 -0
- data/commands/air-execute.md +13 -0
- data/commands/air-plan.md +33 -0
- data/commands/air-record.md +17 -0
- data/commands/air-review.md +12 -0
- data/commands/sample-command.md +1 -0
- data/hooks/hooks.json +31 -0
- data/lib/aircana/cli/app.rb +27 -30
- data/lib/aircana/cli/commands/agents.rb +41 -9
- data/lib/aircana/cli/commands/doctor_checks.rb +2 -3
- data/lib/aircana/cli/commands/generate.rb +0 -11
- data/lib/aircana/cli/commands/hooks.rb +4 -4
- data/lib/aircana/cli/commands/init.rb +266 -0
- data/lib/aircana/cli/commands/plugin.rb +157 -0
- data/lib/aircana/cli/help_formatter.rb +2 -3
- data/lib/aircana/configuration.rb +29 -3
- data/lib/aircana/contexts/manifest.rb +1 -8
- data/lib/aircana/generators/agents_generator.rb +3 -2
- data/lib/aircana/hooks_manifest.rb +189 -0
- data/lib/aircana/plugin_manifest.rb +146 -0
- data/lib/aircana/system_checker.rb +0 -11
- data/lib/aircana/templates/agents/base_agent.erb +2 -2
- data/lib/aircana/templates/hooks/session_start.erb +3 -118
- data/lib/aircana/templates/hooks/user_prompt_submit.erb +0 -6
- data/lib/aircana/version.rb +1 -1
- data/spec_target_1760205040_181/agents/test-agent/manifest.json +15 -0
- data/spec_target_1760205220_486/agents/test-agent/manifest.json +15 -0
- data/spec_target_1760205379_250/agents/test-agent/manifest.json +15 -0
- data/spec_target_1760205601_652/agents/test-agent/manifest.json +15 -0
- data/spec_target_1760205608_135/agents/test-agent/manifest.json +15 -0
- data/spec_target_1760205654_952/agents/test-agent/manifest.json +15 -0
- metadata +29 -7
- data/lib/aircana/cli/commands/install.rb +0 -179
- data/lib/aircana/cli/commands/project.rb +0 -156
- data/lib/aircana/generators/project_config_generator.rb +0 -54
- data/lib/aircana/symlink_manager.rb +0 -158
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aircana
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0.rc2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Weston Dransfield
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: httparty
|
@@ -102,6 +102,7 @@ executables:
|
|
102
102
|
extensions: []
|
103
103
|
extra_rdoc_files: []
|
104
104
|
files:
|
105
|
+
- ".claude-plugin/plugin.json"
|
105
106
|
- ".devcontainer/devcontainer.json"
|
106
107
|
- ".dockerignore"
|
107
108
|
- ".rspec_status"
|
@@ -113,8 +114,23 @@ files:
|
|
113
114
|
- README.md
|
114
115
|
- Rakefile
|
115
116
|
- SECURITY.md
|
117
|
+
- agents/apply_feedback.md
|
118
|
+
- agents/executor.md
|
119
|
+
- agents/jira.md
|
120
|
+
- agents/planner.md
|
121
|
+
- agents/reviewer.md
|
122
|
+
- agents/sub-agent-coordinator.md
|
123
|
+
- agents/test-agent/manifest.json
|
124
|
+
- commands/air-apply-feedback.md
|
125
|
+
- commands/air-ask-expert.md
|
126
|
+
- commands/air-execute.md
|
127
|
+
- commands/air-plan.md
|
128
|
+
- commands/air-record.md
|
129
|
+
- commands/air-review.md
|
130
|
+
- commands/sample-command.md
|
116
131
|
- compose.yml
|
117
132
|
- exe/aircana
|
133
|
+
- hooks/hooks.json
|
118
134
|
- lib/aircana.rb
|
119
135
|
- lib/aircana/cli.rb
|
120
136
|
- lib/aircana/cli/app.rb
|
@@ -125,8 +141,8 @@ files:
|
|
125
141
|
- lib/aircana/cli/commands/dump_context.rb
|
126
142
|
- lib/aircana/cli/commands/generate.rb
|
127
143
|
- lib/aircana/cli/commands/hooks.rb
|
128
|
-
- lib/aircana/cli/commands/
|
129
|
-
- lib/aircana/cli/commands/
|
144
|
+
- lib/aircana/cli/commands/init.rb
|
145
|
+
- lib/aircana/cli/commands/plugin.rb
|
130
146
|
- lib/aircana/cli/help_formatter.rb
|
131
147
|
- lib/aircana/cli/shell_command.rb
|
132
148
|
- lib/aircana/cli/subcommand.rb
|
@@ -149,14 +165,14 @@ files:
|
|
149
165
|
- lib/aircana/generators/helpers.rb
|
150
166
|
- lib/aircana/generators/hooks_generator.rb
|
151
167
|
- lib/aircana/generators/plan_command_generator.rb
|
152
|
-
- lib/aircana/generators/project_config_generator.rb
|
153
168
|
- lib/aircana/generators/record_command_generator.rb
|
154
169
|
- lib/aircana/generators/review_command_generator.rb
|
170
|
+
- lib/aircana/hooks_manifest.rb
|
155
171
|
- lib/aircana/human_logger.rb
|
156
172
|
- lib/aircana/initializers.rb
|
157
173
|
- lib/aircana/llm/claude_client.rb
|
174
|
+
- lib/aircana/plugin_manifest.rb
|
158
175
|
- lib/aircana/progress_tracker.rb
|
159
|
-
- lib/aircana/symlink_manager.rb
|
160
176
|
- lib/aircana/system_checker.rb
|
161
177
|
- lib/aircana/templates/agents/base_agent.erb
|
162
178
|
- lib/aircana/templates/agents/defaults/apply_feedback.erb
|
@@ -181,6 +197,12 @@ files:
|
|
181
197
|
- lib/aircana/templates/hooks/user_prompt_submit.erb
|
182
198
|
- lib/aircana/version.rb
|
183
199
|
- sig/aircana.rbs
|
200
|
+
- spec_target_1760205040_181/agents/test-agent/manifest.json
|
201
|
+
- spec_target_1760205220_486/agents/test-agent/manifest.json
|
202
|
+
- spec_target_1760205379_250/agents/test-agent/manifest.json
|
203
|
+
- spec_target_1760205601_652/agents/test-agent/manifest.json
|
204
|
+
- spec_target_1760205608_135/agents/test-agent/manifest.json
|
205
|
+
- spec_target_1760205654_952/agents/test-agent/manifest.json
|
184
206
|
homepage: https://github.com/westonkd/aircana
|
185
207
|
licenses:
|
186
208
|
- MIT
|
@@ -204,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
226
|
- !ruby/object:Gem::Version
|
205
227
|
version: '0'
|
206
228
|
requirements: []
|
207
|
-
rubygems_version: 3.6.
|
229
|
+
rubygems_version: 3.6.9
|
208
230
|
specification_version: 4
|
209
231
|
summary: Humble workflow and context utilities for engineering with agents
|
210
232
|
test_files: []
|
@@ -1,179 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
require_relative "generate"
|
5
|
-
require_relative "../../generators/project_config_generator"
|
6
|
-
|
7
|
-
module Aircana
|
8
|
-
module CLI
|
9
|
-
module Install
|
10
|
-
class << self
|
11
|
-
def run
|
12
|
-
generate_files
|
13
|
-
ensure_project_config_exists
|
14
|
-
install_commands_to_claude
|
15
|
-
install_hooks_to_claude
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def generate_files
|
21
|
-
Aircana.human_logger.info("Generating files before installation...")
|
22
|
-
Generate.run
|
23
|
-
end
|
24
|
-
|
25
|
-
def ensure_project_config_exists
|
26
|
-
project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
|
27
|
-
return if File.exist?(project_json_path)
|
28
|
-
|
29
|
-
Aircana.human_logger.info("Creating project.json for multi-root support...")
|
30
|
-
Aircana::Generators::ProjectConfigGenerator.new.generate
|
31
|
-
end
|
32
|
-
|
33
|
-
def install_commands_to_claude
|
34
|
-
claude_commands_dir = File.join(Aircana.configuration.claude_code_project_config_path, "commands")
|
35
|
-
Aircana.create_dir_if_needed(claude_commands_dir)
|
36
|
-
|
37
|
-
copy_command_files(claude_commands_dir)
|
38
|
-
install_agents_to_claude
|
39
|
-
end
|
40
|
-
|
41
|
-
def copy_command_files(destination_dir)
|
42
|
-
Dir.glob("#{Aircana.configuration.output_dir}/commands/*").each do |file|
|
43
|
-
Aircana.human_logger.success("Installing #{file} to #{destination_dir}")
|
44
|
-
FileUtils.cp(file, destination_dir)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def install_agents_to_claude
|
49
|
-
claude_agents_dir = File.join(Aircana.configuration.claude_code_project_config_path, "agents")
|
50
|
-
Aircana.create_dir_if_needed(claude_agents_dir)
|
51
|
-
|
52
|
-
copy_agent_files(claude_agents_dir)
|
53
|
-
end
|
54
|
-
|
55
|
-
def copy_agent_files(destination_dir)
|
56
|
-
agent_files_pattern = File.join(Aircana.configuration.output_dir, "agents", "*.md")
|
57
|
-
Dir.glob(agent_files_pattern).each do |file|
|
58
|
-
agent_name = File.basename(file, ".md")
|
59
|
-
next unless default_agent?(agent_name)
|
60
|
-
|
61
|
-
destination_file = File.join(destination_dir, File.basename(file))
|
62
|
-
# Skip copying if source and destination are the same
|
63
|
-
next if File.expand_path(file) == File.expand_path(destination_file)
|
64
|
-
|
65
|
-
Aircana.human_logger.success("Installing default agent #{file} to #{destination_dir}")
|
66
|
-
FileUtils.cp(file, destination_dir)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def default_agent?(agent_name)
|
71
|
-
require_relative "../../generators/agents_generator"
|
72
|
-
Aircana::Generators::AgentsGenerator.available_default_agents.include?(agent_name)
|
73
|
-
end
|
74
|
-
|
75
|
-
def install_hooks_to_claude
|
76
|
-
return unless Dir.exist?(Aircana.configuration.hooks_dir)
|
77
|
-
|
78
|
-
settings_file = File.join(Aircana.configuration.claude_code_project_config_path, "settings.local.json")
|
79
|
-
install_hooks_to_settings(settings_file)
|
80
|
-
end
|
81
|
-
|
82
|
-
def install_hooks_to_settings(settings_file)
|
83
|
-
settings = load_settings(settings_file)
|
84
|
-
hook_configs = build_hook_configs
|
85
|
-
|
86
|
-
return if hook_configs.empty?
|
87
|
-
|
88
|
-
settings["hooks"] = hook_configs
|
89
|
-
save_settings(settings_file, settings)
|
90
|
-
|
91
|
-
Aircana.human_logger.success("Installed hooks to #{settings_file}")
|
92
|
-
end
|
93
|
-
|
94
|
-
def load_settings(settings_file)
|
95
|
-
if File.exist?(settings_file)
|
96
|
-
JSON.parse(File.read(settings_file))
|
97
|
-
else
|
98
|
-
Aircana.create_dir_if_needed(File.dirname(settings_file))
|
99
|
-
{}
|
100
|
-
end
|
101
|
-
rescue JSON::ParserError
|
102
|
-
Aircana.human_logger.warn("Invalid JSON in #{settings_file}, creating new settings")
|
103
|
-
{}
|
104
|
-
end
|
105
|
-
|
106
|
-
def save_settings(settings_file, settings)
|
107
|
-
File.write(settings_file, JSON.pretty_generate(settings))
|
108
|
-
end
|
109
|
-
|
110
|
-
def build_hook_configs
|
111
|
-
hooks = {}
|
112
|
-
|
113
|
-
# Map hook files to Claude Code hook events and their properties
|
114
|
-
hook_mappings = {
|
115
|
-
"pre_tool_use" => { event: "PreToolUse", matcher: nil },
|
116
|
-
"post_tool_use" => { event: "PostToolUse", matcher: nil },
|
117
|
-
"user_prompt_submit" => { event: "UserPromptSubmit", matcher: nil },
|
118
|
-
"session_start" => { event: "SessionStart", matcher: nil },
|
119
|
-
"notification_sqs" => { event: "Notification", matcher: nil },
|
120
|
-
"rubocop_pre_commit" => { event: "PreToolUse", matcher: "Bash" },
|
121
|
-
"rspec_test" => { event: "PostToolUse", matcher: "Bash" },
|
122
|
-
"bundle_install" => { event: "PostToolUse", matcher: "Bash" }
|
123
|
-
}
|
124
|
-
|
125
|
-
Dir.glob("#{Aircana.configuration.hooks_dir}/*.sh").each do |hook_file|
|
126
|
-
hook_name = File.basename(hook_file, ".sh")
|
127
|
-
|
128
|
-
# Determine mapping for this hook
|
129
|
-
mapping = if hook_mappings.key?(hook_name)
|
130
|
-
hook_mappings[hook_name]
|
131
|
-
else
|
132
|
-
# For custom hooks, try to infer the event type from the filename
|
133
|
-
infer_hook_mapping(hook_name)
|
134
|
-
end
|
135
|
-
|
136
|
-
next unless mapping
|
137
|
-
|
138
|
-
event_key = mapping[:event]
|
139
|
-
|
140
|
-
# Create relative path from project root
|
141
|
-
relative_path = File.join(".aircana", "hooks", "#{hook_name}.sh")
|
142
|
-
|
143
|
-
hook_entry = {
|
144
|
-
"hooks" => [
|
145
|
-
{
|
146
|
-
"type" => "command",
|
147
|
-
"command" => relative_path
|
148
|
-
}
|
149
|
-
]
|
150
|
-
}
|
151
|
-
|
152
|
-
# Add matcher if specified
|
153
|
-
hook_entry["matcher"] = mapping[:matcher] if mapping[:matcher]
|
154
|
-
|
155
|
-
hooks[event_key] ||= []
|
156
|
-
hooks[event_key] << hook_entry
|
157
|
-
end
|
158
|
-
|
159
|
-
hooks
|
160
|
-
end
|
161
|
-
|
162
|
-
def infer_hook_mapping(hook_name)
|
163
|
-
# Try to infer the event type from common patterns in the hook name
|
164
|
-
case hook_name
|
165
|
-
when /pre_tool_use|pre_tool|before_tool/i
|
166
|
-
{ event: "PreToolUse", matcher: nil }
|
167
|
-
when /user_prompt|prompt_submit|before_prompt/i
|
168
|
-
{ event: "UserPromptSubmit", matcher: nil }
|
169
|
-
when /session_start|session_init|startup/i
|
170
|
-
{ event: "SessionStart", matcher: nil }
|
171
|
-
else
|
172
|
-
# Default to PostToolUse for unknown custom hooks and post_tool patterns
|
173
|
-
{ event: "PostToolUse", matcher: nil }
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
@@ -1,156 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
require "tty-prompt"
|
5
|
-
require_relative "../../symlink_manager"
|
6
|
-
require_relative "../../generators/project_config_generator"
|
7
|
-
|
8
|
-
module Aircana
|
9
|
-
module CLI
|
10
|
-
module Project
|
11
|
-
class << self
|
12
|
-
def init
|
13
|
-
generator = Aircana::Generators::ProjectConfigGenerator.new
|
14
|
-
config_path = generator.generate
|
15
|
-
|
16
|
-
Aircana.human_logger.success "Initialized project.json at #{config_path}"
|
17
|
-
Aircana.human_logger.info "Add folders using: aircana project add <path>"
|
18
|
-
end
|
19
|
-
|
20
|
-
def add(folder_path)
|
21
|
-
project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
|
22
|
-
|
23
|
-
# Create project.json if it doesn't exist
|
24
|
-
init unless File.exist?(project_json_path)
|
25
|
-
|
26
|
-
# Validate folder exists
|
27
|
-
full_path = File.join(Aircana.configuration.project_dir, folder_path)
|
28
|
-
unless Dir.exist?(full_path)
|
29
|
-
Aircana.human_logger.error "Folder not found: #{folder_path}"
|
30
|
-
return
|
31
|
-
end
|
32
|
-
|
33
|
-
# Load existing config
|
34
|
-
config = JSON.parse(File.read(project_json_path))
|
35
|
-
config["folders"] ||= []
|
36
|
-
|
37
|
-
# Check if folder already exists
|
38
|
-
if config["folders"].any? { |f| f["path"] == folder_path }
|
39
|
-
Aircana.human_logger.warn "Folder already configured: #{folder_path}"
|
40
|
-
return
|
41
|
-
end
|
42
|
-
|
43
|
-
# Add the folder
|
44
|
-
config["folders"] << { "path" => folder_path }
|
45
|
-
|
46
|
-
# Save updated config
|
47
|
-
File.write(project_json_path, JSON.pretty_generate(config))
|
48
|
-
|
49
|
-
Aircana.human_logger.success "Added folder: #{folder_path}"
|
50
|
-
|
51
|
-
# Check what agents/knowledge would be available
|
52
|
-
check_folder_contents(folder_path)
|
53
|
-
|
54
|
-
# Offer to sync
|
55
|
-
prompt = TTY::Prompt.new
|
56
|
-
sync if prompt.yes?("Would you like to sync symlinks now?")
|
57
|
-
end
|
58
|
-
|
59
|
-
def remove(folder_path)
|
60
|
-
project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
|
61
|
-
|
62
|
-
unless File.exist?(project_json_path)
|
63
|
-
Aircana.human_logger.error "No project.json found. Run 'aircana project init' first."
|
64
|
-
return
|
65
|
-
end
|
66
|
-
|
67
|
-
# Load existing config
|
68
|
-
config = JSON.parse(File.read(project_json_path))
|
69
|
-
config["folders"] ||= []
|
70
|
-
|
71
|
-
# Remove the folder
|
72
|
-
original_count = config["folders"].size
|
73
|
-
config["folders"].reject! { |f| f["path"] == folder_path }
|
74
|
-
|
75
|
-
if config["folders"].size == original_count
|
76
|
-
Aircana.human_logger.warn "Folder not found in configuration: #{folder_path}"
|
77
|
-
return
|
78
|
-
end
|
79
|
-
|
80
|
-
# Save updated config
|
81
|
-
File.write(project_json_path, JSON.pretty_generate(config))
|
82
|
-
|
83
|
-
Aircana.human_logger.success "Removed folder: #{folder_path}"
|
84
|
-
|
85
|
-
# Clean up symlinks
|
86
|
-
Aircana::SymlinkManager.cleanup_broken_symlinks
|
87
|
-
end
|
88
|
-
|
89
|
-
def list
|
90
|
-
project_json_path = File.join(Aircana.configuration.project_dir, ".aircana", "project.json")
|
91
|
-
|
92
|
-
unless File.exist?(project_json_path)
|
93
|
-
Aircana.human_logger.info "No project.json found. Run 'aircana project init' to create one."
|
94
|
-
return
|
95
|
-
end
|
96
|
-
|
97
|
-
config = JSON.parse(File.read(project_json_path))
|
98
|
-
folders = config["folders"] || []
|
99
|
-
|
100
|
-
if folders.empty?
|
101
|
-
Aircana.human_logger.info "No folders configured."
|
102
|
-
Aircana.human_logger.info "Add folders using: aircana project add <path>"
|
103
|
-
return
|
104
|
-
end
|
105
|
-
|
106
|
-
Aircana.human_logger.info "Configured folders:"
|
107
|
-
folders.each do |folder|
|
108
|
-
folder_path = folder["path"]
|
109
|
-
status = Dir.exist?(File.join(Aircana.configuration.project_dir, folder_path)) ? "✓" : "✗"
|
110
|
-
Aircana.human_logger.info " #{status} #{folder_path}"
|
111
|
-
|
112
|
-
# Show available agents if folder exists
|
113
|
-
check_folder_contents(folder_path, indent: " ") if status == "✓"
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def sync
|
118
|
-
Aircana.human_logger.info "Syncing multi-root project symlinks..."
|
119
|
-
|
120
|
-
stats = Aircana::SymlinkManager.sync_multi_root_agents
|
121
|
-
|
122
|
-
if stats[:agents].zero? && stats[:knowledge].zero?
|
123
|
-
Aircana.human_logger.info "No agents or knowledge bases to link."
|
124
|
-
else
|
125
|
-
Aircana.human_logger.success "Sync complete: #{stats[:agents]} agents, #{stats[:knowledge]} knowledge bases"
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
private
|
130
|
-
|
131
|
-
def check_folder_contents(folder_path, indent: " ")
|
132
|
-
agents_dir = File.join(folder_path, ".claude", "agents")
|
133
|
-
knowledge_dir = File.join(folder_path, ".aircana", "agents")
|
134
|
-
|
135
|
-
agents = []
|
136
|
-
knowledge = []
|
137
|
-
|
138
|
-
agents = Dir.glob("#{agents_dir}/*.md").map { |f| File.basename(f, ".md") } if Dir.exist?(agents_dir)
|
139
|
-
|
140
|
-
if Dir.exist?(knowledge_dir)
|
141
|
-
knowledge = Dir.glob("#{knowledge_dir}/*").select { |d| File.directory?(d) }
|
142
|
-
.map { |d| File.basename(d) }
|
143
|
-
end
|
144
|
-
|
145
|
-
Aircana.human_logger.info "#{indent}Agents: #{agents.join(", ")}" if agents.any?
|
146
|
-
|
147
|
-
Aircana.human_logger.info "#{indent}Knowledge: #{knowledge.join(", ")}" if knowledge.any?
|
148
|
-
|
149
|
-
return unless agents.empty? && knowledge.empty?
|
150
|
-
|
151
|
-
Aircana.human_logger.info "#{indent}No agents or knowledge found"
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
@@ -1,54 +0,0 @@
|
|
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
|
@@ -1,158 +0,0 @@
|
|
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
|