diff --git a/.gitignore b/.gitignore index f713869e..d8da873d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ linux-s390x/sqlcmd # Build artifacts in root /sqlcmd /sqlcmd_binary +/modern # certificates used for local testing *.der diff --git a/README.md b/README.md index e4a1e35d..0d50d8d3 100644 --- a/README.md +++ b/README.md @@ -59,14 +59,14 @@ The Homebrew package manager may be used on Linux and Windows Subsystem for Linu Use `sqlcmd` to create SQL Server and Azure SQL Edge instances using a local container runtime (e.g. [Docker][] or [Podman][]) -### Create SQL Server instance using local container runtime and connect using Azure Data Studio +### Create SQL Server instance using local container runtime and connect using Visual Studio Code -To create a local SQL Server instance with the AdventureWorksLT database restored, query it, and connect to it using Azure Data Studio, run: +To create a local SQL Server instance with the AdventureWorksLT database restored, query it, and connect to it using Visual Studio Code with the MSSQL extension, run: ``` sqlcmd create mssql --accept-eula --using https://aka.ms/AdventureWorksLT.bak sqlcmd query "SELECT DB_NAME()" -sqlcmd open ads +sqlcmd open vscode ``` Use `sqlcmd --help` to view all the available sub-commands. Use `sqlcmd -?` to view the original ODBC `sqlcmd` flags. diff --git a/cmd/modern/root/open.go b/cmd/modern/root/open.go index d209db81..bbda4d25 100644 --- a/cmd/modern/root/open.go +++ b/cmd/modern/root/open.go @@ -17,7 +17,7 @@ type Open struct { func (c *Open) DefineCommand(...cmdparser.CommandOptions) { options := cmdparser.CommandOptions{ Use: "open", - Short: localizer.Sprintf("Open tools (e.g Azure Data Studio) for current context"), + Short: localizer.Sprintf("Open tools (e.g Visual Studio Code, SSMS) for current context"), SubCommands: c.SubCommands(), } @@ -25,11 +25,13 @@ func (c *Open) DefineCommand(...cmdparser.CommandOptions) { } // SubCommands sets up the sub-commands for `sqlcmd open` such as -// `sqlcmd open ads` +// `sqlcmd open ads`, `sqlcmd open vscode`, and `sqlcmd open ssms` func (c *Open) SubCommands() []cmdparser.Command { dependencies := c.Dependencies() return []cmdparser.Command{ cmdparser.New[*open.Ads](dependencies), + cmdparser.New[*open.VSCode](dependencies), + cmdparser.New[*open.Ssms](dependencies), } } diff --git a/cmd/modern/root/open/ssms.go b/cmd/modern/root/open/ssms.go new file mode 100644 index 00000000..32d7ca52 --- /dev/null +++ b/cmd/modern/root/open/ssms.go @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "fmt" + "runtime" + "strings" + + "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig" + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/config" + "github.com/microsoft/go-sqlcmd/internal/container" + "github.com/microsoft/go-sqlcmd/internal/localizer" + "github.com/microsoft/go-sqlcmd/internal/tools" +) + +// Ssms implements the `sqlcmd open ssms` command. It opens +// SQL Server Management Studio and connects to the current context using the +// credentials specified in the context. +func (c *Ssms) DefineCommand(...cmdparser.CommandOptions) { + options := cmdparser.CommandOptions{ + Use: "ssms", + Short: localizer.Sprintf("Open SQL Server Management Studio and connect to current context"), + Examples: []cmdparser.ExampleOptions{{ + Description: localizer.Sprintf("Open SSMS and connect using the current context"), + Steps: []string{"sqlcmd open ssms"}}}, + Run: c.run, + } + + c.Cmd.DefineCommand(options) +} + +// Launch SSMS and connect to the current context +func (c *Ssms) run() { + endpoint, user := config.CurrentContext() + + // If the context has a local container, ensure it is running, otherwise bail out + if endpoint.ContainerDetails != nil { + c.ensureContainerIsRunning(endpoint) + } + + // Launch SSMS with connection parameters + c.launchSsms(endpoint.Address, endpoint.Port, user) +} + +func (c *Ssms) ensureContainerIsRunning(endpoint sqlconfig.Endpoint) { + output := c.Output() + controller := container.NewController() + if !controller.ContainerRunning(endpoint.Id) { + output.FatalWithHintExamples([][]string{ + {localizer.Sprintf("To start the container"), localizer.Sprintf("sqlcmd start")}, + }, localizer.Sprintf("Container is not running")) + } +} + +// launchSsms launches SQL Server Management Studio using the specified server and user credentials. +func (c *Ssms) launchSsms(host string, port int, user *sqlconfig.User) { + output := c.Output() + + // Build server connection string + serverArg := fmt.Sprintf("%s,%d", host, port) + + args := []string{ + "-S", serverArg, + "-nosplash", + } + + // Add authentication parameters + if user != nil && user.AuthenticationType == "basic" { + // SQL Server authentication + // Escape double quotes in username (SQL Server allows " in login names) + username := strings.ReplaceAll(user.BasicAuth.Username, `"`, `\"`) + args = append(args, "-U", username) + // Note: -P parameter was removed in SSMS 18+ for security reasons + // User will need to enter password in the login dialog + output.Info(localizer.Sprintf("Note: You will need to enter the password in the SSMS login dialog")) + } else { + // Windows integrated authentication + if runtime.GOOS == "windows" { + args = append(args, "-E") + } + } + + tool := tools.NewTool("ssms") + if !tool.IsInstalled() { + output.Fatal(tool.HowToInstall()) + } + + c.displayPreLaunchInfo() + + _, err := tool.Run(args) + c.CheckErr(err) +} diff --git a/cmd/modern/root/open/ssms_darwin.go b/cmd/modern/root/open/ssms_darwin.go new file mode 100644 index 00000000..3c5e0950 --- /dev/null +++ b/cmd/modern/root/open/ssms_darwin.go @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/localizer" +) + +// Type Ssms is used to implement the "open ssms" which launches SQL Server +// Management Studio and establishes a connection to the SQL Server for the current +// context +type Ssms struct { + cmdparser.Cmd +} + +func (c *Ssms) displayPreLaunchInfo() { + output := c.Output() + output.Info(localizer.Sprintf("SSMS is only available on Windows")) + output.Info(localizer.Sprintf("Please use 'sqlcmd open vscode' or 'sqlcmd open ads' instead")) +} diff --git a/cmd/modern/root/open/ssms_linux.go b/cmd/modern/root/open/ssms_linux.go new file mode 100644 index 00000000..3c5e0950 --- /dev/null +++ b/cmd/modern/root/open/ssms_linux.go @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/localizer" +) + +// Type Ssms is used to implement the "open ssms" which launches SQL Server +// Management Studio and establishes a connection to the SQL Server for the current +// context +type Ssms struct { + cmdparser.Cmd +} + +func (c *Ssms) displayPreLaunchInfo() { + output := c.Output() + output.Info(localizer.Sprintf("SSMS is only available on Windows")) + output.Info(localizer.Sprintf("Please use 'sqlcmd open vscode' or 'sqlcmd open ads' instead")) +} diff --git a/cmd/modern/root/open/ssms_test.go b/cmd/modern/root/open/ssms_test.go new file mode 100644 index 00000000..da7fd475 --- /dev/null +++ b/cmd/modern/root/open/ssms_test.go @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig" + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/config" + "runtime" + "testing" +) + +// TestSsms runs a sanity test of `sqlcmd open ssms` +func TestSsms(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("SSMS is only available on Windows") + } + + cmdparser.TestSetup(t) + config.AddEndpoint(sqlconfig.Endpoint{ + AssetDetails: nil, + EndpointDetails: sqlconfig.EndpointDetails{}, + Name: "endpoint", + }) + config.AddContext(sqlconfig.Context{ + ContextDetails: sqlconfig.ContextDetails{ + Endpoint: "endpoint", + User: nil, + }, + Name: "context", + }) + config.SetCurrentContextName("context") + + cmdparser.TestCmd[*Ssms]() +} diff --git a/cmd/modern/root/open/ssms_windows.go b/cmd/modern/root/open/ssms_windows.go new file mode 100644 index 00000000..f0d7462b --- /dev/null +++ b/cmd/modern/root/open/ssms_windows.go @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/localizer" +) + +// Type Ssms is used to implement the "open ssms" which launches SQL Server +// Management Studio and establishes a connection to the SQL Server for the current +// context +type Ssms struct { + cmdparser.Cmd +} + +// On Windows, display info before launching +func (c *Ssms) displayPreLaunchInfo() { + output := c.Output() + output.Info(localizer.Sprintf("Launching SQL Server Management Studio...")) +} diff --git a/cmd/modern/root/open/vscode.go b/cmd/modern/root/open/vscode.go new file mode 100644 index 00000000..6738b998 --- /dev/null +++ b/cmd/modern/root/open/vscode.go @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig" + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/config" + "github.com/microsoft/go-sqlcmd/internal/container" + "github.com/microsoft/go-sqlcmd/internal/localizer" + "github.com/microsoft/go-sqlcmd/internal/tools" +) + +// VSCode implements the `sqlcmd open vscode` command. It opens +// Visual Studio Code and configures a connection profile for the +// current context using the MSSQL extension. +func (c *VSCode) DefineCommand(...cmdparser.CommandOptions) { + options := cmdparser.CommandOptions{ + Use: "vscode", + Short: localizer.Sprintf("Open Visual Studio Code and configure connection for current context"), + Examples: []cmdparser.ExampleOptions{{ + Description: localizer.Sprintf("Open VS Code and configure connection using the current context"), + Steps: []string{"sqlcmd open vscode"}}}, + Run: c.run, + } + + c.Cmd.DefineCommand(options) +} + +// Launch VS Code and configure connection profile for the current context. +// The connection profile will be added to VS Code's user settings to work +// with the MSSQL extension. +func (c *VSCode) run() { + endpoint, user := config.CurrentContext() + + // If the context has a local container, ensure it is running, otherwise bail out + if endpoint.ContainerDetails != nil { + c.ensureContainerIsRunning(endpoint) + } + + // Create or update connection profile in VS Code settings + c.createConnectionProfile(endpoint, user) + + // Launch VS Code + c.launchVSCode() +} + +func (c *VSCode) ensureContainerIsRunning(endpoint sqlconfig.Endpoint) { + output := c.Output() + controller := container.NewController() + if !controller.ContainerRunning(endpoint.Id) { + output.FatalWithHintExamples([][]string{ + {localizer.Sprintf("To start the container"), localizer.Sprintf("sqlcmd start")}, + }, localizer.Sprintf("Container is not running")) + } +} + +// launchVSCode launches Visual Studio Code +func (c *VSCode) launchVSCode() { + output := c.Output() + + tool := tools.NewTool("vscode") + if !tool.IsInstalled() { + output.Fatal(tool.HowToInstall()) + } + + c.displayPreLaunchInfo() + + // Just open VS Code, the connection profile is already configured + _, err := tool.Run([]string{}) + c.CheckErr(err) +} + +// createConnectionProfile creates or updates a connection profile in VS Code's user settings +func (c *VSCode) createConnectionProfile(endpoint sqlconfig.Endpoint, user *sqlconfig.User) { + output := c.Output() + + settingsPath := c.getVSCodeSettingsPath() + + // Ensure the directory exists + dir := filepath.Dir(settingsPath) + if err := os.MkdirAll(dir, 0755); err != nil { + output.FatalWithHintExamples([][]string{ + {localizer.Sprintf("Error"), err.Error()}, + }, localizer.Sprintf("Failed to create VS Code settings directory")) + } + + // Read existing settings or create new + settings := c.readSettings(settingsPath) + + // Create connection profile + profile := c.createProfile(endpoint, user) + + // Add or update the connection profile + connections := c.getConnectionsArray(settings) + connections = c.updateOrAddProfile(connections, profile) + settings["mssql.connections"] = connections + + // Write settings back + c.writeSettings(settingsPath, settings) + + output.Info(localizer.Sprintf("Connection profile created in VS Code settings")) +} + +func (c *VSCode) readSettings(path string) map[string]interface{} { + settings := make(map[string]interface{}) + + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return settings + } + output := c.Output() + output.FatalWithHintExamples([][]string{ + {localizer.Sprintf("Error"), err.Error()}, + }, localizer.Sprintf("Failed to read VS Code settings")) + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &settings); err != nil { + output := c.Output() + output.FatalWithHintExamples([][]string{ + {localizer.Sprintf("Error"), err.Error()}, + }, localizer.Sprintf("Failed to parse VS Code settings")) + } + } + + return settings +} + +func (c *VSCode) writeSettings(path string, settings map[string]interface{}) { + output := c.Output() + + data, err := json.MarshalIndent(settings, "", " ") + if err != nil { + output.FatalWithHintExamples([][]string{ + {localizer.Sprintf("Error"), err.Error()}, + }, localizer.Sprintf("Failed to encode VS Code settings")) + } + + if err := os.WriteFile(path, data, 0644); err != nil { + output.FatalWithHintExamples([][]string{ + {localizer.Sprintf("Error"), err.Error()}, + }, localizer.Sprintf("Failed to write VS Code settings")) + } +} + +func (c *VSCode) getConnectionsArray(settings map[string]interface{}) []interface{} { + connections := []interface{}{} + if existing, ok := settings["mssql.connections"]; ok { + if arr, ok := existing.([]interface{}); ok { + connections = arr + } + } + return connections +} + +func (c *VSCode) createProfile(endpoint sqlconfig.Endpoint, user *sqlconfig.User) map[string]interface{} { + profile := map[string]interface{}{ + "server": fmt.Sprintf("%s,%d", endpoint.Address, endpoint.Port), + "profileName": fmt.Sprintf("sqlcmd-%s", config.CurrentContextName()), + // Set encrypt to "Optional" for compatibility with local development containers + // which often use self-signed certificates. Users can modify this in VS Code settings + // to "Mandatory" or "Strict" for production connections. + "encrypt": "Optional", + } + + if user != nil && user.AuthenticationType == "basic" { + profile["authenticationType"] = "SqlLogin" + profile["user"] = user.BasicAuth.Username + // Note: We don't store the password in settings, VS Code will prompt for it + // or user can configure it through the extension + } else { + if runtime.GOOS == "windows" { + profile["authenticationType"] = "Integrated" + } else { + profile["authenticationType"] = "SqlLogin" + } + } + + return profile +} + +func (c *VSCode) updateOrAddProfile(connections []interface{}, newProfile map[string]interface{}) []interface{} { + profileName := newProfile["profileName"].(string) + + // Check if profile with same name exists and update it + for i, conn := range connections { + if connMap, ok := conn.(map[string]interface{}); ok { + if name, ok := connMap["profileName"].(string); ok && name == profileName { + connections[i] = newProfile + return connections + } + } + } + + // Add new profile + return append(connections, newProfile) +} + +func (c *VSCode) getVSCodeSettingsPath() string { + var stableDir string + var insidersDir string + + switch runtime.GOOS { + case "windows": + base := os.Getenv("APPDATA") + stableDir = filepath.Join(base, "Code", "User") + insidersDir = filepath.Join(base, "Code - Insiders", "User") + case "darwin": + base := filepath.Join(os.Getenv("HOME"), "Library", "Application Support") + stableDir = filepath.Join(base, "Code", "User") + insidersDir = filepath.Join(base, "Code - Insiders", "User") + default: // linux and others + base := filepath.Join(os.Getenv("HOME"), ".config") + stableDir = filepath.Join(base, "Code", "User") + insidersDir = filepath.Join(base, "Code - Insiders", "User") + } + + // Prefer VS Code Insiders settings if the directory exists, since the tool + // searches for and launches Insiders first. Fall back to stable Code. + configDir := stableDir + if info, err := os.Stat(insidersDir); err == nil && info.IsDir() { + configDir = insidersDir + } + + return filepath.Join(configDir, "settings.json") +} diff --git a/cmd/modern/root/open/vscode_darwin.go b/cmd/modern/root/open/vscode_darwin.go new file mode 100644 index 00000000..6c4b2a71 --- /dev/null +++ b/cmd/modern/root/open/vscode_darwin.go @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/config" + "github.com/microsoft/go-sqlcmd/internal/localizer" +) + +// Type VSCode is used to implement the "open vscode" which launches Visual +// Studio Code and establishes a connection to the SQL Server for the current +// context +type VSCode struct { + cmdparser.Cmd +} + +func (c *VSCode) displayPreLaunchInfo() { + output := c.Output() + + output.Info(localizer.Sprintf("Opening VS Code...")) + output.Info(localizer.Sprintf("After VS Code opens:")) + output.Info(localizer.Sprintf("1. Install the MSSQL extension if not already installed (Cmd+Shift+X, search 'SQL Server (mssql)')")) + output.Info(localizer.Sprintf("2. Open the Command Palette (F1 or Cmd+Shift+P)")) + output.Info(localizer.Sprintf("3. Type 'MS SQL: Connect' and select the 'sqlcmd-%s' profile", config.CurrentContextName())) +} diff --git a/cmd/modern/root/open/vscode_linux.go b/cmd/modern/root/open/vscode_linux.go new file mode 100644 index 00000000..ad5538bc --- /dev/null +++ b/cmd/modern/root/open/vscode_linux.go @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/config" + "github.com/microsoft/go-sqlcmd/internal/localizer" +) + +// Type VSCode is used to implement the "open vscode" which launches Visual +// Studio Code and establishes a connection to the SQL Server for the current +// context +type VSCode struct { + cmdparser.Cmd +} + +func (c *VSCode) displayPreLaunchInfo() { + output := c.Output() + + output.Info(localizer.Sprintf("Opening VS Code...")) + output.Info(localizer.Sprintf("After VS Code opens:")) + output.Info(localizer.Sprintf("1. Install the MSSQL extension if not already installed (Ctrl+Shift+X, search 'SQL Server (mssql)')")) + output.Info(localizer.Sprintf("2. Open the Command Palette (F1 or Ctrl+Shift+P)")) + output.Info(localizer.Sprintf("3. Type 'MS SQL: Connect' and select the 'sqlcmd-%s' profile", config.CurrentContextName())) +} diff --git a/cmd/modern/root/open/vscode_test.go b/cmd/modern/root/open/vscode_test.go new file mode 100644 index 00000000..e0609a67 --- /dev/null +++ b/cmd/modern/root/open/vscode_test.go @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig" + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/config" + "runtime" + "testing" +) + +// TestVSCode runs a sanity test of `sqlcmd open vscode` +func TestVSCode(t *testing.T) { + if runtime.GOOS == "linux" { + // Skip on Linux because the tools factory initializes all tools including ADS, + // and ADS's searchLocations() panics on Linux (not implemented). + // This is a pre-existing issue with the test infrastructure, not specific to VSCode. + t.Skip("Skipping on Linux due to ADS tool initialization issue in tools factory") + } + + cmdparser.TestSetup(t) + config.AddEndpoint(sqlconfig.Endpoint{ + AssetDetails: nil, + EndpointDetails: sqlconfig.EndpointDetails{}, + Name: "endpoint", + }) + config.AddContext(sqlconfig.Context{ + ContextDetails: sqlconfig.ContextDetails{ + Endpoint: "endpoint", + User: nil, + }, + Name: "context", + }) + config.SetCurrentContextName("context") + + cmdparser.TestCmd[*VSCode]() +} diff --git a/cmd/modern/root/open/vscode_windows.go b/cmd/modern/root/open/vscode_windows.go new file mode 100644 index 00000000..c6307a80 --- /dev/null +++ b/cmd/modern/root/open/vscode_windows.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package open + +import ( + "github.com/microsoft/go-sqlcmd/internal/cmdparser" + "github.com/microsoft/go-sqlcmd/internal/config" + "github.com/microsoft/go-sqlcmd/internal/localizer" +) + +// Type Vscode is used to implement the "open vscode" which launches Visual +// Studio Code and establishes a connection to the SQL Server for the current +// context +type Vscode struct { + cmdparser.Cmd +} + +// On Windows, show info message before launching +func (c *Vscode) displayPreLaunchInfo() { + output := c.Output() + + output.Info(localizer.Sprintf("Opening VS Code...")) + output.Info(localizer.Sprintf("After VS Code opens:")) + output.Info(localizer.Sprintf("1. Install the MSSQL extension if not already installed (Ctrl+Shift+X, search 'SQL Server (mssql)')")) + output.Info(localizer.Sprintf("2. Open the Command Palette (F1 or Ctrl+Shift+P)")) + output.Info(localizer.Sprintf("3. Type 'MS SQL: Connect' and select the 'sqlcmd-%s' profile", config.CurrentContextName())) +} diff --git a/internal/tools/tool/ssms.go b/internal/tools/tool/ssms.go new file mode 100644 index 00000000..df53afe1 --- /dev/null +++ b/internal/tools/tool/ssms.go @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +import ( + "github.com/microsoft/go-sqlcmd/internal/io/file" + "github.com/microsoft/go-sqlcmd/internal/test" +) + +type SSMS struct { + tool +} + +func (t *SSMS) Init() { + t.SetToolDescription(Description{ + Name: "ssms", + Purpose: "SQL Server Management Studio (SSMS) is an integrated environment for managing SQL Server infrastructure.", + InstallText: t.installText()}) + + for _, location := range t.searchLocations() { + if file.Exists(location) { + t.SetExePathAndName(location) + break + } + } +} + +func (t *SSMS) Run(args []string) (int, error) { + if !test.IsRunningInTestExecutor() { + return t.tool.Run(args) + } + return 0, nil +} diff --git a/internal/tools/tool/ssms_darwin.go b/internal/tools/tool/ssms_darwin.go new file mode 100644 index 00000000..9bb54d34 --- /dev/null +++ b/internal/tools/tool/ssms_darwin.go @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +func (t *SSMS) searchLocations() []string { + // SSMS is not available on macOS + return []string{} +} + +func (t *SSMS) installText() string { + return `SQL Server Management Studio (SSMS) is only available on Windows. + +For macOS, please use: +- Visual Studio Code with the MSSQL extension: sqlcmd open vscode +- Azure Data Studio: sqlcmd open ads` +} diff --git a/internal/tools/tool/ssms_linux.go b/internal/tools/tool/ssms_linux.go new file mode 100644 index 00000000..b1822d69 --- /dev/null +++ b/internal/tools/tool/ssms_linux.go @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +func (t *SSMS) searchLocations() []string { + // SSMS is not available on Linux + return []string{} +} + +func (t *SSMS) installText() string { + return `SQL Server Management Studio (SSMS) is only available on Windows. + +For Linux, please use: +- Visual Studio Code with the MSSQL extension: sqlcmd open vscode +- Azure Data Studio: sqlcmd open ads` +} diff --git a/internal/tools/tool/ssms_test.go b/internal/tools/tool/ssms_test.go new file mode 100644 index 00000000..a60343aa --- /dev/null +++ b/internal/tools/tool/ssms_test.go @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +import "testing" + +func TestSSMS(t *testing.T) { + tool := SSMS{} + tool.Init() + + if tool.Name() != "ssms" { + t.Errorf("Expected name to be 'ssms', got %s", tool.Name()) + } +} diff --git a/internal/tools/tool/ssms_windows.go b/internal/tools/tool/ssms_windows.go new file mode 100644 index 00000000..eadbdf8d --- /dev/null +++ b/internal/tools/tool/ssms_windows.go @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +import ( + "os" + "path/filepath" +) + +// Search in this order - newer versions first +// +// SSMS 20 +// SSMS 19 +// SSMS 18 +func (t *SSMS) searchLocations() []string { + programFiles := os.Getenv("ProgramFiles") + programFilesX86 := os.Getenv("ProgramFiles(x86)") + + return []string{ + filepath.Join(programFiles, "Microsoft SQL Server Management Studio 20\\Common7\\IDE\\Ssms.exe"), + filepath.Join(programFilesX86, "Microsoft SQL Server Management Studio 20\\Common7\\IDE\\Ssms.exe"), + filepath.Join(programFiles, "Microsoft SQL Server Management Studio 19\\Common7\\IDE\\Ssms.exe"), + filepath.Join(programFilesX86, "Microsoft SQL Server Management Studio 19\\Common7\\IDE\\Ssms.exe"), + filepath.Join(programFiles, "Microsoft SQL Server Management Studio 18\\Common7\\IDE\\Ssms.exe"), + filepath.Join(programFilesX86, "Microsoft SQL Server Management Studio 18\\Common7\\IDE\\Ssms.exe"), + } +} + +func (t *SSMS) installText() string { + return `Download the latest version from: + + https://aka.ms/ssmsfullsetup + +Or visit: + + https://docs.microsoft.com/sql/ssms/download-sql-server-management-studio-ssms + +Note: SSMS is only available on Windows.` +} diff --git a/internal/tools/tool/vscode.go b/internal/tools/tool/vscode.go new file mode 100644 index 00000000..29ca5a08 --- /dev/null +++ b/internal/tools/tool/vscode.go @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +import ( + "github.com/microsoft/go-sqlcmd/internal/io/file" + "github.com/microsoft/go-sqlcmd/internal/test" +) + +type VSCode struct { + tool +} + +func (t *VSCode) Init() { + t.SetToolDescription(Description{ + Name: "vscode", + Purpose: "Visual Studio Code is a code editor with support for database management through the MSSQL extension.", + InstallText: t.installText()}) + + for _, location := range t.searchLocations() { + if file.Exists(location) { + t.SetExePathAndName(location) + break + } + } +} + +func (t *VSCode) Run(args []string) (int, error) { + if !test.IsRunningInTestExecutor() { + return t.tool.Run(args) + } + return 0, nil +} diff --git a/internal/tools/tool/vscode_darwin.go b/internal/tools/tool/vscode_darwin.go new file mode 100644 index 00000000..c3cfb9d9 --- /dev/null +++ b/internal/tools/tool/vscode_darwin.go @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +import ( + "os" + "path/filepath" +) + +func (t *VSCode) searchLocations() []string { + userProfile := os.Getenv("HOME") + + return []string{ + filepath.Join("/", "Applications", "Visual Studio Code - Insiders.app"), + filepath.Join(userProfile, "Downloads", "Visual Studio Code - Insiders.app"), + filepath.Join("/", "Applications", "Visual Studio Code.app"), + filepath.Join(userProfile, "Downloads", "Visual Studio Code.app"), + } +} + +func (t *VSCode) installText() string { + return `Download the latest version from: + + https://code.visualstudio.com/download + +After installation, install the MSSQL extension from: + + https://marketplace.visualstudio.com/items?itemName=ms-mssql.mssql + +Or install it directly in VS Code via Extensions (Cmd+Shift+X) and search for "SQL Server (mssql)"` +} diff --git a/internal/tools/tool/vscode_linux.go b/internal/tools/tool/vscode_linux.go new file mode 100644 index 00000000..498b23d8 --- /dev/null +++ b/internal/tools/tool/vscode_linux.go @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +import ( + "os" + "path/filepath" +) + +func (t *VSCode) searchLocations() []string { + userProfile := os.Getenv("HOME") + + return []string{ + filepath.Join("/", "usr", "bin", "code-insiders"), + filepath.Join("/", "usr", "bin", "code"), + filepath.Join(userProfile, ".local", "bin", "code-insiders"), + filepath.Join(userProfile, ".local", "bin", "code"), + filepath.Join("/", "snap", "bin", "code"), + } +} + +func (t *VSCode) installText() string { + return `Download the latest version from: + + https://code.visualstudio.com/download + +After installation, install the MSSQL extension from: + + https://marketplace.visualstudio.com/items?itemName=ms-mssql.mssql + +Or install it directly in VS Code via Extensions (Ctrl+Shift+X) and search for "SQL Server (mssql)"` +} diff --git a/internal/tools/tool/vscode_test.go b/internal/tools/tool/vscode_test.go new file mode 100644 index 00000000..2c35beeb --- /dev/null +++ b/internal/tools/tool/vscode_test.go @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +import "testing" + +func TestVSCode(t *testing.T) { + tool := VSCode{} + tool.Init() + + if tool.Name() != "vscode" { + t.Errorf("Expected name to be 'vscode', got %s", tool.Name()) + } +} diff --git a/internal/tools/tool/vscode_windows.go b/internal/tools/tool/vscode_windows.go new file mode 100644 index 00000000..83148031 --- /dev/null +++ b/internal/tools/tool/vscode_windows.go @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package tool + +import ( + "os" + "path/filepath" +) + +// Search in this order +// +// User Insiders Install +// System Insiders Install +// User non-Insiders install +// System non-Insiders install +func (t *VSCode) searchLocations() []string { + userProfile := os.Getenv("USERPROFILE") + programFiles := os.Getenv("ProgramFiles") + + return []string{ + filepath.Join(userProfile, "AppData\\Local\\Programs\\Microsoft VS Code Insiders\\Code - Insiders.exe"), + filepath.Join(programFiles, "Microsoft VS Code Insiders\\Code - Insiders.exe"), + filepath.Join(userProfile, "AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe"), + filepath.Join(programFiles, "Microsoft VS Code\\Code.exe"), + } +} + +func (t *VSCode) installText() string { + return `Download the latest version from: + + https://code.visualstudio.com/download + +After installation, install the MSSQL extension from: + + https://marketplace.visualstudio.com/items?itemName=ms-mssql.mssql + +Or install it directly in VS Code via Extensions (Ctrl+Shift+X) and search for "SQL Server (mssql)"` +} diff --git a/internal/tools/tools.go b/internal/tools/tools.go index d60d7fee..cb4431e7 100644 --- a/internal/tools/tools.go +++ b/internal/tools/tools.go @@ -9,4 +9,6 @@ import ( var tools = []tool.Tool{ &tool.AzureDataStudio{}, + &tool.VSCode{}, + &tool.SSMS{}, }