docopslab-dev 0.1.0 → 0.2.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +645 -318
  3. data/docopslab-dev.gemspec +2 -3
  4. data/docs/agent/index.md +4 -8
  5. data/docs/agent/misc/bash-styles.md +470 -0
  6. data/docs/agent/missions/conduct-release.md +161 -87
  7. data/docs/agent/missions/setup-new-project.md +228 -134
  8. data/docs/agent/roles/devops-release-engineer.md +60 -17
  9. data/docs/agent/roles/docops-engineer.md +84 -20
  10. data/docs/agent/roles/planner-architect.md +22 -0
  11. data/docs/agent/roles/product-engineer.md +63 -15
  12. data/docs/agent/roles/product-manager.md +57 -24
  13. data/docs/agent/roles/project-manager.md +48 -12
  14. data/docs/agent/roles/qa-testing-engineer.md +48 -14
  15. data/docs/agent/roles/tech-docs-manager.md +63 -17
  16. data/docs/agent/roles/tech-writer.md +68 -14
  17. data/docs/agent/skills/asciidoc.md +65 -238
  18. data/docs/agent/skills/bash-cli-dev.md +135 -0
  19. data/docs/agent/skills/code-commenting.md +143 -106
  20. data/docs/agent/skills/fix-broken-links.md +145 -100
  21. data/docs/agent/skills/fix-jekyll-asciidoc-build-errors.md +1 -10
  22. data/docs/agent/skills/fix-spelling-issues.md +0 -3
  23. data/docs/agent/skills/git.md +69 -34
  24. data/docs/agent/skills/github-issues.md +110 -71
  25. data/docs/agent/skills/rake-cli-dev.md +1 -1
  26. data/docs/agent/skills/readme-driven-dev.md +1 -0
  27. data/docs/agent/skills/release-history.md +1 -7
  28. data/docs/agent/skills/ruby.md +18 -7
  29. data/docs/agent/skills/schemagraphy-sgyml.md +3 -0
  30. data/docs/agent/skills/tests-running.md +22 -14
  31. data/docs/agent/skills/tests-writing.md +51 -28
  32. data/docs/agent/skills/write-the-docs.md +71 -9
  33. data/docs/agent/topics/common-project-paths.md +122 -70
  34. data/docs/agent/topics/dev-tooling-usage.md +70 -77
  35. data/docs/agent/topics/devops-ci-cd.md +3 -1
  36. data/docs/agent/topics/product-docs-deployment.md +18 -12
  37. data/docs/library-readme.adoc +39 -0
  38. data/lib/docopslab/dev/cast_ops.rb +199 -0
  39. data/lib/docopslab/dev/config_manager.rb +6 -6
  40. data/lib/docopslab/dev/data_utils.rb +42 -0
  41. data/lib/docopslab/dev/file_utils.rb +18 -7
  42. data/lib/docopslab/dev/git_branch.rb +201 -0
  43. data/lib/docopslab/dev/git_hooks.rb +17 -11
  44. data/lib/docopslab/dev/initializer.rb +13 -4
  45. data/lib/docopslab/dev/library/cache.rb +167 -0
  46. data/lib/docopslab/dev/library/fetch.rb +209 -0
  47. data/lib/docopslab/dev/library.rb +328 -0
  48. data/lib/docopslab/dev/linters.rb +63 -12
  49. data/lib/docopslab/dev/manifest.rb +28 -0
  50. data/lib/docopslab/dev/paths.rb +0 -17
  51. data/lib/docopslab/dev/script_manager.rb +12 -6
  52. data/lib/docopslab/dev/skim.rb +109 -0
  53. data/lib/docopslab/dev/spell_check.rb +2 -2
  54. data/lib/docopslab/dev/sync_ops.rb +94 -33
  55. data/lib/docopslab/dev/tasks.rb +58 -18
  56. data/lib/docopslab/dev/version.rb +1 -1
  57. data/lib/docopslab/dev.rb +75 -35
  58. data/specs/data/default-manifest.yml +15 -5
  59. data/specs/data/library-index.yml +22 -0
  60. data/specs/data/manifest-schema.yaml +142 -4
  61. data/specs/data/tasks-def.yml +122 -10
  62. metadata +28 -39
  63. data/assets/config-packs/actionlint/base.yml +0 -13
  64. data/assets/config-packs/actionlint/project.yml +0 -13
  65. data/assets/config-packs/htmlproofer/base.yml +0 -27
  66. data/assets/config-packs/htmlproofer/project.yml +0 -25
  67. data/assets/config-packs/rubocop/base.yml +0 -130
  68. data/assets/config-packs/rubocop/project.yml +0 -8
  69. data/assets/config-packs/shellcheck/base.shellcheckrc +0 -14
  70. data/assets/config-packs/subtxt/ai-asciidoc-antipatterns.sub.txt +0 -11
  71. data/assets/config-packs/vale/asciidoc/ExplicitSectionIDs.yml +0 -8
  72. data/assets/config-packs/vale/asciidoc/ExtraLineBeforeLevel1.yml +0 -7
  73. data/assets/config-packs/vale/asciidoc/OneSentencePerLine.yml +0 -8
  74. data/assets/config-packs/vale/asciidoc/PreferSourceBlocks.yml +0 -8
  75. data/assets/config-packs/vale/asciidoc/ProperAdmonitions.yml +0 -8
  76. data/assets/config-packs/vale/asciidoc/ProperDLs.yml +0 -7
  77. data/assets/config-packs/vale/asciidoc/UncleanListStart.yml +0 -8
  78. data/assets/config-packs/vale/authoring/ButParagraph.yml +0 -8
  79. data/assets/config-packs/vale/authoring/ExNotEg.yml +0 -8
  80. data/assets/config-packs/vale/authoring/LiteralTerms.yml +0 -20
  81. data/assets/config-packs/vale/authoring/Spelling.yml +0 -679
  82. data/assets/config-packs/vale/base.ini +0 -38
  83. data/assets/config-packs/vale/config/scripts/ExplicitSectionIDs.tengo +0 -56
  84. data/assets/config-packs/vale/config/scripts/ExtraLineBeforeLevel1.tengo +0 -121
  85. data/assets/config-packs/vale/config/scripts/OneSentencePerLine.tengo +0 -53
  86. data/assets/config-packs/vale/project.ini +0 -5
  87. data/assets/hooks/pre-commit +0 -63
  88. data/assets/hooks/pre-push +0 -72
  89. data/assets/scripts/adoc_section_ids.rb +0 -50
  90. data/assets/scripts/build-common.sh +0 -193
  91. data/assets/scripts/build-docker.sh +0 -64
  92. data/assets/scripts/build.sh +0 -56
  93. data/assets/scripts/parse_jekyll_asciidoc_logs.rb +0 -467
  94. data/assets/templates/Gemfile +0 -7
  95. data/assets/templates/Rakefile +0 -3
  96. data/assets/templates/gitignore +0 -69
  97. data/assets/templates/jekyll-asciidoc-fix.prompt.yml +0 -17
  98. data/assets/templates/spellcheck.prompt.yml +0 -16
  99. data/docs/agent/AGENTS.md +0 -229
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'open3'
4
+ require 'pathname'
5
+ require 'sourcerer/util/pathifier'
4
6
 
