mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
Prepare self-contained open-source release (#1)
* feat: prepare self-contained open-source release * fix: canonicalize missing managed paths --------- Co-authored-by: luckyyzh <luckyyzh@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,13 @@ const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const webDir = join(rootDir, "pi-web");
|
||||
const requiredProfilePaths = [
|
||||
join(rootDir, "config", "settings.default.json"),
|
||||
join(rootDir, "config", "mcp.default.json"),
|
||||
join(rootDir, "config", "models.example.json"),
|
||||
join(rootDir, "config", "subagents.default.json"),
|
||||
join(rootDir, "config", "subagent-tool-description.default.md"),
|
||||
join(rootDir, ".env.example"),
|
||||
join(rootDir, "LICENSE"),
|
||||
join(rootDir, "THIRD_PARTY_NOTICES.md"),
|
||||
join(rootDir, "resources", "skills"),
|
||||
join(rootDir, "resources", "extensions"),
|
||||
join(rootDir, "resources", "prompts"),
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { platform } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
agentDir,
|
||||
dataDir,
|
||||
managedEnvironment,
|
||||
resourcesDir,
|
||||
rootDir,
|
||||
} from "./profile.mjs";
|
||||
|
||||
Object.assign(process.env, managedEnvironment());
|
||||
|
||||
const codingAgentEntry = pathToFileURL(
|
||||
join(rootDir, "pi-web", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"),
|
||||
).href;
|
||||
const { DefaultResourceLoader } = await import(codingAgentEntry);
|
||||
|
||||
const loader = new DefaultResourceLoader({
|
||||
cwd: join(dataDir, "workspaces", "default"),
|
||||
agentDir,
|
||||
additionalExtensionPaths: [join(resourcesDir, "extensions")],
|
||||
additionalSkillPaths: [join(resourcesDir, "skills")],
|
||||
});
|
||||
|
||||
await loader.reload();
|
||||
const loaded = loader.getExtensions();
|
||||
if (loaded.errors.length > 0) {
|
||||
for (const error of loaded.errors) console.error(`Extension load error: ${error.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tools = new Set(loaded.extensions.flatMap((extension) => [...extension.tools.keys()]));
|
||||
const commands = new Set(loaded.extensions.flatMap((extension) => [...extension.commands.keys()]));
|
||||
const requiredTools = [
|
||||
"web_search",
|
||||
"mcp",
|
||||
"lens_diagnostics",
|
||||
"subagent",
|
||||
"web_fetch",
|
||||
"memory_write",
|
||||
"resolve-library-id",
|
||||
"query-docs",
|
||||
];
|
||||
const requiredCommands = ["checkpoint", "rewind"];
|
||||
const missing = [
|
||||
...requiredTools.filter((name) => !tools.has(name)).map((name) => `tool:${name}`),
|
||||
...requiredCommands.filter((name) => !commands.has(name)).map((name) => `command:/${name}`),
|
||||
];
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error(`Managed Pi profile is incomplete: ${missing.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const npmCliPath = process.env.npm_execpath;
|
||||
if (!npmCliPath) {
|
||||
console.error("Run the managed profile check through npm: npm run check:profile");
|
||||
process.exit(1);
|
||||
}
|
||||
const playwrightResult = spawnSync(
|
||||
process.execPath,
|
||||
[npmCliPath, "exec", "--yes", "--package=@playwright/mcp@0.0.78", "--", "playwright-mcp", "--version"],
|
||||
{ cwd: rootDir, env: process.env, encoding: "utf8" },
|
||||
);
|
||||
if (playwrightResult.error || playwrightResult.status !== 0) {
|
||||
process.stderr.write(playwrightResult.stderr ?? "");
|
||||
console.error("Unable to cache @playwright/mcp@0.0.78 for the managed MCP profile.");
|
||||
process.exit(playwrightResult.status ?? 1);
|
||||
}
|
||||
|
||||
if (platform() === "win32") {
|
||||
const edgeCandidates = [
|
||||
join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
join(process.env.ProgramFiles ?? "C:\\Program Files", "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
];
|
||||
if (!edgeCandidates.some(existsSync)) {
|
||||
console.error("Microsoft Edge is required by config/mcp.default.json but was not found.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Managed Pi profile ready: ${loaded.extensions.length} extension(s), ${tools.size} tool(s), ${commands.size} command(s).`,
|
||||
);
|
||||
@@ -0,0 +1,48 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { agentDir, managedEnvironment, rootDir } from "./profile.mjs";
|
||||
|
||||
const settings = JSON.parse(
|
||||
readFileSync(join(rootDir, "config", "settings.default.json"), "utf8"),
|
||||
);
|
||||
const packages = Array.isArray(settings.packages) ? settings.packages : [];
|
||||
const cliPath = join(rootDir, "pi", "packages", "coding-agent", "dist", "cli.js");
|
||||
const env = managedEnvironment();
|
||||
|
||||
function parseExactNpmSource(source) {
|
||||
if (!source.startsWith("npm:")) return undefined;
|
||||
const spec = source.slice(4);
|
||||
const separator = spec.lastIndexOf("@");
|
||||
if (separator <= 0) return undefined;
|
||||
return { name: spec.slice(0, separator), version: spec.slice(separator + 1) };
|
||||
}
|
||||
|
||||
function installedAtExpectedVersion(source) {
|
||||
const parsed = parseExactNpmSource(source);
|
||||
if (!parsed) return false;
|
||||
const packageJson = join(agentDir, "npm", "node_modules", ...parsed.name.split("/"), "package.json");
|
||||
if (!existsSync(packageJson)) return false;
|
||||
try {
|
||||
return JSON.parse(readFileSync(packageJson, "utf8")).version === parsed.version;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of packages) {
|
||||
if (typeof source !== "string" || !source.trim()) continue;
|
||||
if (installedAtExpectedVersion(source)) {
|
||||
console.log(`Managed package ready: ${source}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Installing managed package: ${source}`);
|
||||
const result = spawnSync(process.execPath, [cliPath, "install", source], {
|
||||
cwd: rootDir,
|
||||
env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||
}
|
||||
+67
-2
@@ -1,25 +1,40 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { platform } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { loadEnvFile } from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const projectEnvPath = join(rootDir, ".env");
|
||||
if (existsSync(projectEnvPath)) loadEnvFile(projectEnvPath);
|
||||
|
||||
export const configDir = join(rootDir, "config");
|
||||
export const resourcesDir = join(rootDir, "resources");
|
||||
export const dataDir = resolve(process.env.PI_AGENT_DATA_DIR ?? join(rootDir, "data"));
|
||||
export const agentDir = join(dataDir, "agent");
|
||||
export const homeDir = join(dataDir, "home");
|
||||
export const memoryDir = join(agentDir, "memory");
|
||||
export const skillsHomeDir = join(dataDir, "skills-home");
|
||||
export const stateDir = join(dataDir, "state");
|
||||
|
||||
const managedDirectories = [
|
||||
agentDir,
|
||||
homeDir,
|
||||
join(agentDir, "sessions"),
|
||||
join(agentDir, "npm"),
|
||||
join(agentDir, "git"),
|
||||
join(agentDir, "bin"),
|
||||
join(agentDir, "extensions", "subagent"),
|
||||
memoryDir,
|
||||
join(memoryDir, "daily"),
|
||||
join(memoryDir, "recovery"),
|
||||
join(agentDir, "tools"),
|
||||
join(agentDir, "tmp"),
|
||||
join(dataDir, "logs"),
|
||||
join(dataDir, "cache"),
|
||||
join(dataDir, "cache", "npm"),
|
||||
join(dataDir, "mcp", "playwright", "output"),
|
||||
join(dataDir, "workspaces", "default"),
|
||||
join(skillsHomeDir, ".agents", "skills"),
|
||||
join(skillsHomeDir, ".pi", "agent", "skills"),
|
||||
stateDir,
|
||||
@@ -31,9 +46,50 @@ const managedDirectories = [
|
||||
|
||||
const seedFiles = [
|
||||
[join(configDir, "settings.default.json"), join(agentDir, "settings.json")],
|
||||
[join(configDir, "mcp.default.json"), join(agentDir, "mcp.json")],
|
||||
[join(configDir, "models.example.json"), join(agentDir, "models.json")],
|
||||
[join(configDir, "subagents.default.json"), join(agentDir, "extensions", "subagent", "config.json")],
|
||||
[join(configDir, "subagent-tool-description.default.md"), join(agentDir, "subagent-tool-description.md")],
|
||||
];
|
||||
|
||||
const persistedWindowsEnvironmentKeys = ["SEARXNG_TOKEN", "SEARXNG_URL"];
|
||||
|
||||
function readPersistedWindowsEnvironment(baseEnv) {
|
||||
if (platform() !== "win32") return {};
|
||||
|
||||
const missingKeys = persistedWindowsEnvironmentKeys.filter((key) => !baseEnv[key]?.trim());
|
||||
if (missingKeys.length === 0) return {};
|
||||
|
||||
const keyLiterals = missingKeys.map((key) => `'${key.replaceAll("'", "''")}'`).join(", ");
|
||||
const script = `
|
||||
$result = @{}
|
||||
foreach ($name in @(${keyLiterals})) {
|
||||
$value = [Environment]::GetEnvironmentVariable($name, 'User')
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
$value = [Environment]::GetEnvironmentVariable($name, 'Machine')
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||
$result[$name] = $value
|
||||
}
|
||||
}
|
||||
ConvertTo-Json -Compress -InputObject $result
|
||||
`;
|
||||
|
||||
const result = spawnSync(
|
||||
"powershell.exe",
|
||||
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||
{ encoding: "utf8", windowsHide: true },
|
||||
);
|
||||
if (result.status !== 0 || !result.stdout.trim()) return {};
|
||||
|
||||
try {
|
||||
const values = JSON.parse(result.stdout);
|
||||
return values && typeof values === "object" && !Array.isArray(values) ? values : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureProfile({ quiet = false } = {}) {
|
||||
for (const path of managedDirectories) mkdirSync(path, { recursive: true });
|
||||
|
||||
@@ -49,19 +105,28 @@ export function ensureProfile({ quiet = false } = {}) {
|
||||
if (created.length > 0) console.log(`Initialized ${created.length} profile file(s).`);
|
||||
}
|
||||
|
||||
return { rootDir, configDir, resourcesDir, dataDir, agentDir, skillsHomeDir, stateDir, created };
|
||||
return { rootDir, configDir, resourcesDir, dataDir, agentDir, homeDir, skillsHomeDir, stateDir, created };
|
||||
}
|
||||
|
||||
export function managedEnvironment(baseEnv = process.env) {
|
||||
ensureProfile({ quiet: true });
|
||||
const persistedEnvironment = readPersistedWindowsEnvironment(baseEnv);
|
||||
return {
|
||||
...baseEnv,
|
||||
...persistedEnvironment,
|
||||
PI_AGENT_MANAGED_RUNTIME: "1",
|
||||
PI_AGENT_APP_ROOT: rootDir,
|
||||
PI_AGENT_DATA_DIR: dataDir,
|
||||
PI_AGENT_RESOURCES_DIR: resourcesDir,
|
||||
PI_AGENT_SKILLS_HOME: skillsHomeDir,
|
||||
PI_CODING_AGENT_DIR: agentDir,
|
||||
HOME: homeDir,
|
||||
USERPROFILE: homeDir,
|
||||
PI_MEMORY_DIR: baseEnv.PI_MEMORY_DIR?.trim() || memoryDir,
|
||||
PI_MEMORY_SNAPSHOT: baseEnv.PI_MEMORY_SNAPSHOT?.trim() || "stable",
|
||||
PI_MEMORY_QMD_UPDATE: baseEnv.PI_MEMORY_QMD_UPDATE?.trim() || "off",
|
||||
PI_RETRY_STALL_TIMEOUT_MS: baseEnv.PI_RETRY_STALL_TIMEOUT_MS?.trim() || "180000",
|
||||
npm_config_cache: baseEnv.npm_config_cache?.trim() || join(dataDir, "cache", "npm"),
|
||||
XDG_STATE_HOME: stateDir,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { platform } from "node:os";
|
||||
import { rootDir } from "./profile.mjs";
|
||||
|
||||
const DEV_PORT = 30141;
|
||||
|
||||
function stopWindowsDevelopmentProcesses() {
|
||||
const projectRootLiteral = rootDir.replaceAll("'", "''");
|
||||
const script = `
|
||||
$projectRoot = '${projectRootLiteral}'
|
||||
$currentProcessId = ${process.pid}
|
||||
$excludedProcessIds = [Collections.Generic.HashSet[int]]::new()
|
||||
$ancestorProcessId = $currentProcessId
|
||||
|
||||
while ($ancestorProcessId -gt 0 -and $excludedProcessIds.Add($ancestorProcessId)) {
|
||||
$ancestor = Get-CimInstance Win32_Process -Filter "ProcessId = $ancestorProcessId" -ErrorAction SilentlyContinue
|
||||
if (-not $ancestor) {
|
||||
break
|
||||
}
|
||||
$ancestorProcessId = [int]$ancestor.ParentProcessId
|
||||
}
|
||||
|
||||
$targets = Get-CimInstance Win32_Process | Where-Object {
|
||||
-not $excludedProcessIds.Contains([int]$_.ProcessId) -and
|
||||
$_.CommandLine -and
|
||||
$_.CommandLine.IndexOf($projectRoot, [StringComparison]::OrdinalIgnoreCase) -ge 0 -and
|
||||
$_.Name -in @('node.exe', 'cmd.exe')
|
||||
}
|
||||
|
||||
foreach ($target in $targets) {
|
||||
Stop-Process -Id $target.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 500
|
||||
$listener = Get-NetTCPConnection -LocalPort ${DEV_PORT} -State Listen -ErrorAction SilentlyContinue
|
||||
if ($listener) {
|
||||
throw "Port ${DEV_PORT} is still occupied by PID $($listener[0].OwningProcess)"
|
||||
}
|
||||
|
||||
exit 0
|
||||
`;
|
||||
|
||||
const result = spawnSync(
|
||||
"powershell.exe",
|
||||
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||
{ cwd: rootDir, stdio: "inherit" },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
if (platform() !== "win32") {
|
||||
console.error("npm run restart currently supports Windows only. Stop the existing dev process and run npm run dev.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
stopWindowsDevelopmentProcesses();
|
||||
|
||||
const npmCliPath = process.env.npm_execpath;
|
||||
if (!npmCliPath) {
|
||||
console.error("Run this command through npm: npm run restart");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Restarting Pi Agent Integrated at http://127.0.0.1:${DEV_PORT}`);
|
||||
const child = spawn(process.execPath, [npmCliPath, "run", "dev"], {
|
||||
cwd: rootDir,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(signal, () => child.kill(signal));
|
||||
}
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) process.kill(process.pid, signal);
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
||||
import { managedEnvironment, rootDir } from "./profile.mjs";
|
||||
|
||||
const target = process.argv[2] ?? "dev";
|
||||
const allowedTargets = new Set(["dev", "dev:lan", "start", "start:lan"]);
|
||||
const allowedTargets = new Set(["build", "dev", "dev:lan", "start", "start:lan"]);
|
||||
if (!allowedTargets.has(target)) {
|
||||
console.error(`Unsupported Pi Web script: ${target}`);
|
||||
process.exit(2);
|
||||
|
||||
+23
-6
@@ -3,11 +3,11 @@ import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { ensureProfile } from "./profile.mjs";
|
||||
import { ensureProfile, managedEnvironment } from "./profile.mjs";
|
||||
|
||||
const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const npmCliPath = process.env.npm_execpath;
|
||||
const setupEnv = { ...process.env };
|
||||
const setupEnv = managedEnvironment();
|
||||
|
||||
if (process.env.PI_SETUP_USE_PROXY !== "1") {
|
||||
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]) {
|
||||
@@ -91,17 +91,34 @@ function assertSupportedNodeVersion() {
|
||||
assertSupportedNodeVersion();
|
||||
ensureProfile();
|
||||
|
||||
console.log("[1/4] Installing Pi dependencies...");
|
||||
console.log("[1/5] Installing Pi dependencies...");
|
||||
run(["ci", "--ignore-scripts"], join(rootDir, "pi"));
|
||||
|
||||
console.log("[2/4] Hydrating model data and building local Pi packages...");
|
||||
console.log("[2/5] Hydrating model data and building local Pi packages...");
|
||||
hydrateModelData();
|
||||
run(["run", "build:offline"], join(rootDir, "pi"));
|
||||
|
||||
console.log("[3/4] Installing Pi Web with local Pi package links...");
|
||||
console.log("[3/5] Installing Pi Web with local Pi package links...");
|
||||
run(["ci", "--ignore-scripts", "--install-links"], join(rootDir, "pi-web"));
|
||||
|
||||
console.log("[4/4] Verifying the integration...");
|
||||
console.log("[4/5] Installing and validating managed Pi packages...");
|
||||
const packageResult = spawnSync(process.execPath, [join(rootDir, "scripts", "install-managed-packages.mjs")], {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
env: setupEnv,
|
||||
});
|
||||
if (packageResult.error) throw packageResult.error;
|
||||
if (packageResult.status !== 0) process.exit(packageResult.status ?? 1);
|
||||
|
||||
const profileResult = spawnSync(process.execPath, [join(rootDir, "scripts", "check-managed-profile.mjs")], {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
env: setupEnv,
|
||||
});
|
||||
if (profileResult.error) throw profileResult.error;
|
||||
if (profileResult.status !== 0) process.exit(profileResult.status ?? 1);
|
||||
|
||||
console.log("[5/5] Verifying the integration...");
|
||||
const checkResult = spawnSync(process.execPath, [join(rootDir, "scripts", "check-integration.mjs")], {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { rootDir } from "./profile.mjs";
|
||||
|
||||
const npmCliPath = process.env.npm_execpath;
|
||||
if (!npmCliPath) {
|
||||
console.error("Run this smoke test through npm: npm run smoke:fresh-profile");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fixtureDataDir = mkdtempSync(join(tmpdir(), "pi-agent-integrated-profile-"));
|
||||
const childEnv = {
|
||||
...process.env,
|
||||
PI_AGENT_DATA_DIR: fixtureDataDir,
|
||||
};
|
||||
if (process.env.PI_SETUP_USE_PROXY !== "1") {
|
||||
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]) {
|
||||
delete childEnv[key];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const installResult = spawnSync(
|
||||
process.execPath,
|
||||
[npmCliPath, "run", "profile:packages"],
|
||||
{ cwd: rootDir, env: childEnv, stdio: "inherit" },
|
||||
);
|
||||
if (installResult.error) throw installResult.error;
|
||||
if (installResult.status !== 0) {
|
||||
throw new Error(`Fresh profile package installation exited with ${installResult.status ?? "unknown status"}`);
|
||||
}
|
||||
|
||||
const checkResult = spawnSync(
|
||||
process.execPath,
|
||||
[npmCliPath, "run", "check:profile"],
|
||||
{ cwd: rootDir, env: childEnv, stdio: "inherit" },
|
||||
);
|
||||
if (checkResult.error) throw checkResult.error;
|
||||
if (checkResult.status !== 0) {
|
||||
throw new Error(`Fresh profile validation exited with ${checkResult.status ?? "unknown status"}`);
|
||||
}
|
||||
|
||||
const settingsPath = join(fixtureDataDir, "agent", "settings.json");
|
||||
const mcpPath = join(fixtureDataDir, "agent", "mcp.json");
|
||||
const packagePath = join(fixtureDataDir, "agent", "npm", "package.json");
|
||||
for (const path of [settingsPath, mcpPath, packagePath]) {
|
||||
if (!existsSync(path)) throw new Error(`Fresh profile is missing ${path}`);
|
||||
JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
console.log("Fresh managed profile installed and loaded successfully.");
|
||||
} finally {
|
||||
rmSync(fixtureDataDir, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user