fix(cli): tolerate removed cwd in plugin authoring output (#106425)

This commit is contained in:
Leon-SK668
2026-07-14 09:58:54 +08:00
committed by GitHub
parent 4645a850d5
commit 4100d19f2a
2 changed files with 76 additions and 6 deletions

View File

@@ -3,13 +3,15 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { Type } from "typebox";
import { beforeAll, describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { defineToolPlugin, getToolPluginMetadata } from "../plugin-sdk/tool-plugin.js";
import { defaultRuntime } from "../runtime.js";
import { VERSION } from "../version.js";
import {
buildToolPluginManifest,
buildToolPluginPackageManifest,
loadToolPlugin,
runPluginsBuildCommand,
runPluginsInitCommand,
validateToolPluginProject,
} from "./plugins-authoring-command.js";
@@ -314,6 +316,68 @@ describe("plugin authoring commands", () => {
expect(loaded.metadata.tools.map((tool) => tool.name)).toEqual(["source_echo"]);
});
it("finishes a build from an absolute root after the launch directory is removed", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-deleted-cwd-build-"));
const packagePath = path.join(tmpDir, "package.json");
const entryPath = writeSourceToolPluginProject({
tmpDir,
packageName: "openclaw-plugin-deleted-cwd-build",
pluginId: "deleted-cwd-build",
toolName: "deleted_cwd_echo",
});
const originalCwd = process.cwd();
const originalWriteFileSync = fs.writeFileSync.bind(fs);
let cwdRemoved = false;
const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
const cwd = vi.spyOn(process, "cwd").mockImplementation(() => {
if (cwdRemoved) {
throw new Error("ENOENT: no such file or directory, uv_cwd");
}
return originalCwd;
});
const writeFileSync = vi
.spyOn(fs, "writeFileSync")
.mockImplementation((file, data, options) => {
originalWriteFileSync(file, data, options);
if (file === packagePath) {
cwdRemoved = true;
}
});
try {
await runPluginsBuildCommand({ root: tmpDir, entry: entryPath });
expect(fs.existsSync(path.join(tmpDir, "openclaw.plugin.json"))).toBe(true);
expect(log).toHaveBeenCalledWith(`Wrote ${path.join(tmpDir, "openclaw.plugin.json")}`);
expect(log).toHaveBeenCalledWith(`Updated ${packagePath}`);
} finally {
writeFileSync.mockRestore();
cwd.mockRestore();
log.mockRestore();
fs.rmSync(tmpDir, { force: true, recursive: true });
}
});
it("finishes init with an absolute directory after the launch directory is removed", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-deleted-cwd-init-"));
const projectDir = path.join(tmpDir, "demo");
const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
const cwd = vi.spyOn(process, "cwd").mockImplementation(() => {
throw new Error("ENOENT: no such file or directory, uv_cwd");
});
try {
await runPluginsInitCommand("demo", { directory: projectDir });
expect(fs.existsSync(path.join(projectDir, "package.json"))).toBe(true);
expect(log).toHaveBeenCalledWith(`Created ${projectDir}`);
} finally {
cwd.mockRestore();
log.mockRestore();
fs.rmSync(tmpDir, { force: true, recursive: true });
}
});
it("scaffolds a dist-entry tool plugin project", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-init-"));
const projectDir = path.join(tmpDir, "stock-quotes");

View File

@@ -2,6 +2,7 @@
import fs from "node:fs";
import path from "node:path";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { tryProcessCwd } from "../infra/safe-cwd.js";
import { getToolPluginMetadata, type ToolPluginMetadata } from "../plugin-sdk/tool-plugin.js";
import {
loadPluginManifest,
@@ -62,6 +63,13 @@ function writeJsonFile(filePath: string, value: unknown): void {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
// A removed launch directory must not turn completed authoring work into an
// uv_cwd failure while rendering the final path.
function formatOutputPath(targetPath: string, fallback: string): string {
const cwd = tryProcessCwd();
return cwd ? path.relative(cwd, targetPath) || fallback : targetPath;
}
function jsStringLiteral(value: string): string {
return JSON.stringify(value);
}
@@ -307,10 +315,8 @@ export async function runPluginsBuildCommand(opts: PluginsBuildOptions): Promise
writeJsonFile(manifestPath, manifest);
writeJsonFile(packagePath, nextPackageManifest);
defaultRuntime.log(
`Wrote ${path.relative(process.cwd(), manifestPath) || PLUGIN_MANIFEST_FILENAME}`,
);
defaultRuntime.log(`Updated ${path.relative(process.cwd(), packagePath) || "package.json"}`);
defaultRuntime.log(`Wrote ${formatOutputPath(manifestPath, PLUGIN_MANIFEST_FILENAME)}`);
defaultRuntime.log(`Updated ${formatOutputPath(packagePath, "package.json")}`);
}
export async function runPluginsValidateCommand(opts: PluginsValidateOptions): Promise<void> {
@@ -808,5 +814,5 @@ export async function runPluginsInitCommand(
}
writeJsonFile(path.join(rootDir, "tsconfig.json"), tsconfig);
writeScaffoldVitestConfig(rootDir);
defaultRuntime.log(`Created ${path.relative(process.cwd(), rootDir) || "."}`);
defaultRuntime.log(`Created ${formatOutputPath(rootDir, ".")}`);
}