fix(plugins): auto-install dependencies for bundled plugins
Bundled plugins ship with source but not node_modules. When a bundled plugin is enabled and has external dependencies, the loader now automatically runs npm install on first load. This fixes the issue where plugins like tlon (which depend on @urbit/http-api and @urbit/aura) would fail to load from a fresh npm install because their dependencies weren't installed. Changes: - loader.ts: Add ensureBundledPluginDeps() to install deps before loading - docs/channels/tlon.md: Update to reflect bundled status and auto-install - docs/plugin.md: Add tlon to plugin list, update dep install guidance - docs/reference/RELEASING.md: Clarify bundled vs npm-published plugins
This commit is contained in:
parent
b9098f3401
commit
3bf74611c2
@ -12,30 +12,35 @@ be further restricted via allowlists.
|
||||
Status: supported via plugin. DMs, group mentions, thread replies, and text-only media fallback
|
||||
(URL appended to caption). Reactions, polls, and native media uploads are not supported.
|
||||
|
||||
## Plugin required
|
||||
## Plugin setup
|
||||
|
||||
Tlon ships as a plugin and is not bundled with the core install.
|
||||
Tlon is bundled with Clawdbot. Its dependencies (`@urbit/http-api`, `@urbit/aura`) are
|
||||
automatically installed on first load when you enable the plugin.
|
||||
|
||||
Install via CLI (npm registry):
|
||||
To enable, add `tlon` to your plugins allow list and configure the channel:
|
||||
|
||||
```bash
|
||||
clawdbot plugins install @clawdbot/tlon
|
||||
```
|
||||
|
||||
Local checkout (when running from a git repo):
|
||||
|
||||
```bash
|
||||
clawdbot plugins install ./extensions/tlon
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
allow: ["tlon"]
|
||||
},
|
||||
channels: {
|
||||
tlon: {
|
||||
enabled: true,
|
||||
// ... config below
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Details: [Plugins](/plugin)
|
||||
|
||||
## Setup
|
||||
|
||||
1) Install the Tlon plugin.
|
||||
2) Gather your ship URL and login code.
|
||||
1) Gather your ship URL and login code.
|
||||
2) Add `tlon` to your plugins allow list.
|
||||
3) Configure `channels.tlon`.
|
||||
4) Restart the gateway.
|
||||
4) Restart the gateway (dependencies install automatically on first load).
|
||||
5) DM the bot or mention it in a group channel.
|
||||
|
||||
Minimal config (single account):
|
||||
|
||||
@ -44,6 +44,7 @@ See [Voice Call](/plugins/voice-call) for a concrete example plugin.
|
||||
- [Nostr](/channels/nostr) — `@clawdbot/nostr`
|
||||
- [Zalo](/channels/zalo) — `@clawdbot/zalo`
|
||||
- [Microsoft Teams](/channels/msteams) — `@clawdbot/msteams`
|
||||
- [Tlon/Urbit](/channels/tlon) — bundled as `tlon` (disabled by default; auto-installs deps)
|
||||
- Google Antigravity OAuth (provider auth) — bundled as `google-antigravity-auth` (disabled by default)
|
||||
- Gemini CLI OAuth (provider auth) — bundled as `google-gemini-cli-auth` (disabled by default)
|
||||
- Qwen OAuth (provider auth) — bundled as `qwen-portal-auth` (disabled by default)
|
||||
@ -128,8 +129,9 @@ A plugin directory may include a `package.json` with `clawdbot.extensions`:
|
||||
Each entry becomes a plugin. If the pack lists multiple extensions, the plugin id
|
||||
becomes `name/<fileBase>`.
|
||||
|
||||
If your plugin imports npm deps, install them in that directory so
|
||||
`node_modules` is available (`npm install` / `pnpm install`).
|
||||
If your plugin imports npm deps, bundled plugins will **auto-install** them on
|
||||
first load. For workspace/linked plugins during development, run `npm install`
|
||||
or `pnpm install` in the plugin directory manually.
|
||||
|
||||
### Channel catalog metadata
|
||||
|
||||
|
||||
@ -81,14 +81,24 @@ When the operator says “release”, immediately do this preflight (no extra qu
|
||||
|
||||
## Plugin publish scope (npm)
|
||||
|
||||
We only publish **existing npm plugins** under the `@clawdbot/*` scope. Bundled
|
||||
plugins that are not on npm stay **disk-tree only** (still shipped in
|
||||
`extensions/**`).
|
||||
We publish some plugins to npm under the `@clawdbot/*` scope for users who prefer
|
||||
explicit installation. However, **all plugins are bundled** in `extensions/**` and
|
||||
their dependencies are **auto-installed on first load** when enabled.
|
||||
|
||||
Process to derive the list:
|
||||
1) `npm search @clawdbot --json` and capture the package names.
|
||||
2) Compare with `extensions/*/package.json` names.
|
||||
3) Publish only the **intersection** (already on npm).
|
||||
### Bundled plugins with auto-install
|
||||
|
||||
Bundled plugins ship with source code but not `node_modules`. When a bundled plugin
|
||||
is enabled and has external dependencies (not `clawdbot` workspace refs), the loader
|
||||
automatically runs `npm install` in the plugin directory on first load.
|
||||
|
||||
This means users don't need to manually install plugins or their dependencies—they
|
||||
just enable the plugin in config and restart.
|
||||
|
||||
### npm-published plugins (optional)
|
||||
|
||||
Some plugins are also published to npm for users who prefer explicit installation
|
||||
via `clawdbot plugins install @clawdbot/xxx`. This copies the plugin to
|
||||
`~/.clawdbot/extensions/` and runs `npm install` there.
|
||||
|
||||
Current npm plugin list (update as needed):
|
||||
- @clawdbot/bluebubbles
|
||||
@ -103,5 +113,14 @@ Current npm plugin list (update as needed):
|
||||
- @clawdbot/zalo
|
||||
- @clawdbot/zalouser
|
||||
|
||||
Release notes must also call out **new optional bundled plugins** that are **not
|
||||
on by default** (example: `tlon`).
|
||||
To add a new plugin to npm:
|
||||
1) First-publish manually: `cd extensions/<plugin> && npm publish --access public`
|
||||
2) Add to the list above
|
||||
3) Future releases will include it automatically
|
||||
|
||||
### Bundled-only plugins
|
||||
|
||||
Some plugins are bundled but not published to npm (e.g., `tlon`). These work via
|
||||
the auto-install mechanism—users just enable them in config.
|
||||
|
||||
Release notes should call out **new bundled plugins** so users know they're available.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@ -43,6 +44,69 @@ const registryCache = new Map<string, PluginRegistry>();
|
||||
|
||||
const defaultLogger = () => createSubsystemLogger("plugins");
|
||||
|
||||
/**
|
||||
* Ensures plugin dependencies are installed for bundled plugins.
|
||||
* Bundled plugins ship with source but not node_modules; this installs
|
||||
* deps on first load if the plugin has dependencies in package.json.
|
||||
*/
|
||||
function ensureBundledPluginDeps(params: {
|
||||
rootDir: string;
|
||||
pluginId: string;
|
||||
logger: PluginLogger;
|
||||
}): { ok: boolean; error?: string } {
|
||||
const { rootDir, pluginId, logger } = params;
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return { ok: true }; // No package.json, nothing to install
|
||||
}
|
||||
|
||||
let manifest: { dependencies?: Record<string, string> };
|
||||
try {
|
||||
manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
||||
} catch {
|
||||
return { ok: true }; // Can't parse, skip
|
||||
}
|
||||
|
||||
const deps = manifest.dependencies ?? {};
|
||||
const depNames = Object.keys(deps).filter(
|
||||
(name) => !name.startsWith("clawdbot") && name !== "clawdbot",
|
||||
);
|
||||
|
||||
if (depNames.length === 0) {
|
||||
return { ok: true }; // No external dependencies
|
||||
}
|
||||
|
||||
// Check if first dependency exists in node_modules
|
||||
const firstDep = depNames[0];
|
||||
const depPath = path.join(rootDir, "node_modules", ...firstDep.split("/"));
|
||||
|
||||
if (fs.existsSync(depPath)) {
|
||||
return { ok: true }; // Dependencies already installed
|
||||
}
|
||||
|
||||
logger.info?.(`[plugins] ${pluginId}: installing dependencies…`);
|
||||
|
||||
try {
|
||||
const result = spawnSync("npm", ["install", "--omit=dev", "--silent"], {
|
||||
cwd: rootDir,
|
||||
encoding: "utf-8",
|
||||
timeout: 300_000,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
const errorMsg = result.stderr?.trim() || result.stdout?.trim() || "unknown error";
|
||||
return { ok: false, error: `npm install failed: ${errorMsg}` };
|
||||
}
|
||||
|
||||
logger.info?.(`[plugins] ${pluginId}: dependencies installed`);
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, error: `failed to install dependencies: ${String(err)}` };
|
||||
}
|
||||
}
|
||||
|
||||
const resolvePluginSdkAlias = (): string | null => {
|
||||
try {
|
||||
const modulePath = fileURLToPath(import.meta.url);
|
||||
@ -282,6 +346,29 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure bundled plugin dependencies are installed before loading
|
||||
if (candidate.origin === "bundled") {
|
||||
const depsResult = ensureBundledPluginDeps({
|
||||
rootDir: candidate.rootDir,
|
||||
pluginId,
|
||||
logger,
|
||||
});
|
||||
if (!depsResult.ok) {
|
||||
logger.error(`[plugins] ${record.id} failed to install deps: ${depsResult.error}`);
|
||||
record.status = "error";
|
||||
record.error = depsResult.error ?? "failed to install dependencies";
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mod: ClawdbotPluginModule | null = null;
|
||||
try {
|
||||
mod = jiti(candidate.source) as ClawdbotPluginModule;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user