imcp 0.0.4 → 0.0.5

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.
Files changed (88) hide show
  1. package/README.md +3 -4
  2. package/dist/cli/commands/install.js +2 -0
  3. package/dist/cli/commands/list.js +1 -0
  4. package/dist/cli/index.js +1 -2
  5. package/dist/core/ConfigurationLoader.d.ts +32 -0
  6. package/dist/core/ConfigurationLoader.js +213 -0
  7. package/dist/core/ConfigurationProvider.d.ts +2 -3
  8. package/dist/core/ConfigurationProvider.js +13 -182
  9. package/dist/core/InstallationService.d.ts +8 -0
  10. package/dist/core/InstallationService.js +124 -96
  11. package/dist/core/RequirementService.d.ts +1 -1
  12. package/dist/core/RequirementService.js +5 -9
  13. package/dist/core/constants.js +14 -1
  14. package/dist/core/installers/BaseInstaller.d.ts +5 -4
  15. package/dist/core/installers/BaseInstaller.js +17 -28
  16. package/dist/core/installers/ClientInstaller.js +134 -43
  17. package/dist/core/installers/CommandInstaller.d.ts +1 -0
  18. package/dist/core/installers/CommandInstaller.js +3 -0
  19. package/dist/core/installers/GeneralInstaller.d.ts +1 -0
  20. package/dist/core/installers/GeneralInstaller.js +3 -0
  21. package/dist/core/installers/InstallerFactory.d.ts +9 -7
  22. package/dist/core/installers/InstallerFactory.js +10 -8
  23. package/dist/core/installers/NpmInstaller.d.ts +1 -0
  24. package/dist/core/installers/NpmInstaller.js +3 -0
  25. package/dist/core/installers/PipInstaller.d.ts +6 -3
  26. package/dist/core/installers/PipInstaller.js +21 -8
  27. package/dist/core/installers/RequirementInstaller.d.ts +4 -3
  28. package/dist/core/installers/clients/ClientInstaller.d.ts +23 -0
  29. package/dist/core/installers/clients/ClientInstaller.js +573 -0
  30. package/dist/core/installers/clients/ExtensionInstaller.d.ts +26 -0
  31. package/dist/core/installers/clients/ExtensionInstaller.js +149 -0
  32. package/dist/core/installers/index.d.ts +8 -6
  33. package/dist/core/installers/index.js +8 -6
  34. package/dist/core/installers/requirements/BaseInstaller.d.ts +59 -0
  35. package/dist/core/installers/requirements/BaseInstaller.js +168 -0
  36. package/dist/core/installers/requirements/CommandInstaller.d.ts +37 -0
  37. package/dist/core/installers/requirements/CommandInstaller.js +173 -0
  38. package/dist/core/installers/requirements/GeneralInstaller.d.ts +33 -0
  39. package/dist/core/installers/requirements/GeneralInstaller.js +86 -0
  40. package/dist/core/installers/requirements/InstallerFactory.d.ts +54 -0
  41. package/dist/core/installers/requirements/InstallerFactory.js +97 -0
  42. package/dist/core/installers/requirements/NpmInstaller.d.ts +26 -0
  43. package/dist/core/installers/requirements/NpmInstaller.js +128 -0
  44. package/dist/core/installers/requirements/PipInstaller.d.ts +28 -0
  45. package/dist/core/installers/requirements/PipInstaller.js +128 -0
  46. package/{src/core/installers/RequirementInstaller.ts → dist/core/installers/requirements/RequirementInstaller.d.ts} +33 -38
  47. package/dist/core/installers/requirements/RequirementInstaller.js +3 -0
  48. package/dist/core/types.d.ts +4 -1
  49. package/dist/services/ServerService.js +1 -1
  50. package/dist/utils/githubUtils.d.ts +11 -0
  51. package/dist/utils/githubUtils.js +88 -0
  52. package/dist/utils/osUtils.d.ts +17 -0
  53. package/dist/utils/osUtils.js +184 -0
  54. package/dist/web/public/css/modal.css +97 -3
  55. package/dist/web/public/index.html +21 -2
  56. package/dist/web/public/js/modal.js +177 -28
  57. package/dist/web/public/js/serverCategoryDetails.js +12 -10
  58. package/dist/web/public/js/serverCategoryList.js +20 -5
  59. package/dist/web/public/modal.html +27 -13
  60. package/package.json +1 -1
  61. package/src/cli/commands/install.ts +4 -2
  62. package/src/cli/commands/list.ts +1 -0
  63. package/src/cli/index.ts +1 -1
  64. package/src/core/ConfigurationLoader.ts +251 -0
  65. package/src/core/ConfigurationProvider.ts +13 -195
  66. package/src/core/InstallationService.ts +140 -106
  67. package/src/core/RequirementService.ts +5 -10
  68. package/src/core/constants.ts +15 -1
  69. package/src/core/installers/{ClientInstaller.ts → clients/ClientInstaller.ts} +157 -51
  70. package/src/core/installers/clients/ExtensionInstaller.ts +162 -0
  71. package/src/core/installers/index.ts +9 -7
  72. package/src/core/installers/{BaseInstaller.ts → requirements/BaseInstaller.ts} +10 -118
  73. package/src/core/installers/{CommandInstaller.ts → requirements/CommandInstaller.ts} +7 -3
  74. package/src/core/installers/{GeneralInstaller.ts → requirements/GeneralInstaller.ts} +6 -2
  75. package/src/core/installers/{InstallerFactory.ts → requirements/InstallerFactory.ts} +11 -9
  76. package/src/core/installers/{NpmInstaller.ts → requirements/NpmInstaller.ts} +7 -4
  77. package/src/core/installers/{PipInstaller.ts → requirements/PipInstaller.ts} +26 -10
  78. package/src/core/installers/requirements/RequirementInstaller.ts +41 -0
  79. package/src/core/types.ts +4 -1
  80. package/src/services/ServerService.ts +1 -1
  81. package/src/utils/githubUtils.ts +103 -0
  82. package/src/utils/osUtils.ts +206 -15
  83. package/src/web/public/css/modal.css +97 -3
  84. package/src/web/public/index.html +21 -2
  85. package/src/web/public/js/modal.js +177 -28
  86. package/src/web/public/js/serverCategoryDetails.js +12 -10
  87. package/src/web/public/js/serverCategoryList.js +20 -5
  88. package/src/web/public/modal.html +27 -13
