first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
This commit is contained in:
Executable
+188
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env node
|
||||
// Maintainer tool: refresh the vendored HyperFrames skill in
|
||||
// `skills/hyperframes/` from the upstream `heygen-com/hyperframes`
|
||||
// publication.
|
||||
//
|
||||
// Why vendor instead of relying on `npx skills add`? Coverage. The
|
||||
// `skills` CLI only symlinks into a known list of agent dirs (Claude
|
||||
// Code, Codex, Cursor, Trae, Factory, etc.) — but OD supports a wider
|
||||
// agent set (Hermes, Kimi, Qwen, BYOK CLIs that aren't on `skills`'s
|
||||
// allowlist). By vendoring under `skills/hyperframes/` and routing the
|
||||
// content through OD's own skill scanner (which injects the SKILL.md
|
||||
// body into the system prompt), every OD-supported agent — including
|
||||
// BYOK setups — gets HyperFrames guidance uniformly.
|
||||
//
|
||||
// This script does NOT auto-merge. Reasons:
|
||||
// 1. We add an OD-specific frontmatter shim (od.mode/surface/preview/…)
|
||||
// and an "Open Design integration" section near the top of
|
||||
// SKILL.md. An auto-merge would either drop the shim (breaking OD
|
||||
// classification) or duplicate it on every sync.
|
||||
// 2. Upstream may rename references, restructure subdirs, or change
|
||||
// `triggers`. A human eye catches that in one read.
|
||||
//
|
||||
// What it DOES do:
|
||||
// - Run `npx skills add heygen-com/hyperframes -y` into a temp dir
|
||||
// - Diff the upstream `hyperframes/` subtree against the vendored copy
|
||||
// - Print a summary of changed files (added / modified / removed)
|
||||
// - Exit non-zero when there's drift, so you notice
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/sync-hyperframes-skill.mjs # show diff
|
||||
// node scripts/sync-hyperframes-skill.mjs --apply # NOT IMPLEMENTED;
|
||||
// always reviewed
|
||||
// by hand
|
||||
//
|
||||
// To actually apply: copy the upstream files in by hand, re-add the OD
|
||||
// frontmatter shim and the "Open Design integration" section.
|
||||
|
||||
import { execFile as execFileCb } from 'node:child_process';
|
||||
import { mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const VENDORED = path.join(REPO_ROOT, 'skills', 'hyperframes');
|
||||
|
||||
async function main() {
|
||||
const tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'od-hf-sync-'));
|
||||
try {
|
||||
console.log(`[sync] installing upstream into ${tmpRoot}`);
|
||||
// `-y` auto-accepts the install confirmation prompt; we install just
|
||||
// the `hyperframes` sub-skill (the main one we vendor) to keep the
|
||||
// probe focused.
|
||||
await execFile(
|
||||
'npx',
|
||||
['-y', 'skills', 'add', 'heygen-com/hyperframes', '-s', 'hyperframes', '-y'],
|
||||
{ cwd: tmpRoot, timeout: 90_000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
|
||||
const upstream = path.join(tmpRoot, '.agents', 'skills', 'hyperframes');
|
||||
if (!(await exists(upstream))) {
|
||||
console.error(
|
||||
`[sync] upstream not found at expected path: ${upstream}\n` +
|
||||
' The skills CLI may have changed where it installs to.',
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const upstreamFiles = await collect(upstream);
|
||||
const vendoredFiles = await collect(VENDORED);
|
||||
|
||||
const upstreamMap = new Map(upstreamFiles.map((f) => [f.rel, f]));
|
||||
const vendoredMap = new Map(vendoredFiles.map((f) => [f.rel, f]));
|
||||
|
||||
const added = [];
|
||||
const modified = [];
|
||||
const removed = [];
|
||||
|
||||
for (const [rel, up] of upstreamMap) {
|
||||
const ven = vendoredMap.get(rel);
|
||||
if (!ven) {
|
||||
added.push(rel);
|
||||
continue;
|
||||
}
|
||||
// SKILL.md gets local edits (frontmatter shim + OD integration
|
||||
// section), so a byte-for-byte compare always reports drift.
|
||||
// Compare only the body AFTER our injected section by matching
|
||||
// upstream's first H2 heading. Imperfect but useful as a hint.
|
||||
if (rel === 'SKILL.md') {
|
||||
const upstreamMarker = '\n## Approach\n';
|
||||
const upBody = up.text.includes(upstreamMarker)
|
||||
? up.text.slice(up.text.indexOf(upstreamMarker))
|
||||
: up.text;
|
||||
const venBody = ven.text.includes(upstreamMarker)
|
||||
? ven.text.slice(ven.text.indexOf(upstreamMarker))
|
||||
: ven.text;
|
||||
if (upBody !== venBody) modified.push(`${rel} (body after ## Approach)`);
|
||||
continue;
|
||||
}
|
||||
if (up.text !== ven.text) modified.push(rel);
|
||||
}
|
||||
for (const rel of vendoredMap.keys()) {
|
||||
if (!upstreamMap.has(rel)) removed.push(rel);
|
||||
}
|
||||
|
||||
if (added.length === 0 && modified.length === 0 && removed.length === 0) {
|
||||
console.log('[sync] vendored copy matches upstream — nothing to do.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('\n[sync] DRIFT DETECTED — review and update by hand.\n');
|
||||
if (added.length) {
|
||||
console.log(` Added (in upstream, missing locally):`);
|
||||
for (const r of added) console.log(` + ${r}`);
|
||||
}
|
||||
if (modified.length) {
|
||||
console.log(` Modified upstream:`);
|
||||
for (const r of modified) console.log(` ~ ${r}`);
|
||||
}
|
||||
if (removed.length) {
|
||||
console.log(` Removed upstream (still vendored locally):`);
|
||||
for (const r of removed) console.log(` - ${r}`);
|
||||
}
|
||||
console.log(
|
||||
'\n Upstream copy lives at:\n' +
|
||||
` ${upstream}\n` +
|
||||
' (script does not auto-apply — re-run with diff tools, then\n' +
|
||||
' commit the merge by hand. Re-add OD frontmatter shim if it\n' +
|
||||
' gets dropped during the merge.)',
|
||||
);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Best-effort cleanup. Leaves the upstream dir behind if the user
|
||||
// wants to inspect it in the failure path.
|
||||
if (process.env.OD_KEEP_HF_SYNC_TMP) {
|
||||
console.log(`[sync] OD_KEEP_HF_SYNC_TMP set — leaving ${tmpRoot}`);
|
||||
} else {
|
||||
await rm(tmpRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function exists(p) {
|
||||
try {
|
||||
await stat(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function collect(root) {
|
||||
const out = [];
|
||||
await walk(root, '', out);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function walk(root, rel, out) {
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(path.join(root, rel), { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
||||
if (e.isDirectory()) {
|
||||
await walk(root, childRel, out);
|
||||
continue;
|
||||
}
|
||||
if (!e.isFile()) continue;
|
||||
const text = await readFile(path.join(root, childRel), 'utf8').catch(
|
||||
() => null,
|
||||
);
|
||||
if (text == null) continue;
|
||||
out.push({ rel: childRel, text });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[sync] failed:', err && err.message ? err.message : err);
|
||||
process.exit(2);
|
||||
});
|
||||
Reference in New Issue
Block a user