import { execFileSync, spawnSync } from "node:child_process"
import {
  appendFileSync,
  chmodSync,
  copyFileSync,
  cpSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readdirSync,
  readFileSync,
  renameSync,
  rmSync,
  writeFileSync,
} from "node:fs"
import { cp, writeFile } from "node:fs/promises"
import { createHash } from "node:crypto"
import os from "node:os"
import path from "node:path"
import readline from "node:readline"
import { fileURLToPath } from "node:url"

// The mesh binaries the privileged service holds open while running. On Windows
// a file copy can't overwrite them in place, so the lock-safe `apply` path swaps
// them after stopping the service.
const WINDOWS_MESH_BINARIES = ["easytier-core.exe", "easytier-cli.exe", "wintun.dll", "Packet.dll", "WinDivert64.sys", "vantaloom-mesh.exe"]

// The privileged-mesh files that must NOT be needlessly overwritten on update:
// the service binary (vantaloom-mesh) and the EasyTier engine (easytier-core,
// the Linux setcap target), plus the Windows support DLLs the running service
// holds open. Leaving an unchanged copy in place keeps the service running and
// preserves its Linux file capability — so the update needs no elevation.
function meshBinarySet(platform) {
  return platform === "win32"
    ? WINDOWS_MESH_BINARIES
    : ["vantaloom-mesh", "easytier-core"]
}

// sleepSync blocks for ms milliseconds without async (install runs top-to-bottom
// and must not race the file copy against a process that is still releasing its
// handles). Uses Atomics.wait on a throwaway buffer — no busy spin.
function sleepSync(ms) {
  try {
    Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms))
  } catch {
    const end = Date.now() + ms
    while (Date.now() < end) {} // fallback busy-wait if SAB is unavailable
  }
}

const LOCK_ERROR_CODES = new Set(["EPERM", "EBUSY", "EACCES", "ETXTBSY"])

// copyFileResilient overwrites dst with src, surviving the Windows case where dst
// is a running/locked executable. Windows refuses to OVERWRITE or DELETE an in-use
// .exe (EPERM/EBUSY) but DOES allow RENAMING it — the file handle tracks the file
// object, not its path. So on a lock error we move the locked file aside and copy
// the new one into the freed path; the runtime picks up the new binary on its next
// start, and the moved-aside `.old-*` file is reaped on a later install. This is
// the fix for the EPERM that blocked update/restart when a prior process (api,
// agent, or an orphaned easytier/mesh) still held a binary open.
function copyFileResilient(src, dst) {
  for (let attempt = 0; ; attempt += 1) {
    try {
      copyFileSync(src, dst)
      return
    } catch (err) {
      if (!LOCK_ERROR_CODES.has(err.code) || !existsSync(dst)) {
        if (attempt >= 4) throw err
        sleepSync(250)
        continue
      }
      try {
        const aside = `${dst}.old-${process.pid}-${attempt}`
        renameSync(dst, aside)
        copyFileSync(src, dst)
        try { rmSync(aside, { force: true }) } catch {} // best-effort; may still be locked
        return
      } catch (moveErr) {
        if (attempt >= 4) throw moveErr
        sleepSync(300)
      }
    }
  }
}

// copyDirResilient recursively copies src→dst using copyFileResilient for every
// file, so a single locked binary can't abort the whole bin/ refresh. filter(srcPath)
// gates which entries are copied (mirrors fs.cp's filter).
function copyDirResilient(src, dst, filter) {
  mkdirSync(dst, { recursive: true })
  for (const entry of readdirSync(src, { withFileTypes: true })) {
    const s = path.join(src, entry.name)
    if (filter && !filter(s)) continue
    const d = path.join(dst, entry.name)
    if (entry.isDirectory()) {
      copyDirResilient(s, d, filter)
    } else {
      copyFileResilient(s, d)
    }
  }
}

// cleanupStaleReplacements deletes the `.old-*` files left behind by a prior
// lock-safe replace once their handles have been released (best-effort).
function cleanupStaleReplacements(dir) {
  if (!existsSync(dir)) return
  for (const entry of readdirSync(dir)) {
    if (/\.old-\d+-\d+$/.test(entry)) {
      try { rmSync(path.join(dir, entry), { force: true }) } catch {}
    }
  }
}

const cliRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..")
const repoCandidate = path.resolve(cliRoot, "..", "..")
const installedConfigPath = path.join(cliRoot, "config.json")
const defaultReleaseTag = "runtime-latest"
const defaultRepo = "Timefiles404/Vantaloom-next"
const defaultNpmRegistry = "https://registry.npmjs.org"
const fallbackNpmRegistries = [
  "https://registry.npmmirror.com",
]

// The Claude SDK bridge the Go backend spawns as
// `node <installDir>/adapters/claude-sdk-bridge/index.mjs`. The bridge source ships
// inside this CLI package (packages/cli/adapters/claude-sdk-bridge); bundleAdapter
// installs its sole dependency (@anthropic-ai/claude-agent-sdk, version pinned in the
// bridge's own package.json) into adapters/claude-sdk-bridge/node_modules at
// install-adapter time. There is NO @zed-industries/claude-code-acp anymore — the
// engine speaks the SDK's native stream-json directly.

// Inherit strict-ssl=false from npm/npx config, or respect NODE_TLS_REJECT_UNAUTHORIZED.
// When npm runs us via npx with strict-ssl disabled, it sets npm_config_strict_ssl="false".
// Also check user .npmrc for strict-ssl=false (common in China behind proxies).
function shouldDisableTLS() {
  if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") return true
  if (process.env.npm_config_strict_ssl === "false") return true
  if (process.env.npm_config_strict_ssl === "") return true
  try {
    const npmrcPath = path.join(os.homedir(), ".npmrc")
    if (existsSync(npmrcPath)) {
      const content = readFileSync(npmrcPath, "utf8")
      if (/^\s*strict-ssl\s*=\s*false/m.test(content)) return true
    }
  } catch {}
  return false
}
if (shouldDisableTLS()) {
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
}

const cliVersion = readJSONIfExists(path.join(cliRoot, "package.json")).version ?? "unknown"

export async function main(argv) {
  const command = argv[0] ?? "help"
  const options = parseOptions(argv.slice(1))

  // --no-strict-ssl flag disables TLS certificate verification for fetch calls
  if (options.noStrictSsl) {
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
  }

  if (command === "install" || command === "update") {
    console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
  }

  switch (command) {
    case "install":
      if (options.package) {
        await installFromPackage({ ...options, update: false })
      } else if (shouldUseSourceInstall(options)) {
        await installFromSource(options)
      } else if (shouldUseReleaseSync(options)) {
        await syncFromRelease(options, "install")
      } else {
        await syncFromNpmRegistry(options, "install")
      }
      return
    case "update":
      if (options.package) {
        await installFromPackage({ ...options, update: true })
      } else if (shouldUseSourceInstall(options)) {
        await installFromSource({ ...options, update: true })
      } else if (shouldUseReleaseSync(options)) {
        await syncFromRelease(options, "update")
      } else {
        await syncFromNpmRegistry(options, "update")
      }
      return
    case "package":
      await packageRuntime(options)
      return
    case "install-adapter":
      await installAdapter(options)
      return
    case "platform":
      console.log(platformId())
      return
    case "start":
    case "stop":
    case "restart":
    case "status":
    case "ports":
      runCtl(command, options)
      return
    case "path":
      printPaths(options)
      return
    case "uninstall":
      await uninstallRuntime(options)
      return
    case "login":
      await runLogin(options)
      return
    case "logout":
      await runLogout(options)
      return
    case "help":
    case "-h":
    case "--help":
      printHelp()
      return
    default:
      throw new Error(`unknown command "${command}"`)
  }
}

async function installFromSource(options) {
  const sourceRoot = findSourceRoot(options.source)
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  const buildRoot = path.join(sourceRoot, "artifacts", "local-install", platformId())

  const { version } = await buildRuntimePackage(sourceRoot, buildRoot, {
    buildWeb: options.buildWeb,
    skipAdapter: options.skipAdapter,
  })

  await applyPackage(buildRoot, prefix, {
    noStart: options.noStart,
    sourceRoot,
    update: options.update,
    withMesh: options.withMesh,
    skipMesh: options.skipMesh,
  })

  console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
  console.log(`version: ${version}`)
  ensureInPath(prefix)
  console.log(`run: ${displayCommand(prefix)} status`)
}

async function installFromPackage(options) {
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  const packageRoot = safeDirectory(options.package)
  const version = await applyPackage(packageRoot, prefix, {
    noStart: options.noStart,
    update: options.update,
    withMesh: options.withMesh,
    skipMesh: options.skipMesh,
  })

  console.log(`${options.update ? "updated" : "installed"} Vantaloom: ${prefix}`)
  console.log(`version: ${version}`)
  ensureInPath(prefix)
  console.log(`run: ${displayCommand(prefix)} status`)
}

async function packageRuntime(options) {
  const sourceRoot = findSourceRoot(options.source)
  const targetPlatform = options.target ?? platformId()
  const packageRoot = safeDirectory(
    options.output ?? path.join(sourceRoot, "artifacts", "packages", `vantaloom-${targetPlatform}`)
  )
  const { platform: builtPlatform, version } = await buildRuntimePackage(sourceRoot, packageRoot, {
    buildWeb: options.buildWeb,
    target: targetPlatform,
    skipAdapter: options.skipAdapter,
  })

  if (options.npmPackage) {
    writeRuntimePackageMetadata(packageRoot, sourceRoot, builtPlatform)
  }

  if (options.archive) {
    const archivePath = `${packageRoot}.tar.gz`
    removeKnownPath(archivePath, path.dirname(archivePath))
    run("tar", [
      "-czf",
      archivePath,
      "-C",
      path.dirname(packageRoot),
      path.basename(packageRoot),
    ])
    console.log(`archive: ${archivePath}`)
  }

  console.log(`packaged Vantaloom: ${packageRoot}`)
  console.log(`platform: ${builtPlatform}`)
  console.log(`version: ${version}`)
}

