Module: DocOpsLab::Dev::Library

Defined in:
lib/docopslab/dev/library.rb,
lib/docopslab/dev/library/cache.rb,
lib/docopslab/dev/library/fetch.rb

Overview

Remote library fetch, cache, and resolution. Manages a host-wide asset cache at ~/.cache/docopslab/dev/library/. Callers should use this module directly: Library.fetch!, Library.resolve(path), etc.

Defined Under Namespace

Modules: Cache, Fetch

Class Method Summary collapse

Class Method Details

.available?Boolean

True if a library is available via cache or local_path fallback.

Returns:

  • (Boolean)


131
132
133
134
135
136
# File 'lib/docopslab/dev/library.rb', line 131

def available?
  return true if Cache.available?

  lp = Dev.load_manifest&.dig('library', 'local_path')
  !!(lp && File.exist?(File.join(lp, 'catalog.json')))
end

.cached_pathObject



93
94
95
# File 'lib/docopslab/dev/library.rb', line 93

def cached_path
  Cache.current_path
end

.ensure_available!Object

Ensure the library is available, auto-fetching if necessary. Returns true if available after the call; raises on failure.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/docopslab/dev/library.rb', line 140

def ensure_available!
  return true if available?

  puts '📥 Library cache not found; fetching now...'
  ok = fetch!
  return true if ok && available?

  lp = Dev.load_manifest&.dig('library', 'local_path')
  if lp && Dir.exist?(lp)
    warn "⚠️  Remote fetch failed; using local_path fallback: #{lp}"
    return true
  end

  raise 'Library unavailable. Run `bundle exec rake labdev:sync:library` to fetch it.'
end

.fetch!(config = nil) ⇒ Object



18
19
20
21
# File 'lib/docopslab/dev/library.rb', line 18

def fetch! config=nil
  config ||= library_config_from_manifest
  with_cache_root(config) { Fetch.call(config) }
end

Compare manifest catalog entries against the cached library files Falls back to an on-repo local path if provided in the manifest



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/docopslab/dev/library.rb', line 191

def print_catalog_comparison manifest = nil
  manifest ||= Dev.load_manifest
  lib_cfg = manifest && manifest['library']

  if lib_cfg.nil? || lib_cfg.empty?
    puts "ℹ️  No `library` block found in #{Dev.manifest_path} (or it's empty)."
    return
  end

  catalog = lib_cfg.dig('catalog', 'overrides') || lib_cfg['catalog'] || lib_cfg['catalog_overrides']

  unless catalog && !catalog.empty?
    puts 'ℹ️  No catalog overrides found in manifest.library.catalog; nothing to compare.'
    return
  end

  puts '🔎 Comparing manifest catalog entries to cached library files...'

  entries = []
  case catalog
  when Array
    entries = catalog
  when Hash
    catalog.each do |k, v|
      entries << if v.is_a?(String)
                   v
                 elsif v.is_a?(Hash) && v['path']
                   v['path']
                 else
                   k
                 end
    end
  else
    puts "⚠️  Unrecognized catalog format: #{catalog.class}. Skipping detailed compare."
    return
  end

  missing = []
  present = []

  entries.each do |rel_path|
    rel = rel_path.to_s.sub(%r{^/}, '')
    resolved = resolve(rel)

    # Fallback to on-repo local path if provided
    if resolved.nil? && lib_cfg['local_path']
      repo_local = File.join(Dir.pwd, lib_cfg['local_path'].to_s, rel)
      resolved = File.exist?(repo_local) ? repo_local : nil
    end

    if resolved
      present << { path: rel, full: resolved }
    else
      missing << rel
    end
  end

  if present.any?
    puts "✅ Found #{present.size} catalog entries in the cache or local path:"
    present.each do |p|
      puts "  - #{p[:path]} -> #{p[:full]}"
    end
  end

  if missing.any?
    puts "❌ Missing #{missing.size} catalog entries in the cache/local path:"
    missing.each do |m|
      puts "  - #{m}"
    end
  else
    puts '✅ All catalog entries present in cache/local path.'
  end
end


170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/docopslab/dev/library.rb', line 170

def print_status
  s = status
  if s[:available]
    puts "📚 Library cache: #{s[:cache_path]}"
    puts "   Version    : #{s[:version] || '(unknown)'}"
    puts "   Ref        : #{s[:ref] || '(unknown)'}"
    puts "   Generated  : #{s[:generated_at] || '(unknown)'}"
    puts "   Previous   : #{s[:has_previous] ? 'yes' : 'none'}"
  else
    puts "⚠️  No library cache found at #{s[:cache_path]}"
    lp = Dev.load_manifest&.dig('library', 'local_path')
    if lp && File.exist?(File.join(lp, 'catalog.json'))
      puts "   Local path : #{File.expand_path(lp)} (active fallback)"
    else
      puts '   Run `bundle exec rake labdev:sync:library` to fetch.'
    end
  end
