Skip to main content

runx_cli/
scaffold.rs

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}