// installAdapter downloads + bundles the Claude Code ACP adapter into a LIVE
// install's adapters/ dir (node_modules + patched local-command forwarding +
// reclaude shim), so the user can add Claude Code / reclaude support on demand
// instead of shipping ~50-100MB inside every runtime tarball. The Go backend
// invokes this (silent, no console window) from the settings "install adapter"
// button; it can also be run by hand: `vantaloom install-adapter`.
async function installAdapter(options) {
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  console.log(`vantaloom-cli v${cliVersion} (${platformId()})`)
  console.log(`installing Claude Code ACP adapter into ${path.join(prefix, "adapters")} ...`)
  await bundleAdapter(prefix, { platform: platformId() })
  console.log(`adapter ready`)
}

async function buildRuntimePackage(sourceRoot, packageRoot, options) {
  const version = gitVersion(sourceRoot)
  const platform = options.target ?? platformId()
  const buildBin = path.join(packageRoot, "bin")
  const buildWeb = path.join(packageRoot, "web")

  removeKnownPath(packageRoot, path.dirname(packageRoot))
  mkdirSync(buildBin, { recursive: true })

  buildGo(sourceRoot, buildBin, "vantaloom-api", platform)
  buildGo(sourceRoot, buildBin, "vantaloom-agent", platform)
  buildGo(sourceRoot, buildBin, "vantaloomctl", platform)
  // Official "Vantaloom 浏览器" extension sidecar. Pure-Go (chromedp/CDP, no
  // CGO), cross-compiles for every target. Ships dormant: it's only started by
  // the api when the user installs+enables the browser extension (the heavy
  // Chromium binary is downloaded on demand, NOT bundled here).
  buildGo(sourceRoot, buildBin, "vantaloom-browser", platform)
  // Privileged EasyTier sidecar (runs the mesh node as a service on Windows/macOS).
  // Pure-Go (no CGO), so it cross-compiles cleanly for every target.
  buildGo(sourceRoot, buildBin, "vantaloom-mesh", platform)
  // The Windows system-tray app (vantaloom-tray) was removed — it crash-looped
  // on some Windows 11 builds and got respawned until OOM. No longer built.

  // Bundle the EasyTier mesh binaries (P2P virtual network) for the target platform.
  await copyEasyTier(sourceRoot, buildBin, platform)

  if (options.buildWeb) {
    runPnpm(["--filter", "vantaloom-app", "build"], { cwd: sourceRoot })
  }

  await copyStaticWeb(sourceRoot, buildWeb)
  await copyCliDirectory(path.join(packageRoot, "cli"), sourceRoot, runtimeConfigFromSource(sourceRoot))
  // The Claude Code ACP adapter is NO LONGER bundled into the runtime package
  // (it bloated every platform tarball by ~50-100MB). Instead the user installs
  // it on demand from 设置 → CC 引擎 → "下载/安装适配器", which runs the
  // `install-adapter` CLI command into <installDir>/adapters. Because applyPackage
  // leaves a missing adapters/ dir untouched on update, the user-installed adapter
  // survives runtime updates.
  writeBuildManifest(packageRoot, version, platform)

  return { platform, version }
}

// bundleAdapter installs the Claude Agent SDK into
// <packageRoot>/adapters/claude-sdk-bridge/node_modules and copies the bridge source
// (the thin node sidecar that runs the SDK's query() and pipes its native stream-json
// to the Go runtime). It also writes the reclaude shim the Go engine points the SDK's
// pathToClaudeCodeExecutable at. The SDK version is pinned in the bridge's own
// package.json (single source of truth); native optional deps (e.g. sharp) are
// selected per TARGET platform.
async function bundleAdapter(packageRoot, options = {}) {
  const adaptersRoot = path.join(packageRoot, "adapters")
  const bridgeDest = path.join(adaptersRoot, "claude-sdk-bridge")
  const bridgeSrc = path.join(cliRoot, "adapters", "claude-sdk-bridge")
  const shimPath = path.join(adaptersRoot, "reclaude-shim.mjs")
  const shimSource = `// Vantaloom reclaude shim: the Claude Agent SDK runs \`node <this>\` as the claude
// executable, so we re-exec the real reclaude binary (path in VANTALOOM_RECLAUDE_EXE)
// with the same args + inherited stdio. reclaude's banner goes to stderr, keeping the
// SDK's stdout protocol clean.
import { spawn } from "node:child_process";
const exe = process.env.VANTALOOM_RECLAUDE_EXE;
if (!exe) {
  process.stderr.write("[reclaude-shim] VANTALOOM_RECLAUDE_EXE not set\\n");
  process.exit(127);
}
const child = spawn(exe, process.argv.slice(2), { stdio: "inherit", windowsHide: true });
child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
child.on("error", (e) => {
  process.stderr.write(\`[reclaude-shim] failed to launch reclaude: \${e.message}\\n\`);
  process.exit(127);
});
`

  if (!existsSync(path.join(bridgeSrc, "index.mjs"))) {
    const msg = `claude-sdk-bridge source missing at ${bridgeSrc}`
    if (options.skipAdapter) {
      console.warn(`  warning: skipping SDK bridge bundle (${msg})`)
      return
    }
    throw new Error(msg)
  }

  // Resolve the SDK's platform-gated native optional deps (e.g. sharp's
  // @img/sharp-<os>-<cpu>) for the TARGET platform, not the build host — otherwise a
  // Windows-built bundle would ship the wrong native binary to the mac/linux packages.
  const { os: targetOs, cpu: targetCpu } = parsePlatformId(options.platform ?? platformId())
  console.log(`  bundling Claude Agent SDK bridge (os=${targetOs} cpu=${targetCpu}) ...`)
  const tmp = mkdtempSync(path.join(os.tmpdir(), "vantaloom-sdk-bridge-"))
  try {
    // Install the bridge's OWN dependencies (the SDK, version pinned in its
    // package.json) into a temp prefix from the PUBLIC registry explicitly: the
    // machine's global .npmrc points at npmmirror, which may not mirror this package.
    cpSync(path.join(bridgeSrc, "package.json"), path.join(tmp, "package.json"))
    const npmArgs = [
      "install",
      "--prefix",
      tmp,
      "--registry",
      "https://registry.npmjs.org/",
      "--no-audit",
      "--no-fund",
      "--omit=dev",
      "--os",
      targetOs,
      "--cpu",
      targetCpu,
    ]
    if (targetOs === "linux") {
      npmArgs.push("--libc", "glibc")
    }
    runNpm(npmArgs)

    const tmpModules = path.join(tmp, "node_modules")
    if (!existsSync(tmpModules)) {
      throw new Error(`SDK install produced no node_modules at ${tmpModules}`)
    }

    removeKnownPath(adaptersRoot, path.dirname(adaptersRoot))
    mkdirSync(bridgeDest, { recursive: true })
    // Bridge source: index.mjs (+ package.json, for reference/version).
    cpSync(path.join(bridgeSrc, "index.mjs"), path.join(bridgeDest, "index.mjs"))
    cpSync(path.join(bridgeSrc, "package.json"), path.join(bridgeDest, "package.json"))
    // The SDK + its dependency tree.
    cpSync(tmpModules, path.join(bridgeDest, "node_modules"), { recursive: true })

    writeFileSync(shimPath, shimSource)
    console.log(`  bundled SDK bridge -> adapters/claude-sdk-bridge + reclaude-shim.mjs`)
  } catch (error) {
    if (options.skipAdapter) {
      console.warn(`  warning: skipping SDK bridge bundle (${error.message})`)
      return
    }
    throw new Error(`failed to bundle SDK bridge: ${error.message}`)
  } finally {
    rmSync(tmp, { recursive: true, force: true })
  }
}

// copyEasyTier bundles the vendored EasyTier binaries (easytier-core/easytier-cli,
// plus wintun.dll on Windows) for the target platform into the package bin/ dir.
async function copyEasyTier(sourceRoot, buildBin, platform) {
  const vendorMap = {
    "win32-x64": "windows-x86_64",
    "darwin-arm64": "macos-aarch64",
    "linux-x64": "linux-x86_64",
  }
  const vendorName = vendorMap[platform]
  if (!vendorName) {
    console.warn(`  warning: no EasyTier mapping for ${platform}; mesh disabled for this platform`)
    return
  }
  const srcDir = path.join(sourceRoot, "vendor", "easytier", vendorName, `easytier-${vendorName}`)
  if (!existsSync(srcDir)) {
    console.warn(`  warning: EasyTier binaries not found at ${srcDir}; run vendor download first`)
    return
  }
  const isWin = platform.startsWith("win32")
  // Packet.dll + WinDivert64.sys are REQUIRED: easytier-core.exe statically
  // imports Packet.dll, so without it the process dies at launch with
  // STATUS_DLL_NOT_FOUND (0xC0000135) before writing any log. wintun.dll is the
  // TUN backend. easytier-cli.exe does not need Packet.dll (it still launches).
  const files = isWin
    ? ["easytier-core.exe", "easytier-cli.exe", "wintun.dll", "Packet.dll", "WinDivert64.sys"]
    : ["easytier-core", "easytier-cli"]
  let copied = 0
  for (const f of files) {
    const src = path.join(srcDir, f)
    if (!existsSync(src)) {
      console.warn(`  warning: EasyTier file missing: ${f}`)
      continue
    }
    await cp(src, path.join(buildBin, f), { force: true })
    copied++
  }
  console.log(`  bundled EasyTier ${vendorName} (${copied} files)`)
}

