import { r as resolveCliName } from "./command-format-3xiXujG0.js"; import { t as runCommandWithTimeout } from "./exec-BIMFe4XS.js"; import { t as resolveOpenClawPackageRoot } from "./openclaw-root-Dean7Fp2.js"; import { t as note } from "./note-CBiVaqG7.js"; import { o as trimLogTail } from "./restart-sentinel-DywisDen.js"; import { a as resolveCompletionCachePath, n as installCompletion, o as resolveShellFromEnv, r as isCompletionInstalled, s as usesSlowDynamicCompletion, t as completionCacheExists } from "./completion-cli-BK8NKINf.js"; import { r as parseSemver } from "./runtime-guard-DkjmhnBD.js"; import { spawnSync } from "node:child_process"; import path from "node:path"; import os from "node:os"; import fs from "node:fs/promises"; //#region src/infra/update-channels.ts const DEFAULT_PACKAGE_CHANNEL = "stable"; const DEFAULT_GIT_CHANNEL = "dev"; const DEV_BRANCH = "main"; function normalizeUpdateChannel(value) { if (!value) return null; const normalized = value.trim().toLowerCase(); if (normalized === "stable" || normalized === "beta" || normalized === "dev") return normalized; return null; } function channelToNpmTag(channel) { if (channel === "beta") return "beta"; if (channel === "dev") return "dev"; return "latest"; } function isBetaTag(tag) { return tag.toLowerCase().includes("-beta"); } function isStableTag(tag) { return !isBetaTag(tag); } function resolveEffectiveUpdateChannel(params) { if (params.configChannel) return { channel: params.configChannel, source: "config" }; if (params.installKind === "git") { const tag = params.git?.tag; if (tag) return { channel: isBetaTag(tag) ? "beta" : "stable", source: "git-tag" }; const branch = params.git?.branch; if (branch && branch !== "HEAD") return { channel: "dev", source: "git-branch" }; return { channel: DEFAULT_GIT_CHANNEL, source: "default" }; } if (params.installKind === "package") return { channel: DEFAULT_PACKAGE_CHANNEL, source: "default" }; return { channel: DEFAULT_PACKAGE_CHANNEL, source: "default" }; } function formatUpdateChannelLabel(params) { if (params.source === "config") return `${params.channel} (config)`; if (params.source === "git-tag") return params.gitTag ? `${params.channel} (${params.gitTag})` : `${params.channel} (tag)`; if (params.source === "git-branch") return params.gitBranch ? `${params.channel} (${params.gitBranch})` : `${params.channel} (branch)`; return `${params.channel} (default)`; } //#endregion //#region src/infra/update-check.ts async function exists(p) { try { await fs.access(p); return true; } catch { return false; } } async function detectPackageManager$1(root) { try { const raw = await fs.readFile(path.join(root, "package.json"), "utf-8"); const pm = JSON.parse(raw)?.packageManager?.split("@")[0]?.trim(); if (pm === "pnpm" || pm === "bun" || pm === "npm") return pm; } catch {} const files = await fs.readdir(root).catch(() => []); if (files.includes("pnpm-lock.yaml")) return "pnpm"; if (files.includes("bun.lockb")) return "bun"; if (files.includes("package-lock.json")) return "npm"; return "unknown"; } async function detectGitRoot(root) { const res = await runCommandWithTimeout([ "git", "-C", root, "rev-parse", "--show-toplevel" ], { timeoutMs: 4e3 }).catch(() => null); if (!res || res.code !== 0) return null; const top = res.stdout.trim(); return top ? path.resolve(top) : null; } async function checkGitUpdateStatus(params) { const timeoutMs = params.timeoutMs ?? 6e3; const root = path.resolve(params.root); const base = { root, sha: null, tag: null, branch: null, upstream: null, dirty: null, ahead: null, behind: null, fetchOk: null }; const branchRes = await runCommandWithTimeout([ "git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD" ], { timeoutMs }).catch(() => null); if (!branchRes || branchRes.code !== 0) return { ...base, error: branchRes?.stderr?.trim() || "git unavailable" }; const branch = branchRes.stdout.trim() || null; const shaRes = await runCommandWithTimeout([ "git", "-C", root, "rev-parse", "HEAD" ], { timeoutMs }).catch(() => null); const sha = shaRes && shaRes.code === 0 ? shaRes.stdout.trim() : null; const tagRes = await runCommandWithTimeout([ "git", "-C", root, "describe", "--tags", "--exact-match" ], { timeoutMs }).catch(() => null); const tag = tagRes && tagRes.code === 0 ? tagRes.stdout.trim() : null; const upstreamRes = await runCommandWithTimeout([ "git", "-C", root, "rev-parse", "--abbrev-ref", "@{upstream}" ], { timeoutMs }).catch(() => null); const upstream = upstreamRes && upstreamRes.code === 0 ? upstreamRes.stdout.trim() : null; const dirtyRes = await runCommandWithTimeout([ "git", "-C", root, "status", "--porcelain", "--", ":!dist/control-ui/" ], { timeoutMs }).catch(() => null); const dirty = dirtyRes && dirtyRes.code === 0 ? dirtyRes.stdout.trim().length > 0 : null; const fetchOk = params.fetch ? await runCommandWithTimeout([ "git", "-C", root, "fetch", "--quiet", "--prune" ], { timeoutMs }).then((r) => r.code === 0).catch(() => false) : null; const counts = upstream && upstream.length > 0 ? await runCommandWithTimeout([ "git", "-C", root, "rev-list", "--left-right", "--count", `HEAD...${upstream}` ], { timeoutMs }).catch(() => null) : null; const parseCounts = (raw) => { const parts = raw.trim().split(/\s+/); if (parts.length < 2) return null; const ahead = Number.parseInt(parts[0] ?? "", 10); const behind = Number.parseInt(parts[1] ?? "", 10); if (!Number.isFinite(ahead) || !Number.isFinite(behind)) return null; return { ahead, behind }; }; const parsed = counts && counts.code === 0 ? parseCounts(counts.stdout) : null; return { root, sha, tag, branch, upstream, dirty, ahead: parsed?.ahead ?? null, behind: parsed?.behind ?? null, fetchOk }; } async function statMtimeMs(p) { try { return (await fs.stat(p)).mtimeMs; } catch { return null; } } function resolveDepsMarker(params) { const root = params.root; if (params.manager === "pnpm") return { lockfilePath: path.join(root, "pnpm-lock.yaml"), markerPath: path.join(root, "node_modules", ".modules.yaml") }; if (params.manager === "bun") return { lockfilePath: path.join(root, "bun.lockb"), markerPath: path.join(root, "node_modules") }; if (params.manager === "npm") return { lockfilePath: path.join(root, "package-lock.json"), markerPath: path.join(root, "node_modules") }; return { lockfilePath: null, markerPath: null }; } async function checkDepsStatus(params) { const { lockfilePath, markerPath } = resolveDepsMarker({ root: path.resolve(params.root), manager: params.manager }); if (!lockfilePath || !markerPath) return { manager: params.manager, status: "unknown", lockfilePath, markerPath, reason: "unknown package manager" }; const lockExists = await exists(lockfilePath); const markerExists = await exists(markerPath); if (!lockExists) return { manager: params.manager, status: "unknown", lockfilePath, markerPath, reason: "lockfile missing" }; if (!markerExists) return { manager: params.manager, status: "missing", lockfilePath, markerPath, reason: "node_modules marker missing" }; const lockMtime = await statMtimeMs(lockfilePath); const markerMtime = await statMtimeMs(markerPath); if (!lockMtime || !markerMtime) return { manager: params.manager, status: "unknown", lockfilePath, markerPath }; if (lockMtime > markerMtime + 1e3) return { manager: params.manager, status: "stale", lockfilePath, markerPath, reason: "lockfile newer than install marker" }; return { manager: params.manager, status: "ok", lockfilePath, markerPath }; } async function fetchWithTimeout(url, timeoutMs) { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), Math.max(250, timeoutMs)); try { return await fetch(url, { signal: ctrl.signal }); } finally { clearTimeout(t); } } async function fetchNpmLatestVersion(params) { const res = await fetchNpmTagVersion({ tag: "latest", timeoutMs: params?.timeoutMs }); return { latestVersion: res.version, error: res.error }; } async function fetchNpmTagVersion(params) { const timeoutMs = params?.timeoutMs ?? 3500; const tag = params.tag; try { const res = await fetchWithTimeout(`https://registry.npmjs.org/openclaw/${encodeURIComponent(tag)}`, timeoutMs); if (!res.ok) return { tag, version: null, error: `HTTP ${res.status}` }; const json = await res.json(); return { tag, version: typeof json?.version === "string" ? json.version : null }; } catch (err) { return { tag, version: null, error: String(err) }; } } async function resolveNpmChannelTag(params) { const channelTag = channelToNpmTag(params.channel); const channelStatus = await fetchNpmTagVersion({ tag: channelTag, timeoutMs: params.timeoutMs }); if (params.channel !== "beta") return { tag: channelTag, version: channelStatus.version }; const latestStatus = await fetchNpmTagVersion({ tag: "latest", timeoutMs: params.timeoutMs }); if (!latestStatus.version) return { tag: channelTag, version: channelStatus.version }; if (!channelStatus.version) return { tag: "latest", version: latestStatus.version }; const cmp = compareSemverStrings(channelStatus.version, latestStatus.version); if (cmp != null && cmp < 0) return { tag: "latest", version: latestStatus.version }; return { tag: channelTag, version: channelStatus.version }; } function compareSemverStrings(a, b) { const pa = parseSemver(a); const pb = parseSemver(b); if (!pa || !pb) return null; if (pa.major !== pb.major) return pa.major < pb.major ? -1 : 1; if (pa.minor !== pb.minor) return pa.minor < pb.minor ? -1 : 1; if (pa.patch !== pb.patch) return pa.patch < pb.patch ? -1 : 1; return 0; } async function checkUpdateStatus(params) { const timeoutMs = params.timeoutMs ?? 6e3; const root = params.root ? path.resolve(params.root) : null; if (!root) return { root: null, installKind: "unknown", packageManager: "unknown", registry: params.includeRegistry ? await fetchNpmLatestVersion({ timeoutMs }) : void 0 }; const pm = await detectPackageManager$1(root); const gitRoot = await detectGitRoot(root); const isGit = gitRoot && path.resolve(gitRoot) === root; return { root, installKind: isGit ? "git" : "package", packageManager: pm, git: isGit ? await checkGitUpdateStatus({ root, timeoutMs, fetch: Boolean(params.fetchGit) }) : void 0, deps: await checkDepsStatus({ root, manager: pm }), registry: params.includeRegistry ? await fetchNpmLatestVersion({ timeoutMs }) : void 0 }; } //#endregion //#region src/commands/doctor-completion.ts /** Generate the completion cache by spawning the CLI. */ async function generateCompletionCache() { const root = await resolveOpenClawPackageRoot({ moduleUrl: import.meta.url, argv1: process.argv[1], cwd: process.cwd() }); if (!root) return false; const binPath = path.join(root, "openclaw.mjs"); return spawnSync(process.execPath, [ binPath, "completion", "--write-state" ], { cwd: root, env: process.env, encoding: "utf-8" }).status === 0; } /** Check the status of shell completion for the current shell. */ async function checkShellCompletionStatus(binName = "openclaw") { const shell = resolveShellFromEnv(); return { shell, profileInstalled: await isCompletionInstalled(shell, binName), cacheExists: await completionCacheExists(shell, binName), cachePath: resolveCompletionCachePath(shell, binName), usesSlowPattern: await usesSlowDynamicCompletion(shell, binName) }; } /** * Doctor check for shell completion. * - If profile uses slow dynamic pattern: upgrade to cached version * - If profile has completion but no cache: auto-generate cache and upgrade profile * - If no completion at all: prompt to install (with user confirmation) */ async function doctorShellCompletion(runtime, prompter, options = {}) { const cliName = resolveCliName(); const status = await checkShellCompletionStatus(cliName); if (status.usesSlowPattern) { note(`Your ${status.shell} profile uses slow dynamic completion (source <(...)).\nUpgrading to cached completion for faster shell startup...`, "Shell completion"); if (!status.cacheExists) { if (!await generateCompletionCache()) { note(`Failed to generate completion cache. Run \`${cliName} completion --write-state\` manually.`, "Shell completion"); return; } } await installCompletion(status.shell, true, cliName); note(`Shell completion upgraded. Restart your shell or run: source ~/.${status.shell === "zsh" ? "zshrc" : status.shell === "bash" ? "bashrc" : "config/fish/config.fish"}`, "Shell completion"); return; } if (status.profileInstalled && !status.cacheExists) { note(`Shell completion is configured in your ${status.shell} profile but the cache is missing.\nRegenerating cache...`, "Shell completion"); if (await generateCompletionCache()) note(`Completion cache regenerated at ${status.cachePath}`, "Shell completion"); else note(`Failed to regenerate completion cache. Run \`${cliName} completion --write-state\` manually.`, "Shell completion"); return; } if (!status.profileInstalled) { if (options.nonInteractive) return; if (await prompter.confirm({ message: `Enable ${status.shell} shell completion for ${cliName}?`, initialValue: true })) { if (!await generateCompletionCache()) { note(`Failed to generate completion cache. Run \`${cliName} completion --write-state\` manually.`, "Shell completion"); return; } await installCompletion(status.shell, true, cliName); note(`Shell completion installed. Restart your shell or run: source ~/.${status.shell === "zsh" ? "zshrc" : status.shell === "bash" ? "bashrc" : "config/fish/config.fish"}`, "Shell completion"); } } } /** * Ensure completion cache exists. Used during onboarding/update to fix * cases where profile has completion but no cache. * This is a silent fix - no prompts. */ async function ensureCompletionCacheExists(binName = "openclaw") { if (await completionCacheExists(resolveShellFromEnv(), binName)) return true; return generateCompletionCache(); } //#endregion //#region src/infra/update-global.ts const PRIMARY_PACKAGE_NAME = "openclaw"; const ALL_PACKAGE_NAMES = [PRIMARY_PACKAGE_NAME]; const GLOBAL_RENAME_PREFIX = "."; async function pathExists(targetPath) { try { await fs.access(targetPath); return true; } catch { return false; } } async function tryRealpath(targetPath) { try { return await fs.realpath(targetPath); } catch { return path.resolve(targetPath); } } function resolveBunGlobalRoot() { const bunInstall = process.env.BUN_INSTALL?.trim() || path.join(os.homedir(), ".bun"); return path.join(bunInstall, "install", "global", "node_modules"); } async function resolveGlobalRoot(manager, runCommand, timeoutMs) { if (manager === "bun") return resolveBunGlobalRoot(); const res = await runCommand(manager === "pnpm" ? [ "pnpm", "root", "-g" ] : [ "npm", "root", "-g" ], { timeoutMs }).catch(() => null); if (!res || res.code !== 0) return null; return res.stdout.trim() || null; } async function resolveGlobalPackageRoot(manager, runCommand, timeoutMs) { const root = await resolveGlobalRoot(manager, runCommand, timeoutMs); if (!root) return null; return path.join(root, PRIMARY_PACKAGE_NAME); } async function detectGlobalInstallManagerForRoot(runCommand, pkgRoot, timeoutMs) { const pkgReal = await tryRealpath(pkgRoot); for (const { manager, argv } of [{ manager: "npm", argv: [ "npm", "root", "-g" ] }, { manager: "pnpm", argv: [ "pnpm", "root", "-g" ] }]) { const res = await runCommand(argv, { timeoutMs }).catch(() => null); if (!res || res.code !== 0) continue; const globalRoot = res.stdout.trim(); if (!globalRoot) continue; const globalReal = await tryRealpath(globalRoot); for (const name of ALL_PACKAGE_NAMES) { const expected = path.join(globalReal, name); if (path.resolve(expected) === path.resolve(pkgReal)) return manager; } } const bunGlobalReal = await tryRealpath(resolveBunGlobalRoot()); for (const name of ALL_PACKAGE_NAMES) { const bunExpected = path.join(bunGlobalReal, name); if (path.resolve(bunExpected) === path.resolve(pkgReal)) return "bun"; } return null; } async function detectGlobalInstallManagerByPresence(runCommand, timeoutMs) { for (const manager of ["npm", "pnpm"]) { const root = await resolveGlobalRoot(manager, runCommand, timeoutMs); if (!root) continue; for (const name of ALL_PACKAGE_NAMES) if (await pathExists(path.join(root, name))) return manager; } const bunRoot = resolveBunGlobalRoot(); for (const name of ALL_PACKAGE_NAMES) if (await pathExists(path.join(bunRoot, name))) return "bun"; return null; } function globalInstallArgs(manager, spec) { if (manager === "pnpm") return [ "pnpm", "add", "-g", spec ]; if (manager === "bun") return [ "bun", "add", "-g", spec ]; return [ "npm", "i", "-g", spec ]; } async function cleanupGlobalRenameDirs(params) { const removed = []; const root = params.globalRoot.trim(); const name = params.packageName.trim(); if (!root || !name) return { removed }; const prefix = `${GLOBAL_RENAME_PREFIX}${name}-`; let entries = []; try { entries = await fs.readdir(root); } catch { return { removed }; } for (const entry of entries) { if (!entry.startsWith(prefix)) continue; const target = path.join(root, entry); try { if (!(await fs.lstat(target)).isDirectory()) continue; await fs.rm(target, { recursive: true, force: true }); removed.push(entry); } catch {} } return { removed }; } //#endregion //#region src/infra/update-runner.ts const DEFAULT_TIMEOUT_MS = 20 * 6e4; const MAX_LOG_CHARS = 8e3; const PREFLIGHT_MAX_COMMITS = 10; const START_DIRS = [ "cwd", "argv1", "process" ]; const DEFAULT_PACKAGE_NAME = "openclaw"; const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]); function normalizeDir(value) { if (!value) return null; const trimmed = value.trim(); if (!trimmed) return null; return path.resolve(trimmed); } function resolveNodeModulesBinPackageRoot(argv1) { const normalized = path.resolve(argv1); const parts = normalized.split(path.sep); const binIndex = parts.lastIndexOf(".bin"); if (binIndex <= 0) return null; if (parts[binIndex - 1] !== "node_modules") return null; const binName = path.basename(normalized); const nodeModulesDir = parts.slice(0, binIndex).join(path.sep); return path.join(nodeModulesDir, binName); } function buildStartDirs(opts) { const dirs = []; const cwd = normalizeDir(opts.cwd); if (cwd) dirs.push(cwd); const argv1 = normalizeDir(opts.argv1); if (argv1) { dirs.push(path.dirname(argv1)); const packageRoot = resolveNodeModulesBinPackageRoot(argv1); if (packageRoot) dirs.push(packageRoot); } const proc = normalizeDir(process.cwd()); if (proc) dirs.push(proc); return Array.from(new Set(dirs)); } async function readPackageVersion(root) { try { const raw = await fs.readFile(path.join(root, "package.json"), "utf-8"); const parsed = JSON.parse(raw); return typeof parsed?.version === "string" ? parsed.version : null; } catch { return null; } } async function readPackageName(root) { try { const raw = await fs.readFile(path.join(root, "package.json"), "utf-8"); const name = JSON.parse(raw)?.name?.trim(); return name ? name : null; } catch { return null; } } async function readBranchName(runCommand, root, timeoutMs) { const res = await runCommand([ "git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD" ], { timeoutMs }).catch(() => null); if (!res || res.code !== 0) return null; return res.stdout.trim() || null; } async function listGitTags(runCommand, root, timeoutMs, pattern = "v*") { const res = await runCommand([ "git", "-C", root, "tag", "--list", pattern, "--sort=-v:refname" ], { timeoutMs }).catch(() => null); if (!res || res.code !== 0) return []; return res.stdout.split("\n").map((line) => line.trim()).filter(Boolean); } async function resolveChannelTag(runCommand, root, timeoutMs, channel) { const tags = await listGitTags(runCommand, root, timeoutMs); if (channel === "beta") { const betaTag = tags.find((tag) => isBetaTag(tag)) ?? null; const stableTag = tags.find((tag) => isStableTag(tag)) ?? null; if (!betaTag) return stableTag; if (!stableTag) return betaTag; const cmp = compareSemverStrings(betaTag, stableTag); if (cmp != null && cmp < 0) return stableTag; return betaTag; } return tags.find((tag) => isStableTag(tag)) ?? null; } async function resolveGitRoot(runCommand, candidates, timeoutMs) { for (const dir of candidates) { const res = await runCommand([ "git", "-C", dir, "rev-parse", "--show-toplevel" ], { timeoutMs }); if (res.code === 0) { const root = res.stdout.trim(); if (root) return root; } } return null; } async function findPackageRoot(candidates) { for (const dir of candidates) { let current = dir; for (let i = 0; i < 12; i += 1) { const pkgPath = path.join(current, "package.json"); try { const raw = await fs.readFile(pkgPath, "utf-8"); const name = JSON.parse(raw)?.name?.trim(); if (name && CORE_PACKAGE_NAMES.has(name)) return current; } catch {} const parent = path.dirname(current); if (parent === current) break; current = parent; } } return null; } async function detectPackageManager(root) { try { const raw = await fs.readFile(path.join(root, "package.json"), "utf-8"); const pm = JSON.parse(raw)?.packageManager?.split("@")[0]?.trim(); if (pm === "pnpm" || pm === "bun" || pm === "npm") return pm; } catch {} const files = await fs.readdir(root).catch(() => []); if (files.includes("pnpm-lock.yaml")) return "pnpm"; if (files.includes("bun.lockb")) return "bun"; if (files.includes("package-lock.json")) return "npm"; return "npm"; } async function runStep(opts) { const { runCommand, name, argv, cwd, timeoutMs, env, progress, stepIndex, totalSteps } = opts; const command = argv.join(" "); const stepInfo = { name, command, index: stepIndex, total: totalSteps }; progress?.onStepStart?.(stepInfo); const started = Date.now(); const result = await runCommand(argv, { cwd, timeoutMs, env }); const durationMs = Date.now() - started; const stderrTail = trimLogTail(result.stderr, MAX_LOG_CHARS); progress?.onStepComplete?.({ ...stepInfo, durationMs, exitCode: result.code, stderrTail }); return { name, command, cwd, durationMs, exitCode: result.code, stdoutTail: trimLogTail(result.stdout, MAX_LOG_CHARS), stderrTail: trimLogTail(result.stderr, MAX_LOG_CHARS) }; } function managerScriptArgs(manager, script, args = []) { if (manager === "pnpm") return [ "pnpm", script, ...args ]; if (manager === "bun") return [ "bun", "run", script, ...args ]; if (args.length > 0) return [ "npm", "run", script, "--", ...args ]; return [ "npm", "run", script ]; } function managerInstallArgs(manager) { if (manager === "pnpm") return ["pnpm", "install"]; if (manager === "bun") return ["bun", "install"]; return ["npm", "install"]; } function normalizeTag(tag) { const trimmed = tag?.trim(); if (!trimmed) return "latest"; if (trimmed.startsWith("openclaw@")) return trimmed.slice(9); if (trimmed.startsWith(`${DEFAULT_PACKAGE_NAME}@`)) return trimmed.slice(`${DEFAULT_PACKAGE_NAME}@`.length); return trimmed; } async function runGatewayUpdate(opts = {}) { const startedAt = Date.now(); const runCommand = opts.runCommand ?? (async (argv, options) => { const res = await runCommandWithTimeout(argv, options); return { stdout: res.stdout, stderr: res.stderr, code: res.code }; }); const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; const progress = opts.progress; const steps = []; const candidates = buildStartDirs(opts); let stepIndex = 0; let gitTotalSteps = 0; const step = (name, argv, cwd, env) => { const currentIndex = stepIndex; stepIndex += 1; return { runCommand, name, argv, cwd, timeoutMs, env, progress, stepIndex: currentIndex, totalSteps: gitTotalSteps }; }; const pkgRoot = await findPackageRoot(candidates); let gitRoot = await resolveGitRoot(runCommand, candidates, timeoutMs); if (gitRoot && pkgRoot && path.resolve(gitRoot) !== path.resolve(pkgRoot)) gitRoot = null; if (gitRoot && !pkgRoot) return { status: "error", mode: "unknown", root: gitRoot, reason: "not-openclaw-root", steps: [], durationMs: Date.now() - startedAt }; if (gitRoot && pkgRoot && path.resolve(gitRoot) === path.resolve(pkgRoot)) { const beforeSha = (await runCommand([ "git", "-C", gitRoot, "rev-parse", "HEAD" ], { cwd: gitRoot, timeoutMs })).stdout.trim() || null; const beforeVersion = await readPackageVersion(gitRoot); const channel = opts.channel ?? "dev"; const branch = channel === "dev" ? await readBranchName(runCommand, gitRoot, timeoutMs) : null; const needsCheckoutMain = channel === "dev" && branch !== DEV_BRANCH; gitTotalSteps = channel === "dev" ? needsCheckoutMain ? 11 : 10 : 9; const statusCheck = await runStep(step("clean check", [ "git", "-C", gitRoot, "status", "--porcelain", "--", ":!dist/control-ui/" ], gitRoot)); steps.push(statusCheck); if (statusCheck.stdoutTail && statusCheck.stdoutTail.trim().length > 0) return { status: "skipped", mode: "git", root: gitRoot, reason: "dirty", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; if (channel === "dev") { if (needsCheckoutMain) { const checkoutStep = await runStep(step(`git checkout ${DEV_BRANCH}`, [ "git", "-C", gitRoot, "checkout", DEV_BRANCH ], gitRoot)); steps.push(checkoutStep); if (checkoutStep.exitCode !== 0) return { status: "error", mode: "git", root: gitRoot, reason: "checkout-failed", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; } const upstreamStep = await runStep(step("upstream check", [ "git", "-C", gitRoot, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}" ], gitRoot)); steps.push(upstreamStep); if (upstreamStep.exitCode !== 0) return { status: "skipped", mode: "git", root: gitRoot, reason: "no-upstream", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; const fetchStep = await runStep(step("git fetch", [ "git", "-C", gitRoot, "fetch", "--all", "--prune", "--tags" ], gitRoot)); steps.push(fetchStep); const upstreamShaStep = await runStep(step("git rev-parse @{upstream}", [ "git", "-C", gitRoot, "rev-parse", "@{upstream}" ], gitRoot)); steps.push(upstreamShaStep); const upstreamSha = upstreamShaStep.stdoutTail?.trim(); if (!upstreamShaStep.stdoutTail || !upstreamSha) return { status: "error", mode: "git", root: gitRoot, reason: "no-upstream-sha", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; const revListStep = await runStep(step("git rev-list", [ "git", "-C", gitRoot, "rev-list", `--max-count=${PREFLIGHT_MAX_COMMITS}`, upstreamSha ], gitRoot)); steps.push(revListStep); if (revListStep.exitCode !== 0) return { status: "error", mode: "git", root: gitRoot, reason: "preflight-revlist-failed", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; const candidates = (revListStep.stdoutTail ?? "").split("\n").map((line) => line.trim()).filter(Boolean); if (candidates.length === 0) return { status: "error", mode: "git", root: gitRoot, reason: "preflight-no-candidates", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; const manager = await detectPackageManager(gitRoot); const preflightRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-preflight-")); const worktreeDir = path.join(preflightRoot, "worktree"); const worktreeStep = await runStep(step("preflight worktree", [ "git", "-C", gitRoot, "worktree", "add", "--detach", worktreeDir, upstreamSha ], gitRoot)); steps.push(worktreeStep); if (worktreeStep.exitCode !== 0) { await fs.rm(preflightRoot, { recursive: true, force: true }).catch(() => {}); return { status: "error", mode: "git", root: gitRoot, reason: "preflight-worktree-failed", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; } let selectedSha = null; try { for (const sha of candidates) { const shortSha = sha.slice(0, 8); const checkoutStep = await runStep(step(`preflight checkout (${shortSha})`, [ "git", "-C", worktreeDir, "checkout", "--detach", sha ], worktreeDir)); steps.push(checkoutStep); if (checkoutStep.exitCode !== 0) continue; const depsStep = await runStep(step(`preflight deps install (${shortSha})`, managerInstallArgs(manager), worktreeDir)); steps.push(depsStep); if (depsStep.exitCode !== 0) continue; const lintStep = await runStep(step(`preflight lint (${shortSha})`, managerScriptArgs(manager, "lint"), worktreeDir)); steps.push(lintStep); if (lintStep.exitCode !== 0) continue; const buildStep = await runStep(step(`preflight build (${shortSha})`, managerScriptArgs(manager, "build"), worktreeDir)); steps.push(buildStep); if (buildStep.exitCode !== 0) continue; selectedSha = sha; break; } } finally { const removeStep = await runStep(step("preflight cleanup", [ "git", "-C", gitRoot, "worktree", "remove", "--force", worktreeDir ], gitRoot)); steps.push(removeStep); await runCommand([ "git", "-C", gitRoot, "worktree", "prune" ], { cwd: gitRoot, timeoutMs }).catch(() => null); await fs.rm(preflightRoot, { recursive: true, force: true }).catch(() => {}); } if (!selectedSha) return { status: "error", mode: "git", root: gitRoot, reason: "preflight-no-good-commit", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; const rebaseStep = await runStep(step("git rebase", [ "git", "-C", gitRoot, "rebase", selectedSha ], gitRoot)); steps.push(rebaseStep); if (rebaseStep.exitCode !== 0) { const abortResult = await runCommand([ "git", "-C", gitRoot, "rebase", "--abort" ], { cwd: gitRoot, timeoutMs }); steps.push({ name: "git rebase --abort", command: "git rebase --abort", cwd: gitRoot, durationMs: 0, exitCode: abortResult.code, stdoutTail: trimLogTail(abortResult.stdout, MAX_LOG_CHARS), stderrTail: trimLogTail(abortResult.stderr, MAX_LOG_CHARS) }); return { status: "error", mode: "git", root: gitRoot, reason: "rebase-failed", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; } } else { const fetchStep = await runStep(step("git fetch", [ "git", "-C", gitRoot, "fetch", "--all", "--prune", "--tags" ], gitRoot)); steps.push(fetchStep); if (fetchStep.exitCode !== 0) return { status: "error", mode: "git", root: gitRoot, reason: "fetch-failed", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; const tag = await resolveChannelTag(runCommand, gitRoot, timeoutMs, channel); if (!tag) return { status: "error", mode: "git", root: gitRoot, reason: "no-release-tag", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; const checkoutStep = await runStep(step(`git checkout ${tag}`, [ "git", "-C", gitRoot, "checkout", "--detach", tag ], gitRoot)); steps.push(checkoutStep); if (checkoutStep.exitCode !== 0) return { status: "error", mode: "git", root: gitRoot, reason: "checkout-failed", before: { sha: beforeSha, version: beforeVersion }, steps, durationMs: Date.now() - startedAt }; } const manager = await detectPackageManager(gitRoot); const depsStep = await runStep(step("deps install", managerInstallArgs(manager), gitRoot)); steps.push(depsStep); const buildStep = await runStep(step("build", managerScriptArgs(manager, "build"), gitRoot)); steps.push(buildStep); const uiBuildStep = await runStep(step("ui:build", managerScriptArgs(manager, "ui:build"), gitRoot)); steps.push(uiBuildStep); const restoreUiStep = await runStep(step("restore control-ui", [ "git", "-C", gitRoot, "checkout", "--", "dist/control-ui/" ], gitRoot)); steps.push(restoreUiStep); const doctorStep = await runStep(step("openclaw doctor", managerScriptArgs(manager, "openclaw", ["doctor", "--non-interactive"]), gitRoot, { OPENCLAW_UPDATE_IN_PROGRESS: "1" })); steps.push(doctorStep); const failedStep = steps.find((s) => s.exitCode !== 0); const afterShaStep = await runStep(step("git rev-parse HEAD (after)", [ "git", "-C", gitRoot, "rev-parse", "HEAD" ], gitRoot)); steps.push(afterShaStep); const afterVersion = await readPackageVersion(gitRoot); return { status: failedStep ? "error" : "ok", mode: "git", root: gitRoot, reason: failedStep ? failedStep.name : void 0, before: { sha: beforeSha, version: beforeVersion }, after: { sha: afterShaStep.stdoutTail?.trim() ?? null, version: afterVersion }, steps, durationMs: Date.now() - startedAt }; } if (!pkgRoot) return { status: "error", mode: "unknown", reason: `no root (${START_DIRS.join(",")})`, steps: [], durationMs: Date.now() - startedAt }; const beforeVersion = await readPackageVersion(pkgRoot); const globalManager = await detectGlobalInstallManagerForRoot(runCommand, pkgRoot, timeoutMs); if (globalManager) { const packageName = await readPackageName(pkgRoot) ?? DEFAULT_PACKAGE_NAME; await cleanupGlobalRenameDirs({ globalRoot: path.dirname(pkgRoot), packageName }); const channel = opts.channel ?? DEFAULT_PACKAGE_CHANNEL; const updateStep = await runStep({ runCommand, name: "global update", argv: globalInstallArgs(globalManager, `${packageName}@${normalizeTag(opts.tag ?? channelToNpmTag(channel))}`), cwd: pkgRoot, timeoutMs, progress, stepIndex: 0, totalSteps: 1 }); const steps = [updateStep]; const afterVersion = await readPackageVersion(pkgRoot); return { status: updateStep.exitCode === 0 ? "ok" : "error", mode: globalManager, root: pkgRoot, reason: updateStep.exitCode === 0 ? void 0 : updateStep.name, before: { version: beforeVersion }, after: { version: afterVersion }, steps, durationMs: Date.now() - startedAt }; } return { status: "skipped", mode: "unknown", root: pkgRoot, reason: "not-git-install", before: { version: beforeVersion }, steps: [], durationMs: Date.now() - startedAt }; } //#endregion export { formatUpdateChannelLabel as _, globalInstallArgs as a, doctorShellCompletion as c, compareSemverStrings as d, fetchNpmTagVersion as f, channelToNpmTag as g, DEFAULT_PACKAGE_CHANNEL as h, detectGlobalInstallManagerForRoot as i, ensureCompletionCacheExists as l, DEFAULT_GIT_CHANNEL as m, cleanupGlobalRenameDirs as n, resolveGlobalPackageRoot as o, resolveNpmChannelTag as p, detectGlobalInstallManagerByPresence as r, checkShellCompletionStatus as s, runGatewayUpdate as t, checkUpdateStatus as u, normalizeUpdateChannel as v, resolveEffectiveUpdateChannel as y };