diff --git a/internal/commands/discover/workflow.go b/internal/commands/discover/workflow.go index 24541e0..7619ba2 100644 --- a/internal/commands/discover/workflow.go +++ b/internal/commands/discover/workflow.go @@ -8,6 +8,7 @@ import ( "charm.land/huh/v2" "github.com/goccy/go-yaml" + "github.com/indaco/sley/internal/commands/initialize" "github.com/indaco/sley/internal/config" "github.com/indaco/sley/internal/discovery" "github.com/indaco/sley/internal/parser" @@ -67,6 +68,11 @@ func (w *Workflow) runInitWorkflow(ctx context.Context) (bool, error) { // Check if we have useful suggestions if len(w.result.SyncCandidates) == 0 && len(w.result.Modules) == 0 { + // No .version files found — check for monorepo workspace markers + // (go.work, pnpm-workspace.yaml, package.json workspaces, Cargo.toml [workspace]) + if monoInfo, err := initialize.DetectMonorepo(); err == nil && monoInfo != nil { + return w.runMonorepoInitWorkflow(ctx, monoInfo) + } printer.PrintFaint("Run 'sley init' to create a configuration file.") return false, nil } @@ -99,6 +105,88 @@ func (w *Workflow) runInitWorkflow(ctx context.Context) (bool, error) { return w.createConfigWithDefaults(ctx) } +// runMonorepoInitWorkflow handles the case when no .version files exist but +// a monorepo workspace marker (go.work, pnpm-workspace.yaml, etc.) is found. +// It shows the detected workspace info and offers to run the full workspace +// initialization flow. +func (w *Workflow) runMonorepoInitWorkflow(_ context.Context, monoInfo *initialize.MonorepoInfo) (bool, error) { + fmt.Println() + printer.PrintInfo(fmt.Sprintf("Monorepo detected: %s workspace (%s) with %d module(s):", + monoInfo.Type, monoInfo.MarkerFile, len(monoInfo.Modules))) + for _, m := range monoInfo.Modules { + fmt.Printf(" - %s/\n", m) + } + fmt.Println() + + confirmed, err := w.prompter.Confirm( + "Would you like to initialize as a workspace project?", + "This will create .sley.yaml with workspace discovery, set tag prefix to {module_path}/v,\ncreate .version files in each module, and set versioning to independent.", + ) + if err != nil { + return false, err + } + + if !confirmed { + printer.PrintFaint("Run 'sley init --workspace' when ready.") + return false, nil + } + + // Prompt for plugin selection (same as sley init) + detectionSummary := fmt.Sprintf("Detected: %s workspace (%s)", monoInfo.Type, monoInfo.MarkerFile) + plugins, err := initialize.PromptPluginSelection(detectionSummary) + if err != nil { + return false, err + } + if len(plugins) == 0 { + plugins = initialize.DefaultPluginNames() + } + + // Ensure root .version exists + if err := w.ensureVersionFile(context.Background()); err != nil { + return false, err + } + + // Build DiscoveredModule list from detected monorepo modules + var modules []initialize.DiscoveredModule + for _, m := range monoInfo.Modules { + modules = append(modules, initialize.DiscoveredModule{ + Name: m, + RelPath: m + "/.version", + }) + } + configData, err := initialize.GenerateWorkspaceConfigWithMonorepo(plugins, modules, monoInfo) + if err != nil { + return false, fmt.Errorf("failed to generate config: %w", err) + } + if err := os.WriteFile(".sley.yaml", configData, config.ConfigFilePerm); err != nil { + return false, fmt.Errorf("failed to write config file: %w", err) + } + + // Create .version files in module directories + initialize.CreateMonorepoVersionFiles(monoInfo) + + // Print summary + fmt.Println() + printer.PrintSuccess(fmt.Sprintf("Created .sley.yaml with workspace configuration and %d plugin(s)", len(plugins))) + fmt.Println() + printer.PrintInfo("Enabled plugins:") + for _, p := range plugins { + fmt.Printf(" - %s\n", p) + } + fmt.Println() + printer.PrintInfo("Applied monorepo defaults:") + fmt.Println(" - Versioning: independent") + fmt.Println(" - Tag prefix: {module_path}/v") + fmt.Printf(" - Modules: %d\n", len(monoInfo.Modules)) + fmt.Println() + printer.PrintInfo("Next steps:") + fmt.Println(" - Review .sley.yaml and adjust settings") + fmt.Println(" - Run 'sley bump patch --all' to bump all modules") + fmt.Println(" - Run 'sley doctor' to verify setup") + + return true, nil +} + // WorkspaceChoice represents the user's choice for multi-module configuration. type WorkspaceChoice string diff --git a/internal/commands/initialize/detection_test.go b/internal/commands/initialize/detection_test.go index 40fb5a7..37a07fb 100644 --- a/internal/commands/initialize/detection_test.go +++ b/internal/commands/initialize/detection_test.go @@ -595,7 +595,7 @@ func TestCreateMonorepoVersionFiles(t *testing.T) { Type: "go-work", Modules: []string{"mod-new", "mod-existing"}, } - createMonorepoVersionFiles(info) + CreateMonorepoVersionFiles(info) // mod-new should have .version with 0.0.0 data, err := os.ReadFile("mod-new/.version") diff --git a/internal/commands/initialize/workspace.go b/internal/commands/initialize/workspace.go index b840d45..3570349 100644 --- a/internal/commands/initialize/workspace.go +++ b/internal/commands/initialize/workspace.go @@ -74,7 +74,7 @@ func runWorkspaceInit(path string, yesFlag bool, templateFlag, enableFlag string // Step 6: Create .version files for detected monorepo modules if applyMonorepo { - createMonorepoVersionFiles(monoInfo) + CreateMonorepoVersionFiles(monoInfo) } // Step 7: Print success messages @@ -226,9 +226,9 @@ func createWorkspaceConfigFileWithMonorepo(plugins []string, modules []Discovere return true, nil } -// createMonorepoVersionFiles creates .version files in each detected module directory +// CreateMonorepoVersionFiles creates .version files in each detected module directory // if one does not already exist. Each file is initialized with "0.0.0". -func createMonorepoVersionFiles(monoInfo *MonorepoInfo) { +func CreateMonorepoVersionFiles(monoInfo *MonorepoInfo) { for _, modDir := range monoInfo.Modules { versionFile := filepath.Join(modDir, ".version") if _, err := os.Stat(versionFile); err == nil { @@ -272,14 +272,7 @@ func GenerateWorkspaceConfigWithMonorepo(plugins []string, modules []DiscoveredM sb.WriteString("\n") } - // Plugins section - sb.WriteString("plugins:\n") - for _, pluginName := range plugins { - writePluginConfigWithMonorepo(&sb, pluginName) - } - sb.WriteString("\n") - - // Workspace section with monorepo defaults + // Workspace section first (structure before behavior) sb.WriteString("# Workspace configuration for monorepo support\n") sb.WriteString("workspace:\n") sb.WriteString(" # Versioning mode: \"independent\" (each module versioned separately)\n") @@ -317,6 +310,15 @@ func GenerateWorkspaceConfigWithMonorepo(plugins []string, modules []DiscoveredM } } + sb.WriteString("\n") + + // Plugins section + sb.WriteString("# Plugin configuration\n") + sb.WriteString("plugins:\n") + for _, pluginName := range plugins { + writePluginConfigWithMonorepo(&sb, pluginName) + } + return []byte(sb.String()), nil } @@ -377,14 +379,7 @@ func GenerateWorkspaceConfigWithComments(plugins []string, modules []DiscoveredM sb.WriteString("\n") } - // Plugins section - sb.WriteString("plugins:\n") - for _, pluginName := range plugins { - writePluginConfig(&sb, pluginName) - } - sb.WriteString("\n") - - // Workspace section + // Workspace section first (structure before behavior) sb.WriteString("# Workspace configuration for monorepo support\n") sb.WriteString("workspace:\n") sb.WriteString(" # Discovery settings for automatic module detection\n") @@ -408,6 +403,15 @@ func GenerateWorkspaceConfigWithComments(plugins []string, modules []DiscoveredM } } + sb.WriteString("\n") + + // Plugins section + sb.WriteString("# Plugin configuration\n") + sb.WriteString("plugins:\n") + for _, pluginName := range plugins { + writePluginConfig(&sb, pluginName) + } + return []byte(sb.String()), nil }