5
7
  module DocOpsLab
6
8
  module Dev
@@ -9,9 +11,9 @@ module DocOpsLab
9
11
  def run_rubocop context, file_path=nil, opts_string=''
10
12
  context.generate_rubocop_config if context.respond_to?(:generate_rubocop_config)
11
13
 
12
- rubocop_config_file = CONFIG_PATHS[:rubocop]
14
+ rubocop_config_file = Paths::CONFIG_FILES[:rubocop]
13
15
  unless File.exist?(rubocop_config_file)
14
- rubocop_config_file = RUBOCOP_CONFIG_PATH # Fallback to vendor config
16
+ rubocop_config_file = File.join(Paths.config_vendor_dir, 'rubocop.yml') # Fallback to vendor config
15
17
  end
16
18
 
17
19
  unless File.exist?(rubocop_config_file)
@@ -57,9 +59,9 @@ module DocOpsLab
57
59
  end
58
60
 
59
61
  def run_rubocop_with_filter _context, filter_name
60
- rubocop_config_file = CONFIG_PATHS[:rubocop]
62
+ rubocop_config_file = Paths::CONFIG_FILES[:rubocop]
61
63
  unless File.exist?(rubocop_config_file)
62
- rubocop_config_file = RUBOCOP_CONFIG_PATH # Fallback to vendor config
64
+ rubocop_config_file = File.join(Paths.config_vendor_dir, 'rubocop.yml') # Fallback to vendor config
63
65
  end
