@gilbert_oliveira/commit-wizard 1.2.2 → 2.0.1

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/package.json CHANGED
@@ -1,86 +1,93 @@
1
1
  {
2
2
  "name": "@gilbert_oliveira/commit-wizard",
3
- "version": "1.2.2",
4
- "description": "🧙‍♂️ Gerador inteligente de mensagens de commit usando IA",
5
- "main": "dist/index.js",
3
+ "displayName": "Commit Wizard",
4
+ "publisher": "gilbert-oliveira",
5
+ "description": "CLI inteligente para gerar mensagens de commit usando OpenAI",
6
+ "version": "2.0.1",
7
+ "categories": [
8
+ "Other",
9
+ "SCM Providers"
10
+ ],
11
+ "main": "dist/commit-wizard.js",
12
+ "module": "index.ts",
6
13
  "type": "module",
7
14
  "bin": {
8
- "commit-wizard": "dist/index.js"
9
- },
10
- "publishConfig": {
11
- "access": "public"
15
+ "commit-wizard": "./dist/commit-wizard.js"
12
16
  },
13
17
  "scripts": {
14
- "start": "node dist/index.js",
15
- "dev": "tsc --watch",
16
- "build": "npm run lint && tsc",
17
- "test": "jest",
18
- "test:watch": "jest --watch",
19
- "test:coverage": "jest --coverage",
20
- "lint": "eslint src/**/*.ts",
21
- "lint:fix": "eslint src/**/*.ts --fix",
22
- "format": "prettier --write src/**/*.ts",
23
- "format:check": "prettier --check src/**/*.ts",
24
- "prepare": "npm run build",
25
- "prepack": "npm run build",
26
- "clean": "rimraf dist"
27
- },
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/gilbert-oliveira/commit-wizard.git"
18
+ "dev": "bun run bin/commit-wizard.ts",
19
+ "build": "bun build bin/commit-wizard.ts --outdir=dist --target=bun --minify",
20
+ "test": "bun test",
21
+ "test:watch": "bun test --watch",
22
+ "test:coverage": "bun test && node scripts/generate-lcov.js",
23
+ "test:coverage:report": "c8 --reporter=html --reporter=text --include='src/**/*.ts' --exclude='**/*.test.ts' bun test && open coverage/lcov-report/index.html",
24
+ "test:coverage:upload": "bun test && node scripts/generate-lcov.js && node scripts/upload-codecov.js",
25
+ "prepublishOnly": "bun run build && bun test",
26
+ "start": "bun run bin/commit-wizard.ts",
27
+ "ci:test": "bun test --reporter=verbose",
28
+ "ci:build": "bun run build",
29
+ "ci:lint": "bun run tsc --noEmit",
30
+ "ci:security": "bun audit",
31
+ "ci:integration": "bun test tests/integration.test.ts tests/smart-split.test.ts",
32
+ "release:patch": "./scripts/release.sh patch",
33
+ "release:minor": "./scripts/release.sh minor",
34
+ "release:major": "./scripts/release.sh major",
35
+ "type-check": "bun run tsc --noEmit",
36
+ "lint": "bun run eslint --fix",
37
+ "format": "bun run prettier --write ."
31
38
  },
32
39
  "keywords": [
33
- "commit",
34
- "wizard",
35
- "conventional",
36
- "commits",
37
- "conventional-commits",
38
- "commitizen",
39
- "commitlint",
40
- "husky",
41
- "lint-staged",
42
40
  "git",
41
+ "commit",
43
42
  "ai",
44
43
  "openai",
45
- "gpt"
44
+ "cli",
45
+ "automation",
46
+ "conventional-commits"
46
47
  ],
47
- "author": "Gilbert Oliveira <contato@gilbert.dev.br>",
48
- "engines": {
49
- "node": ">=18.0.0"
50
- },
48
+ "author": "Gilbert <contato@gilbert.dev.br>",
51
49
  "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/gilbert-oliveira/commit-wizard.git"
53
+ },
52
54
  "bugs": {
53
55
  "url": "https://github.com/gilbert-oliveira/commit-wizard/issues"
54
56
  },
55
57
  "homepage": "https://github.com/gilbert-oliveira/commit-wizard#readme",
