import { i as normalizeLowercaseStringOrEmpty, s as normalizeOptionalString } from "./string-coerce-CIT1-v_a.js";
import { n as defaultRuntime } from "./runtime-CgZZEqw7.js";
import { d as pathExists } from "./utils-CE5zxr9Z.js";
import { t as resolveKaijiBotPackageRoot } from "./kaijibot-root-DrKPBBQD.js";
import { t as formatDocsLink } from "./links-D3Ka0iWu.js";
import { r as theme } from "./theme-AvNURkOg.js";
import { n as inheritOptionFromParent } from "./command-options-BprOyVpY.js";
import { a as parseSemver, i as nodeVersionSatisfiesEngine } from "./runtime-guard-B_Pvw7tc.js";
import { n as DEFAULT_GATEWAY_PORT, u as resolveGatewayPort } from "./paths-CAiS8AEq.js";
import { r as runCommandWithTimeout } from "./exec-B1X3fcmA.js";
import { F as asResolvedSourceConfig, I as asRuntimeConfig, l as readConfigFileSnapshot } from "./io-B_JNqEVl.js";
import { r as replaceConfigFile } from "./config-BR67jvKt.js";
import { n as resolveCliName, t as replaceCliName } from "./cli-name-CQICB27Q.js";
import { t as formatCliCommand } from "./command-format-BeJUeZeT.js";
import { d as resolveGatewayLaunchAgentLabel, h as resolveGatewayWindowsTaskName, p as resolveGatewaySystemdServiceName } from "./paths-syJo8Z0T.js";
import { n as stylePromptMessage } from "./prompt-style-Dsx1-MRV.js";
import { o as quoteCmdScriptArg } from "./restart-stale-pids-CVv_-0fS.js";
import { r as resolveGatewayService } from "./service-CP2j6WNs.js";
import { r as runDaemonRestart, s as runDaemonInstall } from "./daemon-cli-BWLLmPWb.js";
import { o as writeGatewayRestartHandoffSync } from "./restart-handoff-B7VBseaV.js";
import { n as formatConfigIssueLines } from "./issue-format-CONY_d8z.js";
import { l as waitForGatewayHealthyRestart, o as renderRestartDiagnostics, s as terminateStaleGatewayPids } from "./restart-health-DRlnO1yl.js";
import { r as formatDurationPrecise } from "./format-duration-D_M5Zinr.js";
import { o as trimLogTail } from "./restart-sentinel-Ca8WUWbf.js";
import { n as resolveGatewayRestartLogPath, r as shellEscapeRestartLogValue, t as renderPosixRestartLogSetup } from "./restart-logs-BmcsK--b.js";
import { t as formatHelpExamples } from "./help-format-CzGgkTGQ.js";
import { d as formatUpdateChannelLabel, g as resolveUpdateChannelDisplay, h as resolveEffectiveUpdateChannel, i as fetchNpmTagVersion, m as normalizeUpdateChannel, n as compareSemverStrings, o as resolveNpmChannelTag, r as fetchNpmPackageTargetStatus, t as checkUpdateStatus, u as channelToNpmTag } from "./update-check-CFiv7MVY.js";
import { i as resolveUpdateAvailability, n as formatUpdateOneLiner, t as formatUpdateAvailableHint } from "./status.update-DznZbl_4.js";
import { a as createGlobalInstallEnv, c as globalInstallArgs, d as resolveGlobalInstallTarget, f as normalizePackageTagInput, i as collectInstalledGlobalPackageErrors, l as resolveExpectedInstalledVersionFromSpec, m as readPackageVersion, n as canResolveRegistryVersionForPackageTarget, o as detectGlobalInstallManagerByPresence, p as readPackageName, r as cleanupGlobalRenameDirs, s as detectGlobalInstallManagerForRoot, t as runGatewayUpdate, u as resolveGlobalInstallSpec } from "./update-runner-DlYRAdIj.js";
import { n as renderTable, t as getTerminalTableWidth } from "./table-B8RObGt5.js";
import { r as installCompletion } from "./completion-cli-B_YzBO2C.js";
import { n as updateNpmInstalledPlugins, t as syncPluginsForUpdateChannel } from "./update-TheZy2xE.js";
import { n as doctorCommand, t as selectStyled } from "./prompt-select-styled-CZKO89Gh.js";
import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-B12WhquS.js";
import path from "node:path";
import { spawn, spawnSync } from "node:child_process";
import os from "node:os";
import fs from "node:fs/promises";
import { confirm, isCancel, spinner } from "@clack/prompts";
//#region src/cli/update-cli/shared.ts
const INVALID_TIMEOUT_ERROR = "--timeout must be a positive integer (seconds)";
function parseTimeoutMsOrExit(timeout) {
	const timeoutMs = timeout ? Number.parseInt(timeout, 10) * 1e3 : void 0;
	if (timeoutMs !== void 0 && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) {
		defaultRuntime.error(INVALID_TIMEOUT_ERROR);
		defaultRuntime.exit(1);
		return null;
	}
	return timeoutMs;
}
const KAIJIBOT_REPO_URL = "https://gitee.com/kaiji1126/kaijibot.git";
const MAX_LOG_CHARS = 8e3;
const DEFAULT_PACKAGE_NAME = "kaijibot";
const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]);
function normalizeTag(value) {
	return normalizePackageTagInput(value, ["kaijibot", DEFAULT_PACKAGE_NAME]);
}
function normalizeVersionTag(tag) {
	const trimmed = tag.trim();
	if (!trimmed) return null;
	const cleaned = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
	return parseSemver(cleaned) ? cleaned : null;
}
async function resolveTargetVersion(tag, timeoutMs) {
	if (!canResolveRegistryVersionForPackageTarget(tag)) return null;
	const direct = normalizeVersionTag(tag);
	if (direct) return direct;
	return (await fetchNpmTagVersion({
		tag,
		timeoutMs
	})).version ?? null;
}
async function isGitCheckout(root) {
	try {
		await fs.stat(path.join(root, ".git"));
		return true;
	} catch {
		return false;
	}
}
async function isCorePackage(root) {
	const name = await readPackageName(root);
	return Boolean(name && CORE_PACKAGE_NAMES.has(name));
}
async function isEmptyDir(targetPath) {
	try {
		return (await fs.readdir(targetPath)).length === 0;
	} catch {
		return false;
	}
}
function resolveGitInstallDir() {
	const override = process.env.KAIJIBOT_GIT_DIR?.trim();
	if (override) return path.resolve(override);
	return resolveDefaultGitDir();
}
function resolveDefaultGitDir() {
	const home = os.homedir();
	if (home.startsWith("/")) return path.posix.join(home, "kaijibot");
	return path.join(home, "kaijibot");
}
function resolveNodeRunner() {
	const base = normalizeLowercaseStringOrEmpty(path.basename(process.execPath));
	if (base === "node" || base === "node.exe") return process.execPath;
	return "node";
}
async function resolveUpdateRoot() {
	return await resolveKaijiBotPackageRoot({
		moduleUrl: import.meta.url,
		argv1: process.argv[1],
		cwd: process.cwd()
	}) ?? process.cwd();
}
async function runUpdateStep(params) {
	const command = params.argv.join(" ");
	params.progress?.onStepStart?.({
		name: params.name,
		command,
		index: 0,
		total: 0
	});
	const started = Date.now();
	const res = await runCommandWithTimeout(params.argv, {
		cwd: params.cwd,
		env: params.env,
		timeoutMs: params.timeoutMs
	});
	const durationMs = Date.now() - started;
	const stderrTail = trimLogTail(res.stderr, MAX_LOG_CHARS);
	params.progress?.onStepComplete?.({
		name: params.name,
		command,
		index: 0,
		total: 0,
		durationMs,
		exitCode: res.code,
		stderrTail
	});
	return {
		name: params.name,
		command,
		cwd: params.cwd ?? process.cwd(),
		durationMs,
		exitCode: res.code,
		stdoutTail: trimLogTail(res.stdout, MAX_LOG_CHARS),
		stderrTail
	};
}
async function ensureGitCheckout(params) {
	const gitEnv = params.env ?? await createGlobalInstallEnv();
	if (!await pathExists(params.dir)) return await runUpdateStep({
		name: "git clone",
		argv: [
			"git",
			"clone",
			KAIJIBOT_REPO_URL,
			params.dir
		],
		env: gitEnv,
		timeoutMs: params.timeoutMs,
		progress: params.progress
	});
	if (!await isGitCheckout(params.dir)) {
		if (!await isEmptyDir(params.dir)) throw new Error(`KAIJIBOT_GIT_DIR points at a non-git directory: ${params.dir}. Set KAIJIBOT_GIT_DIR to an empty folder or an kaijibot checkout.`);
		return await runUpdateStep({
			name: "git clone",
			argv: [
				"git",
				"clone",
				KAIJIBOT_REPO_URL,
				params.dir
			],
			cwd: params.dir,
			env: gitEnv,
			timeoutMs: params.timeoutMs,
			progress: params.progress
		});
	}
	if (!await isCorePackage(params.dir)) throw new Error(`KAIJIBOT_GIT_DIR does not look like a core checkout: ${params.dir}.`);
	return null;
}
async function resolveGlobalManager(params) {
	const runCommand = createGlobalCommandRunner();
	if (params.installKind === "package") {
		const detected = await detectGlobalInstallManagerForRoot(runCommand, params.root, params.timeoutMs);
		if (detected) return detected;
	}
	return await detectGlobalInstallManagerByPresence(runCommand, params.timeoutMs) ?? "npm";
}
async function tryWriteCompletionCache(root, jsonMode) {
	const binPath = path.join(root, "kaijibot.mjs");
	if (!await pathExists(binPath)) return;
	const result = spawnSync(resolveNodeRunner(), [
		binPath,
		"completion",
		"--write-state"
	], {
		cwd: root,
		env: process.env,
		encoding: "utf-8"
	});
	if (result.error) {
		if (!jsonMode) defaultRuntime.log(theme.warn(`Completion cache update failed: ${String(result.error)}`));
		return;
	}
	if (result.status !== 0 && !jsonMode) {
		const stderr = (result.stderr ?? "").toString().trim();
		const detail = stderr ? ` (${stderr})` : "";
		defaultRuntime.log(theme.warn(`Completion cache update failed${detail}.`));
	}
}
function createGlobalCommandRunner() {
	return async (argv, options) => {
		const res = await runCommandWithTimeout(argv, options);
		return {
			stdout: res.stdout,
			stderr: res.stderr,
			code: res.code
		};
	};
}
//#endregion
//#region src/cli/update-cli/status.ts
function formatGitStatusLine(params) {
	const shortSha = params.sha ? params.sha.slice(0, 8) : null;
	const branch = params.branch && params.branch !== "HEAD" ? params.branch : null;
	const tag = params.tag;
	return [
		branch ?? (tag ? "detached" : "git"),
		tag ? `tag ${tag}` : null,
		shortSha ? `@ ${shortSha}` : null
	].filter(Boolean).join(" · ");
}
async function updateStatusCommand(opts) {
	const timeoutMs = parseTimeoutMsOrExit(opts.timeout);
	if (timeoutMs === null) return;
	const root = await resolveUpdateRoot();
	const configSnapshot = await readConfigFileSnapshot();
	const configChannel = configSnapshot.valid ? normalizeUpdateChannel(configSnapshot.config.update?.channel) : null;
	const update = await checkUpdateStatus({
		root,
		timeoutMs: timeoutMs ?? 3500,
		fetchGit: true,
		includeRegistry: true
	});
	const channelInfo = resolveUpdateChannelDisplay({
		configChannel,
		installKind: update.installKind,
		gitTag: update.git?.tag ?? null,
		gitBranch: update.git?.branch ?? null
	});
	const channelLabel = channelInfo.label;
	const gitLabel = update.installKind === "git" ? formatGitStatusLine({
		branch: update.git?.branch ?? null,
		tag: update.git?.tag ?? null,
		sha: update.git?.sha ?? null
	}) : null;
	const updateAvailability = resolveUpdateAvailability(update);
	const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
	if (opts.json) {
		defaultRuntime.writeJson({
			update,
			channel: {
				value: channelInfo.channel,
				source: channelInfo.source,
				label: channelLabel,
				config: configChannel
			},
			availability: updateAvailability
		});
		return;
	}
	const tableWidth = getTerminalTableWidth();
	const rows = [
		{
			Item: "Install",
			Value: update.installKind === "git" ? `git (${update.root ?? "unknown"})` : update.installKind === "package" ? update.packageManager : "unknown"
		},
		{
			Item: "Channel",
			Value: channelLabel
		},
		...gitLabel ? [{
			Item: "Git",
			Value: gitLabel
		}] : [],
		{
			Item: "Update",
			Value: updateAvailability.available ? theme.warn(`available · ${updateLine}`) : updateLine
		}
	];
	defaultRuntime.log(theme.heading("KaijiBot update status"));
	defaultRuntime.log("");
	defaultRuntime.log(renderTable({
		width: tableWidth,
		columns: [{
			key: "Item",
			header: "Item",
			minWidth: 10
		}, {
			key: "Value",
			header: "Value",
			flex: true,
			minWidth: 24
		}],
		rows
	}).trimEnd());
	defaultRuntime.log("");
	const updateHint = formatUpdateAvailableHint(update);
	if (updateHint) defaultRuntime.log(theme.warn(updateHint));
}
//#endregion
//#region src/cli/update-cli/progress.ts
const STEP_LABELS = {
	"clean check": "Working directory is clean",
	"upstream check": "Upstream branch exists",
	"git fetch": "Fetching latest changes",
	"git rebase": "Rebasing onto target commit",
	"git rev-parse @{upstream}": "Resolving upstream commit",
	"git rev-list": "Enumerating candidate commits",
	"git clone": "Cloning git checkout",
	"preflight worktree": "Preparing preflight worktree",
	"preflight cleanup": "Cleaning preflight worktree",
	"deps install": "Installing dependencies",
	build: "Building",
	"ui:build": "Building UI assets",
	"ui:build (post-doctor repair)": "Restoring missing UI assets",
	"ui assets verify": "Validating UI assets",
	"kaijibot doctor entry": "Checking doctor entrypoint",
	"kaijibot doctor": "Running doctor checks",
	"git rev-parse HEAD (after)": "Verifying update",
	"global update": "Updating via package manager",
	"global update (omit optional)": "Retrying update without optional deps",
	"global install": "Installing global package"
};
function getStepLabel(step) {
	return STEP_LABELS[step.name] ?? step.name;
}
function inferUpdateFailureHints(result) {
	if (result.status !== "error") return [];
	if (result.reason === "pnpm-corepack-missing") return ["This pnpm checkout could not auto-enable pnpm because corepack is missing.", "Install pnpm manually or install Node with corepack available, then rerun the update command."];
	if (result.reason === "pnpm-corepack-enable-failed") return ["This pnpm checkout could not auto-enable pnpm via corepack.", "Run `corepack enable` manually or install pnpm manually, then rerun the update command."];
	if (result.reason === "pnpm-npm-bootstrap-failed") return ["This pnpm checkout could not bootstrap pnpm from npm automatically.", "Install pnpm manually, then rerun the update command."];
	if (result.reason === "preferred-manager-unavailable") return ["This checkout requires its declared package manager and the updater could not find it.", "Install the missing package manager manually, then rerun the update command."];
	if (result.mode !== "npm") return [];
	const failedStep = [...result.steps].toReversed().find((step) => step.exitCode !== 0);
	if (!failedStep) return [];
	const stderr = normalizeLowercaseStringOrEmpty(failedStep.stderrTail);
	const hints = [];
	if (failedStep.name.startsWith("global update") && stderr.includes("eacces")) {
		hints.push("Detected permission failure (EACCES). Re-run with a writable global prefix or sudo (for system-managed Node installs).");
		hints.push("Example: npm config set prefix ~/.local && npm i -g kaijibot@latest");
	}
	if (failedStep.name.startsWith("global update") && (stderr.includes("node-gyp") || stderr.includes("prebuild"))) {
		hints.push("Detected native optional dependency build failure. The updater retries with --omit=optional automatically.");
		hints.push("If it still fails: npm i -g kaijibot@latest --omit=optional");
	}
	return hints;
}
function createUpdateProgress(enabled) {
	if (!enabled) return {
		progress: {},
		stop: () => {}
	};
	let currentSpinner = null;
	return {
		progress: {
			onStepStart: (step) => {
				currentSpinner = spinner();
				currentSpinner.start(theme.accent(getStepLabel(step)));
			},
			onStepComplete: (step) => {
				if (!currentSpinner) return;
				const label = getStepLabel(step);
				const duration = theme.muted(`(${formatDurationPrecise(step.durationMs)})`);
				const icon = step.exitCode === 0 ? theme.success("✓") : theme.error("✗");
				currentSpinner.stop(`${icon} ${label} ${duration}`);
				currentSpinner = null;
				if (step.exitCode !== 0 && step.stderrTail) {
					const lines = step.stderrTail.split("\n").slice(-10);
					for (const line of lines) if (line.trim()) defaultRuntime.log(`    ${theme.error(line)}`);
				}
			}
		},
		stop: () => {
			if (currentSpinner) {
				currentSpinner.stop();
				currentSpinner = null;
			}
		}
	};
}
function formatStepStatus(exitCode) {
	if (exitCode === 0) return theme.success("✓");
	if (exitCode === null) return theme.warn("?");
	return theme.error("✗");
}
function printResult(result, opts) {
	if (opts.json) {
		defaultRuntime.writeJson(result);
		return;
	}
	const statusColor = result.status === "ok" ? theme.success : result.status === "skipped" ? theme.warn : theme.error;
	defaultRuntime.log("");
	defaultRuntime.log(`${theme.heading("Update Result:")} ${statusColor(result.status.toUpperCase())}`);
	if (result.root) defaultRuntime.log(`  Root: ${theme.muted(result.root)}`);
	if (result.reason) defaultRuntime.log(`  Reason: ${theme.muted(result.reason)}`);
	if (result.before?.version || result.before?.sha) {
		const before = result.before.version ?? result.before.sha?.slice(0, 8) ?? "";
		defaultRuntime.log(`  Before: ${theme.muted(before)}`);
	}
	if (result.after?.version || result.after?.sha) {
		const after = result.after.version ?? result.after.sha?.slice(0, 8) ?? "";
		defaultRuntime.log(`  After: ${theme.muted(after)}`);
	}
	if (!opts.hideSteps && result.steps.length > 0) {
		defaultRuntime.log("");
		defaultRuntime.log(theme.heading("Steps:"));
		for (const step of result.steps) {
			const status = formatStepStatus(step.exitCode);
			const duration = theme.muted(`(${formatDurationPrecise(step.durationMs)})`);
			defaultRuntime.log(`  ${status} ${step.name} ${duration}`);
			if (step.exitCode !== 0 && step.stderrTail) {
				const lines = step.stderrTail.split("\n").slice(0, 5);
				for (const line of lines) if (line.trim()) defaultRuntime.log(`      ${theme.error(line)}`);
			}
		}
	}
	const hints = inferUpdateFailureHints(result);
	if (hints.length > 0) {
		defaultRuntime.log("");
		defaultRuntime.log(theme.heading("Recovery hints:"));
		for (const hint of hints) defaultRuntime.log(`  - ${theme.warn(hint)}`);
	}
	defaultRuntime.log("");
	defaultRuntime.log(`Total time: ${theme.muted(formatDurationPrecise(result.durationMs))}`);
}
//#endregion
//#region src/cli/update-cli/restart-helper.ts
/**
* Shell-escape a string for embedding in single-quoted shell arguments.
* Replaces every `'` with `'\''` (end quote, escaped quote, resume quote).
* For batch scripts, validates against special characters instead.
*/
function shellEscape(value) {
	return value.replace(/'/g, "'\\''");
}
/** Validates a task name is safe for embedding in Windows restart scripts. */
function isWindowsTaskNameSafe(value) {
	return /^[A-Za-z0-9 _\-().]+$/.test(value);
}
function powerShellSingleQuote(value) {
	return `'${value.replace(/'/g, "''")}'`;
}
function resolveSystemdUnit(env) {
	const override = normalizeOptionalString(env.KAIJIBOT_SYSTEMD_UNIT);
	if (override) return override.endsWith(".service") ? override : `${override}.service`;
	return `${resolveGatewaySystemdServiceName(env.KAIJIBOT_PROFILE)}.service`;
}
function resolveLaunchdLabel(env) {
	const override = normalizeOptionalString(env.KAIJIBOT_LAUNCHD_LABEL);
	if (override) return override;
	return resolveGatewayLaunchAgentLabel(env.KAIJIBOT_PROFILE);
}
function resolveWindowsTaskName(env) {
	const override = env.KAIJIBOT_WINDOWS_TASK_NAME?.trim();
	if (override) return override;
	return resolveGatewayWindowsTaskName(env.KAIJIBOT_PROFILE);
}
/**
* Prepares a standalone script to restart the gateway service.
* This script is written to a temporary directory and does not depend on
* the installed package files, ensuring restart capability even if the
* update process temporarily removes or corrupts installation files.
*/
async function prepareRestartScript(env = process.env, gatewayPort = DEFAULT_GATEWAY_PORT) {
	const timestamp = Date.now();
	const platform = process.platform;
	let scriptContent = "";
	let filename = "";
	try {
		if (platform === "linux") {
			const escaped = shellEscape(resolveSystemdUnit(env));
			const logSetup = renderPosixRestartLogSetup({
				...process.env,
				...env
			});
			filename = `kaijibot-restart-${timestamp}.sh`;
			scriptContent = `#!/bin/sh
# Standalone restart script — survives parent process termination.
# Wait briefly to ensure file locks are released after update.
sleep 1
exec 3>&2
${logSetup}
printf '[%s] kaijibot restart attempt source=update target=%s\\n' "$(date -u +%FT%TZ)" '${escaped}' >&2
if systemctl --user is-active --quiet '${escaped}' || systemctl --user is-enabled --quiet '${escaped}'; then
  if systemctl --user restart '${escaped}'; then
    status=0
    printf '[%s] kaijibot restart done source=update\\n' "$(date -u +%FT%TZ)" >&2
  else
    status=$?
    printf '[%s] kaijibot restart failed source=update status=%s\\n' "$(date -u +%FT%TZ)" "$status" >&2
  fi
elif systemctl is-active --quiet '${escaped}' || systemctl is-enabled --quiet '${escaped}'; then
  status=78
  printf '[%s] system-scoped kaijibot gateway unit detected; update cannot restart it without sudo. Run: sudo systemctl restart %s\\n' "$(date -u +%FT%TZ)" '${escaped}' >&2
  printf '[%s] system-scoped kaijibot gateway unit detected; update cannot restart it without sudo. Run: sudo systemctl restart %s\\n' "$(date -u +%FT%TZ)" '${escaped}' >&3 2>/dev/null || true
else
  if systemctl --user restart '${escaped}'; then
    status=0
    printf '[%s] kaijibot restart done source=update\\n' "$(date -u +%FT%TZ)" >&2
  else
    status=$?
    printf '[%s] kaijibot restart failed source=update status=%s\\n' "$(date -u +%FT%TZ)" "$status" >&2
  fi
fi
# Self-cleanup
script_dir=$(dirname "$0")
exec 3>&-
rm -f "$0"
rmdir "$script_dir" 2>/dev/null || true
exit "$status"
`;
		} else if (platform === "darwin") {
			const label = resolveLaunchdLabel(env);
			const escaped = shellEscape(label);
			const uid = process.getuid ? process.getuid() : 501;
			const home = normalizeOptionalString(env.HOME) || process.env.HOME || os.homedir();
			const escapedPlistPath = shellEscape(path.join(home, "Library", "LaunchAgents", `${label}.plist`));
			const logSetup = renderPosixRestartLogSetup({
				...process.env,
				...env
			});
			filename = `kaijibot-restart-${timestamp}.sh`;
			scriptContent = `#!/bin/sh
# Standalone restart script — survives parent process termination.
# Wait briefly to ensure file locks are released after update.
sleep 1
# Capture launchctl output so bootstrap/kickstart failures leave a durable
# audit trail. Log setup is best-effort: restart must still run if the log path
# is temporarily unavailable.
${logSetup}
printf '[%s] kaijibot restart attempt source=update target=%s\\n' "$(date -u +%FT%TZ)" '${shellEscapeRestartLogValue(label)}' >&2
# Try kickstart first (works when the service is still registered).
# If it fails (e.g. after bootout), clear any persisted disabled state,
# then re-register via bootstrap. Bootstrap loads RunAtLoad agents, so the
# fallback must not immediately kickstart -k the freshly spawned gateway.
# The final status is captured
# before self-cleanup so a genuine failure remains observable.
status=0
if ! launchctl kickstart -k 'gui/${uid}/${escaped}'; then
  launchctl enable 'gui/${uid}/${escaped}'
  if launchctl bootstrap 'gui/${uid}' '${escapedPlistPath}'; then
    status=0
  else
    launchctl kickstart -k 'gui/${uid}/${escaped}'
    status=$?
  fi
fi
if [ "$status" -eq 0 ]; then
  printf '[%s] kaijibot restart done source=update\\n' "$(date -u +%FT%TZ)" >&2
else
  printf '[%s] kaijibot restart failed source=update status=%s\\n' "$(date -u +%FT%TZ)" "$status" >&2
fi
# Self-cleanup (log is retained under the KaijiBot state logs directory).
script_dir=$(dirname "$0")
rm -f "$0"
rmdir "$script_dir" 2>/dev/null || true
exit "$status"
`;
		} else if (platform === "win32") {
			const taskName = resolveWindowsTaskName(env);
			if (!isWindowsTaskNameSafe(taskName)) return null;
			const port = Number.isFinite(gatewayPort) && gatewayPort > 0 ? gatewayPort : DEFAULT_GATEWAY_PORT;
			const quotedLogPath = powerShellSingleQuote(resolveGatewayRestartLogPath({
				...process.env,
				...env
			}));
			const quotedTaskName = powerShellSingleQuote(taskName);
			filename = `kaijibot-restart-${timestamp}.cmd`;
			scriptContent = `@echo off
REM Standalone restart script - survives parent process termination.
REM Keep this as a cmd wrapper so Group Policy script execution policies
REM cannot block the update restart handoff before schtasks.exe runs.
setlocal
set "KAIJIBOT_RESTART_SCRIPT=%~f0"
set "KAIJIBOT_RESTART_SCRIPT_DIR=%~dp0."
powershell -NoProfile -ExecutionPolicy Bypass -Command "$p=$env:KAIJIBOT_RESTART_SCRIPT; $s=Get-Content -Raw -LiteralPath $p; $m='# POWERSHELL'; $i=$s.IndexOf($m); if ($i -lt 0) { exit 1 }; Invoke-Expression $s.Substring($i)"
exit /b %ERRORLEVEL%
# POWERSHELL
# Wait briefly to ensure file locks are released after update.
$ErrorActionPreference = "Continue"
Start-Sleep -Seconds 2

$logPath = ${quotedLogPath}
try {
  $logDir = Split-Path -Parent $logPath
  New-Item -ItemType Directory -Path $logDir -Force | Out-Null
  Add-Content -LiteralPath $logPath -Value "[$(Get-Date -Format o)] kaijibot restart log initialized"
} catch {
  # Restart should still run if log setup is unavailable.
}

function Write-RestartLog {
  param([string]$Message)
  try {
    Add-Content -LiteralPath $logPath -Value "[$(Get-Date -Format o)] $Message"
  } catch {
  }
}

function Join-KaijiBotProcessArguments {
  param([string[]]$Arguments)
  ($Arguments | ForEach-Object {
    if ($_ -match "\\s") {
      '"' + $_ + '"'
    } else {
      $_
    }
  }) -join " "
}

function Invoke-KaijiBotSchtasksWithTimeout {
  param(
    [string[]]$Arguments,
    [int]$TimeoutSeconds
  )
  $process = $null
  try {
    $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
    $startInfo.FileName = "schtasks.exe"
    $startInfo.Arguments = Join-KaijiBotProcessArguments -Arguments $Arguments
    $startInfo.UseShellExecute = $false
    $startInfo.RedirectStandardOutput = $true
    $startInfo.RedirectStandardError = $true
    $process = [System.Diagnostics.Process]::Start($startInfo)
    if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
      try {
        $process.Kill()
      } catch {
      }
      Write-RestartLog "kaijibot restart schtasks timeout source=update args=$($Arguments -join ' ')"
      return 124
    }
    $stdout = $process.StandardOutput.ReadToEnd()
    $stderr = $process.StandardError.ReadToEnd()
    if ($stdout) {
      Write-RestartLog $stdout.Trim()
    }
    if ($stderr) {
      Write-RestartLog $stderr.Trim()
    }
    return $process.ExitCode
  } catch {
    Write-RestartLog "kaijibot restart schtasks failed source=update args=$($Arguments -join ' ') error=$($_.Exception.Message)"
    return 1
  }
}

function Get-KaijiBotScheduledTaskState {
  param([string]$TaskName)
  try {
    $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
    if ($task -and $task.State) {
      return [string]$task.State
    }
  } catch {
  }

  try {
    $queryOutput = & schtasks.exe /Query /TN $TaskName /FO LIST 2>$null
    foreach ($line in $queryOutput) {
      if ($line -match "^\\s*Status:\\s*(.+?)\\s*$") {
        return $Matches[1]
      }
    }
  } catch {
  }

  return "Unknown"
}

function Get-KaijiBotListenerPids {
  param([int]$Port)
  $listenerPids = @()

  try {
    if (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue) {
      $listenerPids += Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
        ForEach-Object { [int]$_.OwningProcess }
    }
  } catch {
  }

  if ($listenerPids.Count -eq 0) {
    try {
      $portPattern = [regex]::Escape(":$Port")
      $linePattern = "^\\s*TCP\\s+\\S+$portPattern\\s+\\S+\\s+LISTENING\\s+(\\d+)\\s*$"
      & netstat.exe -ano -p tcp 2>$null | ForEach-Object {
        if ($_ -match $linePattern) {
          $listenerPids += [int]$Matches[1]
        }
      }
    } catch {
    }
  }

  $listenerPids | Sort-Object -Unique
}

function Invoke-KaijiBotStartupLauncher {
  $launcherPath = Join-Path $env:USERPROFILE ".kaijibot\\gateway.cmd"
  if (-not (Test-Path -LiteralPath $launcherPath)) {
    Write-RestartLog "kaijibot restart startup launcher missing source=update path=$launcherPath"
    return 1
  }

  try {
    Start-Process -FilePath $launcherPath -WindowStyle Hidden | Out-Null
    Write-RestartLog "kaijibot restart launched startup fallback source=update path=$launcherPath"
    return 0
  } catch {
    Write-RestartLog "kaijibot restart startup fallback failed source=update error=$($_.Exception.Message)"
    return 1
  }
}

$taskName = ${quotedTaskName}
$port = ${port}
Write-RestartLog "kaijibot restart attempt source=update target=$taskName"

$taskState = Get-KaijiBotScheduledTaskState -TaskName $taskName
if ($taskState -eq "Running") {
  $endStatus = Invoke-KaijiBotSchtasksWithTimeout -Arguments @("/End", "/TN", $taskName) -TimeoutSeconds 10
  if ($endStatus -ne 0) {
    Write-RestartLog "kaijibot restart schtasks end did not complete cleanly source=update status=$endStatus"
  }
} else {
  Write-RestartLog "kaijibot restart skipped schtasks end source=update state=$taskState"
}

for ($attempt = 1; $attempt -le 10; $attempt++) {
  $listeners = @(Get-KaijiBotListenerPids -Port $port)
  if ($listeners.Count -eq 0) {
    break
  }

  if ($attempt -eq 10) {
    foreach ($listenerPid in $listeners) {
      try {
        Stop-Process -Id $listenerPid -Force -ErrorAction Stop
        Write-RestartLog "kaijibot restart killed stale listener source=update pid=$listenerPid"
      } catch {
        Write-RestartLog "kaijibot restart failed to kill stale listener source=update pid=$listenerPid error=$($_.Exception.Message)"
      }
    }
    break
  }

  Start-Sleep -Seconds 1
}

$status = Invoke-KaijiBotSchtasksWithTimeout -Arguments @("/Run", "/TN", $taskName) -TimeoutSeconds 30
if ($status -ne 0) {
  $status = Invoke-KaijiBotStartupLauncher
}
if ($status -eq 0) {
  Write-RestartLog "kaijibot restart done source=update"
} else {
  Write-RestartLog "kaijibot restart failed source=update status=$status"
}

# Self-cleanup: delete script file and temp directory from within PowerShell.
# Get-Content -Raw read the file into memory, so the file handle is closed
# and Remove-Item can safely delete the .cmd wrapper. Retry handles AV locks.
$scriptFile = $env:KAIJIBOT_RESTART_SCRIPT
$scriptDir = $env:KAIJIBOT_RESTART_SCRIPT_DIR
if ($scriptFile) {
  foreach ($attempt in 1..3) {
    try {
      if (Test-Path -LiteralPath $scriptFile) {
        Remove-Item -LiteralPath $scriptFile -Force -ErrorAction Stop
      }
      break
    } catch {
      if ($attempt -lt 3) { Start-Sleep -Milliseconds 500 }
    }
  }
}
if ($scriptDir -and (Test-Path -LiteralPath $scriptDir)) {
  try { Remove-Item -LiteralPath $scriptDir -Force -Recurse } catch {}
}

exit $status
`;
		} else return null;
		const scriptDir = await fs.mkdtemp(path.join(os.tmpdir(), "kaijibot-restart-"));
		const scriptPath = path.join(scriptDir, filename);
		try {
			await fs.writeFile(scriptPath, scriptContent, {
				mode: 493,
				flag: "wx"
			});
		} catch (error) {
			await fs.rm(scriptDir, {
				recursive: true,
				force: true
			}).catch(() => {});
			throw error;
		}
		return scriptPath;
	} catch {
		return null;
	}
}
/**
* Executes the prepared restart script as a **detached** process.
*
* The script must outlive the CLI process because the CLI itself is part
* of the service being restarted — `systemctl restart` / `launchctl
* kickstart -k` will terminate the current process tree.  Using
* `spawn({ detached: true })` + `unref()` ensures the script survives
* the parent's exit.
*
* Resolves immediately after spawning; the script runs independently.
*/
async function runRestartScript(scriptPath) {
	const isWindows = process.platform === "win32";
	const file = isWindows ? "cmd.exe" : "/bin/sh";
	const args = isWindows ? [
		"/d",
		"/s",
		"/c",
		quoteCmdScriptArg(scriptPath)
	] : [scriptPath];
	try {
		const child = spawn(file, args, {
			detached: true,
			stdio: "ignore",
			windowsHide: true
		});
		child.on("error", () => {});
		child.unref();
	} catch {}
}
//#endregion
//#region src/cli/update-cli/suppress-deprecations.ts
/**
* Suppress Node.js deprecation warnings.
*
* On Node.js v23+ `process.noDeprecation` may be a read-only property
* (defined via a getter on the prototype with no setter), so the
* assignment can throw. We fall back to the environment variable which
* achieves the same effect.
*/
function suppressDeprecations() {
	try {
		process.noDeprecation = true;
	} catch {}
	process.env.NODE_NO_WARNINGS = "1";
}
//#endregion
//#region src/cli/update-cli/update-command.ts
const CLI_NAME = resolveCliName();
const SERVICE_REFRESH_TIMEOUT_MS = 6e4;
const SERVICE_REFRESH_PATH_ENV_KEYS = [
	"KAIJIBOT_HOME",
	"KAIJIBOT_STATE_DIR",
	"KAIJIBOT_CONFIG_PATH"
];
const UPDATE_QUIPS = [
	"代码已更新，认知再升级——想我了吗？",
	"旧版本已蜕壳，新思维更锐利。",
	"修复完毕，认知引擎重新上线。出发！",
	"从 npm 的热汤中重生，更强了。",
	"更新完成。认知模型已刷新，直觉更准了。",
	"新版本已就位——你没注意到我偷偷变聪明了吧？",
	"升级成功。Bug 都跑了，因为我太强了。",
	"版本号小了一步，能力大了一截。",
	"更新完毕。你的 AI 伙伴又进化了一点点。",
	"旧版本说再见，新版本说你好。认知引擎持续运转中。",
	"完成。顺便整理了一下思维路径，现在更清晰了。",
	"更新好了。未来的你会感谢现在升级的你。",
	"新版本来了——少了一些 Bug，多了一些智慧。",
	"升级完成。就像给你的第二大脑做了个系统更新。",
	"更新完毕。每次升级都是一次认知校准。",
	"代码已刷新，思维已重启。我们继续。"
];
function pickUpdateQuip() {
	return UPDATE_QUIPS[Math.floor(Math.random() * UPDATE_QUIPS.length)] ?? "Update complete.";
}
function resolveGatewayInstallEntrypointCandidates(root) {
	if (!root) return [];
	return [
		path.join(root, "dist", "entry.js"),
		path.join(root, "dist", "entry.mjs"),
		path.join(root, "dist", "index.js"),
		path.join(root, "dist", "index.mjs")
	];
}
function formatCommandFailure(stdout, stderr) {
	const detail = (stderr || stdout).trim();
	if (!detail) return "command returned a non-zero exit code";
	return detail.split("\n").slice(-3).join("\n");
}
function tryResolveInvocationCwd() {
	try {
		return process.cwd();
	} catch {
		return;
	}
}
async function resolvePackageRuntimePreflightError(params) {
	if (!canResolveRegistryVersionForPackageTarget(params.tag)) return null;
	const target = params.tag.trim();
	if (!target) return null;
	const status = await fetchNpmPackageTargetStatus({
		target,
		timeoutMs: params.timeoutMs
	});
	if (status.error) return null;
	if (nodeVersionSatisfiesEngine(process.versions.node ?? null, status.nodeEngine) !== false) return null;
	const targetLabel = status.version ?? target;
	return [
		`Node ${process.versions.node ?? "unknown"} is too old for kaijibot@${targetLabel}.`,
		`The requested package requires ${status.nodeEngine}.`,
		"Upgrade Node to 22.14+ or Node 24, then rerun `kaijibot update`.",
		"Bare `npm i -g kaijibot` can silently install an older compatible release.",
		"After upgrading Node, use `npm i -g kaijibot@latest`."
	].join("\n");
}
function resolveServiceRefreshEnv(env, invocationCwd) {
	const resolvedEnv = { ...env };
	for (const key of SERVICE_REFRESH_PATH_ENV_KEYS) {
		const rawValue = resolvedEnv[key]?.trim();
		if (!rawValue) continue;
		if (rawValue.startsWith("~") || path.isAbsolute(rawValue) || path.win32.isAbsolute(rawValue)) {
			resolvedEnv[key] = rawValue;
			continue;
		}
		if (!invocationCwd) {
			resolvedEnv[key] = rawValue;
			continue;
		}
		resolvedEnv[key] = path.resolve(invocationCwd, rawValue);
	}
	return resolvedEnv;
}
function printDryRunPreview(preview, jsonMode) {
	if (jsonMode) {
		defaultRuntime.writeJson(preview);
		return;
	}
	defaultRuntime.log(theme.heading("Update dry-run"));
	defaultRuntime.log(theme.muted("No changes were applied."));
	defaultRuntime.log("");
	defaultRuntime.log(`  Root: ${theme.muted(preview.root)}`);
	defaultRuntime.log(`  Install kind: ${theme.muted(preview.installKind)}`);
	defaultRuntime.log(`  Mode: ${theme.muted(preview.mode)}`);
	defaultRuntime.log(`  Channel: ${theme.muted(preview.effectiveChannel)}`);
	defaultRuntime.log(`  Tag/spec: ${theme.muted(preview.tag)}`);
	if (preview.currentVersion) defaultRuntime.log(`  Current version: ${theme.muted(preview.currentVersion)}`);
	if (preview.targetVersion) defaultRuntime.log(`  Target version: ${theme.muted(preview.targetVersion)}`);
	if (preview.downgradeRisk) defaultRuntime.log(theme.warn("  Downgrade confirmation would be required in a real run."));
	defaultRuntime.log("");
	defaultRuntime.log(theme.heading("Planned actions:"));
	for (const action of preview.actions) defaultRuntime.log(`  - ${action}`);
	if (preview.notes.length > 0) {
		defaultRuntime.log("");
		defaultRuntime.log(theme.heading("Notes:"));
		for (const note of preview.notes) defaultRuntime.log(`  - ${theme.muted(note)}`);
	}
}
async function refreshGatewayServiceEnv(params) {
	const args = [
		"gateway",
		"install",
		"--force"
	];
	if (params.jsonMode) args.push("--json");
	for (const candidate of resolveGatewayInstallEntrypointCandidates(params.result.root)) {
		if (!await pathExists(candidate)) continue;
		const res = await runCommandWithTimeout([
			resolveNodeRunner(),
			candidate,
			...args
		], {
			cwd: params.result.root,
			env: resolveServiceRefreshEnv(process.env, params.invocationCwd),
			timeoutMs: SERVICE_REFRESH_TIMEOUT_MS
		});
		if (res.code === 0) return;
		throw new Error(`updated install refresh failed (${candidate}): ${formatCommandFailure(res.stdout, res.stderr)}`);
	}
	await runDaemonInstall({
		force: true,
		json: params.jsonMode || void 0
	});
}
async function tryInstallShellCompletion(opts) {
	if (opts.jsonMode || !process.stdin.isTTY) return;
	const status = await checkShellCompletionStatus(CLI_NAME);
	if (status.usesSlowPattern) {
		defaultRuntime.log(theme.muted("Upgrading shell completion to cached version..."));
		if (await ensureCompletionCacheExists(CLI_NAME)) await installCompletion(status.shell, true, CLI_NAME);
		return;
	}
	if (status.profileInstalled && !status.cacheExists) {
		defaultRuntime.log(theme.muted("Regenerating shell completion cache..."));
		await ensureCompletionCacheExists(CLI_NAME);
		return;
	}
	if (!status.profileInstalled) {
		defaultRuntime.log("");
		defaultRuntime.log(theme.heading("Shell completion"));
		const shouldInstall = await confirm({
			message: stylePromptMessage(`Enable ${status.shell} shell completion for ${CLI_NAME}?`),
			initialValue: true
		});
		if (isCancel(shouldInstall) || !shouldInstall) {
			if (!opts.skipPrompt) defaultRuntime.log(theme.muted(`Skipped. Run \`${replaceCliName(formatCliCommand("kaijibot completion --install"), CLI_NAME)}\` later to enable.`));
			return;
		}
		if (!await ensureCompletionCacheExists(CLI_NAME)) {
			defaultRuntime.log(theme.warn("Failed to generate completion cache."));
			return;
		}
		await installCompletion(status.shell, opts.skipPrompt, CLI_NAME);
	}
}
async function runPackageInstallUpdate(params) {
	const manager = await resolveGlobalManager({
		root: params.root,
		installKind: params.installKind,
		timeoutMs: params.timeoutMs
	});
	const installEnv = await createGlobalInstallEnv();
	const runCommand = createGlobalCommandRunner();
	const installTarget = await resolveGlobalInstallTarget({
		manager,
		runCommand,
		timeoutMs: params.timeoutMs,
		pkgRoot: params.root
	});
	const pkgRoot = installTarget.packageRoot;
	const packageName = (pkgRoot ? await readPackageName(pkgRoot) : await readPackageName(params.root)) ?? "kaijibot";
	const installSpec = resolveGlobalInstallSpec({
		packageName,
		tag: params.tag,
		env: installEnv
	});
	const beforeVersion = pkgRoot ? await readPackageVersion(pkgRoot) : null;
	if (pkgRoot) await cleanupGlobalRenameDirs({
		globalRoot: path.dirname(pkgRoot),
		packageName
	});
	const steps = [await runUpdateStep({
		name: "global update",
		argv: globalInstallArgs(installTarget, installSpec),
		env: installEnv,
		timeoutMs: params.timeoutMs,
		progress: params.progress
	})];
	let afterVersion = beforeVersion;
	const verifiedPackageRoot = (await resolveGlobalInstallTarget({
		manager: installTarget,
		runCommand,
		timeoutMs: params.timeoutMs
	})).packageRoot ?? pkgRoot;
	if (verifiedPackageRoot) {
		afterVersion = await readPackageVersion(verifiedPackageRoot);
		const verificationErrors = await collectInstalledGlobalPackageErrors({
			packageRoot: verifiedPackageRoot,
			expectedVersion: resolveExpectedInstalledVersionFromSpec(packageName, installSpec)
		});
		if (verificationErrors.length > 0) steps.push({
			name: "global install verify",
			command: `verify ${verifiedPackageRoot}`,
			cwd: verifiedPackageRoot,
			durationMs: 0,
			exitCode: 1,
			stderrTail: verificationErrors.join("\n"),
			stdoutTail: null
		});
		const entryPath = path.join(verifiedPackageRoot, "dist", "entry.js");
		if (await pathExists(entryPath)) {
			const doctorStep = await runUpdateStep({
				name: `${CLI_NAME} doctor`,
				argv: [
					resolveNodeRunner(),
					entryPath,
					"doctor",
					"--non-interactive"
				],
				timeoutMs: params.timeoutMs,
				progress: params.progress
			});
			steps.push(doctorStep);
		}
	}
	const failedStep = steps.find((step) => step.exitCode !== 0);
	return {
		status: failedStep ? "error" : "ok",
		mode: manager,
		root: verifiedPackageRoot ?? params.root,
		reason: failedStep ? failedStep.name : void 0,
		before: { version: beforeVersion },
		after: { version: afterVersion },
		steps,
		durationMs: Date.now() - params.startedAt
	};
}
async function runGitUpdate(params) {
	const updateRoot = params.switchToGit ? resolveGitInstallDir() : params.root;
	const effectiveTimeout = params.timeoutMs ?? 20 * 6e4;
	const installEnv = await createGlobalInstallEnv();
	const cloneStep = params.switchToGit ? await ensureGitCheckout({
		dir: updateRoot,
		env: installEnv,
		timeoutMs: effectiveTimeout,
		progress: params.progress
	}) : null;
	if (cloneStep && cloneStep.exitCode !== 0) {
		const result = {
			status: "error",
			mode: "git",
			root: updateRoot,
			reason: cloneStep.name,
			steps: [cloneStep],
			durationMs: Date.now() - params.startedAt
		};
		params.stop();
		printResult(result, {
			...params.opts,
			hideSteps: params.showProgress
		});
		defaultRuntime.exit(1);
		return result;
	}
	const updateResult = await runGatewayUpdate({
		cwd: updateRoot,
		argv1: params.switchToGit ? void 0 : process.argv[1],
		timeoutMs: params.timeoutMs,
		progress: params.progress,
		channel: params.channel,
		tag: params.tag
	});
	const steps = [...cloneStep ? [cloneStep] : [], ...updateResult.steps];
	if (params.switchToGit && updateResult.status === "ok") {
		const installStep = await runUpdateStep({
			name: "global install",
			argv: globalInstallArgs(await resolveGlobalInstallTarget({
				manager: await resolveGlobalManager({
					root: params.root,
					installKind: params.installKind,
					timeoutMs: effectiveTimeout
				}),
				runCommand: createGlobalCommandRunner(),
				timeoutMs: effectiveTimeout,
				pkgRoot: params.root
			}), updateRoot),
			cwd: updateRoot,
			env: installEnv,
			timeoutMs: effectiveTimeout,
			progress: params.progress
		});
		steps.push(installStep);
		const failedStep = installStep.exitCode !== 0 ? installStep : null;
		return {
			...updateResult,
			status: updateResult.status === "ok" && !failedStep ? "ok" : "error",
			steps,
			durationMs: Date.now() - params.startedAt
		};
	}
	return {
		...updateResult,
		steps,
		durationMs: Date.now() - params.startedAt
	};
}
async function updatePluginsAfterCoreUpdate(params) {
	if (!params.configSnapshot.valid) {
		if (!params.opts.json) defaultRuntime.log(theme.warn("Skipping plugin updates: config is invalid."));
		return;
	}
	const pluginLogger = params.opts.json ? {} : {
		info: (msg) => defaultRuntime.log(msg),
		warn: (msg) => defaultRuntime.log(theme.warn(msg)),
		error: (msg) => defaultRuntime.log(theme.error(msg))
	};
	if (!params.opts.json) {
		defaultRuntime.log("");
		defaultRuntime.log(theme.heading("Updating plugins..."));
	}
	const syncResult = await syncPluginsForUpdateChannel({
		config: params.configSnapshot.config,
		channel: params.channel,
		workspaceDir: params.root,
		logger: pluginLogger
	});
	let pluginConfig = syncResult.config;
	const npmResult = await updateNpmInstalledPlugins({
		config: pluginConfig,
		skipIds: new Set(syncResult.summary.switchedToNpm),
		logger: pluginLogger
	});
	pluginConfig = npmResult.config;
	if (syncResult.changed || npmResult.changed) await replaceConfigFile({
		nextConfig: pluginConfig,
		baseHash: params.configSnapshot.hash
	});
	if (params.opts.json) return;
	const summarizeList = (list) => {
		if (list.length <= 6) return list.join(", ");
		return `${list.slice(0, 6).join(", ")} +${list.length - 6} more`;
	};
	if (syncResult.summary.switchedToBundled.length > 0) defaultRuntime.log(theme.muted(`Switched to bundled plugins: ${summarizeList(syncResult.summary.switchedToBundled)}.`));
	if (syncResult.summary.switchedToNpm.length > 0) defaultRuntime.log(theme.muted(`Restored npm plugins: ${summarizeList(syncResult.summary.switchedToNpm)}.`));
	for (const warning of syncResult.summary.warnings) defaultRuntime.log(theme.warn(warning));
	for (const error of syncResult.summary.errors) defaultRuntime.log(theme.error(error));
	const updated = npmResult.outcomes.filter((entry) => entry.status === "updated").length;
	const unchanged = npmResult.outcomes.filter((entry) => entry.status === "unchanged").length;
	const failed = npmResult.outcomes.filter((entry) => entry.status === "error").length;
	const skipped = npmResult.outcomes.filter((entry) => entry.status === "skipped").length;
	if (npmResult.outcomes.length === 0) defaultRuntime.log(theme.muted("No plugin updates needed."));
	else {
		const parts = [`${updated} updated`, `${unchanged} unchanged`];
		if (failed > 0) parts.push(`${failed} failed`);
		if (skipped > 0) parts.push(`${skipped} skipped`);
		defaultRuntime.log(theme.muted(`npm plugins: ${parts.join(", ")}.`));
	}
	for (const outcome of npmResult.outcomes) {
		if (outcome.status !== "error") continue;
		defaultRuntime.log(theme.error(outcome.message));
	}
}
async function maybeRestartService(params) {
	if (params.shouldRestart) {
		if (!params.opts.json) {
			defaultRuntime.log("");
			defaultRuntime.log(theme.heading("Restarting service..."));
		}
		try {
			let restarted = false;
			let restartInitiated = false;
			if (params.refreshServiceEnv) try {
				await refreshGatewayServiceEnv({
					result: params.result,
					jsonMode: Boolean(params.opts.json),
					invocationCwd: params.invocationCwd
				});
			} catch (err) {
				const message = `Failed to refresh gateway service environment from updated install: ${String(err)}`;
				if (params.opts.json) defaultRuntime.error(message);
				else defaultRuntime.log(theme.warn(message));
			}
			if (params.restartScriptPath) {
				writeGatewayRestartHandoffSync({
					restartKind: "full-process",
					reason: "update.run",
					source: "gateway-update"
				});
				try {
					await runRestartScript(params.restartScriptPath);
				} catch {}
				restartInitiated = true;
			} else restarted = await runDaemonRestart();
			if (!params.opts.json && restarted) {
				defaultRuntime.log(theme.success("Daemon restarted successfully."));
				defaultRuntime.log("");
				process.env.KAIJIBOT_UPDATE_IN_PROGRESS = "1";
				try {
					await doctorCommand(defaultRuntime, { nonInteractive: !(Boolean(process.stdin.isTTY) && !params.opts.json && params.opts.yes !== true) });
				} catch (err) {
					defaultRuntime.log(theme.warn(`Doctor failed: ${String(err)}`));
				} finally {
					delete process.env.KAIJIBOT_UPDATE_IN_PROGRESS;
				}
			}
			if (!params.opts.json && restartInitiated) {
				const service = resolveGatewayService();
				let health = await waitForGatewayHealthyRestart({
					service,
					port: params.gatewayPort
				});
				if (!health.healthy && health.staleGatewayPids.length > 0) {
					if (!params.opts.json) defaultRuntime.log(theme.warn(`Found stale gateway process(es) after restart: ${health.staleGatewayPids.join(", ")}. Cleaning up...`));
					await terminateStaleGatewayPids(health.staleGatewayPids);
					await runDaemonRestart();
					health = await waitForGatewayHealthyRestart({
						service,
						port: params.gatewayPort
					});
				}
				if (health.healthy) defaultRuntime.log(theme.success("Daemon restart completed."));
				else {
					defaultRuntime.log(theme.warn("Gateway did not become healthy after restart."));
					for (const line of renderRestartDiagnostics(health)) defaultRuntime.log(theme.muted(line));
					defaultRuntime.log(theme.muted(`Run \`${replaceCliName(formatCliCommand("kaijibot gateway status --deep"), CLI_NAME)}\` for details.`));
					defaultRuntime.log(theme.muted(`Restart log: ${resolveGatewayRestartLogPath(process.env)}`));
				}
				defaultRuntime.log("");
			}
		} catch (err) {
			if (!params.opts.json) {
				defaultRuntime.log(theme.warn(`Daemon restart failed: ${String(err)}`));
				defaultRuntime.log(theme.muted(`You may need to restart the service manually: ${replaceCliName(formatCliCommand("kaijibot gateway restart"), CLI_NAME)}`));
			}
		}
		return;
	}
	if (!params.opts.json) {
		defaultRuntime.log("");
		if (params.result.mode === "npm" || params.result.mode === "pnpm") defaultRuntime.log(theme.muted(`Tip: Run \`${replaceCliName(formatCliCommand("kaijibot doctor"), CLI_NAME)}\`, then \`${replaceCliName(formatCliCommand("kaijibot gateway restart"), CLI_NAME)}\` to apply updates to a running gateway.`));
		else defaultRuntime.log(theme.muted(`Tip: Run \`${replaceCliName(formatCliCommand("kaijibot gateway restart"), CLI_NAME)}\` to apply updates to a running gateway.`));
	}
}
async function updateCommand(opts) {
	suppressDeprecations();
	const invocationCwd = tryResolveInvocationCwd();
	const timeoutMs = parseTimeoutMsOrExit(opts.timeout);
	const shouldRestart = opts.restart !== false;
	if (timeoutMs === null) return;
	const root = await resolveUpdateRoot();
	const updateStatus = await checkUpdateStatus({
		root,
		timeoutMs: timeoutMs ?? 3500,
		fetchGit: false,
		includeRegistry: false
	});
	const configSnapshot = await readConfigFileSnapshot();
	const storedChannel = configSnapshot.valid ? normalizeUpdateChannel(configSnapshot.config.update?.channel) : null;
	const requestedChannel = normalizeUpdateChannel(opts.channel);
	if (opts.channel && !requestedChannel) {
		defaultRuntime.error(`--channel must be "stable", "beta", or "dev" (got "${opts.channel}")`);
		defaultRuntime.exit(1);
		return;
	}
	if (opts.channel && !configSnapshot.valid) {
		const issues = formatConfigIssueLines(configSnapshot.issues, "-");
		defaultRuntime.error(["Config is invalid; cannot set update channel.", ...issues].join("\n"));
		defaultRuntime.exit(1);
		return;
	}
	const installKind = updateStatus.installKind;
	const switchToGit = requestedChannel === "dev" && installKind !== "git";
	const switchToPackage = requestedChannel !== null && requestedChannel !== "dev" && installKind === "git";
	const updateInstallKind = switchToGit ? "git" : switchToPackage ? "package" : installKind;
	const channel = requestedChannel ?? storedChannel ?? (updateInstallKind === "git" ? "dev" : "stable");
	const explicitTag = normalizeTag(opts.tag);
	let tag = explicitTag ?? channelToNpmTag(channel);
	let currentVersion = null;
	let targetVersion = null;
	let downgradeRisk = false;
	let fallbackToLatest = false;
	let packageInstallSpec = null;
	if (updateInstallKind !== "git") {
		currentVersion = switchToPackage ? null : await readPackageVersion(root);
		if (explicitTag) targetVersion = await resolveTargetVersion(tag, timeoutMs);
		else targetVersion = await resolveNpmChannelTag({
			channel,
			timeoutMs
		}).then((resolved) => {
			tag = resolved.tag;
			fallbackToLatest = channel === "beta" && resolved.tag === "latest";
			return resolved.version;
		});
		const cmp = currentVersion && targetVersion ? compareSemverStrings(currentVersion, targetVersion) : null;
		downgradeRisk = canResolveRegistryVersionForPackageTarget(tag) && !fallbackToLatest && currentVersion != null && (targetVersion == null || cmp != null && cmp > 0);
		packageInstallSpec = resolveGlobalInstallSpec({
			packageName: DEFAULT_PACKAGE_NAME,
			tag,
			env: process.env
		});
	}
	if (opts.dryRun) {
		let mode = "unknown";
		if (updateInstallKind === "git") mode = "git";
		else if (updateInstallKind === "package") mode = await resolveGlobalManager({
			root,
			installKind,
			timeoutMs: timeoutMs ?? 20 * 6e4
		});
		const actions = [];
		if (requestedChannel && requestedChannel !== storedChannel) actions.push(`Persist update.channel=${requestedChannel} in config`);
		if (switchToGit) actions.push("Switch install mode from package to git checkout (dev channel)");
		else if (switchToPackage) actions.push(`Switch install mode from git to package manager (${mode})`);
		else if (updateInstallKind === "git") actions.push(`Run git update flow on channel ${channel} (fetch/rebase/build/doctor)`);
		else actions.push(`Run global package manager update with spec ${packageInstallSpec ?? tag}`);
		actions.push("Run plugin update sync after core update");
		actions.push("Refresh shell completion cache (if needed)");
		actions.push(shouldRestart ? "Restart gateway service and run doctor checks" : "Skip restart (because --no-restart is set)");
		const notes = [];
		if (opts.tag && updateInstallKind === "git") notes.push("--tag applies to npm installs only; git updates ignore it.");
		if (fallbackToLatest) notes.push("Beta channel resolves to latest for this run (fallback).");
		if (explicitTag && !canResolveRegistryVersionForPackageTarget(tag)) notes.push("Non-registry package specs skip npm version lookup and downgrade previews.");
		printDryRunPreview({
			dryRun: true,
			root,
			installKind,
			mode,
			updateInstallKind,
			switchToGit,
			switchToPackage,
			restart: shouldRestart,
			requestedChannel,
			storedChannel,
			effectiveChannel: channel,
			tag: packageInstallSpec ?? tag,
			currentVersion,
			targetVersion,
			downgradeRisk,
			actions,
			notes
		}, Boolean(opts.json));
		return;
	}
	if (downgradeRisk && !opts.yes) {
		if (!process.stdin.isTTY || opts.json) {
			defaultRuntime.error(["Downgrade confirmation required.", "Downgrading can break configuration. Re-run in a TTY to confirm."].join("\n"));
			defaultRuntime.exit(1);
			return;
		}
		const targetLabel = targetVersion ?? `${tag} (unknown)`;
		const ok = await confirm({
			message: stylePromptMessage(`Downgrading from ${currentVersion} to ${targetLabel} can break configuration. Continue?`),
			initialValue: false
		});
		if (isCancel(ok) || !ok) {
			if (!opts.json) defaultRuntime.log(theme.muted("Update cancelled."));
			defaultRuntime.exit(0);
			return;
		}
	}
	if (updateInstallKind === "git" && opts.tag && !opts.json) defaultRuntime.log(theme.muted("Note: --tag applies to npm installs only; git updates ignore it."));
	if (updateInstallKind === "package") {
		const runtimePreflightError = await resolvePackageRuntimePreflightError({
			tag,
			timeoutMs
		});
		if (runtimePreflightError) {
			defaultRuntime.error(runtimePreflightError);
			defaultRuntime.exit(1);
			return;
		}
	}
	const showProgress = !opts.json && process.stdout.isTTY;
	if (!opts.json) {
		defaultRuntime.log(theme.heading("Updating KaijiBot..."));
		defaultRuntime.log("");
	}
	const { progress, stop } = createUpdateProgress(showProgress);
	const startedAt = Date.now();
	let restartScriptPath = null;
	let refreshGatewayServiceEnv = false;
	const gatewayPort = resolveGatewayPort(configSnapshot.valid ? configSnapshot.config : void 0, process.env);
	if (shouldRestart) try {
		if (await resolveGatewayService().isLoaded({ env: process.env })) {
			restartScriptPath = await prepareRestartScript(process.env, gatewayPort);
			refreshGatewayServiceEnv = true;
		}
	} catch {}
	const result = updateInstallKind === "package" ? await runPackageInstallUpdate({
		root,
		installKind,
		tag,
		timeoutMs: timeoutMs ?? 20 * 6e4,
		startedAt,
		progress
	}) : await runGitUpdate({
		root,
		switchToGit,
		installKind,
		timeoutMs,
		startedAt,
		progress,
		channel,
		tag,
		showProgress,
		opts,
		stop
	});
	stop();
	printResult(result, {
		...opts,
		hideSteps: showProgress
	});
	if (result.status === "error") {
		defaultRuntime.exit(1);
		return;
	}
	if (result.status === "skipped") {
		if (result.reason === "dirty") {
			defaultRuntime.error(theme.error("Update blocked: local files are edited in this checkout."));
			defaultRuntime.log(theme.warn("Git-based updates need a clean working tree before they can switch commits, fetch, or rebase."));
			defaultRuntime.log(theme.muted("Commit, stash, or discard the local changes, then rerun `kaijibot update`."));
		}
		if (result.reason === "not-git-install") {
			defaultRuntime.log(theme.warn(`Skipped: this KaijiBot install isn't a git checkout, and the package manager couldn't be detected. Update via your package manager, then run \`${replaceCliName(formatCliCommand("kaijibot doctor"), CLI_NAME)}\` and \`${replaceCliName(formatCliCommand("kaijibot gateway restart"), CLI_NAME)}\`.`));
			defaultRuntime.log(theme.muted(`Examples: \`${replaceCliName("npm i -g kaijibot@latest", CLI_NAME)}\` or \`${replaceCliName("pnpm add -g kaijibot@latest", CLI_NAME)}\``));
		}
		defaultRuntime.exit(0);
		return;
	}
	if (switchToGit && result.status === "ok" && result.mode === "git") {
		if (!opts.json) defaultRuntime.log(theme.muted("Switched from a package install to a git checkout. Skipping remaining post-update work in the old CLI process; rerun follow-up commands from the new git install if needed."));
		defaultRuntime.exit(0);
		return;
	}
	let postUpdateConfigSnapshot = configSnapshot;
	if (requestedChannel && configSnapshot.valid && requestedChannel !== storedChannel) if (switchToGit) {
		if (!opts.json) defaultRuntime.log(theme.muted(`Skipped persisting update.channel=${requestedChannel} in the pre-update CLI process after switching to a git install.`));
	} else {
		const next = {
			...configSnapshot.config,
			update: {
				...configSnapshot.config.update,
				channel: requestedChannel
			}
		};
		await replaceConfigFile({
			nextConfig: next,
			baseHash: configSnapshot.hash
		});
		postUpdateConfigSnapshot = {
			...configSnapshot,
			hash: void 0,
			parsed: next,
			sourceConfig: asResolvedSourceConfig(next),
			resolved: asResolvedSourceConfig(next),
			runtimeConfig: asRuntimeConfig(next),
			config: asRuntimeConfig(next)
		};
		if (!opts.json) defaultRuntime.log(theme.muted(`Update channel set to ${requestedChannel}.`));
	}
	const postUpdateRoot = result.root ?? root;
	const deferOldProcessPostUpdateWork = switchToGit && result.mode === "git";
	if (deferOldProcessPostUpdateWork) {
		if (!opts.json) defaultRuntime.log(theme.muted("Skipped plugin update sync in the pre-update CLI process after switching to a git install."));
	} else await updatePluginsAfterCoreUpdate({
		root: postUpdateRoot,
		channel,
		configSnapshot: postUpdateConfigSnapshot,
		opts
	});
	if (deferOldProcessPostUpdateWork) {
		if (!opts.json) defaultRuntime.log(theme.muted("Skipped completion/restart follow-ups in the pre-update CLI process after switching to a git install."));
	} else {
		await tryWriteCompletionCache(postUpdateRoot, Boolean(opts.json));
		await tryInstallShellCompletion({
			jsonMode: Boolean(opts.json),
			skipPrompt: Boolean(opts.yes)
		});
		await maybeRestartService({
			shouldRestart,
			result,
			opts,
			refreshServiceEnv: refreshGatewayServiceEnv,
			gatewayPort,
			restartScriptPath,
			invocationCwd
		});
	}
	if (!opts.json) defaultRuntime.log(theme.muted(pickUpdateQuip()));
}
//#endregion
//#region src/cli/update-cli/wizard.ts
async function updateWizardCommand(opts = {}) {
	if (!process.stdin.isTTY) {
		defaultRuntime.error("Update wizard requires a TTY. Use `kaijibot update --channel <stable|beta|dev>` instead.");
		defaultRuntime.exit(1);
		return;
	}
	const timeoutMs = parseTimeoutMsOrExit(opts.timeout);
	if (timeoutMs === null) return;
	const root = await resolveUpdateRoot();
	const [updateStatus, configSnapshot] = await Promise.all([checkUpdateStatus({
		root,
		timeoutMs: timeoutMs ?? 3500,
		fetchGit: false,
		includeRegistry: false
	}), readConfigFileSnapshot()]);
	const channelInfo = resolveEffectiveUpdateChannel({
		configChannel: configSnapshot.valid ? normalizeUpdateChannel(configSnapshot.config.update?.channel) : null,
		installKind: updateStatus.installKind,
		git: updateStatus.git ? {
			tag: updateStatus.git.tag,
			branch: updateStatus.git.branch
		} : void 0
	});
	const channelLabel = formatUpdateChannelLabel({
		channel: channelInfo.channel,
		source: channelInfo.source,
		gitTag: updateStatus.git?.tag ?? null,
		gitBranch: updateStatus.git?.branch ?? null
	});
	const pickedChannel = await selectStyled({
		message: "Update channel",
		options: [
			{
				value: "keep",
				label: `Keep current (${channelInfo.channel})`,
				hint: channelLabel
			},
			{
				value: "stable",
				label: "Stable",
				hint: "Tagged releases (npm latest)"
			},
			{
				value: "beta",
				label: "Beta",
				hint: "Prereleases (npm beta)"
			},
			{
				value: "dev",
				label: "Dev",
				hint: "Git main"
			}
		],
		initialValue: "keep"
	});
	if (isCancel(pickedChannel)) {
		defaultRuntime.log(theme.muted("Update cancelled."));
		defaultRuntime.exit(0);
		return;
	}
	const requestedChannel = pickedChannel === "keep" ? null : pickedChannel;
	if (requestedChannel === "dev" && updateStatus.installKind !== "git") {
		const gitDir = resolveGitInstallDir();
		if (!await isGitCheckout(gitDir)) {
			if (await pathExists(gitDir)) {
				if (!await isEmptyDir(gitDir)) {
					defaultRuntime.error(`KAIJIBOT_GIT_DIR points at a non-git directory: ${gitDir}. Set KAIJIBOT_GIT_DIR to an empty folder or an kaijibot checkout.`);
					defaultRuntime.exit(1);
					return;
				}
			}
			const ok = await confirm({
				message: stylePromptMessage(`Create a git checkout at ${gitDir}? (override via KAIJIBOT_GIT_DIR)`),
				initialValue: true
			});
			if (isCancel(ok) || !ok) {
				defaultRuntime.log(theme.muted("Update cancelled."));
				defaultRuntime.exit(0);
				return;
			}
		}
	}
	const restart = await confirm({
		message: stylePromptMessage("Restart the gateway service after update?"),
		initialValue: true
	});
	if (isCancel(restart)) {
		defaultRuntime.log(theme.muted("Update cancelled."));
		defaultRuntime.exit(0);
		return;
	}
	try {
		await updateCommand({
			channel: requestedChannel ?? void 0,
			restart: Boolean(restart),
			timeout: opts.timeout
		});
	} catch (err) {
		defaultRuntime.error(String(err));
		defaultRuntime.exit(1);
	}
}
//#endregion
//#region src/cli/update-cli.ts
function inheritedUpdateJson(command) {
	return Boolean(inheritOptionFromParent(command, "json"));
}
function inheritedUpdateTimeout(opts, command) {
	const timeout = opts.timeout;
	if (timeout) return timeout;
	return inheritOptionFromParent(command, "timeout");
}
function registerUpdateCli(program) {
	program.enablePositionalOptions();
	const update = program.command("update").description("Update KaijiBot and inspect update channel status").option("--json", "Output result as JSON", false).option("--no-restart", "Skip restarting the gateway service after a successful update").option("--dry-run", "Preview update actions without making changes", false).option("--channel <stable|beta|dev>", "Persist update channel (git + npm)").option("--tag <dist-tag|version|spec>", "Override the package target for this update (dist-tag, version, or package spec)").option("--timeout <seconds>", "Timeout for each update step in seconds (default: 1200)").option("--yes", "Skip confirmation prompts (non-interactive)", false).addHelpText("after", () => {
		const fmtExamples = [
			["kaijibot update", "Update a source checkout (git)"],
			["kaijibot update --channel beta", "Switch to beta channel (git + npm)"],
			["kaijibot update --channel dev", "Switch to dev channel (git + npm)"],
			["kaijibot update --tag beta", "One-off update to a dist-tag or version"],
			["kaijibot update --tag main", "One-off package install from GitHub main"],
			["kaijibot update --dry-run", "Preview actions without changing anything"],
			["kaijibot update --no-restart", "Update without restarting the service"],
			["kaijibot update --json", "Output result as JSON"],
			["kaijibot update --yes", "Non-interactive (accept downgrade prompts)"],
			["kaijibot update wizard", "Interactive update wizard"],
			["kaijibot --update", "Shorthand for kaijibot update"]
		].map(([cmd, desc]) => `  ${theme.command(cmd)} ${theme.muted(`# ${desc}`)}`).join("\n");
		return `
${theme.heading("What this does:")}
  - Git checkouts: fetches, rebases, installs deps, builds, and runs doctor
  - npm installs: updates via detected package manager

${theme.heading("Switch channels:")}
  - Use --channel stable|beta|dev to persist the update channel in config
  - Run kaijibot update status to see the active channel and source
  - Use --tag <dist-tag|version|spec> for a one-off package update without persisting

${theme.heading("Non-interactive:")}
  - Use --yes to accept downgrade prompts
  - Combine with --channel/--tag/--restart/--json/--timeout as needed
  - Use --dry-run to preview actions without writing config/installing/restarting

${theme.heading("Examples:")}
${fmtExamples}

${theme.heading("Notes:")}
  - Switch channels with --channel stable|beta|dev
  - For global installs: auto-updates via detected package manager when possible (see docs/install/updating.md)
  - Downgrades require confirmation (can break configuration)
  - Skips update if the working directory has uncommitted changes

${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.kaijibot.ai/cli/update")}`;
	}).action(async (opts) => {
		try {
			await updateCommand({
				json: Boolean(opts.json),
				restart: Boolean(opts.restart),
				dryRun: Boolean(opts.dryRun),
				channel: opts.channel,
				tag: opts.tag,
				timeout: opts.timeout,
				yes: Boolean(opts.yes)
			});
		} catch (err) {
			defaultRuntime.error(String(err));
			defaultRuntime.exit(1);
		}
	});
	update.command("wizard").description("Interactive update wizard").option("--timeout <seconds>", "Timeout for each update step in seconds (default: 1200)").addHelpText("after", `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.kaijibot.ai/cli/update")}\n`).action(async (opts, command) => {
		try {
			await updateWizardCommand({ timeout: inheritedUpdateTimeout(opts, command) });
		} catch (err) {
			defaultRuntime.error(String(err));
			defaultRuntime.exit(1);
		}
	});
	update.command("status").description("Show update channel and version status").option("--json", "Output result as JSON", false).option("--timeout <seconds>", "Timeout for update checks in seconds (default: 3)").addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([
		["kaijibot update status", "Show channel + version status."],
		["kaijibot update status --json", "JSON output."],
		["kaijibot update status --timeout 10", "Custom timeout."]
	])}\n\n${theme.heading("Notes:")}\n${theme.muted("- Shows current update channel (stable/beta/dev) and source")}\n${theme.muted("- Includes git tag/branch/SHA for source checkouts")}\n\n${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.kaijibot.ai/cli/update")}`).action(async (opts, command) => {
		try {
			await updateStatusCommand({
				json: Boolean(opts.json) || inheritedUpdateJson(command),
				timeout: inheritedUpdateTimeout(opts, command)
			});
		} catch (err) {
			defaultRuntime.error(String(err));
			defaultRuntime.exit(1);
		}
	});
}
//#endregion
export { registerUpdateCli, updateCommand, updateStatusCommand, updateWizardCommand };
