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);