mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix: harden source checkout plugin dependency handling
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Replies: strip legacy `[TOOL_CALL]{tool => ..., args => ...}[/TOOL_CALL]` pseudo-call text from user-facing replies and flag it in tool-call diagnostics instead of showing raw tool syntax in channels. Fixes #63610. Thanks @canh0chua.
|
||||
- WhatsApp: close long-lived web sockets through Baileys `end(error)` before falling back to raw websocket close, so listener teardown runs Baileys cleanup instead of leaving zombie sockets. Fixes #52442. Thanks @essendigitalgroup-cyber.
|
||||
- Twitch/plugins: emit a flat JSON Schema for Twitch channel config so single-account and multi-account configs validate before runtime load, and add source-checkout diagnostics for missing pnpm workspace dependencies. Thanks @vincentkoc.
|
||||
- Gateway/sessions: move hot transcript reads and mirror appends onto async bounded IO with serialized parent-linked writes, keeping large session histories from stalling Gateway requests and channel replies. Fixes #75656. Thanks @DerFlash.
|
||||
- macOS/Voice Wake: accept trigger-only phrases in the built-in Voice Wake test, matching the settings UI and runtime trigger-only path instead of requiring extra command text after the wake word. Fixes #64986. Thanks @zoiks65.
|
||||
- Cron/TTS: run cron announce payloads through the normal TTS directive transform before outbound delivery, so scheduled `[[tts]]` replies generate voice payloads instead of leaking raw tags. Fixes #52125. Thanks @kenchen3000.
|
||||
|
||||
@@ -99,6 +99,12 @@ workspace dependencies are available and edits are picked up directly. Source
|
||||
checkout development is pnpm-only; plain `npm install` at the repository root is
|
||||
not a supported way to prepare bundled plugin dependencies.
|
||||
|
||||
| Install shape | Bundled plugin location | Dependency owner |
|
||||
| -------------------------------- | ------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `npm install -g openclaw` | Built runtime tree inside the package | OpenClaw package and explicit plugin install/update/doctor flows |
|
||||
| Git checkout plus `pnpm install` | `extensions/<id>` workspace packages | The pnpm workspace, including each plugin package's own dependencies |
|
||||
| `openclaw plugins install ...` | Managed npm/git/ClawHub plugin root | The plugin install/update flow |
|
||||
|
||||
## Legacy cleanup
|
||||
|
||||
Older OpenClaw versions generated bundled-plugin dependency roots at startup or
|
||||
|
||||
46
extensions/twitch/src/config-schema.test.ts
Normal file
46
extensions/twitch/src/config-schema.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import AjvPkg from "ajv";
|
||||
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TwitchConfigSchema } from "./config-schema.js";
|
||||
|
||||
function validateTwitchConfig(value: unknown): boolean {
|
||||
const Ajv = AjvPkg as unknown as new (opts?: object) => import("ajv").default;
|
||||
const schema = buildChannelConfigSchema(TwitchConfigSchema).schema;
|
||||
const validate = new Ajv({ allErrors: true, strict: false }).compile(schema);
|
||||
const ok = validate(value);
|
||||
if (!ok) {
|
||||
throw new Error(`expected valid Twitch config: ${JSON.stringify(validate.errors)}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
describe("TwitchConfigSchema JSON schema", () => {
|
||||
it("accepts single-account channel config with base fields", () => {
|
||||
expect(
|
||||
validateTwitchConfig({
|
||||
enabled: false,
|
||||
username: "openclaw",
|
||||
accessToken: "oauth:test",
|
||||
clientId: "test-client-id",
|
||||
channel: "openclaw-test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts multi-account channel config with defaultAccount", () => {
|
||||
expect(
|
||||
validateTwitchConfig({
|
||||
enabled: true,
|
||||
defaultAccount: "stream",
|
||||
accounts: {
|
||||
stream: {
|
||||
username: "openclaw",
|
||||
accessToken: "oauth:test",
|
||||
clientId: "test-client-id",
|
||||
channel: "openclaw-test",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -6,10 +6,7 @@ import { z } from "openclaw/plugin-sdk/zod";
|
||||
*/
|
||||
const TwitchRoleSchema = z.enum(["moderator", "owner", "vip", "subscriber", "all"]);
|
||||
|
||||
/**
|
||||
* Twitch account configuration schema
|
||||
*/
|
||||
const TwitchAccountSchema = z.object({
|
||||
const TwitchAccountShape = {
|
||||
/** Twitch username */
|
||||
username: z.string(),
|
||||
/** Twitch OAuth access token (requires chat:read and chat:write scopes) */
|
||||
@@ -36,16 +33,22 @@ const TwitchAccountSchema = z.object({
|
||||
expiresIn: z.number().nullable().optional(),
|
||||
/** Timestamp when token was obtained (optional, for token refresh tracking) */
|
||||
obtainmentTimestamp: z.number().optional(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Twitch account configuration schema
|
||||
*/
|
||||
const TwitchAccountSchema = z.object(TwitchAccountShape);
|
||||
|
||||
/**
|
||||
* Base configuration properties shared by both single and multi-account modes
|
||||
*/
|
||||
const TwitchConfigBaseSchema = z.object({
|
||||
const TwitchConfigBaseShape = {
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
});
|
||||
defaultAccount: z.string().optional(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Simplified single-account configuration schema
|
||||
@@ -53,24 +56,25 @@ const TwitchConfigBaseSchema = z.object({
|
||||
* Use this for single-account setups. Properties are at the top level,
|
||||
* creating an implicit "default" account.
|
||||
*/
|
||||
const SimplifiedSchema = z.intersection(TwitchConfigBaseSchema, TwitchAccountSchema);
|
||||
const SimplifiedSchema = z.object({
|
||||
...TwitchConfigBaseShape,
|
||||
...TwitchAccountShape,
|
||||
});
|
||||
|
||||
/**
|
||||
* Multi-account configuration schema
|
||||
*
|
||||
* Use this for multi-account setups. Each key is an account ID (e.g., "default", "secondary").
|
||||
*/
|
||||
const MultiAccountSchema = z.intersection(
|
||||
TwitchConfigBaseSchema,
|
||||
z
|
||||
.object({
|
||||
/** Per-account configuration (for multi-account setups) */
|
||||
accounts: z.record(z.string(), TwitchAccountSchema),
|
||||
})
|
||||
.refine((val) => Object.keys(val.accounts || {}).length > 0, {
|
||||
message: "accounts must contain at least one entry",
|
||||
}),
|
||||
);
|
||||
const MultiAccountSchema = z
|
||||
.object({
|
||||
...TwitchConfigBaseShape,
|
||||
/** Per-account configuration (for multi-account setups) */
|
||||
accounts: z.record(z.string(), TwitchAccountSchema),
|
||||
})
|
||||
.refine((val) => Object.keys(val.accounts || {}).length > 0, {
|
||||
message: "accounts must contain at least one entry",
|
||||
});
|
||||
|
||||
/**
|
||||
* Twitch plugin configuration schema
|
||||
|
||||
@@ -20,7 +20,7 @@ export function noteSourceInstallIssues(root: string | null) {
|
||||
|
||||
if (fs.existsSync(nodeModules) && !fs.existsSync(pnpmStore)) {
|
||||
warnings.push(
|
||||
"- node_modules was not installed by pnpm (missing node_modules/.pnpm). Run: pnpm install",
|
||||
"- node_modules was not installed by pnpm (missing node_modules/.pnpm). Run: pnpm install so bundled plugins can load package-local dependencies.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function noteSourceInstallIssues(root: string | null) {
|
||||
}
|
||||
|
||||
if (fs.existsSync(srcEntry) && !fs.existsSync(tsxBin)) {
|
||||
warnings.push("- tsx binary is missing for source runs. Run: pnpm install");
|
||||
warnings.push("- tsx binary is missing for source runs. Run: pnpm install.");
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
|
||||
@@ -15103,191 +15103,174 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
anyOf: [
|
||||
{
|
||||
allOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
markdown: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
tables: {
|
||||
type: "string",
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
markdown: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tables: {
|
||||
type: "string",
|
||||
enum: ["off", "bullets", "code", "block"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
enum: ["off", "bullets", "code", "block"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
},
|
||||
accessToken: {
|
||||
type: "string",
|
||||
},
|
||||
clientId: {
|
||||
type: "string",
|
||||
},
|
||||
channel: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
allowFrom: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
allowedRoles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
enum: ["moderator", "owner", "vip", "subscriber", "all"],
|
||||
},
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
responsePrefix: {
|
||||
type: "string",
|
||||
},
|
||||
clientSecret: {
|
||||
type: "string",
|
||||
},
|
||||
refreshToken: {
|
||||
type: "string",
|
||||
},
|
||||
expiresIn: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
obtainmentTimestamp: {
|
||||
defaultAccount: {
|
||||
type: "string",
|
||||
},
|
||||
username: {
|
||||
type: "string",
|
||||
},
|
||||
accessToken: {
|
||||
type: "string",
|
||||
},
|
||||
clientId: {
|
||||
type: "string",
|
||||
},
|
||||
channel: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
allowFrom: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
allowedRoles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
enum: ["moderator", "owner", "vip", "subscriber", "all"],
|
||||
},
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
responsePrefix: {
|
||||
type: "string",
|
||||
},
|
||||
clientSecret: {
|
||||
type: "string",
|
||||
},
|
||||
refreshToken: {
|
||||
type: "string",
|
||||
},
|
||||
expiresIn: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
required: ["username", "accessToken", "channel"],
|
||||
additionalProperties: false,
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
obtainmentTimestamp: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
required: ["username", "accessToken", "channel"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
allOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
markdown: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
tables: {
|
||||
type: "string",
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
markdown: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tables: {
|
||||
type: "string",
|
||||
enum: ["off", "bullets", "code", "block"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
enum: ["off", "bullets", "code", "block"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
defaultAccount: {
|
||||
type: "string",
|
||||
},
|
||||
accounts: {
|
||||
type: "object",
|
||||
properties: {
|
||||
accounts: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
},
|
||||
accessToken: {
|
||||
type: "string",
|
||||
},
|
||||
clientId: {
|
||||
type: "string",
|
||||
},
|
||||
channel: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
allowFrom: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
allowedRoles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
enum: ["moderator", "owner", "vip", "subscriber", "all"],
|
||||
},
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
responsePrefix: {
|
||||
type: "string",
|
||||
},
|
||||
clientSecret: {
|
||||
type: "string",
|
||||
},
|
||||
refreshToken: {
|
||||
type: "string",
|
||||
},
|
||||
expiresIn: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
obtainmentTimestamp: {
|
||||
accessToken: {
|
||||
type: "string",
|
||||
},
|
||||
clientId: {
|
||||
type: "string",
|
||||
},
|
||||
channel: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
allowFrom: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
allowedRoles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
enum: ["moderator", "owner", "vip", "subscriber", "all"],
|
||||
},
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
responsePrefix: {
|
||||
type: "string",
|
||||
},
|
||||
clientSecret: {
|
||||
type: "string",
|
||||
},
|
||||
refreshToken: {
|
||||
type: "string",
|
||||
},
|
||||
expiresIn: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
required: ["username", "accessToken", "channel"],
|
||||
additionalProperties: false,
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
obtainmentTimestamp: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
required: ["username", "accessToken", "channel"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
required: ["accounts"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
required: ["accounts"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import {
|
||||
resolveBundledPluginsDir,
|
||||
resolveSourceCheckoutDependencyDiagnostic,
|
||||
} from "./bundled-dir.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -311,6 +314,31 @@ describe("resolveBundledPluginsDir", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reports missing pnpm workspace deps for source checkouts", () => {
|
||||
const repoRoot = createOpenClawRoot({
|
||||
prefix: "openclaw-bundled-dir-source-deps-",
|
||||
hasExtensions: true,
|
||||
hasSrc: true,
|
||||
hasGitCheckout: true,
|
||||
hasPnpmWorkspace: true,
|
||||
});
|
||||
seedBundledPluginTree(repoRoot, "extensions", "twitch");
|
||||
vi.spyOn(process, "cwd").mockReturnValue(repoRoot);
|
||||
process.argv[1] = path.join(repoRoot, "openclaw.mjs");
|
||||
|
||||
expect(resolveSourceCheckoutDependencyDiagnostic()).toEqual({
|
||||
source: repoRoot,
|
||||
message: expect.stringContaining("run `pnpm install`"),
|
||||
});
|
||||
|
||||
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
|
||||
expect(resolveSourceCheckoutDependencyDiagnostic()).toBeNull();
|
||||
|
||||
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
|
||||
fs.mkdirSync(path.join(repoRoot, "node_modules", ".pnpm"), { recursive: true });
|
||||
expect(resolveSourceCheckoutDependencyDiagnostic()).toBeNull();
|
||||
});
|
||||
|
||||
it("returns a stable empty bundled plugin directory when bundled plugins are disabled", () => {
|
||||
const repoRoot = createOpenClawRoot({
|
||||
prefix: "openclaw-bundled-dir-disabled-",
|
||||
|
||||
@@ -10,6 +10,11 @@ const DISABLED_BUNDLED_PLUGINS_DIR = path.join(os.tmpdir(), "openclaw-empty-bund
|
||||
const TEST_TRUST_BUNDLED_PLUGINS_DIR_ENV = "OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR";
|
||||
let bundledPluginsDirOverrideForTest: string | undefined;
|
||||
|
||||
export type SourceCheckoutDependencyDiagnostic = {
|
||||
source: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function areBundledPluginsDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const raw = normalizeOptionalLowercaseString(env.OPENCLAW_DISABLE_BUNDLED_PLUGINS);
|
||||
return raw === "1" || raw === "true";
|
||||
@@ -87,6 +92,40 @@ function trustedBundledPluginRootsForPackageRoot(packageRoot: string): string[]
|
||||
return roots;
|
||||
}
|
||||
|
||||
function resolvePackageRootsForBundledPlugins(): string[] {
|
||||
const argvRoot = resolveOpenClawPackageRootSync({ argv1: process.argv[1] });
|
||||
const moduleRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });
|
||||
return [argvRoot, moduleRoot].filter(
|
||||
(entry, index, all): entry is string => Boolean(entry) && all.indexOf(entry) === index,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveSourceCheckoutDependencyDiagnostic(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): SourceCheckoutDependencyDiagnostic | null {
|
||||
if (areBundledPluginsDisabled(env)) {
|
||||
return null;
|
||||
}
|
||||
for (const packageRoot of resolvePackageRootsForBundledPlugins()) {
|
||||
if (!isSourceCheckoutRoot(packageRoot)) {
|
||||
continue;
|
||||
}
|
||||
const extensionsDir = path.join(packageRoot, "extensions");
|
||||
if (!hasUsableBundledPluginTree(extensionsDir)) {
|
||||
continue;
|
||||
}
|
||||
if (fs.existsSync(path.join(packageRoot, "node_modules", ".pnpm"))) {
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
source: packageRoot,
|
||||
message:
|
||||
"OpenClaw source checkout detected without pnpm workspace dependencies; run `pnpm install` from the repo root so bundled plugins can load package-local dependencies.",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveTrustedExistingOverride(resolvedOverride: string): string | null {
|
||||
const realOverride = safeRealpathSync(resolvedOverride);
|
||||
if (!realOverride) {
|
||||
|
||||
@@ -488,6 +488,34 @@ describe("discoverOpenClawPlugins", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not warn about source checkout deps when bundled plugins are disabled", () => {
|
||||
const stateDir = makeTempDir();
|
||||
const packageRoot = makeTempDir();
|
||||
mkdirSafe(path.join(packageRoot, "src"));
|
||||
const extensionDir = path.join(packageRoot, "extensions", "twitch");
|
||||
mkdirSafe(extensionDir);
|
||||
fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/fake.git\n", "utf-8");
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "pnpm-workspace.yaml"),
|
||||
"packages:\n - .\n - extensions/*\n",
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(extensionDir, "package.json"),
|
||||
'{"name":"@openclaw/twitch"}\n',
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(path.join(extensionDir, "openclaw.plugin.json"), '{"id":"twitch"}\n', "utf-8");
|
||||
|
||||
const result = withOpenClawPackageArgv(packageRoot, () =>
|
||||
discoverOpenClawPlugins({ env: buildDiscoveryEnv(stateDir) }),
|
||||
);
|
||||
|
||||
expect(result.diagnostics.map((entry) => entry.message)).not.toContainEqual(
|
||||
expect.stringContaining("pnpm install"),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not treat repo-level live or test files as plugin entrypoints", () => {
|
||||
const stateDir = makeTempDir();
|
||||
const packageRoot = path.join(stateDir, "node_modules", "openclaw");
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "../shared/string-coerce.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
|
||||
import { resolveSourceCheckoutDependencyDiagnostic } from "./bundled-dir.js";
|
||||
import { resolvePackagedBundledLoadPathAlias } from "./bundled-load-path-aliases.js";
|
||||
import { listBundledSourceOverlayDirs } from "./bundled-source-overlays.js";
|
||||
import type { PluginBundleFormat, PluginDiagnostic, PluginFormat } from "./manifest-types.js";
|
||||
@@ -971,6 +972,14 @@ export function discoverOpenClawPlugins(params: {
|
||||
"using bind-mounted bundled plugin source overlay; this source overrides the packaged dist bundle for the same plugin id",
|
||||
});
|
||||
}
|
||||
const sourceCheckoutDependencyDiagnostic = resolveSourceCheckoutDependencyDiagnostic(env);
|
||||
if (sourceCheckoutDependencyDiagnostic) {
|
||||
result.diagnostics.push({
|
||||
level: "warn",
|
||||
source: sourceCheckoutDependencyDiagnostic.source,
|
||||
message: sourceCheckoutDependencyDiagnostic.message,
|
||||
});
|
||||
}
|
||||
if (roots.stock) {
|
||||
discoverInDirectory({
|
||||
dir: roots.stock,
|
||||
|
||||
27
src/plugins/source-checkout-runtime.test.ts
Normal file
27
src/plugins/source-checkout-runtime.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
|
||||
describe("source checkout bundled plugin runtime", () => {
|
||||
it("loads enabled bundled plugins from the pnpm workspace source tree", () => {
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
onlyPluginIds: ["twitch"],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
twitch: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const twitch = registry.plugins.find((plugin) => plugin.id === "twitch");
|
||||
expect(twitch).toMatchObject({
|
||||
status: "loaded",
|
||||
origin: "bundled",
|
||||
});
|
||||
expect(twitch?.source).toContain(`${path.sep}extensions${path.sep}twitch${path.sep}index.ts`);
|
||||
expect(twitch?.rootDir).toContain(`${path.sep}extensions${path.sep}twitch`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user