11import chalk from 'chalk' ;
22import ora from 'ora' ;
3+ import inquirer from 'inquirer' ;
34import { CredentialStorage } from '../core/auth/storage' ;
45import { DeployStackAPI } from '../core/auth/api-client' ;
56import { MCPConfigService } from '../core/mcp' ;
7+ import { ServerRestartService } from './server-restart-service' ;
68import { AuthenticationError } from '../types/auth' ;
9+ import { TeamMCPConfig , MCPServerConfig } from '../types/mcp' ;
710
811export interface RefreshOptions {
912 url ?: string ;
@@ -12,10 +15,12 @@ export interface RefreshOptions {
1215export class RefreshService {
1316 private storage : CredentialStorage ;
1417 private mcpService : MCPConfigService ;
18+ private restartService : ServerRestartService ;
1519
1620 constructor ( ) {
1721 this . storage = new CredentialStorage ( ) ;
1822 this . mcpService = new MCPConfigService ( ) ;
23+ this . restartService = new ServerRestartService ( ) ;
1924 }
2025
2126 /**
@@ -51,25 +56,39 @@ export class RefreshService {
5156 const api = new DeployStackAPI ( credentials , backendUrl ) ;
5257
5358 console . log ( chalk . blue ( `🔄 Refreshing MCP configuration for team: ${ chalk . cyan ( credentials . selectedTeam . name ) } ` ) ) ;
59+
60+ // Step 1: Get current configuration for comparison
61+ const oldConfig = await this . mcpService . getMCPConfig ( credentials . selectedTeam . id ) ;
62+
63+ // Step 2: Download new configuration
5464 spinner = ora ( 'Downloading latest MCP configuration...' ) . start ( ) ;
5565
5666 try {
57- const config = await this . mcpService . downloadAndStoreMCPConfig (
67+ const newConfig = await this . mcpService . downloadAndStoreMCPConfig (
5868 credentials . selectedTeam . id ,
5969 credentials . selectedTeam . name ,
6070 api ,
6171 false
6272 ) ;
6373
64- spinner . succeed ( `MCP configuration refreshed (${ config . servers . length } server${ config . servers . length === 1 ? '' : 's' } )` ) ;
74+ spinner . succeed ( `MCP configuration refreshed (${ newConfig . servers . length } server${ newConfig . servers . length === 1 ? '' : 's' } )` ) ;
6575 console . log ( chalk . green ( '✅ MCP configuration has been refreshed' ) ) ;
6676
77+ // Step 3: Detect changes and prompt for restart if needed
78+ const changes = this . detectConfigurationChanges ( oldConfig , newConfig ) ;
79+
80+ if ( changes . hasChanges ) {
81+ await this . handleConfigurationChanges ( changes ) ;
82+ } else {
83+ console . log ( chalk . gray ( '📋 No configuration changes detected' ) ) ;
84+ }
85+
6786 // Show summary
6887 console . log ( chalk . gray ( `\n📊 Configuration Summary:` ) ) ;
69- console . log ( chalk . gray ( ` Team: ${ config . team_name } ` ) ) ;
70- console . log ( chalk . gray ( ` Installations: ${ config . installations . length } ` ) ) ;
71- console . log ( chalk . gray ( ` Servers: ${ config . servers . length } ` ) ) ;
72- console . log ( chalk . gray ( ` Last Updated: ${ new Date ( config . last_updated ) . toLocaleString ( ) } ` ) ) ;
88+ console . log ( chalk . gray ( ` Team: ${ newConfig . team_name } ` ) ) ;
89+ console . log ( chalk . gray ( ` Installations: ${ newConfig . installations . length } ` ) ) ;
90+ console . log ( chalk . gray ( ` Servers: ${ newConfig . servers . length } ` ) ) ;
91+ console . log ( chalk . gray ( ` Last Updated: ${ new Date ( newConfig . last_updated ) . toLocaleString ( ) } ` ) ) ;
7392
7493 } catch ( error ) {
7594 spinner . fail ( 'Failed to refresh MCP configuration' ) ;
@@ -96,4 +115,166 @@ export class RefreshService {
96115 process . exit ( 1 ) ;
97116 }
98117 }
118+
119+ /**
120+ * Detect changes between old and new MCP configurations
121+ */
122+ private detectConfigurationChanges ( oldConfig : TeamMCPConfig | null , newConfig : TeamMCPConfig ) : {
123+ hasChanges : boolean ;
124+ changes : string [ ] ;
125+ addedServers : string [ ] ;
126+ removedServers : string [ ] ;
127+ modifiedServers : string [ ] ;
128+ } {
129+ const changes : string [ ] = [ ] ;
130+ const addedServers : string [ ] = [ ] ;
131+ const removedServers : string [ ] = [ ] ;
132+ const modifiedServers : string [ ] = [ ] ;
133+
134+ // If no old config, everything is new
135+ if ( ! oldConfig ) {
136+ return {
137+ hasChanges : false , // Don't prompt for restart on first-time config
138+ changes : [ 'Initial configuration downloaded' ] ,
139+ addedServers : newConfig . servers . map ( s => s . installation_name ) ,
140+ removedServers : [ ] ,
141+ modifiedServers : [ ]
142+ } ;
143+ }
144+
145+ // Create maps for easier comparison
146+ const oldServers = new Map ( oldConfig . servers . map ( s => [ s . installation_name , s ] ) ) ;
147+ const newServers = new Map ( newConfig . servers . map ( s => [ s . installation_name , s ] ) ) ;
148+
149+ // Check for added servers
150+ for ( const [ name ] of newServers ) {
151+ if ( ! oldServers . has ( name ) ) {
152+ addedServers . push ( name ) ;
153+ changes . push ( `• ${ chalk . green ( name ) } : Added to team configuration` ) ;
154+ }
155+ }
156+
157+ // Check for removed servers
158+ for ( const [ name ] of oldServers ) {
159+ if ( ! newServers . has ( name ) ) {
160+ removedServers . push ( name ) ;
161+ changes . push ( `• ${ chalk . red ( name ) } : Removed from team configuration` ) ;
162+ }
163+ }
164+
165+ // Check for modified servers
166+ for ( const [ name , newServer ] of newServers ) {
167+ const oldServer = oldServers . get ( name ) ;
168+ if ( oldServer && this . hasServerConfigChanged ( oldServer , newServer ) ) {
169+ modifiedServers . push ( name ) ;
170+ const serverChanges = this . getServerChanges ( oldServer , newServer ) ;
171+ changes . push ( `• ${ chalk . yellow ( name ) } : ${ serverChanges . join ( ', ' ) } ` ) ;
172+ }
173+ }
174+
175+ return {
176+ hasChanges : changes . length > 0 ,
177+ changes,
178+ addedServers,
179+ removedServers,
180+ modifiedServers
181+ } ;
182+ }
183+
184+ /**
185+ * Check if a server configuration has changed
186+ */
187+ private hasServerConfigChanged ( oldServer : MCPServerConfig , newServer : MCPServerConfig ) : boolean {
188+ // Check command and args
189+ if ( oldServer . command !== newServer . command ) return true ;
190+ if ( JSON . stringify ( oldServer . args ) !== JSON . stringify ( newServer . args ) ) return true ;
191+
192+ // Check environment variables
193+ if ( JSON . stringify ( oldServer . env ) !== JSON . stringify ( newServer . env ) ) return true ;
194+
195+ // Check runtime
196+ if ( oldServer . runtime !== newServer . runtime ) return true ;
197+
198+ return false ;
199+ }
200+
201+ /**
202+ * Get specific changes for a server
203+ */
204+ private getServerChanges ( oldServer : MCPServerConfig , newServer : MCPServerConfig ) : string [ ] {
205+ const changes : string [ ] = [ ] ;
206+
207+ if ( oldServer . command !== newServer . command ) {
208+ changes . push ( 'command updated' ) ;
209+ }
210+
211+ if ( JSON . stringify ( oldServer . args ) !== JSON . stringify ( newServer . args ) ) {
212+ changes . push ( 'arguments changed' ) ;
213+ }
214+
215+ if ( JSON . stringify ( oldServer . env ) !== JSON . stringify ( newServer . env ) ) {
216+ changes . push ( 'environment variables updated' ) ;
217+ }
218+
219+ if ( oldServer . runtime !== newServer . runtime ) {
220+ changes . push ( 'runtime changed' ) ;
221+ }
222+
223+ return changes ;
224+ }
225+
226+ /**
227+ * Handle configuration changes with interactive restart prompt
228+ */
229+ private async handleConfigurationChanges ( changeInfo : {
230+ hasChanges : boolean ;
231+ changes : string [ ] ;
232+ addedServers : string [ ] ;
233+ removedServers : string [ ] ;
234+ modifiedServers : string [ ] ;
235+ } ) : Promise < void > {
236+ console . log ( chalk . blue ( '\n🔄 Configuration changes detected:' ) ) ;
237+ changeInfo . changes . forEach ( change => console . log ( ` ${ change } ` ) ) ;
238+
239+ console . log ( chalk . yellow ( '\n⚠️ Gateway restart required for changes to take effect.' ) ) ;
240+
241+ // Check if gateway is running
242+ const isRunning = this . restartService . isServerRunning ( ) ;
243+
244+ if ( ! isRunning ) {
245+ console . log ( chalk . gray ( '💡 Gateway is not currently running. Changes will take effect when you start it.' ) ) ;
246+ return ;
247+ }
248+
249+ // Prompt user for restart
250+ const { shouldRestart } = await inquirer . prompt ( [
251+ {
252+ type : 'confirm' ,
253+ name : 'shouldRestart' ,
254+ message : 'Do you want to restart the DeployStack Gateway now?' ,
255+ default : true
256+ }
257+ ] ) ;
258+
259+ if ( shouldRestart ) {
260+ console . log ( chalk . blue ( '\n🔄 Restarting gateway with updated configuration...' ) ) ;
261+
262+ try {
263+ const result = await this . restartService . restartGatewayServer ( ) ;
264+
265+ if ( result . restarted ) {
266+ console . log ( chalk . green ( '✅ Gateway restarted successfully with new configuration' ) ) ;
267+
268+ if ( result . mcpServersStarted !== undefined ) {
269+ console . log ( chalk . blue ( `🤖 Ready to serve ${ result . mcpServersStarted } MCP server${ result . mcpServersStarted === 1 ? '' : 's' } ` ) ) ;
270+ }
271+ }
272+ } catch ( error ) {
273+ console . log ( chalk . red ( `❌ Failed to restart gateway: ${ error instanceof Error ? error . message : String ( error ) } ` ) ) ;
274+ console . log ( chalk . gray ( '💡 You can restart manually with "deploystack restart"' ) ) ;
275+ }
276+ } else {
277+ console . log ( chalk . gray ( '💡 Configuration updated. Restart gateway manually with "deploystack restart" when ready.' ) ) ;
278+ }
279+ }
99280}
0 commit comments