1use super::install::DependencyInstallOpts;
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use foundry_cli::utils::Git;
5use foundry_common::fs;
6use foundry_compilers::artifacts::remappings::Remapping;
7use foundry_config::Config;
8use std::path::{Path, PathBuf};
9use yansi::Paint;
10
11#[derive(Clone, Debug, clap::ValueEnum)]
13pub enum Networks {
14 Tempo,
15}
16
17#[derive(Clone, Debug, Default, Parser)]
19pub struct InitArgs {
20 #[arg(value_hint = ValueHint::DirPath, default_value = ".", value_name = "PATH")]
22 pub root: PathBuf,
23
24 #[arg(long, short)]
26 pub template: Option<String>,
27
28 #[arg(long, short, requires = "template")]
31 pub branch: Option<String>,
32
33 #[arg(long, conflicts_with = "template", visible_alias = "no-deps")]
35 pub offline: bool,
36
37 #[arg(long, conflicts_with = "template")]
39 pub force: bool,
40
41 #[arg(long, conflicts_with = "template")]
44 pub vscode: bool,
45
46 #[arg(long, conflicts_with = "template")]
48 pub vyper: bool,
49
50 #[arg(long, short, conflicts_with_all = &["vyper", "template"])]
52 pub network: Option<Networks>,
53
54 #[arg(long, conflicts_with = "template")]
57 pub use_parent_git: bool,
58
59 #[arg(long, conflicts_with = "template")]
61 pub empty: bool,
62
63 #[command(flatten)]
64 pub install: DependencyInstallOpts,
65}
66
67impl InitArgs {
68 pub async fn run(self) -> Result<()> {
69 let Self {
70 root,
71 template,
72 branch,
73 install,
74 offline,
75 force,
76 vscode,
77 use_parent_git,
78 vyper,
79 network,
80 empty,
81 } = self;
82 let DependencyInstallOpts { shallow, no_git, commit } = install;
83
84 let tempo = matches!(network, Some(Networks::Tempo));
85
86 if !root.exists() {
88 fs::create_dir_all(&root)?;
89 }
90 let root = dunce::canonicalize(root)?;
91 let git = Git::new(&root).shallow(shallow);
92
93 if let Some(template) = template {
97 let template = if template.contains("://") {
98 template
99 } else if template.starts_with("github.com/") {
100 "https://".to_string() + &template
101 } else {
102 "https://github.com/".to_string() + &template
103 };
104 sh_println!("Initializing {} from {}...", root.display(), template)?;
105 git.init()?;
107
108 git.fetch(true, &template, branch)?;
111
112 let commit_hash = git.commit_hash(true, "FETCH_HEAD")?;
115 let commit_msg = format!("chore: init from {template} at {commit_hash}");
117 let new_commit_hash = git.commit_tree("FETCH_HEAD^{tree}", Some(commit_msg))?;
119 git.reset(true, new_commit_hash)?;
121
122 if shallow {
124 git.submodule_init()?;
125 } else {
126 git.submodule_update(false, false, true, true, std::iter::empty::<PathBuf>())?;
128 }
129 } else {
130 if root.read_dir().is_ok_and(|mut i| i.next().is_some()) {
132 if !force {
133 eyre::bail!(
134 "Cannot run `init` on a non-empty directory.\n\
135 Run with the `--force` flag to initialize regardless."
136 );
137 }
138 sh_warn!("Target directory is not empty, but `--force` was specified")?;
139 }
140
141 if !no_git && commit && !force && git.is_in_repo()? {
143 git.ensure_clean()?;
144 }
145
146 sh_println!("Initializing {}...", root.display())?;
147
148 let src = root.join("src");
150 fs::create_dir_all(&src)?;
151
152 let test = root.join("test");
153 fs::create_dir_all(&test)?;
154
155 let script = root.join("script");
156 fs::create_dir_all(&script)?;
157
158 if !empty {
160 if vyper {
161 let contract_path = src.join("Counter.vy");
163 fs::write(
164 contract_path,
165 include_str!("../../assets/vyper/CounterTemplate.vy"),
166 )?;
167 let interface_path = src.join("ICounter.sol");
168 fs::write(
169 interface_path,
170 include_str!("../../assets/vyper/ICounterTemplate.sol"),
171 )?;
172
173 let contract_path = test.join("Counter.t.sol");
175 fs::write(
176 contract_path,
177 include_str!("../../assets/vyper/CounterTemplate.t.sol"),
178 )?;
179
180 let contract_path = script.join("Counter.s.sol");
182 fs::write(
183 contract_path,
184 include_str!("../../assets/vyper/CounterTemplate.s.sol"),
185 )?;
186 } else if tempo {
187 let contract_path = src.join("Mail.sol");
189 fs::write(contract_path, include_str!("../../assets/tempo/MailTemplate.sol"))?;
190
191 let contract_path = test.join("Mail.t.sol");
193 fs::write(
194 contract_path,
195 include_str!("../../assets/tempo/MailTemplate.t.sol"),
196 )?;
197
198 let contract_path = script.join("Mail.s.sol");
200 fs::write(
201 contract_path,
202 include_str!("../../assets/tempo/MailTemplate.s.sol"),
203 )?;
204 } else {
205 let contract_path = src.join("Counter.sol");
207 fs::write(
208 contract_path,
209 include_str!("../../assets/solidity/CounterTemplate.sol"),
210 )?;
211
212 let contract_path = test.join("Counter.t.sol");
214 fs::write(
215 contract_path,
216 include_str!("../../assets/solidity/CounterTemplate.t.sol"),
217 )?;
218
219 let contract_path = script.join("Counter.s.sol");
221 fs::write(
222 contract_path,
223 include_str!("../../assets/solidity/CounterTemplate.s.sol"),
224 )?;
225 }
226 }
227
228 let readme_path = root.join("README.md");
230 if tempo {
231 fs::write(readme_path, include_str!("../../assets/tempo/README.md"))?;
232 } else {
233 fs::write(readme_path, include_str!("../../assets/README.md"))?;
234 }
235
236 let dest = root.join(Config::FILE_NAME);
238 let mut config = Config::load_with_root(&root)?;
239 if !dest.exists() {
240 fs::write(dest, config.clone().into_basic().to_string_pretty()?)?;
241 }
242 let git = self.install.git(&config);
243
244 if !no_git {
246 init_git_repo(git, commit, use_parent_git, vyper, tempo)?;
247 }
248
249 if !offline {
251 if root.join("lib/forge-std").exists() {
252 sh_warn!("\"lib/forge-std\" already exists, skipping install...")?;
253 self.install.install(&mut config, vec![]).await?;
254 } else {
255 let dep = "https://github.com/foundry-rs/forge-std".parse()?;
256 self.install.install(&mut config, vec![dep]).await?;
257 }
258
259 if tempo {
261 if root.join("lib/tempo-std").exists() {
262 sh_warn!("\"lib/tempo-std\" already exists, skipping install...")?;
263 self.install.install(&mut config, vec![]).await?;
264 } else {
265 let dep = "https://github.com/tempoxyz/tempo-std".parse()?;
266 self.install.install(&mut config, vec![dep]).await?;
267 }
268 }
269 }
270
271 if vscode {
273 init_vscode(&root)?;
274 }
275 }
276
277 sh_println!("{}", " Initialized forge project".green())?;
278 Ok(())
279 }
280}
281
282fn init_git_repo(
289 git: Git<'_>,
290 commit: bool,
291 use_parent_git: bool,
292 vyper: bool,
293 tempo: bool,
294) -> Result<()> {
295 if !git.is_in_repo()? || (!use_parent_git && !git.is_repo_root()?) {
297 git.init()?;
298 }
299
300 let gitignore = git.root.join(".gitignore");
302 if !gitignore.exists() {
303 fs::write(gitignore, include_str!("../../assets/.gitignoreTemplate"))?;
304 }
305
306 let workflow = git.root.join(".github/workflows/test.yml");
308 if !workflow.exists() {
309 fs::create_dir_all(workflow.parent().unwrap())?;
310
311 if vyper {
312 fs::write(workflow, include_str!("../../assets/vyper/workflowTemplate.yml"))?;
313 } else if tempo {
314 fs::write(workflow, include_str!("../../assets/tempo/workflowTemplate.yml"))?;
315 } else {
316 fs::write(workflow, include_str!("../../assets/solidity/workflowTemplate.yml"))?;
317 }
318 }
319
320 if commit {
322 git.add(Some("--all"))?;
323 git.commit("chore: forge init")?;
324 }
325
326 Ok(())
327}
328
329fn init_vscode(root: &Path) -> Result<()> {
331 let remappings_file = root.join("remappings.txt");
332 if !remappings_file.exists() {
333 let mut remappings = Remapping::find_many(&root.join("lib"))
334 .into_iter()
335 .map(|r| r.into_relative(root).to_relative_remapping().to_string())
336 .collect::<Vec<_>>();
337 if !remappings.is_empty() {
338 remappings.sort();
339 let content = remappings.join("\n");
340 fs::write(remappings_file, content)?;
341 }
342 }
343
344 let vscode_dir = root.join(".vscode");
345 let settings_file = vscode_dir.join("settings.json");
346 let mut settings = if !vscode_dir.is_dir() {
347 fs::create_dir_all(&vscode_dir)?;
348 serde_json::json!({})
349 } else if settings_file.exists() {
350 foundry_compilers::utils::read_json_file(&settings_file)?
351 } else {
352 serde_json::json!({})
353 };
354
355 let obj = settings.as_object_mut().expect("Expected settings object");
356 let src_key = "solidity.packageDefaultDependenciesContractsDirectory";
358 if !obj.contains_key(src_key) {
359 obj.insert(src_key.to_string(), serde_json::Value::String("src".to_string()));
360 }
361 let lib_key = "solidity.packageDefaultDependenciesDirectory";
362 if !obj.contains_key(lib_key) {
363 obj.insert(lib_key.to_string(), serde_json::Value::String("lib".to_string()));
364 }
365
366 let content = serde_json::to_string_pretty(&settings)?;
367 fs::write(settings_file, content)?;
368
369 Ok(())
370}