From 7f60e3735da180c06026a71e9ea66f5debd71fe7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 16 Jul 2026 14:04:06 +0200 Subject: [PATCH 1/9] :bug: Remove hardcoded metadata URI default on webhook form (#10723) The webhook creation form used a hardcoded AWS instance metadata endpoint (http://169.254.169.254/...) as the default :uri value for new webhooks. This leaked an internal cloud credential endpoint into the UI defaults and could expose it to users. Remove the default :uri so new webhooks start with an empty URI instead of a sensitive hardcoded value. --- frontend/src/app/main/ui/dashboard/team.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 613229567c..9b5de370b1 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -1259,7 +1259,7 @@ (let [initial (mf/with-memo [] (or (some-> webhook (update :uri str)) - {:is-active false :mtype "application/json" :uri "http://169.254.169.254/latest/meta-data/iam/security-credentials/"})) + {:is-active false :mtype "application/json"})) form (fm/use-form :schema schema:webhook-form :initial initial) on-success From 26c4ec18fe6d19970bc76eb382efe053db030e8c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 20 Jul 2026 11:56:07 +0200 Subject: [PATCH 2/9] :bug: Return 400 instead of 500 when ImageMagick fails on invalid images (#10643) When ImageMagick fails to process an uploaded image (e.g., corrupted PNG with invalid IHDR data), the backend was raising :type :internal with :code :imagemagick-error, which mapped to HTTP 500. The frontend treated this as a server error and displayed the full error page. Changed exec-magick! to raise :type :validation with :code :invalid-image instead. This flows through the existing :invalid-image handler in errors.clj which returns HTTP 400. The frontend's handle-media-error and process-error now catch this code and show a notification banner. AI-assisted-by: qwen3.7-plus --- backend/src/app/media.clj | 4 ++-- backend/test/backend_tests/media_test.clj | 4 ++-- frontend/src/app/main/data/media.cljs | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index d3ff7fdff2..30527857ad 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -184,8 +184,8 @@ :env (get-imagemagick-env) :timeout 60)] (when (not= 0 (:exit result)) - (ex/raise :type :internal - :code :imagemagick-error + (ex/raise :type :validation + :code :invalid-image :hint (str "ImageMagick command failed: " (:err result)) :cmd cmd :exit (:exit result))) diff --git a/backend/test/backend_tests/media_test.clj b/backend/test/backend_tests/media_test.clj index 85b749274c..f4d6d78e81 100644 --- a/backend/test/backend_tests/media_test.clj +++ b/backend/test/backend_tests/media_test.clj @@ -67,8 +67,8 @@ (t/is false "should have thrown") (catch Exception e (let [data (ex-data e)] - ;; Could be validation or imagemagick-error depending on what magick does - (t/is (contains? #{:validation :internal} (:type data))))) + (t/is (= :validation (:type data))) + (t/is (= :invalid-image (:code data))))) (finally (fs/delete path)))))) diff --git a/frontend/src/app/main/data/media.cljs b/frontend/src/app/main/data/media.cljs index 688d4d9d5f..74eec0ac56 100644 --- a/frontend/src/app/main/data/media.cljs +++ b/frontend/src/app/main/data/media.cljs @@ -70,6 +70,9 @@ (= (:code error) :media-type-mismatch) (tr "errors.media-type-mismatch") + (= (:code error) :invalid-image) + (tr "errors.media-type-not-allowed") + :else (tr "errors.unexpected-error"))] (rx/of (ntf/error msg)))) From 766368567e72b93516ebd039c8e72fb6f7c05ec7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 20 Jul 2026 14:45:36 +0200 Subject: [PATCH 3/9] :bug: Ensure text position-data always includes :fills key (#10650) Position-data entries for text shapes could omit the :fills key when the element had no explicit fills, causing backend malli validation failures. - Use get-default-text-attrs as base in WASM calculate-position-data - Default fills when CSS --fills property is absent in DOM calc path AI-assisted-by: deepseek-v4-pro --- frontend/src/app/render_wasm/api.cljs | 2 +- frontend/src/app/util/text_svg_position.cljs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index e0273d0125..a96353dea4 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -2557,7 +2557,7 @@ (when (and element element-text) (let [text (subs element-text start-pos end-pos)] (d/patch-object - txt/default-text-attrs + (txt/get-default-text-attrs) (d/without-nils {:x x :y (+ y height) diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index 6cfaa314e2..772a7f6b8a 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.transit :as transit] + [app.common.types.text :as txt] [app.main.fonts :as fonts] [app.util.dom :as dom] [app.util.text-position-data :as tpd] @@ -105,7 +106,8 @@ :text-decoration (dm/str (get-prop styles "text-decoration")) :letter-spacing (dm/str (get-prop styles "letter-spacing")) :font-style (dm/str (get-prop styles "font-style")) - :fills (transit/decode-str (get-prop styles "--fills")) + :fills (or (transit/decode-str (get-prop styles "--fills")) + txt/default-text-fills) :text text})))] (when (some? shape-id) From 4c7863d6c85ae1456ff8ad5f92835ad7109bde93 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 20 Jul 2026 14:45:59 +0200 Subject: [PATCH 4/9] :books: Add .penpot format docs and inspector tool (#10674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :books: Add comprehensive documentation for .penpot file format (v3) Create user-facing documentation for the .penpot binfile format to help developers and power users understand and inspect the ZIP+JSON structure. - Add technical specification with complete schema reference for all JSON files (manifest, file metadata, pages, shapes, library assets, storage objects, plugin data) - Add user-friendly overview explaining the format structure, inspection methods, and version history - Update export-import-files.njk to clarify current format is not deprecated and note that v2 was never released - Add cross-links between documentation pages - Include source code references to authoritative malli schemas AI-assisted-by: qwen3.7-plus * :paperclip: Add playwright dependency to the root package.json * :books: Add browser based .penpot file inspector to docs Add a client-side, no-build inspector for .penpot (v3) files. The tool runs entirely in the browser: JSZip is loaded lazily from a CDN, the file is never uploaded. It provides a collapsible file tree, syntax- highlighted JSON viewer with search, image previews, a summary panel with file/shape/storage counts, shape summary cards with color swatches, and clickable UUID cross-references with backlinks. The inspector is added at docs/technical-guide/developer/data-model/penpot-file-inspector.njk and linked from the format spec and the user-facing format page. AI-assisted-by: minimax-m3 * :books: Use full page width and 2-column layout for inspector The inspector previously sat inside the docs site's 42rem content column, which forced a stacked layout and wasted the wide viewport. - Hide the docs page-navigation sidebar on this page via :has() - Use a 2-column CSS grid: file tree (280px, sticky) on the left, content (flex: 1) on the right - Restore the directory tree as a permanent left-rail navigation (it was removed when the picker was first introduced, then added back as a collapsible panel; a sticky rail is a better fit now that horizontal space is plentiful) - Remove the tree toggle button — the tree is always visible - Collapses to 1 column on viewports below 900px AI-assisted-by: minimax-m3 * :lipstick: Indent nested JSON values in inspector The JSON tree view in the inspector was rendering all properties at the same indent level, making it hard to see which values were nested inside objects or arrays. Root-level properties and deeply nested ones looked identical. Add padding-left and a subtle vertical guide line to .jchildren so each nesting level is visually distinct. Bump the toggle column to 1.2em and add a touch of vertical padding to .jrow for breathing room. AI-assisted-by: minimax-m3 --- .../data-model/penpot-file-format.md | 628 ++++++++ .../data-model/penpot-file-inspector.njk | 1401 +++++++++++++++++ .../export-import/export-import-files.njk | 9 +- docs/user-guide/export-import/index.njk | 8 +- .../export-import/penpot-file-format.njk | 155 ++ package.json | 6 +- pnpm-lock.yaml | 195 +-- 7 files changed, 2272 insertions(+), 130 deletions(-) create mode 100644 docs/technical-guide/developer/data-model/penpot-file-format.md create mode 100644 docs/technical-guide/developer/data-model/penpot-file-inspector.njk create mode 100644 docs/user-guide/export-import/penpot-file-format.njk diff --git a/docs/technical-guide/developer/data-model/penpot-file-format.md b/docs/technical-guide/developer/data-model/penpot-file-format.md new file mode 100644 index 0000000000..431dbffaa2 --- /dev/null +++ b/docs/technical-guide/developer/data-model/penpot-file-format.md @@ -0,0 +1,628 @@ +--- +title: 3.02.01. Penpot file format (.penpot) +desc: Complete technical specification for the .penpot file format, including structure, schemas, and inspection methods. +--- + +# Penpot File Format (.penpot) + +The `.penpot` file format is Penpot's native export format for design files. It's a ZIP archive containing JSON metadata and binary assets, designed to be open, inspectable, and efficient. + +## Overview + +The `.penpot` format (version 3) uses a ZIP container with JSON files for metadata and binary files for media assets. This approach provides several advantages: + +- **Open and inspectable**: All metadata is human-readable JSON +- **Efficient**: ZIP compression reduces file size +- **Interoperable**: Standard formats enable third-party tooling +- **Versioned**: Clear versioning system for format and data evolution + +## Version History + +| Version | Description | Status | +|---------|-------------|--------| +| v1 | Custom binary format | Deprecated | +| v2 | SQLite-based format | Never released (internal only) | +| v3 | ZIP + JSON format | **Current** | + +## File Structure + +A `.penpot` file contains the following structure: + +``` +pencil.penpot (ZIP archive) +├── manifest.json # Root metadata +├── files/ +│ ├── {file-id}.json # File metadata +│ └── {file-id}/ +│ ├── pages/ +│ │ ├── {page-id}.json # Page metadata +│ │ └── {page-id}/ +│ │ └── {shape-id}.json # Individual shapes +│ ├── media/ +│ │ └── {media-id}.json # Media references +│ ├── colors/ +│ │ └── {color-id}.json # Library colors +│ ├── components/ +│ │ └── {component-id}.json # Library components +│ ├── typographies/ +│ │ └── {typography-id}.json # Library typographies +│ ├── tokens.json # Design tokens library +│ └── thumbnails/ +│ └── {tag}/{page-id}/{frame-id}.json # Thumbnail metadata +└── objects/ + ├── {uuid}.json # Storage object metadata + └── {uuid}.{ext} # Binary media files (png, jpg, etc.) +``` + +## Manifest Specification + +The `manifest.json` file is the root of the archive and contains metadata about the export. + +### Schema + +```json +{ + "version": 1, + "type": "penpot/export-files", + "generatedBy": "penpot/2.12.0", + "refer": "penpot", + "files": [ + { + "id": "uuid", + "name": "File Name", + "features": ["feature1", "feature2"] + } + ], + "relations": [] +} +``` + +### Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `version` | integer | Yes | Format version (currently `1`) | +| `type` | string | Yes | Must be `"penpot/export-files"` | +| `generatedBy` | string | No | Penpot version that created the file | +| `refer` | string | No | Source system (typically `"penpot"`) | +| `files` | array | Yes | List of files in the archive | +| `files[].id` | UUID | Yes | File identifier | +| `files[].name` | string | Yes | File name | +| `files[].features` | array | Yes | Set of feature flags | +| `relations` | array | No | Library relationships `[file-id, library-id]` | + +### Example + +```json +{ + "type": "penpot/export-files", + "version": 1, + "generatedBy": "penpot/2.12.0-RC1-99-g40c27591f", + "refer": "penpot", + "files": [ + { + "id": "73b59a94-3ea3-8189-8007-3d36adc8c3e3", + "name": "Pencil | Penpot Design System", + "features": [ + "fdata/path-data", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "components/v2", + "fdata/shape-data-type" + ] + } + ], + "relations": [] +} +``` + +## File Metadata + +Each file has a JSON file at `files/{file-id}.json` containing the file's metadata. + +### Schema + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID | Yes | File identifier | +| `name` | string | Yes | File name | +| `revn` | integer | Yes | Revision number | +| `vern` | integer | No | Version number | +| `createdAt` | timestamp | Yes | Creation timestamp | +| `modifiedAt` | timestamp | Yes | Last modification timestamp | +| `deletedAt` | timestamp | No | Deletion timestamp (if soft-deleted) | +| `projectId` | UUID | No | Project identifier | +| `teamId` | UUID | No | Team identifier | +| `isShared` | boolean | No | Whether file is a shared library | +| `hasMediaTrimmed` | boolean | No | Whether media has been trimmed | +| `features` | array | Yes | Set of enabled feature flags | +| `migrations` | array | No | List of applied data migrations | +| `options` | object | No | File-level options | + +### Features + +The `features` field is a set of strings indicating which features are enabled in the file. Common features include: + +| Feature | Description | +|---------|-------------| +| `fdata/path-data` | Path data format | +| `fdata/shape-data-type` | Shape data type system | +| `design-tokens/v1` | Design tokens support | +| `variants/v1` | Component variants | +| `layout/grid` | Grid layout system | +| `components/v2` | Component system v2 | +| `plugins/runtime` | Plugin runtime support | + +### Migrations + +The `migrations` field lists all data migrations applied to the file. This ensures backward compatibility when the data model evolves. + +### Example + +```json +{ + "id": "73b59a94-3ea3-8189-8007-3d36adc8c3e3", + "name": "Pencil | Penpot Design System", + "revn": 28425, + "vern": 0, + "createdAt": "2025-12-10T10:24:18.686066Z", + "modifiedAt": "2025-12-10T12:13:49.799076Z", + "teamId": "b62e1aa4-d9a7-8147-8005-2813bed4056e", + "projectId": "f23add0e-6b77-8069-8005-41b48b93a5da", + "isShared": true, + "features": [ + "fdata/path-data", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "components/v2", + "fdata/shape-data-type" + ], + "migrations": [ + "legacy-2", + "legacy-3", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2" + ], + "options": { + "componentsV2": true + } +} +``` + +## Pages and Shapes + +### Page Structure + +Each page is stored at `files/{file-id}/pages/{page-id}.json`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID | Yes | Page identifier | +| `name` | string | Yes | Page name | +| `index` | integer | No | Page order in the file | +| `options` | object | No | Page options (guides, etc.) | +| `background` | string | No | Background color (hex) | +| `flows` | object | No | Prototype flows | +| `guides` | object | No | Ruler guides | + +### Shapes + +Individual shapes are stored at `files/{file-id}/pages/{page-id}/{shape-id}.json`. + +#### Shape Types + +Penpot supports 9 shape types: + +| Type | Description | +|------|-------------| +| `frame` | Container frame (artboard) | +| `group` | Group of shapes | +| `rect` | Rectangle | +| `circle` | Circle/Ellipse | +| `path` | Vector path | +| `text` | Text shape | +| `image` | Image | +| `bool` | Boolean operation | +| `svg-raw` | Raw SVG element | + +#### Base Shape Attributes + +All shapes share these base attributes: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID | Yes | Shape identifier | +| `name` | string | Yes | Shape name | +| `type` | string | Yes | Shape type (see above) | +| `selrect` | object | Yes | Selection rectangle `{x, y, width, height}` | +| `points` | array | Yes | Array of points `[{x, y}, ...]` | +| `transform` | array | Yes | 2D transformation matrix | +| `transformInverse` | array | Yes | Inverse transformation matrix | +| `parentId` | UUID | Yes | Parent shape identifier | +| `frameId` | UUID | Yes | Containing frame identifier | + +#### Geometry Attributes + +Shapes with geometry (frame, rect, circle, image, svg-raw, text): + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `x` | number | Yes | X position | +| `y` | number | Yes | Y position | +| `width` | number | Yes | Width | +| `height` | number | Yes | Height | + +#### Generic Attributes + +Optional attributes available on all shapes: + +| Field | Type | Description | +|-------|------|-------------| +| `fills` | array | Fill styles | +| `strokes` | array | Stroke styles | +| `opacity` | number | Opacity (0-1) | +| `blendMode` | string | Blend mode | +| `shadow` | array | Shadow effects | +| `blur` | object | Blur effect | +| `constraintsH` | string | Horizontal constraint | +| `constraintsV` | string | Vertical constraint | +| `r1`, `r2`, `r3`, `r4` | number | Border radius corners | +| `blocked` | boolean | Shape is locked | +| `hidden` | boolean | Shape is hidden | +| `collapsed` | boolean | Shape is collapsed | +| `componentId` | UUID | Component reference | +| `componentFile` | UUID | Component library file | +| `shapeRef` | UUID | Shape reference for components | +| `touched` | array | Modified component properties | +| `interactions` | array | Prototype interactions | +| `exports` | array | Export settings | +| `grids` | array | Grid configurations | +| `appliedTokens` | object | Applied design tokens | +| `pluginData` | object | Plugin-specific data | + +#### Type-Specific Attributes + +**Frame** +- `shapes`: array of child shape UUIDs +- `showContent`: boolean +- `hideInViewer`: boolean + +**Group** +- `shapes`: array of child shape UUIDs + +**Bool** +- `shapes`: array of child shape UUIDs +- `boolType`: string (`union`, `difference`, `exclude`, `intersection`) +- `content`: path data + +**Path** +- `content`: path data (SVG path commands) + +**Text** +- `content`: text content with formatting +- `positionData`: glyph position data + +**Image** +- `metadata`: object with `width`, `height`, `mtype`, `id` + +### Example Shape + +```json +{ + "id": "260aea33-4e55-808c-8007-3d4f2efe4230", + "name": "Rectangle", + "type": "rect", + "x": 100, + "y": 100, + "width": 200, + "height": 150, + "selrect": { + "x": 100, + "y": 100, + "width": 200, + "height": 150 + }, + "points": [ + {"x": 100, "y": 100}, + {"x": 300, "y": 100}, + {"x": 300, "y": 250}, + {"x": 100, "y": 250} + ], + "transform": [1, 0, 0, 1, 0, 0], + "transformInverse": [1, 0, 0, 1, 0, 0], + "parentId": "00000000-0000-0000-0000-000000000000", + "frameId": "00000000-0000-0000-0000-000000000001", + "fills": [ + { + "color": "#FF5733", + "opacity": 1 + } + ], + "r1": 8, + "r2": 8, + "r3": 8, + "r4": 8 +} +``` + +## Library Assets + +### Colors + +Stored at `files/{file-id}/colors/{color-id}.json`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID | Yes | Color identifier | +| `name` | string | Yes | Color name | +| `path` | string | No | Path in the library tree | +| `opacity` | number | No | Opacity (0-1) | +| `color` | string | Conditional | Hex color (for plain colors) | +| `gradient` | object | Conditional | Gradient definition | +| `image` | object | Conditional | Image fill definition | + +#### Plain Color Example + +```json +{ + "id": "abc123...", + "name": "Primary Blue", + "path": "Brand/Primary", + "color": "#0066CC", + "opacity": 1 +} +``` + +#### Gradient Color Example + +```json +{ + "id": "def456...", + "name": "Sunset Gradient", + "gradient": { + "type": "linear", + "startX": 0, + "startY": 0, + "endX": 1, + "endY": 1, + "stops": [ + {"color": "#FF6B6B", "offset": 0, "opacity": 1}, + {"color": "#4ECDC4", "offset": 1, "opacity": 1} + ] + } +} +``` + +### Components + +Stored at `files/{file-id}/components/{component-id}.json`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID | Yes | Component identifier | +| `name` | string | Yes | Component name | +| `path` | string | Yes | Path in the library tree | +| `mainInstanceId` | UUID | Yes | Root shape of main instance | +| `mainInstancePage` | UUID | Yes | Page containing main instance | +| `modifiedAt` | timestamp | No | Last modification | +| `objects` | object | No | Captured shapes (if deleted) | + +### Typographies + +Stored at `files/{file-id}/typographies/{typography-id}.json`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID | Yes | Typography identifier | +| `name` | string | Yes | Typography name | +| `fontId` | string | Yes | Font identifier | +| `fontFamily` | string | Yes | Font family name | +| `fontVariantId` | string | Yes | Font variant | +| `fontSize` | string | Yes | Font size | +| `fontWeight` | string | Yes | Font weight | +| `fontStyle` | string | Yes | Font style | +| `lineHeight` | string | Yes | Line height | +| `letterSpacing` | string | Yes | Letter spacing | +| `textTransform` | string | Yes | Text transform | + +### Design Tokens + +Stored at `files/{file-id}/tokens.json`. + +The tokens library contains: + +- **Sets**: Collections of tokens organized hierarchically +- **Themes**: Named combinations of token sets +- **Active Themes**: Currently applied themes + +#### Token Structure + +```json +{ + "sets": { + "core": { + "id": "uuid", + "name": "Core", + "tokens": { + "color": { + "primary": { + "id": "uuid", + "name": "primary", + "type": "color", + "value": "#0066CC" + } + } + } + } + }, + "themes": { + "light": { + "id": "uuid", + "name": "Light", + "sets": ["core"] + } + }, + "activeThemes": ["light"] +} +``` + +## Media and Storage Objects + +### Storage Objects + +Binary assets (images, fonts, etc.) are stored in the `objects/` directory. + +Each storage object has: +- `objects/{uuid}.json` - Metadata +- `objects/{uuid}.{ext}` - Binary content (png, jpg, svg, etc.) + +#### Metadata Schema + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID | Yes | Storage object identifier | +| `size` | integer | Yes | File size in bytes | +| `contentType` | string | Yes | MIME type | +| `bucket` | string | Yes | Storage bucket | +| `hash` | string | No | Content hash (blake2b) | + +#### Example + +```json +{ + "id": "0039433d-adc8-430d-b2c3-d884dea6e050", + "size": 575, + "contentType": "image/png", + "bucket": "file-media-object", + "hash": "blake2b:77d447db38eb5daf31acb7344a504cacc6b79aa11855a00501d9475c595053d0" +} +``` + +### Media References + +File media references are stored at `files/{file-id}/media/{media-id}.json`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID | Yes | Media identifier | +| `name` | string | Yes | File name | +| `width` | integer | Yes | Image width | +| `height` | integer | Yes | Image height | +| `mtype` | string | Yes | MIME type | +| `mediaId` | UUID | Yes | Reference to storage object | +| `thumbnaillId` | UUID | No | Reference to thumbnail | +| `isLocal` | boolean | No | Whether media is local to file | + +### Thumbnails + +Page thumbnails are stored at `files/{file-id}/thumbnails/{tag}/{page-id}/{frame-id}.json`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `fileId` | UUID | Yes | File identifier | +| `pageId` | UUID | Yes | Page identifier | +| `frameId` | UUID | Yes | Frame identifier | +| `tag` | string | Yes | Thumbnail tag | +| `mediaId` | UUID | Yes | Reference to storage object | + +## Plugin Data + +Plugins can store custom data on files, pages, shapes, components, colors, and typographies using the `pluginData` field. + +### Structure + +```json +{ + "pluginData": { + "plugin-id": { + "key1": "value1", + "key2": "value2" + } + } +} +``` + +The plugin ID is a keyword (e.g., `"my-plugin"`), and the values are string key-value pairs. + +## Inspecting a .penpot File + +### List Contents + +```bash +unzip -l design.penpot +``` + +### Extract and View + +```bash +# Extract to temporary directory +unzip design.penpot -d /tmp/penpot-inspect + +# View manifest +cat /tmp/penpot-inspect/manifest.json | jq . + +# List all files +find /tmp/penpot-inspect -name "*.json" | head -20 + +# View a specific shape +cat /tmp/penpot-inspect/files/*/pages/*/*.json | jq . +``` + +### Browser-Based Inspector + +For an interactive way to explore a `.penpot` file, try the [**Penpot file inspector**](/technical-guide/developer/data-model/penpot-file-inspector/). It runs entirely in your browser, displays a collapsible file tree, syntax-highlighted JSON, image previews, and clickable cross-references between shapes, components, colors, and media. + +### Quick Inspection Script + +```bash +#!/bin/bash +# Inspect .penpot file structure + +FILE=$1 +TMPDIR=$(mktemp -d) + +unzip -q "$FILE" -d "$TMPDIR" + +echo "=== Manifest ===" +cat "$TMPDIR/manifest.json" | jq '{type, version, files: [.files[] | {id, name}]}' + +echo -e "\n=== Files ===" +for f in "$TMPDIR"/files/*.json; do + echo "- $(jq -r '.name' "$f")" +done + +echo -e "\n=== Pages ===" +for f in "$TMPDIR"/files/*/pages/*.json; do + echo "- $(jq -r '.name' "$f")" +done + +echo -e "\n=== Storage Objects ===" +ls -lh "$TMPDIR"/objects/*.{png,jpg,svg} 2>/dev/null | wc -l +echo "media files" + +rm -rf "$TMPDIR" +``` + +## Cross-References + +- [Penpot file inspector](/technical-guide/developer/data-model/penpot-file-inspector/) - Browser-based interactive inspector +- [Data Model](/technical-guide/developer/data-model/) - Conceptual data model +- [Data Guide](/technical-guide/developer/data-guide/) - Working with data structures +- [Export/Import Files](/user-guide/export-import/export-import-files/) - User guide for exporting and importing + +## Source Code References + +The authoritative schema definitions are in the Penpot source code: + +- **Manifest**: `backend/src/app/binfile/v3.clj` (schema:manifest) +- **File**: `common/src/app/common/types/file.cljc` (schema:file) +- **Page**: `common/src/app/common/types/page.cljc` (schema:page) +- **Shape**: `common/src/app/common/types/shape.cljc` (schema:shape) +- **Component**: `common/src/app/common/types/component.cljc` (schema:component) +- **Color**: `common/src/app/common/types/color.cljc` (schema:library-color) +- **Typography**: `common/src/app/common/types/typography.cljc` (schema:typography) +- **Tokens**: `common/src/app/common/types/tokens_lib.cljc` (schema:tokens-lib) +- **Plugin Data**: `common/src/app/common/types/plugins.cljc` (schema:plugin-data) +- **Features**: `common/src/app/common/features.cljc` (schema:features) diff --git a/docs/technical-guide/developer/data-model/penpot-file-inspector.njk b/docs/technical-guide/developer/data-model/penpot-file-inspector.njk new file mode 100644 index 0000000000..5321f79fe0 --- /dev/null +++ b/docs/technical-guide/developer/data-model/penpot-file-inspector.njk @@ -0,0 +1,1401 @@ +--- +title: 3.02.02. Penpot file inspector +desc: Interactive browser-based inspector for .penpot (v3) files. Upload a file to explore its ZIP structure, browse JSON contents, preview images, and navigate cross-references. +--- + + + +

Penpot file inspector

+ +

Upload a .penpot file to explore its contents. The inspector runs entirely in your browser — your file is never uploaded to a server.

+ +
+ +
+ + +
+ +

How it works

+ +

This tool is a static page with no backend. When you upload a file, the browser:

+ +
    +
  1. Reads the file as a ZIP archive using JSZip (loaded lazily from a CDN on first use).
  2. +
  3. Parses the manifest, file metadata, and structure to build an overview.
  4. +
  5. Lets you navigate the file tree, view JSON contents with syntax highlighting, preview embedded images, and click through cross-references (UUIDs).
  6. +
+ +

For full details on the file format, see the complete technical specification.

+ +

Privacy

+ +

Your file is processed entirely in your browser. Nothing is uploaded to any server. The only network request the page makes is fetching the JSZip library from cdn.jsdelivr.net on the first file upload.

+ + diff --git a/docs/user-guide/export-import/export-import-files.njk b/docs/user-guide/export-import/export-import-files.njk index e916fd6304..ac462d64cf 100644 --- a/docs/user-guide/export-import/export-import-files.njk +++ b/docs/user-guide/export-import/export-import-files.njk @@ -48,21 +48,22 @@ desc: Learn how to import and export files in Penpot, the free, open-source desi
Import penpot file

Penpot file format

-

Penpot export to a unique format that streamline the import and export of files and assets by being more efficient and interoperable.

+

Penpot exports to a unique format that streamlines the import and export of files and assets by being more efficient and interoperable.

Unlike other design tools, Penpot's format is built on standard languages. The exported file is essentially a ZIP archive containing binary assets (such as bitmap and vector images) alongside a readable JSON structure. By avoiding proprietary formats, Penpot empowers users with autonomy from specific tools while enabling seamless third-party integrations.

+

For a detailed look at what's inside a .penpot file, see The .penpot file format. Developers can also check the technical specification.

Deprecated Penpot file formats

These formats can only be exported from version 2.3 or earlier versions, but can be imported to any Penpot version.

There are two different deprecated Penpot file formats in which you can import/export Penpot files. A standard one and a binary one. You always have the chance to use both for any file.

-

[Deprecated] Penpot file (.penpot).

+

[Deprecated] Binary file (.penpot) — v1

The fast one. Binary Penpot specific.

  • ✅ Highly efficient in terms of memory and transfer time when exporting and importing.
  • ❌ It can be opened only in Penpot.
  • ❌ Not transparent, code difficult to explore.
-

[Deprecated] Standard file (.zip).

-

The open one. A compressed file that includes SVG and JSON.

+

[Deprecated] Standard file (.zip) — v2 (never released)

+

The open one. A compressed file that includes SVG and JSON. This format was developed but never released to users.

  • ✅ Allows the file to be opened by other softwares (still, for those cases export to SVG seems to be the common practice).
  • ✅ Allows some automations and integrations.
  • diff --git a/docs/user-guide/export-import/index.njk b/docs/user-guide/export-import/index.njk index 48b4bcbd04..f218893dd7 100644 --- a/docs/user-guide/export-import/index.njk +++ b/docs/user-guide/export-import/index.njk @@ -13,7 +13,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial

    How to export and import your Penpot files

    -
  • +
  • + +

    The .penpot file format →

    +

    Understand what's inside a .penpot file and how to inspect it

    +
    +
  • +
  • Exporting layers →

    How to export elements from your design into different file formats

    diff --git a/docs/user-guide/export-import/penpot-file-format.njk b/docs/user-guide/export-import/penpot-file-format.njk new file mode 100644 index 0000000000..6febaa47e5 --- /dev/null +++ b/docs/user-guide/export-import/penpot-file-format.njk @@ -0,0 +1,155 @@ +--- +title: The .penpot file format +order: 2 +desc: Understand the .penpot file format, what's inside, and how to inspect your design files. +--- + +

    The .penpot file format

    +

    Penpot's native file format is designed to be open, inspectable, and efficient. Unlike proprietary formats, you can always look inside a .penpot file to understand your design data.

    + +

    What is a .penpot file?

    + +

    A .penpot file is a ZIP archive containing:

    +
      +
    • JSON metadata — human-readable descriptions of your pages, shapes, colors, components, and more
    • +
    • Binary media files — images, fonts, and other assets used in your design
    • +
    + +

    This means your design data is never locked in a proprietary format. You can always unzip a .penpot file and read the JSON to understand what's inside.

    + +

    What's inside a .penpot file?

    + +

    When you unzip a .penpot file, you'll find this structure:

    + +
    +
    your-design.penpot (ZIP archive)
    +├── manifest.json          ← Table of contents
    +├── files/                 ← Your design data
    +│   ├── file-metadata.json
    +│   └── file-data/
    +│       ├── pages/         ← Each page of your design
    +│       ├── colors/        ← Library colors
    +│       ├── components/    ← Library components
    +│       ├── typographies/  ← Library typographies
    +│       ├── tokens.json    ← Design tokens
    +│       └── media/         ← Media references
    +└── objects/               ← Images and binary assets
    +
    + +

    manifest.json

    +

    The manifest is the table of contents. It tells you:

    +
      +
    • Which version of the format is used
    • +
    • Which Penpot version created the file
    • +
    • What files are included
    • +
    • What features are enabled
    • +
    + +

    File data

    +

    The files/ directory contains all your design data organized by type:

    +
      +
    • Pages — Each page is split into individual shape files for efficiency
    • +
    • Colors — Your color library with hex values, gradients, or image fills
    • +
    • Components — Reusable component definitions
    • +
    • Typographies — Text style definitions
    • +
    • Tokens — Design tokens organized in sets and themes
    • +
    • Media — References to images and other assets
    • +
    + +

    Objects

    +

    The objects/ directory contains the actual binary files (PNG, JPG, SVG) referenced by your design. Each object has a JSON metadata file alongside the binary file.

    + +

    How to inspect a .penpot file

    + +

    Since .penpot files are just ZIP archives, you can inspect them with standard tools.

    + +

    On macOS or Linux

    + +

    1. List the contents without extracting:

    +
    unzip -l your-design.penpot
    + +

    2. Extract to a temporary folder:

    +
    unzip your-design.penpot -d /tmp/penpot-inspect
    + +

    3. View the manifest:

    +
    cat /tmp/penpot-inspect/manifest.json | jq .
    + +

    4. Browse the structure:

    +
    tree /tmp/penpot-inspect
    + +

    5. View a specific shape:

    +
    cat /tmp/penpot-inspect/files/*/pages/*/*.json | jq .
    + +

    On Windows

    + +

    1. Right-click the .penpot file and select "Extract All..." or use 7-Zip.

    + +

    2. Browse the extracted folder to see the structure.

    + +

    3. Open any JSON file with a text editor or use a JSON viewer tool.

    + +

    Using AI tools

    + +

    You can use the Penpot MCP server to inspect .penpot files with AI assistance. The MCP server can read and analyze your design files, making it easy to understand complex designs or automate tasks.

    + +

    Format versions

    + +

    The .penpot format has evolved over time:

    + + + + + + + + + + + + + + + + + + + + + +
    VersionDescriptionStatus
    v3ZIP + JSON format (current)✅ Current
    v1Custom binary format⚠️ Deprecated
    + +

    All current Penpot versions export in v3 format. Old v1 files can still be imported, but new exports will use v3.

    + +

    Version numbers inside the file

    + +

    Inside a .penpot file, you'll see two different version numbers:

    + +
      +
    • Format version (in manifest.json) — Tracks changes to the ZIP structure itself. Currently 1.
    • +
    • Data version (in each file's JSON) — Tracks changes to the data model. This number increases as Penpot evolves.
    • +
    + +

    Feature flags

    + +

    Files also include a list of feature flags that indicate which capabilities are used. For example:

    + +
      +
    • design-tokens/v1 — Uses design tokens
    • +
    • components/v2 — Uses the v2 component system
    • +
    • variants/v1 — Uses component variants
    • +
    • layout/grid — Uses grid layouts
    • +
    + +

    These flags help Penpot know what features need to be supported when importing the file.

    + +

    For developers

    + +

    If you're building tools or integrations that work with .penpot files, the complete technical specification provides detailed schema definitions for every JSON file in the archive.

    + +

    Key resources:

    + diff --git a/package.json b/package.json index 1454dbd2bc..60e85d0149 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@11.10.0+sha512.0b7f8b98060031904c017e3a41eb187a16d40eeb829b95c4f8cb03681761fc4ab53dd219115b9b447f4dce1a05a214764461e7d3703392a9f32f9511ce8c86c8", + "packageManager": "pnpm@11.12.0+sha512.820a6fbd0d9f04c226638002aead1e45340a9139dd5dc077c1d83ef44aa2481c8eb6637b4c9aa696a3c7e35ba818e49cf27213e5f2b91138d09b7a3e26e898ba", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" @@ -16,10 +16,12 @@ "fmt": "./scripts/fmt" }, "devDependencies": { + "@playwright/mcp": "^0.0.76", + "@playwright/test": "^1.61.1", "@types/node": "^26.0.1", "esbuild": "^0.28.1", "mdts": "^0.20.3", "nrepl-client": "^0.3.0", - "opencode-ai": "^1.17.11" + "playwright": "^1.61.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b75584b11a..0deb5862af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: devDependencies: + '@playwright/mcp': + specifier: ^0.0.76 + version: 0.0.76 + '@playwright/test': + specifier: ^1.61.1 + version: 1.61.1 '@types/node': specifier: ^26.0.1 version: 26.0.1 @@ -20,9 +26,9 @@ importers: nrepl-client: specifier: ^0.3.0 version: 0.3.0 - opencode-ai: - specifier: ^1.17.11 - version: 1.17.11 + playwright: + specifier: ^1.61.1 + version: 1.61.1 packages: @@ -188,6 +194,16 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@playwright/mcp@0.0.76': + resolution: {integrity: sha512-ICcW6wAV+HoNEco19eni+JTVylCIIedruWkrRl2Rsv/qcGhBZLT/c0I1VounjIjSVSupw3pHmcr3CNCikTHTBQ==} + engines: {node: '>=18'} + hasBin: true + + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@simple-git/args-pathspec@1.0.3': resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==} @@ -365,6 +381,11 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -545,75 +566,6 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} - opencode-ai@1.17.11: - resolution: {integrity: sha512-a4331YhfsIiDSve0YRP2eNdlY1iDqhzw979Ky66XWiKDA8jeykfsqGUDAmKYKjS3hLV8U8+krFBuMFVO7p5ptQ==} - cpu: [arm64, x64] - os: [darwin, linux, win32] - hasBin: true - - opencode-darwin-arm64@1.17.11: - resolution: {integrity: sha512-WpBokL8RL8BvdPKzJhQlLbVigz4jT0uESDWgwLcsU2JAP8hOWc/bMgzf87C7VtJlcjUY9ao60UzbPlsUffb/0g==} - cpu: [arm64] - os: [darwin] - - opencode-darwin-x64-baseline@1.17.11: - resolution: {integrity: sha512-hay37M7ALa4/4RrtJDggnOOCL226+cgQQQ0KiBFWNMDhygtwBszQnZmNxWxZtoRNIr3sIIS8JNsuBfhG4OwXYg==} - cpu: [x64] - os: [darwin] - - opencode-darwin-x64@1.17.11: - resolution: {integrity: sha512-ZxQzLT92FT96Y8ahpHZiejD+m7vQYhAfskfzMwc0baDjkctEXy6UkZT5gY5jTDl+Bb74xgmKYJK6Lz20luhOXA==} - cpu: [x64] - os: [darwin] - - opencode-linux-arm64-musl@1.17.11: - resolution: {integrity: sha512-haM+NZ/CC14VdHSi81pRlYDbLvu3sXAQF8HH1t7yi0YpH3wLibq0Ms8prM0809Sza+30pyUIxH6aoYghnr2Juw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - opencode-linux-arm64@1.17.11: - resolution: {integrity: sha512-CN3LSlqSrC1LbYHXZs21B8hIB951ebCRowwC+p4SDwPPUKknRzGYi5V7FjXAp8xq5hx27/QGgmGjfauv6RbAiA==} - cpu: [arm64] - os: [linux] - - opencode-linux-x64-baseline-musl@1.17.11: - resolution: {integrity: sha512-AR3soQ1kYquD+QHaNDcWhHxd6lT8yUZJ2jsnls+7oOTisQWUspi5TpFD5FL4Et1MMtoJx63CAySDTzUpRXUO9Q==} - cpu: [x64] - os: [linux] - libc: [musl] - - opencode-linux-x64-baseline@1.17.11: - resolution: {integrity: sha512-y0sibv7zg0KDLD7hdMCLe4+YYP6t5PQmqTV88KR7jWIU7qvl1hyuIAI84QLCt+7DACpNsFsUfofu6Hib051jZw==} - cpu: [x64] - os: [linux] - - opencode-linux-x64-musl@1.17.11: - resolution: {integrity: sha512-cl2SMRa1c6dJMxvs5BQMy7Q2LVBwGXbLHpjc0OaeMkdORwdBfwCXfW1GNA61pNR4mMHMKrbFEV6dETqCDuIaWQ==} - cpu: [x64] - os: [linux] - libc: [musl] - - opencode-linux-x64@1.17.11: - resolution: {integrity: sha512-at2oODO6N4yMTlvtKOFECVDvj4Nz3iFygmSKyoVdokgpMhYt6l9ta63pEp1jAaTxLpjQGbCzpRYbY/QqRAeB9Q==} - cpu: [x64] - os: [linux] - - opencode-windows-arm64@1.17.11: - resolution: {integrity: sha512-pWOT/Ml4e3S12BQTmw8i+CsQ7QF0kI6Z//9z/N60gLW1U5afVYQpHkT1pT5RVjysPVHP8G4TN7Rbyi8m+fapCQ==} - cpu: [arm64] - os: [win32] - - opencode-windows-x64-baseline@1.17.11: - resolution: {integrity: sha512-RkJX3S77bzFZOHyQsqBqkI567hGHiTGU2TTIkBphYvRNxCQy+ZDjn3QAh+h7zyCHfieeWRKA96kl9ycKaJGO4A==} - cpu: [x64] - os: [win32] - - opencode-windows-x64@1.17.11: - resolution: {integrity: sha512-4ON++fPbRVhLpYvy6j0RolFstU1OncbqZ4FLFeAkC4L6CNO06kHEwmwRfEVcGo+zFpug/ljdW2T0W+sYBw20SA==} - cpu: [x64] - os: [win32] - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -635,6 +587,26 @@ packages: plantuml@0.0.2: resolution: {integrity: sha512-3YzQJUO1Yg+mDckTm3Ht5Q8bmtN8g3M9LD8fXqiqHDW3vzUpHrUe9lxVY6AT1I50w7FdOned0hhJno4JBIku2g==} + playwright-core@1.61.0-alpha-1781023400000: + resolution: {integrity: sha512-UdtUd9qnCO0zvb8p3OvOZpelY6mA40mTb3NmWGuMtrD+hqqWuorWCPlSGwj7jw/LEB9AxvYLHTL1CJi2flvksg==} + engines: {node: '>=18'} + hasBin: true + + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.0-alpha-1781023400000: + resolution: {integrity: sha512-8TRUG3IvwaAhuVm6k3C5vB7CwC5Fxq76DCCxOgPr6r1dpTedDwxlmdOBUkSZ0zxfxP14jcuPxi86/Trq4eA03w==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -864,6 +836,15 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@playwright/mcp@0.0.76': + dependencies: + playwright: 1.61.0-alpha-1781023400000 + playwright-core: 1.61.0-alpha-1781023400000 + + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@simple-git/args-pathspec@1.0.3': {} '@simple-git/argv-parser@1.1.1': @@ -1079,6 +1060,9 @@ snapshots: fresh@2.0.0: {} + fsevents@2.3.2: + optional: true + function-bind@1.1.2: {} get-intrinsic@1.3.0: @@ -1263,57 +1247,6 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 - opencode-ai@1.17.11: - optionalDependencies: - opencode-darwin-arm64: 1.17.11 - opencode-darwin-x64: 1.17.11 - opencode-darwin-x64-baseline: 1.17.11 - opencode-linux-arm64: 1.17.11 - opencode-linux-arm64-musl: 1.17.11 - opencode-linux-x64: 1.17.11 - opencode-linux-x64-baseline: 1.17.11 - opencode-linux-x64-baseline-musl: 1.17.11 - opencode-linux-x64-musl: 1.17.11 - opencode-windows-arm64: 1.17.11 - opencode-windows-x64: 1.17.11 - opencode-windows-x64-baseline: 1.17.11 - - opencode-darwin-arm64@1.17.11: - optional: true - - opencode-darwin-x64-baseline@1.17.11: - optional: true - - opencode-darwin-x64@1.17.11: - optional: true - - opencode-linux-arm64-musl@1.17.11: - optional: true - - opencode-linux-arm64@1.17.11: - optional: true - - opencode-linux-x64-baseline-musl@1.17.11: - optional: true - - opencode-linux-x64-baseline@1.17.11: - optional: true - - opencode-linux-x64-musl@1.17.11: - optional: true - - opencode-linux-x64@1.17.11: - optional: true - - opencode-windows-arm64@1.17.11: - optional: true - - opencode-windows-x64-baseline@1.17.11: - optional: true - - opencode-windows-x64@1.17.11: - optional: true - parseurl@1.3.3: {} path-key@3.1.1: {} @@ -1332,6 +1265,22 @@ snapshots: execa: 4.1.0 get-stream: 5.2.0 + playwright-core@1.61.0-alpha-1781023400000: {} + + playwright-core@1.61.1: {} + + playwright@1.61.0-alpha-1781023400000: + dependencies: + playwright-core: 1.61.0-alpha-1781023400000 + optionalDependencies: + fsevents: 2.3.2 + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + powershell-utils@0.1.0: {} proxy-addr@2.0.7: From 79a471a6f196c1711e4c1e7da6f2ea1c47c950d6 Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Mon, 20 Jul 2026 21:28:24 +0200 Subject: [PATCH 5/9] :bug: Fix default exporter config (#10761) --- exporter/src/app/config.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index 8010032680..127a28fa99 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -22,7 +22,7 @@ (def ^:private defaults {:public-uri "http://localhost:3449" - :internal-uri nil + ;; :internal-uri nil ;; internal-uri cannot be nil :tenant "default" :host "localhost" :http-server-port 6061 From 378d97df9331a53381b96cb71021c4002ecb3b02 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 20 Jul 2026 21:47:11 +0200 Subject: [PATCH 6/9] :paperclip: Update refine-prompt skill --- .opencode/skills/refine-prompt/SKILL.md | 32 ++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.opencode/skills/refine-prompt/SKILL.md b/.opencode/skills/refine-prompt/SKILL.md index 8cc7473619..c2439e02b9 100644 --- a/.opencode/skills/refine-prompt/SKILL.md +++ b/.opencode/skills/refine-prompt/SKILL.md @@ -56,7 +56,10 @@ and only weave in Penpot context when it is clearly relevant. - Ask clarifying questions if the intent is unclear or if critical information is missing (e.g. target model, expected output format, tone, constraints). Keep questions concise and grouped. Prefer to ask 1–4 questions at once - rather than one at a time. + rather than one at a time. **Use the `question` tool** to ask them so the + user gets a structured multi-choice UI; reserve a plain `## Clarifying + questions` markdown section for cases where the `question` tool is + unavailable or the question is genuinely open-ended. - Rewrite the prompt using prompt-engineering best practices (see below). - Preserve the user's original intent — do not change the underlying task. - When the user provides Penpot project context, weave in the relevant @@ -105,10 +108,27 @@ Deliver the result in the response as two clearly separated blocks: changes you made and why (3–7 bullets max). Skip the rationale if the changes are trivial. -If you asked clarifying questions, list them in a separate **Clarifying -questions** section above the refined prompt and stop — do not produce a -refined prompt until the user answers. If the user explicitly told you to -proceed without questions (e.g. "just rewrite it"), make reasonable +If you asked clarifying questions via the `question` tool, stop and wait for +the answers before producing a refined prompt. If the `question` tool was not +available and you asked the questions in chat, list them in a separate +**Clarifying questions** section above the refined prompt and stop — do not +produce a refined prompt until the user answers. If the user explicitly told +you to proceed without questions (e.g. "just rewrite it"), make reasonable assumptions and note them under **Assumptions made** in the rationale block. -No file persistence — the refined prompt lives entirely in the response. +## File Persistence + +Always persist the refined prompt to disk so it can be re-used later, versioned +in git, and shared with other agents. The response still contains the prompt +and rationale blocks; the file is an additional artifact, not a replacement. + +- Save the refined prompt (the body inside the fenced code block, **without** + the surrounding ``` fences) to `.opencode/prompts/.md`. +- Use a **kebab-case** filename that summarises the task, e.g. + `add-error-reports-management-rpc.md`, `backend-rpc-security-audit.md`. No + spaces, no uppercase, no version numbers or dates in the filename. +- If `.opencode/prompts/` does not exist, create it before writing. +- If a file with the same name already exists, overwrite it (the file is the + refined prompt, not a log). +- Only skip the file write when the user explicitly opts out (e.g. "don't save + this one", "just show it in the chat"). When in doubt, save it. From 64026fc1f6cb2678c0cad8fbe1771b25848f3728 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Jul 2026 11:11:21 +0200 Subject: [PATCH 7/9] :paperclip: Update changelog --- CHANGES.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index adf585bd3a..0a940745ae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,7 +22,7 @@ - Remove unreachable try/catch in hex->hsl (by @Dexterity104) [#9244](https://github.com/penpot/penpot/issues/9244) (PR: [#9245](https://github.com/penpot/penpot/pull/9245)) - Remove stray debug log in exporter upload-resource (by @iot2edge) [#9270](https://github.com/penpot/penpot/issues/9270) (PR: [#9272](https://github.com/penpot/penpot/pull/9272)) - Release pool connection during font variant creation (by @Dexterity104) [#9286](https://github.com/penpot/penpot/issues/9286) (PR: [#9287](https://github.com/penpot/penpot/pull/9287)) -- Add autocomplete combobox to token creation and edition forms [#9899](https://github.com/penpot/penpot/issues/9899) (PR: [#9109](https://github.com/penpot/penpot/pull/9109)) +- Add autocomplete combobox to token creation and edition forms [#9899](https://github.com/penpot/penpot/issues/9899) (PR: [#9109](https://github.com/penpot/penpot/pull/9109), [#8294](https://github.com/penpot/penpot/pull/8294)) - Add list view mode to color picker UI [#4420](https://github.com/penpot/penpot/issues/4420) (PR: [#9953](https://github.com/penpot/penpot/pull/9953)) - Use Clipboard API consistently across the application (by @MilosM348) [#6514](https://github.com/penpot/penpot/issues/6514) (PR: [#9188](https://github.com/penpot/penpot/pull/9188)) - Use `$` as DTCG token/group discriminator and make `$description` optional [#8342](https://github.com/penpot/penpot/issues/8342) (PR: [#9912](https://github.com/penpot/penpot/pull/9912)) @@ -34,7 +34,6 @@ - Add composite typography token input to the Design sidebar [#9932](https://github.com/penpot/penpot/issues/9932) (PR: [#9128](https://github.com/penpot/penpot/pull/9128), [#9375](https://github.com/penpot/penpot/pull/9375), [#8749](https://github.com/penpot/penpot/pull/8749)) - Avoid deduplicating temporary export files to prevent stale content (by @yong2bba) [#9970](https://github.com/penpot/penpot/issues/9970) (PR: [#9959](https://github.com/penpot/penpot/pull/9959)) - Add layer blur effect [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034)) -- Show and manage comments while designing in the workspace [#10239](https://github.com/penpot/penpot/issues/10239) (PR: [#10275](https://github.com/penpot/penpot/pull/10275)) - Add concurrency limiter for MCP Server Plugin Communications [#9493](https://github.com/penpot/penpot/issues/9493) (PR: [#9748](https://github.com/penpot/penpot/pull/9748)) - Render guides in WebGL [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014)) - Add configurable resource limits to ImageMagick image processing [#10223](https://github.com/penpot/penpot/issues/10223) (PR: [#10240](https://github.com/penpot/penpot/pull/10240)) @@ -42,6 +41,7 @@ - Add color variants and positioning to selection size badge (by @bittoby) [#10258](https://github.com/penpot/penpot/issues/10258) (PR: [#9210](https://github.com/penpot/penpot/pull/9210)) - Use hard reload for render engine switching in the workspace menu [#10441](https://github.com/penpot/penpot/issues/10441) (PR: [#10444](https://github.com/penpot/penpot/pull/10444)) - Rotate size badge when shape is rotated [#10386](https://github.com/penpot/penpot/issues/10386) (PR: [#10393](https://github.com/penpot/penpot/pull/10393)) +- Add separate internal URI for exporter to handle Docker deployments where internal and public URIs differ [#10627](https://github.com/penpot/penpot/issues/10627) (PR: [#10630](https://github.com/penpot/penpot/pull/10630)) ### :bug: Bugs fixed @@ -157,6 +157,16 @@ - Fix SVG raw caching prevented by unnecessary Cow wrapping [#10488](https://github.com/penpot/penpot/issues/10488) (PR: [#10492](https://github.com/penpot/penpot/pull/10492)) - Add component reset operation to plugin API [#10561](https://github.com/penpot/penpot/issues/10561) (PR: [#10533](https://github.com/penpot/penpot/pull/10533)) - Fix blur menu alignment in Firefox [#10576](https://github.com/penpot/penpot/issues/10576) (PR: [#10575](https://github.com/penpot/penpot/pull/10575)) +- Fix sidebar not showing all elements with grid layout [#10539](https://github.com/penpot/penpot/issues/10539) (PR: [#10600](https://github.com/penpot/penpot/pull/10600)) +- Fix sidebar getting stuck when selecting shapes that haven't loaded yet [#10599](https://github.com/penpot/penpot/issues/10599) (PR: [#10600](https://github.com/penpot/penpot/pull/10600)) +- Fix text shape bounding boxes not updating after remote fonts finish loading [#10585](https://github.com/penpot/penpot/issues/10585) (PR: [#10566](https://github.com/penpot/penpot/pull/10566)) +- Fix text editor crash from Draft.js selection offset exceeding DOM node length [#10607](https://github.com/penpot/penpot/issues/10607) (PR: [#10608](https://github.com/penpot/penpot/pull/10608)) +- Fix workspace crash when converting SVG-raw shape to path [#10612](https://github.com/penpot/penpot/issues/10612) (PR: [#10613](https://github.com/penpot/penpot/pull/10613)) +- Fix component variant panel crash when selecting multiple copies with mismatched property counts [#10615](https://github.com/penpot/penpot/issues/10615) (PR: [#10616](https://github.com/penpot/penpot/pull/10616)) +- Fix workspace crash from recursion when clicking shape in comments mode [#10620](https://github.com/penpot/penpot/issues/10620) (PR: [#10622](https://github.com/penpot/penpot/pull/10622)) +- Fix Plugin API validation error when listing shared plugin data keys [#10628](https://github.com/penpot/penpot/issues/10628) (PR: [#10632](https://github.com/penpot/penpot/pull/10632)) +- Fix Plugin API silently dropping plugin data written to shared library [#10629](https://github.com/penpot/penpot/issues/10629) (PR: [#10632](https://github.com/penpot/penpot/pull/10632)) +- Fix workspace crash when event target is a DOM text node [#10640](https://github.com/penpot/penpot/issues/10640) (PR: [#10641](https://github.com/penpot/penpot/pull/10641)) ## 2.16.2 From 73bfc0dc15ce07b3c025361d35b2dea7d332a556 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Jul 2026 09:00:43 +0200 Subject: [PATCH 8/9] :bug: Guard workspace-page* when file-id is not a uuid (#10655) The workspace route can be reached without a `:file-id` query parameter (e.g. `/#/workspace?team-id=...`). When that happened, `workspace*` was emitting `dw/initialize-workspace` with a nil file-id, which stored nil in `:current-file-id`. The `fetch-profiles` event then read nil from state and called `:get-profiles-for-file-comments` with `{:file-id nil}`, producing a 400 response. Move the `use-equal-memo` calls for `file-id` and `page-id` from `workspace*` up to `workspace-page*`, and guard the render with `(when (uuid? file-id) ...)` so `workspace*` only mounts when `file-id` is a valid uuid. Since `workspace*` never mounts with a nil `file-id`, `initialize-workspace` is never emitted with nil, and the 400 is prevented at the source. AI-assisted-by: minimax-m3 --- frontend/src/app/main/ui/workspace.cljs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index bb69485774..e7a3e7b119 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -196,10 +196,7 @@ {::mf/wrap [mf/memo]} [{:keys [team-id project-id file-id page-id layout-name]}] - (let [file-id (hooks/use-equal-memo file-id) - page-id (hooks/use-equal-memo page-id) - - layout (mf/deref refs/workspace-layout) + (let [layout (mf/deref refs/workspace-layout) wglobal (mf/deref refs/workspace-global) team-ref (mf/with-memo [team-id] @@ -287,6 +284,12 @@ (mf/defc workspace-page* {::mf/lazy-load true} - [props] - [:> workspace* props]) + [{:keys [file-id page-id] :as props}] + (let [file-id (hooks/use-equal-memo file-id) + page-id (hooks/use-equal-memo page-id) + props (mf/spread-props props {:file-id file-id + :page-id page-id})] + + (when (uuid? file-id) + [:> workspace* props]))) From f3bf24b4f68263813da2fb95a2ac80e2332b039e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Jul 2026 21:32:54 +0200 Subject: [PATCH 9/9] :recycle: Consolidate dev tooling into scripts/ and reorganize docs Move all development tools from tools/ to scripts/ for consistency. Rename lint/fmt/check-fmt to lint-clj/fmt-clj/check-fmt-clj to clarify they target Clojure specifically. Remove unused scripts (attach-opencode, start-opencode, start-opencode-server) and the backport-commit skill. Update all internal references across .serena/, AGENTS.md, and CONTRIBUTING.md to point to the new script locations. Simplify CONTRIBUTING.md by delegating module-specific fmt/lint instructions to the respective serena memories. AI-assisted-by: deepseek-v4-flash --- .github/workflows/tests-backend.yml | 13 +- .gitignore | 3 +- .opencode/plugins/penpot.js | 299 ++++++++++++++++++ .opencode/skills/backport-commit/SKILL.md | 85 ----- .opencode/skills/nrepl-eval/SKILL.md | 16 +- .opencode/skills/taiga/SKILL.md | 30 +- .opencode/skills/update-changelog/SKILL.md | 44 +-- .serena/memories/backend/core.md | 24 +- .serena/memories/critical-info.md | 27 +- .serena/memories/frontend/core.md | 4 +- .../frontend/handling-errors-and-debugging.md | 4 +- .serena/memories/{tools => scripts}/gh.md | 28 +- .../memories/{tools => scripts}/nrepl-eval.md | 52 ++- .serena/memories/scripts/paren-repair.md | 43 +++ .serena/memories/scripts/psql.md | 39 +++ .serena/memories/{tools => scripts}/taiga.md | 14 +- .serena/memories/tools/paren-repair.md | 29 -- .serena/memories/tools/psql.md | 51 --- .serena/memories/workflow/creating-prs.md | 6 +- AGENTS.md | 20 +- CONTRIBUTING.md | 98 ++---- package.json | 7 +- scripts/attach-opencode | 13 - scripts/{check-fmt => check-fmt-clj} | 0 {tools => scripts}/db-schema | 0 {tools => scripts}/detect-target-branch | 4 +- scripts/fmt | 13 - {tools => scripts}/gh.py | 28 +- scripts/lint | 9 - {tools => scripts}/nrepl-eval.mjs | 0 tools/paren-repair.bb => scripts/paren-repair | 29 +- {tools => scripts}/psql | 0 scripts/start-opencode | 4 - scripts/start-opencode-server | 4 - {tools => scripts}/taiga.py | 14 +- 35 files changed, 598 insertions(+), 456 deletions(-) create mode 100644 .opencode/plugins/penpot.js delete mode 100644 .opencode/skills/backport-commit/SKILL.md rename .serena/memories/{tools => scripts}/gh.md (67%) rename .serena/memories/{tools => scripts}/nrepl-eval.md (66%) create mode 100644 .serena/memories/scripts/paren-repair.md create mode 100644 .serena/memories/scripts/psql.md rename .serena/memories/{tools => scripts}/taiga.md (69%) delete mode 100644 .serena/memories/tools/paren-repair.md delete mode 100644 .serena/memories/tools/psql.md delete mode 100755 scripts/attach-opencode rename scripts/{check-fmt => check-fmt-clj} (100%) rename {tools => scripts}/db-schema (100%) rename {tools => scripts}/detect-target-branch (93%) delete mode 100755 scripts/fmt rename {tools => scripts}/gh.py (96%) delete mode 100755 scripts/lint rename {tools => scripts}/nrepl-eval.mjs (100%) rename tools/paren-repair.bb => scripts/paren-repair (93%) rename {tools => scripts}/psql (100%) delete mode 100755 scripts/start-opencode delete mode 100755 scripts/start-opencode-server rename {tools => scripts}/taiga.py (95%) diff --git a/.github/workflows/tests-backend.yml b/.github/workflows/tests-backend.yml index fb58ed7ea4..d7a3377772 100644 --- a/.github/workflows/tests-backend.yml +++ b/.github/workflows/tests-backend.yml @@ -62,14 +62,15 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + - name: Fmt + working-directory: ./backend + run: | + cljfmt check --parallel=true src/ test/ + - name: Lint working-directory: ./backend run: | - corepack enable; - corepack install; - pnpm install; - pnpm run check-fmt - pnpm run lint + clj-kondo --parallel --lint ../common/src/ src/ - name: Tests working-directory: ./backend @@ -81,4 +82,4 @@ jobs: run: | mkdir -p /tmp/penpot; - clojure -M:dev:test --reporter kaocha.report/documentation + clojure -M:dev:test diff --git a/.gitignore b/.gitignore index dfdb8bae03..a0fb7f7f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .clj-kondo .cpcache .lsp +.env .nrepl-port .nyc_output .rebel_readline_history @@ -73,8 +74,6 @@ opencode.json /frontend/target/ /frontend/test-results/ /frontend/.shadow-cljs -/other/ -/scripts/ /nexus/ /tmp/ /vendor/**/target diff --git a/.opencode/plugins/penpot.js b/.opencode/plugins/penpot.js new file mode 100644 index 0000000000..5f0fbba554 --- /dev/null +++ b/.opencode/plugins/penpot.js @@ -0,0 +1,299 @@ +import { tool } from "@opencode-ai/plugin" +import path from "path" +import { spawn } from "child_process" + +const penpotPsqlTool = tool({ + description: + "Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.", + + args: { + sql: tool.schema + .string() + .describe("SQL command to execute"), + + test: tool.schema + .boolean() + .describe("Use the penpot_test database") + .optional(), + }, + + async execute(args, context) { + const host = process.env.PENPOT_DB_HOST || "postgres" + const user = process.env.PENPOT_DB_USER || "penpot" + const db = args.test + ? "penpot_test" + : process.env.PENPOT_DB_NAME || "penpot" + const password = process.env.PENPOT_DB_PASSWORD || "penpot" + + const psqlArgs = ["-h", host, "-U", user, "-d", db, "-c", args.sql] + + return new Promise((resolve) => { + let stdout = "" + let stderr = "" + + const proc = spawn("psql", psqlArgs, { + cwd: context.worktree, + env: { ...process.env, PGPASSWORD: password }, + }) + + proc.stdout.on("data", (data) => { + stdout += data.toString() + }) + + proc.stderr.on("data", (data) => { + stderr += data.toString() + }) + + proc.on("error", (error) => { + resolve(`Error: ${error.message}`) + }) + + proc.on("close", (exitCode) => { + const output = + exitCode === 0 + ? stdout.trim() || "Query executed successfully" + : `Error (exit ${exitCode}): ${ + (stderr || stdout).trim() || "No error output" + }` + resolve(output) + }) + }) + }, +}) + +const parenRepairTool = tool({ + description: + "Fix mismatched parentheses/braces in Clojure files (.clj, .cljs, .cljc) then reformat with cljfmt.", + + args: { + // A string is used instead of an array so OpenCode displays it + // in the generic tool invocation. + files: tool.schema + .string() + .describe( + "Comma-separated file paths to fix, for example: frontend/src/app/config.cljs, backend/src/core.clj", + ) + .optional(), + + code: tool.schema + .string() + .describe("Code string to fix via stdin") + .optional(), + }, + + async execute(args, context) { + const script = path.join(context.worktree, "scripts/paren-repair") + + const files = args.files + ? args.files + .split(",") + .map((file) => file.trim()) + .filter(Boolean) + : [] + + const paramInfo = + files.length > 0 + ? `files=[${files.join(", ")}]` + : args.code !== undefined + ? `code=(${args.code.length} chars)` + : "none" + + return new Promise((resolve) => { + const childArgs = + files.length > 0 + ? [script, ...files] + : [script] + + const proc = spawn("bb", childArgs, { + cwd: context.worktree, + }) + + let stdout = "" + let stderr = "" + + proc.stdout.on("data", (data) => { + stdout += data.toString() + }) + + proc.stderr.on("data", (data) => { + stderr += data.toString() + }) + + proc.on("error", (error) => { + resolve(`Error: ${error.message}`) + }) + + proc.on("close", (exitCode) => { + const output = + exitCode === 0 + ? stdout.trim() || "No changes needed" + : `Error (exit ${exitCode}): ${ + (stderr || stdout).trim() || "No error output" + }` + + resolve(output) + }) + + // Close stdin in all cases so the process cannot wait indefinitely. + if (args.code !== undefined) { + proc.stdin.end(args.code) + } else { + proc.stdin.end() + } + }) + }, +}) + +export default async function plugin() { + return { + tool: { + "paren-repair": parenRepairTool, + "penpot-psql": penpotPsqlTool, + }, + } +} + + + + + + + + + + + +// import { tool } from "@opencode-ai/plugin" +// import path from "path" +// import { spawn } from "child_process" + +// function formatFiles(files) { +// if (files.length === 0) return "stdin" + +// // Keep the visible tool title reasonably short. +// if (files.length <= 3) return files.join(", ") + +// return `${files.slice(0, 3).join(", ")} (+${files.length - 3} more)` +// } + +// const parenRepairTool = tool({ +// description: +// "Fix mismatched parentheses/braces in Clojure files, then reformat with cljfmt.", + +// args: { +// files: tool.schema +// .array(tool.schema.string()) +// .describe("Array of file paths to fix") +// .optional(), + +// code: tool.schema +// .string() +// .describe("Code string to fix via stdin") +// .optional(), +// }, + +// async execute(args, context) { +// const script = path.join(context.worktree, "scripts/paren-repair") + +// const files = (args.files ?? []).map((file) => { +// const absolute = path.isAbsolute(file) +// ? file +// : path.resolve(context.worktree, file) + +// return path.relative(context.worktree, absolute) +// }) + +// const targetSummary = +// files.length > 0 +// ? formatFiles(files) +// : args.code !== undefined +// ? `stdin (${args.code.length} chars)` +// : "no input" + +// // This updates the tool-call title immediately, while it is running. +// await context.metadata({ +// title: `Paren repair: ${targetSummary}`, +// metadata: { +// files, +// codeChars: args.code?.length, +// }, +// }) + +// const childArgs = +// args.files && args.files.length > 0 +// ? [script, ...args.files] +// : [script] + +// return new Promise((resolve) => { +// const proc = spawn("bb", childArgs, { +// cwd: context.worktree, +// }) + +// let stdout = "" +// let stderr = "" + +// if (args.code !== undefined) { +// proc.stdin.end(args.code) +// } + +// proc.stdout.on("data", (data) => { +// stdout += data.toString() +// }) + +// proc.stderr.on("data", (data) => { +// stderr += data.toString() +// }) + +// proc.on("close", (exitCode) => { +// const successful = exitCode === 0 + +// const commandOutput = successful +// ? stdout.trim() || "No changes needed" +// : `Error (exit ${exitCode}): ${(stderr || stdout).trim()}` + +// const parameterOutput = +// files.length > 0 +// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}` +// : args.code !== undefined +// ? `Input passed through stdin: ${args.code.length} characters` +// : "No files or stdin input were passed" + +// resolve({ +// title: `Paren repair: ${targetSummary}`, +// output: `${parameterOutput}\n\n${commandOutput}`, +// metadata: { +// files, +// codeChars: args.code?.length, +// exitCode, +// successful, +// }, +// }) +// }) + +// proc.on("error", (error) => { +// resolve({ +// title: `Paren repair failed: ${targetSummary}`, +// output: [ +// files.length > 0 +// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}` +// : `Input: ${targetSummary}`, +// `Failed to start bb: ${error.message}`, +// ].join("\n\n"), +// metadata: { +// files, +// codeChars: args.code?.length, +// successful: false, +// }, +// }) +// }) +// }) +// }, +// }) + +// export default async function plugin() { +// return { +// tool: { +// "paren-repair": parenRepairTool, +// }, +// } +// } diff --git a/.opencode/skills/backport-commit/SKILL.md b/.opencode/skills/backport-commit/SKILL.md deleted file mode 100644 index 79494a3c5d..0000000000 --- a/.opencode/skills/backport-commit/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: backport-commit -description: Port changes from a specific Git commit to the current branch by manually applying the diff, avoiding cherry-pick when it would introduce complex conflicts. ---- - -# Backport Commit - -Port changes from a specific Git commit to the current branch by manually -applying the diff, avoiding `git cherry-pick` when it would introduce -complex conflicts. - -## When to Use - -Use this skill whenever the user asks to backport a commit, especially when: - -- The commit touches multiple modules or files with significant divergence -- `git cherry-pick` is explicitly ruled out ("do not use cherry-pick") -- The target commit is old enough that conflicts are likely -- The commit introduces both source changes AND new files (tests, etc.) -- You need full control over how each hunk is applied - -## Workflow - -### 1. Identify the target commit - -```bash -# Verify the commit exists and understand what it does -git log --oneline -1 - -# Get the full diff (including new/deleted files) -git show - -# Capture the original commit message for later reuse -git log --format='%B' -1 -``` - -### 2. Identify affected modules - -From the file paths in the diff, determine which Penpot modules are affected -(frontend, backend, common, render-wasm, etc.) and read their `AGENTS.md` -files **before** making any changes. If a module has no `AGENTS.md`, skip -that step — verify with `ls /AGENTS.md` first. - -### 3. Read the current state of each affected file - -For every file the diff touches, read the current version on disk to understand -context and ensure correct placement before editing. - -### 4. Apply changes manually (the core of this approach) - -Process every hunk in the diff using the appropriate tool: - -| Diff action | Tool to use | -|-------------|-------------| -| Modify existing file | `edit` — use enough surrounding context in `oldString` to uniquely match the location | -| Add new file | `write` — include proper license header and namespace conventions matching project style | -| Delete file | `bash rm ` | -| Rename/move file | `bash mv `, then apply any content changes with `edit` | - -> **Tip:** Group nearby hunks from the same file into a single `edit` call. -> Use separate calls when hunks are far apart to keep `oldString` short and -> unambiguous. - -Repeat until **all** hunks in the diff are ported. - -### 5. Validate - -Run **lint**, **check-fmt**, and **tests** for every affected module (see each -module's `AGENTS.md` for the exact commands). If the formatter auto-fixes -indentation, verify the logic is still semantically correct. All checks must -pass before moving on. - -### 6. Commit - -Ask the `commiter` sub-agent to create a commit. Stage all relevant files -(exclude unrelated untracked files) and provide the original commit message as -a reference, adapting it as needed for the target branch context. - -## Key Principles - -- **Context matters** — always read files before editing; never guess - indentation or surrounding code -- **Lint + format + test** — never skip validation before committing -- **Preserve intent** — keep the original commit message meaning; the - `commiter` agent handles formatting diff --git a/.opencode/skills/nrepl-eval/SKILL.md b/.opencode/skills/nrepl-eval/SKILL.md index b4b3d76d7f..c84dc803c1 100644 --- a/.opencode/skills/nrepl-eval/SKILL.md +++ b/.opencode/skills/nrepl-eval/SKILL.md @@ -1,19 +1,19 @@ --- name: nrepl-eval -description: Evaluate Clojure code via nREPL using the standalone tools/nrepl-eval.mjs CLI tool. +description: Evaluate Clojure code via nREPL using the standalone scripts/nrepl-eval.mjs CLI tool. --- # nREPL Eval Evaluate Clojure (or ClojureScript) code via a running nREPL server using -`tools/nrepl-eval.mjs`. +`scripts/nrepl-eval.mjs`. -Full documentation: `mem:tools/nrepl-eval` (file: `.serena/memories/tools/nrepl-eval.md`) +Full documentation: `mem:scripts/nrepl-eval` (file: `.serena/memories/scripts/nrepl-eval.md`) ## Quick Reference ```bash -./tools/nrepl-eval.mjs [options] [] +./scripts/nrepl-eval.mjs [options] [] ``` | Flag | Description | Default | @@ -30,8 +30,8 @@ Full documentation: `mem:tools/nrepl-eval` (file: `.serena/memories/tools/nrepl- ## Examples ```bash -./tools/nrepl-eval.mjs '(+ 1 2 3)' -./tools/nrepl-eval.mjs --backend '(+ 1 2 3)' -./tools/nrepl-eval.mjs --frontend '(js/alert "hi")' -./tools/nrepl-eval.mjs -e +./scripts/nrepl-eval.mjs '(+ 1 2 3)' +./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)' +./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")' +./scripts/nrepl-eval.mjs -e ``` diff --git a/.opencode/skills/taiga/SKILL.md b/.opencode/skills/taiga/SKILL.md index 890c965c03..e63788698c 100644 --- a/.opencode/skills/taiga/SKILL.md +++ b/.opencode/skills/taiga/SKILL.md @@ -13,7 +13,7 @@ Fetch information from Taiga public API for the **Penpot** project ## Prerequisites -- `python3` — the `tools/taiga.py` CLI script is self-contained (stdlib only) +- `python3` — the `scripts/taiga.py` CLI script is self-contained (stdlib only) ## Quick Start @@ -21,17 +21,17 @@ The easiest way is to use the bundled Python script: ```bash # Pass a Taiga URL directly -python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714 +python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714 # Or use " " syntax -python3 tools/taiga.py us 14128 -python3 tools/taiga.py task 13648 +python3 scripts/taiga.py us 14128 +python3 scripts/taiga.py task 13648 # Add --json for raw output -python3 tools/taiga.py --json issue 13714 +python3 scripts/taiga.py --json issue 13714 # See full usage -python3 tools/taiga.py --help +python3 scripts/taiga.py --help ``` ## URL Pattern Reference @@ -51,30 +51,30 @@ To extract the **type** and **ref** from a URL: ## Python Script Reference -The `tools/taiga.py` script wraps the Taiga API into a single convenient CLI +The `scripts/taiga.py` script wraps the Taiga API into a single convenient CLI with sensible defaults. ### Usage ``` -python3 tools/taiga.py -python3 tools/taiga.py -python3 tools/taiga.py [--json] -python3 tools/taiga.py [--json] +python3 scripts/taiga.py +python3 scripts/taiga.py +python3 scripts/taiga.py [--json] +python3 scripts/taiga.py [--json] ``` ### Examples ```bash # By URL (recommended — no need to think about type/ref) -python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714 +python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714 # By type and ref -python3 tools/taiga.py us 14128 -python3 tools/taiga.py task 13648 +python3 scripts/taiga.py us 14128 +python3 scripts/taiga.py task 13648 # Raw JSON output -python3 tools/taiga.py --json issue 13714 +python3 scripts/taiga.py --json issue 13714 ``` ### Output diff --git a/.opencode/skills/update-changelog/SKILL.md b/.opencode/skills/update-changelog/SKILL.md index 0a241216df..cf15eede73 100644 --- a/.opencode/skills/update-changelog/SKILL.md +++ b/.opencode/skills/update-changelog/SKILL.md @@ -20,7 +20,7 @@ primary link, with the fix PR inline on the same line. - `gh` CLI authenticated (`gh auth status`) - Python 3.8+ -- `tools/gh.py` helper script available +- `scripts/gh.py` helper script available ## Workflow @@ -36,13 +36,13 @@ Use the helper script. It uses GraphQL for efficient single-pass fetching ```bash # All closed issues (default) -python3 tools/gh.py issues "2.16.0" +python3 scripts/gh.py issues "2.16.0" # Include open issues too -python3 tools/gh.py issues "2.16.0" --state all +python3 scripts/gh.py issues "2.16.0" --state all # Exclude entries that should not go in the changelog -python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog" +python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" ``` **Exclusion rules (issue-level):** @@ -68,7 +68,7 @@ If updating from an existing `CHANGES.md`, find issues in the milestone that are NOT yet referenced in the changelog: ```bash -python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md +python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md ``` This returns a filtered JSON array with only the missing issues. @@ -85,23 +85,23 @@ community contribution attribution, or to read the PR body for ```bash # One or more PR numbers -python3 tools/gh.py prs 9179 9204 9311 +python3 scripts/gh.py prs 9179 9204 9311 # From a file -python3 tools/gh.py prs --file prs.txt +python3 scripts/gh.py prs --file prs.txt # From stdin -cat prs.txt | python3 tools/gh.py prs --stdin +cat prs.txt | python3 scripts/gh.py prs --stdin ``` The `prs` command also supports listing all PRs in a milestone in one call: ```bash # All merged PRs in a milestone (default) -python3 tools/gh.py prs --milestone "2.16.0" +python3 scripts/gh.py prs --milestone "2.16.0" # All states (merged, open, closed) -python3 tools/gh.py prs --milestone "2.16.0" --state all +python3 scripts/gh.py prs --milestone "2.16.0" --state all ``` The `prs` command returns JSON with `number`, `title`, `body`, `state`, @@ -113,13 +113,13 @@ You can also list all PRs in a milestone in a single call: ```bash # All merged PRs in a milestone (default) -python3 tools/gh.py prs --milestone "2.16.0" +python3 scripts/gh.py prs --milestone "2.16.0" # All states (merged, open, closed) -python3 tools/gh.py prs --milestone "2.16.0" --state all +python3 scripts/gh.py prs --milestone "2.16.0" --state all # Open PRs only -python3 tools/gh.py prs --milestone "2.16.0" --state open +python3 scripts/gh.py prs --milestone "2.16.0" --state open ``` The milestone path uses paginated GraphQL on the milestone's `pullRequests` @@ -155,7 +155,7 @@ The attribution should reference the **PR author**, not the issue author. The `prs` subcommand includes the `author` field — use that: ```bash -python3 tools/gh.py prs | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])" +python3 scripts/gh.py prs | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])" ``` Placement in the entry line: @@ -190,7 +190,7 @@ only reference **merged** PRs. Verify before writing: ```bash # Collect all PR numbers from the candidate entries and check them -python3 tools/gh.py prs | python3 -c " +python3 scripts/gh.py prs | python3 -c " import json, sys for pr in json.load(sys.stdin): if pr['state'] != 'MERGED': @@ -265,7 +265,7 @@ section) or in the candidate set for the current milestone, check: current milestone since the changelog was last updated (e.g., a fix arrived late and the issue was reassigned to a future milestone)? - Verify the issue is still in the current milestone via - `python3 tools/gh.py issues --state all`. If it's no + `python3 scripts/gh.py issues --state all`. If it's no longer there, remove the entry from the current section. (If the target section doesn't exist yet, the entry is simply dropped.) @@ -337,7 +337,7 @@ cross-reference to catch gaps: ```bash # List all merged PRs in the milestone -python3 tools/gh.py prs --milestone "" --state merged > /tmp/milestone-prs.json +python3 scripts/gh.py prs --milestone "" --state merged > /tmp/milestone-prs.json # Extract PR numbers from the changelog section python3 -c " @@ -378,7 +378,7 @@ changelog or is legitimately excluded (check its labels). Also verify that no closed-unmerged PRs remain in the changelog: ```bash -python3 tools/gh.py prs --milestone "" --state all | python3 -c " +python3 scripts/gh.py prs --milestone "" --state all | python3 -c " import json, sys data = json.load(sys.stdin) closed = [p for p in data if p['state'] == 'CLOSED'] @@ -483,13 +483,13 @@ def fmt_issue_list(nums): # --- Fetch milestone data --- result = subprocess.run( - ["python3", "tools/gh.py", "issues", MILESTONE, "--state", "all"], + ["python3", "scripts/gh.py", "issues", MILESTONE, "--state", "all"], capture_output=True, text=True) all_issues = json.loads(result.stdout) issue_by_num = {i['number']: i for i in all_issues} result = subprocess.run( - ["python3", "tools/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"], + ["python3", "scripts/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"], capture_output=True, text=True) all_prs = json.loads(result.stdout) pr_by_num = {p['number']: p for p in all_prs} @@ -728,7 +728,7 @@ self-contained and clickable in any Markdown viewer. reference if applicable. - **Re-fetch before editing.** Milestones can change — always re-fetch issues before making edits, don't rely on cached data. -- **Use `tools/gh.py`.** Prefer the helper script over raw `gh api` calls for +- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for milestone issue listing and PR detail fetching. It handles GraphQL pagination, batching, and label filtering automatically. - **Verify PR merge status.** Not all closing PRs are merged — community PRs @@ -739,7 +739,7 @@ self-contained and clickable in any Markdown viewer. labels. Check both. - **Cross-reference milestone PRs, not just issues.** The `--compare` flag on the `issues` command only compares issue numbers. Merged PRs not linked to - any milestone issue can be missed. Use `python3 tools/gh.py prs --milestone` + any milestone issue can be missed. Use `python3 scripts/gh.py prs --milestone` for a full PR cross-reference. - **False-positive PR-to-issue associations.** A PR may claim to close an issue from a different project or context. If the PR title and issue title diff --git a/.serena/memories/backend/core.md b/.serena/memories/backend/core.md index 15f0dd109d..6530f74308 100644 --- a/.serena/memories/backend/core.md +++ b/.serena/memories/backend/core.md @@ -39,8 +39,8 @@ Backend RPC command areas without focused memories include access tokens, binfil Database migrations live in `backend/src/app/migrations/`; pure SQL migrations are under `backend/src/app/migrations/sql/`. SQL filenames conventionally start with a sequence and verb/table description, e.g. `0026-mod-profile-table-add-is-active-field`. Applied migrations are tracked in the `migrations` table. -For interactive PostgreSQL access with correct dev defaults, use `tools/psql`; to dump -the current DDL schema, use `tools/db-schema` (see `mem:tools/psql`). +For interactive PostgreSQL access with correct dev defaults, use `scripts/psql`; to dump +the current DDL schema, use `scripts/db-schema` (see `mem:scripts/psql`). For deeper details on transaction semantics, advisory locks, Transit vs JSON helpers, and dev/test DB URLs: `mem:backend/rpc-db-worker-subtleties`. @@ -56,14 +56,14 @@ In devenv, backend nREPL is exposed on port 6064. ### Non-interactive eval (preferred for agents) -`./tools/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session--`. +`./scripts/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session--`. ```bash -./tools/nrepl-eval.mjs '(+ 1 2)' # single expression -./tools/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits -./tools/nrepl-eval.mjs -e # inspect last exception (*e) -./tools/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh -./tools/nrepl-eval.mjs <<'EOF' # multi-expression heredoc +./scripts/nrepl-eval.mjs '(+ 1 2)' # single expression +./scripts/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits +./scripts/nrepl-eval.mjs -e # inspect last exception (*e) +./scripts/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh +./scripts/nrepl-eval.mjs <<'EOF' # multi-expression heredoc (def x 10) (+ x 20) EOF @@ -92,12 +92,12 @@ Fixtures can populate local data for manual testing/perf work. From the backend IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. -* **Linting:** `pnpm run lint` from the repository root. -* **Formatting:** `pnpm run check-fmt`. Use `pnpm run fmt` to fix. Avoid unrelated whitespace diffs. +* **Linting:** `clj-kondo --lint ../common/src/ src/`. +* **Formatting:** `cljfmt check src/ test/` to check, `cljfmt fix src/ test/` to fix. Avoid unrelated whitespace diffs. **Before linting:** if delimiter errors are suspected (after LLM edits), run -`tools/paren-repair.bb` on the affected files first. Delimiter errors produce -misleading linter/compiler output. See `mem:tools/paren-repair`. +`scripts/paren-repair` on the affected files first. Delimiter errors produce +misleading linter/compiler output. See `mem:scripts/paren-repair`. ## Testing diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md index d291c81bf9..a286887ac2 100644 --- a/.serena/memories/critical-info.md +++ b/.serena/memories/critical-info.md @@ -22,8 +22,8 @@ You are working on the GitHub project `penpot/penpot`, a monorepo. - Align `let` binding values: when a `let` form has multiple bindings spanning several lines, align the value forms to the same column with spaces. - If you introduce delimiter errors (mismatched parens/brackets) in Clojure/CLJS files, - fix them with `tools/paren-repair.bb` BEFORE running lint/format checks. - See `mem:tools/paren-repair` for usage. + fix them with `scripts/paren-repair` BEFORE running lint/format checks. + See `mem:scripts/paren-repair` for usage. - Never run anything that destroys data without explicit permission, including `drop-devenv`, `docker compose down -v`, `docker volume rm ...`. The user's real work lives in the volumes of the shared infra. # Project modules @@ -54,18 +54,21 @@ module. You can read it from `mem:/core` # Dev tools -- `tools/nrepl-eval.mjs` — Evaluate Clojure/ClojureScript code via nREPL. +- `scripts/nrepl-eval.mjs` — Evaluate Clojure/ClojureScript code via nREPL. Supports `--backend` (port 6064) and `--frontend` (port 3447) aliases. - See `mem:tools/nrepl-eval`. -- `tools/paren-repair.bb` — Fix mismatched delimiters in Clojure/CLJS files + See `mem:scripts/nrepl-eval`. +- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files and reformat with cljfmt. Run before lint checks when LLM edits break parens. - See `mem:tools/paren-repair`. -- `tools/psql` — PostgreSQL client wrapper with devenv defaults. - Companion: `tools/db-schema` for DDL dumps. See `mem:tools/psql`. -- `tools/taiga.py` — Fetch public issues, user stories, and tasks from the - Penpot Taiga project without authentication. See `mem:tools/taiga`. -- `tools/gh.py` — GitHub operations helper: list milestone issues, fetch PR - details, compare against CHANGES.md. Requires `gh` CLI. See `mem:tools/gh`. + See `mem:scripts/paren-repair`. +- `scripts/psql` — PostgreSQL client wrapper with devenv defaults. + Companion: `scripts/db-schema` for DDL dumps. See `mem:scripts/psql`. +- `scripts/taiga.py` — Fetch public issues, user stories, and tasks from the + Penpot Taiga project without authentication. See `mem:scripts/taiga`. +- `scripts/gh.py` — GitHub operations helper: list milestone issues, fetch PR + details, compare against CHANGES.md. Requires `gh` CLI. See `mem:scripts/gh`. +- `scripts/error-reports.mjs` — Query error reports via RPC API with token + authentication. Supports list/get operations with filtering and pagination. + See `mem:scripts/error-reports`. # Dependency graph diff --git a/.serena/memories/frontend/core.md b/.serena/memories/frontend/core.md index 5f06f8cfb0..b229e2b124 100644 --- a/.serena/memories/frontend/core.md +++ b/.serena/memories/frontend/core.md @@ -27,9 +27,9 @@ From `frontend/`: - Translation formatting after i18n edits: `pnpm run translations`. **Before linting:** if delimiter errors are suspected (after LLM edits, or -lint/compiler reports syntax errors), run `tools/paren-repair.bb` on the +lint/compiler reports syntax errors), run `scripts/paren-repair` on the affected files first. Delimiter errors produce misleading linter output. -See `mem:tools/paren-repair`. +See `mem:scripts/paren-repair`. ## Focused memory routing diff --git a/.serena/memories/frontend/handling-errors-and-debugging.md b/.serena/memories/frontend/handling-errors-and-debugging.md index ede8c1581b..c44cb6b89e 100644 --- a/.serena/memories/frontend/handling-errors-and-debugging.md +++ b/.serena/memories/frontend/handling-errors-and-debugging.md @@ -11,10 +11,10 @@ The latter is needed because syntax errors in parentheses give an uninformative tool can often find the exact location of such errors. When delimiter errors are detected (typically from lint or compiler output), -fix the affected files with `tools/paren-repair.bb`. The `clj_check_parentheses` +fix the affected files with `scripts/paren-repair`. The `clj_check_parentheses` MCP tool can also pinpoint the error location when available, but it is not required — standard build errors are usually enough. -See `mem:tools/paren-repair`. +See `mem:scripts/paren-repair`. ## Runtime patching with `set!` diff --git a/.serena/memories/tools/gh.md b/.serena/memories/scripts/gh.md similarity index 67% rename from .serena/memories/tools/gh.md rename to .serena/memories/scripts/gh.md index 3f74b82b66..ed6adfa8f1 100644 --- a/.serena/memories/tools/gh.md +++ b/.serena/memories/scripts/gh.md @@ -1,6 +1,6 @@ # GitHub operations helper -`tools/gh.py` is a multi-purpose CLI for querying the penpot/penpot GitHub +`scripts/gh.py` is a multi-purpose CLI for querying the penpot/penpot GitHub repository via GraphQL and REST APIs through the authenticated `gh` CLI. ## When to use @@ -23,24 +23,24 @@ List issues in a milestone, with filtering by state, labels, and project status. ```bash # Closed issues in a milestone (default) -python3 tools/gh.py issues "2.16.0" +python3 scripts/gh.py issues "2.16.0" # All issues in a milestone -python3 tools/gh.py issues "2.16.0" --state all +python3 scripts/gh.py issues "2.16.0" --state all # Issues with no milestone -python3 tools/gh.py issues none -python3 tools/gh.py issues none --state open +python3 scripts/gh.py issues none +python3 scripts/gh.py issues none --state open # Filter by label (include only) -python3 tools/gh.py issues "2.16.0" --label "bug" -python3 tools/gh.py issues "2.16.0" --label "bug,regression" +python3 scripts/gh.py issues "2.16.0" --label "bug" +python3 scripts/gh.py issues "2.16.0" --label "bug,regression" # Exclude by label -python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog" +python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" # Show only issues NOT yet in CHANGES.md -python3 tools/gh.py issues "2.16.0" --compare CHANGES.md +python3 scripts/gh.py issues "2.16.0" --compare CHANGES.md ``` **Default filters** (override with flags): @@ -55,19 +55,19 @@ Fetch PR details by number or by milestone. ```bash # Fetch specific PRs -python3 tools/gh.py prs 9179 9204 9311 +python3 scripts/gh.py prs 9179 9204 9311 # Read PR numbers from file -python3 tools/gh.py prs --file prs.txt +python3 scripts/gh.py prs --file prs.txt # Read PR numbers from stdin -cat prs.txt | python3 tools/gh.py prs --stdin +cat prs.txt | python3 scripts/gh.py prs --stdin # All PRs in a milestone (default: merged only) -python3 tools/gh.py prs --milestone "2.16.0" +python3 scripts/gh.py prs --milestone "2.16.0" # All PRs in a milestone (all states) -python3 tools/gh.py prs --milestone "2.16.0" --state all +python3 scripts/gh.py prs --milestone "2.16.0" --state all ``` **Output**: JSON array to stdout; progress to stderr. diff --git a/.serena/memories/tools/nrepl-eval.md b/.serena/memories/scripts/nrepl-eval.md similarity index 66% rename from .serena/memories/tools/nrepl-eval.md rename to .serena/memories/scripts/nrepl-eval.md index f5ec3ab994..96915492fb 100644 --- a/.serena/memories/tools/nrepl-eval.md +++ b/.serena/memories/scripts/nrepl-eval.md @@ -1,7 +1,7 @@ # nREPL Eval Evaluate Clojure (or ClojureScript) code via a running nREPL server using -`tools/nrepl-eval.mjs` — a standalone CLI application. +`scripts/nrepl-eval.mjs` — a standalone CLI application. Session state (defs, in-ns, etc.) persists across invocations via a stored session ID, so you can build up state incrementally. @@ -9,9 +9,9 @@ session ID, so you can build up state incrementally. ## Usage ```bash -node tools/nrepl-eval.mjs [options] [] +node scripts/nrepl-eval.mjs [options] [] # or -./tools/nrepl-eval.mjs [options] [] +./scripts/nrepl-eval.mjs [options] [] ``` ## Options @@ -47,37 +47,37 @@ Sessions are persisted to `/tmp/penpot-nrepl-session--`. State carries across calls automatically: ```bash -./tools/nrepl-eval.mjs '(def x 42)' -./tools/nrepl-eval.mjs 'x' +./scripts/nrepl-eval.mjs '(def x 42)' +./scripts/nrepl-eval.mjs 'x' # => 42 ``` Reset the session to start fresh: ```bash -./tools/nrepl-eval.mjs --reset-session '(def x 0)' +./scripts/nrepl-eval.mjs --reset-session '(def x 0)' ``` ### Evaluate code **Single expression (inline) — uses default port 6064:** ```bash -./tools/nrepl-eval.mjs '(+ 1 2 3)' +./scripts/nrepl-eval.mjs '(+ 1 2 3)' ``` **Backend nREPL (explicit):** ```bash -./tools/nrepl-eval.mjs --backend '(+ 1 2 3)' +./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)' ``` **Frontend nREPL:** ```bash -./tools/nrepl-eval.mjs --frontend '(js/alert "hi")' +./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")' ``` **Multiple expressions via heredoc (recommended — avoids escaping issues):** ```bash -./tools/nrepl-eval.mjs <<'EOF' +./scripts/nrepl-eval.mjs <<'EOF' (def x 10) (+ x 20) EOF @@ -85,7 +85,7 @@ EOF **Override with a different port:** ```bash -./tools/nrepl-eval.mjs -p 7888 '(+ 1 2 3)' +./scripts/nrepl-eval.mjs -p 7888 '(+ 1 2 3)' ``` ### Inspect last exception @@ -93,26 +93,48 @@ EOF After code throws an error, retrieve the full exception details: ```bash -./tools/nrepl-eval.mjs -e +./scripts/nrepl-eval.mjs -e ``` ## Common Patterns **Require a namespace with reload:** ```bash -./tools/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)" +./scripts/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)" ``` **Test a function:** ```bash -./tools/nrepl-eval.mjs "(ns/my-function arg1 arg2)" +./scripts/nrepl-eval.mjs "(ns/my-function arg1 arg2)" ``` **Long-running operation with custom timeout:** ```bash -./tools/nrepl-eval.mjs -t 300000 "(long-running-fn)" +./scripts/nrepl-eval.mjs -t 300000 "(long-running-fn)" ``` +### Accessing Private Functions + +Private functions (declared with `^:private` or `defn-`) cannot be called +directly from outside their namespace. Use the var quote syntax `#'` to +access the underlying var: + +**This fails:** +```bash +./scripts/nrepl-eval.mjs "(app.rpc.commands.error-reports/build-list-query {})" +# => Syntax error: app.rpc.commands.error-reports/build-list-query is not public +``` + +**This works:** +```bash +./scripts/nrepl-eval.mjs "(#'app.rpc.commands.error-reports/build-list-query {})" +# => Returns the result +``` + +The `#'` reader macro resolves to `(var ...)`, giving you direct access to +the var regardless of its visibility modifier. The syntax is `#'` followed +by the fully qualified symbol. + ## Key Principles - **Default port is 6064** — just pass code directly, no `-p` needed when diff --git a/.serena/memories/scripts/paren-repair.md b/.serena/memories/scripts/paren-repair.md new file mode 100644 index 0000000000..162e6d28d8 --- /dev/null +++ b/.serena/memories/scripts/paren-repair.md @@ -0,0 +1,43 @@ +# Paren-Repair + +`scripts/paren-repair` fixes mismatched parentheses, brackets, and braces in +Clojure/ClojureScript files, then reformats them with cljfmt. + +## When to use + +- After LLM edits introduce broken delimiters — proactively run it on files + you just touched. +- When lint (clj-kondo), the Clojure compiler, or shadow-cljs report syntax + errors mentioning mismatched/unclosed delimiters, reader errors, or + unexpected EOF. +- Before running lint/format checks — delimiter errors make linter output + misleading. Fix them first, then lint. + +## How to use (CLI) + +```bash +# File mode (in-place fix + format) +bb scripts/paren-repair path/to/file.clj + +# Pipe mode (stdin → fixed code to stdout) +echo '(def x 1' | bb scripts/paren-repair + +# Help +bb scripts/paren-repair --help +``` +`bb` must be invoked from the repo root so the path `scripts/paren-repair` resolves. + +## Native Tool Available (opencode) + +A native opencode tool `paren-repair` is available at `.opencode/tools/paren-repair.ts`. +The LLM can call it directly with: +- `files`: Array of file paths to fix +- `code`: Code string to fix via stdin + +Example usage by the LLM: +``` +paren-repair(files="src/foo.clj, src/bar.cljs") +paren-repair(code="(defn foo [x") +``` + + diff --git a/.serena/memories/scripts/psql.md b/.serena/memories/scripts/psql.md new file mode 100644 index 0000000000..c969fd6742 --- /dev/null +++ b/.serena/memories/scripts/psql.md @@ -0,0 +1,39 @@ +# Psql + +`scripts/psql` is a wrapper around `psql` that connects to the Penpot PostgreSQL +database using environment variables (`PENPOT_DB_HOST`, `PENPOT_DB_USER`, +`PENPOT_DB_PASSWORD`, `PENPOT_DB_NAME`) with sensible defaults for local +development. + +## When to use + +- Running ad-hoc SQL queries against the Penpot database. +- Inspecting schema, migrations, or data during development or debugging. + +## How to use (CLI) + +```bash +# Default connection (penpot db, localhost) +scripts/psql -c "SELECT version();" + +# Test database +scripts/psql --test -c "SELECT * FROM migrations;" + +# Custom host/user/database +scripts/psql --host myhost --user myuser --db mydb +``` + +`scripts/psql` must be invoked from the repo root so the path resolves. + +## Native Tool Available (opencode) + +A native opencode tool `penpot-psql` is available. The LLM can call it directly +with: +- `sql`: SQL command string to execute +- `test`: Boolean flag to use the `penpot_test` database + +Example usage by the LLM: +``` +penpot-psql(sql="SELECT version();") +penpot-psql(sql="SELECT * FROM migrations;", test=true) +``` diff --git a/.serena/memories/tools/taiga.md b/.serena/memories/scripts/taiga.md similarity index 69% rename from .serena/memories/tools/taiga.md rename to .serena/memories/scripts/taiga.md index b4e623e3e1..d68274fde5 100644 --- a/.serena/memories/tools/taiga.md +++ b/.serena/memories/scripts/taiga.md @@ -1,6 +1,6 @@ # Taiga API client -`tools/taiga.py` fetches public issues, user stories, and tasks from the +`scripts/taiga.py` fetches public issues, user stories, and tasks from the Penpot Taiga project (id 345963) without authentication. ## When to use @@ -13,16 +13,16 @@ Penpot Taiga project (id 345963) without authentication. ```bash # Fetch by full Taiga URL -python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714 +python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714 # Fetch by type and ref number -python3 tools/taiga.py issue 13714 -python3 tools/taiga.py us 14128 -python3 tools/taiga.py task 13648 +python3 scripts/taiga.py issue 13714 +python3 scripts/taiga.py us 14128 +python3 scripts/taiga.py task 13648 # Output raw JSON instead of formatted summary -python3 tools/taiga.py --json issue 13714 -python3 tools/taiga.py --json https://tree.taiga.io/project/penpot/us/14128 +python3 scripts/taiga.py --json issue 13714 +python3 scripts/taiga.py --json https://tree.taiga.io/project/penpot/us/14128 ``` ## Supported types diff --git a/.serena/memories/tools/paren-repair.md b/.serena/memories/tools/paren-repair.md deleted file mode 100644 index e3817ca839..0000000000 --- a/.serena/memories/tools/paren-repair.md +++ /dev/null @@ -1,29 +0,0 @@ -# Paren-Repair - -`tools/paren-repair.bb` fixes mismatched parentheses, brackets, and braces in -Clojure/ClojureScript files, then reformats them with cljfmt. - -## When to use - -- After LLM edits introduce broken delimiters — proactively run it on files - you just touched. -- When lint (clj-kondo), the Clojure compiler, or shadow-cljs report syntax - errors mentioning mismatched/unclosed delimiters, reader errors, or - unexpected EOF. -- Before running lint/format checks — delimiter errors make linter output - misleading. Fix them first, then lint. - -## How to use - -```bash -# File mode (in-place fix + format) -bb tools/paren-repair.bb path/to/file.clj - -# Pipe mode (stdin → fixed code to stdout) -echo '(def x 1' | bb tools/paren-repair.bb - -# Help -bb tools/paren-repair.bb --help -``` - -`bb` must be invoked from the repo root so the path `tools/paren-repair.bb` resolves. diff --git a/.serena/memories/tools/psql.md b/.serena/memories/tools/psql.md deleted file mode 100644 index c502b4c1e9..0000000000 --- a/.serena/memories/tools/psql.md +++ /dev/null @@ -1,51 +0,0 @@ -# PostgreSQL client wrapper - -`tools/psql` is a wrapper around the `psql` command with defaults preconfigured -for the Penpot development environment. - -## When to use - -- Running SQL queries against the dev database (`penpot`) or test database - (`penpot_test`). -- Inspecting table structures, running migrations manually, or debugging - database state. -- Any time you need PostgreSQL access and want the correct host/user/password - without typing them each time. - -## How to use - -```bash -# Interactive session (penpot database) -tools/psql - -# Interactive session (penpot_test database) -tools/psql --test - -# Inline query (penpot) -tools/psql -c "SELECT 1" - -# Inline query (penpot_test) -tools/psql --test -c "SELECT 1" - -# Override defaults -tools/psql -h other-host -U other-user -d other-db - -# Pipe SQL from a file -tools/psql -f some-query.sql -``` - -All standard `psql` flags are passed through after the wrapper's own flags. - -## Defaults - -| Setting | Default | Env override | -|----------|-----------|---------------------| -| Host | `postgres` | `PENPOT_DB_HOST` | -| User | `penpot` | `PENPOT_DB_USER` | -| Password | `penpot` | `PENPOT_DB_PASSWORD` | -| Database | `penpot` | `PENPOT_DB_NAME` | - -## See also - -`tools/db-schema` — a companion script that dumps the current DDL schema -using `pg_dump --schema-only`, with the same defaults and `--test` flag. diff --git a/.serena/memories/workflow/creating-prs.md b/.serena/memories/workflow/creating-prs.md index c0f763f9e3..00252a7661 100644 --- a/.serena/memories/workflow/creating-prs.md +++ b/.serena/memories/workflow/creating-prs.md @@ -4,10 +4,10 @@ PR only on explicit request. Branch: issue/feature-specific; fallback `/ /tmp/pr-body.md << 'PR_BODY' PR_BODY -TARGET=$(tools/detect-target-branch) +TARGET=$(scripts/detect-target-branch) gh pr create \ --repo penpot/penpot \ diff --git a/AGENTS.md b/AGENTS.md index 8c3e6ba431..b6aae1f32d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AI AGENT GUIDE -## Hard rules (always apply — no exceptions) +## HARD RULES (always apply — no exceptions) - **Never `git push`, force-push, or modify `git origin`** (or any other remote). The user pushes from their own shell. If a push is required to surface the @@ -92,3 +92,21 @@ precision while maintaining a strong focus on maintainability and performance. down into atomic steps. 2. Be concise and autonomous. 3. Do **not** touch unrelated modules unless the task explicitly requires it. + +--- + +# Available Scripts & Tools + +## Native opencode Tools (callable directly by the LLM) + +- `paren-repair` — Fix mismatched delimiters + reformat Clojure files. Example: `paren-repair(files="src/foo.clj, src/bar.cljs")` +- `penpot-psql` — Execute SQL against the Penpot database. Example: `penpot-psql(sql="SELECT version();")` + +## Scripts (from repo root via `scripts/`) + +- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files + reformat with cljfmt. See `mem:scripts/paren-repair`. +- `scripts/psql` — Connect to the Penpot PostgreSQL database (wraps `psql` with env-var defaults). See `mem:scripts/psql`. +- `scripts/nrepl-eval.mjs` — Evaluate Clojure code via nREPL (backend + frontend). +- `scripts/check-commit` — Validate commit messages against Penpot's commit guidelines. +- `scripts/check-fmt-clj` — Check Clojure formatting without modifying files. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12729395d4..63b931900f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,9 @@ Center](https://help.penpot.app/). - [Reporting Bugs](#reporting-bugs) - [Pull Requests](#pull-requests) - [Workflow](#workflow) - - [Title format](#title-format) - - [Description](#description) - - [Branch naming](#branch-naming) + - [Format](#format) + - [Title format](#title-format) + - [Description](#description) - [Review process](#review-process) - [What we won't accept](#what-we-wont-accept) - [Good first issues](#good-first-issues) @@ -73,35 +73,23 @@ Advisories](https://github.com/penpot/penpot/security/advisories) 4. **Format and lint** — run the checks described in [Formatting and Linting](#formatting-and-linting) before submitting. -### Title format +### Format + +#### Title + +> **IMPORTANT:** When a PR is squash-merged, the PR title becomes the +> commit message on the main branch. Getting the title right matters. Pull request titles **must** follow the same convention as commit subjects: ``` -:emoji: +:emoji: Subject line (imperative, capitalized, no period, <=70 chars) ``` -- Use the **imperative mood** (e.g. "Fix", not "Fixed"). -- Capitalize the first letter of the subject. -- Do not end the subject with a period. -- Keep the subject to **70 characters** or fewer. -- Use one of the [commit type emojis](#commit-types) listed below. +Read [Creating Commits](./.serena/memories/workflow/creating-commits.md) +for more concrete information. -When a PR contains multiple unrelated commits, choose the emoji that -best represents the dominant change. - -**Examples:** - -``` -:bug: Fix unexpected error on launching modal -:sparkles: Enable new modal for profile -:zap: Improve performance of dashboard navigation -``` - -> **Note:** When a PR is squash-merged, the PR title becomes the -> commit message on the main branch. Getting the title right matters. - -### Description +#### Description Every pull request should include a description that helps reviewers understand the change quickly: @@ -114,24 +102,8 @@ understand the change quickly: 5. **Breaking changes** — call out anything that affects existing users or requires migration steps. -### Branch naming - -Use a descriptive branch name that reflects the type and scope of the -change: - -``` -/ -``` - -Types: `fix`, `feat`, `refactor`, `docs`, `chore`, `perf`. - -Optionally include the issue number: - -``` -fix/9122-email-blacklisting -feat/export-webp -refactor/layout-sizing -``` +Read [Creating PRs](./.serena/memories/workflow/creating-prs.md) +for more concrete information. ### Review process @@ -151,10 +123,10 @@ refactor/layout-sizing To save time on both sides, please avoid submitting PRs that: - Introduce new dependencies without prior discussion. -- Change the build system or CI configuration without maintainer - approval. -- Mix unrelated changes in a single PR — keep PRs focused on one - concern. +- Change the build system or CI configuration without maintainer approval. +- Mix unrelated changes in a single PR — keep PRs focused on one concern. +- Submit AI-generated code without human review. +- Skip local syntax and formatting checks before submitting. - Skip the [discussion step](#workflow) for non-bug-fix changes. ### Good first issues @@ -217,36 +189,8 @@ Commit messages must follow this format: ## Formatting and Linting -We use [cljfmt](https://github.com/weavejester/cljfmt) for formatting and -[clj-kondo](https://github.com/clj-kondo/clj-kondo) for linting. - -```bash -# Check formatting (does not modify files) -./scripts/check-fmt - -# Fix formatting (modifies files in place) -./scripts/fmt - -# Lint -./scripts/lint -``` - -For frontend SCSS, we use `stylelint` for linting and -`Prettier` for formatting: - -```bash -cd frontend - -# Lint SCSS -pnpm run lint:scss (does not modify files) - -# Fix SCSS formatting (modifies files in place) -pnpm run fmt:scss -``` - -Ideally, run these as git pre-commit hooks. -[Husky](https://typicode.github.io/husky/#/) is a convenient option for -setting this up. +Each module has its own linting and formatting commands — see the relevant one on the +[Serena Memories](./.serena/memories/) ## Changelog diff --git a/package.json b/package.json index 60e85d0149..256ffc3436 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "penpot", - "version": "1.20.0", + "version": "2.17.0", "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, @@ -10,11 +10,6 @@ "url": "https://github.com/penpot/penpot" }, "type": "module", - "scripts": { - "lint": "./scripts/lint", - "check-fmt": "./scripts/check-fmt", - "fmt": "./scripts/fmt" - }, "devDependencies": { "@playwright/mcp": "^0.0.76", "@playwright/test": "^1.61.1", diff --git a/scripts/attach-opencode b/scripts/attach-opencode deleted file mode 100755 index ddf43bb828..0000000000 --- a/scripts/attach-opencode +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e; - -PORT=14180 -while [[ $# -gt 0 ]]; do - case "$1" in - --port|-p) PORT="$2"; shift 2 ;; - *) echo "Unknown option: $1"; exit 1 ;; - esac -done - -pnpm exec opencode attach "http://localhost:${PORT}"; diff --git a/scripts/check-fmt b/scripts/check-fmt-clj similarity index 100% rename from scripts/check-fmt rename to scripts/check-fmt-clj diff --git a/tools/db-schema b/scripts/db-schema similarity index 100% rename from tools/db-schema rename to scripts/db-schema diff --git a/tools/detect-target-branch b/scripts/detect-target-branch similarity index 93% rename from tools/detect-target-branch rename to scripts/detect-target-branch index 5d3d584552..65ff49c0ca 100755 --- a/tools/detect-target-branch +++ b/scripts/detect-target-branch @@ -12,8 +12,8 @@ set -euo pipefail # branch shares its tip commit with the target (not yet diverged). # # Usage: -# tools/detect-target-branch # print branch name -# tools/detect-target-branch --verbose # print "branch~N" +# scripts/detect-target-branch # print branch name +# scripts/detect-target-branch --verbose # print "branch~N" # # Exit status: # 0 — target found diff --git a/scripts/fmt b/scripts/fmt deleted file mode 100755 index 3de70a2709..0000000000 --- a/scripts/fmt +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -cljfmt --parallel=true fix \ - common/src/ \ - common/test/ \ - frontend/src/ \ - frontend/test/ \ - backend/src/ \ - backend/test/ \ - exporter/src/ \ - library/src; diff --git a/tools/gh.py b/scripts/gh.py similarity index 96% rename from tools/gh.py rename to scripts/gh.py index 51779cefb3..7017389d52 100755 --- a/tools/gh.py +++ b/scripts/gh.py @@ -9,20 +9,20 @@ Subcommands: prs Fetch details for one or more PRs (by number or milestone) Usage: - python3 tools/gh.py issues (default: state=closed) - python3 tools/gh.py issues "2.16.0" --state all - python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog" - python3 tools/gh.py issues "2.16.0" --label "bug" (include only issues with label) - python3 tools/gh.py issues "2.16.0" --label "bug,regression" --exclude "no changelog" - python3 tools/gh.py issues "2.16.0" --compare CHANGES.md - python3 tools/gh.py issues none (issues with no milestone) - python3 tools/gh.py issues none --label "enhancement" - python3 tools/gh.py issues none --state open - python3 tools/gh.py prs 9179 9204 9311 - python3 tools/gh.py prs --file prs.txt - cat prs.txt | python3 tools/gh.py prs --stdin - python3 tools/gh.py prs --milestone "2.16.0" (default: state=merged) - python3 tools/gh.py prs --milestone "2.16.0" --state all + python3 scripts/gh.py issues (default: state=closed) + python3 scripts/gh.py issues "2.16.0" --state all + python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" + python3 scripts/gh.py issues "2.16.0" --label "bug" (include only issues with label) + python3 scripts/gh.py issues "2.16.0" --label "bug,regression" --exclude "no changelog" + python3 scripts/gh.py issues "2.16.0" --compare CHANGES.md + python3 scripts/gh.py issues none (issues with no milestone) + python3 scripts/gh.py issues none --label "enhancement" + python3 scripts/gh.py issues none --state open + python3 scripts/gh.py prs 9179 9204 9311 + python3 scripts/gh.py prs --file prs.txt + cat prs.txt | python3 scripts/gh.py prs --stdin + python3 scripts/gh.py prs --milestone "2.16.0" (default: state=merged) + python3 scripts/gh.py prs --milestone "2.16.0" --state all Prerequisites: - gh CLI authenticated (gh auth status) diff --git a/scripts/lint b/scripts/lint deleted file mode 100755 index 4ab59aed13..0000000000 --- a/scripts/lint +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -clj-kondo --parallel=true --lint common/src; -clj-kondo --parallel=true --lint frontend/src; -clj-kondo --parallel=true --lint backend/src; -clj-kondo --parallel=true --lint exporter/src/; -clj-kondo --parallel=true --lint library/src; diff --git a/tools/nrepl-eval.mjs b/scripts/nrepl-eval.mjs similarity index 100% rename from tools/nrepl-eval.mjs rename to scripts/nrepl-eval.mjs diff --git a/tools/paren-repair.bb b/scripts/paren-repair similarity index 93% rename from tools/paren-repair.bb rename to scripts/paren-repair index aba0ee26ac..d00930dcc1 100755 --- a/tools/paren-repair.bb +++ b/scripts/paren-repair @@ -323,27 +323,14 @@ success-count (count successes) failure-count (count failures)] - ;; Print results - (println) - (println "paren-repair Results") - (println "========================") - (println) - - (doseq [{:keys [file-path message delimiter-fixed formatted]} results] - (let [tags (when (or delimiter-fixed formatted) - (str " [" - (string/join ", " - (filter some? - [(when delimiter-fixed "delimiter-fixed") - (when formatted "formatted")])) - "]"))] - (println (str " " file-path ": " message tags)))) - - (println) - (println "Summary:") - (println " Success:" success-count) - (println " Failed: " failure-count) - (println) + (doseq [{:keys [file-path success message delimiter-fixed formatted]} results] + (let [status (cond + (not success) (str "error: " message) + (and delimiter-fixed formatted) "delimiter-fixed, formatted" + delimiter-fixed "delimiter-fixed" + formatted "formatted" + :else "no-changes")] + (println (str file-path ": " status)))) (if (zero? failure-count) (System/exit 0) diff --git a/tools/psql b/scripts/psql similarity index 100% rename from tools/psql rename to scripts/psql diff --git a/scripts/start-opencode b/scripts/start-opencode deleted file mode 100755 index 5a7bb34f39..0000000000 --- a/scripts/start-opencode +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -set -e; -pnpm exec opencode; diff --git a/scripts/start-opencode-server b/scripts/start-opencode-server deleted file mode 100755 index b173f24cba..0000000000 --- a/scripts/start-opencode-server +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -set -e; -pnpm exec opencode serve --hostname 0.0.0.0 --port 14180; diff --git a/tools/taiga.py b/scripts/taiga.py similarity index 95% rename from tools/taiga.py rename to scripts/taiga.py index 1e96364a31..78bce5ccbd 100755 --- a/tools/taiga.py +++ b/scripts/taiga.py @@ -4,15 +4,15 @@ Taiga API client — fetch public issues, user stories, and tasks from the Penpot project (id 345963) without authentication. Usage: - python3 tools/taiga.py - python3 tools/taiga.py - python3 tools/taiga.py [--json] - python3 tools/taiga.py [--json] + python3 scripts/taiga.py + python3 scripts/taiga.py + python3 scripts/taiga.py [--json] + python3 scripts/taiga.py [--json] Examples: - python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714 - python3 tools/taiga.py --json https://tree.taiga.io/project/penpot/us/14128 - python3 tools/taiga.py task 13648 + python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714 + python3 scripts/taiga.py --json https://tree.taiga.io/project/penpot/us/14128 + python3 scripts/taiga.py task 13648 """ import argparse