mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 21:00:44 +00:00
feat(plugins): support npm pack installs
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Plugins/install: add `npm-pack:<path.tgz>` installs so local npm pack artifacts run through the same managed npm-root install, lockfile verification, dependency scan, and install-record path as registry npm plugins.
|
||||
- Plugin skills/Windows: publish plugin-provided skill directories as junctions on Windows so standard users without Developer Mode can register plugin skills without symlink EPERM failures. Fixes #77958. (#77971) Thanks @hclsys and @jarro.
|
||||
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.
|
||||
- Gateway/sessions: fast-path already-qualified model refs while building session-list rows so `openclaw sessions` and Control UI session lists avoid heavyweight model resolution on large stores. (#77902) Thanks @ragesaq.
|
||||
|
||||
@@ -74,6 +74,7 @@ openclaw plugins search "calendar" # search ClawHub plugins
|
||||
openclaw plugins install <package> # npm by default
|
||||
openclaw plugins install clawhub:<package> # ClawHub only
|
||||
openclaw plugins install npm:<package> # npm only
|
||||
openclaw plugins install npm-pack:<path.tgz> # local npm pack through npm install semantics
|
||||
openclaw plugins install git:github.com/<owner>/<repo> # git repo
|
||||
openclaw plugins install git:github.com/<owner>/<repo>@<ref>
|
||||
openclaw plugins install <package> --force # overwrite existing install
|
||||
@@ -150,6 +151,12 @@ is available, then fall back to `latest`.
|
||||
<Accordion title="Archives">
|
||||
Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`. Native OpenClaw plugin archives must contain a valid `openclaw.plugin.json` at the extracted plugin root; archives that only contain `package.json` are rejected before OpenClaw writes install records.
|
||||
|
||||
Use `npm-pack:<path.tgz>` when the file is an npm-pack tarball and you want
|
||||
to test the same managed npm-root install path used by registry installs,
|
||||
including `package-lock.json` verification, hoisted dependency scanning, and
|
||||
npm install records. Plain archive paths still install as local archives
|
||||
under the plugin extensions root.
|
||||
|
||||
Claude marketplace installs are also supported.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -46,6 +46,13 @@ npm installs run in the npm root with:
|
||||
npm install --prefix ~/.openclaw/npm <spec> --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
```
|
||||
|
||||
`openclaw plugins install npm-pack:<path.tgz>` uses that same managed npm root
|
||||
for a local npm-pack tarball. OpenClaw reads the tarball's npm metadata, adds it
|
||||
to the managed root as a copied `file:` dependency, runs the normal npm install,
|
||||
and then verifies the installed lockfile metadata before trusting the plugin.
|
||||
This is intended for package-acceptance and release-candidate proof where a
|
||||
local pack artifact should behave like the registry artifact it simulates.
|
||||
|
||||
npm may hoist transitive dependencies to `~/.openclaw/npm/node_modules` beside
|
||||
the plugin package. OpenClaw scans the managed npm root before trusting the
|
||||
install and uses npm to remove npm-managed packages during uninstall, so hoisted
|
||||
|
||||
@@ -38,6 +38,7 @@ For copy-paste install, list, uninstall, update, and publishing examples, see
|
||||
|
||||
# From npm
|
||||
openclaw plugins install npm:@acme/openclaw-plugin
|
||||
openclaw plugins install npm-pack:./openclaw-plugin-1.2.3.tgz
|
||||
|
||||
# From git
|
||||
openclaw plugins install git:github.com/acme/openclaw-plugin@v1.0.0
|
||||
@@ -92,8 +93,8 @@ If you prefer chat-native control, enable `commands.plugins: true` and use:
|
||||
```
|
||||
|
||||
The install path uses the same resolver as the CLI: local path/archive, explicit
|
||||
`clawhub:<pkg>`, explicit `npm:<pkg>`, explicit `git:<repo>`, or bare package
|
||||
spec through npm.
|
||||
`clawhub:<pkg>`, explicit `npm:<pkg>`, explicit `npm-pack:<path.tgz>`,
|
||||
explicit `git:<repo>`, or bare package spec through npm.
|
||||
|
||||
If config is invalid, install normally fails closed and points you at
|
||||
`openclaw doctor --fix`. The only recovery exception is a narrow bundled-plugin
|
||||
|
||||
@@ -81,6 +81,7 @@ export class PromptInputClosedError extends Error {
|
||||
}
|
||||
}
|
||||
export const installPluginFromNpmSpec: AsyncUnknownMock = vi.fn();
|
||||
export const installPluginFromNpmPackArchive: AsyncUnknownMock = vi.fn();
|
||||
export const installPluginFromPath: AsyncUnknownMock = vi.fn();
|
||||
export const installPluginFromClawHub: AsyncUnknownMock = vi.fn();
|
||||
export const parseClawHubPluginSpec: Mock<ParseClawHubPluginSpecFn> = vi.fn();
|
||||
@@ -485,6 +486,16 @@ vi.mock("../plugins/install.js", () => ({
|
||||
installPluginFromNpmSpec,
|
||||
...args,
|
||||
)) as (typeof import("../plugins/install.js"))["installPluginFromNpmSpec"],
|
||||
installPluginFromNpmPackArchive: ((
|
||||
...args: Parameters<(typeof import("../plugins/install.js"))["installPluginFromNpmPackArchive"]>
|
||||
) =>
|
||||
invokeMock<
|
||||
Parameters<(typeof import("../plugins/install.js"))["installPluginFromNpmPackArchive"]>,
|
||||
ReturnType<(typeof import("../plugins/install.js"))["installPluginFromNpmPackArchive"]>
|
||||
>(
|
||||
installPluginFromNpmPackArchive,
|
||||
...args,
|
||||
)) as (typeof import("../plugins/install.js"))["installPluginFromNpmPackArchive"],
|
||||
installPluginFromPath: ((
|
||||
...args: Parameters<(typeof import("../plugins/install.js"))["installPluginFromPath"]>
|
||||
) =>
|
||||
@@ -650,6 +661,7 @@ export function resetPluginsCliTestState() {
|
||||
installPluginFromGitSpec.mockReset();
|
||||
parseGitPluginSpec.mockReset();
|
||||
installPluginFromNpmSpec.mockReset();
|
||||
installPluginFromNpmPackArchive.mockReset();
|
||||
installPluginFromPath.mockReset();
|
||||
installPluginFromClawHub.mockReset();
|
||||
parseClawHubPluginSpec.mockReset();
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
findBundledPluginSourceMock,
|
||||
installHooksFromNpmSpec,
|
||||
installHooksFromPath,
|
||||
installPluginFromNpmPackArchive,
|
||||
installPluginFromClawHub,
|
||||
installPluginFromGitSpec,
|
||||
installPluginFromMarketplace,
|
||||
@@ -127,6 +128,28 @@ function createNpmPluginInstallResult(
|
||||
};
|
||||
}
|
||||
|
||||
function createNpmPackPluginInstallResult(
|
||||
pluginId = "demo",
|
||||
): Awaited<ReturnType<typeof installPluginFromNpmPackArchive>> {
|
||||
return {
|
||||
ok: true,
|
||||
pluginId,
|
||||
targetDir: cliInstallPath(pluginId),
|
||||
version: "1.2.3",
|
||||
extensions: ["dist/index.js"],
|
||||
manifestName: `@openclaw/${pluginId}`,
|
||||
npmTarballName: `openclaw-${pluginId}-1.2.3.tgz`,
|
||||
npmResolution: {
|
||||
name: `@openclaw/${pluginId}`,
|
||||
version: "1.2.3",
|
||||
resolvedSpec: `@openclaw/${pluginId}@1.2.3`,
|
||||
integrity: "sha512-pack-demo",
|
||||
shasum: "packdemosha",
|
||||
resolvedAt: "2026-05-06T00:00:00.000Z",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createGitPluginInstallResult(
|
||||
pluginId = "demo",
|
||||
): Awaited<ReturnType<typeof installPluginFromGitSpec>> {
|
||||
@@ -909,6 +932,47 @@ describe("plugins cli install", () => {
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg);
|
||||
});
|
||||
|
||||
it("installs npm-pack archives through npm install semantics", async () => {
|
||||
const cfg = createEmptyPluginConfig();
|
||||
const enabledCfg = createEnabledPluginConfig("demo");
|
||||
const archivePath = "/tmp/openclaw-demo-1.2.3.tgz";
|
||||
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
installPluginFromNpmPackArchive.mockResolvedValue(createNpmPackPluginInstallResult("demo"));
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
|
||||
recordPluginInstall.mockReturnValue(enabledCfg);
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
config: enabledCfg,
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "install", `npm-pack:${archivePath}`]);
|
||||
|
||||
expect(installPluginFromNpmPackArchive).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
archivePath,
|
||||
mode: "install",
|
||||
}),
|
||||
);
|
||||
expect(installPluginFromPath).not.toHaveBeenCalled();
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
|
||||
demo: expect.objectContaining({
|
||||
source: "npm",
|
||||
spec: "@openclaw/demo@1.2.3",
|
||||
sourcePath: archivePath,
|
||||
installPath: cliInstallPath("demo"),
|
||||
version: "1.2.3",
|
||||
artifactKind: "npm-pack",
|
||||
artifactFormat: "tgz",
|
||||
npmIntegrity: "sha512-pack-demo",
|
||||
npmShasum: "packdemosha",
|
||||
npmTarballName: "openclaw-demo-1.2.3.tgz",
|
||||
}),
|
||||
});
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg);
|
||||
});
|
||||
|
||||
it("keeps npm-prefixed official plugin ids on explicit npm semantics", async () => {
|
||||
const cfg = createEmptyPluginConfig();
|
||||
const enabledCfg = createEnabledPluginConfig("brave");
|
||||
|
||||
@@ -228,3 +228,11 @@ export function parseNpmPrefixSpec(raw: string): string | null {
|
||||
}
|
||||
return trimmed.slice("npm:".length).trim();
|
||||
}
|
||||
|
||||
export function parseNpmPackPrefixPath(raw: string): string | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("npm-pack:")) {
|
||||
return null;
|
||||
}
|
||||
return trimmed.slice("npm-pack:".length).trim();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { resolveDefaultPluginExtensionsDir } from "../plugins/install-paths.js";
|
||||
import type { InstallSafetyOverrides } from "../plugins/install-security-scan.js";
|
||||
import {
|
||||
PLUGIN_INSTALL_ERROR_CODE,
|
||||
installPluginFromNpmPackArchive,
|
||||
installPluginFromNpmSpec,
|
||||
installPluginFromPath,
|
||||
} from "../plugins/install.js";
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
createHookPackInstallLogger,
|
||||
createPluginInstallLogger,
|
||||
formatPluginInstallWithHookFallbackError,
|
||||
parseNpmPackPrefixPath,
|
||||
parseNpmPrefixSpec,
|
||||
} from "./plugins-command-helpers.js";
|
||||
import { persistHookPackInstall, persistPluginInstall } from "./plugins-install-persist.js";
|
||||
@@ -379,6 +381,54 @@ async function tryInstallPluginOrHookPackFromNpmSpec(params: {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function tryInstallPluginFromNpmPackArchive(params: {
|
||||
snapshot: ConfigSnapshotForInstallPersist;
|
||||
installMode: "install" | "update";
|
||||
archivePath: string;
|
||||
safetyOverrides: InstallSafetyOverrides;
|
||||
extensionsDir: string;
|
||||
runtime?: RuntimeEnv;
|
||||
}): Promise<{ ok: true } | { ok: false }> {
|
||||
const result = await installPluginFromNpmPackArchive({
|
||||
...params.safetyOverrides,
|
||||
mode: params.installMode,
|
||||
archivePath: params.archivePath,
|
||||
extensionsDir: params.extensionsDir,
|
||||
logger: createPluginInstallLogger(params.runtime),
|
||||
});
|
||||
if (!result.ok) {
|
||||
(params.runtime ?? defaultRuntime).error(result.error);
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
await persistPluginInstall({
|
||||
snapshot: params.snapshot,
|
||||
pluginId: result.pluginId,
|
||||
install: {
|
||||
source: "npm",
|
||||
spec: result.npmResolution?.resolvedSpec ?? result.manifestName ?? result.pluginId,
|
||||
sourcePath: params.archivePath,
|
||||
installPath: result.targetDir,
|
||||
...(result.version ? { version: result.version } : {}),
|
||||
...(result.npmResolution?.name ? { resolvedName: result.npmResolution.name } : {}),
|
||||
...(result.npmResolution?.version ? { resolvedVersion: result.npmResolution.version } : {}),
|
||||
...(result.npmResolution?.resolvedSpec
|
||||
? { resolvedSpec: result.npmResolution.resolvedSpec }
|
||||
: {}),
|
||||
...(result.npmResolution?.integrity ? { integrity: result.npmResolution.integrity } : {}),
|
||||
...(result.npmResolution?.shasum ? { shasum: result.npmResolution.shasum } : {}),
|
||||
...(result.npmResolution?.resolvedAt ? { resolvedAt: result.npmResolution.resolvedAt } : {}),
|
||||
artifactKind: "npm-pack",
|
||||
artifactFormat: "tgz",
|
||||
...(result.npmResolution?.integrity ? { npmIntegrity: result.npmResolution.integrity } : {}),
|
||||
...(result.npmResolution?.shasum ? { npmShasum: result.npmResolution.shasum } : {}),
|
||||
...(result.npmTarballName ? { npmTarballName: result.npmTarballName } : {}),
|
||||
},
|
||||
runtime: params.runtime,
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function tryInstallPluginFromGitSpec(params: {
|
||||
snapshot: ConfigSnapshotForInstallPersist;
|
||||
installMode: "install" | "update";
|
||||
@@ -753,6 +803,26 @@ export async function runPluginInstallCommand(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const npmPackPath = parseNpmPackPrefixPath(raw);
|
||||
if (npmPackPath !== null) {
|
||||
if (!npmPackPath) {
|
||||
runtime.error("unsupported npm-pack: spec: missing pack archive path");
|
||||
return runtime.exit(1);
|
||||
}
|
||||
const npmPackResult = await tryInstallPluginFromNpmPackArchive({
|
||||
snapshot,
|
||||
installMode,
|
||||
archivePath: npmPackPath,
|
||||
safetyOverrides,
|
||||
extensionsDir,
|
||||
runtime,
|
||||
});
|
||||
if (!npmPackResult.ok) {
|
||||
return runtime.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (gitSpec) {
|
||||
const gitResult = await tryInstallPluginFromGitSpec({
|
||||
snapshot,
|
||||
|
||||
@@ -321,3 +321,52 @@ export async function packNpmSpecToArchive(params: {
|
||||
metadata: parsedJson?.metadata ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveNpmPackArchiveMetadata(params: {
|
||||
archivePath: string;
|
||||
timeoutMs?: number;
|
||||
}): Promise<
|
||||
| {
|
||||
ok: true;
|
||||
archivePath: string;
|
||||
tarballName: string;
|
||||
metadata: NpmSpecResolution;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
error: string;
|
||||
}
|
||||
> {
|
||||
const archivePathResult = await resolveArchiveSourcePath(params.archivePath);
|
||||
if (!archivePathResult.ok) {
|
||||
return archivePathResult;
|
||||
}
|
||||
const archivePath = archivePathResult.path;
|
||||
const res = await runCommandWithTimeout(
|
||||
["npm", "pack", archivePath, "--ignore-scripts", "--dry-run", "--json"],
|
||||
{
|
||||
timeoutMs: Math.max(params.timeoutMs ?? 60_000, 60_000),
|
||||
env: {
|
||||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true",
|
||||
},
|
||||
},
|
||||
);
|
||||
if (res.code !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `npm pack metadata read failed: ${res.stderr.trim() || res.stdout.trim()}`,
|
||||
};
|
||||
}
|
||||
|
||||
const parsedJson = parseNpmPackJsonOutput(res.stdout || "");
|
||||
if (!parsedJson?.metadata.name || !parsedJson.metadata.version) {
|
||||
return { ok: false, error: "npm pack metadata read produced incomplete package metadata" };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
archivePath,
|
||||
tarballName: parsedJson.filename ?? path.basename(archivePath),
|
||||
metadata: parsedJson.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ vi.mock("../process/exec.js", () => ({
|
||||
|
||||
vi.resetModules();
|
||||
|
||||
const { installPluginFromNpmSpec, PLUGIN_INSTALL_ERROR_CODE } = await import("./install.js");
|
||||
const { installPluginFromNpmPackArchive, installPluginFromNpmSpec, PLUGIN_INSTALL_ERROR_CODE } =
|
||||
await import("./install.js");
|
||||
|
||||
const suiteTempRootTracker = createSuiteTempRootTracker("openclaw-plugin-install-npm-spec");
|
||||
|
||||
@@ -38,6 +39,18 @@ function npmViewVersionsArgv(spec: string): string[] {
|
||||
return ["npm", "view", spec, "versions", "--json"];
|
||||
}
|
||||
|
||||
function npmPackArchiveMetadataArgv(archivePath: string): string[] {
|
||||
return ["npm", "pack", archivePath, "--ignore-scripts", "--dry-run", "--json"];
|
||||
}
|
||||
|
||||
function resolveManagedFileDependency(npmRoot: string, dependencySpec: string): string | null {
|
||||
if (!dependencySpec.startsWith("file:")) {
|
||||
return null;
|
||||
}
|
||||
const rawPath = dependencySpec.slice("file:".length);
|
||||
return path.isAbsolute(rawPath) ? rawPath : path.resolve(npmRoot, rawPath);
|
||||
}
|
||||
|
||||
function expectNpmInstallIntoRoot(params: { calls: unknown[][]; npmRoot: string }) {
|
||||
const installCalls = params.calls.filter(
|
||||
(call) => Array.isArray(call[0]) && call[0][0] === "npm" && call[0][1] === "install",
|
||||
@@ -126,7 +139,7 @@ function writeInstalledNpmPlugin(params: {
|
||||
}
|
||||
|
||||
type MockNpmPackage = {
|
||||
spec: string;
|
||||
spec?: string;
|
||||
packageName: string;
|
||||
version: string;
|
||||
npmRoot: string;
|
||||
@@ -143,6 +156,8 @@ type MockNpmPackage = {
|
||||
installedIntegrity?: string;
|
||||
materializesRootOpenClaw?: boolean;
|
||||
skipLockfileEntry?: boolean;
|
||||
packArchivePath?: string;
|
||||
packTarballName?: string;
|
||||
};
|
||||
|
||||
function writeNpmRootPackageLock(params: {
|
||||
@@ -228,8 +243,29 @@ function mockNpmViewAndInstallMany(packages: MockNpmPackage[]) {
|
||||
const packagesByName = new Map(packages.map((pkg) => [pkg.packageName, pkg]));
|
||||
runCommandWithTimeoutMock.mockImplementation(
|
||||
async (argv: string[], options?: { cwd?: string }) => {
|
||||
const packPackage = packages.find(
|
||||
(pkg) =>
|
||||
pkg.packArchivePath &&
|
||||
JSON.stringify(argv) === JSON.stringify(npmPackArchiveMetadataArgv(pkg.packArchivePath)),
|
||||
);
|
||||
if (packPackage) {
|
||||
return successfulSpawn(
|
||||
JSON.stringify([
|
||||
{
|
||||
id: `${packPackage.packageName}@${packPackage.version}`,
|
||||
name: packPackage.packageName,
|
||||
version: packPackage.version,
|
||||
filename:
|
||||
packPackage.packTarballName ??
|
||||
`${packPackage.packageName.replace(/^@/, "").replace("/", "-")}-${packPackage.version}.tgz`,
|
||||
integrity: packPackage.integrity ?? "sha512-plugin-test",
|
||||
shasum: packPackage.shasum ?? "pluginshasum",
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
const viewPackage = packages.find(
|
||||
(pkg) => JSON.stringify(argv) === JSON.stringify(npmViewArgv(pkg.spec)),
|
||||
(pkg) => pkg.spec && JSON.stringify(argv) === JSON.stringify(npmViewArgv(pkg.spec)),
|
||||
);
|
||||
if (viewPackage) {
|
||||
return successfulSpawn(
|
||||
@@ -276,6 +312,12 @@ function mockNpmViewAndInstallMany(packages: MockNpmPackage[]) {
|
||||
`expected managed npm dependency ${packageName}@${pkg.expectedDependencySpec}, got ${dependencySpec ?? ""}`,
|
||||
);
|
||||
}
|
||||
const fileDependencyPath = dependencySpec
|
||||
? resolveManagedFileDependency(npmRoot, dependencySpec)
|
||||
: null;
|
||||
if (fileDependencyPath && !fs.existsSync(fileDependencyPath)) {
|
||||
throw new Error(`missing managed npm file dependency: ${fileDependencyPath}`);
|
||||
}
|
||||
writeInstalledNpmPlugin({
|
||||
...pkg,
|
||||
version: pkg.installedVersion ?? pkg.version,
|
||||
@@ -338,6 +380,114 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("installPluginFromNpmSpec", () => {
|
||||
it("installs npm pack archives through the managed npm root", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
const archivePath = path.join(stateDir, "openclaw-pack-demo-1.2.3.tgz");
|
||||
fs.writeFileSync(archivePath, "fixture pack contents", "utf8");
|
||||
|
||||
mockNpmViewAndInstallMany([
|
||||
{
|
||||
packageName: "@openclaw/pack-demo",
|
||||
version: "1.2.3",
|
||||
pluginId: "pack-demo",
|
||||
npmRoot,
|
||||
integrity: "sha512-pack-demo",
|
||||
shasum: "packdemosha",
|
||||
packArchivePath: archivePath,
|
||||
},
|
||||
{
|
||||
spec: "@openclaw/voice-call@0.0.1",
|
||||
packageName: "@openclaw/voice-call",
|
||||
version: "0.0.1",
|
||||
pluginId: "voice-call",
|
||||
npmRoot,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await installPluginFromNpmPackArchive({
|
||||
archivePath,
|
||||
npmDir: npmRoot,
|
||||
logger: { info: () => {}, warn: () => {} },
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.pluginId).toBe("pack-demo");
|
||||
expect(result.targetDir).toBe(path.join(npmRoot, "node_modules", "@openclaw/pack-demo"));
|
||||
expect(result.npmResolution?.resolvedSpec).toBe("@openclaw/pack-demo@1.2.3");
|
||||
expect(result.npmResolution?.integrity).toBe("sha512-pack-demo");
|
||||
expect(result.npmTarballName).toBe("openclaw-pack-demo-1.2.3.tgz");
|
||||
expectNpmInstallIntoRoot({
|
||||
calls: runCommandWithTimeoutMock.mock.calls,
|
||||
npmRoot,
|
||||
});
|
||||
const managedManifest = JSON.parse(
|
||||
await fs.promises.readFile(path.join(npmRoot, "package.json"), "utf8"),
|
||||
) as { dependencies?: Record<string, string> };
|
||||
const dependencySpec = managedManifest.dependencies?.["@openclaw/pack-demo"];
|
||||
expect(dependencySpec).toMatch(/^file:\.\/_openclaw-pack-archives\/.+\.tgz$/);
|
||||
expect(dependencySpec).not.toContain(archivePath);
|
||||
const stagedArchivePath = dependencySpec
|
||||
? resolveManagedFileDependency(npmRoot, dependencySpec)
|
||||
: null;
|
||||
expect(stagedArchivePath).toBeTruthy();
|
||||
if (!stagedArchivePath) {
|
||||
return;
|
||||
}
|
||||
await expect(fs.promises.readFile(stagedArchivePath, "utf8")).resolves.toBe(
|
||||
"fixture pack contents",
|
||||
);
|
||||
|
||||
fs.unlinkSync(archivePath);
|
||||
const unrelatedResult = await installPluginFromNpmSpec({
|
||||
spec: "@openclaw/voice-call@0.0.1",
|
||||
npmDir: npmRoot,
|
||||
logger: { info: () => {}, warn: () => {} },
|
||||
});
|
||||
expect(unrelatedResult.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects npm pack archive metadata with traversal package names", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
const victimDir = path.join(stateDir, "victim");
|
||||
const archivePath = path.join(stateDir, "evil-pack-1.0.0.tgz");
|
||||
fs.mkdirSync(victimDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(victimDir, "keep.txt"), "keep", "utf8");
|
||||
fs.writeFileSync(archivePath, "fixture pack contents", "utf8");
|
||||
|
||||
mockNpmViewAndInstallMany([
|
||||
{
|
||||
packageName: "@evil/../../../../victim",
|
||||
version: "1.0.0",
|
||||
npmRoot,
|
||||
packArchivePath: archivePath,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await installPluginFromNpmPackArchive({
|
||||
archivePath,
|
||||
npmDir: npmRoot,
|
||||
logger: { info: () => {}, warn: () => {} },
|
||||
mode: "update",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
code: PLUGIN_INSTALL_ERROR_CODE.INVALID_NPM_SPEC,
|
||||
});
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("unsupported npm pack package name");
|
||||
}
|
||||
expect(fs.existsSync(path.join(victimDir, "keep.txt"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(npmRoot, "package.json"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(npmRoot, "_openclaw-pack-archives"))).toBe(false);
|
||||
expect(runCommandWithTimeoutMock.mock.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("installs npm plugins into .openclaw/npm", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { packageNameMatchesId } from "../infra/install-safe-path.js";
|
||||
import {
|
||||
resolveNpmPackArchiveMetadata,
|
||||
resolveNpmSpecMetadata,
|
||||
type NpmIntegrityDrift,
|
||||
type NpmSpecResolution,
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
isPrereleaseSemverVersion,
|
||||
isPrereleaseResolutionAllowed,
|
||||
parseRegistryNpmSpec,
|
||||
validateRegistryNpmSpec,
|
||||
type ParsedRegistryNpmSpec,
|
||||
} from "../infra/npm-registry-spec.js";
|
||||
import {
|
||||
@@ -81,6 +84,7 @@ const PLUGIN_ARCHIVE_ROOT_MARKERS = [
|
||||
".claude-plugin/plugin.json",
|
||||
".cursor-plugin/plugin.json",
|
||||
];
|
||||
const MANAGED_NPM_PACK_ARCHIVE_DIR = "_openclaw-pack-archives";
|
||||
|
||||
export const PLUGIN_INSTALL_ERROR_CODE = {
|
||||
INVALID_NPM_SPEC: "invalid_npm_spec",
|
||||
@@ -383,6 +387,234 @@ function resolveInstalledNpmResolutionMismatch(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveTrustedNpmPackPackageName(packageName: string | undefined):
|
||||
| {
|
||||
ok: true;
|
||||
packageName: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
error: string;
|
||||
code: PluginInstallErrorCode;
|
||||
} {
|
||||
if (!packageName) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "npm pack metadata missing package name",
|
||||
code: PLUGIN_INSTALL_ERROR_CODE.INVALID_NPM_SPEC,
|
||||
};
|
||||
}
|
||||
const specError = validateRegistryNpmSpec(packageName);
|
||||
const parsedSpec = parseRegistryNpmSpec(packageName);
|
||||
if (specError || !parsedSpec || parsedSpec.selectorKind !== "none") {
|
||||
return {
|
||||
ok: false,
|
||||
error: `unsupported npm pack package name: ${packageName}`,
|
||||
code: PLUGIN_INSTALL_ERROR_CODE.INVALID_NPM_SPEC,
|
||||
};
|
||||
}
|
||||
return { ok: true, packageName: parsedSpec.name };
|
||||
}
|
||||
|
||||
async function installPluginFromManagedNpmRoot(
|
||||
params: InstallSafetyOverrides & {
|
||||
packageName: string;
|
||||
dependencySpec: string;
|
||||
displaySpec: string;
|
||||
installPolicyRequest: PluginInstallPolicyRequest;
|
||||
npmResolution: NpmSpecResolution;
|
||||
extensionsDir?: string;
|
||||
npmDir?: string;
|
||||
timeoutMs?: number;
|
||||
logger?: PluginInstallLogger;
|
||||
mode?: "install" | "update";
|
||||
dryRun?: boolean;
|
||||
expectedPluginId?: string;
|
||||
integrityDrift?: NpmIntegrityDrift;
|
||||
},
|
||||
): Promise<InstallPluginResult> {
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
|
||||
params,
|
||||
defaultLogger,
|
||||
);
|
||||
const expectedPluginId = params.expectedPluginId;
|
||||
const npmRoot = params.npmDir ? resolveUserPath(params.npmDir) : resolveDefaultPluginNpmDir();
|
||||
const installRoot = path.join(npmRoot, "node_modules", params.packageName);
|
||||
const effectiveMode = await resolveEffectiveInstallMode({
|
||||
runtime,
|
||||
requestedMode: mode,
|
||||
targetPath: installRoot,
|
||||
});
|
||||
const availability = await ensureInstallTargetAvailableForMode({
|
||||
runtime,
|
||||
targetPath: installRoot,
|
||||
mode: effectiveMode,
|
||||
});
|
||||
if (!availability.ok) {
|
||||
return availability;
|
||||
}
|
||||
if (dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
pluginId: expectedPluginId ?? params.packageName,
|
||||
targetDir: installRoot,
|
||||
extensions: [],
|
||||
npmResolution: params.npmResolution,
|
||||
...(params.integrityDrift ? { integrityDrift: params.integrityDrift } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
logger.info?.(`Installing ${params.displaySpec} into ${npmRoot}…`);
|
||||
if (params.packageName !== "openclaw") {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({
|
||||
npmRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
if (repairedOpenClawPeer) {
|
||||
logger.info?.(`Repaired stale openclaw peer dependency in ${npmRoot}`);
|
||||
}
|
||||
}
|
||||
await upsertManagedNpmRootDependency({
|
||||
npmRoot,
|
||||
packageName: params.packageName,
|
||||
dependencySpec: params.dependencySpec,
|
||||
});
|
||||
const install = await runCommandWithTimeout(
|
||||
[
|
||||
"npm",
|
||||
...createSafeNpmInstallArgs({
|
||||
omitDev: true,
|
||||
loglevel: "error",
|
||||
noAudit: true,
|
||||
noFund: true,
|
||||
}),
|
||||
"--prefix",
|
||||
".",
|
||||
],
|
||||
{
|
||||
cwd: npmRoot,
|
||||
timeoutMs: Math.max(timeoutMs, 300_000),
|
||||
env: createSafeNpmInstallEnv(process.env, { packageLock: true, quiet: true }),
|
||||
},
|
||||
);
|
||||
if (install.code !== 0) {
|
||||
await rollbackManagedNpmPluginInstall({
|
||||
npmRoot,
|
||||
packageName: params.packageName,
|
||||
targetDir: installRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
error: `npm install failed: ${install.stderr.trim() || install.stdout.trim()}`,
|
||||
};
|
||||
}
|
||||
if (params.packageName !== "openclaw") {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({
|
||||
npmRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
if (repairedOpenClawPeer) {
|
||||
logger.info?.(`Repaired stale openclaw peer dependency in ${npmRoot} after npm install`);
|
||||
}
|
||||
}
|
||||
await relinkOpenClawPeerDependenciesInManagedNpmRoot({ npmRoot, logger });
|
||||
|
||||
let installedDependency: ManagedNpmRootInstalledDependency | null;
|
||||
try {
|
||||
installedDependency = await readManagedNpmRootInstalledDependency({
|
||||
npmRoot,
|
||||
packageName: params.packageName,
|
||||
});
|
||||
} catch (error) {
|
||||
await rollbackManagedNpmPluginInstall({
|
||||
npmRoot,
|
||||
packageName: params.packageName,
|
||||
targetDir: installRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
error: `Failed to verify npm install metadata for ${params.packageName}: ${String(error)}`,
|
||||
};
|
||||
}
|
||||
const resolutionMismatch = resolveInstalledNpmResolutionMismatch({
|
||||
packageName: params.packageName,
|
||||
expected: params.npmResolution,
|
||||
installed: installedDependency,
|
||||
});
|
||||
if (resolutionMismatch) {
|
||||
await rollbackManagedNpmPluginInstall({
|
||||
npmRoot,
|
||||
packageName: params.packageName,
|
||||
targetDir: installRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
error: resolutionMismatch,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await installPluginFromInstalledPackageDir({
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
packageDir: installRoot,
|
||||
dependencyScanRootDir: npmRoot,
|
||||
logger,
|
||||
expectedPluginId,
|
||||
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
|
||||
mode: effectiveMode,
|
||||
installPolicyRequest: params.installPolicyRequest,
|
||||
});
|
||||
if (!result.ok) {
|
||||
await rollbackManagedNpmPluginInstall({
|
||||
npmRoot,
|
||||
packageName: params.packageName,
|
||||
targetDir: installRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
npmResolution: params.npmResolution,
|
||||
...(params.integrityDrift ? { integrityDrift: params.integrityDrift } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function stageNpmPackArchiveInManagedRoot(params: {
|
||||
archivePath: string;
|
||||
npmRoot: string;
|
||||
packageName: string;
|
||||
version?: string;
|
||||
integrity?: string;
|
||||
shasum?: string;
|
||||
tarballName: string;
|
||||
}): Promise<{ stableArchivePath: string; dependencySpec: string }> {
|
||||
const archiveStoreDir = path.join(params.npmRoot, MANAGED_NPM_PACK_ARCHIVE_DIR);
|
||||
const identity = params.integrity ?? params.shasum ?? params.tarballName;
|
||||
const identitySlug = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
||||
const packageSlug = safePluginInstallFileName(params.packageName) || "plugin";
|
||||
const versionSlug = safePluginInstallFileName(params.version ?? "pack") || "pack";
|
||||
const archiveFileName = `${packageSlug}-${versionSlug}-${identitySlug}.tgz`;
|
||||
const stableArchivePath = path.join(archiveStoreDir, archiveFileName);
|
||||
|
||||
await fs.mkdir(archiveStoreDir, { recursive: true });
|
||||
await fs.copyFile(params.archivePath, stableArchivePath);
|
||||
|
||||
return {
|
||||
stableArchivePath,
|
||||
dependencySpec: `file:./${path.posix.join(MANAGED_NPM_PACK_ARCHIVE_DIR, archiveFileName)}`,
|
||||
};
|
||||
}
|
||||
|
||||
type PackageInstallCommonParams = InstallSafetyOverrides & {
|
||||
extensionsDir?: string;
|
||||
npmDir?: string;
|
||||
@@ -1322,159 +1554,122 @@ export async function installPluginFromNpmSpec(
|
||||
return { ok: false, error: driftResult.error };
|
||||
}
|
||||
|
||||
const npmRoot = params.npmDir ? resolveUserPath(params.npmDir) : resolveDefaultPluginNpmDir();
|
||||
const installRoot = path.join(npmRoot, "node_modules", parsedSpec.name);
|
||||
const effectiveMode = await resolveEffectiveInstallMode({
|
||||
runtime,
|
||||
requestedMode: mode,
|
||||
targetPath: installRoot,
|
||||
});
|
||||
const availability = await ensureInstallTargetAvailableForMode({
|
||||
runtime,
|
||||
targetPath: installRoot,
|
||||
mode: effectiveMode,
|
||||
});
|
||||
if (!availability.ok) {
|
||||
return availability;
|
||||
}
|
||||
if (dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
pluginId: expectedPluginId ?? parsedSpec.name,
|
||||
targetDir: installRoot,
|
||||
extensions: [],
|
||||
npmResolution,
|
||||
...(driftResult.integrityDrift ? { integrityDrift: driftResult.integrityDrift } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
logger.info?.(`Installing ${spec} into ${npmRoot}…`);
|
||||
if (parsedSpec.name !== "openclaw") {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({
|
||||
npmRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
if (repairedOpenClawPeer) {
|
||||
logger.info?.(`Repaired stale openclaw peer dependency in ${npmRoot}`);
|
||||
}
|
||||
}
|
||||
await upsertManagedNpmRootDependency({
|
||||
npmRoot,
|
||||
return await installPluginFromManagedNpmRoot({
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
|
||||
packageName: parsedSpec.name,
|
||||
dependencySpec: resolveManagedNpmRootDependencySpec({
|
||||
parsedSpec,
|
||||
resolution: npmResolution,
|
||||
}),
|
||||
});
|
||||
const install = await runCommandWithTimeout(
|
||||
[
|
||||
"npm",
|
||||
...createSafeNpmInstallArgs({
|
||||
omitDev: true,
|
||||
loglevel: "error",
|
||||
noAudit: true,
|
||||
noFund: true,
|
||||
}),
|
||||
"--prefix",
|
||||
".",
|
||||
],
|
||||
{
|
||||
cwd: npmRoot,
|
||||
timeoutMs: Math.max(timeoutMs, 300_000),
|
||||
env: createSafeNpmInstallEnv(process.env, { packageLock: true, quiet: true }),
|
||||
},
|
||||
);
|
||||
if (install.code !== 0) {
|
||||
await rollbackManagedNpmPluginInstall({
|
||||
npmRoot,
|
||||
packageName: parsedSpec.name,
|
||||
targetDir: installRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
error: `npm install failed: ${install.stderr.trim() || install.stdout.trim()}`,
|
||||
};
|
||||
}
|
||||
if (parsedSpec.name !== "openclaw") {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({
|
||||
npmRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
if (repairedOpenClawPeer) {
|
||||
logger.info?.(`Repaired stale openclaw peer dependency in ${npmRoot} after npm install`);
|
||||
}
|
||||
}
|
||||
await relinkOpenClawPeerDependenciesInManagedNpmRoot({ npmRoot, logger });
|
||||
|
||||
let installedDependency: ManagedNpmRootInstalledDependency | null;
|
||||
try {
|
||||
installedDependency = await readManagedNpmRootInstalledDependency({
|
||||
npmRoot,
|
||||
packageName: parsedSpec.name,
|
||||
});
|
||||
} catch (error) {
|
||||
await rollbackManagedNpmPluginInstall({
|
||||
npmRoot,
|
||||
packageName: parsedSpec.name,
|
||||
targetDir: installRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
error: `Failed to verify npm install metadata for ${parsedSpec.name}: ${String(error)}`,
|
||||
};
|
||||
}
|
||||
const resolutionMismatch = resolveInstalledNpmResolutionMismatch({
|
||||
packageName: parsedSpec.name,
|
||||
expected: npmResolution,
|
||||
installed: installedDependency,
|
||||
});
|
||||
if (resolutionMismatch) {
|
||||
await rollbackManagedNpmPluginInstall({
|
||||
npmRoot,
|
||||
packageName: parsedSpec.name,
|
||||
targetDir: installRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
error: resolutionMismatch,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await installPluginFromInstalledPackageDir({
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
packageDir: installRoot,
|
||||
dependencyScanRootDir: npmRoot,
|
||||
logger,
|
||||
expectedPluginId,
|
||||
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
|
||||
mode: effectiveMode,
|
||||
displaySpec: spec,
|
||||
installPolicyRequest: {
|
||||
kind: "plugin-npm",
|
||||
requestedSpecifier: spec,
|
||||
},
|
||||
});
|
||||
if (!result.ok) {
|
||||
await rollbackManagedNpmPluginInstall({
|
||||
npmRoot,
|
||||
packageName: parsedSpec.name,
|
||||
targetDir: installRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
extensionsDir: params.extensionsDir,
|
||||
npmDir: params.npmDir,
|
||||
timeoutMs,
|
||||
logger,
|
||||
mode,
|
||||
dryRun,
|
||||
expectedPluginId,
|
||||
npmResolution,
|
||||
...(driftResult.integrityDrift ? { integrityDrift: driftResult.integrityDrift } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function installPluginFromNpmPackArchive(
|
||||
params: InstallSafetyOverrides & {
|
||||
archivePath: string;
|
||||
extensionsDir?: string;
|
||||
npmDir?: string;
|
||||
timeoutMs?: number;
|
||||
logger?: PluginInstallLogger;
|
||||
mode?: "install" | "update";
|
||||
dryRun?: boolean;
|
||||
expectedPluginId?: string;
|
||||
expectedIntegrity?: string;
|
||||
onIntegrityDrift?: (params: PluginNpmIntegrityDriftParams) => boolean | Promise<boolean>;
|
||||
},
|
||||
): Promise<InstallPluginResult & { npmTarballName?: string }> {
|
||||
const runtime = await loadPluginInstallRuntime();
|
||||
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
|
||||
params,
|
||||
defaultLogger,
|
||||
);
|
||||
const metadataResult = await resolveNpmPackArchiveMetadata({
|
||||
archivePath: params.archivePath,
|
||||
timeoutMs,
|
||||
});
|
||||
if (!metadataResult.ok) {
|
||||
return metadataResult;
|
||||
}
|
||||
const npmResolution: NpmSpecResolution = {
|
||||
...metadataResult.metadata,
|
||||
resolvedAt: new Date().toISOString(),
|
||||
};
|
||||
const driftResult = await resolveNpmIntegrityDriftWithDefaultMessage({
|
||||
spec: metadataResult.archivePath,
|
||||
expectedIntegrity: params.expectedIntegrity,
|
||||
resolution: npmResolution,
|
||||
onIntegrityDrift: params.onIntegrityDrift,
|
||||
warn: (message) => logger.warn?.(message),
|
||||
});
|
||||
if (driftResult.error) {
|
||||
return { ok: false, error: driftResult.error };
|
||||
}
|
||||
const packageNameResult = resolveTrustedNpmPackPackageName(metadataResult.metadata.name);
|
||||
if (!packageNameResult.ok) {
|
||||
return packageNameResult;
|
||||
}
|
||||
const packageName = packageNameResult.packageName;
|
||||
const npmRoot = params.npmDir ? resolveUserPath(params.npmDir) : resolveDefaultPluginNpmDir();
|
||||
let dependencySpec = "";
|
||||
if (!dryRun) {
|
||||
try {
|
||||
dependencySpec = (
|
||||
await stageNpmPackArchiveInManagedRoot({
|
||||
archivePath: metadataResult.archivePath,
|
||||
npmRoot,
|
||||
packageName,
|
||||
version: metadataResult.metadata.version,
|
||||
integrity: metadataResult.metadata.integrity,
|
||||
shasum: metadataResult.metadata.shasum,
|
||||
tarballName: metadataResult.tarballName,
|
||||
})
|
||||
).dependencySpec;
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Failed to stage npm pack archive in managed npm root: ${String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = await installPluginFromManagedNpmRoot({
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
|
||||
packageName,
|
||||
dependencySpec,
|
||||
displaySpec: metadataResult.archivePath,
|
||||
installPolicyRequest: {
|
||||
kind: "plugin-npm",
|
||||
requestedSpecifier: `npm-pack:${metadataResult.archivePath}`,
|
||||
},
|
||||
extensionsDir: params.extensionsDir,
|
||||
npmDir: npmRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
mode,
|
||||
dryRun,
|
||||
expectedPluginId: params.expectedPluginId,
|
||||
npmResolution,
|
||||
...(driftResult.integrityDrift ? { integrityDrift: driftResult.integrityDrift } : {}),
|
||||
});
|
||||
return {
|
||||
...result,
|
||||
...(result.ok ? { npmTarballName: metadataResult.tarballName } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user