This repository has been archived on 2026-03-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
openclaw-extensions/wecom/node_modules/openclaw/dist/update-runner-BfDdZ4iY.js
T

1310 lines
36 KiB
JavaScript

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 };