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
|
@@ -15,6 +15,8 @@ const { getDomainQuestions, buildPhase2Prompt } = require('../lib/domain-questio
|
|
|
15
15
|
const { generateProjectAgentsDoc } = require('../lib/project-agents-generator');
|
|
16
16
|
const { launchPhase2Chat, generateDefaultConfig } = require('../lib/phase2-chat');
|
|
17
17
|
const { setupByanWebIntegration, validateByanWebReachability } = require('../lib/byan-web-integration');
|
|
18
|
+
const { resolveTargetUser } = require('../lib/target-user');
|
|
19
|
+
const { commandExists: resolveCommandExists } = require('../lib/resolve-binary');
|
|
18
20
|
const { setupLeantimeIntegration, validateLeantimeReachability } = require('../lib/byan-leantime-integration');
|
|
19
21
|
const { setupRtkIntegration, shouldOfferRtk } = require('../lib/rtk-integration');
|
|
20
22
|
const { setupClaudeNative } = require('../lib/claude-native-setup');
|
|
@@ -180,51 +182,29 @@ async function copyV2Runtime(templateDir, projectRoot, spinner) {
|
|
|
180
182
|
// Detect installed platforms (Yanstaller logic)
|
|
181
183
|
// Detects SYSTEM binaries, not project folders (.codex, .github/agents are created by yanstaller)
|
|
182
184
|
async function detectPlatforms() {
|
|
185
|
+
// Meme resolveur que le moteur d'installation (install/lib/resolve-binary.js),
|
|
186
|
+
// et meme utilisateur cible que lui. Cette fonction lancait `which codex` puis
|
|
187
|
+
// `which claude` : `which` est absent de Windows, et sous elevation le PATH
|
|
188
|
+
// interroge est celui de root, donc les deux binaires passaient pour absents.
|
|
189
|
+
// Le repli sur ~/.config n'aidait pas non plus, os.homedir() rendant /root.
|
|
190
|
+
const cible = resolveTargetUser({ cwd: process.cwd() });
|
|
191
|
+
const home = cible.home || os.homedir();
|
|
183
192
|
const platforms = {
|
|
184
|
-
codex:
|
|
185
|
-
claude:
|
|
193
|
+
codex: resolveCommandExists('codex', { home }),
|
|
194
|
+
claude: resolveCommandExists('claude', { home }),
|
|
186
195
|
};
|
|
187
196
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
];
|
|
200
|
-
for (const p of codexPaths) {
|
|
201
|
-
if (fs.existsSync(p)) {
|
|
202
|
-
platforms.codex = true;
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Claude Code detection (binary + config)
|
|
209
|
-
try {
|
|
210
|
-
const result = execSync('which claude 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
211
|
-
if (result) {
|
|
212
|
-
platforms.claude = true;
|
|
213
|
-
}
|
|
214
|
-
} catch (e) {
|
|
215
|
-
// Fallback: check config directory
|
|
216
|
-
const claudePaths = [
|
|
217
|
-
path.join(os.homedir(), '.config', 'claude'),
|
|
218
|
-
path.join(os.homedir(), '.claude')
|
|
219
|
-
];
|
|
220
|
-
for (const p of claudePaths) {
|
|
221
|
-
if (fs.existsSync(p)) {
|
|
222
|
-
platforms.claude = true;
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
197
|
+
// Repli par le dossier de configuration, dans le home de la CIBLE : un CLI
|
|
198
|
+
// installe puis retire laisse sa configuration, et un binaire hors de tout
|
|
199
|
+
// emplacement connu reste detectable par elle.
|
|
200
|
+
const parConfig = (nom) => [
|
|
201
|
+
path.join(home, '.config', nom),
|
|
202
|
+
path.join(home, `.${nom}`),
|
|
203
|
+
].some((p) => fs.existsSync(p));
|
|
204
|
+
|
|
205
|
+
if (!platforms.codex) platforms.codex = parConfig('codex');
|
|
206
|
+
if (!platforms.claude) platforms.claude = parConfig('claude');
|
|
207
|
+
|
|
228
208
|
return platforms;
|
|
229
209
|
}
|
|
230
210
|
|
|
@@ -1395,7 +1375,7 @@ async function install(options = {}) {
|
|
|
1395
1375
|
console.log(chalk.cyan('byan_web integration (optional — service payant)'));
|
|
1396
1376
|
let byanWebResult = { configured: false };
|
|
1397
1377
|
try {
|
|
1398
|
-
byanWebResult = await setupByanWebIntegration(projectRoot);
|
|
1378
|
+
byanWebResult = await setupByanWebIntegration(projectRoot, { homeDir: resolveTargetUser({ cwd: projectRoot }).home || undefined });
|
|
1399
1379
|
} catch (error) {
|
|
1400
1380
|
console.log(chalk.yellow(` [WARN] byan_web setup skipped: ${error.message}`));
|
|
1401
1381
|
}
|
|
@@ -1914,11 +1894,35 @@ async function installAuto(options) {
|
|
|
1914
1894
|
// Default is notice-only ; --sync-skills opts in to an unattended sync.
|
|
1915
1895
|
const ask = skillsSyncConsent(options);
|
|
1916
1896
|
|
|
1897
|
+
// --ask ouvre les questions au lieu du parcours automatique. On reutilise le
|
|
1898
|
+
// demandeur existant (setupByanWebIntegration -> promptForToken), qui pose
|
|
1899
|
+
// l'URL et le token puis ecrit .env, .mcp.json et ~/.byan/credentials.json.
|
|
1900
|
+
// En ecrire un second ferait diverger l'experience entre create-byan-agent et
|
|
1901
|
+
// update-byan-agent, exactement ce que l'extraction de token-prompt evitait.
|
|
1902
|
+
let apiUrlChoisie = options.apiUrl || null;
|
|
1903
|
+
if (options.ask) {
|
|
1904
|
+
console.log(chalk.cyan('\n Mode question : configuration de l API byan_web\n'));
|
|
1905
|
+
try {
|
|
1906
|
+
// Le home de la CIBLE, pas celui du processus : sous elevation, ce flux
|
|
1907
|
+
// ecrivait le jeton de l'utilisateur dans /root/.byan.
|
|
1908
|
+
const cible = resolveTargetUser({ cwd: projectRoot });
|
|
1909
|
+
const r = await setupByanWebIntegration(projectRoot, { homeDir: cible.home || undefined });
|
|
1910
|
+
if (r && r.configured && r.apiUrl) apiUrlChoisie = r.apiUrl;
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
console.log(chalk.yellow(` [WARN] configuration byan_web ignoree : ${error.message}`));
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1917
1916
|
try {
|
|
1918
1917
|
const result = await engine.runInstall({
|
|
1919
1918
|
projectRoot,
|
|
1920
1919
|
projectName: options.name || undefined,
|
|
1921
1920
|
rtk: options.rtk !== false,
|
|
1921
|
+
apiUrl: apiUrlChoisie,
|
|
1922
|
+
// commander pose options.chown a false quand --no-chown est donne.
|
|
1923
|
+
chown: options.chown !== false,
|
|
1924
|
+
owner: options.owner || null,
|
|
1925
|
+
group: options.group || null,
|
|
1922
1926
|
}, {
|
|
1923
1927
|
onStep: ({ index, total, label }) => {
|
|
1924
1928
|
if (spinner) spinner.succeed();
|
|
@@ -1932,15 +1936,56 @@ async function installAuto(options) {
|
|
|
1932
1936
|
console.log('');
|
|
1933
1937
|
console.log(chalk.green.bold(` Installation terminee — verification ${result.verify.passed}/${result.verify.total}`));
|
|
1934
1938
|
|
|
1939
|
+
// Ce qui n'a PAS tourne se dit a voix haute. Avant, une etape ecartee
|
|
1940
|
+
// disparaissait du plan et le compteur affichait une reussite sur un
|
|
1941
|
+
// perimetre reduit sans le signaler (mesure du 2026-08-11).
|
|
1942
|
+
for (const s of result.skipped || []) {
|
|
1943
|
+
console.log(chalk.yellow(` Etape sautee : ${s.id} — ${s.reason}`));
|
|
1944
|
+
}
|
|
1945
|
+
for (const c of (result.verify.skipped || [])) {
|
|
1946
|
+
console.log(chalk.gray(` Controle saute : ${c.name} — ${c.reason}`));
|
|
1947
|
+
}
|
|
1948
|
+
// Qui a ete pris pour cible, et ce qui a ete repris. Sans cette ligne,
|
|
1949
|
+
// "Claude Code introuvable" reste une affirmation que personne ne peut
|
|
1950
|
+
// verifier, et une reprise de droits reste invisible.
|
|
1951
|
+
if (result.targetUser && result.targetUser.source !== 'self') {
|
|
1952
|
+
const t = result.targetUser;
|
|
1953
|
+
console.log(chalk.gray(` Utilisateur cible : ${t.name || t.uid} (${t.source}), home ${t.home}`));
|
|
1954
|
+
}
|
|
1955
|
+
if (result.ownership) {
|
|
1956
|
+
const o = result.ownership;
|
|
1957
|
+
const g = o.group ? `, groupe ${o.group.name} : ${o.group.outcome}` : '';
|
|
1958
|
+
// On affiche l'issue D'ENSEMBLE : le projet, le home et le groupe peuvent
|
|
1959
|
+
// diverger, et n'annoncer que le projet taisait un echec sur ~/.byan.
|
|
1960
|
+
const issue = o.overall || o.outcome;
|
|
1961
|
+
const teinte = issue === 'failed' ? chalk.yellow : chalk.gray;
|
|
1962
|
+
console.log(teinte(` Droits : ${issue}${o.changed ? ` — ${o.changed} entree(s) reprises` : ''}${g}`));
|
|
1963
|
+
if (o.homeSkipped) console.log(chalk.gray(` ${o.homeSkipped}`));
|
|
1964
|
+
for (const p of o.homeFailed || []) console.log(chalk.yellow(` echec sur ${p}`));
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1935
1967
|
// End-of-install launch (F5): start Claude Code, with the byan-channel
|
|
1936
1968
|
// when this CLI version supports it. --no-launch or a non-interactive
|
|
1937
1969
|
// terminal prints the command instead of running it.
|
|
1938
1970
|
if (result.launch) {
|
|
1939
|
-
|
|
1971
|
+
// SOUS ELEVATION, ON N'OUVRE PAS CLAUDE — ON DONNE LA COMMANDE.
|
|
1972
|
+
//
|
|
1973
|
+
// Lancer claude ici le ferait tourner en root : tout ce qu'il ecrirait
|
|
1974
|
+
// ensuite dans le projet reviendrait a root, et defairait la reprise des
|
|
1975
|
+
// droits acquise a l'etape precedente. Le mode non interactif imprime deja
|
|
1976
|
+
// la commande ; on emprunte le meme chemin plutot que d'inventer une
|
|
1977
|
+
// descente de privilege pour un shell interactif.
|
|
1978
|
+
const sousElevation = Boolean(result.targetUser && result.targetUser.elevated);
|
|
1979
|
+
if (options.launch !== false && isTTY && !sousElevation) {
|
|
1940
1980
|
console.log(chalk.cyan(` Lancement de Claude Code : ${result.launch.command}\n`));
|
|
1941
1981
|
const { spawnSync } = require('child_process');
|
|
1942
1982
|
spawnSync(result.launch.command, { stdio: 'inherit', shell: true });
|
|
1943
1983
|
} else {
|
|
1984
|
+
if (sousElevation) {
|
|
1985
|
+
const qui = result.targetUser.name || result.targetUser.uid;
|
|
1986
|
+
console.log(chalk.yellow(` Installation lancee sous elevation : Claude Code n'est pas demarre ici.`));
|
|
1987
|
+
console.log(chalk.gray(` Reprends la main en tant que ${qui}, puis dans le projet :`));
|
|
1988
|
+
}
|
|
1944
1989
|
console.log(chalk.gray(` Pour lancer Claude Code : ${result.launch.command}`));
|
|
1945
1990
|
}
|
|
1946
1991
|
} else {
|
|
@@ -2013,6 +2058,11 @@ program
|
|
|
2013
2058
|
.option('--no-launch', 'Ne pas lancer Claude Code en fin d installation (mode --cli)')
|
|
2014
2059
|
.option('--no-rtk', 'Ne pas installer rtk automatiquement (mode --cli)')
|
|
2015
2060
|
.option('--sync-skills', 'Mode --cli : synchroniser sans demander les copies globales ~/.claude/skills divergentes')
|
|
2061
|
+
.option('--ask', 'Mode --cli : poser les questions (URL de l API byan_web, token) au lieu du parcours automatique')
|
|
2062
|
+
.option('--api-url <url>', 'URL de l API byan_web (defaut : le host de production ; BYAN_API_URL de l environnement le remplace)')
|
|
2063
|
+
.option('--no-chown', 'Ne pas reprendre le proprietaire des fichiers poses (installation deliberee en root)')
|
|
2064
|
+
.option('--owner <user>', 'Forcer l utilisateur cible (nom ou uid) au lieu de le deduire de l elevation')
|
|
2065
|
+
.option('--group <name>', 'Groupe partage pose sur le projet (setgid) : un usermod -aG suffit ensuite a donner l acces')
|
|
2016
2066
|
.option('--legacy', 'Interview complete d origine (l ancien parcours a questions)')
|
|
2017
2067
|
.action(async (options) => {
|
|
2018
2068
|
const mode = chooseInstallMode(options);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* L'URL de l'API byan_web — une seule source, pour toute l'installation.
|
|
5
|
+
*
|
|
6
|
+
* D'OU VIENT CE FICHIER. Trois endroits posaient `http://localhost:3737` en
|
|
7
|
+
* dur : codex-native-setup.js, platforms/claude-code.js et le demandeur
|
|
8
|
+
* interactif token-prompt.js. Un projet installe sur une machine qui n'heberge
|
|
9
|
+
* pas byan_web recevait donc une configuration pointant sur son propre port
|
|
10
|
+
* 3737, ou rien n'ecoute. Le serveur MCP demarrait et ne joignait jamais l'API.
|
|
11
|
+
*
|
|
12
|
+
* LE HOST DE PRODUCTION, MESURE le 2026-08-11 :
|
|
13
|
+
* curl https://byan-api.stark.a3n.fr/api/health
|
|
14
|
+
* -> 200, application/json, {"status":"ok","version":"1.0.0"}
|
|
15
|
+
* curl https://byan.stark.a3n.fr/api/health
|
|
16
|
+
* -> 302 vers auth.acadenice.com, text/html
|
|
17
|
+
* Le second est l'interface web derriere une authentification SSO, pas l'API.
|
|
18
|
+
* C'est le piege deja ecrit dans .claude/rules/byan-api.md, et le meme que
|
|
19
|
+
* celui rencontre cote Leantime (un POST qui revient en page HTML de connexion
|
|
20
|
+
* au lieu d'un corps JSON). Prendre le host de l'interface pour celui de l'API
|
|
21
|
+
* produit une panne difficile a lire, parce que la reponse ressemble a une
|
|
22
|
+
* reponse.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
// Le host mesure repondant en JSON. Changer cette constante change le defaut
|
|
28
|
+
// de toute l'installation, terminal et assistant web compris.
|
|
29
|
+
const PROD_API_URL = 'https://byan-api.stark.a3n.fr';
|
|
30
|
+
|
|
31
|
+
// Conserve pour qui developpe byan_web en local et pose BYAN_API_URL lui-meme.
|
|
32
|
+
const LOCAL_API_URL = 'http://localhost:3737';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Retire le suffixe /api et la barre oblique finale.
|
|
36
|
+
*
|
|
37
|
+
* Les endpoints portent deja /api : une base qui le contient produit
|
|
38
|
+
* /api/api/projects. La verification de derive de install-core signale
|
|
39
|
+
* d'ailleurs ce cas (verify.js, "BYAN_API_URL has an /api suffix"), preuve que
|
|
40
|
+
* la faute a deja ete commise.
|
|
41
|
+
*/
|
|
42
|
+
function normalizeApiUrl(url) {
|
|
43
|
+
if (typeof url !== 'string') return '';
|
|
44
|
+
const propre = url.trim().replace(/\/+$/, '');
|
|
45
|
+
if (!propre) return '';
|
|
46
|
+
return propre.replace(/\/api$/i, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isHttpUrl(url) {
|
|
50
|
+
return typeof url === 'string' && /^https?:\/\/\S+$/.test(url.trim());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* L'URL retenue, et d'ou elle vient. L'ordre est le contrat :
|
|
55
|
+
*
|
|
56
|
+
* 1. 'explicit' — ce que l'appelant a passe (--api-url, une reponse a une
|
|
57
|
+
* question). L'intention exprimee maintenant gagne.
|
|
58
|
+
* 2. 'env' — BYAN_API_URL dans l'environnement. Le geste d'un
|
|
59
|
+
* administrateur ou d'une chaine d'integration continue.
|
|
60
|
+
* 3. 'credentials' — la valeur memorisee dans ~/.byan/credentials.json lors
|
|
61
|
+
* d'une installation precedente. Ne pas la relire ferait
|
|
62
|
+
* re-saisir a chaque projet ce qui a deja ete donne.
|
|
63
|
+
* 4. 'default' — le host de production.
|
|
64
|
+
*
|
|
65
|
+
* `readCredentials` est injecte pour que ce module reste testable sans toucher
|
|
66
|
+
* au home de la machine.
|
|
67
|
+
*/
|
|
68
|
+
function resolveApiUrl({
|
|
69
|
+
explicit = null,
|
|
70
|
+
env = process.env,
|
|
71
|
+
homeDir = null,
|
|
72
|
+
readCredentials = null,
|
|
73
|
+
} = {}) {
|
|
74
|
+
if (isHttpUrl(explicit)) return { url: normalizeApiUrl(explicit), source: 'explicit' };
|
|
75
|
+
if (env && isHttpUrl(env.BYAN_API_URL)) return { url: normalizeApiUrl(env.BYAN_API_URL), source: 'env' };
|
|
76
|
+
|
|
77
|
+
const lire = readCredentials || defaultReadCredentials;
|
|
78
|
+
try {
|
|
79
|
+
const memorise = lire({ homeDir });
|
|
80
|
+
if (memorise && isHttpUrl(memorise.BYAN_API_URL)) {
|
|
81
|
+
return { url: normalizeApiUrl(memorise.BYAN_API_URL), source: 'credentials' };
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Un fichier de configuration absent ou abime ne doit pas arreter une
|
|
85
|
+
// installation : on descend simplement a l'echelon suivant.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { url: PROD_API_URL, source: 'default' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Chargement paresseux : home-credentials tire fs-extra, et api-defaults est
|
|
92
|
+
// requis par des modules qui n'ont pas besoin de cette chaine.
|
|
93
|
+
function defaultReadCredentials({ homeDir } = {}) {
|
|
94
|
+
// eslint-disable-next-line global-require
|
|
95
|
+
const homeCreds = require(path.join(__dirname, 'home-credentials.js'));
|
|
96
|
+
return homeCreds.readCredentials(homeDir ? { homeDir } : {});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Raccourci pour les appelants qui ne veulent que la chaine. */
|
|
100
|
+
function apiUrl(options = {}) {
|
|
101
|
+
return resolveApiUrl(options).url;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
PROD_API_URL,
|
|
106
|
+
LOCAL_API_URL,
|
|
107
|
+
normalizeApiUrl,
|
|
108
|
+
isHttpUrl,
|
|
109
|
+
resolveApiUrl,
|
|
110
|
+
apiUrl,
|
|
111
|
+
};
|
|
@@ -22,7 +22,7 @@ const {
|
|
|
22
22
|
envConfig: { updateSettingsLocal: sharedUpdateSettingsLocal, updateDotenv: sharedUpdateDotenv },
|
|
23
23
|
tokenPrompt: { promptForLeantime },
|
|
24
24
|
validate: { validateLeantimeReachability },
|
|
25
|
-
} = require('
|
|
25
|
+
} = require('../packages/platform-config');
|
|
26
26
|
|
|
27
27
|
// Shared writers return { path } — unwrap to a plain string path for the
|
|
28
28
|
// console report, matching byan-web-integration's surface.
|
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
validate: { validateByanWebReachability },
|
|
14
14
|
credentials: { writeCredentials: sharedWriteCredentials },
|
|
15
15
|
urlUtils: { stripApiSuffix },
|
|
16
|
-
} = require('
|
|
16
|
+
} = require('../packages/platform-config');
|
|
17
17
|
const { purgeGoogleKeys } = require('./home-credentials');
|
|
18
18
|
|
|
19
19
|
// Shared primitives return { path: string } — unwrap to plain string for
|
|
@@ -45,7 +45,7 @@ async function setupByanWebIntegration(projectRoot, options = {}) {
|
|
|
45
45
|
// Soft-purge Google keys from ~/.byan/credentials.json — ALWAYS, meme si
|
|
46
46
|
// l'utilisateur decline byan_web : les cles Google ne sont plus utilisees
|
|
47
47
|
// par BYAN quel que soit le choix (proxy server-side).
|
|
48
|
-
const purgeResult = purgeGoogleKeys();
|
|
48
|
+
const purgeResult = purgeGoogleKeys(options.homeDir ? { homeDir: options.homeDir } : {});
|
|
49
49
|
if (purgeResult.purged.length > 0) {
|
|
50
50
|
const backupMsg = purgeResult.backupPath ? ` (backup : ${purgeResult.backupPath})` : '';
|
|
51
51
|
console.log(chalk.gray(` Cles Google retirees de ~/.byan/credentials.json${backupMsg}`));
|
|
@@ -71,7 +71,14 @@ async function setupByanWebIntegration(projectRoot, options = {}) {
|
|
|
71
71
|
// boot (env -> ~/.byan/credentials.json -> localhost). This is what makes the
|
|
72
72
|
// MCP portable across shells/OSes and Claude Code AND Codex, instead of
|
|
73
73
|
// relying on .mcp.json ${} expansion or settings.local.json env injection.
|
|
74
|
-
|
|
74
|
+
// Le home est celui de la CIBLE quand l'appelant le connait. Sous elevation,
|
|
75
|
+
// os.homedir() rend /root : ce flux interactif y ecrivait le jeton de
|
|
76
|
+
// l'utilisateur, alors que le chemin non interactif (install-engine) passait
|
|
77
|
+
// deja le bon home. Les deux ecrivent maintenant au meme endroit.
|
|
78
|
+
const credsResult = await sharedWriteCredentials(
|
|
79
|
+
{ BYAN_API_URL: stripApiSuffix(inputs.apiUrl), BYAN_API_TOKEN: inputs.token },
|
|
80
|
+
options.homeDir ? { homedir: options.homeDir } : {}
|
|
81
|
+
);
|
|
75
82
|
|
|
76
83
|
|
|
77
84
|
if (!options.quiet) {
|
|
@@ -23,7 +23,7 @@ const chalk = require('chalk');
|
|
|
23
23
|
|
|
24
24
|
const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates');
|
|
25
25
|
const { whitelistMcpServer } = require('./settings-local');
|
|
26
|
-
const { mcpConfig } = require('
|
|
26
|
+
const { mcpConfig } = require('../packages/platform-config');
|
|
27
27
|
|
|
28
28
|
async function copyClaudeHooks(projectRoot) {
|
|
29
29
|
const src = path.join(TEMPLATE_ROOT, '.claude', 'hooks');
|
|
@@ -18,27 +18,47 @@ const fs = require('fs-extra');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const os = require('os');
|
|
20
20
|
const chalk = require('chalk');
|
|
21
|
+
const apiDefaults = require('./api-defaults');
|
|
21
22
|
|
|
22
23
|
const SERVER_NAME = 'byan';
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// Le home est un PARAMETRE, pas une constante lue sur le processus.
|
|
26
|
+
//
|
|
27
|
+
// Motif mesure le 2026-08-11 : sous elevation de privilege, os.homedir() rend
|
|
28
|
+
// /root. Ces trois fonctions ecrivaient donc dans /root/.codex au lieu du home
|
|
29
|
+
// de l'utilisateur qui a lance la commande. home-credentials.js et
|
|
30
|
+
// global-skills-sync.js acceptaient deja un homeDir ; ce module etait le seul
|
|
31
|
+
// a ne pas offrir de porte d'entree, ce qui rendait la correction inapplicable.
|
|
32
|
+
function safeHome() {
|
|
33
|
+
try {
|
|
34
|
+
return os.homedir();
|
|
35
|
+
} catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getCodexConfigPath(homeDir) {
|
|
41
|
+
return path.join(homeDir || safeHome(), '.codex', 'config.toml');
|
|
26
42
|
}
|
|
27
43
|
|
|
28
|
-
function getCodexSkillsDir() {
|
|
29
|
-
return path.join(
|
|
44
|
+
function getCodexSkillsDir(homeDir) {
|
|
45
|
+
return path.join(homeDir || safeHome(), '.codex', 'skills');
|
|
30
46
|
}
|
|
31
47
|
|
|
32
|
-
async function detectCodex() {
|
|
33
|
-
return fs.pathExists(path.join(
|
|
48
|
+
async function detectCodex(homeDir) {
|
|
49
|
+
return fs.pathExists(path.join(homeDir || safeHome(), '.codex'));
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
// TOML literal-string escape: single quotes don't interpret backslashes,
|
|
37
53
|
// so paths and tokens land verbatim. We just refuse values containing "'".
|
|
38
|
-
function tomlLiteral(value) {
|
|
54
|
+
function tomlLiteral(value, label) {
|
|
39
55
|
const s = String(value);
|
|
40
56
|
if (s.includes("'")) {
|
|
41
|
-
|
|
57
|
+
// LA VALEUR REFUSEE NE VOYAGE PAS DANS LE MESSAGE. Elle peut etre un jeton
|
|
58
|
+
// d'API : le message d'erreur remonte dans le detail de l'etape, journalise
|
|
59
|
+
// par install-engine et rediffuse au navigateur par l'assistant web. On
|
|
60
|
+
// nomme le champ, pas son contenu.
|
|
61
|
+
throw new Error(`valeur non encodable en litteral TOML (apostrophe interdite) : champ ${label || 'inconnu'}`);
|
|
42
62
|
}
|
|
43
63
|
return `'${s}'`;
|
|
44
64
|
}
|
|
@@ -74,20 +94,20 @@ function buildByanBlock({ serverPath, apiUrl, apiToken, startupTimeoutSec = 15 }
|
|
|
74
94
|
const lines = [
|
|
75
95
|
'',
|
|
76
96
|
'[mcp_servers.byan]',
|
|
77
|
-
`command = ${tomlLiteral('node')}`,
|
|
78
|
-
`args = [${tomlLiteral(serverPath)}]`,
|
|
97
|
+
`command = ${tomlLiteral('node', 'command')}`,
|
|
98
|
+
`args = [${tomlLiteral(serverPath, 'serverPath')}]`,
|
|
79
99
|
`startup_timeout_sec = ${Number.isFinite(startupTimeoutSec) ? startupTimeoutSec : 15}`,
|
|
80
100
|
'',
|
|
81
101
|
'[mcp_servers.byan.env]',
|
|
82
|
-
`BYAN_API_URL = ${tomlLiteral(apiUrl)}`,
|
|
83
|
-
`BYAN_API_TOKEN = ${tomlLiteral(apiToken)}`,
|
|
102
|
+
`BYAN_API_URL = ${tomlLiteral(apiUrl, 'BYAN_API_URL')}`,
|
|
103
|
+
`BYAN_API_TOKEN = ${tomlLiteral(apiToken, 'BYAN_API_TOKEN')}`,
|
|
84
104
|
'',
|
|
85
105
|
];
|
|
86
106
|
return lines.join('\n');
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
async function patchCodexConfig(projectRoot, options = {}) {
|
|
90
|
-
const configPath = getCodexConfigPath();
|
|
110
|
+
const configPath = getCodexConfigPath(options.homeDir);
|
|
91
111
|
const serverPath = path.join(
|
|
92
112
|
projectRoot,
|
|
93
113
|
'_byan',
|
|
@@ -103,10 +123,11 @@ async function patchCodexConfig(projectRoot, options = {}) {
|
|
|
103
123
|
existing = await fs.readFile(configPath, 'utf8');
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
// Une seule source pour l'URL de l'API (install/lib/api-defaults.js). Le
|
|
127
|
+
// defaut valait localhost:3737 : un projet installe sur une machine qui
|
|
128
|
+
// n'heberge pas byan_web recevait une configuration pointant sur un port ou
|
|
129
|
+
// rien n'ecoute.
|
|
130
|
+
const apiUrl = apiDefaults.apiUrl({ explicit: options.apiUrl, homeDir: options.homeDir });
|
|
110
131
|
const apiToken =
|
|
111
132
|
options.apiToken !== undefined
|
|
112
133
|
? options.apiToken
|
|
@@ -123,7 +144,21 @@ async function patchCodexConfig(projectRoot, options = {}) {
|
|
|
123
144
|
const merged =
|
|
124
145
|
(stripped.trimEnd().length > 0 ? stripped.trimEnd() + '\n' : '') + block;
|
|
125
146
|
|
|
126
|
-
|
|
147
|
+
// CE FICHIER PORTE UN JETON D'API : IL SE FERME.
|
|
148
|
+
//
|
|
149
|
+
// fs.writeFile n'applique le mode qu'a la CREATION ; sur une reecriture, le
|
|
150
|
+
// fichier garde ses droits d'origine. D'ou le chmod qui suit, et le 0700 sur
|
|
151
|
+
// le dossier. Meme sequence que packages/platform-config/lib/credentials.js
|
|
152
|
+
// pour ~/.byan/credentials.json. Sans ca, config.toml sortait en 644 : lisible
|
|
153
|
+
// par tout compte de la machine.
|
|
154
|
+
await fs.writeFile(configPath, merged, { encoding: 'utf8', mode: 0o600 });
|
|
155
|
+
try {
|
|
156
|
+
await fs.chmod(configPath, 0o600);
|
|
157
|
+
await fs.chmod(path.dirname(configPath), 0o700);
|
|
158
|
+
} catch (_e) {
|
|
159
|
+
// Un systeme de fichiers sans droits POSIX (Windows, exFAT) ne sait pas
|
|
160
|
+
// faire ca. L'ecriture, elle, a eu lieu : on ne la defait pas pour autant.
|
|
161
|
+
}
|
|
127
162
|
return { path: configPath, hadExisting: existing.length > 0, tokenSet: apiToken.length > 0 };
|
|
128
163
|
}
|
|
129
164
|
|
|
@@ -172,7 +207,7 @@ function defaultSkillSourceDirs(projectRoot, options = {}) {
|
|
|
172
207
|
}
|
|
173
208
|
|
|
174
209
|
async function installCodexNativeSkills(projectRoot, options = {}) {
|
|
175
|
-
const destDir = options.destDir || getCodexSkillsDir();
|
|
210
|
+
const destDir = options.destDir || getCodexSkillsDir(options.homeDir);
|
|
176
211
|
const sourceDirs = options.sourceDirs || defaultSkillSourceDirs(projectRoot, options);
|
|
177
212
|
const overwrite = options.overwrite !== false;
|
|
178
213
|
|
|
@@ -205,7 +240,7 @@ async function installCodexNativeSkills(projectRoot, options = {}) {
|
|
|
205
240
|
async function setupCodexNative(projectRoot, options = {}) {
|
|
206
241
|
const log = options.quiet ? () => {} : (...a) => console.log(...a);
|
|
207
242
|
|
|
208
|
-
const present = await detectCodex();
|
|
243
|
+
const present = await detectCodex(options.homeDir);
|
|
209
244
|
if (!present && !options.force) {
|
|
210
245
|
log(chalk.gray(' · Codex CLI not detected (~/.codex absent), skipping'));
|
|
211
246
|
return { skipped: true, reason: 'codex-not-detected' };
|
|
@@ -22,7 +22,7 @@ const path = require('path');
|
|
|
22
22
|
const fs = require('fs-extra');
|
|
23
23
|
const chalk = require('chalk');
|
|
24
24
|
const inquirer = require('inquirer');
|
|
25
|
-
const { writeCredentials } = require('
|
|
25
|
+
const { writeCredentials } = require('../packages/platform-config');
|
|
26
26
|
|
|
27
27
|
const SA_FILENAME = 'google-sa.json';
|
|
28
28
|
|