64
66
 
65
67
  unless File.exist?(rubocop_config_file)
@@ -88,7 +90,16 @@ module DocOpsLab
88
90
  puts "🐚 Running ShellCheck on #{running_on}"
89
91
 
90
92
  shell_scripts = if scope == :file
91
- File.exist?(file_path) ? [file_path] : []
93
+ result = Sourcerer::Util::Pathifier.match(file_path)
94
+ if result.type == :file
95
+ result.enum.to_a
96
+ else
97
+ result.enum.select do |f|
98
+ ext = File.extname(f)
99
+ ext.match?(/\.(sh|bash)$/) ||
100
+ (ext.empty? && FileUtilities.shell_shebang?(f))
101
+ end.sort
102
+ end
92
103
  else
93
104
  context.find_shell_scripts
94
105
  end
@@ -109,7 +120,14 @@ module DocOpsLab
109
120
  success = false
110
121
  passed = false
111
122
  end
112
- cmd = "shellcheck --severity=warning #{opts_string} --rcfile=.config/shellcheckrc #{script}".strip
123
+ # Relativize absolute paths so the command works both natively and inside
124
+ # Docker (which mounts $(pwd) as /workspace and sets -w /workspace).
125
+ script_arg = if script.start_with?('/')
126
+ Pathname.new(script).relative_path_from(Pathname.new(Dir.pwd)).to_s
127
+ else
128
+ script
129
+ end
130
+ cmd = "shellcheck --severity=warning #{opts_string} --rcfile=.config/shellcheckrc #{script_arg}".strip
113
131
  shellcheck = context.run_with_fallback('shellcheck', cmd)
114
132
  unless shellcheck
115
133
  success = false
@@ -183,7 +201,7 @@ module DocOpsLab
183
201
  puts ' ✅ Vale config up to date' unless context.generate_vale_config(style_override: style_override)
184
202
 
185
203
  # Use the generated config file
186
- config_file = CONFIG_PATHS[:vale]
204
+ config_file = Paths::CONFIG_FILES[:vale]
187
205
 
188
206
  unless File.exist?(config_file)
189
207
  puts "❌ No Vale config found. Run 'labdev:sync:all' to generate one."
@@ -207,7 +225,33 @@ module DocOpsLab
207
225
 
208
226
  # Find AsciiDoc files to check, excluding vendor/ignored directories
209
227
  if scope == :file
210
- asciidoc_files = [file_path]
228
+ path_result = Sourcerer::Util::Pathifier.match(file_path)
229
+ if path_result.type == :file
230
+ asciidoc_files = [file_path]
231
+ else
232
+ # Directory or glob: enumerate files and apply ext/skip filters from manifest,
233
+ # so that skip patterns (ex: in docopslab-dev.yml) are respected even when a
234
+ # specific directory or glob is passed via the task argument.
235
+ path_config = context.get_path_config('vale')
236
+ skip_paths = path_config[:skip] || []
237
+ exts = path_config[:exts] || []
238
+ pwd = Pathname.pwd
239
+ asciidoc_files = path_result.enum.select do |f|
240
+ normalized = Pathname.new(f).expand_path.relative_path_from(pwd).to_s
241
+ if exts && !exts.empty?
242
+ ext = File.extname(f).delete_prefix('.')
243
+ next false unless exts.include?(ext)
244
+ end
245
+ next false if skip_paths.any? { |p| FileUtilities.file_matches_ignore_pattern?(normalized, p) }
246
+
247
+ true
248
+ end.sort
249
+ if asciidoc_files.empty?
250
+ puts "📄 No AsciiDoc files found to check in #{file_path}"
251
+ return true
252
+ end
253
+ puts "📄 Found #{asciidoc_files.length} AsciiDoc file(s) to check in #{file_path}"
254
+ end
211
255
  else
