Plugins: add install --force overwrite flag (#60544)

Merged via squash.

Prepared head SHA: 28ae50b615
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana 2026-04-03 18:09:14 -04:00 committed by GitHub
parent 3fd29e549d
commit 9004ef65df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 359 additions and 20 deletions

View File

@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
- Tests/runtime: trim local unit-test import/runtime fan-out across browser, WhatsApp, cron, task, and reply flows so owner suites start faster with lower shared-worker overhead while preserving the same focused behavior coverage. (#60249) Thanks @shakkernerd.
- Tests/secrets runtime: restore split secrets suite cache and env isolation cleanup so broader runs do not leak stale plugin or provider snapshot state. (#60395) Thanks @shakkernerd.
- Providers/Ollama: add bundled Ollama Web Search provider for key-free web_search via your configured Ollama host and `ollama signin`. (#59318) Thanks @BruceMacD.
- Plugins/install: add `openclaw plugins install --force` to overwrite existing plugin and hook-pack install targets without using the dangerous-code override flag. (#60544) Thanks @gumadeiras.
### Fixes

View File

@ -308,7 +308,7 @@ Manage extensions and their config:
- `openclaw plugins list` — discover plugins (use `--json` for machine output).
- `openclaw plugins inspect <id>` — show details for a plugin (`info` is an alias).
- `openclaw plugins install <path|.tgz|npm-spec|plugin@marketplace>` — install a plugin (or add a plugin path to `plugins.load.paths`).
- `openclaw plugins install <path|.tgz|npm-spec|plugin@marketplace>` — install a plugin (or add a plugin path to `plugins.load.paths`; use `--force` to overwrite an existing install target).
- `openclaw plugins marketplace list <marketplace>` — list marketplace entries before install.
- `openclaw plugins enable <id>` / `disable <id>` — toggle `plugins.entries.<id>.enabled`.
- `openclaw plugins doctor` — report plugin load errors.

View File

@ -48,6 +48,7 @@ capabilities.
```bash
openclaw plugins install <package> # ClawHub first, then npm
openclaw plugins install clawhub:<package> # ClawHub only
openclaw plugins install <package> --force # overwrite existing install
openclaw plugins install <package> --pin # pin version
openclaw plugins install <package> --dangerously-force-unsafe-install
openclaw plugins install <path> # local path
@ -58,6 +59,10 @@ openclaw plugins install <plugin> --marketplace <name> # marketplace (explicit)
Bare package names are checked against ClawHub first, then npm. Security note:
treat plugin installs like running code. Prefer pinned versions.
`--force` reuses the existing install target and overwrites an already-installed
plugin or hook pack in place. Use it when you are intentionally reinstalling
the same id from a new local path, archive, ClawHub package, or npm artifact.
`--dangerously-force-unsafe-install` is a break-glass option for false positives
in the built-in dangerous-code scanner. It allows the install to continue even
when the built-in scanner reports `critical` findings, but it does **not**
@ -157,6 +162,9 @@ Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`):
openclaw plugins install -l ./my-plugin
```
`--force` is not supported with `--link` because linked installs reuse the
source path instead of copying over a managed install target.
Use `--pin` on npm installs to save the resolved exact spec (`name@version`) in
`plugins.installs` while keeping the default behavior unpinned.

View File

@ -209,6 +209,7 @@ openclaw plugins doctor # diagnostics
openclaw plugins install <package> # install (ClawHub first, then npm)
openclaw plugins install clawhub:<pkg> # install from ClawHub only
openclaw plugins install <spec> --force # overwrite existing install
openclaw plugins install <path> # install from local path
openclaw plugins install -l <path> # link (no copy) for dev
openclaw plugins install <spec> --dangerously-force-unsafe-install
@ -220,6 +221,10 @@ openclaw plugins enable <id>
openclaw plugins disable <id>
```
`--force` overwrites an existing installed plugin or hook pack in place.
It is not supported with `--link`, which reuses the source path instead of
copying over a managed install target.
`--dangerously-force-unsafe-install` is a break-glass override for false
positives from the built-in dangerous-code scanner. It allows plugin installs
and plugin updates to continue past built-in `critical` findings, but it still

View File

@ -86,6 +86,21 @@ describe("plugins cli install", () => {
resetPluginsCliTestState();
});
it("shows the force overwrite option in install help", async () => {
const { Command } = await import("commander");
const { registerPluginsCli } = await import("./plugins-cli.js");
const program = new Command();
registerPluginsCli(program);
const pluginsCommand = program.commands.find((command) => command.name() === "plugins");
const installCommand = pluginsCommand?.commands.find((command) => command.name() === "install");
const helpText = installCommand?.helpInformation() ?? "";
expect(helpText).toContain("--force");
expect(helpText).toContain("Overwrite an existing installed plugin or");
expect(helpText).toContain("hook pack");
});
it("exits when --marketplace is combined with --link", async () => {
await expect(
runPluginsCommand(["plugins", "install", "alpha", "--marketplace", "local/repo", "--link"]),
@ -95,6 +110,16 @@ describe("plugins cli install", () => {
expect(installPluginFromMarketplace).not.toHaveBeenCalled();
});
it("exits when --force is combined with --link", async () => {
await expect(
runPluginsCommand(["plugins", "install", "./plugin", "--link", "--force"]),
).rejects.toThrow("__exit__:1");
expect(runtimeErrors.at(-1)).toContain("`--force` is not supported with `--link`.");
expect(installPluginFromMarketplace).not.toHaveBeenCalled();
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
});
it("exits when marketplace install fails", async () => {
await expect(
runPluginsCommand(["plugins", "install", "alpha", "--marketplace", "local/repo"]),
@ -197,6 +222,20 @@ describe("plugins cli install", () => {
expect(runtimeLogs.some((line) => line.includes("Installed plugin: alpha"))).toBe(true);
});
it("passes force through as overwrite mode for marketplace installs", async () => {
await expect(
runPluginsCommand(["plugins", "install", "alpha", "--marketplace", "local/repo", "--force"]),
).rejects.toThrow("__exit__:1");
expect(installPluginFromMarketplace).toHaveBeenCalledWith(
expect.objectContaining({
marketplace: "local/repo",
plugin: "alpha",
mode: "update",
}),
);
});
it("installs ClawHub plugins and persists source metadata", async () => {
const cfg = {
plugins: {
@ -256,6 +295,41 @@ describe("plugins cli install", () => {
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
});
it("passes force through as overwrite mode for ClawHub installs", async () => {
const cfg = {
plugins: {
entries: {},
},
} as OpenClawConfig;
const enabledCfg = createEnabledPluginConfig("demo");
loadConfig.mockReturnValue(cfg);
parseClawHubPluginSpec.mockReturnValue({ name: "demo" });
installPluginFromClawHub.mockResolvedValue(
createClawHubInstallResult({
pluginId: "demo",
packageName: "demo",
version: "1.2.3",
channel: "official",
}),
);
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
recordPluginInstall.mockReturnValue(enabledCfg);
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "clawhub:demo", "--force"]);
expect(installPluginFromClawHub).toHaveBeenCalledWith(
expect.objectContaining({
spec: "clawhub:demo",
mode: "update",
}),
);
});
it("prefers ClawHub before npm for bare plugin specs", async () => {
const cfg = {
plugins: {
@ -417,6 +491,48 @@ describe("plugins cli install", () => {
);
});
it("passes force through as overwrite mode for npm installs", async () => {
const cfg = {
plugins: {
entries: {},
},
} as OpenClawConfig;
const enabledCfg = createEnabledPluginConfig("demo");
loadConfig.mockReturnValue(cfg);
installPluginFromClawHub.mockResolvedValue({
ok: false,
error: "ClawHub /api/v1/packages/demo failed (404): Package not found",
code: "package_not_found",
});
installPluginFromNpmSpec.mockResolvedValue({
ok: true,
pluginId: "demo",
targetDir: cliInstallPath("demo"),
version: "1.2.3",
npmResolution: {
packageName: "demo",
resolvedVersion: "1.2.3",
tarballUrl: "https://registry.npmjs.org/demo/-/demo-1.2.3.tgz",
},
});
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
recordPluginInstall.mockReturnValue(enabledCfg);
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "demo", "--force"]);
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "demo",
mode: "update",
}),
);
});
it("does not fall back to npm when ClawHub rejects a real package", async () => {
installPluginFromClawHub.mockResolvedValue({
ok: false,
@ -486,4 +602,53 @@ describe("plugins cli install", () => {
expect(writeConfigFile).toHaveBeenCalledWith(installedCfg);
expect(runtimeLogs.some((line) => line.includes("Installed hook pack: demo-hooks"))).toBe(true);
});
it("passes force through as overwrite mode for hook-pack npm fallback installs", async () => {
const cfg = {} as OpenClawConfig;
const installedCfg = {
hooks: {
internal: {
installs: {
"demo-hooks": {
source: "npm",
spec: "@acme/demo-hooks@1.2.3",
},
},
},
},
} as OpenClawConfig;
loadConfig.mockReturnValue(cfg);
installPluginFromClawHub.mockResolvedValue({
ok: false,
error: "ClawHub /api/v1/packages/@acme/demo-hooks failed (404): Package not found",
code: "package_not_found",
});
installPluginFromNpmSpec.mockResolvedValue({
ok: false,
error: "package.json missing openclaw.plugin.json",
});
installHooksFromNpmSpec.mockResolvedValue({
ok: true,
hookPackId: "demo-hooks",
hooks: ["command-audit"],
targetDir: "/tmp/hooks/demo-hooks",
version: "1.2.3",
npmResolution: {
name: "@acme/demo-hooks",
spec: "@acme/demo-hooks@1.2.3",
integrity: "sha256-demo",
},
});
recordHookInstall.mockReturnValue(installedCfg);
await runPluginsCommand(["plugins", "install", "@acme/demo-hooks", "--force"]);
expect(installHooksFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@acme/demo-hooks",
mode: "update",
}),
);
});
});

View File

@ -770,6 +770,7 @@ export function registerPluginsCli(program: Command) {
"Path (.ts/.js/.zip/.tgz/.tar.gz), npm package spec, or marketplace plugin name",
)
.option("-l, --link", "Link a local path instead of copying", false)
.option("--force", "Overwrite an existing installed plugin or hook pack", false)
.option("--pin", "Record npm installs as exact resolved <name>@<version>", false)
.option(
"--dangerously-force-unsafe-install",
@ -785,6 +786,7 @@ export function registerPluginsCli(program: Command) {
raw: string,
opts: {
dangerouslyForceUnsafeInstall?: boolean;
force?: boolean;
link?: boolean;
pin?: boolean;
marketplace?: string;

View File

@ -38,6 +38,10 @@ import {
} from "./plugins-command-helpers.js";
import { persistHookPackInstall, persistPluginInstall } from "./plugins-install-persist.js";
function resolveInstallMode(force?: boolean): "install" | "update" {
return force ? "update" : "install";
}
async function installBundledPluginSource(params: {
config: OpenClawConfig;
rawSpec: string;
@ -71,6 +75,7 @@ async function installBundledPluginSource(params: {
async function tryInstallHookPackFromLocalPath(params: {
config: OpenClawConfig;
resolvedPath: string;
installMode: "install" | "update";
link?: boolean;
}): Promise<{ ok: true } | { ok: false; error: string }> {
if (params.link) {
@ -122,6 +127,7 @@ async function tryInstallHookPackFromLocalPath(params: {
const result = await installHooksFromPath({
path: params.resolvedPath,
mode: params.installMode,
logger: createHookPackInstallLogger(),
});
if (!result.ok) {
@ -145,11 +151,13 @@ async function tryInstallHookPackFromLocalPath(params: {
async function tryInstallHookPackFromNpmSpec(params: {
config: OpenClawConfig;
installMode: "install" | "update";
spec: string;
pin?: boolean;
}): Promise<{ ok: true } | { ok: false; error: string }> {
const result = await installHooksFromNpmSpec({
spec: params.spec,
mode: params.installMode,
logger: createHookPackInstallLogger(),
});
if (!result.ok) {
@ -245,6 +253,7 @@ export async function loadConfigForInstall(
export async function runPluginInstallCommand(params: {
raw: string;
opts: InstallSafetyOverrides & {
force?: boolean;
link?: boolean;
pin?: boolean;
marketplace?: string;
@ -274,6 +283,10 @@ export async function runPluginInstallCommand(params: {
return defaultRuntime.exit(1);
}
}
if (opts.link && opts.force) {
defaultRuntime.error("`--force` is not supported with `--link`.");
return defaultRuntime.exit(1);
}
const requestResolution = resolvePluginInstallRequestContext({
rawSpec: raw,
marketplace: opts.marketplace,
@ -290,11 +303,13 @@ export async function runPluginInstallCommand(params: {
if (!cfg) {
return defaultRuntime.exit(1);
}
const installMode = resolveInstallMode(opts.force);
if (opts.marketplace) {
const result = await installPluginFromMarketplace({
dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall,
marketplace: opts.marketplace,
mode: installMode,
plugin: raw,
logger: createPluginInstallLogger(),
});
@ -329,6 +344,7 @@ export async function runPluginInstallCommand(params: {
if (!probe.ok) {
const hookFallback = await tryInstallHookPackFromLocalPath({
config: cfg,
installMode,
resolvedPath: resolved,
link: true,
});
@ -366,12 +382,14 @@ export async function runPluginInstallCommand(params: {
const result = await installPluginFromPath({
dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall,
mode: installMode,
path: resolved,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
const hookFallback = await tryInstallHookPackFromLocalPath({
config: cfg,
installMode,
resolvedPath: resolved,
});
if (hookFallback.ok) {
@ -437,6 +455,7 @@ export async function runPluginInstallCommand(params: {
if (clawhubSpec) {
const result = await installPluginFromClawHub({
dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall,
mode: installMode,
spec: raw,
logger: createPluginInstallLogger(),
});
@ -472,6 +491,7 @@ export async function runPluginInstallCommand(params: {
if (preferredClawHubSpec) {
const clawhubResult = await installPluginFromClawHub({
dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall,
mode: installMode,
spec: preferredClawHubSpec,
logger: createPluginInstallLogger(),
});
@ -506,6 +526,7 @@ export async function runPluginInstallCommand(params: {
const result = await installPluginFromNpmSpec({
dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall,
mode: installMode,
spec: raw,
logger: createPluginInstallLogger(),
});
@ -518,6 +539,7 @@ export async function runPluginInstallCommand(params: {
if (!bundledFallbackPlan) {
const hookFallback = await tryInstallHookPackFromNpmSpec({
config: cfg,
installMode,
spec: raw,
pin: opts.pin,
});

View File

@ -207,12 +207,14 @@ async function installFromDirWithWarnings(params: {
pluginDir: string;
extensionsDir: string;
dangerouslyForceUnsafeInstall?: boolean;
mode?: "install" | "update";
}) {
const warnings: string[] = [];
const result = await installPluginFromDir({
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
dirPath: params.pluginDir,
extensionsDir: params.extensionsDir,
mode: params.mode,
logger: {
info: () => {},
warn: (msg: string) => warnings.push(msg),
@ -1005,6 +1007,75 @@ describe("installPluginFromArchive", () => {
).toBe(true);
});
it("reports install mode to before_install when force-style update runs against a missing target", async () => {
const handler = vi.fn().mockReturnValue({});
initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_install", handler }]));
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify({
name: "fresh-force-plugin",
version: "1.0.0",
openclaw: { extensions: ["index.js"] },
}),
);
fs.writeFileSync(path.join(pluginDir, "index.js"), "export {};\n");
const { result } = await installFromDirWithWarnings({
pluginDir,
extensionsDir,
mode: "update",
});
expect(result.ok).toBe(true);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler.mock.calls[0]?.[0]).toMatchObject({
request: {
kind: "plugin-dir",
mode: "install",
},
});
});
it("reports update mode to before_install when replacing an existing target", async () => {
const handler = vi.fn().mockReturnValue({});
initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_install", handler }]));
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
const existingTargetDir = resolvePluginInstallDir("replace-force-plugin", extensionsDir);
fs.mkdirSync(existingTargetDir, { recursive: true });
fs.writeFileSync(
path.join(existingTargetDir, "package.json"),
JSON.stringify({ version: "0.9.0" }),
);
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify({
name: "replace-force-plugin",
version: "1.0.0",
openclaw: { extensions: ["index.js"] },
}),
);
fs.writeFileSync(path.join(pluginDir, "index.js"), "export {};\n");
const { result } = await installFromDirWithWarnings({
pluginDir,
extensionsDir,
mode: "update",
});
expect(result.ok).toBe(true);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler.mock.calls[0]?.[0]).toMatchObject({
request: {
kind: "plugin-dir",
mode: "update",
},
});
});
it("scans extension entry files in hidden directories", async () => {
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
fs.mkdirSync(path.join(pluginDir, ".hidden"), { recursive: true });

View File

@ -283,6 +283,7 @@ async function installPluginDirectoryIntoExtensions(params: {
manifestName?: string;
version?: string;
extensions: string[];
targetDir?: string;
extensionsDir?: string;
logger: PluginInstallLogger;
timeoutMs: number;
@ -295,20 +296,19 @@ async function installPluginDirectoryIntoExtensions(params: {
nameEncoder?: (pluginId: string) => string;
}): Promise<InstallPluginResult> {
const runtime = await loadPluginInstallRuntime();
const extensionsDir = params.extensionsDir
? resolveUserPath(params.extensionsDir)
: path.join(CONFIG_DIR, "extensions");
const targetDirResult = await runtime.resolveCanonicalInstallTarget({
baseDir: extensionsDir,
id: params.pluginId,
invalidNameMessage: "invalid plugin name: path traversal detected",
boundaryLabel: "extensions directory",
nameEncoder: params.nameEncoder,
});
if (!targetDirResult.ok) {
return { ok: false, error: targetDirResult.error };
let targetDir = params.targetDir;
if (!targetDir) {
const targetDirResult = await resolvePluginInstallTarget({
runtime,
pluginId: params.pluginId,
extensionsDir: params.extensionsDir,
nameEncoder: params.nameEncoder,
});
if (!targetDirResult.ok) {
return { ok: false, error: targetDirResult.error };
}
targetDir = targetDirResult.targetDir;
}
const targetDir = targetDirResult.targetDir;
const availability = await runtime.ensureInstallTargetAvailable({
mode: params.mode,
targetDir,
@ -372,6 +372,35 @@ export function resolvePluginInstallDir(pluginId: string, extensionsDir?: string
return targetDirResult.path;
}
async function resolvePluginInstallTarget(params: {
runtime: Awaited<ReturnType<typeof loadPluginInstallRuntime>>;
pluginId: string;
extensionsDir?: string;
nameEncoder?: (pluginId: string) => string;
}): Promise<{ ok: true; targetDir: string } | { ok: false; error: string }> {
const extensionsDir = params.extensionsDir
? resolveUserPath(params.extensionsDir)
: path.join(CONFIG_DIR, "extensions");
return await params.runtime.resolveCanonicalInstallTarget({
baseDir: extensionsDir,
id: params.pluginId,
invalidNameMessage: "invalid plugin name: path traversal detected",
boundaryLabel: "extensions directory",
nameEncoder: params.nameEncoder,
});
}
async function resolveEffectiveInstallMode(params: {
runtime: Awaited<ReturnType<typeof loadPluginInstallRuntime>>;
requestedMode: "install" | "update";
targetPath: string;
}): Promise<"install" | "update"> {
if (params.requestedMode !== "update") {
return "install";
}
return (await params.runtime.fileExists(params.targetPath)) ? "update" : "install";
}
async function installBundleFromSourceDir(
params: {
sourceDir: string;
@ -409,6 +438,20 @@ async function installBundleFromSourceDir(
};
}
const targetDirResult = await resolvePluginInstallTarget({
runtime,
pluginId,
extensionsDir: params.extensionsDir,
});
if (!targetDirResult.ok) {
return { ok: false, error: targetDirResult.error };
}
const effectiveMode = await resolveEffectiveInstallMode({
runtime,
requestedMode: mode,
targetPath: targetDirResult.targetDir,
});
try {
const scanResult = await runtime.scanBundleInstallSource({
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
@ -417,7 +460,7 @@ async function installBundleFromSourceDir(
logger,
requestKind: params.installPolicyRequest?.kind,
requestedSpecifier: params.installPolicyRequest?.requestedSpecifier,
mode,
mode: effectiveMode,
version: manifestRes.manifest.version,
});
if (scanResult?.blocked) {
@ -437,10 +480,11 @@ async function installBundleFromSourceDir(
manifestName: manifestRes.manifest.name,
version: manifestRes.manifest.version,
extensions: [],
targetDir: targetDirResult.targetDir,
extensionsDir: params.extensionsDir,
logger,
timeoutMs,
mode,
mode: effectiveMode,
dryRun,
copyErrorPrefix: "failed to copy plugin bundle",
hasDeps: false,
@ -588,6 +632,21 @@ async function installPluginFromPackageDir(
code: PLUGIN_INSTALL_ERROR_CODE.INCOMPATIBLE_HOST_VERSION,
};
}
const targetDirResult = await resolvePluginInstallTarget({
runtime,
pluginId,
extensionsDir: params.extensionsDir,
nameEncoder: encodePluginInstallDirName,
});
if (!targetDirResult.ok) {
return { ok: false, error: targetDirResult.error };
}
const effectiveMode = await resolveEffectiveInstallMode({
runtime,
requestedMode: mode,
targetPath: targetDirResult.targetDir,
});
try {
const scanResult = await runtime.scanPackageInstallSource({
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
@ -597,7 +656,7 @@ async function installPluginFromPackageDir(
extensions,
requestKind: params.installPolicyRequest?.kind,
requestedSpecifier: params.installPolicyRequest?.requestedSpecifier,
mode,
mode: effectiveMode,
packageName: pkgName || undefined,
manifestId: manifestPluginId,
version: typeof manifest.version === "string" ? manifest.version : undefined,
@ -620,10 +679,11 @@ async function installPluginFromPackageDir(
manifestName: pkgName || undefined,
version: typeof manifest.version === "string" ? manifest.version : undefined,
extensions,
targetDir: targetDirResult.targetDir,
extensionsDir: params.extensionsDir,
logger,
timeoutMs,
mode,
mode: effectiveMode,
dryRun,
copyErrorPrefix: "failed to copy plugin",
hasDeps: Object.keys(deps).length > 0,
@ -747,9 +807,14 @@ export async function installPluginFromFile(params: {
return { ok: false, error: pluginIdError };
}
const targetFile = path.join(extensionsDir, `${safeFileName(pluginId)}${path.extname(filePath)}`);
const effectiveMode = await resolveEffectiveInstallMode({
runtime,
requestedMode: mode,
targetPath: targetFile,
});
const availability = await runtime.ensureInstallTargetAvailable({
mode,
mode: effectiveMode,
targetDir: targetFile,
alreadyExistsError: `plugin already exists: ${targetFile} (delete it first)`,
});
@ -766,7 +831,7 @@ export async function installPluginFromFile(params: {
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
filePath,
logger,
mode,
mode: effectiveMode,
pluginId,
requestedSpecifier: installPolicyRequest.requestedSpecifier,
});