refactor(install): share package dir install
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
resolveArchiveKind,
|
||||
resolvePackedRootDir,
|
||||
} from "../infra/archive.js";
|
||||
import { installPackageDir } from "../infra/install-package-dir.js";
|
||||
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
@@ -214,48 +215,20 @@ async function installHookPackageFromDir(params: {
|
||||
};
|
||||
}
|
||||
|
||||
logger.info?.(`Installing to ${targetDir}…`);
|
||||
let backupDir: string | null = null;
|
||||
if (mode === "update" && (await fileExists(targetDir))) {
|
||||
backupDir = `${targetDir}.backup-${Date.now()}`;
|
||||
await fs.rename(targetDir, backupDir);
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.cp(params.packageDir, targetDir, { recursive: true });
|
||||
} catch (err) {
|
||||
if (backupDir) {
|
||||
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
await fs.rename(backupDir, targetDir).catch(() => undefined);
|
||||
}
|
||||
return { ok: false, error: `failed to copy hook pack: ${String(err)}` };
|
||||
}
|
||||
|
||||
const deps = manifest.dependencies ?? {};
|
||||
const hasDeps = Object.keys(deps).length > 0;
|
||||
if (hasDeps) {
|
||||
logger.info?.("Installing hook pack dependencies…");
|
||||
const npmRes = await runCommandWithTimeout(
|
||||
["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"],
|
||||
{
|
||||
timeoutMs: Math.max(timeoutMs, 300_000),
|
||||
cwd: targetDir,
|
||||
},
|
||||
);
|
||||
if (npmRes.code !== 0) {
|
||||
if (backupDir) {
|
||||
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
await fs.rename(backupDir, targetDir).catch(() => undefined);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: `npm install failed: ${npmRes.stderr.trim() || npmRes.stdout.trim()}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (backupDir) {
|
||||
await fs.rm(backupDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
const installRes = await installPackageDir({
|
||||
sourceDir: params.packageDir,
|
||||
targetDir,
|
||||
mode,
|
||||
timeoutMs,
|
||||
logger,
|
||||
copyErrorPrefix: "failed to copy hook pack",
|
||||
hasDeps,
|
||||
depsLogMessage: "Installing hook pack dependencies…",
|
||||
});
|
||||
if (!installRes.ok) {
|
||||
return installRes;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
68
src/infra/install-package-dir.ts
Normal file
68
src/infra/install-package-dir.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { fileExists } from "./archive.js";
|
||||
|
||||
export async function installPackageDir(params: {
|
||||
sourceDir: string;
|
||||
targetDir: string;
|
||||
mode: "install" | "update";
|
||||
timeoutMs: number;
|
||||
logger?: { info?: (message: string) => void };
|
||||
copyErrorPrefix: string;
|
||||
hasDeps: boolean;
|
||||
depsLogMessage: string;
|
||||
afterCopy?: () => void | Promise<void>;
|
||||
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
params.logger?.info?.(`Installing to ${params.targetDir}…`);
|
||||
let backupDir: string | null = null;
|
||||
if (params.mode === "update" && (await fileExists(params.targetDir))) {
|
||||
backupDir = `${params.targetDir}.backup-${Date.now()}`;
|
||||
await fs.rename(params.targetDir, backupDir);
|
||||
}
|
||||
|
||||
const rollback = async () => {
|
||||
if (!backupDir) {
|
||||
return;
|
||||
}
|
||||
await fs.rm(params.targetDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
await fs.rename(backupDir, params.targetDir).catch(() => undefined);
|
||||
};
|
||||
|
||||
try {
|
||||
await fs.cp(params.sourceDir, params.targetDir, { recursive: true });
|
||||
} catch (err) {
|
||||
await rollback();
|
||||
return { ok: false, error: `${params.copyErrorPrefix}: ${String(err)}` };
|
||||
}
|
||||
|
||||
try {
|
||||
await params.afterCopy?.();
|
||||
} catch (err) {
|
||||
await rollback();
|
||||
return { ok: false, error: `post-copy validation failed: ${String(err)}` };
|
||||
}
|
||||
|
||||
if (params.hasDeps) {
|
||||
params.logger?.info?.(params.depsLogMessage);
|
||||
const npmRes = await runCommandWithTimeout(
|
||||
["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"],
|
||||
{
|
||||
timeoutMs: Math.max(params.timeoutMs, 300_000),
|
||||
cwd: params.targetDir,
|
||||
},
|
||||
);
|
||||
if (npmRes.code !== 0) {
|
||||
await rollback();
|
||||
return {
|
||||
ok: false,
|
||||
error: `npm install failed: ${npmRes.stderr.trim() || npmRes.stdout.trim()}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (backupDir) {
|
||||
await fs.rm(backupDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
resolveArchiveKind,
|
||||
resolvePackedRootDir,
|
||||
} from "../infra/archive.js";
|
||||
import { installPackageDir } from "../infra/install-package-dir.js";
|
||||
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import * as skillScanner from "../security/skill-scanner.js";
|
||||
@@ -248,58 +249,32 @@ async function installPluginFromPackageDir(params: {
|
||||
};
|
||||
}
|
||||
|
||||
logger.info?.(`Installing to ${targetDir}…`);
|
||||
let backupDir: string | null = null;
|
||||
if (mode === "update" && (await fileExists(targetDir))) {
|
||||
backupDir = `${targetDir}.backup-${Date.now()}`;
|
||||
await fs.rename(targetDir, backupDir);
|
||||
}
|
||||
try {
|
||||
await fs.cp(params.packageDir, targetDir, { recursive: true });
|
||||
} catch (err) {
|
||||
if (backupDir) {
|
||||
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
await fs.rename(backupDir, targetDir).catch(() => undefined);
|
||||
}
|
||||
return { ok: false, error: `failed to copy plugin: ${String(err)}` };
|
||||
}
|
||||
|
||||
for (const entry of extensions) {
|
||||
const resolvedEntry = path.resolve(targetDir, entry);
|
||||
if (!isPathInside(targetDir, resolvedEntry)) {
|
||||
logger.warn?.(`extension entry escapes plugin directory: ${entry}`);
|
||||
continue;
|
||||
}
|
||||
if (!(await fileExists(resolvedEntry))) {
|
||||
logger.warn?.(`extension entry not found: ${entry}`);
|
||||
}
|
||||
}
|
||||
|
||||
const deps = manifest.dependencies ?? {};
|
||||
const hasDeps = Object.keys(deps).length > 0;
|
||||
if (hasDeps) {
|
||||
logger.info?.("Installing plugin dependencies…");
|
||||
const npmRes = await runCommandWithTimeout(
|
||||
["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"],
|
||||
{
|
||||
timeoutMs: Math.max(timeoutMs, 300_000),
|
||||
cwd: targetDir,
|
||||
},
|
||||
);
|
||||
if (npmRes.code !== 0) {
|
||||
if (backupDir) {
|
||||
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
await fs.rename(backupDir, targetDir).catch(() => undefined);
|
||||
const installRes = await installPackageDir({
|
||||
sourceDir: params.packageDir,
|
||||
targetDir,
|
||||
mode,
|
||||
timeoutMs,
|
||||
logger,
|
||||
copyErrorPrefix: "failed to copy plugin",
|
||||
hasDeps,
|
||||
depsLogMessage: "Installing plugin dependencies…",
|
||||
afterCopy: async () => {
|
||||
for (const entry of extensions) {
|
||||
const resolvedEntry = path.resolve(targetDir, entry);
|
||||
if (!isPathInside(targetDir, resolvedEntry)) {
|
||||
logger.warn?.(`extension entry escapes plugin directory: ${entry}`);
|
||||
continue;
|
||||
}
|
||||
if (!(await fileExists(resolvedEntry))) {
|
||||
logger.warn?.(`extension entry not found: ${entry}`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: `npm install failed: ${npmRes.stderr.trim() || npmRes.stdout.trim()}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (backupDir) {
|
||||
await fs.rm(backupDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
},
|
||||
});
|
||||
if (!installRes.ok) {
|
||||
return installRes;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user