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.
- checksums.yaml +4 -4
- data/README.adoc +682 -324
- data/docopslab-dev.gemspec +3 -4
- data/lib/docopslab/dev/cast_ops.rb +199 -0
- data/lib/docopslab/dev/config_manager.rb +6 -6
- data/lib/docopslab/dev/data_utils.rb +42 -0
- data/lib/docopslab/dev/docker_aware.rb +40 -0
- data/lib/docopslab/dev/file_utils.rb +18 -7
- data/lib/docopslab/dev/git_branch.rb +201 -0
- data/lib/docopslab/dev/git_hooks.rb +17 -11
- data/lib/docopslab/dev/initializer.rb +34 -11
- data/lib/docopslab/dev/library/cache.rb +167 -0
- data/lib/docopslab/dev/library/fetch.rb +209 -0
- data/lib/docopslab/dev/library.rb +341 -0
- data/lib/docopslab/dev/linters.rb +73 -15
- data/lib/docopslab/dev/manifest.rb +28 -0
- data/lib/docopslab/dev/paths.rb +0 -17
- data/lib/docopslab/dev/script_manager.rb +12 -6
- data/lib/docopslab/dev/skim.rb +109 -0
- data/lib/docopslab/dev/spell_check.rb +2 -2
- data/lib/docopslab/dev/sync_ops.rb +94 -33
- data/lib/docopslab/dev/tasks.rb +58 -18
- data/lib/docopslab/dev/version.rb +1 -1
- data/lib/docopslab/dev.rb +77 -36
- data/specs/data/default-manifest.yml +23 -5
- data/specs/data/library-index.yml +22 -0
- data/specs/data/manifest-schema.yaml +142 -4
- data/specs/data/tasks-def.yml +122 -10
- metadata +28 -73
- data/assets/config-packs/actionlint/base.yml +0 -13
- data/assets/config-packs/actionlint/project.yml +0 -13
- data/assets/config-packs/htmlproofer/base.yml +0 -27
- data/assets/config-packs/htmlproofer/project.yml +0 -25
- data/assets/config-packs/rubocop/base.yml +0 -130
- data/assets/config-packs/rubocop/project.yml +0 -8
- data/assets/config-packs/shellcheck/base.shellcheckrc +0 -14
- data/assets/config-packs/subtxt/ai-asciidoc-antipatterns.sub.txt +0 -11
- data/assets/config-packs/vale/asciidoc/ExplicitSectionIDs.yml +0 -8
- data/assets/config-packs/vale/asciidoc/ExtraLineBeforeLevel1.yml +0 -7
- data/assets/config-packs/vale/asciidoc/OneSentencePerLine.yml +0 -8
- data/assets/config-packs/vale/asciidoc/PreferSourceBlocks.yml +0 -8
- data/assets/config-packs/vale/asciidoc/ProperAdmonitions.yml +0 -8
- data/assets/config-packs/vale/asciidoc/ProperDLs.yml +0 -7
- data/assets/config-packs/vale/asciidoc/UncleanListStart.yml +0 -8
- data/assets/config-packs/vale/authoring/ButParagraph.yml +0 -8
- data/assets/config-packs/vale/authoring/ExNotEg.yml +0 -8
- data/assets/config-packs/vale/authoring/LiteralTerms.yml +0 -20
- data/assets/config-packs/vale/authoring/Spelling.yml +0 -679
- data/assets/config-packs/vale/base.ini +0 -38
- data/assets/config-packs/vale/config/scripts/ExplicitSectionIDs.tengo +0 -56
- data/assets/config-packs/vale/config/scripts/ExtraLineBeforeLevel1.tengo +0 -121
- data/assets/config-packs/vale/config/scripts/OneSentencePerLine.tengo +0 -53
- data/assets/config-packs/vale/project.ini +0 -5
- data/assets/hooks/pre-commit +0 -63
- data/assets/hooks/pre-push +0 -72
- data/assets/scripts/adoc_section_ids.rb +0 -50
- data/assets/scripts/build-common.sh +0 -193
- data/assets/scripts/build-docker.sh +0 -64
- data/assets/scripts/build.sh +0 -56
- data/assets/scripts/parse_jekyll_asciidoc_logs.rb +0 -467
- data/assets/templates/Gemfile +0 -7
- data/assets/templates/Rakefile +0 -3
- data/assets/templates/gitignore +0 -69
- data/assets/templates/jekyll-asciidoc-fix.prompt.yml +0 -17
- data/assets/templates/spellcheck.prompt.yml +0 -16
- data/docs/agent/AGENTS.md +0 -229
- data/docs/agent/index.md +0 -80
- data/docs/agent/missions/conduct-release.md +0 -224
- data/docs/agent/missions/setup-new-project.md +0 -250
- data/docs/agent/roles/devops-release-engineer.md +0 -152
- data/docs/agent/roles/docops-engineer.md +0 -193
- data/docs/agent/roles/planner-architect.md +0 -74
- data/docs/agent/roles/product-engineer.md +0 -153
- data/docs/agent/roles/product-manager.md +0 -130
- data/docs/agent/roles/project-manager.md +0 -139
- data/docs/agent/roles/qa-testing-engineer.md +0 -115
- data/docs/agent/roles/tech-docs-manager.md +0 -143
- data/docs/agent/roles/tech-writer.md +0 -163
- data/docs/agent/skills/asciidoc.md +0 -609
- data/docs/agent/skills/code-commenting.md +0 -347
- data/docs/agent/skills/fix-broken-links.md +0 -309
- data/docs/agent/skills/fix-jekyll-asciidoc-build-errors.md +0 -23
- data/docs/agent/skills/fix-spelling-issues.md +0 -13
- data/docs/agent/skills/git.md +0 -170
- data/docs/agent/skills/github-issues.md +0 -135
- data/docs/agent/skills/product-release-rollback-and-patching.md +0 -71
- data/docs/agent/skills/rake-cli-dev.md +0 -57
- data/docs/agent/skills/readme-driven-dev.md +0 -13
- data/docs/agent/skills/release-history.md +0 -29
- data/docs/agent/skills/ruby.md +0 -192
- data/docs/agent/skills/schemagraphy-sgyml.md +0 -18
- data/docs/agent/skills/tests-running.md +0 -25
- data/docs/agent/skills/tests-writing.md +0 -45
- data/docs/agent/skills/write-the-docs.md +0 -54
- data/docs/agent/topics/common-project-paths.md +0 -117
- data/docs/agent/topics/dev-tooling-usage.md +0 -202
- data/docs/agent/topics/devops-ci-cd.md +0 -55
- 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 =
|
|
14
|
+
rubocop_config_file = Paths::CONFIG_FILES[:rubocop]
|
|
13
15
|
unless File.exist?(rubocop_config_file)
|
|
14
|
-
rubocop_config_file =
|
|
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 =
|
|
62
|
+
rubocop_config_file = Paths::CONFIG_FILES[:rubocop]
|
|
61
63
|
unless File.exist?(rubocop_config_file)
|
|
62
|
-
rubocop_config_file =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
411
|
+
def run_rubocop_auto_fix context, path: nil
|
|
368
412
|
puts '👮 Running RuboCop auto-correction...'
|
|
369
413
|
|
|
370
|
-
|
|
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: #{
|
|
426
|
+
puts "📄 Using config: #{rubocop_config_file}"
|
|
376
427
|
|
|
377
428
|
# Build command with optional path
|
|
378
|
-
cmd = "bundle exec rubocop --config #{
|
|
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
|
-
|
|
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
|
data/lib/docopslab/dev/paths.rb
CHANGED
|
@@ -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
|
|
13
|
-
puts '❌
|
|
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("#{
|
|
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
|
-
|
|
51
|
-
|
|
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("#{
|
|
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)
|