-- integrations/lua-api.lua - merkwerk Lua API integration -- Provides merkwerk version information for Lua applications local merkwerk = {} -- Cache for version info to avoid repeated shell calls local cache = { data = nil, timestamp = 0, ttl = 300 -- 5 minutes default TTL } -- Execute merkwerk command and return result local function execute_merkwerk(args) args = args or "info --json" local command = "./tools/merkwerk/bin/merkwerk " .. args .. " 2>/dev/null" local handle = io.popen(command) if not handle then return nil, "Failed to execute merkwerk command" end local result = handle:read("*a") local success, exit_reason, exit_code = handle:close() if not success or (exit_code and exit_code ~= 0) then return nil, "merkwerk command failed with exit code " .. (exit_code or "unknown") end if result == "" then return nil, "merkwerk returned empty result" end return result, nil end -- Parse JSON response (simple parser for basic merkwerk JSON) local function parse_json_response(json_str) if not json_str then return nil end -- Try to use cjson if available local ok, cjson = pcall(require, "cjson") if ok then local success, data = pcall(cjson.decode, json_str) if success then return data end end -- Fallback: simple manual parsing for merkwerk JSON structure local data = {} -- Extract basic fields using pattern matching data.project_name = json_str:match('"project_name"%s*:%s*"([^"]*)"') or "unknown" data.project_type = json_str:match('"project_type"%s*:%s*"([^"]*)"') or "unknown" data.base_version = json_str:match('"base_version"%s*:%s*"([^"]*)"') or "?.?.?" data.content_hash = json_str:match('"content_hash"%s*:%s*"([^"]*)"') or "unknown" data.full_version = json_str:match('"full_version"%s*:%s*"([^"]*)"') or "?.?.?+unknown" data.timestamp = json_str:match('"timestamp"%s*:%s*"([^"]*)"') or "" -- Extract VCS info local vcs_block = json_str:match('"vcs"%s*:%s*{([^}]*)}') if vcs_block then data.vcs = {} data.vcs.type = vcs_block:match('"type"%s*:%s*"([^"]*)"') or "none" data.vcs.hash = vcs_block:match('"hash"%s*:%s*"([^"]*)"') or "" data.vcs.branch = vcs_block:match('"branch"%s*:%s*"([^"]*)"') or "" else data.vcs = { type = "none", hash = "", branch = "" } end return data end -- Generate fallback info when merkwerk is unavailable local function fallback_info() return { project_name = "unknown", project_type = "lua-api", base_version = "?.?.?", content_hash = "unknown", full_version = "?.?.?+unknown", timestamp = os.date("%Y-%m-%dT%H:%M:%SZ"), vcs = { type = "none", hash = "", branch = "" }, source = "fallback", error = "merkwerk not available" } end -- Check if cached data is still valid local function is_cache_valid(ttl) ttl = ttl or cache.ttl local current_time = os.time() return cache.data and (current_time - cache.timestamp) < ttl end -- Get version information with caching function merkwerk.get_info(options) options = options or {} local use_cache = options.cache ~= false local cache_ttl = options.cache_ttl or cache.ttl local fallback_version = options.fallback_version local include_build_info = options.include_build_info or false -- Return cached data if valid and caching enabled if use_cache and is_cache_valid(cache_ttl) then local result = cache.data if include_build_info then result.build_info = { cached = true, cache_age = os.time() - cache.timestamp, cache_ttl = cache_ttl } end return result end -- Execute merkwerk command local json_result, error_msg = execute_merkwerk("info --json") if not json_result then -- merkwerk failed - use fallback local fallback = fallback_info() if fallback_version then fallback.base_version = fallback_version fallback.full_version = fallback_version .. "+unknown" end if error_msg then fallback.error = error_msg end if include_build_info then fallback.build_info = { cached = false, error = error_msg or "merkwerk unavailable" } end return fallback end -- Parse JSON response local data = parse_json_response(json_result) if not data then -- JSON parsing failed - use fallback local fallback = fallback_info() fallback.error = "Failed to parse merkwerk JSON output" if include_build_info then fallback.build_info = { cached = false, error = "JSON parsing failed", raw_output = json_result:sub(1, 100) -- First 100 chars for debugging } end return fallback end -- Add metadata data.source = "merkwerk" if include_build_info then data.build_info = { cached = false, timestamp = os.time(), cache_ttl = cache_ttl } end -- Update cache if use_cache then cache.data = data cache.timestamp = os.time() end return data end -- Get only the content hash (lightweight) function merkwerk.get_hash() local hash_result, error_msg = execute_merkwerk("hash") if not hash_result then return "unknown" end -- Clean up the result (remove whitespace) return hash_result:gsub("%s+", "") end -- Get version for HTTP health endpoints function merkwerk.get_health_info() local info = merkwerk.get_info({ cache = true, cache_ttl = 600 }) -- 10 minute cache for health checks return { service = info.project_name, version = info.full_version, content_hash = info.content_hash, vcs_info = info.vcs, timestamp = info.timestamp, source = info.source } end -- Get minimal version string for logging function merkwerk.get_version_string() local info = merkwerk.get_info({ cache = true }) return info.full_version end -- Clear cache (useful for testing or forced refresh) function merkwerk.clear_cache() cache.data = nil cache.timestamp = 0 end -- Set cache TTL function merkwerk.set_cache_ttl(ttl) cache.ttl = ttl or 300 end -- Get cache status (for debugging) function merkwerk.get_cache_status() return { has_data = cache.data ~= nil, timestamp = cache.timestamp, age = cache.data and (os.time() - cache.timestamp) or 0, ttl = cache.ttl, valid = is_cache_valid() } end -- Validate merkwerk availability function merkwerk.validate() local result, error_msg = execute_merkwerk("info") return result ~= nil, error_msg end return merkwerk