212
256
  asciidoc_files = context.find_asciidoc_files
213
257
  if asciidoc_files.empty?
@@ -364,18 +408,25 @@ module DocOpsLab
364
408
  results.values.all?
365
409
  end
366
410
 
367
- def run_rubocop_auto_fix _context, path: nil
411
+ def run_rubocop_auto_fix context, path: nil
368
412
  puts '👮 Running RuboCop auto-correction...'
369
413
 
370
- unless File.exist?(RUBOCOP_CONFIG_PATH)
414
+ context.generate_rubocop_config if context.respond_to?(:generate_rubocop_config)
415
+
416
+ rubocop_config_file = Paths::CONFIG_FILES[:rubocop]
417
+ unless File.exist?(rubocop_config_file)
418
+ rubocop_config_file = File.join(Paths.config_vendor_dir, 'rubocop.yml') # Fallback to vendor config
419
+ end
420
+
421
+ unless File.exist?(rubocop_config_file)
371
422
  puts "❌ No RuboCop config found. Run 'labdev:init' to create one."
372
423
  return false
373
424
  end
374
425
 
375
- puts "📄 Using config: #{RUBOCOP_CONFIG_PATH}"
426
+ puts "📄 Using config: #{rubocop_config_file}"
376
427
 
377
428
  # Build command with optional path
378
- cmd = "bundle exec rubocop --config #{RUBOCOP_CONFIG_PATH} --autocorrect-all"
429
+ cmd = "bundle exec rubocop --config #{rubocop_config_file} --autocorrect-all"
379
430
  if path
380
431
  cmd += " #{path}"
381
432
  puts "📄 Targeting path: #{path}"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module DocOpsLab
6
+ module Dev
7
+ # Thin wrapper around the project manifest (.config/docopslab-dev.yml).
8
+ # Shared by tools, sync, and library operations that need manifest data.
9
+ module Manifest
10
+ class << self
11
+ # Load a project manifest YAML file.
12
+ # Returns the parsed hash or nil if the file is absent or unreadable.
13
+ def load path=Dev::MANIFEST_PATH
14
+ return nil unless File.exist?(path)
15
+
16
+ YAML.load_file(path)
17
+ rescue StandardError
18
+ nil
19
+ end
20
+
21
+ # True if data is a non-empty Hash.
22
+ def valid? data
23
+ data.is_a?(Hash) && !data.empty?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -9,23 +9,6 @@ module DocOpsLab
9
9
  File.expand_path('../../..', __dir__)
10
10
  end
11
11
 
12
- # Asset directories in gem
13
- def self.gem_assets
14
- File.join(gem_root, 'assets')
15
- end
16
-
17
- def self.gem_config_packs
18
- File.join(gem_assets, 'config-packs')
19
- end
20
-
21
- def self.gem_hooks
22
- File.join(gem_assets, 'hooks')
23
- end
24
-
25
- def self.gem_scripts
26
- File.join(gem_assets, 'scripts')
27
- end
28
-
29
12
  # Config vendor directory (where config packs are synced to)
30
13
  def self.config_vendor_dir
31
14
  '.config/.vendor/docopslab'
@@ -9,8 +9,13 @@ module DocOpsLab
9
9
  def sync_scripts
10
10
  puts '📜 Syncing common scripts from DocOps Lab...'
11
11
 
12
- unless Dir.exist?(SCRIPTS_SOURCE_DIR)
13
- puts '❌ No scripts directory found in gem'
12
+ unless Library.available?
13
+ puts '❌ Library not available; run `labdev:sync:library` to fetch.'
14
+ return false
15
+ end
16
+ scripts_source = Library.resolve('scripts')
17
+ unless scripts_source && Dir.exist?(scripts_source)
18
+ puts '❌ scripts not found in library; run `labdev:sync:library` to fetch.'
14
19
  return false
15
20
  end
16
21
 
@@ -20,7 +25,7 @@ module DocOpsLab
20
25
 
21
26
  synced_count = 0
22
27
 
