Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-07-22 09:18:25 +02:00
commit 1c917951b6
51 changed files with 2930 additions and 608 deletions

View File

@ -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

3
.gitignore vendored
View File

@ -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

299
.opencode/plugins/penpot.js Normal file
View File

@ -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,
// },
// }
// }

View File

@ -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 <commit-sha>
# Get the full diff (including new/deleted files)
git show <commit-sha>
# Capture the original commit message for later reuse
git log --format='%B' -1 <commit-sha>
```
### 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 <module>/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 <path>` |
| Rename/move file | `bash mv <old> <new>`, 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

View File

@ -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] [<code>]
./scripts/nrepl-eval.mjs [options] [<code>]
```
| 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
```

View File

@ -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 14 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 (37 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/<descriptive-name>.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.

View File

@ -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 "<type> <ref>" 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 <taiga-url>
python3 tools/taiga.py <type> <ref>
python3 tools/taiga.py [--json] <taiga-url>
python3 tools/taiga.py [--json] <type> <ref>
python3 scripts/taiga.py <taiga-url>
python3 scripts/taiga.py <type> <ref>
python3 scripts/taiga.py [--json] <taiga-url>
python3 scripts/taiga.py [--json] <type> <ref>
```
### 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

View File

@ -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 <PR_NUMBER> | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])"
python3 scripts/gh.py prs <PR_NUMBER> | 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 <ALL_PR_NUMBERS> | python3 -c "
python3 scripts/gh.py prs <ALL_PR_NUMBERS> | 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 <MILESTONE> --state all`. If it's no
`python3 scripts/gh.py issues <MILESTONE> --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 "<MILESTONE>" --state merged > /tmp/milestone-prs.json
python3 scripts/gh.py prs --milestone "<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 "<MILESTONE>" --state all | python3 -c "
python3 scripts/gh.py prs --milestone "<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

View File

@ -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-<host>-<port>`.
`./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-<host>-<port>`.
```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

View File

@ -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:<MODULE>/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

View File

@ -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

View File

@ -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!`

View File

@ -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.

View File

@ -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] [<code>]
node scripts/nrepl-eval.mjs [options] [<code>]
# or
./tools/nrepl-eval.mjs [options] [<code>]
./scripts/nrepl-eval.mjs [options] [<code>]
```
## Options
@ -47,37 +47,37 @@ Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. 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

View File

@ -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")
```

View File

@ -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)
```

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -4,10 +4,10 @@ PR only on explicit request. Branch: issue/feature-specific; fallback `<type>/<s
## Target Branch
Auto-detect the base branch with `tools/detect-target-branch`:
Auto-detect the base branch with `scripts/detect-target-branch`:
```bash
TARGET=$(tools/detect-target-branch)
TARGET=$(scripts/detect-target-branch)
```
This outputs `staging` or `develop` by walking the local commit graph (pure local, no remote/network). Do not ask the user for the target branch unless the tool fails.
@ -83,7 +83,7 @@ cat > /tmp/pr-body.md << 'PR_BODY'
<body content here>
PR_BODY
TARGET=$(tools/detect-target-branch)
TARGET=$(scripts/detect-target-branch)
gh pr create \
--repo penpot/penpot \

View File

@ -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/<name>`)
- `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.

View File

@ -45,7 +45,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))
@ -57,7 +57,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))
@ -65,6 +64,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
@ -180,6 +180,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

View File

@ -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: <subject>
: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:
```
<type>/<short-description>
```
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

View File

@ -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)))

View File

