mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
c097c4a6da
25
CHANGES.md
25
CHANGES.md
@ -52,6 +52,31 @@
|
||||
- Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627)
|
||||
- Fix title on shared button [Taiga #13730](https://tree.taiga.io/project/penpot/issue/13730)
|
||||
|
||||
|
||||
## 2.14.1
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Add automatic retry with backoff for idempotent RPC requests on network failures [Github #8792](https://github.com/penpot/penpot/pull/8792)
|
||||
- Add scroll and zoom throttling to one state update per animation frame [Github #8812](https://github.com/penpot/penpot/pull/8812)
|
||||
- Improve error handling and exception formatting [Github #8757](https://github.com/penpot/penpot/pull/8757)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix crash in apply-text-modifier with nil selrect or modifier [Github #8762](https://github.com/penpot/penpot/pull/8762)
|
||||
- Fix incorrect attrs references on generate-sync-shape [Github #8776](https://github.com/penpot/penpot/pull/8776)
|
||||
- Fix regression on subpath support [Github #8793](https://github.com/penpot/penpot/pull/8793)
|
||||
- Improve error reporting on request parsing failures [Github #8805](https://github.com/penpot/penpot/pull/8805)
|
||||
- Fix fetch abort errors escaping the unhandled exception handler [Github #8801](https://github.com/penpot/penpot/pull/8801)
|
||||
- Fix nil deref on missing bounds in layout modifier propagation [Github #8735](https://github.com/penpot/penpot/pull/8735)
|
||||
- Fix TypeError when token error map lacks :error/fn key [Github #8767](https://github.com/penpot/penpot/pull/8767)
|
||||
- Fix dissoc error when detaching stroke color from library [Github #8738](https://github.com/penpot/penpot/pull/8738)
|
||||
- Fix crash when pasting image into text editor
|
||||
- Fix null text crash on paste in text editor
|
||||
- Ensure path content is always PathData when saving
|
||||
- Fix error when get-parent-with-data encounters non-Element nodes
|
||||
|
||||
|
||||
## 2.14.0
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
[app.srepl.main :as srepl]
|
||||
[app.storage :as-alias sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.template :as tmpl]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]
|
||||
@ -71,8 +70,7 @@
|
||||
|
||||
(defn- get-resolved-file
|
||||
[cfg file-id]
|
||||
(some-> (bfc/get-file cfg file-id :migrate? false)
|
||||
(update :data blob/encode)))
|
||||
(bfc/get-file cfg file-id :migrate? false :decode? false))
|
||||
|
||||
(defn prepare-download
|
||||
[file filename]
|
||||
|
||||
@ -90,13 +90,22 @@
|
||||
(Clock/fixed ^Instant (inst instant)
|
||||
^ZoneId (ZoneId/of "Z"))))
|
||||
|
||||
|
||||
|
||||
(defn now
|
||||
[]
|
||||
#?(:clj (Instant/now *clock*)
|
||||
:cljs (new js/Date)))
|
||||
|
||||
#?(:clj
|
||||
(defn tick-millis-clock
|
||||
"Alternate clock with a resolution of milliseconds instead of the default nanoseconds of the Java clock.
|
||||
This may be useful if the instant is going to be serialized to DB with fressian (that does not have
|
||||
resolution enough to store all precission) and need to compare the deserialized value for equality.
|
||||
|
||||
You can replace the global clock (for example in unit tests) with
|
||||
(alter-var-root #'ct/*clock* (constantly (ct/tick-millis-clock)))"
|
||||
[]
|
||||
(Clock/tickMillis (ZoneId/of "Z"))))
|
||||
|
||||
;; --- DURATION
|
||||
|
||||
(defn- resolve-temporal-unit
|
||||
|
||||
@ -254,17 +254,19 @@
|
||||
(update-token- [this token-id f]
|
||||
(assert (uuid? token-id) "expected uuid for `token-id`")
|
||||
(if-let [token (get-token- this token-id)]
|
||||
(let [token' (-> (make-token (f token))
|
||||
(assoc :modified-at (ct/now)))]
|
||||
(TokenSet. id
|
||||
name
|
||||
description
|
||||
(ct/now)
|
||||
(if (= (:name token) (:name token'))
|
||||
(assoc tokens (:name token') token')
|
||||
(-> tokens
|
||||
(d/oassoc-before (:name token) (:name token') token')
|
||||
(dissoc (:name token))))))
|
||||
(let [token' (f token)]
|
||||
(if (not= token token')
|
||||
(let [token' (assoc token' :modified-at (ct/now))]
|
||||
(TokenSet. id
|
||||
name
|
||||
description
|
||||
(ct/now)
|
||||
(if (= (:name token) (:name token'))
|
||||
(assoc tokens (:name token') token')
|
||||
(-> tokens
|
||||
(d/oassoc-before (:name token) (:name token') token')
|
||||
(dissoc (:name token))))))
|
||||
this))
|
||||
this))
|
||||
|
||||
(delete-token- [this token-id]
|
||||
@ -315,6 +317,35 @@
|
||||
(-clj->js [this]
|
||||
(clj->js (datafy this)))))
|
||||
|
||||
(def ^:private set-prefix "S-")
|
||||
|
||||
(def ^:private set-group-prefix "G-")
|
||||
|
||||
(def ^:private set-separator "/")
|
||||
|
||||
(defn get-set-path
|
||||
[token-set]
|
||||
(cpn/split-path (get-name token-set) :separator set-separator))
|
||||
|
||||
(defn split-set-name
|
||||
[name]
|
||||
(cpn/split-path name :separator set-separator))
|
||||
|
||||
(defn join-set-path [path]
|
||||
(cpn/join-path path :separator set-separator :with-spaces? false))
|
||||
|
||||
(defn normalize-set-name
|
||||
"Normalize a set name (ensure that there are no extra spaces, like ' group / set' -> 'group/set').
|
||||
|
||||
If `relative-to` is provided, the normalized name will preserve the same group prefix as reference name."
|
||||
([name]
|
||||
(-> (split-set-name (str name))
|
||||
(cpn/join-path :separator set-separator :with-spaces? false)))
|
||||
([name relative-to]
|
||||
(-> (concat (butlast (split-set-name relative-to))
|
||||
(split-set-name (str name)))
|
||||
(cpn/join-path :separator set-separator :with-spaces? false))))
|
||||
|
||||
(defn token-set?
|
||||
[o]
|
||||
(instance? TokenSet o))
|
||||
@ -369,6 +400,7 @@
|
||||
(def check-token-set
|
||||
(sm/check-fn schema:token-set :hint "expected valid token set"))
|
||||
|
||||
|
||||
(defn map->token-set
|
||||
[& {:as attrs}]
|
||||
(TokenSet. (:id attrs)
|
||||
@ -384,38 +416,10 @@
|
||||
(update :modified-at #(or % (ct/now)))
|
||||
(update :tokens #(into (d/ordered-map) %))
|
||||
(update :description d/nilv "")
|
||||
(update :name normalize-set-name)
|
||||
(check-token-set-attrs)
|
||||
(map->token-set)))
|
||||
|
||||
(def ^:private set-prefix "S-")
|
||||
|
||||
(def ^:private set-group-prefix "G-")
|
||||
|
||||
(def ^:private set-separator "/")
|
||||
|
||||
(defn get-set-path
|
||||
[token-set]
|
||||
(cpn/split-path (get-name token-set) :separator set-separator))
|
||||
|
||||
(defn split-set-name
|
||||
[name]
|
||||
(cpn/split-path name :separator set-separator))
|
||||
|
||||
(defn join-set-path [path]
|
||||
(cpn/join-path path :separator set-separator :with-spaces? false))
|
||||
|
||||
(defn normalize-set-name
|
||||
"Normalize a set name (ensure that there are no extra spaces, like ' group / set' -> 'group/set').
|
||||
|
||||
If `relative-to` is provided, the normalized name will preserve the same group prefix as reference name."
|
||||
([name]
|
||||
(-> (split-set-name name)
|
||||
(cpn/join-path :separator set-separator :with-spaces? false)))
|
||||
([name relative-to]
|
||||
(-> (concat (butlast (split-set-name relative-to))
|
||||
(split-set-name name))
|
||||
(cpn/join-path :separator set-separator :with-spaces? false))))
|
||||
|
||||
(defn normalized-set-name?
|
||||
"Check if a set name is normalized (no extra spaces)."
|
||||
[name]
|
||||
|
||||
@ -10,20 +10,26 @@
|
||||
[app.common.types.token :as cto]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest test-valid-token-name-schema
|
||||
(t/deftest test-valid-token-name
|
||||
;; Allow regular namespace token names
|
||||
(t/is (true? (sm/validate cto/schema:token-name "Foo")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "foo")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "FOO")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "Foo.Bar.Baz")))
|
||||
;; Disallow trailing tokens
|
||||
;; Allow $ inside or at the end of the name, but not at the beginning
|
||||
(t/is (true? (sm/validate cto/schema:token-name "Foo$Bar$Baz")))
|
||||
(t/is (true? (sm/validate cto/schema:token-name "Foo$Bar$Baz$")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "$Foo$Bar$Baz")))
|
||||
;; Disallow starting and trailing dots
|
||||
(t/is (false? (sm/validate cto/schema:token-name "....Foo.Bar.Baz")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
|
||||
;; Disallow multiple separator dots
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
|
||||
;; Disallow any special characters
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey😈Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey%Foo.Bar"))))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "HeyÅFoo.Bar")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey%Foo.Bar")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Hey / Foo/Bar"))))
|
||||
|
||||
|
||||
(t/deftest token-value-with-refs
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
#?(:clj [app.common.test-helpers.tokens :as tht])
|
||||
#?(:clj [clojure.datafy :refer [datafy]])
|
||||
[app.common.data :as d]
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as tr]
|
||||
@ -2034,3 +2033,31 @@
|
||||
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (ctob/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
|
||||
(t/is (false? (ctob/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest token-set-encode-decode-roundtrip-with-invalid-set-name
|
||||
(binding [ct/*clock* (ct/tick-millis-clock)]
|
||||
(let [tokens-lib
|
||||
(-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set
|
||||
(ctob/map->token-set
|
||||
{:id (thi/new-id! :test-token-set)
|
||||
:name "foo / bar"
|
||||
:modified-at (ct/now)
|
||||
:description ""}))
|
||||
(ctob/add-token
|
||||
(thi/id :test-token-set)
|
||||
(ctob/make-token :name "test-token-1"
|
||||
:type :boolean
|
||||
:value true)))
|
||||
|
||||
encoded-tokens-lib
|
||||
(fres/encode tokens-lib)
|
||||
|
||||
decoded-tokens-lib
|
||||
(fres/decode encoded-tokens-lib)]
|
||||
|
||||
(let [tset-a (ctob/get-set tokens-lib (thi/id :test-token-set))
|
||||
tset-b (ctob/get-set decoded-tokens-lib (thi/id :test-token-set))]
|
||||
(t/is (= (ctob/get-name tset-a) "foo / bar"))
|
||||
(t/is (= (ctob/get-name tset-b) "foo/bar")))))))
|
||||
|
||||
@ -8,7 +8,7 @@ localhost:3449 {
|
||||
header -Strict-Transport-Security
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
:3450 {
|
||||
# For subpath test
|
||||
# handle_path /penpot/* {
|
||||
# reverse_proxy localhost:4449
|
||||
|
||||
@ -401,6 +401,20 @@ PENPOT_FLAGS: [...] enable-air-gapped-conf
|
||||
When Penpot starts, it will leave out the Nginx configuration related to external requests. This means that,
|
||||
with this flag enabled, the Penpot configuration will disable as well the libraries and templates dashboard and the use of Google fonts.
|
||||
|
||||
## High availability
|
||||
|
||||
The mechanisms for installing Penpot in HA depend largely on how each infrastructure is managed.
|
||||
In this section, we mention the key factors to consider when replicating a Penpot installation:
|
||||
|
||||
The components that can be replicated are the `frontend`, the `backend`, and the `exporter`.
|
||||
Replication management depends on the infrastructure, whether it's a load balancer or a Kubernetes deployment with HPA.
|
||||
|
||||
In a high-availability (HA) scenario, managing the state outside of replicas is crucial. This affects the following components:
|
||||
|
||||
- Database: Penpot typically operates with a single database instance. This database can also have a replica in case the primary instance fails.
|
||||
- Valkey: Penpot only needs one Valkey instance to function correctly. Due to the nature of the data it manages, replication isn't even essential.
|
||||
- User media storage: This should not be configured with local storage but rather with centralized storage, such as Kubernetes PVC or S3.
|
||||
|
||||
## Backend
|
||||
|
||||
This section enumerates the backend only configuration variables.
|
||||
|
||||
@ -3,9 +3,18 @@ title: 1.1 Recommended settings
|
||||
desc: Learn recommended self-hosting settings, Docker & Kubernetes installs, configuration, and troubleshooting tips in Penpot's technical guide.
|
||||
---
|
||||
|
||||
# Recommended storage
|
||||
# Recommended settings
|
||||
|
||||
Disk requirements depend on your usage, with the primary factors being database storage and user-uploaded files.
|
||||
<p class="advice">
|
||||
These recommended settings do not cover specific <a href="/technical-guide/configuration/#high-availability">high-availability setups</a>, which will depend on your particular best practices.
|
||||
Also, for air-gapped environments, please make sure to read this section of <a href="/technical-guide/configuration/#air-gapped-environments">the guide</a>.
|
||||
</p>
|
||||
|
||||
Regarding **hardware requirements**, these will largely depend on the purpose of the installation. It's important to know that Penpot makes extensive use of the browser, so you need to ensure you have powerful end-user computers. To get the most out of Penpot, we recommend using the latest stable version of Chrome.
|
||||
|
||||
Regarding the **server**, 4 CPUs and 16GB of RAM are sufficient to support thousands of users. So you can also be conservative when allocating resources to the instance.
|
||||
|
||||
**Disk requirements** depend on your usage, with the primary factors being database storage and user-uploaded files.
|
||||
|
||||
As a rule of thumb, start with a **minimum** database size of **50GB** to **100GB** with elastic sizing capability — this configuration should adequately support up to 10 editors. For environments with **more than 10 users**, we recommend adding approximately **5GB** of capacity per additional editor.
|
||||
|
||||
|
||||
@ -85,16 +85,16 @@ On Windows, use the Git Bash terminal to ensure compatibility with the provided
|
||||
Clone the Penpot repository, using the proper branch depending on the
|
||||
version of Penpot you want to use the MCP server with.
|
||||
|
||||
* For the current Penpot release 2.14, use the `mcp-prod-2.14.0` branch:
|
||||
* For the current Penpot release 2.14, use the `mcp-prod-2.14.1` branch:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/penpot/penpot.git --branch mcp-prod-2.14.0 --depth 1
|
||||
git clone https://github.com/penpot/penpot.git --branch mcp-prod-2.14.1 --depth 1
|
||||
```
|
||||
|
||||
* For the latest development version of Penpot (including the MCP beta-test), use the `develop` branch:
|
||||
* For the MCP beta-test, use the `staging` branch:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/penpot/penpot.git --branch develop --depth 1
|
||||
git clone https://github.com/penpot/penpot.git --branch staging --depth 1
|
||||
```
|
||||
|
||||
Then change into the `mcp` directory:
|
||||
@ -302,5 +302,11 @@ you may set the following environment variables to configure the two servers
|
||||
* The [contribution guidelines for Penpot](../CONTRIBUTING.md) apply
|
||||
* Auto-formatting: Use `pnpm run fmt`
|
||||
* Generating API type data: See [types-generator/README.md](types-generator/README.md)
|
||||
* Packaging and publishing:
|
||||
- Create npm package: `bash scripts/pack` (sets version and then calls `npm pack`)
|
||||
* Versioning: Use `bash scripts/set-version` to set the version for the MCP package (in `package.json`).
|
||||
- Ensure that at least the major, minor and patch components of the version are always up-to-date.
|
||||
- The MCP plugin assumes that a mismatch between the MCP version and the Penpot version (as returned by the API)
|
||||
indicates incompatibility, resulting in the display of a warning message in the plugin UI.
|
||||
* Packaging and publishing:
|
||||
1. Ensure release version is set correctly in package.json (call `bash scripts/set-version` to update it automatically)
|
||||
2. Create npm package: `bash scripts/pack` (creates `penpot-mcp-<version>.tgz` for publishing)
|
||||
3. Publish to npm: `npm publish penpot-mcp-<version>.tgz --access public`
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
@ -9,6 +10,14 @@ function run(command) {
|
||||
execSync(command, { cwd: root, stdio: "inherit" });
|
||||
}
|
||||
|
||||
// pnpm-lock.yaml is hard-excluded by npm pack; it is shipped as pnpm-lock.dist.yaml
|
||||
// and restored here before bootstrap runs.
|
||||
const distLock = path.join(root, "pnpm-lock.dist.yaml");
|
||||
const lock = path.join(root, "pnpm-lock.yaml");
|
||||
if (fs.existsSync(distLock)) {
|
||||
fs.copyFileSync(distLock, lock);
|
||||
}
|
||||
|
||||
try {
|
||||
run("corepack pnpm run bootstrap");
|
||||
} catch (error) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@penpot/mcp",
|
||||
"version": "1.0.0",
|
||||
"version": "2.15.0-rc.1.153",
|
||||
"description": "MCP server for Penpot integration",
|
||||
"bin": {
|
||||
"penpot-mcp": "./bin/mcp-local.js"
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="plugin-container">
|
||||
<div id="version-warning" class="version-warning" hidden>
|
||||
<span id="version-warning-text" class="body-s"></span>
|
||||
</div>
|
||||
|
||||
<div id="connection-status" class="status-pill" data-status="idle">
|
||||
<span class="status-dot"></span>
|
||||
<span id="status-text" class="body-s">Not connected</span>
|
||||
|
||||
@ -420,6 +420,11 @@ export class PenpotUtils {
|
||||
* - For mode="fill", it will be whatever format the fill image is stored in.
|
||||
*/
|
||||
public static async exportImage(shape: Shape, mode: "shape" | "fill", asSVG: boolean): Promise<Uint8Array> {
|
||||
// Updates are asynchronous in Penpot, so wait a tick to ensure any pending updates are applied before export.
|
||||
// The constant wait time is a temporary workardound until a better solution for penpot/penpot-mcp#27
|
||||
// is implemented.
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
// Perform export
|
||||
switch (mode) {
|
||||
case "shape":
|
||||
return shape.export({ type: asSVG ? "svg" : "png" });
|
||||
|
||||
9
mcp/packages/plugin/src/index.d.ts
vendored
9
mcp/packages/plugin/src/index.d.ts
vendored
@ -1,3 +1,12 @@
|
||||
import "@penpot/plugin-types";
|
||||
|
||||
declare module "@penpot/plugin-types" {
|
||||
interface Penpot {
|
||||
/** The Penpot application version string. */
|
||||
version: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface McpOptions {
|
||||
getToken(): string;
|
||||
getServerUrl(): string;
|
||||
|
||||
@ -14,6 +14,8 @@ const executedCodeEl = document.getElementById("executed-code") as HTMLTextAreaE
|
||||
const copyCodeBtn = document.getElementById("copy-code-btn") as HTMLButtonElement;
|
||||
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;
|
||||
const disconnectBtn = document.getElementById("disconnect-btn") as HTMLButtonElement;
|
||||
const versionWarningEl = document.getElementById("version-warning") as HTMLElement;
|
||||
const versionWarningTextEl = document.getElementById("version-warning-text") as HTMLElement;
|
||||
|
||||
/**
|
||||
* Updates the status pill and button visibility based on connection state.
|
||||
@ -177,6 +179,15 @@ window.addEventListener("message", (event) => {
|
||||
if (event.data.type === "start-server") {
|
||||
connectToMcpServer(event.data.url, event.data.token);
|
||||
}
|
||||
if (event.data.type === "version-mismatch") {
|
||||
if (versionWarningEl && versionWarningTextEl) {
|
||||
versionWarningTextEl.innerHTML =
|
||||
`<b>Version mismatch detected</b>: This version of the MCP server is intended for Penpot ` +
|
||||
`${event.data.mcpVersion} while the current version is ${event.data.penpotVersion}. ` +
|
||||
`Executions may not work or produce suboptimal results.`;
|
||||
versionWarningEl.hidden = false;
|
||||
}
|
||||
}
|
||||
if (event.data.type === "stop-server") {
|
||||
ws?.close();
|
||||
} else if (event.data.source === "penpot") {
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
|
||||
import { Task, TaskHandler } from "./TaskHandler";
|
||||
|
||||
/**
|
||||
* Extracts the major.minor.patch prefix from a version string.
|
||||
*
|
||||
* @param version - a version string starting with major.minor.patch
|
||||
* @returns the major.minor.patch prefix, or the original string if it does not match
|
||||
*/
|
||||
function extractVersionPrefix(version: string): string {
|
||||
const match = version.match(/^(\d+\.\d+\.\d+)/);
|
||||
return match ? match[1] : version;
|
||||
}
|
||||
|
||||
mcp?.setMcpStatus("connecting");
|
||||
|
||||
/**
|
||||
@ -15,18 +26,33 @@ penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`, {
|
||||
hidden: !!mcp,
|
||||
} as any);
|
||||
|
||||
// Handle messages
|
||||
// Register message handlers
|
||||
penpot.ui.onMessage<string | { id: string; type?: string; status?: string; task: string; params: any }>((message) => {
|
||||
// Handle plugin task requests
|
||||
if (mcp && typeof message === "object" && message.type === "ui-initialized") {
|
||||
penpot.ui.sendMessage({
|
||||
type: "start-server",
|
||||
url: mcp?.getServerUrl(),
|
||||
token: mcp?.getToken(),
|
||||
});
|
||||
if (typeof message === "object" && message.type === "ui-initialized") {
|
||||
// Check Penpot version compatibility
|
||||
const penpotVersionPrefix = penpot.version ? extractVersionPrefix(penpot.version) : "<2.15"; // pre-2.15 versions don't have version info
|
||||
const mcpVersionPrefix = extractVersionPrefix(PENPOT_MCP_VERSION);
|
||||
console.log(`Penpot version: ${penpotVersionPrefix}, MCP version: ${mcpVersionPrefix}`);
|
||||
const isLocalPenpotVersion = penpotVersionPrefix == "0.0.0";
|
||||
if (penpotVersionPrefix !== mcpVersionPrefix && !isLocalPenpotVersion) {
|
||||
penpot.ui.sendMessage({
|
||||
type: "version-mismatch",
|
||||
mcpVersion: mcpVersionPrefix,
|
||||
penpotVersion: penpotVersionPrefix,
|
||||
});
|
||||
}
|
||||
// Initiate connection to remote MCP server (if enabled)
|
||||
if (mcp) {
|
||||
penpot.ui.sendMessage({
|
||||
type: "start-server",
|
||||
url: mcp?.getServerUrl(),
|
||||
token: mcp?.getToken(),
|
||||
});
|
||||
}
|
||||
} else if (typeof message === "object" && message.type === "update-connection-status") {
|
||||
mcp?.setMcpStatus(message.status || "unknown");
|
||||
} else if (typeof message === "object" && message.task && message.id) {
|
||||
// Handle plugin tasks submitted by the MCP server
|
||||
handlePluginTaskRequest(message).catch((error) => {
|
||||
console.error("Error in handlePluginTaskRequest:", error);
|
||||
});
|
||||
|
||||
@ -169,6 +169,18 @@ details[open] > .collapsible-header .collapsible-arrow {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* ── Version warning ─────────────────────────────────────────────── */
|
||||
|
||||
.version-warning {
|
||||
align-items: flex-start;
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border-radius: var(--spacing-8);
|
||||
border: 1px solid var(--warning-500, #f59e0b);
|
||||
color: var(--warning-500, #f59e0b);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── Action buttons ──────────────────────────────────────────────── */
|
||||
|
||||
#connect-btn,
|
||||
|
||||
1
mcp/packages/plugin/src/vite-env.d.ts
vendored
1
mcp/packages/plugin/src/vite-env.d.ts
vendored
@ -2,3 +2,4 @@
|
||||
|
||||
declare const IS_MULTI_USER_MODE: boolean;
|
||||
declare const PENPOT_MCP_WEBSOCKET_URL: string;
|
||||
declare const PENPOT_MCP_VERSION: string;
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import { defineConfig } from "vite";
|
||||
import livePreview from "vite-live-preview";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const rootPkg = require("../../package.json");
|
||||
|
||||
let WS_URI = process.env.WS_URI || "http://localhost:4402";
|
||||
let SERVER_HOST = process.env.PENPOT_MCP_PLUGIN_SERVER_HOST ?? "localhost";
|
||||
let MCP_VERSION = JSON.stringify(rootPkg.version);
|
||||
|
||||
console.log("Will define PENPOT_MCP_WEBSOCKET_URL as:", JSON.stringify(WS_URI));
|
||||
console.log("PENPOT_MCP_WEBSOCKET_URL:", JSON.stringify(WS_URI));
|
||||
console.log("PENPOT_MCP_VERSION:", MCP_VERSION);
|
||||
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
@ -37,5 +43,6 @@ export default defineConfig({
|
||||
},
|
||||
define: {
|
||||
PENPOT_MCP_WEBSOCKET_URL: JSON.stringify(WS_URI),
|
||||
PENPOT_MCP_VERSION: MCP_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
1
mcp/packages/server/.gitignore
vendored
1
mcp/packages/server/.gitignore
vendored
@ -0,0 +1 @@
|
||||
/pnpm-lock.yaml
|
||||
2840
mcp/packages/server/pnpm-lock.yaml
generated
2840
mcp/packages/server/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,7 @@ pnpm -r --filter "mcp-server" run build;
|
||||
rsync -avr packages/server/dist/ ./dist/;
|
||||
|
||||
cp packages/server/package.json ./dist/;
|
||||
cp packages/server/pnpm-lock.yaml ./dist/;
|
||||
cp pnpm-lock.yaml ./dist/;
|
||||
|
||||
touch ./dist/pnpm-workspace.yaml;
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Sets the version from Git tags, then produces the npm tarball.
|
||||
# Must be invoked directly (not via npm/pnpm) so that the version
|
||||
# is written to package.json before npm reads it.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
bash ./scripts/set-version
|
||||
# pnpm-lock.yaml is hard-excluded by npm, but we need it; ship it under a neutral name
|
||||
cp pnpm-lock.yaml pnpm-lock.dist.yaml
|
||||
trap 'rm -f pnpm-lock.dist.yaml' EXIT
|
||||
|
||||
npm pack
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user