From ddc5683c675d77427a06a3fb8b79b186e9723a2e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 31 Jan 2026 09:07:41 +0000 Subject: [PATCH] fix: resolve workspace templates from package root --- src/agents/workspace-templates.test.ts | 34 +++++++++++++ src/agents/workspace-templates.ts | 69 ++++++++++++++++++++++++++ src/agents/workspace.ts | 11 ++-- src/cli/gateway-cli/dev.ts | 9 ++-- 4 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 src/agents/workspace-templates.test.ts create mode 100644 src/agents/workspace-templates.ts diff --git a/src/agents/workspace-templates.test.ts b/src/agents/workspace-templates.test.ts new file mode 100644 index 0000000000..5619ae44f5 --- /dev/null +++ b/src/agents/workspace-templates.test.ts @@ -0,0 +1,34 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { describe, expect, it } from "vitest"; + +import { + resetWorkspaceTemplateDirCache, + resolveWorkspaceTemplateDir, +} from "./workspace-templates.js"; + +async function makeTempRoot(): Promise { + return await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-templates-")); +} + +describe("resolveWorkspaceTemplateDir", () => { + it("resolves templates from package root when module url is dist-rooted", async () => { + resetWorkspaceTemplateDirCache(); + const root = await makeTempRoot(); + await fs.writeFile(path.join(root, "package.json"), JSON.stringify({ name: "openclaw" })); + + const templatesDir = path.join(root, "docs", "reference", "templates"); + await fs.mkdir(templatesDir, { recursive: true }); + await fs.writeFile(path.join(templatesDir, "AGENTS.md"), "# ok\n"); + + const distDir = path.join(root, "dist"); + await fs.mkdir(distDir, { recursive: true }); + const moduleUrl = pathToFileURL(path.join(distDir, "model-selection.mjs")).toString(); + + const resolved = await resolveWorkspaceTemplateDir({ cwd: distDir, moduleUrl }); + expect(resolved).toBe(templatesDir); + }); +}); diff --git a/src/agents/workspace-templates.ts b/src/agents/workspace-templates.ts new file mode 100644 index 0000000000..c55e1d8ed3 --- /dev/null +++ b/src/agents/workspace-templates.ts @@ -0,0 +1,69 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js"; + +const FALLBACK_TEMPLATE_DIR = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../docs/reference/templates", +); + +let cachedTemplateDir: string | undefined; +let resolvingTemplateDir: Promise | undefined; + +async function pathExists(candidate: string): Promise { + try { + await fs.access(candidate); + return true; + } catch { + return false; + } +} + +export async function resolveWorkspaceTemplateDir(opts?: { + cwd?: string; + argv1?: string; + moduleUrl?: string; +}): Promise { + if (cachedTemplateDir) { + return cachedTemplateDir; + } + if (resolvingTemplateDir) { + return resolvingTemplateDir; + } + + resolvingTemplateDir = (async () => { + const moduleUrl = opts?.moduleUrl ?? import.meta.url; + const argv1 = opts?.argv1 ?? process.argv[1]; + const cwd = opts?.cwd ?? process.cwd(); + + const packageRoot = await resolveOpenClawPackageRoot({ moduleUrl, argv1, cwd }); + const candidates = [ + packageRoot ? path.join(packageRoot, "docs", "reference", "templates") : null, + cwd ? path.resolve(cwd, "docs", "reference", "templates") : null, + FALLBACK_TEMPLATE_DIR, + ].filter(Boolean) as string[]; + + for (const candidate of candidates) { + if (await pathExists(candidate)) { + cachedTemplateDir = candidate; + return candidate; + } + } + + cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIR; + return cachedTemplateDir; + })(); + + try { + return await resolvingTemplateDir; + } finally { + resolvingTemplateDir = undefined; + } +} + +export function resetWorkspaceTemplateDirCache() { + cachedTemplateDir = undefined; + resolvingTemplateDir = undefined; +} diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index 1d2c647468..3c7ffc0370 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -1,11 +1,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { fileURLToPath } from "node:url"; - import { isSubagentSessionKey } from "../routing/session-key.js"; import { runCommandWithTimeout } from "../process/exec.js"; import { resolveUserPath } from "../utils.js"; +import { resolveWorkspaceTemplateDir } from "./workspace-templates.js"; export function resolveDefaultAgentWorkspaceDir( env: NodeJS.ProcessEnv = process.env, @@ -29,11 +28,6 @@ export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md"; export const DEFAULT_MEMORY_FILENAME = "MEMORY.md"; export const DEFAULT_MEMORY_ALT_FILENAME = "memory.md"; -const TEMPLATE_DIR = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../../docs/reference/templates", -); - function stripFrontMatter(content: string): string { if (!content.startsWith("---")) { return content; @@ -49,7 +43,8 @@ function stripFrontMatter(content: string): string { } async function loadTemplate(name: string): Promise { - const templatePath = path.join(TEMPLATE_DIR, name); + const templateDir = await resolveWorkspaceTemplateDir(); + const templatePath = path.join(templateDir, name); try { const content = await fs.readFile(templatePath, "utf-8"); return stripFrontMatter(content); diff --git a/src/cli/gateway-cli/dev.ts b/src/cli/gateway-cli/dev.ts index 2503c05522..bd574c8d0a 100644 --- a/src/cli/gateway-cli/dev.ts +++ b/src/cli/gateway-cli/dev.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; +import { resolveWorkspaceTemplateDir } from "../../agents/workspace-templates.js"; import { handleReset } from "../../commands/onboard-helpers.js"; import { createConfigIO, writeConfigFile } from "../../config/config.js"; import { defaultRuntime } from "../../runtime.js"; @@ -13,14 +14,10 @@ const DEV_IDENTITY_THEME = "protocol droid"; const DEV_IDENTITY_EMOJI = "🤖"; const DEV_AGENT_WORKSPACE_SUFFIX = "dev"; -const DEV_TEMPLATE_DIR = path.resolve( - path.dirname(new URL(import.meta.url).pathname), - "../../../docs/reference/templates", -); - async function loadDevTemplate(name: string, fallback: string): Promise { try { - const raw = await fs.promises.readFile(path.join(DEV_TEMPLATE_DIR, name), "utf-8"); + const templateDir = await resolveWorkspaceTemplateDir(); + const raw = await fs.promises.readFile(path.join(templateDir, name), "utf-8"); if (!raw.startsWith("---")) { return raw; }