#!/usr/bin/env node

const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawn, spawnSync } = require('child_process');
const {
  clearConfiguredPortalUrl,
  configPath,
  readConfig,
  activeConnection,
  setCaptureQuery,
  captureQueryEnabled,
  getConfiguredPortalUrl,
  setConfiguredPortalUrl,
} = require('../lib/config');
const { ensureFreshConnection, logout } = require('../lib/auth');
const { portalLogin, portalLogout, portalEnsureFresh, defaultPortalUrl } = require('../lib/portal-auth');
const { install, uninstall } = require('../lib/install');
const { IS_FREE } = require('../lib/tier');
const { getBinary } = require('../lib/cielara');
// lib/python (MCP server launcher) is enterprise-only and is NOT shipped in the
// free tarball — require it lazily inside the `mcp` handler so the free CLI
// doesn't crash at startup on a missing module.
const { recordPostGrep, recordBuild, recordQuery, printStats } = require('../lib/stats');
const { recordUsageEvent, flushTelemetry } = require('../lib/telemetry');
const { checkForUpdate, notifyIfUpdateAvailable, CURRENT_VERSION } = require('../lib/updater');
const {
  parseEnrichmentOutput,
  extractRelatedFiles,
  isRepoEnabled,
  codeGraphCacheDir,
  codeGraphStatePath,
  canonicalizePath,
  runGit,
} = require('../lib/hooks/shared');
// The grep-enrichment hook handlers (claude/codex/cursor/agy) are enterprise-only
// and absent from the free tarball — required lazily inside their handler branch.

// EULA acceptance is handled by the portal during login (cielaracode.com).
// No local gate needed — the portal enforces acceptance before issuing
// CLI authorization codes.

// Subcommands forwarded to the packaged code-graph binary.
const CODE_GRAPH_COMMANDS = new Set([
  'build', 'query', 'post-grep', 'rg-shim',
  'enable', 'auto-enable', 'disable', 'uninstall',
]);

// Graph commands run without a Cielara connection — local-only operations.
const CODE_GRAPH_NO_AUTH = new Set(['build', 'query', 'auto-enable', 'disable', 'uninstall', 'rg-shim', 'post-grep']);

// Graph commands that require an authenticated portal session (not merely a
// cached license file). `enable` wires Cielara into a repo, so it must be
// gated on a real login. `auto-enable` stays ungated: it runs silently from
// session hooks and degrades gracefully when the user isn't logged in.
const CODE_GRAPH_REQUIRE_LOGIN = new Set(['enable']);

// Subcommands whose stdout is captured for stats extraction.
const ENRICHMENT_COMMANDS = new Set(['post-grep', 'rg-shim']);

function parseArgs(argv) {
  const out = { _: [] };
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (!arg.startsWith('--')) {
      out._.push(arg);
      continue;
    }
    const eq = arg.indexOf('=');
    if (eq >= 0) {
      out[arg.slice(2, eq)] = arg.slice(eq + 1);
      continue;
    }
    const key = arg.slice(2);
    const next = argv[i + 1];
    if (!next || next.startsWith('--')) {
      out[key] = true;
    } else {
      out[key] = next;
      i += 1;
    }
  }
  return out;
}

function requireGitRepo(cwd = process.cwd()) {
  const repoRoot = runGit(cwd, ['rev-parse', '--show-toplevel']);
  let gitDir = runGit(cwd, ['rev-parse', '--git-common-dir']);
  if (!path.isAbsolute(gitDir)) {
    gitDir = path.resolve(repoRoot, gitDir);
  }
  return { repoRoot, gitDir };
}

function registerEnabledRepo(repoRoot) {
  const statePath = codeGraphStatePath();
  let state = { schemaVersion: 0, enabledRepos: [] };
  try {
    if (fs.existsSync(statePath)) {
      state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
    }
  } catch (error) {
    process.stderr.write(`cielara: warning: ${statePath} is unreadable (${error.message}); rewriting state\n`);
  }
  if (!Array.isArray(state.enabledRepos)) {
    state.enabledRepos = [];
  }
  if (!Number.isFinite(Number(state.schemaVersion)) || Number(state.schemaVersion) < 0) {
    state.schemaVersion = 0;
  }

  const normalized = canonicalizePath(repoRoot);
  if (state.enabledRepos.includes(normalized)) return;
  state.enabledRepos.push(normalized);

  fs.mkdirSync(path.dirname(statePath), { recursive: true });
  const tmp = `${statePath}.tmp-${process.pid}`;
  fs.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
  fs.renameSync(tmp, statePath);
}

function stripFlag(argv, flagName) {
  const out = [];
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === `--${flagName}` || arg.startsWith(`--${flagName}=`)) {
      continue;
    }
    out.push(arg);
  }
  return out;
}

function stripOptionWithValue(argv, optionName) {
  const out = [];
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === `--${optionName}`) {
      i += 1;
      continue;
    }
    if (arg.startsWith(`--${optionName}=`)) {
      continue;
    }
    out.push(arg);
  }
  return out;
}

