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 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(¤t_directory),
42 EmbeddingCommand::Check => {
43 return embedding::check(¤t_directory);
44 }
45 EmbeddingCommand::Sync => return embedding::sync(¤t_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(¤t_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 ¤t_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}