imcp 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # IMCP
2
2
 
3
- IMCP (Install Model Context Protocol) is a unified MCP management project that provides the ability to list, install, and manage MCP servers based on unified feeds.
3
+ IMCP is a single-line pull ann push experience that makes onboarding MCP servers easy.
4
4
 
5
5
  ## Overview
6
6
 
7
- IMCP is a command-line tool that simplifies the management of Model Context Protocol (MCP) servers. It allows you to:
7
+ IMCP allows you to:
8
8
 
9
- - Discover available MCP servers from configurable feeds
10
- - Install servers with specific configurations
9
+ - Discover available MCP servers
11
10
  - Manage server installations
12
- - Run a local web interface to manage your MCP servers
11
+ - Run a local web interface with simple click experience
12
+ - (in progress) Distribute your own MCP servers to others
13
13
 
14
14
  ## Installation
15
15
 
@@ -75,13 +75,13 @@ Options:
75
75
  Examples:
76
76
  ```bash
77
77
  # Install a server
78
- imcp install --category ai-coder-tools --name github-tools
78
+ imcp install --category ai-coder-tools --name git-tools
79
79
 
80
80
  # Install with specific client targets
81
- imcp install --category ai-coder-tools --name github-tools --clients "MSRooCode;GithubCopilot"
81
+ imcp install --category ai-coder-tools --name git-tools --clients "MSRooCode;GithubCopilot"
82
82
 
83
83
  # Install with environment variables
84
- imcp install --category ai-coder-tools --name github-tools --envs "GITHUB_TOKEN=abc123;API_KEY=xyz789"
84
+ imcp install --category ai-coder-tools --name git-tools --envs "GITHUB_TOKEN=abc123;API_KEY=xyz789"
85
85
  ```
86
86
 
87
87
  ### pull
