@roomi-fields/grillhouse 0.0.0-stage → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/METACADRE.md +41 -0
  3. package/agents/developpeur.md +10 -0
  4. package/agents/explorateur.md +9 -0
  5. package/agents/questionneur.md +11 -0
  6. package/agents/referent-technique.md +9 -0
  7. package/agents/regulateur.md +11 -0
  8. package/agents/relecteur.md +9 -0
  9. package/agents/testwriter.md +10 -0
  10. package/bin/grillhouse.mjs +125 -0
  11. package/cadre/templates/projet/.claude/settings.json +30 -0
  12. package/cadre/templates/projet/.editorconfig +47 -0
  13. package/cadre/templates/projet/.env.example +57 -0
  14. package/cadre/templates/projet/.eslintrc.cjs +56 -0
  15. package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
  16. package/cadre/templates/projet/.mcp.json +13 -0
  17. package/cadre/templates/projet/.prettierrc +11 -0
  18. package/cadre/templates/projet/CHANGELOG.md +61 -0
  19. package/cadre/templates/projet/CLAUDE.md +46 -0
  20. package/cadre/templates/projet/CONTRIBUTING.md +243 -0
  21. package/cadre/templates/projet/CREDITS.md +41 -0
  22. package/cadre/templates/projet/README.md +152 -0
  23. package/cadre/templates/projet/_gitignore +91 -0
  24. package/cadre/templates/projet/_npmignore +46 -0
  25. package/cadre/templates/projet/deployment/README.md +98 -0
  26. package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
  27. package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
  28. package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
  29. package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
  30. package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
  31. package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
  32. package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
  33. package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
  34. package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
  35. package/cadre/templates/projet/ecosystem.config.cjs +73 -0
  36. package/cadre/templates/projet/package.json +90 -0
  37. package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
  38. package/cadre/templates/projet/src/config.ts +68 -0
  39. package/cadre/templates/projet/src/errors.ts +102 -0
  40. package/cadre/templates/projet/src/index.ts +46 -0
  41. package/cadre/templates/projet/src/services/.gitkeep +8 -0
  42. package/cadre/templates/projet/src/types/index.ts +24 -0
  43. package/cadre/templates/projet/src/utils/logger.ts +106 -0
  44. package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
  45. package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
  46. package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
  47. package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
  48. package/cadre/templates/projet/tsconfig.eslint.json +9 -0
  49. package/cadre/templates/projet/tsconfig.json +41 -0
  50. package/cadre/templates/projet/tsdoc.json +6 -0
  51. package/cadre/templates/projet/vitest.config.ts +44 -0
  52. package/cadre/templates/turbo/paquets.json +29 -0
  53. package/cadre/templates/turbo/sans-paquets.json +28 -0
  54. package/charte.md +107 -0
  55. package/docs/ARCHITECTURE.md +90 -0
  56. package/hooks/crochet.mjs +33 -0
  57. package/hooks/hooks.json +58 -0
  58. package/hooks/register.tsx +558 -0
  59. package/package.json +75 -4
  60. package/scripts/avancement.mjs +244 -0
  61. package/scripts/bilan.mjs +523 -0
  62. package/scripts/branche-de-nuit.mjs +59 -0
  63. package/scripts/cadre.mjs +109 -0
  64. package/scripts/circuit.mjs +4375 -0
  65. package/scripts/code-mort.mjs +371 -0
  66. package/scripts/composants-affectes.mjs +598 -0
  67. package/scripts/consommateurs-arbitres.mjs +192 -0
  68. package/scripts/consommateurs.mjs +310 -0
  69. package/scripts/crochet.mjs +24 -0
  70. package/scripts/crochets.mjs +135 -0
  71. package/scripts/dernier-vert.mjs +208 -0
  72. package/scripts/dette.mjs +181 -0
  73. package/scripts/droits.mjs +354 -0
  74. package/scripts/enveloppe/codes.mjs +17 -0
  75. package/scripts/enveloppe/composant.mjs +11 -0
  76. package/scripts/enveloppe/enveloppe.mjs +628 -0
  77. package/scripts/enveloppe/lancer.sh +311 -0
  78. package/scripts/etapes.mjs +323 -0
  79. package/scripts/etat-du-circuit.mjs +104 -0
  80. package/scripts/frontieres.mjs +456 -0
  81. package/scripts/gardes-du-cadre.mjs +72 -0
  82. package/scripts/greffons.mjs +338 -0
  83. package/scripts/grillhouse-maj.mjs +514 -0
  84. package/scripts/index-interfaces.mjs +293 -0
  85. package/scripts/init.mjs +117 -0
  86. package/scripts/integration/file-attente.mjs +61 -0
  87. package/scripts/integration/file.mjs +494 -0
  88. package/scripts/integration/integrer.mjs +381 -0
  89. package/scripts/integration/rejeu.mjs +139 -0
  90. package/scripts/integration/verdict.mjs +13 -0
  91. package/scripts/interfaces-contre-code.mjs +547 -0
  92. package/scripts/ligne-etat/defaut.sh +110 -0
  93. package/scripts/ligne-etat/ligne-etat.mjs +93 -0
  94. package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
  95. package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
  96. package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
  97. package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
  98. package/scripts/migrations.mjs +43 -0
  99. package/scripts/nuit.sh +269 -0
  100. package/scripts/paquets.mjs +79 -0
  101. package/scripts/poussee-cles.mjs +46 -0
  102. package/scripts/preparation.mjs +84 -0
  103. package/scripts/references-par-les-types.mjs +420 -0
  104. package/scripts/registre.mjs +336 -0
  105. package/scripts/resultats.mjs +580 -0
  106. package/scripts/resumes.mjs +57 -0
  107. package/scripts/retirer-copie.sh +127 -0
  108. package/scripts/session-start.sh +203 -0
  109. package/scripts/sortie.mjs +155 -0
  110. package/scripts/sous-modules.sh +316 -0
  111. package/scripts/structure-guard.sh +27 -0
  112. package/scripts/superviseur.mjs +102 -0
  113. package/scripts/tableau.mjs +1886 -0
  114. package/scripts/tests.mjs +236 -0
  115. package/scripts/ticket.sh +23 -0
  116. package/scripts/turbo-des-paquets.mjs +899 -0
  117. package/scripts/un-commit-par-ticket.mjs +212 -0
  118. package/scripts/verifier.mjs +469 -0
  119. package/scripts/verrou-de-fichier.mjs +130 -0
  120. package/scripts/verrou-de-machine.mjs +211 -0
  121. package/scripts/verrous/commande.mjs +180 -0
  122. package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
  123. package/scripts/verrous/verrou.mjs +165 -0
  124. package/scripts/vitest-config/avancement.mjs +39 -0
  125. package/scripts/vitest-config/avancement.reporter.mjs +56 -0
  126. package/scripts/vitest-config/base-de-passe.mjs +128 -0
  127. package/scripts/vitest-config/base.d.ts +6 -0
  128. package/scripts/vitest-config/base.mjs +87 -0
  129. package/scripts/vitest-config/changements.mjs +173 -0
  130. package/scripts/vitest-config/cibles.mjs +207 -0
  131. package/scripts/vitest-config/duree.reporter.mjs +98 -0
  132. package/scripts/vitest-config/machine.setup.mjs +14 -0
  133. package/scripts/vitest-config/mesure.d.ts +6 -0
  134. package/scripts/vitest-config/mesure.mjs +13 -0
  135. package/scripts/vitest-config/mesure.setup.mjs +12 -0
  136. package/scripts/vitest-config/pression.mjs +90 -0
  137. package/scripts/vitest-config/resultats.mjs +23 -0
  138. package/scripts/vitest-config/resultats.setup.mjs +19 -0
  139. package/scripts/vitest.nuit.config.mjs +26 -0
  140. package/skills/developpeur/SKILL.md +175 -0
  141. package/skills/explorateur/SKILL.md +84 -0
  142. package/skills/grill/SKILL.md +238 -0
  143. package/skills/grill/references/releve-structurel.md +38 -0
  144. package/skills/mesure/SKILL.md +168 -0
  145. package/skills/pitmaster/SKILL.md +382 -0
  146. package/skills/pitmaster/references/cadre.md +119 -0
  147. package/skills/pitmaster/references/consigne-agent.md +308 -0
  148. package/skills/pitmaster/references/erreurs.md +25 -0
  149. package/skills/pitmaster/references/tour.md +124 -0
  150. package/skills/questionneur/SKILL.md +64 -0
  151. package/skills/redacteur/SKILL.md +84 -0
  152. package/skills/redacteur/references/documents-d-un-composant.md +105 -0
  153. package/skills/referent-technique/SKILL.md +145 -0
  154. package/skills/regulateur/SKILL.md +220 -0
  155. package/skills/release/SKILL.md +134 -0
  156. package/skills/release/scripts/update-version.cjs +92 -0
  157. package/skills/relecteur/SKILL.md +152 -0
  158. package/skills/relecteur/references/LICENSE-MIT +30 -0
  159. package/skills/relecteur/references/SOURCE.md +8 -0
  160. package/skills/relecteur/references/claims-check.md +14 -0
  161. package/skills/relecteur/references/deletion-check.md +14 -0
  162. package/skills/relecteur/references/edge-case-hunter.md +80 -0
  163. package/skills/relecteur/references/verification-gap.md +113 -0
  164. package/skills/testwriter/SKILL.md +187 -0
  165. package/types/index.d.ts +56 -0
  166. package/README.md +0 -3
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env node
2
+ // The dead-code guard: refuses a file, an export, a type or a dependency that nothing living
3
+ // uses, measured by knip (configured in the project's `knip.json`). Run by `grillhouse:gardes`.
4
+ //
5
+ // - Each finding is named with its file; exit code 1 when there is one. A finding inscribed in the
6
+ // project's debt (`grillhouse-dette.json`, see `dette.mjs`) passes, keyed by its file and knip's
7
+ // kind of issue (`files`, `exports`, `types`, `dependencies`…).
8
+ // - A knip that measured nothing (it failed, or its output is not its report) refuses too: an
9
+ // empty green is the worst green.
10
+ // - A finding that is not dead (a format's full table, a public name a consumer outside the
11
+ // repository reads) leaves by its name in `knip.json`, with its reason, never by a wide pattern.
12
+ // - The packages listed out of the frame (`docs/agents/hors-cadre.txt`, read by `horsCadre` of
13
+ // `paquets.mjs`) are skipped: knip neither reads nor reports them, so a use they make
14
+ // keeps nothing alive. knip then runs on the project's `knip.json` (knip's defaults without
15
+ // one) plus these exclusions and the frame's entries, written to a temporary file outside the
16
+ // repository; the project's `knip.json` is never written.
17
+ // - The tests and benches of a component count for the use of its dependencies only. knip runs a
18
+ // first time on the settings above and gives every finding. It runs a second time, restricted
19
+ // to the dependency findings, with the test and bench files of the repository as its only
20
+ // entries, each place receiving its own files: the root (its `.` key, which knip reads as soon
21
+ // as there are `workspaces`) those that no package holds, each package of the repository those
22
+ // under its directory, the deepest package holding a file owning it. The packages are the
23
+ // workspaces knip makes, with its own glob matcher (`picomatch`, loaded from the repository's
24
+ // knip): the directories holding a `package.json` that the `workspaces` of the root
25
+ // `package.json` select or a key of the `knip.json` names, exact or by pattern. Each place
26
+ // receives the settings knip gives it (`getWorkspaceConfig`): those of the key knip chooses
27
+ // (`getConfigKeyForWorkspace`: the keys sorted by knip's `byPathDepth`, the last one that
28
+ // matches), the whole `knip.json` for the root without `workspaces`, empty settings when no key
29
+ // matches; kept to those knip's schema admits in a workspace key, the top-level-only ones
30
+ // (`$schema`, `rules`…) staying at the top level. So a pattern key (`packages/*`) holds for
31
+ // each package it names and a package no key names is covered too. A dependency is unused when
32
+ // both runs report it, so the entries knip takes by itself stay in the first run. The files,
33
+ // exports and types findings are those of the first run.
34
+ // - The test and bench files are those git does not ignore (`.gitignore`, as knip reads it): a
35
+ // ticket copy or a build that git ignores counts for nothing. A test file is known by the
36
+ // repository's rule (`isTestFile`, `verrous/verrou.mjs`), plus the benches `*.budget.*` and
37
+ // `*.nuit.*`. A file the top-level or workspace `ignore` targets is not an entry: these patterns
38
+ // become negated entry patterns, which knip matches as its own.
39
+ import { spawnSync } from 'node:child_process';
40
+ import { createRequire } from 'node:module';
41
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
42
+ import { tmpdir } from 'node:os';
43
+ import path from 'node:path';
44
+ import { pathToFileURL } from 'node:url';
45
+ import { horsCadre } from './paquets.mjs';
46
+ import { trancher } from './dette.mjs';
47
+ import { isTestFile } from './verrous/verrou.mjs';
48
+
49
+ const GENRES = {
50
+ files: 'fichier sans appelant',
51
+ exports: 'export sans appelant',
52
+ types: 'type exporté sans appelant',
53
+ enumMembers: "membre d'énumération sans appelant",
54
+ dependencies: 'dépendance sans usage',
55
+ devDependencies: 'dépendance de développement sans usage',
56
+ unlisted: 'dépendance non déclarée',
57
+ unresolved: 'import introuvable',
58
+ binaries: 'commande non déclarée',
59
+ duplicates: 'export en double',
60
+ };
61
+
62
+ // The findings of knip's JSON report, of the given kinds (all by default), each
63
+ // `{ fichier, regle, message }`: its file, knip's kind of issue (`exports`, `dependencies`…) and
64
+ // its line; null when the report is not knip's.
65
+ export function constats(texte, genres = Object.keys(GENRES)) {
66
+ let r;
67
+ try {
68
+ r = JSON.parse(texte);
69
+ } catch {
70
+ return null;
71
+ }
72
+ if (!Array.isArray(r?.issues)) {
73
+ return null;
74
+ }
75
+ const out = [];
76
+ for (const i of r.issues) {
77
+ for (const genre of genres) {
78
+ const nom = GENRES[genre];
79
+ for (const x of i[genre] ?? []) {
80
+ const n = Array.isArray(x) ? x.map(y => y.name).join(', ') : x.name;
81
+ out.push({
82
+ fichier: i.file,
83
+ ...(Array.isArray(x) || !x.line ? {} : { ligne: x.line }),
84
+ regle: genre,
85
+ message: genre === 'files' ? `${i.file} : ${nom}` : `${i.file} : ${nom} — ${n}`,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+
93
+ const liste = v => (v === undefined ? [] : [v].flat());
94
+
95
+ // The knip settings of the repository at <racine> with its packages out of the frame skipped:
96
+ // its `knip.json` (empty without one) plus `ignoreWorkspaces` for each listed directory under
97
+ // `packages/` and `ignore` for each listed directory under `packages/` and `src/`; null when the
98
+ // list names nothing.
99
+ function reglages(racine) {
100
+ const hors = [...horsCadre(racine)].sort();
101
+ if (!hors.length) {
102
+ return null;
103
+ }
104
+ const fichier = path.join(racine, 'knip.json');
105
+ const r = existsSync(fichier) ? JSON.parse(readFileSync(fichier, 'utf8')) : {};
106
+ const espaces = [];
107
+ const motifs = [];
108
+ for (const nom of hors) {
109
+ for (const parent of ['packages', 'src']) {
110
+ if (!existsSync(path.join(racine, parent, nom))) {
111
+ continue;
112
+ }
113
+ if (parent === 'packages') {
114
+ espaces.push(`packages/${nom}`);
115
+ }
116
+ motifs.push(`${parent}/${nom}/**`);
117
+ }
118
+ }
119
+ return {
120
+ ...r,
121
+ ignore: [...liste(r.ignore), ...motifs],
122
+ ignoreWorkspaces: [...liste(r.ignoreWorkspaces), ...espaces],
123
+ };
124
+ }
125
+
126
+ const CODE = /\.[cm]?[jt]sx?$/;
127
+ const BANC = /\.(budget|nuit)\.[cm]?[jt]sx?$/;
128
+
129
+ // The test and bench files of the repository at <racine>, relative to it with `/` separators,
130
+ // as `fichiers`, and the directories holding a `package.json` as `dossiers` (`.` for the root);
131
+ // `erreur` is git's message when git fails. git lists every file its ignore rules
132
+ // (`.gitignore`) leave in against a fresh empty index, so a file counts whether tracked or not,
133
+ // and the user's global ignore file stays out, as knip reads them; a file under `node_modules`
134
+ // stays out, as knip skips it.
135
+ function fichiersTests(racine) {
136
+ const index = mkdtempSync(path.join(tmpdir(), 'code-mort-git-'));
137
+ try {
138
+ const init = spawnSync('git', ['init', '-q', '--bare', '--template=', index], {
139
+ encoding: 'utf8',
140
+ });
141
+ const r =
142
+ init.status === 0 &&
143
+ spawnSync(
144
+ 'git',
145
+ [
146
+ '--git-dir',
147
+ index,
148
+ '--work-tree',
149
+ '.',
150
+ '-c',
151
+ 'core.excludesFile=',
152
+ 'ls-files',
153
+ '-o',
154
+ '--exclude-standard',
155
+ '-z',
156
+ ],
157
+ { cwd: racine, encoding: 'utf8', maxBuffer: 1 << 28 }
158
+ );
159
+ if (!r || r.status !== 0) {
160
+ return { erreur: (r || init).stderr || `git : code ${(r || init).status}` };
161
+ }
162
+ const tous = r.stdout.split('\0').filter(f => f && !f.split('/').includes('node_modules'));
163
+ const fichiers = tous.filter(
164
+ f => CODE.test(f) && (isTestFile(f) || BANC.test(path.posix.basename(f)))
165
+ );
166
+ const dossiers = tous
167
+ .filter(f => path.posix.basename(f) === 'package.json')
168
+ .map(f => path.posix.dirname(f));
169
+ return { fichiers, dossiers };
170
+ } finally {
171
+ rmSync(index, { recursive: true, force: true });
172
+ }
173
+ }
174
+
175
+ // The parts of the repository's `knip` the guard follows, loaded from its own files:
176
+ // `picomatch`, its glob matcher; `byPathDepth` (util/workspace.js), its order of workspace keys;
177
+ // `reglagesEspace`, the settings its schema admits in a workspace key
178
+ // (`workspaceConfigurationSchema`, schema/configuration.js). null when knip is not installed there
179
+ // or lacks one of them (the guard then refuses).
180
+ async function knipDuDepot(racine) {
181
+ try {
182
+ const entree = createRequire(path.join(racine, 'noeud.js')).resolve('knip');
183
+ const dist = path.dirname(entree);
184
+ const module = f => import(pathToFileURL(path.join(dist, f)).href);
185
+ const { byPathDepth } = await module('util/workspace.js');
186
+ const { workspaceConfigurationSchema } = await module('schema/configuration.js');
187
+ const reglagesEspace = new Set(Object.keys(workspaceConfigurationSchema.shape));
188
+ const picomatch = createRequire(entree)('picomatch');
189
+ return typeof byPathDepth === 'function' && reglagesEspace.has('entry')
190
+ ? { picomatch, byPathDepth, reglagesEspace }
191
+ : null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ // The workspace keys knip reads in <config>: those of its `workspaces` (`.` without them) less the
198
+ // ones `ignoreWorkspaces` names (`getConfiguredWorkspaceKeys`).
199
+ const clesConfigurees = config =>
200
+ (config?.workspaces ? Object.keys(config.workspaces) : ['.']).filter(
201
+ k => !liste(config?.ignoreWorkspaces).includes(k)
202
+ );
203
+
204
+ // The key of <cles> knip chooses for the workspace <nom> (`getConfigKeyForWorkspace`): the keys
205
+ // sorted by knip's `byPathDepth`, the deepest one that matches; undefined when none does.
206
+ const cleDe = ({ picomatch, byPathDepth }, cles, nom) =>
207
+ [...cles]
208
+ .sort(byPathDepth)
209
+ .reverse()
210
+ .find(c => picomatch.isMatch(nom, c));
211
+
212
+ // The settings knip gives the workspace <nom> in <config> (`getWorkspaceConfig`): those of the key
213
+ // it chooses, the whole of <config> when that key is `.` without `workspaces`, empty settings when
214
+ // no key matches; kept to the settings a workspace key admits (<knipLu>.reglagesEspace), so they
215
+ // can be written under a key of `workspaces`.
216
+ function reglagesDeEspace(knipLu, config, nom) {
217
+ const cle = cleDe(knipLu, clesConfigurees(config), nom);
218
+ const brut =
219
+ cle === undefined
220
+ ? {}
221
+ : cle === '.' && !('.' in (config?.workspaces ?? {}))
222
+ ? (config ?? {})
223
+ : (config.workspaces[cle] ?? {});
224
+ return Object.fromEntries(Object.entries(brut).filter(([k]) => knipLu.reglagesEspace.has(k)));
225
+ }
226
+
227
+ // The workspaces of the repository at <racine> among <dossiers> (the directories holding a
228
+ // `package.json`), as knip makes them (`getWorkspaces`): those the `workspaces` of the root
229
+ // `package.json` select (negated globs exclude), plus those the keys of <config> name, exact or
230
+ // by pattern, even when the root `package.json` lists them not; the ones the negated globs and
231
+ // `ignoreWorkspaces` of <config> target are left out. The root is not one. Sorted.
232
+ function paquets({ picomatch }, racine, dossiers, config) {
233
+ const fichier = path.join(racine, 'package.json');
234
+ const w = existsSync(fichier) ? JSON.parse(readFileSync(fichier, 'utf8')).workspaces : undefined;
235
+ const globs = liste(Array.isArray(w) ? w : w?.packages).map(g => g.replace(/^\.\//, ''));
236
+ const nies = globs.filter(g => g.startsWith('!')).map(g => g.slice(1));
237
+ const cles = clesConfigurees(config).filter(k => k !== '.');
238
+ const retenus = [...globs.filter(g => !g.startsWith('!')), ...cles];
239
+ const ignores = [...nies, ...liste(config?.ignoreWorkspaces).filter(g => !g.endsWith('!'))];
240
+ const exclus = ignores.length ? picomatch(ignores) : () => false;
241
+ return dossiers
242
+ .filter(d => d !== '.' && retenus.some(g => picomatch.isMatch(d, g)) && !exclus(d))
243
+ .sort();
244
+ }
245
+
246
+ // The settings of the dependency pass: <config> whose `workspaces` are replaced by the places
247
+ // of the run, each receiving the settings knip gives it in <config> (`reglagesDeEspace`, <knipLu>
248
+ // the parts of knip it follows) and as entries its own test and bench files of the repository at
249
+ // <racine>, by absolute path: the `.` key the files no package holds, and one exact key per
250
+ // package <paquets> the files under its directory (the deepest package owns a file). The keys of
251
+ // <config> leave: an exact key per package is the only key knip finds for it. The `ignore`
252
+ // patterns follow as negated entry patterns rooted at their directory: the top-level ones in
253
+ // every place, those of a key in the place it serves; knip matches them as it matches its own
254
+ // patterns, and a file they target is not an entry.
255
+ function reglagesDependances(knipLu, racine, config, fichiers, paquets) {
256
+ const r = config ?? {};
257
+ const absolu = (dossier, m) => path.posix.join(racine, dossier === '.' ? '' : dossier, m);
258
+ const sauf = (dossier, motifs) => liste(motifs).map(m => `!${absolu(dossier, m)}`);
259
+ const proprio = f =>
260
+ paquets.filter(p => f.startsWith(`${p}/`)).sort((a, b) => b.length - a.length)[0] ?? '.';
261
+ const parPlace = new Map([['.', []], ...paquets.map(p => [p, []])]);
262
+ for (const f of fichiers) {
263
+ parPlace.get(proprio(f))?.push(path.posix.join(racine, f));
264
+ }
265
+ const racineIgnore = sauf('.', r.ignore);
266
+ const reglagesRacine = reglagesDeEspace(knipLu, r, '.');
267
+ const entryRacine = [...parPlace.get('.'), ...racineIgnore, ...sauf('.', reglagesRacine.ignore)];
268
+ if (!paquets.length && !r.workspaces) {
269
+ return { ...r, entry: entryRacine };
270
+ }
271
+ const espaces = {};
272
+ espaces['.'] = { ...reglagesRacine, entry: entryRacine };
273
+ for (const p of paquets) {
274
+ const reglages = reglagesDeEspace(knipLu, r, p);
275
+ espaces[p] = {
276
+ ...reglages,
277
+ entry: [...parPlace.get(p), ...racineIgnore, ...sauf(p, reglages.ignore)],
278
+ };
279
+ }
280
+ return { ...r, entry: entryRacine, workspaces: espaces };
281
+ }
282
+
283
+ // Runs knip on <config> (knip's defaults when null), restricted to <include> when given; the
284
+ // spawn result.
285
+ function knip(config, include) {
286
+ const args = ['--no-install', 'knip', '--reporter', 'json', '--no-progress'];
287
+ if (include) {
288
+ args.push('--include', include);
289
+ }
290
+ let tmp = null;
291
+ try {
292
+ if (config) {
293
+ tmp = mkdtempSync(path.join(tmpdir(), 'code-mort-'));
294
+ const f = path.join(tmp, 'knip.json');
295
+ writeFileSync(f, JSON.stringify(config));
296
+ args.push('--config', f);
297
+ }
298
+ return spawnSync('npx', args, { encoding: 'utf8' });
299
+ } finally {
300
+ if (tmp) {
301
+ rmSync(tmp, { recursive: true, force: true });
302
+ }
303
+ }
304
+ }
305
+
306
+ // The command on the repository at <racine>: its exit code.
307
+ async function codeMort(racine) {
308
+ const hors = reglages(racine);
309
+ const fichierKnip = path.join(racine, 'knip.json');
310
+ const config =
311
+ hors ?? (existsSync(fichierKnip) ? JSON.parse(readFileSync(fichierKnip, 'utf8')) : null);
312
+ const DEPS = ['dependencies', 'devDependencies'];
313
+ const { fichiers, dossiers, erreur } = fichiersTests(racine);
314
+ if (erreur) {
315
+ process.stderr.write(`⛔ CODE MORT — git n'a pas listé les fichiers de test :\n${erreur}\n`);
316
+ return 1;
317
+ }
318
+ const knipLu = await knipDuDepot(racine);
319
+ if (!knipLu) {
320
+ process.stderr.write(
321
+ '⛔ CODE MORT — knip du dépôt illisible : son matcher (picomatch), son ordre des clés (byPathDepth) ou son schéma des espaces manque.\n'
322
+ );
323
+ return 1;
324
+ }
325
+ const premier = knip(hors ?? config);
326
+ const c1 = constats(premier.stdout ?? '');
327
+ const second = fichiers.length
328
+ ? knip(
329
+ reglagesDependances(
330
+ knipLu,
331
+ racine,
332
+ config,
333
+ fichiers,
334
+ paquets(knipLu, racine, dossiers, config)
335
+ ),
336
+ DEPS.join(',')
337
+ )
338
+ : null;
339
+ const c2 = second ? constats(second.stdout ?? '', DEPS) : null;
340
+ let c = null;
341
+ if (c1 && (c2 || !second)) {
342
+ const messages = liste => new Set(liste.map(x => x.message));
343
+ const deps = messages(constats(premier.stdout, DEPS));
344
+ const lus = c2 ? messages(c2) : deps;
345
+ c = c1.filter(x => !deps.has(x.message) || lus.has(x.message));
346
+ }
347
+ if (c === null) {
348
+ const r = c1 ? second : premier;
349
+ process.stderr.write(
350
+ `⛔ CODE MORT — knip n'a rien mesuré :\n${(r.stderr || r.stdout || '').slice(0, 1000)}\n`
351
+ );
352
+ return 1;
353
+ }
354
+ const { refus, lignes } = trancher(racine, 'code-mort', c);
355
+ if (refus.length) {
356
+ process.stderr.write(
357
+ `⛔ CODE MORT — ${refus.length} constat(s) : chacun se supprime, ou sort par son nom dans knip.json avec sa raison.\n`
358
+ );
359
+ for (const x of refus) {
360
+ process.stderr.write(` ${x.message}\n`);
361
+ }
362
+ }
363
+ for (const l of lignes) {
364
+ process.stderr.write(`${l}\n`);
365
+ }
366
+ return refus.length ? 1 : 0;
367
+ }
368
+
369
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
370
+ process.exitCode = await codeMort(process.cwd());
371
+ }