* fix: make upstream sync actions target the selected remote Ensure fetch and pull actually honor upstream selection so fork maintenance works from the Git sidebar, and surface upstream branch status alongside the primary origin-tracking indicators. * feat: add Windows Electron desktop foundation * fix(electron): stabilize Windows desktop packaging * fix(electron): stabilize Windows desktop chrome Use native Windows titlebar behavior with an Alt-accessible hidden menu, and harden Windows dev command launching so the desktop app follows platform conventions. * fix(electron): stabilize Windows dev startup * fix(electron): clarify desktop artifact names * fix(electron): harden Windows desktop release and launch * fix(electron): address Windows release review * fix(electron): point updater and release links to org repo * Fix Windows settings persistence fallback * Fix Windows Electron dev startup * Add Windows Electron window controls * Fix Windows Electron install and opencode launch * fix: resolve git status for repositories without upstream Fixes repository detection stuck on Checking repository Handles git status when no upstream is configured Adds regression coverage for git status loading * Add Windows app menu button * fix: preserve file editor line endings * ci: add desktop release smoke workflow --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
91 lines
3.2 KiB
JavaScript
91 lines
3.2 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const repoRoot = path.resolve(__dirname, '..', '..', '..');
|
|
const webDir = path.join(repoRoot, 'packages', 'web');
|
|
const electronDir = path.join(repoRoot, 'packages', 'electron');
|
|
|
|
const resourcesDir = path.join(electronDir, 'resources');
|
|
const resourcesWebDistDir = path.join(resourcesDir, 'web-dist');
|
|
const webDistDir = path.join(webDir, 'dist');
|
|
|
|
const quoteWindowsCommandArg = (value) => `"${String(value).replace(/"/g, '""')}"`;
|
|
|
|
const run = (cmd, args, cwd) => {
|
|
const isWindowsCommandScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(cmd);
|
|
const result = isWindowsCommandScript
|
|
? spawnSync(
|
|
process.env.ComSpec || 'cmd.exe',
|
|
['/d', '/s', '/c', ['call', quoteWindowsCommandArg(cmd), ...args.map(quoteWindowsCommandArg)].join(' ')],
|
|
{ cwd, stdio: 'inherit', windowsVerbatimArguments: true },
|
|
)
|
|
: spawnSync(cmd, args, { cwd, stdio: 'inherit' });
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) {
|
|
throw new Error(`Command failed: ${cmd} ${args.join(' ')}`);
|
|
}
|
|
};
|
|
|
|
const resolveBun = () => {
|
|
if (typeof process.env.BUN === 'string' && process.env.BUN.trim()) {
|
|
return process.env.BUN.trim();
|
|
}
|
|
if (process.platform === 'win32') {
|
|
const result = spawnSync('where.exe', ['bun'], { encoding: 'utf8' });
|
|
const candidates = String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
const resolved = candidates.find((entry) => /\.(exe|cmd|bat)$/i.test(entry)) || candidates[0];
|
|
return resolved || 'bun';
|
|
}
|
|
const result = spawnSync('/bin/bash', ['-lc', 'command -v bun'], { encoding: 'utf8' });
|
|
const resolved = (result.stdout || '').trim();
|
|
return resolved || 'bun';
|
|
};
|
|
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
const removeDir = async (target) => {
|
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
try {
|
|
await fs.rm(target, { recursive: true, force: true });
|
|
return;
|
|
} catch (error) {
|
|
if (attempt === 4) throw error;
|
|
if (!['ENOTEMPTY', 'EBUSY', 'EPERM'].includes(error?.code)) throw error;
|
|
await sleep(100 * (attempt + 1));
|
|
}
|
|
}
|
|
};
|
|
|
|
const copyDir = async (src, dst) => {
|
|
await fs.mkdir(dst, { recursive: true });
|
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const from = path.join(src, entry.name);
|
|
const to = path.join(dst, entry.name);
|
|
if (entry.isDirectory()) {
|
|
await copyDir(from, to);
|
|
} else {
|
|
await fs.copyFile(from, to);
|
|
}
|
|
}
|
|
};
|
|
|
|
const bunExe = resolveBun();
|
|
|
|
console.log('[electron] building web UI dist...');
|
|
run(bunExe, ['run', 'build'], webDir);
|
|
|
|
console.log('[electron] staging packaged resources...');
|
|
await fs.mkdir(resourcesDir, { recursive: true });
|
|
const stagedWebDistDir = await fs.mkdtemp(path.join(resourcesDir, 'web-dist-staging-'));
|
|
await copyDir(webDistDir, stagedWebDistDir);
|
|
await removeDir(resourcesWebDistDir);
|
|
await fs.rename(stagedWebDistDir, resourcesWebDistDir);
|
|
|
|
console.log(`[electron] web assets ready: ${resourcesWebDistDir}`);
|