@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,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,4 @@
1
+ export interface PromptParts {
2
+ user: string;
3
+ system: string | null;
4
+ }
@@ -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
+ }