1use anyhow::{Context, Result};
12use robonix_cli::output;
13use std::path::{Path, PathBuf};
14
15fn validate_name(name: &str) -> Result<()> {
17 if name.is_empty() {
18 anyhow::bail!("name must not be empty");
19 }
20 if name.contains('/') || name.contains('\\') || name == ".." || name.starts_with("../") {
21 anyhow::bail!("invalid name '{name}': must not contain path separators or '..' components");
22 }
23 Ok(())
24}
25
26pub async fn execute(name: &str, pkg_type: &str, path: Option<&Path>) -> Result<()> {
27 validate_name(name)?;
28
29 let pkg_dir: PathBuf = if let Some(p) = path {
30 if p.is_absolute() {
32 p.to_path_buf()
33 } else {
34 std::env::current_dir()?.join(p)
35 }
36 } else {
37 let role_dir = match pkg_type {
39 "primitive" => "primitives",
40 "service" => "services",
41 "skill" => "skills",
42 other => {
43 anyhow::bail!("unknown package type '{other}'; expected: primitive, service, skill")
44 }
45 };
46 std::env::current_dir()?.join(role_dir).join(name)
47 };
48
49 if pkg_dir.exists() {
50 anyhow::bail!("directory '{}' already exists", pkg_dir.display());
51 }
52
53 output::action("PackageNew", &format!("creating package '{name}'"));
54
55 let (provider_class, ns_kind) = match pkg_type {
60 "service" => ("Service", "service"),
61 "skill" => ("Skill", "skill"),
62 _ => ("Primitive", "primitive"),
63 };
64
65 for sub in ["scripts", "capabilities"] {
67 let dir = pkg_dir.join(sub);
68 std::fs::create_dir_all(&dir)
69 .with_context(|| format!("failed to create {sub}/ directory"))?;
70 std::fs::write(dir.join(".gitkeep"), "")
72 .with_context(|| format!("failed to write {sub}/.gitkeep"))?;
73 }
74
75 let manifest = format!(
77 r#"manifestVersion: 1
78
79build: bash scripts/build.sh
80
81start: bash scripts/start.sh
82
83package:
84 name: com.vendor.{name}
85 version: 0.0.1
86 vendor: vendor
87 description: TODO
88 license: Apache-2.0
89
90capabilities: []
91
92depends: []
93"#
94 );
95 std::fs::write(pkg_dir.join("package_manifest.yaml"), manifest)
96 .context("failed to write package_manifest.yaml")?;
97
98 let build_sh = format!(
104 r#"#!/usr/bin/env bash
105set -euo pipefail
106PKG="${{RBNX_PACKAGE_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}}"
107
108rbnx codegen -p "$PKG"
109echo "[{name}] build done"
110"#
111 );
112 std::fs::write(pkg_dir.join("scripts/build.sh"), build_sh)
113 .context("failed to write scripts/build.sh")?;
114 #[cfg(unix)]
115 {
116 use std::os::unix::fs::PermissionsExt;
117 std::fs::set_permissions(
118 pkg_dir.join("scripts/build.sh"),
119 std::fs::Permissions::from_mode(0o755),
120 )?;
121 }
122
123 let start_sh = format!(
129 r#"#!/usr/bin/env bash
130set -eo pipefail
131PKG_ROOT="${{RBNX_PACKAGE_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}}"
132cd "$PKG_ROOT"
133
134export PYTHONPATH="$(rbnx path robonix-api):$PKG_ROOT:${{PYTHONPATH:-}}"
135
136exec python3 -m {name}.main
137"#
138 );
139 std::fs::write(pkg_dir.join("scripts/start.sh"), start_sh)
140 .context("failed to write scripts/start.sh")?;
141 #[cfg(unix)]
142 {
143 use std::os::unix::fs::PermissionsExt;
144 std::fs::set_permissions(
145 pkg_dir.join("scripts/start.sh"),
146 std::fs::Permissions::from_mode(0o755),
147 )?;
148 }
149
150 let _ = std::fs::remove_file(pkg_dir.join("scripts/.gitkeep"));
152
153 let module_dir = pkg_dir.join(name);
157 std::fs::create_dir_all(&module_dir).context("failed to create python module directory")?;
158 std::fs::write(module_dir.join("__init__.py"), "").context("failed to write __init__.py")?;
159 let main_py = format!(
160 r#"#!/usr/bin/env python3
161"""{name} — Robonix {provider_class_lower} provider."""
162from robonix_api import {provider_class}, Ok
163
164# `id` must equal this entry's `name:` in the deploy robonix_manifest.yaml.
165# `namespace` groups the capabilities this provider declares.
166provider = {provider_class}(id="{name}", namespace="robonix/{ns_kind}/{name}")
167
168
169@provider.on_init
170def init(cfg: dict):
171 # TODO: initialise hardware / resources; declare capabilities to atlas.
172 return Ok()
173
174
175if __name__ == "__main__":
176 provider.run()
177"#,
178 provider_class_lower = provider_class.to_lowercase(),
179 );
180 std::fs::write(module_dir.join("main.py"), main_py).context("failed to write main.py")?;
181
182 std::fs::write(
184 pkg_dir.join(".gitignore"),
185 "rbnx-build/\n__pycache__/\n*.pyc\n.venv/\n",
186 )
187 .context("failed to write .gitignore")?;
188
189 output::success(&format!(
190 "Package '{name}' created at {}",
191 pkg_dir.display()
192 ));
193 output::sub_step("package_manifest.yaml");
194 output::sub_step("scripts/build.sh");
195 output::sub_step("scripts/start.sh");
196 output::sub_step(&format!("{name}/main.py (provider skeleton)"));
197 output::sub_step("capabilities/ (.gitkeep)");
198 output::sub_step(".gitignore");
199
200 Ok(())
201}