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.
|
|
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(
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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:
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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(`
|
|
195
|
+
console.log(chalk.green(` FS migration applied (backup: ${r.backup})`));
|
|
201
196
|
}
|
|
202
197
|
}
|
|
203
198
|
} catch (e) {
|
|
204
|
-
console.warn(chalk.yellow(`
|
|
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
|
|
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(`
|
|
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(`
|
|
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
|
+
};
|