JavaScript 56.7%
TypeScript 42.6%
1/**2 * Étape 3 — Application du plan (data/plan.json) : renommage des espaces, création de l'arborescence,3 * déplacement + renommage des fichiers, description IA, suppression des anciens dossiers vides.4 * Journal complet dans data/applied.jsonl (rollback.ts). Sauvegarde SQLite avant toute écriture.5 * npx tsx --env-file=.env scripts/organize/apply.ts --dry-run6 * npx tsx --env-file=.env scripts/organize/apply.ts7 */8import fs from "fs";9import path from "path";10import { prisma } from "../../lib/prisma";11import { logActivity } from "../../lib/activity";12import { APPLIED_PATH, DATA_DIR, PLAN_PATH, appendJsonl } from "./common";13import type { Plan, PlanSpace } from "./plan";1415const dry = process.argv.includes("--dry-run");1617interface Applied {18 kind: "file" | "space" | "folder-created" | "folder-deleted";19 fileId?: string; oldName?: string; newName?: string;20 oldFolder?: { id: string; name: string; parentId: string | null; sharedSpaceId: string | null } | null;21 newFolderId?: string;22 spaceId?: string; folderId?: string; folder?: { id: string; name: string; parentId: string | null; sharedSpaceId: string | null };23 at: string;24}2526function printTree(ps: PlanSpace) {27 const byFolder = new Map<string, string[]>();28 for (const f of ps.files) { if (!byFolder.has(f.folder)) byFolder.set(f.folder, []); byFolder.get(f.folder)!.push(f.name); }29 console.log(`\n📁 ${ps.oldName} → « ${ps.newName} » (${ps.files.length} fichiers, ${byFolder.size} dossiers)`);30 for (const folder of Array.from(byFolder.keys()).sort()) {31 const names = byFolder.get(folder)!.sort();32 console.log(` ${folder} (${names.length})`);33 for (const n of names.slice(0, dry ? 8 : 0)) console.log(` · ${n}`);34 if (dry && names.length > 8) console.log(` … +${names.length - 8}`);35 }36 if (ps.notes) console.log(` ℹ ${ps.notes}`);37}3839async function ensureFolder(spaceId: string, pathStr: string, cache: Map<string, string>): Promise<string> {40 const parts = pathStr.split("/").filter(Boolean);41 let parentId: string | null = null;42 let acc = "";43 for (const part of parts) {44 acc = acc ? `${acc}/${part}` : part;45 if (cache.has(acc)) { parentId = cache.get(acc)!; continue; }46 let folder: { id: string; name: string; parentId: string | null; sharedSpaceId: string | null } | null = await prisma.folder.findFirst({ where: { name: part, parentId, sharedSpaceId: spaceId } });47 if (!folder) {48 folder = await prisma.folder.create({ data: { name: part, parentId, sharedSpaceId: spaceId } });49 appendJsonl(APPLIED_PATH, { kind: "folder-created", folderId: folder.id, folder: { id: folder.id, name: folder.name, parentId: folder.parentId, sharedSpaceId: folder.sharedSpaceId }, at: new Date().toISOString() } satisfies Applied);50 }51 cache.set(acc, folder.id);52 parentId = folder.id;53 }54 return parentId!;55}5657async function main() {58 const plan: Plan = JSON.parse(fs.readFileSync(PLAN_PATH, "utf8"));59 const total = plan.spaces.reduce((n, s) => n + s.files.length, 0);60 console.log(`Plan du ${plan.generatedAt} · ${plan.spaces.length} espaces · ${total} fichiers · ${dry ? "SIMULATION" : "APPLICATION"}`);61 for (const ps of plan.spaces) printTree(ps);62 if (dry) { await prisma.$disconnect(); return; }6364 // Sauvegarde cohérente de la base (fonctionne en mode WAL)65 const bak = path.join(DATA_DIR, `dev.db.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`);66 await prisma.$executeRawUnsafe(`VACUUM INTO '${bak.replace(/'/g, "''")}'`);67 console.log(`\n💾 Sauvegarde SQLite : ${bak}`);6869 let moved = 0, renamed = 0, createdFolders = 0, deletedFolders = 0;70 for (const ps of plan.spaces) {71 const cache = new Map<string, string>();72 const oldFolderIds = new Set<string>();73 // Espace74 const space = await prisma.sharedSpace.findUnique({ where: { id: ps.spaceId } });75 if (!space) { console.warn(`Espace introuvable ${ps.spaceId}`); continue; }76 if (space.name !== ps.newName) {77 await prisma.sharedSpace.update({ where: { id: ps.spaceId }, data: { name: ps.newName } });78 appendJsonl(APPLIED_PATH, { kind: "space", spaceId: ps.spaceId, oldName: space.name, newName: ps.newName, at: new Date().toISOString() } satisfies Applied);79 }80 // Fichiers81 for (const pf of ps.files) {82 const file = await prisma.file.findUnique({ where: { id: pf.id }, include: { folder: true } });83 if (!file) { console.warn(`Fichier introuvable ${pf.id} (${pf.oldName})`); continue; }84 if (file.folderId) oldFolderIds.add(file.folderId);85 const before = cache.size;86 const targetId = await ensureFolder(ps.spaceId, pf.folder, cache);87 if (cache.size > before) createdFolders += cache.size - before;88 const changes: { folderId?: string; name?: string; aiDescription?: string } = {};89 if (file.folderId !== targetId) { changes.folderId = targetId; moved++; }90 if (file.name !== pf.name) { changes.name = pf.name; renamed++; }91 if (pf.summary && pf.summary !== file.aiDescription) changes.aiDescription = pf.summary;92 if (Object.keys(changes).length) {93 await prisma.file.update({ where: { id: pf.id }, data: { ...changes, updatedAt: file.updatedAt } });94 appendJsonl(APPLIED_PATH, {95 kind: "file", fileId: pf.id, oldName: file.name, newName: pf.name,96 oldFolder: file.folder ? { id: file.folder.id, name: file.folder.name, parentId: file.folder.parentId, sharedSpaceId: file.folder.sharedSpaceId } : null,97 newFolderId: targetId, at: new Date().toISOString(),98 } satisfies Applied);99 }100 }101 // Anciens dossiers désormais vides (aucun fichier, même en corbeille, aucun sous-dossier)102 const reused = new Set(Array.from(cache.values()));103 for (const fid of Array.from(oldFolderIds)) {104 if (reused.has(fid)) continue; // réutilisé par le plan105 const f = await prisma.folder.findUnique({ where: { id: fid }, include: { _count: { select: { files: true, children: true } } } });106 if (f && f._count.files === 0 && f._count.children === 0) {107 appendJsonl(APPLIED_PATH, { kind: "folder-deleted", folderId: f.id, folder: { id: f.id, name: f.name, parentId: f.parentId, sharedSpaceId: f.sharedSpaceId }, at: new Date().toISOString() } satisfies Applied);108 await prisma.folder.delete({ where: { id: f.id } });109 deletedFolders++;110 }111 }112 await logActivity("ORGANIZE", null, `Réorganisation IA de l'espace « ${ps.newName} » (${ps.files.length} fichiers)`);113 console.log(`✓ ${ps.newName}`);114 }115 console.log(`\nTerminé : ${renamed} renommés · ${moved} déplacés · ${createdFolders} dossiers créés · ${deletedFolders} anciens dossiers vides supprimés · journal ${APPLIED_PATH}`);116 await prisma.$disconnect();117}118119main().catch((e) => { console.error(e); process.exit(1); });120