async function applyPackage(packageRoot, prefix, options) {
  assertRuntimePackage(packageRoot)

  const existingConfig = readInstalledConfig(prefix)
  const packageConfig = readJSONIfExists(path.join(packageRoot, "cli", "config.json"))
  const mergedConfig = mergeRuntimeConfig(packageConfig, existingConfig, {
    sourceRoot: options.sourceRoot,
  })
  const version = readVersion(packageRoot)

  mkdirSync(prefix, { recursive: true })
  const existingCtl = path.join(prefix, "bin", binaryName("vantaloomctl"))
  if (existsSync(existingCtl)) {
    spawnSync(existingCtl, ["stop", "--prefix", prefix], { stdio: "inherit", windowsHide: true })
  }

  // Kill lingering tray process that may hold locks on bin/ (older versions
  // don't write tray.pid, so vantaloomctl stop won't find them).
  killTrayProcess(prefix)

  // Windows releases a stopped process's file handles asynchronously; copying
  // bin/ the instant after `stop` can still hit the old exe's lock (EPERM). Give
  // the OS a moment to close handles, and sweep any `.old-*` files a previous
  // lock-safe replace left behind (now that those handles are likely released).
  // copyFileResilient below is the real safety net if a handle is still open.
  if (existsSync(path.join(prefix, "bin"))) {
    sleepSync(600)
    cleanupStaleReplacements(path.join(prefix, "bin"))
  }

  // On Windows the privileged mesh service holds its binaries open, so we can't
  // wipe or overwrite them with a plain copy. Detect that up front: if the
  // service is running, skip those files in the bin/ copy (the elevated `apply`
  // swaps them after stopping the service).
  const meshLocked =
    process.platform === "win32" &&
    meshServiceRunning(path.join(packageRoot, "bin"), "win32")

  // Copy package contents to install prefix
  const meshNames = meshBinarySet(process.platform)
  // "adapters" carries the bundled Claude Code ACP adapter + reclaude shim. Older
  // packages predate it, so its absence is tolerated (no warning) — bin/web/cli are
  // still required.
  const optionalDirs = new Set(["adapters"])
  for (const name of ["bin", "web", "cli", "adapters"]) {
    const src = path.join(packageRoot, name)
    const dst = path.join(prefix, name)
    if (!existsSync(src)) {
      if (!optionalDirs.has(name)) {
        console.error(`  warning: package missing ${name}/ directory`)
      }
      continue
    }
    const copyOpts = { recursive: true, force: true, dereference: false }
    if (name === "bin") {
      // Never wipe bin/: that would drop the running mesh binaries (and, on Linux,
      // clear easytier-core's setcap capability). Overwrite in place, but SKIP a
      // mesh binary when the running service holds it open (Windows) OR it is
      // byte-identical to what's installed. The upshot: an update where the mesh
      // is unchanged leaves the privileged service entirely untouched, so it needs
      // no UAC/sudo. Changed mesh binaries and all non-mesh files are copied.
      copyOpts.filter = (s) => {
        const base = path.basename(s)
        if (!meshNames.includes(base)) return true
        if (meshLocked) return false
        const installed = path.join(dst, base)
        return !(existsSync(installed) && fileSha(s) === fileSha(installed))
      }
      // bin/ holds the long-lived executables (api, agent, ctl, easytier, mesh).
      // Copy each file with the lock-safe move-aside replace so a still-locked
      // binary from a not-fully-exited prior process never aborts the update with
      // EPERM (the bug that wedged update/restart).
      copyDirResilient(src, dst, copyOpts.filter)
      continue
    } else {
      removeKnownPath(dst, prefix)
    }
    await cp(src, dst, copyOpts)
  }

  // Ensure binaries are executable on Unix (cross-compiled from Windows they lose +x)
  const binDir = path.join(prefix, "bin")
  if (process.platform !== "win32" && existsSync(binDir)) {
    for (const entry of readdirSync(binDir)) {
      const binPath = path.join(binDir, entry)
      try { chmodSync(binPath, 0o755) } catch {}
    }
  }

  // Linux: grant easytier-core the TUN capability so the unprivileged runtime can
  // bring up the mesh virtual network. This is the one privileged step of the
  // install (a single sudo); the app itself stays unprivileged.
  ensureLinuxMeshCapabilities(binDir)

  // Windows/macOS: register (or lock-safely update) the privileged mesh service
  // that runs easytier-core. This is the one elevation of the install on those
  // platforms. Skipped for --no-start and gated to avoid prompting on every
  // source rebuild (see ensureMeshService).
  if (!options.noStart) {
    ensureMeshService(packageRoot, prefix, {
      sourceInstall: Boolean(options.sourceRoot),
      withMesh: Boolean(options.withMesh),
      skipMesh: Boolean(options.skipMesh),
    })
  }

  // Make the runtime start at login/boot (per-user, no elevation). Idempotent.
  if (!options.noStart && !options.skipAutostart) {
    enableRuntimeAutostart(prefix)
  }

  // Verify vantaloomctl binary exists before trying to run it
  const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
  if (!existsSync(ctlBin)) {
    const binContents = existsSync(binDir) ? readdirSync(binDir) : []
    const srcBinContents = existsSync(path.join(packageRoot, "bin")) ? readdirSync(path.join(packageRoot, "bin")) : []
    throw new Error(
      `vantaloomctl binary not found at ${ctlBin}\n` +
      `  installed bin/: [${binContents.join(", ")}]\n` +
      `  package bin/:   [${srcBinContents.join(", ")}]\n` +
      `  platform: ${platformId()}\n` +
      `  This may indicate a corrupt download. Try again or install from source.`
    )
  }

  await writeLauncher(prefix)
  await writeText(path.join(prefix, "cli", "config.json"), `${JSON.stringify(mergedConfig, null, 2)}\n`)
  await writeText(path.join(prefix, "VERSION"), `${version}\n`)
  await cp(path.join(packageRoot, "manifest.json"), path.join(prefix, "manifest.json"), {
    force: true,
  })

  run(ctlBin, [
    "install",
    "--prefix",
    prefix,
    "--version",
    version,
  ])

  if (!options.noStart) {
    run(ctlBin, [
      "start",
      "--prefix",
      prefix,
    ])
  }

  return version
}

// ensureLinuxMeshCapabilities grants easytier-core the network capabilities it
// needs to create a TUN device, so the unprivileged Vantaloom runtime can bring
// up the EasyTier mesh without running as root. Re-run on every update because
// replacing the binary clears file capabilities. Non-fatal: if it can't elevate
// or setcap is missing, mesh just falls back to the Hub relay.
function ensureLinuxMeshCapabilities(binDir) {
  if (process.platform !== "linux") {
    return
  }
  const core = path.join(binDir, "easytier-core")
  if (!existsSync(core)) {
    console.warn("  mesh: easytier-core not bundled; skipping TUN capability setup")
    return
  }
  const caps = "cap_net_admin,cap_net_bind_service+ep"
  // If the capability is already present (we kept an unchanged easytier-core in
  // place above), there's nothing to do — and crucially, no sudo prompt. setcap
  // only runs when the binary was actually replaced or never had the cap.
  const have = spawnSync("getcap", [core], { encoding: "utf8", windowsHide: true })
  if (
    !have.error &&
    typeof have.stdout === "string" &&
    have.stdout.includes("cap_net_admin") &&
    have.stdout.includes("cap_net_bind_service")
  ) {
    return
  }
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0
  console.log(`  mesh: granting TUN capability to easytier-core${isRoot ? "" : " (sudo)"} ...`)
  const result = isRoot
    ? spawnSync("setcap", [caps, core], { stdio: "inherit", windowsHide: true })
    : spawnSync("sudo", ["setcap", caps, core], { stdio: "inherit", windowsHide: true })
  if (result.error || result.status !== 0) {
    console.warn(
      "  mesh: could not set capabilities (P2P will fall back to the Hub relay).\n" +
      `        enable it manually with:  sudo setcap ${caps} ${core}\n` +
      "        (needs the 'setcap' tool, e.g. apt install libcap2-bin)"
    )
  }
}

// ensureMeshService registers (or lock-safely updates) the privileged EasyTier
// sidecar service on Windows/macOS — the one elevation of the install on those
// platforms. Linux uses setcap instead (see ensureLinuxMeshCapabilities).
//
// To keep the source/dev rebuild loop frictionless it only elevates when the
// work is actually needed (service missing, or the mesh binaries changed) and,
// for source installs, defers to a printed one-liner unless --with-mesh is set.
function ensureMeshService(packageRoot, prefix, opts) {
  const platform = process.platform
  if (platform !== "win32" && platform !== "darwin") return // Linux: setcap path
  if (opts.skipMesh) return

  const pkgBin = path.join(packageRoot, "bin")
  const meshExe = path.join(pkgBin, binaryName("vantaloom-mesh"))
  if (!existsSync(meshExe)) return // sidecar not bundled

  if (!meshServiceNeedsApply(pkgBin, path.join(prefix, "bin"), platform)) {
    return // installed binaries current and service already managing them
  }

  if (opts.sourceInstall && !opts.withMesh) {
    printMeshManualHint(prefix, platform)
    return
  }

  if (platform === "win32") {
    elevateWindowsMeshApply(pkgBin, prefix)
  } else {
    elevateDarwinMeshInstall(prefix)
  }
}

// meshServiceNeedsApply reports whether the privileged service has to be
// (re)applied: true if the installed mesh binaries are missing/outdated, or if
// the service isn't currently up.
function meshServiceNeedsApply(pkgBin, installBin, platform) {
  // On Windows compare every lockable mesh file (incl. Packet.dll/WinDivert/wintun)
  // so a missing or changed support DLL also triggers a (re)apply.
  const names = platform === "win32"
    ? WINDOWS_MESH_BINARIES
    : [binaryName("vantaloom-mesh"), binaryName("easytier-core")]
  for (const name of names) {
    const installed = path.join(installBin, name)
    if (!existsSync(installed)) return true
    const staged = path.join(pkgBin, name)
    if (existsSync(staged) && fileSha(staged) !== fileSha(installed)) return true
  }
  return !meshServiceRunning(pkgBin, platform)
}

