From 648f05c1d0159ae9cf7b323ec29c06c5886a9e97 Mon Sep 17 00:00:00 2001 From: ColemanRoo Date: Tue, 26 Nov 2024 11:17:35 -0600 Subject: [PATCH] Adding functionality to check for Roo-Cline updates --- package-lock.json | 4 +- package.json | 4 +- src/extension.ts | 26 ++++ src/utils/__tests__/version-check.test.ts | 151 ++++++++++++++++++++++ src/utils/version-check.ts | 116 +++++++++++++++++ 5 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 src/utils/__tests__/version-check.test.ts create mode 100644 src/utils/version-check.ts diff --git a/package-lock.json b/package-lock.json index 057953752c..4617451da8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "2.0.3", + "version": "2.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "2.0.3", + "version": "2.1.2", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 9fd1509279..c3644ea25b 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "roo-cline", "displayName": "Roo Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", - "version": "2.1.1", + "version": "2.1.2", "files": [ - "bin/roo-cline-2.1.1.vsix", + "bin/roo-cline-2.1.2.vsix", "assets/icons/icon_Roo.png" ], "icon": "assets/icons/icon_Roo.png", diff --git a/src/extension.ts b/src/extension.ts index 6a3dad32fb..a06c418811 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" +import { scheduleUpdateChecks, checkForUpdates } from "./utils/version-check" /* Built using https://github.com/microsoft/vscode-webview-ui-toolkit @@ -26,6 +27,11 @@ export function activate(context: vscode.ExtensionContext) { outputChannel.appendLine("Cline extension activated") + // Schedule periodic update checks + scheduleUpdateChecks(context).catch(error => { + outputChannel.appendLine(`Failed to initialize update checks: ${error}`) + }) + const sidebarProvider = new ClineProvider(context, outputChannel) context.subscriptions.push( @@ -37,6 +43,26 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand("cline.plusButtonClicked", async () => { outputChannel.appendLine("Plus button Clicked") + + // Check for updates before starting new task + const updateCheck = await checkForUpdates(context); + if (updateCheck?.updateAvailable) { + const choice = await vscode.window.showInformationMessage( + `A new version of Roo Cline (${updateCheck.latest}) is available. It's recommended to update before starting a new task.`, + 'Update Now', + 'Continue Anyway', + 'Cancel' + ); + + if (choice === 'Cancel') { + return; + } else if (choice === 'Update Now') { + // User chose to update, don't start new task + return; + } + // If 'Continue Anyway', proceed with new task + } + await sidebarProvider.clearTask() await sidebarProvider.postStateToWebview() await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) diff --git a/src/utils/__tests__/version-check.test.ts b/src/utils/__tests__/version-check.test.ts new file mode 100644 index 0000000000..c0845f5701 --- /dev/null +++ b/src/utils/__tests__/version-check.test.ts @@ -0,0 +1,151 @@ +import * as vscode from 'vscode'; +import { exec } from 'child_process'; +import { checkForUpdates, VersionInfo } from '../version-check'; +import * as fs from 'fs/promises'; + +// Mock dependencies +jest.mock('child_process'); +jest.mock('fs/promises'); +jest.mock('vscode', () => ({ + window: { + showInformationMessage: jest.fn(), + showErrorMessage: jest.fn(), + withProgress: jest.fn(async (options, task) => task()), + }, + commands: { + executeCommand: jest.fn(), + }, + extensions: { + getExtension: jest.fn(), + }, +})); + +describe('version-check', () => { + let mockContext: vscode.ExtensionContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = {} as vscode.ExtensionContext; + + // Mock extension version + (vscode.extensions.getExtension as jest.Mock).mockReturnValue({ + packageJSON: { version: '1.0.0' } + }); + }); + + describe('checkForUpdates', () => { + it('should detect when update is available', async () => { + // Mock CodeArtifact login script exists + (fs.access as jest.Mock).mockResolvedValue(undefined); + + // Mock successful command executions + (exec as unknown as jest.Mock).mockImplementation((cmd, callback) => { + if (callback) { + if (cmd.includes('codeartifact-login.sh')) { + callback(null, { stdout: 'Login successful' }, ''); + } else if (cmd.includes('npm view')) { + callback(null, { stdout: '1.1.0\n' }, ''); + } + } + return { + stdout: cmd.includes('npm view') ? '1.1.0\n' : 'Login successful', + stderr: '', + }; + }); + + const result = await checkForUpdates(mockContext); + + expect(result).toEqual({ + current: '1.0.0', + latest: '1.1.0', + updateAvailable: true + }); + }); + + it('should handle when no update is available', async () => { + // Mock CodeArtifact login script exists + (fs.access as jest.Mock).mockResolvedValue(undefined); + + // Mock successful command executions with same version + (exec as unknown as jest.Mock).mockImplementation((cmd, callback) => { + if (callback) { + if (cmd.includes('codeartifact-login.sh')) { + callback(null, { stdout: 'Login successful' }, ''); + } else if (cmd.includes('npm view')) { + callback(null, { stdout: '1.0.0\n' }, ''); + } + } + return { + stdout: cmd.includes('npm view') ? '1.0.0\n' : 'Login successful', + stderr: '', + }; + }); + + const result = await checkForUpdates(mockContext); + + expect(result).toEqual({ + current: '1.0.0', + latest: '1.0.0', + updateAvailable: false + }); + }); + + it('should fallback to direct auth token when login script missing', async () => { + // Mock CodeArtifact login script does not exist + (fs.access as jest.Mock).mockRejectedValue(new Error('ENOENT')); + + // Mock successful command executions + (exec as unknown as jest.Mock).mockImplementation((cmd, callback) => { + if (callback) { + if (cmd.includes('get-authorization-token')) { + callback(null, { stdout: 'token123' }, ''); + } else if (cmd.includes('npm view')) { + callback(null, { stdout: '1.1.0\n' }, ''); + } + } + return { + stdout: cmd.includes('npm view') ? '1.1.0\n' : 'token123', + stderr: '', + }; + }); + + const result = await checkForUpdates(mockContext); + + expect(result).toEqual({ + current: '1.0.0', + latest: '1.1.0', + updateAvailable: true + }); + + // Verify it tried to get auth token directly + expect(exec).toHaveBeenCalledWith( + expect.stringContaining('aws codeartifact get-authorization-token'), + expect.any(Function) + ); + }); + + it('should handle errors and return null', async () => { + // Mock CodeArtifact login script exists but fails + (fs.access as jest.Mock).mockResolvedValue(undefined); + (exec as unknown as jest.Mock).mockImplementation((cmd, callback) => { + if (callback) { + callback(new Error('Command failed'), '', 'Error executing command'); + } + throw new Error('Command failed'); + }); + + const result = await checkForUpdates(mockContext); + + expect(result).toBeNull(); + }); + + it('should handle missing extension version', async () => { + // Mock extension not found + (vscode.extensions.getExtension as jest.Mock).mockReturnValue(undefined); + + const result = await checkForUpdates(mockContext); + + expect(result).toBeNull(); + }); + }); +}); diff --git a/src/utils/version-check.ts b/src/utils/version-check.ts new file mode 100644 index 0000000000..3d70d02683 --- /dev/null +++ b/src/utils/version-check.ts @@ -0,0 +1,116 @@ +import * as vscode from 'vscode'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as path from 'path'; +import * as fs from 'fs/promises'; +import * as os from 'os'; + +const execAsync = promisify(exec); + +export interface VersionInfo { + current: string; + latest: string; + updateAvailable: boolean; +} + +export async function checkForUpdates(context: vscode.ExtensionContext): Promise { + try { + // Get current version from package.json + const currentVersion = vscode.extensions.getExtension('roo-vet.roo-cline')?.packageJSON.version; + + // Run CodeArtifact login to ensure we have valid credentials + const loginScript = path.join(__dirname, '..', '..', 'scripts', 'codeartifact-login.sh'); + if (await fs.access(loginScript).then(() => true).catch(() => false)) { + await execAsync(`bash "${loginScript}"`); + } else { + // If login script doesn't exist, try to get auth token directly + await execAsync('aws codeartifact get-authorization-token --domain roo --query authorizationToken --output text'); + } + + // Get latest version from CodeArtifact + const { stdout } = await execAsync('npm view roo-cline version'); + const latestVersion = stdout.trim(); + + return { + current: currentVersion, + latest: latestVersion, + updateAvailable: currentVersion !== latestVersion + }; + } catch (error) { + console.error('Failed to check for updates:', error); + return null; + } +} + +export async function scheduleUpdateChecks(context: vscode.ExtensionContext) { + // Check for updates on startup + const initialCheck = await checkForUpdates(context); + if (initialCheck?.updateAvailable) { + showUpdateNotification(initialCheck.latest); + } + + // Check for updates every 24 hours + setInterval(async () => { + const check = await checkForUpdates(context); + if (check?.updateAvailable) { + showUpdateNotification(check.latest); + } + }, 24 * 60 * 60 * 1000); // 24 hours in milliseconds +} + +function showUpdateNotification(latestVersion: string) { + vscode.window.showInformationMessage( + `A new version of Roo Cline (${latestVersion}) is available!`, + 'Update Now', + 'Later' + ).then(selection => { + if (selection === 'Update Now') { + installUpdate(latestVersion); + } + }); +} + +async function installUpdate(version: string) { + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "Updating Roo Cline...", + cancellable: false + }, async (progress) => { + try { + // Create temp directory for installation + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'roo-cline-update-')); + + progress.report({ message: 'Installing new version...' }); + + // Install roo-cline in temp directory + await execAsync('npm init -y', { cwd: tempDir }); + await execAsync('npm install roo-cline', { cwd: tempDir }); + + // Get path to vsix file + const vsixPath = path.join(tempDir, 'node_modules', 'roo-cline', 'bin', `roo-cline-${version}.vsix`); + + progress.report({ message: 'Installing extension...' }); + + // Install the extension + const editor = process.env.TERM_PROGRAM === 'vscode' ? 'code' : 'cursor'; + await execAsync(`${editor} --install-extension "${vsixPath}"`); + + // Clean up temp directory + await fs.rm(tempDir, { recursive: true, force: true }); + + // Show success message with reload button + const action = await vscode.window.showInformationMessage( + 'Roo Cline has been updated successfully!', + 'Reload Now' + ); + + if (action === 'Reload Now') { + // Reload the window to activate the new version + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + } catch (error) { + vscode.window.showErrorMessage('Failed to update Roo Cline. Please try again later.'); + console.error('Update failed:', error); + } + }); +}