diff --git a/grove.hml b/grove.hml index 77d27eb..c056b8e 100644 --- a/grove.hml +++ b/grove.hml @@ -749,6 +749,120 @@ fn handle_check(body) { }); } +fn build_bwrap_format_command(temp_file) { + // Build bubblewrap command for formatting (needs write access to temp file) + let format_cmd = `"${CONFIG["hemlock_path"]}" format "${temp_file}"`; + + let bwrap_cmd = `"${CONFIG["bwrap_path"]}" ` + + `--unshare-user ` + + `--unshare-net ` + + `--unshare-ipc ` + + `--uid 65534 ` + + `--gid 65534 ` + + `--die-with-parent ` + + `--new-session ` + + `--ro-bind /usr /usr ` + + `--ro-bind /lib /lib ` + + `--ro-bind-try /lib64 /lib64 ` + + `--ro-bind /bin /bin ` + + `--ro-bind-try /usr/local /usr/local ` + + `--dir /var ` + + `--dir /var/tmp ` + + // Need write access for formatting (bind instead of ro-bind) + `--bind "${CONFIG["temp_dir"]}" "${CONFIG["temp_dir"]}" ` + + `--tmpfs /tmp ` + + `--clearenv ` + + `--setenv PATH /usr/local/bin:/usr/bin:/bin ` + + `-- sh -c "${format_cmd}"`; + + return bwrap_cmd; +} + +fn handle_format(body) { + // Parse JSON body + let data = null; + try { + data = parse(body); + } catch (e) { + return json_response(400, { + success: false, + error: "Invalid JSON body" + }); + } + + // Get code from body + let code = data["code"]; + if (code == null || code == "") { + return json_response(400, { + success: false, + error: "Missing 'code' field" + }); + } + + // Validate code size + if (code.length > CONFIG["max_code_size"]) { + return json_response(400, { + success: false, + error: "Code exceeds maximum size limit (100KB)" + }); + } + + // Create temp file with random suffix to prevent predictable filenames + ensure_temp_dir(); + let ts = time_ms(); + let rand = random_suffix(12); + let temp_file = `${CONFIG["temp_dir"]}/format_${ts}_${rand}.hml`; + write_file(temp_file, code); + + // Run hemlock format with optional sandboxing + // hemlock format FILE modifies the file in place + let result = null; + if (CONFIG["secure_mode"]) { + let bwrap_cmd = build_bwrap_format_command(temp_file); + result = run_shell(`timeout 30 ${bwrap_cmd} 2>&1`); + } else { + result = run_shell(`timeout 30 "${CONFIG["hemlock_path"]}" format "${temp_file}" 2>&1`); + } + + // Read the formatted code back + let formatted_code = ""; + let format_error = ""; + + if (result["code"] == 0) { + // Success - read the formatted file + if (exists(temp_file)) { + formatted_code = read_file(temp_file); + } + } else { + // Format failed - get error message + format_error = result["stdout"]; + if ((format_error == "" || format_error == null) && result["stderr"] != null) { + format_error = result["stderr"]; + } + // Clean up temp file path from error messages + if (format_error != "" && format_error != null) { + format_error = format_error.split(temp_file).join("main.hml"); + } + } + + // Cleanup temp file + if (exists(temp_file)) { + remove_file(temp_file); + } + + if (result["code"] == 0) { + return json_response(200, { + success: true, + formatted: formatted_code + }); + } else { + return json_response(200, { + success: false, + error: format_error + }); + } +} + fn handle_execute(body) { // Parse JSON body let data = null; @@ -946,6 +1060,8 @@ fn handle_connection(stream) { response = handle_execute(req["body"]); } else if (path == "/check" && method == "POST") { response = handle_check(req["body"]); + } else if (path == "/format" && method == "POST") { + response = handle_format(req["body"]); } else { response = handle_not_found(path); } @@ -996,6 +1112,7 @@ fn main() { print(" GET /health - Health check"); print(" GET /version - Version info"); print(" POST /check - Type check code (hemlockc)"); + print(" POST /format - Format code (hemlock fmt)"); print(" POST /execute - Execute code"); print(""); print("Press Ctrl+C to stop."); diff --git a/playground.html b/playground.html index 664c71b..9bedc07 100644 --- a/playground.html +++ b/playground.html @@ -438,6 +438,7 @@ + @@ -866,6 +867,7 @@ const output = document.getElementById('output'); const runBtn = document.getElementById('run-btn'); const checkBtn = document.getElementById('check-btn'); + const formatBtn = document.getElementById('format-btn'); const statusDot = document.getElementById('status-dot'); const statusText = document.getElementById('status-text'); const versionInfo = document.getElementById('version-info'); @@ -997,7 +999,7 @@ // Check syntax async function checkSyntax() { if (isRunning) return; - + const code = editor.value; if (!code.trim()) { output.innerHTML = 'No code to check'; @@ -1014,7 +1016,7 @@ }); const data = await res.json(); - + if (data.valid) { output.innerHTML = '✓ Syntax OK'; setStatus('Valid'); @@ -1029,6 +1031,53 @@ } } + // Format code + async function formatCode() { + if (isRunning) return; + + const code = editor.value; + if (!code.trim()) { + output.innerHTML = 'No code to format'; + return; + } + + isRunning = true; + runBtn.disabled = true; + checkBtn.disabled = true; + formatBtn.disabled = true; + setStatus('Formatting...', false, true); + + try { + const res = await fetch(`${API_URL}/format`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code }) + }); + + const data = await res.json(); + + if (data.success) { + // Update editor with formatted code + editor.value = data.formatted; + updateHighlight(); + output.innerHTML = '✓ Code formatted'; + setStatus('Formatted'); + } else { + output.innerHTML = `Format error: ${escapeHtml(data.error)}`; + setStatus('Format Error', true); + } + + } catch (e) { + output.innerHTML = `Failed to connect to API: ${e.message}`; + setStatus('API Error', true); + } finally { + isRunning = false; + runBtn.disabled = false; + checkBtn.disabled = false; + formatBtn.disabled = false; + } + } + // Load example function loadExample(name) { if (EXAMPLES[name]) { @@ -1058,6 +1107,7 @@ // Event listeners runBtn.addEventListener('click', runCode); checkBtn.addEventListener('click', checkSyntax); + formatBtn.addEventListener('click', formatCode); // Syntax highlighting events editor.addEventListener('input', updateHighlight);