mirror of
https://github.com/penpot/penpot.git
synced 2026-07-23 06:28:14 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
1c917951b6
13
.github/workflows/tests-backend.yml
vendored
13
.github/workflows/tests-backend.yml
vendored
@ -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
3
.gitignore
vendored
@ -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
299
.opencode/plugins/penpot.js
Normal 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,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
@ -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
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -56,7 +56,10 @@ and only weave in Penpot context when it is clearly relevant.
|
||||
- Ask clarifying questions if the intent is unclear or if critical information
|
||||
is missing (e.g. target model, expected output format, tone, constraints).
|
||||
Keep questions concise and grouped. Prefer to ask 1–4 questions at once
|
||||
rather than one at a time.
|
||||
rather than one at a time. **Use the `question` tool** to ask them so the
|
||||
user gets a structured multi-choice UI; reserve a plain `## Clarifying
|
||||
questions` markdown section for cases where the `question` tool is
|
||||
unavailable or the question is genuinely open-ended.
|
||||
- Rewrite the prompt using prompt-engineering best practices (see below).
|
||||
- Preserve the user's original intent — do not change the underlying task.
|
||||
- When the user provides Penpot project context, weave in the relevant
|
||||
@ -105,10 +108,27 @@ Deliver the result in the response as two clearly separated blocks:
|
||||
changes you made and why (3–7 bullets max). Skip the rationale if the
|
||||
changes are trivial.
|
||||
|
||||
If you asked clarifying questions, list them in a separate **Clarifying
|
||||
questions** section above the refined prompt and stop — do not produce a
|
||||
refined prompt until the user answers. If the user explicitly told you to
|
||||
proceed without questions (e.g. "just rewrite it"), make reasonable
|
||||
If you asked clarifying questions via the `question` tool, stop and wait for
|
||||
the answers before producing a refined prompt. If the `question` tool was not
|
||||
available and you asked the questions in chat, list them in a separate
|
||||
**Clarifying questions** section above the refined prompt and stop — do not
|
||||
produce a refined prompt until the user answers. If the user explicitly told
|
||||
you to proceed without questions (e.g. "just rewrite it"), make reasonable
|
||||
assumptions and note them under **Assumptions made** in the rationale block.
|
||||
|
||||
No file persistence — the refined prompt lives entirely in the response.
|
||||
## File Persistence
|
||||
|
||||
Always persist the refined prompt to disk so it can be re-used later, versioned
|
||||
in git, and shared with other agents. The response still contains the prompt
|
||||
and rationale blocks; the file is an additional artifact, not a replacement.
|
||||
|
||||
- Save the refined prompt (the body inside the fenced code block, **without**
|
||||
the surrounding ``` fences) to `.opencode/prompts/<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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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!`
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -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
|
||||
43
.serena/memories/scripts/paren-repair.md
Normal file
43
.serena/memories/scripts/paren-repair.md
Normal 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")
|
||||
```
|
||||
|
||||
|
||||
39
.serena/memories/scripts/psql.md
Normal file
39
.serena/memories/scripts/psql.md
Normal 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)
|
||||
```
|
||||
@ -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
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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 \
|
||||
|
||||
20
AGENTS.md
20
AGENTS.md
@ -1,6 +1,6 @@
|
||||
# AI AGENT GUIDE
|
||||
|
||||
## Hard rules (always apply — no exceptions)
|
||||
## HARD RULES (always apply — no exceptions)
|
||||
|
||||
- **Never `git push`, force-push, or modify `git origin`** (or any other remote).
|
||||
The user pushes from their own shell. If a push is required to surface the
|
||||
@ -92,3 +92,21 @@ precision while maintaining a strong focus on maintainability and performance.
|
||||
down into atomic steps.
|
||||
2. Be concise and autonomous.
|
||||
3. Do **not** touch unrelated modules unless the task explicitly requires it.
|
||||
|
||||
---
|
||||
|
||||
# Available Scripts & Tools
|
||||
|
||||
## Native opencode Tools (callable directly by the LLM)
|
||||
|
||||
- `paren-repair` — Fix mismatched delimiters + reformat Clojure files. Example: `paren-repair(files="src/foo.clj, src/bar.cljs")`
|
||||
- `penpot-psql` — Execute SQL against the Penpot database. Example: `penpot-psql(sql="SELECT version();")`
|
||||
|
||||
## Scripts (from repo root via `scripts/<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.
|
||||
|
||||
|
||||
14
CHANGES.md
14
CHANGES.md
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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))))))
|
||||
|
||||
|
||||
628
docs/technical-guide/developer/data-model/penpot-file-format.md
Normal file
628
docs/technical-guide/developer/data-model/penpot-file-format.md
Normal 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)
|
||||
1401
docs/technical-guide/developer/data-model/penpot-file-inspector.njk
Normal file
1401
docs/technical-guide/developer/data-model/penpot-file-inspector.njk
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
155
docs/user-guide/export-import/penpot-file-format.njk
Normal file
155
docs/user-guide/export-import/penpot-file-format.njk
Normal 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>
|
||||
@ -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
|
||||
|
||||
@ -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))))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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])))
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
13
package.json
13
package.json
@ -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
195
pnpm-lock.yaml
generated
@ -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:
|
||||
|
||||
@ -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}";
|
||||
@ -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
|
||||
13
scripts/fmt
13
scripts/fmt
@ -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;
|
||||
@ -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)
|
||||
@ -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;
|
||||
@ -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)
|
||||
@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e;
|
||||
pnpm exec opencode;
|
||||
@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e;
|
||||
pnpm exec opencode serve --hostname 0.0.0.0 --port 14180;
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user