Skip to content

Gui/site builder#2455

Open
vibegui wants to merge 26 commits intomainfrom
gui/site-builder
Open

Gui/site builder#2455
vibegui wants to merge 26 commits intomainfrom
gui/site-builder

Conversation

@vibegui
Copy link
Contributor

@vibegui vibegui commented Feb 15, 2026

Summary by cubic

Ship the new Site Editor plugin and SITE_BINDING to let teams compose pages from typed React sections, scan code for JSON Schemas, live‑preview changes, and manage branches/history. Adds a starter template and routing/SDK updates to support local filesystem projects and per‑plugin routes.

  • New Features

    • Define SITE_BINDING (file ops) with optional branch and history tools; alias-aware binder matching.
    • Add mesh-plugin-site-editor (client + server):
      • Client: Pages/Sections/Loaders views, Page Composer with live iframe preview, block/loader pickers, undo/redo, branch switcher, publish bar, history and diff.
      • Server: Page CRUD, block/loader scan/list/get/register, branch create/list/merge/delete, file history/read-at; MCP-aware site proxy.
    • Local-first setup: “Choose local folder” in binding selector creates filesystem MCP; preview panel stores tunnel/local URL; guided empty state.
    • Router: per-plugin static routes and pathless layout IDs to prevent collisions; object-storage updated accordingly.
    • Starter template: React 19 + Vite + Tailwind with typed sections, loader, and .deco scaffolding.
  • Migration

    • Connect a site: open Site Editor → Choose local folder → pick your project.
    • Start your dev server and set the Preview URL (or run “deco link” and paste the tunnel URL).
    • Optional: run block/loader scans or use the starter template to bootstrap .deco files.

Written for commit e36e343. Summary will update on new commits.

vibegui and others added 26 commits February 14, 2026 09:18
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- Add ServerPlugin with empty tools array (tools added in plan 01-03)
- Add ClientPlugin with SITE_BINDING, sidebar groups (Pages, Sections, Loaders)
- Create plugin router with 4 routes (/, /pages/$pageId, /sections, /loaders)
- Add stub page components, header, and empty state
- Register server plugin in apps/mesh/src/server-plugins.ts
- Register client plugin in apps/mesh/src/web/plugins.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CMS_PAGE_LIST: lists pages from .deco/pages/ via LIST_FILES + READ_FILE
- CMS_PAGE_GET: reads single page by ID from .deco/pages/{id}.json
- CMS_PAGE_CREATE: creates page with nanoid ID, writes via PUT_FILE
- CMS_PAGE_UPDATE: reads existing page, merges fields, writes back
- CMS_PAGE_DELETE: tries DELETE_FILE, falls back to tombstone JSON
- All tools use try/finally with proxy.close?.() to prevent leaks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- pages-list.tsx: table view with create dialog, delete action, navigation
- page-editor.tsx: form for title/path with save, breadcrumb, metadata display
- page-api.ts: client-side CRUD helpers using SITE_BINDING tools directly
- query-keys.ts: structured React Query keys for page caching
- router.ts: updated /pages/$pageId route to point to page-editor component

