Browse Source

refactor: break down the cli application function to specific file (#74)

Naman Anand 1 year ago
parent
commit
9090a9e791

+ 113 - 0
cli/src/component.rs

@@ -0,0 +1,113 @@
+use crate::{
+    discover_cluster_url,
+    rust_template::create_component,
+    templates::component::{component_type, component_type_import},
+    workspace::with_workspace,
+};
+use anchor_cli::config::{ConfigOverride, ProgramDeployment};
+use anchor_lang_idl::types::Idl;
+use anyhow::{anyhow, Result};
+use std::{
+    fs::{self, File, OpenOptions},
+    io::Write,
+    path::Path,
+    process::Stdio,
+};
+
+// Create a new component from the template
+pub fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
+    with_workspace(cfg_override, |cfg| {
+        match cfg.path().parent() {
+            None => {
+                println!("Unable to make new component");
+            }
+            Some(parent) => {
+                std::env::set_current_dir(parent)?;
+
+                let cluster = cfg.provider.cluster.clone();
+                let programs = cfg.programs.entry(cluster).or_default();
+                if programs.contains_key(&name) {
+                    return Err(anyhow!("Program already exists"));
+                }
+
+                programs.insert(
+                    name.clone(),
+                    ProgramDeployment {
+                        address: {
+                            create_component(&name)?;
+                            anchor_cli::rust_template::get_or_create_program_id(&name)
+                        },
+                        path: None,
+                        idl: None,
+                    },
+                );
+
+                let toml = cfg.to_string();
+                fs::write("Anchor.toml", toml)?;
+
+                println!("Created new component: {}", name);
+            }
+        };
+        Ok(())
+    })
+}
+
+pub fn extract_component_id(line: &str) -> Option<&str> {
+    let component_id_marker = "#[component_id(";
+    line.find(component_id_marker).map(|start| {
+        let start = start + component_id_marker.len();
+        let end = line[start..].find(')').unwrap() + start;
+        line[start..end].trim_matches('"')
+    })
+}
+
+pub fn fetch_idl_for_component(component_id: &str, url: &str) -> Result<String> {
+    let output = std::process::Command::new("bolt")
+        .arg("idl")
+        .arg("fetch")
+        .arg(component_id)
+        .arg("--provider.cluster")
+        .arg(url)
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .output()?;
+
+    if output.status.success() {
+        let idl_string = String::from_utf8(output.stdout)
+            .map_err(|e| anyhow!("Failed to decode IDL output as UTF-8: {}", e))?
+            .to_string();
+        Ok(idl_string)
+    } else {
+        let error_message = String::from_utf8(output.stderr)
+            .unwrap_or(format!(
+                "Error trying to dynamically generate the type \
+            for component {}, unable to fetch the idl. \nEnsure that the idl is available. Specify \
+            the appropriate cluster using the --provider.cluster option",
+                component_id
+            ))
+            .to_string();
+        Err(anyhow!("Command failed with error: {}", error_message))
+    }
+}
+
+pub fn generate_component_type_file(
+    file_path: &Path,
+    cfg_override: &ConfigOverride,
+    component_id: &str,
+) -> Result<()> {
+    let url = discover_cluster_url(cfg_override)?;
+    let idl_string = fetch_idl_for_component(component_id, &url)?;
+    let idl: Idl = serde_json::from_str(&idl_string)?;
+    let mut file = File::create(file_path)?;
+    file.write_all(component_type(&idl, component_id)?.as_bytes())?;
+    Ok(())
+}
+
+pub fn append_component_to_lib_rs(lib_rs_path: &Path, component_id: &str) -> Result<()> {
+    let mut file = OpenOptions::new()
+        .create(true)
+        .append(true)
+        .open(lib_rs_path)?;
+    file.write_all(component_type_import(component_id).as_bytes())?;
+    Ok(())
+}

+ 18 - 200
cli/src/lib.rs

@@ -1,18 +1,24 @@
+mod component;
 mod rust_template;
+mod system;
+mod templates;
+mod workspace;
 
+use crate::component::new_component;
 use crate::rust_template::{create_component, create_system};
+use crate::system::new_system;
 use anchor_cli::config;
 use anchor_cli::config::{
     BootstrapMode, Config, ConfigOverride, GenesisEntry, ProgramArch, ProgramDeployment,
     TestValidator, Validator, WithPath,
 };
 use anchor_client::Cluster;
-use anchor_lang_idl::types::Idl;
 use anyhow::{anyhow, Result};
 use clap::{Parser, Subcommand};
+use component::{append_component_to_lib_rs, extract_component_id, generate_component_type_file};
 use heck::{ToKebabCase, ToSnakeCase};
 use std::collections::BTreeMap;
-use std::fs::{self, create_dir_all, File, OpenOptions};
+use std::fs::{self, create_dir_all, File};
 use std::io::Write;
 use std::io::{self, BufRead};
 use std::path::{Path, PathBuf};
@@ -23,7 +29,7 @@ pub const ANCHOR_VERSION: &str = anchor_cli::VERSION;
 
 pub const WORLD_PROGRAM: &str = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n";
 
