Files
stripe__ai/scripts/sync.js
T
John O'Sullivan 29f2b79419 Add Agent Plugins 1.0 provider package (#487)
* bootstrapping agent-plugins provider

* nesting into a  folder in case we start hosting more than one plugin in the future

* Align Agent Plugin paths with nested layout

Document the multi-plugin provider convention and keep sync automation pointed at the relocated portable package.

Co-authored-by: Cursor <cursoragent@cursor.com>
Committed-By-Agent: cursor

* changing installation instructions

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 17:15:09 -04:00

160 lines
5.4 KiB
JavaScript

const fs = require("fs").promises;
const path = require("path");
const { execSync } = require("child_process");
const BASE_URL = "https://docs.stripe.com/.well-known/skills";
const fetchText = (url) => {
try {
return execSync(
`curl -sf --user-agent "github.com/stripe/ai/skills" "${url}"`,
{ encoding: "utf8" },
);
} catch (err) {
throw new Error(`Failed to fetch ${url}: ${err.message}`);
}
};
const fetchManifest = () => {
const text = fetchText(`${BASE_URL}/index.json`);
try {
return JSON.parse(text);
} catch (err) {
throw new Error(`Failed to parse manifest: ${err.message}`);
}
};
const PRESERVE_FILES = new Set(["README.md", ".gitkeep"]);
const OMIT_FILES = new Set(["metadata.yaml"]);
const cleanDirectory = async (dir) => {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (PRESERVE_FILES.has(entry.name)) continue;
await fs.rm(path.join(dir, entry.name), { recursive: true, force: true });
}
};
const REPO_ROOT = path.join(__dirname, "..");
const SKILLS_DIR = path.join(__dirname, "../skills");
const PLUGIN_SKILLS_DIRS = [
path.join(__dirname, "../providers/claude/plugin/skills"),
path.join(__dirname, "../providers/codex/plugin/skills"),
path.join(__dirname, "../providers/cursor/plugin/skills"),
path.join(__dirname, "../providers/grok/plugin/skills"),
path.join(__dirname, "../providers/agent-plugins/plugin/skills"),
];
const ALL_OUTPUT_DIRS = [SKILLS_DIR, ...PLUGIN_SKILLS_DIRS];
const VERSION_FILES = [
path.join(__dirname, "../.claude-plugin/marketplace.json"),
path.join(__dirname, "../providers/claude/plugin/.claude-plugin/plugin.json"),
path.join(__dirname, "../providers/codex/plugin/.codex-plugin/plugin.json"),
path.join(__dirname, "../.cursor-plugin/marketplace.json"),
path.join(__dirname, "../providers/cursor/plugin/.cursor-plugin/plugin.json"),
path.join(__dirname, "../.grok-plugin/marketplace.json"),
path.join(__dirname, "../providers/grok/plugin/.grok-plugin/plugin.json"),
path.join(__dirname, "../providers/agent-plugins/plugin/plugin.json"),
];
const bumpVersion = (version, type) => {
const [major, minor, patch] = version.split(".").map(Number);
if (type === "minor") return `${major}.${minor + 1}.0`;
return `${major}.${minor}.${patch + 1}`;
};
const updateVersionFile = async (filePath, bumpType) => {
const raw = await fs.readFile(filePath, "utf8");
const content = JSON.parse(raw);
if (content.version) {
content.version = bumpVersion(content.version, bumpType);
}
if (content.plugins) {
for (const plugin of content.plugins) {
if (plugin.version) plugin.version = bumpVersion(plugin.version, bumpType);
}
}
await fs.writeFile(filePath, JSON.stringify(content, null, 2) + "\n", "utf8");
console.log(` Bumped version in: ${path.relative(REPO_ROOT, filePath)}`);
};
// Returns { added, deleted, modified } by inspecting git working-tree status
// for the canonical skills directory after files have been written.
const getGitSkillChanges = () => {
const skillsRel = path.relative(REPO_ROOT, SKILLS_DIR);
try {
const output = execSync(`git status --porcelain -- "${skillsRel}"`, {
cwd: REPO_ROOT,
encoding: "utf8",
});
const lines = output.trim().split("\n").filter(Boolean);
// porcelain format: "XY path" where X=index, Y=worktree; "??" = untracked
const added = lines.some((l) => l.startsWith("??"));
const deleted = lines.some((l) => l[1] === "D");
const modified = lines.some((l) => !l.startsWith("??") && l[1] !== "D");
return { added, deleted, modified };
} catch {
return { added: false, deleted: false, modified: false };
}
};
const run = async () => {
const manifest = fetchManifest();
const skills = manifest.skills;
console.log(`Found ${skills.length} skills`);
for (const dir of ALL_OUTPUT_DIRS) {
await fs.mkdir(dir, { recursive: true });
await cleanDirectory(dir);
}
let errors = 0;
for (const skill of skills) {
console.log(`Syncing skill: ${skill.name}`);
const skillFiles = skill.files.filter(fileName => !OMIT_FILES.has(fileName));
for (const file of skillFiles) {
const url = `${BASE_URL}/${skill.name}/${file}`;
let content;
try {
content = fetchText(url);
} catch (err) {
console.error(` Error: ${err.message}`);
errors++;
continue;
}
for (const dir of ALL_OUTPUT_DIRS) {
const outputPath = path.join(dir, skill.name, file);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, content, "utf8");
console.log(` Written: ${outputPath}`);
}
}
}
if (errors > 0) {
throw new Error(`Sync completed with ${errors} error(s)`);
}
const { added, deleted, modified } = getGitSkillChanges();
if (added || deleted || modified) {
const bumpType = added || deleted ? "minor" : "patch";
console.log(`Skills changed (type: ${bumpType}), bumping plugin versions`);
for (const versionFile of VERSION_FILES) {
await updateVersionFile(versionFile, bumpType);
}
} else {
console.log("No skill changes detected, skipping version bump");
}
};
run().catch((err) => {
console.error(err.message);
console.error(
"Encountered an error while fetching skills, skills will not be updated. Try triggering the workflow manually.",
);
process.exit(1);
});