Skip to main content

geam_cli/
lib.rs

1mod builtin;
2mod cargo;
3mod command;
4mod embedding;
5mod error;
6mod process;
7mod progress;
8mod project;
9mod provider;
10mod runner;
11mod standalone;
12
13use clap::Parser;
14use command::{
15    Cli, Command, EmbeddingCommand, EntryCommand, Provider, ProviderCommand, RunCommand,
16};
17use error::CliError;
18use std::env;
19use std::io::Write;
20use std::process::ExitCode;
21
22pub fn run() -> ExitCode {
23    let cli = Cli::parse();
24    let result = env::current_dir()
25        .map_err(CliError::CurrentDirectory)
26        .and_then(project::into_utf8_path)
27        .and_then(|current_directory| run_command(cli, current_directory));
28    match result {
29        Ok(()) => ExitCode::SUCCESS,
30        Err(error) => {
31            // A closed stderr must not turn the original failure into a panic.
32            let _ = writeln!(std::io::stderr(), "geam: {error}");
33            ExitCode::FAILURE
34        }
35    }
36}
37
38fn run_command(cli: Cli, current_directory: camino::Utf8PathBuf) -> Result<(), CliError> {
39    let command = match cli.command {
40        Command::Embedding(command) => match command.command {
41            EmbeddingCommand::Init => return embedding::init(&current_directory),
42            EmbeddingCommand::Check => {
43                return embedding::check(&current_directory);
44            }
45            EmbeddingCommand::Sync => return embedding::sync(&current_directory),
46        },
47        Command::Prepare(command) => ProjectCommand::Prepare(command),
48        Command::Run(command) => ProjectCommand::Run(command),
49        Command::Provider(command) => ProjectCommand::Provider(command),
50    };
51    run_project_command(command, current_directory)
52}
53
54enum ProjectCommand {
55    Prepare(EntryCommand),
56    Run(RunCommand),
57    Provider(Provider),
58}
59
60fn run_project_command(
61    command: ProjectCommand,
62    current_directory: camino::Utf8PathBuf,
63) -> Result<(), CliError> {
64    let project_root = project::find_project_root(&current_directory)?;
65    match command {
66        ProjectCommand::Prepare(command) => project::entry_module(&project_root, command.module)
67            .and_then(|module| standalone::prepare(&project_root, module)),
68        ProjectCommand::Run(command) => project::entry_module(&project_root, command.module)
69            .and_then(|module| {
70                standalone::run(
71                    &project_root,
72                    &current_directory,
73                    module,
74                    command.provider_configs,
75                )
76            }),
77        ProjectCommand::Provider(command) => match command.command {
78            ProviderCommand::Add(command) => {
79                provider::add(&project_root, current_directory.as_std_path(), command)
80            }
81            ProviderCommand::List => provider::list(&project_root),
82            ProviderCommand::Remove(command) => provider::remove(&project_root, command),
83        },
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::{Cli, run_command};
90    use crate::error::CliError;
91    use camino::Utf8PathBuf;
92    use clap::Parser;
93    use std::fs;
94    use tempfile::tempdir;
95
96    #[test]
97    fn routes_embedding_before_gleam_project_discovery() {
98        let directory = tempdir().expect("temporary directory should be created");
99        let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf())
100            .expect("temporary path should be valid UTF-8");
101        for operation in ["init", "check", "sync"] {
102            let error = run_command(
103                Cli::try_parse_from(["geam", "embedding", operation])
104                    .expect("embedding command should parse"),
105                root.clone(),
106            )
107            .expect_err("missing Cargo manifest should fail");
108
109            assert!(matches!(
110                error,
111                CliError::CargoManifestNotFound { start } if start == root
112            ));
113        }
114    }
115
116    #[test]
117    fn preserves_entry_resolution_failures_for_prepare_and_run() {
118        let project = tempdir().expect("temporary project should be created");
119        fs::write(project.path().join("gleam.toml"), "invalid")
120            .expect("invalid config should be written");
121        let root = Utf8PathBuf::from_path_buf(project.path().to_path_buf())
122            .expect("temporary path should be valid UTF-8");
123
124        for arguments in [vec!["geam", "prepare"], vec!["geam", "run"]] {
125            let error = run_command(
126                Cli::try_parse_from(arguments).expect("command should parse"),
127                root.clone(),
128            )
129            .expect_err("entry resolution should fail");
130            assert!(matches!(
131                error,
132                CliError::InvalidToml { kind, path, reason }
133                    if kind == "Gleam package config"
134                        && path == root.join("gleam.toml")
135                        && reason.contains("expected")
136            ));
137        }
138    }
139
140    #[test]
141    fn preserves_project_compilation_failures_for_prepare_and_run() {
142        let project = tempdir().expect("temporary project should be created");
143        fs::create_dir(project.path().join("src")).expect("source directory should be created");
144        fs::write(
145            project.path().join("gleam.toml"),
146            "name = \"application\"\nversion = \"1.0.0\"\n",
147        )
148        .expect("package config should be written");
149        fs::write(
150            project.path().join("manifest.toml"),
151            "packages = []\n[requirements]\n",
152        )
153        .expect("manifest should be written");
154        fs::write(
155            project.path().join("src/application.gleam"),
156            "pub fn main() { 1 }\n",
157        )
158        .expect("source should be written");
159        let root = Utf8PathBuf::from_path_buf(project.path().to_path_buf())
160            .expect("temporary path should be valid UTF-8");
161
162        for arguments in [
163            vec!["geam", "prepare", "--module", "missing"],
164            vec!["geam", "run", "--module", "missing"],
165        ] {
166            let error = run_command(
167                Cli::try_parse_from(arguments).expect("command should parse"),
168                root.clone(),
169            )
170            .expect_err("missing entry module should fail");
171            assert!(matches!(
172                error,
173                CliError::Project(geam_core::ProjectError::Frontend(
174                    geam_core::FrontendError::MissingRootModule { package, module }
175                )) if package == "application" && module == "missing"
176            ));
177        }
178    }
179}