create-byan-agent 2.59.3 → 2.60.1

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.
@@ -0,0 +1,398 @@
1
+ 'use strict';
2
+
3
+ // Resolution d'un executable sans passer par un shell.
4
+ //
5
+ // POURQUOI ce fichier existe : l'installateur detectait un binaire avec
6
+ // execSync("command -v X", { shell: '/bin/sh' }). Trois defauts mesures le
7
+ // 2026-08-11 :
8
+ // 1. /bin/sh est absent sous Windows, la sonde ne peut pas s'executer.
9
+ // 2. Avec un PATH minimal (getconf PATH rend /bin:/usr/bin), la sonde sort en
10
+ // 1 alors que le binaire est installe : sur la machine de reference, le
11
+ // claude actif vit dans ~/.local/bin, qui n'appartient a aucun PATH par
12
+ // defaut. Un processus qui n'herite pas du PATH de l'utilisateur conclut a
13
+ // tort a une absence.
14
+ // 3. Sous elevation, le PATH lu est celui de root, pas celui de l'utilisateur.
15
+ //
16
+ // POURQUOI une regle de departage : sur la meme machine, le claude actif est
17
+ // /home/yan/.local/bin/claude (lien vers .../versions/2.1.224) et quatre autres
18
+ // exemplaires 2.1.160 (plus anciens) trainent dans des node_modules hors PATH.
19
+ // Un scanner sans regle retient le 2.1.160 et l'erreur ne se voit pas.
20
+ //
21
+ // POURQUOI l'algorithme de parcours est recopie ici : il existe deja dans
22
+ // install/packages/install-core/lib/lookpath.js, mais le champ files du
23
+ // package.json racine n'expedie pas ce dossier — un require vers la-bas
24
+ // n'arriverait pas chez l'utilisateur.
25
+
26
+ const nodeFs = require('fs');
27
+ const nodePath = require('path');
28
+ const os = require('os');
29
+ const { execSync } = require('child_process');
30
+
31
+ const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
32
+
33
+ // Bit d'execution interroge par access(2). Constante nommee plutot que 1 en dur.
34
+ const X_OK = nodeFs.constants.X_OK;
35
+
36
+ // POURQUOI un module de chemin choisi par plateforme : la plateforme est
37
+ // injectee pour le test, donc nodePath (qui suit la machine hote) donnerait des
38
+ // separateurs et un delimiteur de PATH incoherents avec la plateforme simulee.
39
+ function pathFor(platform) {
40
+ return platform === 'win32' ? nodePath.win32 : nodePath.posix;
41
+ }
42
+
43
+ // Sous Windows les noms de variables d'environnement sont insensibles a la
44
+ // casse (Path, PATH, path) ; un objet injecte, lui, est sensible a la casse.
45
+ function readEnv(env, name, platform) {
46
+ if (!env) return undefined;
47
+ if (typeof env[name] === 'string') return env[name];
48
+ if (platform !== 'win32') return undefined;
49
+ const wanted = name.toLowerCase();
50
+ const key = Object.keys(env).find((k) => k.toLowerCase() === wanted);
51
+ return key ? env[key] : undefined;
52
+ }
53
+
54
+ // os.homedir() leve sur un environnement sans $HOME/$USERPROFILE et sans entree
55
+ // passwd pour l'uid effectif (conteneur distroless). Le repli est une chaine
56
+ // vide : les emplacements derives du home sont alors ecartes.
57
+ function safeHome(env, platform) {
58
+ const fromEnv = platform === 'win32'
59
+ ? readEnv(env, 'USERPROFILE', platform) || readEnv(env, 'HOME', platform)
60
+ : readEnv(env, 'HOME', platform);
61
+ if (fromEnv) return fromEnv;
62
+ try {
63
+ return os.homedir();
64
+ } catch (_e) {
65
+ return '';
66
+ }
67
+ }
68
+
69
+ function extensionsFor(env, platform) {
70
+ if (platform !== 'win32') return [];
71
+ const raw = readEnv(env, 'PATHEXT', platform);
72
+ const source = typeof raw === 'string' && raw.length > 0 ? raw : DEFAULT_PATHEXT;
73
+ return source
74
+ .split(';')
75
+ .map((e) => e.trim())
76
+ .filter(Boolean);
77
+ }
78
+
79
+ function hasKnownExtension(name, exts) {
80
+ const lower = String(name).toLowerCase();
81
+ return exts.some((ext) => lower.endsWith(ext.toLowerCase()));
82
+ }
83
+
84
+ // POURQUOI le nom nu n'est pas tente sous Windows : un fichier sans extension
85
+ // listee dans PATHEXT n'est pas executable par le shell Windows. On ne le
86
+ // retient donc pas, meme s'il porte le bon nom.
87
+ function candidateNames(name, ctx) {
88
+ if (!ctx.win) return [name];
89
+ const names = [];
90
+ if (hasKnownExtension(name, ctx.exts)) names.push(name);
91
+ for (const ext of ctx.exts) names.push(name + ext);
92
+ return names;
93
+ }
94
+
95
+ function isExecutableFile(full, ctx) {
96
+ let stat;
97
+ try {
98
+ stat = ctx.fs.statSync(full);
99
+ } catch (_e) {
100
+ // Absence ou refus de lecture : le candidat n'est pas ici, on continue.
101
+ return false;
102
+ }
103
+ if (!stat || typeof stat.isFile !== 'function' || !stat.isFile()) return false;
104
+ if (ctx.win) return hasKnownExtension(full, ctx.exts);
105
+
106
+ // ON DEMANDE AU NOYAU, ON NE DEDUIT PAS DU MODE.
107
+ //
108
+ // Le masque `mode & 0o111` accepte n'importe quel bit d'execution, y compris
109
+ // celui d'un groupe auquel on n'appartient pas. Mesure du 2026-08-12 : un
110
+ // fichier en 0o010 (executable par son groupe seul) passait le masque pour un
111
+ // utilisateur hors de ce groupe, et le resolveur annoncait un binaire qui
112
+ // aurait refuse de se lancer. access(X_OK) pose la question a laquelle on veut
113
+ // vraiment une reponse : est-ce que MOI je peux l'executer.
114
+ if (typeof ctx.fs.accessSync === 'function') {
115
+ try {
116
+ ctx.fs.accessSync(full, X_OK);
117
+ return true;
118
+ } catch (_e) {
119
+ return false;
120
+ }
121
+ }
122
+ // Un faux systeme de fichiers de test peut ne pas exposer accessSync : on
123
+ // retombe alors sur le masque, moins precis mais suffisant pour un decor.
124
+ return (Number(stat.mode) & 0o111) !== 0;
125
+ }
126
+
127
+ function findInDir(dir, name, ctx) {
128
+ for (const candidate of candidateNames(name, ctx)) {
129
+ const full = ctx.path.join(dir, candidate);
130
+ if (isExecutableFile(full, ctx)) return full;
131
+ }
132
+ return null;
133
+ }
134
+
135
+ function splitPath(env, platform) {
136
+ const raw = readEnv(env, 'PATH', platform);
137
+ if (typeof raw !== 'string' || raw.length === 0) return [];
138
+ return raw.split(pathFor(platform).delimiter).filter(Boolean);
139
+ }
140
+
141
+ // Le prefixe npm est retenu pour la duree du processus, par couple (home,
142
+ // systeme). Mesure du 2026-08-12 : sans cette memoire, chaque sonde relancait
143
+ // `npm config get prefix` — un sous-processus de plus de 200 ms, et
144
+ // l'installateur en lance une par binaire cherche. Le cout est desormais paye
145
+ // une seule fois.
146
+ const prefixeNpmRetenu = new Map();
147
+
148
+ // "npm bin -g" a ete retire de npm 11.16.0 (la sous-commande repond "Unknown
149
+ // command"), d'ou la lecture du prefixe. L'appel reste facultatif : un echec
150
+ // rend null et le scan continue sans ce dossier.
151
+ //
152
+ // LE PREFIXE EST CELUI DE LA CIBLE, PAS DU PROCESSUS. npm lit .npmrc dans le
153
+ // home indique par HOME (USERPROFILE sous Windows). Sous elevation, l'env du
154
+ // processus porte /root : sans surcharge, l'installateur cherchait le dossier
155
+ // bin de root au lieu de celui de l'utilisateur qui recoit l'installation.
156
+ function defaultNpmPrefix({ env, platform = process.platform, home = null }) {
157
+ const cle = String(home || '') + '|' + platform;
158
+ if (prefixeNpmRetenu.has(cle)) return prefixeNpmRetenu.get(cle);
159
+
160
+ const envCible = home
161
+ ? Object.assign({}, env, platform === 'win32' ? { USERPROFILE: home } : { HOME: home })
162
+ : env;
163
+ let resultat = null;
164
+ try {
165
+ const out = execSync('npm config get prefix', {
166
+ encoding: 'utf8',
167
+ stdio: ['ignore', 'pipe', 'ignore'],
168
+ // 1500 ms suffisent a lire une configuration locale ; au-dela, npm est
169
+ // indisponible et le parcours continue sans ce dossier.
170
+ timeout: 1500,
171
+ env: envCible,
172
+ cwd: home && nodeFs.existsSync(home) ? home : undefined,
173
+ });
174
+ const trimmed = String(out || '').trim();
175
+ if (trimmed && trimmed !== 'undefined' && trimmed !== 'null') resultat = trimmed;
176
+ } catch (_e) {
177
+ resultat = null;
178
+ }
179
+ prefixeNpmRetenu.set(cle, resultat);
180
+ return resultat;
181
+ }
182
+
183
+ /** Vide la memoire du prefixe npm. Les tests en ont besoin, le code non. */
184
+ function _resetNpmPrefixCache() {
185
+ prefixeNpmRetenu.clear();
186
+ }
187
+
188
+ function resolveNpmPrefix(npmPrefix, env, platform, home) {
189
+ if (!npmPrefix) return null;
190
+ try {
191
+ const value = typeof npmPrefix === 'function'
192
+ ? npmPrefix({ env, platform, home })
193
+ : npmPrefix;
194
+ if (typeof value !== 'string') return null;
195
+ const trimmed = value.trim();
196
+ return trimmed.length > 0 ? trimmed : null;
197
+ } catch (_e) {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * knownBinDirs -> liste ordonnee des emplacements ou un installateur depose un
204
+ * binaire qui n'appartient a aucun PATH par defaut. Le home est injecte.
205
+ */
206
+ function knownBinDirs({
207
+ env = process.env,
208
+ platform = process.platform,
209
+ home = safeHome(env, platform),
210
+ npmPrefix = defaultNpmPrefix,
211
+ } = {}) {
212
+ const p = pathFor(platform);
213
+ const dirs = [];
214
+
215
+ if (platform === 'win32') {
216
+ const appData = readEnv(env, 'APPDATA', platform);
217
+ const localAppData = readEnv(env, 'LOCALAPPDATA', platform);
218
+ const programFiles = readEnv(env, 'ProgramFiles', platform);
219
+ // Les emplacements derives du HOME comptent aussi sous Windows : WSL, Git
220
+ // Bash, MSYS2, rustup et bun y deposent leurs binaires dans la meme
221
+ // arborescence que sous POSIX. La branche n'en explorait aucun.
222
+ if (home) {
223
+ dirs.push(p.join(home, '.local', 'bin'));
224
+ dirs.push(p.join(home, '.claude', 'local'));
225
+ dirs.push(p.join(home, '.cargo', 'bin'));
226
+ dirs.push(p.join(home, '.bun', 'bin'));
227
+ }
228
+ if (appData) dirs.push(p.join(appData, 'npm')); // le stub claude.cmd pose par npm
229
+ if (localAppData) dirs.push(p.join(localAppData, 'Programs'));
230
+ if (programFiles) dirs.push(programFiles);
231
+ } else {
232
+ if (home) {
233
+ dirs.push(p.join(home, '.local', 'bin'));
234
+ dirs.push(p.join(home, '.local', 'share', 'claude', 'versions'));
235
+ dirs.push(p.join(home, '.claude', 'local'));
236
+ dirs.push(p.join(home, '.cargo', 'bin'));
237
+ dirs.push(p.join(home, 'bin'));
238
+ dirs.push(p.join(home, '.npm-global', 'bin'));
239
+ }
240
+ dirs.push('/usr/local/bin');
241
+ dirs.push('/usr/bin');
242
+ dirs.push('/opt/homebrew/bin');
243
+ if (home) dirs.push(p.join(home, '.bun', 'bin'));
244
+ }
245
+
246
+ const prefix = resolveNpmPrefix(npmPrefix, env, platform, home);
247
+ if (prefix) dirs.push(platform === 'win32' ? prefix : p.join(prefix, 'bin'));
248
+
249
+ return dirs;
250
+ }
251
+
252
+ // Un chemin qui traverse un node_modules perd le departage : c'est le cas
253
+ // mesure du 2026-08-11 (exemplaires 2.1.160 sous node_modules contre le 2.1.224
254
+ // actif dans ~/.local/bin).
255
+ function isInsideNodeModules(value) {
256
+ return /(^|[\\/])node_modules([\\/]|$)/.test(String(value));
257
+ }
258
+
259
+ function normalizeKey(value, platform) {
260
+ return platform === 'win32' ? String(value).toLowerCase() : String(value);
261
+ }
262
+
263
+ // Le lien est le bon point d'entree (il suit les mises a jour de version) ; la
264
+ // cible ne sert qu'au diagnostic, elle ne remplace pas le gagnant.
265
+ function realPathOf(target, ctx) {
266
+ try {
267
+ if (typeof ctx.fs.lstatSync !== 'function') return null;
268
+ const stat = ctx.fs.lstatSync(target);
269
+ if (!stat || typeof stat.isSymbolicLink !== 'function' || !stat.isSymbolicLink()) return null;
270
+ if (typeof ctx.fs.realpathSync !== 'function') return null;
271
+ const real = ctx.fs.realpathSync(target);
272
+ if (typeof real !== 'string' || real.length === 0 || real === target) return null;
273
+ return real;
274
+ } catch (_e) {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * resolveBinary(name, options) -> { path, realPath, source, candidates, searched }
281
+ *
282
+ * - path : le chemin retenu, ou null.
283
+ * - realPath : la cible quand le gagnant est un lien symbolique, sinon null.
284
+ * - source : 'path' | 'known-dir' | 'extra' | null — d'ou vient le gagnant.
285
+ * - candidates : tous les exemplaires trouves, dans l'ordre de preference,
286
+ * chacun { path, source, dir }.
287
+ * - searched : tous les dossiers explores, dans l'ordre. Rempli meme quand
288
+ * path est null, pour que le rapport final dise ou il a cherche.
289
+ *
290
+ * Regle de departage :
291
+ * 1. le PATH prime, premier trouve dans l'ordre du PATH ;
292
+ * 2. sinon les emplacements connus, dans l'ordre de la liste ;
293
+ * 3. sinon les dossiers supplementaires fournis par l'appelant ;
294
+ * 4. un chemin sous node_modules passe derriere tout chemin hors node_modules,
295
+ * quel que soit son groupe.
296
+ */
297
+ function resolveBinary(name, {
298
+ env = process.env,
299
+ platform = process.platform,
300
+ fs = nodeFs,
301
+ home = safeHome(env, platform),
302
+ extraDirs = [],
303
+ npmPrefix = defaultNpmPrefix,
304
+ } = {}) {
305
+ const empty = { path: null, realPath: null, source: null, candidates: [], searched: [] };
306
+ if (typeof name !== 'string' || name.length === 0) return empty;
307
+
308
+ const ctx = {
309
+ fs,
310
+ win: platform === 'win32',
311
+ exts: extensionsFor(env, platform),
312
+ path: pathFor(platform),
313
+ };
314
+
315
+ // LE PARCOURS RESTE COMPLET, ET C'EST DELIBERE.
316
+ //
317
+ // Un arret des que le PATH repond irait plus vite, mais viderait `candidates`
318
+ // et `searched` — or le rapport d'installation s'en sert pour dire OU il a
319
+ // cherche. Le cout qui justifiait l'arret etait le sous-processus
320
+ // `npm config get prefix` ; il est desormais paye une seule fois par
321
+ // processus (voir prefixeNpmRetenu). Ce qui reste est une poignee de stat par
322
+ // dossier. Rasoir d'Ockham : on garde l'inventaire, on supprime la cause.
323
+ const groups = [
324
+ { source: 'path', dirs: splitPath(env, platform) },
325
+ { source: 'known-dir', dirs: knownBinDirs({ env, platform, home, npmPrefix }) },
326
+ { source: 'extra', dirs: Array.isArray(extraDirs) ? extraDirs.filter(Boolean) : [] },
327
+ ];
328
+
329
+ const searched = [];
330
+ const seenDirs = new Set();
331
+ const seenPaths = new Set();
332
+ const found = [];
333
+
334
+ for (const group of groups) {
335
+ for (const dir of group.dirs) {
336
+ const dirKey = normalizeKey(dir, platform);
337
+ if (seenDirs.has(dirKey)) continue;
338
+ seenDirs.add(dirKey);
339
+ searched.push(dir);
340
+
341
+ const hit = findInDir(dir, name, ctx);
342
+ if (!hit) continue;
343
+ const hitKey = normalizeKey(hit, platform);
344
+ if (seenPaths.has(hitKey)) continue;
345
+ seenPaths.add(hitKey);
346
+ found.push({ path: hit, source: group.source, dir });
347
+ }
348
+ }
349
+
350
+ const candidates = found
351
+ .filter((c) => !isInsideNodeModules(c.path))
352
+ .concat(found.filter((c) => isInsideNodeModules(c.path)));
353
+
354
+ if (candidates.length === 0) {
355
+ return { path: null, realPath: null, source: null, candidates, searched };
356
+ }
357
+
358
+ const winner = candidates[0];
359
+ return {
360
+ path: winner.path,
361
+ realPath: realPathOf(winner.path, ctx),
362
+ source: winner.source,
363
+ candidates,
364
+ searched,
365
+ };
366
+ }
367
+
368
+ /**
369
+ * commandExists(name, options) -> booleen. Remplacement direct des sondes
370
+ * execSync("command -v X") existantes, sans reecrire leurs appelants.
371
+ */
372
+ function commandExists(name, options = {}) {
373
+ return resolveBinary(name, options).path !== null;
374
+ }
375
+
376
+ /**
377
+ * commandOnPath(name) -> booleen STRICT : le binaire est-il joignable par son
378
+ * nom nu, c'est-a-dire present dans le PATH ?
379
+ *
380
+ * POURQUOI cette seconde sonde : commandExists accepte un binaire trouve dans
381
+ * un emplacement connu hors PATH, ce qui est le bon comportement pour DETECTER
382
+ * (claude vit dans ~/.local/bin). Mais un appelant qui va ensuite LANCER la
383
+ * commande par son nom nu a besoin de savoir qu'elle sera resolue par le shell.
384
+ * Repondre oui sur un binaire hors PATH lui promet un lancement qui echouera.
385
+ */
386
+ function commandOnPath(name, options = {}) {
387
+ return resolveBinary(name, options).source === 'path';
388
+ }
389
+
390
+ module.exports = {
391
+ resolveBinary,
392
+ _resetNpmPrefixCache,
393
+ commandExists,
394
+ commandOnPath,
395
+ knownBinDirs,
396
+ safeHome,
397
+ DEFAULT_PATHEXT,
398
+ };
@@ -50,6 +50,7 @@
50
50
  const { execSync } = require('child_process');
51
51
  const path = require('path');
52
52
  const { commandExists, resolveBinary, firstAvailable } = require('./native-helper');
53
+ const { commandOnPath } = require('./resolve-binary');
53
54
 
54
55
  // Pinned install target. Bumping rtk = change these two constants only. We pin a
55
56
  // TAG so the install is reproducible and supply-chain-bounded (see header).
@@ -214,8 +215,14 @@ function locateRtk({ run = execSync, has = commandExists, resolve = resolveBinar
214
215
 
215
216
  /**
216
217
  * pickStrategy() -> the first install strategy whose tool is on PATH, or null.
218
+ *
219
+ * LA SONDE EST STRICTEMENT PATH, ET C'EST VOULU. La strategie retenue lance sa
220
+ * commande par son NOM NU (`brew install ...`, `cargo install ...`). Depuis que
221
+ * commandExists accepte un binaire trouve hors PATH — le bon comportement pour
222
+ * detecter claude dans ~/.local/bin — repondre oui ici promettrait un
223
+ * lancement que le shell ne saurait pas resoudre.
217
224
  */
218
- function pickStrategy({ has = commandExists } = {}) {
225
+ function pickStrategy({ has = commandOnPath } = {}) {
219
226
  return firstAvailable(installStrategies(), { has });
220
227
  }
221
228
 
@@ -358,7 +365,7 @@ function setupRtkIntegration({
358
365
  * (BYAN_SKIP_RTK=1), and (c) an installer is actually on PATH — so we never prompt
359
366
  * for something we cannot deliver, and never block a non-interactive/CI install.
360
367
  */
361
- function shouldOfferRtk({ env = process.env, isTTY = !!(process.stdin && process.stdin.isTTY), has = commandExists } = {}) {
368
+ function shouldOfferRtk({ env = process.env, isTTY = !!(process.stdin && process.stdin.isTTY), has = commandOnPath } = {}) {
362
369
  if (env && env.BYAN_SKIP_RTK === '1') return false;
363
370
  if (!isTTY) return false;
364
371
  return Boolean(pickStrategy({ has }));
@@ -9,6 +9,7 @@
9
9
 
10
10
  const { execSync } = require('child_process');
11
11
  const chalk = require('chalk');
12
+ const { commandExists: sharedCommandExists } = require('../resolve-binary');
12
13
 
13
14
  const PARAKEET_MIN_VRAM = 4000; // 4 GB
14
15
  const WHISPER_MIN_VRAM = 1000; // 1 GB for GPU mode
@@ -45,13 +46,11 @@ function detectGPU() {
45
46
  * @param {string} cmd
46
47
  * @returns {boolean}
47
48
  */
49
+ // Delegue au resolveur partage (install/lib/resolve-binary.js) au lieu de
50
+ // lancer `which`. `which` est absent de Windows, et un processus qui n'herite
51
+ // pas du PATH de l'utilisateur conclut a tort a une absence.
48
52
  function commandExists(cmd) {
49
- try {
50
- execSync(`which ${cmd}`, { stdio: 'pipe' });
51
- return true;
52
- } catch {
53
- return false;
54
- }
53
+ return sharedCommandExists(cmd);
55
54
  }
56
55
 
57
56
  /**