@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,628 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The envelope of a role agent: runs a command (the agent's `claude` session) inside a bwrap
|
|
3
|
+
// sandbox that shows the agent its own component, and of every other component only what it
|
|
4
|
+
// publishes. `npx grillhouse enveloppe <copie> <composant> <rôle> -- <commande…>`
|
|
5
|
+
//
|
|
6
|
+
// - The root `/` is read-only. Only these are written: the agent's copy (for a component that is a
|
|
7
|
+
// package, that package alone, below); a `/tmp` empty and
|
|
8
|
+
// proper to the session, where only the scratchpad directory of the copy comes back, the
|
|
9
|
+
// session's working directory inside it (`dossierDeTravail`; Claude Code's scratchpads root,
|
|
10
|
+
// `CLAUDE_CODE_TMPDIR` else the launch's temporary directory, so that under a pass it lies in
|
|
11
|
+
// the base of the pass and goes with it); the npm
|
|
12
|
+
// cache; the Claude session state (`~/.claude` and the copy's project directory), whose
|
|
13
|
+
// configuration, skills, plugins and other projects stay read-only. `~/.claude.json` is a
|
|
14
|
+
// throwaway copy: the session writes it, the real file does not change. The tickets base (the
|
|
15
|
+
// main tree's `.beads/`) is written: the agent claims, plans, hands over and files what it
|
|
16
|
+
// finds. Every `.git`, the
|
|
17
|
+
// copy's included, stays read-only: the agent reads the git state and delivers a patch
|
|
18
|
+
// (`git diff`, and `git diff --no-index /dev/null <file>` for a new file); it never commits.
|
|
19
|
+
// The cache of Turborepo (`<main tree>/.turbo/cache`, shared by every copy) is written: the
|
|
20
|
+
// agent's runs of `npm test` keep their tasks there, as every copy's runs do.
|
|
21
|
+
// - The home and the session directories (`$XDG_RUNTIME_DIR`, `/run/user/<uid>`) show only what
|
|
22
|
+
// the envelope declares: each starts empty and sealed read-only, and the home gets back
|
|
23
|
+
// `~/.claude`, `~/.claude.json`, the npm cache, the paths of `MAISON_EN_LECTURE` (claude, node,
|
|
24
|
+
// git and the session's tools) and the repository. No socket of the session comes back. Of
|
|
25
|
+
// the machine's credentials, only claude's own are in sight.
|
|
26
|
+
// - The session gets only the environment the envelope declares (`environnementDeLaSeance`), as
|
|
27
|
+
// sudo's env_reset and bwrap's --clearenv give it: the base of a login session, claude's own
|
|
28
|
+
// configuration and credentials (`ANTHROPIC_*`, `CLAUDE_CODE_OAUTH_TOKEN`), the author of its
|
|
29
|
+
// comments at the ticket (`BEADS_ACTOR`, which bd reads), the npm cache it
|
|
30
|
+
// mounts, `npm_config_ignore_scripts=true`, and `TMPDIR`, the session's working directory, where
|
|
31
|
+
// `mktemp` and `os.tmpdir()` write. With it npm runs no lifecycle script (`npm install` leaves the
|
|
32
|
+
// `prepare` that writes under the read-only `.git`; the launcher installs outside the session),
|
|
33
|
+
// while `npm test` and `npm run <script>` still run their script, without its `pre` or `post`
|
|
34
|
+
// (npm's `ignore-scripts`, docs/content/using-npm/config.md); no script of the frame has one. bwrap
|
|
35
|
+
// starts with that environment alone, so no other variable of the machine (`GH_TOKEN`,
|
|
36
|
+
// `NPM_TOKEN`…) enters, by name or by argument. The session also gets `GRILLHOUSE_ENVELOPPE`, the
|
|
37
|
+
// component's path (`composant.mjs`): the frame's commands build and write that component only. A declared variable that names a path
|
|
38
|
+
// (`VARIABLES_DE_CHEMIN`) enters only when its path is in sight inside the envelope, tested there
|
|
39
|
+
// before the command runs. The session has its own process space: it reads the environment of
|
|
40
|
+
// no process of the machine.
|
|
41
|
+
// - No index of the repository is in sight: neither codegraph nor rtfm comes back in the home,
|
|
42
|
+
// and every index directory (`INDEX`) of every worktree, the copy's included, is an empty
|
|
43
|
+
// directory sealed read-only; the worktree around it keeps its mount, the copy in writing.
|
|
44
|
+
// - A declared path that is a symbolic link comes back as that link, and its target with it
|
|
45
|
+
// (`avecLiens`).
|
|
46
|
+
// - A worktree of the repository under an emptied directory (the home, `/tmp`) comes back
|
|
47
|
+
// read-only, `.git` included; the copy and the tickets base are then written as above.
|
|
48
|
+
// - An envelope on the main tree is refused, and so is a component that does not exist; when it is
|
|
49
|
+
// the name of a package of the copy, the refusal gives its path.
|
|
50
|
+
// - <copie> is the agent's git worktree; <composant> is the component's path inside it
|
|
51
|
+
// (`packages/a`, or `src/parser` in a one-package project); <rôle> is the agent's role
|
|
52
|
+
// (`testwriter`, `developpeur`…).
|
|
53
|
+
// - In every worktree of the repository, the component's parent directory (`packages/`, `src/`)
|
|
54
|
+
// is emptied. In <copie>, the component is mounted back in writing; every sibling component
|
|
55
|
+
// gets back, read-only, its published parts: `package.json`, `docs/INTERFACE.md`, `dist/`, its
|
|
56
|
+
// package configuration of Turborepo (`turbo.json`, `turbo.jsonc`), by which Turborepo reads in
|
|
57
|
+
// the envelope the task graph it reads outside, the paths that the `files` field of its
|
|
58
|
+
// `package.json` names, and the directory each trace it declares writes (`grillhouse.traces`,
|
|
59
|
+
// which the launcher produces before the session), when they exist inside it.
|
|
60
|
+
// The parent's own files (`src/index.ts`) come back read-only, and the emptied directories are
|
|
61
|
+
// then sealed read-only.
|
|
62
|
+
// - What the component's tests declare they read comes back read-only, as Bazel gives a test the
|
|
63
|
+
// `data` it declares: each path of `grillhouse.lit` of its `package.json`, and of the `lit` of
|
|
64
|
+
// each trace it declares (`cheminsLus`, each path from the root of <copie>), that lies in a
|
|
65
|
+
// sibling component and exists, a directory or a file. A path the envelope already shows (the
|
|
66
|
+
// root, the component itself, the shared test material, whose mount the role decides) adds no
|
|
67
|
+
// mount.
|
|
68
|
+
// - For a component that is a package of the copy (`paquets` of `scripts/paquets.mjs`), the agent
|
|
69
|
+
// writes that package and nothing else of the copy, as a sandbox of Bazel or of Nix opens in
|
|
70
|
+
// writing only what the action declares: the copy is read-only, the folders of its root that no
|
|
71
|
+
// package holds (`libraries/`, `spec/`) included, and a write there fails on « Read-only file
|
|
72
|
+
// system », which the agent sees. Such a folder is written by the component that owns it, never
|
|
73
|
+
// by a right the envelope adds: what the tests declare they read stays read-only. The package
|
|
74
|
+
// of the shared test material, for the `testwriter`, the session's files and the mounts outside
|
|
75
|
+
// the copy keep their rights; where a run of Turborepo writes at the root of the copy
|
|
76
|
+
// (`sortiesDeLaRacine`, its `.turbo`) is an empty directory proper to the session, created on
|
|
77
|
+
// disk before it.
|
|
78
|
+
// - A component that is no package (a folder of the root, `scripts`; `src/parser` in a one-package
|
|
79
|
+
// project) writes the package that holds it, the root: the copy stays written. A folder of the
|
|
80
|
+
// root hides nothing: its scripts work on the whole repository. The rest holds: the root
|
|
81
|
+
// read-only, the copy written, its `.git` read-only.
|
|
82
|
+
// - The package of the shared test material is every component of <copie> whose `package.json`
|
|
83
|
+
// exports `./test-fixtures` (`matieres` of `scripts/consommateurs.mjs`). It comes back whole, after the agent's own component:
|
|
84
|
+
// written for the `testwriter`, read-only for every other role, its own component included. For
|
|
85
|
+
// such a role, where a run of its tests writes in it (`sortiesDeTest`: Turborepo's task logs,
|
|
86
|
+
// Vite's cache) is an empty directory proper to the session, as Bazel writes a test's logs out
|
|
87
|
+
// of its source: what is written there leaves no trace. The envelope creates each of them on
|
|
88
|
+
// disk, empty, before the session, so that bwrap mounts over it inside the read-only package.
|
|
89
|
+
// - The traversal guard refuses to launch when a symbolic link inside the component, or inside a
|
|
90
|
+
// package of the shared test material, points into a hidden directory: such a link would cross
|
|
91
|
+
// the envelope.
|
|
92
|
+
// - The exit codes are named in `codes.mjs`.
|
|
93
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
94
|
+
import {
|
|
95
|
+
copyFileSync,
|
|
96
|
+
existsSync,
|
|
97
|
+
mkdirSync,
|
|
98
|
+
lstatSync,
|
|
99
|
+
readdirSync,
|
|
100
|
+
readFileSync,
|
|
101
|
+
readlinkSync,
|
|
102
|
+
statSync,
|
|
103
|
+
} from 'node:fs';
|
|
104
|
+
import os from 'node:os';
|
|
105
|
+
import path from 'node:path';
|
|
106
|
+
import { pathToFileURL } from 'node:url';
|
|
107
|
+
import { matieres as lesMatieres } from '../consommateurs.mjs';
|
|
108
|
+
import { paquets as lesPaquets } from '../paquets.mjs';
|
|
109
|
+
import { CODE_REFUS, CODE_USAGE } from './codes.mjs';
|
|
110
|
+
import { VARIABLE } from './composant.mjs';
|
|
111
|
+
import { ouvrirBase } from '../vitest-config/base-de-passe.mjs';
|
|
112
|
+
|
|
113
|
+
const PUBLISHED = ['package.json', 'docs/INTERFACE.md', 'dist', 'turbo.json', 'turbo.jsonc'];
|
|
114
|
+
// The role that writes the package of the shared test material.
|
|
115
|
+
const ROLE_MATIERE = 'testwriter';
|
|
116
|
+
// What a run of a package's tests writes in that package, relative to it: the logs of its tasks,
|
|
117
|
+
// which Turborepo writes on every run, a task replayed from its cache included
|
|
118
|
+
// (`.turbo/turbo-<task>.log`), and no option of Turborepo sends elsewhere; the cache of Vite, where
|
|
119
|
+
// Vitest writes the results of the run (`node_modules/.vite/vitest/`), and whose failure fails it.
|
|
120
|
+
const SORTIES_DE_TEST = ['.turbo', 'node_modules/.vite'];
|
|
121
|
+
// The directories of <dir> where a run of its tests writes.
|
|
122
|
+
const sortiesDeTest = dir => SORTIES_DE_TEST.map(s => path.join(dir, s));
|
|
123
|
+
// What a run of Turborepo writes at the root of the repository, relative to it: its run summaries
|
|
124
|
+
// and the state of its daemon (`.turbo`), its cache living in the main tree (`magasins`).
|
|
125
|
+
const SORTIES_DE_LA_RACINE = ['.turbo'];
|
|
126
|
+
// The directories of the copy <copie> where a run of Turborepo writes.
|
|
127
|
+
const sortiesDeLaRacine = copie => SORTIES_DE_LA_RACINE.map(s => path.join(copie, s));
|
|
128
|
+
// True when <composant> of <copie> is a package of the copy (`paquets`, the directory of each
|
|
129
|
+
// relative to it): its envelope then writes that package alone.
|
|
130
|
+
const estUnPaquet = (copie, composant, paquets) =>
|
|
131
|
+
paquets.some(p => path.resolve(copie, p.dossier) === path.resolve(copie, composant));
|
|
132
|
+
// The directories where codegraph and rtfm keep the index of a tree they index: the source of
|
|
133
|
+
// every component, which the envelope hides.
|
|
134
|
+
const INDEX = ['.codegraph', '.rtfm'];
|
|
135
|
+
// The machine's temporary directory: an empty one, proper to the session, replaces it.
|
|
136
|
+
const TEMPORAIRE = '/tmp';
|
|
137
|
+
// What stays read-only inside `~/.claude`: the configuration and what it loads.
|
|
138
|
+
const CLAUDE_EN_LECTURE = [
|
|
139
|
+
'settings.json',
|
|
140
|
+
'settings.local.json',
|
|
141
|
+
'CLAUDE.md',
|
|
142
|
+
'keybindings.json',
|
|
143
|
+
'agents',
|
|
144
|
+
'commands',
|
|
145
|
+
'hooks',
|
|
146
|
+
'plugins',
|
|
147
|
+
'skills',
|
|
148
|
+
'projects',
|
|
149
|
+
];
|
|
150
|
+
// What the home shows read-only besides `~/.claude`, as paths relative to it: claude's launcher
|
|
151
|
+
// and versions, node and its global tools (bd), the state of bd and of its dolt database, and the
|
|
152
|
+
// git configuration. The home shows nothing else: neither codegraph nor rtfm, whose index is for
|
|
153
|
+
// the roles that see the whole repository.
|
|
154
|
+
const MAISON_EN_LECTURE = [
|
|
155
|
+
'.local/bin/claude',
|
|
156
|
+
'.local/share/claude',
|
|
157
|
+
'.nvm',
|
|
158
|
+
'.config/bd',
|
|
159
|
+
'.beads',
|
|
160
|
+
'.dolt',
|
|
161
|
+
'.gitconfig',
|
|
162
|
+
'.config/git/config',
|
|
163
|
+
'.config/git/ignore',
|
|
164
|
+
'.config/git/attributes',
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
// The variables of a login session that the envelope passes on, as sudo's env_reset keeps them,
|
|
168
|
+
// claude's OAuth credential, the author of the session's comments at the ticket (`BEADS_ACTOR`,
|
|
169
|
+
// which the circuit sets to the session's role), and the prefixes of the families it passes on:
|
|
170
|
+
// the locale, and the configuration and credentials of claude's API. The state of a launching claude session
|
|
171
|
+
// (`CLAUDE_CODE_SESSION_ID`, its messaging socket and token…) stays out.
|
|
172
|
+
const ENVIRONNEMENT_DECLARE = [
|
|
173
|
+
'HOME',
|
|
174
|
+
'PATH',
|
|
175
|
+
'USER',
|
|
176
|
+
'LOGNAME',
|
|
177
|
+
'SHELL',
|
|
178
|
+
'LANG',
|
|
179
|
+
'LANGUAGE',
|
|
180
|
+
'TZ',
|
|
181
|
+
'TERM',
|
|
182
|
+
'COLORTERM',
|
|
183
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
184
|
+
'BEADS_ACTOR',
|
|
185
|
+
];
|
|
186
|
+
const FAMILLES_DECLAREES = ['LC_', 'ANTHROPIC_'];
|
|
187
|
+
// The declared variables that name a path: each enters only when its path is in sight inside the
|
|
188
|
+
// envelope, so a tool never reads a path the envelope hides.
|
|
189
|
+
const VARIABLES_DE_CHEMIN = [
|
|
190
|
+
'NODE_EXTRA_CA_CERTS',
|
|
191
|
+
'SSL_CERT_FILE',
|
|
192
|
+
'SSL_CERT_DIR',
|
|
193
|
+
'NVM_DIR',
|
|
194
|
+
'XDG_RUNTIME_DIR',
|
|
195
|
+
];
|
|
196
|
+
// Run inside the envelope before the command: unsets each named variable whose path does not
|
|
197
|
+
// exist there, then runs the command. `sh -c EPREUVE sh <variables…> -- <commande…>`.
|
|
198
|
+
const EPREUVE =
|
|
199
|
+
'while [ "$1" != -- ]; do eval "p=\\${$1-}"; ' +
|
|
200
|
+
'if [ -n "$p" ] && [ ! -e "$p" ]; then unset "$1"; fi; shift; done; shift; exec "$@"';
|
|
201
|
+
|
|
202
|
+
// The directory of the scratchpads of <copie> under the environment <env> of the launch: Claude
|
|
203
|
+
// Code keeps them under `<temporary>/claude-<uid>/<working directory in dashes>/`, every sign
|
|
204
|
+
// outside [A-Za-z0-9] a dash, <temporary> its `CLAUDE_CODE_TMPDIR`, else the system's temporary
|
|
205
|
+
// directory as `os.tmpdir()` reads it (TMPDIR, TMP, TEMP, else /tmp): under a pass, the base of
|
|
206
|
+
// the pass (scripts/vitest-config/base-de-passe.mjs), removed with it.
|
|
207
|
+
function scratchpadsDe(copie, env = process.env) {
|
|
208
|
+
const enTirets = path.resolve(copie).replace(/[^A-Za-z0-9]/g, '-');
|
|
209
|
+
const temporaire = env.CLAUDE_CODE_TMPDIR || env.TMPDIR || env.TMP || env.TEMP || TEMPORAIRE;
|
|
210
|
+
return path.join(path.resolve(temporaire), `claude-${process.getuid()}`, enTirets);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// The working directory of a session in <copie> under the environment <env> of the launch:
|
|
214
|
+
// `travail` in the copy's scratchpads, which the envelope mounts in writing. The envelope creates
|
|
215
|
+
// it and names it in `TMPDIR`; the launcher names it in the instruction.
|
|
216
|
+
export function dossierDeTravail(copie, env = process.env) {
|
|
217
|
+
return path.join(scratchpadsDe(copie, env), 'travail');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The environment of the session, from the environment <env> of the launch: the declared
|
|
221
|
+
// variables and families that <env> holds, `npm_config_cache` set to the npm cache that the
|
|
222
|
+
// envelope mounts in writing, `npm_config_ignore_scripts` set to `true` (npm runs no lifecycle
|
|
223
|
+
// script: a `prepare` that writes under the read-only `.git` would fail `npm install`; the
|
|
224
|
+
// launcher installs outside the session), `TMPDIR` set to the session's working directory
|
|
225
|
+
// <travail>, and `GRILLHOUSE_ENVELOPPE` set to <composant>. Nothing else of <env> enters.
|
|
226
|
+
function environnementDeLaSeance(env, { cacheNpm, composant, travail }) {
|
|
227
|
+
const declare = nom =>
|
|
228
|
+
ENVIRONNEMENT_DECLARE.includes(nom) ||
|
|
229
|
+
VARIABLES_DE_CHEMIN.includes(nom) ||
|
|
230
|
+
FAMILLES_DECLAREES.some(f => nom.startsWith(f));
|
|
231
|
+
const garde = Object.entries(env).filter(([nom, v]) => v !== undefined && declare(nom));
|
|
232
|
+
return {
|
|
233
|
+
...Object.fromEntries(garde),
|
|
234
|
+
npm_config_cache: cacheNpm,
|
|
235
|
+
npm_config_ignore_scripts: 'true',
|
|
236
|
+
TMPDIR: travail,
|
|
237
|
+
[VARIABLE]: composant,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The command run inside the envelope: <commande>, behind the test of the variables that name a
|
|
242
|
+
// path.
|
|
243
|
+
function commandeEprouvee(commande) {
|
|
244
|
+
return ['/bin/sh', '-c', EPREUVE, 'sh', ...VARIABLES_DE_CHEMIN, '--', ...commande];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The mounts outside the copy, in bwrap's order: an empty `/tmp`; the home that holds
|
|
248
|
+
// `~/.claude` and the session directories, empty and sealed; the copy's scratchpads, the npm
|
|
249
|
+
// cache and `~/.claude` in writing; the throwaway `~/.claude.json`; over them, what the session
|
|
250
|
+
// reads without writing; the copy's project directory, in writing; last, the declared paths of
|
|
251
|
+
// the home, read-only.
|
|
252
|
+
export function montagesDeLaSeance({
|
|
253
|
+
cacheNpm,
|
|
254
|
+
claude,
|
|
255
|
+
projet,
|
|
256
|
+
etatJetable,
|
|
257
|
+
scratchpads,
|
|
258
|
+
sessions = [],
|
|
259
|
+
}) {
|
|
260
|
+
const maison = path.dirname(claude);
|
|
261
|
+
return [
|
|
262
|
+
{ chemin: TEMPORAIRE, vide: true },
|
|
263
|
+
...[maison, ...sessions].map(chemin => ({ chemin, vide: true, scelle: true })),
|
|
264
|
+
...[scratchpads, cacheNpm, claude].map(chemin => ({ chemin, ecriture: true })),
|
|
265
|
+
{ chemin: `${claude}.json`, source: etatJetable, ecriture: true },
|
|
266
|
+
...CLAUDE_EN_LECTURE.map(nom => ({ chemin: path.join(claude, nom), ecriture: false })),
|
|
267
|
+
{ chemin: projet, ecriture: true },
|
|
268
|
+
...MAISON_EN_LECTURE.map(rel => ({ chemin: path.join(maison, rel), ecriture: false })),
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The mounts with every declared path that is a symbolic link on disk turned into that same link,
|
|
273
|
+
// followed by what it leads to, as Flatpak exposes a link: link by link, each target that lies in
|
|
274
|
+
// an emptied directory and under no other declared path comes back with the link's rights, so a
|
|
275
|
+
// launcher finds its package beside its real path. A target the root shows needs no mount.
|
|
276
|
+
// `lien(path)` gives the text of the link at <path>, or null when <path> is not a link.
|
|
277
|
+
function avecLiens(montages, lien) {
|
|
278
|
+
const videes = montages.filter(m => m.vide).map(m => m.chemin);
|
|
279
|
+
const declares = montages.filter(m => !m.vide && !m.lien).map(m => m.chemin);
|
|
280
|
+
return montages.flatMap(m => {
|
|
281
|
+
if (m.vide || m.lien || (m.source ?? m.chemin) !== m.chemin) return [m];
|
|
282
|
+
const suite = [];
|
|
283
|
+
const vus = new Set();
|
|
284
|
+
let chemin = m.chemin;
|
|
285
|
+
let texte = lien(chemin);
|
|
286
|
+
if (texte === null) return [m];
|
|
287
|
+
while (texte !== null && !vus.has(chemin)) {
|
|
288
|
+
vus.add(chemin);
|
|
289
|
+
suite.push({ chemin, lien: texte });
|
|
290
|
+
chemin = path.resolve(path.dirname(chemin), texte);
|
|
291
|
+
const cachee = videes.some(v => inside(chemin, v));
|
|
292
|
+
const tenue = declares.some(d => d !== m.chemin && inside(chemin, d));
|
|
293
|
+
if (!cachee || tenue) return suite;
|
|
294
|
+
texte = lien(chemin);
|
|
295
|
+
}
|
|
296
|
+
return vus.has(chemin) ? suite : [...suite, { chemin, ecriture: m.ecriture }];
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const lienSurDisque = p => {
|
|
301
|
+
try {
|
|
302
|
+
return lstatSync(p).isSymbolicLink() ? readlinkSync(p) : null;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// The worktrees of the repository that holds <copie>, main tree first.
|
|
309
|
+
function worktrees(copie) {
|
|
310
|
+
const out = execFileSync('git', ['-C', copie, 'worktree', 'list', '--porcelain'], {
|
|
311
|
+
encoding: 'utf8',
|
|
312
|
+
});
|
|
313
|
+
return out
|
|
314
|
+
.split('\n')
|
|
315
|
+
.filter(l => l.startsWith('worktree '))
|
|
316
|
+
.map(l => l.slice('worktree '.length));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// What a directory of the real disk holds, as { name, dir } entries.
|
|
320
|
+
function lister(dir) {
|
|
321
|
+
return readdirSync(dir, { withFileTypes: true }).map(e => ({
|
|
322
|
+
name: e.name,
|
|
323
|
+
dir: e.isDirectory(),
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The symbolic links under <dir> whose resolved target lies outside <dir>.
|
|
328
|
+
function liensSortants(dir) {
|
|
329
|
+
const found = [];
|
|
330
|
+
const walk = d => {
|
|
331
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
332
|
+
const p = path.join(d, e.name);
|
|
333
|
+
if (e.isSymbolicLink()) {
|
|
334
|
+
const target = path.resolve(d, readlinkSync(p));
|
|
335
|
+
const rel = path.relative(dir, target);
|
|
336
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) found.push({ lien: p, cible: target });
|
|
337
|
+
} else if (e.isDirectory() && e.name !== 'node_modules') {
|
|
338
|
+
walk(p);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
walk(dir);
|
|
343
|
+
return found;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// The paths, relative to <dir>, a package of <copie>, that its `package.json` publishes beyond the
|
|
347
|
+
// parts every package publishes: what its `files` field names, and the directory each trace it
|
|
348
|
+
// declares writes (`grillhouse.traces`, its `ecrit` from the root of <copie>); none without them.
|
|
349
|
+
function fichiersPublies(dir, copie) {
|
|
350
|
+
const manifeste = path.join(dir, 'package.json');
|
|
351
|
+
if (!existsSync(manifeste)) return [];
|
|
352
|
+
const { files, grillhouse } = JSON.parse(readFileSync(manifeste, 'utf8'));
|
|
353
|
+
const traces = Object.values(grillhouse?.traces ?? {})
|
|
354
|
+
.filter(t => typeof t?.ecrit === 'string')
|
|
355
|
+
.map(t => path.relative(dir, path.join(copie, t.ecrit)));
|
|
356
|
+
return [...(Array.isArray(files) ? files : []), ...traces];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The paths that the tests of the package at <dir> declare they read: its `grillhouse.lit`, then
|
|
361
|
+
* the `lit` of each trace it declares (`grillhouse.traces`), each path from the root; none without
|
|
362
|
+
* a package.json or a declaration.
|
|
363
|
+
* @param {string} dir the directory of the package
|
|
364
|
+
* @returns {string[]}
|
|
365
|
+
*/
|
|
366
|
+
export function cheminsLus(dir) {
|
|
367
|
+
const manifeste = path.join(dir, 'package.json');
|
|
368
|
+
if (!existsSync(manifeste)) return [];
|
|
369
|
+
const { grillhouse } = JSON.parse(readFileSync(manifeste, 'utf8'));
|
|
370
|
+
const liste = v => (Array.isArray(v) ? v.filter(l => typeof l === 'string') : []);
|
|
371
|
+
return [
|
|
372
|
+
...liste(grillhouse?.lit),
|
|
373
|
+
...Object.values(grillhouse?.traces ?? {}).flatMap(t => liste(t?.lit)),
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const inside = (child, parent) => {
|
|
378
|
+
const rel = path.relative(parent, child);
|
|
379
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// The refusal of <composant> in the repository at <depot>, or null when it is the path of a
|
|
383
|
+
// directory there (`.`, the whole repository, included). A <composant> that is the name of a
|
|
384
|
+
// package of `paquets` (`paquets` of `scripts/paquets.mjs`), or the name of its folder, is told
|
|
385
|
+
// its path, as git says « did you mean ». `fs` gives `existe(path)`. The launcher judges the
|
|
386
|
+
// component with it before preparing the copy, in the ticket's copy when it exists, else in the
|
|
387
|
+
// main tree or in the tree of the ticket's branch, its `existe` accepting either; `plan` judges it
|
|
388
|
+
// in the copy.
|
|
389
|
+
export function jugerComposant({ depot, composant, paquets = [] }, fs) {
|
|
390
|
+
const propre = path.join(depot, composant);
|
|
391
|
+
if (fs.existe(propre)) return null;
|
|
392
|
+
const nomme =
|
|
393
|
+
paquets.find(p => p.pkg?.name === composant) ??
|
|
394
|
+
paquets.find(p => path.basename(p.dossier) === composant);
|
|
395
|
+
return nomme
|
|
396
|
+
? `\`${composant}\` n'est pas le chemin d'un composant ; écris \`${nomme.dossier}\`.`
|
|
397
|
+
: `Le composant ${propre} n'existe pas.`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// The bwrap arguments for this agent, or the refusal that stops its launch. `montages` lists, in
|
|
401
|
+
// order, the paths outside the copy that are emptied, written, read back or made a symbolic link
|
|
402
|
+
// (`lien`, its text) (`montagesDeLaSeance`); an emptied one marked `scelle` is sealed read-only
|
|
403
|
+
// once every mount inside it is made.
|
|
404
|
+
// `matieres` lists the packages of the shared test material, relative to the copy
|
|
405
|
+
// (`matieres` of `scripts/consommateurs.mjs`); `role` is the agent's role, and only the `testwriter` writes them.
|
|
406
|
+
// `magasins` lists the task caches written by every run, the agent's included (Turborepo's
|
|
407
|
+
// `<main tree>/.turbo/cache`); one that does not exist is not mounted.
|
|
408
|
+
// `paquets` lists the packages of the copy, which `jugerComposant` reads to judge <composant>;
|
|
409
|
+
// a <composant> among them writes itself alone of the copy.
|
|
410
|
+
// `fs` gives `lister(dir)`, `existe(path)`, `liens(dir)`, `publies(dir, copie)` and `lus(dir)`
|
|
411
|
+
// (what the tests of the package at <dir> declare they read, `cheminsLus`), so the plan is testable
|
|
412
|
+
// without disk.
|
|
413
|
+
export function plan(
|
|
414
|
+
{ trees, copie, composant, role, matieres = [], montages = [], magasins = [], paquets = [] },
|
|
415
|
+
fs
|
|
416
|
+
) {
|
|
417
|
+
// The worktrees start with the main tree: an envelope on it would let the agent write its root.
|
|
418
|
+
if (trees.length > 0 && path.resolve(trees[0]) === path.resolve(copie)) {
|
|
419
|
+
return {
|
|
420
|
+
refus: `La copie ${copie} est l'arbre principal : un agent travaille dans sa propre copie.`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const parentRel = path.dirname(composant);
|
|
424
|
+
const nom = path.basename(composant);
|
|
425
|
+
// A folder of the root (`scripts`) works on the whole repository: no neighbour to hide.
|
|
426
|
+
const racine = parentRel === '.' || parentRel === '';
|
|
427
|
+
const propre = path.join(copie, composant);
|
|
428
|
+
const refusDuComposant = jugerComposant({ depot: copie, composant, paquets }, fs);
|
|
429
|
+
if (refusDuComposant) return { refus: refusDuComposant };
|
|
430
|
+
|
|
431
|
+
const caches = racine ? [] : trees.map(t => path.join(t, parentRel));
|
|
432
|
+
const matiere = matieres.map(m => path.join(copie, m));
|
|
433
|
+
for (const { lien, cible } of [propre, ...matiere].flatMap(d => fs.liens(d))) {
|
|
434
|
+
if (caches.some(c => inside(cible, c))) {
|
|
435
|
+
return {
|
|
436
|
+
refus: `Le lien ${lien} vise ${cible}, que l'enveloppe masque : il traverserait l'enveloppe.`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// /dev/shm, which /dev brings in writing, becomes an empty directory proper to the session.
|
|
442
|
+
const args = [
|
|
443
|
+
'--ro-bind',
|
|
444
|
+
'/',
|
|
445
|
+
'/',
|
|
446
|
+
'--dev-bind',
|
|
447
|
+
'/dev',
|
|
448
|
+
'/dev',
|
|
449
|
+
'--tmpfs',
|
|
450
|
+
'/dev/shm',
|
|
451
|
+
'--unshare-pid',
|
|
452
|
+
'--proc',
|
|
453
|
+
'/proc',
|
|
454
|
+
];
|
|
455
|
+
for (const { chemin, source = chemin, ecriture, vide, lien } of montages) {
|
|
456
|
+
if (vide) args.push('--tmpfs', chemin);
|
|
457
|
+
else if (lien !== undefined) args.push('--symlink', lien, chemin);
|
|
458
|
+
else if (fs.existe(source)) args.push(ecriture ? '--bind' : '--ro-bind', source, chemin);
|
|
459
|
+
}
|
|
460
|
+
// A worktree that an emptied directory covers comes back read-only, before the copy is written.
|
|
461
|
+
const videes = montages.filter(m => m.vide).map(m => m.chemin);
|
|
462
|
+
for (const t of trees) {
|
|
463
|
+
if (videes.some(v => inside(t, v)) && fs.existe(t)) args.push('--ro-bind', t, t);
|
|
464
|
+
}
|
|
465
|
+
// The copy is mounted after them: an emptied /tmp does not hide it. Its .git stays read-only.
|
|
466
|
+
// For a component that is a package, the copy is read-only and its package alone comes back in
|
|
467
|
+
// writing below, as a sandbox of Bazel opens only what the action declares; where a run of
|
|
468
|
+
// Turborepo writes at the root is an empty directory proper to the session.
|
|
469
|
+
const paquet = !racine && estUnPaquet(copie, composant, paquets);
|
|
470
|
+
args.push(paquet ? '--ro-bind' : '--bind', copie, copie);
|
|
471
|
+
if (paquet) for (const s of sortiesDeLaRacine(copie)) if (fs.existe(s)) args.push('--tmpfs', s);
|
|
472
|
+
const gitDeLaCopie = path.join(copie, '.git');
|
|
473
|
+
if (fs.existe(gitDeLaCopie)) args.push('--ro-bind', gitDeLaCopie, gitDeLaCopie);
|
|
474
|
+
// The task cache (Turborepo's) is written by the agent's runs, as by every copy's.
|
|
475
|
+
for (const m of magasins) if (fs.existe(m)) args.push('--bind', m, m);
|
|
476
|
+
const tickets = trees.length > 0 ? path.join(trees[0], '.beads') : null;
|
|
477
|
+
if (tickets && fs.existe(tickets)) args.push('--bind', tickets, tickets);
|
|
478
|
+
// Every index directory of every worktree, the copy's included, becomes an empty directory in
|
|
479
|
+
// its place; the worktree around it keeps its mount.
|
|
480
|
+
const index = [...new Set([...trees, copie])]
|
|
481
|
+
.flatMap(t => INDEX.map(i => path.join(t, i)))
|
|
482
|
+
.filter(i => fs.existe(i));
|
|
483
|
+
for (const i of index) args.push('--tmpfs', i);
|
|
484
|
+
for (const c of caches) if (fs.existe(c)) args.push('--tmpfs', c);
|
|
485
|
+
const parent = path.join(copie, parentRel);
|
|
486
|
+
for (const e of racine ? [] : fs.lister(parent)) {
|
|
487
|
+
const p = path.join(parent, e.name);
|
|
488
|
+
if (!e.dir) {
|
|
489
|
+
args.push('--ro-bind', p, p);
|
|
490
|
+
} else if (e.name !== nom && !matiere.includes(p)) {
|
|
491
|
+
for (const part of new Set([...PUBLISHED, ...fs.publies(p, copie)])) {
|
|
492
|
+
const q = path.join(p, part);
|
|
493
|
+
if (inside(q, p) && fs.existe(q)) args.push('--ro-bind', q, q);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// What the component's tests declare they read, in a sibling the envelope hides.
|
|
498
|
+
if (!racine) {
|
|
499
|
+
for (const l of new Set(fs.lus?.(propre) ?? [])) {
|
|
500
|
+
const q = path.join(copie, l);
|
|
501
|
+
const visible = inside(q, propre) || matiere.some(m => inside(q, m));
|
|
502
|
+
if (inside(q, parent) && q !== parent && !visible && fs.existe(q)) {
|
|
503
|
+
args.push('--ro-bind', q, q);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
args.push('--bind', propre, propre);
|
|
508
|
+
for (const m of matiere) {
|
|
509
|
+
args.push(role === ROLE_MATIERE ? '--bind' : '--ro-bind', m, m);
|
|
510
|
+
// What a run of its tests writes goes, for a role that does not write it, to an empty
|
|
511
|
+
// directory proper to the session, as Bazel writes a test's logs out of its source.
|
|
512
|
+
if (role === ROLE_MATIERE) continue;
|
|
513
|
+
for (const s of sortiesDeTest(m)) if (fs.existe(s)) args.push('--tmpfs', s);
|
|
514
|
+
}
|
|
515
|
+
// The emptied directories close behind the mounts: nothing new is written there.
|
|
516
|
+
for (const c of caches) if (fs.existe(c)) args.push('--remount-ro', c);
|
|
517
|
+
for (const { chemin } of montages.filter(m => m.scelle)) args.push('--remount-ro', chemin);
|
|
518
|
+
for (const i of index) args.push('--remount-ro', i);
|
|
519
|
+
args.push('--chdir', copie);
|
|
520
|
+
return { args };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const disque = {
|
|
524
|
+
lister,
|
|
525
|
+
existe: existsSync,
|
|
526
|
+
liens: d => (existsSync(d) && statSync(d).isDirectory() ? liensSortants(d) : []),
|
|
527
|
+
publies: fichiersPublies,
|
|
528
|
+
lus: cheminsLus,
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// The command: the session of <commande> in the envelope of <composant> in <copie>; its exit code.
|
|
532
|
+
function envelopper(argv) {
|
|
533
|
+
const sep = argv.indexOf('--');
|
|
534
|
+
const [copieArg, composant, role] = argv.slice(0, sep < 0 ? undefined : sep);
|
|
535
|
+
const commande = sep < 0 ? [] : argv.slice(sep + 1);
|
|
536
|
+
if (!copieArg || !composant || !role || commande.length === 0) {
|
|
537
|
+
process.stderr.write('usage: enveloppe.mjs <copie> <composant> <rôle> -- <commande…>\n');
|
|
538
|
+
return CODE_USAGE;
|
|
539
|
+
}
|
|
540
|
+
const copie = path.resolve(copieArg);
|
|
541
|
+
// A launch from a package of a repository of workspaces sets npm's `workspace`, which
|
|
542
|
+
// `npm config` refuses: the cache is the user's, whatever the workspace.
|
|
543
|
+
const cacheNpm = execFileSync('npm', ['config', 'get', 'cache', '--workspaces=false'], {
|
|
544
|
+
encoding: 'utf8',
|
|
545
|
+
}).trim();
|
|
546
|
+
const claude = path.join(os.homedir(), '.claude');
|
|
547
|
+
// A session's project directory carries its working directory, every sign outside
|
|
548
|
+
// [A-Za-z0-9] turned into a dash; it is created here so that bwrap mounts it in writing.
|
|
549
|
+
const enTirets = copie.replace(/[^A-Za-z0-9]/g, '-');
|
|
550
|
+
const projet = path.join(claude, 'projects', enTirets);
|
|
551
|
+
mkdirSync(projet, { recursive: true });
|
|
552
|
+
const scratchpads = scratchpadsDe(copie);
|
|
553
|
+
// The session's working directory, created here inside the scratchpads that bwrap mounts.
|
|
554
|
+
const travail = dossierDeTravail(copie);
|
|
555
|
+
mkdirSync(travail, { recursive: true });
|
|
556
|
+
// `~/.claude.json` holds the permissions and servers of every project: it enters the session
|
|
557
|
+
// as a throwaway copy, in a base of its own removed on exit and swept when the envelope is
|
|
558
|
+
// killed (../vitest-config/base-de-passe.mjs).
|
|
559
|
+
const temporaire = ouvrirBase('enveloppe');
|
|
560
|
+
const etatJetable = path.join(temporaire.dossier, 'claude.json');
|
|
561
|
+
if (existsSync(`${claude}.json`)) copyFileSync(`${claude}.json`, etatJetable);
|
|
562
|
+
// The session directories of this user that exist: the XDG one and the systemd one.
|
|
563
|
+
const sessions = [
|
|
564
|
+
...new Set(
|
|
565
|
+
[process.env.XDG_RUNTIME_DIR, `/run/user/${process.getuid()}`]
|
|
566
|
+
.filter(d => d && existsSync(d))
|
|
567
|
+
.map(d => path.resolve(d))
|
|
568
|
+
),
|
|
569
|
+
];
|
|
570
|
+
const montages = avecLiens(
|
|
571
|
+
montagesDeLaSeance({ cacheNpm, claude, projet, etatJetable, scratchpads, sessions }),
|
|
572
|
+
lienSurDisque
|
|
573
|
+
);
|
|
574
|
+
const matieres = lesMatieres(copie).map(m => path.relative(copie, m.dir));
|
|
575
|
+
// For a role that does not write it, the directories where a run of the material's tests writes
|
|
576
|
+
// are created here, so that bwrap mounts an empty one over each inside the read-only package.
|
|
577
|
+
if (role !== ROLE_MATIERE) {
|
|
578
|
+
for (const m of matieres) {
|
|
579
|
+
for (const s of sortiesDeTest(path.join(copie, m))) mkdirSync(s, { recursive: true });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// For a component that is a package, where a run of Turborepo writes at the root of the copy is
|
|
583
|
+
// created here, so that bwrap mounts an empty one over it inside the read-only copy.
|
|
584
|
+
const paquets = lesPaquets(copie);
|
|
585
|
+
if (estUnPaquet(copie, composant, paquets)) {
|
|
586
|
+
for (const s of sortiesDeLaRacine(copie)) mkdirSync(s, { recursive: true });
|
|
587
|
+
}
|
|
588
|
+
// The cache of Turborepo, in the main tree (the folder of git's common directory), created here
|
|
589
|
+
// so that bwrap mounts it in writing.
|
|
590
|
+
const commun = execFileSync(
|
|
591
|
+
'git',
|
|
592
|
+
['-C', copie, 'rev-parse', '--path-format=absolute', '--git-common-dir'],
|
|
593
|
+
{ encoding: 'utf8' }
|
|
594
|
+
).trim();
|
|
595
|
+
const magasins = [path.join(path.dirname(commun), '.turbo', 'cache')];
|
|
596
|
+
for (const m of magasins) mkdirSync(m, { recursive: true });
|
|
597
|
+
const { args, refus } = plan(
|
|
598
|
+
{
|
|
599
|
+
trees: worktrees(copie),
|
|
600
|
+
copie,
|
|
601
|
+
composant,
|
|
602
|
+
role,
|
|
603
|
+
matieres,
|
|
604
|
+
montages,
|
|
605
|
+
magasins,
|
|
606
|
+
paquets,
|
|
607
|
+
},
|
|
608
|
+
disque
|
|
609
|
+
);
|
|
610
|
+
let code;
|
|
611
|
+
if (refus) {
|
|
612
|
+
process.stderr.write(`⛔ ENVELOPPE REFUSÉE — ${refus}\n`);
|
|
613
|
+
code = CODE_REFUS;
|
|
614
|
+
} else {
|
|
615
|
+
const r = spawnSync('bwrap', [...args, ...commandeEprouvee(commande)], {
|
|
616
|
+
stdio: 'inherit',
|
|
617
|
+
env: environnementDeLaSeance(process.env, { cacheNpm, composant, travail }),
|
|
618
|
+
});
|
|
619
|
+
if (r.error) process.stderr.write(`⛔ ENVELOPPE IMPOSSIBLE — ${r.error.message}\n`);
|
|
620
|
+
code = r.error ? CODE_REFUS : (r.status ?? 1);
|
|
621
|
+
}
|
|
622
|
+
temporaire.fermer();
|
|
623
|
+
return code;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
627
|
+
process.exitCode = envelopper(process.argv.slice(2));
|
|
628
|
+
}
|