Compare commits
8 Commits
8da042ccd3
...
de2b804aad
| Author | SHA1 | Date | |
|---|---|---|---|
| de2b804aad | |||
| 55e92c4b4f | |||
| 02e72fc9d8 | |||
| 405cdaab81 | |||
| 933aa295b2 | |||
| 85aa369793 | |||
| 2af7d3fc2f | |||
| 11576a8693 |
1355
Cargo.lock
generated
1355
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,10 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.32", features = ["derive"] }
|
clap = { version = "4.5.32", features = ["derive"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
|
||||||
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
zip = "2.2.2"
|
zip = "2.2.2"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
anyhow = "1.0.97"
|
|
||||||
dirs = "5.0"
|
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
142
build.rs
Normal file
142
build.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
env, fs, io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_DIRS_FILE: &str = ".empty-dirs";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=examples");
|
||||||
|
|
||||||
|
let examples_root = PathBuf::from("examples");
|
||||||
|
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is not set"));
|
||||||
|
let out_file = out_dir.join("embedded_examples.rs");
|
||||||
|
|
||||||
|
let mut dirs = Vec::new();
|
||||||
|
let mut files = Vec::new();
|
||||||
|
let mut empty_dirs_by_example = BTreeMap::new();
|
||||||
|
|
||||||
|
if examples_root.is_dir() {
|
||||||
|
collect_examples(
|
||||||
|
&examples_root,
|
||||||
|
&examples_root,
|
||||||
|
&mut dirs,
|
||||||
|
&mut files,
|
||||||
|
&mut empty_dirs_by_example,
|
||||||
|
)
|
||||||
|
.expect("failed to collect example templates");
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs.sort();
|
||||||
|
dirs.dedup();
|
||||||
|
files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
for empty_dirs in empty_dirs_by_example.values_mut() {
|
||||||
|
empty_dirs.sort();
|
||||||
|
empty_dirs.dedup();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut generated = String::new();
|
||||||
|
generated.push_str(
|
||||||
|
"#[allow(dead_code)]\nstruct EmbeddedFile {\n path: &'static str,\n contents: &'static [u8],\n}\n\n",
|
||||||
|
);
|
||||||
|
generated.push_str("#[allow(dead_code)]\nstatic EMBEDDED_EXAMPLE_DIRS: &[&str] = &[\n");
|
||||||
|
for dir in &dirs {
|
||||||
|
generated.push_str(&format!(" {:?},\n", dir));
|
||||||
|
}
|
||||||
|
generated.push_str("];\n\n");
|
||||||
|
generated.push_str("#[allow(dead_code)]\nstatic EMBEDDED_EXAMPLE_FILES: &[EmbeddedFile] = &[\n");
|
||||||
|
for (relative_path, absolute_path) in &files {
|
||||||
|
generated.push_str(&format!(
|
||||||
|
" EmbeddedFile {{ path: {:?}, contents: include_bytes!(r#\"{}\"#) }},\n",
|
||||||
|
relative_path,
|
||||||
|
absolute_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
generated.push_str("];\n\n");
|
||||||
|
generated.push_str(
|
||||||
|
"#[allow(dead_code)]\nstruct EmbeddedExampleEmptyDirs {\n example: &'static str,\n dirs: &'static [&'static str],\n}\n\n",
|
||||||
|
);
|
||||||
|
generated.push_str(
|
||||||
|
"#[allow(dead_code)]\nstatic EMBEDDED_EXAMPLE_EMPTY_DIRS: &[EmbeddedExampleEmptyDirs] = &[\n",
|
||||||
|
);
|
||||||
|
for (example, empty_dirs) in &empty_dirs_by_example {
|
||||||
|
generated.push_str(&format!(
|
||||||
|
" EmbeddedExampleEmptyDirs {{ example: {:?}, dirs: &[\n",
|
||||||
|
example
|
||||||
|
));
|
||||||
|
for dir in empty_dirs {
|
||||||
|
generated.push_str(&format!(" {:?},\n", dir));
|
||||||
|
}
|
||||||
|
generated.push_str(" ] },\n");
|
||||||
|
}
|
||||||
|
generated.push_str("];\n");
|
||||||
|
|
||||||
|
fs::write(out_file, generated).expect("failed to write embedded example list");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_examples(
|
||||||
|
root: &Path,
|
||||||
|
dir: &Path,
|
||||||
|
dirs: &mut Vec<String>,
|
||||||
|
files: &mut Vec<(String, PathBuf)>,
|
||||||
|
empty_dirs_by_example: &mut BTreeMap<String, Vec<String>>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
for entry in fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
let relative_path = path
|
||||||
|
.strip_prefix(root)
|
||||||
|
.expect("example path is outside examples root")
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
dirs.push(relative_path);
|
||||||
|
collect_examples(root, &path, dirs, files, empty_dirs_by_example)?;
|
||||||
|
} else if path.is_file() {
|
||||||
|
if path.file_name().and_then(|name| name.to_str()) == Some(EMPTY_DIRS_FILE) {
|
||||||
|
collect_empty_dirs(&relative_path, &path, dirs, empty_dirs_by_example)?;
|
||||||
|
} else {
|
||||||
|
files.push((relative_path, path.canonicalize()?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_empty_dirs(
|
||||||
|
relative_path: &str,
|
||||||
|
path: &Path,
|
||||||
|
dirs: &mut Vec<String>,
|
||||||
|
empty_dirs_by_example: &mut BTreeMap<String, Vec<String>>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let (example, file_name) = relative_path.split_once('/').ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("{} must be inside an example directory", EMPTY_DIRS_FILE),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if file_name != EMPTY_DIRS_FILE {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("unexpected {} path: {}", EMPTY_DIRS_FILE, relative_path),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
let empty_dirs = empty_dirs_by_example
|
||||||
|
.entry(example.to_string())
|
||||||
|
.or_insert_with(Vec::new);
|
||||||
|
for line in contents.lines() {
|
||||||
|
let dir = line.trim();
|
||||||
|
if dir.is_empty() || dir.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dirs.push(format!("{}/{}", example, dir));
|
||||||
|
empty_dirs.push(dir.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
37
examples/default/.empty-dirs
Normal file
37
examples/default/.empty-dirs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
behavior_pack/BoxData
|
||||||
|
behavior_pack/entities
|
||||||
|
behavior_pack/items
|
||||||
|
behavior_pack/loot_tables
|
||||||
|
behavior_pack/netease_feature_rules
|
||||||
|
behavior_pack/netease_features
|
||||||
|
behavior_pack/netease_items_beh
|
||||||
|
behavior_pack/Parts
|
||||||
|
behavior_pack/Presets
|
||||||
|
behavior_pack/recipes
|
||||||
|
behavior_pack/spawn_rules
|
||||||
|
behavior_pack/structures
|
||||||
|
behavior_pack/trading
|
||||||
|
behavior_pack/Galaxy/Macro
|
||||||
|
behavior_pack/Galaxy/Template
|
||||||
|
behavior_pack/storyline/level
|
||||||
|
resource_pack/animation_controllers
|
||||||
|
resource_pack/animations
|
||||||
|
resource_pack/attachables
|
||||||
|
resource_pack/effects
|
||||||
|
resource_pack/entity
|
||||||
|
resource_pack/font
|
||||||
|
resource_pack/materials
|
||||||
|
resource_pack/models/animation
|
||||||
|
resource_pack/models/editor_materials
|
||||||
|
resource_pack/models/effect
|
||||||
|
resource_pack/models/geometry
|
||||||
|
resource_pack/models/mesh
|
||||||
|
resource_pack/models/netease_block
|
||||||
|
resource_pack/render_controllers
|
||||||
|
resource_pack/shaders/glsl
|
||||||
|
resource_pack/sounds
|
||||||
|
resource_pack/textures/entity
|
||||||
|
resource_pack/textures/models
|
||||||
|
resource_pack/textures/particle
|
||||||
|
resource_pack/textures/ui
|
||||||
|
resource_pack/ui
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::commands::ComponentsArgs;
|
use crate::commands::ComponentsArgs;
|
||||||
use crate::entity;
|
use crate::entity;
|
||||||
use crate::utils::file;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::utils::file;
|
||||||
use serde_json::{json, to_string_pretty};
|
use serde_json::{json, to_string_pretty};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -28,11 +28,12 @@ fn run_components(args: &ComponentsArgs) -> Result<()> {
|
|||||||
args.geo.as_deref().unwrap_or("./model.geo.json"),
|
args.geo.as_deref().unwrap_or("./model.geo.json"),
|
||||||
args.texture.as_deref().unwrap_or("./texture.png"),
|
args.texture.as_deref().unwrap_or("./texture.png"),
|
||||||
identifier,
|
identifier,
|
||||||
&project_path
|
&project_path,
|
||||||
),
|
),
|
||||||
_ => Err(crate::error::CliError::NotFound(
|
_ => Err(crate::error::CliError::NotFound(format!(
|
||||||
format!("组件 '{}' 不存在", args.component)
|
"组件 '{}' 不存在",
|
||||||
)),
|
args.component
|
||||||
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,15 +42,17 @@ fn validate_input_files(geo: &Option<String>, texture: &Option<String>) -> Resul
|
|||||||
let texture_path = texture.as_deref().unwrap_or("./texture.png");
|
let texture_path = texture.as_deref().unwrap_or("./texture.png");
|
||||||
|
|
||||||
if !PathBuf::from(geo_path).exists() {
|
if !PathBuf::from(geo_path).exists() {
|
||||||
return Err(crate::error::CliError::NotFound(
|
return Err(crate::error::CliError::NotFound(format!(
|
||||||
format!("几何文件 {} 不存在", geo_path)
|
"几何文件 {} 不存在",
|
||||||
));
|
geo_path
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !PathBuf::from(texture_path).exists() {
|
if !PathBuf::from(texture_path).exists() {
|
||||||
return Err(crate::error::CliError::NotFound(
|
return Err(crate::error::CliError::NotFound(format!(
|
||||||
format!("材质文件 {} 不存在", texture_path)
|
"材质文件 {} 不存在",
|
||||||
));
|
texture_path
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -137,12 +140,7 @@ fn create_resource_item_json(identifier: &str) -> serde_json::Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_assets(
|
fn copy_assets(res_path: &PathBuf, geo: &str, texture: &str, identifier: &str) -> Result<()> {
|
||||||
res_path: &PathBuf,
|
|
||||||
geo: &str,
|
|
||||||
texture: &str,
|
|
||||||
identifier: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let f_identifier = identifier.replace(":", "_");
|
let f_identifier = identifier.replace(":", "_");
|
||||||
|
|
||||||
copy_texture(res_path, texture, &f_identifier)?;
|
copy_texture(res_path, texture, &f_identifier)?;
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
commands::CreateArgs, entity::project::ProjectInfo, error::Result, template::TemplateEngine,
|
||||||
entity::project::ProjectInfo,
|
|
||||||
template::TemplateEngine,
|
|
||||||
utils::{file, http::HttpClient},
|
|
||||||
};
|
};
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use crate::commands::CreateArgs;
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::utils::git;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn execute(args: &CreateArgs, temp_dir: &PathBuf) {
|
include!(concat!(env!("OUT_DIR"), "/embedded_examples.rs"));
|
||||||
if let Err(e) = create_project(&args.name, args.target.as_deref(), temp_dir) {
|
|
||||||
eprintln!("错误: {}", e);
|
pub fn execute(args: &CreateArgs) {
|
||||||
|
if let Err(e) = create_project(&args.name, args.target.as_deref()) {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
println!("成功: 项目已创建");
|
println!("Success: project created.");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_project(name: &str, target: Option<&str>, temp_dir: &PathBuf) -> Result<()> {
|
fn create_project(name: &str, target: Option<&str>) -> Result<()> {
|
||||||
let target = target.unwrap_or("default");
|
let target = target.unwrap_or("default");
|
||||||
|
|
||||||
check_example_exists(target)?;
|
check_example_exists(target)?;
|
||||||
@@ -27,30 +22,16 @@ fn create_project(name: &str, target: Option<&str>, temp_dir: &PathBuf) -> Resul
|
|||||||
let local_dir = PathBuf::from(format!("./{}", name));
|
let local_dir = PathBuf::from(format!("./{}", name));
|
||||||
fs::create_dir(&local_dir)?;
|
fs::create_dir(&local_dir)?;
|
||||||
|
|
||||||
let template_dir = clone_and_copy_template(target, temp_dir, &local_dir)?;
|
copy_embedded_template(target, &local_dir)?;
|
||||||
|
initialize_project_with_template(&local_dir, &local_dir, name)?;
|
||||||
initialize_project_with_template(&template_dir, &local_dir, name)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_example_exists(target: &str) -> Result<()> {
|
fn check_example_exists(target: &str) -> Result<()> {
|
||||||
let check_url = format!(
|
if !embedded_example_exists(target) {
|
||||||
"https://api.github.com/repos/AiYo-Studio/emod-cli/contents/examples/{}",
|
|
||||||
target
|
|
||||||
);
|
|
||||||
|
|
||||||
let client = if cfg!(debug_assertions) {
|
|
||||||
HttpClient::new_with_proxy("http://127.0.0.1:1080")?
|
|
||||||
} else {
|
|
||||||
HttpClient::new()?
|
|
||||||
};
|
|
||||||
|
|
||||||
let resp = client.get(&check_url)?;
|
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
|
||||||
return Err(crate::error::CliError::NotFound(format!(
|
return Err(crate::error::CliError::NotFound(format!(
|
||||||
"示例模板 '{}' 不存在",
|
"example target '{}' was not found in the built-in examples",
|
||||||
target
|
target
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -58,21 +39,47 @@ fn check_example_exists(target: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clone_and_copy_template(
|
fn embedded_example_exists(target: &str) -> bool {
|
||||||
target: &str,
|
let target = normalize_embedded_path(target);
|
||||||
temp_dir: &PathBuf,
|
let prefix = format!("{}/", target);
|
||||||
local_dir: &PathBuf,
|
|
||||||
) -> Result<PathBuf> {
|
|
||||||
let _ = fs::remove_dir_all(format!("{}/tmp", temp_dir.display()));
|
|
||||||
|
|
||||||
let config = Config::load();
|
EMBEDDED_EXAMPLE_DIRS
|
||||||
let url = &config.repo_url;
|
.iter()
|
||||||
git::clone_remote_project(url.to_string(), temp_dir)?;
|
.any(|dir| *dir == target || dir.starts_with(&prefix))
|
||||||
|
|| EMBEDDED_EXAMPLE_FILES
|
||||||
|
.iter()
|
||||||
|
.any(|file| file.path.starts_with(&prefix))
|
||||||
|
}
|
||||||
|
|
||||||
let target_dir = PathBuf::from(format!("{}/tmp/examples/{}", temp_dir.display(), target));
|
fn copy_embedded_template(target: &str, local_dir: &PathBuf) -> Result<()> {
|
||||||
file::copy_folder(&target_dir, local_dir)?;
|
let target = normalize_embedded_path(target);
|
||||||
|
let prefix = format!("{}/", target);
|
||||||
|
|
||||||
Ok(target_dir)
|
for dir in EMBEDDED_EXAMPLE_DIRS {
|
||||||
|
if let Some(relative_path) = dir.strip_prefix(&prefix) {
|
||||||
|
if !relative_path.is_empty() {
|
||||||
|
fs::create_dir_all(local_dir.join(relative_path))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in EMBEDDED_EXAMPLE_FILES {
|
||||||
|
let Some(relative_path) = file.path.strip_prefix(&prefix) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_path = local_dir.join(relative_path);
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
fs::write(output_path, file.contents)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_embedded_path(path: &str) -> String {
|
||||||
|
path.trim_matches(['/', '\\']).replace('\\', "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize_project_with_template(
|
fn initialize_project_with_template(
|
||||||
@@ -80,14 +87,10 @@ fn initialize_project_with_template(
|
|||||||
local_dir: &PathBuf,
|
local_dir: &PathBuf,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let lower_name = format!(
|
let lower_name = lower_first_char(name)?;
|
||||||
"{}{}",
|
|
||||||
name.chars().next().unwrap().to_lowercase(),
|
|
||||||
&name[1..]
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("项目名称: {}", name);
|
println!("Project name: {}", name);
|
||||||
println!("标识名称: {}", lower_name);
|
println!("Lower project name: {}", lower_name);
|
||||||
|
|
||||||
let project_info = generate_project_info(name, &lower_name);
|
let project_info = generate_project_info(name, &lower_name);
|
||||||
|
|
||||||
@@ -124,10 +127,30 @@ fn initialize_project_with_template(
|
|||||||
);
|
);
|
||||||
|
|
||||||
engine.process_directory(local_dir)?;
|
engine.process_directory(local_dir)?;
|
||||||
|
remove_template_config(local_dir)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_template_config(local_dir: &PathBuf) -> Result<()> {
|
||||||
|
let template_config = local_dir.join("template.toml");
|
||||||
|
if template_config.exists() {
|
||||||
|
fs::remove_file(template_config)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_first_char(name: &str) -> Result<String> {
|
||||||
|
let mut chars = name.chars();
|
||||||
|
let Some(first) = chars.next() else {
|
||||||
|
return Err(crate::error::CliError::InvalidInput(
|
||||||
|
"project name cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(format!("{}{}", first.to_lowercase(), chars.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_project_info(name: &str, lower_name: &str) -> ProjectInfo {
|
fn generate_project_info(name: &str, lower_name: &str) -> ProjectInfo {
|
||||||
ProjectInfo {
|
ProjectInfo {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
|||||||
67
src/commands/init.rs
Normal file
67
src/commands/init.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use crate::commands::InitArgs;
|
||||||
|
use crate::entity;
|
||||||
|
use crate::error::{CliError, Result};
|
||||||
|
use crate::utils::file;
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/embedded_examples.rs"));
|
||||||
|
|
||||||
|
pub fn execute(args: &InitArgs) {
|
||||||
|
if let Err(e) = run_init(args) {
|
||||||
|
eprintln!("❌ 项目初始化失败: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("🍀 项目初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_init(args: &InitArgs) -> Result<()> {
|
||||||
|
let project_dir = file::find_project_dir(&args.path)?;
|
||||||
|
let target = args.target.as_deref().unwrap_or("default");
|
||||||
|
let empty_dirs = lookup_empty_dirs(target)?;
|
||||||
|
let release_info = entity::get_current_release_info(&project_dir)?;
|
||||||
|
|
||||||
|
let behavior_pack = format!("behavior_pack_{}", release_info.behavior_identifier);
|
||||||
|
let resource_pack = format!("resource_pack_{}", release_info.resource_identifier);
|
||||||
|
|
||||||
|
let mut created = 0usize;
|
||||||
|
let mut skipped = 0usize;
|
||||||
|
|
||||||
|
for rel in empty_dirs {
|
||||||
|
let rewritten = rewrite_pack_path(rel, &behavior_pack, &resource_pack);
|
||||||
|
let abs = project_dir.join(&rewritten);
|
||||||
|
if abs.is_dir() {
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(&abs).map_err(|e| file::io_error("创建目录", &abs, e))?;
|
||||||
|
println!("📁 创建目录: {}", rewritten);
|
||||||
|
created += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ 新建 {} 个目录, 跳过 {} 个已存在目录", created, skipped);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_empty_dirs(target: &str) -> Result<&'static [&'static str]> {
|
||||||
|
EMBEDDED_EXAMPLE_EMPTY_DIRS
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.example == target)
|
||||||
|
.map(|e| e.dirs)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CliError::NotFound(format!(
|
||||||
|
"example target '{}' was not found in the built-in examples",
|
||||||
|
target
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_pack_path(rel: &str, behavior: &str, resource: &str) -> String {
|
||||||
|
if let Some(rest) = rel.strip_prefix("behavior_pack/") {
|
||||||
|
format!("{}/{}", behavior, rest)
|
||||||
|
} else if let Some(rest) = rel.strip_prefix("resource_pack/") {
|
||||||
|
format!("{}/{}", resource, rest)
|
||||||
|
} else {
|
||||||
|
rel.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
use clap::{arg, Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand, arg};
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
|
pub mod init;
|
||||||
pub mod release;
|
pub mod release;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -24,6 +25,8 @@ pub enum Commands {
|
|||||||
Release(ReleaseArgs),
|
Release(ReleaseArgs),
|
||||||
/// Create a new mod project
|
/// Create a new mod project
|
||||||
Create(CreateArgs),
|
Create(CreateArgs),
|
||||||
|
/// Initialize standard empty directories for an existing project
|
||||||
|
Init(InitArgs),
|
||||||
/// Create a new component
|
/// Create a new component
|
||||||
Components(ComponentsArgs),
|
Components(ComponentsArgs),
|
||||||
}
|
}
|
||||||
@@ -34,8 +37,12 @@ pub struct ReleaseArgs {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
/// The version of the project
|
/// The version of the project
|
||||||
#[arg(short, long)]
|
#[arg(short, long, conflicts_with = "pin")]
|
||||||
pub ver: Option<String>,
|
pub ver: Option<String>,
|
||||||
|
/// Reuse the current version without auto-incrementing.
|
||||||
|
/// Useful when retrying after a failed release that already wrote new version files.
|
||||||
|
#[arg(short = 'P', long, conflicts_with = "ver")]
|
||||||
|
pub pin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
@@ -48,6 +55,16 @@ pub struct CreateArgs {
|
|||||||
pub target: Option<String>,
|
pub target: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct InitArgs {
|
||||||
|
/// The path of the project (default: current directory)
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub path: Option<String>,
|
||||||
|
/// Example target whose layout to apply
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub target: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct ComponentsArgs {
|
pub struct ComponentsArgs {
|
||||||
/// The path of the project
|
/// The path of the project
|
||||||
@@ -64,5 +81,5 @@ pub struct ComponentsArgs {
|
|||||||
pub texture: Option<String>,
|
pub texture: Option<String>,
|
||||||
/// The item's identifier
|
/// The item's identifier
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub identifier: Option<String>
|
pub identifier: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,40 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct Config {
|
|
||||||
#[serde(default = "default_repo_url")]
|
|
||||||
pub repo_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_repo_url() -> String {
|
|
||||||
"https://github.com/AiYo-Studio/emod-cli.git".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
repo_url: default_repo_url(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn load() -> Self {
|
|
||||||
let config_path = Self::config_path();
|
|
||||||
|
|
||||||
if let Ok(content) = fs::read_to_string(&config_path) {
|
|
||||||
if let Ok(config) = serde_json::from_str(&content) {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_path() -> PathBuf {
|
|
||||||
let home = dirs::home_dir().expect("无法获取用户主目录");
|
|
||||||
home.join(".emod-cli.json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
src/error.rs
36
src/error.rs
@@ -1,13 +1,11 @@
|
|||||||
use std::io;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::io;
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CliError {
|
pub enum CliError {
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
Json(serde_json::Error),
|
Json(serde_json::Error),
|
||||||
Network(reqwest::Error),
|
|
||||||
Anyhow(anyhow::Error),
|
|
||||||
Zip(zip::result::ZipError),
|
Zip(zip::result::ZipError),
|
||||||
Walkdir(walkdir::Error),
|
Walkdir(walkdir::Error),
|
||||||
Parse(ParseIntError),
|
Parse(ParseIntError),
|
||||||
@@ -20,17 +18,15 @@ pub enum CliError {
|
|||||||
impl fmt::Display for CliError {
|
impl fmt::Display for CliError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
CliError::Io(e) => write!(f, "IO错误: {}", e),
|
CliError::Io(e) => write!(f, "I/O error: {}", e),
|
||||||
CliError::Json(e) => write!(f, "JSON解析错误: {}", e),
|
CliError::Json(e) => write!(f, "JSON parse error: {}", e),
|
||||||
CliError::Network(e) => write!(f, "网络错误: {}", e),
|
CliError::Zip(e) => write!(f, "ZIP error: {}", e),
|
||||||
CliError::Anyhow(e) => write!(f, "{}", e),
|
CliError::Walkdir(e) => write!(f, "directory traversal error: {}", e),
|
||||||
CliError::Zip(e) => write!(f, "压缩错误: {}", e),
|
CliError::Parse(e) => write!(f, "parse error: {}", e),
|
||||||
CliError::Walkdir(e) => write!(f, "目录遍历错误: {}", e),
|
CliError::Toml(e) => write!(f, "TOML parse error: {}", e),
|
||||||
CliError::Parse(e) => write!(f, "解析错误: {}", e),
|
CliError::NotFound(msg) => write!(f, "not found: {}", msg),
|
||||||
CliError::Toml(e) => write!(f, "TOML解析错误: {}", e),
|
CliError::InvalidData(msg) => write!(f, "invalid data: {}", msg),
|
||||||
CliError::NotFound(msg) => write!(f, "未找到: {}", msg),
|
CliError::InvalidInput(msg) => write!(f, "invalid input: {}", msg),
|
||||||
CliError::InvalidData(msg) => write!(f, "无效数据: {}", msg),
|
|
||||||
CliError::InvalidInput(msg) => write!(f, "无效输入: {}", msg),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,18 +45,6 @@ impl From<serde_json::Error> for CliError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::Error> for CliError {
|
|
||||||
fn from(err: reqwest::Error) -> Self {
|
|
||||||
CliError::Network(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<anyhow::Error> for CliError {
|
|
||||||
fn from(err: anyhow::Error) -> Self {
|
|
||||||
CliError::Anyhow(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<zip::result::ZipError> for CliError {
|
impl From<zip::result::ZipError> for CliError {
|
||||||
fn from(err: zip::result::ZipError) -> Self {
|
fn from(err: zip::result::ZipError) -> Self {
|
||||||
CliError::Zip(err)
|
CliError::Zip(err)
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@@ -1,29 +1,18 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod entity;
|
mod entity;
|
||||||
mod utils;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod config;
|
|
||||||
mod template;
|
mod template;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use crate::commands::{Cli, Commands};
|
use crate::commands::{Cli, Commands};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::{env, fs, path::PathBuf};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let temp_dir = check_temp_dir();
|
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Commands::Release(args) => commands::release::execute(args),
|
Commands::Release(args) => commands::release::execute(args),
|
||||||
Commands::Create(args) => commands::create::execute(args, &temp_dir),
|
Commands::Create(args) => commands::create::execute(args),
|
||||||
|
Commands::Init(args) => commands::init::execute(args),
|
||||||
Commands::Components(args) => commands::components::execute(args),
|
Commands::Components(args) => commands::components::execute(args),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_temp_dir() -> PathBuf {
|
|
||||||
let mut temp_dir = env::temp_dir();
|
|
||||||
temp_dir.push("emod-cli");
|
|
||||||
if let Err(e) = fs::create_dir_all(&temp_dir) {
|
|
||||||
eprintln!("Error: Failed to create temp directory: {}", e);
|
|
||||||
}
|
|
||||||
temp_dir
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use regex::Regex;
|
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
@@ -62,7 +62,7 @@ impl TemplateEngine {
|
|||||||
for (key, var_config) in &self.config.variables {
|
for (key, var_config) in &self.config.variables {
|
||||||
if var_config.required && !self.variables.contains_key(key) {
|
if var_config.required && !self.variables.contains_key(key) {
|
||||||
return Err(crate::error::CliError::InvalidInput(format!(
|
return Err(crate::error::CliError::InvalidInput(format!(
|
||||||
"缺少必需的变量: {} ({})",
|
"missing required template variable: {} ({})",
|
||||||
key, var_config.description
|
key, var_config.description
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -72,31 +72,19 @@ impl TemplateEngine {
|
|||||||
|
|
||||||
pub fn process_directory(&self, dir: &Path) -> crate::error::Result<()> {
|
pub fn process_directory(&self, dir: &Path) -> crate::error::Result<()> {
|
||||||
self.validate_variables()?;
|
self.validate_variables()?;
|
||||||
|
|
||||||
self.replace_in_files(dir)?;
|
self.replace_in_files(dir)?;
|
||||||
|
|
||||||
self.apply_renames(dir)?;
|
self.apply_renames(dir)?;
|
||||||
|
|
||||||
self.verify_no_placeholders(dir)?;
|
self.verify_no_placeholders(dir)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_in_files(&self, dir: &Path) -> crate::error::Result<()> {
|
fn replace_in_files(&self, dir: &Path) -> crate::error::Result<()> {
|
||||||
let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap();
|
let placeholder_regex = placeholder_regex();
|
||||||
|
|
||||||
for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
|
for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if !path.is_file() {
|
if !path.is_file() || !self.should_process(path) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
|
||||||
if !self.config.process.file_extensions.contains(&ext.to_string()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +93,7 @@ impl TemplateEngine {
|
|||||||
|
|
||||||
for cap in placeholder_regex.captures_iter(&content) {
|
for cap in placeholder_regex.captures_iter(&content) {
|
||||||
let placeholder = &cap[0];
|
let placeholder = &cap[0];
|
||||||
let var_name = &cap[1];
|
let var_name = placeholder_name(&cap);
|
||||||
|
|
||||||
if let Some(value) = self.variables.get(var_name) {
|
if let Some(value) = self.variables.get(var_name) {
|
||||||
updated = updated.replace(placeholder, value);
|
updated = updated.replace(placeholder, value);
|
||||||
@@ -114,7 +102,7 @@ impl TemplateEngine {
|
|||||||
|
|
||||||
if updated != content {
|
if updated != content {
|
||||||
fs::write(path, updated)?;
|
fs::write(path, updated)?;
|
||||||
println!(" - 处理文件: {}", path.display());
|
println!(" - processed template file: {}", path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +110,7 @@ impl TemplateEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_renames(&self, dir: &Path) -> crate::error::Result<()> {
|
fn apply_renames(&self, dir: &Path) -> crate::error::Result<()> {
|
||||||
for rule in self.config.renames.iter().rev() {
|
for rule in &self.config.renames {
|
||||||
let from_path = dir.join(&rule.from);
|
let from_path = dir.join(&rule.from);
|
||||||
|
|
||||||
if !from_path.exists() {
|
if !from_path.exists() {
|
||||||
@@ -137,19 +125,19 @@ impl TemplateEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs::rename(&from_path, &to_path)?;
|
fs::rename(&from_path, &to_path)?;
|
||||||
println!(" - 重命名: {} -> {}", rule.from, to_path_str);
|
println!(" - renamed: {} -> {}", rule.from, to_path_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_placeholders(&self, text: &str) -> String {
|
fn replace_placeholders(&self, text: &str) -> String {
|
||||||
let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap();
|
let placeholder_regex = placeholder_regex();
|
||||||
let mut result = text.to_string();
|
let mut result = text.to_string();
|
||||||
|
|
||||||
for cap in placeholder_regex.captures_iter(text) {
|
for cap in placeholder_regex.captures_iter(text) {
|
||||||
let placeholder = &cap[0];
|
let placeholder = &cap[0];
|
||||||
let var_name = &cap[1];
|
let var_name = placeholder_name(&cap);
|
||||||
|
|
||||||
if let Some(value) = self.variables.get(var_name) {
|
if let Some(value) = self.variables.get(var_name) {
|
||||||
result = result.replace(placeholder, value);
|
result = result.replace(placeholder, value);
|
||||||
@@ -160,39 +148,58 @@ impl TemplateEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn verify_no_placeholders(&self, dir: &Path) -> crate::error::Result<()> {
|
fn verify_no_placeholders(&self, dir: &Path) -> crate::error::Result<()> {
|
||||||
let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap();
|
let placeholder_regex = placeholder_regex();
|
||||||
let mut found_placeholders = Vec::new();
|
let mut found_placeholders = Vec::new();
|
||||||
|
|
||||||
for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
|
for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if !path.is_file() {
|
if !path.is_file() || !self.should_process(path) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
|
||||||
if !self.config.process.file_extensions.contains(&ext.to_string()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(path)?;
|
let content = fs::read_to_string(path)?;
|
||||||
|
|
||||||
for cap in placeholder_regex.captures_iter(&content) {
|
for cap in placeholder_regex.captures_iter(&content) {
|
||||||
let var_name = &cap[1];
|
let var_name = placeholder_name(&cap);
|
||||||
|
if !self.config.variables.contains_key(var_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
found_placeholders.push(format!("{}:{}", path.display(), var_name));
|
found_placeholders.push(format!("{}:{}", path.display(), var_name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found_placeholders.is_empty() {
|
if !found_placeholders.is_empty() {
|
||||||
eprintln!("警告: 发现未替换的占位符:");
|
return Err(crate::error::CliError::InvalidData(format!(
|
||||||
for placeholder in found_placeholders {
|
"unresolved template placeholders: {}",
|
||||||
eprintln!(" - {}", placeholder);
|
found_placeholders.join(", ")
|
||||||
}
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_process(&self, path: &Path) -> bool {
|
||||||
|
path.extension()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|ext| {
|
||||||
|
self.config
|
||||||
|
.process
|
||||||
|
.file_extensions
|
||||||
|
.iter()
|
||||||
|
.any(|configured| configured == ext)
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_regex() -> Regex {
|
||||||
|
Regex::new(r"\{\{(\w+)\}\}|__(\w+)__").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_name<'a>(cap: &'a regex::Captures<'a>) -> &'a str {
|
||||||
|
cap.get(1)
|
||||||
|
.or_else(|| cap.get(2))
|
||||||
|
.expect("placeholder capture is missing")
|
||||||
|
.as_str()
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,21 @@
|
|||||||
use crate::error::Result;
|
use crate::error::{CliError, Result};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{fs, path::PathBuf};
|
use std::{
|
||||||
|
fs, io,
|
||||||
pub fn copy_folder(src: &PathBuf, dest: &PathBuf) -> Result<()> {
|
path::{Path, PathBuf},
|
||||||
if !src.exists() || !src.is_dir() {
|
};
|
||||||
return Err(crate::error::CliError::NotFound(format!(
|
|
||||||
"源目录不存在: {}",
|
|
||||||
src.display()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
if !dest.exists() {
|
|
||||||
fs::create_dir_all(dest)?;
|
|
||||||
}
|
|
||||||
for entry in fs::read_dir(src)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let src_path = entry.path();
|
|
||||||
let dest_path = dest.join(src_path.file_name().unwrap());
|
|
||||||
if src_path.is_file() {
|
|
||||||
fs::copy(&src_path, &dest_path)?;
|
|
||||||
} else if src_path.is_dir() {
|
|
||||||
copy_folder(&src_path, &dest_path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_file_to_json(path: &PathBuf) -> Result<Value> {
|
pub fn read_file_to_json(path: &PathBuf) -> Result<Value> {
|
||||||
let file = fs::read_to_string(path)?;
|
let content = fs::read_to_string(path).map_err(|e| io_error("读取文件", path, e))?;
|
||||||
let json: Value = serde_json::from_str(&file)?;
|
serde_json::from_str(&content)
|
||||||
Ok(json)
|
.map_err(|e| CliError::InvalidData(format!("解析 JSON '{}' 失败: {}", path.display(), e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_json_to_file(path: &PathBuf, value: &Value) -> Result<()> {
|
pub fn write_json_to_file(path: &PathBuf, value: &Value) -> Result<()> {
|
||||||
let content = serde_json::to_string_pretty(value)?;
|
let content = serde_json::to_string_pretty(value).map_err(|e| {
|
||||||
fs::write(path, content)?;
|
CliError::InvalidData(format!("序列化 JSON '{}' 失败: {}", path.display(), e))
|
||||||
|
})?;
|
||||||
|
fs::write(path, content).map_err(|e| io_error("写入文件", path, e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,3 +33,12 @@ pub fn find_project_dir(path: &Option<String>) -> Result<PathBuf> {
|
|||||||
let path = path.as_deref().unwrap_or(".");
|
let path = path.as_deref().unwrap_or(".");
|
||||||
Ok(PathBuf::from(path))
|
Ok(PathBuf::from(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wraps an [`io::Error`] with the operation name and path so error messages
|
||||||
|
/// stop being anonymous "os error 3" strings.
|
||||||
|
pub fn io_error(op: &str, path: &Path, err: io::Error) -> CliError {
|
||||||
|
CliError::Io(io::Error::new(
|
||||||
|
err.kind(),
|
||||||
|
format!("{} '{}' 失败: {}", op, path.display(), err),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use crate::error::Result;
|
|
||||||
|
|
||||||
pub fn clone_remote_project(url: String, temp_dir: &PathBuf) -> Result<()> {
|
|
||||||
std::process::Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg(url)
|
|
||||||
.arg(format!("{}/tmp", temp_dir.display()))
|
|
||||||
.output()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
use reqwest::blocking::Client;
|
|
||||||
use crate::error::Result;
|
|
||||||
|
|
||||||
pub struct HttpClient {
|
|
||||||
client: Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HttpClient {
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
let client = Client::builder().build()?;
|
|
||||||
Ok(Self { client })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_with_proxy(proxy_url: &str) -> Result<Self> {
|
|
||||||
let proxy = reqwest::Proxy::all(proxy_url)?;
|
|
||||||
let client = Client::builder().proxy(proxy).build()?;
|
|
||||||
Ok(Self { client })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, url: &str) -> Result<reqwest::blocking::Response> {
|
|
||||||
Ok(self.client
|
|
||||||
.get(url)
|
|
||||||
.header("User-Agent", "emod-cli")
|
|
||||||
.send()?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
pub mod git;
|
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod http;
|
|
||||||
Reference in New Issue
Block a user