Skip to content

Commit 7346a77

Browse files
author
Miguel Targa
committed
Global SSH config with workspace merge
1 parent 45247ea commit 7346a77

File tree

4 files changed

+104
-45
lines changed

4 files changed

+104
-45
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to the "ssh-control" extension will be documented in this fi
44

55
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
66

7+
## [1.0.1] - 2025-10-01
8+
9+
### Changed
10+
- **Global Configuration by Default**: SSH config now saves to `~/.ssh-control/ssh-config.json` by default
11+
- Configuration is now shared across all workspaces
12+
- Workspace-specific configs (`ssh-config.json` in project root) are still supported if they exist
13+
- Both configs are merged when workspace config is present (workspace groups prefixed with `[Workspace]`)
14+
- **Config Button Behavior**: Open config button now opens workspace config if it exists, otherwise opens global config
15+
16+
### Fixed
17+
- Removed annoying "SSH Control loaded" notification that appeared on every terminal open
18+
719
## [1.0.0] - 2025-09-26
820

921
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "ssh-control",
33
"displayName": "SSH Control",
44
"description": "Manage SSH connections directly from VSCode.",
5-
"version": "1.0.0",
5+
"version": "1.0.1",
66
"publisher": "migueltarga",
77
"author": {
88
"name": "Miguel Targa",

src/extension.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export function activate(context: vscode.ExtensionContext) {
175175
});
176176

