Skip to content

Commit de898f1

Browse files
feat: add sqlcmd open vscode and sqlcmd open ssms commands
Add 'sqlcmd open vscode' and 'sqlcmd open ssms' subcommands that launch VS Code or SSMS pre-configured with the active sqlcmd context's connection. VS Code integration: - Creates/updates mssql.connections in VS Code settings.json - Preserves JSONC comments via surgical byte-level patchJSONCKey - Atomic temp-file-then-rename write to prevent corruption - Copies password to clipboard with security warning SSMS integration: - Launches SSMS with connection parameters via command line - Windows-only with platform stub for Unix Includes clipboard helpers, JSONC parser/patcher, platform abstractions, and comprehensive tests.
1 parent 82b4df4 commit de898f1

43 files changed

Lines changed: 2389 additions & 31 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ linux-s390x/sqlcmd
3636
# Build artifacts in root
3737
/sqlcmd
3838
/sqlcmd_binary
39+
/modern
3940

4041
# certificates used for local testing
4142
*.der

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,51 @@ The Homebrew package manager may be used on Linux and Windows Subsystem for Linu
6161

6262
Use `sqlcmd` to create SQL Server and Azure SQL Edge instances using a local container runtime (e.g. [Docker][] or [Podman][])
6363

64-
### Create SQL Server instance using local container runtime and connect using Azure Data Studio
64+
### Create SQL Server instance using local container runtime
6565

66-
To create a local SQL Server instance with the AdventureWorksLT database restored, query it, and connect to it using Azure Data Studio, run:
66+
To create a local SQL Server instance with the AdventureWorksLT database restored, run:
6767

6868
```
6969
sqlcmd create mssql --accept-eula --using https://aka.ms/AdventureWorksLT.bak
7070
sqlcmd query "SELECT DB_NAME()"
71-
sqlcmd open ads
7271
```
7372

7473
Use `sqlcmd --help` to view all the available sub-commands. Use `sqlcmd -?` to view the original ODBC `sqlcmd` flags.
7574

75+
### Connect using Visual Studio Code
76+
77+
Use `sqlcmd open vscode` to open Visual Studio Code with a connection profile configured for the current context:
78+
79+
```
80+
sqlcmd open vscode
81+
```
82+
83+
This command will:
84+
1. **Create a connection profile** in VS Code's user settings with the current context name
85+
2. **Copy the password to clipboard** so you can paste it when prompted
86+
3. **Launch VS Code** ready to connect
87+
88+
To also install the MSSQL extension (if not already installed), add the `--install-extension` flag:
89+
90+
```
91+
sqlcmd open vscode --install-extension
92+
```
93+
94+
Once VS Code opens, use the MSSQL extension's Object Explorer to connect using the profile. When you connect to the container, VS Code will automatically detect it as a Docker container and provide additional container management features (start/stop/delete) directly from the Object Explorer.
95+
96+
### Connect using SQL Server Management Studio (Windows)
97+
98+
On Windows, use `sqlcmd open ssms` to open SQL Server Management Studio pre-configured to connect to the current context:
99+
100+
```
101+
sqlcmd open ssms
102+
```
103+
104+
This command will:
105+
1. **Copy the password to clipboard** so you can paste it in the login dialog
106+
2. **Launch SSMS** with the server and username pre-filled
107+
3. You'll be prompted for the password - just paste from clipboard (Ctrl+V)
108+
76109
### The ~/.sqlcmd/sqlconfig file
77110

78111
Each time `sqlcmd create` completes, a new context is created (e.g. mssql, mssql2, mssql3 etc.). A context contains the endpoint and user configuration detail. To switch between contexts, run `sqlcmd config use <context-name>`, to view name of the current context, run `sqlcmd config current-context`, to list all contexts, run `sqlcmd config get-contexts`.

