feat(mosaic): mosaic update re-seeds framework + relaunches agents (R13) (#610)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #610.
This commit is contained in:
@@ -16,7 +16,8 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -453,6 +454,98 @@ export function getInstallAllCommand(outdated: PackageUpdateResult[]): string {
|
||||
return `npm i -g ${pkgs.join(' ')}`;
|
||||
}
|
||||
|
||||
// ─── Post-update framework re-seed + agent relaunch (F3-m3 / R13) ─────────────
|
||||
//
|
||||
// `mosaic update` installs the new npm CLI but, on its own, leaves the framework
|
||||
// files in ~/.config/mosaic/ stale — so shipped launcher/runtime changes (e.g.
|
||||
// the agent-name export + native heartbeat) never ACTIVATE until a re-seed.
|
||||
// These helpers run the package's own install.sh in sync-only mode (the P4
|
||||
// data-safe reconcile: framework-owned overwrite + backup-once; SOUL/USER/
|
||||
// *.local/credentials preserved) and, opt-in, relaunch durable agents.
|
||||
|
||||
/** Resolve the framework/ directory bundled in the installed package. */
|
||||
export function resolveBundledFrameworkRoot(): string {
|
||||
// dist/runtime/update-checker.js → ../../framework (package files: dist + framework)
|
||||
return resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
|
||||
}
|
||||
|
||||
export const FRAMEWORK_RESEED_PACKAGE = PKG;
|
||||
|
||||
/**
|
||||
* Build the framework re-seed invocation: the package's install.sh in
|
||||
* sync-only mode (file phase only — no environment-touching post-install),
|
||||
* keep mode (never overwrite user files). Returned as data so it is unit
|
||||
* testable; `runFrameworkReseed` executes it.
|
||||
*/
|
||||
export function buildReseedCommand(
|
||||
frameworkRoot: string,
|
||||
mosaicHome: string,
|
||||
): { installer: string; command: string; env: Record<string, string> } {
|
||||
const installer = join(frameworkRoot, 'install.sh');
|
||||
return {
|
||||
installer,
|
||||
command: `bash ${installer}`,
|
||||
env: {
|
||||
MOSAIC_SYNC_ONLY: '1',
|
||||
MOSAIC_INSTALL_MODE: 'keep',
|
||||
MOSAIC_HOME: mosaicHome,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-seed the framework from the freshly-installed package. Returns a result
|
||||
* describing what happened (so callers can message + decide on relaunch).
|
||||
* Best-effort: a missing installer or a non-zero exit is reported, not thrown.
|
||||
*/
|
||||
export function runFrameworkReseed(
|
||||
frameworkRoot = resolveBundledFrameworkRoot(),
|
||||
mosaicHome = join(homedir(), '.config', 'mosaic'),
|
||||
): { ok: boolean; reason?: string } {
|
||||
const { installer, command, env } = buildReseedCommand(frameworkRoot, mosaicHome);
|
||||
if (!existsSync(installer)) {
|
||||
return { ok: false, reason: `installer not found: ${installer}` };
|
||||
}
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit', env: { ...process.env, ...env }, timeout: 120_000 });
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort parse of the fleet roster for agent names (used to relaunch
|
||||
* durable agents after a re-seed). Returns [] when no roster exists.
|
||||
*/
|
||||
export function readRosterAgentNames(mosaicHome = join(homedir(), '.config', 'mosaic')): string[] {
|
||||
const rosterPath = join(mosaicHome, 'fleet', 'roster.yaml');
|
||||
if (!existsSync(rosterPath)) return [];
|
||||
let text: string;
|
||||
try {
|
||||
text = readFileSync(rosterPath, 'utf-8');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
// Roster agents are listed as `- name: <id>` entries under `agents:`.
|
||||
const names: string[] = [];
|
||||
for (const line of text.split('\n')) {
|
||||
const m = line.match(/^\s*-?\s*name:\s*["']?([A-Za-z0-9._-]+)["']?\s*$/);
|
||||
if (m && m[1]) names.push(m[1]);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Build the per-agent systemd relaunch commands (drain+relaunch via restart). */
|
||||
export function buildRelaunchCommands(agentNames: string[]): string[][] {
|
||||
return agentNames.map((name) => [
|
||||
'systemctl',
|
||||
'--user',
|
||||
'restart',
|
||||
`mosaic-agent@${name}.service`,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a table showing all packages with their current/latest versions.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user