#!/usr/bin/env node // mosaic business validate // mosaic business resolve [--project ] // // validate loads the business file, every role definition it uses, the // project files that exist under its declared roots, and stat-checks every // credential reference. resolve prints one role instance's resolved record. // Both print JSON on stdout and problems on stderr. // // Exit codes: 0 ok; 2 invalid (business, role, project file or a credential // reference); 3 system config problem; 4 usage, or a required file missing. import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { BusinessError } from "./errors.mjs"; import { loadBusiness } from "./business.mjs"; import { loadProject, projectFilePath } from "./project.mjs"; import { checkCredentialRef } from "./credentials.mjs"; import { systemVars, resolveInstance } from "./resolve.mjs"; const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); const USAGE = "usage: mosaic business validate | resolve [--project ]"; function fail(code, message) { process.stderr.write(`mosaic business: ${message}\n`); process.exit(code); } function loadSystem() { const proc = spawnSync(process.execPath, [join(REPO, "scripts", "mosaic-config.mjs"), "validate"], { encoding: "utf8", maxBuffer: 1024 * 1024, }); if (proc.status !== 0) fail(3, `system config problem (mosaic-config exit ${proc.status}): ${proc.stderr.trim()}`); try { return JSON.parse(proc.stdout); } catch { fail(3, "system config: mosaic-config printed something that isn't JSON"); } } function rolesDir() { return resolve(process.env.MOSAIC_ROLES_DIR || join(REPO, "roles")); } // Check credential references; print warnings; return the problem list. function checkRefs(refs, forbiddenRoots) { const problems = []; const warnings = []; for (const ref of refs) { const result = checkCredentialRef(ref, { forbiddenRoots }); problems.push(...result.problems); warnings.push(...result.warnings); } return { problems, warnings }; } function forbiddenRoots(config, business) { return [REPO, config.dataRoot, ...Object.values(business.projects).map((p) => p.root)]; } function validate(args) { if (args.length !== 1) fail(4, USAGE); const config = loadSystem(); const system = systemVars(config); const business = loadBusiness(args[0], { rolesDir: rolesDir() }); const projects = {}; const warnings = []; for (const [id, entry] of Object.entries(business.projects)) { const file = projectFilePath(entry.root); if (!existsSync(file)) { projects[id] = "absent"; warnings.push(`project ${id}: no project file at ${file}; it has no project variables`); continue; } const project = loadProject(entry.root); if (project.id !== id) fail(2, `project file ${file} has id ${project.id}, but business ${business.id} declares it as ${id}`); for (const instance of Object.keys(business.roles)) resolveInstance({ system, business, project, instance }); projects[id] = "valid"; } const refs = [ ...Object.values(business.tracker.sync.credentials), ...Object.values(business.roles).flatMap((r) => Object.values(r.credentials)), ]; const checked = checkRefs(refs, forbiddenRoots(config, business)); warnings.push(...checked.warnings); const instances = {}; for (const instance of Object.keys(business.roles)) { instances[instance] = resolveInstance({ system, business, instance }).digest; } for (const w of warnings) process.stderr.write(`mosaic business: warning: ${w}\n`); if (checked.problems.length > 0) { for (const p of checked.problems) process.stderr.write(`mosaic business: ${p}\n`); fail(2, `business ${business.id}: ${checked.problems.length} credential reference problem(s)`); } process.stdout.write(`${JSON.stringify({ business: business.id, file: business.file, projects, instances }, null, 2)}\n`); } function resolveCommand(args) { let project = null; const rest = []; for (let i = 0; i < args.length; i++) { if (args[i] === "--project") { if (!args[i + 1] || project !== null) fail(4, USAGE); project = args[++i]; } else rest.push(args[i]); } if (rest.length !== 2) fail(4, USAGE); const [businessId, instance] = rest; const config = loadSystem(); const business = loadBusiness(businessId, { rolesDir: rolesDir() }); let loaded = null; if (project !== null) { const entry = Object.hasOwn(business.projects, project) ? business.projects[project] : null; if (!entry) fail(2, `business ${business.id} declares no project ${JSON.stringify(project)}`); loaded = loadProject(entry.root); } const resolved = resolveInstance({ system: systemVars(config), business, project: loaded, instance }); const checked = checkRefs(Object.values(resolved.credentials), forbiddenRoots(config, business)); for (const w of checked.warnings) process.stderr.write(`mosaic business: warning: ${w}\n`); if (checked.problems.length > 0) { for (const p of checked.problems) process.stderr.write(`mosaic business: ${p}\n`); fail(2, `${business.id} ${instance}: ${checked.problems.length} credential reference problem(s)`); } process.stdout.write(`${JSON.stringify(resolved, null, 2)}\n`); } const [verb, ...args] = process.argv.slice(2); try { if (verb === "validate") validate(args); else if (verb === "resolve") resolveCommand(args); else fail(4, USAGE); } catch (error) { if (error instanceof BusinessError) fail(error.exitCode, error.message); throw error; }