atlasia-ghost 0.1.0 → 0.3.0

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 (4) hide show
  1. package/README.md +16 -0
  2. package/ghost.js +98 -32
  3. package/package.json +8 -3
  4. package/test.js +0 -36
package/README.md CHANGED
@@ -39,10 +39,26 @@ ghost
39
39
  | Option | Description |
40
40
  | :--- | :--- |
41
41
  | `--model <name>` | Utiliser un modèle spécifique (ex: `llama-3.1-8b-instant`) |
42
+ | `--provider <name>` | Choisir le fournisseur (`groq` [défaut], `openai`) |
42
43
  | `--no-security` | Désactiver l'audit de sécurité (scan de secrets) |
43
44
  | `--dry-run` | Générer le message sans effectuer le commit |
44
45
  | `--help`, `-h` | Afficher l'aide |
45
46
 
47
+ ## 📝 Personnalisation
48
+
49
+ ### Sélection de fichiers
50
+ Ghost propose désormais une sélection interactive si plusieurs fichiers sont modifiés. Vous pouvez spécifier les fichiers à analyser (ex: `1,3,5`) ou tout analyser (`all`).
51
+
52
+ ### Configuration locale (`.ghostrc`)
53
+ Vous pouvez personnaliser le comportement de Ghost par projet en créant un fichier `.ghostrc` à la racine :
54
+
55
+ ```json
56
+ {
57
+ "prompt": "Ton prompt système personnalisé pour l'IA ici",
58
+ "provider": "openai"
59
+ }
60
+ ```
61
+
46
62
  ## 🛡️ Sécurité
47
63
 
48
64
  Ghost effectue un double audit :
package/ghost.js CHANGED
@@ -17,6 +17,7 @@ const os = require('os');
17
17
  // ⚙️ CONFIGURATION & CONSTANTES
18
18
  // ==============================================================================
19
19
  const CONFIG_FILE = path.join(os.homedir(), '.ghost');
20
+ const HISTORY_FILE = path.join(os.homedir(), '.ghost_history');
20
21
  const SAFE_EXTENSIONS = new Set(['.md', '.txt', '.csv', '.html', '.css', '.scss', '.lock', '.xml', '.json']);
21
22
  const SAFE_FILES = new Set(['mvnw', 'gradlew', 'package-lock.json', 'yarn.lock', 'pom.xml']);
22
23
 
@@ -62,21 +63,29 @@ class ConfigManager {
62
63
  console.log(`${Colors.DIM}Configuration sauvegardée dans ${CONFIG_FILE}${Colors.ENDC}`);
63
64
  }
64
65
 
