docopslab-dev 0.1.0 → 0.3.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +682 -324
  3. data/docopslab-dev.gemspec +3 -4
  4. data/lib/docopslab/dev/cast_ops.rb +199 -0
  5. data/lib/docopslab/dev/config_manager.rb +6 -6
  6. data/lib/docopslab/dev/data_utils.rb +42 -0
  7. data/lib/docopslab/dev/docker_aware.rb +40 -0
  8. data/lib/docopslab/dev/file_utils.rb +18 -7
  9. data/lib/docopslab/dev/git_branch.rb +201 -0
  10. data/lib/docopslab/dev/git_hooks.rb +17 -11
  11. data/lib/docopslab/dev/initializer.rb +34 -11
  12. data/lib/docopslab/dev/library/cache.rb +167 -0
  13. data/lib/docopslab/dev/library/fetch.rb +209 -0
  14. data/lib/docopslab/dev/library.rb +341 -0
  15. data/lib/docopslab/dev/linters.rb +73 -15
  16. data/lib/docopslab/dev/manifest.rb +28 -0
  17. data/lib/docopslab/dev/paths.rb +0 -17
  18. data/lib/docopslab/dev/script_manager.rb +12 -6
  19. data/lib/docopslab/dev/skim.rb +109 -0
  20. data/lib/docopslab/dev/spell_check.rb +2 -2
  21. data/lib/docopslab/dev/sync_ops.rb +94 -33
  22. data/lib/docopslab/dev/tasks.rb +58 -18
  23. data/lib/docopslab/dev/version.rb +1 -1
  24. data/lib/docopslab/dev.rb +77 -36
  25. data/specs/data/default-manifest.yml +23 -5
  26. data/specs/data/library-index.yml +22 -0
  27. data/specs/data/manifest-schema.yaml +142 -4
  28. data/specs/data/tasks-def.yml +122 -10
  29. metadata +28 -73
  30. data/assets/config-packs/actionlint/base.yml +0 -13
  31. data/assets/config-packs/actionlint/project.yml +0 -13
  32. data/assets/config-packs/htmlproofer/base.yml +0 -27
  33. data/assets/config-packs/htmlproofer/project.yml +0 -25
  34. data/assets/config-packs/rubocop/base.yml +0 -130
  35. data/assets/config-packs/rubocop/project.yml +0 -8
  36. data/assets/config-packs/shellcheck/base.shellcheckrc +0 -14
  37. data/assets/config-packs/subtxt/ai-asciidoc-antipatterns.sub.txt +0 -11
  38. data/assets/config-packs/vale/asciidoc/ExplicitSectionIDs.yml +0 -8
  39. data/assets/config-packs/vale/asciidoc/ExtraLineBeforeLevel1.yml +0 -7
  40. data/assets/config-packs/vale/asciidoc/OneSentencePerLine.yml +0 -8
  41. data/assets/config-packs/vale/asciidoc/PreferSourceBlocks.yml +0 -8
  42. data/assets/config-packs/vale/asciidoc/ProperAdmonitions.yml +0 -8
  43. data/assets/config-packs/vale/asciidoc/ProperDLs.yml +0 -7
  44. data/assets/config-packs/vale/asciidoc/UncleanListStart.yml +0 -8
  45. data/assets/config-packs/vale/authoring/ButParagraph.yml +0 -8
  46. data/assets/config-packs/vale/authoring/ExNotEg.yml +0 -8
  47. data/assets/config-packs/vale/authoring/LiteralTerms.yml +0 -20
  48. data/assets/config-packs/vale/authoring/Spelling.yml +0 -679
  49. data/assets/config-packs/vale/base.ini +0 -38
  50. data/assets/config-packs/vale/config/scripts/ExplicitSectionIDs.tengo +0 -56
  51. data/assets/config-packs/vale/config/scripts/ExtraLineBeforeLevel1.tengo +0 -121
  52. data/assets/config-packs/vale/config/scripts/OneSentencePerLine.tengo +0 -53
  53. data/assets/config-packs/vale/project.ini +0 -5
  54. data/assets/hooks/pre-commit +0 -63
  55. data/assets/hooks/pre-push +0 -72
  56. data/assets/scripts/adoc_section_ids.rb +0 -50
  57. data/assets/scripts/build-common.sh +0 -193
  58. data/assets/scripts/build-docker.sh +0 -64
  59. data/assets/scripts/build.sh +0 -56
  60. data/assets/scripts/parse_jekyll_asciidoc_logs.rb +0 -467
  61. data/assets/templates/Gemfile +0 -7
  62. data/assets/templates/Rakefile +0 -3
  63. data/assets/templates/gitignore +0 -69
  64. data/assets/templates/jekyll-asciidoc-fix.prompt.yml +0 -17
  65. data/assets/templates/spellcheck.prompt.yml +0 -16
  66. data/docs/agent/AGENTS.md +0 -229
  67. data/docs/agent/index.md +0 -80
  68. data/docs/agent/missions/conduct-release.md +0 -224
  69. data/docs/agent/missions/setup-new-project.md +0 -250
  70. data/docs/agent/roles/devops-release-engineer.md +0 -152
  71. data/docs/agent/roles/docops-engineer.md +0 -193
  72. data/docs/agent/roles/planner-architect.md +0 -74
  73. data/docs/agent/roles/product-engineer.md +0 -153
  74. data/docs/agent/roles/product-manager.md +0 -130
  75. data/docs/agent/roles/project-manager.md +0 -139
  76. data/docs/agent/roles/qa-testing-engineer.md +0 -115
  77. data/docs/agent/roles/tech-docs-manager.md +0 -143
  78. data/docs/agent/roles/tech-writer.md +0 -163
  79. data/docs/agent/skills/asciidoc.md +0 -609
  80. data/docs/agent/skills/code-commenting.md +0 -347
  81. data/docs/agent/skills/fix-broken-links.md +0 -309
  82. data/docs/agent/skills/fix-jekyll-asciidoc-build-errors.md +0 -23
  83. data/docs/agent/skills/fix-spelling-issues.md +0 -13
  84. data/docs/agent/skills/git.md +0 -170
  85. data/docs/agent/skills/github-issues.md +0 -135
  86. data/docs/agent/skills/product-release-rollback-and-patching.md +0 -71
  87. data/docs/agent/skills/rake-cli-dev.md +0 -57
  88. data/docs/agent/skills/readme-driven-dev.md +0 -13
  89. data/docs/agent/skills/release-history.md +0 -29
  90. data/docs/agent/skills/ruby.md +0 -192
  91. data/docs/agent/skills/schemagraphy-sgyml.md +0 -18
  92. data/docs/agent/skills/tests-running.md +0 -25
  93. data/docs/agent/skills/tests-writing.md +0 -45
  94. data/docs/agent/skills/write-the-docs.md +0 -54
  95. data/docs/agent/topics/common-project-paths.md +0 -117
  96. data/docs/agent/topics/dev-tooling-usage.md +0 -202
  97. data/docs/agent/topics/devops-ci-cd.md +0 -55
  98. data/docs/agent/topics/product-docs-deployment.md +0 -25
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+ require 'shellwords'
6
+ require 'tmpdir'
7
+ require_relative 'manifest'
8
+ require_relative 'library/cache'
9
+ require_relative 'library/fetch'
10
+
11
+ module DocOpsLab
12
+ module Dev
13
+ # Remote library fetch, cache, and resolution.
14
+ # Manages a host-wide asset cache at ~/.cache/docopslab/dev/library/ (native)
15
+ # or ./.docopslab-cache/ (Docker without cache mount).
16
+ #
17
+ # Docker Cache Strategy:
18
+ # - If running in Docker with host cache mounted (-v ~/.cache/docopslab:...):
19
+ # uses host cache path (DockerAware.cache_mount_accessible? = true)
20
+ # - If running in Docker without cache mount: uses workspace-relative cache
21
+ # (./.docopslab-cache/) which persists with the project
22
+ # - If running natively: uses ~/.cache/docopslab/ via XDG_CACHE_HOME
23
+ #
24
+ # Callers should use this module directly: Library.fetch!, Library.resolve(path), etc.
25
+ module Library
26
+ class << self
27
+ def fetch! config=nil
28
+ config ||= library_config_from_manifest
29
+ with_cache_root(config) { Fetch.call(config) }
30
+ end
31
+
32
+ # Fetch the library if the cache is absent or stale, then sync all
33
+ # manifest-driven content (docs, config files, templates, scripts) to
34
+ # local paths. This is the main entry point for `labdev:sync:library`.
35
+ def sync! force: false
36
+ config = library_config_from_manifest
37
+ with_cache_root(config) do
38
+ if local_path_active?(config)
39
+ puts "📚 Using local library at #{File.expand_path(config['local_path'])}"
40
+ elsif !force && Cache.available? && sha_current?(config)
41
+ puts "\u2705 Library cache is up to date (#{Cache.stored_head&.slice(0, 8)})"
42
+ else
43
+ puts Cache.available? ? '🔄 Library has updates; refreshing...' : '📥 Library cache not found; fetching...'
44
+ ok = Fetch.call(config)
45
+ unless ok
46
+ warn '⚠️ Library fetch failed. Using existing cache if available.'
47
+ raise 'Library unavailable.' unless available?
48
+ end
49
+ end
50
+
51
+ context = Dev
52
+ SyncOps.sync_config_files(context)
53
+ SyncOps.sync_docs(context, force: force)
54
+ SyncOps.sync_templates(context, force: force)
55
+ SyncOps.sync_scripts(context)
56
+ end
57
+ end
58
+
59
+ # Copy a local library directory into the host cache and sync content to
60
+ # manifest-configured paths. Intended for development workflows where
61
+ # assets live in the +lab+ monorepo (.library/ or library/current/) and
62
+ # have not yet been published to the remote branch.
63
+ #
64
+ # Resolution order for +source_path+:
65
+ # 1. Explicit argument (task arg or direct call)
66
+ # 2. manifest +library.local_path+ (resolved relative to cwd)
67
+ # 3. .library/ in the current working directory
68
+ # 4. ../lab/.library/ relative to cwd (downstream-project fallback)
69
+ #
70
+ # A minimal catalog.json is generated into a staging copy if the source
71
+ # directory does not already contain one.
72
+ def stage! source_path: nil
73
+ resolved = resolve_stage_source(source_path)
74
+ unless resolved
75
+ warn '⚠️ No local library path found. ' \
76
+ "Pass a path, or set library.local_path in #{Dev::MANIFEST_PATH}."
77
+ return false
78
+ end
79
+
80
+ puts "📦 Staging local library from #{resolved}..."
81
+
82
+ Dir.mktmpdir('docopslab-stage-') do |tmpdir|
83
+ dest = File.join(tmpdir, 'stage')
84
+ FileUtils.cp_r(resolved, dest)
85
+ ensure_catalog!(dest)
86
+ Cache.write!(dest)
87
+ end
88
+
89
+ puts "✅ Local library staged to #{Cache.current_path}"
90
+
91
+ context = Dev
92
+ SyncOps.sync_config_files(context)
93
+ SyncOps.sync_docs(context, force: true)
94
+ SyncOps.sync_templates(context, force: true)
95
+ SyncOps.sync_scripts(context)
96
+ true
97
+ rescue StandardError => e
98
+ warn "⚠️ Stage failed: #{e.message}"
99
+ false
100
+ end
101
+
102
+ def cached_path
103
+ Cache.current_path
104
+ end
105
+
106
+ # Returns the effective library root directory (nil if unavailable).
107
+ # Does not auto-fetch; call ensure_available! first if needed.
108
+ # Resolution order mirrors resolve():
109
+ # 1. XDG host cache 2. local_path from manifest
110
+ def root
111
+ return Cache.current_path if Cache.available?
112
+
113
+ lp = Dev.load_manifest&.dig('library', 'local_path')
114
+ return File.expand_path(lp) if lp && File.exist?(File.join(lp, 'catalog.json'))
115
+
116
+ nil
117
+ end
118
+
119
+ # Returns the absolute path to a cached file, or nil if absent.
120
+ # Resolution order:
121
+ # 1. XDG host cache (~/.cache/docopslab/dev/library/current/)
122
+ # 2. local_path from manifest (dev/monorepo fallback, e.g. .library/)
123
+ def resolve relative_path
124
+ if Cache.available?
125
+ full_path = File.join(Cache.current_path, relative_path)
126
+ return full_path if File.exist?(full_path)
127
+ end
128
+
129
+ # local_path fallback for monorepo dev and offline use
130
+ lp = Dev.load_manifest&.dig('library', 'local_path')
131
+ if lp
132
+ local_full = File.expand_path(File.join(lp, relative_path))
133
+ return local_full if File.exist?(local_full)
134
+ end
135
+
136
+ nil
137
+ end
138
+
139
+ # True if a library is available via cache or local_path fallback.
140
+ def available?
141
+ return true if Cache.available?
142
+
143
+ lp = Dev.load_manifest&.dig('library', 'local_path')
144
+ !!(lp && File.exist?(File.join(lp, 'catalog.json')))
145
+ end
146
+
147
+ # Ensure the library is available, auto-fetching if necessary.
148
+ # Returns true if available after the call; raises on failure.
149
+ def ensure_available!
150
+ return true if available?
151
+
152
+ puts '📥 Library cache not found; fetching now...'
153
+ ok = fetch!
154
+ return true if ok && available?
155
+
156
+ lp = Dev.load_manifest&.dig('library', 'local_path')
157
+ if lp && Dir.exist?(lp)
158
+ warn "⚠️ Remote fetch failed; using local_path fallback: #{lp}"
159
+ return true
160
+ end
161
+
162
+ raise 'Library unavailable. Run `bundle exec rake labdev:sync:library` to fetch it.'
163
+ end
164
+
165
+ def status
166
+ Cache.status
167
+ end
168
+
169
+ def rollback!
170
+ if Cache.rollback!
171
+ puts "✅ Library rolled back to previous snapshot at #{Cache.current_path}"
172
+ true
173
+ else
174
+ warn '⚠️ No previous library snapshot available for rollback.'
175
+ false
176
+ end
177
+ end
178
+
179
+ def print_status
180
+ s = status
181
+ if s[:available]
182
+ puts "📚 Library cache: #{s[:cache_path]}"
183
+ puts " Version : #{s[:version] || '(unknown)'}"
184
+ puts " Ref : #{s[:ref] || '(unknown)'}"
185
+ puts " Generated : #{s[:generated_at] || '(unknown)'}"
186
+ puts " Previous : #{s[:has_previous] ? 'yes' : 'none'}"
187
+ else
188
+ puts "⚠️ No library cache found at #{s[:cache_path]}"
189
+ lp = Dev.load_manifest&.dig('library', 'local_path')
190
+ if lp && File.exist?(File.join(lp, 'catalog.json'))
191
+ puts " Local path : #{File.expand_path(lp)} (active fallback)"
192
+ else
193
+ puts ' Run `bundle exec rake labdev:sync:library` to fetch.'
194
+ end
195
+ end
196
+ end
197
+
198
+ # Compare manifest catalog entries against the cached library files
199
+ # Falls back to an on-repo local path if provided in the manifest
200
+ def print_catalog_comparison manifest = nil
201
+ manifest ||= Dev.load_manifest
202
+ lib_cfg = manifest && manifest['library']
203
+
204
+ if lib_cfg.nil? || lib_cfg.empty?
205
+ puts "ℹ️ No `library` block found in #{Dev.manifest_path} (or it's empty)."
206
+ return
207
+ end
208
+
209
+ catalog = lib_cfg.dig('catalog', 'overrides') || lib_cfg['catalog'] || lib_cfg['catalog_overrides']
210
+
211
+ unless catalog && !catalog.empty?
212
+ puts 'ℹ️ No catalog overrides found in manifest.library.catalog; nothing to compare.'
213
+ return
214
+ end
215
+
216
+ puts '🔎 Comparing manifest catalog entries to cached library files...'
217
+
218
+ entries = []
219
+ case catalog
220
+ when Array
221
+ entries = catalog
222
+ when Hash
223
+ catalog.each do |k, v|
224
+ entries << if v.is_a?(String)
225
+ v
226
+ elsif v.is_a?(Hash) && v['path']
227
+ v['path']
228
+ else
229
+ k
230
+ end
231
+ end
232
+ else
233
+ puts "⚠️ Unrecognized catalog format: #{catalog.class}. Skipping detailed compare."
234
+ return
235
+ end
236
+
237
+ missing = []
238
+ present = []
239
+
240
+ entries.each do |rel_path|
241
+ rel = rel_path.to_s.sub(%r{^/}, '')
242
+ resolved = resolve(rel)
243
+
244
+ # Fallback to on-repo local path if provided
245
+ if resolved.nil? && lib_cfg['local_path']
246
+ repo_local = File.join(Dir.pwd, lib_cfg['local_path'].to_s, rel)
247
+ resolved = File.exist?(repo_local) ? repo_local : nil
248
+ end
249
+
250
+ if resolved
251
+ present << { path: rel, full: resolved }
252
+ else
253
+ missing << rel
254
+ end
255
+ end
256
+
257
+ if present.any?
258
+ puts "✅ Found #{present.size} catalog entries in the cache or local path:"
259
+ present.each do |p|
260
+ puts " - #{p[:path]} -> #{p[:full]}"
261
+ end
262
+ end
263
+
264
+ if missing.any?
265
+ puts "❌ Missing #{missing.size} catalog entries in the cache/local path:"
266
+ missing.each do |m|
267
+ puts " - #{m}"
268
+ end
269
+ else
270
+ puts '✅ All catalog entries present in cache/local path.'
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ def sha_current? config
277
+ remote = Fetch.remote_head(config)
278
+ return Cache.fresh? unless remote # network unavailable; fall back to TTL
279
+
280
+ Cache.stored_head == remote
281
+ end
282
+
283
+ # True when local_path is configured and its catalog is present on disk.
284
+ # When active, sync! uses the local directory directly and skips the
285
+ # remote SHA check; the caller (library maintainer) manages it locally.
286
+ def local_path_active? config
287
+ lp = config['local_path']
288
+ lp && File.exist?(File.join(File.expand_path(lp), 'catalog.json'))
289
+ end
290
+
291
+ def with_cache_root(config, &)
292
+ cr = config.dig('sync', 'cache_root')
293
+ # Auto-derive from local_path when no explicit cache_root is set.
294
+ # local_path points to the 'current' snapshot dir, so its parent is
295
+ # the cache root (mirrors Cache::XDG_CACHE_SUBPATH layout).
296
+ cr = File.join(File.expand_path(config['local_path']), '..') if cr.nil? && local_path_active?(config)
297
+
298
+ # In Docker without access to host cache, use workspace-local cache
299
+ cr = DockerAware.workspace_cache_path if cr.nil? && DockerAware.docker_without_cache?
300
+
301
+ Cache.with_root_override(cr ? File.expand_path(cr) : nil, &)
302
+ end
303
+
304
+ def library_config_from_manifest
305
+ Dev.load_manifest&.dig('library') || {}
306
+ end
307
+
308
+ # Resolve the source directory for stage! using the priority chain:
309
+ # explicit arg → manifest local_path → .library/ in cwd → ../lab/.library/
310
+ def resolve_stage_source explicit_path
311
+ candidates = []
312
+ candidates << File.expand_path(explicit_path) if explicit_path
313
+
314
+ lp = library_config_from_manifest['local_path']
315
+ candidates << File.expand_path(lp) if lp
316
+
317
+ candidates << File.join(Dir.pwd, '.library')
318
+ candidates << File.expand_path(File.join(Dir.pwd, '..', 'lab', '.library'))
319
+
320
+ candidates.find { |p| Dir.exist?(p) }
321
+ end
322
+
323
+ # Write a minimal catalog.json into +dir+ if one is not already present.
324
+ def ensure_catalog! dir
325
+ catalog_file = File.join(dir, 'catalog.json')
326
+ return if File.exist?(catalog_file)
327
+
328
+ files = Dir.glob("#{dir}/**/*").reject { |f| File.directory?(f) }
329
+ .map { |f| f.delete_prefix("#{dir}/") }
330
+ catalog = {
331
+ 'library_version' => 'local',
332
+ 'library_ref' => 'local-stage',
333
+ 'generated_at' => Time.now.utc.iso8601,
334
+ 'files' => files
335
+ }
336
+ File.write(catalog_file, JSON.generate(catalog))
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end
@@ -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}"
@@ -437,10 +488,17 @@ module DocOpsLab
437
488
  def check_shebang file_path
438
489
  return false unless File.exist?(file_path)
439
490
 
440
- first_line = File.open(file_path, &:readline).strip
491
+ lines = File.readlines(file_path).map(&:strip)
492
+ return false if lines.empty?
493
+
494
+ first_line = lines[0]
495
+ second_line = lines[1]
496
+
497
+ # If second line has shellcheck directive for sh, require sh shebang
498
+ return first_line == '#!/bin/sh' if second_line == '# shellcheck shell=sh'
499
+
500
+ # Otherwise, require bash shebang
441
501
  first_line == '#!/usr/bin/env bash'
442
- rescue EOFError
443
- false
444
502
  rescue StandardError => e
445
503
  puts "⚠️ Error checking shebang for #{file_path}: #{e.message}"
446
504
  false
@@ -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)