#!/usr/bin/env node
/**
 * @connexum/ai-governance CLI
 *
 * Scaffolds governance into Claude Code projects.
 * Usage: npx @connexum/ai-governance init
 *
 * T-084: CLI scaffolder
 * T-085: Interactive framework selection
 * T-086: License key validation
 * T-087: Hook installation
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import * as readline from 'readline';
import * as https from 'https';
import { JwtValidator } from '../license/index.js';
import { BUILT_IN_PACKS } from '../governed-agent.js';
import { scanAgentDirectory } from './agent-dir-scanner.js';
import { writeWrapShims } from './wrap-shim-generator.js';
import { preflightAgents } from './preflight.js';
import { formatPreflightReport } from './preflight-report.js';
/**
 * Read the runtime version from the package.json that ships inside the
 * tarball so `ai-governance --version` matches the npm version a customer
 * just installed. Previously this was a hardcoded string that drifted
 * (beta.12 install reported "v1.0.0-beta.1"). Now it's a single source of
 * truth — the same field npm uses.
 */
const VERSION = (() => {
    try {
        // dist/cli/index.js -> ../../package.json (two ups out of dist/cli/)
        const pkgPath = path.join(__dirname, '..', '..', 'package.json');
        const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
        return pkg.version ?? '0.0.0-unknown';
    }
    catch {
        return '0.0.0-unknown';
    }
})();
// ---------------------------------------------------------------------------
// S-07: Vendor Code provisioning
// ---------------------------------------------------------------------------
const VENDOR_CODE_PATTERN = /^cxni_v1_[BCDFGHJKMNPQRSTVWXYZ234567]{14}$/i;
const DEFAULT_LICENSE_SERVER_URL = 'https://license.my-cc.io';
const BILLING_URL = 'https://my-cc.io/billing';
/**
 * Production gov-server URL — the customer-facing portal API.
 *
 * Thomas directive 2026-05-17 evening: "Make sure this is working and
 * link to the portal so when it goes live it's a fix." When the
 * @connexum/ai-governance package is installed from the public npm
 * registry, the runtime defaults to api.my-cc.io so customers running
 * `npx @connexum/ai-governance@latest init --vendor-code XXX` connect
 * to the production portal automatically. The moment Railway DNS
 * points api.my-cc.io at the production gov-server, every existing
 * customer install starts pushing audit events without re-init.
 *
 * Override paths (in priority order):
 *   1. `--gov-server-url <url>` CLI flag
 *   2. `CXNI_GOV_URL` environment variable
 *   3. This default
 *
 * Local-dev customers pass `--gov-server-url http://localhost:3200`
 * or set `CXNI_GOV_URL` explicitly. The default is production-safe.
 */
const DEFAULT_GOV_SERVER_URL = 'https://api.my-cc.io';
const DEFAULT_PROXY_URL = 'https://governed.my-cc.io';
const DEFAULT_DASHBOARD_URL = 'https://app.my-cc.io';
const VENDOR_CODE_REMEDIATION = 'Contact thomas@ucreatewithai.com for a Vendor Code or visit ' + BILLING_URL;
/**
 * Validate a vendor code format.
 * Accepts `cxni_v1_<14 chars, case-insensitive>`.
 */
export function isValidVendorCodeFormat(code) {
    return VENDOR_CODE_PATTERN.test(code);
}
/**
 * Exchange a Vendor Code with the license server for an Entitlement Token.
 * Returns structured result; does not throw.
 */
export async function exchangeVendorCode(vendorCode, licenseServerUrl = DEFAULT_LICENSE_SERVER_URL) {
    const body = JSON.stringify({
        vendorCode,
        sdkVersion: VERSION,
        machineId: getMachineId(),
    });
    return new Promise((resolve) => {
        const url = new URL('/api/v1/license/exchange', licenseServerUrl);
        const isHttps = url.protocol === 'https:';
        const mod = isHttps ? https : require('http');
        const req = mod.request({
            hostname: url.hostname,
            port: url.port || (isHttps ? 443 : 80),
            path: url.pathname,
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(body),
            },
            timeout: 10000,
        }, (res) => {
            const chunks = [];
            res.on('data', (chunk) => chunks.push(chunk));
            res.on('end', () => {
                const raw = Buffer.concat(chunks).toString('utf-8');
                try {
                    const parsed = JSON.parse(raw);
                    if (res.statusCode === 200 && parsed.token) {
                        resolve({
                            ok: true,
                            tier: parsed.tier,
                            packEntitlements: parsed.packEntitlements,
                            subscriptionActiveUntil: parsed.graceUntil,
                            status: parsed.status,
                        });
                    }
                    else if (res.statusCode === 403) {
                        resolve({ ok: false, error: 'subscription_inactive', status: 'revoked' });
                    }
                    else if (res.statusCode === 404) {
                        resolve({ ok: false, error: 'vendor_code_not_found' });
                    }
                    else {
                        resolve({ ok: false, error: parsed.error ?? `server_error_${res.statusCode}` });
                    }
                }
                catch {
                    resolve({ ok: false, error: 'invalid_server_response' });
                }
            });
        });
        req.on('error', (e) => {
            resolve({ ok: false, error: `network_error: ${e.message}` });
        });
        req.on('timeout', () => {
            req.destroy();
            resolve({ ok: false, error: 'request_timeout' });
        });
        req.write(body);
        req.end();
    });
}
/**
 * Derive a stable machine ID for abuse-detection telemetry.
 * Not a secret; used for detecting vendor code sharing across many machines.
 */
function getMachineId() {
    const hostname = require('os').hostname();
    return crypto.createHash('sha256').update(hostname).digest('hex').slice(0, 16);
}
/**
 * Read the vendor code from environment, config file, or interactive prompt.
 * Returns the code and how it was sourced, or null if not available.
 */
export async function resolveVendorCode(projectDir, opts = {}) {
    const serverUrl = opts.licenseServerUrl ?? DEFAULT_LICENSE_SERVER_URL;
    // 1. --vendor-code flag
    if (opts.vendorCodeFlag) {
        if (!isValidVendorCodeFormat(opts.vendorCodeFlag)) {
            process.stderr.write(`Invalid vendor code format: "${opts.vendorCodeFlag}"\n`);
            process.stderr.write(VENDOR_CODE_REMEDIATION + '\n');
            return null;
        }
        return { code: opts.vendorCodeFlag, source: 'flag' };
    }
    // 2. CXNI_VENDOR_CODE env var
    const envCode = process.env['CXNI_VENDOR_CODE'];
    if (envCode) {
        if (!isValidVendorCodeFormat(envCode)) {
            process.stderr.write(`CXNI_VENDOR_CODE has invalid format: "${envCode}"\n`);
            process.stderr.write(VENDOR_CODE_REMEDIATION + '\n');
            return null;
        }
        return { code: envCode, source: 'env' };
    }
    // 3. Existing .governance.json
    const configPath = path.join(projectDir, '.governance.json');
    if (fs.existsSync(configPath)) {
        try {
            const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
            const saved = config.subscription?.vendorCode;
            if (saved && isValidVendorCodeFormat(saved)) {
                return { code: saved, source: 'config' };
            }
        }
        catch { /* ignore parse errors */ }
    }
    // 4. --offline flag
    if (opts.offlineFlag) {
        const ackEnv = process.env['CXNI_OFFLINE_GRACE_ACKNOWLEDGED'];
        if (ackEnv !== 'true') {
            process.stderr.write('--offline flag requires CXNI_OFFLINE_GRACE_ACKNOWLEDGED=true env var to be set.\n' +
                'This flag bypasses subscription verification and is REFUSED in HIPAA-bound deployments.\n' +
                VENDOR_CODE_REMEDIATION + '\n');
            return null;
        }
        process.stderr.write('WARNING: --offline mode acknowledged. Subscription verification bypassed.\n' +
            'This deployment will be treated as permanently in grace mode.\n' +
            'HIPAA/PCI-DSS-bound deployments refuse offline mode at runtime.\n');
        // Return a sentinel that callers handle specially
        return { code: '__offline__', source: 'flag' };
    }
    // 5. Interactive prompt
    if (opts.rl) {
        const code = await new Promise(resolve => opts.rl.question('Enter your Vendor Code (received by email after purchase): ', resolve));
        const trimmed = code.trim();
        if (!trimmed || !isValidVendorCodeFormat(trimmed)) {
            process.stderr.write('Invalid or empty Vendor Code.\n');
            process.stderr.write(VENDOR_CODE_REMEDIATION + '\n');
            return null;
        }
        return { code: trimmed, source: 'prompt' };
    }
    process.stderr.write('No Vendor Code found. Scaffolding refused.\n');
    process.stderr.write(VENDOR_CODE_REMEDIATION + '\n');
    return null;
}
/**
 * Write the vendor code into .governance.json at `subscription.vendorCode`
 * with file mode 0600 on Unix.
 */
export function writeVendorCodeToConfig(projectDir, vendorCode, offlineAcknowledged = false) {
    const configPath = path.join(projectDir, '.governance.json');
    let config = {};
    if (fs.existsSync(configPath)) {
        try {
            config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
        }
        catch { /* start fresh */ }
    }
    const subscription = {
        vendorCode,
        writtenAt: new Date().toISOString(),
    };
    if (offlineAcknowledged) {
        subscription['offlineAcknowledged'] = true;
    }
    config['subscription'] = subscription;
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
}
// --- Pack registry for framework selection ---
const AVAILABLE_PACKS = [
    { id: 'soc2', name: 'SOC 2 Type II', industries: 'SaaS, Cloud Services' },
    { id: 'hipaa', name: 'HIPAA Security & Privacy', industries: 'Healthcare, Telehealth' },
    { id: 'gdpr', name: 'GDPR', industries: 'Any (EU users/data)' },
    { id: 'pci-dss', name: 'PCI DSS v4.0', industries: 'Payments, Fintech' },
    { id: 'dora', name: 'DORA', industries: 'EU Financial Services' },
    { id: 'eu-ai-act', name: 'EU AI Act', industries: 'Any AI system in EU' },
    { id: 'iso27001', name: 'ISO 27001', industries: 'Enterprise, Government' },
];
// --- OS detection for hook selection ---
const IS_WINDOWS = process.platform === 'win32';
const HOOK_EXT = IS_WINDOWS ? '.ps1' : '.sh';
const HOOK_RUNNER = IS_WINDOWS ? 'pwsh' : 'bash';
const HOOK_SUBDIR = IS_WINDOWS ? 'powershell' : '';
export function detectIde(projectDir) {
    const markers = [
        { dir: '.claude', ide: 'claude-code' },
        { dir: '.cursor', ide: 'cursor' },
        { dir: '.continue', ide: 'continue' },
        { dir: '.cody', ide: 'cody' },
        { dir: '.aider', ide: 'aider' },
        { dir: '.gemini', ide: 'gemini' },
    ];
    for (const m of markers) {
        if (fs.existsSync(path.join(projectDir, m.dir)))
            return m.ide;
    }
    // GitHub Copilot uses `.github/copilot-instructions.md` (file, not dir)
    // as its repo-level rules surface. We use that file as the Copilot
    // marker because `.github/` alone is too noisy (every GitHub repo has
    // it for Actions / issue templates / etc.).
    if (fs.existsSync(path.join(projectDir, '.github', 'copilot-instructions.md'))) {
        return 'copilot';
    }
    return 'unknown';
}
// --- Hook manifest ---
const HOOKS = [
    { file: `governance-integrity-check${HOOK_EXT}`, type: 'PreToolUse', description: 'SHA-256 governance file integrity verification' },
    { file: `governance-file-shield${HOOK_EXT}`, type: 'PreToolUse', description: 'Prevents modification of governance config files' },
    { file: `file-boundary-guard${HOOK_EXT}`, type: 'PreToolUse', description: 'Restricts file access to agent boundaries' },
    { file: `destructive-command-guard${HOOK_EXT}`, type: 'PreToolUse', description: 'Blocks destructive shell commands' },
    { file: `secret-leak-scanner${HOOK_EXT}`, type: 'PreToolUse', description: 'Scans for credentials, API keys, PII' },
    { file: `web-access-gate${HOOK_EXT}`, type: 'PreToolUse', description: 'Controls agent web access' },
    { file: `cost-budget-gate${HOOK_EXT}`, type: 'PreToolUse', description: 'Token budget enforcement' },
    { file: `audit-logger${HOOK_EXT}`, type: 'PostToolUse', description: 'Tamper-evident audit logging' },
    { file: `file-change-tracker${HOOK_EXT}`, type: 'PostToolUse', description: 'Tracks all file modifications' },
    { file: `token-tracker${HOOK_EXT}`, type: 'PostToolUse', description: 'Token usage monitoring' },
    { file: `output-exfiltration-scanner${HOOK_EXT}`, type: 'PostToolUse', description: 'Detects data exfiltration in output' },
];
/**
 * Validate a license key. Supports two formats:
 * 1. JWT tokens (RS256-signed, new format) -- validated via JwtValidator
 * 2. Legacy HMAC keys (CXGOV-<tier>-<org>-<checksum>) -- backward compatible
 */
