Skip to content
Closed
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) =>
pluginConfig.path ??
path.join(targetConfig.pluginRoot ?? "plugins", pluginName),

buildPluginManifest: ({ metadata, version, pluginName, pluginConfig }) =>

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: The new Claude target implementation in src/targets/claude.ts does not apply manifest override objects (pluginConfig.manifest and targetConfig.manifest) to the generated plugin and marketplace manifests, unlike the legacy claudePluginManifest and emitClaude logic in src/targets.ts, so user-specified manifest overrides will no longer take effect once the registry-based Claude target is used.

Suggested fix: Update the claude target in src/targets/claude.ts so that buildPluginManifest deep-merges the base manifest with pluginConfig.manifest and buildMarketplaceManifest deep-merges the base marketplace object with targetConfig.manifest, mirroring the existing behavior of claudePluginManifest and emitClaude in src/targets.ts (which use deepMerge(..., pluginConfig.manifest ?? {}) and deepMerge(..., targetConfig.manifest ?? {})) before calling stripUndefined.

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

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

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