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
|
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.
|
(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
|
```json5
|
||||||
clawdbot plugins install @clawdbot/tlon
|
{
|
||||||
```
|
plugins: {
|
||||||
|
allow: ["tlon"]
|
||||||
Local checkout (when running from a git repo):
|
},
|
||||||
|
channels: {
|
||||||
```bash
|
tlon: {
|
||||||
clawdbot plugins install ./extensions/tlon
|
enabled: true,
|
||||||
|
// ... config below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Details: [Plugins](/plugin)
|
Details: [Plugins](/plugin)
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1) Install the Tlon plugin.
|
1) Gather your ship URL and login code.
|
||||||
2) Gather your ship URL and login code.
|
2) Add `tlon` to your plugins allow list.
|
||||||
3) Configure `channels.tlon`.
|
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.
|
5) DM the bot or mention it in a group channel.
|
||||||
|
|
||||||
Minimal config (single account):
|
Minimal config (single account):
|
||||||
|
|||||||
@ -44,6 +44,7 @@ See [Voice Call](/plugins/voice-call) for a concrete example plugin.
|
|||||||
- [Nostr](/channels/nostr) — `@clawdbot/nostr`
|
- [Nostr](/channels/nostr) — `@clawdbot/nostr`
|
||||||
- [Zalo](/channels/zalo) — `@clawdbot/zalo`
|
- [Zalo](/channels/zalo) — `@clawdbot/zalo`
|
||||||
- [Microsoft Teams](/channels/msteams) — `@clawdbot/msteams`
|
- [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)
|
- 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)
|
- 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)
|
- 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
|
Each entry becomes a plugin. If the pack lists multiple extensions, the plugin id
|
||||||
becomes `name/<fileBase>`.
|
becomes `name/<fileBase>`.
|
||||||
|
|
||||||
If your plugin imports npm deps, install them in that directory so
|
If your plugin imports npm deps, bundled plugins will **auto-install** them on
|
||||||
`node_modules` is available (`npm install` / `pnpm install`).
|
first load. For workspace/linked plugins during development, run `npm install`
|
||||||
|
or `pnpm install` in the plugin directory manually.
|
||||||
|
|
||||||
### Channel catalog metadata
|
### Channel catalog metadata
|
||||||
|
|
||||||
|
|||||||
@ -81,14 +81,24 @@ When the operator says “release”, immediately do this preflight (no extra qu
|
|||||||
|
|
||||||
## Plugin publish scope (npm)
|
## Plugin publish scope (npm)
|
||||||
|
|
||||||
We only publish **existing npm plugins** under the `@clawdbot/*` scope. Bundled
|
We publish some plugins to npm under the `@clawdbot/*` scope for users who prefer
|
||||||
plugins that are not on npm stay **disk-tree only** (still shipped in
|
explicit installation. However, **all plugins are bundled** in `extensions/**` and
|
||||||
`extensions/**`).
|
their dependencies are **auto-installed on first load** when enabled.
|
||||||
|
|
||||||
Process to derive the list:
|
### Bundled plugins with auto-install
|
||||||
1) `npm search @clawdbot --json` and capture the package names.
|
|
||||||
2) Compare with `extensions/*/package.json` names.
|
Bundled plugins ship with source code but not `node_modules`. When a bundled plugin
|
||||||
3) Publish only the **intersection** (already on npm).
|
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):
|
Current npm plugin list (update as needed):
|
||||||
- @clawdbot/bluebubbles
|
- @clawdbot/bluebubbles
|
||||||
@ -103,5 +113,14 @@ Current npm plugin list (update as needed):
|
|||||||
- @clawdbot/zalo
|
- @clawdbot/zalo
|
||||||
- @clawdbot/zalouser
|
- @clawdbot/zalouser
|
||||||
|
|
||||||
Release notes must also call out **new optional bundled plugins** that are **not
|
To add a new plugin to npm:
|
||||||
on by default** (example: `tlon`).
|
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 fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@ -43,6 +44,69 @@ const registryCache = new Map<string, PluginRegistry>();
|
|||||||
|
|
||||||
const defaultLogger = () => createSubsystemLogger("plugins");
|
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 => {
|
const resolvePluginSdkAlias = (): string | null => {
|
||||||
try {
|
try {
|
||||||
const modulePath = fileURLToPath(import.meta.url);
|
const modulePath = fileURLToPath(import.meta.url);
|
||||||
@ -282,6 +346,29 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
continue;
|
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;
|
let mod: ClawdbotPluginModule | null = null;
|
||||||
try {
|
try {
|
||||||
mod = jiti(candidate.source) as ClawdbotPluginModule;
|
mod = jiti(candidate.source) as ClawdbotPluginModule;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user