Deviation: Client calls SITE_BINDING tools (READ_FILE, PUT_FILE, LIST_FILES)
directly via page-api helpers instead of CMS_PAGE_* server tools, because
the plugin toolCaller is connected to the site MCP (not the SELF MCP where
server tools live). Server tools still exist for AI agent access via SELF MCP.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ts-morph and ts-json-schema-generator dependencies
- Create BlockDefinition, ComponentInfo, ScanResult types in types.ts
- Implement createProjectFromMCP in extract.ts (in-memory ts-morph from MCP files)
- Implement discoverComponents in discover.ts (default + named export detection)
- Implement generateSchema in schema.ts (JSON Schema generation with $ref inlining)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add CMS_BLOCK_SCAN tool (ts-morph pipeline: extract -> discover -> schema -> write)
- Add CMS_BLOCK_LIST tool (read .deco/blocks/ summaries)
- Add CMS_BLOCK_GET tool (full block definition with schema)
- Add CMS_BLOCK_REGISTER tool (manual block registration)
- Register all 4 block tools in server/tools/index.ts (now 9 total)
- Add client block-api with listBlocks and getBlock via SITE_BINDING
- Add block query keys to query-keys.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Custom FieldTemplate with type icons, labels, required indicators
- Custom ObjectFieldTemplate with left-border indent for nested objects
- Custom ArrayFieldTemplate with add/remove controls
- TextWidget, NumberWidget, CheckboxWidget, SelectWidget using @deco/ui
- PropEditor wrapper component rendering @rjsf/core Form
- Added @rjsf/core, @rjsf/utils, @rjsf/validator-ajv8, lucide-react deps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SectionsList: block browser grouped by category with empty state
- BlockDetail: metadata display, collapsible JSON Schema, PropEditor form
- Router: added /sections/$blockId route for block detail navigation
- Block list fetches via listBlocks from block-api with React Query
- Block detail uses ref-based formData sync pattern (no useEffect)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add editor-protocol.ts with EditorMessage/SiteMessage discriminated unions
- Add use-editor-messages.ts hook with send/subscribe and source filtering
- Add BlockInstance interface to page-api.ts replacing unknown[] in Page.blocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…iew panel

- Add viewport-toggle.tsx with mobile/tablet/desktop at 375/768/1440px
- Rewrite preview-panel.tsx with postMessage protocol via useIframeBridge
- Add use-iframe-bridge.ts using useSyncExternalStore for ready state
- Add page-composer.tsx three-panel layout with section list, preview, prop editor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…cker

- SectionListSidebar with DnD reordering, selection, and delete
- BlockPicker modal with category grouping and search filter
- Added @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/modifiers, @dnd-kit/utilities deps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…D reorder

- Replace left panel placeholder with SectionListSidebar + DnD reordering via arrayMove
- Add BlockPicker modal for adding new sections from block library
- Wire onDelete, onReorder, onAddClick handlers with debounced save to git
- Invalidate query cache on successful save
- Rewrite page-editor.tsx to render PageComposer instead of metadata form

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Generic useUndoRedo<T> hook with push/undo/redo/reset/clearFuture
- Atomic state transitions via useReducer (past/present/future)
- 100-entry cap on past stack to prevent unbounded memory growth
- clearFuture action for clearing redo stack after save

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rtcuts

- Replace local page state with useUndoRedo<BlockInstance[]> hook
- All edit operations (prop change, reorder, add, delete) push snapshots
- Keyboard shortcuts: Cmd+Z undo, Cmd+Shift+Z/Cmd+Y redo
- Undo/redo toolbar buttons with disabled state when stack empty
- Preview iframe updates on undo/redo via deco:page-config postMessage
- Redo stack cleared on save (debounced and manual) to prevent divergence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add LoaderInfo, LoaderDefinition, LoaderSummary, LoaderRef types to scanner types
- Add discoverLoaders() function that scans .ts files for default-exported functions
- Extract both input Props type and unwrapped Promise return type
- Skip functions with JSX return types (those are components)
- Support zero-parameter loaders (propsTypeName = null)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add CMS_LOADER_SCAN tool: discovers loaders, generates input/output schemas, writes to .deco/loaders/
- Add CMS_LOADER_LIST tool: lists loader definitions with summaries
- Add CMS_LOADER_GET tool: reads full loader definition by ID
- Register all three tools in server/tools/index.ts
- Add client loader-api.ts with listLoaders and getLoader helpers via SITE_BINDING
- Add loaders namespace to query-keys.ts with all/detail keys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace loaders-list stub with full categorized list using loader-api
- Create loader-detail with metadata, output schema, and PropEditor for input params
- Add /loaders/$loaderId route to plugin router

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add LoaderRef type and isLoaderRef guard to page-api
- Create loader-picker modal with category grouping and search filter
- Integrate loader binding into page-composer prop editing panel
- Show existing bindings with remove option, bind-to-prop links for each schema prop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…example sections

