@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.
- package/.claude-plugin/plugin.json +9 -0
- package/METACADRE.md +41 -0
- package/agents/developpeur.md +10 -0
- package/agents/explorateur.md +9 -0
- package/agents/questionneur.md +11 -0
- package/agents/referent-technique.md +9 -0
- package/agents/regulateur.md +11 -0
- package/agents/relecteur.md +9 -0
- package/agents/testwriter.md +10 -0
- package/bin/grillhouse.mjs +125 -0
- package/cadre/templates/projet/.claude/settings.json +30 -0
- package/cadre/templates/projet/.editorconfig +47 -0
- package/cadre/templates/projet/.env.example +57 -0
- package/cadre/templates/projet/.eslintrc.cjs +56 -0
- package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
- package/cadre/templates/projet/.mcp.json +13 -0
- package/cadre/templates/projet/.prettierrc +11 -0
- package/cadre/templates/projet/CHANGELOG.md +61 -0
- package/cadre/templates/projet/CLAUDE.md +46 -0
- package/cadre/templates/projet/CONTRIBUTING.md +243 -0
- package/cadre/templates/projet/CREDITS.md +41 -0
- package/cadre/templates/projet/README.md +152 -0
- package/cadre/templates/projet/_gitignore +91 -0
- package/cadre/templates/projet/_npmignore +46 -0
- package/cadre/templates/projet/deployment/README.md +98 -0
- package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
- package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
- package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
- package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
- package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
- package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
- package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
- package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
- package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
- package/cadre/templates/projet/ecosystem.config.cjs +73 -0
- package/cadre/templates/projet/package.json +90 -0
- package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
- package/cadre/templates/projet/src/config.ts +68 -0
- package/cadre/templates/projet/src/errors.ts +102 -0
- package/cadre/templates/projet/src/index.ts +46 -0
- package/cadre/templates/projet/src/services/.gitkeep +8 -0
- package/cadre/templates/projet/src/types/index.ts +24 -0
- package/cadre/templates/projet/src/utils/logger.ts +106 -0
- package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
- package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
- package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
- package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
- package/cadre/templates/projet/tsconfig.eslint.json +9 -0
- package/cadre/templates/projet/tsconfig.json +41 -0
- package/cadre/templates/projet/tsdoc.json +6 -0
- package/cadre/templates/projet/vitest.config.ts +44 -0
- package/cadre/templates/turbo/paquets.json +29 -0
- package/cadre/templates/turbo/sans-paquets.json +28 -0
- package/charte.md +107 -0
- package/docs/ARCHITECTURE.md +90 -0
- package/hooks/crochet.mjs +33 -0
- package/hooks/hooks.json +58 -0
- package/hooks/register.tsx +558 -0
- package/package.json +75 -4
- package/scripts/avancement.mjs +244 -0
- package/scripts/bilan.mjs +523 -0
- package/scripts/branche-de-nuit.mjs +59 -0
- package/scripts/cadre.mjs +109 -0
- package/scripts/circuit.mjs +4375 -0
- package/scripts/code-mort.mjs +371 -0
- package/scripts/composants-affectes.mjs +598 -0
- package/scripts/consommateurs-arbitres.mjs +192 -0
- package/scripts/consommateurs.mjs +310 -0
- package/scripts/crochet.mjs +24 -0
- package/scripts/crochets.mjs +135 -0
- package/scripts/dernier-vert.mjs +208 -0
- package/scripts/dette.mjs +181 -0
- package/scripts/droits.mjs +354 -0
- package/scripts/enveloppe/codes.mjs +17 -0
- package/scripts/enveloppe/composant.mjs +11 -0
- package/scripts/enveloppe/enveloppe.mjs +628 -0
- package/scripts/enveloppe/lancer.sh +311 -0
- package/scripts/etapes.mjs +323 -0
- package/scripts/etat-du-circuit.mjs +104 -0
- package/scripts/frontieres.mjs +456 -0
- package/scripts/gardes-du-cadre.mjs +72 -0
- package/scripts/greffons.mjs +338 -0
- package/scripts/grillhouse-maj.mjs +514 -0
- package/scripts/index-interfaces.mjs +293 -0
- package/scripts/init.mjs +117 -0
- package/scripts/integration/file-attente.mjs +61 -0
- package/scripts/integration/file.mjs +494 -0
- package/scripts/integration/integrer.mjs +381 -0
- package/scripts/integration/rejeu.mjs +139 -0
- package/scripts/integration/verdict.mjs +13 -0
- package/scripts/interfaces-contre-code.mjs +547 -0
- package/scripts/ligne-etat/defaut.sh +110 -0
- package/scripts/ligne-etat/ligne-etat.mjs +93 -0
- package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
- package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
- package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
- package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
- package/scripts/migrations.mjs +43 -0
- package/scripts/nuit.sh +269 -0
- package/scripts/paquets.mjs +79 -0
- package/scripts/poussee-cles.mjs +46 -0
- package/scripts/preparation.mjs +84 -0
- package/scripts/references-par-les-types.mjs +420 -0
- package/scripts/registre.mjs +336 -0
- package/scripts/resultats.mjs +580 -0
- package/scripts/resumes.mjs +57 -0
- package/scripts/retirer-copie.sh +127 -0
- package/scripts/session-start.sh +203 -0
- package/scripts/sortie.mjs +155 -0
- package/scripts/sous-modules.sh +316 -0
- package/scripts/structure-guard.sh +27 -0
- package/scripts/superviseur.mjs +102 -0
- package/scripts/tableau.mjs +1886 -0
- package/scripts/tests.mjs +236 -0
- package/scripts/ticket.sh +23 -0
- package/scripts/turbo-des-paquets.mjs +899 -0
- package/scripts/un-commit-par-ticket.mjs +212 -0
- package/scripts/verifier.mjs +469 -0
- package/scripts/verrou-de-fichier.mjs +130 -0
- package/scripts/verrou-de-machine.mjs +211 -0
- package/scripts/verrous/commande.mjs +180 -0
- package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
- package/scripts/verrous/verrou.mjs +165 -0
- package/scripts/vitest-config/avancement.mjs +39 -0
- package/scripts/vitest-config/avancement.reporter.mjs +56 -0
- package/scripts/vitest-config/base-de-passe.mjs +128 -0
- package/scripts/vitest-config/base.d.ts +6 -0
- package/scripts/vitest-config/base.mjs +87 -0
- package/scripts/vitest-config/changements.mjs +173 -0
- package/scripts/vitest-config/cibles.mjs +207 -0
- package/scripts/vitest-config/duree.reporter.mjs +98 -0
- package/scripts/vitest-config/machine.setup.mjs +14 -0
- package/scripts/vitest-config/mesure.d.ts +6 -0
- package/scripts/vitest-config/mesure.mjs +13 -0
- package/scripts/vitest-config/mesure.setup.mjs +12 -0
- package/scripts/vitest-config/pression.mjs +90 -0
- package/scripts/vitest-config/resultats.mjs +23 -0
- package/scripts/vitest-config/resultats.setup.mjs +19 -0
- package/scripts/vitest.nuit.config.mjs +26 -0
- package/skills/developpeur/SKILL.md +175 -0
- package/skills/explorateur/SKILL.md +84 -0
- package/skills/grill/SKILL.md +238 -0
- package/skills/grill/references/releve-structurel.md +38 -0
- package/skills/mesure/SKILL.md +168 -0
- package/skills/pitmaster/SKILL.md +382 -0
- package/skills/pitmaster/references/cadre.md +119 -0
- package/skills/pitmaster/references/consigne-agent.md +308 -0
- package/skills/pitmaster/references/erreurs.md +25 -0
- package/skills/pitmaster/references/tour.md +124 -0
- package/skills/questionneur/SKILL.md +64 -0
- package/skills/redacteur/SKILL.md +84 -0
- package/skills/redacteur/references/documents-d-un-composant.md +105 -0
- package/skills/referent-technique/SKILL.md +145 -0
- package/skills/regulateur/SKILL.md +220 -0
- package/skills/release/SKILL.md +134 -0
- package/skills/release/scripts/update-version.cjs +92 -0
- package/skills/relecteur/SKILL.md +152 -0
- package/skills/relecteur/references/LICENSE-MIT +30 -0
- package/skills/relecteur/references/SOURCE.md +8 -0
- package/skills/relecteur/references/claims-check.md +14 -0
- package/skills/relecteur/references/deletion-check.md +14 -0
- package/skills/relecteur/references/edge-case-hunter.md +80 -0
- package/skills/relecteur/references/verification-gap.md +113 -0
- package/skills/testwriter/SKILL.md +187 -0
- package/types/index.d.ts +56 -0
- package/README.md +0 -3
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The components a realisation ticket touches, surveyed from its text, as the « affected
|
|
3
|
+
// components » of a design document (an RFC of Rust, a design doc of Chromium) and the dependents
|
|
4
|
+
// of a package in Turborepo (`--filter=...<paquet>`). The explorateur runs it on the draft of a
|
|
5
|
+
// ticket, whose title then names these components and whose section « Architecture » says the
|
|
6
|
+
// survey; the circuit (scripts/circuit.mjs) runs it again before the testwriter of the first
|
|
7
|
+
// component, and stops on a component the title does not name.
|
|
8
|
+
//
|
|
9
|
+
// npx grillhouse composants-affectes <ticket> the ticket `bd show` gives
|
|
10
|
+
// npx grillhouse composants-affectes --description <fichier> a draft of its description
|
|
11
|
+
// npx grillhouse composants-affectes <ticket> --admettre <composant> --raison <texte>
|
|
12
|
+
//
|
|
13
|
+
// prints the survey (`texteDuReleve`), from the repository root; for a ticket, then the components
|
|
14
|
+
// its title misses, exit 1 when there is one. `--admettre` admits out of the title a component the
|
|
15
|
+
// survey finds and the ticket does not change (the explorateur at the creation of the ticket, or
|
|
16
|
+
// the supervisor): kept in the ticket's record, its reason written at the ticket
|
|
17
|
+
// (`Composants · <composant> admis hors du titre : <raison>`); a component the repository does not
|
|
18
|
+
// hold, or no reason, is refused, exit 2.
|
|
19
|
+
//
|
|
20
|
+
// - The text read is the ticket's section « Architecture » and its section of criteria (a `##`
|
|
21
|
+
// heading that begins with « Critères »), each up to the next `##` heading; the rest of the
|
|
22
|
+
// description (its provenance, its notes) names nothing.
|
|
23
|
+
// - A component is named by its package name, as a whole word, or by a path in it
|
|
24
|
+
// (`packages/<x>`, `src/<x>`, its files included), the components being those of `composants`
|
|
25
|
+
// (scripts/consommateurs.mjs).
|
|
26
|
+
// - An element is a name between backquotes that begins with an identifier: a call (`f()`) or a
|
|
27
|
+
// member (`A.b`) is read by its head. Its provider is the component whose interface
|
|
28
|
+
// (`docs/INTERFACE.md`) publishes it: a heading of its generated part
|
|
29
|
+
// (scripts/index-interfaces.mjs). A name no interface publishes, that names a symbol of the
|
|
30
|
+
// repository (a declaration at the top of a component's source) or a path of it, is set apart
|
|
31
|
+
// (`sansFournisseur`); a word that names neither (an example of the ticket, a fault to create)
|
|
32
|
+
// is left out.
|
|
33
|
+
// - A reader is a component that reads, never one that carries a type, as in a graph of declared
|
|
34
|
+
// dependencies (the `deps` and `data` of Bazel, the `implicitDependencies` of Nx).
|
|
35
|
+
// - The readers of an element are the other components that: its provider's `## Consommateurs`
|
|
36
|
+
// declares for it, or for `*` (`consommateurs`); import it, or the whole provider, in their
|
|
37
|
+
// production code (`import`, the uses of scripts/consommateurs.mjs); hold a test file that names
|
|
38
|
+
// it, as a whole word, and imports the provider (the test file's path).
|
|
39
|
+
// - By the types (scripts/references-par-les-types.mjs): each element, a member of a type
|
|
40
|
+
// included, resolved by TypeScript's language service over the components' `tsconfig.json`;
|
|
41
|
+
// the components whose entry exports it, and the readers its references reach (`types`). An
|
|
42
|
+
// element that only names a symbol internal to a package leads to no component, its package
|
|
43
|
+
// included, as « find all references » starts from a published symbol: the ticket names the
|
|
44
|
+
// package it changes. A survey by the types that cannot run says why, and the survey stands on
|
|
45
|
+
// the names; a name the types resolve is no longer set apart.
|
|
46
|
+
// - By the paths read (`lit`): each component whose tests declare they read a path
|
|
47
|
+
// (`grillhouse.lit` of its package.json, `cheminsLus` of scripts/enveloppe/enveloppe.mjs) that a
|
|
48
|
+
// path the ticket changes holds or lies in: a path of the repository the sections write (its
|
|
49
|
+
// `:<line>` left out), or the folder of a component they name.
|
|
50
|
+
// - By the architecture: the items of the « Rend » of the `docs/CADRE.md` of each component that
|
|
51
|
+
// publishes an element (by its interface or by the types), each component they name
|
|
52
|
+
// (`architecture`), the readers of what it returns; its « Reçoit » names its producers, which
|
|
53
|
+
// read nothing of it.
|
|
54
|
+
// - The components of the survey go in the title's order, the provider first: each one after
|
|
55
|
+
// every component it reads among them (its imports, the providers of the elements it reads), in
|
|
56
|
+
// the order of their names otherwise.
|
|
57
|
+
// - The record of a ticket's survey, beside its copy (`.claude/worktrees/<ticket>.composants.json`
|
|
58
|
+
// under the main tree), holds the missing components of the circuit's last check and the
|
|
59
|
+
// components admitted out of the title, by `--admettre` or by the supervisor's answer to a stop
|
|
60
|
+
// (`repondre`): an admitted component is no longer missing.
|
|
61
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
62
|
+
import path from 'node:path';
|
|
63
|
+
import { pathToFileURL } from 'node:url';
|
|
64
|
+
import { spawnSync } from 'node:child_process';
|
|
65
|
+
import {
|
|
66
|
+
composants as lesComposants,
|
|
67
|
+
declares,
|
|
68
|
+
imports,
|
|
69
|
+
lireSiPresent,
|
|
70
|
+
NOM_DE_TEST,
|
|
71
|
+
parcourir,
|
|
72
|
+
utilise,
|
|
73
|
+
} from './consommateurs.mjs';
|
|
74
|
+
import { cheminsLus } from './enveloppe/enveloppe.mjs';
|
|
75
|
+
import { lire } from './index-interfaces.mjs';
|
|
76
|
+
import { elementsEcrits, parLesTypes } from './references-par-les-types.mjs';
|
|
77
|
+
|
|
78
|
+
const DOSSIERS_DE_TEST = new Set(['test', 'tests', '__tests__']);
|
|
79
|
+
const SAUTES = new Set(['node_modules', 'dist', 'build', 'coverage']);
|
|
80
|
+
const SOURCE = /\.[cm]?[jt]sx?$/;
|
|
81
|
+
|
|
82
|
+
// The sections the survey reads in <description>: « Architecture » and the criteria, as one text.
|
|
83
|
+
function lesSections(description) {
|
|
84
|
+
const out = [];
|
|
85
|
+
let dedans = false;
|
|
86
|
+
for (const l of String(description ?? '').split('\n')) {
|
|
87
|
+
const m = /^## (.+)$/.exec(l);
|
|
88
|
+
if (m) dedans = /^(Architecture|Critères)\b/.test(m[1].trim());
|
|
89
|
+
else if (dedans) out.push(l);
|
|
90
|
+
}
|
|
91
|
+
return out.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const echapper = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
95
|
+
// A package name as a whole word of a text; an identifier as a whole word of a source.
|
|
96
|
+
const motEntier = nom => new RegExp(`(?<![\\w@/.-])${echapper(nom)}(?![\\w-])`);
|
|
97
|
+
const identifiant = nom => new RegExp(`(?<![\\w$])${echapper(nom)}(?![\\w$])`);
|
|
98
|
+
|
|
99
|
+
// The title's name of the component <c>: its package name, else its path.
|
|
100
|
+
const nomAuTitre = (racine, c) => c.paquet ?? path.relative(racine, c.dir);
|
|
101
|
+
|
|
102
|
+
// The components <texte> names, by their package name or a path in them.
|
|
103
|
+
function nommes(racine, tous, texte) {
|
|
104
|
+
return tous.filter(c => {
|
|
105
|
+
const rel = path.relative(racine, c.dir).split(path.sep).join('/');
|
|
106
|
+
return (
|
|
107
|
+
(c.paquet && motEntier(c.paquet).test(texte)) ||
|
|
108
|
+
new RegExp(`(?<![\\w/.-])${echapper(rel)}(?![\\w-])`).test(texte)
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The elements <texte> names between backquotes, each by its head, in their order, once each.
|
|
114
|
+
function elementsNommes(texte) {
|
|
115
|
+
const vus = new Set();
|
|
116
|
+
for (const [, brut] of texte.matchAll(/`([^`\n]+)`/g)) {
|
|
117
|
+
const m = /^([A-Za-z_$][\w$]*)((?:\.[A-Za-z_$][\w$]*|\([^)]*\))*)$/.exec(brut.trim());
|
|
118
|
+
if (m) vus.add(m[1]);
|
|
119
|
+
}
|
|
120
|
+
return [...vus];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The paths of the repository at <racine> that <texte> writes, each once, from the root, its
|
|
124
|
+
// `:<line>` left out: a word with a `/` or an extension that names a file or a folder there.
|
|
125
|
+
function cheminsEcrits(racine, texte) {
|
|
126
|
+
const out = new Set();
|
|
127
|
+
for (const [brut] of texte.matchAll(/[\w.@-]+(?:\/[\w.@-]+)*/g)) {
|
|
128
|
+
const p = brut.replace(/\.+$/, '');
|
|
129
|
+
if (!p.includes('/') && !/\.\w+$/.test(p)) continue;
|
|
130
|
+
const net = path.normalize(p);
|
|
131
|
+
if (net.startsWith('..') || path.isAbsolute(net)) continue;
|
|
132
|
+
if (existsSync(path.join(racine, net))) out.add(net.split(path.sep).join('/'));
|
|
133
|
+
}
|
|
134
|
+
return [...out];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Whether the path <a> holds the path <b> or lies in it, both from the root.
|
|
138
|
+
const recouvre = (a, b) => a === b || b.startsWith(`${a}/`) || a.startsWith(`${b}/`);
|
|
139
|
+
|
|
140
|
+
// The components of <tous> whose tests declare they read a path that one of <changes> holds or
|
|
141
|
+
// lies in, out of their own folder: Map component → Set of « lit <path read> (<paths changed>) ».
|
|
142
|
+
function lecteursDesChemins(racine, tous, changes) {
|
|
143
|
+
const out = new Map();
|
|
144
|
+
for (const c of tous) {
|
|
145
|
+
const propre = path.relative(racine, c.dir).split(path.sep).join('/');
|
|
146
|
+
const par = new Set();
|
|
147
|
+
for (const brut of new Set(cheminsLus(c.dir))) {
|
|
148
|
+
const lu = brut.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
149
|
+
const atteints = changes.filter(ch => !recouvre(propre, ch) && recouvre(lu, ch)).sort();
|
|
150
|
+
if (atteints.length) par.add(`lit ${lu} (${atteints.join(', ')})`);
|
|
151
|
+
}
|
|
152
|
+
if (par.size) out.set(c, par);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A declaration of <nom> at the top of a source, exported or not.
|
|
158
|
+
const DECLARATION_EN_TETE = nom =>
|
|
159
|
+
new RegExp(
|
|
160
|
+
`^(?:export\\s+)?(?:default\\s+)?(?:declare\\s+)?(?:abstract\\s+)?(?:async\\s+)?(?:function\\*?|class|interface|type|enum|const|let|var)\\s+${echapper(nom)}(?![\\w$])`,
|
|
161
|
+
'm'
|
|
162
|
+
);
|
|
163
|
+
// The texts of the production sources of the components <tous>, their tests left out.
|
|
164
|
+
function sourcesDe(tous) {
|
|
165
|
+
return tous.flatMap(c =>
|
|
166
|
+
parcourir(c.dir, p => SAUTES.has(path.basename(p)))
|
|
167
|
+
.filter(f => {
|
|
168
|
+
if (!SOURCE.test(f) || NOM_DE_TEST.test(path.basename(f))) return false;
|
|
169
|
+
return !path
|
|
170
|
+
.relative(c.dir, f)
|
|
171
|
+
.split(path.sep)
|
|
172
|
+
.some(d => DOSSIERS_DE_TEST.has(d));
|
|
173
|
+
})
|
|
174
|
+
.map(f => lireSiPresent(f) ?? '')
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Each component's published elements and declared consumers, read in its interface.
|
|
179
|
+
function interfacesDe(racine, tous) {
|
|
180
|
+
const out = new Map();
|
|
181
|
+
for (const c of tous) {
|
|
182
|
+
const f = path.join(c.dir, 'docs', 'INTERFACE.md');
|
|
183
|
+
if (!existsSync(f)) continue;
|
|
184
|
+
const i = lire(path.relative(racine, f).split(path.sep).join('/'), readFileSync(f, 'utf8'));
|
|
185
|
+
const publies = new Set(
|
|
186
|
+
i.parties.filter(p => !p.consommateurs).map(p => p.titre.replace(/`/g, '').trim())
|
|
187
|
+
);
|
|
188
|
+
const consommateurs = i.parties.find(p => p.consommateurs)?.consommateurs ?? declares('');
|
|
189
|
+
out.set(c, { publies, consommateurs });
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The test sources of the component <c>: test files, and the sources of its test folders, built
|
|
195
|
+
// files left out.
|
|
196
|
+
function testsDe(c) {
|
|
197
|
+
return parcourir(c.dir, p => SAUTES.has(path.basename(p))).filter(f => {
|
|
198
|
+
if (!SOURCE.test(f)) return false;
|
|
199
|
+
const rel = path.relative(c.dir, f).split(path.sep);
|
|
200
|
+
return NOM_DE_TEST.test(path.basename(f)) || rel.some(d => DOSSIERS_DE_TEST.has(d));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Whether the source <texte> of the file <fichier> imports the component <p>.
|
|
205
|
+
function importe(texte, fichier, p) {
|
|
206
|
+
return imports(texte).some(({ spec }) => {
|
|
207
|
+
if (spec.startsWith('.')) {
|
|
208
|
+
const cible = path.resolve(path.dirname(fichier), spec);
|
|
209
|
+
return cible === p.dir || cible.startsWith(p.dir + path.sep);
|
|
210
|
+
}
|
|
211
|
+
return Boolean(p.paquet) && (spec === p.paquet || spec.startsWith(`${p.paquet}/`));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The components <liste> in the title's order: each one after those it reads among them
|
|
216
|
+
// (<lit>: Map component → Set of its providers), in the order of <nom> otherwise.
|
|
217
|
+
function ordonner(liste, lit, nom) {
|
|
218
|
+
const restants = [...liste].sort((a, b) => nom(a).localeCompare(nom(b)));
|
|
219
|
+
const out = [];
|
|
220
|
+
while (restants.length) {
|
|
221
|
+
const i = restants.findIndex(
|
|
222
|
+
c => ![...(lit.get(c) ?? [])].some(f => f !== c && restants.includes(f))
|
|
223
|
+
);
|
|
224
|
+
out.push(...restants.splice(i < 0 ? 0 : i, 1));
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The survey of the description <description> in the repository at <racine>:
|
|
230
|
+
// `{ nommes, elements: [{ nom, fournisseur, lecteurs: [{ composant, par }] }], sansFournisseur,
|
|
231
|
+
// types: { etat, raison, elements: [{ nom, declarations: [{ composant, publie }], lecteurs }] },
|
|
232
|
+
// lit: [{ composant, par }], architecture: [{ composant, par }], composants }`, each component by
|
|
233
|
+
// its title's name, `composants` in the title's order.
|
|
234
|
+
export function releve(racine, description) {
|
|
235
|
+
const texte = lesSections(description);
|
|
236
|
+
const tous = lesComposants(racine);
|
|
237
|
+
const reel = utilise(racine, tous);
|
|
238
|
+
const ifaces = interfacesDe(racine, tous);
|
|
239
|
+
const tests = new Map();
|
|
240
|
+
const testsDuComposant = c => {
|
|
241
|
+
if (!tests.has(c))
|
|
242
|
+
tests.set(
|
|
243
|
+
c,
|
|
244
|
+
testsDe(c).map(f => ({ f, texte: lireSiPresent(f) ?? '' }))
|
|
245
|
+
);
|
|
246
|
+
return tests.get(c);
|
|
247
|
+
};
|
|
248
|
+
const trouves = new Set(nommes(racine, tous, texte));
|
|
249
|
+
const elements = [];
|
|
250
|
+
const sansFournisseur = [];
|
|
251
|
+
for (const nom of elementsNommes(texte)) {
|
|
252
|
+
const fournisseurs = tous.filter(c => ifaces.get(c)?.publies.has(nom));
|
|
253
|
+
if (fournisseurs.length === 0) {
|
|
254
|
+
if (!tous.some(c => c.paquet === nom)) sansFournisseur.push(nom);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
for (const p of fournisseurs) {
|
|
258
|
+
trouves.add(p);
|
|
259
|
+
const lecteurs = [];
|
|
260
|
+
for (const c of tous) {
|
|
261
|
+
if (c === p) continue;
|
|
262
|
+
const par = [];
|
|
263
|
+
const declare =
|
|
264
|
+
ifaces.get(p).consommateurs.get(c.nom) ?? ifaces.get(p).consommateurs.get(c.paquet);
|
|
265
|
+
if (declare && (declare.has(nom) || declare.has('*'))) par.push('consommateurs');
|
|
266
|
+
const noms = reel.get(p.nom)?.get(c.nom);
|
|
267
|
+
if (noms && (noms.has(nom) || noms.has('*'))) par.push('import');
|
|
268
|
+
for (const t of testsDuComposant(c)) {
|
|
269
|
+
if (identifiant(nom).test(t.texte) && importe(t.texte, t.f, p)) {
|
|
270
|
+
par.push(path.relative(racine, t.f).split(path.sep).join('/'));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (par.length) {
|
|
274
|
+
lecteurs.push({ c, par });
|
|
275
|
+
trouves.add(c);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
elements.push({ nom, p, lecteurs });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// By the types: each element resolved by the language service, the components that publish it
|
|
282
|
+
// and their readers.
|
|
283
|
+
const types = parLesTypes(racine, tous, elementsEcrits(texte), {
|
|
284
|
+
fichiers: new Set(
|
|
285
|
+
[...texte.matchAll(/[\w.-]+\.[cm]?[jt]sx?\b/g)].map(m => path.basename(m[0]))
|
|
286
|
+
),
|
|
287
|
+
composants: new Set(nommes(racine, tous, texte)),
|
|
288
|
+
});
|
|
289
|
+
const resolus = new Set(types.elements.map(e => e.ecrit));
|
|
290
|
+
for (const e of types.elements) {
|
|
291
|
+
for (const d of e.declarations) if (d.publie) trouves.add(d.c);
|
|
292
|
+
for (const c of e.lecteurs.keys()) trouves.add(c);
|
|
293
|
+
}
|
|
294
|
+
// By the paths read: the components whose tests declare they read a path the ticket changes.
|
|
295
|
+
const parChemins = lecteursDesChemins(racine, tous, [
|
|
296
|
+
...cheminsEcrits(racine, texte),
|
|
297
|
+
...nommes(racine, tous, texte).map(c => path.relative(racine, c.dir).split(path.sep).join('/')),
|
|
298
|
+
]);
|
|
299
|
+
for (const c of parChemins.keys()) trouves.add(c);
|
|
300
|
+
// By the architecture: the Rend of the CADRE.md of each component that publishes an element.
|
|
301
|
+
const declarants = new Set([
|
|
302
|
+
...elements.map(e => e.p),
|
|
303
|
+
...types.elements.flatMap(e => e.declarations.filter(d => d.publie).map(d => d.c)),
|
|
304
|
+
]);
|
|
305
|
+
const architecture = new Map();
|
|
306
|
+
for (const p of declarants) {
|
|
307
|
+
for (const { regle, texte: ligne } of rendus(p)) {
|
|
308
|
+
for (const c of nommes(racine, tous, ligne)) {
|
|
309
|
+
if (c === p) continue;
|
|
310
|
+
if (!architecture.has(c)) architecture.set(c, new Set());
|
|
311
|
+
architecture.get(c).add(`CADRE de ${nomAuTitre(racine, p)}, Rend ${regle}`);
|
|
312
|
+
trouves.add(c);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// What each component reads: its imports, and the providers of the elements it reads.
|
|
317
|
+
const lit = new Map(tous.map(c => [c, new Set()]));
|
|
318
|
+
for (const p of tous) {
|
|
319
|
+
for (const nomLecteur of reel.get(p.nom)?.keys() ?? []) {
|
|
320
|
+
for (const c of tous) if (c.nom === nomLecteur) lit.get(c).add(p);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const e of elements) for (const l of e.lecteurs) lit.get(l.c).add(e.p);
|
|
324
|
+
const titre = c => nomAuTitre(racine, c);
|
|
325
|
+
const parNom = (a, b) => titre(a).localeCompare(titre(b));
|
|
326
|
+
let sources = null;
|
|
327
|
+
const lecteursDe = m =>
|
|
328
|
+
[...m]
|
|
329
|
+
.sort(([a], [b]) => parNom(a, b))
|
|
330
|
+
.map(([c, par]) => ({ composant: titre(c), par: [...par] }));
|
|
331
|
+
return {
|
|
332
|
+
nommes: [...nommes(racine, tous, texte)].sort(parNom).map(titre),
|
|
333
|
+
elements: elements.map(e => ({
|
|
334
|
+
nom: e.nom,
|
|
335
|
+
fournisseur: titre(e.p),
|
|
336
|
+
lecteurs: e.lecteurs
|
|
337
|
+
.sort((a, b) => parNom(a.c, b.c))
|
|
338
|
+
.map(l => ({ composant: titre(l.c), par: l.par })),
|
|
339
|
+
})),
|
|
340
|
+
sansFournisseur: sansFournisseur.filter(
|
|
341
|
+
n =>
|
|
342
|
+
!resolus.has(n) &&
|
|
343
|
+
(existsSync(path.join(racine, n)) ||
|
|
344
|
+
(sources ??= sourcesDe(tous)).some(s => DECLARATION_EN_TETE(n).test(s)))
|
|
345
|
+
),
|
|
346
|
+
types: {
|
|
347
|
+
etat: types.etat,
|
|
348
|
+
raison: types.raison,
|
|
349
|
+
elements: types.elements.map(e => ({
|
|
350
|
+
nom: e.nom,
|
|
351
|
+
declarations: e.declarations.map(d => ({
|
|
352
|
+
composant: titre(d.c),
|
|
353
|
+
publie: d.publie,
|
|
354
|
+
})),
|
|
355
|
+
lecteurs: lecteursDe(e.lecteurs),
|
|
356
|
+
})),
|
|
357
|
+
},
|
|
358
|
+
lit: lecteursDe(parChemins),
|
|
359
|
+
architecture: lecteursDe(architecture),
|
|
360
|
+
composants: ordonner([...trouves], lit, titre).map(titre),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// What the CADRE.md of the component <c> says it returns: each item of its rubric « Rend » (a
|
|
365
|
+
// `##` heading that ends with that word), its rule (`**R4.**`, else its line), its continuation
|
|
366
|
+
// lines included.
|
|
367
|
+
function rendus(c) {
|
|
368
|
+
const texte = lireSiPresent(path.join(c.dir, 'docs', 'CADRE.md'));
|
|
369
|
+
if (!texte) return [];
|
|
370
|
+
const out = [];
|
|
371
|
+
let dedans = false;
|
|
372
|
+
for (const [i, l] of texte.split('\n').entries()) {
|
|
373
|
+
const h = /^## (.+)$/.exec(l);
|
|
374
|
+
if (h) {
|
|
375
|
+
dedans = /Rend\s*$/.test(h[1].trim());
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (!dedans) continue;
|
|
379
|
+
if (/^[-*] /.test(l)) {
|
|
380
|
+
const regle = /^[-*] \*\*(R\d+)\.?\*\*/.exec(l)?.[1] ?? `l.${i + 1}`;
|
|
381
|
+
out.push({ regle, texte: l });
|
|
382
|
+
} else if (/^\s+\S/.test(l) && out.length) out.at(-1).texte += ` ${l.trim()}`;
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// The record of a ticket's survey beside its copy (<fichier>): `{ manquants, admis, reponses }`,
|
|
388
|
+
// the components the last survey found missing, those the supervisor answered for, and his
|
|
389
|
+
// answers; empty without it.
|
|
390
|
+
function lireGarde(fichier) {
|
|
391
|
+
try {
|
|
392
|
+
const g = JSON.parse(readFileSync(fichier, 'utf8'));
|
|
393
|
+
return { manquants: g.manquants ?? [], admis: g.admis ?? [], reponses: g.reponses ?? [] };
|
|
394
|
+
} catch {
|
|
395
|
+
return { manquants: [], admis: [], reponses: [] };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function ecrireGarde(fichier, g) {
|
|
399
|
+
mkdirSync(path.dirname(fichier), { recursive: true });
|
|
400
|
+
writeFileSync(fichier, `${JSON.stringify(g, null, 2)}\n`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// The components the supervisor answered for, kept in <fichier>.
|
|
404
|
+
const admis = fichier => lireGarde(fichier).admis;
|
|
405
|
+
|
|
406
|
+
// Keeps in <fichier> the components <liste> the supervisor answered for, with his answer.
|
|
407
|
+
export function admettre(fichier, liste, reponse) {
|
|
408
|
+
const g = lireGarde(fichier);
|
|
409
|
+
ecrireGarde(fichier, {
|
|
410
|
+
...g,
|
|
411
|
+
manquants: g.manquants.filter(c => !liste.includes(c)),
|
|
412
|
+
admis: [...new Set([...g.admis, ...liste])],
|
|
413
|
+
reponses: [...g.reponses, { date: new Date().toISOString(), composants: liste, reponse }],
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// The components of the survey <r> the title <titre> does not name (its components as written,
|
|
418
|
+
// package names or paths, or `.`), nor the answers kept in <fichier>, in the survey's order.
|
|
419
|
+
export function manquants(racine, r, titre, fichier = null) {
|
|
420
|
+
if (titre.includes('.')) return [];
|
|
421
|
+
const tous = lesComposants(racine);
|
|
422
|
+
const garde = new Set(fichier ? admis(fichier) : []);
|
|
423
|
+
const couvre = c => {
|
|
424
|
+
const rel = path.relative(racine, c.dir).split(path.sep).join('/');
|
|
425
|
+
return titre.some(t => {
|
|
426
|
+
const n = path.normalize(t).replace(/\/$/, '');
|
|
427
|
+
return t === c.paquet || rel === n || rel.startsWith(`${n}/`);
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
return r.composants.filter(nom => {
|
|
431
|
+
if (garde.has(nom)) return false;
|
|
432
|
+
const c = tous.find(x => nomAuTitre(racine, x) === nom);
|
|
433
|
+
return !c || !couvre(c);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// The check of the circuit before the testwriter of the first component: the survey of
|
|
438
|
+
// <description> in <racine> against the title's components <titre>, the missing ones kept in
|
|
439
|
+
// <fichier>. `{ manquants, rapport, releve }`, the report opening on the missing components (null
|
|
440
|
+
// without one), the survey as the section « Architecture » says it, which the questionneur reads.
|
|
441
|
+
export function verifierLeTitre({ racine, ticket, description, titre, fichier }) {
|
|
442
|
+
const r = releve(racine, description);
|
|
443
|
+
const liste = manquants(racine, r, titre, fichier);
|
|
444
|
+
ecrireGarde(fichier, { ...lireGarde(fichier), manquants: liste });
|
|
445
|
+
if (!liste.length) return { manquants: [], rapport: null, releve: texteDuReleve(r) };
|
|
446
|
+
const rapport = [
|
|
447
|
+
`Composants manquants au titre : ${liste.join(', ')}.`,
|
|
448
|
+
'',
|
|
449
|
+
texteDuReleve(r),
|
|
450
|
+
'',
|
|
451
|
+
`Le superviseur ajoute chacun au titre, le fournisseur d'abord (\`bd update ${ticket} --title "…"\`), puis relance le circuit ; ou il répond pourquoi il reste dehors : \`npx grillhouse circuit ${ticket} --reponse "<texte>"\`.`,
|
|
452
|
+
].join('\n');
|
|
453
|
+
return { manquants: liste, rapport, releve: texteDuReleve(r) };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// The supervisor's answer <reponse> to the missing components kept in <fichier>: they pass from
|
|
457
|
+
// then on. The components answered for, in their order.
|
|
458
|
+
export function repondre(fichier, reponse) {
|
|
459
|
+
const { manquants: liste } = lireGarde(fichier);
|
|
460
|
+
admettre(fichier, liste, reponse);
|
|
461
|
+
return liste;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// The survey <r> as the section « Architecture » says it, in Markdown.
|
|
465
|
+
export function texteDuReleve(r) {
|
|
466
|
+
const out = [
|
|
467
|
+
`**Composants relevés** : ${r.composants.length ? r.composants.join(', ') : 'aucun'}.`,
|
|
468
|
+
];
|
|
469
|
+
for (const e of r.elements) {
|
|
470
|
+
const lus = e.lecteurs.map(l => `${l.composant} (${l.par.join(', ')})`).join(', ');
|
|
471
|
+
out.push(`- \`${e.nom}\` : fourni par ${e.fournisseur} ; lu par ${lus || 'aucun autre'}.`);
|
|
472
|
+
}
|
|
473
|
+
if (r.types.etat !== 'fait') {
|
|
474
|
+
out.push(`- Par les types : impossible (${r.types.raison}) ; le relevé s'en tient aux noms.`);
|
|
475
|
+
} else if (!r.types.elements.length) {
|
|
476
|
+
out.push('- Par les types : aucun élément résolu.');
|
|
477
|
+
} else {
|
|
478
|
+
out.push('- Par les types (service de langage de TypeScript) :');
|
|
479
|
+
// The elements of one provenance and the same readers on one line; each reader by its first
|
|
480
|
+
// reason, the count of the others after it.
|
|
481
|
+
const lignes = new Map();
|
|
482
|
+
for (const e of r.types.elements) {
|
|
483
|
+
const ou = [
|
|
484
|
+
...new Set(
|
|
485
|
+
e.declarations.map(d =>
|
|
486
|
+
d.publie ? `publié par ${d.composant}` : `interne à ${d.composant}`
|
|
487
|
+
)
|
|
488
|
+
),
|
|
489
|
+
].join(' ; ');
|
|
490
|
+
const lus = e.lecteurs
|
|
491
|
+
.map(
|
|
492
|
+
l =>
|
|
493
|
+
`${l.composant} (${l.par[0]}${l.par.length > 1 ? ` ; ${l.par.length - 1} de plus` : ''})`
|
|
494
|
+
)
|
|
495
|
+
.join(', ');
|
|
496
|
+
const cle = `${ou} ; lu par ${lus || 'aucun autre'}.`;
|
|
497
|
+
lignes.set(cle, [...(lignes.get(cle) ?? []), `\`${e.nom}\``]);
|
|
498
|
+
}
|
|
499
|
+
for (const [cle, noms] of lignes) out.push(` - ${noms.join(', ')} : ${cle}`);
|
|
500
|
+
}
|
|
501
|
+
if (r.lit.length) {
|
|
502
|
+
const lus = r.lit.map(l => `${l.composant} (${l.par.join(' ; ')})`).join(', ');
|
|
503
|
+
out.push(`- Par les chemins lus (\`grillhouse.lit\`) : ${lus}.`);
|
|
504
|
+
}
|
|
505
|
+
if (r.architecture.length) {
|
|
506
|
+
const lus = r.architecture.map(l => `${l.composant} (${l.par.join(' ; ')})`).join(', ');
|
|
507
|
+
out.push(`- Par l'architecture : ${lus}.`);
|
|
508
|
+
}
|
|
509
|
+
if (r.nommes.length) out.push(`- Nommés au ticket : ${r.nommes.join(', ')}.`);
|
|
510
|
+
if (r.sansFournisseur.length) {
|
|
511
|
+
out.push(
|
|
512
|
+
`- Sans interface qui les publie : ${r.sansFournisseur.map(n => `\`${n}\``).join(', ')}.`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return out.join('\n');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// The admission of <nom> out of the title of <ticket>, its reason after `--raison` in <args>: its
|
|
519
|
+
// exit code.
|
|
520
|
+
function admettreAuTicket(racine, ticket, nom, args) {
|
|
521
|
+
const r = args.indexOf('--raison');
|
|
522
|
+
const raison = r >= 0 ? args[r + 1]?.trim() : '';
|
|
523
|
+
const c = lesComposants(racine).find(
|
|
524
|
+
x => x.paquet === nom || path.relative(racine, x.dir) === path.normalize(nom ?? '')
|
|
525
|
+
);
|
|
526
|
+
if (!c || !raison) {
|
|
527
|
+
process.stderr.write(
|
|
528
|
+
`⛔ ${!c ? `${nom ?? '(aucun)'} : aucun composant de ce nom` : '--raison manque'}.\n` +
|
|
529
|
+
'usage: npx grillhouse composants-affectes <ticket> --admettre <composant> --raison <texte>\n'
|
|
530
|
+
);
|
|
531
|
+
return 2;
|
|
532
|
+
}
|
|
533
|
+
const titre = nomAuTitre(racine, c);
|
|
534
|
+
admettre(path.join(racine, '.claude', 'worktrees', `${ticket}.composants.json`), [titre], raison);
|
|
535
|
+
const texte = `Composants · ${titre} admis hors du titre : ${raison}`;
|
|
536
|
+
const bd = spawnSync('bd', ['comments', 'add', ticket, texte], {
|
|
537
|
+
cwd: racine,
|
|
538
|
+
encoding: 'utf8',
|
|
539
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
540
|
+
});
|
|
541
|
+
if (bd.status !== 0) process.stderr.write(`⚠️ ligne non écrite au ticket : ${bd.stderr}\n`);
|
|
542
|
+
process.stdout.write(`${texte}\n`);
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// The command: its exit code.
|
|
547
|
+
function commande(racine, args) {
|
|
548
|
+
const i = args.indexOf('--description');
|
|
549
|
+
if (i >= 0) {
|
|
550
|
+
if (!args[i + 1]) {
|
|
551
|
+
process.stderr.write('usage: npx grillhouse composants-affectes --description <fichier>\n');
|
|
552
|
+
return 2;
|
|
553
|
+
}
|
|
554
|
+
const r = releve(racine, readFileSync(path.resolve(racine, args[i + 1]), 'utf8'));
|
|
555
|
+
process.stdout.write(`${texteDuReleve(r)}\n`);
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
const [ticket] = args;
|
|
559
|
+
const a = args.indexOf('--admettre');
|
|
560
|
+
if (ticket && a >= 0) return admettreAuTicket(racine, ticket, args[a + 1], args);
|
|
561
|
+
if (!ticket) {
|
|
562
|
+
process.stderr.write(
|
|
563
|
+
'usage: npx grillhouse composants-affectes <ticket> | --description <fichier>\n'
|
|
564
|
+
);
|
|
565
|
+
return 2;
|
|
566
|
+
}
|
|
567
|
+
const show = spawnSync('bd', ['show', ticket, '--json'], {
|
|
568
|
+
cwd: racine,
|
|
569
|
+
encoding: 'utf8',
|
|
570
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
571
|
+
});
|
|
572
|
+
if (show.status !== 0) {
|
|
573
|
+
process.stderr.write(`⛔ ticket ${ticket} illisible : ${show.stderr}\n`);
|
|
574
|
+
return 2;
|
|
575
|
+
}
|
|
576
|
+
const t = JSON.parse(show.stdout)[0] ?? {};
|
|
577
|
+
const r = releve(racine, t.description);
|
|
578
|
+
process.stdout.write(`${texteDuReleve(r)}\n`);
|
|
579
|
+
const parts = String(t.title ?? '').split(' — ');
|
|
580
|
+
const titre =
|
|
581
|
+
parts.length < 2
|
|
582
|
+
? []
|
|
583
|
+
: parts
|
|
584
|
+
.at(-1)
|
|
585
|
+
.split(',')
|
|
586
|
+
.map(s => s.trim())
|
|
587
|
+
.filter(Boolean);
|
|
588
|
+
const manque = manquants(racine, r, titre);
|
|
589
|
+
if (manque.length) {
|
|
590
|
+
process.stdout.write(`Composants manquants au titre : ${manque.join(', ')}.\n`);
|
|
591
|
+
return 1;
|
|
592
|
+
}
|
|
593
|
+
return 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
597
|
+
process.exitCode = commande(process.cwd(), process.argv.slice(2));
|
|
598
|
+
}
|