-#[derive(Debug, Subcommand)]
+#[derive(Subcommand)]
 pub enum BoltCommand {
     #[clap(about = "Create a new component")]
     Component(ComponentCommand),
@@ -50,7 +56,7 @@ pub struct SystemCommand {
     pub name: String,
 }
 
-#[derive(Debug, Parser)]
+#[derive(Parser)]
 #[clap(version = VERSION)]
 pub struct Opts {
     /// Rebuild the auto-generated types
@@ -275,10 +281,10 @@ fn init(
     fs::write("Anchor.toml", toml)?;
 
     // Initialize .gitignore file
-    fs::write(".gitignore", rust_template::git_ignore())?;
+    fs::write(".gitignore", templates::workspace::git_ignore())?;
 
     // Initialize .prettierignore file
-    fs::write(".prettierignore", rust_template::prettier_ignore())?;
+    fs::write(".prettierignore", templates::workspace::prettier_ignore())?;
 
     // Remove the default programs if `--force` is passed
     if force {
@@ -349,21 +355,21 @@ fn init(
     if javascript {
         // Build javascript config
         let mut package_json = File::create("package.json")?;
-        package_json.write_all(rust_template::package_json(jest).as_bytes())?;
+        package_json.write_all(templates::workspace::package_json(jest).as_bytes())?;
 
         if jest {
             let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
             if solidity {
                 test.write_all(anchor_cli::solidity_template::jest(&project_name).as_bytes())?;
             } else {
-                test.write_all(rust_template::jest(&project_name).as_bytes())?;
+                test.write_all(templates::workspace::jest(&project_name).as_bytes())?;
             }
         } else {
             let mut test = File::create(format!("tests/{}.js", &project_name))?;
             if solidity {
                 test.write_all(anchor_cli::solidity_template::mocha(&project_name).as_bytes())?;
             } else {
-                test.write_all(rust_template::mocha(&project_name).as_bytes())?;
+                test.write_all(templates::workspace::mocha(&project_name).as_bytes())?;
             }
         }
 
@@ -376,7 +382,7 @@ fn init(
         ts_config.write_all(anchor_cli::rust_template::ts_config(jest).as_bytes())?;
 
         let mut ts_package_json = File::create("package.json")?;
-        ts_package_json.write_all(rust_template::ts_package_json(jest).as_bytes())?;
+        ts_package_json.write_all(templates::workspace::ts_package_json(jest).as_bytes())?;
 
         let mut deploy = File::create("migrations/deploy.ts")?;
         deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?;
@@ -385,7 +391,7 @@ fn init(
         if solidity {
             mocha.write_all(anchor_cli::solidity_template::ts_mocha(&project_name).as_bytes())?;
         } else {
-            mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?;
+            mocha.write_all(templates::workspace::ts_mocha(&project_name).as_bytes())?;
         }
     }
 
@@ -488,134 +494,6 @@ fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
         .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
 }
 
-// Create a new component from the template
-fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
-    with_workspace(cfg_override, |cfg| {
-        match cfg.path().parent() {
-            None => {
-                println!("Unable to make new component");
-            }
-            Some(parent) => {
-                std::env::set_current_dir(parent)?;
-
-                let cluster = cfg.provider.cluster.clone();
-                let programs = cfg.programs.entry(cluster).or_default();
-                if programs.contains_key(&name) {
-                    return Err(anyhow!("Program already exists"));
-                }
-
-                programs.insert(
-                    name.clone(),
-                    ProgramDeployment {
-                        address: {
-                            create_component(&name)?;
-                            anchor_cli::rust_template::get_or_create_program_id(&name)
-                        },
-                        path: None,
-                        idl: None,
-                    },
-                );
-
-                let toml = cfg.to_string();
-                fs::write("Anchor.toml", toml)?;
-
-                println!("Created new component: {}", name);
-            }
-        };
-        Ok(())
-    })
-}
-
-// Create a new system from the template
-fn new_system(cfg_override: &ConfigOverride, name: String) -> Result<()> {
-    with_workspace(cfg_override, |cfg| {
-        match cfg.path().parent() {
-            None => {
-                println!("Unable to make new system");
-            }
-            Some(parent) => {
-                std::env::set_current_dir(parent)?;
-
-                let cluster = cfg.provider.cluster.clone();
-                let programs = cfg.programs.entry(cluster).or_default();
-                if programs.contains_key(&name) {
-                    return Err(anyhow!("Program already exists"));
-                }
-
-                programs.insert(
-                    name.clone(),
-                    anchor_cli::config::ProgramDeployment {
-                        address: {
-                            rust_template::create_system(&name)?;
-                            anchor_cli::rust_template::get_or_create_program_id(&name)
-                        },
-                        path: None,
-                        idl: None,
-                    },
-                );
-
-                let toml = cfg.to_string();
-                fs::write("Anchor.toml", toml)?;
-
-                println!("Created new system: {}", name);
-            }
-        };
-        Ok(())
-    })
-}
-
-// with_workspace ensures the current working directory is always the top level
-// workspace directory, i.e., where the `Anchor.toml` file is located, before
-// and after the closure invocation.
-//
-// The closure passed into this function must never change the working directory
-// to be outside the workspace. Doing so will have undefined behavior.
-fn with_workspace<R>(
-    cfg_override: &ConfigOverride,
-    f: impl FnOnce(&mut WithPath<Config>) -> R,
-) -> R {
-    set_workspace_dir_or_exit();
-
-    let mut cfg = Config::discover(cfg_override)
-        .expect("Previously set the workspace dir")
-        .expect("Anchor.toml must always exist");
-
-    let r = f(&mut cfg);
-
-    set_workspace_dir_or_exit();
-
-    r
-}
-
-fn set_workspace_dir_or_exit() {
-    let d = match Config::discover(&ConfigOverride::default()) {
-        Err(err) => {
-            println!("Workspace configuration error: {err}");
-            std::process::exit(1);
-        }
-        Ok(d) => d,
-    };
-    match d {
-        None => {
-            println!("Not in anchor workspace.");
-            std::process::exit(1);
-        }
-        Some(cfg) => {
-            match cfg.path().parent() {
-                None => {
-                    println!("Unable to make new program");
-                }
-                Some(parent) => {
-                    if std::env::set_current_dir(parent).is_err() {
-                        println!("Not in anchor workspace.");
-                        std::process::exit(1);
-                    }
-                }
-            };
-        }
-    }
-}
-
 fn discover_cluster_url(cfg_override: &ConfigOverride) -> Result<String> {
     let url = match Config::discover(cfg_override)? {
         Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
@@ -668,7 +546,7 @@ fn build_dynamic_types(
         .join("Cargo.toml");
     if !cargo_path.exists() {
         let mut file = File::create(cargo_path)?;
-        file.write_all(rust_template::types_cargo_toml().as_bytes())?;
+        file.write_all(templates::workspace::types_cargo_toml().as_bytes())?;
     }
     std::env::set_current_dir(cur_dir)?;
     Ok(())
@@ -720,63 +598,3 @@ fn add_types_crate_dependency(program_name: &str, types_path: &str) -> Result<()
         })?;
     Ok(())
 }
-
-fn extract_component_id(line: &str) -> Option<&str> {
-    let component_id_marker = "#[component_id(";
-    line.find(component_id_marker).map(|start| {
-        let start = start + component_id_marker.len();
-        let end = line[start..].find(')').unwrap() + start;
-        line[start..end].trim_matches('"')
-    })
-}
-
-fn fetch_idl_for_component(component_id: &str, url: &str) -> Result<String> {
-    let output = std::process::Command::new("bolt")
-        .arg("idl")
-        .arg("fetch")
-        .arg(component_id)
-        .arg("--provider.cluster")
-        .arg(url)
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .output()?;
-
-    if output.status.success() {
-        let idl_string = String::from_utf8(output.stdout)
-            .map_err(|e| anyhow!("Failed to decode IDL output as UTF-8: {}", e))?
-            .to_string();
-        Ok(idl_string)
-    } else {
-        let error_message = String::from_utf8(output.stderr)
-            .unwrap_or(format!(
-                "Error trying to dynamically generate the type \
-            for component {}, unable to fetch the idl. \nEnsure that the idl is available. Specify \
-            the appropriate cluster using the --provider.cluster option",
-                component_id
-            ))
-            .to_string();
-        Err(anyhow!("Command failed with error: {}", error_message))
-    }
-}
-
-fn generate_component_type_file(
-    file_path: &Path,
-    cfg_override: &ConfigOverride,
-    component_id: &str,
-) -> Result<()> {
-    let url = discover_cluster_url(cfg_override)?;
-    let idl_string = fetch_idl_for_component(component_id, &url)?;
-    let idl: Idl = serde_json::from_str(&idl_string)?;
-    let mut file = File::create(file_path)?;
-    file.write_all(rust_template::component_type(&idl, component_id)?.as_bytes())?;
-    Ok(())
-}
-
-fn append_component_to_lib_rs(lib_rs_path: &Path, component_id: &str) -> Result<()> {
-    let mut file = OpenOptions::new()
-        .create(true)
-        .append(true)
-        .open(lib_rs_path)?;
-    file.write_all(rust_template::component_type_import(component_id).as_bytes())?;
-    Ok(())
-}

+ 8 - 707
cli/src/rust_template.rs

@@ -1,15 +1,15 @@
-use crate::VERSION;
-use anchor_cli::rust_template::{get_or_create_program_id, ProgramTemplate};
+use anchor_cli::rust_template::ProgramTemplate;
 use anchor_cli::{create_files, Files};
-use anchor_lang_idl::types::{
-    Idl, IdlArrayLen, IdlDefinedFields, IdlGenericArg, IdlType, IdlTypeDef, IdlTypeDefGeneric,
-    IdlTypeDefTy,
-};
+use anchor_lang_idl::types::{IdlArrayLen, IdlGenericArg, IdlType};
 use anyhow::Result;
-use heck::{ToSnakeCase, ToUpperCamelCase};
 use std::path::{Path, PathBuf};
 
-pub const ANCHOR_VERSION: &str = anchor_cli::VERSION;
+use crate::templates::component::create_component_template_simple;
+use crate::templates::program::{create_program_template_multiple, create_program_template_single};
+use crate::templates::system::create_system_template_simple;
+use crate::templates::workspace::{
+    cargo_toml, cargo_toml_with_serde, workspace_manifest, xargo_toml,
+};
 
 /// Create a component from the given name.
 pub fn create_component(name: &str) -> Result<()> {
@@ -60,531 +60,6 @@ pub fn create_program(name: &str, template: ProgramTemplate) -> Result<()> {
     create_files(&[common_files, template_files].concat())
 }
 
-/// Create a component which holds position data.
-fn create_component_template_simple(name: &str, program_path: &Path) -> Files {
-    vec![(
-        program_path.join("src").join("lib.rs"),
-        format!(
-            r#"use bolt_lang::*;
-
-declare_id!("{}");
-
-#[component]
-#[derive(Default)]
-pub struct {} {{
-    pub x: i64,
-    pub y: i64,
-    pub z: i64,
-    #[max_len(20)]
-    pub description: String,
-}}
-"#,
-            anchor_cli::rust_template::get_or_create_program_id(name),
-            name.to_upper_camel_case(),
-        ),
-    )]
-}
-
-/// Create a system which operates on a Position component.
-fn create_system_template_simple(name: &str, program_path: &Path) -> Files {
-    vec![(
-        program_path.join("src").join("lib.rs"),
-        format!(
-            r#"use bolt_lang::*;
-use position::Position;
-
-declare_id!("{}");
-
-#[system]
-pub mod {} {{
-
-    pub fn execute(ctx: Context<Components>, _args_p: Vec<u8>) -> Result<Components> {{
-        let position = &mut ctx.accounts.position;
-        position.x += 1;
-        position.y += 1;
-        Ok(ctx.accounts)
-    }}
-
-    #[system_input]
-    pub struct Components {{
-        pub position: Position,
-    }}
-
-}}
-"#,
-            anchor_cli::rust_template::get_or_create_program_id(name),
-            name.to_snake_case(),
-        ),
-    )]
-}
-
-fn create_program_template_single(name: &str, program_path: &Path) -> Files {
-    vec![(
-        program_path.join("src").join("lib.rs"),
-        format!(
-            r#"use anchor_lang::prelude::*;
-
-declare_id!("{}");
-
-#[program]
-pub mod {} {{
-    use super::*;
-
-    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
-        Ok(())
-    }}
-}}
-
-#[derive(Accounts)]
-pub struct Initialize {{}}
-"#,
-            get_or_create_program_id(name),
-            name.to_snake_case(),
-        ),
-    )]
-}
-
-/// Create a program with multiple files for instructions, state...
-fn create_program_template_multiple(name: &str, program_path: &Path) -> Files {
-    let src_path = program_path.join("src");
-    vec![
-        (
-            src_path.join("lib.rs"),
-            format!(
-                r#"pub mod constants;
-pub mod error;
-pub mod instructions;
-pub mod state;
-
-use anchor_lang::prelude::*;
-
-pub use constants::*;
-pub use instructions::*;
-pub use state::*;
-
-declare_id!("{}");
-
-#[program]
-pub mod {} {{
-    use super::*;
-
-    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
-        initialize::handler(ctx)
-    }}
-}}
-"#,
-                get_or_create_program_id(name),
-                name.to_snake_case(),
-            ),
-        ),
-        (
-            src_path.join("constants.rs"),
-            r#"use anchor_lang::prelude::*;
-
-#[constant]
-pub const SEED: &str = "anchor";
-"#
-            .into(),
-        ),
-        (
-            src_path.join("error.rs"),
-            r#"use anchor_lang::prelude::*;
-
-#[error_code]
-pub enum ErrorCode {
-    #[msg("Custom error message")]
-    CustomError,
-}
-"#
-            .into(),
-        ),
-        (
-            src_path.join("instructions").join("mod.rs"),
-            r#"pub mod initialize;
-
-pub use initialize::*;
-"#
-            .into(),
-        ),
-        (
-            src_path.join("instructions").join("initialize.rs"),
-            r#"use anchor_lang::prelude::*;
-
-#[derive(Accounts)]
-pub struct Initialize {}
-
-pub fn handler(ctx: Context<Initialize>) -> Result<()> {
-    Ok(())
-}
-"#
-            .into(),
-        ),
-        (src_path.join("state").join("mod.rs"), r#""#.into()),
-    ]
-}
-
-const fn workspace_manifest() -> &'static str {
-    r#"[workspace]
-members = [
-    "programs/*",
-    "programs-ecs/components/*",
-    "programs-ecs/systems/*"
-]
-resolver = "2"
-
-[profile.release]
-overflow-checks = true
-lto = "fat"
-codegen-units = 1
-[profile.release.build-override]
-opt-level = 3
-incremental = false
-codegen-units = 1
-"#
-}
-
-pub fn package_json(jest: bool) -> String {
-    if jest {
-        format!(
-            r#"{{
-        "scripts": {{
-            "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
-            "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
-        }},
-        "dependencies": {{
-            "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
-        }},
-        "devDependencies": {{
-            "jest": "^29.0.3",
-            "prettier": "^2.6.2"
-        }}
-    }}
-    "#
-        )
-    } else {
-        format!(
-            r#"{{
-    "scripts": {{
-        "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
-        "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
-    }},
-    "dependencies": {{
-        "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
-    }},
-    "devDependencies": {{
-        "chai": "^4.3.4",
-        "mocha": "^9.0.3",
-        "prettier": "^2.6.2",
-        "@metaplex-foundation/beet": "^0.7.1",
-        "@metaplex-foundation/beet-solana": "^0.4.0",
-         "@magicblock-labs/bolt-sdk": "latest"
-    }}
-}}
-"#
-        )
-    }
-}
-
-pub fn ts_package_json(jest: bool) -> String {
-    if jest {
-        format!(
-            r#"{{
-        "scripts": {{
-            "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
-            "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
-        }},
-        "dependencies": {{
-            "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
-        }},
-        "devDependencies": {{
-            "@types/bn.js": "^5.1.0",
-            "@types/jest": "^29.0.3",
-            "jest": "^29.0.3",
-            "prettier": "^2.6.2",
-            "ts-jest": "^29.0.2",
-            "typescript": "^4.3.5",
-            "@metaplex-foundation/beet": "^0.7.1",
-            "@metaplex-foundation/beet-solana": "^0.4.0",
-            "@magicblock-labs/bolt-sdk": "latest"
-        }}
-    }}
-    "#
-        )
-    } else {
-        format!(
-            r#"{{
-    "scripts": {{
-        "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
-        "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
-    }},
-    "dependencies": {{
-        "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
-    }},
-    "devDependencies": {{
-        "chai": "^4.3.4",
-        "mocha": "^9.0.3",
-        "ts-mocha": "^10.0.0",
-        "@types/bn.js": "^5.1.0",
-        "@types/chai": "^4.3.0",
-        "@types/mocha": "^9.0.0",
-        "typescript": "^4.3.5",
-        "prettier": "^2.6.2",
-        "@metaplex-foundation/beet": "^0.7.1",
-        "@metaplex-foundation/beet-solana": "^0.4.0",
-        "@magicblock-labs/bolt-sdk": "latest"
-    }}
-}}
-"#
-        )
-    }
-}
-
-pub fn mocha(name: &str) -> String {
-    format!(
-        r#"const anchor = require("@coral-xyz/anchor");
-const boltSdk = require("@magicblock-labs/bolt-sdk");
-const {{
-    InitializeNewWorld,
-}} = boltSdk;
-
-describe("{}", () => {{
-  // Configure the client to use the local cluster.
-  const provider = anchor.AnchorProvider.env();
-  anchor.setProvider(provider);
-
-  it("InitializeNewWorld", async () => {{
-    const initNewWorld = await InitializeNewWorld({{
-      payer: provider.wallet.publicKey,
-      connection: provider.connection,
-    }});
-    const txSign = await provider.sendAndConfirm(initNewWorld.transaction);
-    console.log(`Initialized a new world (ID=${{initNewWorld.worldPda}}). Initialization signature: ${{txSign}}`);
-    }});
-  }});
-}});
-"#,
-        name,
-    )
-}
-
-pub fn jest(name: &str) -> String {
-    format!(
-        r#"const anchor = require("@coral-xyz/anchor");
-const boltSdk = require("@magicblock-labs/bolt-sdk");
-const {{
-    InitializeNewWorld,
-}} = boltSdk;
-
-describe("{}", () => {{
-  // Configure the client to use the local cluster.
-  const provider = anchor.AnchorProvider.env();
-  anchor.setProvider(provider);
-
-  // Constants used to test the program.
-  let worldPda: PublicKey;
-
-  it("InitializeNewWorld", async () => {{
-    const initNewWorld = await InitializeNewWorld({{
-      payer: provider.wallet.publicKey,
-      connection: provider.connection,
-    }});
-    const txSign = await provider.sendAndConfirm(initNewWorld.transaction);
-    worldPda = initNewWorld.worldPda;
-    console.log(`Initialized a new world (ID=${{worldPda}}). Initialization signature: ${{txSign}}`);
-    }});
-  }});
-"#,
-        name,
-    )
-}
-
-pub fn ts_mocha(name: &str) -> String {
-    format!(
-        r#"import * as anchor from "@coral-xyz/anchor";
-import {{ Program }} from "@coral-xyz/anchor";
-import {{ PublicKey }} from "@solana/web3.js";
-import {{ Position }} from "../target/types/position";
-import {{ Movement }} from "../target/types/movement";
-import {{
-    InitializeNewWorld,
-    AddEntity,
-    InitializeComponent,
-    ApplySystem,
-}} from "@magicblock-labs/bolt-sdk"
-import {{expect}} from "chai";
-
-describe("{}", () => {{
-  // Configure the client to use the local cluster.
-  const provider = anchor.AnchorProvider.env();
-  anchor.setProvider(provider);
-
-  // Constants used to test the program.
-  let worldPda: PublicKey;
-  let entityPda: PublicKey;
-  let componentPda: PublicKey;
-
-  const positionComponent = anchor.workspace.Position as Program<Position>;
-  const systemMovement = anchor.workspace.Movement as Program<Movement>;
-
-  it("InitializeNewWorld", async () => {{
-    const initNewWorld = await InitializeNewWorld({{
-      payer: provider.wallet.publicKey,
-      connection: provider.connection,
-    }});
-    const txSign = await provider.sendAndConfirm(initNewWorld.transaction);
-    worldPda = initNewWorld.worldPda;
-    console.log(`Initialized a new world (ID=${{worldPda}}). Initialization signature: ${{txSign}}`);
-  }});
-
-  it("Add an entity", async () => {{
-    const addEntity = await AddEntity({{
-      payer: provider.wallet.publicKey,
-      world: worldPda,
-      connection: provider.connection,
-    }});
-    const txSign = await provider.sendAndConfirm(addEntity.transaction);
-    entityPda = addEntity.entityPda;
-    console.log(`Initialized a new Entity (ID=${{addEntity.entityId}}). Initialization signature: ${{txSign}}`);
-  }});
-
-  it("Add a component", async () => {{
-    const initializeComponent = await InitializeComponent({{
-      payer: provider.wallet.publicKey,
-      entity: entityPda,
-      componentId: positionComponent.programId,
-    }});
-    const txSign = await provider.sendAndConfirm(initializeComponent.transaction);
-    componentPda = initializeComponent.componentPda;
-    console.log(`Initialized the grid component. Initialization signature: ${{txSign}}`);
-  }});
-
-  it("Apply a system", async () => {{
-    // Check that the component has been initialized and x is 0
-    const positionBefore = await positionComponent.account.position.fetch(
-      componentPda
-    );
-    expect(positionBefore.x.toNumber()).to.equal(0);
-
-    // Run the movement system
-    const applySystem = await ApplySystem({{
-      authority: provider.wallet.publicKey,
-      systemId: systemMovement.programId,
-      entities: [{{
-        entity: entityPda,
-        components: [{{ componentId: positionComponent.programId }}],
-      }}]
-    }});
-    const txSign = await provider.sendAndConfirm(applySystem.transaction);
-    console.log(`Applied a system. Signature: ${{txSign}}`);
-
-    // Check that the system has been applied and x is > 0
-    const positionAfter = await positionComponent.account.position.fetch(
-      componentPda
-    );
-    expect(positionAfter.x.toNumber()).to.gt(0);
-  }});
-
-}});
-"#,
-        name.to_upper_camel_case(),
-    )
-}
-
-fn cargo_toml(name: &str) -> String {
-    format!(
-        r#"[package]
-name = "{0}"
-version = "{2}"
-description = "Created with Bolt"
-edition = "2021"
-
-[lib]
-crate-type = ["cdylib", "lib"]
-name = "{1}"
-
-[features]
-no-entrypoint = []
-no-idl = []
-no-log-ix-name = []
-cpi = ["no-entrypoint"]
-default = []
-idl-build = ["anchor-lang/idl-build"]
-
-[dependencies]
-bolt-lang = "{2}"
-anchor-lang = "{3}"
-"#,
-        name,
-        name.to_snake_case(),
-        VERSION,
-        ANCHOR_VERSION
-    )
-}
-
-/// TODO: Remove serde dependency
-fn cargo_toml_with_serde(name: &str) -> String {
-    format!(
-        r#"[package]
-name = "{0}"
-version = "{2}"
-description = "Created with Bolt"
-edition = "2021"
-
-[lib]
-crate-type = ["cdylib", "lib"]
-name = "{1}"
-
-[features]
-no-entrypoint = []
-no-idl = []
-no-log-ix-name = []
-cpi = ["no-entrypoint"]
-default = []
-idl-build = ["anchor-lang/idl-build"]
-
-[dependencies]
-bolt-lang = "{2}"
-anchor-lang = "{3}"
-serde = {{ version = "1.0", features = ["derive"] }}
-"#,
-        name,
-        name.to_snake_case(),
-        VERSION,
-        ANCHOR_VERSION
-    )
-}
-
-fn xargo_toml() -> &'static str {
-    r#"[target.bpfel-unknown-unknown.dependencies.std]
-features = []
-"#
-}
-pub fn git_ignore() -> &'static str {
-    r#"
-.anchor
-.bolt
-.DS_Store
-target
-**/*.rs.bk
-node_modules
-test-ledger
-.yarn
-"#
-}
-
-pub fn prettier_ignore() -> &'static str {
-    r#"
-.anchor
-.bolt
-.DS_Store
-target
-node_modules
-dist
-build
-test-ledger
-"#
-}
-
 pub fn registry_account() -> &'static str {
     r#"
 {
@@ -604,112 +79,6 @@ pub fn registry_account() -> &'static str {
 "#
 }
 
-/// Automatic generation of crates from the components idl
-
-pub fn component_type(idl: &Idl, component_id: &str) -> Result<String> {
-    let component_account = idl
-        .accounts
-        .iter()
-        .filter(|a| a.name.to_lowercase() != "Entity")
-        .last();
-    let component_account =
-        component_account.ok_or_else(|| anyhow::anyhow!("Component account not found in IDL"))?;
-
-    let type_def = &idl
-        .types
-        .iter()
-        .rfind(|ty| ty.name == component_account.name);
-    let type_def = match type_def {
-        Some(ty) => ty,
-        None => return Err(anyhow::anyhow!("Component type not found in IDL")),
-    };
-    let component_code = component_to_rust_code(type_def, component_id);
-    let types_code = component_types_to_rust_code(&idl.types, &component_account.name);
-    Ok(format!(
-        r#"use bolt_lang::*;
-
-#[component_deserialize]
-#[derive(Clone, Copy)]
-{}
-
-{}
-"#,
-        component_code, types_code
-    ))
-}
-
-/// Convert the component type definition to rust code
-fn component_to_rust_code(component: &IdlTypeDef, component_id: &str) -> String {
-    let mut code = String::new();
-    // Add documentation comments, if any
-    for doc in &component.docs {
-        code += &format!("/// {}\n", doc);
-    }
-    // Handle generics
-    let generics = {
-        let generic_names: Vec<String> = component
-            .generics
-            .iter()
-            .map(|gen| match gen {
-                IdlTypeDefGeneric::Type { name } => name.clone(),
-                IdlTypeDefGeneric::Const { name, .. } => name.clone(),
-            })
-            .collect();
-        if generic_names.is_empty() {
-            "".to_string()
-        } else {
-            format!("<{}>", generic_names.join(", "))
-        }
-    };
-    let composite_name = format!("Component{}", component_id);
-    if let IdlTypeDefTy::Struct { fields } = &component.ty {
-        code += &format!("pub struct {}{} {{\n", composite_name, generics);
-        code += &*component_fields_to_rust_code(fields);
-        code += "}\n\n";
-        code += &format!("pub use {} as {};", composite_name, component.name);
-    }
-    code
-}
-
-/// Code to expose the generated type, to be added to lib.rs
-pub fn component_type_import(component_id: &str) -> String {
-    format!(
-        r#"#[allow(non_snake_case)]
-mod component_{0};
-pub use component_{0}::*;
-"#,
-        component_id,
-    )
-}
-
-/// Convert fields to rust code
-fn component_fields_to_rust_code(fields: &Option<IdlDefinedFields>) -> String {
-    let mut code = String::new();
-    if let Some(fields) = fields {
-        match fields {
-            IdlDefinedFields::Named(named_fields) => {
-                for field in named_fields {
-                    if field.name.to_lowercase() == "bolt_metadata" {
-                        continue;
-                    }
-                    for doc in &field.docs {
-                        code += &format!("    /// {}\n", doc);
-                    }
-                    let field_type = convert_idl_type_to_str(&field.ty);
-                    code += &format!("    pub {}: {},\n", field.name, field_type);
-                }
-            }
-            IdlDefinedFields::Tuple(tuple_types) => {
-                for (index, ty) in tuple_types.iter().enumerate() {
-                    let field_type = convert_idl_type_to_str(ty);
-                    code += &format!("    pub field_{}: {},\n", index, field_type);
-                }
-            }
-        }
-    }
-    code
-}
-
 /// Map Idl type to rust type
 pub fn convert_idl_type_to_str(ty: &IdlType) -> String {
     match ty {
@@ -760,71 +129,3 @@ pub fn convert_idl_type_to_str(ty: &IdlType) -> String {
         _ => unimplemented!("{ty:?}"),
     }
 }
-
-/// Convert the component types definition to rust code
-fn component_types_to_rust_code(types: &[IdlTypeDef], component_name: &str) -> String {
-    types
-        .iter()
-        .filter(|ty| ty.name.to_lowercase() != "boltmetadata" && ty.name != component_name)
-        .map(component_type_to_rust_code)
-        .collect::<Vec<_>>()
-        .join("\n")
-}
-
-/// Convert the component type definition to rust code
-fn component_type_to_rust_code(component_type: &IdlTypeDef) -> String {
-    let mut code = String::new();
-    // Add documentation comments, if any
-    for doc in &component_type.docs {
-        code += &format!("/// {}\n", doc);
-    }
-    // Handle generics
-    let gen = &component_type.generics;
-    let generics = {
-        let generic_names: Vec<String> = gen
-            .iter()
-            .map(|gen| match gen {
-                IdlTypeDefGeneric::Type { name } => name.clone(),
-                IdlTypeDefGeneric::Const { name, .. } => name.clone(),
-            })
-            .collect();
-        if generic_names.is_empty() {
-            "".to_string()
-        } else {
-            format!("<{}>", generic_names.join(", "))
-        }
-    };
-    if let IdlTypeDefTy::Struct { fields } = &component_type.ty {
-        code += &format!(
-            "#[component_deserialize]\n#[derive(Clone, Copy)]\npub struct {}{} {{\n",
-            component_type.name, generics
-        );
-        code += &*component_fields_to_rust_code(fields);
-        code += "}\n\n";
-    }
-    code
-}
-
-pub(crate) fn types_cargo_toml() -> String {
-    let name = "bolt-types";
-    format!(
-        r#"[package]
-name = "{0}"
-version = "{2}"
-description = "Autogenerate types for the bolt language"
-edition = "2021"
-
-[lib]
-crate-type = ["cdylib", "lib"]
-name = "{1}"
-
-[dependencies]
-bolt-lang = "{2}"
-anchor-lang = "{3}"
-"#,
-        name,
-        name.to_snake_case(),
-        VERSION,
-        ANCHOR_VERSION
-    )
-}

+ 42 - 0
cli/src/system.rs

@@ -0,0 +1,42 @@
+use crate::{rust_template::create_system, workspace::with_workspace};
+use anchor_cli::config::ConfigOverride;
+use anyhow::{anyhow, Result};
+use std::fs;
+
+// Create a new system from the template
+pub fn new_system(cfg_override: &ConfigOverride, name: String) -> Result<()> {
+    with_workspace(cfg_override, |cfg| {
+        match cfg.path().parent() {
+            None => {
+                println!("Unable to make new system");
+            }
+            Some(parent) => {
+                std::env::set_current_dir(parent)?;
+
+                let cluster = cfg.provider.cluster.clone();
+                let programs = cfg.programs.entry(cluster).or_default();
+                if programs.contains_key(&name) {
+                    return Err(anyhow!("Program already exists"));
+                }
+
+                programs.insert(
+                    name.clone(),
+                    anchor_cli::config::ProgramDeployment {
+                        address: {
+                            create_system(&name)?;
+                            anchor_cli::rust_template::get_or_create_program_id(&name)
+                        },
+                        path: None,
+                        idl: None,
+                    },
+                );
+
+                let toml = cfg.to_string();
+                fs::write("Anchor.toml", toml)?;
+
+                println!("Created new system: {}", name);
+            }
+        };
+        Ok(())
+    })
+}

+ 182 - 0
cli/src/templates/component.rs

@@ -0,0 +1,182 @@
+use anchor_cli::Files;
+use anchor_lang_idl::types::{Idl, IdlDefinedFields, IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy};
+use anyhow::Result;
+use heck::ToUpperCamelCase;
+use std::path::Path;
+
+use crate::rust_template::convert_idl_type_to_str; // Import the trait
+
+/// Create a component which holds position data.
+pub fn create_component_template_simple(name: &str, program_path: &Path) -> Files {
+    vec![(
+        program_path.join("src").join("lib.rs"),
+        format!(
+            r#"use bolt_lang::*;
+
+declare_id!("{}");
+
+#[component]
+#[derive(Default)]
+pub struct {} {{
+    pub x: i64,
+    pub y: i64,
+    pub z: i64,
+    #[max_len(20)]
+    pub description: String,
+}}
+"#,
+            anchor_cli::rust_template::get_or_create_program_id(name),
+            name.to_upper_camel_case(),
+        ),
+    )]
+}
+
+/// Automatic generation of crates from the components idl
+
+pub fn component_type(idl: &Idl, component_id: &str) -> Result<String> {
+    let component_account = idl
+        .accounts
+        .iter()
+        .filter(|a| a.name.to_lowercase() != "Entity")
+        .last();
+    let component_account =
+        component_account.ok_or_else(|| anyhow::anyhow!("Component account not found in IDL"))?;
+
+    let type_def = &idl
+        .types
+        .iter()
+        .rfind(|ty| ty.name == component_account.name);
+    let type_def = match type_def {
+        Some(ty) => ty,
+        None => return Err(anyhow::anyhow!("Component type not found in IDL")),
+    };
+    let component_code = component_to_rust_code(type_def, component_id);
+    let types_code = component_types_to_rust_code(&idl.types, &component_account.name);
+    Ok(format!(
+        r#"use bolt_lang::*;
+
+#[component_deserialize]
+#[derive(Clone, Copy)]
+{}
+
+{}
+"#,
+        component_code, types_code
+    ))
+}
+
+/// Convert the component type definition to rust code
+fn component_to_rust_code(component: &IdlTypeDef, component_id: &str) -> String {
+    let mut code = String::new();
+    // Add documentation comments, if any
+    for doc in &component.docs {
+        code += &format!("/// {}\n", doc);
+    }
+    // Handle generics
+    let generics = {
+        let generic_names: Vec<String> = component
+            .generics
+            .iter()
+            .map(|gen| match gen {
+                IdlTypeDefGeneric::Type { name } => name.clone(),
+                IdlTypeDefGeneric::Const { name, .. } => name.clone(),
+            })
+            .collect();
+        if generic_names.is_empty() {
+            "".to_string()
+        } else {
+            format!("<{}>", generic_names.join(", "))
+        }
+    };
+    let composite_name = format!("Component{}", component_id);
+    if let IdlTypeDefTy::Struct { fields } = &component.ty {
+        code += &format!("pub struct {}{} {{\n", composite_name, generics);
+        code += &*component_fields_to_rust_code(fields);
+        code += "}\n\n";
+        code += &format!("pub use {} as {};", composite_name, component.name);
+    }
+    code
+}
+
+/// Code to expose the generated type, to be added to lib.rs
+pub fn component_type_import(component_id: &str) -> String {
+    format!(
+        r#"#[allow(non_snake_case)]
+mod component_{0};
+pub use component_{0}::*;
+"#,
+        component_id,
+    )
+}
+
+/// Convert fields to rust code
+fn component_fields_to_rust_code(fields: &Option<IdlDefinedFields>) -> String {
+    let mut code = String::new();
+    if let Some(fields) = fields {
+        match fields {
+            IdlDefinedFields::Named(named_fields) => {
+                for field in named_fields {
+                    if field.name.to_lowercase() == "bolt_metadata" {
+                        continue;
+                    }
+                    for doc in &field.docs {
+                        code += &format!("    /// {}\n", doc);
+                    }
+                    let field_type = convert_idl_type_to_str(&field.ty);
+                    code += &format!("    pub {}: {},\n", field.name, field_type);
+                }
+            }
+            IdlDefinedFields::Tuple(tuple_types) => {
+                for (index, ty) in tuple_types.iter().enumerate() {
+                    let field_type = convert_idl_type_to_str(ty);
+                    code += &format!("    pub field_{}: {},\n", index, field_type);
+                }
+            }
+        }
+    }
+    code
+}
+
+/// Convert the component types definition to rust code
+fn component_types_to_rust_code(types: &[IdlTypeDef], component_name: &str) -> String {
+    types
+        .iter()
+        .filter(|ty| ty.name.to_lowercase() != "boltmetadata" && ty.name != component_name)
+        .map(component_type_to_rust_code)
+        .collect::<Vec<_>>()
+        .join("\n")
+}
+
+/// Convert the component type definition to rust code
+fn component_type_to_rust_code(component_type: &IdlTypeDef) -> String {
+    let mut code = String::new();
+    // Add documentation comments, if any
+    for doc in &component_type.docs {
+        code += &format!("/// {}\n", doc);
+    }
+    // Handle generics
+    let gen = &component_type.generics;
+    let generics = {
+        let generic_names: Vec<String> = gen
+            .iter()
+            .map(|gen| match gen {
+                IdlTypeDefGeneric::Type { name } => name.clone(),
+                IdlTypeDefGeneric::Const { name, .. } => name.clone(),
+            })
+            .collect();
+        if generic_names.is_empty() {
+            "".to_string()
+        } else {
+            format!("<{}>", generic_names.join(", "))
+        }
+    };
+    if let IdlTypeDefTy::Struct { fields } = &component_type.ty {
+        code += &format!(
+            "#[component_deserialize]\n#[derive(Clone, Copy)]\npub struct {}{} {{\n",
+            component_type.name, generics
+        );
+        code += &*component_fields_to_rust_code(fields);
+        code += "}\n\n";
+    }
+    code
+}

+ 4 - 0
cli/src/templates/mod.rs

@@ -0,0 +1,4 @@
+pub mod component;
+pub mod program;
+pub mod system;
+pub mod workspace;

+ 108 - 0
cli/src/templates/program.rs

@@ -0,0 +1,108 @@
+use anchor_cli::{rust_template::get_or_create_program_id, Files};
+use heck::ToSnakeCase;
+use std::path::Path; // Import the trait
+
+pub fn create_program_template_single(name: &str, program_path: &Path) -> Files {
+    vec![(
+        program_path.join("src").join("lib.rs"),
+        format!(
+            r#"use anchor_lang::prelude::*;
+
+declare_id!("{}");
+
+#[program]
+pub mod {} {{
+    use super::*;
+
+    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
+        Ok(())
+    }}
+}}
+
+#[derive(Accounts)]
+pub struct Initialize {{}}
+"#,
+            get_or_create_program_id(name),
+            name.to_snake_case(),
+        ),
+    )]
+}
+
+/// Create a program with multiple files for instructions, state...
+pub fn create_program_template_multiple(name: &str, program_path: &Path) -> Files {
+    let src_path = program_path.join("src");
+    vec![
+        (
+            src_path.join("lib.rs"),
+            format!(
+                r#"pub mod constants;
+pub mod error;
+pub mod instructions;
+pub mod state;
+
+use anchor_lang::prelude::*;
+
+pub use constants::*;
+pub use instructions::*;
+pub use state::*;
+
+declare_id!("{}");
+
+#[program]
+pub mod {} {{
+    use super::*;
+
+    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
+        initialize::handler(ctx)
+    }}
+}}
+"#,
+                get_or_create_program_id(name),
+                name.to_snake_case(),
+            ),
+        ),
+        (
+            src_path.join("constants.rs"),
+            r#"use anchor_lang::prelude::*;
+
+#[constant]
+pub const SEED: &str = "anchor";
+"#
+            .into(),
+        ),
+        (
+            src_path.join("error.rs"),
+            r#"use anchor_lang::prelude::*;
+
+#[error_code]
+pub enum ErrorCode {
+    #[msg("Custom error message")]
+    CustomError,
+}
+"#
+            .into(),
+        ),
+        (
+            src_path.join("instructions").join("mod.rs"),
+            r#"pub mod initialize;
+
+pub use initialize::*;
+"#
+            .into(),
+        ),
+        (
+            src_path.join("instructions").join("initialize.rs"),
+            r#"use anchor_lang::prelude::*;
+
+#[derive(Accounts)]
+pub struct Initialize {}
+
+pub fn handler(ctx: Context<Initialize>) -> Result<()> {
+    Ok(())
+}
+"#
+            .into(),
+        ),
+        (src_path.join("state").join("mod.rs"), r#""#.into()),
+    ]
+}

+ 36 - 0
cli/src/templates/system.rs

@@ -0,0 +1,36 @@
+use anchor_cli::Files;
+use heck::ToSnakeCase;
+use std::path::Path;
+
+/// Create a system which operates on a Position component.
+pub fn create_system_template_simple(name: &str, program_path: &Path) -> Files {
+    vec![(
+        program_path.join("src").join("lib.rs"),
+        format!(
+            r#"use bolt_lang::*;
+use position::Position;
+
+declare_id!("{}");
+
+#[system]
+pub mod {} {{
+
+    pub fn execute(ctx: Context<Components>, _args_p: Vec<u8>) -> Result<Components> {{
+        let position = &mut ctx.accounts.position;
+        position.x += 1;
+        position.y += 1;
+        Ok(ctx.accounts)
+    }}
+
+    #[system_input]
+    pub struct Components {{
+        pub position: Position,
+    }}
+
+}}
+"#,
+            anchor_cli::rust_template::get_or_create_program_id(name),
+            name.to_snake_case(),
+        ),
+    )]
+}

+ 389 - 0
cli/src/templates/workspace.rs

@@ -0,0 +1,389 @@
+use crate::VERSION;
+use heck::{ToSnakeCase, ToUpperCamelCase};
+pub const ANCHOR_VERSION: &str = anchor_cli::VERSION;
+
+pub const fn workspace_manifest() -> &'static str {
+    r#"[workspace]
+members = [
+    "programs/*",
+    "programs-ecs/components/*",
+    "programs-ecs/systems/*"
+]
+resolver = "2"
+
+[profile.release]
+overflow-checks = true
+lto = "fat"
+codegen-units = 1
+[profile.release.build-override]
+opt-level = 3
+incremental = false
+codegen-units = 1
+"#
+}
+
+pub fn package_json(jest: bool) -> String {
+    if jest {
+        format!(
+            r#"{{
+        "scripts": {{
+            "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
+            "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
+        }},
+        "dependencies": {{
+            "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
+        }},
+        "devDependencies": {{
+            "jest": "^29.0.3",
+            "prettier": "^2.6.2"
+        }}
+    }}
+    "#
+        )
+    } else {
+        format!(
+            r#"{{
+    "scripts": {{
+        "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
+        "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
+    }},
+    "dependencies": {{
+        "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
+    }},
+    "devDependencies": {{
+        "chai": "^4.3.4",
+        "mocha": "^9.0.3",
+        "prettier": "^2.6.2",
+        "@metaplex-foundation/beet": "^0.7.1",
+        "@metaplex-foundation/beet-solana": "^0.4.0",
+         "@magicblock-labs/bolt-sdk": "latest"
+    }}
+}}
+"#
+        )
+    }
+}
+
+pub fn ts_package_json(jest: bool) -> String {
+    if jest {
+        format!(
+            r#"{{
+        "scripts": {{
+            "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
+            "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
+        }},
+        "dependencies": {{
+            "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
+        }},
+        "devDependencies": {{
+            "@types/bn.js": "^5.1.0",
+            "@types/jest": "^29.0.3",
+            "jest": "^29.0.3",
+            "prettier": "^2.6.2",
+            "ts-jest": "^29.0.2",
+            "typescript": "^4.3.5",
+            "@metaplex-foundation/beet": "^0.7.1",
+            "@metaplex-foundation/beet-solana": "^0.4.0",
+            "@magicblock-labs/bolt-sdk": "latest"
+        }}
+    }}
+    "#
+        )
+    } else {
+        format!(
+            r#"{{
+    "scripts": {{
+        "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
+        "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
+    }},
+    "dependencies": {{
+        "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
+    }},
+    "devDependencies": {{
+        "chai": "^4.3.4",
+        "mocha": "^9.0.3",
+        "ts-mocha": "^10.0.0",
+        "@types/bn.js": "^5.1.0",
+        "@types/chai": "^4.3.0",
+        "@types/mocha": "^9.0.0",
+        "typescript": "^4.3.5",
+        "prettier": "^2.6.2",
+        "@metaplex-foundation/beet": "^0.7.1",
+        "@metaplex-foundation/beet-solana": "^0.4.0",
+        "@magicblock-labs/bolt-sdk": "latest"
+    }}
+}}
+"#
+        )
+    }
+}
+
+pub fn mocha(name: &str) -> String {
+    format!(
+        r#"const anchor = require("@coral-xyz/anchor");
+const boltSdk = require("@magicblock-labs/bolt-sdk");
+const {{
+    InitializeNewWorld,
+}} = boltSdk;
+
+describe("{}", () => {{
+  // Configure the client to use the local cluster.
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  it("InitializeNewWorld", async () => {{
+    const initNewWorld = await InitializeNewWorld({{
+      payer: provider.wallet.publicKey,
+      connection: provider.connection,
+    }});
+    const txSign = await provider.sendAndConfirm(initNewWorld.transaction);
+    console.log(`Initialized a new world (ID=${{initNewWorld.worldPda}}). Initialization signature: ${{txSign}}`);
+    }});
+  }});
+}});
+"#,
+        name,
+    )
+}
+
+pub fn jest(name: &str) -> String {
+    format!(
+        r#"const anchor = require("@coral-xyz/anchor");
+const boltSdk = require("@magicblock-labs/bolt-sdk");
+const {{
+    InitializeNewWorld,
+}} = boltSdk;
+
+describe("{}", () => {{
+  // Configure the client to use the local cluster.
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  // Constants used to test the program.
+  let worldPda: PublicKey;
+
+  it("InitializeNewWorld", async () => {{
+    const initNewWorld = await InitializeNewWorld({{
+      payer: provider.wallet.publicKey,
+      connection: provider.connection,
+    }});
+    const txSign = await provider.sendAndConfirm(initNewWorld.transaction);
+    worldPda = initNewWorld.worldPda;
+    console.log(`Initialized a new world (ID=${{worldPda}}). Initialization signature: ${{txSign}}`);
+    }});
+  }});
+"#,
+        name,
+    )
+}
+
+pub fn ts_mocha(name: &str) -> String {
+    format!(
+        r#"import * as anchor from "@coral-xyz/anchor";
+import {{ Program }} from "@coral-xyz/anchor";
+import {{ PublicKey }} from "@solana/web3.js";
+import {{ Position }} from "../target/types/position";
+import {{ Movement }} from "../target/types/movement";
+import {{
+    InitializeNewWorld,
+    AddEntity,
+    InitializeComponent,
+    ApplySystem,
+}} from "@magicblock-labs/bolt-sdk"
+import {{expect}} from "chai";
+
+describe("{}", () => {{
+  // Configure the client to use the local cluster.
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  // Constants used to test the program.
+  let worldPda: PublicKey;
+  let entityPda: PublicKey;
+  let componentPda: PublicKey;
+
+  const positionComponent = anchor.workspace.Position as Program<Position>;
+  const systemMovement = anchor.workspace.Movement as Program<Movement>;
+
+  it("InitializeNewWorld", async () => {{
+    const initNewWorld = await InitializeNewWorld({{
+      payer: provider.wallet.publicKey,
+      connection: provider.connection,
+    }});
+    const txSign = await provider.sendAndConfirm(initNewWorld.transaction);
+    worldPda = initNewWorld.worldPda;
+    console.log(`Initialized a new world (ID=${{worldPda}}). Initialization signature: ${{txSign}}`);
+  }});
+
+  it("Add an entity", async () => {{
+    const addEntity = await AddEntity({{
+      payer: provider.wallet.publicKey,
+      world: worldPda,
+      connection: provider.connection,
+    }});
+    const txSign = await provider.sendAndConfirm(addEntity.transaction);
+    entityPda = addEntity.entityPda;
+    console.log(`Initialized a new Entity (ID=${{addEntity.entityId}}). Initialization signature: ${{txSign}}`);
+  }});
+
+  it("Add a component", async () => {{
+    const initializeComponent = await InitializeComponent({{
+      payer: provider.wallet.publicKey,
+      entity: entityPda,
+      componentId: positionComponent.programId,
+    }});
+    const txSign = await provider.sendAndConfirm(initializeComponent.transaction);
+    componentPda = initializeComponent.componentPda;
+    console.log(`Initialized the grid component. Initialization signature: ${{txSign}}`);
+  }});
+
+  it("Apply a system", async () => {{
+    // Check that the component has been initialized and x is 0
+    const positionBefore = await positionComponent.account.position.fetch(
+      componentPda
+    );
+    expect(positionBefore.x.toNumber()).to.equal(0);
+
+    // Run the movement system
+    const applySystem = await ApplySystem({{
+      authority: provider.wallet.publicKey,
+      systemId: systemMovement.programId,
+      entities: [{{
+        entity: entityPda,
+        components: [{{ componentId: positionComponent.programId }}],
+      }}]
+    }});
+    const txSign = await provider.sendAndConfirm(applySystem.transaction);
+    console.log(`Applied a system. Signature: ${{txSign}}`);
+
+    // Check that the system has been applied and x is > 0
+    const positionAfter = await positionComponent.account.position.fetch(
+      componentPda
+    );
+    expect(positionAfter.x.toNumber()).to.gt(0);
+  }});
+
+}});
+"#,
+        name.to_upper_camel_case(),
+    )
+}
+
+pub fn cargo_toml(name: &str) -> String {
+    format!(
+        r#"[package]
+name = "{0}"
+version = "{2}"
+description = "Created with Bolt"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "{1}"
+
+[features]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+cpi = ["no-entrypoint"]
+default = []
+idl-build = ["anchor-lang/idl-build"]
+
+[dependencies]
+bolt-lang = "{2}"
+anchor-lang = "{3}"
+"#,
+        name,
+        name.to_snake_case(),
+        VERSION,
+        ANCHOR_VERSION
+    )
+}
+
+/// TODO: Remove serde dependency
+pub fn cargo_toml_with_serde(name: &str) -> String {
+    format!(
+        r#"[package]
+name = "{0}"
+version = "{2}"
+description = "Created with Bolt"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "{1}"
+
+[features]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+cpi = ["no-entrypoint"]
+default = []
+idl-build = ["anchor-lang/idl-build"]
+
+[dependencies]
+bolt-lang = "{2}"
+anchor-lang = "{3}"
+serde = {{ version = "1.0", features = ["derive"] }}
+"#,
+        name,
+        name.to_snake_case(),
+        VERSION,
+        ANCHOR_VERSION
+    )
+}
+
+pub fn xargo_toml() -> &'static str {
+    r#"[target.bpfel-unknown-unknown.dependencies.std]
+features = []
+"#
+}
+pub fn git_ignore() -> &'static str {
+    r#"
+.anchor
+.bolt
+.DS_Store
+target
+**/*.rs.bk
+node_modules
+test-ledger
+.yarn
+"#
+}
+
+pub fn prettier_ignore() -> &'static str {
+    r#"
+.anchor
+.bolt
+.DS_Store
+target
+node_modules
+dist
+build
+test-ledger
+"#
+}
+
+pub(crate) fn types_cargo_toml() -> String {
+    let name = "bolt-types";
+    format!(
+        r#"[package]
+name = "{0}"
+version = "{2}"
+description = "Autogenerate types for the bolt language"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "{1}"
+
+[dependencies]
+bolt-lang = "{2}"
+anchor-lang = "{3}"
+"#,
+        name,
+        name.to_snake_case(),
+        VERSION,
+        ANCHOR_VERSION
+    )
+}

