Fix/update app (#594)

* cleanup/readme

* fix(updater): remove redundant source updater and add beta→stable downgrade

The app had two update systems: electron-updater (correct) and a
redundant "source updater" that caused version desync. After updating,
getEffectiveVersion() checked stale .update-metadata.json first,
showing wrong version numbers.

Changes:
- Remove redundant auto-claude-updater and source update handlers
- Clean up stale metadata directories on app startup
- Use app.getVersion() directly for version display
- Add beta→stable downgrade when user disables beta updates
- Fetch latest stable from GitHub API and offer to install

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(updater): enable stable version downgrade with allowDowngrade flag

Fixes critical issue where electron-updater's semver comparison prevented
downloading older stable versions when on a beta release. Also addresses
several robustness issues in the update mechanism:

- Set allowDowngrade=true in downloadStableVersion() to enable downgrades
- Add dedicated IPC channel APP_UPDATE_DOWNLOAD_STABLE for stable downloads
- Add HTTP status code validation to GitHub API requests
- Add 10-second timeout to prevent hanging requests
- Add JSON array validation before processing releases
- Fix fire-and-forget async call with proper error handling
- Fix UI handlers to check IPCResult and reset loading state on failure
- Clear beta update info when disabling beta so stable downgrade UI shows

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-01-05 11:00:20 +01:00
committed by GitHub
parent 8be0e6ff1a
commit 1e3e8bda1d
31 changed files with 612 additions and 1632 deletions
+5 -110
View File
@@ -4,11 +4,9 @@
![Auto Claude Kanban Board](.github/assets/Auto-Claude-Kanban.png)
<!-- TOP_VERSION_BADGE -->
[![Version](https://img.shields.io/badge/version-2.7.2-blue?style=flat-square)](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.7.2)
<!-- TOP_VERSION_BADGE_END -->
[![License](https://img.shields.io/badge/license-AGPL--3.0-green?style=flat-square)](./agpl-3.0.txt)
[![Discord](https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/KCXaPBr4Dj)
[![YouTube](https://img.shields.io/badge/YouTube-Subscribe-FF0000?style=flat-square&logo=youtube&logoColor=white)](https://www.youtube.com/@AndreMikalsen)
[![CI](https://img.shields.io/github/actions/workflow/status/AndyMik90/Auto-Claude/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/AndyMik90/Auto-Claude/actions)
---
@@ -59,7 +57,6 @@
- **Claude Pro/Max subscription** - [Get one here](https://claude.ai/upgrade)
- **Claude Code CLI** - `npm install -g @anthropic-ai/claude-code`
- **Git repository** - Your project must be initialized as a git repo
- **Python 3.12+** - Required for the backend and Memory Layer
---
@@ -148,113 +145,11 @@ See [guides/CLI-USAGE.md](guides/CLI-USAGE.md) for complete CLI documentation.
---
## Configuration
## Development
Create `apps/backend/.env` from the example:
Want to build from source or contribute? See [CONTRIBUTING.md](CONTRIBUTING.md) for complete development setup instructions.
```bash
cp apps/backend/.env.example apps/backend/.env
```
| Variable | Required | Description |
|----------|----------|-------------|
| `CLAUDE_CODE_OAUTH_TOKEN` | Yes | OAuth token from `claude setup-token` |
| `GRAPHITI_ENABLED` | No | Enable Memory Layer for cross-session context |
| `AUTO_BUILD_MODEL` | No | Override the default Claude model |
| `GITLAB_TOKEN` | No | GitLab Personal Access Token for GitLab integration |
| `GITLAB_INSTANCE_URL` | No | GitLab instance URL (defaults to gitlab.com) |
| `LINEAR_API_KEY` | No | Linear API key for task sync |
---
## Building from Source
For contributors and development:
```bash
# Clone the repository
git clone https://github.com/AndyMik90/Auto-Claude.git
cd Auto-Claude
# Install all dependencies
npm run install:all
# Run in development mode
npm run dev
# Or build and run
npm start
```
**System requirements for building:**
- Node.js 24+
- Python 3.12+
- npm 10+
**Installing dependencies by platform:**
<details>
<summary><b>Windows</b></summary>
```bash
winget install Python.Python.3.12
winget install OpenJS.NodeJS.LTS
```
</details>
<details>
<summary><b>macOS</b></summary>
```bash
brew install python@3.12 node@24
```
</details>
<details>
<summary><b>Linux (Ubuntu/Debian)</b></summary>
```bash
sudo apt install python3.12 python3.12-venv
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
```
</details>
<details>
<summary><b>Linux (Fedora)</b></summary>
```bash
sudo dnf install python3.12 nodejs npm
```
</details>
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed development setup.
### Building Flatpak
To build the Flatpak package, you need additional dependencies:
```bash
# Fedora/RHEL
sudo dnf install flatpak-builder
# Ubuntu/Debian
sudo apt install flatpak-builder
# Install required Flatpak runtimes
flatpak install flathub org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08
flatpak install flathub org.electronjs.Electron2.BaseApp//25.08
# Build the Flatpak
cd apps/frontend
npm run package:flatpak
```
The Flatpak will be created in `apps/frontend/dist/`.
For Linux-specific builds (Flatpak, AppImage), see [guides/linux.md](guides/linux.md).
---
@@ -284,7 +179,7 @@ All releases are:
| `npm run package:mac` | Package for macOS |
| `npm run package:win` | Package for Windows |
| `npm run package:linux` | Package for Linux |
| `npm run package:flatpak` | Package as Flatpak |
| `npm run package:flatpak` | Package as Flatpak (see [guides/linux.md](guides/linux.md)) |
| `npm run lint` | Run linter |
| `npm test` | Run frontend tests |
| `npm run test:backend` | Run backend tests |
+203 -1
View File
@@ -18,12 +18,16 @@
*/
import { autoUpdater } from 'electron-updater';
import { app } from 'electron';
import { app, net } from 'electron';
import type { BrowserWindow } from 'electron';
import { IPC_CHANNELS } from '../shared/constants';
import type { AppUpdateInfo } from '../shared/types';
import { compareVersions } from './updater/version-manager';
// GitHub repo info for API calls
const GITHUB_OWNER = 'AndyMik90';
const GITHUB_REPO = 'Auto-Claude';
// Debug mode - DEBUG_UPDATER=true or development mode
const DEBUG_UPDATER = process.env.DEBUG_UPDATER === 'true' || process.env.NODE_ENV === 'development';
@@ -251,3 +255,201 @@ export function quitAndInstall(): void {
export function getCurrentVersion(): string {
return autoUpdater.currentVersion.version;
}
/**
* Check if a version string represents a prerelease (beta, alpha, rc, etc.)
*/
export function isPrerelease(version: string): boolean {
return /-(alpha|beta|rc|dev|canary)\.\d+$/i.test(version) || version.includes('-');
}
// Timeout for GitHub API requests (10 seconds)
const GITHUB_API_TIMEOUT = 10000;
/**
* Fetch the latest stable release from GitHub API
* Returns the latest non-prerelease version
*/
async function fetchLatestStableRelease(): Promise<AppUpdateInfo | null> {
const fetchPromise = new Promise<AppUpdateInfo | null>((resolve) => {
const url = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases`;
console.warn('[app-updater] Fetching releases from:', url);
const request = net.request({
url,
method: 'GET'
});
request.setHeader('Accept', 'application/vnd.github.v3+json');
request.setHeader('User-Agent', `Auto-Claude/${getCurrentVersion()}`);
let data = '';
request.on('response', (response) => {
// Validate HTTP status code
const statusCode = response.statusCode;
if (statusCode !== 200) {
console.error(`[app-updater] GitHub API error: HTTP ${statusCode}`);
if (statusCode === 403) {
console.error('[app-updater] Rate limit may have been exceeded');
} else if (statusCode === 404) {
console.error('[app-updater] Repository or releases not found');
}
resolve(null);
return;
}
response.on('data', (chunk) => {
data += chunk.toString();
});
response.on('end', () => {
try {
const parsed = JSON.parse(data);
// Validate response is an array
if (!Array.isArray(parsed)) {
console.error('[app-updater] Unexpected response format - expected array, got:', typeof parsed);
resolve(null);
return;
}
const releases = parsed as Array<{
tag_name: string;
prerelease: boolean;
draft: boolean;
body?: string;
published_at?: string;
html_url?: string;
}>;
// Find the first non-prerelease, non-draft release
const latestStable = releases.find(r => !r.prerelease && !r.draft);
if (!latestStable) {
console.warn('[app-updater] No stable release found');
resolve(null);
return;
}
const version = latestStable.tag_name.replace(/^v/, '');
console.warn('[app-updater] Found latest stable release:', version);
resolve({
version,
releaseNotes: latestStable.body,
releaseDate: latestStable.published_at
});
} catch (e) {
console.error('[app-updater] Failed to parse releases JSON:', e);
resolve(null);
}
});
});
request.on('error', (error) => {
console.error('[app-updater] Failed to fetch releases:', error);
resolve(null);
});
request.end();
});
// Add timeout to prevent hanging indefinitely
const timeoutPromise = new Promise<AppUpdateInfo | null>((resolve) => {
setTimeout(() => {
console.error(`[app-updater] GitHub API request timed out after ${GITHUB_API_TIMEOUT}ms`);
resolve(null);
}, GITHUB_API_TIMEOUT);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
/**
* Check if we should offer a downgrade to stable
* Called when user disables beta updates while on a prerelease version
*
* Returns the latest stable version if:
* 1. Current version is a prerelease
* 2. A stable version exists
*/
export async function checkForStableDowngrade(): Promise<AppUpdateInfo | null> {
const currentVersion = getCurrentVersion();
// Only check for downgrade if currently on a prerelease
if (!isPrerelease(currentVersion)) {
console.warn('[app-updater] Current version is not a prerelease, no downgrade needed');
return null;
}
console.warn('[app-updater] Current version is prerelease:', currentVersion);
console.warn('[app-updater] Checking for stable version to downgrade to...');
const latestStable = await fetchLatestStableRelease();
if (!latestStable) {
console.warn('[app-updater] No stable release available for downgrade');
return null;
}
console.warn('[app-updater] Stable downgrade available:', latestStable.version);
return latestStable;
}
/**
* Set update channel with optional downgrade check
* When switching from beta to stable, checks if user should be offered a downgrade
*
* @param channel - The update channel to switch to
* @param triggerDowngradeCheck - Whether to check for stable downgrade (when disabling beta)
*/
export async function setUpdateChannelWithDowngradeCheck(
channel: UpdateChannel,
triggerDowngradeCheck = false
): Promise<AppUpdateInfo | null> {
autoUpdater.channel = channel;
console.warn(`[app-updater] Update channel set to: ${channel}`);
// If switching to stable and downgrade check requested, look for stable version
if (channel === 'latest' && triggerDowngradeCheck) {
const stableVersion = await checkForStableDowngrade();
if (stableVersion && mainWindow) {
// Notify the renderer about the available stable downgrade
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_STABLE_DOWNGRADE, stableVersion);
}
return stableVersion;
}
return null;
}
/**
* Download a specific version (for downgrade)
* Uses electron-updater with allowDowngrade enabled to download older stable versions
*/
export async function downloadStableVersion(): Promise<void> {
// Switch to stable channel
autoUpdater.channel = 'latest';
// Enable downgrade to allow downloading older versions (e.g., stable when on beta)
autoUpdater.allowDowngrade = true;
console.warn('[app-updater] Downloading stable version (allowDowngrade=true)...');
try {
// Force a fresh check on the stable channel, then download
const result = await autoUpdater.checkForUpdates();
if (result) {
await autoUpdater.downloadUpdate();
} else {
throw new Error('No stable version available for download');
}
} catch (error) {
console.error('[app-updater] Failed to download stable version:', error);
throw error;
} finally {
// Reset allowDowngrade to prevent unintended downgrades in normal update checks
autoUpdater.allowDowngrade = false;
}
}
@@ -1,48 +0,0 @@
/**
* Auto Claude Source Updater
*
* Checks GitHub Releases for updates and downloads them.
* GitHub Releases are the single source of truth for versioning.
*
* Update flow:
* 1. Check GitHub Releases API for the latest release
* 2. Compare release tag with current app version
* 3. If update available, download release tarball and apply
* 4. Existing project update system handles pushing to individual projects
*
* Versioning:
* - Single source of truth: GitHub Releases
* - Current version: app.getVersion() (from package.json at build time)
* - Latest version: Fetched from GitHub Releases API
* - To release: Create a GitHub release with tag (e.g., v1.2.0)
*/
// Export types
export type {
GitHubRelease,
AutoBuildUpdateCheck,
AutoBuildUpdateResult,
UpdateProgressCallback,
UpdateMetadata
} from './updater/types';
// Export version management
export { getBundledVersion, getEffectiveVersion } from './updater/version-manager';
// Export path resolution
export {
getBundledSourcePath,
getEffectiveSourcePath
} from './updater/path-resolver';
// Export update checking
export { checkForUpdates } from './updater/update-checker';
// Export update installation
export { downloadAndApplyUpdate } from './updater/update-installer';
// Export update status
export {
hasPendingSourceUpdate,
getUpdateMetadata
} from './updater/update-status';
+31 -1
View File
@@ -1,6 +1,6 @@
import { app, BrowserWindow, shell, nativeImage, session } from 'electron';
import { join } from 'path';
import { accessSync, readFileSync, writeFileSync } from 'fs';
import { accessSync, readFileSync, writeFileSync, rmSync, existsSync } from 'fs';
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
import { setupIpcHandlers } from './ipc-setup';
import { AgentManager } from './agent';
@@ -30,6 +30,32 @@ function loadSettingsSync(): AppSettings {
return { ...DEFAULT_APP_SETTINGS, ...savedSettings } as AppSettings;
}
/**
* Clean up stale update metadata files from the redundant source updater system.
*
* The old "source updater" wrote .update-metadata.json files that could persist
* across app updates and cause version display desync. This cleanup ensures
* we use the actual bundled version from app.getVersion().
*/
function cleanupStaleUpdateMetadata(): void {
const userData = app.getPath('userData');
const stalePaths = [
join(userData, 'auto-claude-source'),
join(userData, 'backend-source'),
];
for (const stalePath of stalePaths) {
if (existsSync(stalePath)) {
try {
rmSync(stalePath, { recursive: true, force: true });
console.warn(`[main] Cleaned up stale update metadata: ${stalePath}`);
} catch (e) {
console.warn(`[main] Failed to clean up stale metadata at ${stalePath}:`, e);
}
}
}
}
// Get icon path based on platform
function getIconPath(): string {
// In dev mode, __dirname is out/main, so we go up to project root then into resources
@@ -133,6 +159,10 @@ app.whenReady().then(() => {
.catch((err) => console.warn('[main] Failed to clear cache:', err));
}
// Clean up stale update metadata from the old source updater system
// This prevents version display desync after electron-updater installs a new version
cleanupStaleUpdateMetadata();
// Set dock icon on macOS
if (process.platform === 'darwin') {
const iconPath = getIconPath();
@@ -11,6 +11,7 @@ import type { IPCResult, AppUpdateInfo } from '../../shared/types';
import {
checkForUpdates,
downloadUpdate,
downloadStableVersion,
quitAndInstall,
getCurrentVersion
} from '../app-updater';
@@ -65,6 +66,26 @@ export function registerAppUpdateHandlers(): void {
}
);
/**
* APP_UPDATE_DOWNLOAD_STABLE: Download stable version (for downgrade from beta)
* Uses allowDowngrade to download an older stable version
*/
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_DOWNLOAD_STABLE,
async (): Promise<IPCResult> => {
try {
await downloadStableVersion();
return { success: true };
} catch (error) {
console.error('[app-update-handlers] Download stable version failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to download stable version'
};
}
}
);
/**
* APP_UPDATE_INSTALL: Quit and install update
* Quits the app and installs the downloaded update
@@ -1,321 +0,0 @@
import { ipcMain } from 'electron';
import type { BrowserWindow } from 'electron';
import { IPC_CHANNELS } from '../../shared/constants';
import type { IPCResult } from '../../shared/types';
import path from 'path';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import type { AutoBuildSourceUpdateProgress, SourceEnvConfig, SourceEnvCheckResult } from '../../shared/types';
import { checkForUpdates as checkSourceUpdates, downloadAndApplyUpdate, getBundledVersion, getEffectiveVersion, getEffectiveSourcePath } from '../auto-claude-updater';
import { debugLog } from '../../shared/utils/debug-logger';
/**
* Register all autobuild-source-related IPC handlers
*/
export function registerAutobuildSourceHandlers(
getMainWindow: () => BrowserWindow | null
): void {
// ============================================
// Auto Claude Source Update Operations
// ============================================
ipcMain.handle(
IPC_CHANNELS.AUTOBUILD_SOURCE_CHECK,
async (): Promise<IPCResult<{ updateAvailable: boolean; currentVersion: string; latestVersion?: string; releaseNotes?: string; releaseUrl?: string; error?: string }>> => {
console.log('[autobuild-source] Check for updates called');
debugLog('[IPC] AUTOBUILD_SOURCE_CHECK called');
try {
const result = await checkSourceUpdates();
console.log('[autobuild-source] Check result:', JSON.stringify(result, null, 2));
debugLog('[IPC] AUTOBUILD_SOURCE_CHECK result:', result);
return { success: true, data: result };
} catch (error) {
console.error('[autobuild-source] Check error:', error);
debugLog('[IPC] AUTOBUILD_SOURCE_CHECK error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to check for updates'
};
}
}
);
ipcMain.on(
IPC_CHANNELS.AUTOBUILD_SOURCE_DOWNLOAD,
() => {
debugLog('[IPC] Autobuild source download requested');
const mainWindow = getMainWindow();
if (!mainWindow) {
debugLog('[IPC] No main window available, aborting update');
return;
}
// Start download in background
downloadAndApplyUpdate((progress) => {
debugLog('[IPC] Update progress:', progress.stage, progress.message);
mainWindow.webContents.send(
IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS,
progress
);
}).then((result) => {
if (result.success) {
debugLog('[IPC] Update completed successfully, version:', result.version);
mainWindow.webContents.send(
IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS,
{
stage: 'complete',
message: `Updated to version ${result.version}`,
newVersion: result.version // Include new version for UI refresh
} as AutoBuildSourceUpdateProgress
);
} else {
debugLog('[IPC] Update failed:', result.error);
mainWindow.webContents.send(
IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS,
{
stage: 'error',
message: result.error || 'Update failed'
} as AutoBuildSourceUpdateProgress
);
}
}).catch((error) => {
debugLog('[IPC] Update error:', error instanceof Error ? error.message : error);
mainWindow.webContents.send(
IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS,
{
stage: 'error',
message: error instanceof Error ? error.message : 'Update failed'
} as AutoBuildSourceUpdateProgress
);
});
// Send initial progress
mainWindow.webContents.send(
IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS,
{
stage: 'checking',
message: 'Starting update...'
} as AutoBuildSourceUpdateProgress
);
}
);
ipcMain.handle(
IPC_CHANNELS.AUTOBUILD_SOURCE_VERSION,
async (): Promise<IPCResult<string>> => {
try {
// Use effective version which accounts for source updates
const version = getEffectiveVersion();
debugLog('[IPC] Returning effective version:', version);
return { success: true, data: version };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get version'
};
}
}
);
// ============================================
// Auto Claude Source Environment Operations
// ============================================
/**
* Parse an .env file content into a key-value object
*/
const parseSourceEnvFile = (content: string): Record<string, string> => {
const vars: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
let value = trimmed.substring(eqIndex + 1).trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
vars[key] = value;
}
}
return vars;
};
ipcMain.handle(
IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_GET,
async (): Promise<IPCResult<SourceEnvConfig>> => {
try {
const sourcePath = getEffectiveSourcePath();
if (!sourcePath) {
return {
success: true,
data: {
hasClaudeToken: false,
envExists: false,
sourcePath: undefined
}
};
}
const envPath = path.join(sourcePath, '.env');
const envExists = existsSync(envPath);
if (!envExists) {
return {
success: true,
data: {
hasClaudeToken: false,
envExists: false,
sourcePath
}
};
}
const content = readFileSync(envPath, 'utf-8');
const vars = parseSourceEnvFile(content);
const hasToken = !!vars['CLAUDE_CODE_OAUTH_TOKEN'];
return {
success: true,
data: {
hasClaudeToken: hasToken,
claudeOAuthToken: hasToken ? vars['CLAUDE_CODE_OAUTH_TOKEN'] : undefined,
envExists: true,
sourcePath
}
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get source env'
};
}
}
);
ipcMain.handle(
IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_UPDATE,
async (_, config: { claudeOAuthToken?: string }): Promise<IPCResult> => {
try {
const sourcePath = getEffectiveSourcePath();
if (!sourcePath) {
return {
success: false,
error: 'Auto-Claude source path not found. Please configure it in App Settings.'
};
}
const envPath = path.join(sourcePath, '.env');
// Read existing content or start fresh
let existingContent = '';
const existingVars: Record<string, string> = {};
if (existsSync(envPath)) {
existingContent = readFileSync(envPath, 'utf-8');
Object.assign(existingVars, parseSourceEnvFile(existingContent));
}
// Update the token
if (config.claudeOAuthToken !== undefined) {
existingVars['CLAUDE_CODE_OAUTH_TOKEN'] = config.claudeOAuthToken;
}
// Rebuild the .env file preserving comments and structure
const lines = existingContent.split('\n');
const processedKeys = new Set<string>();
const outputLines: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
outputLines.push(line);
continue;
}
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
if (key in existingVars) {
outputLines.push(`${key}=${existingVars[key]}`);
processedKeys.add(key);
} else {
outputLines.push(line);
}
} else {
outputLines.push(line);
}
}
// Add any new keys that weren't in the original file
for (const [key, value] of Object.entries(existingVars)) {
if (!processedKeys.has(key)) {
outputLines.push(`${key}=${value}`);
}
}
writeFileSync(envPath, outputLines.join('\n'));
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update source env'
};
}
}
);
ipcMain.handle(
IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_CHECK_TOKEN,
async (): Promise<IPCResult<SourceEnvCheckResult>> => {
try {
const sourcePath = getEffectiveSourcePath();
if (!sourcePath) {
return {
success: true,
data: {
hasToken: false,
sourcePath: undefined,
error: 'Auto-Claude source path not found'
}
};
}
const envPath = path.join(sourcePath, '.env');
if (!existsSync(envPath)) {
return {
success: true,
data: {
hasToken: false,
sourcePath,
error: '.env file does not exist'
}
};
}
const content = readFileSync(envPath, 'utf-8');
const vars = parseSourceEnvFile(content);
const hasToken = !!vars['CLAUDE_CODE_OAUTH_TOKEN'] && vars['CLAUDE_CODE_OAUTH_TOKEN'].length > 0;
return {
success: true,
data: {
hasToken,
sourcePath
}
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to check source token'
};
}
}
);
}
@@ -23,7 +23,6 @@ import { registerEnvHandlers } from './env-handlers';
import { registerLinearHandlers } from './linear-handlers';
import { registerGithubHandlers } from './github-handlers';
import { registerGitlabHandlers } from './gitlab-handlers';
import { registerAutobuildSourceHandlers } from './autobuild-source-handlers';
import { registerIdeationHandlers } from './ideation-handlers';
import { registerChangelogHandlers } from './changelog-handlers';
import { registerInsightsHandlers } from './insights-handlers';
@@ -92,9 +91,6 @@ export function setupIpcHandlers(
// GitLab integration handlers
registerGitlabHandlers(agentManager, getMainWindow);
// Auto-build source update handlers
registerAutobuildSourceHandlers(getMainWindow);
// Ideation handlers
registerIdeationHandlers(agentManager, getMainWindow);
@@ -140,7 +136,6 @@ export {
registerLinearHandlers,
registerGithubHandlers,
registerGitlabHandlers,
registerAutobuildSourceHandlers,
registerIdeationHandlers,
registerChangelogHandlers,
registerInsightsHandlers,
@@ -10,8 +10,7 @@ import type {
} from '../../shared/types';
import { AgentManager } from '../agent';
import type { BrowserWindow } from 'electron';
import { getEffectiveVersion } from '../auto-claude-updater';
import { setUpdateChannel } from '../app-updater';
import { setUpdateChannel, setUpdateChannelWithDowngradeCheck } from '../app-updater';
import { getSettingsPath, readSettingsFile } from '../settings-utils';
import { configureTools, getToolPath, getToolInfo, isPathFromWrongPlatform } from '../cli-tool-manager';
@@ -211,8 +210,16 @@ export function registerSettingsHandlers(
// Update auto-updater channel if betaUpdates setting changed
if (settings.betaUpdates !== undefined) {
const channel = settings.betaUpdates ? 'beta' : 'latest';
setUpdateChannel(channel);
if (settings.betaUpdates) {
// Enabling beta updates - just switch channel
setUpdateChannel('beta');
} else {
// Disabling beta updates - switch to stable and check if downgrade is available
// This will notify the renderer if user is on a prerelease and stable version exists
setUpdateChannelWithDowngradeCheck('latest', true).catch((err) => {
console.error('[settings-handlers] Failed to check for stable downgrade:', err);
});
}
}
return { success: true };
@@ -372,8 +379,8 @@ export function registerSettingsHandlers(
// ============================================
ipcMain.handle(IPC_CHANNELS.APP_VERSION, async (): Promise<string> => {
// Use effective version which accounts for source updates
const version = getEffectiveVersion();
// Return the actual bundled version from package.json
const version = app.getVersion();
console.log('[settings-handlers] APP_VERSION returning:', version);
return version;
});
@@ -6,7 +6,7 @@ import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
import { execSync, execFileSync, spawn, spawnSync, exec, execFile } from 'child_process';
import { projectStore } from '../../project-store';
import { getConfiguredPythonPath, PythonEnvManager, pythonEnvManager as pythonEnvManagerSingleton } from '../../python-env-manager';
import { getEffectiveSourcePath } from '../../auto-claude-updater';
import { getEffectiveSourcePath } from '../../updater/path-resolver';
import { getProfileEnv } from '../../rate-limit-detector';
import { findTaskAndProject } from './shared';
import { parsePythonCommand } from '../../python-detector';
-30
View File
@@ -1,30 +0,0 @@
/**
* Configuration for Auto Claude updater
*/
/**
* GitHub repository configuration
*/
export const GITHUB_CONFIG = {
owner: 'AndyMik90',
repo: 'Auto-Claude',
autoBuildPath: 'apps/backend' // Path within repo where auto-claude backend lives
} as const;
/**
* Files and directories to preserve during updates
*/
export const PRESERVE_FILES = ['.env', 'specs'] as const;
/**
* Files and directories to skip when copying
*/
export const SKIP_FILES = ['__pycache__', '.DS_Store', '.git', 'specs', '.env'] as const;
/**
* Update-related timeouts (in milliseconds)
*/
export const TIMEOUTS = {
requestTimeout: 10000,
downloadTimeout: 60000
} as const;
@@ -1,135 +0,0 @@
/**
* File operation utilities for updates
*/
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'fs';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { SKIP_FILES } from './config';
const execAsync = promisify(exec);
/**
* Extract a .tar.gz file
* Uses system tar command on Unix or PowerShell on Windows
*/
export async function extractTarball(tarballPath: string, destPath: string): Promise<void> {
try {
if (process.platform === 'win32') {
// On Windows, try multiple approaches:
// 1. Modern Windows 10/11 has built-in tar
// 2. Fall back to PowerShell's Expand-Archive for .zip (but .tar.gz needs tar)
// 3. Use PowerShell to extract via .NET
try {
// First try native tar (available on Windows 10 1803+)
await execAsync(`tar -xzf "${tarballPath}" -C "${destPath}"`);
} catch {
// Fall back to PowerShell with .NET for gzip decompression
// This is more complex but works on older Windows versions
const psScript = `
$tarball = "${tarballPath.replace(/\\/g, '\\\\')}"
$dest = "${destPath.replace(/\\/g, '\\\\')}"
$tempTar = Join-Path $env:TEMP "auto-claude-update.tar"
# Decompress gzip
$gzipStream = [System.IO.File]::OpenRead($tarball)
$decompressedStream = New-Object System.IO.Compression.GZipStream($gzipStream, [System.IO.Compression.CompressionMode]::Decompress)
$tarStream = [System.IO.File]::Create($tempTar)
$decompressedStream.CopyTo($tarStream)
$tarStream.Close()
$decompressedStream.Close()
$gzipStream.Close()
# Extract tar using tar command (should work even if gzip didn't)
tar -xf $tempTar -C $dest
Remove-Item $tempTar -Force
`;
await execAsync(`powershell -NoProfile -Command "${psScript.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`);
}
} else {
// Unix systems - use native tar
await execAsync(`tar -xzf "${tarballPath}" -C "${destPath}"`);
}
} catch (error) {
throw new Error(`Failed to extract tarball: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Recursively copy directory
*/
export function copyDirectoryRecursive(
src: string,
dest: string,
preserveExisting: boolean = false
): void {
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const entries = readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
// Skip certain files/directories
if (SKIP_FILES.includes(entry.name as (typeof SKIP_FILES)[number])) {
continue;
}
// In preserve mode, skip existing files
if (preserveExisting && existsSync(destPath)) {
if (entry.isDirectory()) {
copyDirectoryRecursive(srcPath, destPath, preserveExisting);
}
continue;
}
if (entry.isDirectory()) {
copyDirectoryRecursive(srcPath, destPath, preserveExisting);
} else {
copyFileSync(srcPath, destPath);
}
}
}
/**
* Preserve specified files before update
*/
export function preserveFiles(targetPath: string, filesToPreserve: readonly string[]): Record<string, Buffer> {
const preservedContent: Record<string, Buffer> = {};
for (const file of filesToPreserve) {
const filePath = path.join(targetPath, file);
if (existsSync(filePath)) {
if (!statSync(filePath).isDirectory()) {
preservedContent[file] = readFileSync(filePath);
}
}
}
return preservedContent;
}
/**
* Restore preserved files after update
*/
export function restoreFiles(targetPath: string, preservedContent: Record<string, Buffer>): void {
for (const [file, content] of Object.entries(preservedContent)) {
writeFileSync(path.join(targetPath, file), content);
}
}
/**
* Clean target directory while preserving specified files
*/
export function cleanTargetDirectory(targetPath: string, preserveFiles: readonly string[]): void {
const items = readdirSync(targetPath);
for (const item of items) {
if (!preserveFiles.includes(item)) {
rmSync(path.join(targetPath, item), { recursive: true, force: true });
}
}
}
@@ -1,189 +0,0 @@
/**
* HTTP client utilities for fetching updates
*/
import https from 'https';
import { createWriteStream } from 'fs';
import { TIMEOUTS, GITHUB_CONFIG } from './config';
/**
* Fetch JSON from a URL using https
*/
export function fetchJson<T>(url: string): Promise<T> {
return new Promise((resolve, reject) => {
const headers = {
'User-Agent': 'Auto-Claude-UI',
'Accept': 'application/vnd.github+json'
};
const request = https.get(url, { headers }, (response) => {
// Handle redirects
if (response.statusCode === 301 || response.statusCode === 302) {
const redirectUrl = response.headers.location;
if (redirectUrl) {
fetchJson<T>(redirectUrl).then(resolve).catch(reject);
return;
}
}
// Handle HTTP 300 Multiple Choices (branch/tag name collision)
if (response.statusCode === 300) {
let data = '';
response.on('data', chunk => data += chunk);
response.on('end', () => {
console.error('[HTTP] Multiple choices for resource:', {
url,
statusCode: 300,
response: data
});
reject(new Error(
`Multiple resources found for ${url}. ` +
`This usually means a branch and tag have the same name. ` +
`Please report this issue at https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/issues`
));
});
response.on('error', reject);
return;
}
if (response.statusCode !== 200) {
// Collect response body for error details (limit to 10KB)
const maxErrorSize = 10 * 1024;
let errorData = '';
response.on('data', chunk => {
if (errorData.length < maxErrorSize) {
errorData += chunk.toString().slice(0, maxErrorSize - errorData.length);
}
});
response.on('end', () => {
const errorMsg = `HTTP ${response.statusCode}: ${errorData || response.statusMessage || 'No error details'}`;
reject(new Error(errorMsg));
});
response.on('error', reject);
return;
}
let data = '';
response.on('data', chunk => data += chunk);
response.on('end', () => {
try {
resolve(JSON.parse(data) as T);
} catch (_e) {
reject(new Error('Failed to parse JSON response'));
}
});
response.on('error', reject);
});
request.on('error', reject);
request.setTimeout(TIMEOUTS.requestTimeout, () => {
request.destroy();
reject(new Error('Request timeout'));
});
});
}
/**
* Download a file with progress tracking
*/
export function downloadFile(
url: string,
destPath: string,
onProgress?: (percent: number) => void
): Promise<void> {
return new Promise((resolve, reject) => {
const file = createWriteStream(destPath);
// GitHub API URLs need the GitHub Accept header to get a redirect to the actual file
// Non-API URLs (CDN, direct downloads) use octet-stream
const isGitHubApi = url.includes('api.github.com');
const headers = {
'User-Agent': 'Auto-Claude-UI',
'Accept': isGitHubApi ? 'application/vnd.github+json' : 'application/octet-stream'
};
const request = https.get(url, { headers }, (response) => {
// Handle redirects
if (response.statusCode === 301 || response.statusCode === 302) {
file.close();
const redirectUrl = response.headers.location;
if (redirectUrl) {
downloadFile(redirectUrl, destPath, onProgress).then(resolve).catch(reject);
return;
}
}
// Handle HTTP 300 Multiple Choices (branch/tag name collision)
if (response.statusCode === 300) {
file.close();
let data = '';
response.on('data', chunk => data += chunk);
response.on('end', () => {
console.error('[HTTP] Multiple choices for resource:', {
url,
statusCode: 300,
response: data
});
reject(new Error(
`Multiple resources found for ${url}. ` +
`This usually means a branch and tag have the same name. ` +
`Please download the latest version manually from: ` +
`https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`
));
});
response.on('error', reject);
return;
}
if (response.statusCode !== 200) {
file.close();
// Collect response body for error details (limit to 10KB)
const maxErrorSize = 10 * 1024;
let errorData = '';
response.on('data', chunk => {
if (errorData.length < maxErrorSize) {
errorData += chunk.toString().slice(0, maxErrorSize - errorData.length);
}
});
response.on('end', () => {
const errorMsg = `HTTP ${response.statusCode}: ${errorData || response.statusMessage || 'No error details'}`;
reject(new Error(errorMsg));
});
response.on('error', reject);
return;
}
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
let downloadedSize = 0;
response.on('data', (chunk) => {
downloadedSize += chunk.length;
if (totalSize > 0 && onProgress) {
onProgress(Math.round((downloadedSize / totalSize) * 100));
}
});
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
file.on('error', (err) => {
file.close();
reject(err);
});
});
request.on('error', (err) => {
file.close();
reject(err);
});
request.setTimeout(TIMEOUTS.downloadTimeout, () => {
request.destroy();
reject(new Error('Download timeout'));
});
});
}
-63
View File
@@ -1,63 +0,0 @@
/**
* Type definitions for Auto Claude updater system
*/
/**
* GitHub Release API response (partial)
*/
export interface GitHubRelease {
tag_name: string;
name: string;
body: string;
html_url: string;
tarball_url: string;
published_at: string;
prerelease: boolean;
draft: boolean;
}
/**
* Result of checking for updates
*/
export interface AutoBuildUpdateCheck {
updateAvailable: boolean;
currentVersion: string;
latestVersion?: string;
releaseNotes?: string;
releaseUrl?: string;
error?: string;
}
/**
* Result of applying an update
*/
export interface AutoBuildUpdateResult {
success: boolean;
version?: string;
error?: string;
}
/**
* Update progress stages
*/
export type UpdateStage = 'checking' | 'downloading' | 'extracting' | 'complete' | 'error';
/**
* Progress callback for download
*/
export type UpdateProgressCallback = (progress: {
stage: UpdateStage;
percent?: number;
message: string;
}) => void;
/**
* Update metadata stored after successful update
*/
export interface UpdateMetadata {
version: string;
updatedAt: string;
source: string;
releaseTag: string;
releaseName: string;
}
@@ -1,77 +0,0 @@
/**
* Update checking functionality
*/
import { GITHUB_CONFIG } from './config';
import { fetchJson } from './http-client';
import { getEffectiveVersion, parseVersionFromTag, compareVersions } from './version-manager';
import { GitHubRelease, AutoBuildUpdateCheck } from './types';
import { debugLog } from '../../shared/utils/debug-logger';
// Cache for the latest release info (used by download)
let cachedLatestRelease: GitHubRelease | null = null;
/**
* Get cached release (if available)
*/
export function getCachedRelease(): GitHubRelease | null {
return cachedLatestRelease;
}
/**
* Set cached release
*/
export function setCachedRelease(release: GitHubRelease | null): void {
cachedLatestRelease = release;
}
/**
* Clear cached release
*/
export function clearCachedRelease(): void {
cachedLatestRelease = null;
}
/**
* Check GitHub Releases for the latest version
*/
export async function checkForUpdates(): Promise<AutoBuildUpdateCheck> {
// Use effective version which accounts for source updates
const currentVersion = getEffectiveVersion();
debugLog('[UpdateCheck] Current effective version:', currentVersion);
try {
// Fetch latest release from GitHub Releases API
const releaseUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`;
const release = await fetchJson<GitHubRelease>(releaseUrl);
// Cache for download function
setCachedRelease(release);
// Parse version from tag (e.g., "v1.2.0" -> "1.2.0")
const latestVersion = parseVersionFromTag(release.tag_name);
debugLog('[UpdateCheck] Latest version:', latestVersion);
// Compare versions
const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
debugLog('[UpdateCheck] Update available:', updateAvailable);
return {
updateAvailable,
currentVersion,
latestVersion,
releaseNotes: release.body || undefined,
releaseUrl: release.html_url || undefined
};
} catch (error) {
// Clear cache on error
clearCachedRelease();
debugLog('[UpdateCheck] Error:', error instanceof Error ? error.message : error);
return {
updateAvailable: false,
currentVersion,
error: error instanceof Error ? error.message : 'Failed to check for updates'
};
}
}
@@ -1,224 +0,0 @@
/**
* Update installation and application
*/
import { existsSync, mkdirSync, writeFileSync, rmSync, readdirSync } from 'fs';
import path from 'path';
import { app } from 'electron';
import { GITHUB_CONFIG, PRESERVE_FILES } from './config';
import { downloadFile, fetchJson } from './http-client';
import { parseVersionFromTag } from './version-manager';
import { getUpdateCachePath, getUpdateTargetPath } from './path-resolver';
import { extractTarball, copyDirectoryRecursive, preserveFiles, restoreFiles, cleanTargetDirectory } from './file-operations';
import { getCachedRelease, setCachedRelease, clearCachedRelease } from './update-checker';
import { GitHubRelease, AutoBuildUpdateResult, UpdateProgressCallback, UpdateMetadata } from './types';
import { debugLog } from '../../shared/utils/debug-logger';
/**
* Download and apply the latest auto-claude update from GitHub Releases
*
* Note: In production, this updates the bundled source in userData.
* For packaged apps, we can't modify resourcesPath directly,
* so we use a "source override" system.
*/
export async function downloadAndApplyUpdate(
onProgress?: UpdateProgressCallback
): Promise<AutoBuildUpdateResult> {
const cachePath = getUpdateCachePath();
debugLog('[Update] Starting update process...');
debugLog('[Update] Cache path:', cachePath);
try {
onProgress?.({
stage: 'checking',
message: 'Fetching release info...'
});
// Ensure cache directory exists
if (!existsSync(cachePath)) {
mkdirSync(cachePath, { recursive: true });
debugLog('[Update] Created cache directory');
}
// Get release info (use cache or fetch fresh)
let release = getCachedRelease();
if (!release) {
const releaseUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`;
debugLog('[Update] Fetching release info from:', releaseUrl);
release = await fetchJson<GitHubRelease>(releaseUrl);
setCachedRelease(release);
} else {
debugLog('[Update] Using cached release info');
}
// Use explicit tag reference URL to avoid HTTP 300 when branch/tag names collide
// See: https://github.com/AndyMik90/Auto-Claude/issues/78
const tarballUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/tarball/refs/tags/${release.tag_name}`;
const releaseVersion = parseVersionFromTag(release.tag_name);
debugLog('[Update] Release version:', releaseVersion);
debugLog('[Update] Tarball URL:', tarballUrl);
const tarballPath = path.join(cachePath, 'auto-claude-update.tar.gz');
const extractPath = path.join(cachePath, 'extracted');
// Clean up previous extraction
if (existsSync(extractPath)) {
rmSync(extractPath, { recursive: true, force: true });
}
mkdirSync(extractPath, { recursive: true });
onProgress?.({
stage: 'downloading',
percent: 0,
message: 'Downloading update...'
});
debugLog('[Update] Starting download to:', tarballPath);
// Download the tarball
await downloadFile(tarballUrl, tarballPath, (percent) => {
onProgress?.({
stage: 'downloading',
percent,
message: `Downloading... ${percent}%`
});
});
debugLog('[Update] Download complete');
onProgress?.({
stage: 'extracting',
message: 'Extracting update...'
});
debugLog('[Update] Extracting to:', extractPath);
// Extract the tarball
await extractTarball(tarballPath, extractPath);
debugLog('[Update] Extraction complete');
// Find the auto-claude folder in extracted content
// GitHub tarballs have a root folder like "owner-repo-hash/"
const extractedDirs = readdirSync(extractPath);
if (extractedDirs.length === 0) {
throw new Error('Empty tarball');
}
const rootDir = path.join(extractPath, extractedDirs[0]);
const autoBuildSource = path.join(rootDir, GITHUB_CONFIG.autoBuildPath);
if (!existsSync(autoBuildSource)) {
throw new Error('auto-claude folder not found in download');
}
// Determine where to install the update
const targetPath = getUpdateTargetPath();
debugLog('[Update] Target install path:', targetPath);
// Backup existing source (if in dev mode)
const backupPath = path.join(cachePath, 'backup');
if (!app.isPackaged && existsSync(targetPath)) {
if (existsSync(backupPath)) {
rmSync(backupPath, { recursive: true, force: true });
}
// Simple copy for backup
debugLog('[Update] Creating backup at:', backupPath);
copyDirectoryRecursive(targetPath, backupPath);
}
// Apply the update
debugLog('[Update] Applying update...');
await applyUpdate(targetPath, autoBuildSource);
debugLog('[Update] Update applied successfully');
// Write update metadata
const metadata: UpdateMetadata = {
version: releaseVersion,
updatedAt: new Date().toISOString(),
source: 'github-release',
releaseTag: release.tag_name,
releaseName: release.name
};
writeUpdateMetadata(targetPath, metadata);
// Clear the cache after successful update
clearCachedRelease();
// Cleanup
rmSync(tarballPath, { force: true });
rmSync(extractPath, { recursive: true, force: true });
onProgress?.({
stage: 'complete',
message: `Updated to version ${releaseVersion}`
});
debugLog('[Update] ============================================');
debugLog('[Update] UPDATE SUCCESSFUL');
debugLog('[Update] New version:', releaseVersion);
debugLog('[Update] Target path:', targetPath);
debugLog('[Update] ============================================');
return {
success: true,
version: releaseVersion
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Update failed';
debugLog('[Update] ============================================');
debugLog('[Update] UPDATE FAILED');
debugLog('[Update] Error:', errorMessage);
debugLog('[Update] ============================================');
// Provide user-friendly error message for HTTP 300 errors
let displayMessage = errorMessage;
if (errorMessage.includes('Multiple resources found')) {
displayMessage =
`Update failed due to repository configuration issue (HTTP 300). ` +
`Please download the latest version manually from: ` +
`https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`;
}
onProgress?.({
stage: 'error',
message: displayMessage
});
return {
success: false,
error: displayMessage
};
}
}
/**
* Apply update to target directory
*/
async function applyUpdate(targetPath: string, sourcePath: string): Promise<void> {
if (existsSync(targetPath)) {
// Preserve important files
const preservedContent = preserveFiles(targetPath, PRESERVE_FILES);
// Clean target but preserve certain files
cleanTargetDirectory(targetPath, PRESERVE_FILES);
// Copy new files
copyDirectoryRecursive(sourcePath, targetPath, true);
// Restore preserved files that might have been overwritten
restoreFiles(targetPath, preservedContent);
} else {
mkdirSync(targetPath, { recursive: true });
copyDirectoryRecursive(sourcePath, targetPath, false);
}
}
/**
* Write update metadata to disk
*/
function writeUpdateMetadata(targetPath: string, metadata: UpdateMetadata): void {
const metadataPath = path.join(targetPath, '.update-metadata.json');
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
}
@@ -1,51 +0,0 @@
/**
* Update status checking utilities
*/
import { existsSync, readFileSync } from 'fs';
import path from 'path';
import { app } from 'electron';
import { getBundledVersion, compareVersions } from './version-manager';
import { UpdateMetadata } from './types';
/**
* Check if there's a pending source update that requires restart
*/
export function hasPendingSourceUpdate(): boolean {
if (!app.isPackaged) {
return false;
}
const overridePath = path.join(app.getPath('userData'), 'auto-claude-source');
const metadataPath = path.join(overridePath, '.update-metadata.json');
if (!existsSync(metadataPath)) {
return false;
}
try {
const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8')) as UpdateMetadata;
const bundledVersion = getBundledVersion();
return compareVersions(metadata.version, bundledVersion) > 0;
} catch {
return false;
}
}
/**
* Get update metadata if available
*/
export function getUpdateMetadata(): UpdateMetadata | null {
const overridePath = path.join(app.getPath('userData'), 'auto-claude-source');
const metadataPath = path.join(overridePath, '.update-metadata.json');
if (!existsSync(metadataPath)) {
return null;
}
try {
return JSON.parse(readFileSync(metadataPath, 'utf-8')) as UpdateMetadata;
} catch {
return null;
}
}
@@ -1,96 +1,22 @@
/**
* Version management utilities
*
* Simplified version that uses only the bundled app version.
* The "source updater" system has been removed since the backend
* is bundled with the app and updates via electron-updater.
*/
import { app } from 'electron';
import { existsSync, readFileSync } from 'fs';
import path from 'path';
import type { UpdateMetadata } from './types';
/**
* Get the current app/framework version from package.json
*
* Uses app.getVersion() (from package.json) as the base version.
* Uses app.getVersion() (from package.json) as the version.
*/
export function getBundledVersion(): string {
return app.getVersion();
}
/**
* Get the effective version - accounts for source updates
*
* Returns the updated source version if an update has been applied,
* otherwise returns the bundled version.
*/
export function getEffectiveVersion(): string {
const isDebug = process.env.DEBUG === 'true';
// Build list of paths to check for update metadata
const metadataPaths: string[] = [];
if (app.isPackaged) {
// Production: check userData override path
metadataPaths.push(
path.join(app.getPath('userData'), 'auto-claude-source', '.update-metadata.json')
);
} else {
// Development: check the actual source paths where updates are written
const possibleSourcePaths = [
// Apps structure: apps/backend
path.join(app.getAppPath(), '..', 'backend'),
path.join(process.cwd(), 'apps', 'backend'),
path.resolve(__dirname, '..', '..', '..', 'backend')
];
for (const sourcePath of possibleSourcePaths) {
metadataPaths.push(path.join(sourcePath, '.update-metadata.json'));
}
}
if (isDebug) {
console.log('[Version] Checking metadata paths:', metadataPaths);
}
// Check each path for metadata
for (const metadataPath of metadataPaths) {
const exists = existsSync(metadataPath);
if (isDebug) {
console.log(`[Version] Checking ${metadataPath}: ${exists ? 'EXISTS' : 'not found'}`);
}
if (exists) {
try {
const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8')) as UpdateMetadata;
if (metadata.version) {
if (isDebug) {
console.log(`[Version] Found metadata version: ${metadata.version}`);
}
return metadata.version;
}
} catch (e) {
if (isDebug) {
console.log(`[Version] Error reading metadata: ${e}`);
}
// Continue to next path
}
}
}
const bundledVersion = app.getVersion();
if (isDebug) {
console.log(`[Version] No metadata found, using bundled version: ${bundledVersion}`);
}
return bundledVersion;
}
/**
* Parse version from GitHub release tag
* Handles tags like "v1.2.0", "1.2.0", "v1.2.0-beta"
*/
export function parseVersionFromTag(tag: string): string {
// Remove leading 'v' if present
return tag.replace(/^v/, '');
}
/**
* Parse a version string into its components
* Handles versions like "2.7.2", "2.7.2-beta.6", "2.7.2-alpha.1"
@@ -8,7 +8,6 @@
* - Changelog operations
* - Linear integration
* - GitHub integration
* - Auto-build source updates
* - Shell operations
*/
@@ -19,7 +18,6 @@ import { createChangelogAPI, ChangelogAPI } from './modules/changelog-api';
import { createLinearAPI, LinearAPI } from './modules/linear-api';
import { createGitHubAPI, GitHubAPI } from './modules/github-api';
import { createGitLabAPI, GitLabAPI } from './modules/gitlab-api';
import { createAutoBuildAPI, AutoBuildAPI } from './modules/autobuild-api';
import { createShellAPI, ShellAPI } from './modules/shell-api';
/**
@@ -34,7 +32,6 @@ export interface AgentAPI extends
LinearAPI,
GitHubAPI,
GitLabAPI,
AutoBuildAPI,
ShellAPI {}
/**
@@ -50,7 +47,6 @@ export const createAgentAPI = (): AgentAPI => {
const linearAPI = createLinearAPI();
const githubAPI = createGitHubAPI();
const gitlabAPI = createGitLabAPI();
const autobuildAPI = createAutoBuildAPI();
const shellAPI = createShellAPI();
return {
@@ -75,9 +71,6 @@ export const createAgentAPI = (): AgentAPI => {
// GitLab Integration API
...gitlabAPI,
// Auto-Build Source Update API
...autobuildAPI,
// Shell Operations API
...shellAPI
};
@@ -92,6 +85,5 @@ export type {
LinearAPI,
GitHubAPI,
GitLabAPI,
AutoBuildAPI,
ShellAPI
};
@@ -16,6 +16,7 @@ export interface AppUpdateAPI {
// Operations
checkAppUpdate: () => Promise<IPCResult<AppUpdateInfo | null>>;
downloadAppUpdate: () => Promise<IPCResult>;
downloadStableUpdate: () => Promise<IPCResult>;
installAppUpdate: () => void;
getAppVersion: () => Promise<string>;
@@ -29,6 +30,9 @@ export interface AppUpdateAPI {
onAppUpdateProgress: (
callback: (progress: AppUpdateProgress) => void
) => IpcListenerCleanup;
onAppUpdateStableDowngrade: (
callback: (info: AppUpdateInfo) => void
) => IpcListenerCleanup;
}
/**
@@ -42,6 +46,9 @@ export const createAppUpdateAPI = (): AppUpdateAPI => ({
downloadAppUpdate: (): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_DOWNLOAD),
downloadStableUpdate: (): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_DOWNLOAD_STABLE),
installAppUpdate: (): void => {
invokeIpc(IPC_CHANNELS.APP_UPDATE_INSTALL);
},
@@ -63,5 +70,10 @@ export const createAppUpdateAPI = (): AppUpdateAPI => ({
onAppUpdateProgress: (
callback: (progress: AppUpdateProgress) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.APP_UPDATE_PROGRESS, callback)
createIpcListener(IPC_CHANNELS.APP_UPDATE_PROGRESS, callback),
onAppUpdateStableDowngrade: (
callback: (info: AppUpdateInfo) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.APP_UPDATE_STABLE_DOWNGRADE, callback)
});
@@ -1,43 +0,0 @@
import { IPC_CHANNELS } from '../../../shared/constants';
import type {
AutoBuildSourceUpdateCheck,
AutoBuildSourceUpdateProgress,
IPCResult
} from '../../../shared/types';
import { createIpcListener, invokeIpc, sendIpc, IpcListenerCleanup } from './ipc-utils';
/**
* Auto-Build Source Update API operations
*/
export interface AutoBuildAPI {
// Operations
checkAutoBuildSourceUpdate: () => Promise<IPCResult<AutoBuildSourceUpdateCheck>>;
downloadAutoBuildSourceUpdate: () => void;
getAutoBuildSourceVersion: () => Promise<IPCResult<string>>;
// Event Listeners
onAutoBuildSourceUpdateProgress: (
callback: (progress: AutoBuildSourceUpdateProgress) => void
) => IpcListenerCleanup;
}
/**
* Creates the Auto-Build Source Update API implementation
*/
export const createAutoBuildAPI = (): AutoBuildAPI => ({
// Operations
checkAutoBuildSourceUpdate: (): Promise<IPCResult<AutoBuildSourceUpdateCheck>> =>
invokeIpc(IPC_CHANNELS.AUTOBUILD_SOURCE_CHECK),
downloadAutoBuildSourceUpdate: (): void =>
sendIpc(IPC_CHANNELS.AUTOBUILD_SOURCE_DOWNLOAD),
getAutoBuildSourceVersion: (): Promise<IPCResult<string>> =>
invokeIpc(IPC_CHANNELS.AUTOBUILD_SOURCE_VERSION),
// Event Listeners
onAutoBuildSourceUpdateProgress: (
callback: (progress: AutoBuildSourceUpdateProgress) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS, callback)
});
@@ -11,6 +11,5 @@ export * from './insights-api';
export * from './changelog-api';
export * from './linear-api';
export * from './github-api';
export * from './autobuild-api';
export * from './shell-api';
export * from './debug-api';
@@ -3,25 +3,21 @@ import { useTranslation } from 'react-i18next';
import {
RefreshCw,
CheckCircle2,
AlertCircle,
CloudDownload,
Loader2,
ExternalLink,
Download,
Sparkles
Sparkles,
ArrowDownToLine,
X
} from 'lucide-react';
import { Button } from '../ui/button';
import { Label } from '../ui/label';
import { Switch } from '../ui/switch';
import { Progress } from '../ui/progress';
import { cn } from '../../lib/utils';
import { SettingsSection } from './SettingsSection';
import type {
AppSettings,
AutoBuildSourceUpdateCheck,
AutoBuildSourceUpdateProgress,
AppUpdateAvailableEvent,
AppUpdateProgress,
AppUpdateInfo,
NotificationSettings
} from '../../../shared/types';
@@ -75,53 +71,22 @@ interface AdvancedSettingsProps {
export function AdvancedSettings({ settings, onSettingsChange, section, version }: AdvancedSettingsProps) {
const { t } = useTranslation('settings');
// Auto Claude source update state
const [sourceUpdateCheck, setSourceUpdateCheck] = useState<AutoBuildSourceUpdateCheck | null>(null);
const [isCheckingSourceUpdate, setIsCheckingSourceUpdate] = useState(false);
const [isDownloadingUpdate, setIsDownloadingUpdate] = useState(false);
const [downloadProgress, setDownloadProgress] = useState<AutoBuildSourceUpdateProgress | null>(null);
// Local version state that can be updated after successful update
const [displayVersion, setDisplayVersion] = useState<string>(version);
// Electron app update state
const [appUpdateInfo, setAppUpdateInfo] = useState<AppUpdateAvailableEvent | null>(null);
const [_isCheckingAppUpdate, setIsCheckingAppUpdate] = useState(false);
const [isCheckingAppUpdate, setIsCheckingAppUpdate] = useState(false);
const [isDownloadingAppUpdate, setIsDownloadingAppUpdate] = useState(false);
const [appDownloadProgress, setAppDownloadProgress] = useState<AppUpdateProgress | null>(null);
const [isAppUpdateDownloaded, setIsAppUpdateDownloaded] = useState(false);
// Sync displayVersion with prop when it changes
useEffect(() => {
setDisplayVersion(version);
}, [version]);
// Stable downgrade state (shown when user turns off beta while on prerelease)
const [stableDowngradeInfo, setStableDowngradeInfo] = useState<AppUpdateInfo | null>(null);
// Check for updates on mount
useEffect(() => {
if (section === 'updates') {
checkForSourceUpdates();
checkForAppUpdates();
}
}, [section]);
// Listen for source download progress
useEffect(() => {
const cleanup = window.electronAPI.onAutoBuildSourceUpdateProgress((progress) => {
setDownloadProgress(progress);
if (progress.stage === 'complete') {
setIsDownloadingUpdate(false);
// Update the displayed version if a new version was provided
if (progress.newVersion) {
setDisplayVersion(progress.newVersion);
}
checkForSourceUpdates();
} else if (progress.stage === 'error') {
setIsDownloadingUpdate(false);
}
});
return cleanup;
}, []);
// Listen for app update events
useEffect(() => {
const cleanupAvailable = window.electronAPI.onAppUpdateAvailable((info) => {
@@ -134,16 +99,24 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
setIsDownloadingAppUpdate(false);
setIsAppUpdateDownloaded(true);
setAppDownloadProgress(null);
// Clear downgrade info if any update downloaded
setStableDowngradeInfo(null);
});
const cleanupProgress = window.electronAPI.onAppUpdateProgress((progress) => {
setAppDownloadProgress(progress);
});
// Listen for stable downgrade available (when user turns off beta while on prerelease)
const cleanupStableDowngrade = window.electronAPI.onAppUpdateStableDowngrade((info) => {
setStableDowngradeInfo(info);
});
return () => {
cleanupAvailable();
cleanupDownloaded();
cleanupProgress();
cleanupStableDowngrade();
};
}, []);
@@ -167,7 +140,12 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
const handleDownloadAppUpdate = async () => {
setIsDownloadingAppUpdate(true);
try {
await window.electronAPI.downloadAppUpdate();
const result = await window.electronAPI.downloadAppUpdate();
if (!result.success) {
console.error('Failed to download app update:', result.error);
setIsDownloadingAppUpdate(false);
}
// Note: Success case is handled by the onAppUpdateDownloaded event listener
} catch (err) {
console.error('Failed to download app update:', err);
setIsDownloadingAppUpdate(false);
@@ -178,30 +156,24 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
window.electronAPI.installAppUpdate();
};
const checkForSourceUpdates = async () => {
console.log('[AdvancedSettings] Checking for source updates...');
setIsCheckingSourceUpdate(true);
const handleDownloadStableVersion = async () => {
setIsDownloadingAppUpdate(true);
try {
const result = await window.electronAPI.checkAutoBuildSourceUpdate();
console.log('[AdvancedSettings] Check result:', result);
if (result.success && result.data) {
setSourceUpdateCheck(result.data);
// Update displayed version from the check result (most accurate)
if (result.data.currentVersion) {
setDisplayVersion(result.data.currentVersion);
}
// Use dedicated stable download API with allowDowngrade enabled
const result = await window.electronAPI.downloadStableUpdate();
if (!result.success) {
console.error('Failed to download stable version:', result.error);
setIsDownloadingAppUpdate(false);
}
// Note: Success case is handled by the onAppUpdateDownloaded event listener
} catch (err) {
console.error('[AdvancedSettings] Check error:', err);
} finally {
setIsCheckingSourceUpdate(false);
console.error('Failed to download stable version:', err);
setIsDownloadingAppUpdate(false);
}
};
const handleDownloadSourceUpdate = () => {
setIsDownloadingUpdate(true);
setDownloadProgress(null);
window.electronAPI.downloadAutoBuildSourceUpdate();
const dismissStableDowngrade = () => {
setStableDowngradeInfo(null);
};
if (section === 'updates') {
@@ -211,7 +183,45 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
description={t('updates.description')}
>
<div className="space-y-6">
{/* Electron App Update Section */}
{/* Current Version Display */}
<div className="rounded-lg border border-border bg-muted/50 p-5 space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground uppercase tracking-wider mb-1">{t('updates.version')}</p>
<p className="text-base font-medium text-foreground">
{version || t('updates.loading')}
</p>
</div>
{isCheckingAppUpdate ? (
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
) : appUpdateInfo ? (
<Download className="h-6 w-6 text-info" />
) : (
<CheckCircle2 className="h-6 w-6 text-success" />
)}
</div>
{/* Update status */}
{!appUpdateInfo && !isCheckingAppUpdate && (
<p className="text-sm text-muted-foreground">
{t('updates.latestVersion')}
</p>
)}
<div className="pt-2">
<Button
size="sm"
variant="outline"
onClick={checkForAppUpdates}
disabled={isCheckingAppUpdate}
>
<RefreshCw className={`mr-2 h-4 w-4 ${isCheckingAppUpdate ? 'animate-spin' : ''}`} />
{t('updates.checkForUpdates')}
</Button>
</div>
</div>
{/* Electron App Update Section - shows when update available */}
{(appUpdateInfo || isAppUpdateDownloaded) && (
<div className="rounded-lg border-2 border-info/50 bg-info/5 p-5 space-y-4">
<div className="flex items-center gap-2 text-info">
@@ -302,113 +312,6 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
</div>
)}
{/* Unified Version Display with Update Check */}
<div className="rounded-lg border border-border bg-muted/50 p-5 space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground uppercase tracking-wider mb-1">{t('updates.version')}</p>
<p className="text-base font-medium text-foreground">
{displayVersion || t('updates.loading')}
</p>
</div>
{isCheckingSourceUpdate ? (
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
) : sourceUpdateCheck?.updateAvailable ? (
<AlertCircle className="h-6 w-6 text-info" />
) : (
<CheckCircle2 className="h-6 w-6 text-success" />
)}
</div>
{/* Update status */}
{isCheckingSourceUpdate ? (
<p className="text-sm text-muted-foreground">
{t('updates.checkingForUpdates')}
</p>
) : sourceUpdateCheck ? (
<>
{sourceUpdateCheck.latestVersion && sourceUpdateCheck.updateAvailable && (
<p className="text-sm text-info">
{t('updates.newVersionAvailable')} {sourceUpdateCheck.latestVersion}
</p>
)}
{sourceUpdateCheck.error && (
<p className="text-sm text-destructive">{sourceUpdateCheck.error}</p>
)}
{!sourceUpdateCheck.updateAvailable && !sourceUpdateCheck.error && (
<p className="text-sm text-muted-foreground">
{t('updates.latestVersion')}
</p>
)}
{sourceUpdateCheck.updateAvailable && (
<div className="space-y-4 pt-2">
{sourceUpdateCheck.releaseNotes && (
<div className="bg-background rounded-lg p-4 max-h-48 overflow-y-auto border border-border/50">
<ReleaseNotesRenderer markdown={sourceUpdateCheck.releaseNotes} />
</div>
)}
{sourceUpdateCheck.releaseUrl && (
<button
onClick={() => window.electronAPI.openExternal(sourceUpdateCheck.releaseUrl!)}
className="inline-flex items-center gap-1.5 text-sm text-info hover:text-info/80 hover:underline transition-colors"
>
<ExternalLink className="h-3.5 w-3.5" />
{t('updates.viewRelease')}
</button>
)}
{isDownloadingUpdate ? (
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm">
<RefreshCw className="h-4 w-4 animate-spin" />
<span>{downloadProgress?.message || 'Downloading...'}</span>
</div>
{downloadProgress?.percent !== undefined && (
<Progress value={downloadProgress.percent} className="h-2" />
)}
</div>
) : downloadProgress?.stage === 'complete' ? (
<div className="flex items-center gap-3 text-sm text-success">
<CheckCircle2 className="h-5 w-5" />
<span>{downloadProgress.message}</span>
</div>
) : downloadProgress?.stage === 'error' ? (
<div className="flex items-center gap-3 text-sm text-destructive">
<AlertCircle className="h-5 w-5" />
<span>{downloadProgress.message}</span>
</div>
) : (
<Button onClick={handleDownloadSourceUpdate}>
<CloudDownload className="mr-2 h-4 w-4" />
{t('updates.downloadUpdate')}
</Button>
)}
</div>
)}
</>
) : (
<p className="text-sm text-muted-foreground">
{t('updates.unableToCheck')}
</p>
)}
<div className="pt-2">
<Button
size="sm"
variant="outline"
onClick={checkForSourceUpdates}
disabled={isCheckingSourceUpdate}
>
<RefreshCw className={cn('mr-2 h-4 w-4', isCheckingSourceUpdate && 'animate-spin')} />
{t('updates.checkForUpdates')}
</Button>
</div>
</div>
<div className="flex items-center justify-between p-4 rounded-lg border border-border">
<div className="space-y-1">
<Label className="font-medium text-foreground">{t('updates.autoUpdateProjects')}</Label>
@@ -433,11 +336,113 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
</div>
<Switch
checked={settings.betaUpdates ?? false}
onCheckedChange={(checked) =>
onSettingsChange({ ...settings, betaUpdates: checked })
}
onCheckedChange={(checked) => {
onSettingsChange({ ...settings, betaUpdates: checked });
if (checked) {
// Clear downgrade info when enabling beta again
setStableDowngradeInfo(null);
} else {
// Clear beta update info when disabling beta, so stable downgrade UI can show
setAppUpdateInfo(null);
}
}}
/>
</div>
{/* Stable Downgrade Section - shown when user turns off beta while on prerelease */}
{stableDowngradeInfo && !appUpdateInfo && (
<div className="rounded-lg border-2 border-warning/50 bg-warning/5 p-5 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-warning">
<ArrowDownToLine className="h-5 w-5" />
<h3 className="font-semibold">{t('updates.stableDowngradeAvailable')}</h3>
</div>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={dismissStableDowngrade}
>
<X className="h-4 w-4" />
</Button>
</div>
<p className="text-sm text-muted-foreground">
{t('updates.stableDowngradeDescription')}
</p>
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
{t('updates.stableVersion')}
</p>
<p className="text-base font-medium text-foreground">
{stableDowngradeInfo.version}
</p>
{stableDowngradeInfo.releaseDate && (
<p className="text-xs text-muted-foreground mt-1">
{t('updates.released')} {new Date(stableDowngradeInfo.releaseDate).toLocaleDateString()}
</p>
)}
</div>
{isDownloadingAppUpdate ? (
<RefreshCw className="h-6 w-6 animate-spin text-warning" />
) : (
<ArrowDownToLine className="h-6 w-6 text-warning" />
)}
</div>
{/* Release Notes */}
{stableDowngradeInfo.releaseNotes && (
<div className="bg-background rounded-lg p-4 max-h-48 overflow-y-auto border border-border/50">
<ReleaseNotesRenderer markdown={stableDowngradeInfo.releaseNotes} />
</div>
)}
{/* Download Progress */}
{isDownloadingAppUpdate && appDownloadProgress && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{t('updates.downloading')}</span>
<span className="text-foreground font-medium">
{Math.round(appDownloadProgress.percent)}%
</span>
</div>
<Progress value={appDownloadProgress.percent} className="h-2" />
<p className="text-xs text-muted-foreground text-right">
{(appDownloadProgress.transferred / 1024 / 1024).toFixed(2)} MB / {(appDownloadProgress.total / 1024 / 1024).toFixed(2)} MB
</p>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-3">
<Button
onClick={handleDownloadStableVersion}
disabled={isDownloadingAppUpdate}
variant="outline"
>
{isDownloadingAppUpdate ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
{t('updates.downloading')}
</>
) : (
<>
<ArrowDownToLine className="mr-2 h-4 w-4" />
{t('updates.downloadStableVersion')}
</>
)}
</Button>
<Button
variant="ghost"
onClick={dismissStableDowngrade}
>
{t('common:actions.dismiss')}
</Button>
</div>
</div>
)}
</div>
</SettingsSection>
);
@@ -174,28 +174,6 @@ export const infrastructureMock = {
onIdeationTypeComplete: () => () => {},
onIdeationTypeFailed: () => () => {},
// Auto-Build Source Update Operations
checkAutoBuildSourceUpdate: async () => ({
success: true,
data: {
updateAvailable: true,
currentVersion: '1.0.0',
latestVersion: '1.1.0',
releaseNotes: '## v1.1.0\n\n- New feature: Enhanced spec creation\n- Bug fix: Improved error handling\n- Performance improvements'
}
}),
downloadAutoBuildSourceUpdate: () => {
console.warn('[Browser Mock] downloadAutoBuildSourceUpdate called');
},
getAutoBuildSourceVersion: async () => ({
success: true,
data: '1.0.0'
}),
onAutoBuildSourceUpdateProgress: () => () => {},
// Shell Operations
openExternal: async (url: string) => {
console.warn('[Browser Mock] openExternal:', url);
@@ -36,10 +36,12 @@ export const settingsMock = {
// App Update Operations (mock - no updates in browser mode)
checkAppUpdate: async () => ({ success: true, data: null }),
downloadAppUpdate: async () => ({ success: true }),
downloadStableUpdate: async () => ({ success: true }),
installAppUpdate: () => { console.warn('[browser-mock] installAppUpdate called'); },
// App Update Event Listeners (no-op in browser mode)
onAppUpdateAvailable: () => () => {},
onAppUpdateDownloaded: () => () => {},
onAppUpdateProgress: () => () => {}
onAppUpdateProgress: () => () => {},
onAppUpdateStableDowngrade: () => () => {}
};
+2 -6
View File
@@ -400,12 +400,6 @@ export const IPC_CHANNELS = {
OLLAMA_PULL_MODEL: 'ollama:pullModel',
OLLAMA_PULL_PROGRESS: 'ollama:pullProgress',
// Auto Claude source updates
AUTOBUILD_SOURCE_CHECK: 'autobuild:source:check',
AUTOBUILD_SOURCE_DOWNLOAD: 'autobuild:source:download',
AUTOBUILD_SOURCE_VERSION: 'autobuild:source:version',
AUTOBUILD_SOURCE_PROGRESS: 'autobuild:source:progress',
// Auto Claude source environment configuration
AUTOBUILD_SOURCE_ENV_GET: 'autobuild:source:env:get',
AUTOBUILD_SOURCE_ENV_UPDATE: 'autobuild:source:env:update',
@@ -463,6 +457,7 @@ export const IPC_CHANNELS = {
// App auto-update operations
APP_UPDATE_CHECK: 'app-update:check',
APP_UPDATE_DOWNLOAD: 'app-update:download',
APP_UPDATE_DOWNLOAD_STABLE: 'app-update:download-stable', // Download stable version (for downgrade from beta)
APP_UPDATE_INSTALL: 'app-update:install',
APP_UPDATE_GET_VERSION: 'app-update:get-version',
@@ -471,6 +466,7 @@ export const IPC_CHANNELS = {
APP_UPDATE_DOWNLOADED: 'app-update:downloaded',
APP_UPDATE_PROGRESS: 'app-update:progress',
APP_UPDATE_ERROR: 'app-update:error',
APP_UPDATE_STABLE_DOWNGRADE: 'app-update:stable-downgrade', // Stable version available for downgrade from beta
// Release operations
RELEASE_SUGGEST_VERSION: 'release:suggestVersion',
@@ -161,7 +161,11 @@
"autoUpdateProjects": "Auto-Update Projects",
"autoUpdateProjectsDescription": "Automatically update Auto Claude in projects when a new version is available",
"betaUpdates": "Beta Updates",
"betaUpdatesDescription": "Receive pre-release beta versions with new features (may be less stable)"
"betaUpdatesDescription": "Receive pre-release beta versions with new features (may be less stable)",
"stableDowngradeAvailable": "Stable Version Available",
"stableDowngradeDescription": "You're currently on a beta version. Since you've disabled beta updates, you can switch to the latest stable release.",
"stableVersion": "Stable Version",
"downloadStableVersion": "Download Stable Version"
},
"notifications": {
"title": "Notifications",
@@ -161,7 +161,11 @@
"autoUpdateProjects": "Mise à jour automatique des projets",
"autoUpdateProjectsDescription": "Mettre à jour automatiquement Auto Claude dans les projets quand une nouvelle version est disponible",
"betaUpdates": "Mises à jour bêta",
"betaUpdatesDescription": "Recevoir les versions bêta pré-release avec de nouvelles fonctionnalités (peut être moins stable)"
"betaUpdatesDescription": "Recevoir les versions bêta pré-release avec de nouvelles fonctionnalités (peut être moins stable)",
"stableDowngradeAvailable": "Version stable disponible",
"stableDowngradeDescription": "Vous êtes actuellement sur une version bêta. Comme vous avez désactivé les mises à jour bêta, vous pouvez passer à la dernière version stable.",
"stableVersion": "Version stable",
"downloadStableVersion": "Télécharger la version stable"
},
"notifications": {
"title": "Notifications",
+5 -11
View File
@@ -62,7 +62,7 @@ import type {
ClaudeAuthResult,
ClaudeUsageSnapshot
} from './agent';
import type { AppSettings, SourceEnvConfig, SourceEnvCheckResult, AutoBuildSourceUpdateCheck, AutoBuildSourceUpdateProgress } from './settings';
import type { AppSettings, SourceEnvConfig, SourceEnvCheckResult } from './settings';
import type { AppUpdateInfo, AppUpdateProgress, AppUpdateAvailableEvent, AppUpdateDownloadedEvent } from './app-update';
import type {
ChangelogTask,
@@ -567,19 +567,10 @@ export interface ElectronAPI {
callback: (projectId: string, ideationType: string) => void
) => () => void;
// Auto Claude source update operations
checkAutoBuildSourceUpdate: () => Promise<IPCResult<AutoBuildSourceUpdateCheck>>;
downloadAutoBuildSourceUpdate: () => void;
getAutoBuildSourceVersion: () => Promise<IPCResult<string>>;
// Auto Claude source update event listeners
onAutoBuildSourceUpdateProgress: (
callback: (progress: AutoBuildSourceUpdateProgress) => void
) => () => void;
// Electron app update operations
checkAppUpdate: () => Promise<IPCResult<AppUpdateInfo | null>>;
downloadAppUpdate: () => Promise<IPCResult>;
downloadStableUpdate: () => Promise<IPCResult>;
installAppUpdate: () => void;
// Electron app update event listeners
@@ -592,6 +583,9 @@ export interface ElectronAPI {
onAppUpdateProgress: (
callback: (progress: AppUpdateProgress) => void
) => () => void;
onAppUpdateStableDowngrade: (
callback: (info: AppUpdateInfo) => void
) => () => void;
// Shell operations
openExternal: (url: string) => Promise<void>;
@@ -294,27 +294,3 @@ export interface SourceEnvCheckResult {
sourcePath?: string;
error?: string;
}
// Auto Claude Source Update Types
export interface AutoBuildSourceUpdateCheck {
updateAvailable: boolean;
currentVersion: string;
latestVersion?: string;
releaseNotes?: string;
releaseUrl?: string;
error?: string;
}
export interface AutoBuildSourceUpdateResult {
success: boolean;
version?: string;
error?: string;
}
export interface AutoBuildSourceUpdateProgress {
stage: 'checking' | 'downloading' | 'extracting' | 'complete' | 'error';
percent?: number;
message: string;
/** New version after successful update - used to refresh UI */
newVersion?: string;
}
+28
View File
@@ -182,7 +182,35 @@ python validate_spec.py --spec-dir specs/001-feature --checkpoint all
## Environment Variables
Copy `.env.example` to `.env` and configure as needed:
```bash
cp .env.example .env
```
### Core Settings
| Variable | Required | Description |
|----------|----------|-------------|
| `CLAUDE_CODE_OAUTH_TOKEN` | Yes | OAuth token from `claude setup-token` |
| `AUTO_BUILD_MODEL` | No | Model override (default: claude-opus-4-5-20251101) |
| `DEFAULT_BRANCH` | No | Base branch for worktrees (auto-detects main/master) |
| `DEBUG` | No | Enable debug logging (default: false) |
### Integrations
| Variable | Required | Description |
|----------|----------|-------------|
| `LINEAR_API_KEY` | No | Linear API key for task sync |
| `GITLAB_TOKEN` | No | GitLab Personal Access Token |
| `GITLAB_INSTANCE_URL` | No | GitLab instance URL (defaults to gitlab.com) |
### Memory Layer (Graphiti)
| Variable | Required | Description |
|----------|----------|-------------|
| `GRAPHITI_ENABLED` | No | Enable Memory Layer (default: true) |
| `GRAPHITI_LLM_PROVIDER` | No | LLM provider: openai, anthropic, ollama, google, openrouter |
| `GRAPHITI_EMBEDDER_PROVIDER` | No | Embedder: openai, voyage, ollama, google, openrouter |
See `.env.example` for complete configuration options including provider-specific settings.
+95
View File
@@ -0,0 +1,95 @@
# Linux Installation & Building Guide
This guide covers Linux-specific installation options and building from source.
## Flatpak Installation
Flatpak packages are available for Linux users who prefer sandboxed applications.
### Download Flatpak
See the [main README](../README.md#beta-release) for Flatpak download links in the Beta Release section.
### Building Flatpak from Source
To build the Flatpak package yourself, you need additional dependencies:
```bash
# Fedora/RHEL
sudo dnf install flatpak-builder
# Ubuntu/Debian
sudo apt install flatpak-builder
# Install required Flatpak runtimes
flatpak install flathub org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08
flatpak install flathub org.electronjs.Electron2.BaseApp//25.08
# Build the Flatpak
cd apps/frontend
npm run package:flatpak
```
The Flatpak will be created in `apps/frontend/dist/`.
### Installing the Built Flatpak
After building, install the Flatpak locally:
```bash
flatpak install --user apps/frontend/dist/Auto-Claude-*.flatpak
```
### Running from Flatpak
```bash
flatpak run com.autoclaude.AutoClaude
```
## Other Linux Packages
### AppImage
AppImage files are portable and don't require installation:
```bash
# Make executable
chmod +x Auto-Claude-*-linux-x86_64.AppImage
# Run
./Auto-Claude-*-linux-x86_64.AppImage
```
### Debian Package (.deb)
For Ubuntu/Debian systems:
```bash
sudo dpkg -i Auto-Claude-*-linux-amd64.deb
```
## Troubleshooting
### Flatpak Runtime Issues
If you encounter runtime issues with Flatpak:
```bash
# Update runtimes
flatpak update
# Check for missing runtimes
flatpak list --runtime
```
### AppImage Not Starting
If the AppImage doesn't start:
```bash
# Check for missing libraries
ldd ./Auto-Claude-*-linux-x86_64.AppImage
# Try running with debug output
./Auto-Claude-*-linux-x86_64.AppImage --verbose
```