fix(fleet): preserve agent env overrides on install #567
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaicstack/mosaic",
|
"name": "@mosaicstack/mosaic",
|
||||||
"version": "0.0.32",
|
"version": "0.0.33",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getDefaultOperatorSourceLabel,
|
getDefaultOperatorSourceLabel,
|
||||||
getRosterAgent,
|
getRosterAgent,
|
||||||
loadFleetRoster,
|
loadFleetRoster,
|
||||||
|
mergeAgentEnv,
|
||||||
registerFleetCommand,
|
registerFleetCommand,
|
||||||
resolveFleetPaths,
|
resolveFleetPaths,
|
||||||
type CommandRunner,
|
type CommandRunner,
|
||||||
@@ -121,6 +122,37 @@ describe('fleet roster parsing', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => {
|
||||||
|
const generated = [
|
||||||
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
|
'MOSAIC_AGENT_RUNTIME=codex',
|
||||||
|
'MOSAIC_AGENT_WORKDIR=/srv/new',
|
||||||
|
'MOSAIC_TMUX_SOCKET=mosaic-factory',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
const existing = [
|
||||||
|
'MOSAIC_AGENT_NAME=old-name',
|
||||||
|
'MOSAIC_AGENT_RUNTIME=old-runtime',
|
||||||
|
'MOSAIC_AGENT_WORKDIR=/srv/old',
|
||||||
|
'MOSAIC_TMUX_SOCKET=old-socket',
|
||||||
|
'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh',
|
||||||
|
'# site note',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
expect(mergeAgentEnv(generated, existing)).toBe(
|
||||||
|
[
|
||||||
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
|
'MOSAIC_AGENT_RUNTIME=codex',
|
||||||
|
'MOSAIC_AGENT_WORKDIR=/srv/new',
|
||||||
|
'MOSAIC_TMUX_SOCKET=mosaic-factory',
|
||||||
|
'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh',
|
||||||
|
'# site note',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
||||||
cleanup = await tempDir();
|
cleanup = await tempDir();
|
||||||
const rosterPath = join(cleanup, 'roster.yaml');
|
const rosterPath = join(cleanup, 'roster.yaml');
|
||||||
|
|||||||
@@ -148,6 +148,29 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeAgentEnv(generatedEnv: string, existingEnv?: string): string {
|
||||||
|
if (!existingEnv?.trim()) {
|
||||||
|
return generatedEnv;
|
||||||
|
}
|
||||||
|
const generatedKeys = new Set(
|
||||||
|
generatedEnv
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1])
|
||||||
|
.filter((key): key is string => key !== undefined),
|
||||||
|
);
|
||||||
|
const preservedLines = existingEnv.split('\n').filter((line) => {
|
||||||
|
if (!line.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const key = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1];
|
||||||
|
return key === undefined || !generatedKeys.has(key);
|
||||||
|
});
|
||||||
|
if (preservedLines.length === 0) {
|
||||||
|
return generatedEnv;
|
||||||
|
}
|
||||||
|
return [generatedEnv.trimEnd(), ...preservedLines, ''].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] {
|
export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] {
|
||||||
const service = agentName ? `mosaic-agent@${agentName}.service` : 'mosaic-tmux-holder.service';
|
const service = agentName ? `mosaic-agent@${agentName}.service` : 'mosaic-tmux-holder.service';
|
||||||
return ['systemctl', '--user', action, service];
|
return ['systemctl', '--user', action, service];
|
||||||
@@ -477,10 +500,9 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const agent of roster.agents) {
|
for (const agent of roster.agents) {
|
||||||
await writeFile(
|
const envPath = join(activePaths.agentEnvDir, `${agent.name}.env`);
|
||||||
join(activePaths.agentEnvDir, `${agent.name}.env`),
|
const existingEnv = (await canRead(envPath)) ? await readFile(envPath, 'utf8') : undefined;
|
||||||
generateAgentEnv(roster, agent),
|
await writeFile(envPath, mergeAgentEnv(generateAgentEnv(roster, agent), existingEnv));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Installed fleet files for ${roster.agents.length} agent(s).`);
|
console.log(`Installed fleet files for ${roster.agents.length} agent(s).`);
|
||||||
|
|||||||
Reference in New Issue
Block a user