58
+ "engines": {
59
+ "node": ">=18.0.0",
60
+ "bun": ">=1.0.0"
61
+ },
62
+ "files": [
63
+ "bin/",
64
+ "src/",
65
+ "dist/",
66
+ "README.md",
67
+ "LICENSE",
68
+ ".commit-wizardrc"
69
+ ],
56
70
  "dependencies": {
57
- "@types/node": "^22.14.1",
58
- "chalk": "^5.3.0",
59
- "cli-progress": "^3.12.0",
60
- "gpt-tokenizer": "^1.0.0",
61
- "inquirer": "^12.5.2",
62
- "ora": "^8.2.0",
63
- "ts-node": "^10.9.2",
64
- "typescript": "^5.8.3"
71
+ "@clack/prompts": "^0.11.0",
72
+ "clipboardy": "^4.0.0",
73
+ "simple-git": "^3.25.0",
74
+ "dotenv": "^17.2.0"
65
75
  },
66
76
  "devDependencies": {
67
- "@eslint/js": "^9.0.0",
68
- "@types/cli-progress": "^3.11.6",
69
- "@types/inquirer": "^9.0.7",
70
- "@types/jest": "^29.5.12",
71
- "@typescript-eslint/eslint-plugin": "^8.0.0",
72
- "@typescript-eslint/parser": "^8.0.0",
73
- "eslint": "^9.0.0",
74
- "eslint-config-prettier": "^9.1.0",
75
- "eslint-plugin-prettier": "^5.1.3",
76
- "jest": "^29.7.0",
77
- "prettier": "^3.2.5",
78
- "rimraf": "^5.0.5",
79
- "ts-jest": "^29.1.2"
77
+ "@types/bun": "latest",
78
+ "@types/node": "^24.0.13",
79
+ "@typescript-eslint/eslint-plugin": "^8.36.0",
80
+ "@typescript-eslint/parser": "^8.36.0",
81
+ "c8": "^10.1.3",
82
+ "chalk": "^5.3.0",
83
+ "eslint": "^9.30.1",
84
+ "prettier": "^3.6.2"
80
85
  },
81
- "files": [
82
- "dist",
83
- "README.md",
84
- "LICENSE"
85
- ]
86
+ "peerDependencies": {
87
+ "typescript": "^5"
88
+ },
89
+ "preferGlobal": true,
90
+ "publishConfig": {
91
+ "access": "public"
92
+ }
86
93
  }