+ 53 - 0
cli/src/workspace.rs

@@ -0,0 +1,53 @@
+use anchor_cli::config::{Config, ConfigOverride, WithPath};
+
+// with_workspace ensures the current working directory is always the top level
+// workspace directory, i.e., where the `Anchor.toml` file is located, before
+// and after the closure invocation.
+//
+// The closure passed into this function must never change the working directory
+// to be outside the workspace. Doing so will have undefined behavior.
+pub fn with_workspace<R>(
+    cfg_override: &ConfigOverride,
+    f: impl FnOnce(&mut WithPath<Config>) -> R,
+) -> R {
+    set_workspace_dir_or_exit();
+
+    let mut cfg = Config::discover(cfg_override)
+        .expect("Previously set the workspace dir")
+        .expect("Anchor.toml must always exist");
+
+    let r = f(&mut cfg);
+
+    set_workspace_dir_or_exit();
+
+    r
+}
+
+pub fn set_workspace_dir_or_exit() {
+    let d = match Config::discover(&ConfigOverride::default()) {
+        Err(err) => {
+            println!("Workspace configuration error: {err}");
+            std::process::exit(1);
+        }
+        Ok(d) => d,
+    };
+    match d {
+        None => {
+            println!("Not in anchor workspace.");
+            std::process::exit(1);
+        }
+        Some(cfg) => {
+            match cfg.path().parent() {
+                None => {
+                    println!("Unable to make new program");
+                }
+                Some(parent) => {
+                    if std::env::set_current_dir(parent).is_err() {
+                        println!("Not in anchor workspace.");
+                        std::process::exit(1);
+                    }
+                }
+            };
+        }
+    }
+}