@frontify/fondue
Version:
Design system of Frontify
225 lines (223 loc) • 8.93 kB
JavaScript
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();