From 5b851b8bfbaf4b05ec7cdf1d96967030e050478f Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 15 Aug 2025 16:22:12 +0200 Subject: [PATCH 1/8] fix(devdocs) delete project-tree.txt --- projct-tree.txt | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 projct-tree.txt diff --git a/projct-tree.txt b/projct-tree.txt deleted file mode 100644 index 7cc561e..0000000 --- a/projct-tree.txt +++ /dev/null @@ -1,38 +0,0 @@ -. -├── furt-lua -│   ├── config -│   │   └── server.lua -│   ├── deployment -│   │   └── openbsd -│   │   └── rc.d-furt -│   ├── production_checklist.md -│   ├── README.md -│   ├── scripts -│   │   ├── cleanup_debug.sh -│   │   ├── manual_mail_test.sh -│   │   ├── production_test_sequence.sh -│   │   ├── setup_env.sh -│   │   ├── start.sh -│   │   ├── stress_test.sh -│   │   ├── test_auth.sh -│   │   ├── test_curl.sh -│   │   ├── test_modular.sh -│   │   └── test_smtp.sh -│   ├── src -│   │   ├── auth.lua -│   │   ├── ip_utils.lua -│   │   ├── main.lua -│   │   ├── rate_limiter.lua -│   │   ├── routes -│   │   │   ├── auth.lua -│   │   │   └── mail.lua -│   │   └── smtp.lua -│   └── tests -│   └── test_http.lua -├── LICENSE -├── projct-tree.txt -├── README.md -└── tools - └── gitea -> /home/michael/tools/tool-gitea-workflow/scripts - -11 directories, 25 files From 7053af3c0d91ab4a9486fcc0010e29b73b10429d Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 15 Aug 2025 16:57:33 +0200 Subject: [PATCH 2/8] feat(core): implement file-based API versioning system (DAW/furt#83) - Add VERSION file in repository root - Add read_version() function with error handling - Update /health endpoint to show file-based version - Add version display during server startup - Fallback to ?.?.? when VERSION file unreadable Enables deployment tracking across dev/test/prod environments --- VERSION | 1 + src/main.lua | 79 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6da28dd --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.1 \ No newline at end of file diff --git a/src/main.lua b/src/main.lua index eaf98bd..3345cf4 100644 --- a/src/main.lua +++ b/src/main.lua @@ -13,6 +13,25 @@ local AuthRoute = require("src.routes.auth") -- Load configuration local config = require("config.server") +-- Read version from VERSION file +local function read_version() + local file, err = io.open("VERSION", "r") + if not file then + print("WARNING: Could not read VERSION file: " .. (err or "unknown error")) + return "?.?.?" + end + + local version = file:read("*line") + file:close() + + if not version or version:match("^%s*$") then + print("WARNING: VERSION file is empty or contains only whitespace") + return "?.?.?" + end + + return version:match("^%s*(.-)%s*$") -- trim whitespace +end + -- HTTP-Server Module local FurtServer = {} @@ -47,23 +66,23 @@ function FurtServer:parse_request(client) if not request_line then return nil end - + -- Parse request line: "POST /v1/mail/send HTTP/1.1" local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)") if not method then return nil end - + -- Parse headers local headers = {} local content_length = 0 - + while true do local line = client:receive() if not line or line == "" then break end - + local key, value = line:match("([^:]+): (.+)") if key and value then headers[key:lower()] = value @@ -72,13 +91,13 @@ function FurtServer:parse_request(client) end end end - + -- Parse body local body = "" if content_length > 0 then body = client:receive(content_length) end - + return { method = method, path = path, @@ -97,11 +116,11 @@ function FurtServer:add_cors_headers(request) "https://dragons-at-work.de", "https://www.dragons-at-work.de" } - + -- Check if request has Origin header local origin = request and request.headers and request.headers.origin local cors_origin = "*" -- Default fallback - + -- If origin is provided and in allowed list, use it if origin then for _, allowed in ipairs(allowed_origins) do @@ -111,7 +130,7 @@ function FurtServer:add_cors_headers(request) end end end - + return { ["Access-Control-Allow-Origin"] = cors_origin, ["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS", @@ -125,38 +144,38 @@ end function FurtServer:create_response(status, data, content_type, additional_headers, request) content_type = content_type or "application/json" local body = "" - + if type(data) == "table" then body = cjson.encode(data) else body = tostring(data or "") end - + -- Start with CORS headers local headers = self:add_cors_headers(request) - + -- Add standard headers headers["Content-Type"] = content_type headers["Content-Length"] = tostring(#body) headers["Connection"] = "close" headers["Server"] = "Furt-Lua/1.1" - + -- Add additional headers if provided if additional_headers then for key, value in pairs(additional_headers) do headers[key] = value end end - + -- Build response local response = string.format("HTTP/1.1 %d %s\r\n", status, self:get_status_text(status)) - + for key, value in pairs(headers) do response = response .. key .. ": " .. value .. "\r\n" end - + response = response .. "\r\n" .. body - + return response end @@ -184,23 +203,23 @@ function FurtServer:handle_client(client) client:send(response) return end - - print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"), + + print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"), request.method, request.path)) - + -- Handle OPTIONS preflight requests (CORS) if request.method == "OPTIONS" then local response = self:create_response(204, "", "text/plain", nil, request) client:send(response) return end - + -- Route handling local handler = nil if self.routes[request.method] and self.routes[request.method][request.path] then handler = self.routes[request.method][request.path] end - + if handler then local success, result = pcall(handler, request, self) if success then @@ -223,13 +242,16 @@ function FurtServer:start() if not self.server then error("Failed to bind to " .. self.host .. ":" .. self.port) end - + + local version = read_version() + print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port)) + print("Version: " .. version) print("API-Key authentication: ENABLED") print("Rate limiting: ENABLED (60 req/hour per API key, 100 req/hour per IP)") print("CORS enabled for configured origins") print("Press Ctrl+C to stop") - + while true do local client = self.server:accept() if client then @@ -245,10 +267,11 @@ local server = FurtServer:new() -- Public routes (no authentication required) server:add_route("GET", "/health", function(request, server) + local version = read_version() local response_data = { status = "healthy", service = "furt-lua", - version = "1.1.0", + version = version, timestamp = os.time(), features = { smtp_configured = config.mail and config.mail.username ~= nil, @@ -268,15 +291,15 @@ if os.getenv("ENABLE_TEST_ENDPOINT") == "true" then headers_count = 0, warning = "This is a development endpoint" } - + -- Count headers for _ in pairs(request.headers) do response_data.headers_count = response_data.headers_count + 1 end - + return server:create_response(200, response_data, nil, nil, request) end) - print("⚠️ Test endpoint enabled (development mode)") + print("[WARN] Test endpoint enabled (development mode)") end -- Protected routes (require authentication) From 00b8a1852799472730763510a100418b1b09a234 Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 19 Aug 2025 21:28:22 +0200 Subject: [PATCH 3/8] feat(health): migrate VERSION file to merkwerk integration (#83) - Replace read_version() with merkwerk.get_health_info() - Health endpoint now returns content_hash, vcs_info, source tracking - Add merkwerk_integrated feature flag - Enhanced startup logs with content-hash and VCS info - Maintain backward compatibility with version field - lua51 compatible integration for OpenBSD deployment Migration from static VERSION file to dynamic merkwerk version tracking. Health endpoint now provides rich metadata for debugging and monitoring. Resolves DAW/furt#83 --- .version_history | 3 + integrations/lua-api.lua | 243 +++++++++++++++++++++++++++++++++++++++ src/main.lua | 48 +++++--- 3 files changed, 276 insertions(+), 18 deletions(-) create mode 100644 .version_history create mode 100644 integrations/lua-api.lua diff --git a/.version_history b/.version_history new file mode 100644 index 0000000..4a48ffe --- /dev/null +++ b/.version_history @@ -0,0 +1,3 @@ +# merkwerk version history +# Format: content_hash,vcs_hash,branch,timestamp,author,vcs_type,project_type +7e82f537,7053af3,main,2025-08-19T18:14:06Z,michael,git,lua-api diff --git a/integrations/lua-api.lua b/integrations/lua-api.lua new file mode 100644 index 0000000..cd96832 --- /dev/null +++ b/integrations/lua-api.lua @@ -0,0 +1,243 @@ +-- 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 + diff --git a/src/main.lua b/src/main.lua index 3345cf4..eee08ef 100644 --- a/src/main.lua +++ b/src/main.lua @@ -13,23 +13,29 @@ local AuthRoute = require("src.routes.auth") -- Load configuration local config = require("config.server") --- Read version from VERSION file -local function read_version() - local file, err = io.open("VERSION", "r") - if not file then - print("WARNING: Could not read VERSION file: " .. (err or "unknown error")) - return "?.?.?" +local function get_version_info() + -- Load merkwerk integration + local success, merkwerk = pcall(require, "integrations.lua-api") + if not success then + print("WARNING: merkwerk integration not available, using fallback") + return { + service = "furt-lua", + version = "?.?.?", + content_hash = "unknown", + vcs_info = { type = "none", hash = "", branch = "" }, + source = "fallback-no-merkwerk" + } end - local version = file:read("*line") - file:close() + -- Get merkwerk health info + local health_info = merkwerk.get_health_info() - if not version or version:match("^%s*$") then - print("WARNING: VERSION file is empty or contains only whitespace") - return "?.?.?" + -- Ensure compatibility with old VERSION-only expectations + if not health_info.version then + health_info.version = "?.?.?" end - return version:match("^%s*(.-)%s*$") -- trim whitespace + return health_info end -- HTTP-Server Module @@ -243,10 +249,12 @@ function FurtServer:start() error("Failed to bind to " .. self.host .. ":" .. self.port) end - local version = read_version() + local version_info = get_version_info() print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port)) - print("Version: " .. version) + print("Version: " .. version_info.version .. " (merkwerk)") + print("Content-Hash: " .. (version_info.content_hash or "unknown")) + print("VCS: " .. (version_info.vcs_info and version_info.vcs_info.hash or "none")) print("API-Key authentication: ENABLED") print("Rate limiting: ENABLED (60 req/hour per API key, 100 req/hour per IP)") print("CORS enabled for configured origins") @@ -267,16 +275,20 @@ local server = FurtServer:new() -- Public routes (no authentication required) server:add_route("GET", "/health", function(request, server) - local version = read_version() + local version_info = get_version_info() local response_data = { status = "healthy", - service = "furt-lua", - version = version, + service = version_info.service or "furt-lua", + version = version_info.version, + content_hash = version_info.content_hash, + vcs_info = version_info.vcs_info, timestamp = os.time(), + source = version_info.source, features = { smtp_configured = config.mail and config.mail.username ~= nil, auth_enabled = true, - rate_limiting = true + rate_limiting = true, + merkwerk_integrated = version_info.source == "merkwerk" } } return server:create_response(200, response_data, nil, nil, request) From f2d925ee57b828243dba201eec24cdfc2befad62 Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 19 Aug 2025 21:36:34 +0200 Subject: [PATCH 4/8] chore: merkwerk auto-update --- .version_history | 1 + 1 file changed, 1 insertion(+) diff --git a/.version_history b/.version_history index 4a48ffe..1f81f87 100644 --- a/.version_history +++ b/.version_history @@ -1,3 +1,4 @@ # merkwerk version history # Format: content_hash,vcs_hash,branch,timestamp,author,vcs_type,project_type 7e82f537,7053af3,main,2025-08-19T18:14:06Z,michael,git,lua-api +7e41647c,00b8a18,main,2025-08-19T19:36:33Z,michael,git,lua-api From 62ddc17393a76d634af30ba8c45e46054b6eaf2c Mon Sep 17 00:00:00 2001 From: michael Date: Wed, 20 Aug 2025 06:07:56 +0200 Subject: [PATCH 5/8] feat(integration): production-ready .version_history priority - Change get_info() priority: .version_history first, merkwerk fallback - Add read_version_history() for production deployment compatibility - Works without merkwerk binary (tar.gz deployments) - Maintains development fallback to merkwerk command Production-ready: tar.gz deployments work without merkwerk installation. --- integrations/lua-api.lua | 162 +++++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 47 deletions(-) diff --git a/integrations/lua-api.lua b/integrations/lua-api.lua index cd96832..0d15243 100644 --- a/integrations/lua-api.lua +++ b/integrations/lua-api.lua @@ -70,6 +70,67 @@ local function parse_json_response(json_str) 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 { @@ -96,7 +157,7 @@ local function is_cache_valid(ttl) return cache.data and (current_time - cache.timestamp) < ttl end --- Get version information with caching +-- Get version information with NEW priority order function merkwerk.get_info(options) options = options or {} local use_cache = options.cache ~= false @@ -117,66 +178,73 @@ function merkwerk.get_info(options) return result end - -- Execute merkwerk command + -- 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 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 + data.build_info = { + cached = false, + source = "merkwerk_command", + method = "development_fallback", + history_error = history_error + } + end - if include_build_info then - fallback.build_info = { - cached = false, - error = error_msg or "merkwerk unavailable" - } - end + -- Update cache + if use_cache then + cache.data = data + cache.timestamp = os.time() + end - return fallback + return data + end 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 + -- LAST RESORT: Pure fallback + local fallback = fallback_info() + if fallback_version then + fallback.base_version = fallback_version + fallback.full_version = fallback_version .. "+unknown" end - -- Add metadata - data.source = "merkwerk" + fallback.error = "Both version_history and merkwerk failed" if include_build_info then - data.build_info = { + fallback.build_info = { cached = false, - timestamp = os.time(), - cache_ttl = cache_ttl + source = "fallback", + method = "emergency_fallback", + history_error = history_error, + merkwerk_error = error_msg or "merkwerk unavailable" } end - -- Update cache - if use_cache then - cache.data = data - cache.timestamp = os.time() - end - - return data + return fallback end -- Get only the content hash (lightweight) From 6b2da02429a83c27ab29c7e186e01c8163c28c77 Mon Sep 17 00:00:00 2001 From: michael Date: Wed, 20 Aug 2025 06:07:56 +0200 Subject: [PATCH 6/8] chore: merkwerk auto-update --- .version_history | 1 + 1 file changed, 1 insertion(+) diff --git a/.version_history b/.version_history index 1f81f87..5a41dd4 100644 --- a/.version_history +++ b/.version_history @@ -2,3 +2,4 @@ # Format: content_hash,vcs_hash,branch,timestamp,author,vcs_type,project_type 7e82f537,7053af3,main,2025-08-19T18:14:06Z,michael,git,lua-api 7e41647c,00b8a18,main,2025-08-19T19:36:33Z,michael,git,lua-api +7e41647c,62ddc17,main,2025-08-20T04:08:04Z,michael,git,lua-api From 95dcdbaebbf3819cd0a601ba87d25e1659e382df Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 28 Aug 2025 17:34:36 +0200 Subject: [PATCH 7/8] feat(integration): add universal merkwerk binary detection - Check development binary (./bin/merkwerk) - Check installed binary (/usr/local/bin/merkwerk) - Fallback to PATH lookup (command -v merkwerk) - Proper error handling for missing binary Related to DAW/furt#94 --- integrations/lua-api.lua | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/integrations/lua-api.lua b/integrations/lua-api.lua index 0d15243..b543bce 100644 --- a/integrations/lua-api.lua +++ b/integrations/lua-api.lua @@ -10,10 +10,51 @@ local cache = { 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 command = "./tools/merkwerk/bin/merkwerk " .. args .. " 2>/dev/null" + + 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 From 82da58b35801fe7f2df1fb5c4d3a6b23af7c1a61 Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 28 Aug 2025 17:34:36 +0200 Subject: [PATCH 8/8] chore: merkwerk auto-update --- .version_history | 1 + 1 file changed, 1 insertion(+) diff --git a/.version_history b/.version_history index 5a41dd4..7dea2cc 100644 --- a/.version_history +++ b/.version_history @@ -3,3 +3,4 @@ 7e82f537,7053af3,main,2025-08-19T18:14:06Z,michael,git,lua-api 7e41647c,00b8a18,main,2025-08-19T19:36:33Z,michael,git,lua-api 7e41647c,62ddc17,main,2025-08-20T04:08:04Z,michael,git,lua-api +7e41647c,95dcdba,main,2025-08-28T15:34:36Z,michael,git,lua-api