Skip to content

Commit 1ec81b1

Browse files
committed
return deleted files
1 parent a42580a commit 1ec81b1

5 files changed

Lines changed: 180 additions & 54 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jfrog",
3-
"description": "Use the JFrog Platform from Claude Code: Artifactory repos and artifacts, security findings and exposures, Catalog package safety and downloads, workflows across the SDLC, and platform administration. Includes governance for installing MCP servers exclusively via the JFrog MCP Gateway.",
3+
"description": "Use the JFrog Platform from Claude Code: Artifactory repos and artifacts, security findings and exposures, Catalog package safety and downloads, workflows across the SDLC, and platform administration.",
44
"version": "0.1.1",
55
"author": {
66
"name": "JFrog Ltd.",

.github/workflows/validate.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) JFrog Ltd. 2026
2+
# Licensed under the Apache License, Version 2.0
3+
# https://www.apache.org/licenses/LICENSE-2.0
4+
5+
name: Validate plugin
6+
7+
on:
8+
push:
9+
branches: [main]
10+
pull_request:
11+
12+
jobs:
13+
validate:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v5
17+
18+
- name: Set up Node.js
19+
uses: actions/setup-node@v5
20+
with:
21+
node-version: "24"
22+
23+
- name: Validate plugin layout
24+
run: node scripts/validate-claude-plugin.mjs