@ -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))))))

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -48,21 +48,22 @@ desc: Learn how to import and export files in Penpot, the free, open-source desi
<figure><img src="/img/import-export/import-selection.webp" alt="Import penpot file" /></figure>
<h2 id="penpot-formats">Penpot file format</h2>
<p>Penpot export to a unique format that streamline the import and export of files and assets by being more efficient and interoperable.</p>
<p>Penpot exports to a unique format that streamlines the import and export of files and assets by being more efficient and interoperable.</p>
<p>Unlike other design tools, <strong>Penpot's format is built on standard languages</strong>. 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.</p>
<p>For a detailed look at what's inside a .penpot file, see <a href="/user-guide/export-import/penpot-file-format/">The .penpot file format</a>. Developers can also check the <a href="/technical-guide/developer/data-model/penpot-file-format/">technical specification</a>.</p>
<h3>Deprecated Penpot file formats</h3>
<p class="advice">These formats can only be exported from version 2.3 or earlier versions, but can be imported to any Penpot version.</p>
<p>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.</p>
<h4>[Deprecated] Penpot file (.penpot).</h4>
<h4>[Deprecated] Binary file (.penpot) — v1</h4>
<p>The fast one. Binary Penpot specific.</p>
<ul>
<li>✅ Highly efficient in terms of memory and transfer time when exporting and importing.</li>
<li>❌ It can be opened only in Penpot.</li>
<li>❌ Not transparent, code difficult to explore.</li>
</ul>
<h4>[Deprecated] Standard file (.zip).</h4>
<p>The open one. A compressed file that includes SVG and JSON.</p>
<h4>[Deprecated] Standard file (.zip) — v2 (never released)</h4>
<p>The open one. A compressed file that includes SVG and JSON. This format was developed but never released to users.</p>
<ul>
<li>✅ Allows the file to be opened by other softwares (still, for those cases export to SVG seems to be the common practice).</li>
<li>✅ Allows some automations and integrations.</li>

View File

@ -13,7 +13,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
<p>How to export and import your Penpot files</p>
</a>
</li>
<li>
<li>
<a href="/user-guide/export-import/penpot-file-format/">
<h2>The .penpot file format →</h2>
<p>Understand what's inside a .penpot file and how to inspect it</p>
</a>
</li>
<li>
<a href="/user-guide/export-import/exporting-layers/">
<h2>Exporting layers →</h2>
<p>How to export elements from your design into different file formats</p>

View File

