fix(ui): fix web UI after tsdown migration and typing changes

This commit is contained in:
Gustavo Madeira Santana
2026-02-03 13:56:20 -05:00
parent 1c4db91593
commit 5935c4d23d
24 changed files with 499 additions and 43 deletions

View File

@@ -15,6 +15,7 @@ import {
type SkillInstallSpec,
type SkillsInstallPreferences,
} from "./skills.js";
import { resolveBundledSkillsContext } from "./skills/bundled-context.js";
export type SkillStatusConfigCheck = {
path: string;
@@ -33,6 +34,7 @@ export type SkillStatusEntry = {
name: string;
description: string;
source: string;
bundled: boolean;
filePath: string;
baseDir: string;
skillKey: string;
@@ -167,6 +169,7 @@ function buildSkillStatus(
config?: OpenClawConfig,
prefs?: SkillsInstallPreferences,
eligibility?: SkillEligibilityContext,
bundledNames?: Set<string>,
): SkillStatusEntry {
const skillKey = resolveSkillKey(entry);
const skillConfig = resolveSkillConfig(config, skillKey);
@@ -181,6 +184,10 @@ function buildSkillStatus(
entry.frontmatter.website ??
entry.frontmatter.url;
const homepage = homepageRaw?.trim() ? homepageRaw.trim() : undefined;
const bundled =
bundledNames && bundledNames.size > 0
? bundledNames.has(entry.skill.name)
: entry.skill.source === "openclaw-bundled";
const requiredBins = entry.metadata?.requires?.bins ?? [];
const requiredAnyBins = entry.metadata?.requires?.anyBins ?? [];
@@ -256,6 +263,7 @@ function buildSkillStatus(
name: entry.skill.name,
description: entry.skill.description,
source: entry.skill.source,
bundled,
filePath: entry.skill.filePath,
baseDir: entry.skill.baseDir,
skillKey,
@@ -289,13 +297,20 @@ export function buildWorkspaceSkillStatus(
},
): SkillStatusReport {
const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
const skillEntries = opts?.entries ?? loadWorkspaceSkillEntries(workspaceDir, opts);
const bundledContext = resolveBundledSkillsContext();
const skillEntries =
opts?.entries ??
loadWorkspaceSkillEntries(workspaceDir, {
config: opts?.config,
managedSkillsDir,
bundledSkillsDir: bundledContext.dir,
});
const prefs = resolveSkillsInstallPreferences(opts?.config);
return {
workspaceDir,
managedSkillsDir,
skills: skillEntries.map((entry) =>
buildSkillStatus(entry, opts?.config, prefs, opts?.eligibility),
buildSkillStatus(entry, opts?.config, prefs, opts?.eligibility, bundledContext.names),
),
};
}

View File

@@ -0,0 +1,24 @@
import { loadSkillsFromDir } from "@mariozechner/pi-coding-agent";
import { resolveBundledSkillsDir, type BundledSkillsResolveOptions } from "./bundled-dir.js";
export type BundledSkillsContext = {
dir?: string;
names: Set<string>;
};
export function resolveBundledSkillsContext(
opts: BundledSkillsResolveOptions = {},
): BundledSkillsContext {
const dir = resolveBundledSkillsDir(opts);
const names = new Set<string>();
if (!dir) {
return { dir, names };
}
const result = loadSkillsFromDir({ dir, source: "openclaw-bundled" });
for (const skill of result.skills) {
if (skill.name.trim()) {
names.add(skill.name);
}
}
return { dir, names };
}

View File

@@ -0,0 +1,54 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { resolveBundledSkillsDir } from "./bundled-dir.js";
async function writeSkill(dir: string, name: string) {
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(
path.join(dir, "SKILL.md"),
`---\nname: ${name}\ndescription: ${name}\n---\n\n# ${name}\n`,
"utf-8",
);
}
describe("resolveBundledSkillsDir", () => {
const originalOverride = process.env.OPENCLAW_BUNDLED_SKILLS_DIR;
afterEach(() => {
if (originalOverride === undefined) {
delete process.env.OPENCLAW_BUNDLED_SKILLS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_SKILLS_DIR = originalOverride;
}
});
it("resolves bundled skills under a flattened dist layout", async () => {
delete process.env.OPENCLAW_BUNDLED_SKILLS_DIR;
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-bundled-"));
await fs.writeFile(path.join(root, "package.json"), JSON.stringify({ name: "openclaw" }));
await writeSkill(path.join(root, "skills", "peekaboo"), "peekaboo");
const distDir = path.join(root, "dist");
await fs.mkdir(distDir, { recursive: true });
const argv1 = path.join(distDir, "index.js");
await fs.writeFile(argv1, "// stub", "utf-8");
const moduleUrl = pathToFileURL(path.join(distDir, "skills.js")).href;
const execPath = path.join(root, "bin", "node");
await fs.mkdir(path.dirname(execPath), { recursive: true });
const resolved = resolveBundledSkillsDir({
argv1,
moduleUrl,
cwd: distDir,
execPath,
});
expect(resolved).toBe(path.join(root, "skills"));
});
});

View File

@@ -1,8 +1,41 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
export function resolveBundledSkillsDir(): string | undefined {
function looksLikeSkillsDir(dir: string): boolean {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".")) {
continue;
}
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && entry.name.endsWith(".md")) {
return true;
}
if (entry.isDirectory()) {
if (fs.existsSync(path.join(fullPath, "SKILL.md"))) {
return true;
}
}
}
} catch {
return false;
}
return false;
}
export type BundledSkillsResolveOptions = {
argv1?: string;
moduleUrl?: string;
cwd?: string;
execPath?: string;
};
export function resolveBundledSkillsDir(
opts: BundledSkillsResolveOptions = {},
): string | undefined {
const override = process.env.OPENCLAW_BUNDLED_SKILLS_DIR?.trim();
if (override) {
return override;
@@ -10,7 +43,8 @@ export function resolveBundledSkillsDir(): string | undefined {
// bun --compile: ship a sibling `skills/` next to the executable.
try {
const execDir = path.dirname(process.execPath);
const execPath = opts.execPath ?? process.execPath;
const execDir = path.dirname(execPath);
const sibling = path.join(execDir, "skills");
if (fs.existsSync(sibling)) {
return sibling;
@@ -21,11 +55,32 @@ export function resolveBundledSkillsDir(): string | undefined {
// npm/dev: resolve `<packageRoot>/skills` relative to this module.
try {
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(moduleDir, "..", "..", "..");
const candidate = path.join(root, "skills");
if (fs.existsSync(candidate)) {
return candidate;
const moduleUrl = opts.moduleUrl ?? import.meta.url;
const moduleDir = path.dirname(fileURLToPath(moduleUrl));
const argv1 = opts.argv1 ?? process.argv[1];
const cwd = opts.cwd ?? process.cwd();
const packageRoot = resolveOpenClawPackageRootSync({
argv1,
moduleUrl,
cwd,
});
if (packageRoot) {
const candidate = path.join(packageRoot, "skills");
if (looksLikeSkillsDir(candidate)) {
return candidate;
}
}
let current = moduleDir;
for (let depth = 0; depth < 6; depth += 1) {
const candidate = path.join(current, "skills");
if (looksLikeSkillsDir(candidate)) {
return candidate;
}
const next = path.dirname(current);
if (next === current) {
break;
}
current = next;
}
} catch {
// ignore