mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
- Updated the `clap` dependency in `Cargo.toml` to include the "env" feature for improved command-line argument handling. - Introduced a new `roblox_api` module to encapsulate Roblox API interactions, including authentication and asset downloading. - Refactored the `CloneCommand` and `StudioCommand` to utilize the new `roblox_api` functions for fetching experience names and universe IDs, enhancing code clarity and reducing duplication. - Removed obsolete functions related to Roblox API calls from the CLI commands, streamlining the codebase. - Added error handling for authentication failures, ensuring better user feedback when required credentials are missing. These changes aim to improve the overall structure and functionality of the CLI while enhancing the integration with the Roblox API.
192 lines
5.7 KiB
Rust
192 lines
5.7 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::{bail, Context};
|
|
use clap::Parser;
|
|
|
|
use crate::roblox_api;
|
|
|
|
use super::init::{InitCommand, InitKind};
|
|
use super::syncback::SyncbackCommand;
|
|
use super::GlobalOptions;
|
|
|
|
/// Initializes a new Rojo project from a Roblox place and syncs it back.
|
|
///
|
|
/// Fetches the experience name from Roblox to auto-generate the project
|
|
/// folder. Equivalent to `rojo init` + `rojo syncback --download`.
|
|
#[derive(Debug, Parser)]
|
|
pub struct CloneCommand {
|
|
/// The place ID to download and sync back.
|
|
pub placeid: u64,
|
|
|
|
/// Path to create the project in. If omitted, a directory is
|
|
/// auto-generated from the experience name.
|
|
#[clap(long)]
|
|
pub path: Option<PathBuf>,
|
|
|
|
/// The kind of project to create, 'place', 'plugin', or 'model'.
|
|
#[clap(long, default_value = "place")]
|
|
pub kind: InitKind,
|
|
|
|
/// Skips the initialization of a git repository.
|
|
#[clap(long)]
|
|
pub skip_git: bool,
|
|
|
|
/// Skip cloning cursor rules into .cursor directory.
|
|
#[clap(long)]
|
|
pub skip_rules: bool,
|
|
|
|
/// Skip changing the working directory into the project after creation.
|
|
#[clap(long)]
|
|
pub skip_cd: bool,
|
|
}
|
|
|
|
impl CloneCommand {
|
|
pub fn run(self, global: GlobalOptions) -> anyhow::Result<()> {
|
|
let path = match self.path {
|
|
Some(p) => p,
|
|
None => {
|
|
let auth = roblox_api::try_resolve_auth(global.opencloud.as_deref());
|
|
let name = match auth {
|
|
Some(a) => roblox_api::fetch_experience_name(self.placeid, &a)?
|
|
.context("Could not fetch experience name from Roblox API")?,
|
|
None => bail!(
|
|
"No --opencloud API key or Roblox cookie available to fetch experience name. \
|
|
Use --path to specify a folder manually, or pass --opencloud <KEY>."
|
|
),
|
|
};
|
|
let folder = sanitize_name(&name);
|
|
if folder.is_empty() {
|
|
bail!(
|
|
"Could not derive a folder name from experience '{}'. \
|
|
Use --path to specify one manually.",
|
|
name
|
|
);
|
|
}
|
|
if Path::new(&folder).exists() {
|
|
bail!(
|
|
"Directory '{}' already exists. \
|
|
Remove it or use --path to specify a different location.",
|
|
folder
|
|
);
|
|
}
|
|
println!("Using folder: {folder}");
|
|
PathBuf::from(folder)
|
|
}
|
|
};
|
|
|
|
let skip_git = self.skip_git;
|
|
|
|
let init = InitCommand {
|
|
path: path.clone(),
|
|
kind: self.kind,
|
|
skip_git,
|
|
placeid: Some(self.placeid),
|
|
skip_rules: self.skip_rules,
|
|
skip_cd: true,
|
|
};
|
|
|
|
init.run()?;
|
|
|
|
let syncback = SyncbackCommand {
|
|
project: PathBuf::from("default.project.json5"),
|
|
input: PathBuf::from("Project.rbxl"),
|
|
download: Some(self.placeid),
|
|
list: false,
|
|
dry_run: false,
|
|
interactive: false,
|
|
incremental: false,
|
|
sourcemap: false,
|
|
working_dir: path.clone(),
|
|
};
|
|
|
|
syncback.run(global)?;
|
|
|
|
if !skip_git {
|
|
crate::git::git_add_all_and_commit(&path, "syncback");
|
|
}
|
|
|
|
if !self.skip_cd {
|
|
std::env::set_current_dir(&path)
|
|
.with_context(|| format!("Failed to cd into {}", path.display()))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Sanitize a Roblox experience name into a valid folder name.
|
|
///
|
|
/// 1. Strip `[...]` and `(...)` sections
|
|
/// 2. Remove noise words (e.g. "testing")
|
|
/// 3. Keep only ASCII alphanumeric and spaces
|
|
/// 4. Collapse whitespace, lowercase, join with hyphens
|
|
fn sanitize_name(name: &str) -> String {
|
|
// Remove [...] and (...) sections (handles nesting)
|
|
let mut cleaned = String::with_capacity(name.len());
|
|
let mut bracket_depth: u32 = 0;
|
|
let mut paren_depth: u32 = 0;
|
|
|
|
for ch in name.chars() {
|
|
match ch {
|
|
'[' => bracket_depth += 1,
|
|
']' if bracket_depth > 0 => bracket_depth -= 1,
|
|
'(' => paren_depth += 1,
|
|
')' if paren_depth > 0 => paren_depth -= 1,
|
|
_ if bracket_depth == 0 && paren_depth == 0 => cleaned.push(ch),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Keep only ASCII alphanumeric + spaces, then collapse/join
|
|
cleaned
|
|
.split(|c: char| !(c.is_ascii_alphanumeric() || c == ' '))
|
|
.collect::<String>()
|
|
.split_whitespace()
|
|
.filter(|w| !w.eq_ignore_ascii_case("testing"))
|
|
.collect::<Vec<_>>()
|
|
.join("-")
|
|
.to_lowercase()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn sanitize_typical_name() {
|
|
assert_eq!(
|
|
sanitize_name("Escape Tsunami For Brainrots!"),
|
|
"escape-tsunami-for-brainrots"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_brackets_and_parens() {
|
|
assert_eq!(sanitize_name("My Game (UPDATE) [v2.0]"), "my-game");
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_emojis() {
|
|
assert_eq!(sanitize_name("Cool Game 🌊🏃⚡"), "cool-game");
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_complex() {
|
|
assert_eq!(
|
|
sanitize_name("some crazy game! (UPDATE) [LOL] 🎮"),
|
|
"some-crazy-game"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_only_special_chars() {
|
|
assert_eq!(sanitize_name("🎮🌊⚡"), "");
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_removes_testing() {
|
|
assert_eq!(sanitize_name("My Game Testing"), "my-game");
|
|
assert_eq!(sanitize_name("TESTING My Game"), "my-game");
|
|
assert_eq!(sanitize_name("My testing Game"), "my-game");
|
|
}
|
|
}
|