Files
antigravity-skills-reference/apps/web-app/refresh-skills-plugin.js
sck_0 45844de534 refactor: reorganize repo docs and tooling layout
Consolidate the repository into clearer apps, tools, and layered docs areas so contributors can navigate and maintain it more reliably. Align validation, metadata sync, and CI around the same canonical workflow to reduce drift across local checks and GitHub Actions.
2026-03-06 15:01:38 +01:00

299 lines
11 KiB
JavaScript

import https from 'https';
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, '..');
const UPSTREAM_REPO = 'https://github.com/sickn33/antigravity-awesome-skills.git';
const UPSTREAM_NAME = 'upstream';
const REPO_TAR_URL = 'https://github.com/sickn33/antigravity-awesome-skills/archive/refs/heads/main.tar.gz';
const REPO_ZIP_URL = 'https://github.com/sickn33/antigravity-awesome-skills/archive/refs/heads/main.zip';
const COMMITS_API_URL = 'https://api.github.com/repos/sickn33/antigravity-awesome-skills/commits/main';
const SHA_FILE = path.join(__dirname, '.last-sync-sha');
// ─── Utility helpers ───
const MIME_TYPES = {
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
'.json': 'application/json', '.md': 'text/markdown', '.txt': 'text/plain',
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
'.yaml': 'text/yaml', '.yml': 'text/yaml', '.xml': 'text/xml',
'.py': 'text/plain', '.sh': 'text/plain', '.bat': 'text/plain',
};
/** Check if git is available on this system. Cached after first check. */
let _gitAvailable = null;
function isGitAvailable() {
if (_gitAvailable !== null) return _gitAvailable;
try {
execSync('git --version', { stdio: 'ignore' });
// Also check we're inside a git repo
execSync('git rev-parse --git-dir', { cwd: ROOT_DIR, stdio: 'ignore' });
_gitAvailable = true;
} catch {
_gitAvailable = false;
}
return _gitAvailable;
}
/** Run a git command in the project root. */
function git(cmd) {
return execSync(`git ${cmd}`, { cwd: ROOT_DIR, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
}
/** Ensure the upstream remote exists. */
function ensureUpstream() {
const remotes = git('remote');
if (!remotes.split('\n').includes(UPSTREAM_NAME)) {
git(`remote add ${UPSTREAM_NAME} ${UPSTREAM_REPO}`);
console.log(`[Sync] Added upstream remote: ${UPSTREAM_REPO}`);
}
}
/** Download a file following HTTP redirects. */
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
const request = (url) => {
https.get(url, { headers: { 'User-Agent': 'antigravity-skills-app' } }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
request(res.headers.location);
return;
}
if (res.statusCode !== 200) {
reject(new Error(`Download failed with status ${res.statusCode}`));
return;
}
res.pipe(file);
file.on('finish', () => { file.close(); resolve(); });
}).on('error', (err) => { fs.unlink(dest, () => { }); reject(err); });
};
request(url);
});
}
/** Check latest commit SHA via GitHub API. */
function checkRemoteSha() {
return new Promise((resolve) => {
https.get(COMMITS_API_URL, {
headers: { 'User-Agent': 'antigravity-skills-app', 'Accept': 'application/vnd.github.v3+json' },
}, (res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
try {
if (res.statusCode === 200) {
resolve(JSON.parse(body).sha || null);
} else {
resolve(null);
}
} catch {
resolve(null);
}
});
}).on('error', () => resolve(null));
});
}
/** Copy folder recursively. */
function copyFolderSync(from, to) {
if (!fs.existsSync(to)) fs.mkdirSync(to, { recursive: true });
for (const element of fs.readdirSync(from)) {
const srcPath = path.join(from, element);
const destPath = path.join(to, element);
if (fs.lstatSync(srcPath).isFile()) {
fs.copyFileSync(srcPath, destPath);
} else {
copyFolderSync(srcPath, destPath);
}
}
}
// ─── Sync strategies ───
/**
* FAST PATH: Use git fetch + merge (only downloads delta).
* Typically completes in 5-15 seconds.
*/
async function syncWithGit() {
ensureUpstream();
const headBefore = git('rev-parse HEAD');
console.log('[Sync] Fetching from upstream (git)...');
git(`fetch ${UPSTREAM_NAME} main`);
const upstreamHead = git(`rev-parse ${UPSTREAM_NAME}/main`);
if (headBefore === upstreamHead) {
return { upToDate: true };
}
console.log('[Sync] Merging updates...');
try {
git(`merge ${UPSTREAM_NAME}/main --ff-only`);
} catch {
console.log('[Sync] Fast-forward failed, resetting to upstream...');
git(`reset --hard ${UPSTREAM_NAME}/main`);
}
return { upToDate: false };
}
/**
* FALLBACK: Download archive when git is not available.
* Tries tar.gz first (faster), falls back to zip if tar isn't available.
*/
async function syncWithArchive() {
// Check SHA first to skip if up to date
const remoteSha = await checkRemoteSha();
if (remoteSha) {
let storedSha = null;
if (fs.existsSync(SHA_FILE)) {
storedSha = fs.readFileSync(SHA_FILE, 'utf-8').trim();
}
if (storedSha === remoteSha) {
return { upToDate: true };
}
}
const tempDir = path.join(ROOT_DIR, 'update_temp');
// Try tar first, fall back to zip
let useTar = true;
try {
execSync('tar --version', { stdio: 'ignore' });
} catch {
useTar = false;
}
const archivePath = path.join(ROOT_DIR, useTar ? 'update.tar.gz' : 'update.zip');
try {
// 1. Download
console.log(`[Sync] Downloading (${useTar ? 'tar.gz' : 'zip'})...`);
await downloadFile(useTar ? REPO_TAR_URL : REPO_ZIP_URL, archivePath);
// 2. Extract
console.log('[Sync] Extracting...');
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
fs.mkdirSync(tempDir, { recursive: true });
if (useTar) {
execSync(`tar -xzf "${archivePath}" -C "${tempDir}"`, { stdio: 'ignore' });
} else if (process.platform === 'win32') {
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${tempDir}' -Force"`, { stdio: 'ignore' });
} else {
execSync(`unzip -o "${archivePath}" -d "${tempDir}"`, { stdio: 'ignore' });
}
// 3. Move skills to root
const extractedRoot = path.join(tempDir, 'antigravity-awesome-skills-main');
const srcSkills = path.join(extractedRoot, 'skills');
const srcIndex = path.join(extractedRoot, 'skills_index.json');
const destSkills = path.join(ROOT_DIR, 'skills');
const destIndex = path.join(ROOT_DIR, 'skills_index.json');
if (!fs.existsSync(srcSkills)) {
throw new Error('Skills folder not found in downloaded archive.');
}
console.log('[Sync] Updating skills...');
if (fs.existsSync(destSkills)) fs.rmSync(destSkills, { recursive: true, force: true });
fs.renameSync(srcSkills, destSkills);
if (fs.existsSync(srcIndex)) fs.copyFileSync(srcIndex, destIndex);
// Save SHA
if (remoteSha) fs.writeFileSync(SHA_FILE, remoteSha, 'utf-8');
return { upToDate: false };
} finally {
if (fs.existsSync(archivePath)) fs.unlinkSync(archivePath);
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
}
}
// ─── Vite Plugin ───
export default function refreshSkillsPlugin() {
return {
name: 'refresh-skills',
configureServer(server) {
// Serve /skills.json directly from ROOT_DIR
server.middlewares.use('/skills.json', (req, res, next) => {
const filePath = path.join(ROOT_DIR, 'skills_index.json');
if (fs.existsSync(filePath)) {
res.setHeader('Content-Type', 'application/json');
fs.createReadStream(filePath).pipe(res);
} else {
next();
}
});
// Serve /skills/* directly from ROOT_DIR/skills/
server.middlewares.use((req, res, next) => {
if (!req.url || !req.url.startsWith('/skills/')) return next();
const relativePath = decodeURIComponent(req.url.replace(/\?.*$/, ''));
const filePath = path.join(ROOT_DIR, relativePath);
const resolved = path.resolve(filePath);
if (!resolved.startsWith(path.join(ROOT_DIR, 'skills'))) return next();
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const ext = path.extname(filePath).toLowerCase();
res.setHeader('Content-Type', MIME_TYPES[ext] || 'application/octet-stream');
fs.createReadStream(filePath).pipe(res);
} else {
next();
}
});
// Sync API endpoint
server.middlewares.use('/api/refresh-skills', async (req, res) => {
res.setHeader('Content-Type', 'application/json');
try {
let result;
if (isGitAvailable()) {
console.log('[Sync] Using git (fast path)...');
result = await syncWithGit();
} else {
console.log('[Sync] Git not available, using archive download (slower)...');
result = await syncWithArchive();
}
if (result.upToDate) {
console.log('[Sync] ✅ Already up to date!');
res.end(JSON.stringify({ success: true, upToDate: true }));
return;
}
// Count skills
const indexPath = path.join(ROOT_DIR, 'skills_index.json');
let count = 0;
if (fs.existsSync(indexPath)) {
const data = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
count = Array.isArray(data) ? data.length : 0;
}
console.log(`[Sync] ✅ Successfully synced ${count} skills!`);
res.end(JSON.stringify({ success: true, upToDate: false, count }));
} catch (err) {
console.error('[Sync] ❌ Failed:', err.message);
res.statusCode = 500;
res.end(JSON.stringify({ success: false, error: err.message }));
}
});
}
};
}