11import * as vscode from 'vscode' ;
22import * as fs from 'fs' ;
33import * as path from 'path' ;
4+ import * as os from 'os' ;
45import { SSHConfig , SSHGroup , SSHHost } from './types' ;
56
67export 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