- package.json with React 19, React Router 7, Vite 7, Tailwind CSS 4
- react-router.config.ts with prerender reading .deco/pages/
- Three example sections (hero, features, footer) with typed prop interfaces
- Products loader with typed input/output
- Home route and catch-all route rendering sections from page config
- Root layout with Tailwind CSS import and cn utility
- Add CREATE_BRANCH, LIST_BRANCHES, MERGE_BRANCH, DELETE_BRANCH to SITE_BINDING (optional)
- Create 4 server tools (CMS_BRANCH_LIST/CREATE/MERGE/DELETE)
- Create client branch-api.ts with graceful degradation
- Add DRAFT_BRANCH_PREFIX constant to shared.ts
- Add branches query key group

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… loaders

- Home page config with Hero, Features, Footer block instances
- Block definitions with JSON Schema matching TypeScript prop interfaces
- Loader definition with input/output schemas
- README with quick start, project structure, and usage guide
- Create BranchSwitcher dropdown with draft creation inline form
- Create PublishBar with merge-to-main and discard actions
- Integrate both into plugin-header with lazy loading
- Use useSyncExternalStore-based branch store for cross-tree state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…/client APIs

- Add GET_FILE_HISTORY and READ_FILE_AT optional tool binders to SITE_BINDING (9 total)
- Create CMS_FILE_HISTORY and CMS_FILE_READ_AT server tools in page-history.ts
- Create client history-api.ts with getFileHistory, readFileAt, revertPage
- Add history query key group to query-keys.ts
- Revert implemented as read-old-content + PUT_FILE (non-destructive)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

- Create PageHistory component with vertical timeline, relative timestamps, inline revert confirmation
- Create PageDiff component with structured property-level comparison (scalar fields + block changes)
- Add Clock icon button to page composer toolbar toggling history panel in right sidebar
- History panel shows author, message, view-diff, and one-click revert per version
- Diff view uses green/red backgrounds for added/removed blocks and prop changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nnect

- Per-plugin static routes to fix collision between site-editor and object-storage
- Root directory discovery via list_allowed_directories for correct path resolution
- Plugin router strips ID-only layout route prefix from URLs
- Preview panel with inline URL input form to connect dev server
- Preview URL persisted in connection metadata via COLLECTION_CONNECTIONS_UPDATE

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 2.99.1-alpha.1
🎉 Patch 2.99.1
❤️ Minor 2.100.0
🚀 Major 3.0.0

Current version: 2.99.0

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

34 issues found across 102 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/mesh-plugin-site-editor/client/components/pages-list.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/pages-list.tsx:40">
P2: `toLocaleDateString` only formats the date portion, so the hour/minute options won’t be reflected. Use `toLocaleString` if you want the time included for “Last Updated.”</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/client/components/pages-list.tsx:228">
P2: Avoid nesting a `<Button>` inside the row `<button>`—nested buttons are invalid HTML and can break accessibility/interaction. Use a non-button container for the row (e.g., a `<div role="button">`) or render the action as a non-button element.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/loaders-list.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/loaders-list.tsx:100">
P2: The “Scan Codebase” button is non-functional (only logs to console), which is misleading for users. Until the scan action is wired, disable the button or hook it to the real scan handler.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/branch-api.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/branch-api.ts:30">
P2: Catching all errors and returning null hides real failures (e.g., network/auth errors) and makes them indistinguishable from “tool not supported.” Consider only returning null for the unsupported-tool error and rethrowing others.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/rjsf/widgets.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/rjsf/widgets.tsx:109">
P2: SelectWidget always passes a string to RJSF. For numeric/boolean enum options this changes the value type (e.g., 1 → "1"), which can break schema validation. Map the selected string back to the original enum option value before calling onChange.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/branch-merge.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/branch-merge.ts:42">
P2: The schema description says the target defaults to "main", but the handler passes `target` through as `undefined`. This can lead to inconsistent behavior if the MCP tool doesn’t apply its own default. Apply the default in the handler (or in the schema) to match the documented behavior.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/block-picker.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/block-picker.tsx:77">
P2: New blocks are always created with empty props because the picker passes `{}` instead of the block definition defaults. This skips any default props defined in the block schema and can lead to missing required values on insert.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/publish-bar.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/publish-bar.tsx:49">
P2: The success flash is unreachable after publishing because the component returns null as soon as currentBranch becomes "main". This prevents the "Published successfully" banner from ever displaying.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/page-delete.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/page-delete.ts:22">
P1: Validate/sanitize pageId before building the file path; as written, arbitrary path segments (e.g., "../") can escape `.deco/pages` and delete/overwrite unintended files.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/loader-get.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/loader-get.ts:18">
P1: Validate `loaderId` to prevent path traversal before using it in a file path.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/block-list.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/block-list.ts:85">
P2: Ensure `label` is always a string before pushing to `blocks`, otherwise `localeCompare` can throw and the output schema is violated for blocks missing a label.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/branch-create.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/branch-create.ts:37">
P2: The handler advertises a default source branch of "main" but forwards `from` as-is. If `from` is omitted, the call sends `from: undefined`, so the default may not be applied. Provide an explicit default when building the tool arguments.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/use-tunnel-url.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/use-tunnel-url.ts:71">
P2: Avoid invalidating all React Query caches here; it triggers unnecessary refetches. Invalidate only the connections query (or the specific query backing `connection` metadata).</violation>
</file>

