feat: add project plugin initializer command

This commit is contained in:
云服务部-叶林立
2026-08-24 19:30:59 +08:00
parent a7891f18bf
commit e941e71ed8
6 changed files with 396 additions and 7 deletions
+2 -1
View File
@@ -25,6 +25,7 @@
- `@keenable/pi-search`:加载 `keenable_search` / `keenable_fetch` 及其 skill,默认 keyless`KEENABLE_API_KEY` 仅用于提高速率限制。 - `@keenable/pi-search`:加载 `keenable_search` / `keenable_fetch` 及其 skill,默认 keyless`KEENABLE_API_KEY` 仅用于提高速率限制。
- `search_config.sh``extensions/search-config.ts`:以交互式或参数方式把三家搜索 key 写入用户级 `search.env`(权限 `600`),并在搜索扩展初始化前加载;显式进程环境变量优先。 - `search_config.sh``extensions/search-config.ts`:以交互式或参数方式把三家搜索 key 写入用户级 `search.env`(权限 `600`),并在搜索扩展初始化前加载;显式进程环境变量优先。
- `extensions/mcp.ts`:通过单个共享 `pi-mcp-adapter` 实例同时连接 Exa 托管 MCP 与机器现有的 `codegraph serve --mcp`,避免重复注册 Pi 的全局 MCP flag 与命令;Exa key 只通过 `x-api-key` 请求头发送,原始工具映射为统一的来源前缀形式,CodeGraph 只暴露 `codegraph_explore` - `extensions/mcp.ts`:通过单个共享 `pi-mcp-adapter` 实例同时连接 Exa 托管 MCP 与机器现有的 `codegraph serve --mcp`,避免重复注册 Pi 的全局 MCP flag 与命令;Exa key 只通过 `x-api-key` 请求头发送,原始工具映射为统一的来源前缀形式,CodeGraph 只暴露 `codegraph_explore`
- `extensions/plugin-init.ts`:注册用户显式触发的 `/plugin_init`,在确认当前项目后只初始化缺失的 CodeGraph/Hippo 状态,并在全部成功后调用 Pi 官方热重载。
- `context-mode`:加载上游 Pi adapter 与 skills,提供 `ctx_*` 工具、隔离式大输出处理和会话连续性。 - `context-mode`:加载上游 Pi adapter 与 skills,提供 `ctx_*` 工具、隔离式大输出处理和会话连续性。
- `extensions/hashline.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`,以行哈希锚点覆盖内置 `read` / `edit`Hashline 自带 `grep` 强制关闭。 - `extensions/hashline.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`,以行哈希锚点覆盖内置 `read` / `edit`Hashline 自带 `grep` 强制关闭。
- `extensions/permission-system.ts`:直接加载仓库内 `pi-permission-system/src/index.ts`,并在权限扩展注册前将 `config/pi-permission-system.json` 同步为全局权威配置。 - `extensions/permission-system.ts`:直接加载仓库内 `pi-permission-system/src/index.ts`,并在权限扩展注册前将 `config/pi-permission-system.json` 同步为全局权威配置。
@@ -68,7 +69,7 @@
- 标准组合包工具全部命中预置目录时 Tool Search 不得调用模型、创建用户缓存或发送隐藏 schema。只有存在未识别第三方工具时才允许惰性模型增强;生成结果必须保留全部预置 assignment,模型元数据不得替代真实名称、参数或 schema。缓存必须位于 agent 目录且权限 `600`;模型失败必须继续使用预置目录 + 确定性未知工具分组。`groupOverrides` 优先级高于预置分组。 - 标准组合包工具全部命中预置目录时 Tool Search 不得调用模型、创建用户缓存或发送隐藏 schema。只有存在未识别第三方工具时才允许惰性模型增强;生成结果必须保留全部预置 assignment,模型元数据不得替代真实名称、参数或 schema。缓存必须位于 agent 目录且权限 `600`;模型失败必须继续使用预置目录 + 确定性未知工具分组。`groupOverrides` 优先级高于预置分组。
- Tool Search 必须直接使用当前 `@earendil-works/pi-coding-agent``typebox``ModelRegistry.complete()``setActiveTools()`;首次纯增加载使用当前 Pi 的增量结果传播,LRU 替换允许走宿主安全 fallback。不得安装或映射旧 `@mariozechner` runtime,不得恢复 provider payload 改写、代理分发或隐藏 `sendMessage` steer/retry 兼容循环。 - Tool Search 必须直接使用当前 `@earendil-works/pi-coding-agent``typebox``ModelRegistry.complete()``setActiveTools()`;首次纯增加载使用当前 Pi 的增量结果传播,LRU 替换允许走宿主安全 fallback。不得安装或映射旧 `@mariozechner` runtime,不得恢复 provider payload 改写、代理分发或隐藏 `sendMessage` steer/retry 兼容循环。
- `.pi-debug/``/dump-system-prompt` 生成的本地诊断目录,不提交到仓库,也不作为组合包运行时配置源。 - `.pi-debug/``/dump-system-prompt` 生成的本地诊断目录,不提交到仓库,也不作为组合包运行时配置源。
- CodeGraph 扩展只配置 Pi 到外部 `codegraph` 命令的 MCP 连接。根包安装本身不安装 CodeGraph;便捷脚本仅在组合包安装完成且用户明确选择 `Y` 后调用官方安装器。仓库不执行 `codegraph init`,不创建或管理 `.codegraph/`,也不改动索引、更新或遥测设置。 - CodeGraph MCP 扩展只配置 Pi 到外部 `codegraph` 命令的连接。根包安装本身不安装 CodeGraph;便捷脚本仅在组合包安装完成且用户明确选择 `Y` 后调用官方安装器。安装、升级和普通启动流程不得执行 `codegraph init`;只有用户在目标项目显式调用 `/plugin_init` 并确认后,命令才可以 `.codegraph/codegraph.db``.hippo/hippo.db` 为权威标志初始化缺失状态。命令必须先预检两个 CLI,任一步失败不得热重载,也不得隐瞒此前已完成的部分初始化;不得改动 CodeGraph 更新或遥测设置。
- CodeGraph MCP 使用 `keep-alive` 并只直接暴露 `codegraph_explore`;命令缺失、项目未初始化或连接失败时不得阻止其他扩展加载。 - CodeGraph MCP 使用 `keep-alive` 并只直接暴露 `codegraph_explore`;命令缺失、项目未初始化或连接失败时不得阻止其他扩展加载。
- 所有 MCP 服务必须合并到 `extensions/mcp.ts` 创建的唯一 `pi-mcp-adapter` 实例;不得新增独立 adapter 扩展入口,否则会重复注册 `--mcp-config``/mcp` 等全局接口。根测试必须扫描全部本地扩展入口并强制这一不变量。 - 所有 MCP 服务必须合并到 `extensions/mcp.ts` 创建的唯一 `pi-mcp-adapter` 实例;不得新增独立 adapter 扩展入口,否则会重复注册 `--mcp-config``/mcp` 等全局接口。根测试必须扫描全部本地扩展入口并强制这一不变量。
- Exa MCP 使用托管 Streamable HTTP 端点和 `eager` lifecycle`EXA_API_KEY` 通过 `x-api-key` 请求头发送,不得放入 URL、仓库文件或日志。所有在线搜索工具统一采用“来源名 + 原始语义工具名”的命名形式,例如 `tavily_web_search``exa_web_search``keenable_search`。Exa 高级搜索必须显式约束结果数量和文本长度;Tavily 输出默认控制 `max_results` 且非必要不请求 raw content;Keenable 优先利用中文、站点和日期筛选能力。 - Exa MCP 使用托管 Streamable HTTP 端点和 `eager` lifecycle`EXA_API_KEY` 通过 `x-api-key` 请求头发送,不得放入 URL、仓库文件或日志。所有在线搜索工具统一采用“来源名 + 原始语义工具名”的命名形式,例如 `tavily_web_search``exa_web_search``keenable_search`。Exa 高级搜索必须显式约束结果数量和文本长度;Tavily 输出默认控制 `max_results` 且非必要不请求 raw content;Keenable 优先利用中文、站点和日期筛选能力。
+15 -6
View File
@@ -109,15 +109,16 @@ Hippo 官方 Pi Extension 存在于上游 Git 仓库的 `extensions/pi-extension
`hippo-memory@1.33.0` npm tarball 中。本仓库从官方 `v1.33.0` / `hippo-memory@1.33.0` npm tarball 中。本仓库从官方 `v1.33.0` /
`e928179a3b35e8fe5837878aed071d6025ced45c` 导入源码到 `hippo-memory-pi/` 并直接加载; `e928179a3b35e8fe5837878aed071d6025ced45c` 导入源码到 `hippo-memory-pi/` 并直接加载;
匹配的 CLI 版本由 `config/hippo-memory-version` 声明。`install.sh` 会在 CLI 缺失时询问是否执行 匹配的 CLI 版本由 `config/hippo-memory-version` 声明。`install.sh` 会在 CLI 缺失时询问是否执行
`npm install -g hippo-memory@<固定版本>`,但不会选择项目或执行 `hippo init`。用户应进入需要记忆的 `npm install -g hippo-memory@<固定版本>`,但安装脚本不会选择或初始化项目。进入目标项目后可在 Pi 中执行
项目目录自行运行 `hippo init` `/plugin_init`:命令会显示当前目录并要求确认,以 `.codegraph/codegraph.db` `.hippo/hippo.db`
初始化标志,只为缺失状态运行 `codegraph init``hippo init`;全部成功后通过 Pi 官方热重载重新连接 CodeGraph MCP 并重新触发 Hippo
session start。也可以继续在终端手动执行两个 CLI 的 `init` 命令。
默认配置不需要系统 `rtk` CLI。只有以后在 `/rtk` 中主动开启 默认配置不需要系统 `rtk` CLI。只有以后在 `/rtk` 中主动开启 RTK command rewriting 时,才需要另外安装 `rtk` 可执行文件。
`RTK command rewriting` 时,才需要另外安装 `rtk` 可执行文件。
CodeGraph 本体不由组合包安装。需要使用 CodeGraph 的机器应自行确保 `codegraph` CodeGraph 本体不由组合包安装。需要使用 CodeGraph 的机器应自行确保 `codegraph`
启动 Pi 的 `PATH`,并在目标项目执行过 `codegraph init`。组合包不会创建或维护 启动 Pi 的 `PATH`。组合包只有在用户显式调用 `/plugin_init` 并确认当前项目后才会创建
`.codegraph/`,也不会修改 CodeGraph 的索引、更新或遥测设置。 `.codegraph/`;安装、升级和普通启动流程不会创建或维护索引,也不会修改 CodeGraph 的更新或遥测设置。
## 组合行为 ## 组合行为
@@ -238,6 +239,14 @@ codegraph --version
codegraph status codegraph status
``` ```
在当前项目需要同时初始化 CodeGraph 和 Hippo 时,可直接执行:
```text
/plugin_init
```
该命令拒绝文件系统根目录和用户主目录,先确认两个 CLI 均可用,再依次初始化缺失数据库状态。任一步失败都不会热重载,也不会回滚此前已成功完成的初始化;两者已初始化时仍可用该命令快速热重载扩展。
### 权限基线自动部署 ### 权限基线自动部署
`pi-permission-system/` 从上游 `pi-permission-system-v26.2.1` tag 导入,初始源码快照为 `ec4fdb11343dc94f7185b113e559a4cf9f8dc035`。根包通过本地 `file:` 依赖提供其运行时依赖,并由包装入口直接加载仓库源码,不再加载 npm 包中的实现。 `pi-permission-system/` 从上游 `pi-permission-system-v26.2.1` tag 导入,初始源码快照为 `ec4fdb11343dc94f7185b113e559a4cf9f8dc035`。根包通过本地 `file:` 依赖提供其运行时依赖,并由包装入口直接加载仓库源码,不再加载 npm 包中的实现。
+164
View File
@@ -0,0 +1,164 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { stat } from "node:fs/promises";
import { homedir } from "node:os";
import { join, parse, resolve } from "node:path";
interface ExecResult {
code: number;
stdout: string;
stderr: string;
}
export type PluginInitExecutor = (
command: string,
args: string[],
options: { cwd: string },
) => Promise<ExecResult>;
interface InitTarget {
name: "CodeGraph" | "Hippo";
command: "codegraph" | "hippo";
database: string;
initArgs(cwd: string): string[];
}
const INIT_TARGETS: InitTarget[] = [
{
name: "CodeGraph",
command: "codegraph",
database: join(".codegraph", "codegraph.db"),
initArgs: (cwd) => ["init", cwd],
},
{
name: "Hippo",
command: "hippo",
database: join(".hippo", "hippo.db"),
initArgs: () => ["init"],
},
];
async function isFile(path: string): Promise<boolean> {
try {
return (await stat(path)).isFile();
} catch (error) {
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return false;
throw error;
}
}
export function validatePluginInitCwd(cwd: string): string {
const projectRoot = resolve(cwd);
if (projectRoot === parse(projectRoot).root) {
throw new Error("拒绝在文件系统根目录初始化项目插件。");
}
if (projectRoot === resolve(homedir())) {
throw new Error("拒绝直接在用户主目录初始化项目插件;请先进入具体项目目录。");
}
return projectRoot;
}
export async function findMissingPluginInitializers(cwd: string): Promise<InitTarget[]> {
const projectRoot = validatePluginInitCwd(cwd);
const states = await Promise.all(
INIT_TARGETS.map(async (target) => ({
target,
initialized: await isFile(join(projectRoot, target.database)),
})),
);
return states.filter(({ initialized }) => !initialized).map(({ target }) => target);
}
function compactFailure(result: ExecResult): string {
const output = (result.stderr.trim() || result.stdout.trim()).replace(/\s+/gu, " ");
return output ? output.slice(0, 400) : `退出码 ${result.code}`;
}
async function executeChecked(
exec: PluginInitExecutor,
target: InitTarget,
args: string[],
cwd: string,
phase: "检测" | "初始化",
): Promise<void> {
let result: ExecResult;
try {
result = await exec(target.command, args, { cwd });
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${target.name} ${phase}失败:${detail}`, { cause: error });
}
if (result.code !== 0) {
throw new Error(`${target.name} ${phase}失败:${compactFailure(result)}`);
}
}
export async function initializeMissingPlugins(
cwd: string,
targets: InitTarget[],
exec: PluginInitExecutor,
onStart?: (target: InitTarget) => void,
): Promise<void> {
const projectRoot = validatePluginInitCwd(cwd);
// Preflight both CLIs before creating either project's state.
for (const target of INIT_TARGETS) {
await executeChecked(exec, target, ["--version"], projectRoot, "检测");
}
const completed: InitTarget[] = [];
for (const target of targets) {
onStart?.(target);
try {
await executeChecked(exec, target, target.initArgs(projectRoot), projectRoot, "初始化");
completed.push(target);
} catch (error) {
if (completed.length === 0) throw error;
const detail = error instanceof Error ? error.message : String(error);
throw new Error(
`${detail};已完成的 ${completed.map(({ name }) => name).join("、")} 初始化会保留,不会自动回滚。`,
{ cause: error },
);
}
}
}
export default function pluginInitExtension(pi: ExtensionAPI): void {
pi.registerCommand("plugin_init", {
description: "Initialize CodeGraph and Hippo for the current project, then hot-reload Pi",
handler: async (_args, ctx) => {
if (!ctx.hasUI) return;
let projectRoot: string;
let missing: InitTarget[];
try {
projectRoot = validatePluginInitCwd(ctx.cwd);
missing = await findMissingPluginInitializers(projectRoot);
} catch (error) {
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
return;
}
const pending = missing.length > 0 ? missing.map(({ name }) => name).join("、") : "无(均已初始化)";
const confirmed = await ctx.ui.confirm(
"初始化项目插件",
`项目:${projectRoot}\n待初始化:${pending}\n\n完成后将热重载 Pi 扩展。是否继续?`,
);
if (!confirmed) return;
try {
await initializeMissingPlugins(
projectRoot,
missing,
(command, args, options) => pi.exec(command, args, options),
(target) => ctx.ui.notify(`正在初始化 ${target.name}`, "info"),
);
ctx.ui.notify(
missing.length > 0 ? "项目插件初始化完成,正在热重载 Pi…" : "项目插件均已初始化,正在热重载 Pi…",
"info",
);
await ctx.reload();
} catch (error) {
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
}
},
});
}
+1
View File
@@ -47,6 +47,7 @@
"./extensions/tavily-override.ts", "./extensions/tavily-override.ts",
"./node_modules/@keenable/pi-search/src/index.ts", "./node_modules/@keenable/pi-search/src/index.ts",
"./extensions/mcp.ts", "./extensions/mcp.ts",
"./extensions/plugin-init.ts",
"./pi-rtk-optimizer/index.ts", "./pi-rtk-optimizer/index.ts",
"./extensions/hashline.ts", "./extensions/hashline.ts",
"./node_modules/context-mode/build/adapters/pi/extension.js", "./node_modules/context-mode/build/adapters/pi/extension.js",
+4
View File
@@ -15,6 +15,10 @@ test("all package extensions load together without global registration conflicts
files: string[]; files: string[];
pi: { extensions: string[] }; pi: { extensions: string[] };
}; };
assert.ok(
packageJson.pi.extensions.includes("./extensions/plugin-init.ts"),
"the bundle must expose the project plugin initializer command",
);
assert.ok( assert.ok(
packageJson.pi.extensions.includes("./hippo-memory-pi/index.ts"), packageJson.pi.extensions.includes("./hippo-memory-pi/index.ts"),
"the bundle must load the vendored official Hippo Memory extension", "the bundle must load the vendored official Hippo Memory extension",
+210
View File
@@ -0,0 +1,210 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import assert from "node:assert/strict";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { join, parse } from "node:path";
import test from "node:test";
import pluginInitExtension, {
findMissingPluginInitializers,
initializeMissingPlugins,
validatePluginInitCwd,
} from "../extensions/plugin-init.ts";
test("initializes missing CodeGraph and Hippo state after preflighting both CLIs", async () => {
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
const calls: Array<{ command: string; args: string[]; cwd: string }> = [];
try {
const targets = await findMissingPluginInitializers(cwd);
assert.deepEqual(targets.map(({ name }) => name), ["CodeGraph", "Hippo"]);
await initializeMissingPlugins(cwd, targets, async (command, args, options) => {
calls.push({ command, args, cwd: options.cwd });
return { code: 0, stdout: "ok", stderr: "" };
});
assert.deepEqual(calls, [
{ command: "codegraph", args: ["--version"], cwd },
{ command: "hippo", args: ["--version"], cwd },
{ command: "codegraph", args: ["init", cwd], cwd },
{ command: "hippo", args: ["init"], cwd },
]);
} finally {
await rm(cwd, { recursive: true, force: true });
}
});
test("uses the CLIs' database files as authoritative initialization markers", async () => {
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
const calls: string[] = [];
try {
await mkdir(join(cwd, ".codegraph"));
await mkdir(join(cwd, ".hippo"));
assert.deepEqual(
(await findMissingPluginInitializers(cwd)).map(({ name }) => name),
["CodeGraph", "Hippo"],
"bare state directories must not count as initialized",
);
await writeFile(join(cwd, ".codegraph", "codegraph.db"), "");
assert.deepEqual((await findMissingPluginInitializers(cwd)).map(({ name }) => name), ["Hippo"]);
await writeFile(join(cwd, ".hippo", "hippo.db"), "");
const targets = await findMissingPluginInitializers(cwd);
assert.deepEqual(targets, []);
await initializeMissingPlugins(cwd, targets, async (command, args) => {
calls.push(`${command} ${args.join(" ")}`);
return { code: 0, stdout: "ok", stderr: "" };
});
assert.deepEqual(calls, ["codegraph --version", "hippo --version"]);
} finally {
await rm(cwd, { recursive: true, force: true });
}
});
test("rejects unsafe project roots", () => {
assert.throws(() => validatePluginInitCwd(parse(homedir()).root), /文件系统根目录/);
assert.throws(() => validatePluginInitCwd(homedir()), /用户主目录/);
});
test("preflights both CLIs before creating project state", async () => {
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
const calls: string[] = [];
try {
const targets = await findMissingPluginInitializers(cwd);
await assert.rejects(
initializeMissingPlugins(cwd, targets, async (command, args) => {
calls.push(`${command} ${args.join(" ")}`);
return command === "hippo"
? { code: 1, stdout: "", stderr: "not installed" }
: { code: 0, stdout: "ok", stderr: "" };
}),
/Hippo 检测失败:not installed/,
);
assert.deepEqual(calls, ["codegraph --version", "hippo --version"]);
} finally {
await rm(cwd, { recursive: true, force: true });
}
});
test("plugin_init confirms the target and hot-reloads after initialization", async () => {
let command: { handler: (args: string, ctx: any) => Promise<void> } | undefined;
const calls: string[] = [];
const pi = {
registerCommand(name: string, definition: typeof command) {
assert.equal(name, "plugin_init");
command = definition;
},
exec: async (program: string, args: string[]) => {
calls.push(`${program} ${args.join(" ")}`);
return { code: 0, stdout: "ok", stderr: "" };
},
} as unknown as ExtensionAPI;
pluginInitExtension(pi);
assert.ok(command);
const registeredCommand = command!;
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
let reloaded = false;
let confirmation = "";
try {
await registeredCommand.handler("", {
cwd,
hasUI: true,
ui: {
confirm: async (_title: string, message: string) => {
confirmation = message;
return true;
},
notify() {},
},
reload: async () => {
reloaded = true;
},
});
assert.match(confirmation, new RegExp(cwd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.deepEqual(calls, [
"codegraph --version",
"hippo --version",
`codegraph init ${cwd}`,
"hippo init",
]);
assert.equal(reloaded, true);
} finally {
await rm(cwd, { recursive: true, force: true });
}
});
test("plugin_init reports retained partial state and does not reload when a later initializer fails", async () => {
let command: { handler: (args: string, ctx: any) => Promise<void> } | undefined;
const pi = {
registerCommand(_name: string, definition: typeof command) {
command = definition;
},
exec: async (program: string, args: string[]) => ({
code: program === "hippo" && args[0] === "init" ? 1 : 0,
stdout: "",
stderr: "mock failure",
}),
} as unknown as ExtensionAPI;
pluginInitExtension(pi);
assert.ok(command);
const registeredCommand = command!;
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
let reloaded = false;
let error = "";
try {
await registeredCommand.handler("", {
cwd,
hasUI: true,
ui: {
confirm: async () => true,
notify(message: string, level: string) {
if (level === "error") error = message;
},
},
reload: async () => {
reloaded = true;
},
});
assert.match(error, /Hippo 初始化失败:mock failure/);
assert.match(error, /已完成的 CodeGraph 初始化会保留,不会自动回滚/);
assert.equal(reloaded, false);
} finally {
await rm(cwd, { recursive: true, force: true });
}
});
test("plugin_init stops without executing or reloading when confirmation is cancelled", async () => {
let command: { handler: (args: string, ctx: any) => Promise<void> } | undefined;
let executed = false;
const pi = {
registerCommand(_name: string, definition: typeof command) {
command = definition;
},
exec: async () => {
executed = true;
return { code: 0, stdout: "", stderr: "" };
},
} as unknown as ExtensionAPI;
pluginInitExtension(pi);
const registeredCommand = command!;
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
let reloaded = false;
try {
await registeredCommand.handler("", {
cwd,
hasUI: true,
ui: { confirm: async () => false, notify() {} },
reload: async () => {
reloaded = true;
},
});
assert.equal(executed, false);
assert.equal(reloaded, false);
} finally {
await rm(cwd, { recursive: true, force: true });
}
});