site-mariage/_byan/mcp/byan-mcp-server/lib/update.js
Corentin Joguet bff653acd6 first commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:30:37 +02:00

127 lines
4.5 KiB
JavaScript

/**
* BYAN update lifecycle helper for the MCP server.
*
* Read-only check: compares the installed BYAN version (from
* <projectRoot>/_byan/.manifest.json, falling back to package.json) against
* the latest published version on the npm registry.
*
* Apply path: returns *instructions* (a shell command) rather than executing
* anything itself. Update is destructive (overwrites files) and must remain
* an explicit user action, gated through the regular yanstaller pipeline
* (`npx create-byan-agent update`).
*/
import fsPromises from 'node:fs/promises';
import nodePath from 'node:path';
import https from 'node:https';
export async function checkForUpdate(projectRoot) {
const installed = await getInstalledVersion(projectRoot);
let latest;
let networkError;
try {
latest = await getLatestVersion('create-byan-agent');
} catch (err) {
networkError = err.message || String(err);
}
if (!latest) {
return {
installed,
latest: null,
updateAvailable: false,
networkError,
note: 'Latest version unknown — npm registry unreachable. Skipped silently.',
};
}
if (installed === 'unknown') {
return {
installed,
latest,
updateAvailable: false,
note: '_byan/.manifest.json missing — installed version cannot be determined. The project may be a fresh manual setup; consider running `npx create-byan-agent` to write the manifest.',
};
}
const cmp = compareVersions(installed, latest);
return {
installed,
latest,
updateAvailable: cmp < 0,
isCurrent: cmp >= 0,
delta: cmp < 0 ? 'behind' : cmp > 0 ? 'ahead' : 'same',
};
}
export function formatApplyInstructions({ preview = false, force = false } = {}) {
const args = ['update'];
if (preview) args.push('--preview');
if (force) args.push('--force');
return {
command: `npx create-byan-agent ${args.join(' ')}`,
rationale: preview
? 'Preview mode — no files are written. Inspects diff against the latest npm template.'
: 'Apply update via the yanstaller pipeline. yanstaller backs up the project, diffs vs latest npm template, and merges only non-user-modified files unless --force is set.',
safety: [
'Backup is automatic before any write.',
'User-modified files are preserved (unless --force).',
'A rollback command (`npx create-byan-agent rollback`) is available afterwards.',
],
next: 'Ask the user to run the command above. Do not execute it from inside this tool.',
};
}
async function getInstalledVersion(projectRoot) {
// _byan/.manifest.json is the canonical source — written by yanstaller after
// every install/update. byan-mcp-server/package.json carries an unrelated
// local version and would lie if used as fallback, so we don't fall back at
// all: return 'unknown' and let callers surface that honestly.
const manifestPath = nodePath.join(projectRoot, '_byan', '.manifest.json');
try {
const raw = await fsPromises.readFile(manifestPath, 'utf8');
const manifest = JSON.parse(raw);
if (manifest && manifest.version) return manifest.version;
} catch {}
return 'unknown';
}
function getLatestVersion(packageName, { timeoutMs = 5000 } = {}) {
const url = `https://registry.npmjs.org/${packageName}/latest`;
return new Promise((resolve, reject) => {
const req = https.get(
url,
{ headers: { Accept: 'application/json' } },
(res) => {
if (res.statusCode !== 200) {
reject(new Error(`npm registry returned ${res.statusCode} for ${packageName}`));
res.resume();
return;
}
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
resolve(JSON.parse(data).version);
} catch (err) {
reject(new Error(`Failed to parse npm registry response: ${err.message}`));
}
});
}
);
req.on('error', (err) => reject(new Error(`Failed to reach npm registry: ${err.message}`)));
req.setTimeout(timeoutMs, () => {
req.destroy(new Error(`npm registry request timed out after ${timeoutMs}ms`));
});
});
}
function compareVersions(a, b) {
const pa = String(a).replace(/^v/, '').split('.').map((n) => Number(n) || 0);
const pb = String(b).replace(/^v/, '').split('.').map((n) => Number(n) || 0);
const len = Math.max(pa.length, pb.length);
for (let i = 0; i < len; i++) {
const na = pa[i] || 0;
const nb = pb[i] || 0;
if (na < nb) return -1;
if (na > nb) return 1;
}
return 0;
}