// meshServiceRunning queries the OS service state via the (unprivileged) mesh
// `status` command. binDir is where to find the vantaloom-mesh binary to run.
function meshServiceRunning(binDir, platform) {
  const exe = path.join(binDir, binaryName("vantaloom-mesh"))
  if (!existsSync(exe)) return false
  const r = spawnSync(exe, ["status"], { encoding: "utf8", windowsHide: true })
  if (r.error || typeof r.stdout !== "string") return false
  const out = r.stdout.toLowerCase()
  if (platform === "win32") return out.includes("running")
  return r.status === 0 && !out.includes("not loaded") && !out.includes("not installed")
}

function fileSha(file) {
  return createHash("sha256").update(readFileSync(file)).digest("hex")
}

// elevateWindowsMeshApply runs the lock-safe `apply` elevated (one UAC prompt).
// It launches from the package copy so it can overwrite the installed binary.
function elevateWindowsMeshApply(pkgBin, prefix) {
  const exe = path.join(pkgBin, "vantaloom-mesh.exe")
  console.log("  mesh: registering privileged P2P service (UAC prompt) ...")
  const argList = ["apply", "--pkg-bin", pkgBin, "--install-dir", prefix].map(psQuote).join(",")
  const ps = `$ErrorActionPreference='Stop'; $p = Start-Process -FilePath ${psQuote(exe)} -ArgumentList ${argList} -Verb RunAs -Wait -PassThru; exit $p.ExitCode`
  const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "inherit", windowsHide: true })
  if (r.error || r.status !== 0) {
    const installed = path.join(prefix, "bin", "vantaloom-mesh.exe")
    console.warn("  mesh: service registration was cancelled or failed; P2P falls back to the Hub relay.")
    console.warn(`        enable it later (as Administrator):  "${installed}" install --install-dir "${prefix}"`)
  } else {
    console.log("  mesh: privileged P2P service active")
  }
}

// elevateDarwinMeshInstall registers the LaunchDaemon via sudo (one prompt). The
// runtime already copied the fresh binary into place; install reloads the daemon.
function elevateDarwinMeshInstall(prefix) {
  const exe = path.join(prefix, "bin", "vantaloom-mesh")
  console.log("  mesh: registering privileged P2P service (sudo) ...")
  const r = spawnSync("sudo", [exe, "install", "--install-dir", prefix], { stdio: "inherit", windowsHide: true })
  if (r.error || r.status !== 0) {
    console.warn("  mesh: LaunchDaemon registration failed; P2P falls back to the Hub relay.")
    console.warn(`        enable it later:  sudo "${exe}" install --install-dir "${prefix}"`)
  } else {
    console.log("  mesh: privileged P2P service active")
  }
}

function printMeshManualHint(prefix, platform) {
  console.log("  mesh: P2P service not auto-registered for source installs.")
  if (platform === "win32") {
    const exe = path.join(prefix, "bin", "vantaloom-mesh.exe")
    console.log(`        enable it once (as Administrator):  "${exe}" install --install-dir "${prefix}"`)
    console.log("        or re-run install/update with --with-mesh")
  } else {
    const exe = path.join(prefix, "bin", "vantaloom-mesh")
    console.log(`        enable it once:  sudo "${exe}" install --install-dir "${prefix}"`)
    console.log("        or re-run install/update with --with-mesh")
  }
}

// psQuote wraps a value as a PowerShell single-quoted string literal.
function psQuote(value) {
  return "'" + String(value).replace(/'/g, "''") + "'"
}

// uninstallRuntime tears down a local install: stop the runtime, remove the
// privileged mesh service (elevated), then delete the install directory.
async function uninstallRuntime(options) {
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  if (!existsSync(prefix)) {
    console.log(`Vantaloom is not installed at ${prefix}`)
    return
  }
  console.log(`Uninstalling Vantaloom from ${prefix} ...`)

  // 1. Stop the runtime (api/agent/web/tray). Best-effort.
  const ctlBin = path.join(prefix, "bin", binaryName("vantaloomctl"))
  if (existsSync(ctlBin)) {
    spawnSync(ctlBin, ["stop", "--prefix", prefix], { stdio: "inherit", windowsHide: true })
  }
  killTrayProcess(prefix)

  // 2. Remove the privileged mesh service (releases TUN adapter + file locks).
  removeMeshService(prefix, options)

  // 2b. Remove the login-autostart entry (lives outside the install dir).
  disableRuntimeAutostart()

  // 3. Delete the install directory.
  try {
    const parent = path.dirname(prefix)
    removeKnownPath(prefix, parent)
    console.log(`removed ${prefix}`)
  } catch (error) {
    console.warn(`  could not remove ${prefix}: ${error.message}`)
    console.warn("  some files may still be locked; re-run after closing Vantaloom processes.")
  }

  console.log("Vantaloom uninstalled.")
  console.log("note: the install dir was left out of PATH edits; remove the bin/ entry from your shell profile if you added it.")
}

function removeMeshService(prefix, options) {
  const platform = process.platform
  if (platform !== "win32" && platform !== "darwin") return // Linux: nothing registered
  const exe = path.join(prefix, "bin", binaryName("vantaloom-mesh"))
  if (!existsSync(exe)) return
  if (options.skipMesh) return

  if (platform === "win32") {
    if (!meshServiceRunningOrInstalled(path.join(prefix, "bin"))) return
    console.log("  mesh: removing privileged P2P service (UAC prompt) ...")
    const ps = `$ErrorActionPreference='Stop'; $p = Start-Process -FilePath ${psQuote(exe)} -ArgumentList 'uninstall' -Verb RunAs -Wait -PassThru; exit $p.ExitCode`
    const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "inherit", windowsHide: true })
    if (r.error || r.status !== 0) {
      console.warn(`  mesh: could not remove service; run manually (as Administrator):  "${exe}" uninstall`)
    }
  } else {
    console.log("  mesh: removing privileged P2P service (sudo) ...")
    const r = spawnSync("sudo", [exe, "uninstall"], { stdio: "inherit", windowsHide: true })
    if (r.error || r.status !== 0) {
      console.warn(`  mesh: could not remove service; run manually:  sudo "${exe}" uninstall`)
    }
  }
}

// meshServiceRunningOrInstalled reports whether the Windows service exists at
// all (running or stopped), so uninstall only prompts for elevation when there
// is actually something to remove.
function meshServiceRunningOrInstalled(binDir) {
  const exe = path.join(binDir, "vantaloom-mesh.exe")
  if (!existsSync(exe)) return false
  const r = spawnSync(exe, ["status"], { encoding: "utf8", windowsHide: true })
  if (r.error || typeof r.stdout !== "string") return false
  return !r.stdout.toLowerCase().includes("not installed")
}

// ── Runtime login-autostart (no elevation) ──
// Start the local runtime (api/agent/web/tray) at login/boot using only
// per-user mechanisms — no UAC/sudo. This is separate from the privileged mesh
// service: the mesh sidecar autostarts via the OS service manager; this brings
// up the unprivileged runtime that joins the mesh and serves the local API.

const AUTOSTART_LABEL = "online.timefiles.vantaloom.runtime"
// PowerShell HKCU: drive path. Set-ItemProperty handles values with spaces and
// embedded quotes cleanly (reg.exe's /d escaping is brittle), and the Run key is
// not blocked by Controlled Folder Access the way the Startup folder is.
const WINDOWS_RUN_KEY = "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
const WINDOWS_RUN_VALUE = "Vantaloom"

function enableRuntimeAutostart(prefix) {
  try {
    if (process.platform === "win32") {
      // A .vbs launches the runtime with a hidden window (no console flash); the
      // HKCU Run key runs it at login without elevation.
      const launcher = path.join(prefix, "vantaloom.cmd")
      const vbsPath = path.join(prefix, "autostart.vbs")
      const vbs = `' Vantaloom runtime autostart (hidden)\r\nCreateObject("WScript.Shell").Run """${launcher}"" start", 0, False\r\n`
      writeFileSync(vbsPath, vbs)
      const result = spawnSync(
        "powershell",
        [
          "-NoProfile",
          "-NonInteractive",
          "-Command",
          `Set-ItemProperty -Path '${WINDOWS_RUN_KEY}' -Name '${WINDOWS_RUN_VALUE}' -Value 'wscript.exe "${vbsPath}"'`,
        ],
        { stdio: "ignore", windowsHide: true }
      )
      if (result.error || result.status !== 0) {
        console.warn("  autostart: could not register login entry (HKCU Run)")
      } else {
        console.log("  autostart: enabled (login Run key)")
      }
      return
    }
    if (process.platform === "darwin") {
      const launcher = path.join(prefix, "vantaloom")
      const dir = path.join(os.homedir(), "Library", "LaunchAgents")
      mkdirSync(dir, { recursive: true })
      const plistPath = path.join(dir, `${AUTOSTART_LABEL}.plist`)
      // RunAtLoad only (no KeepAlive): `vantaloom start` spawns detached and exits.
      const plist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>${AUTOSTART_LABEL}</string>
  <key>ProgramArguments</key><array><string>${launcher}</string><string>start</string></array>
  <key>RunAtLoad</key><true/>
</dict></plist>
`
      writeFileSync(plistPath, plist)
      spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore", windowsHide: true })
      spawnSync("launchctl", ["load", "-w", plistPath], { stdio: "ignore", windowsHide: true })
      console.log("  autostart: enabled (LaunchAgent)")
      return
    }
    // Linux: user systemd unit + linger (no root).
    const launcher = path.join(prefix, "vantaloom")
    const dir = path.join(os.homedir(), ".config", "systemd", "user")
    mkdirSync(dir, { recursive: true })
    const unit = `[Unit]
Description=Vantaloom local runtime
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${launcher} start
ExecStop=${launcher} stop

[Install]
WantedBy=default.target
`
    writeFileSync(path.join(dir, "vantaloom-runtime.service"), unit)
    spawnSync("loginctl", ["enable-linger"], { stdio: "ignore", windowsHide: true })
    spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore", windowsHide: true })
    spawnSync("systemctl", ["--user", "enable", "vantaloom-runtime.service"], { stdio: "ignore", windowsHide: true })
    console.log("  autostart: enabled (systemd user unit)")
  } catch (error) {
    console.warn(`  autostart: could not enable (${error.message})`)
  }
}

function disableRuntimeAutostart() {
  try {
    if (process.platform === "win32") {
      // Remove the Run-key entry; the autostart.vbs lives in the install dir and
      // is deleted with it.
      spawnSync(
        "powershell",
        ["-NoProfile", "-NonInteractive", "-Command", `Remove-ItemProperty -Path '${WINDOWS_RUN_KEY}' -Name '${WINDOWS_RUN_VALUE}' -ErrorAction SilentlyContinue`],
        { stdio: "ignore", windowsHide: true }
      )
      return
    }
    if (process.platform === "darwin") {
      const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`)
      if (existsSync(plistPath)) {
        spawnSync("launchctl", ["unload", "-w", plistPath], { stdio: "ignore", windowsHide: true })
        rmSync(plistPath, { force: true })
      }
      return
    }
    const unit = path.join(os.homedir(), ".config", "systemd", "user", "vantaloom-runtime.service")
    if (existsSync(unit)) {
      spawnSync("systemctl", ["--user", "disable", "vantaloom-runtime.service"], { stdio: "ignore", windowsHide: true })
      rmSync(unit, { force: true })
      spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore", windowsHide: true })
    }
  } catch (error) {
    console.warn(`  autostart: could not disable (${error.message})`)
  }
}