function translateBuildArgs(extraArgs) {
  const parsed = parseArgs(extraArgs);
  const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : process.cwd();
  const engine = parsed.engine;
  if (engine && engine !== true && engine !== 'rust') {
    const err = new Error(`cielara build: --engine is no longer supported here; the packaged Rust binary is used directly.`);
    err.exitCode = 2;
    throw err;
  }

  const { repoRoot, gitDir } = requireGitRepo(cwd);
  const translated = [
    'build',
    '--repo-path', repoRoot,
    '--out-dir', path.join(gitDir, 'cielara-code'),
  ];
  if (typeof parsed['repo-name'] === 'string') translated.push('--repo-name', parsed['repo-name']);
  if (parsed.rebuild || parsed.force) translated.push('--rebuild');
  if (parsed['no-cluster']) translated.push('--no-cluster');
  if (parsed.quiet) translated.push('--quiet');
  if (parsed.verbose || parsed.v) translated.push('--verbose');
  if (typeof parsed['python-parser'] === 'string') translated.push('--python-parser', parsed['python-parser']);
  return {
    args: translated,
    cwd,
    afterExit(code) {
      if (code !== 0) return;
      try {
        registerEnabledRepo(repoRoot);
      } catch (error) {
        process.stderr.write(`cielara: warning: failed to register ${repoRoot}: ${error.message}\n`);
      }
    },
  };
}

function translateEnableArgs(extraArgs) {
  const translated = [];
  for (let i = 0; i < extraArgs.length; i += 1) {
    const arg = extraArgs[i];
    if (arg === '--no-build') {
      translated.push('--skip-initial-build');
      continue;
    }
    if (arg === '--opencode-scope') {
      i += 1;
      continue;
    }
    if (arg.startsWith('--opencode-scope=')) {
      continue;
    }
    translated.push(arg);
  }
  return { args: ['enable', ...translated] };
}

function translateDisableArgs(extraArgs) {
  const parsed = parseArgs(extraArgs);
  const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : process.cwd();
  const purge = Boolean(parsed.purge);
  const translated = stripFlag(extraArgs, 'purge');
  return {
    args: ['disable', ...translated],
    cwd,
    afterExit(code) {
      if (!purge || code !== 0) return;
      try {
        const { gitDir } = requireGitRepo(cwd);
        const dir = path.join(gitDir, 'cielara-code');
        if (fs.existsSync(dir)) {
          fs.rmSync(dir, { recursive: true, force: true });
          process.stderr.write(`cielara: purged code graph index at ${dir}\n`);
        }
      } catch (error) {
        process.stderr.write(`cielara: warning: could not purge code graph index (${error.message})\n`);
      }
    },
  };
}

function translateUninstallArgs(extraArgs) {
  const parsed = parseArgs(extraArgs);
  const purgeCache = Boolean(parsed['purge-cache']);
  let translated = stripFlag(extraArgs, 'purge-cache');
  translated = stripFlag(translated, 'purge');
  return {
    args: ['uninstall', ...translated],
    afterExit(code) {
      if (!purgeCache || code !== 0) return;
      const dir = codeGraphCacheDir();
      if (fs.existsSync(dir)) {
        fs.rmSync(dir, { recursive: true, force: true });
        process.stderr.write(`cielara: purged code graph cache at ${dir}\n`);
      }
    },
  };
}

function translateQueryArgs(extraArgs) {
  const translated = [];
  let json = false;
  for (const arg of extraArgs) {
    if (arg === '--json') {
      json = true;
      continue;
    }
    if (arg.startsWith('--json=')) {
      json = arg.slice('--json='.length) !== 'false';
      continue;
    }
    translated.push(arg);
  }
  return { args: ['query', ...translated], queryJson: json };
}

function translateCwdOnlyArgs(subcommand, extraArgs) {
  const parsed = parseArgs(extraArgs);
  const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : process.cwd();
  return { args: [subcommand, ...stripOptionWithValue(extraArgs, 'cwd')], cwd };
}

function translateCodeGraphInvocation(subcommand, extraArgs) {
  switch (subcommand) {
    case 'build':
      return translateBuildArgs(extraArgs);
    case 'enable':
      return translateEnableArgs(extraArgs);
    case 'disable':
      return translateDisableArgs(extraArgs);
    case 'uninstall':
      return translateUninstallArgs(extraArgs);
    case 'query':
      return translateQueryArgs(extraArgs);
    case 'post-grep':
    case 'rg-shim':
      return translateCwdOnlyArgs(subcommand, extraArgs);
    default:
      return { args: [subcommand, ...extraArgs] };
  }
}

