feat: support git plugin installs

This commit is contained in:
Peter Steinberger
2026-05-01 10:56:44 +01:00
parent 6e3fd67084
commit 7ddf28c0d4
30 changed files with 1188 additions and 74 deletions

View File

@@ -35,6 +35,30 @@ function writePlugin([dir, id, version, method, name]) {
writePluginManifest(path.join(dir, "openclaw.plugin.json"), id);
}
function writePluginWithCli([dir, id, version, method, name, cliRoot, cliOutput]) {
for (const [value, label] of [
[dir, "dir"],
[id, "id"],
[version, "version"],
[method, "method"],
[name, "name"],
[cliRoot, "cliRoot"],
[cliOutput, "cliOutput"],
]) {
requireArg(value, label);
}
writeJson(path.join(dir, "package.json"), {
name: `@openclaw/${id}`,
version,
openclaw: { extensions: ["./index.js"] },
});
write(
path.join(dir, "index.js"),
`module.exports = { id: ${JSON.stringify(id)}, name: ${JSON.stringify(name)}, register(api) { api.registerGatewayMethod(${JSON.stringify(method)}, async () => ({ ok: true })); api.registerCli(({ program }) => { const root = program.command(${JSON.stringify(cliRoot)}).description(${JSON.stringify(`${name} fixture command`)}); root.command("ping").description("Print fixture ping output").action(() => { console.log(${JSON.stringify(cliOutput)}); }); }, { descriptors: [{ name: ${JSON.stringify(cliRoot)}, description: ${JSON.stringify(`${name} fixture command`)}, hasSubcommands: true }] }); }, };\n`,
);
writePluginManifest(path.join(dir, "openclaw.plugin.json"), id);
}
function writeClaudeBundle([root]) {
root = requireArg(root, "root");
writeJson(path.join(root, ".claude-plugin", "plugin.json"), { name: "claude-bundle-e2e" });
@@ -75,6 +99,7 @@ function writePluginMarketplace([root]) {
export const pluginCommands = {
"plugin-demo": writePluginDemo,
plugin: writePlugin,
"plugin-cli": writePluginWithCli,
"plugin-manifest": ([file, id]) =>
writePluginManifest(requireArg(file, "file"), requireArg(id, "id")),
"claude-bundle": writeClaudeBundle,

View File

@@ -4,6 +4,20 @@ import path from "node:path";
const command = process.argv[2];
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
function getInstallRecords() {
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
if (!allowLegacyCompat && !index.installRecords) {
throw new Error("expected modern installRecords in installed plugin index");
}
return allowLegacyCompat
? (index.installRecords ?? index.records ?? config.plugins?.installs ?? {})
: (index.installRecords ?? {});
}
function recordFixturePluginTrust() {
const pluginId = process.argv[3];
const pluginRoot = process.argv[4];
@@ -173,17 +187,7 @@ function assertMarketplaceInstalled() {
}
function assertMarketplaceRecords() {
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const index = readJson(indexPath);
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
if (!allowLegacyCompat && !index.installRecords) {
throw new Error("expected modern installRecords in installed plugin index");
}
const installRecords = allowLegacyCompat
? (index.installRecords ?? index.records ?? config.plugins?.installs ?? {})
: (index.installRecords ?? {});
const installRecords = getInstallRecords();
for (const id of ["marketplace-shortcut", "marketplace-direct"]) {
const record = installRecords[id];
if (!record) {
@@ -205,6 +209,56 @@ function assertMarketplaceRecords() {
}
}
function assertGitPlugin() {
const repoUrl = process.argv[3];
const gitRef = process.argv[4];
assertSimplePlugin(
"/tmp/plugins-git.json",
"/tmp/plugins-git-inspect.json",
"demo-plugin-git",
"demo.git",
);
const inspect = readJson("/tmp/plugins-git-inspect.json");
if (!Array.isArray(inspect.cliCommands) || !inspect.cliCommands.includes("demo-git")) {
throw new Error(`expected demo-git cli command, got ${inspect.cliCommands?.join(", ")}`);
}
const cliOutput = fs.readFileSync("/tmp/plugins-git-cli.txt", "utf8");
if (!cliOutput.includes("demo-plugin-git:pong")) {
throw new Error(`unexpected git plugin cli output: ${cliOutput.trim()}`);
}
const record = getInstallRecords()["demo-plugin-git"];
if (!record) {
throw new Error("missing git install record for demo-plugin-git");
}
if (record.source !== "git") {
throw new Error(`unexpected git install source: ${record.source}`);
}
if (record.gitUrl !== repoUrl) {
throw new Error(`unexpected git url: ${record.gitUrl}, expected ${repoUrl}`);
}
if (record.gitRef !== gitRef) {
throw new Error(`unexpected git ref: ${record.gitRef}, expected ${gitRef}`);
}
if (record.gitCommit !== gitRef) {
throw new Error(`unexpected git commit: ${record.gitCommit}, expected ${gitRef}`);
}
if (record.spec !== `git:${repoUrl}@${gitRef}`) {
throw new Error(`unexpected git spec: ${record.spec}`);
}
const installPath = record.installPath?.replace(/^~(?=$|\/)/u, process.env.HOME);
if (!installPath || !fs.existsSync(installPath)) {
throw new Error(`git install path missing on disk: ${installPath}`);
}
const extensionsRoot = path.join(process.env.HOME, ".openclaw", "extensions");
if (!installPath.startsWith(`${extensionsRoot}${path.sep}`)) {
throw new Error(`git install path is outside managed extensions root: ${installPath}`);
}
}
function assertRealPathInside(parentPath, childPath, label) {
const parentRealPath = fs.realpathSync(parentPath);
const childRealPath = fs.realpathSync(childPath);
@@ -414,6 +468,7 @@ const commands = {
"bundle-disabled": assertClaudeBundleDisabled,
"bundle-inspect": assertClaudeBundleInspect,
"slash-install": assertSlashInstall,
"plugin-git": assertGitPlugin,
"marketplace-list": assertMarketplaceList,
"marketplace-installed": assertMarketplaceInstalled,
"marketplace-records": assertMarketplaceRecords,

View File

@@ -20,6 +20,18 @@ write_fixture_plugin() {
node scripts/e2e/lib/fixture.mjs plugin "$dir" "$id" "$version" "$method" "$name"
}
write_fixture_plugin_with_cli() {
local dir="$1"
local id="$2"
local version="$3"
local method="$4"
local name="$5"
local cli_root="$6"
local cli_output="$7"
node scripts/e2e/lib/fixture.mjs plugin-cli "$dir" "$id" "$version" "$method" "$name" "$cli_root" "$cli_output"
}
write_fixture_manifest() {
local file="$1"
local id="$2"

View File

@@ -23,8 +23,8 @@ run_plugins_marketplace_scenario() {
run_logged install-marketplace-shortcut node "$OPENCLAW_ENTRY" plugins install marketplace-shortcut@claude-fixtures
run_logged install-marketplace-direct node "$OPENCLAW_ENTRY" plugins install marketplace-direct --marketplace claude-fixtures
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins-marketplace.json
node "$OPENCLAW_ENTRY" plugins inspect marketplace-shortcut --json >/tmp/plugins-marketplace-shortcut-inspect.json
node "$OPENCLAW_ENTRY" plugins inspect marketplace-direct --json >/tmp/plugins-marketplace-direct-inspect.json
node "$OPENCLAW_ENTRY" plugins inspect marketplace-shortcut --runtime --json >/tmp/plugins-marketplace-shortcut-inspect.json
node "$OPENCLAW_ENTRY" plugins inspect marketplace-direct --runtime --json >/tmp/plugins-marketplace-direct-inspect.json
node scripts/e2e/lib/plugins/assertions.mjs marketplace-installed
@@ -39,7 +39,7 @@ run_plugins_marketplace_scenario() {
run_logged update-marketplace-shortcut-dry-run node "$OPENCLAW_ENTRY" plugins update marketplace-shortcut --dry-run
run_logged update-marketplace-shortcut node "$OPENCLAW_ENTRY" plugins update marketplace-shortcut
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins-marketplace-updated.json
node "$OPENCLAW_ENTRY" plugins inspect marketplace-shortcut --json >/tmp/plugins-marketplace-updated-inspect.json
node "$OPENCLAW_ENTRY" plugins inspect marketplace-shortcut --runtime --json >/tmp/plugins-marketplace-updated-inspect.json
node scripts/e2e/lib/plugins/assertions.mjs marketplace-updated
}

View File

@@ -22,7 +22,7 @@ write_demo_fixture_plugin "$demo_plugin_root"
record_fixture_plugin_trust "$demo_plugin_id" "$demo_plugin_root" 1
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin --json >/tmp/plugins-inspect.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin --runtime --json >/tmp/plugins-inspect.json
node scripts/e2e/lib/plugins/assertions.mjs demo-plugin
@@ -32,7 +32,7 @@ pack_fixture_plugin "$pack_dir" /tmp/demo-plugin-tgz.tgz demo-plugin-tgz 0.0.1 d
run_logged install-tgz node "$OPENCLAW_ENTRY" plugins install /tmp/demo-plugin-tgz.tgz
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins2.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-tgz --json >/tmp/plugins2-inspect.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-tgz --runtime --json >/tmp/plugins2-inspect.json
node scripts/e2e/lib/plugins/assertions.mjs plugin-tgz
@@ -42,7 +42,7 @@ write_fixture_plugin "$dir_plugin" demo-plugin-dir 0.0.1 demo.dir "Demo Plugin D
run_logged install-dir node "$OPENCLAW_ENTRY" plugins install "$dir_plugin"
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins3.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-dir --json >/tmp/plugins3-inspect.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-dir --runtime --json >/tmp/plugins3-inspect.json
node scripts/e2e/lib/plugins/assertions.mjs plugin-dir
@@ -52,10 +52,29 @@ write_fixture_plugin "$file_pack_dir/package" demo-plugin-file 0.0.1 demo.file "
run_logged install-file node "$OPENCLAW_ENTRY" plugins install "file:$file_pack_dir/package"
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins4.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-file --json >/tmp/plugins4-inspect.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-file --runtime --json >/tmp/plugins4-inspect.json
node scripts/e2e/lib/plugins/assertions.mjs plugin-file
echo "Testing install from git repo and plugin CLI execution..."
git_fixture_root="$(mktemp -d "/tmp/openclaw-plugin-git.XXXXXX")"
git_repo="$git_fixture_root/repo"
git_repo_url="file://$git_repo"
write_fixture_plugin_with_cli "$git_repo" demo-plugin-git 0.0.1 demo.git "Demo Plugin Git" demo-git "demo-plugin-git:pong"
git -C "$git_repo" init -q
git -C "$git_repo" config user.email "docker-e2e@openclaw.local"
git -C "$git_repo" config user.name "OpenClaw Docker E2E"
git -C "$git_repo" add -A
git -C "$git_repo" commit -qm "test fixture"
git_ref="$(git -C "$git_repo" rev-parse HEAD)"
run_logged install-git node "$OPENCLAW_ENTRY" plugins install "git:$git_repo_url@$git_ref"
node "$OPENCLAW_ENTRY" plugins list --json >/tmp/plugins-git.json
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-git --runtime --json >/tmp/plugins-git-inspect.json
run_logged exec-git-plugin-cli bash -c 'node "$OPENCLAW_ENTRY" demo-git ping >/tmp/plugins-git-cli.txt'
node scripts/e2e/lib/plugins/assertions.mjs plugin-git "$git_repo_url" "$git_ref"
echo "Testing Claude bundle enable and inspect flow..."
bundle_plugin_id="claude-bundle-e2e"
bundle_root="$OPENCLAW_PLUGIN_HOME/$bundle_plugin_id"
@@ -74,7 +93,7 @@ slash_install_dir="$(mktemp -d "/tmp/openclaw-plugin-slash-install.XXXXXX")"
write_fixture_plugin "$slash_install_dir" slash-install-plugin 0.0.1 demo.slash.install "Slash Install Plugin"
run_logged install-slash-plugin node "$OPENCLAW_ENTRY" plugins install "$slash_install_dir"
node "$OPENCLAW_ENTRY" plugins inspect slash-install-plugin --json >/tmp/plugin-command-install-show.json
node "$OPENCLAW_ENTRY" plugins inspect slash-install-plugin --runtime --json >/tmp/plugin-command-install-show.json
node scripts/e2e/lib/plugins/assertions.mjs slash-install
run_plugins_marketplace_scenario