feat(fleet): add reviewed v1-to-v2 migration preview
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-07-15 22:04:51 -05:00
parent c1aecfabe9
commit 0e814324ca
21 changed files with 3282 additions and 45 deletions

View File

@@ -1,6 +1,7 @@
import { createHash, randomUUID } from 'node:crypto';
import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { compareCodePoints } from './deterministic-order.js';
export type AgentEnvironmentKind = 'generated' | 'local';
@@ -30,6 +31,15 @@ export interface AgentEnvironmentProjectionResult {
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
/** Sanitized, non-mutating projection evidence safe for migration output. */
export interface AgentEnvironmentProjectionPreview {
readonly agentName: string;
readonly generated: 'rebuild';
readonly legacy: 'absent' | 'regenerate-only' | 'relocate-local' | 'quarantine';
readonly relocatedKeys: readonly string[];
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
/** A projection fully validated without changing managed files. */
export interface PreparedAgentEnvironmentProjection {
readonly mosaicHome: string;
@@ -40,6 +50,7 @@ export interface PreparedAgentEnvironmentProjection {
readonly generated: string;
readonly local: string;
readonly legacy?: string;
readonly legacyRelocatedKeys: readonly string[];
readonly quarantinePath?: string;
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
@@ -186,6 +197,7 @@ export async function prepareAgentEnvironmentProjection(
generated,
local,
...(legacy === undefined ? {} : { legacy }),
legacyRelocatedKeys: Object.keys(legacyDisposition.localValues).sort(compareCodePoints),
...(quarantinePath === undefined ? {} : { quarantinePath }),
diagnostics: legacyDisposition.diagnostics,
};
@@ -208,6 +220,33 @@ export async function prepareAgentGeneratedProjectionDeletion(
return generatedPath;
}
export async function previewAgentEnvironmentProjection(
options: AgentEnvironmentProjectionOptions,
): Promise<AgentEnvironmentProjectionPreview> {
const prepared = await prepareAgentEnvironmentProjection(options);
const relocatedKeys = prepared.legacyRelocatedKeys;
const legacy =
prepared.legacy === undefined
? 'absent'
: prepared.diagnostics.length > 0
? 'quarantine'
: relocatedKeys.length > 0
? 'relocate-local'
: 'regenerate-only';
return {
agentName: options.agentName,
generated: 'rebuild',
legacy,
relocatedKeys,
diagnostics: [...prepared.diagnostics].sort((left, right): number =>
compareCodePoints(
`${left.key}:${left.code}:${left.sha256}`,
`${right.key}:${right.code}:${right.sha256}`,
),
),
};
}
/** Applies a previously prepared deterministic projection. */
export async function applyPreparedAgentEnvironmentProjection(
prepared: PreparedAgentEnvironmentProjection,
@@ -292,13 +331,13 @@ function renderLocalAgentEnvironment(values: Readonly<Record<string, string>>):
if (Object.keys(values).length === 0) return '';
const parsed = parseAgentEnvironment(
Object.entries(values)
.sort(([left], [right]): number => left.localeCompare(right))
.sort(([left], [right]): number => compareCodePoints(left, right))
.map(([key, value]): string => `${key}=${value}`)
.join('\n'),
'local',
);
return `${Object.entries(parsed)
.sort(([left], [right]): number => left.localeCompare(right))
.sort(([left], [right]): number => compareCodePoints(left, right))
.map(([key, value]): string => `${key}=${value}`)
.join('\n')}\n`;
}