diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000000..7cd53fdbc08 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +**/node_modules/ diff --git a/extensions/.npmignore b/extensions/.npmignore new file mode 100644 index 00000000000..7cd53fdbc08 --- /dev/null +++ b/extensions/.npmignore @@ -0,0 +1 @@ +**/node_modules/ diff --git a/scripts/release-check.ts b/scripts/release-check.ts index fe2a9a1ea9c..6f621cef2d5 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -218,6 +218,16 @@ function runPackDry(): PackResult[] { return JSON.parse(raw) as PackResult[]; } +export function collectForbiddenPackPaths(paths: Iterable): string[] { + return [...paths] + .filter( + (path) => + forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) || + /(^|\/)node_modules\//.test(path), + ) + .toSorted(); +} + function checkPluginVersions() { const rootPackagePath = resolve("package.json"); const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8")) as PackageJson; @@ -422,9 +432,7 @@ function main() { return paths.has(group) ? [] : [group]; }) .toSorted(); - const forbidden = [...paths].filter((path) => - forbiddenPrefixes.some((prefix) => path.startsWith(prefix)), - ); + const forbidden = collectForbiddenPackPaths(paths); if (missing.length > 0 || forbidden.length > 0) { if (missing.length > 0) { diff --git a/src/discord/monitor/provider.test.ts b/src/discord/monitor/provider.test.ts index 0e79e476382..91f61a7ce1f 100644 --- a/src/discord/monitor/provider.test.ts +++ b/src/discord/monitor/provider.test.ts @@ -36,6 +36,7 @@ const { resolveDiscordAllowlistConfigMock, resolveNativeCommandsEnabledMock, resolveNativeSkillsEnabledMock, + voiceRuntimeModuleLoadedMock, } = vi.hoisted(() => { const createdBindingManagers: Array<{ stop: ReturnType }> = []; return { @@ -103,6 +104,7 @@ const { })), resolveNativeCommandsEnabledMock: vi.fn(() => true), resolveNativeSkillsEnabledMock: vi.fn(() => false), + voiceRuntimeModuleLoadedMock: vi.fn(), }; }); @@ -210,10 +212,13 @@ vi.mock("../voice/command.js", () => ({ createDiscordVoiceCommand: () => ({ name: "voice-command" }), })); -vi.mock("../voice/manager.js", () => ({ - DiscordVoiceManager: class DiscordVoiceManager {}, - DiscordVoiceReadyListener: class DiscordVoiceReadyListener {}, -})); +vi.mock("../voice/manager.runtime.js", () => { + voiceRuntimeModuleLoadedMock(); + return { + DiscordVoiceManager: class DiscordVoiceManager {}, + DiscordVoiceReadyListener: class DiscordVoiceReadyListener {}, + }; +}); vi.mock("./agent-components.js", () => ({ createAgentComponentButton: () => ({ id: "btn" }), @@ -390,6 +395,7 @@ describe("monitorDiscordProvider", () => { }); resolveNativeCommandsEnabledMock.mockClear().mockReturnValue(true); resolveNativeSkillsEnabledMock.mockClear().mockReturnValue(false); + voiceRuntimeModuleLoadedMock.mockClear(); }); it("stops thread bindings when startup fails before lifecycle begins", async () => { @@ -424,6 +430,38 @@ describe("monitorDiscordProvider", () => { expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1); }); + it("does not load the Discord voice runtime when voice is disabled", async () => { + const { monitorDiscordProvider } = await import("./provider.js"); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + expect(voiceRuntimeModuleLoadedMock).not.toHaveBeenCalled(); + }); + + it("loads the Discord voice runtime only when voice is enabled", async () => { + resolveDiscordAccountMock.mockReturnValue({ + accountId: "default", + token: "cfg-token", + config: { + commands: { native: true, nativeSkills: false }, + voice: { enabled: true }, + agentComponents: { enabled: false }, + execApprovals: { enabled: false }, + }, + }); + const { monitorDiscordProvider } = await import("./provider.js"); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + expect(voiceRuntimeModuleLoadedMock).toHaveBeenCalledTimes(1); + }); + it("treats ACP error status as uncertain during startup thread-binding probes", async () => { const { monitorDiscordProvider } = await import("./provider.js"); getAcpSessionStatusMock.mockResolvedValue({ state: "error" }); diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 08de298a062..b1bfdde58c1 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -48,7 +48,6 @@ import { resolveDiscordAccount } from "../accounts.js"; import { fetchDiscordApplicationId } from "../probe.js"; import { normalizeDiscordToken } from "../token.js"; import { createDiscordVoiceCommand } from "../voice/command.js"; -import { DiscordVoiceManager, DiscordVoiceReadyListener } from "../voice/manager.js"; import { createAgentComponentButton, createAgentSelectMenu, @@ -104,6 +103,17 @@ export type MonitorDiscordOpts = { setStatus?: DiscordMonitorStatusSink; }; +type DiscordVoiceManager = import("../voice/manager.js").DiscordVoiceManager; + +type DiscordVoiceRuntimeModule = typeof import("../voice/manager.runtime.js"); + +let discordVoiceRuntimePromise: Promise | undefined; + +async function loadDiscordVoiceRuntime(): Promise { + discordVoiceRuntimePromise ??= import("../voice/manager.runtime.js"); + return await discordVoiceRuntimePromise; +} + function formatThreadBindingDurationForConfigLabel(durationMs: number): string { const label = formatThreadBindingDurationLabel(durationMs); return label === "disabled" ? "off" : label; @@ -663,6 +673,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { } if (voiceEnabled) { + const { DiscordVoiceManager, DiscordVoiceReadyListener } = await loadDiscordVoiceRuntime(); voiceManager = new DiscordVoiceManager({ client, cfg, diff --git a/src/discord/voice/manager.runtime.ts b/src/discord/voice/manager.runtime.ts new file mode 100644 index 00000000000..77574b166e5 --- /dev/null +++ b/src/discord/voice/manager.runtime.ts @@ -0,0 +1 @@ +export { DiscordVoiceManager, DiscordVoiceReadyListener } from "./manager.js"; diff --git a/test/release-check.test.ts b/test/release-check.test.ts index 636cc9bb39a..a399407aa98 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -3,6 +3,7 @@ import { collectAppcastSparkleVersionErrors, collectBundledExtensionManifestErrors, collectBundledExtensionRootDependencyGapErrors, + collectForbiddenPackPaths, } from "../scripts/release-check.ts"; function makeItem(shortVersion: string, sparkleVersion: string): string { @@ -150,3 +151,15 @@ describe("collectBundledExtensionManifestErrors", () => { ]); }); }); + +describe("collectForbiddenPackPaths", () => { + it("flags nested node_modules leaking into npm pack output", () => { + expect( + collectForbiddenPackPaths([ + "dist/index.js", + "extensions/tlon/node_modules/.bin/tlon", + "node_modules/.bin/openclaw", + ]), + ).toEqual(["extensions/tlon/node_modules/.bin/tlon", "node_modules/.bin/openclaw"]); + }); +});