@ -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.
---
<h1 id="penpot-file-format">The .penpot file format</h1>
<p class="main-paragraph">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.</p>
<h2 id="what-is-penpot-format">What is a .penpot file?</h2>
<p>A <code class="language-text">.penpot</code> file is a <strong>ZIP archive</strong> containing:</p>
<ul>
<li><strong>JSON metadata</strong> — human-readable descriptions of your pages, shapes, colors, components, and more</li>
<li><strong>Binary media files</strong> — images, fonts, and other assets used in your design</li>
</ul>
<p>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.</p>
<h2 id="whats-inside">What's inside a .penpot file?</h2>
<p>When you unzip a .penpot file, you'll find this structure:</p>
<figure>
<pre class="language-text"><code>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</code></pre>
</figure>
<h3>manifest.json</h3>
<p>The manifest is the table of contents. It tells you:</p>
<ul>
<li>Which version of the format is used</li>
<li>Which Penpot version created the file</li>
<li>What files are included</li>
<li>What features are enabled</li>
</ul>
<h3>File data</h3>
<p>The <code class="language-text">files/</code> directory contains all your design data organized by type:</p>
<ul>
<li><strong>Pages</strong> — Each page is split into individual shape files for efficiency</li>
<li><strong>Colors</strong> — Your color library with hex values, gradients, or image fills</li>
<li><strong>Components</strong> — Reusable component definitions</li>
<li><strong>Typographies</strong> — Text style definitions</li>
<li><strong>Tokens</strong> — Design tokens organized in sets and themes</li>
<li><strong>Media</strong> — References to images and other assets</li>
</ul>
<h3>Objects</h3>
<p>The <code class="language-text">objects/</code> directory contains the actual binary files (PNG, JPG, SVG) referenced by your design. Each object has a JSON metadata file alongside the binary file.</p>
<h2 id="how-to-inspect">How to inspect a .penpot file</h2>
<p>Since .penpot files are just ZIP archives, you can inspect them with standard tools.</p>
<h3>On macOS or Linux</h3>
<p><strong>1. List the contents without extracting:</strong></p>
<pre class="language-bash"><code>unzip -l your-design.penpot</code></pre>
<p><strong>2. Extract to a temporary folder:</strong></p>
<pre class="language-bash"><code>unzip your-design.penpot -d /tmp/penpot-inspect</code></pre>
<p><strong>3. View the manifest:</strong></p>
<pre class="language-bash"><code>cat /tmp/penpot-inspect/manifest.json | jq .</code></pre>
<p><strong>4. Browse the structure:</strong></p>
<pre class="language-bash"><code>tree /tmp/penpot-inspect</code></pre>
<p><strong>5. View a specific shape:</strong></p>
<pre class="language-bash"><code>cat /tmp/penpot-inspect/files/*/pages/*/*.json | jq .</code></pre>
<h3>On Windows</h3>
<p><strong>1. Right-click the .penpot file</strong> and select "Extract All..." or use 7-Zip.</p>
<p><strong>2. Browse the extracted folder</strong> to see the structure.</p>
<p><strong>3. Open any JSON file</strong> with a text editor or use a JSON viewer tool.</p>
<h3>Using AI tools</h3>
<p>You can use the <a href="/mcp/">Penpot MCP server</a> 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.</p>
<h2 id="versioning">Format versions</h2>
<p>The .penpot format has evolved over time:</p>
<table>
<thead>
<tr>
<th>Version</th>
<th>Description</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>v3</strong></td>
<td>ZIP + JSON format (current)</td>
<td>✅ Current</td>
</tr>
<tr>
<td>v1</td>
<td>Custom binary format</td>
<td>⚠️ Deprecated</td>
</tr>
</tbody>
</table>
<p class="advice">All current Penpot versions export in v3 format. Old v1 files can still be imported, but new exports will use v3.</p>
<h3>Version numbers inside the file</h3>
<p>Inside a .penpot file, you'll see two different version numbers:</p>
<ul>
<li><strong>Format version</strong> (in <code class="language-text">manifest.json</code>) — Tracks changes to the ZIP structure itself. Currently <code class="language-text">1</code>.</li>
<li><strong>Data version</strong> (in each file's JSON) — Tracks changes to the data model. This number increases as Penpot evolves.</li>
</ul>
<h3>Feature flags</h3>
<p>Files also include a list of <strong>feature flags</strong> that indicate which capabilities are used. For example:</p>
<ul>
<li><code class="language-text">design-tokens/v1</code> — Uses design tokens</li>
<li><code class="language-text">components/v2</code> — Uses the v2 component system</li>
<li><code class="language-text">variants/v1</code> — Uses component variants</li>
<li><code class="language-text">layout/grid</code> — Uses grid layouts</li>
</ul>
<p>These flags help Penpot know what features need to be supported when importing the file.</p>
<h2 id="for-developers">For developers</h2>
<p>If you're building tools or integrations that work with .penpot files, the <a href="/technical-guide/developer/data-model/penpot-file-format/">complete technical specification</a> provides detailed schema definitions for every JSON file in the archive.</p>
<p>Key resources:</p>
<ul>
<li><a href="/technical-guide/developer/data-model/penpot-file-format/">Technical specification</a> — Complete schema reference</li>
<li><a href="/technical-guide/developer/data-model/penpot-file-inspector/">File inspector</a> — Browser-based tool to interactively explore a .penpot file</li>
<li><a href="/mcp/">MCP Server</a> — AI-powered file inspection and manipulation</li>
<li><a href="/technical-guide/developer/data-model/">Data model</a> — Conceptual overview of Penpot's data structures</li>
</ul>

View File

@ -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

View File

@ -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))))

View File

@ -1266,7 +1266,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

View File

@ -198,10 +198,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]
@ -296,6 +293,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])))

View File

@ -2593,7 +2593,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)

View File

@ -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)

View File

@ -1,25 +1,22 @@
{
"name": "penpot",
"version": "1.20.0",
"version": "2.17.0",
"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"
},
"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",
"@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"
}
}

195
pnpm-lock.yaml generated
View File

@ -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:

View File

@ -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}";

View File

@ -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

View File

@ -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;

View File

@ -9,20 +9,20 @@ Subcommands:
prs Fetch details for one or more PRs (by number or milestone)
Usage:
python3 tools/gh.py issues <milestone-title> (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 <milestone-title> (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)

View File

@ -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;

View File

@ -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)

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -e;
pnpm exec opencode;

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -e;
pnpm exec opencode serve --hostname 0.0.0.0 --port 14180;

View File

@ -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 <taiga-url>
python3 tools/taiga.py <type> <ref>
python3 tools/taiga.py [--json] <taiga-url>
python3 tools/taiga.py [--json] <type> <ref>
python3 scripts/taiga.py <taiga-url>
python3 scripts/taiga.py <type> <ref>
python3 scripts/taiga.py [--json] <taiga-url>
python3 scripts/taiga.py [--json] <type> <ref>
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