mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Add prompt to migrate workspace extensions (#7065)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -15,7 +15,9 @@ import {
|
|||||||
disableExtension,
|
disableExtension,
|
||||||
enableExtension,
|
enableExtension,
|
||||||
installExtension,
|
installExtension,
|
||||||
|
loadExtension,
|
||||||
loadExtensions,
|
loadExtensions,
|
||||||
|
performWorkspaceExtensionMigration,
|
||||||
uninstallExtension,
|
uninstallExtension,
|
||||||
updateExtension,
|
updateExtension,
|
||||||
} from './extension.js';
|
} from './extension.js';
|
||||||
@@ -46,6 +48,7 @@ vi.mock('child_process', async (importOriginal) => {
|
|||||||
execSync: vi.fn(),
|
execSync: vi.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
|
const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
|
||||||
|
|
||||||
describe('loadExtensions', () => {
|
describe('loadExtensions', () => {
|
||||||
@@ -430,6 +433,80 @@ describe('uninstallExtension', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('performWorkspaceExtensionMigration', () => {
|
||||||
|
let tempWorkspaceDir: string;
|
||||||
|
let tempHomeDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempWorkspaceDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'gemini-cli-test-workspace-'),
|
||||||
|
);
|
||||||
|
tempHomeDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||||
|
);
|
||||||
|
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
|
||||||
|
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should install the extensions in the user directory', async () => {
|
||||||
|
const workspaceExtensionsDir = path.join(
|
||||||
|
tempWorkspaceDir,
|
||||||
|
EXTENSIONS_DIRECTORY_NAME,
|
||||||
|
);
|
||||||
|
fs.mkdirSync(workspaceExtensionsDir, { recursive: true });
|
||||||
|
const ext1Path = createExtension(workspaceExtensionsDir, 'ext1', '1.0.0');
|
||||||
|
const ext2Path = createExtension(workspaceExtensionsDir, 'ext2', '1.0.0');
|
||||||
|
const extensionsToMigrate = [
|
||||||
|
loadExtension(ext1Path)!,
|
||||||
|
loadExtension(ext2Path)!,
|
||||||
|
];
|
||||||
|
const failed =
|
||||||
|
await performWorkspaceExtensionMigration(extensionsToMigrate);
|
||||||
|
|
||||||
|
expect(failed).toEqual([]);
|
||||||
|
|
||||||
|
const userExtensionsDir = path.join(tempHomeDir, '.gemini', 'extensions');
|
||||||
|
const userExt1Path = path.join(userExtensionsDir, 'ext1');
|
||||||
|
const extensions = loadExtensions(tempWorkspaceDir);
|
||||||
|
|
||||||
|
expect(extensions).toHaveLength(2);
|
||||||
|
const metadataPath = path.join(userExt1Path, INSTALL_METADATA_FILENAME);
|
||||||
|
expect(fs.existsSync(metadataPath)).toBe(true);
|
||||||
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
||||||
|
expect(metadata).toEqual({
|
||||||
|
source: ext1Path,
|
||||||
|
type: 'local',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the names of failed installations', async () => {
|
||||||
|
const workspaceExtensionsDir = path.join(
|
||||||
|
tempWorkspaceDir,
|
||||||
|
EXTENSIONS_DIRECTORY_NAME,
|
||||||
|
);
|
||||||
|
fs.mkdirSync(workspaceExtensionsDir, { recursive: true });
|
||||||
|
|
||||||
|
const ext1Path = createExtension(workspaceExtensionsDir, 'ext1', '1.0.0');
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
loadExtension(ext1Path)!,
|
||||||
|
{
|
||||||
|
path: '/ext/path/1',
|
||||||
|
config: { name: 'ext2', version: '1.0.0' },
|
||||||
|
contextFiles: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const failed = await performWorkspaceExtensionMigration(extensions);
|
||||||
|
expect(failed).toEqual(['ext2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function createExtension(
|
function createExtension(
|
||||||
extensionsDir: string,
|
extensionsDir: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
|||||||
@@ -77,15 +77,51 @@ export class ExtensionStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getWorkspaceExtensions(workspaceDir: string): Extension[] {
|
||||||
|
return loadExtensionsFromDir(workspaceDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyExtension(
|
||||||
|
source: string,
|
||||||
|
destination: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await fs.promises.cp(source, destination, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function performWorkspaceExtensionMigration(
|
||||||
|
extensions: Extension[],
|
||||||
|
): Promise<string[]> {
|
||||||
|
const failedInstallNames: string[] = [];
|
||||||
|
|
||||||
|
for (const extension of extensions) {
|
||||||
|
try {
|
||||||
|
const installMetadata: ExtensionInstallMetadata = {
|
||||||
|
source: extension.path,
|
||||||
|
type: 'local',
|
||||||
|
};
|
||||||
|
await installExtension(installMetadata);
|
||||||
|
} catch (_) {
|
||||||
|
failedInstallNames.push(extension.config.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failedInstallNames;
|
||||||
|
}
|
||||||
|
|
||||||
export function loadExtensions(workspaceDir: string): Extension[] {
|
export function loadExtensions(workspaceDir: string): Extension[] {
|
||||||
const allExtensions = [
|
const settings = loadSettings(workspaceDir).merged;
|
||||||
...loadExtensionsFromDir(workspaceDir),
|
const disabledExtensions = settings.extensions?.disabled ?? [];
|
||||||
...loadExtensionsFromDir(os.homedir()),
|
const allExtensions = [...loadUserExtensions()];
|
||||||
];
|
|
||||||
|
if (!settings.extensionManagement) {
|
||||||
|
allExtensions.push(...getWorkspaceExtensions(workspaceDir));
|
||||||
|
}
|
||||||
|
|
||||||
const uniqueExtensions = new Map<string, Extension>();
|
const uniqueExtensions = new Map<string, Extension>();
|
||||||
for (const extension of allExtensions) {
|
for (const extension of allExtensions) {
|
||||||
if (!uniqueExtensions.has(extension.config.name)) {
|
if (
|
||||||
|
!uniqueExtensions.has(extension.config.name) &&
|
||||||
|
!disabledExtensions.includes(extension.config.name)
|
||||||
|
) {
|
||||||
uniqueExtensions.set(extension.config.name, extension);
|
uniqueExtensions.set(extension.config.name, extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,18 +319,6 @@ async function cloneFromGit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies an extension from a source to a destination path.
|
|
||||||
* @param source The source path of the extension.
|
|
||||||
* @param destination The destination path to copy the extension to.
|
|
||||||
*/
|
|
||||||
async function copyExtension(
|
|
||||||
source: string,
|
|
||||||
destination: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await fs.promises.cp(source, destination, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installExtension(
|
export async function installExtension(
|
||||||
installMetadata: ExtensionInstallMetadata,
|
installMetadata: ExtensionInstallMetadata,
|
||||||
cwd: string = process.cwd(),
|
cwd: string = process.cwd(),
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
mcpServers: {},
|
mcpServers: {},
|
||||||
includeDirectories: [],
|
includeDirectories: [],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(settings.errors.length).toBe(0);
|
expect(settings.errors.length).toBe(0);
|
||||||
});
|
});
|
||||||
@@ -157,6 +161,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
mcpServers: {},
|
mcpServers: {},
|
||||||
includeDirectories: [],
|
includeDirectories: [],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,6 +200,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
mcpServers: {},
|
mcpServers: {},
|
||||||
includeDirectories: [],
|
includeDirectories: [],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,6 +237,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
mcpServers: {},
|
mcpServers: {},
|
||||||
includeDirectories: [],
|
includeDirectories: [],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -264,6 +280,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
mcpServers: {},
|
mcpServers: {},
|
||||||
includeDirectories: [],
|
includeDirectories: [],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -315,6 +335,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
mcpServers: {},
|
mcpServers: {},
|
||||||
includeDirectories: [],
|
includeDirectories: [],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -377,6 +401,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
'/system/dir',
|
'/system/dir',
|
||||||
],
|
],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -537,6 +565,7 @@ describe('Settings Loading and Merging', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||||
|
|
||||||
expect(settings.user.settings.excludedProjectEnvVars).toEqual([
|
expect(settings.user.settings.excludedProjectEnvVars).toEqual([
|
||||||
'DEBUG',
|
'DEBUG',
|
||||||
'NODE_ENV',
|
'NODE_ENV',
|
||||||
@@ -944,6 +973,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
mcpServers: {},
|
mcpServers: {},
|
||||||
includeDirectories: [],
|
includeDirectories: [],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that error objects are populated in settings.errors
|
// Check that error objects are populated in settings.errors
|
||||||
@@ -1316,6 +1349,10 @@ describe('Settings Loading and Merging', () => {
|
|||||||
mcpServers: {},
|
mcpServers: {},
|
||||||
includeDirectories: [],
|
includeDirectories: [],
|
||||||
chatCompression: {},
|
chatCompression: {},
|
||||||
|
extensions: {
|
||||||
|
disabled: [],
|
||||||
|
workspacesWithMigrationNudge: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -133,6 +133,28 @@ function mergeSettings(
|
|||||||
...(safeWorkspace.chatCompression || {}),
|
...(safeWorkspace.chatCompression || {}),
|
||||||
...(system.chatCompression || {}),
|
...(system.chatCompression || {}),
|
||||||
},
|
},
|
||||||
|
extensions: {
|
||||||
|
...(systemDefaults.extensions || {}),
|
||||||
|
...(user.extensions || {}),
|
||||||
|
...(safeWorkspace.extensions || {}),
|
||||||
|
...(system.extensions || {}),
|
||||||
|
disabled: [
|
||||||
|
...new Set([
|
||||||
|
...(systemDefaults.extensions?.disabled || []),
|
||||||
|
...(user.extensions?.disabled || []),
|
||||||
|
...(safeWorkspace.extensions?.disabled || []),
|
||||||
|
...(system.extensions?.disabled || []),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
workspacesWithMigrationNudge: [
|
||||||
|
...new Set([
|
||||||
|
...(systemDefaults.extensions?.workspacesWithMigrationNudge || []),
|
||||||
|
...(user.extensions?.workspacesWithMigrationNudge || []),
|
||||||
|
...(safeWorkspace.extensions?.workspacesWithMigrationNudge || []),
|
||||||
|
...(system.extensions?.workspacesWithMigrationNudge || []),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -572,6 +572,16 @@ export const SETTINGS_SCHEMA = {
|
|||||||
description: 'List of disabled extensions.',
|
description: 'List of disabled extensions.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
|
workspacesWithMigrationNudge: {
|
||||||
|
type: 'array',
|
||||||
|
label: 'Workspaces with Migration Nudge',
|
||||||
|
category: 'Extensions',
|
||||||
|
requiresRestart: false,
|
||||||
|
default: [] as string[],
|
||||||
|
description:
|
||||||
|
'List of workspaces for which the migration nudge has been shown.',
|
||||||
|
showInDialog: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
skipNextSpeakerCheck: {
|
skipNextSpeakerCheck: {
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ import { SettingsDialog } from './components/SettingsDialog.js';
|
|||||||
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
||||||
import { appEvents, AppEvent } from '../utils/events.js';
|
import { appEvents, AppEvent } from '../utils/events.js';
|
||||||
import { isNarrowWidth } from './utils/isNarrowWidth.js';
|
import { isNarrowWidth } from './utils/isNarrowWidth.js';
|
||||||
|
import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
|
||||||
|
import { WorkspaceMigrationDialog } from './components/WorkspaceMigrationDialog.js';
|
||||||
|
|
||||||
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
||||||
// Maximum number of queued messages to display in UI to prevent performance issues
|
// Maximum number of queued messages to display in UI to prevent performance issues
|
||||||
@@ -223,6 +225,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
>();
|
>();
|
||||||
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||||
|
const {
|
||||||
|
showWorkspaceMigrationDialog,
|
||||||
|
workspaceExtensions,
|
||||||
|
onWorkspaceMigrationDialogOpen,
|
||||||
|
onWorkspaceMigrationDialogClose,
|
||||||
|
} = useWorkspaceMigration(settings);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = ideContext.subscribeToIdeContext(setIdeContextState);
|
const unsubscribe = ideContext.subscribeToIdeContext(setIdeContextState);
|
||||||
@@ -1018,8 +1026,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{showWorkspaceMigrationDialog ? (
|
||||||
{shouldShowIdePrompt && currentIDE ? (
|
<WorkspaceMigrationDialog
|
||||||
|
workspaceExtensions={workspaceExtensions}
|
||||||
|
onOpen={onWorkspaceMigrationDialogOpen}
|
||||||
|
onClose={onWorkspaceMigrationDialogClose}
|
||||||
|
/>
|
||||||
|
) : shouldShowIdePrompt && currentIDE ? (
|
||||||
<IdeIntegrationNudge
|
<IdeIntegrationNudge
|
||||||
ide={currentIDE}
|
ide={currentIDE}
|
||||||
onComplete={handleIdePromptComplete}
|
onComplete={handleIdePromptComplete}
|
||||||
@@ -1167,8 +1180,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
<Box paddingLeft={2}>
|
<Box paddingLeft={2}>
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
... (+
|
... (+
|
||||||
{messageQueue.length -
|
{messageQueue.length - MAX_DISPLAYED_QUEUED_MESSAGES}
|
||||||
MAX_DISPLAYED_QUEUED_MESSAGES}{' '}
|
|
||||||
more)
|
more)
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
108
packages/cli/src/ui/components/WorkspaceMigrationDialog.tsx
Normal file
108
packages/cli/src/ui/components/WorkspaceMigrationDialog.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import {
|
||||||
|
type Extension,
|
||||||
|
performWorkspaceExtensionMigration,
|
||||||
|
} from '../../config/extension.js';
|
||||||
|
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||||
|
import { Colors } from '../colors.js';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function WorkspaceMigrationDialog(props: {
|
||||||
|
workspaceExtensions: Extension[];
|
||||||
|
onOpen: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const { workspaceExtensions, onOpen, onClose } = props;
|
||||||
|
const [migrationComplete, setMigrationComplete] = useState(false);
|
||||||
|
const [failedExtensions, setFailedExtensions] = useState<string[]>([]);
|
||||||
|
onOpen();
|
||||||
|
const onMigrate = async () => {
|
||||||
|
const failed =
|
||||||
|
await performWorkspaceExtensionMigration(workspaceExtensions);
|
||||||
|
setFailedExtensions(failed);
|
||||||
|
setMigrationComplete(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useInput((input) => {
|
||||||
|
if (migrationComplete && input === 'q') {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (migrationComplete) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor={Colors.Gray}
|
||||||
|
padding={1}
|
||||||
|
>
|
||||||
|
{failedExtensions.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
The following extensions failed to migrate. Please try installing
|
||||||
|
them manually. To see other changes, Gemini CLI must be restarted.
|
||||||
|
Press {"'q'"} to quit.
|
||||||
|
</Text>
|
||||||
|
<Box flexDirection="column" marginTop={1} marginLeft={2}>
|
||||||
|
{failedExtensions.map((failed) => (
|
||||||
|
<Text key={failed}>- {failed}</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
Migration complete. To see changes, Gemini CLI must be restarted.
|
||||||
|
Press {"'q'"} to quit.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor={Colors.Gray}
|
||||||
|
padding={1}
|
||||||
|
>
|
||||||
|
<Text bold>Workspace-level extensions are deprecated{'\n'}</Text>
|
||||||
|
<Text>Would you like to install them at the user level?</Text>
|
||||||
|
<Text>
|
||||||
|
The extension definition will remain in your workspace directory.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
If you opt to skip, you can install them manually using the extensions
|
||||||
|
install command.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box flexDirection="column" marginTop={1} marginLeft={2}>
|
||||||
|
{workspaceExtensions.map((extension) => (
|
||||||
|
<Text key={extension.config.name}>- {extension.config.name}</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<RadioButtonSelect
|
||||||
|
items={[
|
||||||
|
{ label: 'Install all', value: 'migrate' },
|
||||||
|
{ label: 'Skip', value: 'skip' },
|
||||||
|
]}
|
||||||
|
onSelect={(value: string) => {
|
||||||
|
if (value === 'migrate') {
|
||||||
|
onMigrate();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
packages/cli/src/ui/hooks/useWorkspaceMigration.ts
Normal file
66
packages/cli/src/ui/hooks/useWorkspaceMigration.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
type Extension,
|
||||||
|
getWorkspaceExtensions,
|
||||||
|
} from '../../config/extension.js';
|
||||||
|
import { type LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
|
export function useWorkspaceMigration(settings: LoadedSettings) {
|
||||||
|
const [showWorkspaceMigrationDialog, setShowWorkspaceMigrationDialog] =
|
||||||
|
useState(false);
|
||||||
|
const [workspaceExtensions, setWorkspaceExtensions] = useState<Extension[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!settings.merged.extensionManagement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const extensions = getWorkspaceExtensions(cwd);
|
||||||
|
if (
|
||||||
|
extensions.length > 0 &&
|
||||||
|
!settings.merged.extensions?.workspacesWithMigrationNudge?.includes(cwd)
|
||||||
|
) {
|
||||||
|
setWorkspaceExtensions(extensions);
|
||||||
|
setShowWorkspaceMigrationDialog(true);
|
||||||
|
console.log(settings.merged.extensions);
|
||||||
|
}
|
||||||
|
}, [settings.merged.extensions, settings.merged.extensionManagement]);
|
||||||
|
|
||||||
|
const onWorkspaceMigrationDialogOpen = () => {
|
||||||
|
const userSettings = settings.forScope(SettingScope.User);
|
||||||
|
const extensionSettings = userSettings.settings.extensions || {
|
||||||
|
disabled: [],
|
||||||
|
};
|
||||||
|
const workspacesWithMigrationNudge =
|
||||||
|
extensionSettings.workspacesWithMigrationNudge || [];
|
||||||
|
|
||||||
|
const cwd = process.cwd();
|
||||||
|
if (!workspacesWithMigrationNudge.includes(cwd)) {
|
||||||
|
workspacesWithMigrationNudge.push(cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionSettings.workspacesWithMigrationNudge =
|
||||||
|
workspacesWithMigrationNudge;
|
||||||
|
settings.setValue(SettingScope.User, 'extensions', extensionSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWorkspaceMigrationDialogClose = () => {
|
||||||
|
setShowWorkspaceMigrationDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
showWorkspaceMigrationDialog,
|
||||||
|
workspaceExtensions,
|
||||||
|
onWorkspaceMigrationDialogOpen,
|
||||||
|
onWorkspaceMigrationDialogClose,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user