mirror of
https://github.com/penpot/penpot.git
synced 2026-07-23 22:48:12 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
a004f7cd84
@ -52,7 +52,7 @@ built fresh from `shared/codex.toml` + `templates/codex.toml` at launch.
|
||||
`manage.sh`.
|
||||
* **`mcp/`** (Claude Code, opencode) is the result of merging `shared/` with
|
||||
the port-substituted `templates/`. `manage.sh` writes these on every
|
||||
`run-devenv-agentic` pass. Gitignored, dedicated paths with no developer
|
||||
`run-devenv --agentic` pass. Gitignored, dedicated paths with no developer
|
||||
content — never edit by hand, your edits will be overwritten on the next
|
||||
reconcile.
|
||||
* **`.vscode/mcp.json`** is the same merge, but written to the path VS Code
|
||||
|
||||
27
.opencode/skills/create-issue/SKILL.md
Normal file
27
.opencode/skills/create-issue/SKILL.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
name: create-issue
|
||||
description: Create or update GitHub issues (from PR, from draft body, retitle existing). Routes to the canonical flow in `mem:workflow/creating-issues`.
|
||||
---
|
||||
|
||||
# Skill: create-issue
|
||||
|
||||
Entry point for all GitHub issue work. All rules (title derivation, metadata,
|
||||
body templates, Issue Type IDs), all flows, and all `gh` / GraphQL commands
|
||||
live in `mem:workflow/creating-issues` (file:
|
||||
`.serena/memories/workflow/creating-issues.md`). This skill routes to the
|
||||
right flow.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Create from PR** — PR exists; the issue is the changelog/release unit,
|
||||
the PR is the implementation. Issue = WHAT, PR = HOW.
|
||||
→ memory section **Creating Issues from PRs**
|
||||
- **Create from draft body** — Taiga story, user report, discussion; no PR
|
||||
yet.
|
||||
→ memory section **Creating Issues from Draft Body**
|
||||
- **Retitle existing issue** — current title is vague, prefixed, or stale.
|
||||
→ memory section **Retitling an Existing Issue**
|
||||
|
||||
Everything else (title derivation, metadata policy, body templates, Issue
|
||||
Type IDs, create/verify/cleanup commands) lives in the memory — go to the
|
||||
matching section there.
|
||||
@ -1,227 +0,0 @@
|
||||
---
|
||||
name: gh-issue-from-pr
|
||||
description: Create a user-facing GitHub issue from a PR, separating the WHAT from the HOW, with correct milestone, project, labels, and issue type.
|
||||
---
|
||||
|
||||
# Skill: gh-issue-from-pr
|
||||
|
||||
Create a GitHub issue that captures the **WHAT** (user-facing feature or
|
||||
bug) from an existing PR that describes the **HOW** (implementation).
|
||||
Used when the project board needs an issue as the primary changelog/release unit.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Create a tracking issue from a PR for changelog purposes
|
||||
- Extract the user-facing problem/feature from a PR's implementation details
|
||||
- Assign milestone, project, labels, and issue type to a new issue derived from a PR
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`)
|
||||
- Permission to create issues and edit PRs in the target repository
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Understand the PR
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot \
|
||||
--json title,body,author,labels,baseRefName,mergedAt,state,milestone
|
||||
```
|
||||
|
||||
Identify:
|
||||
|
||||
- **WHAT** — user-facing problem or feature. Goes into the issue.
|
||||
Describe symptoms and impact, not internal mechanisms.
|
||||
- **HOW** — implementation details. These belong in the PR, not the issue.
|
||||
|
||||
### 2. Determine metadata
|
||||
|
||||
| Field | Source | Rule |
|
||||
|-------|--------|------|
|
||||
| **Title** | PR title | Rewrite from user perspective. Strip leading emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`). Focus on observable behavior. Use imperative mood. Use the `issue-title` skill to generate this. |
|
||||
| **Labels** | PR labels | Copy `community contribution` if present. Skip `bug` and `enhancement` (redundant with Issue Type). Skip workflow labels (`backport candidate`, `team-qa`). |
|
||||
| **Milestone** | PR milestone | **Always copy what's on the PR.** Fetch with: `gh pr view <PR_NUMBER> --json milestone --jq '.milestone.title'` If the PR has no milestone, create the issue without one. |
|
||||
| **Project** | Always `Main` | Penpot uses the `Main` project (number 8) for all issues. |
|
||||
| **Body** | PR's user-facing section | Extract steps to reproduce or feature description. Omit internal details. Use templates below. |
|
||||
| **Issue Type** | PR labels / title | Map: `bug` label or `:bug:` title → `Bug`. `enhancement` label or `:sparkles:` title → `Enhancement`. Feature/epic → `Feature`. Default → `Task`. |
|
||||
|
||||
### 3. Write the issue body
|
||||
|
||||
**Bug template:**
|
||||
|
||||
```markdown
|
||||
### Description
|
||||
|
||||
<what breaks, what the user experiences>
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
1. <step 1>
|
||||
2. <step 2>
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<what should happen instead>
|
||||
|
||||
### Affected versions
|
||||
|
||||
<version>
|
||||
```
|
||||
|
||||
**Enhancement template:**
|
||||
|
||||
```markdown
|
||||
### Description
|
||||
|
||||
<what the user can now do that they couldn't before>
|
||||
|
||||
### Use case
|
||||
|
||||
<why this is useful, who benefits>
|
||||
|
||||
### Affected versions
|
||||
|
||||
<version>
|
||||
```
|
||||
|
||||
### 4. Create the issue
|
||||
|
||||
Write the body to a temp file to avoid shell quoting issues:
|
||||
|
||||
```bash
|
||||
cat > /tmp/issue-body.md << 'ISSUE_BODY'
|
||||
<body content here>
|
||||
ISSUE_BODY
|
||||
```
|
||||
|
||||
Create:
|
||||
|
||||
```bash
|
||||
gh issue create \
|
||||
--repo penpot/penpot \
|
||||
--title "<Title>" \
|
||||
--label "community contribution" \ # only if PR has this label
|
||||
--milestone "<milestone>" \
|
||||
--project "Main" \
|
||||
--body-file /tmp/issue-body.md
|
||||
```
|
||||
|
||||
Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
|
||||
|
||||
### 5. Assign to the PR author
|
||||
|
||||
Assign the issue to the PR author so they're responsible for it:
|
||||
|
||||
```bash
|
||||
AUTHOR=$(gh pr view <PR_NUMBER> --repo penpot/penpot --json author --jq '.author.login')
|
||||
gh issue edit <ISSUE_NUMBER> --repo penpot/penpot --add-assignee "$AUTHOR"
|
||||
```
|
||||
|
||||
### 6. Set the Issue Type
|
||||
|
||||
`gh issue create` can't set the Issue Type directly. Use GraphQL.
|
||||
|
||||
Get the issue's GraphQL node ID:
|
||||
|
||||
```bash
|
||||
ISSUE_ID=$(gh api graphql -f query='
|
||||
query { repository(owner: "penpot", name: "penpot") {
|
||||
issue(number: <ISSUE_NUMBER>) { id }
|
||||
}}' --jq '.data.repository.issue.id')
|
||||
```
|
||||
|
||||
Issue Type IDs for the Penpot repo:
|
||||
|
||||
| Type | ID |
|
||||
|------|----|
|
||||
| Bug | `IT_kwDOAcyBPM4AX5Nb` |
|
||||
| Enhancement | `IT_kwDOAcyBPM4B_IQN` |
|
||||
| Feature | `IT_kwDOAcyBPM4AX5Nf` |
|
||||
| Task | `IT_kwDOAcyBPM4AX5NY` |
|
||||
| Question | `IT_kwDOAcyBPM4B_IQj` |
|
||||
| Docs | `IT_kwDOAcyBPM4B_IQz` |
|
||||
|
||||
Set it:
|
||||
|
||||
```bash
|
||||
gh api graphql -f query='
|
||||
mutation {
|
||||
updateIssue(input: {
|
||||
id: "'"$ISSUE_ID"'"
|
||||
issueTypeId: "<TYPE_ID>"
|
||||
}) {
|
||||
issue { number issueType { name } }
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 7. Verify
|
||||
|
||||
```bash
|
||||
gh issue view <ISSUE_NUMBER> --repo penpot/penpot \
|
||||
--json title,milestone,projectItems,labels \
|
||||
--jq '{title, milestone: .milestone.title, projects: [.projectItems[].title], labels: [.labels[].name]}'
|
||||
|
||||
gh api graphql -f query='
|
||||
query { repository(owner: "penpot", name: "penpot") {
|
||||
issue(number: <ISSUE_NUMBER>) { issueType { name } }
|
||||
}}' --jq '.data.repository.issue.issueType.name'
|
||||
```
|
||||
|
||||
### 8. Link the PR to the issue
|
||||
|
||||
Append `Closes #<ISSUE_NUMBER>` to the PR body:
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot --json body --jq '.body' > /tmp/pr-body.md
|
||||
printf "\n\nCloses #<ISSUE_NUMBER>\n" >> /tmp/pr-body.md
|
||||
gh pr edit <PR_NUMBER> --repo penpot/penpot --body-file /tmp/pr-body.md
|
||||
|
||||
# Verify
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot --json body \
|
||||
--jq '.body | test("Closes #<ISSUE_NUMBER>")'
|
||||
```
|
||||
|
||||
**Note:** If the PR is already merged, `Closes` won't auto-close the issue
|
||||
— it only creates the "Development" sidebar link. This is the desired
|
||||
behavior since the issue is a tracking artifact.
|
||||
|
||||
### 9. Clean up
|
||||
|
||||
```bash
|
||||
rm -f /tmp/issue-body.md /tmp/pr-body.md
|
||||
```
|
||||
|
||||
## Label rules
|
||||
|
||||
| PR has | Issue gets |
|
||||
|--------|-----------|
|
||||
| `community contribution` | `community contribution` |
|
||||
| `bug`, `enhancement` | *(skip — redundant with Issue Type)* |
|
||||
| `backport candidate` | *(skip — workflow label)* |
|
||||
| `team-qa` | *(skip — workflow label)* |
|
||||
|
||||
## Issue Type mapping
|
||||
|
||||
| PR label(s) / title prefix | Issue Type |
|
||||
|----------------------------|-----------|
|
||||
| `bug` or `:bug:` | Bug |
|
||||
| `enhancement` or `:sparkles:` or `:tada:` | Enhancement |
|
||||
| Feature / epic | Feature |
|
||||
| Documentation | Docs |
|
||||
| None of the above | Task |
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Issue = WHAT, PR = HOW.** Never put implementation details in the
|
||||
issue body. The issue is for users, QA, and changelog readers.
|
||||
- **Copy the milestone from the PR.** Don't guess based on branch names.
|
||||
If the PR has no milestone, create the issue without one.
|
||||
- **Set Issue Type via GraphQL** — `gh issue create` can't set it.
|
||||
- **Link via PR body** — `Closes #<NUMBER>` creates the "Development"
|
||||
sidebar link automatically.
|
||||
- **One issue per PR** — even if a PR fixes multiple things, create a
|
||||
single issue that summarizes the overall change.
|
||||
- **Community attribution:** if the PR has the `community contribution`
|
||||
label or the author is not a core team member, add the label to the issue.
|
||||
@ -1,123 +0,0 @@
|
||||
---
|
||||
name: issue-title
|
||||
description: Derive a clear, well-formatted title for a GitHub issue from its description body, using descriptive present-tense for bugs and imperative mood for features, always including the "where" (location in the UI/module).
|
||||
---
|
||||
|
||||
# Skill: issue-title
|
||||
|
||||
Derive a concise, descriptive title for a GitHub issue based on its body
|
||||
content. Use **descriptive present tense for bugs** (e.g. "Plugin API
|
||||
crashes when setting text fills") and **imperative mood for features** (e.g.
|
||||
"Add customizable dash and gap controls"). No emoji or type prefixes
|
||||
(`feat:`, `bug:`, `feature:`, etc.).
|
||||
|
||||
Can be used both when **creating a new issue** and when **updating an
|
||||
existing one** that has a vague or outdated title.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Creating a new issue and need a well-formatted title from the draft body
|
||||
- An existing issue has a vague, outdated, or auto-generated title (e.g.
|
||||
`[PENPOT FEEDBACK]: ...`, `feature: ...`)
|
||||
- The current title doesn't reflect the actual content of the description
|
||||
- The title is missing the "where" (which part of the UI/module is affected)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`)
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Get the issue body
|
||||
|
||||
For an **existing issue**, fetch it:
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot --json title,body
|
||||
```
|
||||
|
||||
For a **new issue**, read the draft body from wherever it was provided
|
||||
(Taiga link, user report, discussion, etc.).
|
||||
|
||||
### 2. Read the body and derive a title
|
||||
|
||||
Extract the core problem or request from the description. Distinguish between
|
||||
bug reports and feature requests:
|
||||
|
||||
**Bug titles (descriptive, present tense):**
|
||||
Describe the symptom as it appears to the user. Format:
|
||||
`[Where] [present-tense verb] when [condition]`
|
||||
|
||||
- *"Plugin API crashes when setting text fills"*
|
||||
- *"Canvas renders glitches when zooming quickly"*
|
||||
- *"French Canada locale falls back to French (fr) translations"*
|
||||
- *"Text layer content is not deleted when WebGL render is enabled"*
|
||||
|
||||
Do **not** start bug titles with "Fix" or any imperative verb. The title
|
||||
should state what's broken, not command a fix.
|
||||
|
||||
**Feature / Enhancement titles (imperative mood):**
|
||||
Command what should be built. Format:
|
||||
`[Imperative verb] [what] in/on [where]`
|
||||
|
||||
- *"Add customizable dash and gap length controls to dashed strokes in the sidebar"*
|
||||
- *"Show user, timestamp, and hash in the workspace history panel like git commits"*
|
||||
- *"Validate shape on add-object to catch malformed inputs early"*
|
||||
|
||||
**Universal rules (both types):**
|
||||
- **Include the "where"** — specify the UI location or module (e.g.
|
||||
"in the sidebar", "in the workspace history panel", "on the stroke
|
||||
options")
|
||||
- **No prefixes** — strip `bug:`, `feature:`, `feat:`, `:bug:`, `:sparkles:`,
|
||||
`[PENPOT FEEDBACK]`, etc.
|
||||
- **No emoji** — plain text only
|
||||
- **Be specific** — prefer concrete detail over generality. If the
|
||||
description mentions two related problems, capture both.
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Original / draft title | Type | New title |
|
||||
|---|---|---|
|
||||
| `[PENPOT FEEDBACK]: WebGL` | Bug | `Canvas renders glitches when zooming quickly — text appears distorted and nodes have background-colored rectangles` |
|
||||
| `bug: flatten-nested-tokens-json uses $type instead of $value as the DTCG token/group discriminator` | Bug | `Token import fails when group-level type inheritance is used — parser misidentifies groups as tokens` |
|
||||
| `feature: Dashed stroke customization` | Feature | `Add customizable dash and gap length controls to dashed strokes in the sidebar` |
|
||||
| `feature: Add more detail to history of actions` | Feature | `Show user, timestamp, and hash in the workspace history panel like git commits` |
|
||||
|
||||
### 3. Apply the title
|
||||
|
||||
**If updating an existing issue:**
|
||||
|
||||
```bash
|
||||
gh issue edit <NUMBER> --repo penpot/penpot --title "<NEW TITLE>"
|
||||
```
|
||||
|
||||
**If creating a new issue:**
|
||||
|
||||
```bash
|
||||
gh issue create --repo penpot/penpot --title "<NEW TITLE>" --body "<BODY>"
|
||||
```
|
||||
|
||||
### 4. Confirm
|
||||
|
||||
For updates, the command returns the issue URL. Verify by optionally fetching
|
||||
again:
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot --json title
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Bug titles describe the symptom** — present tense, 3rd person:
|
||||
"crashes", "fails", "shows", "is cut off", "does not load". Do not
|
||||
start with "Fix" or "Bug:".
|
||||
- **Feature titles use imperative mood** — command form: "Add", "Show",
|
||||
"Use", "Validate", "Support", "Toggle".
|
||||
- **Always include the "where"** — a title like "Crashes when zooming"
|
||||
is too vague; "Canvas crashes when zooming quickly" is clear.
|
||||
- **No prefixes, no emoji** — strip all type labels and decorative
|
||||
characters from the title.
|
||||
- **Derive from the body, not the current title** — the body contains
|
||||
the real detail; the current title may be auto-generated or stale.
|
||||
- **Two problems → cover both** — if the description has two distinct
|
||||
but related issues, capture both in the title joined by "and".
|
||||
@ -39,6 +39,9 @@ Backend RPC command areas without focused memories include access tokens, binfil
|
||||
|
||||
Database migrations live in `backend/src/app/migrations/`; pure SQL migrations are under `backend/src/app/migrations/sql/`. SQL filenames conventionally start with a sequence and verb/table description, e.g. `0026-mod-profile-table-add-is-active-field`. Applied migrations are tracked in the `migrations` table.
|
||||
|
||||
For interactive PostgreSQL access with correct dev defaults, use `tools/psql`; to dump
|
||||
the current DDL schema, use `tools/db-schema` (see `mem:tools/psql`).
|
||||
|
||||
For deeper details on transaction semantics, advisory locks, Transit vs JSON helpers, and dev/test DB URLs: `mem:backend/rpc-db-worker-subtleties`.
|
||||
|
||||
## Background tasks
|
||||
|
||||
@ -9,7 +9,11 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
|
||||
|
||||
# Development workflow
|
||||
|
||||
- Commit only when explicitly asked. Commit/PR format + changelog: `mem:workflow/creating-commits`, `mem:workflow/creating-prs`. Issue creation (titles, labels, body templates, Issue Types): `mem:workflow/creating-issues`.
|
||||
- Commit/PR/issue creation is **on explicit request only**. Before any of these actions, read the relevant memory — don't infer format from prior examples:
|
||||
- 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)
|
||||
- **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.
|
||||
- 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.
|
||||
@ -45,7 +49,8 @@ module. You can read it from `mem:<MODULE>/core`
|
||||
tmux session lifecycle, MinIO provisioning, or anything in `manage.sh`'s
|
||||
`*-devenv` commands, read `mem:devenv/core`.
|
||||
- `tools/` contains standalone dev utilities: `nrepl-eval.mjs` (backend REPL eval),
|
||||
`paren-repair.bb` (delimiter-error fixer, see `mem:tools/paren-repair`), and
|
||||
`paren-repair.bb` (delimiter-error fixer, see `mem:tools/paren-repair`),
|
||||
`psql` / `db-schema` (PostgreSQL client and schema dump wrappers, see `mem:tools/psql`), and
|
||||
`taiga.py` / `gh.py` (issue management helpers).
|
||||
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
|
||||
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
|
||||
|
||||
@ -25,7 +25,7 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
|
||||
|
||||
## Worker policy
|
||||
|
||||
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. ws0 must be running whenever any ws1+ is running, and is the last instance to stop — `run-devenv-agentic --ws N` (N≥1) auto-starts ws0 first; `stop-devenv` refuses to stop ws0 while any ws1+ is up. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
|
||||
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. ws0 must be running whenever any ws1+ is running, and is the last instance to stop — `run-devenv --agentic --ws N` (N≥1) auto-starts ws0 first; `stop-devenv` refuses to stop ws0 while any ws1+ is up. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
|
||||
|
||||
## Port layout
|
||||
|
||||
@ -44,13 +44,13 @@ Everything else (frontend dev, backend API, exporter, storybook, REPLs, plugin d
|
||||
|
||||
## Tmux + MCP routing
|
||||
|
||||
`docker/devenv/files/start-tmux.sh` is session-level idempotent. Reads `PENPOT_TMUX_ATTACH`. If the session exists it attaches or exits; otherwise creates 4 base windows (frontend watch / storybook / exporter / backend) plus `mcp` (when `enable-mcp` in `PENPOT_FLAGS`) and `serena` (when `SERENA_ENABLED=true`). `run-devenv-agentic` always sets both env vars. The legacy `run-devenv` alias doesn't, hence its 4-window-only session. To switch from a legacy session to agentic, `stop-devenv` then `run-devenv-agentic` — the conditional windows are only added at session create time.
|
||||
`docker/devenv/files/start-tmux.sh` is session-level idempotent. Reads `PENPOT_TMUX_ATTACH`. If the session exists it attaches or exits; otherwise creates 4 base windows (frontend watch / storybook / exporter / backend) plus `mcp` (when `enable-mcp` in `PENPOT_FLAGS`) and `serena` (when `SERENA_ENABLED=true`). `run-devenv --agentic` always sets both env vars. The legacy `run-devenv` alias doesn't, hence its 4-window-only session. To switch from a legacy session to agentic, `stop-devenv` then `run-devenv --agentic` — the conditional windows are only added at session create time.
|
||||
|
||||
MCP plugin routing is same-origin: frontend uses `<public-uri>/mcp/ws`, per-instance nginx proxies to MCP port 4401 in-container. For the plugin↔MCP server wiring (how the browser plugin discovers the URL, the in-memory connection registry, why DB-mediated routing isn't needed), see `mem:mcp/core`.
|
||||
|
||||
## Workspace orchestration (ws1+)
|
||||
|
||||
Workspace directories are user-maintained at `${PENPOT_WORKSPACES_DIR}/wsN`. `run-devenv-agentic --ws i` syncs only when `--sync` is passed, with one exception: if the workspace directory is missing on first use, sync runs implicitly to seed it.
|
||||
Workspace directories are user-maintained at `${PENPOT_WORKSPACES_DIR}/wsN`. `run-devenv --agentic --ws i` syncs only when `--sync` is passed, with one exception: if the workspace directory is missing on first use, sync runs implicitly to seed it.
|
||||
|
||||
`sync-workspace wsN`:
|
||||
1. `assert-clean-git-state` — refuses on `.git/{rebase-apply,rebase-merge,MERGE_HEAD,CHERRY_PICK_HEAD,index.lock}`. No `--sync-force` escape.
|
||||
@ -63,7 +63,7 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
|
||||
|
||||
## CLI surface
|
||||
|
||||
- `run-devenv-agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). Auto-starts ws0 first when the target is ws1+ and ws0 is not yet up.
|
||||
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). Auto-starts ws0 first when the target is ws1+ and ws0 is not yet up.
|
||||
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` (N≥1) stops just that workspace. `--ws 0` or no flag stops ws0 + shared infra, refused while any ws1+ is running. `--all` stops every ws highest-first then ws0, then infra.
|
||||
- `run-devenv`: legacy alias, ws0 non-agentic attached.
|
||||
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
|
||||
|
||||
51
.serena/memories/tools/psql.md
Normal file
51
.serena/memories/tools/psql.md
Normal file
@ -0,0 +1,51 @@
|
||||
# PostgreSQL client wrapper
|
||||
|
||||
`tools/psql` is a wrapper around the `psql` command with defaults preconfigured
|
||||
for the Penpot development environment.
|
||||
|
||||
## When to use
|
||||
|
||||
- Running SQL queries against the dev database (`penpot`) or test database
|
||||
(`penpot_test`).
|
||||
- Inspecting table structures, running migrations manually, or debugging
|
||||
database state.
|
||||
- Any time you need PostgreSQL access and want the correct host/user/password
|
||||
without typing them each time.
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
# Interactive session (penpot database)
|
||||
tools/psql
|
||||
|
||||
# Interactive session (penpot_test database)
|
||||
tools/psql --test
|
||||
|
||||
# Inline query (penpot)
|
||||
tools/psql -c "SELECT 1"
|
||||
|
||||
# Inline query (penpot_test)
|
||||
tools/psql --test -c "SELECT 1"
|
||||
|
||||
# Override defaults
|
||||
tools/psql -h other-host -U other-user -d other-db
|
||||
|
||||
# Pipe SQL from a file
|
||||
tools/psql -f some-query.sql
|
||||
```
|
||||
|
||||
All standard `psql` flags are passed through after the wrapper's own flags.
|
||||
|
||||
## Defaults
|
||||
|
||||
| Setting | Default | Env override |
|
||||
|----------|-----------|---------------------|
|
||||
| Host | `postgres` | `PENPOT_DB_HOST` |
|
||||
| User | `penpot` | `PENPOT_DB_USER` |
|
||||
| Password | `penpot` | `PENPOT_DB_PASSWORD` |
|
||||
| Database | `penpot` | `PENPOT_DB_NAME` |
|
||||
|
||||
## See also
|
||||
|
||||
`tools/db-schema` — a companion script that dumps the current DDL schema
|
||||
using `pg_dump --schema-only`, with the same defaults and `--test` flag.
|
||||
@ -79,6 +79,8 @@ Write the body to a temp file to avoid shell quoting issues:
|
||||
<version>
|
||||
```
|
||||
|
||||
Note: do not soft-wrap paragraphs in the body. Each paragraph is a single line in the source; newlines are reserved for structural breaks (section headers, list items, code-block fences, blank-line separators). List items stay on a single line each. GitHub renders single-line paragraphs correctly, and wrapping makes diffs noisy on every small wording change. Same rule applies to PR bodies.
|
||||
|
||||
## Creating the Issue
|
||||
|
||||
```bash
|
||||
@ -155,6 +157,199 @@ query { repository(owner: "penpot", name: "penpot") {
|
||||
rm -f /tmp/issue-body.md
|
||||
```
|
||||
|
||||
## Creating Issues from PRs
|
||||
|
||||
Used when the project board needs an issue as the primary changelog/release
|
||||
unit and the PR describes the implementation. The issue is the **WHAT**
|
||||
(user-facing), the PR is the **HOW** (implementation).
|
||||
|
||||
### Fetch the PR
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot \
|
||||
--json title,body,author,labels,baseRefName,mergedAt,state,milestone
|
||||
```
|
||||
|
||||
Identify:
|
||||
|
||||
- **WHAT** — user-facing problem or feature. Goes into the issue.
|
||||
Describe symptoms and impact, not internal mechanisms.
|
||||
- **HOW** — implementation details. These belong in the PR, not the issue.
|
||||
|
||||
### Determine metadata
|
||||
|
||||
- **Title:** rewrite from user perspective using the title rules above. Strip
|
||||
leading emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`). Focus on
|
||||
observable behavior.
|
||||
- **Labels:** copy `community contribution` if present on the PR.
|
||||
- **Milestone:** always copy what's on the PR.
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --json milestone --jq '.milestone.title'
|
||||
```
|
||||
|
||||
If the PR has no milestone, create the issue without one.
|
||||
- **Project:** `Main`.
|
||||
- **Body:** extract the user-facing section (steps to reproduce or feature
|
||||
description). Omit internal details. Use the templates above.
|
||||
- **Issue Type:** use the mapping table above (also handles `:bug:` /
|
||||
`:sparkles:` / `:tada:` title prefixes).
|
||||
|
||||
### Create the issue
|
||||
|
||||
```bash
|
||||
cat > /tmp/issue-body.md << 'ISSUE_BODY'
|
||||
<body content here>
|
||||
ISSUE_BODY
|
||||
|
||||
gh issue create \
|
||||
--repo penpot/penpot \
|
||||
--title "<Title>" \
|
||||
--label "community contribution" \ # only if PR has this label
|
||||
--milestone "<milestone>" \
|
||||
--project "Main" \
|
||||
--body-file /tmp/issue-body.md
|
||||
```
|
||||
|
||||
Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
|
||||
|
||||
### Assign to the PR author
|
||||
|
||||
```bash
|
||||
AUTHOR=$(gh pr view <PR_NUMBER> --repo penpot/penpot --json author --jq '.author.login')
|
||||
gh issue edit <ISSUE_NUMBER> --repo penpot/penpot --add-assignee "$AUTHOR"
|
||||
```
|
||||
|
||||
### Set Issue Type and verify
|
||||
|
||||
See the **Setting the Issue Type** and **Verification** sections above — the
|
||||
GraphQL mutations and `gh issue view` calls are identical regardless of how
|
||||
the issue was sourced.
|
||||
|
||||
### Link the PR to the issue
|
||||
|
||||
Append `Closes #<ISSUE_NUMBER>` to the PR body:
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot --json body --jq '.body' > /tmp/pr-body.md
|
||||
printf "\n\nCloses #<ISSUE_NUMBER>\n" >> /tmp/pr-body.md
|
||||
gh pr edit <PR_NUMBER> --repo penpot/penpot --body-file /tmp/pr-body.md
|
||||
|
||||
# Verify
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot --json body \
|
||||
--jq '.body | test("Closes #<ISSUE_NUMBER>")'
|
||||
```
|
||||
|
||||
**Note:** If the PR is already merged, `Closes` won't auto-close the issue —
|
||||
it only creates the "Development" sidebar link. This is the desired
|
||||
behavior since the issue is a tracking artifact.
|
||||
|
||||
### Clean up
|
||||
|
||||
```bash
|
||||
rm -f /tmp/issue-body.md /tmp/pr-body.md
|
||||
```
|
||||
|
||||
### Rules for this flow
|
||||
|
||||
- **One issue per PR** — even if a PR fixes multiple things, create a single
|
||||
issue that summarizes the overall change.
|
||||
- **Community attribution:** if the PR has the `community contribution`
|
||||
label or the author is not a core team member, add the label to the issue.
|
||||
- **Don't put implementation details in the issue body** — the issue is for
|
||||
users, QA, and changelog readers.
|
||||
|
||||
## Creating Issues from Draft Body
|
||||
|
||||
Used when the user provides a draft body from elsewhere (Taiga story, user
|
||||
report, discussion transcript) and there is no PR yet.
|
||||
|
||||
### Get the body
|
||||
|
||||
Read the draft body from wherever it was provided. If the user gives only a
|
||||
vague one-liner, ask them to expand it (steps to reproduce, expected vs.
|
||||
actual, use case) before proceeding.
|
||||
|
||||
### Derive the title
|
||||
|
||||
Apply the title rules in the **Title Derivation** section above. Distinguish
|
||||
bug vs. feature from the body content:
|
||||
|
||||
- Steps to reproduce + expected vs. actual → bug
|
||||
- "would be nice", "add support for", "allow users to" → feature / enhancement
|
||||
|
||||
### Choose a body template
|
||||
|
||||
Use the bug or enhancement template from the **Issue Body Template** section
|
||||
above. Fill in placeholders with the user-provided details. If the body
|
||||
doesn't fit either, ask the user which template to use.
|
||||
|
||||
### Determine metadata
|
||||
|
||||
- **Project:** `Main` (always).
|
||||
- **Milestone:** ask the user if not obvious; otherwise omit.
|
||||
- **Labels:** usually none for new user-reported issues. Add
|
||||
`community contribution` if the user is a non-team contributor.
|
||||
- **Issue Type:** use the mapping table above (bug description → Bug; feature
|
||||
request → Enhancement or Feature).
|
||||
|
||||
### Create the issue
|
||||
|
||||
```bash
|
||||
cat > /tmp/issue-body.md << 'ISSUE_BODY'
|
||||
<body content here>
|
||||
ISSUE_BODY
|
||||
|
||||
gh issue create \
|
||||
--repo penpot/penpot \
|
||||
--title "<Title>" \
|
||||
--label "community contribution" \ # only if applicable
|
||||
--milestone "<milestone>" \ # only if provided
|
||||
--project "Main" \
|
||||
--body-file /tmp/issue-body.md
|
||||
```
|
||||
|
||||
### Set Issue Type and verify
|
||||
|
||||
Same GraphQL mutation and `gh issue view` commands as in the
|
||||
**Setting the Issue Type** and **Verification** sections above.
|
||||
|
||||
### Clean up
|
||||
|
||||
```bash
|
||||
rm -f /tmp/issue-body.md
|
||||
```
|
||||
|
||||
## Retitling an Existing Issue
|
||||
|
||||
Used when an issue's current title is vague, prefixed, or no longer matches
|
||||
the body (e.g. `[PENPOT FEEDBACK]: ...`, `feature: ...`).
|
||||
|
||||
### Fetch the issue
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot --json title,body
|
||||
```
|
||||
|
||||
### Derive a new title
|
||||
|
||||
Read the body (not the current title) and apply the title rules in the
|
||||
**Title Derivation** section above.
|
||||
|
||||
### Apply the new title
|
||||
|
||||
```bash
|
||||
gh issue edit <NUMBER> --repo penpot/penpot --title "<NEW TITLE>"
|
||||
```
|
||||
|
||||
### Confirm
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot --json title
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- Creating issues **from PRs** (separating WHAT from HOW): `mem:workflow/creating-prs`
|
||||
- End-to-end orchestration entry point: the `create-issue` skill at
|
||||
`.opencode/skills/create-issue/SKILL.md`. The skill is a thin entry
|
||||
point; this memory is the canonical home for all issue-creation rules.
|
||||
|
||||
15
AGENTS.md
15
AGENTS.md
@ -1,5 +1,20 @@
|
||||
# AI AGENT GUIDE
|
||||
|
||||
## Hard rules (always apply — no exceptions)
|
||||
|
||||
- **Never `git push`, force-push, or modify `git origin`** (or any other remote).
|
||||
The user pushes from their own shell. If a push is required to surface the
|
||||
agent's work (e.g. force-push after an amend), state this in the response and
|
||||
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.
|
||||
- **Read the workflow memory BEFORE the corresponding action**:
|
||||
- Before `git commit` → `mem:workflow/creating-commits` (commit format, AI-assisted-by trailer)
|
||||
- Before `gh issue create` → `mem:workflow/creating-issues` (title derivation, body template, Issue Type)
|
||||
- Before `gh pr create` / `gh pr edit` → `mem:workflow/creating-prs` (title format, body structure, AI note)
|
||||
Don't infer format from the title of a previous commit/issue/PR — the memory
|
||||
is the source of truth.
|
||||
|
||||
## CRITICAL: Read module memories BEFORE writing any code
|
||||
|
||||
Do this **before planning, before coding, before touching any file**:
|
||||
|
||||
@ -64,6 +64,7 @@
|
||||
- Add resource limits to font processing child processes [#10234](https://github.com/penpot/penpot/issues/10234) (PR: [#10274](https://github.com/penpot/penpot/pull/10274))
|
||||
- Add color variants and positioning to selection size badge (by @bittoby) [#10258](https://github.com/penpot/penpot/issues/10258) (PR: [#9210](https://github.com/penpot/penpot/pull/9210))
|
||||
- Use hard reload for render engine switching in the workspace menu [#10441](https://github.com/penpot/penpot/issues/10441) (PR: [#10444](https://github.com/penpot/penpot/pull/10444))
|
||||
- Rotate size badge when shape is rotated [#10386](https://github.com/penpot/penpot/issues/10386) (PR: [#10393](https://github.com/penpot/penpot/pull/10393))
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@ -171,6 +172,14 @@
|
||||
- Fix emojis without fill but with outer stroke not being rendered [#10473](https://github.com/penpot/penpot/issues/10473) (PR: [#10519](https://github.com/penpot/penpot/pull/10519))
|
||||
- Fix texts with lots of emojis not being rendered across multiple tiles [#10474](https://github.com/penpot/penpot/issues/10474) (PR: [#10504](https://github.com/penpot/penpot/pull/10504))
|
||||
- Fix artifacts in texts with inner strokes [#10476](https://github.com/penpot/penpot/issues/10476) (PR: [#10509](https://github.com/penpot/penpot/pull/10509))
|
||||
- Fix open overlay position control showing only center icon instead of full grid [#10176](https://github.com/penpot/penpot/issues/10176) (PR: [#10512](https://github.com/penpot/penpot/pull/10512))
|
||||
- Fix wrong text color in selection size badge [#10256](https://github.com/penpot/penpot/issues/10256) (PR: [#10393](https://github.com/penpot/penpot/pull/10393))
|
||||
- Fix several issues with plugins [#10394](https://github.com/penpot/penpot/issues/10394)
|
||||
- Fix pixel grid rendered on top of the rulers [#10426](https://github.com/penpot/penpot/issues/10426) (PR: [#10430](https://github.com/penpot/penpot/pull/10430))
|
||||
- Fix double-clicking text without fills creating a black file [#10472](https://github.com/penpot/penpot/issues/10472) (PR: [#10483](https://github.com/penpot/penpot/pull/10483))
|
||||
- Fix SVG raw caching prevented by unnecessary Cow wrapping [#10488](https://github.com/penpot/penpot/issues/10488) (PR: [#10492](https://github.com/penpot/penpot/pull/10492))
|
||||
- Add component reset operation to plugin API [#10561](https://github.com/penpot/penpot/issues/10561) (PR: [#10533](https://github.com/penpot/penpot/pull/10533))
|
||||
- Fix blur menu alignment in Firefox [#10576](https://github.com/penpot/penpot/issues/10576) (PR: [#10575](https://github.com/penpot/penpot/pull/10575))
|
||||
|
||||
## 2.16.2
|
||||
|
||||
|
||||
@ -1,30 +1,22 @@
|
||||
---
|
||||
title: 3.11. Agentic Development Environment
|
||||
title: 3.11. Agentic Dev Environment
|
||||
desc: Dive into agentic Penpot development.
|
||||
---
|
||||
|
||||
# Agentic Development Environment
|
||||
# Agentic Dev Environment
|
||||
|
||||
The agentic DevEnv is an extension of the standard DevEnv (the
|
||||
[general DevEnv instructions](/technical-guide/developer/devenv/) apply),
|
||||
optimised for AI agent-based development. It adds MCP servers (Penpot,
|
||||
Serena, Playwright) and supports a launcher that wires them into your AI client.
|
||||
|
||||
Two things to know up front:
|
||||
|
||||
- **Parallel workspaces are first-class.** Run several devenv instances side
|
||||
by side - one per AI agent if you like - each with its own source-tree
|
||||
clone, ports, and tmux session. Pass `--ws N` to target one.
|
||||
- **Your existing AI-client config is preserved.** The launcher loads a
|
||||
per-workspace MCP config on top of your global one.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Bring up one or more workspaces**[^cfg]:
|
||||
|
||||
```bash
|
||||
./manage.sh run-devenv-agentic # ws0 (the live repo)
|
||||
./manage.sh run-devenv-agentic --ws 1 # ws1 (sibling clone)
|
||||
./manage.sh run-devenv --agentic --attach # ws0 (the live repo)
|
||||
./manage.sh run-devenv --agentic --ws 1 # ws1 (sibling clone)
|
||||
```
|
||||
|
||||
Add `--ws 2`, `--ws 3`, … for more parallel workspaces.
|
||||
@ -47,20 +39,20 @@ Two things to know up front:
|
||||
"MCP Server" on. The agentic DevEnv runs the MCP server in single-user
|
||||
mode - the key and proxied URL shown in the UI are not needed.
|
||||
|
||||
4. **Launch your AI client** against the workspace you want it to drive:
|
||||
4. **Launch your AI client**. Either use your manually configured client
|
||||
(as described below) or conveniently launch an explicitly supported client
|
||||
against the workspace you want it to drive:
|
||||
|
||||
```bash
|
||||
./manage.sh start-coding-agent claude # ws0
|
||||
./manage.sh start-coding-agent claude # ws0 (main)
|
||||
./manage.sh start-coding-agent claude --ws 1 # ws1
|
||||
```
|
||||
|
||||
Supported clients: `claude` | `opencode` | `vscode` | `codex`.
|
||||
5. **Attach to the tmux session** for the workspace (optional):
|
||||
Note: The launcher loads a per-workspace MCP config on top of your global one (if any).
|
||||
|
||||
```bash
|
||||
./manage.sh attach-devenv # ws0
|
||||
./manage.sh attach-devenv --ws 1 # ws1
|
||||
```
|
||||
5. **Work within your client**, which is now equipped with extended capabilities
|
||||
for Penpot development (see below for details).
|
||||
|
||||
6. **Shut down workspaces** with `./manage.sh stop-devenv`, either one by one or all at once.
|
||||
You cannot shut down `ws0` if any other workspace is still running, since it's the worker-bearer.
|
||||
@ -113,10 +105,10 @@ var penpotFlags = "enable-mcp";
|
||||
```
|
||||
|
||||
The file is gitignored and lives in the live repo only. On every
|
||||
`run-devenv-agentic` call it is read directly for ws0; for wsN (N ≥ 1) it is
|
||||
`run-devenv --agentic` call it is read directly for ws0; for wsN (N ≥ 1) it is
|
||||
copied into the workspace clone on the **initial** sync only - subsequent
|
||||
`--sync` passes leave the workspace's copy alone so per-workspace
|
||||
customisations survive. `run-devenv-agentic` refuses to start if the file is
|
||||
customisations survive. `run-devenv --agentic` refuses to start if the file is
|
||||
missing.
|
||||
|
||||
**Browser remote debugging.** The Playwright MCP server drives a real
|
||||
@ -148,13 +140,13 @@ devenv image itself (add a tool, change a base layer):
|
||||
./manage.sh build-devenv --local
|
||||
```
|
||||
|
||||
The default `run-devenv-agentic` flow pulls the published image
|
||||
The default `run-devenv --agentic` flow pulls the published image
|
||||
automatically, so regular users never run this.
|
||||
|
||||
### Bringing up workspaces
|
||||
|
||||
```bash
|
||||
./manage.sh run-devenv-agentic \
|
||||
./manage.sh run-devenv --agentic \
|
||||
[--ws N] [--sync] [--serena-context CTX] \
|
||||
[--git-user-name NAME] [--git-user-email EMAIL]
|
||||
```
|
||||
@ -171,7 +163,7 @@ every bring-up so you don't compute offsets by hand. See the
|
||||
semantics, and stop ordering.
|
||||
|
||||
**Git identity for agent commits.** Coding agents typically need to commit
|
||||
inside the devenv, so `run-devenv-agentic` wires a Git identity into the
|
||||
inside the devenv, so `run-devenv --agentic` wires a Git identity into the
|
||||
container's global config on every bring-up. By default it propagates the
|
||||
host's effective `git config user.{name,email}` (local repo override wins
|
||||
over `~/.gitconfig`, matching what `git commit` on the host would record).
|
||||
@ -189,7 +181,7 @@ the full mechanics.
|
||||
>
|
||||
> ```bash
|
||||
> ./manage.sh stop-devenv
|
||||
> ./manage.sh run-devenv-agentic
|
||||
> ./manage.sh run-devenv --agentic
|
||||
> ```
|
||||
|
||||
### Launching an AI client
|
||||
@ -198,7 +190,7 @@ The agentic environment supports any AI client, one just needs to set the right
|
||||
see [manual configuration](#manual-ai-client-configuration) below. For some popular clients, the `manage.sh`
|
||||
CLI offers direct support through the following mechanism:
|
||||
|
||||
Every `run-devenv-agentic` regenerates three MCP-client config files with
|
||||
Every `run-devenv --agentic` regenerates three MCP-client config files with
|
||||
the workspace's ports baked in; Codex is wired up at launch instead (see
|
||||
below):
|
||||
|
||||
|
||||
@ -45,31 +45,38 @@ This is an incomplete list of devenv related subcommands found on
|
||||
manage.sh script:
|
||||
|
||||
```bash
|
||||
./manage.sh build-devenv --local # builds the local devenv docker image
|
||||
./manage.sh start-devenv # brings up the shared infra + ws0 in background
|
||||
./manage.sh run-devenv # ws0 with non-agentic tmux, attached (legacy alias)
|
||||
./manage.sh run-devenv-agentic # one agentic instance; --ws to target ws1+; see below
|
||||
./manage.sh attach-devenv # re-attaches to the tmux session of a running instance
|
||||
./manage.sh stop-devenv # stops one instance (or --all); infra stops with the last
|
||||
./manage.sh drop-devenv # removes containers (data volumes preserved)
|
||||
./manage.sh build-devenv --local # builds the local devenv docker image
|
||||
./manage.sh start-devenv # brings up the shared infra + ws0 in background
|
||||
./manage.sh run-devenv --attach # bring up main devenv instance and attach to its tmux session
|
||||
./manage.sh run-devenv --agentic --attach # bring up main devenv instance in agentic mode and attach tmux
|
||||
./manage.sh attach-devenv # re-attaches to the tmux session of a running instance
|
||||
./manage.sh stop-devenv # stops one instance (or --all); infra stops with the last
|
||||
./manage.sh drop-devenv # removes containers (data volumes preserved)
|
||||
```
|
||||
|
||||
### Agentic Mode
|
||||
|
||||
The `--agentic` flag enables additional features for AI-assisted development.
|
||||
See the dedicated section [Agentic Dev Environment](../agentic-devenv/) for details.
|
||||
|
||||
### Parallel workspaces
|
||||
|
||||
The devenv runs as separate compose projects: shared infra (`penpotdev-infra`:
|
||||
Postgres, MinIO, mailer, LDAP) plus one `penpotdev-wsN` project per runtime
|
||||
instance. `ws0` (a.k.a. `main`) binds the live repo; `ws1+` bind clones the
|
||||
developer maintains explicitly under `${PENPOT_WORKSPACES_DIR}/wsN/`
|
||||
(default `~/.penpot/penpot_workspaces/`).
|
||||
The devenv runs as separate compose projects:
|
||||
* shared infra (`penpotdev-infra`: Postgres, MinIO, mailer, LDAP)
|
||||
* `penpotdev-wsN` project per runtime instance.
|
||||
- `ws0` (a.k.a. `main`) is the current state of your repo;
|
||||
- `ws1` and up are clones that you maintain explicitly under `${PENPOT_WORKSPACES_DIR}/wsN/`
|
||||
(default `~/.penpot/penpot_workspaces/`). You can explicitly sync them
|
||||
with the `--sync` flag (automatic on first start).
|
||||
|
||||
Each call to `run-devenv-agentic` brings up one instance, and ws0 is always
|
||||
Each call to `run-devenv` brings up one instance, and ws0 is always
|
||||
running whenever any ws1+ is — `--ws N` (N≥1) auto-starts ws0 first if it
|
||||
isn't already up:
|
||||
|
||||
```bash
|
||||
./manage.sh run-devenv-agentic # main (ws0)
|
||||
./manage.sh run-devenv-agentic --ws 1 # ws0 if needed, then ws1
|
||||
./manage.sh run-devenv-agentic --ws 2 --sync # ws2, re-seeding from the live repo
|
||||
./manage.sh run-devenv # main (ws0)
|
||||
./manage.sh run-devenv --ws 1 # ws0 if needed, then ws1
|
||||
./manage.sh run-devenv --ws 2 --sync # ws2, re-seeding from the live repo
|
||||
```
|
||||
|
||||
Starting an instance that is already running is an error. `--sync` is only
|
||||
@ -102,12 +109,12 @@ Host ports are offset by `10000 × N`:
|
||||
| Serena MCP | `http://localhost:14181` | `http://localhost:24181` | `http://localhost:34181` |
|
||||
|
||||
Container-internal ports stay fixed. Target a specific instance with
|
||||
`--ws N` on `attach-devenv`, `run-devenv-agentic`, `stop-devenv`,
|
||||
`--ws N` on `attach-devenv`, `run-devenv`, `stop-devenv`,
|
||||
`start-coding-agent`, `run-devenv-shell`, and `isolated-shell`. `--ws`
|
||||
accepts a **non-negative integer only** — `--ws main` or `--ws ws1` is
|
||||
rejected, keeping the flag shape uniform across commands. `run-devenv` is
|
||||
ws0-only and takes no workspace flag. `run-devenv-agentic` also accepts
|
||||
`--serena-context CTX` and `--git-user-name NAME` / `--git-user-email
|
||||
ws0-only and takes no workspace flag. `run-devenv` also accepts
|
||||
`--serena-context CTX` (used together with `--agentic`) and `--git-user-name NAME` / `--git-user-email
|
||||
EMAIL` (see below).
|
||||
|
||||
Configuration lives in one tracked file, `docker/devenv/defaults.env` (the
|
||||
@ -116,7 +123,7 @@ derived and injected automatically, so there is no per-instance file to edit.
|
||||
|
||||
### Git identity inside the container
|
||||
|
||||
`run-devenv-agentic` wires a Git author identity into the container's
|
||||
`run-devenv` wires a Git author identity into the container's
|
||||
**global** git config (`git config --global user.{name,email}`) so commits
|
||||
made from inside the devenv carry a real author/committer. Without this,
|
||||
the container would commit as the unconfigured `penpot@<container>`
|
||||
@ -130,7 +137,7 @@ returns at the working directory `manage.sh` is invoked from — local
|
||||
`git commit` on the host would record. If neither is available the script
|
||||
prints a warning and continues — commits will fail inside the container
|
||||
until you set an identity. The values are applied every time
|
||||
`run-devenv-agentic` brings an instance up (idempotent), so re-running
|
||||
`run-devenv` brings an instance up (idempotent), so re-running
|
||||
with different flags is the way to change the in-container identity.
|
||||
|
||||
### Shared state and workers
|
||||
@ -185,11 +192,10 @@ docker rm penpotdev-postgres-1 penpotdev-minio-1 penpotdev-minio-setup-1 \
|
||||
docker network rm penpotdev_default 2>/dev/null
|
||||
|
||||
# Bring up infra + ws0 under the new project layout.
|
||||
./manage.sh run-devenv-agentic
|
||||
./manage.sh run-devenv
|
||||
```
|
||||
|
||||
After the cleanup, normal `./manage.sh start-devenv` / `run-devenv` /
|
||||
`run-devenv-agentic` commands work against the new layout. The legacy
|
||||
After the cleanup, normal `./manage.sh start-devenv` / `run-devenv` work against the new layout. The legacy
|
||||
`penpotdev` compose project is no longer used.
|
||||
|
||||
Having the container running and tmux opened inside the container,
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.variants :as dwva]
|
||||
[app.main.data.workspace.viewport :as dwv]
|
||||
[app.main.data.workspace.wasm-text :as dwwt]
|
||||
[app.main.data.workspace.zoom :as dwz]
|
||||
[app.main.errors]
|
||||
[app.main.features :as features]
|
||||
@ -441,6 +442,14 @@
|
||||
;; Keep comment thread positions in sync on undo/redo
|
||||
(rx/of (dwcm/watch-comment-thread-position-changes stoper-s))
|
||||
|
||||
;; Resize auto-grow text shapes whose selrect does not match
|
||||
;; the WASM text layout once their fonts finish loading.
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? :app.render-wasm.api/stale-text-selrects))
|
||||
(rx/map deref)
|
||||
(rx/map (fn [{:keys [ids]}]
|
||||
(dwwt/resize-wasm-text-all ids))))
|
||||
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
|
||||
@ -48,12 +48,14 @@
|
||||
(wasm.api/set-shape-text-images id content))
|
||||
(let [dimension (when (not= :fixed grow-type)
|
||||
(wasm.api/get-text-dimensions))]
|
||||
{:width (if (#{:fixed :auto-height} grow-type)
|
||||
(:width selrect)
|
||||
(:width dimension))
|
||||
:height (if (= :fixed grow-type)
|
||||
(:height selrect)
|
||||
(:height dimension))}))))
|
||||
;; nil dimension = shape not present in WASM state; skip the resize.
|
||||
(when (or (= :fixed grow-type) (some? dimension))
|
||||
{:width (if (#{:fixed :auto-height} grow-type)
|
||||
(:width selrect)
|
||||
(:width dimension))
|
||||
:height (if (= :fixed grow-type)
|
||||
(:height selrect)
|
||||
(:height dimension))})))))
|
||||
|
||||
(defn resize-wasm-text-modifiers
|
||||
([shape]
|
||||
|
||||
@ -575,15 +575,19 @@
|
||||
(wasm.api/push-ruler-theme-colors!)
|
||||
(wasm.api/request-render "rulers-colors-theme")))))
|
||||
|
||||
;; Ruler overlay updates below only change the UI surface, not the shapes.
|
||||
;; They use `render-from-cache!` (cached tiles + UI, atomic) instead of a full
|
||||
;; `request-render`, which would kick off a progressive tile-by-tile shape
|
||||
;; re-render that flashes on zoomed-in views (see penpot ruler-selection flash).
|
||||
(mf/with-effect [@canvas-init? frame-visible?]
|
||||
(when @canvas-init?
|
||||
(wasm.api/set-rulers-frame-visible! frame-visible?)
|
||||
(wasm.api/request-render "rulers-frame")))
|
||||
(wasm.api/render-from-cache!)))
|
||||
|
||||
(mf/with-effect [@canvas-init? show-rulers?]
|
||||
(when @canvas-init?
|
||||
(wasm.api/set-rulers-visible! show-rulers?)
|
||||
(wasm.api/request-render "rulers-visible")))
|
||||
(wasm.api/render-from-cache!)))
|
||||
|
||||
(mf/with-effect [@canvas-init? show-rulers? offset-x offset-y]
|
||||
(when (and @canvas-init? show-rulers?)
|
||||
@ -594,7 +598,7 @@
|
||||
(some-> ruler-selection :width) (some-> ruler-selection :height)]
|
||||
(when (and @canvas-init? show-rulers?)
|
||||
(wasm.api/set-rulers-selection! ruler-selection)
|
||||
(wasm.api/request-render "rulers-selection")))
|
||||
(wasm.api/render-from-cache!)))
|
||||
|
||||
;; Paint background + rulers instantly, before shapes finish loading. Runs
|
||||
;; after the ruler push effects so the WASM ruler state is already set.
|
||||
|
||||
@ -58,6 +58,7 @@
|
||||
[app.util.timers :as timers]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@ -459,6 +460,20 @@
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_render_ui_only")))
|
||||
|
||||
(defn render-from-cache!
|
||||
"Blit the shapes from the cached tile atlas and redraw the UI overlay
|
||||
(rulers, selection band) fresh on top, in a single atomic frame. The
|
||||
*shapes* are the cached part (already-rasterized tiles, not rebuilt); the UI
|
||||
is re-rendered every call, which is what lets it reflect a new selection.
|
||||
|
||||
Use for UI-only updates that don't change shapes (e.g. the ruler selection
|
||||
band): unlike `request-render`, it never kicks off a progressive,
|
||||
tile-by-tile shape re-render, so it does not flash on zoomed-in views where
|
||||
the scene spans multiple tiles."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_render_from_cache" 0)))
|
||||
|
||||
;; CSS-pixel blur radius for the page-transition snapshot (DPR-scaled in WASM).
|
||||
(def ^:private TRANSITION_BLUR_RADIUS 4.0)
|
||||
|
||||
@ -1282,17 +1297,19 @@
|
||||
([]
|
||||
(if-not (initialized?)
|
||||
{:x 0 :y 0 :width 0 :height 0 :max-width 0}
|
||||
(let [offset (-> (h/call wasm/internal-module "_get_text_dimensions")
|
||||
(mem/->offset-32))
|
||||
heapf32 (mem/get-heap-f32)
|
||||
width (aget heapf32 (+ offset 0))
|
||||
height (aget heapf32 (+ offset 1))
|
||||
max-width (aget heapf32 (+ offset 2))
|
||||
(let [ptr (h/call wasm/internal-module "_get_text_dimensions")]
|
||||
;; NULL pointer when there is no current shape or it is not a text.
|
||||
(when-not (zero? ptr)
|
||||
(let [offset (mem/->offset-32 ptr)
|
||||
heapf32 (mem/get-heap-f32)
|
||||
width (aget heapf32 (+ offset 0))
|
||||
height (aget heapf32 (+ offset 1))
|
||||
max-width (aget heapf32 (+ offset 2))
|
||||
|
||||
x (aget heapf32 (+ offset 3))
|
||||
y (aget heapf32 (+ offset 4))]
|
||||
(mem/free)
|
||||
{:x x :y y :width width :height height :max-width max-width}))))
|
||||
x (aget heapf32 (+ offset 3))
|
||||
y (aget heapf32 (+ offset 4))]
|
||||
(mem/free)
|
||||
{:x x :y y :width width :height height :max-width max-width}))))))
|
||||
|
||||
(defn intersect-position-in-shape
|
||||
[id position]
|
||||
@ -1452,19 +1469,45 @@
|
||||
[text-ids]
|
||||
(run! f/force-update-text-layout text-ids))
|
||||
|
||||
(defn- text-selrect-stale?
|
||||
"Check if the WASM-measured dimensions of an auto-grow text shape differ
|
||||
from its stored selrect (same 0.1px tolerance as the classic renderer)."
|
||||
[{:keys [id selrect grow-type]}]
|
||||
(when-let [{:keys [width height]} (get-text-dimensions id)]
|
||||
(case grow-type
|
||||
:auto-width (or (not (mth/close? width (:width selrect) 0.1))
|
||||
(not (mth/close? height (:height selrect) 0.1)))
|
||||
:auto-height (not (mth/close? height (:height selrect) 0.1))
|
||||
false)))
|
||||
|
||||
(defn- sync-stale-text-selrects!
|
||||
"Emit the ids of auto-grow text shapes whose selrect no longer matches the
|
||||
measured layout, so the workspace resizes them (data-event instead of a
|
||||
direct call to avoid a circular dependency; see the watcher in
|
||||
`app.main.data.workspace/initialize-workspace`)."
|
||||
[shapes]
|
||||
(let [stale-ids (into []
|
||||
(comp (filter cfh/text-shape?)
|
||||
(filter (comp #{:auto-width :auto-height} :grow-type))
|
||||
(filter text-selrect-stale?)
|
||||
(map :id))
|
||||
shapes)]
|
||||
(when (seq stale-ids)
|
||||
(st/emit! (ptk/data-event ::stale-text-selrects {:ids stale-ids})))))
|
||||
|
||||
(defn- relayout-after-fonts!
|
||||
"Relayout text shapes once their pending fonts have resolved. Shapes in
|
||||
`font-pending-ids` had a font fetched, so they get a forced relayout to pick
|
||||
up the real glyph metrics; the remaining text shapes get a normal layout."
|
||||
"Relayout text shapes once their pending fonts have resolved. Font fetches
|
||||
are deduped per URL and storing a font does not invalidate cached layouts,
|
||||
so every text shape (not only the fetch triggers in `font-pending-ids`)
|
||||
needs a forced relayout; then re-sync selrects that drifted."
|
||||
[shapes font-pending-ids]
|
||||
(let [force-ids (set font-pending-ids)
|
||||
text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)
|
||||
forced (filterv force-ids text-ids)
|
||||
rest-ids (filterv (complement force-ids) text-ids)]
|
||||
(when (seq forced)
|
||||
(force-update-text-layouts forced))
|
||||
(when (seq rest-ids)
|
||||
(update-text-layouts rest-ids))))
|
||||
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
|
||||
(when (seq text-ids)
|
||||
(if (seq font-pending-ids)
|
||||
(do
|
||||
(force-update-text-layouts text-ids)
|
||||
(sync-stale-text-selrects! shapes))
|
||||
(update-text-layouts text-ids)))))
|
||||
|
||||
(defn process-pending
|
||||
[shapes thumbnails full font-pending-ids on-complete]
|
||||
|
||||
@ -1,21 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Entry point (bin) of the published @penpot/mcp package.
|
||||
// This script is not intended to be run from a source checkout; there, use
|
||||
// `pnpm run bootstrap` directly, as described in the README.
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const pkg = require(path.join(root, "package.json"));
|
||||
const packageRoot = path.resolve(__dirname, "..");
|
||||
const pkg = require(path.join(packageRoot, "package.json"));
|
||||
|
||||
function pnpmVersion() {
|
||||
const match = (pkg.packageManager || "").match(/^pnpm@([^+]+)/);
|
||||
return match ? match[1] : "latest";
|
||||
}
|
||||
|
||||
function run(command) {
|
||||
execSync(command, { cwd: root, stdio: "inherit" });
|
||||
/**
|
||||
* Prepares the directory in which to bootstrap and run.
|
||||
*
|
||||
* The package contents are copied once per version to a runtime directory
|
||||
* owned by this launcher. The npm-owned package directory cannot be used;
|
||||
* we have to assume it is read-only (#9947).
|
||||
*
|
||||
* @returns {string} the directory to run the bootstrap in
|
||||
*/
|
||||
function prepareRuntimeRoot() {
|
||||
const cacheBase = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
|
||||
const runtimeRoot = path.join(cacheBase, "penpot-mcp", pkg.version);
|
||||
if (fs.existsSync(runtimeRoot)) {
|
||||
return runtimeRoot;
|
||||
}
|
||||
|
||||
// copy to a temporary sibling first and rename, so an interrupted copy
|
||||
// cannot leave a partial runtime directory behind
|
||||
console.log(`Preparing runtime directory ${runtimeRoot} ...`);
|
||||
const tempRoot = `${runtimeRoot}.tmp-${process.pid}`;
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
fs.cpSync(packageRoot, tempRoot, {
|
||||
recursive: true,
|
||||
filter: (src) => path.basename(src) !== "node_modules",
|
||||
});
|
||||
try {
|
||||
fs.renameSync(tempRoot, runtimeRoot);
|
||||
} catch (error) {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
if (!fs.existsSync(runtimeRoot)) {
|
||||
throw error;
|
||||
}
|
||||
// a concurrent launch created the runtime directory in the meantime; use it
|
||||
}
|
||||
return runtimeRoot;
|
||||
}
|
||||
|
||||
const root = prepareRuntimeRoot();
|
||||
|
||||
// pnpm-lock.yaml is hard-excluded by npm pack; it is shipped as pnpm-lock.dist.yaml
|
||||
// and restored here before bootstrap runs.
|
||||
const distLock = path.join(root, "pnpm-lock.dist.yaml");
|
||||
@ -25,7 +65,7 @@ if (fs.existsSync(distLock)) {
|
||||
}
|
||||
|
||||
try {
|
||||
run(`npx -y pnpm@${pnpmVersion()} run bootstrap`);
|
||||
execSync(`npx -y pnpm@${pnpmVersion()} run bootstrap`, { cwd: root, stdio: "inherit" });
|
||||
} catch (error) {
|
||||
process.exit(error.status ?? 1);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@penpot/mcp",
|
||||
"version": "2.16.0",
|
||||
"version": "2.17.0",
|
||||
"description": "MCP server for Penpot integration",
|
||||
"license": "MPL-2.0",
|
||||
"bin": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@penpot/plugin-types",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"typings": "./index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@penpot/plugins-runtime",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"dependencies": {
|
||||
"@penpot/plugin-types": "workspace:^",
|
||||
"ses": "^2.1.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@penpot/plugin-styles",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"dependencies": {},
|
||||
"scripts": {
|
||||
"build": "node ../../tools/scripts/build-css.mjs",
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import esbuild from 'esbuild';
|
||||
import { copy } from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const source = 'libs/plugins-styles';
|
||||
const dist = 'dist/plugins-styles';
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const source = resolve(root, 'libs/plugins-styles');
|
||||
const dist = resolve(root, 'dist/plugins-styles');
|
||||
|
||||
const handleErr = (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
Promise.all([
|
||||
esbuild.build({
|
||||
entryPoints: [`${source}/src/lib/styles.css`],
|
||||
bundle: true,
|
||||
outfile: `${dist}/styles.css`,
|
||||
@ -18,9 +21,8 @@ esbuild
|
||||
loader: {
|
||||
'.svg': 'dataurl',
|
||||
},
|
||||
})
|
||||
.catch(handleErr);
|
||||
|
||||
copy(`${source}/package.json`, `${dist}/package.json`).catch(handleErr);
|
||||
copy(`${source}/README.md`, `${dist}/README.md`).catch(handleErr);
|
||||
copy(`LICENSE`, `${dist}/LICENSE`).catch(handleErr);
|
||||
}),
|
||||
copy(`${source}/package.json`, `${dist}/package.json`),
|
||||
copy(`${source}/README.md`, `${dist}/README.md`),
|
||||
copy(resolve(root, 'LICENSE'), `${dist}/LICENSE`),
|
||||
]).catch(handleErr);
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import { copy } from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const source = 'libs/plugin-types';
|
||||
const dist = 'dist/plugin-types';
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const source = resolve(root, 'libs/plugin-types');
|
||||
const dist = resolve(root, 'dist/plugin-types');
|
||||
|
||||
const handleErr = (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
copy(`${source}/package.json`, `${dist}/package.json`).catch(handleErr);
|
||||
copy(`${source}/README.md`, `${dist}/README.md`).catch(handleErr);
|
||||
copy(`${source}/index.d.ts`, `${dist}/index.d.ts`).catch(handleErr);
|
||||
copy(`LICENSE`, `${dist}/LICENSE`).catch(handleErr);
|
||||
Promise.all([
|
||||
copy(`${source}/package.json`, `${dist}/package.json`),
|
||||
copy(`${source}/README.md`, `${dist}/README.md`),
|
||||
copy(`${source}/index.d.ts`, `${dist}/index.d.ts`),
|
||||
copy(resolve(root, 'LICENSE'), `${dist}/LICENSE`),
|
||||
]).catch(handleErr);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { cpSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
@ -66,6 +66,21 @@ const writePackageJson = (packagePath: string, content: PackageJson): void => {
|
||||
writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n');
|
||||
};
|
||||
|
||||
const getPublishPath = (packagePath: string): string => {
|
||||
return join('dist', packagePath.split('/').pop()!);
|
||||
};
|
||||
|
||||
const prepareRuntimePackage = (): void => {
|
||||
const source = 'libs/plugins-runtime';
|
||||
const dist = getPublishPath(source);
|
||||
|
||||
mkdirSync(dist, { recursive: true });
|
||||
cpSync(join(source, 'package.json'), join(dist, 'package.json'));
|
||||
cpSync(join(source, 'README.md'), join(dist, 'README.md'));
|
||||
cpSync('LICENSE', join(dist, 'LICENSE'));
|
||||
cpSync(join(source, 'dist'), join(dist, 'dist'), { recursive: true });
|
||||
};
|
||||
|
||||
const incrementVersion = (
|
||||
currentVersion: string,
|
||||
specifier: string,
|
||||
@ -164,6 +179,7 @@ const log = (message: string, verbose: boolean, forceLog = false): void => {
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
prepareRuntimePackage();
|
||||
} else {
|
||||
console.log(' [DRY RUN] Skipping build\n');
|
||||
}
|
||||
@ -176,7 +192,7 @@ const log = (message: string, verbose: boolean, forceLog = false): void => {
|
||||
|
||||
for (const packagePath of PACKAGES) {
|
||||
const pkg = readPackageJson(packagePath);
|
||||
const distPath = join('dist', packagePath.split('/').pop()!);
|
||||
const distPath = getPublishPath(packagePath);
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(
|
||||
@ -188,7 +204,7 @@ const log = (message: string, verbose: boolean, forceLog = false): void => {
|
||||
`pnpm publish --tag ${tag} --access public --no-git-checks`,
|
||||
{
|
||||
cwd: join(process.cwd(), distPath),
|
||||
stdio: args.verbose ? 'inherit' : 'pipe',
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
console.log(` ✅ Published ${pkg.name}@${newVersion}`);
|
||||
|
||||
34
tools/db-schema
Executable file
34
tools/db-schema
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${PENPOT_DB_HOST:-postgres}"
|
||||
USER="${PENPOT_DB_USER:-penpot}"
|
||||
PASSWORD="${PENPOT_DB_PASSWORD:-penpot}"
|
||||
DB="${PENPOT_DB_NAME:-penpot}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--test)
|
||||
DB="penpot_test"
|
||||
shift
|
||||
;;
|
||||
--host|-h)
|
||||
HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--user|-U)
|
||||
USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--db|-d)
|
||||
DB="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PGPASSWORD="$PASSWORD"
|
||||
exec pg_dump -h "$HOST" -U "$USER" -d "$DB" --schema-only --no-owner --no-privileges "$@"
|
||||
34
tools/psql
Executable file
34
tools/psql
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${PENPOT_DB_HOST:-postgres}"
|
||||
USER="${PENPOT_DB_USER:-penpot}"
|
||||
PASSWORD="${PENPOT_DB_PASSWORD:-penpot}"
|
||||
DB="${PENPOT_DB_NAME:-penpot}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--test)
|
||||
DB="penpot_test"
|
||||
shift
|
||||
;;
|
||||
--host|-h)
|
||||
HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--user|-U)
|
||||
USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--db|-d)
|
||||
DB="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PGPASSWORD="$PASSWORD"
|
||||
exec psql -h "$HOST" -U "$USER" -d "$DB" "$@"
|
||||
Loading…
x
Reference in New Issue
Block a user