* 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>
108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
const dir = process.env.LATEST_YML_DIR;
|
|
const repo = process.env.GH_REPO;
|
|
const version = process.env.OPENCHAMBER_VERSION;
|
|
|
|
if (!dir) throw new Error('LATEST_YML_DIR is required');
|
|
if (!repo) throw new Error('GH_REPO is required');
|
|
if (!version) throw new Error('OPENCHAMBER_VERSION is required');
|
|
|
|
const parse = (content) => {
|
|
const lines = content.split('\n');
|
|
let releaseDate = '';
|
|
let parsedVersion = '';
|
|
const files = [];
|
|
let current;
|
|
|
|
const flush = () => {
|
|
if (current?.url && current?.sha512 && current?.size) {
|
|
files.push(current);
|
|
}
|
|
current = undefined;
|
|
};
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
const indented = line.startsWith(' ') || line.startsWith(' -');
|
|
if (line.startsWith('version:')) {
|
|
parsedVersion = line.slice('version:'.length).trim();
|
|
} else if (line.startsWith('releaseDate:')) {
|
|
releaseDate = line.slice('releaseDate:'.length).trim().replace(/^'|'$/g, '');
|
|
} else if (trimmed.startsWith('- url:')) {
|
|
flush();
|
|
current = { url: trimmed.slice('- url:'.length).trim() };
|
|
} else if (indented && current && trimmed.startsWith('sha512:')) {
|
|
current.sha512 = trimmed.slice('sha512:'.length).trim();
|
|
} else if (indented && current && trimmed.startsWith('size:')) {
|
|
current.size = Number(trimmed.slice('size:'.length).trim());
|
|
} else if (indented && current && trimmed.startsWith('blockMapSize:')) {
|
|
current.blockMapSize = Number(trimmed.slice('blockMapSize:'.length).trim());
|
|
} else if (!indented && current) {
|
|
flush();
|
|
}
|
|
}
|
|
|
|
flush();
|
|
return { version: parsedVersion, releaseDate, files };
|
|
};
|
|
|
|
const serialize = (data) => {
|
|
const lines = [`version: ${data.version}`, 'files:'];
|
|
for (const file of data.files) {
|
|
lines.push(` - url: ${file.url}`);
|
|
lines.push(` sha512: ${file.sha512}`);
|
|
lines.push(` size: ${file.size}`);
|
|
if (file.blockMapSize) {
|
|
lines.push(` blockMapSize: ${file.blockMapSize}`);
|
|
}
|
|
}
|
|
lines.push(`releaseDate: '${data.releaseDate}'`);
|
|
return `${lines.join('\n')}\n`;
|
|
};
|
|
|
|
const read = async (subdir, filename) => {
|
|
const filePath = path.join(dir, subdir, filename);
|
|
try {
|
|
return parse(await fs.readFile(filePath, 'utf8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const output = {};
|
|
|
|
const winX64 = await read('latest-yml-x86_64-pc-windows-msvc', 'latest.yml');
|
|
const winArm64 = await read('latest-yml-aarch64-pc-windows-msvc', 'latest.yml');
|
|
if (winX64 || winArm64) {
|
|
const base = winArm64 || winX64;
|
|
output['latest.yml'] = serialize({
|
|
version: base.version,
|
|
files: [...(winArm64?.files || []), ...(winX64?.files || [])],
|
|
releaseDate: base.releaseDate,
|
|
});
|
|
}
|
|
|
|
const macX64 = await read('latest-yml-x86_64-apple-darwin', 'latest-mac.yml');
|
|
const macArm64 = await read('latest-yml-aarch64-apple-darwin', 'latest-mac.yml');
|
|
if (macX64 || macArm64) {
|
|
const base = macArm64 || macX64;
|
|
output['latest-mac.yml'] = serialize({
|
|
version: base.version,
|
|
files: [...(macArm64?.files || []), ...(macX64?.files || [])],
|
|
releaseDate: base.releaseDate,
|
|
});
|
|
}
|
|
|
|
const tag = `v${version}`;
|
|
const tmp = process.env.RUNNER_TEMP || '/tmp';
|
|
for (const [filename, content] of Object.entries(output)) {
|
|
const outputPath = path.join(tmp, filename);
|
|
await fs.writeFile(outputPath, content);
|
|
console.log(`prepared ${outputPath} for upload to ${repo} release ${tag}`);
|
|
}
|
|
|
|
console.log('finalized latest yml files');
|