use {
crate::{error::CliResult, style, utils},
std::{fs, path::Path},
};
pub fn run_instruction(name: &str) -> CliResult {
let snake = name.replace('-', "_");
if snake.is_empty()
|| snake.starts_with(|c: char| c.is_ascii_digit())
|| !snake.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
eprintln!(
" {}",
style::fail(&format!("invalid instruction name: \"{name}\""))
);
eprintln!(
" {}",
style::dim("must be a valid Rust identifier (e.g. transfer, create_pool)")
);
std::process::exit(1);
}
let instructions_dir = Path::new("src").join("instructions");
let lib_path = Path::new("src").join("lib.rs");
if !lib_path.exists() {
eprintln!(
" {}",
style::fail("src/lib.rs not found — are you in a Quasar project?")
);
std::process::exit(1);
}
if !instructions_dir.exists() {
fs::create_dir_all(&instructions_dir).map_err(anyhow::Error::from)?;
let lib_content = fs::read_to_string(&lib_path).map_err(anyhow::Error::from)?;
if !lib_content.contains("mod instructions;") {
let insert = "mod instructions;\nuse instructions::*;\n";
let updated = if let Some(pos) = lib_content.find("#[program]") {
let mut result = String::with_capacity(lib_content.len() + insert.len());
result.push_str(&lib_content[..pos]);
result.push_str(insert);
result.push('\n');
result.push_str(&lib_content[pos..]);
result
} else {
format!("{insert}\n{lib_content}")
};
fs::write(&lib_path, updated).map_err(anyhow::Error::from)?;
println!(" {} src/instructions/", style::success("created"));
}
}
let file_path = instructions_dir.join(format!("{snake}.rs"));
if file_path.exists() {
eprintln!(
" {}",
style::fail(&format!("src/instructions/{snake}.rs already exists"))
);
std::process::exit(1);
}
let pascal = utils::snake_to_pascal(&snake);
let content = format!(
r#"use quasar_lang::prelude::*;
#[derive(Accounts)]
pub struct {pascal}<'info> {{
pub payer: &'info mut Signer,
pub system_program: &'info Program<System>,
}}
impl<'info> {pascal}<'info> {{
#[inline(always)]
pub fn {snake}(&self) -> Result<(), ProgramError> {{
Ok(())
}}
}}
"#
);
fs::write(&file_path, content).map_err(anyhow::Error::from)?;
let mod_path = instructions_dir.join("mod.rs");
let existing_mod = fs::read_to_string(&mod_path).unwrap_or_default();
if !existing_mod.contains(&format!("mod {snake};")) {
let new_line = format!("mod {snake};\npub use {snake}::*;\n");
let updated = format!("{existing_mod}{new_line}");
fs::write(&mod_path, updated).map_err(anyhow::Error::from)?;
}
if lib_path.exists() {
let lib_content = fs::read_to_string(&lib_path).map_err(anyhow::Error::from)?;
if let Some(updated) = add_instruction_to_entrypoint(&lib_content, &snake, &pascal) {
fs::write(&lib_path, updated).map_err(anyhow::Error::from)?;
println!(" {} src/lib.rs", style::success("updated"));
}
}
println!(
" {} src/instructions/{snake}.rs",
style::success("created")
);
println!(" {} src/instructions/mod.rs", style::success("updated"));
Ok(())
}
fn add_instruction_to_entrypoint(lib_content: &str, snake: &str, pascal: &str) -> Option<String> {
let mut max_disc: i64 = -1;
for line in lib_content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("#[instruction(discriminator") {
if let Some(start) = trimmed.find("= ") {
if let Some(end) = trimmed[start + 2..].find(')') {
if let Ok(n) = trimmed[start + 2..start + 2 + end].trim().parse::<i64>() {
if n > max_disc {
max_disc = n;
}
}
}
}
}
}
let next_disc = (max_disc + 1) as u64;
let mut in_program = false;
let mut program_brace_depth = 0;
let mut insert_pos = None;
let mut pos = 0;
for line in lib_content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("#[program]") {
in_program = true;
}
if in_program {
for ch in trimmed.chars() {
if ch == '{' {
program_brace_depth += 1;
} else if ch == '}' {
program_brace_depth -= 1;
if program_brace_depth == 0 {
insert_pos = Some(pos);
break;
}
}
}
}
if insert_pos.is_some() {
break;
}
pos += line.len() + 1; }
let insert_pos = insert_pos?;
let new_entry = format!(
"\n #[instruction(discriminator = {next_disc})]\n pub fn {snake}(ctx: \
Ctx<{pascal}>) -> Result<(), ProgramError> {{\n ctx.accounts.{snake}()\n }}\n"
);
let mut result = String::with_capacity(lib_content.len() + new_entry.len());
result.push_str(&lib_content[..insert_pos]);
result.push_str(&new_entry);
result.push_str(&lib_content[insert_pos..]);
Some(result)
}
pub fn run_state(name: &str) -> CliResult {
let snake = name.replace('-', "_");
if snake.is_empty()
|| snake.starts_with(|c: char| c.is_ascii_digit())
|| !snake.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
eprintln!(
" {}",
style::fail(&format!("invalid state name: \"{name}\""))
);
eprintln!(
" {}",
style::dim("must be a valid Rust identifier (e.g. vault, user_profile)")
);
std::process::exit(1);
}
let pascal = utils::snake_to_pascal(&snake);
let state_path = Path::new("src").join("state.rs");
let already_exists = state_path.exists();
if already_exists {
let existing = fs::read_to_string(&state_path).map_err(anyhow::Error::from)?;
let mut max_disc: i64 = 0;
for line in existing.lines() {
let trimmed = line.trim();
if trimmed.starts_with("#[account(discriminator") {
if let Some(start) = trimmed.find("= ") {
if let Some(end) = trimmed[start + 2..].find(')') {
if let Ok(n) = trimmed[start + 2..start + 2 + end].trim().parse::<i64>() {
if n > max_disc {
max_disc = n;
}
}
}
}
}
}
let next_disc = max_disc + 1;
let new_struct = format!(
"\n#[account(discriminator = {next_disc})]\npub struct {pascal} {{\n pub \
authority: Address,\n}}\n"
);
let updated = format!("{existing}{new_struct}");
fs::write(&state_path, updated).map_err(anyhow::Error::from)?;
} else {
let content = format!(
r#"use quasar_lang::prelude::*;
#[account(discriminator = 1)]
pub struct {pascal} {{
pub authority: Address,
}}
"#
);
fs::write(&state_path, content).map_err(anyhow::Error::from)?;
}
println!(
" {} src/state.rs ({})",
style::success(if already_exists { "updated" } else { "created" }),
pascal,
);
Ok(())
}
pub fn run_error(name: &str) -> CliResult {
let snake = name.replace('-', "_");
if snake.is_empty()
|| snake.starts_with(|c: char| c.is_ascii_digit())
|| !snake.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
eprintln!(
" {}",
style::fail(&format!("invalid error name: \"{name}\""))
);
eprintln!(
" {}",
style::dim("must be a valid Rust identifier (e.g. vault_error, access_error)")
);
std::process::exit(1);
}
let pascal = utils::snake_to_pascal(&snake);
let errors_path = Path::new("src").join("errors.rs");
let already_exists = errors_path.exists();
if already_exists {
let existing = fs::read_to_string(&errors_path).map_err(anyhow::Error::from)?;
let new_enum = format!("\n#[error_code]\npub enum {pascal} {{\n Unknown,\n}}\n");
let updated = format!("{existing}{new_enum}");
fs::write(&errors_path, updated).map_err(anyhow::Error::from)?;
} else {
let content = format!(
r#"use quasar_lang::prelude::*;
#[error_code]
pub enum {pascal} {{
Unknown,
}}
"#
);
fs::write(&errors_path, content).map_err(anyhow::Error::from)?;
}
println!(
" {} src/errors.rs ({})",
style::success(if already_exists { "updated" } else { "created" }),
pascal,
);
Ok(())
}