SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
4 h agolast push
JavaScript 56.7% TypeScript 42.6%
4.6 KB · 132 lines typescript
Raw Blame History
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