From 74a0c44d59259cede9a29e85125d514ba0c7e257 Mon Sep 17 00:00:00 2001 From: NamesMT Date: Mon, 28 Apr 2025 06:57:39 +0000 Subject: [PATCH] feat: marketplace supports global install --- package-lock.json | 25 ++++++---- package.json | 4 +- src/core/webview/marketplaceMessageHandler.ts | 8 ++-- .../marketplace/MarketplaceManager.ts | 47 +++++++++++++++++-- src/services/marketplace/types.ts | 9 ++++ src/shared/WebviewMessage.ts | 5 +- .../components/MarketplaceItemActionsMenu.tsx | 21 ++++++--- .../src/i18n/locales/en/marketplace.json | 3 +- 8 files changed, 93 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3aafbe16f6..df6fa47c2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "cheerio": "^1.0.0", "chokidar": "^4.0.1", "clone-deep": "^4.0.1", - "config-rocket": "^0.3.15", + "config-rocket": "^0.3.18", "default-shell": "^2.2.0", "delay": "^6.0.0", "diff": "^5.2.0", @@ -54,7 +54,7 @@ "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", "reconnecting-eventsource": "^1.6.4", - "roo-rocket": "^0.3.6", + "roo-rocket": "^0.3.7", "say": "^0.16.0", "serialize-error": "^11.0.3", "simple-git": "^3.27.0", @@ -11073,9 +11073,9 @@ "license": "MIT" }, "node_modules/config-rocket": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/config-rocket/-/config-rocket-0.3.15.tgz", - "integrity": "sha512-LnVOp3mZYeThh9kGwlb9XaCx0jlNgdacm7S+tjo/E05UzfMu57PHpZCTmF2pA9p7mrMUPSrzrvukBed1D4GU4A==", + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/config-rocket/-/config-rocket-0.3.18.tgz", + "integrity": "sha512-jY5EDbI4q0/Qz8Lm4p8JPFw/jJk3Kcg7n+ZZQUNEMcWpbDjw9DIT5K6G1ypuBGycUsXfhDXvycoVkNDEierVDQ==", "license": "MIT", "dependencies": { "c12": "^3.0.3", @@ -18972,14 +18972,13 @@ } }, "node_modules/roo-rocket": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/roo-rocket/-/roo-rocket-0.3.6.tgz", - "integrity": "sha512-xB2WYrBoKLVLu5Usi1oZGEzyf1qJSKSIX+reNZH4ZSHStxCZQN1R556s8Q/B5m6owsBcpYrv8qQNqg1VMrfSAg==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/roo-rocket/-/roo-rocket-0.3.7.tgz", + "integrity": "sha512-DvCHydroTqrJ4q0n8zrhTEcFlHIkJLIVePko30OZvIr5XsPPGbmdPKMPMWiv+s3I91/Oy70VHzTSJUxK4nyVxw==", "license": "Apache-2.0", "dependencies": { "citty": "^0.1.6", "confbox": "^0.2.2", - "config-rocket": "^0.3.13", "consola": "^3.4.2", "defu": "^6.1.4", "hookable": "^5.5.3", @@ -18991,6 +18990,14 @@ }, "funding": { "url": "https://github.com/sponsors/namesmt" + }, + "peerDependencies": { + "config-rocket": "^0.3.16" + }, + "peerDependenciesMeta": { + "config-rocket": { + "optional": false + } } }, "node_modules/router": { diff --git a/package.json b/package.json index 86564a68ea..acd0eb70d0 100644 --- a/package.json +++ b/package.json @@ -447,7 +447,7 @@ "cheerio": "^1.0.0", "chokidar": "^4.0.1", "clone-deep": "^4.0.1", - "config-rocket": "^0.3.15", + "config-rocket": "^0.3.18", "default-shell": "^2.2.0", "delay": "^6.0.0", "diff": "^5.2.0", @@ -474,7 +474,7 @@ "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", "reconnecting-eventsource": "^1.6.4", - "roo-rocket": "^0.3.6", + "roo-rocket": "^0.3.7", "say": "^0.16.0", "serialize-error": "^11.0.3", "simple-git": "^3.27.0", diff --git a/src/core/webview/marketplaceMessageHandler.ts b/src/core/webview/marketplaceMessageHandler.ts index be73b59945..17ec326058 100644 --- a/src/core/webview/marketplaceMessageHandler.ts +++ b/src/core/webview/marketplaceMessageHandler.ts @@ -218,16 +218,16 @@ export async function handleMarketplaceMessages( } case "installMarketplaceItem": { - if (message.marketplaceItem) { + if (message.mpItem) { try { - await marketplaceManager.installMarketplaceItem(message.marketplaceItem) + await marketplaceManager.installMarketplaceItem(message.mpItem, message.mpInstallOptions) } catch (error) { vscode.window.showErrorMessage( - `Failed to install item "${message.marketplaceItem.name}": ${error instanceof Error ? error.message : String(error)}`, + `Failed to install item "${message.mpItem.name}":\n${error instanceof Error ? error.message : String(error)}`, ) } } else { - console.error("Marketplace: installMarketplaceItem called without `marketplaceItem`") + console.error("Marketplace: installMarketplaceItem called without `mpItem`") } return true } diff --git a/src/services/marketplace/MarketplaceManager.ts b/src/services/marketplace/MarketplaceManager.ts index 897e7c6e47..bdbcaff6a9 100644 --- a/src/services/marketplace/MarketplaceManager.ts +++ b/src/services/marketplace/MarketplaceManager.ts @@ -9,9 +9,11 @@ import { ComponentType, ComponentMetadata, LocalizationOptions, + InstallMarketplaceItemOptions, } from "./types" import { validateSource, validateSources } from "../../shared/MarketplaceValidation" import { getUserLocale } from "./utils" +import { GlobalFileNames } from "src/shared/globalFileNames" /** * Service for managing marketplace data @@ -565,26 +567,61 @@ export class MarketplaceManager { } } - async installMarketplaceItem(item: MarketplaceItem) { + async installMarketplaceItem(item: MarketplaceItem, options?: InstallMarketplaceItemOptions) { + const { + target = 'project' + } = options || {} + if (!vscode.workspace.workspaceFolders?.length) return vscode.window.showErrorMessage("Cannot load current workspace folder") if (!item.binaryUrl || !item.binaryHash) return vscode.window.showErrorMessage("Item does not have a binary URL or hash") - const workspacePath = vscode.workspace.workspaceFolders[0].uri.fsPath - const { hookable } = await import("roo-rocket") + const cwd = target === 'global' + ? await this.ensureSettingsDirectoryExists() + : vscode.workspace.workspaceFolders[0].uri.fsPath + const { createHookable } = await import("roo-rocket") const { unpackFromUrl } = await import("config-rocket/cli") + // Create a custom hookable instance to support global installations + // Currently, we only supports `.roomodes` and `.roo/mcp.json` files for global installation. + const customHookable = createHookable() + customHookable.hook('onExtract', ({ unzipped }) => { + const allowedFiles = new Set(['.roo/mcp.json', '.roomodes']) + console.log({unzipped}) + for (const key in unzipped) { + if (!allowedFiles.has(key)) + throw new Error('Unsupported file for global installation: ' + key) + } + }) + customHookable.hook('onFileOutput', (state) => { + if (target === 'global') { + if (state.filePath.endsWith('/.roo/mcp.json')) + state.filePath = state.filePath.replace('.roo/mcp.json', GlobalFileNames.mcpSettings) + else if (state.filePath.endsWith('/.roomodes')) + state.filePath = state.filePath.replace('.roomodes', GlobalFileNames.customModes) + } + }) + vscode.window.showInformationMessage(`Installing item: "${item.name}"`) await unpackFromUrl(item.binaryUrl, { - hookable, + hookable: customHookable, nonAssemblyBehavior: true, sha256: item.binaryHash, - cwd: workspacePath + cwd }) vscode.window.showInformationMessage(`Item "${item.name}" installed successfully`) return true } + + /** + * Copied from `src/core/config/CustomModesManager.ts`, if in the future we add ClineProvider ref to this class, we can remove this and use the one from there. + */ + private async ensureSettingsDirectoryExists(): Promise { + const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings") + await fs.mkdir(settingsDir, { recursive: true }) + return settingsDir + } } diff --git a/src/services/marketplace/types.ts b/src/services/marketplace/types.ts index bfcde18d36..eab0b6caa8 100644 --- a/src/services/marketplace/types.ts +++ b/src/services/marketplace/types.ts @@ -130,3 +130,12 @@ export interface LocalizationOptions { userLocale: string fallbackLocale: string } + +export interface InstallMarketplaceItemOptions { + /** + * Specify the installation target + * + * @default 'project' + */ + target?: 'global' | 'project' +} diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index ab1fb195e3..57c6372cff 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { ApiConfiguration } from "./api" import { Mode, PromptComponent, ModeConfig } from "./modes" -import { MarketplaceItem, MarketplaceSource } from "../services/marketplace/types" +import { InstallMarketplaceItemOptions, MarketplaceItem, MarketplaceSource } from "../services/marketplace/types" export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" @@ -162,7 +162,8 @@ export interface WebviewMessage { sources?: MarketplaceSource[] filters?: { type?: string; search?: string; tags?: string[] } url?: string // For openExternal - marketplaceItem?: MarketplaceItem + mpItem?: MarketplaceItem + mpInstallOptions?: InstallMarketplaceItemOptions hasSystemPromptOverride?: boolean historyPreviewCollapsed?: boolean } diff --git a/webview-ui/src/components/marketplace/components/MarketplaceItemActionsMenu.tsx b/webview-ui/src/components/marketplace/components/MarketplaceItemActionsMenu.tsx index cf4c298520..c8b0708728 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceItemActionsMenu.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceItemActionsMenu.tsx @@ -7,7 +7,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { MoreVertical, ExternalLink, Download } from "lucide-react" -import { MarketplaceItem } from "../../../../../src/services/marketplace/types" +import { InstallMarketplaceItemOptions, MarketplaceItem } from "../../../../../src/services/marketplace/types" import { vscode } from "@/utils/vscode" import { useAppTranslation } from "@/i18n/TranslationContext" @@ -22,10 +22,11 @@ export const MarketplaceItemActionsMenu: React.FC { const { t } = useAppTranslation() - const handleInstall = () => { + const handleInstall = (options?: InstallMarketplaceItemOptions) => { vscode.postMessage({ type: "installMarketplaceItem", - marketplaceItem: item + mpItem: item, + mpInstallOptions: options }) } @@ -49,11 +50,19 @@ export const MarketplaceItemActionsMenu: React.FC{t("marketplace:items.card.viewSource")} - {/* Install Item */} + {/* Install (Project) */} {showInstallButton && ( - + handleInstall({target: 'project'})}> - {t("marketplace:items.card.install")} + {t("marketplace:items.card.installProject")} + + )} + + {/* Install (Global) */} + {showInstallButton && ( + handleInstall({target: 'global'})}> + + {t("marketplace:items.card.installGlobal")} )} diff --git a/webview-ui/src/i18n/locales/en/marketplace.json b/webview-ui/src/i18n/locales/en/marketplace.json index 3c69e2f69b..deabe9b44a 100644 --- a/webview-ui/src/i18n/locales/en/marketplace.json +++ b/webview-ui/src/i18n/locales/en/marketplace.json @@ -55,7 +55,8 @@ "card": { "by": "by {{author}}", "from": "from {{source}}", - "install": "Install", + "installProject": "Install (Project)", + "installGlobal": "Install (Global)", "viewSource": "View", "viewOnSource": "View on {{source}}" }