imcp 0.0.3 → 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 (93) hide show
  1. package/README.md +5 -6
  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 +159 -39
  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/clientUtils.d.ts +0 -6
  51. package/dist/utils/clientUtils.js +3 -2
  52. package/dist/utils/githubUtils.d.ts +11 -0
  53. package/dist/utils/githubUtils.js +88 -0
  54. package/dist/utils/osUtils.d.ts +17 -0
  55. package/dist/utils/osUtils.js +184 -0
  56. package/dist/web/public/css/modal.css +97 -3
  57. package/dist/web/public/index.html +21 -2
  58. package/dist/web/public/js/modal.js +177 -28
  59. package/dist/web/public/js/serverCategoryDetails.js +12 -10
  60. package/dist/web/public/js/serverCategoryList.js +20 -5
  61. package/dist/web/public/modal.html +27 -13
  62. package/dist/web/server.js +1 -1
  63. package/package.json +2 -1
  64. package/src/cli/commands/install.ts +4 -2
  65. package/src/cli/commands/list.ts +1 -0
  66. package/src/cli/index.ts +1 -1
  67. package/src/core/ConfigurationLoader.ts +251 -0
  68. package/src/core/ConfigurationProvider.ts +13 -195
  69. package/src/core/InstallationService.ts +140 -106
  70. package/src/core/RequirementService.ts +5 -10
  71. package/src/core/constants.ts +15 -1
  72. package/src/core/installers/{ClientInstaller.ts → clients/ClientInstaller.ts} +185 -46
  73. package/src/core/installers/clients/ExtensionInstaller.ts +162 -0
  74. package/src/core/installers/index.ts +9 -7
  75. package/src/core/installers/{BaseInstaller.ts → requirements/BaseInstaller.ts} +10 -118
  76. package/src/core/installers/{CommandInstaller.ts → requirements/CommandInstaller.ts} +7 -3
  77. package/src/core/installers/{GeneralInstaller.ts → requirements/GeneralInstaller.ts} +6 -2
  78. package/src/core/installers/{InstallerFactory.ts → requirements/InstallerFactory.ts} +11 -9
  79. package/src/core/installers/{NpmInstaller.ts → requirements/NpmInstaller.ts} +7 -4
  80. package/src/core/installers/{PipInstaller.ts → requirements/PipInstaller.ts} +26 -10
  81. package/src/core/installers/requirements/RequirementInstaller.ts +41 -0
  82. package/src/core/types.ts +4 -1
  83. package/src/services/ServerService.ts +1 -1
  84. package/src/utils/clientUtils.ts +4 -2
  85. package/src/utils/githubUtils.ts +103 -0
  86. package/src/utils/osUtils.ts +206 -15
  87. package/src/web/public/css/modal.css +97 -3
  88. package/src/web/public/index.html +21 -2
  89. package/src/web/public/js/modal.js +177 -28
  90. package/src/web/public/js/serverCategoryDetails.js +12 -10
  91. package/src/web/public/js/serverCategoryList.js +20 -5
  92. package/src/web/public/modal.html +27 -13
  93. package/src/web/server.ts +1 -1
@@ -14,7 +14,7 @@ export function getOSType(): OSType {
14
14
  action: 'get_os_type',
15
15
  platform
16
16
  });
17
-
17
+
18
18
  switch (platform) {
19
19
  case 'win32':
20
20
  return OSType.Windows;
@@ -36,7 +36,7 @@ export async function installCLI(tool: 'gh' | 'git'): Promise<void> {
36
36
  tool,
37
37
  osType
38
38
  });
