@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.
- 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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Config } from '../config/index';
|
|
2
|
+
import type { CommitlintRules } from '../commitlint/index';
|
|
3
|
+
import type { CommitSuggestion } from '../providers/openai';
|
|
4
|
+
|
|
5
|
+
export interface EnforceResult {
|
|
6
|
+
message: string;
|
|
7
|
+
enforced: boolean;
|
|
8
|
+
violations: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GenerateOptions {
|
|
12
|
+
diff: string;
|
|
13
|
+
files: string[];
|
|
14
|
+
config: Config;
|
|
15
|
+
rules?: CommitlintRules | null;
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GenerateResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
message?: string;
|
|
22
|
+
type?: CommitSuggestion['type'];
|
|
23
|
+
enforced: boolean;
|
|
24
|
+
retries: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Config } from '../config/index';
|
|
2
|
+
import type { CommitlintRules } from '../commitlint/index';
|
|
3
|
+
import type { PromptParts } from './types';
|
|
4
|
+
import { getStyleInstructions, getStyleInstructionsFromRules } from './templates';
|
|
5
|
+
import { buildSystemMessage } from './system';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds the user prompt for the AI based on diff and configuration.
|
|
9
|
+
*
|
|
10
|
+
* Note: diff filtering (smartFilterDiff) is handled by the caller before
|
|
11
|
+
* passing the diff in, or by the provider layer.
|
|
12
|
+
*/
|
|
13
|
+
export function buildPrompt(
|
|
14
|
+
diff: string,
|
|
15
|
+
config: Config,
|
|
16
|
+
filenames: string[],
|
|
17
|
+
commitlintRules?: CommitlintRules | null
|
|
18
|
+
): string {
|
|
19
|
+
const language = config.language === 'pt' ? 'português' : 'english';
|
|
20
|
+
const styleInstructions =
|
|
21
|
+
commitlintRules != null
|
|
22
|
+
? getStyleInstructionsFromRules(
|
|
23
|
+
config.commitStyle,
|
|
24
|
+
config.language,
|
|
25
|
+
commitlintRules
|
|
26
|
+
)
|
|
27
|
+
: getStyleInstructions(config.commitStyle, config.language);
|
|
28
|
+
|
|
29
|
+
const fileList =
|
|
30
|
+
filenames.length > 10
|
|
31
|
+
? `${filenames.length} arquivos: ${filenames.slice(0, 5).join(', ')}...`
|
|
32
|
+
: filenames.join(', ');
|
|
33
|
+
|
|
34
|
+
return `Gere mensagem de commit em ${language} (${config.commitStyle}).
|
|
35
|
+
|
|
36
|
+
Arquivos: ${fileList}
|
|
37
|
+
|
|
38
|
+
${styleInstructions}
|
|
39
|
+
|
|
40
|
+
Diff:
|
|
41
|
+
\`\`\`
|
|
42
|
+
${diff}
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
Mensagem:`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Builds both user and system prompt parts in a single call.
|
|
50
|
+
*/
|
|
51
|
+
export function buildPromptParts(
|
|
52
|
+
diff: string,
|
|
53
|
+
config: Config,
|
|
54
|
+
filenames: string[],
|
|
55
|
+
commitlintRules?: CommitlintRules | null
|
|
56
|
+
): PromptParts {
|
|
57
|
+
return {
|
|
58
|
+
user: buildPrompt(diff, config, filenames, commitlintRules),
|
|
59
|
+
system: buildSystemMessage(config, commitlintRules),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Config } from '../config/index';
|
|
2
|
+
import type { CommitlintRules } from '../commitlint/index';
|
|
3
|
+
import { DEFAULT_COMMIT_TYPES } from './templates';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a system-role message that strictly enforces commitlint rules.
|
|
7
|
+
* System messages carry higher precedence than user messages, making the AI
|
|
8
|
+
* more reliably follow hard constraints like header-max-length.
|
|
9
|
+
*
|
|
10
|
+
* Returns null when no commitlint rules are provided or no rules apply.
|
|
11
|
+
*/
|
|
12
|
+
export function buildSystemMessage(
|
|
13
|
+
config: Config,
|
|
14
|
+
commitlintRules?: CommitlintRules | null
|
|
15
|
+
): string | null {
|
|
16
|
+
if (!commitlintRules) return null;
|
|
17
|
+
|
|
18
|
+
const isPt = config.language === 'pt';
|
|
19
|
+
const rules: string[] = [];
|
|
20
|
+
|
|
21
|
+
if (config.commitStyle === 'conventional') {
|
|
22
|
+
const types = commitlintRules.typeEnum?.join(', ') ?? DEFAULT_COMMIT_TYPES;
|
|
23
|
+
rules.push(isPt ? `- Formato: tipo(escopo): descrição` : `- Format: type(scope): description`);
|
|
24
|
+
rules.push(isPt ? `- Tipos permitidos: ${types}` : `- Allowed types: ${types}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (commitlintRules.headerMaxLength != null) {
|
|
28
|
+
const maxLen = commitlintRules.headerMaxLength;
|
|
29
|
+
rules.push(
|
|
30
|
+
isPt
|
|
31
|
+
? `- CRÍTICO: o header (primeira linha) NÃO PODE exceder ${maxLen} caracteres. Conte os caracteres antes de responder.`
|
|
32
|
+
: `- CRITICAL: the header (first line) MUST NOT exceed ${maxLen} characters. Count characters before responding.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (commitlintRules.scopeEnum && commitlintRules.scopeEnum.length > 0) {
|
|
37
|
+
rules.push(
|
|
38
|
+
isPt
|
|
39
|
+
? `- Escopos permitidos: ${commitlintRules.scopeEnum.join(', ')}`
|
|
40
|
+
: `- Allowed scopes: ${commitlintRules.scopeEnum.join(', ')}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (commitlintRules.subjectCase) {
|
|
45
|
+
const { condition, cases } = commitlintRules.subjectCase;
|
|
46
|
+
const caseList = cases.join(', ');
|
|
47
|
+
if (condition === 'never') {
|
|
48
|
+
rules.push(isPt ? `- Subject nunca em: ${caseList}` : `- Subject must never be: ${caseList}`);
|
|
49
|
+
} else if (condition === 'always') {
|
|
50
|
+
rules.push(isPt ? `- Subject deve estar em: ${caseList}` : `- Subject must be in: ${caseList}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (commitlintRules.subjectFullStop) {
|
|
55
|
+
const { condition, value } = commitlintRules.subjectFullStop;
|
|
56
|
+
const char = value ?? '.';
|
|
57
|
+
if (condition === 'never') {
|
|
58
|
+
rules.push(isPt ? `- Não termine o subject com "${char}"` : `- Do not end subject with "${char}"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (rules.length === 0) return null;
|
|
63
|
+
|
|
64
|
+
const intro = isPt
|
|
65
|
+
? 'Você é um gerador de mensagens de commit. Siga ESTRITAMENTE as seguintes regras do commitlint do projeto. Responda APENAS com a mensagem de commit, sem explicações adicionais.'
|
|
66
|
+
: 'You are a commit message generator. STRICTLY follow the project commitlint rules below. Respond with ONLY the commit message, no additional explanation.';
|
|
67
|
+
|
|
68
|
+
return `${intro}\n\n${rules.join('\n')}`;
|
|
69
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { CommitlintRules } from '../commitlint/index';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_COMMIT_TYPES = 'feat, fix, docs, style, refactor, test, chore, build, ci';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds style instructions dynamically from parsed commitlint rules.
|
|
7
|
+
* Falls back to sensible defaults for any rules not explicitly configured.
|
|
8
|
+
*/
|
|
9
|
+
export function getStyleInstructionsFromRules(
|
|
10
|
+
style: string,
|
|
11
|
+
language: string,
|
|
12
|
+
rules: CommitlintRules
|
|
13
|
+
): string {
|
|
14
|
+
const isPt = language === 'pt';
|
|
15
|
+
const lines: string[] = [];
|
|
16
|
+
|
|
17
|
+
if (style === 'conventional') {
|
|
18
|
+
lines.push(
|
|
19
|
+
isPt
|
|
20
|
+
? '- Use formato: tipo(escopo): descrição'
|
|
21
|
+
: '- Use format: type(scope): description'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const types = rules.typeEnum?.join(', ') ?? DEFAULT_COMMIT_TYPES;
|
|
25
|
+
lines.push(
|
|
26
|
+
isPt ? `- Tipos válidos: ${types}` : `- Valid types: ${types}`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const maxLen = rules.headerMaxLength ?? 72;
|
|
30
|
+
lines.push(
|
|
31
|
+
isPt
|
|
32
|
+
? `- OBRIGATÓRIO: primeira linha deve ter no máximo ${maxLen} caracteres (regra header-max-length do commitlint)`
|
|
33
|
+
: `- REQUIRED: first line must not exceed ${maxLen} characters (commitlint header-max-length rule)`
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const exampleType = rules.typeEnum?.[0] ?? 'feat';
|
|
37
|
+
const examplePt = `${exampleType}(auth): adicionar validação de email`;
|
|
38
|
+
const exampleEn = `${exampleType}(auth): add email validation`;
|
|
39
|
+
if (isPt && examplePt.length <= maxLen) {
|
|
40
|
+
lines.push(`- Exemplo: "${examplePt}"`);
|
|
41
|
+
} else if (!isPt && exampleEn.length <= maxLen) {
|
|
42
|
+
lines.push(`- Example: "${exampleEn}"`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (rules.subjectCase) {
|
|
46
|
+
const { condition, cases } = rules.subjectCase;
|
|
47
|
+
const caseList = cases.join(', ');
|
|
48
|
+
if (condition === 'never') {
|
|
49
|
+
lines.push(
|
|
50
|
+
isPt
|
|
51
|
+
? `- Subject nunca em: ${caseList}`
|
|
52
|
+
: `- Subject must never be: ${caseList}`
|
|
53
|
+
);
|
|
54
|
+
} else if (condition === 'always') {
|
|
55
|
+
lines.push(
|
|
56
|
+
isPt
|
|
57
|
+
? `- Subject deve estar em: ${caseList}`
|
|
58
|
+
: `- Subject must be in: ${caseList}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (rules.subjectFullStop) {
|
|
64
|
+
const { condition, value } = rules.subjectFullStop;
|
|
65
|
+
const char = value ?? '.';
|
|
66
|
+
if (condition === 'never') {
|
|
67
|
+
lines.push(
|
|
68
|
+
isPt
|
|
69
|
+
? `- Não termine o subject com "${char}"`
|
|
70
|
+
: `- Do not end subject with "${char}"`
|
|
71
|
+
);
|
|
72
|
+
} else if (condition === 'always') {
|
|
73
|
+
lines.push(
|
|
74
|
+
isPt
|
|
75
|
+
? `- Termine o subject com "${char}"`
|
|
76
|
+
: `- End subject with "${char}"`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (rules.bodyLeadingBlank === true) {
|
|
82
|
+
lines.push(
|
|
83
|
+
isPt
|
|
84
|
+
? '- Adicione uma linha em branco entre o header e o body'
|
|
85
|
+
: '- Add a blank line between header and body'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (rules.scopeEnum && rules.scopeEnum.length > 0) {
|
|
90
|
+
lines.push(
|
|
91
|
+
isPt
|
|
92
|
+
? `- Escopos permitidos: ${rules.scopeEnum.join(', ')}`
|
|
93
|
+
: `- Allowed scopes: ${rules.scopeEnum.join(', ')}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
} else if (style === 'simple') {
|
|
97
|
+
const maxLen = rules.headerMaxLength ?? 50;
|
|
98
|
+
if (isPt) {
|
|
99
|
+
lines.push('- Use formato simples e direto');
|
|
100
|
+
lines.push('- Comece com verbo no infinitivo');
|
|
101
|
+
lines.push('- Exemplo: "corrigir validação de formulário"');
|
|
102
|
+
lines.push(`- Máximo ${maxLen} caracteres`);
|
|
103
|
+
} else {
|
|
104
|
+
lines.push('- Use simple and direct format');
|
|
105
|
+
lines.push('- Start with imperative verb');
|
|
106
|
+
lines.push('- Example: "fix form validation"');
|
|
107
|
+
lines.push(`- Maximum ${maxLen} characters`);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// detailed
|
|
111
|
+
const maxLen = rules.headerMaxLength ?? 72;
|
|
112
|
+
if (isPt) {
|
|
113
|
+
lines.push(`- Primeira linha: resumo em até ${maxLen} caracteres`);
|
|
114
|
+
lines.push('- Se necessário, adicione corpo explicativo');
|
|
115
|
+
lines.push('- Use presente do indicativo');
|
|
116
|
+
lines.push('- Seja descritivo mas conciso');
|
|
117
|
+
} else {
|
|
118
|
+
lines.push(`- First line: summary under ${maxLen} characters`);
|
|
119
|
+
lines.push('- Add explanatory body if needed');
|
|
120
|
+
lines.push('- Use imperative mood');
|
|
121
|
+
lines.push('- Be descriptive but concise');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns style instructions based on commit style and language (no commitlint rules).
|
|
130
|
+
*/
|
|
131
|
+
export function getStyleInstructions(style: string, language: string): string {
|
|
132
|
+
const instructions = {
|
|
133
|
+
pt: {
|
|
134
|
+
conventional: `- Use formato: tipo(escopo): descrição
|
|
135
|
+
- Tipos válidos: ${DEFAULT_COMMIT_TYPES}
|
|
136
|
+
- Exemplo: "feat(auth): adicionar validação de email"
|
|
137
|
+
- Mantenha a primeira linha com até 50 caracteres`,
|
|
138
|
+
|
|
139
|
+
simple: `- Use formato simples e direto
|
|
140
|
+
- Comece com verbo no infinitivo
|
|
141
|
+
- Exemplo: "corrigir validação de formulário"
|
|
142
|
+
- Máximo 50 caracteres`,
|
|
143
|
+
|
|
144
|
+
detailed: `- Primeira linha: resumo em até 50 caracteres
|
|
145
|
+
- Se necessário, adicione corpo explicativo
|
|
146
|
+
- Use presente do indicativo
|
|
147
|
+
- Seja descritivo mas conciso`,
|
|
148
|
+
},
|
|
149
|
+
en: {
|
|
150
|
+
conventional: `- Use format: type(scope): description
|
|
151
|
+
- Valid types: ${DEFAULT_COMMIT_TYPES}
|
|
152
|
+
- Example: "feat(auth): add email validation"
|
|
153
|
+
- Keep first line under 50 characters`,
|
|
154
|
+
|
|
155
|
+
simple: `- Use simple and direct format
|
|
156
|
+
- Start with imperative verb
|
|
157
|
+
- Example: "fix form validation"
|
|
158
|
+
- Maximum 50 characters`,
|
|
159
|
+
|
|
160
|
+
detailed: `- First line: summary under 50 characters
|
|
161
|
+
- Add explanatory body if needed
|
|
162
|
+
- Use imperative mood
|
|
163
|
+
- Be descriptive but concise`,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const lang = language === 'pt' ? 'pt' : 'en';
|
|
168
|
+
return (
|
|
169
|
+
instructions[lang][style as keyof typeof instructions.pt] ||
|
|
170
|
+
instructions[lang].conventional
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Config } from '../config/index';
|
|
2
|
+
import type { AIProvider } from './types';
|
|
3
|
+
import { OpenAIProvider } from './openai';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Factory que instancia o provider correto baseado em config.provider.
|
|
7
|
+
* Para adicionar novo provider: implementar AIProvider e adicionar case aqui.
|
|
8
|
+
*/
|
|
9
|
+
export function createProvider(config: Config): AIProvider {
|
|
10
|
+
switch (config.provider) {
|
|
11
|
+
case 'openai':
|
|
12
|
+
return new OpenAIProvider(config);
|
|
13
|
+
default: {
|
|
14
|
+
const _exhaustive: never = config.provider;
|
|
15
|
+
throw new Error(`Provider '${_exhaustive}' não suportado`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|