23
- Dir.glob("#{SCRIPTS_SOURCE_DIR}/*").each do |script_path|
28
+ Dir.glob("#{scripts_source}/*").each do |script_path|
24
29
  next unless File.file?(script_path)
25
30
 
26
31
  script_name = File.basename(script_path)
@@ -47,14 +52,15 @@ module DocOpsLab
47
52
  end
48
53
 
49
54
  def list_script_templates
50
- unless Dir.exist?(SCRIPTS_SOURCE_DIR)
51
- puts '❌ No scripts directory found in gem'
55
+ scripts_source = Library.resolve('scripts')
56
+ unless scripts_source && Dir.exist?(scripts_source)
57
+ puts '❌ scripts not found in library; run `labdev:sync:library` to fetch.'
52
58
  return false
53
59
  end
54
60
 
55
61
  puts '📜 Available script templates:'
56
62
 
57
- Dir.glob("#{SCRIPTS_SOURCE_DIR}/*").each do |script_path|
63
+ Dir.glob("#{scripts_source}/*").each do |script_path|
58
64
  next unless File.file?(script_path)
59
65
 
60
66
  script_name = File.basename(script_path)
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'asciisourcerer'
5
+ require 'sourcerer/util/pathifier'
6
+
7
+ module DocOpsLab
8
+ module Dev
9
+ # Source skimming via Sourcerer::SourceSkim
10
+ module Skim
11
+ ADOC_EXTS = %w[.adoc .asc .ad .asciidoc].freeze
12
+ MD_EXTS = %w[.md .markdown].freeze
13
+ ALL_EXTS = (ADOC_EXTS + MD_EXTS).freeze
14
+
15
+ class << self
16
+ # Skim all supported file types (AsciiDoc + Markdown), format auto-detected.
17
+ def run path, form: nil, syntax: nil
18
+ run_with_format(path, exts: ALL_EXTS, form: form, syntax: syntax)
19
+ end
20
+
21
+ # Skim AsciiDoc source files only.
22
+ def run_adoc path, form: nil, syntax: nil
23
+ run_with_format(path, exts: ADOC_EXTS, form: form, syntax: syntax, default_forms: [:tree])
24
+ end
25
+
26
+ # Skim Markdown source files only, with optional upstream:local overlay support.
27
+ def run_md path, form: nil, syntax: nil
28
+ run_with_format(path, exts: MD_EXTS, form: form, syntax: syntax, overlay: true)
29
+ end
30
+
31
+ private
32
+
33
+ def run_with_format path, exts:, form: nil, syntax: nil, default_forms: nil, overlay: false
34
+ unless path
35
+ puts '❌ Path is required.'
36
+ puts 'Usage: bundle exec rake labdev:skim[path,form,syntax]'
37
+ return
38
+ end
39
+
40
+ forms = form ? parse_forms(form) : default_forms
41
+ file_paths = overlay ? resolve_overlay_paths(path, exts) : resolve_paths(path, exts)
42
+
43
+ if file_paths.empty?
44
+ ext_desc = exts.size == 1 ? exts.first : exts.join(', ')
45
+ warn "No #{ext_desc} files found for: #{path}"
46
+ return
47
+ end
48
+
49
+ results = {}
50
+ cats = Sourcerer::SourceSkim::DEFAULT_CATEGORIES - [:attributes_custom]
51
+ file_paths.each do |fp|
52
+ skim_opts = { categories: cats }
53
+ skim_opts[:forms] = forms if forms
54
+ results[fp] = Sourcerer::SourceSkim.skim_file(fp, **skim_opts)
55
+ end
56
+ portable = JSON.parse(JSON.generate(results))
57
+
58
+ output_syntax = resolve_syntax(syntax, form)
59
+ puts output_syntax == :json ? JSON.pretty_generate(portable) : portable.to_yaml
60
+ end
61
+
62
+ # Resolve file paths with optional upstream:local overlay.
63
+ #
64
+ # When path_arg contains ':', split into upstream_dir:local_dir. Local files shadow
65
+ # upstream files that share the same relative path; local-only files are appended.
66
+ def resolve_overlay_paths path_arg, exts
67
+ parts = path_arg.split(':', 2).map(&:strip)
68
+ return resolve_paths(parts[0], exts) if parts.size == 1
69
+
70
+ upstream_dir, local_dir = parts
71
+ upstream_map = build_relative_map(upstream_dir, exts)
72
+ local_map = build_relative_map(local_dir, exts)
73
+ upstream_map.merge(local_map).values
74
+ end
75
+
76
+ def build_relative_map dir_path, exts
77
+ dir_path = dir_path.chomp('/')
78
+ return {} unless File.directory?(dir_path)
79
+
80
+ abs_base = File.expand_path(dir_path)
81
+ Sourcerer::Util::Pathifier.match(dir_path).enum
82
+ .select { |p| exts.any? { |e| p.end_with?(e) } }
83
+ .each_with_object({}) do |abs_path, map|
84
+ map[abs_path.sub("#{abs_base}/", '')] = abs_path
85
+ end
86
+ end
87
+
88
+ def parse_forms form
89
+ form.split(',').map { |f| f.strip.to_sym }
90
+ end
91
+
92
+ def resolve_paths path, exts
93
+ result = Sourcerer::Util::Pathifier.match(path)
94
+ result.type == :dir ? result.enum.select { |p| exts.any? { |e| p.end_with?(e) } } : result.enum.to_a
95
+ end
96
+
97
+ def resolve_syntax syntax, form
98
+ return :yaml unless form
99
+
100
+ syntax = 'yaml' if syntax == 'yml'
101
+ puts
102
+ return syntax.to_sym if syntax
103
+
104
+ :json
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -146,8 +146,8 @@ module DocOpsLab
146
146
  require 'erb'
