refactor(cli): fetch skills from GitHub repos instead of bundling

Skills are now fetched directly from their source repos at runtime:
- HyperFrames skills: github.com/heygen-com/hyperframes (skills/ dir)
- GSAP skills: github.com/greensock/gsap-skills (skills/ dir)

Both cached in ~/.cache/hyperframes/ and updated on each run.
Removed skills bundling from build:copy step.

Requires the hyperframes repo to be public for HyperFrames skills
to install. GSAP skills work immediately (public repo).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-03-26 17:50:46 +00:00
committed by Vance Ingalls
co-authored by Claude Opus 4.6
parent 8658e550b2
commit 6957e2d8a8
2 changed files with 68 additions and 63 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
"build": "bun run build:studio && tsup && bun run build:runtime && bun run build:copy",
"build:studio": "cd ../studio && bun run build",
"build:runtime": "tsx scripts/build-runtime.ts",
"build:copy": "mkdir -p dist/studio dist/docs dist/templates dist/skills && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli dist/templates/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true) && (cp -r ../../skills/compose-video ../../skills/asset-management dist/skills/ 2>/dev/null || true)",
"build:copy": "mkdir -p dist/studio dist/docs dist/templates && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli dist/templates/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)",
"typecheck": "tsc --noEmit"
},
"dependencies": {
+67 -62
View File
@@ -1,9 +1,8 @@
import { defineCommand } from "citty";
import { existsSync, mkdirSync, readdirSync, rmSync, cpSync } from "node:fs";
import { join, resolve, dirname } from "node:path";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import * as clack from "@clack/prompts";
import { c } from "../ui/colors.js";
@@ -45,22 +44,35 @@ const TARGETS: Target[] = [
},
];
const GSAP_REPO = "https://github.com/greensock/gsap-skills.git";
const GSAP_CACHE = join(homedir(), ".cache", "hyperframes", "gsap-skills");
// ---------------------------------------------------------------------------
// Bundled HyperFrames skills
// Skill sources — all fetched from GitHub
// ---------------------------------------------------------------------------
function getBundledSkillsDir(): string {
const dir = dirname(fileURLToPath(import.meta.url));
const devPath = resolve(dir, "..", "..", "..", "..", "skills");
const builtPath = resolve(dir, "skills");
return existsSync(devPath) ? devPath : builtPath;
interface SkillSource {
name: string;
repo: string;
/** Subdirectory within the repo that contains skill folders */
skillsPath: string;
cache: string;
}
const SOURCES: SkillSource[] = [
{
name: "HyperFrames",
repo: "https://github.com/heygen-com/hyperframes.git",
skillsPath: "skills",
cache: join(homedir(), ".cache", "hyperframes", "hyperframes-skills"),
},
{
name: "GSAP",
repo: "https://github.com/greensock/gsap-skills.git",
skillsPath: "skills",
cache: join(homedir(), ".cache", "hyperframes", "gsap-skills"),
},
];
// ---------------------------------------------------------------------------
// GSAP skills — cloned from GitHub
// Git helpers
// ---------------------------------------------------------------------------
function hasGit(): boolean {
@@ -72,40 +84,40 @@ function hasGit(): boolean {
}
}
function fetchGsapSkills(): string {
if (existsSync(GSAP_CACHE)) {
function fetchRepo(source: SkillSource): string {
if (existsSync(source.cache)) {
try {
execSync("git pull --ff-only", { cwd: GSAP_CACHE, stdio: "ignore", timeout: 30_000 });
execSync("git pull --ff-only", { cwd: source.cache, stdio: "ignore", timeout: 30_000 });
} catch {
rmSync(GSAP_CACHE, { recursive: true, force: true });
execSync(`git clone --depth 1 ${GSAP_REPO} ${GSAP_CACHE}`, {
rmSync(source.cache, { recursive: true, force: true });
execSync(`git clone --depth 1 ${source.repo} ${source.cache}`, {
stdio: "ignore",
timeout: 60_000,
});
}
} else {
mkdirSync(dirname(GSAP_CACHE), { recursive: true });
execSync(`git clone --depth 1 ${GSAP_REPO} ${GSAP_CACHE}`, {
mkdirSync(dirname(source.cache), { recursive: true });
execSync(`git clone --depth 1 ${source.repo} ${source.cache}`, {
stdio: "ignore",
timeout: 60_000,
});
}
return GSAP_CACHE;
return join(source.cache, source.skillsPath);
}
// ---------------------------------------------------------------------------
// Install logic — generic per target directory
// Install logic
// ---------------------------------------------------------------------------
interface InstalledSkill {
name: string;
source: "hyperframes" | "gsap";
source: string;
}
function installSkillsFromDir(
sourceDir: string,
targetDir: string,
source: "hyperframes" | "gsap",
sourceName: string,
): InstalledSkill[] {
const installed: InstalledSkill[] = [];
if (!existsSync(sourceDir)) return installed;
@@ -120,7 +132,7 @@ function installSkillsFromDir(
if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true });
mkdirSync(destDir, { recursive: true });
cpSync(join(sourceDir, entry.name), destDir, { recursive: true });
installed.push({ name: entry.name, source });
installed.push({ name: entry.name, source: sourceName });
}
return installed;
}
@@ -131,39 +143,40 @@ function installSkillsFromDir(
function resolveTargets(args: Record<string, unknown>): Target[] {
const hasAnyFlag = TARGETS.some((t) => args[t.flag] === true);
if (hasAnyFlag) {
// Explicit flags — install only to those targets
return TARGETS.filter((t) => args[t.flag] === true);
}
// No flags — install to all default-enabled targets
return TARGETS.filter((t) => t.defaultEnabled);
}
async function runInstall({ args }: { args: Record<string, unknown> }): Promise<void> {
clack.intro(c.bold("hyperframes skills"));
if (!hasGit()) {
clack.log.error(c.error("git is required to install skills. Install git and retry."));
clack.outro(c.warn("No skills installed."));
return;
}
const targets = resolveTargets(args);
// 1. Gather skill sources
const bundledDir = getBundledSkillsDir();
const hasBundled = existsSync(bundledDir);
// 1. Fetch all skill sources
const fetched: { source: SkillSource; skillsDir: string }[] = [];
let gsapSourceDir: string | undefined;
if (!hasGit()) {
clack.log.warn(c.warn("git not found — skipping GSAP skills. Install git and retry."));
} else {
const gsapSpinner = clack.spinner();
gsapSpinner.start("Fetching GSAP skills from GitHub...");
for (const source of SOURCES) {
const spinner = clack.spinner();
spinner.start(`Fetching ${source.name} skills...`);
try {
const cacheDir = fetchGsapSkills();
const skillsRoot = join(cacheDir, "skills");
if (existsSync(skillsRoot)) gsapSourceDir = skillsRoot;
gsapSpinner.stop(c.success("GSAP skills fetched"));
const skillsDir = fetchRepo(source);
if (existsSync(skillsDir)) {
fetched.push({ source, skillsDir });
spinner.stop(c.success(`${source.name} skills fetched`));
} else {
spinner.stop(c.warn(`${source.name}: no skills directory found`));
}
} catch (err) {
gsapSpinner.stop(
c.warn(`Failed to fetch GSAP skills: ${err instanceof Error ? err.message : err}`),
spinner.stop(
c.warn(`Failed to fetch ${source.name}: ${err instanceof Error ? err.message : err}`),
);
}
}
@@ -178,33 +191,25 @@ async function runInstall({ args }: { args: Record<string, unknown> }): Promise<
mkdirSync(target.dir, { recursive: true });
let count = 0;
if (hasBundled) {
const hf = installSkillsFromDir(bundledDir, target.dir, "hyperframes");
count += hf.length;
// Only track for summary from first target to avoid duplicates
if (target === targets[0]) allInstalled.push(...hf);
}
if (gsapSourceDir) {
const gsap = installSkillsFromDir(gsapSourceDir, target.dir, "gsap");
count += gsap.length;
if (target === targets[0]) allInstalled.push(...gsap);
for (const { source, skillsDir } of fetched) {
const skills = installSkillsFromDir(skillsDir, target.dir, source.name);
count += skills.length;
if (target === targets[0]) allInstalled.push(...skills);
}
spinner.stop(c.success(`${count} skills → ${target.name} ${c.dim(target.dir)}`));
}
// 3. Summary
const hfNames = allInstalled.filter((s) => s.source === "hyperframes").map((s) => s.name);
const gsapNames = allInstalled.filter((s) => s.source === "gsap").map((s) => s.name);
console.log();
if (hfNames.length > 0) {
console.log(` ${c.dim("HyperFrames:")} ${hfNames.map((s) => c.accent(s)).join(", ")}`);
for (const source of SOURCES) {
const names = allInstalled.filter((s) => s.source === source.name).map((s) => s.name);
if (names.length > 0) {
const label = `${source.name}:`.padEnd(14);
console.log(` ${c.dim(label)} ${names.map((s) => c.accent(s)).join(", ")}`);
}
}
if (gsapNames.length > 0) {
console.log(` ${c.dim("GSAP:")} ${gsapNames.map((s) => c.accent(s)).join(", ")}`);
}
console.log(` ${c.dim("Targets:")} ${targets.map((t) => t.name).join(", ")}`);
console.log(` ${c.dim("Targets:")} ${targets.map((t) => t.name).join(", ")}`);
console.log();
clack.outro(