#!/usr/bin/env node /** * Mosaic development configuration: bootstrap, validate, resolve. * * Operations: * bootstrap Create the default config ONLY if absent; otherwise validate * the existing file without writing (idempotent). * validate Load and strictly validate; print resolved config on stdout. * env Print shell-safe exports for launcher scripts: * MOSAIC_DATA_ROOT, MOSAIC_PROVIDER, MOSAIC_MODEL. * * Config location: $MOSAIC_CONFIG or ~/.config/mosaic-dev/config.json * * Exit codes: * 0 success * 2 configuration exists but is invalid (never modified by this tool) * 3 configuration is missing for a read operation (validate/env) * * Invariants (docs/plans/2026-09-02_atomic-mosaic-foundation.md): * - Existing configuration is never overwritten. * - Validation failure modifies nothing. * - Secrets are never stored in configuration. */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import process from "node:process"; const SUPPORTED_CONFIG_VERSION = 1; const SUPPORTED_ENVIRONMENTS = new Set(["development", "production"]); const SUPPORTED_BACKENDS = new Set(["docker"]); const SUPPORTED_ADAPTERS = new Set(["pi", "mock"]); // mock: test-only, see adapters/README.md const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/; function fail(exitCode, message) { process.stderr.write(`mosaic-config: ${message}\n`); process.exit(exitCode); } function configPath() { return process.env.MOSAIC_CONFIG ? path.resolve(process.env.MOSAIC_CONFIG) : path.join(os.homedir(), ".config", "mosaic-dev", "config.json"); } function readRaw(file) { let stat; try { stat = fs.lstatSync(file); } catch { return null; // missing } if (!stat.isFile() || stat.isSymbolicLink()) { fail(2, `configuration path must be a regular, non-symbolic-link file: ${file}`); } try { return fs.readFileSync(file, "utf8"); } catch { fail(2, `configuration file is not readable: ${file}`); } } function rejectUnknownKeys(object, allowed, where) { for (const key of Object.keys(object)) { if (!allowed.includes(key)) { fail(2, `unsupported ${where} key: "${key}"`); } } } function isPlainObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function validateDataRoot(value, file) { if (typeof value !== "string" || value.length === 0) { fail(2, 'execution "dataRoot" must be a non-empty string'); } if (value.includes("\0")) { fail(2, 'execution "dataRoot" contains a NUL byte'); } if (!path.isAbsolute(value)) { fail(2, `execution "dataRoot" must be an absolute path (got "${value}")`); } const resolved = path.resolve(value); if (resolved !== value) { fail(2, `execution "dataRoot" must be canonical without ".", "..", trailing slashes, or redundant separators (got "${value}")`); } if (resolved === path.parse(resolved).root) { fail(2, 'execution "dataRoot" must not be the filesystem root'); } const home = path.resolve(os.homedir()); if (resolved === home || isAncestorOf(resolved, home)) { fail(2, `execution "dataRoot" must not be or contain the home directory (${home})`); } const configDir = path.dirname(path.resolve(file)); if (resolved === configDir || isAncestorOf(resolved, configDir)) { fail(2, `execution "dataRoot" must not be or contain the configuration directory (${configDir})`); } return resolved; } function isAncestorOf(ancestor, candidate) { const rel = path.relative(ancestor, candidate); return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel); } function validate(document, file) { if (!isPlainObject(document)) { fail(2, "configuration must be a JSON object"); } rejectUnknownKeys( document, ["configVersion", "environment", "dataRoot", "execution"], "configuration", ); if (document.configVersion !== SUPPORTED_CONFIG_VERSION) { fail(2, `unsupported configVersion: ${JSON.stringify(document.configVersion)} (supported: ${SUPPORTED_CONFIG_VERSION})`); } if (!SUPPORTED_ENVIRONMENTS.has(document.environment)) { fail(2, `unsupported environment: ${JSON.stringify(document.environment)} (supported: ${[...SUPPORTED_ENVIRONMENTS].join(", ")})`); } const dataRoot = validateDataRoot(document.dataRoot, file); if (!isPlainObject(document.execution)) { fail(2, '"execution" must be a JSON object'); } rejectUnknownKeys(document.execution, ["backend", "provider", "model", "adapter"], '"execution"'); if (!SUPPORTED_BACKENDS.has(document.execution.backend)) { fail(2, `unsupported execution.backend: ${JSON.stringify(document.execution.backend)} (supported: ${[...SUPPORTED_BACKENDS].join(", ")})`); } for (const key of ["provider", "model"]) { const value = document.execution[key]; if (typeof value !== "string" || !NAME_PATTERN.test(value)) { fail(2, `execution.${key} must match ${NAME_PATTERN} (got ${JSON.stringify(value)})`); } } const adapter = document.execution.adapter === undefined || document.execution.adapter === null ? "pi" : document.execution.adapter; if (typeof adapter !== "string" || !SUPPORTED_ADAPTERS.has(adapter)) { fail(2, `unsupported execution.adapter: ${JSON.stringify(adapter)} (supported: ${[...SUPPORTED_ADAPTERS].join(", ")})`); } return { configVersion: document.configVersion, environment: document.environment, dataRoot, execution: { backend: document.execution.backend, provider: document.execution.provider, model: document.execution.model, adapter, }, }; } function load(file) { const raw = readRaw(file); if (raw === null) { fail(3, `configuration not found: ${file} (run scripts/bootstrap.sh to create it)`); } let document; try { document = JSON.parse(raw); } catch (error) { fail(2, `configuration is not valid JSON (${file}): ${error.message}`); } return validate(document, file); } function shellQuote(value) { return `'${String(value).replaceAll("'", `'\\''`)}'`; } const DEFAULT_CONFIG = { configVersion: 1, environment: "development", dataRoot: path.join(os.homedir(), ".mosaic-dev"), execution: { backend: "docker", provider: "zai", model: "glm-5.3-flash", }, }; const operation = process.argv[2]; const file = configPath(); switch (operation) { case "bootstrap": { if (fs.existsSync(file)) { load(file); // validate only; never rewrite process.stderr.write(`mosaic-config: configuration already present, validated without changes: ${file}\n`); process.exit(0); } fs.mkdirSync(path.dirname(file), { recursive: true }); let fd; try { // 'wx': creation is exclusive; an existing file is never overwritten. fd = fs.openSync(file, "wx", 0o644); fs.writeFileSync(fd, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`); } catch (error) { if (error.code === "EEXIST") { load(file); process.exit(0); } fail(1, `unable to create configuration: ${error.message}`); } finally { if (fd !== undefined) fs.closeSync(fd); } load(file); process.stderr.write(`mosaic-config: created default configuration: ${file}\n`); process.exit(0); } case "validate": { const resolved = load(file); process.stdout.write(`${JSON.stringify(resolved, null, 2)}\n`); process.exit(0); } case "env": { const resolved = load(file); process.stdout.write( [ `MOSAIC_DATA_ROOT=${shellQuote(resolved.dataRoot)}`, `MOSAIC_PROVIDER=${shellQuote(resolved.execution.provider)}`, `MOSAIC_MODEL=${shellQuote(resolved.execution.model)}`, `MOSAIC_ADAPTER=${shellQuote(resolved.execution.adapter)}`, "", ].join("\n"), ); process.exit(0); } default: fail(1, `unknown operation: ${JSON.stringify(operation ?? "")} (expected bootstrap | validate | env)`); }