async function syncFromNpmRegistry(options, action) {
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  const installedConfig = readInstalledConfig(prefix)
  // Always derive runtimePackage from current platform — never trust stale config
  // from a different platform (e.g. win32 config baked into a darwin package).
  // Only explicit --runtime-package flag can override.
  const runtimePackage = options.runtimePackage || runtimePackageName(platformId())
  const runtimeVersion = options.runtimeVersion || installedConfig.runtimeVersion || "latest"
  const explicitRegistry = options.npmRegistry || installedConfig.npmRegistry
  const registry = normalizeRegistry(explicitRegistry || detectNpmRegistry() || defaultNpmRegistry)
  const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-npm-"))

  try {
    const extractRoot = path.join(tempRoot, "extract")
    const archive = path.join(tempRoot, `${packageBasename(runtimePackage)}-${runtimeVersion}.tgz`)
    mkdirSync(extractRoot, { recursive: true })

    const resolved = await resolveNpmPackageWithFallback({
      registries: explicitRegistry ? [registry] : [registry, ...fallbackNpmRegistries.map(normalizeRegistry)],
      name: runtimePackage,
      version: runtimeVersion,
    })
    await downloadNpmTarball({
      tarballUrl: resolved.tarball,
      target: archive,
      packageName: runtimePackage,
      version: resolved.version,
    })
    run("tar", ["-xzf", archive, "-C", extractRoot])

    const packageRoot = findExtractedNpmPackage(extractRoot)
    const version = await applyPackage(packageRoot, prefix, {
      noStart: options.noStart,
      runtimePackage,
      runtimeVersion: options.runtimeVersion ? resolved.version : "latest",
      npmRegistry: resolved.registry,
      update: action === "update",
      withMesh: options.withMesh,
      skipMesh: options.skipMesh,
    })

    console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
    console.log(`version: ${version}`)
    console.log(`source: ${runtimePackage}@${resolved.version}`)
    console.log(`registry: ${resolved.registry}`)
    ensureInPath(prefix)
    console.log(`run: ${displayCommand(prefix)} status`)
  } finally {
    removeKnownPath(tempRoot, os.tmpdir())
  }
}

async function syncFromRelease(options, action) {
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  const installedConfig = readInstalledConfig(prefix)
  const sourceRoot = installedConfig.sourceRoot
    ? tryAssertSourceRoot(installedConfig.sourceRoot)
    : tryFindSourceRoot()
  const sourceRemote = sourceRoot ? gitRemoteUrl(sourceRoot) : ""
  const repo = options.repo || installedConfig.repo || inferGitHubRepo(options.remote ?? installedConfig.remote ?? sourceRemote) || defaultRepo
  const releaseTag = options.releaseTag || installedConfig.releaseTag || defaultReleaseTag
  const tempRoot = mkdtempSync(path.join(os.tmpdir(), "vantaloom-update-"))

  try {
    const extractRoot = path.join(tempRoot, "extract")
    const archive = path.join(tempRoot, `vantaloom-${platformId()}.tar.gz`)
    mkdirSync(extractRoot, { recursive: true })

    await downloadReleaseAsset({
      repo,
      releaseTag,
      assetName: `vantaloom-${platformId()}.tar.gz`,
      target: archive,
      token: options.githubToken,
    })
    run("tar", ["-xzf", archive, "-C", extractRoot])

    const packageRoot = findExtractedPackage(extractRoot, platformId())
    const version = await applyPackage(packageRoot, prefix, {
      noStart: options.noStart,
      sourceRoot: installedConfig.sourceRoot,
      update: action === "update",
      withMesh: options.withMesh,
      skipMesh: options.skipMesh,
    })

    console.log(`${action === "update" ? "updated" : "installed"} Vantaloom: ${prefix}`)
    console.log(`version: ${version}`)
    console.log(`source: ${repo}@${releaseTag}`)
    ensureInPath(prefix)
    console.log(`run: ${displayCommand(prefix)} status`)
  } finally {
    removeKnownPath(tempRoot, os.tmpdir())
  }
}

