153 lines
4.9 KiB
TypeScript
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,
|
|
};
|
|
}
|