177177
const openConfigCommand = vscode.commands.registerCommand('sshServers.openConfig', async () => {
178-
const configPath = configManager.getConfigFilePath();
178+
const configPath = configManager.getConfigFilePathForEditing();
179179
try {
180180
const document = await vscode.workspace.openTextDocument(configPath);
181181
await vscode.window.showTextDocument(document);
@@ -248,7 +248,6 @@ export function activate(context: vscode.ExtensionContext) {
248248
hostHistoryClearFilterCommand,
249249
runSnippetCommand
250250
);
251-
vscode.window.showInformationMessage(`SSH Control loaded. Config file: ${configManager.getConfigFilePath()}`);
252251
}
253252

254253
export function deactivate() {}

src/sshConfigManager.ts

Lines changed: 90 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,105 @@
11
import * as vscode from 'vscode';
22
import * as fs from 'fs';
33
import * as path from 'path';
4+
import * as os from 'os';
45
import { SSHConfig, SSHGroup, SSHHost } from './types';
56

67
export class SSHConfigManager {
7-
private configPath: string;
8+
private readonly globalConfigPath: string;
9+
private readonly workspaceConfigPath: string | null;
810
private _onDidChangeConfig: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
911
readonly onDidChangeConfig: vscode.Event<void> = this._onDidChangeConfig.event;
1012

1113
constructor() {
12-
this.configPath = this.getConfigPath();
14+
this.globalConfigPath = path.join(os.homedir(), '.ssh-control', 'ssh-config.json');
15+
16+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
17+
this.workspaceConfigPath = workspaceFolder
18+
? path.join(workspaceFolder.uri.fsPath, 'ssh-config.json')
19+
: null;
1320
}
1421

15-
private getConfigPath(): string {
16-
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
17-
if (workspaceFolder) {
18-
return path.join(workspaceFolder.uri.fsPath, 'ssh-config.json');
19-
}
22+
private hasWorkspaceConfig(): boolean {
23+
return this.workspaceConfigPath !== null && fs.existsSync(this.workspaceConfigPath);
24+
}
25+
26+
private getSaveConfigPath(): string {
27+
return this.hasWorkspaceConfig() ? this.workspaceConfigPath! : this.globalConfigPath;
28+
}
2029

21-
const homeDir = require('os').homedir();
22-
return path.join(homeDir, '.ssh-control', 'ssh-config.json');
30+
private getDefaultConfig(): SSHConfig {
31+
return {
32+
groups: [
33+
{
34+
name: "Default",
35+
defaultUser: "root",
36+
defaultPort: 22,
37+
defaultIdentityFile: "",
38+
snippets: [
39+
{
40+
name: "System Status",
41+
command: "uname -a && uptime && df -h"
42+
},
43+
{
44+
name: "Process Monitor",
45+
command: "top -n 1 | head -20"
46+
}
47+
],
48+
hosts: []
49+
}
50+
]
51+
};
2352
}
2453

2554
private async ensureConfigExists(): Promise<void> {
26-
if (!fs.existsSync(this.configPath)) {
27-
const defaultConfig: SSHConfig = {
28-
groups: [
29-
{
30-
name: "Default",
31-
defaultUser: "root",
32-
defaultPort: 22,
33-
defaultIdentityFile: "",
34-
snippets: [
35-
{
36-
name: "System Status",
37-
command: "uname -a && uptime && df -h"
38-
},
39-
{
40-
name: "Process Monitor",
41-
command: "top -n 1 | head -20"
42-
}
43-
],
44-
hosts: []
45-
}
46-
]
47-
};
48-
49-
const configDir = path.dirname(this.configPath);
50-
if (!fs.existsSync(configDir)) {
51-
fs.mkdirSync(configDir, { recursive: true });
55+
const globalConfigDir = path.dirname(this.globalConfigPath);
56+
if (!fs.existsSync(globalConfigDir)) {
57+
fs.mkdirSync(globalConfigDir, { recursive: true });
58+
}
59+
60+
if (!fs.existsSync(this.globalConfigPath) && !this.hasWorkspaceConfig()) {
61+
await this.saveConfig(this.getDefaultConfig());
62+
}
63+
}
64+
65+
private mergeConfigs(globalConfig: SSHConfig, workspaceConfig: SSHConfig): SSHConfig {
66+
const workspaceGroups = workspaceConfig.groups.map(group => ({
67+
...group,
68+
name: `[Workspace] ${group.name}`
69+
}));
70+
71+
return {
72+
groups: [...globalConfig.groups, ...workspaceGroups]
73+
};
74+
}
75+
76+
private loadConfigFile(filePath: string): SSHConfig | null {
77+
try {
78+
if (!fs.existsSync(filePath)) {
79+
return null;
5280
}
53-
54-
await this.saveConfig(defaultConfig);
81+
const configData = fs.readFileSync(filePath, 'utf8');
82+
return this.validateAndNormalizeConfig(JSON.parse(configData));
83+
} catch (error) {
84+
console.error(`Failed to load config from ${filePath}:`, error);
85+
return null;
5586
}
5687
}
5788

5889
async loadConfig(): Promise<SSHConfig> {
5990
await this.ensureConfigExists();
6091

6192
try {
62-
const configData = fs.readFileSync(this.configPath, 'utf8');
63-
const config = JSON.parse(configData);
64-
return this.validateAndNormalizeConfig(config);
93+
const globalConfig = this.loadConfigFile(this.globalConfigPath) || { groups: [] };
94+
95+
if (this.hasWorkspaceConfig()) {
96+
const workspaceConfig = this.loadConfigFile(this.workspaceConfigPath!);
97+
if (workspaceConfig) {
98+
return this.mergeConfigs(globalConfig, workspaceConfig);
99+
}
100+
}
101+
102+
return globalConfig;
65103
} catch (error) {
66104
vscode.window.showErrorMessage(`Failed to load SSH config: ${error}`);
67105
return { groups: [] };
@@ -101,7 +139,8 @@ export class SSHConfigManager {
101139
async saveConfig(config: SSHConfig): Promise<void> {
102140
try {
103141
const configData = JSON.stringify(config, null, 2);
104-
fs.writeFileSync(this.configPath, configData, 'utf8');
142+
const targetPath = this.getSaveConfigPath();
143+
fs.writeFileSync(targetPath, configData, 'utf8');
105144
this._onDidChangeConfig.fire();
106145
} catch (error) {
107146
vscode.window.showErrorMessage(`Failed to save SSH config: ${error}`);
@@ -187,6 +226,15 @@ export class SSHConfigManager {
187226
}
188227

189228
getConfigFilePath(): string {
190-
return this.configPath;
229+
const paths = [];
230+
if (this.hasWorkspaceConfig()) {
231+
paths.push(`Workspace: ${this.workspaceConfigPath}`);
232+
}
233+
paths.push(`Global: ${this.globalConfigPath}`);
234+
return paths.join(' | ');
235+
}
236+
237+
getConfigFilePathForEditing(): string {
238+
return this.getSaveConfigPath();
191239
}
192240
}

0 commit comments

Comments
 (0)