- agent/library.yaml: catalog of all 48 skills (37 operational + 11 features), 4 agents with skill assignments, 2 prompt refs - src/skill-library.ts: loadLibrary, searchSkills, getSkillContent, getAgentSkills, validateLibrary, reloadLibrary - scripts/skill-list.ts: grouped table output with color, optional search query - scripts/skill-add.ts: add skill from local/codeberg/github/raw source - scripts/skill-sync.ts: refresh all remote-sourced skills in cache - scripts/validate-library.ts: validate all local: sources exist on disk - .agent/identities/: COORDINATOR, SYSADMIN, DB_ADMIN, GIT_ADMIN stubs - .agent/context/FREEBSD.md: FreeBSD gotchas context for agents Typecheck passes. `just skill-list` and `just skill-search` ready to wire up in the justfile (Phase 4). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- Build: pass | Tests: pass — Tests 942 passed (942)
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
#!/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<string> {
|
|
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<void> {
|
|
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);
|
|
});
|