@@ -74,7 +74,12 @@ func (w *Workflow) runInitWorkflow(ctx context.Context) (bool, error) {
7474 return false , nil
7575 }
7676
77- // If we have sync candidates (multi-module project), run the dependency-check setup
77+ // Handle multi-module projects specially
78+ if w .result .Mode == discovery .MultiModule {
79+ return w .runMultiModuleSetup (ctx )
80+ }
81+
82+ // If we have sync candidates (manifest files), run the dependency-check setup
7883 if len (w .result .SyncCandidates ) > 0 {
7984 return w .runDependencyCheckSetup (ctx )
8085 }
@@ -83,6 +88,245 @@ func (w *Workflow) runInitWorkflow(ctx context.Context) (bool, error) {
8388 return w .createConfigWithDefaults (ctx )
8489}
8590
91+ // WorkspaceChoice represents the user's choice for multi-module configuration.
92+ type WorkspaceChoice string
93+
94+ const (
95+ // WorkspaceChoiceCoordinated syncs all .version files to root (coordinated versioning).
96+ WorkspaceChoiceCoordinated WorkspaceChoice = "coordinated"
97+ // WorkspaceChoiceWorkspace configures independent module versions (workspace mode).
98+ WorkspaceChoiceWorkspace WorkspaceChoice = "workspace"
99+ // WorkspaceChoiceSingleRoot uses only the root .version for all manifests.
100+ WorkspaceChoiceSingleRoot WorkspaceChoice = "single"
101+ )
102+
103+ // runMultiModuleSetup handles configuration for multi-module/monorepo projects.
104+ func (w * Workflow ) runMultiModuleSetup (ctx context.Context ) (bool , error ) {
105+ fmt .Println ()
106+ printer .PrintInfo (fmt .Sprintf ("Found %d modules - this appears to be a monorepo." , len (w .result .Modules )))
107+ fmt .Println ()
108+
109+ // Show discovered modules
110+ printer .PrintFaint ("Discovered modules:" )
111+ for _ , m := range w .result .Modules {
112+ fmt .Printf (" - %s (%s)\n " , m .Name , m .RelPath )
113+ }
114+ fmt .Println ()
115+
116+ // Ask how to configure the project
117+ choice , err := w .prompter .Select (
118+ "How would you like to configure versioning?" ,
119+ "Choose how to manage versions in this monorepo." ,
120+ []huh.Option [string ]{
121+ huh .NewOption ("Coordinated versioning (recommended) - all .version files sync to root" , string (WorkspaceChoiceCoordinated )),
122+ huh .NewOption ("Independent workspace - each module versioned separately" , string (WorkspaceChoiceWorkspace )),
123+ huh .NewOption ("Single root only - ignore submodule .version files" , string (WorkspaceChoiceSingleRoot )),
124+ },
125+ )
126+ if err != nil {
127+ return false , err
128+ }
129+
130+ if choice == "" {
131+ printer .PrintFaint ("Configuration canceled." )
132+ return false , nil
133+ }
134+
135+ switch WorkspaceChoice (choice ) {
136+ case WorkspaceChoiceCoordinated :
137+ return w .createConfigWithCoordinatedVersioning (ctx )
138+ case WorkspaceChoiceWorkspace :
139+ return w .createConfigWithWorkspace (ctx )
140+ case WorkspaceChoiceSingleRoot :
141+ // Only configure dependency-check for manifest files found near root
142+ return w .runDependencyCheckSetup (ctx )
143+ default :
144+ return w .createConfigWithDefaults (ctx )
145+ }
146+ }
147+
148+ // createConfigWithCoordinatedVersioning creates .sley.yaml with coordinated versioning.
149+ // All submodule .version files and manifest files sync to the root .version.
150+ func (w * Workflow ) createConfigWithCoordinatedVersioning (ctx context.Context ) (bool , error ) {
151+ // Ensure root .version exists
152+ if err := w .ensureVersionFile (ctx ); err != nil {
153+ return false , err
154+ }
155+
156+ // Combine: submodule .version files + manifest files as sync candidates
157+ var allSyncCandidates []discovery.SyncCandidate
158+
159+ // Add submodule .version files (excluding root)
160+ for _ , m := range w .result .Modules {
161+ if m .RelPath == ".version" {
162+ continue // Skip root
163+ }
164+ allSyncCandidates = append (allSyncCandidates , discovery.SyncCandidate {
165+ Path : m .RelPath ,
166+ Format : parser .FormatRaw ,
167+ Field : "" ,
168+ Version : m .Version ,
169+ Description : "Version file (" + m .RelPath + ")" ,
170+ })
171+ }
172+
173+ // Add manifest files
174+ allSyncCandidates = append (allSyncCandidates , w .result .SyncCandidates ... )
175+
176+ // Create config with dependency-check for ALL files
177+ return w .createConfigWithDependencyCheck (ctx , allSyncCandidates )
178+ }
179+
180+ // createConfigWithWorkspace creates .sley.yaml with workspace configuration for multi-module projects.
181+ func (w * Workflow ) createConfigWithWorkspace (ctx context.Context ) (bool , error ) {
182+ // Initialize .version file if needed
183+ if err := w .ensureVersionFile (ctx ); err != nil {
184+ return false , err
185+ }
186+
187+ // Default plugins: commit-parser and tag-manager
188+ selectedPlugins := []string {"commit-parser" , "tag-manager" }
189+
190+ // Generate config with workspace discovery enabled
191+ configData , err := generateConfigYAMLWithWorkspace (defaultVersionPath (), selectedPlugins , w .result .SyncCandidates )
192+ if err != nil {
193+ return false , fmt .Errorf ("failed to generate config: %w" , err )
194+ }
195+
196+ // Write config file
197+ if err := os .WriteFile (".sley.yaml" , configData , config .ConfigFilePerm ); err != nil {
198+ return false , fmt .Errorf ("failed to write config file: %w" , err )
199+ }
200+
201+ w .printWorkspaceInitSuccess (selectedPlugins )
202+ return true , nil
203+ }
204+
205+ // printWorkspaceInitSuccess prints success messages after workspace initialization.
206+ func (w * Workflow ) printWorkspaceInitSuccess (plugins []string ) {
207+ fmt .Println ()
208+ printer .PrintSuccess (fmt .Sprintf ("Created .sley.yaml with workspace configuration and %d plugin(s)" , len (plugins )))
209+
210+ // Show enabled plugins
211+ fmt .Println ()
212+ printer .PrintInfo ("Enabled plugins:" )
213+ for _ , p := range plugins {
214+ fmt .Printf (" - %s\n " , p )
215+ }
216+
217+ // Show workspace info
218+ fmt .Println ()
219+ printer .PrintInfo ("Workspace configuration:" )
220+ fmt .Println (" - Auto-discovery enabled" )
221+ fmt .Println (" - Each module manages its own .version file" )
222+
223+ // Show discovered modules
224+ fmt .Println ()
225+ printer .PrintInfo (fmt .Sprintf ("Discovered %d module(s):" , len (w .result .Modules )))
226+ for _ , m := range w .result .Modules {
227+ fmt .Printf (" - %s (%s)\n " , m .Name , m .RelPath )
228+ }
229+
230+ // Note about per-module dependency-check
231+ if len (w .result .SyncCandidates ) > 0 {
232+ fmt .Println ()
233+ printer .PrintFaint ("Tip: Each module can have its own .sley.yaml with dependency-check" )
234+ printer .PrintFaint (" configured for manifests in that module's directory." )
235+ }
236+
237+ // Next steps
238+ fmt .Println ()
239+ printer .PrintInfo ("Next steps:" )
240+ fmt .Println (" - Review .sley.yaml and adjust settings" )
241+ fmt .Println (" - Run 'sley bump patch' to see available modules" )
242+ fmt .Println (" - Run 'sley doctor' to verify setup" )
243+ }
244+
245+ // generateConfigYAMLWithWorkspace generates the YAML configuration content with workspace settings.
246+ func generateConfigYAMLWithWorkspace (versionPath string , plugins []string , syncCandidates []discovery.SyncCandidate ) ([]byte , error ) {
247+ cfg := & config.Config {
248+ Path : versionPath ,
249+ }
250+
251+ // Create workspace config with discovery enabled
252+ enabled := true
253+ recursive := true
254+ moduleMaxDepth := 10
255+ cfg .Workspace = & config.WorkspaceConfig {
256+ Discovery : & config.DiscoveryConfig {
257+ Enabled : & enabled ,
258+ Recursive : & recursive ,
259+ ModuleMaxDepth : & moduleMaxDepth ,
260+ Exclude : []string {"testdata" , "node_modules" },
261+ },
262+ }
263+
264+ // Create plugins config based on selections
265+ pluginsCfg := & config.PluginConfig {}
266+
267+ for _ , name := range plugins {
268+ switch name {
269+ case "commit-parser" :
270+ pluginsCfg .CommitParser = true
271+ case "tag-manager" :
272+ pluginsCfg .TagManager = & config.TagManagerConfig {
273+ Enabled : true ,
274+ }
275+ case "dependency-check" :
276+ depCheck := & config.DependencyCheckConfig {
277+ Enabled : true ,
278+ AutoSync : true ,
279+ }
280+ if len (syncCandidates ) > 0 {
281+ depCheck .Files = make ([]config.DependencyFileConfig , len (syncCandidates ))
282+ for i , c := range syncCandidates {
283+ depCheck .Files [i ] = config.DependencyFileConfig {
284+ Path : c .Path ,
285+ Format : c .Format .String (),
286+ Field : c .Field ,
287+ Pattern : c .Pattern ,
288+ }
289+ }
290+ }
291+ pluginsCfg .DependencyCheck = depCheck
292+ }
293+ }
294+
295+ cfg .Plugins = pluginsCfg
296+
297+ return marshalConfigWithWorkspaceComments (cfg , plugins )
298+ }
299+
300+ // marshalConfigWithWorkspaceComments marshals config to YAML with helpful comments for workspace.
301+ func marshalConfigWithWorkspaceComments (cfg * config.Config , plugins []string ) ([]byte , error ) {
302+ data , err := marshalToYAML (cfg )
303+ if err != nil {
304+ return nil , err
305+ }
306+
307+ // Add header comments
308+ var result strings.Builder
309+ result .WriteString ("# sley configuration file\n " )
310+ result .WriteString ("# Documentation: https://github.com/indaco/sley\n " )
311+ result .WriteString ("# Generated by 'sley discover'\n " )
312+ result .WriteString ("\n " )
313+ result .WriteString ("# This is a workspace configuration for a multi-module project.\n " )
314+ result .WriteString ("# Each module with a .version file is discovered automatically.\n " )
315+ result .WriteString ("# Modules can have their own .sley.yaml for module-specific settings.\n " )
316+ result .WriteString ("\n " )
317+
318+ if len (plugins ) > 0 {
319+ result .WriteString ("# Enabled plugins:\n " )
320+ for _ , name := range plugins {
321+ result .WriteString (fmt .Sprintf ("# - %s\n " , name ))
322+ }
323+ result .WriteString ("\n " )
324+ }
325+
326+ result .Write (data )
327+ return []byte (result .String ()), nil
328+ }
329+
86330// runExistingConfigWorkflow handles the case when .sley.yaml already exists.
87331func (w * Workflow ) runExistingConfigWorkflow (ctx context.Context ) (bool , error ) {
88332 // Check for mismatches and offer to fix
0 commit comments