async function downloadReleaseAsset({ repo, releaseTag, assetName, target, token }) {
  const releaseUrl = `https://api.github.com/repos/${repo}/releases/tags/${releaseTag}`
  const headers = {
    "User-Agent": "vantaloom-cli",
  }
  const githubToken = token || process.env.VANTALOOM_GITHUB_TOKEN || process.env.GITHUB_TOKEN
  if (githubToken) {
    headers.Authorization = `Bearer ${githubToken}`
  }

  const releaseResponse = await fetch(releaseUrl, { headers })
  if (!releaseResponse.ok) {
    const detail = await releaseResponse.text().catch(() => "")
    const authHint = releaseResponse.status === 404 || releaseResponse.status === 403
      ? " If the repository is private, set VANTALOOM_GITHUB_TOKEN to a GitHub token that can read releases."
      : ""
    throw new Error(`failed to inspect ${repo}@${releaseTag}: HTTP ${releaseResponse.status}.${authHint}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
  }

  const release = await releaseResponse.json()
  const asset = release.assets?.find((asset) => asset.name === assetName)
  if (!asset) {
    const names = release.assets?.map((asset) => asset.name).join(", ") || "none"
    throw new Error(`missing release asset ${assetName} in ${repo}@${releaseTag}; available assets: ${names}`)
  }

  const response = await fetch(asset.url, {
    headers: {
      ...headers,
      Accept: "application/octet-stream",
    },
  })
  if (!response.ok) {
    const detail = await response.text().catch(() => "")
    const authHint = response.status === 404 || response.status === 403
      ? " If the repository is private, set VANTALOOM_GITHUB_TOKEN to a GitHub token that can read releases."
      : ""
    throw new Error(`failed to download ${assetName} from ${repo}@${releaseTag}: HTTP ${response.status}.${authHint}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
  }

  const buffer = Buffer.from(await response.arrayBuffer())
  await writeFile(target, buffer)
}

function detectNpmRegistry() {
  // 1. NPM_CONFIG_REGISTRY env var (highest priority, set by npm/npx when running)
  if (process.env.NPM_CONFIG_REGISTRY) {
    return process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY
  }
  // npm also sets the lowercase variant
  if (process.env.npm_config_registry) {
    return process.env.npm_config_registry
  }

  // 2. Read user .npmrc
  try {
    const npmrcPaths = [
      path.join(os.homedir(), ".npmrc"),
    ]
    // Also check project-level .npmrc
    const localNpmrc = path.resolve(".npmrc")
    if (localNpmrc !== npmrcPaths[0]) {
      npmrcPaths.unshift(localNpmrc)
    }
    for (const npmrcPath of npmrcPaths) {
      if (existsSync(npmrcPath)) {
        const content = readFileSync(npmrcPath, "utf8")
        const match = content.match(/^\s*registry\s*=\s*(.+)/m)
        if (match) {
          return match[1].trim()
        }
      }
    }
  } catch {
    // Ignore .npmrc read errors
  }

  return ""
}

async function resolveNpmPackageWithFallback({ registries, name, version }) {
  const errors = []
  for (const registry of registries) {
    try {
      const result = await resolveNpmPackage({ registry, name, version })
      return { ...result, registry }
    } catch (error) {
      const causeCode = error?.cause?.code || ""
      const isNetworkError = causeCode === "ECONNREFUSED"
        || causeCode === "ENOTFOUND"
        || causeCode === "ETIMEDOUT"
        || causeCode === "ECONNRESET"
        || causeCode === "UND_ERR_CONNECT_TIMEOUT"
        || (error instanceof TypeError && error.message === "fetch failed")
      const isSslError = causeCode === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
        || causeCode === "CERT_HAS_EXPIRED"
        || causeCode === "DEPTH_ZERO_SELF_SIGNED_CERT"
        || causeCode === "SELF_SIGNED_CERT_IN_CHAIN"
        || causeCode === "ERR_TLS_CERT_ALTNAME_INVALID"
      const isRetryable = isNetworkError || isSslError
      errors.push({ registry, error, isRetryable, isSslError })

      if (isRetryable && registries.length > 1) {
        const hint = isSslError ? " (SSL certificate error)" : ""
        console.error(`vantaloom: ${registry} unreachable${hint}, trying next registry...`)
        continue
      }
      if (isSslError) {
        throw new Error(
          `SSL certificate error connecting to ${registry}: ${causeCode}\n` +
          `  Fix: run with --no-strict-ssl, or set npm config: npm config set strict-ssl false`
        )
      }
      // Non-network error (404, parse error, etc.) — don't try fallbacks
      throw error
    }
  }

  // All registries failed
  const hasSslError = errors.some((e) => e.isSslError)
  const tried = errors.map((e) => e.registry).join(", ")
  const sslHint = hasSslError
    ? `\n  SSL fix: run with --no-strict-ssl, or set npm config: npm config set strict-ssl false`
    : ""
  throw new Error(
    `all registries unreachable (tried: ${tried}). ` +
    `Check your network or specify --npm-registry <url>${sslHint}`
  )
}

async function resolveNpmPackage({ registry, name, version }) {
  const metadataUrl = `${registry}/${encodeURIComponent(name).replace("%2F", "%2f")}`
  let response
  try {
    response = await fetch(metadataUrl, {
      headers: {
        Accept: "application/vnd.npm.install-v1+json",
        "User-Agent": "vantaloom-cli",
      },
      signal: AbortSignal.timeout(15000),
    })
  } catch (error) {
    if (error instanceof TypeError && error.message === "fetch failed") {
      const causeCode = error.cause?.code || ""
      const causeMsg = causeCode || error.cause?.message || String(error.cause || "")
      throw Object.assign(
        new Error(`cannot reach registry ${registry} (${causeMsg})`),
        { cause: error.cause }
      )
    }
    throw error
  }
  if (!response.ok) {
    const detail = await response.text().catch(() => "")
    throw new Error(`failed to inspect npm package ${name}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
  }

  const metadata = await response.json()
  const selectedVersion = metadata["dist-tags"]?.[version] ?? version
  const packageVersion = metadata.versions?.[selectedVersion]
  if (!packageVersion?.dist?.tarball) {
    const available = Object.keys(metadata.versions ?? {}).slice(-8).join(", ") || "none"
    throw new Error(`missing npm package ${name}@${version}; available versions: ${available}`)
  }
  return {
    version: selectedVersion,
    tarball: packageVersion.dist.tarball,
  }
}

async function downloadNpmTarball({ tarballUrl, target, packageName, version }) {
  console.log(`  downloading ${packageName}@${version}...`)
  let response
  try {
    response = await fetch(tarballUrl, {
      headers: {
        "User-Agent": "vantaloom-cli",
      },
      signal: AbortSignal.timeout(120000),
    })
  } catch (error) {
    const causeCode = error?.cause?.code || ""
    const causeMsg = causeCode || error?.cause?.message || error.message || ""
    const isSsl = causeCode.includes("CERT") || causeCode === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
    const hint = isSsl ? `\n  Fix: run with --no-strict-ssl` : ""
    throw new Error(`failed to download ${packageName}@${version} from ${tarballUrl}: ${causeMsg}${hint}`)
  }
  if (!response.ok) {
    const detail = await response.text().catch(() => "")
    throw new Error(`failed to download ${packageName}@${version}: HTTP ${response.status}${detail ? ` ${detail.slice(0, 200)}` : ""}`)
  }

  const buffer = Buffer.from(await response.arrayBuffer())
  await writeFile(target, buffer)
  console.log(`  downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`)
}

function shouldUseSourceInstall(options) {
  return Boolean(options.local || options.source || options.buildWeb)
}

function shouldUseReleaseSync(options) {
  return Boolean(options.repo || options.remote || options.releaseTag || options.githubToken)
}

function assertRuntimePackage(packageRoot) {
  for (const name of ["bin", "web", "cli", "manifest.json"]) {
    if (!existsSync(path.join(packageRoot, name))) {
      throw new Error(`invalid Vantaloom package, missing ${name}: ${packageRoot}`)
    }
  }
}

function readVersion(packageRoot) {
  const versionPath = path.join(packageRoot, "VERSION")
  if (existsSync(versionPath)) {
    return readFileSync(versionPath, "utf8").trim() || "dev"
  }
  const manifest = readJSONIfExists(path.join(packageRoot, "manifest.json"))
  return manifest.version ?? "dev"
}

function readInstalledConfig(prefix) {
  return readJSONIfExists(path.join(prefix, "cli", "config.json"))
}

function readJSONIfExists(filePath) {
  if (!existsSync(filePath)) {
    return {}
  }
  return JSON.parse(readFileSync(filePath, "utf8"))
}

function mergeRuntimeConfig(packageConfig, existingConfig, overrides) {
  const merged = { ...packageConfig }
  for (const key of ["sourceRoot", "remote", "repo", "releaseTag", "runtimePackage", "runtimeVersion", "npmRegistry"]) {
    if (!merged[key] && existingConfig[key]) {
      merged[key] = existingConfig[key]
    }
  }
  if (overrides.sourceRoot) {
    merged.sourceRoot = overrides.sourceRoot
  }
  if (overrides.runtimePackage) {
    merged.runtimePackage = overrides.runtimePackage
  }
  if (overrides.runtimeVersion) {
    merged.runtimeVersion = overrides.runtimeVersion
  }
  if (overrides.npmRegistry) {
    merged.npmRegistry = overrides.npmRegistry
  }
  if (!merged.repo) {
    merged.repo = defaultRepo
  }
  if (!merged.releaseTag) {
    merged.releaseTag = defaultReleaseTag
  }
  // Always force runtimePackage to match the running platform — a cross-compiled
  // package may carry a config for a different platform (e.g. win32 inside darwin).
  merged.runtimePackage = runtimePackageName(platformId())
  if (!merged.runtimeVersion) {
    merged.runtimeVersion = "latest"
  }
  if (!merged.npmRegistry) {
    merged.npmRegistry = defaultNpmRegistry
  }
  return merged
}

function runtimeConfigFromSource(sourceRoot) {
  const remote = gitRemoteUrl(sourceRoot)
  const repo = inferGitHubRepo(remote) || defaultRepo
  return {
    ...(process.env.GITHUB_ACTIONS ? {} : { sourceRoot }),
    ...(remote ? { remote } : {}),
    repo,
    releaseTag: defaultReleaseTag,
    runtimePackage: runtimePackageName(platformId()),
    runtimeVersion: "latest",
    npmRegistry: detectNpmRegistry() || defaultNpmRegistry,
  }
}

function findExtractedPackage(extractRoot, platform) {
  const expected = path.join(extractRoot, `vantaloom-${platform}`)
  if (existsSync(expected)) {
    return expected
  }

  const directories = readdirSync(extractRoot, { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => path.join(extractRoot, entry.name))
  if (directories.length === 1) {
    return directories[0]
  }

  throw new Error(`could not find extracted Vantaloom package in ${extractRoot}`)
}

function findExtractedNpmPackage(extractRoot) {
  const expected = path.join(extractRoot, "package")
  if (existsSync(expected)) {
    return expected
  }

  const directories = readdirSync(extractRoot, { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => path.join(extractRoot, entry.name))
  if (directories.length === 1) {
    return directories[0]
  }

  throw new Error(`could not find extracted npm package in ${extractRoot}`)
}

function tryFindSourceRoot() {
  try {
    return findSourceRoot()
  } catch {
    return ""
  }
}

function tryAssertSourceRoot(sourceRoot) {
  try {
    return assertSourceRoot(sourceRoot)
  } catch {
    return ""
  }
}

function gitRemoteUrl(sourceRoot) {
  const result = spawnSync("git", ["remote", "get-url", "origin"], {
    cwd: sourceRoot,
    encoding: "utf8",
    windowsHide: true,
  })
  if (result.status === 0) {
    return result.stdout.trim()
  }
  return ""
}

function inferGitHubRepo(remote) {
  if (!remote) {
    return ""
  }
  const normalized = remote.replace(/\.git$/, "")
  const httpsMatch = normalized.match(/github\.com[:/]([^/]+\/[^/]+)$/)
  if (httpsMatch) {
    return httpsMatch[1]
  }
  const sshMatch = normalized.match(/^[^:]+:([^/]+\/[^/]+)$/)
  return sshMatch?.[1] ?? ""
}

async function copyStaticWeb(sourceRoot, buildWeb) {
  const exportRoot = path.join(sourceRoot, "apps", "vantaloom", "out")
  if (!existsSync(exportRoot)) {
    throw new Error(
      "missing Next static export output; let GitHub CI run production build, or pass --build-web for a local one-off build"
    )
  }

  removeKnownPath(buildWeb, path.dirname(buildWeb))
  await copyDir(exportRoot, buildWeb)
}

async function copyCliDirectory(target, sourceRoot, config) {
  const sourceCliRoot = path.join(sourceRoot, "packages", "cli")
  if (!existsSync(path.join(sourceCliRoot, "bin", "vantaloom.mjs"))) {
    throw new Error(`missing source CLI package: ${sourceCliRoot}`)
  }
  removeKnownPath(target, path.dirname(target))
  mkdirSync(target, { recursive: true })
  await copyDir(sourceCliRoot, target)
  await writeText(
    path.join(target, "config.json"),
    `${JSON.stringify(config, null, 2)}\n`
  )
}

async function writeLauncher(prefix) {
  if (process.platform === "win32") {
    await writeText(
      path.join(prefix, "vantaloom.cmd"),
      `@echo off\r\nnode "%~dp0cli\\bin\\vantaloom.mjs" %*\r\n`
    )
  } else {
    const launcher = `#!/usr/bin/env sh\nexec node "$(dirname "$0")/cli/bin/vantaloom.mjs" "$@"\n`
    const launcherPath = path.join(prefix, "vantaloom")
    await writeText(launcherPath, launcher)
    chmodSync(launcherPath, 0o755)
  }
}

function runCtl(command, options) {
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  const ctl = path.join(prefix, "bin", binaryName("vantaloomctl"))
  if (!existsSync(ctl)) {
    throw new Error(`missing installed runtime at ${prefix}; run "vantaloom install" first`)
  }

  const args = [command, "--prefix", prefix]
  if (options.component) {
    args.push("--component", options.component)
  }
  run(ctl, args)
}

function printPaths(options) {
  const sourceRoot = findSourceRoot(options.source)
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  console.log(JSON.stringify({ sourceRoot, prefix }, null, 2))
}

// ── Hub login (headless / no-web) ──
// Authenticates to the Hub entirely from the command line, so a machine that
// never opens the web UI (e.g. a server with no public access) can still join
// the workgroup. Reuses the exact endpoints the web app uses, then tells the
// local runtime to connect (which persists the Hub config to local.json).

const DEFAULT_HUB_URL = "http://vanta.timefiles.online"

function localApiBase(prefix) {
  let port
  try {
    port = readFileSync(path.join(prefix, "runtime", "api.port"), "utf8").trim()
  } catch {}
  if (!port) {
    const cfg = readJSONIfExists(path.join(prefix, "config", "local.json"))
    port = cfg.apiPort ? String(cfg.apiPort) : "8780"
  }
  return `http://127.0.0.1:${port}`
}

function localMachineInfo() {
  const platMap = { win32: "windows", darwin: "darwin", linux: "linux" }
  const archMap = { x64: "amd64", arm64: "arm64" }
  return {
    name: `${platMap[process.platform] ?? process.platform}-${os.hostname() || "unknown"}`,
    platform: platMap[process.platform] ?? process.platform,
    arch: archMap[process.arch] ?? process.arch,
    localIp: "127.0.0.1",
  }
}

async function hubFetch(url, { method = "GET", body, token } = {}) {
  const headers = { "Content-Type": "application/json" }
  if (token) headers.Authorization = `Bearer ${token}`
  const res = await fetch(url, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  })
  const data = await res.json().catch(() => ({}))
  if (!res.ok) {
    throw new Error(data.error || `${url}: HTTP ${res.status}`)
  }
  return data
}

function promptVisible(query) {
  return new Promise((resolve) => {
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
    rl.question(query, (answer) => {
      rl.close()
      resolve(answer.trim())
    })
  })
}

function promptHidden(query) {
  return new Promise((resolve) => {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
      terminal: true,
    })
    process.stdout.write(query)
    // Suppress echo so the password isn't shown as it's typed.
    rl._writeToOutput = () => {}
    rl.question("", (answer) => {
      rl.close()
      process.stdout.write("\n")
      resolve(answer)
    })
  })
}

