use crate::commands::cicd::github;
use crate::commands::init::InitCommand;
use crate::error::Error;
use crate::project::Project;
use crate::runner::Runner;
use crate::writer::Writer;
use eyre::{eyre, WrapErr};
use kinetics_parser::Role;
use reqwest::Response;
use serde_json::json;
use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use toml_edit::{value, DocumentMut};
const CRON_TEMPLATE_URL: &str =
"https://github.com/ottofeller/kinetics-cron-template/archive/refs/heads/main.zip";
const ENDPOINT_TEMPLATE_URL: &str =
"https://github.com/ottofeller/kinetics-endpoint-template/archive/refs/heads/main.zip";
const WORKER_TEMPLATE_URL: &str =
"https://github.com/ottofeller/kinetics-worker-template/archive/refs/heads/main.zip";
pub(crate) struct InitRunner<'a> {
pub(super) command: InitCommand,
pub(super) dir: PathBuf,
pub(super) writer: &'a Writer,
}
impl<'a> Runner for InitRunner<'a> {
async fn run(&mut self) -> Result<(), Error> {
let function_role = if self.command.cron {
Role::Cron
} else if self.command.worker {
Role::Worker
} else {
Role::Endpoint
};
let is_git_enabled = !self.command.no_git;
self.set_dir()?;
self.writer.text(&format!(
"\n{} {} {}...\n",
console::style("Starting project").green().bold(),
console::style("in").dim(),
console::style(&self.dir.to_string_lossy()).bold()
))?;
fs::create_dir_all(&self.dir)
.wrap_err("Failed to create project directory")
.map_err(|e| self.error(None, None, Some(e.into())))?;
self.writer.text(&format!(
"\r\x1B[K{}",
console::style("Downloading template archive").dim()
))?;
let client = reqwest::Client::new();
let template_url = match function_role {
Role::Cron => CRON_TEMPLATE_URL,
Role::Worker => WORKER_TEMPLATE_URL,
Role::Endpoint => ENDPOINT_TEMPLATE_URL,
};
let response = match client.get(template_url).send().await {
Ok(resp) => {
if !resp.status().is_success() {
log::error!("Template server returned error: {resp:?}");
self.cleanup();
return Err(self.server_error(None));
}
resp
}
Err(e) => {
log::error!("Request to template server failed: {e:?}");
self.cleanup();
return Err(self.server_error(None));
}
};
self.writer.text(&format!(
"\r\x1B[K{}",
console::style("Extracting template").dim()
))?;
let unpack_result = self.unpack(response).await;
if unpack_result.is_err() {
self.cleanup();
return Err(self.error(
Some("Failed to unpack template archive"),
Some("Check if tar is installed and you have enough FS permissions."),
Some(unpack_result.err().unwrap().into()),
));
};
self.writer
.text(&format!("\r\x1B[K{}", console::style("Cleaning up").dim()))?;
let extracted_dir = self.dir.join(
template_url
.replace("https://github.com/ottofeller/", "")
.replace("/archive/refs/heads/main.zip", "-main"),
);
let status = Command::new("bash")
.args([
"-c",
&format!(
"mv {}/* {}",
extracted_dir.to_string_lossy(),
self.dir.to_string_lossy()
),
])
.status()
.wrap_err("Failed to move template files")
.map_err(|e| self.error(None, None, Some(e.into())))?;
if !status.success() {
self.cleanup();
return Err(self.error(
Some("Failed to move template files"),
Some("The bash command failed. Check file permissions."),
None,
));
}
fs::remove_dir_all(&extracted_dir).unwrap_or(());
self.writer.text(&format!(
"\r\x1B[K{}",
console::style("Renaming project").dim()
))?;
self.rename(&self.command.name)
.map_err(|e| self.error(
Some("Failed to update Cargo.toml"),
Some("Template might be corrupted (reach us at support@kineticscloud.com), or check file system permissions."),
Some(e.into())
))?;
self.writer.text(&format!("\r\x1B[K"))?;
if is_git_enabled {
self.init_git().map_err(|e| {
self.cleanup();
self.error(None, None, Some(e.into()))
})?;
}
self.writer
.text(&format!("{}\n", console::style("Done").bold().green()))?;
self.writer.json(json!({"success": true}))?;
Ok(())
}
}
impl<'a> InitRunner<'a> {
fn set_dir(&mut self) -> Result<(), Error> {
let dir = env::current_dir()
.wrap_err("Failed to determine current directory")
.map_err(|e| self.error(None, None, Some(e.into())))?
.join(&self.command.name);
if dir.exists() {
return Err(self.error(
Some(&format!("Directory '{}' already exists", dir.display())),
Some("Choose a different name or delete the existing directory."),
None,
));
}
self.dir = dir;
Ok(())
}
fn cleanup(&self) -> () {
fs::remove_dir_all(&self.dir).unwrap_or(())
}
fn rename(&self, name: &str) -> eyre::Result<()> {
let cargo_toml_path = self.dir.join("Cargo.toml");
let cargo_toml_content = fs::read_to_string(&cargo_toml_path)
.inspect_err(|e| log::error!("Can't read: {e:?}"))?;
let mut doc = cargo_toml_content
.parse::<DocumentMut>()
.inspect_err(|e| log::error!("Can't parse: {e:?}"))?;
let Some(package) = doc.get_mut("package") else {
log::error!("Missing [package] section");
return Err(eyre!("Invalid Cargo.toml format"));
};
let Some(package_table) = package.as_table_mut() else {
log::error!("Cargo.toml:package is not a table");
return Err(eyre!("Invalid Cargo.toml format"));
};
package_table["name"] = value(name);
let updated_content = doc.to_string();
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&cargo_toml_path)
.inspect_err(|e| log::error!("Can't open: {e:?}"))?;
file.write_all(updated_content.as_bytes())
.inspect_err(|e| log::error!("Can't write: {e:?}"))?;
Ok(())
}
async fn unpack(&self, response: Response) -> eyre::Result<()> {
let archive_bytes = response
.bytes()
.await
.inspect_err(|e| log::error!("Failed to read archive data: {e:?}"))?;
log::info!("Extracting template files...");
let temp_file_path = self.dir.join("template.tar.gz");
let mut temp_file = fs::File::create(&temp_file_path)
.inspect_err(|e| log::error!("Can't create tmp file: {e:?}"))?;
temp_file
.write_all(&archive_bytes)
.inspect_err(|e| log::error!("Can't write to tmp file: {e:?}"))?;
let status = Command::new("tar")
.args([
"xzf",
&temp_file_path.to_string_lossy(),
"-C",
&self.dir.to_string_lossy(),
])
.status()
.inspect_err(|e| log::error!("Can't run tar command: {e:?}"))?;
fs::remove_file(&temp_file_path).unwrap_or(());
if !status.success() {
log::error!("Can't unpack: {status:?}");
return Err(eyre!("Failed to extract template archive"));
}
Ok(())
}
fn init_git(&self) -> eyre::Result<()> {
let is_repo = Command::new("git")
.arg("rev-parse")
.arg("--is-inside-work-tree")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|exit_status| exit_status.success())
.unwrap_or_default();
if is_repo {
return Ok(());
}
log::info!("No git repo found. Init a new one.");
let status = Command::new("git")
.args(["init", "--quiet"])
.current_dir(&self.dir)
.status()
.inspect_err(|e| log::error!("Can't init git: {e:?}"))
.wrap_err(Error::new(
"Failed to init git",
Some("Make sure you have proper permissions."),
))?;
if !status.success() {
log::error!("Can't init git: {status:?}");
return Err(eyre!("Failed to init git"));
}
fs::write(self.dir.join(".gitignore"), "target/\n")
.inspect_err(|e| log::error!("Can't write .gitignore file: {:?}", e))
.wrap_err(Error::new(
"Failed to write .gitignore file",
Some("Check file system permissions."),
))?;
github::workflow(
&Project::from_path(self.dir.clone().into())?,
true,
self.writer,
)
}
}