From 3bd1af3bc4ffc9368b48015c2545d6504ac939ba Mon Sep 17 00:00:00 2001 From: itowlson Date: Mon, 17 Aug 2026 09:32:29 +1200 Subject: [PATCH] Enforce kebab-casing in `spin new` and `spin add` Signed-off-by: itowlson --- src/commands/new.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/commands/new.rs b/src/commands/new.rs index 0659a3a899..a302d49c39 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -182,7 +182,16 @@ impl TemplateNewCommandCore { } let name = match &name { - Some(name) => name.to_owned(), + Some(name) => { + if is_kebabbable(name) { + name.to_owned() + } else { + eprintln!( + "Name must be kebab-cased, and each segment must begin with a letter." + ); + prompt_name(&variant).await? + } + } None => prompt_name(&variant).await?, }; @@ -398,6 +407,10 @@ fn infer_target_env_from_file( } } +fn is_kebabbable(name: &str) -> bool { + spin_manifest::schema::v2::KebabId::try_from(name.to_string()).is_ok() +} + #[derive(Clone, Debug)] pub struct ParameterValue { pub name: String, @@ -520,6 +533,11 @@ async fn prompt_name(variant: &TemplateVariantInfo) -> anyhow::Result { if result.trim().is_empty() { prompt = format!("Name is required. Try another {noun} name (or Crl+C to exit)"); continue; + } else if !is_kebabbable(&result) { + prompt = format!( + "Name must be kebab-cased, and each segment must start with a letter. Try another {noun} name (or Crl+C to exit)" + ); + continue; } else { return Ok(result); }