Files
stack/packages/mosaic/src/commands/brain-doctor-check.ts
T
2026-08-05 14:50:45 -05:00

153 lines
4.9 KiB
TypeScript

import { homedir } from 'node:os';
import { join } from 'node:path';
import { z } from 'zod';
import { readBrainConfigSecure } from './brain-secure-config.js';
import { resolveBrainOwnerPolicy } from './brain-owner-resolver.js';
import { deriveBrainTarget } from './brain-store.js';
import {
collectBrainDoctorReport,
repairBrainDoctor,
type CommandRunner,
type DoctorRuntimeReport,
} from './brain-store-runtime.js';
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const manifestSchema = z
.object({
version: z.literal(2),
status: z.literal('committed'),
sourceRepo: z.string().min(1),
})
.passthrough();
export interface InstalledDoctorFinding {
readonly code: string;
readonly reasonCode: string | null;
}
export interface InstalledDoctorResult {
readonly status: 'ok' | 'warn' | 'error';
readonly findings: readonly InstalledDoctorFinding[];
readonly lines: readonly string[];
}
function configurationError(code: string, reasonCode = code): InstalledDoctorResult {
return {
status: 'error',
findings: [{ code, reasonCode }],
lines: [`[mosaic-doctor] [ERROR] ${code}`],
};
}
function renderReport(report: DoctorRuntimeReport): InstalledDoctorResult {
const findings = report.findings.map(
(finding): InstalledDoctorFinding => ({
code: finding.code,
reasonCode: finding.reasonCode,
}),
);
const hard = findings.some(
(finding): boolean =>
finding.code.endsWith('-error') ||
finding.code.endsWith('-indeterminate') ||
finding.code === 'brain-not-git-repository' ||
finding.code === 'brain-root-permissions-unsafe',
);
const status: InstalledDoctorResult['status'] =
findings.length === 0 ? 'ok' : hard ? 'error' : 'warn';
const severity = status === 'error' ? 'ERROR' : status === 'warn' ? 'WARN' : 'OK';
const lines =
findings.length === 0
? ['[mosaic-doctor] [OK] mosaic-brain ready']
: findings.map(
(finding): string =>
`[mosaic-doctor] [${severity}] ${finding.code}${
finding.reasonCode === null ? '' : ` reason=${finding.reasonCode}`
}`,
);
return { status, findings, lines };
}
export function runInstalledBrainDoctorCheck(
options: {
readonly mosaicHome: string;
readonly home: string;
readonly identity?: string;
readonly fix: boolean;
},
run: CommandRunner,
): InstalledDoctorResult {
if (options.identity === undefined || !IDENTITY.test(options.identity)) {
return configurationError('brain-identity-required', 'identity-required');
}
const registryPath = join(options.mosaicHome, 'cred', 'estates.json');
const manifestPath = join(options.mosaicHome, '.install-manifest.json');
const ownerPolicyPath = join(options.mosaicHome, 'brain', 'owners.json');
let registrySource: string;
try {
registrySource = readBrainConfigSecure(registryPath, options.mosaicHome);
} catch {
return configurationError('brain-estate-registry-unavailable');
}
let manifestSource: string;
try {
manifestSource = readBrainConfigSecure(manifestPath, options.mosaicHome);
} catch {
return configurationError('brain-install-manifest-unavailable');
}
let manifestRaw: unknown;
try {
manifestRaw = JSON.parse(manifestSource);
} catch {
return configurationError('brain-install-manifest-invalid');
}
const manifest = manifestSchema.safeParse(manifestRaw);
if (!manifest.success) return configurationError('brain-install-manifest-invalid');
let ownerPolicySource: string;
try {
ownerPolicySource = readBrainConfigSecure(ownerPolicyPath, options.mosaicHome);
} catch {
return configurationError('brain-owner-policy-unavailable');
}
let preliminaryTarget: ReturnType<typeof deriveBrainTarget>;
try {
preliminaryTarget = deriveBrainTarget(registrySource, manifest.data.sourceRepo, 'policy-probe');
} catch {
return configurationError('brain-estate-registry-invalid');
}
const ownerPolicy = resolveBrainOwnerPolicy(ownerPolicySource, preliminaryTarget.estate);
if (ownerPolicy === undefined) return configurationError('brain-owner-policy-invalid');
const input = {
registrySource,
targetGitUrl: manifest.data.sourceRepo,
brainNamespace: ownerPolicy.brainNamespace,
identity: options.identity,
root: join(options.home, '.mosaic'),
};
try {
return renderReport(
options.fix ? repairBrainDoctor(input, run) : collectBrainDoctorReport(input, run),
);
} catch {
return configurationError('brain-estate-registry-invalid');
}
}
export function defaultInstalledBrainDoctorOptions(fix: boolean): {
readonly mosaicHome: string;
readonly home: string;
readonly identity?: string;
readonly fix: boolean;
} {
const home = homedir();
const identity = process.env['MOSAIC_GIT_IDENTITY'];
return {
mosaicHome: process.env['MOSAIC_HOME'] ?? join(home, '.config', 'mosaic'),
home,
...(identity === undefined ? {} : { identity }),
fix,
};
}