Skip to main content

quasar_cli/
new.rs

1use {
2    crate::{error::CliResult, style, utils},
3    std::{fs, path::Path},
4};
5
6pub fn run_instruction(name: &str) -> CliResult {
7    let snake = name.replace('-', "_");
8
9    // Validate: must be a valid Rust identifier (ascii alphanumeric + underscore,
10    // not starting with digit)
11    if snake.is_empty()
12        || snake.starts_with(|c: char| c.is_ascii_digit())
13        || !snake.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
14    {
15        eprintln!(
16            "  {}",
17            style::fail(&format!("invalid instruction name: \"{name}\""))
18        );
19        eprintln!(
20            "  {}",
21            style::dim("must be a valid Rust identifier (e.g. transfer, create_pool)")
22        );
23        std::process::exit(1);
24    }
25
26    let instructions_dir = Path::new("src").join("instructions");
27    let lib_path = Path::new("src").join("lib.rs");
28
29    if !lib_path.exists() {
30        eprintln!(
31            "  {}",
32            style::fail("src/lib.rs not found — are you in a Quasar project?")
33        );
34        std::process::exit(1);
35    }
36
37    // Create instructions directory if it doesn't exist (minimal template)
38    if !instructions_dir.exists() {
39        fs::create_dir_all(&instructions_dir).map_err(anyhow::Error::from)?;
40
41        // Wire up `mod instructions;` and `use instructions::*;` in lib.rs
42        let lib_content = fs::read_to_string(&lib_path).map_err(anyhow::Error::from)?;
43        if !lib_content.contains("mod instructions;") {
44            // Insert after the last `use` or `mod` line at the top
45            let insert = "mod instructions;\nuse instructions::*;\n";
46            let updated = if let Some(pos) = lib_content.find("#[program]") {
47                let mut result = String::with_capacity(lib_content.len() + insert.len());
48                result.push_str(&lib_content[..pos]);
49                result.push_str(insert);
50                result.push('\n');
51                result.push_str(&lib_content[pos..]);
52                result
53            } else {
54                format!("{insert}\n{lib_content}")
55            };
56            fs::write(&lib_path, updated).map_err(anyhow::Error::from)?;
57            println!("  {} src/instructions/", style::success("created"));
58        }
59    }
60
61    let file_path = instructions_dir.join(format!("{snake}.rs"));
62    if file_path.exists() {
63        eprintln!(
64            "  {}",
65            style::fail(&format!("src/instructions/{snake}.rs already exists"))
66        );
67        std::process::exit(1);
68    }
69
70    // Write the instruction file
71    let pascal = utils::snake_to_pascal(&snake);
72    let content = format!(
73        r#"use quasar_lang::prelude::*;
74
75#[derive(Accounts)]
76pub struct {pascal}<'info> {{
77    pub payer: &'info mut Signer,
78    pub system_program: &'info Program<System>,
79}}
80
81impl<'info> {pascal}<'info> {{
82    #[inline(always)]
83    pub fn {snake}(&self) -> Result<(), ProgramError> {{
84        Ok(())
85    }}
86}}
87"#
88    );
89    fs::write(&file_path, content).map_err(anyhow::Error::from)?;
90
91    // Update mod.rs
92    let mod_path = instructions_dir.join("mod.rs");
93    let existing_mod = fs::read_to_string(&mod_path).unwrap_or_default();
94
95    if !existing_mod.contains(&format!("mod {snake};")) {
96        let new_line = format!("mod {snake};\npub use {snake}::*;\n");
97        let updated = format!("{existing_mod}{new_line}");
98        fs::write(&mod_path, updated).map_err(anyhow::Error::from)?;
99    }
100
101    // Update lib.rs — add instruction to #[program] block
102    if lib_path.exists() {
103        let lib_content = fs::read_to_string(&lib_path).map_err(anyhow::Error::from)?;
104        if let Some(updated) = add_instruction_to_entrypoint(&lib_content, &snake, &pascal) {
105            fs::write(&lib_path, updated).map_err(anyhow::Error::from)?;
106            println!("  {} src/lib.rs", style::success("updated"));
107        }
108    }
109
110    println!(
111        "  {} src/instructions/{snake}.rs",
112        style::success("created")
113    );
114    println!("  {} src/instructions/mod.rs", style::success("updated"));
115
116    Ok(())
117}
118
119/// Find the highest discriminator in the #[program] block and insert
120/// a new #[instruction] entry with discriminator = max + 1.
121fn add_instruction_to_entrypoint(lib_content: &str, snake: &str, pascal: &str) -> Option<String> {
122    // Find the highest existing discriminator
123    let mut max_disc: i64 = -1;
124    for line in lib_content.lines() {
125        let trimmed = line.trim();
126        if trimmed.starts_with("#[instruction(discriminator") {
127            if let Some(start) = trimmed.find("= ") {
128                if let Some(end) = trimmed[start + 2..].find(')') {
129                    if let Ok(n) = trimmed[start + 2..start + 2 + end].trim().parse::<i64>() {
130                        if n > max_disc {
131                            max_disc = n;
132                        }
133                    }
134                }
135            }
136        }
137    }
138
139    let next_disc = (max_disc + 1) as u64;
140
141    // Find the closing `}}` of the #[program] mod block.
142    // Strategy: find the last `}` that closes the program module.
143    // We look for the pattern: a line with just `}` or `}}` that ends the mod
144    // block. The program block ends with a `}` at indent level 0 after
145    // `#[program]`.
146    let mut in_program = false;
147    let mut program_brace_depth = 0;
148    let mut insert_pos = None;
149
150    let mut pos = 0;
151    for line in lib_content.lines() {
152        let trimmed = line.trim();
153
154        if trimmed.starts_with("#[program]") {
155            in_program = true;
156        }
157
158        if in_program {
159            for ch in trimmed.chars() {
160                if ch == '{' {
161                    program_brace_depth += 1;
162                } else if ch == '}' {
163                    program_brace_depth -= 1;
164                    if program_brace_depth == 0 {
165                        // This `}` closes the program mod — insert before this line
166                        insert_pos = Some(pos);
167                        break;
168                    }
169                }
170            }
171        }
172
173        if insert_pos.is_some() {
174            break;
175        }
176
177        pos += line.len() + 1; // +1 for newline
178    }
179
180    let insert_pos = insert_pos?;
181
182    let new_entry = format!(
183        "\n    #[instruction(discriminator = {next_disc})]\n    pub fn {snake}(ctx: \
184         Ctx<{pascal}>) -> Result<(), ProgramError> {{\n        ctx.accounts.{snake}()\n    }}\n"
185    );
186
187    let mut result = String::with_capacity(lib_content.len() + new_entry.len());
188    result.push_str(&lib_content[..insert_pos]);
189    result.push_str(&new_entry);
190    result.push_str(&lib_content[insert_pos..]);
191    Some(result)
192}
193
194pub fn run_state(name: &str) -> CliResult {
195    let snake = name.replace('-', "_");
196
197    if snake.is_empty()
198        || snake.starts_with(|c: char| c.is_ascii_digit())
199        || !snake.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
200    {
201        eprintln!(
202            "  {}",
203            style::fail(&format!("invalid state name: \"{name}\""))
204        );
205        eprintln!(
206            "  {}",
207            style::dim("must be a valid Rust identifier (e.g. vault, user_profile)")
208        );
209        std::process::exit(1);
210    }
211
212    let pascal = utils::snake_to_pascal(&snake);
213    let state_path = Path::new("src").join("state.rs");
214    let already_exists = state_path.exists();
215
216    if already_exists {
217        let existing = fs::read_to_string(&state_path).map_err(anyhow::Error::from)?;
218
219        // Find the highest existing discriminator in state.rs
220        let mut max_disc: i64 = 0;
221        for line in existing.lines() {
222            let trimmed = line.trim();
223            if trimmed.starts_with("#[account(discriminator") {
224                if let Some(start) = trimmed.find("= ") {
225                    if let Some(end) = trimmed[start + 2..].find(')') {
226                        if let Ok(n) = trimmed[start + 2..start + 2 + end].trim().parse::<i64>() {
227                            if n > max_disc {
228                                max_disc = n;
229                            }
230                        }
231                    }
232                }
233            }
234        }
235
236        let next_disc = max_disc + 1;
237        let new_struct = format!(
238            "\n#[account(discriminator = {next_disc})]\npub struct {pascal} {{\n    pub \
239             authority: Address,\n}}\n"
240        );
241
242        let updated = format!("{existing}{new_struct}");
243        fs::write(&state_path, updated).map_err(anyhow::Error::from)?;
244    } else {
245        let content = format!(
246            r#"use quasar_lang::prelude::*;
247
248#[account(discriminator = 1)]
249pub struct {pascal} {{
250    pub authority: Address,
251}}
252"#
253        );
254        fs::write(&state_path, content).map_err(anyhow::Error::from)?;
255    }
256
257    println!(
258        "  {} src/state.rs ({})",
259        style::success(if already_exists { "updated" } else { "created" }),
260        pascal,
261    );
262
263    Ok(())
264}
265
266pub fn run_error(name: &str) -> CliResult {
267    let snake = name.replace('-', "_");
268
269    if snake.is_empty()
270        || snake.starts_with(|c: char| c.is_ascii_digit())
271        || !snake.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
272    {
273        eprintln!(
274            "  {}",
275            style::fail(&format!("invalid error name: \"{name}\""))
276        );
277        eprintln!(
278            "  {}",
279            style::dim("must be a valid Rust identifier (e.g. vault_error, access_error)")
280        );
281        std::process::exit(1);
282    }
283
284    let pascal = utils::snake_to_pascal(&snake);
285    let errors_path = Path::new("src").join("errors.rs");
286    let already_exists = errors_path.exists();
287
288    if already_exists {
289        let existing = fs::read_to_string(&errors_path).map_err(anyhow::Error::from)?;
290
291        let new_enum = format!("\n#[error_code]\npub enum {pascal} {{\n    Unknown,\n}}\n");
292
293        let updated = format!("{existing}{new_enum}");
294        fs::write(&errors_path, updated).map_err(anyhow::Error::from)?;
295    } else {
296        let content = format!(
297            r#"use quasar_lang::prelude::*;
298
299#[error_code]
300pub enum {pascal} {{
301    Unknown,
302}}
303"#
304        );
305        fs::write(&errors_path, content).map_err(anyhow::Error::from)?;
306    }
307
308    println!(
309        "  {} src/errors.rs ({})",
310        style::success(if already_exists { "updated" } else { "created" }),
311        pascal,
312    );
313
314    Ok(())
315}