import fs from "node:fs"; 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]; const enabled = process.argv[5] === "1"; const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); const config = fs.existsSync(configPath) ? readJson(configPath) : {}; const plugins = (config.plugins ??= {}); const entries = (plugins.entries ??= {}); entries[pluginId] = { ...entries[pluginId], enabled }; delete plugins.installs; plugins.allow = Array.from(new Set([...(plugins.allow ?? []), pluginId])).toSorted((a, b) => a.localeCompare(b), ); fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); const ledgerPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); const ledger = fs.existsSync(ledgerPath) ? readJson(ledgerPath) : { version: 1, warning: "DO NOT EDIT. This file is generated by OpenClaw plugin install/update/uninstall commands. Use `openclaw plugins install/update/uninstall` instead.", records: {}, }; ledger.updatedAtMs = Date.now(); ledger.records ??= {}; ledger.records[pluginId] = { ...ledger.records[pluginId], source: "path", installPath: pluginRoot, sourcePath: pluginRoot, }; fs.mkdirSync(path.dirname(ledgerPath), { recursive: true }); fs.writeFileSync(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, "utf8"); } function assertDemoPlugin() { const data = readJson("/tmp/plugins.json"); const inspect = readJson("/tmp/plugins-inspect.json"); const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin"); if (!plugin) { throw new Error("plugin not found"); } if (plugin.status !== "loaded") { throw new Error(`unexpected status: ${plugin.status}`); } const assertIncludes = (list, value, label) => { if (!Array.isArray(list) || !list.includes(value)) { throw new Error(`${label} missing: ${value}`); } }; const inspectToolNames = Array.isArray(inspect.tools) ? inspect.tools.flatMap((entry) => (Array.isArray(entry?.names) ? entry.names : [])) : []; assertIncludes(inspectToolNames, "demo_tool", "tool"); assertIncludes(inspect.gatewayMethods, "demo.ping", "gateway method"); assertIncludes(inspect.cliCommands, "demo", "cli command"); assertIncludes(inspect.services, "demo-service", "service"); const diagErrors = (data.diagnostics || []).filter((diag) => diag.level === "error"); if (diagErrors.length > 0) { throw new Error(`diagnostics errors: ${diagErrors.map((diag) => diag.message).join("; ")}`); } } function assertSimplePlugin(jsonFile, inspectFile, pluginId, method) { const data = readJson(jsonFile); const inspect = readJson(inspectFile); const plugin = (data.plugins || []).find((entry) => entry.id === pluginId); if (!plugin) { throw new Error(`${pluginId} plugin not found`); } if (plugin.status !== "loaded") { throw new Error(`unexpected status: ${plugin.status}`); } if (!Array.isArray(inspect.gatewayMethods) || !inspect.gatewayMethods.includes(method)) { throw new Error(`expected gateway method ${method}`); } } function assertUpdateOutput(logFile, expectedSnippet) { const output = fs.readFileSync(logFile, "utf8"); if (!output.includes(expectedSnippet)) { throw new Error( `expected update output to include ${JSON.stringify(expectedSnippet)}:\n${output}`, ); } } function assertClaudeBundleDisabled() { const data = readJson("/tmp/plugins-bundle-disabled.json"); const plugin = (data.plugins || []).find((entry) => entry.id === "claude-bundle-e2e"); if (!plugin) { throw new Error("Claude bundle plugin not found"); } if (plugin.status !== "disabled") { throw new Error(`expected disabled bundle before enable, got ${plugin.status}`); } } function assertClaudeBundleInspect() { const inspect = readJson("/tmp/plugins-bundle-inspect.json"); if (inspect.plugin?.bundleFormat !== "claude") { throw new Error(`expected Claude bundle format, got ${inspect.plugin?.bundleFormat}`); } if (inspect.plugin?.enabled !== true || inspect.plugin?.status !== "loaded") { throw new Error( `expected enabled loaded Claude bundle, got enabled=${inspect.plugin?.enabled} status=${inspect.plugin?.status}`, ); } } function assertSlashInstall() { const inspect = readJson("/tmp/plugin-command-install-show.json"); if (inspect.plugin?.status !== "loaded") { throw new Error(`expected loaded status after install, got ${inspect.plugin?.status}`); } if (inspect.plugin?.enabled !== true) { throw new Error(`expected enabled status after install, got ${inspect.plugin?.enabled}`); } if (!inspect.gatewayMethods.includes("demo.slash.install")) { throw new Error(`expected installed gateway method, got ${inspect.gatewayMethods.join(", ")}`); } } function parseClawHubPackageName(rawSpec) { const value = rawSpec.slice("clawhub:".length).trim(); const slashIndex = value.lastIndexOf("/"); const atIndex = value.lastIndexOf("@"); return atIndex > 0 && atIndex > slashIndex ? value.slice(0, atIndex) : value; } function assertMarketplaceList() { const data = readJson("/tmp/marketplace-list.json"); const names = (data.plugins || []).map((entry) => entry.name).toSorted(); if (data.name !== "Fixture Marketplace") { throw new Error(`unexpected marketplace name: ${data.name}`); } if (!names.includes("marketplace-shortcut") || !names.includes("marketplace-direct")) { throw new Error(`unexpected marketplace plugins: ${names.join(", ")}`); } } function assertMarketplaceInstalled() { const data = readJson("/tmp/plugins-marketplace.json"); const shortcutInspect = readJson("/tmp/plugins-marketplace-shortcut-inspect.json"); const directInspect = readJson("/tmp/plugins-marketplace-direct-inspect.json"); const getPlugin = (id) => { const plugin = (data.plugins || []).find((entry) => entry.id === id); if (!plugin) { throw new Error(`plugin not found: ${id}`); } if (plugin.status !== "loaded") { throw new Error(`unexpected status for ${id}: ${plugin.status}`); } return plugin; }; const shortcut = getPlugin("marketplace-shortcut"); const direct = getPlugin("marketplace-direct"); if (shortcut.version !== "0.0.1") { throw new Error(`unexpected shortcut version: ${shortcut.version}`); } if (direct.version !== "0.0.1") { throw new Error(`unexpected direct version: ${direct.version}`); } if (!shortcutInspect.gatewayMethods.includes("demo.marketplace.shortcut.v1")) { throw new Error("expected marketplace shortcut gateway method"); } if (!directInspect.gatewayMethods.includes("demo.marketplace.direct.v1")) { throw new Error("expected marketplace direct gateway method"); } } function assertMarketplaceRecords() { const installRecords = getInstallRecords(); for (const id of ["marketplace-shortcut", "marketplace-direct"]) { const record = installRecords[id]; if (!record) { if (allowLegacyCompat) { console.log(`legacy package did not persist marketplace install record for ${id}`); continue; } throw new Error(`missing marketplace install record for ${id}`); } if (record.source !== "marketplace") { throw new Error(`unexpected source for ${id}: ${record.source}`); } if (record.marketplaceSource !== "claude-fixtures") { throw new Error(`unexpected marketplace source for ${id}: ${record.marketplaceSource}`); } if (record.marketplacePlugin !== id) { throw new Error(`unexpected marketplace plugin for ${id}: ${record.marketplacePlugin}`); } } } 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 gitRoot = path.join(process.env.HOME, ".openclaw", "git"); if (!installPath.endsWith(`${path.sep}repo`)) { throw new Error(`git install path should point at cloned repo root: ${installPath}`); } assertRealPathInside(gitRoot, installPath, "git install path"); const dependencyPackagePath = path.join(installPath, "node_modules", "is-number", "package.json"); if (!fs.existsSync(dependencyPackagePath)) { throw new Error(`missing git plugin installed dependency: ${dependencyPackagePath}`); } assertRealPathInside(installPath, dependencyPackagePath, "git plugin installed dependency"); } function assertRealPathInside(parentPath, childPath, label) { const parentRealPath = fs.realpathSync(parentPath); const childRealPath = fs.realpathSync(childPath); if ( childRealPath !== parentRealPath && !childRealPath.startsWith(`${parentRealPath}${path.sep}`) ) { throw new Error(`${label} resolved outside ${parentPath}: ${childRealPath}`); } } function assertClawHubExternalInstallContract(installPath) { const openclawPeerPath = path.join(installPath, "node_modules", "openclaw"); if (!fs.existsSync(openclawPeerPath)) { throw new Error(`missing ClawHub openclaw peer symlink: ${openclawPeerPath}`); } if (!fs.lstatSync(openclawPeerPath).isSymbolicLink()) { throw new Error(`ClawHub openclaw peer is not a symlink: ${openclawPeerPath}`); } const hostRoot = fs.realpathSync(process.cwd()); const linkedHostRoot = fs.realpathSync(openclawPeerPath); if (linkedHostRoot !== hostRoot) { throw new Error(`expected ClawHub openclaw peer ${linkedHostRoot} to target ${hostRoot}`); } const dependencyPackagePath = path.join(installPath, "node_modules", "is-number", "package.json"); if (fs.existsSync(dependencyPackagePath)) { assertRealPathInside(installPath, dependencyPackagePath, "ClawHub isolated dependency"); } } function assertClawHubArtifactMetadata(record, pluginId) { if (record.artifactKind === "legacy-zip") { if (record.artifactFormat !== "zip") { throw new Error( `missing ClawHub legacy ZIP artifact metadata for ${pluginId}: ${JSON.stringify(record)}`, ); } return; } if (record.artifactKind !== "npm-pack" || record.artifactFormat !== "tgz") { throw new Error(`missing ClawHub artifact metadata for ${pluginId}: ${JSON.stringify(record)}`); } if (!record.clawpackSha256 || typeof record.clawpackSize !== "number") { throw new Error(`missing ClawHub ClawPack metadata for ${pluginId}: ${JSON.stringify(record)}`); } if (!record.npmIntegrity || !record.npmShasum || !record.npmTarballName) { throw new Error( `missing ClawHub npm artifact metadata for ${pluginId}: ${JSON.stringify(record)}`, ); } } function assertPluginDirDeps() { const sourceDir = process.argv[3]; assertSimplePlugin( "/tmp/plugins-dir-deps.json", "/tmp/plugins-dir-deps-inspect.json", "demo-plugin-dir-deps", "demo.dir.deps", ); const record = getInstallRecords()["demo-plugin-dir-deps"]; if (!record) { throw new Error("missing local dependency plugin install record"); } if (record.source !== "path") { throw new Error(`unexpected local dependency plugin source: ${record.source}`); } if (record.sourcePath !== sourceDir) { throw new Error(`unexpected local dependency plugin source path: ${record.sourcePath}`); } const installPath = record.installPath?.replace(/^~(?=$|\/)/u, process.env.HOME); if (!installPath || !fs.existsSync(installPath)) { throw new Error(`local dependency plugin install path missing on disk: ${installPath}`); } const dependencyPackagePath = path.join(installPath, "node_modules", "is-number", "package.json"); if (!fs.existsSync(dependencyPackagePath)) { throw new Error(`missing copied local plugin dependency: ${dependencyPackagePath}`); } assertRealPathInside(installPath, dependencyPackagePath, "local plugin copied dependency"); } function assertLocalPathUpdateSkipped() { assertUpdateOutput("/tmp/plugins-dir-update.log", 'Skipping "demo-plugin-dir" (source: path).'); } function assertNpmPlugin() { assertSimplePlugin( "/tmp/plugins-npm.json", "/tmp/plugins-npm-inspect.json", "demo-plugin-npm", "demo.npm", ); const inspect = readJson("/tmp/plugins-npm-inspect.json"); if (!Array.isArray(inspect.cliCommands) || !inspect.cliCommands.includes("demo-npm")) { throw new Error(`expected demo-npm cli command, got ${inspect.cliCommands?.join(", ")}`); } const cliOutput = fs.readFileSync("/tmp/plugins-npm-cli.txt", "utf8"); if (!cliOutput.includes("demo-plugin-npm:pong")) { throw new Error(`unexpected npm plugin cli output: ${cliOutput.trim()}`); } const record = getInstallRecords()["demo-plugin-npm"]; if (!record) { throw new Error("missing npm install record for demo-plugin-npm"); } if (record.source !== "npm") { throw new Error(`unexpected npm install source: ${record.source}`); } if (record.spec !== "@openclaw/demo-plugin-npm@0.0.1") { throw new Error(`unexpected npm spec: ${record.spec}`); } if (record.resolvedName !== "@openclaw/demo-plugin-npm") { throw new Error(`unexpected npm resolved name: ${record.resolvedName}`); } if (record.resolvedVersion !== "0.0.1") { throw new Error(`unexpected npm resolved version: ${record.resolvedVersion}`); } const installPath = record.installPath?.replace(/^~(?=$|\/)/u, process.env.HOME); if (!installPath || !fs.existsSync(installPath)) { throw new Error(`npm install path missing on disk: ${installPath}`); } const nodeModulesRoot = path.dirname(path.dirname(installPath)); const npmRoot = path.dirname(nodeModulesRoot); const dependencyPackagePath = path.join(nodeModulesRoot, "is-number", "package.json"); if (!fs.existsSync(dependencyPackagePath)) { throw new Error(`missing npm plugin installed dependency: ${dependencyPackagePath}`); } assertRealPathInside(npmRoot, dependencyPackagePath, "npm plugin installed dependency"); } function assertNpmPluginUpdateUnchanged() { assertUpdateOutput("/tmp/plugins-npm-update.log", "demo-plugin-npm is up to date (0.0.1)."); assertNpmPlugin(); } function assertMarketplaceUpdated() { const data = readJson("/tmp/plugins-marketplace-updated.json"); const inspect = readJson("/tmp/plugins-marketplace-updated-inspect.json"); const plugin = (data.plugins || []).find((entry) => entry.id === "marketplace-shortcut"); if (!plugin) { throw new Error("updated marketplace plugin not found"); } if (plugin.version !== "0.0.2") { throw new Error(`unexpected updated version: ${plugin.version}`); } if (!inspect.gatewayMethods.includes("demo.marketplace.shortcut.v2")) { throw new Error(`expected updated gateway method, got ${inspect.gatewayMethods.join(", ")}`); } } function assertGitPluginUpdated() { const beforeCommit = process.argv[3]; assertSimplePlugin( "/tmp/plugins-git-update.json", "/tmp/plugins-git-update-inspect.json", "demo-plugin-git-update", "demo.git.update.v2", ); const inspect = readJson("/tmp/plugins-git-update-inspect.json"); if (!Array.isArray(inspect.cliCommands) || !inspect.cliCommands.includes("demo-git-update")) { throw new Error(`expected demo-git-update cli command, got ${inspect.cliCommands?.join(", ")}`); } const cliOutput = fs.readFileSync("/tmp/plugins-git-update-cli.txt", "utf8"); if (!cliOutput.includes("demo-plugin-git-update:pong-v2")) { throw new Error(`unexpected updated git plugin cli output: ${cliOutput.trim()}`); } const record = getInstallRecords()["demo-plugin-git-update"]; if (!record) { throw new Error("missing git update install record for demo-plugin-git-update"); } if (record.source !== "git") { throw new Error(`unexpected git update source: ${record.source}`); } if (record.gitRef !== "main") { throw new Error(`unexpected git update ref: ${record.gitRef}`); } if (!record.gitCommit || record.gitCommit === beforeCommit) { throw new Error( `expected git update commit to advance from ${beforeCommit}, got ${record.gitCommit}`, ); } if (record.version !== "0.0.2") { throw new Error(`unexpected git update version: ${record.version}`); } assertUpdateOutput( "/tmp/plugins-git-update.log", "Updated demo-plugin-git-update: 0.0.1 -> 0.0.2.", ); } async function assertClawHubPreflight() { const spec = process.env.CLAWHUB_PLUGIN_SPEC; if (!spec?.startsWith("clawhub:")) { throw new Error(`expected clawhub: spec, got ${spec}`); } const packageName = parseClawHubPackageName(spec); const baseUrl = ( process.env.OPENCLAW_CLAWHUB_URL || process.env.CLAWHUB_URL || "https://clawhub.ai" ).replace(/\/+$/, ""); const token = process.env.OPENCLAW_CLAWHUB_TOKEN || process.env.CLAWHUB_TOKEN || process.env.CLAWHUB_AUTH_TOKEN || ""; const response = await fetch(`${baseUrl}/api/v1/packages/${encodeURIComponent(packageName)}`, { headers: token ? { Authorization: `Bearer ${token}` } : undefined, }); if (!response.ok) { const body = await response.text().catch(() => ""); throw new Error( `ClawHub package preflight failed for ${packageName}: ${response.status} ${body}`, ); } const detail = await response.json(); const family = detail.package?.family; if (family !== "code-plugin" && family !== "bundle-plugin") { throw new Error(`ClawHub package ${packageName} is not installable as a plugin: ${family}`); } if (detail.package?.runtimeId && detail.package.runtimeId !== process.env.CLAWHUB_PLUGIN_ID) { throw new Error( `ClawHub package ${packageName} runtimeId ${detail.package.runtimeId} does not match expected ${process.env.CLAWHUB_PLUGIN_ID}`, ); } console.log(`Using ClawHub package ${packageName} (${family}).`); } function assertClawHubInstalled() { const pluginId = process.env.CLAWHUB_PLUGIN_ID; const spec = process.env.CLAWHUB_PLUGIN_SPEC; const packageName = parseClawHubPackageName(spec); const list = readJson("/tmp/plugins-clawhub-installed.json"); const inspect = readJson("/tmp/plugins-clawhub-inspect.json"); const plugin = (list.plugins || []).find((entry) => entry.id === pluginId); if (!plugin) { throw new Error(`ClawHub plugin not found after install: ${pluginId}`); } if (plugin.status !== "loaded") { throw new Error(`unexpected ClawHub plugin status for ${pluginId}: ${plugin.status}`); } if (inspect.plugin?.id !== pluginId) { throw new Error(`unexpected ClawHub inspect plugin id: ${inspect.plugin?.id}`); } 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 record = installRecords[pluginId]; if (!record) { throw new Error(`missing ClawHub install record for ${pluginId}`); } if (record.source !== "clawhub") { throw new Error(`unexpected ClawHub install source for ${pluginId}: ${record.source}`); } if (record.clawhubPackage !== packageName) { throw new Error( `unexpected ClawHub package for ${pluginId}: ${record.clawhubPackage}, expected ${packageName}`, ); } if (record.clawhubFamily !== "code-plugin" && record.clawhubFamily !== "bundle-plugin") { throw new Error(`unexpected ClawHub family for ${pluginId}: ${record.clawhubFamily}`); } if (typeof record.installPath !== "string" || record.installPath.length === 0) { throw new Error(`missing ClawHub install path for ${pluginId}`); } assertClawHubArtifactMetadata(record, pluginId); const installPath = record.installPath.replace(/^~(?=$|\/)/u, process.env.HOME); const extensionsRoot = path.join(process.env.HOME, ".openclaw", "extensions"); if (!installPath.startsWith(`${extensionsRoot}${path.sep}`)) { throw new Error(`ClawHub install path is outside managed extensions root: ${installPath}`); } if (!fs.existsSync(installPath)) { throw new Error(`ClawHub install path missing on disk: ${installPath}`); } if (record.artifactKind === "npm-pack") { assertClawHubExternalInstallContract(installPath); } fs.writeFileSync("/tmp/plugins-clawhub-install-path.txt", installPath, "utf8"); } function assertClawHubRemoved() { const pluginId = process.env.CLAWHUB_PLUGIN_ID; const installPath = fs.readFileSync("/tmp/plugins-clawhub-install-path.txt", "utf8").trim(); const list = readJson("/tmp/plugins-clawhub-uninstalled.json"); if ((list.plugins || []).some((entry) => entry.id === pluginId)) { throw new Error(`ClawHub plugin still listed after uninstall: ${pluginId}`); } 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 installRecords = index.installRecords ?? index.records ?? config.plugins?.installs ?? {}; if (installRecords[pluginId]) { throw new Error(`ClawHub install record still present after uninstall: ${pluginId}`); } const configAfterUninstallPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); const configAfterUninstall = fs.existsSync(configAfterUninstallPath) ? readJson(configAfterUninstallPath) : {}; if (configAfterUninstall.plugins?.entries?.[pluginId]) { throw new Error(`ClawHub config entry still present after uninstall: ${pluginId}`); } if ((configAfterUninstall.plugins?.allow || []).includes(pluginId)) { throw new Error(`ClawHub allowlist entry still present after uninstall: ${pluginId}`); } if ((configAfterUninstall.plugins?.deny || []).includes(pluginId)) { throw new Error(`ClawHub denylist entry still present after uninstall: ${pluginId}`); } if (fs.existsSync(installPath)) { throw new Error( `ClawHub managed install directory still exists after uninstall: ${installPath}`, ); } } function assertClawHubUpdated() { const output = fs.readFileSync("/tmp/plugins-clawhub-update.log", "utf8"); if (!output.includes(`${process.env.CLAWHUB_PLUGIN_ID} already at `)) { throw new Error(`expected ClawHub update to report already-at version:\n${output}`); } assertClawHubInstalled(); } const commands = { "record-fixture-plugin-trust": recordFixturePluginTrust, "demo-plugin": assertDemoPlugin, "plugin-tgz": () => assertSimplePlugin( "/tmp/plugins2.json", "/tmp/plugins2-inspect.json", "demo-plugin-tgz", "demo.tgz", ), "plugin-dir": () => assertSimplePlugin( "/tmp/plugins3.json", "/tmp/plugins3-inspect.json", "demo-plugin-dir", "demo.dir", ), "plugin-dir-update-skipped": assertLocalPathUpdateSkipped, "plugin-dir-deps": assertPluginDirDeps, "plugin-file": () => assertSimplePlugin( "/tmp/plugins4.json", "/tmp/plugins4-inspect.json", "demo-plugin-file", "demo.file", ), "plugin-npm": assertNpmPlugin, "plugin-npm-update": assertNpmPluginUpdateUnchanged, "bundle-disabled": assertClaudeBundleDisabled, "bundle-inspect": assertClaudeBundleInspect, "slash-install": assertSlashInstall, "plugin-git": assertGitPlugin, "plugin-git-updated": assertGitPluginUpdated, "marketplace-list": assertMarketplaceList, "marketplace-installed": assertMarketplaceInstalled, "marketplace-records": assertMarketplaceRecords, "marketplace-updated": assertMarketplaceUpdated, "clawhub-preflight": assertClawHubPreflight, "clawhub-installed": assertClawHubInstalled, "clawhub-updated": assertClawHubUpdated, "clawhub-removed": assertClawHubRemoved, }; const fn = commands[command]; if (!fn) { throw new Error(`unknown plugins assertion command: ${command}`); } await fn();