Files
BetterDesk/web-nodejs/scripts/update-cli.js
UNITRONIX 9c6490f6ab fix(ci,update): unblock Web Console CI and non-root in-app updates.
Use npm install without a lockfile in CI, treat root-owned installer scripts as non-critical update failures, and skip privileged H-7 hooks when betterdesk already exists without sudo.
2026-06-06 14:46:41 +02:00

173 lines
6.4 KiB
JavaScript
Executable File

#!/usr/bin/env node
'use strict';
const readline = require('readline');
const updateService = require('../services/updateService');
const args = new Set(process.argv.slice(2));
const yes = args.has('--yes') || args.has('-y') || args.has('--auto');
const dryRun = args.has('--check') || args.has('--dry-run');
const createBackup = !args.has('--no-backup');
function getOptionValue(name, fallback) {
const prefix = `${name}=`;
const found = process.argv.slice(2).find(arg => arg.startsWith(prefix));
return found ? found.slice(prefix.length) : fallback;
}
const serverStrategy = getOptionValue('--server-strategy', 'auto');
function step(index, total, message) {
console.log(`[${index}/${total}] ${message}`);
}
function printComponents(components) {
const entries = Object.entries(components || {}).filter(([, info]) => info.changed);
if (!entries.length) return;
console.log('Changed components:');
for (const [name, info] of entries) {
const mode = info.autoUpdate ? 'auto' : 'manual';
console.log(` - ${info.label || name}: ${info.fileCount || 0} file(s), ${mode}`);
}
}
function ask(question) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer.trim());
});
});
}
function summarizeResult(result) {
console.log('Update summary:');
console.log(` Applied: ${result.applied?.length || 0}`);
console.log(` Removed: ${result.removed?.length || 0}`);
console.log(` Skipped: ${result.skipped?.length || 0}`);
console.log(` Backup: ${result.backupPath || 'not created'}`);
if (result.serverBuild) {
console.log(` Go server build: ${result.serverBuild.success ? 'ok' : 'failed'} (${result.serverBuild.method})`);
}
if (result.serverDeploy) {
console.log(` Go server deploy: ${result.serverDeploy.success ? 'ok' : 'failed'}`);
}
if (result.serverServiceConfig?.changed) {
console.log(` Service config: ${result.serverServiceConfig.changes.join('; ')}`);
}
if (result.toolchainInstall) {
console.log(` Go toolchain: ${result.toolchainInstall.success ? 'ready' : 'failed'}`);
}
if (result.failed?.length) {
console.log('Failures:');
for (const failure of result.failed) {
console.log(` - ${failure.file}: ${failure.error}`);
}
}
}
async function main() {
console.log('BetterDesk terminal updater');
console.log('Using the same update engine as the web console.');
console.log('');
step(1, 6, 'Checking remote update state');
const check = await updateService.checkForUpdates();
if (check.baselineEstablished) {
console.log(`Baseline established at ${check.remoteSHA.slice(0, 7)}. No update applied.`);
return;
}
if (!check.updateAvailable) {
console.log(`Already up to date (${check.remoteSHA.slice(0, 7)}).`);
return;
}
console.log(`Local SHA: ${check.localSHA || 'unknown'}`);
console.log(`Remote SHA: ${check.remoteSHA}`);
console.log(`Commits behind: ${check.commitsBehind}`);
printComponents(check.components);
step(2, 6, 'Loading changed file list');
const changedData = await updateService.getChangedFiles(check.remoteSHA);
console.log(`Changed files: ${changedData.totalFiles}`);
for (const commit of changedData.commits.slice(0, 5)) {
console.log(` - ${commit.sha} ${commit.message}`);
}
if (dryRun) {
console.log('Dry run complete. No files were changed.');
return;
}
if (!yes) {
const answer = await ask('Apply this update now? [y/N] ');
if (!/^y(es)?$/i.test(answer)) {
console.log('Update cancelled.');
return;
}
}
step(3, 6, createBackup ? 'Creating backup and applying files' : 'Applying files without backup');
const result = await updateService.applyUpdate(check.remoteSHA, changedData, {
createBackup,
serverStrategy
});
step(4, 6, 'Summarizing result');
summarizeResult(result);
// Distinguish critical failures (file download/write errors) from
// non-critical ones (server binary not available — source was still
// applied). See issue #154: server binary failures caused infinite
// update loop because SHA was never saved.
const criticalFailures = (result.failed || []).filter(f =>
!updateService.isNonCriticalUpdateFailure(f.file) && !f.nonCritical
);
const nonCriticalFailures = (result.failed || []).filter(f =>
updateService.isNonCriticalUpdateFailure(f.file) || f.nonCritical
);
if (criticalFailures.length) {
console.log(`\n${criticalFailures.length} critical failure(s) — update incomplete.`);
process.exitCode = 1;
return;
}
if (nonCriticalFailures.length) {
console.log(`\n${nonCriticalFailures.length} non-critical issue(s) (server binary not built/downloaded).`);
console.log('Console and script files were applied successfully. Rebuild Go server manually if needed.');
}
step(5, 6, 'Restarting affected services');
if (result.needsServerRestart) {
const serviceName = process.platform === 'win32' ? 'BetterDeskServer' : 'betterdesk-server';
const restart = updateService.restartService(serviceName);
if (restart.success) console.log(` - ${serviceName}: restarted`);
else {
console.log(` - ${serviceName}: restart failed (${restart.error})`);
process.exitCode = 1;
}
}
if (result.needsConsoleRestart) {
const serviceName = process.platform === 'win32' ? 'BetterDeskConsole' : 'betterdesk-console';
const restart = updateService.restartService(serviceName);
if (restart.success) console.log(` - ${serviceName}: restarted`);
else {
console.log(` - ${serviceName}: restart failed (${restart.error})`);
process.exitCode = 1;
}
}
if (!result.needsServerRestart && !result.needsConsoleRestart) {
console.log(' - No service restart required');
}
step(6, 6, 'Done');
if (result.shaSaved) {
console.log(`Updated baseline SHA to ${check.remoteSHA.slice(0, 7)}.`);
}
}
main().catch(err => {
console.error(`Update failed: ${err.message}`);
process.exitCode = 1;
});