portail 2.1.0

Unified proxy/gateway: AI Gateway + MCP Gateway + CDN cache
Documentation
use anyhow::Result;
use std::fs;
use std::path::Path;

/// Convert a shell script to Amber language
#[allow(unused_assignments)]
pub fn amberify(input: &Path) -> Result<String> {
    let content = fs::read_to_string(input)?;
    let mut output = String::new();

    // Add header
    output.push_str(&format!("// Amber conversion of {}\n", input.display()));
    output.push_str("// Generated by portail amberify\n\n");

    // Add common imports
    output.push_str("import {{ read, write, exists }} from \"std/fs\"\n");
    output.push_str("import {{ trim, split, replace }} from \"std/text\"\n\n");

    // Convert shell constructs to Amber
    let lines: Vec<&str> = content.lines().collect();
    let mut in_function = false;
    #[allow(unused_variables)]
    let mut in_loop = false;
    #[allow(unused_variables)]
    let mut in_if = false;

    for line in lines.iter() {
        let trimmed = line.trim();

        // Skip shebang
        if trimmed.starts_with("#!") {
            continue;
        }

        // Convert comments
        if trimmed.starts_with("#") {
            let comment = trimmed.trim_start_matches('#').trim();
            output.push_str(&format!("// {}\n", comment));
            continue;
        }

        // Convert variable assignments
        if trimmed.contains("=") && !trimmed.starts_with("if") && !trimmed.starts_with("for") {
            let parts: Vec<&str> = trimmed.splitn(2, '=').collect();
            if parts.len() == 2 {
                let var_name = parts[0]
                    .trim()
                    .trim_start_matches("let ")
                    .trim_start_matches("local ");
                let value = parts[1].trim().trim_matches('"');

                // Check if it's a command substitution
                if value.starts_with("$(") && value.ends_with(')') {
                    let cmd = &value[2..value.len() - 1];
                    output.push_str(&format!("let {} = $ {} $\n", var_name, cmd));
                } else {
                    output.push_str(&format!("let {} = \"{}\"\n", var_name, value));
                }
                continue;
            }
        }

        // Convert echo
        if trimmed.starts_with("echo ") || trimmed.starts_with("echo(") {
            let content = if trimmed.starts_with("echo(") {
                trimmed
                    .strip_prefix("echo(")
                    .unwrap()
                    .strip_suffix(')')
                    .unwrap_or("")
            } else {
                trimmed.strip_prefix("echo ").unwrap_or("")
            };

            // Convert variable interpolation
            let converted = content.replace("${", "{").replace("$", "");

            output.push_str(&format!("echo({})\n", converted));
            continue;
        }

        // Convert function definitions
        if trimmed.starts_with("fun ") || trimmed.starts_with("function ") {
            in_function = true;
            let name = if trimmed.starts_with("fun ") {
                trimmed.strip_prefix("fun ").unwrap()
            } else {
                trimmed.strip_prefix("function ").unwrap()
            };
            output.push_str(&format!(
                "fun {} {{\n",
                name.trim_end_matches("(){").trim_end_matches("()")
            ));
            continue;
        }

        // Convert if statements
        if trimmed.starts_with("if ") {
            in_if = true;
            let condition = trimmed
                .strip_prefix("if ")
                .unwrap()
                .trim_end_matches("; then")
                .trim_end_matches("then");

            // Convert test conditions
            let converted = condition
                .replace("[ ", "")
                .replace(" ]", "")
                .replace(" -eq ", " == ")
                .replace(" -ne ", " != ")
                .replace(" -gt ", " > ")
                .replace(" -lt ", " < ")
                .replace(" -ge ", " >= ")
                .replace(" -le ", " <= ")
                .replace(" -z ", " len() == 0 ")
                .replace(" -n ", " len() > 0 ");

            output.push_str(&format!("if {} {{\n", converted));
            continue;
        }

        // Convert else
        if trimmed == "else" || trimmed == "else;" {
            output.push_str("} else {\n");
            continue;
        }

        // Convert fi
        if trimmed == "fi" || trimmed == "fi;" {
            in_if = false;
            output.push_str("}\n");
            continue;
        }

        // Convert for loops
        if trimmed.starts_with("for ") {
            in_loop = true;
            let parts: Vec<&str> = trimmed.split_whitespace().collect();
            if parts.len() >= 4 && parts[2] == "in" {
                let var_name = parts[1];
                let iterable = parts[3..]
                    .join(" ")
                    .trim_end_matches("; do")
                    .trim_end_matches("do")
                    .to_string();
                output.push_str(&format!("for {} in {} {{\n", var_name, iterable));
            }
            continue;
        }

        // Convert done
        if trimmed == "done" || trimmed == "done;" {
            in_loop = false;
            output.push_str("}\n");
            continue;
        }

        // Convert function end
        if trimmed == "}" && in_function {
            in_function = false;
            output.push_str("}\n\n");
            continue;
        }

        // Convert return
        if trimmed.starts_with("return ") {
            let value = trimmed.strip_prefix("return ").unwrap();
            output.push_str(&format!("return {}\n", value));
            continue;
        }

        // Convert exit
        if trimmed.starts_with("exit ") {
            let code = trimmed.strip_prefix("exit ").unwrap();
            output.push_str(&format!("exit {}\n", code));
            continue;
        }

        // Pass through other commands as shell commands
        if !trimmed.is_empty() && !trimmed.starts_with("//") {
            // Check if it's a command that should be wrapped
            if trimmed.starts_with("$ ") || trimmed.starts_with("${") {
                output.push_str(&format!("$ {} $\n", trimmed));
            } else {
                output.push_str(&format!("$ {} $\n", trimmed));
            }
        }
    }

    Ok(output)
}

/// Process a file or directory
pub fn process_path(input: &Path, output_dir: Option<&Path>) -> Result<()> {
    if input.is_dir() {
        // Process all .sh files in directory
        for entry in fs::read_dir(input)? {
            let entry = entry?;
            let path = entry.path();
            if path.extension().is_some_and(|ext| ext == "sh") {
                process_file(&path, output_dir)?;
            }
        }
    } else {
        process_file(input, output_dir)?;
    }
    Ok(())
}

/// Process a single file
fn process_file(input: &Path, output_dir: Option<&Path>) -> Result<()> {
    let amber_content = amberify(input)?;

    // Determine output path
    let output_path = if let Some(dir) = output_dir {
        let filename = input.file_stem().unwrap_or_default();
        dir.join(format!("{}.ab", filename.to_string_lossy()))
    } else {
        let mut output = input.to_path_buf();
        output.set_extension("ab");
        output
    };

    // Write output
    fs::write(&output_path, amber_content)?;
    println!("{}{}", input.display(), output_path.display());

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;

    #[test]
    fn test_basic_conversion() {
        // Create a temporary file
        let dir = std::env::temp_dir();
        let input_path = dir.join("test_input.sh");
        let mut file = fs::File::create(&input_path).unwrap();
        writeln!(file, "#!/bin/bash").unwrap();
        writeln!(file, "# This is a comment").unwrap();
        writeln!(file, "NAME=\"world\"").unwrap();
        writeln!(file, "echo \"Hello, ${{NAME}}!\"").unwrap();

        let result = amberify(&input_path).unwrap();
        assert!(result.contains("// This is a comment"));
        assert!(result.contains("let NAME"));
        assert!(result.contains("echo"));

        // Cleanup
        fs::remove_file(&input_path).unwrap();
    }
}