39
-
39
+
40
40
  try {
41
41
  switch (osType) {
42
42
  case OSType.Windows:
@@ -48,7 +48,7 @@ export async function installCLI(tool: 'gh' | 'git'): Promise<void> {
48
48
  // Refresh PATH environment variable after installation
49
49
  await refreshPathEnv();
50
50
  break;
51
-
51
+
52
52
  case OSType.MacOS:
53
53
  if (tool === 'git') {
54
54
  await execAsync('brew install git');
@@ -60,7 +60,7 @@ export async function installCLI(tool: 'gh' | 'git'): Promise<void> {
60
60
  await execAsync('source ~/.zshrc || source ~/.bash_profile || source ~/.bashrc || true');
61
61
  }
62
62
  break;
63
-
63
+
64
64
  case OSType.Linux:
65
65
  if (tool === 'git') {
66
66
  await execAsync('sudo apt-get update && sudo apt-get install -y git');
@@ -70,14 +70,14 @@ export async function installCLI(tool: 'gh' | 'git'): Promise<void> {
70
70
  // Source bash profile to refresh PATH
71
71
  await execAsync('source ~/.bashrc || source ~/.profile || true');
72
72
  break;
73
-
73
+
74
74
  default:
75
75
  throw new Error(`Unsupported operating system for installing ${tool}`);
76
76
  }
77
-
77
+
78
78
  // Wait a moment for system to register the new binaries
79
79
  await new Promise(resolve => setTimeout(resolve, 1000));
80
-
80
+
81
81
  Logger.debug({
82
82
  action: 'install_cli_success',
83
83
  tool,
@@ -98,6 +98,7 @@ export async function installCLI(tool: 'gh' | 'git'): Promise<void> {
98
98
  * This is necessary because when tools are installed using package managers,
99
99
  * they update the system PATH but the current Node process doesn't see those changes.
100
100
  */
101
+
101
102
  export async function refreshPathEnv(): Promise<void> {
102
103
  const osType = getOSType();
103
104
  Logger.debug({
@@ -115,7 +116,7 @@ export async function refreshPathEnv(): Promise<void> {
115
116
  Logger.debug('Refreshed PATH from Windows registry');
116
117
  }
117
118
  break;
118
-
119
+
119
120
  case OSType.MacOS:
120
121
  case OSType.Linux:
121
122
  // On Unix systems, typical installation locations if PATH isn't updated
@@ -129,7 +130,7 @@ export async function refreshPathEnv(): Promise<void> {
129
130
  '/opt/homebrew/bin', // For M1 Macs
130
131
  `${os.homedir()}/.local/bin`
131
132
  ];
132
-
133
+
133
134
  // Ensure these paths are in process.env.PATH
134
135
  if (process.env.PATH) {
135
136
  const currentPaths = process.env.PATH.split(':');
@@ -157,7 +158,7 @@ export async function isToolInstalled(tool: 'gh' | 'git', retries = 3): Promise<
157
158
  tool,
158
159
  retries
159
160
  });
160
-
161
+
161
162
  // Try to execute tool command
162
163
  try {
163
164
  await execAsync(`${tool} --version`);
@@ -171,17 +172,17 @@ export async function isToolInstalled(tool: 'gh' | 'git', retries = 3): Promise<
171
172
  // If we have retries left, refresh PATH and try again
172
173
  if (retries > 0) {
173
174
  Logger.debug(`${tool} not found, refreshing PATH and retrying...`);
174
-
175
+
175
176
  // Refresh environment PATH variable
176
177
  await refreshPathEnv();
177
-
178
+
178
179
  // Wait a moment before retrying
179
180
  await new Promise(resolve => setTimeout(resolve, 500));
180
-
181
+
181
182
  // Recursive retry with decremented counter
182
183
  return isToolInstalled(tool, retries - 1);
183
184
  }
184
-
185
+
185
186
  // No retries left, tool is not installed
186
187
  Logger.debug({
187
188
  action: 'check_tool_installed_success',
@@ -233,7 +234,7 @@ export async function openBrowser(url: string): Promise<void> {
233
234
  default:
234
235
  throw new Error(`Unsupported operating system for opening browser`);
235
236
  }
236
-
237
+
237
238
  Logger.debug({
238
239
  action: 'open_browser_success',
239
240
  url,
@@ -247,4 +248,194 @@ export async function openBrowser(url: string): Promise<void> {
247
248
  });
248
249
  // Don't throw the error - just log it and continue
249
250
  }
251
+ }
252
+
253
+ /**
254
+ * Gets the directory containing Python packages (site-packages) based on Python executable path.
255
+ * Handles different Python installations (system, venv, conda) across different OS platforms.
256
+ * @param pythonExecutablePath The full path to the Python executable
257
+ * @returns The site-packages directory path
258
+ */
259
+ export function getPythonPackagePath(pythonExecutablePath: string): string {
260
+ Logger.debug({
261
+ action: 'get_python_package_path',
262
+ pythonExecutablePath
263
+ });
264
+
265
+ try {
266
+ const dir = path.dirname(pythonExecutablePath);
267
+ const isWindows = process.platform === 'win32';
268
+
269
+ // Handle common Python installation patterns
270
+ if (isWindows) {
271
+ // Windows: Check for Scripts directory (venv) or python.exe location
272
+ if (dir.endsWith('Scripts')) {
273
+ // Virtual environment structure on Windows: <venv>/Scripts/python.exe
274
+ const venvRoot = path.dirname(dir);
275
+ return path.join(venvRoot, 'Lib', 'site-packages');
276
+ } else if (dir.toLowerCase().includes('python')) {
277
+ // System Python or Conda on Windows
278
+ return path.join(dir, 'Lib', 'site-packages');
279
+ }
280
+ } else {
281
+ // Unix systems (MacOS/Linux)
282
+ if (dir.endsWith('bin')) {
283
+ // Virtual environment structure on Unix: <venv>/bin/python
284
+ const venvRoot = path.dirname(dir);
285
+ // Try to find python version-specific site-packages
286
+ const libDir = path.join(venvRoot, 'lib');
287
+ if (fs.existsSync(libDir)) {
288
+ const pythonDirs = fs.readdirSync(libDir).filter(d => d.startsWith('python'));
289
+ if (pythonDirs.length > 0) {
290
+ // Use the first python directory found
291
+ return path.join(libDir, pythonDirs[0], 'site-packages');
292
+ }
293
+ }
294
+ // Fallback to a generic lib/python3/site-packages if version-specific not found
295
+ return path.join(venvRoot, 'lib', 'python3', 'site-packages');
296
+ } else if (dir.toLowerCase().includes('python')) {
297
+ // System Python or Conda on Unix
298
+ const libDir = path.join(dir, 'lib');
299
+ if (fs.existsSync(libDir)) {
300
+ const pythonDirs = fs.readdirSync(libDir).filter(d => d.startsWith('python'));
301
+ if (pythonDirs.length > 0) {
302
+ // Use the first python directory found
303
+ return path.join(libDir, pythonDirs[0], 'site-packages');
304
+ }
305
+ }
306
+ // Fallback to a generic lib/python3/site-packages
307
+ return path.join(dir, 'lib', 'python3', 'site-packages');
308
+ }
309
+ }
310
+
311
+ // Default fallback: return the original directory
312
+ Logger.debug('No standard Python directory structure found, using original directory');
313
+ return dir;
314
+ } catch (error) {
315
+ Logger.error('Error getting Python package path', {
316
+ pythonExecutablePath,
317
+ error
318
+ });
319
+ return path.dirname(pythonExecutablePath);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Finds the directory of the system's default Python installation.
325
+ * @returns The directory path or null if not found.
326
+ */
327
+ export async function getSystemPythonPackageDirectory(): Promise<string | null> {
328
+ const command = process.platform === 'win32' ? 'where python' : 'which python';
329
+
330
+ Logger.debug({
331
+ action: 'get_system_python_package_directory',
332
+ command
333
+ });
334
+
335
+ try {
336
+ const { stdout } = await execAsync(command);
337
+ // Use the first path found, trim whitespace
338
+ const pythonPath = stdout.split('\n')[0].trim();
339
+ if (pythonPath) {
340
+ const packagePath = getPythonPackagePath(pythonPath);
341
+ Logger.debug({
342
+ action: 'get_system_python_package_directory_success',
343
+ pythonPath,
344
+ packagePath
345
+ });
346
+ return packagePath;
347
+ }
348
+ Logger.debug('No Python executable found');
349
+ return null;
350
+ } catch (error) {
351
+ Logger.debug(`Could not find system python using "${command}": ${error}`);
352
+ return null;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Gets the path of the system's default browser.
358
+ * @returns The path to the default browser executable.
359
+ */
360
+ export async function GetBrowserPath(): Promise<string> {
361
+ const osType = getOSType();
362
+ Logger.debug({
363
+ action: 'get_system_browser_path',
364
+ osType
365
+ });
366
+
367
+ try {
368
+ switch (osType) {
369
+ case OSType.Windows: {
370
+ // Check for Edge first
371
+ const edgePaths = [
372
+ `${process.env['PROGRAMFILES(X86)']}\\Microsoft\\Edge\\Application\\msedge.exe`,
373
+ `${process.env['PROGRAMFILES']}\\Microsoft\\Edge\\Application\\msedge.exe`
374
+ ];
375
+ for (const path of edgePaths) {
376
+ if (fs.existsSync(path)) {
377
+ return path;
378
+ }
379
+ }
380
+
381
+ // Then check for Chrome
382
+ const chromePaths = [
383
+ `${process.env['PROGRAMFILES(X86)']}\\Google\\Chrome\\Application\\chrome.exe`,
384
+ `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`
385
+ ];
386
+ for (const path of chromePaths) {
387
+ if (fs.existsSync(path)) {
388
+ return path;
389
+ }
390
+ }
391
+ break;
392
+ }
393
+
394
+ case OSType.MacOS: {
395
+ // Check for Edge first
396
+ const edgePath = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';
397
+ if (fs.existsSync(edgePath)) {
398
+ return edgePath;
399
+ }
400
+
401
+ // Then check for Chrome
402
+ const chromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
403
+ if (fs.existsSync(chromePath)) {
404
+ return chromePath;
405
+ }
406
+
407
+ // Finally check for Safari
408
+ const safariPath = '/Applications/Safari.app/Contents/MacOS/Safari';
409
+ if (fs.existsSync(safariPath)) {
410
+ return safariPath;
411
+ }
412
+ break;
413
+ }
414
+
415
+ case OSType.Linux: {
416
+ // Try Edge first
417
+ try {
418
+ const { stdout: edgePath } = await execAsync('which microsoft-edge');
419
+ if (edgePath.trim()) {
420
+ return edgePath.trim();
421
+ }
422
+ } catch { }
423
+
424
+ // Then try Chrome or Chromium
425
+ try {
426
+ const { stdout: chromePath } = await execAsync('which google-chrome chromium');
427
+ if (chromePath.trim()) {
428
+ return chromePath.trim();
429
+ }
430
+ } catch { }
431
+ break;
432
+ }
433
+ }
434
+
435
+ // If no browser found, throw error
436
+ throw new Error('No supported browser found on the system');
437
+ } catch (error) {
438
+ Logger.error('Failed to get browser path', error);
439
+ throw error;
440
+ }
250
441
  }
@@ -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>