@frockbot/plugin-desktop-clipboard 0.0.0 → 0.1.0

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/frockbot.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "id": "desktop-clipboard",
4
+ "displayName": "Desktop Clipboard",
5
+ "version": "0.0.1",
6
+ "compatibility": { "frockbot": ">=0.0.1" },
7
+ "contributions": {
8
+ "desktop": {
9
+ "entry": "./desktop",
10
+ "execution": "trusted-main",
11
+ "commands": []
12
+ }
13
+ },
14
+ "permissions": ["desktop:clipboard:read", "desktop:clipboard:write"]
15
+ }
package/package.json CHANGED
@@ -1,14 +1,37 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-desktop-clipboard",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving this name for trusted publishing. Superseded by the first release.",
5
- "license": "UNLICENSED",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./desktop": "./src/desktop.ts",
9
+ "./manifest": "./src/manifest.ts",
10
+ "./frockbot.json": "./frockbot.json",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "frockbot": {
14
+ "manifest": "./frockbot.json"
15
+ },
16
+ "scripts": {
17
+ "test": "bun test src",
18
+ "typecheck": "tsc --noEmit -p tsconfig.json"
19
+ },
20
+ "dependencies": {
21
+ "@frockbot/desktop-core": "0.1.0",
22
+ "cordis": "4.0.0-rc.8"
23
+ },
24
+ "devDependencies": {
25
+ "@frockbot/plugin-testkit": "0.1.0",
26
+ "@types/bun": "1.4.0",
27
+ "typescript": "^7.0.2"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
6
32
  "repository": {
7
33
  "type": "git",
8
34
  "url": "git+https://github.com/timoconnellaus/frockbot.git",
9
35
  "directory": "packages/plugin-desktop-clipboard"
10
- },
11
- "publishConfig": {
12
- "access": "public"
13
36
  }
14
37
  }
