Merge pull request #5 from NamesMT/marketplace/roo-rocket

feat: marketplace install MVP
This commit is contained in:
JB Brown 2025-04-29 06:57:25 -07:00 committed by GitHub
commit 6a4dc46c80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 453 additions and 32 deletions

240
package-lock.json generated
View file

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

View file

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

View file

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

View file

@ -1282,6 +1282,7 @@ export const webviewMessageHandler = async (
marketplaceManager &&
(message.type === "marketplaceSources" ||
message.type === "openExternal" ||
message.type === "installMarketplaceItem" ||
message.type === "refreshMarketplaceSource" ||
message.type === "filterMarketplaceItems")
) {

View file

@ -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<string> {
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())
}

View file

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

View file

@ -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(),

View file

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

View file

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

View file

@ -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<MarketplaceItemActionsMenuProps> = ({
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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label={t("marketplace:items.card.actionsMenuLabel") || "Actions"}>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="bottom">
{/* View Source / External Link Item */}
<DropdownMenuItem
aria-label={item.sourceName}
onClick={handleOpenSourceUrl}
>
<ExternalLink className="mr-2 h-4 w-4" />
<span>{t("marketplace:items.card.viewSource")}</span>
</DropdownMenuItem>
{/* Install (Project) */}
{showInstallButton && (
<DropdownMenuItem onClick={() => handleInstall({target: 'project'})}>
<Download className="mr-2 h-4 w-4" />
<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>
</DropdownMenu>
)
}

View file

@ -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<MarketplaceItemCardProps> = ({
}, [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<MarketplaceItemCardProps> = ({
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<MarketplaceItemCardProps> = ({
)}
</div>
<Button
onClick={handleOpenUrl}
aria-label={
item.sourceUrl && isValidUrl(item.sourceUrl)
? ""
: item.sourceName || t("marketplace:items.card.viewSource")
}>
<span
className={`codicon codicon-link-external${!item.sourceUrl || !isValidUrl(item.sourceUrl) ? " mr-2" : ""}`}></span>
{(!item.sourceUrl || !isValidUrl(item.sourceUrl)) &&
(item.sourceName || t("marketplace:items.card.viewSource"))}
</Button>
<MarketplaceItemActionsMenu item={item} handleOpenSourceUrl={handleOpenSourceUrl} />
</div>
{item.type === "package" && (

View file

@ -55,6 +55,8 @@
"card": {
"by": "by {{author}}",
"from": "from {{source}}",
"installProject": "Install (Project)",
"installGlobal": "Install (Global)",
"viewSource": "View",
"viewOnSource": "View on {{source}}"
}