@xpufx/paseo-forges 0.1.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.
@@ -0,0 +1,71 @@
1
+ import React from "react";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import type {
4
+ PluginTimelineItemProps,
5
+ PluginTimelineRendererContribution,
6
+ PluginTimelineTransformerContribution,
7
+ } from "@getpaseo/plugin/client";
8
+ import { Icon } from "@getpaseo/plugin/client/react-native";
9
+ import { Badge } from "paseo-plugin-helper/client";
10
+ import {
11
+ forgejoNotificationCardSchema,
12
+ forgejoNotificationItem,
13
+ type ForgejoNotificationCardData,
14
+ } from "../shared/notification.js";
15
+
16
+ /**
17
+ * Forgejo digest notifications are emitted as host `notification` rows rather
18
+ * than chat messages. Turn them into a typed card without claiming unrelated
19
+ * notifications (including provider errors and other plugins' notices).
20
+ */
21
+ export const forgejoNotificationTransformer: PluginTimelineTransformerContribution<"notification"> = {
22
+ id: "forgejo-notification",
23
+ query: { itemType: "notification" },
24
+ transform({ item }) {
25
+ return forgejoNotificationItem(item);
26
+ },
27
+ };
28
+
29
+ function levelColor(
30
+ theme: PluginTimelineItemProps<ForgejoNotificationCardData>["theme"],
31
+ level: ForgejoNotificationCardData["level"],
32
+ ) {
33
+ if (level === "error") return theme.colors.statusDanger;
34
+ if (level === "warning") return theme.colors.statusWarning;
35
+ return theme.colors.accent;
36
+ }
37
+
38
+ export function ForgejoNotificationCard({
39
+ item,
40
+ theme,
41
+ }: PluginTimelineItemProps<ForgejoNotificationCardData>) {
42
+ const color = levelColor(theme, item.data.level);
43
+ return (
44
+ <View style={[styles.card, { backgroundColor: theme.colors.surface1, borderColor: theme.colors.border }]}>
45
+ <View style={styles.header}>
46
+ <Icon name="Bell" size={14} color={color} />
47
+ <Text style={[styles.title, { color: theme.colors.foreground }]}>Forgejo digest</Text>
48
+ <Badge variant={item.data.level === "error" ? "danger" : item.data.level === "warning" ? "warning" : "info"} label={item.data.level} />
49
+ </View>
50
+ <Text style={[styles.message, { color: theme.colors.foreground }]}>{item.data.message}</Text>
51
+ <Text style={[styles.footer, { color: theme.colors.foregroundMuted }]}>via forges</Text>
52
+ </View>
53
+ );
54
+ }
55
+
56
+ export const forgejoNotificationRenderer: PluginTimelineRendererContribution<
57
+ typeof forgejoNotificationCardSchema
58
+ > = {
59
+ kind: "forgejo-notification",
60
+ version: 1,
61
+ schema: forgejoNotificationCardSchema,
62
+ Component: ForgejoNotificationCard,
63
+ };
64
+
65
+ const styles = StyleSheet.create({
66
+ card: { borderRadius: 8, borderWidth: 1, padding: 8, marginVertical: 2, gap: 6 },
67
+ header: { flexDirection: "row", alignItems: "center", gap: 6 },
68
+ title: { fontSize: 12, fontWeight: "600", flex: 1 },
69
+ message: { fontSize: 13 },
70
+ footer: { fontSize: 10, fontStyle: "italic" },
71
+ });
@@ -24,16 +24,23 @@ export interface ClipboardEnvironment {
24
24
  * `react-native-web` `Clipboard.setString` reports success even when its
25
25
  * `document.execCommand("copy")` fails, which leaves the previous clipboard
26
26
  * item in place while the UI claims success (xpufx-org/paseo#278); it is only
27
- * usable off-DOM (native), where it is the real platform clipboard. In a DOM
28
- * the checked `execCommand` fallback is preferred to that unverifiable path.
27
+ * usable off-DOM (native), where it is the real platform clipboard. The same
28
+ * applies to any `setStringAsync` whose DOM fallback is that unverified
29
+ * `execCommand`. In a DOM the checked `execCommand` fallback is preferred to
30
+ * those unverifiable paths.
29
31
  */
30
32
  export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
31
33
  const tiers: ClipboardTier[] = [];
32
34
  if (env.hasNavigatorClipboard) tiers.push("navigator");
33
35
  if (env.hasHostCopyText) tiers.push("host");
34
- if (env.hasRnSetStringAsync) {
36
+ // On a DOM both RN-web paths are unverifiable: `setString` reports success
37
+ // even when its `execCommand` no-ops, and a `setStringAsync` that falls back
38
+ // to it does the same. Off-DOM they are the real native clipboard, so they
39
+ // lead there; on a DOM only the checked `execCommand` fallback is honest.
40
+ const rnDomOk = !env.isDom;
41
+ if (env.hasRnSetStringAsync && rnDomOk) {
35
42
  tiers.push("rnAsync");
36
- } else if (env.hasRnClipboard && !env.isDom) {
43
+ } else if (env.hasRnClipboard && rnDomOk) {
37
44
  tiers.push("rnSync");
38
45
  }
39
46
  tiers.push("execCommand");
@@ -0,0 +1,127 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+ import { useRpc } from "@getpaseo/plugin/client";
3
+ import { Icon, Modal, useToast, ScrollView, FlatList, TextInput as HostTextInput, copyText } from "@getpaseo/plugin/client/react-native";
4
+ import {
5
+ initClientHelpers,
6
+ registerComposerPill,
7
+ registerSidebarSurface,
8
+ } from "paseo-plugin-helper/client";
9
+ import {
10
+ ISSUES_PILL_ID,
11
+ ForgePill,
12
+ ForgeIssuesModal,
13
+ ForgeIssuesPanel,
14
+ } from "./client/issues-pill.js";
15
+ import { createForgeLabelResolver } from "./client/pill-label.js";
16
+ import {
17
+ forgeLinkUserTransformer,
18
+ forgeLinkAssistantTransformer,
19
+ forgeLinkRenderer,
20
+ } from "./client/linkifier.js";
21
+ import {
22
+ forgeBoardAlertUserTransformer,
23
+ forgeBoardAlertAssistantTransformer,
24
+ forgeBoardAlertRenderer,
25
+ } from "./client/board-alert.js";
26
+ import {
27
+ forgejoWebhookUserTransformer,
28
+ forgejoWebhookRenderer,
29
+ } from "./client/webhook-card.js";
30
+ import {
31
+ forgejoNotificationTransformer,
32
+ forgejoNotificationRenderer,
33
+ } from "./client/notification-card.js";
34
+ import {
35
+ ForgeHookQueuePanel,
36
+ ForgeHookQueueSurface,
37
+ } from "./client/hook-queue-panel.js";
38
+
39
+ initClientHelpers({ Icon, Modal, useRpc, useToast, copyText, ScrollView, FlatList, TextInput: HostTextInput });
40
+
41
+ export default function contribute(client: PluginClientContext) {
42
+ // Hook cards are registered before the linkifier: first-match-wins, and hook
43
+ // messages carry a bare issue URL the linkifier would otherwise claim.
44
+ const removeWebhookUser = client.addTimelineTransformer(forgejoWebhookUserTransformer);
45
+ const removeWebhookRenderer = client.addTimelineRenderer(forgejoWebhookRenderer);
46
+ const removeNotification = client.addTimelineTransformer(forgejoNotificationTransformer);
47
+ const removeNotificationRenderer = client.addTimelineRenderer(forgejoNotificationRenderer);
48
+ const removeUserLink = client.addTimelineTransformer(forgeLinkUserTransformer);
49
+ const removeAssistantLink = client.addTimelineTransformer(forgeLinkAssistantTransformer);
50
+ const removeLinkRenderer = client.addTimelineRenderer(forgeLinkRenderer);
51
+ const removeBoardAlertUser = client.addTimelineTransformer(forgeBoardAlertUserTransformer);
52
+ const removeBoardAlertAssistant = client.addTimelineTransformer(forgeBoardAlertAssistantTransformer);
53
+ const removeBoardAlertRenderer = client.addTimelineRenderer(forgeBoardAlertRenderer);
54
+
55
+ // RPC is host-provided and absent on some older hosts; the resolver degrades
56
+ // to the workspace-name / ellipsis fallback instead of failing registration.
57
+ const rawRpc = typeof client.rpc === "function" ? client.rpc.bind(client) : null;
58
+ const forgeLabel = createForgeLabelResolver({
59
+ rpc: (contract, input) =>
60
+ rawRpc
61
+ ? (rawRpc(contract as never, input as never) as Promise<unknown>)
62
+ : Promise.reject(new Error("client.rpc unavailable")),
63
+ resolveWorkspace: async (workspaceId) => {
64
+ const workspace = await client.paseo.workspaces.ref(workspaceId).refresh();
65
+ return workspace
66
+ ? {
67
+ directory: workspace.workspaceDirectory ?? undefined,
68
+ projectRootPath: workspace.projectRootPath,
69
+ }
70
+ : null;
71
+ },
72
+ });
73
+
74
+ const removePill = registerComposerPill(client, {
75
+ id: ISSUES_PILL_ID,
76
+ title: "issues",
77
+ modalTitle: "Forge Issues",
78
+ modalIcon: "GitPullRequest",
79
+ icon: "GitPullRequest",
80
+ resolveLabel: (ctx) => forgeLabel.resolve(ctx),
81
+ refreshIntervalMs: 5000,
82
+ renderPill: (props) => <ForgePill {...props} />,
83
+ renderModal: (props) => <ForgeIssuesModal {...props} />,
84
+ });
85
+
86
+ const removePanel = client.addWorkspacePanel({
87
+ id: "forges-issues",
88
+ title: "Forge Issues",
89
+ icon: "GitPullRequest",
90
+ context: "workspace",
91
+ locations: ["workspace", "explorer"],
92
+ Component: ForgeIssuesPanel,
93
+ });
94
+
95
+ const removeQueuePanel = client.addWorkspacePanel({
96
+ id: "forges-queues",
97
+ title: "Forge Queues",
98
+ icon: "Layers",
99
+ context: "workspace",
100
+ locations: ["workspace", "explorer"],
101
+ Component: ForgeHookQueuePanel,
102
+ });
103
+
104
+ const removeQueueSurface = registerSidebarSurface(client, {
105
+ id: "queues",
106
+ title: "Forge Queues",
107
+ icon: "Layers",
108
+ Component: ForgeHookQueueSurface,
109
+ });
110
+
111
+ return () => {
112
+ removeQueueSurface?.();
113
+ removeQueuePanel?.();
114
+ removePanel();
115
+ removePill();
116
+ removeWebhookUser();
117
+ removeWebhookRenderer();
118
+ removeNotification();
119
+ removeNotificationRenderer();
120
+ removeUserLink();
121
+ removeAssistantLink();
122
+ removeLinkRenderer();
123
+ removeBoardAlertUser();
124
+ removeBoardAlertAssistant();
125
+ removeBoardAlertRenderer();
126
+ };
127
+ }
@@ -0,0 +1,63 @@
1
+ import type { PluginServerContext } from "@getpaseo/plugin/server";
2
+ import { createPluginLogger } from "paseo-plugin-helper/server";
3
+ import {
4
+ addCommentContract,
5
+ createIssueContract,
6
+ forgeContextContract,
7
+ forgeSettingsContract,
8
+ issueDetailContract,
9
+ openIssuesContract,
10
+ searchIssuesContract,
11
+ setLabelContract,
12
+ } from "./shared/issues.js";
13
+ import {
14
+ handleAddComment,
15
+ handleCreateIssue,
16
+ handleForgeContext,
17
+ handleIssueDetail,
18
+ handleOpenIssues,
19
+ handleSearchIssues,
20
+ handleSetLabel,
21
+ } from "./server/issues.js";
22
+ import { settingsHandlers } from "./server/settings.js";
23
+ import {
24
+ hookStatusContract,
25
+ hookQueuesContract,
26
+ hookPauseContract,
27
+ hookResumeContract,
28
+ hookDrainContract,
29
+ } from "./shared/hook-queue.js";
30
+ import {
31
+ handleHookStatus,
32
+ handleHookQueues,
33
+ handleHookPause,
34
+ handleHookResume,
35
+ handleHookDrain,
36
+ } from "./server/hook-queue.js";
37
+
38
+ const log = createPluginLogger("forges");
39
+
40
+ export default function contribute(server: PluginServerContext) {
41
+ server.handle(openIssuesContract, handleOpenIssues);
42
+ server.handle(searchIssuesContract, handleSearchIssues);
43
+ server.handle(forgeContextContract, handleForgeContext);
44
+ server.handle(issueDetailContract, handleIssueDetail);
45
+ server.handle(setLabelContract, handleSetLabel);
46
+ server.handle(addCommentContract, handleAddComment);
47
+ server.handle(createIssueContract, handleCreateIssue);
48
+
49
+ server.handle(hookStatusContract, handleHookStatus);
50
+ server.handle(hookQueuesContract, handleHookQueues);
51
+ server.handle(hookPauseContract, handleHookPause);
52
+ server.handle(hookResumeContract, handleHookResume);
53
+ server.handle(hookDrainContract, handleHookDrain);
54
+
55
+ // forge.install-labels is operator-only: the optional label-set install is
56
+ // hidden from the release surface (issue #163). handleInstallLabels stays in
57
+ // server/issues.ts for our own board; the RPC is deliberately not registered.
58
+ server.handle(forgeSettingsContract.get, settingsHandlers.get);
59
+ server.handle(forgeSettingsContract.update, settingsHandlers.update);
60
+ server.handle(forgeSettingsContract.reset, settingsHandlers.reset);
61
+ log.info("forges server handlers registered");
62
+ return () => {};
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xpufx/paseo-forges",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Work with Forge/Gitea-family issues from inside Paseo via the embedded fetch API client.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -8,7 +8,7 @@
8
8
  "scripts": {
9
9
  "stamp": "node -e \"import('./server/vendor/paseo-plugin-helper/version.ts').then(m => m.stampVersion({ targetFile: 'shared/version.ts' }))\"",
10
10
  "typecheck": "tsc --noEmit",
11
- "test": "esbuild shared/issues.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-issues.test.mjs --log-level=error && esbuild shared/webhook.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-webhook.test.mjs --log-level=error && esbuild shared/hook-queue.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-hook-queue.test.mjs --log-level=error && esbuild server/git-origin.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-git-origin.test.mjs --log-level=error && esbuild server/registry.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-registry.test.mjs --log-level=error && esbuild server/forge-guard.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-forge-guard.test.mjs --log-level=error && esbuild server/forge-client.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-forge-client.test.mjs --log-level=error && esbuild server/hook-queue.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-server-hook-queue.test.mjs --log-level=error && node --test ${TMPDIR:-/tmp}/paseo-forges-issues.test.mjs ${TMPDIR:-/tmp}/paseo-forges-webhook.test.mjs ${TMPDIR:-/tmp}/paseo-forges-hook-queue.test.mjs ${TMPDIR:-/tmp}/paseo-forges-git-origin.test.mjs ${TMPDIR:-/tmp}/paseo-forges-registry.test.mjs ${TMPDIR:-/tmp}/paseo-forges-forge-guard.test.mjs ${TMPDIR:-/tmp}/paseo-forges-forge-client.test.mjs ${TMPDIR:-/tmp}/paseo-forges-server-hook-queue.test.mjs"
11
+ "test": "esbuild shared/issues.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-issues.test.mjs --log-level=error && esbuild shared/webhook.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-webhook.test.mjs --log-level=error && esbuild shared/notification.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-notification.test.mjs --log-level=error && esbuild shared/hook-queue.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-hook-queue.test.mjs --log-level=error && esbuild server/git-origin.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-git-origin.test.mjs --log-level=error && esbuild server/registry.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-registry.test.mjs --log-level=error && esbuild server/forge-guard.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-forge-guard.test.mjs --log-level=error && esbuild server/forge-client.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-forge-client.test.mjs --log-level=error && esbuild server/hook-queue.test.ts --bundle --platform=node --format=esm --outfile=${TMPDIR:-/tmp}/paseo-forges-server-hook-queue.test.mjs --log-level=error && node --test ${TMPDIR:-/tmp}/paseo-forges-issues.test.mjs ${TMPDIR:-/tmp}/paseo-forges-webhook.test.mjs ${TMPDIR:-/tmp}/paseo-forges-notification.test.mjs ${TMPDIR:-/tmp}/paseo-forges-hook-queue.test.mjs ${TMPDIR:-/tmp}/paseo-forges-git-origin.test.mjs ${TMPDIR:-/tmp}/paseo-forges-registry.test.mjs ${TMPDIR:-/tmp}/paseo-forges-forge-guard.test.mjs ${TMPDIR:-/tmp}/paseo-forges-forge-client.test.mjs ${TMPDIR:-/tmp}/paseo-forges-server-hook-queue.test.mjs"
12
12
  },
13
13
  "devDependencies": {
14
14
  "@getpaseo/plugin": "0.9.0-beta.2",
@@ -21,6 +21,8 @@
21
21
  "paseo-plugin.json",
22
22
  "README.md",
23
23
  "LICENSE",
24
+ "index.client.tsx",
25
+ "index.server.ts",
24
26
  "client",
25
27
  "server",
26
28
  "shared",
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+
3
+ /** The notification emitted by the Forgejo digest worker. */
4
+ export const FORGEJO_DIGEST_PREFIX = "🔔 Forgejo digest";
5
+
6
+ export const forgejoNotificationCardSchema = z.object({
7
+ level: z.enum(["info", "warning", "error"]),
8
+ message: z.string(),
9
+ });
10
+
11
+ export type ForgejoNotificationCardData = z.infer<typeof forgejoNotificationCardSchema>;
12
+
13
+ /**
14
+ * Creates a presentation-only card for Forgejo digest notifications. Other
15
+ * host notifications deliberately pass through to the host renderer: this
16
+ * plugin owns the digest format, not the notification timeline type itself.
17
+ */
18
+ export function forgejoNotificationItem(item: {
19
+ level: ForgejoNotificationCardData["level"];
20
+ message: string;
21
+ }) {
22
+ if (!item.message.trimStart().startsWith(FORGEJO_DIGEST_PREFIX)) return undefined;
23
+ return {
24
+ items: [
25
+ {
26
+ type: "plugin" as const,
27
+ kind: "forgejo-notification",
28
+ version: 1,
29
+ data: { level: item.level, message: item.message },
30
+ },
31
+ ],
32
+ };
33
+ }