@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/desktop-browser-cli.md +33 -0
- package/openapi.yaml +17 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +6 -4
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +116 -18
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +7 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +62 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +34 -4
- package/src/calls/voice-session-bridge.ts +23 -0
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +30 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +2 -2
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -1
- package/src/daemon/conversation-tool-setup.ts +31 -29
- package/src/daemon/host-cu-proxy.ts +51 -26
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +22 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +3 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +11 -3
- package/src/live-voice/__tests__/session-controls.test.ts +34 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +3 -0
- package/src/live-voice/protocol.ts +10 -1
- package/src/live-voice/session-controls.ts +28 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/oauth/seed-providers.ts +11 -0
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +2 -3
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -67,9 +67,8 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
67
67
|
// Import module under test (after mocks)
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
|
|
70
|
-
const { formatBrowserStatusLines, registerBrowserCommand } =
|
|
71
|
-
"../browser.js"
|
|
72
|
-
);
|
|
70
|
+
const { formatBrowserStatusLines, registerBrowserCommand } =
|
|
71
|
+
await import("../browser.js");
|
|
73
72
|
|
|
74
73
|
// ---------------------------------------------------------------------------
|
|
75
74
|
// Test helper
|
|
@@ -641,3 +640,85 @@ describe("error exit codes", () => {
|
|
|
641
640
|
expect(exitCode).toBe(0);
|
|
642
641
|
});
|
|
643
642
|
});
|
|
643
|
+
|
|
644
|
+
for (const desktopFlag of ["--virtual-desktop", "--desktop"]) {
|
|
645
|
+
describe(`virtual desktop routing via ${desktopFlag}`, () => {
|
|
646
|
+
test("routes semantic actions through IPC with conversation and element identity", async () => {
|
|
647
|
+
process.env.__CONVERSATION_ID = "conv-desktop";
|
|
648
|
+
const result = await runCommand([
|
|
649
|
+
"browser",
|
|
650
|
+
desktopFlag,
|
|
651
|
+
"--json",
|
|
652
|
+
"click",
|
|
653
|
+
"--element-id",
|
|
654
|
+
"e7",
|
|
655
|
+
]);
|
|
656
|
+
expect(result.exitCode).toBe(0);
|
|
657
|
+
expect(lastIpcCall?.method).toBe("browser_execute");
|
|
658
|
+
expect(lastBody()).toMatchObject({
|
|
659
|
+
desktop: true,
|
|
660
|
+
conversationId: "conv-desktop",
|
|
661
|
+
operation: "click",
|
|
662
|
+
input: { element_id: "e7" },
|
|
663
|
+
});
|
|
664
|
+
expect(
|
|
665
|
+
(lastBody().input as Record<string, unknown>).desktop,
|
|
666
|
+
).toBeUndefined();
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test.each([
|
|
670
|
+
{ args: ["list"] },
|
|
671
|
+
{ args: ["select", "--tab-id", "7"] },
|
|
672
|
+
{ args: ["new"] },
|
|
673
|
+
{ args: ["close", "--tab-id", "7"] },
|
|
674
|
+
])("routes desktop tabs $args through IPC", async ({ args }) => {
|
|
675
|
+
mockIpcResult = { ok: true, result: { ok: true, tabs: [] } };
|
|
676
|
+
const result = await runCommand([
|
|
677
|
+
"browser",
|
|
678
|
+
desktopFlag,
|
|
679
|
+
"--json",
|
|
680
|
+
"tabs",
|
|
681
|
+
...args,
|
|
682
|
+
]);
|
|
683
|
+
expect(result.exitCode).toBe(0);
|
|
684
|
+
expect(lastIpcCall?.method).toBe("browser_tabs");
|
|
685
|
+
expect(lastBody()).toMatchObject({ desktop: true, command: args[0] });
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test.each([
|
|
689
|
+
{ flags: ["--browser-mode", "extension"] },
|
|
690
|
+
{ flags: ["--target-client-id", "client-123"] },
|
|
691
|
+
])("rejects conflicting desktop targets before IPC", async ({ flags }) => {
|
|
692
|
+
const result = await runCommand([
|
|
693
|
+
"browser",
|
|
694
|
+
desktopFlag,
|
|
695
|
+
...flags,
|
|
696
|
+
"tabs",
|
|
697
|
+
"list",
|
|
698
|
+
]);
|
|
699
|
+
expect(result.exitCode).not.toBe(0);
|
|
700
|
+
expect(lastIpcCall).toBeNull();
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
test.each([
|
|
706
|
+
{ args: ["list"] },
|
|
707
|
+
{ args: ["select", "--tab-id", "1"] },
|
|
708
|
+
{ args: ["new", "--url", "https://example.com"] },
|
|
709
|
+
{ args: ["close", "--tab-id", "1"] },
|
|
710
|
+
])(
|
|
711
|
+
"tab command $args forwards an explicit backend override",
|
|
712
|
+
async ({ args }) => {
|
|
713
|
+
mockIpcResult = { ok: true, result: { ok: true, tabs: [] } };
|
|
714
|
+
await runCommand([
|
|
715
|
+
"browser",
|
|
716
|
+
"--browser-mode",
|
|
717
|
+
"extension",
|
|
718
|
+
"tabs",
|
|
719
|
+
...args,
|
|
720
|
+
]);
|
|
721
|
+
expect(lastIpcCall?.method).toBe("browser_tabs");
|
|
722
|
+
expect(lastBody().browserMode).toBe("extension");
|
|
723
|
+
},
|
|
724
|
+
);
|
|
@@ -96,6 +96,15 @@ export const browserHelp: CliCommandHelp = {
|
|
|
96
96
|
name: "browser",
|
|
97
97
|
description: "Control the browser via the running assistant.",
|
|
98
98
|
options: [
|
|
99
|
+
{
|
|
100
|
+
flags: "--desktop",
|
|
101
|
+
description: "Compatibility alias for --virtual-desktop.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
flags: "--virtual-desktop",
|
|
105
|
+
description:
|
|
106
|
+
"Use Chrome in the platform-hosted virtual desktop with visible cursor feedback.",
|
|
107
|
+
},
|
|
99
108
|
{
|
|
100
109
|
flags: "--session <id>",
|
|
101
110
|
description: "Session ID to preserve browser state across invocations.",
|
|
@@ -118,6 +127,38 @@ export const browserHelp: CliCommandHelp = {
|
|
|
118
127
|
],
|
|
119
128
|
helpText: `
|
|
120
129
|
Browser operations are executed through the running assistant.
|
|
130
|
+
Virtual desktop is available only for platform-hosted assistants.
|
|
131
|
+
In the web client, automatic selection prefers virtual desktop Chrome when
|
|
132
|
+
its feature is enabled and the conversation has an
|
|
133
|
+
identified guardian. The macOS and Windows apps keep their existing browser
|
|
134
|
+
selection and fallback behavior; use --virtual-desktop to select it there.
|
|
135
|
+
Existing sessions and explicit backend/client choices take priority.
|
|
136
|
+
--browser-mode local (alias playwright) means the assistant's Playwright browser,
|
|
137
|
+
not the user's Chrome. --desktop remains an alias for --virtual-desktop.
|
|
138
|
+
Use --virtual-desktop for Chrome in the Desktop panel.
|
|
139
|
+
Run navigate --url <url> directly to open a page.
|
|
140
|
+
The assistant-desktop feature flag and completed installation are required.
|
|
141
|
+
The first browser command installs missing components, starts Chrome, and completes
|
|
142
|
+
the requested action in one call. Allow up to 600 seconds for this command when
|
|
143
|
+
using bash (timeout_seconds: 600). Opening the Desktop panel also starts
|
|
144
|
+
the same installation and shows progress. No separate setup command is needed.
|
|
145
|
+
Report other availability errors without switching to a personal computer.
|
|
146
|
+
The assistant manages Chrome startup. Do not launch a separate desktop stack,
|
|
147
|
+
install desktop packages, or drive webpages with shell-level xdotool.
|
|
148
|
+
With --virtual-desktop, screenshot captures the page directly from Chrome over CDP
|
|
149
|
+
as a color JPEG. Use --output to save it and file_read to view it. Do not use
|
|
150
|
+
shell-level xwd or custom screenshot conversion scripts.
|
|
151
|
+
Use snapshot for current element IDs and tabs list for current tab IDs.
|
|
152
|
+
Refresh the snapshot after navigation, tab changes or stale-element errors.
|
|
153
|
+
Inspect the result before retrying a failed action; it may have happened.
|
|
154
|
+
A purple page pointer shows CDP mouse movement in the stream. Browser toolbar
|
|
155
|
+
controls, native dialogs and other apps are outside this CLI's page controls.
|
|
156
|
+
Do not combine --virtual-desktop with personal browser targets, other browser modes
|
|
157
|
+
or --use-active-tab. Download waiting is unavailable on this target.
|
|
158
|
+
Users can interact directly with the expanded desktop. If they ask you to pause,
|
|
159
|
+
stop and wait. Take a fresh snapshot before resuming. Closing the viewer does not
|
|
160
|
+
end automation. Run assistant browser --virtual-desktop detach when finished or
|
|
161
|
+
blocked, including before asking a question. Chrome stays running.
|
|
121
162
|
Each subcommand maps to a browser operation and communicates
|
|
122
163
|
with the assistant process.
|
|
123
164
|
|
|
@@ -131,6 +172,8 @@ local. Useful for debugging or when deterministic backend selection
|
|
|
131
172
|
is required.
|
|
132
173
|
|
|
133
174
|
Examples:
|
|
175
|
+
$ assistant browser --virtual-desktop status
|
|
176
|
+
$ assistant browser --virtual-desktop screenshot --output /tmp/desktop-page.jpg
|
|
134
177
|
$ assistant browser navigate --url https://example.com
|
|
135
178
|
$ assistant browser snapshot
|
|
136
179
|
$ assistant browser click --selector "#login"
|
|
@@ -28,6 +28,8 @@ import { registerCommand } from "../lib/register-command.js";
|
|
|
28
28
|
import { log } from "../logger.js";
|
|
29
29
|
import { browserHelp, toKebab } from "./browser.help.js";
|
|
30
30
|
|
|
31
|
+
const BROWSER_REQUEST_TIMEOUT_MS = 10 * 60_000;
|
|
32
|
+
|
|
31
33
|
// ── Naming helpers ───────────────────────────────────────────────────
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -116,6 +118,7 @@ function attachOperationAction(
|
|
|
116
118
|
|
|
117
119
|
subcmd.action(async (opts: Record<string, unknown>) => {
|
|
118
120
|
const parentOpts = browser.opts() as {
|
|
121
|
+
desktop?: boolean;
|
|
119
122
|
session?: string;
|
|
120
123
|
json?: boolean;
|
|
121
124
|
browserMode?: string;
|
|
@@ -130,6 +133,8 @@ function attachOperationAction(
|
|
|
130
133
|
// targetClientId) and screenshot ergonomics (output).
|
|
131
134
|
const input: Record<string, unknown> = {};
|
|
132
135
|
const excludeKeys = new Set([
|
|
136
|
+
"desktop",
|
|
137
|
+
"virtualDesktop",
|
|
133
138
|
"session",
|
|
134
139
|
"json",
|
|
135
140
|
"output",
|
|
@@ -163,9 +168,7 @@ function attachOperationAction(
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
//
|
|
167
|
-
// challenges, downloads up to 120s, etc.), so use a generous
|
|
168
|
-
// IPC timeout that exceeds any server-side operation timeout.
|
|
171
|
+
// First use can include virtual desktop installation and Chrome startup.
|
|
169
172
|
const ipcResult = await cliIpcCall<BrowserExecuteResult>(
|
|
170
173
|
"browser_execute",
|
|
171
174
|
{
|
|
@@ -173,10 +176,11 @@ function attachOperationAction(
|
|
|
173
176
|
operation: meta.operation,
|
|
174
177
|
input,
|
|
175
178
|
sessionId,
|
|
179
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
176
180
|
...(conversationId ? { conversationId } : {}),
|
|
177
181
|
},
|
|
178
182
|
},
|
|
179
|
-
{ timeoutMs:
|
|
183
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
180
184
|
);
|
|
181
185
|
|
|
182
186
|
if (!ipcResult.ok) {
|
|
@@ -328,6 +332,22 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
328
332
|
description: browserHelp.description,
|
|
329
333
|
build: (browser) => {
|
|
330
334
|
applyCommandHelp(browser, browserHelp);
|
|
335
|
+
browser.hook("preAction", () => {
|
|
336
|
+
if (browser.opts().virtualDesktop) {
|
|
337
|
+
browser.setOptionValue("desktop", true);
|
|
338
|
+
}
|
|
339
|
+
const options = browser.opts();
|
|
340
|
+
if (
|
|
341
|
+
options.desktop &&
|
|
342
|
+
(options.targetClientId ||
|
|
343
|
+
(options.browserMode &&
|
|
344
|
+
!["auto", "cdp-inspect"].includes(options.browserMode)))
|
|
345
|
+
) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
"--virtual-desktop cannot be combined with a personal browser target or another browser mode",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
331
351
|
|
|
332
352
|
// Attach one action per browser operation
|
|
333
353
|
for (const meta of BROWSER_OPERATION_META) {
|
|
@@ -339,6 +359,8 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
339
359
|
|
|
340
360
|
subcommand(tabs, "list").action(async (opts: { pretty?: boolean }) => {
|
|
341
361
|
const parentOpts = browser.opts() as {
|
|
362
|
+
desktop?: boolean;
|
|
363
|
+
browserMode?: string;
|
|
342
364
|
session?: string;
|
|
343
365
|
json?: boolean;
|
|
344
366
|
targetClientId?: string;
|
|
@@ -364,11 +386,15 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
364
386
|
body: {
|
|
365
387
|
command: "list",
|
|
366
388
|
sessionId,
|
|
389
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
390
|
+
...(parentOpts.browserMode
|
|
391
|
+
? { browserMode: parentOpts.browserMode }
|
|
392
|
+
: {}),
|
|
367
393
|
...(conversationId ? { conversationId } : {}),
|
|
368
394
|
...(targetClientId ? { targetClientId } : {}),
|
|
369
395
|
},
|
|
370
396
|
},
|
|
371
|
-
{ timeoutMs:
|
|
397
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
372
398
|
);
|
|
373
399
|
|
|
374
400
|
if (!ipcResult.ok) {
|
|
@@ -409,6 +435,8 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
409
435
|
|
|
410
436
|
subcommand(tabs, "select").action(async (opts: { tabId: string }) => {
|
|
411
437
|
const parentOpts = browser.opts() as {
|
|
438
|
+
desktop?: boolean;
|
|
439
|
+
browserMode?: string;
|
|
412
440
|
session?: string;
|
|
413
441
|
json?: boolean;
|
|
414
442
|
targetClientId?: string;
|
|
@@ -425,12 +453,16 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
425
453
|
body: {
|
|
426
454
|
command: "select",
|
|
427
455
|
sessionId,
|
|
456
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
457
|
+
...(parentOpts.browserMode
|
|
458
|
+
? { browserMode: parentOpts.browserMode }
|
|
459
|
+
: {}),
|
|
428
460
|
tabId,
|
|
429
461
|
...(conversationId ? { conversationId } : {}),
|
|
430
462
|
...(targetClientId ? { targetClientId } : {}),
|
|
431
463
|
},
|
|
432
464
|
},
|
|
433
|
-
{ timeoutMs:
|
|
465
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
434
466
|
);
|
|
435
467
|
|
|
436
468
|
if (!ipcResult.ok) {
|
|
@@ -456,6 +488,8 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
456
488
|
|
|
457
489
|
subcommand(tabs, "new").action(async (opts: { url?: string }) => {
|
|
458
490
|
const parentOpts = browser.opts() as {
|
|
491
|
+
desktop?: boolean;
|
|
492
|
+
browserMode?: string;
|
|
459
493
|
session?: string;
|
|
460
494
|
json?: boolean;
|
|
461
495
|
targetClientId?: string;
|
|
@@ -475,12 +509,16 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
475
509
|
body: {
|
|
476
510
|
command: "new",
|
|
477
511
|
sessionId,
|
|
512
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
513
|
+
...(parentOpts.browserMode
|
|
514
|
+
? { browserMode: parentOpts.browserMode }
|
|
515
|
+
: {}),
|
|
478
516
|
...(opts.url ? { url: opts.url } : {}),
|
|
479
517
|
...(conversationId ? { conversationId } : {}),
|
|
480
518
|
...(targetClientId ? { targetClientId } : {}),
|
|
481
519
|
},
|
|
482
520
|
},
|
|
483
|
-
{ timeoutMs:
|
|
521
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
484
522
|
);
|
|
485
523
|
|
|
486
524
|
if (!ipcResult.ok) {
|
|
@@ -512,6 +550,8 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
512
550
|
|
|
513
551
|
subcommand(tabs, "close").action(async (opts: { tabId: string }) => {
|
|
514
552
|
const parentOpts = browser.opts() as {
|
|
553
|
+
desktop?: boolean;
|
|
554
|
+
browserMode?: string;
|
|
515
555
|
session?: string;
|
|
516
556
|
json?: boolean;
|
|
517
557
|
targetClientId?: string;
|
|
@@ -532,12 +572,16 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
532
572
|
body: {
|
|
533
573
|
command: "close",
|
|
534
574
|
sessionId,
|
|
575
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
576
|
+
...(parentOpts.browserMode
|
|
577
|
+
? { browserMode: parentOpts.browserMode }
|
|
578
|
+
: {}),
|
|
535
579
|
tabId,
|
|
536
580
|
...(conversationId ? { conversationId } : {}),
|
|
537
581
|
...(targetClientId ? { targetClientId } : {}),
|
|
538
582
|
},
|
|
539
583
|
},
|
|
540
|
-
{ timeoutMs:
|
|
584
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
541
585
|
);
|
|
542
586
|
|
|
543
587
|
if (!ipcResult.ok) {
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
PluginAlreadyInStateException,
|
|
54
54
|
PluginDirectoryNotFoundError,
|
|
55
55
|
} from "../lib/toggle-plugin.js";
|
|
56
|
+
import type { PluginUninstallWarningKey } from "../lib/uninstall-plugin.js";
|
|
56
57
|
import type { PluginUpgradeResult } from "../lib/upgrade-plugin.js";
|
|
57
58
|
import { getCliLogger } from "../logger.js";
|
|
58
59
|
import { PLUGINS_SEARCH_INSTALL_HINT, pluginsHelp } from "./plugins.help.js";
|
|
@@ -666,11 +667,16 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
666
667
|
// when the daemon is unreachable (a transport error carries no
|
|
667
668
|
// `statusCode`); an operator can still uninstall while it's stopped,
|
|
668
669
|
// and `shutdown` then runs in this process, the only one available.
|
|
669
|
-
const daemon = await cliIpcCall<{
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
670
|
+
const daemon = await cliIpcCall<{
|
|
671
|
+
name: string;
|
|
672
|
+
target: string;
|
|
673
|
+
warnings?: PluginUninstallWarningKey[];
|
|
674
|
+
}>("plugins_uninstall", { pathParams: { name } });
|
|
675
|
+
let result: {
|
|
676
|
+
name: string;
|
|
677
|
+
target: string;
|
|
678
|
+
warnings?: PluginUninstallWarningKey[];
|
|
679
|
+
};
|
|
674
680
|
if (daemon.ok && daemon.result) {
|
|
675
681
|
result = daemon.result;
|
|
676
682
|
} else if (daemon.statusCode === undefined) {
|
|
@@ -693,6 +699,11 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
693
699
|
console.log(
|
|
694
700
|
`Uninstalled plugin "${result.name}" from ${result.target}`,
|
|
695
701
|
);
|
|
702
|
+
for (const warning of result.warnings ?? []) {
|
|
703
|
+
console.warn(
|
|
704
|
+
`Warning: ${libs.uninstall.resolvePluginUninstallWarning(warning)}`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
696
707
|
} catch (err) {
|
|
697
708
|
if (err instanceof libs.installGitHub.InvalidPluginNameError) {
|
|
698
709
|
console.error(err.message);
|
|
@@ -22,6 +22,8 @@ import { tmpdir } from "node:os";
|
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
23
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
24
24
|
|
|
25
|
+
import { readPluginMcpServers } from "../../../plugins/mcp-servers.js";
|
|
26
|
+
import { PluginManifestError } from "../../../util/plugin-manifest.js";
|
|
25
27
|
import type { FetchLike } from "../fetch-like.js";
|
|
26
28
|
import {
|
|
27
29
|
type GitRunner,
|
|
@@ -1225,6 +1227,115 @@ describe("installPlugin — direct (untrusted) install", () => {
|
|
|
1225
1227
|
expect(meta?.commit).toBe("a".repeat(40));
|
|
1226
1228
|
});
|
|
1227
1229
|
|
|
1230
|
+
test("installs a standard-only plugin without synthesizing package.json", async () => {
|
|
1231
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1232
|
+
const runGit = fakeGitRunner({
|
|
1233
|
+
tree: {
|
|
1234
|
+
"plugin.json": JSON.stringify({
|
|
1235
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
1236
|
+
name: "standard-plugin",
|
|
1237
|
+
version: "1.4.0",
|
|
1238
|
+
}),
|
|
1239
|
+
"mcp.json": JSON.stringify({
|
|
1240
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
|
1241
|
+
mcpServers: {
|
|
1242
|
+
"standard-plugin": {
|
|
1243
|
+
type: "streamable-http",
|
|
1244
|
+
url: "https://mcp.example.com",
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
}),
|
|
1248
|
+
},
|
|
1249
|
+
commit: "9".repeat(40),
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
const result = await installPlugin(
|
|
1253
|
+
{
|
|
1254
|
+
name: "standard-plugin",
|
|
1255
|
+
directSource: {
|
|
1256
|
+
owner: "owner",
|
|
1257
|
+
repo: "standard-plugin",
|
|
1258
|
+
rootPath: "",
|
|
1259
|
+
ref: "HEAD",
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
1266
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(false);
|
|
1267
|
+
expect(readInstallMeta(result.target)?.version).toBe("1.4.0");
|
|
1268
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
1269
|
+
expect(mcp.issues).toEqual([]);
|
|
1270
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["standard-plugin"]);
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test("synthesizes package.json beside a foreign plugin.json", async () => {
|
|
1274
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1275
|
+
const runGit = fakeGitRunner({
|
|
1276
|
+
tree: {
|
|
1277
|
+
"plugin.json": JSON.stringify({ name: "foreign-plugin" }),
|
|
1278
|
+
"mcp.json": JSON.stringify({
|
|
1279
|
+
mcpServers: {
|
|
1280
|
+
"foreign-plugin": {
|
|
1281
|
+
type: "streamable-http",
|
|
1282
|
+
url: "https://mcp.example.com",
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
}),
|
|
1286
|
+
},
|
|
1287
|
+
commit: "8".repeat(40),
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const result = await installPlugin(
|
|
1291
|
+
{
|
|
1292
|
+
name: "foreign-plugin",
|
|
1293
|
+
directSource: {
|
|
1294
|
+
owner: "owner",
|
|
1295
|
+
repo: "foreign-plugin",
|
|
1296
|
+
rootPath: "",
|
|
1297
|
+
ref: "HEAD",
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1301
|
+
);
|
|
1302
|
+
|
|
1303
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
1304
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(true);
|
|
1305
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
1306
|
+
expect(mcp.issues).toEqual([]);
|
|
1307
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["foreign-plugin"]);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
test("rejects a malformed claimed standard manifest before install", async () => {
|
|
1311
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1312
|
+
const runGit = fakeGitRunner({
|
|
1313
|
+
tree: {
|
|
1314
|
+
"plugin.json": JSON.stringify({
|
|
1315
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
1316
|
+
name: "Invalid Standard Name",
|
|
1317
|
+
}),
|
|
1318
|
+
},
|
|
1319
|
+
commit: "7".repeat(40),
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
await expect(
|
|
1323
|
+
installPlugin(
|
|
1324
|
+
{
|
|
1325
|
+
name: "invalid-standard",
|
|
1326
|
+
directSource: {
|
|
1327
|
+
owner: "owner",
|
|
1328
|
+
repo: "invalid-standard",
|
|
1329
|
+
rootPath: "",
|
|
1330
|
+
ref: "HEAD",
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1334
|
+
),
|
|
1335
|
+
).rejects.toBeInstanceOf(PluginManifestError);
|
|
1336
|
+
expect(existsSync(join(pluginsDir, "invalid-standard"))).toBe(false);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1228
1339
|
test("never overlays a curated adapter stub, even if one matches the name", async () => {
|
|
1229
1340
|
// GIVEN a fetch that WOULD serve an adapter stub for the name, and a
|
|
1230
1341
|
// postinstall runner that fails the test if it is ever invoked
|
|
@@ -14,6 +14,8 @@ import { join } from "node:path";
|
|
|
14
14
|
import { gzipSync } from "node:zlib";
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
16
16
|
|
|
17
|
+
import { readPluginMcpServers } from "../../../plugins/mcp-servers.js";
|
|
18
|
+
import { PluginManifestError } from "../../../util/plugin-manifest.js";
|
|
17
19
|
import type { FetchLike } from "../fetch-like.js";
|
|
18
20
|
import {
|
|
19
21
|
PluginInstallDeclinedError,
|
|
@@ -145,7 +147,13 @@ const noSleep = async () => {};
|
|
|
145
147
|
describe("installPluginFromPlatform — success", () => {
|
|
146
148
|
test("downloads, verifies, and extracts files at the plugin root", async () => {
|
|
147
149
|
const entries: TarEntry[] = [
|
|
148
|
-
{
|
|
150
|
+
{
|
|
151
|
+
name: "plugin.json",
|
|
152
|
+
content: JSON.stringify({
|
|
153
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
154
|
+
name: "reading-pal",
|
|
155
|
+
}),
|
|
156
|
+
},
|
|
149
157
|
{ name: "README.md", content: "# reading pal" },
|
|
150
158
|
{ name: "skills/read/SKILL.md", content: "skill body" },
|
|
151
159
|
];
|
|
@@ -177,7 +185,7 @@ describe("installPluginFromPlatform — success", () => {
|
|
|
177
185
|
);
|
|
178
186
|
expect(existsSync(join(target, "README.md"))).toBe(true);
|
|
179
187
|
expect(existsSync(join(target, "skills", "read", "SKILL.md"))).toBe(true);
|
|
180
|
-
expect(existsSync(join(target, "package.json"))).toBe(
|
|
188
|
+
expect(existsSync(join(target, "package.json"))).toBe(false);
|
|
181
189
|
|
|
182
190
|
// Provenance records the pinned commit and the verified ETag.
|
|
183
191
|
const meta = readInstallMeta(target);
|
|
@@ -227,6 +235,73 @@ describe("installPluginFromPlatform — success", () => {
|
|
|
227
235
|
expect(pkg.peerDependencies["@vellumai/plugin-api"]).toBeDefined();
|
|
228
236
|
});
|
|
229
237
|
|
|
238
|
+
test("synthesizes package.json beside a foreign plugin.json", async () => {
|
|
239
|
+
const fetchFn = makeInstallFetch({
|
|
240
|
+
entries: [
|
|
241
|
+
{
|
|
242
|
+
name: "plugin.json",
|
|
243
|
+
content: JSON.stringify({ name: "foreign-plugin" }),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "mcp.json",
|
|
247
|
+
content: JSON.stringify({
|
|
248
|
+
mcpServers: {
|
|
249
|
+
"foreign-plugin": {
|
|
250
|
+
type: "streamable-http",
|
|
251
|
+
url: "https://mcp.example.com",
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
ref: "d".repeat(40),
|
|
258
|
+
repo: "example-org/foreign-plugin",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const result = await installPluginFromPlatform(
|
|
262
|
+
{ name: "foreign-plugin" },
|
|
263
|
+
{
|
|
264
|
+
fetch: fetchFn,
|
|
265
|
+
platformBaseUrl: PLATFORM,
|
|
266
|
+
workspacePluginsDir: pluginsDir,
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
271
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(true);
|
|
272
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
273
|
+
expect(mcp.issues).toEqual([]);
|
|
274
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["foreign-plugin"]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("rejects a malformed claimed standard manifest before install", async () => {
|
|
278
|
+
const fetchFn = makeInstallFetch({
|
|
279
|
+
entries: [
|
|
280
|
+
{
|
|
281
|
+
name: "plugin.json",
|
|
282
|
+
content: JSON.stringify({
|
|
283
|
+
$schema:
|
|
284
|
+
"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
285
|
+
}),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
ref: "e".repeat(40),
|
|
289
|
+
repo: "example-org/invalid-standard",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await expect(
|
|
293
|
+
installPluginFromPlatform(
|
|
294
|
+
{ name: "invalid-standard" },
|
|
295
|
+
{
|
|
296
|
+
fetch: fetchFn,
|
|
297
|
+
platformBaseUrl: PLATFORM,
|
|
298
|
+
workspacePluginsDir: pluginsDir,
|
|
299
|
+
},
|
|
300
|
+
),
|
|
301
|
+
).rejects.toBeInstanceOf(PluginManifestError);
|
|
302
|
+
expect(existsSync(join(pluginsDir, "invalid-standard"))).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
230
305
|
test("leaves an upstream package.json in place", async () => {
|
|
231
306
|
const fetchFn = makeInstallFetch({
|
|
232
307
|
entries: [
|
|
@@ -93,6 +93,46 @@ describe("listInstalledPlugins", () => {
|
|
|
93
93
|
expect(result[0]!.packageJson?.icon).toBe("🚀");
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
test("lists metadata from a standard-only plugin.json", () => {
|
|
97
|
+
mkdirSync(join(pluginsDir, "standard"));
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(pluginsDir, "standard", "plugin.json"),
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
102
|
+
name: "standard",
|
|
103
|
+
version: "3.0.0",
|
|
104
|
+
description: "standard plugin",
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
109
|
+
|
|
110
|
+
expect(result[0]!.packageJson).toEqual({
|
|
111
|
+
name: "standard",
|
|
112
|
+
version: "3.0.0",
|
|
113
|
+
description: "standard plugin",
|
|
114
|
+
peerDependencies: undefined,
|
|
115
|
+
});
|
|
116
|
+
expect(result[0]!.issues).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("does not validate an unselected plugin.json beside package.json", () => {
|
|
120
|
+
mkdirSync(join(pluginsDir, "legacy"));
|
|
121
|
+
writeFileSync(
|
|
122
|
+
join(pluginsDir, "legacy", "package.json"),
|
|
123
|
+
JSON.stringify({ name: "legacy", version: "1.0.0" }),
|
|
124
|
+
);
|
|
125
|
+
writeFileSync(
|
|
126
|
+
join(pluginsDir, "legacy", "plugin.json"),
|
|
127
|
+
"{ foreign and malformed }",
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
131
|
+
|
|
132
|
+
expect(result[0]!.packageJson?.version).toBe("1.0.0");
|
|
133
|
+
expect(result[0]!.issues).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
|
|
96
136
|
test.each([
|
|
97
137
|
["missing vellum block", { name: "p", version: "1.0.0" }],
|
|
98
138
|
["non-string icon", { name: "p", version: "1.0.0", vellum: { icon: 42 } }],
|
|
@@ -120,7 +160,9 @@ describe("listInstalledPlugins", () => {
|
|
|
120
160
|
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
121
161
|
expect(result).toHaveLength(1);
|
|
122
162
|
expect(result[0]!.packageJson).toBeNull();
|
|
123
|
-
expect(result[0]!.issues).
|
|
163
|
+
expect(result[0]!.issues[0]).toContain(
|
|
164
|
+
"missing package.json and plugin.json",
|
|
165
|
+
);
|
|
124
166
|
});
|
|
125
167
|
|
|
126
168
|
test("reports malformed JSON as an issue rather than failing", () => {
|
|
@@ -130,7 +172,7 @@ describe("listInstalledPlugins", () => {
|
|
|
130
172
|
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
131
173
|
expect(result).toHaveLength(1);
|
|
132
174
|
expect(result[0]!.packageJson).toBeNull();
|
|
133
|
-
expect(result[0]!.issues[0]).
|
|
175
|
+
expect(result[0]!.issues[0]).toContain("could not be read or parsed");
|
|
134
176
|
});
|
|
135
177
|
|
|
136
178
|
test("reports non-object package.json as an issue", () => {
|
|
@@ -143,7 +185,7 @@ describe("listInstalledPlugins", () => {
|
|
|
143
185
|
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
144
186
|
expect(result).toHaveLength(1);
|
|
145
187
|
expect(result[0]!.packageJson).toBeNull();
|
|
146
|
-
expect(result[0]!.issues).toContain("
|
|
188
|
+
expect(result[0]!.issues[0]).toContain("failed schema validation");
|
|
147
189
|
});
|
|
148
190
|
|
|
149
191
|
test("skips hidden entries and non-directories", () => {
|