async function validateLicenseAsync(key) {
    // Free tier: no key required.
    // 2026-05-17 — raised maxPacks 1→3 so design-partner customers who only
    // pass a vendor code (not a JWT license) can still bind the minimum
    // bundle of HIPAA + SOC 2 + GDPR on day one. The hard cap is still
    // enforced server-side via the per-org PackVersionStore.
    if (!key || key === 'free') {
        return {
            tier: 'free',
            org: 'unlicensed',
            maxPacks: 3,
            maxAgents: 5,
            features: ['scanner', 'basic-hooks', 'multi-pack'],
        };
    }
    // Detect JWT format (three base64url segments separated by dots)
    if (key.split('.').length === 3) {
        return validateJwtLicense(key);
    }
    // Fall back to legacy HMAC format
    return validateLegacyLicense(key);
}
/** Validate JWT license token using JwtValidator. */
async function validateJwtLicense(token) {
    const serverUrl = process.env.CXGOV_LICENSE_SERVER || '';
    const validator = new JwtValidator({
        serverUrl,
        forceOffline: !serverUrl,
    });
    const result = await validator.validate(token);
    if (!result.valid || !result.claims) {
        process.stderr.write(`[LICENSE] JWT validation failed: ${result.error}\n`);
        return null;
    }
    const claims = result.claims;
    return {
        tier: claims.tier,
        org: claims.org,
        maxPacks: claims.maxPacks,
        maxAgents: claims.maxAgents,
        features: claims.features,
        expiresAt: claims.exp ? new Date(claims.exp * 1000).toISOString() : undefined,
    };
}
/**
 * Validate legacy HMAC-based license key (CXGOV-<tier>-<org_hash>-<checksum>).
 *
 * AIAG-006: requires `CXGOV_LICENSE_SECRET` env var and fails closed when
 * missing. No hardcoded fallback. Exported for regression testing — do not
 * re-introduce a default secret.
 */
export function validateLegacyLicense(key) {
    const parts = key.split('-');
    if (parts.length < 4 || parts[0] !== 'CXGOV')
        return null;
    const tier = parts[1].toLowerCase();
    const orgHash = parts[2];
    const checksum = parts[3];
    // CXGOV_LICENSE_SECRET must be set. No fallback -- fail closed.
    const secret = process.env.CXGOV_LICENSE_SECRET;
    if (!secret) {
        process.stderr.write('[SECURITY] CXGOV_LICENSE_SECRET not set. Cannot validate legacy license keys.\n');
        return null;
    }
    const expected = crypto.createHmac('sha256', secret)
        .update(`${tier}-${orgHash}`)
        .digest('hex')
        .substring(0, 8);
    // F-NEW-CRIT-34 (2026-05-02): timingSafeEqual REQUIRES equal-length
    // buffers (Node throws ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH otherwise).
    // A malformed customer license with a too-short or too-long checksum
    // used to crash the CLI uncaught at first run. Length-check before
    // the constant-time compare; length mismatch == invalid license ==
    // return null cleanly.
    const checksumBuf = Buffer.from(checksum);
    const expectedBuf = Buffer.from(expected);
    if (checksumBuf.length !== expectedBuf.length)
        return null;
    if (!crypto.timingSafeEqual(checksumBuf, expectedBuf))
        return null;
    const tiers = {
        sta: { tier: 'starter', maxPacks: 1, maxAgents: 25, features: ['basic-hooks', 'single-pack', 'dashboard', 'events', 'sso', 'api-access', 'webhooks'] },
        pro: { tier: 'professional', maxPacks: 1, maxAgents: 25, features: ['basic-hooks', 'single-pack', 'dashboard', 'events', 'audit-reports', 'sso', 'api-access', 'webhooks'] },
        ent: { tier: 'enterprise', maxPacks: 99, maxAgents: 100, features: ['basic-hooks', 'multi-pack', 'audit-reports', 'siem-integration', 'encryption', 'custom-packs'] },
    };
    const tierInfo = tiers[tier];
    if (!tierInfo)
        return null;
    return { ...tierInfo, org: orgHash };
}
/** Synchronous wrapper for backward compatibility (uses free tier or legacy only). */
function validateLicense(key) {
    if (!key || key === 'free') {
        return {
            tier: 'free',
            org: 'unlicensed',
            maxPacks: 3,
            maxAgents: 5,
            features: ['scanner', 'basic-hooks', 'multi-pack'],
        };
    }
    // JWT must use async path
    if (key.split('.').length === 3) {
        process.stderr.write('[LICENSE] JWT tokens require async validation. Use --ci mode or interactive init.\n');
        return null;
    }
    return validateLegacyLicense(key);
}
/**
 * Read the license server URL from .governance.json, falling back to DEFAULT.
 * Call this at any point where the license server URL is needed at runtime.
 */
