Compare commits
1 Commits
feat/h1-he
...
fix/framew
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a88ff8e232 |
@@ -30,6 +30,7 @@ import {
|
||||
refreshActiveFleetUnits,
|
||||
readRosterAgentNames,
|
||||
buildRelaunchCommands,
|
||||
checkFrameworkDrift,
|
||||
FRAMEWORK_RESEED_PACKAGE,
|
||||
} from './runtime/update-checker.js';
|
||||
import { runWizard } from './wizard.js';
|
||||
@@ -418,6 +419,48 @@ program
|
||||
// checkForAllUpdates imported statically above
|
||||
const { execSync } = await import('node:child_process');
|
||||
|
||||
// Re-seed the framework from the freshly-installed package, propagate shipped
|
||||
// systemd unit fixes to the active units, and (opt-in) relaunch durable
|
||||
// agents. Shared by the "packages updated" and the "framework drift" paths.
|
||||
const reseedFramework = (reason: string): void => {
|
||||
console.log(reason);
|
||||
const reseed = runFrameworkReseed();
|
||||
if (!reseed.ok) {
|
||||
console.error(
|
||||
`\n⚠ Framework re-seed skipped: ${reseed.reason ?? 'unknown'}.\n` +
|
||||
' Activate manually: bash "$(npm root -g)/@mosaicstack/mosaic/framework/install.sh" ' +
|
||||
'(MOSAIC_SYNC_ONLY=1 MOSAIC_INSTALL_MODE=keep)',
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log('✔ Framework re-seeded.');
|
||||
// Propagate shipped systemd unit fixes to the ACTIVE units (re-seed only
|
||||
// touches ~/.config/mosaic/systemd/user; systemd runs ~/.config/systemd/user).
|
||||
const units = refreshActiveFleetUnits();
|
||||
if (units.refreshed.length > 0) {
|
||||
console.log(`✔ Refreshed ${units.refreshed.length} active systemd unit(s).`);
|
||||
}
|
||||
const agents = readRosterAgentNames();
|
||||
if (agents.length === 0) return;
|
||||
if (opts.relaunch) {
|
||||
console.log(`\nRelaunching ${agents.length} fleet agent(s) to pick up the new runtime…`);
|
||||
for (const restart of buildRelaunchCommands(agents)) {
|
||||
try {
|
||||
execSync(restart.join(' '), { stdio: 'inherit', timeout: 30_000 });
|
||||
} catch {
|
||||
console.error(` ⚠ failed to restart agent — run: ${restart.join(' ')}`);
|
||||
}
|
||||
}
|
||||
console.log('✔ Agents relaunched.');
|
||||
} else {
|
||||
console.log(
|
||||
`\nℹ ${agents.length} fleet agent(s) are still running the previous runtime. ` +
|
||||
'Restart them to activate the update:\n mosaic update --relaunch ' +
|
||||
'(or: mosaic fleet restart <agent>)',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Checking for updates…');
|
||||
const results = checkForAllUpdates({ skipCache: true });
|
||||
|
||||
@@ -432,6 +475,18 @@ program
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\n✔ All packages up to date.');
|
||||
// #642: the CLI may have been upgraded outside `mosaic update` (e.g. a
|
||||
// direct `npm i -g`), leaving the framework files stale even though no
|
||||
// package is reported outdated. Detect that via the framework version and
|
||||
// re-seed so shipped launcher/runtime fixes still activate.
|
||||
const drift = checkFrameworkDrift();
|
||||
if (drift.drifted && opts.reseed !== false) {
|
||||
reseedFramework(
|
||||
`\nFramework drift detected (on-disk v${drift.installed} < bundled v${drift.bundled}) — ` +
|
||||
'the CLI was updated outside `mosaic update`. Re-seeding framework files into ' +
|
||||
'~/.config/mosaic (data-safe; keeps your edits)…',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -456,52 +511,17 @@ program
|
||||
// F3-m3 / R13: the CLI is updated, but the framework files in
|
||||
// ~/.config/mosaic/ are still the previous version. Re-seed them from the
|
||||
// freshly-installed package so shipped launcher/runtime changes ACTIVATE.
|
||||
// Only when the framework-bearing package itself updated.
|
||||
// Re-seed when the framework-bearing package itself updated OR the on-disk
|
||||
// framework is older than the freshly-installed one (#642 — e.g. only
|
||||
// sibling packages were outdated but the CLI was already ahead).
|
||||
const mosaicUpdated = outdated.some(
|
||||
(r: { package: string }) => r.package === FRAMEWORK_RESEED_PACKAGE,
|
||||
);
|
||||
if (mosaicUpdated && opts.reseed !== false) {
|
||||
console.log(
|
||||
const drift = checkFrameworkDrift();
|
||||
if ((mosaicUpdated || drift.drifted) && opts.reseed !== false) {
|
||||
reseedFramework(
|
||||
'\nRe-seeding framework files into ~/.config/mosaic (data-safe; keeps your edits)…',
|
||||
);
|
||||
const reseed = runFrameworkReseed();
|
||||
if (reseed.ok) {
|
||||
console.log('✔ Framework re-seeded.');
|
||||
// Propagate shipped systemd unit fixes to the ACTIVE units (re-seed only
|
||||
// touches ~/.config/mosaic/systemd/user; systemd runs ~/.config/systemd/user).
|
||||
const units = refreshActiveFleetUnits();
|
||||
if (units.refreshed.length > 0) {
|
||||
console.log(`✔ Refreshed ${units.refreshed.length} active systemd unit(s).`);
|
||||
}
|
||||
const agents = readRosterAgentNames();
|
||||
if (agents.length > 0) {
|
||||
if (opts.relaunch) {
|
||||
console.log(
|
||||
`\nRelaunching ${agents.length} fleet agent(s) to pick up the new runtime…`,
|
||||
);
|
||||
for (const restart of buildRelaunchCommands(agents)) {
|
||||
try {
|
||||
execSync(restart.join(' '), { stdio: 'inherit', timeout: 30_000 });
|
||||
} catch {
|
||||
console.error(` ⚠ failed to restart agent — run: ${restart.join(' ')}`);
|
||||
}
|
||||
}
|
||||
console.log('✔ Agents relaunched.');
|
||||
} else {
|
||||
console.log(
|
||||
`\nℹ ${agents.length} fleet agent(s) are still running the previous runtime. ` +
|
||||
'Restart them to activate the update:\n mosaic update --relaunch ' +
|
||||
'(or: mosaic fleet restart <agent>)',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`\n⚠ Framework re-seed skipped: ${reseed.reason ?? 'unknown'}.\n` +
|
||||
' Activate manually: bash "$(npm root -g)/@mosaicstack/mosaic/framework/install.sh" ' +
|
||||
'(MOSAIC_SYNC_ONLY=1 MOSAIC_INSTALL_MODE=keep)',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
readRosterAgentNames,
|
||||
runFrameworkReseed,
|
||||
refreshActiveFleetUnits,
|
||||
readInstalledFrameworkVersion,
|
||||
readBundledFrameworkVersion,
|
||||
checkFrameworkDrift,
|
||||
} from './update-checker.js';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
|
||||
@@ -123,3 +126,73 @@ describe('refreshActiveFleetUnits', () => {
|
||||
expect(existsSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* #642: re-seed when the on-disk framework is older than the bundled one even
|
||||
* if no package is reported outdated (CLI upgraded outside `mosaic update`).
|
||||
*/
|
||||
describe('framework drift detection', () => {
|
||||
let home: string; // stand-in for ~/.config/mosaic
|
||||
let fw: string; // stand-in for the bundled framework root
|
||||
|
||||
beforeEach(() => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'mosaic-drift-'));
|
||||
home = join(root, 'mosaic');
|
||||
fw = join(root, 'framework');
|
||||
mkdirSync(home, { recursive: true });
|
||||
mkdirSync(fw, { recursive: true });
|
||||
});
|
||||
afterEach(() => {
|
||||
rmSync(join(home, '..'), { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const writeInstalled = (v: string) => writeFileSync(join(home, '.framework-version'), v);
|
||||
const writeBundled = (v: string) =>
|
||||
writeFileSync(join(fw, 'install.sh'), `#!/usr/bin/env bash\nFRAMEWORK_VERSION=${v}\n`);
|
||||
|
||||
describe('readInstalledFrameworkVersion', () => {
|
||||
it('returns undefined when the version file is absent', () => {
|
||||
expect(readInstalledFrameworkVersion(home)).toBeUndefined();
|
||||
});
|
||||
it('parses the integer (tolerating surrounding whitespace)', () => {
|
||||
writeInstalled(' 3\n');
|
||||
expect(readInstalledFrameworkVersion(home)).toBe(3);
|
||||
});
|
||||
it('returns undefined for non-numeric content', () => {
|
||||
writeInstalled('not-a-number\n');
|
||||
expect(readInstalledFrameworkVersion(home)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('readBundledFrameworkVersion', () => {
|
||||
it('returns undefined when install.sh is absent', () => {
|
||||
expect(readBundledFrameworkVersion(fw)).toBeUndefined();
|
||||
});
|
||||
it('parses FRAMEWORK_VERSION=<n> from install.sh', () => {
|
||||
writeBundled('4');
|
||||
expect(readBundledFrameworkVersion(fw)).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkFrameworkDrift', () => {
|
||||
it('reports drift when on-disk is older than bundled', () => {
|
||||
writeInstalled('3');
|
||||
writeBundled('4');
|
||||
expect(checkFrameworkDrift(home, fw)).toEqual({ drifted: true, installed: 3, bundled: 4 });
|
||||
});
|
||||
it('no drift when versions match', () => {
|
||||
writeInstalled('4');
|
||||
writeBundled('4');
|
||||
expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false });
|
||||
});
|
||||
it('no drift when on-disk is newer than bundled', () => {
|
||||
writeInstalled('5');
|
||||
writeBundled('4');
|
||||
expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false });
|
||||
});
|
||||
it('no drift (conservative) when a version cannot be read', () => {
|
||||
writeBundled('4'); // installed version file missing
|
||||
expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false, bundled: 4 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -521,6 +521,75 @@ export function runFrameworkReseed(
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Framework drift detection (#642) ────────────────────────────────────────
|
||||
//
|
||||
// `mosaic update` only re-seeds the framework when the @mosaicstack/mosaic
|
||||
// package itself is upgraded *within that command*. When the CLI is upgraded
|
||||
// some OTHER way — a direct `npm i -g @mosaicstack/mosaic`, or an upgrade run
|
||||
// where only sibling packages were outdated — the framework files in
|
||||
// ~/.config/mosaic stay stale and shipped launcher/runtime fixes never
|
||||
// activate. Comparing the on-disk framework schema version against the version
|
||||
// bundled in the installed package detects exactly that situation.
|
||||
|
||||
/** Read the framework schema version recorded on disk (~/.config/mosaic/.framework-version). */
|
||||
export function readInstalledFrameworkVersion(
|
||||
mosaicHome = join(homedir(), '.config', 'mosaic'),
|
||||
): number | undefined {
|
||||
const vf = join(mosaicHome, '.framework-version');
|
||||
if (!existsSync(vf)) return undefined;
|
||||
try {
|
||||
const n = parseInt(readFileSync(vf, 'utf-8').trim(), 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the framework schema version shipped in the installed package by parsing
|
||||
* `FRAMEWORK_VERSION=<n>` out of the bundled install.sh (the authoritative
|
||||
* source the installer writes to .framework-version).
|
||||
*/
|
||||
export function readBundledFrameworkVersion(
|
||||
frameworkRoot = resolveBundledFrameworkRoot(),
|
||||
): number | undefined {
|
||||
const installer = join(frameworkRoot, 'install.sh');
|
||||
if (!existsSync(installer)) return undefined;
|
||||
try {
|
||||
const m = readFileSync(installer, 'utf-8').match(/^\s*FRAMEWORK_VERSION=(\d+)/m);
|
||||
const raw = m?.[1];
|
||||
if (!raw) return undefined;
|
||||
const n = parseInt(raw, 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FrameworkDrift {
|
||||
/** True only when both versions are known AND the on-disk one is older. */
|
||||
drifted: boolean;
|
||||
installed?: number;
|
||||
bundled?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the on-disk framework is older than the framework bundled in
|
||||
* the installed CLI (#642). Conservative: if either version can't be read the
|
||||
* result is no-drift, so a missing/unreadable version file never triggers an
|
||||
* unexpected re-seed.
|
||||
*/
|
||||
export function checkFrameworkDrift(
|
||||
mosaicHome = join(homedir(), '.config', 'mosaic'),
|
||||
frameworkRoot = resolveBundledFrameworkRoot(),
|
||||
): FrameworkDrift {
|
||||
const installed = readInstalledFrameworkVersion(mosaicHome);
|
||||
const bundled = readBundledFrameworkVersion(frameworkRoot);
|
||||
const drifted =
|
||||
typeof installed === 'number' && typeof bundled === 'number' && installed < bundled;
|
||||
return { drifted, installed, bundled };
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort parse of the fleet roster for agent names (used to relaunch
|
||||
* durable agents after a re-seed). Returns [] when no roster exists.
|
||||
|
||||
Reference in New Issue
Block a user