From 6b54065633e69218ab12134fb6b1dc4e9ca964c0 Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Sat, 14 Feb 2026 09:18:06 -0300 Subject: [PATCH 01/26] feat(01-01): define SITE_BINDING and create site-editor plugin skeleton - Add SITE_BINDING with READ_FILE, PUT_FILE, LIST_FILES tool binders - Export SITE_BINDING from @decocms/bindings and add ./site entry point - Create mesh-plugin-site-editor package with server/client entry points - Add shared.ts with PLUGIN_ID and PLUGIN_DESCRIPTION constants Co-Authored-By: Claude Opus 4.6 --- packages/bindings/package.json | 1 + packages/bindings/src/index.ts | 12 ++ packages/bindings/src/well-known/site.ts | 108 ++++++++++++++++++ packages/mesh-plugin-site-editor/package.json | 31 +++++ packages/mesh-plugin-site-editor/shared.ts | 9 ++ .../mesh-plugin-site-editor/tsconfig.json | 12 ++ 6 files changed, 173 insertions(+) create mode 100644 packages/bindings/src/well-known/site.ts create mode 100644 packages/mesh-plugin-site-editor/package.json create mode 100644 packages/mesh-plugin-site-editor/shared.ts create mode 100644 packages/mesh-plugin-site-editor/tsconfig.json diff --git a/packages/bindings/package.json b/packages/bindings/package.json index 79386ff3ca..a4d5688308 100644 --- a/packages/bindings/package.json +++ b/packages/bindings/package.json @@ -22,6 +22,7 @@ "./collections": "./src/well-known/collections.ts", "./llm": "./src/well-known/language-model.ts", "./object-storage": "./src/well-known/object-storage.ts", + "./site": "./src/well-known/site.ts", "./connection": "./src/core/connection.ts", "./client": "./src/core/client/index.ts", "./mcp": "./src/well-known/mcp.ts", diff --git a/packages/bindings/src/index.ts b/packages/bindings/src/index.ts index eee481f77b..67a79a4f58 100644 --- a/packages/bindings/src/index.ts +++ b/packages/bindings/src/index.ts @@ -102,6 +102,18 @@ export { type DeleteObjectsOutput, } from "./well-known/object-storage"; +// Re-export site binding types +export { + SITE_BINDING, + type SiteBinding, + type ReadFileInput, + type ReadFileOutput, + type PutFileInput, + type PutFileOutput, + type ListFilesInput, + type ListFilesOutput, +} from "./well-known/site"; + // Re-export workflow binding types export { WORKFLOWS_COLLECTION_BINDING } from "./well-known/workflow"; diff --git a/packages/bindings/src/well-known/site.ts b/packages/bindings/src/well-known/site.ts new file mode 100644 index 0000000000..465a8c556c --- /dev/null +++ b/packages/bindings/src/well-known/site.ts @@ -0,0 +1,108 @@ +/** + * Site Well-Known Binding + * + * Defines the interface for site file operations (read, write, list). + * Any MCP that implements this binding can provide file management + * for a site's pages, sections, and loaders. + * + * This binding includes: + * - READ_FILE: Read a file's content by path + * - PUT_FILE: Write content to a file by path + * - LIST_FILES: List files with optional prefix filtering + */ + +import { z } from "zod"; +import type { Binder, ToolBinder } from "../core/binder"; + +// ============================================================================ +// Tool Schemas +// ============================================================================ + +/** + * READ_FILE - Read a file's content by path + */ +const ReadFileInputSchema = z.object({ + path: z.string().describe("File path relative to project root"), +}); + +const ReadFileOutputSchema = z.object({ + content: z.string().describe("File content as UTF-8 string"), +}); + +export type ReadFileInput = z.infer; +export type ReadFileOutput = z.infer; + +/** + * PUT_FILE - Write content to a file by path + */ +const PutFileInputSchema = z.object({ + path: z.string().describe("File path relative to project root"), + content: z.string().describe("File content as UTF-8 string"), +}); + +const PutFileOutputSchema = z.object({ + success: z.boolean().describe("Whether the write succeeded"), +}); + +export type PutFileInput = z.infer; +export type PutFileOutput = z.infer; + +/** + * LIST_FILES - List files with optional prefix filtering + */ +const ListFilesInputSchema = z.object({ + prefix: z + .string() + .optional() + .describe("Path prefix filter (e.g., '.deco/pages/')"), +}); + +const ListFilesOutputSchema = z.object({ + files: z.array( + z.object({ + path: z.string().describe("File path relative to project root"), + sizeInBytes: z.number().describe("File size"), + mtime: z.number().describe("Last modified timestamp (epoch ms)"), + }), + ), + count: z.number().describe("Total file count"), +}); + +export type ListFilesInput = z.infer; +export type ListFilesOutput = z.infer; + +// ============================================================================ +// Binding Definition +// ============================================================================ + +/** + * Site Binding + * + * Defines the interface for site file operations. + * Any MCP that implements this binding can be used with the Site Editor plugin + * to provide a CMS UI for managing pages, sections, and loaders. + * + * Required tools: + * - READ_FILE: Read a file's content + * - PUT_FILE: Write content to a file + * - LIST_FILES: List files with prefix filtering + */ +export const SITE_BINDING = [ + { + name: "READ_FILE" as const, + inputSchema: ReadFileInputSchema, + outputSchema: ReadFileOutputSchema, + } satisfies ToolBinder<"READ_FILE", ReadFileInput, ReadFileOutput>, + { + name: "PUT_FILE" as const, + inputSchema: PutFileInputSchema, + outputSchema: PutFileOutputSchema, + } satisfies ToolBinder<"PUT_FILE", PutFileInput, PutFileOutput>, + { + name: "LIST_FILES" as const, + inputSchema: ListFilesInputSchema, + outputSchema: ListFilesOutputSchema, + } satisfies ToolBinder<"LIST_FILES", ListFilesInput, ListFilesOutput>, +] as const satisfies Binder; + +export type SiteBinding = typeof SITE_BINDING; diff --git a/packages/mesh-plugin-site-editor/package.json b/packages/mesh-plugin-site-editor/package.json new file mode 100644 index 0000000000..1ca5216186 --- /dev/null +++ b/packages/mesh-plugin-site-editor/package.json @@ -0,0 +1,31 @@ +{ + "name": "mesh-plugin-site-editor", + "version": "0.1.0", + "type": "module", + "description": "Site editor plugin for Mesh - manage pages, sections, and loaders", + "scripts": { + "check": "tsc --noEmit", + "test": "bun test" + }, + "exports": { + "./server": "./server/index.ts", + "./client": "./client/index.tsx" + }, + "dependencies": { + "@decocms/bindings": "workspace:*", + "@decocms/mesh-sdk": "workspace:*", + "@deco/ui": "workspace:*", + "@untitledui/icons": "^0.0.19", + "zod": "^4.0.0" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "@tanstack/react-router": ">=1.0.0", + "nanoid": ">=5.0.0", + "react": "^19.2.0" + } +} diff --git a/packages/mesh-plugin-site-editor/shared.ts b/packages/mesh-plugin-site-editor/shared.ts new file mode 100644 index 0000000000..b247d58259 --- /dev/null +++ b/packages/mesh-plugin-site-editor/shared.ts @@ -0,0 +1,9 @@ +/** + * Site Editor Plugin - Shared Constants + * + * Safe for importing in both server and client bundles. + */ + +export const PLUGIN_ID = "site-editor"; +export const PLUGIN_DESCRIPTION = + "CMS for managing site pages, sections, and loaders"; diff --git a/packages/mesh-plugin-site-editor/tsconfig.json b/packages/mesh-plugin-site-editor/tsconfig.json new file mode 100644 index 0000000000..88d880a5b3 --- /dev/null +++ b/packages/mesh-plugin-site-editor/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx" + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} From a0e32de867b6175efe9b18f3285ad03eeb4cc04c Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Sat, 14 Feb 2026 09:18:52 -0300 Subject: [PATCH 02/26] feat(01-02): add preview panel component and tunnel URL hook - PreviewPanel renders iframe pointing to tunnel URL with sandbox attrs - useTunnelUrl resolves preview URL from connection metadata - Empty state shows instructions to run `deco link` - Component accepts path prop for page-specific previews Co-Authored-By: Claude Opus 4.6 --- .../client/components/preview-panel.tsx | 61 +++++++++++++++++++ .../client/lib/use-tunnel-url.ts | 44 +++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 packages/mesh-plugin-site-editor/client/components/preview-panel.tsx create mode 100644 packages/mesh-plugin-site-editor/client/lib/use-tunnel-url.ts diff --git a/packages/mesh-plugin-site-editor/client/components/preview-panel.tsx b/packages/mesh-plugin-site-editor/client/components/preview-panel.tsx new file mode 100644 index 0000000000..4fa3c70e53 --- /dev/null +++ b/packages/mesh-plugin-site-editor/client/components/preview-panel.tsx @@ -0,0 +1,61 @@ +/** + * Preview Panel Component + * + * Renders a full-size iframe pointing to the user's running local dev server + * via the tunnel URL created by `deco link`. When no tunnel URL is available, + * shows a helpful empty state with instructions. + * + * This component fills its parent container (100% width and height). + * Responsive viewport toggles will be added in Phase 3 (EDIT-04). + */ + +import { useTunnelUrl } from "../lib/use-tunnel-url"; + +interface PreviewPanelProps { + /** Page path to preview (e.g., "/", "/about") */ + path?: string; +} + +export function PreviewPanel({ path = "/" }: PreviewPanelProps) { + const { url, isLoading } = useTunnelUrl(); + + if (isLoading) { + return ( +
+ + Loading preview... + +
+ ); + } + + if (!url) { + return ( +
+

No preview available

+

+ Start your dev server and run{" "} + + deco link + {" "} + to see a live preview +

+
+ ); + } + + const previewUrl = path !== "/" ? `${url}${path}` : url; + + return ( +
+