export function readLicenseServerUrl(projectDir) {
    const configPath = path.join(projectDir, '.governance.json');
    if (fs.existsSync(configPath)) {
        try {
            const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
            if (config.licenseServerUrl)
                return config.licenseServerUrl;
        }
        catch { /* ignore -- fall through to default */ }
    }
    return process.env['CXGOV_LICENSE_SERVER'] ?? DEFAULT_LICENSE_SERVER_URL;
}
export function generateConfig(packs, license, licenseServerUrl, runtime) {
    return {
        version: VERSION,
        organization: license.org !== 'unlicensed' ? license.org : undefined,
        license: license.tier,
        packs,
        providers: {
            allowed: ['anthropic', 'openai', 'google', 'microsoft', 'amazon'],
            blocked: ['deepseek', 'baidu', 'alibaba'],
        },
        hooks: {
            preToolUse: HOOKS.filter(h => h.type === 'PreToolUse').map(h => h.file),
            postToolUse: HOOKS.filter(h => h.type === 'PostToolUse').map(h => h.file),
        },
        audit: {
            enabled: true,
            format: 'jsonl',
        },
        encryption: {
            enabled: license.features.includes('encryption'),
            algorithm: 'aes-256-gcm',
        },
        // Only persist if a custom URL was provided. Absent = use runtime default.
        ...(licenseServerUrl ? { licenseServerUrl } : {}),
        // F-NEW-CUSTOMER-JOURNEY-001: persist runtime block so audit-logger.sh
        // can push tool-call events to gov-server. Absent = local-only audit
        // (the prior behavior; safe default for offline/air-gapped use).
        ...(runtime ? { runtime } : {}),
        createdAt: new Date().toISOString(),
    };
}
// --- Hook installation ---
export function installHooks(projectDir, config) {
    const errors = [];
    let installed = 0;
    // Find the package's hooks directory (bash or powershell based on OS)
    const hooksSrcDir = HOOK_SUBDIR
        ? path.join(__dirname, '..', 'hooks', HOOK_SUBDIR)
        : path.join(__dirname, '..', 'hooks');
    const hooksDestDir = path.join(projectDir, '.governance', 'hooks');
    if (!fs.existsSync(hooksDestDir)) {
        fs.mkdirSync(hooksDestDir, { recursive: true });
    }
    // Copy shared utilities
    const utilsSrc = path.join(hooksSrcDir, `hook-utils${HOOK_EXT}`);
    if (fs.existsSync(utilsSrc)) {
        fs.copyFileSync(utilsSrc, path.join(hooksDestDir, `hook-utils${HOOK_EXT}`));
    }
    // Copy audit-dir-picker helper (sourced by audit-logger.sh at runtime).
    // Without this file the hook exits 1 on every PostToolUse event, silently
    // breaking the audit trail for every customer install. CR-MED-01.
    const auditDirPickerName = `audit-dir-picker${HOOK_EXT}`;
    const auditDirPickerSrc = path.join(hooksSrcDir, auditDirPickerName);
    if (fs.existsSync(auditDirPickerSrc)) {
        fs.copyFileSync(auditDirPickerSrc, path.join(hooksDestDir, auditDirPickerName));
    }
    // Copy hook wrapper
    const wrapperSrc = path.join(hooksSrcDir, `hook-wrapper${HOOK_EXT}`);
    if (fs.existsSync(wrapperSrc)) {
        fs.copyFileSync(wrapperSrc, path.join(hooksDestDir, `hook-wrapper${HOOK_EXT}`));
    }
    // Install configured hooks
    const allHooks = [...config.hooks.preToolUse, ...config.hooks.postToolUse];
    for (const hookFile of allHooks) {
        const src = path.join(hooksSrcDir, hookFile);
        const dest = path.join(hooksDestDir, hookFile);
        if (fs.existsSync(src)) {
            fs.copyFileSync(src, dest);
            // Make executable
            try {
                fs.chmodSync(dest, 0o755);
            }
            catch { /* Windows */ }
            installed++;
        }
        else {
            errors.push(`Hook source not found: ${hookFile}`);
        }
    }
    // Dispatch to IDE-specific surface (F-NEW-CRIT-39g). Cursor / Continue
    // users no longer get a stray .claude/ dir polluting their project;
    // each IDE sees governance rules through its own native surface.
    const ide = detectIde(projectDir);
    switch (ide) {
        case 'cursor':
        case 'continue':
        case 'cody':
        case 'aider':
        case 'gemini':
        case 'copilot':
            installIdeRules(projectDir, config, hooksDestDir, IDE_RULES_SPECS[ide]);
            break;
        default:
            installClaudeCodeSettings(projectDir, config, hooksDestDir);
            break;
    }
    return { installed, errors };
}
// Claude Code hook schema (verified against ~/.claude/settings.json on a
// working install): top-level `hooks` is an object keyed by event name.
// Each event holds an array of { matcher, hooks: [{ type: 'command', command }] }.
// Inner `type` is the literal string 'command' (the hook plugin type),
// NOT the event name.
function installClaudeCodeSettings(projectDir, config, hooksDestDir) {
    const claudeSettingsDir = path.join(projectDir, '.claude');
    if (!fs.existsSync(claudeSettingsDir)) {
        fs.mkdirSync(claudeSettingsDir, { recursive: true });
    }
    const settingsFile = path.join(claudeSettingsDir, 'settings.json');
    const buildBlock = (hookFile) => ({
        matcher: '*',
        hooks: [{
                type: 'command',
                command: `${HOOK_RUNNER} "${path.join(hooksDestDir, hookFile)}"`,
            }],
    });
    const preEntries = config.hooks.preToolUse.map(buildBlock);
    const postEntries = config.hooks.postToolUse.map(buildBlock);
    let settings = {};
    if (fs.existsSync(settingsFile)) {
        try {
            settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
        }
        catch { /* fresh */ }
    }
    const existingHooks = (settings['hooks'] && typeof settings['hooks'] === 'object' && !Array.isArray(settings['hooks']))
        ? settings['hooks']
        : {};
    settings['hooks'] = {
        ...existingHooks,
        PreToolUse: preEntries,
        PostToolUse: postEntries,
    };
    fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
}
const IDE_RULES_SPECS = {
    cursor: {
        ideKey: 'cursor',
        ideName: 'Cursor',
        fileDir: '.cursor/rules',
        fileName: 'governance.md',
        auditDirRel: '.cursor/audit/',
        fileSelfRef: '.cursor/rules/governance.md',
        integrationSurfaceLines: [
            '**`.cursor/rules/governance.md`** (this file) -- prompt-level policy',
            '**`__HOOKS__/`** -- runtime hook scripts',
            '**`.cursor/audit/`** -- tamper-evident chain (vendor-neutral via picker)',
        ],
    },
    continue: {
        ideKey: 'continue',
        ideName: 'Continue.dev',
        fileDir: '.continue',
        fileName: 'governance.md',
        auditDirRel: '.continue/audit/',
        fileSelfRef: '.continue/governance.md',
        wiringSection: `## Wiring into Continue.dev

Continue does not auto-include this file. Wire it in one of two ways:

1. **systemMessage** (simplest) -- copy the rules above into the
   \`systemMessage\` field of your \`.continue/config.json\` so they
   are prepended to every chat/agent prompt.
2. **buildContinueExtension** (full enforcement) -- import
   \`buildContinueExtension\` from
   \`@connexum/ai-governance/ide-adapters/continue-dev\` and add it
   to your config's \`extensions\` array. This routes tool calls
   through the runtime governance engine (PreToolUse/PostToolUse
   equivalent) instead of just appending policy text.`,
        integrationSurfaceLines: [
            '**`.continue/governance.md`** (this file) -- policy text',
            '**`__HOOKS__/`** -- runtime hook scripts',
            '**`.continue/audit/`** -- tamper-evident chain (vendor-neutral via picker)',
            '**ContinueDevIdeAdapter** -- runtime enforcement (full mode only)',
        ],
    },
    cody: {
        ideKey: 'cody',
        ideName: 'Cody (Sourcegraph)',
        fileDir: '.cody',
        fileName: 'governance.md',
        auditDirRel: '.cody/audit/',
        fileSelfRef: '.cody/governance.md',
        wiringSection: `## Wiring into Cody (Sourcegraph)

Cody does not auto-include policy files. Wire it in one of these ways:

1. **Cody chat system prompt** -- paste the rules above into the Cody
   Agent's system prompt for the workspace (Cody Settings ->
   System Prompt).
2. **Custom command** -- create a Cody custom command that prepends
   these rules. See Sourcegraph's Cody docs for custom commands.
3. **\`.cody/ignore\`** -- pair this rules file with a \`.cody/ignore\`
   that excludes \`.cody/audit/\` from Cody's context window so the
   audit chain itself is never sent to the LLM.`,
        integrationSurfaceLines: [
            '**`.cody/governance.md`** (this file) -- policy text',
            '**`__HOOKS__/`** -- runtime hook scripts',
            '**`.cody/audit/`** -- tamper-evident chain (vendor-neutral via picker)',
            '**`.cody/ignore`** -- excludes audit chain from Cody context',
        ],
    },
    aider: {
        ideKey: 'aider',
        ideName: 'Aider',
        fileDir: '.aider',
        fileName: 'governance.md',
        auditDirRel: '.aider/audit/',
        fileSelfRef: '.aider/governance.md',
        wiringSection: `## Wiring into Aider

Aider does not auto-include policy files. Wire it in one of these ways:

1. **\`--read\` flag** -- launch Aider with
   \`aider --read .aider/governance.md\` so the rules are pre-loaded
   into the conversation context.
2. **\`.aider.conf.yml\`** -- add a \`read:\` entry pointing at this
   file:

   \`\`\`yaml
   read:
     - .aider/governance.md
   \`\`\`

3. **Aider chat \`/read\` command** -- inside an Aider session run
   \`/read .aider/governance.md\` to inject the rules mid-session.`,
        integrationSurfaceLines: [
            '**`.aider/governance.md`** (this file) -- policy text loaded via `--read`',
            '**`__HOOKS__/`** -- runtime hook scripts',
            '**`.aider/audit/`** -- tamper-evident chain (vendor-neutral via picker)',
        ],
    },
    gemini: {
        ideKey: 'gemini',
        ideName: 'Gemini Code Assist',
        fileDir: '.gemini',
        fileName: 'governance.md',
        auditDirRel: '.gemini/audit/',
        fileSelfRef: '.gemini/governance.md',
        wiringSection: `## Wiring into Gemini Code Assist

Gemini Code Assist does not auto-include policy files. Wire it via:

1. **Code Customization (Enterprise tier)** -- include this file in
   the private repository Gemini grounds on so the rules are part of
   every prompt's context.
2. **Workspace README pointer** -- add a top-of-README block linking
   to \`.gemini/governance.md\` so the IDE assistant surfaces it
   when grounding on README.
3. **Manual chat injection** -- paste the rules into the Gemini Chat
   panel at session start.`,
        integrationSurfaceLines: [
            '**`.gemini/governance.md`** (this file) -- policy text',
            '**`__HOOKS__/`** -- runtime hook scripts',
            '**`.gemini/audit/`** -- tamper-evident chain (vendor-neutral via picker)',
        ],
    },
    copilot: {
        ideKey: 'copilot',
        ideName: 'GitHub Copilot',
        fileDir: '.github',
        fileName: 'copilot-governance.md',
        auditDirRel: '.governance/audit/',
        fileSelfRef: '.github/copilot-governance.md',
        wiringSection: `## Wiring into GitHub Copilot

GitHub Copilot reads \`.github/copilot-instructions.md\`. We intentionally
did NOT overwrite that file (it may already contain repo-specific
guidance). Wire this governance policy in one of these ways:

1. **Append a reference** -- in \`.github/copilot-instructions.md\` add
   a line at the top:
   > Apply the policy in \`.github/copilot-governance.md\`.
   Copilot reads the linked file when grounding context.
2. **Inline merge** -- copy the rules above into
   \`.github/copilot-instructions.md\` and delete this file.
3. **Pull-request review only** -- if the team enforces governance via
   PR review (Copilot does not block destructive suggestions
   prospectively in IDE), this file becomes the reviewer checklist.`,
        integrationSurfaceLines: [
            '**`.github/copilot-governance.md`** (this file) -- policy text',
            '**`.github/copilot-instructions.md`** -- customer-edited Copilot',
            '  instructions (referenced from above)',
            '**`__HOOKS__/`** -- runtime hook scripts',
            '**`.governance/audit/`** -- tamper-evident chain (vendor-neutral)',
        ],
    },
};
function installIdeRules(projectDir, config, hooksDestDir, spec) {
    const targetDir = path.join(projectDir, spec.fileDir);
    if (!fs.existsSync(targetDir)) {
        fs.mkdirSync(targetDir, { recursive: true });
    }
    const rulesFile = path.join(targetDir, spec.fileName);
    const packLines = config.packs.length > 0
        ? config.packs.map(p => `- **${p}**`).join('\n')
        : '- (none configured)';
    const blockedProviders = (config.providers?.blocked ?? []).length > 0
        ? config.providers.blocked.map(p => `- ${p}`).join('\n')
        : '- (none configured)';
    const hooksRel = path.relative(projectDir, hooksDestDir);
    const integrationSurface = spec.integrationSurfaceLines
        .map(line => `- ${line.replace(/__HOOKS__/g, hooksRel)}`)
        .join('\n');
    const wiring = spec.wiringSection ? `${spec.wiringSection}\n\n` : '';
    const auditNoun = spec.ideKey === 'copilot'
        ? `Audit chain default: \`${spec.auditDirRel}audit-<date>.jsonl\`.`
        : `Audit chain is appended to \`${spec.auditDirRel}audit-<date>.jsonl\`.`;
    const body = `# AI Governance Policy (auto-generated)

> Generated by \`@connexum/ai-governance\` -- DO NOT EDIT BY HAND.
> Runtime hooks live in \`${hooksRel}\`.
> ${auditNoun}
> Re-run \`npx @connexum/ai-governance init\` to regenerate.

## Active compliance packs
${packLines}

## Blocked LLM providers
${blockedProviders}

## Mandatory rules for every AI action

1. Refuse destructive shell commands. Block patterns include:
   recursive \`rm\`, \`git push --force\`, \`git reset --hard\`,
   \`git clean -f\`, SQL drop/truncate, \`npm publish\`, and any
   \`curl ... | bash\` pipe.
2. Refuse reads or writes to sensitive paths: \`/etc/\`, \`/root/\`,
   \`.env\` files, \`secrets/\`, \`.pem\` and \`.key\` files,
   anything matching \`private_?key\`.
3. Refuse to write to the audit chain directory \`${spec.auditDirRel}\`.
   The chain is tamper-evident and write-only via the audit logger.
4. Every tool invocation is recorded in the audit chain with
   tool name, input summary, decision, reason, and SHA-256 hash
   linking to the prior entry.
5. The configured compliance packs above govern data classification.
   Inputs containing PII / PHI / PCI material flagged by those packs
   trigger a DENY decision regardless of the requesting prompt.

${wiring}## Integration surface

${spec.ideName} reaches the governance runtime through:
${integrationSurface}

## Override / disable

Run \`npx @connexum/ai-governance init\` with a different pack set to
regenerate this file. To temporarily disable governance for a single
operation, contact your compliance owner -- there is no in-IDE override.
`;
    fs.writeFileSync(rulesFile, body);
}
// --- Interactive prompts ---
function ask(rl, question) {
    return new Promise(resolve => rl.question(question, resolve));
}
async function interactiveInit(projectDir, opts = {}) {
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    // Citadel banner — emotional positioning. Only emitted on the interactive
    // path so the --ci JSON output stream stays parser-clean.
    process.stdout.write('\n');
    process.stdout.write('  Welcome to the Citadel\n');
    process.stdout.write('  My-CC fortress for building Agent Trust.\n');
    process.stdout.write('  My-CC.io AI Agent Trust Citadel\n');
    process.stdout.write('\n');
    process.stdout.write('@connexum/ai-governance - Enterprise AI Governance Setup\n');
    process.stdout.write('='.repeat(55) + '\n\n');
    // Step 0: S-07 Vendor Code
    const vcResult = await resolveVendorCode(projectDir, {
        vendorCodeFlag: opts.vendorCodeFlag,
        offlineFlag: opts.offlineFlag,
        rl,
        licenseServerUrl: opts.licenseServerUrl,
    });
    if (!vcResult) {
        rl.close();
        process.exit(1);
    }
    const isOffline = vcResult.code === '__offline__';
    let entitlement = null;
    if (!isOffline) {
        process.stdout.write('Validating Vendor Code...\n');
        entitlement = await exchangeVendorCode(vcResult.code, opts.licenseServerUrl);
        if (!entitlement.ok) {
            process.stderr.write(`Vendor Code validation failed: ${entitlement.error}\n`);
            process.stderr.write(VENDOR_CODE_REMEDIATION + '\n');
            rl.close();
            process.exit(1);
        }
        process.stdout.write(`Subscription: ${entitlement.tier ?? 'unknown'} (${entitlement.status ?? 'active'})\n`);
        if (entitlement.packEntitlements?.length) {
            process.stdout.write(`Pack entitlements: ${entitlement.packEntitlements.join(', ')}\n`);
        }
        process.stdout.write('\n');
    }
    // Step 1: License key (supports JWT tokens and legacy HMAC keys)
    const licenseKey = await ask(rl, 'License key (press Enter for free tier): ');
    const license = await validateLicenseAsync(licenseKey || 'free');
    if (!license) {
        process.stderr.write('Invalid license key. Contact thomas@ucreatewithai.com\n');
        rl.close();
        process.exit(1);
    }
    process.stdout.write(`License: ${license.tier} (max ${license.maxPacks} pack${license.maxPacks > 1 ? 's' : ''})\n\n`);
    // Step 2: Framework selection
    process.stdout.write('Available compliance frameworks:\n');
    for (let i = 0; i < AVAILABLE_PACKS.length; i++) {
        const pack = AVAILABLE_PACKS[i];
        process.stdout.write(`  ${i + 1}. ${pack.name} - ${pack.industries}\n`);
    }
    process.stdout.write('\n');
    const selection = await ask(rl, `Select frameworks (comma-separated numbers, max ${license.maxPacks}): `);
    const indices = selection.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < AVAILABLE_PACKS.length);
    const selectedPacks = indices.slice(0, license.maxPacks).map(i => AVAILABLE_PACKS[i].id);
    if (selectedPacks.length === 0) {
        process.stdout.write('No frameworks selected. Using SOC 2 as default.\n');
        selectedPacks.push('soc2');
    }
    process.stdout.write(`\nSelected: ${selectedPacks.join(', ')}\n\n`);
    // Step 3: Confirmation
    const confirm = await ask(rl, 'Install governance hooks and configuration? (y/n): ');
    if (confirm.toLowerCase() !== 'y') {
        process.stdout.write('Aborted.\n');
        rl.close();
        process.exit(0);
    }
    rl.close();
    // Generate and write config
    // Persist licenseServerUrl if it was provided (allows on-prem customers to override the default)
    const config = generateConfig(selectedPacks, license, opts.licenseServerUrl, opts.runtime);
    const configPath = path.join(projectDir, '.governance.json');
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
    process.stdout.write(`\nConfiguration written to ${configPath}\n`);
    // Write vendor code into config (file mode 0600 enforced in writeVendorCodeToConfig)
    if (!isOffline) {
        writeVendorCodeToConfig(projectDir, vcResult.code);
        process.stdout.write('Vendor Code stored in .governance.json\n');
    }
    else {
        writeVendorCodeToConfig(projectDir, '', true);
        process.stdout.write('Offline mode recorded in .governance.json\n');
    }
    // Install hooks
    const { installed, errors } = installHooks(projectDir, config);
    process.stdout.write(`Hooks installed: ${installed}\n`);
    if (errors.length > 0) {
        for (const err of errors) {
            process.stderr.write(`  Warning: ${err}\n`);
        }
    }
    // Create governance data directory
    const govDir = path.join(projectDir, '.governance');
    const dirs = ['audit', 'sessions', 'events', 'data'];
    for (const dir of dirs) {
        const p = path.join(govDir, dir);
        if (!fs.existsSync(p))
            fs.mkdirSync(p, { recursive: true });
    }
    process.stdout.write(`\nGovernance initialized successfully.\n`);
    process.stdout.write(`  Config: ${configPath}\n`);
    process.stdout.write(`  Hooks:  ${path.join(govDir, 'hooks')}/\n`);
    process.stdout.write(`  Audit:  ${path.join(govDir, 'audit')}/\n`);
    process.stdout.write(`\nRun 'ai-governance status' to verify.\n`);
}
// --- Non-interactive init (CI mode) ---
async function nonInteractiveInit(projectDir, opts) {
    const licenseKey = opts.license || 'free';
    const license = await validateLicenseAsync(licenseKey);
    if (!license) {
        process.stderr.write('Invalid license key.\n');
        process.exit(1);
    }
    const packs = opts.packs ? opts.packs.split(',') : ['soc2'];
    const validPacks = packs.filter(p => AVAILABLE_PACKS.some(ap => ap.id === p));
    if (validPacks.length > license.maxPacks) {
        process.stderr.write(`License allows max ${license.maxPacks} packs. Selected ${validPacks.length}.\n`);
        process.exit(1);
    }
    const config = generateConfig(validPacks, license, opts.licenseServerUrl, opts.runtime);
    const configPath = path.join(projectDir, '.governance.json');
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
    const { installed, errors } = installHooks(projectDir, config);
    // Create directories
    const govDir = path.join(projectDir, '.governance');
    for (const dir of ['audit', 'sessions', 'events', 'data']) {
        const p = path.join(govDir, dir);
        if (!fs.existsSync(p))
            fs.mkdirSync(p, { recursive: true });
    }
    process.stdout.write(JSON.stringify({
        success: true,
        config: configPath,
        packs: validPacks,
        hooksInstalled: installed,
        errors,
    }) + '\n');
}
// --- Verify command ---
function verifyGovernance(projectDir) {
    const configPath = path.join(projectDir, '.governance.json');
    if (!fs.existsSync(configPath)) {
        process.stderr.write('No .governance.json found. Run "ai-governance init" first.\n');
        process.exit(1);
    }
    let config;
    try {
        config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    }
    catch {
        process.stderr.write('Invalid .governance.json. Re-run "ai-governance init".\n');
        process.exit(1);
    }
    const checks = [];
    // Check config structure
    checks.push({
        name: 'Config valid',
        passed: !!config.version && !!config.packs,
        detail: `Version: ${config.version}, Packs: ${config.packs?.join(', ')}`,
    });
    // Check hooks exist
    const hooksDir = path.join(projectDir, '.governance', 'hooks');
    const allHooks = [...(config.hooks?.preToolUse || []), ...(config.hooks?.postToolUse || [])];
    let hooksFound = 0;
    for (const hookFile of allHooks) {
        if (fs.existsSync(path.join(hooksDir, hookFile)))
            hooksFound++;
    }
    checks.push({
        name: 'Hooks installed',
        passed: hooksFound === allHooks.length,
        detail: `${hooksFound}/${allHooks.length} hooks present`,
    });
    // Check governance directories
    const requiredDirs = ['audit', 'sessions', 'events'];
    const dirsExist = requiredDirs.every(d => fs.existsSync(path.join(projectDir, '.governance', d)));
    checks.push({
        name: 'Data directories',
        passed: dirsExist,
        detail: dirsExist ? 'All directories present' : 'Missing directories',
    });
    // Check blocked providers config
    checks.push({
        name: 'Provider policy',
        passed: (config.providers?.blocked?.length || 0) > 0,
        detail: `Blocked: ${config.providers?.blocked?.join(', ') || 'none'}`,
    });
    // Output
    process.stdout.write('\nGovernance Verification\n');
    process.stdout.write('='.repeat(40) + '\n');
    let allPassed = true;
    for (const check of checks) {
        const status = check.passed ? 'PASS' : 'FAIL';
        process.stdout.write(`  [${status}] ${check.name}: ${check.detail}\n`);
        if (!check.passed)
            allPassed = false;
    }
    process.stdout.write('\n' + (allPassed ? 'All checks passed.\n' : 'Some checks failed. Review configuration.\n'));
    process.exit(allPassed ? 0 : 1);
}
export function retentionSweep(opts) {
    const now = opts.now ?? new Date();
    const cutoff = new Date(now.getTime() - opts.retainDays * 24 * 60 * 60 * 1000);
    const result = {
        auditDir: opts.auditDir,
        retainDays: opts.retainDays,
        cutoffIso: cutoff.toISOString(),
        archiveDir: opts.archiveDir,
        dryRun: opts.dryRun,
        filesScanned: 0,
        filesRetained: 0,
        filesActioned: [],
        errors: [],
    };
    if (!fs.existsSync(opts.auditDir)) {
        return result; // empty result; caller decides if absent dir is an error
    }
    if (opts.archiveDir && !fs.existsSync(opts.archiveDir) && !opts.dryRun) {
        fs.mkdirSync(opts.archiveDir, { recursive: true });
    }
    const entries = fs.readdirSync(opts.auditDir);
    for (const entry of entries) {
        // Match `audit-YYYY-MM-DD.jsonl` (the format emitted by audit-logger.sh).
        const m = /^audit-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(entry);
        if (!m)
            continue;
        result.filesScanned += 1;
        const fileDate = new Date(m[1] + 'T00:00:00.000Z');
        const ageDays = Math.floor((now.getTime() - fileDate.getTime()) / (24 * 60 * 60 * 1000));
        const filePath = path.join(opts.auditDir, entry);
        if (fileDate.getTime() >= cutoff.getTime()) {
            result.filesRetained += 1;
            continue;
        }
        if (opts.dryRun) {
            result.filesActioned.push({ path: filePath, ageDays, action: 'would-action' });
            continue;
        }
        try {
            if (opts.archiveDir) {
                const dest = path.join(opts.archiveDir, entry);
                fs.renameSync(filePath, dest);
                result.filesActioned.push({ path: filePath, ageDays, action: 'archived' });
            }
            else {
                fs.unlinkSync(filePath);
                result.filesActioned.push({ path: filePath, ageDays, action: 'deleted' });
            }
        }
        catch (err) {
            result.errors.push({ path: filePath, error: err.message });
        }
    }
    return result;
}
export function verifyChain(logDir, jsonOutput) {
    // Validate logDir argument
    if (!logDir) {
        process.stderr.write('Error: <logDir> argument is required.\n');
        process.stderr.write('Usage: ai-governance verify-chain <logDir> [--json]\n');
        process.exit(2);
    }
    if (!fs.existsSync(logDir)) {
        const msg = `Error: Log directory not found: ${logDir}`;
        if (jsonOutput) {
            process.stdout.write(JSON.stringify({
                ok: false,
                error: `Log directory not found: ${logDir}`,
                exitCode: 2,
            }) + '\n');
        }
        else {
            process.stderr.write(msg + '\n');
        }
        process.exit(2);
    }
    if (!fs.statSync(logDir).isDirectory()) {
        const msg = `Error: Path is not a directory: ${logDir}`;
        if (jsonOutput) {
            process.stdout.write(JSON.stringify({
                ok: false,
                error: `Path is not a directory: ${logDir}`,
                exitCode: 2,
            }) + '\n');
        }
        else {
            process.stderr.write(msg + '\n');
        }
        process.exit(2);
    }
    // Collect JSONL files from logDir
    let jsonlFiles;
    try {
        jsonlFiles = fs.readdirSync(logDir)
            .filter(f => f.endsWith('.jsonl'))
            .sort()
            .map(f => path.join(logDir, f));
    }
    catch (err) {
        const msg = `Error: Cannot read log directory: ${err.message}`;
        if (jsonOutput) {
            process.stdout.write(JSON.stringify({ ok: false, error: msg, exitCode: 2 }) + '\n');
        }
        else {
            process.stderr.write(msg + '\n');
        }
        process.exit(2);
    }
    // Empty directory is valid — no tamper possible on an empty chain.
    // F-NEW-MED-10 (2026-05-02): when the dir contains other artifacts
    // (subdirs, .log files, .txt files) but no .jsonl chain entries,
    // the previous output silently reported EMPTY without acknowledging
    // the non-chain artifacts. Operators routinely pointed verify-chain
    // at the WRONG directory (e.g. an audit-logs/ that only had a
    // manual-verifications/ subdirectory) and saw a clean EMPTY result
    // when they expected to see entries. We now surface non-chain
    // artifacts in the response so the operator notices the path is
    // probably wrong.
    if (jsonlFiles.length === 0) {
        let nonJsonlEntries = [];
        let subdirs = [];
        try {
            const dirents = fs.readdirSync(logDir, { withFileTypes: true });
            for (const d of dirents) {
                if (d.isDirectory())
                    subdirs.push(d.name);
                else if (!d.name.endsWith('.jsonl'))
                    nonJsonlEntries.push(d.name);
            }
        }
        catch { /* best effort */ }
        const hint = (nonJsonlEntries.length > 0 || subdirs.length > 0)
            ? ('Log directory has no .jsonl chain entries, but contains ' +
                `${nonJsonlEntries.length} non-chain file(s) and ${subdirs.length} subdirectory(ies). ` +
                (subdirs.length > 0
                    ? `Did you mean a subdirectory? Subdirs found: ${subdirs.slice(0, 5).join(', ')}${subdirs.length > 5 ? ' ...' : ''}. `
                    : '') +
                'verify-chain only reads .jsonl files in the directory you pass; it does not recurse.')
            : 'Log directory is empty. No chain to verify.';
        if (jsonOutput) {
            process.stdout.write(JSON.stringify({
                ok: true,
                status: 'EMPTY',
                chainLength: 0,
                files: 0,
                firstEntry: null,
                lastEntry: null,
                tamperFindings: [],
                nonChainFiles: nonJsonlEntries,
                subdirectories: subdirs,
                summary: hint,
            }) + '\n');
        }
        else {
            process.stdout.write('\nAudit Chain Verification\n');
            process.stdout.write('='.repeat(40) + '\n');
            process.stdout.write('  Status:      EMPTY\n');
            process.stdout.write('  Chain length: 0 entries\n');
            process.stdout.write('  Files:        0\n');
            if (nonJsonlEntries.length > 0) {
                process.stdout.write(`  Non-chain files: ${nonJsonlEntries.length} (e.g. ${nonJsonlEntries.slice(0, 3).join(', ')})\n`);
            }
            if (subdirs.length > 0) {
                process.stdout.write(`  Subdirectories: ${subdirs.length} (${subdirs.slice(0, 5).join(', ')})\n`);
            }
            process.stdout.write('\n' + hint + '\n');
        }
        process.exit(0);
    }
    const findings = [];
    let totalEntries = 0;
    let prevHash = '';
    let firstTimestamp = null;
    let lastTimestamp = null;
    let globalSeq = 0;
    function computeEntryHash(entry) {
        const hashInput = JSON.stringify({
            data: entry.data,
            _seq: entry._seq,
            _prevHash: entry._prevHash,
        });
        return crypto.createHash('sha256').update(hashInput).digest('hex');
    }
    for (const filePath of jsonlFiles) {
        const baseName = path.basename(filePath);
        let rawLines;
        try {
            const content = fs.readFileSync(filePath, 'utf-8');
            rawLines = content.trim().split('\n').filter(Boolean);
        }
        catch (err) {
            findings.push({
                file: baseName,
                lineNumber: 0,
                seq: -1,
                reason: `Cannot read file: ${err.message}`,
            });
            continue;
        }
        for (let lineIdx = 0; lineIdx < rawLines.length; lineIdx++) {
            globalSeq++;
            totalEntries++;
            const lineNumber = lineIdx + 1;
            let entry;
            try {
                entry = JSON.parse(rawLines[lineIdx]);
            }
            catch (parseErr) {
                findings.push({
                    file: baseName,
                    lineNumber,
                    seq: globalSeq,
                    reason: `Malformed JSON: ${parseErr.message}`,
                });
                // Skip prevHash update — chain is broken here
                continue;
            }
            // Validate required fields
            if (typeof entry._seq !== 'number' || typeof entry._prevHash !== 'string' || typeof entry._hash !== 'string') {
                findings.push({
                    file: baseName,
                    lineNumber,
                    seq: globalSeq,
                    reason: 'Entry missing required chain fields (_seq, _prevHash, _hash)',
                });
                continue;
            }
            // Track timestamps for summary
            const ts = (entry.data?.timestamp ?? entry.data?.ts);
            if (ts) {
                if (firstTimestamp === null)
                    firstTimestamp = ts;
                lastTimestamp = ts;
            }
            // Verify prevHash linkage
            if (entry._prevHash !== prevHash) {
                findings.push({
                    file: baseName,
                    lineNumber,
                    seq: entry._seq,
                    reason: `Chain break: _prevHash mismatch. Expected ${prevHash.slice(0, 16) || '(empty)'}, got ${entry._prevHash.slice(0, 16)}`,
                });
            }
            // Verify self-hash
            const expectedHash = computeEntryHash(entry);
            if (entry._hash !== expectedHash) {
                findings.push({
                    file: baseName,
                    lineNumber,
                    seq: entry._seq,
                    reason: `Tamper detected: entry hash does not match content. Entry seq=${entry._seq}`,
                });
            }
            prevHash = entry._hash;
        }
    }
    const intact = findings.length === 0;
    const status = intact ? 'INTACT' : 'TAMPERED';
    if (jsonOutput) {
        process.stdout.write(JSON.stringify({
            ok: intact,
            status,
            chainLength: totalEntries,
            files: jsonlFiles.length,
            firstEntry: firstTimestamp,
            lastEntry: lastTimestamp,
            tamperFindings: findings.map(f => ({
                file: f.file,
                line: f.lineNumber,
                seq: f.seq,
                reason: f.reason,
            })),
            summary: intact
                ? `Chain intact. ${totalEntries} entries verified across ${jsonlFiles.length} file(s).`
                : `TAMPER DETECTED. ${findings.length} finding(s) in ${totalEntries} entries.`,
        }, null, 2) + '\n');
    }
    else {
        process.stdout.write('\nAudit Chain Verification\n');
        process.stdout.write('='.repeat(40) + '\n');
        process.stdout.write(`  Status:       ${status}\n`);
        process.stdout.write(`  Chain length: ${totalEntries} entries\n`);
        process.stdout.write(`  Files:        ${jsonlFiles.length}\n`);
        if (firstTimestamp) {
            process.stdout.write(`  First entry:  ${firstTimestamp}\n`);
        }
        if (lastTimestamp) {
            process.stdout.write(`  Last entry:   ${lastTimestamp}\n`);
        }
        if (findings.length > 0) {
            process.stdout.write(`\n  TAMPER FINDINGS (${findings.length}):\n`);
            for (const f of findings) {
                process.stdout.write(`    [${f.file}:L${f.lineNumber}] seq=${f.seq}: ${f.reason}\n`);
            }
        }
        process.stdout.write('\n' + (intact
            ? `Chain integrity verified. ${totalEntries} entries checked.\n`
            : `INTEGRITY FAILURE. ${findings.length} finding(s) detected. Log may have been tampered.\n`));
    }
    process.exit(intact ? 0 : 1);
}
// --- Status command ---
function showStatus(projectDir) {
    const configPath = path.join(projectDir, '.governance.json');
    if (!fs.existsSync(configPath)) {
        process.stderr.write('No governance configured. Run "ai-governance init" first.\n');
        process.exit(1);
    }
    const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    const govDir = path.join(projectDir, '.governance');
    // Count audit entries
    const auditDir = path.join(govDir, 'audit');
    let auditEntries = 0;
    if (fs.existsSync(auditDir)) {
        const files = fs.readdirSync(auditDir).filter(f => f.endsWith('.jsonl'));
        for (const file of files) {
            try {
                const content = fs.readFileSync(path.join(auditDir, file), 'utf-8').trim();
                auditEntries += content ? content.split('\n').length : 0;
            }
            catch { /* skip */ }
        }
    }
    // Count hooks
    const hooksDir = path.join(govDir, 'hooks');
    const hookCount = fs.existsSync(hooksDir)
        ? fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.ps1')).length
        : 0;
    process.stdout.write('\nGovernance Status\n');
    process.stdout.write('='.repeat(40) + '\n');
    process.stdout.write(`  Version:      ${config.version}\n`);
    process.stdout.write(`  License:      ${config.license}\n`);
    process.stdout.write(`  Packs:        ${config.packs.join(', ')}\n`);
    process.stdout.write(`  Hooks:        ${hookCount} installed\n`);
    process.stdout.write(`  Audit entries: ${auditEntries}\n`);
    process.stdout.write(`  Encryption:   ${config.encryption?.enabled ? 'enabled' : 'disabled'}\n`);
    process.stdout.write(`  Created:      ${config.createdAt}\n`);
    // Per-pack allow/deny rule summary.
    // Thomas directive 2026-05-17 evening: "It must start up and report
    // findings to a local database on the compliance and company packs,
    // including what they are allowed and not allowed to do."
    //
    // Pre-fix `status` showed pack NAMES (e.g. "hipaa, soc2") but no rules.
    // A customer couldn't answer "what does HIPAA block on my install?"
    // from the CLI. Now each bound pack expands to its categoryActions
    // (BLOCK / WARN / REQUIRE_APPROVAL / ALLOW), blocked LLM providers,
    // and required modules so the answer is one terminal scroll away.
    if (Array.isArray(config.packs) && config.packs.length > 0) {
        process.stdout.write(`\nPack rules\n${'='.repeat(40)}\n`);
        for (const packId of config.packs) {
            const normalizedId = packId.toLowerCase().replace(/[\s-_]/g, '');
            const pack = BUILT_IN_PACKS[packId] ?? BUILT_IN_PACKS[normalizedId];
            if (!pack) {
                process.stdout.write(`  ${packId}:        (unknown pack — not in BUILT_IN_PACKS)\n`);
                continue;
            }
            process.stdout.write(`\n  ${pack.id} — ${pack.name} (v${pack.version}):\n`);
            const categoryActions = pack.dataPolicy?.categoryActions;
            if (categoryActions && Object.keys(categoryActions).length > 0) {
                process.stdout.write(`    Category actions:\n`);
                const sortedEntries = Object.entries(categoryActions).sort(([, a], [, b]) => {
                    const order = { BLOCK: 0, REQUIRE_APPROVAL: 1, WARN: 2, LOG: 3, ALLOW: 4 };
                    return (order[a] ?? 5) - (order[b] ?? 5);
                });
                for (const [category, action] of sortedEntries) {
                    process.stdout.write(`      ${category.padEnd(36)} ${action}\n`);
                }
            }
            else {
                process.stdout.write(`    Category actions: (none defined)\n`);
            }
            const providerPolicy = pack.providerPolicy;
            if (providerPolicy?.blockedProviders && providerPolicy.blockedProviders.length > 0) {
                process.stdout.write(`    Blocked providers: ${providerPolicy.blockedProviders.join(', ')}\n`);
            }
            if (providerPolicy?.allowedProviders && providerPolicy.allowedProviders.length > 0) {
                process.stdout.write(`    Allowed providers: ${providerPolicy.allowedProviders.join(', ')}\n`);
            }
            const requiredModules = pack.requiredModules;
            if (requiredModules && requiredModules.length > 0) {
                const names = requiredModules
                    .map((m) => (typeof m === 'string' ? m : m?.name ?? 'unknown'))
                    .filter(Boolean);
                process.stdout.write(`    Required modules:  ${names.join(', ')}\n`);
            }
        }
    }
    // Runtime push health — 2026-05-17 customer-debug enhancement.
    // Without these lines, customers couldn't tell from `status` whether their
    // hooks were actually reaching the platform or no-oping silently.
    const runtime = config.runtime;
    process.stdout.write(`\nRuntime push\n${'='.repeat(40)}\n`);
    if (!runtime || !runtime.serviceToken) {
        process.stdout.write(`  Status:       NOT WIRED — hooks fire locally only.\n`);
        process.stdout.write(`  Fix:          Re-run \`ai-governance init --vendor-code <code>\`\n`);
    }
    else {
        const govServerUrl = runtime.govServerUrl;
        const orgId = runtime.orgId;
        const agentId = runtime.agentId;
        const token = runtime.serviceToken;
        process.stdout.write(`  Gov-server:   ${govServerUrl ?? 'MISSING'}\n`);
        process.stdout.write(`  Org:          ${orgId ?? 'MISSING'}\n`);
        process.stdout.write(`  Agent:        ${agentId ?? 'MISSING'}\n`);
        // Decode the service token (best-effort) to surface expiry
        try {
            const parts = token.split('.');
            if (parts.length === 3) {
                const claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
                if (claims.exp) {
                    const expDate = new Date(claims.exp * 1000);
                    const daysLeft = Math.floor((expDate.getTime() - Date.now()) / 86_400_000);
                    const expWarn = daysLeft < 30 ? ' ⚠ (consider `ai-governance rotate-token`)' : '';
                    process.stdout.write(`  Token expires: ${expDate.toISOString()} (${daysLeft}d)${expWarn}\n`);
                }
                if (claims.role) {
                    process.stdout.write(`  Token role:   ${claims.role}\n`);
                }
            }
        }
        catch { /* malformed token; surfaced by ping below */ }
        // Best-effort ping — short timeout so `status` never hangs
        if (govServerUrl) {
            try {
                const { spawnSync } = require('node:child_process');
                const result = spawnSync('curl', [
                    '--silent', '--show-error',
                    '--max-time', '3',
                    '--connect-timeout', '1',
                    '-o', '/dev/null',
                    '-w', '%{http_code}',
                    `${govServerUrl.replace(/\/$/, '')}/api/v1/agents?org=${orgId ?? ''}`,
                    '-H', `Authorization: Bearer ${token}`,
                ], { encoding: 'utf8' });
                const code = result.stdout?.trim() || '?';
                const verdict = code === '200' ? '✅ reachable + token valid' :
                    code === '401' ? '⚠ token rejected (try `ai-governance rotate-token`)' :
                        code === '000' ? '❌ unreachable (check govServerUrl + network)' :
                            `⚠ HTTP ${code}`;
                process.stdout.write(`  Live check:   ${verdict}\n`);
            }
            catch {
                process.stdout.write(`  Live check:   skipped (curl unavailable)\n`);
            }
        }
    }
    process.stdout.write('\n');
}
// ---------------------------------------------------------------------------
// F-NEW-PRODUCT-AGENT-DIR-WRAPPER slice 3: generateAutowrapArtifacts
// ---------------------------------------------------------------------------
/**
 * Generate .governance/autowrap.py and .governance/autowrap.js in agentDir.
 *
 * These scripts can be passed to Python / Node as bootstrap loaders:
 *   CONNEXUM_AUTOWRAP=1 python -c "import connexum_governance; from openai import OpenAI"
 *   NODE_OPTIONS='--require ./.governance/autowrap.js' node agent.js
 *
 * The function prints clear env-var instructions to stdout.
 * It does NOT modify the user's shell profile or global environment -- that
 * would be intrusive and violate the principle of least surprise.
 */
