@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.
- package/LICENSE +15 -0
- package/README.md +154 -0
- package/dist/config-server.d.ts +6 -0
- package/dist/config-server.d.ts.map +1 -0
- package/dist/config-server.js +465 -0
- package/dist/config-server.js.map +1 -0
- package/dist/gui.d.ts +3 -0
- package/dist/gui.d.ts.map +1 -0
- package/dist/gui.js +26 -0
- package/dist/gui.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +687 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +59 -0
- package/dist/logger.js.map +1 -0
- package/dist/utils/config.d.ts +109 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +349 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/ssh.d.ts +16 -0
- package/dist/utils/ssh.d.ts.map +1 -0
- package/dist/utils/ssh.js +53 -0
- package/dist/utils/ssh.js.map +1 -0
- package/package.json +41 -0
- package/public/index.html +996 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Restricted GitHub MCP Server
|
|
2
|
+
|
|
3
|
+
Un serveur MCP (Model Context Protocol) spécialisé pour automatiser le workflow Git local vers GitHub, avec délégation des compilations/tests à un serveur distant via SSH. Conçu pour être minimaliste et sécurisé.
|
|
4
|
+
|
|
5
|
+
## 🚀 Outils disponibles
|
|
6
|
+
|
|
7
|
+
### Git local → GitHub
|
|
8
|
+
|
|
9
|
+
- **`commit`** : Exécute `git add .` puis crée un commit avec le message fourni. *Ne pousse pas vers le remote.*
|
|
10
|
+
- **`push`** : Pousse la branche actuelle vers `TARGET_BRANCH`. *Ne fait pas de commit.*
|
|
11
|
+
- **`commit_and_push`** : Enchaîne `git add .`, commit et push vers `TARGET_BRANCH`.
|
|
12
|
+
- **`pull_request`** : Crée une Pull Request GitHub de `TARGET_BRANCH` vers `BASE_BRANCH`.
|
|
13
|
+
- **`clone`** : Clone `TARGET_REPO` dans `CLONE_DIR/<TARGET_BRANCH>/`. Crée la branche distante si elle n'existe pas encore.
|
|
14
|
+
- **`get_info`** : Affiche la configuration active (dépôt, branches, chemins) sans exposer les secrets.
|
|
15
|
+
- **`get_logs`** : Récupère les dernières lignes du fichier de log serveur. Argument optionnel : `lines` (défaut : 50).
|
|
16
|
+
|
|
17
|
+
### Serveur distant via SSH
|
|
18
|
+
|
|
19
|
+
- **`deploy`** : Sur le serveur SSH configuré, exécute `git fetch --all`, `git checkout TARGET_BRANCH`, `git pull` dans `SSH_REMOTE_PATH`. Le repo doit être préalablement cloné manuellement sur le serveur.
|
|
20
|
+
- **`test`** : Exécute `TEST_COMMAND` sur le serveur distant (cwd = `SSH_REMOTE_PATH`) et renvoie stdout, stderr et exit code. Stocke la sortie pour `test_logs`.
|
|
21
|
+
- **`test_logs`** : Renvoie la sortie du dernier `test` exécuté sans relancer la commande.
|
|
22
|
+
|
|
23
|
+
### Flux typique agent
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
clone → (modifier des fichiers) → commit_and_push → deploy → test
|
|
27
|
+
↑ |
|
|
28
|
+
└─── (corriger) ────────┘ (si exit code ≠ 0)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 🖥️ Interface de Configuration (GUI)
|
|
32
|
+
|
|
33
|
+
Le serveur lance automatiquement une interface graphique au démarrage (si `PROXY_URL` est défini). Elle permet de :
|
|
34
|
+
|
|
35
|
+
- Gérer plusieurs configurations nommées (multi-projets).
|
|
36
|
+
- Sélectionner le dépôt et les branches via des menus déroulants GitHub.
|
|
37
|
+
- Visualiser la configuration active en temps réel.
|
|
38
|
+
|
|
39
|
+
> **Proxy dynamique** : La GUI s'enregistre auprès du reverse proxy défini dans `PROXY_URL` et reçoit un port dynamique. Elle est alors accessible via `http://[PROXY_URL][GUI_PATH]/`.
|
|
40
|
+
|
|
41
|
+
## ⚙️ Configuration
|
|
42
|
+
|
|
43
|
+
Toutes les variables sont définies dans un fichier `.env` ou dans la section `env` du client MCP.
|
|
44
|
+
|
|
45
|
+
### Variables GitHub
|
|
46
|
+
|
|
47
|
+
| Variable | Description | Exemple |
|
|
48
|
+
| :--- | :--- | :--- |
|
|
49
|
+
| `GITHUB_TOKEN` | Personal Access Token GitHub (PAT) | `github_pat_...` |
|
|
50
|
+
| `MCP_DATA_DIR` | Dossier contenant `configs.json` (multi-configs) | `./data` |
|
|
51
|
+
| `CLONE_DIR` | Dossier racine dans lequel les branches seront clonées | `./cloned_repos` |
|
|
52
|
+
|
|
53
|
+
> Les configurations par projet (dépôt, branches, mode lecture seule…) sont gérées dans l'interface GUI et stockées dans `configs.json`.
|
|
54
|
+
|
|
55
|
+
### Variables SSH (outils `deploy` / `test`)
|
|
56
|
+
|
|
57
|
+
| Variable | Description | Exemple |
|
|
58
|
+
| :--- | :--- | :--- |
|
|
59
|
+
| `SSH_HOST` | Adresse IP ou hostname du serveur distant | `195.35.24.113` |
|
|
60
|
+
| `SSH_USER` | Utilisateur SSH | `root` |
|
|
61
|
+
| `SSH_PORT` | Port SSH (défaut : 22) | `22` |
|
|
62
|
+
| `SSH_KEY_PATH` | Chemin absolu vers la clé privée SSH | `C:/Users/.../.ssh/id_ed25519` |
|
|
63
|
+
| `SSH_REMOTE_PATH` | Chemin du repo sur le serveur distant | `/var/www/mon-projet` |
|
|
64
|
+
| `TEST_COMMAND` | Commande de test à exécuter sur le serveur | `npx tsc --noEmit --pretty` |
|
|
65
|
+
|
|
66
|
+
> **Note** : Pour `tsc`, utilisez `--pretty` pour obtenir la sortie complète avec contexte de code (sans cette option, `tsc` passe en format compact une ligne par erreur quand il n'y a pas de TTY).
|
|
67
|
+
|
|
68
|
+
### Variables GUI / Proxy
|
|
69
|
+
|
|
70
|
+
| Variable | Description | Exemple |
|
|
71
|
+
| :--- | :--- | :--- |
|
|
72
|
+
| `PROXY_URL` | URL du reverse proxy central | `http://localhost:3000` |
|
|
73
|
+
| `GUI_PATH` | Chemin d'accès via le proxy | `/github-mcp` (défaut) |
|
|
74
|
+
| `GUI_NAME` | Nom affiché dans le proxy | `GitHub MCP Config` (défaut) |
|
|
75
|
+
| `AGENT_NAME` | Nom de l'agent affiché dans la GUI | `Agent Zero` |
|
|
76
|
+
|
|
77
|
+
## 📝 Journalisation (Logs)
|
|
78
|
+
|
|
79
|
+
- **Windows** : `C:\var\log\restricted-github-mcp\server.log`
|
|
80
|
+
- **Linux/macOS** : `/var/log/restricted-github-mcp/server.log`
|
|
81
|
+
|
|
82
|
+
Le dossier est créé automatiquement. Les logs du processus principal (`[MASTER]`) et de la GUI (`[GUI]`) sont centralisés dans le même fichier.
|
|
83
|
+
|
|
84
|
+
## 🖥️ Exemple de Configuration MCP
|
|
85
|
+
|
|
86
|
+
### Via npx (Recommandé)
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"@imenam/mcp-github": {
|
|
92
|
+
"command": "npx",
|
|
93
|
+
"args": ["-y", "@imenam/mcp-github"],
|
|
94
|
+
"env": {
|
|
95
|
+
"GITHUB_TOKEN": "votre_token_github_ici",
|
|
96
|
+
"CLONE_DIR": "/chemin/vers/dossier-de-clonage",
|
|
97
|
+
"MCP_DATA_DIR": "./data",
|
|
98
|
+
"PROXY_URL": "http://localhost:3000",
|
|
99
|
+
"GUI_PATH": "/github-mcp",
|
|
100
|
+
"GUI_NAME": "GitHub MCP Config",
|
|
101
|
+
"AGENT_NAME": "Mon Agent",
|
|
102
|
+
"SSH_HOST": "mon-serveur.example.com",
|
|
103
|
+
"SSH_USER": "ubuntu",
|
|
104
|
+
"SSH_PORT": "22",
|
|
105
|
+
"SSH_KEY_PATH": "/chemin/vers/.ssh/id_ed25519",
|
|
106
|
+
"SSH_REMOTE_PATH": "/var/www/mon-projet",
|
|
107
|
+
"TEST_COMMAND": "npx tsc --noEmit --pretty"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
> Les variables `PROXY_URL`, `GUI_*`, `AGENT_NAME` et SSH sont optionnelles selon votre usage.
|
|
115
|
+
|
|
116
|
+
### Via installation locale
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"@imenam/mcp-github": {
|
|
122
|
+
"command": "node",
|
|
123
|
+
"args": ["C:/chemin/vers/gitHubMCP/dist/index.js"],
|
|
124
|
+
"env": {
|
|
125
|
+
"GITHUB_TOKEN": "votre_token_github_ici",
|
|
126
|
+
"CLONE_DIR": "./cloned_repos"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 🛠️ Installation locale & build
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
git clone https://github.com/votre-pseudo/gitHubMCP.git
|
|
137
|
+
cd gitHubMCP
|
|
138
|
+
npm install
|
|
139
|
+
cp .env.example .env
|
|
140
|
+
# Éditez .env avec vos valeurs
|
|
141
|
+
npm run build
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 📦 Publication Automatisée
|
|
145
|
+
|
|
146
|
+
Ce projet est configuré pour être publié sur npm via **GitHub Actions** à chaque nouvelle Release :
|
|
147
|
+
|
|
148
|
+
1. Mettez à jour la version dans `package.json`.
|
|
149
|
+
2. Poussez sur GitHub et créez une Release.
|
|
150
|
+
3. Le workflow OIDC publie automatiquement sur npm.
|
|
151
|
+
|
|
152
|
+
## 📄 Licence
|
|
153
|
+
|
|
154
|
+
ISC
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-server.d.ts","sourceRoot":"","sources":["../src/config-server.ts"],"names":[],"mappings":"AAyEA;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAczD;AAED,wBAAsB,iBAAiB,kBAobtC"}
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { Octokit } from "octokit";
|
|
6
|
+
import { logToFile } from "./logger.js";
|
|
7
|
+
import { getConfig, updateEnvFile, safeSend, migrateFromLegacyEnv, getAllConfigNames, getConfigByName, saveConfiguration, deleteConfiguration, setCurrentConfiguration, getCurrentConfigName, } from "./utils/config.js";
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
if (!getConfig().PROXY_URL) {
|
|
11
|
+
process.stderr.write('[GUI] PROXY_URL non défini — arrêt.\n');
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
// État du proxy pour le désenregistrement
|
|
15
|
+
let registeredPath = null;
|
|
16
|
+
/**
|
|
17
|
+
* Tente de s'enregistrer auprès du proxy dynamique.
|
|
18
|
+
* @returns Le port attribué par le proxy, ou null en cas d'échec.
|
|
19
|
+
*/
|
|
20
|
+
async function tryProxyRegistration() {
|
|
21
|
+
const { PROXY_URL, GUI_PATH, GUI_NAME } = getConfig();
|
|
22
|
+
if (!PROXY_URL) {
|
|
23
|
+
logToFile("[GUI] PROXY_URL non défini, utilisation du port par défaut");
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
29
|
+
const response = await fetch(`${PROXY_URL}/proxy/register`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({ path: GUI_PATH, name: GUI_NAME }),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
clearTimeout(timeoutId);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const errorData = await response.json().catch(() => ({}));
|
|
38
|
+
logToFile(`[GUI] Échec enregistrement proxy: ${errorData.error || response.statusText}`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
registeredPath = GUI_PATH;
|
|
43
|
+
logToFile(`[GUI] Enregistré auprès du proxy (path: ${GUI_PATH}, port: ${data.port})`);
|
|
44
|
+
return data.port;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const reason = error.name === "AbortError" ? "timeout" : error.message;
|
|
48
|
+
logToFile(`[GUI] Échec enregistrement proxy: ${reason}`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Se désenregistre du proxy dynamique (si enregistré).
|
|
54
|
+
*/
|
|
55
|
+
export async function unregisterFromProxy() {
|
|
56
|
+
const { PROXY_URL } = getConfig();
|
|
57
|
+
if (!PROXY_URL || !registeredPath)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
await fetch(`${PROXY_URL}/proxy/unregister`, {
|
|
61
|
+
method: "DELETE",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ path: registeredPath }),
|
|
64
|
+
});
|
|
65
|
+
logToFile(`[GUI] Désenregistré du proxy (path: ${registeredPath})`);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignorer les erreurs de désenregistrement
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function startConfigServer() {
|
|
72
|
+
logToFile("[GUI] Initializing config server...");
|
|
73
|
+
// Migrate legacy .env to multi-config format on startup
|
|
74
|
+
migrateFromLegacyEnv();
|
|
75
|
+
// Enregistrement auprès du proxy (obligatoire)
|
|
76
|
+
const port = await tryProxyRegistration();
|
|
77
|
+
if (!port) {
|
|
78
|
+
process.stderr.write('[GUI] Enregistrement proxy échoué — arrêt.\n');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const app = express();
|
|
82
|
+
app.use(express.json());
|
|
83
|
+
// Logging middleware for GUI requests
|
|
84
|
+
app.use((req, res, next) => {
|
|
85
|
+
logToFile(`[GUI] Request: ${req.method} ${req.url}`);
|
|
86
|
+
next();
|
|
87
|
+
});
|
|
88
|
+
// Health check route (requise par le proxy)
|
|
89
|
+
app.get("/proxy/health", (req, res) => {
|
|
90
|
+
res.status(200).json({ status: "healthy" });
|
|
91
|
+
});
|
|
92
|
+
// Utilise un chemin relatif au fichier compilé pour trouver public
|
|
93
|
+
const publicPath = path.join(__dirname, "..", "public");
|
|
94
|
+
logToFile(`[GUI] Looking for static files in: ${publicPath}`);
|
|
95
|
+
if (!fs.existsSync(publicPath)) {
|
|
96
|
+
const errorMsg = `[GUI] CRITICAL ERROR: Public directory NOT FOUND at ${publicPath}`;
|
|
97
|
+
console.error(errorMsg);
|
|
98
|
+
logToFile(errorMsg);
|
|
99
|
+
}
|
|
100
|
+
app.use(express.static(publicPath));
|
|
101
|
+
app.get("/api/config", (req, res) => {
|
|
102
|
+
logToFile("[GUI] GET /api/config called");
|
|
103
|
+
try {
|
|
104
|
+
const config = getConfig();
|
|
105
|
+
res.json({
|
|
106
|
+
TARGET_REPO: config.TARGET_REPO,
|
|
107
|
+
TARGET_BRANCH: config.TARGET_BRANCH,
|
|
108
|
+
BASE_BRANCH: config.BASE_BRANCH,
|
|
109
|
+
REPO_PATH: config.REPO_PATH,
|
|
110
|
+
CLONE_DIR: config.CLONE_DIR,
|
|
111
|
+
READ_ONLY: config.READ_ONLY,
|
|
112
|
+
INCLUDE_PUBLIC: config.INCLUDE_PUBLIC,
|
|
113
|
+
INCLUDE_NOT_OWNED: config.INCLUDE_NOT_OWNED,
|
|
114
|
+
AGENT_NAME: config.AGENT_NAME,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const msg = `[GUI] Failed to get configuration: ${error.message}`;
|
|
119
|
+
logToFile(msg);
|
|
120
|
+
res.status(500).json({ error: msg });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
app.post("/api/delete-working-dir", async (req, res) => {
|
|
124
|
+
logToFile("[GUI] POST /api/delete-working-dir called");
|
|
125
|
+
try {
|
|
126
|
+
const config = getConfig();
|
|
127
|
+
const repoPath = config.REPO_PATH;
|
|
128
|
+
if (repoPath && fs.existsSync(repoPath)) {
|
|
129
|
+
logToFile(`[GUI] Suppression du dossier de travail : ${repoPath}`);
|
|
130
|
+
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
131
|
+
logToFile(`[GUI] Dossier de travail supprimé.`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
logToFile(`[GUI] Tentative de suppression d'un dossier inexistant ou non défini : ${repoPath}`);
|
|
135
|
+
}
|
|
136
|
+
// Vider REPO_PATH dans .env et process.env
|
|
137
|
+
await updateEnvFile("REPO_PATH", "");
|
|
138
|
+
process.env.REPO_PATH = "";
|
|
139
|
+
// Notifier le master
|
|
140
|
+
safeSend(process, { type: "CONFIG_UPDATED", config: { REPO_PATH: "" } });
|
|
141
|
+
res.status(200).json({ message: "Dossier de travail supprimé et configuration mise à jour" });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const msg = `[GUI] Échec de la suppression du dossier : ${error.message}`;
|
|
145
|
+
logToFile(msg);
|
|
146
|
+
res.status(500).json({ error: msg });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
app.post("/api/clone", async (req, res) => {
|
|
150
|
+
logToFile("[GUI] POST /api/clone called");
|
|
151
|
+
try {
|
|
152
|
+
const config = getConfig();
|
|
153
|
+
if (!config.TARGET_REPO) {
|
|
154
|
+
return res.status(400).json({ error: "TARGET_REPO non défini." });
|
|
155
|
+
}
|
|
156
|
+
if (!config.TARGET_BRANCH) {
|
|
157
|
+
return res.status(400).json({ error: "TARGET_BRANCH non défini." });
|
|
158
|
+
}
|
|
159
|
+
logToFile(`[GUI] Envoi du signal CLONE au master process (CLONE_DIR: ${config.CLONE_DIR})`);
|
|
160
|
+
const sent = safeSend(process, { type: "CLONE" });
|
|
161
|
+
if (sent) {
|
|
162
|
+
res.status(200).json({ message: "Clonage démarré..." });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
throw new Error("Impossible de communiquer avec le processus Master");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const msg = `[GUI] Échec du lancement du clonage : ${error.message}`;
|
|
170
|
+
logToFile(msg);
|
|
171
|
+
res.status(500).json({ error: msg });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
app.get("/api/repositories", async (req, res) => {
|
|
175
|
+
logToFile("[GUI] GET /api/repositories called");
|
|
176
|
+
try {
|
|
177
|
+
const config = getConfig();
|
|
178
|
+
const token = config.GITHUB_TOKEN;
|
|
179
|
+
if (!token) {
|
|
180
|
+
logToFile("[GUI] Repositories endpoint: Token non configuré");
|
|
181
|
+
return res.status(400).json({ error: "Token GitHub non configuré" });
|
|
182
|
+
}
|
|
183
|
+
const octokit = new Octokit({ auth: token });
|
|
184
|
+
const user = await octokit.rest.users.getAuthenticated();
|
|
185
|
+
logToFile(`[GUI] Authenticated as: ${user.data.login}`);
|
|
186
|
+
const includePublic = config.INCLUDE_PUBLIC;
|
|
187
|
+
const includeNotOwned = config.INCLUDE_NOT_OWNED;
|
|
188
|
+
const search = req.query.search;
|
|
189
|
+
let accessibleRepos = [];
|
|
190
|
+
if (search && search.length > 0) {
|
|
191
|
+
// Mode Recherche : Utilise l'API de recherche
|
|
192
|
+
const user = await octokit.rest.users.getAuthenticated();
|
|
193
|
+
// Si on n'inclut pas les "non-owned", on restreint la recherche à l'utilisateur actuel
|
|
194
|
+
const query = includeNotOwned
|
|
195
|
+
? `${search} in:name fork:true`
|
|
196
|
+
: `${search} user:${user.data.login} in:name fork:true`;
|
|
197
|
+
const { data: searchResults } = await octokit.rest.search.repos({
|
|
198
|
+
q: query,
|
|
199
|
+
sort: "updated",
|
|
200
|
+
per_page: 10,
|
|
201
|
+
});
|
|
202
|
+
accessibleRepos = searchResults.items.map(repo => repo.full_name);
|
|
203
|
+
logToFile(`[GUI] Search for "${search}" found ${accessibleRepos.length} repos`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Mode Liste : Utilise l'API de listing classique (top 10 récents)
|
|
207
|
+
const { data } = await octokit.rest.repos.listForAuthenticatedUser({
|
|
208
|
+
affiliation: includeNotOwned ? "owner,collaborator,organization_member" : "owner",
|
|
209
|
+
visibility: includePublic ? "all" : "private",
|
|
210
|
+
sort: "updated",
|
|
211
|
+
per_page: 10,
|
|
212
|
+
});
|
|
213
|
+
accessibleRepos = data.map(repo => repo.full_name);
|
|
214
|
+
logToFile(`[GUI] Default list found ${accessibleRepos.length} repos`);
|
|
215
|
+
}
|
|
216
|
+
res.json(accessibleRepos);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const msg = `[GUI] Failed to fetch repositories: ${error.message}`;
|
|
220
|
+
logToFile(msg);
|
|
221
|
+
res.status(500).json({ error: msg });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
app.get("/api/branches", async (req, res) => {
|
|
225
|
+
logToFile("[GUI] GET /api/branches called");
|
|
226
|
+
try {
|
|
227
|
+
const config = getConfig();
|
|
228
|
+
const repo = req.query.repo;
|
|
229
|
+
const token = config.GITHUB_TOKEN;
|
|
230
|
+
if (!repo) {
|
|
231
|
+
logToFile("[GUI] Branches endpoint: Paramètre repo manquant");
|
|
232
|
+
return res.status(400).json({ error: "Paramètre repo manquant" });
|
|
233
|
+
}
|
|
234
|
+
if (!token) {
|
|
235
|
+
logToFile("[GUI] Branches endpoint: Token non configuré");
|
|
236
|
+
return res.status(400).json({ error: "Token GitHub non configuré" });
|
|
237
|
+
}
|
|
238
|
+
const [owner, repoName] = repo.split("/");
|
|
239
|
+
if (!owner || !repoName) {
|
|
240
|
+
logToFile("[GUI] Branches endpoint: Format de repo invalide");
|
|
241
|
+
return res.status(400).json({ error: "Format de repo invalide (owner/repo)" });
|
|
242
|
+
}
|
|
243
|
+
const octokit = new Octokit({ auth: token });
|
|
244
|
+
const { data } = await octokit.rest.repos.listBranches({
|
|
245
|
+
owner,
|
|
246
|
+
repo: repoName,
|
|
247
|
+
per_page: 100,
|
|
248
|
+
});
|
|
249
|
+
const branches = data.map((branch) => branch.name).reverse();
|
|
250
|
+
logToFile(`[GUI] Branches endpoint: ${branches.length} branches retournées pour ${repo}`);
|
|
251
|
+
res.json(branches);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const msg = `[GUI] Failed to fetch branches: ${error.message}`;
|
|
255
|
+
logToFile(msg);
|
|
256
|
+
res.status(500).json({ error: msg });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
app.post("/api/config", async (req, res) => {
|
|
260
|
+
logToFile("[GUI] POST /api/config called");
|
|
261
|
+
const newConfig = req.body;
|
|
262
|
+
try {
|
|
263
|
+
// Update current process env
|
|
264
|
+
Object.assign(process.env, newConfig);
|
|
265
|
+
// Persist each key to .env without destroying other existing keys
|
|
266
|
+
for (const [key, value] of Object.entries(newConfig)) {
|
|
267
|
+
await updateEnvFile(key, value);
|
|
268
|
+
}
|
|
269
|
+
// Notify master process about the update
|
|
270
|
+
logToFile("[GUI] Sending CONFIG_UPDATED to master process");
|
|
271
|
+
safeSend(process, { type: "CONFIG_UPDATED", config: newConfig });
|
|
272
|
+
logToFile("[GUI] Configuration updated and master notified");
|
|
273
|
+
res.sendStatus(200);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const msg = `[GUI] Failed to save .env: ${error.message}`;
|
|
277
|
+
console.error(msg);
|
|
278
|
+
logToFile(msg);
|
|
279
|
+
res.status(500).json({ error: msg });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
// =========================================================================
|
|
283
|
+
// Multi-configuration management endpoints
|
|
284
|
+
// =========================================================================
|
|
285
|
+
app.get("/api/configs", (req, res) => {
|
|
286
|
+
logToFile("[GUI] GET /api/configs called");
|
|
287
|
+
try {
|
|
288
|
+
const names = getAllConfigNames();
|
|
289
|
+
const current = getCurrentConfigName();
|
|
290
|
+
res.json({ names, current });
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
const msg = `[GUI] Failed to get configs: ${error.message}`;
|
|
294
|
+
logToFile(msg);
|
|
295
|
+
res.status(500).json({ error: msg });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
app.get("/api/configs/:name", (req, res) => {
|
|
299
|
+
logToFile(`[GUI] GET /api/configs/${req.params.name} called`);
|
|
300
|
+
try {
|
|
301
|
+
const config = getConfigByName(req.params.name);
|
|
302
|
+
if (config) {
|
|
303
|
+
res.json(config);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
res.status(404).json({ error: "Configuration not found" });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
const msg = `[GUI] Failed to get config: ${error.message}`;
|
|
311
|
+
logToFile(msg);
|
|
312
|
+
res.status(500).json({ error: msg });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
app.post("/api/configs", async (req, res) => {
|
|
316
|
+
logToFile("[GUI] POST /api/configs called");
|
|
317
|
+
const { name, config } = req.body;
|
|
318
|
+
if (!name || !config) {
|
|
319
|
+
return res.status(400).json({ error: "Name and config are required" });
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
saveConfiguration(name, config);
|
|
323
|
+
// Set as current if no current config
|
|
324
|
+
if (!getCurrentConfigName()) {
|
|
325
|
+
setCurrentConfiguration(name);
|
|
326
|
+
}
|
|
327
|
+
safeSend(process, { type: "CONFIG_UPDATED" });
|
|
328
|
+
res.json({ message: `Configuration "${name}" saved` });
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
const msg = `[GUI] Failed to save config: ${error.message}`;
|
|
332
|
+
logToFile(msg);
|
|
333
|
+
res.status(500).json({ error: msg });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
app.delete("/api/configs/:name", (req, res) => {
|
|
337
|
+
logToFile(`[GUI] DELETE /api/configs/${req.params.name} called`);
|
|
338
|
+
try {
|
|
339
|
+
const success = deleteConfiguration(req.params.name);
|
|
340
|
+
if (success) {
|
|
341
|
+
safeSend(process, { type: "CONFIG_UPDATED" });
|
|
342
|
+
res.json({ message: `Configuration "${req.params.name}" deleted` });
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
res.status(404).json({ error: "Configuration not found" });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
const msg = `[GUI] Failed to delete config: ${error.message}`;
|
|
350
|
+
logToFile(msg);
|
|
351
|
+
res.status(500).json({ error: msg });
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
app.post("/api/configs/:name/activate", (req, res) => {
|
|
355
|
+
logToFile(`[GUI] POST /api/configs/${req.params.name}/activate called`);
|
|
356
|
+
try {
|
|
357
|
+
const success = setCurrentConfiguration(req.params.name);
|
|
358
|
+
if (success) {
|
|
359
|
+
safeSend(process, { type: "CONFIG_UPDATED" });
|
|
360
|
+
res.json({ message: `Switched to configuration "${req.params.name}"` });
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
res.status(404).json({ error: "Configuration not found" });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
const msg = `[GUI] Failed to activate config: ${error.message}`;
|
|
368
|
+
logToFile(msg);
|
|
369
|
+
res.status(500).json({ error: msg });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
// GET /api/configurations — Convention méta-config proxy
|
|
373
|
+
app.get("/api/configurations", (req, res) => {
|
|
374
|
+
const names = getAllConfigNames();
|
|
375
|
+
const current = getCurrentConfigName();
|
|
376
|
+
const configurations = names.map(name => {
|
|
377
|
+
const cfg = getConfigByName(name);
|
|
378
|
+
return {
|
|
379
|
+
id: name,
|
|
380
|
+
name: name,
|
|
381
|
+
description: cfg ? `GitHub config: ${name}` : "",
|
|
382
|
+
active: name === current
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
res.json({ configurations });
|
|
386
|
+
});
|
|
387
|
+
// POST /api/configurations/apply — Convention méta-config proxy
|
|
388
|
+
app.post("/api/configurations/apply", (req, res) => {
|
|
389
|
+
const { configId } = req.body;
|
|
390
|
+
if (!configId) {
|
|
391
|
+
return res.status(400).json({ success: false, error: "configId is required" });
|
|
392
|
+
}
|
|
393
|
+
const success = setCurrentConfiguration(configId);
|
|
394
|
+
if (!success) {
|
|
395
|
+
return res.status(404).json({ success: false, error: `Configuration "${configId}" not found` });
|
|
396
|
+
}
|
|
397
|
+
if (process.send && process.connected) {
|
|
398
|
+
process.send({ type: "CONFIG_UPDATED" });
|
|
399
|
+
}
|
|
400
|
+
return res.json({ success: true, appliedConfig: configId });
|
|
401
|
+
});
|
|
402
|
+
// =========================================================================
|
|
403
|
+
app.post("/api/restart", (req, res) => {
|
|
404
|
+
logToFile("[GUI] POST /api/restart called (Full MCP)");
|
|
405
|
+
try {
|
|
406
|
+
res.json({ message: "Restarting GitHub MCP server..." });
|
|
407
|
+
setTimeout(() => {
|
|
408
|
+
logToFile("[GUI] Sending RESTART signal to master process");
|
|
409
|
+
if (!safeSend(process, { type: "RESTART" })) {
|
|
410
|
+
logToFile("[GUI] ERROR: process.send failed or not connected");
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
}, 500);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
const msg = `[GUI] Failed to restart: ${error.message}`;
|
|
417
|
+
logToFile(msg);
|
|
418
|
+
res.status(500).json({ error: msg });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
app.post("/api/stop", (req, res) => {
|
|
422
|
+
logToFile("[GUI] POST /api/stop called (Full MCP)");
|
|
423
|
+
try {
|
|
424
|
+
res.json({ message: "Stopping GitHub MCP server..." });
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
logToFile("[GUI] Sending STOP signal to master process");
|
|
427
|
+
if (!safeSend(process, { type: "STOP" })) {
|
|
428
|
+
logToFile("[GUI] ERROR: process.send failed or not connected");
|
|
429
|
+
process.exit(0);
|
|
430
|
+
}
|
|
431
|
+
}, 500);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
const msg = `[GUI] Failed to stop: ${error.message}`;
|
|
435
|
+
logToFile(msg);
|
|
436
|
+
res.status(500).json({ error: msg });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
try {
|
|
440
|
+
logToFile(`[GUI] Attempting to listen on port ${port}...`);
|
|
441
|
+
const serverListener = app.listen(port, () => {
|
|
442
|
+
const config = getConfig();
|
|
443
|
+
const msg = `[GUI] Config GUI disponible sur http://localhost:${port} - via proxy (${config.PROXY_URL}${config.GUI_PATH})`;
|
|
444
|
+
console.error(msg);
|
|
445
|
+
logToFile(msg);
|
|
446
|
+
});
|
|
447
|
+
serverListener.on('error', (err) => {
|
|
448
|
+
let msg = `[GUI] SERVER ERROR (on port ${port}): ${err.message}`;
|
|
449
|
+
if (err.code === 'EADDRINUSE') {
|
|
450
|
+
msg = `[GUI] PORT CONFLICT: Port ${port} is already in use by another process.`;
|
|
451
|
+
}
|
|
452
|
+
console.error(msg);
|
|
453
|
+
logToFile(msg);
|
|
454
|
+
});
|
|
455
|
+
serverListener.on('close', () => {
|
|
456
|
+
logToFile(`[GUI] Server listener CLOSED unexpectedly.`);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
const msg = `[GUI] UNEXPECTED EXCEPTION during app.listen: ${error.message}`;
|
|
461
|
+
console.error(msg);
|
|
462
|
+
logToFile(msg);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
//# sourceMappingURL=config-server.js.map
|