65
- async getApiKey() {
66
- let key = process.env.GROQ_API_KEY || this.config.groq_api_key;
66
+ async getApiKey(provider = 'groq') {
67
+ const keyMap = {
68
+ groq: { env: 'GROQ_API_KEY', config: 'groq_api_key', label: 'Groq', url: 'https://console.groq.com', prefix: 'gsk_' },
69
+ openai: { env: 'OPENAI_API_KEY', config: 'openai_api_key', label: 'OpenAI', url: 'https://platform.openai.com', prefix: 'sk-' },
70
+ anthropic: { env: 'ANTHROPIC_API_KEY', config: 'anthropic_api_key', label: 'Anthropic', url: 'https://console.anthropic.com', prefix: 'sk-ant' },
71
+ gemini: { env: 'GEMINI_API_KEY', config: 'gemini_api_key', label: 'Gemini', url: 'https://aistudio.google.com', prefix: '' }
72
+ };
73
+
74
+ const info = keyMap[provider] || keyMap.groq;
75
+ let key = process.env[info.env] || this.config[info.config];
67
76
 
68
77
  if (!key) {
69
- console.log(`\n${Colors.WARNING}⚠️ Configuration manquante${Colors.ENDC}`);
70
- console.log(`${Colors.DIM}Pour utiliser Ghost, vous avez besoin d'une clé API Groq (Gratuite).${Colors.ENDC}`);
71
- console.log(`${Colors.BLUE}👉 Obtenir une clé : https://console.groq.com${Colors.ENDC}\n`);
78
+ console.log(`\n${Colors.WARNING}⚠️ Configuration manquante pour ${info.label}${Colors.ENDC}`);
79
+ console.log(`${Colors.DIM}Pour utiliser Ghost avec ${info.label}, vous avez besoin d'une clé API.${Colors.ENDC}`);
80
+ console.log(`${Colors.BLUE}👉 Obtenir une clé : ${info.url}${Colors.ENDC}\n`);
72
81
 
73
- key = await promptUser(`${Colors.BOLD}Collez votre clé Groq (gsk_...) : ${Colors.ENDC}`);
82
+ key = await promptUser(`${Colors.BOLD}Collez votre clé ${info.label} : ${Colors.ENDC}`);
74
83
 
75
- if (key && key.trim().startsWith('gsk_')) {
76
- this.config.groq_api_key = key.trim();
84
+ if (key && key.trim()) {
85
+ this.config[info.config] = key.trim();
77
86
  this.save();
78
87
  } else {
79
- console.log(`${Colors.FAIL}❌ Clé invalide ou manquante. Abandon.${Colors.ENDC}`);
88
+ console.log(`${Colors.FAIL}❌ Clé manquante. Abandon.${Colors.ENDC}`);
80
89
  process.exit(1);
81
90
  }
82
91
  }
@@ -96,14 +105,27 @@ class ConfigManager {
96
105
  // 🧠 MOTEUR IA (Client HTTPS Natif)
97
106
  // ==============================================================================
98
107
  class AIEngine {
99
- constructor(apiKey, model) {
108
+ constructor(apiKey, model, provider = 'groq') {
100
109
  this.apiKey = apiKey;
101
- this.hostname = "api.groq.com";
102
- this.path = "/openai/v1/chat/completions";
103
110
  this.model = model || DEFAULT_MODEL;
111
+ this.provider = provider;
112
+
113
+ // Configuration des providers
114
+ this.providers = {
115
+ groq: {
116
+ hostname: "api.groq.com",
117
+ path: "/openai/v1/chat/completions"
118
+ },
119
+ openai: {
120
+ hostname: "api.openai.com",
121
+ path: "/v1/chat/completions"
122
+ }
123
+ };
104
124
  }
105
125
 
106
126
  async call(systemPrompt, userPrompt, temperature = 0.3, jsonMode = false) {
127
+ const config = this.providers[this.provider] || this.providers.groq;
128
+
107
129
  const payload = {
108
130
  model: this.model,
109
131
  messages: [
@@ -118,8 +140,8 @@ class AIEngine {
118
140
  }
119
141
 
120
142
  const options = {
121
- hostname: this.hostname,
122
- path: this.path,
143
+ hostname: config.hostname,
144
+ path: config.path,
123
145
  method: 'POST',
124
146
  headers: {
125
147
  'Authorization': `Bearer ${this.apiKey}`,
@@ -136,9 +158,9 @@ class AIEngine {
136
158
  if (res.statusCode >= 400) {
137
159
  try {
138
160
  const errBody = JSON.parse(data);
139
- reject(new Error(`API Error ${res.statusCode}: ${errBody.error?.message || data}`));
161
+ reject(new Error(`API Error ${res.statusCode} (${this.provider}): ${errBody.error?.message || data}`));
140
162
  } catch (e) {
141
- reject(new Error(`API Error ${res.statusCode}: ${data}`));
163
+ reject(new Error(`API Error ${res.statusCode} (${this.provider}): ${data}`));
142
164
  }
143
165
  } else {
144
166
  try {
@@ -165,6 +187,7 @@ function parseArgs() {
165
187
  const args = process.argv.slice(2);
166
188
  const flags = {
167
189
  model: null,
190
+ provider: null,
168
191
  noSecurity: false,
169
192
  dryRun: false,
170
193
  help: false
@@ -174,6 +197,9 @@ function parseArgs() {
174
197
  if (args[i] === '--model' && args[i + 1]) {
175
198
  flags.model = args[i + 1];
176
199
  i++;
200
+ } else if (args[i] === '--provider' && args[i + 1]) {
201
+ flags.provider = args[i + 1];
202
+ i++;
177
203
  } else if (args[i] === '--no-security') {
178
204
  flags.noSecurity = true;
179
205
  } else if (args[i] === '--dry-run') {
@@ -187,17 +213,22 @@ function parseArgs() {
187
213
 
188
214
  function showHelp() {
189
215
  console.log(`
190
- ${Colors.BOLD}${Colors.CYAN}GHOST CLI v0.1.0${Colors.ENDC}
191
- Assistant Git Intelligent basé sur l'IA (Groq)
216
+ ${Colors.BOLD}${Colors.CYAN}GHOST CLI v0.2.0${Colors.ENDC}
217
+ Assistant Git Intelligent basé sur l'IA (Groq/OpenAI)
192
218
 
193
219
  ${Colors.BOLD}USAGE:${Colors.ENDC}
194
220
  ghost [options]
195
221
 
196
222
  ${Colors.BOLD}OPTIONS:${Colors.ENDC}
197
- --model <name> Utiliser un modèle spécifique (ex: llama-3.1-8b-instant)
198
- --no-security Désactiver l'audit de sécurité
199
- --dry-run Générer le message sans effectuer le commit
200
- --help, -h Afficher cette aide
223
+ --model <name> Utiliser un modèle spécifique (ex: llama-3.1-8b-instant)
224
+ --provider <name> Choisir le fournisseur (groq [défaut], openai)
225
+ --no-security Désactiver l'audit de sécurité
226
+ --dry-run Générer le message sans effectuer le commit
227
+ --help, -h Afficher cette aide
228
+
229
+ ${Colors.BOLD}CONFIGURATION LOCALE (.ghostrc):${Colors.ENDC}
230
+ Créez un fichier ${Colors.CYAN}.ghostrc${Colors.ENDC} JSON dans votre projet pour personnaliser le prompt :
231
+ { "prompt": "Ton prompt personnalisé ici" }
201
232
  `);
202
233
  }
203
234
 
@@ -338,9 +369,22 @@ async function main() {
338
369
  }
339
370
 
340
371
  console.clear();
341
- console.log(`\n${Colors.BOLD}${Colors.CYAN} 👻 GHOST CLI ${Colors.ENDC}${Colors.DIM} v0.1.0${Colors.ENDC}`);
372
+ console.log(`\n${Colors.BOLD}${Colors.CYAN} 👻 GHOST CLI ${Colors.ENDC}${Colors.DIM} v0.2.0${Colors.ENDC}`);
342
373
  console.log(`${Colors.DIM} ─────────────────────────────────────${Colors.ENDC}\n`);
343
374
 
375
+ // 0. Chargement de la configuration locale .ghostrc
376
+ const localConfigPath = path.join(process.cwd(), '.ghostrc');
377
+ let customPrompt = null;
378
+ if (fs.existsSync(localConfigPath)) {
379
+ try {
380
+ const localConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf8'));
381
+ if (localConfig.prompt) customPrompt = localConfig.prompt;
382
+ console.log(`${Colors.DIM}📝 Configuration locale .ghostrc chargée${Colors.ENDC}`);
383
+ } catch (e) {
384
+ console.log(`${Colors.WARNING}⚠️ Erreur lecture .ghostrc: ${e.message}${Colors.ENDC}`);
385
+ }
386
+ }
387
+
344
388
  // 1. Vérification Git
345
389
  if (!checkGitRepo()) {
346
390
  console.log(`${Colors.FAIL}❌ Erreur : Ce dossier n'est pas un dépôt Git.${Colors.ENDC}`);
@@ -350,8 +394,9 @@ async function main() {
350
394
 
351
395
  const config = new ConfigManager();
352
396
  const apiKey = await config.getApiKey();
353
- const model = flags.model || config.getModel(); // Priorité au flag --model
354
- const ai = new AIEngine(apiKey, model);
397
+ const model = flags.model || config.getModel();
398
+ const provider = flags.provider || config.config.provider || 'groq';
399
+ const ai = new AIEngine(apiKey, model, provider);
355
400
 
356
401
  // 2. Récupération du Diff
357
402
  const { text: fullDiffText, map: diffMap, files: fileList } = getStagedDiff();
@@ -364,15 +409,36 @@ async function main() {
364
409
 
365
410
  // Affichage des fichiers détectés
366
411
  console.log(`${Colors.BOLD}📂 Fichiers détectés (${fileList.length}) :${Colors.ENDC}`);
367
- fileList.forEach(f => console.log(` ${Colors.DIM} ${f}${Colors.ENDC}`));
368
- console.log(""); // Saut de ligne
412
+ fileList.forEach((f, idx) => console.log(` ${Colors.DIM}${idx + 1}. ${f}${Colors.ENDC}`));
413
+ console.log("");
414
+
415
+ let selectedFiles = fileList;
416
+ let finalDiffText = fullDiffText;
417
+ let finalDiffMap = diffMap;
418
+
419
+ if (fileList.length > 1) {
420
+ const selection = await promptUser(`${Colors.BOLD}Sélectionnez les fichiers (ex: 1,3,5 ou 'all' [par défaut]) : ${Colors.ENDC}`);
421
+ if (selection && selection.toLowerCase() !== 'all' && selection.trim() !== '') {
422
+ const indices = selection.split(',').map(s => parseInt(s.trim()) - 1).filter(idx => idx >= 0 && idx < fileList.length);
423
+ if (indices.length > 0) {
424
+ selectedFiles = indices.map(idx => fileList[idx]);
425
+ finalDiffMap = {};
426
+ finalDiffText = "";
427
+ selectedFiles.forEach(f => {
428
+ finalDiffMap[f] = diffMap[f];
429
+ finalDiffText += `\n--- ${f} ---\n${diffMap[f]}\n`;
430
+ });
431
+ console.log(`${Colors.GREEN}✅ ${selectedFiles.length} fichier(s) sélectionné(s).${Colors.ENDC}\n`);
432
+ }
433
+ }
434
+ }
369
435
 
370
436
  // 3. Audit de Sécurité
371
437
  if (!flags.noSecurity) {
372
438
  process.stdout.write(`${Colors.BLUE}🛡️ [1/2] Audit de Sécurité... ${Colors.ENDC}`);
373
439
 
374
440
  const potentialLeaks = {};
375
- for (const [fname, content] of Object.entries(diffMap)) {
441
+ for (const [fname, content] of Object.entries(finalDiffMap)) {
376
442
  const suspects = scanForSecrets(content);
377
443
  if (suspects.length > 0) potentialLeaks[fname] = suspects;
378
444
  }
@@ -403,14 +469,14 @@ async function main() {
403
469
  }
404
470
 
405
471
  // 4. Génération
406
- const tokensEstimates = Math.ceil(fullDiffText.length / 4);
472
+ const tokensEstimates = Math.ceil(finalDiffText.length / 4);
407
473
  console.log(`${Colors.BLUE}⚡ [2/2] Génération du message... ${Colors.DIM}(~${tokensEstimates} tokens)${Colors.ENDC}`);
408
- console.log(`${Colors.DIM} Modèle utilisé : ${model}${Colors.ENDC}`);
474
+ console.log(`${Colors.DIM} Modèle utilisé : ${model} (${provider})${Colors.ENDC}`);
409
475
 
410
- const sysPrompt = "Tu es un assistant Git expert. Génère UNIQUEMENT un message de commit suivant la convention 'Conventional Commits' (ex: feat: add login). Sois concis, descriptif et professionnel. N'utilise pas de markdown (pas de backticks), pas de guillemets autour du message.";
476
+ const sysPrompt = customPrompt || "Tu es un assistant Git expert. Génère UNIQUEMENT un message de commit suivant la convention 'Conventional Commits' (ex: feat: add login). Sois concis, descriptif et professionnel. N'utilise pas de markdown (pas de backticks), pas de guillemets autour du message.";
411
477
 
412
478
  try {
413
- let commitMsg = await ai.call(sysPrompt, `Diff :\n${fullDiffText.substring(0, 12000)}`);
479
+ let commitMsg = await ai.call(sysPrompt, `Diff :\n${finalDiffText.substring(0, 12000)}`);
414
480
  commitMsg = commitMsg.trim().replace(/^['"`]|['"`]$/g, ''); // Nettoyage final
415
481
 
416
482
  console.log(`\n${Colors.CYAN}──────────────────────────────────────────────────${Colors.ENDC}`);
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "atlasia-ghost",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Utilitaire de verification, validation, et de proposition de commits basé sur l'IA",
5
5
  "main": "ghost.js",
6
+ "files": [
7
+ "ghost.js",
8
+ "LICENSE",
9
+ "README.md"
10
+ ],
6
11
  "bin": {
7
- "ghost": "./ghost.js"
12
+ "ghost": "ghost.js"
8
13
  },
9
14
  "scripts": {
10
15
  "test": "node test.js",
@@ -21,7 +26,7 @@
21
26
  "automate",
22
27
  "ai",
23
28
  "cli",
24
- "devsecops"
29
+ "npm"
25
30
  ],
26
31
  "author": "Adel Lamallam",
27
32
  "license": "MIT",
package/test.js DELETED
@@ -1,36 +0,0 @@
1
- const assert = require('assert');
2
- const { execSync } = require('child_process');
3
- const path = require('path');
4
-
5
- // Mock Colors if needed or import from ghost.js if it were exported
6
- const Colors = {
7
- CYAN: '', BOLD: '', ENDC: '', DIM: '', GREEN: '', FAIL: ''
8
- };
9
-
10
- console.log('🧪 Running Ghost CLI Tests...\n');
11
-
12
- try {
13
- // Test 1: Version check
14
- const packageJson = require('./package.json');
15
- assert.strictEqual(packageJson.version, '0.1.0', 'package.json version should be 0.1.0');
16
- console.log('✅ Test 1: package.json version is correct');
17
-
18
- // Test 2: Help flag
19
- const helpOutput = execSync('node ghost.js --help', { encoding: 'utf8' });
20
- assert.ok(helpOutput.includes('GHOST CLI v0.1.0'), 'Help output should contain version');
21
- assert.ok(helpOutput.includes('--model'), 'Help output should contain --model flag');
22
- assert.ok(helpOutput.includes('--no-security'), 'Help output should contain --no-security flag');
23
- assert.ok(helpOutput.includes('--dry-run'), 'Help output should contain --dry-run flag');
24
- console.log('✅ Test 2: Help flag output is correct');
25
-
26
- // Test 3: Internal logic (Entropy)
27
- // We can't easily import internal functions without exporting them
28
- // But we can test if the file is valid JS
29
- execSync('node --check ghost.js');
30
- console.log('✅ Test 3: ghost.js syntax is valid');
31
-
32
- console.log('\n🎉 All tests passed successfully!');
33
- } catch (error) {
34
- console.error(`\n❌ Test failed: ${error.message}`);
35
- process.exit(1);
36
- }