fix: resolve workspace templates from package root

This commit is contained in:
Peter Steinberger
2026-01-31 09:07:41 +00:00
parent 68ba1afb34
commit ddc5683c67
4 changed files with 109 additions and 14 deletions

View File

@@ -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<string> {
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);
});
});

View File

@@ -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<string> | undefined;
async function pathExists(candidate: string): Promise<boolean> {
try {
await fs.access(candidate);
return true;
} catch {
return false;
}
}
export async function resolveWorkspaceTemplateDir(opts?: {
cwd?: string;
argv1?: string;
moduleUrl?: string;
}): Promise<string> {
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;
}

View File

@@ -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<string> {
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);

View File

@@ -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<string> {
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;
}