33//! Auto-detects workspace structure, toolchain settings, and generates
44//! a sensible .config/rail.toml with smart defaults.
55
6- use crate :: config:: { PolicyConfig , RailConfig , SecurityConfig , ToolchainConfig , UnifyConfig , WorkspaceConfig } ;
6+ use crate :: config:: {
7+ CratePath , PolicyConfig , RailConfig , SecurityConfig , SplitConfig , SplitMode , ToolchainConfig , UnifyConfig ,
8+ WorkspaceConfig , WorkspaceMode ,
9+ } ;
710use crate :: error:: { RailError , RailResult } ;
811use crate :: workspace:: WorkspaceContext ;
912use std:: fs;
@@ -52,6 +55,7 @@ pub fn run_init(
5255 let policy = detect_policy_config ( workspace_root) ?;
5356 let unify = default_unify_config ( ) ;
5457 let security = default_security_config ( ) ;
58+ let splits = detect_workspace_splits ( ctx) ;
5559
5660 // 3. Display summary
5761 println ! ( "Workspace Analysis:" ) ;
@@ -105,7 +109,7 @@ pub fn run_init(
105109 // 5. Build config
106110 println ! ( "\n ✅ Generated configuration with smart defaults\n " ) ;
107111
108- let config = build_rail_config ( workspace_root. to_path_buf ( ) , toolchain, policy, unify, security) ;
112+ let config = build_rail_config ( workspace_root. to_path_buf ( ) , toolchain, policy, unify, security, splits ) ;
109113
110114 // 6. Serialize with comments
111115 let toml_content = serialize_config_with_comments ( & config) ?;
@@ -291,13 +295,52 @@ fn default_security_config() -> SecurityConfig {
291295 SecurityConfig :: default ( )
292296}
293297
298+ /// Auto-detect workspace members and create split configs
299+ fn detect_workspace_splits ( ctx : & WorkspaceContext ) -> Vec < SplitConfig > {
300+ let workspace_root = ctx. workspace_root ( ) ;
301+ let members = ctx. cargo . metadata ( ) . list_crates ( ) ;
302+
303+ let mut splits = Vec :: new ( ) ;
304+
305+ for pkg in members {
306+ // Get relative path from workspace root to crate directory
307+ let crate_dir = pkg. manifest_path . parent ( ) . expect ( "manifest has parent" ) ;
308+ let rel_path = match crate_dir. strip_prefix ( workspace_root) {
309+ Ok ( p) => p. to_path_buf ( ) ,
310+ Err ( _) => continue , // Skip if not under workspace root
311+ } ;
312+
313+ // Generate a reasonable remote URL placeholder (GitHub org/repo pattern)
314+ let remote =
format ! ( "[email protected] :org/{}.git" , pkg
. name
) ; 315+
316+ // Check if crate has publish = false in Cargo.toml
317+ let publish = pkg. publish . as_ref ( ) . map ( |p| !p. is_empty ( ) ) . unwrap_or ( true ) ;
318+
319+ splits. push ( SplitConfig {
320+ name : pkg. name . to_string ( ) ,
321+ remote,
322+ branch : "main" . to_string ( ) ,
323+ mode : SplitMode :: Single ,
324+ workspace_mode : WorkspaceMode :: default ( ) ,
325+ paths : vec ! [ CratePath { path: rel_path. into( ) } ] ,
326+ include : vec ! [ ] ,
327+ exclude : vec ! [ ] ,
328+ publish,
329+ changelog_path : None , // Use default from ReleaseConfig
330+ } ) ;
331+ }
332+
333+ splits
334+ }
335+
294336/// Build a complete RailConfig from detected/default values
295337fn build_rail_config (
296338 _workspace_root : PathBuf ,
297339 toolchain : ToolchainConfig ,
298340 policy : PolicyConfig ,
299341 unify : UnifyConfig ,
300342 security : SecurityConfig ,
343+ splits : Vec < SplitConfig > ,
301344) -> RailConfig {
302345 RailConfig {
303346 workspace : WorkspaceConfig {
@@ -307,7 +350,8 @@ fn build_rail_config(
307350 policy,
308351 unify,
309352 security,
310- splits : vec ! [ ] ,
353+ release : crate :: config:: ReleaseConfig :: default ( ) ,
354+ splits,
311355 }
312356}
313357
@@ -553,33 +597,138 @@ fn serialize_config_with_comments(config: &RailConfig) -> RailResult<String> {
553597
554598 output. push ( '\n' ) ;
555599
600+ // Release
601+ output. push_str ( "# ┌─────────────────────────────────────────────────────────────────────────┐\n " ) ;
602+ output. push_str ( "# │ Release & Publishing │\n " ) ;
603+ output. push_str ( "# └─────────────────────────────────────────────────────────────────────────┘\n " ) ;
604+ output. push_str ( "# Workspace-wide release defaults for version bumping and publishing.\n " ) ;
605+ output. push_str ( "# Per-crate settings are configured in [[splits]] below.\n " ) ;
606+ output. push_str ( "#\n " ) ;
607+ output. push_str ( "# Commands:\n " ) ;
608+ output. push_str ( "# cargo rail release plan - Preview release changes (dry-run)\n " ) ;
609+ output. push_str ( "# cargo rail release publish --execute - Execute release\n " ) ;
610+ output. push_str ( "# cargo rail release check - Validate release readiness (CI)\n " ) ;
611+ output. push_str ( "#\n " ) ;
612+ output. push_str ( "# Fields:\n " ) ;
613+ output. push_str ( "# tag_prefix - Prefix for git tags (default: \" v\" )\n " ) ;
614+ output. push_str ( "# tag_format - Tag template: {crate}-v{version} for monorepos\n " ) ;
615+ output. push_str ( "# require_clean - Require clean working directory\n " ) ;
616+ output. push_str ( "# publish_delay - Seconds between crate publishes\n " ) ;
617+ output. push_str ( "# create_github_release - Auto-create GitHub releases via gh CLI\n " ) ;
618+ output. push_str ( "# sign_tags - Sign git tags with GPG/SSH\n " ) ;
619+ output. push_str ( "# changelog_path - Default changelog filename\n \n " ) ;
620+
621+ output. push_str ( "[release]\n " ) ;
622+ output. push_str ( & format ! (
623+ "tag_prefix = \" {}\" # Prefix for version tags\n " ,
624+ config. release. tag_prefix
625+ ) ) ;
626+ output. push_str ( & format ! (
627+ "tag_format = \" {}\" # Variables: {{crate}}, {{version}}\n " ,
628+ config. release. tag_format
629+ ) ) ;
630+ output. push_str ( & format ! (
631+ "require_clean = {} # Require clean working directory\n " ,
632+ config. release. require_clean
633+ ) ) ;
634+ output. push_str ( & format ! (
635+ "publish_delay = {} # Seconds between publishes\n " ,
636+ config. release. publish_delay
637+ ) ) ;
638+ output. push_str ( & format ! (
639+ "create_github_release = {} # Create GitHub releases\n " ,
640+ config. release. create_github_release
641+ ) ) ;
642+ output. push_str ( & format ! (
643+ "sign_tags = {} # Sign tags with GPG/SSH\n " ,
644+ config. release. sign_tags
645+ ) ) ;
646+ output. push_str ( & format ! (
647+ "changelog_path = \" {}\" # Default changelog file\n " ,
648+ config. release. changelog_path
649+ ) ) ;
650+
651+ output. push ( '\n' ) ;
652+
556653 // Split/Sync
557654 output. push_str ( "# ┌─────────────────────────────────────────────────────────────────────────┐\n " ) ;
558655 output. push_str ( "# │ Split/Sync Configuration (Monorepo ↔ Separate Repos) │\n " ) ;
559656 output. push_str ( "# └─────────────────────────────────────────────────────────────────────────┘\n " ) ;
560- output. push_str ( "# Configure crates to split from monorepo into standalone repositories \n " ) ;
561- output. push_str ( "# with bidirectional synchronization .\n " ) ;
657+ output. push_str ( "# Each workspace member is auto-detected and configured for split/sync. \n " ) ;
658+ output. push_str ( "# Update 'remote' URLs and 'publish' flags as needed .\n " ) ;
562659 output. push_str ( "#\n " ) ;
563660 output. push_str ( "# Commands:\n " ) ;
564661 output. push_str ( "# cargo rail split <crate> - Extract crate to separate repo\n " ) ;
565662 output. push_str ( "# cargo rail sync <crate> - Bidirectional sync\n " ) ;
566663 output. push_str ( "# cargo rail status - Show split/sync status\n " ) ;
567664 output. push_str ( "#\n " ) ;
568- output. push_str ( "# Example configuration:\n " ) ;
569- output. push_str ( "#\n " ) ;
570- output. push_str ( "# [[splits]]\n " ) ;
571- output. push_str ( "# name = \" my-crate\" # Crate name\n " ) ;
572- output
. push_str ( "# remote = \" [email protected] :org/my-crate.git\" # Target repository\n " ) ; 573- output. push_str ( "# branch = \" main\" # Branch to sync\n " ) ;
574- output. push_str ( "# mode = \" single\" # \" single\" or \" multi\" (layout mode)\n " ) ;
575- output. push_str ( "#\n " ) ;
576- output. push_str ( "# # Paths to include in split (workspace members)\n " ) ;
577- output. push_str ( "# [[splits.paths]]\n " ) ;
578- output. push_str ( "# crate = \" crates/my-crate\" # Relative path from workspace root\n " ) ;
665+ output. push_str ( "# Fields per [[splits]] entry:\n " ) ;
666+ output. push_str ( "# name - Crate name\n " ) ;
667+ output. push_str ( "# remote - Target repository URL (update this!)\n " ) ;
668+ output. push_str ( "# branch - Branch to sync (default: main)\n " ) ;
669+ output. push_str ( "# mode - \" single\" or \" combined\" layout\n " ) ;
670+ output. push_str ( "# publish - Enable publishing to crates.io (default: true)\n " ) ;
671+ output. push_str ( "# changelog_path - Per-crate changelog override (optional)\n " ) ;
579672 output. push_str ( "#\n " ) ;
580- output. push_str ( "# # Optional: Additional paths to sync\n " ) ;
581- output. push_str ( "# [[splits.paths]]\n " ) ;
582- output. push_str ( "# crate = \" crates/my-crate-macros\" \n " ) ;
673+
674+ // Serialize detected splits
675+ if config. splits . is_empty ( ) {
676+ output. push_str ( "# No workspace members detected. Example:\n " ) ;
677+ output. push_str ( "#\n " ) ;
678+ output. push_str ( "# [[splits]]\n " ) ;
679+ output. push_str ( "# name = \" my-crate\" \n " ) ;
680+ output
. push_str ( "# remote = \" [email protected] :org/my-crate.git\" \n " ) ; 681+ output. push_str ( "# branch = \" main\" \n " ) ;
682+ output. push_str ( "# mode = \" single\" \n " ) ;
683+ output. push_str ( "# publish = true\n " ) ;
684+ output. push_str ( "#\n " ) ;
685+ output. push_str ( "# [[splits.paths]]\n " ) ;
686+ output. push_str ( "# crate = \" crates/my-crate\" \n " ) ;
687+ } else {
688+ output. push_str ( & format ! (
689+ "# Auto-detected {} workspace member(s):\n \n " ,
690+ config. splits. len( )
691+ ) ) ;
692+
693+ for split in & config. splits {
694+ output. push_str ( "[[splits]]\n " ) ;
695+ output. push_str ( & format ! ( "name = \" {}\" \n " , split. name) ) ;
696+ output. push_str ( & format ! (
697+ "remote = \" {}\" # TODO: Update with actual repository URL\n " ,
698+ split. remote
699+ ) ) ;
700+ output. push_str ( & format ! ( "branch = \" {}\" \n " , split. branch) ) ;
701+ output. push_str ( & format ! (
702+ "mode = \" {}\" \n " ,
703+ match split. mode {
704+ SplitMode :: Single => "single" ,
705+ SplitMode :: Combined => "combined" ,
706+ }
707+ ) ) ;
708+ output. push_str ( & format ! (
709+ "publish = {} # {}\n " ,
710+ split. publish,
711+ if split. publish {
712+ "Enable crates.io publishing"
713+ } else {
714+ "Skip publishing (publish = false in Cargo.toml)"
715+ }
716+ ) ) ;
717+
718+ if let Some ( ref changelog) = split. changelog_path {
719+ output. push_str ( & format ! ( "changelog_path = \" {}\" \n " , changelog. display( ) ) ) ;
720+ }
721+
722+ output. push ( '\n' ) ;
723+
724+ for path in & split. paths {
725+ output. push_str ( "[[splits.paths]]\n " ) ;
726+ output. push_str ( & format ! ( "crate = \" {}\" \n " , path. path. display( ) ) ) ;
727+ }
728+
729+ output. push ( '\n' ) ;
730+ }
731+ }
583732
584733 Ok ( output)
585734}
@@ -692,6 +841,7 @@ pub fn run_init_standalone(
692841 pr_branch_pattern : "rail/sync/{crate}/{timestamp}" . to_string ( ) ,
693842 protected_branches : vec ! [ "main" . to_string( ) , "master" . to_string( ) ] ,
694843 } ,
844+ release : crate :: config:: ReleaseConfig :: default ( ) ,
695845 splits : vec ! [ ] ,
696846 } ;
697847
@@ -830,6 +980,7 @@ rust-version = "1.91"
830980 policy : PolicyConfig :: default ( ) ,
831981 unify : UnifyConfig :: default ( ) ,
832982 security : SecurityConfig :: default ( ) ,
983+ release : crate :: config:: ReleaseConfig :: default ( ) ,
833984 splits : vec ! [ ] ,
834985 } ;
835986
0 commit comments