* feat(electron): add Linux AppImage releases * ci: cache Linux OpenCode CLI artifacts * fix(ci): await Linux release inventory check * fix(electron): add frameless window controls on Linux desktop Linux AppImages were created without native WM decorations and without in-app controls, leaving users unable to close the window with a mouse. Treat Linux like Windows: frameless BrowserWindow plus the existing WindowsWindowControls header buttons and app-menu entry. macOS keeps hidden title bar with traffic lights unchanged. Shared usesFramelessElectronChrome() helper drives main window, mini chat, header insets, and titlebar controls. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * feat(desktop): add configurable window controls position by OS Add desktopWindowControlsPosition setting (auto/left/right) with OS-aware defaults: Linux left, Windows right. Wire frameless chrome controls in Header, TitlebarLeftControls, and MiniChatLayout, plus a Sessions settings control for Windows and Linux desktop shells. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(desktop): address Linux AppImage release review findings Propagate updater capability errors to the UI, treat missing latest-linux.yml feeds as no-update, stop installed-apps IPC spam on Linux, document FUSE/AppImage limits, add CHANGELOG entry, migrate remaining btriapitsyn URLs, and run Electron Linux unit tests on PRs. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: jibanez-staticduo <staticduo@gmail.com> Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
73 lines
2.8 KiB
JavaScript
73 lines
2.8 KiB
JavaScript
#!/usr/bin/env node
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
export const parseUpdateManifest = (content) => {
|
|
const version = content.match(/^version:\s*(\S+)\s*$/m)?.[1] || '';
|
|
const lines = content.split(/\r?\n/);
|
|
const files = [];
|
|
let entry = null;
|
|
for (const line of lines) {
|
|
const start = line.match(/^\s{2}-\s+(url|sha512|size|blockMapSize):\s*(\S+)\s*$/);
|
|
const field = start || line.match(/^\s{4}(url|sha512|size|blockMapSize):\s*(\S+)\s*$/);
|
|
if (start) {
|
|
if (entry) files.push(entry);
|
|
entry = {};
|
|
}
|
|
if (!field || !entry) continue;
|
|
const [, key, value] = field;
|
|
entry[key] = key === 'size' || key === 'blockMapSize' ? Number(value) : value;
|
|
}
|
|
if (entry) files.push(entry);
|
|
return {
|
|
version,
|
|
files: files.filter((file) => file.url && file.sha512 && Number.isSafeInteger(file.size)),
|
|
};
|
|
};
|
|
|
|
export const verifyUpdateManifest = ({ manifestPath, artifactPath, expectedVersion }) => {
|
|
const manifest = parseUpdateManifest(fs.readFileSync(manifestPath, 'utf8'));
|
|
const expectedName = path.basename(artifactPath);
|
|
if (manifest.version !== expectedVersion) {
|
|
throw new Error(`Update manifest version mismatch: expected ${expectedVersion}, got ${manifest.version || '(missing)'}`);
|
|
}
|
|
if (manifest.files.length !== 1) {
|
|
throw new Error(`Linux update manifest must contain exactly one artifact, got ${manifest.files.length}`);
|
|
}
|
|
const [entry] = manifest.files;
|
|
if (decodeURIComponent(path.basename(entry.url)) !== expectedName) {
|
|
throw new Error(`Update manifest artifact mismatch: expected ${expectedName}, got ${entry.url}`);
|
|
}
|
|
const bytes = fs.readFileSync(artifactPath);
|
|
if (entry.size !== bytes.length) {
|
|
throw new Error(`Update manifest size mismatch: expected ${bytes.length}, got ${entry.size}`);
|
|
}
|
|
const checksum = crypto.createHash('sha512').update(bytes).digest('base64');
|
|
if (entry.sha512 !== checksum) throw new Error('Update manifest sha512 mismatch');
|
|
return { name: expectedName, size: bytes.length, version: manifest.version };
|
|
};
|
|
|
|
const main = () => {
|
|
const [manifestPath, artifactPath, expectedVersion] = process.argv.slice(2);
|
|
if (!manifestPath || !artifactPath || !expectedVersion) {
|
|
throw new Error('Usage: verify-update-manifest.mjs <manifest> <artifact> <version>');
|
|
}
|
|
const result = verifyUpdateManifest({
|
|
manifestPath: path.resolve(manifestPath),
|
|
artifactPath: path.resolve(artifactPath),
|
|
expectedVersion,
|
|
});
|
|
console.log(`[electron] verified ${path.basename(manifestPath)} for ${result.name} (${result.size} bytes)`);
|
|
};
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
}
|
|
}
|