@roomi-fields/grillhouse 0.23.1 → 0.24.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 +1 -1
- package/METACADRE.md +1 -1
- package/agents/explorateur.md +4 -2
- package/agents/questionneur.md +1 -1
- package/bin/grillhouse.mjs +3 -58
- package/cadre/templates/projet/CLAUDE.md +5 -1
- package/cadre/templates/projet/docs/agents/issue-tracker.md +4 -4
- package/charte.md +23 -6
- package/docs/ARCHITECTURE.md +127 -1
- package/hooks/register.tsx +3 -13
- package/package.json +13 -1
- package/scripts/bilan.mjs +134 -88
- package/scripts/circuit.mjs +1547 -2248
- package/scripts/composants-affectes.mjs +27 -108
- package/scripts/consommateurs-arbitres.mjs +15 -21
- package/scripts/consommateurs.d.ts +8 -0
- package/scripts/consommateurs.mjs +5 -1
- package/scripts/dette.d.ts +23 -0
- package/scripts/dette.mjs +76 -13
- package/scripts/droits.mjs +38 -44
- package/scripts/elements-du-lot.mjs +246 -0
- package/scripts/enveloppe/enveloppe.mjs +105 -232
- package/scripts/enveloppe/lancer.sh +79 -49
- package/scripts/enveloppe/paquets-du-plan.mjs +49 -0
- package/scripts/etapes.mjs +148 -213
- package/scripts/gardes-du-cadre.mjs +8 -6
- package/scripts/grillhouse-maj.mjs +31 -9
- package/scripts/index-interfaces.mjs +21 -1
- package/scripts/integration/integrer.mjs +2 -2
- package/scripts/interfaces-contre-code.mjs +25 -17
- package/scripts/migrations/0.23.0-config-vitest.mjs +26 -23
- package/scripts/migrations/0.23.2-copies-du-cadre.mjs +201 -0
- package/scripts/migrations/0.24.0-circuit-simplifie.mjs +231 -0
- package/scripts/migrations.mjs +41 -6
- package/scripts/paquets.d.ts +5 -0
- package/scripts/paquets.mjs +3 -0
- package/scripts/references-par-les-types.mjs +55 -36
- package/scripts/registre.mjs +7 -15
- package/scripts/resultats.mjs +283 -142
- package/scripts/resumes.mjs +1 -1
- package/scripts/session-start.sh +1 -2
- package/scripts/sources.mjs +28 -0
- package/scripts/sous-commandes.mjs +62 -0
- package/scripts/tableau.mjs +85 -191
- package/scripts/tests.mjs +19 -17
- package/scripts/verifier.mjs +5 -4
- package/scripts/verrou-de-machine.mjs +131 -24
- package/scripts/verrous/verrou.mjs +66 -5
- package/skills/developpeur/SKILL.md +60 -50
- package/skills/explorateur/SKILL.md +90 -64
- package/skills/grill/SKILL.md +1 -1
- package/skills/mesure/SKILL.md +2 -1
- package/skills/pitmaster/SKILL.md +132 -118
- package/skills/pitmaster/references/consigne-agent.md +54 -148
- package/skills/questionneur/SKILL.md +18 -18
- package/skills/relecteur/SKILL.md +69 -23
- package/skills/testwriter/SKILL.md +46 -35
- package/types/index.d.ts +1 -3
- package/agents/referent-technique.md +0 -9
- package/agents/regulateur.md +0 -11
- package/scripts/enveloppe/composant.mjs +0 -11
- package/scripts/etat-du-circuit.mjs +0 -104
- package/skills/referent-technique/SKILL.md +0 -145
- package/skills/regulateur/SKILL.md +0 -220
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// --ticket is a usage error, named. In order, each step refusing stops the run:
|
|
15
15
|
// 1. the verdict: the ticket has `ACCEPTÉ` as its last reviewer verdict (`ACCEPTÉ` or
|
|
16
16
|
// `RENDU` at the start of a comment of the author `relecteur`; another author's is none) and, when labelled `arbitrage`, each arbitrage
|
|
17
|
-
// (`estArbitrage`: a « ## Arbitrage » comment,
|
|
18
|
-
// (`arbitrageTranche`:
|
|
17
|
+
// (`estArbitrage`: a « ## Arbitrage » comment, the supervisor's) is settled
|
|
18
|
+
// (`arbitrageTranche`: `### Verdict — tranché`, or a « ## Réponse du
|
|
19
19
|
// responsable » comment after it);
|
|
20
20
|
// 2. the lots: the tests lot touches only what the testwriter writes (`isTesterFile`: test files
|
|
21
21
|
// and the shared test material), the code lot none of it;
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
// `faute-mal-formee`, `faute-hors-catalogue`, `faute-non-nommee`, `faute-non-produite`,
|
|
46
46
|
// `faute-non-listee`), or by the declaration file and TSDoc's message id for a TSDoc message.
|
|
47
47
|
//
|
|
48
|
-
// In the envelope of
|
|
49
|
-
// check and `npm run api`
|
|
50
|
-
//
|
|
51
|
-
//
|
|
48
|
+
// In the envelope of an agent (`GRILLHOUSE_ENVELOPPE`, scripts/enveloppe/paquets-du-plan.mjs), the
|
|
49
|
+
// check and `npm run api` judge and write the components of its plan alone: the others are
|
|
50
|
+
// read-only there, and the integration checks every component on the whole repository. The
|
|
51
|
+
// component's build comes first, as outside (`npm test -- <its files>` builds it).
|
|
52
52
|
//
|
|
53
53
|
// A component without `docs/INTERFACE.md` is reported and skipped. A project that prefers a
|
|
54
54
|
// bench per component calls `verifierComposant(racine, composant)` from its test.
|
|
@@ -58,7 +58,7 @@ import { tmpdir } from 'node:os';
|
|
|
58
58
|
import path from 'node:path';
|
|
59
59
|
import { pathToFileURL } from 'node:url';
|
|
60
60
|
import { composants, lireSiPresent, parcourir } from './consommateurs.mjs';
|
|
61
|
-
import {
|
|
61
|
+
import { paquetsDeLEnveloppe } from './enveloppe/paquets-du-plan.mjs';
|
|
62
62
|
import { trancher } from './dette.mjs';
|
|
63
63
|
|
|
64
64
|
const require = createRequire(import.meta.url);
|
|
@@ -428,7 +428,10 @@ function avecPartie(m, texte, partie) {
|
|
|
428
428
|
// the debt (`dette.mjs`) judges, keyed by the component's interface (by the declaration file for a
|
|
429
429
|
// TSDoc message, its rule TSDoc's message id); a refusal that a command or a build repairs (a
|
|
430
430
|
// generated part, an API report, a built entry, the marks, the TSDoc configuration) has no key and
|
|
431
|
-
// is never inscribed.
|
|
431
|
+
// is never inscribed. The refusals `npm run api` lifts carry their rule without a file, never
|
|
432
|
+
// inscribed either: `rapport-api` (the API report absent or stale) and `partie-generee` (the
|
|
433
|
+
// generated part of the interface), by which the circuit regenerates them at the final pass
|
|
434
|
+
// (scripts/circuit.mjs, the references of the frame).
|
|
432
435
|
export function verifierComposant(racine, c, { ecrire = false } = {}) {
|
|
433
436
|
const refus = [];
|
|
434
437
|
const signaux = [];
|
|
@@ -442,6 +445,8 @@ export function verifierComposant(racine, c, { ecrire = false } = {}) {
|
|
|
442
445
|
? { fichier, ...(ligne ? { ligne } : {}), regle, message: `${rel} : ${texte}` }
|
|
443
446
|
: { message: `${rel} : ${texte}` }
|
|
444
447
|
);
|
|
448
|
+
// A refusal `npm run api` lifts: its rule, no file.
|
|
449
|
+
const aRegenerer = (texte, regle) => refus.push({ regle, message: `${rel} : ${texte}` });
|
|
445
450
|
if (!existsSync(iface)) {
|
|
446
451
|
signaux.push(`${rel} n'a pas d'interface (docs/INTERFACE.md) : rien n'est vérifié.`);
|
|
447
452
|
return { refus, signaux };
|
|
@@ -474,12 +479,14 @@ export function verifierComposant(racine, c, { ecrire = false } = {}) {
|
|
|
474
479
|
}
|
|
475
480
|
const manque = !ecrire && !existsSync(path.join(c.dir, 'docs', 'INTERFACE.api.md'));
|
|
476
481
|
if (manque) {
|
|
477
|
-
|
|
478
|
-
"son rapport docs/INTERFACE.api.md manque ; npm run api l'écrit, après la construction."
|
|
482
|
+
aRegenerer(
|
|
483
|
+
"son rapport docs/INTERFACE.api.md manque ; npm run api l'écrit, après la construction.",
|
|
484
|
+
'rapport-api'
|
|
479
485
|
);
|
|
480
486
|
} else if (!api.ajour && !ecrire) {
|
|
481
|
-
|
|
482
|
-
"l'API a changé sans son rapport docs/INTERFACE.api.md ; npm run api le régénère, après la construction."
|
|
487
|
+
aRegenerer(
|
|
488
|
+
"l'API a changé sans son rapport docs/INTERFACE.api.md ; npm run api le régénère, après la construction.",
|
|
489
|
+
'rapport-api'
|
|
483
490
|
);
|
|
484
491
|
}
|
|
485
492
|
const nommees = new Set();
|
|
@@ -497,8 +504,9 @@ export function verifierComposant(racine, c, { ecrire = false } = {}) {
|
|
|
497
504
|
if (ecrire) {
|
|
498
505
|
if (attendu !== texte) writeFileSync(iface, attendu);
|
|
499
506
|
} else if (attendu !== texte) {
|
|
500
|
-
|
|
501
|
-
`la part générée de docs/INTERFACE.md (entre ${DEBUT} et ${FIN}) n'est pas celle du code ; npm run api la refait
|
|
507
|
+
aRegenerer(
|
|
508
|
+
`la part générée de docs/INTERFACE.md (entre ${DEBUT} et ${FIN}) n'est pas celle du code ; npm run api la refait.`,
|
|
509
|
+
'partie-generee'
|
|
502
510
|
);
|
|
503
511
|
}
|
|
504
512
|
}
|
|
@@ -522,13 +530,13 @@ function codesContreCatalogue(c, racine, fautes, refuser) {
|
|
|
522
530
|
}
|
|
523
531
|
}
|
|
524
532
|
|
|
525
|
-
// The components a run reads: every one of <racine>; in
|
|
526
|
-
//
|
|
533
|
+
// The components a run reads: every one of <racine>; in an envelope, those its plan names, the
|
|
534
|
+
// others never written; every one when the plan names none of them (a folder of the root, `.`).
|
|
527
535
|
export function aVerifier(racine, env = process.env) {
|
|
528
536
|
const tous = composants(racine);
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
return
|
|
537
|
+
const plan = (paquetsDeLEnveloppe(env) ?? []).map(p => path.resolve(racine, p));
|
|
538
|
+
const siens = tous.filter(c => plan.includes(path.resolve(c.dir)));
|
|
539
|
+
return siens.length ? siens : tous;
|
|
532
540
|
}
|
|
533
541
|
|
|
534
542
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
@@ -2,22 +2,24 @@
|
|
|
2
2
|
// (and `/mesure`), in place of the workspace package `@grillhouse/vitest-config` that the frame's
|
|
3
3
|
// copied files held.
|
|
4
4
|
//
|
|
5
|
-
// - Each source file of the project (
|
|
6
|
-
// .git, dist, coverage, .turbo, .claude, .beads, .codegraph and .rtfm left out) that names
|
|
5
|
+
// - Each source file of the project (scripts/sources.mjs) that names
|
|
7
6
|
// `@grillhouse/vitest-config` or `@grillhouse/vitest-config/mesure` in quotes names the package's
|
|
8
7
|
// instead; a root configuration that imports the base by its former path
|
|
9
8
|
// (`./scripts/vitest-config/base.mjs`, `./packages/grillhouse/scripts/vitest-config/base.mjs`)
|
|
10
|
-
// imports it by its name.
|
|
9
|
+
// imports it by its name. `formesAnterieures` gives back what a text so rewritten held before,
|
|
10
|
+
// by which the migration of the copies (0.23.2-copies-du-cadre.mjs) still knows a former copy of
|
|
11
|
+
// the frame by its fingerprint.
|
|
11
12
|
// - Each package.json of the project (the root's and each workspace's) that declares
|
|
12
13
|
// `@grillhouse/vitest-config` declares `@roomi-fields/grillhouse` in the same field in its place,
|
|
13
14
|
// at the range the root declares (else `^<version>` of the package), unless it already declares
|
|
14
15
|
// it; the root's workspaces lose the former directory of the package (`scripts/vitest-config`,
|
|
15
16
|
// `packages/grillhouse/scripts/vitest-config`).
|
|
16
17
|
// - A project already in the new form is not written.
|
|
17
|
-
import { existsSync,
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
19
|
import path from 'node:path';
|
|
19
20
|
import { VERSION } from '../cadre.mjs';
|
|
20
21
|
import { paquets } from '../paquets.mjs';
|
|
22
|
+
import { sources } from '../sources.mjs';
|
|
21
23
|
|
|
22
24
|
export const version = '0.23.0';
|
|
23
25
|
export const nom = 'configuration de base de Vitest';
|
|
@@ -27,33 +29,34 @@ const PAQUET = '@roomi-fields/grillhouse';
|
|
|
27
29
|
const NOUVEAU = `${PAQUET}/vitest-config`;
|
|
28
30
|
const ANCIENS_ESPACES = ['scripts/vitest-config', 'packages/grillhouse/scripts/vitest-config'];
|
|
29
31
|
const CHAMPS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
|
|
30
|
-
const EXTENSIONS = /\.(?:[mc]?ts|[mc]?js)$/;
|
|
31
|
-
const HORS = new Set([
|
|
32
|
-
'node_modules',
|
|
33
|
-
'.git',
|
|
34
|
-
'dist',
|
|
35
|
-
'coverage',
|
|
36
|
-
'.turbo',
|
|
37
|
-
'.claude',
|
|
38
|
-
'.beads',
|
|
39
|
-
'.codegraph',
|
|
40
|
-
'.rtfm',
|
|
41
|
-
]);
|
|
42
32
|
// A quoted name of the former package, or a former path of the base at the root.
|
|
43
33
|
const NOMME =
|
|
44
34
|
/(['"])(?:@grillhouse\/vitest-config(\/mesure)?|\.\/(?:packages\/grillhouse\/)?scripts\/vitest-config\/base\.mjs)\1/g;
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
36
|
+
// A quoted name of the package's base, as this migration writes it.
|
|
37
|
+
const ECRIT = /(['"])@roomi-fields\/grillhouse\/vitest-config(\/mesure)?\1/g;
|
|
38
|
+
const ANCIENS_CHEMINS = [
|
|
39
|
+
'./scripts/vitest-config/base.mjs',
|
|
40
|
+
'./packages/grillhouse/scripts/vitest-config/base.mjs',
|
|
41
|
+
];
|
|
53
42
|
|
|
54
43
|
const lireJson = f => JSON.parse(readFileSync(f, 'utf8'));
|
|
55
44
|
const ecrireJson = (f, v) => writeFileSync(f, `${JSON.stringify(v, null, 2)}\n`);
|
|
56
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The texts <texte> may have held before this migration rewrote it: each name it wrote back to the
|
|
48
|
+
* former package's, or each name of the base back to one former path; none when it wrote nothing.
|
|
49
|
+
*/
|
|
50
|
+
export function formesAnterieures(texte) {
|
|
51
|
+
if (texte.search(ECRIT) < 0) return [];
|
|
52
|
+
return [
|
|
53
|
+
texte.replace(ECRIT, (_, q, mesure) => `${q}${ANCIEN}${mesure ?? ''}${q}`),
|
|
54
|
+
...ANCIENS_CHEMINS.map(c =>
|
|
55
|
+
texte.replace(ECRIT, (tout, q, mesure) => (mesure ? tout : `${q}${c}${q}`))
|
|
56
|
+
),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
57
60
|
/** Brings the project to the package's base configuration of Vitest; returns what it did. */
|
|
58
61
|
export function migrer(projet) {
|
|
59
62
|
const lignes = [];
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// The frame's copies leave a project in 0.22.x: the package carries the scripts, its plugin the
|
|
2
|
+
// skills, agents, charter and meta-frame, so the copies the former update placed
|
|
3
|
+
// (`.claude/grillhouse/fichiers.txt`, each file's SHA-256 in `.claude/grillhouse/empreintes.json`)
|
|
4
|
+
// are what this version replaces, as a codemod of `@turbo/codemod` removes what a version replaces.
|
|
5
|
+
// The list says what to remove: nothing is guessed.
|
|
6
|
+
//
|
|
7
|
+
// - Each file `empreintes.json` records that a line of `fichiers.txt` keeps (the file itself, or a
|
|
8
|
+
// directory « …/ » above it; a line « - … » keeps none) goes when its bytes have the recorded
|
|
9
|
+
// SHA-256 (as the former update computed it), or had it before the migration 0.23.0 of Vitest
|
|
10
|
+
// rewrote its import of the base (`formesAnterieures` of 0.23.0-config-vitest.mjs); a directory
|
|
11
|
+
// it leaves empty goes with it. A file the template of a project holds at the same path
|
|
12
|
+
// (`tsdoc.json`, `docs/agents/issue-tracker.md`) is the project's own since 0.23.0, as
|
|
13
|
+
// `grillhouse init` places it, and stays.
|
|
14
|
+
// - Under a directory the list keeps that is no workspace of the project (scripts/paquets.mjs), a
|
|
15
|
+
// file the former generator of the packages' turbo.json wrote (its first line « // Généré par
|
|
16
|
+
// `…turbo-des-paquets…` ») goes too: it belonged to the former workspace of a copy.
|
|
17
|
+
// - A file whose bytes differ (the project modified it) stays: the migration leaves it to the
|
|
18
|
+
// project (`{ aFaire }`), with the command that removes it, and the update's exit code is
|
|
19
|
+
// non-zero; an npm script that runs it is named with it, since its removal breaks that script.
|
|
20
|
+
// While one stays, the list and the fingerprints stay, so that the next update names it again;
|
|
21
|
+
// once none stays, `.claude/grillhouse/` goes whole (the list, the fingerprints, the version, the
|
|
22
|
+
// former charter). A modified copy of a role 0.24.0 removes stays too, but is named by that
|
|
23
|
+
// migration alone (`deRoleSupprime` of 0.24.0-circuit-simplifie.mjs), once.
|
|
24
|
+
// - Each root npm script that runs a copy removed (`node scripts/<chemin>`, `bash scripts/<chemin>`,
|
|
25
|
+
// its arguments kept) runs its subcommand, `grillhouse <sous-commande>`, by the table of
|
|
26
|
+
// scripts/sous-commandes.mjs; a script that still names a copy removed (no subcommand runs it) is
|
|
27
|
+
// named, not rewritten.
|
|
28
|
+
// - A dependency of the project's root that a copy removed imported, that the package declares in
|
|
29
|
+
// its own `dependencies`, that the template of a project does not declare, and that no source of
|
|
30
|
+
// the project still imports, is named, not removed: the project decides.
|
|
31
|
+
// - A project without `fichiers.txt` and `empreintes.json` is not written: a second pass finds
|
|
32
|
+
// neither, or finds only the copies left to the project, which it names again.
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
34
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { CADRE, PAQUET } from '../cadre.mjs';
|
|
37
|
+
import { paquets } from '../paquets.mjs';
|
|
38
|
+
import { sources } from '../sources.mjs';
|
|
39
|
+
import { SOUS_COMMANDES } from '../sous-commandes.mjs';
|
|
40
|
+
import { formesAnterieures } from './0.23.0-config-vitest.mjs';
|
|
41
|
+
import { deRoleSupprime } from './0.24.0-circuit-simplifie.mjs';
|
|
42
|
+
|
|
43
|
+
export const version = '0.23.2';
|
|
44
|
+
export const nom = 'copies du cadre';
|
|
45
|
+
|
|
46
|
+
const DOSSIER = '.claude/grillhouse';
|
|
47
|
+
const LISTE = `${DOSSIER}/fichiers.txt`;
|
|
48
|
+
const EMPREINTES = `${DOSSIER}/empreintes.json`;
|
|
49
|
+
const GABARIT = 'templates/projet';
|
|
50
|
+
const CHAMPS = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
51
|
+
const CODE = /\.(?:[mc]?ts|[mc]?js)$/;
|
|
52
|
+
// A call of a script of the frame's former copies in an npm script: its runner, then its path.
|
|
53
|
+
const APPEL = /(^|[\s;&|(])(?:node|bash)\s+(?:\.\/)?(scripts\/[^\s'";&|)]+)/g;
|
|
54
|
+
// The first line of a file the generator of the packages' turbo.json wrote.
|
|
55
|
+
const GENERE = /^\/\/ Généré par `[^`\n]*turbo-des-paquets[^`\n]*`/;
|
|
56
|
+
|
|
57
|
+
const hacher = octets => createHash('sha256').update(octets).digest('hex');
|
|
58
|
+
const lireJson = f => JSON.parse(readFileSync(f, 'utf8'));
|
|
59
|
+
const echapper = t => t.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
60
|
+
// Whether the source text <texte> imports the package <dep> (or a path into it).
|
|
61
|
+
const importe = (texte, dep) =>
|
|
62
|
+
new RegExp(
|
|
63
|
+
`(?:\\bfrom\\s*|\\bimport\\s*\\(?\\s*|\\brequire\\s*\\(\\s*)(['"])${echapper(dep)}(?:/[^'"]*)?\\1`
|
|
64
|
+
).test(texte);
|
|
65
|
+
|
|
66
|
+
// The subcommand that runs each script of the package, by the path of its former copy.
|
|
67
|
+
const PAR_COPIE = new Map(Object.entries(SOUS_COMMANDES).map(([n, s]) => [`scripts/${s}`, n]));
|
|
68
|
+
|
|
69
|
+
// The lines of the list that keep a path: a file, or a directory « …/ ».
|
|
70
|
+
const gardees = texte =>
|
|
71
|
+
texte
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map(l => l.trim())
|
|
74
|
+
.filter(l => l !== '' && !l.startsWith('#') && !l.startsWith('-'));
|
|
75
|
+
const couvert = (lignes, f) => lignes.some(l => (l.endsWith('/') ? f.startsWith(l) : f === l));
|
|
76
|
+
|
|
77
|
+
// Whether the file at <absolu> has the fingerprint <attendue>, as copied or as 0.23.0 rewrote it.
|
|
78
|
+
function intacte(absolu, attendue) {
|
|
79
|
+
const octets = readFileSync(absolu);
|
|
80
|
+
if (hacher(octets) === attendue) return true;
|
|
81
|
+
return formesAnterieures(octets.toString('utf8')).some(t => hacher(t) === attendue);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The files under the directory <rel> of <projet>, as paths from it.
|
|
85
|
+
function fichiersSous(projet, rel) {
|
|
86
|
+
const absolu = path.join(projet, rel);
|
|
87
|
+
if (!existsSync(absolu)) return [];
|
|
88
|
+
return readdirSync(absolu, { withFileTypes: true }).flatMap(e => {
|
|
89
|
+
const r = `${rel}/${e.name}`;
|
|
90
|
+
if (e.isDirectory()) return fichiersSous(projet, r);
|
|
91
|
+
return e.isFile() ? [r] : [];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Removes each directory above <f> (a path from <projet>) that is left empty.
|
|
96
|
+
function viderDossiers(projet, f) {
|
|
97
|
+
for (let d = path.dirname(f); d !== '.'; d = path.dirname(d)) {
|
|
98
|
+
const absolu = path.join(projet, d);
|
|
99
|
+
if (!existsSync(absolu) || readdirSync(absolu).length > 0) return;
|
|
100
|
+
rmSync(absolu, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Takes the frame's former copies off the project at <projet>; returns what it did. */
|
|
105
|
+
export function migrer(projet) {
|
|
106
|
+
const liste = path.join(projet, LISTE);
|
|
107
|
+
const fEmpreintes = path.join(projet, EMPREINTES);
|
|
108
|
+
if (!existsSync(liste) || !existsSync(fEmpreintes)) return [];
|
|
109
|
+
const lignes = gardees(readFileSync(liste, 'utf8'));
|
|
110
|
+
const enregistres = Object.entries(lireJson(fEmpreintes).fichiers ?? {}).filter(
|
|
111
|
+
([f]) =>
|
|
112
|
+
couvert(lignes, f) &&
|
|
113
|
+
!f.startsWith(`${DOSSIER}/`) &&
|
|
114
|
+
!existsSync(path.join(CADRE, GABARIT, f))
|
|
115
|
+
);
|
|
116
|
+
const sortie = [];
|
|
117
|
+
const retires = new Map();
|
|
118
|
+
const restes = [];
|
|
119
|
+
const retirer = (f, ligne) => {
|
|
120
|
+
const absolu = path.join(projet, f);
|
|
121
|
+
retires.set(f, CODE.test(f) ? readFileSync(absolu, 'utf8') : '');
|
|
122
|
+
rmSync(absolu);
|
|
123
|
+
viderDossiers(projet, f);
|
|
124
|
+
sortie.push(ligne);
|
|
125
|
+
};
|
|
126
|
+
for (const [f, attendue] of enregistres) {
|
|
127
|
+
if (!existsSync(path.join(projet, f))) continue;
|
|
128
|
+
if (intacte(path.join(projet, f), attendue)) {
|
|
129
|
+
retirer(f, `${f} : copie du cadre retirée, que le paquet porte`);
|
|
130
|
+
} else if (!deRoleSupprime(f)) {
|
|
131
|
+
restes.push(f);
|
|
132
|
+
sortie.push({
|
|
133
|
+
aFaire: `${f} : copie du cadre modifiée par le projet, laissée en place ; \`git rm ${f}\` la retire, le paquet porte le cadre`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const espaces = new Set(paquets(projet).map(p => p.dossier));
|
|
138
|
+
for (const d of lignes.filter(l => l.endsWith('/') && !l.startsWith(`${DOSSIER}/`))) {
|
|
139
|
+
const dossier = d.replace(/\/$/, '');
|
|
140
|
+
if (espaces.has(dossier)) continue;
|
|
141
|
+
for (const f of fichiersSous(projet, dossier)) {
|
|
142
|
+
if (!GENERE.test(readFileSync(path.join(projet, f), 'utf8'))) continue;
|
|
143
|
+
retirer(f, `${f} : fichier engendré pour l'ancien espace de travail ${dossier}, retiré`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (restes.length === 0) {
|
|
147
|
+
rmSync(path.join(projet, DOSSIER), { recursive: true, force: true });
|
|
148
|
+
viderDossiers(projet, DOSSIER);
|
|
149
|
+
sortie.push(`${DOSSIER}/ : retiré (la liste et les empreintes des copies du cadre)`);
|
|
150
|
+
} else {
|
|
151
|
+
for (const e of readdirSync(path.join(projet, DOSSIER))) {
|
|
152
|
+
if (`${DOSSIER}/${e}` !== LISTE && `${DOSSIER}/${e}` !== EMPREINTES) {
|
|
153
|
+
retirer(`${DOSSIER}/${e}`, `${DOSSIER}/${e} : retiré`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fPkg = path.join(projet, 'package.json');
|
|
159
|
+
if (!existsSync(fPkg)) return sortie;
|
|
160
|
+
const pkg = lireJson(fPkg);
|
|
161
|
+
let reecrit = false;
|
|
162
|
+
for (const [n, script] of Object.entries(pkg.scripts ?? {})) {
|
|
163
|
+
if (typeof script !== 'string') continue;
|
|
164
|
+
const neuf = script.replace(APPEL, (tout, avant, f) =>
|
|
165
|
+
retires.has(f) && PAR_COPIE.has(f) ? `${avant}grillhouse ${PAR_COPIE.get(f)}` : tout
|
|
166
|
+
);
|
|
167
|
+
if (neuf !== script) {
|
|
168
|
+
pkg.scripts[n] = neuf;
|
|
169
|
+
reecrit = true;
|
|
170
|
+
sortie.push(`package.json : le script ${n} lance \`${neuf}\``);
|
|
171
|
+
}
|
|
172
|
+
for (const f of retires.keys()) {
|
|
173
|
+
if (neuf.includes(f)) {
|
|
174
|
+
sortie.push(
|
|
175
|
+
`package.json : le script ${n} nomme ${f}, copie retirée qu'aucune sous-commande ne remplace ; à réécrire`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const f of restes.filter(r => neuf.includes(r))) {
|
|
180
|
+
const sous = PAR_COPIE.get(f);
|
|
181
|
+
sortie.push({
|
|
182
|
+
aFaire: `package.json : le script ${n} lance ${f}, copie modifiée laissée en place ; son \`git rm\` le casse : ${sous ? `\`grillhouse ${sous}\` le remplace` : 'à réécrire'}`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (reecrit) writeFileSync(fPkg, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
187
|
+
|
|
188
|
+
const duPaquet = Object.keys(lireJson(path.join(PAQUET, 'package.json')).dependencies ?? {});
|
|
189
|
+
const gabarit = lireJson(path.join(CADRE, GABARIT, 'package.json'));
|
|
190
|
+
const textes = sources(projet).map(f => readFileSync(path.join(projet, f), 'utf8'));
|
|
191
|
+
for (const dep of duPaquet) {
|
|
192
|
+
if (!CHAMPS.some(c => pkg[c]?.[dep] !== undefined)) continue;
|
|
193
|
+
if (CHAMPS.some(c => gabarit[c]?.[dep] !== undefined)) continue;
|
|
194
|
+
if (![...retires.values()].some(t => importe(t, dep))) continue;
|
|
195
|
+
if (textes.some(t => importe(t, dep))) continue;
|
|
196
|
+
sortie.push(
|
|
197
|
+
`package.json : la dépendance ${dep} n'est plus importée que par le paquet, qui la déclare lui-même ; \`npm uninstall ${dep}\` la retire si le projet n'en a pas l'usage`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return sortie;
|
|
201
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// The circuit of 0.24.0 has no regulator and no technical referent: the circuit makes the mechanical
|
|
2
|
+
// gestures, and any other question goes up to the supervisor with its piece, who takes to the owner
|
|
3
|
+
// what is his. What a project holds of the former circuit is rewritten here, as a codemod of
|
|
4
|
+
// `@turbo/codemod` rewrites what a version changes, and nothing else.
|
|
5
|
+
//
|
|
6
|
+
// - The rubric « Ce qui monte au superviseur » of the project's CLAUDE.md: the paragraph of the
|
|
7
|
+
// template of 0.22.x and 0.23.x (« Le fonctionnel que le régulateur ne tranche jamais seul : <la
|
|
8
|
+
// liste>. Le cadre et l'architecture montent toujours (charte commune). »), its list as the
|
|
9
|
+
// project filled it, becomes the paragraph of the template of 0.24.0, the list kept, wrapped at
|
|
10
|
+
// 100 columns; the rest of the rubric stays. A rubric the project wrote itself that still names
|
|
11
|
+
// a removed role is left to the project (`{ aFaire }`); one that names none is left silently.
|
|
12
|
+
// - `docs/agents/issue-tracker.md`, the project's own since 0.23.0 (`grillhouse init` places it):
|
|
13
|
+
// a file whose bytes are those of the template of 0.21.0 to 0.22.x, or of 0.23.x (their SHA-256
|
|
14
|
+
// in FORMES), becomes the template's; a file the project modified that names a removed role is
|
|
15
|
+
// left to the project.
|
|
16
|
+
// - A copy of a removed role (`.claude/agents/<rôle>.md`, `.claude/skills/<rôle>/`) still in the
|
|
17
|
+
// project is one the migration of the copies (0.23.2-copies-du-cadre.mjs, played before) could
|
|
18
|
+
// not know as the frame's: the project modified it, or no list named it. It is left to the
|
|
19
|
+
// project, with the command that removes it; the migration of the copies leaves it to this one
|
|
20
|
+
// (deRoleSupprime), so that it is named once.
|
|
21
|
+
// - What stays to the project (`restes`, which the update plays after its own update and its
|
|
22
|
+
// regeneration, so that it sees the final state): a tracked file of the project (`git ls-files`,
|
|
23
|
+
// node_modules and the changelogs aside) that still calls or names a script of the frame at its former address under `scripts/`
|
|
24
|
+
// (`node scripts/<script>`, `$RACINE/scripts/<script>`, `path.join(…, 'scripts', '<script>')`;
|
|
25
|
+
// a tracked path that is no regular file, a submodule or a link, is skipped),
|
|
26
|
+
// where the project holds no such file any more, is named with its file:line and what replaces
|
|
27
|
+
// it: the subcommand (`SOUS_COMMANDES` of scripts/sous-commandes.mjs), the hook
|
|
28
|
+
// (`grillhouse hook <nom>`, `CROCHETS` of scripts/crochets.mjs) or the named entry (the
|
|
29
|
+
// `exports` of the package). The project's scripts are its own: none is rewritten, and the
|
|
30
|
+
// lines are a report, which never holds back the version the project records, as a codemod
|
|
31
|
+
// reports what it leaves without failing.
|
|
32
|
+
// - No version of the update wrote a setting of a role in `.claude/settings.json` or under
|
|
33
|
+
// `grillhouse` of the package.json (the update of 0.22.x wrote the hooks, the status line and the
|
|
34
|
+
// plugins; the rights of a role are given on the command line of its session, scripts/droits.mjs):
|
|
35
|
+
// the migration writes neither.
|
|
36
|
+
// - A project already in the new form is not written, and a second pass reports only what it left
|
|
37
|
+
// to the project.
|
|
38
|
+
import { execFileSync } from 'node:child_process';
|
|
39
|
+
import { createHash } from 'node:crypto';
|
|
40
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync } from 'node:fs';
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
import { CADRE, PAQUET } from '../cadre.mjs';
|
|
43
|
+
import { CROCHETS } from '../crochets.mjs';
|
|
44
|
+
import { SOUS_COMMANDES } from '../sous-commandes.mjs';
|
|
45
|
+
|
|
46
|
+
export const version = '0.24.0';
|
|
47
|
+
export const nom = 'circuit simplifié';
|
|
48
|
+
|
|
49
|
+
const SUPPRIMES = ['regulateur', 'referent-technique'];
|
|
50
|
+
|
|
51
|
+
/** Whether <f>, a path from a project's root, is in a copy of a removed role, which this migration names. */
|
|
52
|
+
export const deRoleSupprime = f =>
|
|
53
|
+
SUPPRIMES.some(r => f === `.claude/agents/${r}.md` || f.startsWith(`.claude/skills/${r}/`));
|
|
54
|
+
// A removed role named in a text, in its words or by its name.
|
|
55
|
+
const NOMME_UN_SUPPRIME = /r[ée]gulat(?:eur|rice)|referent-technique|r[ée]f[ée]rents? techniques?/i;
|
|
56
|
+
|
|
57
|
+
const RUBRIQUE = 'Ce qui monte au superviseur';
|
|
58
|
+
// The section of the rubric, its title line apart, up to the next section or the end.
|
|
59
|
+
const SECTION = new RegExp(`^(## ${RUBRIQUE}[ \\t]*\\n)([^]*?)(?=^## |(?![^]))`, 'm');
|
|
60
|
+
// The paragraph of the former template, its list captured, as a paragraph of its own.
|
|
61
|
+
const ANCIEN =
|
|
62
|
+
/(^\n*|\n\n)Le\s+fonctionnel\s+que\s+le\s+régulateur\s+ne\s+tranche\s+jamais\s+seul\s+:\s+([^]*?)\.\s+Le\s+cadre\s+et\s+l'architecture\s+montent\s+toujours\s+\(charte\s+commune\)\.(?=\n\n|\n?$)/;
|
|
63
|
+
const NEUF = liste =>
|
|
64
|
+
`Une question qu'aucun geste du circuit ne tranche monte au superviseur, avec sa pièce. Il monte au responsable le fonctionnel, qu'il ne tranche jamais seul : ${liste}. Le cadre et l'architecture montent toujours (charte commune).`;
|
|
65
|
+
const COLONNES = 100;
|
|
66
|
+
|
|
67
|
+
const SUIVI = 'docs/agents/issue-tracker.md';
|
|
68
|
+
// The SHA-256 of the guide of the issue tracker as the template held it: 0.21.0 to 0.22.6 (the
|
|
69
|
+
// copy the update placed), 0.23.0 and 0.23.1 (the template `grillhouse init` places).
|
|
70
|
+
const FORMES = new Set([
|
|
71
|
+
'2b7c70b22001c83009887372f2bc5b04509e8e4dbc14f1e647df2c686b0ba5e0',
|
|
72
|
+
'a6d6e9ecbb75b82dad9c6adadd310fe1047c4a5f83d4d789718cd921eda6b258',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const hacher = octets => createHash('sha256').update(octets).digest('hex');
|
|
76
|
+
|
|
77
|
+
// The words of <texte> in lines of at most COLONNES characters.
|
|
78
|
+
function replier(texte) {
|
|
79
|
+
const lignes = [];
|
|
80
|
+
for (const mot of texte.split(/\s+/).filter(Boolean)) {
|
|
81
|
+
const l = lignes.length - 1;
|
|
82
|
+
if (l >= 0 && lignes[l].length + 1 + mot.length <= COLONNES) {
|
|
83
|
+
lignes[l] += ` ${mot}`;
|
|
84
|
+
} else {
|
|
85
|
+
lignes.push(mot);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lignes.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The rubric of the CLAUDE.md of <projet>.
|
|
92
|
+
function rubrique(projet) {
|
|
93
|
+
const fichier = path.join(projet, 'CLAUDE.md');
|
|
94
|
+
if (!existsSync(fichier)) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const texte = readFileSync(fichier, 'utf8');
|
|
98
|
+
const s = SECTION.exec(texte);
|
|
99
|
+
if (!s) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const [tout, titre, corps] = s;
|
|
103
|
+
if (ANCIEN.test(corps)) {
|
|
104
|
+
const neuf = corps.replace(ANCIEN, (_, avant, liste) => `${avant}${replier(NEUF(liste))}`);
|
|
105
|
+
writeFileSync(
|
|
106
|
+
fichier,
|
|
107
|
+
texte.replace(tout, () => `${titre}${neuf}`)
|
|
108
|
+
);
|
|
109
|
+
return [
|
|
110
|
+
`CLAUDE.md : la rubrique « ${RUBRIQUE} » dit le circuit de la 0.24.0 (une question qu'aucun geste ne tranche monte au superviseur, avec sa pièce), sa liste gardée`,
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
if (!NOMME_UN_SUPPRIME.test(corps)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
aFaire: `CLAUDE.md : la rubrique « ${RUBRIQUE} », que le projet a écrite, nomme un rôle que la 0.24.0 supprime (le régulateur ou le référent technique) ; à réécrire : une question qu'aucun geste du circuit ne tranche monte au superviseur, avec sa pièce, et il monte au responsable ce que la rubrique nomme`,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The guide of the issue tracker of <projet>.
|
|
124
|
+
function suivi(projet) {
|
|
125
|
+
const fichier = path.join(projet, SUIVI);
|
|
126
|
+
if (!existsSync(fichier)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const octets = readFileSync(fichier);
|
|
130
|
+
if (FORMES.has(hacher(octets))) {
|
|
131
|
+
writeFileSync(fichier, readFileSync(path.join(CADRE, 'templates/projet', SUIVI)));
|
|
132
|
+
return [`${SUIVI} : le guide du gabarit, qui dit le circuit de la 0.24.0`];
|
|
133
|
+
}
|
|
134
|
+
if (!NOMME_UN_SUPPRIME.test(octets.toString('utf8'))) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
aFaire: `${SUIVI} : modifié par le projet, il nomme un rôle que la 0.24.0 supprime ; le gabarit du paquet (templates/projet/${SUIVI}) dit le circuit neuf`,
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The copies of the removed roles left in <projet>.
|
|
145
|
+
function copies(projet) {
|
|
146
|
+
return SUPPRIMES.flatMap(r => [
|
|
147
|
+
{ f: `.claude/agents/${r}.md`, rm: 'git rm' },
|
|
148
|
+
{ f: `.claude/skills/${r}`, rm: 'git rm -r' },
|
|
149
|
+
])
|
|
150
|
+
.filter(({ f }) => existsSync(path.join(projet, f)))
|
|
151
|
+
.map(({ f, rm }) => ({
|
|
152
|
+
aFaire: `${f} : copie d'un rôle que la 0.24.0 supprime (le circuit ne le lance plus), laissée en place ; \`${rm} ${f}\` la retire`,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// What replaces each script of the frame, by its path under `scripts/`: its subcommand, its hook,
|
|
157
|
+
// its named entry.
|
|
158
|
+
function remplacants() {
|
|
159
|
+
const r = new Map();
|
|
160
|
+
const ajouter = (script, quoi) => r.set(script, [...(r.get(script) ?? []), quoi]);
|
|
161
|
+
for (const [n, s] of Object.entries(SOUS_COMMANDES)) {
|
|
162
|
+
ajouter(s, `\`grillhouse ${n}\``);
|
|
163
|
+
}
|
|
164
|
+
for (const [n, c] of Object.entries(CROCHETS)) {
|
|
165
|
+
ajouter(c.garde, `\`grillhouse hook ${n}\``);
|
|
166
|
+
}
|
|
167
|
+
const { exports = {} } = JSON.parse(readFileSync(path.join(PAQUET, 'package.json'), 'utf8'));
|
|
168
|
+
for (const [entree, cible] of Object.entries(exports)) {
|
|
169
|
+
const d = typeof cible === 'string' ? cible : cible?.default;
|
|
170
|
+
if (typeof d === 'string' && d.startsWith('./scripts/')) {
|
|
171
|
+
ajouter(d.slice('./scripts/'.length), `\`@roomi-fields/grillhouse${entree.slice(1)}\``);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return r;
|
|
175
|
+
}
|
|
176
|
+
// A script named at its former address: `scripts/<chemin>`, alone or after `./` or `$VAR/`.
|
|
177
|
+
const ADRESSE = /(?<![\w./-])(?:\.\/|\$\{?\w+\}?\/)?scripts\/([\w./-]+?\.(?:mjs|sh))(?![\w.-])/g;
|
|
178
|
+
// The same, as the segments of a path.join: `'scripts', 'dette.mjs'`.
|
|
179
|
+
const SEGMENTS = /(['"])scripts\1((?:\s*,\s*(['"])[\w.-]+\3)+)/g;
|
|
180
|
+
const SANS = /(^|\/)(?:node_modules\/|CHANGELOG\.md$)/;
|
|
181
|
+
|
|
182
|
+
/** The lines of the tracked files of <projet> that still reach a script of the frame at its former address. */
|
|
183
|
+
export function restes(projet) {
|
|
184
|
+
let suivis;
|
|
185
|
+
try {
|
|
186
|
+
suivis = execFileSync('git', ['-C', projet, 'ls-files', '-z'], { encoding: 'utf8' })
|
|
187
|
+
.split('\0')
|
|
188
|
+
.filter(f => f && !SANS.test(f));
|
|
189
|
+
} catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const par = remplacants();
|
|
193
|
+
const sortie = [];
|
|
194
|
+
for (const f of suivis) {
|
|
195
|
+
const absolu = path.join(projet, f);
|
|
196
|
+
// A submodule, a link, a file gone from the disk: no text of the project to read.
|
|
197
|
+
if (!existsSync(absolu) || !lstatSync(absolu).isFile()) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const texte = readFileSync(absolu, 'utf8');
|
|
201
|
+
if (texte.includes('\0')) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
texte.split('\n').forEach((ligne, i) => {
|
|
205
|
+
const scripts = [
|
|
206
|
+
...[...ligne.matchAll(ADRESSE)].map(m => m[1]),
|
|
207
|
+
...[...ligne.matchAll(SEGMENTS)].map(m =>
|
|
208
|
+
m[2]
|
|
209
|
+
.split(',')
|
|
210
|
+
.map(s => s.trim().slice(1, -1))
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join('/')
|
|
213
|
+
),
|
|
214
|
+
];
|
|
215
|
+
for (const s of new Set(scripts)) {
|
|
216
|
+
if (!par.has(s) || existsSync(path.join(projet, 'scripts', s))) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
sortie.push(
|
|
220
|
+
`${f}:${i + 1} : appelle scripts/${s}, copie du cadre retirée ; ${par.get(s).join(' ou ')} la remplace`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return sortie;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Rewrites in the project at <projet> what the circuit of 0.24.0 changes; returns what it did. */
|
|
229
|
+
export function migrer(projet) {
|
|
230
|
+
return [...rubrique(projet), ...suivi(projet), ...copies(projet)];
|
|
231
|
+
}
|