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 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 if !instructions_dir.exists() {
39 fs::create_dir_all(&instructions_dir).map_err(anyhow::Error::from)?;
40
41 let lib_content = fs::read_to_string(&lib_path).map_err(anyhow::Error::from)?;
43 if !lib_content.contains("mod instructions;") {
44 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 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 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 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
119fn add_instruction_to_entrypoint(lib_content: &str, snake: &str, pascal: &str) -> Option<String> {
122 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 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 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; }
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 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}