@@ -0,0 +1,11 @@
1
+ import { RegistryConfig, RequirementConfig } from '../core/types.js';
2
+ /**
3
+ * Helper to handle GitHub release downloads
4
+ * @param requirement The requirement configuration
5
+ * @param registry The GitHub release registry configuration
6
+ * @returns The path to the downloaded file
7
+ */
8
+ export declare function handleGitHubRelease(requirement: RequirementConfig, registry: RegistryConfig['githubRelease']): Promise<{
9
+ resolvedVersion: string;
10
+ resolvedPath: string;
11
+ }>;
@@ -0,0 +1,88 @@
1
+ import { exec } from 'child_process';
2
+ import util from 'util';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { extractZipFile } from './clientUtils.js';
6
+ import { Logger } from './logger.js';
7
+ import { SETTINGS_DIR } from '../core/constants.js';
8
+ const execAsync = util.promisify(exec);
9
+ /**
10
+ * Helper to handle GitHub release downloads
11
+ * @param requirement The requirement configuration
12
+ * @param registry The GitHub release registry configuration
13
+ * @returns The path to the downloaded file
14
+ */
15
+ export async function handleGitHubRelease(requirement, registry) {
16
+ if (!registry) {
17
+ throw new Error('GitHub release registry configuration is required');
18
+ }
19
+ const downloadsDir = path.join(SETTINGS_DIR, 'downloads');
20
+ await fs.mkdir(downloadsDir, { recursive: true });
21
+ const { repository, assetsName, assetName } = registry;
22
+ if (!repository) {
23
+ throw new Error('GitHub repository is required for GitHub release downloads');
24
+ }
25
+ let version = requirement.version;
26
+ let resolvedAssetName = assetName || '';
27
+ let resolvedAssetsName = assetsName || '';
28
+ const { stdout } = await execAsync(`gh release view --repo ${repository} --json tagName --jq .tagName`);
29
+ const latestTag = stdout.trim();
30
+ let latestVersion = latestTag;
31
+ const tagWithVPrefix = latestVersion.startsWith('v');
32
+ if (tagWithVPrefix)
33
+ latestVersion = latestVersion.substring(1); // Remove 'v' prefix if present
34
+ version = version.includes("latest") ? latestVersion : version;
35
+ if (assetsName) {
36
+ resolvedAssetsName = assetsName.replace('${latest}', version).replace('${version}', version);
37
+ }
38
+ if (assetName) {
39
+ resolvedAssetName = assetName.replace('${latest}', version).replace('${version}', version);
40
+ }
41
+ Logger.debug(`Downloading ${requirement.name} from GitHub release ${repository} version ${version}`);
42
+ Logger.debug(`ResolvedAssetsName} ${resolvedAssetName}; ResolvedAsetName} ${resolvedAssetName}`);
43
+ const pattern = resolvedAssetsName ? resolvedAssetsName : resolvedAssetName;
44
+ Logger.debug(`Resolved pattern: ${pattern}`);
45
+ if (!pattern) {
46
+ throw new Error('Either assetsName or assetName must be specified for GitHub release downloads');
47
+ }
48
+ // Download the release asset
49
+ const downloadPath = path.join(downloadsDir, path.basename(pattern));
50
+ if (!await fileExists(downloadPath)) {
51
+ await execAsync(`gh release download ${tagWithVPrefix ? `v${version}` : version} --repo ${repository} --pattern "${pattern}" -O "${downloadPath}"`);
52
+ }
53
+ // Handle zip file extraction if the downloaded file is a zip
54
+ if (downloadPath.endsWith('.zip')) {
55
+ const extractDir = path.join(downloadsDir, path.basename(pattern, '.zip'));
56
+ await fs.mkdir(extractDir, { recursive: true });
57
+ // Extract the zip file
58
+ await extractZipFile(downloadPath, { dir: extractDir });
59
+ let assetPath = '';
60
+ // If resolvedAssetName is specified, look for it in the extracted directory
61
+ if (resolvedAssetName) {
62
+ assetPath = path.join(extractDir, resolvedAssetName);
63
+ try {
64
+ await fs.access(assetPath);
65
+ return { resolvedVersion: version, resolvedPath: assetPath };
66
+ }
67
+ catch (error) {
68
+ throw new Error(`Asset ${resolvedAssetName} not found in extracted directory ${extractDir}`);
69
+ }
70
+ }
71
+ else {
72
+ assetPath = path.join(extractDir, path.basename(pattern, '.zip') + '.tgz');
73
+ }
74
+ // If no specific asset is required, return the extraction directory
75
+ return { resolvedVersion: version, resolvedPath: extractDir };
76
+ }
77
+ return { resolvedVersion: version, resolvedPath: downloadPath };
78
+ }
79
+ async function fileExists(filePath) {
80
+ try {
81
+ await fs.access(filePath);
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ //# sourceMappingURL=githubUtils.js.map
@@ -14,3 +14,20 @@ export declare function isToolInstalled(tool: 'gh' | 'git', retries?: number): P
14
14
  * @returns A promise that resolves when the browser is launched
15
15
  */
16
16
  export declare function openBrowser(url: string): Promise<void>;
17
+ /**
18
+ * Gets the directory containing Python packages (site-packages) based on Python executable path.
19
+ * Handles different Python installations (system, venv, conda) across different OS platforms.
20
+ * @param pythonExecutablePath The full path to the Python executable
21
+ * @returns The site-packages directory path
22
+ */
23
+ export declare function getPythonPackagePath(pythonExecutablePath: string): string;
24
+ /**
25
+ * Finds the directory of the system's default Python installation.
26
+ * @returns The directory path or null if not found.
27
+ */
28
+ export declare function getSystemPythonPackageDirectory(): Promise<string | null>;
29
+ /**
30
+ * Gets the path of the system's default browser.
31
+ * @returns The path to the default browser executable.
32
+ */
33
+ export declare function GetBrowserPath(): Promise<string>;
@@ -3,6 +3,8 @@ import os from 'os';
3
3
  import { exec } from 'child_process';
4
4
  import util from 'util';
5
5
  import { Logger } from './logger.js';
6
+ import fs from 'fs';
7
+ import path from 'path';
6
8
  const execAsync = util.promisify(exec);
7
9
  export function getOSType() {
8
10
  const platform = os.platform();
@@ -232,4 +234,186 @@ export async function openBrowser(url) {
232
234
  // Don't throw the error - just log it and continue
233
235
  }
234
236
  }
237
+ /**
238
+ * Gets the directory containing Python packages (site-packages) based on Python executable path.
239
+ * Handles different Python installations (system, venv, conda) across different OS platforms.
240
+ * @param pythonExecutablePath The full path to the Python executable
241
+ * @returns The site-packages directory path
242
+ */
243
+ export function getPythonPackagePath(pythonExecutablePath) {
244
+ Logger.debug({
245
+ action: 'get_python_package_path',
246
+ pythonExecutablePath
247
+ });
248
+ try {
249
+ const dir = path.dirname(pythonExecutablePath);
250
+ const isWindows = process.platform === 'win32';
251
+ // Handle common Python installation patterns
252
+ if (isWindows) {
253
+ // Windows: Check for Scripts directory (venv) or python.exe location
254
+ if (dir.endsWith('Scripts')) {
255
+ // Virtual environment structure on Windows: <venv>/Scripts/python.exe
256
+ const venvRoot = path.dirname(dir);
257
+ return path.join(venvRoot, 'Lib', 'site-packages');
258
+ }
259
+ else if (dir.toLowerCase().includes('python')) {
260
+ // System Python or Conda on Windows
261
+ return path.join(dir, 'Lib', 'site-packages');
262
+ }
263
+ }
264
+ else {
265
+ // Unix systems (MacOS/Linux)
266
+ if (dir.endsWith('bin')) {
267
+ // Virtual environment structure on Unix: <venv>/bin/python
268
+ const venvRoot = path.dirname(dir);
269
+ // Try to find python version-specific site-packages
270
+ const libDir = path.join(venvRoot, 'lib');
271
+ if (fs.existsSync(libDir)) {
272
+ const pythonDirs = fs.readdirSync(libDir).filter(d => d.startsWith('python'));
273
+ if (pythonDirs.length > 0) {
274
+ // Use the first python directory found
275
+ return path.join(libDir, pythonDirs[0], 'site-packages');
276
+ }
277
+ }
278
+ // Fallback to a generic lib/python3/site-packages if version-specific not found
279
+ return path.join(venvRoot, 'lib', 'python3', 'site-packages');
280
+ }
281
+ else if (dir.toLowerCase().includes('python')) {
282
+ // System Python or Conda on Unix
283
+ const libDir = path.join(dir, 'lib');
284
+ if (fs.existsSync(libDir)) {
285
+ const pythonDirs = fs.readdirSync(libDir).filter(d => d.startsWith('python'));
286
+ if (pythonDirs.length > 0) {
287
+ // Use the first python directory found
288
+ return path.join(libDir, pythonDirs[0], 'site-packages');
289
+ }
290
+ }
291
+ // Fallback to a generic lib/python3/site-packages
292
+ return path.join(dir, 'lib', 'python3', 'site-packages');
293
+ }
294
+ }
295
+ // Default fallback: return the original directory
296
+ Logger.debug('No standard Python directory structure found, using original directory');
297
+ return dir;
298
+ }
299
+ catch (error) {
300
+ Logger.error('Error getting Python package path', {
301
+ pythonExecutablePath,
302
+ error
303
+ });
304
+ return path.dirname(pythonExecutablePath);
305
+ }
306
+ }
307
+ /**
308
+ * Finds the directory of the system's default Python installation.
309
+ * @returns The directory path or null if not found.
310
+ */
311
+ export async function getSystemPythonPackageDirectory() {
312
+ const command = process.platform === 'win32' ? 'where python' : 'which python';
313
+ Logger.debug({
314
+ action: 'get_system_python_package_directory',
315
+ command
316
+ });
317
+ try {
318
+ const { stdout } = await execAsync(command);
319
+ // Use the first path found, trim whitespace
320
+ const pythonPath = stdout.split('\n')[0].trim();
321
+ if (pythonPath) {
322
+ const packagePath = getPythonPackagePath(pythonPath);
323
+ Logger.debug({
324
+ action: 'get_system_python_package_directory_success',
325
+ pythonPath,
326
+ packagePath
327
+ });
328
+ return packagePath;
329
+ }
330
+ Logger.debug('No Python executable found');
331
+ return null;
332
+ }
333
+ catch (error) {
334
+ Logger.debug(`Could not find system python using "${command}": ${error}`);
335
+ return null;
336
+ }
337
+ }
338
+ /**
339
+ * Gets the path of the system's default browser.
340
+ * @returns The path to the default browser executable.
341
+ */
342
+ export async function GetBrowserPath() {
343
+ const osType = getOSType();
344
+ Logger.debug({
345
+ action: 'get_system_browser_path',
346
+ osType
347
+ });
348
+ try {
349
+ switch (osType) {
350
+ case OSType.Windows: {
351
+ // Check for Edge first
352
+ const edgePaths = [
353
+ `${process.env['PROGRAMFILES(X86)']}\\Microsoft\\Edge\\Application\\msedge.exe`,
354
+ `${process.env['PROGRAMFILES']}\\Microsoft\\Edge\\Application\\msedge.exe`
355
+ ];
356
+ for (const path of edgePaths) {
357
+ if (fs.existsSync(path)) {
358
+ return path;
359
+ }
360
+ }
361
+ // Then check for Chrome
362
+ const chromePaths = [
363
+ `${process.env['PROGRAMFILES(X86)']}\\Google\\Chrome\\Application\\chrome.exe`,
364
+ `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`
365
+ ];
366
+ for (const path of chromePaths) {
367
+ if (fs.existsSync(path)) {
368
+ return path;
369
+ }
370
+ }
371
+ break;
372
+ }
373
+ case OSType.MacOS: {
374
+ // Check for Edge first
375
+ const edgePath = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';
376
+ if (fs.existsSync(edgePath)) {
377
+ return edgePath;
378
+ }
379
+ // Then check for Chrome
380
+ const chromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
381
+ if (fs.existsSync(chromePath)) {
382
+ return chromePath;
383
+ }
384
+ // Finally check for Safari
385
+ const safariPath = '/Applications/Safari.app/Contents/MacOS/Safari';
386
+ if (fs.existsSync(safariPath)) {
387
+ return safariPath;
388
+ }
389
+ break;
390
+ }
391
+ case OSType.Linux: {
392
+ // Try Edge first
393
+ try {
394
+ const { stdout: edgePath } = await execAsync('which microsoft-edge');
395
+ if (edgePath.trim()) {
396
+ return edgePath.trim();
397
+ }
398
+ }
399
+ catch { }
400
+ // Then try Chrome or Chromium
401
+ try {
402
+ const { stdout: chromePath } = await execAsync('which google-chrome chromium');
403
+ if (chromePath.trim()) {
404
+ return chromePath.trim();
405
+ }
406
+ }
407
+ catch { }
408
+ break;
409
+ }
410
+ }
411
+ // If no browser found, throw error
412
+ throw new Error('No supported browser found on the system');
413
+ }
414
+ catch (error) {
415
+ Logger.error('Failed to get browser path', error);
416
+ throw error;
417
+ }
418
+ }
235
419
  //# sourceMappingURL=osUtils.js.map
@@ -60,7 +60,7 @@ body {
60
60
  border: none;
61
61
  border-radius: 1rem;
62
62
  width: 90%;
63
- max-width: 550px;
63
+ max-width: 900px;
64
64
  position: relative;
65
65
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
66
66
  transform: translateY(0);
@@ -117,6 +117,9 @@ body {
117
117
  /* Sections layout */
118
118
  .modal-sections {
119
119
  margin-top: 1.5rem;
120
+ display: grid;
121
+ grid-template-columns: 1fr 1fr;
122
+ gap: 1rem;
120
123
  }
121
124
 
122
125
  .section-container {
@@ -135,10 +138,16 @@ body {
135
138
  /* Client grid */
136
139
  .client-grid {
137
140
  display: grid;
138
- grid-template-columns: 1fr;
141
+ grid-template-columns: repeat(2, 1fr);
139
142
  gap: 0.75rem;
140
143
  }
141
144
 
145
+ /* Make sections take full width */
146
+ .section-container:first-child,
147
+ .section-container:nth-child(2) {
148
+ grid-column: 1 / -1;
149
+ }
150
+
142
151
  /* Client item styling */
143
152
  .client-item {
144
153
  display: flex;
@@ -221,10 +230,11 @@ body {
221
230
  /* Environment variables section */
222
231
  #modalEnvInputs input {
223
232
  width: 100%;
224
- padding: 0.75rem 1rem;
233
+ padding: 0.5rem 0.75rem;
225
234
  border: 1px solid #e5e7eb;
226
235
  border-radius: 0.5rem;
227
236
  transition: all 0.2s ease;
237
+ height: 36px;
228
238
  }
229
239
 
230
240
  #modalEnvInputs input:focus {
@@ -233,6 +243,90 @@ body {
233
243
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
234
244
  }
235
245
 
246
+ /* Arguments section styling */
247
+ .args-container {
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: 0.5rem;
251
+ }
252
+
253
+ .arg-input {
254
+ height: 36px !important;
255
+ padding: 0.5rem 0.75rem !important;
256
+ border: 1px solid #e5e7eb;
257
+ border-radius: 0.5rem;
258
+ transition: all 0.2s ease;
259
+ font-family: 'Consolas', 'Monaco', monospace;
260
+ font-size: 0.875rem;
261
+ }
262
+
263
+ .arg-input:focus {
264
+ outline: none;
265
+ border-color: #2563eb;
266
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
267
+ }
268
+
269
+ .add-arg-button {
270
+ align-self: flex-start;
271
+ display: inline-flex;
272
+ align-items: center;
273
+ gap: 0.25rem;
274
+ height: 32px;
275
+ }
276
+
277
+ .add-arg-button i {
278
+ font-size: 1.25rem;
279
+ }
280
+
281
+ .remove-arg-button {
282
+ padding: 0.25rem;
283
+ border-radius: 0.375rem;
284
+ transition: all 0.2s ease;
285
+ }
286
+
287
+ .remove-arg-button:hover {
288
+ background-color: rgba(239, 68, 68, 0.1);
289
+ }
290
+
291
+ .remove-arg-button i {
292
+ font-size: 1.25rem;
293
+ }
294
+
295
+ /* Arguments textarea styling */
296
+ #install_args {
297
+ font-family: 'Consolas', 'Monaco', monospace;
298
+ font-size: 0.875rem;
299
+ line-height: 1.5;
300
+ resize: vertical;
301
+ transition: all 0.2s ease;
302
+ margin-bottom: 0.5rem;
303
+ }
304
+
305
+ #install_args:focus {
306
+ outline: none;
307
+ border-color: #2563eb;
308
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
309
+ }
310
+
311
+ /* Python environment input styling */
312
+ #python_env {
313
+ height: 36px !important;
314
+ width: 100%;
315
+ padding: 0.75rem 1rem;
316
+ border: 1px solid #e5e7eb;
317
+ border-radius: 0.5rem;
318
+ transition: all 0.2s ease;
319
+ font-family: 'Consolas', 'Monaco', monospace;
320
+ font-size: 0.875rem;
321
+ }
322
+
323
+ #python_env:focus {
324
+ outline: none;
325
+ border-color: #2563eb;
326
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
327
+ }
328
+
329
+
236
330
  /* Form buttons */
237
331
  .submit-button {
238
332
  background-color: #2563eb;
@@ -97,6 +97,11 @@
97
97
  <!-- Environment variable inputs will be injected here -->
98
98
  </div>
99
99
 
100
+ <!-- Arguments Section -->
101
+ <div id="modalArguments" class="space-y-4 mb-6">
102
+ <!-- Arguments will be injected here -->
103
+ </div>
104
+
100
105
  <div class="flex justify-end space-x-4 mt-6">
101
106
  <button type="button" onclick="closeModal()"
102
107
  class="px-4 py-2 text-gray-600 hover:text-gray-800 font-medium rounded-md hover:bg-gray-100 transition-colors">
@@ -151,10 +156,24 @@
151
156
  window.uninstallTool = uninstallTool;
152
157
 
153
158
  // Initialize
154
- document.addEventListener('DOMContentLoaded', () => {
159
+ document.addEventListener('DOMContentLoaded', async () => {
155
160
  setupSearch();
156
161
  setupModalOutsideClick();
157
- fetchServerCategories();
162
+
163
+ // Check URL parameters for category
164
+ const urlParams = new URLSearchParams(window.location.search);
165
+ const categoryParam = urlParams.get('category');
166
+
167
+ // If we have a category parameter or last selected category
168
+ const lastSelected = categoryParam || localStorage.getItem('lastSelectedCategory');
169
+
170
+ // First fetch categories
171
+ await fetchServerCategories();
172
+
173
+ // Then show the selected category if it exists
174
+ if (lastSelected) {
175
+ showServerDetails(lastSelected);
176
+ }
158
177
  });
159
178
  </script>
160
179
  <script src="js/modal.js" type="module"></script>