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:
@@ -4,11 +4,9 @@
|
||||
|
||||

|
||||
|
||||
<!-- TOP_VERSION_BADGE -->
|
||||
[](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.7.2)
|
||||
<!-- TOP_VERSION_BADGE_END -->
|
||||
[](./agpl-3.0.txt)
|
||||
[](https://discord.gg/KCXaPBr4Dj)
|
||||
[](https://www.youtube.com/@AndreMikalsen)
|
||||
[](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 |
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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: () => () => {}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user