async function runLogin(options) {
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  const existing = readJSONIfExists(path.join(prefix, "config", "local.json"))
  const hubUrl = (options.hub ?? existing.hubUrl ?? DEFAULT_HUB_URL).replace(/\/+$/, "")
  const localApi = localApiBase(prefix)

  // The local runtime must be running: we need its stable hardware id (so the
  // Hub dedups re-registrations) and it's what actually connects to the Hub.
  let hardwareId = null
  try {
    const res = await fetch(`${localApi}/v1/hub/status`)
    if (res.ok) hardwareId = (await res.json()).hardwareId ?? null
  } catch {}
  if (!hardwareId) {
    throw new Error(
      `local runtime not reachable at ${localApi}. Start it first with "vantaloom start", then retry "vantaloom login".`
    )
  }

  const email = options.email ?? (await promptVisible("Hub email: "))
  const password = options.password ?? (await promptHidden("Hub password: "))
  if (!email || !password) {
    throw new Error("email and password are required")
  }

  console.log(`logging in to ${hubUrl} ...`)
  const login = await hubFetch(`${hubUrl}/api/auth/login`, {
    method: "POST",
    body: { email, password },
  })
  const userToken = login.token
  if (!userToken) throw new Error("login failed: no token returned")

  const { deviceToken } = await hubFetch(`${hubUrl}/api/machines/token`, {
    method: "POST",
    token: userToken,
  })
  const info = localMachineInfo()
  const connect = await hubFetch(`${hubUrl}/api/connect`, {
    method: "POST",
    body: { deviceToken, hardwareId, ...info },
  })
  const machineId = connect.machine?.id
  if (!machineId) throw new Error("machine registration failed: no machine id")

  // Hand the credentials to the running runtime; it persists them to local.json
  // and brings up the Hub WebSocket + heartbeat + mesh.
  const res = await fetch(`${localApi}/v1/hub/connect`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ hubUrl, hubToken: userToken, machineId }),
  })
  if (!res.ok) {
    throw new Error(`runtime failed to connect to Hub: HTTP ${res.status}`)
  }

  console.log(`\n✓ logged in as ${login.user?.email ?? email}`)
  console.log(`✓ machine registered: ${info.name} (${machineId})`)
  console.log(`✓ runtime connected to Hub at ${hubUrl}`)
  console.log(`\nThis machine is now in your workgroup. It will reconnect automatically on restart.`)
}

async function runLogout(options) {
  const prefix = safeDirectory(options.prefix ?? defaultPrefix())
  const localApi = localApiBase(prefix)
  try {
    const res = await fetch(`${localApi}/v1/hub/disconnect`, { method: "POST" })
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    console.log("✓ disconnected from Hub (runtime keeps running locally).")
  } catch (error) {
    throw new Error(
      `could not reach local runtime at ${localApi}: ${error.message}`
    )
  }
}

function buildGo(sourceRoot, buildBin, name, targetPlatform) {
  const goEnv = targetPlatform ? platformToGoEnv(targetPlatform) : {}
  const isWindowsTarget = targetPlatform
    ? targetPlatform.startsWith("win32")
    : process.platform === "win32"
  const ext = isWindowsTarget ? ".exe" : ""
  let ldflags = "-s -w"
  const args = ["build"]
  // The privileged mesh sidecar is built REPRODUCIBLY: with no embedded git VCS
  // stamp or build id, unchanged mesh source yields byte-identical output across
  // releases. That lets an update where the mesh hasn't changed be detected as a
  // no-op and SKIP the privileged service re-apply — so routine updates need no
  // UAC (Windows) / sudo (Linux/macOS) and can run fully unattended/remotely.
  if (name === "vantaloom-mesh") {
    args.push("-trimpath", "-buildvcs=false")
    ldflags += " -buildid="
  }
  args.push(
    "-ldflags", ldflags,
    "-o", path.join(buildBin, `${name}${ext}`),
    `./apps/api/cmd/${name}`
  )
  run("go", args, { cwd: sourceRoot, env: { ...process.env, ...goEnv } })
}

async function copyDir(source, destination, options = {}) {
  if (!existsSync(source)) {
    throw new Error(`missing source directory: ${source}`)
  }
  removeKnownPath(destination, path.dirname(destination))
  mkdirSync(destination, { recursive: true })
  await cp(source, destination, {
    recursive: true,
    force: true,
    dereference: options.dereference ?? true,
  })
}

function writeBuildManifest(buildRoot, version, platform) {
  const components = ["api", "agent", "web", "ctl"]
  writeFileSync(path.join(buildRoot, "VERSION"), `${version}\n`)
  writeFileSync(
    path.join(buildRoot, "manifest.json"),
    `${JSON.stringify(
      {
        name: "Vantaloom Local Runtime",
        version,
        platform,
        updatedAt: new Date().toISOString(),
        components,
      },
      null,
      2
    )}\n`
  )
}

function writeRuntimePackageMetadata(packageRoot, sourceRoot, platform) {
  const version = npmPackageVersion(sourceRoot)
  const { os: runtimeOS, cpu } = parsePlatformId(platform)
  const name = runtimePackageName(platform)
  writeFileSync(
    path.join(packageRoot, "package.json"),
    `${JSON.stringify(
      {
        name,
        version,
        private: false,
        description: `Vantaloom local runtime for ${platform}.`,
        type: "module",
        os: [runtimeOS],
        cpu: [cpu],
        files: ["bin", "web", "cli", "manifest.json", "VERSION", "README.md"],
        publishConfig: {
          access: "public",
        },
        engines: {
          node: ">=20",
        },
      },
      null,
      2
    )}\n`
  )
  writeFileSync(
    path.join(packageRoot, "README.md"),
    `# ${name}\n\nPlatform runtime package for Vantaloom ${platform}. Install @vantaloom/cli instead of this package directly.\n`
  )
}

function findSourceRoot(sourceOption) {
  if (sourceOption) {
    return assertSourceRoot(path.resolve(sourceOption))
  }
  if (process.env.VANTALOOM_SOURCE) {
    return assertSourceRoot(path.resolve(process.env.VANTALOOM_SOURCE))
  }
  if (existsSync(installedConfigPath)) {
    const config = JSON.parse(readFileSync(installedConfigPath, "utf8"))
    if (config.sourceRoot) {
      return assertSourceRoot(path.resolve(config.sourceRoot))
    }
  }
  return assertSourceRoot(repoCandidate)
}

function npmPackageVersion(sourceRoot) {
  const packageJSON = readJSONIfExists(path.join(sourceRoot, "packages", "cli", "package.json"))
  return packageJSON.version ?? "0.0.0"
}

function assertSourceRoot(sourceRoot) {
  if (!existsSync(path.join(sourceRoot, "apps", "api", "go.mod"))) {
    throw new Error(`not a Vantaloom source root: ${sourceRoot}`)
  }
  return sourceRoot
}

function gitVersion(sourceRoot) {
  const result = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
    cwd: sourceRoot,
    encoding: "utf8",
    windowsHide: true,
  })
  if (result.status === 0) {
    return result.stdout.trim() || "dev"
  }
  return "dev"
}

