use std::path::{Path, PathBuf};
use std::process::Command;
use miette::Diagnostic;
use thiserror::Error;
#[derive(Debug, Error, Diagnostic)]
#[error("cannot initialize project: {message}")]
#[diagnostic(help("ensure you have write permissions and target files do not already exist"))]
pub struct InitError {
message: String,
}
impl InitError {
pub fn new(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
}
}
}
pub fn run(
path: Option<PathBuf>,
name: Option<String>,
mcu: Option<String>,
no_install: bool,
) -> Result<(), InitError> {
let base_dir = path.unwrap_or_else(|| PathBuf::from("."));
if !base_dir.exists() {
std::fs::create_dir_all(&base_dir)
.map_err(|e| InitError::new(format!("failed to create directory: {e}")))?;
}
let yml_path = base_dir.join("fusible.yml");
let yaml_path = base_dir.join("fusible.yaml");
if yml_path.exists() || yaml_path.exists() {
return Err(InitError::new(
"fusible.yml already exists in target directory",
));
}
let project_name = name.unwrap_or_else(|| {
base_dir
.canonicalize()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
.unwrap_or_else(|| "sensor-node".to_string())
});
let mcu_chip = mcu.unwrap_or_else(|| "esp32-s3".to_string());
let manifest_content = format!(
r#"name: {project_name}
hardware:
entry: hardware/index.tsx
out_dir: dist
port: 3020
mcu:
chip: {mcu_chip}
firmware:
path: apps/firmware
framework: esp-idf
pins_rs: apps/firmware/src/pins.rs
build_cmd: cargo test
"#
);
std::fs::write(&yml_path, manifest_content)
.map_err(|e| InitError::new(format!("failed to write fusible.yml: {e}")))?;
let hw_dir = base_dir.join("hardware");
std::fs::create_dir_all(&hw_dir)
.map_err(|e| InitError::new(format!("failed to create hardware directory: {e}")))?;
let pkg_json = format!(
r#"{{
"name": "{project_name}-hardware",
"version": "0.1.0",
"private": true,
"dependencies": {{
"@tscircuit/core": "^0.0.229",
"tscircuit": "^0.0.229"
}},
"devDependencies": {{
"@types/react": "^18.3.0"
}}
}}
"#
);
std::fs::write(hw_dir.join("package.json"), pkg_json)
.map_err(|e| InitError::new(format!("failed to write package.json: {e}")))?;
let tsconfig_json = r#"{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@tscircuit/core",
"strict": true,
"skipLibCheck": true
},
"include": ["**/*.ts", "**/*.tsx"]
}
"#;
std::fs::write(hw_dir.join("tsconfig.json"), tsconfig_json)
.map_err(|e| InitError::new(format!("failed to write tsconfig.json: {e}")))?;
let index_tsx = r#"export default () => (
<board width="40mm" height="30mm">
{/* Microcontroller */}
<chip
name="U1"
footprint="qfp32"
pinLabels={{
pin1: "GND",
pin2: "VDD3V3",
pin3: "GPIO0",
pin4: "GPIO1",
}}
/>
{/* Power Input Header */}
<chip
name="J1"
footprint="pinrow2"
pinLabels={{
pin1: "VDD3V3",
pin2: "GND",
}}
/>
{/* Status LED with Current Limiting */}
<resistor name="R_LED" resistance="1k" footprint="0603" />
{/* Power Rails */}
<trace from=".J1 > .VDD3V3" to="net.VDD3V3" />
<trace from=".J1 > .GND" to="net.GND" />
<trace from=".U1 > .VDD3V3" to="net.VDD3V3" />
<trace from=".U1 > .GND" to="net.GND" />
{/* Status LED Circuit */}
<trace from=".U1 > .GPIO0" to=".R_LED > .pin1" />
<trace from=".R_LED > .pin2" to="net.GND" />
</board>
);
"#;
std::fs::write(hw_dir.join("index.tsx"), index_tsx)
.map_err(|e| InitError::new(format!("failed to write index.tsx: {e}")))?;
let gitignore_path = base_dir.join(".gitignore");
if !gitignore_path.exists() {
let gitignore_content = "node_modules/\ndist/\n.tscircuit/\ntarget/\n*.kicad_prl\n*.bak\n";
let _ = std::fs::write(gitignore_path, gitignore_content);
}
if !no_install {
install_hardware_deps(&hw_dir);
}
println!("✨ Successfully initialized Fusible project \"{project_name}\"");
println!(" ├── fusible.yml (Fusible configuration & fuse contract)");
println!(" └── hardware/");
println!(" ├── index.tsx (tscircuit schematic + layout design)");
println!(" ├── package.json (tscircuit dependencies)");
println!(" └── tsconfig.json");
println!();
println!("Next steps:");
println!(" fusible dev Start interactive hot-reloading webviewer");
println!(
" fusible check Validate electrical design rules & extract firmware pins"
);
println!(" fusible export --preset cad Export KiCad project for Electrical Engineers");
println!(" fusible add <pkg> Install tscircuit packages or footprints");
println!(" fusible build Compile hardware artifacts & build firmware");
Ok(())
}
fn install_hardware_deps(hw_dir: &Path) {
println!("📦 Installing hardware dependencies...");
let runners = ["bun", "npm"];
for runner in runners {
let mut cmd = Command::new(runner);
cmd.arg("install").current_dir(hw_dir);
if let Ok(mut child) = cmd.spawn() {
let _ = child.wait();
return;
}
}
println!("⚠️ Neither bun nor npm found; run 'fusible doctor --install' or install dependencies manually.");
}