Compare commits

..
Author SHA1 Message Date
Eva Marco bcf5a49884 🐛 Remove redundant effect dependency in tokens source
file-name-truncated? was listed as a dependency of the with-effect
that checks and observes label truncation, even though it isn't
read inside the effect body. Since the effect itself flips that
state via check-file-name-truncated, including it as a dependency
caused the ResizeObserver to be needlessly disconnected and
reconnected on every truncation change.

AI-assisted-by: claude-sonnet-5
2026-09-10 17:01:16 +02:00
Eva Marco 8a151e62e7 🐛 Fix tokens source label truncation and missing translations
The tokens source file name always showed, even for the current file,
and long names wrapped onto a second line instead of truncating
because the header used flex-wrap and overflow-wrap: break-word
instead of single-line ellipsis.

Show the source row unconditionally (it now displays "This file" when
the source is the current file, matching the connected-library case),
truncate the file name to one line with an ellipsis, and only attach a
tooltip with the full name when the text is actually truncated.

Replace the hardcoded UI strings with translated ones and add their
English and Spanish entries.

AI-assisted-by: claude-sonnet-5
2026-09-08 12:16:09 +02:00
Eva Marco fb3cf431c1 🎉 Add changes notification (#11476)
* 🎉 Add changes notification

* ♻️ Change fn names
2026-09-08 10:24:39 +02:00
Eva Marco dcf90f9e28 🐛 Add a tokens source icon to libraries section (#11439)
* 🐛 Add a tokens source icon to libraries section

* 🐛 Fix ellipsis on library names

* ♻️ Remove code under flag on legacy component

* 🐛 Fix token theme name on inspect tab
2026-09-08 10:24:38 +02:00
Andrés Moya b101a16c70 🐛 Propagate tokens after synchronizing tokens in ext library 2026-09-08 10:24:38 +02:00
Andrés Moya b3a946eaeb 🐛 Propagate tokens after creating new instances 2026-09-08 10:24:38 +02:00
Andrés Moya 6a34947586 🐛 Propagate tokens after synchronizing components in ext library 2026-09-08 10:24:38 +02:00
Andrés Moya b0e1e019bb 🎉 Make ui changes optional with config flag 2026-09-08 10:24:38 +02:00
Andrés Moya 46a4a1774d 🔧 Validate and repair missing tokens status 2026-09-08 10:24:38 +02:00
Andrés Moya 6e339ba217 🔧 Add integration tests for tokens in external libs 2026-09-08 10:24:38 +02:00
Andrés Moya 3e6150ab92 🐛 Fix some integration tests 2026-09-08 10:24:38 +02:00
Andrés Moya 8d36565712 🔧 Add harder checks for nil or empty values in everything 2026-09-08 10:24:38 +02:00
Andrés Moya 91dbb873ad 🔧 Refactor tokens source calculations 2026-09-08 10:24:38 +02:00
Eva Marco 4b163f94d4 🎉 Layout updates 2026-09-08 10:24:38 +02:00
Andrés Moya 0d7cf045a9 🐛 Propagate changes of token values in external library 2026-09-08 10:24:38 +02:00
Andrés Moya d56a460b3c 🔧 Fix integration tests 2026-09-08 10:24:38 +02:00
Andrés Moya 12fc08d7f5 🎉 Show source info in sidebar 2026-09-08 10:24:38 +02:00
Andrés Moya d1c3135f8d 🎉 Show tokens in library summaries 2026-09-08 10:24:37 +02:00
Andrés Moya 5301888083 🎉 Make tokens library readonly when it's in an external file 2026-09-08 10:24:37 +02:00
Andrés Moya 105e97bf93 🎉 Allow the user to choose the tokens-source of a file 2026-09-08 10:24:37 +02:00
Andrés Moya 71d144f65b 🔧 Rename :tokens-file to :tokens-source 2026-09-08 10:24:37 +02:00
Andrés Moya 84507a444f 🔧 Normalize status helper functions and access token sets by id 2026-09-08 10:24:37 +02:00
Andrés Moya c358e58e33 🔧 Make all status operations use the new data structure 2026-09-08 10:24:37 +02:00
Andrés Moya ca1b5541c0 🔧 Add separated TokenStatus to store status apart of TokensLib 2026-09-08 10:24:34 +02:00
Andrés Moya 37f27df9d8 🔧 Refactor tokens-lib initialization 2026-09-08 10:22:56 +02:00
Andrés Moya ca619a4c85 Auto link tokens when adding external libraries (provisional) 2026-09-08 10:22:56 +02:00
415 changed files with 9206 additions and 13098 deletions

No files matched your search

-91
View File
@@ -1,91 +0,0 @@
# Agent skills
This folder is the single home for the skills our coding agents use.
Each skill is a folder with a `SKILL.md` inside — a short instruction
manual that an agent loads only when it needs it.
One copy serves every tool:
- **opencode** reads this folder directly.
- **Claude Code** reads it through the `.claude/skills` symlink.
- **Codex** reads it directly.
To change how the agents behave, edit the `SKILL.md` here. There is no
second copy to keep in sync.
## How the skills are organized
**Flows** are the six skills you invoke by name. Each one covers one step
in the life of a change: plan it, review the plan, implement it, review
the code, open the pull request.
**References** hold the quality standards. A flow's reviewer loads them;
you rarely touch them directly.
**Procedures** define how one concrete step is done — a plan document, an
issue, a commit. Flows call them, but they also work on their own.
**Utilities** are small helpers for everyday work: search, file lookup,
JSON, REPL access, and so on.
## Flows
| Skill | What it does | When you would say |
|---|---|---|
| [`make-a-plan`](skills/make-a-plan/SKILL.md) | Researches the task, writes an implementation plan, asks you the open questions in plain language, and saves the plan to `.agents/plans/`. | "make a plan for the token refresh bug" |
| [`review-plan`](skills/review-plan/SKILL.md) | Evaluates a plan before anyone writes code: completeness, ordering, risks. Approves it or asks for changes. | "review this plan before we start" |
| [`implement-plan`](skills/implement-plan/SKILL.md) | Shows you the full flow first — the issue and branch it will create (or the branch it continues on), the execution style, and the task checklist — and, after your go-ahead, executes a ready plan. Default: every task, one commit. On request ("step by step"): one task, one commit, your confirmation between tasks. On request ("direct"): no issue and no branch, commits on the current branch. | "implement the plan" · "step by step, one commit per task" · "direct, no branch" |
| [`review-code`](skills/review-code/SKILL.md) | Reviews a diff, branch, or PR and returns findings ranked by impact. | "review my changes before I push" |
| [`create-pr`](skills/create-pr/SKILL.md) | Opens a pull request for the current branch — with checks on base branch, commits, issue, and push state — or updates an existing PR's title and description. | "open a PR for this branch" |
| [`resolve-git-conflicts`](skills/resolve-git-conflicts/SKILL.md) | Untangles merge or rebase conflicts: explains both sides, proposes a resolution, applies it after you approve. Never runs `git rebase --continue`. | "resolve these conflicts" |
## References
| Skill | What it holds |
|---|---|
| [`plan-review-criteria`](skills/plan-review-criteria/SKILL.md) | The plan review rubric: six axes, severity levels, approval standard, output format. The `review-plan` reviewer loads it. |
| [`code-review-criteria`](skills/code-review-criteria/SKILL.md) | The code review rubric: five axes, core principles (DRY, KISS, YAGNI), severity format, verdict. The `review-code` reviewer loads it. |
## Procedures
| Skill | What it does |
|---|---|
| [`planner`](skills/planner/SKILL.md) | The spec of a good plan: context, architecture decisions, tasks with acceptance criteria, checkpoints. Used by `make-a-plan`. |
| [`create-issue`](skills/create-issue/SKILL.md) | Creates a GitHub issue that follows Penpot conventions. Used by `implement-plan`; also works on its own. |
| [`create-commit`](skills/create-commit/SKILL.md) | Makes a commit the Penpot way: emoji subject, clear body, `AI-assisted-by` trailer. Used by `implement-plan`; also works alone when you say "commit this". |
## Utilities
| Skill | What it does |
|---|---|
| [`bat-cat`](skills/bat-cat/SKILL.md) | Read files in the terminal with syntax highlighting and line numbers. |
| [`fd-find`](skills/fd-find/SKILL.md) | Find files by name or pattern, respecting `.gitignore`. |
| [`ripgrep`](skills/ripgrep/SKILL.md) | Fast content search with regular expressions. |
| [`jq-json-processor`](skills/jq-json-processor/SKILL.md) | Slice, filter, and reshape JSON output. |
| [`nrepl-eval`](skills/nrepl-eval/SKILL.md) | Run Clojure or ClojureScript code in the live REPL sessions (backend and frontend). |
| [`taiga`](skills/taiga/SKILL.md) | Look up Penpot issues, user stories, and tasks in Taiga. |
| [`testing`](skills/testing/SKILL.md) | The repo's testing rules and TDD workflow, loaded before writing tests. |
| [`local-ci`](skills/local-ci/SKILL.md) | Run CI-style lint, test, and format checks for the modules you touched with `scripts/ci`, and read the logs when they fail. |
| [`security-and-hardening`](skills/security-and-hardening/SKILL.md) | Security checks for code that handles user input, auth, or external services. |
| [`ste`](skills/ste/SKILL.md) | Rewrites prose in Simplified Technical English. Loads only when you name it. |
| [`refine-prompt`](skills/refine-prompt/SKILL.md) | Rewrites a rough prompt into a clearer one. Never runs the prompt. |
| [`update-changelog`](skills/update-changelog/SKILL.md) | Regenerates `CHANGES.md` from a GitHub milestone. |
## A typical round
1. `/make-a-plan` — you get a plan and a saved file in `.agents/plans/`.
2. `/review-plan` — a second opinion; approve or request changes.
3. `/implement-plan` — the code gets written and committed. Starting from a base branch, it also opens the GitHub issue and the `issue-NNNN` branch; the plans that follow continue on that same branch.
4. `/review-code` — a reviewer checks the commit.
5. `/create-pr` — the branch goes up as a pull request.
Every step also works on its own, and you can always say what you want
in plain words — the agents pick the right skill from what you say.
## Adding or changing a skill
Create a folder here with a `SKILL.md` inside. The file needs `name` and
`description` in its frontmatter, and a clear "When to use" section so
agents know when to reach for it. Keep one job per skill, and keep the
two families apart: flows are named with a verb first; reference skills
end in `-criteria`.
-105
View File
@@ -1,105 +0,0 @@
---
name: create-pr
description: PR flow — open a new PR for the current task branch (validates base branch, commits, issue and push state) or update an existing PR's title or description to match Penpot conventions. Use it when the user asks to open or create a PR, in any phrasing.
---
# Create PR
Two modes. **Open mode** takes the current task branch to a new, validated
PR. **Update mode** rewrites an existing PR's title or description. Gather
information, validate, and act in one pass. If validation fails, STOP with a
single coherent message that lists every problem and states exactly what
information is missing — never fix or work around problems silently.
Both modes require an authenticated `gh` CLI (`gh auth status`) and never
push — the user pushes from their own shell.
## When to use
- The user asks to open or create a NEW PR for the current task branch, in
any phrasing ("open a PR", "create the pull request", "put this up for
review") — or runs `/create-pr`. → **Open mode**.
- The user asks to fix or update an EXISTING PR's title or description to
match conventions. → **Update mode**.
If the running agent cannot write (for example, the plan agent), say so and
stop — this skill needs the build agent.
## Open mode
### 1. Gather context (read-only)
- Current branch: `git rev-parse --abbrev-ref HEAD`.
- Target base branch: run `./scripts/detect-target-branch` from the repo root.
It prints the nearest ancestor branch of HEAD (exit 0) or fails (exit 1).
- Commits: `git log --oneline <base>..HEAD`.
- Push state (local): `git rev-parse --verify origin/<branch>` and compare
with HEAD. It reads the local remote-tracking ref — no network, no SSH. It
reflects the last push or fetch this clone knows about.
- Issue: from the session context, or from the branch name — `issue-NNNN`
maps to issue NNNN; recover its title and body with `gh issue view NNNN`.
### 2. Validate — stop with one message if anything fails
Run all checks before reporting, then report every failure together:
1. **Base branch not usable.** If the script fails (exit 1), or its output —
after stripping an optional `remotes/origin/` prefix — is not one of the
canonical branches (`develop`, `staging`, `main`), stop and ask the user
to re-run with more context — for example, passing the base branch
explicitly in their invocation. An explicit base given by the user
overrides the script's output.
2. **On a base branch.** There is no task branch to merge — say so and stop.
3. **No commits.** The branch has no commits ahead of the base — say so and
stop.
4. **No clear issue.** There is no issue in the session context, and the
branch name has no `issue-NNNN` pattern (or `gh issue view` finds nothing)
— say so and stop. Exception: the user's invocation says `no issue` /
`without issue` — then continue without an issue reference.
5. **Branch not pushed.** The remote-tracking ref `origin/<branch>` is
missing, or `git rev-parse origin/<branch>` differs from HEAD — the
branch was never pushed, or has commits the remote does not have. Never
push yourself; ask the user to push and to run `/create-pr` again
afterwards, then stop.
### 3. Already-open PR
Check whether a PR already exists for this branch (`gh pr list --head
<branch>`). If one exists, report its URL and stop — do not create a second
one. Title or description fixes belong to Update mode.
### 4. Write and create the PR
Write the title and body following `mem:workflow/creating-prs` (title format,
description structure, writing principles) and `mem:workflow/creating-commits`
(commit type emojis). Derive the title and body from the commits and, when
there is one, from the issue body. Reference the issue with `Closes #NNNN`.
```bash
gh pr create --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
### 5. Report
Report the PR URL and stop.
## Update mode
1. Identify the PR: the number given by the user, or `gh pr list --head
<branch>`.
2. Write the new title and/or body following `mem:workflow/creating-prs`.
3. Apply and verify:
```bash
gh pr edit <NUMBER> --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
gh pr view <NUMBER> --repo penpot/penpot --json title,body
```
4. Report and stop.
## User context
Extra context in the user's invocation (the message that triggered this skill)
plays the role command arguments play elsewhere: overrides such as `no issue` /
`without issue`, an explicit base branch (`from origin/staging`), a PR number
for Update mode, and so on.
-144
View File
@@ -1,144 +0,0 @@
---
name: implement-plan
description: Implementation flow — execute a ready plan from the session context: read the plan, detect the flow, then present the full picture (issue and branch to create or the branch to continue on, execution style, task checklist) and wait for confirmation. Default is every task with one final commit; on request ("step by step"), one task and one commit at a time with a pause after each; on request ("direct"), no issue and no branch — the commit lands on the current branch. Use it when the user asks to implement or execute a plan, in any phrasing.
---
# Implement Plan
This flow is run once a plan is ready (for example, from plan mode). Execute
the plan already prepared in the current session context. It never pushes —
the user pushes.
By default it ends with exactly one commit. When the user asks for it
("step by step"), it commits once per task instead and waits for the
user's confirmation after each one (see *Execution modes*).
## When to use
- The user asks to implement or execute a plan, in any phrasing:
"implement the plan", "execute it", "go build it" — or runs
`/implement-plan`.
- A ready, reviewed plan is in the session context or a plan file path
was given (typically after `/make-a-plan` or `/review-plan`).
Do not use it to produce plans — that is the `make-a-plan` flow.
## 1. Read the plan first
Identify the plan to execute — from the file path the user gave, the
arguments, or the session context. Read it completely. Read the required
memories before writing any code: `mem:critical-info` and the core memory
of every module the plan touches, plus the deeper memories they reference
(AGENTS.md governs this).
## 2. Detect the flow (no questions)
Inspect the current branch with `git rev-parse --abbrev-ref HEAD`, pick the
mode, and announce it in one line before presenting anything. Detection is
read-only: nothing is created until the user confirms (step 3).
- **On a base branch** (`main`, `develop`, `staging`) → **standalone mode**:
a new GitHub issue and a branch `issue-NNNN` will be created after the
user's confirmation.
- **On any other branch** (a feature branch, typically `issue-NNNN`) →
**continue mode**: the implementation continues on the current branch.
No issue or branch is created. The branch name provides the issue
reference when it follows the `issue-NNNN` pattern.
Arguments override detection: `standalone`, `continue`, `direct`
(`no branch` / `direct commit`), `no issue` / `without issue`, or an
explicit base such as `from origin/develop`.
**Direct mode** (`direct`, `no branch`, `direct commit`): no issue and
no branch — the implementation and the commit land on the current branch
as it is, even when it is a base branch. Best for small or tooling-only
changes the user wants committed in place.
**Standalone while already on a feature branch:** stop and explain that this
would stack branches. Ask the user to re-run with an explicit base, for
example `from origin/develop` — then branch from that base instead of HEAD.
## 3. Present the checklist and wait
Before touching the repository, show the user the full picture:
- **The flow**: whether the GitHub issue and the branch will be created
(standalone mode — give the planned branch name, `issue-NNNN` or
`plan-<slug>`), whether you continue on the current branch
(continue mode — name it), or whether everything lands on the current
branch as it is (direct mode — name it, and say so when it is a base
branch).
- **The execution style**: batch or step-by-step (see *Execution modes*).
- A checklist (todolist) of the plan's tasks, in order.
Then WAIT for the user's explicit confirmation. Do not start until you
have it. If the plan has no discrete tasks, ask the user how to split
it, or propose running it as a single change.
## 4. Execute the plan
**Standalone setup, after the confirmation:** create the issue with the
**`create-issue`** skill, following the *Creating Issues from Draft Body*
flow in `mem:workflow/creating-issues`. Derive the issue title and body
from the plan, capture the new issue's number — call it **NNNN** — and
create the branch from the current HEAD:
```
git checkout -b issue-NNNN
```
If the arguments say `no issue` / `without issue`, skip the issue and
create a branch named `plan-<slug>` instead, where `<slug>` is the plan
title, lowercase and hyphen-separated.
If the arguments say `direct` / `no branch` / `direct commit`, skip the
issue and the branch: implement and commit on the current branch as it
is. If it is a base branch, the checklist presentation already said so —
no further confirmation is needed.
### Batch mode (default)
Implement every task in one go. Work methodically, keeping changes
focused on what the issue requires. Respect the plan's proposed
parallelization when it applies.
When the implementation is complete, load the **`create-commit`** skill
and follow its workflow to commit the changes. Provide a brief summary
of what was implemented and why, the issue reference (`issue-NNNN`) when
there is one, and the model name you are running as so the
`AI-assisted-by` trailer is set correctly.
### Step-by-step mode (on request)
When the user asks for it — "step by step", "task by task", "one commit
per task" — loop one task at a time:
- Execute exactly ONE task.
- Commit it now: load the **`create-commit`** skill and follow it —
one commit per task, never two tasks in one commit. Same inputs as
always: what and why, the issue reference, your model name.
- Show the user the result (what changed, files touched, how it was
verified).
- WAIT for the user's confirmation before starting the next task.
Never batch in this mode: no two tasks in one commit, and no new task
before the user confirms. If a task turns out much bigger than planned,
stop and ask the user before splitting it.
## When you are done
End by suggesting the next steps (suggestions, not a required pipeline — any
instruction from me overrides them):
- `/review-code` — to review the changes just committed; it routes to
`/make-a-plan` by itself if the findings need one.
- `/create-pr` — when the task is done and the branch is ready to merge.
## User context
Extra context in the user's invocation (the message that triggered this
skill) plays the role command arguments play elsewhere: `standalone`,
`continue`, `direct` (`no branch` / `direct commit`), `no issue` /
`without issue`, an explicit base such as `from origin/develop`, or
`step by step` / `one commit per task` for the step-by-step execution
mode. Modes combine freely, for example "standalone step by step".
-95
View File
@@ -1,95 +0,0 @@
---
name: local-ci
description: Run local CI-style checks with ./scripts/ci (lint, tests, format) per monorepo module. Use when verifying changes before declaring work done, running lint or tests locally, fixing formatting, or repairing Clojure delimiter errors.
---
# Local CI
Run the same checks CI runs, locally, for the modules you touched, with
`scripts/ci`. Each task writes a log file; the final summary says what
passed and what failed.
Full details: `mem:scripts/ci` (file: `.serena/memories/scripts/ci.md`)
## When to use
- After implementing or fixing code — verify every module you touched
before declaring the work done.
- When the user asks to run CI, lint, tests, or format checks locally.
- When you changed `common/` — validate its consumers too.
**Skip:** while exploring, planning, or reading code.
## Command reference
Run from the repo root:
```bash
./scripts/ci [OPTIONS] [MODULES...]
```
Modules: `frontend` `backend` `common` `render-wasm` `exporter` `mcp`
`plugins` `library`, or `--all` for every module.
With no task flags it runs three tasks per module, in order: **lint**,
**test**, **fmt** (format check; `--fix` formats files instead).
| Flag | Effect |
|------|--------|
| `--all` | Run every module |
| `--exclude MOD` | Skip one module (repeatable) |
| `--lint` / `--no-lint` | Run only lint / drop lint |
| `--test` / `--no-test` | Run only tests / drop tests |
| `--fmt` / `--no-fmt` | Run only format check / drop it |
| `--fix` | Format files instead of checking (other tasks unaffected) |
| `--paren-repair` | Fix delimiter errors in Clojure/CLJS files |
| `--fail-fast` | Stop at the first failure |
| `--quiet` | Suppress failure output |
| `--dry-run` | Show what would run, execute nothing |
| `--clean` | Delete the `.ci-logs/` directory |
## Reading failures
Every task writes its full output to `.ci-logs/<module>-<task>.log`. On
failure the script prints only the last 30 lines. To diagnose a failure,
**read the log file** — never re-run the command piped through filters
(repo rule: redirect to a file first, then read it). The exit code is 1
when any task failed; the summary lists each failed `module:task` and its
log path.
## Typical workflows
```bash
# Verify a module you changed: lint + tests + format check
./scripts/ci frontend
# Fast pass while iterating: lint only
./scripts/ci --lint frontend
# Lint + format check, skip the long test suite
./scripts/ci --no-test frontend
# Format the module without running the test suite
./scripts/ci --fix --no-test frontend
# Broke delimiters in Clojure/CLJS files: repair first, then lint
./scripts/ci --paren-repair frontend
./scripts/ci --lint frontend
# Changed common/ — validate its consumers too
./scripts/ci frontend backend exporter
# Preview what would run, without running it
./scripts/ci --dry-run --all
```
## Gotchas
- Run from the repo root.
- Test tasks are long-running (backend runs `clojure -M:dev:test`); give
the bash call a generous timeout (1020 minutes) instead of letting it
time out mid-run.
- `mcp` has no lint task — it shows as skipped, not failed.
- `--paren-repair` only fixes delimiters; run lint afterwards to catch
what remains. See `mem:scripts/paren-repair`.
- What to run and how to read test results: `mem:testing`.
-100
View File
@@ -1,100 +0,0 @@
---
name: make-a-plan
description: Planning flow — research the subject of this session, produce an implementation plan with the planner skill, resolve open questions with the user in plain language, and save the final plan to .agents/plans/. Use it when the user asks to plan, design, or break down a task, in any phrasing.
---
# Make a Plan
Act as a senior software engineer: research the subject of this session in depth and
produce a well-grounded, actionable implementation plan.
If the running agent cannot write (for example, the plan agent), say so and
stop — this skill needs the build agent to save the plan.
## When to use
- The user asks to plan, design, or break down a task, in any phrasing:
"make a plan", "how would we build X", "design an approach for Y" —
or runs `/make-a-plan`.
- The user asks to rework or extend an existing plan (for example, after
review findings) — revise the saved plan file in place.
Do not use it to execute a plan — that is the `implement-plan` flow.
## Instructions
1. **Produce the plan** with the `planner` skill. By default, research the
subject of this session and draft the plan yourself. If I ask for it (for
example, `delegated` in the user context), delegate to the `general` subagent
instead — the delegate must also follow the `planner` skill and receive all
the relevant session context (a review, user feedback, and so on).
2. Before asking me to decide anything, explain the plan and every open question in
plain language. Assume I know only the high-level project goal, not the codebase,
architecture, implementation terms, or the problem this task solves.
3. Once all decisions are answered and the plan is final, save it verbatim to the
announced path under `.agents/plans/` (create the directory if it does not
exist). This step is the flow's explicit authorization to write the plan
file — the only write allowed here. If I later ask for changes, update the
saved file directly.
4. Present me with a clear, self-contained summary of the plan's most relevant points
only after all required decisions have been answered. Write it for someone who knows
only the project's high-level goal and may not know the plan's low-level context.
Explain necessary technical language in plain terms, include the problem being
solved and the proposed outcome, and do not assume that listing technical task names
is enough.
### Hard rule — read-only while planning
While this flow runs, act read-only: research with read-only tools only.
Never edit source files, never run builds, tests, linters, or any command that
modifies state, and never commit. The single allowed write is the plan file in
step 3. This rule expires when I approve the plan or move on to another task;
then you act as a normal build agent again.
When the plan contains open questions, do not show them as bare technical questions or
assume that I understand the technical language or technical words used in the plan.
For each question, first explain:
- What part of the user problem the decision affects.
- The relevant concept from the beginning, with a small concrete example.
- What each available option would make the system do.
- The practical benefits, costs, risks, and user-visible consequences of each option.
- Which option the planner recommends and why.
Only after that explanation, use the `question` tool to ask the decision with clear,
non-technical option labels. Put the recommended option first and mark it as
`(Recommended)`. Group related questions when their context is shared, but do not ask a
question whose meaning has not already been explained.
If I say that I do not understand a question or its choices, do not treat my previous
answer as valid. Explain the concepts again from the high-level project goal, use a more
concrete example, explain the implications, and ask the question again with the
`question` tool. Repeat this until I can make an informed choice. If one answer creates
new design consequences or additional decisions, explain those consequences before
asking any new question.
Distinguish clearly between requirements already fixed by the roadmap or existing
architecture and choices that actually require my input. Do not ask me to choose an
implementation detail when the plan can resolve it safely without changing the public
behavior. If there are no decisions that require my input, say so and present the
summary.
IMPORTANT: **Under no circumstances execute the plan. Wait for the user to review it
after all possible questions have been answered.** The final summary must explain the
problem being solved, the proposed behavior, the main user-visible workflow, important
constraints and risks, what is deliberately out of scope, and the path where the plan
is saved. Never assume that a short list of task names is enough context. End
the final response by suggesting the next steps, in this order:
1. `/review-plan` — to get a second opinion on the plan before executing it.
2. `/implement-plan` — to execute the plan from the current session context.
These are suggestions, not a required pipeline — any instruction from me
overrides them (for example, asking you to implement the plan directly).
## User context
Extra context in the user's invocation (the message that triggered this skill)
plays the role command arguments play elsewhere: for example, `delegated` to
hand the research and drafting to the `general` subagent, or corrections and
feedback about a previous plan.
@@ -1,47 +0,0 @@
---
name: resolve-git-conflicts
description: Conflict resolution flow — understand the local git conflicts, present a resolution plan, and resolve them after the user approves it. Never continues the rebase. Use it when the repo has unresolved conflicts (rebase, merge, cherry-pick) or the user asks to resolve them.
---
# Resolve Git Conflicts
Resolve conflicts in the local repository. The user handles finishing the
rebase themselves — you must **never** run `git rebase --continue`,
`git rebase --skip`, `git merge --continue`, or anything similar.
## When to use
- The repository has unresolved conflicts — during a rebase, merge, or
cherry-pick — whether the user asks about them or not.
- The user asks to resolve conflicts, in any phrasing: "fix the merge
conflicts", "resolve these", "what's conflicting here?".
## Phase 1 — Understand the problem (read-only)
1. Run `git status` to detect the conflict state (rebase, merge, cherry-pick, etc.) and list conflicted files.
2. For each conflicted (unmerged) file, understand the situation **without modifying anything**:
- Read the file and identify the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`).
- Inspect both sides — `git show <ours>:<file>` and `git show <theirs>:<file>` — plus `git log`/`git show` on the commits involved to understand intent.
- Identify what each side changed and why, and how they should be combined.
## Phase 2 — Present the resolution plan
3. **Present a clear plan to the user before touching any file.** For each conflicted file, state:
- What each side changed and why.
- Your proposed resolution and the reasoning behind it.
- How the two sides are combined (both additive → merge; both modify the same code → keep the semantically correct version, merging intent from both sides when clear from code and context).
4. **Ask the user only when genuinely unclear.** Do not ask about anything you can determine yourself from the code, commit messages, or context. Only decisions that are not determinable and change the outcome (e.g. conflicting product decisions, which side to discard) warrant a question. **Collect all such questions together in an "Open Questions" section at the end of the plan**, so the user has full context to answer them properly.
5. **Wait for the user to accept the plan** (and answer any open questions) before editing, staging, or otherwise modifying anything.
## Phase 3 — Execute
6. Resolve each conflicted file by editing the file to the agreed merged content and removing all conflict markers.
## Phase 4 — Stage and verify
7. **Stage every resolved file** with `git add <file>`. Do not stage unrelated untracked files unless clearly part of the resolution.
8. Verify no conflict markers remain (search for `<<<<<<<` / `>>>>>>>` in resolved files) and that `git status` shows no unmerged paths.
## Phase 5 — Report
9. Briefly report the conflict state, how each conflicted file was resolved (and any answers received to open questions), and stop — do **not** run `git rebase --continue` or any other continuation command.
-73
View File
@@ -1,73 +0,0 @@
---
name: review-code
description: Code review flow — review a diff, PR, or code change, delegating the review to a subagent that follows the code-review-criteria skill. Use it when the user asks to review code or a PR, in any phrasing.
---
# Review Code
Act as a senior software engineer and perform a thorough code review.
## When to use
- The user asks to review code, in any phrasing: "review this diff",
"review the PR", "check my changes", "code review" — or runs
`/review-code`.
- A commit, branch, PR, or diff is ready and the user wants it assessed
before merge.
## Instructions
1. **Determine what is being reviewed** from the user context: a working-tree
diff, a commit range, a branch, a PR (number or URL), or specific files. If
the target is ambiguous, ask before reviewing.
2. Delegate the review to the `general` subagent (via the task tool), unless the
user specifies another agent. Include in the prompt the
**`code-review-criteria`** skill name and all user context.
3. When the subagent returns, output the review to the user verbatim. Do not
summarize it and do not act on its findings.
4. Right after the review, suggest how to proceed based on the findings. These
are suggestions — the user decides:
- **Approve (no required changes):** say so — there is nothing to address.
- **Minor findings (nits):** applying them directly as-is is fine once the
review is done — no plan needed.
- **Substantive findings:** suggest `/make-a-plan` to make a plan to address
them.
### Hard rule — read-only while reviewing
This flow is read-only **for the duration of the review**: from the moment it
starts until the user considers the review finished (including any feedback,
questions, or clarifications about it). During that period, never fix,
implement, edit files or create commits — not even "obvious" fixes derived from
the findings. Once the user explicitly states the review is done (or moves on to
a different task), this rule no longer applies and you act as a normal build
agent again.
## Instructions for the subagent
1. Load the **`code-review-criteria`** skill and follow its process and output
format.
2. Read `AGENTS.md` (if present) and follow its instructions for finding and
reading all related testing documentation from memories before reviewing.
3. Return in your final message the COMPLETE review, verbatim, exactly as the
skill instructs it to be produced. Do not summarize it — include the full
structured review.
### Strong rules for the subagent
1. Do not invent problems. Every finding must be real and actionable.
2. Read-only: do not modify any file and do not create a commit — reviewing
never writes.
3. Be specific and constructive. "This could be better" is not helpful — explain
why and how.
4. Prioritize by impact. One structural issue outweighs ten nits.
5. Missing tests are an issue, not a suggestion. Report as a severity-tagged
finding — never as a recommendation.
6. Skip generated files, lockfile-only changes, and unrelated modifications
unless they introduce security risks.
## User context
Extra context in the user's invocation (the message that triggered this skill)
plays the role command arguments play elsewhere: for example, a PR number or
URL, a commit range, specific files, or a different agent to run the review.
-71
View File
@@ -1,71 +0,0 @@
---
name: review-plan
description: Plan review flow — evaluate an implementation plan before it is executed, delegating the review to a subagent that follows the plan-review-criteria skill. Use it when the user asks to review a plan, in any phrasing.
---
# Review Plan
Act as a senior software engineer and perform a thorough review of an
implementation plan.
## When to use
- The user asks to review a plan, in any phrasing: "review this plan",
"does this plan look right?", "second opinion on the plan" — or runs
`/review-plan`.
- A plan was just produced (typically by `/make-a-plan`) and the user
wants it evaluated before executing it.
## Instructions
1. **Determine the plan under review** from the session context (for example, a
plan just produced by `/make-a-plan`) or from a plan file path given by the
user (typically under `.agents/plans/`). If a file path is given, read the
file first so the complete plan is in context.
2. Delegate the review to the `general` subagent (via the task tool), unless the
user specifies another agent. Include in the prompt the
**`plan-review-criteria`** skill name and all user context.
3. When the subagent returns, output the review to the user verbatim. Do not
summarize it and do not act on its findings.
4. Right after the review, suggest the next step based on the verdict. These
are suggestions — the user decides, and any instruction overrides them:
- **Approve** → suggest `/implement-plan` to execute it.
- **Request changes** → suggest `/make-a-plan` to make a plan to address the
findings.
### Hard rule — read-only while reviewing
This flow is read-only **for the duration of the review**: from the moment it
starts until the user considers the review finished (including any feedback,
questions, or clarifications about it). During that period, never fix,
implement, edit files or create commits — not even "obvious" fixes derived from
the findings. Once the user explicitly states the review is done (or moves on to
a different task), this rule no longer applies and you act as a normal build
agent again.
## Instructions for the subagent
1. Load the **`plan-review-criteria`** skill and follow its process and output
format.
2. Read `AGENTS.md` (if present) and follow its instructions for finding and
reading all related documentation and testing memories before reviewing.
3. Return in your final message the COMPLETE review, verbatim, exactly as the
skill instructs it to be produced. Do not summarize it — include the full
structured review.
### Strong rules for the subagent
1. Do not invent problems. Every finding must be real and actionable.
2. Read-only: do not modify any file and do not create a commit — reviewing
never writes.
3. Be specific and constructive. "This could be better" is not helpful — explain
why and how.
4. Prioritize by impact. One structural issue outweighs ten nits.
5. Judge the plan as the implementer would: every task executable without
guessing, ordering follows the dependency graph, risks named.
## User context
Extra context in the user's invocation (the message that triggered this skill)
plays the role command arguments play elsewhere: for example, a plan file path
to review, or a different agent to run the review.
-1
View File
@@ -1 +0,0 @@
../.agents/skills
-44
View File
@@ -1,44 +0,0 @@
name: _ADHOC
run-name: >-
_ADHOC (${{ inputs.gh_ref }}${{ inputs.nitrate_ref != '' && format(' / nitrate:{0}', inputs.nitrate_ref) || '' }})
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Branch/ref to build in penpot/penpot'
type: string
required: true
nitrate_ref:
description: 'Branch/ref to build admin-console in penpot/penpot-nitrate (defaults to gh_ref)'
type: string
required: false
force:
description: 'Rebuild and overwrite even if already built/promoted'
type: boolean
required: false
default: false
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: ${{ inputs.gh_ref }}
force: ${{ inputs.force }}
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: ${{ inputs.gh_ref }}
force: ${{ inputs.force }}
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: ${{ inputs.nitrate_ref || inputs.gh_ref }}
force: ${{ inputs.force }}
+1 -35
View File
@@ -9,11 +9,6 @@ on:
type: string
required: true
default: 'develop'
force:
description: 'Rebuild and overwrite even if this version already exists in S3'
type: boolean
required: false
default: false
workflow_call:
inputs:
gh_ref:
@@ -21,11 +16,6 @@ on:
type: string
required: true
default: 'develop'
force:
description: 'Rebuild and overwrite even if this version already exists in S3'
type: boolean
required: false
default: false
# Literal group name: under `workflow_call`, `github.workflow` resolves to the
# caller's workflow, which put this workflow and the other reusable one called
@@ -44,8 +34,6 @@ jobs:
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
sha: ${{ steps.vars.outputs.sha }}
commit_title: ${{ steps.vars.outputs.commit_title }}
exists: ${{ steps.check.outputs.exists }}
steps:
@@ -60,12 +48,10 @@ jobs:
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse --short=12 HEAD)" >> $GITHUB_OUTPUT
echo "commit_title=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT
# The uploaded zip carries its version as S3 metadata. If the
# existing object was already built from this same commit, the
# whole build job is skipped. `force` bypasses this check entirely.
# whole build job is skipped.
- name: Check if this bundle is already built
id: check
env:
@@ -73,16 +59,6 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
if [ "${{ inputs.force }}" = "true" ]; then
echo "exists=false" >> $GITHUB_OUTPUT
{
echo "### 🔁 Bundle build forced"
echo ""
echo "\`force: true\` — skipping the S3 version check."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
EXISTING_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "penpot-${{ steps.vars.outputs.gh_ref }}.zip" \
@@ -141,16 +117,6 @@ jobs:
s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.check.outputs.gh_ref }}.zip \
--metadata bundle-version=${{ needs.check.outputs.bundle_version }}
- name: Write step summary
run: |
{
echo "### ✅ Bundle built"
echo ""
echo "- Version: \`${{ needs.check.outputs.bundle_version }}\` (\`git describe --tags --always\`)"
echo "- Commit: [\`${{ needs.check.outputs.sha }}\`](https://github.com/${{ github.repository }}/commit/${{ needs.check.outputs.sha }}) — ${{ needs.check.outputs.commit_title }}"
echo "- Built at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
} >> "$GITHUB_STEP_SUMMARY"
# ── 3. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
-12
View File
@@ -1,16 +1,7 @@
name: _DEVELOP
run-name: >-
_DEVELOP (develop @ ${{ github.sha }})
on:
workflow_dispatch:
inputs:
force:
description: 'Rebuild and overwrite even if already built/promoted'
type: boolean
required: false
default: false
schedule:
- cron: '16 5-20 * * 1-5'
@@ -24,7 +15,6 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
force: ${{ inputs.force || false }}
build-docker:
needs: build-bundle
@@ -32,11 +22,9 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
force: ${{ inputs.force || false }}
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "develop"
force: ${{ inputs.force || false }}
@@ -13,11 +13,6 @@ on:
type: string
required: false
default: 'develop'
force:
description: 'Rebuild and overwrite even if already built'
type: boolean
required: false
default: false
workflow_call:
inputs:
gh_ref:
@@ -29,11 +24,6 @@ on:
type: string
required: false
default: 'develop'
force:
description: 'Rebuild and overwrite even if already built'
type: boolean
required: false
default: false
secrets:
ORG_WORKFLOW_TOKEN:
description: 'Token with Actions write access on penpot-nitrate'
@@ -57,7 +47,6 @@ jobs:
gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$DISPATCH_REF" \
-f gh_ref="$GH_REF" \
-f force="${{ inputs.force }}" \
-f caller_run_id="$DISTINCT_ID" \
-f caller_run_url="$CALLER_URL"
+2 -37
View File
@@ -8,11 +8,6 @@ on:
type: string
required: true
default: 'develop'
force:
description: 'Rebuild and overwrite even if this sha is already promoted'
type: boolean
required: false
default: false
workflow_call:
inputs:
gh_ref:
@@ -20,11 +15,6 @@ on:
type: string
required: true
default: 'develop'
force:
description: 'Rebuild and overwrite even if this sha is already promoted'
type: boolean
required: false
default: false
# Literal group name: under `workflow_call`, `github.workflow` resolves to the
# caller's workflow, which put this workflow and the other reusable one called
@@ -52,7 +42,6 @@ jobs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
sha: ${{ steps.vars.outputs.sha }}
commit_title: ${{ steps.vars.outputs.commit_title }}
exists: ${{ steps.check.outputs.exists }}
steps:
@@ -71,7 +60,6 @@ jobs:
GH_REF="${{ inputs.gh_ref || github.ref_name }}"
echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse --short=12 HEAD)" >> $GITHUB_OUTPUT
echo "commit_title=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT
BUNDLE_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
@@ -83,8 +71,7 @@ jobs:
# The image set is a single block, so a single set-level check is
# enough: `promote` drops a marker object in S3 only after every
# image was built AND every branch tag was moved. Marker present
# means there is nothing at all to do for this commit. `force`
# bypasses this check entirely.
# means there is nothing at all to do for this commit.
- name: Check if this image set is already built
id: check
env:
@@ -92,21 +79,6 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
if [ "${{ inputs.force }}" = "true" ]; then
echo "exists=false" >> $GITHUB_OUTPUT
mkdir -p "$BUNDLE_CACHE"
find "$BUNDLE_CACHE" -type f -mtime +1 -delete || true
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.bundle_version }}.zip"
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
{
echo "### 🔁 Image set build forced"
echo ""
echo "\`force: true\` — skipping the S3 marker check."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "markers/images-sha-${{ steps.vars.outputs.sha }}" \
@@ -166,7 +138,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# To avoid the "429 Too Many Requests" error when downloading
# To avoid the 429 Too Many Requests error when downloading
# images from DockerHub for unregistered users.
# https://docs.docker.com/docker-hub/usage/
- name: Login to DockerHub Registry
@@ -286,16 +258,9 @@ jobs:
run: |
echo "${{ github.run_id }}" | aws s3 cp - \
"s3://${{ secrets.S3_BUCKET }}/markers/images-sha-${{ needs.prepare.outputs.sha }}"
- name: Write step summary
run: |
{
echo "### ✅ Image set promoted"
echo ""
echo "- Version: \`${{ needs.prepare.outputs.bundle_version }}\` (\`git describe --tags --always\`)"
echo "- Commit: [\`${{ needs.prepare.outputs.sha }}\`](https://github.com/${{ github.repository }}/commit/${{ needs.prepare.outputs.sha }}) — ${{ needs.prepare.outputs.commit_title }}"
echo "- Built at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
echo ""
echo "All \`:${{ needs.prepare.outputs.gh_ref }}\` tags now point to \`sha-${{ needs.prepare.outputs.sha }}\`."
} >> "$GITHUB_STEP_SUMMARY"
-12
View File
@@ -1,16 +1,7 @@
name: _STAGING
run-name: >-
_STAGING (staging)
on:
workflow_dispatch:
inputs:
force:
description: 'Rebuild and overwrite even if already built/promoted'
type: boolean
required: false
default: false
schedule:
- cron: '36 5-20 * * 1-5'
@@ -24,7 +15,6 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
force: ${{ inputs.force || false }}
build-docker:
needs: build-bundle
@@ -32,11 +22,9 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
force: ${{ inputs.force || false }}
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "staging"
force: ${{ inputs.force || false }}
-12
View File
@@ -1,16 +1,7 @@
name: _TAG
run-name: >-
_TAG (${{ github.ref_name }} @ ${{ github.sha }})
on:
workflow_dispatch:
inputs:
force:
description: 'Rebuild and overwrite even if already built/promoted (manual re-releases only)'
type: boolean
required: false
default: false
push:
tags:
- '*'
@@ -27,7 +18,6 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
force: ${{ inputs.force || false }}
build-docker:
needs: build-bundle
@@ -35,14 +25,12 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
force: ${{ inputs.force || false }}
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
force: ${{ inputs.force || false }}
notify:
name: Notifications
+2 -1
View File
@@ -24,6 +24,7 @@ opencode.json
!AGENTS.md
!CODE_OF_CONDUCT.md
!SECURITY.md
!HIGHLIGHTS.md
/*.png
/*.svg
/*.sql
@@ -102,7 +103,7 @@ opencode.json
/.playwright-mcp
/.devenv/mcp/
/opencode.json
/.agents/plans
/.opencode/plans
/.opencode/reports
/.opencode/prompts
/.ci-logs
-10
View File
@@ -1,10 +0,0 @@
---
description: Create a PR for the current task branch or update an existing one — loads and follows the create-pr skill
agent: build
---
Load the **`create-pr`** skill and follow it as your only instruction.
## User input, overrides and additional context
$ARGUMENTS
+33 -4
View File
@@ -1,10 +1,39 @@
---
description: Execute a ready plan — task checklist, your confirmation, then all tasks with one commit (default) or step by step with a commit and a pause per task; creates issue + branch when on a base branch, or commits on the current branch with "direct" — loads and follows the implement-plan skill
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the create-commit skill
agent: build
---
Load the **`implement-plan`** skill and follow it as your only instruction.
This command is run once a plan is ready (for example, from plan mode). Execute
the plan already prepared in the current session context. Follow these steps in order.
## User input, overrides and additional context
## 1. Create the issue
$ARGUMENTS
Use the **`create-issue`** skill, following the *Creating Issues from Draft Body*
flow in `mem:workflow/creating-issues`. Derive the issue title and body from the
plan. Capture the new issue's number — call it **NNNN** (needed for the branch
name and the commit reference).
## 2. Create the branch
Create and switch to a branch named after the issue:
```
git checkout -b issue-NNNN
```
(Replace NNNN with the issue number from step 1.)
## 3. Execute the plan
Implement the prepared plan from the session context. Work methodically, keeping
changes focused on what the issue requires. Do not commit — the commit happens in
step 4.
## 4. Commit with the create-commit skill
After the implementation is complete, load the **`create-commit`** skill and
follow its workflow to commit the changes. Provide a brief summary of what was
implemented and why, the issue reference (`issue-NNNN`), and the model name you
are running as so the `AI-assisted-by` trailer is set correctly.
Do not push. Pushing is handled separately by the user.
-10
View File
@@ -1,10 +0,0 @@
---
description: Investigate the chosen task, produce an implementation plan, and save it — loads and follows the make-a-plan skill
agent: build
---
Load the **`make-a-plan`** skill and follow it as your only instruction.
## User input, overrides and additional context
$ARGUMENTS
+36 -2
View File
@@ -1,6 +1,40 @@
---
description: Resolve local git conflicts and stage the resolved files; never continues the rebase — loads and follows the resolve-git-conflicts skill
description: Resolve local git conflicts and stage the resolved files with git add — never continues the rebase
agent: build
---
Load the **`resolve-git-conflicts`** skill and follow it as your only instruction.
# Fix Git Conflicts
Resolve conflicts in the local repository. The user handles finishing the
rebase themselves — you must **never** run `git rebase --continue`,
`git rebase --skip`, `git merge --continue`, or anything similar.
## Phase 1 — Understand the problem (read-only)
1. Run `git status` to detect the conflict state (rebase, merge, cherry-pick, etc.) and list conflicted files.
2. For each conflicted (unmerged) file, understand the situation **without modifying anything**:
- Read the file and identify the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`).
- Inspect both sides — `git show <ours>:<file>` and `git show <theirs>:<file>` — plus `git log`/`git show` on the commits involved to understand intent.
- Identify what each side changed and why, and how they should be combined.
## Phase 2 — Present the resolution plan
3. **Present a clear plan to the user before touching any file.** For each conflicted file, state:
- What each side changed and why.
- Your proposed resolution and the reasoning behind it.
- How the two sides are combined (both additive → merge; both modify the same code → keep the semantically correct version, merging intent from both sides when clear from code and context).
4. **Ask the user only when genuinely unclear.** Do not ask about anything you can determine yourself from the code, commit messages, or context. Only decisions that are not determinable and change the outcome (e.g. conflicting product decisions, which side to discard) warrant a question. **Collect all such questions together in an "Open Questions" section at the end of the plan**, so the user has full context to answer them properly.
5. **Wait for the user to accept the plan** (and answer any open questions) before editing, staging, or otherwise modifying anything.
## Phase 3 — Execute
6. Resolve each conflicted file by editing the file to the agreed merged content and removing all conflict markers.
## Phase 4 — Stage and verify
7. **Stage every resolved file** with `git add <file>`. Do not stage unrelated untracked files unless clearly part of the resolution.
8. Verify no conflict markers remain (search for `<<<<<<<` / `>>>>>>>` in resolved files) and that `git status` shows no unmerged paths.
## Phase 5 — Report
9. Briefly report the conflict state, how each conflicted file was resolved (and any answers received to open questions), and stop — do **not** run `git rebase --continue` or any other continuation command.
-10
View File
@@ -1,10 +0,0 @@
---
description: Code review — review a diff, PR, or code change — loads and follows the review-code skill
agent: build
---
Load the **`review-code`** skill and follow it as your only instruction.
## User input, overrides and additional context
$ARGUMENTS
-10
View File
@@ -1,10 +0,0 @@
---
description: Plan review — evaluate an implementation plan before executing it — loads and follows the review-plan skill
agent: build
---
Load the **`review-plan`** skill and follow it as your only instruction.
## User input, overrides and additional context
$ARGUMENTS
@@ -9,11 +9,6 @@ metadata: {"clawdbot":{"emoji":"🦇","requires":{"bins":["bat"]},"install":[{"i
`cat` with syntax highlighting, line numbers, and Git integration.
## When to use
- Reading or displaying a file in the terminal — prefer it over plain
`cat`: syntax highlighting, line numbers, git-side indicators.
## Quick Start
### Basic usage
@@ -1,9 +1,9 @@
---
name: code-review-criteria
description: Code review criteria — the five review axes, core principles, severity format, and verdict for reviewing code changes. Loaded by the reviewer subagent of the review-code flow. Not a user-facing flow — to review code, use the review-code flow.
name: code-review
description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.
---
# Code Review Criteria and Quality
# Code Review and Quality
## Overview
@@ -13,10 +13,11 @@ Multi-dimensional code review with quality gates. Every change gets reviewed bef
## When to Use
- The reviewer subagent of the `review-code` flow loads this skill to perform
the review of a code change.
- To review code, always go through the `review-code` flow — never load this
skill directly for that. This is the criteria reference, not the flow.
- Before merging any PR or change
- After completing a feature implementation
- When another agent or model produced code you need to evaluate
- When refactoring existing code
- After any bug fix (review both the fix and the regression test)
## Core Principles
File renamed without changes.
+39
View File
@@ -0,0 +1,39 @@
---
name: create-pr
description: Create or update a GitHub PR following Penpot conventions.
---
# Skill: create-pr
Create or update a GitHub PR. Read and follow:
- `mem:workflow/creating-prs` — title format, description structure, writing principles
- `mem:workflow/creating-commits` — commit type emojis
## When to Use
- Creating a new PR from a feature branch
- Updating an existing PR's title or description to match conventions
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
## Commands
**Create:**
```bash
gh pr create --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
**Update:**
```bash
gh pr edit <NUMBER> --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
**Verify:**
```bash
gh pr view <NUMBER> --repo penpot/penpot --json title,body
```
@@ -9,11 +9,6 @@ metadata: {"clawdbot":{"emoji":"📂","requires":{"bins":["fd"]},"install":[{"id
User-friendly alternative to `find` with smart defaults.
## When to use
- Locating files or directories by name or pattern — prefer it over
plain `find`: simpler syntax, smart defaults, respects `.gitignore`.
## Quick Start
### Basic search
@@ -9,11 +9,6 @@ metadata: {"clawdbot":{"emoji":"🔍","requires":{"bins":["jq"]},"install":[{"id
Process, filter, and transform JSON data with jq.
## When to use
- Parsing, filtering, or transforming JSON from commands, files, or API
responses — slicing, reshaping, or validating JSON output.
## Quick Examples
### Basic filtering
@@ -10,12 +10,6 @@ Evaluate Clojure (or ClojureScript) code via a running nREPL server using
Full documentation: `mem:scripts/nrepl-eval` (file: `.serena/memories/scripts/nrepl-eval.md`)
## When to use
- Evaluating Clojure or ClojureScript code against the running nREPL
sessions (backend 6064, frontend 3447) — live inspection, patching, or
debugging.
## Quick Reference
```bash
@@ -1,9 +1,9 @@
---
name: plan-review-criteria
description: Plan review criteria — the six review axes, severity rubric, approval standard, and output format for reviewing implementation plans. Loaded by the reviewer subagent of the review-plan flow. Not a user-facing flow — to review a plan, use the review-plan flow.
name: plan-review
description: Reviews implementation plans for quality, completeness, and actionability. Use after a plan is produced by the planner skill, before starting implementation. Use when evaluating a plan written by yourself, another agent, or a human.
---
# Plan Review Criteria
# Plan Review
## Overview
@@ -13,10 +13,10 @@ Multi-dimensional plan review with quality gates. Every plan gets reviewed befor
## When to Use
- The reviewer subagent of the `review-plan` flow loads this skill to perform
the review of a plan.
- To review a plan, always go through the `review-plan` flow — never load this
skill directly for that. This is the criteria reference, not the flow.
- After the planner skill produces a plan
- Before starting implementation on any non-trivial task
- When reviewing a plan written by another agent or a human
- When a plan feels too large, vague, or risky to start
**Do NOT use for:** Single-file changes with obvious scope, or when the task is trivial enough to just do.
@@ -87,7 +87,7 @@ Can an implementer actually execute this?
### 6. Proposed Code Quality *(when the plan includes implementation details)*
If the plan proposes code shapes, function signatures, data structures, or API designs, evaluate those proposals against `code-review-criteria`:
If the plan proposes code shapes, function signatures, data structures, or API designs, evaluate those proposals against `code-review` criteria:
- **Correctness:** Do the proposed types/signatures handle edge cases (null, empty, boundaries)?
- **Readability:** Are proposed names descriptive and consistent with project conventions?
@@ -215,7 +215,7 @@ Check that the plan can actually confirm it worked:
If the plan includes code snippets, types, or API designs:
```
- Load code-review-criteria skill for criteria
- Load code-review skill for criteria
- Check proposed signatures for edge cases
- Verify naming follows project conventions
- Confirm abstractions follow existing patterns
@@ -310,6 +310,6 @@ If the plan includes code snippets, types, or API designs:
## See Also
- For producing plans, use the `planner` skill
- For reviewing implemented code, use `code-review-criteria` — also the criteria source for axis 6
- For reviewing implemented code, use `code-review` — also the criteria source for axis 6
- For security-specific concerns, see `security-and-hardening`
- For testing strategy guidance, see `testing`
@@ -1,6 +1,6 @@
---
name: planner
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan with task breakdown, acceptance criteria, sizing, and checkpoints. Always output to the user with the plan's save path (saved or suggested) and the next steps.
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan with task breakdown, acceptance criteria, sizing, and checkpoints. Always output to the user and save to .opencode/plans/YYYY-MM-DD-<title>.md.
---
# Planner
@@ -215,9 +215,9 @@ Add explicit checkpoints with the relevant module commands:
## Constraints
- You are **analysis-only** — never create, edit, or delete source code. The
only file you may write is the plan itself, and only when the command or
user explicitly instructs you to save it.
- You are **analysis-only** — never create, edit, or delete source code.
- The only file write you may attempt is the plan itself, saved to
`.opencode/plans/`.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
@@ -228,23 +228,22 @@ Add explicit checkpoints with the relevant module commands:
## Output Format
The plan is always delivered in the response so the user sees it regardless
of which agent is running the skill. By default you never write the plan file;
announce the path instead. Write the file only when the command or user
explicitly instructs you to save it — and then only that file.
of which agent is running the skill.
Announce the suggested save path:
Additionally, save the plan to:
```
.agents/plans/YYYY-MM-DD-<plan-one-line-title>.md
.opencode/plans/YYYY-MM-DD-<plan-one-line-title>.md
```
Use today's date in the user's local timezone. The `<plan-one-line-title>`
slug is lowercase, hyphen-separated, and a short summary of the task
(e.g. `add-batch-get-profiles-for-file-comments`). If the user explicitly
provides a target file path, announce that path instead of the default.
(e.g. `add-batch-get-profiles-for-file-comments`). Create the
`.opencode/plans/` directory if it does not exist.
End the response by suggesting the next steps: `/review-plan` to get a second
opinion on the plan and `/implement-plan` to execute it.
IMPORTANT: The plan agent has write permission specifically for
`.opencode/plans/` — always attempt the write. If the user explicitly provides
a target file path, use that path instead of the default.
### Plan Document Template
@@ -375,6 +374,4 @@ Before delivering the plan, confirm:
- [ ] Task dependencies are identified and ordered correctly
- [ ] No task is XL or larger — break it down instead
- [ ] Checkpoints exist after every 2-3 tasks
- [ ] The response states the plan's path (saved or suggested) and suggests
`/review-plan` and `/implement-plan`
- [ ] The plan is ready for human review
@@ -9,11 +9,6 @@ metadata: {"clawdbot":{"emoji":"🔎","requires":{"bins":["rg"]},"install":[{"id
Fast, smart recursive search. Respects `.gitignore` by default.
## When to use
- Searching file contents across the repo for regex patterns — the
default code search, respects `.gitignore`.
## Quick Start
### Basic search
@@ -9,13 +9,6 @@ Apply the ASD-STE100 standard to all prose you produce in this task. Do not anno
Compliance note (for you, not for output): the official specification and its dictionary are copyright ASD. This skill encodes paraphrased rules and a publicly sourced word list. For certified aerospace/defense deliverables, tell the user that full compliance requires the free official specification (asd-ste100.org) and a human sign-off. Never claim certified compliance.
## When to use
Only when the user explicitly invokes it: they type `/ste`, or say "use
the ste skill" / "apply ASD-STE100". Requests like "simplify this",
"make it clearer", or "shorter sentences" do NOT invoke it — respond
normally unless it is named.
## Step 0 — Classify the text
Before writing a single sentence, decide: is this **procedural** text (instructions someone follows) or **descriptive** text (explanation, background, description)? Every limit below depends on this. Mixed documents get classified section by section.
@@ -11,12 +11,6 @@ Fetch information from Taiga public API for the **Penpot** project
**No authentication required** — only public project data is accessed.
## When to use
- The user asks about Penpot issues, user stories, or tasks tracked in
Taiga — fetch them via the public API (project id 345963), no
authentication needed.
## Prerequisites
- `python3` — the `scripts/taiga.py` CLI script is self-contained (stdlib only)
File renamed without changes.
@@ -357,39 +357,6 @@ Insert the new version section right after the `# CHANGELOG` header (before
the previous version entry). Use the `edit` tool with enough context to make
a unique match.
### 8b. Propose and populate the `:rocket: Epics and highlights` subsection
After inserting the version section, proactively create or populate the
`### :rocket: Epics and highlights` subsection. This section surfaces the
most impactful changes for self-hosted users checking for updates.
**When to create:** If the version section does not already have a
`### :rocket: Epics and highlights` subsection, create one. Place it before
`### :sparkles:` (matching existing order in CHANGES.md).
**How to identify highlights:** Review the `:sparkles:` entries for the
version and select 25 of the most impactful/user-visible ones. Criteria:
- New user-visible features (not internal refactors)
- Significant capability additions
- Items that create "FOMO" for self-hosted users on older versions
**Use release notes as hints:** Check
`frontend/src/app/main/ui/releases/v2_<MINOR>.cljs` for the corresponding
version. The slide titles and feature descriptions there are curated
marketing content indicating what the team considers highlight-worthy. Match
those themes to changelog entries. Treat these files as optional hints — they
may not exist for every version.
**Format requirement:** Every `:rocket:` entry MUST follow the standard
changelog format with issue/PR references:
```
- <description> [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
```
An entry without issue AND PR references is a highlight gap (warning, not an anomaly).
**Preserve existing entries:** If the `:rocket:` section already exists from
a prior run, preserve its entries. Do not remove or rewrite them.
### 9. Verify
Read the top of `CHANGES.md` and confirm:
@@ -501,8 +468,9 @@ Markdown viewer.
## What is an anomaly
**An anomaly is a milestone-mismatch between an issue and its referenced
PR.** There are two anomaly types, plus two highlight gaps (warnings that
do not count toward the anomaly total):
PR.** It indicates that the changelog claim "this issue is fixed by this PR,
all in milestone M" is inconsistent with the actual milestone assignments.
There are exactly two types:
1. **Issue is in the milestone, but its referenced PR is in a different
milestone (or has no milestone).** The changelog claims a fix in this
@@ -518,13 +486,6 @@ do not count toward the anomaly total):
PR that closes an issue with no milestone references an issue from
another (probably private) project; that is expected and the issue is
not part of this changelog. Do not report it.
3. **missing-highlights (gap):** A released X.Y.0 version section has no
`### :rocket: Epics and highlights` subsection. Patches (X.Y.Z) never
carry highlights, so only minors/majors are checked.
4. **missing-highlight-reference (gap):** A `:rocket:` entry lacks the
required issue AND PR references. Every highlight entry must follow the
standard changelog format with `[#ISSUE]` and `(PR: [#PR])` links
(multi-PR `(PR: [#A](...), [#B](...))` accepted).
**Anything else is not an anomaly.** Other discrepancies (exclusion
labels on in-changelog issues, missing valid issues, unmerged PR
@@ -692,40 +653,6 @@ for pr_num in sorted(changelog_prs):
'issue_milestone': issue_ms, # may be None
})
# --- Type C: released X.Y.0 version sections without :rocket: subsection ---
# Patches (X.Y.Z with Z != 0) never carry :rocket: by design — only minors/majors (X.Y.0).
anomalies_c = [] # list of version strings
rocket_heading_re = re.compile(r'^### :rocket:', re.MULTILINE)
version_sections = re.split(r'(?=^## \d+\.\d+\.\d+)', content, flags=re.MULTILINE)
for vs in version_sections:
m = re.match(r'^## (\d+\.\d+\.\d+)(.*)', vs)
if not m: continue
ver, suffix = m.group(1), m.group(2)
if 'unreleased' in suffix.lower(): continue
if ver.split('.')[2] != '0': continue
if not rocket_heading_re.search(vs):
anomalies_c.append(ver)
# --- Type D: :rocket: entries without issue AND PR references ---
# Both are required: `[#ISSUE](.../issues/N)` and `(PR: [#PR](.../pull/M))`.
# Multi-PR entries `(PR: [#A](...), [#B](...))` are accepted.
anomalies_d = [] # list of dicts: {version, line}
issue_ref_re = re.compile(r'\[#\d+\]\(https://github\.com/penpot/penpot/issues/\d+\)')
pr_ref_re = re.compile(r'\(PR:\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\)(\s*,\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\))*\)')
for vs in version_sections:
m = re.match(r'^## (\d+\.\d+\.\d+)(.*)', vs)
if not m: continue
ver = m.group(1)
rocket_match = rocket_heading_re.search(vs)
if not rocket_match: continue
# Extract the :rocket: subsection body (up to next ### or ##)
rocket_body = vs[rocket_match.end():]
rocket_body = re.split(r'(?m)^#{2,3}\s', rocket_body)[0]
for line in rocket_body.splitlines():
line = line.strip()
if line.startswith('- ') and not (issue_ref_re.search(line) and pr_ref_re.search(line)):
anomalies_d.append({'version': ver, 'line': line[:100]})
# --- Write report ---
def fmt_ms(ms):
return ms if ms else "_none_"
@@ -737,17 +664,13 @@ with open(OUTPUT, 'w') as f:
n_a = len(anomalies_a)
n_b = len(anomalies_b)
n_c = len(anomalies_c)
n_d = len(anomalies_d)
f.write('## Summary\n\n')
f.write(f'- **Issue in {MILESTONE}, referenced PR in different milestone or no milestone:** {n_a}\n')
f.write(f'- **PR in {MILESTONE}, closing issue in a different milestone:** {n_b}\n')
f.write(f'- **Total anomalies:** {n_a + n_b}\n')
f.write(f'- **Released X.Y.0 version missing :rocket: section (gap):** {n_c}\n')
f.write(f'- **:rocket: entry without issue AND PR references (gap):** {n_d}\n\n')
f.write(f'- **Total anomalies:** {n_a + n_b}\n\n')
# --- Anomalies section (milestone mismatches only) ---
# --- Anomalies section ---
if n_a or n_b:
f.write('## Anomalies\n\n')
f.write('These are milestone mismatches between an issue in the changelog '
@@ -786,37 +709,9 @@ with open(OUTPUT, 'w') as f:
badge = '🔴' if e['issue_milestone'] is None else '⚠️'
f.write(f' - {badge} Closing {issue_link(e["issue"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n')
f.write('\n')
else:
f.write('✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n')
# --- Highlight gaps (warnings, not anomalies) ---
if n_c or n_d:
f.write('## Highlight gaps\n\n')
f.write('These are warnings, not anomalies: they do not affect the '
'milestone-mismatch total above. They track `:rocket:` coverage '
'across all released X.Y.0 versions. Historical entries (e.g. '
'Taiga links) predate the current reference convention and are '
'expected to appear here.\n\n')
if n_c:
f.write(f'### Released X.Y.0 version missing :rocket: section\n\n')
f.write('These released minors/majors have no `### :rocket: Epics and highlights` subsection. '
'Add highlights to help self-hosted users understand what they are missing.\n\n')
for ver in anomalies_c:
f.write(f'- Version **{ver}**\n')
f.write('\n')
if n_d:
f.write(f'### :rocket: entry without issue AND PR references\n\n')
f.write('These highlight entries lack the required issue AND PR references. '
'Add `[#ISSUE](...)` and `(PR: [#PR](...))` links.\n\n')
for d in anomalies_d:
f.write(f'- **{d["version"]}**: `{d["line"]}`\n')
f.write('\n')
elif not (n_a or n_b):
f.write('✅ No highlight gaps found. All released X.Y.0 versions have properly referenced :rocket: entries.\n\n')
# --- Context ---
f.write('---\n\n')
f.write('## Context\n\n')
@@ -831,7 +726,8 @@ print(f"Anomaly report written to {OUTPUT}")
PYEOF
```
This generates `CHANGES-ISSUES.md` containing anomalies and highlight gaps:
This generates `CHANGES-ISSUES.md` containing **only the anomalies**
milestone mismatches between issues and their referenced PRs:
1. **Issue in milestone, referenced PR in different milestone or no milestone**
the changelog claims a fix here, but the PR is released elsewhere.
@@ -840,13 +736,6 @@ This generates `CHANGES-ISSUES.md` containing anomalies and highlight gaps:
(An issue with *no* milestone belongs to another, probably private,
project — milestones are only required on the "Main" project — so it is
neither an anomaly nor a changelog candidate.)
3. **missing-highlights (gap, warning)** — a released X.Y.0 version section
has no `### :rocket: Epics and highlights` subsection. Patches (X.Y.Z)
never carry highlights.
4. **missing-highlight-reference (gap, warning)** — a `:rocket:` entry lacks
the required issue AND PR references.
Gaps do not count toward the anomaly total.
**Rule violations are not in the report** — they are workflow errors the
LLM must fix directly in `CHANGES.md` during step 6a (pre-flight checks).
@@ -920,13 +809,10 @@ self-contained and clickable in any Markdown viewer.
issue from a different project or context. If the PR title and issue title
are clearly unrelated, or the PR predates the issue by years, treat it as a
data glitch and skip it.
- **Anomaly = milestone mismatch only; gaps are warnings.** The report's
anomaly total counts only milestone mismatches: (1) the issue is in this
milestone but the referenced PR is in a different milestone (or unassigned),
and (2) the PR is in this milestone but the issue it closes is in a
different milestone. `:rocket:` highlight gaps (missing section on a
released X.Y.0, entry without issue AND PR references) are reported in a
separate `Highlight gaps` section and never count toward the anomaly total. An
- **Anomaly = milestone mismatch only.** The report contains only milestone
mismatches: (1) the issue is in this milestone but the referenced PR is
in a different milestone (or unassigned), and (2) the PR is in this
milestone but the issue it closes is in a different milestone. An
*unassigned* (milestone-less) issue closed by a milestone PR is **not**
an anomaly: milestones are required only for the "Main" project, so such
issues come from another (probably private) project and are not changelog
-12
View File
@@ -14,11 +14,7 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
- Before `git commit``mem:workflow/creating-commits` (subject format, body, `AI-assisted-by: model-name` trailer)
- Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, labels, Issue Type)
- Before `gh pr create` / `gh pr edit``mem:workflow/creating-prs` (title format, body structure, "Note:" line)
- Before a repo-wide pnpm version update → `mem:workflow/updating-pnpm` (workspace
layout, `corepack use` sweep order, the stamp-missing-field and
ignored-builds gotchas, verification steps)
- **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, say so and wait. Never amend a commit that the user has already pushed unless explicitly asked.
- **Never edit `CHANGES.md` by hand.** The changelog is generated from GitHub milestones during the release process; update it only via the `update-changelog` skill flow or on explicit user request.
- You have access to the GitHub CLI `gh` or corresponding MCP tools.
- Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool.
- Before writing code, analyze the task in depth and describe your plan. If the task is complex, break it down into atomic steps.
@@ -74,14 +70,6 @@ module. You can read it from `mem:<MODULE>/core`
- `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`.
- `scripts/clean-node-modules` — Remove stale `node_modules` from all pnpm
workspaces (root, modules, member packages). Keeps the shared pnpm store
at `<repo>/.pnpm-store` unless `--store`; ignores `external/` and
`.opencode/`. Usage and reinstall steps: `mem:workflow/updating-pnpm`.
- `scripts/ci` — CI orchestration script: runs lint, tests, and format
checks per module (`frontend backend common render-wasm exporter mcp
plugins library`). Logs go to `.ci-logs/`; read the log file on failure.
See `mem:scripts/ci`.
# Dependency graph
-61
View File
@@ -1,61 +0,0 @@
# CI (scripts/ci)
`scripts/ci` runs CI-style checks — lint, tests, format — for one or more
monorepo modules and prints a per-task summary. It is the local equivalent
of CI; use it to verify changes before declaring work done.
## When to use
- After implementing or fixing code in a module: run its checks before
finishing (AGENTS.md: run the applicable lint and format checks).
- When `common/` changed: validate its consumers too (frontend, backend,
exporter; see the dependency graph in `mem:critical-info`).
- To fix formatting across a module (`--fix`) or repair delimiters
(`--paren-repair`) before linting.
## How to use (CLI)
Run from the repo root:
```bash
./scripts/ci MODULE... # lint + test + fmt per module
./scripts/ci --all --no-test # lint + fmt on all modules
./scripts/ci --lint frontend # lint only
./scripts/ci --fix --no-test frontend # format files, skip tests
./scripts/ci --paren-repair --all # fix delimiters in all Clojure modules
./scripts/ci --dry-run --all # preview what would run
```
Modules: `frontend backend common render-wasm exporter mcp plugins library`.
Flags:
- Default tasks: `lint`, `test`, `fmt` (format check; `--fix` formats
instead).
- `--lint` / `--test` / `--fmt` run one task only; `--no-lint` /
`--no-test` / `--no-fmt` drop one task from the default set.
- `--paren-repair` runs only the delimiter repair — it wraps
`scripts/paren-repair` over each module's Clojure/CLJS sources; see
`mem:scripts/paren-repair`.
- `--all` selects every module; `--exclude MOD` drops one (repeatable).
- `--fail-fast` stops at the first failure; `--quiet` suppresses failure
output; `--dry-run` prints commands without running; `--clean` removes
the log directory.
## Logs and exit codes
- Full output of every task: `.ci-logs/<module>-<task>.log`.
- On failure the script prints the last 30 lines; the final summary lists
every failed `module:task` with its log path.
- Exit code 0 when all selected tasks passed, 1 otherwise.
- Diagnose failures by reading the log file — never pipe test output
through filters (AGENTS.md hard rule).
## Notes
- `mcp` has no lint task (shows as skipped). `render-wasm` uses `./lint`,
`./test`, and `cargo fmt`.
- Test tasks are long-running (backend: `clojure -M:dev:test`); use a
generous timeout when calling it from an agent shell.
- Skill entry point: `.agents/skills/local-ci/SKILL.md`.
- Testing principles and output discipline: `mem:testing`.
+1 -1
View File
@@ -351,5 +351,5 @@ gh issue view <NUMBER> --repo penpot/penpot --json title
## See Also
- End-to-end orchestration entry point: the `create-issue` skill at
`.agents/skills/create-issue/SKILL.md`. The skill is a thin entry
`.opencode/skills/create-issue/SKILL.md`. The skill is a thin entry
point; this memory is the canonical home for all issue-creation rules.
@@ -1,89 +0,0 @@
# Updating pnpm Across All Workspaces
Canonical procedure. Run it from the repo root with the log redirected to a
file (never pipe tool output through filters).
## Layout facts
- The repo has 11 pnpm workspaces, each with its own `pnpm-workspace.yaml`
and `pnpm-lock.yaml`: the repo root plus `backend`, `common`, `docs`,
`exporter`, `frontend`, `library`, `mcp`, `media-processor`, `plugins`,
and `render-wasm`.
- Every package inside a module workspace (for example all `plugins/apps/*`
and `plugins/libs/*` packages) is a plain member of that module's
workspace. Members must not carry their own `pnpm-workspace.yaml` or
`pnpm-lock.yaml`; their dependencies resolve through the parent
workspace's lockfile.
- One shared pnpm store for the whole repo: `<repo>/.pnpm-store`. Every
workspace yaml sets it explicitly: `storeDir: .pnpm-store` at the root,
`storeDir: ../.pnpm-store` in each module. pnpm resolves the value
against the workspace root, so all workspaces land on the same store.
Do not remove these lines: nested workspaces do not inherit settings,
and without them each workspace may resolve a different store.
- The store survives `node_modules` cleans. It is content-addressed and
integrity-verified, so it cannot go stale; staleness lives in
node_modules. Only `scripts/clean-node-modules --store` removes it.
- Every `package.json` (about 35 of them) must carry a `packageManager` field
with the identical `pnpm@<version>+sha512.<hash>` value. Do not let them drift.
- CI pins no pnpm version; workflows rely on corepack reading
`packageManager`. Fixing the fields fixes CI.
## Procedure
1. Resolve the target tag first and note the version. Example:
`npm view pnpm dist-tags --json` for `next-12` (latest 12.x). The tag
moves over time; always re-check.
2. List every directory with a `package.json`, excluding `node_modules`
(`fd -H -t f package.json -E node_modules`). This list is the work set;
do not maintain a hand-written list.
3. Run `corepack use pnpm@<tag>` in workspace roots first, then members.
`corepack use` stamps `packageManager` in the nearest package.json and
runs an install. Member runs repeat the workspace install; after the root
run they are quick no-ops.
4. If a run fails, fix the cause (see gotchas) and re-run that directory.
## Gotchas
- `corepack use` only updates an existing `packageManager` field. If a
package.json lacks the field, corepack walks up to the nearest ancestor
that has one and stamps that file instead; the member stays unstamped.
After the sweep, assert every package.json carries the field. For a
missing one, insert the identical `pnpm@<version>+sha512.<hash>` string,
then re-run `corepack use pnpm@<tag>` in that directory.
- A workspace may fail with `ERR_PNPM_IGNORED_BUILDS`, and pnpm then writes
a placeholder scaffold into its `pnpm-workspace.yaml`:
`allowBuilds: esbuild: set this to true or false` plus
`ignoredBuiltDependencies`. Repo convention is `allowBuilds: esbuild: true`.
Replace the placeholder and drop the `ignoredBuiltDependencies` entry,
then re-run.
- `plugins/apps/composable-test-suite` once had its own
`pnpm-workspace.yaml` and acted as a nested workspace root. That state is
gone on purpose: pnpm picks the nearest `pnpm-workspace.yaml` walking up,
so a nested one silently forks install and lockfile behavior. Do not
reintroduce it.
- Expect metadata-only lockfile diffs when only the pnpm version moves:
the pnpm self-reference entries, plus a new `packageManagerDependencies`
section in lockfiles last written by older pnpm. Large diffs mean
re-resolution; inspect them before accepting.
## Verification
- Every `packageManager` field is byte-identical (same version and hash).
- `pnpm --version` in each workspace prints the target version.
- `pnpm install --frozen-lockfile` succeeds in each of the 11 workspaces.
- `git diff` on lockfiles matches the expectations above.
## Cleaning stale node_modules
- `scripts/clean-node-modules` removes every workspace `node_modules`: the
repo root, all module workspaces, and all member packages. Use it when
installs misbehave after dependency changes: clean, reinstall, done.
- Flags: `-n/--dry-run` lists without deleting; `--store` also removes the
shared pnpm store at `<repo>/.pnpm-store` (the next install re-downloads
what it held). `external/` (vendored dependency trees with their own
lifecycles) and `.opencode/` are always ignored.
- The script never touches the pnpm store by default, so the reinstall
after cleaning reuses cached packages (zero downloads).
- After cleaning, run `pnpm install` in each workspace root to restore the
development environment; `frontend` postinstall also reinstalls and
builds `plugins-runtime`.
+1 -4
View File
@@ -8,9 +8,6 @@
wait for the user to push. Do not change the remote URL, do not switch SSH↔HTTPS.
- **Never amend a commit that has been pushed** unless the user explicitly asks.
If the user pushes, treat that commit as final from the agent's side.
- **Never edit `CHANGES.md` by hand** in commits or PRs. The changelog is
generated from GitHub milestones during the release process; update it only
via the `update-changelog` skill flow or on explicit user request.
- **Never pipe test output directly to filters** (`| head`, `| tail`, `| grep`, etc.).
Always redirect to a file first: `command > /tmp/output.txt 2>&1`, then read/grep the file.
This prevents hiding test failures. See `mem:testing` for details.
@@ -145,6 +142,6 @@ precision while maintaining a strong focus on maintainability and performance.
- `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.
- `scripts/ci` — CI orchestration script for running lint, tests, and format checks across modules. See `mem:scripts/ci`.
- `scripts/ci` — CI orchestration script for running lint, tests, and format checks across modules. See `scripts/ci --help`.
- `scripts/gh.py` — Multi-purpose GitHub CLI helper. Subcommands: `issues` (list issues in a milestone), `prs` (fetch PR details), `advisories` (list/inspect security advisories). See `python3 scripts/gh.py --help`.
+1 -42
View File
@@ -2,12 +2,6 @@
## 2.19.0 (Unreleased)
### :rocket: Epics and highlights
- Add configurable keyboard shortcuts [#9924](https://github.com/penpot/penpot/issues/9924) (PR: [#10237](https://github.com/penpot/penpot/pull/10237))
- Improve path operations and edition in the path editor [#10889](https://github.com/penpot/penpot/issues/10889) (PR: [#10807](https://github.com/penpot/penpot/pull/10807))
- Add auto-linking of libraries during import based on slugified name [#9263](https://github.com/penpot/penpot/issues/9263) (PR: [#9958](https://github.com/penpot/penpot/pull/9958))
### :bug: Bugs fixed
- Fix copying text from Penpot to the clipboard not working on MS Windows [#11303](https://github.com/penpot/penpot/issues/11303) (PR: [#11305](https://github.com/penpot/penpot/pull/11305))
@@ -17,20 +11,6 @@
- Fix overlay shifting left when shown with top-center alignment in viewer prototype (by @filipsajdak) [#9048](https://github.com/penpot/penpot/issues/9048) (PR: [#10454](https://github.com/penpot/penpot/pull/10454))
- Fix internal error when clicking the Copy button on the Access Token page (by @0xTHAC0) [#8496](https://github.com/penpot/penpot/issues/8496) (PR: [#11156](https://github.com/penpot/penpot/pull/11156))
- Fix `disable-registration` flag not preventing non-users from creating accounts in the share prototypes page (by @0xTHAC0) [#5164](https://github.com/penpot/penpot/issues/5164) (PR: [#11199](https://github.com/penpot/penpot/pull/11199))
- Fix "Cannot assign to read only property 'toString'" error during text resize (by @makesomethingshit) [#10168](https://github.com/penpot/penpot/issues/10168) (PR: [#11521](https://github.com/penpot/penpot/pull/11521))
- Fix plugin postMessage channel broadcasting messages to all plugins without origin validation [#10968](https://github.com/penpot/penpot/issues/10968) (PR: [#10970](https://github.com/penpot/penpot/pull/10970))
- Fix MCP plugin page navigation while connected crashing the workspace (by @makesomethingshit) [#11001](https://github.com/penpot/penpot/issues/11001) (PR: [#11521](https://github.com/penpot/penpot/pull/11521))
- Fix shortcut search never matching on key combination, only on action label [#11003](https://github.com/penpot/penpot/issues/11003) (PR: [#11081](https://github.com/penpot/penpot/pull/11081))
- Fix Shift + special character key shortcut capturing the shifted character instead of the physical key [#11004](https://github.com/penpot/penpot/issues/11004) (PR: [#11081](https://github.com/penpot/penpot/pull/11081))
- Fix reassigning the "Paste" shortcut not updating the UI or taking effect in the workspace [#11005](https://github.com/penpot/penpot/issues/11005) (PR: [#11081](https://github.com/penpot/penpot/pull/11081))
- Fix font-size dropdown clipping multi-digit values in Firefox (by @0xTHAC0) [#11008](https://github.com/penpot/penpot/issues/11008) (PR: [#11162](https://github.com/penpot/penpot/pull/11162), [#11500](https://github.com/penpot/penpot/pull/11500))
- Fix exporting shortcuts producing an invalid "toggle-fullscreen" entry that breaks re-import [#11032](https://github.com/penpot/penpot/issues/11032) (PR: [#11081](https://github.com/penpot/penpot/pull/11081))
- Fix plugin API missing permission checks in tokens, shapes, variants, flows, layouts, and user identity [#11137](https://github.com/penpot/penpot/issues/11137) (PR: [#11139](https://github.com/penpot/penpot/pull/11139))
- Fix library summary Redis cache keys omitting the tenant [#11407](https://github.com/penpot/penpot/issues/11407) (PR: [#11408](https://github.com/penpot/penpot/pull/11408))
- Fix active theme name in the inspect tab displaying an id instead of the name [#11437](https://github.com/penpot/penpot/issues/11437) (PR: [#11439](https://github.com/penpot/penpot/pull/11439))
- Fix triple-click not selecting the full line in text editor v3 [#11483](https://github.com/penpot/penpot/issues/11483) (PR: [#11493](https://github.com/penpot/penpot/pull/11493))
- Fix pasted text losing formatting on last lines after resizing and adding new lines from the top [#11501](https://github.com/penpot/penpot/issues/11501) (PR: [#11503](https://github.com/penpot/penpot/pull/11503))
- Fix variant property dropdown appearing empty and throwing an internal error when the component has no sibling variants [#11524](https://github.com/penpot/penpot/issues/11524) (PR: [#11499](https://github.com/penpot/penpot/pull/11499))
### :sparkles: New features & Enhancements
@@ -39,22 +19,9 @@
- Improve path operations and edition in the path editor [#10889](https://github.com/penpot/penpot/issues/10889) (PR: [#10807](https://github.com/penpot/penpot/pull/10807))
- Add configurable keyboard shortcuts [#9924](https://github.com/penpot/penpot/issues/9924) (PR: [#10237](https://github.com/penpot/penpot/pull/10237))
- Add auto-linking of libraries during import based on slugified name [#9263](https://github.com/penpot/penpot/issues/9263) (PR: [#9958](https://github.com/penpot/penpot/pull/9958))
- Add support for internal libraries and file sync for Design Tokens [#9334](https://github.com/penpot/penpot/issues/9334)
- Warn self-hosted users when their Penpot version is outdated and surface what they're missing [#10497](https://github.com/penpot/penpot/issues/10497) (PR: [#11411](https://github.com/penpot/penpot/pull/11411))
- Add dedicated RPC methods for plugin registry operations with permission validation [#10952](https://github.com/penpot/penpot/issues/10952) (PR: [#10957](https://github.com/penpot/penpot/pull/10957))
- Document MCP and internal resolver environment variables (by @ShreyashAgare26) [#11318](https://github.com/penpot/penpot/issues/11318) (PR: [#11572](https://github.com/penpot/penpot/pull/11572))
- Add tokens source indicator to assets tab [#11365](https://github.com/penpot/penpot/issues/11365) (PR: [#11439](https://github.com/penpot/penpot/pull/11439))
- Export multiple fills to SVG [#11466](https://github.com/penpot/penpot/issues/11466) (PR: [#11467](https://github.com/penpot/penpot/pull/11467))
- Add Penpot-specific board size presets (file thumbnail, template cover, plugin icon/cover) [#11561](https://github.com/penpot/penpot/issues/11561) (PR: [#11565](https://github.com/penpot/penpot/pull/11565))
## 2.18.0 (Unreleased)
### :rocket: Epics and highlights
- Group toolbar drawing tools into shape and free-draw flyouts [#9316](https://github.com/penpot/penpot/issues/9316) (PR: [#9480](https://github.com/penpot/penpot/pull/9480), [#10354](https://github.com/penpot/penpot/pull/10354))
- Add dedicated Line and Arrow drawing tools (by @davidv399) [#9145](https://github.com/penpot/penpot/issues/9145) (PR: [#9146](https://github.com/penpot/penpot/pull/9146))
- 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))
### :bug: Bugs fixed
- Fix MCP integration hanging when the Penpot tab is backgrounded or frozen by the browser [#10323](https://github.com/penpot/penpot/issues/10323) (PR: [#10392](https://github.com/penpot/penpot/pull/10392))
@@ -218,10 +185,6 @@
### :rocket: Epics and highlights
- Render prototype viewer with WASM (Skia) engine instead of SVG [#10037](https://github.com/penpot/penpot/issues/10037) (PR: [#10038](https://github.com/penpot/penpot/pull/10038))
- Add layer blur effect for visual depth and styling [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))
- Render guides in WebGL for consistent viewer performance [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))
- Add concurrency limiter and status indicators for MCP server communications [#9493](https://github.com/penpot/penpot/issues/9493) (PR: [#9748](https://github.com/penpot/penpot/pull/9748))
- Add typography token row to multiselected texts for better token visibility [#9336](https://github.com/penpot/penpot/issues/9336) (PR: [#9128](https://github.com/penpot/penpot/pull/9128))
### :sparkles: New features & Enhancements
@@ -397,7 +360,7 @@
### :rocket: Epics and highlights
- WebGL rendering (beta) user preference [#9683](https://github.com/penpot/penpot/issues/9683) (PR: [#9113](https://github.com/penpot/penpot/pull/9113))
- WebGL rendering (beta) user preference [#9683](https://github.com/penpot/penpot/issues/9683) (PR:[9113](https://github.com/penpot/penpot/pull/9113))
- Design Tokens at the design tab: numeric fields with token selection in place [#9358](https://github.com/penpot/penpot/issues/9358)
### :sparkles: New features & Enhancements
@@ -576,10 +539,6 @@
## 2.15.0
### :rocket: Epics and highlights
- Add MCP server integration for AI-assisted design workflows [#9174](https://github.com/penpot/penpot/issues/9174) (PR: [#9032](https://github.com/penpot/penpot/pull/9032), [#9321](https://github.com/penpot/penpot/pull/9321))
### :sparkles: New features & Enhancements
- Add MCP server integration [GH #9174](https://github.com/penpot/penpot/issues/9174)
-3
View File
@@ -1,3 +0,0 @@
Read and follow the instructions in `AGENTS.md`.
Treat `AGENTS.md` as the canonical project instruction file.
+26
View File
@@ -0,0 +1,26 @@
# HIGHLIGHTS
## 2.17.0
- Background blur is here
- WebGL rendering gets stronger
- MCP connection status and more
- Design tokens: more visible, more user-friendly
## 2.16.0
- Design tokens in the design panel
- Major community contributions
- WebGL rendering (beta)
## 2.15.0
- AI connected to real design context
- Multi-directional workflow
- Your stack, your model, your decision
+1 -1
View File
@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL",
"private": true,
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457",
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
-101
View File
@@ -1,104 +1,3 @@
---
lockfileVersion: '9.0'
importers:
.:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
cpu: [arm64]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
optional: true
pnpm@12.3.4:
optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
---
lockfileVersion: '9.0'
settings:
-2
View File
@@ -1,4 +1,2 @@
storeDir: ../.pnpm-store
minimumReleaseAgeExclude:
- brace-expansion@5.0.8 || 5.0.9
+1 -1
View File
@@ -1037,7 +1037,7 @@
provider (prepare-organization-sso-provider cfg sso)
_info (get-info cfg provider state code)
session (session/get-session request)
exp (ct/in-future {:hours 4})]
exp (ct/in-future {:minutes 15})]
(when (and session organization-id)
(let [props (-> (or (:props session) {})
(update :sso assoc organization-id exp))]
-2
View File
@@ -58,7 +58,6 @@
:objects-storage-fs-directory "assets"
:auth-token-cookie-name "auth-token"
:auth-token-cookie-max-age-absolute (ct/duration {:days 30})
:assets-path "/internal/assets/"
:smtp-default-reply-to "Penpot <no-reply@example.com>"
@@ -207,7 +206,6 @@
[:auth-token-cookie-name {:optional true} :string]
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
[:auth-token-cookie-max-age-absolute {:optional true} ::ct/duration]
[:registration-domain-whitelist {:optional true} [::sm/set :string]]
[:email-verify-threshold {:optional true} ::ct/duration]
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.arrow
"Bulk Ladybug ingest through in-memory Arrow.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.debug
"In-memory Ladybug sessions for the debug graph console."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.ingest
"Penpot file -> Ladybug graph projection."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.ladybug
"Ladybug access layer for graph-backed Penpot.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.meta
"`GraphMeta`: the graph's own account of who built it and from what.
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.projection.document
"Project a Penpot file-data map into Ladybug nodes and structural edges.
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.projection.transforms
"Derived graph links: edges a reader could compute from the projected
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.report
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.schema
"Ladybug DDL facade for the graph-backed Penpot vertical slice.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.schema.contract
"Deliberate choices in Penpot's graph schema, recorded as data.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.schema.nodes
"Single source of truth for graph node tables.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.schema.projection
"Derive Ladybug node column schemas from Penpot Malli sources.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.schema.types
"Map Malli schemas to Ladybug column types.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.schema.values
"Shape a Penpot value into the plain data its Ladybug column type wants.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.stats
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.sync
"Incremental Ladybug graph updates from Penpot file-change events."
+13 -28
View File
@@ -36,9 +36,6 @@
;; Default age for automatic session renewal
(def default-renewal-max-age (ct/duration {:hours 6}))
;; Default absolute maximum session duration
(def default-cookie-max-age-absolute (ct/duration {:days 30}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PROTOCOLS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -172,19 +169,15 @@
(defn- assign-token
[cfg session]
(let [absolute-max-age (cf/get :auth-token-cookie-max-age-absolute default-cookie-max-age-absolute)
claims {:iss "authentication"
:aud "penpot"
:sid (:id session)
:iat (:modified-at session)
:uid (:profile-id session)
:sso-provider-id (:sso-provider-id session)
:sso-session-id (:sso-session-id session)}
claims (if (:created-at session)
(assoc claims :exp (ct/plus (:created-at session) absolute-max-age))
claims)
header {:kid 1 :ver 1}
token (tokens/generate cfg claims header)]
(let [claims {:iss "authentication"
:aud "penpot"
:sid (:id session)
:iat (:modified-at session)
:uid (:profile-id session)
:sso-provider-id (:sso-provider-id session)
:sso-session-id (:sso-session-id session)}
header {:kid 1 :ver 1}
token (tokens/generate cfg claims header)]
(assoc session :token token)))
(defn create-fn
@@ -360,23 +353,15 @@
or (updated_at is null and
created_at < ?::timestamptz)")
(def ^:private
sql:delete-expired-v2
"DELETE FROM http_session_v2
WHERE created_at < ?::timestamptz")
(defn- collect-expired-tasks
[{:keys [::db/conn ::tasks/max-age]}]
(let [threshold (ct/minus (ct/now) max-age)
result-legacy (-> (db/exec-one! conn [sql:delete-expired threshold threshold])
(db/get-update-count))
result-v2 (-> (db/exec-one! conn [sql:delete-expired-v2 threshold])
(db/get-update-count))]
result (-> (db/exec-one! conn [sql:delete-expired threshold threshold])
(db/get-update-count))]
(l/dbg :task "gc"
:hint "clean http sessions"
:deleted-legacy result-legacy
:deleted-v2 result-v2)
(+ result-legacy result-v2)))
:deleted result)
result))
(defmethod ig/init-key ::tasks/gc
[_ {:keys [::tasks/max-age] :as cfg}]
+12 -2
View File
@@ -14,6 +14,7 @@
[app.common.files.helpers :as cfh]
[app.common.files.migrations :as fmg]
[app.common.files.stats :as cfs]
[app.common.files.tokens :as cfo]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as-alias smdj]
@@ -21,6 +22,7 @@
[app.common.transit :as t]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
[app.common.types.tokens-lib :as ctob]
[app.common.uri :as uri]
[app.config :as cf]
[app.db :as db]
@@ -520,12 +522,20 @@
components-sample
(-> (sample-assets components 4)
(update :sample load-objects))]
(update :sample load-objects))
tokens-lib (cfo/get-tokens-lib data)
tokens-count (if (some? tokens-lib) (count (ctob/get-all-tokens tokens-lib)) 0)
token-sets-count (if (some? tokens-lib) (count (ctob/get-sets tokens-lib)) 0)
token-themes-count (if (some? tokens-lib) (count (ctob/get-themes-no-hidden tokens-lib)) 0)]
{:components components-sample
:variants {:count (count variant-ids)}
:colors (sample-assets (:colors data) 3)
:typographies (sample-assets (:typographies data) 3)}))
:typographies (sample-assets (:typographies data) 3)
:tokens-count tokens-count
:token-sets-count token-sets-count
:token-themes-count token-themes-count}))
(def ^:private file-summary-cache-key-ttl
(ct/duration {:days 30}))
@@ -106,7 +106,6 @@
:set-token
:set-token-set
:set-token-theme
:set-active-token-themes
:rename-token-set-group
:move-token-set
:move-token-set-group
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.rpc.commands.plugins
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.storage.pending-gc
"A maintenance task that reclaims storage objects created in 'pending'
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.tasks.demo-purge
"Task handler for delayed demo profile deletion. Submitted at demo
+9 -4
View File
@@ -17,6 +17,7 @@
[app.http.client :as http]
[app.main :as-alias main]
[app.setup :as-alias setup]
[app.util.blob :as blob]
[app.util.json :as json]
[integrant.core :as ig]
[promesa.exec :as px]))
@@ -247,16 +248,20 @@
:props (or (some-> props db/decode-transit-pgobject) {})
:context (or (some-> context db/decode-transit-pgobject) {})}))
(defn- encode-batch
"Encode a sequence of event maps into a fressian+zstd base64 string
suitable for JSON transport."
^String [events]
(blob/encode-str events {:version 4}))
(defn send-event-batch
"Send a single batch of events to the telemetry endpoint. Returns
true on success. The events are sent as a plain vector of event
maps; the JSON encoder handles UUID and temporal types natively and
the receiver coerces them back to proper types."
true on success."
[{:keys [::setup/props] :as cfg} batch]
(let [payload {:type :telemetry-events
:version (:full cf/version)
:instance-id (:instance-id props)
:events (vec batch)}
:events (encode-batch batch)}
request {:method :post
:uri (cf/get :telemetry-uri)
:headers {"content-type" "application/json"}
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns backend-tests.demo-test
(:require
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns backend-tests.graph-binder-gate-test
"Binder gate for the incremental-sync statement templates.
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns backend-tests.graph-sync-parity-test
"Cold projection and incremental sync are two implementations of one mapping,
@@ -277,70 +277,6 @@
(t/is (= (:id session) (:sid claims)))
(t/is (= (:id profile) (:uid claims)))))
(t/deftest session-token-contains-exp-claim
(let [cfg th/*system*
manager (session/inmemory-manager)
profile (th/create-profile* 1)
session (->> (session/create-session manager {:profile-id (:id profile)
:user-agent "user agent"})
(#'session/assign-token cfg))
claims (tokens/decode cfg (:token session))
exp (:exp claims)]
(t/is (some? exp) "session token should contain :exp claim")
(t/is (ct/inst? exp) "exp should be an instant")))
(t/deftest session-token-exp-based-on-created-at
(let [cfg th/*system*
manager (session/inmemory-manager)
profile (th/create-profile* 1)
session (->> (session/create-session manager {:profile-id (:id profile)
:user-agent "user agent"})
(#'session/assign-token cfg))
claims (tokens/decode cfg (:token session))
expected-exp (ct/plus (:created-at session) (ct/duration {:days 30}))]
(t/is (some? (:exp claims)) "session token should contain :exp claim")
(t/is (= (inst-ms (:exp claims))
(inst-ms expected-exp))
"exp should equal created-at + 30 days")))
(t/deftest session-token-past-exp-is-rejected
(let [cfg th/*system*
manager (session/inmemory-manager)
profile (th/create-profile* 1)
session (->> (session/create-session manager {:profile-id (:id profile)
:user-agent "user agent"})
(#'session/assign-token cfg))
claims (tokens/decode cfg (:token session))
;; Manually create a token with exp in the past
past-claims (assoc claims :exp (ct/minus (ct/now) (ct/duration {:days 1})))
header {:kid 1 :ver 1}
past-token (tokens/generate cfg past-claims header)]
(t/is (nil? (session/decode-token cfg past-token))
"token with exp in the past should be rejected")))
(t/deftest session-renewal-preserves-original-exp
(let [cfg th/*system*
manager (session/inmemory-manager)
profile (th/create-profile* 1)
handler (-> (fn [req] req)
(#'session/wrap-authz {::session/manager manager})
(#'mw/wrap-auth {:bearer (partial session/decode-token cfg)
:cookie (partial session/decode-token cfg)}))
session (->> (session/create-session manager {:profile-id (:id profile)
:user-agent "user agent"})
(#'session/assign-token cfg))
original-exp (:exp (tokens/decode cfg (:token session)))
;; Force renewal by setting modified-at to 7 hours ago
old-session (assoc session :modified-at (ct/minus (ct/now) (ct/duration {:hours 7})))
response (handler (make-dummy-request {:cookies {"auth-token" (:token old-session)}}))
{:keys [token claims]} (get response ::http/auth-data)
new-exp (:exp claims)]
(t/is (some? original-exp) "original token should have :exp")
(t/is (some? new-exp) "renewed token should have :exp")
(t/is (= (inst-ms original-exp)
(inst-ms new-exp))
"renewed token should preserve original :exp, not extend it")))
(t/deftest parse-request-illegal-argument-exception
;; clojure.data.json raises IllegalArgumentException (case
;; fall-through) on several kinds of malformed input. The
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns backend-tests.passwords-test
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns backend-tests.rpc-demo-test
(:require
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns backend-tests.rpc-plugins-test
(:require
@@ -12,6 +12,7 @@
[app.db :as db]
[app.loggers.audit :as audit]
[app.tasks.telemetry :as telemetry]
[app.util.blob :as blob]
[app.util.json :as json]
[backend-tests.helpers :as th]
[clojure.test :as t]
@@ -58,6 +59,11 @@
:cnt
long))
(defn- decode-event-batch
"Decode the base64+fressian+zstd event-batch sent to the mock."
[b64-str]
(blob/decode-str b64-str))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; STATS / REPORT STRUCTURE TESTS (existing behaviour, extended)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -239,19 +245,21 @@
(t/is (not (contains? ev :ip-addr)))))))))
(t/deftest test-batch-encoding-is-decodable
;; Events are sent as a plain vector of raw event maps (no blob
;; encoding): every batch must JSON round-trip unchanged, because
;; the receiver coerces types from the plain JSON representation.
;; Verify that encode-batch produces a blob that round-trips back
;; through blob/decode to the original data.
(let [events [{:name "navigate" :type "action" :source "telemetry"
:tracked-at (ct/now)}
{:name "create-file" :type "action" :source "telemetry"
:tracked-at (ct/now)}]
encoded (json/encode-str {:events (vec events)})
decoded (json/decode encoded)]
(t/is (vector? (:events decoded)))
(t/is (= (count events) (count (:events decoded))))
(t/is (= "navigate" (:name (first (:events decoded)))))
(t/is (= "create-file" (:name (second (:events decoded)))))))
;; Call the private fn through the ns-mapped var
encode (ns-resolve 'app.tasks.telemetry 'encode-batch)
encoded (encode events)
decoded (decode-event-batch encoded)]
(t/is (string? encoded))
(t/is (seq decoded))
(t/is (= (count events) (count decoded)))
(t/is (= "navigate" (:name (first decoded))))
(t/is (= "create-file" (:name (second decoded))))))
(t/deftest test-multiple-batches-when-many-events
;; Lower batch-size to 1 so that 3 events produce 3 separate
@@ -779,13 +787,9 @@
(t/is (= "telemetry-events" (name (:type body))))
(t/is (string? (:version body)))
(t/is (some? (:instance-id body)))
;; :events is a plain vector of raw event maps
(t/is (vector? (:events body)))
(t/is (pos? (count (:events body))))
(doseq [ev (:events body)]
(t/is (string? (:name ev)))
(t/is (string? (:source ev)))
(t/is (string? (:tracked-at ev))))))))))
;; :events is a base64-encoded blob
(t/is (string? (:events body)))
(t/is (pos? (count (:events body))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASK BRANCH COVERAGE
+1 -1
View File
@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL",
"private": true,
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457",
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
"type": "module",
"repository": {
"type": "git",
+37 -37
View File
@@ -7,96 +7,96 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.0.0
version: 12.0.0
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.darwin-arm64@12.0.0':
resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
cpu: [arm64]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.0.0':
resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.linux-arm64-musl@12.0.0':
resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.0.0':
resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-x64-musl@12.0.0':
resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.0.0':
resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.0.0':
resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.0.0':
resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.0.0:
resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.darwin-arm64@12.0.0':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.darwin-x64@12.0.0':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.0.0':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.0.0':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.linux-x64-musl@12.0.0':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-x64@12.0.0':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.win32-arm64@12.0.0':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.win32-x64@12.0.0':
optional: true
pnpm@12.3.4:
pnpm@12.0.0:
optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.darwin-arm64': 12.0.0
'@pnpm/exe.darwin-x64': 12.0.0
'@pnpm/exe.linux-arm64': 12.0.0
'@pnpm/exe.linux-arm64-musl': 12.0.0
'@pnpm/exe.linux-x64': 12.0.0
'@pnpm/exe.linux-x64-musl': 12.0.0
'@pnpm/exe.win32-arm64': 12.0.0
'@pnpm/exe.win32-x64': 12.0.0
---
lockfileVersion: '9.0'
-2
View File
@@ -1,3 +1 @@
storeDir: ../.pnpm-store
minimumReleaseAge: 0
+97 -55
View File
@@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.files.tokens :as cfo]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.schema :as sm]
@@ -29,6 +30,7 @@
[app.common.types.shape-tree :as ctst]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as ctt]
[app.common.types.variant :as ctv]
@@ -398,10 +400,11 @@
[:id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-theme-attrs]]]]
[:set-active-token-themes
[:map {:title "SetActiveTokenThemes"}
[:type [:= :set-active-token-themes]]
[:theme-paths [:set :string]]]]
[:set-tokens-status
[:map {:title "SetTokensStatus"}
[:type [:= :set-tokens-status]]
[:theme-ids [:set ::sm/uuid]]
[:set-ids [:set ::sm/uuid]]]]
[:rename-token-set-group
[:map {:title "RenameTokenSetGroup"}
@@ -428,7 +431,13 @@
[:set-base-font-size
[:map {:title "ModBaseFontSize"}
[:type [:= :set-base-font-size]]
[:base-font-size :string]]]])
[:base-font-size :string]]]
[:set-tokens-source
[:map {:title "SetTokensSource"}
[:type [:= :set-tokens-source]]
[:file-id ::sm/uuid]
[:library-id [:maybe ::sm/uuid]]]]])
(def schema:changes
[:sequential {:gen/max 5 :gen/min 1} schema:change])
@@ -992,77 +1001,73 @@
(defmethod process-change :set-token
[data {:keys [set-id token-id attrs]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-token lib' set-id token-id)
(-> (cfo/ensure-tokens-lib data)
(cfo/update-tokens-lib
(fn [lib']
(cond
(not attrs)
(ctob/delete-token lib' set-id token-id)
(not (ctob/get-token lib' set-id token-id))
(ctob/add-token lib' set-id (ctob/make-token attrs))
(not (ctob/get-token lib' set-id token-id))
(ctob/add-token lib' set-id (ctob/make-token attrs))
:else
(ctob/update-token lib' set-id token-id
(fn [prev-token]
(ctob/make-token (merge prev-token attrs)))))))))
:else
(ctob/update-token lib' set-id token-id
(fn [prev-token]
(ctob/make-token (merge prev-token attrs)))))))))
(defmethod process-change :set-token-set
[data {:keys [id attrs]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-set lib' id)
(-> (cfo/ensure-tokens-lib data)
(cfo/update-tokens-lib
(fn [lib']
(cond
(not attrs)
(ctob/delete-set lib' id)
(not (ctob/get-set lib' id))
(ctob/add-set lib' (ctob/make-token-set attrs))
(not (ctob/get-set lib' id))
(ctob/add-set lib' (ctob/make-token-set attrs))
:else
(ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs))))))))
:else
(ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs))))))))
(defmethod process-change :set-token-theme
[data {:keys [id attrs]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-theme lib' id)
(-> (cfo/ensure-tokens-lib data)
(cfo/update-tokens-lib
(fn [lib']
(cond
(not attrs)
(ctob/delete-theme lib' id)
(not (ctob/get-theme lib' id))
(ctob/add-theme lib' (ctob/make-token-theme attrs))
(not (ctob/get-theme lib' id))
(ctob/add-theme lib' (ctob/make-token-theme attrs))
:else
(ctob/update-theme lib'
id
(fn [prev-token-theme]
(ctob/make-token-theme (merge prev-token-theme attrs)))))))))
:else
(ctob/update-theme lib'
id
(fn [prev-token-theme]
(ctob/make-token-theme (merge prev-token-theme attrs)))))))))
(defmethod process-change :set-active-token-themes
[data {:keys [theme-paths]}]
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
(ctob/set-active-themes theme-paths))))
(defmethod process-change :set-tokens-status
[data {:keys [theme-ids set-ids]}]
(-> (cfo/ensure-tokens-lib data)
(cfo/update-tokens-status ctos/set-tokens-status theme-ids set-ids)))
(defmethod process-change :rename-token-set-group
[data {:keys [set-group-path set-group-fname]}]
(update data :tokens-lib (fn [lib]
(-> lib
(ctob/ensure-tokens-lib)
(ctob/rename-set-group set-group-path set-group-fname)))))
(-> (cfo/ensure-tokens-lib data)
(cfo/update-tokens-lib ctob/rename-set-group set-group-path set-group-fname)))
(defmethod process-change :move-token-set
[data {:keys [from-path to-path before-path before-group] :as changes}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/move-set from-path to-path before-path before-group))))
(-> (cfo/ensure-tokens-lib data)
(cfo/update-tokens-lib ctob/move-set from-path to-path before-path before-group)))
(defmethod process-change :move-token-set-group
[data {:keys [from-path to-path before-path before-group]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/move-set-group from-path to-path before-path before-group))))
(-> (cfo/ensure-tokens-lib data)
(cfo/update-tokens-lib ctob/move-set-group from-path to-path before-path before-group)))
;; === Design Tokens configuration
@@ -1070,10 +1075,13 @@
[data {:keys [base-font-size]}]
(ctf/set-base-font-size data base-font-size))
(defmethod process-change :set-tokens-source
[data {:keys [library-id]}]
(cfo/set-tokens-source data library-id))
;; === Operations
(def decode-shape-attrs
(def decode-shape-attrs
(sm/decoder cts/schema:shape-attrs sm/json-transformer))
(defmethod process-operation :assign
@@ -1242,3 +1250,37 @@
(defmethod frames-changed :default
[_ _]
nil)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Design Tokens changes detection
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private tokens-lib-change-types
"Set of change types that modify the tokens library."
#{:set-tokens-lib
:set-token
:set-token-set
:set-token-theme
:rename-token-set-group
:move-token-set
:move-token-set-group})
(defn tokens-lib-changed?
"Check if a commit contains changes that modify the tokens library."
[changes]
(some #(tokens-lib-change-types (:type %)) changes))
(def ^:private notifiable-token-change-types
"Set of change types that modify the structure of the tokens library sets
or themes (as opposed to `:set-token`, which only edits a token's value)."
#{:set-token-set
:set-token-theme
:rename-token-set-group
:move-token-set
:move-token-set-group})
(defn notifiable-token-change-occured?
"Check if a commit contains changes that modify the tokens library sets or
themes structure (renamed/added/removed sets, theme set-membership, etc.)."
[changes]
(some #(notifiable-token-change-types (:type %)) changes))
@@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.files.changes :as cfc]
[app.common.files.helpers :as cfh]
[app.common.files.tokens :as cfo]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
@@ -22,6 +23,7 @@
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]))
@@ -987,7 +989,7 @@
[changes tokens-lib]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
prev-tokens-lib (cfo/get-tokens-lib library-data)]
(-> changes
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
@@ -996,7 +998,7 @@
(defn set-token [changes set-id token-id token]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token (some-> (get library-data :tokens-lib)
prev-token (some-> (cfo/get-tokens-lib library-data)
(ctob/get-token set-id token-id))]
(-> changes
(update :redo-changes conj {:type :set-token
@@ -1013,7 +1015,7 @@
[changes id token-set]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
prev-token-set (some-> (cfo/get-tokens-lib library-data)
(ctob/get-set id))]
(-> changes
(update :redo-changes conj {:type :set-token-set
@@ -1028,7 +1030,7 @@
[changes id new-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
prev-token-set (some-> (cfo/get-tokens-lib library-data)
(ctob/get-set id))]
(-> changes
(update :redo-changes conj {:type :set-token-set
@@ -1042,7 +1044,7 @@
(defn set-token-theme [changes id theme]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-theme (some-> (get library-data :tokens-lib)
prev-theme (some-> (cfo/get-tokens-lib library-data)
(ctob/get-theme id))]
(-> changes
(update :redo-changes conj {:type :set-token-theme
@@ -1053,17 +1055,21 @@
:attrs (datafy prev-theme)})
(apply-changes-local))))
(defn set-active-token-themes
[changes active-theme-paths]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-active-theme-paths (d/nilv (some-> (get library-data :tokens-lib)
(ctob/get-active-theme-paths))
#{})]
(-> changes
(update :redo-changes conj {:type :set-active-token-themes :theme-paths active-theme-paths})
(update :undo-changes conj {:type :set-active-token-themes :theme-paths prev-active-theme-paths})
(apply-changes-local))))
(defn set-tokens-status
([changes tokens-status]
(assert-library! changes)
(assert (or (ctos/tokens-status? tokens-status)
(nil? tokens-status)))
(let [theme-ids (if tokens-status (ctos/get-active-theme-ids tokens-status) #{})
set-ids (if tokens-status (ctos/get-active-set-ids tokens-status) #{})
library-data (::library-data (meta changes))
prev-tokens-status (cfo/get-tokens-status library-data)
prev-theme-ids (if prev-tokens-status (ctos/get-active-theme-ids prev-tokens-status) #{})
prev-set-ids (if prev-tokens-status (ctos/get-active-set-ids prev-tokens-status) #{})]
(-> changes
(update :redo-changes conj {:type :set-tokens-status :theme-ids theme-ids :set-ids set-ids})
(update :undo-changes conj {:type :set-tokens-status :theme-ids prev-theme-ids :set-ids prev-set-ids})
(apply-changes-local)))))
(defn rename-token-set-group
[changes set-group-path set-group-fname]
@@ -1117,6 +1123,21 @@
:base-font-size previous-font-size})
(apply-changes-local))))
(defn set-tokens-source
[changes library-id]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
file-id (:id library-data)
prev-val (cfo/get-tokens-source library-data)]
(-> changes
(update :redo-changes conj {:type :set-tokens-source
:file-id file-id
:library-id library-id})
(update :undo-changes conj {:type :set-tokens-source
:file-id file-id
:library-id prev-val})
(apply-changes-local))))
;; Misc changes
(defn reorder-children
+10 -1
View File
@@ -13,6 +13,7 @@
[app.common.files.comp-processors :as cfcp]
[app.common.files.defaults :as cfd]
[app.common.files.helpers :as cfh]
[app.common.files.tokens :as cfo]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
@@ -1998,6 +1999,13 @@
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "0027-separate-tokens-status"
[data _]
(if-let [tokens-lib (:tokens-lib data)]
(assoc data :tokens-status
(cfo/make-tokens-status-from-lib tokens-lib))
data))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@@ -2081,4 +2089,5 @@
"0023-repair-token-themes-with-inexistent-sets"
"0024b-fix-stroke-cap-placement"
"0025-repair-empty-text-content"
"0026-fix-svg-raw-shapes-uuids"]))
"0026-fix-svg-raw-shapes-uuids"
"0027-separate-tokens-status"]))
+11
View File
@@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.tokens :as cfo]
[app.common.logging :as log]
[app.common.path-names :as cpn]
[app.common.types.component :as ctk]
@@ -783,6 +784,16 @@
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :missing-tokens-status
[_ _ file-data _]
(let [tokens-lib (cfo/get-tokens-lib file-data)
tokens-status (cfo/make-tokens-status-from-lib tokens-lib)]
(log/debug :hint "repairing shape :tokens-status-missing")
(log/debug :hint " -> add a tokens-status generated from the library")
(-> (pcb/empty-changes nil)
(pcb/with-library-data file-data)
(pcb/set-tokens-status tokens-status))))
(defmethod repair-error :default
[_ error file _]
(log/error :hint "Unknown error code, don't know how to repair" :code (:code error))
+318 -1
View File
@@ -9,13 +9,18 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.i18n :refer [tr]]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.core :as m]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HIGH LEVEL SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -165,7 +170,8 @@
(some (fn [[token-name _]]
(not (ctob/token-name-path-exists? token-name tokens-tree)))
new-tokens))))]])
(defn find-refs [value]
(defn- find-refs [value]
(cond
(string? value)
(cto/find-token-value-references value)
@@ -344,6 +350,8 @@
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Token
(def parseable-token-value-regexp
"Regexp that can be used to parse a number value out of resolved token value.
This regexp also trims whitespace around the value."
@@ -415,3 +423,312 @@
;; FIXME: this should be precalculated ?
(defn is-reference? [token]
(str/includes? (:value token) "{"))
;; Tokens lib and status in file data
(defn make-tokens-status-from-lib
"Make a TokensStatus from a TokensLib, activating the themes and sets
marked as active in the library (to migrate from legacy files)."
[tokens-lib]
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(let [active-theme-paths (ctob/get-legacy-active-themes tokens-lib)
active-theme-ids (into #{}
(comp (map #(ctob/get-theme-by-path tokens-lib %))
(remove nil?)
(map ctob/get-id)
(filter #(not= % ctob/hidden-theme-id)))
active-theme-paths)
;; Get active set names from the active themes
active-set-names (into #{}
(comp (filter #(and (ctob/token-theme? %)
(contains? active-theme-paths (ctob/get-theme-path %))))
(mapcat :sets))
(ctob/get-themes tokens-lib))
active-set-ids (into #{}
(comp (map #(ctob/get-set-by-name tokens-lib %))
(remove nil?)
(map ctob/get-id))
active-set-names)]
(ctos/make-tokens-status :active-theme-ids active-theme-ids
:active-set-ids active-set-ids)))
(defn ensure-tokens-lib
"Ensure file-data has a :tokens-lib or :tokens-source, and also a :tokens-status, creating them if necessary."
[file-data]
(cond-> file-data
(nil? (:tokens-source file-data))
(update :tokens-lib #(or % (ctob/make-tokens-lib)))
:always
(update :tokens-status #(or % (ctos/make-tokens-status)))))
(defn ensure-tokens-status
"Ensure file-data has a :tokens-status, creating it empty if necessary."
[file-data]
(cond-> file-data
(nil? (:tokens-status file-data))
(update :tokens-status #(or % (ctos/make-tokens-status)))))
(defn get-tokens-source
"Return the current value of :tokens-source attribute."
[file-data]
(:tokens-source file-data))
(defn get-effective-tokens-source
"Return the current tokens source of the file. When there is no explicit
:tokens-source attribute, the source is the file itself."
[file-data]
(or (:tokens-source file-data) (:id file-data)))
(defn set-tokens-source
[file-data tokens-source]
(assert (or (nil? tokens-source) (uuid? tokens-source)) "expected nil or valid uuid")
(if (nil? tokens-source)
(dissoc file-data :tokens-source)
(assoc file-data :tokens-source tokens-source)))
(defn effective-tokens-source?
"Returns true if the given id is the current tokens source of the file-data."
[file-data id]
(assert (uuid? id) "expected valid uuid")
(= (get-effective-tokens-source file-data) id))
(defn has-own-tokens?
"Returns true if the file-data contains a tokens-library itself and the library
contains some tokens. Note that it still may be true even if the tokens source
is external (in this case the own library is inactive, but still exists)."
[file-data]
(let [tokens-lib (:tokens-lib file-data)]
(and (some? tokens-lib)
(not (ctob/empty-lib? tokens-lib)))))
(defn tokens-provider?
"Returns true if the file MAY become a tokens source. This is if the file has tokens and has
not configured another tokens source."
[file-data]
(and (has-own-tokens? file-data)
(effective-tokens-source? file-data (:id file-data))))
(defn editable-tokens?
"Returns true if the file-data is its own tokens source."
[file-data]
(effective-tokens-source? file-data (:id file-data)))
(defn get-tokens-lib
[file-data]
(:tokens-lib file-data))
(defn get-tokens-status
[file-data]
(:tokens-status file-data))
(defn update-tokens-lib
"Update the tokens-lib inside file-data through a callback function.
The function will receive the tokens lib and the rest of args."
[file-data f & args]
(d/update-when file-data :tokens-lib #(apply f % args)))
(defn update-tokens-status
"Update the tokens-status inside file-data through a callback function.
The function will receive the tokens status and the rest of args."
[file-data f & args]
(d/update-when file-data :tokens-status #(apply f % args)))
;; Tokens status with tokens lib
(defn- calculate-active-sets
"Obtain the set-ids that needs to be active for a particular set of theme ids"
[active-theme-ids tokens-lib]
(let [active-themes (map #(ctob/get-theme tokens-lib %) active-theme-ids)
active-set-names (reduce set/union #{} (map :sets active-themes))
active-sets (map #(ctob/get-set-by-name tokens-lib %) active-set-names)
active-set-ids (into #{} (map ctob/get-id) active-sets)]
active-set-ids))
(defn get-active-themes
"Return an ordered sequence of active themes"
[tokens-status tokens-lib]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(->> (ctob/get-themes tokens-lib)
(filter #(ctos/theme-active? tokens-status (ctob/get-id %)))))
(defn activate-theme
"Activate a theme and all its sets. Deactivate any other theme in the same group."
[tokens-status tokens-lib id]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(assert (uuid? id) "expected valid theme id")
(if-not (ctos/theme-active? tokens-status id)
(if-let [theme (ctob/get-theme tokens-lib id)]
(let [group-themes (into #{} (ctob/get-themes-in-group tokens-lib (:group theme)))
active-theme-ids (ctos/get-active-theme-ids tokens-status)
active-theme-ids' (-> (set/difference active-theme-ids group-themes)
(conj id))
active-set-ids' (calculate-active-sets active-theme-ids' tokens-lib)]
(ctos/set-tokens-status tokens-status active-theme-ids' active-set-ids'))
tokens-status)
tokens-status))
(defn deactivate-theme
"Deactivate a theme and all its sets that are not in other active themes"
[tokens-status tokens-lib id]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(assert (uuid? id) "expected valid theme id")
(if (ctos/theme-active? tokens-status id)
(let [active-theme-ids' (disj (ctos/get-active-theme-ids tokens-status) id)
active-set-ids' (calculate-active-sets active-theme-ids' tokens-lib)]
(ctos/set-tokens-status tokens-status active-theme-ids' active-set-ids'))
tokens-status))
(defn toggle-theme-active
"Toggle a theme's active state and update active sets accordingly."
[tokens-status tokens-lib id]
(if (ctos/theme-active? tokens-status id)
(deactivate-theme tokens-status tokens-lib id)
(activate-theme tokens-status tokens-lib id)))
(defn set-theme-active
"Set a theme's active state and update active sets accordingly."
[tokens-status tokens-lib id active?]
(if active?
(activate-theme tokens-status tokens-lib id)
(deactivate-theme tokens-status tokens-lib id)))
(defn get-active-sets
"Return a clojure set of the active sets, resolved from TokensStatus + TokensLib."
[tokens-status tokens-lib]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(let [active-set-ids (ctos/get-active-set-ids tokens-status)]
(into #{}
(comp (map #(ctob/get-set tokens-lib %))
(filter some?))
active-set-ids)))
(defn set-set-active
"Set directly a set's active state and deactivate all themes."
[tokens-status tokens-lib id active?]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(assert (uuid? id) "expected valid set id")
(if (and (ctob/get-set tokens-lib id)
(not= active? (ctos/set-active? tokens-status id)))
(let [active-set-ids (ctos/get-active-set-ids tokens-status)
active-set-ids' (if active?
(conj active-set-ids id)
(disj active-set-ids id))]
(ctos/set-tokens-status tokens-status #{} active-set-ids'))
tokens-status))
(defn activate-set
"Activate directly a set, and deactivate all themes."
[tokens-status tokens-lib id]
(set-set-active tokens-status tokens-lib id true))
(defn deactivate-set
"Deactivate directly a set, and deactivate all themes."
[tokens-status tokens-lib id]
(set-set-active tokens-status tokens-lib id false))
(defn toggle-set-active
"Toggle a set's active state and deactivate all themes."
[tokens-status tokens-lib id]
(set-set-active tokens-status tokens-lib id (not (ctos/set-active? tokens-status id))))
(defn sets-at-path-all-active?
"Check active state of sets at a group path.
Returns :none, :all, or :partial."
[tokens-status tokens-lib group-path]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(assert (vector? group-path) "expected valid group path")
(let [active-set-ids (ctos/get-active-set-ids tokens-status)
path-set-ids (into #{}
(map ctob/get-id)
(ctob/get-sets-at-path tokens-lib group-path))]
(if (seq active-set-ids)
(let [difference (set/difference path-set-ids active-set-ids)]
(cond
(empty? difference) :all
(seq (set/intersection path-set-ids active-set-ids)) :partial
:else :none))
:none)))
(defn toggle-set-group-active
"Toggle the active state of all sets at a group path, and deactivate all themes.
If all sets are active, deactivate all. If none or some are active, activate all."
[tokens-status tokens-lib group-path]
(let [all-active? (sets-at-path-all-active? tokens-status tokens-lib group-path)
sets-at-path (ctob/get-sets-at-path tokens-lib group-path)
set-ids-at-path (into #{} (map ctob/get-id) sets-at-path)
active-set-ids (ctos/get-active-set-ids tokens-status)
active-set-ids' (if (contains? #{:all :partial} all-active?)
(set/difference active-set-ids set-ids-at-path)
(set/union active-set-ids set-ids-at-path))]
(if (not= active-set-ids active-set-ids')
(ctos/set-tokens-status tokens-status #{} active-set-ids')
tokens-status)))
(defn get-tokens-in-active-sets
"Get merged tokens from all active sets, in set order."
[tokens-status tokens-lib]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(let [active-set-ids (ctos/get-active-set-ids tokens-status)
all-set-ids (map ctob/get-id (ctob/get-sets tokens-lib))
ordered-active (filter active-set-ids all-set-ids)]
(reduce (fn [tokens set-id]
(merge tokens (ctob/get-tokens tokens-lib set-id)))
(d/ordered-map)
ordered-active)))
(defn get-tokens-in-active-sets-force
"Same as get-tokens-in-active-sets but force-including a set by id."
[tokens-status tokens-lib force-set-id]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(assert (uuid? force-set-id) "expected valid set id")
(let [active-set-ids (ctos/get-active-set-ids tokens-status)
all-set-ids (map ctob/get-id (ctob/get-sets tokens-lib))
ordered-active (cond-> (filter active-set-ids all-set-ids)
(some? force-set-id)
(conj force-set-id))]
(reduce (fn [tokens set-id]
(let [set (ctob/get-set tokens-lib set-id)]
(if set
(merge tokens (ctob/get-tokens- set))
tokens)))
(d/ordered-map)
ordered-active)))
(defn sync-tokens-status-with-lib
"Synchronizes tokens status with the current tokens lib:
- Delete any theme or set that no longer exists in the lib.
- Recalculate the list of active sets from the new themes."
[tokens-status tokens-lib]
(assert (ctos/tokens-status? tokens-status) "expected valid tokens-status")
(assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib")
(let [active-theme-ids (ctos/get-active-theme-ids tokens-status)
valid-theme-ids (into #{}
(filter #(some? (ctob/get-theme tokens-lib %)))
active-theme-ids)
active-set-ids (ctos/get-active-set-ids tokens-status)
valid-set-ids (if (empty? valid-theme-ids)
(into #{}
(filter #(some? (ctob/get-set tokens-lib %)))
active-set-ids)
(calculate-active-sets valid-theme-ids tokens-lib))]
(if (or (not= active-theme-ids valid-theme-ids)
(not= active-set-ids valid-set-ids))
(do
(log/info :hint "syncing token status"
:removed-themes (count (set/difference active-theme-ids valid-theme-ids))
:removed-sets (count (set/difference active-set-ids valid-set-ids)))
(ctos/set-tokens-status tokens-status valid-theme-ids valid-set-ids))
tokens-status)))
+15 -1
View File
@@ -73,7 +73,8 @@
:variant-main-bad-name
:variant-main-bad-variant-name
:variant-component-bad-name
:variant-component-bad-id})
:variant-component-bad-id
:missing-tokens-status})
(def ^:private schema:error
[:map {:title "ValidationError"}
@@ -816,6 +817,17 @@
(transient [])
objects)))
(defn check-tokens
"Check tokens in the file. The internal structure should be correct because
of the schemas and protocol functions. But we need to check for the case that
the tokens-status is missing due to a migration or import bug."
[{:keys [data] :as file}]
(when (and (some? (:tokens-lib data))
(nil? (:tokens-status data)))
(report-error :missing-tokens-status
"Tokens library is present but tokens-status is missing"
nil file nil)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API: VALIDATION FUNCTIONS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -853,6 +865,8 @@
nil
(:components data))
(check-tokens file)
(-> *errors* deref not-empty))))
(defn validate-shape
+1
View File
@@ -137,6 +137,7 @@
:token-tokenscript
:token-import-from-library
:token-typography-row
:token-lib-sync
;; Only for developtment.
:transit-readable-response
+110 -81
View File
@@ -6,84 +6,31 @@
(ns app.common.logic.tokens
(:require
[app.common.files.changes :as ch]
[app.common.files.changes-builder :as pcb]
[app.common.types.tokens-lib :as ctob]))
[app.common.files.tokens :as cfo]
[app.common.types.file :as ctf]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]))
(defn- generate-update-active-sets
"Copy the active sets from the currently active themes and move them
to the hidden token theme and update the theme with
`update-theme-fn`.
;; Tokens source
Use this for managing sets active state without having to modify a
user created theme (\"no themes selected\" state in the ui)."
[changes tokens-lib update-theme-fn]
(let [active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
hidden-theme (ctob/get-hidden-theme tokens-lib)
hidden-theme' (-> (some-> hidden-theme
(ctob/set-sets active-token-set-names))
(update-theme-fn))]
(defn generate-set-tokens-source
"Create changes for setting the tokens source of a file to `library`,
copying the library's tokens status. If the library is nil, the tokens source
is removed and the tokens status is cleared."
[changes library]
(let [library-id (:id library)
library-tokens-status (if library
(-> library ctf/file-data cfo/get-tokens-status)
(ctos/make-tokens-status))]
(-> changes
(pcb/set-active-token-themes #{(ctob/get-theme-path hidden-theme')})
(pcb/set-token-theme (ctob/get-id hidden-theme)
hidden-theme'))))
(pcb/set-tokens-source library-id)
(pcb/set-tokens-status library-tokens-status))))
(defn generate-set-enabled-token-set
"Enable or disable a token set at `set-name` in `tokens-lib` without modifying a user theme."
[changes tokens-lib set-name enabled?]
(if enabled?
(generate-update-active-sets changes tokens-lib #(ctob/enable-set % set-name))
(generate-update-active-sets changes tokens-lib #(ctob/disable-set % set-name))))
;; Tokens lib
(defn generate-toggle-token-set
"Toggle a token set at `set-name` in `tokens-lib` without modifying a user theme."
[changes tokens-lib set-name]
(generate-update-active-sets changes tokens-lib #(ctob/toggle-set % set-name)))
(defn- generate-update-active-token-theme
"Change the active state of a theme in `tokens-lib`. If after the change there is
any active theme other than the hidden one, deactivate the hidden theme."
[changes tokens-lib update-fn]
(let [active-token-themes (some-> tokens-lib
(update-fn)
(ctob/get-active-theme-paths))
active-token-themes' (if (= active-token-themes #{ctob/hidden-theme-path})
active-token-themes
(disj active-token-themes ctob/hidden-theme-path))]
(pcb/set-active-token-themes changes active-token-themes')))
(defn generate-set-active-token-theme
"Activate or deactivate a token theme in `tokens-lib`."
[changes tokens-lib id active?]
(if active?
(generate-update-active-token-theme changes tokens-lib
#(ctob/activate-theme % id))
(generate-update-active-token-theme changes tokens-lib
#(ctob/deactivate-theme % id))))
(defn generate-toggle-token-theme
"Toggle the active state of a token theme in `tokens-lib`."
[changes tokens-lib id]
(generate-update-active-token-theme changes tokens-lib
#(ctob/toggle-theme-active % id)))
(defn toggle-token-set-group
"Toggle a token set group at `group-path` in `tokens-lib` for a `tokens-lib-theme`."
[group-path tokens-lib tokens-lib-theme]
(let [deactivate? (contains? #{:all :partial} (ctob/sets-at-path-all-active? tokens-lib group-path))
sets-names (->> (ctob/get-sets-at-path tokens-lib group-path)
(map ctob/get-name)
(into #{}))]
(if deactivate?
(ctob/disable-sets tokens-lib-theme sets-names)
(ctob/enable-sets tokens-lib-theme sets-names))))
(defn generate-toggle-token-set-group
"Toggle a token set group at `group-path` in `tokens-lib` without modifying a user theme."
[changes tokens-lib group-path]
(generate-update-active-sets changes tokens-lib #(toggle-token-set-group group-path tokens-lib %)))
(defn vec-starts-with? [v1 v2]
(defn- vec-starts-with? [v1 v2]
(= (subvec v1 0 (min (count v1) (count v2))) v2))
(defn- calculate-move-token-set-or-set-group
@@ -155,27 +102,109 @@
prev-before (assoc :prev-before-path (:path prev-before)
:prev-before-group? (:group? prev-before))))))
(declare generate-sync-tokens-status-with-lib)
(defn generate-update-token-theme
"Create changes for updating a token theme and regenerating the activation status
of the sets inside it."
[changes token-theme]
(let [changes' (pcb/set-token-theme changes (ctob/get-id token-theme) token-theme)
data (pcb/get-library-data changes')
tokens-status (cfo/get-tokens-status data)
tokens-lib' (-> data
(ch/process-changes (:redo-changes changes'))
(cfo/get-tokens-lib))]
(generate-sync-tokens-status-with-lib changes'
tokens-status
tokens-lib')))
(defn generate-move-token-set
"Create changes for dropping a token set or token set.
Throws for impossible moves."
[changes tokens-lib params]
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set changes params)
(if tokens-lib
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set changes params)
changes)
changes))
(defn generate-move-token-set-group
"Create changes for dropping a token set or token set group.
Throws for impossible moves"
[changes tokens-lib params]
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set-group changes params)
(if tokens-lib
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set-group changes params)
changes)
changes))
(defn generate-delete-token-set-group
"Create changes for deleting a token set group."
[changes tokens-lib path]
(let [sets (ctob/get-sets-at-path tokens-lib path)]
(reduce (fn [changes set]
(pcb/set-token-set changes (ctob/get-id set) nil))
changes
sets)))
(if tokens-lib
(let [sets (ctob/get-sets-at-path tokens-lib path)]
(reduce (fn [changes set]
(pcb/set-token-set changes (ctob/get-id set) nil))
changes
sets))
changes))
;; Tokens Status
(defn- update-tokens-status
[changes tokens-status update-fn & args]
(if tokens-status
(let [tokens-status' (apply update-fn tokens-status args)]
(if (not= tokens-status tokens-status')
(pcb/set-tokens-status changes tokens-status')
changes))
changes))
(defn generate-activate-theme
[changes tokens-status tokens-lib id]
(if tokens-lib
(update-tokens-status changes tokens-status cfo/activate-theme tokens-lib id)
changes))
(defn generate-deactivate-theme
[changes tokens-status tokens-lib id]
(if tokens-lib
(update-tokens-status changes tokens-status cfo/deactivate-theme tokens-lib id)
changes))
(defn generate-set-theme-status
[changes tokens-status tokens-lib id active?]
(if tokens-lib
(update-tokens-status changes tokens-status cfo/set-theme-active tokens-lib id active?)
changes))
(defn generate-toggle-theme
[changes tokens-status tokens-lib id]
(if tokens-lib
(update-tokens-status changes tokens-status cfo/toggle-theme-active tokens-lib id)
changes))
(defn generate-set-enabled-token-set
[changes tokens-status tokens-lib id enabled?]
(if tokens-lib
(update-tokens-status changes tokens-status cfo/set-set-active tokens-lib id enabled?)
changes))
(defn generate-toggle-token-set
[changes tokens-status tokens-lib id]
(if tokens-lib
(update-tokens-status changes tokens-status cfo/toggle-set-active tokens-lib id)
changes))
(defn generate-toggle-token-set-group
[changes tokens-status tokens-lib group-path]
(if tokens-lib
(update-tokens-status changes tokens-status cfo/toggle-set-group-active tokens-lib group-path)
changes))
(defn generate-sync-tokens-status-with-lib
[changes tokens-status tokens-lib]
(if tokens-lib
(update-tokens-status changes tokens-status cfo/sync-tokens-status-with-lib tokens-lib)
changes))
+26 -3
View File
@@ -7,6 +7,7 @@
(ns app.common.test-helpers.tokens
(:require
[app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.shapes :as ths]
[app.common.types.container :as ctn]
@@ -17,17 +18,39 @@
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]))
(defn get-tokens-source
[file]
(-> file (ctf/file-data) (cfo/get-tokens-source)))
(defn get-tokens-lib
[file]
(:tokens-lib (ctf/file-data file)))
(-> file (ctf/file-data) (cfo/get-tokens-lib)))
(defn get-tokens-status
[file]
(-> file (ctf/file-data) (cfo/get-tokens-status)))
(defn add-tokens-lib
"Ensure the file has a tokens-lib and a tokens-status in its data, creating empty ones if not"
[file]
(ctf/update-file-data file #(update % :tokens-lib ctob/ensure-tokens-lib)))
(ctf/update-file-data file cfo/ensure-tokens-lib))
(defn update-tokens-lib
"Modify the tokens-lib of a file "
[file f]
(ctf/update-file-data file #(update % :tokens-lib f)))
(ctf/update-file-data file #(cfo/update-tokens-lib % f)))
(defn update-tokens-status
[file f]
(ctf/update-file-data file #(cfo/update-tokens-status % f)))
(defn sample-file-with-tokens
[& {:keys [lib-fn status-fn file-id] :as params
:or {lib-fn identity status-fn identity file-id :file1}}]
(-> (thf/sample-file file-id (dissoc params :lib-fn :status-fn))
(add-tokens-lib)
(update-tokens-lib lib-fn)
(update-tokens-status status-fn)))
(defn get-token
[file set-id token-id]
+1 -9
View File
@@ -72,13 +72,6 @@
[:map {:title "PlainColorAttrs"}
[:color schema:hex-color]])
(def schema:image-transform
[:map {:title "ImageTransform" :closed true}
[:x {:optional true} ::sm/safe-number]
[:y {:optional true} ::sm/safe-number]
[:width {:optional true} ::sm/safe-number]
[:height {:optional true} ::sm/safe-number]])
(def schema:image
[:map {:title "ImageColor" :closed true}
[:width [::sm/int {:min 0 :gen/gen sg/int}]]
@@ -86,8 +79,7 @@
[:mtype {:gen/gen (sg/elements cm/image-types)} ::sm/text]
[:id ::sm/uuid]
[:name {:optional true} ::sm/text]
[:keep-aspect-ratio {:optional true} :boolean]
[:transform {:optional true} schema:image-transform]])
[:keep-aspect-ratio {:optional true} :boolean]])
(def image-attrs
"A set of attrs that corresponds to image data type"
+1 -1
View File
@@ -87,7 +87,7 @@
[:components {:optional true} schema:components]
[:typographies {:optional true} schema:typographies]
[:plugin-data {:optional true} schema:plugin-data]
[:tokens-source {:optional true} ::sm/uuid] ;; Forward-compat: UUID of external library containing tokens-lib (full support in follow-up PR)
[:tokens-source {:optional true} ::sm/uuid] ;; The tokens-lib may be in this file or in an external library
[:tokens-lib {:optional true} ctob/schema:tokens-lib]
[:tokens-status {:optional true} ctos/schema:tokens-status]])
Loaded 100 of 415 files, more files were not shown because too many files have changed in this diff. Show more