From b58218839e23e675a5f9c0e8202869e8e61bb886 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 15:24:34 -0400 Subject: [PATCH 1/4] refactor: default missing ConfigEntry fields and dedupe default construction --- src/config/crud.rs | 24 ++-------------- src/config/types.rs | 25 +++++++++++++++- tests/cli/resolve.rs | 38 ++++++------------------ tests/config.rs | 2 -- tests/config/crud.rs | 62 +++++++++++++++++----------------------- tests/config/display.rs | 37 +++++++++++++++++------- tests/config/fixtures.rs | 21 -------------- tests/config/io.rs | 21 ++++---------- 8 files changed, 93 insertions(+), 137 deletions(-) delete mode 100644 tests/config/fixtures.rs diff --git a/src/config/crud.rs b/src/config/crud.rs index 90daaa5..1570091 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -1,5 +1,4 @@ use crate::{ - cli::BuildType::Debug, config::{format_entry, persist_or_dry_run, save_config, ConfigEntry, SetupConfig}, ctx::{IoCtx, RunFlags}, prompts::confirm_abort, @@ -54,26 +53,9 @@ pub fn create_default_config( } else { let mut config = SetupConfig::new(); config.path = Some(path.clone()); - config.configs.insert( - "default".to_string(), - ConfigEntry { - ssh: false, - build_type: Debug, - build_dir: "build".to_string(), - mono_dir: "build-mono".to_string(), - no_build: false, - clean: false, - verbose: false, - timing: false, - dry_run: false, - watch: false, - no_watch: false, - dev: false, - no_dev: false, - cmake_flags: vec![], - meson_flags: vec![], - }, - ); + config + .configs + .insert("default".to_string(), ConfigEntry::default()); let path = save_config(&mut config, flags.timing, &mut io.output)?; diff --git a/src/config/types.rs b/src/config/types.rs index 80a02c0..e3a6057 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -6,7 +6,8 @@ use std::{collections::HashMap, path::PathBuf}; /// Represents a single named configuration entry. #[allow(clippy::struct_excessive_bools)] -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize)] +#[serde(default)] pub struct ConfigEntry { /// Use SSH instead of HTTPS for cloning. pub ssh: bool, @@ -80,6 +81,28 @@ impl ConfigEntry { } } +impl Default for ConfigEntry { + fn default() -> Self { + Self { + ssh: false, + build_type: BuildType::Debug, + build_dir: "build".to_string(), + mono_dir: "build-mono".to_string(), + no_build: false, + clean: false, + verbose: false, + timing: false, + dry_run: false, + watch: false, + no_watch: false, + dev: false, + no_dev: false, + cmake_flags: vec![], + meson_flags: vec![], + } + } +} + impl From<&ResolvedArgs> for ConfigEntry { fn from(args: &ResolvedArgs) -> Self { Self { diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs index 3631c4a..0db2577 100644 --- a/tests/cli/resolve.rs +++ b/tests/cli/resolve.rs @@ -4,28 +4,6 @@ use star_setup::{ config::{ConfigEntry, SetupConfig}, }; -/* ===== HELPERS ===== */ -/// Generates a base `ConfigEntry` with defaults. -fn create_test_config_entry() -> ConfigEntry { - ConfigEntry { - ssh: false, - verbose: false, - build_type: BuildType::Debug, - build_dir: "build".to_string(), - mono_dir: "build-mono".to_string(), - no_build: false, - clean: false, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], - watch: false, - no_watch: false, - dev: false, - no_dev: false, - } -} - /// Helper to quickly build a `SetupConfig` with a populated profile entry. fn config_with_entry(name: &str, entry: ConfigEntry) -> SetupConfig { let mut config = SetupConfig::new(); @@ -124,7 +102,7 @@ fn test_resolve_with_config_applies_config_defaults() { no_build: true, clean: true, cmake_flags: vec!["-DTEST=ON".to_string()], - ..create_test_config_entry() + ..ConfigEntry::default() }, ); @@ -139,7 +117,7 @@ fn test_resolve_with_config_applies_config_defaults() { #[test] fn test_resolve_with_config_cli_overrides_config() { - let config = config_with_entry("default", create_test_config_entry()); + let config = config_with_entry("default", ConfigEntry::default()); let mut args = default_args(); args.connection.ssh = true; @@ -188,7 +166,7 @@ fn test_resolve_with_config_named_config_pulls_correct_values() { build_type: BuildType::RelWithDebInfo, build_dir: "out".to_string(), clean: true, - ..create_test_config_entry() + ..ConfigEntry::default() }, ); @@ -208,7 +186,7 @@ fn test_resolve_with_config_cli_cmake_flags_not_overwritten_by_config() { "default", ConfigEntry { cmake_flags: vec!["-DCONFIG_FLAG=ON".to_string()], - ..create_test_config_entry() + ..ConfigEntry::default() }, ); @@ -228,7 +206,7 @@ fn test_resolve_with_config_negative_flags_override_config() { verbose: true, no_build: true, clean: true, - ..create_test_config_entry() + ..ConfigEntry::default() }, ); @@ -252,7 +230,7 @@ fn test_resolve_with_config_watch_dev_defaults_from_config() { ConfigEntry { watch: true, dev: true, - ..create_test_config_entry() + ..ConfigEntry::default() }, ); @@ -270,7 +248,7 @@ fn test_resolve_with_config_cli_negatives_override_watch_dev_config() { ConfigEntry { watch: true, dev: true, - ..create_test_config_entry() + ..ConfigEntry::default() }, ); @@ -292,7 +270,7 @@ fn test_resolve_with_config_cli_positives_override_no_watch_no_dev_config() { ConfigEntry { no_watch: true, no_dev: true, - ..create_test_config_entry() + ..ConfigEntry::default() }, ); diff --git a/tests/config.rs b/tests/config.rs index 5d1498e..921a074 100644 --- a/tests/config.rs +++ b/tests/config.rs @@ -5,8 +5,6 @@ mod common; mod crud; #[path = "config/display.rs"] mod display; -#[path = "config/fixtures.rs"] -mod fixtures; #[path = "config/io.rs"] mod io; #[path = "config/types.rs"] diff --git a/tests/config/crud.rs b/tests/config/crud.rs index 4877d98..48d8fe0 100644 --- a/tests/config/crud.rs +++ b/tests/config/crud.rs @@ -1,13 +1,7 @@ -use crate::{ - common::{make_flags, with_io_dir, with_io_input_output, with_io_output}, - fixtures::sample_entry, -}; -use star_setup::{ - cli::BuildType, - config::{ - add_config, create_default_config, has_config, insert_config, list_configs, remove_config, - remove_config_entry, save_config, ConfigEntry, SetupConfig, - }, +use crate::common::{make_flags, with_io_dir, with_io_input_output, with_io_output}; +use star_setup::config::{ + add_config, create_default_config, has_config, insert_config, list_configs, remove_config, + remove_config_entry, save_config, ConfigEntry, SetupConfig, }; use std::fs::{read_to_string, write}; use tempfile::TempDir; @@ -29,7 +23,7 @@ fn test_add_config_inserts_and_saves() { add_config( &mut config, "myconfig", - sample_entry(), + ConfigEntry::default(), true, io, make_flags(), @@ -47,28 +41,19 @@ fn test_add_config_aborts_when_exists_and_not_confirmed() { let tmp = TempDir::new().unwrap(); let mut config = SetupConfig::new(); config.path = Some(tmp.path().join(".star-setup.json")); - insert_config(&mut config, "myconfig", sample_entry()); - - add_config( + insert_config( &mut config, "myconfig", ConfigEntry { - ssh: false, - build_type: BuildType::Debug, - build_dir: "build".to_string(), - mono_dir: "mono".to_string(), - no_build: false, - clean: false, - verbose: false, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], - watch: false, - no_watch: false, - dev: false, - no_dev: false, + ssh: true, + ..ConfigEntry::default() }, + ); + + add_config( + &mut config, + "myconfig", + ConfigEntry::default(), false, io, make_flags(), @@ -82,14 +67,21 @@ fn test_add_config_aborts_when_exists_and_not_confirmed() { #[test] fn test_has_config_true() { let mut config = SetupConfig::new(); - insert_config(&mut config, "myconfig", sample_entry()); + insert_config(&mut config, "myconfig", ConfigEntry::default()); assert!(has_config(&config, "myconfig")); } #[test] fn test_insert_config() { let mut config = SetupConfig::new(); - insert_config(&mut config, "myconfig", sample_entry()); + insert_config( + &mut config, + "myconfig", + ConfigEntry { + ssh: true, + ..ConfigEntry::default() + }, + ); assert!(config.configs.contains_key("myconfig")); assert!(config.configs["myconfig"].ssh); } @@ -97,7 +89,7 @@ fn test_insert_config() { #[test] fn test_remove_config_entry_exists() { let mut config = SetupConfig::new(); - insert_config(&mut config, "myconfig", sample_entry()); + insert_config(&mut config, "myconfig", ConfigEntry::default()); assert!(remove_config_entry(&mut config, "myconfig")); assert!(!config.configs.contains_key("myconfig")); } @@ -138,7 +130,7 @@ fn test_list_configs_empty() { fn test_list_configs_with_entries() { let ((), out) = with_io_output(|io| { let mut config = SetupConfig::new(); - insert_config(&mut config, "myconfig", sample_entry()); + insert_config(&mut config, "myconfig", ConfigEntry::default()); list_configs(&config, io); }); assert!(out.contains("myconfig")); @@ -159,7 +151,7 @@ fn test_remove_config_removes_and_saves() { let path = tmp.join(".star-setup.json"); let mut config = SetupConfig::new(); config.path = Some(path.clone()); - insert_config(&mut config, "myconfig", sample_entry()); + insert_config(&mut config, "myconfig", ConfigEntry::default()); save_config(&mut config, false, &mut io.output).unwrap(); remove_config(&mut config, "myconfig", true, io, make_flags()).unwrap(); @@ -181,7 +173,7 @@ fn test_remove_config_aborts_when_not_confirmed() { let tmp = TempDir::new().unwrap(); let mut config = SetupConfig::new(); config.path = Some(tmp.path().join(".star-setup.json")); - insert_config(&mut config, "myconfig", sample_entry()); + insert_config(&mut config, "myconfig", ConfigEntry::default()); remove_config(&mut config, "myconfig", false, io, make_flags()).unwrap(); assert!(has_config(&config, "myconfig")); diff --git a/tests/config/display.rs b/tests/config/display.rs index 13bd6fc..234f999 100644 --- a/tests/config/display.rs +++ b/tests/config/display.rs @@ -1,9 +1,16 @@ -use crate::fixtures::sample_entry; -use star_setup::config::format_entry; +use star_setup::{ + cli::BuildType, + config::{format_entry, ConfigEntry}, +}; #[test] fn test_format_entry_contains_fields() { - let entry = sample_entry(); + let entry = ConfigEntry { + ssh: true, + build_type: BuildType::Release, + clean: true, + ..ConfigEntry::default() + }; let output = format_entry(&entry); assert!(output.contains("SSH: true")); assert!(output.contains("Build Type: Release")); @@ -13,16 +20,20 @@ fn test_format_entry_contains_fields() { #[test] fn test_format_entry_single_cmake_flag() { - let mut entry = sample_entry(); - entry.cmake_flags = vec!["-DTEST=ON".to_string()]; + let entry = ConfigEntry { + cmake_flags: vec!["-DTEST=ON".to_string()], + ..ConfigEntry::default() + }; let output = format_entry(&entry); assert!(output.contains("CMake argument: -DTEST=ON")); } #[test] fn test_format_entry_multiple_cmake_flags() { - let mut entry = sample_entry(); - entry.cmake_flags = vec!["-DTEST=ON".to_string(), "-DDEBUG=OFF".to_string()]; + let entry = ConfigEntry { + cmake_flags: vec!["-DTEST=ON".to_string(), "-DDEBUG=OFF".to_string()], + ..ConfigEntry::default() + }; let output = format_entry(&entry); assert!(output.contains("CMake arguments:")); assert!(output.contains("-DTEST=ON")); @@ -31,16 +42,20 @@ fn test_format_entry_multiple_cmake_flags() { #[test] fn test_format_entry_single_meson_flag() { - let mut entry = sample_entry(); - entry.meson_flags = vec!["-Db_lto=true".to_string()]; + let entry = ConfigEntry { + meson_flags: vec!["-Db_lto=true".to_string()], + ..ConfigEntry::default() + }; let output = format_entry(&entry); assert!(output.contains("Meson argument: -Db_lto=true")); } #[test] fn test_format_entry_multiple_meson_flags() { - let mut entry = sample_entry(); - entry.meson_flags = vec!["-Db_lto=true".to_string(), "-Db_ndebug=true".to_string()]; + let entry = ConfigEntry { + meson_flags: vec!["-Db_lto=true".to_string(), "-Db_ndebug=true".to_string()], + ..ConfigEntry::default() + }; let output = format_entry(&entry); assert!(output.contains("Meson arguments:")); assert!(output.contains("-Db_lto=true")); diff --git a/tests/config/fixtures.rs b/tests/config/fixtures.rs deleted file mode 100644 index c40ebd5..0000000 --- a/tests/config/fixtures.rs +++ /dev/null @@ -1,21 +0,0 @@ -use star_setup::{cli::BuildType, config::ConfigEntry}; - -pub fn sample_entry() -> ConfigEntry { - ConfigEntry { - ssh: true, - build_type: BuildType::Release, - build_dir: "build".to_string(), - mono_dir: "mono".to_string(), - no_build: false, - clean: true, - verbose: false, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], - watch: false, - no_watch: false, - dev: false, - no_dev: false, - } -} diff --git a/tests/config/io.rs b/tests/config/io.rs index 8c9d346..f12af43 100644 --- a/tests/config/io.rs +++ b/tests/config/io.rs @@ -1,4 +1,4 @@ -use crate::{common::with_io_output, fixtures::sample_entry}; +use crate::common::with_io_output; use star_setup::{ cli::BuildType, config::{insert_config, load_config, save_config, ConfigEntry, SetupConfig}, @@ -19,19 +19,8 @@ fn test_save_and_load_roundtrip() { ConfigEntry { ssh: true, build_type: BuildType::Release, - build_dir: "build".to_string(), mono_dir: "mono".to_string(), - no_build: false, - clean: false, - verbose: false, - timing: false, - dry_run: false, - cmake_flags: vec![], - meson_flags: vec![], - watch: false, - no_watch: false, - dev: false, - no_dev: false, + ..ConfigEntry::default() }, ); @@ -92,11 +81,11 @@ fn test_load_config_first_valid_wins() { with_io_output(|io| { config1.path = Some(path1.clone()); - insert_config(&mut config1, "first", sample_entry()); + insert_config(&mut config1, "first", ConfigEntry::default()); save_config(&mut config1, false, &mut io.output).unwrap(); config2.path = Some(path2.clone()); - insert_config(&mut config2, "second", sample_entry()); + insert_config(&mut config2, "second", ConfigEntry::default()); save_config(&mut config2, false, &mut io.output).unwrap(); let loaded = load_config(&[path1, path2], false, false, &mut io.output); @@ -116,7 +105,7 @@ fn test_load_config_falls_through_invalid_to_valid() { let mut config2 = SetupConfig::new(); config2.path = Some(path2.clone()); - insert_config(&mut config2, "second", sample_entry()); + insert_config(&mut config2, "second", ConfigEntry::default()); with_io_output(|io| { save_config(&mut config2, false, &mut io.output).unwrap(); From 822cac0c6f24451909d4b7d1be85486bedbed5d5 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 15:36:54 -0400 Subject: [PATCH 2/4] refactor: split resolve_with_config into per-flag-group resolvers --- src/cli/resolve.rs | 174 +++++++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 93 deletions(-) diff --git a/src/cli/resolve.rs b/src/cli/resolve.rs index 22cdcab..45b2553 100644 --- a/src/cli/resolve.rs +++ b/src/cli/resolve.rs @@ -1,8 +1,9 @@ use crate::{ cli::{ - Args, BuildType, ResolvedArgs, ResolvedBuildFlags, ResolvedConnectionFlags, ResolvedMonoFlags, + Args, BuildFlags, BuildType, ConnectionFlags, DiagnosticFlags, MonoRepoFlags, ResolvedArgs, + ResolvedBuildFlags, ResolvedConnectionFlags, ResolvedMonoFlags, }, - config::SetupConfig, + config::{ConfigEntry, SetupConfig}, ctx::RunFlags, }; @@ -35,112 +36,99 @@ fn resolve_flag_pair( ) } -/// Resolves raw `Args` into `ResolvedArgs` by applying config defaults and CLI overrides. -/// # Errors -/// Returns an error if the named config does not exist in the provided `SetupConfig`. -pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result { - let config_name = args.config_name.as_deref().unwrap_or("default"); - let default = config.configs.get(config_name); +fn resolve_connection_flags( + c: &ConnectionFlags, + default: Option<&ConfigEntry>, +) -> ResolvedConnectionFlags { + ResolvedConnectionFlags { + ssh: resolve_bool(c.ssh, c.https, default.map(|e| e.ssh), false), + } +} - if args.config_name.is_some() && default.is_none() { - return Err(format!("Configuration '{config_name}' not found")); +fn resolve_run_flags(d: &DiagnosticFlags, default: Option<&ConfigEntry>) -> RunFlags { + RunFlags { + verbose: resolve_bool(d.verbose, d.no_verbose, default.map(|e| e.verbose), false), + timing: resolve_bool(d.timing, d.no_timing, default.map(|e| e.timing), false), + dry_run: resolve_bool(d.dry_run, d.no_dry_run, default.map(|e| e.dry_run), false), } +} - let ssh = resolve_bool( - args.connection.ssh, - args.connection.https, - default.map(|e| e.ssh), - false, - ); - let verbose = resolve_bool( - args.diagnostic.verbose, - args.diagnostic.no_verbose, - default.map(|e| e.verbose), - false, - ); - let timing = resolve_bool( - args.diagnostic.timing, - args.diagnostic.no_timing, - default.map(|e| e.timing), - false, - ); - let dry_run = resolve_bool( - args.diagnostic.dry_run, - args.diagnostic.no_dry_run, - default.map(|e| e.dry_run), - false, - ); - let no_build = resolve_bool( - args.build.no_build, - args.build.build, - default.map(|e| e.no_build), - false, - ); - let clean = resolve_bool( - args.build.clean, - args.build.no_clean, - default.map(|e| e.clean), - false, - ); +fn resolve_build_flags( + b: BuildFlags, + default: Option<&ConfigEntry>, +) -> Result { let (watch, no_watch) = resolve_flag_pair( - args.build.watch, args.build.no_watch, - default.map(|e| e.watch), default.map(|e| e.no_watch), + b.watch, + b.no_watch, + default.map(|e| e.watch), + default.map(|e| e.no_watch), ); let (dev, no_dev) = resolve_flag_pair( - args.build.dev, args.build.no_dev, - default.map(|e| e.dev), default.map(|e| e.no_dev), + b.dev, + b.no_dev, + default.map(|e| e.dev), + default.map(|e| e.no_dev), ); - - let cmake_flags = Some(args.build.cmake_flags) + let cmake_flags = Some(b.cmake_flags) .filter(|f| !f.is_empty()) .unwrap_or_else(|| default.map_or_else(Vec::new, |e| e.cmake_flags.clone())); - - let meson_flags = Some(args.build.meson_flags) + let meson_flags = Some(b.meson_flags) .filter(|f| !f.is_empty()) .unwrap_or_else(|| default.map_or_else(Vec::new, |e| e.meson_flags.clone())); - let repos = args.mono.repos.take(); - let profile = args.mono.profile.take(); - let mono_repo = args.mono.mono_repo || repos.is_some() || profile.is_some(); + Ok(ResolvedBuildFlags { + build_type: match b.build_type { + Some(s) => s.parse::()?, + None => default.map(|e| e.build_type).unwrap_or_default(), + }, + build_dir: b + .build_dir + .or_else(|| default.map(|e| e.build_dir.clone())) + .unwrap_or_else(|| "build".to_string()), + build_system: b.build_system, + no_build: resolve_bool(b.no_build, b.build, default.map(|e| e.no_build), false), + clean: resolve_bool(b.clean, b.no_clean, default.map(|e| e.clean), false), + watch, + no_watch, + dev, + no_dev, + cmake_flags, + meson_flags, + }) +} + +fn resolve_mono_flags(mono: MonoRepoFlags, default: Option<&ConfigEntry>) -> ResolvedMonoFlags { + let repos = mono.repos; + let profile = mono.profile; + let mono_repo = mono.mono_repo || repos.is_some() || profile.is_some(); + ResolvedMonoFlags { + mono_repo, + mono_dir: mono + .mono_dir + .or_else(|| default.map(|e| e.mono_dir.clone())) + .unwrap_or_else(|| "build-mono".to_string()), + repos, + profile, + } +} + +/// Resolves raw `Args` into `ResolvedArgs` by applying config defaults and CLI overrides. +/// # Errors +/// Returns an error if the named config does not exist in the provided `SetupConfig`. +pub fn resolve_with_config(args: Args, config: &SetupConfig) -> Result { + let config_name = args.config_name.as_deref().unwrap_or("default"); + let default = config.configs.get(config_name); + + if args.config_name.is_some() && default.is_none() { + return Err(format!("Configuration '{config_name}' not found")); + } Ok(ResolvedArgs { repo: args.repo, yes: args.yes, - connection: ResolvedConnectionFlags { ssh }, - diagnostic: RunFlags { - verbose, - timing, - dry_run, - }, - build: ResolvedBuildFlags { - build_type: match args.build.build_type { - Some(s) => s.parse::()?, - None => default.map(|e| e.build_type).unwrap_or_default(), - }, - build_dir: args - .build - .build_dir - .or_else(|| default.map(|e| e.build_dir.clone())) - .unwrap_or_else(|| "build".to_string()), - build_system: args.build.build_system, - no_build, - clean, - watch, - no_watch, - dev, - no_dev, - cmake_flags, - meson_flags, - }, - mono: ResolvedMonoFlags { - mono_repo, - mono_dir: args - .mono - .mono_dir - .or_else(|| default.map(|e| e.mono_dir.clone())) - .unwrap_or_else(|| "build-mono".to_string()), - repos, - profile, - }, + connection: resolve_connection_flags(&args.connection, default), + diagnostic: resolve_run_flags(&args.diagnostic, default), + build: resolve_build_flags(args.build, default)?, + mono: resolve_mono_flags(args.mono, default), }) } From 74977ff29bc6f70dd1f0b32175baf14f380ad305 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 15:39:08 -0400 Subject: [PATCH 3/4] test: cover config load with missing fields --- tests/config/io.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/config/io.rs b/tests/config/io.rs index f12af43..42e0122 100644 --- a/tests/config/io.rs +++ b/tests/config/io.rs @@ -113,3 +113,25 @@ fn test_load_config_falls_through_invalid_to_valid() { assert!(loaded.configs.contains_key("second")); }); } + +#[test] +fn test_load_config_defaults_missing_fields() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join(".star-setup.json"); + write( + &path, + r#"{ "configs": { "default": { "ssh": true, "verbose": true } } }"#, + ) + .unwrap(); + + with_io_output(|io| { + let loaded = load_config(&[path], false, false, &mut io.output); + let entry = &loaded.configs["default"]; + assert!(entry.ssh); + assert!(entry.verbose); + assert!(!entry.dev); + assert_eq!(entry.build_dir, "build"); + assert_eq!(entry.mono_dir, "build-mono"); + assert_eq!(entry.build_type, BuildType::Debug); + }); +} From 4ee08ba6db4cc150ef2ec180763f0c9a25169d0d Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sat, 4 Jul 2026 15:39:29 -0400 Subject: [PATCH 4/4] chore: bump v0.4.6 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4cf84ea..17d7bc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "star-setup" -version = "0.4.5" +version = "0.4.6" edition = "2021" repository = "https://github.com/star-setup/core" description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems"