Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,11 @@ export async function emitCursor(
return artifact(target, outDir, files);
}

/** Emits the Claude target's plugins and marketplace manifest. */
/**
* @deprecated Legacy emitter, superseded by `src/targets/claude.ts` via the
* registry in `src/targets/registry.ts`. Kept only until every target has
* migrated (see `src/adapters.ts`).
*/
export async function emitClaude(
project: ResolvedProject,
target: TargetName,
Expand Down
183 changes: 183 additions & 0 deletions src/targets/claude.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import path from "node:path";
import { stripUndefined } from "./shared.js";
import {
error,
readJson,
validateBareStringSourceEntry,
validateFrontmatter,
validateHooksShape,
validateMarketplaceBasics,
} from "./validation-shared.js";
import type { PluginTargetDefinition } from "./types.js";

const pluginNamePattern = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;

/**
* Claude Code agent-plugin target. `validateManifest`'s required fields
* (`version`, `description`, `author.name`) were verified directly against
* `claude plugin validate --strict` — a minimal manifest with only `name`
* fails that check, so pluginpack's existing stricter validation matches the
* real CLI rather than over-constraining it.
*/
export const claude: PluginTargetDefinition = {
name: "claude",

defaultComponents: ["skills", "agents", "hooks", "scripts", "assets"],

resolvePluginPath: (pluginName, pluginConfig, targetConfig) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: In src/targets/claude.ts, resolvePluginPath does not normalize the computed pluginPath with toPosix(), unlike the legacy emitClaude and the migrated Copilot target, so on Windows the marketplace entry source field will contain backslashes (e.g. "./plugins\my-plugin") instead of the expected POSIX-style path, changing behavior and potentially breaking consumers that expect forward slashes.

Suggested fix: Update resolvePluginPath in src/targets/claude.ts to mirror the legacy Claude and Copilot implementations by wrapping the joined path in toPosix(), and import toPosix from ../fs.js. For example:

import path from "node:path";
import { toPosix } from "../fs.js";
...
resolvePluginPath: (pluginName, pluginConfig, targetConfig) =>
  pluginConfig.path ??
  toPosix(path.join(targetConfig.pluginRoot ?? "plugins", pluginName)),

🔧 Tag @ glean-for-engineering to fix or click here to fix in Glean

💬 Help us improve! Was this comment helpful? React with 👍 or 👎

pluginConfig.path ??
path.join(targetConfig.pluginRoot ?? "plugins", pluginName),

buildPluginManifest: ({ metadata, version, pluginName, pluginConfig }) =>
stripUndefined({
name: pluginName,
version: pluginConfig.version ?? version,
description: pluginConfig.description ?? metadata?.description,
author: metadata?.author,
homepage: metadata?.homepage,
repository: metadata?.repository,
license: metadata?.license,
keywords: metadata?.keywords,
}),
manifestPaths: (pluginPath, targetConfig) => [
path.join(
pluginPath,
targetConfig.marketplaceDir ?? ".claude-plugin",
"plugin.json",
),
],

buildMarketplaceEntry: ({ pluginName, pluginPath, pluginConfig, manifest }) =>
stripUndefined({
name: pluginName,
source: `./${pluginPath}`,
description:
pluginConfig.description ??
(manifest?.description as string | undefined),
}),

buildMarketplaceManifest: ({ project, version, plugins }) =>
stripUndefined({
$schema: "https://anthropic.com/claude-code/marketplace.schema.json",
name: project.config.name,
version,
description: project.config.metadata?.description,
owner: project.config.metadata?.owner ?? project.config.metadata?.author,
plugins,
}),
marketplacePaths: (targetConfig) => [
path.join(
targetConfig.marketplaceDir ?? ".claude-plugin",
"marketplace.json",
),
],

mcpConfigPath: (pluginPath) => path.join(pluginPath, ".mcp.json"),
hooksPath: (pluginPath) => path.join(pluginPath, "hooks", "hooks.json"),

validateManifest: (manifest, pluginName, issues) => {
for (const field of ["name", "version", "description"]) {
if (typeof manifest[field] !== "string" || !manifest[field]) {
error(
issues,
`${pluginName}: plugin.json is missing required field "${field}".`,
);
}
}
const author = manifest.author as Record<string, unknown> | undefined;
if (!author || typeof author.name !== "string" || !author.name) {
error(issues, `${pluginName}: plugin.json is missing "author.name".`);
}
},
validateMarketplaceEntry: (entry, index, root, issues) =>
validateBareStringSourceEntry(
entry,
index,
root,
issues,
pluginNamePattern,
),

validateOutput: async (root, issues) => {
const marketplacePath = path.join(
root,
".claude-plugin",
"marketplace.json",
);
const marketplace = await readJson(
marketplacePath,
"Marketplace manifest",
issues,
);
if (!marketplace) {
return;
}
validateMarketplaceBasics(marketplace, issues);
const plugins = Array.isArray(marketplace.plugins)
? marketplace.plugins
: [];
if (plugins.length === 0) {
error(issues, 'Marketplace "plugins" must be a non-empty array.');
return;
}
for (const [index, entry] of plugins.entries()) {
const pluginName = claude.validateMarketplaceEntry(
entry,
index,
root,
issues,
);
if (!pluginName) {
continue;
}
const pluginDir = path.join(root, entry.source);
const manifest = await readJson(
path.join(pluginDir, ".claude-plugin", "plugin.json"),
`${pluginName} plugin manifest`,
issues,
);
if (!manifest) {
continue;
}
if (manifest.name !== pluginName) {
error(
issues,
`${pluginName}: marketplace entry name does not match plugin.json name ("${manifest.name}").`,
);
}
claude.validateManifest(manifest, pluginName, issues);
await validateFrontmatter(pluginDir, pluginName, "claude", issues);
await validateHooksShape(
pluginDir,
pluginName,
"hooks/hooks.json",
issues,
);
}
},

installSnippet: {
userConfigurable: true,
build: ({ repository, pluginName, marketplaceName }) => ({
kind: "command",
snippet: `/plugin marketplace add ${repository}\n/plugin install ${pluginName}@${marketplaceName}`,
note: `Once the marketplace is added, "claude plugin install ${pluginName}@${marketplaceName}" also works as a standalone shell command — but marketplace add itself is slash-only, with no shell equivalent.`,
}),
citation: {
claim:
"/plugin marketplace add is slash-only; claude plugin install works as a standalone shell command once a marketplace is already added",
documentationUrl:
"https://code.claude.com/docs/en/plugins-reference#cli-commands-reference",
verifiedAt: "2026-07-25",
},
},

citations: [
{
claim:
"claude plugin validate --strict treats missing version/description/author.name as errors",
documentationUrl: "https://code.claude.com/docs/en/plugins-reference",
verifiedAt: "2026-07-26",
},
],
};
2 changes: 2 additions & 0 deletions src/targets/registry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { antigravity } from "./antigravity.js";
import { claude } from "./claude.js";
import { copilot } from "./copilot.js";
import { cursor } from "./cursor.js";
import type { TargetName } from "../types.js";
Expand All @@ -15,4 +16,5 @@ export const targets: Partial<Record<TargetName, PluginTargetDefinition>> = {
copilot,
antigravity,
cursor,
claude,
};
7 changes: 6 additions & 1 deletion src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,12 @@ export async function validateCursor(
}
}

/** Validates a built Claude target's output directory. */
/**
* @deprecated Legacy validator, superseded by `src/targets/claude.ts`'s
* `PluginTargetDefinition.validateOutput` via the registry in
* `src/targets/registry.ts`. Kept only until every target has migrated (see
* `src/adapters.ts`).
*/
export async function validateClaude(
root: string,
issues: ValidationIssue[],
Expand Down