import fs from 'node:fs';
import path from 'node:path';
export const OPENCLAW_PLUGIN_SDK_PREFIX = 'openclaw/plugin-sdk/';
const OPENCLAW_PLUGIN_SDK_SPECIFIER_RE = /(["'])openclaw\/plugin-sdk\/([^"'\r\n]+)\1/g;
function assertSafePluginSdkSubpath(subpath) {
if (!subpath || subpath.startsWith('/') || subpath.startsWith('\\')) {
throw new Error(`Invalid OpenClaw plugin-sdk import subpath: ${JSON.stringify(subpath)}`);
}
const parts = subpath.split(/[\\/]+/);
if (parts.some((part) => !part || part === '.' || part === '..')) {
throw new Error(`Invalid OpenClaw plugin-sdk import subpath: ${JSON.stringify(subpath)}`);
}
}
export function toImportSpecifier(relativePath) {
const normalized = relativePath.split(path.sep).join('/');
if (normalized.startsWith('./') || normalized.startsWith('../')) {
return normalized;
}
return `./${normalized}`;
}
export function resolvePluginSdkTarget(distDir, subpath) {
assertSafePluginSdkSubpath(subpath);
const targetSubpath = subpath.endsWith('.js') ? subpath : `${subpath}.js`;
return path.join(distDir, 'plugin-sdk', ...targetSubpath.split(/[\\/]+/));
}
export function rewriteOpenClawPluginSdkSpecifiers(content, options) {
const { filePath, distDir } = options;
let replacements = 0;
const nextContent = content.replace(
OPENCLAW_PLUGIN_SDK_SPECIFIER_RE,
(match, quote, subpath) => {
const target = resolvePluginSdkTarget(distDir, subpath);
if (!fs.existsSync(target)) {
throw new Error(
`Cannot rewrite ${match} in ${filePath}: missing bundled SDK target ${target}`,
);
}
replacements++;
const relativePath = path.relative(path.dirname(filePath), target);
return `${quote}${toImportSpecifier(relativePath)}${quote}`;
},
);
return {
content: nextContent,
replacements,
};
}
function listJavaScriptFiles(rootDir) {
const files = [];
const stack = [rootDir];
while (stack.length > 0) {
const current = stack.pop();
let entries;
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
continue;
}
if (entry.isFile() && entry.name.endsWith('.js')) {
files.push(fullPath);
}
}
}
return files;
}
export function patchExtensionOpenClawSelfImports(outputDir) {
const distDir = path.join(outputDir, 'dist');
const extensionsDir = path.join(distDir, 'extensions');
if (!fs.existsSync(extensionsDir)) {
return {
filesScanned: 0,
filesPatched: 0,
specifiersPatched: 0,
};
}
let filesScanned = 0;
let filesPatched = 0;
let specifiersPatched = 0;
for (const filePath of listJavaScriptFiles(extensionsDir)) {
filesScanned++;
const content = fs.readFileSync(filePath, 'utf8');
if (!content.includes(OPENCLAW_PLUGIN_SDK_PREFIX)) {
continue;
}
const result = rewriteOpenClawPluginSdkSpecifiers(content, {
filePath,
distDir,
});
if (result.replacements > 0 && result.content !== content) {
fs.writeFileSync(filePath, result.content, 'utf8');
filesPatched++;
specifiersPatched += result.replacements;
}
}
return {
filesScanned,
filesPatched,
specifiersPatched,
};
}