use kcl_lang::{API, ExecProgramArgs, ExternalPkg}; use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; use nu_plugin::{MsgPackSerializer, Plugin, PluginCommand, serve_plugin}; use nu_protocol::IntoPipelineData; use nu_protocol::{ Category, Example, LabeledError, PipelineData, Signature, SyntaxShape, Type, Value, }; use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; /// Nushell plugin for executing KCL files and inline KCL code. /// /// This plugin integrates [KCL](https://kcl-lang.io/) (Kusion Configuration Language) with [Nushell](https://www.nushell.sh/), allowing you to execute KCL files or inline KCL code directly from your Nushell pipeline. /// /// # Provided Commands /// /// - `kcl-exec`: Execute one or more KCL files and return the result as YAML. Accepts an array of file paths and an optional `--work_dir` flag to specify the working directory. /// - `kcl-run`: Execute inline KCL code from the pipeline and return the result as YAML. /// /// # Usage /// /// ## kcl-exec /// /// Run multiple KCL files in the current directory: /// /// ```shell /// kcl-exec [settings.k defs/aws_defaults.k] /// ``` /// /// Run multiple KCL files from any directory, specifying the project root: /// /// ```shell /// kcl-exec [settings.k defs/aws_defaults.k] --work_dir /path/to/project /// ``` /// /// Run a single KCL file: /// /// ```shell /// kcl-exec [settings.k] --work_dir /path/to/project /// ``` /// /// ## kcl-run /// /// Run inline KCL code: /// /// ```shell /// 'a = 1' | kcl-run /// ``` /// /// Run a KCL file from the pipeline: /// /// ```shell /// open settings.k | kcl-run /// ``` /// /// # Limitations /// /// - The `kcl-exec` command requires the specified files to exist and be accessible. /// - The `kcl-run` command expects a string of KCL code from the pipeline input. /// - Example-based tests that require actual files or pipeline input may not run in all test environments. /// /// # Resources /// /// - [KCL Language Documentation](https://kcl-lang.io/docs/) /// - [Nushell Plugin Development](https://www.nushell.sh/book/plugins.html) pub struct KclPlugin; impl Plugin for KclPlugin { fn version(&self) -> String { // This automatically uses the version of your package from Cargo.toml as the plugin version // sent to Nushell env!("CARGO_PKG_VERSION").into() } fn commands(&self) -> Vec>> { vec![ // Commands should be added here Box::new(KclExec), Box::new(KclValidate), ] } } /// Command to execute KCL files from a file path. /// /// This command takes a path to a KCL file and executes it, returning the result as YAML. /// /// # Examples /// /// ```shell /// kcl-exec ./src/myfile.k /// ``` /// /// # Arguments /// /// - `file`: Path to the KCL file to execute (required). /// - `work_dir`: Optional working directory for KCL execution. /// /// # Limitations /// /// The file must exist and be readable by the plugin process. /// - If `--work_dir` is not provided, the plugin will automatically search for `kcl.mod` upwards from the first file in the files array and use its directory as the working directory. pub struct KclExec; impl SimplePluginCommand for KclExec { type Plugin = KclPlugin; fn name(&self) -> &str { "kcl-exec" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::Any, Type::String) .required( "files", SyntaxShape::List(Box::new(SyntaxShape::Filepath)), "KCL files to execute (array)", ) .named( "work_dir", SyntaxShape::Directory, "Work directory", Some('w'), ) .category(Category::Experimental) } fn description(&self) -> &str { "Exec KCL files and return result in YAML" } fn examples(&self) -> Vec { let examples: Vec = vec![Example { example: "kcl-exec [settings.k defs/aws_defaults.k] --work_dir .", description: "Execute multiple KCL files in the current directory", result: None, }]; examples } fn run( &self, _plugin: &KclPlugin, _engine: &EngineInterface, call: &EvaluatedCall, _input: &Value, ) -> Result { let files: Vec = call.req(0)?; let work_dir: Option = call.get_flag("work_dir")?; match exec_kcl_file(&files, work_dir.as_deref()) { Ok(yaml) => Ok(Value::string(yaml, call.head)), Err(e) => Err(LabeledError::new("KCL code execution error").with_label(e, call.head)), } } } /// Command to execute inline KCL code from the Nushell pipeline. /// /// This command takes a string of KCL code from the pipeline and executes it, returning the result as YAML. /// /// # Example /// /// ```shell /// open myfile.k | kcl-run /// ``` /// /// # Arguments /// /// - `filename`: Optional virtual filename for the KCL code (default: `main.k`). /// /// # Limitations /// /// The input must be a string containing valid KCL code. struct KclValidate; impl PluginCommand for KclValidate { type Plugin = KclPlugin; fn name(&self) -> &str { "kcl-run" } fn description(&self) -> &str { "Exec KCL inline code from pipeline" } fn signature(&self) -> Signature { Signature::build(PluginCommand::name(self)) .input_output_type(Type::String, Type::String) .optional( "filename", SyntaxShape::String, "Nombre virtual del archivo", ) .category(Category::Experimental) } fn run( &self, _plugin: &KclPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: PipelineData, ) -> Result { let filename = call .opt::(0)? .unwrap_or_else(|| "main.k".to_string()); let value = input.into_value(call.head); let code = match value { Ok(Value::String { val, .. }) => val, Ok(_) => { return Err(LabeledError::new("KCL code expected as string") .with_label("Use a string as KCL code", call.head)); } Err(e) => { return Err(LabeledError::new("Failed to extract input value") .with_label(e.to_string(), call.head)); } }; match exec_kcl_inline(&code, &filename) { Ok(yaml) => Ok(Value::string(yaml, call.head).into_pipeline_data()), Err(e) => Err(LabeledError::new("KCL code execution error").with_label(e, call.head)), } } fn examples(&self) -> Vec { vec![Example { example: "'a = 1' | kcl-run", description: "Run inline KCL code from the pipeline", result: None, }] } } fn main() { serve_plugin(&KclPlugin, MsgPackSerializer); } fn exec_kcl_file(file_paths: &[String], work_dir: Option<&str>) -> Result { let work_dir = match work_dir { Some(dir) => dir.to_string(), None => { // Try to find kcl.mod upwards from the first file if let Some(first_file) = file_paths.get(0) { match find_kcl_mod_dir(first_file) { Some(dir) => dir, None => return Err("No kcl.mod found in parent directories of the first file, and --work_dir not provided".to_string()), } } else { return Err("No files provided to execute".to_string()); } } }; let external_pkgs = resolve_dependencies_toml(&work_dir)?; // Debug: mostrar dependencias resueltas for pkg in &external_pkgs { println!("Dependencies: {} -> {}", pkg.pkg_name, pkg.pkg_path); println!("Existe: {}", Path::new(&pkg.pkg_path).exists()); } let api = API::default(); let args = ExecProgramArgs { k_filename_list: file_paths.to_vec(), work_dir, ..Default::default() }; match api.exec_program(&args) { Ok(result) => Ok(result.yaml_result), Err(e) => Err(e.to_string()), } } /// Search upwards from the given file for a kcl.mod file and return its directory as a String. fn find_kcl_mod_dir>(start_file: P) -> Option { let mut dir = PathBuf::from(start_file.as_ref()).parent()?.to_path_buf(); loop { let kcl_mod = dir.join("kcl.mod"); if kcl_mod.exists() { return Some(dir.to_string_lossy().to_string()); } if !dir.pop() { break; } } None } fn exec_kcl_inline(code: &str, filename: &str) -> Result { let api = API::default(); let args = ExecProgramArgs { k_filename_list: vec![filename.to_string()], k_code_list: vec![code.to_string()], ..Default::default() }; match api.exec_program(&args) { Ok(result) => Ok(result.yaml_result), Err(e) => Err(e.to_string()), } } #[allow(dead_code)] #[derive(Deserialize)] struct KclMod { package: Package, dependencies: Option>, } #[allow(dead_code)] #[derive(Deserialize)] struct Package { name: String, edition: String, version: String, } #[allow(dead_code)] #[derive(Deserialize)] struct Dependency { path: Option, version: String, } fn resolve_dependencies_toml(work_dir: &str) -> Result, String> { let kcl_mod_path = Path::new(work_dir).join("kcl.mod"); if !kcl_mod_path.exists() { return Ok(vec![]); } let content = std::fs::read_to_string(&kcl_mod_path) .map_err(|e| format!("Error reading kcl.mod: {}", e))?; let kcl_mod: KclMod = toml::from_str(&content).map_err(|e| format!("Error parsing kcl.mod: {}", e))?; let mut external_pkgs = Vec::new(); if let Some(dependencies) = kcl_mod.dependencies { for (pkg_name, dep) in dependencies { if let Some(relative_path) = dep.path { let absolute_path = Path::new(work_dir) .join(relative_path) .canonicalize() .map_err(|e| format!("Error resolving path for {}: {}", pkg_name, e))? .to_string_lossy() .to_string(); external_pkgs.push(ExternalPkg { pkg_name, pkg_path: absolute_path, }); } } } dbg!(&external_pkgs); Ok(external_pkgs) } /// Test all examples for the `kcl-exec` command. /// /// This test runs the examples provided in the `examples` method of `KclExec`. /// Note: Examples that require actual files may not pass unless the files exist. #[test] fn test_examples() -> Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; PluginTest::new("kcl", KclPlugin.into())?.test_command_examples(&KclExec) } /// Test all examples for the `kcl-run` command. /// /// This test runs the examples provided in the `examples` method of `KclValidate`. /// Note: Examples that require pipeline input may not be fully testable in all environments. #[test] fn test_kcl_validate_examples() -> Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; PluginTest::new("kcl", KclPlugin.into())?.test_command_examples(&KclValidate) } /// Entry point for the KCL Nushell plugin. /// /// This function starts the plugin and serves it to Nushell using message pack serialization. #[cfg(test)] mod unit_tests { use super::*; use std::fs; use std::io::Write; extern crate tempfile; use tempfile::tempdir; #[test] fn test_exec_kcl_file_nonexistent() { let result = exec_kcl_file(&vec!["/tmp/does_not_exist.k".to_string()], None); assert!(result.is_err()); } #[test] fn test_exec_kcl_inline_invalid_code() { let result = exec_kcl_inline("not valid kcl", "main.k"); assert!(result.is_err()); } #[test] fn test_resolve_dependencies_toml_success() { let dir = tempdir().unwrap(); let dep_dir = dir.path().join("dep"); fs::create_dir_all(&dep_dir).unwrap(); let kcl_mod_content = r#" [package] name = "testpkg" edition = "v0.9.0" version = "0.1.0" [dependencies] foo = { path = "dep" , version = "0.1.0" } "#; let kcl_mod_path = dir.path().join("kcl.mod"); let mut file = fs::File::create(&kcl_mod_path).unwrap(); file.write_all(kcl_mod_content.as_bytes()).unwrap(); let pkgs = resolve_dependencies_toml(dir.path().to_str().unwrap()).unwrap(); assert_eq!(pkgs.len(), 1); assert_eq!(pkgs[0].pkg_name, "foo"); assert!(pkgs[0].pkg_path.ends_with("dep")); } #[test] fn test_resolve_dependencies_toml_no_kcl_mod() { let dir = tempdir().unwrap(); let pkgs = resolve_dependencies_toml(dir.path().to_str().unwrap()).unwrap(); assert!(pkgs.is_empty()); } }