create-byan-agent 2.59.2 → 2.60.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/install/bin/create-byan-agent-v2.js +94 -44
- package/install/lib/api-defaults.js +111 -0
- package/install/lib/byan-leantime-integration.js +1 -1
- package/install/lib/byan-web-integration.js +10 -3
- package/install/lib/claude-native-setup.js +1 -1
- package/install/lib/codex-native-setup.js +55 -20
- package/install/lib/gdoc-setup.js +1 -1
- package/install/lib/install-engine.js +490 -58
- package/install/lib/mcp-extensions/index.js +1 -1
- package/install/lib/native-helper.js +13 -2
- package/install/lib/ownership.js +489 -0
- package/install/lib/platforms/claude-code.js +8 -4
- package/install/lib/resolve-binary.js +398 -0
- package/install/lib/rtk-integration.js +9 -2
- package/install/lib/stt/engine.js +5 -6
- package/install/lib/target-user.js +373 -0
- package/install/lib/yanstaller/agent-launcher.js +7 -16
- package/install/package.json +1 -1
- package/install/packages/platform-config/lib/token-prompt.js +7 -1
- package/install/src/webui/api.js +14 -1
- package/install/src/webui/public/app.js +39 -0
- package/package.json +1 -2
- package/update-byan-agent/lib/migrate-mcp-config.js +1 -1
- package/update-byan-agent/package.json +0 -1
|
@@ -32,6 +32,10 @@ const { setupClaudeNative } = require('./claude-native-setup');
|
|
|
32
32
|
const { setupCodexNative } = require('./codex-native-setup');
|
|
33
33
|
const { offerGlobalSkillsSync } = require('./global-skills-sync');
|
|
34
34
|
const homeCreds = require('./home-credentials');
|
|
35
|
+
const { resolveTargetUser, resolveHomeFor } = require('./target-user');
|
|
36
|
+
const { resolveBinary, commandExists: resolveCommandExists } = require('./resolve-binary');
|
|
37
|
+
const { ensureOwnership, ensureSharedGroup, resolveGroupGid } = require('./ownership');
|
|
38
|
+
const { resolveApiUrl } = require('./api-defaults');
|
|
35
39
|
|
|
36
40
|
// Gen3 by-type dirs copied from templates/_byan (same list as the legacy bin).
|
|
37
41
|
const BYAN_DIRS = ['agent', 'workflow', 'connaissance', 'command', 'worker', 'memoire',
|
|
@@ -42,22 +46,53 @@ function defaultTemplateDir() {
|
|
|
42
46
|
return fs.existsSync(p) ? p : null;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// L'ancienne sonde lancait execSync('command -v X', { shell: '/bin/sh' }).
|
|
50
|
+
// Trois defauts mesures le 2026-08-11 : /bin/sh est absent de Windows ; avec le
|
|
51
|
+
// PATH POSIX par defaut (getconf PATH) la commande sort en 1 alors que claude
|
|
52
|
+
// EST installe, parce qu'il vit dans ~/.local/bin qui n'appartient a aucun PATH
|
|
53
|
+
// par defaut ; et sous elevation de privilege le PATH est celui de root.
|
|
54
|
+
// La sonde passe desormais par resolve-binary : parcours du PATH en JavaScript,
|
|
55
|
+
// puis emplacements connus du home de l'utilisateur CIBLE.
|
|
56
|
+
function commandExists(cmd, options = {}) {
|
|
57
|
+
return resolveCommandExists(cmd, options);
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Detection: platforms on this machine + stored credentials. Facts only.
|
|
62
|
+
*
|
|
63
|
+
* Le home interroge est celui de l'utilisateur CIBLE, pas celui du processus.
|
|
64
|
+
* Sous sudo, os.homedir() rend /root : l'ancienne version cherchait ~/.claude
|
|
65
|
+
* dans /root, ne trouvait rien, et l'installateur sautait l'etape Claude en
|
|
66
|
+
* annoncant une reussite.
|
|
67
|
+
*/
|
|
68
|
+
function detectEnvironment({
|
|
69
|
+
homeDir,
|
|
70
|
+
env = process.env,
|
|
71
|
+
platform = process.platform,
|
|
72
|
+
target = null,
|
|
73
|
+
} = {}) {
|
|
74
|
+
const cible = target || resolveTargetUser({ env, platform });
|
|
75
|
+
const home = homeDir || cible.home || os.homedir();
|
|
76
|
+
const sonde = (nom) => resolveBinary(nom, { env, platform, home });
|
|
77
|
+
const claudeBin = sonde('claude');
|
|
78
|
+
const codexBin = sonde('codex');
|
|
79
|
+
const rtkBin = sonde('rtk');
|
|
56
80
|
return {
|
|
57
|
-
claude: fs.existsSync(path.join(
|
|
58
|
-
codex: fs.existsSync(path.join(
|
|
59
|
-
rtk:
|
|
60
|
-
storedCredentialKeys: homeCreds.storedKeys({ homeDir }),
|
|
81
|
+
claude: fs.existsSync(path.join(home, '.claude')) || Boolean(claudeBin.path),
|
|
82
|
+
codex: fs.existsSync(path.join(home, '.codex')) || Boolean(codexBin.path),
|
|
83
|
+
rtk: Boolean(rtkBin.path),
|
|
84
|
+
storedCredentialKeys: homeCreds.storedKeys({ homeDir: home }),
|
|
85
|
+
// De quoi rendre un rapport honnete : QUI on a pris pour cible, OU on a
|
|
86
|
+
// cherche, et ce qu'on a trouve. Sans ca, "Claude Code introuvable" est une
|
|
87
|
+
// affirmation que personne ne peut verifier.
|
|
88
|
+
targetUser: cible,
|
|
89
|
+
searched: {
|
|
90
|
+
home,
|
|
91
|
+
homeSource: cible.homeSource,
|
|
92
|
+
claude: { path: claudeBin.path, realPath: claudeBin.realPath, dirs: claudeBin.searched },
|
|
93
|
+
codex: { path: codexBin.path, dirs: codexBin.searched },
|
|
94
|
+
rtk: { path: rtkBin.path, dirs: rtkBin.searched },
|
|
95
|
+
},
|
|
61
96
|
};
|
|
62
97
|
}
|
|
63
98
|
|
|
@@ -66,12 +101,17 @@ function detectEnvironment({ homeDir = os.homedir() } = {}) {
|
|
|
66
101
|
* Claude Code research-preview feature behind an explicit flag; when the
|
|
67
102
|
* installed CLI does not know the flag, the plain `claude` command is the
|
|
68
103
|
* fallback. We only BUILD the command here — executing it is the caller's move.
|
|
104
|
+
*
|
|
105
|
+
* Les deux sondes sont injectables parce que la version par defaut LANCE
|
|
106
|
+
* reellement `claude --help` avec un delai de 8 s. Mesure du 2026-08-11 : cet
|
|
107
|
+
* appel partait a chaque runInstall, tests compris, ce qui faisait dependre la
|
|
108
|
+
* suite de tests de ce que porte la machine hote.
|
|
69
109
|
*/
|
|
70
|
-
function claudeLaunchCommand() {
|
|
71
|
-
if (!
|
|
110
|
+
function claudeLaunchCommand({ exists = commandExists, run = execSync } = {}) {
|
|
111
|
+
if (!exists('claude')) return null;
|
|
72
112
|
let helpText = '';
|
|
73
113
|
try {
|
|
74
|
-
helpText =
|
|
114
|
+
helpText = run('claude --help', { encoding: 'utf8', timeout: 8000 });
|
|
75
115
|
} catch {
|
|
76
116
|
return { command: 'claude', channel: false };
|
|
77
117
|
}
|
|
@@ -81,6 +121,125 @@ function claudeLaunchCommand() {
|
|
|
81
121
|
: { command: 'claude', channel: false };
|
|
82
122
|
}
|
|
83
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Le premier dossier qui existe en remontant depuis le chemin donne.
|
|
126
|
+
*
|
|
127
|
+
* Le dossier d'installation peut ne pas exister encore : c'est alors son parent
|
|
128
|
+
* qui porte l'identite a lire. Remonte jusqu'a la racine, et retombe sur le
|
|
129
|
+
* dossier courant si le chemin est absent ou vide.
|
|
130
|
+
*/
|
|
131
|
+
function plusProcheAncetreExistant(depart) {
|
|
132
|
+
if (!depart) return process.cwd();
|
|
133
|
+
let courant = path.resolve(depart);
|
|
134
|
+
for (;;) {
|
|
135
|
+
if (fs.existsSync(courant)) return courant;
|
|
136
|
+
const parent = path.dirname(courant);
|
|
137
|
+
if (parent === courant) return process.cwd();
|
|
138
|
+
courant = parent;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* --owner=<nom|uid> : l'appelant designe la cible a la place de la detection.
|
|
144
|
+
*
|
|
145
|
+
* `elevated` se lit sur l'uid effectif, comme dans resolveTargetUser. Le poser
|
|
146
|
+
* en dur a true faisait croire a une elevation qui n'existait pas : la reprise
|
|
147
|
+
* partait, se faisait refuser, et le rapport parlait de montage sans
|
|
148
|
+
* proprietaire POSIX au lieu de privilege manquant.
|
|
149
|
+
*/
|
|
150
|
+
function forcedTarget(owner) {
|
|
151
|
+
const details = resolveHomeFor(owner);
|
|
152
|
+
const eleve = typeof process.geteuid === 'function' && process.geteuid() === 0;
|
|
153
|
+
return { ...details, source: 'option', elevated: eleve };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Lecture tolerante d'un YAML : un fichier absent ou abime rend un objet vide. */
|
|
157
|
+
function readYamlSafe(p) {
|
|
158
|
+
try {
|
|
159
|
+
const brut = fs.readFileSync(p, 'utf8');
|
|
160
|
+
const charge = yaml.load(brut);
|
|
161
|
+
return charge && typeof charge === 'object' ? charge : {};
|
|
162
|
+
} catch {
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Le dossier d'installation designe-t-il une installation deliberee en root ?
|
|
169
|
+
*
|
|
170
|
+
* LA QUESTION PORTE SUR LE DOSSIER, PAS SUR L'ECHELON DE RESOLUTION. La version
|
|
171
|
+
* precedente testait `cible.source === 'root'`, un echelon qui n'est atteint que
|
|
172
|
+
* lorsque AUCUNE variable d'elevation n'est posee. Sous un simple sudo, la
|
|
173
|
+
* source vaut 'sudo' et la protection ne se declenchait pas : installer dans
|
|
174
|
+
* /opt avec sudo remettait l'arborescence a l'utilisateur appelant et cassait un
|
|
175
|
+
* montage qui fonctionnait. Le critere reel est le proprietaire du dossier vise
|
|
176
|
+
* et de son parent.
|
|
177
|
+
*/
|
|
178
|
+
function installationEnRootAssumee(projectRoot) {
|
|
179
|
+
const cible = plusProcheAncetreExistant(cheminResolu(projectRoot));
|
|
180
|
+
const st = statSafe(cible);
|
|
181
|
+
if (!st || st.uid !== 0) return false;
|
|
182
|
+
const parent = path.dirname(cible);
|
|
183
|
+
if (parent === cible) return true;
|
|
184
|
+
const stParent = statSafe(parent);
|
|
185
|
+
return Boolean(stParent && stParent.uid === 0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function statSafe(p) {
|
|
189
|
+
try {
|
|
190
|
+
return fs.lstatSync(p);
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Le chemin d'installation, liens symboliques resolus.
|
|
198
|
+
*
|
|
199
|
+
* POURQUOI : `--dir /srv/byan` ou /srv/byan est un lien vers /mnt/data/byan.
|
|
200
|
+
* Les etapes d'ecriture suivent le lien (fs.copy et consorts le font), mais la
|
|
201
|
+
* reprise des droits lisait le lien lui-meme : lstat d'un lien rend
|
|
202
|
+
* isDirectory() faux, donc le parcours ne demarrait pas et l'arborescence reelle
|
|
203
|
+
* gardait son proprietaire. Meme cecite pour la garde d'installation en root.
|
|
204
|
+
* On resout donc une fois, a l'entree, et tout le monde vise la meme chose.
|
|
205
|
+
*/
|
|
206
|
+
function cheminResolu(depart) {
|
|
207
|
+
const ancetre = plusProcheAncetreExistant(depart);
|
|
208
|
+
let reel = ancetre;
|
|
209
|
+
try {
|
|
210
|
+
reel = fs.realpathSync(ancetre);
|
|
211
|
+
} catch {
|
|
212
|
+
reel = ancetre;
|
|
213
|
+
}
|
|
214
|
+
const vise = path.resolve(depart || process.cwd());
|
|
215
|
+
if (vise === ancetre) return reel;
|
|
216
|
+
return path.join(reel, path.relative(ancetre, vise));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Faut-il reprendre les droits, et sinon pourquoi ? Chaque abstention porte une
|
|
221
|
+
* raison distincte, parce qu'un message faux coute plus cher qu'un message
|
|
222
|
+
* absent : dire "ce systeme ne porte pas de proprietaire POSIX" a quelqu'un qui
|
|
223
|
+
* a juste tape un nom d'utilisateur inexistant l'envoie chercher au mauvais
|
|
224
|
+
* endroit.
|
|
225
|
+
*/
|
|
226
|
+
function raisonReprise({ chown, cible, groupePartage, rootAssume }) {
|
|
227
|
+
if (chown === false) return 'ecarte par l\'option --no-chown';
|
|
228
|
+
if (cible.uid === null || cible.uid === undefined) {
|
|
229
|
+
return cible.name
|
|
230
|
+
? `l'utilisateur ${cible.name} est inconnu de ce systeme : aucun identifiant a poser`
|
|
231
|
+
: 'ce systeme ne porte pas de proprietaire POSIX';
|
|
232
|
+
}
|
|
233
|
+
// Cette reprise existe pour SORTIR une arborescence de root. La rendre A root
|
|
234
|
+
// est le geste inverse. Mesure du 2026-08-11 : avec SUDO_UID=0 dans
|
|
235
|
+
// l'environnement, la cible ressortait a uid 0 et l'arborescence de
|
|
236
|
+
// l'utilisateur passait en root:root — exactement la panne a corriger.
|
|
237
|
+
if (cible.uid === 0) return 'la cible resolue est root : cette reprise sert a sortir une arborescence de root, pas a l\'y mettre';
|
|
238
|
+
if (rootAssume) return 'installation en root assumee : l\'arborescence est laissee telle quelle';
|
|
239
|
+
if (!cible.elevated && !groupePartage) return 'aucune elevation : les fichiers appartiennent deja a l\'utilisateur';
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
84
243
|
/**
|
|
85
244
|
* Run the full installation.
|
|
86
245
|
*
|
|
@@ -103,6 +262,17 @@ function claudeLaunchCommand() {
|
|
|
103
262
|
* @returns {Promise<{ok:boolean, steps:Array, verify:{passed:number,total:number,failed:string[]}, launch:{command:string,channel:boolean}|null}>}
|
|
104
263
|
*/
|
|
105
264
|
async function runInstall(options = {}, hooks = {}) {
|
|
265
|
+
// L'utilisateur CIBLE se resout avant tout le reste : c'est lui qui donne le
|
|
266
|
+
// home a interroger et le proprietaire a reposer. --owner le force a la main.
|
|
267
|
+
//
|
|
268
|
+
// Le dossier interroge est celui ou on INSTALLE, pas le dossier courant.
|
|
269
|
+
// `create-byan-agent --cli --dir /opt/byan` lance depuis le home donnait
|
|
270
|
+
// sinon le proprietaire du home, et la regle qui protege une installation
|
|
271
|
+
// deliberee en root ne se declenchait pas sur la bonne arborescence.
|
|
272
|
+
const cible = options.targetUser
|
|
273
|
+
|| (options.owner ? forcedTarget(options.owner)
|
|
274
|
+
: resolveTargetUser({ cwd: plusProcheAncetreExistant(options.projectRoot) }));
|
|
275
|
+
|
|
106
276
|
const {
|
|
107
277
|
projectRoot,
|
|
108
278
|
projectName = path.basename(options.projectRoot || ''),
|
|
@@ -111,8 +281,16 @@ async function runInstall(options = {}, hooks = {}) {
|
|
|
111
281
|
language = 'Francais',
|
|
112
282
|
rtk = true,
|
|
113
283
|
credentials = null,
|
|
114
|
-
homeDir = os.homedir(),
|
|
284
|
+
homeDir = cible.home || os.homedir(),
|
|
285
|
+
chown = true,
|
|
286
|
+
group = null,
|
|
287
|
+
apiUrl = null,
|
|
115
288
|
} = options;
|
|
289
|
+
|
|
290
|
+
// Une seule source pour l'URL de l'API, resolue ICI et transmise aux etapes :
|
|
291
|
+
// explicite (--api-url, une reponse a --ask), puis BYAN_API_URL, puis la
|
|
292
|
+
// valeur memorisee d'une installation precedente, puis le host de production.
|
|
293
|
+
const api = resolveApiUrl({ explicit: apiUrl, homeDir });
|
|
116
294
|
const onStep = hooks.onStep || (() => {});
|
|
117
295
|
const log = hooks.log || (() => {});
|
|
118
296
|
const claudeSetup = hooks.claudeSetup || setupClaudeNative;
|
|
@@ -124,40 +302,115 @@ async function runInstall(options = {}, hooks = {}) {
|
|
|
124
302
|
}
|
|
125
303
|
|
|
126
304
|
// Detection is injectable so tests do not depend on what THIS machine has.
|
|
127
|
-
const detected = hooks.detect ? hooks.detect() : detectEnvironment({ homeDir });
|
|
305
|
+
const detected = hooks.detect ? hooks.detect() : detectEnvironment({ homeDir, target: cible });
|
|
128
306
|
const platforms = options.platforms || { claude: detected.claude, codex: detected.codex };
|
|
129
307
|
const byanDir = path.join(projectRoot, '_byan');
|
|
130
|
-
const results = [];
|
|
131
308
|
|
|
132
|
-
//
|
|
133
|
-
|
|
309
|
+
// Le groupe partage survit a une reinstallation : --group le pose, et une
|
|
310
|
+
// installation ulterieure sans l'option le relit dans la configuration au lieu
|
|
311
|
+
// de l'effacer. L'ancienne etape config reecrivait config.yaml integralement
|
|
312
|
+
// depuis un objet fixe, ce qui aurait supprime la cle a chaque passage.
|
|
313
|
+
const configPath = path.join(byanDir, 'bmb', 'config.yaml');
|
|
314
|
+
const configExistante = readYamlSafe(configPath);
|
|
315
|
+
const groupePartage = group || configExistante.shared_group || null;
|
|
316
|
+
|
|
317
|
+
// Calculee UNE fois : le plan et la garde de l'etape lisaient chacun leur
|
|
318
|
+
// propre appel, et deux calculs d'une meme decision finissent par diverger.
|
|
319
|
+
const raisonDroits = raisonReprise({
|
|
320
|
+
chown,
|
|
321
|
+
cible,
|
|
322
|
+
groupePartage,
|
|
323
|
+
rootAssume: installationEnRootAssumee(projectRoot),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// LE HOME DE LA CIBLE EST-IL REELLEMENT RESOLU ?
|
|
327
|
+
//
|
|
328
|
+
// Quand il ne l'est pas — un uid absent de /etc/passwd, cas courant sous
|
|
329
|
+
// pkexec ou su avec un annuaire LDAP ou SSSD — homeDir retombe sur le home du
|
|
330
|
+
// PROCESSUS, c'est-a-dire /root sous elevation. Toutes les etapes qui ecrivent
|
|
331
|
+
// hors du projet partaient alors dans /root : le jeton d'API dans
|
|
332
|
+
// /root/.byan/credentials.json, la configuration Codex dans /root/.codex, les
|
|
333
|
+
// skills globales dans /root/.claude. Le rapport rendait ok:true.
|
|
334
|
+
//
|
|
335
|
+
// On ECARTE ces etapes plutot que de les rediriger. Un fichier de secrets pose
|
|
336
|
+
// dans le home de root n'est pas un demi-succes : c'est un fichier au mauvais
|
|
337
|
+
// endroit, que la reprise des droits ne peut plus rattraper puisqu'elle passe
|
|
338
|
+
// apres. Constat bloquant de la revue du 2026-08-12, reproduit.
|
|
339
|
+
const homeResolu = Boolean(cible.home);
|
|
340
|
+
const homeDouteux = !homeResolu && cible.elevated;
|
|
341
|
+
const raisonHome = homeDouteux
|
|
342
|
+
? 'home de l\'utilisateur cible non resolu sous elevation : ecrire ici poserait le fichier dans /root'
|
|
343
|
+
: null;
|
|
344
|
+
|
|
345
|
+
// Le gid du groupe partage est resolu AVANT la reprise du proprietaire.
|
|
346
|
+
// Sinon ensureOwnership reposait cible.gid partout, et ensureSharedGroup ne
|
|
347
|
+
// rattrapait ensuite que la racine : le groupe partage ne portait que sur un
|
|
348
|
+
// seul dossier, ce qui ne donne acces a rien.
|
|
349
|
+
let gidGroupe = null;
|
|
350
|
+
if (groupePartage) {
|
|
351
|
+
const resolu = (hooks.resolveGroupGid || resolveGroupGid)(groupePartage);
|
|
352
|
+
gidGroupe = typeof resolu.gid === 'number' ? resolu.gid : null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// LE PLAN PORTE L'ENSEMBLE INTENDU, pas seulement ce qui va tourner.
|
|
356
|
+
//
|
|
357
|
+
// L'ancien plan omettait purement les etapes non retenues. L'omission etait
|
|
358
|
+
// invisible dans le rapport : `results.every(...)` interrogeait une liste qui
|
|
359
|
+
// ne contenait plus l'etape, donc un plan ampute par une detection fausse
|
|
360
|
+
// repondait quand meme "installation reussie" (mesure du 2026-08-11 sur
|
|
361
|
+
// install-engine.js:285). Le denominateur retrecissait avec le plan.
|
|
362
|
+
//
|
|
363
|
+
// Desormais chaque etape possible figure ici avec, le cas echeant, la RAISON
|
|
364
|
+
// pour laquelle elle est ecartee. Une etape sautee reste dans le rapport.
|
|
365
|
+
const aDesCredentials = Boolean(credentials && Object.keys(credentials).length);
|
|
366
|
+
const intended = [
|
|
134
367
|
{ id: 'detect', label: 'Detection de l\'environnement' },
|
|
135
368
|
{ id: 'copy-byan', label: 'Copie de la plateforme _byan/' },
|
|
136
369
|
{ id: 'config', label: 'Ecriture de la configuration du projet' },
|
|
370
|
+
{ id: 'claude', label: 'Installation Claude Code (.claude + serveur MCP + .mcp.json)',
|
|
371
|
+
skipReason: platforms.claude ? null : 'Claude Code non detecte sur cette machine' },
|
|
372
|
+
{ id: 'codex', label: 'Installation Codex (~/.codex)',
|
|
373
|
+
skipReason: raisonHome || (platforms.codex ? null : 'Codex non detecte sur cette machine') },
|
|
374
|
+
{ id: 'credentials', label: 'Memorisation de la configuration (~/.byan/credentials.json)',
|
|
375
|
+
skipReason: raisonHome || (aDesCredentials ? null : 'aucune valeur a memoriser') },
|
|
376
|
+
{ id: 'google-purge', label: 'Retrait des cles Google obsoletes',
|
|
377
|
+
skipReason: raisonHome },
|
|
378
|
+
{ id: 'skills-sync', label: 'Controle des copies globales de skills',
|
|
379
|
+
skipReason: raisonHome || (platforms.claude ? null : 'sans Claude Code, il n\'y a pas de copie globale a controler') },
|
|
380
|
+
{ id: 'rtk', label: 'Verification / installation de rtk',
|
|
381
|
+
skipReason: rtk ? null : 'ecarte par l\'option rtk=false' },
|
|
382
|
+
// APRES le dernier sous-processus, et c'est une contrainte, pas un detail.
|
|
383
|
+
// npm install (serveur MCP), git config core.hooksPath et setup-rtk creent
|
|
384
|
+
// des fichiers qu'aucun crochet pose au niveau de fs ne peut voir. Une
|
|
385
|
+
// reprise placee plus tot laisserait node_modules en root:root — le dossier
|
|
386
|
+
// le plus lourd et le plus penible a reparer a la main.
|
|
387
|
+
{ id: 'ownership', label: 'Reprise du proprietaire et des droits',
|
|
388
|
+
skipReason: raisonDroits },
|
|
389
|
+
{ id: 'verify', label: 'Verification finale' },
|
|
137
390
|
];
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
391
|
+
const active = intended.filter((s) => !s.skipReason);
|
|
392
|
+
|
|
393
|
+
// Un etat par etape INTENDUE. Les sautees naissent deja renseignees, avec leur
|
|
394
|
+
// raison ; les autres passent de 'pending' a 'done' ou 'failed'.
|
|
395
|
+
const etats = new Map(intended.map((s) => [s.id, s.skipReason
|
|
396
|
+
? { id: s.id, ok: true, status: 'skipped', reason: s.skipReason, detail: '' }
|
|
397
|
+
: { id: s.id, ok: false, status: 'pending', reason: null, detail: '' }]));
|
|
145
398
|
|
|
146
399
|
let index = 0;
|
|
147
400
|
const step = async (id, label, fn, { critical = true } = {}) => {
|
|
148
401
|
index += 1;
|
|
149
|
-
onStep({ index, total:
|
|
402
|
+
onStep({ index, total: active.length, id, label });
|
|
150
403
|
try {
|
|
151
404
|
const detail = await fn();
|
|
152
|
-
|
|
405
|
+
etats.set(id, { id, ok: true, status: 'done', reason: null, detail: detail || '' });
|
|
153
406
|
} catch (err) {
|
|
154
|
-
|
|
407
|
+
etats.set(id, { id, ok: false, status: 'failed', reason: null, detail: err.message });
|
|
155
408
|
if (critical) throw err;
|
|
156
409
|
log(`[!] etape ${id} en echec (non bloquant) : ${err.message}`);
|
|
157
410
|
}
|
|
158
411
|
};
|
|
159
412
|
|
|
160
|
-
await step('detect',
|
|
413
|
+
await step('detect', intended[0].label, async () => {
|
|
161
414
|
log(`Claude: ${detected.claude ? 'present' : 'absent'} ; Codex: ${detected.codex ? 'present' : 'absent'} ; rtk: ${detected.rtk ? 'present' : 'absent'}`);
|
|
162
415
|
if (detected.storedCredentialKeys.length) {
|
|
163
416
|
log(`Configuration memorisee reutilisee (${detected.storedCredentialKeys.length} cle(s), rien a re-saisir)`);
|
|
@@ -186,7 +439,10 @@ async function runInstall(options = {}, hooks = {}) {
|
|
|
186
439
|
await step('config', 'Ecriture de la configuration du projet', async () => {
|
|
187
440
|
const bmbDir = path.join(byanDir, 'bmb');
|
|
188
441
|
await fs.ensureDir(bmbDir);
|
|
442
|
+
// FUSION, pas reecriture. Les cles que cette version connait sont refaites ;
|
|
443
|
+
// celles posees ailleurs (shared_group, reglages d'un autre outil) survivent.
|
|
189
444
|
const configContent = {
|
|
445
|
+
...configExistante,
|
|
190
446
|
bmb_creations_output_folder: '{project-root}/_byan-output/bmb-creations',
|
|
191
447
|
user_name: userName,
|
|
192
448
|
communication_language: language,
|
|
@@ -197,8 +453,9 @@ async function runInstall(options = {}, hooks = {}) {
|
|
|
197
453
|
install_mode: 'engine-auto',
|
|
198
454
|
byan_version: readOwnVersion(),
|
|
199
455
|
};
|
|
200
|
-
|
|
201
|
-
|
|
456
|
+
if (groupePartage) configContent.shared_group = groupePartage;
|
|
457
|
+
await fs.writeFile(configPath, yaml.dump(configContent), 'utf8');
|
|
458
|
+
return groupePartage ? `config.yaml ecrit (groupe partage ${groupePartage})` : 'config.yaml ecrit';
|
|
202
459
|
});
|
|
203
460
|
|
|
204
461
|
if (platforms.claude) {
|
|
@@ -213,14 +470,16 @@ async function runInstall(options = {}, hooks = {}) {
|
|
|
213
470
|
});
|
|
214
471
|
}
|
|
215
472
|
|
|
216
|
-
if (platforms.codex) {
|
|
473
|
+
if (platforms.codex && !homeDouteux) {
|
|
217
474
|
await step('codex', 'Installation Codex', async () => {
|
|
218
|
-
|
|
475
|
+
// homeDir est celui de l'utilisateur CIBLE : sans lui, ~/.codex partait
|
|
476
|
+
// dans /root sous elevation.
|
|
477
|
+
await codexSetup(projectRoot, { templateDir, force: true, homeDir, apiUrl: api.url });
|
|
219
478
|
return 'squelettes .codex + config';
|
|
220
479
|
}, { critical: false });
|
|
221
480
|
}
|
|
222
481
|
|
|
223
|
-
if (
|
|
482
|
+
if (aDesCredentials && !homeDouteux) {
|
|
224
483
|
await step('credentials', 'Memorisation de la configuration', async () => {
|
|
225
484
|
const merged = homeCreds.writeCredentials(credentials, { homeDir });
|
|
226
485
|
return `${Object.keys(merged).length} cle(s) en memoire dans ${homeCreds.credentialsPath(homeDir)}`;
|
|
@@ -229,12 +488,14 @@ async function runInstall(options = {}, hooks = {}) {
|
|
|
229
488
|
|
|
230
489
|
// Chemin non-interactif : la purge des cles Google obsoletes tourne aussi
|
|
231
490
|
// sans passage par byan-web-integration (parite avec le flux interactif).
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
491
|
+
if (!homeDouteux) {
|
|
492
|
+
await step('google-purge', 'Retrait des cles Google obsoletes', async () => {
|
|
493
|
+
const r = homeCreds.purgeGoogleKeys({ homeDir });
|
|
494
|
+
return r.purged.length ? `${r.purged.length} cle(s) Google retiree(s)` : 'aucune cle Google presente';
|
|
495
|
+
}, { critical: false });
|
|
496
|
+
}
|
|
236
497
|
|
|
237
|
-
if (platforms.claude) {
|
|
498
|
+
if (platforms.claude && !homeDouteux) {
|
|
238
499
|
await step('skills-sync', 'Controle des copies globales de skills', async () => {
|
|
239
500
|
const r = await offerGlobalSkillsSync(projectRoot, templateDir, { ask: hooks.ask || null, homeDir, log });
|
|
240
501
|
return r.diverged.length === 0 ? 'copies globales fideles' : `${r.diverged.length} divergente(s), ${r.synced.length} synchronisee(s)`;
|
|
@@ -247,8 +508,52 @@ async function runInstall(options = {}, hooks = {}) {
|
|
|
247
508
|
const rtkInstall = hooks.rtkInstall || (() => {
|
|
248
509
|
// The official rtk setup script shipped with this package. Non-TTY
|
|
249
510
|
// safe: it runs unattended; a failure is reported, never fatal.
|
|
511
|
+
//
|
|
512
|
+
// DESCENTE DE PRIVILEGE, pas reprise apres coup. Ce script delegue a
|
|
513
|
+
// brew, curl ou cargo, qui ecrivent dans le HOME (~/.cargo, les caches).
|
|
514
|
+
// Une reprise posterieure ne saurait pas ce que cargo a touche hors du
|
|
515
|
+
// projet ; il faut donc que le sous-processus tourne d'emblee sous
|
|
516
|
+
// l'identite de la cible, avec SON home.
|
|
250
517
|
const script = path.join(__dirname, '..', 'setup-rtk.js');
|
|
251
|
-
const
|
|
518
|
+
const spawnOptions = { encoding: 'utf8', timeout: 300000 };
|
|
519
|
+
|
|
520
|
+
// LA DESCENTE EST ENTIERE OU N'A PAS LIEU.
|
|
521
|
+
//
|
|
522
|
+
// Node ignore silencieusement un gid null : le sous-processus tournait
|
|
523
|
+
// alors sous l'uid de la cible mais gardait le GROUPE root, et tout ce
|
|
524
|
+
// que cargo posait dans son cache arrivait avec un groupe qu'elle ne
|
|
525
|
+
// controle pas. Un depouillement a moitie fait est pire qu'un refus
|
|
526
|
+
// clair, parce qu'il ne se voit pas.
|
|
527
|
+
const descendable = cible.elevated
|
|
528
|
+
&& process.platform !== 'win32'
|
|
529
|
+
&& typeof cible.uid === 'number' && cible.uid !== 0
|
|
530
|
+
&& typeof cible.gid === 'number';
|
|
531
|
+
if (cible.elevated && !descendable && process.platform !== 'win32') {
|
|
532
|
+
throw new Error('descente de privilege impossible (uid ou gid de la cible inconnu) : rtk n\'est pas installe pour eviter de poser des fichiers au nom de root');
|
|
533
|
+
}
|
|
534
|
+
if (descendable) {
|
|
535
|
+
spawnOptions.uid = cible.uid;
|
|
536
|
+
spawnOptions.gid = cible.gid;
|
|
537
|
+
// L'environnement transmis est NETTOYE : garder SUDO_* ferait croire
|
|
538
|
+
// au sous-processus qu'il tourne encore sous elevation, et le PATH de
|
|
539
|
+
// root ne contient pas les dossiers de la cible.
|
|
540
|
+
const envPropre = { ...process.env };
|
|
541
|
+
for (const cle of Object.keys(envPropre)) {
|
|
542
|
+
if (/^(SUDO_|DOAS_|PKEXEC_)/.test(cle)) delete envPropre[cle];
|
|
543
|
+
}
|
|
544
|
+
spawnOptions.env = {
|
|
545
|
+
...envPropre,
|
|
546
|
+
HOME: homeDir,
|
|
547
|
+
USER: cible.name || '',
|
|
548
|
+
LOGNAME: cible.name || '',
|
|
549
|
+
PATH: [path.join(homeDir, '.local', 'bin'), path.join(homeDir, '.cargo', 'bin'), envPropre.PATH || '']
|
|
550
|
+
.filter(Boolean).join(path.delimiter),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const r = spawnSync(process.execPath, [script], spawnOptions);
|
|
554
|
+
// La cause du refus fait partie du message : un EPERM sur le spawn ne
|
|
555
|
+
// se lit pas dans un code de sortie absent.
|
|
556
|
+
if (r.error) throw new Error(`setup-rtk n'a pas pu demarrer (${r.error.code || r.error.message})`);
|
|
252
557
|
if (r.status !== 0) throw new Error(`setup-rtk sortie ${r.status}: ${(r.stderr || '').slice(0, 200)}`);
|
|
253
558
|
return 'rtk installe';
|
|
254
559
|
});
|
|
@@ -256,36 +561,162 @@ async function runInstall(options = {}, hooks = {}) {
|
|
|
256
561
|
}, { critical: false });
|
|
257
562
|
}
|
|
258
563
|
|
|
259
|
-
|
|
564
|
+
// La reprise des droits ferme la marche, apres tous les sous-processus.
|
|
565
|
+
let ownership = null;
|
|
566
|
+
if (!raisonDroits) {
|
|
567
|
+
await step('ownership', 'Reprise du proprietaire et des droits', async () => {
|
|
568
|
+
const dit = [];
|
|
569
|
+
const reprendre = hooks.ensureOwnership || ensureOwnership;
|
|
570
|
+
// Le chemin est RESOLU : les etapes d'ecriture suivent les liens
|
|
571
|
+
// symboliques, la reprise doit viser la meme arborescence qu'elles.
|
|
572
|
+
const racine = cheminResolu(projectRoot);
|
|
573
|
+
// Le groupe partage, quand il existe, s'applique a TOUTE l'arborescence.
|
|
574
|
+
// Le poser sur la seule racine ne donne acces a rien : les fichiers
|
|
575
|
+
// gardent le groupe primaire de la cible.
|
|
576
|
+
const gidVise = gidGroupe !== null ? gidGroupe : cible.gid;
|
|
577
|
+
const reprise = reprendre(racine, { uid: cible.uid, gid: gidVise }, { groupWritable: gidGroupe !== null });
|
|
578
|
+
ownership = { ...reprise, target: { uid: cible.uid, gid: cible.gid, name: cible.name, source: cible.source } };
|
|
579
|
+
dit.push(`proprietaire ${cible.name || cible.uid} : ${reprise.outcome}${reprise.changed ? ` (${reprise.changed} entree(s))` : ''}`);
|
|
580
|
+
for (const err of (reprise.errors || []).slice(0, 3)) log(`[!] droits : ${err.path || ''} ${err.message || err}`);
|
|
581
|
+
|
|
582
|
+
// LES ECRITURES DANS LE HOME SUIVENT L'UTILISATEUR, PAS LE DOSSIER CIBLE.
|
|
583
|
+
//
|
|
584
|
+
// Deux classes d'ecritures, deux regles. Router ~/.byan et ~/.codex vers
|
|
585
|
+
// le bon home ne suffit pas : sous elevation, le fichier y arrive quand
|
|
586
|
+
// meme en root:root. Pour credentials.json, ecrit en 0600, ca donne un
|
|
587
|
+
// fichier de secrets que son proprietaire ne peut plus lire — un etat pire
|
|
588
|
+
// que la panne d'origine, ou il etait seulement au mauvais endroit.
|
|
589
|
+
const reprisesHome = [];
|
|
590
|
+
if (!homeResolu) {
|
|
591
|
+
ownership.homeSkipped = 'home de la cible non resolu : les ecritures hors projet ne sont pas reprises';
|
|
592
|
+
log(`[i] ${ownership.homeSkipped}`);
|
|
593
|
+
} else {
|
|
594
|
+
const dansLeHome = [
|
|
595
|
+
path.join(homeDir, '.byan'),
|
|
596
|
+
path.join(homeDir, '.codex'),
|
|
597
|
+
path.join(homeDir, '.claude', 'skills'),
|
|
598
|
+
];
|
|
599
|
+
for (const cheminHome of dansLeHome) {
|
|
600
|
+
if (!fs.existsSync(cheminHome)) continue;
|
|
601
|
+
reprisesHome.push({ path: cheminHome, ...reprendre(cheminHome, { uid: cible.uid, gid: cible.gid }) });
|
|
602
|
+
}
|
|
603
|
+
if (reprisesHome.length) {
|
|
604
|
+
ownership.home = reprisesHome;
|
|
605
|
+
const repris = reprisesHome.reduce((n, r) => n + (r.changed || 0), 0);
|
|
606
|
+
dit.push(`home : ${reprisesHome.length} emplacement(s), ${repris} entree(s)`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (groupePartage) {
|
|
611
|
+
const g = (hooks.ensureSharedGroup || ensureSharedGroup)(cheminResolu(projectRoot), groupePartage);
|
|
612
|
+
ownership.group = { name: groupePartage, ...g };
|
|
613
|
+
dit.push(`groupe ${groupePartage} : ${g.outcome}${g.gid ? ` (gid ${g.gid})` : ''}`);
|
|
614
|
+
for (const w of g.warnings || []) log(`[i] ${w}`);
|
|
615
|
+
|
|
616
|
+
// Le module abaisse le umask du PROCESSUS et rend l'ancienne valeur en
|
|
617
|
+
// ecrivant que l'appelant doit la restaurer. Sans cette restitution, le
|
|
618
|
+
// reste de la commande — et le `claude` qu'elle lance — heriterait d'un
|
|
619
|
+
// umask 002 qu'aucun d'eux n'a demande.
|
|
620
|
+
if (typeof g.previousUmask === 'number' && typeof process.umask === 'function') {
|
|
621
|
+
try {
|
|
622
|
+
process.umask(g.previousUmask);
|
|
623
|
+
ownership.group.umaskRestored = true;
|
|
624
|
+
} catch (err) {
|
|
625
|
+
log(`[i] umask non restaure : ${err.message}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// UNE ISSUE D'ENSEMBLE. La reprise du projet, celle du home et la pose du
|
|
631
|
+
// groupe peuvent diverger : sans agregation, un echec sur ~/.byan restait
|
|
632
|
+
// invisible dans le rapport, alors que c'est le fichier de secrets.
|
|
633
|
+
const issues = [reprise.outcome]
|
|
634
|
+
.concat(reprisesHome.map((r) => r.outcome))
|
|
635
|
+
.concat(ownership.group ? [ownership.group.outcome] : []);
|
|
636
|
+
ownership.overall = issues.includes('failed') ? 'failed'
|
|
637
|
+
: issues.every((o) => o === 'not-applicable') ? 'not-applicable'
|
|
638
|
+
: 'ok';
|
|
639
|
+
if (ownership.overall === 'failed') {
|
|
640
|
+
const rates = reprisesHome.filter((r) => r.outcome === 'failed').map((r) => r.path);
|
|
641
|
+
if (rates.length) {
|
|
642
|
+
ownership.homeFailed = rates;
|
|
643
|
+
dit.push(`echec dans le home : ${rates.join(', ')}`);
|
|
644
|
+
}
|
|
645
|
+
throw new Error(`reprise des droits incomplete : ${dit.join(' ; ')}`);
|
|
646
|
+
}
|
|
647
|
+
return dit.join(' ; ');
|
|
648
|
+
}, { critical: false });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Meme principe que le plan : la liste des controles porte l'ensemble intendu.
|
|
652
|
+
// Avant, les quatre controles Claude n'etaient meme pas ajoutes quand Claude
|
|
653
|
+
// etait absent, et le rapport annoncait "4/4 controles OK" sur un perimetre
|
|
654
|
+
// reduit en silence. Ils figurent maintenant, marques sautes avec leur raison.
|
|
655
|
+
let verify = { passed: 0, total: 0, failed: [], skipped: [], intendedTotal: 0 };
|
|
260
656
|
await step('verify', 'Verification finale', async () => {
|
|
657
|
+
const raisonClaude = platforms.claude ? null : 'Claude Code non detecte sur cette machine';
|
|
261
658
|
const checks = [
|
|
262
659
|
{ name: 'Dossier agents', p: path.join(byanDir, 'agent') },
|
|
263
660
|
{ name: 'Agent BYAN', p: path.join(byanDir, 'agent', 'byan', 'byan.md') },
|
|
264
661
|
{ name: 'Workflows', p: path.join(byanDir, 'workflow') },
|
|
265
662
|
{ name: 'Config', p: path.join(byanDir, 'bmb', 'config.yaml') },
|
|
663
|
+
{ name: 'CLAUDE.md', p: path.join(projectRoot, '.claude', 'CLAUDE.md'), skipReason: raisonClaude },
|
|
664
|
+
{ name: 'Regles Claude', p: path.join(projectRoot, '.claude', 'rules'), skipReason: raisonClaude },
|
|
665
|
+
{ name: 'Skill byan-byan', p: path.join(projectRoot, '.claude', 'skills', 'byan-byan', 'SKILL.md'), skipReason: raisonClaude },
|
|
666
|
+
{ name: 'Workflow auto-dispatch', p: path.join(projectRoot, '.claude', 'workflows', 'byan-auto-dispatch.js'), skipReason: raisonClaude },
|
|
266
667
|
];
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{ name: 'CLAUDE.md', p: path.join(projectRoot, '.claude', 'CLAUDE.md') },
|
|
270
|
-
{ name: 'Regles Claude', p: path.join(projectRoot, '.claude', 'rules') },
|
|
271
|
-
{ name: 'Skill byan-byan', p: path.join(projectRoot, '.claude', 'skills', 'byan-byan', 'SKILL.md') },
|
|
272
|
-
{ name: 'Workflow auto-dispatch', p: path.join(projectRoot, '.claude', 'workflows', 'byan-auto-dispatch.js') },
|
|
273
|
-
);
|
|
274
|
-
}
|
|
668
|
+
const applicables = checks.filter((c) => !c.skipReason);
|
|
669
|
+
const skipped = checks.filter((c) => c.skipReason).map((c) => ({ name: c.name, reason: c.skipReason }));
|
|
275
670
|
const failed = [];
|
|
276
|
-
for (const c of
|
|
671
|
+
for (const c of applicables) {
|
|
277
672
|
if (!await fs.pathExists(c.p)) failed.push(c.name);
|
|
278
673
|
}
|
|
279
|
-
verify = {
|
|
674
|
+
verify = {
|
|
675
|
+
passed: applicables.length - failed.length,
|
|
676
|
+
total: applicables.length,
|
|
677
|
+
failed,
|
|
678
|
+
skipped,
|
|
679
|
+
intendedTotal: checks.length,
|
|
680
|
+
};
|
|
280
681
|
if (failed.length) throw new Error(`verification incomplete : ${failed.join(', ')}`);
|
|
281
|
-
|
|
682
|
+
const suffixe = skipped.length ? ` — ${skipped.length} controle(s) sur ${checks.length} sautes : ${skipped[0].reason}` : '';
|
|
683
|
+
return `${verify.passed}/${verify.total} controles OK${suffixe}`;
|
|
282
684
|
});
|
|
283
685
|
|
|
686
|
+
// OK EST FONDE SUR LES ECHECS, PAS SUR UNE LISTE D'ETAPES CRITIQUES.
|
|
687
|
+
//
|
|
688
|
+
// L'ancien calcul ne pouvait structurellement pas basculer : les cinq etapes
|
|
689
|
+
// listees comme critiques levent toutes, donc runInstall n'atteignait jamais
|
|
690
|
+
// ce point avec l'une d'elles en echec, et les etapes laterales n'entraient
|
|
691
|
+
// pas dans le test. ok valait vrai par construction.
|
|
692
|
+
//
|
|
693
|
+
// La question honnete est : une etape INTENDUE a-t-elle echoue ? Trois etapes
|
|
694
|
+
// restent tolerantes parce qu'elles n'alterent pas ce qui est livre — rtk est
|
|
695
|
+
// un outil d'appoint, la purge Google et le controle des copies globales ne
|
|
696
|
+
// touchent pas le projet. Tout le reste compte.
|
|
697
|
+
const TOLERANTES = new Set(['rtk', 'google-purge', 'skills-sync']);
|
|
698
|
+
const steps = intended.map((s) => etats.get(s.id));
|
|
699
|
+
const echecs = steps.filter((r) => r.status === 'failed' && !TOLERANTES.has(r.id));
|
|
700
|
+
const ok = echecs.length === 0;
|
|
701
|
+
|
|
284
702
|
return {
|
|
285
|
-
ok
|
|
286
|
-
steps
|
|
703
|
+
ok,
|
|
704
|
+
steps,
|
|
705
|
+
skipped: steps.filter((r) => r.status === 'skipped').map((r) => ({ id: r.id, reason: r.reason })),
|
|
287
706
|
verify,
|
|
288
|
-
|
|
707
|
+
// Qui a ete pris pour cible, et ou la detection a cherche. Sans ces deux
|
|
708
|
+
// champs, "Claude Code introuvable" reste une affirmation invérifiable.
|
|
709
|
+
targetUser: { uid: cible.uid, gid: cible.gid, name: cible.name, home: cible.home, source: cible.source, elevated: cible.elevated },
|
|
710
|
+
detection: detected.searched || null,
|
|
711
|
+
ownership,
|
|
712
|
+
failed: echecs.map((r) => ({ id: r.id, detail: r.detail })),
|
|
713
|
+
// La sonde finale interroge le MEME home que la detection. Sans ca, elle
|
|
714
|
+
// repartait sur le PATH et le home du processus : sous elevation, elle
|
|
715
|
+
// annoncait "Claude Code introuvable" apres une detection qui l'avait
|
|
716
|
+
// trouve deux minutes plus tot.
|
|
717
|
+
launch: (hooks.launchProbe || (() => claudeLaunchCommand({
|
|
718
|
+
exists: (c) => resolveCommandExists(c, { home: homeDir }),
|
|
719
|
+
})))(),
|
|
289
720
|
};
|
|
290
721
|
}
|
|
291
722
|
|
|
@@ -299,6 +730,7 @@ function readOwnVersion() {
|
|
|
299
730
|
|
|
300
731
|
module.exports = {
|
|
301
732
|
runInstall,
|
|
733
|
+
installationEnRootAssumee,
|
|
302
734
|
detectEnvironment,
|
|
303
735
|
claudeLaunchCommand,
|
|
304
736
|
BYAN_DIRS,
|