@@ -0,0 +1,237 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import fs from 'fs';
3
+ import { join } from 'path';
4
+ import dotenv from 'dotenv';
5
+
6
+ // Carregar variáveis de ambiente
7
+ dotenv.config();
8
+
9
+ export interface Config {
10
+ openai: {
11
+ model: string;
12
+ maxTokens: number;
13
+ temperature: number;
14
+ apiKey?: string;
15
+ timeout: number;
16
+ retries: number;
17
+ };
18
+ language: string;
19
+ commitStyle: 'conventional' | 'simple' | 'detailed';
20
+ autoCommit: boolean;
21
+ splitCommits: boolean;
22
+ dryRun: boolean;
23
+ smartSplit: {
24
+ enabled: boolean;
25
+ minGroupSize: number;
26
+ maxGroups: number;
27
+ confidenceThreshold: number;
28
+ };
29
+ cache: {
30
+ enabled: boolean;
31
+ ttl: number; // Time to live em minutos
32
+ maxSize: number; // Número máximo de entradas
33
+ };
34
+ }
35
+
36
+ const DEFAULT_CONFIG: Config = {
37
+ openai: {
38
+ model: 'gpt-4o',
39
+ maxTokens: 150,
40
+ temperature: 0.7,
41
+ timeout: 30000, // 30 segundos
42
+ retries: 2,
43
+ },
44
+ language: 'pt',
45
+ commitStyle: 'conventional',
46
+ autoCommit: false,
47
+ splitCommits: false,
48
+ dryRun: false,
49
+ smartSplit: {
50
+ enabled: true,
51
+ minGroupSize: 1,
52
+ maxGroups: 5,
53
+ confidenceThreshold: 0.7,
54
+ },
55
+ cache: {
56
+ enabled: true,
57
+ ttl: 60, // 1 hora
58
+ maxSize: 100,
59
+ },
60
+ };
61
+
62
+ // Adicionar interface para userConfig
63
+ interface UserConfig {
64
+ openai?: Partial<Config['openai']>;
65
+ language?: string;
66
+ commitStyle?: Config['commitStyle'];
67
+ autoCommit?: boolean;
68
+ splitCommits?: boolean;
69
+ dryRun?: boolean;
70
+ smartSplit?: Partial<Config['smartSplit']>;
71
+ cache?: Partial<Config['cache']>;
72
+ }
73
+
74
+ export function loadConfig(configPath?: string): Config {
75
+ // Usar um caminho mais seguro para ambientes CI
76
+ let defaultConfigPath: string;
77
+ try {
78
+ defaultConfigPath = join(process.cwd(), '.commit-wizardrc');
79
+ } catch {
80
+ // Fallback para ambientes onde process.cwd() falha
81
+ defaultConfigPath = '/tmp/.commit-wizardrc';
82
+ }
83
+
84
+ const globalConfigPath = join(
85
+ process.env.HOME || process.env.USERPROFILE || '/tmp',
86
+ '.commit-wizardrc'
87
+ );
88
+
89
+ let config = { ...DEFAULT_CONFIG };
90
+
91
+ // Carregar configuração global primeiro
92
+ try {
93
+ if (existsSync(globalConfigPath)) {
94
+ const fileContent = readFileSync(globalConfigPath, 'utf-8');
95
+ const globalConfig = JSON.parse(fileContent);
96
+ config = mergeConfig(config, globalConfig);
97
+ }
98
+ } catch {
99
+ console.warn(`⚠️ Erro ao ler configuração global: Erro desconhecido`);
100
+ }
101
+
102
+ // Carregar configuração local (sobrescreve a global)
103
+ const actualConfigPath = configPath || defaultConfigPath;
104
+ try {
105
+ if (existsSync(actualConfigPath)) {
106
+ const fileContent = readFileSync(actualConfigPath, 'utf-8');
107
+ const userConfig = JSON.parse(fileContent);
108
+ config = mergeConfig(config, userConfig);
109
+ }
110
+ } catch {
111
+ console.warn(`⚠️ Erro ao ler .commit-wizardrc: Erro desconhecido`);
112
+ }
113
+
114
+ // Adicionar chave da OpenAI das variáveis de ambiente
115
+ config.openai.apiKey = process.env.OPENAI_API_KEY;
116
+
117
+ // Aplicar configurações de ambiente
118
+ if (process.env.COMMIT_WIZARD_DEBUG === 'true') {
119
+ // config.advanced.enableDebug = true; // Removed advanced options
120
+ // config.advanced.logLevel = 'debug'; // Removed advanced options
121
+ }
122
+
123
+ if (process.env.COMMIT_WIZARD_DRY_RUN === 'true') {
124
+ config.dryRun = true;
125
+ }
126
+
127
+ return config;
128
+ }
129
+
130
+ function mergeConfig(defaultConfig: Config, userConfig: UserConfig): Config {
131
+ return {
132
+ ...defaultConfig,
133
+ ...userConfig,
134
+ openai: {
135
+ ...defaultConfig.openai,
136
+ ...userConfig.openai,
137
+ },
138
+ smartSplit: {
139
+ ...defaultConfig.smartSplit,
140
+ ...userConfig.smartSplit,
141
+ },
142
+ cache: {
143
+ ...defaultConfig.cache,
144
+ ...userConfig.cache,
145
+ },
146
+ };
147
+ }
148
+
149
+ export function validateConfig(config: Config): string[] {
150
+ const errors: string[] = [];
151
+
152
+ // Validação básica
153
+ if (!config.openai.apiKey) {
154
+ errors.push('OPENAI_API_KEY não encontrada nas variáveis de ambiente');
155
+ }
156
+
157
+ if (config.openai.maxTokens < 10 || config.openai.maxTokens > 4000) {
158
+ errors.push('maxTokens deve estar entre 10 e 4000');
159
+ }
160
+
161
+ if (config.openai.temperature < 0 || config.openai.temperature > 2) {
162
+ errors.push('temperature deve estar entre 0 e 2');
163
+ }
164
+
165
+ if (
166
+ !['pt', 'en', 'es', 'fr', 'de', 'it', 'ja', 'ko', 'zh'].includes(
167
+ config.language
168
+ )
169
+ ) {
170
+ errors.push(
171
+ 'language deve ser um idioma suportado (pt, en, es, fr, de, it, ja, ko, zh)'
172
+ );
173
+ }
174
+
175
+ if (!['conventional', 'simple', 'detailed'].includes(config.commitStyle)) {
176
+ errors.push('commitStyle deve ser conventional, simple ou detailed');
177
+ }
178
+
179
+ // Validação Smart Split
180
+ if (config.smartSplit.minGroupSize < 1) {
181
+ errors.push('smartSplit.minGroupSize deve ser pelo menos 1');
182
+ }
183
+
184
+ if (config.smartSplit.maxGroups < 1 || config.smartSplit.maxGroups > 10) {
185
+ errors.push('smartSplit.maxGroups deve estar entre 1 e 10');
186
+ }
187
+
188
+ if (
189
+ config.smartSplit.confidenceThreshold < 0 ||
190
+ config.smartSplit.confidenceThreshold > 1
191
+ ) {
192
+ errors.push('smartSplit.confidenceThreshold deve estar entre 0 e 1');
193
+ }
194
+
195
+ // Validação Cache
196
+ if (config.cache.ttl < 1) {
197
+ errors.push('cache.ttl deve ser pelo menos 1 minuto');
198
+ }
199
+
200
+ if (config.cache.maxSize < 1) {
201
+ errors.push('cache.maxSize deve ser pelo menos 1');
202
+ }
203
+
204
+ return errors;
205
+ }
206
+
207
+ /**
208
+ * Cria um arquivo de configuração exemplo
209
+ */
210
+ export function createExampleConfig(path: string = '.commit-wizardrc'): void {
211
+ const exampleConfig = {
212
+ language: 'pt',
213
+ commitStyle: 'conventional',
214
+ autoCommit: false,
215
+ splitCommits: true,
216
+ openai: {
217
+ model: 'gpt-4o',
218
+ maxTokens: 200,
219
+ temperature: 0.7,
220
+ timeout: 30000,
221
+ retries: 2,
222
+ },
223
+ smartSplit: {
224
+ enabled: true,
225
+ minGroupSize: 1,
226
+ maxGroups: 5,
227
+ confidenceThreshold: 0.7,
228
+ },
229
+ cache: {
230
+ enabled: true,
231
+ ttl: 60,
232
+ maxSize: 100,
233
+ },
234
+ };
235
+
236
+ fs.writeFileSync(path, JSON.stringify(exampleConfig, null, 2));
237
+ }
@@ -0,0 +1,210 @@
1
+ import type { Config } from '../config/index.ts';
2
+ import type { FileGroup } from './smart-split.ts';
3
+ import crypto from 'crypto';
4
+
5
+ export interface CacheEntry {
6
+ groups: FileGroup[];
7
+ timestamp: number;
8
+ hash: string;
9
+ }
10
+
11
+ export interface CacheResult {
12
+ hit: boolean;
13
+ groups?: FileGroup[];
14
+ }
15
+
16
+ class AnalysisCache {
17
+ private cache: Map<string, CacheEntry> = new Map();
18
+ private config: Config;
19
+
20
+ constructor(config: Config) {
21
+ this.config = config;
22
+ }
23
+
24
+ /**
25
+ * Gera hash do contexto para identificar análises similares
26
+ */
27
+ private generateHash(files: string[], overallDiff: string): string {
28
+ const context = {
29
+ files: files.sort(), // Ordenar para consistência
30
+ diff: overallDiff.substring(0, 1000), // Limitar tamanho do diff
31
+ model: this.config.openai.model,
32
+ temperature: this.config.openai.temperature,
33
+ };
34
+
35
+ return crypto
36
+ .createHash('md5')
37
+ .update(JSON.stringify(context))
38
+ .digest('hex');
39
+ }
40
+
41
+ /**
42
+ * Verifica se há cache válido para o contexto
43
+ */
44
+ get(files: string[], overallDiff: string): CacheResult {
45
+ if (!this.config.cache.enabled) {
46
+ return { hit: false };
47
+ }
48
+
49
+ const hash = this.generateHash(files, overallDiff);
50
+ const entry = this.cache.get(hash);
51
+
52
+ if (!entry) {
53
+ return { hit: false };
54
+ }
55
+
56
+ // Verificar se o cache expirou
57
+ const now = Date.now();
58
+ const ttlMs = this.config.cache.ttl * 60 * 1000; // Converter minutos para ms
59
+
60
+ if (now - entry.timestamp > ttlMs) {
61
+ this.cache.delete(hash);
62
+ return { hit: false };
63
+ }
64
+
65
+ return { hit: true, groups: entry.groups };
66
+ }
67
+
68
+ /**
69
+ * Armazena resultado no cache
70
+ */
71
+ set(files: string[], overallDiff: string, groups: FileGroup[]): void {
72
+ if (!this.config.cache.enabled) {
73
+ return;
74
+ }
75
+
76
+ // Limpar cache se exceder tamanho máximo
77
+ if (this.cache.size >= this.config.cache.maxSize) {
78
+ this.cleanup();
79
+ }
80
+
81
+ // Se ainda exceder após cleanup, não adicionar
82
+ if (this.cache.size >= this.config.cache.maxSize) {
83
+ return;
84
+ }
85
+
86
+ const hash = this.generateHash(files, overallDiff);
87
+ const entry: CacheEntry = {
88
+ groups,
89
+ timestamp: Date.now(),
90
+ hash,
91
+ };
92
+
93
+ this.cache.set(hash, entry);
94
+ }
95
+
96
+ /**
97
+ * Limpa cache expirado e reduz tamanho se necessário
98
+ */
99
+ private cleanup(): void {
100
+ const now = Date.now();
101
+ const ttlMs = this.config.cache.ttl * 60 * 1000;
102
+
103
+ // Remover entradas expiradas
104
+ for (const [hash, entry] of this.cache.entries()) {
105
+ if (now - entry.timestamp > ttlMs) {
106
+ this.cache.delete(hash);
107
+ }
108
+ }
109
+
110
+ // Se ainda exceder tamanho máximo, remover entradas mais antigas
111
+ if (this.cache.size >= this.config.cache.maxSize) {
112
+ const entries = Array.from(this.cache.entries()).sort(
113
+ (a, b) => a[1].timestamp - b[1].timestamp
114
+ );
115
+
116
+ // Remover 50% das entradas mais antigas para garantir espaço
117
+ const toRemove = entries.slice(
118
+ 0,
119
+ Math.ceil(this.config.cache.maxSize * 0.5)
120
+ );
121
+ for (const [hash] of toRemove) {
122
+ this.cache.delete(hash);
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Limpa todo o cache
129
+ */
130
+ clear(): void {
131
+ this.cache.clear();
132
+ }
133
+
134
+ /**
135
+ * Retorna estatísticas do cache
136
+ */
137
+ getStats(): { size: number; maxSize: number; enabled: boolean } {
138
+ return {
139
+ size: this.cache.size,
140
+ maxSize: this.config.cache.maxSize,
141
+ enabled: this.config.cache.enabled,
142
+ };
143
+ }
144
+ }
145
+
146
+ // Instância global do cache
147
+ let globalCache: AnalysisCache | null = null;
148
+
149
+ /**
150
+ * Inicializa o cache global
151
+ */
152
+ export function initializeCache(config: Config): void {
153
+ globalCache = new AnalysisCache(config);
154
+ }
155
+
156
+ /**
157
+ * Obtém o cache global
158
+ */
159
+ export function getCache(): AnalysisCache | null {
160
+ return globalCache;
161
+ }
162
+
163
+ /**
164
+ * Verifica se há cache válido para o contexto
165
+ */
166
+ export function getCachedAnalysis(
167
+ files: string[],
168
+ overallDiff: string
169
+ ): CacheResult {
170
+ const cache = getCache();
171
+ return cache ? cache.get(files, overallDiff) : { hit: false };
172
+ }
173
+
174
+ /**
175
+ * Armazena resultado no cache
176
+ */
177
+ export function setCachedAnalysis(
178
+ files: string[],
179
+ overallDiff: string,
180
+ groups: FileGroup[]
181
+ ): void {
182
+ const cache = getCache();
183
+ if (cache) {
184
+ cache.set(files, overallDiff, groups);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Retorna estatísticas do cache
190
+ */
191
+ export function getCacheStats(): {
192
+ size: number;
193
+ maxSize: number;
194
+ enabled: boolean;
195
+ } | null {
196
+ const cache = getCache();
197
+ return cache ? cache.getStats() : null;
198
+ }
199
+
200
+ /**
201
+ * Limpa o cache global
202
+ */
203
+ export function clearCache(): void {
204
+ const cache = getCache();
205
+ if (cache) {
206
+ cache.clear();
207
+ }
208
+ // Resetar a instância global
209
+ globalCache = null;
210
+ }