Skip to main content

quasar_cli/
lib.rs

1use {
2    clap::{ArgAction, Args, CommandFactory, Parser, Subcommand},
3    std::path::PathBuf,
4};
5
6pub mod build;
7pub mod cfg;
8pub mod clean;
9pub mod config;
10pub mod deploy;
11pub mod dump;
12pub mod error;
13pub mod idl;
14pub mod init;
15pub mod new;
16pub mod style;
17pub mod test;
18pub mod toolchain;
19pub mod utils;
20pub use error::CliResult;
21
22#[derive(Parser, Debug)]
23#[command(
24    name = "quasar",
25    version,
26    about = "Build programs that execute at the speed of light",
27    disable_help_subcommand = true
28)]
29pub struct Cli {
30    #[command(subcommand)]
31    pub command: Command,
32}
33
34#[derive(Subcommand, Debug)]
35pub enum Command {
36    /// Scaffold a new Quasar project
37    Init(InitCommand),
38    /// Add instructions, state, and errors to the project
39    Add(AddCommand),
40    /// Compile the on-chain program
41    Build(BuildCommand),
42    /// Run the test suite
43    Test(TestCommand),
44    /// Deploy the program to a cluster
45    Deploy(DeployCommand),
46    /// Remove build artifacts
47    Clean(CleanCommand),
48    /// Manage global settings
49    Config(ConfigCommand),
50    /// Generate the IDL for a program crate
51    Idl(IdlCommand),
52    /// Measure compute-unit usage
53    Profile(ProfileCommand),
54    /// Dump sBPF assembly
55    Dump(DumpCommand),
56    /// Generate shell completions
57    Completions(CompletionsCommand),
58}
59
60// ---------------------------------------------------------------------------
61// Command args
62// ---------------------------------------------------------------------------
63
64#[derive(Args, Debug, Default)]
65pub struct InitCommand {
66    /// Project name — skips the interactive name prompt
67    #[arg(value_name = "NAME")]
68    pub name: Option<String>,
69
70    /// Skip prompts and use saved defaults
71    #[arg(long, short, action = ArgAction::SetTrue)]
72    pub yes: bool,
73
74    /// Skip git init
75    #[arg(long, action = ArgAction::SetTrue)]
76    pub no_git: bool,
77
78    /// Testing framework (none, mollusk, quasarsvm-rust, quasarsvm-web3js,
79    /// quasarsvm-kit)
80    #[arg(long)]
81    pub framework: Option<String>,
82
83    /// Project template (minimal, full)
84    #[arg(long)]
85    pub template: Option<String>,
86
87    /// Toolchain (solana, upstream)
88    #[arg(long)]
89    pub toolchain: Option<String>,
90}
91
92#[derive(Args, Debug)]
93pub struct AddCommand {
94    /// Add a new instruction handler
95    #[arg(short, long, value_name = "NAME")]
96    pub instruction: Option<String>,
97
98    /// Add a new state account
99    #[arg(short, long, value_name = "NAME")]
100    pub state: Option<String>,
101
102    /// Add a new error enum
103    #[arg(short, long, value_name = "NAME")]
104    pub error: Option<String>,
105}
106
107#[derive(Args, Debug, Default)]
108pub struct BuildCommand {
109    /// Emit debug symbols (required for profiling)
110    #[arg(long, action = ArgAction::SetTrue)]
111    pub debug: bool,
112
113    /// Watch src/ for changes and rebuild automatically
114    #[arg(long, short, action = ArgAction::SetTrue)]
115    pub watch: bool,
116
117    /// Cargo features to enable (comma-separated or repeated)
118    #[arg(long, value_name = "FEATURES")]
119    pub features: Option<String>,
120}
121
122#[derive(Args, Debug, Default)]
123pub struct TestCommand {
124    /// Build with debug symbols before testing
125    #[arg(long, action = ArgAction::SetTrue)]
126    pub debug: bool,
127
128    /// Only run tests whose name matches PATTERN
129    #[arg(long, short, value_name = "PATTERN")]
130    pub filter: Option<String>,
131
132    /// Watch src/ for changes and re-run tests automatically
133    #[arg(long, short, action = ArgAction::SetTrue)]
134    pub watch: bool,
135
136    /// Skip the build step (use existing binary)
137    #[arg(long, action = ArgAction::SetTrue)]
138    pub no_build: bool,
139
140    /// Cargo features to enable (comma-separated or repeated)
141    #[arg(long, value_name = "FEATURES")]
142    pub features: Option<String>,
143}
144
145#[derive(Args, Debug, Default)]
146pub struct DeployCommand {
147    /// Path to a program keypair (default: target/deploy/<name>-keypair.json)
148    #[arg(long, value_name = "KEYPAIR")]
149    pub program_keypair: Option<PathBuf>,
150
151    /// Upgrade authority keypair (default: Solana CLI default keypair)
152    #[arg(long, value_name = "KEYPAIR")]
153    pub upgrade_authority: Option<PathBuf>,
154
155    /// Payer keypair (default: Solana CLI default keypair)
156    #[arg(long, short, value_name = "KEYPAIR")]
157    pub keypair: Option<PathBuf>,
158
159    /// Cluster URL (default: Solana CLI configured cluster)
160    #[arg(long, short, value_name = "URL")]
161    pub url: Option<String>,
162
163    /// Skip the build step
164    #[arg(long, action = ArgAction::SetTrue)]
165    pub skip_build: bool,
166}
167
168#[derive(Args, Debug, Default)]
169pub struct CleanCommand {
170    /// Also run cargo clean (removes all build artifacts)
171    #[arg(long, short, action = ArgAction::SetTrue)]
172    pub all: bool,
173}
174
175#[derive(Args, Debug)]
176pub struct ConfigCommand {
177    #[command(subcommand)]
178    pub action: Option<ConfigAction>,
179}
180
181#[derive(Subcommand, Debug)]
182pub enum ConfigAction {
183    /// Read a single config value
184    Get {
185        /// Config key (e.g. ui.animation, defaults.toolchain)
186        #[arg(value_name = "KEY")]
187        key: String,
188    },
189    /// Write a config value
190    Set {
191        /// Config key
192        #[arg(value_name = "KEY")]
193        key: String,
194        /// New value
195        #[arg(value_name = "VALUE")]
196        value: String,
197    },
198    /// Print every config value
199    List,
200    /// Restore factory defaults
201    Reset,
202}
203
204#[derive(Args, Debug)]
205pub struct IdlCommand {
206    /// Path to the program crate directory
207    #[arg(value_name = "PATH")]
208    pub crate_path: PathBuf,
209}
210
211#[derive(Args, Debug, Clone)]
212pub struct DumpCommand {
213    /// Path to a compiled .so (auto-detected from target/deploy/ if omitted)
214    #[arg(value_name = "ELF")]
215    pub elf_path: Option<PathBuf>,
216
217    /// Disassemble only this symbol (demangled name)
218    #[arg(long, short, value_name = "SYMBOL")]
219    pub function: Option<String>,
220
221    /// Interleave source code (requires debug build)
222    #[arg(long, short = 'S', action = ArgAction::SetTrue)]
223    pub source: bool,
224}
225
226#[derive(Args, Debug, Clone)]
227pub struct ProfileCommand {
228    /// Path to a compiled .so (auto-detected from target/deploy/ if omitted)
229    #[arg(value_name = "ELF")]
230    pub elf_path: Option<PathBuf>,
231
232    /// Compare CU cost against an on-chain program by name
233    #[arg(long = "diff", value_name = "PROGRAM", conflicts_with = "elf_path")]
234    pub diff_program: Option<String>,
235
236    /// Upload the profile result and get a shareable link
237    #[arg(long, action = ArgAction::SetTrue, conflicts_with = "diff_program")]
238    pub share: bool,
239
240    /// Show full terminal output with all functions
241    #[arg(long, action = ArgAction::SetTrue)]
242    pub expand: bool,
243
244    /// Watch src/ for changes and re-profile automatically
245    #[arg(long, short, action = ArgAction::SetTrue)]
246    pub watch: bool,
247}
248
249#[derive(Args, Debug)]
250pub struct CompletionsCommand {
251    /// Shell to generate completions for
252    #[arg(value_enum)]
253    pub shell: clap_complete::Shell,
254}
255
256// ---------------------------------------------------------------------------
257// Run
258// ---------------------------------------------------------------------------
259
260pub fn run(cli: Cli) -> CliResult {
261    match cli.command {
262        Command::Init(cmd) => init::run(
263            cmd.name,
264            cmd.yes,
265            cmd.no_git,
266            cmd.framework,
267            cmd.template,
268            cmd.toolchain,
269        ),
270        Command::Add(cmd) => {
271            if cmd.instruction.is_none() && cmd.state.is_none() && cmd.error.is_none() {
272                eprintln!(
273                    "  {}",
274                    style::fail(
275                        "specify at least one of -i/--instruction, -s/--state, or -e/--error"
276                    )
277                );
278                std::process::exit(1);
279            }
280            if let Some(name) = cmd.instruction {
281                new::run_instruction(&name)?;
282            }
283            if let Some(name) = cmd.state {
284                new::run_state(&name)?;
285            }
286            if let Some(name) = cmd.error {
287                new::run_error(&name)?;
288            }
289            Ok(())
290        }
291        Command::Build(cmd) => build::run(cmd.debug, cmd.watch, cmd.features),
292        Command::Test(cmd) => {
293            test::run(cmd.debug, cmd.filter, cmd.watch, cmd.no_build, cmd.features)
294        }
295        Command::Deploy(cmd) => deploy::run(
296            cmd.program_keypair,
297            cmd.upgrade_authority,
298            cmd.keypair,
299            cmd.url,
300            cmd.skip_build,
301        ),
302        Command::Clean(cmd) => clean::run(cmd.all),
303        Command::Config(cmd) => cfg::run(cmd.action),
304        Command::Idl(cmd) => idl::run(cmd),
305        Command::Dump(cmd) => dump::run(cmd.elf_path, cmd.function, cmd.source),
306        Command::Completions(cmd) => {
307            clap_complete::generate(
308                cmd.shell,
309                &mut Cli::command(),
310                "quasar",
311                &mut std::io::stdout(),
312            );
313            Ok(())
314        }
315        Command::Profile(cmd) => {
316            if cmd.watch {
317                return profile_watch(cmd.expand);
318            }
319
320            let elf_path = if let Some(path) = cmd.elf_path {
321                path
322            } else if cmd.diff_program.is_none() {
323                // Auto-build with debug symbols for profiling
324                build::profile_build()?
325            } else {
326                // --diff mode doesn't need an ELF
327                std::path::PathBuf::new()
328            };
329
330            quasar_profile::run(quasar_profile::ProfileCommand {
331                elf_path: if elf_path.as_os_str().is_empty() {
332                    None
333                } else {
334                    Some(elf_path)
335                },
336                diff_program: cmd.diff_program,
337                share: cmd.share,
338                expand: cmd.expand,
339            });
340            Ok(())
341        }
342    }
343}
344
345// ---------------------------------------------------------------------------
346// Custom help — shown for `quasar`, `quasar -h`, `quasar --help`, `quasar help`
347// ---------------------------------------------------------------------------
348
349pub fn print_help() {
350    let v = env!("CARGO_PKG_VERSION");
351
352    println!();
353    println!(
354        "  {} {}",
355        style::bold("quasar"),
356        style::dim(&format!("v{v}"))
357    );
358    println!(
359        "  {}",
360        style::dim("Build programs that execute at the speed of light")
361    );
362    println!();
363    println!("  {}", style::bold("Commands:"));
364    print_cmd(
365        "init    [name] [-y] [--no-git] [--template]",
366        "Scaffold a new project",
367    );
368    print_cmd(
369        "add     [-i name] [-s name] [-e name]",
370        "Add instructions, state, errors",
371    );
372    print_cmd(
373        "build   [--debug] [-w] [--features]",
374        "Compile the on-chain program",
375    );
376    print_cmd(
377        "test    [--debug] [-f] [-w] [--features]",
378        "Run the test suite",
379    );
380    print_cmd(
381        "deploy  [-u url] [-k keypair] [--skip-build]",
382        "Deploy to a cluster",
383    );
384    print_cmd("clean   [-a]", "Remove build artifacts");
385    print_cmd("config  [get|set|list|reset]", "Manage global settings");
386    print_cmd("idl     <path>", "Generate the program IDL");
387    print_cmd(
388        "profile [elf] [--expand] [--diff] [-w]",
389        "Measure compute-unit usage",
390    );
391    print_cmd("dump    [elf] [-f] [-S]", "Dump sBPF assembly");
392    println!();
393    println!("  {}", style::bold("Options:"));
394    print_cmd("-h, --help", "Print help");
395    print_cmd("-V, --version", "Print version");
396    println!();
397    println!(
398        "  Run {} for details on any command.",
399        style::bold("quasar <command> --help")
400    );
401    println!();
402}
403
404fn print_cmd(cmd: &str, desc: &str) {
405    println!("    {}  {}", style::color(45, &format!("{cmd:<34}")), desc);
406}
407
408fn profile_watch(expand: bool) -> CliResult {
409    fn profile_once(expand: bool) {
410        match build::profile_build() {
411            Ok(elf) => {
412                quasar_profile::run(quasar_profile::ProfileCommand {
413                    elf_path: Some(elf),
414                    diff_program: None,
415                    share: false,
416                    expand,
417                });
418            }
419            Err(e) => {
420                eprintln!("  {}", style::fail(&format!("{e}")));
421            }
422        }
423    }
424
425    profile_once(expand);
426
427    loop {
428        let baseline = build::collect_mtimes(std::path::Path::new("src"));
429        loop {
430            std::thread::sleep(std::time::Duration::from_secs(1));
431            let current = build::collect_mtimes(std::path::Path::new("src"));
432            if current != baseline {
433                profile_once(expand);
434                break;
435            }
436        }
437    }
438}