@gilbert_oliveira/commit-wizard 2.12.2-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.
- package/README.md +2 -0
- package/dist/commit-wizard.js +85 -65
- package/package.json +4 -2
- package/src/bin/commit-wizard.ts +12 -11
- package/src/cache/analysis.ts +210 -0
- package/src/commands/commit.ts +410 -0
- package/src/commands/init.ts +17 -0
- package/src/commands/version.ts +5 -0
- package/src/commitlint/index.ts +253 -0
- package/src/config/index.ts +25 -0
- package/src/core/cache.ts +13 -209
- package/src/core/index.ts +3 -385
- package/src/core/openai.ts +18 -465
- package/src/core/smart-split.ts +7 -722
- package/src/git/diff-filter.ts +118 -0
- package/src/pipeline/enforce.ts +152 -0
- package/src/pipeline/generate.ts +116 -0
- package/src/pipeline/split.ts +737 -0
- package/src/pipeline/types.ts +26 -0
- package/src/prompt/builder.ts +61 -0
- package/src/prompt/system.ts +69 -0
- package/src/prompt/templates.ts +172 -0
- package/src/prompt/types.ts +4 -0
- package/src/providers/factory.ts +18 -0
- package/src/providers/openai.ts +367 -0
- package/src/providers/types.ts +11 -0
- package/src/types/clack.d.ts +49 -4
- package/src/ui/format.ts +33 -0
- package/src/ui/index.ts +9 -5
- package/src/ui/smart-split.ts +1 -1
- package/src/ui/spinner.ts +23 -0
- package/src/ui/theme.ts +17 -0
- package/src/utils/args.ts +16 -8
- package/src/utils/hash.ts +9 -0
- package/src/utils/version.ts +11 -6
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { Config } from '../config/index';
|
|
2
|
+
import type { CommitlintRules } from '../commitlint/index';
|
|
3
|
+
import type { AIProvider } from './types';
|
|
4
|
+
import { buildPrompt } from '../prompt/builder';
|
|
5
|
+
import { buildSystemMessage } from '../prompt/system';
|
|
6
|
+
import { shouldIgnoreFile } from '../git/diff-filter';
|
|
7
|
+
|
|
8
|
+
export interface CommitSuggestion {
|
|
9
|
+
message: string;
|
|
10
|
+
type:
|
|
11
|
+
| 'feat'
|
|
12
|
+
| 'fix'
|
|
13
|
+
| 'docs'
|
|
14
|
+
| 'style'
|
|
15
|
+
| 'refactor'
|
|
16
|
+
| 'test'
|
|
17
|
+
| 'chore'
|
|
18
|
+
| 'build'
|
|
19
|
+
| 'ci';
|
|
20
|
+
confidence: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OpenAIResponse {
|
|
24
|
+
success: boolean;
|
|
25
|
+
suggestion?: CommitSuggestion;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prioriza e trunca diffs para caber no limite de tokens da IA.
|
|
31
|
+
* Deprioritiza lockfiles e arquivos de build (delegando detecção ao diff-filter.ts).
|
|
32
|
+
* Para filtragem completa de arquivos irrelevantes, use filterDiff() antes desta função.
|
|
33
|
+
*/
|
|
34
|
+
export function smartFilterDiff(diff: string, maxLength: number): string {
|
|
35
|
+
if (diff.length <= maxLength) {
|
|
36
|
+
return diff;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fileDiffs = diff.split(/(?=^diff --git)/m).filter((block) => block.trim());
|
|
40
|
+
|
|
41
|
+
const highPriorityDiffs: string[] = [];
|
|
42
|
+
const lowPriorityDiffs: string[] = [];
|
|
43
|
+
|
|
44
|
+
fileDiffs.forEach((fileDiff) => {
|
|
45
|
+
const headerMatch = fileDiff.match(/^diff --git a\/(.*?) b\//m);
|
|
46
|
+
const filename = headerMatch ? headerMatch[1]! : '';
|
|
47
|
+
if (shouldIgnoreFile(filename)) {
|
|
48
|
+
lowPriorityDiffs.push(fileDiff);
|
|
49
|
+
} else {
|
|
50
|
+
highPriorityDiffs.push(fileDiff);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let result = '';
|
|
55
|
+
let remainingLength = maxLength;
|
|
56
|
+
|
|
57
|
+
for (const fileDiff of highPriorityDiffs) {
|
|
58
|
+
if (fileDiff.length <= remainingLength) {
|
|
59
|
+
result += fileDiff;
|
|
60
|
+
remainingLength -= fileDiff.length;
|
|
61
|
+
} else if (remainingLength > 200) {
|
|
62
|
+
result += fileDiff.substring(0, remainingLength - 50);
|
|
63
|
+
remainingLength = maxLength - result.length;
|
|
64
|
+
break;
|
|
65
|
+
} else {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!result && lowPriorityDiffs.length > 0 && remainingLength > 0) {
|
|
71
|
+
for (const fileDiff of lowPriorityDiffs) {
|
|
72
|
+
if (fileDiff.length <= remainingLength) {
|
|
73
|
+
result += fileDiff;
|
|
74
|
+
remainingLength -= fileDiff.length;
|
|
75
|
+
} else if (remainingLength > 200) {
|
|
76
|
+
result += fileDiff.substring(0, remainingLength - 50);
|
|
77
|
+
remainingLength = maxLength - result.length;
|
|
78
|
+
break;
|
|
79
|
+
} else {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const optimizationMessage = '\n... (diff otimizado para focar em mudanças principais)';
|
|
86
|
+
if (remainingLength > 200 && lowPriorityDiffs.length > 0) {
|
|
87
|
+
const lowPrioritySummary = `\n\n... (${lowPriorityDiffs.length} arquivo(s) de dependências/build omitido(s): ${lowPriorityDiffs
|
|
88
|
+
.map((d) => {
|
|
89
|
+
const match = d.match(/^diff --git a\/(.*?) b\//);
|
|
90
|
+
return match ? match[1] : '';
|
|
91
|
+
})
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(', ')})`;
|
|
94
|
+
|
|
95
|
+
const availableSpace = remainingLength - optimizationMessage.length;
|
|
96
|
+
if (lowPrioritySummary.length < availableSpace) {
|
|
97
|
+
result += lowPrioritySummary;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const wasFiltered = result.trim() !== diff.trim();
|
|
102
|
+
|
|
103
|
+
return wasFiltered
|
|
104
|
+
? result + optimizationMessage
|
|
105
|
+
: result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extrai o tipo de commit da mensagem gerada pela OpenAI
|
|
110
|
+
*/
|
|
111
|
+
export function extractCommitTypeFromMessage(
|
|
112
|
+
message: string
|
|
113
|
+
): CommitSuggestion['type'] | null {
|
|
114
|
+
const typePatterns = {
|
|
115
|
+
feat: /^(feat|feature)(\([^)]+\))?:/i,
|
|
116
|
+
fix: /^(fix|bugfix)(\([^)]+\))?:/i,
|
|
117
|
+
docs: /^(docs|documentation)(\([^)]+\))?:/i,
|
|
118
|
+
style: /^(style|format)(\([^)]+\))?:/i,
|
|
119
|
+
refactor: /^(refactor|refactoring)(\([^)]+\))?:/i,
|
|
120
|
+
test: /^(test|testing)(\([^)]+\))?:/i,
|
|
121
|
+
chore: /^(chore|maintenance)(\([^)]+\))?:/i,
|
|
122
|
+
build: /^(build|ci)(\([^)]+\))?:/i,
|
|
123
|
+
ci: /^(ci|continuous-integration)(\([^)]+\))?:/i,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
for (const [type, pattern] of Object.entries(typePatterns)) {
|
|
127
|
+
if (pattern.test(message)) {
|
|
128
|
+
return type as CommitSuggestion['type'];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Detecta o tipo de commit baseado no diff
|
|
137
|
+
*/
|
|
138
|
+
export function detectCommitType(
|
|
139
|
+
diff: string,
|
|
140
|
+
filenames: string[]
|
|
141
|
+
): CommitSuggestion['type'] {
|
|
142
|
+
const diffLower = diff.toLowerCase();
|
|
143
|
+
const filesStr = filenames.join(' ').toLowerCase();
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
filesStr.includes('test') ||
|
|
147
|
+
filesStr.includes('spec') ||
|
|
148
|
+
diffLower.includes('test(')
|
|
149
|
+
) {
|
|
150
|
+
return 'test';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
filesStr.includes('readme') ||
|
|
155
|
+
filesStr.includes('.md') ||
|
|
156
|
+
filesStr.includes('docs')
|
|
157
|
+
) {
|
|
158
|
+
return 'docs';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
filesStr.includes('package.json') ||
|
|
163
|
+
filesStr.includes('dockerfile') ||
|
|
164
|
+
filesStr.includes('.yml') ||
|
|
165
|
+
filesStr.includes('.yaml') ||
|
|
166
|
+
filesStr.includes('webpack') ||
|
|
167
|
+
filesStr.includes('tsconfig')
|
|
168
|
+
) {
|
|
169
|
+
return 'build';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (
|
|
173
|
+
filesStr.includes('.css') ||
|
|
174
|
+
filesStr.includes('.scss') ||
|
|
175
|
+
diffLower.includes('style') ||
|
|
176
|
+
diffLower.includes('format')
|
|
177
|
+
) {
|
|
178
|
+
return 'style';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
diffLower.includes('fix') ||
|
|
183
|
+
diffLower.includes('bug') ||
|
|
184
|
+
diffLower.includes('error') ||
|
|
185
|
+
diffLower.includes('issue')
|
|
186
|
+
) {
|
|
187
|
+
return 'fix';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
diffLower.includes('add') ||
|
|
192
|
+
diffLower.includes('new') ||
|
|
193
|
+
diffLower.includes('create') ||
|
|
194
|
+
diffLower.includes('implement')
|
|
195
|
+
) {
|
|
196
|
+
return 'feat';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
diffLower.includes('refactor') ||
|
|
201
|
+
diffLower.includes('restructure') ||
|
|
202
|
+
diffLower.includes('rename')
|
|
203
|
+
) {
|
|
204
|
+
return 'refactor';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return 'chore';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Processa a mensagem retornada pela OpenAI removendo formatação desnecessária
|
|
212
|
+
*/
|
|
213
|
+
export function processOpenAIMessage(message: string): string {
|
|
214
|
+
if (message.startsWith('```')) {
|
|
215
|
+
const blockWithBodyMatch = message.match(/^```[^\n`]*\n([\s\S]*?)\n\s*```([\s\S]*)$/);
|
|
216
|
+
if (blockWithBodyMatch) {
|
|
217
|
+
const codeContent = blockWithBodyMatch[1].trim();
|
|
218
|
+
const afterBlock = blockWithBodyMatch[2].trim();
|
|
219
|
+
message = afterBlock ? `${codeContent}\n\n${afterBlock}` : codeContent;
|
|
220
|
+
} else if (message.match(/^```[\s\S]*```$/)) {
|
|
221
|
+
message = message
|
|
222
|
+
.replace(/^```(?:plaintext|javascript|typescript|python|java|html|css|json|xml|yaml|yml|bash|shell|text)?\s*/, '')
|
|
223
|
+
.replace(/\s*```$/, '');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
message = message.trim();
|
|
228
|
+
|
|
229
|
+
return message;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* OpenAIProvider implements the AIProvider interface for the OpenAI API.
|
|
234
|
+
*/
|
|
235
|
+
export class OpenAIProvider implements AIProvider {
|
|
236
|
+
constructor(private config: Config) {}
|
|
237
|
+
|
|
238
|
+
async generate(prompt: string, systemMessage: string | null): Promise<string> {
|
|
239
|
+
if (!this.config.openai.apiKey) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
'Chave da OpenAI não encontrada. Configure OPENAI_API_KEY nas variáveis de ambiente.'
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const messages: Array<{ role: string; content: string }> = [];
|
|
246
|
+
if (systemMessage) {
|
|
247
|
+
messages.push({ role: 'system', content: systemMessage });
|
|
248
|
+
}
|
|
249
|
+
messages.push({ role: 'user', content: prompt });
|
|
250
|
+
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
const timeoutId = setTimeout(
|
|
253
|
+
() => controller.abort(),
|
|
254
|
+
this.config.openai.timeout
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: {
|
|
261
|
+
Authorization: `Bearer ${this.config.openai.apiKey}`,
|
|
262
|
+
'Content-Type': 'application/json',
|
|
263
|
+
},
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
model: this.config.openai.model,
|
|
266
|
+
messages,
|
|
267
|
+
max_tokens: Math.min(this.config.openai.maxTokens, 150),
|
|
268
|
+
temperature: this.config.openai.temperature,
|
|
269
|
+
}),
|
|
270
|
+
signal: controller.signal,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
const errorData = (await response.json().catch(() => ({}))) as any;
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Erro da OpenAI (${response.status}): ${errorData.error?.message || 'Erro desconhecido'}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const data = (await response.json()) as any;
|
|
281
|
+
const message = data.choices?.[0]?.message?.content?.trim();
|
|
282
|
+
|
|
283
|
+
if (!message) {
|
|
284
|
+
throw new Error('OpenAI retornou resposta vazia');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return message;
|
|
288
|
+
} finally {
|
|
289
|
+
clearTimeout(timeoutId);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Consome a API da OpenAI para gerar mensagem de commit
|
|
296
|
+
*/
|
|
297
|
+
export async function generateCommitMessage(
|
|
298
|
+
diff: string,
|
|
299
|
+
config: Config,
|
|
300
|
+
filenames: string[],
|
|
301
|
+
commitlintRules?: CommitlintRules | null
|
|
302
|
+
): Promise<OpenAIResponse> {
|
|
303
|
+
try {
|
|
304
|
+
const provider = new OpenAIProvider(config);
|
|
305
|
+
|
|
306
|
+
// Filter diff before building the prompt
|
|
307
|
+
const maxDiffLength = 6000;
|
|
308
|
+
const filteredDiff = smartFilterDiff(diff, maxDiffLength);
|
|
309
|
+
|
|
310
|
+
const prompt = buildPrompt(filteredDiff, config, filenames, commitlintRules);
|
|
311
|
+
const systemMessage = buildSystemMessage(config, commitlintRules);
|
|
312
|
+
|
|
313
|
+
const rawMessage = await provider.generate(prompt, systemMessage);
|
|
314
|
+
const message = processOpenAIMessage(rawMessage);
|
|
315
|
+
|
|
316
|
+
const extractedType = extractCommitTypeFromMessage(message);
|
|
317
|
+
const fallbackType = detectCommitType(diff, filenames);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
suggestion: {
|
|
322
|
+
message,
|
|
323
|
+
type: extractedType || fallbackType,
|
|
324
|
+
confidence: 0.8,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
error: `Erro ao conectar com OpenAI: ${error instanceof Error ? error.message : 'Erro desconhecido'}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Gera mensagem com retry em caso de falha
|
|
337
|
+
*/
|
|
338
|
+
export async function generateWithRetry(
|
|
339
|
+
diff: string,
|
|
340
|
+
config: Config,
|
|
341
|
+
filenames: string[],
|
|
342
|
+
maxRetries: number = 3,
|
|
343
|
+
commitlintRules?: CommitlintRules | null
|
|
344
|
+
): Promise<OpenAIResponse> {
|
|
345
|
+
let lastError = '';
|
|
346
|
+
|
|
347
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
348
|
+
const result = await generateCommitMessage(diff, config, filenames, commitlintRules);
|
|
349
|
+
|
|
350
|
+
if (result.success) {
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
lastError = result.error || 'Erro desconhecido';
|
|
355
|
+
|
|
356
|
+
if (i < maxRetries - 1) {
|
|
357
|
+
await new Promise((resolve) =>
|
|
358
|
+
setTimeout(resolve, Math.pow(2, i) * 1000)
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
error: `Falha após ${maxRetries} tentativas. Último erro: ${lastError}`,
|
|
366
|
+
};
|
|
367
|
+
}
|
package/src/types/clack.d.ts
CHANGED
|
@@ -1,9 +1,54 @@
|
|
|
1
1
|
declare module '@clack/prompts' {
|
|
2
|
-
export const intro: (message
|
|
3
|
-
export const outro: (message
|
|
2
|
+
export const intro: (message?: string) => void;
|
|
3
|
+
export const outro: (message?: string) => void;
|
|
4
|
+
export const cancel: (message?: string) => void;
|
|
5
|
+
export const note: (message?: string, title?: string) => void;
|
|
6
|
+
export function isCancel(value: unknown): value is symbol;
|
|
7
|
+
|
|
4
8
|
export const log: {
|
|
5
|
-
|
|
9
|
+
message: (message?: string) => void;
|
|
6
10
|
info: (message: string) => void;
|
|
7
11
|
success: (message: string) => void;
|
|
12
|
+
step: (message: string) => void;
|
|
13
|
+
warn: (message: string) => void;
|
|
14
|
+
warning: (message: string) => void;
|
|
15
|
+
error: (message: string) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function text(opts: {
|
|
19
|
+
message: string;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
defaultValue?: string;
|
|
22
|
+
initialValue?: string;
|
|
23
|
+
validate?: (value: string) => string | Error | undefined;
|
|
24
|
+
}): Promise<string | symbol>;
|
|
25
|
+
|
|
26
|
+
export function confirm(opts: {
|
|
27
|
+
message: string;
|
|
28
|
+
active?: string;
|
|
29
|
+
inactive?: string;
|
|
30
|
+
initialValue?: boolean;
|
|
31
|
+
}): Promise<boolean | symbol>;
|
|
32
|
+
|
|
33
|
+
export function select<Value>(opts: {
|
|
34
|
+
message: string;
|
|
35
|
+
options: Array<{ value: Value; label?: string; hint?: string }>;
|
|
36
|
+
initialValue?: Value;
|
|
37
|
+
maxItems?: number;
|
|
38
|
+
}): Promise<Value | symbol>;
|
|
39
|
+
|
|
40
|
+
export function multiselect<Value>(opts: {
|
|
41
|
+
message: string;
|
|
42
|
+
options: Array<{ value: Value; label?: string; hint?: string }>;
|
|
43
|
+
initialValues?: Value[];
|
|
44
|
+
maxItems?: number;
|
|
45
|
+
required?: boolean;
|
|
46
|
+
cursorAt?: Value;
|
|
47
|
+
}): Promise<Value[] | symbol>;
|
|
48
|
+
|
|
49
|
+
export function spinner(opts?: { indicator?: 'dots' | 'timer' }): {
|
|
50
|
+
start: (msg?: string) => void;
|
|
51
|
+
stop: (msg?: string, code?: number) => void;
|
|
52
|
+
message: (msg?: string) => void;
|
|
8
53
|
};
|
|
9
|
-
}
|
|
54
|
+
}
|
package/src/ui/format.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { COLORS } from './theme';
|
|
2
|
+
|
|
3
|
+
const TYPE_COLORS: Record<string, string> = {
|
|
4
|
+
feat: COLORS.success,
|
|
5
|
+
fix: COLORS.error,
|
|
6
|
+
docs: COLORS.info,
|
|
7
|
+
style: COLORS.info,
|
|
8
|
+
refactor: COLORS.warning,
|
|
9
|
+
perf: COLORS.warning,
|
|
10
|
+
test: COLORS.info,
|
|
11
|
+
chore: COLORS.dim,
|
|
12
|
+
ci: COLORS.dim,
|
|
13
|
+
build: COLORS.dim,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Adiciona cor ANSI ao tipo de commit no header (feat, fix, docs, etc.).
|
|
18
|
+
* Preserva o restante da mensagem sem alteração.
|
|
19
|
+
*/
|
|
20
|
+
export function formatCommitPreview(message: string): string {
|
|
21
|
+
const lines = message.split('\n');
|
|
22
|
+
const header = lines[0];
|
|
23
|
+
|
|
24
|
+
const typeMatch = header.match(/^(\w+)(\([^)]+\))?:/);
|
|
25
|
+
if (typeMatch) {
|
|
26
|
+
const type = typeMatch[1];
|
|
27
|
+
const color = TYPE_COLORS[type] ?? COLORS.reset;
|
|
28
|
+
const coloredHeader = header.replace(type, `${color}${type}${COLORS.reset}`);
|
|
29
|
+
return [coloredHeader, ...lines.slice(1)].join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return message;
|
|
33
|
+
}
|
package/src/ui/index.ts
CHANGED
|
@@ -62,21 +62,25 @@ export async function showCommitPreview(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* Permite edição da mensagem de commit
|
|
65
|
+
* Permite edição da mensagem de commit com contador de caracteres opcional.
|
|
66
|
+
* @param maxLength - limite de caracteres para o header (primeira linha)
|
|
66
67
|
*/
|
|
67
68
|
export async function editCommitMessage(
|
|
68
|
-
originalMessage: string
|
|
69
|
+
originalMessage: string,
|
|
70
|
+
maxLength?: number
|
|
69
71
|
): Promise<UIAction> {
|
|
72
|
+
const limit = maxLength ?? 72;
|
|
70
73
|
const editedMessage = await text({
|
|
71
|
-
message:
|
|
74
|
+
message: `Edite a mensagem do commit:`,
|
|
72
75
|
initialValue: originalMessage,
|
|
73
76
|
placeholder: 'Digite a mensagem do commit...',
|
|
74
77
|
validate: (value) => {
|
|
75
78
|
if (!value || value.trim().length === 0) {
|
|
76
79
|
return 'A mensagem não pode estar vazia';
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const headerLength = value.split('\n')[0].length;
|
|
82
|
+
if (headerLength > limit) {
|
|
83
|
+
return `Header muito longo: ${headerLength}/${limit} caracteres`;
|
|
80
84
|
}
|
|
81
85
|
},
|
|
82
86
|
});
|
package/src/ui/smart-split.ts
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spinner } from '@clack/prompts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Envolve uma chamada assíncrona com spinner animado.
|
|
5
|
+
* Exibe tempo estimado opcional e ícone de sucesso/erro ao finalizar.
|
|
6
|
+
*/
|
|
7
|
+
export async function withSpinner<T>(
|
|
8
|
+
message: string,
|
|
9
|
+
fn: () => Promise<T>,
|
|
10
|
+
estimatedTime?: number
|
|
11
|
+
): Promise<T> {
|
|
12
|
+
const s = spinner();
|
|
13
|
+
s.start(estimatedTime ? `${message} (~${estimatedTime}s)` : message);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = await fn();
|
|
17
|
+
s.stop(`${message} ✅`);
|
|
18
|
+
return result;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
s.stop(`${message} ❌`);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/ui/theme.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const COLORS = {
|
|
2
|
+
success: '\x1b[32m',
|
|
3
|
+
warning: '\x1b[33m',
|
|
4
|
+
error: '\x1b[31m',
|
|
5
|
+
info: '\x1b[36m',
|
|
6
|
+
dim: '\x1b[2m',
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ICONS = {
|
|
11
|
+
success: '✅',
|
|
12
|
+
warning: '⚠️',
|
|
13
|
+
error: '❌',
|
|
14
|
+
info: 'ℹ️',
|
|
15
|
+
rocket: '🚀',
|
|
16
|
+
wizard: '🧙',
|
|
17
|
+
};
|
package/src/utils/args.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface CLIArgs {
|
|
|
7
7
|
dryRun: boolean;
|
|
8
8
|
help: boolean;
|
|
9
9
|
version: boolean;
|
|
10
|
+
validate: boolean;
|
|
11
|
+
initCommitlint: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function parseArgs(args: string[]): CLIArgs {
|
|
@@ -19,6 +21,8 @@ export function parseArgs(args: string[]): CLIArgs {
|
|
|
19
21
|
dryRun: args.includes('--dry-run') || args.includes('-n'),
|
|
20
22
|
help: args.includes('--help') || args.includes('-h'),
|
|
21
23
|
version: args.includes('--version') || args.includes('-v'),
|
|
24
|
+
validate: args.includes('--validate'),
|
|
25
|
+
initCommitlint: args.includes('--init-commitlint'),
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -30,14 +34,16 @@ USAGE:
|
|
|
30
34
|
commit-wizard [OPTIONS]
|
|
31
35
|
|
|
32
36
|
OPTIONS:
|
|
33
|
-
-s, --silent
|
|
34
|
-
-y, --yes
|
|
35
|
-
-a, --auto
|
|
36
|
-
--split
|
|
37
|
-
--smart-split
|
|
38
|
-
-n, --dry-run
|
|
39
|
-
|
|
40
|
-
-
|
|
37
|
+
-s, --silent Modo silencioso (sem logs detalhados)
|
|
38
|
+
-y, --yes Confirmar automaticamente sem prompts
|
|
39
|
+
-a, --auto Modo automático (--yes + --silent)
|
|
40
|
+
--split Modo split manual (commits separados por arquivo)
|
|
41
|
+
--smart-split Modo smart split (IA agrupa por contexto)
|
|
42
|
+
-n, --dry-run Visualizar mensagem sem fazer commit
|
|
43
|
+
--validate Habilitar validação com commitlint
|
|
44
|
+
--init-commitlint Criar configuração padrão do commitlint
|
|
45
|
+
-h, --help Mostrar esta ajuda
|
|
46
|
+
-v, --version Mostrar versão
|
|
41
47
|
|
|
42
48
|
EXAMPLES:
|
|
43
49
|
commit-wizard # Modo interativo padrão
|
|
@@ -46,6 +52,8 @@ EXAMPLES:
|
|
|
46
52
|
commit-wizard --smart-split # Smart split com IA
|
|
47
53
|
commit-wizard --dry-run # Apenas visualizar mensagem
|
|
48
54
|
commit-wizard --auto # Modo totalmente automático
|
|
55
|
+
commit-wizard --validate # Habilitar validação com commitlint
|
|
56
|
+
commit-wizard --init-commitlint # Criar configuração padrão do commitlint
|
|
49
57
|
|
|
50
58
|
Para mais informações, visite: https://github.com/user/commit-wizard
|
|
51
59
|
`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gera um hash SHA-256 hexadecimal de uma string.
|
|
5
|
+
* Usado principalmente para gerar chaves de cache a partir de diffs do git.
|
|
6
|
+
*/
|
|
7
|
+
export function hashString(content: string): string {
|
|
8
|
+
return createHash('sha256').update(content).digest('hex');
|
|
9
|
+
}
|
package/src/utils/version.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Lê a versão do package.json dinamicamente
|
|
6
7
|
*/
|
|
7
8
|
export function getVersion(): string {
|
|
8
9
|
try {
|
|
10
|
+
// Resolver __dirname de forma compatível com ESM e CJS
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
9
14
|
// Tentar diferentes caminhos para encontrar o package.json
|
|
10
15
|
const possiblePaths = [
|
|
11
|
-
// Caminho
|
|
12
|
-
join(
|
|
13
|
-
//
|
|
14
|
-
join(process.cwd(), '..', 'package.json'),
|
|
15
|
-
// Caminho absoluto baseado no __dirname
|
|
16
|
+
// Caminho absoluto baseado no __dirname (prioridade máxima - para instalação global)
|
|
17
|
+
join(__dirname, '..', 'package.json'),
|
|
18
|
+
// Fallback para estrutura de desenvolvimento/build alternativa
|
|
16
19
|
join(__dirname, '..', '..', 'package.json'),
|
|
20
|
+
// Último recurso: diretório atual
|
|
21
|
+
join(process.cwd(), 'package.json'),
|
|
17
22
|
];
|
|
18
23
|
|
|
19
24
|
for (const packagePath of possiblePaths) {
|