CONTRIBUTING.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ All contributors must sign the [JFrog CLA](https://jfrog.com/cla/) before contri
1010

1111
1. **Fork** the repository and create a feature branch from `main`.
1212
2. Make your changes, ensuring they follow the existing code style and project conventions.
13-
3. Before a release or directory submission, run **`claude plugin validate`** (requires [Claude Code](https://code.claude.com/docs) CLI).
13+
3. **Validate** locally:
14+
15+
```bash
16+
node scripts/validate-claude-plugin.mjs
17+
```
18+
19+
Before a release or directory submission, also run **`claude plugin validate`** (requires [Claude Code](https://code.claude.com/docs) CLI).
1420

1521
4. **Test** by loading the repository as the plugin (the repo root is the plugin root):
1622

@@ -25,6 +31,7 @@ Exercise the skills you changed (for example `/jfrog:<skill-name>`). Run `/reloa
2531

2632
## Pre-release checklist
2733

34+
- [ ] `node scripts/validate-claude-plugin.mjs` passes.
2835
- [ ] `claude plugin validate` passes (before directory submission or major releases).
2936
- [ ] Version bumped in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) when the plugin changes.
3037
- [ ] No secrets, credentials, or files under `**/local-cache/` committed.

README.md

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,61 +16,10 @@ This repository **is** the plugin: manifest at [`.claude-plugin/plugin.json`](.c
1616

1717
Skills are vendored from [jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills); the pinned **release and commit** are in [`skills/VENDOR.md`](skills/VENDOR.md).
1818

19-
## JFrog MCP Gateway integration
20-
21-
When the plugin is enabled, every Claude Code session starts by asking the JFrog MCP Gateway whether the current tenant is entitled to the AI Catalog feature (`npx @jfrog/mcp-gateway --should-inject`). If the gateway returns **entitled**, the rules in [`templates/jfrog-mcp-management.md`](templates/jfrog-mcp-management.md) are injected into the model's context via the [`additionalContext`](https://docs.anthropic.com/en/docs/claude-code/hooks#sessionstart) `SessionStart` hook output. **No instructions file is written to your repo** — the rules live only in the plugin and reach the model directly through the hook.
22-
23-
The check fails closed: anything other than a clean "entitled" response (entitlement denied, no JFrog server resolvable, network/`npx` failure, timeout) skips the injection so the plugin stays inert when the gateway can't authoritatively say "yes".
24-
25-
You can short-circuit the check with the `JF_MCP_GATEWAY_FORCE_ENABLE` env var in the shell that launches Claude Code — this is mainly for tests, demos, and air-gapped setups:
26-
27-
| Value | Effect |
28-
| --- | --- |
29-
| `true` | Always inject the instructions; skip the entitlement check entirely. |
30-
| `false` | Never inject; skip the entitlement check entirely. |
31-
| unset / anything else | Run `--should-inject` and respect its decision (default). |
32-
33-
Set `JF_MCP_GATEWAY_DEBUG=1` to have the hook log its decision to stderr (visible in Claude Code's logs panel) for troubleshooting.
34-
35-
The rules tell Claude to:
36-
37-
- Add MCP servers only through `npx @jfrog/mcp-gateway --inspect / --login / --list-available`, never via direct `npx`/`pip`/`docker` or hand-rolled catalog API calls.
38-
- Resolve project / server ID from `.claude/settings.json` (`allowedMcpServers`), then existing `.mcp.json` / `~/.claude.json`, then `~/.jfrog/jfrog-cli.conf.v6`.
39-
- Write the entry to **`.mcp.json` (project scope) by default** — creating the file if it doesn't exist — so the entry stays alongside the project and the team can share it via git. Only fall back to `~/.claude.json` (user scope) when you ask for it explicitly. Either way, secrets stay out of the file via `${ENV_VAR}` expansion (Claude Code has no native interactive secret prompt).
40-
- Run the gateway's `--login` automatically for OAuth-only remote MCPs.
41-
42-
After a new MCP entry is written, you must (1) **export every `${VAR}` referenced by the entry** in the shell that will launch Claude Code so the gateway has them at server-start time (an unset variable shows as `[Contains warnings]` in `/mcp` — informational only — and any tool call needing that value will fail at runtime), and (2) **quit and relaunch Claude Code** in the same directory. The first time you launch in a project, Claude Code prompts you both for workspace trust and once per `.mcp.json` server (`Approve MCP server <name> from .mcp.json?`); accept each one. On subsequent launches the `mcpServers` block loads silently. Verify with `/mcp` (under "Project MCPs (.../.mcp.json)") or `claude mcp list`.
43-
44-
Note: in Claude Code v2.1.x, per-project approve/reject decisions are persisted in **`<cwd>/.claude/settings.local.json`** (`enabledMcpjsonServers` / `disabledMcpjsonServers`), **not** in `~/.claude.json`. `claude mcp reset-project-choices` and deleting `projects.<cwd>` from `~/.claude.json` do **not** touch that file, so a previously-rejected `.mcp.json` server stays silently disabled and is never re-prompted. To re-enable it, edit `<cwd>/.claude/settings.local.json`, remove the entry from `disabledMcpjsonServers`, append it to `enabledMcpjsonServers`, and relaunch.
45-
46-
To **enforce** the gateway as the only allowed transport in a project, add an `allowedMcpServers` policy to `.claude/settings.json`:
47-
48-
```json
49-
{
50-
"allowedMcpServers": [
51-
{
52-
"serverCommand": [
53-
"npx",
54-
"--yes",
55-
"--registry",
56-
"https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/",
57-
"@jfrog/mcp-gateway",
58-
"--server",
59-
"<JFROG_SERVER_ID>"
60-
]
61-
}
62-
]
63-
}
64-
```
65-
66-
To override the registry without editing the manifest, set `JFROG_MCP_GATEWAY_REPO` in your shell.
67-
6819
## Prerequisites
6920

7021
1. **JFrog Platform** access (Cloud or self-hosted).
71-
2. **Node.js** (for `npx @jfrog/mcp-gateway` and the plugin's `SessionStart` hook).
72-
3. **JFrog credentials** — either a `JFROG_ACCESS_TOKEN` / `JF_ACCESS_TOKEN` env var, or a server registered with the [JFrog CLI](https://jfrog.com/getcli) via `jf c add`. The gateway picks up either source automatically.
73-
4. **JFrog CLI** (`jf`) is used by several skills for authentication and REST API operations. It can be installed automatically if missing, or manually via [Get JFrog CLI](https://jfrog.com/getcli) (installers and downloads) or the [Install JFrog CLI](https://docs.jfrog.com/integrations/docs/download-and-install-the-jfrog-cli) documentation for full steps and troubleshooting.
22+
2. **JFrog CLI** (`jf`) is used by several skills for authentication and REST API operations. It can be installed automatically if missing, or manually via [Get JFrog CLI](https://jfrog.com/getcli) (installers and downloads) or the [Install JFrog CLI](https://docs.jfrog.com/integrations/docs/download-and-install-the-jfrog-cli) documentation for full steps and troubleshooting.
7423

7524
## Setup
7625

scripts/validate-claude-plugin.mjs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env node
2+
3+
// Copyright (c) JFrog Ltd. 2026
4+
// Licensed under the Apache License, Version 2.0
5+
// https://www.apache.org/licenses/LICENSE-2.0
6+
7+
import { promises as fs } from "node:fs";
8+
import path from "node:path";
9+
import process from "node:process";
10+
11+
const repoRoot = process.cwd();
12+
const errors = [];
13+
const warnings = [];
14+
15+
const pluginNamePattern = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
16+
17+
function addError(message) {
18+
errors.push(message);
19+
}
20+
21+
function addWarning(message) {
22+
warnings.push(message);
23+
}
24+
25+
async function pathExists(targetPath) {
26+
try {
27+
await fs.access(targetPath);
28+
return true;
29+
} catch {
30+
return false;
31+
}
32+
}
33+
34+
async function readJsonFile(filePath, context) {
35+
let raw;
36+
try {
37+
raw = await fs.readFile(filePath, "utf8");
38+
} catch {
39+
addError(`${context} is missing: ${filePath}`);
40+
return null;
41+
}
42+
43+
try {
44+
return JSON.parse(raw);
45+
} catch (error) {
46+
addError(`${context} contains invalid JSON (${filePath}): ${error.message}`);
47+
return null;
48+
}
49+
}
50+
51+
function normalizeNewlines(content) {
52+
return content.replace(/\r\n/g, "\n");
53+
}
54+
55+
function extractFrontmatterBlock(content) {
56+
const normalized = normalizeNewlines(content);
57+
if (!normalized.startsWith("---\n")) {
58+
return null;
59+
}
60+
const closingIndex = normalized.indexOf("\n---\n", 4);
61+
if (closingIndex === -1) {
62+
return null;
63+
}
64+
return normalized.slice(4, closingIndex);
65+
}
66+
67+
async function validateSkillFile(filePath, pluginName) {
68+
const content = await fs.readFile(filePath, "utf8");
69+
const relativeFile = path.relative(repoRoot, filePath);
70+
const block = extractFrontmatterBlock(content);
71+
if (!block) {
72+
addError(`${pluginName}: skill missing YAML frontmatter: ${relativeFile}`);
73+
return;
74+
}
75+
if (!/^name:\s+/m.test(block)) {
76+
addError(`${pluginName}: skill missing "name" in frontmatter: ${relativeFile}`);
77+
}
78+
if (!/^description:\s+/m.test(block)) {
79+
addError(`${pluginName}: skill missing "description" in frontmatter: ${relativeFile}`);
80+
}
81+
}
82+
83+
async function validateSkills(pluginDir, pluginName) {
84+
const skillsDir = path.join(pluginDir, "skills");
85+
if (!(await pathExists(skillsDir))) {
86+
addWarning(`${pluginName}: no skills/ directory`);
87+
return;
88+
}
89+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
90+
let foundSkill = false;
91+
for (const entry of entries) {
92+
if (!entry.isDirectory()) {
93+
continue;
94+
}
95+
const skillMd = path.join(skillsDir, entry.name, "SKILL.md");
96+
if (await pathExists(skillMd)) {
97+
foundSkill = true;
98+
await validateSkillFile(skillMd, pluginName);
99+
}
100+
}
101+
if (!foundSkill) {
102+
addError(`${pluginName}: no skills/*/SKILL.md found under ${path.relative(repoRoot, skillsDir)}`);
103+
}
104+
}
105+
106+
async function main() {
107+
const manifestPath = path.join(repoRoot, ".claude-plugin", "plugin.json");
108+
const pluginManifest = await readJsonFile(manifestPath, "Plugin manifest (.claude-plugin/plugin.json)");
109+
if (!pluginManifest) {
110+
summarizeAndExit();
111+
return;
112+
}
113+
114+
const pluginName = pluginManifest.name || "plugin";
115+
if (typeof pluginManifest.name !== "string" || !pluginNamePattern.test(pluginManifest.name)) {
116+
addError(
117+
`"name" in plugin.json must be lowercase and use only alphanumerics, hyphens, and periods.`
118+
);
119+
}
120+
121+
await validateSkills(repoRoot, pluginName);
122+
123+
summarizeAndExit();
124+
}
125+
126+
function summarizeAndExit() {
127+
if (warnings.length > 0) {
128+
console.log("Warnings:");
129+
for (const warning of warnings) {
130+
console.log(`- ${warning}`);
131+
}
132+
console.log("");
133+
}
134+
135+
if (errors.length > 0) {
136+
console.error("Validation failed:");
137+
for (const error of errors) {
138+
console.error(`- ${error}`);
139+
}
140+
process.exit(1);
141+
}
142+
143+
console.log("Validation passed.");
144+
}
145+
146+
await main();

0 commit comments

Comments
 (0)