Skip to content
129 changes: 129 additions & 0 deletions .github/scripts/sync-skills.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/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
// <plugin>/.vendor.json, then run this script to regenerate the
// plugin's skills/ tree, then commit both alongside each other.
//
// Usage:
// node .github/scripts/sync-skills.mjs
//
// Steps the script performs:
// 1. Reads marketplace.json and walks each plugin entry.
// 2. For each plugin, reads <plugin>/.vendor.json to learn which
// repo + ref to pull.
// 3. Downloads that tarball from codeload.github.com (public, no auth).
// 4. Extracts it into a temp directory.
// 5. Copies the requested paths (e.g. "skills") into the plugin folder,
// replacing any existing tree.
//
// The pin in .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";

// 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)}`);
}

// Resolves the plugin's local directory from the marketplace `source` field.
function localPluginDir(plugin) {
if (typeof plugin.source === "string") return plugin.source;
if (plugin.source && typeof plugin.source.path === "string") return plugin.source.path;
return null;
}

// Sync one plugin: read its .vendor.json, download + extract + copy.
// Plugins without a local path or without a .vendor.json are silently skipped.
async function syncPlugin(plugin, workDir) {
const localPath = localPluginDir(plugin);
if (!localPath) return;
const pluginDir = path.resolve(localPath);
const vendorPath = path.join(pluginDir, ".vendor.json");
if (!(await fileExists(vendorPath))) return;

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(`--- ${plugin.name} (ref: ${pin}) ---`);

// `slug` is just a unique filename for this plugin's tarball + extract.
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);
}

// Entry point: walk marketplace.json, sync each plugin sequentially,
// always clean up the temp work directory.
async function main() {
const marketplace = await readJson("marketplace.json");
const workDir = await fs.mkdtemp(path.join(tmpdir(), "sync-skills-"));
try {
for (const plugin of marketplace.plugins ?? []) {
await syncPlugin(plugin, workDir);
}
} finally {
await fs.rm(workDir, { recursive: true, force: true });
}
console.log("done.");
}

await main();
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# IDE settings
.idea/
61 changes: 61 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Contributing to the JFrog VS Code Plugin

Thank you for your interest in contributing! This project is maintained by JFrog and licensed under the [Apache License 2.0](LICENSE).

## Contributor License Agreement (CLA)

All contributors must sign the [JFrog CLA](https://jfrog.com/cla/) before contributions can be merged. A CLA check runs automatically on every pull request — follow the prompts to sign if you haven't already.

## How to Contribute

1. **Fork** the repository and create a feature branch from `main`.
2. Make your changes, ensuring they follow the existing code style and project conventions.
3. **Test** by loading the plugin from source — see [the README](README.md#installation) for setup instructions.
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 `plugin/skills/` tree is vendored from [jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills) and committed to `main` — see [`VENDOR.md`](VENDOR.md) for the full flow. To regenerate the tree locally against the pin in [`plugin/.vendor.json`](plugin/.vendor.json):

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

This downloads the pinned upstream tarball and replaces the contents of `plugin/skills/`. Commit the result alongside any pin/version bumps.

## Pre-release checklist

- [ ] Version bumped in [`plugin/.claude-plugin/plugin.json`](plugin/.claude-plugin/plugin.json) when the plugin changes.
- [ ] No secrets, credentials, or files under `**/local-cache/` committed.
- [ ] If the skill tree changed: `plugin/.vendor.json` `pin` matches the upstream tag the new tree was generated from.
- [ ] Smoke-test the plugin locally.

## Reporting Issues

Open a [GitHub issue](https://github.com/jfrog/vscode-plugin/issues) with:

- A clear title and description of the problem.
- Steps to reproduce (if applicable).
- Expected vs. actual behavior.

## Code Guidelines

- Keep changes focused — one logical change per PR.
- Follow existing patterns and naming conventions in the codebase.
- Do not commit secrets, credentials, or API keys.
- Add copyright headers to new source files:

```
// Copyright (c) JFrog Ltd. 2026
// Licensed under the Apache License, Version 2.0
// https://www.apache.org/licenses/LICENSE-2.0
```

## Code of Conduct

Be respectful and constructive. We are committed to providing a welcoming and inclusive experience for everyone.

## Questions?

Reach out to the JFrog DevRel team at <devrel@jfrog.com>.
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 `plugin/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 [`plugin/.vendor.json`](plugin/.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 [`plugin/.vendor.json`](plugin/.vendor.json) to the new tag.
2. Re-syncs and commits the refreshed `plugin/skills/` tree.
3. Bumps `version` in [`plugin/.claude-plugin/plugin.json`](plugin/.claude-plugin/plugin.json) so users actually receive the update (Claude Code/Copilot skip 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 `plugin/.vendor.json`, downloads the pinned upstream tarball from `codeload.github.com`, and replaces the directories listed in `paths` (today: `plugin/skills/`).
1 change: 0 additions & 1 deletion marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
{
"name": "jfrog",
"description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices",
"version": "1.0.1",
"source": "plugin",
"categories": ["security", "artifact-management", "supply-chain", "devops", "mcp", "mlops", "agent-guard", "ai-catalog"],
"platforms": ["darwin", "linux", "windows"],
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jfrog",
"description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices",
"version": "0.1.0",
"version": "0.1.1",
"author": { "name": "JFrog", "url": "https://jfrog.com" },
"hooks": "hooks/hooks.json"
}
5 changes: 5 additions & 0 deletions plugin/.vendor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"repo": "jfrog/jfrog-skills",
"pin": "v0.11.0",
"paths": ["skills"]
}
Loading