JavaScript 56.7%
TypeScript 42.6%
1/**2 * Réorganisation intelligente des espaces partagés Richard ↔ Simon-Pierre.3 * Utilitaires communs (périmètre, cache JSONL, nettoyage de noms).4 *5 * Exécution sur le nœud, depuis la racine du projet :6 * npx tsx --env-file=.env scripts/organize/extract.ts7 * npx tsx --env-file=.env scripts/organize/plan.ts8 * npx tsx --env-file=.env scripts/organize/apply.ts [--dry-run]9 */10import fs from "fs";11import path from "path";12import { prisma } from "../../lib/prisma";1314export const DATA_DIR = path.resolve("scripts/organize/data");15export const EXTRACT_PATH = path.join(DATA_DIR, "extract.jsonl");16export const PLAN_PATH = path.join(DATA_DIR, "plan.json");17export const APPLIED_PATH = path.join(DATA_DIR, "applied.jsonl");18fs.mkdirSync(DATA_DIR, { recursive: true });1920export const MODEL = "claude-opus-5";2122/** Utilisateurs dont la co-appartenance définit le périmètre. */23export const MEMBERS = ["rboucher", "spboucher"];2425export interface ScopeFile {26 id: string;27 name: string;28 mimeType: string;29 size: number;30 storagePath: string;31 isEncrypted: boolean;32 folderId: string | null;33 folderName: string;34 spaceId: string;35 spaceName: string;36 createdAt: string;37}3839export interface Extraction {40 id: string;41 name: string;42 spaceId: string;43 spaceName: string;44 folderName: string;45 mimeType: string;46 kind: "photo" | "pdf" | "text" | "office" | "other";47 ok: boolean;48 error?: string;49 data?: Record<string, unknown>;50 at: string;51}5253/** Espaces partagés où TOUS les membres requis sont présents. */54export async function sharedSpacesInScope() {55 const spaces = await prisma.sharedSpace.findMany({56 include: { members: { include: { user: { select: { username: true } } } } },57 orderBy: { name: "asc" },58 });59 return spaces.filter((s) => MEMBERS.every((u) => s.members.some((m) => m.user.username === u)));60}6162export async function filesInScope(): Promise<ScopeFile[]> {63 const spaces = await sharedSpacesInScope();64 const spaceById = new Map(spaces.map((s) => [s.id, s]));65 const folders = await prisma.folder.findMany({ where: { sharedSpaceId: { in: spaces.map((s) => s.id) } } });66 const folderById = new Map(folders.map((f) => [f.id, f]));67 const files = await prisma.file.findMany({68 where: { deletedAt: null, folderId: { in: folders.map((f) => f.id) } },69 orderBy: { name: "asc" },70 });71 return files.map((f) => {72 const folder = folderById.get(f.folderId!)!;73 const space = spaceById.get(folder.sharedSpaceId!)!;74 return {75 id: f.id, name: f.name, mimeType: f.mimeType, size: Number(f.size), storagePath: f.storagePath,76 isEncrypted: f.isEncrypted, folderId: f.folderId, folderName: folder.name, spaceId: space.id, spaceName: space.name,77 createdAt: f.createdAt.toISOString(),78 };79 });80}8182export function readJsonl<T>(p: string): T[] {83 if (!fs.existsSync(p)) return [];84 return fs.readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l) as T);85}86export function appendJsonl(p: string, obj: unknown) {87 fs.appendFileSync(p, JSON.stringify(obj) + "\n");88}8990/** Extrait le premier objet JSON d'une réponse texte. */91export function parseJson(text: string): Record<string, unknown> | null {92 const m = text.match(/\{[\s\S]*\}/);93 if (!m) return null;94 try { return JSON.parse(m[0]); } catch { return null; }95}9697/** Nettoie un nom de fichier/dossier : accents conservés, caractères interdits retirés, espaces normalisés. */98export function cleanName(s: string, maxLen = 120): string {99 let n = s.normalize("NFC").replace(/[\\/:*?"<>|]/g, "-").replace(/\s+/g, " ").trim();100 n = n.replace(/^[.\s]+|[.\s]+$/g, "");101 if (n.length > maxLen) n = n.slice(0, maxLen).trim();102 return n || "Sans nom";103}104105export function extOf(name: string): string {106 const m = name.match(/\.([A-Za-z0-9]{1,6})$/);107 return m ? m[1].toLowerCase() : "";108}109110/** Slug ASCII compact pour les photos (Titre_court). */111export function slugTitle(s: string, maxLen = 48): string {112 const t = s.normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");113 const cut = t.slice(0, maxLen).replace(/_+$/g, "");114 return cut || "Photo";115}116117export function pad(n: number, w = 3) { return String(n).padStart(w, "0"); }118119export function sleep(ms: number) { return new Promise((r) => setTimeout(r, ms)); }120121/** Exécute des tâches avec une concurrence bornée. */122export async function pool<T>(items: T[], limit: number, fn: (item: T, idx: number) => Promise<void>) {123 let i = 0;124 const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {125 while (i < items.length) {126 const idx = i++;127 await fn(items[idx], idx);128 }129 });130 await Promise.all(workers);131}132