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
5 changes: 5 additions & 0 deletions .github/scripts/sync-skills-vendor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
Comment thread
davida-jfrog marked this conversation as resolved.
"repo": "jfrog/jfrog-skills",
"pin": "v0.11.0",
"paths": ["skills"]
}
118 changes: 118 additions & 0 deletions .github/scripts/sync-skills.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env node
// Vendors skill content from the upstream jfrog/jfrog-skills repository
// into this plugin. Run manually when bumping the pin: bump `pin` in
// sync-skills-vendor.json, then run this
// script to regenerate `skills/`, then commit both alongside each other.
//
// Usage:
// node .github/scripts/sync-skills.mjs
//
// Steps the script performs:
// 1. Reads sync-skills-vendor.json to learn which repo + ref to pull.
// 2. Downloads that tarball from codeload.github.com (public, no auth).
// 3. Extracts it into a temp directory.
// 4. Copies the requested paths (e.g. "skills") into the plugin
// directory (plugins/jfrog/), replacing any existing tree.
//
// The pin in sync-skills-vendor.json is the single source of truth —
// there is no runtime override. To ship a different skill version,
// change the pin in a PR and commit the synced tree alongside it.

import { promises as fs, createWriteStream } from "node:fs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";

// filesystem helpers
async function readJson(filePath) {
return JSON.parse(await fs.readFile(filePath, "utf8"));
}

async function fileExists(filePath) {
try { await fs.access(filePath); return true; } catch { return false; }
}

// download the upstream tarball

// codeload.github.com serves any public repo's archive over HTTPS
// without auth, accepting a tag, branch, or commit SHA as the ref.
async function downloadTarball(repo, ref, destPath) {
const url = `https://codeload.github.com/${repo}/tar.gz/${encodeURIComponent(ref)}`;
const res = await fetch(url, { redirect: "follow" });
if (!res.ok) throw new Error(`Could not download ${repo}@${ref} (HTTP ${res.status})`);
await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
console.log(` fetched ${url}`);
}

// extract the tarball

// Shells out to the system `tar` instead of pulling in an npm tar library —
// keeps the script zero-dependency.
//
// GitHub tarballs always have exactly one top-level directory whose
// name encodes the repo + commit. We return that path so the caller
// knows where to find the extracted tree.
async function extractTarball(tarballPath, intoDir) {
await fs.mkdir(intoDir, { recursive: true });
const result = spawnSync("tar", ["-xzf", tarballPath, "-C", intoDir], { stdio: "inherit" });
if (result.status !== 0) throw new Error(`tar exited with status ${result.status}`);
const [topLevel] = await fs.readdir(intoDir);
return path.join(intoDir, topLevel);
}

// copy one path from the extracted tree into the plugin

// Removes the destination first so we never end up with stale leftovers
// from a previous sync, then creates the destination's parent directory then copies.
async function copyPath(fromDir, toDir, relativePath) {
const from = path.join(fromDir, relativePath);
const to = path.join(toDir, relativePath);
if (!(await fileExists(from))) {
throw new Error(`path missing in upstream tarball: ${relativePath}`);
}
await fs.rm(to, { recursive: true, force: true });
await fs.mkdir(path.dirname(to), { recursive: true });
await fs.cp(from, to, { recursive: true });
console.log(` ${relativePath} -> ${path.relative(process.cwd(), to)}`);
}

// Sync this plugin: read sync-skills-vendor.json, download + extract + copy.
//
// Paths are resolved relative to the script itself rather than CWD, so
// the script works regardless of where it's invoked from. The repo root
// is two levels up from .github/scripts/, and the plugin directory
// (where skills/ lives) is plugins/jfrog under that.
async function main() {
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..", "..");
const pluginDir = path.resolve(repoRoot, "plugins", "jfrog");
const vendorPath = path.join(scriptDir, "sync-skills-vendor.json");
if (!(await fileExists(vendorPath))) {
throw new Error(`missing sync-skills-vendor.json at ${vendorPath}`);
}

const { repo, pin, paths } = await readJson(vendorPath);
if (!repo || !pin || !Array.isArray(paths) || paths.length === 0) {
throw new Error(`${vendorPath} must define 'repo', 'pin' and a non-empty 'paths' array`);
}

console.log(`--- ${repo} (ref: ${pin}) ---`);

const workDir = await fs.mkdtemp(path.join(tmpdir(), "sync-skills-"));
try {
// `slug` is just a unique filename for this tarball + extract dir.
const slug = `${repo.replace("/", "-")}-${pin.replace(/[^A-Za-z0-9._-]/g, "_")}`;
const tarball = path.join(workDir, `${slug}.tar.gz`);
await downloadTarball(repo, pin, tarball);
const extracted = await extractTarball(tarball, path.join(workDir, slug));
for (const rel of paths) await copyPath(extracted, pluginDir, rel);
} finally {
await fs.rm(workDir, { recursive: true, force: true });
}
console.log("done.");
}

await main();
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
.env.*
!.env.example

# IDE settings
.idea/

