Skip to main content

quasar_cli/
init.rs

1use {
2    crate::{
3        config::{GlobalConfig, GlobalDefaults, UiConfig},
4        error::CliResult,
5        toolchain,
6    },
7    dialoguer::{theme::ColorfulTheme, Input, Select},
8    serde::Serialize,
9    std::{fmt, fs, path::Path},
10};
11
12// ---------------------------------------------------------------------------
13// Enums
14// ---------------------------------------------------------------------------
15
16#[derive(Debug, Clone, Copy)]
17enum Toolchain {
18    Solana,
19    Upstream,
20}
21
22impl fmt::Display for Toolchain {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Toolchain::Solana => write!(f, "solana"),
26            Toolchain::Upstream => write!(f, "upstream"),
27        }
28    }
29}
30
31#[derive(Debug, Clone, Copy)]
32enum Framework {
33    None,
34    Mollusk,
35    QuasarSVMWeb3js,
36    QuasarSVMKit,
37    QuasarSVMRust,
38}
39
40impl Framework {
41    fn has_typescript(&self) -> bool {
42        matches!(self, Framework::QuasarSVMWeb3js | Framework::QuasarSVMKit)
43    }
44
45    fn is_kit(&self) -> bool {
46        matches!(self, Framework::QuasarSVMKit)
47    }
48
49    fn has_rust_tests(&self) -> bool {
50        matches!(self, Framework::Mollusk | Framework::QuasarSVMRust)
51    }
52}
53
54impl fmt::Display for Framework {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Framework::None => write!(f, "none"),
58            Framework::Mollusk => write!(f, "mollusk"),
59            Framework::QuasarSVMWeb3js => write!(f, "quasarsvm-web3js"),
60            Framework::QuasarSVMKit => write!(f, "quasarsvm-kit"),
61            Framework::QuasarSVMRust => write!(f, "quasarsvm-rust"),
62        }
63    }
64}
65
66#[derive(Debug, Clone, Copy)]
67enum Template {
68    Minimal,
69    Full,
70}
71
72impl fmt::Display for Template {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            Template::Minimal => write!(f, "minimal"),
76            Template::Full => write!(f, "full"),
77        }
78    }
79}
80
81// ---------------------------------------------------------------------------
82// Quasar.toml schema
83// ---------------------------------------------------------------------------
84
85#[derive(Serialize)]
86struct QuasarToml {
87    project: QuasarProject,
88    toolchain: QuasarToolchain,
89    testing: QuasarTesting,
90}
91
92#[derive(Serialize)]
93struct QuasarProject {
94    name: String,
95}
96
97#[derive(Serialize)]
98struct QuasarToolchain {
99    #[serde(rename = "type")]
100    toolchain_type: String,
101}
102
103#[derive(Serialize)]
104struct QuasarTesting {
105    framework: String,
106}
107
108// ---------------------------------------------------------------------------
109// Banner — sparse blue aurora + FIGlet "Quasar" text reveal
110// ---------------------------------------------------------------------------
111
112fn print_banner() {
113    use std::io::{self, IsTerminal, Write};
114
115    let stdout = io::stdout();
116    if !stdout.is_terminal() {
117        println!("\n  Quasar\n  Build programs that execute at the speed of light\n");
118        return;
119    }
120
121    use std::{thread, time::Duration};
122
123    // Restore cursor if interrupted during animation
124    ctrlc::set_handler(move || {
125        print!("\x1b[?25h");
126        std::process::exit(130);
127    })
128    .ok();
129
130    let mut out = stdout.lock();
131    write!(out, "\x1b[?25l").ok();
132
133    let w: usize = 70;
134    let h: usize = 11; // 1 blank + 7 figlet + 1 blank + 1 tagline + 1 byline
135    let n_frames: usize = 22;
136    let nebula_w: f32 = 30.0; // width of the sweeping nebula band
137
138    // FIGlet "Quasar" — block style, 7 lines tall
139    #[rustfmt::skip]
140    let figlet: [&str; 7] = [
141        " ██████╗ ██╗   ██╗ █████╗ ███████╗ █████╗ ██████╗ ",
142        "██╔═══██╗██║   ██║██╔══██╗██╔════╝██╔══██╗██╔══██╗",
143        "██║   ██║██║   ██║███████║███████╗███████║██████╔╝",
144        "██║▄▄ ██║██║   ██║██╔══██║╚════██║██╔══██║██╔══██╗",
145        "╚██████╔╝╚██████╔╝██║  ██║███████║██║  ██║██║  ██║",
146        " ╚══▀▀═╝  ╚═════╝ ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝",
147        "",
148    ];
149    let fig: Vec<Vec<char>> = figlet.iter().map(|l| l.chars().collect()).collect();
150    let fig_w = fig.iter().map(|l| l.len()).max().unwrap_or(0);
151    let fig_off = w.saturating_sub(fig_w) / 2;
152
153    let tagline = "Build programs that execute at the speed of light";
154    let tag_chars: Vec<char> = tagline.chars().collect();
155    let tag_off = w.saturating_sub(tag_chars.len()) / 2;
156
157    let byline = "by blueshift.gg";
158    let by_chars: Vec<char> = byline.chars().collect();
159    let by_off = w.saturating_sub(by_chars.len()) / 2;
160
161    // Reserve space
162    writeln!(out).ok();
163    for _ in 0..h {
164        writeln!(out).ok();
165    }
166    out.flush().ok();
167
168    for frame in 0..n_frames {
169        write!(out, "\x1b[{h}A").ok();
170        let is_final = frame == n_frames - 1;
171
172        // Leading edge sweeps left → right, revealing text in its wake
173        let t = frame as f32 / (n_frames - 2).max(1) as f32;
174        let edge = -nebula_w + t * (w as f32 + nebula_w * 2.0);
175
176        #[allow(clippy::needless_range_loop)]
177        for li in 0..h {
178            write!(out, "\x1b[2K  ").ok();
179
180            if is_final {
181                // ── Final clean frame ──
182                match li {
183                    1..=7 => {
184                        let row = &fig[li - 1];
185                        for _ in 0..fig_off {
186                            write!(out, " ").ok();
187                        }
188                        for &ch in row.iter() {
189                            if ch != ' ' {
190                                write!(out, "\x1b[36m{ch}\x1b[0m").ok();
191                            } else {
192                                write!(out, " ").ok();
193                            }
194                        }
195                    }
196                    9 => {
197                        for _ in 0..tag_off {
198                            write!(out, " ").ok();
199                        }
200                        write!(out, "\x1b[1m{tagline}\x1b[0m").ok();
201                    }
202                    10 => {
203                        for _ in 0..by_off {
204                            write!(out, " ").ok();
205                        }
206                        write!(out, "\x1b[90mby \x1b[36mblueshift.gg\x1b[0m").ok();
207                    }
208                    _ => {}
209                }
210            } else {
211                // ── Nebula sweep: reveals text as it passes ──
212                for ci in 0..w {
213                    let dist = ci as f32 - edge;
214
215                    // Text character at this position
216                    let text_ch = match li {
217                        1..=7 if ci >= fig_off && ci - fig_off < fig_w => {
218                            fig[li - 1].get(ci - fig_off).copied().unwrap_or(' ')
219                        }
220                        9 if ci >= tag_off && ci - tag_off < tag_chars.len() => {
221                            tag_chars[ci - tag_off]
222                        }
223                        10 if ci >= by_off && ci - by_off < by_chars.len() => by_chars[ci - by_off],
224                        _ => ' ',
225                    };
226
227                    if dist < -nebula_w {
228                        // Behind the nebula: text fully revealed
229                        write_text_char(&mut out, text_ch, li, ci, by_off);
230                    } else if dist < nebula_w {
231                        // Inside the nebula band
232                        let blend = (dist + nebula_w) / (nebula_w * 2.0);
233                        let intensity = 1.0 - (dist.abs() / nebula_w);
234                        let d = aurora_density(ci, li, frame) * intensity;
235
236                        if blend < 0.3 && text_ch != ' ' {
237                            // Trailing edge: text bleeds through
238                            write_text_char(&mut out, text_ch, li, ci, by_off);
239                        } else {
240                            write_nebula_char(&mut out, d);
241                        }
242                    } else {
243                        // Ahead of nebula: dark
244                        write!(out, " ").ok();
245                    }
246                }
247            }
248            writeln!(out).ok();
249        }
250        out.flush().ok();
251
252        if !is_final {
253            thread::sleep(Duration::from_millis(55));
254        }
255    }
256
257    write!(out, "\x1b[?25h").ok();
258    writeln!(out).ok();
259    out.flush().ok();
260}
261
262fn write_text_char(
263    out: &mut impl std::io::Write,
264    ch: char,
265    line: usize,
266    col: usize,
267    by_off: usize,
268) {
269    if ch == ' ' {
270        write!(out, " ").ok();
271    } else {
272        match line {
273            1..=7 => {
274                write!(out, "\x1b[36m{ch}\x1b[0m").ok();
275            }
276            9 => {
277                write!(out, "\x1b[1m{ch}\x1b[0m").ok();
278            }
279            10 => {
280                if col - by_off < 3 {
281                    write!(out, "\x1b[90m{ch}\x1b[0m").ok();
282                } else {
283                    write!(out, "\x1b[36m{ch}\x1b[0m").ok();
284                }
285            }
286            _ => {
287                write!(out, " ").ok();
288            }
289        };
290    }
291}
292
293fn write_nebula_char(out: &mut impl std::io::Write, d: f32) {
294    if d < 0.10 {
295        write!(out, " ").ok();
296    } else if d < 0.25 {
297        write!(out, "\x1b[38;2;15;25;85m░\x1b[0m").ok();
298    } else if d < 0.42 {
299        write!(out, "\x1b[38;2;30;55;145m░\x1b[0m").ok();
300    } else if d < 0.60 {
301        write!(out, "\x1b[38;2;50;95;200m▒\x1b[0m").ok();
302    } else if d < 0.78 {
303        write!(out, "\x1b[38;2;75;140;235m▓\x1b[0m").ok();
304    } else {
305        write!(out, "\x1b[38;2;100;170;255m█\x1b[0m").ok();
306    }
307}
308
309/// Aurora density — sine waves flowing rightward, tuned for sparse output.
310fn aurora_density(col: usize, line: usize, frame: usize) -> f32 {
311    let c = col as f32;
312    let l = line as f32;
313    let f = frame as f32;
314
315    let w1 = ((c - f * 5.0) / 8.0 + l * 0.35).sin();
316    let w2 = ((c - f * 3.5) / 5.5 - l * 0.25).sin() * 0.45;
317    let w3 = ((c - f * 7.0) / 12.0 + l * 0.15).sin() * 0.3;
318
319    ((w1 + w2 + w3 + 1.5) / 3.5).clamp(0.0, 1.0)
320}
321
322// ---------------------------------------------------------------------------
323// ANSI helpers (delegate to shared style module)
324// ---------------------------------------------------------------------------
325
326fn color(code: u8, s: &str) -> String {
327    crate::style::color(code, s)
328}
329
330fn bold(s: &str) -> String {
331    crate::style::bold(s)
332}
333
334fn dim(s: &str) -> String {
335    crate::style::dim(s)
336}
337
338// ---------------------------------------------------------------------------
339// Entry point
340// ---------------------------------------------------------------------------
341
342pub fn run(
343    name: Option<String>,
344    yes: bool,
345    no_git: bool,
346    framework_override: Option<String>,
347    template_override: Option<String>,
348    toolchain_override: Option<String>,
349) -> CliResult {
350    let globals = GlobalConfig::load();
351
352    // Skip prompts when a name is provided (or --yes is set), or when explicit
353    // flags given
354    let skip_prompts = yes
355        || name.is_some()
356        || framework_override.is_some()
357        || template_override.is_some()
358        || toolchain_override.is_some();
359
360    // Validate explicit flag values before proceeding
361    if let Some(ref f) = framework_override {
362        if !matches!(
363            f.as_str(),
364            "none" | "mollusk" | "quasarsvm-rust" | "quasarsvm-web3js" | "quasarsvm-kit"
365        ) {
366            eprintln!(
367                "  {}",
368                crate::style::fail(&format!("unknown framework: {f}"))
369            );
370            eprintln!(
371                "  {}",
372                dim("valid: none, mollusk, quasarsvm-rust, quasarsvm-web3js, quasarsvm-kit")
373            );
374            std::process::exit(1);
375        }
376    }
377    if let Some(ref t) = template_override {
378        if !matches!(t.as_str(), "minimal" | "full") {
379            eprintln!(
380                "  {}",
381                crate::style::fail(&format!("unknown template: {t}"))
382            );
383            eprintln!("  {}", dim("valid: minimal, full"));
384            std::process::exit(1);
385        }
386    }
387    if let Some(ref t) = toolchain_override {
388        if !matches!(t.as_str(), "solana" | "upstream") {
389            eprintln!(
390                "  {}",
391                crate::style::fail(&format!("unknown toolchain: {t}"))
392            );
393            eprintln!("  {}", dim("valid: solana, upstream"));
394            std::process::exit(1);
395        }
396    }
397
398    if globals.ui.animation && !skip_prompts {
399        print_banner();
400    }
401
402    let theme = ColorfulTheme::default();
403
404    // Project name
405    let name: String = if skip_prompts {
406        name.unwrap_or_else(|| {
407            eprintln!(
408                "  {}",
409                crate::style::fail("a project name is required when using flags")
410            );
411            eprintln!(
412                "  {}",
413                crate::style::dim("usage: quasar init <name> [--framework ...] [--template ...]")
414            );
415            std::process::exit(1);
416        })
417    } else {
418        let mut prompt = Input::with_theme(&theme).with_prompt("Project name");
419        if let Some(default) = name {
420            prompt = prompt.default(default);
421        }
422        prompt.interact_text().map_err(anyhow::Error::from)?
423    };
424
425    // When scaffolding into ".", derive the crate name from the current directory
426    let crate_name = if name == "." {
427        std::env::current_dir()
428            .ok()
429            .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
430            .unwrap_or_else(|| "my-program".to_string())
431    } else {
432        name.clone()
433    };
434
435    // Toolchain
436    let toolchain_default = match toolchain_override
437        .as_deref()
438        .or(globals.defaults.toolchain.as_deref())
439    {
440        Some("upstream") => 1,
441        _ => 0,
442    };
443    let toolchain_idx = if skip_prompts {
444        toolchain_default
445    } else {
446        let toolchain_items = &[
447            "solana    (cargo build-sbf)",
448            "upstream  (cargo +nightly build-bpf)",
449        ];
450        Select::with_theme(&theme)
451            .with_prompt("Toolchain")
452            .items(toolchain_items)
453            .default(toolchain_default)
454            .interact()
455            .map_err(anyhow::Error::from)?
456    };
457    let toolchain = match toolchain_idx {
458        0 => Toolchain::Solana,
459        _ => Toolchain::Upstream,
460    };
461
462    // For upstream: sbpf-linker must be installed
463    if matches!(toolchain, Toolchain::Upstream) && !toolchain::has_sbpf_linker() {
464        eprintln!();
465        eprintln!("  {} sbpf-linker not found.", color(196, "\u{2718}"));
466        eprintln!();
467        eprintln!("  Install platform-tools first:");
468        eprintln!(
469            "    {}",
470            bold("git clone https://github.com/anza-xyz/platform-tools")
471        );
472        eprintln!("    {}", bold("cd platform-tools"));
473        eprintln!("    {}", bold("cargo install-with-gallery"));
474        eprintln!();
475        std::process::exit(1);
476    }
477
478    // Testing framework
479    let framework_default = match framework_override
480        .as_deref()
481        .or(globals.defaults.framework.as_deref())
482    {
483        Some("quasarsvm-rust") => 1,
484        Some("quasarsvm-web3js") => 2,
485        Some("quasarsvm-kit") => 3,
486        Some("mollusk") => 4,
487        Some("none") => 0,
488        _ => 1,
489    };
490    let framework_idx = if skip_prompts {
491        framework_default
492    } else {
493        let framework_items = &["None", "Rust", "TypeScript", "Kit", "Rust (Mollusk)"];
494        Select::with_theme(&theme)
495            .with_prompt("Testing framework")
496            .items(framework_items)
497            .default(framework_default)
498            .interact()
499            .map_err(anyhow::Error::from)?
500    };
501    let framework = match framework_idx {
502        0 => Framework::None,
503        1 => Framework::QuasarSVMRust,
504        2 => Framework::QuasarSVMWeb3js,
505        3 => Framework::QuasarSVMKit,
506        _ => Framework::Mollusk,
507    };
508
509    // Template
510    let template_default = match template_override
511        .as_deref()
512        .or(globals.defaults.template.as_deref())
513    {
514        Some("full") => 1,
515        _ => 0,
516    };
517    let template_idx = if skip_prompts {
518        template_default
519    } else {
520        let template_items = &[
521            "Minimal (instruction file only)",
522            "Full (state, errors, and instruction files)",
523        ];
524        Select::with_theme(&theme)
525            .with_prompt("Template")
526            .items(template_items)
527            .default(template_default)
528            .interact()
529            .map_err(anyhow::Error::from)?
530    };
531    let template = match template_idx {
532        0 => Template::Minimal,
533        _ => Template::Full,
534    };
535
536    if skip_prompts {
537        println!();
538        println!(
539            "  {} {} {} {} {}",
540            dim("Using:"),
541            bold(&toolchain.to_string()),
542            dim("+"),
543            bold(&framework.to_string()),
544            bold(&template.to_string()),
545        );
546    }
547
548    scaffold(&name, &crate_name, toolchain, framework, template)?;
549
550    // git init (unless --no-git or already in a git repo)
551    if !no_git {
552        let root = Path::new(&name);
553        let already_git = if name == "." {
554            Path::new(".git").exists()
555        } else {
556            root.join(".git").exists()
557        };
558        if !already_git {
559            let _ = std::process::Command::new("git")
560                .args(["init", "--quiet"])
561                .current_dir(root)
562                .status();
563        }
564    }
565
566    // Save preferences for next time (disable animation after first run)
567    let new_globals = GlobalConfig {
568        defaults: GlobalDefaults {
569            toolchain: Some(toolchain.to_string()),
570            framework: Some(framework.to_string()),
571            template: Some(template.to_string()),
572        },
573        ui: UiConfig {
574            animation: false,
575            ..globals.ui
576        },
577    };
578    let _ = new_globals.save(); // best-effort
579
580    // Success message
581    println!();
582    println!(
583        "  {}  Created {} {}",
584        color(83, "\u{2714}"),
585        bold(&crate_name),
586        dim("project")
587    );
588    println!();
589    println!("  {}", dim("Next steps:"));
590    if name != "." {
591        println!(
592            "    {}  {}",
593            color(45, "\u{276f}"),
594            bold(&format!("cd {name}"))
595        );
596    }
597    println!("    {}  {}", color(45, "\u{276f}"), bold("quasar build"));
598    if framework.has_rust_tests() || framework.has_typescript() {
599        println!("    {}  {}", color(45, "\u{276f}"), bold("quasar test"));
600    }
601    println!();
602    println!(
603        "  {} saved to {}",
604        dim("Preferences"),
605        dim(&GlobalConfig::path().display().to_string()),
606    );
607    println!();
608
609    Ok(())
610}
611
612fn scaffold(
613    dir: &str,
614    name: &str,
615    toolchain: Toolchain,
616    framework: Framework,
617    template: Template,
618) -> CliResult {
619    let root = Path::new(dir);
620
621    if dir == "." {
622        // Scaffold into current directory — check it doesn't already have a project
623        if root.join("Cargo.toml").exists() || root.join("Quasar.toml").exists() {
624            eprintln!(
625                "  {}",
626                crate::style::fail("current directory already contains a project")
627            );
628            std::process::exit(1);
629        }
630    } else if root.exists() {
631        eprintln!(
632            "  {}",
633            crate::style::fail(&format!("directory '{dir}' already exists"))
634        );
635        std::process::exit(1);
636    }
637
638    let src = root.join("src");
639    fs::create_dir_all(&src).map_err(anyhow::Error::from)?;
640
641    // Quasar.toml
642    let config = QuasarToml {
643        project: QuasarProject {
644            name: name.to_string(),
645        },
646        toolchain: QuasarToolchain {
647            toolchain_type: toolchain.to_string(),
648        },
649        testing: QuasarTesting {
650            framework: framework.to_string(),
651        },
652    };
653    let toml_str = toml::to_string_pretty(&config).map_err(anyhow::Error::from)?;
654    fs::write(root.join("Quasar.toml"), toml_str).map_err(anyhow::Error::from)?;
655
656    // Cargo.toml
657    fs::write(
658        root.join("Cargo.toml"),
659        generate_cargo_toml(name, toolchain, framework),
660    )
661    .map_err(anyhow::Error::from)?;
662
663    // .cargo/config.toml (upstream only)
664    if matches!(toolchain, Toolchain::Upstream) {
665        let cargo_dir = root.join(".cargo");
666        fs::create_dir_all(&cargo_dir).map_err(anyhow::Error::from)?;
667        fs::write(cargo_dir.join("config.toml"), CARGO_CONFIG).map_err(anyhow::Error::from)?;
668    }
669
670    // .gitignore
671    fs::write(root.join(".gitignore"), GITIGNORE).map_err(anyhow::Error::from)?;
672
673    // Generate program keypair
674    let deploy_dir = root.join("target").join("deploy");
675    fs::create_dir_all(&deploy_dir).map_err(anyhow::Error::from)?;
676
677    let signing_key = ed25519_dalek::SigningKey::generate(&mut rand::thread_rng());
678    let program_id = bs58::encode(signing_key.verifying_key().as_bytes()).into_string();
679
680    // Write keypair as Solana CLI-compatible JSON (64-byte array: secret + public)
681    let mut keypair_bytes = Vec::with_capacity(64);
682    keypair_bytes.extend_from_slice(signing_key.as_bytes());
683    keypair_bytes.extend_from_slice(signing_key.verifying_key().as_bytes());
684    let keypair_json = serde_json::to_string(&keypair_bytes).map_err(anyhow::Error::from)?;
685    fs::write(
686        deploy_dir.join(format!("{name}-keypair.json")),
687        &keypair_json,
688    )
689    .map_err(anyhow::Error::from)?;
690
691    // src/lib.rs
692    let module_name = name.replace('-', "_");
693    let has_rust_tests = framework.has_rust_tests();
694    fs::write(
695        src.join("lib.rs"),
696        generate_lib_rs(&module_name, &program_id, template, has_rust_tests),
697    )
698    .map_err(anyhow::Error::from)?;
699
700    // Template-specific files
701    match template {
702        Template::Minimal => {
703            // Everything lives in lib.rs — no instructions/ directory needed
704        }
705        Template::Full => {
706            let instructions_dir = src.join("instructions");
707            fs::create_dir_all(&instructions_dir).map_err(anyhow::Error::from)?;
708            fs::write(instructions_dir.join("mod.rs"), INSTRUCTIONS_MOD)
709                .map_err(anyhow::Error::from)?;
710            fs::write(
711                instructions_dir.join("initialize.rs"),
712                INSTRUCTION_INITIALIZE,
713            )
714            .map_err(anyhow::Error::from)?;
715            fs::write(src.join("state.rs"), STATE_RS).map_err(anyhow::Error::from)?;
716            fs::write(src.join("errors.rs"), ERRORS_RS).map_err(anyhow::Error::from)?;
717        }
718    }
719
720    // Rust test scaffold
721    if framework.has_rust_tests() {
722        fs::write(
723            src.join("tests.rs"),
724            generate_tests_rs(&module_name, framework, template, toolchain),
725        )
726        .map_err(anyhow::Error::from)?;
727    }
728
729    // TypeScript test scaffold
730    if framework.has_typescript() {
731        let tests_dir = root.join("tests");
732        fs::create_dir_all(&tests_dir).map_err(anyhow::Error::from)?;
733
734        // package.json and tsconfig.json go in the project root
735        fs::write(
736            root.join("package.json"),
737            generate_package_json(name, framework),
738        )
739        .map_err(anyhow::Error::from)?;
740        fs::write(root.join("tsconfig.json"), TS_TEST_TSCONFIG).map_err(anyhow::Error::from)?;
741
742        // Test files go in tests/
743        fs::write(
744            tests_dir.join(format!("{}.test.ts", name)),
745            generate_test_ts(name, framework, toolchain),
746        )
747        .map_err(anyhow::Error::from)?;
748    }
749
750    Ok(())
751}
752
753// ---------------------------------------------------------------------------
754// Generators
755// ---------------------------------------------------------------------------
756
757fn generate_cargo_toml(name: &str, toolchain: Toolchain, framework: Framework) -> String {
758    let mut out = format!(
759        r#"[package]
760name = "{name}"
761version = "0.1.0"
762edition = "2021"
763
764[lints.rust.unexpected_cfgs]
765level = "warn"
766check-cfg = [
767    'cfg(target_os, values("solana"))',
768]
769
770[lib]
771crate-type = ["cdylib"]
772
773[features]
774alloc = []
775client = []
776debug = []
777
778[dependencies]
779quasar-lang = {{ git = "https://github.com/blueshift-gg/quasar" }}
780"#,
781    );
782
783    if matches!(toolchain, Toolchain::Solana) {
784        out.push_str("solana-instruction = { version = \"3.2.0\" }\n");
785    }
786
787    // Dev dependencies based on testing framework
788    let client_dep = format!("{name}-client = {{ path = \"target/client/rust/{name}-client\" }}\n");
789
790    match framework {
791        Framework::None => {}
792        Framework::Mollusk => {
793            out.push_str(&format!(
794                r#"
795[dev-dependencies]
796{client_dep}mollusk-svm = "0.10.3"
797solana-account = {{ version = "3.4.0" }}
798solana-address = {{ version = "2.2.0", features = ["decode"] }}
799solana-instruction = {{ version = "3.2.0", features = ["bincode"] }}
800"#,
801            ));
802        }
803        Framework::QuasarSVMRust => {
804            out.push_str(&format!(
805                r#"
806[dev-dependencies]
807{client_dep}quasar-svm = {{ git = "https://github.com/blueshift-gg/quasar-svm" }}
808solana-account = {{ version = "3.4.0" }}
809solana-address = {{ version = "2.2.0", features = ["decode"] }}
810solana-instruction = {{ version = "3.2.0", features = ["bincode"] }}
811solana-pubkey = {{ version = "4.1.0" }}
812"#,
813            ));
814        }
815        Framework::QuasarSVMWeb3js | Framework::QuasarSVMKit => {
816            out.push_str(&format!(
817                r#"
818[dev-dependencies]
819{client_dep}solana-account = {{ version = "3.4.0" }}
820solana-address = {{ version = "2.2.0", features = ["decode"] }}
821solana-instruction = {{ version = "3.2.0", features = ["bincode"] }}
822"#,
823            ));
824        }
825    }
826
827    out
828}
829
830fn generate_lib_rs(
831    module_name: &str,
832    program_id: &str,
833    template: Template,
834    has_tests: bool,
835) -> String {
836    let test_mod = if has_tests {
837        "\n#[cfg(test)]\nmod tests;\n"
838    } else {
839        ""
840    };
841
842    match template {
843        Template::Minimal => {
844            format!(
845                r#"#![cfg_attr(not(test), no_std)]
846
847use quasar_lang::prelude::*;
848
849declare_id!("{program_id}");
850
851#[derive(Accounts)]
852pub struct Initialize<'info> {{
853    pub payer: &'info mut Signer,
854    pub system_program: &'info Program<System>,
855}}
856
857impl<'info> Initialize<'info> {{
858    #[inline(always)]
859    pub fn initialize(&self) -> Result<(), ProgramError> {{
860        Ok(())
861    }}
862}}
863
864#[program]
865mod {module_name} {{
866    use super::*;
867
868    #[instruction(discriminator = 0)]
869    pub fn initialize(ctx: Ctx<Initialize>) -> Result<(), ProgramError> {{
870        ctx.accounts.initialize()
871    }}
872}}
873{test_mod}"#
874            )
875        }
876        Template::Full => {
877            format!(
878                r#"#![cfg_attr(not(test), no_std)]
879
880use quasar_lang::prelude::*;
881
882mod errors;
883mod instructions;
884mod state;
885use instructions::*;
886
887declare_id!("{program_id}");
888
889#[program]
890mod {module_name} {{
891    use super::*;
892
893    #[instruction(discriminator = 0)]
894    pub fn initialize(ctx: Ctx<Initialize>) -> Result<(), ProgramError> {{
895        ctx.accounts.initialize()
896    }}
897}}
898{test_mod}"#
899            )
900        }
901    }
902}
903
904fn generate_package_json(name: &str, framework: Framework) -> String {
905    let solana_dep = if framework.is_kit() {
906        "\"@solana/kit\": \"^6.0.0\""
907    } else {
908        "\"@solana/web3.js\": \"github:blueshift-gg/web3.js#v2\""
909    };
910    format!(
911        r#"{{
912  "name": "{name}",
913  "version": "0.1.0",
914  "private": true,
915  "type": "commonjs",
916  "scripts": {{
917    "test": "mocha --require tsx --delay tests/*.test.ts"
918  }},
919  "dependencies": {{
920    "@blueshift-gg/quasar-svm": "^0.1",
921    {solana_dep}
922  }},
923  "devDependencies": {{
924    "@types/chai": "^5.2.0",
925    "@types/mocha": "^10.0.0",
926    "@types/node": "^22.0.0",
927    "chai": "^6.2.2",
928    "mocha": "^11.7.5",
929    "tsx": "^4.21.0",
930    "typescript": "^5.9.3"
931  }}
932}}
933"#
934    )
935}
936
937fn generate_test_ts(name: &str, framework: Framework, toolchain: Toolchain) -> String {
938    let module_name = name.replace('-', "_");
939    let class_name = crate::utils::snake_to_pascal(&module_name);
940    let so_name = match toolchain {
941        Toolchain::Upstream => format!("lib{module_name}"),
942        Toolchain::Solana => module_name.clone(),
943    };
944
945    if framework.is_kit() {
946        format!(
947            r#"import {{ generateKeyPairSigner }} from "@solana/kit";
948import {{ {class_name}Client, PROGRAM_ADDRESS }} from "../target/client/typescript/{module_name}/kit";
949import {{ describe, it, run }} from "mocha";
950import {{ QuasarSvm, createKeyedSystemAccount }} from "@blueshift-gg/quasar-svm/kit";
951import {{ readFile }} from "node:fs/promises";
952import {{ assert }} from "chai";
953
954const {class_name}Program = new {class_name}Client();
955
956describe("{class_name} Program", async () => {{
957
958  const vm = new QuasarSvm();
959  vm.addProgram(PROGRAM_ADDRESS, await readFile("target/deploy/{so_name}.so"));
960
961  const payer = await generateKeyPairSigner();
962
963  it("initializes", async () => {{
964    const initializeInstruction = {class_name}Program.createInitializeInstruction({{
965      payer: payer.address,
966    }});
967
968    const result = vm.processInstruction(initializeInstruction, [
969      createKeyedSystemAccount(payer.address),
970    ]);
971
972    assert.isTrue(result.status.ok, `initialize failed:\n${{result.logs.join("\n")}}`);
973  }});
974
975  run()
976}});
977"#
978        )
979    } else {
980        format!(
981            r#"import {{ Keypair }} from "@solana/web3.js";
982import {{ {class_name}Client }} from "../target/client/typescript/{module_name}/web3.js";
983import {{ readFile }} from "node:fs/promises";
984import {{ describe, it, run }} from "mocha";
985import {{ assert }} from "chai";
986import {{ QuasarSvm, createKeyedSystemAccount }} from "@blueshift-gg/quasar-svm/web3.js";
987
988const {class_name}Program = new {class_name}Client();
989
990describe("{class_name} Program", async () => {{
991  const vm = new QuasarSvm();
992  vm.addProgram({class_name}Client.programId, await readFile("target/deploy/{so_name}.so"));
993
994  const {{ publicKey: payer }} = await Keypair.generate();
995
996  it("initializes", async () => {{
997    const initializeInstruction = {class_name}Program.createInitializeInstruction({{
998      payer,
999    }});
1000
1001    const result = vm.processInstruction(initializeInstruction, [
1002      createKeyedSystemAccount(payer),
1003    ]);
1004
1005    assert.isTrue(result.status.ok, `initialize failed:\n${{result.logs.join("\n")}}`);
1006  }});
1007
1008  run();
1009}});
1010"#
1011        )
1012    }
1013}
1014
1015fn generate_tests_rs(
1016    module_name: &str,
1017    framework: Framework,
1018    template: Template,
1019    toolchain: Toolchain,
1020) -> String {
1021    let mut libname = module_name.to_string();
1022    if matches!(toolchain, Toolchain::Upstream) {
1023        libname = format!("lib{libname}");
1024    };
1025    let client_crate = format!("{module_name}_client");
1026
1027    match (framework, template) {
1028        (Framework::Mollusk, Template::Minimal | Template::Full) => {
1029            format!(
1030                r#"extern crate std;
1031
1032use mollusk_svm::{{program::keyed_account_for_system_program, Mollusk}};
1033use solana_account::Account;
1034use solana_address::Address;
1035use solana_instruction::Instruction;
1036
1037use {client_crate}::InitializeInstruction;
1038
1039fn setup() -> Mollusk {{
1040    Mollusk::new(&crate::ID, "target/deploy/{libname}")
1041}}
1042
1043#[test]
1044fn test_initialize() {{
1045    let mollusk = setup();
1046    let (system_program, system_program_account) = keyed_account_for_system_program();
1047
1048    let payer = Address::new_unique();
1049    let payer_account = Account::new(10_000_000_000, 0, &system_program);
1050
1051    let instruction: Instruction = InitializeInstruction {{
1052        payer,
1053        system_program,
1054    }}
1055    .into();
1056
1057    let result = mollusk.process_instruction(
1058        &instruction,
1059        &[
1060            (payer, payer_account),
1061            (system_program, system_program_account),
1062        ],
1063    );
1064
1065    assert!(
1066        result.program_result.is_ok(),
1067        "initialize failed: {{:?}}",
1068        result.program_result,
1069    );
1070}}
1071"#
1072            )
1073        }
1074        (Framework::QuasarSVMRust, Template::Minimal | Template::Full) => {
1075            format!(
1076                r#"extern crate std;
1077
1078use quasar_svm::{{Account, Instruction, Pubkey, QuasarSvm}};
1079use solana_address::Address;
1080
1081use {client_crate}::InitializeInstruction;
1082
1083fn setup() -> QuasarSvm {{
1084    let elf = include_bytes!("../target/deploy/{libname}.so");
1085    QuasarSvm::new()
1086        .with_program(&Pubkey::from(crate::ID), elf)
1087}}
1088
1089#[test]
1090fn test_initialize() {{
1091    let mut svm = setup();
1092
1093    let payer = Pubkey::new_unique();
1094
1095    let instruction: Instruction = InitializeInstruction {{
1096        payer: Address::from(payer.to_bytes()),
1097        system_program: Address::from(quasar_svm::system_program::ID.to_bytes()),
1098    }}
1099    .into();
1100
1101    let result = svm.process_instruction(
1102        &instruction,
1103        &[Account {{
1104            address: payer,
1105            lamports: 10_000_000_000,
1106            data: vec![],
1107            owner: quasar_svm::system_program::ID,
1108            executable: false,
1109        }}],
1110    );
1111
1112    result.assert_success();
1113}}
1114"#
1115            )
1116        }
1117        _ => r#"extern crate std;
1118
1119#[test]
1120fn test_initialize() {
1121    // TODO: implement test
1122}
1123"#
1124        .to_string(),
1125    }
1126}
1127
1128// ---------------------------------------------------------------------------
1129// Static templates
1130// ---------------------------------------------------------------------------
1131
1132const GITIGNORE: &str = "\
1133# Build artifacts
1134/target
1135
1136# Lock files
1137Cargo.lock
1138package-lock.json
1139yarn.lock
1140
1141# Dependencies
1142node_modules
1143
1144# Environment
1145.env
1146.env.*
1147
1148# OS
1149.DS_Store
1150";
1151
1152const CARGO_CONFIG: &str = r#"[unstable]
1153build-std = ["core", "alloc"]
1154
1155[target.bpfel-unknown-none]
1156rustflags = [
1157"--cfg", "target_os=\"solana\"",
1158"--cfg", "feature=\"mem_unaligned\"",
1159"-C", "linker=sbpf-linker",
1160"-C", "panic=abort",
1161"-C", "relocation-model=static",
1162"-C", "link-arg=--disable-memory-builtins",
1163"-C", "link-arg=--llvm-args=--bpf-stack-size=4096",
1164"-C", "link-arg=--disable-expand-memcpy-in-order",
1165"-C", "link-arg=--export=entrypoint",
1166"-C", "target-cpu=v2",
1167]
1168[alias]
1169build-bpf = "build --release --target bpfel-unknown-none"
1170"#;
1171
1172const INSTRUCTIONS_MOD: &str = r#"mod initialize;
1173pub use initialize::*;
1174"#;
1175
1176const INSTRUCTION_INITIALIZE: &str = r#"use quasar_lang::prelude::*;
1177
1178#[derive(Accounts)]
1179pub struct Initialize<'info> {
1180    pub payer: &'info mut Signer,
1181    pub system_program: &'info Program<System>,
1182}
1183
1184impl<'info> Initialize<'info> {
1185    #[inline(always)]
1186    pub fn initialize(&self) -> Result<(), ProgramError> {
1187        Ok(())
1188    }
1189}
1190"#;
1191
1192const STATE_RS: &str = r#"use quasar_lang::prelude::*;
1193
1194#[account(discriminator = 1)]
1195pub struct MyAccount {
1196    pub authority: Address,
1197    pub value: u64,
1198}
1199"#;
1200
1201const ERRORS_RS: &str = r#"use quasar_lang::prelude::*;
1202
1203#[error_code]
1204pub enum MyError {
1205    Unauthorized,
1206}
1207"#;
1208
1209const TS_TEST_TSCONFIG: &str = r#"{
1210  "compilerOptions": {
1211    "target": "es2020",
1212    "module": "commonjs",
1213    "strict": true,
1214    "esModuleInterop": true,
1215    "skipLibCheck": true,
1216    "resolveJsonModule": true,
1217    "types": ["node", "mocha"]
1218  },
1219  "include": ["tests/*.test.ts"]
1220}
1221"#;