1use anyhow::{Context, Result};
5use robonix_cli::output;
6use std::path::Path;
7
8fn validate_name(name: &str) -> Result<()> {
10 if name.is_empty() {
11 anyhow::bail!("name must not be empty");
12 }
13 if name.contains('/') || name.contains('\\') || name == ".." || name.starts_with("../") {
14 anyhow::bail!("invalid name '{name}': must not contain path separators or '..' components");
15 }
16 Ok(())
17}
18
19fn robot_catalog_name(name: &str) -> String {
20 let suffix = name.strip_prefix("robot-").unwrap_or(name);
21 format!("robonix.robot.{}", suffix.replace('-', "."))
22}
23
24fn deployment_manifest(name: &str) -> String {
25 let catalog_name = robot_catalog_name(name);
26 format!(
27 r#"manifestVersion: 1
28catalog:
29 name: {catalog_name}
30 version: 0.1.0
31 description: "TODO: describe this robot deployment."
32 license: Apache-2.0
33 tags:
34 - robot
35 - deploy
36 - robonix
37 maintainers:
38 - Your Name <you@example.com>
39
40name: {name}
41
42env:
43 LOG: "INFO"
44
45system:
46 atlas:
47 listen: 127.0.0.1:50051
48 log: info
49 executor:
50 listen: 127.0.0.1:50061
51 log: info
52 pilot:
53 listen: 127.0.0.1:50071
54 log: info
55 vlm:
56 upstream: ${{VLM_BASE_URL}}
57 api_key: ${{VLM_API_KEY}}
58 model: ${{VLM_MODEL}}
59 api_format: openai
60 liaison:
61 listen: 0.0.0.0:50081
62 log: info
63
64primitive: []
65
66service: []
67
68skill: []
69"#
70 )
71}
72
73pub async fn execute(name: &str, path: Option<&Path>) -> Result<()> {
74 validate_name(name)?;
75
76 let base = match path {
77 Some(p) => p.to_path_buf(),
78 None => std::env::current_dir()?,
79 };
80 let project_dir = base.join(name);
81
82 if project_dir.exists() {
83 anyhow::bail!("directory '{}' already exists", project_dir.display());
84 }
85
86 output::action("Init", &format!("creating robot deployment '{name}'"));
87
88 std::fs::create_dir_all(&project_dir).context("failed to create deployment directory")?;
89
90 let manifest = deployment_manifest(name);
92 std::fs::write(project_dir.join("robonix_manifest.yaml"), manifest)
93 .context("failed to write robonix_manifest.yaml")?;
94
95 let gitignore = "\
97rbnx-build/
98rbnx-boot/
99__pycache__/
100*.pyc
101.venv/
102";
103 std::fs::write(project_dir.join(".gitignore"), gitignore)
104 .context("failed to write .gitignore")?;
105
106 output::success(&format!(
107 "Robot deployment '{name}' created at {}",
108 project_dir.display()
109 ));
110 output::sub_step("robonix_manifest.yaml");
111 output::sub_step(".gitignore");
112
113 Ok(())
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn generated_manifest_has_complete_robot_catalog_metadata() {
122 let root: serde_yaml::Value =
123 serde_yaml::from_str(&deployment_manifest("robot-example-mobile")).unwrap();
124 let catalog = root
125 .get("catalog")
126 .and_then(|value| value.as_mapping())
127 .unwrap();
128
129 assert_eq!(
130 root.get("manifestVersion").and_then(|value| value.as_u64()),
131 Some(1)
132 );
133 for key in [
134 "name",
135 "version",
136 "description",
137 "license",
138 "tags",
139 "maintainers",
140 ] {
141 assert!(
142 catalog.contains_key(serde_yaml::Value::String(key.into())),
143 "missing catalog.{key}"
144 );
145 }
146 }
147}