ace-bundle 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ace-defaults/bundle/config.yml +28 -0
- data/.ace-defaults/bundle/presets/base.md +15 -0
- data/.ace-defaults/bundle/presets/code-review.md +61 -0
- data/.ace-defaults/bundle/presets/development.md +16 -0
- data/.ace-defaults/bundle/presets/documentation-review.md +52 -0
- data/.ace-defaults/bundle/presets/mixed-content-example.md +94 -0
- data/.ace-defaults/bundle/presets/project-context.md +79 -0
- data/.ace-defaults/bundle/presets/project.md +35 -0
- data/.ace-defaults/bundle/presets/section-example-simple.md +27 -0
- data/.ace-defaults/bundle/presets/security-review.md +53 -0
- data/.ace-defaults/bundle/presets/simple-project.md +43 -0
- data/.ace-defaults/bundle/presets/team.md +18 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-bundle.yml +19 -0
- data/CHANGELOG.md +384 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +22 -0
- data/exe/ace-bundle +14 -0
- data/handbook/skills/as-bundle/SKILL.md +28 -0
- data/handbook/skills/as-onboard/SKILL.md +33 -0
- data/handbook/workflow-instructions/bundle.wf.md +111 -0
- data/handbook/workflow-instructions/onboard.wf.md +20 -0
- data/lib/ace/bundle/atoms/boundary_finder.rb +122 -0
- data/lib/ace/bundle/atoms/bundle_normalizer.rb +128 -0
- data/lib/ace/bundle/atoms/content_checker.rb +46 -0
- data/lib/ace/bundle/atoms/line_counter.rb +37 -0
- data/lib/ace/bundle/atoms/preset_list_formatter.rb +44 -0
- data/lib/ace/bundle/atoms/preset_validator.rb +69 -0
- data/lib/ace/bundle/atoms/section_validator.rb +215 -0
- data/lib/ace/bundle/atoms/typo_detector.rb +76 -0
- data/lib/ace/bundle/cli/commands/load.rb +347 -0
- data/lib/ace/bundle/cli.rb +26 -0
- data/lib/ace/bundle/models/bundle_data.rb +75 -0
- data/lib/ace/bundle/molecules/bundle_chunker.rb +280 -0
- data/lib/ace/bundle/molecules/bundle_file_writer.rb +269 -0
- data/lib/ace/bundle/molecules/bundle_merger.rb +248 -0
- data/lib/ace/bundle/molecules/preset_manager.rb +331 -0
- data/lib/ace/bundle/molecules/section_compressor.rb +249 -0
- data/lib/ace/bundle/molecules/section_formatter.rb +580 -0
- data/lib/ace/bundle/molecules/section_processor.rb +460 -0
- data/lib/ace/bundle/organisms/bundle_loader.rb +1436 -0
- data/lib/ace/bundle/organisms/pr_bundle_loader.rb +147 -0
- data/lib/ace/bundle/version.rb +7 -0
- data/lib/ace/bundle.rb +251 -0
- metadata +190 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Bundle
|
|
5
|
+
module Atoms
|
|
6
|
+
# Pure functions for detecting typos in frontmatter keys
|
|
7
|
+
# Uses Levenshtein-like edit distance for similarity detection
|
|
8
|
+
module TypoDetector
|
|
9
|
+
# Known keys for templates and workflows
|
|
10
|
+
KNOWN_FRONTMATTER_KEYS = %w[
|
|
11
|
+
context files commands include exclude diffs
|
|
12
|
+
name description allowed-tools params argument-hint
|
|
13
|
+
update frequency sections last-updated
|
|
14
|
+
auto_generate template-refs embed_document_source
|
|
15
|
+
doc-type purpose source title author version
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Detect suspicious frontmatter keys that might be typos
|
|
20
|
+
# @param frontmatter [Hash] Parsed frontmatter YAML
|
|
21
|
+
# @param path [String] File path for warning message
|
|
22
|
+
# @return [Array<String>] List of warning messages
|
|
23
|
+
def detect_suspicious_keys(frontmatter, path)
|
|
24
|
+
warnings = []
|
|
25
|
+
frontmatter.keys.each do |key|
|
|
26
|
+
next if KNOWN_FRONTMATTER_KEYS.include?(key)
|
|
27
|
+
|
|
28
|
+
# Check for common typos using Levenshtein-like distance
|
|
29
|
+
KNOWN_FRONTMATTER_KEYS.each do |known|
|
|
30
|
+
if typo_distance(key, known) <= 2
|
|
31
|
+
warnings << "Possible typo in #{path}: frontmatter key '#{key}' looks similar to known key '#{known}'"
|
|
32
|
+
break
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
warnings
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Calculate simple edit distance between two strings
|
|
41
|
+
# Uses Levenshtein distance algorithm
|
|
42
|
+
# @param str1 [String] First string
|
|
43
|
+
# @param str2 [String] Second string
|
|
44
|
+
# @return [Integer] Edit distance
|
|
45
|
+
def typo_distance(str1, str2)
|
|
46
|
+
return str2.length if str1.empty?
|
|
47
|
+
return str1.length if str2.empty?
|
|
48
|
+
|
|
49
|
+
# Create distance matrix
|
|
50
|
+
rows = str1.length + 1
|
|
51
|
+
cols = str2.length + 1
|
|
52
|
+
dist = Array.new(rows) { Array.new(cols, 0) }
|
|
53
|
+
|
|
54
|
+
# Initialize first row and column
|
|
55
|
+
(0...rows).each { |i| dist[i][0] = i }
|
|
56
|
+
(0...cols).each { |j| dist[0][j] = j }
|
|
57
|
+
|
|
58
|
+
# Fill in rest of matrix
|
|
59
|
+
(1...rows).each do |i|
|
|
60
|
+
(1...cols).each do |j|
|
|
61
|
+
cost = (str1[i - 1] == str2[j - 1]) ? 0 : 1
|
|
62
|
+
dist[i][j] = [
|
|
63
|
+
dist[i - 1][j] + 1, # deletion
|
|
64
|
+
dist[i][j - 1] + 1, # insertion
|
|
65
|
+
dist[i - 1][j - 1] + cost # substitution
|
|
66
|
+
].min
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
dist[rows - 1][cols - 1]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "ace/support/fs"
|
|
5
|
+
require_relative "../../atoms/line_counter"
|
|
6
|
+
require_relative "../../atoms/preset_list_formatter"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Bundle
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
# ace-support-cli Command class for the load command
|
|
13
|
+
#
|
|
14
|
+
# Loads context from preset, file, or protocol URL
|
|
15
|
+
class Load < Ace::Support::Cli::Command
|
|
16
|
+
include Ace::Support::Cli::Base
|
|
17
|
+
|
|
18
|
+
desc <<~DESC.strip
|
|
19
|
+
Load context from preset, file, or protocol URL
|
|
20
|
+
|
|
21
|
+
INPUT can be:
|
|
22
|
+
- Preset name (e.g., 'project', 'base')
|
|
23
|
+
- File path (e.g., '/path/to/config.yml', './context.md')
|
|
24
|
+
- Protocol URL (e.g., 'wfi://workflow', 'guide://testing')
|
|
25
|
+
|
|
26
|
+
Configuration:
|
|
27
|
+
Global config: ~/.ace/bundle/config.yml
|
|
28
|
+
Project config: .ace/bundle/config.yml
|
|
29
|
+
Example: ace-bundle/.ace-defaults/bundle/config.yml
|
|
30
|
+
|
|
31
|
+
Presets defined in: .ace/bundle/presets/
|
|
32
|
+
|
|
33
|
+
Output:
|
|
34
|
+
By default, output saved to cache and file path printed
|
|
35
|
+
Use --output stdio to print to stdout
|
|
36
|
+
Exit codes: 0 (success), 1 (error)
|
|
37
|
+
|
|
38
|
+
Protocols:
|
|
39
|
+
wfi:// Workflow instructions
|
|
40
|
+
guide:// Development guides
|
|
41
|
+
prompt:// Prompt templates
|
|
42
|
+
tmpl:// General templates
|
|
43
|
+
DESC
|
|
44
|
+
|
|
45
|
+
example [
|
|
46
|
+
"project # Load project preset",
|
|
47
|
+
"wfi://work-on-task # Load workflow via protocol",
|
|
48
|
+
"-p base -p custom # Merge multiple presets",
|
|
49
|
+
"-f config.yml # Load from file",
|
|
50
|
+
"--inspect-config # Show resolved configuration"
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Define positional argument
|
|
54
|
+
argument :input, required: false, desc: "Preset name, file path, or protocol URL"
|
|
55
|
+
|
|
56
|
+
# Preset options
|
|
57
|
+
option :preset, type: :array, aliases: %w[-p], desc: "Load context from preset (can be used multiple times)"
|
|
58
|
+
option :presets, type: :string, desc: "Load multiple presets (comma-separated list)"
|
|
59
|
+
|
|
60
|
+
# File options
|
|
61
|
+
option :file, type: :array, aliases: %w[-f], desc: "Load context from file (can be used multiple times)"
|
|
62
|
+
|
|
63
|
+
# Config options
|
|
64
|
+
option :inspect_config, type: :boolean, desc: "Show merged configuration without loading files"
|
|
65
|
+
|
|
66
|
+
# Output options
|
|
67
|
+
option :embed_source, type: :boolean, aliases: %w[-e], desc: "Embed source document in output"
|
|
68
|
+
option :output, type: :string, aliases: %w[-o], desc: "Output mode: stdio, cache, or file path"
|
|
69
|
+
option :format, type: :string, desc: "Output format (markdown, yaml, xml, markdown-xml, json)"
|
|
70
|
+
|
|
71
|
+
# Compression
|
|
72
|
+
option :compressor, type: :string, default: nil,
|
|
73
|
+
desc: "Enable/disable compression: on, off"
|
|
74
|
+
option :compressor_mode, type: :string, default: nil,
|
|
75
|
+
desc: "Compressor engine: exact, agent (default: exact)"
|
|
76
|
+
option :compressor_source_scope, type: :string, default: nil,
|
|
77
|
+
desc: "Source handling: off, per-source, merged (default: off)"
|
|
78
|
+
|
|
79
|
+
# Resource limits
|
|
80
|
+
option :max_size, type: :integer, desc: "Maximum file size in bytes"
|
|
81
|
+
option :timeout, type: :integer, desc: "Command timeout in seconds"
|
|
82
|
+
|
|
83
|
+
# Standard options (inherited from Base but need explicit definition for ace-support-cli)
|
|
84
|
+
option :version, type: :boolean, desc: "Show version information"
|
|
85
|
+
option :list_presets, type: :boolean, desc: "List available context presets"
|
|
86
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
87
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
88
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
89
|
+
|
|
90
|
+
def call(input: nil, **options)
|
|
91
|
+
# Handle --help/-h passed as input argument
|
|
92
|
+
if input == "--help" || input == "-h"
|
|
93
|
+
# ace-support-cli will handle this
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if options[:version]
|
|
98
|
+
puts "ace-bundle #{Ace::Bundle::VERSION}"
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if options[:list_presets]
|
|
103
|
+
presets = Ace::Bundle.list_presets
|
|
104
|
+
Atoms::PresetListFormatter.format(presets).each { |line| puts line }
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Type-convert numeric options using Base helper for proper validation
|
|
109
|
+
# coerce_types uses Integer() which raises ArgumentError on invalid input
|
|
110
|
+
# (unlike .to_i which silently returns 0)
|
|
111
|
+
coerce_types(options, max_size: :integer, timeout: :integer)
|
|
112
|
+
|
|
113
|
+
# Handle repeatable options (type: :array returns array, single values need wrapping)
|
|
114
|
+
# --preset returns array when used multiple times, nil otherwise
|
|
115
|
+
if options[:preset] && options[:presets]
|
|
116
|
+
# If both --preset and --presets provided, merge them
|
|
117
|
+
presets = [options[:preset]].flatten + options[:presets].split(",")
|
|
118
|
+
options[:preset] = presets.map(&:strip)
|
|
119
|
+
elsif options[:presets]
|
|
120
|
+
options[:preset] = options[:presets].split(",").map(&:strip)
|
|
121
|
+
elsif options[:preset]
|
|
122
|
+
# Ensure array even for single value
|
|
123
|
+
options[:preset] = [options[:preset]].flatten
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Same for file option
|
|
127
|
+
options[:file] = [options[:file]].flatten if options[:file]
|
|
128
|
+
|
|
129
|
+
# Normalize --compressor toggle
|
|
130
|
+
if options.key?(:compressor) && options[:compressor]
|
|
131
|
+
val = options[:compressor].to_s.downcase
|
|
132
|
+
options[:compressor] = case val
|
|
133
|
+
when "true", "yes", "on", "1" then "on"
|
|
134
|
+
when "false", "no", "off", "0" then "off"
|
|
135
|
+
else val
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Normalize compressor_source_scope option
|
|
140
|
+
if options.key?(:compressor_source_scope) && options[:compressor_source_scope]
|
|
141
|
+
val = options[:compressor_source_scope].to_s.downcase
|
|
142
|
+
options[:compressor_source_scope] = case val
|
|
143
|
+
when "true", "yes", "on", "" then "per-source"
|
|
144
|
+
when "false", "no", "off" then "off"
|
|
145
|
+
else val
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
execute(input, options)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def execute(input, options)
|
|
155
|
+
display_config_summary(options)
|
|
156
|
+
# Process repeatable options and extract mutable values
|
|
157
|
+
presets, files = process_options(options)
|
|
158
|
+
|
|
159
|
+
# Determine input source and load context
|
|
160
|
+
result = if options[:inspect_config]
|
|
161
|
+
inspect_config_mode(presets, files, input, options)
|
|
162
|
+
elsif @multi_input_mode
|
|
163
|
+
load_multiple_inputs(presets, files, options)
|
|
164
|
+
elsif input
|
|
165
|
+
load_auto(input, options)
|
|
166
|
+
else
|
|
167
|
+
load_auto("default", options)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Handle errors
|
|
171
|
+
if result[:context].metadata[:error]
|
|
172
|
+
msg = result[:context].metadata[:error]
|
|
173
|
+
if result[:context].metadata[:errors] && options[:debug]
|
|
174
|
+
msg = "#{msg}\n#{result[:context].metadata[:errors].join("\n")}"
|
|
175
|
+
end
|
|
176
|
+
raise Ace::Support::Cli::Error.new(msg)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Handle output
|
|
180
|
+
handle_output(result[:context], result[:input], options)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def process_options(options)
|
|
184
|
+
# Extract preset options (already normalized in call method)
|
|
185
|
+
presets = Array(options[:preset] || []).compact
|
|
186
|
+
|
|
187
|
+
# Extract file options
|
|
188
|
+
files = Array(options[:file] || []).compact
|
|
189
|
+
|
|
190
|
+
# Determine if we're in multi-input mode
|
|
191
|
+
@multi_input_mode = presets.any? || files.any?
|
|
192
|
+
|
|
193
|
+
[presets, files]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def inspect_config_mode(presets, files, input, options)
|
|
197
|
+
inputs = []
|
|
198
|
+
inputs.concat(presets) if presets.any?
|
|
199
|
+
inputs.concat(files) if files.any?
|
|
200
|
+
inputs << input if input && inputs.empty?
|
|
201
|
+
inputs << "default" if inputs.empty?
|
|
202
|
+
|
|
203
|
+
context = Ace::Bundle.inspect_config(inputs, options)
|
|
204
|
+
{context: context, input: inputs.join("-")}
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def load_multiple_inputs(presets, files, options)
|
|
208
|
+
context = Ace::Bundle.load_multiple_inputs(presets, files, options)
|
|
209
|
+
|
|
210
|
+
# Create input string for cache filename
|
|
211
|
+
all_inputs = presets + files.map { |f| File.basename(f, ".*") }
|
|
212
|
+
input = all_inputs.join("-")
|
|
213
|
+
|
|
214
|
+
{context: context, input: input}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def load_auto(input, options)
|
|
218
|
+
context = Ace::Bundle.load_auto(input, options)
|
|
219
|
+
{context: context, input: input}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def handle_output(context, input, options)
|
|
223
|
+
# Determine output mode
|
|
224
|
+
# Priority: CLI flag > preset metadata > auto-format based on line count
|
|
225
|
+
explicit_output = options[:output] || context.metadata[:output]
|
|
226
|
+
|
|
227
|
+
if explicit_output
|
|
228
|
+
# Explicit output mode specified - honor it
|
|
229
|
+
output_mode = explicit_output
|
|
230
|
+
else
|
|
231
|
+
# Auto-format: decide based on line count vs threshold
|
|
232
|
+
size_key = :raw_content_for_auto_format
|
|
233
|
+
size_source = context.metadata[size_key] ||
|
|
234
|
+
context.metadata[size_key.to_s] ||
|
|
235
|
+
context.content
|
|
236
|
+
line_count = Atoms::LineCounter.count(size_source)
|
|
237
|
+
threshold = Ace::Bundle.auto_format_threshold
|
|
238
|
+
|
|
239
|
+
output_mode = (line_count >= threshold) ? "cache" : "stdio"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Handle output based on mode
|
|
243
|
+
case output_mode
|
|
244
|
+
when "stdio"
|
|
245
|
+
puts context.content
|
|
246
|
+
when "cache"
|
|
247
|
+
write_to_cache(context, input, options)
|
|
248
|
+
else
|
|
249
|
+
write_to_file(context, output_mode, options)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def write_to_cache(context, input, options)
|
|
254
|
+
project_root = Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current
|
|
255
|
+
configured_cache_dir = Ace::Bundle.cache_dir
|
|
256
|
+
cache_dir = if configured_cache_dir.start_with?("/")
|
|
257
|
+
configured_cache_dir
|
|
258
|
+
else
|
|
259
|
+
File.join(project_root, configured_cache_dir)
|
|
260
|
+
end
|
|
261
|
+
FileUtils.mkdir_p(cache_dir)
|
|
262
|
+
|
|
263
|
+
# Generate cache filename from input (preset name, protocol, or sanitized file path)
|
|
264
|
+
cache_name = input.gsub(/[^a-zA-Z0-9-]/, "_")
|
|
265
|
+
cache_file = File.join(cache_dir, "#{cache_name}.md")
|
|
266
|
+
result = Ace::Bundle.write_output(context, cache_file, options)
|
|
267
|
+
|
|
268
|
+
if result[:success]
|
|
269
|
+
if result[:chunked]
|
|
270
|
+
chunks = result[:results].select { |r| r[:file_type] == "chunk" }
|
|
271
|
+
total_lines = chunks.sum { |r| r[:lines] || 0 }
|
|
272
|
+
total_size = chunks.sum { |r| r[:size] || 0 }
|
|
273
|
+
puts "Bundle saved (#{total_lines} lines, #{format_size(total_size)}) in #{chunks.size} chunks:"
|
|
274
|
+
chunks.each { |r| puts r[:path] }
|
|
275
|
+
else
|
|
276
|
+
puts "Bundle saved (#{result[:lines]} lines, #{result[:size_formatted]}), output file:"
|
|
277
|
+
puts cache_file
|
|
278
|
+
end
|
|
279
|
+
else
|
|
280
|
+
raise Ace::Support::Cli::Error.new("Error writing cache: #{result[:error]}")
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def write_to_file(context, file_path, options)
|
|
285
|
+
output_dir = File.dirname(file_path)
|
|
286
|
+
FileUtils.mkdir_p(output_dir) unless output_dir == "."
|
|
287
|
+
|
|
288
|
+
result = Ace::Bundle.write_output(context, file_path, options)
|
|
289
|
+
|
|
290
|
+
if result[:success]
|
|
291
|
+
if result[:chunked]
|
|
292
|
+
chunks = result[:results].select { |r| r[:file_type] == "chunk" }
|
|
293
|
+
total_lines = chunks.sum { |r| r[:lines] || 0 }
|
|
294
|
+
total_size = chunks.sum { |r| r[:size] || 0 }
|
|
295
|
+
puts "Bundle saved (#{total_lines} lines, #{format_size(total_size)}) in #{chunks.size} chunks:"
|
|
296
|
+
chunks.each { |r| puts r[:path] }
|
|
297
|
+
else
|
|
298
|
+
puts "Bundle saved (#{result[:lines]} lines, #{result[:size_formatted]}), output file:"
|
|
299
|
+
puts file_path
|
|
300
|
+
end
|
|
301
|
+
else
|
|
302
|
+
raise Ace::Support::Cli::Error.new("Error writing file: #{result[:error]}")
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def format_size(bytes)
|
|
307
|
+
units = ["B", "KB", "MB", "GB"]
|
|
308
|
+
size = bytes.to_f
|
|
309
|
+
unit_index = 0
|
|
310
|
+
while size >= 1024 && unit_index < units.size - 1
|
|
311
|
+
size /= 1024
|
|
312
|
+
unit_index += 1
|
|
313
|
+
end
|
|
314
|
+
"#{size.round(2)} #{units[unit_index]}"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def display_config_summary(options)
|
|
318
|
+
return if options[:quiet]
|
|
319
|
+
|
|
320
|
+
require "ace/core"
|
|
321
|
+
Ace::Core::Atoms::ConfigSummary.display(
|
|
322
|
+
command: "load",
|
|
323
|
+
config: Ace::Bundle.config,
|
|
324
|
+
defaults: load_gem_defaults,
|
|
325
|
+
options: options,
|
|
326
|
+
quiet: false
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def load_gem_defaults
|
|
331
|
+
gem_root = Gem.loaded_specs["ace-bundle"]&.gem_dir ||
|
|
332
|
+
File.expand_path("../../../../..", __dir__)
|
|
333
|
+
defaults_path = File.join(gem_root, ".ace-defaults", "bundle", "config.yml")
|
|
334
|
+
|
|
335
|
+
if File.exist?(defaults_path)
|
|
336
|
+
require "yaml"
|
|
337
|
+
data = YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
|
|
338
|
+
data["bundle"] || data
|
|
339
|
+
else
|
|
340
|
+
{}
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "../bundle"
|
|
6
|
+
require_relative "cli/commands/load"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Bundle
|
|
10
|
+
# CLI namespace for ace-bundle command loading.
|
|
11
|
+
#
|
|
12
|
+
# ace-bundle uses a single-command ace-support-cli entrypoint that calls
|
|
13
|
+
# CLI::Commands::Load directly from the executable.
|
|
14
|
+
module CLI
|
|
15
|
+
# Entry point for CLI invocation (used by tests via cli_helpers)
|
|
16
|
+
#
|
|
17
|
+
# Mirrors exe behavior: empty args show help.
|
|
18
|
+
#
|
|
19
|
+
# @param args [Array<String>] Command-line arguments
|
|
20
|
+
def self.start(args)
|
|
21
|
+
args = ["--help"] if args.empty?
|
|
22
|
+
Ace::Support::Cli::Runner.new(Commands::Load).call(args: args)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Bundle
|
|
5
|
+
module Models
|
|
6
|
+
# Data model for bundle information
|
|
7
|
+
class BundleData
|
|
8
|
+
attr_accessor :preset_name, :files, :metadata, :content, :commands, :sections
|
|
9
|
+
|
|
10
|
+
def initialize(preset_name: nil, files: nil, metadata: nil, content: "", commands: nil, sections: nil)
|
|
11
|
+
@preset_name = preset_name
|
|
12
|
+
@files = files || []
|
|
13
|
+
@metadata = metadata || {}
|
|
14
|
+
@content = content
|
|
15
|
+
@commands = commands || []
|
|
16
|
+
@sections = sections || {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
preset_name: preset_name,
|
|
22
|
+
files: files,
|
|
23
|
+
metadata: metadata,
|
|
24
|
+
content: content,
|
|
25
|
+
commands: commands,
|
|
26
|
+
sections: sections
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_file(path, content)
|
|
31
|
+
@files << {path: path, content: content}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def file_count
|
|
35
|
+
@files.size
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def total_size
|
|
39
|
+
@files.sum { |f| f[:content].to_s.bytesize }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Section-related methods
|
|
43
|
+
def add_section(name, section_data)
|
|
44
|
+
@sections[name] = section_data
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_section(name)
|
|
48
|
+
@sections[name]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def has_sections?
|
|
52
|
+
!@sections.empty?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def section_count
|
|
56
|
+
@sections.size
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sorted_sections
|
|
60
|
+
# In Ruby 3.2+, hash insertion order is preserved
|
|
61
|
+
# This returns sections in the order they appear in the YAML file
|
|
62
|
+
@sections.to_a
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def section_names
|
|
66
|
+
@sections.keys
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def clear_sections
|
|
70
|
+
@sections.clear
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|