chore: import upstream snapshot with attribution
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
// Prebuild the beat-detection browser bundle into dist so `hyperframes beats`
|
||||
// works in the published CLI (which ships only dist, not source). Mirrors how
|
||||
// the runtime IIFE is shipped. headlessAnalyzer.ts loads this at runtime and
|
||||
// injects it into a headless page.
|
||||
import { build } from "esbuild";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const coreRoot = dirname(require.resolve("@hyperframes/core/package.json"));
|
||||
const entry = join(coreRoot, "src/beats/beatDetection.ts");
|
||||
|
||||
await build({
|
||||
stdin: {
|
||||
contents:
|
||||
`import { analyzeMusicFromBuffer } from ${JSON.stringify(entry)};\n` +
|
||||
`globalThis.__hfAnalyze = analyzeMusicFromBuffer;`,
|
||||
resolveDir: coreRoot,
|
||||
loader: "ts",
|
||||
},
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
platform: "browser",
|
||||
target: "es2020",
|
||||
outfile: "dist/beat-analyzer.global.js",
|
||||
});
|
||||
|
||||
console.log("built dist/beat-analyzer.global.js");
|
||||
@@ -0,0 +1,138 @@
|
||||
// Cross-platform replacement for the previous `mkdir -p … && cp -r …` shell
|
||||
// chain, which failed on Windows because `cp` doesn't accept `-r` there.
|
||||
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const CLI_ROOT = resolve(HERE, "..");
|
||||
const REPO_ROOT = resolve(CLI_ROOT, "..", "..");
|
||||
const DIST = join(CLI_ROOT, "dist");
|
||||
|
||||
// Studio's vite build clears its dist before rewriting it; don't start the
|
||||
// copy until both sentinels are present so we never observe a partial tree.
|
||||
const STUDIO_WAIT_TIMEOUT_MS = 30_000;
|
||||
const STUDIO_POLL_INTERVAL_MS = 250;
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
async function waitForStudioDist(dir) {
|
||||
const deadline = Date.now() + STUDIO_WAIT_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const entries = new Set(readdirSync(dir));
|
||||
// vite emits `assets/` before rewriting `index.html` at the end of the
|
||||
// build — so once both are present, the tree is complete.
|
||||
if (entries.has("index.html") && entries.has("assets")) return;
|
||||
} catch {
|
||||
// dir doesn't exist yet — vite will create it
|
||||
}
|
||||
await sleep(STUDIO_POLL_INTERVAL_MS);
|
||||
}
|
||||
throw new Error(`[build-copy] timed out waiting for studio dist at ${dir}`);
|
||||
}
|
||||
|
||||
function copyDir(src, dest) {
|
||||
cpSync(src, dest, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function copyDirContents(src, dest) {
|
||||
for (const entry of readdirSync(src)) {
|
||||
cpSync(join(src, entry), join(dest, entry), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function copyMdFiles(srcDir, destDir) {
|
||||
if (!existsSync(srcDir)) return;
|
||||
for (const name of readdirSync(srcDir)) {
|
||||
if (name.endsWith(".md")) {
|
||||
cpSync(join(srcDir, name), join(destDir, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
async function main() {
|
||||
for (const sub of ["studio", "docs", "templates", "skills", "docker"]) {
|
||||
mkdirSync(join(DIST, sub), { recursive: true });
|
||||
}
|
||||
mkdirSync(join(DIST, "commands"), { recursive: true });
|
||||
|
||||
const studioDist = resolve(CLI_ROOT, "..", "studio", "dist");
|
||||
await waitForStudioDist(studioDist);
|
||||
copyDirContents(studioDist, join(DIST, "studio"));
|
||||
|
||||
for (const tmpl of ["blank", "_shared"]) {
|
||||
copyDir(join(CLI_ROOT, "src", "templates", tmpl), join(DIST, "templates", tmpl));
|
||||
}
|
||||
|
||||
// Bundle warm-grain from the repo registry so the built CLI can scaffold it
|
||||
// offline and CI smoke tests pick up PR-branch changes before merge to main.
|
||||
const warmGrainSrc = join(REPO_ROOT, "registry", "examples", "warm-grain");
|
||||
if (existsSync(warmGrainSrc)) {
|
||||
copyDir(warmGrainSrc, join(DIST, "templates", "warm-grain"));
|
||||
}
|
||||
|
||||
// Skills bundled into the published CLI. Branches don't all carry the same
|
||||
// skills/ tree (it gets restructured), so each entry is existsSync-guarded:
|
||||
// a missing skill dir warns + skips instead of crashing the build.
|
||||
for (const skill of ["hyperframes", "hyperframes-cli", "gsap"]) {
|
||||
const src = join(REPO_ROOT, "skills", skill);
|
||||
if (!existsSync(src)) {
|
||||
console.warn(`[build-copy] skill not found, skipping: skills/${skill}`);
|
||||
continue;
|
||||
}
|
||||
copyDir(src, join(DIST, "skills", skill));
|
||||
}
|
||||
|
||||
const dockerfile = join(CLI_ROOT, "src", "docker", "Dockerfile.render");
|
||||
if (existsSync(dockerfile)) {
|
||||
cpSync(dockerfile, join(DIST, "docker", "Dockerfile.render"));
|
||||
}
|
||||
|
||||
const layoutAuditScript = join(CLI_ROOT, "src", "commands", "layout-audit.browser.js");
|
||||
if (existsSync(layoutAuditScript)) {
|
||||
cpSync(layoutAuditScript, join(DIST, "commands", "layout-audit.browser.js"));
|
||||
}
|
||||
|
||||
const contrastAuditScript = join(CLI_ROOT, "src", "commands", "contrast-audit.browser.js");
|
||||
if (existsSync(contrastAuditScript)) {
|
||||
cpSync(contrastAuditScript, join(DIST, "commands", "contrast-audit.browser.js"));
|
||||
}
|
||||
|
||||
const motionSampleScript = join(CLI_ROOT, "src", "commands", "motion-sample.browser.js");
|
||||
if (existsSync(motionSampleScript)) {
|
||||
cpSync(motionSampleScript, join(DIST, "commands", "motion-sample.browser.js"));
|
||||
}
|
||||
|
||||
// Player bundles for the standalone browser player used by `present` and
|
||||
// `play`. resolvePlayerPath/resolveSlideshowPath look for these alongside the
|
||||
// built CLI (dist/<name>.global.js), so they must ship in the package — the
|
||||
// monorepo-dev fallback paths don't exist once installed from npm. Without
|
||||
// this, `npx hyperframes present` fails with "@hyperframes/player not found".
|
||||
const playerDist = join(REPO_ROOT, "packages", "player", "dist");
|
||||
const playerGlobals = [
|
||||
[join(playerDist, "hyperframes-player.global.js"), join(DIST, "hyperframes-player.global.js")],
|
||||
[
|
||||
join(playerDist, "slideshow", "hyperframes-slideshow.global.js"),
|
||||
join(DIST, "hyperframes-slideshow.global.js"),
|
||||
],
|
||||
];
|
||||
for (const [src, dest] of playerGlobals) {
|
||||
if (existsSync(src)) {
|
||||
cpSync(src, dest);
|
||||
} else {
|
||||
console.warn(`[build-copy] player bundle not found, skipping: ${src}`);
|
||||
}
|
||||
}
|
||||
|
||||
copyMdFiles(join(CLI_ROOT, "src", "docs"), join(DIST, "docs"));
|
||||
|
||||
console.log("[build-copy] done");
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const target = "../producer/src/services/fontData.generated.ts";
|
||||
|
||||
if (existsSync(target)) {
|
||||
console.log("[build:fonts] skipped — fontData.generated.ts already exists");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
execSync("cd ../producer && tsx scripts/generate-font-data.ts", {
|
||||
stdio: "inherit",
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { copyFileSync, readFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const coreDistDir = resolve(__dirname, "../../core/dist");
|
||||
|
||||
// Read the pre-built manifest to find the IIFE artifact name
|
||||
const manifest = JSON.parse(readFileSync(resolve(coreDistDir, "hyperframe.manifest.json"), "utf8"));
|
||||
const iifeFileName = manifest.artifacts?.iife ?? "hyperframe.runtime.iife.js";
|
||||
|
||||
// Copy the pre-built artifacts from core/dist — these have matching SHA256
|
||||
// checksums. Do NOT regenerate via loadHyperframeRuntimeSource() as that
|
||||
// produces output without the trailing newline, causing a checksum mismatch.
|
||||
copyFileSync(resolve(coreDistDir, "hyperframe.manifest.json"), "dist/hyperframe.manifest.json");
|
||||
copyFileSync(resolve(coreDistDir, iifeFileName), `dist/${iifeFileName}`);
|
||||
|
||||
// Keep legacy name for backward compat (e.g. studio dev server)
|
||||
copyFileSync(resolve(coreDistDir, iifeFileName), "dist/hyperframe-runtime.js");
|
||||
@@ -0,0 +1,86 @@
|
||||
// Generate (or verify) skills-manifest.json (repo root) — the published
|
||||
// "latest" fingerprint of the HyperFrames skill bundle.
|
||||
//
|
||||
// bun run --cwd packages/cli gen:skills-manifest # write/update
|
||||
// bun run --cwd packages/cli gen:skills-manifest --check # verify only (CI)
|
||||
//
|
||||
// The manifest is just per-skill content hashes (no version / timestamp), so it
|
||||
// is fully deterministic: same skill content ⇒ byte-identical manifest. `--check`
|
||||
// exits non-zero when the committed manifest doesn't match current skill content.
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildManifest, MANIFEST_FILE, type SkillsManifest } from "../src/utils/skillsManifest.js";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(here, "..", "..", ".."); // packages/cli/scripts → repo root
|
||||
const skillsRoot = join(repoRoot, "skills");
|
||||
const outPath = join(repoRoot, MANIFEST_FILE);
|
||||
const isCheck = process.argv.includes("--check");
|
||||
|
||||
/** Stable signature of the content hashes (order-independent). */
|
||||
function signature(skills: SkillsManifest["skills"]): string {
|
||||
return Object.keys(skills)
|
||||
.sort()
|
||||
.map((name) => `${name}:${skills[name]!.hash}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function driftLine(name: string, oldHash?: string, newHash?: string): string | null {
|
||||
if (oldHash === newHash) return null;
|
||||
if (!oldHash) return ` + ${name} (new)`;
|
||||
if (!newHash) return ` - ${name} (removed)`;
|
||||
return ` ~ ${name} (${oldHash} → ${newHash})`;
|
||||
}
|
||||
|
||||
function hashOf(skills: SkillsManifest["skills"], name: string): string | undefined {
|
||||
return skills[name]?.hash;
|
||||
}
|
||||
|
||||
function reportDrift(fresh: SkillsManifest, committed: SkillsManifest | null): void {
|
||||
const oldSkills = committed === null ? {} : committed.skills;
|
||||
const names = [...new Set([...Object.keys(fresh.skills), ...Object.keys(oldSkills)])].sort();
|
||||
for (const name of names) {
|
||||
const line = driftLine(name, hashOf(oldSkills, name), hashOf(fresh.skills, name));
|
||||
if (line) console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
const fresh = buildManifest(skillsRoot, { source: "heygen-com/hyperframes" });
|
||||
|
||||
// Read the committed manifest directly (no existsSync precheck) so there's no
|
||||
// check-then-write race on outPath — a missing or unreadable file just means
|
||||
// "no committed manifest yet", and we write a fresh one below.
|
||||
let committed: SkillsManifest | null = null;
|
||||
try {
|
||||
committed = JSON.parse(readFileSync(outPath, "utf8")) as SkillsManifest;
|
||||
} catch {
|
||||
committed = null;
|
||||
}
|
||||
|
||||
const inSync = committed !== null && signature(committed.skills) === signature(fresh.skills);
|
||||
const count = Object.keys(fresh.skills).length;
|
||||
|
||||
if (isCheck) {
|
||||
if (inSync) {
|
||||
console.log(`✓ ${MANIFEST_FILE} is in sync (${count} skills)`);
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(`✗ ${MANIFEST_FILE} is out of date — a skill changed without regenerating it.`);
|
||||
reportDrift(fresh, committed);
|
||||
console.error(
|
||||
`\nRun: bun run --cwd packages/cli gen:skills-manifest (then commit ${MANIFEST_FILE})`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Write mode — churn-free: only rewrite when a content hash actually changed.
|
||||
if (inSync) {
|
||||
console.log(`${MANIFEST_FILE} already in sync — no change (${count} skills)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
writeFileSync(outPath, JSON.stringify(fresh, null, 2) + "\n");
|
||||
console.log(`Wrote ${outPath} (${count} skills)`);
|
||||
reportDrift(fresh, committed);
|
||||
@@ -0,0 +1,169 @@
|
||||
// Generate (or verify) packages/cli/src/utils/agentDirs.generated.ts — the
|
||||
// home-relative GLOBAL skills directory for every agent the upstream
|
||||
// vercel-labs/skills CLI knows about, plus a marker dir that means "this agent
|
||||
// is installed on this machine".
|
||||
//
|
||||
// bun run --cwd packages/cli gen:agent-dirs # write/update (fetches upstream)
|
||||
// bun run --cwd packages/cli gen:agent-dirs --check # verify only (CI / pre-commit)
|
||||
// bun packages/cli/scripts/sync-agent-dirs.ts --src <path/to/agents.ts> # offline
|
||||
//
|
||||
// Why generated, not hand-maintained: `skills add --global` installs into the
|
||||
// per-agent dirs encoded in upstream's `src/agents.ts` (~70 agents). We mirror
|
||||
// the canonical store into those same dirs, so the list must track upstream. The
|
||||
// `skills` npm package exports nothing importable (CLI-only, bundled dist), so
|
||||
// we parse the source at a PINNED tag and commit the result — deterministic at
|
||||
// runtime, no network at install time. Bump SKILLS_REF and re-run on upgrade.
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Pin to the upstream release whose dir layout we install against. Bump this
|
||||
// (and re-run) when the bundled `skills` version moves.
|
||||
const SKILLS_REPO = "vercel-labs/skills";
|
||||
const SKILLS_REF = "v1.5.13";
|
||||
const AGENTS_TS_URL = `https://raw.githubusercontent.com/${SKILLS_REPO}/${SKILLS_REF}/src/agents.ts`;
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const outPath = join(here, "..", "src", "utils", "agentDirs.generated.ts");
|
||||
|
||||
const isCheck = process.argv.includes("--check");
|
||||
const srcFlag = process.argv.indexOf("--src");
|
||||
const srcPath = srcFlag !== -1 ? process.argv[srcFlag + 1] : undefined;
|
||||
|
||||
// The base directories agents.ts builds globalSkillsDir from. Each is an env
|
||||
// override with a documented default — the mirror resolves them at runtime so a
|
||||
// machine with XDG_CONFIG_HOME / CODEX_HOME / CLAUDE_CONFIG_DIR set lands in the
|
||||
// same place the agent actually reads. We store the base NAME + suffix here
|
||||
// rather than a frozen path so that resolution stays faithful to upstream.
|
||||
const BASE_VARS = [
|
||||
"home",
|
||||
"configHome",
|
||||
"codexHome",
|
||||
"claudeHome",
|
||||
"vibeHome",
|
||||
"hermesHome",
|
||||
"autohandHome",
|
||||
] as const;
|
||||
type BaseVar = (typeof BASE_VARS)[number];
|
||||
|
||||
interface AgentGlobalDir {
|
||||
agent: string;
|
||||
base: BaseVar;
|
||||
sub: string;
|
||||
}
|
||||
|
||||
function posixJoin(parts: string[]): string {
|
||||
return parts
|
||||
.flatMap((p) => p.split("/"))
|
||||
.filter((s) => s && s !== ".")
|
||||
.join("/");
|
||||
}
|
||||
|
||||
/** Resolve a globalSkillsDir expression to { base, sub }, or null if none. */
|
||||
function resolveGlobalExpr(expr: string): { base: BaseVar; sub: string } | null {
|
||||
const e = expr.trim();
|
||||
if (e === "undefined") return null; // agent defines no global skills dir
|
||||
// openclaw's helper falls back to ~/.openclaw/skills when no variant is present.
|
||||
if (e.startsWith("getOpenClawGlobalSkillsDir")) return { base: "home", sub: ".openclaw/skills" };
|
||||
const m = e.match(/^join\(([\s\S]+)\)$/);
|
||||
if (!m) throw new Error(`Unparseable globalSkillsDir: ${expr}`);
|
||||
const args = m[1]!.split(",").map((a) => a.trim());
|
||||
const first = args[0]!;
|
||||
if (!(BASE_VARS as readonly string[]).includes(first)) {
|
||||
throw new Error(`globalSkillsDir does not start with a known base var: ${expr}`);
|
||||
}
|
||||
const segs: string[] = [];
|
||||
for (const raw of args.slice(1)) {
|
||||
const lit = raw.match(/^['"]([^'"]*)['"]$/);
|
||||
if (!lit) throw new Error(`Non-literal segment "${raw}" in globalSkillsDir: ${expr}`);
|
||||
segs.push(lit[1]!);
|
||||
}
|
||||
return { base: first as BaseVar, sub: posixJoin(segs) };
|
||||
}
|
||||
|
||||
function parseAgents(source: string): AgentGlobalDir[] {
|
||||
const re = /^ {2}(?:"([a-z0-9-]+)"|'([a-z0-9-]+)'|([a-z0-9-]+)):\s*\{/gm;
|
||||
const blocks: { key: string; pos: number }[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(source))) blocks.push({ key: m[1] || m[2] || m[3]!, pos: m.index });
|
||||
if (blocks.length < 60) {
|
||||
throw new Error(`Parsed only ${blocks.length} agent blocks — upstream layout likely changed`);
|
||||
}
|
||||
|
||||
const out: AgentGlobalDir[] = [];
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const seg = source.slice(blocks[i]!.pos, blocks[i + 1] ? blocks[i + 1]!.pos : source.length);
|
||||
const gd = seg.match(/globalSkillsDir:\s*([\s\S]+?),\n/);
|
||||
const expr = gd ? gd[1]!.trim().replace(/\s+/g, " ") : "undefined";
|
||||
const resolved = resolveGlobalExpr(expr);
|
||||
if (resolved === null) continue; // no global dir (e.g. eve, promptscript)
|
||||
out.push({ agent: blocks[i]!.key, base: resolved.base, sub: resolved.sub });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function render(rows: AgentGlobalDir[]): string {
|
||||
const lines = rows.map(
|
||||
(r) =>
|
||||
` { agent: ${JSON.stringify(r.agent)}, base: ${JSON.stringify(r.base)}, sub: ${JSON.stringify(r.sub)} },`,
|
||||
);
|
||||
return `// @generated by packages/cli/scripts/sync-agent-dirs.ts — DO NOT EDIT.
|
||||
// Source: ${SKILLS_REPO}@${SKILLS_REF} (src/agents.ts). Regenerate with:
|
||||
// bun run --cwd packages/cli gen:agent-dirs
|
||||
//
|
||||
// Each entry is one agent the upstream \`skills\` CLI installs to. The agent's
|
||||
// GLOBAL skills directory is \`join(<base>, <sub>)\`, where \`base\` is one of the
|
||||
// env-overridable home dirs below (resolved at runtime by skillsMirror.ts, so
|
||||
// XDG_CONFIG_HOME / CODEX_HOME / CLAUDE_CONFIG_DIR are honored). Agents with no
|
||||
// global skills dir upstream (eve, promptscript) are omitted.
|
||||
|
||||
/** Env-overridable base dirs, matching upstream agents.ts. */
|
||||
export type AgentDirBase =
|
||||
${BASE_VARS.map((b) => ` | ${JSON.stringify(b)}`).join("\n")};
|
||||
|
||||
export interface AgentGlobalDir {
|
||||
/** Upstream agent key. */
|
||||
agent: string;
|
||||
/** Base directory the global skills dir is rooted at. */
|
||||
base: AgentDirBase;
|
||||
/** POSIX suffix joined onto the resolved base. */
|
||||
sub: string;
|
||||
}
|
||||
|
||||
export const AGENT_GLOBAL_DIRS: readonly AgentGlobalDir[] = [
|
||||
${lines.join("\n")}
|
||||
];
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadAgentsSource(): Promise<string> {
|
||||
if (srcPath) return readFileSync(srcPath, "utf8");
|
||||
const res = await fetch(AGENTS_TS_URL);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${AGENTS_TS_URL}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
const source = await loadAgentsSource();
|
||||
const rows = parseAgents(source);
|
||||
const next = render(rows);
|
||||
|
||||
if (isCheck) {
|
||||
let current = "";
|
||||
try {
|
||||
current = readFileSync(outPath, "utf8");
|
||||
} catch {
|
||||
/* missing → drift */
|
||||
}
|
||||
if (current !== next) {
|
||||
console.error(
|
||||
`agentDirs.generated.ts is out of date (parsed ${rows.length} agents from ${SKILLS_REPO}@${SKILLS_REF}).\n` +
|
||||
`Run: bun run --cwd packages/cli gen:agent-dirs`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`agentDirs.generated.ts is up to date (${rows.length} agents).`);
|
||||
} else {
|
||||
writeFileSync(outPath, next, "utf8");
|
||||
console.log(`Wrote ${rows.length} agents → ${outPath}`);
|
||||
}
|
||||
Reference in New Issue
Block a user