mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
fix(plugins): repair missing required platform packages
This commit is contained in:
@@ -1278,6 +1278,7 @@ Important examples:
|
||||
| `openclaw.compat.pluginApi` | Minimum OpenClaw plugin API range required by this package, using a semver floor like `>=2026.5.27`. |
|
||||
| `openclaw.install.expectedIntegrity` | Expected npm dist integrity string such as `sha512-...`; install and update flows verify the fetched artifact against it. |
|
||||
| `openclaw.install.allowInvalidConfigRecovery` | Allows a narrow bundled-plugin reinstall recovery path when config is invalid. |
|
||||
| `openclaw.install.requiredPlatformPackages` | npm package aliases that must materialize when their lockfile platform constraints match the current host. |
|
||||
| `openclaw.startup.deferConfiguredChannelFullLoadUntilAfterListen` | Lets setup-runtime channel surfaces load before listen, then defers the full configured channel plugin until post-listen activation. |
|
||||
|
||||
Manifest metadata decides which provider/channel/setup choices appear in
|
||||
@@ -1290,6 +1291,13 @@ registry loading for non-bundled plugin sources. Invalid values are rejected;
|
||||
newer-but-valid values skip external plugins on older hosts. Bundled source
|
||||
plugins are assumed to be co-versioned with the host checkout.
|
||||
|
||||
`openclaw.install.requiredPlatformPackages` is for npm packages that expose
|
||||
required native binaries through optional, platform-specific aliases. List the
|
||||
bare npm package name for every supported platform alias. During npm install,
|
||||
OpenClaw verifies only the declared alias whose lockfile constraints match the
|
||||
current host. If npm reports success but omits that alias, OpenClaw retries once
|
||||
with a fresh cache and rolls back the install if the alias is still missing.
|
||||
|
||||
`openclaw.compat.pluginApi` is enforced during package install for non-bundled
|
||||
plugin sources. Use it for the OpenClaw plugin SDK/runtime API floor that the
|
||||
package was built against. It can be stricter than `minHostVersion` when a
|
||||
|
||||
@@ -163,6 +163,7 @@ Example:
|
||||
| `minHostVersion` | `string` | Minimum supported OpenClaw version in the form `>=x.y.z` or `>=x.y.z-prerelease`. |
|
||||
| `expectedIntegrity` | `string` | Expected npm dist integrity string, usually `sha512-...`, for pinned installs. |
|
||||
| `allowInvalidConfigRecovery` | `boolean` | Lets bundled-plugin reinstall flows recover from specific stale-config failures. |
|
||||
| `requiredPlatformPackages` | `string[]` | Required platform-specific npm aliases verified during npm install. |
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Onboarding behavior">
|
||||
|
||||
@@ -23,7 +23,15 @@
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/codex",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.5.1-beta.1"
|
||||
"minHostVersion": ">=2026.5.1-beta.1",
|
||||
"requiredPlatformPackages": [
|
||||
"@openai/codex-linux-x64",
|
||||
"@openai/codex-linux-arm64",
|
||||
"@openai/codex-darwin-x64",
|
||||
"@openai/codex-darwin-arm64",
|
||||
"@openai/codex-win32-x64",
|
||||
"@openai/codex-win32-arm64"
|
||||
]
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.8-beta.2"
|
||||
|
||||
@@ -6,6 +6,11 @@ import { MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION } from "./app-server/version.j
|
||||
type CodexPackageManifest = {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
install?: {
|
||||
requiredPlatformPackages?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
describe("codex package manifest", () => {
|
||||
@@ -18,5 +23,13 @@ describe("codex package manifest", () => {
|
||||
expect(packageJson.dependencies?.["@openai/codex"]).toBe(
|
||||
MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION,
|
||||
);
|
||||
expect(packageJson.openclaw?.install?.requiredPlatformPackages).toEqual([
|
||||
"@openai/codex-linux-x64",
|
||||
"@openai/codex-linux-arm64",
|
||||
"@openai/codex-darwin-x64",
|
||||
"@openai/codex-darwin-arm64",
|
||||
"@openai/codex-win32-x64",
|
||||
"@openai/codex-win32-arm64",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { CommandOptions } from "../process/exec.js";
|
||||
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import {
|
||||
listMissingRequiredPlatformPackages,
|
||||
repairManagedNpmRootOpenClawPeer,
|
||||
removeManagedNpmRootDependency,
|
||||
readManagedNpmRootInstalledDependency,
|
||||
@@ -99,6 +100,67 @@ function requireCommandOptions(
|
||||
}
|
||||
|
||||
describe("managed npm root", () => {
|
||||
it("finds explicitly required optional packages for the current platform", async () => {
|
||||
const npmRoot = await makeTempRoot();
|
||||
const matchingPackage = "@vendor/tool-platform";
|
||||
const scriptedPackage = "@vendor/tool-scripted";
|
||||
const foreignPackage = "@vendor/tool-foreign";
|
||||
const unconstrainedPackage = "@vendor/tool-optional";
|
||||
const unlistedPackage = "@vendor/tool-unlisted";
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "package-lock.json"),
|
||||
`${JSON.stringify({
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": {},
|
||||
[`node_modules/${matchingPackage}`]: {
|
||||
optional: true,
|
||||
os: [process.platform],
|
||||
cpu: [process.arch],
|
||||
},
|
||||
[`node_modules/${scriptedPackage}`]: {
|
||||
optional: true,
|
||||
hasInstallScript: true,
|
||||
os: [process.platform],
|
||||
cpu: [process.arch],
|
||||
},
|
||||
[`node_modules/${foreignPackage}`]: {
|
||||
optional: true,
|
||||
os: [`not-${process.platform}`],
|
||||
cpu: [process.arch],
|
||||
},
|
||||
[`node_modules/${unconstrainedPackage}`]: {
|
||||
optional: true,
|
||||
},
|
||||
[`node_modules/${unlistedPackage}`]: {
|
||||
optional: true,
|
||||
os: [process.platform],
|
||||
cpu: [process.arch],
|
||||
},
|
||||
},
|
||||
})}\n`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
listMissingRequiredPlatformPackages({
|
||||
npmRoot,
|
||||
requiredPackageNames: [
|
||||
matchingPackage,
|
||||
scriptedPackage,
|
||||
foreignPackage,
|
||||
unconstrainedPackage,
|
||||
],
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
[matchingPackage, scriptedPackage]
|
||||
.map((name) => ({
|
||||
name,
|
||||
packagePath: path.join(npmRoot, "node_modules", ...name.split("/")),
|
||||
}))
|
||||
.toSorted((left, right) => left.packagePath.localeCompare(right.packagePath)),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps existing plugin dependencies when adding another managed plugin", async () => {
|
||||
const npmRoot = await makeTempRoot();
|
||||
await fs.writeFile(
|
||||
|
||||
@@ -374,13 +374,11 @@ function isUnsupportedOptionalLockPackage(value: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function readLockPackageName(location: string, value: unknown): string | undefined {
|
||||
if (isRecord(value)) {
|
||||
const packageName = readOptionalString(value.name);
|
||||
if (packageName) {
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
function hasNpmPlatformConstraint(value: Record<string, unknown>): boolean {
|
||||
return value.os !== undefined || value.cpu !== undefined || value.libc !== undefined;
|
||||
}
|
||||
|
||||
function readLockPackageLocationName(location: string): string | undefined {
|
||||
const parts = location.split("/");
|
||||
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
||||
if (parts[index] !== "node_modules") {
|
||||
@@ -399,10 +397,79 @@ function readLockPackageName(location: string, value: unknown): string | undefin
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readLockPackageName(location: string, value: unknown): string | undefined {
|
||||
if (isRecord(value)) {
|
||||
const packageName = readOptionalString(value.name);
|
||||
if (packageName) {
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
return readLockPackageLocationName(location);
|
||||
}
|
||||
|
||||
function resolveManagedNpmLockPackagePath(params: {
|
||||
npmRoot: string;
|
||||
location: string;
|
||||
}): string | undefined {
|
||||
const npmRoot = path.resolve(params.npmRoot);
|
||||
const packagePath = path.resolve(npmRoot, ...params.location.split("/"));
|
||||
const relativePath = path.relative(npmRoot, packagePath);
|
||||
if (
|
||||
!relativePath ||
|
||||
relativePath === ".." ||
|
||||
relativePath.startsWith(`..${path.sep}`) ||
|
||||
path.isAbsolute(relativePath)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
function isTopLevelLockPackageLocation(location: string): boolean {
|
||||
return location.split("/").filter((part) => part === "node_modules").length === 1;
|
||||
}
|
||||
|
||||
export type MissingRequiredPlatformPackage = {
|
||||
name: string;
|
||||
packagePath: string;
|
||||
};
|
||||
|
||||
/** Lists explicitly required current-platform packages that npm recorded but did not materialize. */
|
||||
export async function listMissingRequiredPlatformPackages(params: {
|
||||
npmRoot: string;
|
||||
requiredPackageNames: ReadonlySet<string> | readonly string[];
|
||||
}): Promise<MissingRequiredPlatformPackage[]> {
|
||||
const requiredPackageNames = new Set(params.requiredPackageNames);
|
||||
if (requiredPackageNames.size === 0) {
|
||||
return [];
|
||||
}
|
||||
const lockPath = path.join(params.npmRoot, "package-lock.json");
|
||||
const parsed = await readJson<unknown>(lockPath);
|
||||
if (!isRecord(parsed) || !isRecord(parsed.packages)) {
|
||||
return [];
|
||||
}
|
||||
const missing: MissingRequiredPlatformPackage[] = [];
|
||||
for (const [location, value] of Object.entries(parsed.packages)) {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
value.optional !== true ||
|
||||
!hasNpmPlatformConstraint(value) ||
|
||||
isUnsupportedOptionalLockPackage(value)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const name = readLockPackageLocationName(location);
|
||||
const packagePath = resolveManagedNpmLockPackagePath({ npmRoot: params.npmRoot, location });
|
||||
if (!name || !requiredPackageNames.has(name) || !isSafePackageName(name) || !packagePath) {
|
||||
continue;
|
||||
}
|
||||
if (!(await pathExists(packagePath))) {
|
||||
missing.push({ name, packagePath });
|
||||
}
|
||||
}
|
||||
return missing.toSorted((left, right) => left.packagePath.localeCompare(right.packagePath));
|
||||
}
|
||||
|
||||
function findLockPackageVersion(params: {
|
||||
lockfile: ManagedNpmRootLockfile;
|
||||
packageName: string;
|
||||
|
||||
@@ -287,6 +287,30 @@ function writeNpmRootPackageLock(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function writeMissingCurrentPlatformOptionalPackage(params: {
|
||||
npmRoot: string;
|
||||
packageName: string;
|
||||
packageLocation: string;
|
||||
}): void {
|
||||
const lockPath = path.join(params.npmRoot, "package-lock.json");
|
||||
const lockfile = JSON.parse(fs.readFileSync(lockPath, "utf8")) as {
|
||||
packages?: Record<string, unknown>;
|
||||
};
|
||||
lockfile.packages ??= {};
|
||||
lockfile.packages[params.packageLocation] = {
|
||||
name: params.packageName,
|
||||
version: "1.0.0-platform",
|
||||
optional: true,
|
||||
os: [process.platform],
|
||||
cpu: [process.arch],
|
||||
};
|
||||
fs.writeFileSync(lockPath, `${JSON.stringify(lockfile, null, 2)}\n`, "utf8");
|
||||
fs.rmSync(path.join(params.npmRoot, ...params.packageLocation.split("/")), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
function readTextFileTree(dir: string, rootDir = dir): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
@@ -950,6 +974,139 @@ describe("installPluginFromNpmSpec", () => {
|
||||
expect(fs.existsSync(resolveTestPluginPackageDir(npmRoot, "missing-lock-plugin"))).toBe(false);
|
||||
});
|
||||
|
||||
it("repairs omitted current-platform packages with a fresh npm cache", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
const packageName = "@openclaw/codex-fixture";
|
||||
const platformPackage = "@vendor/codex-platform";
|
||||
const npmProjectRoot = resolvePluginNpmProjectDir({ npmDir: npmRoot, packageName });
|
||||
const platformPackageLocation = path.posix.join(
|
||||
"node_modules",
|
||||
packageName,
|
||||
"node_modules",
|
||||
platformPackage,
|
||||
);
|
||||
const warnings: string[] = [];
|
||||
mockNpmViewAndInstall({
|
||||
spec: `${packageName}@1.0.0`,
|
||||
packageName,
|
||||
version: "1.0.0",
|
||||
pluginId: "codex-fixture",
|
||||
npmRoot,
|
||||
expectedDependencySpec: "1.0.0",
|
||||
openclaw: {
|
||||
extensions: ["./dist/index.js"],
|
||||
install: { requiredPlatformPackages: [platformPackage] },
|
||||
},
|
||||
});
|
||||
const delegate = runCommandWithTimeoutMock.getMockImplementation();
|
||||
if (!delegate) {
|
||||
throw new Error("expected npm mock implementation");
|
||||
}
|
||||
let managedInstallAttempts = 0;
|
||||
let repairCacheDir = "";
|
||||
runCommandWithTimeoutMock.mockImplementation(
|
||||
async (argv: string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv }) => {
|
||||
const result = await delegate(argv, options);
|
||||
if (isManagedNpmInstallCommand(argv) && options?.cwd === npmProjectRoot) {
|
||||
managedInstallAttempts += 1;
|
||||
if (managedInstallAttempts === 1) {
|
||||
writeMissingCurrentPlatformOptionalPackage({
|
||||
npmRoot: npmProjectRoot,
|
||||
packageName: platformPackage,
|
||||
packageLocation: platformPackageLocation,
|
||||
});
|
||||
} else {
|
||||
repairCacheDir = options.env?.npm_config_cache ?? "";
|
||||
const packageDir = path.join(npmProjectRoot, ...platformPackageLocation.split("/"));
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(packageDir, "package.json"),
|
||||
JSON.stringify({ name: platformPackage, version: "1.0.0-platform" }),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: `${packageName}@1.0.0`,
|
||||
npmDir: npmRoot,
|
||||
logger: { info: () => {}, warn: (message) => warnings.push(message) },
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(managedInstallAttempts).toBe(2);
|
||||
expect(repairCacheDir).toContain("openclaw-npm-cache-");
|
||||
expect(fs.existsSync(repairCacheDir)).toBe(false);
|
||||
expect(warnings).toContain(
|
||||
`npm omitted current-platform package(s) ${platformPackage}; retrying once with a fresh cache.`,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects installs that still omit current-platform packages after repair", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
const packageName = "@openclaw/codex-fixture";
|
||||
const platformPackage = "@vendor/codex-platform";
|
||||
const npmProjectRoot = resolvePluginNpmProjectDir({ npmDir: npmRoot, packageName });
|
||||
const platformPackageLocation = path.posix.join(
|
||||
"node_modules",
|
||||
packageName,
|
||||
"node_modules",
|
||||
platformPackage,
|
||||
);
|
||||
mockNpmViewAndInstall({
|
||||
spec: `${packageName}@1.0.0`,
|
||||
packageName,
|
||||
version: "1.0.0",
|
||||
pluginId: "codex-fixture",
|
||||
npmRoot,
|
||||
expectedDependencySpec: "1.0.0",
|
||||
openclaw: {
|
||||
extensions: ["./dist/index.js"],
|
||||
install: { requiredPlatformPackages: [platformPackage] },
|
||||
},
|
||||
});
|
||||
const delegate = runCommandWithTimeoutMock.getMockImplementation();
|
||||
if (!delegate) {
|
||||
throw new Error("expected npm mock implementation");
|
||||
}
|
||||
let managedInstallAttempts = 0;
|
||||
runCommandWithTimeoutMock.mockImplementation(
|
||||
async (argv: string[], options?: { cwd?: string }) => {
|
||||
const result = await delegate(argv, options);
|
||||
if (isManagedNpmInstallCommand(argv) && options?.cwd === npmProjectRoot) {
|
||||
managedInstallAttempts += 1;
|
||||
writeMissingCurrentPlatformOptionalPackage({
|
||||
npmRoot: npmProjectRoot,
|
||||
packageName: platformPackage,
|
||||
packageLocation: platformPackageLocation,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: `${packageName}@1.0.0`,
|
||||
npmDir: npmRoot,
|
||||
logger: { info: () => {}, warn: () => {} },
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(managedInstallAttempts).toBe(2);
|
||||
expect(result.error).toContain(
|
||||
`npm install reported success but omitted required current-platform package(s): ${platformPackage}`,
|
||||
);
|
||||
expect(fs.existsSync(resolveTestPluginPackageDir(npmRoot, packageName))).toBe(false);
|
||||
});
|
||||
|
||||
it("quarantines and rebuilds a corrupt managed npm project after npm from-argument failures", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { resolveNpmIntegrityDriftWithDefaultMessage } from "../infra/npm-integrity.js";
|
||||
import {
|
||||
type ManagedNpmRootPeerDependencySnapshot,
|
||||
listMissingRequiredPlatformPackages,
|
||||
readManagedNpmRootInstalledDependency,
|
||||
readManagedNpmRootPeerDependencySnapshot,
|
||||
readOpenClawManagedNpmRootOverrides,
|
||||
@@ -1070,6 +1071,41 @@ function resolveManagedNpmRootPackageDir(npmRoot: string, packageName: string):
|
||||
return path.join(npmRoot, "node_modules", ...packageName.split("/"));
|
||||
}
|
||||
|
||||
function resolveRequiredPlatformPackageNames(
|
||||
packageMetadata?: OpenClawPackageManifest,
|
||||
): { ok: true; packageNames: string[] } | { ok: false; error: string } {
|
||||
const raw = packageMetadata?.install?.requiredPlatformPackages as unknown;
|
||||
if (raw === undefined) {
|
||||
return { ok: true, packageNames: [] };
|
||||
}
|
||||
if (!Array.isArray(raw)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "package.json openclaw.install.requiredPlatformPackages must be an array",
|
||||
};
|
||||
}
|
||||
const packageNames = new Set<string>();
|
||||
for (const value of raw) {
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
"package.json openclaw.install.requiredPlatformPackages must contain only npm package names",
|
||||
};
|
||||
}
|
||||
const specError = validateRegistryNpmSpec(value);
|
||||
const parsed = parseRegistryNpmSpec(value);
|
||||
if (specError || !parsed || parsed.selectorKind !== "none") {
|
||||
return {
|
||||
ok: false,
|
||||
error: `package.json openclaw.install.requiredPlatformPackages contains invalid package name: ${value}`,
|
||||
};
|
||||
}
|
||||
packageNames.add(parsed.name);
|
||||
}
|
||||
return { ok: true, packageNames: [...packageNames] };
|
||||
}
|
||||
|
||||
async function listNewManagedNpmRootPackageDirs(params: {
|
||||
beforeInstallPackageNames: Set<string>;
|
||||
npmRoot: string;
|
||||
@@ -1407,6 +1443,93 @@ async function installPluginFromManagedNpmRoot(
|
||||
"npm install could not settle managed peer dependencies after 10 sync passes; refusing to leave a partially reconciled plugin dependency tree.",
|
||||
});
|
||||
}
|
||||
const packageManifestResult = await readOptionalPackageManifest({
|
||||
runtime,
|
||||
packageDir: installRoot,
|
||||
});
|
||||
if (!packageManifestResult.ok) {
|
||||
return await rollbackFailedManagedNpmInstall(packageManifestResult);
|
||||
}
|
||||
const requiredPlatformPackageNames = resolveRequiredPlatformPackageNames(
|
||||
packageManifestResult.manifest
|
||||
? runtime.getPackageManifestMetadata(packageManifestResult.manifest)
|
||||
: undefined,
|
||||
);
|
||||
if (!requiredPlatformPackageNames.ok) {
|
||||
return await rollbackFailedManagedNpmInstall({
|
||||
ok: false,
|
||||
error: requiredPlatformPackageNames.error,
|
||||
});
|
||||
}
|
||||
let omittedPlatformPackages: Awaited<ReturnType<typeof listMissingRequiredPlatformPackages>>;
|
||||
try {
|
||||
omittedPlatformPackages = await listMissingRequiredPlatformPackages({
|
||||
npmRoot,
|
||||
requiredPackageNames: requiredPlatformPackageNames.packageNames,
|
||||
});
|
||||
} catch (error) {
|
||||
return await rollbackFailedManagedNpmInstall({
|
||||
ok: false,
|
||||
error: `Failed to verify platform-specific npm dependencies for ${params.packageName}: ${String(error)}`,
|
||||
});
|
||||
}
|
||||
if (omittedPlatformPackages.length > 0) {
|
||||
const omittedPlatformPackageNames = omittedPlatformPackages.map((entry) => entry.name);
|
||||
logger.warn?.(
|
||||
`npm omitted current-platform package(s) ${omittedPlatformPackageNames.join(", ")}; retrying once with a fresh cache.`,
|
||||
);
|
||||
let freshCacheDir: string | undefined;
|
||||
try {
|
||||
freshCacheDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-npm-cache-"));
|
||||
install = await runCommandWithTimeout(npmInstallArgs, {
|
||||
...npmInstallOptions,
|
||||
env: {
|
||||
...npmInstallOptions.env,
|
||||
NPM_CONFIG_CACHE: freshCacheDir,
|
||||
npm_config_cache: freshCacheDir,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return await rollbackFailedManagedNpmInstall({
|
||||
ok: false,
|
||||
error: `Failed to repair omitted current-platform package(s) ${omittedPlatformPackageNames.join(", ")}: ${String(error)}`,
|
||||
});
|
||||
} finally {
|
||||
if (freshCacheDir) {
|
||||
try {
|
||||
await fs.rm(freshCacheDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
logger.warn?.(
|
||||
`Failed to remove temporary npm cache ${freshCacheDir}: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (install.code !== 0) {
|
||||
return await rollbackFailedManagedNpmInstall({
|
||||
ok: false,
|
||||
error: `npm install failed while repairing omitted current-platform package(s) ${omittedPlatformPackageNames.join(", ")}: ${formatNpmCommandFailureOutput(install)}`,
|
||||
});
|
||||
}
|
||||
let stillOmittedPlatformPackages: typeof omittedPlatformPackages;
|
||||
try {
|
||||
stillOmittedPlatformPackages = await listMissingRequiredPlatformPackages({
|
||||
npmRoot,
|
||||
requiredPackageNames: requiredPlatformPackageNames.packageNames,
|
||||
});
|
||||
} catch (error) {
|
||||
return await rollbackFailedManagedNpmInstall({
|
||||
ok: false,
|
||||
error: `Failed to verify repaired platform-specific npm dependencies for ${params.packageName}: ${String(error)}`,
|
||||
});
|
||||
}
|
||||
if (stillOmittedPlatformPackages.length > 0) {
|
||||
return await rollbackFailedManagedNpmInstall({
|
||||
ok: false,
|
||||
error: `npm install reported success but omitted required current-platform package(s): ${stillOmittedPlatformPackages.map((entry) => entry.name).join(", ")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (params.packageName !== "openclaw") {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({
|
||||
npmRoot,
|
||||
|
||||
@@ -1953,6 +1953,7 @@ export type PluginPackageInstall = {
|
||||
minHostVersion?: string;
|
||||
expectedIntegrity?: string;
|
||||
allowInvalidConfigRecovery?: boolean;
|
||||
requiredPlatformPackages?: string[];
|
||||
};
|
||||
|
||||
export type OpenClawPackageStartup = {
|
||||
|
||||
Reference in New Issue
Block a user