create-byan-agent 2.59.3 → 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-web-integration.js +9 -2
- package/install/lib/codex-native-setup.js +55 -20
- package/install/lib/install-engine.js +490 -58
- package/install/lib/native-helper.js +13 -2
- package/install/lib/ownership.js +489 -0
- package/install/lib/platforms/claude-code.js +7 -3
- 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 -1
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
/**
|