diff --git a/package-lock.json b/package-lock.json index c0c6f89e94..7adc2a2837 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "cheerio": "^1.0.0", "chokidar": "^4.0.1", "clone-deep": "^4.0.1", + "config-rocket": "^0.3.20", "default-shell": "^2.2.0", "delay": "^6.0.0", "diff": "^5.2.0", @@ -53,6 +54,7 @@ "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", "reconnecting-eventsource": "^1.6.4", + "roo-rocket": "^0.3.12", "say": "^0.16.0", "serialize-error": "^11.0.3", "simple-git": "^3.27.0", @@ -10546,6 +10548,34 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.0.3.tgz", + "integrity": "sha512-uC3MacKBb0Z15o5QWCHvHWj5Zv34pGQj9P+iXKSpTuSGFS0KKhUWf4t9AJ+gWjYOdmWCPEGpEzm8sS0iqbpo1w==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.4.7", + "exsolve": "^1.0.4", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.1.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -10775,6 +10805,15 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cjs-module-lexer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", @@ -11027,11 +11066,44 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, + "node_modules/config-rocket": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/config-rocket/-/config-rocket-0.3.20.tgz", + "integrity": "sha512-Zim3CGz4Psu2OvLU3U7dGhtj+Nzr+fAh51S2/YUO6vAjQUQa1Ll06+I53W1xMQFI4TcLBYKSpJV/jy+YfUZnHg==", + "license": "MIT", + "dependencies": { + "c12": "^3.0.3", + "citty": "^0.1.6", + "confbox": "^0.2.2", + "consola": "^3.4.2", + "defu": "^6.1.4", + "fflate": "^0.8.2", + "hookable": "^5.5.3", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinyglobby": "^0.2.13" + }, + "bin": { + "config-rocket": "dist/cli-entry.mjs", + "rocket-zip": "dist/cli-rocket-zip-entry.mjs" + }, + "engines": { + "node": ">=20.13.1" + }, + "funding": { + "url": "https://github.com/sponsors/namesmt" + } + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -11568,6 +11640,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -11609,6 +11687,12 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -11769,7 +11853,6 @@ "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -12702,6 +12785,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, + "node_modules/exsolve": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", + "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -12880,6 +12969,12 @@ "pend": "~1.2.0" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -13468,6 +13563,23 @@ "node": ">= 14" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -13696,6 +13808,12 @@ "node": ">= 0.4" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -15466,7 +15584,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -16920,6 +17037,12 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", + "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -17230,6 +17353,25 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nypm": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", + "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^2.0.0", + "tinyexec": "^0.3.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -17287,6 +17429,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -17811,6 +17959,12 @@ "node": ">=16" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/pdf-parse": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz", @@ -17836,6 +17990,12 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -17957,6 +18117,17 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", + "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.1", + "exsolve": "^1.0.1", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -18417,6 +18588,16 @@ "node": ">=0.10.0" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -18790,6 +18971,35 @@ "fsevents": "~2.3.2" } }, + "node_modules/roo-rocket": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/roo-rocket/-/roo-rocket-0.3.12.tgz", + "integrity": "sha512-oy9pXRcys5VS3OhtzScve97UJkQ00bjacPpVJQ8GeIlDsMGfQl1ega2X95/rRyWFGsFT1jr/ri4q4AUs2BNzOg==", + "license": "Apache-2.0", + "dependencies": { + "citty": "^0.1.6", + "confbox": "^0.2.2", + "consola": "^3.4.2", + "defu": "^6.1.4", + "hookable": "^5.5.3", + "pathe": "^2.0.3", + "std-env": "^3.9.0" + }, + "bin": { + "roo-rocket": "dist/cli-entry.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/namesmt" + }, + "peerDependencies": { + "config-rocket": "^0.3.19" + }, + "peerDependenciesMeta": { + "config-rocket": { + "optional": false + } + } + }, "node_modules/router": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", @@ -19432,6 +19642,12 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -20057,17 +20273,15 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", - "dev": true, + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "license": "MIT", "dependencies": { - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { @@ -20078,10 +20292,9 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", - "dev": true, + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -20096,7 +20309,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 1efbdeb357..a7a58bdfa4 100644 --- a/package.json +++ b/package.json @@ -447,6 +447,7 @@ "cheerio": "^1.0.0", "chokidar": "^4.0.1", "clone-deep": "^4.0.1", + "config-rocket": "^0.3.20", "default-shell": "^2.2.0", "delay": "^6.0.0", "diff": "^5.2.0", @@ -473,6 +474,7 @@ "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", "reconnecting-eventsource": "^1.6.4", + "roo-rocket": "^0.3.13", "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 5b389941ef..17ec326058 100644 --- a/src/core/webview/marketplaceMessageHandler.ts +++ b/src/core/webview/marketplaceMessageHandler.ts @@ -217,6 +217,21 @@ export async function handleMarketplaceMessages( return true } + case "installMarketplaceItem": { + if (message.mpItem) { + try { + await marketplaceManager.installMarketplaceItem(message.mpItem, message.mpInstallOptions) + } catch (error) { + vscode.window.showErrorMessage( + `Failed to install item "${message.mpItem.name}":\n${error instanceof Error ? error.message : String(error)}`, + ) + } + } else { + console.error("Marketplace: installMarketplaceItem called without `mpItem`") + } + return true + } + case "refreshMarketplaceSource": { if (message.url) { try { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 4474607b8a..b4523f295f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1282,6 +1282,7 @@ export const webviewMessageHandler = async ( marketplaceManager && (message.type === "marketplaceSources" || message.type === "openExternal" || + message.type === "installMarketplaceItem" || message.type === "refreshMarketplaceSource" || message.type === "filterMarketplaceItems") ) { diff --git a/src/services/marketplace/MarketplaceManager.ts b/src/services/marketplace/MarketplaceManager.ts index 8ff476e81e..c34ad6d67c 100644 --- a/src/services/marketplace/MarketplaceManager.ts +++ b/src/services/marketplace/MarketplaceManager.ts @@ -9,9 +9,15 @@ import { ComponentType, ComponentMetadata, LocalizationOptions, + InstallMarketplaceItemOptions, } from "./types" import { validateSource, validateSources } from "../../shared/MarketplaceValidation" import { getUserLocale } from "./utils" +import { GlobalFileNames } from "src/shared/globalFileNames" +import { TerminalRegistry } from "src/integrations/terminal/TerminalRegistry" +import { assertsMpContext, createHookable, MarketplaceContext, registerMarketplaceHooks } from "roo-rocket" +import { unpackFromUint8 } from "config-rocket/cli" +import { uint8IsConfigPackWithParameters } from 'config-rocket' /** * Service for managing marketplace data @@ -564,4 +570,107 @@ export class MarketplaceManager { } } } + + async installMarketplaceItem(item: MarketplaceItem, options?: InstallMarketplaceItemOptions) { + const { + target = 'project' + } = options || {} + + vscode.window.showInformationMessage(`Installing item: "${item.name}"`) + + if (target === 'project' && !vscode.workspace.workspaceFolders?.length) + return vscode.window.showErrorMessage("Cannot load current workspace folder") + + const cwd = target === 'project' + ? vscode.workspace.workspaceFolders![0].uri.fsPath + : await this.ensureSettingsDirectoryExists() + + if (!item.binaryUrl || !item.binaryHash) + return vscode.window.showErrorMessage("Item does not have a binary URL or hash") + + // Creates `mpContext` to delegate context to `roo-rocket` + const mpContext = (target === 'project' + ? { target } + : { + target, + globalFileNames: { + mcp: GlobalFileNames.mcpSettings, + mode: GlobalFileNames.customModes, + } + } + ) satisfies MarketplaceContext + assertsMpContext(mpContext) + + const binaryUint8 = await fetchBinary(item.binaryUrl) + + // Install via CLI if binary is a configurable pack. + // TODO: think of a way to send the binary to the npx process + if (await uint8IsConfigPackWithParameters(binaryUint8)) { + vscode.window.showInformationMessage(`"${item.name}" is configurable, invoking interactive CLI...`) + + let pResult: string[] = [] + let pExitCode: number | undefined + // We don't want to create a new terminal at the global dir, so I'm not using cwd here + const terminalClass = await TerminalRegistry.getOrCreateTerminal(vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath ?? '', false, `IMI-${item.name}`) + terminalClass.terminal.show() + await terminalClass.runCommand(`npx --yes roo-rocket@latest --mp="${JSON.stringify(mpContext).replaceAll(/"/g, '\\"')}" --cwd="${cwd}" --sha256="${item.binaryHash}" --url="${item.binaryUrl}"`, { + onLine: (line) => { + pResult.push(line) + }, + onShellExecutionComplete: (details) => { + pExitCode = details.exitCode + }, + }) + + if (pExitCode === 0) + vscode.window.showInformationMessage(`"${item.name}" CLI reported success!`) + else { + console.error(pResult) + // Revert so error search is potentially faster + pResult.reverse() + // Search for error line in the result + const errorLine = ( + pResult.find(line => /^((\r)?\n)+ ERROR /.test(line)) ?? // Prefer formatting error + pResult.find(line => /error/i.test(line)) ?? // General error + 'N/A' + ) + return vscode.window.showErrorMessage(`"${item.name}" CLI reported error: (${pExitCode}): ${errorLine}`) + } + } + // Fast install for non-configurable packs. + else { + // Create a custom hookable instance to support global installations + const customHookable = createHookable() + registerMarketplaceHooks(customHookable, mpContext) + + vscode.window.showInformationMessage(`"${item.name}" is non-configurable, fast install...`) + await unpackFromUint8(binaryUint8, { + hookable: customHookable, + nonAssemblyBehavior: true, + sha256: item.binaryHash, + cwd + }) + vscode.window.showInformationMessage(`"${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 + } +} + +async function fetchBinary(url: string) { + const res = await fetch(url) + if (!res.ok) + throw new Error(`Failed to download binary from ${url}`) + + return new Uint8Array(await res.arrayBuffer()) } diff --git a/src/services/marketplace/MetadataScanner.ts b/src/services/marketplace/MetadataScanner.ts index af2495efcd..241bdd0746 100644 --- a/src/services/marketplace/MetadataScanner.ts +++ b/src/services/marketplace/MetadataScanner.ts @@ -253,6 +253,8 @@ export class MetadataScanner { description: metadata.description, type: metadata.type, version: metadata.version, + binaryUrl: metadata.binaryUrl, + binaryHash: metadata.binaryHash, tags: metadata.tags, url: `${repoUrl}/tree/main/${urlPath}`, repoUrl, diff --git a/src/services/marketplace/schemas.ts b/src/services/marketplace/schemas.ts index 86763cbfe3..df70da0bc6 100644 --- a/src/services/marketplace/schemas.ts +++ b/src/services/marketplace/schemas.ts @@ -8,6 +8,8 @@ export const baseMetadataSchema = z.object({ name: z.string().min(1, "Name is required"), description: z.string(), version: z.string().regex(/^\d+\.\d+\.\d+$/, "Version must be in semver format (e.g., 1.0.0)"), + binaryUrl: z.string().url("Binary URL must be a valid URL").optional(), + binaryHash: z.string().optional(), tags: z.array(z.string()).optional(), author: z.string().optional(), authorUrl: z.string().url("Author URL must be a valid URL").optional(), diff --git a/src/services/marketplace/types.ts b/src/services/marketplace/types.ts index 2827b6bb38..eab0b6caa8 100644 --- a/src/services/marketplace/types.ts +++ b/src/services/marketplace/types.ts @@ -24,6 +24,8 @@ export interface BaseMetadata { name: string description: string version: string + binaryUrl?: string + binaryHash?: string tags?: string[] author?: string authorUrl?: string @@ -78,6 +80,8 @@ export interface MarketplaceItem { authorUrl?: string tags?: string[] version?: string + binaryUrl?: string + binaryHash?: string lastUpdated?: string sourceUrl?: string defaultBranch?: string @@ -126,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 7ab688439e..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 { MarketplaceSource } from "../services/marketplace/types" +import { InstallMarketplaceItemOptions, MarketplaceItem, MarketplaceSource } from "../services/marketplace/types" export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" @@ -129,6 +129,7 @@ export interface WebviewMessage { | "fetchMarketplaceItems" | "filterMarketplaceItems" | "marketplaceButtonClicked" + | "installMarketplaceItem" | "refreshMarketplaceSource" | "repositoryRefreshComplete" | "openExternal" @@ -161,6 +162,8 @@ export interface WebviewMessage { sources?: MarketplaceSource[] filters?: { type?: string; search?: string; tags?: string[] } url?: string // For openExternal + 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 new file mode 100644 index 0000000000..c8b0708728 --- /dev/null +++ b/webview-ui/src/components/marketplace/components/MarketplaceItemActionsMenu.tsx @@ -0,0 +1,71 @@ +import React from "react" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { MoreVertical, ExternalLink, Download } from "lucide-react" +import { InstallMarketplaceItemOptions, MarketplaceItem } from "../../../../../src/services/marketplace/types" +import { vscode } from "@/utils/vscode" +import { useAppTranslation } from "@/i18n/TranslationContext" + +interface MarketplaceItemActionsMenuProps { + item: MarketplaceItem + handleOpenSourceUrl: () => void +} + +export const MarketplaceItemActionsMenu: React.FC = ({ + item, + handleOpenSourceUrl, +}) => { + const { t } = useAppTranslation() + + const handleInstall = (options?: InstallMarketplaceItemOptions) => { + vscode.postMessage({ + type: "installMarketplaceItem", + mpItem: item, + mpInstallOptions: options + }) + } + + // Don't show for `package` items for now + const showInstallButton = item.type !== "package" // Don't show for package containers + + return ( + + + + + + {/* View Source / External Link Item */} + + + {t("marketplace:items.card.viewSource")} + + + {/* Install (Project) */} + {showInstallButton && ( + handleInstall({target: 'project'})}> + + {t("marketplace:items.card.installProject")} + + )} + + {/* Install (Global) */} + {showInstallButton && ( + handleInstall({target: 'global'})}> + + {t("marketplace:items.card.installGlobal")} + + )} + + + ) +} diff --git a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx index b7d2c20f09..4c467499af 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx @@ -1,5 +1,4 @@ import React, { useMemo, useCallback } from "react" -import { Button } from "@/components/ui/button" import { MarketplaceItem } from "../../../../../src/services/marketplace/types" import { vscode } from "@/utils/vscode" import { groupItemsByType, GroupedItems } from "../utils/grouping" @@ -7,6 +6,7 @@ import { ExpandableSection } from "./ExpandableSection" import { TypeGroup } from "./TypeGroup" import { ViewState } from "../MarketplaceViewStateManager" import { useAppTranslation } from "@/i18n/TranslationContext" +import { MarketplaceItemActionsMenu } from "./MarketplaceItemActionsMenu" interface MarketplaceItemCardProps { item: MarketplaceItem @@ -64,7 +64,7 @@ export const MarketplaceItemCard: React.FC = ({ }, [item.type]) // Memoize URL calculation - const urlToOpen = useMemo(() => { + const itemSourceUrl = useMemo(() => { if (item.sourceUrl && isValidUrl(item.sourceUrl)) { return item.sourceUrl } @@ -80,12 +80,12 @@ export const MarketplaceItemCard: React.FC = ({ return url }, [item.sourceUrl, item.repoUrl, item.defaultBranch, item.path]) - const handleOpenUrl = useCallback(() => { + const handleOpenSourceUrl = useCallback(() => { vscode.postMessage({ type: "openExternal", - url: urlToOpen, + url: itemSourceUrl, }) - }, [urlToOpen]) + }, [itemSourceUrl]) // Group items by type const groupedItems = useMemo(() => { @@ -194,18 +194,7 @@ export const MarketplaceItemCard: React.FC = ({ )} - + {item.type === "package" && ( diff --git a/webview-ui/src/i18n/locales/en/marketplace.json b/webview-ui/src/i18n/locales/en/marketplace.json index dd5c7a6848..deabe9b44a 100644 --- a/webview-ui/src/i18n/locales/en/marketplace.json +++ b/webview-ui/src/i18n/locales/en/marketplace.json @@ -55,6 +55,8 @@ "card": { "by": "by {{author}}", "from": "from {{source}}", + "installProject": "Install (Project)", + "installGlobal": "Install (Global)", "viewSource": "View", "viewOnSource": "View on {{source}}" }