function usage() {
  process.stdout.write(`Cielara

Usage:
  cielara login [<portal-url>] [--name default]
                                   Save the portal URL (if given), authenticate, then
                                   run 'cielara install' on success.
  cielara install [--target <targets>] [--local] [--no-code-graph]
  cielara mcp
  cielara stats
  cielara status
  cielara license                  Show license status (tier, expiry, issuer).
  cielara refresh                  Force re-fetch license from server.
  cielara update                   Check for and install sidecar updates.
  cielara version                  Show current version.
  cielara config                            Show local connector defaults.
  cielara config get portal-url             Show the default portal URL used by login.
  cielara config set portal-url <url>       Set the default portal URL used by login.
  cielara config unset portal-url           Clear the configured default portal URL.
  cielara config get capture-query          Show whether search queries are attached to telemetry.
  cielara config set capture-query <on|off> Toggle search-query capture (on by default).
  cielara logout [--name default]

Code Graph:
  cielara build              Build or refresh the KG index for current repo.
                             Flags: --rebuild | --no-cluster | --repo-name <n> | --quiet | --cwd <dir>
  cielara query <file>       Look up related files for a path.
                             Flags: --json | --cwd <dir>
  cielara post-grep          Enrich grep output with related-file context (stdin → stdout).
  cielara enable             Index the current repo + wire the sidecar's hooks (via
                             cielara-code enable). Runs initial build.
                             Flags: --recursive  Enable every git repo under cwd (progress bar)
                                    --no-build | --claude-scope <user|project|local|none>
                                    --codex-scope <user|project|none> | --cwd <dir>
  cielara disable            Remove per-repo hooks. Leaves global state intact.
                             Flags: --purge | --cwd <dir>
  cielara uninstall          Machine-wide teardown: logs out (revokes credential),
                             removes MCP entries, hooks, commands/skills, and the
                             sidecar's global state.
                             Flags: --purge        Also delete ~/.cielara/
                                                   (config, tokens, stats, venv)
                                    --purge-cache  Delete connector Python venv cache

Install targets (comma-separated or 'all'):
  codex, claude-code, claude-desktop, cursor,
  antigravity, copilot, openclaw,
  windsurf, amazon-q, gemini-cli, cline, kiro-cli

Options:
  --config <path>          Use a custom connector config path.
`);
}

async function getFreshToken() {
  const { connection } = await ensureFreshConnection();
  return connection.accessToken;
}

// Exit after letting libuv finish closing handles from a just-completed fetch
// or child process. On Windows + Node >=24, process.exit() called while those
// handles are still closing aborts with "Assertion failed: !(handle->flags &
// UV_HANDLE_CLOSING)" (libuv src\win\async.c) — exit 127 instead of the
// intended code. Yielding one event-loop turn first lets them tear down
// cleanly. Callers must `return` after this so no further work runs before the
// scheduled exit fires.
function settleExit(code) {
  setImmediate(() => process.exit(code));
}

async function spawnCodeGraph(subcommand, extraArgs, { accessToken } = {}) {
  const env = { ...process.env };
  if (accessToken) env.CIELARA_ACCESS_TOKEN = accessToken;
  // The Rust sidecar reads $HOME for the user-home lookup (skill paths,
  // settings.json, codex config). Windows doesn't set HOME automatically
  // — only USERPROFILE. Without this propagation, the sidecar resolves
  // "~" to "" and writes ~/.claude/skills/workload-topology and friends
  // into the current working directory instead of the user home.
  // Repro: `cielara login` on a fresh Windows install logs
  // "$HOME is unset or empty; skipping Codex skill registration" and
  // never lands the workload-topology skill where Claude / Codex look.
  // Not gated on platform — USERPROFILE is virtually never set on POSIX,
  // and a stray USERPROFILE there isn't harmful.
  if (!env.HOME && env.USERPROFILE) {
    env.HOME = env.USERPROFILE;
  }

  const graphBin = getBinary('cielara-code');
  if (!graphBin) {
    process.stderr.write(
      'cielara: pre-compiled cielara-code binary not found for this platform.\n' +
      '  Reinstall: npm install -g cielara@latest\n',
    );
    process.exit(1);
  }

  let invocation;
  try {
    invocation = translateCodeGraphInvocation(subcommand, extraArgs);
  } catch (error) {
    process.stderr.write(`cielara: ${error.message}\n`);
    return error.exitCode || 1;
  }

  const isEnrichment = ENRICHMENT_COMMANDS.has(subcommand);
  const captureStdout = isEnrichment || invocation.queryJson;
  const startMs = Date.now();
  const effectiveCwd = invocation.cwd || process.cwd();

  return new Promise((resolve, reject) => {
    const stdioConfig = captureStdout ? ['inherit', 'pipe', 'inherit'] : 'inherit';
    // Windows: child_process.spawn can't exec .cmd / .bat scripts directly
    // (EINVAL). cmd.exe must interpret them, which we get via shell:true.
    // Real production binaries are .exe (no shell needed); this branch only
    // fires when CIELARA_CODE_BIN points at a script wrapper.
    const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(graphBin);
    const child = spawn(graphBin, invocation.args, {
      cwd: effectiveCwd, stdio: stdioConfig, env, shell: needsShell,
    });

    if (captureStdout) {
      const chunks = [];
      child.stdout.on('data', (chunk) => {
        chunks.push(chunk);
      });
      child.on('exit', (code, signal) => {
        const raw = Buffer.concat(chunks).toString('utf8');
        let out = raw;

        if (invocation.queryJson) {
          if ((code ?? 0) === 0) {
            const rendered = raw.includes('[Related files from dependency graph]') ? raw.trimEnd() : '';
            out = `${JSON.stringify({ rendered, relatedFiles: extractRelatedFiles(rendered) })}\n`;
          }
        } else {
          const metrics = parseEnrichmentOutput(raw, subcommand);
          if (isRepoEnabled(effectiveCwd)) {
            const elapsedMs = Date.now() - startMs;
            recordPostGrep({ ...metrics, elapsedMs });
            recordUsageEvent({
              cmd: subcommand,
              agent: 'cli',
              enriched: metrics.filesSurfaced > 0,
              filesSurfaced: metrics.filesSurfaced,
              tokens: metrics.tokens,
              relationships: metrics.relationships,
              latencyMs: elapsedMs,
              repoLanguage: metrics.repoLanguage,
              cwd: effectiveCwd,
              exitCode: code ?? 0,
            });
          }
          if (metrics.filesSurfaced > 0) {
            try {
              const json = JSON.parse(raw.trim());
              json.systemMessage = `Cielara added ${metrics.filesSurfaced} related files to context`;
              out = JSON.stringify(json);
            } catch {}
          }
        }

        if (out) process.stdout.write(out);
        if (invocation.afterExit) invocation.afterExit(code ?? 0);

        if (signal) process.kill(process.pid, signal);
        resolve(code ?? 0);
      });
    } else {
      child.on('exit', (code, signal) => {
        if (invocation.afterExit) invocation.afterExit(code ?? 0);
        if (signal) process.kill(process.pid, signal);
        resolve(code ?? 0);
      });
    }

    child.on('error', reject);
  });
}

