@penpair/web 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Niklas Westman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,225 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/niklas-westman/penpair/main/docs/assets/logo.png" alt="penpair" width="120" height="120">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ <h1 align="center">penpair</h1>
6
+
7
+ <p align="center"><strong>Design together. Build together.</strong></p>
8
+
9
+ <p align="center">
10
+ One command connects your AI coding assistant to your <a href="https://penpot.app">Penpot</a> design file,<br>
11
+ so people and agents work on the same design, safely and repeatably.
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/@penpair/web"><img alt="npm" src="https://img.shields.io/npm/v/@penpair/web?color=0A5CFF&label=%40penpair%2Fweb"></a>
16
+ <a href="https://github.com/niklas-westman/penpair/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/niklas-westman/penpair/actions/workflows/ci.yml/badge.svg"></a>
17
+ <a href="https://github.com/niklas-westman/penpair/blob/main/LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/licence-MIT-0A5CFF"></a>
18
+ <img alt="Node 22 or later" src="https://img.shields.io/badge/node-%E2%89%A522-0A5CFF">
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="#why-penpair">Why</a> ·
23
+ <a href="#quick-start">Quick start</a> ·
24
+ <a href="#web-or-docker">Web or Docker</a> ·
25
+ <a href="#add-penpair-to-your-repo">Add to your repo</a> ·
26
+ <a href="#packages">Packages</a> ·
27
+ <a href="#use-it-with-marwes">Marwes</a> ·
28
+ <a href="#check-each-page-with-reflection">Reflection</a> ·
29
+ <a href="#for-an-ai-agent">For an AI agent</a>
30
+ </p>
31
+
32
+ ---
33
+
34
+ penpair makes a Penpot design file the **source of truth** for your project's UI, and makes your AI assistant dependable at working in it. Prompt for a whole page; the assistant reads the real design, builds with tested operations that look first, write once and read back, and follows written-down rules for how to work in Penpot. You review the result in Penpot as usual, and [Reflection](#check-each-page-with-reflection) can check the page it later builds in code against the design.
35
+
36
+ | | |
37
+ | --- | --- |
38
+ | **Connect it** | Point it at Penpot on the web, or your own, with one guided command and a check that it works. |
39
+ | **Drive it** | Inspect, build, audit, export, archive and manage design tokens through Penpot's MCP and plugin API. |
40
+ | **Know it** | The rules and the quirks of the tool ship with the code as skills your agent follows. |
41
+ | **Extend it** | Plug in your component library, your checks and your own snippets. The core stays neutral. |
42
+
43
+ ## Why penpair
44
+
45
+ An AI assistant can already reach Penpot through its MCP server: it gets one tool that runs code inside your open file. That is power without hands-on knowledge, and designing a whole page that way fails in small, expensive ways. penpair is what makes it dependable, so you can prompt for a page and get a page.
46
+
47
+ **What goes wrong with a bare connection**, all met while building real pages:
48
+
49
+ - The first export after a change is often a stale render, so the assistant "sees" a defect that is not there, or misses one that is.
50
+ - A token binding appears seconds after the call, so a correct write reads back as "not bound", and applying the same token twice removes it.
51
+ - A request over about 110 KB is refused, a switched page takes effect only after the script ends, and a sleeping browser tab raises an error even though the write already landed.
52
+ - Every new session rediscovers these, and a plain write cannot tell "done" from "half done".
53
+
54
+ **What penpair adds:**
55
+
56
+ | | What you get | Why it matters for a full page |
57
+ | --- | --- | --- |
58
+ | **Skills** | The traps, the flat auto-layout rules, token binding and screen-design judgment, written for the agent and installed into your repo | The assistant starts out knowing what took us hours to learn, and follows your component library and tokens instead of inventing styles |
59
+ | **A safe write protocol** | Look first, write once, read back from scratch, then look at an export | A page is built in verified steps; a half-finished change is reported, not assumed |
60
+ | **Stable exports and an audit** | Exports that wait for a stable render; a check for unbound colours, spacing off your scale, overflow and blank text | The assistant can judge its own page and show you evidence |
61
+ | **One-command setup and `doctor`** | A connection that is checked, a key that never leaves your machine, an agent-readable `doctor --json` | A new teammate or a new agent is productive in minutes |
62
+
63
+ **What a prompt can look like** once it is set up:
64
+
65
+ > Design the candidate list page for tablet portrait as master and detail. Use our list and card components, bind every value to tokens, keep the header from the other pages. Check it, export it and show me.
66
+
67
+ The assistant reads the project's skills, inspects the file, builds the page in one verified write, exports it (twice, so the render is stable), audits it and shows you the frames. You review in Penpot as usual. The protocol and the rules come from building a full page, in desktop, tablet and phone variants, on a real product.
68
+
69
+ penpair does not design for you, and it has no "make me a page" button. It makes the assistant that does the designing reliable, repeatable and checkable.
70
+
71
+ ## Quick start
72
+
73
+ > **New here, or not a terminal person?** Follow the step-by-step guide with the browser steps: **[Getting started →](https://github.com/niklas-westman/penpair/blob/main/docs/getting-started.md)**
74
+
75
+ You need [Node.js](https://nodejs.org) 22 or later and a terminal. In your project's folder:
76
+
77
+ ```bash
78
+ npx @penpair/web setup
79
+ ```
80
+
81
+ (`@penpair/web` is the package for Penpot on the web; the command it installs is called `penpair`. A shorter `npx penpair` is coming once npm allows the name. For Penpot in Docker on your computer, see [Web or Docker](#web-or-docker).)
82
+
83
+ It asks where your Penpot is and does the rest, saying in plain words what it is doing and what it is waiting for:
84
+
85
+ 1. writes a small config you can commit (`penpair.config.json`, no secrets),
86
+ 2. takes your personal key **without showing it** and keeps it in a git-ignored `.env.local`,
87
+ 3. waits while you connect the plugin in your browser (**File, MCP Server, Connect**),
88
+ 4. reads your file once to prove it works,
89
+ 5. installs the rules your AI assistant follows.
90
+
91
+ Run it again any time; it continues where it stopped. `npx @penpair/web doctor` tells you what works, what does not and what to do about it.
92
+
93
+ ## Web or Docker
94
+
95
+ Two ways to run Penpot, one way to connect. Pick one; you can switch later.
96
+
97
+ | | **On the web** (recommended) | **In Docker, on your computer** |
98
+ | --- | --- | --- |
99
+ | What | [design.penpot.app](https://design.penpot.app) (free account) or your team's own Penpot | A private Penpot per project on your machine |
100
+ | Install | `@penpair/web` | `@penpair/docker` instead (it includes everything in `@penpair/web`), Docker Desktop, a 2 GB first download |
101
+ | Good for | Getting going fast, sharing with a team by invite | Offline work, data that stays with you, an instance you can throw away |
102
+ | Backup | Penpot's version history and its `.penpot` export | `penpair backup`, `backup verify`, `restore` |
103
+ | Set it up | `npx @penpair/web setup --web` | `npx @penpair/docker setup --docker` (or `npm i -D @penpair/docker`, then `npx penpair setup --docker`) |
104
+
105
+ The full comparison, what is checked and how to switch: **[Web or Docker →](https://github.com/niklas-westman/penpair/blob/main/packages/core/skills/penpair-setup/references/web-or-docker.md)**
106
+
107
+ ## Add penpair to your repo
108
+
109
+ ```bash
110
+ cd your-project
111
+ npx @penpair/web setup # asks: web or this computer
112
+ ```
113
+
114
+ Then commit what it made (not `.env.local`; it is already in `.gitignore`):
115
+
116
+ ```bash
117
+ git add penpair.config.json .gitignore .skills .agents .claude
118
+ git commit -m "Add penpair"
119
+ ```
120
+
121
+ Everyone who works with the assistant does their own `npx @penpair/web connect`: the key belongs to a person, not to the repo. To pin penpair for the team, add it as a dev dependency: `npm i -D @penpair/web` (or `@penpair/docker` for Penpot in Docker; install one of the two, not both). The command is then `npx penpair …`. To write your own Penpot operations, use the library from the same package: `import { defineSnippet } from "@penpair/web"` (the library alone is `@penpair/core`).
122
+
123
+ ## Packages
124
+
125
+ | Package | What it is | Status |
126
+ | --- | --- | --- |
127
+ | [`@penpair/web`](https://www.npmjs.com/package/@penpair/web) | The `penpair` command for Penpot on the web. Includes everything in the core | 0.1.1 |
128
+ | [`@penpair/docker`](https://www.npmjs.com/package/@penpair/docker) | The same command, plus Penpot in Docker on your computer: `up`, `down`, `backup`, `restore`. Includes everything in the core | 0.1.1 |
129
+ | [`@penpair/core`](https://www.npmjs.com/package/@penpair/core) | The library and engine, with the skills. No command of its own; the two above are built on it | 0.1.1 |
130
+ | `penpair` | The short name for `@penpair/web`, so that `npx penpair setup` works | Waiting for npm to allow the name (it is blocked as too close to an unrelated package). Until then use `npx @penpair/web`. |
131
+ | `@penpair/marwes-ui` | The adapter for the Marwes UI component library | Planned, see below |
132
+ | `@penpair/reflection-check` | The link to Reflection's `reflection-check`, as its own package (today `penpair baselines` does it) | Planned |
133
+
134
+ You install **one** command package, `@penpair/web` or `@penpair/docker`; the core comes with it.
135
+
136
+ ## Use it with Marwes
137
+
138
+ Using [Marwes UI](https://marwes.io), the themeable component library for React, Vue and Svelte? Connecting Marwes is the recommended setup: your Penpot file then speaks the same tokens and components as your code, so what is designed is what gets built. The adapter, `@penpair/marwes-ui`, is the next package on the plan: it will import the Marwes tokens into your Penpot file and check that frames use them. Until it ships, set up penpair as above and name Marwes as your library in the project facts the setup skill asks you to fill in; the assistant then works against Marwes's tokens and components.
139
+
140
+ ## Check each page with Reflection
141
+
142
+ Designing a page is half the job; the other half is knowing the assistant built what the design says. We recommend pairing penpair with [Reflection](https://github.com/niklas-westman/reflection) (`reflection-check` on npm), which renders your real UI in a browser and compares it with an approved image, state by state, and shows you the difference.
143
+
144
+ ```
145
+ prompt ──▶ the assistant designs the page in Penpot ──▶ penpair export ──▶ an approved image per page and state
146
+ │
147
+ you review ◀── Reflection: the rendered page against that image, with a diff ◀── the assistant builds the page in code
148
+ ```
149
+
150
+ Why this pairing helps:
151
+
152
+ - **The design is the reference, not the assistant's memory.** The exported frame is what the delivery is measured against, so a misreading of the design shows up as a visible difference, not as a surprise in review.
153
+ - **Each page and state is one piece of evidence.** You look at a diff for the list, the open card, the empty state, the phone variant, instead of re-reading the whole change.
154
+ - **Nothing passes on a promise.** Reflection is review-first: a mismatch is shown to you, and an approved image only changes when you say so.
155
+
156
+ **The hand-off is one config block and one command.** List the frames of a page that are the design of a route, in `penpair.config.json`:
157
+
158
+ ```json
159
+ "baselines": {
160
+ "page": "Candidate",
161
+ "root": "tests/baselines",
162
+ "frames": [
163
+ { "frame": "F / candidates", "id": "candidates-desktop", "route": "candidates" },
164
+ { "frame": "F tablet / candidates", "id": "candidates-tablet", "route": "candidates" }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ ```bash
170
+ npx @penpair/web baselines # look: which images are new, the same, or changed in the design
171
+ npx @penpair/web baselines --apply # write the new images, a manifest and reflection-cases.json
172
+ ```
173
+
174
+ penpair exports each frame at a stable render, writes `tests/baselines/<id>.png`, and generates the Reflection cases for you: each frame's exact size becomes the viewport, so the capture and the reference have the same dimensions, and each check carries `ref: "penpair:<id>"` to join it back to the frame. In your `reflection.config.ts` you add the generated viewports to the route and spread the generated `visualSmoke` into the browser contract (a working example is in the [guide](https://github.com/niklas-westman/penpair/blob/main/docs/guide.md#baselines-for-a-check)), then run `npx reflection run --config reflection.config.ts --mode smoke`.
175
+
176
+ A changed design is never silently approved: when a frame no longer matches its image, `penpair baselines` says `changed` and leaves the image alone until you run `--apply --update`, which is the act of approving the new design. We ran this chain end to end (Penpot frame, `penpair baselines`, Reflection, browser, diff) and the check, its diff image and its `ref` all came out right.
177
+
178
+ One thing to know before you rely on it: the percentage is not a verdict. A correct implementation already differs from a Penpot render by about 1 to 2% on text alone (fonts, anti-aliasing), and a faint colour shift or a one-line copy change can sit below that. Read the diff image and Reflection's diagnostics; they point at the likely cause. What is still planned is the richer link, where each page's states are read from Penpot and shown in Reflection's own viewer (through `reflection-projection`, already tried on a real page of 30 states).
179
+
180
+ ## For an AI agent
181
+
182
+ If you were told to set penpair up, read [the setup skill](https://github.com/niklas-westman/penpair/blob/main/packages/core/skills/penpair-setup/SKILL.md) first and follow it. In short:
183
+
184
+ - Run `npx @penpair/web doctor --json` to see where things stand; every failing check carries a `remedy`.
185
+ - Web is the default; use Docker only when the person asks for offline or local data.
186
+ - **The personal MCP key never goes through you.** The person pastes it themselves into `npx @penpair/web connect` (a hidden prompt), or pipes it from their clipboard.
187
+ - Done means `doctor` reports `ok: true` and "editor and plugin" connected. Then `npx @penpair/web skills install` gives you the rules for working in Penpot (`penpot-design`) and for judging a screen (`design-judgment`).
188
+
189
+ ## Commands
190
+
191
+ `npx @penpair/web --help` lists them all. The ones you need first:
192
+
193
+ | Command | What it does |
194
+ | --- | --- |
195
+ | `setup [--web \| --docker]` | The guided path from nothing to a working connection |
196
+ | `connect [--replace]` | Only the connection: your key (hidden), then waits for the editor |
197
+ | `doctor [--json]` | What works, what does not, and what to do about it |
198
+ | `pages`, `tree`, `audit`, `tokens dump` | Read your Penpot file |
199
+ | `export <shapeId> <out.png>` | A stable PNG of a frame |
200
+ | `baselines [--apply] [--update]` | Export a page's frames as approved images, with ready-made Reflection cases |
201
+ | `skills install` | The agent's rules, into `.skills/`, visible to Codex and Claude Code |
202
+ | `up`, `down`, `backup`, `restore` | Docker only (they come with `@penpair/docker`) |
203
+
204
+ ## Good to know
205
+
206
+ - Penpot's plugin needs the **editor tab open and visible** while the assistant works, so penpair cannot run unattended overnight. It acts on one Penpot at a time, on the page you have open.
207
+ - Checked against Penpot 2.18.3 (Docker) and the hosted Penpot 2.18.4: the same tools and limits, and a full deep check on both ([compatibility](https://github.com/niklas-westman/penpair/blob/main/docs/compatibility.md)). Not proven yet: a large file with a shared library on the hosted service, paid plans, two editors in one file, and a full `.penpot` export and import round trip.
208
+ - penpair is early (0.1.0). Expect rough edges and quick fixes. The plan and the open questions are in [PLAN.md](https://github.com/niklas-westman/penpair/blob/main/PLAN.md).
209
+
210
+ ## Documentation
211
+
212
+ - [Getting started](https://github.com/niklas-westman/penpair/blob/main/docs/getting-started.md): step by step, for designers and for whoever sets it up
213
+ - [Guide](https://github.com/niklas-westman/penpair/blob/main/docs/guide.md): extension points, instances, commands
214
+ - [Compatibility](https://github.com/niklas-westman/penpair/blob/main/docs/compatibility.md): what was checked on which Penpot
215
+ - [Releasing](https://github.com/niklas-westman/penpair/blob/main/docs/releasing.md), [Changelog](https://github.com/niklas-westman/penpair/blob/main/CHANGELOG.md)
216
+
217
+ penpair is the other half of [Reflection](https://github.com/niklas-westman/reflection): Reflection checks that an implementation matches its design; penpair holds and drives the design that everything is measured against.
218
+
219
+ ## Contributing and security
220
+
221
+ Issues and small pull requests are welcome: see [CONTRIBUTING](https://github.com/niklas-westman/penpair/blob/main/CONTRIBUTING.md). Your Penpot MCP key is a password; read [SECURITY](https://github.com/niklas-westman/penpair/blob/main/SECURITY.md) before you report a problem.
222
+
223
+ ## Licence
224
+
225
+ [MIT](https://github.com/niklas-westman/penpair/blob/main/LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "@penpair/core";
3
+ // The `penpair` command for Penpot on the web. (@penpair/docker is the same command with the Docker commands added.)
4
+ process.exitCode = await runCli(process.argv.slice(2));
@@ -0,0 +1 @@
1
+ export * from "@penpair/core";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // Everything the core has, so that a project with this package imports from one place.
2
+ export * from "@penpair/core";
@@ -0,0 +1,29 @@
1
+ # Compatibility
2
+
3
+ What has been checked, where, and what differs. Update it whenever a Penpot is probed. "Not run" means not run, not that it fails.
4
+
5
+ | | Local Docker, Penpot 2.18.3 | Hosted, design.penpot.app, Penpot 2.18.4 (free account) |
6
+ | --- | --- | --- |
7
+ | Tools offered | `execute_code`, `high_level_overview`, `penpot_api_info`, `export_shape` | the same four |
8
+ | Request body | 90 KB accepted, 110 KB refused | the same |
9
+ | Response size | 3 MB returned | 1 MB returned (larger not tried) |
10
+ | Plugin API: tokens, `uploadMediaData`, `createBoard`, `openPage`, file export | present | present |
11
+ | `currentFile.export` | fails ("No matching clause") | not tried |
12
+ | Deep check (`probe --deep --write`): flex layout, text, token set and binding, components and swap, clone, PNG export, clean-up | all steps pass, nothing left behind (run on a fresh instance's empty file) | all steps pass, nothing left behind (run twice) |
13
+ | `pages`, `tree`, `audit`, `tokens dump`, `export` (waits for a stable render), `archive` with `--apply`, a project snippet that writes (`run seed-demo`) | all in use | all verified on seeded sample content |
14
+ | `doctor` (external profile) | ok | ok |
15
+ | `init`, `up`, `doctor` for a Penpot penpair starts (a fresh instance on its own port, then the owner's account, key and plugin connection) | ok: 16 seconds to answer with the images cached; `doctor` all ok | not applicable |
16
+
17
+ ## Differences found
18
+ - **A new token set starts inactive, with no themes in the file.** Seen on both the hosted Penpot and a fresh local instance. (An earlier note that local sets start active was wrong or applied to a file with themes.)
19
+ - **A token binding is recorded after the call, not during it.** On the hosted Penpot it appeared after about half a second to a few seconds; reading it straight away shows nothing. Code that checks a binding has to wait for it.
20
+ - **Applying the same token again removes the binding** (seen on the hosted Penpot, as in the local notes).
21
+ - **A library component's name is split into a path and a name.** Naming the shapes "Demo / button" and creating the component gives a component whose path is "Demo" and whose name is "button"; setting the component's name to the full text afterwards makes the main shape "Demo / Demo / button". Create the component and leave its name.
22
+ - **Exporting a small board took about 3 to 4 seconds on the hosted Penpot and about 7 seconds on the local one.** The first export after a change can be stale; `export` takes two until they match.
23
+ - **Versions differ by one patch release** (2.18.3 local, 2.18.4 hosted). Nothing in the checks depended on it. Pin the version that was tested, and run the deep check before moving it.
24
+
25
+ ## Not checked
26
+ - The hosted Penpot on a large file, with real design content and a shared library.
27
+ - Paid plans, and whether the free plan keeps the remote MCP.
28
+ - Two people in the same hosted file while an agent writes.
29
+ - Penpot's own version history as a backup for an external project.
@@ -0,0 +1,99 @@
1
+ # Getting started
2
+
3
+ Draft. Written for someone who has not used a terminal much. The steps marked **(not tried yet)** come from Penpot's help pages, not from a run; the first person to follow this guide on a clean machine should mark what differs.
4
+
5
+ The normal way is to use Penpot on the web (or your team's own Penpot) and connect the AI assistant to it. Nothing runs on your computer except penpair. If you want your own copy of Penpot on your computer instead, see "Alternative: run Penpot on your own computer" at the end.
6
+
7
+ There are two roles. You may be both.
8
+
9
+ - **The designer** works in Penpot in the browser (go to "If you are the designer").
10
+ - **The person who sets it up** connects the AI assistant to that Penpot (go to "If you set it up").
11
+
12
+ ## If you are the designer
13
+ You need a link to your team's Penpot and an account.
14
+
15
+ 1. Open the link and log in.
16
+ 2. Open the design file you will work on.
17
+ 3. Choose **File, MCP Server, Connect** (not tried yet). A small window may appear; leave it open.
18
+ 4. Keep this browser tab **visible** while the assistant works. If the browser puts the tab to sleep, the assistant stops and says the tab is suspended. Bring the tab to the front and it carries on.
19
+ 5. Tell the assistant which page to work on. It works on the page you have open.
20
+
21
+ That is all. The assistant looks first, changes things in one go, and checks its own work; you review the result in Penpot as usual.
22
+
23
+ ## If you set it up
24
+ You need: a Mac or Linux computer, **Node.js** (the current LTS version), a terminal, and an account on a Penpot (the hosted one at design.penpot.app is free to start; use your team's address if it runs its own).
25
+
26
+ **The quick way:** in your project's folder run `npx @penpair/web setup`. It asks where your Penpot is (press Enter for the web), does steps 1 to 5 below, and says in plain words what it is waiting for. Run it again if you stop halfway. The rest of this section is what it does, one step at a time, for when you want to do it by hand or something needs fixing.
27
+
28
+ ### 1. Connect your project to that Penpot
29
+ In the folder of your project:
30
+
31
+ ```
32
+ npx @penpair/web init --external https://design.penpot.app --gitignore
33
+ ```
34
+
35
+ `init` writes a small settings file (`penpair.config.json`, commit it) and keeps your private files out of git for you.
36
+
37
+ ### 2. Make your account and a file (once)
38
+ Open the Penpot address, create an account and log in. Create a project and a design file for this work.
39
+
40
+ ### 3. Connect the assistant (once)
41
+ 1. In Penpot open **Account, Integrations, MCP Server** and switch it on (not tried yet). Copy the address it shows; it contains your personal key, so treat it like a password. The key belongs to you, not to the team; each person who uses the assistant makes their own.
42
+ 2. In your project folder run `npx @penpair/web connect` and paste the address when asked. Nothing is shown as you paste. It saves the address in `.env.local` (readable only by you, and added to `.gitignore`) and refuses an address that belongs to a different Penpot. (By hand instead: put the line `PENPAIR_MCP_URL=<the address>` in `.env.local` and keep that file out of git.)
43
+ 3. Open your design file in Penpot, choose **File, MCP Server, Connect**, and keep that tab visible.
44
+
45
+ ### 4. Check that it works
46
+ ```
47
+ npx @penpair/web doctor
48
+ ```
49
+ Every line should say `ok`. If one says `FAIL`, the line under it says what to do. The usual ones:
50
+
51
+ | It says | What to do |
52
+ | --- | --- |
53
+ | penpot: not answering | Check the address in the config and your internet connection |
54
+ | editor and plugin: the MCP address points at another instance | The address in `.env.local` belongs to a different Penpot. Copy the one from this Penpot |
55
+ | editor and plugin: not connected | Do step 3.3 again and keep the tab visible |
56
+
57
+ Then try: `npx @penpair/web pages` lists the pages in the open file.
58
+
59
+ If you want to know what your Penpot offers before you start, `npx @penpair/web probe` lists its tools and limits. If `execute_code` is missing from that list, penpair cannot work with it. The hosted service, checked 2026-10-20 on Penpot 2.18.4, offers the same four tools and the same limits as a local Penpot (see `compatibility.md`).
60
+
61
+ ### 5. Give the assistant its rules (once)
62
+ ```
63
+ npx @penpair/web skills install
64
+ ```
65
+ This copies the rules the assistant follows (how to work safely in Penpot, and how to judge a screen) into `.skills/` and makes them visible to Codex and Claude Code. It prints a line for each to add to your `AGENTS.md`. Then fill in `.skills/penpot-design/references/project-facts.md` (your file, never replaced): the Penpot file, the token sets, the names you use, who approves.
66
+
67
+ ### Every day
68
+ Open the file in Penpot, choose **File, MCP Server, Connect**, keep the tab visible, and start working with the assistant.
69
+
70
+ ### Your safety net
71
+ `up`, `down`, `status`, `backup` and `restore` belong to the Docker alternative and do not apply here. Your safety net is Penpot's own version history and the `.penpot` export in the File menu. Whether a `.penpot` export restores everything (components, token sets, themes) is not proven yet; see the [plan](https://github.com/niklas-westman/penpair/blob/main/PLAN.md).
72
+
73
+ ## What it will not do
74
+ It needs the Penpot tab open and visible while the assistant works, so it cannot run on its own overnight. It changes one Penpot at a time and only the page you have open.
75
+
76
+ ## Alternative: run Penpot on your own computer
77
+ Choose this when you want to work offline, keep the data on your machine, or have one Penpot per project that you can throw away. It needs **Docker Desktop** running in addition to Node.js, and the package `@penpair/docker`, which is kept apart so that web users carry none of it. The first start downloads about 2 GB.
78
+
79
+ ```
80
+ npm i -D @penpair/docker
81
+ npx penpair init --gitignore
82
+ npx penpair up
83
+ ```
84
+
85
+ (For a one-off run without installing: `npx @penpair/docker setup --docker`.) `@penpair/docker` is the same command as `@penpair/web` with the Docker commands added, and it includes everything in it, so install it instead of `@penpair/web`, not beside it. Without it, a Docker command stops and says where it comes from.
86
+
87
+ `init` also writes a hidden folder `.penpair/` with the Penpot settings and secret keys. When `up` says "Penpot is up at http://127.0.0.1:9001" (your port may differ; use `--port 9002` with `init` if one is taken), open that address and create an account (no email confirmation is needed). Then do steps 3 to 5 above in that Penpot (with `npx penpair connect`, `npx penpair doctor` and `npx penpair skills install`). `npx penpair doctor` also checks Docker, the secrets file and the containers:
88
+
89
+ | It says | What to do |
90
+ | --- | --- |
91
+ | docker: not answering | Start Docker Desktop and wait until it says it is running |
92
+ | containers: not running | Run `npx penpair up` |
93
+ | backend: not ready yet | Wait ten seconds and run `doctor` again |
94
+
95
+ Every day: `npx penpair up` (if Docker was restarted) and `npx penpair down` to stop; your designs stay. Back up before big changes with `npx penpair backup`, and prove a backup works with `npx penpair backup verify <the folder it printed>`. The backup contains account data, so never put it in git.
96
+
97
+ ## Still to make easier (see the [plan](https://github.com/niklas-westman/penpair/blob/main/PLAN.md))
98
+ - A short illustrated version with a screenshot of each browser step.
99
+ - A shared Penpot for a team, so designers need no setup.
package/docs/guide.md ADDED
@@ -0,0 +1,93 @@
1
+ # Guide (skeleton)
2
+
3
+ The contract an adopting project works through. It grows with the code; each section ends in a checklist like Reflection's provider guide.
4
+
5
+ ## Extension points
6
+ | Point | A project supplies | First implementations |
7
+ | --- | --- | --- |
8
+ | Connection profile | endpoint, Penpot version, where the token comes from | local Docker, shared self-hosted, hosted |
9
+ | Library adapter | where tokens and components come from | Marwes |
10
+ | Snippet registry | named plugin snippets with inputs and a size check | tree, audit, tokens, archive |
11
+ | Verification provider | what "matches the design" means | `penpair baselines` (images and Reflection cases, below), a page-verify gate |
12
+ | Skill pack | rules the agent follows | the Penpot skill, design judgment |
13
+
14
+ ## Sections to write
15
+ - Starting from an empty machine
16
+ - Instances (written, below): one per project, ports, volumes, secrets, backup and restore
17
+ - The write protocol
18
+ - Writing a snippet
19
+ - Writing a library adapter
20
+ - What breaks when Penpot is upgraded
21
+
22
+ ## Instances
23
+ One Penpot per project, configured in `penpair.config.json` (no secrets, commit it):
24
+
25
+ ```json
26
+ { "instance": "my-app", "penpotVersion": "2.18.3", "ports": { "frontend": 9002, "mail": 1081 }, "directory": ".penpair" }
27
+ ```
28
+
29
+ | Command | What it does |
30
+ | --- | --- |
31
+ | `penpair setup [--web \| --docker]` | The guided path: where is Penpot, config, start it if it is Docker, take the key (hidden), wait for the editor, read the file, install the skills |
32
+ | `penpair connect [--replace]` | Only the key and the wait for the editor; accepts a pipe (`pbpaste \| penpair connect`) |
33
+ | `penpair init [--instance <name>] [--port 9002] [--gitignore]` | Writes the config if missing, the compose file, and a secrets file (owner-only) that is never replaced |
34
+ | `penpair up` / `down` / `status` | Start (waits until the backend answers), stop (the data volumes stay), look |
35
+ | `penpair doctor [--json]` | Docker, secrets, containers, frontend, backend, then the editor and plugin through the MCP, and that `.env.local` is kept out of git (a web project checks only the address, the editor and the key file) |
36
+ | `penpair backup` | A Postgres dump and a tarball of the assets, with checksums and counts, in `.penpair/backups/` |
37
+ | `penpair backup verify <dir>` | Restores the dump into a throwaway database and compares counts, revisions and asset files |
38
+ | `penpair restore <dir> --yes` | Replaces the instance's data; a safety backup of the current data comes first; the counts are compared afterwards |
39
+
40
+ Things to know:
41
+ - Two instances never collide: the compose project is `penpair-<instance>`, so containers and volumes have their own names. `composeProject` in the config adopts an instance that was started earlier under another name.
42
+ - Keep `.penpair/` out of git. Backups hold account data (password hashes).
43
+ - The database password must not change while the database volume exists; the secrets file is never regenerated.
44
+ - The frontend answers a few seconds before the backend; `up` and `doctor` look at `/readyz`.
45
+ - `doctor` compares the MCP key's port with the instance's, because a key for another instance silently drives the wrong file.
46
+ - Penpot's accounts come from the first registration in the browser; the MCP key is made in Account, Integrations, MCP Server and goes in `.env.local` as `PENPAIR_MCP_URL`.
47
+
48
+ ## Baselines for a check
49
+
50
+ `penpair baselines` hands a page's design to a check that compares the implementation against images. It reads the `baselines` block of `penpair.config.json`:
51
+
52
+ ```json
53
+ "baselines": {
54
+ "page": "Candidate",
55
+ "root": "tests/baselines",
56
+ "threshold": 0.05,
57
+ "frames": [
58
+ { "frame": "F / candidates", "id": "candidates-desktop", "route": "candidates" },
59
+ { "frame": "F / my work", "id": "my-work" }
60
+ ]
61
+ }
62
+ ```
63
+
64
+ - `frame` is the exact name of a top-level frame on the page; two frames with the same name is an error, so the reference is never ambiguous.
65
+ - `id` names the image (`<id>.png`) and the check. `route` is the route in your app this frame is the design of; a frame without a route is exported but gets no check.
66
+ - Without `--apply` the command only looks and reports each frame as `new`, `same`, `changed`, `unstable` (the export never settled) or `missing`. `--apply` writes new images, `penpair-baselines.json` (file, revision, sizes, hashes) and `reflection-cases.json`. A `changed` image is replaced only with `--apply --update`: that is the act of approving the new design, and the owner's call. The exit code is 2 when a design differs from its image, 1 when a frame is missing or unstable.
67
+
68
+ The generated `reflection-cases.json` holds, per route, the viewports (the image's exact size) and the `visualSmoke` cases (review-only, `strict: false`). A Reflection config uses it like this; only the route's own expectations are written by hand:
69
+
70
+ ```ts
71
+ import { defineReflection } from 'reflection-check';
72
+ import cases from './tests/baselines/reflection-cases.json' with { type: 'json' };
73
+
74
+ export default defineReflection({
75
+ project: 'my-app',
76
+ run: { defaultMode: 'smoke', ciMode: 'smoke' },
77
+ contracts: {
78
+ browser: {
79
+ enabled: true,
80
+ blocking: true,
81
+ baseUrl: 'http://127.0.0.1:5173',
82
+ routes: [
83
+ { id: 'candidates', name: 'Candidates', path: '/candidates', viewports: cases.routes.candidates.viewports, expects: [{ screenshot: 'final' }] },
84
+ ],
85
+ visualSmoke: cases.visualSmoke as never,
86
+ },
87
+ },
88
+ });
89
+ ```
90
+
91
+ `baselineRoot` in the generated cases is relative to the Reflection config's folder, so keep the two in the same project root. The same images work with any other image comparison; the Reflection cases are one output of the manifest.
92
+
93
+ The percentage a comparison reports is not a verdict: a correct implementation differs from a Penpot render by about 1 to 2% on text alone, so read the diff image.
@@ -0,0 +1,76 @@
1
+ # Releasing
2
+
3
+ All packages in this repository are released **together, at one version, even a package that did not change**: `@penpair/core`, `@penpair/web`, `@penpair/docker`, `penpair`, and anything added under `packages/` later. The root of the repository is a private workspace and is never published. The rule is enforced, not remembered:
4
+
5
+ | Guard | What it stops |
6
+ | --- | --- |
7
+ | `pnpm check` (and CI) runs `scripts/check-versions.mjs` | Any package at a different version, a version that differs from `packages/core/src/version.ts`, a private package, an internal dependency that is not `workspace:*`, a package without the publish guard |
8
+ | `pnpm bump patch\|minor\|major\|x.y.z` | Drift starting at all: it sets every package and `version.ts` in one step |
9
+ | `prepublishOnly` in every package | `pnpm publish` inside one package: it refuses unless `pnpm release` started it, and checks the versions again |
10
+ | `pnpm release` | A half release: it checks, publishes the whole set in dependency order, looks each one up on npm, and reports anything missing |
11
+
12
+ Publishing cannot be undone: a version, once published, cannot be replaced. Check first, publish once.
13
+
14
+ ## One time
15
+
16
+ 1. The npm organisation `penpair` exists (npmjs.com, Add organization, free for public packages).
17
+ 2. `npm whoami` prints your user name. If it answers `401`, run `npm login`.
18
+
19
+ ## Each release
20
+
21
+ ```bash
22
+ pnpm bump patch # or minor, major, or an exact x.y.z: every package and version.ts move together
23
+ # add a section to CHANGELOG.md
24
+ pnpm check # typecheck, tests, build and the version check
25
+ git commit -am "Release 0.1.2" && git push
26
+
27
+ pnpm release # dry run: checks everything, prints the plan, publishes nothing
28
+ pnpm release --publish # the real thing; npm asks for your one-time code (or add --otp <code>)
29
+ ```
30
+
31
+ `pnpm release` refuses unless all of this is true, and tells you which part is not:
32
+
33
+ - every package has the same version, and the source version matches;
34
+ - you are on `main`, the working tree is clean, and `main` equals `origin/main`, so what is published is what is on GitHub;
35
+ - npm has none of the packages at this version (or it is an interrupted release, below);
36
+ - `pnpm check` and `pnpm smoke:package` pass (the smoke test packs every package, installs the tarballs in empty projects and runs them).
37
+
38
+ Then it publishes in dependency order (the core, then the packages that depend on it), waits until npm shows each one, and finally looks up all of them. If any is missing it says `INCOMPLETE` and exits with 3.
39
+
40
+ ## If a release stops half way
41
+
42
+ A publish can fail after some packages are out (a network error, a wrong one-time code, a name npm refuses). The version is then on npm for some packages and not for others, which must not stay that way. Fix the cause and run:
43
+
44
+ ```bash
45
+ pnpm release --publish --continue # publishes only what is missing at this version
46
+ ```
47
+
48
+ Without `--continue`, `pnpm release` refuses to run while only some packages are at the version, so a partial release cannot be forgotten.
49
+
50
+ ## A package npm will not accept yet
51
+
52
+ `penpair` (the short name) is refused by npm while its name is blocked as too close to another package. To release the rest and keep the rule visible, leave it out explicitly:
53
+
54
+ ```bash
55
+ pnpm release --publish --except penpair
56
+ ```
57
+
58
+ The release is then reported as `INCOMPLETE` (exit code 3) on purpose, and `pnpm release --publish --continue` finishes it the day npm allows the name. Nothing else can be left out, and only by naming it.
59
+
60
+ ## Verify
61
+
62
+ ```bash
63
+ npm view @penpair/core version
64
+ npm view @penpair/web version
65
+ npm view @penpair/docker version
66
+ ```
67
+
68
+ Then, in an empty folder, run `npx @penpair/web --version`, `npx @penpair/web setup --help` and `npx @penpair/docker --version` (and, once the short name is published, the same with `npx penpair`). Tag the release if you want one: `git tag v<version> && git push origin v<version>`.
69
+
70
+ ## What each package ships
71
+
72
+ The core ships `dist`, `skills`, `LICENSE` and a short `README.md`. The web package ships `dist`, `docs`, `LICENSE` and the repository's `README.md` (copied in by `prepack`, git-ignored there). The Docker package ships `dist`, `LICENSE` and `README.md`. The short name ships `bin`, `LICENSE` and `README.md`. Check with `npm pack --dry-run --ignore-scripts` in a package.
73
+
74
+ ## After publishing
75
+
76
+ Update the version in `PLAN.md` and add the change log line. A problem found in a published version is answered with a new version of **all** packages, never by changing a published one.
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@penpair/web",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "The penpair command for Penpot on the web (design.penpot.app or your team's own): guided setup, a safe write protocol, skills for AI agents. Includes everything in @penpair/core.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/niklas-westman/penpair.git",
9
+ "directory": "packages/web"
10
+ },
11
+ "homepage": "https://github.com/niklas-westman/penpair#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/niklas-westman/penpair/issues"
14
+ },
15
+ "type": "module",
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/index.js",
24
+ "types": "./dist/index.d.ts"
25
+ },
26
+ "./cli": "./dist/cli.js",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "bin": {
30
+ "penpair": "dist/cli.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "docs",
35
+ "LICENSE",
36
+ "README.md"
37
+ ],
38
+ "keywords": [
39
+ "penpot",
40
+ "design-systems",
41
+ "mcp",
42
+ "ai-agents",
43
+ "design-tokens",
44
+ "developer-tools"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "dependencies": {
50
+ "@penpair/core": "0.1.1"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.json",
54
+ "typecheck": "tsc -p tsconfig.json --noEmit"
55
+ }
6
56
  }