fix: harden windows native updates
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated `session.store` roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.
|
||||
- Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process `HOME`/`OPENCLAW_HOME` changes no longer reuse stale plugin state or misreport `~/...` plugins as untracked. (#44046) thanks @gumadeiras.
|
||||
- Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and `models list --plain`, and migrate legacy duplicated `openrouter/openrouter/...` config entries forward on write.
|
||||
- Windows/native update: make package installs use the npm update path instead of the git path, carry portable Git into native Windows updates, and mirror the installer's Windows npm env so `openclaw update` no longer dies early on missing `git` or `node-llama-cpp` download setup.
|
||||
- Sandbox/write: preserve pinned mutation-helper payload stdin so sandboxed `write` no longer reports success while creating empty files. (#43876) Thanks @glitch418x.
|
||||
- Security/exec approvals: escape invisible Unicode format characters in approval prompts so zero-width command text renders as visible `\u{...}` escapes instead of spoofing the reviewed command. (`GHSA-pcqg-f7rg-xfvv`)(#43687) Thanks @EkiXu and @vincentkoc.
|
||||
- Security/exec detection: normalize compatibility Unicode and strip invisible formatting code points before obfuscation checks so zero-width and fullwidth command tricks no longer suppress heuristic detection. (`GHSA-9r3v-37xh-2cf6`)(#44091) Thanks @wooluo and @vincentkoc.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/types.openclaw.js";
|
||||
@@ -390,14 +391,13 @@ describe("update-cli", () => {
|
||||
},
|
||||
{
|
||||
name: "defaults to stable channel for package installs when unset",
|
||||
mode: "npm" as const,
|
||||
options: { yes: true },
|
||||
prepare: async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
mockPackageInstallStatus(tempDir);
|
||||
},
|
||||
expectedChannel: "stable" as const,
|
||||
expectedTag: "latest",
|
||||
expectedChannel: undefined as "stable" | undefined,
|
||||
expectedTag: undefined as string | undefined,
|
||||
},
|
||||
{
|
||||
name: "uses stored beta channel when configured",
|
||||
@@ -414,14 +414,25 @@ describe("update-cli", () => {
|
||||
},
|
||||
])("$name", async ({ mode, options, prepare, expectedChannel, expectedTag }) => {
|
||||
await prepare();
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult({ mode }));
|
||||
if (mode) {
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult({ mode }));
|
||||
}
|
||||
|
||||
await updateCommand(options);
|
||||
|
||||
const call = expectUpdateCallChannel(expectedChannel);
|
||||
if (expectedTag !== undefined) {
|
||||
expect(call?.tag).toBe(expectedTag);
|
||||
if (expectedChannel !== undefined) {
|
||||
const call = expectUpdateCallChannel(expectedChannel);
|
||||
if (expectedTag !== undefined) {
|
||||
expect(call?.tag).toBe(expectedTag);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
["npm", "i", "-g", "openclaw@latest", "--no-fund", "--no-audit", "--loglevel=error"],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to latest when beta tag is older than release", async () => {
|
||||
@@ -436,32 +447,78 @@ describe("update-cli", () => {
|
||||
tag: "latest",
|
||||
version: "1.2.3-1",
|
||||
});
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(
|
||||
makeOkUpdateResult({
|
||||
mode: "npm",
|
||||
}),
|
||||
);
|
||||
|
||||
await updateCommand({});
|
||||
|
||||
const call = expectUpdateCallChannel("beta");
|
||||
expect(call?.tag).toBe("latest");
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
["npm", "i", "-g", "openclaw@latest", "--no-fund", "--no-audit", "--loglevel=error"],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("honors --tag override", async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
|
||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(
|
||||
makeOkUpdateResult({
|
||||
mode: "npm",
|
||||
}),
|
||||
);
|
||||
mockPackageInstallStatus(tempDir);
|
||||
|
||||
await updateCommand({ tag: "next" });
|
||||
|
||||
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
|
||||
expect(call?.tag).toBe("next");
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
["npm", "i", "-g", "openclaw@next", "--no-fund", "--no-audit", "--loglevel=error"],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("prepends portable Git PATH for package updates on Windows", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
const localAppData = createCaseDir("openclaw-localappdata");
|
||||
const portableGitMingw = path.join(
|
||||
localAppData,
|
||||
"OpenClaw",
|
||||
"deps",
|
||||
"portable-git",
|
||||
"mingw64",
|
||||
"bin",
|
||||
);
|
||||
const portableGitUsr = path.join(
|
||||
localAppData,
|
||||
"OpenClaw",
|
||||
"deps",
|
||||
"portable-git",
|
||||
"usr",
|
||||
"bin",
|
||||
);
|
||||
await fs.mkdir(portableGitMingw, { recursive: true });
|
||||
await fs.mkdir(portableGitUsr, { recursive: true });
|
||||
mockPackageInstallStatus(tempDir);
|
||||
pathExists.mockImplementation(
|
||||
async (candidate: string) => candidate === portableGitMingw || candidate === portableGitUsr,
|
||||
);
|
||||
|
||||
await withEnvAsync({ LOCALAPPDATA: localAppData }, async () => {
|
||||
await updateCommand({ yes: true });
|
||||
});
|
||||
|
||||
platformSpy.mockRestore();
|
||||
|
||||
const updateCall = vi
|
||||
.mocked(runCommandWithTimeout)
|
||||
.mock.calls.find(
|
||||
(call) =>
|
||||
Array.isArray(call[0]) &&
|
||||
call[0][0] === "npm" &&
|
||||
call[0][1] === "i" &&
|
||||
call[0][2] === "-g",
|
||||
);
|
||||
const mergedPath = updateCall?.[1]?.env?.Path ?? updateCall?.[1]?.env?.PATH ?? "";
|
||||
expect(mergedPath.split(path.delimiter).slice(0, 2)).toEqual([
|
||||
portableGitMingw,
|
||||
portableGitUsr,
|
||||
]);
|
||||
expect(updateCall?.[1]?.env?.NPM_CONFIG_SCRIPT_SHELL).toBe("cmd.exe");
|
||||
expect(updateCall?.[1]?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
|
||||
});
|
||||
|
||||
it("updateCommand outputs JSON when --json is set", async () => {
|
||||
@@ -648,15 +705,15 @@ describe("update-cli", () => {
|
||||
name: "requires confirmation without --yes",
|
||||
options: {},
|
||||
shouldExit: true,
|
||||
shouldRunUpdate: false,
|
||||
shouldRunPackageUpdate: false,
|
||||
},
|
||||
{
|
||||
name: "allows downgrade with --yes",
|
||||
options: { yes: true },
|
||||
shouldExit: false,
|
||||
shouldRunUpdate: true,
|
||||
shouldRunPackageUpdate: true,
|
||||
},
|
||||
])("$name in non-interactive mode", async ({ options, shouldExit, shouldRunUpdate }) => {
|
||||
])("$name in non-interactive mode", async ({ options, shouldExit, shouldRunPackageUpdate }) => {
|
||||
await setupNonInteractiveDowngrade();
|
||||
await updateCommand(options);
|
||||
|
||||
@@ -667,7 +724,12 @@ describe("update-cli", () => {
|
||||
expect(vi.mocked(defaultRuntime.exit).mock.calls.some((call) => call[0] === 1)).toBe(
|
||||
shouldExit,
|
||||
);
|
||||
expect(vi.mocked(runGatewayUpdate).mock.calls.length > 0).toBe(shouldRunUpdate);
|
||||
expect(vi.mocked(runGatewayUpdate).mock.calls.length > 0).toBe(false);
|
||||
expect(
|
||||
vi
|
||||
.mocked(runCommandWithTimeout)
|
||||
.mock.calls.some((call) => Array.isArray(call[0]) && call[0][0] === "npm"),
|
||||
).toBe(shouldRunPackageUpdate);
|
||||
});
|
||||
|
||||
it("dry-run bypasses downgrade confirmation checks in non-interactive mode", async () => {
|
||||
|
||||
@@ -144,6 +144,7 @@ export async function runUpdateStep(params: {
|
||||
cwd?: string;
|
||||
timeoutMs: number;
|
||||
progress?: UpdateStepProgress;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<UpdateStepResult> {
|
||||
const command = params.argv.join(" ");
|
||||
params.progress?.onStepStart?.({
|
||||
@@ -156,6 +157,7 @@ export async function runUpdateStep(params: {
|
||||
const started = Date.now();
|
||||
const res = await runCommandWithTimeout(params.argv, {
|
||||
cwd: params.cwd,
|
||||
env: params.env,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
const durationMs = Date.now() - started;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
checkUpdateStatus,
|
||||
} from "../../infra/update-check.js";
|
||||
import {
|
||||
createGlobalInstallEnv,
|
||||
cleanupGlobalRenameDirs,
|
||||
globalInstallArgs,
|
||||
resolveGlobalPackageRoot,
|
||||
@@ -269,6 +270,7 @@ async function runPackageInstallUpdate(params: {
|
||||
installKind: params.installKind,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
const installEnv = await createGlobalInstallEnv();
|
||||
const runCommand = createGlobalCommandRunner();
|
||||
|
||||
const pkgRoot = await resolveGlobalPackageRoot(manager, runCommand, params.timeoutMs);
|
||||
@@ -287,6 +289,7 @@ async function runPackageInstallUpdate(params: {
|
||||
const updateStep = await runUpdateStep({
|
||||
name: "global update",
|
||||
argv: globalInstallArgs(manager, `${packageName}@${params.tag}`),
|
||||
env: installEnv,
|
||||
timeoutMs: params.timeoutMs,
|
||||
progress: params.progress,
|
||||
});
|
||||
@@ -380,6 +383,7 @@ async function runGitUpdate(params: {
|
||||
name: "global install",
|
||||
argv: globalInstallArgs(manager, updateRoot),
|
||||
cwd: updateRoot,
|
||||
env: await createGlobalInstallEnv(),
|
||||
timeoutMs: effectiveTimeout,
|
||||
progress: params.progress,
|
||||
});
|
||||
@@ -835,28 +839,29 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const result = switchToPackage
|
||||
? await runPackageInstallUpdate({
|
||||
root,
|
||||
installKind,
|
||||
tag,
|
||||
timeoutMs: timeoutMs ?? 20 * 60_000,
|
||||
startedAt,
|
||||
progress,
|
||||
})
|
||||
: await runGitUpdate({
|
||||
root,
|
||||
switchToGit,
|
||||
installKind,
|
||||
timeoutMs,
|
||||
startedAt,
|
||||
progress,
|
||||
channel,
|
||||
tag,
|
||||
showProgress,
|
||||
opts,
|
||||
stop,
|
||||
});
|
||||
const result =
|
||||
updateInstallKind === "package"
|
||||
? await runPackageInstallUpdate({
|
||||
root,
|
||||
installKind,
|
||||
tag,
|
||||
timeoutMs: timeoutMs ?? 20 * 60_000,
|
||||
startedAt,
|
||||
progress,
|
||||
})
|
||||
: await runGitUpdate({
|
||||
root,
|
||||
switchToGit,
|
||||
installKind,
|
||||
timeoutMs,
|
||||
startedAt,
|
||||
progress,
|
||||
channel,
|
||||
tag,
|
||||
showProgress,
|
||||
opts,
|
||||
stop,
|
||||
});
|
||||
|
||||
stop();
|
||||
printResult(result, { ...opts, hideSteps: showProgress });
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathExists } from "../utils.js";
|
||||
import { applyPathPrepend } from "./path-prepend.js";
|
||||
|
||||
export type GlobalInstallManager = "npm" | "pnpm" | "bun";
|
||||
|
||||
@@ -19,6 +20,60 @@ const NPM_GLOBAL_INSTALL_OMIT_OPTIONAL_FLAGS = [
|
||||
...NPM_GLOBAL_INSTALL_QUIET_FLAGS,
|
||||
] as const;
|
||||
|
||||
async function resolvePortableGitPathPrepend(
|
||||
env: NodeJS.ProcessEnv | undefined,
|
||||
): Promise<string[]> {
|
||||
if (process.platform !== "win32") {
|
||||
return [];
|
||||
}
|
||||
const localAppData = env?.LOCALAPPDATA?.trim() || process.env.LOCALAPPDATA?.trim();
|
||||
if (!localAppData) {
|
||||
return [];
|
||||
}
|
||||
const portableGitRoot = path.join(localAppData, "OpenClaw", "deps", "portable-git");
|
||||
const candidates = [
|
||||
path.join(portableGitRoot, "mingw64", "bin"),
|
||||
path.join(portableGitRoot, "usr", "bin"),
|
||||
path.join(portableGitRoot, "cmd"),
|
||||
path.join(portableGitRoot, "bin"),
|
||||
];
|
||||
const existing: string[] = [];
|
||||
for (const candidate of candidates) {
|
||||
if (await pathExists(candidate)) {
|
||||
existing.push(candidate);
|
||||
}
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
function applyWindowsPackageInstallEnv(env: Record<string, string>) {
|
||||
if (process.platform !== "win32") {
|
||||
return;
|
||||
}
|
||||
env.NPM_CONFIG_UPDATE_NOTIFIER = "false";
|
||||
env.NPM_CONFIG_FUND = "false";
|
||||
env.NPM_CONFIG_AUDIT = "false";
|
||||
env.NPM_CONFIG_SCRIPT_SHELL = "cmd.exe";
|
||||
env.NODE_LLAMA_CPP_SKIP_DOWNLOAD = "1";
|
||||
}
|
||||
|
||||
export async function createGlobalInstallEnv(
|
||||
env?: NodeJS.ProcessEnv,
|
||||
): Promise<NodeJS.ProcessEnv | undefined> {
|
||||
const pathPrepend = await resolvePortableGitPathPrepend(env);
|
||||
if (pathPrepend.length === 0 && process.platform !== "win32") {
|
||||
return env;
|
||||
}
|
||||
const merged = Object.fromEntries(
|
||||
Object.entries(env ?? process.env)
|
||||
.filter(([, value]) => value != null)
|
||||
.map(([key, value]) => [key, String(value)]),
|
||||
) as Record<string, string>;
|
||||
applyPathPrepend(merged, pathPrepend);
|
||||
applyWindowsPackageInstallEnv(merged);
|
||||
return merged;
|
||||
}
|
||||
|
||||
async function tryRealpath(targetPath: string): Promise<string> {
|
||||
try {
|
||||
return await fs.realpath(targetPath);
|
||||
|
||||
@@ -156,12 +156,15 @@ describe("runGatewayUpdate", () => {
|
||||
}
|
||||
|
||||
async function runWithCommand(
|
||||
runCommand: (argv: string[]) => Promise<CommandResult>,
|
||||
runCommand: (
|
||||
argv: string[],
|
||||
options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number },
|
||||
) => Promise<CommandResult>,
|
||||
options?: { channel?: "stable" | "beta"; tag?: string; cwd?: string },
|
||||
) {
|
||||
return runGatewayUpdate({
|
||||
cwd: options?.cwd ?? tempDir,
|
||||
runCommand: async (argv, _runOptions) => runCommand(argv),
|
||||
runCommand: async (argv, runOptions) => runCommand(argv, runOptions),
|
||||
timeoutMs: 5000,
|
||||
...(options?.channel ? { channel: options.channel } : {}),
|
||||
...(options?.tag ? { tag: options.tag } : {}),
|
||||
@@ -419,6 +422,41 @@ describe("runGatewayUpdate", () => {
|
||||
expect(calls.some((call) => call === expectedInstallCommand)).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to global npm update when git is missing from PATH", async () => {
|
||||
const nodeModules = path.join(tempDir, "node_modules");
|
||||
const pkgRoot = path.join(nodeModules, "openclaw");
|
||||
await seedGlobalPackageRoot(pkgRoot);
|
||||
|
||||
const calls: string[] = [];
|
||||
const runCommand = async (argv: string[]): Promise<CommandResult> => {
|
||||
const key = argv.join(" ");
|
||||
calls.push(key);
|
||||
if (key === `git -C ${pkgRoot} rev-parse --show-toplevel`) {
|
||||
throw Object.assign(new Error("spawn git ENOENT"), { code: "ENOENT" });
|
||||
}
|
||||
if (key === "npm root -g") {
|
||||
return { stdout: nodeModules, stderr: "", code: 0 };
|
||||
}
|
||||
if (key === "pnpm root -g") {
|
||||
return { stdout: "", stderr: "", code: 1 };
|
||||
}
|
||||
if (key === "npm i -g openclaw@latest --no-fund --no-audit --loglevel=error") {
|
||||
await fs.writeFile(
|
||||
path.join(pkgRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
return { stdout: "ok", stderr: "", code: 0 };
|
||||
};
|
||||
|
||||
const result = await runWithCommand(runCommand, { cwd: pkgRoot });
|
||||
|
||||
expect(result.status).toBe("ok");
|
||||
expect(result.mode).toBe("npm");
|
||||
expect(calls).toContain("npm i -g openclaw@latest --no-fund --no-audit --loglevel=error");
|
||||
});
|
||||
|
||||
it("cleans stale npm rename dirs before global update", async () => {
|
||||
const nodeModules = path.join(tempDir, "node_modules");
|
||||
const pkgRoot = path.join(nodeModules, "openclaw");
|
||||
@@ -477,6 +515,74 @@ describe("runGatewayUpdate", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("prepends portable Git PATH for global Windows npm updates", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const localAppData = path.join(tempDir, "local-app-data");
|
||||
const portableGitMingw = path.join(
|
||||
localAppData,
|
||||
"OpenClaw",
|
||||
"deps",
|
||||
"portable-git",
|
||||
"mingw64",
|
||||
"bin",
|
||||
);
|
||||
const portableGitUsr = path.join(
|
||||
localAppData,
|
||||
"OpenClaw",
|
||||
"deps",
|
||||
"portable-git",
|
||||
"usr",
|
||||
"bin",
|
||||
);
|
||||
await fs.mkdir(portableGitMingw, { recursive: true });
|
||||
await fs.mkdir(portableGitUsr, { recursive: true });
|
||||
|
||||
const nodeModules = path.join(tempDir, "node_modules");
|
||||
const pkgRoot = path.join(nodeModules, "openclaw");
|
||||
await seedGlobalPackageRoot(pkgRoot);
|
||||
|
||||
let installEnv: NodeJS.ProcessEnv | undefined;
|
||||
const runCommand = async (
|
||||
argv: string[],
|
||||
options?: { env?: NodeJS.ProcessEnv },
|
||||
): Promise<CommandResult> => {
|
||||
const key = argv.join(" ");
|
||||
if (key === `git -C ${pkgRoot} rev-parse --show-toplevel`) {
|
||||
return { stdout: "", stderr: "not a git repository", code: 128 };
|
||||
}
|
||||
if (key === "npm root -g") {
|
||||
return { stdout: nodeModules, stderr: "", code: 0 };
|
||||
}
|
||||
if (key === "pnpm root -g") {
|
||||
return { stdout: "", stderr: "", code: 1 };
|
||||
}
|
||||
if (key === "npm i -g openclaw@latest --no-fund --no-audit --loglevel=error") {
|
||||
installEnv = options?.env;
|
||||
await fs.writeFile(
|
||||
path.join(pkgRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
return { stdout: "ok", stderr: "", code: 0 };
|
||||
};
|
||||
|
||||
await withEnvAsync({ LOCALAPPDATA: localAppData }, async () => {
|
||||
const result = await runWithCommand(runCommand, { cwd: pkgRoot });
|
||||
expect(result.status).toBe("ok");
|
||||
});
|
||||
|
||||
platformSpy.mockRestore();
|
||||
|
||||
const mergedPath = installEnv?.Path ?? installEnv?.PATH ?? "";
|
||||
expect(mergedPath.split(path.delimiter).slice(0, 2)).toEqual([
|
||||
portableGitMingw,
|
||||
portableGitUsr,
|
||||
]);
|
||||
expect(installEnv?.NPM_CONFIG_SCRIPT_SHELL).toBe("cmd.exe");
|
||||
expect(installEnv?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
|
||||
});
|
||||
|
||||
it("updates global bun installs when detected", async () => {
|
||||
const bunInstall = path.join(tempDir, "bun-install");
|
||||
await withEnvAsync({ BUN_INSTALL: bunInstall }, async () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { compareSemverStrings } from "./update-check.js";
|
||||
import {
|
||||
cleanupGlobalRenameDirs,
|
||||
createGlobalInstallEnv,
|
||||
detectGlobalInstallManagerForRoot,
|
||||
globalInstallArgs,
|
||||
globalInstallFallbackArgs,
|
||||
@@ -201,7 +202,10 @@ async function resolveGitRoot(
|
||||
for (const dir of candidates) {
|
||||
const res = await runCommand(["git", "-C", dir, "rev-parse", "--show-toplevel"], {
|
||||
timeoutMs,
|
||||
});
|
||||
}).catch(() => null);
|
||||
if (!res) {
|
||||
continue;
|
||||
}
|
||||
if (res.code === 0) {
|
||||
const root = res.stdout.trim();
|
||||
if (root) {
|
||||
@@ -870,12 +874,14 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
const tag = normalizeTag(opts.tag ?? channelToNpmTag(channel));
|
||||
const spec = `${packageName}@${tag}`;
|
||||
const steps: UpdateStepResult[] = [];
|
||||
const globalInstallEnv = await createGlobalInstallEnv();
|
||||
const updateStep = await runStep({
|
||||
runCommand,
|
||||
name: "global update",
|
||||
argv: globalInstallArgs(globalManager, spec),
|
||||
cwd: pkgRoot,
|
||||
timeoutMs,
|
||||
env: globalInstallEnv,
|
||||
progress,
|
||||
stepIndex: 0,
|
||||
totalSteps: 1,
|
||||
@@ -892,6 +898,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
argv: fallbackArgv,
|
||||
cwd: pkgRoot,
|
||||
timeoutMs,
|
||||
env: globalInstallEnv,
|
||||
progress,
|
||||
stepIndex: 0,
|
||||
totalSteps: 1,
|
||||
|
||||
Reference in New Issue
Block a user