cmd/modern/root/open.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@ type Open struct {
1717
func (c *Open) DefineCommand(...cmdparser.CommandOptions) {
1818
options := cmdparser.CommandOptions{
1919
Use: "open",
20-
Short: localizer.Sprintf("Open tools (e.g Azure Data Studio) for current context"),
20+
Short: localizer.Sprintf("Open tools (e.g., Azure Data Studio, VS Code, SSMS) for current context"),
2121
SubCommands: c.SubCommands(),
2222
}
2323

2424
c.Cmd.DefineCommand(options)
2525
}
2626

2727
// SubCommands sets up the sub-commands for `sqlcmd open` such as
28-
// `sqlcmd open ads`
28+
// `sqlcmd open ads`, `sqlcmd open vscode`, and `sqlcmd open ssms`
2929
func (c *Open) SubCommands() []cmdparser.Command {
3030
dependencies := c.Dependencies()
3131

3232
return []cmdparser.Command{
3333
cmdparser.New[*open.Ads](dependencies),
34+
cmdparser.New[*open.VSCode](dependencies),
35+
cmdparser.New[*open.Ssms](dependencies),
3436
}
3537
}

cmd/modern/root/open/ads.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,44 @@ func (c *Ads) DefineCommand(...cmdparser.CommandOptions) {
3737
// specific credential store, e.g. on Windows we use the Windows Credential
3838
// Manager.
3939
func (c *Ads) run() {
40+
output := c.Output()
41+
output.Warn(localizer.Sprintf("Azure Data Studio is being retired. This command will be removed in a future release."))
42+
43+
switch runtime.GOOS {
44+
case "windows":
45+
output.Info(localizer.Sprintf(`Alternatives:
46+
47+
VS Code:
48+
winget install Microsoft.VisualStudioCode
49+
sqlcmd open vscode --install-extension
50+
51+
SSMS:
52+
winget install Microsoft.SQLServerManagementStudio
53+
sqlcmd open ssms
54+
`))
55+
case "darwin":
56+
output.Info(localizer.Sprintf(`Alternatives:
57+
58+
VS Code:
59+
brew install --cask visual-studio-code
60+
sqlcmd open vscode --install-extension
61+
Or download: https://code.visualstudio.com/download
62+
`))
63+
default:
64+
output.Info(localizer.Sprintf(`Alternatives:
65+
66+
VS Code:
67+
snap install code --classic
68+
sqlcmd open vscode --install-extension
69+
Or download: https://code.visualstudio.com/download
70+
`))
71+
}
72+
73+
tool := tools.NewTool("ads")
74+
if !tool.IsInstalled() {
75+
output.Fatal(localizer.Sprintf("Azure Data Studio is not installed."))
76+
}
77+
4078
endpoint, user := config.CurrentContext()
4179

4280
// If the context has a local container, ensure it is running, otherwise bail out
@@ -66,7 +104,6 @@ func (c *Ads) ensureContainerIsRunning(endpoint sqlconfig.Endpoint) {
66104

67105
// launchAds launches the Azure Data Studio using the specified server and username.
68106
func (c *Ads) launchAds(host string, port int, username string) {
69-
output := c.Output()
70107
args := []string{
71108
"-r",
72109
fmt.Sprintf(
@@ -89,9 +126,7 @@ func (c *Ads) launchAds(host string, port int, username string) {
89126
}
90127

91128
tool := tools.NewTool("ads")
92-
if !tool.IsInstalled() {
93-
output.Fatal(tool.HowToInstall())
94-
}
129+
tool.IsInstalled() // precondition for Run; already verified in run()
95130

96131
c.displayPreLaunchInfo()
97132

cmd/modern/root/open/ads_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@
44
package open
55

66
import (
7+
"runtime"
8+
"testing"
9+
710
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
811
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
912
"github.com/microsoft/go-sqlcmd/internal/config"
10-
"runtime"
11-
"testing"
13+
"github.com/microsoft/go-sqlcmd/internal/tools"
1214
)
1315

14-
// TestOpen runs a sanity test of `sqlcmd open`
16+
// TestAds runs a sanity test of `sqlcmd open ads`
1517
func TestAds(t *testing.T) {
1618
if runtime.GOOS != "windows" {
17-
t.Skip("Ads support only on Windows at this time")
19+
t.Skip("ADS support only on Windows at this time")
20+
}
21+
22+
tool := tools.NewTool("ads")
23+
if !tool.IsInstalled() {
24+
t.Skip("Azure Data Studio is not installed")
1825
}
1926

2027
cmdparser.TestSetup(t)

cmd/modern/root/open/ads_windows.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ type Ads struct {
2626
// Ctrl+C here.
2727
func (c *Ads) displayPreLaunchInfo() {
2828
output := c.Output()
29-
30-
output.Info(localizer.Sprintf("Press Ctrl+C to exit this process..."))
29+
output.Info(localizer.Sprintf("Launching Azure Data Studio..."))
3130
}
3231

3332
// persistCredentialForAds stores a SQL password in the Windows Credential Manager
@@ -77,7 +76,12 @@ func (c *Ads) adsKey(instance, database, authType, user string) string {
7776
// the same target name as the current instance's credential.
7877
func (c *Ads) removePreviousCredential() {
7978
credentials, err := credman.EnumerateCredentials("", true)
80-
c.CheckErr(err)
79+
if err != nil {
80+
// ERROR_NOT_FOUND (element not found) is expected when no
81+
// credentials exist yet. Any other error is non-fatal here
82+
// since we're only trying to clean up a previous entry.
83+
return
84+
}
8185

8286
for _, cred := range credentials {
8387
if cred.TargetName == c.credential.TargetName {

cmd/modern/root/open/clipboard.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package open
5+
6+
import (
7+
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
8+
"github.com/microsoft/go-sqlcmd/internal/config"
9+
"github.com/microsoft/go-sqlcmd/internal/localizer"
10+
"github.com/microsoft/go-sqlcmd/internal/output"
11+
"github.com/microsoft/go-sqlcmd/internal/pal"
12+
)
13+
14+
// copyPasswordToClipboard copies the SQL password to the system clipboard.
15+
// The password remains on the clipboard until the user or another application
16+
// clears it; callers rely on the user heeding the "clear your clipboard" message.
17+
func copyPasswordToClipboard(user *sqlconfig.User, out *output.Output) bool {
18+
if user == nil || user.AuthenticationType != "basic" {
19+
return false
20+
}
21+
22+
_, _, password := config.GetCurrentContextInfo()
23+
24+
if password == "" {
25+
return false
26+
}
27+
28+
err := pal.CopyToClipboard(password)
29+
if err != nil {
30+
out.Warn(localizer.Sprintf("Could not copy password to clipboard: %s", err.Error()))
31+
return false
32+
}
33+
34+
out.Info(localizer.Sprintf("Password copied to clipboard - paste it when prompted, then clear your clipboard"))
35+
return true
36+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package open
5+
6+
import (
7+
"runtime"
8+
"testing"
9+
10+
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
11+
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
12+
)
13+
14+
func TestCopyPasswordToClipboardWithNoUser(t *testing.T) {
15+
if runtime.GOOS == "linux" {
16+
t.Skip("Skipping on Linux due to ADS tool initialization issue in tools factory")
17+
}
18+
19+
cmdparser.TestSetup(t)
20+
21+
result := copyPasswordToClipboard(nil, nil)
22+
if result {
23+
t.Error("Expected false when user is nil")
24+
}
25+
}
26+
27+
func TestCopyPasswordToClipboardWithNonBasicAuth(t *testing.T) {
28+
if runtime.GOOS == "linux" {
29+
t.Skip("Skipping on Linux due to ADS tool initialization issue in tools factory")
30+
}
31+
32+
cmdparser.TestSetup(t)
33+
34+
user := &sqlconfig.User{
35+
AuthenticationType: "windows",
36+
Name: "test-user",
37+
}
38+
39+
result := copyPasswordToClipboard(user, nil)
40+
if result {
41+
t.Error("Expected false when auth type is not 'basic'")
42+
}
43+
}

0 commit comments

Comments
 (0)