147
147
 
148
148
  unless agent_prompt
149
- template_path = File.join(TEMPLATES_DIR, 'spellcheck.prompt.yml')
150
- agent_prompt = File.read(template_path) if File.exist?(template_path)
149
+ template_path = Library.resolve('templates/spellcheck.prompt.yml')
150
+ agent_prompt = File.read(template_path) if template_path && File.exist?(template_path)
151
151
  end
152
152
 
153
153
  # Create the YAML content with ERB templating for better formatting
@@ -6,36 +6,35 @@ require 'pathname'
6
6
 
7
7
  module DocOpsLab
8
8
  module Dev
9
+ # rubocop:disable Metrics/ModuleLength
9
10
  module SyncOps
10
- # rubocop :disable Metrics/ClassLength
11
+ # rubocop:disable Metrics/ClassLength
11
12
  class << self
12
13
  def install_vale_styles context
13
- return unless File.exist?(CONFIG_PATHS[:vale]) && context.tool_available?('vale')
14
+ return unless File.exist?(Paths::CONFIG_FILES[:vale]) && context.tool_available?('vale')
14
15
 
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")
16
+ puts "📚 Syncing Vale styles using Packages key in #{Paths::CONFIG_FILES[:vale]} (local and remote packages)"
17
+ context.run_with_fallback('vale', "vale --config=#{Paths::CONFIG_FILES[:vale]} sync")
17
18
  end
18
19
 
19
20
  def sync_vale_styles context, local: false
20
21
  puts '📚 Syncing Vale styles...'
21
22
 
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
23
+ unless Library.available?
24
+ puts '❌ Library not available; run `labdev:sync:library` to fetch.'
25
+ return false
26
+ end
27
+ styles_source_root = Library.resolve('config-packs/vale')
28
+ unless styles_source_root
29
+ puts '❌ config-packs/vale not found in library; run `labdev:sync:library` to fetch.'
30
+ return false
31
+ end
29
32
  styles_dest_root = '.config/.vendor/vale/styles'
30
33
  FileUtils.mkdir_p(styles_dest_root)
31
34
 
32
35
  # Get the list of local styles from tools.yml
33
36
  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
37
+ tools_yml_path = Dev.tools_def_path
39
38
 
40
39
  style_paths_array = YAML.load_file(tools_yml_path)
41
40
  .find { |t| t['slug'] == 'vale' }['packaging']['packages']
@@ -77,7 +76,7 @@ module DocOpsLab
77
76
  # If not local-only, also run Vale sync for remote styles
