diff --git a/auto-claude-ui/src/__tests__/integration/subprocess-spawn.test.ts b/auto-claude-ui/src/__tests__/integration/subprocess-spawn.test.ts index d753a9d4..1aeced25 100644 --- a/auto-claude-ui/src/__tests__/integration/subprocess-spawn.test.ts +++ b/auto-claude-ui/src/__tests__/integration/subprocess-spawn.test.ts @@ -39,8 +39,8 @@ function setupTestDirs(): void { // Create auto-claude source directory that getAutoBuildSourcePath looks for mkdirSync(AUTO_CLAUDE_SOURCE, { recursive: true }); - // Create VERSION file (required by getAutoBuildSourcePath) - writeFileSync(path.join(AUTO_CLAUDE_SOURCE, 'VERSION'), '1.0.0'); + // Create requirements.txt file (used as marker by getAutoBuildSourcePath) + writeFileSync(path.join(AUTO_CLAUDE_SOURCE, 'requirements.txt'), '# Mock requirements'); // Create runners subdirectory (where spec_runner.py lives after restructure) mkdirSync(path.join(AUTO_CLAUDE_SOURCE, 'runners'), { recursive: true }); diff --git a/auto-claude-ui/src/main/agent/agent-process.ts b/auto-claude-ui/src/main/agent/agent-process.ts index d885637c..d0afe73e 100644 --- a/auto-claude-ui/src/main/agent/agent-process.ts +++ b/auto-claude-ui/src/main/agent/agent-process.ts @@ -65,7 +65,8 @@ export class AgentProcessManager { ]; for (const p of possiblePaths) { - if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) { + // Use requirements.txt as marker - it always exists in auto-claude source + if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { return p; } } @@ -174,7 +175,8 @@ export class AgentProcessManager { ...process.env, ...extraEnv, ...profileEnv, // Include active Claude profile config - PYTHONUNBUFFERED: '1' // Ensure real-time output + PYTHONUNBUFFERED: '1', // Ensure real-time output + PYTHONIOENCODING: 'utf-8' // Ensure UTF-8 encoding on Windows } }); diff --git a/auto-claude-ui/src/main/changelog/changelog-service.ts b/auto-claude-ui/src/main/changelog/changelog-service.ts index da293039..a5cf5b87 100644 --- a/auto-claude-ui/src/main/changelog/changelog-service.ts +++ b/auto-claude-ui/src/main/changelog/changelog-service.ts @@ -150,7 +150,8 @@ export class ChangelogService extends EventEmitter { ]; for (const p of possiblePaths) { - if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) { + // Use requirements.txt as marker - it always exists in auto-claude source + if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { return p; } } diff --git a/auto-claude-ui/src/main/insights/config.ts b/auto-claude-ui/src/main/insights/config.ts index 9ac2213b..11884379 100644 --- a/auto-claude-ui/src/main/insights/config.ts +++ b/auto-claude-ui/src/main/insights/config.ts @@ -45,7 +45,8 @@ export class InsightsConfig { ]; for (const p of possiblePaths) { - if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) { + // Use requirements.txt as marker - it always exists in auto-claude source + if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { return p; } } @@ -103,7 +104,8 @@ export class InsightsConfig { ...process.env as Record, ...autoBuildEnv, ...profileEnv, - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8' }; } } diff --git a/auto-claude-ui/src/main/ipc-handlers/github/release-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/github/release-handlers.ts index ae8e542b..431359fe 100644 --- a/auto-claude-ui/src/main/ipc-handlers/github/release-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/github/release-handlers.ts @@ -4,9 +4,12 @@ import { ipcMain } from 'electron'; import { execSync } from 'child_process'; +import { existsSync, readFileSync } from 'fs'; +import path from 'path'; import { IPC_CHANNELS } from '../../../shared/constants'; -import type { IPCResult } from '../../../shared/types'; +import type { IPCResult, GitCommit, VersionSuggestion } from '../../../shared/types'; import { projectStore } from '../../project-store'; +import { changelogService } from '../../changelog-service'; import type { ReleaseOptions } from './types'; /** @@ -118,9 +121,146 @@ export function registerCreateRelease(): void { ); } +/** + * Get the latest git tag in the repository + */ +function getLatestTag(projectPath: string): string | null { + try { + const tag = execSync('git describe --tags --abbrev=0 2>/dev/null || echo ""', { + cwd: projectPath, + encoding: 'utf-8' + }).trim(); + return tag || null; + } catch { + return null; + } +} + +/** + * Get commits since a specific tag (or all commits if no tag) + */ +function getCommitsSinceTag(projectPath: string, tag: string | null): GitCommit[] { + try { + const range = tag ? `${tag}..HEAD` : 'HEAD'; + const format = '%H|%s|%an|%ae|%aI'; + const output = execSync(`git log ${range} --pretty=format:"${format}"`, { + cwd: projectPath, + encoding: 'utf-8' + }).trim(); + + if (!output) return []; + + return output.split('\n').map(line => { + const [fullHash, subject, authorName, authorEmail, date] = line.split('|'); + return { + hash: fullHash.substring(0, 7), + fullHash, + subject, + author: authorName, + authorEmail, + date + }; + }); + } catch { + return []; + } +} + +/** + * Get current version from package.json + */ +function getCurrentVersion(projectPath: string): string { + try { + const pkgPath = path.join(projectPath, 'package.json'); + if (!existsSync(pkgPath)) { + return '0.0.0'; + } + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + return pkg.version || '0.0.0'; + } catch { + return '0.0.0'; + } +} + +/** + * Suggest version for release using AI analysis of commits + */ +export function registerSuggestVersion(): void { + ipcMain.handle( + IPC_CHANNELS.RELEASE_SUGGEST_VERSION, + async (_, projectId: string): Promise> => { + const project = projectStore.getProject(projectId); + if (!project) { + return { success: false, error: 'Project not found' }; + } + + try { + // Get current version from package.json + const currentVersion = getCurrentVersion(project.path); + + // Get latest tag + const latestTag = getLatestTag(project.path); + + // Get commits since last tag + const commits = getCommitsSinceTag(project.path, latestTag); + + if (commits.length === 0) { + // No commits since last release, suggest patch bump + const [major, minor, patch] = currentVersion.split('.').map(Number); + return { + success: true, + data: { + suggestedVersion: `${major}.${minor}.${patch + 1}`, + currentVersion, + bumpType: 'patch', + reason: 'No new commits since last release', + commitCount: 0 + } + }; + } + + // Use AI to analyze commits and suggest version + const suggestion = await changelogService.suggestVersionFromCommits( + project.path, + commits, + currentVersion + ); + + return { + success: true, + data: { + suggestedVersion: suggestion.version, + currentVersion, + bumpType: suggestion.reason.includes('breaking') ? 'major' : + suggestion.reason.includes('feature') || suggestion.reason.includes('minor') ? 'minor' : 'patch', + reason: suggestion.reason, + commitCount: commits.length + } + }; + } catch (error) { + // Fallback to patch bump on error + const currentVersion = getCurrentVersion(project.path); + const [major, minor, patch] = currentVersion.split('.').map(Number); + + return { + success: true, + data: { + suggestedVersion: `${major}.${minor}.${patch + 1}`, + currentVersion, + bumpType: 'patch', + reason: 'Fallback suggestion (AI analysis unavailable)', + commitCount: 0 + } + }; + } + } + ); +} + /** * Register all release-related handlers */ export function registerReleaseHandlers(): void { registerCreateRelease(); + registerSuggestVersion(); } diff --git a/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts index 7cd07d45..868a5b25 100644 --- a/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts @@ -146,8 +146,9 @@ const detectAutoBuildSourcePath = (): string | null => { } for (const p of possiblePaths) { - const versionPath = path.join(p, 'VERSION'); - const exists = existsSync(p) && existsSync(versionPath); + // Use requirements.txt as marker - it always exists in auto-claude source + const markerPath = path.join(p, 'requirements.txt'); + const exists = existsSync(p) && existsSync(markerPath); if (debug) { console.log(`[project-handlers:detectAutoBuildSourcePath] Checking ${p}: ${exists ? '✓ FOUND' : '✗ not found'}`); diff --git a/auto-claude-ui/src/main/ipc-handlers/settings-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/settings-handlers.ts index 955583b9..a724731f 100644 --- a/auto-claude-ui/src/main/ipc-handlers/settings-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/settings-handlers.ts @@ -60,8 +60,9 @@ const detectAutoBuildSourcePath = (): string | null => { } for (const p of possiblePaths) { - const versionPath = path.join(p, 'VERSION'); - const exists = existsSync(p) && existsSync(versionPath); + // Use requirements.txt as marker - it always exists in auto-claude source + const markerPath = path.join(p, 'requirements.txt'); + const exists = existsSync(p) && existsSync(markerPath); if (debug) { console.log(`[detectAutoBuildSourcePath] Checking ${p}: ${exists ? '✓ FOUND' : '✗ not found'}`); diff --git a/auto-claude-ui/src/main/project-initializer.ts b/auto-claude-ui/src/main/project-initializer.ts index 4b121511..9bc687b2 100644 --- a/auto-claude-ui/src/main/project-initializer.ts +++ b/auto-claude-ui/src/main/project-initializer.ts @@ -101,8 +101,9 @@ export interface InitializationResult { */ export function hasLocalSource(projectPath: string): boolean { const localSourcePath = path.join(projectPath, 'auto-claude'); - const versionFile = path.join(localSourcePath, 'VERSION'); - return existsSync(localSourcePath) && existsSync(versionFile); + // Use requirements.txt as marker - it always exists in auto-claude source + const markerFile = path.join(localSourcePath, 'requirements.txt'); + return existsSync(localSourcePath) && existsSync(markerFile); } /** diff --git a/auto-claude-ui/src/main/release-service.ts b/auto-claude-ui/src/main/release-service.ts index 640c6d11..f17501ce 100644 --- a/auto-claude-ui/src/main/release-service.ts +++ b/auto-claude-ui/src/main/release-service.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; import path from 'path'; -import { existsSync, readFileSync, readdirSync } from 'fs'; +import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'; import { execSync, spawn } from 'child_process'; import type { ReleaseableVersion, @@ -17,7 +17,7 @@ import { DEFAULT_CHANGELOG_PATH } from '../shared/constants'; /** * Service for creating GitHub releases with worktree-aware pre-flight checks. - * + * * Key feature: Worktree checks are SCOPED to tasks in the release version. * If a worktree exists for a task NOT in this release, it won't block the release. */ @@ -32,7 +32,7 @@ export class ReleaseService extends EventEmitter { */ parseChangelogVersions(projectPath: string): ReleaseableVersion[] { const changelogPath = path.join(projectPath, DEFAULT_CHANGELOG_PATH); - + if (!existsSync(changelogPath)) { return []; } @@ -49,7 +49,7 @@ export class ReleaseService extends EventEmitter { const version = match[1]; const date = match[2] || ''; const startIndex = match.index! + match[0].length; - + // Content is until next version header or end of file const endIndex = i < matches.length - 1 ? matches[i + 1].index! : content.length; const versionContent = content.slice(startIndex, endIndex).trim(); @@ -98,17 +98,17 @@ export class ReleaseService extends EventEmitter { tasks: Task[] ): Promise { const versions = this.parseChangelogVersions(projectPath); - + // Populate task spec IDs for each version for (const version of versions) { const { specIds } = this.getTasksForVersion(projectPath, version.version, tasks); version.taskSpecIds = specIds; - + // Check if already released on GitHub try { const tagExists = this.checkTagExists(projectPath, version.tagName); version.isReleased = tagExists; - + if (tagExists) { // Try to get release URL version.releaseUrl = this.getGitHubReleaseUrl(projectPath, version.tagName); @@ -129,11 +129,11 @@ export class ReleaseService extends EventEmitter { try { // Check local tags execSync(`git tag -l "${tagName}"`, { cwd: projectPath, encoding: 'utf-8' }); - const localTags = execSync(`git tag -l "${tagName}"`, { - cwd: projectPath, - encoding: 'utf-8' + const localTags = execSync(`git tag -l "${tagName}"`, { + cwd: projectPath, + encoding: 'utf-8' }).trim(); - + if (localTags) return true; // Check remote tags @@ -147,7 +147,7 @@ export class ReleaseService extends EventEmitter { cwd: projectPath, encoding: 'utf-8' }).trim(); - + return !!remoteTags; } catch { return false; @@ -166,7 +166,7 @@ export class ReleaseService extends EventEmitter { cwd: projectPath, encoding: 'utf-8' }).trim(); - + return result || undefined; } catch { return undefined; @@ -175,7 +175,7 @@ export class ReleaseService extends EventEmitter { /** * Run pre-flight checks for a specific version. - * + * * IMPORTANT: Worktree checks are scoped to tasks in this version only. * Worktrees for other tasks (future releases) won't block this release. */ @@ -186,7 +186,7 @@ export class ReleaseService extends EventEmitter { ): Promise { const tagName = `v${version}`; const { specIds } = this.getTasksForVersion(projectPath, version, tasks); - + const status: ReleasePreflightStatus = { canRelease: false, checks: { @@ -303,7 +303,7 @@ export class ReleaseService extends EventEmitter { if (unmergedWorktrees.length === 0) { status.checks.worktreesMerged = { passed: true, - message: specIds.length > 0 + message: specIds.length > 0 ? `All ${specIds.length} feature(s) in this release are merged` : 'No features to check (version may have been manually added)', unmergedWorktrees: [] @@ -314,7 +314,7 @@ export class ReleaseService extends EventEmitter { message: `${unmergedWorktrees.length} feature(s) have unmerged worktrees`, unmergedWorktrees }; - + for (const wt of unmergedWorktrees) { status.blockers.push( `Feature "${wt.taskTitle}" (${wt.specId}) has unmerged changes in worktree` @@ -330,7 +330,7 @@ export class ReleaseService extends EventEmitter { /** * Check worktrees ONLY for tasks that are part of this release version. - * + * * This is the key function that scopes worktree checks to the release: * - If a task is in the release AND has an unmerged worktree → BLOCK * - If a task is NOT in the release but has a worktree → IGNORE (it's for a future release) @@ -344,7 +344,7 @@ export class ReleaseService extends EventEmitter { // Get worktrees directory const worktreesDir = path.join(projectPath, '.worktrees', 'auto-claude'); - + if (!existsSync(worktreesDir)) { // No worktrees exist at all - all clear return []; @@ -364,7 +364,7 @@ export class ReleaseService extends EventEmitter { for (const specId of releaseSpecIds) { // Find the worktree folder for this spec // Spec IDs are like "001-feature-name", worktree folders match - const worktreeFolder = worktreeFolders.find(folder => + const worktreeFolder = worktreeFolders.find(folder => folder === specId || folder.startsWith(`${specId}-`) ); @@ -374,7 +374,7 @@ export class ReleaseService extends EventEmitter { } const worktreePath = path.join(worktreesDir, worktreeFolder); - + // Get the task info for better error messages const task = tasks.find(t => t.specId === specId); const taskTitle = task?.title || specId; @@ -442,7 +442,7 @@ export class ReleaseService extends EventEmitter { cwd: worktreePath, encoding: 'utf-8' }).trim(); - + return !hasChanges; } @@ -454,7 +454,167 @@ export class ReleaseService extends EventEmitter { } /** - * Create a GitHub release. + * Bump version in package.json with safe git workflow. + * Preserves user's current work by stashing, switching to main, then restoring. + */ + async bumpVersion( + projectPath: string, + version: string, + mainBranch: string, + projectId: string + ): Promise<{ success: boolean; error?: string }> { + // Save current state + let originalBranch: string; + let hadChanges = false; + let stashCreated = false; + + try { + originalBranch = execSync('git rev-parse --abbrev-ref HEAD', { + cwd: projectPath, + encoding: 'utf-8' + }).trim(); + } catch { + return { success: false, error: 'Failed to get current git branch' }; + } + + // Check for uncommitted changes + const gitStatus = execSync('git status --porcelain', { + cwd: projectPath, + encoding: 'utf-8' + }).trim(); + hadChanges = !!gitStatus; + + try { + // Stash any changes (staged or unstaged) + if (hadChanges) { + this.emitProgress(projectId, { + stage: 'bumping_version', + progress: 5, + message: 'Stashing current changes...' + }); + + execSync('git stash push -m "auto-claude-release-temp"', { + cwd: projectPath, + encoding: 'utf-8' + }); + stashCreated = true; + } + + // Checkout main branch + this.emitProgress(projectId, { + stage: 'bumping_version', + progress: 10, + message: `Switching to ${mainBranch}...` + }); + + if (originalBranch !== mainBranch) { + execSync(`git checkout "${mainBranch}"`, { + cwd: projectPath, + encoding: 'utf-8' + }); + } + + // Pull latest from origin + this.emitProgress(projectId, { + stage: 'bumping_version', + progress: 15, + message: `Pulling latest from origin/${mainBranch}...` + }); + + try { + execSync(`git pull origin "${mainBranch}"`, { + cwd: projectPath, + encoding: 'utf-8' + }); + } catch { + // Pull might fail if no upstream, continue anyway + } + + // Update package.json + this.emitProgress(projectId, { + stage: 'bumping_version', + progress: 20, + message: `Updating package.json to ${version}...` + }); + + const pkgPath = path.join(projectPath, 'package.json'); + if (!existsSync(pkgPath)) { + throw new Error('package.json not found in project root'); + } + + const pkgContent = readFileSync(pkgPath, 'utf-8'); + const pkg = JSON.parse(pkgContent); + pkg.version = version; + + // Preserve formatting (detect indent) + const indent = pkgContent.match(/^(\s+)/m)?.[1] || ' '; + writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + '\n'); + + // Stage and commit only package.json + this.emitProgress(projectId, { + stage: 'bumping_version', + progress: 25, + message: 'Committing version bump...' + }); + + execSync('git add package.json', { + cwd: projectPath, + encoding: 'utf-8' + }); + + execSync(`git commit -m "chore: release v${version}"`, { + cwd: projectPath, + encoding: 'utf-8' + }); + + // Push to origin + this.emitProgress(projectId, { + stage: 'bumping_version', + progress: 30, + message: `Pushing to origin/${mainBranch}...` + }); + + execSync(`git push origin "${mainBranch}"`, { + cwd: projectPath, + encoding: 'utf-8' + }); + + return { success: true }; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + + } finally { + // Always restore user's original state + try { + if (originalBranch !== mainBranch) { + execSync(`git checkout "${originalBranch}"`, { + cwd: projectPath, + encoding: 'utf-8' + }); + } + } catch { + // Log but don't fail - user might need to manually switch back + console.warn('[ReleaseService] Failed to restore original branch'); + } + + if (stashCreated) { + try { + execSync('git stash pop', { + cwd: projectPath, + encoding: 'utf-8' + }); + } catch { + // Stash conflict - warn user + console.warn('[ReleaseService] Failed to pop stash - user may need to run "git stash pop" manually'); + } + } + } + } + + /** + * Create a GitHub release with optional version bump. */ async createRelease( projectPath: string, @@ -462,12 +622,36 @@ export class ReleaseService extends EventEmitter { ): Promise { const tagName = `v${request.version}`; const title = request.title || tagName; + const shouldBumpVersion = request.bumpVersion !== false; // Default to true try { + // Stage 0: Bump version in package.json (if enabled) + if (shouldBumpVersion && request.mainBranch) { + const bumpResult = await this.bumpVersion( + projectPath, + request.version, + request.mainBranch, + request.projectId + ); + + if (!bumpResult.success) { + this.emitProgress(request.projectId, { + stage: 'error', + progress: 0, + message: `Version bump failed: ${bumpResult.error}`, + error: bumpResult.error + }); + return { + success: false, + error: `Version bump failed: ${bumpResult.error}` + }; + } + } + // Stage 1: Create local tag this.emitProgress(request.projectId, { stage: 'tagging', - progress: 25, + progress: 40, message: `Creating tag ${tagName}...` }); @@ -479,7 +663,7 @@ export class ReleaseService extends EventEmitter { // Stage 2: Push tag to remote this.emitProgress(request.projectId, { stage: 'pushing', - progress: 50, + progress: 60, message: `Pushing tag ${tagName} to origin...` }); @@ -491,7 +675,7 @@ export class ReleaseService extends EventEmitter { // Stage 3: Create GitHub release this.emitProgress(request.projectId, { stage: 'creating_release', - progress: 75, + progress: 80, message: 'Creating GitHub release...' }); @@ -567,7 +751,7 @@ export class ReleaseService extends EventEmitter { } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - + // Try to clean up the tag if it was created but release failed try { execSync(`git tag -d "${tagName}" 2>/dev/null || true`, { @@ -602,4 +786,3 @@ export class ReleaseService extends EventEmitter { // Export singleton instance export const releaseService = new ReleaseService(); - diff --git a/auto-claude-ui/src/main/terminal-name-generator.ts b/auto-claude-ui/src/main/terminal-name-generator.ts index 7e62b72f..d5d8010e 100644 --- a/auto-claude-ui/src/main/terminal-name-generator.ts +++ b/auto-claude-ui/src/main/terminal-name-generator.ts @@ -55,7 +55,8 @@ export class TerminalNameGenerator extends EventEmitter { ]; for (const p of possiblePaths) { - if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) { + // Use requirements.txt as marker - it always exists in auto-claude source + if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { return p; } } @@ -135,7 +136,8 @@ export class TerminalNameGenerator extends EventEmitter { ...process.env, ...autoBuildEnv, ...profileEnv, // Include active Claude profile config - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8' } }); diff --git a/auto-claude-ui/src/main/title-generator.ts b/auto-claude-ui/src/main/title-generator.ts index 4c272643..e53fb6ee 100644 --- a/auto-claude-ui/src/main/title-generator.ts +++ b/auto-claude-ui/src/main/title-generator.ts @@ -55,7 +55,8 @@ export class TitleGenerator extends EventEmitter { ]; for (const p of possiblePaths) { - if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) { + // Use requirements.txt as marker - it always exists in auto-claude source + if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { return p; } } @@ -134,7 +135,8 @@ export class TitleGenerator extends EventEmitter { ...process.env, ...autoBuildEnv, ...profileEnv, // Include active Claude profile config - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8' } }); diff --git a/auto-claude-ui/src/preload/api/modules/github-api.ts b/auto-claude-ui/src/preload/api/modules/github-api.ts index dc558341..fe8f9977 100644 --- a/auto-claude-ui/src/preload/api/modules/github-api.ts +++ b/auto-claude-ui/src/preload/api/modules/github-api.ts @@ -6,7 +6,8 @@ import type { GitHubImportResult, GitHubInvestigationStatus, GitHubInvestigationResult, - IPCResult + IPCResult, + VersionSuggestion } from '../../../shared/types'; import { createIpcListener, invokeIpc, sendIpc, IpcListenerCleanup } from './ipc-utils'; @@ -28,6 +29,9 @@ export interface GitHubAPI { options?: { draft?: boolean; prerelease?: boolean } ) => Promise>; + /** AI-powered version suggestion based on commits since last release */ + suggestReleaseVersion: (projectId: string) => Promise>; + // OAuth operations (gh CLI) checkGitHubCli: () => Promise>; checkGitHubAuth: () => Promise>; @@ -79,6 +83,9 @@ export const createGitHubAPI = (): GitHubAPI => ({ ): Promise> => invokeIpc(IPC_CHANNELS.GITHUB_CREATE_RELEASE, projectId, version, releaseNotes, options), + suggestReleaseVersion: (projectId: string): Promise> => + invokeIpc(IPC_CHANNELS.RELEASE_SUGGEST_VERSION, projectId), + // OAuth operations (gh CLI) checkGitHubCli: (): Promise> => invokeIpc(IPC_CHANNELS.GITHUB_CHECK_CLI), diff --git a/auto-claude-ui/src/shared/constants/ipc.ts b/auto-claude-ui/src/shared/constants/ipc.ts index 98caab75..3999b4d4 100644 --- a/auto-claude-ui/src/shared/constants/ipc.ts +++ b/auto-claude-ui/src/shared/constants/ipc.ts @@ -269,5 +269,14 @@ export const IPC_CHANNELS = { APP_UPDATE_AVAILABLE: 'app-update:available', APP_UPDATE_DOWNLOADED: 'app-update:downloaded', APP_UPDATE_PROGRESS: 'app-update:progress', - APP_UPDATE_ERROR: 'app-update:error' + APP_UPDATE_ERROR: 'app-update:error', + + // Release operations + RELEASE_SUGGEST_VERSION: 'release:suggestVersion', + RELEASE_CREATE: 'release:create', + RELEASE_PREFLIGHT: 'release:preflight', + RELEASE_GET_VERSIONS: 'release:getVersions', + + // Release events (main -> renderer) + RELEASE_PROGRESS: 'release:progress' } as const; diff --git a/auto-claude-ui/src/shared/types/changelog.ts b/auto-claude-ui/src/shared/types/changelog.ts index 7253ecfe..b7fec5f7 100644 --- a/auto-claude-ui/src/shared/types/changelog.ts +++ b/auto-claude-ui/src/shared/types/changelog.ts @@ -183,6 +183,10 @@ export interface CreateReleaseRequest { body: string; draft?: boolean; prerelease?: boolean; + /** Main branch to push version bump to (uses project setting if not specified) */ + mainBranch?: string; + /** Whether to bump version in package.json before release (default: true) */ + bumpVersion?: boolean; } export interface CreateReleaseResult { @@ -193,8 +197,19 @@ export interface CreateReleaseResult { } export interface ReleaseProgress { - stage: 'checking' | 'tagging' | 'pushing' | 'creating_release' | 'complete' | 'error'; + stage: 'bumping_version' | 'checking' | 'tagging' | 'pushing' | 'creating_release' | 'complete' | 'error'; progress: number; message: string; error?: string; } + +/** + * AI-powered version suggestion result + */ +export interface VersionSuggestion { + suggestedVersion: string; + currentVersion: string; + bumpType: 'major' | 'minor' | 'patch'; + reason: string; + commitCount: number; +}