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
@@ -3,7 +3,7 @@
|
|
3
3
|
require "json"
|
4
4
|
require "tty-prompt"
|
5
5
|
require_relative "../../generators/hooks_generator"
|
6
|
-
require_relative "
|
6
|
+
require_relative "init"
|
7
7
|
|
8
8
|
module Aircana
|
9
9
|
module CLI
|
@@ -40,7 +40,7 @@ module Aircana
|
|
40
40
|
Aircana::Generators::HooksGenerator.create_default_hook(hook_name)
|
41
41
|
|
42
42
|
# Install hooks to Claude settings
|
43
|
-
|
43
|
+
Init.run
|
44
44
|
|
45
45
|
Aircana.human_logger.success "Hook '#{hook_name}' has been enabled."
|
46
46
|
end
|
@@ -56,7 +56,7 @@ module Aircana
|
|
56
56
|
File.delete(hook_file)
|
57
57
|
|
58
58
|
# Reinstall remaining hooks to update Claude settings
|
59
|
-
|
59
|
+
Init.run
|
60
60
|
|
61
61
|
Aircana.human_logger.success "Hook '#{hook_name}' has been disabled."
|
62
62
|
end
|
@@ -152,7 +152,7 @@ module Aircana
|
|
152
152
|
Aircana.human_logger.info "You may need to customize the hook script for your specific needs."
|
153
153
|
|
154
154
|
# Install hooks to Claude settings
|
155
|
-
|
155
|
+
Init.run
|
156
156
|
Aircana.human_logger.success "Hook installed to Claude settings"
|
157
157
|
|
158
158
|
# Optionally offer to open in editor
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "tty-prompt"
|
5
|
+
require_relative "generate"
|
6
|
+
require_relative "../../plugin_manifest"
|
7
|
+
require_relative "../../hooks_manifest"
|
8
|
+
|
9
|
+
module Aircana
|
10
|
+
module CLI
|
11
|
+
module Init # rubocop:disable Metrics/ModuleLength
|
12
|
+
class << self # rubocop:disable Metrics/ClassLength
|
13
|
+
def run(directory: nil, plugin_name: nil)
|
14
|
+
target_dir = resolve_target_directory(directory)
|
15
|
+
|
16
|
+
with_directory_config(target_dir) do
|
17
|
+
# Collect plugin metadata
|
18
|
+
metadata = collect_plugin_metadata(target_dir, plugin_name)
|
19
|
+
|
20
|
+
# Create plugin structure
|
21
|
+
create_plugin_structure(target_dir)
|
22
|
+
|
23
|
+
# Create plugin manifest
|
24
|
+
create_plugin_manifest(target_dir, metadata)
|
25
|
+
|
26
|
+
# Generate and install commands
|
27
|
+
generate_files
|
28
|
+
install_commands
|
29
|
+
|
30
|
+
# Install hooks
|
31
|
+
install_hooks
|
32
|
+
|
33
|
+
# Success message
|
34
|
+
display_success_message(metadata)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def resolve_target_directory(directory)
|
41
|
+
return Dir.pwd if directory.nil? || directory.empty?
|
42
|
+
|
43
|
+
dir = File.expand_path(directory)
|
44
|
+
FileUtils.mkdir_p(dir)
|
45
|
+
|
46
|
+
dir
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_directory_config(target_dir)
|
50
|
+
original_project_dir = Aircana.configuration.project_dir
|
51
|
+
original_plugin_root = Aircana.configuration.plugin_root
|
52
|
+
|
53
|
+
begin
|
54
|
+
# Temporarily override configuration to use target directory
|
55
|
+
Aircana.configuration.project_dir = target_dir
|
56
|
+
Aircana.configuration.plugin_root = target_dir
|
57
|
+
Aircana.configuration.instance_variable_set(:@plugin_manifest_dir,
|
58
|
+
File.join(target_dir, ".claude-plugin"))
|
59
|
+
Aircana.configuration.instance_variable_set(:@commands_dir, File.join(target_dir, "commands"))
|
60
|
+
Aircana.configuration.instance_variable_set(:@agents_dir, File.join(target_dir, "agents"))
|
61
|
+
Aircana.configuration.instance_variable_set(:@hooks_dir, File.join(target_dir, "hooks"))
|
62
|
+
Aircana.configuration.instance_variable_set(:@agent_knowledge_dir, File.join(target_dir, "agents"))
|
63
|
+
|
64
|
+
yield
|
65
|
+
ensure
|
66
|
+
# Restore original configuration
|
67
|
+
Aircana.configuration.project_dir = original_project_dir
|
68
|
+
Aircana.configuration.plugin_root = original_plugin_root
|
69
|
+
Aircana.configuration.instance_variable_set(:@plugin_manifest_dir,
|
70
|
+
File.join(original_plugin_root, ".claude-plugin"))
|
71
|
+
Aircana.configuration.instance_variable_set(:@commands_dir,
|
72
|
+
File.join(original_plugin_root, "commands"))
|
73
|
+
Aircana.configuration.instance_variable_set(:@agents_dir, File.join(original_plugin_root, "agents"))
|
74
|
+
Aircana.configuration.instance_variable_set(:@hooks_dir, File.join(original_plugin_root, "hooks"))
|
75
|
+
Aircana.configuration.instance_variable_set(:@agent_knowledge_dir,
|
76
|
+
File.join(original_plugin_root, "agents"))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def collect_plugin_metadata(target_dir, plugin_name)
|
81
|
+
prompt = TTY::Prompt.new
|
82
|
+
|
83
|
+
default_name = plugin_name || PluginManifest.default_plugin_name(target_dir)
|
84
|
+
|
85
|
+
{
|
86
|
+
name: prompt.ask("Plugin name:", default: default_name),
|
87
|
+
version: prompt.ask("Initial version:", default: "0.1.0"),
|
88
|
+
description: prompt.ask("Plugin description:", default: "A Claude Code plugin created with Aircana"),
|
89
|
+
author: prompt.ask("Author name:"),
|
90
|
+
license: prompt.ask("License:", default: "MIT")
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_plugin_structure(target_dir)
|
95
|
+
# Create plugin directories
|
96
|
+
[".claude-plugin", "commands", "agents", "hooks"].each do |dir|
|
97
|
+
dir_path = File.join(target_dir, dir)
|
98
|
+
Aircana.create_dir_if_needed(dir_path)
|
99
|
+
Aircana.human_logger.info("Created directory: #{dir}/")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def create_plugin_manifest(target_dir, metadata)
|
104
|
+
manifest = PluginManifest.new(target_dir)
|
105
|
+
manifest.create(metadata)
|
106
|
+
Aircana.human_logger.success("Created plugin manifest at .claude-plugin/plugin.json")
|
107
|
+
end
|
108
|
+
|
109
|
+
def generate_files
|
110
|
+
Aircana.human_logger.info("Generating files...")
|
111
|
+
Generate.run
|
112
|
+
end
|
113
|
+
|
114
|
+
def install_commands
|
115
|
+
commands_dir = Aircana.configuration.commands_dir
|
116
|
+
Aircana.create_dir_if_needed(commands_dir)
|
117
|
+
|
118
|
+
copy_command_files(commands_dir)
|
119
|
+
install_default_agents
|
120
|
+
end
|
121
|
+
|
122
|
+
def copy_command_files(destination_dir)
|
123
|
+
Dir.glob("#{Aircana.configuration.output_dir}/commands/*").each do |file|
|
124
|
+
Aircana.human_logger.success("Installing command: #{File.basename(file)}")
|
125
|
+
FileUtils.cp(file, destination_dir)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def install_default_agents
|
130
|
+
agents_dir = Aircana.configuration.agents_dir
|
131
|
+
Aircana.create_dir_if_needed(agents_dir)
|
132
|
+
|
133
|
+
copy_agent_files(agents_dir)
|
134
|
+
end
|
135
|
+
|
136
|
+
def copy_agent_files(destination_dir)
|
137
|
+
agent_files_pattern = File.join(Aircana.configuration.output_dir, "agents", "*.md")
|
138
|
+
Dir.glob(agent_files_pattern).each do |file|
|
139
|
+
agent_name = File.basename(file, ".md")
|
140
|
+
next unless default_agent?(agent_name)
|
141
|
+
|
142
|
+
destination_file = File.join(destination_dir, File.basename(file))
|
143
|
+
# Skip copying if source and destination are the same
|
144
|
+
next if File.expand_path(file) == File.expand_path(destination_file)
|
145
|
+
|
146
|
+
Aircana.human_logger.success("Installing default agent: #{agent_name}")
|
147
|
+
FileUtils.cp(file, destination_dir)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def default_agent?(agent_name)
|
152
|
+
require_relative "../../generators/agents_generator"
|
153
|
+
Aircana::Generators::AgentsGenerator.available_default_agents.include?(agent_name)
|
154
|
+
end
|
155
|
+
|
156
|
+
def install_hooks
|
157
|
+
hooks_dir = Aircana.configuration.hooks_dir
|
158
|
+
return unless Dir.exist?(Aircana.configuration.output_dir)
|
159
|
+
|
160
|
+
# Copy hook scripts
|
161
|
+
hook_files = Dir.glob("#{Aircana.configuration.output_dir}/hooks/*.sh")
|
162
|
+
return unless hook_files.any?
|
163
|
+
|
164
|
+
hook_files.each do |file|
|
165
|
+
Aircana.human_logger.success("Installing hook: #{File.basename(file)}")
|
166
|
+
FileUtils.cp(file, hooks_dir)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Create hooks manifest
|
170
|
+
create_hooks_manifest
|
171
|
+
end
|
172
|
+
|
173
|
+
def create_hooks_manifest
|
174
|
+
hooks_config = build_hooks_config
|
175
|
+
|
176
|
+
return if hooks_config.empty?
|
177
|
+
|
178
|
+
manifest = HooksManifest.new(Aircana.configuration.plugin_root)
|
179
|
+
manifest.create(hooks_config)
|
180
|
+
|
181
|
+
Aircana.human_logger.success("Created hooks manifest at hooks/hooks.json")
|
182
|
+
end
|
183
|
+
|
184
|
+
def build_hooks_config
|
185
|
+
hooks = {}
|
186
|
+
|
187
|
+
# Map hook files to Claude Code hook events and their properties
|
188
|
+
hook_mappings = {
|
189
|
+
"pre_tool_use" => { event: "PreToolUse", matcher: nil },
|
190
|
+
"post_tool_use" => { event: "PostToolUse", matcher: nil },
|
191
|
+
"user_prompt_submit" => { event: "UserPromptSubmit", matcher: nil },
|
192
|
+
"session_start" => { event: "SessionStart", matcher: nil },
|
193
|
+
"notification_sqs" => { event: "Notification", matcher: nil },
|
194
|
+
"rubocop_pre_commit" => { event: "PreToolUse", matcher: "Bash" },
|
195
|
+
"rspec_test" => { event: "PostToolUse", matcher: "Bash" },
|
196
|
+
"bundle_install" => { event: "PostToolUse", matcher: "Bash" }
|
197
|
+
}
|
198
|
+
|
199
|
+
Dir.glob("#{Aircana.configuration.hooks_dir}/*.sh").each do |hook_file|
|
200
|
+
hook_name = File.basename(hook_file, ".sh")
|
201
|
+
|
202
|
+
# Determine mapping for this hook
|
203
|
+
mapping = if hook_mappings.key?(hook_name)
|
204
|
+
hook_mappings[hook_name]
|
205
|
+
else
|
206
|
+
# For custom hooks, try to infer the event type from the filename
|
207
|
+
infer_hook_mapping(hook_name)
|
208
|
+
end
|
209
|
+
|
210
|
+
next unless mapping
|
211
|
+
|
212
|
+
event_key = mapping[:event]
|
213
|
+
|
214
|
+
# Create relative path using ${CLAUDE_PLUGIN_ROOT}
|
215
|
+
relative_path = "${CLAUDE_PLUGIN_ROOT}/hooks/#{hook_name}.sh"
|
216
|
+
|
217
|
+
hook_entry = {
|
218
|
+
"type" => "command",
|
219
|
+
"command" => relative_path
|
220
|
+
}
|
221
|
+
|
222
|
+
hook_config = {
|
223
|
+
"hooks" => [hook_entry]
|
224
|
+
}
|
225
|
+
|
226
|
+
# Add matcher if specified
|
227
|
+
hook_config["matcher"] = mapping[:matcher] if mapping[:matcher]
|
228
|
+
|
229
|
+
hooks[event_key] ||= []
|
230
|
+
hooks[event_key] << hook_config
|
231
|
+
end
|
232
|
+
|
233
|
+
hooks
|
234
|
+
end
|
235
|
+
|
236
|
+
def infer_hook_mapping(hook_name)
|
237
|
+
# Try to infer the event type from common patterns in the hook name
|
238
|
+
case hook_name
|
239
|
+
when /pre_tool_use|pre_tool|before_tool/i
|
240
|
+
{ event: "PreToolUse", matcher: nil }
|
241
|
+
when /user_prompt|prompt_submit|before_prompt/i
|
242
|
+
{ event: "UserPromptSubmit", matcher: nil }
|
243
|
+
when /session_start|session_init|startup/i
|
244
|
+
{ event: "SessionStart", matcher: nil }
|
245
|
+
else
|
246
|
+
# Default to PostToolUse for unknown custom hooks and post_tool patterns
|
247
|
+
{ event: "PostToolUse", matcher: nil }
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def display_success_message(metadata)
|
252
|
+
Aircana.human_logger.success("\nPlugin '#{metadata[:name]}' initialized successfully!")
|
253
|
+
Aircana.human_logger.info("\nPlugin structure:")
|
254
|
+
Aircana.human_logger.info(" .claude-plugin/plugin.json - Plugin metadata")
|
255
|
+
Aircana.human_logger.info(" commands/ - Slash commands")
|
256
|
+
Aircana.human_logger.info(" agents/ - Specialized agents")
|
257
|
+
Aircana.human_logger.info(" hooks/ - Event hooks")
|
258
|
+
Aircana.human_logger.info("\nNext steps:")
|
259
|
+
Aircana.human_logger.info(" - Create agents: aircana agents create")
|
260
|
+
Aircana.human_logger.info(" - Install plugin in Claude Code")
|
261
|
+
Aircana.human_logger.info(" - Run: aircana plugin info")
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "tty-prompt"
|
5
|
+
require_relative "../../plugin_manifest"
|
6
|
+
|
7
|
+
module Aircana
|
8
|
+
module CLI
|
9
|
+
module Plugin # rubocop:disable Metrics/ModuleLength
|
10
|
+
class << self
|
11
|
+
def info # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
12
|
+
ensure_plugin_exists!
|
13
|
+
|
14
|
+
manifest = PluginManifest.new(Aircana.configuration.plugin_root)
|
15
|
+
data = manifest.read
|
16
|
+
|
17
|
+
Aircana.human_logger.info("Plugin Information:")
|
18
|
+
Aircana.human_logger.info(" Name: #{data["name"]}")
|
19
|
+
Aircana.human_logger.info(" Version: #{data["version"]}")
|
20
|
+
Aircana.human_logger.info(" Description: #{data["description"]}") if data["description"]
|
21
|
+
Aircana.human_logger.info(" Author: #{data["author"]}") if data["author"]
|
22
|
+
Aircana.human_logger.info(" License: #{data["license"]}") if data["license"]
|
23
|
+
Aircana.human_logger.info(" Homepage: #{data["homepage"]}") if data["homepage"]
|
24
|
+
Aircana.human_logger.info(" Repository: #{data["repository"]}") if data["repository"]
|
25
|
+
|
26
|
+
Aircana.human_logger.info(" Keywords: #{data["keywords"].join(", ")}") if data["keywords"]&.any?
|
27
|
+
|
28
|
+
Aircana.human_logger.info("\nManifest location: #{manifest.manifest_path}")
|
29
|
+
end
|
30
|
+
|
31
|
+
def update # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
32
|
+
ensure_plugin_exists!
|
33
|
+
|
34
|
+
manifest = PluginManifest.new(Aircana.configuration.plugin_root)
|
35
|
+
current_data = manifest.read
|
36
|
+
|
37
|
+
prompt = TTY::Prompt.new
|
38
|
+
|
39
|
+
# Build update hash with only fields that user wants to change
|
40
|
+
updates = {}
|
41
|
+
|
42
|
+
field_prompts = {
|
43
|
+
"description" => "Description",
|
44
|
+
"author" => "Author",
|
45
|
+
"homepage" => "Homepage URL",
|
46
|
+
"repository" => "Repository URL",
|
47
|
+
"license" => "License"
|
48
|
+
}
|
49
|
+
|
50
|
+
field_prompts.each do |field, label|
|
51
|
+
current = current_data[field]
|
52
|
+
value = prompt.ask("#{label}:", default: current)
|
53
|
+
updates[field] = value if value != current
|
54
|
+
end
|
55
|
+
|
56
|
+
# Handle keywords separately (array)
|
57
|
+
if prompt.yes?("Update keywords?")
|
58
|
+
current_keywords = (current_data["keywords"] || []).join(", ")
|
59
|
+
keywords_input = prompt.ask("Keywords (comma-separated):", default: current_keywords)
|
60
|
+
updates["keywords"] = keywords_input.split(",").map(&:strip) if keywords_input
|
61
|
+
end
|
62
|
+
|
63
|
+
if updates.empty?
|
64
|
+
Aircana.human_logger.info("No changes made.")
|
65
|
+
return
|
66
|
+
end
|
67
|
+
|
68
|
+
manifest.update(updates)
|
69
|
+
Aircana.human_logger.success("Plugin manifest updated successfully!")
|
70
|
+
end
|
71
|
+
|
72
|
+
def version(action = nil, bump_type = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
73
|
+
ensure_plugin_exists!
|
74
|
+
|
75
|
+
manifest = PluginManifest.new(Aircana.configuration.plugin_root)
|
76
|
+
|
77
|
+
case action
|
78
|
+
when "bump"
|
79
|
+
type = bump_type&.to_sym || :patch
|
80
|
+
new_version = manifest.bump_version(type)
|
81
|
+
Aircana.human_logger.success("Version bumped to #{new_version}")
|
82
|
+
when "set"
|
83
|
+
prompt = TTY::Prompt.new
|
84
|
+
new_version = prompt.ask("New version:")
|
85
|
+
manifest.update("version" => new_version)
|
86
|
+
Aircana.human_logger.success("Version set to #{new_version}")
|
87
|
+
else
|
88
|
+
data = manifest.read
|
89
|
+
Aircana.human_logger.info("Current version: #{data["version"]}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def validate # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
94
|
+
Aircana.human_logger.info("Validating plugin structure...")
|
95
|
+
|
96
|
+
errors = []
|
97
|
+
|
98
|
+
# Check plugin manifest
|
99
|
+
manifest = PluginManifest.new(Aircana.configuration.plugin_root)
|
100
|
+
if manifest.exists?
|
101
|
+
begin
|
102
|
+
manifest.validate!
|
103
|
+
Aircana.human_logger.success("✓ Plugin manifest is valid")
|
104
|
+
rescue Aircana::Error => e
|
105
|
+
errors << "Plugin manifest validation failed: #{e.message}"
|
106
|
+
end
|
107
|
+
else
|
108
|
+
errors << "Plugin manifest not found at #{manifest.manifest_path}"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Check directory structure
|
112
|
+
%w[agents commands hooks].each do |dir|
|
113
|
+
dir_path = File.join(Aircana.configuration.plugin_root, dir)
|
114
|
+
if Dir.exist?(dir_path)
|
115
|
+
Aircana.human_logger.success("✓ Directory exists: #{dir}/")
|
116
|
+
else
|
117
|
+
errors << "Missing directory: #{dir}/"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Check hooks manifest if hooks directory exists
|
122
|
+
if Dir.exist?(Aircana.configuration.hooks_dir)
|
123
|
+
hooks_manifest = HooksManifest.new(Aircana.configuration.plugin_root)
|
124
|
+
if hooks_manifest.exists?
|
125
|
+
begin
|
126
|
+
hooks_manifest.validate!
|
127
|
+
Aircana.human_logger.success("✓ Hooks manifest is valid")
|
128
|
+
rescue Aircana::Error => e
|
129
|
+
errors << "Hooks manifest validation failed: #{e.message}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Summary
|
135
|
+
if errors.empty?
|
136
|
+
Aircana.human_logger.success("\nPlugin validation passed! ✓")
|
137
|
+
else
|
138
|
+
Aircana.human_logger.error("\nValidation failed with #{errors.size} error(s):")
|
139
|
+
errors.each { |error| Aircana.human_logger.error(" - #{error}") }
|
140
|
+
exit 1
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def ensure_plugin_exists!
|
147
|
+
manifest = PluginManifest.new(Aircana.configuration.plugin_root)
|
148
|
+
return if manifest.exists?
|
149
|
+
|
150
|
+
Aircana.human_logger.error("No plugin found in current directory.")
|
151
|
+
Aircana.human_logger.info("Run 'aircana init' to create a new plugin.")
|
152
|
+
exit 1
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -26,8 +26,7 @@ module Aircana
|
|
26
26
|
"File Management" => %w[files],
|
27
27
|
"Agent Management" => %w[agents],
|
28
28
|
"Hook Management" => %w[hooks],
|
29
|
-
"
|
30
|
-
"System" => %w[generate install doctor dump-context]
|
29
|
+
"System" => %w[generate init doctor dump-context]
|
31
30
|
}
|
32
31
|
end
|
33
32
|
|
@@ -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)
|
@@ -4,27 +4,53 @@ module Aircana
|
|
4
4
|
class Configuration
|
5
5
|
attr_accessor :global_dir, :project_dir, :stream, :output_dir,
|
6
6
|
:claude_code_config_path, :claude_code_project_config_path, :agent_knowledge_dir,
|
7
|
-
:hooks_dir, :confluence_base_url, :confluence_username, :confluence_api_token
|
7
|
+
:hooks_dir, :confluence_base_url, :confluence_username, :confluence_api_token,
|
8
|
+
:plugin_root, :plugin_manifest_dir, :commands_dir, :agents_dir
|
8
9
|
|
9
10
|
def initialize
|
10
11
|
setup_directory_paths
|
12
|
+
setup_plugin_paths
|
11
13
|
setup_claude_code_paths
|
12
14
|
setup_stream
|
13
15
|
setup_confluence_config
|
14
16
|
end
|
15
17
|
|
18
|
+
# Returns true if the current directory is a plugin (has .claude-plugin/plugin.json)
|
19
|
+
def plugin_mode?
|
20
|
+
File.exist?(File.join(@plugin_root, ".claude-plugin", "plugin.json"))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the path to the plugin manifest file
|
24
|
+
def plugin_manifest_path
|
25
|
+
File.join(@plugin_manifest_dir, "plugin.json")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the path to the hooks manifest file
|
29
|
+
def hooks_manifest_path
|
30
|
+
File.join(@hooks_dir, "hooks.json")
|
31
|
+
end
|
32
|
+
|
16
33
|
private
|
17
34
|
|
18
35
|
def setup_directory_paths
|
19
36
|
@global_dir = File.join(Dir.home, ".aircana")
|
20
37
|
@project_dir = Dir.pwd
|
21
38
|
@output_dir = File.join(@global_dir, "aircana.out")
|
22
|
-
|
23
|
-
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_plugin_paths
|
42
|
+
# Plugin root is the project directory by default
|
43
|
+
@plugin_root = @project_dir
|
44
|
+
@plugin_manifest_dir = File.join(@plugin_root, ".claude-plugin")
|
45
|
+
@commands_dir = File.join(@plugin_root, "commands")
|
46
|
+
@agents_dir = File.join(@plugin_root, "agents")
|
47
|
+
@hooks_dir = File.join(@plugin_root, "hooks")
|
48
|
+
@agent_knowledge_dir = File.join(@plugin_root, "agents")
|
24
49
|
end
|
25
50
|
|
26
51
|
def setup_claude_code_paths
|
27
52
|
@claude_code_config_path = File.join(Dir.home, ".claude")
|
53
|
+
# For backward compatibility, keep this but plugin mode uses plugin_root
|
28
54
|
@claude_code_project_config_path = File.join(Dir.pwd, ".claude")
|
29
55
|
end
|
30
56
|
|
@@ -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)
|
@@ -60,11 +60,12 @@ module Aircana
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def default_output_path
|
63
|
-
File.join(Aircana.configuration.
|
63
|
+
File.join(Aircana.configuration.agents_dir, "#{agent_name}.md")
|
64
64
|
end
|
65
65
|
|
66
66
|
def knowledge_path
|
67
|
-
|
67
|
+
# Use plugin-relative path for knowledge base
|
68
|
+
"agents/#{agent_name}/knowledge/"
|
68
69
|
end
|
69
70
|
end
|
70
71
|
end
|