#!/usr/bin/env npx tsx /** * scripts/skill-sync.ts — Refresh all remote-sourced skills in the cache. * * Usage: * just skill-sync * * Iterates all non-local: sources in library.yaml, fetches the latest * content, and updates .agent/library-cache/. */ import fs from 'fs'; import path from 'path'; import https from 'https'; import { parse } from 'yaml'; import { PROJECT_ROOT } from '../src/config.js'; const LIBRARY_PATH = path.join(PROJECT_ROOT, 'agent', 'library.yaml'); const CACHE_DIR = path.join(PROJECT_ROOT, '.agent', 'library-cache'); interface Entry { id: string; source: string; } function sourceToUrl(src: string): string | null { if (src.startsWith('local:')) return null; if (src.startsWith('raw:')) return src.slice('raw:'.length); if (src.startsWith('codeberg:')) { const rest = src.slice('codeberg:'.length); const [owner, repo, ...fileParts] = rest.split('/'); return `https://codeberg.org/${owner}/${repo}/raw/branch/main/${fileParts.join('/')}`; } if (src.startsWith('github:')) { const rest = src.slice('github:'.length); const [owner, repo, ...fileParts] = rest.split('/'); return `https://raw.githubusercontent.com/${owner}/${repo}/main/${fileParts.join('/')}`; } return null; } function fetchUrl(url: string): Promise { return new Promise((resolve, reject) => { https.get(url, (res) => { if (res.statusCode === 301 || res.statusCode === 302) { fetchUrl(res.headers.location!).then(resolve).catch(reject); return; } if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode} fetching ${url}`)); return; } const chunks: Buffer[] = []; res.on('data', (c: Buffer) => chunks.push(c)); res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); res.on('error', reject); }).on('error', reject); }); } async function main(): Promise { if (!fs.existsSync(LIBRARY_PATH)) { console.error(`Library not found: ${LIBRARY_PATH}`); process.exit(1); } const lib = parse(fs.readFileSync(LIBRARY_PATH, 'utf-8')) as { skills?: Entry[]; features?: Entry[]; agents?: Entry[]; prompts?: Entry[]; }; const allEntries: Entry[] = [ ...(lib.skills ?? []), ...(lib.features ?? []), ...(lib.agents ?? []), ...(lib.prompts ?? []), ]; const remote = allEntries.filter((e) => !e.source.startsWith('local:')); if (remote.length === 0) { console.log('No remote sources in library — nothing to sync.'); return; } if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true }); let updated = 0; let failed = 0; for (const entry of remote) { const url = sourceToUrl(entry.source); if (!url) { console.warn(` [skip] ${entry.id}: unknown source prefix`); continue; } try { process.stdout.write(` Fetching ${entry.id}... `); const content = await fetchUrl(url); const safe = entry.source.replace(/[^a-zA-Z0-9._-]/g, '_'); fs.writeFileSync(path.join(CACHE_DIR, safe), content, 'utf-8'); console.log('ok'); updated++; } catch (err) { console.log(`FAILED (${(err as Error).message})`); failed++; } } console.log(`\nSync complete: ${updated} updated, ${failed} failed.`); if (failed > 0) process.exit(1); } main().catch((err: Error) => { console.error(err.message); process.exit(1); });