fix: add bundled plugin deps repair command

This commit is contained in:
Peter Steinberger
2026-04-29 23:23:08 +01:00
parent 9a3a341d93
commit 4c712d3372
12 changed files with 624 additions and 18 deletions

View File

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugins/runtime-deps: add `openclaw plugins deps` inspection and repair with script-free package-manager defaults shared across plugin installers, so operators can repair missing bundled runtime deps without corrupting JSON output or blocking unrelated conflict-free deps. Thanks @vincentkoc.
- Agents/sessions: emit a terminal lifecycle backstop when embedded timeout/error turns return without `agent_end`, so Gateway sessions no longer stay stuck in `running` after failover surfaces a timeout. Fixes #74607. Thanks @millerc79.
- Gateway/diagnostics: include stuck-session reason hints and recovery skip causes in warnings, so operators can tell whether a lane is waiting on active work, queued work, or stale bookkeeping. Thanks @vincentkoc.
- Agents/Codex: bound embedded-run cleanup, trajectory flushing, and command-lane task timeouts after runtime failures, so Discord and other chat sessions return to idle instead of staying stuck in processing. Thanks @vincentkoc.

View File

@@ -1,5 +1,5 @@
---
summary: "CLI reference for `openclaw plugins` (list, install, marketplace, uninstall, enable/disable, doctor)"
summary: "CLI reference for `openclaw plugins` (list, install, marketplace, uninstall, enable/disable, deps, doctor)"
read_when:
- You want to install or manage Gateway plugins or compatible bundles
- You want to debug plugin load failures
@@ -41,6 +41,10 @@ openclaw plugins disable <id>
openclaw plugins registry
openclaw plugins registry --refresh
openclaw plugins uninstall <id>
openclaw plugins deps
openclaw plugins deps --repair
openclaw plugins deps --prune
openclaw plugins deps --json
openclaw plugins doctor
openclaw plugins update <id-or-npm-spec>
openclaw plugins update --all
@@ -252,6 +256,19 @@ Plugin install metadata is machine-managed state, not user config. Installs and
When OpenClaw sees shipped legacy `plugins.installs` records in config, it moves them into the plugin index and removes the config key; if either write fails, the config records are kept so the install metadata is not lost.
### Runtime deps
```bash
openclaw plugins deps
openclaw plugins deps --repair
openclaw plugins deps --prune
openclaw plugins deps --json
```
`plugins deps` inspects the packaged runtime dependency stage for OpenClaw-owned bundled plugins. It is not the install/update path for third-party npm or ClawHub plugins.
Use `--repair` when a packaged install reports missing bundled runtime dependencies during Gateway startup or `plugins doctor`. Repair installs only missing enabled bundled-plugin deps with lifecycle scripts disabled. Use `--prune` to remove stale unknown external runtime-dependency roots left behind by older packaged layouts.
### Uninstall
```bash

View File

@@ -255,10 +255,15 @@ dual-format packages from being partially installed as bundles.
## Runtime dependencies and cleanup
- Bundled plugin runtime dependencies ship inside the OpenClaw package under
`dist/*`. OpenClaw does **not** run `npm install` at startup for bundled
plugins; the release pipeline is responsible for shipping a complete bundled
dependency payload (see the postpublish verification rule in
- Third-party compatible bundles do not get startup `npm install` repair. They
should be installed through `openclaw plugins install` and ship everything
they need in the installed plugin directory.
- OpenClaw-owned packaged bundled plugins have a narrow exception: when one is
enabled, Gateway startup can repair missing declared runtime dependencies
before import. Operators can inspect or repair that stage with
`openclaw plugins deps`.
- The release pipeline is still responsible for shipping a complete bundled
dependency payload when possible (see the postpublish verification rule in
[Releasing](/reference/RELEASING)).
## Security

View File

@@ -517,7 +517,7 @@ For npm-sourced installs, `openclaw plugins install` runs project-local `npm ins
</Info>
<Note>
Bundled OpenClaw-owned plugins are the only startup repair exception: when a packaged install sees one enabled by plugin config, legacy channel config, or its bundled default-enabled manifest, startup installs that plugin's missing runtime dependencies before import. Third-party plugins should not rely on startup installs; keep using the explicit plugin installer.
Bundled OpenClaw-owned plugins are the only startup repair exception: when a packaged install sees one enabled by plugin config, legacy channel config, or its bundled default-enabled manifest, startup installs that plugin's missing runtime dependencies before import. Operators can inspect or repair that stage with `openclaw plugins deps`. Third-party plugins should not rely on startup installs; keep using the explicit plugin installer.
</Note>
Bundled package-level runtime deps are explicit metadata, not inferred from built JavaScript at gateway startup. If a shared OpenClaw root dependency must be available inside the external bundled-plugin runtime mirror, declare it in `openclaw.bundle.mirroredRootRuntimeDependencies` in the root package manifest.

View File

@@ -53,6 +53,13 @@ export type PluginRegistryOptions = {
refresh?: boolean;
};
export type PluginsDepsCliOptions = {
json?: boolean;
packageRoot?: string;
prune?: boolean;
repair?: boolean;
};
const quietPluginJsonLogger: PluginLogger = {
debug: () => undefined,
info: () => undefined,
@@ -252,6 +259,21 @@ export function registerPluginsCli(program: Command) {
defaultRuntime.log(lines.join("\n").trim());
});
plugins
.command("deps")
.description("Inspect or repair bundled plugin runtime dependencies")
.option("--json", "Print JSON")
.option("--package-root <path>", "OpenClaw package root to inspect")
.option("--prune", "Prune stale unknown external runtime dependency roots", false)
.option("--repair", "Install missing bundled runtime dependencies", false)
.action(async (opts: PluginsDepsCliOptions) => {
const { runPluginsDepsCommand } = await import("./plugins-deps-command.js");
await runPluginsDepsCommand({
config: getRuntimeConfig(),
options: opts,
});
});
plugins
.command("inspect")
.alias("info")

View File

@@ -0,0 +1,251 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
type RuntimeDepFixture = {
name: string;
version: string;
pluginIds: string[];
};
const mocks = vi.hoisted(() => {
const runtimeLogs: string[] = [];
const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" ");
return {
runtimeLogs,
defaultRuntime: {
log: vi.fn((...args: unknown[]) => {
runtimeLogs.push(stringifyArgs(args));
}),
error: vi.fn((...args: unknown[]) => {
runtimeLogs.push(stringifyArgs(args));
}),
writeStdout: vi.fn((value: string) => {
runtimeLogs.push(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
runtimeLogs.push(JSON.stringify(value, null, space > 0 ? space : undefined));
}),
exit: vi.fn((code: number) => {
throw new Error(`__exit__:${code}`);
}),
},
createBundledRuntimeDepsInstallSpecs: vi.fn((params: { deps: readonly RuntimeDepFixture[] }) =>
params.deps.map((dep) => `${dep.name}@${dep.version}`),
),
pruneUnknownBundledRuntimeDepsRoots: vi.fn(),
repairBundledRuntimeDepsInstallRootAsync: vi.fn(),
resolveBundledRuntimeDependencyPackageInstallRootPlan: vi.fn(),
resolveOpenClawPackageRootSync: vi.fn(),
scanBundledPluginRuntimeDeps: vi.fn(),
};
});
vi.mock("../runtime.js", () => ({
defaultRuntime: mocks.defaultRuntime,
}));
vi.mock("../infra/openclaw-root.js", () => ({
resolveOpenClawPackageRootSync: mocks.resolveOpenClawPackageRootSync,
}));
vi.mock("../plugins/bundled-runtime-deps.js", () => ({
createBundledRuntimeDepsInstallSpecs: mocks.createBundledRuntimeDepsInstallSpecs,
pruneUnknownBundledRuntimeDepsRoots: mocks.pruneUnknownBundledRuntimeDepsRoots,
repairBundledRuntimeDepsInstallRootAsync: mocks.repairBundledRuntimeDepsInstallRootAsync,
resolveBundledRuntimeDependencyPackageInstallRootPlan:
mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan,
scanBundledPluginRuntimeDeps: mocks.scanBundledPluginRuntimeDeps,
}));
const { runPluginsDepsCommand } = await import("./plugins-deps-command.js");
describe("plugins deps command", () => {
beforeEach(() => {
mocks.runtimeLogs.length = 0;
mocks.defaultRuntime.log.mockClear();
mocks.defaultRuntime.error.mockClear();
mocks.defaultRuntime.writeStdout.mockClear();
mocks.defaultRuntime.writeJson.mockClear();
mocks.defaultRuntime.exit.mockClear();
mocks.createBundledRuntimeDepsInstallSpecs.mockClear();
mocks.pruneUnknownBundledRuntimeDepsRoots.mockReset();
mocks.repairBundledRuntimeDepsInstallRootAsync.mockReset();
mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan.mockReset();
mocks.resolveOpenClawPackageRootSync.mockReset();
mocks.scanBundledPluginRuntimeDeps.mockReset();
mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan.mockReturnValue({
installRoot: "/runtime-deps",
searchRoots: ["/runtime-deps"],
external: true,
});
});
it("does not reinstall already materialized bundled runtime deps", async () => {
mocks.scanBundledPluginRuntimeDeps.mockReturnValue({
deps: [{ name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] }],
missing: [],
conflicts: [],
});
await runPluginsDepsCommand({
config: {},
options: {
json: true,
packageRoot: "/openclaw-package",
repair: true,
},
});
expect(mocks.repairBundledRuntimeDepsInstallRootAsync).not.toHaveBeenCalled();
expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
expect.objectContaining({
packageRoot: "/openclaw-package",
installSpecs: ["zod@4.0.0"],
missingSpecs: [],
repairedSpecs: [],
}),
);
});
it("repairs only when bundled runtime deps are missing", async () => {
const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
mocks.scanBundledPluginRuntimeDeps
.mockReturnValueOnce({
deps: [dep],
missing: [dep],
conflicts: [],
})
.mockReturnValueOnce({
deps: [dep],
missing: [],
conflicts: [],
});
mocks.repairBundledRuntimeDepsInstallRootAsync.mockResolvedValue({
installSpecs: ["zod@4.0.0"],
skipped: false,
});
await runPluginsDepsCommand({
config: {},
options: {
json: true,
packageRoot: "/openclaw-package",
repair: true,
},
});
expect(mocks.repairBundledRuntimeDepsInstallRootAsync).toHaveBeenCalledWith(
expect.objectContaining({
installRoot: "/runtime-deps",
installSpecs: ["zod@4.0.0"],
missingSpecs: ["zod@4.0.0"],
}),
);
expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
expect.objectContaining({
missing: [],
missingSpecs: [],
repairedSpecs: ["zod@4.0.0"],
warnings: [],
}),
);
});
it("keeps repair warnings inside JSON output", async () => {
const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
mocks.scanBundledPluginRuntimeDeps
.mockReturnValueOnce({
deps: [dep],
missing: [dep],
conflicts: [],
})
.mockReturnValueOnce({
deps: [dep],
missing: [],
conflicts: [],
});
mocks.repairBundledRuntimeDepsInstallRootAsync.mockImplementation(async (params: unknown) => {
(params as { warn: (message: string) => void }).warn("low disk space");
return {
installSpecs: ["zod@4.0.0"],
skipped: false,
};
});
await runPluginsDepsCommand({
config: {},
options: {
json: true,
packageRoot: "/openclaw-package",
repair: true,
},
});
expect(mocks.runtimeLogs).toHaveLength(1);
expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
expect.objectContaining({
missing: [],
repairedSpecs: ["zod@4.0.0"],
warnings: ["low disk space"],
}),
);
});
it("repairs missing deps even when separate deps have version conflicts", async () => {
const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
const conflict = {
name: "shared-conflict",
versions: ["1.0.0", "2.0.0"],
pluginIdsByVersion: new Map([
["1.0.0", ["openclaw-one"]],
["2.0.0", ["openclaw-two"]],
]),
};
mocks.scanBundledPluginRuntimeDeps
.mockReturnValueOnce({
deps: [dep],
missing: [dep],
conflicts: [conflict],
})
.mockReturnValueOnce({
deps: [dep],
missing: [],
conflicts: [conflict],
});
mocks.repairBundledRuntimeDepsInstallRootAsync.mockResolvedValue({
installSpecs: ["zod@4.0.0"],
skipped: false,
});
await runPluginsDepsCommand({
config: {},
options: {
json: true,
packageRoot: "/openclaw-package",
repair: true,
},
});
expect(mocks.repairBundledRuntimeDepsInstallRootAsync).toHaveBeenCalledWith(
expect.objectContaining({
installSpecs: ["zod@4.0.0"],
missingSpecs: ["zod@4.0.0"],
}),
);
expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
expect.objectContaining({
missing: [],
conflicts: [
{
name: "shared-conflict",
versions: ["1.0.0", "2.0.0"],
pluginIdsByVersion: {
"1.0.0": ["openclaw-one"],
"2.0.0": ["openclaw-two"],
},
},
],
repairedSpecs: ["zod@4.0.0"],
}),
);
});
});

View File

@@ -0,0 +1,197 @@
import path from "node:path";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
import {
createBundledRuntimeDepsInstallSpecs,
pruneUnknownBundledRuntimeDepsRoots,
repairBundledRuntimeDepsInstallRootAsync,
resolveBundledRuntimeDependencyPackageInstallRootPlan,
scanBundledPluginRuntimeDeps,
} from "../plugins/bundled-runtime-deps.js";
import { defaultRuntime } from "../runtime.js";
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
import { theme } from "../terminal/theme.js";
import { shortenHomePath } from "../utils.js";
export type PluginsDepsOptions = {
json?: boolean;
packageRoot?: string;
prune?: boolean;
repair?: boolean;
};
function resolvePackageRoot(rawPackageRoot: string | undefined): string | null {
if (rawPackageRoot?.trim()) {
return path.resolve(rawPackageRoot.trim());
}
return resolveOpenClawPackageRootSync({
argv1: process.argv[1],
cwd: process.cwd(),
moduleUrl: import.meta.url,
});
}
function formatRuntimeDepOwners(pluginIds: readonly string[]): string {
return pluginIds.length > 0 ? pluginIds.join(", ") : "-";
}
function formatRuntimeDepConflicts(
conflicts: ReturnType<typeof scanBundledPluginRuntimeDeps>["conflicts"],
) {
return conflicts.map((conflict) => ({
name: conflict.name,
versions: conflict.versions,
pluginIdsByVersion: Object.fromEntries(conflict.pluginIdsByVersion),
}));
}
function createWarningSink(params: { json?: boolean; warnings: string[] }) {
return (message: string) => {
params.warnings.push(message);
if (!params.json) {
defaultRuntime.log(theme.warn(message));
}
};
}
export async function runPluginsDepsCommand(params: {
config: OpenClawConfig;
options: PluginsDepsOptions;
}): Promise<void> {
const packageRoot = resolvePackageRoot(params.options.packageRoot);
if (!packageRoot) {
const message = "Could not resolve the OpenClaw package root for bundled plugin deps.";
if (params.options.json) {
defaultRuntime.writeJson({ ok: false, error: message });
return;
}
defaultRuntime.error(message);
return defaultRuntime.exit(1);
}
const warnings: string[] = [];
const warn = createWarningSink({ json: params.options.json, warnings });
const pruned = params.options.prune
? pruneUnknownBundledRuntimeDepsRoots({
env: process.env,
warn,
})
: undefined;
const scanRuntimeDeps = () =>
scanBundledPluginRuntimeDeps({
packageRoot,
config: params.config,
includeConfiguredChannels: true,
env: process.env,
});
let scan = scanRuntimeDeps();
const installRootPlan = resolveBundledRuntimeDependencyPackageInstallRootPlan(packageRoot, {
env: process.env,
});
let installSpecs = createBundledRuntimeDepsInstallSpecs({ deps: scan.deps });
let missingSpecs = createBundledRuntimeDepsInstallSpecs({ deps: scan.missing });
let repairedSpecs: string[] = [];
if (params.options.repair && missingSpecs.length > 0) {
const result = await repairBundledRuntimeDepsInstallRootAsync({
installRoot: installRootPlan.installRoot,
missingSpecs,
installSpecs,
env: process.env,
warn,
onProgress: (message) => {
if (!params.options.json) {
defaultRuntime.log(theme.muted(message));
}
},
});
repairedSpecs = result.installSpecs;
scan = scanRuntimeDeps();
installSpecs = createBundledRuntimeDepsInstallSpecs({ deps: scan.deps });
missingSpecs = createBundledRuntimeDepsInstallSpecs({ deps: scan.missing });
}
if (params.options.json) {
defaultRuntime.writeJson({
packageRoot,
installRoot: installRootPlan.installRoot,
installRootExternal: installRootPlan.external,
searchRoots: installRootPlan.searchRoots,
deps: scan.deps,
missing: scan.missing,
conflicts: formatRuntimeDepConflicts(scan.conflicts),
installSpecs,
missingSpecs,
repairedSpecs,
warnings,
...(pruned ? { pruned } : {}),
});
return;
}
const lines = [
theme.heading("Bundled Plugin Runtime Deps"),
`${theme.muted("Package root:")} ${shortenHomePath(packageRoot)}`,
`${theme.muted("Install root:")} ${shortenHomePath(installRootPlan.installRoot)}${
installRootPlan.external ? theme.muted(" (external)") : ""
}`,
];
if (pruned) {
lines.push(
`${theme.muted("Pruned unknown roots:")} ${pruned.removed}/${pruned.scanned}${
pruned.skippedLocked > 0 ? theme.muted(` (${pruned.skippedLocked} locked)`) : ""
}`,
);
}
if (scan.conflicts.length > 0) {
lines.push("");
lines.push(theme.error("Version conflicts:"));
for (const conflict of scan.conflicts) {
const owners = conflict.versions
.map((version) => `${version}: ${conflict.pluginIdsByVersion.get(version)?.join(", ")}`)
.join("; ");
lines.push(`- ${conflict.name}: ${owners}`);
}
}
if (scan.deps.length === 0) {
lines.push("");
lines.push(theme.muted("No packaged bundled runtime deps are required for this checkout."));
defaultRuntime.log(lines.join("\n"));
return;
}
lines.push("");
lines.push(
`${theme.muted("Status:")} ${
scan.missing.length === 0 ? theme.success("materialized") : theme.warn("missing")
}`,
);
if (repairedSpecs.length > 0) {
lines.push(`${theme.muted("Repaired:")} ${repairedSpecs.join(", ")}`);
} else if (params.options.repair && scan.conflicts.length > 0) {
lines.push(theme.warn("Repair skipped because runtime dependency versions conflict."));
}
lines.push("");
lines.push(
renderTable({
width: getTerminalTableWidth(),
columns: [
{ key: "Name", header: "Name", minWidth: 18, flex: true },
{ key: "Version", header: "Version", minWidth: 12 },
{ key: "Status", header: "Status", minWidth: 12 },
{ key: "Plugins", header: "Plugins", minWidth: 24, flex: true },
],
rows: scan.deps.map((dep) => ({
Name: dep.name,
Version: dep.version,
Status: scan.missing.some(
(missing) => missing.name === dep.name && missing.version === dep.version,
)
? theme.warn("missing")
: theme.success("ok"),
Plugins: formatRuntimeDepOwners(dep.pluginIds),
})),
}).trimEnd(),
);
defaultRuntime.log(lines.join("\n"));
}

View File

@@ -3,7 +3,7 @@ import path from "node:path";
import { runCommandWithTimeout } from "../process/exec.js";
import { fileExists } from "./archive.js";
import { assertCanonicalPathWithinBase } from "./install-safe-path.js";
import { createNpmProjectInstallEnv } from "./npm-install-env.js";
import { createSafeNpmInstallArgs, createSafeNpmInstallEnv } from "./safe-package-install.js";
const INSTALL_BASE_CHANGED_ERROR_MESSAGE = "install base directory changed during install";
const INSTALL_BASE_CHANGED_ABORT_WARNING =
@@ -261,15 +261,11 @@ export async function installPackageDir(params: {
// Verified on Blacksmith Ubuntu/Node 24/npm 11: `--silent` can make npm fail
// with empty stdout/stderr for bad specs like `workspace:^`; `--loglevel=error`
// stays quiet on success while preserving the actionable npm failure text.
["npm", "install", "--omit=dev", "--loglevel=error", "--ignore-scripts"],
["npm", ...createSafeNpmInstallArgs({ omitDev: true, loglevel: "error" })],
{
timeoutMs: Math.max(params.timeoutMs, 300_000),
cwd: stageDir,
env: {
...createNpmProjectInstallEnv(process.env),
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
NPM_CONFIG_IGNORE_SCRIPTS: "true",
},
env: createSafeNpmInstallEnv(process.env),
},
);
} finally {

View File

@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { createSafeNpmInstallArgs, createSafeNpmInstallEnv } from "./safe-package-install.js";
describe("safe npm install helpers", () => {
it("builds script-free npm install args", () => {
expect(
createSafeNpmInstallArgs({
omitDev: true,
loglevel: "error",
noAudit: true,
noFund: true,
}),
).toEqual([
"install",
"--omit=dev",
"--loglevel=error",
"--ignore-scripts",
"--no-audit",
"--no-fund",
]);
});
it("forces project-local script-free npm install env", () => {
expect(
createSafeNpmInstallEnv(
{
PATH: "/usr/bin:/bin",
npm_config_global: "true",
npm_config_location: "global",
npm_config_package_lock: "true",
},
{
cacheDir: "/tmp/openclaw-npm-cache",
legacyPeerDeps: true,
packageLock: false,
quiet: true,
},
),
).toEqual({
PATH: "/usr/bin:/bin",
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
NPM_CONFIG_IGNORE_SCRIPTS: "true",
npm_config_audit: "false",
npm_config_cache: "/tmp/openclaw-npm-cache",
npm_config_dry_run: "false",
npm_config_fetch_retries: "5",
npm_config_fetch_retry_maxtimeout: "120000",
npm_config_fetch_retry_mintimeout: "10000",
npm_config_fetch_timeout: "300000",
npm_config_fund: "false",
npm_config_global: "false",
npm_config_legacy_peer_deps: "true",
npm_config_location: "project",
npm_config_loglevel: "error",
npm_config_package_lock: "false",
npm_config_progress: "false",
npm_config_save: "false",
npm_config_yes: "true",
});
});
});

View File

@@ -0,0 +1,49 @@
import type { NpmProjectInstallEnvOptions } from "./npm-install-env.js";
import { createNpmProjectInstallEnv } from "./npm-install-env.js";
export type SafeNpmInstallEnvOptions = NpmProjectInstallEnvOptions & {
legacyPeerDeps?: boolean;
packageLock?: boolean;
quiet?: boolean;
};
export type SafeNpmInstallArgsOptions = {
loglevel?: "error" | "silent";
noAudit?: boolean;
noFund?: boolean;
omitDev?: boolean;
};
export function createSafeNpmInstallEnv(
env: NodeJS.ProcessEnv,
options: SafeNpmInstallEnvOptions = {},
): NodeJS.ProcessEnv {
const nextEnv: NodeJS.ProcessEnv = {
...createNpmProjectInstallEnv(env, options),
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
NPM_CONFIG_IGNORE_SCRIPTS: "true",
npm_config_audit: "false",
npm_config_fund: "false",
npm_config_package_lock: options.packageLock === true ? "true" : "false",
...(options.legacyPeerDeps ? { npm_config_legacy_peer_deps: "true" } : {}),
};
if (options.quiet) {
Object.assign(nextEnv, {
npm_config_loglevel: "error",
npm_config_progress: "false",
npm_config_yes: "true",
});
}
return nextEnv;
}
export function createSafeNpmInstallArgs(options: SafeNpmInstallArgsOptions = {}): string[] {
return [
"install",
...(options.omitDev ? ["--omit=dev"] : []),
...(options.loglevel ? [`--loglevel=${options.loglevel}`] : []),
"--ignore-scripts",
...(options.noAudit ? ["--no-audit"] : []),
...(options.noFund ? ["--no-fund"] : []),
];
}

View File

@@ -1,6 +1,9 @@
import fs from "node:fs";
import path from "node:path";
import { createNpmProjectInstallEnv } from "../infra/npm-install-env.js";
import {
createSafeNpmInstallArgs,
createSafeNpmInstallEnv,
} from "../infra/safe-package-install.js";
export type BundledRuntimeDepsNpmRunner = {
command: string;
@@ -21,11 +24,13 @@ export function createBundledRuntimeDepsInstallEnv(
options: { cacheDir?: string } = {},
): NodeJS.ProcessEnv {
const nextEnv: NodeJS.ProcessEnv = {
...createNpmProjectInstallEnv(env, options),
...createSafeNpmInstallEnv(env, {
...options,
legacyPeerDeps: true,
packageLock: true,
}),
npm_config_audit: "false",
npm_config_fund: "false",
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "true",
};
for (const key of Object.keys(nextEnv)) {
if (key.toLowerCase() === NPM_EXECPATH_ENV_KEY) {
@@ -36,7 +41,7 @@ export function createBundledRuntimeDepsInstallEnv(
}
export function createBundledRuntimeDepsInstallArgs(): string[] {
return ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--omit=dev"];
return [...createSafeNpmInstallArgs({ noAudit: true, noFund: true }), "--omit=dev"];
}
function createBundledRuntimeDepsPnpmInstallArgs(params: { storeDir: string }): string[] {

View File

@@ -125,6 +125,8 @@ describe("resolveBundledRuntimeDepsNpmRunner", () => {
{ cacheDir: "/opt/openclaw/runtime-cache" },
),
).toEqual({
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
NPM_CONFIG_IGNORE_SCRIPTS: "true",
PATH: "/usr/bin:/bin",
npm_config_audit: "false",
npm_config_cache: "/opt/openclaw/runtime-cache",