78
77
  unless local
79
78
  puts '📦 Syncing remote Vale packages...'
80
- vale_config = CONFIG_PATHS[:vale]
79
+ vale_config = Paths::CONFIG_FILES[:vale]
81
80
  context.generate_vale_config unless File.exist?(vale_config)
82
81
  context.run_with_fallback('vale', "vale --config=#{vale_config} sync")
83
82
  end
@@ -92,6 +91,12 @@ module DocOpsLab
92
91
  docs_entries = manifest['docs']
93
92
  return false unless docs_entries.is_a?(Array)
94
93
 
94
+ lib_root = Library.root
95
+ unless lib_root
96
+ puts '❌ Library not available; run `labdev:sync:library` to fetch.'
97
+ return false
98
+ end
99
+
95
100
  puts '📚 Syncing documentation files...'
96
101
 
97
102
  synced_count = 0
@@ -107,16 +112,14 @@ module DocOpsLab
107
112
  next unless source_pattern
108
113
  next if synced # Only collect exclusions
109
114
 
110
- # Resolve source file path
115
+ # Resolve source file path relative to library root
111
116
  if source_pattern.include?('*')
112
- # Glob pattern for exclusions
113
- source_glob = File.join(GEM_ROOT, source_pattern)
117
+ source_glob = File.join(lib_root, source_pattern)
114
118
  Dir.glob(source_glob).each do |source_file|
115
119
  excluded_files.add(source_file) if File.file?(source_file)
116
120
  end
117
121
  else
118
- # Single file exclusion
119
- source_file = File.join(GEM_ROOT, source_pattern)
122
+ source_file = File.join(lib_root, source_pattern)
120
123
  excluded_files.add(source_file) if File.exist?(source_file)
121
124
  end
122
125
  end
@@ -134,8 +137,7 @@ module DocOpsLab
134
137
 
135
138
  # Check if source is a glob pattern
136
139
  if source_pattern.include?('*')
137
- # Glob pattern; copy matching files
138
- source_glob = File.join(GEM_ROOT, source_pattern)
140
+ source_glob = File.join(lib_root, source_pattern)
139
141
  matching_files = Dir.glob(source_glob)
140
142
 
141
143
  if matching_files.empty?
@@ -147,13 +149,18 @@ module DocOpsLab
147
149
  next unless File.file?(source_file)
148
150
  next if sources_checked.include?(source_file)
149
151
 
150
- # Skip if explicitly excluded
151
152
  if excluded_files.include?(source_file)
152
153
  puts " ⏭️ Skipped #{File.basename(source_file)} (explicitly excluded)"
153
154
  next
154
155
  end
155
156
 
156
- # Determine target file path
157
+ # Guard: docs/agent/AGENTS.md was relocated to templates/AGENTS.markdown.
158
+ if source_file.end_with?('docs/agent/AGENTS.md')
159
+ puts ' ⏭️ Skipped docs/agent/AGENTS.md \
160
+ (relocated to templates/AGENTS.markdown; remove from manifest)'
161
+ next
162
+ end
163
+
157
164
  filename = File.basename(source_file)
158
165
  target_file = File.join(target_path, filename)
159
166
 
@@ -163,11 +170,11 @@ module DocOpsLab
163
170
  skipped_count += 1 if result == :skipped
164
171
  end
165
172
  else # Single file
166
- source_file = File.join(GEM_ROOT, source_pattern)
173
+ source_file = File.join(lib_root, source_pattern)
167
174
 
168
175
  unless File.exist?(source_file)
169
176
  puts " ❌ Source file not found: #{source_file}"
170
- puts " Run 'bundle exec rake gemdo:gen_agent_docs' in DocOps/lab to generate docs"
177
+ puts " Run 'bundle exec rake labdev:sync:library` then 'labdev:sync:docs' to refresh"
171
178
  next
172
179
  end
173
180
 
@@ -179,6 +186,12 @@ module DocOpsLab
179
186
  next
180
187
  end
181
188
 
