1use std::ffi::{OsStr, OsString};
2
3pub const DEFAULT_NPM_PACKAGE: &str = "@runxhq/cli@latest";
4
5#[derive(Debug, Eq, PartialEq)]
6pub enum LauncherAction {
7 Delegate(CommandPlan),
8 PrintHelp,
9 PrintVersion,
10}
11
12#[derive(Debug, Eq, PartialEq)]
13pub struct CommandPlan {
14 pub program: OsString,
15 pub args: Vec<OsString>,
16}
17
18pub fn plan_launcher(
19 args: Vec<OsString>,
20 npm_package: Option<OsString>,
21 js_bin: Option<OsString>,
22) -> LauncherAction {
23 if has_arg(&args, "--shim-version") {
24 return LauncherAction::PrintVersion;
25 }
26
27 if has_arg(&args, "--shim-help") {
28 return LauncherAction::PrintHelp;
29 }
30
31 if let Some(js_bin) = non_empty_os(js_bin) {
32 let mut planned_args = Vec::with_capacity(args.len() + 1);
33 planned_args.push(js_bin);
34 planned_args.extend(args);
35 return LauncherAction::Delegate(CommandPlan {
36 program: node_command().into(),
37 args: planned_args,
38 });
39 }
40
41 let package = non_empty_os(npm_package).unwrap_or_else(|| DEFAULT_NPM_PACKAGE.into());
42 let mut planned_args = vec![
43 "exec".into(),
44 "--yes".into(),
45 "--package".into(),
46 package,
47 "--".into(),
48 "runx".into(),
49 ];
50 planned_args.extend(args);
51
52 LauncherAction::Delegate(CommandPlan {
53 program: npm_command().into(),
54 args: planned_args,
55 })
56}
57
58pub fn shim_help() -> String {
59 format!(
60 "\
61runx Cargo launcher
62
63Usage:
64 runx [runx CLI args]
65 runx --shim-version
66 runx --shim-help
67
68Environment:
69 RUNX_NPM_PACKAGE npm package spec to execute, defaults to {DEFAULT_NPM_PACKAGE}
70 RUNX_JS_BIN local JavaScript runx entrypoint to execute with node
71"
72 )
73}
74
75pub fn npm_command() -> &'static str {
76 if cfg!(windows) { "npm.cmd" } else { "npm" }
77}
78
79pub fn node_command() -> &'static str {
80 if cfg!(windows) { "node.exe" } else { "node" }
81}
82
83fn has_arg(args: &[OsString], expected: &str) -> bool {
84 args.iter().any(|arg| arg == OsStr::new(expected))
85}
86
87fn non_empty_os(value: Option<OsString>) -> Option<OsString> {
88 value.filter(|value| !value.is_empty())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn defaults_to_latest_npm_cli_package() {
97 let action = plan_launcher(vec!["--help".into()], None, None);
98
99 assert_eq!(
100 action,
101 LauncherAction::Delegate(CommandPlan {
102 program: npm_command().into(),
103 args: vec![
104 "exec".into(),
105 "--yes".into(),
106 "--package".into(),
107 DEFAULT_NPM_PACKAGE.into(),
108 "--".into(),
109 "runx".into(),
110 "--help".into(),
111 ],
112 })
113 );
114 }
115
116 #[test]
117 fn accepts_pinned_npm_package() {
118 let action = plan_launcher(
119 vec!["skill".into(), "sourcey".into()],
120 Some("@runxhq/cli@0.5.22".into()),
121 None,
122 );
123
124 assert_eq!(
125 action,
126 LauncherAction::Delegate(CommandPlan {
127 program: npm_command().into(),
128 args: vec![
129 "exec".into(),
130 "--yes".into(),
131 "--package".into(),
132 "@runxhq/cli@0.5.22".into(),
133 "--".into(),
134 "runx".into(),
135 "skill".into(),
136 "sourcey".into(),
137 ],
138 })
139 );
140 }
141
142 #[test]
143 fn local_js_bin_overrides_npm_package() {
144 let action = plan_launcher(
145 vec!["--help".into()],
146 Some("@runxhq/cli@0.5.22".into()),
147 Some("/repo/oss/packages/cli/bin/runx.js".into()),
148 );
149
150 assert_eq!(
151 action,
152 LauncherAction::Delegate(CommandPlan {
153 program: node_command().into(),
154 args: vec!["/repo/oss/packages/cli/bin/runx.js".into(), "--help".into()],
155 })
156 );
157 }
158
159 #[test]
160 fn shim_flags_do_not_delegate() {
161 assert_eq!(
162 plan_launcher(vec!["--shim-version".into()], None, None),
163 LauncherAction::PrintVersion
164 );
165 assert_eq!(
166 plan_launcher(vec!["--shim-help".into()], None, None),
167 LauncherAction::PrintHelp
168 );
169 }
170}