Transformers-tenets / app /scripts /sync-template.mjs
Molbap's picture
Molbap HF Staff
push a bunch of updates
e903a32
raw
history blame
11.7 kB
#!/usr/bin/env node
/**
* Template synchronization script for research-article-template
*
* This script:
* 1. Clones or updates the template repo in a temporary directory
* 2. Copies all files EXCEPT those in ./src/content which contain specific content
* 3. Preserves important local configuration files
* 4. Creates backups of files that will be overwritten
*
* Usage: npm run sync:template [--dry-run] [--backup] [--force]
*/
import { execSync } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const APP_ROOT = path.resolve(__dirname, '..');
const PROJECT_ROOT = path.resolve(APP_ROOT, '..');
const TEMP_DIR = path.join(PROJECT_ROOT, '.temp-template-sync');
const TEMPLATE_REPO = 'https://huggingface.co/spaces/tfrere/research-article-template';
// Files and directories to PRESERVE (do not overwrite)
const PRESERVE_PATHS = [
// Project-specific content
'app/src/content',
// Public data (symlink to our data) - CRITICAL: preserve this symlink
'app/public/data',
// Local configuration
'app/package-lock.json',
'app/node_modules',
// Project-specific scripts (preserve our sync script)
'app/scripts/sync-template.mjs',
// Project configuration files
'README.md',
'tools',
// Backup and temporary files
'.backup-*',
'.temp-*',
// Git
'.git',
'.gitignore'
];
// Files to handle with caution (require confirmation)
const SENSITIVE_FILES = [
'app/package.json',
'app/astro.config.mjs',
'Dockerfile',
'nginx.conf'
];
const args = process.argv.slice(2);
const isDryRun = args.includes('--dry-run');
const shouldBackup = args.includes('--backup'); // Disabled by default, use --backup to enable
const isForce = args.includes('--force');
console.log('🔄 Template synchronization script for research-article-template');
console.log(`📁 Working directory: ${PROJECT_ROOT}`);
console.log(`🎯 Template source: ${TEMPLATE_REPO}`);
if (isDryRun) console.log('🔍 DRY-RUN mode enabled - no files will be modified');
if (shouldBackup) console.log('💾 Backup enabled');
if (!shouldBackup) console.log('🚫 Backup disabled (use --backup to enable)');
console.log('');
async function executeCommand(command, options = {}) {
try {
if (isDryRun && !options.allowInDryRun) {
console.log(`[DRY-RUN] Command: ${command}`);
return '';
}
console.log(`$ ${command}`);
const result = execSync(command, {
encoding: 'utf8',
cwd: options.cwd || PROJECT_ROOT,
stdio: options.quiet ? 'pipe' : 'inherit'
});
return result;
} catch (error) {
console.error(`❌ Error during execution: ${command}`);
console.error(error.message);
throw error;
}
}
async function pathExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async function isPathPreserved(relativePath) {
return PRESERVE_PATHS.some(preserve =>
relativePath === preserve ||
relativePath.startsWith(preserve + '/')
);
}
async function createBackup(filePath) {
if (!shouldBackup || isDryRun) return;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${filePath}.backup-${timestamp}`;
try {
await fs.copyFile(filePath, backupPath);
console.log(`💾 Backup created: ${path.relative(PROJECT_ROOT, backupPath)}`);
} catch (error) {
console.warn(`⚠️ Unable to create backup for ${filePath}: ${error.message}`);
}
}
async function syncFile(sourcePath, targetPath) {
const relativeTarget = path.relative(PROJECT_ROOT, targetPath);
// Check if the file should be preserved
if (await isPathPreserved(relativeTarget)) {
console.log(`🔒 PRESERVED: ${relativeTarget}`);
return;
}
// Check if it's a sensitive file
if (SENSITIVE_FILES.includes(relativeTarget)) {
if (!isForce) {
console.log(`⚠️ SENSITIVE (ignored): ${relativeTarget} (use --force to overwrite)`);
return;
} else {
console.log(`⚠️ SENSITIVE (forced): ${relativeTarget}`);
}
}
// Check if target file is a symbolic link to preserve
if (await pathExists(targetPath)) {
try {
const targetStats = await fs.lstat(targetPath);
if (targetStats.isSymbolicLink()) {
console.log(`🔗 SYMLINK TARGET (preserved): ${relativeTarget}`);
return;
}
} catch (error) {
console.warn(`⚠️ Impossible de vérifier ${targetPath}: ${error.message}`);
}
}
// Create backup if file already exists (and is not a symbolic link)
if (await pathExists(targetPath)) {
try {
const stats = await fs.lstat(targetPath);
if (!stats.isSymbolicLink()) {
await createBackup(targetPath);
}
} catch (error) {
console.warn(`⚠️ Impossible de vérifier ${targetPath}: ${error.message}`);
}
}
if (isDryRun) {
console.log(`[DRY-RUN] COPY: ${relativeTarget}`);
return;
}
// Ensure the parent directory exists
await fs.mkdir(path.dirname(targetPath), { recursive: true });
// Check if source is a symbolic link
try {
const sourceStats = await fs.lstat(sourcePath);
if (sourceStats.isSymbolicLink()) {
console.log(`🔗 SYMLINK SOURCE (ignored): ${relativeTarget}`);
return;
}
} catch (error) {
console.warn(`⚠️ Unable to check source ${sourcePath}: ${error.message}`);
return;
}
// Remove target file if it exists (to handle symbolic links)
if (await pathExists(targetPath)) {
await fs.rm(targetPath, { recursive: true, force: true });
}
// Copier le fichier
await fs.copyFile(sourcePath, targetPath);
console.log(`✅ COPIED: ${relativeTarget}`);
}
async function syncDirectory(sourceDir, targetDir) {
const items = await fs.readdir(sourceDir, { withFileTypes: true });
for (const item of items) {
const sourcePath = path.join(sourceDir, item.name);
const targetPath = path.join(targetDir, item.name);
const relativeTarget = path.relative(PROJECT_ROOT, targetPath);
if (await isPathPreserved(relativeTarget)) {
console.log(`🔒 DOSSIER PRÉSERVÉ: ${relativeTarget}/`);
continue;
}
if (item.isDirectory()) {
if (!isDryRun) {
await fs.mkdir(targetPath, { recursive: true });
}
await syncDirectory(sourcePath, targetPath);
} else {
await syncFile(sourcePath, targetPath);
}
}
}
async function cloneOrUpdateTemplate() {
console.log('📥 Fetching template...');
// Nettoyer le dossier temporaire s'il existe
if (await pathExists(TEMP_DIR)) {
await fs.rm(TEMP_DIR, { recursive: true, force: true });
if (isDryRun) {
console.log(`[DRY-RUN] Suppression: ${TEMP_DIR}`);
}
}
// Clone template repo (even in dry-run to be able to compare)
await executeCommand(`git clone ${TEMPLATE_REPO} "${TEMP_DIR}"`, { allowInDryRun: true });
return TEMP_DIR;
}
async function ensureDataSymlink() {
const dataSymlinkPath = path.join(APP_ROOT, 'public', 'data');
const dataSourcePath = path.join(APP_ROOT, 'src', 'content', 'assets', 'data');
// Check if symlink exists and is correct
if (await pathExists(dataSymlinkPath)) {
try {
const stats = await fs.lstat(dataSymlinkPath);
if (stats.isSymbolicLink()) {
const target = await fs.readlink(dataSymlinkPath);
const expectedTarget = path.relative(path.dirname(dataSymlinkPath), dataSourcePath);
if (target === expectedTarget) {
console.log('🔗 Data symlink is correct');
return;
} else {
console.log(`⚠️ Data symlink points to wrong target: ${target} (expected: ${expectedTarget})`);
}
} else {
console.log('⚠️ app/public/data exists but is not a symlink');
}
} catch (error) {
console.log(`⚠️ Error checking symlink: ${error.message}`);
}
}
// Recreate symlink
if (!isDryRun) {
if (await pathExists(dataSymlinkPath)) {
await fs.rm(dataSymlinkPath, { recursive: true, force: true });
}
await fs.symlink(path.relative(path.dirname(dataSymlinkPath), dataSourcePath), dataSymlinkPath);
console.log('✅ Data symlink recreated');
} else {
console.log('[DRY-RUN] Would recreate data symlink');
}
}
async function showSummary(templateDir) {
console.log('\n📊 SYNCHRONIZATION SUMMARY');
console.log('================================');
console.log('\n🔒 Preserved files/directories:');
for (const preserve of PRESERVE_PATHS) {
const fullPath = path.join(PROJECT_ROOT, preserve);
if (await pathExists(fullPath)) {
console.log(` ✓ ${preserve}`);
} else {
console.log(` - ${preserve} (n'existe pas)`);
}
}
console.log('\n⚠️ Sensitive files (require --force):');
for (const sensitive of SENSITIVE_FILES) {
const fullPath = path.join(PROJECT_ROOT, sensitive);
if (await pathExists(fullPath)) {
console.log(` ! ${sensitive}`);
}
}
if (isDryRun) {
console.log('\n🔍 To execute for real: npm run sync:template');
console.log('🔧 To force sensitive files: npm run sync:template -- --force');
}
}
async function cleanup() {
console.log('\n🧹 Cleaning up...');
if (await pathExists(TEMP_DIR)) {
if (!isDryRun) {
await fs.rm(TEMP_DIR, { recursive: true, force: true });
}
console.log(`🗑️ Temporary directory removed: ${TEMP_DIR}`);
}
}
async function main() {
try {
// Verify we're in the correct directory
const packageJsonPath = path.join(APP_ROOT, 'package.json');
if (!(await pathExists(packageJsonPath))) {
throw new Error(`Package.json not found in ${APP_ROOT}. Are you in the correct directory?`);
}
// Clone the template
const templateDir = await cloneOrUpdateTemplate();
// Synchroniser
console.log('\n🔄 Synchronisation en cours...');
await syncDirectory(templateDir, PROJECT_ROOT);
// Ensure the data symlink is correct
console.log('\n🔗 Vérification du lien symbolique des données...');
await ensureDataSymlink();
// Display the summary
await showSummary(templateDir);
console.log('\n✅ Synchronization completed!');
} catch (error) {
console.error('\n❌ Error during synchronization:');
console.error(error.message);
process.exit(1);
} finally {
await cleanup();
}
}
// Signal handling to clean up on interruption
process.on('SIGINT', async () => {
console.log('\n\n⚠️ Interruption detected, cleaning up...');
await cleanup();
process.exit(1);
});
process.on('SIGTERM', async () => {
console.log('\n\n⚠️ Shutdown requested, cleaning up...');
await cleanup();
process.exit(1);
});
main();