aircana 2.0.0.rc5 → 3.0.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 +185 -185
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +2 -2
- data/README.md +4 -12
- data/lib/aircana/cli/app.rb +0 -31
- data/lib/aircana/cli/commands/agents.rb +1 -1
- data/lib/aircana/cli/commands/generate.rb +0 -11
- data/lib/aircana/cli/commands/install.rb +0 -10
- data/lib/aircana/cli/help_formatter.rb +1 -2
- data/lib/aircana/configuration.rb +1 -1
- data/lib/aircana/contexts/manifest.rb +1 -8
- data/lib/aircana/generators/agents_generator.rb +1 -1
- data/lib/aircana/system_checker.rb +0 -10
- data/lib/aircana/templates/agents/base_agent.erb +2 -2
- data/lib/aircana/templates/hooks/session_start.erb +3 -118
- data/lib/aircana/version.rb +1 -1
- metadata +3 -6
- 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
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require "json"
|
4
4
|
require_relative "generate"
|
5
|
-
require_relative "../../generators/project_config_generator"
|
6
5
|
|
7
6
|
module Aircana
|
8
7
|
module CLI
|
@@ -10,7 +9,6 @@ module Aircana
|
|
10
9
|
class << self
|
11
10
|
def run
|
12
11
|
generate_files
|
13
|
-
ensure_project_config_exists
|
14
12
|
install_commands_to_claude
|
15
13
|
install_hooks_to_claude
|
16
14
|
end
|
@@ -22,14 +20,6 @@ module Aircana
|
|
22
20
|
Generate.run
|
23
21
|
end
|
24
22
|
|
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
23
|
def install_commands_to_claude
|
34
24
|
claude_commands_dir = File.join(Aircana.configuration.claude_code_project_config_path, "commands")
|
35
25
|
Aircana.create_dir_if_needed(claude_commands_dir)
|
@@ -26,7 +26,6 @@ module Aircana
|
|
26
26
|
"File Management" => %w[files],
|
27
27
|
"Agent Management" => %w[agents],
|
28
28
|
"Hook Management" => %w[hooks],
|
29
|
-
"Project Management" => %w[project],
|
30
29
|
"System" => %w[generate install doctor dump-context]
|
31
30
|
}
|
32
31
|
end
|
@@ -53,7 +52,7 @@ module Aircana
|
|
53
52
|
end
|
54
53
|
|
55
54
|
def subcommand?(cmd_name)
|
56
|
-
%w[files agents hooks
|
55
|
+
%w[files agents hooks].include?(cmd_name)
|
57
56
|
end
|
58
57
|
|
59
58
|
def print_subcommand_group(subcommand_name, cmd)
|
@@ -19,7 +19,7 @@ module Aircana
|
|
19
19
|
@global_dir = File.join(Dir.home, ".aircana")
|
20
20
|
@project_dir = Dir.pwd
|
21
21
|
@output_dir = File.join(@global_dir, "aircana.out")
|
22
|
-
@agent_knowledge_dir = File.join(@project_dir, ".
|
22
|
+
@agent_knowledge_dir = File.join(@project_dir, ".claude", "agents")
|
23
23
|
@hooks_dir = File.join(@project_dir, ".aircana", "hooks")
|
24
24
|
end
|
25
25
|
|
@@ -73,14 +73,7 @@ module Aircana
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def resolve_agent_path(agent)
|
76
|
-
|
77
|
-
|
78
|
-
# If this is a symlink (multi-root scenario), resolve to original
|
79
|
-
if File.symlink?(base_path)
|
80
|
-
File.readlink(base_path)
|
81
|
-
else
|
82
|
-
base_path
|
83
|
-
end
|
76
|
+
File.join(Aircana.configuration.agent_knowledge_dir, agent)
|
84
77
|
end
|
85
78
|
|
86
79
|
def build_manifest_data(agent, sources)
|
@@ -23,16 +23,6 @@ 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
|
-
}
|
36
26
|
}
|
37
27
|
}.freeze
|
38
28
|
|
@@ -5,7 +5,7 @@ model: <%= model %>
|
|
5
5
|
color: <%= color %>
|
6
6
|
---
|
7
7
|
|
8
|
-
<%= helpers.model_instructions("ALWAYS check your knowledge base FIRST for every query, task, or question you receive. Use `ls .
|
8
|
+
<%= helpers.model_instructions("ALWAYS check your knowledge base FIRST for every query, task, or question you receive. Use `ls .claude/agents/#{agent_name}/knowledge/*.md` to list files from your knowledge base.
|
9
9
|
|
10
10
|
MANDATORY WORKFLOW:
|
11
11
|
1. BEFORE responding to ANY request - search and read relevant files in your knowledge base
|
@@ -18,7 +18,7 @@ Your knowledge base contains domain-specific information that takes priority ove
|
|
18
18
|
|
19
19
|
## Knowledge Base Integration
|
20
20
|
|
21
|
-
Your specialized knowledge is in the current project directory. Use `ls .
|
21
|
+
Your specialized knowledge is in the current project directory. Use `ls .claude/agents/<%= agent_name %>/knowledge/*.md` to list available files, then read them with the Read tool.
|
22
22
|
|
23
23
|
This knowledge base contains:
|
24
24
|
- Domain-specific documentation from Confluence
|
@@ -1,127 +1,12 @@
|
|
1
1
|
#!/bin/bash
|
2
|
-
#
|
2
|
+
# Session start hook for Aircana
|
3
3
|
# This hook runs when a new Claude Code session starts
|
4
4
|
|
5
|
-
PROJECT_JSON=".aircana/project.json"
|
6
|
-
CLAUDE_AGENTS_DIR=".claude/agents"
|
7
|
-
AIRCANA_AGENTS_DIR=".aircana/agents"
|
8
|
-
|
9
5
|
# Create log directory if it doesn't exist
|
10
6
|
mkdir -p ~/.aircana
|
11
7
|
|
12
8
|
# Log session start
|
13
9
|
echo "$(date): New Claude Code session started in $(pwd)" >> ~/.aircana/hooks.log
|
14
10
|
|
15
|
-
#
|
16
|
-
|
17
|
-
echo "$(date): Warning - jq not found. Multi-root support disabled." >> ~/.aircana/hooks.log
|
18
|
-
echo "{}"
|
19
|
-
exit 0
|
20
|
-
fi
|
21
|
-
|
22
|
-
# Check if project.json exists
|
23
|
-
if [ ! -f "$PROJECT_JSON" ]; then
|
24
|
-
echo "$(date): No project.json found, skipping multi-root setup" >> ~/.aircana/hooks.log
|
25
|
-
echo "{}"
|
26
|
-
exit 0
|
27
|
-
fi
|
28
|
-
|
29
|
-
echo "$(date): Processing multi-root configuration from $PROJECT_JSON" >> ~/.aircana/hooks.log
|
30
|
-
|
31
|
-
# Ensure directories exist
|
32
|
-
mkdir -p "$CLAUDE_AGENTS_DIR" 2>/dev/null
|
33
|
-
mkdir -p "$AIRCANA_AGENTS_DIR" 2>/dev/null
|
34
|
-
|
35
|
-
# Clean up existing symlinks (only remove symlinks, not real files)
|
36
|
-
find "$CLAUDE_AGENTS_DIR" -type l -delete 2>/dev/null
|
37
|
-
find "$AIRCANA_AGENTS_DIR" -type l -delete 2>/dev/null
|
38
|
-
|
39
|
-
# Parse folders from project.json
|
40
|
-
FOLDERS=$(jq -r '.folders[]?.path // empty' "$PROJECT_JSON" 2>/dev/null)
|
41
|
-
|
42
|
-
if [ -z "$FOLDERS" ]; then
|
43
|
-
echo "$(date): No folders configured in project.json" >> ~/.aircana/hooks.log
|
44
|
-
echo "{}"
|
45
|
-
exit 0
|
46
|
-
fi
|
47
|
-
|
48
|
-
# Track what we've linked for reporting
|
49
|
-
LINKED_AGENTS=0
|
50
|
-
LINKED_KNOWLEDGE=0
|
51
|
-
|
52
|
-
# Create symlinks for each configured folder
|
53
|
-
for folder in $FOLDERS; do
|
54
|
-
# Validate folder exists
|
55
|
-
if [ ! -d "$folder" ]; then
|
56
|
-
echo "$(date): Warning - folder '$folder' not found, skipping" >> ~/.aircana/hooks.log
|
57
|
-
continue
|
58
|
-
fi
|
59
|
-
|
60
|
-
echo "$(date): Processing folder: $folder" >> ~/.aircana/hooks.log
|
61
|
-
|
62
|
-
# Get folder name for prefix (replace slashes with underscores for nested paths)
|
63
|
-
PREFIX=$(echo "$folder" | tr '/' '_')
|
64
|
-
|
65
|
-
# Link agents from sub-folder .claude/agents
|
66
|
-
if [ -d "$folder/.claude/agents" ]; then
|
67
|
-
for agent_file in "$folder/.claude/agents"/*.md; do
|
68
|
-
if [ ! -f "$agent_file" ]; then
|
69
|
-
continue
|
70
|
-
fi
|
71
|
-
|
72
|
-
AGENT_NAME=$(basename "$agent_file" .md)
|
73
|
-
LINK_NAME="${PREFIX}_${AGENT_NAME}.md"
|
74
|
-
TARGET_PATH="$CLAUDE_AGENTS_DIR/$LINK_NAME"
|
75
|
-
|
76
|
-
# Create relative path from .claude/agents to the agent file
|
77
|
-
RELATIVE_PATH=$(realpath --relative-to="$CLAUDE_AGENTS_DIR" "$agent_file" 2>/dev/null)
|
78
|
-
|
79
|
-
if [ -n "$RELATIVE_PATH" ]; then
|
80
|
-
ln -sf "$RELATIVE_PATH" "$TARGET_PATH"
|
81
|
-
echo "$(date): Linked agent: $LINK_NAME -> $RELATIVE_PATH" >> ~/.aircana/hooks.log
|
82
|
-
((LINKED_AGENTS++))
|
83
|
-
fi
|
84
|
-
done
|
85
|
-
fi
|
86
|
-
|
87
|
-
# Link knowledge from sub-folder .aircana/agents
|
88
|
-
if [ -d "$folder/.aircana/agents" ]; then
|
89
|
-
for agent_dir in "$folder/.aircana/agents"/*; do
|
90
|
-
if [ ! -d "$agent_dir" ]; then
|
91
|
-
continue
|
92
|
-
fi
|
93
|
-
|
94
|
-
AGENT_NAME=$(basename "$agent_dir")
|
95
|
-
LINK_NAME="${PREFIX}_${AGENT_NAME}"
|
96
|
-
TARGET_PATH="$AIRCANA_AGENTS_DIR/$LINK_NAME"
|
97
|
-
|
98
|
-
# Create relative path from .aircana/agents to the knowledge directory
|
99
|
-
RELATIVE_PATH=$(realpath --relative-to="$AIRCANA_AGENTS_DIR" "$agent_dir" 2>/dev/null)
|
100
|
-
|
101
|
-
if [ -n "$RELATIVE_PATH" ]; then
|
102
|
-
ln -sf "$RELATIVE_PATH" "$TARGET_PATH"
|
103
|
-
echo "$(date): Linked knowledge: $LINK_NAME -> $RELATIVE_PATH" >> ~/.aircana/hooks.log
|
104
|
-
((LINKED_KNOWLEDGE++))
|
105
|
-
fi
|
106
|
-
done
|
107
|
-
fi
|
108
|
-
done
|
109
|
-
|
110
|
-
# Report results
|
111
|
-
echo "$(date): Multi-root setup complete - linked $LINKED_AGENTS agents and $LINKED_KNOWLEDGE knowledge bases" >> ~/.aircana/hooks.log
|
112
|
-
|
113
|
-
# Return success with optional context
|
114
|
-
if [ $LINKED_AGENTS -gt 0 ] || [ $LINKED_KNOWLEDGE -gt 0 ]; then
|
115
|
-
CONTEXT="Multi-root: Linked $LINKED_AGENTS agents and $LINKED_KNOWLEDGE knowledge bases from configured folders."
|
116
|
-
ESCAPED_CONTEXT=$(echo -n "$CONTEXT" | sed 's/"/\\"/g')
|
117
|
-
cat << EOF
|
118
|
-
{
|
119
|
-
"hookSpecificOutput": {
|
120
|
-
"hookEventName": "SessionStart",
|
121
|
-
"additionalContext": "$ESCAPED_CONTEXT"
|
122
|
-
}
|
123
|
-
}
|
124
|
-
EOF
|
125
|
-
else
|
126
|
-
echo "{}"
|
127
|
-
fi
|
11
|
+
# Return success
|
12
|
+
echo "{}"
|
data/lib/aircana/version.rb
CHANGED
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.rc1
|
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
|
@@ -126,7 +126,6 @@ files:
|
|
126
126
|
- lib/aircana/cli/commands/generate.rb
|
127
127
|
- lib/aircana/cli/commands/hooks.rb
|
128
128
|
- lib/aircana/cli/commands/install.rb
|
129
|
-
- lib/aircana/cli/commands/project.rb
|
130
129
|
- lib/aircana/cli/help_formatter.rb
|
131
130
|
- lib/aircana/cli/shell_command.rb
|
132
131
|
- lib/aircana/cli/subcommand.rb
|
@@ -149,14 +148,12 @@ files:
|
|
149
148
|
- lib/aircana/generators/helpers.rb
|
150
149
|
- lib/aircana/generators/hooks_generator.rb
|
151
150
|
- lib/aircana/generators/plan_command_generator.rb
|
152
|
-
- lib/aircana/generators/project_config_generator.rb
|
153
151
|
- lib/aircana/generators/record_command_generator.rb
|
154
152
|
- lib/aircana/generators/review_command_generator.rb
|
155
153
|
- lib/aircana/human_logger.rb
|
156
154
|
- lib/aircana/initializers.rb
|
157
155
|
- lib/aircana/llm/claude_client.rb
|
158
156
|
- lib/aircana/progress_tracker.rb
|
159
|
-
- lib/aircana/symlink_manager.rb
|
160
157
|
- lib/aircana/system_checker.rb
|
161
158
|
- lib/aircana/templates/agents/base_agent.erb
|
162
159
|
- lib/aircana/templates/agents/defaults/apply_feedback.erb
|
@@ -204,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
201
|
- !ruby/object:Gem::Version
|
205
202
|
version: '0'
|
206
203
|
requirements: []
|
207
|
-
rubygems_version: 3.6.
|
204
|
+
rubygems_version: 3.6.9
|
208
205
|
specification_version: 4
|
209
206
|
summary: Humble workflow and context utilities for engineering with agents
|
210
207
|
test_files: []
|
@@ -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
|