mirror of
https://github.com/penpot/penpot.git
synced 2026-09-07 12:40: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`.
|
`manage.sh`.
|
||||||
* **`mcp/`** (Claude Code, opencode) is the result of merging `shared/` with
|
* **`mcp/`** (Claude Code, opencode) is the result of merging `shared/` with
|
||||||
the port-substituted `templates/`. `manage.sh` writes these on every
|
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
|
content — never edit by hand, your edits will be overwritten on the next
|
||||||
reconcile.
|
reconcile.
|
||||||
* **`.vscode/mcp.json`** is the same merge, but written to the path VS Code
|
* **`.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.
|
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`.
|
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
|
## Background tasks
|
||||||
|
|||||||
@ -9,7 +9,11 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
|
|||||||
|
|
||||||
# Development workflow
|
# 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.
|
- 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.
|
- 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.
|
- 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
|
tmux session lifecycle, MinIO provisioning, or anything in `manage.sh`'s
|
||||||
`*-devenv` commands, read `mem:devenv/core`.
|
`*-devenv` commands, read `mem:devenv/core`.
|
||||||
- `tools/` contains standalone dev utilities: `nrepl-eval.mjs` (backend REPL eval),
|
- `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).
|
`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.
|
- `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.
|
- `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
|
## 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
|
## Port layout
|
||||||
|
|
||||||
@ -44,13 +44,13 @@ Everything else (frontend dev, backend API, exporter, storybook, REPLs, plugin d
|
|||||||
|
|
||||||
## Tmux + MCP routing
|
## 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`.
|
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 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`:
|
`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.
|
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
|
## 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.
|
- `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.
|
- `run-devenv`: legacy alias, ws0 non-agentic attached.
|
||||||
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
|
- `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>
|
<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
|
## Creating the Issue
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -155,6 +157,199 @@ query { repository(owner: "penpot", name: "penpot") {
|
|||||||
rm -f /tmp/issue-body.md
|
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
|
## 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
|
# 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
|
## CRITICAL: Read module memories BEFORE writing any code
|
||||||
|
|
||||||
Do this **before planning, before coding, before touching any file**:
|
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 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))
|
- 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))
|
- 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
|
### :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 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 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 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
|
## 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.
|
desc: Dive into agentic Penpot development.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Agentic Development Environment
|
# Agentic Dev Environment
|
||||||
|
|
||||||
The agentic DevEnv is an extension of the standard DevEnv (the
|
The agentic DevEnv is an extension of the standard DevEnv (the
|
||||||
[general DevEnv instructions](/technical-guide/developer/devenv/) apply),
|
[general DevEnv instructions](/technical-guide/developer/devenv/) apply),
|
||||||
optimised for AI agent-based development. It adds MCP servers (Penpot,
|
optimised for AI agent-based development. It adds MCP servers (Penpot,
|
||||||
Serena, Playwright) and supports a launcher that wires them into your AI client.
|
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
|
## Quick Start
|
||||||
|
|
||||||
1. **Bring up one or more workspaces**[^cfg]:
|
1. **Bring up one or more workspaces**[^cfg]:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./manage.sh run-devenv-agentic # ws0 (the live repo)
|
./manage.sh run-devenv --agentic --attach # ws0 (the live repo)
|
||||||
./manage.sh run-devenv-agentic --ws 1 # ws1 (sibling clone)
|
./manage.sh run-devenv --agentic --ws 1 # ws1 (sibling clone)
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `--ws 2`, `--ws 3`, … for more parallel workspaces.
|
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
|
"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.
|
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
|
```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
|
./manage.sh start-coding-agent claude --ws 1 # ws1
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported clients: `claude` | `opencode` | `vscode` | `codex`.
|
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
|
5. **Work within your client**, which is now equipped with extended capabilities
|
||||||
./manage.sh attach-devenv # ws0
|
for Penpot development (see below for details).
|
||||||
./manage.sh attach-devenv --ws 1 # ws1
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Shut down workspaces** with `./manage.sh stop-devenv`, either one by one or all at once.
|
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.
|
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
|
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
|
copied into the workspace clone on the **initial** sync only - subsequent
|
||||||
`--sync` passes leave the workspace's copy alone so per-workspace
|
`--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.
|
missing.
|
||||||
|
|
||||||
**Browser remote debugging.** The Playwright MCP server drives a real
|
**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
|
./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.
|
automatically, so regular users never run this.
|
||||||
|
|
||||||
### Bringing up workspaces
|
### Bringing up workspaces
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./manage.sh run-devenv-agentic \
|
./manage.sh run-devenv --agentic \
|
||||||
[--ws N] [--sync] [--serena-context CTX] \
|
[--ws N] [--sync] [--serena-context CTX] \
|
||||||
[--git-user-name NAME] [--git-user-email EMAIL]
|
[--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.
|
semantics, and stop ordering.
|
||||||
|
|
||||||
**Git identity for agent commits.** Coding agents typically need to commit
|
**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
|
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
|
host's effective `git config user.{name,email}` (local repo override wins
|
||||||
over `~/.gitconfig`, matching what `git commit` on the host would record).
|
over `~/.gitconfig`, matching what `git commit` on the host would record).
|
||||||
@ -189,7 +181,7 @@ the full mechanics.
|
|||||||
>
|
>
|
||||||
> ```bash
|
> ```bash
|
||||||
> ./manage.sh stop-devenv
|
> ./manage.sh stop-devenv
|
||||||
> ./manage.sh run-devenv-agentic
|
> ./manage.sh run-devenv --agentic
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
### Launching an AI client
|
### 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`
|
see [manual configuration](#manual-ai-client-configuration) below. For some popular clients, the `manage.sh`
|
||||||
CLI offers direct support through the following mechanism:
|
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
|
the workspace's ports baked in; Codex is wired up at launch instead (see
|
||||||
below):
|
below):
|
||||||
|
|
||||||
|
|||||||
@ -45,31 +45,38 @@ This is an incomplete list of devenv related subcommands found on
|
|||||||
manage.sh script:
|
manage.sh script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./manage.sh build-devenv --local # builds the local devenv docker image
|
./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 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 --attach # bring up main devenv instance and attach to its tmux session
|
||||||
./manage.sh run-devenv-agentic # one agentic instance; --ws to target ws1+; see below
|
./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 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 stop-devenv # stops one instance (or --all); infra stops with the last
|
||||||
./manage.sh drop-devenv # removes containers (data volumes preserved)
|
./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
|
### Parallel workspaces
|
||||||
|
|
||||||
The devenv runs as separate compose projects: shared infra (`penpotdev-infra`:
|
The devenv runs as separate compose projects:
|
||||||
Postgres, MinIO, mailer, LDAP) plus one `penpotdev-wsN` project per runtime
|
* shared infra (`penpotdev-infra`: Postgres, MinIO, mailer, LDAP)
|
||||||
instance. `ws0` (a.k.a. `main`) binds the live repo; `ws1+` bind clones the
|
* `penpotdev-wsN` project per runtime instance.
|
||||||
developer maintains explicitly under `${PENPOT_WORKSPACES_DIR}/wsN/`
|
- `ws0` (a.k.a. `main`) is the current state of your repo;
|
||||||
(default `~/.penpot/penpot_workspaces/`).
|
- `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
|
running whenever any ws1+ is — `--ws N` (N≥1) auto-starts ws0 first if it
|
||||||
isn't already up:
|
isn't already up:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./manage.sh run-devenv-agentic # main (ws0)
|
./manage.sh run-devenv # main (ws0)
|
||||||
./manage.sh run-devenv-agentic --ws 1 # ws0 if needed, then ws1
|
./manage.sh run-devenv --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 --ws 2 --sync # ws2, re-seeding from the live repo
|
||||||
```
|
```
|
||||||
|
|
||||||
Starting an instance that is already running is an error. `--sync` is only
|
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` |
|
| Serena MCP | `http://localhost:14181` | `http://localhost:24181` | `http://localhost:34181` |
|
||||||
|
|
||||||
Container-internal ports stay fixed. Target a specific instance with
|
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`
|
`start-coding-agent`, `run-devenv-shell`, and `isolated-shell`. `--ws`
|
||||||
accepts a **non-negative integer only** — `--ws main` or `--ws ws1` is
|
accepts a **non-negative integer only** — `--ws main` or `--ws ws1` is
|
||||||
rejected, keeping the flag shape uniform across commands. `run-devenv` is
|
rejected, keeping the flag shape uniform across commands. `run-devenv` is
|
||||||
ws0-only and takes no workspace flag. `run-devenv-agentic` also accepts
|
ws0-only and takes no workspace flag. `run-devenv` also accepts
|
||||||
`--serena-context CTX` and `--git-user-name NAME` / `--git-user-email
|
`--serena-context CTX` (used together with `--agentic`) and `--git-user-name NAME` / `--git-user-email
|
||||||
EMAIL` (see below).
|
EMAIL` (see below).
|
||||||
|
|
||||||
Configuration lives in one tracked file, `docker/devenv/defaults.env` (the
|
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
|
### 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
|
**global** git config (`git config --global user.{name,email}`) so commits
|
||||||
made from inside the devenv carry a real author/committer. Without this,
|
made from inside the devenv carry a real author/committer. Without this,
|
||||||
the container would commit as the unconfigured `penpot@<container>`
|
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
|
`git commit` on the host would record. If neither is available the script
|
||||||
prints a warning and continues — commits will fail inside the container
|
prints a warning and continues — commits will fail inside the container
|
||||||
until you set an identity. The values are applied every time
|
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.
|
with different flags is the way to change the in-container identity.
|
||||||
|
|
||||||
### Shared state and workers
|
### 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
|
docker network rm penpotdev_default 2>/dev/null
|
||||||
|
|
||||||
# Bring up infra + ws0 under the new project layout.
|
# 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` /
|
After the cleanup, normal `./manage.sh start-devenv` / `run-devenv` work against the new layout. The legacy
|
||||||
`run-devenv-agentic` commands work against the new layout. The legacy
|
|
||||||
`penpotdev` compose project is no longer used.
|
`penpotdev` compose project is no longer used.
|
||||||
|
|
||||||
Having the container running and tmux opened inside the container,
|
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.undo :as dwu]
|
||||||
[app.main.data.workspace.variants :as dwva]
|
[app.main.data.workspace.variants :as dwva]
|
||||||
[app.main.data.workspace.viewport :as dwv]
|
[app.main.data.workspace.viewport :as dwv]
|
||||||
|
[app.main.data.workspace.wasm-text :as dwwt]
|
||||||
[app.main.data.workspace.zoom :as dwz]
|
[app.main.data.workspace.zoom :as dwz]
|
||||||
[app.main.errors]
|
[app.main.errors]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
@ -441,6 +442,14 @@
|
|||||||
;; Keep comment thread positions in sync on undo/redo
|
;; Keep comment thread positions in sync on undo/redo
|
||||||
(rx/of (dwcm/watch-comment-thread-position-changes stoper-s))
|
(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
|
(let [local-commits-s
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter dch/commit?)
|
(rx/filter dch/commit?)
|
||||||
|
|||||||
@ -48,12 +48,14 @@
|
|||||||
(wasm.api/set-shape-text-images id content))
|
(wasm.api/set-shape-text-images id content))
|
||||||
(let [dimension (when (not= :fixed grow-type)
|
(let [dimension (when (not= :fixed grow-type)
|
||||||
(wasm.api/get-text-dimensions))]
|
(wasm.api/get-text-dimensions))]
|
||||||
{:width (if (#{:fixed :auto-height} grow-type)
|
;; nil dimension = shape not present in WASM state; skip the resize.
|
||||||
(:width selrect)
|
(when (or (= :fixed grow-type) (some? dimension))
|
||||||
(:width dimension))
|
{:width (if (#{:fixed :auto-height} grow-type)
|
||||||
:height (if (= :fixed grow-type)
|
(:width selrect)
|
||||||
(:height selrect)
|
(:width dimension))
|
||||||
(:height dimension))}))))
|
:height (if (= :fixed grow-type)
|
||||||
|
(:height selrect)
|
||||||
|
(:height dimension))})))))
|
||||||
|
|
||||||
(defn resize-wasm-text-modifiers
|
(defn resize-wasm-text-modifiers
|
||||||
([shape]
|
([shape]
|
||||||
|
|||||||
@ -575,15 +575,19 @@
|
|||||||
(wasm.api/push-ruler-theme-colors!)
|
(wasm.api/push-ruler-theme-colors!)
|
||||||
(wasm.api/request-render "rulers-colors-theme")))))
|
(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?]
|
(mf/with-effect [@canvas-init? frame-visible?]
|
||||||
(when @canvas-init?
|
(when @canvas-init?
|
||||||
(wasm.api/set-rulers-frame-visible! frame-visible?)
|
(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?]
|
(mf/with-effect [@canvas-init? show-rulers?]
|
||||||
(when @canvas-init?
|
(when @canvas-init?
|
||||||
(wasm.api/set-rulers-visible! show-rulers?)
|
(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]
|
(mf/with-effect [@canvas-init? show-rulers? offset-x offset-y]
|
||||||
(when (and @canvas-init? show-rulers?)
|
(when (and @canvas-init? show-rulers?)
|
||||||
@ -594,7 +598,7 @@
|
|||||||
(some-> ruler-selection :width) (some-> ruler-selection :height)]
|
(some-> ruler-selection :width) (some-> ruler-selection :height)]
|
||||||
(when (and @canvas-init? show-rulers?)
|
(when (and @canvas-init? show-rulers?)
|
||||||
(wasm.api/set-rulers-selection! ruler-selection)
|
(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
|
;; Paint background + rulers instantly, before shapes finish loading. Runs
|
||||||
;; after the ruler push effects so the WASM ruler state is already set.
|
;; after the ruler push effects so the WASM ruler state is already set.
|
||||||
|
|||||||
@ -58,6 +58,7 @@
|
|||||||
[app.util.timers :as timers]
|
[app.util.timers :as timers]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
[potok.v2.core :as ptk]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
@ -459,6 +460,20 @@
|
|||||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||||
(h/call wasm/internal-module "_render_ui_only")))
|
(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).
|
;; CSS-pixel blur radius for the page-transition snapshot (DPR-scaled in WASM).
|
||||||
(def ^:private TRANSITION_BLUR_RADIUS 4.0)
|
(def ^:private TRANSITION_BLUR_RADIUS 4.0)
|
||||||
|
|
||||||
@ -1282,17 +1297,19 @@
|
|||||||
([]
|
([]
|
||||||
(if-not (initialized?)
|
(if-not (initialized?)
|
||||||
{:x 0 :y 0 :width 0 :height 0 :max-width 0}
|
{:x 0 :y 0 :width 0 :height 0 :max-width 0}
|
||||||
(let [offset (-> (h/call wasm/internal-module "_get_text_dimensions")
|
(let [ptr (h/call wasm/internal-module "_get_text_dimensions")]
|
||||||
(mem/->offset-32))
|
;; NULL pointer when there is no current shape or it is not a text.
|
||||||
heapf32 (mem/get-heap-f32)
|
(when-not (zero? ptr)
|
||||||
width (aget heapf32 (+ offset 0))
|
(let [offset (mem/->offset-32 ptr)
|
||||||
height (aget heapf32 (+ offset 1))
|
heapf32 (mem/get-heap-f32)
|
||||||
max-width (aget heapf32 (+ offset 2))
|
width (aget heapf32 (+ offset 0))
|
||||||
|
height (aget heapf32 (+ offset 1))
|
||||||
|
max-width (aget heapf32 (+ offset 2))
|
||||||
|
|
||||||
x (aget heapf32 (+ offset 3))
|
x (aget heapf32 (+ offset 3))
|
||||||
y (aget heapf32 (+ offset 4))]
|
y (aget heapf32 (+ offset 4))]
|
||||||
(mem/free)
|
(mem/free)
|
||||||
{:x x :y y :width width :height height :max-width max-width}))))
|
{:x x :y y :width width :height height :max-width max-width}))))))
|
||||||
|
|
||||||
(defn intersect-position-in-shape
|
(defn intersect-position-in-shape
|
||||||
[id position]
|
[id position]
|
||||||
@ -1452,19 +1469,45 @@
|
|||||||
[text-ids]
|
[text-ids]
|
||||||
(run! f/force-update-text-layout 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!
|
(defn- relayout-after-fonts!
|
||||||
"Relayout text shapes once their pending fonts have resolved. Shapes in
|
"Relayout text shapes once their pending fonts have resolved. Font fetches
|
||||||
`font-pending-ids` had a font fetched, so they get a forced relayout to pick
|
are deduped per URL and storing a font does not invalidate cached layouts,
|
||||||
up the real glyph metrics; the remaining text shapes get a normal layout."
|
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]
|
[shapes font-pending-ids]
|
||||||
(let [force-ids (set font-pending-ids)
|
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
|
||||||
text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)
|
(when (seq text-ids)
|
||||||
forced (filterv force-ids text-ids)
|
(if (seq font-pending-ids)
|
||||||
rest-ids (filterv (complement force-ids) text-ids)]
|
(do
|
||||||
(when (seq forced)
|
(force-update-text-layouts text-ids)
|
||||||
(force-update-text-layouts forced))
|
(sync-stale-text-selrects! shapes))
|
||||||
(when (seq rest-ids)
|
(update-text-layouts text-ids)))))
|
||||||
(update-text-layouts rest-ids))))
|
|
||||||
|
|
||||||
(defn process-pending
|
(defn process-pending
|
||||||
[shapes thumbnails full font-pending-ids on-complete]
|
[shapes thumbnails full font-pending-ids on-complete]
|
||||||
|
|||||||
@ -1,21 +1,61 @@
|
|||||||
#!/usr/bin/env node
|
#!/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 { execSync } = require("child_process");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const root = path.resolve(__dirname, "..");
|
const packageRoot = path.resolve(__dirname, "..");
|
||||||
const pkg = require(path.join(root, "package.json"));
|
const pkg = require(path.join(packageRoot, "package.json"));
|
||||||
|
|
||||||
function pnpmVersion() {
|
function pnpmVersion() {
|
||||||
const match = (pkg.packageManager || "").match(/^pnpm@([^+]+)/);
|
const match = (pkg.packageManager || "").match(/^pnpm@([^+]+)/);
|
||||||
return match ? match[1] : "latest";
|
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
|
// pnpm-lock.yaml is hard-excluded by npm pack; it is shipped as pnpm-lock.dist.yaml
|
||||||
// and restored here before bootstrap runs.
|
// and restored here before bootstrap runs.
|
||||||
const distLock = path.join(root, "pnpm-lock.dist.yaml");
|
const distLock = path.join(root, "pnpm-lock.dist.yaml");
|
||||||
@ -25,7 +65,7 @@ if (fs.existsSync(distLock)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
run(`npx -y pnpm@${pnpmVersion()} run bootstrap`);
|
execSync(`npx -y pnpm@${pnpmVersion()} run bootstrap`, { cwd: root, stdio: "inherit" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
process.exit(error.status ?? 1);
|
process.exit(error.status ?? 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@penpot/mcp",
|
"name": "@penpot/mcp",
|
||||||
"version": "2.16.0",
|
"version": "2.17.0",
|
||||||
"description": "MCP server for Penpot integration",
|
"description": "MCP server for Penpot integration",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@penpot/plugin-types",
|
"name": "@penpot/plugin-types",
|
||||||
"version": "1.4.2",
|
"version": "1.5.0",
|
||||||
"typings": "./index.d.ts",
|
"typings": "./index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@penpot/plugins-runtime",
|
"name": "@penpot/plugins-runtime",
|
||||||
"version": "1.4.2",
|
"version": "1.5.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@penpot/plugin-types": "workspace:^",
|
"@penpot/plugin-types": "workspace:^",
|
||||||
"ses": "^2.1.0",
|
"ses": "^2.1.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@penpot/plugin-styles",
|
"name": "@penpot/plugin-styles",
|
||||||
"version": "1.4.2",
|
"version": "1.5.0",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node ../../tools/scripts/build-css.mjs",
|
"build": "node ../../tools/scripts/build-css.mjs",
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import { copy } from 'fs-extra';
|
import { copy } from 'fs-extra';
|
||||||
|
import { dirname, resolve } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const source = 'libs/plugins-styles';
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||||
const dist = 'dist/plugins-styles';
|
const source = resolve(root, 'libs/plugins-styles');
|
||||||
|
const dist = resolve(root, 'dist/plugins-styles');
|
||||||
|
|
||||||
const handleErr = (err) => {
|
const handleErr = (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
esbuild
|
Promise.all([
|
||||||
.build({
|
esbuild.build({
|
||||||
entryPoints: [`${source}/src/lib/styles.css`],
|
entryPoints: [`${source}/src/lib/styles.css`],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
outfile: `${dist}/styles.css`,
|
outfile: `${dist}/styles.css`,
|
||||||
@ -18,9 +21,8 @@ esbuild
|
|||||||
loader: {
|
loader: {
|
||||||
'.svg': 'dataurl',
|
'.svg': 'dataurl',
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
.catch(handleErr);
|
copy(`${source}/package.json`, `${dist}/package.json`),
|
||||||
|
copy(`${source}/README.md`, `${dist}/README.md`),
|
||||||
copy(`${source}/package.json`, `${dist}/package.json`).catch(handleErr);
|
copy(resolve(root, 'LICENSE'), `${dist}/LICENSE`),
|
||||||
copy(`${source}/README.md`, `${dist}/README.md`).catch(handleErr);
|
]).catch(handleErr);
|
||||||
copy(`LICENSE`, `${dist}/LICENSE`).catch(handleErr);
|
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
import { copy } from 'fs-extra';
|
import { copy } from 'fs-extra';
|
||||||
|
import { dirname, resolve } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const source = 'libs/plugin-types';
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||||
const dist = 'dist/plugin-types';
|
const source = resolve(root, 'libs/plugin-types');
|
||||||
|
const dist = resolve(root, 'dist/plugin-types');
|
||||||
|
|
||||||
const handleErr = (err) => {
|
const handleErr = (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
copy(`${source}/package.json`, `${dist}/package.json`).catch(handleErr);
|
Promise.all([
|
||||||
copy(`${source}/README.md`, `${dist}/README.md`).catch(handleErr);
|
copy(`${source}/package.json`, `${dist}/package.json`),
|
||||||
copy(`${source}/index.d.ts`, `${dist}/index.d.ts`).catch(handleErr);
|
copy(`${source}/README.md`, `${dist}/README.md`),
|
||||||
copy(`LICENSE`, `${dist}/LICENSE`).catch(handleErr);
|
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 { execSync } from 'child_process';
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
import { cpSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from 'yargs/helpers';
|
||||||
@ -66,6 +66,21 @@ const writePackageJson = (packagePath: string, content: PackageJson): void => {
|
|||||||
writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n');
|
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 = (
|
const incrementVersion = (
|
||||||
currentVersion: string,
|
currentVersion: string,
|
||||||
specifier: string,
|
specifier: string,
|
||||||
@ -164,6 +179,7 @@ const log = (message: string, verbose: boolean, forceLog = false): void => {
|
|||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
prepareRuntimePackage();
|
||||||
} else {
|
} else {
|
||||||
console.log(' [DRY RUN] Skipping build\n');
|
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) {
|
for (const packagePath of PACKAGES) {
|
||||||
const pkg = readPackageJson(packagePath);
|
const pkg = readPackageJson(packagePath);
|
||||||
const distPath = join('dist', packagePath.split('/').pop()!);
|
const distPath = getPublishPath(packagePath);
|
||||||
|
|
||||||
if (args.dryRun) {
|
if (args.dryRun) {
|
||||||
console.log(
|
console.log(
|
||||||
@ -188,7 +204,7 @@ const log = (message: string, verbose: boolean, forceLog = false): void => {
|
|||||||
`pnpm publish --tag ${tag} --access public --no-git-checks`,
|
`pnpm publish --tag ${tag} --access public --no-git-checks`,
|
||||||
{
|
{
|
||||||
cwd: join(process.cwd(), distPath),
|
cwd: join(process.cwd(), distPath),
|
||||||
stdio: args.verbose ? 'inherit' : 'pipe',
|
stdio: 'inherit',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
console.log(` ✅ Published ${pkg.name}@${newVersion}`);
|
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