# OS and editor
.DS_Store
*.swp
Expand Down
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ node scripts/validate-template.mjs
4. **Commit** with a clear, descriptive message.
5. Open a **pull request** against `main` with a summary of what changed and why.

## Updating the vendored skills

The `skills/` tree is vendored from [`jfrog/jfrog-skills`](https://github.com/jfrog/jfrog-skills) at the version pinned in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json). To pull a newer upstream release into this repo:

1. Bump `pin` in `.github/scripts/sync-skills-vendor.json` to the new tag (e.g. `v0.12.0`).
2. Run the sync script from the repo root:

```bash
node .github/scripts/sync-skills.mjs
```

It downloads the pinned tarball from `codeload.github.com`, extracts it, and replaces the directories listed in `paths` (today: `skills/`) under `plugins/jfrog/`.
3. Bump `version` in [`plugins/jfrog/.cursor-plugin/plugin.json`](plugins/jfrog/.cursor-plugin/plugin.json) so users actually receive the update — Cursor skips installs whose resolved version hasn't changed.
4. Commit the pin bump, the regenerated `plugins/jfrog/skills/` tree, and the version bump together, and open a PR.

See [`VENDOR.md`](VENDOR.md) for the full picture.

## Reporting Issues

Open a [GitHub issue](https://github.com/jfrog/cursor-plugin/issues) with:
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Before installing, make sure you have:
- **JFrog host URL and access token** — Your JFrog platform URL and a valid access token.
- **Cursor** — Installed with AI features enabled.
- **Node.js** (≥ 14) — with `npx` on your `PATH`.
- **JFrog CLI** (≥ 2.x, optional) — If missing, the agent will attempt to install it. Recommended for CLI-based operations (see [Authentication](#authentication)).
- **Skill runtime requirements** — `jf` CLI, `jq`, and `curl` on `PATH`, plus a configured JFrog instance. For the minimum versions, see the upstream skills [`Requirements`](https://github.com/jfrog/jfrog-skills/blob/v0.11.0/README.md#requirements). Configure the CLI with `jf config add` — see [Authentication](#authentication).
- **JFrog Platform access** (optional) — If you want to use the Agent Guard feature, your JFrog subscription needs to include the AI Catalog entitlement. Contact your JFrog account team if you're unsure whether it's enabled.
- **JFrog project** (optional) — If you want to use the Agent Guard feature.

Expand Down Expand Up @@ -109,6 +109,26 @@ See the [JFrog MCP Registry troubleshooting guide](https://docs.jfrog.com/ai-ml/

---

## Updating the vendored skills

The `skills/` tree is vendored from [`jfrog/jfrog-skills`](https://github.com/jfrog/jfrog-skills) at the version pinned in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json). To pull a newer upstream release into this repo:

1. Bump `pin` in `.github/scripts/sync-skills-vendor.json` to the new tag (e.g. `v0.12.0`).
2. Run the sync script from the repo root:

```bash
node .github/scripts/sync-skills.mjs
```

It downloads the pinned tarball from `codeload.github.com`, extracts it, and replaces the directories listed in `paths` (today: `skills/`) under `plugins/jfrog/`.
3. Bump `version` in [`plugins/jfrog/.cursor-plugin/plugin.json`](plugins/jfrog/.cursor-plugin/plugin.json) so users actually receive the update — Cursor skips installs whose resolved version hasn't changed.
4. Update the pinned-version link in the [Prerequisites](#prerequisites) section so the skill runtime requirements point at the new tag.
5. Commit the pin bump, the regenerated `plugins/jfrog/skills/` tree, the version bump, and the README link bump together, and open a PR.

See [`VENDOR.md`](VENDOR.md) for the full picture.

---

## Contributing

See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development workflow and pull-request expectations.
Expand Down
26 changes: 26 additions & 0 deletions VENDOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Vendored skills

The skill packages under `plugins/jfrog/skills/` are vendored from **[jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills)** and committed to `main`.

| | |
| --- | --- |
| **Repository** | https://github.com/jfrog/jfrog-skills |
| **Pinned release** | see `pin` in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json) |

Included directories: `jfrog/`, `jfrog-package-safety-and-download/` (as of the pinned release).

## Refreshing

When the upstream repo publishes a new release, refresh the vendored tree via a PR that:

1. Bumps `pin` in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json) to the new tag.
2. Re-syncs and commits the refreshed `plugins/jfrog/skills/` tree.
3. Bumps `version` in [`plugins/jfrog/.cursor-plugin/plugin.json`](plugins/jfrog/.cursor-plugin/plugin.json) so users actually receive the update (Cursor skips installs whose resolved version hasn't changed).

To regenerate the tree locally before opening the PR:

```bash
node .github/scripts/sync-skills.mjs
```

The script reads its sibling `sync-skills-vendor.json`, downloads the pinned upstream tarball from `codeload.github.com`, and replaces the directories listed in `paths` (today: `skills/`) under `plugins/jfrog/`.
13 changes: 0 additions & 13 deletions plugins/jfrog/skills/VENDOR.md

This file was deleted.

Empty file.
Loading