penpot-local-stack 0.1.0__tar.gz
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.
- penpot_local_stack-0.1.0/.gitignore +23 -0
- penpot_local_stack-0.1.0/.penpot.yaml.example +9 -0
- penpot_local_stack-0.1.0/CONTRIBUTING.md +121 -0
- penpot_local_stack-0.1.0/LICENSE +21 -0
- penpot_local_stack-0.1.0/Makefile +84 -0
- penpot_local_stack-0.1.0/PKG-INFO +180 -0
- penpot_local_stack-0.1.0/README.md +155 -0
- penpot_local_stack-0.1.0/bin/penpot-stack +12 -0
- penpot_local_stack-0.1.0/config/autologin.conf +5 -0
- penpot_local_stack-0.1.0/config/compose.yaml +86 -0
- penpot_local_stack-0.1.0/libs/__init__.py +1 -0
- penpot_local_stack-0.1.0/libs/cli.py +135 -0
- penpot_local_stack-0.1.0/libs/compose.py +45 -0
- penpot_local_stack-0.1.0/libs/penpot.py +83 -0
- penpot_local_stack-0.1.0/libs/prompts.py +34 -0
- penpot_local_stack-0.1.0/libs/settings.py +46 -0
- penpot_local_stack-0.1.0/libs/templates.py +90 -0
- penpot_local_stack-0.1.0/pyproject.toml +79 -0
- penpot_local_stack-0.1.0/tests/conftest.py +16 -0
- penpot_local_stack-0.1.0/tests/test_cli.py +149 -0
- penpot_local_stack-0.1.0/tests/test_compose.py +37 -0
- penpot_local_stack-0.1.0/tests/test_prompts.py +13 -0
- penpot_local_stack-0.1.0/tests/test_settings.py +47 -0
- penpot_local_stack-0.1.0/tests/test_templates.py +91 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Agents Configs
|
|
2
|
+
.claude/
|
|
3
|
+
.gemini/
|
|
4
|
+
CLAUDE.local.md
|
|
5
|
+
GEMINI.md
|
|
6
|
+
.mcp.json
|
|
7
|
+
|
|
8
|
+
# IDE
|
|
9
|
+
.venv
|
|
10
|
+
.idea
|
|
11
|
+
|
|
12
|
+
# Python build and test artifacts
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
*.egg-info/
|
|
16
|
+
dist/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
|
|
19
|
+
# pre-commit cache, kept in-tree by PRE_COMMIT_HOME
|
|
20
|
+
.pre-commit
|
|
21
|
+
|
|
22
|
+
# Local stack settings, written by install.sh
|
|
23
|
+
.penpot.yaml
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Penpot local stack. Copy to .penpot.yaml and edit; install.sh does that for
|
|
2
|
+
# you. Remove a key to fall back to its default, or override it in the
|
|
3
|
+
# environment as PENPOT_PORT, PENPOT_TEMPLATES and so on.
|
|
4
|
+
|
|
5
|
+
templates: templates
|
|
6
|
+
host: 127.0.0.1
|
|
7
|
+
port: 9001
|
|
8
|
+
project: penpot-local
|
|
9
|
+
version: 2.17.2
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Everything in the checkout runs through `make`, on the system `python3`:
|
|
4
|
+
there is no virtualenv here and nothing installs itself. Python 3.12 or newer
|
|
5
|
+
with the packages from `requirements.txt`, Docker with the Compose plugin, and
|
|
6
|
+
[uv](https://docs.astral.sh/uv/) for `make build`.
|
|
7
|
+
|
|
8
|
+
- [Layout](#layout)
|
|
9
|
+
- [Make targets](#make-targets)
|
|
10
|
+
- [Working on the stack](#working-on-the-stack)
|
|
11
|
+
- [Tests and linters](#tests-and-linters)
|
|
12
|
+
- [Continuous integration](#continuous-integration)
|
|
13
|
+
- [Releases](#releases)
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
bin/penpot-stack launcher, runs the code straight from the checkout
|
|
19
|
+
libs/ the modules: cli, settings, compose, penpot, templates, prompts
|
|
20
|
+
config/ compose.yaml and autologin.conf
|
|
21
|
+
install/ the curl installer and its documentation
|
|
22
|
+
tests/ pytest suite, no network and no Docker
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The wheel carries `libs/` and `config/` under one importable name: the build
|
|
26
|
+
maps `libs` to `penpot_stack` and `config` to `penpot_stack/config`, so the
|
|
27
|
+
Compose file ships with the code and `penpot-stack` runs from anywhere. That
|
|
28
|
+
mapping lives in `[tool.hatch.build.targets.wheel]` in `pyproject.toml`.
|
|
29
|
+
|
|
30
|
+
Modules import each other relatively, which is what lets the same files work
|
|
31
|
+
as `libs` in the checkout and as `penpot_stack` once installed.
|
|
32
|
+
|
|
33
|
+
`libs/settings.py` is the only place that decides anything configurable: a
|
|
34
|
+
pydantic-settings model filled from defaults, then `.penpot.yaml` in the
|
|
35
|
+
working directory, then `PENPOT_*` variables. `config/compose.yaml` reads the
|
|
36
|
+
same names through `${PENPOT_PORT:-9001}` style defaults, so the file still
|
|
37
|
+
runs under a bare `docker compose`. `.penpot.yaml.example` in the repository
|
|
38
|
+
root is the documented shape of that file and what `install.sh` copies into a
|
|
39
|
+
directory; `.penpot.yaml` itself is gitignored, so the checkout runs on the
|
|
40
|
+
defaults until you copy the example over.
|
|
41
|
+
|
|
42
|
+
Nothing in the checkout installs that package, so an editable install never
|
|
43
|
+
comes up - which is just as well, because hatchling rejects a dev-mode install
|
|
44
|
+
whose `sources` rewrite replaces a prefix.
|
|
45
|
+
|
|
46
|
+
## Make targets
|
|
47
|
+
|
|
48
|
+
| Target | What it does |
|
|
49
|
+
| -------------- | ---------------------------------------------------------- |
|
|
50
|
+
| `make` | Open an interactive subshell with `PRE_COMMIT_HOME` set |
|
|
51
|
+
| `make test` | Run the pytest suite |
|
|
52
|
+
| `make lint` | Run the pre-commit hooks over every file |
|
|
53
|
+
| `make up` | Start the stack from the checkout |
|
|
54
|
+
| `make down` | Offer the export, confirm the wipe, stop the stack |
|
|
55
|
+
| `make import` | Import a template into the running stack |
|
|
56
|
+
| `make export` | Export a file back into `templates/` |
|
|
57
|
+
| `make extract` | Unpack a `.penpot` file into `templates/`, no stack needed |
|
|
58
|
+
| `make convert` | Pack a template into a `.penpot` file, no stack needed |
|
|
59
|
+
| `make build` | Build the wheel and the sdist into `dist/` |
|
|
60
|
+
| `make clean` | Remove the build artifacts |
|
|
61
|
+
|
|
62
|
+
Start by giving your `python3` what `requirements.txt` lists, however this
|
|
63
|
+
machine installs packages, then wire up the hooks once:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pre-commit install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Working on the stack
|
|
70
|
+
|
|
71
|
+
`make up` and the rest run `python3 bin/penpot-stack`, so they use the working
|
|
72
|
+
tree against `templates/` in the repository root. Point `PENPOT_TEMPLATES` somewhere else to keep the repository clean:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
PENPOT_TEMPLATES=~/mockups make up
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The stack itself is `config/compose.yaml`: Penpot frontend, backend, exporter
|
|
79
|
+
and MCP, with Postgres on tmpfs and Valkey without persistence. Nothing in it
|
|
80
|
+
is meant to survive `down`.
|
|
81
|
+
|
|
82
|
+
## Tests and linters
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
make test
|
|
86
|
+
make lint
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The tests cover the parts that do not need Penpot: template discovery, the
|
|
90
|
+
export filter that drops thumbnails and refuses raster images, the Compose
|
|
91
|
+
command, and every confirm in the command flow. Anything that talks to the
|
|
92
|
+
API is mocked, so the suite needs neither network nor Docker.
|
|
93
|
+
|
|
94
|
+
`make lint` runs ruff, shellcheck, shfmt, markdownlint and the commit-message
|
|
95
|
+
checks through pre-commit. Both are expected to pass before a commit.
|
|
96
|
+
|
|
97
|
+
## Continuous integration
|
|
98
|
+
|
|
99
|
+
`.github/workflows/ci.yml` runs on every pull request and on pushes to `main`:
|
|
100
|
+
one job runs the pre-commit hooks over all files, the other runs the suite on
|
|
101
|
+
Python 3.12, 3.13 and 3.14. Actions are pinned by commit, with the version in
|
|
102
|
+
a trailing comment. Nothing is published from CI.
|
|
103
|
+
|
|
104
|
+
Dependabot watches three ecosystems weekly: `pip` for `requirements.txt`,
|
|
105
|
+
`pre-commit` for the hook revisions and `github-actions` for those pins.
|
|
106
|
+
|
|
107
|
+
## Releases
|
|
108
|
+
|
|
109
|
+
The version lives in `pyproject.toml` and is bumped with commitizen, which
|
|
110
|
+
writes the changelog and the tag:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
cz bump
|
|
114
|
+
git push --follow-tags
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Users upgrade by re-running `install/install.sh`, which unfolds a tag with
|
|
118
|
+
`--ref`, so a release is the tag itself.
|
|
119
|
+
|
|
120
|
+
`make build` puts the wheel and the sdist in `dist/`. Uploading them is not
|
|
121
|
+
wired into this repository, by hand or from CI.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 oberon-systems
|
|
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.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Developer entry points for penpot-local-stack. Run `make help` for the list.
|
|
2
|
+
|
|
3
|
+
VENV ?= .venv
|
|
4
|
+
PYTHON := $(VENV)/bin/python
|
|
5
|
+
PIP := $(VENV)/bin/pip
|
|
6
|
+
UV ?= uv
|
|
7
|
+
STACK := $(PYTHON) bin/penpot-stack
|
|
8
|
+
|
|
9
|
+
VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
|
|
10
|
+
|
|
11
|
+
# Hook environments live in the repository, not in ~/.cache/pre-commit.
|
|
12
|
+
export PRE_COMMIT_HOME := $(CURDIR)/.pre-commit
|
|
13
|
+
|
|
14
|
+
.DEFAULT_GOAL := shell
|
|
15
|
+
|
|
16
|
+
.PHONY: help install lint test shell up down import export extract convert
|
|
17
|
+
.PHONY: build clean require-uv
|
|
18
|
+
|
|
19
|
+
help: ## Show the current version and the available targets
|
|
20
|
+
@echo "penpot-local-stack $(VERSION)"
|
|
21
|
+
@echo
|
|
22
|
+
@echo "Targets:"
|
|
23
|
+
@awk 'BEGIN {FS = ":.*## "} /^[a-z-]+:.*## / {printf " %-9s %s\n", $$1, $$2}' \
|
|
24
|
+
$(MAKEFILE_LIST)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# development
|
|
28
|
+
install: ## Create the virtualenv, install everything and wire up the hooks
|
|
29
|
+
python3 -m venv --prompt penpot-local-stack $(VENV)
|
|
30
|
+
$(PIP) install --upgrade pip
|
|
31
|
+
$(PIP) install -r requirements.txt
|
|
32
|
+
$(VENV)/bin/pre-commit install
|
|
33
|
+
|
|
34
|
+
lint: ## Run the pre-commit hooks over every file
|
|
35
|
+
$(VENV)/bin/pre-commit run --all-files
|
|
36
|
+
|
|
37
|
+
test: ## Run the test suite
|
|
38
|
+
$(VENV)/bin/pytest
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# stack
|
|
42
|
+
up: ## Start the stack, import a template and open the browser logged in
|
|
43
|
+
$(STACK) up
|
|
44
|
+
|
|
45
|
+
down: ## Offer to export, then stop the stack and wipe everything it holds
|
|
46
|
+
$(STACK) down
|
|
47
|
+
|
|
48
|
+
import: ## Import a template from templates/ into the running stack
|
|
49
|
+
$(STACK) import
|
|
50
|
+
|
|
51
|
+
export: ## Export a file from the running stack back into templates/
|
|
52
|
+
$(STACK) export
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# archives
|
|
56
|
+
extract: ## Unpack a .penpot file from this directory into templates/
|
|
57
|
+
$(STACK) extract
|
|
58
|
+
|
|
59
|
+
convert: ## Pack a template from templates/ into a .penpot file
|
|
60
|
+
$(STACK) convert
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# packaging
|
|
64
|
+
build: require-uv clean ## Build the wheel and the sdist into dist/
|
|
65
|
+
$(UV) build
|
|
66
|
+
|
|
67
|
+
clean: ## Remove the build artifacts from dist/
|
|
68
|
+
rm -f dist/*.whl dist/*.tar.gz
|
|
69
|
+
|
|
70
|
+
require-uv:
|
|
71
|
+
@command -v $(UV) >/dev/null 2>&1 || { \
|
|
72
|
+
echo "$(UV) not found, install it first:" >&2; \
|
|
73
|
+
echo " https://docs.astral.sh/uv/getting-started/installation/" >&2; \
|
|
74
|
+
exit 1; \
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# defaults
|
|
79
|
+
shell: ## Open an interactive subshell with the virtualenv activated
|
|
80
|
+
@rc="$$(mktemp)"; \
|
|
81
|
+
trap 'rm -f "$$rc"' EXIT; \
|
|
82
|
+
cat ~/.bashrc 2> /dev/null > "$$rc" || true; \
|
|
83
|
+
echo 'source $(CURDIR)/$(VENV)/bin/activate' >> "$$rc"; \
|
|
84
|
+
bash --rcfile "$$rc" -i || true
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: penpot-local-stack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A disposable local Penpot stack: import a template, draw, export it back
|
|
5
|
+
Project-URL: Repository, https://github.com/oberon-systems/penpot-local-stack
|
|
6
|
+
Project-URL: Issues, https://github.com/oberon-systems/penpot-local-stack/issues
|
|
7
|
+
Author-email: zombig <me@zombig.name>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
21
|
+
Requires-Dist: pydantic>=2
|
|
22
|
+
Requires-Dist: pyyaml>=6
|
|
23
|
+
Requires-Dist: questionary>=2
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# penpot-local-stack
|
|
27
|
+
|
|
28
|
+
A disposable [Penpot](https://penpot.app) stack for drawing UI mockups, built
|
|
29
|
+
for AI-driven UI/UX work. Penpot itself is open source; its code is at
|
|
30
|
+
[penpot/penpot](https://github.com/penpot/penpot).
|
|
31
|
+
|
|
32
|
+
The stack exists so a design session leaves nothing behind but the drawing.
|
|
33
|
+
It runs from a Compose file with no persistent state: the database lives in
|
|
34
|
+
tmpfs and goes away with the containers, the profile is created on every
|
|
35
|
+
start, and the only thing that survives is `templates/` in your directory.
|
|
36
|
+
Mockups are stored there as unpacked Penpot exports, JSON plus SVG, so git
|
|
37
|
+
shows readable diffs and an agent can read a mockup directly instead of
|
|
38
|
+
looking at a picture of it.
|
|
39
|
+
|
|
40
|
+
- [Install](#install)
|
|
41
|
+
- [Settings](#settings)
|
|
42
|
+
- [Design loop](#design-loop)
|
|
43
|
+
- [Commands](#commands)
|
|
44
|
+
- [Templates](#templates)
|
|
45
|
+
- [Connect Penpot MCP](#connect-penpot-mcp)
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
Docker with the Compose plugin and Python 3.12 or newer have to be there
|
|
50
|
+
already, and that `python3` needs `httpx`, `questionary`, `pydantic-settings`
|
|
51
|
+
and `PyYAML`. The installer checks and tells you what is missing; it never
|
|
52
|
+
installs anything itself.
|
|
53
|
+
|
|
54
|
+
Run it in the directory that will hold the mockups:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
curl -fsSL https://raw.githubusercontent.com/oberon-systems/penpot-local-stack/main/install/install.sh | bash
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
It downloads the stack and lays it out:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
bin/ libs/ config/ the stack, run by the system python3
|
|
64
|
+
.penpot.yaml settings, read from the directory you run in
|
|
65
|
+
Makefile up, down, import, export, extract, convert
|
|
66
|
+
templates/ unpacked Penpot exports, empty at first
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Re-running it replaces `bin/`, `libs/` and `config/`, which is the upgrade,
|
|
70
|
+
and keeps `.penpot.yaml` and `templates/`. `--ref` unfolds a branch or a tag
|
|
71
|
+
instead of `main`. See [install/README.md](install/README.md) for the details.
|
|
72
|
+
|
|
73
|
+
## Settings
|
|
74
|
+
|
|
75
|
+
The installer copies `.penpot.yaml.example` from the repository to
|
|
76
|
+
`.penpot.yaml` next to the `Makefile`, and every command reads that file from
|
|
77
|
+
the directory it runs in:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
templates: templates
|
|
81
|
+
host: 127.0.0.1
|
|
82
|
+
port: 9001
|
|
83
|
+
project: penpot-local
|
|
84
|
+
version: 2.17.2
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Key | Default | What it sets |
|
|
88
|
+
| ----------- | ---------------------- | ------------------------------- |
|
|
89
|
+
| `templates` | `templates` | Where the unpacked exports live |
|
|
90
|
+
| `host` | `127.0.0.1` | The address Penpot binds to |
|
|
91
|
+
| `port` | `9001` | The port Penpot binds to |
|
|
92
|
+
| `project` | `penpot-local` | The Compose project name |
|
|
93
|
+
| `version` | `2.17.2` | The Penpot image tag |
|
|
94
|
+
| `email` | `designer@example.com` | The throwaway profile |
|
|
95
|
+
| `password` | `penpot-local` | Its password |
|
|
96
|
+
|
|
97
|
+
Drop a key to fall back to its default. Every key also answers to an
|
|
98
|
+
environment variable with a `PENPOT_` prefix, and the variable wins over the
|
|
99
|
+
file:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
PENPOT_PORT=9100 make up
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Design loop
|
|
106
|
+
|
|
107
|
+
Start the stack and pick a template or `(empty)`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
make up
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`up` creates a throwaway profile and opens the browser already logged in
|
|
114
|
+
through the printed `/autologin` link. If that session is lost, log in as
|
|
115
|
+
`designer@example.com` with the password `penpot-local`.
|
|
116
|
+
|
|
117
|
+
Draw in the browser at `http://localhost:9001`, or wherever `port` points. When you are done, pick the
|
|
118
|
+
file to export and the template to write it to, then let the stack go down:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
make down
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`down` writes the export into `templates/<name>/` and runs
|
|
125
|
+
`docker compose down -v`, which drops the database and the assets. Pick
|
|
126
|
+
`(skip export)` to throw the work away; the wipe is confirmed first whenever
|
|
127
|
+
the stack still holds a file.
|
|
128
|
+
|
|
129
|
+
## Commands
|
|
130
|
+
|
|
131
|
+
| Target | What it does |
|
|
132
|
+
| -------------- | --------------------------------------------------------- |
|
|
133
|
+
| `make up` | Start the stack, import a template, open the browser |
|
|
134
|
+
| `make down` | Offer the export, confirm the wipe, stop the stack |
|
|
135
|
+
| `make import` | Import a template into the running stack, after a confirm |
|
|
136
|
+
| `make export` | Export a file into `templates/`, after a confirm |
|
|
137
|
+
| `make extract` | Unpack a `.penpot` file into `templates/` |
|
|
138
|
+
| `make convert` | Pack a template into a `.penpot` file |
|
|
139
|
+
|
|
140
|
+
`import` and `export` work against a running stack, so a template can be
|
|
141
|
+
swapped in or a file saved off without ending the session. Both ask before
|
|
142
|
+
they touch anything: `import` names the template it is about to load, `export`
|
|
143
|
+
says whether it writes a new template or overwrites one that is already there.
|
|
144
|
+
|
|
145
|
+
`extract` and `convert` are the offline pair and need no stack at all.
|
|
146
|
+
`extract` lists the `.penpot` files next to you, asks which template to write
|
|
147
|
+
and unpacks the archive under the same rules as `export`: frame thumbnails are
|
|
148
|
+
dropped, JSON is reformatted, and an archive holding raster images is refused
|
|
149
|
+
with the offending files listed. `convert` goes the other way and packs
|
|
150
|
+
`templates/<name>/` into `<name>.penpot`, which is what you hand to a Penpot
|
|
151
|
+
that is not this one. Both confirm before writing, and say whether they are
|
|
152
|
+
creating something or overwriting it.
|
|
153
|
+
|
|
154
|
+
## Templates
|
|
155
|
+
|
|
156
|
+
- One directory per template: `manifest.json`, `files/` and `objects/`.
|
|
157
|
+
- Templates are read from `templates/` in the current directory. Set
|
|
158
|
+
`PENPOT_TEMPLATES` to read them from somewhere else.
|
|
159
|
+
- Images and icons must be SVG. An export that holds raster images is refused
|
|
160
|
+
and the offending files are listed, so you can replace them and export
|
|
161
|
+
again.
|
|
162
|
+
- Frame thumbnails are raster renders Penpot rebuilds on its own, so the
|
|
163
|
+
export leaves them out.
|
|
164
|
+
- Import keeps the Penpot file id, so a round trip changes only what you drew.
|
|
165
|
+
|
|
166
|
+
## Connect Penpot MCP
|
|
167
|
+
|
|
168
|
+
`up` enables MCP for the throwaway profile, issues its key and serves it
|
|
169
|
+
behind the fixed `/mcp/claude` URL, so an agent needs the server added only
|
|
170
|
+
once. Add it, then restart the agent:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
claude mcp add --transport http penpot http://localhost:9001/mcp/claude
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
After later `up` runs, reconnect `penpot` from `/mcp` instead of restarting.
|
|
177
|
+
While the stack is down, the server just shows as failed.
|
|
178
|
+
|
|
179
|
+
The MCP plugin runs inside the Penpot browser tab. Keep a file open in the
|
|
180
|
+
workspace while the agent works; a hidden or unloaded tab stops MCP.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# penpot-local-stack
|
|
2
|
+
|
|
3
|
+
A disposable [Penpot](https://penpot.app) stack for drawing UI mockups, built
|
|
4
|
+
for AI-driven UI/UX work. Penpot itself is open source; its code is at
|
|
5
|
+
[penpot/penpot](https://github.com/penpot/penpot).
|
|
6
|
+
|
|
7
|
+
The stack exists so a design session leaves nothing behind but the drawing.
|
|
8
|
+
It runs from a Compose file with no persistent state: the database lives in
|
|
9
|
+
tmpfs and goes away with the containers, the profile is created on every
|
|
10
|
+
start, and the only thing that survives is `templates/` in your directory.
|
|
11
|
+
Mockups are stored there as unpacked Penpot exports, JSON plus SVG, so git
|
|
12
|
+
shows readable diffs and an agent can read a mockup directly instead of
|
|
13
|
+
looking at a picture of it.
|
|
14
|
+
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Settings](#settings)
|
|
17
|
+
- [Design loop](#design-loop)
|
|
18
|
+
- [Commands](#commands)
|
|
19
|
+
- [Templates](#templates)
|
|
20
|
+
- [Connect Penpot MCP](#connect-penpot-mcp)
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
Docker with the Compose plugin and Python 3.12 or newer have to be there
|
|
25
|
+
already, and that `python3` needs `httpx`, `questionary`, `pydantic-settings`
|
|
26
|
+
and `PyYAML`. The installer checks and tells you what is missing; it never
|
|
27
|
+
installs anything itself.
|
|
28
|
+
|
|
29
|
+
Run it in the directory that will hold the mockups:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl -fsSL https://raw.githubusercontent.com/oberon-systems/penpot-local-stack/main/install/install.sh | bash
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It downloads the stack and lays it out:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
bin/ libs/ config/ the stack, run by the system python3
|
|
39
|
+
.penpot.yaml settings, read from the directory you run in
|
|
40
|
+
Makefile up, down, import, export, extract, convert
|
|
41
|
+
templates/ unpacked Penpot exports, empty at first
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Re-running it replaces `bin/`, `libs/` and `config/`, which is the upgrade,
|
|
45
|
+
and keeps `.penpot.yaml` and `templates/`. `--ref` unfolds a branch or a tag
|
|
46
|
+
instead of `main`. See [install/README.md](install/README.md) for the details.
|
|
47
|
+
|
|
48
|
+
## Settings
|
|
49
|
+
|
|
50
|
+
The installer copies `.penpot.yaml.example` from the repository to
|
|
51
|
+
`.penpot.yaml` next to the `Makefile`, and every command reads that file from
|
|
52
|
+
the directory it runs in:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
templates: templates
|
|
56
|
+
host: 127.0.0.1
|
|
57
|
+
port: 9001
|
|
58
|
+
project: penpot-local
|
|
59
|
+
version: 2.17.2
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Key | Default | What it sets |
|
|
63
|
+
| ----------- | ---------------------- | ------------------------------- |
|
|
64
|
+
| `templates` | `templates` | Where the unpacked exports live |
|
|
65
|
+
| `host` | `127.0.0.1` | The address Penpot binds to |
|
|
66
|
+
| `port` | `9001` | The port Penpot binds to |
|
|
67
|
+
| `project` | `penpot-local` | The Compose project name |
|
|
68
|
+
| `version` | `2.17.2` | The Penpot image tag |
|
|
69
|
+
| `email` | `designer@example.com` | The throwaway profile |
|
|
70
|
+
| `password` | `penpot-local` | Its password |
|
|
71
|
+
|
|
72
|
+
Drop a key to fall back to its default. Every key also answers to an
|
|
73
|
+
environment variable with a `PENPOT_` prefix, and the variable wins over the
|
|
74
|
+
file:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
PENPOT_PORT=9100 make up
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Design loop
|
|
81
|
+
|
|
82
|
+
Start the stack and pick a template or `(empty)`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
make up
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`up` creates a throwaway profile and opens the browser already logged in
|
|
89
|
+
through the printed `/autologin` link. If that session is lost, log in as
|
|
90
|
+
`designer@example.com` with the password `penpot-local`.
|
|
91
|
+
|
|
92
|
+
Draw in the browser at `http://localhost:9001`, or wherever `port` points. When you are done, pick the
|
|
93
|
+
file to export and the template to write it to, then let the stack go down:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
make down
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`down` writes the export into `templates/<name>/` and runs
|
|
100
|
+
`docker compose down -v`, which drops the database and the assets. Pick
|
|
101
|
+
`(skip export)` to throw the work away; the wipe is confirmed first whenever
|
|
102
|
+
the stack still holds a file.
|
|
103
|
+
|
|
104
|
+
## Commands
|
|
105
|
+
|
|
106
|
+
| Target | What it does |
|
|
107
|
+
| -------------- | --------------------------------------------------------- |
|
|
108
|
+
| `make up` | Start the stack, import a template, open the browser |
|
|
109
|
+
| `make down` | Offer the export, confirm the wipe, stop the stack |
|
|
110
|
+
| `make import` | Import a template into the running stack, after a confirm |
|
|
111
|
+
| `make export` | Export a file into `templates/`, after a confirm |
|
|
112
|
+
| `make extract` | Unpack a `.penpot` file into `templates/` |
|
|
113
|
+
| `make convert` | Pack a template into a `.penpot` file |
|
|
114
|
+
|
|
115
|
+
`import` and `export` work against a running stack, so a template can be
|
|
116
|
+
swapped in or a file saved off without ending the session. Both ask before
|
|
117
|
+
they touch anything: `import` names the template it is about to load, `export`
|
|
118
|
+
says whether it writes a new template or overwrites one that is already there.
|
|
119
|
+
|
|
120
|
+
`extract` and `convert` are the offline pair and need no stack at all.
|
|
121
|
+
`extract` lists the `.penpot` files next to you, asks which template to write
|
|
122
|
+
and unpacks the archive under the same rules as `export`: frame thumbnails are
|
|
123
|
+
dropped, JSON is reformatted, and an archive holding raster images is refused
|
|
124
|
+
with the offending files listed. `convert` goes the other way and packs
|
|
125
|
+
`templates/<name>/` into `<name>.penpot`, which is what you hand to a Penpot
|
|
126
|
+
that is not this one. Both confirm before writing, and say whether they are
|
|
127
|
+
creating something or overwriting it.
|
|
128
|
+
|
|
129
|
+
## Templates
|
|
130
|
+
|
|
131
|
+
- One directory per template: `manifest.json`, `files/` and `objects/`.
|
|
132
|
+
- Templates are read from `templates/` in the current directory. Set
|
|
133
|
+
`PENPOT_TEMPLATES` to read them from somewhere else.
|
|
134
|
+
- Images and icons must be SVG. An export that holds raster images is refused
|
|
135
|
+
and the offending files are listed, so you can replace them and export
|
|
136
|
+
again.
|
|
137
|
+
- Frame thumbnails are raster renders Penpot rebuilds on its own, so the
|
|
138
|
+
export leaves them out.
|
|
139
|
+
- Import keeps the Penpot file id, so a round trip changes only what you drew.
|
|
140
|
+
|
|
141
|
+
## Connect Penpot MCP
|
|
142
|
+
|
|
143
|
+
`up` enables MCP for the throwaway profile, issues its key and serves it
|
|
144
|
+
behind the fixed `/mcp/claude` URL, so an agent needs the server added only
|
|
145
|
+
once. Add it, then restart the agent:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
claude mcp add --transport http penpot http://localhost:9001/mcp/claude
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
After later `up` runs, reconnect `penpot` from `/mcp` instead of restarting.
|
|
152
|
+
While the stack is down, the server just shows as failed.
|
|
153
|
+
|
|
154
|
+
The MCP plugin runs inside the Penpot browser tab. Keep a file open in the
|
|
155
|
+
workspace while the agent works; a hidden or unloaded tab stops MCP.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run the stack from a checkout, without installing the package."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
8
|
+
|
|
9
|
+
from libs.cli import main # noqa: E402
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
main()
|