Pq234/robot-learning-tutorial
0
1#!/usr/bin/env node2 3/**4 * Template synchronization script for research-article-template5 * 6 * This script:7 * 1. Clones or updates the template repo in a temporary directory8 * 2. Copies all files EXCEPT those in ./src/content which contain specific content9 * 3. Preserves important local configuration files10 * 4. Creates backups of files that will be overwritten11 * 12 * Usage: npm run sync:template [--dry-run] [--backup] [--force]13 */14 15import { execSync } from 'child_process';16import fs from 'fs/promises';17import path from 'path';18import { fileURLToPath } from 'url';19 20const __dirname = path.dirname(fileURLToPath(import.meta.url));21const APP_ROOT = path.resolve(__dirname, '..');22const PROJECT_ROOT = path.resolve(APP_ROOT, '..');23const TEMP_DIR = path.join(PROJECT_ROOT, '.temp-template-sync');24const TEMPLATE_REPO = 'https://huggingface.co/spaces/tfrere/research-article-template';25 26// Files and directories to PRESERVE (do not overwrite)27const PRESERVE_PATHS = [28 // Project-specific content29 'app/src/content',30 31 // Public data (symlink to our data) - CRITICAL: preserve this symlink32 'app/public/data',33 34 // Local configuration35 'app/package-lock.json',36 'app/node_modules',37 38 // Project-specific scripts (preserve our sync script)39 'app/scripts/sync-template.mjs',40 41 // Project configuration files42 'README.md',43 'tools',44 45 // Backup and temporary files46 '.backup-*',47 '.temp-*',48 49 // Git50 '.git',51 '.gitignore'52];53 54// Files to handle with caution (require confirmation)55const SENSITIVE_FILES = [56 'app/package.json',57 'app/astro.config.mjs',58 'Dockerfile',59 'nginx.conf'60];61 62const args = process.argv.slice(2);63const isDryRun = args.includes('--dry-run');64const shouldBackup = args.includes('--backup'); // Disabled by default, use --backup to enable65const isForce = args.includes('--force');66 67console.log('🔄 Template synchronization script for research-article-template');68console.log(`📁 Working directory: ${PROJECT_ROOT}`);69console.log(`🎯 Template source: ${TEMPLATE_REPO}`);70if (isDryRun) console.log('🔍 DRY-RUN mode enabled - no files will be modified');71if (shouldBackup) console.log('💾 Backup enabled');72if (!shouldBackup) console.log('🚫 Backup disabled (use --backup to enable)');73console.log('');74 75async function executeCommand(command, options = {}) {76 try {77 if (isDryRun && !options.allowInDryRun) {78 console.log(`[DRY-RUN] Command: ${command}`);79 return '';80 }81 console.log(`$ ${command}`);82 const result = execSync(command, {83 encoding: 'utf8',84 cwd: options.cwd || PROJECT_ROOT,85 stdio: options.quiet ? 'pipe' : 'inherit'86 });87 return result;88 } catch (error) {89 console.error(`❌ Error during execution: ${command}`);90 console.error(error.message);91 throw error;92 }93}94 95async function pathExists(filePath) {96 try {97 await fs.access(filePath);98 return true;99 } catch {100 return false;101 }102}103 104async function isPathPreserved(relativePath) {105 return PRESERVE_PATHS.some(preserve =>106 relativePath === preserve ||107 relativePath.startsWith(preserve + '/')108 );109}110 111async function createBackup(filePath) {112 if (!shouldBackup || isDryRun) return;113 114 const timestamp = new Date().toISOString().replace(/[:.]/g, '-');115 const backupPath = `${filePath}.backup-${timestamp}`;116 117 try {118 await fs.copyFile(filePath, backupPath);119 console.log(`💾 Backup created: ${path.relative(PROJECT_ROOT, backupPath)}`);120 } catch (error) {121 console.warn(`⚠️ Unable to create backup for ${filePath}: ${error.message}`);122 }123}124 125async function syncFile(sourcePath, targetPath) {126 const relativeTarget = path.relative(PROJECT_ROOT, targetPath);127 128 // Check if the file should be preserved129 if (await isPathPreserved(relativeTarget)) {130 console.log(`🔒 PRESERVED: ${relativeTarget}`);131 return;132 }133 134 // Check if it's a sensitive file135 if (SENSITIVE_FILES.includes(relativeTarget)) {136 if (!isForce) {137 console.log(`⚠️ SENSITIVE (ignored): ${relativeTarget} (use --force to overwrite)`);138 return;139 } else {140 console.log(`⚠️ SENSITIVE (forced): ${relativeTarget}`);141 }142 }143 144 // Check if target file is a symbolic link to preserve145 if (await pathExists(targetPath)) {146 try {147 const targetStats = await fs.lstat(targetPath);148 if (targetStats.isSymbolicLink()) {149 console.log(`🔗 SYMLINK TARGET (preserved): ${relativeTarget}`);150 return;151 }152 } catch (error) {153 console.warn(`⚠️ Impossible de vérifier ${targetPath}: ${error.message}`);154 }155 }156 157 // Create backup if file already exists (and is not a symbolic link)158 if (await pathExists(targetPath)) {159 try {160 const stats = await fs.lstat(targetPath);161 if (!stats.isSymbolicLink()) {162 await createBackup(targetPath);163 }164 } catch (error) {165 console.warn(`⚠️ Impossible de vérifier ${targetPath}: ${error.message}`);166 }167 }168 169 if (isDryRun) {170 console.log(`[DRY-RUN] COPY: ${relativeTarget}`);171 return;172 }173 174 // Assurer que le répertoire parent existe175 await fs.mkdir(path.dirname(targetPath), { recursive: true });176 177 // Check if source is a symbolic link178 try {179 const sourceStats = await fs.lstat(sourcePath);180 if (sourceStats.isSymbolicLink()) {181 console.log(`🔗 SYMLINK SOURCE (ignored): ${relativeTarget}`);182 return;183 }184 } catch (error) {185 console.warn(`⚠️ Unable to check source ${sourcePath}: ${error.message}`);186 return;187 }188 189 // Remove target file if it exists (to handle symbolic links)190 if (await pathExists(targetPath)) {191 await fs.rm(targetPath, { recursive: true, force: true });192 }193 194 // Copier le fichier195 await fs.copyFile(sourcePath, targetPath);196 console.log(`✅ COPIED: ${relativeTarget}`);197}198 199async function syncDirectory(sourceDir, targetDir) {200 const items = await fs.readdir(sourceDir, { withFileTypes: true });201 202 for (const item of items) {203 const sourcePath = path.join(sourceDir, item.name);204 const targetPath = path.join(targetDir, item.name);205 const relativeTarget = path.relative(PROJECT_ROOT, targetPath);206 207 if (await isPathPreserved(relativeTarget)) {208 console.log(`🔒 DOSSIER PRÉSERVÉ: ${relativeTarget}/`);209 continue;210 }211 212 if (item.isDirectory()) {213 if (!isDryRun) {214 await fs.mkdir(targetPath, { recursive: true });215 }216 await syncDirectory(sourcePath, targetPath);217 } else {218 await syncFile(sourcePath, targetPath);219 }220 }221}222 223async function cloneOrUpdateTemplate() {224 console.log('📥 Fetching template...');225 226 // Nettoyer le dossier temporaire s'il existe227 if (await pathExists(TEMP_DIR)) {228 await fs.rm(TEMP_DIR, { recursive: true, force: true });229 if (isDryRun) {230 console.log(`[DRY-RUN] Suppression: ${TEMP_DIR}`);231 }232 }233 234 // Clone template repo (even in dry-run to be able to compare)235 await executeCommand(`git clone ${TEMPLATE_REPO} "${TEMP_DIR}"`, { allowInDryRun: true });236 237 return TEMP_DIR;238}239 240async function ensureDataSymlink() {241 const dataSymlinkPath = path.join(APP_ROOT, 'public', 'data');242 const dataSourcePath = path.join(APP_ROOT, 'src', 'content', 'assets', 'data');243 244 // Check if symlink exists and is correct245 if (await pathExists(dataSymlinkPath)) {246 try {247 const stats = await fs.lstat(dataSymlinkPath);248 if (stats.isSymbolicLink()) {249 const target = await fs.readlink(dataSymlinkPath);250 const expectedTarget = path.relative(path.dirname(dataSymlinkPath), dataSourcePath);251 if (target === expectedTarget) {252 console.log('🔗 Data symlink is correct');253 return;254 } else {255 console.log(`⚠️ Data symlink points to wrong target: ${target} (expected: ${expectedTarget})`);256 }257 } else {258 console.log('⚠️ app/public/data exists but is not a symlink');259 }260 } catch (error) {261 console.log(`⚠️ Error checking symlink: ${error.message}`);262 }263 }264 265 // Recreate symlink266 if (!isDryRun) {267 if (await pathExists(dataSymlinkPath)) {268 await fs.rm(dataSymlinkPath, { recursive: true, force: true });269 }270 await fs.symlink(path.relative(path.dirname(dataSymlinkPath), dataSourcePath), dataSymlinkPath);271 console.log('✅ Data symlink recreated');272 } else {273 console.log('[DRY-RUN] Would recreate data symlink');274 }275}276 277async function showSummary(templateDir) {278 console.log('\n📊 SYNCHRONIZATION SUMMARY');279 console.log('================================');280 281 console.log('\n🔒 Preserved files/directories:');282 for (const preserve of PRESERVE_PATHS) {283 const fullPath = path.join(PROJECT_ROOT, preserve);284 if (await pathExists(fullPath)) {285 console.log(` ✓ ${preserve}`);286 } else {287 console.log(` - ${preserve} (n'existe pas)`);288 }289 }290 291 console.log('\n⚠️ Sensitive files (require --force):');292 for (const sensitive of SENSITIVE_FILES) {293 const fullPath = path.join(PROJECT_ROOT, sensitive);294 if (await pathExists(fullPath)) {295 console.log(` ! ${sensitive}`);296 }297 }298 299 if (isDryRun) {300 console.log('\n🔍 To execute for real: npm run sync:template');301 console.log('🔧 To force sensitive files: npm run sync:template -- --force');302 }303}304 305async function cleanup() {306 console.log('\n🧹 Cleaning up...');307 if (await pathExists(TEMP_DIR)) {308 if (!isDryRun) {309 await fs.rm(TEMP_DIR, { recursive: true, force: true });310 }311 console.log(`🗑️ Temporary directory removed: ${TEMP_DIR}`);312 }313}314 315async function main() {316 try {317 // Verify we're in the correct directory318 const packageJsonPath = path.join(APP_ROOT, 'package.json');319 if (!(await pathExists(packageJsonPath))) {320 throw new Error(`Package.json not found in ${APP_ROOT}. Are you in the correct directory?`);321 }322 323 // Clone the template324 const templateDir = await cloneOrUpdateTemplate();325 326 // Synchroniser327 console.log('\n🔄 Synchronisation en cours...');328 await syncDirectory(templateDir, PROJECT_ROOT);329 330 // S'assurer que le lien symbolique des données est correct331 console.log('\n🔗 Vérification du lien symbolique des données...');332 await ensureDataSymlink();333 334 // Afficher le résumé335 await showSummary(templateDir);336 337 console.log('\n✅ Synchronization completed!');338 339 } catch (error) {340 console.error('\n❌ Error during synchronization:');341 console.error(error.message);342 process.exit(1);343 } finally {344 await cleanup();345 }346}347 348// Signal handling to clean up on interruption349process.on('SIGINT', async () => {350 console.log('\n\n⚠️ Interruption detected, cleaning up...');351 await cleanup();352 process.exit(1);353});354 355process.on('SIGTERM', async () => {356 console.log('\n\n⚠️ Shutdown requested, cleaning up...');357 await cleanup();358 process.exit(1);359});360 361main();362 