| #!/usr/bin/env node |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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'; |
|
|
| |
| const PRESERVE_PATHS = [ |
| |
| 'app/src/content', |
|
|
| |
| 'app/public/data', |
|
|
| |
| 'app/package-lock.json', |
| 'app/node_modules', |
|
|
| |
| 'app/scripts/sync-template.mjs', |
|
|
| |
| 'README.md', |
| 'tools', |
|
|
| |
| '.backup-*', |
| '.temp-*', |
|
|
| |
| '.git', |
| '.gitignore' |
| ]; |
|
|
| |
| 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'); |
| 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); |
|
|
| |
| if (await isPathPreserved(relativeTarget)) { |
| console.log(`🔒 PRESERVED: ${relativeTarget}`); |
| return; |
| } |
|
|
| |
| if (SENSITIVE_FILES.includes(relativeTarget)) { |
| if (!isForce) { |
| console.log(`⚠️ SENSITIVE (ignored): ${relativeTarget} (use --force to overwrite)`); |
| return; |
| } else { |
| console.log(`⚠️ SENSITIVE (forced): ${relativeTarget}`); |
| } |
| } |
|
|
| |
| 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}`); |
| } |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| await fs.mkdir(path.dirname(targetPath), { recursive: true }); |
|
|
| |
| 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; |
| } |
|
|
| |
| if (await pathExists(targetPath)) { |
| await fs.rm(targetPath, { recursive: true, force: true }); |
| } |
|
|
| |
| 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...'); |
|
|
| |
| if (await pathExists(TEMP_DIR)) { |
| await fs.rm(TEMP_DIR, { recursive: true, force: true }); |
| if (isDryRun) { |
| console.log(`[DRY-RUN] Suppression: ${TEMP_DIR}`); |
| } |
| } |
|
|
| |
| 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'); |
|
|
| |
| 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}`); |
| } |
| } |
|
|
| |
| 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 { |
| |
| 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?`); |
| } |
|
|
| |
| const templateDir = await cloneOrUpdateTemplate(); |
|
|
| |
| console.log('\n🔄 Synchronisation en cours...'); |
| await syncDirectory(templateDir, PROJECT_ROOT); |
|
|
| |
| console.log('\n🔗 Vérification du lien symbolique des données...'); |
| await ensureDataSymlink(); |
|
|
| |
| 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(); |
| } |
| } |
|
|
| |
| 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(); |
|
|