From 67368b719fc3268f8d6c51c984c1d8fc7330234d Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 6 Oct 2026 05:51:31 -0400 Subject: [PATCH] [ADD] scripts: Windows install and run through PowerShell 5.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install.cmd, install_dev.cmd and run.cmd hand over to PowerShell scripts that place Node of the .node-version major under %LOCALAPPDATA%, checked against the SHA-256 that nodejs.org publishes, then run npm ci: no administrator right is needed. The scripts stay within Windows PowerShell 5.1, shipped with Windows, and carry a UTF-8 byte order mark, without which 5.1 reads them in the system code page and mangles accents. The licence guard now reads .ps1, .cmd, .nix and Makefile. Checked: parsed and run under pwsh in a container, Node for win-x64 downloaded, verified and extracted; not yet run on Windows itself. --- FR --- [ADD] scripts : installer et lancer sous Windows, par PowerShell 5.1 install.cmd, install_dev.cmd et run.cmd passent la main à des scripts PowerShell qui posent Node de la majeure de .node-version sous %LOCALAPPDATA%, vérifié contre la somme SHA-256 que publie nodejs.org, puis lancent npm ci : aucun droit d'administrateur n'est requis. Les scripts s'en tiennent à Windows PowerShell 5.1, livré avec Windows, et portent une marque d'ordre d'octets UTF-8, sans laquelle 5.1 les lit dans la page de code du système et défigure les accents. La garde de licence lit désormais les .ps1, .cmd, .nix et le Makefile. Vérifié : analysés et exécutés sous pwsh en conteneur, Node win-x64 téléchargé, vérifié, extrait ; pas encore exécutés sous Windows même. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb --- .gitattributes | 2 + install.cmd | 6 ++ install_dev.cmd | 6 ++ run.cmd | 6 ++ scripts/windows/avec_node.ps1 | 33 +++++++ scripts/windows/commun.ps1 | 171 ++++++++++++++++++++++++++++++++++ scripts/windows/installer.ps1 | 23 +++++ scripts/windows/lancer.ps1 | 38 ++++++++ test/licence.test.js | 56 +++++++---- 9 files changed, 325 insertions(+), 16 deletions(-) create mode 100644 .gitattributes create mode 100644 install.cmd create mode 100644 install_dev.cmd create mode 100644 run.cmd create mode 100644 scripts/windows/avec_node.ps1 create mode 100644 scripts/windows/commun.ps1 create mode 100644 scripts/windows/installer.ps1 create mode 100644 scripts/windows/lancer.ps1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4b887bf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.cmd text eol=crlf diff --git a/install.cmd b/install.cmd new file mode 100644 index 0000000..de09565 --- /dev/null +++ b/install.cmd @@ -0,0 +1,6 @@ +@REM © 2026 TechnoLibre (http://www.technolibre.ca) +@REM License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +@echo off +rem Installe de quoi lancer l'application depuis les sources : scripts\windows\installer.ps1 +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\windows\installer.ps1" %* +exit /b %ERRORLEVEL% diff --git a/install_dev.cmd b/install_dev.cmd new file mode 100644 index 0000000..2871f0c --- /dev/null +++ b/install_dev.cmd @@ -0,0 +1,6 @@ +@REM © 2026 TechnoLibre (http://www.technolibre.ca) +@REM License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +@echo off +rem Installe l'environnement de developpement : scripts\windows\installer.ps1 +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\windows\installer.ps1" -Dev %* +exit /b %ERRORLEVEL% diff --git a/run.cmd b/run.cmd new file mode 100644 index 0000000..3c365f3 --- /dev/null +++ b/run.cmd @@ -0,0 +1,6 @@ +@REM © 2026 TechnoLibre (http://www.technolibre.ca) +@REM License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +@echo off +rem Lance l'application depuis les sources : scripts\windows\lancer.ps1 +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\windows\lancer.ps1" %* +exit /b %ERRORLEVEL% diff --git a/scripts/windows/avec_node.ps1 b/scripts/windows/avec_node.ps1 new file mode 100644 index 0000000..0458ba3 --- /dev/null +++ b/scripts/windows/avec_node.ps1 @@ -0,0 +1,33 @@ +# © 2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +# Exécute une commande à la racine du projet, le Node du projet en tête du +# PATH, une fois l'installation vérifiée : c'est par lui que le Makefile +# lance npm, npx et node sous Windows. +# +# powershell -File scripts\windows\avec_node.ps1 npm test +param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Commande) +. (Join-Path $PSScriptRoot 'commun.ps1') + +$elements = @($Commande | Where-Object { $_ }) +if ($elements.Count -eq 0) { + Write-Gtt 'usage : scripts\windows\avec_node.ps1 [argument…]' + exit 2 +} +Initialize-Gtt +Push-Location $script:GttRacine +try { + $programme = $elements[0] + # npm et npx se lancent par leur .cmd : leur variante .ps1 dépend de la + # stratégie d'exécution de la machine. + if (@('npm', 'npx') -contains $programme) { $programme = "$programme.cmd" } + if ($elements.Count -gt 1) { + $reste = $elements[1..($elements.Count - 1)] + & $programme @reste + } else { + & $programme + } + exit $LASTEXITCODE +} finally { + Pop-Location +} diff --git a/scripts/windows/commun.ps1 b/scripts/windows/commun.ps1 new file mode 100644 index 0000000..c218eed --- /dev/null +++ b/scripts/windows/commun.ps1 @@ -0,0 +1,171 @@ +# © 2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +# Fonctions communes des scripts Windows : le Node du projet, posé hors du +# dépôt sous %LOCALAPPDATA%, et la vérification de l'installation. Ce +# fichier se charge par « . ». Il s'en tient à Windows PowerShell 5.1, livré +# avec Windows : ni ??, ni ?., ni opérateur ternaire. Il est enregistré en +# UTF-8 avec marque d'ordre d'octets, sans laquelle Windows PowerShell 5.1 le +# lit dans la page de code du système et défigure les accents. + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3.0 +# Sans ces réglages, Windows PowerShell 5.1 écrit les accents dans la page de +# code de la console, peut refuser TLS 1.2 que nodejs.org exige, et ralentit +# chaque téléchargement par sa barre de progression. +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +$ProgressPreference = 'SilentlyContinue' + +$script:GttRacine = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path + +function Write-Gtt([string]$Message) { + [Console]::Error.WriteLine($Message) +} + +function Stop-Gtt([string]$Message) { + Write-Gtt "Refusé : $Message" + exit 1 +} + +# Majeure de Node que vise le projet ; .node-version en est la seule source. +function Get-GttMajeure { + $majeure = (Get-Content -Raw (Join-Path $script:GttRacine '.node-version')).Trim() + if ($majeure -notmatch '^[0-9]+$') { + Stop-Gtt ".node-version doit porter une majeure entière : « $majeure »." + } + return $majeure +} + +# Architecture au sens de nodejs.org. PROCESSOR_ARCHITEW6432 nomme celle du +# système quand PowerShell lui-même tourne en 32 bits. +function Get-GttArchNode { + $arch = $env:PROCESSOR_ARCHITEW6432 + if (-not $arch) { $arch = $env:PROCESSOR_ARCHITECTURE } + switch ($arch) { + 'AMD64' { return 'x64' } + 'ARM64' { return 'arm64' } + default { Stop-Gtt "architecture $arch : nodejs.org n'y publie pas Node pour Windows." } + } +} + +function Get-GttDossierDonnees { + return (Join-Path $env:LOCALAPPDATA 'gestion_table_tournante_libre') +} + +function Get-GttDossierNode { + return (Join-Path (Get-GttDossierDonnees) ('node-{0}-win-{1}' -f (Get-GttMajeure), (Get-GttArchNode))) +} + +# Somme et nom de l'archive .zip d'une architecture dans les lignes d'un +# SHASUMS256.txt de nodejs.org ; $null quand elle n'y figure pas. +function Find-GttArchiveNode([string[]]$Lignes, [string]$Arch) { + $suffixe = "-win-$Arch.zip" + foreach ($ligne in $Lignes) { + $champs = @($ligne.Trim() -split '\s+') + if ($champs.Count -ge 2 -and $champs[1] -match '^node-v[0-9]+\.[0-9]+\.[0-9]+-' -and + $champs[1].EndsWith($suffixe, [StringComparison]::Ordinal)) { + return @($champs[0], $champs[1]) + } + } + return $null +} + +# Pose la dernière version publiée de la majeure voulue, une fois. L'archive +# est vérifiée contre la somme que publie nodejs.org, puis extraite à côté de +# la cible et renommée : une extraction interrompue ne laisse jamais une +# cible à moitié écrite. +function Install-GttNode { + $majeure = Get-GttMajeure + $arch = Get-GttArchNode + $cible = Get-GttDossierNode + $node = Join-Path $cible 'node.exe' + if (Test-Path $node) { + Write-Gtt ('Node {0} : {1}' -f (& $node --version), $cible) + return + } + $cache = Join-Path (Get-GttDossierDonnees) 'cache' + New-Item -ItemType Directory -Force -Path $cache | Out-Null + $base = "https://nodejs.org/dist/latest-v$majeure.x" + $sommes = Join-Path $cache "SHASUMS256-v$majeure.txt" + Invoke-WebRequest -UseBasicParsing -Uri "$base/SHASUMS256.txt" -OutFile $sommes + $trouve = Find-GttArchiveNode (Get-Content $sommes) $arch + if (-not $trouve) { Stop-Gtt "nodejs.org ne publie pas Node $majeure pour win-$arch." } + $somme = $trouve[0] + $nom = $trouve[1] + $archive = Join-Path $cache $nom + if (-not (Test-Path $archive) -or (Get-FileHash -Algorithm SHA256 $archive).Hash -ne $somme) { + Write-Gtt "Téléchargement de $nom" + $partiel = "$archive.$PID.partiel" + Invoke-WebRequest -UseBasicParsing -Uri "$base/$nom" -OutFile $partiel + if ((Get-FileHash -Algorithm SHA256 $partiel).Hash -ne $somme) { + Remove-Item -Force $partiel + Stop-Gtt "$nom : la somme SHA-256 ne correspond pas à celle que publie nodejs.org." + } + Move-Item -Force $partiel $archive + } + $tmp = Join-Path (Get-GttDossierDonnees) ('extraction.' + [guid]::NewGuid().ToString('N')) + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($archive, $tmp) + if (Test-Path $cible) { Remove-Item -Recurse -Force $cible } + Move-Item (Join-Path $tmp ([IO.Path]::GetFileNameWithoutExtension($nom))) $cible + Remove-Item -Force $tmp + Write-Gtt ('Node {0} posé : {1}' -f (& $node --version), $cible) +} + +# Met le Node du projet en tête du PATH de la séance ; faux quand il n'est +# pas posé. +function Enable-GttNode { + $cible = Get-GttDossierNode + if (-not (Test-Path (Join-Path $cible 'node.exe'))) { return $false } + $env:Path = "$cible;$env:Path" + return $true +} + +# Vrai quand les dépendances npm suivent package-lock.json : npm écrit +# node_modules\.package-lock.json à chaque installation. +function Test-GttDependances { + $recu = Join-Path $script:GttRacine 'node_modules\.package-lock.json' + if (-not (Test-Path $recu)) { return $false } + $verrou = Get-Item (Join-Path $script:GttRacine 'package-lock.json') + return ($verrou.LastWriteTimeUtc -le (Get-Item $recu).LastWriteTimeUtc) +} + +# Vrai sur une réponse affirmative — o, oui, y, yes. Le défaut est non, et +# une entrée redirigée ne consent à rien. GTT_OUI=1 répond oui d'avance. +function Confirm-Gtt([string]$Question) { + if ($env:GTT_OUI -eq '1') { return $true } + if ([Console]::IsInputRedirected) { return $false } + $reponse = Read-Host "$Question [o/N]" + return (@('o', 'oui', 'y', 'yes') -contains $reponse.Trim().ToLowerInvariant()) +} + +# Exécute un programme et refuse quand il échoue, en le nommant. +function Invoke-Gtt([string]$Programme, [string[]]$Arguments) { + & $Programme @Arguments + if ($LASTEXITCODE -ne 0) { Stop-Gtt "$Programme $($Arguments -join ' ') échoue (code $LASTEXITCODE)." } +} + +function Invoke-GttNpmCi { + Invoke-Gtt 'npm.cmd' @('ci', '--no-audit', '--no-fund') +} + +# Rend prêt à exécuter : le Node du projet en tête du PATH, et des +# dépendances npm qui suivent package-lock.json. Node absent, il propose +# install.cmd ; des dépendances en retard se réinstallent sans question. +function Initialize-Gtt { + if (-not (Enable-GttNode)) { + Write-Gtt "Node $(Get-GttMajeure) n'est pas posé." + if (-not (Confirm-Gtt 'Lancer install.cmd (Node, dépendances npm) ?')) { + Stop-Gtt 'installation non lancée. Lancer install.cmd, puis recommencer.' + } + & (Join-Path $PSScriptRoot 'installer.ps1') + if (-not (Enable-GttNode)) { Stop-Gtt "Node $(Get-GttMajeure) reste introuvable après l'installation." } + } + if (-not (Test-GttDependances)) { + Write-Gtt 'Les dépendances npm ne suivent pas package-lock.json : npm ci.' + Push-Location $script:GttRacine + try { Invoke-GttNpmCi } finally { Pop-Location } + } +} diff --git a/scripts/windows/installer.ps1 b/scripts/windows/installer.ps1 new file mode 100644 index 0000000..eab16cd --- /dev/null +++ b/scripts/windows/installer.ps1 @@ -0,0 +1,23 @@ +# © 2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +# Installe sous Windows de quoi lancer l'application depuis les sources et +# jouer les épreuves : Node de la majeure de .node-version, posé sous +# %LOCALAPPDATA% sans droits d'administrateur, et les dépendances de +# package-lock.json. -Dev ajoute le Chromium de Playwright des épreuves du +# navigateur. install.cmd et install_dev.cmd l'appellent. +param([switch]$Dev) +. (Join-Path $PSScriptRoot 'commun.ps1') + +Install-GttNode +[void](Enable-GttNode) +Push-Location $script:GttRacine +try { + Invoke-GttNpmCi + if ($Dev) { + Invoke-Gtt 'npx.cmd' @('--no-install', 'playwright', 'install', '--only-shell', 'chromium') + } +} finally { + Pop-Location +} +Write-Gtt 'Installation terminée. Lancer : run.cmd' diff --git a/scripts/windows/lancer.ps1 b/scripts/windows/lancer.ps1 new file mode 100644 index 0000000..171be51 --- /dev/null +++ b/scripts/windows/lancer.ps1 @@ -0,0 +1,38 @@ +# © 2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +# Lance l'application depuis les sources, sous Windows : la coquille +# Electron charge la page que Vite construit dans www\ (§ 13.1). run.cmd +# l'appelle, ses options sont celles de run.sh. +# +# run.cmd Electron +# run.cmd --web le serveur de Vite, http://localhost:5173 +param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Options) +. (Join-Path $PSScriptRoot 'commun.ps1') + +$mode = 'electron' +foreach ($option in @($Options | Where-Object { $_ })) { + switch ($option) { + '--web' { $mode = 'web' } + '--electron' { $mode = 'electron' } + default { + Write-Gtt 'usage : run.cmd [--web | --electron]' + exit 2 + } + } +} + +Initialize-Gtt +Push-Location $script:GttRacine +try { + if ($mode -eq 'web') { + Write-Gtt "Serveur de développement de Vite : http://localhost:5173 (Ctrl+C pour l'arrêter)." + & npm.cmd run --silent dev + exit $LASTEXITCODE + } + Invoke-Gtt 'npm.cmd' @('run', '--silent', 'build') + & npx.cmd --no-install electron . + exit $LASTEXITCODE +} finally { + Pop-Location +} diff --git a/test/licence.test.js b/test/licence.test.js index 1e305bc..f6cf892 100644 --- a/test/licence.test.js +++ b/test/licence.test.js @@ -3,12 +3,13 @@ // Chaque fichier source du projet s'ouvre sur l'en-tête de licence : la ligne // du titulaire, puis celle de la licence, après un éventuel shebang ou la -// déclaration . Les deux lignes sont des commentaires du +// déclaration , et après la marque d'ordre d'octets que +// portent les scripts PowerShell. Les deux lignes sont des commentaires du // langage du fichier ; le texte ne varie pas. package.json déclare la même // licence et le même titulaire, que reprennent les outils d'emballage. import assert from 'node:assert/strict'; import { readdirSync, readFileSync } from 'node:fs'; -import { extname, join, relative, sep } from 'node:path'; +import { basename, extname, join, relative, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, test } from './lanceur.js'; @@ -17,8 +18,11 @@ const RACINE = fileURLToPath(new URL('..', import.meta.url)); const TITULAIRE = /^© \d{4} TechnoLibre \(http:\/\/www\.technolibre\.ca\)$/; const LICENCE = 'License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)'; -// Marqueurs de commentaire, par extension de fichier source. Un commentaire -// de ligne n'a pas de fermant. +// Marqueurs de commentaire, par extension de fichier source, ou par nom pour +// un fichier sans extension. Un commentaire de ligne n'a pas de fermant. Un +// ouvrant est une chaîne qui ouvre la ligne, ou un motif : REM ouvre une +// ligne de commandes Windows, précédé ou non de @, quelle que soit sa casse, +// et suivi d'un blanc — @REMARQUE est une commande. const COMMENTAIRE = new Map([ ['.js', { ouvrant: '//', fermant: null }], ['.cjs', { ouvrant: '//', fermant: null }], @@ -27,8 +31,16 @@ const COMMENTAIRE = new Map([ ['.svelte', { ouvrant: '' }], ['.html', { ouvrant: '' }], ['.css', { ouvrant: '/*', fermant: '*/' }], + ['.ps1', { ouvrant: '#', fermant: null }], + ['.nix', { ouvrant: '#', fermant: null }], + ['.cmd', { ouvrant: /^@?rem(?=\s)/i, fermant: null }], + ['Makefile', { ouvrant: '#', fermant: null }], ]); +// Clé d'un fichier dans COMMENTAIRE : son nom quand il y figure, son +// extension sinon. +const cle = (chemin) => (COMMENTAIRE.has(basename(chemin)) ? basename(chemin) : extname(chemin)); + // Périmètre : les fichiers de la racine et les arbres des sources, des // épreuves, des scripts et de la coquille. Les dépendances et les sorties de // construction n'y entrent pas, ni les données d'épreuve, qui sont des @@ -55,7 +67,7 @@ function fichiersSources() { } } return chemins - .filter((chemin) => COMMENTAIRE.has(extname(chemin))) + .filter((chemin) => COMMENTAIRE.has(cle(chemin))) .filter((chemin) => !chemin.startsWith(DONNEES_EPREUVE)) .sort(); } @@ -69,8 +81,9 @@ function fichiersSources() { // invalidée dans une feuille, une commande dans un script. function enCommentaire(premiere, seconde, { ouvrant, fermant }) { const suite = seconde.trim(); - if (!premiere.startsWith(ouvrant)) return false; - if (fermant === null) return suite.startsWith(ouvrant); + const ouvre = (ligne) => (typeof ouvrant === 'string' ? ligne.startsWith(ouvrant) : ouvrant.test(ligne)); + if (!ouvre(premiere)) return false; + if (fermant === null) return ouvre(suite); const courant = !premiere.includes(fermant) && suite.endsWith(fermant); const repete = premiere.trimEnd().endsWith(fermant) && @@ -79,21 +92,22 @@ function enCommentaire(premiere, seconde, { ouvrant, fermant }) { return courant || repete; } -// Vrai quand le contenu s'ouvre sur l'en-tête, après un éventuel shebang ou -// la déclaration : deux lignes de commentaire du langage de -// l'extension, dont le texte, débarrassé des marqueurs, est celui de -// l'en-tête. -function porteLEnTete(contenu, extension) { - let lignes = contenu.split('\n'); +// Vrai quand le contenu s'ouvre sur l'en-tête, après une éventuelle marque +// d'ordre d'octets, un shebang ou la déclaration : deux +// lignes de commentaire du langage de la clé, extension ou nom, dont le +// texte, débarrassé des marqueurs, est celui de l'en-tête. Les fins de ligne +// sont celles d'Unix ou de Windows. +function porteLEnTete(contenu, cleFichier) { + let lignes = contenu.replace(/^\uFEFF/, '').split(/\r?\n/); if (lignes[0].startsWith('#!')) lignes = lignes.slice(1); if (/^$/i.test(lignes[0])) lignes = lignes.slice(1); const [premiere = '', seconde = ''] = lignes; const texte = (ligne) => ligne - .replace(/^\s*(?:\/\/|#||\*\/)?\s*$/, ''); return ( - enCommentaire(premiere, seconde, COMMENTAIRE.get(extension)) && + enCommentaire(premiere, seconde, COMMENTAIRE.get(cleFichier)) && TITULAIRE.test(texte(premiere)) && texte(seconde) === LICENCE ); @@ -104,7 +118,7 @@ describe('licence', () => { const fichiers = fichiersSources(); assert.ok(fichiers.length > 0, 'aucun fichier source examiné'); const sansEnTete = fichiers - .filter((chemin) => !porteLEnTete(readFileSync(chemin, 'utf8'), extname(chemin))) + .filter((chemin) => !porteLEnTete(readFileSync(chemin, 'utf8'), cle(chemin))) .map((chemin) => relative(RACINE, chemin)); assert.deepEqual(sansEnTete, []); }); @@ -129,6 +143,16 @@ describe('licence', () => { ['.html', `\n`, false], ['.css', `/* ${titulaire} */\n${LICENCE}\n`, false], ['.css', `/* ${titulaire} */\n * ${LICENCE} */\n`, false], + ['.ps1', `\uFEFF# ${titulaire}\r\n# ${LICENCE}\r\n`, true], + ['.ps1', `# ${titulaire}\n# ${LICENCE}\n`, true], + ['.nix', `# ${titulaire}\n# ${LICENCE}\n`, true], + ['.cmd', `@REM ${titulaire}\r\n@REM ${LICENCE}\r\n@echo off\r\n`, true], + ['.cmd', `REM ${titulaire}\r\nrem ${LICENCE}\r\n`, true], + ['Makefile', `# ${titulaire}\n# ${LICENCE}\n`, true], + ['.ps1', `\uFEFF# ${titulaire}\r\nWrite-Host '${LICENCE}'\r\n`, false], + ['.cmd', `@REM ${titulaire}\r\n@echo ${LICENCE}\r\n`, false], + ['.cmd', `@REMARQUE ${titulaire}\r\n@REM ${LICENCE}\r\n`, false], + ['Makefile', `# ${titulaire}\n\t${LICENCE}\n`, false], ]; const ecarts = cas .filter(([extension, contenu, admis]) => porteLEnTete(contenu, extension) !== admis)