feat: marketplace supports global install

This commit is contained in:
NamesMT 2025-04-28 06:57:39 +00:00
parent 04d7360da9
commit 74a0c44d59
8 changed files with 93 additions and 29 deletions

25
package-lock.json generated
View file

@ -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": {

View file

@ -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",

View file

@ -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
}

View file

@ -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<string> {
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
}

View file

@ -130,3 +130,12 @@ export interface LocalizationOptions {
userLocale: string
fallbackLocale: string
}
export interface InstallMarketplaceItemOptions {
/**
* Specify the installation target
*
* @default 'project'
*/
target?: 'global' | 'project'
}

View file

@ -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
}

View file

@ -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<MarketplaceItemActionsMenuProp
}) => {
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<MarketplaceItemActionsMenuProp
<span>{t("marketplace:items.card.viewSource")}</span>
</DropdownMenuItem>
{/* Install Item */}
{/* Install (Project) */}
{showInstallButton && (
<DropdownMenuItem onClick={handleInstall}>
<DropdownMenuItem onClick={() => handleInstall({target: 'project'})}>
<Download className="mr-2 h-4 w-4" />
<span>{t("marketplace:items.card.install")}</span>
<span>{t("marketplace:items.card.installProject")}</span>
</DropdownMenuItem>
)}
{/* Install (Global) */}
{showInstallButton && (
<DropdownMenuItem onClick={() => handleInstall({target: 'global'})}>
<Download className="mr-2 h-4 w-4" />
<span>{t("marketplace:items.card.installGlobal")}</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>

View file

@ -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}}"
}