<file name="apps/mesh/src/web/layouts/plugin-layout.tsx">

<violation number="1" location="apps/mesh/src/web/layouts/plugin-layout.tsx:165">
P2: `PUT_FILE` response adapter unconditionally returns `{ success: true }` without inspecting the actual result. Consider checking `rawResult` for error indicators (e.g., `isError` field) to avoid silently masking write failures.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/page-api.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/page-api.ts:169">
P2: Prevent updating tombstoned pages; updating a deleted page will write a malformed Page because the tombstone lacks id/path/blocks. Add a deleted check after parsing and fail early (or return) instead of applying updates.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/use-editor-messages.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/use-editor-messages.ts:30">
P1: Avoid using "*" for postMessage targetOrigin; restrict it to the iframe’s expected origin to prevent data leakage if the iframe navigates to a different site.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/client/lib/use-editor-messages.ts:37">
P2: Validate `e.origin` before handling messages so a navigated or compromised iframe can’t send trusted messages from an unexpected origin.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/branch-switcher.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/branch-switcher.tsx:49">
P2: Draft detection is hardcoded to `main`, which can mislabel the published/default branch when it isn’t named `main`. Use the draft prefix (or default branch from data) to determine draft status.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/page-composer.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/page-composer.tsx:119">
P1: Side effect during render: `queueMicrotask(() => send(...))` is called in the render body. React render functions should be pure — in Strict Mode, React double-invokes render, so this will send duplicate `deco:page-config` messages to the iframe. Move this to a `useEffect` keyed on `blocks`.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/client/components/page-composer.tsx:132">
P2: `useSyncExternalStore` is misused here as an event listener registration mechanism. The snapshot always returns `null`, so `notify()` never causes re-renders — the actual work is done via refs as side effects. Use a `useEffect` instead, which is the standard pattern for DOM event listeners and is much clearer to readers.</violation>
</file>

<file name="apps/mesh/src/web/components/binding-selector.tsx">

<violation number="1" location="apps/mesh/src/web/components/binding-selector.tsx:201">
P3: Folder name extraction only splits on `/`, so Windows paths with backslashes produce incorrect titles. Split on both separators to derive the final folder name reliably.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/scanner/schema.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/scanner/schema.ts:115">
P2: Inlining $ref discards sibling keywords (description/title/default), so metadata on a $ref node is lost after resolution. Merge the resolved definition with the original node (excluding $ref) to preserve overrides.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/use-iframe-bridge.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/use-iframe-bridge.ts:56">
P2: `postMessage(msg, "*")` sends editor data (page config, block selections) to any origin. If the iframe navigates to an untrusted page, sensitive data could be leaked. Consider passing the expected site origin instead of `"*"`.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/client/lib/use-iframe-bridge.ts:121">
P2: `removeEventListener("load", handleIframeLoad)` will silently fail because `handleIframeLoad` is a new function reference each render. The function passed to `addEventListener` in a prior render is a different object than the one passed to `removeEventListener` here. Store the handler in a ref to guarantee the same reference is used for both add and remove.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/site-proxy.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/site-proxy.ts:78">
P1: Normalize relative paths to prevent `..` segments from escaping the allowed root directory before concatenating them.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/server/site-proxy.ts:147">
P2: Preserve error responses from the underlying MCP tool before transforming LIST_FILES output so failures aren’t silently treated as empty results.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/preview-panel.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/preview-panel.tsx:75">
P2: Validate the user-provided preview URL before persisting it; otherwise a `javascript:`/`data:` URL can be stored and executed when the iframe loads.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/loader-scan.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/loader-scan.ts:77">
P2: Handle MCP proxy creation inside the try/catch so connection failures are captured in `errors` instead of crashing the tool.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/page-get.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/page-get.ts:28">
P1: Validate/sanitize pageId before using it in the file path to prevent path traversal (e.g., only allow expected page ID format).</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/block-detail.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/block-detail.tsx:73">
P2: Avoid calling setFormData during render. Render-phase updates can cause extra render passes and warnings in concurrent rendering; sync block defaults in a useEffect instead.</violation>
</file>

