Skip to content

Commit fa27800

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 fa27800

40 files changed

Lines changed: 2665 additions & 21 deletions

.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: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,55 @@ 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+
> **Note:** For remote servers that use password-based (basic) authentication, use `sqlcmd open ads` instead. The `open vscode` command only supports password-based auth for local containers. Passwordless auth (Windows integrated, AAD) works for all contexts.
97+
98+
### Connect using SQL Server Management Studio (Windows)
99+
100+
On Windows, use `sqlcmd open ssms` to open SQL Server Management Studio pre-configured to connect to the current context:
101+
102+
```
103+
sqlcmd open ssms
104+
```
105+
106+
This command will:
107+
1. **Copy the password to clipboard** so you can paste it in the login dialog
108+
2. **Launch SSMS** with the server and username pre-filled
109+
3. You'll be prompted for the password - just paste from clipboard (Ctrl+V)
110+
111+
> **Note:** For remote servers that use password-based (basic) authentication, use `sqlcmd open ads` instead. The `open ssms` command only supports password-based auth for local containers. Passwordless auth (Windows integrated, AAD) works for all contexts.
112+
76113
### The ~/.sqlcmd/sqlconfig file
77114

78115
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_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/clipboard.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
func copyPasswordToClipboard(user *sqlconfig.User, out *output.Output) bool {
15+
if user == nil || user.AuthenticationType != "basic" {
16+
return false
17+
}
18+
19+
_, _, password := config.GetCurrentContextInfo()
20+
21+
if password == "" {
22+
return false
23+
}
24+
25+
err := pal.CopyToClipboard(password)
26+
if err != nil {
27+
out.Warn(localizer.Sprintf("Could not copy password to clipboard: %s", err.Error()))
28+
return false
29+
}
30+
31+
out.Info(localizer.Sprintf("Password copied to clipboard - paste it when prompted, then clear your clipboard"))
32+
return true
33+
}
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+
}

cmd/modern/root/open/jsonc.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package open
5+
6+
// stripJSONC removes comments (// and /* */) and trailing commas from JSONC
7+
// data, producing valid JSON. String literals are preserved as-is.
8+
func stripJSONC(data []byte) []byte {
9+
var result []byte
10+
i := 0
11+
n := len(data)
12+
13+
for i < n {
14+
// String literal: copy verbatim, respecting escape sequences
15+
if data[i] == '"' {
16+
result = append(result, data[i])
17+
i++
18+
for i < n && data[i] != '"' {
19+
if data[i] == '\\' && i+1 < n {
20+
result = append(result, data[i], data[i+1])
21+
i += 2
22+
continue
23+
}
24+
result = append(result, data[i])
25+
i++
26+
}
27+
if i < n {
28+
result = append(result, data[i]) // closing "
29+
i++
30+
}
31+
continue
32+
}
33+
34+
// Line comment: skip to end of line
35+
if i+1 < n && data[i] == '/' && data[i+1] == '/' {
36+
i += 2
37+
for i < n && data[i] != '\n' {
38+
i++
39+
}
40+
continue
41+
}
42+
43+
// Block comment: skip to closing */
44+
if i+1 < n && data[i] == '/' && data[i+1] == '*' {
45+
i += 2
46+
for i+1 < n {
47+
if data[i] == '*' && data[i+1] == '/' {
48+
i += 2
49+
break
50+
}
51+
i++
52+
}
53+
continue
54+
}
55+
56+
result = append(result, data[i])
57+
i++
58+
}
59+
60+
// Second pass: remove trailing commas before ] or }
61+
cleaned := make([]byte, 0, len(result))
62+
for i := 0; i < len(result); i++ {
63+
if result[i] == ',' {
64+
j := i + 1
65+
for j < len(result) && (result[j] == ' ' || result[j] == '\t' || result[j] == '\n' || result[j] == '\r') {
66+
j++
67+
}
68+
if j < len(result) && (result[j] == ']' || result[j] == '}') {
69+
continue // skip trailing comma
70+
}
71+
}
72+
cleaned = append(cleaned, result[i])
73+
}
74+
75+
return cleaned
76+
}

0 commit comments

Comments
 (0)