-- 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 } -- Find merkwerk binary using universal detection pattern local function find_merkwerk_binary() -- Check development binary local dev_handle = io.popen("test -x './bin/merkwerk' && echo './bin/merkwerk' 2>/dev/null") if dev_handle then local dev_result = dev_handle:read("*line") dev_handle:close() if dev_result and dev_result ~= "" then return dev_result end end -- Check installed binary local inst_handle = io.popen("test -x '/usr/local/bin/merkwerk' && echo '/usr/local/bin/merkwerk' 2>/dev/null") if inst_handle then local inst_result = inst_handle:read("*line") inst_handle:close() if inst_result and inst_result ~= "" then return inst_result end end -- Check PATH local path_handle = io.popen("command -v merkwerk 2>/dev/null") if path_handle then local path_result = path_handle:read("*line") path_handle:close() if path_result and path_result ~= "" then return "merkwerk" end end return nil end -- Execute merkwerk command and return result local function execute_merkwerk(args) args = args or "info --json" local merkwerk_cmd = find_merkwerk_binary() if not merkwerk_cmd then return nil, "merkwerk binary not found" end local command = merkwerk_cmd .. " " .. 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 -- Read latest entry from .version_history file local function read_version_history() local file = io.open(".version_history", "r") if not file then return nil, "No .version_history file found" end local last_line = nil for line in file:lines() do -- Skip comment lines if not line:match("^%s*#") and line:match("%S") then last_line = line end end file:close() if not last_line then return nil, ".version_history contains no valid entries" end -- Parse: content_hash,vcs_hash,branch,timestamp,author,vcs_type,project_type local parts = {} for part in last_line:gmatch("([^,]+)") do table.insert(parts, part) end if #parts < 7 then return nil, "Invalid .version_history format" end -- Get base version from VERSION file if available local base_version = "?.?.?" local version_file = io.open("VERSION", "r") if version_file then local version_content = version_file:read("*line") if version_content and not version_content:match("^%s*$") then base_version = version_content:match("^%s*(.-)%s*$") end version_file:close() end -- Build response in same format as merkwerk local data = { project_name = parts[7] and parts[7]:gsub("-api$", "") or "unknown", -- lua-api → lua project_type = parts[7] or "unknown", base_version = base_version, content_hash = parts[1] or "unknown", full_version = base_version .. "+" .. (parts[1] or "unknown"), version = base_version .. "+" .. (parts[1] or "unknown"), timestamp = parts[4] or "", vcs = { type = parts[6] or "none", hash = parts[2] or "", branch = parts[3] or "" }, source = "version_history" } return data, nil 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 NEW priority order 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 -- PRODUCTION PRIORITY: Try .version_history FIRST local history_data, history_error = read_version_history() if history_data then -- Success: Use version history data if include_build_info then history_data.build_info = { cached = false, source = "version_history", method = "production_ready" } end -- Update cache if use_cache then cache.data = history_data cache.timestamp = os.time() end return history_data end -- FALLBACK: Try merkwerk command (development/testing) local json_result, error_msg = execute_merkwerk("info --json") if json_result then local data = parse_json_response(json_result) if data then data.source = "merkwerk" if include_build_info then data.build_info = { cached = false, source = "merkwerk_command", method = "development_fallback", history_error = history_error } end -- Update cache if use_cache then cache.data = data cache.timestamp = os.time() end return data end end -- LAST RESORT: Pure fallback local fallback = fallback_info() if fallback_version then fallback.base_version = fallback_version fallback.full_version = fallback_version .. "+unknown" end fallback.error = "Both version_history and merkwerk failed" if include_build_info then fallback.build_info = { cached = false, source = "fallback", method = "emergency_fallback", history_error = history_error, merkwerk_error = error_msg or "merkwerk unavailable" } end return fallback 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