mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-12 14:22:00 -07:00
Add support for auto-updating git extensions (#8511)
This commit is contained in:
@@ -85,9 +85,8 @@ import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||
import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
|
||||
import { useSessionStats } from './contexts/SessionContext.js';
|
||||
import { useGitBranchName } from './hooks/useGitBranchName.js';
|
||||
import { useExtensionUpdates } from './hooks/useExtensionUpdates.js';
|
||||
import { FocusContext } from './contexts/FocusContext.js';
|
||||
import type { ExtensionUpdateState } from './state/extensions.js';
|
||||
import { checkForAllExtensionUpdates } from '../config/extension.js';
|
||||
|
||||
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
||||
|
||||
@@ -149,9 +148,14 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const [isTrustedFolder, setIsTrustedFolder] = useState<boolean | undefined>(
|
||||
config.isTrustedFolder(),
|
||||
);
|
||||
const [extensionsUpdateState, setExtensionsUpdateState] = useState(
|
||||
new Map<string, ExtensionUpdateState>(),
|
||||
);
|
||||
|
||||
const extensions = config.getExtensions();
|
||||
const { extensionsUpdateState, setExtensionsUpdateState } =
|
||||
useExtensionUpdates(
|
||||
extensions,
|
||||
historyManager.addItem,
|
||||
config.getWorkingDir(),
|
||||
);
|
||||
|
||||
// Helper to determine the effective model, considering the fallback state.
|
||||
const getEffectiveModel = useCallback(() => {
|
||||
@@ -1196,11 +1200,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
],
|
||||
);
|
||||
|
||||
const extensions = config.getExtensions();
|
||||
useEffect(() => {
|
||||
checkForAllExtensionUpdates(extensions, setExtensionsUpdateState);
|
||||
}, [extensions, setExtensionsUpdateState]);
|
||||
|
||||
return (
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<UIActionsContext.Provider value={uiActions}>
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import {
|
||||
updateAllUpdatableExtensions,
|
||||
updateExtensionByName,
|
||||
} from '../../config/extension.js';
|
||||
updateExtension,
|
||||
} from '../../config/extensions/update.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { extensionsCommand } from './extensionsCommand.js';
|
||||
@@ -20,14 +21,15 @@ import {
|
||||
beforeEach,
|
||||
type MockedFunction,
|
||||
} from 'vitest';
|
||||
import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
|
||||
vi.mock('../../config/extension.js', () => ({
|
||||
updateExtensionByName: vi.fn(),
|
||||
vi.mock('../../config/extensions/update.js', () => ({
|
||||
updateExtension: vi.fn(),
|
||||
updateAllUpdatableExtensions: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUpdateExtensionByName = updateExtensionByName as MockedFunction<
|
||||
typeof updateExtensionByName
|
||||
const mockUpdateExtension = updateExtension as MockedFunction<
|
||||
typeof updateExtension
|
||||
>;
|
||||
|
||||
const mockUpdateAllUpdatableExtensions =
|
||||
@@ -35,6 +37,8 @@ const mockUpdateAllUpdatableExtensions =
|
||||
typeof updateAllUpdatableExtensions
|
||||
>;
|
||||
|
||||
const mockGetExtensions = vi.fn();
|
||||
|
||||
describe('extensionsCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
|
||||
@@ -43,7 +47,7 @@ describe('extensionsCommand', () => {
|
||||
mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getExtensions: () => [],
|
||||
getExtensions: mockGetExtensions,
|
||||
getWorkingDir: () => '/test/dir',
|
||||
},
|
||||
},
|
||||
@@ -147,36 +151,73 @@ describe('extensionsCommand', () => {
|
||||
});
|
||||
|
||||
it('should update a single extension by name', async () => {
|
||||
mockUpdateExtensionByName.mockResolvedValue({
|
||||
const extension: GeminiCLIExtension = {
|
||||
name: 'ext-one',
|
||||
originalVersion: '1.0.0',
|
||||
type: 'git',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: '/test/dir/ext-one',
|
||||
autoUpdate: false,
|
||||
};
|
||||
mockUpdateExtension.mockResolvedValue({
|
||||
name: extension.name,
|
||||
originalVersion: extension.version,
|
||||
updatedVersion: '1.0.1',
|
||||
});
|
||||
mockGetExtensions.mockReturnValue([extension]);
|
||||
mockContext.ui.extensionsUpdateState.set(
|
||||
extension.name,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
);
|
||||
await updateAction(mockContext, 'ext-one');
|
||||
expect(mockUpdateExtensionByName).toHaveBeenCalledWith(
|
||||
'ext-one',
|
||||
expect(mockUpdateExtension).toHaveBeenCalledWith(
|
||||
extension,
|
||||
'/test/dir',
|
||||
[],
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors when updating a single extension', async () => {
|
||||
mockUpdateExtensionByName.mockRejectedValue(
|
||||
new Error('Extension not found'),
|
||||
);
|
||||
mockUpdateExtension.mockRejectedValue(new Error('Extension not found'));
|
||||
mockGetExtensions.mockReturnValue([]);
|
||||
await updateAction(mockContext, 'ext-one');
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: 'Extension not found',
|
||||
text: 'Extension ext-one not found.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update multiple extensions by name', async () => {
|
||||
mockUpdateExtensionByName
|
||||
const extensionOne: GeminiCLIExtension = {
|
||||
name: 'ext-one',
|
||||
type: 'git',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: '/test/dir/ext-one',
|
||||
autoUpdate: false,
|
||||
};
|
||||
const extensionTwo: GeminiCLIExtension = {
|
||||
name: 'ext-two',
|
||||
type: 'git',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: '/test/dir/ext-two',
|
||||
autoUpdate: false,
|
||||
};
|
||||
mockGetExtensions.mockReturnValue([extensionOne, extensionTwo]);
|
||||
mockContext.ui.extensionsUpdateState.set(
|
||||
extensionOne.name,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
);
|
||||
mockContext.ui.extensionsUpdateState.set(
|
||||
extensionTwo.name,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
);
|
||||
mockUpdateExtension
|
||||
.mockResolvedValueOnce({
|
||||
name: 'ext-one',
|
||||
originalVersion: '1.0.0',
|
||||
@@ -188,7 +229,7 @@ describe('extensionsCommand', () => {
|
||||
updatedVersion: '2.0.1',
|
||||
});
|
||||
await updateAction(mockContext, 'ext-one ext-two');
|
||||
expect(mockUpdateExtensionByName).toHaveBeenCalledTimes(2);
|
||||
expect(mockUpdateExtension).toHaveBeenCalledTimes(2);
|
||||
expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith({
|
||||
type: MessageType.EXTENSIONS_LIST,
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
updateExtensionByName,
|
||||
updateAllUpdatableExtensions,
|
||||
type ExtensionUpdateInfo,
|
||||
} from '../../config/extension.js';
|
||||
updateExtension,
|
||||
} from '../../config/extensions/update.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import {
|
||||
type CommandContext,
|
||||
@@ -55,19 +56,36 @@ async function updateAction(context: CommandContext, args: string) {
|
||||
context.ui.setExtensionsUpdateState,
|
||||
);
|
||||
} else if (names?.length) {
|
||||
const workingDir = context.services.config!.getWorkingDir();
|
||||
const extensions = context.services.config!.getExtensions();
|
||||
for (const name of names) {
|
||||
updateInfos.push(
|
||||
await updateExtensionByName(
|
||||
name,
|
||||
context.services.config!.getWorkingDir(),
|
||||
context.services.config!.getExtensions(),
|
||||
(updateState) => {
|
||||
const newState = new Map(context.ui.extensionsUpdateState);
|
||||
newState.set(name, updateState);
|
||||
context.ui.setExtensionsUpdateState(newState);
|
||||
},
|
||||
),
|
||||
const extension = extensions.find(
|
||||
(extension) => extension.name === name,
|
||||
);
|
||||
if (!extension) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: `Extension ${name} not found.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const updateInfo = await updateExtension(
|
||||
extension,
|
||||
workingDir,
|
||||
context.ui.extensionsUpdateState.get(extension.name) ??
|
||||
ExtensionUpdateState.UNKNOWN,
|
||||
(updateState) => {
|
||||
context.ui.setExtensionsUpdateState((prev) => {
|
||||
const newState = new Map(prev);
|
||||
newState.set(name, updateState);
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
);
|
||||
if (updateInfo) updateInfos.push(updateInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
import type { Content, PartListUnion } from '@google/genai';
|
||||
import type { HistoryItemWithoutId, HistoryItem } from '../types.js';
|
||||
import type { Config, GitService, Logger } from '@google/gemini-cli-core';
|
||||
@@ -63,9 +63,9 @@ export interface CommandContext {
|
||||
setGeminiMdFileCount: (count: number) => void;
|
||||
reloadCommands: () => void;
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateState>;
|
||||
setExtensionsUpdateState: (
|
||||
updateState: Map<string, ExtensionUpdateState>,
|
||||
) => void;
|
||||
setExtensionsUpdateState: Dispatch<
|
||||
SetStateAction<Map<string, ExtensionUpdateState>>
|
||||
>;
|
||||
};
|
||||
// Session-specific data
|
||||
session: {
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useEffect, useState } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
import { type PartListUnion } from '@google/genai';
|
||||
import process from 'node:process';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -45,9 +52,9 @@ interface SlashCommandProcessorActions {
|
||||
quit: (messages: HistoryItem[]) => void;
|
||||
setDebugMessage: (message: string) => void;
|
||||
toggleCorgiMode: () => void;
|
||||
setExtensionsUpdateState: (
|
||||
updateState: Map<string, ExtensionUpdateState>,
|
||||
) => void;
|
||||
setExtensionsUpdateState: Dispatch<
|
||||
SetStateAction<Map<string, ExtensionUpdateState>>
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
211
packages/cli/src/ui/hooks/useExtensionUpdates.test.ts
Normal file
211
packages/cli/src/ui/hooks/useExtensionUpdates.test.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
annotateActiveExtensions,
|
||||
loadExtension,
|
||||
} from '../../config/extension.js';
|
||||
import { createExtension } from '../../test-utils/createExtension.js';
|
||||
import { useExtensionUpdates } from './useExtensionUpdates.js';
|
||||
import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { isWorkspaceTrusted } from '../../config/trustedFolders.js';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { MessageType } from '../types.js';
|
||||
|
||||
const mockGit = {
|
||||
clone: vi.fn(),
|
||||
getRemotes: vi.fn(),
|
||||
fetch: vi.fn(),
|
||||
checkout: vi.fn(),
|
||||
listRemote: vi.fn(),
|
||||
revparse: vi.fn(),
|
||||
// Not a part of the actual API, but we need to use this to do the correct
|
||||
// file system interactions.
|
||||
path: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('simple-git', () => ({
|
||||
simpleGit: vi.fn((path: string) => {
|
||||
mockGit.path.mockReturnValue(path);
|
||||
return mockGit;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const mockedOs = await importOriginal<typeof os>();
|
||||
return {
|
||||
...mockedOs,
|
||||
homedir: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/trustedFolders.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../config/trustedFolders.js')>();
|
||||
return {
|
||||
...actual,
|
||||
isWorkspaceTrusted: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
const mockLogExtensionInstallEvent = vi.fn();
|
||||
const mockLogExtensionUninstallEvent = vi.fn();
|
||||
return {
|
||||
...actual,
|
||||
ClearcutLogger: {
|
||||
getInstance: vi.fn(() => ({
|
||||
logExtensionInstallEvent: mockLogExtensionInstallEvent,
|
||||
logExtensionUninstallEvent: mockLogExtensionUninstallEvent,
|
||||
})),
|
||||
},
|
||||
Config: vi.fn(),
|
||||
ExtensionInstallEvent: vi.fn(),
|
||||
ExtensionUninstallEvent: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('child_process')>();
|
||||
return {
|
||||
...actual,
|
||||
execSync: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockQuestion = vi.hoisted(() => vi.fn());
|
||||
const mockClose = vi.hoisted(() => vi.fn());
|
||||
vi.mock('node:readline', () => ({
|
||||
createInterface: vi.fn(() => ({
|
||||
question: mockQuestion,
|
||||
close: mockClose,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('useExtensionUpdates', () => {
|
||||
let tempHomeDir: string;
|
||||
let userExtensionsDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHomeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||
);
|
||||
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
|
||||
userExtensionsDir = path.join(tempHomeDir, GEMINI_DIR, 'extensions');
|
||||
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||
Object.values(mockGit).forEach((fn) => fn.mockReset());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should check for updates and log a message if an update is available', async () => {
|
||||
const extensions = [
|
||||
{
|
||||
name: 'test-extension',
|
||||
type: 'git',
|
||||
version: '1.0.0',
|
||||
path: '/some/path',
|
||||
isActive: true,
|
||||
installMetadata: {
|
||||
type: 'git',
|
||||
source: 'https://some/repo',
|
||||
autoUpdate: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
const addItem = vi.fn();
|
||||
const cwd = '/test/cwd';
|
||||
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
{
|
||||
name: 'origin',
|
||||
refs: {
|
||||
fetch: 'https://github.com/google/gemini-cli.git',
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockGit.revparse.mockResolvedValue('local-hash');
|
||||
mockGit.listRemote.mockResolvedValue('remote-hash\tHEAD');
|
||||
|
||||
renderHook(() =>
|
||||
useExtensionUpdates(extensions as GeminiCLIExtension[], addItem, cwd),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Extension test-extension has an update available, run "/extensions update test-extension" to install it.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should check for updates and automatically update if autoUpdate is true', async () => {
|
||||
const extensionDir = createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
installMetadata: {
|
||||
source: 'https://some.git/repo',
|
||||
type: 'git',
|
||||
autoUpdate: true,
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension({ extensionDir, workspaceDir: tempHomeDir })!],
|
||||
[],
|
||||
tempHomeDir,
|
||||
)[0];
|
||||
|
||||
const addItem = vi.fn();
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
{
|
||||
name: 'origin',
|
||||
refs: {
|
||||
fetch: 'https://github.com/google/gemini-cli.git',
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockGit.revparse.mockResolvedValue('local-hash');
|
||||
mockGit.listRemote.mockResolvedValue('remote-hash\tHEAD');
|
||||
mockGit.clone.mockImplementation(async (_, destination) => {
|
||||
fs.mkdirSync(path.join(mockGit.path(), destination), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(mockGit.path(), destination, EXTENSIONS_CONFIG_FILENAME),
|
||||
JSON.stringify({ name: 'test-extension', version: '1.1.0' }),
|
||||
);
|
||||
});
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
|
||||
|
||||
renderHook(() => useExtensionUpdates([extension], addItem, tempHomeDir));
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Extension "test-extension" successfully updated: 1.0.0 → 1.1.0.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
88
packages/cli/src/ui/hooks/useExtensionUpdates.ts
Normal file
88
packages/cli/src/ui/hooks/useExtensionUpdates.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import {
|
||||
checkForAllExtensionUpdates,
|
||||
updateExtension,
|
||||
} from '../../config/extensions/update.js';
|
||||
|
||||
export const useExtensionUpdates = (
|
||||
extensions: GeminiCLIExtension[],
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
cwd: string,
|
||||
) => {
|
||||
const [extensionsUpdateState, setExtensionsUpdateState] = useState(
|
||||
new Map<string, ExtensionUpdateState>(),
|
||||
);
|
||||
useMemo(() => {
|
||||
const checkUpdates = async () => {
|
||||
const updateState = await checkForAllExtensionUpdates(
|
||||
extensions,
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState,
|
||||
);
|
||||
for (const extension of extensions) {
|
||||
const prevState = extensionsUpdateState.get(extension.name);
|
||||
const currentState = updateState.get(extension.name);
|
||||
if (
|
||||
prevState === currentState ||
|
||||
currentState !== ExtensionUpdateState.UPDATE_AVAILABLE
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (extension.installMetadata?.autoUpdate) {
|
||||
updateExtension(extension, cwd, currentState, (newState) => {
|
||||
setExtensionsUpdateState((prev) => {
|
||||
const finalState = new Map(prev);
|
||||
finalState.set(extension.name, newState);
|
||||
return finalState;
|
||||
});
|
||||
})
|
||||
.then((result) => {
|
||||
if (!result) return;
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Extension "${extension.name}" successfully updated: ${result.originalVersion} → ${result.updatedVersion}.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`Error updating extension "${extension.name}": ${getErrorMessage(error)}.`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Extension ${extension.name} has an update available, run "/extensions update ${extension.name}" to install it.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
checkUpdates();
|
||||
}, [
|
||||
extensions,
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState,
|
||||
addItem,
|
||||
cwd,
|
||||
]);
|
||||
return {
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState,
|
||||
};
|
||||
};
|
||||
@@ -12,4 +12,5 @@ export enum ExtensionUpdateState {
|
||||
UP_TO_DATE = 'up to date',
|
||||
ERROR = 'error checking for updates',
|
||||
NOT_UPDATABLE = 'not updatable',
|
||||
UNKNOWN = 'unknown',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user