Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,12 @@ __debug_bin

# Test cache
.cache/
docs/

# Added by MoMorph
.mcp.json
mcp.json
.momorph/
.claude
CLAUDE.md
.windsurf/
24 changes: 21 additions & 3 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/momorph/cli/internal/api"
"github.com/momorph/cli/internal/auth"
"github.com/momorph/cli/internal/beads"
"github.com/momorph/cli/internal/config"
"github.com/momorph/cli/internal/logger"
"github.com/momorph/cli/internal/template"
Expand All @@ -20,8 +21,9 @@ import (
)

var (
aiTool string
templateTag string
aiTool string
templateTag string
installBeads bool
// ErrUserCancelled is returned when the user cancels an operation
ErrUserCancelled = errors.New("user cancelled")
)
Expand All @@ -31,14 +33,16 @@ var initCmd = &cobra.Command{
Short: "Initialize a new MoMorph project from the latest template",
Example: ` momorph init my-project --ai=copilot
momorph init . --ai=cursor
momorph init my-project`,
momorph init my-project --with-beads
momorph init my-project --ai=claude --tag=stable --with-beads`,
Args: cobra.ExactArgs(1),
RunE: runInit,
}

func init() {
initCmd.Flags().StringVar(&aiTool, "ai", "", "AI tool to use (copilot, cursor, claude, windsurf, gemini)")
initCmd.Flags().StringVar(&templateTag, "tag", "", "Template version tag (stable, latest, or specific version)")
initCmd.Flags().BoolVar(&installBeads, "with-beads", false, "Install uv and beads-mcp for task management")
rootCmd.AddCommand(initCmd)
}

Expand Down Expand Up @@ -194,6 +198,20 @@ func runInit(cmd *cobra.Command, args []string) error {
}
}

// Install beads-mcp (requires uv) - only if flag is set
if installBeads {
fmt.Println("🔮 Installing beads-mcp...")
beadsResult := beads.EnsureInstalled()
if beadsResult.Error != nil {
logger.Warn("beads-mcp installation failed: %v", beadsResult.Error)
fmt.Printf(" ⚠ %s\n", beadsResult.Message)
} else if beadsResult.Installed {
fmt.Printf(" ✓ %s\n", beadsResult.Message)
} else {
fmt.Printf(" ⚠ %s\n", beadsResult.Message)
}
}

// Install VS Code extension
fmt.Println("📦 Installing VS Code extension...")
result := vscode.InstallExtension()
Expand Down
104 changes: 104 additions & 0 deletions internal/beads/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package beads

import (
"bytes"
"fmt"
"os/exec"
"strings"

"github.com/momorph/cli/internal/logger"
"github.com/momorph/cli/internal/uv"
)

// InstallResult represents the result of beads-mcp installation
type InstallResult struct {
Installed bool
Message string
Error error
}

// IsInstalled checks if beads-mcp is installed
func IsInstalled() bool {
// Check if beads-mcp is available via uvx
cmd := exec.Command("uvx", "beads-mcp", "--version")
if err := cmd.Run(); err != nil {
logger.Debug("beads-mcp not found via uvx: %v", err)
return false
}
logger.Debug("beads-mcp is installed")
return true
}

// Install installs beads-mcp using uv tool install
func Install() InstallResult {
// First ensure uv is installed
if !uv.IsInstalled() {
uvResult := uv.Install()
if uvResult.Error != nil {
return InstallResult{
Installed: false,
Message: fmt.Sprintf("Cannot install beads-mcp: uv is required. %s", uvResult.Message),
Error: uvResult.Error,
}
}
// If uv was just installed, it might not be in PATH yet
if !uv.IsInstalled() {
return InstallResult{
Installed: false,
Message: "uv was installed but is not available in PATH. Please restart your terminal and run 'uv tool install beads-mcp' manually",
Error: nil,
}
}
}

// Install beads-mcp using uv tool install (with packaging dependency)
logger.Debug("Installing beads-mcp with: uv tool install beads-mcp --with packaging")

cmd := exec.Command("uv", "tool", "install", "beads-mcp", "--with", "packaging")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
// Check if it's already installed (uv returns error if already installed)
stderrStr := stderr.String()
if strings.Contains(stderrStr, "already installed") || strings.Contains(stdout.String(), "already installed") {
return InstallResult{
Installed: true,
Message: "beads-mcp is already installed",
Error: nil,
}
}

logger.Debug("beads-mcp install stdout: %s", stdout.String())
logger.Debug("beads-mcp install stderr: %s", stderrStr)
return InstallResult{
Installed: false,
Message: fmt.Sprintf("Failed to install beads-mcp: %v", err),
Error: err,
}
}

return InstallResult{
Installed: true,
Message: "beads-mcp installed successfully",
Error: nil,
}
}

// EnsureInstalled ensures beads-mcp is installed
func EnsureInstalled() InstallResult {
// First ensure uv is available
if !uv.IsInstalled() {
uvResult := uv.EnsureInstalled()
if uvResult.Error != nil || !uvResult.Installed {
return InstallResult{
Installed: false,
Message: fmt.Sprintf("Cannot install beads-mcp: %s", uvResult.Message),
Error: uvResult.Error,
}
}
}

return Install()
}
111 changes: 91 additions & 20 deletions internal/template/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func (c *copilotConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpSe
// cursorConfigUpdater handles Cursor-specific config updates
type cursorConfigUpdater struct{}

// ConfigureMCPServer updates Cursor's global mcp.json with MoMorph server
// ConfigureMCPServer updates Cursor's global mcp.json with servers from project's .mcp.json
// Config file: ~/.cursor/mcp.json
func (c *cursorConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpServerEndpoint string) error {
// Cursor config is in user's home directory, not project directory
Expand All @@ -139,7 +139,7 @@ func (c *cursorConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpSer
return fmt.Errorf("failed to create .cursor directory: %w", err)
}

// Read existing config or create new one
// Read existing global config or create new one
var mcpConfig map[string]interface{}
if data, err := os.ReadFile(mcpFilePath); err == nil {
if err := json.Unmarshal(data, &mcpConfig); err != nil {
Expand All @@ -151,18 +151,32 @@ func (c *cursorConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpSer
mcpConfig = make(map[string]interface{})
}

// Get or create mcpServers
var servers map[string]interface{}
// Get or create mcpServers in global config
var globalServers map[string]interface{}
if serversInterface, exists := mcpConfig["mcpServers"]; exists {
servers, _ = serversInterface.(map[string]interface{})
globalServers, _ = serversInterface.(map[string]interface{})
}
if servers == nil {
servers = make(map[string]interface{})
mcpConfig["mcpServers"] = servers
if globalServers == nil {
globalServers = make(map[string]interface{})
mcpConfig["mcpServers"] = globalServers
}

// Add/update momorph server configuration
servers["momorph"] = map[string]interface{}{
// Read project's .mcp.json and merge servers
projectMCPPath := filepath.Join(projectDir, ".mcp.json")
if projectServers, err := readMCPServers(projectMCPPath); err == nil {
for name, server := range projectServers {
// Skip if server already exists in global config (don't overwrite user's config)
if _, exists := globalServers[name]; !exists {
globalServers[name] = server
logger.Debug("Added server '%s' to Cursor config", name)
}
}
} else {
logger.Debug("Could not read project .mcp.json for Cursor: %v", err)
}

// Always update momorph server with current token and endpoint
globalServers["momorph"] = map[string]interface{}{
"url": mcpServerEndpoint,
"headers": map[string]string{
"x-github-token": githubToken,
Expand All @@ -186,7 +200,7 @@ func (c *cursorConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpSer
// windsurfConfigUpdater handles Windsurf-specific config updates
type windsurfConfigUpdater struct{}

// ConfigureMCPServer updates Windsurf's global mcp_config.json with MoMorph server
// ConfigureMCPServer updates Windsurf's global mcp_config.json with servers from project's .mcp.json
// Config file: ~/.codeium/windsurf/mcp_config.json
func (w *windsurfConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpServerEndpoint string) error {
// Windsurf config is in user's home directory
Expand All @@ -203,7 +217,7 @@ func (w *windsurfConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpS
return fmt.Errorf("failed to create windsurf config directory: %w", err)
}

// Read existing config or create new one
// Read existing global config or create new one
var mcpConfig map[string]interface{}
if data, err := os.ReadFile(mcpFilePath); err == nil {
if err := json.Unmarshal(data, &mcpConfig); err != nil {
Expand All @@ -214,19 +228,38 @@ func (w *windsurfConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpS
mcpConfig = make(map[string]interface{})
}

// Get or create mcpServers
var servers map[string]interface{}
// Get or create mcpServers in global config
var globalServers map[string]interface{}
if serversInterface, exists := mcpConfig["mcpServers"]; exists {
servers, _ = serversInterface.(map[string]interface{})
globalServers, _ = serversInterface.(map[string]interface{})
}
if servers == nil {
servers = make(map[string]interface{})
mcpConfig["mcpServers"] = servers
if globalServers == nil {
globalServers = make(map[string]interface{})
mcpConfig["mcpServers"] = globalServers
}

// Read project's .mcp.json and merge servers (with key transformation)
projectMCPPath := filepath.Join(projectDir, ".mcp.json")
if projectServers, err := readMCPServers(projectMCPPath); err == nil {
for name, server := range projectServers {
// Skip if server already exists in global config (don't overwrite user's config)
if _, exists := globalServers[name]; !exists {
// Transform server config for Windsurf (url -> serverUrl)
if serverMap, ok := server.(map[string]interface{}); ok {
globalServers[name] = transformServerForWindsurf(serverMap)
} else {
globalServers[name] = server
}
logger.Debug("Added server '%s' to Windsurf config", name)
}
}
} else {
logger.Debug("Could not read project .mcp.json for Windsurf: %v", err)
}

// Add/update momorph server configuration
// Always update momorph server with current token and endpoint
// Windsurf uses "serverUrl" instead of "url"
servers["momorph"] = map[string]interface{}{
globalServers["momorph"] = map[string]interface{}{
"serverUrl": mcpServerEndpoint,
"headers": map[string]string{
"x-github-token": githubToken,
Expand All @@ -247,6 +280,44 @@ func (w *windsurfConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpS
return nil
}

// readMCPServers reads the mcpServers from a .mcp.json file
func readMCPServers(mcpFilePath string) (map[string]interface{}, error) {
data, err := os.ReadFile(mcpFilePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}

var mcpConfig map[string]interface{}
if err := json.Unmarshal(data, &mcpConfig); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}

serversInterface, exists := mcpConfig["mcpServers"]
if !exists {
return nil, fmt.Errorf("no mcpServers field found")
}

servers, ok := serversInterface.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("mcpServers is not a valid object")
}

return servers, nil
}

// transformServerForWindsurf converts server config for Windsurf (url -> serverUrl)
func transformServerForWindsurf(server map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range server {
if k == "url" {
result["serverUrl"] = v
} else {
result[k] = v
}
}
return result
}

// GetConfigUpdater returns the appropriate config updater for the given AI tool
func GetConfigUpdater(aiTool string) ConfigUpdater {
switch aiTool {
Expand Down
Loading