create-byan-agent 2.19.1 → 2.19.2

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/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [2.19.2] - 2026-06-02
11
+
12
+ ### Fixed - `update-byan-agent update` is non-destructive, non-interactive, and local
13
+
14
+ Field testing of the 2.19.1 updater surfaced four real bugs in
15
+ `update-byan-agent/bin/update-byan-agent.js` (the published template `_byan` was
16
+ verified healthy and is untouched):
17
+
18
+ - **No more redundant network install.** The updater runs via
19
+ `npx -p create-byan-agent@latest`, so the `@latest` package (and its template)
20
+ is already on disk next to the running bin. It now resolves the template from
21
+ the running package root (`path.resolve(__dirname, '..', '..')`, with a
22
+ `node_modules/create-byan-agent` fallback) instead of re-running
23
+ `npm install --no-save create-byan-agent@latest` into the user project (which
24
+ pulled ~215 packages, took minutes, had no timeout, and emitted no output).
25
+ The `.github/agents`, Claude-native, and fs-migration refreshes now resolve
26
+ from that same local root, so the F22 Gen3 stub refresh is no longer silently
27
+ skipped when the user project has no local `node_modules`.
28
+ - **Non-destructive rebuild.** The replacement template is validated as a
29
+ non-empty directory and fully staged beside the live tree before anything is
30
+ deleted; the swap is then two atomic renames. A failed or empty source aborts
31
+ the update with the existing `_byan` left intact (previously `_byan` was
32
+ `rm -rf`'d up front, so a failing install left the project relying on backup
33
+ rollback).
34
+ - **Non-interactive support.** `update` accepts `-y/--yes` and
35
+ `--non-interactive`, and auto-confirms when `--force`, `--yes`,
36
+ `--non-interactive`, or a non-TTY stdout is detected. In CI / headless / piped
37
+ runs the updater no longer hangs on the `Y/n` prompt.
38
+ - **Honest diagnostics.** A template that cannot be used now reports whether the
39
+ package could not be resolved at all vs. the template directory being present
40
+ but empty, and prints the probed paths — instead of the misleading
41
+ "_byan directory not found in npm package" that blamed a healthy package.
42
+
43
+ Covered by `update-byan-agent/__tests__/apply-update.test.js` (11 tests:
44
+ local resolution, full Gen2->Gen3 replace, stub refresh, swap atomicity, and the
45
+ destructive-safety guard). The updater bin now reports its real package version
46
+ instead of a stale hard-coded literal.
47
+
48
+ ---
49
+
10
50
  ## [2.19.1] - 2026-06-02
11
51
 
12
52
  ### Fixed - Agent stubs repointed to the by-type (Gen3) layout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.19.1",
3
+ "version": "2.19.2",
4
4
  "description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Copilot CLI, Claude Code, Codex). Merise Agile + TDD + 64 Mantras. ~54% LLM cost savings.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -6,16 +6,22 @@ const ora = require('ora');
6
6
  const inquirer = require('inquirer');
7
7
  const path = require('path');
8
8
  const fs = require('fs');
9
- const { execSync } = require('child_process');
10
9
 
11
10
  const Analyzer = require('../lib/analyzer');
12
11
  const Backup = require('../lib/backup');
13
12
  const CustomizationDetector = require('../lib/customization-detector');
13
+ const { applyUpdate, resolvePackageRoot } = require('../lib/apply-update');
14
+
15
+ // Read the version from this package, not a hand-maintained literal that drifts.
16
+ let UPDATER_VERSION = '0.0.0';
17
+ try {
18
+ UPDATER_VERSION = require('../package.json').version;
19
+ } catch { /* keep fallback */ }
14
20
 
15
21
  program
16
22
  .name('update-byan-agent')
17
23
  .description('Gestion des mises a jour BYAN avec detection de conflits')
18
- .version('2.6.1');
24
+ .version(UPDATER_VERSION);
19
25
 
20
26
  program