@@ -0,0 +1,132 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ DesktopClipboardCapability,
4
+ DesktopCommandRegistry,
5
+ } from "@frockbot/desktop-core";
6
+ import {
7
+ createPluginHarness,
8
+ verifyPluginPackage,
9
+ } from "@frockbot/plugin-testkit";
10
+ import manifest from "../frockbot.json" with { type: "json" };
11
+ import packageJson from "../package.json" with { type: "json" };
12
+ import desktopClipboardPlugin, {
13
+ READ_CLIPBOARD_TEXT_COMMAND,
14
+ type ReadClipboardTextResult,
15
+ WRITE_CLIPBOARD_TEXT_COMMAND,
16
+ type WriteClipboardTextResult,
17
+ } from "./desktop.js";
18
+
19
+ class FakeClipboard extends DesktopClipboardCapability {
20
+ text = "initial";
21
+
22
+ readText(signal: AbortSignal): Promise<string> {
23
+ signal.throwIfAborted();
24
+ return Promise.resolve(this.text);
25
+ }
26
+
27
+ writeText(text: string, signal: AbortSignal): Promise<void> {
28
+ signal.throwIfAborted();
29
+ this.text = text;
30
+ return Promise.resolve();
31
+ }
32
+ }
33
+
34
+ async function createHost(): Promise<{
35
+ harness: Awaited<ReturnType<typeof createPluginHarness>>;
36
+ clipboard: FakeClipboard;
37
+ }> {
38
+ const harness = await createPluginHarness([
39
+ DesktopCommandRegistry,
40
+ FakeClipboard,
41
+ ]);
42
+ return {
43
+ harness,
44
+ clipboard: harness.root.desktopClipboard as FakeClipboard,
45
+ };
46
+ }
47
+
48
+ async function expectFailure(
49
+ promise: Promise<unknown>,
50
+ message: string,
51
+ ): Promise<void> {
52
+ let failure: unknown;
53
+ try {
54
+ await promise;
55
+ } catch (error) {
56
+ failure = error;
57
+ }
58
+ expect(failure).toBeInstanceOf(Error);
59
+ expect(failure instanceof Error ? failure.message : "").toContain(message);
60
+ }
61
+
62
+ describe("desktop clipboard plugin", () => {
63
+ test("registers read and write commands over the granted capability", async () => {
64
+ const { harness, clipboard } = await createHost();
65
+ await harness.mount(desktopClipboardPlugin);
66
+
67
+ expect(
68
+ await harness.root.desktopCommands.invoke<ReadClipboardTextResult>(
69
+ READ_CLIPBOARD_TEXT_COMMAND,
70
+ {},
71
+ ),
72
+ ).toEqual({ text: "initial" });
73
+ expect(
74
+ await harness.root.desktopCommands.invoke<WriteClipboardTextResult>(
75
+ WRITE_CLIPBOARD_TEXT_COMMAND,
76
+ { text: "updated" },
77
+ ),
78
+ ).toEqual({ written: true });
79
+ expect(clipboard.text).toBe("updated");
80
+ await harness.dispose();
81
+ });
82
+
83
+ test("preserves exact clipboard text, including an empty string", async () => {
84
+ const { harness, clipboard } = await createHost();
85
+ await harness.mount(desktopClipboardPlugin);
86
+
87
+ await harness.root.desktopCommands.invoke(WRITE_CLIPBOARD_TEXT_COMMAND, {
88
+ text: "",
89
+ });
90
+ expect(clipboard.text).toBe("");
91
+ await harness.dispose();
92
+ });
93
+
94
+ test("unregisters both commands on disposal", async () => {
95
+ const { harness } = await createHost();
96
+ const fiber = await harness.mount(desktopClipboardPlugin);
97
+
98
+ await fiber.dispose();
99
+ await expectFailure(
100
+ harness.root.desktopCommands.invoke(READ_CLIPBOARD_TEXT_COMMAND, {}),
101
+ "is unavailable",
102
+ );
103
+ await expectFailure(
104
+ harness.root.desktopCommands.invoke(WRITE_CLIPBOARD_TEXT_COMMAND, {
105
+ text: "not written",
106
+ }),
107
+ "is unavailable",
108
+ );
109
+ await harness.dispose();
110
+ });
111
+
112
+ test("rejects malformed writes before invoking the capability", async () => {
113
+ const { harness, clipboard } = await createHost();
114
+ await harness.mount(desktopClipboardPlugin);
115
+
116
+ await expectFailure(
117
+ harness.root.desktopCommands.invoke(WRITE_CLIPBOARD_TEXT_COMMAND, {
118
+ text: 42,
119
+ }),
120
+ "clipboard text must be a string",
121
+ );
122
+ expect(clipboard.text).toBe("initial");
123
+ await harness.dispose();
124
+ });
125
+
126
+ test("satisfies plugin package conventions", () => {
127
+ expect(verifyPluginPackage({ packageJson, manifest })).toMatchObject({
128
+ name: "@frockbot/plugin-desktop-clipboard",
129
+ contributionKinds: ["desktop"],
130
+ });
131
+ });
132
+ });
package/src/desktop.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type {
2
+ DesktopCommand,
3
+ DesktopClipboardCapability,
4
+ } from "@frockbot/desktop-core";
5
+ import type { Plugin } from "cordis";
6
+
7
+ export const READ_CLIPBOARD_TEXT_COMMAND = "desktop.clipboard.readText";
8
+ export const WRITE_CLIPBOARD_TEXT_COMMAND = "desktop.clipboard.writeText";
9
+ export const MAX_CLIPBOARD_TEXT_LENGTH = 1_000_000;
10
+
11
+ export type ReadClipboardTextInput = Record<string, never>;
12
+
13
+ export interface ReadClipboardTextResult {
14
+ text: string;
15
+ }
16
+
17
+ export interface WriteClipboardTextInput {
18
+ text: string;
19
+ }
20
+
21
+ export interface WriteClipboardTextResult {
22
+ written: true;
23
+ }
24
+
25
+ function inputRecord(input: unknown): Record<string, unknown> {
26
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
27
+ throw new Error("clipboard input must be an object");
28
+ }
29
+ return input as Record<string, unknown>;
30
+ }
31
+
32
+ export function decodeReadClipboardTextInput(
33
+ input: unknown,
34
+ ): ReadClipboardTextInput {
35
+ inputRecord(input);
36
+ return {};
37
+ }
38
+
39
+ export function decodeWriteClipboardTextInput(
40
+ input: unknown,
41
+ ): WriteClipboardTextInput {
42
+ const record = inputRecord(input);
43
+ if (typeof record.text !== "string") {
44
+ throw new Error("clipboard text must be a string");
45
+ }
46
+ if (record.text.length > MAX_CLIPBOARD_TEXT_LENGTH) {
47
+ throw new Error(
48
+ `clipboard text must be at most ${MAX_CLIPBOARD_TEXT_LENGTH} characters`,
49
+ );
50
+ }
51
+ return { text: record.text };
52
+ }
53
+
54
+ function readCommand(
55
+ clipboard: DesktopClipboardCapability,
56
+ ): DesktopCommand<ReadClipboardTextInput, ReadClipboardTextResult> {
57
+ return {
58
+ id: READ_CLIPBOARD_TEXT_COMMAND,
59
+ decode: decodeReadClipboardTextInput,
60
+ async execute(_input, context): Promise<ReadClipboardTextResult> {
61
+ return { text: await clipboard.readText(context.signal) };
62
+ },
63
+ };
64
+ }
65
+
66
+ function writeCommand(
67
+ clipboard: DesktopClipboardCapability,
68
+ ): DesktopCommand<WriteClipboardTextInput, WriteClipboardTextResult> {
69
+ return {
70
+ id: WRITE_CLIPBOARD_TEXT_COMMAND,
71
+ decode: decodeWriteClipboardTextInput,
72
+ async execute(input, context): Promise<WriteClipboardTextResult> {
73
+ await clipboard.writeText(input.text, context.signal);
74
+ return { written: true };
75
+ },
76
+ };
77
+ }
78
+
79
+ export const desktopClipboardPlugin: Plugin.Function = (ctx) => [
80
+ ctx.desktopCommands.register(readCommand(ctx.desktopClipboard)),
81
+ ctx.desktopCommands.register(writeCommand(ctx.desktopClipboard)),
82
+ ];
83
+ desktopClipboardPlugin.inject = ["desktopCommands", "desktopClipboard"];
84
+
85
+ export default desktopClipboardPlugin;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./desktop.js";
2
+ export { default as desktopClipboardManifest } from "./manifest.js";
@@ -0,0 +1,3 @@
1
+ import manifest from "../frockbot.json" with { type: "json" };
2
+
3
+ export default manifest;
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "resolveJsonModule": true,
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "lib": ["ES2023", "DOM"],
12
+ "types": ["bun"]
13
+ },
14
+ "include": ["src/**/*.ts"]
15
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @frockbot/plugin-desktop-clipboard
2
-
3
- Placeholder reserving this name. See https://github.com/timoconnellaus/frockbot.