| | #!/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(); |
| |
|