// Daemon-backed app preferences (onboarding state, agent/skill/DS selection).
//
// The web frontend pushes preferences here via PUT /api/app-config; the
// daemon persists them to <dataDir>/app-config.json (where dataDir defaults
// to <projectRoot>/.od but follows OD_DATA_DIR when set, keeping test and
// multi-namespace runs isolated). This survives browser storage resets and
// origin changes so onboarding and agent selection don't reappear unexpectedly.
//
// `agentCliEnv` is intentionally limited by allowlist below. It may include
// proxy/auth overrides for local CLIs (for example ANTHROPIC_BASE_URL +
// ANTHROPIC_API_KEY for Claude Code, or OPENAI_BASE_URL + OPENAI_API_KEY for
// Codex). Those values are local-only and should not be logged or returned
// outside this machine.
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { randomBytes } from 'node:crypto';
import path from 'node:path';
const ALLOWED_KEYS = new Set([
    'onboardingCompleted',
    'agentId',
    'agentModels',
    'agentCliEnv',
    'skillId',
    'designSystemId',
    'disabledSkills',
    'disabledDesignSystems',
    'installationId',
    'telemetry',
    'privacyDecisionAt',
    'orbit',
    'customInstructions',
]);
function configFile(dataDir) {
    return path.join(dataDir, 'app-config.json');
}
const AGENT_MODEL_KEYS = new Set(['model', 'reasoning']);
const TELEMETRY_KEYS = new Set([
    'metrics',
    'content',
    'artifactManifest',
]);
function validateTelemetry(raw) {
    if (raw === undefined || raw === null)
        return undefined;
    if (typeof raw !== 'object' || Array.isArray(raw))
        return undefined;
    const result = Object.create(null);
    for (const [k, v] of Object.entries(raw)) {
        if (k === '__proto__' || k === 'constructor')
            continue;
        if (!TELEMETRY_KEYS.has(k))
            continue;
        if (typeof v === 'boolean')
            result[k] = v;
    }
    return Object.keys(result).length > 0 ? result : undefined;
}
const AGENT_CLI_ENV_KEYS = new Map([
    ['claude', new Set(['CLAUDE_CONFIG_DIR', 'CLAUDE_BIN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_API_KEY'])],
    ['codex', new Set(['CODEX_HOME', 'CODEX_BIN', 'OPENAI_BASE_URL', 'OPENAI_API_KEY'])],
    ['copilot', new Set(['COPILOT_BIN'])],
    ['cursor-agent', new Set(['CURSOR_AGENT_BIN'])],
    ['deepseek', new Set(['DEEPSEEK_BIN'])],
    ['devin', new Set(['DEVIN_BIN'])],
    ['gemini', new Set(['GEMINI_BIN'])],
    ['hermes', new Set(['HERMES_BIN'])],
    ['kimi', new Set(['KIMI_BIN'])],
    ['kiro', new Set(['KIRO_BIN'])],
    ['kilo', new Set(['KILO_BIN'])],
    ['opencode', new Set(['OPENCODE_BIN'])],
    ['pi', new Set(['PI_BIN'])],
    ['qoder', new Set(['QODER_BIN'])],
    ['qwen', new Set(['QWEN_BIN'])],
    ['vibe', new Set(['VIBE_BIN'])],
]);
function isValidAgentModelEntry(v) {
    if (!v || typeof v !== 'object' || Array.isArray(v))
        return false;
    const obj = v;
    for (const k of Object.keys(obj)) {
        if (!AGENT_MODEL_KEYS.has(k))
            return false;
        if (obj[k] !== undefined && typeof obj[k] !== 'string')
            return false;
    }
    return true;
}
function validateAgentModels(raw) {
    if (raw === undefined || raw === null)
        return undefined;
    if (typeof raw !== 'object' || Array.isArray(raw))
        return undefined;
    const result = Object.create(null);
    for (const [k, v] of Object.entries(raw)) {
        if (k === '__proto__' || k === 'constructor')
            continue;
        if (isValidAgentModelEntry(v)) {
            result[k] = v;
        }
    }
    return Object.keys(result).length > 0 ? result : undefined;
}
export function validateAgentCliEnv(raw) {
    if (raw === undefined || raw === null)
        return undefined;
    if (typeof raw !== 'object' || Array.isArray(raw))
        return undefined;
    const result = Object.create(null);
    for (const [agentId, value] of Object.entries(raw)) {
        if (agentId === '__proto__' || agentId === 'constructor')
            continue;
        const allowed = AGENT_CLI_ENV_KEYS.get(agentId);
        if (!allowed || typeof value !== 'object' || value === null || Array.isArray(value)) {
            continue;
        }
        const env = Object.create(null);
        for (const [envKey, envValue] of Object.entries(value)) {
            if (!allowed.has(envKey))
                continue;
            if (typeof envValue !== 'string')
                continue;
            const trimmed = envValue.trim();
            if (!trimmed)
                continue;
            env[envKey] = trimmed;
        }
        if (Object.keys(env).length > 0)
            result[agentId] = env;
    }
    return Object.keys(result).length > 0 ? result : undefined;
}
function isValidOrbitTime(time) {
    const match = /^(\d{2}):(\d{2})$/.exec(time);
    if (!match)
        return false;
    const hours = Number(match[1]);
    const minutes = Number(match[2]);
    return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
}
function validateOrbit(raw) {
    if (raw === undefined || raw === null)
        return undefined;
    if (typeof raw !== 'object' || Array.isArray(raw))
        return undefined;
    const obj = raw;
    const enabled = typeof obj.enabled === 'boolean' ? obj.enabled : false;
    const time = typeof obj.time === 'string' && isValidOrbitTime(obj.time)
        ? obj.time
        : '08:00';
    const orbit = { enabled, time };
    if (Object.hasOwn(obj, 'templateSkillId')) {
        orbit.templateSkillId = typeof obj.templateSkillId === 'string' && obj.templateSkillId.trim()
            ? obj.templateSkillId.trim()
            : null;
    }
    return orbit;
}
export function agentCliEnvForAgent(prefs, agentId) {
    if (!prefs || typeof agentId !== 'string')
        return {};
    const env = prefs[agentId];
    if (!env || typeof env !== 'object' || Array.isArray(env))
        return {};
    return { ...env };
}
function applyConfigValue(target, key, value) {
    if (key === 'onboardingCompleted') {
        if (typeof value === 'boolean')
            target[key] = value;
        return;
    }
    if (key === 'agentId' || key === 'skillId' || key === 'designSystemId') {
        if (typeof value === 'string' || value === null)
            target[key] = value;
        return;
    }
    if (key === 'agentModels') {
        const validated = validateAgentModels(value);
        if (validated !== undefined) {
            target[key] = validated;
        }
        else {
            delete target[key];
        }
    }
    if (key === 'agentCliEnv') {
        const validated = validateAgentCliEnv(value);
        if (validated !== undefined) {
            target[key] = validated;
        }
        else {
            delete target[key];
        }
    }
    if (key === 'disabledSkills' || key === 'disabledDesignSystems') {
        if (Array.isArray(value) && value.every((v) => typeof v === 'string')) {
            target[key] = value;
        }
        else {
            delete target[key];
        }
    }
    if (key === 'installationId') {
        if (typeof value === 'string' || value === null)
            target[key] = value;
        return;
    }
    if (key === 'telemetry') {
        const validated = validateTelemetry(value);
        if (validated !== undefined) {
            target[key] = validated;
        }
        else {
            delete target[key];
        }
    }
    if (key === 'privacyDecisionAt') {
        if (value === null ||
            (typeof value === 'number' && Number.isFinite(value) && value >= 0)) {
            target[key] = value;
        }
        else {
            delete target[key];
        }
        return;
    }
    if (key === 'orbit') {
        const validated = validateOrbit(value);
        if (validated !== undefined) {
            target[key] = validated;
        }
        else {
            delete target[key];
        }
    }
    if (key === 'customInstructions') {
        if (typeof value === 'string') {
            target[key] = value.slice(0, 5000);
        }
        else if (value === null) {
            target[key] = value;
        }
        return;
    }
}
function filterAllowedKeys(obj) {
    const result = Object.create(null);
    for (const key of Object.keys(obj)) {
        if (ALLOWED_KEYS.has(key)) {
            applyConfigValue(result, key, obj[key]);
        }
    }
    return result;
}
export async function readAppConfig(dataDir) {
    try {
        const raw = await readFile(configFile(dataDir), 'utf8');
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
            return filterAllowedKeys(parsed);
        }
        console.warn('[app-config] Invalid shape in config file, returning empty');
        return {};
    }
    catch (err) {
        const e = err;
        if (e.code === 'ENOENT')
            return {};
        if (e.name === 'SyntaxError') {
            console.error('[app-config] Corrupted JSON, returning empty:', e.message);
            return {};
        }
        throw err;
    }
}
// Serialize concurrent writes to the same dataDir so the read-modify-write
// cycle doesn't lose updates when two PUT requests overlap.
const writeLocks = new Map();
export async function writeAppConfig(dataDir, partial) {
    const prev = writeLocks.get(dataDir) ?? Promise.resolve();
    const task = prev.catch(() => { }).then(() => doWrite(dataDir, partial));
    writeLocks.set(dataDir, task);
    try {
        return await task;
    }
    finally {
        if (writeLocks.get(dataDir) === task)
            writeLocks.delete(dataDir);
    }
}
async function doWrite(dataDir, partial) {
    const existing = await readAppConfig(dataDir);
    const next = { ...existing };
    for (const key of Object.keys(partial)) {
        if (!ALLOWED_KEYS.has(key))
            continue;
        applyConfigValue(next, key, partial[key]);
    }
    const file = configFile(dataDir);
    await mkdir(path.dirname(file), { recursive: true });
    const tmp = file + '.' + randomBytes(4).toString('hex') + '.tmp';
    await writeFile(tmp, JSON.stringify(next, null, 2), 'utf8');
    await rename(tmp, file);
    return next;
}
//# sourceMappingURL=app-config.js.map