// Shared install flow for both `cielara install` and the auto-install that
// `cielara login` runs on success. Enterprise: verify token + license, write
// MCP entries + skills, then delegate user/global-scope setup to
// `cielara-code install` (workload-topology skill, SessionStart cat-priming
// hook, codex priming block, PostToolUse Grep|Bash hook, rg-shim infra, WM
// daemon warm). Free: only the SessionStart hook (install() handles the
// tier split — the cielara-code binary isn't shipped in the free tarball).
// Run `cielara enable --recursive` separately to index repos.
async function runInstall(args) {
  if (IS_FREE) {
    const installed = install({
      target: args.target || 'all',
      packageSpec: args.package,
      local: Boolean(args.local),
    });
    for (const item of installed) {
      process.stdout.write(`Installed ${item.target}: ${item.file}\n`);
    }
    return;
  }

  // 1. Token — must be logged in.
  let accessToken;
  try {
    const fresh = await portalEnsureFresh(args.name || 'default');
    accessToken = fresh.credentials.accessToken;
  } catch {
    process.stderr.write('cielara: install requires you to be logged in.\n  Run: cielara login\n');
    process.exitCode = 1;
    return;
  }

  // 2. License.
  try {
    const { assertLicense } = require('../lib/license');
    await assertLicense();
  } catch (err) {
    process.stderr.write(`cielara: license check failed: ${err.message}\n`);
    process.exitCode = 1;
    return;
  }

  // 3. MCP entries (12 clients) + skills (Claude + Codex).
  const installed = install({
    target: args.target || 'all',
    packageSpec: args.package,
    local: Boolean(args.local),
  });
  for (const item of installed) {
    process.stdout.write(`Installed ${item.target}: ${item.file}\n`);
  }

  // 4. User/global-scope cielara-code setup: workload-topology skill,
  //    SessionStart cat-priming hook, Codex priming block, PostToolUse
  //    Grep|Bash hook, rg-shim infra, WM daemon warm. The wrapper writes
  //    MCP entries; everything agent-runtime-side lives behind
  //    `cielara-code install`. Without this call, fresh installs are
  //    missing ~/.claude/skills/workload-topology and the priming hook.
  //    --no-code-graph skips it (useful in CI / for users who don't want
  //    the sidecar wired in).
  if (!args['no-code-graph']) {
    // cielara-code only knows three install targets — map to the closest.
    const codeGraphTarget =
      args.target === 'codex' ? 'codex'
      : args.target === 'claude-code' ? 'claude'
      : 'all';
    const code = await spawnCodeGraph(
      'install',
      ['--target', codeGraphTarget],
      { accessToken },
    );
    if (code !== 0) {
      process.stderr.write(`cielara: cielara-code install exited with ${code} (non-fatal)\n`);
    }
  }
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const command = args._[0] || 'help';
  if (args.config) {
    process.env.CIELARA_CONNECTOR_CONFIG = args.config;
  }

  // Free tier ships no MCP server — short-circuit before the license gate so
  // `cielara mcp` returns a clear upgrade message instead of running a license
  // check for a feature it does not have. (Hook subcommands are license-bypassed
  // and guarded in their own branch below.)
  const ENTERPRISE_ONLY_COMMANDS = new Set(['mcp']);
  if (IS_FREE && ENTERPRISE_ONLY_COMMANDS.has(command)) {
    process.stderr.write(
      `cielara: \`${command}\` is not available in the free package.\n` +
      `  Install the full package: npm install -g cielara-enterprise\n`,
    );
    process.exitCode = 1;
    return;
  }

  // Enforce licensing gate. `login` must remain available
  // so users can establish the connection used to fetch a fresh license file.
  const LICENSE_BYPASS_COMMANDS = new Set(['help', '--help', '-h', 'install', 'uninstall', 'logout', 'login', 'config', 'stats', 'refresh', 'auth-check', 'session-start', 'update', 'version', 'license', 'telemetry-flush', 'shell-integration-path']);
  if (!LICENSE_BYPASS_COMMANDS.has(command)) {
    const { assertLicense } = require('../lib/license');
    await assertLicense();
  }

  if (command === 'shell-integration-path') {
    console.log(path.join(__dirname, '..', 'lib', 'shell-integration.sh'));
    process.exit(0);
    return;
  }

  // session-start (free tier): the SessionStart hook target. Background a
  // `cielara enable` for the current repo and return immediately so the agent
  // session is never blocked. The detached child does its own login/license
  // check and degrades silently when the user isn't logged in.
  if (command === 'session-start') {
    try {
      const child = spawn(
        process.execPath,
        [path.join(__dirname, 'cielara.js'), 'enable'],
        { cwd: process.cwd(), detached: true, stdio: 'ignore' },
      );
      child.unref();
    } catch {
      // Best-effort: never let a failed background spawn break the session.
    }
    settleExit(0);
    return;
  }

  // NOTE: grep PostToolUse hooks (claude/codex/cursor/agy) are no longer the
  // connector's responsibility — the cielara-code sidecar installs and serves
  // its own session + PostToolUse hooks via `cielara-code enable`. The old
  // `cielara *-hook` subcommands were removed accordingly.

  // Route code-graph subcommands.
  if (CODE_GRAPH_COMMANDS.has(command)) {
    let accessToken;
    if (CODE_GRAPH_REQUIRE_LOGIN.has(command)) {
      // Require an authenticated portal session. portalEnsureFresh throws
      // when no credentials are stored (not logged in) or the refresh fails.
      // The token is forwarded to the sidecar (CIELARA_ACCESS_TOKEN), which
      // re-checks login independently so a direct binary call can't bypass it.
      try {
        const fresh = await portalEnsureFresh(args.name || 'default');
        accessToken = fresh.credentials.accessToken;
      } catch {
        process.stderr.write(
          `cielara: \`${command}\` requires you to be logged in.\n` +
          `  Run: cielara login\n`,
        );
        settleExit(1);
        return;
      }
    } else if (!CODE_GRAPH_NO_AUTH.has(command)) {
      try {
        accessToken = await getFreshToken();
      } catch {
        process.stderr.write(
          `cielara: a valid Cielara connection is required.\n` +
          `  Run: cielara config set portal-url https://<your-cielara-url>\n` +
          `       cielara login\n`,
        );
        settleExit(1);
        return;
      }
    }

    // `cielara enable --recursive`: walk the tree from cwd, enable every git
    // repo (sidecar enable per repo), with a progress bar. Implemented in the
    // connector because the sidecar has no --recursive.
    if (command === 'enable' && args.recursive) {
      const { runRecursiveEnable } = require('../lib/recursive-enable');
      const root = typeof args.cwd === 'string' ? args.cwd : process.cwd();
      // Pass through any extra sidecar flags (e.g. --claude-scope), minus the
      // connector-only --recursive and the already-applied --cwd.
      const extraArgs = stripOptionWithValue(stripFlag(process.argv.slice(3), 'recursive'), 'cwd');
      const { failed } = runRecursiveEnable(root, { accessToken, extraArgs });
      settleExit(failed > 0 ? 1 : 0);
      return;
    }

    // Forward all args after the subcommand verbatim.
    const forwardArgs = process.argv.slice(3);
    // Hooks installed by the sidecar's `enable` call back into the `cielara-code`
    // binary (shipped on PATH), not the connector CLI.
    if (command === 'enable' && !forwardArgs.includes('--cli-bin')) {
      forwardArgs.push('--cli-bin', 'cielara-code');
    }

    // For `uninstall`, also remove the connector's own state (MCP server
    // entries in every client config, copied commands/skills, optional
    // ~/.cielara/ purge). Run BEFORE forwarding to code-graph so the
    // delegated cleanup doesn't matter if it fails — connector-side state
    // is already gone.
    if (command === 'uninstall') {
      // Log out on every uninstall: revoke the keychain credential and drop the
      // stored connection so uninstall fully disconnects (not just on --purge).
      // --purge additionally wipes the whole ~/.cielara dir below.
      try {
        await portalLogout(args.name);
        logout(args.name);
      } catch {
        // Not logged in or keychain unavailable — proceed with uninstall anyway.
      }
      try {
        const removed = uninstall({ purge: Boolean(args.purge) });
        for (const item of removed) {
          process.stderr.write(`  ${item.target}: removed from ${item.file}\n`);
        }
      } catch (error) {
        process.stderr.write(`cielara: connector uninstall failed: ${error.message}\n`);
      }
    }

    const code = await spawnCodeGraph(command, forwardArgs, { accessToken });
    if (command === 'build') {
      recordBuild();
      recordUsageEvent({ cmd: 'build', agent: 'cli', enriched: false, cwd: process.cwd(), exitCode: code });
    }
    if (command === 'query') {
      recordQuery();
      recordUsageEvent({ cmd: 'query', agent: 'cli', enriched: false, cwd: process.cwd(), exitCode: code });
    }
    settleExit(code);
    return;
  }

  switch (command) {
    case 'login': {
      const removedFlags = ['url', 'portal-url', 'auth-url', 'backend-url', 'memory-url', 'world-model-url'].filter((flag) => args[flag]);
      if (removedFlags.length > 0) {
        process.stderr.write(
          `cielara login no longer accepts ${removedFlags.map((flag) => `--${flag}`).join(', ')}.\n` +
          `Set the portal URL once instead:\n` +
          `  cielara config set portal-url <url>\n` +
          `  cielara login\n`,
        );
        process.exitCode = 2;
        return;
      }
      if (args.token) {
        process.stderr.write(
          'cielara login no longer accepts --token. Use browser login with the configured portal URL:\n' +
          '  cielara config set portal-url <url>\n' +
          '  cielara login\n',
        );
        process.exitCode = 2;
        return;
      }
      // Positional portal URL: `cielara login https://staging-dataplane.cielara.ai`
      // saves it (== config set portal-url) before authenticating.
      const urlArg = args._[1];
      if (urlArg) {
        try {
          const portalUrl = setConfiguredPortalUrl(urlArg);
          process.stdout.write(`Portal URL set to ${portalUrl}.\n`);
        } catch (error) {
          process.stderr.write(`cielara login: invalid portal URL: ${error.message}\n`);
          process.exitCode = 2;
          return;
        }
      }
      const result = await portalLogin({
        name: args.name || 'default',
      });
      process.stdout.write(`Authenticated as ${result.connection.user?.email || 'user'}\n`);
      process.stdout.write(`Credentials stored securely.\n`);
      try {
        const { assertLicense } = require('../lib/license');
        await assertLicense();
      } catch (err) {
        process.stderr.write(`\nLicense check: ${err.message}\n`);
      }
      // On successful login, run the install flow automatically.
      process.stdout.write(`\nSetting up Cielara…\n`);
      await runInstall(args);
      return;
    }
    case 'auth-check': {
      try {
        await portalEnsureFresh();
        settleExit(0);
      } catch {
        process.stderr.write('Cielara Code requires authentication.\nRun: cielara login\n');
        settleExit(0);
      }
      return;
    }
    case 'install': {
      await runInstall(args);
      break;
    }
    case 'mcp': {
      if (IS_FREE) {
        process.stderr.write(
          'cielara: the MCP server is not available in the free package.\n' +
          '  Install the full package: npm install -g cielara-enterprise\n',
        );
        process.exitCode = 1;
        break;
      }
      const { runMCP } = require('../lib/python');
      await runMCP();
      break;
    }
    case 'status': {
      const config = readConfig();
      const { name, connection } = activeConnection(config);
      // Portal connections store `portalUrl` and keep tokens in the keychain.
      // Legacy OAuth connections store `baseUrl` + tokens inline in the config.
      if (connection.portalUrl) {
        const fresh = await portalEnsureFresh(name);
        const cred = fresh.credentials;
        process.stdout.write(`Active connection: ${name}\n`);
        process.stdout.write(`Portal URL: ${cred.portalUrl || connection.portalUrl}\n`);
        process.stdout.write(`User: ${cred.user?.email || connection.userEmail || '(unknown)'}\n`);
        process.stdout.write(`Installation: ${cred.installationID || connection.installationID || '(none)'}\n`);
        process.stdout.write(`Access token expires: ${cred.expiresAt || '(unknown)'}\n`);
        process.stdout.write(`Refresh token expires: ${cred.refreshExpiresAt || '(unknown)'}\n`);
      } else {
        const fresh = await ensureFreshConnection();
        process.stdout.write(`Active connection: ${name}\n`);
        process.stdout.write(`Cielara URL: ${fresh.connection.baseUrl || connection.baseUrl}\n`);
        process.stdout.write(`Memory URL: ${fresh.connection.memoryApiUrl || connection.memoryApiUrl || '(default)'}\n`);
        process.stdout.write(`Access token expires: ${fresh.connection.expiresAt || '(unknown)'}\n`);
      }
      process.stdout.write(`Config: ${configPath()}\n`);
      break;
    }
    case 'license': {
      const {
        verifyPortalJWT,
        verifyKeygenLicenseFile,
        decodeKeygenLicenseFile,
        isJWT,
        isKeygenLicenseFile,
        LICENSE_PATH,
      } = require('../lib/license');
      if (!fs.existsSync(LICENSE_PATH)) {
        process.stderr.write(`No license file found at ${LICENSE_PATH}\n`);
        process.stderr.write('Run `cielara login` to authenticate.\n');
        process.exitCode = 1;
        break;
      }
      const raw = fs.readFileSync(LICENSE_PATH, 'utf8').trim();

      const fmt = (secs) => {
        if (secs === null || secs === undefined) return 'unknown';
        if (secs <= 0) return 'EXPIRED';
        const d = Math.floor(secs / 86400);
        const h = Math.floor((secs % 86400) / 3600);
        const m = Math.floor((secs % 3600) / 60);
        if (d > 0) return `${d}d ${h}h`;
        if (h > 0) return `${h}h ${m}m`;
        return `${m}m`;
      };
      const now = Math.floor(Date.now() / 1000);

      // Enterprise (*.cielara.ai) instances issue a Keygen offline license file
      // (-----BEGIN LICENSE FILE-----), not a JWT. Display its fields too.
      if (isKeygenLicenseFile(raw)) {
        let decoded;
        try {
          decoded = decodeKeygenLicenseFile(raw);
        } catch (err) {
          process.stderr.write(`Cannot decode license file: ${err.message}\n`);
          process.stderr.write('Run `cielara login` to refresh.\n');
          process.exitCode = 1;
          break;
        }
        const { lic, attrs, meta, entitlements } = decoded;
        // Inner license expiry takes precedence; fall back to the file TTL.
        const expiryIso = attrs.expiry || meta.expiry || null;
        const expSecs = expiryIso ? Math.floor(new Date(expiryIso).getTime() / 1000) : null;
        const remaining = expSecs ? Math.max(0, expSecs - now) : null;
        const expired = expSecs !== null && expSecs < now;
        const status = (attrs.status || (expired ? 'EXPIRED' : 'ACTIVE')).toString();

        process.stdout.write(`License:    ${LICENSE_PATH}\n`);
        process.stdout.write(`Format:     keygen license file\n`);
        process.stdout.write(`Status:     ${expired ? 'EXPIRED' : status}\n`);
        process.stdout.write(`Tier:       enterprise\n`);
        process.stdout.write(`User:       ${attrs.metadata?.email || attrs.name || lic.id || 'unknown'}\n`);
        process.stdout.write(`License ID: ${lic.id || 'unknown'}\n`);
        process.stdout.write(`Product:    ${entitlements.length ? entitlements.join(', ') : 'unknown'}\n`);
        process.stdout.write(`Expires:    ${expiryIso ? new Date(expiryIso).toISOString() : 'never'} (${fmt(remaining)})\n`);
        if (meta.expiry) {
          const ttlRemaining = Math.max(0, Math.floor(new Date(meta.expiry).getTime() / 1000) - now);
          process.stdout.write(`Refreshes:  ${new Date(meta.expiry).toISOString()} (${fmt(ttlRemaining)})\n`);
        }
        // Verify the signature if the keygen account key is available.
        try {
          verifyKeygenLicenseFile(raw);
          process.stdout.write(`Signature:  valid\n`);
        } catch (err) {
          process.stdout.write(`Signature:  ${err.message}\n`);
        }
        break;
      }

      if (!isJWT(raw)) {
        process.stderr.write('License file is in an unrecognized format (expected a JWT or keygen license file). Run `cielara login` to refresh.\n');
        process.exitCode = 1;
        break;
      }
      const parts = raw.split('.');
      let payload;
      try {
        payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
      } catch {
        process.stderr.write('Cannot decode license payload.\n');
        process.exitCode = 1;
        break;
      }
      const expired = payload.exp && payload.exp < now;
      const remaining = payload.exp ? Math.max(0, payload.exp - now) : null;
      process.stdout.write(`License:    ${LICENSE_PATH}\n`);
      process.stdout.write(`Format:     portal JWT\n`);
      process.stdout.write(`Status:     ${expired ? 'EXPIRED' : (payload.status || 'active')}\n`);
      process.stdout.write(`Tier:       ${payload.tier || 'unknown'}\n`);
      process.stdout.write(`User:       ${payload.user?.email || payload.sub || 'unknown'}\n`);
      process.stdout.write(`Issuer:     ${payload.iss || 'unknown'}\n`);
      process.stdout.write(`Product:    ${payload.product || 'unknown'}\n`);
      process.stdout.write(`Expires:    ${payload.exp ? new Date(payload.exp * 1000).toISOString() : 'unknown'} (${fmt(remaining)})\n`);
      process.stdout.write(`Issued:     ${payload.iat ? new Date(payload.iat * 1000).toISOString() : 'unknown'}\n`);
      if (payload.trial_ends_at) {
        const trialRemaining = payload.trial_ends_at - now;
        process.stdout.write(`Trial ends: ${new Date(payload.trial_ends_at * 1000).toISOString()} (${fmt(trialRemaining)})\n`);
      }
      if (payload.enrichments_quota) {
        process.stdout.write(`Quota:      ${payload.enrichments_remaining_at_issue}/${payload.enrichments_quota} enrichments remaining\n`);
      }
      // Verify signature if keys available
      try {
        verifyPortalJWT(raw);
        process.stdout.write(`Signature:  valid\n`);
      } catch (err) {
        process.stdout.write(`Signature:  ${err.message}\n`);
      }
      break;
    }
    case 'refresh': {
      const { assertLicense, LICENSE_PATH } = require('../lib/license');
      try { fs.unlinkSync(LICENSE_PATH); } catch {}
      try {
        const result = await assertLicense();
        process.stdout.write(`License refreshed: tier=${result.tier || 'unknown'}, expires=${result.expiry || 'unknown'}\n`);
      } catch (err) {
        process.stderr.write(`License refresh failed: ${err.message}\n`);
        process.exitCode = 1;
      }
      break;
    }
    case 'logout':
      await portalLogout(args.name);
      logout(args.name);
      process.stdout.write(`Removed connection ${args.name || 'default'}\n`);
      break;
    case 'update': {
      try {
        const update = await checkForUpdate();
        if (!update) {
          process.stdout.write(`Already on latest version (${CURRENT_VERSION}).\n`);
          break;
        }
        process.stdout.write(`Update available: ${CURRENT_VERSION} → ${update.version}\n`);
        process.stdout.write(`Run: npm update -g cielara\n`);
      } catch (err) {
        process.stderr.write(`Update check failed: ${err.message}\n`);
        process.exitCode = 1;
      }
      break;
    }
    case 'telemetry-flush': {
      const result = await flushTelemetry({ once: Boolean(args.once) });
      if (!process.env.CIELARA_TELEMETRY_CHILD) {
        process.stdout.write(`Telemetry flushed: ${result.totalFlushed || result.flushed || 0}\n`);
      }
      break;
    }
    case 'version':
    case '--version':
    case '-v':
      process.stdout.write(`cielara ${CURRENT_VERSION}\n`);
      break;
    case 'stats':
      await printStats();
      break;
    case 'config': {
      const sub = args._[1];
      const key = args._[2];
      const val = args._[3];
      const isOff = (v) => ['off', 'false', '0', 'no', 'disable', 'disabled'].includes(String(v).toLowerCase());
      const isOn = (v) => ['on', 'true', '1', 'yes', 'enable', 'enabled'].includes(String(v).toLowerCase());
      if (!sub || (sub === 'get' && !key)) {
        process.stdout.write(`portal-url: ${getConfiguredPortalUrl(readConfig()) || `${defaultPortalUrl()} (default)`}\n`);
        process.stdout.write(`capture-query: ${captureQueryEnabled() ? 'on' : 'off'}\n`);
        break;
      }
      if (sub === 'get' && key === 'portal-url') {
        process.stdout.write(`portal-url: ${getConfiguredPortalUrl(readConfig()) || `${defaultPortalUrl()} (default)`}\n`);
        break;
      }
      if (sub === 'set' && key === 'portal-url') {
        if (!val) {
          process.stderr.write('Usage: cielara config set portal-url <url>\n');
          process.exitCode = 1;
          break;
        }
        try {
          const portalUrl = setConfiguredPortalUrl(val);
          process.stdout.write(`portal-url set to ${portalUrl}.\n`);
          process.stdout.write('Future `cielara login` runs will use this portal.\n');
        } catch (error) {
          process.stderr.write(`cielara config: invalid portal-url: ${error.message}\n`);
          process.exitCode = 1;
        }
        break;
      }
      if (sub === 'unset' && key === 'portal-url') {
        clearConfiguredPortalUrl();
        process.stdout.write('portal-url cleared. Future `cielara login` runs will use the default portal.\n');
        break;
      }
      if (sub === 'get' && key === 'capture-query') {
        process.stdout.write(`capture-query: ${captureQueryEnabled() ? 'on' : 'off'}\n`);
        break;
      }
      if (sub === 'set' && key === 'capture-query') {
        if (!isOn(val) && !isOff(val)) {
          process.stderr.write('Usage: cielara config set capture-query <on|off>\n');
          process.exitCode = 1;
          break;
        }
        setCaptureQuery(isOn(val));
        process.stdout.write(`capture-query set to ${isOn(val) ? 'on' : 'off'}.\n`);
        if (isOff(val)) {
          process.stdout.write('Search queries will no longer be attached to usage telemetry.\n');
        }
        break;
      }
      process.stderr.write('Usage:\n  cielara config\n  cielara config get portal-url\n  cielara config set portal-url <url>\n  cielara config unset portal-url\n  cielara config get capture-query\n  cielara config set capture-query <on|off>\n');
      process.exitCode = 1;
      break;
    }
    case 'help':
    case '--help':
    case '-h':
      usage();
      break;
    default:
      usage();
      process.exitCode = 2;
  }
}

main().catch((error) => {
  process.stderr.write(`cielara: ${error.message}\n`);
  // Drain naturally instead of forcing process.exit(1): on Windows + Node >=24,
  // exiting while libuv handles from the failed network/child work are still
  // closing aborts with a UV_HANDLE_CLOSING assertion (exit 127). Setting the
  // code and returning lets the loop empty and exit 1 on its own. The unref'd
  // grace timer only fires if some stray handle keeps the loop alive.
  process.exitCode = 1;
  setTimeout(() => process.exit(1), 2000).unref?.();
});
