docopslab-dev 0.1.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/LICENSE +21 -0
- data/README.adoc +904 -0
- data/assets/config-packs/actionlint/base.yml +13 -0
- data/assets/config-packs/actionlint/project.yml +13 -0
- data/assets/config-packs/htmlproofer/base.yml +27 -0
- data/assets/config-packs/htmlproofer/project.yml +25 -0
- data/assets/config-packs/rubocop/base.yml +130 -0
- data/assets/config-packs/rubocop/project.yml +8 -0
- data/assets/config-packs/shellcheck/base.shellcheckrc +14 -0
- data/assets/config-packs/subtxt/ai-asciidoc-antipatterns.sub.txt +11 -0
- data/assets/config-packs/vale/asciidoc/ExplicitSectionIDs.yml +8 -0
- data/assets/config-packs/vale/asciidoc/ExtraLineBeforeLevel1.yml +7 -0
- data/assets/config-packs/vale/asciidoc/OneSentencePerLine.yml +8 -0
- data/assets/config-packs/vale/asciidoc/PreferSourceBlocks.yml +8 -0
- data/assets/config-packs/vale/asciidoc/ProperAdmonitions.yml +8 -0
- data/assets/config-packs/vale/asciidoc/ProperDLs.yml +7 -0
- data/assets/config-packs/vale/asciidoc/UncleanListStart.yml +8 -0
- data/assets/config-packs/vale/authoring/ButParagraph.yml +8 -0
- data/assets/config-packs/vale/authoring/ExNotEg.yml +8 -0
- data/assets/config-packs/vale/authoring/LiteralTerms.yml +20 -0
- data/assets/config-packs/vale/authoring/Spelling.yml +679 -0
- data/assets/config-packs/vale/base.ini +38 -0
- data/assets/config-packs/vale/config/scripts/ExplicitSectionIDs.tengo +56 -0
- data/assets/config-packs/vale/config/scripts/ExtraLineBeforeLevel1.tengo +121 -0
- data/assets/config-packs/vale/config/scripts/OneSentencePerLine.tengo +53 -0
- data/assets/config-packs/vale/project.ini +5 -0
- data/assets/hooks/pre-commit +63 -0
- data/assets/hooks/pre-push +72 -0
- data/assets/scripts/adoc_section_ids.rb +50 -0
- data/assets/scripts/build-common.sh +193 -0
- data/assets/scripts/build-docker.sh +64 -0
- data/assets/scripts/build.sh +56 -0
- data/assets/scripts/parse_jekyll_asciidoc_logs.rb +467 -0
- data/assets/templates/Gemfile +7 -0
- data/assets/templates/Rakefile +3 -0
- data/assets/templates/gitignore +69 -0
- data/assets/templates/jekyll-asciidoc-fix.prompt.yml +17 -0
- data/assets/templates/spellcheck.prompt.yml +16 -0
- data/docopslab-dev.gemspec +56 -0
- data/docs/agent/AGENTS.md +229 -0
- data/docs/agent/index.md +80 -0
- data/docs/agent/missions/conduct-release.md +224 -0
- data/docs/agent/missions/setup-new-project.md +250 -0
- data/docs/agent/roles/devops-release-engineer.md +152 -0
- data/docs/agent/roles/docops-engineer.md +193 -0
- data/docs/agent/roles/planner-architect.md +74 -0
- data/docs/agent/roles/product-engineer.md +153 -0
- data/docs/agent/roles/product-manager.md +130 -0
- data/docs/agent/roles/project-manager.md +139 -0
- data/docs/agent/roles/qa-testing-engineer.md +115 -0
- data/docs/agent/roles/tech-docs-manager.md +143 -0
- data/docs/agent/roles/tech-writer.md +163 -0
- data/docs/agent/skills/asciidoc.md +609 -0
- data/docs/agent/skills/code-commenting.md +347 -0
- data/docs/agent/skills/fix-broken-links.md +309 -0
- data/docs/agent/skills/fix-jekyll-asciidoc-build-errors.md +23 -0
- data/docs/agent/skills/fix-spelling-issues.md +13 -0
- data/docs/agent/skills/git.md +170 -0
- data/docs/agent/skills/github-issues.md +135 -0
- data/docs/agent/skills/product-release-rollback-and-patching.md +71 -0
- data/docs/agent/skills/rake-cli-dev.md +57 -0
- data/docs/agent/skills/readme-driven-dev.md +13 -0
- data/docs/agent/skills/release-history.md +29 -0
- data/docs/agent/skills/ruby.md +192 -0
- data/docs/agent/skills/schemagraphy-sgyml.md +18 -0
- data/docs/agent/skills/tests-running.md +25 -0
- data/docs/agent/skills/tests-writing.md +45 -0
- data/docs/agent/skills/write-the-docs.md +54 -0
- data/docs/agent/topics/common-project-paths.md +117 -0
- data/docs/agent/topics/dev-tooling-usage.md +202 -0
- data/docs/agent/topics/devops-ci-cd.md +55 -0
- data/docs/agent/topics/product-docs-deployment.md +25 -0
- data/lib/docopslab/dev/auto_fix_asciidoc.rb +46 -0
- data/lib/docopslab/dev/checkers.rb +108 -0
- data/lib/docopslab/dev/config_manager.rb +241 -0
- data/lib/docopslab/dev/file_utils.rb +140 -0
- data/lib/docopslab/dev/git_hooks.rb +140 -0
- data/lib/docopslab/dev/help.rb +121 -0
- data/lib/docopslab/dev/initializer.rb +95 -0
- data/lib/docopslab/dev/linters.rb +451 -0
- data/lib/docopslab/dev/log_parser.rb +31 -0
- data/lib/docopslab/dev/paths.rb +46 -0
- data/lib/docopslab/dev/script_manager.rb +136 -0
- data/lib/docopslab/dev/spell_check.rb +194 -0
- data/lib/docopslab/dev/sync_ops.rb +468 -0
- data/lib/docopslab/dev/tasks.rb +440 -0
- data/lib/docopslab/dev/tool_execution.rb +68 -0
- data/lib/docopslab/dev/version.rb +8 -0
- data/lib/docopslab/dev.rb +392 -0
- data/specs/data/default-manifest.yml +64 -0
- data/specs/data/manifest-schema.yaml +63 -0
- data/specs/data/tasks-def.yml +321 -0
- data/specs/data/tools.yml +60 -0
- metadata +362 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
module DocOpsLab
|
|
8
|
+
module Dev
|
|
9
|
+
module SyncOps
|
|
10
|
+
# rubocop :disable Metrics/ClassLength
|
|
11
|
+
class << self
|
|
12
|
+
def install_vale_styles context
|
|
13
|
+
return unless File.exist?(CONFIG_PATHS[:vale]) && context.tool_available?('vale')
|
|
14
|
+
|
|
15
|
+
puts "๐ Syncing Vale styles using Packages key in #{CONFIG_PATHS[:vale]} (local and remote packages)"
|
|
16
|
+
context.run_with_fallback('vale', "vale --config=#{CONFIG_PATHS[:vale]} sync")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def sync_vale_styles context, local: false
|
|
20
|
+
puts '๐ Syncing Vale styles...'
|
|
21
|
+
|
|
22
|
+
styles_source_root = if context.lab_dev_mode?
|
|
23
|
+
# Running inside lab monorepo
|
|
24
|
+
'gems/docopslab-dev/assets/config-packs/vale'
|
|
25
|
+
else
|
|
26
|
+
# Running from consumer project with path dependency
|
|
27
|
+
File.join(GEM_ROOT, 'assets', 'config-packs', 'vale')
|
|
28
|
+
end
|
|
29
|
+
styles_dest_root = '.config/.vendor/vale/styles'
|
|
30
|
+
FileUtils.mkdir_p(styles_dest_root)
|
|
31
|
+
|
|
32
|
+
# Get the list of local styles from tools.yml
|
|
33
|
+
begin
|
|
34
|
+
tools_yml_path = if context.lab_dev_mode?
|
|
35
|
+
'gems/docopslab-dev/specs/data/tools.yml'
|
|
36
|
+
else
|
|
37
|
+
File.join(GEM_ROOT, 'specs', 'data', 'tools.yml')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
style_paths_array = YAML.load_file(tools_yml_path)
|
|
41
|
+
.find { |t| t['slug'] == 'vale' }['packaging']['packages']
|
|
42
|
+
|
|
43
|
+
synced_styles = 0
|
|
44
|
+
style_paths_array.each do |package|
|
|
45
|
+
vale_name = package['target']
|
|
46
|
+
src_name = package['source']
|
|
47
|
+
source_style_dir = File.join(styles_source_root, src_name)
|
|
48
|
+
dest_style_dir = File.join(styles_dest_root, vale_name)
|
|
49
|
+
|
|
50
|
+
next unless File.directory?(source_style_dir)
|
|
51
|
+
|
|
52
|
+
# Copy style directory
|
|
53
|
+
FileUtils.rm_rf(dest_style_dir)
|
|
54
|
+
FileUtils.cp_r(source_style_dir, dest_style_dir)
|
|
55
|
+
puts " โ Synced custom style: #{vale_name}"
|
|
56
|
+
synced_styles += 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# copy style scripts directory
|
|
60
|
+
scripts_source = File.join(styles_source_root, 'config', 'scripts')
|
|
61
|
+
scripts_dest = File.join(styles_dest_root, 'config', 'scripts')
|
|
62
|
+
if File.directory?(scripts_source)
|
|
63
|
+
FileUtils.rm_rf(scripts_dest)
|
|
64
|
+
FileUtils.mkdir_p(File.dirname(scripts_dest))
|
|
65
|
+
FileUtils.cp_r(scripts_source, scripts_dest)
|
|
66
|
+
puts ' โ Synced style scripts directory'
|
|
67
|
+
else
|
|
68
|
+
puts " โ ๏ธ No style scripts directory found at #{scripts_source}; skipping"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
puts " โ
Synced #{synced_styles} custom Vale style(s)" if synced_styles.positive?
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
puts " โ ๏ธ Error syncing local Vale styles: #{e.message}"
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# If not local-only, also run Vale sync for remote styles
|
|
78
|
+
unless local
|
|
79
|
+
puts '๐ฆ Syncing remote Vale packages...'
|
|
80
|
+
vale_config = CONFIG_PATHS[:vale]
|
|
81
|
+
context.generate_vale_config unless File.exist?(vale_config)
|
|
82
|
+
context.run_with_fallback('vale', "vale --config=#{vale_config} sync")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def sync_docs context, force: false
|
|
89
|
+
manifest = context.load_manifest
|
|
90
|
+
return false unless manifest
|
|
91
|
+
|
|
92
|
+
docs_entries = manifest['docs']
|
|
93
|
+
return false unless docs_entries.is_a?(Array)
|
|
94
|
+
|
|
95
|
+
puts '๐ Syncing documentation files...'
|
|
96
|
+
|
|
97
|
+
synced_count = 0
|
|
98
|
+
skipped_count = 0
|
|
99
|
+
sources_checked = []
|
|
100
|
+
excluded_files = Set.new
|
|
101
|
+
|
|
102
|
+
# First pass: collect all explicitly excluded files (synced: false)
|
|
103
|
+
docs_entries.each do |entry|
|
|
104
|
+
source_pattern = entry['source']
|
|
105
|
+
synced = entry.fetch('synced', false)
|
|
106
|
+
|
|
107
|
+
next unless source_pattern
|
|
108
|
+
next if synced # Only collect exclusions
|
|
109
|
+
|
|
110
|
+
# Resolve source file path
|
|
111
|
+
if source_pattern.include?('*')
|
|
112
|
+
# Glob pattern for exclusions
|
|
113
|
+
source_glob = File.join(GEM_ROOT, source_pattern)
|
|
114
|
+
Dir.glob(source_glob).each do |source_file|
|
|
115
|
+
excluded_files.add(source_file) if File.file?(source_file)
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
# Single file exclusion
|
|
119
|
+
source_file = File.join(GEM_ROOT, source_pattern)
|
|
120
|
+
excluded_files.add(source_file) if File.exist?(source_file)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# rubocop:disable Style/CombinableLoops
|
|
125
|
+
# Second pass: process inclusions, respecting exclusions
|
|
126
|
+
# These loops cannot be combined as they implement different phases of a two-pass algorithm
|
|
127
|
+
docs_entries.each do |entry|
|
|
128
|
+
source_pattern = entry['source']
|
|
129
|
+
target_path = entry['target']
|
|
130
|
+
synced = entry.fetch('synced', false)
|
|
131
|
+
|
|
132
|
+
next unless source_pattern && target_path
|
|
133
|
+
next unless synced # Only process inclusions
|
|
134
|
+
|
|
135
|
+
# Check if source is a glob pattern
|
|
136
|
+
if source_pattern.include?('*')
|
|
137
|
+
# Glob pattern; copy matching files
|
|
138
|
+
source_glob = File.join(GEM_ROOT, source_pattern)
|
|
139
|
+
matching_files = Dir.glob(source_glob)
|
|
140
|
+
|
|
141
|
+
if matching_files.empty?
|
|
142
|
+
puts " โ ๏ธ No files matched pattern: #{source_pattern}"
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
matching_files.each do |source_file|
|
|
147
|
+
next unless File.file?(source_file)
|
|
148
|
+
next if sources_checked.include?(source_file)
|
|
149
|
+
|
|
150
|
+
# Skip if explicitly excluded
|
|
151
|
+
if excluded_files.include?(source_file)
|
|
152
|
+
puts " โญ๏ธ Skipped #{File.basename(source_file)} (explicitly excluded)"
|
|
153
|
+
next
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Determine target file path
|
|
157
|
+
filename = File.basename(source_file)
|
|
158
|
+
target_file = File.join(target_path, filename)
|
|
159
|
+
|
|
160
|
+
sources_checked << source_file
|
|
161
|
+
result = copy_doc_file(source_file, target_file, synced: synced, force: force)
|
|
162
|
+
synced_count += 1 if result == :copied
|
|
163
|
+
skipped_count += 1 if result == :skipped
|
|
164
|
+
end
|
|
165
|
+
else # Single file
|
|
166
|
+
source_file = File.join(GEM_ROOT, source_pattern)
|
|
167
|
+
|
|
168
|
+
unless File.exist?(source_file)
|
|
169
|
+
puts " โ Source file not found: #{source_file}"
|
|
170
|
+
puts " Run 'bundle exec rake gemdo:gen_agent_docs' in DocOps/lab to generate docs"
|
|
171
|
+
next
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
next if sources_checked.include?(source_file)
|
|
175
|
+
|
|
176
|
+
# Skip if explicitly excluded (shouldn't happen for inclusions, but safety check)
|
|
177
|
+
if excluded_files.include?(source_file)
|
|
178
|
+
puts " โญ๏ธ Skipped #{File.basename(source_file)} (explicitly excluded)"
|
|
179
|
+
next
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
sources_checked << source_file
|
|
183
|
+
|
|
184
|
+
result = copy_doc_file(source_file, target_path, synced: synced, force: force)
|
|
185
|
+
synced_count += 1 if result == :copied
|
|
186
|
+
skipped_count += 1 if result == :skipped
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
# rubocop:enable Style/CombinableLoops
|
|
190
|
+
|
|
191
|
+
puts "โ
Synced #{synced_count} doc files" if synced_count.positive?
|
|
192
|
+
puts "โน๏ธ Skipped #{skipped_count} existing files (use --force to overwrite)" if skipped_count.positive?
|
|
193
|
+
|
|
194
|
+
synced_count.positive? || skipped_count.positive?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def sync_scripts _context
|
|
198
|
+
ScriptManager.sync_scripts
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def sync_config_files context, tool_filter: :all, offline: false
|
|
202
|
+
# Validate tool filter parameter
|
|
203
|
+
unless tool_filter == :all || tool_filter.is_a?(String) || tool_filter.is_a?(Symbol)
|
|
204
|
+
puts "โ Invalid tool filter: #{tool_filter}. Must be :all, tool name string, or tool symbol"
|
|
205
|
+
return false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
puts offline ? '๐ Syncing configs (offline mode)...' : '๐ Syncing configuration files...'
|
|
209
|
+
|
|
210
|
+
# Check for docopslab-dev.yml manifest
|
|
211
|
+
unless File.exist?(MANIFEST_PATH)
|
|
212
|
+
puts "โน๏ธ No #{MANIFEST_PATH} found"
|
|
213
|
+
puts "โ Legacy sync mode not implemented. Run 'rake labdev:init' to create manifest."
|
|
214
|
+
return false
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Parse manifest
|
|
218
|
+
begin
|
|
219
|
+
manifest = YAML.load_file(MANIFEST_PATH)
|
|
220
|
+
rescue StandardError => e
|
|
221
|
+
puts "โ Failed to parse #{MANIFEST_PATH}: #{e.message}"
|
|
222
|
+
return false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
unless Dir.exist?(CONFIG_PACKS_SOURCE_DIR)
|
|
226
|
+
puts 'โ No assets/config-packs directory found in gem'
|
|
227
|
+
return false
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Get available tools from manifest for validation
|
|
231
|
+
available_tools = manifest['tools']&.map { |t| t['tool'] } || []
|
|
232
|
+
|
|
233
|
+
# Validate specific tool filter
|
|
234
|
+
if tool_filter != :all
|
|
235
|
+
tool_filter_str = tool_filter.to_s
|
|
236
|
+
unless available_tools.include?(tool_filter_str)
|
|
237
|
+
puts "โ Tool '#{tool_filter_str}' not found in manifest. Available tools: #{available_tools.join(', ')}"
|
|
238
|
+
return false
|
|
239
|
+
end
|
|
240
|
+
puts "๐ฆ Filtering to tool: #{tool_filter_str}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
synced_count = 0
|
|
244
|
+
expected_targets = Set.new
|
|
245
|
+
|
|
246
|
+
# Process each tool from manifest
|
|
247
|
+
manifest['tools']&.each do |tool_entry|
|
|
248
|
+
tool_name = tool_entry['tool']
|
|
249
|
+
enabled = tool_entry.fetch('enabled', true)
|
|
250
|
+
|
|
251
|
+
# Skip if filtering to specific tool and this isn't it
|
|
252
|
+
next if tool_filter != :all && tool_name != tool_filter.to_s
|
|
253
|
+
|
|
254
|
+
unless enabled
|
|
255
|
+
puts "โญ๏ธ Skipping #{tool_name} (disabled in manifest)"
|
|
256
|
+
next
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
puts "๐ฆ Processing #{tool_name} config pack..."
|
|
260
|
+
|
|
261
|
+
# Process each file mapping
|
|
262
|
+
tool_entry['files']&.each do |file_config|
|
|
263
|
+
source_rel = file_config['source']
|
|
264
|
+
target_path = file_config['target']
|
|
265
|
+
synced = file_config.fetch('synced', true)
|
|
266
|
+
file_enabled = file_config.fetch('enabled', true)
|
|
267
|
+
|
|
268
|
+
unless file_enabled
|
|
269
|
+
puts " โญ๏ธ Skipping #{source_rel} (disabled)"
|
|
270
|
+
next
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
source_path = File.join(CONFIG_PACKS_SOURCE_DIR, source_rel)
|
|
274
|
+
|
|
275
|
+
unless File.exist?(source_path)
|
|
276
|
+
puts " โ Source not found: #{source_rel}"
|
|
277
|
+
next
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Add to expected targets for cleanup, regardless of synced status
|
|
281
|
+
expected_targets.add(target_path)
|
|
282
|
+
|
|
283
|
+
# Handle directory syncing (source ends with /)
|
|
284
|
+
if source_rel.end_with?('/')
|
|
285
|
+
sync_result = sync_directory(
|
|
286
|
+
source_path, target_path, synced: synced, expected_targets: expected_targets)
|
|
287
|
+
synced_count += sync_result
|
|
288
|
+
else
|
|
289
|
+
# Create destination directory if needed
|
|
290
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
|
291
|
+
|
|
292
|
+
# Determine if we should copy the file
|
|
293
|
+
file_existed_before_copy = File.exist?(target_path)
|
|
294
|
+
|
|
295
|
+
should_copy = if synced # If synced: true, copy if missing or different
|
|
296
|
+
!file_existed_before_copy || File.read(source_path) != File.read(target_path)
|
|
297
|
+
else # If synced: false, copy only if missing
|
|
298
|
+
!file_existed_before_copy
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
if should_copy
|
|
302
|
+
FileUtils.cp(source_path, target_path)
|
|
303
|
+
message = if synced
|
|
304
|
+
"๐ Synced: #{target_path} (auto-sync)"
|
|
305
|
+
elsif !file_existed_before_copy
|
|
306
|
+
"โ
Created: #{target_path}"
|
|
307
|
+
else
|
|
308
|
+
"๐ Synced: #{target_path}" # Fallback
|
|
309
|
+
end
|
|
310
|
+
puts " #{message}"
|
|
311
|
+
synced_count += 1
|
|
312
|
+
else
|
|
313
|
+
puts " โ
Up to date: #{target_path}"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
cleanup_count = cleanup_obsolete_files(context, expected_targets)
|
|
320
|
+
|
|
321
|
+
# Generate runtime configs after syncing base configs
|
|
322
|
+
puts '๐ง Generating runtime configs...'
|
|
323
|
+
generated_count = 0
|
|
324
|
+
generated_count += 1 if context.generate_vale_config
|
|
325
|
+
generated_count += 1 if context.generate_htmlproofer_config
|
|
326
|
+
|
|
327
|
+
puts ' โ
All runtime configs up to date' if generated_count.zero?
|
|
328
|
+
|
|
329
|
+
total_changes = synced_count + cleanup_count + generated_count
|
|
330
|
+
if total_changes.positive?
|
|
331
|
+
puts "โ
Config sync complete; #{synced_count} files updated, " \
|
|
332
|
+
"#{cleanup_count} files cleaned up, #{generated_count} configs generated"
|
|
333
|
+
else
|
|
334
|
+
puts 'โ
All configs up to date'
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
true
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def sync_directory source_dir, target_dir, synced: false, expected_targets: nil
|
|
341
|
+
synced_count = 0
|
|
342
|
+
|
|
343
|
+
FileUtils.mkdir_p(target_dir)
|
|
344
|
+
|
|
345
|
+
# Sync all files in the source directory
|
|
346
|
+
Dir.glob("#{source_dir}/**/*", File::FNM_DOTMATCH).each do |source_file|
|
|
347
|
+
next if File.directory?(source_file)
|
|
348
|
+
next if File.basename(source_file).start_with?('.') && ['.', '..'].include?(File.basename(source_file))
|
|
349
|
+
|
|
350
|
+
# Calculate relative path within source directory
|
|
351
|
+
rel_path = Pathname.new(source_file).relative_path_from(Pathname.new(source_dir))
|
|
352
|
+
target_file = File.join(target_dir, rel_path)
|
|
353
|
+
|
|
354
|
+
# Track expected files for cleanup detection
|
|
355
|
+
expected_targets&.add(target_file) if synced
|
|
356
|
+
|
|
357
|
+
# Create target subdirectory if needed
|
|
358
|
+
FileUtils.mkdir_p(File.dirname(target_file))
|
|
359
|
+
|
|
360
|
+
# Copy file if it doesn't exist or is different
|
|
361
|
+
if !File.exist?(target_file) || File.read(source_file) != File.read(target_file)
|
|
362
|
+
FileUtils.cp(source_file, target_file)
|
|
363
|
+
puts " ๐ Synced: #{target_file}#{' (auto-sync)' if synced}"
|
|
364
|
+
synced_count += 1
|
|
365
|
+
else
|
|
366
|
+
puts " โ
Up to date: #{target_file}"
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
synced_count
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def cleanup_obsolete_files _context, expected_targets
|
|
374
|
+
cleanup_count = 0
|
|
375
|
+
obsolete_files = []
|
|
376
|
+
# Common vendor paths to check for obsolete files
|
|
377
|
+
vendor_patterns = [
|
|
378
|
+
File.join(CONFIG_VENDOR_DIR, '**', '*')
|
|
379
|
+
]
|
|
380
|
+
vendor_patterns.each do |pattern|
|
|
381
|
+
Dir.glob(pattern).each do |file_path|
|
|
382
|
+
next if File.directory?(file_path)
|
|
383
|
+
next if file_path.include?('/.git/') # Skip git files
|
|
384
|
+
|
|
385
|
+
# Check if this file is expected based on manifest
|
|
386
|
+
obsolete_files << file_path unless expected_targets.include?(file_path)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
return 0 if obsolete_files.empty?
|
|
390
|
+
|
|
391
|
+
puts "\n๐งน Found #{obsolete_files.length} potentially obsolete vendor files:"
|
|
392
|
+
obsolete_files.sort.each do |file|
|
|
393
|
+
puts " ๐ #{file}"
|
|
394
|
+
end
|
|
395
|
+
print "\nClean up these obsolete files? [y/N]: "
|
|
396
|
+
response = $stdin.gets.chomp.downcase
|
|
397
|
+
if %w[y yes].include?(response)
|
|
398
|
+
obsolete_files.each do |file|
|
|
399
|
+
File.delete(file)
|
|
400
|
+
puts " ๐๏ธ Removed: #{file}"
|
|
401
|
+
cleanup_count += 1
|
|
402
|
+
rescue StandardError => e
|
|
403
|
+
puts " โ Failed to remove #{file}: #{e.message}"
|
|
404
|
+
end
|
|
405
|
+
# Clean up empty directories
|
|
406
|
+
vendor_patterns.each do |pattern|
|
|
407
|
+
base_dir = pattern.split('/**').first
|
|
408
|
+
next unless Dir.exist?(base_dir)
|
|
409
|
+
|
|
410
|
+
cleanup_empty_directories(base_dir)
|
|
411
|
+
end
|
|
412
|
+
else
|
|
413
|
+
puts 'โญ๏ธ Skipping cleanup of obsolete files'
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
cleanup_count
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
private
|
|
420
|
+
|
|
421
|
+
def cleanup_empty_directories dir_path
|
|
422
|
+
return unless Dir.exist?(dir_path)
|
|
423
|
+
|
|
424
|
+
# Get all subdirectories, sorted by depth (deepest first)
|
|
425
|
+
subdirs = Dir.glob("#{dir_path}/**/*/").sort_by { |d| -d.count('/') }
|
|
426
|
+
|
|
427
|
+
subdirs.each do |subdir|
|
|
428
|
+
next unless Dir.exist?(subdir)
|
|
429
|
+
next unless Dir.empty?(subdir)
|
|
430
|
+
|
|
431
|
+
begin
|
|
432
|
+
Dir.rmdir(subdir)
|
|
433
|
+
puts " ๐ Removed empty directory: #{subdir}"
|
|
434
|
+
rescue StandardError
|
|
435
|
+
# Ignore errors; directory might not be empty due to hidden files
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def copy_doc_file source_file, target_path, synced:, force:
|
|
441
|
+
# Ensure target directory exists
|
|
442
|
+
target_dir = File.dirname(target_path)
|
|
443
|
+
FileUtils.mkdir_p(target_dir)
|
|
444
|
+
|
|
445
|
+
# Check if target already exists
|
|
446
|
+
if File.exist?(target_path)
|
|
447
|
+
if synced || force
|
|
448
|
+
# Overwrite if synced or force flag
|
|
449
|
+
FileUtils.cp(source_file, target_path)
|
|
450
|
+
puts " ๐ Updated #{target_path}"
|
|
451
|
+
:copied
|
|
452
|
+
else
|
|
453
|
+
# Skip if not synced and no force
|
|
454
|
+
puts " โญ๏ธ Skipped #{target_path} (already exists, synced=false)"
|
|
455
|
+
:skipped
|
|
456
|
+
end
|
|
457
|
+
else
|
|
458
|
+
# Create new file
|
|
459
|
+
FileUtils.cp(source_file, target_path)
|
|
460
|
+
puts " โ Created #{target_path}"
|
|
461
|
+
:copied
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
# rubocop :enable Metrics/ClassLength
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|