189
+ # Guard: docs/agent/AGENTS.md was relocated to templates/AGENTS.markdown.
190
+ if source_file.end_with?('docs/agent/AGENTS.md')
191
+ puts ' ⏭️ Skipped docs/agent/AGENTS.md (relocated to templates/AGENTS.markdown; remove from manifest)'
192
+ next
193
+ end
194
+
182
195
  sources_checked << source_file
183
196
 
184
197
  result = copy_doc_file(source_file, target_path, synced: synced, force: force)
@@ -198,6 +211,52 @@ module DocOpsLab
198
211
  ScriptManager.sync_scripts
199
212
  end
200
213
 
214
+ def sync_templates context, force: false
215
+ manifest = context.load_manifest
216
+ return false unless manifest
217
+
218
+ template_entries = manifest['templates']
219
+ return false unless template_entries.is_a?(Array)
220
+
221
+ lib_root = Library.root
222
+ unless lib_root
223
+ puts '❌ Library not available; run `labdev:sync:library` to fetch.'
224
+ return false
225
+ end
226
+
227
+ puts '📄 Syncing template files...'
228
+
229
+ synced_count = 0
230
+ skipped_count = 0
231
+
232
+ template_entries.each do |entry|
233
+ source_rel = entry['source']
234
+ target_path = entry['target']
235
+ synced = entry.fetch('synced', false)
236
+
237
+ next unless source_rel && target_path
238
+
239
+ source_file = File.join(lib_root, source_rel)
240
+
241
+ unless File.exist?(source_file)
242
+ puts " \u274c Template source not found: #{source_rel}"
243
+ puts ' Run `bundle exec rake labdev:sync:library` to fetch the latest library.'
244
+ next
245
+ end
246
+
247
+ result = copy_doc_file(source_file, target_path, synced: synced, force: force)
248
+ synced_count += 1 if result == :copied
249
+ skipped_count += 1 if result == :skipped
250
+ end
251
+
252
+ puts "\u2705 Synced #{synced_count} template file(s)" if synced_count.positive?
253
+ if skipped_count.positive?
254
+ puts "\u2139\ufe0f Skipped #{skipped_count} existing template(s) (use --force to overwrite)"
255
+ end
256
+
257
+ synced_count.positive? || skipped_count.positive?
258
+ end
259
+
201
260
  def sync_config_files context, tool_filter: :all, offline: false
202
261
  # Validate tool filter parameter
203
262
  unless tool_filter == :all || tool_filter.is_a?(String) || tool_filter.is_a?(Symbol)
@@ -222,8 +281,9 @@ module DocOpsLab
222
281
  return false
223
282
  end
224
283
 
225
- unless Dir.exist?(CONFIG_PACKS_SOURCE_DIR)
226
- puts '❌ No assets/config-packs directory found in gem'
284
+ config_packs_root = Library.resolve('config-packs')
285
+ unless config_packs_root && Dir.exist?(config_packs_root)
286
+ puts '❌ config-packs not found in library; run `labdev:sync:library` to fetch.'
227
287
  return false
228
288
  end
229
289
 
@@ -270,7 +330,7 @@ module DocOpsLab
270
330
  next
271
331
  end
272
332
 
273
- source_path = File.join(CONFIG_PACKS_SOURCE_DIR, source_rel)
333
+ source_path = File.join(config_packs_root, source_rel)
274
334
 
275
335
  unless File.exist?(source_path)
276
336
  puts " ❌ Source not found: #{source_rel}"
@@ -375,7 +435,7 @@ module DocOpsLab
375
435
  obsolete_files = []
376
436
  # Common vendor paths to check for obsolete files
377
437
  vendor_patterns = [
378
- File.join(CONFIG_VENDOR_DIR, '**', '*')
438
+ File.join(Paths.config_vendor_dir, '**', '*')
379
439
  ]
380
440
  vendor_patterns.each do |pattern|
381
441
  Dir.glob(pattern).each do |file_path|
@@ -462,7 +522,8 @@ module DocOpsLab
462
522
  end
463
523
  end
464
524
  end
465
- # rubocop :enable Metrics/ClassLength
525
+ # rubocop:enable Metrics/ClassLength
466
526
  end
527
+ # rubocop:enable Metrics/ModuleLength
467
528
  end
468
529
  end