fix(install): preserve user fleet data on re-seed + refresh active units (CRITICAL) (#632)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #632.
This commit is contained in:
2026-06-22 21:38:09 +00:00
committed by jason.woltje
parent d539d61e0e
commit bf2a6745c8
9 changed files with 216 additions and 13 deletions

View File

@@ -14,7 +14,14 @@
*/
import { execSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
readdirSync,
copyFileSync,
} from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -536,6 +543,47 @@ export function readRosterAgentNames(mosaicHome = join(homedir(), '.config', 'mo
return names;
}
/**
* Refresh the ACTIVE systemd user units from the freshly re-seeded copies.
*
* The re-seed updates `~/.config/mosaic/systemd/user/*.service`, but the units
* systemd actually runs live at `~/.config/systemd/user/`. Without this copy,
* shipped unit fixes (e.g. the socket-env change) never take effect after
* `mosaic update` until `mosaic fleet install` is re-run. Best-effort + scoped:
* only refreshes when a fleet is already installed (the active dir already
* carries `mosaic-*` units), so non-fleet hosts are untouched.
*/
export function refreshActiveFleetUnits(
mosaicHome = join(homedir(), '.config', 'mosaic'),
env: NodeJS.ProcessEnv = process.env,
): { refreshed: string[]; ok: boolean; reason?: string } {
const src = join(mosaicHome, 'systemd', 'user');
const configHome = env['XDG_CONFIG_HOME'] ?? join(homedir(), '.config');
const dest = join(configHome, 'systemd', 'user');
if (!existsSync(src)) return { refreshed: [], ok: true };
// Only refresh when a fleet is already installed (active dir has mosaic units).
const fleetInstalled =
existsSync(dest) &&
readdirSync(dest).some((f) => f.startsWith('mosaic-') && f.endsWith('.service'));
if (!fleetInstalled) return { refreshed: [], ok: true };
const units = readdirSync(src).filter((f) => f.startsWith('mosaic-') && f.endsWith('.service'));
const refreshed: string[] = [];
for (const unit of units) {
try {
copyFileSync(join(src, unit), join(dest, unit));
refreshed.push(unit);
} catch {
// best-effort per unit
}
}
try {
execSync('systemctl --user daemon-reload', { stdio: 'ignore', timeout: 15_000 });
} catch {
// non-systemd host or no session bus — non-fatal
}
return { refreshed, ok: true };
}
/** Build the per-agent systemd relaunch commands (drain+relaunch via restart). */
export function buildRelaunchCommands(agentNames: string[]): string[][] {
return agentNames.map((name) => [