Move marketplace types to @roo-code/types (#4671)

And do some cleanup & consolidation.
This commit is contained in:
John Richmond 2025-06-13 17:02:16 -07:00 committed by GitHub
parent 7d0b22f9e6
commit 7dd56d6551
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 147 additions and 237 deletions

View file

@ -7,6 +7,7 @@ export * from "./experiment.js"
export * from "./global-settings.js"
export * from "./history.js"
export * from "./ipc.js"
export * from "./marketplace.js"
export * from "./mcp.js"
export * from "./message.js"
export * from "./mode.js"

View file

@ -0,0 +1,88 @@
import { z } from "zod"
/**
* Schema for MCP parameter definitions
*/
export const mcpParameterSchema = z.object({
name: z.string().min(1),
key: z.string().min(1),
placeholder: z.string().optional(),
optional: z.boolean().optional().default(false),
})
export type McpParameter = z.infer<typeof mcpParameterSchema>
/**
* Schema for MCP installation method with name
*/
export const mcpInstallationMethodSchema = z.object({
name: z.string().min(1),
content: z.string().min(1),
parameters: z.array(mcpParameterSchema).optional(),
prerequisites: z.array(z.string()).optional(),
})
export type McpInstallationMethod = z.infer<typeof mcpInstallationMethodSchema>
/**
* Component type validation
*/
export const marketplaceItemTypeSchema = z.enum(["mode", "mcp"] as const)
export type MarketplaceItemType = z.infer<typeof marketplaceItemTypeSchema>
/**
* Base schema for common marketplace item fields
*/
const baseMarketplaceItemSchema = z.object({
id: z.string().min(1),
name: z.string().min(1, "Name is required"),
description: z.string(),
author: z.string().optional(),
authorUrl: z.string().url("Author URL must be a valid URL").optional(),
tags: z.array(z.string()).optional(),
prerequisites: z.array(z.string()).optional(),
})
/**
* Type-specific schemas for YAML parsing (without type field, added programmatically)
*/
export const modeMarketplaceItemSchema = baseMarketplaceItemSchema.extend({
content: z.string().min(1), // YAML content for modes
})
export type ModeMarketplaceItem = z.infer<typeof modeMarketplaceItemSchema>
export const mcpMarketplaceItemSchema = baseMarketplaceItemSchema.extend({
url: z.string().url(), // Required url field
content: z.union([z.string().min(1), z.array(mcpInstallationMethodSchema)]), // Single config or array of methods
parameters: z.array(mcpParameterSchema).optional(),
})
export type McpMarketplaceItem = z.infer<typeof mcpMarketplaceItemSchema>
/**
* Unified marketplace item schema using discriminated union
*/
export const marketplaceItemSchema = z.discriminatedUnion("type", [
// Mode marketplace item
modeMarketplaceItemSchema.extend({
type: z.literal("mode"),
}),
// MCP marketplace item
mcpMarketplaceItemSchema.extend({
type: z.literal("mcp"),
}),
])
export type MarketplaceItem = z.infer<typeof marketplaceItemSchema>
/**
* Installation options for marketplace items
*/
export const installMarketplaceItemOptionsSchema = z.object({
target: z.enum(["global", "project"]).optional().default("project"),
parameters: z.record(z.string(), z.any()).optional(),
})
export type InstallMarketplaceItemOptions = z.infer<typeof installMarketplaceItemOptionsSchema>

View file

@ -4,7 +4,7 @@ import * as path from "path"
import * as yaml from "yaml"
import { RemoteConfigLoader } from "./RemoteConfigLoader"
import { SimpleInstaller } from "./SimpleInstaller"
import { MarketplaceItem, MarketplaceItemType } from "./types"
import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types"
import { GlobalFileNames } from "../../shared/globalFileNames"
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
import { t } from "../../i18n"

View file

@ -2,8 +2,8 @@ import axios from "axios"
import * as yaml from "yaml"
import { z } from "zod"
import { getRooCodeApiUrl } from "@roo-code/cloud"
import { MarketplaceItem, MarketplaceItemType } from "./types"
import { modeMarketplaceItemSchema, mcpMarketplaceItemSchema } from "./schemas"
import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types"
import { modeMarketplaceItemSchema, mcpMarketplaceItemSchema } from "@roo-code/types"
// Response schemas for YAML API responses
const modeMarketplaceResponse = z.object({
@ -43,8 +43,8 @@ export class RemoteConfigLoader {
const yamlData = yaml.parse(data)
const validated = modeMarketplaceResponse.parse(yamlData)
const items = validated.items.map((item) => ({
type: "mode" as MarketplaceItemType,
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mode" as const,
...item,
}))
@ -63,8 +63,8 @@ export class RemoteConfigLoader {
const yamlData = yaml.parse(data)
const validated = mcpMarketplaceResponse.parse(yamlData)
const items = validated.items.map((item) => ({
type: "mcp" as MarketplaceItemType,
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mcp" as const,
...item,
}))

View file

@ -2,7 +2,7 @@ import * as vscode from "vscode"
import * as path from "path"
import * as fs from "fs/promises"
import * as yaml from "yaml"
import { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "./types"
import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types"
import { GlobalFileNames } from "../../shared/globalFileNames"
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
@ -23,7 +23,7 @@ export class SimpleInstaller {
case "mcp":
return await this.installMcp(item, target, options)
default:
throw new Error(`Unsupported item type: ${item.type}`)
throw new Error(`Unsupported item type: ${(item as any).type}`)
}
}
@ -135,7 +135,8 @@ export class SimpleInstaller {
}
// Merge parameters (method-specific override global)
const allParameters = [...(item.parameters || []), ...methodParameters]
const itemParameters = item.type === "mcp" ? item.parameters || [] : []
const allParameters = [...itemParameters, ...methodParameters]
const uniqueParameters = Array.from(new Map(allParameters.map((p) => [p.key, p])).values())
// Replace parameters if provided
@ -158,7 +159,8 @@ export class SimpleInstaller {
methodParameters = method.parameters || []
// Re-merge parameters with the newly selected method
const allParametersForNewMethod = [...(item.parameters || []), ...methodParameters]
const itemParametersForNewMethod = item.type === "mcp" ? item.parameters || [] : []
const allParametersForNewMethod = [...itemParametersForNewMethod, ...methodParameters]
const uniqueParametersForNewMethod = Array.from(
new Map(allParametersForNewMethod.map((p) => [p.key, p])).values(),
)
@ -239,7 +241,7 @@ export class SimpleInstaller {
await this.removeMcp(item, target)
break
default:
throw new Error(`Unsupported item type: ${item.type}`)
throw new Error(`Unsupported item type: ${(item as any).type}`)
}
}

View file

@ -1,5 +1,5 @@
import { MarketplaceManager } from "../MarketplaceManager"
import { MarketplaceItem } from "../types"
import type { MarketplaceItem } from "@roo-code/types"
// Mock axios
jest.mock("axios")
@ -116,6 +116,7 @@ describe("MarketplaceManager", () => {
name: "Test MCP",
description: "A test MCP",
type: "mcp",
url: "https://example.com/mcp",
content: '{"command": "node", "args": ["server.js"]}',
},
]
@ -204,6 +205,7 @@ describe("MarketplaceManager", () => {
name: "Test MCP",
description: "A test MCP",
type: "mcp",
url: "https://example.com/mcp",
content: '{"command": "node", "args": ["server.js"]}',
}
@ -244,6 +246,7 @@ describe("MarketplaceManager", () => {
name: "Test MCP",
description: "A test MCP",
type: "mcp",
url: "https://example.com/mcp",
content: '{"command": "node", "args": ["server.js"]}',
}

View file

@ -1,6 +1,6 @@
import axios from "axios"
import { RemoteConfigLoader } from "../RemoteConfigLoader"
import { MarketplaceItemType } from "../types"
import type { MarketplaceItemType } from "@roo-code/types"
// Mock axios
jest.mock("axios")

View file

@ -2,7 +2,7 @@ import { SimpleInstaller } from "../SimpleInstaller"
import * as fs from "fs/promises"
import * as yaml from "yaml"
import * as vscode from "vscode"
import { MarketplaceItem } from "../types"
import type { MarketplaceItem } from "@roo-code/types"
import * as path from "path"
jest.mock("fs/promises")
@ -126,6 +126,7 @@ describe("SimpleInstaller", () => {
name: "Test MCP",
description: "A test MCP server for testing",
type: "mcp",
url: "https://example.com/mcp",
content: JSON.stringify({
command: "test-server",
args: ["--test"],

View file

@ -75,6 +75,7 @@ describe("Marketplace Setting Check", () => {
type: "mcp" as const,
description: "Test description",
content: "test content",
url: "https://example.com/test-mcp",
},
mpInstallOptions: { target: "project" as const },
}

View file

@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest"
import { mcpInstallationMethodSchema, mcpMarketplaceItemYamlSchema } from "../schemas"
import { McpInstallationMethod, McpMarketplaceItem } from "../types"
import { mcpInstallationMethodSchema, mcpMarketplaceItemSchema } from "@roo-code/types"
import type { McpInstallationMethod, McpMarketplaceItem } from "@roo-code/types"
describe("Nested Parameters", () => {
describe("McpInstallationMethod Schema", () => {
@ -95,7 +95,7 @@ describe("Nested Parameters", () => {
],
}
const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters).toHaveLength(1)
expect(result.parameters![0].key).toBe("api_key")
@ -131,7 +131,7 @@ describe("Nested Parameters", () => {
],
}
const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters).toHaveLength(1)
const methods = result.content as McpInstallationMethod[]
@ -160,7 +160,7 @@ describe("Nested Parameters", () => {
],
}
const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters).toBeUndefined()
const methods = result.content as McpInstallationMethod[]
@ -182,7 +182,7 @@ describe("Nested Parameters", () => {
],
}
const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters).toBeUndefined()
const methods = result.content as McpInstallationMethod[]
@ -221,7 +221,7 @@ describe("Nested Parameters", () => {
}
// This should validate successfully - the conflict resolution happens at runtime
const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters![0].key).toBe("version")
const methods = result.content as McpInstallationMethod[]

View file

@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest"
import { mcpParameterSchema } from "../schemas"
import { McpParameter } from "../types"
import { mcpParameterSchema } from "@roo-code/types"
import type { McpParameter } from "@roo-code/types"
describe("Optional Parameters", () => {
describe("McpParameter Schema", () => {
@ -67,23 +67,4 @@ describe("Optional Parameters", () => {
}).toThrow()
})
})
describe("Type Definitions", () => {
it("should allow optional field in McpParameter interface", () => {
const requiredParam: McpParameter = {
name: "Required Param",
key: "required_key",
}
const optionalParam: McpParameter = {
name: "Optional Param",
key: "optional_key",
optional: true,
}
// These should compile without errors
expect(requiredParam.optional).toBeUndefined()
expect(optionalParam.optional).toBe(true)
})
})
})

View file

@ -1,4 +1,3 @@
export * from "./SimpleInstaller"
export * from "./MarketplaceManager"
export * from "./types"
export * from "./schemas"
export type { MarketplaceItemType } from "@roo-code/types"

View file

@ -1,84 +0,0 @@
import { z } from "zod"
/**
* Schema for MCP parameter definitions
*/
export const mcpParameterSchema = z.object({
name: z.string().min(1),
key: z.string().min(1),
placeholder: z.string().optional(),
optional: z.boolean().optional().default(false),
})
/**
* Schema for MCP installation method with name
*/
export const mcpInstallationMethodSchema = z.object({
name: z.string().min(1),
content: z.string().min(1),
parameters: z.array(mcpParameterSchema).optional(),
prerequisites: z.array(z.string()).optional(),
})
/**
* Component type validation
*/
export const marketplaceItemTypeSchema = z.enum(["mode", "mcp"] as const)
/**
* Schema for a marketplace item (supports both mode and mcp types)
*/
export const marketplaceItemSchema = z.object({
id: z.string().min(1),
name: z.string().min(1, "Name is required"),
description: z.string(),
type: marketplaceItemTypeSchema,
author: z.string().optional(),
authorUrl: z.string().url("Author URL must be a valid URL").optional(),
tags: z.array(z.string()).optional(),
content: z.union([z.string().min(1), z.array(mcpInstallationMethodSchema)]), // Embedded content (YAML for modes, JSON for mcps, or named methods)
prerequisites: z.array(z.string()).optional(),
})
/**
* Local marketplace config schema (JSON format)
*/
export const marketplaceConfigSchema = z.object({
items: z.record(z.string(), marketplaceItemSchema),
})
/**
* Local marketplace YAML config schema (uses any for items since they're validated separately by type)
*/
export const marketplaceYamlConfigSchema = z.object({
items: z.array(z.any()), // Items are validated separately by type-specific schemas
})
// Schemas for YAML files (without type field, as type is added programmatically)
export const modeMarketplaceItemYamlSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
author: z.string().optional(),
authorUrl: z.string().url().optional(),
tags: z.array(z.string()).optional(),
content: z.string(),
prerequisites: z.array(z.string()).optional(),
})
export const mcpMarketplaceItemYamlSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
author: z.string().optional(),
authorUrl: z.string().url().optional(),
url: z.string().url(), // Required url field
tags: z.array(z.string()).optional(),
content: z.union([z.string(), z.array(mcpInstallationMethodSchema)]),
parameters: z.array(mcpParameterSchema).optional(),
prerequisites: z.array(z.string()).optional(),
})
// Export aliases for backward compatibility (these are the same as the YAML schemas)
export const modeMarketplaceItemSchema = modeMarketplaceItemYamlSchema
export const mcpMarketplaceItemSchema = mcpMarketplaceItemYamlSchema

View file

@ -1,92 +0,0 @@
/**
* Supported component types
*/
export type MarketplaceItemType = "mode" | "mcp"
/**
* Local marketplace config types
*/
export interface MarketplaceConfig<T = any> {
items: Record<string, T>
}
export interface MarketplaceYamlConfig<T = any> {
items: T[]
}
export interface ModeMarketplaceItem {
id: string
name: string
description: string
author?: string
authorUrl?: string
tags?: string[]
content: string // Embedded YAML content for .roomodes
prerequisites?: string[]
}
export interface McpParameter {
name: string
key: string
placeholder?: string
optional?: boolean // Defaults to false if not provided
}
export interface McpInstallationMethod {
name: string
content: string
parameters?: McpParameter[]
prerequisites?: string[]
}
export interface McpMarketplaceItem {
id: string
name: string
description: string
author?: string
authorUrl?: string
url: string // Required url field
tags?: string[]
content: string | McpInstallationMethod[] // Can be a single config or array of named methods
parameters?: McpParameter[]
prerequisites?: string[]
}
/**
* Unified marketplace item for UI
*/
export interface MarketplaceItem {
id: string
name: string
description: string
type: MarketplaceItemType
author?: string
authorUrl?: string
url?: string // Optional - only MCPs have url
tags?: string[]
content: string | McpInstallationMethod[] // Can be a single config or array of named methods
parameters?: McpParameter[] // Optional parameters for MCPs
prerequisites?: string[]
}
export interface InstallMarketplaceItemOptions {
/**
* Specify the target scope
*
* @default 'project'
*/
target?: "global" | "project"
/**
* Parameters provided by the user for configurable marketplace items
*/
parameters?: Record<string, any>
}
export interface RemoveInstalledMarketplaceItemOptions {
/**
* Specify the target scope
*
* @default 'project'
*/
target?: "global" | "project"
}

View file

@ -16,7 +16,7 @@ import { GitCommit } from "../utils/git"
import { McpServer } from "./mcp"
import { Mode } from "./modes"
import { RouterModels } from "./api"
import { MarketplaceItem } from "../services/marketplace/types"
import type { MarketplaceItem } from "@roo-code/types"
// Indexing status types
export interface IndexingStatus {

View file

@ -1,8 +1,13 @@
import { z } from "zod"
import type { ProviderSettings, PromptComponent, ModeConfig } from "@roo-code/types"
import { InstallMarketplaceItemOptions, MarketplaceItem } from "../services/marketplace/types"
import { marketplaceItemSchema } from "../services/marketplace/schemas"
import type {
ProviderSettings,
PromptComponent,
ModeConfig,
InstallMarketplaceItemOptions,
MarketplaceItem,
} from "@roo-code/types"
import { marketplaceItemSchema } from "@roo-code/types"
import { Mode } from "./modes"
@ -229,7 +234,7 @@ export interface IndexClearedPayload {
}
export const installMarketplaceItemWithParametersPayloadSchema = z.object({
item: marketplaceItemSchema.strict(),
item: marketplaceItemSchema,
parameters: z.record(z.string(), z.any()),
})

View file

@ -11,7 +11,7 @@
* 3. Using minimal state updates to avoid resetting scroll position
*/
import { MarketplaceItem } from "../../../../src/services/marketplace/types"
import { MarketplaceItem } from "@roo-code/types"
import { vscode } from "../../utils/vscode"
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"

View file

@ -1,5 +1,5 @@
import React, { useState, useMemo, useEffect } from "react"
import { MarketplaceItem, McpParameter, McpInstallationMethod } from "../../../../../src/services/marketplace/types"
import { MarketplaceItem, McpParameter, McpInstallationMethod } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { useAppTranslation } from "@/i18n/TranslationContext"
import {
@ -61,7 +61,7 @@ export const MarketplaceInstallModal: React.FC<MarketplaceInstallModalProps> = (
const effectiveParameters = useMemo(() => {
if (!item) return []
const globalParams = item.parameters || []
const globalParams = item.type === "mcp" ? item.parameters || [] : []
let methodParams: McpParameter[] = []
// Get method-specific parameters if content is an array
@ -100,7 +100,7 @@ export const MarketplaceInstallModal: React.FC<MarketplaceInstallModalProps> = (
React.useEffect(() => {
if (item) {
// Get effective parameters for current method
const globalParams = item.parameters || []
const globalParams = item.type === "mcp" ? item.parameters || [] : []
let methodParams: McpParameter[] = []
if (Array.isArray(item.content)) {

View file

@ -1,5 +1,5 @@
import React, { useMemo, useState } from "react"
import { MarketplaceItem } from "../../../../../src/services/marketplace/types"
import { MarketplaceItem } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { ViewState } from "../MarketplaceViewStateManager"
import { useAppTranslation } from "@/i18n/TranslationContext"
@ -54,7 +54,7 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
<div className="flex gap-2 items-start">
<div>
<h3 className="text-lg font-semibold text-vscode-foreground mt-0 mb-1 leading-none">
{item.url && isValidUrl(item.url) ? (
{item.type === "mcp" && item.url && isValidUrl(item.url) ? (
<Button
variant="link"
className="p-0 h-auto text-lg font-semibold text-vscode-foreground hover:underline"

View file

@ -1,7 +1,7 @@
import React from "react"
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { MarketplaceInstallModal } from "../MarketplaceInstallModal"
import { MarketplaceItem } from "../../../../../../src/services/marketplace/types"
import { MarketplaceItem } from "@roo-code/types"
// Mock vscode
const mockPostMessage = jest.fn()
@ -39,6 +39,7 @@ describe("MarketplaceInstallModal - Optional Parameters", () => {
name: "Test MCP",
description: "Test MCP with parameters",
type: "mcp",
url: "https://example.com/test-mcp",
content: '{"test-server": {"command": "test", "args": ["--key", "{{api_key}}", "--endpoint", "{{endpoint}}"]}}',
parameters,
})

View file

@ -1,7 +1,7 @@
import React from "react"
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { MarketplaceInstallModal } from "../MarketplaceInstallModal"
import { MarketplaceItem } from "../../../../../../src/services/marketplace/types"
import { MarketplaceItem } from "@roo-code/types"
// Mock the vscode module before importing the component
jest.mock("@/utils/vscode", () => ({
@ -44,6 +44,7 @@ describe("MarketplaceInstallModal - Nested Parameters", () => {
name: "Test MCP Server",
description: "A test MCP server",
type: "mcp",
url: "https://example.com/test-mcp",
author: "Test Author",
tags: ["test"],
// Global parameters

View file

@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { MarketplaceItemCard } from "../MarketplaceItemCard"
import { vscode } from "@/utils/vscode"
import { MarketplaceItem } from "../../../../../../src/services/marketplace/types"
import { MarketplaceItem } from "@roo-code/types"
import { TooltipProvider } from "@/components/ui/tooltip"
// Mock vscode API
jest.mock("@/utils/vscode", () => ({

View file

@ -1,5 +1,5 @@
import { groupItemsByType, formatItemText, getTotalItemCount, getUniqueTypes } from "../grouping"
import { MarketplaceItem } from "../../../../../../src/services/marketplace/types"
import { MarketplaceItem } from "@roo-code/types"
describe("grouping utilities", () => {
const mockItems: MarketplaceItem[] = [
@ -8,6 +8,7 @@ describe("grouping utilities", () => {
name: "Test Server",
description: "A test MCP server",
type: "mcp",
url: "https://example.com/test-server",
content: "test content",
},
{
@ -22,6 +23,7 @@ describe("grouping utilities", () => {
name: "Another Server",
description: "Another test MCP server",
type: "mcp",
url: "https://example.com/another-server",
content: "test content",
},
]
@ -50,6 +52,7 @@ describe("grouping utilities", () => {
name: "",
description: "",
type: "mcp",
url: "https://example.com/test-item",
content: "test content",
},
]

View file

@ -1,4 +1,4 @@
import { MarketplaceItem } from "../../../../../src/services/marketplace/types"
import { MarketplaceItem } from "@roo-code/types"
export interface GroupedItems {
[type: string]: {