@gilbert_oliveira/commit-wizard 2.12.3-canary.1 → 2.12.3-canary.2

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,253 @@
1
+ import { existsSync, writeFileSync, readFileSync } from 'fs';
2
+ import { join, extname, basename } from 'path';
3
+ import { spawnSync } from 'child_process';
4
+
5
+ export interface CommitlintResult {
6
+ valid: boolean;
7
+ errors: string[];
8
+ warnings: string[];
9
+ }
10
+
11
+ const COMMITLINT_CONFIG_FILES = [
12
+ 'commitlint.config.js',
13
+ 'commitlint.config.ts',
14
+ 'commitlint.config.mjs',
15
+ 'commitlint.config.cjs',
16
+ '.commitlintrc',
17
+ '.commitlintrc.js',
18
+ '.commitlintrc.json',
19
+ '.commitlintrc.yml',
20
+ '.commitlintrc.yaml',
21
+ ];
22
+
23
+ export interface CommitlintRules {
24
+ typeEnum?: string[];
25
+ headerMaxLength?: number;
26
+ subjectCase?: { severity: number; condition: string; cases: string[] };
27
+ subjectFullStop?: { severity: number; condition: string; value: string };
28
+ bodyLeadingBlank?: boolean;
29
+ scopeEnum?: string[];
30
+ }
31
+
32
+ function extractRulesFromConfig(config: unknown): CommitlintRules {
33
+ const rules: CommitlintRules = {};
34
+ const rawRules =
35
+ (config as { rules?: Record<string, unknown[]> })?.rules ?? {};
36
+
37
+ const typeEnumRule = rawRules['type-enum'];
38
+ if (Array.isArray(typeEnumRule) && Array.isArray(typeEnumRule[2])) {
39
+ rules.typeEnum = typeEnumRule[2] as string[];
40
+ }
41
+
42
+ const headerMaxLengthRule = rawRules['header-max-length'];
43
+ if (
44
+ Array.isArray(headerMaxLengthRule) &&
45
+ typeof headerMaxLengthRule[2] === 'number'
46
+ ) {
47
+ rules.headerMaxLength = headerMaxLengthRule[2];
48
+ }
49
+
50
+ const subjectCaseRule = rawRules['subject-case'];
51
+ if (Array.isArray(subjectCaseRule)) {
52
+ const [severity, condition, cases] = subjectCaseRule;
53
+ if (Array.isArray(cases)) {
54
+ rules.subjectCase = {
55
+ severity: severity as number,
56
+ condition: condition as string,
57
+ cases: cases as string[],
58
+ };
59
+ } else if (typeof cases === 'string') {
60
+ rules.subjectCase = {
61
+ severity: severity as number,
62
+ condition: condition as string,
63
+ cases: [cases],
64
+ };
65
+ }
66
+ }
67
+
68
+ const subjectFullStopRule = rawRules['subject-full-stop'];
69
+ if (Array.isArray(subjectFullStopRule)) {
70
+ const [severity, condition, value] = subjectFullStopRule;
71
+ rules.subjectFullStop = {
72
+ severity: severity as number,
73
+ condition: condition as string,
74
+ value: (value as string) ?? '.',
75
+ };
76
+ }
77
+
78
+ const bodyLeadingBlankRule = rawRules['body-leading-blank'];
79
+ if (Array.isArray(bodyLeadingBlankRule)) {
80
+ rules.bodyLeadingBlank = bodyLeadingBlankRule[1] === 'always';
81
+ }
82
+
83
+ const scopeEnumRule = rawRules['scope-enum'];
84
+ if (Array.isArray(scopeEnumRule) && Array.isArray(scopeEnumRule[2])) {
85
+ rules.scopeEnum = scopeEnumRule[2] as string[];
86
+ }
87
+
88
+ return rules;
89
+ }
90
+
91
+ /**
92
+ * Reads and parses a commitlint config file, returning the extracted rules.
93
+ * Returns null if the file cannot be parsed or the format is unsupported.
94
+ */
95
+ export function parseCommitlintRules(configPath: string): CommitlintRules | null {
96
+ try {
97
+ const ext = extname(configPath).toLowerCase();
98
+ const base = basename(configPath);
99
+
100
+ // JSON format: .commitlintrc.json or .commitlintrc (extensionless, try JSON)
101
+ if (ext === '.json' || (ext === '' && base === '.commitlintrc')) {
102
+ try {
103
+ const content = readFileSync(configPath, 'utf-8');
104
+ const config = JSON.parse(content) as unknown;
105
+ return extractRulesFromConfig(config);
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // JS / MJS / CJS: use a Node.js subprocess to evaluate the config
112
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
113
+ // Try ESM dynamic import first (handles export default)
114
+ const esmScript = [
115
+ "import { pathToFileURL } from 'url';",
116
+ `const mod = await import(pathToFileURL(${JSON.stringify(configPath)}).href);`,
117
+ 'const cfg = mod.default ?? mod;',
118
+ 'process.stdout.write(JSON.stringify(cfg));',
119
+ ].join('\n');
120
+
121
+ const esmResult = spawnSync(process.execPath, ['--input-type=module'], {
122
+ input: esmScript,
123
+ encoding: 'utf-8',
124
+ timeout: 5000,
125
+ });
126
+
127
+ if (esmResult.status === 0 && esmResult.stdout) {
128
+ try {
129
+ const config = JSON.parse(esmResult.stdout) as unknown;
130
+ return extractRulesFromConfig(config);
131
+ } catch {
132
+ // JSON parse error – fall through to CJS
133
+ }
134
+ }
135
+
136
+ // Fallback: try CommonJS require
137
+ const cjsScript = [
138
+ 'try {',
139
+ ` const c = require(${JSON.stringify(configPath)});`,
140
+ ' process.stdout.write(JSON.stringify(c.default ?? c));',
141
+ '} catch (e) {',
142
+ ' process.exit(1);',
143
+ '}',
144
+ ].join('\n');
145
+ const cjsResult = spawnSync(process.execPath, ['-e', cjsScript], {
146
+ encoding: 'utf-8',
147
+ timeout: 5000,
148
+ });
149
+
150
+ if (cjsResult.status === 0 && cjsResult.stdout) {
151
+ try {
152
+ const config = JSON.parse(cjsResult.stdout) as unknown;
153
+ return extractRulesFromConfig(config);
154
+ } catch {
155
+ // JSON parse error
156
+ }
157
+ }
158
+ }
159
+
160
+ // Unsupported format (e.g. YAML, TypeScript): return null gracefully
161
+ return null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ export function findCommitlintConfig(cwd?: string): string | null {
168
+ const dir = cwd || process.cwd();
169
+ for (const file of COMMITLINT_CONFIG_FILES) {
170
+ const fullPath = join(dir, file);
171
+ if (existsSync(fullPath)) {
172
+ return fullPath;
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+
178
+ export function isCommitlintInstalled(cwd?: string): boolean {
179
+ const dir = cwd || process.cwd();
180
+ const binPath = join(dir, 'node_modules', '.bin', 'commitlint');
181
+ return existsSync(binPath);
182
+ }
183
+
184
+ export function validateCommitMessage(
185
+ message: string,
186
+ cwd?: string
187
+ ): CommitlintResult {
188
+ const dir = cwd || process.cwd();
189
+ const binPath = join(dir, 'node_modules', '.bin', 'commitlint');
190
+
191
+ const result = spawnSync(binPath, [], {
192
+ input: message,
193
+ encoding: 'utf-8',
194
+ cwd: dir,
195
+ });
196
+
197
+ if (result.status === 0) {
198
+ return { valid: true, errors: [], warnings: [] };
199
+ }
200
+
201
+ const output = (result.stdout || '') + (result.stderr || '');
202
+ const lines = output.split('\n');
203
+ const errors: string[] = [];
204
+ const warnings: string[] = [];
205
+
206
+ for (const line of lines) {
207
+ const trimmed = line.trim();
208
+ if (!trimmed) continue;
209
+ if (trimmed.startsWith('✖') || trimmed.includes('[error]')) {
210
+ errors.push(trimmed);
211
+ } else if (trimmed.startsWith('⚠') || trimmed.includes('[warning]')) {
212
+ warnings.push(trimmed);
213
+ }
214
+ }
215
+
216
+ // If we couldn't parse individual errors, extract meaningful lines only
217
+ // (skip stack trace noise: file paths, `at ...` frames, `throw err;`, etc.)
218
+ if (errors.length === 0 && result.status !== 0) {
219
+ const rawErrors = lines
220
+ .map((l) => l.trim())
221
+ .filter((l) => {
222
+ if (!l.length) return false;
223
+ if (l.startsWith('⧗') || l.startsWith('✔')) return false;
224
+ // Skip Node.js stack trace lines
225
+ if (l.startsWith('at ')) return false;
226
+ if (l.startsWith('file:///') || l.match(/^[^:]+\.(?:js|ts|mjs|cjs):\d+$/)) return false;
227
+ if (l === '^' || l === 'throw err;') return false;
228
+ if (l.startsWith('Node.js v')) return false;
229
+ return true;
230
+ });
231
+ errors.push(...rawErrors);
232
+ }
233
+
234
+ return { valid: false, errors, warnings };
235
+ }
236
+
237
+ export function initCommitlintConfig(cwd?: string): void {
238
+ const dir = cwd || process.cwd();
239
+ const configPath = join(dir, 'commitlint.config.js');
240
+
241
+ if (existsSync(configPath)) {
242
+ throw new Error(
243
+ 'commitlint.config.js já existe neste diretório. Remova-o antes de continuar.'
244
+ );
245
+ }
246
+
247
+ const content = `export default {
248
+ extends: ['@commitlint/config-conventional'],
249
+ };
250
+ `;
251
+
252
+ writeFileSync(configPath, content, 'utf-8');
253
+ }
@@ -5,7 +5,10 @@ import { join } from 'path';
5
5
 
6
6
  // Removido: dotenv.config();
7
7
 
8
+ export type ProviderType = 'openai';
9
+
8
10
  export interface Config {
11
+ provider: ProviderType;
9
12
  openai: {
10
13
  model: string;
11
14
  maxTokens: number;
@@ -30,9 +33,13 @@ export interface Config {
30
33
  ttl: number; // Time to live em minutos
31
34
  maxSize: number; // Número máximo de entradas
32
35
  };
36
+ commitlint: {
37
+ enabled: boolean;
38
+ };
33
39
  }
34
40
 
35
41
  const DEFAULT_CONFIG: Config = {
42
+ provider: 'openai',
36
43
  openai: {
37
44
  model: 'gpt-4o',
38
45
  maxTokens: 150,
@@ -56,10 +63,14 @@ const DEFAULT_CONFIG: Config = {
56
63
  ttl: 60, // 1 hora
57
64
  maxSize: 100,
58
65
  },
66
+ commitlint: {
67
+ enabled: true,
68
+ },
59
69
  };
60
70
 
61
71
  // Adicionar interface para userConfig
62
72
  interface UserConfig {
73
+ provider?: ProviderType;
63
74
  openai?: Partial<Config['openai']>;
64
75
  language?: string;
65
76
  commitStyle?: Config['commitStyle'];
@@ -68,6 +79,7 @@ interface UserConfig {
68
79
  dryRun?: boolean;
69
80
  smartSplit?: Partial<Config['smartSplit']>;
70
81
  cache?: Partial<Config['cache']>;
82
+ commitlint?: Partial<Config['commitlint']>;
71
83
  }
72
84
 
73
85
  export function loadConfig(configPath?: string): Config {
@@ -142,12 +154,25 @@ function mergeConfig(defaultConfig: Config, userConfig: UserConfig): Config {
142
154
  ...defaultConfig.cache,
143
155
  ...userConfig.cache,
144
156
  },
157
+ commitlint: {
158
+ ...defaultConfig.commitlint,
159
+ ...userConfig.commitlint,
160
+ },
145
161
  };
146
162
  }
147
163
 
164
+ const SUPPORTED_PROVIDERS: ProviderType[] = ['openai'];
165
+
148
166
  export function validateConfig(config: Config): string[] {
149
167
  const errors: string[] = [];
150
168
 
169
+ // Validação de provider
170
+ if (!SUPPORTED_PROVIDERS.includes(config.provider)) {
171
+ errors.push(
172
+ `provider deve ser um dos suportados: ${SUPPORTED_PROVIDERS.join(', ')}`
173
+ );
174
+ }
175
+
151
176
  // Validação básica
152
177
  if (!config.openai.apiKey) {
153
178
  errors.push('OPENAI_API_KEY não encontrada nas variáveis de ambiente');
package/src/core/cache.ts CHANGED
@@ -1,210 +1,14 @@
1
- import type { Config } from '../config/index';
2
- import type { FileGroup } from './smart-split';
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
1
  /**
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
- }
2
+ * Re-exports from src/cache/analysis.ts for backwards compatibility.
3
+ * This file is kept as a shim during the v3 migration.
4
+ * Consumers should migrate to importing directly from ../cache/analysis.
5
+ */
6
+ export type { CacheEntry, CacheResult } from '../cache/analysis';
7
+ export {
8
+ initializeCache,
9
+ getCache,
10
+ getCachedAnalysis,
11
+ setCachedAnalysis,
12
+ getCacheStats,
13
+ clearCache,
14
+ } from '../cache/analysis';