pub enum Launcher {
Help,
Boot {
profile: String,
from_default: Option<String>,
patches: Vec<String>,
app_args: Vec<String>,
},
Dump {
profile: String,
from_default: Option<String>,
kind: DumpKind,
patches: Vec<String>,
},
Plugin {
profile: String,
pnpm_args: Vec<String>,
},
Error(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DumpKind {
Config,
DefaultConfig,
Schema,
}
pub fn split_launcher_args(profile_flag: Option<String>, extra: Vec<String>) -> Launcher {
if extra.first().is_some_and(|s| s.as_str() == "plugin") {
let mut profile = profile_flag;
let mut pnpm: Vec<String> = Vec::new();
let mut it = extra.into_iter().skip(1);
while let Some(a) = it.next() {
if a == "--profile" {
profile = it.next();
} else if let Some(v) = a.strip_prefix("--profile=") {
profile = Some(v.to_string());
} else {
pnpm.push(a);
pnpm.extend(it);
break;
}
}
return match profile {
Some(p) if !p.is_empty() && !pnpm.is_empty() => Launcher::Plugin {
profile: p,
pnpm_args: pnpm,
},
_ if pnpm.is_empty() => Launcher::Error(
"error: plugin needs pnpm arguments to forward (e.g. add <package>)".into(),
),
_ => Launcher::Error("error: --profile <name> is required".into()),
};
}
let mut profile = profile_flag;
let mut from_default: Option<String> = None;
let mut patches: Vec<String> = Vec::new();
let mut dump = false;
let mut dump_default = false;
let mut dump_schema = false;
let mut app_args: Vec<String> = Vec::new();
let mut positional: Vec<String> = Vec::new();
let mut it = extra.into_iter();
while let Some(a) = it.next() {
match a.as_str() {
"-h" | "--help" if profile.is_none() && positional.is_empty() => return Launcher::Help,
"--profile" => {
if profile.is_some() {
return Launcher::Error("error: select a profile only once".into());
}
profile = it.next();
if profile.is_none() {
return Launcher::Error("error: --profile needs a name".into());
}
}
"--from-default-profile" => match it.next() {
Some(p) if !p.is_empty() => from_default = Some(p),
_ => {
return Launcher::Error("error: --from-default-profile needs a name".into());
}
},
"--patch" => match it.next() {
Some(p) if !p.is_empty() => patches.push(p),
_ => return Launcher::Error("error: --patch needs a path".into()),
},
"--dump-config" => dump = true,
"--dump-default-config" => dump_default = true,
"--dump-config-schema" => dump_schema = true,
_ if a.starts_with("--profile=") => {
if profile.is_some() {
return Launcher::Error("error: select a profile only once".into());
}
profile = a.strip_prefix("--profile=").map(|v| v.to_string());
}
_ if a.starts_with('-') => {
app_args.push(a);
app_args.extend(it);
break;
}
_ => positional.push(a),
}
}
if profile.is_none() {
let mut pos = positional.into_iter();
if let Some(p) = pos.next() {
profile = Some(p);
let rest: Vec<String> = pos.collect();
if !rest.is_empty() {
app_args = [rest, app_args].concat();
}
}
} else if !positional.is_empty() {
app_args = [positional, app_args].concat();
}
let profile = match profile {
Some(p) if !p.is_empty() => p,
_ => {
if dump || dump_default || dump_schema {
return Launcher::Error("error: --profile <name> is required".into());
}
return Launcher::Help;
}
};
if profile.to_lowercase() == "desktop" {
return Launcher::Error(
"error: profile desktop is managed exclusively by the Electron application".into(),
);
}
let dumps = [dump, dump_default, dump_schema]
.iter()
.copied()
.filter(|x| *x)
.count();
if dumps > 1 {
return Launcher::Error("error: --dump-config, --dump-default-config, and --dump-config-schema are mutually exclusive".into());
}
if dumps == 1 {
if !app_args.is_empty() {
return Launcher::Error("error: config dumps take no app arguments".into());
}
if dump_default && !patches.is_empty() {
return Launcher::Error(
"error: --dump-default-config prints the bundle layers and takes no --patch".into(),
);
}
let kind = if dump {
DumpKind::Config
} else if dump_default {
DumpKind::DefaultConfig
} else {
DumpKind::Schema
};
return Launcher::Dump {
profile,
from_default,
kind,
patches,
};
}
if app_args.first().is_some_and(|s| s.as_str() == "--") {
app_args.remove(0);
}
Launcher::Boot {
profile,
from_default,
patches,
app_args,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sv(v: &[&str]) -> Vec<String> {
v.iter().map(ToString::to_string).collect()
}
#[test]
fn positional_is_profile() {
match split_launcher_args(None, sv(&["tui"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "tui");
assert!(app_args.is_empty());
}
_ => panic!("expected boot"),
}
}
#[test]
fn positional_profile_preserves_prompt() {
match split_launcher_args(None, sv(&["headless", "run the tests"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "headless");
assert_eq!(app_args, sv(&["run the tests"]));
}
_ => panic!("expected boot"),
}
}
#[test]
fn positional_profile_preserves_empty_arguments_and_following_app_flags() {
match split_launcher_args(None, sv(&["headless", "", "run", "--resume", "abc"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "headless");
assert_eq!(app_args, sv(&["", "run", "--resume", "abc"]));
}
_ => panic!("expected boot"),
}
}
#[test]
fn positional_profile_preserves_double_dash_boundary() {
match split_launcher_args(None, sv(&["headless", "--", "run the tests"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "headless");
assert_eq!(app_args, sv(&["run the tests"]));
}
_ => panic!("expected boot"),
}
}
#[test]
fn unknown_flag_starts_app_args() {
match split_launcher_args(Some("tui".into()), sv(&["--resume", "abc"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "tui");
assert_eq!(app_args, sv(&["--resume", "abc"]));
}
_ => panic!("expected boot"),
}
}
#[test]
fn rejects_desktop() {
match split_launcher_args(None, sv(&["desktop"])) {
Launcher::Error(_) => {}
_ => panic!("expected error"),
}
}
#[test]
fn rejects_double_profile() {
match split_launcher_args(Some("a".into()), sv(&["--profile", "b"])) {
Launcher::Error(_) => {}
_ => panic!("expected error"),
}
}
#[test]
fn rejects_dump_with_args() {
match split_launcher_args(Some("tui".into()), sv(&["--dump-config", "--foo"])) {
Launcher::Error(_) => {}
_ => panic!("expected error"),
}
}
#[test]
fn abbreviated_profile_keeps_positional_args() {
match split_launcher_args(None, sv(&["headless", "run the tests"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "headless");
assert_eq!(app_args, sv(&["run the tests"]));
}
_ => panic!("expected boot"),
}
match split_launcher_args(Some("headless".into()), sv(&["run the tests"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "headless");
assert_eq!(app_args, sv(&["run the tests"]));
}
_ => panic!("expected boot"),
}
}
#[test]
fn dump_flags_keep_their_kind() {
for (flag, expected) in [
("--dump-config", DumpKind::Config),
("--dump-default-config", DumpKind::DefaultConfig),
("--dump-config-schema", DumpKind::Schema),
] {
match split_launcher_args(Some("web".into()), sv(&[flag])) {
Launcher::Dump { kind, .. } => assert_eq!(kind, expected),
_ => panic!("expected dump for {flag}"),
}
}
}
#[test]
fn abbreviated_profile_keeps_many_positional_args_in_order() {
match split_launcher_args(None, sv(&["web", "hello", "world"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "web");
assert_eq!(app_args, sv(&["hello", "world"]));
}
_ => panic!("expected boot"),
}
match split_launcher_args(None, sv(&["web", "", "--foo", "bar"])) {
Launcher::Boot {
profile, app_args, ..
} => {
assert_eq!(profile, "web");
assert_eq!(app_args, sv(&["", "--foo", "bar"]));
}
_ => panic!("expected boot"),
}
}
#[test]
fn dump_keeps_from_default_profile() {
match split_launcher_args(
None,
sv(&["rescue", "--from-default-profile", "web", "--dump-config"]),
) {
Launcher::Dump {
profile,
from_default,
kind,
..
} => {
assert_eq!(profile, "rescue");
assert_eq!(from_default.as_deref(), Some("web"));
assert_eq!(kind, DumpKind::Config);
}
_ => panic!("expected dump"),
}
}
#[test]
fn from_default_profile_without_value_is_rejected() {
match split_launcher_args(None, sv(&["rescue", "--from-default-profile"])) {
Launcher::Error(msg) => assert!(msg.contains("--from-default-profile"), "{msg}"),
_ => panic!("expected an error for a value-less --from-default-profile"),
}
}
#[test]
fn plugin_needs_pnpm_args() {
match split_launcher_args(Some("tui".into()), sv(&["plugin"])) {
Launcher::Error(_) => {}
_ => panic!("expected error"),
}
}
}