fix(ui): correct Claude Code skills install command and settings.json snippet

This commit is contained in:
Devin AI 2026-07-16 10:07:21 +00:00
parent 2f03789927
commit 60f73c9519
3 changed files with 68 additions and 58 deletions

View file

@ -1,6 +1,8 @@
import { describe, expect, it } from "vitest";
import {
formatInstallCommand,
buildMarketplaceSettings,
MARKETPLACE_NAME,
extractCategories,
validatePluginName,
getSourceDisplayText,
@ -18,31 +20,42 @@ import {
parseSkillSource,
isValidSubPath,
} from "./helpers";
import { MarketplacePluginEntry, PluginSource } from "./types";
import { MarketplacePluginEntry } from "./types";
describe("formatInstallCommand", () => {
it("formats github source with repo", () => {
const source: PluginSource = { source: "github", repo: "org/repo" };
expect(formatInstallCommand({ name: "my-plugin", source })).toBe("/plugin marketplace add org/repo");
it("installs the plugin from the litellm marketplace by name", () => {
expect(formatInstallCommand({ name: "my-plugin" })).toBe("/plugin install my-plugin@litellm");
});
it("formats url source", () => {
const source: PluginSource = { source: "url", url: "https://example.com/plugin" };
expect(formatInstallCommand({ name: "my-plugin", source })).toBe(
"/plugin marketplace add https://example.com/plugin",
);
it("uses the marketplace name the proxy publishes, not the source", () => {
expect(formatInstallCommand({ name: "code-formatter" })).toBe(`/plugin install code-formatter@${MARKETPLACE_NAME}`);
expect(formatInstallCommand({ name: "code-formatter" })).not.toContain("marketplace add");
});
});
describe("buildMarketplaceSettings", () => {
const url = "https://proxy.example.com/claude-code/marketplace.json";
it("keys the marketplace under the name the proxy returns", () => {
const settings = buildMarketplaceSettings(url);
expect(Object.keys(settings.extraKnownMarketplaces)).toEqual([MARKETPLACE_NAME]);
expect(settings.extraKnownMarketplaces).not.toHaveProperty("my-org");
});
it("formats git-subdir source using its url", () => {
const source: PluginSource = { source: "git-subdir", url: "https://github.com/org/repo", path: "plugins/x" };
expect(formatInstallCommand({ name: "my-plugin", source })).toBe(
"/plugin marketplace add https://github.com/org/repo",
);
it("nests source as an object with a url, not a flat string", () => {
const settings = buildMarketplaceSettings(url);
expect(settings.extraKnownMarketplaces[MARKETPLACE_NAME].source).toEqual({ source: "url", url });
expect(typeof settings.extraKnownMarketplaces[MARKETPLACE_NAME].source).toBe("object");
});
it("falls back to plugin name when no repo or url", () => {
const source: PluginSource = { source: "github" };
expect(formatInstallCommand({ name: "my-plugin", source })).toBe("/plugin marketplace add my-plugin");
it("produces a snippet Claude Code accepts", () => {
expect(buildMarketplaceSettings(url)).toEqual({
extraKnownMarketplaces: {
litellm: {
source: { source: "url", url },
},
},
});
});
});

View file

@ -177,20 +177,39 @@ export const parseSkillSource = (rawUrl: string, subPath?: string): SkillSourceP
};
/**
* Generate install command for Claude Code CLI
* Format: /plugin marketplace add org/repo OR /plugin marketplace add url
* Marketplace name the proxy publishes in marketplace.json. Claude Code keys its
* `extraKnownMarketplaces` config and `@<marketplace>` install suffix off this value.
*/
export const formatInstallCommand = (plugin: { name: string; source: PluginSource }): string => {
const { source } = plugin;
if (source.source === "github" && source.repo) {
return `/plugin marketplace add ${source.repo}`;
}
if ((source.source === "url" || source.source === "git-subdir") && source.url) {
return `/plugin marketplace add ${source.url}`;
}
// Fallback to plugin name
return `/plugin marketplace add ${plugin.name}`;
};
export const MARKETPLACE_NAME = "litellm";
/**
* Path the proxy serves the Claude Code marketplace manifest from
*/
export const MARKETPLACE_MANIFEST_PATH = "/claude-code/marketplace.json";
/**
* Generate the Claude Code install command for a registered skill
* Format: /plugin install <name>@litellm
*/
export const formatInstallCommand = (plugin: { name: string }): string =>
`/plugin install ${plugin.name}@${MARKETPLACE_NAME}`;
export interface MarketplaceSettings {
extraKnownMarketplaces: Record<string, { source: { source: "url"; url: string } }>;
}
/**
* Build the `~/.claude/settings.json` snippet that registers the proxy as a Claude Code
* marketplace. Claude Code requires `source` to be a nested object, and the marketplace key
* must match the name the proxy returns in marketplace.json.
*/
export const buildMarketplaceSettings = (marketplaceUrl: string): MarketplaceSettings => ({
extraKnownMarketplaces: {
[MARKETPLACE_NAME]: {
source: { source: "url", url: marketplaceUrl },
},
},
});
/**
* Extract unique categories from plugins list

View file

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { ArrowLeftOutlined, CopyOutlined, CheckOutlined, LinkOutlined } from "@ant-design/icons";
import { formatInstallCommand } from "./helpers";
import { formatInstallCommand, buildMarketplaceSettings, MARKETPLACE_MANIFEST_PATH } from "./helpers";
import { Plugin } from "./types";
interface SkillDetailProps {
@ -31,6 +31,9 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
const installCommand = formatInstallCommand(skill);
const marketplaceUrl = `${typeof window !== "undefined" ? window.location.origin : "<proxy-url>"}${MARKETPLACE_MANIFEST_PATH}`;
const settingsSnippet = JSON.stringify(buildMarketplaceSettings(marketplaceUrl), null, 2);
const detailRows = [
...(skill.category ? [{ property: "Category", value: skill.category }] : []),
...(skill.domain ? [{ property: "Domain", value: skill.domain }] : []),
@ -298,21 +301,7 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
>
<span style={{ fontSize: 13, color: "#3c4043", fontWeight: 500 }}>~/.claude/settings.json</span>
<button
onClick={() => {
const snippet = JSON.stringify(
{
extraKnownMarketplaces: {
"my-org": {
source: "url",
url: `${typeof window !== "undefined" ? window.location.origin : ""}/claude-code/marketplace.json`,
},
},
},
null,
2,
);
copyToClipboard(snippet, "settings");
}}
onClick={() => copyToClipboard(settingsSnippet, "settings")}
style={{
display: "flex",
alignItems: "center",
@ -339,18 +328,7 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
backgroundColor: "#fff",
}}
>
{JSON.stringify(
{
extraKnownMarketplaces: {
"my-org": {
source: "url",
url: `${typeof window !== "undefined" ? window.location.origin : "<proxy-url>"}/claude-code/marketplace.json`,
},
},
},
null,
2,
)}
{settingsSnippet}
</pre>
</div>
</div>