@frockbot/plugin-desktop-notifications 0.0.0 → 0.1.1

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-notifications",
4
+ "displayName": "Desktop Notifications",
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:notifications"]
15
+ }
package/package.json CHANGED
@@ -1,14 +1,36 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-desktop-notifications",
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.1",
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.1",
22
+ "cordis": "4.0.0-rc.8"
23
+ },
24
+ "devDependencies": {
25
+ "@types/bun": "1.4.0",
26
+ "typescript": "^7.0.2"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
6
31
  "repository": {
7
32
  "type": "git",
8
33
  "url": "git+https://github.com/timoconnellaus/frockbot.git",
9
34
  "directory": "packages/plugin-desktop-notifications"
10
- },
11
- "publishConfig": {
12
- "access": "public"
13
35
  }
14
36
  }
@@ -0,0 +1,105 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ DesktopCommandRegistry,
4
+ DesktopNotificationCapability,
5
+ type DesktopNotificationRequest,
6
+ } from "@frockbot/desktop-core";
7
+ import { Context } from "cordis";
8
+ import {
9
+ desktopNotificationsPlugin,
10
+ SHOW_NOTIFICATION_COMMAND,
11
+ type ShowNotificationResult,
12
+ } from "./desktop.js";
13
+
14
+ class FakeNotifications extends DesktopNotificationCapability {
15
+ requests: DesktopNotificationRequest[] = [];
16
+
17
+ show(
18
+ request: DesktopNotificationRequest,
19
+ signal: AbortSignal,
20
+ ): Promise<void> {
21
+ signal.throwIfAborted();
22
+ this.requests.push(request);
23
+ return Promise.resolve();
24
+ }
25
+ }
26
+
27
+ const roots: Context[] = [];
28
+
29
+ async function createHost(): Promise<{
30
+ root: Context;
31
+ notifications: FakeNotifications;
32
+ }> {
33
+ const root = new Context();
34
+ roots.push(root);
35
+ await root.plugin(DesktopCommandRegistry);
36
+ await root.plugin(FakeNotifications);
37
+ return {
38
+ root,
39
+ notifications: root.desktopNotifications as FakeNotifications,
40
+ };
41
+ }
42
+
43
+ afterEach(async () => {
44
+ await Promise.all(roots.splice(0).map((root) => root.fiber.dispose()));
45
+ });
46
+
47
+ async function expectFailure(
48
+ promise: Promise<unknown>,
49
+ message: string,
50
+ ): Promise<void> {
51
+ let failure: unknown;
52
+ try {
53
+ await promise;
54
+ } catch (error) {
55
+ failure = error;
56
+ }
57
+ expect(failure).toBeInstanceOf(Error);
58
+ expect(failure instanceof Error ? failure.message : "").toContain(message);
59
+ }
60
+
61
+ describe("desktop notifications plugin", () => {
62
+ test("registers a command backed by the granted capability", async () => {
63
+ const { root, notifications } = await createHost();
64
+ await root.plugin(desktopNotificationsPlugin);
65
+
66
+ expect(
67
+ await root.desktopCommands.invoke<ShowNotificationResult>(
68
+ SHOW_NOTIFICATION_COMMAND,
69
+ { title: " Turn complete ", body: " FrockBot is idle " },
70
+ ),
71
+ ).toEqual({ shown: true });
72
+ expect(notifications.requests).toEqual([
73
+ {
74
+ title: "Turn complete",
75
+ body: "FrockBot is idle",
76
+ urgency: "normal",
77
+ },
78
+ ]);
79
+ });
80
+
81
+ test("unregisters its command when its fiber is disposed", async () => {
82
+ const { root } = await createHost();
83
+ const fiber = await root.plugin(desktopNotificationsPlugin);
84
+
85
+ await fiber.dispose();
86
+
87
+ await expectFailure(
88
+ root.desktopCommands.invoke(SHOW_NOTIFICATION_COMMAND, {
89
+ title: "Not delivered",
90
+ }),
91
+ "is unavailable",
92
+ );
93
+ });
94
+
95
+ test("rejects malformed input before invoking the capability", async () => {
96
+ const { root, notifications } = await createHost();
97
+ await root.plugin(desktopNotificationsPlugin);
98
+
99
+ await expectFailure(
100
+ root.desktopCommands.invoke(SHOW_NOTIFICATION_COMMAND, { title: " " }),
101
+ "notification title is required",
102
+ );
103
+ expect(notifications.requests).toEqual([]);
104
+ });
105
+ });
package/src/desktop.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type {
2
+ DesktopCommand,
3
+ DesktopNotificationRequest,
4
+ } from "@frockbot/desktop-core";
5
+ import type { Plugin } from "cordis";
6
+
7
+ export const SHOW_NOTIFICATION_COMMAND = "desktop.notifications.show";
8
+
9
+ export interface ShowNotificationInput {
10
+ title: string;
11
+ body?: string;
12
+ urgency?: "normal" | "critical";
13
+ }
14
+
15
+ export interface ShowNotificationResult {
16
+ shown: true;
17
+ }
18
+
19
+ function optionalString(
20
+ record: Record<string, unknown>,
21
+ key: string,
22
+ maxLength: number,
23
+ ): string | undefined {
24
+ const value = record[key];
25
+ if (value === undefined) return undefined;
26
+ if (typeof value !== "string") throw new Error(`${key} must be a string`);
27
+ const normalized = value.trim();
28
+ if (!normalized) return undefined;
29
+ if (normalized.length > maxLength) {
30
+ throw new Error(`${key} must be at most ${maxLength} characters`);
31
+ }
32
+ return normalized;
33
+ }
34
+
35
+ export function decodeShowNotificationInput(
36
+ input: unknown,
37
+ ): ShowNotificationInput {
38
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
39
+ throw new Error("notification input must be an object");
40
+ }
41
+ const record = input as Record<string, unknown>;
42
+ const title = optionalString(record, "title", 200);
43
+ if (!title) throw new Error("notification title is required");
44
+ const body = optionalString(record, "body", 4_096);
45
+ const urgency = record.urgency ?? "normal";
46
+ if (urgency !== "normal" && urgency !== "critical") {
47
+ throw new Error('notification urgency must be "normal" or "critical"');
48
+ }
49
+ return { title, body, urgency };
50
+ }
51
+
52
+ export const desktopNotificationsPlugin: Plugin.Function = (ctx) => {
53
+ const command: DesktopCommand<ShowNotificationInput, ShowNotificationResult> =
54
+ {
55
+ id: SHOW_NOTIFICATION_COMMAND,
56
+ decode: decodeShowNotificationInput,
57
+ async execute(input, context): Promise<ShowNotificationResult> {
58
+ const request: DesktopNotificationRequest = {
59
+ title: input.title,
60
+ body: input.body,
61
+ urgency: input.urgency ?? "normal",
62
+ };
63
+ await ctx.desktopNotifications.show(request, context.signal);
64
+ return { shown: true };
65
+ },
66
+ };
67
+ return ctx.desktopCommands.register(command);
68
+ };
69
+ desktopNotificationsPlugin.inject = ["desktopCommands", "desktopNotifications"];
70
+
71
+ export default desktopNotificationsPlugin;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./desktop.js";
2
+ export { default as desktopNotificationsManifest } 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,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "resolveJsonModule": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "lib": ["ES2023", "DOM"],
11
+ "types": ["bun"]
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @frockbot/plugin-desktop-notifications
2
-
3
- Placeholder reserving this name. See https://github.com/timoconnellaus/frockbot.