From e9b2ab96b5648c543aa9616192697d375335f718 Mon Sep 17 00:00:00 2001 From: indaco Date: Thu, 2 Apr 2026 18:29:01 +0200 Subject: [PATCH] feat: discover offers workspace init for monorepos When no .version files or config exist, sley discover now detects monorepo workspace markers and offers the full workspace initialization flow with plugin selection. Also reorders generated .sley.yaml to place workspace section before plugins, matching the documented structure. --- internal/commands/discover/workflow.go | 88 +++++++++++++++++++ .../commands/initialize/detection_test.go | 2 +- internal/commands/initialize/workspace.go | 42 +++++---- 3 files changed, 112 insertions(+), 20 deletions(-) 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 }