function parseOptions(args) {
  const options = {}
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index]
    if (!arg.startsWith("--")) {
      throw new Error(`unexpected argument "${arg}"`)
    }
    const [key, inlineValue] = arg.slice(2).split("=", 2)
    switch (key) {
      case "prefix":
      case "source":
      case "component":
      case "package":
      case "output":
      case "repo":
      case "remote":
      case "release-tag":
      case "github-token":
      case "runtime-package":
      case "runtime-version":
      case "npm-registry":
      case "hub":
      case "email":
      case "password":
        options[toCamel(key)] = inlineValue ?? args[++index]
        if (!options[toCamel(key)]) {
          throw new Error(`missing value for --${key}`)
        }
        break
      case "build-web":
        options.buildWeb = true
        break
      case "no-start":
        options.noStart = true
        break
      case "archive":
        options.archive = true
        break
      case "npm-package":
        options.npmPackage = true
        break
      case "target":
        options.target = inlineValue ?? args[++index]
        if (!options.target) {
          throw new Error("missing value for --target")
        }
        break
      case "local":
        options.local = true
        break
      case "with-mesh":
        options.withMesh = true
        break
      case "skip-mesh":
        options.skipMesh = true
        break
      case "skip-adapter":
        options.skipAdapter = true
        break
      case "skip-autostart":
        options.skipAutostart = true
        break
      case "no-strict-ssl":
        options.noStrictSsl = true
        break
      default:
        throw new Error(`unknown option --${key}`)
    }
  }
  return options
}

function safeDirectory(value) {
  const full = path.resolve(value)
  const parsed = path.parse(full)
  if (full === parsed.root) {
    throw new Error(`refusing to operate on unsafe directory: ${value}`)
  }
  return full
}

function removeKnownPath(target, expectedParent) {
  if (!existsSync(target)) {
    return
  }
  const full = path.resolve(target)
  const parent = path.resolve(expectedParent)
  const prefix = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`
  if (!full.startsWith(prefix)) {
    throw new Error(`refusing to remove path outside expected parent: ${full}`)
  }
  rmSync(full, { recursive: true, force: true })
}

function run(command, args, options = {}) {
  try {
    execFileSync(command, args, { stdio: "inherit", windowsHide: true, ...options })
  } catch (error) {
    if (error && typeof error.status === "number") {
      throw new Error(`${command} exited with ${error.status}`)
    }
    throw error
  }
}

function runPnpm(args, options = {}) {
  if (process.platform === "win32") {
    run("cmd.exe", ["/d", "/s", "/c", "pnpm", ...args], options)
    return
  }
  run("pnpm", args, options)
}

// runNpm invokes npm cross-platform. On Windows npm is `npm.cmd`, which
// execFileSync (used by run) can't resolve directly (ENOENT) — so wrap via
// cmd.exe, mirroring runPnpm.
function runNpm(args, options = {}) {
  if (process.platform === "win32") {
    run("cmd.exe", ["/d", "/s", "/c", "npm", ...args], options)
    return
  }
  run("npm", args, options)
}

function binaryName(name, targetPlatform) {
  const isWindows = targetPlatform
    ? targetPlatform.startsWith("win32")
    : process.platform === "win32"
  return isWindows ? `${name}.exe` : name
}

function platformToGoEnv(platform) {
  const { os: runtimeOS, cpu } = parsePlatformId(platform)
  const goosMap = { win32: "windows", linux: "linux", darwin: "darwin" }
  const goarchMap = { x64: "amd64", arm64: "arm64" }
  return {
    GOOS: goosMap[runtimeOS] ?? runtimeOS,
    GOARCH: goarchMap[cpu] ?? cpu,
    CGO_ENABLED: "0",
  }
}

function platformId() {
  return `${process.platform}-${process.arch}`
}

function runtimePackageName(platform) {
  return `@vantaloom/runtime-${platform}`
}

function parsePlatformId(platform) {
  const parts = platform.split("-")
  const cpu = parts.pop()
  const runtimeOS = parts.join("-")
  if (!runtimeOS || !cpu) {
    throw new Error(`invalid platform id: ${platform}`)
  }
  return { os: runtimeOS, cpu }
}

function packageBasename(name) {
  return name.split("/").pop() ?? name
}

function normalizeRegistry(value) {
  return value.replace(/\/+$/, "")
}

function defaultPrefix() {
  if (process.env.VANTALOOM_HOME) {
    return process.env.VANTALOOM_HOME
  }
  if (process.platform === "win32") {
    // Prefer D: (the historical default) but fall back to C: when this machine
    // has no D: drive. Keep in sync with the desktop shell's DefaultPrefix().
    return existsSync("D:\\") ? "D:\\Vantaloom" : "C:\\Vantaloom"
  }
  if (process.platform === "darwin") {
    return path.join(os.homedir(), "Applications", "Vantaloom")
  }
  return path.join(os.homedir(), ".local", "vantaloom")
}

function displayCommand(prefix) {
  if (process.platform === "win32") {
    return path.join(prefix, "vantaloom.cmd")
  }
  return path.join(prefix, "vantaloom")
}

function toCamel(value) {
  return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
}

async function writeText(filePath, content) {
  mkdirSync(path.dirname(filePath), { recursive: true })
  await writeFile(filePath, content)
}

// ensureInPath adds the Vantaloom install directory to the user's shell PATH
// on macOS and Linux, so `vantaloom` can be run directly after install.
// On Windows this is not needed (vantaloom.cmd is run by full path or added via installer).
function ensureInPath(prefix) {
  if (process.platform === "win32") return

  // Check if already in PATH
  const pathDirs = (process.env.PATH || "").split(":")
  if (pathDirs.includes(prefix)) return

  // Determine shell profile file
  const home = os.homedir()
  const shell = process.env.SHELL || ""
  let profilePath
  if (shell.endsWith("/zsh") || existsSync(path.join(home, ".zshrc"))) {
    profilePath = path.join(home, ".zshrc")
  } else if (shell.endsWith("/bash")) {
    // On macOS, bash uses .bash_profile; on Linux, .bashrc
    profilePath = process.platform === "darwin"
      ? path.join(home, ".bash_profile")
      : path.join(home, ".bashrc")
  } else if (existsSync(path.join(home, ".profile"))) {
    profilePath = path.join(home, ".profile")
  } else {
    profilePath = path.join(home, ".profile")
  }

  // Use $HOME-relative path for portability
  const homeRelative = prefix.startsWith(home)
    ? `$HOME${prefix.slice(home.length)}`
    : prefix
  const exportLine = `export PATH="${homeRelative}:$PATH"`
  const marker = "# vantaloom"

  // Check if already added to profile
  try {
    if (existsSync(profilePath)) {
      const content = readFileSync(profilePath, "utf8")
      if (content.includes("vantaloom") && content.includes("PATH")) return
    }
  } catch {}

  // Append to profile
  try {
    const entry = `\n${marker}\n${exportLine}\n`
    appendFileSync(profilePath, entry)
    console.log(`PATH: added ${prefix} to ${profilePath}`)
    console.log(`  run: source ${profilePath}  (or open a new terminal)`)
  } catch (error) {
    console.log(`PATH: could not update ${profilePath}: ${error.message}`)
    console.log(`  add manually: ${exportLine}`)
  }
}

function killTrayProcess(prefix) {
  if (process.platform === "win32") {
    // Try PID file first (new tray versions write runtime/tray.pid).
    const pidFile = path.join(prefix, "runtime", "tray.pid")
    if (existsSync(pidFile)) {
      const pid = readFileSync(pidFile, "utf8").trim()
      if (pid) {
        spawnSync("taskkill", ["/PID", pid, "/F"], { stdio: "ignore", windowsHide: true })
        try { rmSync(pidFile, { force: true }) } catch {}
      }
    }
    // Fallback: kill by image name if the binary is inside our prefix.
    const result = spawnSync("tasklist", ["/FI", "IMAGENAME eq vantaloom-tray.exe", "/FO", "CSV", "/NH"], {
      encoding: "utf8",
      windowsHide: true,
    })
    if (result.stdout) {
      for (const line of result.stdout.split("\n")) {
        const match = line.match(/"vantaloom-tray\.exe","(\d+)"/)
        if (match) {
          spawnSync("taskkill", ["/PID", match[1], "/F"], { stdio: "ignore", windowsHide: true })
        }
      }
    }
    // Brief pause to let file handles release.
    spawnSync("timeout", ["/t", "1", "/nobreak"], { stdio: "ignore", windowsHide: true })
  } else {
    // Unix: pkill by name (best-effort).
    spawnSync("pkill", ["-f", "vantaloom-tray"], { stdio: "ignore", windowsHide: true })
  }
}

function printHelp() {
  console.log(`Vantaloom CLI

Usage:
  vantaloom install   [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--package <dir>] [--no-start] [--with-mesh|--skip-mesh]
  vantaloom install   --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start] [--with-mesh]
  vantaloom update    [--prefix <dir>] [--runtime-version <version>] [--npm-registry <url>] [--no-start] [--with-mesh|--skip-mesh]
  vantaloom update    --local [--prefix <dir>] [--source <repo>] [--build-web] [--no-start] [--with-mesh]
  vantaloom uninstall [--prefix <dir>] [--skip-mesh]
  vantaloom package   [--source <repo>] [--output <dir>] [--build-web] [--archive] [--npm-package] [--target <platform>]
  vantaloom install-adapter [--prefix <dir>]   (download the Claude Code ACP adapter into the install)
  vantaloom start     [--prefix <dir>] [--component all|api|agent|web]
  vantaloom stop      [--prefix <dir>] [--component all|api|agent|web]
  vantaloom restart   [--prefix <dir>] [--component all|api|agent|web]
  vantaloom status    [--prefix <dir>]
  vantaloom ports     [--prefix <dir>]
  vantaloom path      [--prefix <dir>] [--source <repo>]
  vantaloom platform
  vantaloom login     [--hub <url>] [--email <email>] [--password <pw>] [--prefix <dir>]
  vantaloom logout    [--prefix <dir>]

Hub login (for machines with no web access):
  "vantaloom login" authenticates to the Hub from the terminal and joins this
  machine to your workgroup — no browser needed. Email/password may be passed as
  flags (for scripts) or entered interactively. The local runtime must be running
  (run "vantaloom start" first).

Mesh (P2P) service:
  Windows/macOS register a privileged EasyTier service at install (one UAC/sudo prompt).
  Linux grants TUN capability via setcap instead. Use --skip-mesh to opt out,
  or --with-mesh to force registration during a --local source install.
`)
}
