@imenam/mcp-github 1.1.45

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.
@@ -0,0 +1,996 @@
1
+ <!DOCTYPE html>
2
+ <html lang="fr" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GitHub MCP</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ body {
10
+ background-color: #0f172a;
11
+ color: #f8fafc;
12
+ }
13
+
14
+ /* Layout */
15
+ .app-container {
16
+ display: grid;
17
+ grid-template-columns: 320px 1fr;
18
+ gap: 2rem;
19
+ max-width: 1200px;
20
+ margin: 0 auto;
21
+ }
22
+
23
+ @media (max-width: 768px) {
24
+ .app-container {
25
+ grid-template-columns: 1fr;
26
+ }
27
+ }
28
+
29
+ /* iPhone style slider */
30
+ .switch {
31
+ position: relative;
32
+ display: inline-block;
33
+ width: 44px;
34
+ height: 24px;
35
+ flex-shrink: 0;
36
+ }
37
+
38
+ .switch input {
39
+ opacity: 0;
40
+ width: 0;
41
+ height: 0;
42
+ }
43
+
44
+ .slider {
45
+ position: absolute;
46
+ cursor: pointer;
47
+ top: 0;
48
+ left: 0;
49
+ right: 0;
50
+ bottom: 0;
51
+ background-color: #334155;
52
+ transition: .4s;
53
+ border-radius: 24px;
54
+ }
55
+
56
+ .slider:before {
57
+ position: absolute;
58
+ content: "";
59
+ height: 18px;
60
+ width: 18px;
61
+ left: 3px;
62
+ bottom: 3px;
63
+ background-color: white;
64
+ transition: .4s;
65
+ border-radius: 50%;
66
+ }
67
+
68
+ input:checked + .slider {
69
+ background-color: #2563eb;
70
+ }
71
+
72
+ input:checked + .slider:before {
73
+ transform: translateX(20px);
74
+ }
75
+
76
+ /* Custom dropdown styles */
77
+ .search-results {
78
+ position: absolute;
79
+ top: 100%;
80
+ left: 0;
81
+ right: 0;
82
+ z-index: 50;
83
+ background-color: #0f172a;
84
+ border: 1px solid #334155;
85
+ border-radius: 0.5rem;
86
+ margin-top: 0.25rem;
87
+ max-height: 15rem;
88
+ overflow-y: auto;
89
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
90
+ }
91
+
92
+ .search-result-item {
93
+ padding: 0.5rem 1rem;
94
+ cursor: pointer;
95
+ transition: background-color 0.2s;
96
+ font-size: 0.875rem;
97
+ }
98
+
99
+ .search-result-item:hover {
100
+ background-color: #1e293b;
101
+ color: #60a5fa;
102
+ }
103
+
104
+ .search-result-item.active {
105
+ background-color: #1e293b;
106
+ color: #60a5fa;
107
+ outline: 1px solid #3b82f6;
108
+ }
109
+
110
+ .search-result-item.selected {
111
+ background-color: #2563eb;
112
+ color: white;
113
+ }
114
+
115
+ /* Toasts */
116
+ .toast {
117
+ animation: slideIn 0.3s ease-out forwards, fadeOut 0.3s ease-in forwards 4.7s;
118
+ max-width: 24rem;
119
+ }
120
+
121
+ @keyframes slideIn {
122
+ from { transform: translateX(100%); opacity: 0; }
123
+ to { transform: translateX(0); opacity: 1; }
124
+ }
125
+
126
+ @keyframes fadeOut {
127
+ from { opacity: 1; }
128
+ to { opacity: 0; }
129
+ }
130
+ </style>
131
+ </head>
132
+ <body class="min-h-screen p-8">
133
+ <div class="app-container">
134
+ <!-- Sidebar -->
135
+ <aside class="space-y-6">
136
+ <div class="bg-slate-800 rounded-xl shadow-2xl p-6 border border-slate-700 sticky top-8">
137
+ <h2 class="text-xl font-bold mb-4 text-blue-400 flex items-center">
138
+ <svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
139
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
140
+ </svg>
141
+ Configuration
142
+ </h2>
143
+
144
+ <!-- Configuration Selector -->
145
+ <div class="mb-6 space-y-2">
146
+ <div class="flex gap-2">
147
+ <select id="configSelector" class="flex-1 px-3 py-2 bg-slate-900 border border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none">
148
+ <option value="">-- Sélectionner --</option>
149
+ </select>
150
+ <button type="button" id="newConfigBtn" class="px-3 py-2 bg-green-600 hover:bg-green-700 rounded-lg transition duration-200" title="Nouvelle configuration">
151
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
152
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
153
+ </svg>
154
+ </button>
155
+ <button type="button" id="deleteConfigBtn" class="px-3 py-2 bg-red-600 hover:bg-red-700 disabled:bg-slate-700 disabled:cursor-not-allowed rounded-lg transition duration-200" title="Supprimer la configuration" disabled>
156
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
157
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
158
+ </svg>
159
+ </button>
160
+ </div>
161
+ </div>
162
+
163
+ <div class="border-t border-slate-700 pt-4 mb-4">
164
+ <h3 class="text-sm font-medium text-slate-400 mb-3">Actions</h3>
165
+ </div>
166
+
167
+ <div class="space-y-4">
168
+ <button type="button" onclick="document.getElementById('configForm').requestSubmit()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
169
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
170
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
171
+ </svg>
172
+ <span>Sauvegarder</span>
173
+ </button>
174
+
175
+ <button type="button" id="cloneBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-slate-700 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
176
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
177
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path>
178
+ </svg>
179
+ <span>Cloner le dépôt</span>
180
+ </button>
181
+
182
+ <button type="button" id="restartBtn" class="w-full bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
183
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
184
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
185
+ </svg>
186
+ <span>Redémarrer</span>
187
+ </button>
188
+
189
+ <button type="button" id="deleteWorkingDirBtn" class="w-full bg-orange-600 hover:bg-orange-700 disabled:bg-slate-700 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
190
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
191
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
192
+ </svg>
193
+ <span>Nettoyer Repo</span>
194
+ </button>
195
+
196
+ <div class="border-t border-slate-700 pt-4">
197
+ <button type="button" id="stopBtn" class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
198
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
199
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
200
+ </svg>
201
+ <span>Arrêter</span>
202
+ </button>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </aside>
207
+
208
+ <!-- Main Content -->
209
+ <main class="bg-slate-800 rounded-xl shadow-2xl p-8 border border-slate-700">
210
+ <div class="flex items-center gap-4 mb-6">
211
+ <h1 class="text-3xl font-bold text-blue-400 flex items-center">
212
+ <svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
213
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
214
+ </svg>
215
+ GitHub MCP
216
+ </h1>
217
+ <span id="agentNameBadge" class="hidden px-3 py-1 bg-blue-900/50 text-blue-300 text-sm font-medium rounded-full border border-blue-700"></span>
218
+ </div>
219
+
220
+ <form id="configForm" class="space-y-6">
221
+ <div class="space-y-4" id="inputs">
222
+ <!-- Inputs will be injected here -->
223
+ <div class="flex justify-center p-8">
224
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400"></div>
225
+ </div>
226
+ </div>
227
+ </form>
228
+
229
+ <div class="mt-8 text-sm text-slate-400 border-t border-slate-700 pt-4 italic">
230
+ Note: Le serveur MCP doit être redémarré pour appliquer les changements.
231
+ </div>
232
+ </main>
233
+ </div>
234
+
235
+ <!-- Toast Container -->
236
+ <div id="toast-container" class="fixed bottom-8 right-8 z-50 flex flex-col gap-3"></div>
237
+
238
+ <script>
239
+ // Détection automatique du base path pour fonctionner via proxy
240
+ const pathname = window.location.pathname;
241
+ const BASE_PATH = pathname.includes('.')
242
+ ? pathname.replace(/\/[^/]*$/, '') // /github-mcp/index.html -> /github-mcp
243
+ : pathname.endsWith('/')
244
+ ? pathname.slice(0, -1) // /github-mcp/ -> /github-mcp
245
+ : pathname === '/' ? '' : pathname; // /github-mcp -> /github-mcp, / -> ''
246
+
247
+ // Helper pour construire les URLs d'API
248
+ const api = (endpoint) => `${BASE_PATH}/${endpoint}`.replace(/\/+/g, '/');
249
+
250
+ const form = document.getElementById('configForm');
251
+ const inputsContainer = document.getElementById('inputs');
252
+ const restartBtn = document.getElementById('restartBtn');
253
+ const stopBtn = document.getElementById('stopBtn');
254
+ const deleteWorkingDirBtn = document.getElementById('deleteWorkingDirBtn');
255
+ const cloneBtn = document.getElementById('cloneBtn');
256
+
257
+ const fields = [
258
+ { id: 'TARGET_REPO', label: 'Target Repository', type: 'select', placeholder: 'owner/repo' },
259
+ { id: 'BASE_BRANCH', label: 'Base Branch', type: 'select', placeholder: 'main' },
260
+ { id: 'TARGET_BRANCH', label: 'Target Branch', type: 'select', placeholder: 'feature-branch' },
261
+ { id: 'READ_ONLY', label: 'Mode Lecture Seule', type: 'slider' },
262
+ { id: 'INCLUDE_PUBLIC', label: 'Inclure les dépôts publics', type: 'slider' },
263
+ { id: 'INCLUDE_NOT_OWNED', label: 'Inclure les dépôts dont je ne suis pas propriétaire', type: 'slider' }
264
+ ];
265
+
266
+ // État de l'application
267
+ let currentConfig = {};
268
+ let currentConfigName = '';
269
+ let configNames = [];
270
+ const selectState = {
271
+ repos: [],
272
+ branches: {},
273
+ loadingRepos: false,
274
+ loadingBranches: {}
275
+ };
276
+
277
+ let searchTimeout;
278
+ let activeIndex = -1;
279
+
280
+ // Configuration management
281
+ const configSelector = document.getElementById('configSelector');
282
+ const newConfigBtn = document.getElementById('newConfigBtn');
283
+ const deleteConfigBtn = document.getElementById('deleteConfigBtn');
284
+
285
+ async function fetchConfigs() {
286
+ try {
287
+ const response = await fetch(api('api/configs'));
288
+ if (response.ok) {
289
+ const data = await response.json();
290
+ configNames = data.names || [];
291
+ currentConfigName = data.current || '';
292
+ renderConfigSelector();
293
+ }
294
+ } catch (error) {
295
+ console.warn('Error fetching configs:', error);
296
+ }
297
+ }
298
+
299
+ function renderConfigSelector() {
300
+ configSelector.innerHTML = '<option value="">-- Sélectionner --</option>' +
301
+ configNames.map(name =>
302
+ `<option value="${name}" ${name === currentConfigName ? 'selected' : ''}>${name}</option>`
303
+ ).join('');
304
+
305
+ deleteConfigBtn.disabled = !currentConfigName || configNames.length <= 1;
306
+ }
307
+
308
+ async function activateConfig(name) {
309
+ if (!name) return;
310
+ try {
311
+ const response = await fetch(api(`api/configs/${encodeURIComponent(name)}/activate`), {
312
+ method: 'POST'
313
+ });
314
+ if (response.ok) {
315
+ currentConfigName = name;
316
+ // Reset branches state to force reload for new config
317
+ selectState.branches = {};
318
+ showStatus(`Configuration "${name}" activée`, 'success');
319
+ await fetchConfig();
320
+ } else {
321
+ throw new Error('Erreur lors de l\'activation');
322
+ }
323
+ } catch (error) {
324
+ showStatus(error.message, 'error');
325
+ }
326
+ }
327
+
328
+ async function createNewConfig() {
329
+ const name = prompt('Nom de la nouvelle configuration:');
330
+ if (!name || !name.trim()) return;
331
+
332
+ const trimmedName = name.trim();
333
+ if (configNames.includes(trimmedName)) {
334
+ showStatus('Une configuration avec ce nom existe déjà', 'error');
335
+ return;
336
+ }
337
+
338
+ const newConfig = {
339
+ TARGET_REPO: '',
340
+ TARGET_BRANCH: '',
341
+ BASE_BRANCH: '',
342
+ READ_ONLY: currentConfig.READ_ONLY || false,
343
+ INCLUDE_PUBLIC: currentConfig.INCLUDE_PUBLIC || false,
344
+ INCLUDE_NOT_OWNED: currentConfig.INCLUDE_NOT_OWNED || false
345
+ };
346
+
347
+ try {
348
+ const response = await fetch(api('api/configs'), {
349
+ method: 'POST',
350
+ headers: { 'Content-Type': 'application/json' },
351
+ body: JSON.stringify({ name: trimmedName, config: newConfig })
352
+ });
353
+
354
+ if (response.ok) {
355
+ showStatus(`Configuration "${trimmedName}" créée (basée sur la configuration actuelle)`, 'success');
356
+ await fetchConfigs();
357
+ await activateConfig(trimmedName);
358
+ } else {
359
+ throw new Error('Erreur lors de la création');
360
+ }
361
+ } catch (error) {
362
+ showStatus(error.message, 'error');
363
+ }
364
+ }
365
+
366
+ async function deleteCurrentConfig() {
367
+ if (!currentConfigName) return;
368
+ if (configNames.length <= 1) {
369
+ showStatus('Impossible de supprimer la dernière configuration', 'error');
370
+ return;
371
+ }
372
+
373
+ if (!confirm(`Êtes-vous sûr de vouloir supprimer la configuration "${currentConfigName}" ?`)) {
374
+ return;
375
+ }
376
+
377
+ try {
378
+ const response = await fetch(api(`api/configs/${encodeURIComponent(currentConfigName)}`), {
379
+ method: 'DELETE'
380
+ });
381
+
382
+ if (response.ok) {
383
+ showStatus(`Configuration "${currentConfigName}" supprimée`, 'success');
384
+ await fetchConfigs();
385
+ // Activate first remaining config
386
+ if (configNames.length > 0) {
387
+ await activateConfig(configNames[0]);
388
+ }
389
+ } else {
390
+ throw new Error('Erreur lors de la suppression');
391
+ }
392
+ } catch (error) {
393
+ showStatus(error.message, 'error');
394
+ }
395
+ }
396
+
397
+ configSelector.addEventListener('change', (e) => {
398
+ if (e.target.value) {
399
+ activateConfig(e.target.value);
400
+ }
401
+ });
402
+
403
+ newConfigBtn.addEventListener('click', createNewConfig);
404
+ deleteConfigBtn.addEventListener('click', deleteCurrentConfig);
405
+
406
+ // Fetch repositories
407
+ async function fetchRepositories(searchTerm = '') {
408
+ const loadingIndicator = document.getElementById('REPO_LOADING_INDICATOR');
409
+ if (loadingIndicator) loadingIndicator.classList.remove('hidden');
410
+
411
+ try {
412
+ const url = api(`api/repositories${searchTerm ? `?search=${encodeURIComponent(searchTerm)}` : ''}`);
413
+ const response = await fetch(url);
414
+ if (response.ok) {
415
+ selectState.repos = await response.json();
416
+ renderRepoResults();
417
+ }
418
+ } catch (error) {
419
+ console.warn('Error fetching repositories:', error);
420
+ } finally {
421
+ if (loadingIndicator) loadingIndicator.classList.add('hidden');
422
+ }
423
+ }
424
+
425
+ // Affiche les résultats chargés dans le DOM
426
+ function renderRepoResults() {
427
+ const resultsContainer = document.getElementById('REPO_RESULTS');
428
+ if (!resultsContainer) return;
429
+
430
+ if (selectState.repos.length > 0) {
431
+ const currentRepo = document.getElementById('TARGET_REPO')?.value;
432
+ resultsContainer.innerHTML = selectState.repos.map((repo, index) => `
433
+ <div class="search-result-item ${repo === currentRepo ? 'selected' : ''}" data-index="${index}" onclick="selectRepo('${repo}')">
434
+ ${repo}
435
+ ${repo === currentRepo ? '<span class="float-right text-blue-400">✓</span>' : ''}
436
+ </div>
437
+ `).join('');
438
+ resultsContainer.classList.remove('hidden');
439
+ activeIndex = -1;
440
+ } else {
441
+ resultsContainer.innerHTML = '<div class="p-3 text-slate-500 text-sm">Aucun résultat</div>';
442
+ resultsContainer.classList.remove('hidden');
443
+ }
444
+ }
445
+
446
+ // Sélectionner un repo depuis la liste custom
447
+ window.selectRepo = async function(repo) {
448
+ const searchInput = document.getElementById('REPO_SEARCH');
449
+ const hiddenInput = document.getElementById('TARGET_REPO');
450
+ const resultsContainer = document.getElementById('REPO_RESULTS');
451
+
452
+ if (searchInput) searchInput.value = repo;
453
+ if (hiddenInput) {
454
+ hiddenInput.value = repo;
455
+ currentConfig.TARGET_REPO = repo;
456
+ }
457
+ if (resultsContainer) resultsContainer.classList.add('hidden');
458
+
459
+ // Réinitialiser les branches lors du changement de repository
460
+ currentConfig.BASE_BRANCH = '';
461
+ currentConfig.TARGET_BRANCH = '';
462
+
463
+ // Charger les branches pour ce repo (renderInputs is called inside fetchBranches)
464
+ await fetchBranches(repo);
465
+ };
466
+
467
+ // Fetch branches for a repository
468
+ async function fetchBranches(repo, preserveConfig = false) {
469
+ if (!repo) return [];
470
+
471
+ selectState.loadingBranches[repo] = true;
472
+ // Use renderInputs directly to avoid overwriting currentConfig from DOM
473
+ renderInputs(currentConfig);
474
+
475
+ try {
476
+ const url = api(`api/branches?repo=${encodeURIComponent(repo)}`);
477
+ const response = await fetch(url);
478
+ if (response.ok) {
479
+ selectState.branches[repo] = await response.json();
480
+ }
481
+ } catch (error) {
482
+ console.error('[fetchBranches] Fetch error:', error);
483
+ selectState.branches[repo] = [];
484
+ } finally {
485
+ selectState.loadingBranches[repo] = false;
486
+ // Use renderInputs directly to avoid overwriting currentConfig from DOM
487
+ renderInputs(currentConfig);
488
+ }
489
+ }
490
+
491
+ // Render select field
492
+ function renderSelectField(field, value, config) {
493
+ let options = '<option value="">-- Sélectionner une option --</option>';
494
+
495
+ if (field.id === 'TARGET_REPO') {
496
+ if (selectState.loadingRepos && selectState.repos.length === 0) {
497
+ return `
498
+ <div class="space-y-1">
499
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
500
+ <div id="${field.id}_loading" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg text-slate-400 flex items-center">
501
+ <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
502
+ Chargement des dépôts...
503
+ </div>
504
+ </div>
505
+ `;
506
+ }
507
+
508
+ options += selectState.repos.map(repo =>
509
+ `<option value="${repo}" ${value === repo ? 'selected' : ''}>${repo}</option>`
510
+ ).join('');
511
+
512
+ if (value && !selectState.repos.includes(value)) {
513
+ options += `<option value="${value}" selected>${value}</option>`;
514
+ }
515
+
516
+ return `
517
+ <div class="space-y-1">
518
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
519
+ <div class="relative">
520
+ <div class="relative">
521
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
522
+ <svg class="h-4 w-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
523
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
524
+ </svg>
525
+ </div>
526
+ <input type="text" id="REPO_SEARCH" placeholder="Rechercher un dépôt..." class="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm text-slate-200" autocomplete="off" value="${value || ''}">
527
+ <div id="REPO_LOADING_INDICATOR" class="absolute inset-y-0 right-0 pr-3 flex items-center hidden">
528
+ <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400"></div>
529
+ </div>
530
+ </div>
531
+ <div id="REPO_RESULTS" class="search-results hidden"></div>
532
+ <input type="hidden" id="${field.id}" value="${value || ''}">
533
+ </div>
534
+ </div>
535
+ `;
536
+ } else if (field.id === 'BASE_BRANCH') {
537
+ const currentRepo = config.TARGET_REPO;
538
+ const branches = selectState.branches[currentRepo] || [];
539
+
540
+ if (selectState.loadingBranches[currentRepo]) {
541
+ return `
542
+ <div class="space-y-1">
543
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
544
+ <div id="${field.id}_loading" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg text-slate-400 flex items-center">
545
+ <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
546
+ Chargement des branches...
547
+ </div>
548
+ </div>
549
+ `;
550
+ }
551
+
552
+ // Recréer les options avec l'option par défaut sélectionnée si value est vide
553
+ let branchOptions = `<option value="" ${!value ? 'selected' : ''}>-- Choisir une branche --</option>`;
554
+ branchOptions += branches.map(branch =>
555
+ `<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
556
+ ).join('');
557
+
558
+ return `
559
+ <div class="space-y-1">
560
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
561
+ <select id="${field.id}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100">
562
+ ${branchOptions}
563
+ </select>
564
+ </div>
565
+ `;
566
+ } else if (field.id === 'TARGET_BRANCH') {
567
+ const currentRepo = config.TARGET_REPO;
568
+ const branches = selectState.branches[currentRepo] || [];
569
+
570
+ if (selectState.loadingBranches[currentRepo]) {
571
+ return `
572
+ <div class="space-y-1">
573
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
574
+ <div id="${field.id}_loading" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg text-slate-400 flex items-center">
575
+ <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
576
+ Chargement des branches...
577
+ </div>
578
+ </div>
579
+ `;
580
+ }
581
+
582
+ let selectOptions = '<option value="">-- Créer une nouvelle branche --</option>';
583
+ selectOptions += branches.map(branch =>
584
+ `<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
585
+ ).join('');
586
+
587
+ const isCustom = value && !branches.includes(value);
588
+ const shouldShowCustom = isCustom || value === '';
589
+
590
+ return `
591
+ <div class="space-y-1">
592
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
593
+ <div class="space-y-2">
594
+ <select id="${field.id}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100">
595
+ ${selectOptions}
596
+ </select>
597
+ <input type="text" id="${field.id}_custom" placeholder="Entrez le nom de la nouvelle branche" value="${isCustom ? value : ''}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100 ${shouldShowCustom ? '' : 'hidden'}">
598
+ </div>
599
+ </div>
600
+ `;
601
+ }
602
+
603
+ return `
604
+ <div class="space-y-1">
605
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
606
+ <select id="${field.id}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100">
607
+ ${options}
608
+ </select>
609
+ </div>
610
+ `;
611
+ }
612
+
613
+ // Fetch current config
614
+ async function fetchConfig() {
615
+ try {
616
+ // Fetch configs list first
617
+ await fetchConfigs();
618
+
619
+ const response = await fetch(api('api/config'));
620
+ if (!response.ok) {
621
+ const data = await response.json().catch(() => ({ error: 'Erreur lors du chargement' }));
622
+ throw new Error(data.error || 'Erreur lors du chargement');
623
+ }
624
+ currentConfig = await response.json();
625
+
626
+ // Afficher le nom de l'agent si défini
627
+ const agentBadge = document.getElementById('agentNameBadge');
628
+ if (currentConfig.AGENT_NAME) {
629
+ agentBadge.textContent = currentConfig.AGENT_NAME;
630
+ agentBadge.classList.remove('hidden');
631
+ } else {
632
+ agentBadge.classList.add('hidden');
633
+ }
634
+
635
+ // Le token est désormais côté serveur, on tente de charger les repos directement
636
+ await fetchRepositories();
637
+ if (currentConfig.TARGET_REPO) {
638
+ await fetchBranches(currentConfig.TARGET_REPO);
639
+ }
640
+
641
+ renderInputs(currentConfig);
642
+ } catch (error) {
643
+ showStatus(error.message || 'Erreur lors du chargement de la configuration', 'error');
644
+ }
645
+ }
646
+
647
+ // Render inputs
648
+ function renderInputs(config) {
649
+ // CLONE_DIR en lecture seule (variable d'environnement)
650
+ const cloneDirInfo = config.CLONE_DIR
651
+ ? `<div class="p-3 bg-slate-700/50 rounded-lg flex items-center justify-between">
652
+ <span class="text-sm font-medium text-slate-400">Répertoire de clonage</span>
653
+ <span class="text-sm text-slate-200 font-mono">${config.CLONE_DIR}</span>
654
+ </div>`
655
+ : '';
656
+
657
+ inputsContainer.innerHTML = fields.map(field => {
658
+ if (field.type === 'slider') {
659
+ return `
660
+ <div class="flex items-center justify-between p-3 bg-slate-700/50 rounded-lg">
661
+ <label for="${field.id}" class="text-sm font-medium text-slate-200">${field.label}</label>
662
+ <label class="switch">
663
+ <input type="checkbox" id="${field.id}" ${config[field.id] === 'true' || config[field.id] === true ? 'checked' : ''}>
664
+ <span class="slider"></span>
665
+ </label>
666
+ </div>
667
+ `;
668
+ }
669
+ if (field.type === 'checkbox') {
670
+ return `
671
+ <div class="flex items-center space-x-3 p-3 bg-slate-700/50 rounded-lg">
672
+ <input type="checkbox" id="${field.id}" ${config[field.id] === 'true' || config[field.id] === true ? 'checked' : ''} class="w-5 h-5 text-blue-600 rounded focus:ring-blue-500 bg-slate-900 border-slate-600">
673
+ <label for="${field.id}" class="text-sm font-medium text-slate-200">${field.label}</label>
674
+ </div>
675
+ `;
676
+ }
677
+ if (field.type === 'select') {
678
+ return renderSelectField(field, config[field.id] || '', config);
679
+ }
680
+ return `
681
+ <div class="space-y-1">
682
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
683
+ <input type="${field.type}" id="${field.id}" value="${config[field.id] || ''}" placeholder="${field.placeholder}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100">
684
+ </div>
685
+ `;
686
+ }).join('') + cloneDirInfo;
687
+
688
+ if (deleteWorkingDirBtn) {
689
+ deleteWorkingDirBtn.disabled = !config.REPO_PATH;
690
+ }
691
+
692
+ if (cloneBtn) {
693
+ cloneBtn.disabled = !config.TARGET_REPO || !config.TARGET_BRANCH;
694
+ }
695
+
696
+ setupEventListeners();
697
+ }
698
+
699
+ // Update select UI
700
+ function updateSelectUI() {
701
+ // Mettre à jour currentConfig à partir du DOM pour les éléments qui existent
702
+ fields.forEach(field => {
703
+ const input = document.getElementById(field.id);
704
+ if (input) {
705
+ if (field.type === 'checkbox' || field.type === 'slider') {
706
+ currentConfig[field.id] = input.checked;
707
+ } else if (field.id === 'TARGET_BRANCH') {
708
+ const customInput = document.getElementById('TARGET_BRANCH_custom');
709
+ currentConfig[field.id] = (input.value === '' && customInput && customInput.value) ? customInput.value : input.value;
710
+ } else {
711
+ currentConfig[field.id] = input.value;
712
+ }
713
+ }
714
+ });
715
+ renderInputs(currentConfig);
716
+ }
717
+
718
+ // Setup event listeners
719
+ function setupEventListeners() {
720
+ const targetBranchSelect = document.getElementById('TARGET_BRANCH');
721
+ const repoSearch = document.getElementById('REPO_SEARCH');
722
+ const resultsContainer = document.getElementById('REPO_RESULTS');
723
+
724
+ if (repoSearch) {
725
+ // Focus: ouvre la liste avec ce qui est déjà là
726
+ repoSearch.addEventListener('focus', () => {
727
+ if (selectState.repos.length > 0) {
728
+ renderRepoResults();
729
+ } else {
730
+ // Si vraiment vide, on tente de charger une fois
731
+ fetchRepositories();
732
+ }
733
+ });
734
+
735
+ // Clavier: navigation avec flèches et Enter
736
+ repoSearch.addEventListener('keydown', (e) => {
737
+ const items = resultsContainer.querySelectorAll('.search-result-item');
738
+ if (items.length === 0) return;
739
+
740
+ if (e.key === 'ArrowDown') {
741
+ e.preventDefault();
742
+ activeIndex = Math.min(activeIndex + 1, items.length - 1);
743
+ updateActiveItem(items);
744
+ } else if (e.key === 'ArrowUp') {
745
+ e.preventDefault();
746
+ activeIndex = Math.max(activeIndex - 1, 0);
747
+ updateActiveItem(items);
748
+ } else if (e.key === 'Enter') {
749
+ e.preventDefault();
750
+ if (activeIndex >= 0) {
751
+ selectRepo(selectState.repos[activeIndex]);
752
+ }
753
+ } else if (e.key === 'Escape') {
754
+ resultsContainer.classList.add('hidden');
755
+ }
756
+ });
757
+
758
+ repoSearch.addEventListener('input', (e) => {
759
+ const term = e.target.value;
760
+ clearTimeout(searchTimeout);
761
+ searchTimeout = setTimeout(() => {
762
+ fetchRepositories(term);
763
+ }, 500);
764
+ });
765
+ }
766
+
767
+ function updateActiveItem(items) {
768
+ items.forEach((item, index) => {
769
+ item.classList.toggle('active', index === activeIndex);
770
+ if (index === activeIndex) {
771
+ item.scrollIntoView({ block: 'nearest' });
772
+ }
773
+ });
774
+ }
775
+
776
+ document.addEventListener('click', (e) => {
777
+ if (resultsContainer && !resultsContainer.contains(e.target) && e.target !== repoSearch) {
778
+ resultsContainer.classList.add('hidden');
779
+ }
780
+ });
781
+
782
+ ['INCLUDE_PUBLIC', 'INCLUDE_NOT_OWNED'].forEach(id => {
783
+ const input = document.getElementById(id);
784
+ if (input) {
785
+ input.addEventListener('change', async () => {
786
+ const config = {};
787
+ fields.forEach(f => {
788
+ const inp = document.getElementById(f.id);
789
+ if (inp) {
790
+ if (f.type === 'checkbox' || f.type === 'slider') config[f.id] = inp.checked;
791
+ else config[f.id] = inp.value;
792
+ }
793
+ });
794
+
795
+ try {
796
+ await fetch(api('api/config'), {
797
+ method: 'POST',
798
+ headers: { 'Content-Type': 'application/json' },
799
+ body: JSON.stringify(config)
800
+ });
801
+ await fetchRepositories(repoSearch?.value || '');
802
+ } catch (e) {
803
+ console.error('Erreur lors de la mise à jour auto:', e);
804
+ }
805
+ });
806
+ }
807
+ });
808
+
809
+ if (targetBranchSelect) {
810
+ targetBranchSelect.addEventListener('change', () => {
811
+ const customInput = document.getElementById('TARGET_BRANCH_custom');
812
+ if (!customInput) return;
813
+
814
+ const isNewBranch = targetBranchSelect.value === '';
815
+ customInput.classList.toggle('hidden', !isNewBranch);
816
+ if (isNewBranch) {
817
+ customInput.focus();
818
+ } else {
819
+ customInput.value = '';
820
+ }
821
+ });
822
+ }
823
+ }
824
+
825
+ function showStatus(message, type) {
826
+ const container = document.getElementById('toast-container');
827
+ const toast = document.createElement('div');
828
+
829
+ const bgColor = type === 'success' ? 'bg-green-900/90' : 'bg-red-900/90';
830
+ const borderColor = type === 'success' ? 'border-green-500' : 'border-red-500';
831
+ const textColor = type === 'success' ? 'text-green-100' : 'text-red-100';
832
+ const icon = type === 'success'
833
+ ? '<svg class="w-5 h-5 mr-3 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>'
834
+ : '<svg class="w-5 h-5 mr-3 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
835
+
836
+ toast.className = `toast flex items-center p-4 rounded-lg border shadow-xl ${bgColor} ${borderColor} ${textColor}`;
837
+ toast.innerHTML = `${icon}<span class="flex-1">${message}</span>`;
838
+
839
+ container.appendChild(toast);
840
+
841
+ // Supprimer le toast après l'animation (5s total)
842
+ setTimeout(() => {
843
+ toast.remove();
844
+ }, 5000);
845
+ }
846
+
847
+ // Uniformisation: addEventListener au lieu de onsubmit/onclick
848
+ form.addEventListener('submit', async (e) => {
849
+ e.preventDefault();
850
+
851
+ // Check if a configuration is selected
852
+ if (!currentConfigName) {
853
+ showStatus('Veuillez sélectionner ou créer une configuration', 'error');
854
+ return;
855
+ }
856
+
857
+ const config = {};
858
+ fields.forEach(field => {
859
+ const input = document.getElementById(field.id);
860
+ if (!input) return; // Protection contre les éléments manquants
861
+ if (field.type === 'checkbox' || field.type === 'slider') {
862
+ config[field.id] = input.checked;
863
+ } else if (field.id === 'TARGET_BRANCH') {
864
+ const customInput = document.getElementById(`${field.id}_custom`);
865
+ config[field.id] = customInput && customInput.value ? customInput.value : input.value;
866
+ } else {
867
+ config[field.id] = input.value;
868
+ }
869
+ });
870
+
871
+ try {
872
+ // Save to configs.json via the new endpoint
873
+ const response = await fetch(api('api/configs'), {
874
+ method: 'POST',
875
+ headers: { 'Content-Type': 'application/json' },
876
+ body: JSON.stringify({ name: currentConfigName, config })
877
+ });
878
+
879
+ if (response.ok) {
880
+ showStatus(`Configuration "${currentConfigName}" enregistrée avec succès !`, 'success');
881
+ } else {
882
+ const data = await response.json().catch(() => ({ error: 'Erreur inconnue' }));
883
+ throw new Error(data.error || 'Erreur lors de l\'enregistrement');
884
+ }
885
+ } catch (error) {
886
+ showStatus(error.message, 'error');
887
+ }
888
+ });
889
+
890
+ restartBtn.addEventListener('click', async () => {
891
+ if (!confirm('Êtes-vous sûr de vouloir redémarrer le serveur MCP ? Cela coupera la connexion actuelle pour appliquer les changements.')) {
892
+ return;
893
+ }
894
+
895
+ const originalHTML = restartBtn.innerHTML; // Sauvegarder le HTML original
896
+ try {
897
+ restartBtn.disabled = true;
898
+ restartBtn.innerHTML = '<span>Redémarrage en cours...</span>';
899
+
900
+ const response = await fetch(api('api/restart'), { method: 'POST' });
901
+
902
+ if (response.ok) {
903
+ showStatus('Signal de redémarrage envoyé. Le serveur va se relancer...', 'success');
904
+ setTimeout(() => window.location.reload(), 3000);
905
+ } else {
906
+ throw new Error('Erreur lors de la demande de redémarrage');
907
+ }
908
+ } catch (error) {
909
+ showStatus(error.message, 'error');
910
+ restartBtn.disabled = false;
911
+ restartBtn.innerHTML = originalHTML; // Restaurer le HTML original
912
+ }
913
+ });
914
+
915
+ stopBtn.addEventListener('click', async () => {
916
+ if (!confirm('Êtes-vous sûr de vouloir arrêter COMPLÈTEMENT le serveur MCP ?')) {
917
+ return;
918
+ }
919
+
920
+ const originalHTML = stopBtn.innerHTML; // Sauvegarder le HTML original
921
+ try {
922
+ stopBtn.disabled = true;
923
+ stopBtn.innerHTML = '<span>Arrêt en cours...</span>';
924
+
925
+ const response = await fetch(api('api/stop'), { method: 'POST' });
926
+
927
+ if (response.ok) {
928
+ showStatus('Le serveur s\'arrête. Cette page ne sera plus disponible.', 'success');
929
+ } else {
930
+ throw new Error('Erreur lors de la demande d\'arrêt');
931
+ }
932
+ } catch (error) {
933
+ showStatus(error.message, 'error');
934
+ stopBtn.disabled = false;
935
+ stopBtn.innerHTML = originalHTML; // Restaurer le HTML original
936
+ }
937
+ });
938
+
939
+ deleteWorkingDirBtn.addEventListener('click', async () => {
940
+ if (!confirm('Êtes-vous sûr de vouloir SUPPRIMER physiquement le dossier de travail local ? Cette action est irréversible et vous devrez effectuer un nouveau "clone" pour utiliser les outils Git.')) {
941
+ return;
942
+ }
943
+
944
+ const originalHTML = deleteWorkingDirBtn.innerHTML;
945
+ try {
946
+ deleteWorkingDirBtn.disabled = true;
947
+ deleteWorkingDirBtn.innerHTML = '<span>Suppression...</span>';
948
+
949
+ const response = await fetch(api('api/delete-working-dir'), { method: 'POST' });
950
+
951
+ if (response.ok) {
952
+ showStatus('Dossier de travail supprimé avec succès.', 'success');
953
+ currentConfig.REPO_PATH = '';
954
+ deleteWorkingDirBtn.disabled = true;
955
+ } else {
956
+ const data = await response.json().catch(() => ({ error: 'Erreur lors de la suppression' }));
957
+ throw new Error(data.error || 'Erreur lors de la suppression');
958
+ }
959
+ } catch (error) {
960
+ showStatus(error.message, 'error');
961
+ } finally {
962
+ deleteWorkingDirBtn.innerHTML = originalHTML;
963
+ deleteWorkingDirBtn.disabled = !currentConfig.REPO_PATH;
964
+ }
965
+ });
966
+
967
+ cloneBtn.addEventListener('click', async () => {
968
+ if (!confirm('Voulez-vous lancer le clonage du dépôt ? Assurez-vous d\'avoir enregistré la configuration (Parent Projects Path) avant.')) {
969
+ return;
970
+ }
971
+
972
+ const originalHTML = cloneBtn.innerHTML;
973
+ try {
974
+ cloneBtn.disabled = true;
975
+ cloneBtn.innerHTML = '<span>Clonage...</span>';
976
+
977
+ const response = await fetch(api('api/clone'), { method: 'POST' });
978
+
979
+ if (response.ok) {
980
+ showStatus('Le processus de clonage a été lancé en arrière-plan. Vérifiez les logs pour le résultat.', 'success');
981
+ } else {
982
+ const data = await response.json().catch(() => ({ error: 'Erreur lors du clonage' }));
983
+ throw new Error(data.error || 'Erreur lors du clonage');
984
+ }
985
+ } catch (error) {
986
+ showStatus(error.message, 'error');
987
+ cloneBtn.disabled = false;
988
+ } finally {
989
+ cloneBtn.innerHTML = originalHTML;
990
+ }
991
+ });
992
+
993
+ fetchConfig();
994
+ </script>
995
+ </body>
996
+ </html>