<file name="packages/bindings/src/core/plugin-router.tsx">

<violation number="1" location="packages/bindings/src/core/plugin-router.tsx:41">
P2: `prependBasePath` now removes the first path segment for all absolute plugin routes, which drops legitimate segments when the path does not include an ID-only layout prefix (e.g., `"/$appName"` becomes just the base path). Guard the stripping logic so it only removes known layout IDs (e.g., segments ending in `-layout`) and otherwise preserve the original absolute path.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/loader-list.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/loader-list.ts:87">
P2: Guard against missing `label` values before sorting. As written, an undefined label from a malformed loader JSON will cause `localeCompare` to throw and the tool to fail.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/scanner/discover.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/scanner/discover.ts:32">
P2: The bare `"Element"` pattern is too broad for a substring match. It will match any return type containing the word "Element" (e.g., `HTMLElement`, `SVGElement`, `CustomElement`), causing false positives in component detection and false negatives in loader detection. The legitimate cases are already covered by `"JSX.Element"` and `"ReactElement"`.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/server/scanner/discover.ts:289">
P2: Bug: JSDoc is never extracted for arrow function components. The parent traversal stops one level too short — `fn.getParent().getParent()` reaches `VariableDeclarationList`, not `VariableStatement` (which is where JSDoc is attached in the TS AST). You need to go up one more level.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

