Contribuer
Les contributions sont les bienvenues, en particulier de nouveaux emplacements de caches, une meilleure détection de ce qui peut être supprimé sans risque, et des rapports de bug accompagnés d'une sortie de lu-cleaner scan (anonymisée). lu-cleaner supprime des fichiers sur les machines des gens : toute modification qui peut mener à une suppression passe donc par une revue de sécurité.
Mise en place
Section intitulée « Mise en place »Il vous faut macOS, Go 1.25 ou plus récent, et git. Node.js 22 n'est nécessaire que pour le site de documentation.
git clone https://github.com/ludwig-pro/lu-cleanercd lu-cleanermake build # bin/lu-cleaner./bin/lu-cleaner scan --summary # lecture seule./bin/lu-cleaner clean --yes --smart --dry-runTenez-vous-en à scan, à --dry-run et au mode simulation du sélecteur pendant que vous développez. make run ARGS="scan -c js --summary" compile et lance en une seule étape.
Cibles Make
Section intitulée « Cibles Make »| Cible | Ce qu'elle fait |
|---|---|
make build |
Compile bin/lu-cleaner (version tirée de git describe) |
make install |
Installe dans $(go env GOPATH)/bin |
make test |
go test ./... |
make vet |
go vet ./... |
make fmt |
gofmt -w . |
make lint |
go vet plus une vérification gofmt |
make run ARGS="…" |
Compile, puis lance avec les arguments donnés |
make clean |
Supprime bin/ |
make docs |
Régénère les pages de référence du site à partir de la CLI |
make site-dev |
make docs, puis le site sur http://localhost:4321/lu-cleaner/ |
make site-build |
make docs, puis compile le site dans site/dist |
Visite de l'architecture
Section intitulée « Visite de l'architecture »- cmd/lu-cleaner/main.go point d'entrée, version définie avec
-ldflags Répertoireinternal/
Répertoirecore/ modèle de données :
Item,Risk,Method,Category,Provider,Env,Filter,Recommend,TopLevel- …
Répertoirefsx/ calcul concurrent de la taille des répertoires (blocs alloués, prise en compte des liens physiques et des clones APFS,
getattrlistbulk), protection des conteneurs des autres applications, lecture des tailles et des âges- …
Répertoiresysx/ espace disque libre, processus en cours, répertoires de travail des processus, snapshots locaux APFS
- …
Répertoiresafety/ le
Guard: dernière ligne de défense avant toute suppression- …
Répertoireconfig/
~/.config/lu-cleaner/config.toml- …
Répertoireengine/ exécute les scanners en parallèle et diffuse les mises à jour d'éléments
- …
Répertoireclean/ exécuteur : revérifications, suppression, Corbeille, commandes,
git worktree remove, historique- …
Répertoireproviders/
- registry.go la liste des scanners
Répertoirecatalog/ chemins connus décrits par des données, un fichier
data_*.gopar domaine- …
Répertoireworktrees/ worktrees git des outils d'IA et des humains
- …
Répertoireartifacts/ sorties de build et dépendances des projets
- …
Répertoireapple/ Xcode et simulateurs iOS
- …
Répertoireandroid/ AVD, paquets du SDK, Gradle, Android Studio, JDK
- …
Répertoireaitools/ données des outils de code IA
- …
Répertoirejsdev/ versions de node, stores des gestionnaires de paquets, restes d'outils JS
- …
Répertoiresystem/ Corbeille, conteneurs, Homebrew et chaînes d'outils, éditeurs, Downloads, état de macOS
- …
Répertoiretui/ Bubble Tea : le sélecteur et l'analyseur
- …
Répertoirecli/ commandes Cobra, tableaux de sortie, JSON,
gen-docs- …
- docs/ARCHITECTURE.md
Répertoiresite/ cette documentation (Astro Starlight)
- …
Le flux va dans un seul sens :
providers ──emit(*Item) upsert──▶ engine.Run ──▶ TUI picker / CLI table / JSON │ sélection utilisateur ▼ clean.Run(items) ── Guard.Check each target ──▶ rm / Trash / command / git worktree remove └─▶ history.jsonl + statfs before/afterLes contrats que toute modification doit respecter :
- Un provider ne supprime jamais rien. Il se contente d'émettre des valeurs
*core.Item. Toutes les suppressions passent parclean.Run. - Émettre à nouveau le même
Item.IDremplace la valeur précédente. Servez-vous-en pour afficher d'abord un élément avecSizing: true, puis sa version mesurée. Ne modifiez jamais un élément après l'avoir émis : émettezit.Clone()si vous continuez à travailler dessus. Sizeest la taille allouée (st_blocks × 512, liens physiques comptés une fois). RenseignezReclaim(viaSetReclaim) quand la suppression libère moins parce que les données sont partagées avec des fichiers extérieurs à l'élément : liens physiques (store pnpm) et clones APFS (installations bun et pnpm,cp -c).fsx.Sizecalcule les deux.LastUsedest le meilleur signal d'activité réelle, et non la date de modification d'un dossier. Il pilote les âges,--older-thanet la sélection intelligente.WarnetNoRecommendexcluent un élément de la sélection intelligente.NoRecommendest un veto pour ce qui ne doit être nettoyé que sur sélection explicite (irréversible, ou d'origine incertaine). Un élément qui porte unWarnest aussi retenu parclean --yessauf avec--risk caution.RequireForcefait refuser l'élément par l'exécuteur sans--force, en simulation aussi (dossiers de worktrees orphelins).- Chaque cible est revalidée par
safety.Guardjuste avant la suppression, indépendamment du scanner qui l'a produite. Il refuse tout répertoire qui contient une entrée.git(répertoire, fichier ou lien symbolique) ou qui est organisé comme un dépôt nu, sauf si l'élément activeAllowGitRepo. - Ne lisez jamais sous le conteneur d'une autre application sans accès complet au disque. Sur macOS 14 et versions ultérieures, le premier
open()oustat()dans~/Library/Containers/<app>ou~/Library/Group Containers/<group>reste bloqué sur une demande d'autorisation. Vérifiez d'abordfsx.AppDataProtected(oufsx.GlobPrefixProtectedpour un glob) ;fsx.Sizey renvoie déjàfsx.ErrNeedsFullDiskAccess. - Comparez les chemins avec
safety.Key(insensible à la casse et à la normalisation Unicode, comme APFS), jamais comme de simples chaînes. - La sortie destinée aux humains n'affiche jamais de caractère de contrôle brut : les noms, chemins et messages passent par
sanitize(CLI) ousafeText(TUI), qui transforment les caractères de contrôle et les caractères de forçage bidirectionnel en échappements visibles.
internal/cli reçoit toutes ses dépendances via cli.App (providers, configuration, exécuteur de nettoyage, TUI, requêtes système) : ses tests exécutent donc les vraies commandes contre des implémentations factices. Les pages Fonctionnement et Modèle de sécurité décrivent le comportement plus en détail.
Ajouter une entrée au catalogue
Section intitulée « Ajouter une entrée au catalogue »La plupart des nouveaux emplacements de caches tiennent en quelques lignes de données. Utilisez le catalogue quand l'emplacement est fixe (un glob sous ~/ ou $TMPDIR/) et que la décision de le proposer ne demande pas d'autre logique que l'âge, la taille, une application en cours d'exécution ou « garder les N plus récents ».
-
Choisissez le fichier de données du domaine dans
internal/providers/catalog/:data_apple.go,data_androiddev.go,data_jsdev.go,data_ai.gooudata_system.go. Lisez le commentaire en haut du fichier : certains emplacements sont volontairement confiés à un scanner dynamique et ne doivent pas être ajoutés au catalogue. -
Ajoutez une
Entryà l'appeladd(...):Entry{ID: "js-example-tool-cache", Category: core.CatJS, Name: "Example tool cache",Paths: []string{"~/Library/Caches/example-tool"}, Risk: core.RiskSafe,Note: "Downloads cached by example-tool; fetched again on its next run.",},Entry{ID: "js-example-tool-versions", Category: core.CatJS, Name: "Example tool versions",Paths: []string{"~/.example-tool/versions/*"}, Risk: core.RiskModerate,Mode: Each, KeepLatest: 1,Note: "Old versions of example-tool; the newest is kept and `example-tool install` downloads others again.",}, -
Lancez les tests du catalogue, puis essayez l'entrée sur votre machine :
Fenêtre de terminal go test ./internal/providers/catalog/make build./bin/lu-cleaner catalog -k js-example-tool./bin/lu-cleaner scan -k js-example-tool-cache -
Régénérez les pages de référence avec
make docset commitez le fichiersite/src/content/docs/reference/catalog.mdmis à jour. La CI échoue quand il n'est pas à jour.
Champs d'une entrée
Section intitulée « Champs d'une entrée »| Champ | Requis | Signification |
|---|---|---|
ID |
oui | Unique, en kebab-case ; devient le type d'élément (--kind). Préfixez-le par le domaine (js-, ai-, xcode-…) |
Category |
oui | Une des catégories core.Cat* |
Name |
oui | Libellé lisible |
Paths |
oui | Globs commençant par ~/ ou $TMPDIR/, syntaxe de filepath.Glob (*, ?, […]), sans **. Sans accès complet au disque, un chemin situé dans le conteneur d'une autre application devient un élément en rapport seul avec un avertissement |
Note |
oui | Une phrase : ce que c'est, et comment cela revient après le nettoyage |
Risk |
oui | core.RiskSafe, RiskModerate, RiskCaution ou RiskNever. La valeur zéro est RiskSafe : choisissez délibérément |
Method |
non | MethodDelete par défaut. MethodCommand exige Command ; MethodReport ne nettoie jamais ; MethodWorktree n'est pas autorisé ici |
Command, Requires |
non | Argv de la commande de nettoyage ; binaire qui doit être dans le PATH pour que l'entrée soit évaluée |
Exclude |
non | Globs de noms de base des correspondances à écarter |
Mode |
non | Group (par défaut : toutes les correspondances forment un seul élément) ou Each (un élément par correspondance) |
KeepLatest |
non | Avec Mode: Each, garde hors de la liste les N correspondances modifiées le plus récemment (porté à la valeur du réglage keep_latest quand celle-ci est plus élevée) |
OlderThan |
non | Seulement les correspondances dont la date de modification est plus ancienne que cette durée |
ProcessGuard |
non | Processus qui ne doivent pas tourner pendant le nettoyage (nom exact de l'exécutable, ou sous-chaîne de chemin contenant /) |
MinBytes |
non | En dessous de cette taille, l'élément n'est pas sélectionnable (et il est masqué en mode Each) |
Files |
non | Les correspondances sont des fichiers, pas des répertoires |
AllowGitRepo |
non | Autorise une correspondance qui est un dépôt git (rare : un dépôt de specs cloné) |
Recommended |
non | Force la sélection intelligente pour cette entrée, quels que soient sa taille ou son âge (un avertissement la bloque tout de même) |
Ce que vérifient les tests
Section intitulée « Ce que vérifient les tests »TestEntriesAreValid parcourt toutes les entrées et échoue sur un identifiant en double ou qui n'est pas en kebab-case, une catégorie inconnue, un nom ou une note manquants, des chemins manquants, une méthode commande sans commande, une méthode worktree, un chemin qui ne commence pas par ~/ ou $TMPDIR/, un ** ou un glob invalide, un chemin simple (sans glob) que le garde-fou de sécurité protège, ou KeepLatest sans Mode: Each.
TestDataFilesAreCompiled protège contre un piège du système de build de Go. Un nom de fichier qui se termine par _<GOOS>.go ou _<GOARCH>.go est une contrainte de build implicite : data_js.go ne serait compilé que pour GOOS=js, data_android.go que pour Android, data_ios.go que pour iOS. Le fichier serait silencieusement ignoré sur macOS, et ses entrées disparaîtraient sans la moindre erreur. C'est pourquoi les fichiers JS et Android s'appellent data_jsdev.go et data_androiddev.go. Le test échoue si un fichier data_*.go est exclu des builds darwin ; si vous créez un nouveau fichier de données, donnez-lui un nom qui n'est ni une valeur de GOOS ni une valeur de GOARCH.
Ajouter un scanner
Section intitulée « Ajouter un scanner »Écrivez un scanner quand la décision demande de la logique : quelle version est utilisée, si un dossier appartient encore à quelque chose de vivant, ce que dit l'état propre d'un outil. Un chemin fixe a sa place dans le catalogue.
-
Créez un package dans
internal/providers/<name>/qui implémentecore.Provider:package exampleimport ("context""path/filepath""github.com/ludwig-pro/lu-cleaner/internal/core""github.com/ludwig-pro/lu-cleaner/internal/fsx")type Provider struct{}func New() *Provider { return &Provider{} }func (p *Provider) ID() string { return "example" }func (p *Provider) Title() string { return "Example tool" }func (p *Provider) Categories() []core.Category { return []core.Category{core.CatJS} }// Scan doit respecter ctx et ne jamais rien modifier.func (p *Provider) Scan(ctx context.Context, env *core.Env, emit core.Emit) error {dir := filepath.Join(env.Home, ".example-tool", "state")if !fsx.IsDir(dir) || env.Excluded(dir) || env.IsProtected(dir) {return nil}it := &core.Item{ID: "example:example-state:" + dir, Category: core.CatJS,Kind: "example-state", Name: "Example tool state", Path: dir,Risk: core.RiskModerate, Method: core.MethodDelete, Selectable: true,Note: "State of example-tool; rebuilt on its next run (slower first start).",Sizing: true,}emit(it.Clone()) // élément provisoire, affiché immédiatementst, _ := fsx.Size(ctx, dir, nil)if ctx.Err() != nil {return ctx.Err()}it.Sizing = falseit.Size, it.Files, it.LastUsed = st.Bytes, st.Files, st.Newestit.SetReclaim(st.Reclaim)emit(it) // même ID : remplace l'élément provisoirereturn nil} -
Enregistrez-le dans
internal/providers/registry.go. L'ordre deAll()définit la priorité d'affichage. -
Testez-le avec des fixtures construites dans
t.TempDir()et un fauxcore.Runnerpour les commandes externes (git,xcrun…), afin que les tests ne dépendent jamais de la machine. Ajoutez unreal_test.goconditionné parLU_REAL=1qui analyse la vraie machine en lecture seule et affiche ce qu'il trouve. -
Documentez ses types dans
site/src/content/docs/reference/scanners.md: ce qu'est chaque type, son risque, sa méthode et quand il est recommandé.
Consignes pour les scanners :
- Exécutez les commandes externes via
env.Output(leRunner), avec un délai maximal, et uniquement des commandes en lecture seule :brew cleanup -n,watchman --no-spawn,sqlite3 -readonlysur une URI immuable.env.Runnerdésactive déjà les verrous optionnels de git. - Mesurez avec
fsx.Sizesans options : le moteur donne à chaque analyse un cache partagé, si bien qu'un dossier mesuré par deux scanners n'est parcouru qu'une fois. - Consultez
env.IsProtectedetenv.Excludedavant de proposer un chemin. Signalez les éléments situés sur un autre volume avecMethodReportau lieu de les proposer. - Renseignez
Warnpour tout ce qu'un humain devrait regarder en premier (une application en cours d'exécution, un processus à l'intérieur, une version épinglée par un projet). Un avertissement exclut l'élément de la sélection intelligente, et declean --yessans--risk caution. - Renseignez
NoRecommendquand l'élément ne doit jamais être recommandé, quel que soit son âge : des données qui ne reviennent pas, ou que leur propriétaire peut encore vouloir garder. MettreRecommended = falsen'est pas un veto. - Vérifiez
fsx.AppDataProtectedavant de toucher à quoi que ce soit sous~/Library/Containersou~/Library/Group Containers, et signalez ces éléments au lieu de les lire. - Utilisez
ProcessGuard,RequireSiblingetRecheckpour exprimer les conditions que l'exécuteur doit revérifier au moment du nettoyage.Recheckdoit refuser en cas de doute (fail closed). - Préférez la commande de nettoyage propre à l'outil (
MethodCommand) quand l'outil tient sa propre comptabilité. - Un panic dans
Scanest rattrapé et affiché comme une erreur de scanner, mais pas dans les goroutines que le scanner lance lui-même. Renvoyez des erreurs plutôt que de déclencher un panic, et arrêtez-vous rapidement quandctxest annulé.
make test # toutgo test ./internal/safety/ # un packagego test ./internal/cli/ -run TestCleanYes -v # les tests dont le nom correspondmake lint # vet + vérification gofmtLes tests sur machine réelle analysent votre propre Mac, en lecture seule, et affichent ce qu'ils trouvent. Ils sont ignorés, sauf si vous les activez explicitement :
LU_REAL=1 go test ./internal/providers/apple -run TestRealMachine -vLU_REAL=1 go test ./internal/providers/worktrees -run TestRealMachine -vLa CI tourne sur macOS : gofmt, go vet, go test ./..., make build, et make docs suivi d'une vérification que les pages de référence générées sont bien commitées.
Pages de référence et site
Section intitulée « Pages de référence et site »La documentation se trouve dans site/ (Astro Starlight). Les pages anglaises sont dans site/src/content/docs/, les pages françaises dans site/src/content/docs/fr/.
Deux types de pages sont générés et ne doivent jamais être modifiés à la main :
reference/commands/*.md, une page par commande, à partir des définitions Cobra (Short,Long,Example, options) ;reference/catalog.md, à partir des entrées du catalogue.
Pour les modifier, changez le texte d'aide de la commande ou l'entrée du catalogue, puis lancez :
make docs # go run ./cmd/lu-cleaner gen-docs --out site/src/content/docs/referencePour travailler sur le site :
cd site && npm install && npm run dev # http://localhost:4321/lu-cleaner/make site-dev fait la même chose après avoir régénéré les pages de référence. Les liens internes doivent être absolus, inclure la base /lu-cleaner/ et se terminer par une barre oblique (/lu-cleaner/concepts/safety/, ou /lu-cleaner/fr/concepts/safety/ pour une page française) ; le build les valide. Chaque page a besoin d'un title, d'une description d'une phrase (utilisée aussi pour llms.txt) et d'un sidebar.order.
Revue de sécurité
Section intitulée « Revue de sécurité »Toute pull request qui ajoute ou modifie quelque chose qui peut être supprimé est examinée à l'aune de ces questions. Répondez-y dans la description.
- Qu'est-ce qui est proposé exactement, et comment cela revient-il ? La note doit le dire. Si cela ne revient pas (fichiers utilisateur, enregistrements, sessions), le risque est au moins caution.
- Le risque est-il le niveau honnête le plus bas ? safe signifie un cache pur : aucune donnée utilisateur, régénéré automatiquement. En cas de doute, montez d'un niveau.
- Et si c'est en cours d'utilisation ? Déclarez un
ProcessGuard, un avertissement, ou unRecheckpour un état qui peut changer entre l'analyse et le nettoyage. Les vérifications refusent en cas de doute. - Le chemin pourrait-il être autre chose ? Les noms génériques ont besoin de marqueurs, de signatures de contenu ou d'une vérification que git les ignore. Les motifs glob ne doivent pas correspondre à plus que prévu.
- Le scanner est-il en lecture seule ? Aucune écriture et aucune commande à effet de bord pendant l'analyse.
- La sélection intelligente reste-t-elle prudente ? Forcer
Recommendedexige une raison plus forte que la taille. Les données irréversibles ou incertaines reçoiventNoRecommend. - L'analyse pourrait-elle rester bloquée sur une demande de macOS ? Rien sous le conteneur d'une autre application n'est lu sans accès complet au disque.
- Le garde-fou tient-il toujours ? Les modifications de
internal/safety/guard.goexigent des tests dansinternal/safety/. Ajouter un chemin protégé est toujours bienvenu ; en retirer un exige une très bonne raison.
Testez votre modification avec --dry-run sur votre propre machine, et collez la partie pertinente de la sortie de scan ou de clean --dry-run dans la pull request, en masquant vos propres chemins et noms (~/dev/my-app, feature/login).