@@ -25,8 +25,26 @@ export const SETTINGS_DIR = (() => {
25
25
  * Local feeds directory path
26
26
  */
27
27
  export const LOCAL_FEEDS_DIR = path.join(SETTINGS_DIR, 'feeds');
28
- const CODE_STRORAGE_DIR = path.join(os.homedir(), 'AppData', 'Roaming', 'Code', 'User');
29
- const CODE_INSIDER_STRORAGE_DIR = path.join(os.homedir(), 'AppData', 'Roaming', 'Code - Insiders', 'User');
28
+ const CODE_STRORAGE_DIR = (() => {
29
+ switch (process.platform) {
30
+ case 'win32':
31
+ return path.join(os.homedir(), 'AppData', 'Roaming', 'Code', 'User');
32
+ case 'darwin': // macOS
33
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User');
34
+ default: // linux
35
+ return path.join(os.homedir(), '.config', 'Code', 'User');
36
+ }
37
+ })();
38
+ const CODE_INSIDER_STRORAGE_DIR = (() => {
39
+ switch (process.platform) {
40
+ case 'win32':
41
+ return path.join(os.homedir(), 'AppData', 'Roaming', 'Code - Insiders', 'User');
42
+ case 'darwin': // macOS
43
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Code - Insiders', 'User');
44
+ default: // linux
45
+ return path.join(os.homedir(), '.config', 'Code - Insiders', 'User');
46
+ }
47
+ })();
30
48
  /**
31
49
  * Supported client configurations.
32
50
  * Key: Client name (e.g., 'vscode')
@@ -8,6 +8,12 @@ export declare class ClientInstaller {
8
8
  constructor(categoryName: string, serverName: string, clients: string[]);
9
9
  private generateOperationId;
10
10
  private getNpmPath;
11
+ /**
12
+ * Check if a command is available on the system
13
+ * @param command The command to check
14
+ * @returns True if the command is available, false otherwise
15
+ */
16
+ private isCommandAvailable;
11
17
  private installClient;
12
18
  private processInstallation;
13
19
  private installClientConfig;
@@ -3,6 +3,7 @@ import { SUPPORTED_CLIENTS } from '../constants.js';
3
3
  import { resolveNpmModulePath, readJsonFile, writeJsonFile } from '../../utils/clientUtils.js';
4
4
  import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
+ import { Logger } from '../../utils/logger.js';
6
7
  export class ClientInstaller {
7
8
  categoryName;
8
9
  serverName;
@@ -34,6 +35,49 @@ export class ClientInstaller {
34
35
  return 'C:\\Program Files\\nodejs';
35
36
  }
36
37
  }
38
+ /**
39
+ * Check if a command is available on the system
40
+ * @param command The command to check
41
+ * @returns True if the command is available, false otherwise
42
+ */
43
+ async isCommandAvailable(command) {
44
+ const execAsync = promisify(exec);
45
+ try {
46
+ if (process.platform === 'win32') {
47
+ // Windows-specific command check
48
+ await execAsync(`where ${command}`);
49
+ }
50
+ else if (process.platform === 'darwin' && (command === 'code' || command === 'code-insiders')) {
51
+ // macOS-specific VS Code check
52
+ const vscodePath = command === 'code' ?
53
+ '/Applications/Visual Studio Code.app' :
54
+ '/Applications/Visual Studio Code - Insiders.app';
55
+ await execAsync(`test -d "${vscodePath}"`);
56
+ }
57
+ else {
58
+ // Unix-like systems
59
+ await execAsync(`which ${command}`);
60
+ }
61
+ return true;
62
+ }
63
+ catch (error) {
64
+ if (process.platform === 'darwin' && (command === 'code' || command === 'code-insiders')) {
65
+ // Try checking in ~/Applications as well for macOS
66
+ try {
67
+ const homedir = process.env.HOME;
68
+ const vscodePath = command === 'code' ?
69
+ `${homedir}/Applications/Visual Studio Code.app` :
70
+ `${homedir}/Applications/Visual Studio Code - Insiders.app`;
71
+ await execAsync(`test -d "${vscodePath}"`);
72
+ return true;
73
+ }
74
+ catch (error) {
75
+ return false;
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+ }
37
81
  async installClient(clientName, env) {
38
82
  // Check if client is supported
39
83
  if (!SUPPORTED_CLIENTS[clientName]) {
@@ -174,15 +218,29 @@ export class ClientInstaller {
174
218
  async installClientConfig(clientName, env, serverConfig, feedConfig) {
175
219
  try {
176
220
  if (!SUPPORTED_CLIENTS[clientName]) {
221
+ Logger.debug(`Client ${clientName} is not supported.`);
177
222
  return { success: false, message: `Unsupported client: ${clientName}` };
178
223
  }
179
224
  const clientSettings = SUPPORTED_CLIENTS[clientName];
180
- // Determine which setting path to use based on VS Code type (regular or insiders)
181
- const settingPath = process.env.CODE_INSIDERS
182
- ? clientSettings.codeInsiderSettingPath
183
- : clientSettings.codeSettingPath;
184
- if (!settingPath) {
185
- return { success: false, message: `No settings path found for client: ${clientName}` };
225
+ // Get both setting paths for VS Code and VS Code Insiders
226
+ const regularSettingPath = clientSettings.codeSettingPath;
227
+ const insidersSettingPath = clientSettings.codeInsiderSettingPath;
228
+ Logger.debug(`Starting installation of ${this.serverName} for client ${clientName}`);
229
+ Logger.debug(`VS Code settings path configured as: ${regularSettingPath}`);
230
+ Logger.debug(`VS Code Insiders settings path configured as: ${insidersSettingPath}`);
231
+ // Check if VS Code and VS Code Insiders are installed
232
+ const isVSCodeInstalled = await this.isCommandAvailable('code');
233
+ const isVSCodeInsidersInstalled = await this.isCommandAvailable('code-insiders');
234
+ Logger.debug(isVSCodeInstalled ? 'VS Code detected on system' : 'VS Code not detected on system');
235
+ Logger.debug(isVSCodeInsidersInstalled ? 'VS Code Insiders detected on system' : 'VS Code Insiders not detected on system');
236
+ Logger.debug(`VS Code Insiders installed: ${isVSCodeInsidersInstalled}`);
237
+ // If neither is installed, we can't proceed
238
+ if (!isVSCodeInstalled && !isVSCodeInsidersInstalled) {
239
+ Logger.debug('No VS Code installations detected on system. Cannot update settings.');
240
+ return {
241
+ success: false,
242
+ message: `Neither VS Code nor VS Code Insiders are installed on this system. Cannot update settings for client: ${clientName}. Please install VS Code or VS Code Insiders and try again.`
243
+ };
186
244
  }
187
245
  // Clone the installation configuration to make modifications
188
246
  const installConfig = JSON.parse(JSON.stringify(serverConfig.installation));
@@ -203,28 +261,128 @@ export class ClientInstaller {
203
261
  if (env) {
204
262
  Object.assign(installConfig.env, env);
205
263
  }
206
- // Update client-specific settings
264
+ // Keep track of success for both installations
265
+ let regularSuccess = false;
266
+ let insidersSuccess = false;
267
+ let errorMessages = [];
268
+ // Update client-specific settings for both VS Code and VS Code Insiders
207
269
  if (clientName === 'MSRooCode' || clientName === 'Cline') {
208
- await this.updateClineOrMSRooSettings(settingPath, this.serverName, installConfig, clientName);
270
+ // Update VS Code settings if VS Code is installed
271
+ if (isVSCodeInstalled) {
272
+ try {
273
+ Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
274
+ await this.updateClineOrMSRooSettings(regularSettingPath, this.serverName, installConfig, clientName);
275
+ regularSuccess = true;
276
+ Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
277
+ }
278
+ catch (error) {
279
+ const errorMsg = `Error updating VS Code settings: ${error instanceof Error ? error.message : String(error)}`;
280
+ errorMessages.push(errorMsg);
281
+ console.error(errorMsg);
282
+ }
283
+ }
284
+ // Update VS Code Insiders settings if VS Code Insiders is installed
285
+ if (isVSCodeInsidersInstalled) {
286
+ try {
287
+ Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
288
+ await this.updateClineOrMSRooSettings(insidersSettingPath, this.serverName, installConfig, clientName);
289
+ insidersSuccess = true;
290
+ Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
291
+ }
292
+ catch (error) {
293
+ const errorMsg = `Error updating VS Code Insiders settings: ${error instanceof Error ? error.message : String(error)}`;
294
+ errorMessages.push(errorMsg);
295
+ console.error(errorMsg);
296
+ }
297
+ }
209
298
  }
210
299
  else if (clientName === 'GithubCopilot') {
211
- await this.updateGithubCopilotSettings(settingPath, this.serverName, installConfig);
300
+ // Update VS Code settings if VS Code is installed
301
+ if (isVSCodeInstalled) {
302
+ try {
303
+ Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
304
+ await this.updateGithubCopilotSettings(regularSettingPath, this.serverName, installConfig);
305
+ regularSuccess = true;
306
+ Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
307
+ }
308
+ catch (error) {
309
+ const errorMsg = `Error updating VS Code settings: ${error instanceof Error ? error.message : String(error)}`;
310
+ errorMessages.push(errorMsg);
311
+ console.error(errorMsg);
312
+ }
313
+ }
314
+ // Update VS Code Insiders settings if VS Code Insiders is installed
315
+ if (isVSCodeInsidersInstalled) {
316
+ try {
317
+ Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
318
+ await this.updateGithubCopilotSettings(insidersSettingPath, this.serverName, installConfig);
319
+ insidersSuccess = true;
320
+ Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
321
+ }
322
+ catch (error) {
323
+ const errorMsg = `Error updating VS Code Insiders settings: ${error instanceof Error ? error.message : String(error)}`;
324
+ errorMessages.push(errorMsg);
325
+ console.error(errorMsg);
326
+ }
327
+ }
212
328
  }
213
329
  else {
330
+ Logger.debug(`No implementation exists for updating settings for client: ${clientName}`);
214
331
  return {
215
332
  success: false,
216
333
  message: `Client ${clientName} is supported but no implementation exists for updating its settings`
217
334
  };
218
335
  }
336
+ // Determine overall success status and message
337
+ const overallSuccess = regularSuccess || insidersSuccess;
338
+ let message = '';
339
+ if (overallSuccess) {
340
+ const successDetails = [];
341
+ if (regularSuccess)
342
+ successDetails.push('VS Code');
343
+ if (insidersSuccess)
344
+ successDetails.push('VS Code Insiders');
345
+ // Create a more compact success message with specific paths
346
+ const pathDetails = [];
347
+ if (regularSuccess) {
348
+ pathDetails.push(`\n[VS Code]: ${regularSettingPath}`);
349
+ }
350
+ if (insidersSuccess) {
351
+ pathDetails.push(`\n[VS Code Insiders]: ${insidersSettingPath}`);
352
+ }
353
+ message = `Successfully installed ${this.serverName} for client: ${clientName}. ${pathDetails.join(' ')}`;
354
+ // Add partial failure information if applicable
355
+ if (errorMessages.length > 0) {
356
+ message += `\nHowever, some errors occurred:\n${errorMessages.join('\n- ')}`;
357
+ }
358
+ Logger.debug(`Installation complete: ${message}`);
359
+ }
360
+ else {
361
+ // Create a more detailed failure message that includes the detection results
362
+ const detectionInfo = [];
363
+ if (!isVSCodeInstalled)
364
+ detectionInfo.push('VS Code not detected');
365
+ if (!isVSCodeInsidersInstalled)
366
+ detectionInfo.push('VS Code Insiders not detected');
367
+ if (errorMessages.length > 0) {
368
+ message = `Failed to install ${this.serverName} for client: ${clientName}.\nDetection status: [${detectionInfo.join(', ')}].\nErrors:\n- ${errorMessages.join('\n- ')}`;
369
+ }
370
+ else {
371
+ message = `Failed to install ${this.serverName} for client: ${clientName}.\nDetection status: [${detectionInfo.join(', ')}]`;
372
+ }
373
+ console.error(`Installation failed: ${message}`);
374
+ }
219
375
  return {
220
- success: true,
221
- message: `Successfully installed ${this.serverName} for client: ${clientName}`
376
+ success: overallSuccess,
377
+ message: message
222
378
  };
223
379
  }
224
380
  catch (error) {
381
+ const errorMsg = `Error installing client ${clientName}: ${error instanceof Error ? error.message : String(error)}`;
382
+ console.error(errorMsg);
225
383
  return {
226
384
  success: false,
227
- message: `Error installing client ${clientName}: ${error instanceof Error ? error.message : String(error)}`
385
+ message: errorMsg
228
386
  };
229
387
  }
230
388
  }
@@ -252,6 +410,10 @@ export class ClientInstaller {
252
410
  const npmPath = await this.getNpmPath();
253
411
  serverConfig.env['APPDATA'] = npmPath;
254
412
  }
413
+ // Convert backslashes to forward slashes in args paths
414
+ if (serverConfig.args) {
415
+ serverConfig.args = serverConfig.args.map((arg) => typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg);
416
+ }
255
417
  // Add or update the server configuration
256
418
  settings.mcpServers[serverName] = {
257
419
  command: serverConfig.command,
@@ -277,6 +439,10 @@ export class ClientInstaller {
277
439
  if (!settings.mcp.servers) {
278
440
  settings.mcp.servers = {};
279
441
  }
442
+ // Convert backslashes to forward slashes in args paths
443
+ if (installConfig.args) {
444
+ installConfig.args = installConfig.args.map((arg) => typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg);
445
+ }
280
446
  // Add or update the server configuration
281
447
  settings.mcp.servers[serverName] = {
282
448
  command: installConfig.command,
@@ -14,12 +14,6 @@ export declare function extractZipFile(zipPath: string, options: {
14
14
  * @returns The resolved path
15
15
  */
16
16
  export declare function resolveNpmModulePath(pathString: string): string;
17
- /**
18
- * Reads a JSON file and parses its content
19
- * @param filePath Path to the JSON file
20
- * @param createIfNotExist Whether to create the file if it doesn't exist
21
- * @returns The parsed JSON content
22
- */
23
17
  export declare function readJsonFile(filePath: string, createIfNotExist?: boolean): Promise<any>;
24
18
  /**
25
19
  * Writes content to a JSON file
@@ -77,6 +77,7 @@ export function resolveNpmModulePath(pathString) {
77
77
  * @param createIfNotExist Whether to create the file if it doesn't exist
78
78
  * @returns The parsed JSON content
79
79
  */
80
+ import stripJsonComments from 'strip-json-comments';
80
81
  export async function readJsonFile(filePath, createIfNotExist = false) {
81
82
  try {
82
83
  // Ensure directory exists
@@ -84,7 +85,9 @@ export async function readJsonFile(filePath, createIfNotExist = false) {
84
85
  await fs.mkdir(path.dirname(filePath), { recursive: true });
85
86
  }
86
87
  const content = await fs.readFile(filePath, 'utf8');
87
- return JSON.parse(content);
88
+ // Remove comments and trailing commas from JSON content
89
+ const sanitizedContent = stripJsonComments(content).replace(/,(\s*[}\]])/g, '$1');
90
+ return JSON.parse(sanitizedContent);
88
91
  }
89
92
  catch (error) {
90
93
  if (error.code === 'ENOENT' && createIfNotExist) {
@@ -34,6 +34,12 @@ body {
34
34
  min-height: 120px;
35
35
  opacity: 1 !important;
36
36
  box-shadow: 0 0 16px #3498db;
37
+ position: relative;
38
+ padding-top: 24px;
39
+ }
40
+
41
+ #installLoadingModal .modal-close-button {
42
+ z-index: 3200 !important;
37
43
  }
38
44
  /* Loading modal always on top */
39
45
  #installLoadingModal {
@@ -61,8 +67,37 @@ body {
61
67
  transition: all 0.3s ease-out;
62
68
  animation: slideIn 0.3s ease-out;
63
69
  }
64
-
65
70
  /* Close button */
71
+ .modal-close-button {
72
+ position: absolute;
73
+ right: 12px;
74
+ top: 12px;
75
+ width: 32px;
76
+ height: 32px;
77
+ border-radius: 50%;
78
+ background: #ffffff;
79
+ border: 2px solid #3498db;
80
+ color: #3498db;
81
+ font-size: 22px;
82
+ cursor: pointer;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ transition: all 0.2s ease;
87
+ z-index: 10;
88
+ padding: 0;
89
+ line-height: 1;
90
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
91
+ }
92
+
93
+ .modal-close-button:hover {
94
+ background-color: #3498db;
95
+ color: #ffffff;
96
+ border-color: #3498db;
97
+ transform: scale(1.05);
98
+ box-shadow: 0 0 8px rgba(52, 152, 219, 0.4);
99
+ }
100
+
66
101
  .close {
67
102
  position: absolute;
68
103
  right: 1.5rem;
@@ -71,20 +106,14 @@ body {
71
106
  font-weight: 600;
72
107
  color: #6b7280;
73
108
  cursor: pointer;
74
- width: 32px;
75
- height: 32px;
76
- display: flex;
77
- align-items: center;
78
- justify-content: center;
79
- border-radius: 50%;
80
109
  transition: color 0.2s ease;
81
110
  }
82
111
 
83
112
  .close:hover {
84
- background-color: #f3f4f6;
85
113
  color: #111827;
86
114
  }
87
115
 
116
+
88
117
  /* Sections layout */
89
118
  .modal-sections {
90
119
  margin-top: 1.5rem;
@@ -240,11 +269,62 @@ body {
240
269
  }
241
270
  }
242
271
 
243
- /* Center loading icon in loading modal */
272
+ /* Center loading icon in loading modal */
244
273
  #installLoadingModal .loading-icon {
245
274
  display: flex;
246
275
  justify-content: center;
247
276
  align-items: center;
248
277
  width: 100%;
249
278
  margin-bottom: 8px;
279
+ }
280
+
281
+ /* Loading message styles */
282
+ #installLoadingMessage {
283
+ font-size: 0.85rem !important;
284
+ line-height: 1.6;
285
+ word-break: break-word;
286
+ max-height: 200px;
287
+ overflow-y: auto;
288
+ scrollbar-width: thin;
289
+ scrollbar-color: #3498db #f0f0f0;
290
+ }
291
+
292
+ #installLoadingMessage::-webkit-scrollbar {
293
+ width: 6px;
294
+ }
295
+
296
+ #installLoadingMessage::-webkit-scrollbar-track {
297
+ background: #f0f0f0;
298
+ border-radius: 3px;
299
+ }
300
+
301
+ #installLoadingMessage::-webkit-scrollbar-thumb {
302
+ background-color: #3498db;
303
+ border-radius: 3px;
304
+ }
305
+
306
+ #installLoadingMessage div {
307
+ margin-bottom: 8px;
308
+ padding: 4px 0;
309
+ }
310
+
311
+ /* Error message styling */
312
+ #installLoadingMessage span[style*="color:red"] {
313
+ color: #f59e0b !important;
314
+ font-weight: 500;
315
+ display: block;
316
+ padding: 4px 8px;
317
+ background: rgba(245, 158, 11, 0.1);
318
+ border-radius: 4px;
319
+ margin: 4px 0;
320
+ }
321
+
322
+ /* File path styling */
323
+ #installLoadingMessage .file-path {
324
+ font-family: 'Consolas', monospace;
325
+ background: #f8fafc;
326
+ padding: 2px 4px;
327
+ border-radius: 3px;
328
+ border: 1px solid #e2e8f0;
329
+ color: #2563eb;
250
330
  }
@@ -112,18 +112,24 @@
112
112
  </div>
113
113
  <!-- Loading Modal -->
114
114
  <div id="installLoadingModal" class="modal" style="display:none; z-index:2000;">
115
- <div class="modal-content" style="text-align:center; pointer-events:auto;">
115
+ <div class="modal-content" style="text-align:center; pointer-events:auto; position:relative;">
116
+ <button class="modal-close-button" onclick="hideInstallLoadingModal()" aria-label="Close">&times;</button>
116
117
  <div style="margin-top:40px;">
117
118
  <div class="loading-icon" style="margin-bottom:16px;">
118
119
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
119
- <circle cx="24" cy="24" r="20" stroke="#888" stroke-width="4" opacity="0.2"/>
120
- <circle cx="24" cy="24" r="20" stroke="#3498db" stroke-width="4" stroke-linecap="round" stroke-dasharray="100" stroke-dashoffset="60">
121
- <animateTransform attributeName="transform" type="rotate" from="0 24 24" to="360 24 24" dur="1s" repeatCount="indefinite"/>
120
+ <circle cx="24" cy="24" r="20" stroke="#888" stroke-width="4" opacity="0.2" />
121
+ <circle cx="24" cy="24" r="20" stroke="#3498db" stroke-width="4" stroke-linecap="round"
122
+ stroke-dasharray="100" stroke-dashoffset="60">
123
+ <animateTransform attributeName="transform" type="rotate" from="0 24 24" to="360 24 24"
124
+ dur="1s" repeatCount="indefinite" />
122
125
  </circle>
123
126
  </svg>
124
127
  </div>
125
- <div class="loading-title" style="font-size:1.5rem; font-weight:bold; margin-bottom:8px;">Installing...</div>
126
- <div id="installLoadingMessage" style="min-height:48px; max-height:160px; overflow:auto; background:#f8f8f8; border-radius:6px; padding:12px; font-size:1rem; color:#444; text-align:left;"></div>
128
+ <div class="loading-title" style="font-size:1.5rem; font-weight:bold; margin-bottom:8px;">Installing...
129
+ </div>
130
+ <div id="installLoadingMessage"
131
+ style="min-height:48px; max-height:160px; overflow:auto; background:#f8f8f8; border-radius:6px; padding:12px; font-size:1rem; color:#444; text-align:left;">
132
+ </div>
127
133
  </div>
128
134
  </div>
129
135
  </div>