21
27
  .command('check')
@@ -58,37 +64,45 @@ program
58
64
  .description('Mettre a jour installation BYAN')
59
65
  .option('--dry-run', 'Analyser sans appliquer les changements')
60
66
  .option('--force', 'Forcer la mise a jour meme si deja a jour')
67
+ .option('-y, --yes', 'Mode non-interactif : confirmer automatiquement')
68
+ .option('--non-interactive', 'Alias de --yes (utile en CI / headless)')
61
69
  .action(async (options) => {
62
70
  const installPath = process.cwd();
63
-
71
+
64
72
  try {
65
73
  // Step 1: Check version
66
74
  const spinner = ora('Verification version...').start();
67
75
  const analyzer = new Analyzer(installPath);
68
76
  const versionInfo = await analyzer.checkVersion();
69
77
  spinner.succeed(`Version actuelle: ${versionInfo.current}, npm: ${versionInfo.latest}`);
70
-
78
+
71
79
  if (versionInfo.upToDate && !options.force) {
72
80
  console.log(chalk.green('\nBYAN est deja a jour!'));
73
81
  return;
74
82
  }
75
-
83
+
76
84
  if (options.dryRun) {
77
85
  console.log(chalk.cyan('\nMode dry-run: Aucune modification appliquee'));
78
86
  return;
79
87
  }
80
-
81
- // Step 2: Confirm update
82
- const { confirmUpdate } = await inquirer.prompt([{
83
- type: 'confirm',
84
- name: 'confirmUpdate',
85
- message: `Mettre a jour BYAN ${versionInfo.current} -> ${versionInfo.latest}?`,
86
- default: true
87
- }]);
88
-
89
- if (!confirmUpdate) {
90
- console.log(chalk.yellow('Mise a jour annulee'));
91
- return;
88
+
89
+ // Step 2: Confirm update. Skip the prompt when explicitly non-interactive
90
+ // (--yes / --non-interactive / --force) or when stdout is not a TTY (CI,
91
+ // pipe, headless) — otherwise the updater hangs on the Y/n in automation.
92
+ const autoConfirm =
93
+ options.yes || options.nonInteractive || options.force ||
94
+ !process.stdout.isTTY || !process.stdin.isTTY;
95
+ if (!autoConfirm) {
96
+ const { confirmUpdate } = await inquirer.prompt([{
97
+ type: 'confirm',
98
+ name: 'confirmUpdate',
99
+ message: `Mettre a jour BYAN ${versionInfo.current} -> ${versionInfo.latest}?`,
100
+ default: true
101
+ }]);
102
+ if (!confirmUpdate) {
103
+ console.log(chalk.yellow('Mise a jour annulee'));
104
+ return;
105
+ }
92
106
  }
93
107
 
94
108
  // Step 3: Detect customizations
@@ -130,66 +144,47 @@ program
130
144
  }
131
145
  preserveSpinner.succeed('Personnalisations sauvegardees');
132
146
 
133
- // Step 6: Download and install latest version
134
- const updateSpinner = ora('Telechargement derniere version...').start();
147
+ // Step 6: Rebuild from the running package template (no network install).
148
+ // The updater is launched via `npx -p create-byan-agent@latest`, so the
149
+ // @latest package (and its template) is already on disk next to this bin.
150
+ // We resolve it locally instead of re-installing it into the user project
151
+ // (BUG3), validate the template BEFORE deleting anything, and swap via
152
+ // rename so a failure never leaves _byan missing (BUG2).
153
+ const updateSpinner = ora('Reconstruction depuis le template du package...').start();
154
+ let pkgRoot;
135
155
  try {
136
- // Remove current _byan directory
137
- const byanDir = path.join(installPath, '_byan');
138
- if (fs.existsSync(byanDir)) {
139
- fs.rmSync(byanDir, { recursive: true, force: true });
140
- }
141
-
142
- // Run npm install to get latest create-byan-agent
143
- execSync('npm install --no-save create-byan-agent@latest', {
144
- cwd: installPath,
145
- stdio: 'pipe'
146
- });
147
-
148
- // Copy _byan from node_modules to project root. The published tarball
149
- // ships _byan source under install/templates/_byan/, not at the root.
150
- // Fall back to root-level _byan/ for legacy tarballs that may still
151
- // have shipped it there.
152
- const pkgRoot = path.join(installPath, 'node_modules', 'create-byan-agent');
153
- const candidates = [
154
- path.join(pkgRoot, 'install', 'templates', '_byan'),
155
- path.join(pkgRoot, '_byan'),
156
- ];
157
- const nodeModulesByan = candidates.find((p) => fs.existsSync(p));
158
- if (nodeModulesByan) {
159
- copyRecursive(nodeModulesByan, byanDir);
160
- } else {
161
- throw new Error(
162
- `_byan directory not found in npm package (looked in: ${candidates.map((p) => path.relative(pkgRoot, p)).join(', ')})`
163
- );
164
- }
156
+ const resolved = resolvePackageRoot({ installPath, binDir: __dirname });
157
+ pkgRoot = resolved.pkgRoot;
165
158
 
166
- // Also refresh .github/agents/ from templates (Copilot stubs)
167
- const ghAgentsSrc = path.join(pkgRoot, 'install', 'templates', '.github', 'agents');
168
- const ghAgentsDst = path.join(installPath, '.github', 'agents');
169
- if (fs.existsSync(ghAgentsSrc)) {
170
- if (fs.existsSync(ghAgentsDst)) {
171
- fs.rmSync(ghAgentsDst, { recursive: true, force: true });
172
- }
173
- fs.mkdirSync(path.dirname(ghAgentsDst), { recursive: true });
174
- copyRecursive(ghAgentsSrc, ghAgentsDst);
175
- }
159
+ const report = applyUpdate({ installPath, pkgRoot });
160
+ updateSpinner.succeed(
161
+ `Template applique (${report.byanEntries} entrees _byan` +
162
+ (report.githubAgentsEntries != null
163
+ ? `, ${report.githubAgentsEntries} stubs .github/agents` : '') +
164
+ `) depuis ${resolved.source}`
165
+ );
176
166
 
177
167
  // Refresh Claude Code native (.claude/hooks, .claude/skills,
178
- // .claude/agents, .claude/settings.json, .mcp.json, _byan/mcp/)
168
+ // .claude/agents, .claude/settings.json, .mcp.json, _byan/mcp/) from the
169
+ // SAME local package root.
170
+ const nativeSpinner = ora('Refresh Claude Code native...').start();
179
171
  try {
180
172
  const setupModule = path.join(pkgRoot, 'install', 'lib', 'claude-native-setup.js');
181
173
  if (fs.existsSync(setupModule)) {
182
174
  // eslint-disable-next-line import/no-dynamic-require, global-require
183
175
  const { setupClaudeNative } = require(setupModule);
184
- await setupClaudeNative(installPath, { installDeps: true, quiet: false });
176
+ await setupClaudeNative(installPath, { installDeps: true, quiet: true });
177
+ nativeSpinner.succeed('Claude Code native rafraichi');
178
+ } else {
179
+ nativeSpinner.info('Module claude-native-setup absent, refresh ignore');
185
180
  }
186
181
  } catch (e) {
187
- console.warn(chalk.yellow(`Claude native refresh skipped: ${e.message}`));
182
+ nativeSpinner.warn(`Claude native refresh ignore: ${e.message}`);
188
183
  }
189
184
 
190
185
  // FS migration (F11) — dormant by default. Acts only when explicitly
191
186
  // enabled (env BYAN_FS_MIGRATE=1 or _byan/_config/migrate-fs.enabled)
192
- // AND the legacy module layout is present. Backs up _byan/ first.
187
+ // AND the legacy module layout is present. Same local package root.
193
188
  try {
194
189
  const hookModule = path.join(pkgRoot, 'install', 'lib', 'fs-migration-hook.js');
195
190
  if (fs.existsSync(hookModule)) {
@@ -197,22 +192,22 @@ program
197
192
  const { runFsMigration } = require(hookModule);
198
193
  const r = runFsMigration({ projectRoot: installPath });
199
194
  if (r.ran) {
200
- console.log(chalk.green(` FS migration applied (backup: ${r.backup})`));
195
+ console.log(chalk.green(` FS migration applied (backup: ${r.backup})`));
201
196
  }
202
197
  }
203
198
  } catch (e) {
204
- console.warn(chalk.yellow(` FS migration skipped: ${e.message}`));
199
+ console.warn(chalk.yellow(` FS migration skipped: ${e.message}`));
205
200
  }
206
-
207
- updateSpinner.succeed('Derniere version installee');
208
201
  } catch (error) {
209
- updateSpinner.fail('Erreur installation');
210
-
211
- // Rollback
202
+ updateSpinner.fail('Erreur reconstruction');
203
+
204
+ // Rollback. With the atomic-swap rebuild, _byan is only ever replaced
205
+ // after a validated stage, so most failures happen before destruction;
206
+ // the backup restore is the belt-and-suspenders net.
212
207
  const rollbackSpinner = ora('Restauration backup...').start();
213
208
  await backup.restore(backupPath);
214
209
  rollbackSpinner.succeed('Backup restaure');
215
-
210
+
216
211
  throw error;
217
212
  }
218
213
 
@@ -253,9 +248,9 @@ program
253
248
  const { runMigration } = require('../lib/migrate-mcp-config');
254
249
  const result = await runMigration(process.cwd(), { verbose: false });
255
250
  if (result.migrated) {
256
- console.log(chalk.green(` .mcp.json migrated (${result.changes.length} change${result.changes.length > 1 ? 's' : ''})`));
251
+ console.log(chalk.green(` .mcp.json migrated (${result.changes.length} change${result.changes.length > 1 ? 's' : ''})`));
257
252
  } else if (result.reason === 'no-token-available') {
258
- console.log(chalk.yellow(` ${result.hint}`));
253
+ console.log(chalk.yellow(` ${result.hint}`));
259
254
  }
260
255
  // silent on already-ok / no-mcp-json / no-byan-server
261
256
  } catch (err) {
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Core of `update-byan-agent update`: rebuild the installed _byan tree and the
5
+ * Copilot stubs from the create-byan-agent template, without a network install
6
+ * and without ever leaving _byan deleted on failure.
7
+ *
8
+ * Design constraints (field-reported bugs this module fixes):
9
+ * - BUG3 : the updater already runs from the @latest package (npx -p ...), so
10
+ * the template lives next to the running bin. Resolve it locally instead of
11
+ * re-running `npm install create-byan-agent@latest` into the user project.
12
+ * - BUG2 : validate the replacement source on disk BEFORE touching the live
13
+ * _byan, then swap via rename (stage -> rename), so a failure never leaves
14
+ * the project without a _byan.
15
+ * - BUG1 : a usable-template failure reports whether the package could not be
16
+ * resolved at all vs. the template dir being present but empty, with the
17
+ * probed paths, instead of a flat "not found in npm package".
18
+ *
19
+ * Pure module: no commander, no inquirer, no spinners, no network. The bin
20
+ * wires UI/version/backup/customization-preservation around it.
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ /** True when p is a directory that contains at least one entry. */
27
+ function isNonEmptyDir(p) {
28
+ try {
29
+ if (!fs.statSync(p).isDirectory()) return false;
30
+ return fs.readdirSync(p).length > 0;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ /** Recursively copy a directory tree (files + dirs), creating dest as needed. */
37
+ function copyRecursive(src, dest) {
38
+ fs.mkdirSync(dest, { recursive: true });
39
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
40
+ const srcPath = path.join(src, entry.name);
41
+ const destPath = path.join(dest, entry.name);
42
+ if (entry.isDirectory()) {
43
+ copyRecursive(srcPath, destPath);
44
+ } else if (entry.isFile()) {
45
+ fs.copyFileSync(srcPath, destPath);
46
+ }
47
+ // symlinks / specials are intentionally skipped (template ships none)
48
+ }
49
+ }
50
+
51
+ function rmrf(p) {
52
+ fs.rmSync(p, { recursive: true, force: true });
53
+ }
54
+
55
+ /**
56
+ * Resolve the create-byan-agent package root that holds install/templates.
57
+ * Preference order:
58
+ * 1. the running package itself (the bin lives at <pkg>/update-byan-agent/bin)
59
+ * 2. node_modules/create-byan-agent under the install path (legacy fallback)
60
+ *
61
+ * @param {object} opts
62
+ * @param {string} opts.installPath project root being updated
63
+ * @param {string} opts.binDir __dirname of the running bin
64
+ * @returns {{ pkgRoot: string, source: 'running-package'|'node_modules' }}
65
+ * @throws {Error} BUG1 diagnostic when no package root carries a template dir
66
+ */
67
+ function resolvePackageRoot({ installPath, binDir }) {
68
+ const runningPkg = path.resolve(binDir, '..', '..');
69
+ const nodeModulesPkg = path.join(installPath, 'node_modules', 'create-byan-agent');
70
+ const candidates = [
71
+ { pkgRoot: runningPkg, source: 'running-package' },
72
+ { pkgRoot: nodeModulesPkg, source: 'node_modules' },
73
+ ];
74
+
75
+ const probed = [];
76
+ for (const c of candidates) {
77
+ const tplDir = path.join(c.pkgRoot, 'install', 'templates');
78
+ const byanTpl = path.join(tplDir, '_byan');
79
+ probed.push(byanTpl);
80
+ if (isNonEmptyDir(byanTpl)) return c;
81
+ // Distinguish "present but empty" (real package defect) from "absent".
82
+ if (fs.existsSync(byanTpl)) {
83
+ throw new Error(
84
+ `create-byan-agent template is present but EMPTY at ${byanTpl}. ` +
85
+ `The package is malformed for this version; do not delete _byan. ` +
86
+ `Reinstall the package or republish.`
87
+ );
88
+ }
89
+ }
90
+
91
+ throw new Error(
92
+ `Could not resolve the create-byan-agent package template. Probed:\n` +
93
+ probed.map((p) => ` - ${p}`).join('\n') +
94
+ `\nThis means the running package layout is unexpected (not a template ` +
95
+ `absence in a healthy package). Re-run via ` +
96
+ `'npx -p create-byan-agent@latest update-byan-agent update'.`
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Replace a destination dir with a freshly staged copy of `src`, never leaving
102
+ * the destination missing for more than two atomic renames. The replacement
103
+ * source is fully staged (and validated non-empty) before the live dir is moved
104
+ * aside, so an interrupted/failed stage cannot destroy the existing content.
105
+ *
106
+ * @param {string} src validated, non-empty source dir
107
+ * @param {string} dest live dir to replace
108
+ * @param {string} label for error messages
109
+ * @returns {number} number of top-level entries staged
110
+ */
111
+ function stageAndSwap(src, dest, label) {
112
+ if (!isNonEmptyDir(src)) {
113
+ throw new Error(`Refusing to replace ${label}: source ${src} is missing or empty.`);
114
+ }
115
+
116
+ const staging = `${dest}.staging`;
117
+ const prev = `${dest}.prev`;
118
+ // Clean any residue from a prior interrupted run. The replacement is always
119
+ // rebuilt from the validated `src`, so a stale `${dest}.prev` is debris, not
120
+ // data to recover (the bin's separate _byan.backup is the crash net).
121
+ rmrf(staging);
122
+ rmrf(prev);
123
+
124
+ // 1. Stage the new content beside the destination. If this throws, the live
125
+ // dest is still untouched.
126
+ copyRecursive(src, staging);
127
+ if (!isNonEmptyDir(staging)) {
128
+ rmrf(staging);
129
+ throw new Error(`Staging ${label} produced no files (source ${src}).`);
130
+ }
131
+
132
+ // 2. Swap. Move the live dir aside (atomic), move staging into place
133
+ // (atomic), then drop the old one. On a swap failure, restore and leave no
134
+ // .staging debris behind.
135
+ const destExists = fs.existsSync(dest);
136
+ try {
137
+ if (destExists) fs.renameSync(dest, prev);
138
+ fs.renameSync(staging, dest);
139
+ } catch (err) {
140
+ if (destExists && !fs.existsSync(dest) && fs.existsSync(prev)) {
141
+ try { fs.renameSync(prev, dest); } catch { /* leave prev for manual recovery */ }
142
+ }
143
+ rmrf(staging);
144
+ throw err;
145
+ }
146
+
147
+ // The swap is committed. A failure to drop the old copy must NOT fail the
148
+ // update (e.g. transient EBUSY on a locked file); a stale .prev is cleaned by
149
+ // the next run's top-of-function rmrf.
150
+ try { rmrf(prev); } catch { /* best-effort */ }
151
+
152
+ return fs.readdirSync(dest).length;
153
+ }
154
+
155
+ /**
156
+ * Apply the file-system part of an update: rebuild _byan and refresh the
157
+ * Copilot stubs from the resolved package template.
158
+ *
159
+ * Caller is responsible for preserving/restoring user customizations around
160
+ * this call (config.yaml, memory, _byan-output, etc.).
161
+ *
162
+ * @param {object} opts
163
+ * @param {string} opts.installPath project root being updated
164
+ * @param {string} opts.pkgRoot resolved create-byan-agent package root
165
+ * @returns {{ byanEntries: number, githubAgentsEntries: number|null, templateRoot: string }}
166
+ */
167
+ function applyUpdate({ installPath, pkgRoot }) {
168
+ const templateRoot = path.join(pkgRoot, 'install', 'templates');
169
+ const byanSrc = path.join(templateRoot, '_byan');
170
+
171
+ // Validate BEFORE any destruction (BUG2). resolvePackageRoot already vetted
172
+ // this, but applyUpdate may be called directly, so re-check.
173
+ if (!isNonEmptyDir(byanSrc)) {
174
+ throw new Error(
175
+ `Template _byan is missing or empty at ${byanSrc}. ` +
176
+ `Aborting update without deleting the existing _byan.`
177
+ );
178
+ }
179
+
180
+ const byanDir = path.join(installPath, '_byan');
181
+ const byanEntries = stageAndSwap(byanSrc, byanDir, '_byan');
182
+
183
+ // Refresh Copilot stubs (.github/agents) from the template, same discipline.
184
+ // Optional: only when the template ships them.
185
+ let githubAgentsEntries = null;
186
+ const ghSrc = path.join(templateRoot, '.github', 'agents');
187
+ if (isNonEmptyDir(ghSrc)) {
188
+ const ghDst = path.join(installPath, '.github', 'agents');
189
+ fs.mkdirSync(path.dirname(ghDst), { recursive: true });
190
+ githubAgentsEntries = stageAndSwap(ghSrc, ghDst, '.github/agents');
191
+ }
192
+
193
+ return { byanEntries, githubAgentsEntries, templateRoot };
194
+ }
195
+
196
+ module.exports = {
197
+ applyUpdate,
198
+ resolvePackageRoot,
199
+ stageAndSwap,
200
+ isNonEmptyDir,
201
+ copyRecursive,
202
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "update-byan-agent",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI tool for managing BYAN updates with intelligent conflict detection and customization preservation",
5
5
  "bin": {
6
6
  "update-byan-agent": "bin/update-byan-agent.js"