diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json new file mode 100644 index 0000000..b4c4d06 --- /dev/null +++ b/.github/scripts/sync-skills-vendor.json @@ -0,0 +1,5 @@ +{ + "repo": "jfrog/jfrog-skills", + "pin": "v0.11.0", + "paths": ["skills"] +} diff --git a/.github/scripts/sync-skills.mjs b/.github/scripts/sync-skills.mjs new file mode 100755 index 0000000..73bb2aa --- /dev/null +++ b/.github/scripts/sync-skills.mjs @@ -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(); diff --git a/.gitignore b/.gitignore index 466f0c8..dc167fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ .env.* !.env.example +# IDE settings +.idea/ + # OS and editor .DS_Store *.swp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee1aa81..1b11640 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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: diff --git a/README.md b/README.md index 627aeb1..53f3739 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. diff --git a/VENDOR.md b/VENDOR.md new file mode 100644 index 0000000..96f2c48 --- /dev/null +++ b/VENDOR.md @@ -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/`. diff --git a/plugins/jfrog/skills/VENDOR.md b/plugins/jfrog/skills/VENDOR.md deleted file mode 100644 index c72b69e..0000000 --- a/plugins/jfrog/skills/VENDOR.md +++ /dev/null @@ -1,13 +0,0 @@ -# Vendored skills - -The skill packages in this directory are vendored from **[jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills)**. - -| | | -| --- | --- | -| **Repository** | https://github.com/jfrog/jfrog-skills | -| **Release** | [v0.11.0](https://github.com/jfrog/jfrog-skills/releases/tag/v0.11.0) | -| **Source commit** | `66e7d1d1e7b762bbf9e356d680511c4fb4ce231c` | - -Included directories: `jfrog/`, `jfrog-package-safety-and-download/`. - -To refresh: take the [latest release tarball](https://github.com/jfrog/jfrog-skills/releases/latest), replace those skill trees under `skills/`, and update this file with the new tag and commit SHA. diff --git a/plugins/jfrog/skills/jfrog/assets/.gitkeep b/plugins/jfrog/skills/jfrog/assets/.gitkeep new file mode 100644 index 0000000..e69de29