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.
@@ -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: false,
185
- claude: false
193
+ codex: resolveCommandExists('codex', { home }),
194
+ claude: resolveCommandExists('claude', { home }),
186
195
  };
187
196
 
188
- // Codex detection (binary + config, NOT project .codex/ folder)
189
- try {
190
- const result = execSync('which codex 2>/dev/null', { encoding: 'utf8' }).trim();
191
- if (result) {
192
- platforms.codex = true;
193
- }
194
- } catch (e) {
195
- // Fallback: check config directory
196
- const codexPaths = [
197
- path.join(os.homedir(), '.config', 'codex'),
198
- path.join(os.homedir(), '.codex')
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
- if (options.launch !== false && isTTY) {
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
+ };
@@ -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
- const credsResult = await sharedWriteCredentials({ BYAN_API_URL: stripApiSuffix(inputs.apiUrl), BYAN_API_TOKEN: inputs.token });
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) {
@@ -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
- function getCodexConfigPath() {
25
- return path.join(os.homedir(), '.codex', 'config.toml');
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(os.homedir(), '.codex', 'skills');
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(os.homedir(), '.codex'));
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
- throw new Error(`Cannot encode value with single quote in TOML literal: ${s}`);
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
- const apiUrl =
107
- options.apiUrl ||
108
- process.env.BYAN_API_URL ||
109
- 'http://localhost:3737';
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
- await fs.writeFile(configPath, merged, 'utf8');
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' };