end

.resolve(relative_path) ⇒ Object

Returns the absolute path to a cached file, or nil if absent. Resolution order:

1. XDG host cache (~/.cache/docopslab/dev/library/current/)
2. local_path from manifest (dev/monorepo fallback, e.g. .library/)


114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/docopslab/dev/library.rb', line 114

def resolve relative_path
  if Cache.available?
    full_path = File.join(Cache.current_path, relative_path)
    return full_path if File.exist?(full_path)
  end

  # local_path fallback for monorepo dev and offline use
  lp = Dev.load_manifest&.dig('library', 'local_path')
  if lp
    local_full = File.expand_path(File.join(lp, relative_path))
    return local_full if File.exist?(local_full)
  end

  nil
end

.rollback!Object



160
161
162
163
164
165
166
167
168
# File 'lib/docopslab/dev/library.rb', line 160

def rollback!
  if Cache.rollback!
    puts "✅ Library rolled back to previous snapshot at #{Cache.current_path}"
    true
  else
    warn '⚠️  No previous library snapshot available for rollback.'
    false
  end
end

.rootObject

Returns the effective library root directory (nil if unavailable). Does not auto-fetch; call ensure_available! first if needed. Resolution order mirrors resolve():

1. XDG host cache  2. local_path from manifest


101
102
103
104
105
106
107
108
# File 'lib/docopslab/dev/library.rb', line 101

def root
  return Cache.current_path if Cache.available?

  lp = Dev.load_manifest&.dig('library', 'local_path')
  return File.expand_path(lp) if lp && File.exist?(File.join(lp, 'catalog.json'))

  nil
end

.stage!(source_path: nil) ⇒ Object

Copy a local library directory into the host cache and sync content to manifest-configured paths. Intended for development workflows where assets live in the lab monorepo (.library/ or library/current/) and have not yet been published to the remote branch.

Resolution order for source_path:

1. Explicit argument (task arg or direct call)
2. manifest +library.local_path+ (resolved relative to cwd)
3. .library/ in the current working directory
4. ../lab/.library/ relative to cwd (downstream-project fallback)

A minimal catalog.json is generated into a staging copy if the source directory does not already contain one.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/docopslab/dev/library.rb', line 63

def stage! source_path: nil
  resolved = resolve_stage_source(source_path)
  unless resolved
    warn '⚠️  No local library path found. ' \
         "Pass a path, or set library.local_path in #{Dev::MANIFEST_PATH}."
    return false
  end

  puts "📦 Staging local library from #{resolved}..."

  Dir.mktmpdir('docopslab-stage-') do |tmpdir|
    dest = File.join(tmpdir, 'stage')
    FileUtils.cp_r(resolved, dest)
    ensure_catalog!(dest)
    Cache.write!(dest)
  end

  puts "✅ Local library staged to #{Cache.current_path}"

  context = Dev
  SyncOps.sync_config_files(context)
  SyncOps.sync_docs(context, force: true)
  SyncOps.sync_templates(context, force: true)
  SyncOps.sync_scripts(context)
  true
rescue StandardError => e
  warn "⚠️  Stage failed: #{e.message}"
  false
end

.statusObject



156
157
158
# File 'lib/docopslab/dev/library.rb', line 156

def status
  Cache.status
end

.sync!(force: false) ⇒ Object

Fetch the library if the cache is absent or stale, then sync all manifest-driven content (docs, config files, templates, scripts) to local paths. This is the main entry point for ‘labdev:sync:library`.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/docopslab/dev/library.rb', line 26

def sync! force: false
  config = library_config_from_manifest
  with_cache_root(config) do
    if local_path_active?(config)
      puts "📚 Using local library at #{File.expand_path(config['local_path'])}"
    elsif !force && Cache.available? && sha_current?(config)
      puts "\u2705 Library cache is up to date (#{Cache.stored_head&.slice(0, 8)})"
    else
      puts Cache.available? ? '🔄 Library has updates; refreshing...' : '📥 Library cache not found; fetching...'
      ok = Fetch.call(config)
      unless ok
        warn '⚠️  Library fetch failed. Using existing cache if available.'
        raise 'Library unavailable.' unless available?
      end
    end

    context = Dev
    SyncOps.sync_config_files(context)
    SyncOps.sync_docs(context, force: force)
    SyncOps.sync_templates(context, force: force)
    SyncOps.sync_scripts(context)
  end
end