@mks2508/better-logger 5.0.0 → 5.0.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/dist/chunks/{Logger-Cbi1f10o.js → Logger-CKqzXDh3.js} +2 -2
- package/dist/chunks/{Logger-Cbi1f10o.js.map → Logger-CKqzXDh3.js.map} +1 -1
- package/dist/chunks/{Logger-BZ7B7OXE.js → Logger-D_vuKP4l.js} +2 -2
- package/dist/chunks/{Logger-BZ7B7OXE.js.map → Logger-D_vuKP4l.js.map} +1 -1
- package/dist/chunks/cli-module-B-njrTWb.js +2 -0
- package/dist/chunks/cli-module-B-njrTWb.js.map +1 -0
- package/dist/chunks/{cli-module-ClIJmaT8.js → cli-module-Cx7qotPY.js} +16 -9
- package/dist/chunks/cli-module-Cx7qotPY.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/exports.cjs +1 -1
- package/dist/exports.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +3 -3
- package/dist/styling.cjs +1 -1
- package/dist/styling.js +2 -2
- package/dist/terminal/formatter.d.ts.map +1 -1
- package/package.json +5 -1
- package/.claude/settings.local.json +0 -48
- package/.github/workflows/ci-quality.yml +0 -357
- package/.github/workflows/docs-demo.yml +0 -119
- package/.github/workflows/releases-core.yml +0 -512
- package/.github/workflows/releases-full.yml +0 -582
- package/.github/workflows-backup/ci.yml +0 -221
- package/.github/workflows-backup/nightly.yml +0 -196
- package/.github/workflows-backup/release-optimized.yml +0 -373
- package/.github/workflows-backup/release.yml +0 -269
- package/.release-notes-0.2.0.md +0 -129
- package/.yamllint +0 -28
- package/CHANGELOG.json +0 -1076
- package/CHANGELOG.md +0 -209
- package/CLAUDE.md +0 -217
- package/bun.lock +0 -333
- package/demo.html +0 -847
- package/dist/chunks/cli-module-CadqgZ1Z.js +0 -2
- package/dist/chunks/cli-module-CadqgZ1Z.js.map +0 -1
- package/dist/chunks/cli-module-ClIJmaT8.js.map +0 -1
- package/docs/API.md +0 -918
- package/docs/CORE.md +0 -264
- package/docs/DEVELOPMENT.md +0 -731
- package/docs/EXPORTS.md +0 -467
- package/docs/PACKAGES.md +0 -244
- package/docs/STYLING.md +0 -405
- package/docs/_config.yml +0 -36
- package/docs/index.md +0 -179
- package/examples/README.md +0 -208
- package/examples/basic-logging.js +0 -75
- package/examples/data-export.js +0 -234
- package/examples/package.json +0 -16
- package/examples/performance-timing.js +0 -170
- package/examples/simplified-api.js +0 -124
- package/examples/styling-themes.js +0 -207
- package/index.html +0 -358
- package/packages/core/package.json +0 -57
- package/packages/exports/package.json +0 -41
- package/packages/nodejs-opentui/README.md +0 -232
- package/packages/nodejs-opentui/package.json +0 -72
- package/packages/nodejs-opentui/src/LogRenderer.ts +0 -171
- package/packages/nodejs-opentui/src/OpenTUILogHandler.ts +0 -178
- package/packages/nodejs-opentui/src/components/LogBadge.tsx +0 -131
- package/packages/nodejs-opentui/src/index.ts +0 -133
- package/packages/nodejs-opentui/src/types.ts +0 -158
- package/packages/nodejs-opentui/tsconfig.json +0 -20
- package/packages/styling/package.json +0 -41
- package/playground/demo-all.ts +0 -95
- package/playground/demo-box.ts +0 -85
- package/playground/demo-levels.ts +0 -56
- package/playground/demo-real-world.ts +0 -98
- package/playground/demo-spinner.ts +0 -77
- package/playground/demo-steps.ts +0 -72
- package/playground/demo-table.ts +0 -83
- package/project-utils/README.md +0 -172
- package/project-utils/auto-release-gemini.ts +0 -1193
- package/project-utils/auto-release-ui.ts +0 -1329
- package/project-utils/commit-generator.ts +0 -1385
- package/project-utils/commit-ui.ts +0 -264
- package/project-utils/git-utils.ts +0 -199
- package/project-utils/github-release-manager.ts +0 -466
- package/project-utils/project-config.ts +0 -260
- package/project-utils/prompt-templates.js +0 -345
- package/project-utils/prompt-templates.ts +0 -422
- package/project-utils/version-manager.ts +0 -1078
- package/public/vite.svg +0 -1
- package/src/Logger.ts +0 -1858
- package/src/ScopedLogger.ts +0 -256
- package/src/cli/CommandProcessor.ts +0 -248
- package/src/cli/commands/ConfigCommand.ts +0 -93
- package/src/cli/commands/ExportCommand.ts +0 -276
- package/src/cli/commands/HistoryCommand.ts +0 -117
- package/src/cli/commands/StatusCommand.ts +0 -112
- package/src/cli/commands/ThemeCommand.ts +0 -88
- package/src/cli/help.ts +0 -127
- package/src/cli/index.ts +0 -71
- package/src/cli-module.ts +0 -21
- package/src/cli-primitives/box.ts +0 -86
- package/src/cli-primitives/cli-table.ts +0 -62
- package/src/cli-primitives/divider.ts +0 -17
- package/src/cli-primitives/header.ts +0 -18
- package/src/cli-primitives/index.ts +0 -13
- package/src/cli-primitives/server-fallback.ts +0 -54
- package/src/cli-primitives/spinner.ts +0 -133
- package/src/cli-primitives/step.ts +0 -22
- package/src/constants.ts +0 -313
- package/src/core.ts +0 -397
- package/src/example.ts +0 -210
- package/src/exports-module.ts +0 -311
- package/src/handlers/AnalyticsLogHandler.ts +0 -22
- package/src/handlers/ExportLogHandler.ts +0 -610
- package/src/handlers/FileLogHandler.ts +0 -169
- package/src/handlers/RemoteLogHandler.ts +0 -42
- package/src/handlers/index.ts +0 -8
- package/src/hooks/HookManager.ts +0 -177
- package/src/hooks/index.ts +0 -1
- package/src/index.ts +0 -395
- package/src/main.ts +0 -196
- package/src/serializers/SerializerRegistry.ts +0 -173
- package/src/serializers/index.ts +0 -1
- package/src/style.css +0 -96
- package/src/styling/LogStyleBuilder.ts +0 -355
- package/src/styling/SemanticStyles.ts +0 -380
- package/src/styling/SmartPresets.ts +0 -288
- package/src/styling/StyleBuilder.ts +0 -319
- package/src/styling/StyleCache.ts +0 -131
- package/src/styling/banners.ts +0 -168
- package/src/styling/index.ts +0 -27
- package/src/styling/themes.ts +0 -235
- package/src/styling-module.ts +0 -244
- package/src/terminal/color-converter.ts +0 -315
- package/src/terminal/formatter.ts +0 -236
- package/src/terminal/terminal-renderer.ts +0 -342
- package/src/transports/ConsoleTransport.ts +0 -28
- package/src/transports/FileTransport.ts +0 -53
- package/src/transports/HttpTransport.ts +0 -56
- package/src/transports/TransportManager.ts +0 -130
- package/src/transports/index.ts +0 -4
- package/src/types/core.ts +0 -417
- package/src/types/handlers.ts +0 -95
- package/src/types/hooks.ts +0 -45
- package/src/types/index.ts +0 -83
- package/src/types/serializers.ts +0 -30
- package/src/types/transports.ts +0 -52
- package/src/typescript.svg +0 -1
- package/src/utils/adapter.ts +0 -291
- package/src/utils/ansi-colors.ts +0 -333
- package/src/utils/environment-detector.ts +0 -170
- package/src/utils/environment.ts +0 -94
- package/src/utils/formatting.ts +0 -332
- package/src/utils/index.ts +0 -33
- package/src/utils/opentui-detection.ts +0 -138
- package/src/utils/output.ts +0 -227
- package/src/utils/stackTrace.ts +0 -144
- package/src/utils/timestamps.ts +0 -80
- package/src/vite-env.d.ts +0 -1
- package/src/writers/BufferWriter.ts +0 -157
- package/src/writers/index.ts +0 -6
- package/test-core-browser.html +0 -237
- package/test-core-node.js +0 -63
- package/tests/test-conflict-resolution.ts +0 -391
- package/tests/validate-workflows.sh +0 -131
- package/tests/yaml-autofix.sh +0 -146
- package/tsconfig.json +0 -50
- package/vite.config.ts +0 -256
|
@@ -1,1385 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Generador Automático de Commits con Gemini CLI
|
|
5
|
-
* Analiza todos los cambios del repositorio y genera commits coherentes
|
|
6
|
-
* siguiendo los patrones establecidos para el proyecto Better Logger
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { spawn } from 'child_process';
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
11
|
-
import { createCommitPrompt, GeminiResponseParser, BETTER_LOGGER_PROJECT_CONFIG, type GeminiPromptConfig } from './prompt-templates';
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
|
|
14
|
-
interface FileChange {
|
|
15
|
-
path: string;
|
|
16
|
-
status: 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked';
|
|
17
|
-
diff?: string;
|
|
18
|
-
lines_added?: number;
|
|
19
|
-
lines_removed?: number;
|
|
20
|
-
is_binary?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface GitStats {
|
|
24
|
-
total_files: number;
|
|
25
|
-
total_additions: number;
|
|
26
|
-
total_deletions: number;
|
|
27
|
-
files_by_extension: Record<string, number>;
|
|
28
|
-
directories_affected: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface CommitAnalysis {
|
|
32
|
-
files: FileChange[];
|
|
33
|
-
stats: GitStats;
|
|
34
|
-
project_context: {
|
|
35
|
-
name: string;
|
|
36
|
-
description: string;
|
|
37
|
-
tech_stack: string[];
|
|
38
|
-
target_platform: string;
|
|
39
|
-
};
|
|
40
|
-
commit_patterns: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface CommitProposal {
|
|
44
|
-
title: string;
|
|
45
|
-
description: string;
|
|
46
|
-
technical: string;
|
|
47
|
-
changelog: string;
|
|
48
|
-
files?: string[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
class CommitGenerator {
|
|
52
|
-
private projectRoot: string;
|
|
53
|
-
private tempDir: string;
|
|
54
|
-
private autoApprove: boolean;
|
|
55
|
-
private noPush: boolean;
|
|
56
|
-
private quiet: boolean;
|
|
57
|
-
private proposalFile?: string;
|
|
58
|
-
private outputDir?: string;
|
|
59
|
-
|
|
60
|
-
constructor() {
|
|
61
|
-
this.projectRoot = process.cwd();
|
|
62
|
-
this.tempDir = this.getArgValue('--output-dir') || join(this.projectRoot, 'project-utils/.temp');
|
|
63
|
-
this.autoApprove = process.argv.includes('--auto-approve');
|
|
64
|
-
this.noPush = process.argv.includes('--no-push');
|
|
65
|
-
this.quiet = process.argv.includes('--quiet');
|
|
66
|
-
this.proposalFile = this.getArgValue('--proposal-file');
|
|
67
|
-
this.outputDir = this.getArgValue('--output-dir');
|
|
68
|
-
this.ensureTempDir();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private getArgValue(argName: string): string | undefined {
|
|
72
|
-
const args = process.argv;
|
|
73
|
-
const argIndex = args.indexOf(argName);
|
|
74
|
-
if (argIndex > -1 && args[argIndex + 1] && !args[argIndex + 1].startsWith('--')) {
|
|
75
|
-
return args[argIndex + 1];
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private ensureTempDir(): void {
|
|
81
|
-
if (!existsSync(this.tempDir)) {
|
|
82
|
-
const { spawnSync } = require('child_process');
|
|
83
|
-
spawnSync('mkdir', ['-p', this.tempDir], { stdio: 'ignore' });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private log(message: string): void {
|
|
88
|
-
if (!this.quiet) {
|
|
89
|
-
console.log(message);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private logInfo(message: string): void {
|
|
94
|
-
if (!this.quiet) {
|
|
95
|
-
console.log(message);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Ejecuta un comando git y devuelve el resultado
|
|
101
|
-
*/
|
|
102
|
-
private async gitCommand(args: string[]): Promise<string> {
|
|
103
|
-
const { spawnSync } = await import('child_process');
|
|
104
|
-
const result = spawnSync('git', args, {
|
|
105
|
-
cwd: this.projectRoot,
|
|
106
|
-
encoding: 'utf-8',
|
|
107
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
if (result.status !== 0) {
|
|
111
|
-
const error = result.stderr || 'Git command failed';
|
|
112
|
-
throw new Error(`Git error: ${error}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return (result.stdout || '').trim();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Agrega todos los cambios al staging area
|
|
120
|
-
*/
|
|
121
|
-
private async stageAllChanges(): Promise<void> {
|
|
122
|
-
this.log('📦 Agregando todos los cambios al staging area...');
|
|
123
|
-
await this.gitCommand(['add', '-A']);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Obtiene el estado actual del repositorio
|
|
128
|
-
*/
|
|
129
|
-
private async getRepositoryStatus(): Promise<FileChange[]> {
|
|
130
|
-
this.log('🔍 Analizando estado del repositorio...');
|
|
131
|
-
|
|
132
|
-
const statusOutput = await this.gitCommand(['status', '--porcelain']);
|
|
133
|
-
const files: FileChange[] = [];
|
|
134
|
-
|
|
135
|
-
for (const line of statusOutput.split('\n').filter(l => l.trim())) {
|
|
136
|
-
const status = line.substring(0, 2);
|
|
137
|
-
const filePath = line.substring(3);
|
|
138
|
-
|
|
139
|
-
let fileStatus: FileChange['status'];
|
|
140
|
-
if (status.includes('A')) fileStatus = 'added';
|
|
141
|
-
else if (status.includes('M')) fileStatus = 'modified';
|
|
142
|
-
else if (status.includes('D')) fileStatus = 'deleted';
|
|
143
|
-
else if (status.includes('R')) fileStatus = 'renamed';
|
|
144
|
-
else fileStatus = 'untracked';
|
|
145
|
-
|
|
146
|
-
files.push({
|
|
147
|
-
path: filePath,
|
|
148
|
-
status: fileStatus,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return files;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Obtiene el diff de un archivo específico
|
|
157
|
-
*/
|
|
158
|
-
private async getFileDiff(filePath: string, isStaged: boolean = true): Promise<string> {
|
|
159
|
-
try {
|
|
160
|
-
const diffArgs = isStaged
|
|
161
|
-
? ['diff', '--cached', '--', filePath]
|
|
162
|
-
: ['diff', '--', filePath];
|
|
163
|
-
|
|
164
|
-
return await this.gitCommand(diffArgs);
|
|
165
|
-
} catch (error) {
|
|
166
|
-
// Si es un archivo nuevo o binario, devolver información básica
|
|
167
|
-
try {
|
|
168
|
-
const showArgs = ['show', `HEAD:${filePath}`];
|
|
169
|
-
await this.gitCommand(showArgs);
|
|
170
|
-
return `New file: ${filePath}`;
|
|
171
|
-
} catch {
|
|
172
|
-
return `Binary or new file: ${filePath}`;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Obtiene estadísticas del repositorio
|
|
179
|
-
*/
|
|
180
|
-
private async getGitStats(): Promise<GitStats> {
|
|
181
|
-
this.log('📊 Calculando estadísticas de cambios...');
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const diffStat = await this.gitCommand(['diff', '--cached', '--stat']);
|
|
185
|
-
const lines = diffStat.split('\n').filter(l => l.trim());
|
|
186
|
-
|
|
187
|
-
let totalFiles = 0;
|
|
188
|
-
let totalAdditions = 0;
|
|
189
|
-
let totalDeletions = 0;
|
|
190
|
-
const filesByExtension: Record<string, number> = {};
|
|
191
|
-
const directoriesAffected = new Set<string>();
|
|
192
|
-
|
|
193
|
-
for (const line of lines) {
|
|
194
|
-
if (line.includes('|')) {
|
|
195
|
-
totalFiles++;
|
|
196
|
-
const filePath = line.split('|')[0].trim();
|
|
197
|
-
|
|
198
|
-
// Extraer extensión
|
|
199
|
-
const ext = filePath.split('.').pop() || 'no-ext';
|
|
200
|
-
filesByExtension[ext] = (filesByExtension[ext] || 0) + 1;
|
|
201
|
-
|
|
202
|
-
// Extraer directorio
|
|
203
|
-
const dir = filePath.split('/')[0];
|
|
204
|
-
directoriesAffected.add(dir);
|
|
205
|
-
|
|
206
|
-
// Extraer adiciones y eliminaciones
|
|
207
|
-
const stats = line.split('|')[1];
|
|
208
|
-
const plusCount = (stats.match(/\+/g) || []).length;
|
|
209
|
-
const minusCount = (stats.match(/\-/g) || []).length;
|
|
210
|
-
totalAdditions += plusCount;
|
|
211
|
-
totalDeletions += minusCount;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
total_files: totalFiles,
|
|
217
|
-
total_additions: totalAdditions,
|
|
218
|
-
total_deletions: totalDeletions,
|
|
219
|
-
files_by_extension: filesByExtension,
|
|
220
|
-
directories_affected: Array.from(directoriesAffected),
|
|
221
|
-
};
|
|
222
|
-
} catch (error) {
|
|
223
|
-
return {
|
|
224
|
-
total_files: 0,
|
|
225
|
-
total_additions: 0,
|
|
226
|
-
total_deletions: 0,
|
|
227
|
-
files_by_extension: {},
|
|
228
|
-
directories_affected: [],
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Genera el contexto completo para Gemini CLI
|
|
235
|
-
*/
|
|
236
|
-
private async generateAnalysisContext(): Promise<CommitAnalysis> {
|
|
237
|
-
this.log('🧠 Generando contexto de análisis...');
|
|
238
|
-
|
|
239
|
-
await this.stageAllChanges();
|
|
240
|
-
|
|
241
|
-
const files = await this.getRepositoryStatus();
|
|
242
|
-
const stats = await this.getGitStats();
|
|
243
|
-
|
|
244
|
-
// Obtener diffs para cada archivo
|
|
245
|
-
for (const file of files) {
|
|
246
|
-
if (file.status !== 'deleted') {
|
|
247
|
-
try {
|
|
248
|
-
file.diff = await this.getFileDiff(file.path);
|
|
249
|
-
|
|
250
|
-
// Calcular líneas agregadas/eliminadas del diff
|
|
251
|
-
if (file.diff) {
|
|
252
|
-
file.lines_added = (file.diff.match(/^\+[^+]/gm) || []).length;
|
|
253
|
-
file.lines_removed = (file.diff.match(/^-[^-]/gm) || []).length;
|
|
254
|
-
file.is_binary = file.diff.includes('Binary files differ');
|
|
255
|
-
}
|
|
256
|
-
} catch (error) {
|
|
257
|
-
file.diff = `Error getting diff: ${error}`;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Cargar patrones de commit
|
|
263
|
-
const patternsPath = join(this.projectRoot, 'commit-templates/commit-patterns.md');
|
|
264
|
-
const commitPatterns = existsSync(patternsPath)
|
|
265
|
-
? readFileSync(patternsPath, 'utf-8')
|
|
266
|
-
: 'No commit patterns found';
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
files,
|
|
270
|
-
stats,
|
|
271
|
-
project_context: {
|
|
272
|
-
name: 'OpenTUI',
|
|
273
|
-
description: 'Modern Terminal User Interface Framework',
|
|
274
|
-
tech_stack: ['TypeScript', 'Node.js', 'Terminal UI', 'CLI'],
|
|
275
|
-
target_platform: 'Cross-platform (macOS, Linux, Windows)',
|
|
276
|
-
},
|
|
277
|
-
commit_patterns: commitPatterns,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private createStandardPrompt(analysis: CommitAnalysis, extraContext: string = ''): string {
|
|
282
|
-
const config: GeminiPromptConfig = {
|
|
283
|
-
projectContext: {
|
|
284
|
-
name: BETTER_LOGGER_PROJECT_CONFIG.name,
|
|
285
|
-
description: BETTER_LOGGER_PROJECT_CONFIG.description,
|
|
286
|
-
version: BETTER_LOGGER_PROJECT_CONFIG.version,
|
|
287
|
-
techStack: [...BETTER_LOGGER_PROJECT_CONFIG.techStack],
|
|
288
|
-
targetPlatform: BETTER_LOGGER_PROJECT_CONFIG.targetPlatform,
|
|
289
|
-
},
|
|
290
|
-
analysisType: 'commit',
|
|
291
|
-
specificContext: extraContext,
|
|
292
|
-
data: {
|
|
293
|
-
stats: analysis.stats,
|
|
294
|
-
files: analysis.files.map(file => ({
|
|
295
|
-
path: file.path,
|
|
296
|
-
status: file.status,
|
|
297
|
-
lines_added: file.lines_added,
|
|
298
|
-
lines_removed: file.lines_removed,
|
|
299
|
-
is_binary: file.is_binary,
|
|
300
|
-
diff_preview: file.diff?.substring(0, 1500) || 'No diff available'
|
|
301
|
-
})),
|
|
302
|
-
patterns: analysis.commit_patterns
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
return createCommitPrompt(config);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private createExhaustivePrompt(analysis: CommitAnalysis, extraContext: string = ''): string {
|
|
310
|
-
const config: GeminiPromptConfig = {
|
|
311
|
-
projectContext: {
|
|
312
|
-
name: BETTER_LOGGER_PROJECT_CONFIG.name,
|
|
313
|
-
description: BETTER_LOGGER_PROJECT_CONFIG.description,
|
|
314
|
-
version: BETTER_LOGGER_PROJECT_CONFIG.version,
|
|
315
|
-
techStack: [...BETTER_LOGGER_PROJECT_CONFIG.techStack],
|
|
316
|
-
targetPlatform: BETTER_LOGGER_PROJECT_CONFIG.targetPlatform,
|
|
317
|
-
},
|
|
318
|
-
analysisType: 'commit',
|
|
319
|
-
specificContext: `MODO EXHAUSTIVO: Análisis profundo requerido.\n${extraContext}`,
|
|
320
|
-
data: {
|
|
321
|
-
mode: 'exhaustive',
|
|
322
|
-
stats: analysis.stats,
|
|
323
|
-
files: analysis.files.map(file => ({
|
|
324
|
-
path: file.path,
|
|
325
|
-
status: file.status,
|
|
326
|
-
lines_added: file.lines_added,
|
|
327
|
-
lines_removed: file.lines_removed,
|
|
328
|
-
is_binary: file.is_binary,
|
|
329
|
-
diff_preview: file.diff?.substring(0, 2000) || 'No diff available'
|
|
330
|
-
})),
|
|
331
|
-
patterns: analysis.commit_patterns
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
return createCommitPrompt(config);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Construye contexto mejorado con parámetros adicionales
|
|
340
|
-
*/
|
|
341
|
-
private buildEnhancedContext(
|
|
342
|
-
extraContext: string,
|
|
343
|
-
contextDescription: string,
|
|
344
|
-
workType: string,
|
|
345
|
-
affectedComponents: string,
|
|
346
|
-
performanceImpact: string,
|
|
347
|
-
breakingChanges: string
|
|
348
|
-
): string {
|
|
349
|
-
let enhancedContext = extraContext;
|
|
350
|
-
|
|
351
|
-
const contextParts = [];
|
|
352
|
-
|
|
353
|
-
if (contextDescription) {
|
|
354
|
-
contextParts.push(`**Descripción del trabajo**: ${contextDescription}`);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (workType) {
|
|
358
|
-
const workTypeDescriptions = {
|
|
359
|
-
'feature': 'Nueva funcionalidad o capacidad',
|
|
360
|
-
'bugfix': 'Corrección de error o fallo',
|
|
361
|
-
'refactor': 'Mejora del código sin cambios de funcionalidad',
|
|
362
|
-
'docs': 'Actualización de documentación',
|
|
363
|
-
'performance': 'Optimización de rendimiento',
|
|
364
|
-
'ui': 'Cambios en interfaz de usuario',
|
|
365
|
-
'api': 'Modificaciones en API o endpoints',
|
|
366
|
-
'security': 'Mejoras de seguridad',
|
|
367
|
-
'test': 'Adición o modificación de tests'
|
|
368
|
-
};
|
|
369
|
-
contextParts.push(`**Tipo de trabajo**: ${workType} - ${workTypeDescriptions[workType] || workType}`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (affectedComponents) {
|
|
373
|
-
contextParts.push(`**Componentes afectados**: ${affectedComponents}`);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (performanceImpact) {
|
|
377
|
-
const performanceDescriptions = {
|
|
378
|
-
'mejora': 'Este cambio mejora el rendimiento del sistema',
|
|
379
|
-
'neutro': 'Este cambio no afecta significativamente el rendimiento',
|
|
380
|
-
'regresion': 'Este cambio puede impactar negativamente el rendimiento (justificado por otros beneficios)'
|
|
381
|
-
};
|
|
382
|
-
contextParts.push(`**Impacto en rendimiento**: ${performanceImpact} - ${performanceDescriptions[performanceImpact] || performanceImpact}`);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (breakingChanges) {
|
|
386
|
-
const breakingDescription = breakingChanges.toLowerCase() === 'si'
|
|
387
|
-
? 'Este cambio introduce cambios que rompen compatibilidad hacia atrás'
|
|
388
|
-
: 'Este cambio mantiene compatibilidad hacia atrás';
|
|
389
|
-
contextParts.push(`**Cambios incompatibles**: ${breakingChanges} - ${breakingDescription}`);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (contextParts.length > 0) {
|
|
393
|
-
const contextSection = contextParts.join('\n');
|
|
394
|
-
enhancedContext = enhancedContext
|
|
395
|
-
? `${enhancedContext}\n\n## Contexto Estructurado\n\n${contextSection}`
|
|
396
|
-
: `## Contexto Estructurado\n\n${contextSection}`;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return enhancedContext;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Invoca Gemini CLI con el contexto de análisis
|
|
404
|
-
*/
|
|
405
|
-
private async analyzeWithGemini(analysis: CommitAnalysis, exhaustive: boolean = false, extraContext: string = ''): Promise<string> {
|
|
406
|
-
this.log(`🤖 Analizando cambios con Gemini CLI... ${exhaustive ? '(Modo Exhaustivo)' : ''}`);
|
|
407
|
-
|
|
408
|
-
const prompt = exhaustive
|
|
409
|
-
? this.createExhaustivePrompt(analysis, extraContext)
|
|
410
|
-
: this.createStandardPrompt(analysis, extraContext);
|
|
411
|
-
|
|
412
|
-
// Guardar el contexto en un archivo temporal
|
|
413
|
-
const contextPath = join(this.tempDir, 'analysis-context.json');
|
|
414
|
-
writeFileSync(contextPath, JSON.stringify(analysis, null, 2));
|
|
415
|
-
|
|
416
|
-
// Guardar el prompt en un archivo temporal
|
|
417
|
-
const promptPath = join(this.tempDir, 'gemini-prompt.txt');
|
|
418
|
-
writeFileSync(promptPath, prompt);
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
// Ejecutar Gemini CLI
|
|
422
|
-
const geminiResult = Bun.spawnSync(['gemini'], {
|
|
423
|
-
cwd: this.projectRoot,
|
|
424
|
-
stdin: Buffer.from(prompt) as any,
|
|
425
|
-
stdout: 'pipe',
|
|
426
|
-
stderr: 'pipe',
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
if (geminiResult.exitCode !== 0) {
|
|
430
|
-
const error = geminiResult.stderr?.toString() || 'Gemini CLI failed';
|
|
431
|
-
throw new Error(`Gemini CLI error: ${error}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const response = geminiResult.stdout?.toString() || '';
|
|
435
|
-
|
|
436
|
-
// Guardar la respuesta
|
|
437
|
-
const responsePath = join(this.tempDir, 'gemini-response.md');
|
|
438
|
-
writeFileSync(responsePath, response);
|
|
439
|
-
|
|
440
|
-
return response;
|
|
441
|
-
} catch (error) {
|
|
442
|
-
console.error('❌ Error ejecutando Gemini CLI:', error);
|
|
443
|
-
console.error('💡 Verifica que Gemini CLI esté instalado y configurado');
|
|
444
|
-
console.log('📝 Contexto guardado en:', contextPath);
|
|
445
|
-
console.log('📝 Prompt guardado en:', promptPath);
|
|
446
|
-
throw error;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Guarda la propuesta de commits
|
|
452
|
-
*/
|
|
453
|
-
private saveCommitProposal(analysis: string): string {
|
|
454
|
-
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
|
455
|
-
const proposalPath = join(this.tempDir, `commit-proposal-${timestamp}.md`);
|
|
456
|
-
|
|
457
|
-
writeFileSync(proposalPath, analysis);
|
|
458
|
-
return proposalPath;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Parsea propuestas de commit de la respuesta de Gemini
|
|
463
|
-
*/
|
|
464
|
-
private parseCommitProposals(aiResponse: string, allFiles: FileChange[]): CommitProposal[] {
|
|
465
|
-
// Usar el parser estandarizado
|
|
466
|
-
const parsedProposals = GeminiResponseParser.parseCommitProposals(aiResponse);
|
|
467
|
-
|
|
468
|
-
// Si hay múltiples commits, dividir archivos inteligentemente
|
|
469
|
-
if (parsedProposals.length > 1) {
|
|
470
|
-
return this.distributeFilesAcrossCommits(parsedProposals, allFiles);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Commit único usa todos los archivos
|
|
474
|
-
return parsedProposals.map(proposal => ({
|
|
475
|
-
title: proposal.title,
|
|
476
|
-
description: proposal.description,
|
|
477
|
-
technical: proposal.technical,
|
|
478
|
-
changelog: proposal.changelog,
|
|
479
|
-
files: allFiles.map(f => f.path).filter(path =>
|
|
480
|
-
!path.includes('.temp/') &&
|
|
481
|
-
!path.startsWith('.release-notes-') &&
|
|
482
|
-
!path.includes(' -> ')
|
|
483
|
-
)
|
|
484
|
-
}));
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Distribuye archivos entre múltiples commits basado en patrones
|
|
489
|
-
*/
|
|
490
|
-
private distributeFilesAcrossCommits(
|
|
491
|
-
parsedProposals: any[],
|
|
492
|
-
allFiles: FileChange[]
|
|
493
|
-
): CommitProposal[] {
|
|
494
|
-
const availableFiles = allFiles
|
|
495
|
-
.map(f => f.path)
|
|
496
|
-
.filter(path =>
|
|
497
|
-
!path.includes('.temp/') &&
|
|
498
|
-
!path.startsWith('.release-notes-') &&
|
|
499
|
-
!path.includes(' -> ')
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
const fileDistribution = new Map<string, string[]>();
|
|
503
|
-
const usedFiles = new Set<string>();
|
|
504
|
-
|
|
505
|
-
// Patterns para clasificar archivos por tipo de commit
|
|
506
|
-
const patterns = {
|
|
507
|
-
ci: ['.github/workflows/', 'project-utils/', '.yml'],
|
|
508
|
-
architecture: ['package.json', 'packages/', 'vite.config.ts', 'tsconfig.json'],
|
|
509
|
-
core: ['src/core', 'src/Logger.ts', 'src/constants.ts'],
|
|
510
|
-
styling: ['src/styling', 'src/style'],
|
|
511
|
-
exports: ['src/exports', 'src/handlers/'],
|
|
512
|
-
docs: ['README.md', 'docs/', '.md'],
|
|
513
|
-
config: ['vite.config', 'tsconfig', '.json']
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
// Asignar archivos a commits basado en contenido/título
|
|
517
|
-
parsedProposals.forEach((proposal, index) => {
|
|
518
|
-
const commitFiles: string[] = [];
|
|
519
|
-
const title = proposal.title.toLowerCase();
|
|
520
|
-
const description = (proposal.description || '').toLowerCase();
|
|
521
|
-
|
|
522
|
-
// Detectar tipo de commit por título/descripción
|
|
523
|
-
let commitType = 'misc';
|
|
524
|
-
if (title.includes('ci') || title.includes('workflow') || title.includes('bun')) {
|
|
525
|
-
commitType = 'ci';
|
|
526
|
-
} else if (title.includes('arquitectura') || title.includes('monorepo') || title.includes('modular')) {
|
|
527
|
-
commitType = 'architecture';
|
|
528
|
-
} else if (title.includes('core') || title.includes('logger')) {
|
|
529
|
-
commitType = 'core';
|
|
530
|
-
} else if (title.includes('styling') || title.includes('style')) {
|
|
531
|
-
commitType = 'styling';
|
|
532
|
-
} else if (title.includes('export') || title.includes('handler')) {
|
|
533
|
-
commitType = 'exports';
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Asignar archivos que coincidan con el patrón
|
|
537
|
-
if (patterns[commitType]) {
|
|
538
|
-
availableFiles.forEach(file => {
|
|
539
|
-
if (!usedFiles.has(file)) {
|
|
540
|
-
const matchesPattern = patterns[commitType].some(pattern =>
|
|
541
|
-
file.includes(pattern)
|
|
542
|
-
);
|
|
543
|
-
if (matchesPattern) {
|
|
544
|
-
commitFiles.push(file);
|
|
545
|
-
usedFiles.add(file);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
fileDistribution.set(`commit_${index}`, commitFiles);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
// Asignar archivos restantes al primer commit (fallback)
|
|
555
|
-
const remainingFiles = availableFiles.filter(file => !usedFiles.has(file));
|
|
556
|
-
if (remainingFiles.length > 0) {
|
|
557
|
-
const firstCommitFiles = fileDistribution.get('commit_0') || [];
|
|
558
|
-
firstCommitFiles.push(...remainingFiles);
|
|
559
|
-
fileDistribution.set('commit_0', firstCommitFiles);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Crear propuestas finales
|
|
563
|
-
return parsedProposals.map((proposal, index) => {
|
|
564
|
-
const commitFiles = fileDistribution.get(`commit_${index}`) || [];
|
|
565
|
-
|
|
566
|
-
// Log de asignación (solo en modo debug)
|
|
567
|
-
if (!this.quiet) {
|
|
568
|
-
console.log(`📋 Commit ${index + 1}: "${proposal.title}" → ${commitFiles.length} archivos`);
|
|
569
|
-
commitFiles.forEach(file => console.log(` - ${file}`));
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return {
|
|
573
|
-
title: proposal.title,
|
|
574
|
-
description: proposal.description,
|
|
575
|
-
technical: proposal.technical,
|
|
576
|
-
changelog: proposal.changelog,
|
|
577
|
-
files: commitFiles
|
|
578
|
-
};
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Ejecuta un commit individual
|
|
584
|
-
*/
|
|
585
|
-
private async executeCommit(proposal: CommitProposal, allFiles: FileChange[]): Promise<boolean> {
|
|
586
|
-
this.log(`\n🔨 Ejecutando commit: ${proposal.title}`);
|
|
587
|
-
|
|
588
|
-
try {
|
|
589
|
-
// Si no hay archivos específicos, usar todos los archivos disponibles (excluyendo temp files)
|
|
590
|
-
const targetFiles = proposal.files && proposal.files.length > 0
|
|
591
|
-
? proposal.files
|
|
592
|
-
: allFiles
|
|
593
|
-
.map(f => f.path)
|
|
594
|
-
.filter(path => !path.includes('.temp/') && !path.startsWith('.release-notes-'))
|
|
595
|
-
.filter(path => !path.includes(' -> ')); // Filtrar sintaxis de rename "file.yml -> backup/file.yml"
|
|
596
|
-
|
|
597
|
-
// Validación pre-commit: verificar que hay archivos para procesar
|
|
598
|
-
if (targetFiles.length === 0) {
|
|
599
|
-
console.warn(` ⚠️ No hay archivos asignados a este commit`);
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
this.log(` 📋 Procesando ${targetFiles.length} archivos asignados`);
|
|
604
|
-
|
|
605
|
-
// Agregar archivos específicos al staging area
|
|
606
|
-
let stagedFiles = 0;
|
|
607
|
-
for (const file of targetFiles) {
|
|
608
|
-
try {
|
|
609
|
-
await this.gitCommand(['add', file]);
|
|
610
|
-
stagedFiles++;
|
|
611
|
-
this.log(` ✓ Agregado: ${file}`);
|
|
612
|
-
} catch (error) {
|
|
613
|
-
console.warn(` ⚠️ No se pudo agregar ${file}:`, error);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Validación: verificar que se staged al menos un archivo
|
|
618
|
-
if (stagedFiles === 0) {
|
|
619
|
-
console.warn(` ⚠️ No se pudo agregar ningún archivo al staging area`);
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Verificar que hay algo para commitear
|
|
624
|
-
try {
|
|
625
|
-
const statusResult = await this.gitCommand(['diff', '--cached', '--name-only']);
|
|
626
|
-
if (!statusResult.trim()) {
|
|
627
|
-
console.warn(` ⚠️ No hay cambios staged para este commit (staged files: ${stagedFiles})`);
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const stagedFilesList = statusResult.trim().split('\n');
|
|
632
|
-
this.log(` 📦 ${stagedFilesList.length} archivos listos para commit`);
|
|
633
|
-
|
|
634
|
-
} catch (error) {
|
|
635
|
-
// Fallback si diff --cached no funciona
|
|
636
|
-
this.log(` 🔍 Verificando staging area...`);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Crear mensaje de commit
|
|
640
|
-
let commitMessage = proposal.title;
|
|
641
|
-
if (proposal.description) {
|
|
642
|
-
commitMessage += `\n\n${proposal.description}`;
|
|
643
|
-
}
|
|
644
|
-
if (proposal.technical) {
|
|
645
|
-
commitMessage += `\n\n<technical>\n${proposal.technical}\n</technical>`;
|
|
646
|
-
}
|
|
647
|
-
if (proposal.changelog) {
|
|
648
|
-
commitMessage += `\n\n<changelog>\n${proposal.changelog}\n</changelog>`;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Ejecutar commit
|
|
652
|
-
await this.gitCommand(['commit', '-m', commitMessage]);
|
|
653
|
-
this.log(` ✅ Commit exitoso`);
|
|
654
|
-
return true;
|
|
655
|
-
|
|
656
|
-
} catch (error) {
|
|
657
|
-
console.error(` ❌ Error en commit:`, error);
|
|
658
|
-
return false;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Detecta si estamos ejecutando en un contexto de release
|
|
664
|
-
* - GITHUB_ACTIONS: ejecutando desde workflow
|
|
665
|
-
* - RELEASE_WORKFLOW: variable específica de release workflow
|
|
666
|
-
* - Análisis de archivos modificados relacionados con release
|
|
667
|
-
*/
|
|
668
|
-
private isReleaseWorkflowContext(): boolean {
|
|
669
|
-
// Contexto explícito de GitHub Actions release
|
|
670
|
-
if (process.env.GITHUB_ACTIONS === 'true' && process.env.RELEASE_WORKFLOW === 'true') {
|
|
671
|
-
return true;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Detección por archivos modificados relacionados con release
|
|
675
|
-
return this.hasReleaseRelatedFiles();
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Verifica si los archivos modificados están relacionados con releases
|
|
680
|
-
*/
|
|
681
|
-
private hasReleaseRelatedFiles(): boolean {
|
|
682
|
-
try {
|
|
683
|
-
// Obtener archivos staged (ya llamado en generateAnalysisContext)
|
|
684
|
-
const stagedFiles = this.getAllStagedFiles();
|
|
685
|
-
|
|
686
|
-
const releasePatterns = [
|
|
687
|
-
'package.json', // Cambios de versión
|
|
688
|
-
'packages/*/package.json', // Sub-packages
|
|
689
|
-
'CHANGELOG.json', // Cambios en changelog
|
|
690
|
-
'.release-notes-', // Archivos de release notes
|
|
691
|
-
'project-utils/.temp/' // Archivos temporales de release
|
|
692
|
-
];
|
|
693
|
-
|
|
694
|
-
return stagedFiles.some(file =>
|
|
695
|
-
releasePatterns.some(pattern => {
|
|
696
|
-
if (pattern.includes('*')) {
|
|
697
|
-
// Patrón con wildcard
|
|
698
|
-
const regex = new RegExp(pattern.replace('*', '.*'));
|
|
699
|
-
return regex.test(file);
|
|
700
|
-
} else {
|
|
701
|
-
// Patrón exacto
|
|
702
|
-
return file.includes(pattern);
|
|
703
|
-
}
|
|
704
|
-
})
|
|
705
|
-
);
|
|
706
|
-
} catch (error) {
|
|
707
|
-
// Si no podemos verificar, asumir contexto normal
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Detecta el tipo específico de conflictos de git
|
|
714
|
-
*/
|
|
715
|
-
private async analyzeConflictType(): Promise<{
|
|
716
|
-
hasConflicts: boolean;
|
|
717
|
-
conflictType: 'none' | 'merge' | 'rebase' | 'tmp-files' | 'manual';
|
|
718
|
-
conflictedFiles: string[];
|
|
719
|
-
canAutoResolve: boolean;
|
|
720
|
-
requiresManualIntervention: boolean;
|
|
721
|
-
}> {
|
|
722
|
-
try {
|
|
723
|
-
// Verificar si hay archivos con conflictos de merge
|
|
724
|
-
const conflictedFiles = await this.getConflictedFiles();
|
|
725
|
-
|
|
726
|
-
if (conflictedFiles.length === 0) {
|
|
727
|
-
return {
|
|
728
|
-
hasConflicts: false,
|
|
729
|
-
conflictType: 'none',
|
|
730
|
-
conflictedFiles: [],
|
|
731
|
-
canAutoResolve: true,
|
|
732
|
-
requiresManualIntervention: false
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// Analizar tipo de conflictos
|
|
737
|
-
const tmpFiles = conflictedFiles.filter(file =>
|
|
738
|
-
file.includes('.temp') ||
|
|
739
|
-
file.includes('tmp') ||
|
|
740
|
-
file.includes('.cache') ||
|
|
741
|
-
file.startsWith('project-utils/.temp/')
|
|
742
|
-
);
|
|
743
|
-
|
|
744
|
-
const criticalFiles = conflictedFiles.filter(file =>
|
|
745
|
-
file.includes('src/') ||
|
|
746
|
-
file.includes('package.json') ||
|
|
747
|
-
file.includes('tsconfig.json') ||
|
|
748
|
-
file.includes('.github/workflows/')
|
|
749
|
-
);
|
|
750
|
-
|
|
751
|
-
// Determinar si es auto-resoluble
|
|
752
|
-
const canAutoResolve = tmpFiles.length > 0 && criticalFiles.length === 0;
|
|
753
|
-
|
|
754
|
-
let conflictType: 'merge' | 'rebase' | 'tmp-files' | 'manual' = 'manual';
|
|
755
|
-
|
|
756
|
-
if (tmpFiles.length === conflictedFiles.length) {
|
|
757
|
-
conflictType = 'tmp-files';
|
|
758
|
-
} else if (await this.isInRebaseState()) {
|
|
759
|
-
conflictType = 'rebase';
|
|
760
|
-
} else {
|
|
761
|
-
conflictType = 'merge';
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return {
|
|
765
|
-
hasConflicts: true,
|
|
766
|
-
conflictType,
|
|
767
|
-
conflictedFiles,
|
|
768
|
-
canAutoResolve,
|
|
769
|
-
requiresManualIntervention: criticalFiles.length > 0
|
|
770
|
-
};
|
|
771
|
-
} catch (error) {
|
|
772
|
-
this.log(`❌ Error analizando conflictos: ${error}`);
|
|
773
|
-
return {
|
|
774
|
-
hasConflicts: false,
|
|
775
|
-
conflictType: 'none',
|
|
776
|
-
conflictedFiles: [],
|
|
777
|
-
canAutoResolve: false,
|
|
778
|
-
requiresManualIntervention: true
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Obtiene archivos con conflictos de merge/rebase
|
|
785
|
-
*/
|
|
786
|
-
private async getConflictedFiles(): Promise<string[]> {
|
|
787
|
-
try {
|
|
788
|
-
const { spawnSync } = require('child_process');
|
|
789
|
-
const result = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], {
|
|
790
|
-
cwd: this.projectRoot,
|
|
791
|
-
encoding: 'utf-8',
|
|
792
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
if (result.status !== 0) {
|
|
796
|
-
return [];
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
return (result.stdout || '').trim().split('\n').filter(line => line.length > 0);
|
|
800
|
-
} catch (error) {
|
|
801
|
-
return [];
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Verifica si estamos en estado de rebase
|
|
807
|
-
*/
|
|
808
|
-
private async isInRebaseState(): Promise<boolean> {
|
|
809
|
-
try {
|
|
810
|
-
const { existsSync } = require('fs');
|
|
811
|
-
const rebaseMergeDir = join(this.projectRoot, '.git', 'rebase-merge');
|
|
812
|
-
const rebaseApplyDir = join(this.projectRoot, '.git', 'rebase-apply');
|
|
813
|
-
return existsSync(rebaseMergeDir) || existsSync(rebaseApplyDir);
|
|
814
|
-
} catch {
|
|
815
|
-
return false;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Resuelve conflictos automáticamente cuando es seguro
|
|
821
|
-
*/
|
|
822
|
-
private async autoResolveConflicts(conflictAnalysis: any): Promise<boolean> {
|
|
823
|
-
if (!conflictAnalysis.canAutoResolve) {
|
|
824
|
-
return false;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
this.log('🔧 Intentando resolución automática de conflictos...');
|
|
828
|
-
|
|
829
|
-
try {
|
|
830
|
-
// Para archivos temporales, usar la versión local (nuestra)
|
|
831
|
-
for (const file of conflictAnalysis.conflictedFiles) {
|
|
832
|
-
if (file.includes('.temp') || file.includes('tmp') || file.includes('.cache')) {
|
|
833
|
-
await this.gitCommand(['add', file]);
|
|
834
|
-
this.log(`✅ Auto-resuelto conflicto en archivo temporal: ${file}`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
return true;
|
|
839
|
-
} catch (error) {
|
|
840
|
-
this.log(`❌ Error en resolución automática: ${error}`);
|
|
841
|
-
return false;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/**
|
|
846
|
-
* Crea backup de cambios importantes antes de operaciones peligrosas
|
|
847
|
-
*/
|
|
848
|
-
private async createSafetyBackup(): Promise<string> {
|
|
849
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
850
|
-
const backupBranch = `backup-before-conflict-resolution-${timestamp}`;
|
|
851
|
-
|
|
852
|
-
try {
|
|
853
|
-
// Crear rama de backup
|
|
854
|
-
await this.gitCommand(['checkout', '-b', backupBranch]);
|
|
855
|
-
|
|
856
|
-
// Volver a la rama original
|
|
857
|
-
const currentBranch = await this.getCurrentBranch();
|
|
858
|
-
await this.gitCommand(['checkout', currentBranch]);
|
|
859
|
-
|
|
860
|
-
this.log(`💾 Backup creado en rama: ${backupBranch}`);
|
|
861
|
-
return backupBranch;
|
|
862
|
-
} catch (error) {
|
|
863
|
-
this.log(`⚠️ No se pudo crear backup: ${error}`);
|
|
864
|
-
throw error;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* Obtiene la rama actual
|
|
870
|
-
*/
|
|
871
|
-
private async getCurrentBranch(): Promise<string> {
|
|
872
|
-
try {
|
|
873
|
-
const result = await this.gitCommand(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
874
|
-
return result.trim();
|
|
875
|
-
} catch {
|
|
876
|
-
return 'main';
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Obtiene todos los archivos en staging area
|
|
882
|
-
*/
|
|
883
|
-
private getAllStagedFiles(): string[] {
|
|
884
|
-
try {
|
|
885
|
-
const { spawnSync } = require('child_process');
|
|
886
|
-
const result = spawnSync('git', ['diff', '--cached', '--name-only'], {
|
|
887
|
-
cwd: this.projectRoot,
|
|
888
|
-
encoding: 'utf-8',
|
|
889
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
if (result.status !== 0) {
|
|
893
|
-
return [];
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
return (result.stdout || '').trim().split('\n').filter(Boolean);
|
|
897
|
-
} catch (error) {
|
|
898
|
-
return [];
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
/**
|
|
903
|
-
* Maneja commits en contexto de release con estrategia anti-conflictos
|
|
904
|
-
*/
|
|
905
|
-
private async handleReleaseWorkflowCommit(): Promise<void> {
|
|
906
|
-
this.log('🔄 Contexto de release detectado - aplicando estrategia anti-conflictos...');
|
|
907
|
-
|
|
908
|
-
try {
|
|
909
|
-
const currentBranch = await this.gitCommand(['branch', '--show-current']);
|
|
910
|
-
|
|
911
|
-
// Estrategia: Pull-Rebase-Push con reintentos (MAX_RETRIES=3)
|
|
912
|
-
let retryCount = 0;
|
|
913
|
-
const maxRetries = 3;
|
|
914
|
-
const MAX_RETRIES = 3;
|
|
915
|
-
|
|
916
|
-
while (retryCount < maxRetries) {
|
|
917
|
-
try {
|
|
918
|
-
// Paso 1: Fetch último estado del remote (git fetch origin)
|
|
919
|
-
this.log(`🔄 Intento ${retryCount + 1}/${maxRetries}: Sincronizando con remote...`);
|
|
920
|
-
await this.gitCommand(['fetch', 'origin', currentBranch]);
|
|
921
|
-
|
|
922
|
-
// Paso 2: Intentar rebase automático (git rebase)
|
|
923
|
-
try {
|
|
924
|
-
await this.gitCommand(['rebase', `origin/${currentBranch}`]);
|
|
925
|
-
this.log('✅ Rebase completado exitosamente');
|
|
926
|
-
} catch (rebaseError) {
|
|
927
|
-
this.log('⚠️ Conflictos detectados durante rebase - analizando...');
|
|
928
|
-
|
|
929
|
-
// Analizar tipo de conflictos
|
|
930
|
-
const conflictAnalysis = await this.analyzeConflictType();
|
|
931
|
-
|
|
932
|
-
if (conflictAnalysis.hasConflicts) {
|
|
933
|
-
this.log(`📊 Análisis de conflictos:`);
|
|
934
|
-
this.log(` Tipo: ${conflictAnalysis.conflictType}`);
|
|
935
|
-
this.log(` Archivos: ${conflictAnalysis.conflictedFiles.join(', ')}`);
|
|
936
|
-
this.log(` Auto-resoluble: ${conflictAnalysis.canAutoResolve ? 'Sí' : 'No'}`);
|
|
937
|
-
this.log(` Intervención manual: ${conflictAnalysis.requiresManualIntervention ? 'Requerida' : 'No necesaria'}`);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// Crear backup antes de intentar resolución
|
|
941
|
-
let backupBranch: string | null = null;
|
|
942
|
-
if (conflictAnalysis.requiresManualIntervention) {
|
|
943
|
-
try {
|
|
944
|
-
backupBranch = await this.createSafetyBackup();
|
|
945
|
-
} catch (backupError) {
|
|
946
|
-
this.log(`⚠️ No se pudo crear backup: ${backupError}`);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Intentar resolución automática
|
|
951
|
-
if (conflictAnalysis.canAutoResolve) {
|
|
952
|
-
const resolved = await this.autoResolveConflicts(conflictAnalysis);
|
|
953
|
-
if (resolved) {
|
|
954
|
-
this.log('✅ Conflictos resueltos automáticamente, continuando rebase...');
|
|
955
|
-
await this.gitCommand(['rebase', '--continue']);
|
|
956
|
-
return; // Continuar con el flujo normal
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Si no se pudo resolver automáticamente, aplicar estrategia conservadora
|
|
961
|
-
this.log('⚠️ Aplicando estrategia conservadora (reset hard)');
|
|
962
|
-
await this.gitCommand(['rebase', '--abort']);
|
|
963
|
-
await this.gitCommand(['reset', '--hard', `origin/${currentBranch}`]);
|
|
964
|
-
|
|
965
|
-
// Re-aplicar nuestros commits si es necesario
|
|
966
|
-
const hasLocalCommits = await this.hasUnpushedCommits();
|
|
967
|
-
if (hasLocalCommits) {
|
|
968
|
-
this.log('🔄 Re-aplicando cambios locales...');
|
|
969
|
-
const errorMsg = `Conflictos no resolubles automáticamente - requiere intervención manual.${backupBranch ? ` Backup disponible en rama: ${backupBranch}` : ''}`;
|
|
970
|
-
throw new Error(errorMsg);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// Paso 3: Push con fuerza controlled
|
|
975
|
-
await this.gitCommand(['push', 'origin', currentBranch]);
|
|
976
|
-
this.log('✅ Push en contexto release completado');
|
|
977
|
-
return;
|
|
978
|
-
|
|
979
|
-
} catch (pushError) {
|
|
980
|
-
retryCount++;
|
|
981
|
-
if (retryCount >= maxRetries) {
|
|
982
|
-
throw pushError;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
this.log(`⚠️ Push falló (intento ${retryCount}), reintentando en 2s...`);
|
|
986
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
} catch (error) {
|
|
991
|
-
console.error('❌ Error en manejo de contexto release:', error);
|
|
992
|
-
throw error;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* Verifica si hay commits locales no pusheados
|
|
998
|
-
*/
|
|
999
|
-
private async hasUnpushedCommits(): Promise<boolean> {
|
|
1000
|
-
try {
|
|
1001
|
-
const currentBranch = await this.gitCommand(['branch', '--show-current']);
|
|
1002
|
-
const unpushedOutput = await this.gitCommand(['log', `origin/${currentBranch}..HEAD`, '--oneline']);
|
|
1003
|
-
return unpushedOutput.trim().length > 0;
|
|
1004
|
-
} catch (error) {
|
|
1005
|
-
// Si no podemos verificar, asumir que sí hay commits locales
|
|
1006
|
-
return true;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
/**
|
|
1011
|
-
* Ejecuta push de todos los commits
|
|
1012
|
-
*/
|
|
1013
|
-
private async pushCommits(): Promise<void> {
|
|
1014
|
-
if (this.noPush) {
|
|
1015
|
-
this.log('⏭️ Push deshabilitado por --no-push');
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
this.log('\n📤 Pushing commits to remote...');
|
|
1020
|
-
|
|
1021
|
-
// Detectar si estamos en contexto de release y usar estrategia especializada
|
|
1022
|
-
if (this.isReleaseWorkflowContext()) {
|
|
1023
|
-
return this.handleReleaseWorkflowCommit();
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// Push estándar para contexto de desarrollo
|
|
1027
|
-
try {
|
|
1028
|
-
const currentBranch = await this.gitCommand(['branch', '--show-current']);
|
|
1029
|
-
await this.gitCommand(['push', 'origin', currentBranch]);
|
|
1030
|
-
this.log('✅ Push completado exitosamente');
|
|
1031
|
-
} catch (error) {
|
|
1032
|
-
console.error('❌ Error en push:', error);
|
|
1033
|
-
this.log('💡 Los commits están en tu repositorio local');
|
|
1034
|
-
throw error;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
/**
|
|
1039
|
-
* Valida que auto-approve es seguro de ejecutar
|
|
1040
|
-
*/
|
|
1041
|
-
private async validateAutoApprove(): Promise<boolean> {
|
|
1042
|
-
try {
|
|
1043
|
-
// Verificar que estamos en la rama correcta (master o main)
|
|
1044
|
-
const currentBranch = await this.gitCommand(['branch', '--show-current']);
|
|
1045
|
-
const validBranches = ['master', 'main'];
|
|
1046
|
-
if (!validBranches.includes(currentBranch)) {
|
|
1047
|
-
console.warn(`⚠️ No estás en una rama principal válida (actual: ${currentBranch}, válidas: ${validBranches.join(', ')})`);
|
|
1048
|
-
return false;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Verificar que el repositorio está limpio (sin conflictos)
|
|
1052
|
-
const statusOutput = await this.gitCommand(['status', '--porcelain']);
|
|
1053
|
-
const conflicts = statusOutput.split('\n').filter(line => line.startsWith('UU'));
|
|
1054
|
-
if (conflicts.length > 0) {
|
|
1055
|
-
console.error('❌ Hay conflictos de merge sin resolver');
|
|
1056
|
-
return false;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
return true;
|
|
1060
|
-
} catch (error) {
|
|
1061
|
-
console.error('❌ Error validando repositorio:', error);
|
|
1062
|
-
return false;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
/**
|
|
1067
|
-
* Ejecuta commits desde un archivo de propuesta existente
|
|
1068
|
-
*/
|
|
1069
|
-
private async executeFromProposalFile(proposalPath: string): Promise<void> {
|
|
1070
|
-
this.log(`📂 Cargando propuesta desde: ${proposalPath}`);
|
|
1071
|
-
|
|
1072
|
-
try {
|
|
1073
|
-
const proposalContent = readFileSync(proposalPath, 'utf-8');
|
|
1074
|
-
|
|
1075
|
-
// Obtener archivos disponibles
|
|
1076
|
-
const analysis = await this.generateAnalysisContext();
|
|
1077
|
-
const proposals = this.parseCommitProposals(proposalContent, analysis.files);
|
|
1078
|
-
|
|
1079
|
-
if (proposals.length === 0) {
|
|
1080
|
-
console.error('❌ No se encontraron commits válidos en el archivo de propuesta');
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
this.log(`📦 Encontrados ${proposals.length} commits en la propuesta:`);
|
|
1085
|
-
proposals.forEach((p, i) => {
|
|
1086
|
-
this.log(` ${i + 1}. ${p.title}`);
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
if (!this.autoApprove) {
|
|
1090
|
-
console.log('\n💡 Usa --auto-approve para ejecutar estos commits automáticamente');
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// Obtener archivos actuales para el contexto
|
|
1095
|
-
const files = await this.getRepositoryStatus();
|
|
1096
|
-
|
|
1097
|
-
let successfulCommits = 0;
|
|
1098
|
-
|
|
1099
|
-
for (let i = 0; i < proposals.length; i++) {
|
|
1100
|
-
const proposal = proposals[i];
|
|
1101
|
-
const success = await this.executeCommit(proposal, files);
|
|
1102
|
-
if (success) {
|
|
1103
|
-
successfulCommits++;
|
|
1104
|
-
} else {
|
|
1105
|
-
console.error(`❌ Falló commit ${i + 1}: ${proposal.title}`);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
this.log(`\n📊 Resultados: ${successfulCommits}/${proposals.length} commits exitosos`);
|
|
1110
|
-
|
|
1111
|
-
if (successfulCommits > 0) {
|
|
1112
|
-
await this.pushCommits();
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
this.log('\n✅ Ejecución desde archivo completada');
|
|
1116
|
-
|
|
1117
|
-
} catch (error) {
|
|
1118
|
-
console.error('❌ Error leyendo archivo de propuesta:', error);
|
|
1119
|
-
throw error;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
/**
|
|
1124
|
-
* Ejecuta typecheck para validar tipos antes del commit
|
|
1125
|
-
*/
|
|
1126
|
-
private async runTypeCheck(): Promise<void> {
|
|
1127
|
-
this.log('🔍 Ejecutando typecheck...');
|
|
1128
|
-
|
|
1129
|
-
try {
|
|
1130
|
-
const { spawnSync } = await import('child_process');
|
|
1131
|
-
const result = spawnSync('npm', ['run', 'type-check'], {
|
|
1132
|
-
cwd: this.projectRoot,
|
|
1133
|
-
encoding: 'utf-8',
|
|
1134
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
1135
|
-
});
|
|
1136
|
-
|
|
1137
|
-
if (result.status !== 0) {
|
|
1138
|
-
const error = result.stderr || result.stdout || 'Typecheck failed';
|
|
1139
|
-
this.log(`❌ Typecheck falló:`);
|
|
1140
|
-
this.log(error);
|
|
1141
|
-
throw new Error(`Typecheck failed: ${error}`);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
this.log('✅ Typecheck completado correctamente');
|
|
1145
|
-
} catch (error) {
|
|
1146
|
-
this.log(`❌ Error ejecutando typecheck: ${error}`);
|
|
1147
|
-
throw error;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* Ejecuta el generador completo
|
|
1153
|
-
*/
|
|
1154
|
-
async generate(): Promise<void> {
|
|
1155
|
-
// Si se especifica un archivo de propuesta existente, usarlo directamente
|
|
1156
|
-
if (this.proposalFile && existsSync(this.proposalFile)) {
|
|
1157
|
-
return this.executeFromProposalFile(this.proposalFile);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
this.log(`🚀 Iniciando generador de commits...${this.autoApprove ? ' (AUTO-APPROVE MODE)' : ''}\n`);
|
|
1161
|
-
|
|
1162
|
-
// Ejecutar typecheck antes de continuar
|
|
1163
|
-
await this.runTypeCheck();
|
|
1164
|
-
|
|
1165
|
-
const args = process.argv.slice(2);
|
|
1166
|
-
const isExhaustive = args.includes('-exhaustive');
|
|
1167
|
-
|
|
1168
|
-
// Parsear parámetros de contexto mejorados
|
|
1169
|
-
let extraContext = '';
|
|
1170
|
-
let workType = '';
|
|
1171
|
-
let contextDescription = '';
|
|
1172
|
-
let affectedComponents = '';
|
|
1173
|
-
let performanceImpact = '';
|
|
1174
|
-
let breakingChanges = '';
|
|
1175
|
-
|
|
1176
|
-
const extraIndex = args.indexOf('--extra');
|
|
1177
|
-
if (extraIndex > -1 && args[extraIndex + 1]) {
|
|
1178
|
-
extraContext = args[extraIndex + 1];
|
|
1179
|
-
this.log(`💬 Contexto extra proporcionado por el usuario.`);
|
|
1180
|
-
} else if (extraIndex > -1) {
|
|
1181
|
-
console.warn('⚠️ El parámetro --extra requiere un valor de texto después.');
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
const contextIndex = args.indexOf('--context');
|
|
1185
|
-
if (contextIndex > -1 && args[contextIndex + 1]) {
|
|
1186
|
-
contextDescription = args[contextIndex + 1];
|
|
1187
|
-
this.log(`📋 Contexto del trabajo: ${contextDescription}`);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
const workTypeIndex = args.indexOf('--work-type');
|
|
1191
|
-
if (workTypeIndex > -1 && args[workTypeIndex + 1]) {
|
|
1192
|
-
workType = args[workTypeIndex + 1];
|
|
1193
|
-
this.log(`🏷️ Tipo de trabajo: ${workType}`);
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
const componentsIndex = args.indexOf('--affected-components');
|
|
1197
|
-
if (componentsIndex > -1 && args[componentsIndex + 1]) {
|
|
1198
|
-
affectedComponents = args[componentsIndex + 1];
|
|
1199
|
-
this.log(`🎯 Componentes afectados: ${affectedComponents}`);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
const perfIndex = args.indexOf('--performance-impact');
|
|
1203
|
-
if (perfIndex > -1 && args[perfIndex + 1]) {
|
|
1204
|
-
performanceImpact = args[perfIndex + 1];
|
|
1205
|
-
this.log(`⚡ Impacto en rendimiento: ${performanceImpact}`);
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
const breakingIndex = args.indexOf('--breaking-changes');
|
|
1209
|
-
if (breakingIndex > -1 && args[breakingIndex + 1]) {
|
|
1210
|
-
breakingChanges = args[breakingIndex + 1];
|
|
1211
|
-
this.log(`⚠️ Cambios que rompen compatibilidad: ${breakingChanges}`);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
try {
|
|
1215
|
-
// Verificar que estamos en un repositorio git
|
|
1216
|
-
await this.gitCommand(['status']);
|
|
1217
|
-
|
|
1218
|
-
// Generar análisis completo
|
|
1219
|
-
const analysis = await this.generateAnalysisContext();
|
|
1220
|
-
|
|
1221
|
-
if (analysis.files.length === 0) {
|
|
1222
|
-
this.log('✅ No hay cambios para procesar');
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
const fileCount = analysis.files.length;
|
|
1227
|
-
const exhaustiveMode = isExhaustive || fileCount > 50;
|
|
1228
|
-
|
|
1229
|
-
this.log(`📋 Encontrados ${fileCount} archivos modificados`);
|
|
1230
|
-
this.log(`📊 Estadísticas: +${analysis.stats.total_additions} -${analysis.stats.total_deletions} líneas`);
|
|
1231
|
-
if (exhaustiveMode) {
|
|
1232
|
-
this.log('⚡️ Activado modo de análisis exhaustivo.');
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// Preparar contexto completo mejorado
|
|
1236
|
-
const enhancedContext = this.buildEnhancedContext(
|
|
1237
|
-
extraContext,
|
|
1238
|
-
contextDescription,
|
|
1239
|
-
workType,
|
|
1240
|
-
affectedComponents,
|
|
1241
|
-
performanceImpact,
|
|
1242
|
-
breakingChanges
|
|
1243
|
-
);
|
|
1244
|
-
|
|
1245
|
-
// Analizar con Gemini
|
|
1246
|
-
const commitProposal = await this.analyzeWithGemini(analysis, exhaustiveMode, enhancedContext);
|
|
1247
|
-
|
|
1248
|
-
// Guardar propuesta
|
|
1249
|
-
const proposalPath = this.saveCommitProposal(commitProposal);
|
|
1250
|
-
|
|
1251
|
-
if (this.autoApprove) {
|
|
1252
|
-
// Validar que es seguro ejecutar auto-approve
|
|
1253
|
-
const isValid = await this.validateAutoApprove();
|
|
1254
|
-
if (!isValid) {
|
|
1255
|
-
console.error('❌ Auto-approve cancelado por validaciones de seguridad');
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// Parsear y ejecutar commits
|
|
1260
|
-
this.log('\n🤖 Ejecutando commits automáticamente...');
|
|
1261
|
-
const proposals = this.parseCommitProposals(commitProposal, analysis.files);
|
|
1262
|
-
|
|
1263
|
-
if (proposals.length === 0) {
|
|
1264
|
-
console.warn('⚠️ No se encontraron commits válidos para ejecutar');
|
|
1265
|
-
console.log('📋 Revisa la propuesta manualmente:');
|
|
1266
|
-
console.log(commitProposal);
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// En modo quiet, mostrar progreso compacto
|
|
1271
|
-
if (this.quiet) {
|
|
1272
|
-
console.log(`🚀 Ejecutando ${proposals.length} commits propuestos...`);
|
|
1273
|
-
} else {
|
|
1274
|
-
this.log(`📦 Encontrados ${proposals.length} commits para ejecutar:`);
|
|
1275
|
-
proposals.forEach((p, i) => {
|
|
1276
|
-
this.log(` ${i + 1}. ${p.title}`);
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
let successfulCommits = 0;
|
|
1281
|
-
|
|
1282
|
-
// Ejecutar cada commit secuencialmente
|
|
1283
|
-
for (let i = 0; i < proposals.length; i++) {
|
|
1284
|
-
const proposal = proposals[i];
|
|
1285
|
-
const success = await this.executeCommit(proposal, analysis.files);
|
|
1286
|
-
if (success) {
|
|
1287
|
-
successfulCommits++;
|
|
1288
|
-
// Mostrar éxito incluso en modo quiet
|
|
1289
|
-
const status = this.quiet ? `✅ Commit ${i + 1}/${proposals.length}: "${proposal.title}"`
|
|
1290
|
-
: `✅ Commit ${i + 1} exitoso`;
|
|
1291
|
-
console.log(status);
|
|
1292
|
-
} else {
|
|
1293
|
-
// Mostrar errores siempre (incluso en quiet)
|
|
1294
|
-
console.error(`❌ Commit ${i + 1}/${proposals.length}: "${proposal.title}" - No hay cambios staged`);
|
|
1295
|
-
// Continuar con los siguientes commits
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// Mostrar resultados finales (siempre visible)
|
|
1300
|
-
console.log(`\n📊 Resultado: ${successfulCommits}/${proposals.length} commits exitosos`);
|
|
1301
|
-
|
|
1302
|
-
if (successfulCommits > 0) {
|
|
1303
|
-
await this.pushCommits();
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
this.log('\n✅ Auto-approve completado');
|
|
1307
|
-
|
|
1308
|
-
} else {
|
|
1309
|
-
// Modo normal - solo mostrar propuesta
|
|
1310
|
-
this.log('\n✅ Análisis completado');
|
|
1311
|
-
console.log(`📄 Propuesta guardada en: ${proposalPath}`);
|
|
1312
|
-
if (!this.quiet) {
|
|
1313
|
-
console.log('\n📋 Propuesta de commits:');
|
|
1314
|
-
console.log('─'.repeat(60));
|
|
1315
|
-
console.log(commitProposal);
|
|
1316
|
-
console.log('─'.repeat(60));
|
|
1317
|
-
console.log('\n💡 Usa --auto-approve para ejecutar automáticamente los commits');
|
|
1318
|
-
console.log('💡 O usa --proposal-file <ruta> para reutilizar esta propuesta');
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
} catch (error) {
|
|
1323
|
-
console.error('❌ Error en el generador:', error);
|
|
1324
|
-
process.exit(1);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// Ejecutar el generador si se llama directamente
|
|
1330
|
-
if (import.meta.main) {
|
|
1331
|
-
const args = process.argv.slice(2);
|
|
1332
|
-
|
|
1333
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
1334
|
-
console.log(`
|
|
1335
|
-
🚀 Generador Automático de Commits con Gemini CLI
|
|
1336
|
-
|
|
1337
|
-
Analiza cambios del repositorio y genera commits coherentes siguiendo los patrones del proyecto.
|
|
1338
|
-
|
|
1339
|
-
Uso:
|
|
1340
|
-
node project-utils/commit-generator.ts [opciones]
|
|
1341
|
-
|
|
1342
|
-
Opciones principales:
|
|
1343
|
-
--auto-approve Ejecutar automáticamente los commits propuestos y hacer push
|
|
1344
|
-
--proposal-file <ruta> Usar propuesta existente (ej: project-utils/.temp/commit-proposal-*.md)
|
|
1345
|
-
--quiet Ejecución silenciosa (solo errores y resultados finales)
|
|
1346
|
-
--output-dir <directorio> Directorio personalizado para archivos temporales
|
|
1347
|
-
|
|
1348
|
-
Opciones de configuración:
|
|
1349
|
-
--no-push Con --auto-approve, no hacer push (solo commits locales)
|
|
1350
|
-
--extra <texto> Contexto adicional para mejorar el análisis
|
|
1351
|
-
--context <descripción> Descripción del trabajo actual
|
|
1352
|
-
--work-type <tipo> Tipo: feature|fix|refactor|docs|test
|
|
1353
|
-
--affected-components <lista> Componentes afectados (ej: "core,styling,exports")
|
|
1354
|
-
--performance-impact <tipo> Impacto: none|minor|major
|
|
1355
|
-
--breaking-changes <si|no> Si introduce cambios incompatibles
|
|
1356
|
-
--exhaustive Análisis exhaustivo para proyectos complejos
|
|
1357
|
-
--help, -h Mostrar esta ayuda
|
|
1358
|
-
|
|
1359
|
-
Ejemplos:
|
|
1360
|
-
node project-utils/commit-generator.ts # Generar propuesta básica
|
|
1361
|
-
node project-utils/commit-generator.ts --auto-approve # Ejecutar automáticamente
|
|
1362
|
-
node project-utils/commit-generator.ts --quiet --auto-approve # Ejecución silenciosa
|
|
1363
|
-
node project-utils/commit-generator.ts --proposal-file project-utils/.temp/commit-proposal-20240101-120000.md --auto-approve
|
|
1364
|
-
node project-utils/commit-generator.ts --context "logger functionality" --work-type feature
|
|
1365
|
-
node project-utils/commit-generator.ts --work-type fix --affected-components "core,exports"
|
|
1366
|
-
|
|
1367
|
-
Modo Auto-Approve:
|
|
1368
|
-
- Valida estado del repositorio (rama main/master, sin conflictos)
|
|
1369
|
-
- Parsea commits propuestos por Gemini AI
|
|
1370
|
-
- Ejecuta cada commit secuencialmente con archivos apropiados
|
|
1371
|
-
- Hace push automático a origin/current-branch (excepto con --no-push)
|
|
1372
|
-
- Manejo de errores y rollback en caso de fallos
|
|
1373
|
-
|
|
1374
|
-
Seguridad:
|
|
1375
|
-
- Solo funciona en rama main o master
|
|
1376
|
-
- Validación de conflictos antes de ejecutar
|
|
1377
|
-
- Commits atómicos con manejo de errores individual
|
|
1378
|
-
- Logs completos de todas las operaciones
|
|
1379
|
-
`);
|
|
1380
|
-
process.exit(0);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
const generator = new CommitGenerator();
|
|
1384
|
-
await generator.generate();
|
|
1385
|
-
}
|