ai-changelog-generator-extension 0.4.0

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,370 @@
1
+ import * as vscode from 'vscode'
2
+ import { AIChangelogGenerator } from '@entro314labs/ai-changelog-generator'
3
+ import { ExtensionConfigurationManager } from '../services/ExtensionConfigurationManager'
4
+ import { StatusService } from '../services/StatusService'
5
+
6
+ export class ReleaseSidebarProvider implements vscode.WebviewViewProvider {
7
+ _view?: vscode.WebviewView
8
+
9
+ constructor(
10
+ private readonly _extensionUri: vscode.Uri,
11
+ private readonly _configManager: ExtensionConfigurationManager
12
+ ) {}
13
+
14
+ public resolveWebviewView(
15
+ webviewView: vscode.WebviewView,
16
+ context: vscode.WebviewViewResolveContext,
17
+ _token: vscode.CancellationToken
18
+ ) {
19
+ this._view = webviewView
20
+
21
+ webviewView.webview.options = {
22
+ enableScripts: true,
23
+ localResourceRoots: [this._extensionUri],
24
+ }
25
+
26
+ webviewView.webview.html = this._getHtmlForWebview(webviewView.webview)
27
+
28
+ webviewView.webview.onDidReceiveMessage(async (data) => {
29
+ switch (data.type) {
30
+ case 'refresh':
31
+ await this.refresh()
32
+ break
33
+ case 'generate':
34
+ await this.generateChangelog()
35
+ break
36
+ }
37
+ })
38
+
39
+ // Initial refresh
40
+ setTimeout(() => this.refresh(), 1000)
41
+ }
42
+
43
+ public async refresh() {
44
+ if (!this._view) {
45
+ return
46
+ }
47
+
48
+ StatusService.log('[ReleaseView] Starting refresh...', true)
49
+
50
+ try {
51
+ const workspaceFolder = this.getWorkspaceFolder()
52
+ if (!workspaceFolder) {
53
+ StatusService.log('[ReleaseView] No workspace folder', true)
54
+ this._view.webview.postMessage({
55
+ type: 'error',
56
+ message: 'No workspace folder open. Please open a git repository.',
57
+ })
58
+ return
59
+ }
60
+
61
+ const rootPath = workspaceFolder.uri.fsPath
62
+ const configPath = require('path').join(rootPath, '.env.local')
63
+
64
+ StatusService.log(`[ReleaseView] Repository: ${rootPath}`, true)
65
+
66
+ // Use AI Changelog Generator to get repository info
67
+ StatusService.log('[ReleaseView] Creating generator...', true)
68
+ const generator = new AIChangelogGenerator({
69
+ repositoryPath: rootPath,
70
+ configPath: require('fs').existsSync(configPath) ? configPath : undefined,
71
+ silent: true,
72
+ })
73
+
74
+ // Get tags - analyze repository to see if it has tags
75
+ let tags: string[] = []
76
+ try {
77
+ StatusService.log('[ReleaseView] Analyzing for tags...', true)
78
+
79
+ // Add timeout to prevent hanging
80
+ const timeoutPromise = new Promise((_, reject) =>
81
+ setTimeout(() => reject(new Error('Analysis timeout after 15 seconds')), 15000)
82
+ )
83
+
84
+ const analysis: any = await Promise.race([
85
+ generator.analyzeRepository(),
86
+ timeoutPromise
87
+ ])
88
+
89
+ tags = analysis.tags || []
90
+ StatusService.log(`[ReleaseView] Found ${tags.length} tags`, true)
91
+ } catch (e: any) {
92
+ // No tags yet - that's okay
93
+ StatusService.log(`[ReleaseView] No tags: ${e.message}`, true)
94
+ }
95
+
96
+ StatusService.log(`[ReleaseView] Posting ${tags.length} tags`, true)
97
+ this._view.webview.postMessage({ type: 'update', tags })
98
+ } catch (error: any) {
99
+ StatusService.log(`[ReleaseView] ERROR: ${error.message}`, true)
100
+ console.error('Error refreshing release view:', error)
101
+ this._view.webview.postMessage({
102
+ type: 'error',
103
+ message: `Failed to refresh: ${error.message || 'Unknown error'}`,
104
+ })
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get the current workspace folder, handling multi-root workspaces
110
+ */
111
+ private getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
112
+ const workspaceFolders = vscode.workspace.workspaceFolders
113
+ if (!workspaceFolders || workspaceFolders.length === 0) {
114
+ return undefined
115
+ }
116
+
117
+ // If there's an active text editor, use its workspace folder
118
+ if (vscode.window.activeTextEditor) {
119
+ const activeFolder = vscode.workspace.getWorkspaceFolder(
120
+ vscode.window.activeTextEditor.document.uri
121
+ )
122
+ if (activeFolder) {
123
+ return activeFolder
124
+ }
125
+ }
126
+
127
+ // Default to first workspace folder
128
+ return workspaceFolders[0]
129
+ }
130
+
131
+ private async generateChangelog() {
132
+ const workspaceFolder = this.getWorkspaceFolder()
133
+ if (!workspaceFolder) {
134
+ vscode.window.showErrorMessage('No workspace folder open')
135
+ return
136
+ }
137
+ const rootPath = workspaceFolder.uri.fsPath
138
+
139
+ try {
140
+ // Ensure configuration is initialized
141
+ if (!this._configManager.isInitialized()) {
142
+ await this._configManager.initialize()
143
+ }
144
+
145
+ const configPath = require('path').join(rootPath, '.env.local')
146
+
147
+ // Create AI Changelog Generator
148
+ const generator = new AIChangelogGenerator({
149
+ repositoryPath: rootPath,
150
+ configPath: require('fs').existsSync(configPath) ? configPath : undefined,
151
+ silent: true,
152
+ })
153
+
154
+ // Analyze recent commits to suggest next version
155
+ StatusService.log('[ReleaseView] Analyzing commits for version suggestion...', true)
156
+ const recentCommits = await generator.analyzeRecentCommits(50)
157
+
158
+ // Calculate suggested version based on semantic versioning rules
159
+ const currentVersion = await this.getCurrentVersion(rootPath)
160
+ const suggestedVersion = this.calculateNextVersion(currentVersion, recentCommits)
161
+
162
+ StatusService.log(`[ReleaseView] Current: ${currentVersion}, Suggested: ${suggestedVersion}`, true)
163
+
164
+ const version = await vscode.window.showInputBox({
165
+ prompt: `Enter release version (suggested: ${suggestedVersion} based on commit analysis)`,
166
+ placeHolder: suggestedVersion,
167
+ value: suggestedVersion,
168
+ valueSelection: undefined,
169
+ validateInput: (value) => {
170
+ if (!value || value.trim() === '') {
171
+ return 'Version is required'
172
+ }
173
+ // Basic version format validation
174
+ if (!/^v?\d+\.\d+\.\d+/.test(value)) {
175
+ return 'Please use semantic versioning format (e.g., v1.0.1 or 1.0.1)'
176
+ }
177
+ return null
178
+ },
179
+ })
180
+
181
+ if (!version) return
182
+
183
+ await vscode.window.withProgress(
184
+ {
185
+ location: vscode.ProgressLocation.Notification,
186
+ title: `Generating Changelog for ${version}...`,
187
+ cancellable: false,
188
+ },
189
+ async () => {
190
+ // Generate changelog for the version
191
+ const result = await generator.generateChangelog(version)
192
+
193
+ vscode.window.showInformationMessage(
194
+ `✅ Changelog generated successfully for ${version}!`,
195
+ 'Open File'
196
+ ).then((action) => {
197
+ if (action === 'Open File') {
198
+ // Open the generated changelog file
199
+ const changelogPath = require('path').join(rootPath, 'AI_CHANGELOG.md')
200
+ vscode.workspace.openTextDocument(changelogPath).then(doc => {
201
+ vscode.window.showTextDocument(doc)
202
+ })
203
+ }
204
+ })
205
+
206
+ // Refresh the view
207
+ setTimeout(() => this.refresh(), 500)
208
+ }
209
+ )
210
+ } catch (error: any) {
211
+ console.error('Changelog generation failed:', error)
212
+ vscode.window.showErrorMessage(
213
+ `Failed to generate changelog: ${error.message || 'Unknown error'}`
214
+ )
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Get current version from git tags or package.json
220
+ */
221
+ private async getCurrentVersion(rootPath: string): Promise<string> {
222
+ try {
223
+ // Try to get latest git tag first
224
+ const { execSync } = require('child_process')
225
+ const latestTag = execSync('git describe --tags --abbrev=0', {
226
+ cwd: rootPath,
227
+ encoding: 'utf8',
228
+ stdio: ['pipe', 'pipe', 'ignore']
229
+ }).trim()
230
+
231
+ if (latestTag) {
232
+ return latestTag
233
+ }
234
+ } catch (e) {
235
+ // No tags yet, try package.json
236
+ }
237
+
238
+ try {
239
+ const packageJsonPath = require('path').join(rootPath, 'package.json')
240
+ const fs = require('fs')
241
+ if (fs.existsSync(packageJsonPath)) {
242
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
243
+ if (packageJson.version) {
244
+ return packageJson.version.startsWith('v') ? packageJson.version : `v${packageJson.version}`
245
+ }
246
+ }
247
+ } catch (e) {
248
+ // No package.json
249
+ }
250
+
251
+ return 'v0.0.0'
252
+ }
253
+
254
+ /**
255
+ * Calculate next semantic version based on commit types
256
+ * - feat = minor bump (0.1.0 -> 0.2.0)
257
+ * - fix = patch bump (0.1.0 -> 0.1.1)
258
+ * - breaking = major bump (0.1.0 -> 1.0.0)
259
+ */
260
+ private calculateNextVersion(currentVersion: string, commits: any): string {
261
+ // Parse current version
262
+ const versionMatch = currentVersion.match(/v?(\d+)\.(\d+)\.(\d+)/)
263
+ if (!versionMatch) {
264
+ return 'v0.1.0'
265
+ }
266
+
267
+ let [, major, minor, patch] = versionMatch.map(Number)
268
+
269
+ // Analyze commits for version bump type
270
+ const hasBreaking = commits.some((c: any) =>
271
+ c.breaking ||
272
+ c.breakingChanges?.length > 0 ||
273
+ c.message?.includes('BREAKING CHANGE')
274
+ )
275
+
276
+ const hasFeat = commits.some((c: any) =>
277
+ c.type === 'feat' ||
278
+ c.message?.startsWith('feat:') ||
279
+ c.message?.startsWith('feat(')
280
+ )
281
+
282
+ const hasFix = commits.some((c: any) =>
283
+ c.type === 'fix' ||
284
+ c.message?.startsWith('fix:') ||
285
+ c.message?.startsWith('fix(')
286
+ )
287
+
288
+ StatusService.log(`[ReleaseView] Version analysis: breaking=${hasBreaking}, feat=${hasFeat}, fix=${hasFix}`, true)
289
+
290
+ if (hasBreaking) {
291
+ // Major version bump
292
+ major++
293
+ minor = 0
294
+ patch = 0
295
+ } else if (hasFeat) {
296
+ // Minor version bump
297
+ minor++
298
+ patch = 0
299
+ } else if (hasFix) {
300
+ // Patch version bump
301
+ patch++
302
+ } else {
303
+ // No significant changes, suggest patch bump
304
+ patch++
305
+ }
306
+
307
+ return `v${major}.${minor}.${patch}`
308
+ }
309
+
310
+ private _getHtmlForWebview(webview: vscode.Webview) {
311
+ return `<!DOCTYPE html>
312
+ <html lang="en">
313
+ <head>
314
+ <meta charset="UTF-8">
315
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
316
+ <title>Release Manager</title>
317
+ <style>
318
+ body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 10px; }
319
+ .tag-item { display: flex; align-items: center; margin-bottom: 5px; padding: 5px; background: var(--vscode-list-hoverBackground); border-radius: 4px; }
320
+ .tag-icon { margin-right: 10px; }
321
+ .tag-name { flex: 1; font-weight: bold; }
322
+ .button { background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; padding: 8px 12px; cursor: pointer; width: 100%; margin-top: 10px; }
323
+ .button:hover { background-color: var(--vscode-button-hoverBackground); }
324
+ .error { color: var(--vscode-errorForeground); }
325
+ h3 { margin-top: 0; }
326
+ </style>
327
+ </head>
328
+ <body>
329
+ <h3>Recent Tags</h3>
330
+ <div id="tag-list">Loading...</div>
331
+
332
+ <button class="button" onclick="handleGenerate()">Draft New Release</button>
333
+ <button class="button" onclick="handleRefresh()">Refresh System</button>
334
+
335
+ <script>
336
+ const vscode = acquireVsCodeApi();
337
+
338
+ window.addEventListener('message', event => {
339
+ const message = event.data;
340
+ const list = document.getElementById('tag-list');
341
+
342
+ if (message.type === 'update') {
343
+ if (message.tags.length === 0) {
344
+ list.innerHTML = '<p>No tags found.</p>';
345
+ return;
346
+ }
347
+
348
+ list.innerHTML = message.tags.map(t => \`
349
+ <div class="tag-item">
350
+ <span class="tag-icon">🏷️</span>
351
+ <span class="tag-name">\${t}</span>
352
+ </div>
353
+ \`).join('');
354
+ } else if (message.type === 'error') {
355
+ list.innerHTML = \`<p class="error">\${message.message}</p>\`;
356
+ }
357
+ });
358
+
359
+ function handleRefresh() {
360
+ vscode.postMessage({ type: 'refresh' });
361
+ }
362
+
363
+ function handleGenerate() {
364
+ vscode.postMessage({ type: 'generate' });
365
+ }
366
+ </script>
367
+ </body>
368
+ </html>`
369
+ }
370
+ }