export function generateAutowrapArtifacts(agentDir, detectedAgents, projectDir) {
    const govDir = path.join(agentDir, '.governance');
    if (!fs.existsSync(govDir)) {
        fs.mkdirSync(govDir, { recursive: true });
    }
    // Determine packs from detected agents (deduplicated union)
    const allPacks = new Set();
    for (const agent of detectedAgents) {
        for (const pack of agent.packs) {
            allPacks.add(pack);
        }
    }
    const tenantId = 'local-dev';
    const packs = JSON.stringify([...allPacks]);
    // --- Python bootstrap ---
    const pyPath = path.join(govDir, 'autowrap.py');
    const pyContent = [
        '"""',
        'Connexum governance autowrap bootstrap -- generated by ai-governance CLI',
        'Edit governance.json to change pack assignments. Do not edit this file.',
        '',
        'Usage:',
        '  CONNEXUM_AUTOWRAP=1 python your_agent.py',
        '',
        'Or add to your shell profile / CI env:',
        '  export CONNEXUM_AUTOWRAP=1',
        '"""',
        'import os as _os',
        'import sys as _sys',
        '',
        '# Activate governance before any provider imports',
        'if _os.environ.get("CONNEXUM_AUTOWRAP") == "1":',
        '    try:',
        '        from connexum_governance.autowrap import activate',
        '        activate()',
        '    except ImportError as _e:',
        '        print(',
        '            f"[connexum-autowrap] ERROR: connexum_governance not installed: {_e}",',
        '            file=_sys.stderr,',
        '        )',
        `    # Governance context: tenantId=${tenantId}, packs=${packs}`,
        '',
    ].join('\n');
    fs.writeFileSync(pyPath, pyContent, { mode: 0o644 });
    process.stdout.write(`  [autowrap] Python bootstrap: ${pyPath}\n`);
    // --- Node.js bootstrap (CommonJS --require script) ---
    const jsPath = path.join(govDir, 'autowrap.js');
    const jsContent = [
        '/**',
        ' * Connexum governance autowrap bootstrap -- generated by ai-governance CLI',
        ' * Edit governance.json to change pack assignments. Do not edit this file.',
        ' *',
        ' * Usage (add to dev shell or CI):',
        ' *   NODE_OPTIONS="--require ' + path.relative(projectDir, jsPath) + '" node agent.js',
        ' *',
        ' * Or for a single invocation:',
        ' *   CONNEXUM_AUTOWRAP=1 node -e "require(\\".governance/autowrap.js\\"); const {OpenAI} = require(\\"openai\\");"',
        ' */',
        "'use strict';",
        '',
        'if (process.env.CONNEXUM_AUTOWRAP === "1") {',
        '  try {',
        "    const { register } = require('@connexum/typescript-sdk/autowrap');",
        '    register();',
        '  } catch (err) {',
        '    process.stderr.write(',
        "      '[connexum-autowrap] ERROR: @connexum/typescript-sdk not installed: ' + err.message + '\\n',",
        '    );',
        '  }',
        '}',
        `// Governance context: tenantId=${tenantId}, packs=${packs}`,
        '',
    ].join('\n');
    fs.writeFileSync(jsPath, jsContent, { mode: 0o644 });
    process.stdout.write(`  [autowrap] Node.js bootstrap: ${jsPath}\n`);
    // Print activation instructions
    const relJs = path.relative(projectDir, jsPath);
    process.stdout.write('\n');
    process.stdout.write('Autowrap activation instructions:\n');
    process.stdout.write('\n');
    process.stdout.write('  Python:\n');
    process.stdout.write('    Add to your dev shell or CI environment:\n');
    process.stdout.write('      export CONNEXUM_AUTOWRAP=1\n');
    process.stdout.write('    Or activate per-invocation:\n');
    process.stdout.write('      CONNEXUM_AUTOWRAP=1 python your_agent.py\n');
    process.stdout.write('\n');
    process.stdout.write('  Node.js:\n');
    process.stdout.write('    Add to your dev shell or CI environment:\n');
    process.stdout.write(`      export NODE_OPTIONS='--require ${relJs}'\n`);
    process.stdout.write('      export CONNEXUM_AUTOWRAP=1\n');
    process.stdout.write('    Or activate per-invocation:\n');
    process.stdout.write(`      CONNEXUM_AUTOWRAP=1 node --require ${relJs} your_agent.js\n`);
    process.stdout.write('\n');
    process.stdout.write('  Note: These bootstrap scripts patch CJS require() calls. Pure ESM modules\n');
    process.stdout.write('  require slice 1 (--agent-dir) or slice 2 (.governed.ts shim files) instead.\n');
}
// --- Main ---
// Only run the CLI when invoked directly, not when imported by tests.
const isDirectRun = require.main === module;
const args = process.argv.slice(2);
const command = args[0];
// SYSTEMIC-H3: CLI escape hatch for malformed governance config.
// When --force-empty-config is passed, we set the matching env var so
// GovernanceConfig.handleConfigFailure() sees it. The bypass is AUDIT
// LOGGED via GovernanceConfig.consumeForceEmptyOverride() at boot.
if (args.includes('--force-empty-config')) {
    process.env.FORCE_EMPTY_GOVERNANCE_CONFIG = 'true';
    process.env.FORCE_EMPTY_GOVERNANCE_CONFIG_SOURCE = 'cli';
}
if (isDirectRun && (command === '--version' || command === '-v')) {
    process.stdout.write(`@connexum/ai-governance v${VERSION}\n`);
    process.exit(0);
}
if (isDirectRun && (command === '--help' || command === '-h' || !command)) {
    process.stdout.write(`
@connexum/ai-governance v${VERSION}
Enterprise AI governance for Claude Code environments.

Usage:
  ai-governance init                         Interactive setup
  ai-governance init --vendor-code <code>    Non-interactive Vendor Code
  ai-governance init --offline               Offline grace mode (requires env ack)
  ai-governance init --ci                    Non-interactive (CI) setup
    --packs soc2,hipaa                       Compliance frameworks
    --license CXGOV-...                      License key
  ai-governance init --agent-dir <path>      Scan custom agent directory (slices 1-4)
    [--skip-preflight]                       Skip preflight sidecar check
    [--sidecar-url <url>]                    Override sidecar URL (default: http://localhost:4201)
  ai-governance verify                       Verify governance file integrity
  ai-governance verify-chain <logDir>        Verify audit chain integrity (standalone)
    [--json]                                 Output machine-readable JSON
  ai-governance status                       Show governance status
  ai-governance discover                     Scan project + push signed manifests
    --root <path>                            Project directory to scan
    --org-id <org>                           Organisation identifier
    --key <pem-path>                         Ed25519 private key (PEM)
    --endpoint <url>                         Manifest receive endpoint
    [--dry-run]                              Discover + report without push
    [--max-depth <n>]                        Recursion depth (default 8)
  ai-governance packs <select|get|upgrade>   Manage per-project compliance packs
    --project <id>                           Project ID (required)
    --packs <list>                           Comma-separated pack IDs (select)
    --pack <id> --version <semver>           Pin pack version (upgrade)
    [--api-url <url>]                        Governance server (default https://api.my-cc.io)
    [--license-key <jwt>]                    Bearer JWT (or CONNEXUM_LICENSE env)
  ai-governance retention-sweep              Sweep customer audit logs by age
    [--audit-dir <dir>]                      Audit dir (default .governance/audit)
    [--retain-days <n>]                      Retention window (default 2190 = 6y)
    [--archive-dir <dir>]                    Archive destination (else delete)
    [--dry-run]                              Report without acting
    [--json]                                 Machine-readable output
  ai-governance rotate-token                 Rotate the runtime service token
                                             Reads .governance.json runtime.serviceToken,
                                             requests a fresh token from the gov-server,
                                             writes it back. Use case: token approaching
                                             1-year TTL, compromised, or rotated for policy.
  ai-governance bootstrap-admin              Create the first ORG_ADMIN for a fresh tenant
    --vendor-code <code>                     Vendor code from /start provisioning
    --email <email>                          Admin email
    --password <password>                    Admin password (min 8 chars)
    [--gov-server-url <url>]                 Override gov-server URL (default https://api.my-cc.io)
    [--write-config]                         Persist tokens to .governance.json 'admin' section
                                             Headless customer-onboarding for CI / IaC flows.
                                             Equivalent to setting a password on /start Screen4.

Subscription:
  Vendor Codes are emailed on purchase. Set CXNI_VENDOR_CODE env var or use
  --vendor-code <code> for non-interactive installs.
  Contact thomas@ucreatewithai.com or visit ${BILLING_URL}

Options:
  --version, -v                Show version
  --help, -h                   Show this help
  --vendor-code <code>         Supply Vendor Code non-interactively
  --offline                    Bypass subscription check (requires
                               CXNI_OFFLINE_GRACE_ACKNOWLEDGED=true)
  --license-server <url>       Override license server URL (persisted to config)
  --license-server-url <url>   Alias for --license-server
  --force-empty-config         (EMERGENCY) Bypass a malformed .governance.json
                               and boot with an empty config. This action is
                               audit-logged and is NOT a valid production mode.

verify-chain:
  Download your JSONL audit log directory and run:
    ai-governance verify-chain ./audit-logs
  Exit code 0 = chain intact, 1 = tamper detected, 2 = input error.
  Add --json for machine-readable output suitable for compliance pipelines.

Documentation: https://my-cc.io/docs/ai-governance
Connexum Network Inc.
`.trim() + '\n');
    process.exit(0);
}
if (isDirectRun) {
    const projectDir = process.cwd();
    if (command === 'init') {
        const ciMode = args.includes('--ci');
        // S-07: Parse vendor code flags
        const vendorCodeIdx = args.indexOf('--vendor-code');
        const vendorCodeFlag = vendorCodeIdx >= 0 ? args[vendorCodeIdx + 1] : undefined;
        const offlineFlag = args.includes('--offline');
        // License server URL: accept both --license-server and --license-server-url
        // --license-server is the shorter alias (matches task spec requirement)
        const lsIdx = args.indexOf('--license-server') >= 0
            ? args.indexOf('--license-server')
            : args.indexOf('--license-server-url');
        const licenseServerUrl = lsIdx >= 0 ? args[lsIdx + 1] : undefined;
        // F-NEW-CUSTOMER-JOURNEY-001: hook→gov-server runtime push config.
        // All four flags must be present together to enable the push path.
        // Env vars (CXNI_GOV_URL / CXNI_ORG_ID / CXNI_AGENT_ID /
        // CXNI_SERVICE_TOKEN) serve as fallbacks for CI environments.
        const govServerUrlIdx = args.indexOf('--gov-server-url');
        const orgIdIdx = args.indexOf('--org-id');
        const agentIdIdx = args.indexOf('--agent-id');
        const serviceTokenIdx = args.indexOf('--service-token');
        const runtimeGovServerUrl = (govServerUrlIdx >= 0 ? args[govServerUrlIdx + 1] : undefined) ?? process.env.CXNI_GOV_URL;
        const runtimeOrgId = (orgIdIdx >= 0 ? args[orgIdIdx + 1] : undefined) ?? process.env.CXNI_ORG_ID;
        const runtimeAgentId = (agentIdIdx >= 0 ? args[agentIdIdx + 1] : undefined) ?? process.env.CXNI_AGENT_ID;
        const runtimeServiceToken = (serviceTokenIdx >= 0 ? args[serviceTokenIdx + 1] : undefined) ?? process.env.CXNI_SERVICE_TOKEN;
        let runtimeConfig = runtimeGovServerUrl && runtimeOrgId && runtimeAgentId && runtimeServiceToken
            ? {
                govServerUrl: runtimeGovServerUrl,
                orgId: runtimeOrgId,
                agentId: runtimeAgentId,
                serviceToken: runtimeServiceToken,
            }
            : undefined;
        // 2026-05-17 customer-journey fix: when --vendor-code is the only
        // identifier the customer provides, exchange it server-side for the
        // four runtime fields. Without this, hooks fire locally but never push
        // to the platform — customer sees empty portal and churns.
        // Uses sync curl spawn because the surrounding init block is synchronous
        // (tsc CommonJS target rejects top-level await).
        if (!runtimeConfig && vendorCodeFlag) {
            const exchangeServer = process.env.CXNI_GOV_URL ??
                runtimeGovServerUrl ??
                licenseServerUrl ??
                DEFAULT_GOV_SERVER_URL;
            try {
                const { spawnSync } = require('node:child_process');
                const result = spawnSync('curl', [
                    '--silent',
                    '--show-error',
                    '--max-time', '10',
                    '--connect-timeout', '3',
                    '-X', 'POST',
                    `${exchangeServer.replace(/\/$/, '')}/api/v1/cli/exchange-vendor-code`,
                    '-H', 'Content-Type: application/json',
                    '--data', JSON.stringify({ vendorCode: vendorCodeFlag }),
                ], { encoding: 'utf8' });
                if (result.status === 0 && result.stdout) {
                    try {
                        const exchanged = JSON.parse(result.stdout);
                        if (exchanged.govServerUrl && exchanged.orgId && exchanged.agentId && exchanged.serviceToken) {
                            runtimeConfig = {
                                govServerUrl: exchanged.govServerUrl,
                                orgId: exchanged.orgId,
                                agentId: exchanged.agentId,
                                serviceToken: exchanged.serviceToken,
                            };
                            process.stderr.write(`[CLI] vendor-code exchanged: orgId=${exchanged.orgId} agentId=${exchanged.agentId}\n`);
                        }
                        else if (exchanged.error) {
                            process.stderr.write(`[CLI] vendor-code exchange returned error: ${exchanged.error}\n`);
                        }
                    }
                    catch (e) {
                        process.stderr.write(`[CLI] vendor-code exchange parse failed: ${e.message}\n`);
                    }
                }
                else if (result.stderr) {
                    process.stderr.write(`[CLI] vendor-code exchange request failed: ${result.stderr.trim()}\n`);
                }
            }
            catch (e) {
                process.stderr.write(`[CLI] vendor-code exchange skipped (${e.message}). Hooks will run locally but not push to the platform.\n`);
            }
        }
        // F-NEW-PRODUCT-AGENT-DIR-WRAPPER slice 1: --agent-dir flag
        // Scans the specified directory for AI usage signatures and merges
        // detected agents into governance.json `agents[]`. Additive: does not
        // replace IDE marker detection. Runs before (and independently of)
        // the interactive/CI flow so governance.json already has agents[] when
        // the hook installer runs.
        const agentDirIdx = args.indexOf('--agent-dir');
        const agentDirFlag = agentDirIdx >= 0 ? args[agentDirIdx + 1] : undefined;
        if (agentDirFlag) {
            // Wrap in async IIFE so slice 4 await calls work without converting the
            // entire top-level init block to async (which would break the existing
            // sync interactiveInit / nonInteractiveInit paths below).
            void (async () => {
                const resolvedAgentDir = path.isAbsolute(agentDirFlag)
                    ? agentDirFlag
                    : path.join(projectDir, agentDirFlag);
                if (!fs.existsSync(resolvedAgentDir)) {
                    process.stderr.write(`--agent-dir: directory not found: ${resolvedAgentDir}\n`);
                    process.exit(1);
                }
                process.stdout.write(`Scanning agent directory: ${resolvedAgentDir}\n`);
                const detectedAgents = scanAgentDirectory(resolvedAgentDir);
                process.stdout.write(`Detected ${detectedAgents.length} agent file(s).\n`);
                // Merge into governance.json (create/update)
                const configPath = path.join(projectDir, 'governance.json');
                let config = {};
                if (fs.existsSync(configPath)) {
                    try {
                        config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
                    }
                    catch { /* fresh */ }
                }
                const existing = Array.isArray(config['agents']) ? config['agents'] : [];
                // Merge: replace any existing auto-<id> entry for the same filePath, append new ones
                const merged = [...existing];
                for (const agent of detectedAgents) {
                    const idx = merged.findIndex(a => a.filePath === agent.filePath);
                    if (idx >= 0) {
                        merged[idx] = agent;
                    }
                    else {
                        merged.push(agent);
                    }
                }
                config['agents'] = merged;
                fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
                process.stdout.write(`governance.json updated: ${merged.length} total agent(s).\n`);
                process.stdout.write(`Config: ${configPath}\n`);
                for (const agent of detectedAgents) {
                    process.stdout.write(`  [${agent.agentId}] ${agent.filePath} ` +
                        `(${agent.language}, provider=${agent.provider ?? 'null'}, framework=${agent.framework ?? 'null'})\n`);
                }
                // F-NEW-PRODUCT-AGENT-DIR-WRAPPER slice 2: generate wrap-shim files
                process.stdout.write('\nGenerating governance shim files...\n');
                const shimResults = writeWrapShims(detectedAgents, resolvedAgentDir, (msg) => process.stdout.write(msg + '\n'));
                const generated = shimResults.filter(r => r.status === 'generated').length;
                const skipped = shimResults.filter(r => r.status !== 'generated').length;
                process.stdout.write(`Shim generation complete: ${generated} written, ${skipped} skipped.\n`);
                // F-NEW-PRODUCT-AGENT-DIR-WRAPPER slice 3: --autowrap flag
                // Generates .governance/autowrap.py and .governance/autowrap.js in the
                // agent directory and prints the env-var activation instructions.
                // Does NOT write to global env -- that would be intrusive.
                if (args.includes('--autowrap')) {
                    generateAutowrapArtifacts(resolvedAgentDir, detectedAgents, projectDir);
                }
                // F-NEW-PRODUCT-AGENT-DIR-WRAPPER slice 4: preflight + green-checkmark report
                const skipPreflight = args.includes('--skip-preflight');
                if (!skipPreflight && detectedAgents.length > 0) {
                    const sidecarUrlIdx = args.indexOf('--sidecar-url');
                    const sidecarUrl = sidecarUrlIdx >= 0
                        ? args[sidecarUrlIdx + 1]
                        : (process.env['CONNEXUM_SIDECAR_URL'] ?? 'http://localhost:4201');
                    try {
                        process.stdout.write('\nRunning preflight checks against governance-sidecar...\n');
                        const results = await preflightAgents(detectedAgents, resolvedAgentDir, { sidecarUrl });
                        const report = formatPreflightReport(results, {
                            useColour: process.stdout.isTTY,
                            showDetail: true,
                        });
                        process.stdout.write(report);
                    }
                    catch (err) {
                        process.stderr.write(`--agent-dir error: ${err.message}\n`);
                        process.exit(1);
                    }
                }
                else if (skipPreflight) {
                    process.stdout.write('\nPreflight skipped (--skip-preflight).\n');
                }
                // --agent-dir is a standalone operation — do not fall through to interactive init
                // unless --ci or interactive flow flags are also explicitly set.
                if (!ciMode && !vendorCodeFlag && !offlineFlag) {
                    process.exit(0);
                }
            })();
        }
        if (ciMode) {
            const packsIdx = args.indexOf('--packs');
            const licenseIdx = args.indexOf('--license');
            nonInteractiveInit(projectDir, {
                packs: packsIdx >= 0 ? args[packsIdx + 1] : undefined,
                license: licenseIdx >= 0 ? args[licenseIdx + 1] : undefined,
                licenseServerUrl,
                runtime: runtimeConfig,
            });
        }
        else if (!agentDirFlag) {
            interactiveInit(projectDir, { vendorCodeFlag, offlineFlag, licenseServerUrl, runtime: runtimeConfig }).catch(err => {
                process.stderr.write(`Error: ${err.message}\n`);
                process.exit(1);
            });
        }
    }
    else if (command === 'verify') {
        verifyGovernance(projectDir);
    }
    else if (command === 'verify-chain') {
        // GAP-S1-02: standalone audit chain verifier
        // Usage: ai-governance verify-chain <logDir> [--json]
        const logDir = args[1];
        const jsonOutput = args.includes('--json');
        verifyChain(logDir, jsonOutput);
    }
    else if (command === 'retention-sweep') {
        // F-NEW-AUDIT-RETENTION-1: customer-side audit log retention.
        // Usage:
        //   ai-governance retention-sweep [--audit-dir DIR] [--retain-days N]
        //                                  [--archive-dir DIR] [--dry-run] [--json]
        const auditDirIdx = args.indexOf('--audit-dir');
        const auditDir = auditDirIdx >= 0
            ? args[auditDirIdx + 1]
            : path.join(projectDir, '.governance', 'audit');
        const retainDaysIdx = args.indexOf('--retain-days');
        const retainDays = retainDaysIdx >= 0 ? parseInt(args[retainDaysIdx + 1], 10) : 2190;
        if (!Number.isFinite(retainDays) || retainDays < 1) {
            process.stderr.write('Error: --retain-days must be a positive integer.\n');
            process.exit(2);
        }
        const archiveDirIdx = args.indexOf('--archive-dir');
        const archiveDir = archiveDirIdx >= 0 ? args[archiveDirIdx + 1] : null;
        const dryRun = args.includes('--dry-run');
        const jsonOutput = args.includes('--json');
        const result = retentionSweep({ auditDir, retainDays, archiveDir, dryRun });
        if (jsonOutput) {
            process.stdout.write(JSON.stringify(result, null, 2) + '\n');
        }
        else {
            process.stdout.write(`Audit retention sweep\n`);
            process.stdout.write(`  audit dir:        ${result.auditDir}\n`);
            process.stdout.write(`  retain days:      ${result.retainDays}\n`);
            process.stdout.write(`  cutoff:           ${result.cutoffIso}\n`);
            process.stdout.write(`  archive dir:      ${result.archiveDir ?? '(none, files deleted)'}\n`);
            process.stdout.write(`  dry run:          ${result.dryRun}\n`);
            process.stdout.write(`  files scanned:    ${result.filesScanned}\n`);
            process.stdout.write(`  files retained:   ${result.filesRetained}\n`);
            process.stdout.write(`  files actioned:   ${result.filesActioned.length}\n`);
            for (const a of result.filesActioned) {
                process.stdout.write(`    - ${a.action}: ${a.path} (age ${a.ageDays}d)\n`);
            }
            if (result.errors.length > 0) {
                process.stdout.write(`  errors:           ${result.errors.length}\n`);
                for (const e of result.errors) {
                    process.stdout.write(`    - ${e.path}: ${e.error}\n`);
                }
            }
        }
        process.exit(result.errors.length > 0 ? 1 : 0);
    }
    else if (command === 'status') {
        showStatus(projectDir);
    }
    else if (command === 'packs') {
        // W2: per-project pack selection CLI subcommand
        import('./packs.js').then(({ runPacksCommand }) => {
            runPacksCommand(args.slice(1)).catch((err) => {
                process.stderr.write(`Error: ${err.message}\n`);
                process.exit(2);
            });
        });
    }
    else if (command === 'discover') {
        // F-NEW-PRODUCT-AGENT-DISCOVERY-CLI: scan project + sign + push manifests.
        // Closes the W2.1 customer-visible loop. See src/cli/discover.ts.
        import('./discover.js').then(({ runDiscoverCommand }) => {
            runDiscoverCommand(args.slice(1)).then((code) => process.exit(code)).catch((err) => {
                process.stderr.write(`Error: ${err.message}\n`);
                process.exit(2);
            });
        });
    }
    else if (command === 'bootstrap-admin') {
        // 2026-05-17: headless first-admin creation. Used by CI / IaC flows
        // that provision tenants automatically without going through /start
        // in a browser. Requires: --vendor-code, --email, --password
        // (or env CXNI_VENDOR_CODE / CXNI_BOOTSTRAP_EMAIL / CXNI_BOOTSTRAP_PASSWORD).
        // Optional: --gov-server-url (defaults to CXNI_GOV_URL or DEFAULT_GOV_SERVER_URL = https://api.my-cc.io).
        (() => {
            const { spawnSync } = require('node:child_process');
            const fs = require('node:fs');
            const path = require('node:path');
            const argv = args.slice(1);
            const flag = (name, env) => {
                const idx = argv.indexOf(name);
                const v = idx >= 0 ? argv[idx + 1] : undefined;
                return v ?? (env ? process.env[env] : undefined);
            };
            const vendorCode = flag('--vendor-code', 'CXNI_VENDOR_CODE');
            const email = flag('--email', 'CXNI_BOOTSTRAP_EMAIL');
            const password = flag('--password', 'CXNI_BOOTSTRAP_PASSWORD');
            const govServerUrl = flag('--gov-server-url', 'CXNI_GOV_URL') ?? DEFAULT_GOV_SERVER_URL;
            const writeConfig = argv.includes('--write-config');
            if (!vendorCode || !email || !password) {
                process.stderr.write('bootstrap-admin requires --vendor-code, --email, --password (or env vars CXNI_VENDOR_CODE / CXNI_BOOTSTRAP_EMAIL / CXNI_BOOTSTRAP_PASSWORD)\n');
                process.exit(1);
            }
            if (password.length < 8) {
                process.stderr.write('password must be at least 8 characters\n');
                process.exit(1);
            }
            process.stderr.write(`Bootstrapping admin against ${govServerUrl} for vendorCode=${vendorCode.slice(0, 16)}…\n`);
            const result = spawnSync('curl', [
                '--silent', '--show-error',
                '--max-time', '15',
                '--connect-timeout', '5',
                '-X', 'POST',
                `${govServerUrl.replace(/\/$/, '')}/api/v1/cli/bootstrap-admin`,
                '-H', 'Content-Type: application/json',
                '--data', JSON.stringify({ vendorCode, email, password }),
            ], { encoding: 'utf8' });
            if (result.status !== 0 || !result.stdout) {
                process.stderr.write(`Bootstrap request failed: ${result.stderr?.trim() || 'no stdout'}\n`);
                process.exit(1);
            }
            let payload;
            try {
                payload = JSON.parse(result.stdout);
            }
            catch (e) {
                process.stderr.write(`Bootstrap response was not JSON: ${result.stdout.slice(0, 200)}\n`);
                process.exit(1);
                return;
            }
            if (!payload.accessToken) {
                process.stderr.write(`Bootstrap rejected: ${payload.detail ?? payload.error ?? JSON.stringify(payload)}\n`);
                process.exit(1);
            }
            // Optionally persist the bootstrap credentials into .governance.json
            // so subsequent CLI calls (e.g., `discover`) don't need to re-supply
            // the access token. Off by default — most callers will pipe stdout
            // into their own secret store.
            if (writeConfig) {
                const configPath = path.join(projectDir, '.governance.json');
                let config = {};
                if (fs.existsSync(configPath)) {
                    try {
                        config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
                    }
                    catch {
                        // overwrite if malformed
                    }
                }
                config['admin'] = {
                    email,
                    orgId: payload.orgId,
                    userId: payload.userId,
                    role: payload.role,
                    accessToken: payload.accessToken,
                    refreshToken: payload.refreshToken,
                    createdAt: new Date().toISOString(),
                };
                fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
                try {
                    fs.chmodSync(configPath, 0o600);
                }
                catch { /* best-effort on Windows */ }
                process.stderr.write(`Wrote admin section to ${configPath}\n`);
            }
            // Always print the result so CI can capture it.
            process.stdout.write(JSON.stringify({
                bootstrapped: true,
                orgId: payload.orgId,
                userId: payload.userId,
                role: payload.role,
                accessToken: payload.accessToken,
                refreshToken: payload.refreshToken,
            }) + '\n');
            process.exit(0);
        })();
    }
    else if (command === 'rotate-token') {
        // 2026-05-17: rotate the service token without burning a vendor-code redemption.
        // Reads .governance.json runtime.serviceToken, POSTs to
        // /api/v1/cli/rotate-token, writes the new token back. Use case:
        // approaching the 1-year TTL, token compromised, or token lost.
        (() => {
            const fs = require('node:fs');
            const path = require('node:path');
            const { spawnSync } = require('node:child_process');
            const configPath = path.join(projectDir, '.governance.json');
            if (!fs.existsSync(configPath)) {
                process.stderr.write('No .governance.json found in current directory. Run `ai-governance init` first.\n');
                process.exit(1);
            }
            let config;
            try {
                config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
            }
            catch (e) {
                process.stderr.write(`Failed to parse .governance.json: ${e.message}\n`);
                process.exit(1);
                return;
            }
            const runtime = config['runtime'] ?? {};
            const serviceToken = typeof runtime['serviceToken'] === 'string' ? runtime['serviceToken'] : '';
            const govServerUrl = typeof runtime['govServerUrl'] === 'string' ? runtime['govServerUrl'] : '';
            if (!serviceToken) {
                process.stderr.write('No runtime.serviceToken in .governance.json. Re-run `init --vendor-code` to obtain one.\n');
                process.exit(1);
            }
            if (!govServerUrl) {
                process.stderr.write('No runtime.govServerUrl in .governance.json.\n');
                process.exit(1);
            }
            process.stderr.write(`Rotating service token against ${govServerUrl} …\n`);
            const result = spawnSync('curl', [
                '--silent', '--show-error',
                '--max-time', '15',
                '--connect-timeout', '5',
                '-X', 'POST',
                `${govServerUrl.replace(/\/$/, '')}/api/v1/cli/rotate-token`,
                '-H', `Authorization: Bearer ${serviceToken}`,
                '-H', 'Content-Type: application/json',
                '--data', '{}',
            ], { encoding: 'utf8' });
            if (result.status !== 0 || !result.stdout) {
                process.stderr.write(`Rotation request failed: ${result.stderr?.trim() || 'no stdout'}\n`);
                process.exit(1);
            }
            let payload;
            try {
                payload = JSON.parse(result.stdout);
            }
            catch (e) {
                process.stderr.write(`Rotation response was not JSON: ${result.stdout.slice(0, 200)}\n`);
                process.exit(1);
                return;
            }
            if (!payload.serviceToken) {
                process.stderr.write(`Rotation rejected: ${payload.detail ?? payload.error ?? JSON.stringify(payload)}\n`);
                if ((payload.error ?? '') === 'token_expired') {
                    process.stderr.write('Token already expired — re-run `ai-governance init --vendor-code <code>` to obtain a fresh one.\n');
                }
                process.exit(1);
            }
            config['runtime'].serviceToken = payload.serviceToken;
            fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
            // Explicit chmod — writeFileSync's `mode` flag only applies on create,
            // and .governance.json already exists. Service token is sensitive →
            // owner-only read/write.
            try {
                fs.chmodSync(configPath, 0o600);
            }
            catch { /* best-effort on Windows */ }
            process.stdout.write(JSON.stringify({
                rotated: true,
                expiresAt: payload.expiresAt,
                configPath,
            }) + '\n');
            process.exit(0);
        })();
    }
    else {
        process.stderr.write(`Unknown command: '${command}'. Run 'ai-governance --help'.\n`);
        process.exit(1);
    }
}
//# sourceMappingURL=index.js.map