mirror of
https://github.com/cline/cline.git
synced 2026-10-25 18:33:44 +08:00
161 lines
5.3 KiB
TypeScript
161 lines
5.3 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { gzipSync } from "node:zlib";
|
|
import { $ } from "bun";
|
|
|
|
const repoRoot = resolve(import.meta.dir, "../../..");
|
|
let resources: Promise<{ hash: string; payload: string }> | undefined;
|
|
|
|
/** Embed disk-only plugin modules in every compiled CLI, including SSH runtimes. */
|
|
export function buildPluginRuntimeResources() {
|
|
resources ??= buildResources();
|
|
return resources;
|
|
}
|
|
async function buildResources() {
|
|
const directory = mkdtempSync(join(tmpdir(), "cline-plugin-runtime-"));
|
|
try {
|
|
await $`bun build ${join(repoRoot, "sdk/packages/core/src/extensions/plugin/plugin-sandbox-bootstrap.ts")} --target=node --outfile ${join(directory, "plugin-sandbox-bootstrap.js")}`;
|
|
const requireFromCore = createRequire(
|
|
join(repoRoot, "sdk/packages/core/package.json"),
|
|
);
|
|
const jitiDir = dirname(requireFromCore.resolve("jiti/package.json"));
|
|
await buildPluginHostSdk(directory);
|
|
const files: Record<string, string> = {
|
|
"node_modules/jiti/package.json": readFileSync(
|
|
join(jitiDir, "package.json"),
|
|
"utf8",
|
|
),
|
|
"node_modules/jiti/dist/babel.cjs": readFileSync(
|
|
join(jitiDir, "dist/babel.cjs"),
|
|
"utf8",
|
|
),
|
|
};
|
|
function collect(dir: string, prefix = "") {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true }).sort(
|
|
(a, b) => a.name.localeCompare(b.name),
|
|
)) {
|
|
const name = prefix + entry.name;
|
|
if (entry.isDirectory()) collect(join(dir, entry.name), `${name}/`);
|
|
else files[name] = readFileSync(join(dir, entry.name), "utf8");
|
|
}
|
|
}
|
|
collect(directory);
|
|
const json = JSON.stringify(files);
|
|
return {
|
|
hash: createHash("sha256").update(json).digest("hex"),
|
|
payload: gzipSync(json).toString("base64"),
|
|
};
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
// Public SDK entry points a plugin may import. Anything a plugin could
|
|
// reasonably `import` is here; process entrypoints (hub daemon, remote helper)
|
|
// and optional peer boundaries are not.
|
|
const PLUGIN_HOST_SDK_SUBPATHS: Record<string, string[]> = {
|
|
shared: [
|
|
".",
|
|
"./browser",
|
|
"./storage",
|
|
"./db",
|
|
"./node",
|
|
"./automation",
|
|
"./remote-config",
|
|
],
|
|
llms: [".", "./browser"],
|
|
agents: ["."],
|
|
core: [".", "./hub", "./telemetry", "./cloud"],
|
|
sdk: ["."],
|
|
};
|
|
|
|
// Plugins import the host SDK (`import { createTool } from "@cline/core"`).
|
|
// The sandbox resolves those specifiers to a `node_modules/@cline/<pkg>` it
|
|
// finds next to its bootstrap, which an npm install of the CLI provides and
|
|
// the packaged app does not. Ship one self-contained bundle of every public
|
|
// SDK entry point (the packages share most of their code) plus CommonJS
|
|
// package entry files exposing the corresponding namespace for each @cline/* export.
|
|
const buildPluginHostSdk = async (resourceDir: string): Promise<void> => {
|
|
const sdkPackagesDir = join(repoRoot, "sdk/packages");
|
|
const nodeModulesDir = join(resourceDir, "node_modules");
|
|
const bundleDir = join(nodeModulesDir, "cline-host-sdk");
|
|
const bundleFile = join(bundleDir, "index.cjs");
|
|
const namespaceFor = (pkg: string, subpath: string): string =>
|
|
`${pkg}${subpath === "." ? "" : subpath.replace(/\W+/g, "_")}`;
|
|
|
|
const entryLines: string[] = [];
|
|
const shims: Array<{
|
|
pkg: string;
|
|
version: string;
|
|
exports: Record<string, string>;
|
|
files: Array<{ name: string; namespace: string }>;
|
|
}> = [];
|
|
for (const [pkg, subpaths] of Object.entries(PLUGIN_HOST_SDK_SUBPATHS)) {
|
|
const packageDir = join(sdkPackagesDir, pkg);
|
|
const manifest = (await Bun.file(
|
|
join(packageDir, "package.json"),
|
|
).json()) as {
|
|
version: string;
|
|
exports: Record<string, { import?: string } | string>;
|
|
};
|
|
const shim: (typeof shims)[number] = {
|
|
pkg,
|
|
version: manifest.version,
|
|
exports: {},
|
|
files: [],
|
|
};
|
|
for (const subpath of subpaths) {
|
|
const exportValue = manifest.exports[subpath];
|
|
const distEntry =
|
|
typeof exportValue === "string" ? exportValue : exportValue?.import;
|
|
if (!distEntry) {
|
|
throw new Error(`@cline/${pkg} has no "${subpath}" export`);
|
|
}
|
|
const namespace = namespaceFor(pkg, subpath);
|
|
entryLines.push(
|
|
`export * as ${namespace} from ${JSON.stringify(join(packageDir, distEntry))};`,
|
|
);
|
|
const fileName =
|
|
subpath === "."
|
|
? "index.cjs"
|
|
: `${subpath.slice(2).replace(/\//g, "-")}.cjs`;
|
|
shim.exports[subpath] = `./${fileName}`;
|
|
shim.files.push({ name: fileName, namespace });
|
|
}
|
|
shims.push(shim);
|
|
}
|
|
|
|
await $`mkdir -p ${bundleDir}`;
|
|
const entryFile = join(bundleDir, "entry.ts");
|
|
await Bun.write(entryFile, `${entryLines.join("\n")}\n`);
|
|
await $`bun build ${entryFile} --target=node --format=cjs --minify --outfile ${bundleFile}`;
|
|
await $`rm ${entryFile}`;
|
|
|
|
for (const shim of shims) {
|
|
const shimDir = join(nodeModulesDir, "@cline", shim.pkg);
|
|
await $`mkdir -p ${shimDir}`;
|
|
await Bun.write(
|
|
join(shimDir, "package.json"),
|
|
`${JSON.stringify(
|
|
{
|
|
name: `@cline/${shim.pkg}`,
|
|
version: shim.version,
|
|
private: true,
|
|
exports: shim.exports,
|
|
},
|
|
null,
|
|
"\t",
|
|
)}\n`,
|
|
);
|
|
for (const file of shim.files) {
|
|
await Bun.write(
|
|
join(shimDir, file.name),
|
|
`module.exports = require("../../cline-host-sdk/index.cjs").${file.namespace};\n`,
|
|
);
|
|
}
|
|
}
|
|
};
|