1use std::env;
2use std::path::{Path, PathBuf};
3use std::process::ExitCode;
4
5use runx_runtime::scaffold::{
6 InitAction, InitGeneratedValues, RunxInitOptions, RunxInitResult, RunxNewOptions,
7 RunxNewResult, runx_init, sanitize_runx_package_name, scaffold_runx_package,
8};
9use serde::Serialize;
10
11use crate::router::{InitPlan, NewPlan};
12
13pub fn run_native_new(plan: NewPlan) -> ExitCode {
14 let cwd = match env::current_dir() {
15 Ok(cwd) => cwd,
16 Err(error) => {
17 let _ignored = write_stderr_line(&format!("runx: failed to resolve cwd: {error}"));
18 return ExitCode::from(1);
19 }
20 };
21 let env = crate::history::env_map();
22 let directory =
23 resolve_new_package_directory(&plan.name, plan.directory.as_deref(), &env, &cwd);
24 let options = RunxNewOptions {
25 name: plan.name,
26 directory,
27 };
28
29 match scaffold_runx_package(&options) {
30 Ok(result) => render_new_result(plan.json, &result),
31 Err(error) => {
32 let _ignored = write_stderr_line(&format!("runx: {error}"));
33 ExitCode::from(1)
34 }
35 }
36}
37
38pub fn run_native_init(plan: InitPlan) -> ExitCode {
39 let cwd = match env::current_dir() {
40 Ok(cwd) => cwd,
41 Err(error) => {
42 let _ignored = write_stderr_line(&format!("runx: failed to resolve cwd: {error}"));
43 return ExitCode::from(1);
44 }
45 };
46 let env = crate::history::env_map();
47 let global_home_dir = resolve_global_home_dir(&env, &cwd);
48 let official_cache_dir = resolve_official_skills_dir(&env, &cwd, &global_home_dir);
49 let options = RunxInitOptions {
50 action: if plan.global {
51 InitAction::Global
52 } else {
53 InitAction::Project
54 },
55 project_dir: resolve_project_dir(&env, &cwd),
56 global_home_dir,
57 official_cache_dir,
58 prefetch_official: plan.prefetch_official,
59 generated: InitGeneratedValues::generate(),
60 };
61
62 match runx_init(&options) {
63 Ok(result) => render_init_result(plan.json, &result),
64 Err(error) => {
65 let _ignored = write_stderr_line(&format!("runx: {error}"));
66 ExitCode::from(1)
67 }
68 }
69}
70
71fn write_json<T: serde::Serialize>(command: &str, result: &T) -> ExitCode {
72 match serde_json::to_string_pretty(result) {
73 Ok(output) => write_stdout_line(&output),
74 Err(error) => {
75 let _ignored = write_stderr_line(&format!(
76 "runx: failed to serialize {command} result: {error}"
77 ));
78 ExitCode::from(1)
79 }
80 }
81}
82
83fn render_new_result(json: bool, result: &RunxNewResult) -> ExitCode {
84 if json {
85 return write_json(
86 "new",
87 &NewJsonResult {
88 status: "success",
89 new: NewCommandResult {
90 action: "skill",
91 name: &result.name,
92 directory: &result.directory,
93 files: &result.files,
94 next_steps: &result.next_steps,
95 },
96 },
97 );
98 }
99 write_stdout(&render_key_values(
100 "runx new",
101 &[
102 ("skill", Some(result.name.clone())),
103 ("directory", Some(result.directory.display().to_string())),
104 ("files", Some(result.files.len().to_string())),
105 ("next", Some(result.next_steps.join(" && "))),
106 ],
107 ))
108}
109
110fn render_init_result(json: bool, result: &RunxInitResult) -> ExitCode {
111 if json {
112 return write_json(
113 "init",
114 &InitJsonResult {
115 status: "success",
116 init: result,
117 },
118 );
119 }
120 let title = match &result.action {
121 InitAction::Global => "runx global init",
122 InitAction::Project => "runx project init",
123 };
124 write_stdout(&render_key_values(
125 title,
126 &[
127 (
128 "created",
129 Some(if result.created { "yes" } else { "no" }.to_owned()),
130 ),
131 (
132 "project",
133 result
134 .project_dir
135 .as_ref()
136 .map(|path| path.display().to_string()),
137 ),
138 ("project_id", result.project_id.clone()),
139 (
140 "home",
141 result
142 .global_home_dir
143 .as_ref()
144 .map(|path| path.display().to_string()),
145 ),
146 ("installation_id", result.installation_id.clone()),
147 (
148 "official_cache",
149 result
150 .official_cache_dir
151 .as_ref()
152 .map(|path| path.display().to_string()),
153 ),
154 ],
155 ))
156}
157
158fn render_key_values(title: &str, rows: &[(&str, Option<String>)]) -> String {
159 let mut output = format!("\n {title} success\n\n");
160 for (key, value) in rows {
161 output.push_str(&format!(" {key} {}\n", value.as_deref().unwrap_or("-")));
162 }
163 output.push('\n');
164 output
165}
166
167fn resolve_new_package_directory(
168 name: &str,
169 directory: Option<&Path>,
170 env: &std::collections::BTreeMap<String, String>,
171 cwd: &Path,
172) -> PathBuf {
173 let root = new_package_base(env, cwd);
174 match directory {
175 Some(directory) if directory.is_absolute() => directory.to_path_buf(),
176 Some(directory) => root.join(directory),
177 None => root.join(sanitize_runx_package_name(name)),
178 }
179}
180
181fn new_package_base(env: &std::collections::BTreeMap<String, String>, cwd: &Path) -> PathBuf {
182 runx_runtime::resolve_runx_workspace_base(env, cwd)
183}
184
185fn resolve_project_dir(env: &std::collections::BTreeMap<String, String>, cwd: &Path) -> PathBuf {
186 if let Some(project_dir) = env.get("RUNX_PROJECT_DIR") {
187 return resolve_user_path(project_dir, env, cwd);
188 }
189 find_nearest_project_runx_dir(cwd)
190 .unwrap_or_else(|| runx_runtime::resolve_runx_workspace_base(env, cwd).join(".runx"))
191}
192
193fn resolve_global_home_dir(
194 env: &std::collections::BTreeMap<String, String>,
195 cwd: &Path,
196) -> PathBuf {
197 env.get("RUNX_HOME")
198 .map(|value| resolve_user_path(value, env, cwd))
199 .unwrap_or_else(default_home_runx_dir)
200}
201
202fn resolve_official_skills_dir(
203 env: &std::collections::BTreeMap<String, String>,
204 cwd: &Path,
205 global_home_dir: &Path,
206) -> PathBuf {
207 env.get("RUNX_OFFICIAL_SKILLS_DIR")
208 .map(|value| resolve_user_path(value, env, cwd))
209 .unwrap_or_else(|| global_home_dir.join("official-skills"))
210}
211
212fn resolve_user_path(
213 value: &str,
214 env: &std::collections::BTreeMap<String, String>,
215 cwd: &Path,
216) -> PathBuf {
217 let path = PathBuf::from(value);
218 if path.is_absolute() {
219 path
220 } else {
221 runx_runtime::resolve_runx_workspace_base(env, cwd).join(path)
222 }
223}
224
225fn find_nearest_project_runx_dir(start: &Path) -> Option<PathBuf> {
226 for current in start.ancestors() {
227 let candidate = current.join(".runx");
228 if candidate.join("project.json").exists() {
229 return Some(candidate);
230 }
231 }
232 None
233}
234
235fn default_home_runx_dir() -> PathBuf {
236 env::var_os("HOME")
237 .or_else(|| env::var_os("USERPROFILE"))
238 .map(PathBuf::from)
239 .unwrap_or_else(|| PathBuf::from("."))
240 .join(".runx")
241}
242
243#[derive(Serialize)]
244struct NewJsonResult<'a> {
245 status: &'static str,
246 new: NewCommandResult<'a>,
247}
248
249#[derive(Serialize)]
250struct NewCommandResult<'a> {
251 action: &'static str,
252 name: &'a str,
253 directory: &'a Path,
254 files: &'a [String],
255 next_steps: &'a [String],
256}
257
258#[derive(Serialize)]
259struct InitJsonResult<'a> {
260 status: &'static str,
261 init: &'a RunxInitResult,
262}
263
264fn write_stdout(message: &str) -> ExitCode {
265 crate::cli_io::write_stdout_code(message, 0)
266}
267
268fn write_stdout_line(message: &str) -> ExitCode {
269 crate::cli_io::write_stdout_code(&format!("{message}\n"), 0)
270}
271
272fn write_stderr_line(message: &str) -> ExitCode {
273 crate::cli_io::write_stderr_code(&format!("{message}\n"))
274}