description: "Delete a CMS page by ID.",
inputSchema: z.object({
connectionId: z.string().describe("MCP connection ID for the site"),
pageId: z.string().describe("Page ID to delete"),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Validate/sanitize pageId before building the file path; as written, arbitrary path segments (e.g., "../") can escape .deco/pages and delete/overwrite unintended files.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/tools/page-delete.ts, line 22:

<comment>Validate/sanitize pageId before building the file path; as written, arbitrary path segments (e.g., "../") can escape `.deco/pages` and delete/overwrite unintended files.</comment>

<file context>
@@ -0,0 +1,70 @@
+  description: "Delete a CMS page by ID.",
+  inputSchema: z.object({
+    connectionId: z.string().describe("MCP connection ID for the site"),
+    pageId: z.string().describe("Page ID to delete"),
+  }),
+  outputSchema: z.object({
</file context>
Fix with Cubic

"Get a single CMS loader definition by ID, including its full input and output JSON Schemas.",
inputSchema: z.object({
connectionId: z.string().describe("MCP connection ID for the site"),
loaderId: z.string().describe('Loader ID (e.g., "loaders--productList")'),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Validate loaderId to prevent path traversal before using it in a file path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/tools/loader-get.ts, line 18:

<comment>Validate `loaderId` to prevent path traversal before using it in a file path.</comment>

<file context>
@@ -0,0 +1,77 @@
+    "Get a single CMS loader definition by ID, including its full input and output JSON Schemas.",
+  inputSchema: z.object({
+    connectionId: z.string().describe("MCP connection ID for the site"),
+    loaderId: z.string().describe('Loader ID (e.g., "loaders--productList")'),
+  }),
+  outputSchema: z.object({
</file context>
Fix with Cubic

iframeRef: RefObject<HTMLIFrameElement | null>,
): UseEditorMessagesResult {
const send = (msg: EditorMessage) => {
iframeRef.current?.contentWindow?.postMessage(msg, "*");
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Avoid using "*" for postMessage targetOrigin; restrict it to the iframe’s expected origin to prevent data leakage if the iframe navigates to a different site.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/client/lib/use-editor-messages.ts, line 30:

<comment>Avoid using "*" for postMessage targetOrigin; restrict it to the iframe’s expected origin to prevent data leakage if the iframe navigates to a different site.</comment>

<file context>
@@ -0,0 +1,44 @@
+  iframeRef: RefObject<HTMLIFrameElement | null>,
+): UseEditorMessagesResult {
+  const send = (msg: EditorMessage) => {
+    iframeRef.current?.contentWindow?.postMessage(msg, "*");
+  };
+
</file context>
Fix with Cubic

const prevBlocksRef = useRef<BlockInstance[]>(blocks);
if (localPage && blocks !== prevBlocksRef.current) {
prevBlocksRef.current = blocks;
queueMicrotask(() => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Side effect during render: queueMicrotask(() => send(...)) is called in the render body. React render functions should be pure — in Strict Mode, React double-invokes render, so this will send duplicate deco:page-config messages to the iframe. Move this to a useEffect keyed on blocks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/client/components/page-composer.tsx, line 119:

<comment>Side effect during render: `queueMicrotask(() => send(...))` is called in the render body. React render functions should be pure — in Strict Mode, React double-invokes render, so this will send duplicate `deco:page-config` messages to the iframe. Move this to a `useEffect` keyed on `blocks`.</comment>

<file context>
@@ -0,0 +1,543 @@
+  const prevBlocksRef = useRef<BlockInstance[]>(blocks);
+  if (localPage && blocks !== prevBlocksRef.current) {
+    prevBlocksRef.current = blocks;
+    queueMicrotask(() => {
+      if (localPage) {
+        send({ type: "deco:page-config", page: localPage });
</file context>
Fix with Cubic

*/
function toAbsolute(rootDir: string | null, relativePath: string): string {
if (!rootDir || relativePath.startsWith("/")) return relativePath;
return `${rootDir.replace(/\/$/, "")}/${relativePath}`;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Normalize relative paths to prevent .. segments from escaping the allowed root directory before concatenating them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/site-proxy.ts, line 78:

<comment>Normalize relative paths to prevent `..` segments from escaping the allowed root directory before concatenating them.</comment>

<file context>
@@ -0,0 +1,184 @@
+ */
+function toAbsolute(rootDir: string | null, relativePath: string): string {
+  if (!rootDir || relativePath.startsWith("/")) return relativePath;
+  return `${rootDir.replace(/\/$/, "")}/${relativePath}`;
+}
+
</file context>
Fix with Cubic

Comment on lines +41 to +44
const rest = to.substring(1); // "site-editor-layout/pages/$pageId"
const slashIdx = rest.indexOf("/");
const urlPath = slashIdx >= 0 ? rest.substring(slashIdx) : "";
return `/${org}/${project}/${pluginId}${urlPath}`;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: prependBasePath now removes the first path segment for all absolute plugin routes, which drops legitimate segments when the path does not include an ID-only layout prefix (e.g., "/$appName" becomes just the base path). Guard the stripping logic so it only removes known layout IDs (e.g., segments ending in -layout) and otherwise preserve the original absolute path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bindings/src/core/plugin-router.tsx, line 41:

<comment>`prependBasePath` now removes the first path segment for all absolute plugin routes, which drops legitimate segments when the path does not include an ID-only layout prefix (e.g., `"/$appName"` becomes just the base path). Guard the stripping logic so it only removes known layout IDs (e.g., segments ending in `-layout`) and otherwise preserve the original absolute path.</comment>

<file context>
@@ -28,15 +33,36 @@ function prependBasePath(
+    // e.g., "/site-editor-layout/pages/$pageId" → "/pages/$pageId"
+    // e.g., "/site-editor-layout/" → "/"
+    // e.g., "/site-editor-layout" → ""
+    const rest = to.substring(1); // "site-editor-layout/pages/$pageId"
+    const slashIdx = rest.indexOf("/");
+    const urlPath = slashIdx >= 0 ? rest.substring(slashIdx) : "";
</file context>
Suggested change
const rest = to.substring(1); // "site-editor-layout/pages/$pageId"
const slashIdx = rest.indexOf("/");
const urlPath = slashIdx >= 0 ? rest.substring(slashIdx) : "";
return `/${org}/${project}/${pluginId}${urlPath}`;
const rest = to.substring(1); // "site-editor-layout/pages/$pageId"
const slashIdx = rest.indexOf("/");
const firstSegment = slashIdx >= 0 ? rest.substring(0, slashIdx) : rest;
const urlPath = firstSegment.endsWith("-layout")
? slashIdx >= 0
? rest.substring(slashIdx)
: ""
: `/${rest}`;
return `/${org}/${project}/${pluginId}${urlPath}`;
Fix with Cubic

loaders.push({
id: loader.id,
source: loader.source,
label: loader.label,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard against missing label values before sorting. As written, an undefined label from a malformed loader JSON will cause localeCompare to throw and the tool to fail.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/tools/loader-list.ts, line 87:

<comment>Guard against missing `label` values before sorting. As written, an undefined label from a malformed loader JSON will cause `localeCompare` to throw and the tool to fail.</comment>

<file context>
@@ -0,0 +1,105 @@
+          loaders.push({
+            id: loader.id,
+            source: loader.source,
+            label: loader.label,
+            category: loader.category ?? "Other",
+            inputParamsCount,
</file context>
Fix with Cubic

"ReactElement",
"React.FC",
"React.FunctionComponent",
"Element",
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The bare "Element" pattern is too broad for a substring match. It will match any return type containing the word "Element" (e.g., HTMLElement, SVGElement, CustomElement), causing false positives in component detection and false negatives in loader detection. The legitimate cases are already covered by "JSX.Element" and "ReactElement".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/scanner/discover.ts, line 32:

<comment>The bare `"Element"` pattern is too broad for a substring match. It will match any return type containing the word "Element" (e.g., `HTMLElement`, `SVGElement`, `CustomElement`), causing false positives in component detection and false negatives in loader detection. The legitimate cases are already covered by `"JSX.Element"` and `"ReactElement"`.</comment>

<file context>
@@ -0,0 +1,546 @@
+  "ReactElement",
+  "React.FC",
+  "React.FunctionComponent",
+  "Element",
+];
+
</file context>
Fix with Cubic

if (!parent) return "";

// Walk up to the variable statement which can hold JSDoc
const varStatement = parent.getParent();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Bug: JSDoc is never extracted for arrow function components. The parent traversal stops one level too short — fn.getParent().getParent() reaches VariableDeclarationList, not VariableStatement (which is where JSDoc is attached in the TS AST). You need to go up one more level.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/scanner/discover.ts, line 289:

<comment>Bug: JSDoc is never extracted for arrow function components. The parent traversal stops one level too short — `fn.getParent().getParent()` reaches `VariableDeclarationList`, not `VariableStatement` (which is where JSDoc is attached in the TS AST). You need to go up one more level.</comment>

<file context>
@@ -0,0 +1,546 @@
+  if (!parent) return "";
+
+  // Walk up to the variable statement which can hold JSDoc
+  const varStatement = parent.getParent();
+  if (varStatement?.isKind(SyntaxKind.VariableStatement)) {
+    const jsDocs = varStatement.getJsDocs();
</file context>
Fix with Cubic

if (!folderPath) return;

const folderName =
folderPath.split("/").filter(Boolean).pop() ?? "folder";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Folder name extraction only splits on /, so Windows paths with backslashes produce incorrect titles. Split on both separators to derive the final folder name reliably.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/binding-selector.tsx, line 201:

<comment>Folder name extraction only splits on `/`, so Windows paths with backslashes produce incorrect titles. Split on both separators to derive the final folder name reliably.</comment>

<file context>
@@ -149,8 +165,82 @@ export function BindingSelector({
+      if (!folderPath) return;
+
+      const folderName =
+        folderPath.split("/").filter(Boolean).pop() ?? "folder";
+
+      // For object storage bindings, use the local-object-storage bridge
</file context>
Suggested change
folderPath.split("/").filter(Boolean).pop() ?? "folder";
folderPath.split(/[/\\]/).filter(Boolean).pop() ?? "folder";
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant