UNPKG

@frontify/fondue

Version:
225 lines (223 loc) 8.93 kB
#!/usr/bin/env node import { Command } from "commander"; import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, rmSync, mkdirSync, copyFileSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { homedir } from "node:os"; import { fileURLToPath } from "node:url"; const log = (message) => { process.stdout.write(`${message} `); }; const fail = (message, code = 1) => { process.stderr.write(`fondue: ${message} `); process.exit(code); }; const TOOL_DIR = dirname(fileURLToPath(import.meta.url)); const SKILL_SOURCE_DIR = resolve(TOOL_DIR, "adapters", "claude-skill", "skill"); const SKILL_NAME = "fondue"; const CONSUMER_PACKAGE = "@frontify/fondue"; const FONDUE_VERSION = "13.6.2"; const scopeOf = (options) => options.user ? "user" : "project"; const projectRoot = () => { let dir = process.cwd(); while (true) { if (existsSync(join(dir, "package.json")) || existsSync(join(dir, ".git"))) { return dir; } const parent = dirname(dir); if (parent === dir) { return process.cwd(); } dir = parent; } }; const skillsRoot = (scope) => scope === "user" ? join(homedir(), ".claude", "skills") : join(projectRoot(), ".claude", "skills"); const targetDirFor = (scope) => join(skillsRoot(scope), SKILL_NAME); const stampPathFor = (scope) => join(skillsRoot(scope), `.${SKILL_NAME}-installed.json`); const readStamp = (stampPath) => { if (!existsSync(stampPath)) { return { status: "missing" }; } try { const stamp = JSON.parse(readFileSync(stampPath, "utf8")); return { status: "ok", stamp }; } catch (error) { return { status: "corrupted", error: error instanceof Error ? error.message : String(error) }; } }; const writeStamp = (stampPath, scope) => { const stamp = { package: CONSUMER_PACKAGE, version: FONDUE_VERSION, installedAt: (/* @__PURE__ */ new Date()).toISOString(), scope }; writeFileSync(stampPath, `${JSON.stringify(stamp, null, 2)} `); }; const install = (options) => { const scope = scopeOf(options); const target = targetDirFor(scope); const stampPath = stampPathFor(scope); if (!existsSync(SKILL_SOURCE_DIR)) { fail(`Bundled skill directory missing at ${SKILL_SOURCE_DIR}. Reinstall ${CONSUMER_PACKAGE}.`); } const files = readdirSync(SKILL_SOURCE_DIR).filter((name) => statSync(join(SKILL_SOURCE_DIR, name)).isFile()); if (!files.includes("SKILL.md")) { fail(`Bundled skill is missing SKILL.md. Reinstall ${CONSUMER_PACKAGE}.`); } let previousVersion = null; if (existsSync(target)) { const read = readStamp(stampPath); if (read.status === "ok") { previousVersion = read.stamp.version; } else if (read.status === "corrupted" && !options.force) { fail( `Stamp file at ${stampPath} is unreadable (${read.error}). Inspect it, or pass --force to overwrite both the stamp and the skill.` ); } else if (read.status === "missing" && !options.force) { fail( `${target} exists and was not installed by this CLI. Remove it manually, or pass --force to overwrite.` ); } rmSync(target, { recursive: true, force: true }); } mkdirSync(target, { recursive: true }); for (const name of files) { copyFileSync(join(SKILL_SOURCE_DIR, name), join(target, name)); } writeStamp(stampPath, scope); if (previousVersion === null) { log(`Installed Fondue Claude Code skill (${CONSUMER_PACKAGE}@${FONDUE_VERSION}) → ${target}`); } else if (previousVersion === FONDUE_VERSION) { log(`Refreshed Fondue Claude Code skill at ${CONSUMER_PACKAGE}@${FONDUE_VERSION}${target}`); } else { log( `Updated Fondue Claude Code skill (${CONSUMER_PACKAGE}@${previousVersion} → @${FONDUE_VERSION}) → ${target}` ); } log(scope === "project" ? "Scope: project (this directory only)." : "Scope: user (every project)."); if (previousVersion === null) { log(""); log("Next steps:"); log(` • Open Claude Code in a project that has \`${CONSUMER_PACKAGE}\` installed.`); log(' • Ask "what Fondue component should I use for X?" — Claude picks the skill up automatically,'); log(` or invoke it explicitly with /${SKILL_NAME}.`); if (scope === "project") { log(` • Commit \`.claude/skills/${SKILL_NAME}/\` if you want it shared with your team.`); } } }; const status = (options) => { const scope = scopeOf(options); const target = targetDirFor(scope); const stampPath = stampPathFor(scope); const targetExists = existsSync(target); const read = readStamp(stampPath); if (!targetExists && read.status === "missing") { log(`Not installed at ${target}.`); return; } if (read.status === "ok") { if (!targetExists) { log(`Stamp at ${stampPath} but skill directory is missing at ${target}.`); log("Run `fondue adapter install claude-skill` to repair."); return; } log(`Installed at ${target}`); log(` package: ${read.stamp.package}@${read.stamp.version}`); log(` scope: ${read.stamp.scope}`); log(` installedAt: ${read.stamp.installedAt}`); return; } if (read.status === "corrupted") { log(`Stamp at ${stampPath} is unreadable (${read.error}).`); if (targetExists) { log(`Skill directory present at ${target}. Re-run install to repair, or pass --force.`); } return; } log(`Directory exists at ${target} but was not installed by this CLI.`); }; const uninstall = (options) => { const scope = scopeOf(options); const target = targetDirFor(scope); const stampPath = stampPathFor(scope); const targetExists = existsSync(target); const stampExists = existsSync(stampPath); if (!targetExists && !stampExists) { log(`Nothing to uninstall — no skill at ${target}.`); return; } if (targetExists) { const read = readStamp(stampPath); if (read.status === "missing" && !options.force) { fail(`${target} exists but was not installed by this CLI. Remove it manually, or pass --force.`); } if (read.status === "corrupted" && !options.force) { fail( `Stamp file at ${stampPath} is unreadable (${read.error}). Inspect it, or pass --force to remove the skill and the stamp.` ); } rmSync(target, { recursive: true, force: true }); } if (stampExists) { rmSync(stampPath, { force: true }); } log(`Removed ${target}.`); }; const claudeSkill = { name: "claude-skill", description: "Claude Code skill — teaches Claude to query the Fondue SDK directly", install, uninstall, status }; const ADAPTERS = [claudeSkill]; const adapterNames = () => ADAPTERS.map((i) => i.name).join(", "); const adapterCatalog = () => { const width = Math.max(...ADAPTERS.map((i) => i.name.length)); return ADAPTERS.map((i) => ` ${i.name.padEnd(width)} ${i.description}`).join("\n"); }; const requireAdapter = (name, verb) => { if (name === void 0) { return fail( `Specify which adapter to ${verb}. Available: ${adapterCatalog()} Example: fondue adapter ${verb} ${ADAPTERS[0]?.name ?? "<name>"}` ); } const adapter2 = ADAPTERS.find((i) => i.name === name); if (adapter2 !== void 0) { return adapter2; } return fail(`Unknown adapter "${name}". Available: ${adapterNames()}.`); }; const program = new Command().name("fondue").description("CLI for Frontify Fondue.").version("13.6.2"); const adapter = program.command("adapter").description("Install and manage Fondue SDK adapters (Claude Code skill, MCP, REST, …)."); adapter.command("install [name]").aliases(["add", "update"]).description( "Install an adapter, or refresh an existing install in place. Run `adapter list` to see what `<name>` accepts." ).option("--user", "Install for the current user instead of the current project").option("--force", "Overwrite a directory at the target path that was not installed by this CLI").action((name, options) => { requireAdapter(name, "install").install(options); }); adapter.command("uninstall [name]").alias("remove").description("Uninstall an adapter previously installed by this CLI.").option("--user", "Target the user-scope install").option("--force", "Force removal even if not installed by this CLI").action((name, options) => { requireAdapter(name, "uninstall").uninstall(options); }); adapter.command("status [name]").description("Show installation status for one adapter, or every adapter when no name is given.").option("--user", "Target the user-scope install").action((name, options) => { if (name !== void 0) { requireAdapter(name, "status").status(options); return; } for (const [index, registered] of ADAPTERS.entries()) { if (index > 0) { log(""); } log(`[${registered.name}]`); registered.status(options); } }); adapter.command("list").alias("ls").description("List every registered adapter and what it does.").action(() => { log(adapterCatalog()); }); program.parse();