feat: align update channel installs
This commit is contained in:
109
src/infra/update-global.ts
Normal file
109
src/infra/update-global.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export type GlobalInstallManager = "npm" | "pnpm" | "bun";
|
||||
|
||||
export type CommandRunner = (
|
||||
argv: string[],
|
||||
options: { timeoutMs: number; cwd?: string; env?: NodeJS.ProcessEnv },
|
||||
) => Promise<{ stdout: string; stderr: string; code: number | null }>;
|
||||
|
||||
async function pathExists(targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(targetPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRealpath(targetPath: string): Promise<string> {
|
||||
try {
|
||||
return await fs.realpath(targetPath);
|
||||
} catch {
|
||||
return path.resolve(targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBunGlobalRoot(): string {
|
||||
const bunInstall = process.env.BUN_INSTALL?.trim() || path.join(os.homedir(), ".bun");
|
||||
return path.join(bunInstall, "install", "global", "node_modules");
|
||||
}
|
||||
|
||||
export async function resolveGlobalRoot(
|
||||
manager: GlobalInstallManager,
|
||||
runCommand: CommandRunner,
|
||||
timeoutMs: number,
|
||||
): Promise<string | null> {
|
||||
if (manager === "bun") return resolveBunGlobalRoot();
|
||||
const argv = manager === "pnpm" ? ["pnpm", "root", "-g"] : ["npm", "root", "-g"];
|
||||
const res = await runCommand(argv, { timeoutMs }).catch(() => null);
|
||||
if (!res || res.code !== 0) return null;
|
||||
const root = res.stdout.trim();
|
||||
return root || null;
|
||||
}
|
||||
|
||||
export async function resolveGlobalPackageRoot(
|
||||
manager: GlobalInstallManager,
|
||||
runCommand: CommandRunner,
|
||||
timeoutMs: number,
|
||||
): Promise<string | null> {
|
||||
const root = await resolveGlobalRoot(manager, runCommand, timeoutMs);
|
||||
if (!root) return null;
|
||||
return path.join(root, "clawdbot");
|
||||
}
|
||||
|
||||
export async function detectGlobalInstallManagerForRoot(
|
||||
runCommand: CommandRunner,
|
||||
pkgRoot: string,
|
||||
timeoutMs: number,
|
||||
): Promise<GlobalInstallManager | null> {
|
||||
const pkgReal = await tryRealpath(pkgRoot);
|
||||
|
||||
const candidates: Array<{
|
||||
manager: "npm" | "pnpm";
|
||||
argv: string[];
|
||||
}> = [
|
||||
{ manager: "npm", argv: ["npm", "root", "-g"] },
|
||||
{ manager: "pnpm", argv: ["pnpm", "root", "-g"] },
|
||||
];
|
||||
|
||||
for (const { manager, argv } of candidates) {
|
||||
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);
|
||||
const expected = path.join(globalReal, "clawdbot");
|
||||
if (path.resolve(expected) === path.resolve(pkgReal)) return manager;
|
||||
}
|
||||
|
||||
const bunGlobalRoot = resolveBunGlobalRoot();
|
||||
const bunGlobalReal = await tryRealpath(bunGlobalRoot);
|
||||
const bunExpected = path.join(bunGlobalReal, "clawdbot");
|
||||
if (path.resolve(bunExpected) === path.resolve(pkgReal)) return "bun";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function detectGlobalInstallManagerByPresence(
|
||||
runCommand: CommandRunner,
|
||||
timeoutMs: number,
|
||||
): Promise<GlobalInstallManager | null> {
|
||||
for (const manager of ["npm", "pnpm"] as const) {
|
||||
const root = await resolveGlobalRoot(manager, runCommand, timeoutMs);
|
||||
if (!root) continue;
|
||||
if (await pathExists(path.join(root, "clawdbot"))) return manager;
|
||||
}
|
||||
|
||||
const bunRoot = resolveBunGlobalRoot();
|
||||
if (await pathExists(path.join(bunRoot, "clawdbot"))) return "bun";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function globalInstallArgs(manager: GlobalInstallManager, spec: string): string[] {
|
||||
if (manager === "pnpm") return ["pnpm", "add", "-g", spec];
|
||||
if (manager === "bun") return ["bun", "add", "-g", spec];
|
||||
return ["npm", "i", "-g", spec];
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import os from "node:os";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { type CommandOptions, runCommandWithTimeout } from "../process/exec.js";
|
||||
import { compareSemverStrings } from "./update-check.js";
|
||||
import { DEV_BRANCH, isBetaTag, isStableTag, type UpdateChannel } from "./update-channels.js";
|
||||
import { detectGlobalInstallManagerForRoot, globalInstallArgs } from "./update-global.js";
|
||||
import { trimLogTail } from "./restart-sentinel.js";
|
||||
|
||||
export type UpdateStepResult = {
|
||||
@@ -210,52 +210,6 @@ async function detectPackageManager(root: string) {
|
||||
return "npm";
|
||||
}
|
||||
|
||||
async function tryRealpath(value: string): Promise<string> {
|
||||
try {
|
||||
return await fs.realpath(value);
|
||||
} catch {
|
||||
return path.resolve(value);
|
||||
}
|
||||
}
|
||||
|
||||
async function detectGlobalInstallManager(
|
||||
runCommand: CommandRunner,
|
||||
pkgRoot: string,
|
||||
timeoutMs: number,
|
||||
): Promise<"npm" | "pnpm" | "bun" | null> {
|
||||
const pkgReal = await tryRealpath(pkgRoot);
|
||||
|
||||
const candidates: Array<{
|
||||
manager: "npm" | "pnpm";
|
||||
argv: string[];
|
||||
}> = [
|
||||
{ manager: "npm", argv: ["npm", "root", "-g"] },
|
||||
{ manager: "pnpm", argv: ["pnpm", "root", "-g"] },
|
||||
];
|
||||
|
||||
for (const { manager, argv } of candidates) {
|
||||
const res = await runCommand(argv, { timeoutMs }).catch(() => null);
|
||||
if (!res) continue;
|
||||
if (res.code !== 0) continue;
|
||||
const globalRoot = res.stdout.trim();
|
||||
if (!globalRoot) continue;
|
||||
|
||||
const globalReal = await tryRealpath(globalRoot);
|
||||
const expected = path.join(globalReal, "clawdbot");
|
||||
if (path.resolve(expected) === path.resolve(pkgReal)) return manager;
|
||||
}
|
||||
|
||||
// Bun doesn't have an officially stable "global root" command across versions,
|
||||
// so we check the common global install path (best-effort).
|
||||
const bunInstall = process.env.BUN_INSTALL?.trim() || path.join(os.homedir(), ".bun");
|
||||
const bunGlobalRoot = path.join(bunInstall, "install", "global", "node_modules");
|
||||
const bunGlobalReal = await tryRealpath(bunGlobalRoot);
|
||||
const bunExpected = path.join(bunGlobalReal, "clawdbot");
|
||||
if (path.resolve(bunExpected) === path.resolve(pkgReal)) return "bun";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
type RunStepOptions = {
|
||||
runCommand: CommandRunner;
|
||||
name: string;
|
||||
@@ -324,13 +278,6 @@ function normalizeTag(tag?: string) {
|
||||
return trimmed.startsWith("clawdbot@") ? trimmed.slice("clawdbot@".length) : trimmed;
|
||||
}
|
||||
|
||||
function globalUpdateArgs(manager: "pnpm" | "npm" | "bun", tag?: string) {
|
||||
const spec = `clawdbot@${normalizeTag(tag)}`;
|
||||
if (manager === "pnpm") return ["pnpm", "add", "-g", spec];
|
||||
if (manager === "bun") return ["bun", "add", "-g", spec];
|
||||
return ["npm", "i", "-g", spec];
|
||||
}
|
||||
|
||||
export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<UpdateRunResult> {
|
||||
const startedAt = Date.now();
|
||||
const runCommand =
|
||||
@@ -604,12 +551,13 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
}
|
||||
|
||||
const beforeVersion = await readPackageVersion(pkgRoot);
|
||||
const globalManager = await detectGlobalInstallManager(runCommand, pkgRoot, timeoutMs);
|
||||
const globalManager = await detectGlobalInstallManagerForRoot(runCommand, pkgRoot, timeoutMs);
|
||||
if (globalManager) {
|
||||
const spec = `clawdbot@${normalizeTag(opts.tag)}`;
|
||||
const updateStep = await runStep({
|
||||
runCommand,
|
||||
name: "global update",
|
||||
argv: globalUpdateArgs(globalManager, opts.tag),
|
||||
argv: globalInstallArgs(globalManager, spec),
|
||||
cwd: pkgRoot,
|
||||
timeoutMs,
|
||||
progress,
|
||||
|
||||
Reference in New Issue
Block a user