Skip to content

Commit 4100d19

Browse files
authored
fix(cli): tolerate removed cwd in plugin authoring output (#106425)
1 parent 4645a85 commit 4100d19

2 files changed

Lines changed: 76 additions & 6 deletions

File tree

src/cli/plugins-authoring-command.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import fs from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
55
import { Type } from "typebox";
6-
import { beforeAll, describe, expect, it } from "vitest";
6+
import { beforeAll, describe, expect, it, vi } from "vitest";
77
import { defineToolPlugin, getToolPluginMetadata } from "../plugin-sdk/tool-plugin.js";
8+
import { defaultRuntime } from "../runtime.js";
89
import { VERSION } from "../version.js";
910
import {
1011
buildToolPluginManifest,
1112
buildToolPluginPackageManifest,
1213
loadToolPlugin,
14+
runPluginsBuildCommand,
1315
runPluginsInitCommand,
1416
validateToolPluginProject,
1517
} from "./plugins-authoring-command.js";
@@ -314,6 +316,68 @@ describe("plugin authoring commands", () => {
314316
expect(loaded.metadata.tools.map((tool) => tool.name)).toEqual(["source_echo"]);
315317
});
316318

319+
it("finishes a build from an absolute root after the launch directory is removed", async () => {
320+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-deleted-cwd-build-"));
321+
const packagePath = path.join(tmpDir, "package.json");
322+
const entryPath = writeSourceToolPluginProject({
323+
tmpDir,
324+
packageName: "openclaw-plugin-deleted-cwd-build",
325+
pluginId: "deleted-cwd-build",
326+
toolName: "deleted_cwd_echo",
327+
});
328+
const originalCwd = process.cwd();
329+
const originalWriteFileSync = fs.writeFileSync.bind(fs);
330+
let cwdRemoved = false;
331+
const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
332+
const cwd = vi.spyOn(process, "cwd").mockImplementation(() => {
333+
if (cwdRemoved) {
334+
throw new Error("ENOENT: no such file or directory, uv_cwd");
335+
}
336+
return originalCwd;
337+
});
338+
const writeFileSync = vi
339+
.spyOn(fs, "writeFileSync")
340+
.mockImplementation((file, data, options) => {
341+
originalWriteFileSync(file, data, options);
342+
if (file === packagePath) {
343+
cwdRemoved = true;
344+
}
345+
});
346+
347+
try {
348+
await runPluginsBuildCommand({ root: tmpDir, entry: entryPath });
349+
350+
expect(fs.existsSync(path.join(tmpDir, "openclaw.plugin.json"))).toBe(true);
351+
expect(log).toHaveBeenCalledWith(`Wrote ${path.join(tmpDir, "openclaw.plugin.json")}`);
352+
expect(log).toHaveBeenCalledWith(`Updated ${packagePath}`);
353+
} finally {
354+
writeFileSync.mockRestore();
355+
cwd.mockRestore();
356+
log.mockRestore();
357+
fs.rmSync(tmpDir, { force: true, recursive: true });
358+
}
359+
});
360+
361+
it("finishes init with an absolute directory after the launch directory is removed", async () => {
362+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-deleted-cwd-init-"));
363+
const projectDir = path.join(tmpDir, "demo");
364+
const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
365+
const cwd = vi.spyOn(process, "cwd").mockImplementation(() => {
366+
throw new Error("ENOENT: no such file or directory, uv_cwd");
367+
});
368+
369+
try {
370+
await runPluginsInitCommand("demo", { directory: projectDir });
371+
372+
expect(fs.existsSync(path.join(projectDir, "package.json"))).toBe(true);
373+
expect(log).toHaveBeenCalledWith(`Created ${projectDir}`);
374+
} finally {
375+
cwd.mockRestore();
376+
log.mockRestore();
377+
fs.rmSync(tmpDir, { force: true, recursive: true });
378+
}
379+
});
380+
317381
it("scaffolds a dist-entry tool plugin project", async () => {
318382
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-init-"));
319383
const projectDir = path.join(tmpDir, "stock-quotes");

src/cli/plugins-authoring-command.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import fs from "node:fs";
33
import path from "node:path";
44
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
5+
import { tryProcessCwd } from "../infra/safe-cwd.js";
56
import { getToolPluginMetadata, type ToolPluginMetadata } from "../plugin-sdk/tool-plugin.js";
67
import {
78
loadPluginManifest,
@@ -62,6 +63,13 @@ function writeJsonFile(filePath: string, value: unknown): void {
6263
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
6364
}
6465

66+
// A removed launch directory must not turn completed authoring work into an
67+
// uv_cwd failure while rendering the final path.
68+
function formatOutputPath(targetPath: string, fallback: string): string {
69+
const cwd = tryProcessCwd();
70+
return cwd ? path.relative(cwd, targetPath) || fallback : targetPath;
71+
}
72+
6573
function jsStringLiteral(value: string): string {
6674
return JSON.stringify(value);
6775
}
@@ -307,10 +315,8 @@ export async function runPluginsBuildCommand(opts: PluginsBuildOptions): Promise
307315

308316
writeJsonFile(manifestPath, manifest);
309317
writeJsonFile(packagePath, nextPackageManifest);
310-
defaultRuntime.log(
311-
`Wrote ${path.relative(process.cwd(), manifestPath) || PLUGIN_MANIFEST_FILENAME}`,
312-
);
313-
defaultRuntime.log(`Updated ${path.relative(process.cwd(), packagePath) || "package.json"}`);
318+
defaultRuntime.log(`Wrote ${formatOutputPath(manifestPath, PLUGIN_MANIFEST_FILENAME)}`);
319+
defaultRuntime.log(`Updated ${formatOutputPath(packagePath, "package.json")}`);
314320
}
315321

316322
export async function runPluginsValidateCommand(opts: PluginsValidateOptions): Promise<void> {
@@ -808,5 +814,5 @@ export async function runPluginsInitCommand(
808814
}
809815
writeJsonFile(path.join(rootDir, "tsconfig.json"), tsconfig);
810816
writeScaffoldVitestConfig(rootDir);
811-
defaultRuntime.log(`Created ${path.relative(process.cwd(), rootDir) || "."}`);
817+
defaultRuntime.log(`Created ${formatOutputPath(rootDir, ".")}`);
812818
}

0 commit comments

Comments
 (0)