* fix(docker): support multi-platform builds and fix monorepo paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf(renderer): cache pre-rasterized paragraph images to avoid per-frame glyph rasterization   (#76)

* fix(canvas): stabilize frame label size during zoom

  Draw frame labels in screen-space after the viewport transform
  restore, converting scene coords manually. Previously fontSize=12/zoom
  fed into Math.ceil caused integer-boundary jumps that made labels
  flicker during zoom. Also skip shadow rendering while actively
  zooming for smoother performance.

* perf(renderer): cache pre-rasterized paragraph images to avoid per-frame glyph rasterization

   - Add paraImageCache (SkImage, 128 MB LRU limit) keyed on the same key as paraCache
   - Use drawImageRect instead of drawParagraph on cache hit, skipping per-frame glyph shaping and rasterization
   - Fall back to direct drawParagraph only when off-screen surface creation (MakeSurface) fails
   - Extract _dpr getter to deduplicate device-pixel-ratio resolution logic across draw paths
   - Evict oldest entries when cache exceeds byte limit; delete SkImage on eviction and dispose()

* feat(cli): introduce OpenPencil CLI for terminal control of the design tool

- Added a new CLI application under `apps/cli` to manage OpenPencil from the terminal.
- Implemented commands for app control (`start`, `stop`, `status`), document operations (`open`, `save`, `get`, `selection`), and design manipulation (`design`, `import`).
- Enhanced documentation with usage instructions and platform support details.
- Updated build scripts to include CLI compilation and publishing processes.
- Introduced a new GitHub Actions workflow for publishing the CLI to npm.
- Updated existing workflows to integrate CLI build steps and ensure proper versioning across packages.

* docs: update README files to include CLI tool details and multi-platform code export

- Added CLI section to README files in multiple languages, detailing commands for terminal control of the design tool.
- Included instructions for global installation and usage examples for the CLI.
- Expanded documentation on multi-platform code export capabilities from a single `.op` file to various frameworks.
- Updated CLAUDE.md to reference the new CLI documentation and its integration with the design tool.

* chore(bun.lock): update package dependencies to specific versions

- Removed workspace references for several packages in the bun.lock file.
- Updated dependencies for `@zseven-w/pen-core`, `@zseven-w/pen-types`, `@zseven-w/pen-codegen`, `@zseven-w/pen-figma`, and `@zseven-w/pen-renderer` to version `0.5.1-beta.1`.
- Ensured consistency in dependency management across the project.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: leinaldo <60176594+leinaldo@users.noreply.github.com>
This commit is contained in:
Kayshen Xu 2026-03-23 21:20:59 +08:00 committed by GitHub
parent 6c89c6dc9f
commit b4d1d2a7bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 5302 additions and 213 deletions

View file

@ -2,9 +2,9 @@ name: CI
on:
push:
branches: [main, v0.0.1]
branches: [main, 'v*']
pull_request:
branches: [main, v0.0.1]
branches: [main]
jobs:
lint-and-test:
@ -47,6 +47,9 @@ jobs:
- name: Build
run: bun --bun run build
- name: Build CLI
run: bun run cli:compile
- name: Upload web build artifact
uses: actions/upload-artifact@v4
with:

View file

@ -24,24 +24,31 @@ jobs:
- variant: base
target: base
suffix: ''
platforms: linux/amd64,linux/arm64
- variant: claude
target: with-claude
suffix: '-claude'
platforms: linux/amd64,linux/arm64
- variant: codex
target: with-codex
suffix: '-codex'
platforms: linux/amd64,linux/arm64
- variant: opencode
target: with-opencode
suffix: '-opencode'
platforms: linux/amd64,linux/arm64
- variant: copilot
target: with-copilot
suffix: '-copilot'
platforms: linux/amd64,linux/arm64
- variant: gemini
target: with-gemini
suffix: '-gemini'
platforms: linux/amd64,linux/arm64
- variant: full
target: full
suffix: '-full'
platforms: linux/amd64,linux/arm64
steps:
- uses: actions/checkout@v4
@ -65,6 +72,9 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@ -73,6 +83,7 @@ jobs:
with:
context: .
target: ${{ matrix.target }}
platforms: ${{ matrix.platforms }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

105
.github/workflows/publish-cli.yml vendored Normal file
View file

@ -0,0 +1,105 @@
name: Publish npm
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
publish:
name: Publish to npm
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Get version
id: version
run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
- name: Replace workspace:* with version
run: |
VERSION=${{ steps.version.outputs.version }}
for f in packages/*/package.json apps/cli/package.json; do
if [ -f "$f" ]; then
jq --arg v "$VERSION" '
if .dependencies then
.dependencies |= with_entries(
if .value == "workspace:*" then .value = $v else . end
)
else . end |
if .devDependencies then
.devDependencies |= with_entries(
if .value == "workspace:*" then .value = $v else . end
)
else . end
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
echo "Updated $f"
fi
done
- name: Compile CLI
run: bun run cli:compile
- name: Verify CLI build
run: node apps/cli/dist/openpencil-cli.cjs --version
# Publish in topological order
- name: Publish pen-types
run: npm publish --access public || true
working-directory: packages/pen-types
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish pen-core
run: npm publish --access public || true
working-directory: packages/pen-core
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish pen-codegen
run: npm publish --access public || true
working-directory: packages/pen-codegen
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish pen-figma
run: npm publish --access public || true
working-directory: packages/pen-figma
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish pen-renderer
run: npm publish --access public || true
working-directory: packages/pen-renderer
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish pen-sdk
run: npm publish --access public || true
working-directory: packages/pen-sdk
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish CLI
run: npm publish --access public || true
working-directory: apps/cli
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -1,7 +1,7 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Detailed module docs are in `packages/CLAUDE.md`, `apps/web/CLAUDE.md`, and `apps/desktop/CLAUDE.md` — loaded automatically when working in those directories.
Detailed module docs are in `packages/CLAUDE.md`, `apps/web/CLAUDE.md`, `apps/desktop/CLAUDE.md`, and `apps/cli/CLAUDE.md` — loaded automatically when working in those directories.
## Commands
@ -16,6 +16,9 @@ Detailed module docs are in `packages/CLAUDE.md`, `apps/web/CLAUDE.md`, and `app
- **Electron dev:** `bun run electron:dev` (starts Vite + Electron together)
- **Electron compile:** `bun run electron:compile` (esbuild electron/ to out/desktop/)
- **Electron build:** `bun run electron:build` (full web build + compile + electron-builder package)
- **CLI compile:** `bun run cli:compile` (esbuild CLI to apps/cli/dist/)
- **CLI dev:** `bun run cli:dev` (run CLI from source via Bun)
- **Publish beta:** `bun run publish:beta [N]` (publish all npm packages with beta tag)
## Architecture
@ -25,7 +28,8 @@ OpenPencil is an open-source vector design tool (alternative to Pencil.dev) with
openpencil/
├── apps/
│ ├── web/ TanStack Start full-stack React app (Vite + Nitro)
│ └── desktop/ Electron desktop app (macOS, Windows, Linux)
│ ├── desktop/ Electron desktop app (macOS, Windows, Linux)
│ └── cli/ CLI tool — control the design tool from the terminal
├── packages/
│ ├── pen-types/ Type definitions for PenDocument model
│ ├── pen-core/ Document tree ops, layout engine, variables, boolean ops, clone utilities
@ -33,6 +37,7 @@ openpencil/
│ ├── pen-figma/ Figma .fig file parser and converter
│ ├── pen-renderer/ Standalone CanvasKit/Skia renderer
│ └── pen-sdk/ Umbrella SDK (re-exports all packages)
├── scripts/ Build and publish scripts
└── .githooks/ Pre-commit version sync from branch name
```
@ -93,10 +98,22 @@ External LLMs (Claude Code, Codex, Gemini CLI, etc.) can generate designs via MC
Tailwind CSS v4 imported via `apps/web/src/styles.css`. UI primitives from shadcn/ui. Icons from `lucide-react`.
### CLI (`apps/cli/`)
The `op` command-line tool controls the desktop app or web server from the terminal. Arguments that accept JSON or DSL support three input methods: inline string, `@filepath` (read from file), or `-` (read from stdin).
- **App control:** `op start [--desktop|--web]`, `op stop`, `op status`
- **Design:** `op design <dsl|@file|->` — batch design DSL operations
- **Document:** `op open`, `op save`, `op get`, `op selection`
- **Nodes:** `op insert`, `op update`, `op delete`, `op move`, `op copy`, `op replace`
- **Export:** `op export <react|html|vue|svelte|flutter|swiftui|compose|rn|css>`
- **Cross-platform:** macOS, Windows (NSIS/portable), Linux (AppImage/deb/snap/flatpak)
### CI / CD
- **`.github/workflows/ci.yml`** — Push/PR: type check, tests, web build
- **`.github/workflows/ci.yml`** — Push/PR on `main` and `v*` branches: type check, tests, web build
- **`.github/workflows/build-electron.yml`** — Tag push (`v*`) or manual: builds Electron for all platforms, creates draft GitHub Release
- **`.github/workflows/publish-cli.yml`** — Tag push (`v*`) or manual: publishes all `@zseven-w/*` npm packages in topological order
- **`.github/workflows/docker.yml`** — Docker image build and push
### Version Sync
@ -119,7 +136,7 @@ Use [Conventional Commits](https://www.conventionalcommits.org/) format: `<type>
**Types:** `feat`, `fix`, `refactor`, `perf`, `style`, `docs`, `test`, `chore`
**Scopes:** `editor`, `canvas`, `panels`, `history`, `ai`, `codegen`, `store`, `types`, `variables`, `figma`, `mcp`, `electron`, `renderer`, `sdk`
**Scopes:** `editor`, `canvas`, `panels`, `history`, `ai`, `codegen`, `store`, `types`, `variables`, `figma`, `mcp`, `electron`, `renderer`, `sdk`, `cli`
**Rules:** Subject in English, lowercase start, no period, imperative mood. Body is optional; explain **why** not what. One commit per change.

View file

@ -80,6 +80,22 @@ Ein-Klick-Installation in Claude Code, Codex, Gemini, OpenCode, Kiro oder Copilo
Web-App + native Desktop-Anwendung auf macOS, Windows und Linux über Electron. Auto-Updates über GitHub Releases. `.op`-Dateizuordnung — Doppelklick zum Öffnen.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Steuern Sie das Design-Tool vom Terminal aus. `op design`, `op insert`, `op export` — Batch-Design-DSL, Knotenmanipulation, Code-Export. Pipe-Eingabe von Dateien oder stdin. Funktioniert mit der Desktop-App oder dem Webserver.
</td>
<td width="50%">
### 🎯 Multiplattform-Code-Export
Export aus einer einzigen `.op`-Datei nach React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Design-Variablen werden zu CSS Custom Properties.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Global installieren und das Design-Tool vom Terminal aus steuern:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Desktop-App starten
op design @landing.txt # Batch-Design aus Datei
op insert '{"type":"RECT"}' # Knoten einfügen
op export react --out . # Nach React + Tailwind exportieren
op import:figma design.fig # Figma-Datei importieren
cat design.dsl | op design - # Pipe von stdin
```
Unterstützt drei Eingabemethoden: Inline-String, `@filepath` (aus Datei lesen) oder `-` (von stdin lesen). Funktioniert mit der Desktop-App oder dem Web-Entwicklungsserver. Siehe [CLI README](./apps/cli/README.md) für die vollständige Befehlsreferenz.
## Funktionen
**Canvas und Zeichnen**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — Terminal-Steuerung, Batch-Design-DSL, Code-Export |
| **KI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Laufzeit** | Bun · Vite 7 |
| **Dateiformat** | `.op` — JSON-basiert, menschenlesbar, Git-freundlich |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro-API — Streaming-Chat, Generierung, Validierung
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot-Wrapper
│ └── desktop/ Electron-Desktop-App
│ ├── main.ts Fenster, Nitro-Fork, natives Menü, Auto-Updater
│ ├── ipc-handlers.ts Native Dateidialoge, Theme-Sync, Einstellungen-IPC
│ └── preload.ts IPC-Brücke
│ ├── desktop/ Electron-Desktop-App
│ │ ├── main.ts Fenster, Nitro-Fork, natives Menü, Auto-Updater
│ │ ├── ipc-handlers.ts Native Dateidialoge, Theme-Sync, Einstellungen-IPC
│ │ └── preload.ts IPC-Brücke
│ └── cli/ CLI-Tool — `op`-Befehl
│ ├── src/commands/ Design-, Dokument-, Export-, Import-, Knoten-, Seiten-, Variablen-Befehle
│ ├── connection.ts WebSocket-Verbindung zur laufenden App
│ └── launcher.ts Automatische Erkennung und Start der Desktop-App oder des Webservers
├── packages/
│ ├── pen-types/ Typdefinitionen für das PenDocument-Modell
│ ├── pen-core/ Dokumentbaum-Operationen, Layout-Engine, Variablen
@ -281,6 +321,8 @@ npx tsc --noEmit # Typprüfung
bun run bump <version> # Version über alle package.json synchronisieren
bun run electron:dev # Electron-Entwicklung
bun run electron:build # Electron-Paketierung
bun run cli:dev # CLI aus Quellcode ausführen
bun run cli:compile # CLI nach dist kompilieren
```
## Mitwirken
@ -305,6 +347,7 @@ Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetail
- [x] Boolesche Operationen (Vereinigung, Subtraktion, Schnittmenge)
- [x] Multi-Modell-Fähigkeitsprofile
- [x] Monorepo-Umstrukturierung mit wiederverwendbaren Paketen
- [x] CLI-Tool (`op`) für Terminal-Steuerung
- [ ] Kollaboratives Bearbeiten
- [ ] Plugin-System

View file

@ -80,6 +80,22 @@ Los archivos `.op` son JSON — legibles por humanos, compatibles con Git, compa
Aplicación web + escritorio nativo en macOS, Windows y Linux mediante Electron. Actualizaciones automáticas desde GitHub Releases. Asociación de archivos `.op` — doble clic para abrir.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Controla la herramienta de diseño desde la terminal. `op design`, `op insert`, `op export` — DSL de diseño por lotes, manipulación de nodos, exportación de código. Entrada por pipe desde archivos o stdin. Funciona con la app de escritorio o el servidor web.
</td>
<td width="50%">
### 🎯 Exportación de Código Multiplataforma
Exporta desde un solo archivo `.op` a React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Las variables de diseño se convierten en propiedades CSS personalizadas.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Instala globalmente y controla la herramienta de diseño desde tu terminal:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Iniciar la app de escritorio
op design @landing.txt # Diseño por lotes desde archivo
op insert '{"type":"RECT"}' # Insertar un nodo
op export react --out . # Exportar a React + Tailwind
op import:figma design.fig # Importar archivo de Figma
cat design.dsl | op design - # Entrada por pipe desde stdin
```
Soporta tres métodos de entrada: cadena inline, `@filepath` (leer desde archivo), o `-` (leer desde stdin). Funciona con la app de escritorio o el servidor de desarrollo web. Consulta el [README del CLI](./apps/cli/README.md) para la referencia completa de comandos.
## Características
**Lienzo y Dibujo**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **Estado** | Zustand v5 |
| **Servidor** | Nitro |
| **Escritorio** | Electron 35 |
| **CLI** | `op` — control desde terminal, DSL de diseño por lotes, exportación de código |
| **IA** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Formato de archivo** | `.op` — basado en JSON, legible por humanos, compatible con Git |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ API Nitro — chat en streaming, generación, validación
│ │ └── utils/ Wrappers de Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Aplicación de escritorio Electron
│ ├── main.ts Ventana, fork Nitro, menú nativo, actualizador automático
│ ├── ipc-handlers.ts Diálogos de archivos nativos, sincronización de tema, preferencias IPC
│ └── preload.ts Puente IPC
│ ├── desktop/ Aplicación de escritorio Electron
│ │ ├── main.ts Ventana, fork Nitro, menú nativo, actualizador automático
│ │ ├── ipc-handlers.ts Diálogos de archivos nativos, sincronización de tema, preferencias IPC
│ │ └── preload.ts Puente IPC
│ └── cli/ Herramienta CLI — comando `op`
│ ├── src/commands/ Comandos de diseño, documento, exportación, importación, nodo, página, variable
│ ├── connection.ts Conexión WebSocket a la app en ejecución
│ └── launcher.ts Auto-detección e inicio de la app de escritorio o servidor web
├── packages/
│ ├── pen-types/ Definiciones de tipos para el modelo PenDocument
│ ├── pen-core/ Operaciones de árbol del documento, motor de diseño, variables
@ -281,6 +321,8 @@ npx tsc --noEmit # Verificación de tipos
bun run bump <version> # Sincronizar versión en todos los package.json
bun run electron:dev # Desarrollo con Electron
bun run electron:build # Empaquetado de Electron
bun run cli:dev # Ejecutar CLI desde el código fuente
bun run cli:compile # Compilar CLI a dist
```
## Contribuir
@ -305,6 +347,7 @@ bun run electron:build # Empaquetado de Electron
- [x] Operaciones booleanas (unión, sustracción, intersección)
- [x] Perfiles de capacidad multimodelo
- [x] Reestructuración en monorepo con paquetes reutilizables
- [x] Herramienta CLI (`op`) para control desde terminal
- [ ] Edición colaborativa
- [ ] Sistema de plugins

View file

@ -80,6 +80,22 @@ Les fichiers `.op` sont du JSON — lisibles par l'humain, compatibles Git, comp
Application web + bureau natif sur macOS, Windows et Linux via Electron. Mises à jour automatiques depuis GitHub Releases. Association de fichiers `.op` — double-cliquez pour ouvrir.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Contrôlez l'outil de design depuis le terminal. `op design`, `op insert`, `op export` — DSL de design par lots, manipulation de nœuds, export de code. Entrée par pipe depuis des fichiers ou stdin. Fonctionne avec l'app de bureau ou le serveur web.
</td>
<td width="50%">
### 🎯 Export de Code Multi-Plateforme
Exportez depuis un seul fichier `.op` vers React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Les variables de design deviennent des propriétés CSS personnalisées.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Installez globalement et contrôlez l'outil de design depuis votre terminal :
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Lancer l'app de bureau
op design @landing.txt # Design par lots depuis un fichier
op insert '{"type":"RECT"}' # Insérer un nœud
op export react --out . # Exporter en React + Tailwind
op import:figma design.fig # Importer un fichier Figma
cat design.dsl | op design - # Pipe depuis stdin
```
Supporte trois méthodes d'entrée : chaîne en ligne, `@filepath` (lecture depuis un fichier), ou `-` (lecture depuis stdin). Fonctionne avec l'app de bureau ou le serveur de développement web. Voir le [README du CLI](./apps/cli/README.md) pour la référence complète des commandes.
## Fonctionnalités
**Canevas et dessin**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **État** | Zustand v5 |
| **Serveur** | Nitro |
| **Bureau** | Electron 35 |
| **CLI** | `op` — contrôle depuis le terminal, DSL de design par lots, export de code |
| **IA** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Format de fichier** | `.op` — basé sur JSON, lisible par l'humain, compatible Git |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ API Nitro — chat en streaming, génération, validation
│ │ └── utils/ Enveloppes Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Application de bureau Electron
│ ├── main.ts Fenêtre, fork Nitro, menu natif, mise à jour automatique
│ ├── ipc-handlers.ts Dialogues fichiers natifs, sync thème, préférences IPC
│ └── preload.ts Pont IPC
│ ├── desktop/ Application de bureau Electron
│ │ ├── main.ts Fenêtre, fork Nitro, menu natif, mise à jour automatique
│ │ ├── ipc-handlers.ts Dialogues fichiers natifs, sync thème, préférences IPC
│ │ └── preload.ts Pont IPC
│ └── cli/ Outil CLI — commande `op`
│ ├── src/commands/ Commandes design, document, export, import, nœud, page, variable
│ ├── connection.ts Connexion WebSocket à l'app en cours d'exécution
│ └── launcher.ts Détection automatique et lancement de l'app de bureau ou du serveur web
├── packages/
│ ├── pen-types/ Définitions de types pour le modèle PenDocument
│ ├── pen-core/ Opérations sur l'arbre du document, moteur de mise en page, variables
@ -281,6 +321,8 @@ npx tsc --noEmit # Vérification des types
bun run bump <version> # Synchroniser la version dans tous les package.json
bun run electron:dev # Développement Electron
bun run electron:build # Packaging Electron
bun run cli:dev # Exécuter le CLI depuis les sources
bun run cli:compile # Compiler le CLI vers dist
```
## Contribuer
@ -305,6 +347,7 @@ Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour
- [x] Opérations booléennes (union, soustraction, intersection)
- [x] Profils de capacités multi-modèles
- [x] Restructuration en monorepo avec packages réutilisables
- [x] Outil CLI (`op`) pour le contrôle depuis le terminal
- [ ] Édition collaborative
- [ ] Système de plugins

View file

@ -80,6 +80,22 @@ Claude Code, Codex, Gemini, OpenCode, Kiro, या Copilot CLIs में वन
वेब ऐप + Electron के ज़रिए macOS, Windows और Linux पर नेटिव डेस्कटॉप। GitHub Releases से ऑटो-अपडेट। `.op` फ़ाइल एसोसिएशन — डबल-क्लिक से खोलें।
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें। `op design`, `op insert`, `op export` — बैच डिज़ाइन DSL, नोड मैनिपुलेशन, कोड एक्सपोर्ट। फ़ाइलों या stdin से पाइप करें। डेस्कटॉप ऐप या वेब सर्वर के साथ काम करता है।
</td>
<td width="50%">
### 🎯 मल्टी-प्लेटफ़ॉर्म कोड एक्सपोर्ट
एक `.op` फ़ाइल से React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native में एक्सपोर्ट करें। डिज़ाइन वेरिएबल CSS कस्टम प्रॉपर्टीज़ बन जाते हैं।
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
वैश्विक रूप से इंस्टॉल करें और अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # डेस्कटॉप ऐप लॉन्च करें
op design @landing.txt # फ़ाइल से बैच डिज़ाइन
op insert '{"type":"RECT"}' # एक नोड डालें
op export react --out . # React + Tailwind में एक्सपोर्ट
op import:figma design.fig # Figma फ़ाइल इम्पोर्ट करें
cat design.dsl | op design - # stdin से पाइप करें
```
तीन इनपुट विधियाँ समर्थित हैं: इनलाइन स्ट्रिंग, `@filepath` (फ़ाइल से पढ़ें), या `-` (stdin से पढ़ें)। डेस्कटॉप ऐप या वेब डेव सर्वर के साथ काम करता है। पूर्ण कमांड संदर्भ के लिए [CLI README](./apps/cli/README.md) देखें।
## विशेषताएँ
**कैनवास और ड्रॉइंग**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **स्टेट** | Zustand v5 |
| **सर्वर** | Nitro |
| **डेस्कटॉप** | Electron 35 |
| **CLI** | `op` — टर्मिनल नियंत्रण, बैच डिज़ाइन DSL, कोड एक्सपोर्ट |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **रनटाइम** | Bun · Vite 7 |
| **फ़ाइल फ़ॉर्मेट** | `.op` — JSON-आधारित, मानव-पठनीय, Git-फ्रेंडली |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — स्ट्रीमिंग चैट, जनरेशन, वैलिडेशन
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot रैपर
│ └── desktop/ Electron डेस्कटॉप ऐप
│ ├── main.ts विंडो, Nitro फ़ोर्क, नेटिव मेनू, ऑटो-अपडेटर
│ ├── ipc-handlers.ts नेटिव फ़ाइल डायलॉग, थीम सिंक, प्राथमिकताएँ IPC
│ └── preload.ts IPC ब्रिज
│ ├── desktop/ Electron डेस्कटॉप ऐप
│ │ ├── main.ts विंडो, Nitro फ़ोर्क, नेटिव मेनू, ऑटो-अपडेटर
│ │ ├── ipc-handlers.ts नेटिव फ़ाइल डायलॉग, थीम सिंक, प्राथमिकताएँ IPC
│ │ └── preload.ts IPC ब्रिज
│ └── cli/ CLI टूल — `op` कमांड
│ ├── src/commands/ डिज़ाइन, दस्तावेज़, एक्सपोर्ट, इम्पोर्ट, नोड, पेज, वेरिएबल कमांड
│ ├── connection.ts चालू ऐप से WebSocket कनेक्शन
│ └── launcher.ts डेस्कटॉप ऐप या वेब सर्वर का स्वचालित पता लगाना और लॉन्च
├── packages/
│ ├── pen-types/ PenDocument मॉडल के लिए टाइप परिभाषाएँ
│ ├── pen-core/ दस्तावेज़ ट्री ऑपरेशन, लेआउट इंजन, वेरिएबल
@ -281,6 +321,8 @@ npx tsc --noEmit # टाइप चेक
bun run bump <version> # सभी package.json में वर्शन सिंक करें
bun run electron:dev # Electron डेव
bun run electron:build # Electron पैकेज
bun run cli:dev # सोर्स से CLI चलाएँ
bun run cli:compile # CLI को dist में कंपाइल करें
```
## योगदान
@ -305,6 +347,7 @@ bun run electron:build # Electron पैकेज
- [x] बूलियन ऑपरेशन (यूनियन, सबट्रैक्ट, इंटरसेक्ट)
- [x] मल्टी-मॉडल क्षमता प्रोफ़ाइल
- [x] पुन: उपयोगी पैकेज के साथ मोनोरेपो पुनर्गठन
- [x] CLI टूल (`op`) टर्मिनल नियंत्रण
- [ ] सहयोगी संपादन
- [ ] प्लगइन सिस्टम

View file

@ -80,6 +80,22 @@ File `.op` adalah JSON — mudah dibaca manusia, ramah Git, mudah dibandingkan.
Aplikasi web + desktop native di macOS, Windows, dan Linux melalui Electron. Pembaruan otomatis dari GitHub Releases. Asosiasi file `.op` — klik dua kali untuk membuka.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Kontrol alat desain dari terminal Anda. `op design`, `op insert`, `op export` — batch design DSL, manipulasi node, ekspor kode. Pipe dari file atau stdin. Bekerja dengan aplikasi desktop atau web server.
</td>
<td width="50%">
### 🎯 Ekspor Kode Multi-Platform
Ekspor dari satu file `.op` ke React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Variabel desain menjadi CSS custom properties.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Instal secara global dan kontrol alat desain dari terminal Anda:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Jalankan aplikasi desktop
op design @landing.txt # Desain batch dari file
op insert '{"type":"RECT"}' # Sisipkan sebuah node
op export react --out . # Ekspor ke React + Tailwind
op import:figma design.fig # Impor file Figma
cat design.dsl | op design - # Pipe dari stdin
```
Mendukung tiga metode input: string inline, `@filepath` (baca dari file), atau `-` (baca dari stdin). Bekerja dengan aplikasi desktop atau web dev server. Lihat [CLI README](./apps/cli/README.md) untuk referensi perintah lengkap.
## Fitur
**Kanvas & Menggambar**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — kontrol terminal, batch design DSL, ekspor kode |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Format file** | `.op` — berbasis JSON, mudah dibaca manusia, ramah Git |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — chat streaming, pembuatan, validasi
│ │ └── utils/ Pembungkus Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Aplikasi desktop Electron
│ ├── main.ts Jendela, fork Nitro, menu native, pembaruan otomatis
│ ├── ipc-handlers.ts Dialog file native, sinkronisasi tema, preferensi IPC
│ └── preload.ts Jembatan IPC
│ ├── desktop/ Aplikasi desktop Electron
│ │ ├── main.ts Jendela, fork Nitro, menu native, pembaruan otomatis
│ │ ├── ipc-handlers.ts Dialog file native, sinkronisasi tema, preferensi IPC
│ │ └── preload.ts Jembatan IPC
│ └── cli/ Alat CLI — perintah `op`
│ ├── src/commands/ Perintah design, document, export, import, node, page, variable
│ ├── connection.ts Koneksi WebSocket ke aplikasi yang berjalan
│ └── launcher.ts Deteksi otomatis dan jalankan aplikasi desktop atau web server
├── packages/
│ ├── pen-types/ Definisi tipe untuk model PenDocument
│ ├── pen-core/ Operasi pohon dokumen, mesin tata letak, variabel
@ -281,6 +321,8 @@ npx tsc --noEmit # Pemeriksaan tipe
bun run bump <version> # Sinkronisasi versi di semua package.json
bun run electron:dev # Pengembangan Electron
bun run electron:build # Paket Electron
bun run cli:dev # Jalankan CLI dari sumber
bun run cli:compile # Kompilasi CLI ke dist
```
## Berkontribusi
@ -305,6 +347,7 @@ Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitekt
- [x] Operasi boolean (gabung, kurangi, potong)
- [x] Profil kemampuan multi-model
- [x] Restrukturisasi monorepo dengan paket yang dapat digunakan ulang
- [x] Alat CLI (`op`) kontrol terminal
- [ ] Pengeditan kolaboratif
- [ ] Sistem plugin

View file

@ -80,6 +80,22 @@ Claude Code、Codex、Gemini、OpenCode、Kiro、Copilot CLI にワンクリッ
Web アプリ + Electron による macOS・Windows・Linux ネイティブデスクトップ。GitHub Releases からの自動アップデート。`.op` ファイル関連付け — ダブルクリックで開く。
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
ターミナルからデザインツールを操作。`op design`、`op insert`、`op export` — バッチデザインDSL、ード操作、コードエクスポート。ファイルやstdinからのパイプ入力に対応。デスクトップアプリまたはWebサーバーと連携。
</td>
<td width="50%">
### 🎯 マルチプラットフォームコードエクスポート
1つの`.op`ファイルからReact + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Nativeへエクスポート。デザイン変数はCSSカスタムプロパティに変換。
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS、HTML + CSS、CSS Variables
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
## CLI — `op`
グローバルインストールしてターミナルからデザインツールを操作:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # デスクトップアプリを起動
op design @landing.txt # ファイルからバッチデザイン
op insert '{"type":"RECT"}' # ノードを挿入
op export react --out . # React + Tailwind にエクスポート
op import:figma design.fig # Figma ファイルをインポート
cat design.dsl | op design - # stdin からパイプ入力
```
3つの入力方法に対応インライン文字列、`@filepath`(ファイルから読み込み)、`-`stdin から読み込み)。デスクトップアプリまたは Web 開発サーバーと連携。完全なコマンドリファレンスは [CLI README](./apps/cli/README.md) を参照。
## 機能
**キャンバスと描画**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **状態管理** | Zustand v5 |
| **サーバー** | Nitro |
| **デスクトップ** | Electron 35 |
| **CLI** | `op` — ターミナル制御、バッチデザインDSL、コードエクスポート |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **ランタイム** | Bun · Vite 7 |
| **ファイル形式** | `.op` — JSON ベース、人間が読みやすく、Git フレンドリー |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — ストリーミングチャット、生成、バリデーション
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot ラッパー
│ └── desktop/ Electron デスクトップアプリ
│ ├── main.ts ウィンドウ、Nitro フォーク、ネイティブメニュー、自動アップデーター
│ ├── ipc-handlers.ts ネイティブファイルダイアログ、テーマ同期、設定 IPC
│ └── preload.ts IPC ブリッジ
│ ├── desktop/ Electron デスクトップアプリ
│ │ ├── main.ts ウィンドウ、Nitro フォーク、ネイティブメニュー、自動アップデーター
│ │ ├── ipc-handlers.ts ネイティブファイルダイアログ、テーマ同期、設定 IPC
│ │ └── preload.ts IPC ブリッジ
│ └── cli/ CLIツール — `op` コマンド
│ ├── src/commands/ デザイン、ドキュメント、エクスポート、インポート、ノード、ページ、変数コマンド
│ ├── connection.ts 実行中アプリへのWebSocket接続
│ └── launcher.ts デスクトップアプリまたはWebサーバーの自動検出・起動
├── packages/
│ ├── pen-types/ PenDocument モデルの型定義
│ ├── pen-core/ ドキュメントツリー操作、レイアウトエンジン、変数
@ -281,6 +321,8 @@ npx tsc --noEmit # 型チェック
bun run bump <version> # すべての package.json のバージョンを同期
bun run electron:dev # Electron 開発モード
bun run electron:build # Electron パッケージング
bun run cli:dev # ソースから CLI を実行
bun run cli:compile # CLI を dist にコンパイル
```
## コントリビュート
@ -305,6 +347,7 @@ bun run electron:build # Electron パッケージング
- [x] ブーリアン演算(合体、型抜き、交差)
- [x] マルチモデル能力プロファイル
- [x] 再利用可能なパッケージによるモノレポ構成
- [x] CLIツール`op`)ターミナル制御
- [ ] 共同編集
- [ ] プラグインシステム

View file

@ -80,6 +80,22 @@ Claude Code, Codex, Gemini, OpenCode, Kiro 또는 Copilot CLI에 원클릭 설
웹 앱 + Electron을 통한 macOS, Windows, Linux 네이티브 데스크톱. GitHub Releases에서 자동 업데이트. `.op` 파일 연결 — 더블 클릭으로 열기.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
터미널에서 디자인 도구 제어. `op design`, `op insert`, `op export` — 배치 디자인 DSL, 노드 조작, 코드 내보내기. 파일이나 stdin에서 파이프 입력 지원. 데스크톱 앱 또는 웹 서버와 연동.
</td>
<td width="50%">
### 🎯 멀티 플랫폼 코드 내보내기
하나의 `.op` 파일에서 React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native로 내보내기. 디자인 변수는 CSS 커스텀 프로퍼티로 변환.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
전역 설치 후 터미널에서 디자인 도구를 제어하세요:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # 데스크톱 앱 실행
op design @landing.txt # 파일에서 배치 디자인
op insert '{"type":"RECT"}' # 노드 삽입
op export react --out . # React + Tailwind로 내보내기
op import:figma design.fig # Figma 파일 가져오기
cat design.dsl | op design - # stdin에서 파이프 입력
```
세 가지 입력 방식을 지원합니다: 인라인 문자열, `@filepath` (파일에서 읽기), `-` (stdin에서 읽기). 데스크톱 앱 또는 웹 개발 서버와 연동됩니다. 전체 명령어 레퍼런스는 [CLI README](./apps/cli/README.md)를 참고하세요.
## 기능
**캔버스 & 드로잉**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **상태 관리** | Zustand v5 |
| **서버** | Nitro |
| **데스크톱** | Electron 35 |
| **CLI** | `op` — 터미널 제어, 배치 디자인 DSL, 코드 내보내기 |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **런타임** | Bun · Vite 7 |
| **파일 형식** | `.op` — JSON 기반, 사람이 읽을 수 있는, Git 친화적 |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — 스트리밍 채팅, 생성, 유효성 검사
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot 래퍼
│ └── desktop/ Electron 데스크톱 앱
│ ├── main.ts 윈도우, Nitro 포크, 네이티브 메뉴, 자동 업데이터
│ ├── ipc-handlers.ts 네이티브 파일 대화상자, 테마 동기화, 환경설정 IPC
│ └── preload.ts IPC 브리지
│ ├── desktop/ Electron 데스크톱 앱
│ │ ├── main.ts 윈도우, Nitro 포크, 네이티브 메뉴, 자동 업데이터
│ │ ├── ipc-handlers.ts 네이티브 파일 대화상자, 테마 동기화, 환경설정 IPC
│ │ └── preload.ts IPC 브리지
│ └── cli/ CLI 도구 — `op` 명령어
│ ├── src/commands/ 디자인, 문서, 내보내기, 가져오기, 노드, 페이지, 변수 명령어
│ ├── connection.ts 실행 중인 앱과의 WebSocket 연결
│ └── launcher.ts 데스크톱 앱 또는 웹 서버 자동 감지 및 실행
├── packages/
│ ├── pen-types/ PenDocument 모델 타입 정의
│ ├── pen-core/ 문서 트리 연산, 레이아웃 엔진, 변수
@ -281,6 +321,8 @@ npx tsc --noEmit # 타입 검사
bun run bump <version> # 모든 package.json에 버전 동기화
bun run electron:dev # Electron 개발 모드
bun run electron:build # Electron 패키징
bun run cli:dev # 소스에서 CLI 실행
bun run cli:compile # CLI를 dist로 컴파일
```
## 기여하기
@ -305,6 +347,7 @@ bun run electron:build # Electron 패키징
- [x] 불리언 연산 (합치기, 빼기, 교차)
- [x] 멀티 모델 역량 프로파일
- [x] 재사용 가능한 패키지를 포함한 모노레포 구조 변경
- [x] CLI 도구 (`op`) 터미널 제어
- [ ] 공동 편집
- [ ] 플러그인 시스템

View file

@ -80,6 +80,22 @@ One-click install into Claude Code, Codex, Gemini, OpenCode, Kiro, or Copilot CL
Web app + native desktop on macOS, Windows, and Linux via Electron. Auto-updates from GitHub Releases. `.op` file association — double-click to open.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Control the design tool from your terminal. `op design`, `op insert`, `op export` — batch design DSL, node manipulation, code export. Pipe in from files or stdin. Works with desktop app or web server.
</td>
<td width="50%">
### 🎯 Multi-Platform Code Export
Export to React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native — all from one `.op` file. Design variables become CSS custom properties.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Install globally and control the design tool from your terminal:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Launch desktop app
op design @landing.txt # Batch design from file
op insert '{"type":"RECT"}' # Insert a node
op export react --out . # Export to React + Tailwind
op import:figma design.fig # Import Figma file
cat design.dsl | op design - # Pipe from stdin
```
Supports three input methods: inline string, `@filepath` (read from file), or `-` (read from stdin). Works with desktop app or web dev server. See [CLI README](./apps/cli/README.md) for full command reference.
## Features
**Canvas & Drawing**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — terminal control, batch design DSL, code export |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **File format** | `.op` — JSON-based, human-readable, Git-friendly |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
│ └── desktop/ Electron desktop app
│ ├── main.ts Window, Nitro fork, native menu, auto-updater
│ ├── ipc-handlers.ts Native file dialogs, theme sync, prefs IPC
│ └── preload.ts IPC bridge
│ ├── desktop/ Electron desktop app
│ │ ├── main.ts Window, Nitro fork, native menu, auto-updater
│ │ ├── ipc-handlers.ts Native file dialogs, theme sync, prefs IPC
│ │ └── preload.ts IPC bridge
│ └── cli/ CLI tool — `op` command
│ ├── src/commands/ Design, document, export, import, node, page, variable commands
│ ├── connection.ts WebSocket connection to running app
│ └── launcher.ts Auto-detect and launch desktop app or web server
├── packages/
│ ├── pen-types/ Type definitions for PenDocument model
│ ├── pen-core/ Document tree ops, layout engine, variables
@ -281,6 +321,8 @@ npx tsc --noEmit # Type check
bun run bump <version> # Sync version across all package.json
bun run electron:dev # Electron dev
bun run electron:build # Electron package
bun run cli:dev # Run CLI from source
bun run cli:compile # Compile CLI to dist
```
## Contributing
@ -305,6 +347,7 @@ Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details
- [x] Boolean operations (union, subtract, intersect)
- [x] Multi-model capability profiles
- [x] Monorepo restructure with reusable packages
- [x] CLI tool (`op`) for terminal control
- [ ] Collaborative editing
- [ ] Plugin system

View file

@ -80,6 +80,22 @@ Arquivos `.op` são JSON — legíveis por humanos, compatíveis com Git, com di
App web + desktop nativo no macOS, Windows e Linux via Electron. Atualização automática a partir do GitHub Releases. Associação de arquivos `.op` — clique duplo para abrir.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Controle a ferramenta de design pelo terminal. `op design`, `op insert`, `op export` — DSL de design em lote, manipulação de nós, exportação de código. Entrada por pipe de arquivos ou stdin. Funciona com o app desktop ou servidor web.
</td>
<td width="50%">
### 🎯 Exportação de Código Multiplataforma
Exporte de um único arquivo `.op` para React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Variáveis de design se tornam propriedades CSS customizadas.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Instale globalmente e controle a ferramenta de design pelo terminal:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Iniciar app desktop
op design @landing.txt # Design em lote a partir de arquivo
op insert '{"type":"RECT"}' # Inserir um nó
op export react --out . # Exportar para React + Tailwind
op import:figma design.fig # Importar arquivo Figma
cat design.dsl | op design - # Entrada por pipe via stdin
```
Suporta três métodos de entrada: string inline, `@filepath` (ler de arquivo) ou `-` (ler de stdin). Funciona com o app desktop ou servidor web de desenvolvimento. Veja o [CLI README](./apps/cli/README.md) para referência completa de comandos.
## Funcionalidades
**Canvas e Desenho**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **Estado** | Zustand v5 |
| **Servidor** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — controle pelo terminal, DSL de design em lote, exportação de código |
| **IA** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Formato de arquivo** | `.op` — baseado em JSON, legível por humanos, compatível com Git |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ API Nitro — chat em streaming, geração, validação
│ │ └── utils/ Wrappers de cliente Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Aplicativo desktop Electron
│ ├── main.ts Janela, fork do Nitro, menu nativo, atualizador automático
│ ├── ipc-handlers.ts Diálogos de arquivo nativos, sincronização de tema, preferências IPC
│ └── preload.ts Ponte IPC
│ ├── desktop/ Aplicativo desktop Electron
│ │ ├── main.ts Janela, fork do Nitro, menu nativo, atualizador automático
│ │ ├── ipc-handlers.ts Diálogos de arquivo nativos, sincronização de tema, preferências IPC
│ │ └── preload.ts Ponte IPC
│ └── cli/ Ferramenta CLI — comando `op`
│ ├── src/commands/ Comandos de design, documento, exportação, importação, nó, página, variável
│ ├── connection.ts Conexão WebSocket com o app em execução
│ └── launcher.ts Detecção automática e inicialização do app desktop ou servidor web
├── packages/
│ ├── pen-types/ Definições de tipos para o modelo PenDocument
│ ├── pen-core/ Operações de árvore de documento, motor de layout, variáveis
@ -281,6 +321,8 @@ npx tsc --noEmit # Verificação de tipos
bun run bump <version> # Sincronizar versão em todos os package.json
bun run electron:dev # Desenvolvimento com Electron
bun run electron:build # Empacotamento do Electron
bun run cli:dev # Executar CLI a partir do código-fonte
bun run cli:compile # Compilar CLI para dist
```
## Contribuindo
@ -305,6 +347,7 @@ Contribuições são bem-vindas! Consulte o [CLAUDE.md](./CLAUDE.md) para detalh
- [x] Operações booleanas (união, subtração, interseção)
- [x] Perfis de capacidade multi-modelo
- [x] Reestruturação em monorepo com pacotes reutilizáveis
- [x] Ferramenta CLI (`op`) para controle pelo terminal
- [ ] Edição colaborativa
- [ ] Sistema de plugins

View file

@ -80,6 +80,22 @@
Веб-приложение + нативный десктоп на macOS, Windows и Linux через Electron. Автообновление из GitHub Releases. Ассоциация файлов `.op` — двойной клик для открытия.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Управляйте инструментом дизайна из терминала. `op design`, `op insert`, `op export` — пакетный DSL дизайна, манипуляция узлами, экспорт кода. Ввод через pipe из файлов или stdin. Работает с десктопным приложением или веб-сервером.
</td>
<td width="50%">
### 🎯 Мультиплатформенный экспорт кода
Экспорт из одного файла `.op` в React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Переменные дизайна превращаются в пользовательские свойства CSS.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Установите глобально и управляйте инструментом дизайна из терминала:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Запустить десктопное приложение
op design @landing.txt # Пакетный дизайн из файла
op insert '{"type":"RECT"}' # Вставить узел
op export react --out . # Экспорт в React + Tailwind
op import:figma design.fig # Импортировать файл Figma
cat design.dsl | op design - # Передача через stdin
```
Поддерживает три метода ввода: строка, `@filepath` (чтение из файла) или `-` (чтение из stdin). Работает с десктопным приложением или веб-сервером разработки. Подробнее в [CLI README](./apps/cli/README.md).
## Возможности
**Холст и рисование**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **Состояние** | Zustand v5 |
| **Сервер** | Nitro |
| **Десктоп** | Electron 35 |
| **CLI** | `op` — управление из терминала, пакетный DSL дизайна, экспорт кода |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Среда выполнения** | Bun · Vite 7 |
| **Формат файла** | `.op` — на основе JSON, удобочитаемый, дружественный к Git |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — стриминговый чат, генерация, валидация
│ │ └── utils/ Обёртки клиентов Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Десктопное приложение Electron
│ ├── main.ts Окно, форк Nitro, нативное меню, автообновление
│ ├── ipc-handlers.ts Нативные файловые диалоги, синхронизация темы, настройки IPC
│ └── preload.ts IPC-мост
│ ├── desktop/ Десктопное приложение Electron
│ │ ├── main.ts Окно, форк Nitro, нативное меню, автообновление
│ │ ├── ipc-handlers.ts Нативные файловые диалоги, синхронизация темы, настройки IPC
│ │ └── preload.ts IPC-мост
│ └── cli/ CLI-инструмент — команда `op`
│ ├── src/commands/ Команды: дизайн, документ, экспорт, импорт, узлы, страницы, переменные
│ ├── connection.ts WebSocket-соединение с запущенным приложением
│ └── launcher.ts Автоопределение и запуск десктопного приложения или веб-сервера
├── packages/
│ ├── pen-types/ Определения типов для модели PenDocument
│ ├── pen-core/ Операции с деревом документа, движок раскладки, переменные
@ -281,6 +321,8 @@ npx tsc --noEmit # Проверка типов
bun run bump <version> # Синхронизация версий во всех package.json
bun run electron:dev # Разработка Electron
bun run electron:build # Упаковка Electron
bun run cli:dev # Запуск CLI из исходников
bun run cli:compile # Компиляция CLI в dist
```
## Участие в разработке
@ -305,6 +347,7 @@ bun run electron:build # Упаковка Electron
- [x] Булевы операции (объединение, вычитание, пересечение)
- [x] Мультимодельные профили возможностей
- [x] Реструктуризация в монорепозиторий с переиспользуемыми пакетами
- [x] CLI-инструмент (`op`) для управления из терминала
- [ ] Совместное редактирование
- [ ] Система плагинов

View file

@ -80,6 +80,22 @@ Orchestrator แบ่งหน้าที่ซับซ้อนออกเ
เว็บแอป + เดสก์ท็อปแบบ native บน macOS, Windows และ Linux ผ่าน Electron อัปเดตอัตโนมัติจาก GitHub Releases เชื่อมโยงไฟล์ `.op` — ดับเบิลคลิกเพื่อเปิด
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
ควบคุมเครื่องมือออกแบบจาก terminal ของคุณ `op design`, `op insert`, `op export` — batch design DSL, จัดการ node, ส่งออกโค้ด Pipe จากไฟล์หรือ stdin ทำงานร่วมกับแอปเดสก์ท็อปหรือ web server
</td>
<td width="50%">
### 🎯 ส่งออกโค้ดหลายแพลตฟอร์ม
ส่งออกจากไฟล์ `.op` ไฟล์เดียวไปยัง React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native Design variables กลายเป็น CSS custom properties
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
ติดตั้งแบบ global และควบคุมเครื่องมือออกแบบจาก terminal ของคุณ:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # เปิดแอปเดสก์ท็อป
op design @landing.txt # ออกแบบแบบ batch จากไฟล์
op insert '{"type":"RECT"}' # แทรก node
op export react --out . # ส่งออกเป็น React + Tailwind
op import:figma design.fig # นำเข้าไฟล์ Figma
cat design.dsl | op design - # Pipe จาก stdin
```
รองรับ 3 วิธีการป้อนข้อมูล: สตริงแบบ inline, `@filepath` (อ่านจากไฟล์) หรือ `-` (อ่านจาก stdin) ทำงานร่วมกับแอปเดสก์ท็อปหรือ web dev server ดู [CLI README](./apps/cli/README.md) สำหรับคู่มือคำสั่งฉบับเต็ม
## ฟีเจอร์
**Canvas และการวาด**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — ควบคุมจาก terminal, batch design DSL, ส่งออกโค้ด |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **รูปแบบไฟล์** | `.op` — ใช้ JSON, อ่านได้โดยมนุษย์, Git-friendly |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
│ └── desktop/ Electron desktop app
│ ├── main.ts Window, Nitro fork, native menu, auto-updater
│ ├── ipc-handlers.ts ไดอะล็อกไฟล์เนทีฟ, ซิงค์ธีม, การตั้งค่า IPC
│ └── preload.ts IPC bridge
│ ├── desktop/ Electron desktop app
│ │ ├── main.ts Window, Nitro fork, native menu, auto-updater
│ │ ├── ipc-handlers.ts ไดอะล็อกไฟล์เนทีฟ, ซิงค์ธีม, การตั้งค่า IPC
│ │ └── preload.ts IPC bridge
│ └── cli/ เครื่องมือ CLI — คำสั่ง `op`
│ ├── src/commands/ คำสั่ง design, document, export, import, node, page, variable
│ ├── connection.ts การเชื่อมต่อ WebSocket ไปยังแอปที่กำลังทำงาน
│ └── launcher.ts ตรวจจับและเปิดแอปเดสก์ท็อปหรือ web server อัตโนมัติ
├── packages/
│ ├── pen-types/ Type definitions สำหรับ PenDocument model
│ ├── pen-core/ Document tree ops, layout engine, variables
@ -281,6 +321,8 @@ npx tsc --noEmit # ตรวจสอบ type
bun run bump <version> # Sync version ในทุก package.json
bun run electron:dev # Electron dev
bun run electron:build # Electron package
bun run cli:dev # รัน CLI จาก source
bun run cli:compile # คอมไพล์ CLI ไปยัง dist
```
## การมีส่วนร่วม
@ -305,6 +347,7 @@ bun run electron:build # Electron package
- [x] Boolean operations (union, subtract, intersect)
- [x] โปรไฟล์ความสามารถหลายโมเดล
- [x] ปรับโครงสร้างเป็น monorepo พร้อม package ที่นำกลับมาใช้ใหม่ได้
- [x] เครื่องมือ CLI (`op`) ควบคุมจาก terminal
- [ ] การแก้ไขร่วมกัน
- [ ] ระบบปลั๊กอิน

View file

@ -80,6 +80,22 @@ Claude Code, Codex, Gemini, OpenCode, Kiro veya Copilot CLI'larına tek tıkla k
Web uygulaması + Electron ile macOS, Windows ve Linux'ta yerel masaüstü. GitHub Releases'ten otomatik güncelleme. `.op` dosya ilişkilendirmesi — açmak için çift tıklayın.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Tasarım aracını terminalinizden kontrol edin. `op design`, `op insert`, `op export` — toplu tasarım DSL, düğüm manipülasyonu, kod dışa aktarımı. Dosyalardan veya stdin'den pipe ile besleyin. Masaüstü uygulama veya web sunucusuyla çalışır.
</td>
<td width="50%">
### 🎯 Çok Platformlu Kod Dışa Aktarımı
Tek bir `.op` dosyasından React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native'e dışa aktarın. Tasarım değişkenleri CSS özel özelliklerine dönüşür.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Global olarak yükleyin ve tasarım aracını terminalinizden kontrol edin:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Masaüstü uygulamayı başlat
op design @landing.txt # Dosyadan toplu tasarım
op insert '{"type":"RECT"}' # Bir düğüm ekle
op export react --out . # React + Tailwind'e dışa aktar
op import:figma design.fig # Figma dosyasını içe aktar
cat design.dsl | op design - # stdin'den pipe ile besle
```
Üç giriş yöntemini destekler: satır içi metin, `@filepath` (dosyadan oku) veya `-` (stdin'den oku). Masaüstü uygulama veya web geliştirme sunucusuyla çalışır. Tam komut referansı için [CLI README](./apps/cli/README.md) dosyasına bakın.
## Özellikler
**Kanvas ve Çizim**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **Durum Yönetimi** | Zustand v5 |
| **Sunucu** | Nitro |
| **Masaüstü** | Electron 35 |
| **CLI** | `op` — terminal kontrolü, toplu tasarım DSL, kod dışa aktarımı |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Çalışma Ortamı** | Bun · Vite 7 |
| **Dosya Formatı** | `.op` — JSON tabanlı, insan tarafından okunabilir, Git dostu |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — akış sohbet, üretim, doğrulama
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot sarmalayıcıları
│ └── desktop/ Electron masaüstü uygulaması
│ ├── main.ts Pencere, Nitro çatallanması, yerel menü, otomatik güncelleyici
│ ├── ipc-handlers.ts Yerel dosya diyalogları, tema senkronizasyonu, tercihler IPC
│ └── preload.ts IPC köprüsü
│ ├── desktop/ Electron masaüstü uygulaması
│ │ ├── main.ts Pencere, Nitro çatallanması, yerel menü, otomatik güncelleyici
│ │ ├── ipc-handlers.ts Yerel dosya diyalogları, tema senkronizasyonu, tercihler IPC
│ │ └── preload.ts IPC köprüsü
│ └── cli/ CLI aracı`op` komutu
│ ├── src/commands/ Tasarım, belge, dışa aktarma, içe aktarma, düğüm, sayfa, değişken komutları
│ ├── connection.ts Çalışan uygulamaya WebSocket bağlantısı
│ └── launcher.ts Masaüstü uygulamayı veya web sunucusunu otomatik algıla ve başlat
├── packages/
│ ├── pen-types/ PenDocument modeli için tür tanımları
│ ├── pen-core/ Belge ağacı işlemleri, düzen motoru, değişkenler
@ -281,6 +321,8 @@ npx tsc --noEmit # Tür denetimi
bun run bump <version> # Tüm package.json dosyalarında sürümü eşitle
bun run electron:dev # Electron geliştirme modu
bun run electron:build # Electron paketleme
bun run cli:dev # CLI'yi kaynaktan çalıştır
bun run cli:compile # CLI'yi dist'e derle
```
## Katkıda Bulunma
@ -305,6 +347,7 @@ Katkılarınızı bekliyoruz! Mimari ayrıntılar ve kod stili için [CLAUDE.md]
- [x] Boolean işlemler (birleştirme, çıkarma, kesişim)
- [x] Çoklu model yetenek profilleri
- [x] Yeniden kullanılabilir paketlerle monorepo yapılandırması
- [x] CLI aracı (`op`) terminal kontrolü
- [ ] Ortak düzenleme
- [ ] Eklenti sistemi

View file

@ -80,6 +80,22 @@ Tệp `.op` là JSON — dễ đọc, thân thiện Git, dễ so sánh khác bi
Ứng dụng web + desktop gốc trên macOS, Windows và Linux qua Electron. Tự động cập nhật từ GitHub Releases. Liên kết tệp `.op` — nhấp đúp để mở.
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
Điều khiển công cụ thiết kế từ terminal của bạn. `op design`, `op insert`, `op export` — batch design DSL, thao tác node, xuất mã. Pipe từ tệp hoặc stdin. Hoạt động với ứng dụng desktop hoặc web server.
</td>
<td width="50%">
### 🎯 Xuất mã Đa nền tảng
Xuất từ một tệp `.op` duy nhất sang React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Biến thiết kế trở thành thuộc tính tùy chỉnh CSS.
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS, HTML + CSS, CSS Variables
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
## CLI — `op`
Cài đặt toàn cục và điều khiển công cụ thiết kế từ terminal của bạn:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # Khởi chạy ứng dụng desktop
op design @landing.txt # Thiết kế hàng loạt từ tệp
op insert '{"type":"RECT"}' # Chèn một node
op export react --out . # Xuất sang React + Tailwind
op import:figma design.fig # Nhập tệp Figma
cat design.dsl | op design - # Pipe từ stdin
```
Hỗ trợ ba phương thức nhập liệu: chuỗi inline, `@filepath` (đọc từ tệp), hoặc `-` (đọc từ stdin). Hoạt động với ứng dụng desktop hoặc web dev server. Xem [CLI README](./apps/cli/README.md) để biết đầy đủ các lệnh.
## Tính năng
**Canvas và Vẽ**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **Trạng thái** | Zustand v5 |
| **Máy chủ** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — điều khiển từ terminal, batch design DSL, xuất mã |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Định dạng tệp** | `.op` — dựa trên JSON, dễ đọc, thân thiện với Git |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
│ └── desktop/ Ứng dụng desktop Electron
│ ├── main.ts Cửa sổ, Nitro fork, menu gốc, auto-updater
│ ├── ipc-handlers.ts Hộp thoại file gốc, đồng bộ theme, tùy chọn IPC
│ └── preload.ts IPC bridge
│ ├── desktop/ Ứng dụng desktop Electron
│ │ ├── main.ts Cửa sổ, Nitro fork, menu gốc, auto-updater
│ │ ├── ipc-handlers.ts Hộp thoại file gốc, đồng bộ theme, tùy chọn IPC
│ │ └── preload.ts IPC bridge
│ └── cli/ Công cụ CLI — lệnh `op`
│ ├── src/commands/ Lệnh design, document, export, import, node, page, variable
│ ├── connection.ts Kết nối WebSocket đến ứng dụng đang chạy
│ └── launcher.ts Tự động phát hiện và khởi chạy ứng dụng desktop hoặc web server
├── packages/
│ ├── pen-types/ Định nghĩa kiểu cho mô hình PenDocument
│ ├── pen-core/ Thao tác cây tài liệu, layout engine, biến
@ -281,6 +321,8 @@ npx tsc --noEmit # Kiểm tra kiểu
bun run bump <version> # Đồng bộ phiên bản trên tất cả package.json
bun run electron:dev # Electron dev
bun run electron:build # Đóng gói Electron
bun run cli:dev # Chạy CLI từ mã nguồn
bun run cli:compile # Biên dịch CLI sang dist
```
## Đóng góp
@ -305,6 +347,7 @@ Chào mừng đóng góp! Xem [CLAUDE.md](./CLAUDE.md) để biết chi tiết v
- [x] Phép toán Boolean (hợp nhất, trừ, giao)
- [x] Hồ sơ năng lực đa mô hình
- [x] Tái cấu trúc monorepo với các gói tái sử dụng
- [x] Công cụ CLI (`op`) điều khiển từ terminal
- [ ] Chỉnh sửa cộng tác
- [ ] Hệ thống plugin

View file

@ -80,6 +80,22 @@
Web 應用程式 + 透過 Electron 在 macOS、Windows 和 Linux 上原生執行。從 GitHub Releases 自動更新。`.op` 檔案關聯 — 雙擊即可開啟。
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
從終端機控制設計工具。`op design`、`op insert`、`op export` — 批次設計 DSL、節點操作、程式碼匯出。支援從檔案或 stdin 管道輸入。可搭配桌面應用程式或 Web 伺服器使用。
</td>
<td width="50%">
### 🎯 多平台程式碼匯出
從單個 `.op` 檔案匯出至 React + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native。設計變數自動轉換為 CSS 自訂屬性。
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS、HTML + CSS、CSS Variables
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
## CLI — `op`
全域安裝後即可從終端機控制設計工具:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # 啟動桌面應用程式
op design @landing.txt # 從檔案批次設計
op insert '{"type":"RECT"}' # 插入節點
op export react --out . # 匯出為 React + Tailwind
op import:figma design.fig # 匯入 Figma 檔案
cat design.dsl | op design - # 從 stdin 管道輸入
```
支援三種輸入方式:內嵌字串、`@filepath`(從檔案讀取)、`-`(從 stdin 讀取)。可搭配桌面應用程式或 Web 開發伺服器使用。完整命令參考請查閱 [CLI README](./apps/cli/README.md)。
## 功能特色
**畫布與繪圖**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **狀態管理** | Zustand v5 |
| **伺服器** | Nitro |
| **桌面端** | Electron 35 |
| **CLI** | `op` — 終端機控制、批次設計 DSL、程式碼匯出 |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **執行環境** | Bun · Vite 7 |
| **檔案格式** | `.op` — 基於 JSON人類可讀對 Git 友好 |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — 串流聊天、生成、驗證
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot 客戶端封裝
│ └── desktop/ Electron 桌面應用程式
│ ├── main.ts 視窗、Nitro 子處理序、原生選單、自動更新
│ ├── ipc-handlers.ts 原生檔案對話框、主題同步、偏好設定 IPC
│ └── preload.ts IPC 橋接
│ ├── desktop/ Electron 桌面應用程式
│ │ ├── main.ts 視窗、Nitro 子處理序、原生選單、自動更新
│ │ ├── ipc-handlers.ts 原生檔案對話框、主題同步、偏好設定 IPC
│ │ └── preload.ts IPC 橋接
│ └── cli/ CLI 工具 — `op` 命令
│ ├── src/commands/ 設計、文件、匯出、匯入、節點、頁面、變數命令
│ ├── connection.ts 與執行中應用程式的 WebSocket 連線
│ └── launcher.ts 自動偵測並啟動桌面應用程式或 Web 伺服器
├── packages/
│ ├── pen-types/ PenDocument 模型型別定義
│ ├── pen-core/ 文件樹操作、版面配置引擎、變數
@ -281,6 +321,8 @@ npx tsc --noEmit # 型別檢查
bun run bump <version> # 在所有 package.json 間同步版本號
bun run electron:dev # Electron 開發模式
bun run electron:build # Electron 封裝
bun run cli:dev # 從原始碼執行 CLI
bun run cli:compile # 編譯 CLI 到 dist
```
## 參與貢獻
@ -305,6 +347,7 @@ bun run electron:build # Electron 封裝
- [x] 布林運算(聯集、減去、交集)
- [x] 多模型能力設定檔
- [x] Monorepo 重構,支援可重複使用套件
- [x] CLI 工具(`op`)終端控制
- [ ] 協同編輯
- [ ] 外掛程式系統

View file

@ -80,6 +80,22 @@
Web 应用 + 通过 Electron 支持 macOS、Windows 和 Linux 原生桌面端。从 GitHub Releases 自动更新。`.op` 文件关联 — 双击即可打开。
</td>
</tr>
<tr>
<td width="50%">
### ⌨️ CLI — `op`
从终端控制设计工具。`op design`、`op insert`、`op export` — 批量设计 DSL、节点操作、代码导出。支持从文件或 stdin 管道输入。可搭配桌面应用或 Web 服务器使用。
</td>
<td width="50%">
### 🎯 多平台代码导出
从单个 `.op` 文件导出到 React + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native。设计变量自动转换为 CSS 自定义属性。
</td>
</tr>
</table>
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
- React + Tailwind CSS、HTML + CSS、CSS Variables
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
## CLI — `op`
全局安装后即可从终端控制设计工具:
```bash
npm install -g @zseven-w/openpencil
```
```bash
op start # 启动桌面应用
op design @landing.txt # 从文件批量设计
op insert '{"type":"RECT"}' # 插入节点
op export react --out . # 导出为 React + Tailwind
op import:figma design.fig # 导入 Figma 文件
cat design.dsl | op design - # 从 stdin 管道输入
```
支持三种输入方式:内联字符串、`@filepath`(从文件读取)、`-`(从 stdin 读取)。可搭配桌面应用或 Web 开发服务器使用。完整命令参考请查阅 [CLI README](./apps/cli/README.md)。
## 功能特性
**画布与绘图**
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
| **状态管理** | Zustand v5 |
| **服务器** | Nitro |
| **桌面端** | Electron 35 |
| **CLI** | `op` — 终端控制、批量设计 DSL、代码导出 |
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **运行时** | Bun · Vite 7 |
| **文件格式** | `.op` — 基于 JSON人类可读对 Git 友好 |
@ -239,10 +275,14 @@ openpencil/
│ │ └── server/
│ │ ├── api/ai/ Nitro API — 流式聊天、生成、验证
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot 客户端封装
│ └── desktop/ Electron 桌面应用
│ ├── main.ts 窗口、Nitro 子进程、原生菜单、自动更新
│ ├── ipc-handlers.ts 原生文件对话框、主题同步、偏好设置 IPC
│ └── preload.ts IPC 桥接
│ ├── desktop/ Electron 桌面应用
│ │ ├── main.ts 窗口、Nitro 子进程、原生菜单、自动更新
│ │ ├── ipc-handlers.ts 原生文件对话框、主题同步、偏好设置 IPC
│ │ └── preload.ts IPC 桥接
│ └── cli/ CLI 工具 — `op` 命令
│ ├── src/commands/ 设计、文档、导出、导入、节点、页面、变量命令
│ ├── connection.ts 与运行中应用的 WebSocket 连接
│ └── launcher.ts 自动检测并启动桌面应用或 Web 服务器
├── packages/
│ ├── pen-types/ PenDocument 模型类型定义
│ ├── pen-core/ 文档树操作、布局引擎、变量
@ -281,6 +321,8 @@ npx tsc --noEmit # 类型检查
bun run bump <version> # 同步所有 package.json 的版本号
bun run electron:dev # Electron 开发模式
bun run electron:build # Electron 打包
bun run cli:dev # 从源码运行 CLI
bun run cli:compile # 编译 CLI 到 dist
```
## 参与贡献
@ -305,6 +347,7 @@ bun run electron:build # Electron 打包
- [x] 布尔运算(合并、减去、相交)
- [x] 多模型能力配置
- [x] Monorepo 重构与可复用包
- [x] CLI 工具(`op`)终端控制
- [ ] 协同编辑
- [ ] 插件系统

1
apps/cli/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
dist/

41
apps/cli/CLAUDE.md Normal file
View file

@ -0,0 +1,41 @@
# apps/cli/ — OpenPencil CLI
The `op` command-line tool controls the OpenPencil desktop app or web server from the terminal.
## Structure
```text
apps/cli/
├── src/
│ ├── index.ts Entry point — arg parsing, command dispatch, help text
│ ├── connection.ts WebSocket connection to running app instance
│ ├── launcher.ts Auto-detect and launch desktop app or web dev server
│ ├── output.ts JSON output formatting (--pretty support)
│ └── commands/
│ ├── app.ts start, stop, status
│ ├── design.ts design, design:skeleton, design:content, design:refine
│ ├── document.ts open, save, get, selection
│ ├── export.ts export (react, html, vue, svelte, flutter, swiftui, compose, rn, css)
│ ├── import.ts import:svg, import:figma
│ ├── layout.ts layout, find-space
│ ├── nodes.ts insert, update, delete, move, copy, replace
│ ├── pages.ts page list/add/remove/rename/reorder/duplicate
│ └── variables.ts vars, vars:set, themes, themes:set, theme:save/load/list
├── dist/ Compiled output (openpencil-cli.cjs)
├── package.json @zseven-w/openpencil, bin: { op }
└── README.md
```
## Commands
- **Compile:** `bun run cli:compile` (esbuild to `dist/openpencil-cli.cjs`)
- **Dev run:** `bun run cli:dev` (run from source via Bun)
## Key Patterns
- **Input methods:** Commands accepting JSON/DSL support inline string, `@filepath`, or `-` (stdin)
- **Connection:** WebSocket to running app instance (desktop or web server)
- **Launcher:** Auto-detects installed desktop app paths per platform (macOS, Windows, Linux)
- **esbuild:** Compiles with `--alias:@=src` to resolve web app imports, `--external:canvas --external:paper`
- **Output:** All commands output JSON; `--pretty` flag for human-readable formatting
- **Global flags:** `--file <path>` (target .op file), `--page <id>` (target page)

132
apps/cli/README.de.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [**Deutsch**](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
CLI fuer [OpenPencil](https://github.com/ZSeven-W/openpencil) — steuere das Design-Tool von deinem Terminal aus.
## Installation
```bash
npm install -g @zseven-w/openpencil
```
## Plattformunterstuetzung
Das CLI erkennt und startet die OpenPencil-Desktop-App automatisch auf allen Plattformen:
| Plattform | Erkannte Installationspfade |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS pro Benutzer (`%LOCALAPPDATA%`), systemweit (`%PROGRAMFILES%`), portabel |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Verwendung
```bash
op <Befehl> [Optionen]
```
### Eingabemethoden
Argumente, die JSON oder DSL akzeptieren, koennen auf drei Arten uebergeben werden:
```bash
op design '...' # Inline-Zeichenkette (kleine Nutzlasten)
op design @design.txt # Aus Datei lesen (empfohlen fuer grosse Designs)
cat design.txt | op design - # Von stdin lesen (Piping)
```
### App-Steuerung
```bash
op start [--desktop|--web] # OpenPencil starten (standardmaessig Desktop)
op stop # Laufende Instanz beenden
op status # Pruefen, ob die App laeuft
```
### Design (Batch-DSL)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### Dokumentoperationen
```bash
op open [file.op] # Datei oeffnen oder mit aktivem Canvas verbinden
op save <file.op> # Aktuelles Dokument speichern
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Aktuelle Canvas-Auswahl abrufen
```
### Knotenmanipulation
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Code-Export
```bash
op export <format> [--out file]
# Formate: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variablen und Themes
```bash
op vars # Variablen abrufen
op vars:set <json> # Variablen setzen
op themes # Themes abrufen
op themes:set <json> # Themes setzen
op theme:save <file.optheme> # Theme-Preset speichern
op theme:load <file.optheme> # Theme-Preset laden
op theme:list [dir] # Theme-Presets auflisten
```
### Seiten
```bash
op page list # Seiten auflisten
op page add [--name N] # Eine Seite hinzufuegen
op page remove <id> # Eine Seite entfernen
op page rename <id> <name> # Eine Seite umbenennen
op page reorder <id> <index> # Eine Seite neu anordnen
op page duplicate <id> # Eine Seite duplizieren
```
### Import
```bash
op import:svg <file.svg> # SVG-Datei importieren
op import:figma <file.fig> # Figma-.fig-Datei importieren
```
### Layout
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Globale Optionen
```text
--file <path> Ziel-.op-Datei (Standard: aktives Canvas)
--page <id> Zielseiten-ID
--pretty Menschenlesbare JSON-Ausgabe
--help Hilfe anzeigen
--version Version anzeigen
```
## Lizenz
MIT

132
apps/cli/README.es.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [**Español**](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
CLI para [OpenPencil](https://github.com/ZSeven-W/openpencil) — controla la herramienta de diseno desde tu terminal.
## Instalacion
```bash
npm install -g @zseven-w/openpencil
```
## Soporte de plataformas
El CLI detecta y lanza automaticamente la aplicacion de escritorio OpenPencil en todas las plataformas:
| Plataforma | Rutas de instalacion detectadas |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS por usuario (`%LOCALAPPDATA%`), por maquina (`%PROGRAMFILES%`), portable |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Uso
```bash
op <comando> [opciones]
```
### Metodos de entrada
Los argumentos que aceptan JSON o DSL se pueden pasar de tres maneras:
```bash
op design '...' # Cadena en linea (cargas pequenas)
op design @design.txt # Leer desde archivo (recomendado para disenos grandes)
cat design.txt | op design - # Leer desde stdin (tuberia)
```
### Control de la aplicacion
```bash
op start [--desktop|--web] # Iniciar OpenPencil (escritorio por defecto)
op stop # Detener la instancia en ejecucion
op status # Verificar si esta en ejecucion
```
### Diseno (DSL por lotes)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### Operaciones de documento
```bash
op open [file.op] # Abrir archivo o conectar al lienzo activo
op save <file.op> # Guardar el documento actual
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Obtener la seleccion actual del lienzo
```
### Manipulacion de nodos
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Exportacion de codigo
```bash
op export <format> [--out file]
# Formatos: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variables y temas
```bash
op vars # Obtener variables
op vars:set <json> # Establecer variables
op themes # Obtener temas
op themes:set <json> # Establecer temas
op theme:save <file.optheme> # Guardar preset de tema
op theme:load <file.optheme> # Cargar preset de tema
op theme:list [dir] # Listar presets de tema
```
### Paginas
```bash
op page list # Listar paginas
op page add [--name N] # Agregar una pagina
op page remove <id> # Eliminar una pagina
op page rename <id> <name> # Renombrar una pagina
op page reorder <id> <index> # Reordenar una pagina
op page duplicate <id> # Duplicar una pagina
```
### Importacion
```bash
op import:svg <file.svg> # Importar archivo SVG
op import:figma <file.fig> # Importar archivo Figma .fig
```
### Disposicion
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Opciones globales
```text
--file <path> Archivo .op de destino (por defecto: lienzo activo)
--page <id> ID de la pagina de destino
--pretty Salida JSON legible
--help Mostrar ayuda
--version Mostrar version
```
## Licencia
MIT

132
apps/cli/README.fr.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [**Français**](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
CLI pour [OpenPencil](https://github.com/ZSeven-W/openpencil) — controlez l'outil de design depuis votre terminal.
## Installation
```bash
npm install -g @zseven-w/openpencil
```
## Plateformes supportees
Le CLI detecte et lance automatiquement l'application de bureau OpenPencil sur toutes les plateformes :
| Plateforme | Chemins d'installation detectes |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS par utilisateur (`%LOCALAPPDATA%`), par machine (`%PROGRAMFILES%`), portable |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Utilisation
```bash
op <commande> [options]
```
### Methodes de saisie
Les arguments acceptant du JSON ou du DSL peuvent etre passes de trois manieres :
```bash
op design '...' # Chaine en ligne (petites charges)
op design @design.txt # Lecture depuis un fichier (recommande pour les grands designs)
cat design.txt | op design - # Lecture depuis stdin (pipe)
```
### Controle de l'application
```bash
op start [--desktop|--web] # Lancer OpenPencil (bureau par defaut)
op stop # Arreter l'instance en cours
op status # Verifier si l'application est en cours d'execution
```
### Design (DSL par lot)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### Operations sur les documents
```bash
op open [file.op] # Ouvrir un fichier ou se connecter au canevas actif
op save <file.op> # Enregistrer le document actuel
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Obtenir la selection actuelle du canevas
```
### Manipulation des noeuds
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Export de code
```bash
op export <format> [--out file]
# Formats : react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variables et themes
```bash
op vars # Obtenir les variables
op vars:set <json> # Definir les variables
op themes # Obtenir les themes
op themes:set <json> # Definir les themes
op theme:save <file.optheme> # Enregistrer un preset de theme
op theme:load <file.optheme> # Charger un preset de theme
op theme:list [dir] # Lister les presets de theme
```
### Pages
```bash
op page list # Lister les pages
op page add [--name N] # Ajouter une page
op page remove <id> # Supprimer une page
op page rename <id> <name> # Renommer une page
op page reorder <id> <index> # Reordonner une page
op page duplicate <id> # Dupliquer une page
```
### Importation
```bash
op import:svg <file.svg> # Importer un fichier SVG
op import:figma <file.fig> # Importer un fichier Figma .fig
```
### Mise en page
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Options globales
```text
--file <path> Fichier .op cible (par defaut : canevas actif)
--page <id> ID de la page cible
--pretty Sortie JSON lisible
--help Afficher l'aide
--version Afficher la version
```
## Licence
MIT

132
apps/cli/README.hi.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [**हिन्दी**](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
[OpenPencil](https://github.com/ZSeven-W/openpencil) के लिए CLI — अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें।
## इंस्टॉल करें
```bash
npm install -g @zseven-w/openpencil
```
## प्लेटफ़ॉर्म समर्थन
CLI सभी प्लेटफ़ॉर्म पर OpenPencil डेस्कटॉप ऐप को स्वचालित रूप से पहचानता और लॉन्च करता है:
| प्लेटफ़ॉर्म | पहचाने गए इंस्टॉलेशन पथ |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS प्रति-उपयोगकर्ता (`%LOCALAPPDATA%`), प्रति-मशीन (`%PROGRAMFILES%`), पोर्टेबल |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## उपयोग
```bash
op <कमंड> [विकल्प]
```
### इनपुट विधियाँ
JSON या DSL स्वीकार करने वाले आर्गुमेंट तीन तरीकों से पास किए जा सकते हैं:
```bash
op design '...' # इनलाइन स्ट्रिंग (छोटे पेलोड)
op design @design.txt # फ़ाइल से पढ़ें (बड़े डिज़ाइन के लिए अनुशंसित)
cat design.txt | op design - # stdin से पढ़ें (पाइपिंग)
```
### ऐप नियंत्रण
```bash
op start [--desktop|--web] # OpenPencil लॉन्च करें (डिफ़ॉल्ट रूप से डेस्कटॉप)
op stop # चल रहे इंस्टेंस को बंद करें
op status # जाँचें कि चल रहा है या नहीं
```
### डिज़ाइन (बैच DSL)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### दस्तावेज़ संचालन
```bash
op open [file.op] # फ़ाइल खोलें या लाइव कैनवास से कनेक्ट करें
op save <file.op> # वर्तमान दस्तावेज़ सहेजें
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # वर्तमान कैनवास चयन प्राप्त करें
```
### नोड हेरफेर
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### कोड निर्यात
```bash
op export <format> [--out file]
# प्रारूप: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### वेरिएबल और थीम
```bash
op vars # वेरिएबल प्राप्त करें
op vars:set <json> # वेरिएबल सेट करें
op themes # थीम प्राप्त करें
op themes:set <json> # थीम सेट करें
op theme:save <file.optheme> # थीम प्रीसेट सहेजें
op theme:load <file.optheme> # थीम प्रीसेट लोड करें
op theme:list [dir] # थीम प्रीसेट सूचीबद्ध करें
```
### पेज
```bash
op page list # पेज सूचीबद्ध करें
op page add [--name N] # एक पेज जोड़ें
op page remove <id> # एक पेज हटाएँ
op page rename <id> <name> # एक पेज का नाम बदलें
op page reorder <id> <index> # एक पेज का क्रम बदलें
op page duplicate <id> # एक पेज डुप्लिकेट करें
```
### आयात
```bash
op import:svg <file.svg> # SVG फ़ाइल आयात करें
op import:figma <file.fig> # Figma .fig फ़ाइल आयात करें
```
### लेआउट
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### वैश्विक फ़्लैग
```text
--file <path> लक्ष्य .op फ़ाइल (डिफ़ॉल्ट: लाइव कैनवास)
--page <id> लक्ष्य पेज ID
--pretty मानव-पठनीय JSON आउटपुट
--help सहायता दिखाएँ
--version संस्करण दिखाएँ
```
## लाइसेंस
MIT

132
apps/cli/README.id.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [**Bahasa Indonesia**](./README.id.md)
CLI untuk [OpenPencil](https://github.com/ZSeven-W/openpencil) — kendalikan alat desain dari terminal Anda.
## Instalasi
```bash
npm install -g @zseven-w/openpencil
```
## Dukungan Platform
CLI secara otomatis mendeteksi dan meluncurkan aplikasi desktop OpenPencil di semua platform:
| Platform | Jalur instalasi yang terdeteksi |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS per-pengguna (`%LOCALAPPDATA%`), per-mesin (`%PROGRAMFILES%`), portabel |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Penggunaan
```bash
op <perintah> [opsi]
```
### Metode Input
Argumen yang menerima JSON atau DSL dapat diberikan dengan tiga cara:
```bash
op design '...' # String inline (data kecil)
op design @design.txt # Baca dari file (disarankan untuk desain besar)
cat design.txt | op design - # Baca dari stdin (piping)
```
### Kontrol Aplikasi
```bash
op start [--desktop|--web] # Jalankan OpenPencil (desktop secara default)
op stop # Hentikan instance yang berjalan
op status # Periksa apakah sedang berjalan
```
### Desain (Batch DSL)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### Operasi Dokumen
```bash
op open [file.op] # Buka file atau hubungkan ke kanvas langsung
op save <file.op> # Simpan dokumen saat ini
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Dapatkan seleksi kanvas saat ini
```
### Manipulasi Node
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Ekspor Kode
```bash
op export <format> [--out file]
# Format: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variabel & Tema
```bash
op vars # Dapatkan variabel
op vars:set <json> # Atur variabel
op themes # Dapatkan tema
op themes:set <json> # Atur tema
op theme:save <file.optheme> # Simpan preset tema
op theme:load <file.optheme> # Muat preset tema
op theme:list [dir] # Daftar preset tema
```
### Halaman
```bash
op page list # Daftar halaman
op page add [--name N] # Tambah halaman
op page remove <id> # Hapus halaman
op page rename <id> <name> # Ganti nama halaman
op page reorder <id> <index> # Urutkan ulang halaman
op page duplicate <id> # Duplikasi halaman
```
### Impor
```bash
op import:svg <file.svg> # Impor file SVG
op import:figma <file.fig> # Impor file Figma .fig
```
### Tata Letak
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Flag Global
```text
--file <path> File .op target (default: kanvas langsung)
--page <id> ID halaman target
--pretty Output JSON yang mudah dibaca
--help Tampilkan bantuan
--version Tampilkan versi
```
## Lisensi
MIT

132
apps/cli/README.ja.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [**日本語**](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
[OpenPencil](https://github.com/ZSeven-W/openpencil) 用 CLI — ターミナルからデザインツールを操作できます。
## インストール
```bash
npm install -g @zseven-w/openpencil
```
## プラットフォーム対応
CLI はすべてのプラットフォームで OpenPencil デスクトップアプリを自動検出して起動します:
| プラットフォーム | 検出されるインストールパス |
| ---------------- | ------------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS ユーザー単位 (`%LOCALAPPDATA%`)、マシン単位 (`%PROGRAMFILES%`)、ポータブル |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## 使い方
```bash
op <command> [options]
```
### 入力方法
JSON または DSL を受け付ける引数は、3 つの方法で渡すことができます:
```bash
op design '...' # インライン文字列(小さなペイロード向け)
op design @design.txt # ファイルから読み込み(大きなデザインに推奨)
cat design.txt | op design - # 標準入力から読み込み(パイプ)
```
### アプリ制御
```bash
op start [--desktop|--web] # OpenPencil を起動(デフォルトはデスクトップ)
op stop # 実行中のインスタンスを停止
op status # 実行中かどうかを確認
```
### デザイン(バッチ DSL
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### ドキュメント操作
```bash
op open [file.op] # ファイルを開く、またはライブキャンバスに接続
op save <file.op> # 現在のドキュメントを保存
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # 現在のキャンバスの選択を取得
```
### ノード操作
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### コードエクスポート
```bash
op export <format> [--out file]
# フォーマット: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### 変数とテーマ
```bash
op vars # 変数を取得
op vars:set <json> # 変数を設定
op themes # テーマを取得
op themes:set <json> # テーマを設定
op theme:save <file.optheme> # テーマプリセットを保存
op theme:load <file.optheme> # テーマプリセットを読み込み
op theme:list [dir] # テーマプリセットを一覧表示
```
### ページ
```bash
op page list # ページを一覧表示
op page add [--name N] # ページを追加
op page remove <id> # ページを削除
op page rename <id> <name> # ページの名前を変更
op page reorder <id> <index> # ページを並べ替え
op page duplicate <id> # ページを複製
```
### インポート
```bash
op import:svg <file.svg> # SVG ファイルをインポート
op import:figma <file.fig> # Figma .fig ファイルをインポート
```
### レイアウト
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### グローバルフラグ
```text
--file <path> 対象の .op ファイル(デフォルト: ライブキャンバス)
--page <id> 対象のページ ID
--pretty 人間が読みやすい JSON 出力
--help ヘルプを表示
--version バージョンを表示
```
## ライセンス
MIT

132
apps/cli/README.ko.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [**한국어**](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
[OpenPencil](https://github.com/ZSeven-W/openpencil)용 CLI — 터미널에서 디자인 도구를 제어합니다.
## 설치
```bash
npm install -g @zseven-w/openpencil
```
## 플랫폼 지원
CLI는 모든 플랫폼에서 OpenPencil 데스크톱 앱을 자동으로 감지하고 실행합니다:
| 플랫폼 | 감지되는 설치 경로 |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS 사용자별 (`%LOCALAPPDATA%`), 시스템 전체 (`%PROGRAMFILES%`), 포터블 |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## 사용법
```bash
op <command> [options]
```
### 입력 방식
JSON 또는 DSL을 받는 인자는 세 가지 방법으로 전달할 수 있습니다:
```bash
op design '...' # 인라인 문자열 (작은 페이로드)
op design @design.txt # 파일에서 읽기 (대규모 디자인에 권장)
cat design.txt | op design - # 표준 입력에서 읽기 (파이핑)
```
### 앱 제어
```bash
op start [--desktop|--web] # OpenPencil 실행 (기본값: 데스크톱)
op stop # 실행 중인 인스턴스 중지
op status # 실행 상태 확인
```
### 디자인 (배치 DSL)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### 문서 작업
```bash
op open [file.op] # 파일 열기 또는 라이브 캔버스에 연결
op save <file.op> # 현재 문서 저장
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # 현재 캔버스 선택 항목 가져오기
```
### 노드 조작
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### 코드 내보내기
```bash
op export <format> [--out file]
# 형식: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### 변수 및 테마
```bash
op vars # 변수 가져오기
op vars:set <json> # 변수 설정
op themes # 테마 가져오기
op themes:set <json> # 테마 설정
op theme:save <file.optheme> # 테마 프리셋 저장
op theme:load <file.optheme> # 테마 프리셋 불러오기
op theme:list [dir] # 테마 프리셋 목록 보기
```
### 페이지
```bash
op page list # 페이지 목록 보기
op page add [--name N] # 페이지 추가
op page remove <id> # 페이지 제거
op page rename <id> <name> # 페이지 이름 변경
op page reorder <id> <index> # 페이지 순서 변경
op page duplicate <id> # 페이지 복제
```
### 가져오기
```bash
op import:svg <file.svg> # SVG 파일 가져오기
op import:figma <file.fig> # Figma .fig 파일 가져오기
```
### 레이아웃
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### 전역 플래그
```text
--file <path> 대상 .op 파일 (기본값: 라이브 캔버스)
--page <id> 대상 페이지 ID
--pretty 사람이 읽기 쉬운 JSON 출력
--help 도움말 표시
--version 버전 표시
```
## 라이선스
MIT

132
apps/cli/README.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[**English**](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
CLI for [OpenPencil](https://github.com/ZSeven-W/openpencil) — control the design tool from your terminal.
## Install
```bash
npm install -g @zseven-w/openpencil
```
## Platform Support
The CLI automatically detects and launches the OpenPencil desktop app on all platforms:
| Platform | Installation paths detected |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS per-user (`%LOCALAPPDATA%`), per-machine (`%PROGRAMFILES%`), portable |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Usage
```bash
op <command> [options]
```
### Input Methods
Arguments that accept JSON or DSL can be passed in three ways:
```bash
op design '...' # Inline string (small payloads)
op design @design.txt # Read from file (recommended for large designs)
cat design.txt | op design - # Read from stdin (piping)
```
### App Control
```bash
op start [--desktop|--web] # Launch OpenPencil (desktop by default)
op stop # Stop running instance
op status # Check if running
```
### Design (Batch DSL)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### Document Operations
```bash
op open [file.op] # Open file or connect to live canvas
op save <file.op> # Save current document
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Get current canvas selection
```
### Node Manipulation
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Code Export
```bash
op export <format> [--out file]
# Formats: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variables & Themes
```bash
op vars # Get variables
op vars:set <json> # Set variables
op themes # Get themes
op themes:set <json> # Set themes
op theme:save <file.optheme> # Save theme preset
op theme:load <file.optheme> # Load theme preset
op theme:list [dir] # List theme presets
```
### Pages
```bash
op page list # List pages
op page add [--name N] # Add a page
op page remove <id> # Remove a page
op page rename <id> <name> # Rename a page
op page reorder <id> <index> # Reorder a page
op page duplicate <id> # Duplicate a page
```
### Import
```bash
op import:svg <file.svg> # Import SVG file
op import:figma <file.fig> # Import Figma .fig file
```
### Layout
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Global Flags
```text
--file <path> Target .op file (default: live canvas)
--page <id> Target page ID
--pretty Human-readable JSON output
--help Show help
--version Show version
```
## License
MIT

132
apps/cli/README.pt.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [**Português**](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
CLI para o [OpenPencil](https://github.com/ZSeven-W/openpencil) — controle a ferramenta de design pelo seu terminal.
## Instalar
```bash
npm install -g @zseven-w/openpencil
```
## Suporte a Plataformas
A CLI detecta e inicia automaticamente o aplicativo desktop OpenPencil em todas as plataformas:
| Plataforma | Caminhos de instalacao detectados |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS por usuario (`%LOCALAPPDATA%`), por maquina (`%PROGRAMFILES%`), portatil |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Uso
```bash
op <comando> [opcoes]
```
### Metodos de Entrada
Argumentos que aceitam JSON ou DSL podem ser passados de tres formas:
```bash
op design '...' # String inline (payloads pequenos)
op design @design.txt # Ler de arquivo (recomendado para designs grandes)
cat design.txt | op design - # Ler da entrada padrao (piping)
```
### Controle do Aplicativo
```bash
op start [--desktop|--web] # Iniciar o OpenPencil (desktop por padrao)
op stop # Parar a instancia em execucao
op status # Verificar se esta em execucao
```
### Design (DSL em Lote)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### Operacoes de Documento
```bash
op open [file.op] # Abrir arquivo ou conectar ao canvas ativo
op save <file.op> # Salvar o documento atual
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Obter a selecao atual do canvas
```
### Manipulacao de Nos
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Exportacao de Codigo
```bash
op export <format> [--out file]
# Formatos: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variaveis e Temas
```bash
op vars # Obter variaveis
op vars:set <json> # Definir variaveis
op themes # Obter temas
op themes:set <json> # Definir temas
op theme:save <file.optheme> # Salvar preset de tema
op theme:load <file.optheme> # Carregar preset de tema
op theme:list [dir] # Listar presets de temas
```
### Paginas
```bash
op page list # Listar paginas
op page add [--name N] # Adicionar uma pagina
op page remove <id> # Remover uma pagina
op page rename <id> <name> # Renomear uma pagina
op page reorder <id> <index> # Reordenar uma pagina
op page duplicate <id> # Duplicar uma pagina
```
### Importacao
```bash
op import:svg <file.svg> # Importar arquivo SVG
op import:figma <file.fig> # Importar arquivo .fig do Figma
```
### Layout
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Flags Globais
```text
--file <path> Arquivo .op alvo (padrao: canvas ativo)
--page <id> ID da pagina alvo
--pretty Saida JSON legivel
--help Mostrar ajuda
--version Mostrar versao
```
## Licenca
MIT

132
apps/cli/README.ru.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [**Русский**](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
CLI для [OpenPencil](https://github.com/ZSeven-W/openpencil) — управляйте инструментом дизайна из терминала.
## Установка
```bash
npm install -g @zseven-w/openpencil
```
## Поддержка платформ
CLI автоматически обнаруживает и запускает настольное приложение OpenPencil на всех платформах:
| Платформа | Обнаруживаемые пути установки |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS для пользователя (`%LOCALAPPDATA%`), для машины (`%PROGRAMFILES%`), портативная версия |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Использование
```bash
op <команда> [параметры]
```
### Методы ввода
Аргументы, принимающие JSON или DSL, можно передать тремя способами:
```bash
op design '...' # Встроенная строка (небольшие данные)
op design @design.txt # Чтение из файла (рекомендуется для больших дизайнов)
cat design.txt | op design - # Чтение из stdin (через конвейер)
```
### Управление приложением
```bash
op start [--desktop|--web] # Запустить OpenPencil (по умолчанию — настольное приложение)
op stop # Остановить запущенный экземпляр
op status # Проверить, запущено ли приложение
```
### Дизайн (пакетный DSL)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### Операции с документом
```bash
op open [file.op] # Открыть файл или подключиться к активному холсту
op save <file.op> # Сохранить текущий документ
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Получить текущее выделение на холсте
```
### Работа с узлами
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Экспорт кода
```bash
op export <format> [--out file]
# Форматы: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Переменные и темы
```bash
op vars # Получить переменные
op vars:set <json> # Задать переменные
op themes # Получить темы
op themes:set <json> # Задать темы
op theme:save <file.optheme> # Сохранить пресет темы
op theme:load <file.optheme> # Загрузить пресет темы
op theme:list [dir] # Список пресетов тем
```
### Страницы
```bash
op page list # Список страниц
op page add [--name N] # Добавить страницу
op page remove <id> # Удалить страницу
op page rename <id> <name> # Переименовать страницу
op page reorder <id> <index> # Изменить порядок страницы
op page duplicate <id> # Дублировать страницу
```
### Импорт
```bash
op import:svg <file.svg> # Импортировать SVG-файл
op import:figma <file.fig> # Импортировать файл Figma .fig
```
### Макет
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Глобальные флаги
```text
--file <path> Целевой файл .op (по умолчанию: активный холст)
--page <id> ID целевой страницы
--pretty Читаемый вывод JSON
--help Показать справку
--version Показать версию
```
## Лицензия
MIT

132
apps/cli/README.th.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [**ไทย**](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
CLI สำหรับ [OpenPencil](https://github.com/ZSeven-W/openpencil) — ควบคุมเครื่องมือออกแบบจากเทอร์มินัลของคุณ
## การติดตั้ง
```bash
npm install -g @zseven-w/openpencil
```
## การรองรับแพลตฟอร์ม
CLI จะตรวจจับและเปิดแอปเดสก์ท็อป OpenPencil โดยอัตโนมัติบนทุกแพลตฟอร์ม:
| แพลตฟอร์ม | เส้นทางการติดตั้งที่ตรวจพบ |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS ต่อผู้ใช้ (`%LOCALAPPDATA%`), ต่อเครื่อง (`%PROGRAMFILES%`), แบบพกพา |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## การใช้งาน
```bash
op <คำสั่ง> [ตัวเลือก]
```
### วิธีการป้อนข้อมูล
อาร์กิวเมนต์ที่รับ JSON หรือ DSL สามารถส่งได้สามวิธี:
```bash
op design '...' # ข้อความแบบอินไลน์ (ข้อมูลขนาดเล็ก)
op design @design.txt # อ่านจากไฟล์ (แนะนำสำหรับการออกแบบขนาดใหญ่)
cat design.txt | op design - # อ่านจาก stdin (การไพพ์)
```
### การควบคุมแอป
```bash
op start [--desktop|--web] # เปิด OpenPencil (เดสก์ท็อปเป็นค่าเริ่มต้น)
op stop # หยุดอินสแตนซ์ที่กำลังทำงาน
op status # ตรวจสอบว่ากำลังทำงานอยู่หรือไม่
```
### การออกแบบ (Batch DSL)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### การดำเนินการเอกสาร
```bash
op open [file.op] # เปิดไฟล์หรือเชื่อมต่อกับแคนวาสสด
op save <file.op> # บันทึกเอกสารปัจจุบัน
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # รับการเลือกแคนวาสปัจจุบัน
```
### การจัดการโหนด
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### การส่งออกโค้ด
```bash
op export <format> [--out file]
# รูปแบบ: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### ตัวแปรและธีม
```bash
op vars # รับตัวแปร
op vars:set <json> # ตั้งค่าตัวแปร
op themes # รับธีม
op themes:set <json> # ตั้งค่าธีม
op theme:save <file.optheme> # บันทึกพรีเซ็ตธีม
op theme:load <file.optheme> # โหลดพรีเซ็ตธีม
op theme:list [dir] # แสดงรายการพรีเซ็ตธีม
```
### หน้า
```bash
op page list # แสดงรายการหน้า
op page add [--name N] # เพิ่มหน้า
op page remove <id> # ลบหน้า
op page rename <id> <name> # เปลี่ยนชื่อหน้า
op page reorder <id> <index> # จัดลำดับหน้าใหม่
op page duplicate <id> # ทำสำเนาหน้า
```
### การนำเข้า
```bash
op import:svg <file.svg> # นำเข้าไฟล์ SVG
op import:figma <file.fig> # นำเข้าไฟล์ Figma .fig
```
### เลย์เอาต์
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### แฟล็กทั่วไป
```text
--file <path> ไฟล์ .op เป้าหมาย (ค่าเริ่มต้น: แคนวาสสด)
--page <id> ID หน้าเป้าหมาย
--pretty แสดงผล JSON แบบอ่านง่าย
--help แสดงความช่วยเหลือ
--version แสดงเวอร์ชัน
```
## สัญญาอนุญาต
MIT

132
apps/cli/README.tr.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [**Türkçe**](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
[OpenPencil](https://github.com/ZSeven-W/openpencil) icin CLI — tasarim aracini terminalinizden kontrol edin.
## Kurulum
```bash
npm install -g @zseven-w/openpencil
```
## Platform Destegi
CLI, tum platformlarda OpenPencil masaustu uygulamasini otomatik olarak algilar ve baslatir:
| Platform | Algilanan kurulum yollari |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | Kullanici basina NSIS (`%LOCALAPPDATA%`), makine basina (`%PROGRAMFILES%`), tasinabilir |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Kullanim
```bash
op <komut> [secenekler]
```
### Girdi Yontemleri
JSON veya DSL kabul eden argumanlar uc sekilde iletilebilir:
```bash
op design '...' # Satir ici metin (kucuk veriler)
op design @design.txt # Dosyadan oku (buyuk tasarimlar icin onerilir)
cat design.txt | op design - # Stdin'den oku (borulama)
```
### Uygulama Kontrolu
```bash
op start [--desktop|--web] # OpenPencil'i baslat (varsayilan: masaustu)
op stop # Calisan ornegi durdur
op status # Calisip calismadigini kontrol et
```
### Tasarim (Toplu DSL)
```bash
op design <dsl|@dosya|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@dosya|->
op design:content <bolum-id> <json|@dosya|->
op design:refine --root-id <id>
```
### Belge Islemleri
```bash
op open [dosya.op] # Dosya ac veya canli tuvale baglan
op save <dosya.op> # Mevcut belgeyi kaydet
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Mevcut tuval secimini al
```
### Dugum Manipulasyonu
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Kod Disari Aktarimi
```bash
op export <format> [--out dosya]
# Formatlar: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Degiskenler ve Temalar
```bash
op vars # Degiskenleri al
op vars:set <json> # Degiskenleri ayarla
op themes # Temalari al
op themes:set <json> # Temalari ayarla
op theme:save <dosya.optheme> # Tema onayarini kaydet
op theme:load <dosya.optheme> # Tema onayarini yukle
op theme:list [dizin] # Tema onayarlarini listele
```
### Sayfalar
```bash
op page list # Sayfalari listele
op page add [--name N] # Sayfa ekle
op page remove <id> # Sayfa kaldir
op page rename <id> <ad> # Sayfayi yeniden adlandir
op page reorder <id> <indeks> # Sayfayi yeniden sirala
op page duplicate <id> # Sayfayi cogalt
```
### Iceri Aktarma
```bash
op import:svg <dosya.svg> # SVG dosyasi iceri aktar
op import:figma <dosya.fig> # Figma .fig dosyasi iceri aktar
```
### Yerlesim
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Genel Bayraklar
```text
--file <yol> Hedef .op dosyasi (varsayilan: canli tuval)
--page <id> Hedef sayfa kimligi
--pretty Okunabilir JSON ciktisi
--help Yardimi goster
--version Surumu goster
```
## Lisans
MIT

132
apps/cli/README.vi.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [**Tiếng Việt**](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
CLI cho [OpenPencil](https://github.com/ZSeven-W/openpencil) — điều khiển công cụ thiết kế từ terminal của bạn.
## Cài đặt
```bash
npm install -g @zseven-w/openpencil
```
## Hỗ trợ nền tảng
CLI tự động phát hiện và khởi chạy ứng dụng desktop OpenPencil trên tất cả các nền tảng:
| Nền tảng | Đường dẫn cài đặt được phát hiện |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS theo người dùng (`%LOCALAPPDATA%`), theo máy (`%PROGRAMFILES%`), di động |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## Sử dụng
```bash
op <lệnh> [tùy-chọn]
```
### Phương thức nhập liệu
Các đối số chấp nhận JSON hoặc DSL có thể được truyền theo ba cách:
```bash
op design '...' # Chuỗi nội tuyến (dữ liệu nhỏ)
op design @design.txt # Đọc từ tệp (khuyến nghị cho thiết kế lớn)
cat design.txt | op design - # Đọc từ stdin (đường ống)
```
### Điều khiển ứng dụng
```bash
op start [--desktop|--web] # Khởi chạy OpenPencil (mặc định: desktop)
op stop # Dừng phiên bản đang chạy
op status # Kiểm tra trạng thái hoạt động
```
### Thiết kế (Batch DSL)
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### Thao tác tài liệu
```bash
op open [file.op] # Mở tệp hoặc kết nối với canvas trực tiếp
op save <file.op> # Lưu tài liệu hiện tại
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # Lấy vùng chọn canvas hiện tại
```
### Thao tác nút
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Xuất mã nguồn
```bash
op export <format> [--out file]
# Định dạng: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Biến và giao diện
```bash
op vars # Lấy biến
op vars:set <json> # Đặt biến
op themes # Lấy giao diện
op themes:set <json> # Đặt giao diện
op theme:save <file.optheme> # Lưu bộ giao diện mẫu
op theme:load <file.optheme> # Tải bộ giao diện mẫu
op theme:list [dir] # Liệt kê bộ giao diện mẫu
```
### Trang
```bash
op page list # Liệt kê trang
op page add [--name N] # Thêm trang
op page remove <id> # Xóa trang
op page rename <id> <name> # Đổi tên trang
op page reorder <id> <index> # Sắp xếp lại trang
op page duplicate <id> # Nhân bản trang
```
### Nhập
```bash
op import:svg <file.svg> # Nhập tệp SVG
op import:figma <file.fig> # Nhập tệp Figma .fig
```
### Bố cục
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### Cờ toàn cục
```text
--file <path> Tệp .op đích (mặc định: canvas trực tiếp)
--page <id> ID trang đích
--pretty Xuất JSON dễ đọc
--help Hiển thị trợ giúp
--version Hiển thị phiên bản
```
## Giấy phép
MIT

132
apps/cli/README.zh-TW.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [简体中文](./README.zh.md) · [**繁體中文**](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
[OpenPencil](https://github.com/ZSeven-W/openpencil) 的命令列工具 — 從終端機控制設計工具。
## 安裝
```bash
npm install -g @zseven-w/openpencil
```
## 平台支援
CLI 會自動偵測並啟動所有平台上的 OpenPencil 桌面應用程式:
| 平台 | 偵測的安裝路徑 |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`、`~/Applications/OpenPencil.app` |
| **Windows** | NSIS 使用者安裝(`%LOCALAPPDATA%`)、全域安裝(`%PROGRAMFILES%`)、可攜版 |
| **Linux** | `/usr/bin`、`/usr/local/bin`、`~/.local/bin`、AppImage`~/Applications`、`~/Downloads`、Snap、Flatpak |
## 使用方式
```bash
op <command> [options]
```
### 輸入方式
接受 JSON 或 DSL 的參數可透過三種方式傳入:
```bash
op design '...' # 內嵌字串(適合小型內容)
op design @design.txt # 從檔案讀取(建議用於大型設計)
cat design.txt | op design - # 從標準輸入讀取(管線傳輸)
```
### 應用程式控制
```bash
op start [--desktop|--web] # 啟動 OpenPencil預設為桌面版
op stop # 停止執行中的實例
op status # 檢查是否正在執行
```
### 設計(批次 DSL
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### 文件操作
```bash
op open [file.op] # 開啟檔案或連線至即時畫布
op save <file.op> # 儲存目前的文件
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # 取得目前畫布的選取項目
```
### 節點操作
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### 程式碼匯出
```bash
op export <format> [--out file]
# 格式react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### 變數與主題
```bash
op vars # 取得變數
op vars:set <json> # 設定變數
op themes # 取得主題
op themes:set <json> # 設定主題
op theme:save <file.optheme> # 儲存主題預設
op theme:load <file.optheme> # 載入主題預設
op theme:list [dir] # 列出主題預設
```
### 頁面
```bash
op page list # 列出頁面
op page add [--name N] # 新增頁面
op page remove <id> # 移除頁面
op page rename <id> <name> # 重新命名頁面
op page reorder <id> <index> # 重新排序頁面
op page duplicate <id> # 複製頁面
```
### 匯入
```bash
op import:svg <file.svg> # 匯入 SVG 檔案
op import:figma <file.fig> # 匯入 Figma .fig 檔案
```
### 版面配置
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### 全域旗標
```text
--file <path> 目標 .op 檔案(預設:即時畫布)
--page <id> 目標頁面 ID
--pretty 人類可讀的 JSON 輸出
--help 顯示說明
--version 顯示版本
```
## 授權條款
MIT

132
apps/cli/README.zh.md Normal file
View file

@ -0,0 +1,132 @@
# @zseven-w/openpencil
[English](./README.md) · [**简体中文**](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
[OpenPencil](https://github.com/ZSeven-W/openpencil) 的命令行工具 — 从终端控制设计工具。
## 安装
```bash
npm install -g @zseven-w/openpencil
```
## 平台支持
CLI 会自动检测并启动各平台上的 OpenPencil 桌面应用:
| 平台 | 检测的安装路径 |
| ----------- | --------------------------------------------------------------------------------------------------- |
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
| **Windows** | NSIS 用户级 (`%LOCALAPPDATA%`)、系统级 (`%PROGRAMFILES%`)、便携版 |
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
## 用法
```bash
op <command> [options]
```
### 输入方式
接受 JSON 或 DSL 的参数支持三种传入方式:
```bash
op design '...' # 内联字符串(适合小型内容)
op design @design.txt # 从文件读取(推荐用于大型设计)
cat design.txt | op design - # 从标准输入读取(管道传入)
```
### 应用控制
```bash
op start [--desktop|--web] # 启动 OpenPencil默认桌面版
op stop # 停止运行中的实例
op status # 检查运行状态
```
### 设计(批量 DSL
```bash
op design <dsl|@file|-> [--post-process] [--canvas-width N]
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
```
### 文档操作
```bash
op open [file.op] # 打开文件或连接到实时画布
op save <file.op> # 保存当前文档
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection # 获取当前画布选中项
```
### 节点操作
```bash
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### 代码导出
```bash
op export <format> [--out file]
# 格式react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### 变量与主题
```bash
op vars # 获取变量
op vars:set <json> # 设置变量
op themes # 获取主题
op themes:set <json> # 设置主题
op theme:save <file.optheme> # 保存主题预设
op theme:load <file.optheme> # 加载主题预设
op theme:list [dir] # 列出主题预设
```
### 页面
```bash
op page list # 列出页面
op page add [--name N] # 添加页面
op page remove <id> # 删除页面
op page rename <id> <name> # 重命名页面
op page reorder <id> <index> # 调整页面顺序
op page duplicate <id> # 复制页面
```
### 导入
```bash
op import:svg <file.svg> # 导入 SVG 文件
op import:figma <file.fig> # 导入 Figma .fig 文件
```
### 布局
```bash
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
```
### 全局选项
```text
--file <path> 目标 .op 文件(默认:实时画布)
--page <id> 目标页面 ID
--pretty 人类可读的 JSON 输出
--help 显示帮助
--version 显示版本
```
## 许可证
MIT

20
apps/cli/package.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "@zseven-w/openpencil",
"version": "0.5.1",
"description": "CLI for OpenPencil — control the design tool from your terminal",
"author": {
"name": "ZSeven-W",
"email": "xkayshen@gmail.com"
},
"license": "MIT",
"type": "module",
"bin": {
"op": "dist/openpencil-cli.cjs"
},
"files": [
"dist"
],
"scripts": {
"compile": "cd ../web && esbuild ../cli/src/index.ts --bundle --platform=node --target=node20 --outfile=../cli/dist/openpencil-cli.cjs --format=cjs --sourcemap --alias:@=src --define:import.meta.env={} --external:canvas --external:paper"
}
}

View file

@ -0,0 +1,44 @@
import { getAppInfo } from '../connection'
import { startDesktop, startWeb, stopApp } from '../launcher'
import { output, outputError } from '../output'
export async function cmdStart(flags: {
desktop?: boolean
web?: boolean
}): Promise<void> {
try {
let result: { port: number; pid: number }
if (flags.web) {
result = await startWeb()
} else {
result = await startDesktop()
}
output({ ok: true, ...result, url: `http://127.0.0.1:${result.port}` })
} catch (err) {
outputError((err as Error).message)
}
}
export async function cmdStop(): Promise<void> {
const stopped = await stopApp()
if (stopped) {
output({ ok: true, message: 'OpenPencil stopped' })
} else {
output({ ok: true, message: 'No running instance found' })
}
}
export async function cmdStatus(): Promise<void> {
const info = await getAppInfo()
if (info) {
output({
running: true,
port: info.port,
pid: info.pid,
url: info.url,
uptime: Math.floor((Date.now() - info.timestamp) / 1000),
})
} else {
output({ running: false })
}
}

View file

@ -0,0 +1,70 @@
import { handleBatchDesign } from '@/mcp/tools/batch-design'
import { handleDesignSkeleton } from '@/mcp/tools/design-skeleton'
import { handleDesignContent } from '@/mcp/tools/design-content'
import { handleDesignRefine } from '@/mcp/tools/design-refine'
import { output, outputError, parseJsonArg, resolveArg } from '../output'
interface GlobalFlags {
file?: string
page?: string
}
export async function cmdDesign(
args: string[],
flags: GlobalFlags & { postProcess?: boolean; canvasWidth?: string },
): Promise<void> {
const operations = await resolveArg(args[0])
const result = await handleBatchDesign({
filePath: flags.file,
operations,
postProcess: flags.postProcess !== false,
canvasWidth: flags.canvasWidth ? parseInt(flags.canvasWidth, 10) : undefined,
pageId: flags.page,
})
output(result)
}
export async function cmdDesignSkeleton(
args: string[],
flags: GlobalFlags,
): Promise<void> {
const json = (await parseJsonArg(args[0])) as Record<string, unknown>
const result = await handleDesignSkeleton({
filePath: flags.file,
rootFrame: json.rootFrame as any,
sections: json.sections as any,
pageId: flags.page,
})
output(result)
}
export async function cmdDesignContent(
args: string[],
flags: GlobalFlags & { canvasWidth?: string },
): Promise<void> {
const sectionId = args[0]
if (!sectionId) outputError('Usage: openpencil design:content <section-id> <json>')
const json = (await parseJsonArg(args[1])) as Record<string, unknown>
const result = await handleDesignContent({
filePath: flags.file,
sectionId,
children: json.children as any,
canvasWidth: flags.canvasWidth ? parseInt(flags.canvasWidth, 10) : undefined,
pageId: flags.page,
})
output(result)
}
export async function cmdDesignRefine(
args: string[],
flags: GlobalFlags & { rootId?: string; canvasWidth?: string },
): Promise<void> {
if (!flags.rootId) outputError('Usage: openpencil design:refine --root-id <id>')
const result = await handleDesignRefine({
filePath: flags.file,
rootId: flags.rootId!,
canvasWidth: flags.canvasWidth ? parseInt(flags.canvasWidth, 10) : undefined,
pageId: flags.page,
})
output(result)
}

View file

@ -0,0 +1,57 @@
import { handleOpenDocument } from '@/mcp/tools/open-document'
import { handleBatchGet } from '@/mcp/tools/batch-get'
import { handleGetSelection } from '@/mcp/tools/get-selection'
import { openDocument, saveDocument, resolveDocPath } from '@/mcp/document-manager'
import { output, outputError } from '../output'
interface GlobalFlags {
file?: string
page?: string
}
export async function cmdOpen(args: string[], flags: GlobalFlags): Promise<void> {
const result = await handleOpenDocument({ filePath: flags.file ?? args[0] })
output(result)
}
export async function cmdSave(args: string[], flags: GlobalFlags): Promise<void> {
const target = args[0]
if (!target) outputError('Usage: openpencil save <file.op>')
const doc = await openDocument(resolveDocPath(flags.file))
await saveDocument(target, doc)
output({ ok: true, filePath: target })
}
export async function cmdGet(args: string[], flags: GlobalFlags & {
type?: string
name?: string
id?: string
depth?: string
parent?: string
}): Promise<void> {
const patterns: { type?: string; name?: string }[] = []
if (flags.type || flags.name) {
patterns.push({ type: flags.type, name: flags.name })
}
const nodeIds: string[] = []
if (flags.id) nodeIds.push(flags.id)
const result = await handleBatchGet({
filePath: flags.file,
patterns: patterns.length ? patterns : undefined,
nodeIds: nodeIds.length ? nodeIds : undefined,
parentId: flags.parent,
readDepth: flags.depth ? parseInt(flags.depth, 10) : undefined,
pageId: flags.page,
})
output(result)
}
export async function cmdSelection(flags: GlobalFlags & { depth?: string }): Promise<void> {
const result = await handleGetSelection({
filePath: flags.file,
readDepth: flags.depth ? parseInt(flags.depth, 10) : undefined,
})
output(result)
}

View file

@ -0,0 +1,66 @@
import { openDocument, resolveDocPath } from '@/mcp/document-manager'
import {
generateReactFromDocument,
generateHTMLFromDocument,
generateVueFromDocument,
generateSvelteFromDocument,
generateFlutterFromDocument,
generateSwiftUIFromDocument,
generateComposeFromDocument,
generateReactNativeFromDocument,
generateCSSVariables,
} from '@zseven-w/pen-codegen'
import { writeFile } from 'node:fs/promises'
import { output, outputError } from '../output'
type GeneratorResult = string | { html: string; css: string }
const GENERATORS: Record<string, (doc: any) => GeneratorResult> = {
react: generateReactFromDocument,
html: generateHTMLFromDocument,
vue: generateVueFromDocument,
svelte: generateSvelteFromDocument,
flutter: generateFlutterFromDocument,
swiftui: generateSwiftUIFromDocument,
compose: generateComposeFromDocument,
rn: generateReactNativeFromDocument,
'react-native': generateReactNativeFromDocument,
css: (doc: any) => generateCSSVariables(doc.variables ?? {}),
}
function resultToString(result: GeneratorResult): string {
if (typeof result === 'string') return result
// HTML generator returns { html, css }
const parts: string[] = []
if (result.css) parts.push(`<style>\n${result.css}\n</style>`)
parts.push(result.html)
return parts.join('\n\n')
}
export async function cmdExport(
args: string[],
flags: { file?: string; out?: string },
): Promise<void> {
const format = args[0]
if (!format) {
outputError(
`Usage: op export <format> [--out file]\nFormats: ${Object.keys(GENERATORS).join(', ')}`,
)
}
const generator = GENERATORS[format]
if (!generator) {
outputError(`Unknown format: "${format}". Available: ${Object.keys(GENERATORS).join(', ')}`)
}
const filePath = resolveDocPath(flags.file)
const doc = await openDocument(filePath)
const result = generator(doc)
const code = resultToString(result)
if (flags.out) {
await writeFile(flags.out, code, 'utf-8')
output({ ok: true, format, file: flags.out, length: code.length })
} else {
process.stdout.write(code)
}
}

View file

@ -0,0 +1,48 @@
import { handleImportSvg } from '@/mcp/tools/import-svg'
import { openDocument, saveDocument, resolveDocPath } from '@/mcp/document-manager'
import { parseFigFile, figmaAllPagesToPenDocument } from '@zseven-w/pen-figma'
import { readFile } from 'node:fs/promises'
import { output, outputError } from '../output'
interface GlobalFlags {
file?: string
page?: string
}
export async function cmdImportSvg(
args: string[],
flags: GlobalFlags & { parent?: string },
): Promise<void> {
const svgPath = args[0]
if (!svgPath) outputError('Usage: op import:svg <file.svg>')
const result = await handleImportSvg({
filePath: flags.file,
svgPath,
parent: flags.parent ?? null,
pageId: flags.page,
})
output(result)
}
export async function cmdImportFigma(
args: string[],
flags: GlobalFlags & { out?: string },
): Promise<void> {
const figPath = args[0]
if (!figPath) outputError('Usage: op import:figma <file.fig> [--out output.op]')
const buf = await readFile(figPath)
const figFile = parseFigFile(new Uint8Array(buf))
const doc = figmaAllPagesToPenDocument(figFile)
const outPath = flags.out ?? figPath.replace(/\.fig$/, '.op')
await saveDocument(outPath, doc)
output({
ok: true,
filePath: outPath,
pageCount: doc.pages?.length ?? 1,
nodeCount: doc.pages
? doc.pages.reduce((s, p) => s + p.children.length, 0)
: doc.children.length,
})
}

View file

@ -0,0 +1,33 @@
import { handleSnapshotLayout } from '@/mcp/tools/snapshot-layout'
import { handleFindEmptySpace } from '@/mcp/tools/find-empty-space'
import { output, outputError } from '../output'
interface GlobalFlags {
file?: string
page?: string
}
export async function cmdLayout(
flags: GlobalFlags & { parent?: string; depth?: string },
): Promise<void> {
const result = await handleSnapshotLayout({
filePath: flags.file,
parentId: flags.parent,
maxDepth: flags.depth ? parseInt(flags.depth, 10) : undefined,
pageId: flags.page,
})
output(result)
}
export async function cmdFindSpace(
flags: GlobalFlags & { direction?: string; width?: string; height?: string },
): Promise<void> {
const result = await handleFindEmptySpace({
filePath: flags.file,
direction: (flags.direction as 'right' | 'bottom' | 'left' | 'top') ?? 'right',
width: flags.width ? parseInt(flags.width, 10) : undefined,
height: flags.height ? parseInt(flags.height, 10) : undefined,
pageId: flags.page,
})
output(result)
}

View file

@ -0,0 +1,108 @@
import {
handleInsertNode,
handleUpdateNode,
handleDeleteNode,
handleMoveNode,
handleCopyNode,
handleReplaceNode,
} from '@/mcp/tools/node-crud'
import { output, outputError, parseJsonArg } from '../output'
interface GlobalFlags {
file?: string
page?: string
}
export async function cmdInsert(
args: string[],
flags: GlobalFlags & { parent?: string; index?: string; postProcess?: boolean },
): Promise<void> {
const data = (await parseJsonArg(args[0])) as Record<string, unknown>
const result = await handleInsertNode({
filePath: flags.file,
parent: flags.parent ?? null,
data,
postProcess: flags.postProcess,
pageId: flags.page,
})
output(result)
}
export async function cmdUpdate(
args: string[],
flags: GlobalFlags & { postProcess?: boolean },
): Promise<void> {
const nodeId = args[0]
if (!nodeId) outputError('Usage: openpencil update <node-id> <json>')
const data = (await parseJsonArg(args[1])) as Record<string, unknown>
const result = await handleUpdateNode({
filePath: flags.file,
nodeId,
data,
postProcess: flags.postProcess,
pageId: flags.page,
})
output(result)
}
export async function cmdDelete(
args: string[],
flags: GlobalFlags,
): Promise<void> {
const nodeId = args[0]
if (!nodeId) outputError('Usage: openpencil delete <node-id>')
const result = await handleDeleteNode({
filePath: flags.file,
nodeId,
pageId: flags.page,
})
output(result)
}
export async function cmdMove(
args: string[],
flags: GlobalFlags & { parent?: string; index?: string },
): Promise<void> {
const nodeId = args[0]
if (!nodeId) outputError('Usage: openpencil move <node-id> --parent <parent-id>')
const result = await handleMoveNode({
filePath: flags.file,
nodeId,
parent: flags.parent ?? null,
index: flags.index ? parseInt(flags.index, 10) : undefined,
pageId: flags.page,
})
output(result)
}
export async function cmdCopy(
args: string[],
flags: GlobalFlags & { parent?: string },
): Promise<void> {
const sourceId = args[0]
if (!sourceId) outputError('Usage: openpencil copy <source-id> [--parent <parent-id>]')
const result = await handleCopyNode({
filePath: flags.file,
sourceId,
parent: flags.parent ?? null,
pageId: flags.page,
})
output(result)
}
export async function cmdReplace(
args: string[],
flags: GlobalFlags & { postProcess?: boolean },
): Promise<void> {
const nodeId = args[0]
if (!nodeId) outputError('Usage: openpencil replace <node-id> <json>')
const data = (await parseJsonArg(args[1])) as Record<string, unknown>
const result = await handleReplaceNode({
filePath: flags.file,
nodeId,
data,
postProcess: flags.postProcess,
pageId: flags.page,
})
output(result)
}

View file

@ -0,0 +1,87 @@
import {
handleAddPage,
handleRemovePage,
handleRenamePage,
handleReorderPage,
handleDuplicatePage,
} from '@/mcp/tools/pages'
import { handleOpenDocument } from '@/mcp/tools/open-document'
import { output, outputError } from '../output'
interface GlobalFlags {
file?: string
}
export async function cmdPageList(flags: GlobalFlags): Promise<void> {
const result = await handleOpenDocument({ filePath: flags.file })
output({
pages: result.document.pages ?? [
{ id: 'default', name: 'Page 1', childCount: result.document.childCount },
],
})
}
export async function cmdPageAdd(
args: string[],
flags: GlobalFlags & { name?: string },
): Promise<void> {
const result = await handleAddPage({
filePath: flags.file,
name: flags.name ?? args[0],
})
output(result)
}
export async function cmdPageRemove(
args: string[],
flags: GlobalFlags,
): Promise<void> {
const pageId = args[0]
if (!pageId) outputError('Usage: op page remove <page-id>')
const result = await handleRemovePage({
filePath: flags.file,
pageId,
})
output(result)
}
export async function cmdPageRename(
args: string[],
flags: GlobalFlags,
): Promise<void> {
const [pageId, name] = args
if (!pageId || !name) outputError('Usage: op page rename <page-id> <name>')
const result = await handleRenamePage({
filePath: flags.file,
pageId,
name,
})
output(result)
}
export async function cmdPageReorder(
args: string[],
flags: GlobalFlags,
): Promise<void> {
const [pageId, indexStr] = args
if (!pageId || !indexStr) outputError('Usage: op page reorder <page-id> <index>')
const result = await handleReorderPage({
filePath: flags.file,
pageId,
index: parseInt(indexStr, 10),
})
output(result)
}
export async function cmdPageDuplicate(
args: string[],
flags: GlobalFlags,
): Promise<void> {
const pageId = args[0]
if (!pageId) outputError('Usage: op page duplicate <page-id>')
const result = await handleDuplicatePage({
filePath: flags.file,
pageId,
})
output(result)
}

View file

@ -0,0 +1,81 @@
import { handleGetVariables, handleSetVariables, handleSetThemes } from '@/mcp/tools/variables'
import {
handleSaveThemePreset,
handleLoadThemePreset,
handleListThemePresets,
} from '@/mcp/tools/theme-presets'
import { output, outputError, parseJsonArg } from '../output'
interface GlobalFlags {
file?: string
}
export async function cmdVars(flags: GlobalFlags): Promise<void> {
const result = await handleGetVariables({ filePath: flags.file })
output(result)
}
export async function cmdVarsSet(
args: string[],
flags: GlobalFlags & { replace?: boolean },
): Promise<void> {
const data = (await parseJsonArg(args[0])) as Record<string, unknown>
const result = await handleSetVariables({
filePath: flags.file,
variables: data as any,
replace: flags.replace,
})
output(result)
}
export async function cmdThemes(flags: GlobalFlags): Promise<void> {
const result = await handleGetVariables({ filePath: flags.file })
output({ themes: result.themes })
}
export async function cmdThemesSet(
args: string[],
flags: GlobalFlags & { replace?: boolean },
): Promise<void> {
const data = (await parseJsonArg(args[0])) as Record<string, unknown>
const result = await handleSetThemes({
filePath: flags.file,
themes: data as any,
replace: flags.replace,
})
output(result)
}
export async function cmdThemeSave(
args: string[],
flags: GlobalFlags,
): Promise<void> {
const presetPath = args[0]
if (!presetPath) outputError('Usage: op theme:save <file.optheme>')
const result = await handleSaveThemePreset({
filePath: flags.file,
presetPath,
})
output(result)
}
export async function cmdThemeLoad(
args: string[],
flags: GlobalFlags,
): Promise<void> {
const presetPath = args[0]
if (!presetPath) outputError('Usage: op theme:load <file.optheme>')
const result = await handleLoadThemePreset({
filePath: flags.file,
presetPath,
})
output(result)
}
export async function cmdThemeList(args: string[]): Promise<void> {
if (!args[0]) outputError('Usage: op theme:list <directory>')
const result = await handleListThemePresets({
directory: args[0],
})
output(result)
}

View file

@ -0,0 +1,57 @@
/** Port file discovery and app health check. */
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { homedir } from 'node:os'
const PORT_FILE_DIR = '.openpencil'
const PORT_FILE_NAME = '.port'
const PORT_FILE_PATH = join(homedir(), PORT_FILE_DIR, PORT_FILE_NAME)
function isPidAlive(pid: number): boolean {
try {
process.kill(pid, 0)
return true
} catch (err: unknown) {
return (err as NodeJS.ErrnoException).code === 'EPERM'
}
}
export interface AppInfo {
port: number
pid: number
timestamp: number
url: string
}
/** Read port file and return app info, or null if no running instance. */
export async function getAppInfo(): Promise<AppInfo | null> {
try {
const raw = await readFile(PORT_FILE_PATH, 'utf-8')
const { port, pid, timestamp } = JSON.parse(raw) as {
port: number
pid: number
timestamp: number
}
if (!isPidAlive(pid)) return null
return { port, pid, timestamp, url: `http://127.0.0.1:${port}` }
} catch {
return null
}
}
/** Get app URL or throw if not running. */
export async function requireApp(): Promise<string> {
const info = await getAppInfo()
if (!info) {
throw new Error(
'No running OpenPencil instance found. Run `openpencil start` first.',
)
}
return info.url
}
/** Quick check if app is running. */
export async function isAppRunning(): Promise<boolean> {
return (await getAppInfo()) !== null
}

403
apps/cli/src/index.ts Normal file
View file

@ -0,0 +1,403 @@
#!/usr/bin/env node
import pkg from '../package.json'
import { setPretty, output, outputError } from './output'
// --- Arg parsing ---
interface ParsedArgs {
command: string
positionals: string[]
flags: Record<string, string | boolean>
}
function parseArgs(argv: string[]): ParsedArgs {
const args = argv.slice(2)
const positionals: string[] = []
const flags: Record<string, string | boolean> = {}
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith('--')) {
const key = arg.slice(2)
const next = args[i + 1]
if (next && !next.startsWith('--')) {
flags[key] = next
i++
} else {
flags[key] = true
}
} else {
positionals.push(arg)
}
}
return {
command: positionals[0] ?? '',
positionals: positionals.slice(1),
flags,
}
}
// --- Help ---
const HELP = `OpenPencil CLI v${pkg.version}
Usage: op <command> [options]
App:
op start [--desktop|--web] Launch OpenPencil
op stop Stop running instance
op status Check if running
Document:
op open [file.op] Open file or connect to live canvas
op save <file.op> Save current document to file
op get [--type X] [--name Y] [--id Z] [--depth N]
op selection Get current canvas selection
Nodes:
op insert <json> [--parent P] [--index N] [--post-process]
op update <id> <json> [--post-process]
op delete <id>
op move <id> --parent <P> [--index N]
op copy <id> [--parent P]
op replace <id> <json> [--post-process]
Design:
op design <dsl|@file|->
op design:skeleton <json|@file|->
op design:content <section-id> <json|@file|->
op design:refine --root-id <id>
Export:
op export <format> [--out file]
Formats: react, html, vue, svelte, flutter, swiftui, compose, rn, css
Variables & Themes:
op vars Get variables
op vars:set <json> Set variables
op themes Get themes
op themes:set <json> Set themes
op theme:save <file.optheme> Save theme preset
op theme:load <file.optheme> Load theme preset
op theme:list [dir] List theme presets
Pages:
op page list
op page add [--name N]
op page remove <id>
op page rename <id> <name>
op page reorder <id> <index>
op page duplicate <id>
Import:
op import:svg <file.svg> Import SVG file
op import:figma <file.fig> Import Figma file
Layout:
op layout [--parent P] [--depth N]
op find-space [--direction right|bottom|left|top]
Arguments that accept JSON or DSL can be passed as:
<value> Inline string
@filepath Read from file (e.g. @design.txt)
- Read from stdin (e.g. cat design.txt | op design -)
Global Flags:
--file <path> Target .op file (default: live canvas)
--page <id> Target page ID
--pretty Human-readable JSON output
--help Show this help
--version Show version
`
// --- Main ---
async function main(): Promise<void> {
const { command, positionals, flags } = parseArgs(process.argv)
if (flags.pretty) setPretty(true)
if (flags.help || command === 'help') {
process.stdout.write(HELP)
return
}
if (flags.version || command === 'version') {
output({ version: pkg.version })
return
}
if (!command) {
process.stdout.write(HELP)
return
}
const globalFlags = {
file: flags.file as string | undefined,
page: flags.page as string | undefined,
}
switch (command) {
// --- App ---
case 'start': {
const { cmdStart } = await import('./commands/app')
await cmdStart({ desktop: !!flags.desktop, web: !!flags.web })
break
}
case 'stop': {
const { cmdStop } = await import('./commands/app')
await cmdStop()
break
}
case 'status': {
const { cmdStatus } = await import('./commands/app')
await cmdStatus()
break
}
// --- Document ---
case 'open': {
const { cmdOpen } = await import('./commands/document')
await cmdOpen(positionals, globalFlags)
break
}
case 'save': {
const { cmdSave } = await import('./commands/document')
await cmdSave(positionals, globalFlags)
break
}
case 'get': {
const { cmdGet } = await import('./commands/document')
await cmdGet(positionals, {
...globalFlags,
type: flags.type as string | undefined,
name: flags.name as string | undefined,
id: flags.id as string | undefined,
depth: flags.depth as string | undefined,
parent: flags.parent as string | undefined,
})
break
}
case 'selection': {
const { cmdSelection } = await import('./commands/document')
await cmdSelection({ ...globalFlags, depth: flags.depth as string | undefined })
break
}
// --- Nodes ---
case 'insert': {
const { cmdInsert } = await import('./commands/nodes')
await cmdInsert(positionals, {
...globalFlags,
parent: flags.parent as string | undefined,
index: flags.index as string | undefined,
postProcess: !!flags['post-process'],
})
break
}
case 'update': {
const { cmdUpdate } = await import('./commands/nodes')
await cmdUpdate(positionals, {
...globalFlags,
postProcess: !!flags['post-process'],
})
break
}
case 'delete': {
const { cmdDelete } = await import('./commands/nodes')
await cmdDelete(positionals, globalFlags)
break
}
case 'move': {
const { cmdMove } = await import('./commands/nodes')
await cmdMove(positionals, {
...globalFlags,
parent: flags.parent as string | undefined,
index: flags.index as string | undefined,
})
break
}
case 'copy': {
const { cmdCopy } = await import('./commands/nodes')
await cmdCopy(positionals, {
...globalFlags,
parent: flags.parent as string | undefined,
})
break
}
case 'replace': {
const { cmdReplace } = await import('./commands/nodes')
await cmdReplace(positionals, {
...globalFlags,
postProcess: !!flags['post-process'],
})
break
}
// --- Design ---
case 'design': {
const { cmdDesign } = await import('./commands/design')
await cmdDesign(positionals, {
...globalFlags,
postProcess: flags['post-process'] !== false ? true : undefined,
canvasWidth: flags['canvas-width'] as string | undefined,
})
break
}
case 'design:skeleton': {
const { cmdDesignSkeleton } = await import('./commands/design')
await cmdDesignSkeleton(positionals, globalFlags)
break
}
case 'design:content': {
const { cmdDesignContent } = await import('./commands/design')
await cmdDesignContent(positionals, {
...globalFlags,
canvasWidth: flags['canvas-width'] as string | undefined,
})
break
}
case 'design:refine': {
const { cmdDesignRefine } = await import('./commands/design')
await cmdDesignRefine(positionals, {
...globalFlags,
rootId: flags['root-id'] as string | undefined,
canvasWidth: flags['canvas-width'] as string | undefined,
})
break
}
// --- Export ---
case 'export': {
const { cmdExport } = await import('./commands/export')
await cmdExport(positionals, {
file: globalFlags.file,
out: flags.out as string | undefined,
})
break
}
// --- Variables & Themes ---
case 'vars': {
const { cmdVars } = await import('./commands/variables')
await cmdVars(globalFlags)
break
}
case 'vars:set': {
const { cmdVarsSet } = await import('./commands/variables')
await cmdVarsSet(positionals, { ...globalFlags, replace: !!flags.replace })
break
}
case 'themes': {
const { cmdThemes } = await import('./commands/variables')
await cmdThemes(globalFlags)
break
}
case 'themes:set': {
const { cmdThemesSet } = await import('./commands/variables')
await cmdThemesSet(positionals, { ...globalFlags, replace: !!flags.replace })
break
}
case 'theme:save': {
const { cmdThemeSave } = await import('./commands/variables')
await cmdThemeSave(positionals, globalFlags)
break
}
case 'theme:load': {
const { cmdThemeLoad } = await import('./commands/variables')
await cmdThemeLoad(positionals, globalFlags)
break
}
case 'theme:list': {
const { cmdThemeList } = await import('./commands/variables')
await cmdThemeList(positionals)
break
}
// --- Pages ---
case 'page': {
const subCmd = positionals[0]
const subArgs = positionals.slice(1)
switch (subCmd) {
case 'list': {
const { cmdPageList } = await import('./commands/pages')
await cmdPageList(globalFlags)
break
}
case 'add': {
const { cmdPageAdd } = await import('./commands/pages')
await cmdPageAdd(subArgs, { ...globalFlags, name: flags.name as string | undefined })
break
}
case 'remove': {
const { cmdPageRemove } = await import('./commands/pages')
await cmdPageRemove(subArgs, globalFlags)
break
}
case 'rename': {
const { cmdPageRename } = await import('./commands/pages')
await cmdPageRename(subArgs, globalFlags)
break
}
case 'reorder': {
const { cmdPageReorder } = await import('./commands/pages')
await cmdPageReorder(subArgs, globalFlags)
break
}
case 'duplicate': {
const { cmdPageDuplicate } = await import('./commands/pages')
await cmdPageDuplicate(subArgs, globalFlags)
break
}
default:
outputError(`Unknown page subcommand: "${subCmd}". Use: list, add, remove, rename, reorder, duplicate`)
}
break
}
// --- Import ---
case 'import:svg': {
const { cmdImportSvg } = await import('./commands/import')
await cmdImportSvg(positionals, {
...globalFlags,
parent: flags.parent as string | undefined,
})
break
}
case 'import:figma': {
const { cmdImportFigma } = await import('./commands/import')
await cmdImportFigma(positionals, {
...globalFlags,
out: flags.out as string | undefined,
})
break
}
// --- Layout ---
case 'layout': {
const { cmdLayout } = await import('./commands/layout')
await cmdLayout({
...globalFlags,
parent: flags.parent as string | undefined,
depth: flags.depth as string | undefined,
})
break
}
case 'find-space': {
const { cmdFindSpace } = await import('./commands/layout')
await cmdFindSpace({
...globalFlags,
direction: flags.direction as string | undefined,
width: flags.width as string | undefined,
height: flags.height as string | undefined,
})
break
}
default:
outputError(`Unknown command: "${command}". Run "op --help" for usage.`)
}
}
main().catch((err) => {
outputError(err instanceof Error ? err.message : String(err))
})

203
apps/cli/src/launcher.ts Normal file
View file

@ -0,0 +1,203 @@
/** Start/stop OpenPencil app from the CLI. */
import { spawn, fork, execSync } from 'node:child_process'
import { createServer } from 'node:net'
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises'
import { join, dirname } from 'node:path'
import { homedir } from 'node:os'
import { existsSync } from 'node:fs'
import { getAppInfo } from './connection'
const IS_WIN = process.platform === 'win32'
const PORT_FILE_DIR = join(homedir(), '.openpencil')
const PORT_FILE_PATH = join(PORT_FILE_DIR, '.port')
function getFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = createServer()
server.listen(0, '127.0.0.1', () => {
const addr = server.address()
if (addr && typeof addr === 'object') {
const { port } = addr
server.close(() => resolve(port))
} else {
reject(new Error('Failed to get free port'))
}
})
server.on('error', reject)
})
}
async function waitForPortFile(timeoutMs = 15_000): Promise<{ port: number; pid: number }> {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
try {
const raw = await readFile(PORT_FILE_PATH, 'utf-8')
const data = JSON.parse(raw)
if (data.port && data.pid) return data
} catch {
// not ready yet
}
await new Promise((r) => setTimeout(r, 300))
}
throw new Error('Timeout waiting for OpenPencil to start')
}
/** Find the installed desktop app binary. */
function findDesktopBinary(): string | null {
const candidates: string[] = []
if (process.platform === 'darwin') {
candidates.push('/Applications/OpenPencil.app/Contents/MacOS/OpenPencil')
candidates.push(
join(homedir(), 'Applications', 'OpenPencil.app', 'Contents', 'MacOS', 'OpenPencil'),
)
} else if (process.platform === 'win32') {
const localAppData = process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local')
const programFiles = process.env.PROGRAMFILES ?? 'C:\\Program Files'
const programFilesX86 = process.env['PROGRAMFILES(X86)'] ?? 'C:\\Program Files (x86)'
// NSIS per-user install (default)
candidates.push(join(localAppData, 'Programs', 'openpencil', 'OpenPencil.exe'))
// NSIS per-machine install
candidates.push(join(programFiles, 'OpenPencil', 'OpenPencil.exe'))
candidates.push(join(programFilesX86, 'OpenPencil', 'OpenPencil.exe'))
// Portable — same directory as CLI
candidates.push(join(__dirname, '..', 'OpenPencil.exe'))
} else {
// Linux — AppImage, deb, snap, flatpak, manual
candidates.push('/usr/bin/openpencil')
candidates.push('/usr/local/bin/openpencil')
candidates.push(join(homedir(), '.local', 'bin', 'openpencil'))
// AppImage in common download locations
const appImageDirs = [
join(homedir(), 'Applications'),
join(homedir(), 'Downloads'),
join(homedir(), '.local', 'share', 'applications'),
]
for (const dir of appImageDirs) {
// Match OpenPencil*.AppImage (version may vary)
try {
if (existsSync(dir)) {
const files = require('node:fs').readdirSync(dir) as string[]
const appImage = files.find(
(f: string) => f.startsWith('OpenPencil') && f.endsWith('.AppImage'),
)
if (appImage) candidates.push(join(dir, appImage))
}
} catch { /* skip */ }
}
// Snap
candidates.push('/snap/bin/openpencil')
// Flatpak
candidates.push('/var/lib/flatpak/exports/bin/dev.openpencil.app')
candidates.push(join(homedir(), '.local', 'share', 'flatpak', 'exports', 'bin', 'dev.openpencil.app'))
}
for (const path of candidates) {
if (existsSync(path)) return path
}
return null
}
/** Find the Nitro server entry relative to CLI's location. */
function findServerEntry(): string | null {
// When compiled, __dirname points to dist/
// Server is at ../../out/web/server/index.mjs or relative to monorepo root
const candidates = [
join(__dirname, '..', '..', '..', 'out', 'web', 'server', 'index.mjs'),
join(__dirname, '..', '..', 'out', 'web', 'server', 'index.mjs'),
join(__dirname, '..', 'server', 'index.mjs'), // when bundled in Electron resources
]
for (const path of candidates) {
if (existsSync(path)) return path
}
return null
}
export async function startDesktop(): Promise<{ port: number; pid: number }> {
const info = await getAppInfo()
if (info) return { port: info.port, pid: info.pid }
const binary = findDesktopBinary()
if (!binary) {
throw new Error(
'OpenPencil desktop app not found. Install it or use `op start --web` for the web server.',
)
}
const child = spawn(binary, [], {
detached: true,
stdio: 'ignore',
...(IS_WIN ? { windowsHide: true, shell: false } : {}),
})
child.unref()
return waitForPortFile()
}
export async function startWeb(): Promise<{ port: number; pid: number }> {
const info = await getAppInfo()
if (info) return { port: info.port, pid: info.pid }
const entry = findServerEntry()
if (!entry) {
throw new Error(
'Nitro server not found. Run `bun run build` first, or use `op start --desktop`.',
)
}
const port = await getFreePort()
const child = fork(entry, [], {
detached: true,
stdio: 'ignore',
...(IS_WIN ? { windowsHide: true } : {}),
env: {
...process.env,
HOST: '127.0.0.1',
PORT: String(port),
NITRO_HOST: '127.0.0.1',
NITRO_PORT: String(port),
},
})
child.unref()
// Write port file (the Nitro plugin also writes it, but write early for faster discovery)
await mkdir(PORT_FILE_DIR, { recursive: true })
await writeFile(
PORT_FILE_PATH,
JSON.stringify({ port, pid: child.pid, timestamp: Date.now() }),
'utf-8',
)
return { port, pid: child.pid! }
}
export async function stopApp(): Promise<boolean> {
const info = await getAppInfo()
if (!info) return false
try {
if (IS_WIN) {
// Windows: SIGTERM is not supported, use taskkill for graceful shutdown
execSync(`taskkill /PID ${info.pid}`, { stdio: 'ignore' })
} else {
process.kill(info.pid, 'SIGTERM')
}
} catch {
// already dead — try force kill on Windows
if (IS_WIN) {
try {
execSync(`taskkill /F /PID ${info.pid}`, { stdio: 'ignore' })
} catch { /* ignore */ }
}
}
try {
await unlink(PORT_FILE_PATH)
} catch {
// ignore
}
return true
}

73
apps/cli/src/output.ts Normal file
View file

@ -0,0 +1,73 @@
/** Output formatting and process exit helpers for the CLI. */
import { readFile } from 'node:fs/promises'
let prettyMode = false
export function setPretty(v: boolean): void {
prettyMode = v
}
export function output(data: unknown): void {
const json = prettyMode
? JSON.stringify(data, null, 2)
: JSON.stringify(data)
process.stdout.write(json + '\n')
}
export function outputSuccess(data: Record<string, unknown> = {}): void {
output({ ok: true, ...data })
}
export function outputError(message: string, code = 1): never {
process.stderr.write(
JSON.stringify({ error: message }) + '\n',
)
process.exit(code)
}
/** Read all of stdin when not a TTY (piped input). */
export async function readStdin(): Promise<string> {
if (process.stdin.isTTY) return ''
const chunks: Buffer[] = []
for await (const chunk of process.stdin) chunks.push(chunk as Buffer)
return Buffer.concat(chunks).toString('utf-8')
}
/**
* Resolve a CLI argument that may be:
* - `@filepath` read file contents
* - `-` read from stdin
* - raw string use as-is
* - undefined fall back to stdin (if piped)
*/
export async function resolveArg(arg: string | undefined): Promise<string> {
if (arg === '-') {
const stdin = await readStdin()
if (!stdin.trim()) outputError('No data received from stdin.')
return stdin.trim()
}
if (arg && arg.startsWith('@')) {
const filePath = arg.slice(1)
try {
return (await readFile(filePath, 'utf-8')).trim()
} catch {
outputError(`Cannot read file: ${filePath}`)
}
}
if (arg) return arg
// No explicit arg — try stdin if piped
const stdin = await readStdin()
if (stdin.trim()) return stdin.trim()
outputError('No data provided. Pass as argument, @filepath, or pipe via stdin.')
}
/** Parse JSON from a CLI positional arg, @filepath, or stdin. */
export async function parseJsonArg(arg: string | undefined): Promise<unknown> {
const raw = await resolveArg(arg)
try {
return JSON.parse(raw)
} catch {
outputError(`Invalid JSON: ${raw.slice(0, 200)}...`)
}
}

19
apps/cli/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": false,
"outDir": "dist",
"rootDir": "src",
"paths": {
"@/*": ["../../apps/web/src/*"]
}
},
"include": ["src/**/*.ts"],
"references": []
}

View file

@ -7,11 +7,10 @@ directories:
buildResources: apps/desktop/build
files:
- from: out/desktop
to: .
filter:
- "**/*"
- "!node_modules"
- package.json
- out/desktop/**/*
- "!**/node_modules/**"
- "!**/.git/**"
extraResources:
- from: out/web/server
@ -20,6 +19,8 @@ extraResources:
to: public
- from: out/mcp-server.cjs
to: mcp-server.cjs
- from: apps/cli/dist/openpencil-cli.cjs
to: openpencil-cli.cjs
mac:
category: public.app-category.graphics-design

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/desktop",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module"
}

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/web",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module"
}

View file

@ -9,7 +9,6 @@ import {
buildSpawnClaudeCodeProcess,
getClaudeAgentDebugFilePath,
} from '../../utils/resolve-claude-agent-env'
/** Pattern for detecting sensitive data in debug log output */
export const SENSITIVE_LOG_PATTERN = /ANTHROPIC_API_KEY=|Authorization:\s*Bearer|api[_-]?key\s*[:=]/i
@ -51,26 +50,68 @@ async function readDebugTail(path?: string, maxLines = 40): Promise<string[] | u
function buildClaudeExitHint(rawError: string, debugTail?: string[]): string | undefined {
if (!/process exited with code 1/i.test(rawError)) return undefined
if (!debugTail || debugTail.length === 0) return undefined
const text = debugTail.join('\n')
const hints: string[] = []
if (/Failed to save config with lock: Error: EPERM|operation not permitted, .*\.claude\.json/i.test(text)) {
hints.push('Claude Code cannot write ~/.claude.json in the current runtime (permission denied).')
if (debugTail && debugTail.length > 0) {
const text = debugTail.join('\n')
if (/Failed to save config with lock: Error: EPERM|operation not permitted, .*\.claude\.json/i.test(text)) {
hints.push(
'Claude Code cannot write ~/.claude.json (permission denied). ' +
'On Windows, try running as Administrator or manually create the file: echo {} > %USERPROFILE%\\.claude.json',
)
}
if (/Connection error|Could not resolve host|Failed to connect|ECONNREFUSED|ETIMEDOUT/i.test(text)) {
hints.push('Upstream API connection failed. Check proxy/DNS/network reachability to your ANTHROPIC_BASE_URL.')
}
if (/ANTHROPIC_CUSTOM_HEADERS present: false, has Authorization header: false/i.test(text)) {
hints.push(
'No API auth header detected. Run "claude login" to authenticate, ' +
'or set ANTHROPIC_API_KEY in ~/.claude/settings.json ' +
'(env: { "ANTHROPIC_API_KEY": "sk-..." }).',
)
}
if (/invalid.*api.?key|unauthorized|401|authentication/i.test(text)) {
hints.push('API key authentication failed. Verify your ANTHROPIC_API_KEY is correct and has not expired.')
}
if (/ENOTFOUND|getaddrinfo/i.test(text)) {
hints.push('DNS resolution failed for the API endpoint. Check your ANTHROPIC_BASE_URL is correct.')
}
if (/certificate|CERT_|ssl|tls/i.test(text)) {
hints.push(
'TLS/SSL certificate error. If using a corporate proxy, set NODE_TLS_REJECT_UNAUTHORIZED=0 in ~/.claude/settings.json env (not recommended for production).',
)
}
}
if (/Connection error|Could not resolve host|Failed to connect/i.test(text)) {
hints.push('Upstream API connection failed (check proxy/DNS/network reachability to your ANTHROPIC_BASE_URL).')
}
if (/ANTHROPIC_CUSTOM_HEADERS present: false, has Authorization header: false/i.test(text)) {
// Detect proxy/custom endpoint — most common cause of exit-code-1 on Windows
const env = process.env
const hasProxy = !!(env.http_proxy || env.https_proxy || env.HTTP_PROXY || env.HTTPS_PROXY)
const hasCustomBaseUrl = !!env.ANTHROPIC_BASE_URL
if ((hasProxy || hasCustomBaseUrl) && !env.NODE_TLS_REJECT_UNAUTHORIZED) {
hints.push(
'No API auth header detected. Run "claude login" to authenticate, ' +
'or set ANTHROPIC_API_KEY in ~/.claude/settings.json ' +
'(env: { "ANTHROPIC_API_KEY": "sk-..." }).',
'Proxy or custom ANTHROPIC_BASE_URL detected but NODE_TLS_REJECT_UNAUTHORIZED is not set. ' +
'If your proxy uses a self-signed or corporate certificate, add ' +
'"NODE_TLS_REJECT_UNAUTHORIZED": "0" to the env section of ~/.claude/settings.json.',
)
}
if (hints.length === 0) return undefined
return `${rawError}\n${hints.join(' ')}`
// If no debug info available, provide generic Windows guidance
if (hints.length === 0) {
const isWin = process.platform === 'win32'
if (isWin) {
hints.push(
'Claude Code process crashed on Windows. Common fixes: ' +
'(1) Ensure ~/.claude.json exists: echo {} > %USERPROFILE%\\.claude.json ' +
'(2) Check ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL in ~/.claude/settings.json ' +
'(3) If using a proxy, set NODE_TLS_REJECT_UNAUTHORIZED=0 in env.',
)
} else {
return undefined
}
}
return `${rawError}\n${hints.join('\n')}`
}
/**
@ -210,6 +251,7 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
debugFile = getClaudeAgentDebugFilePath()
const claudePath = resolveClaudeCli()
const spawnProcess = buildSpawnClaudeCodeProcess()
const thinking = getAgentThinkingConfig(body)
// When images are attached, strip the "NEVER use tools" restriction from
@ -238,7 +280,7 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
env,
...(debugFile ? { debugFile } : {}),
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}),
...(spawnProcess ? { spawnClaudeCodeProcess: spawnProcess } : {}),
},
})
@ -288,7 +330,7 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
env,
...(debugFile ? { debugFile } : {}),
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}),
...(spawnProcess ? { spawnClaudeCodeProcess: spawnProcess } : {}),
},
})
@ -332,9 +374,16 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
)
} catch (error) {
const rawContent = error instanceof Error ? error.message : 'Unknown error'
const tail = await readDebugTail(debugFile)
const hintedContent = buildClaudeExitHint(rawContent, tail)
const content = hintedContent ?? rawContent
// Append debug log tail so the user can see what Claude Code actually reported
let content = hintedContent ?? rawContent
if (tail && tail.length > 0 && /process exited with code/i.test(rawContent)) {
const debugSnippet = tail.slice(-10).join('\n')
content += `\n\n[Debug log]:\n${debugSnippet}`
}
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'error', content })}\n\n`),
)

View file

@ -1,5 +1,5 @@
import { defineEventHandler, readBody, setResponseHeaders } from 'h3'
import { existsSync } from 'node:fs'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import type { GroupedModel } from '../../../src/types/agent-settings'
import { resolveClaudeCli } from '../../utils/resolve-claude-cli'
@ -121,6 +121,8 @@ async function connectClaudeCode(): Promise<ConnectResult> {
serverLog.info(`[connect-agent] claude env keys: ${Object.keys(env).join(', ')}`)
serverLog.info(`[connect-agent] claude debugFile: ${debugFile ?? 'none'}`)
const spawnProcess = buildSpawnClaudeCodeProcess()
const q = query({
prompt: '',
options: {
@ -131,7 +133,7 @@ async function connectClaudeCode(): Promise<ConnectResult> {
env,
...(debugFile ? { debugFile } : {}),
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}),
...(spawnProcess ? { spawnClaudeCodeProcess: spawnProcess } : {}),
},
})
@ -168,7 +170,34 @@ async function connectClaudeCode(): Promise<ConnectResult> {
serverLog.info('[connect-agent] using fallback model list (proxy detected)')
const fallbackEnv = buildClaudeAgentEnv()
const claudeInfo = buildClaudeConnectionInfo(fallbackEnv, null)
return { connected: true, models: FALLBACK_CLAUDE_MODELS, ...claudeInfo }
// Read debug log for diagnostic warning — the process may have written
// useful info (e.g. TLS errors, auth failures) before exiting
let warning: string | undefined
const debugPath = getClaudeAgentDebugFilePath()
if (debugPath) {
try {
const raw = readFileSync(debugPath, 'utf-8')
const lines = raw.split('\n').filter((l) => l.trim().length > 0)
const tail = lines.slice(-10).join('\n')
if (tail) {
// Surface specific issues as warnings
if (/certificate|CERT_|ssl|tls/i.test(tail)) {
warning = 'TLS/SSL error detected. If using a proxy, add "NODE_TLS_REJECT_UNAUTHORIZED": "0" to ~/.claude/settings.json env.'
} else if (/EPERM|operation not permitted/i.test(tail)) {
warning = 'Permission error writing config. Try: echo {} > %USERPROFILE%\\.claude.json'
} else if (/stderr exit=/i.test(tail)) {
// Show captured stderr
const stderrMatch = tail.match(/\[stderr exit=\d+\]\s*(.+)/s)
if (stderrMatch) {
warning = `Claude Code stderr: ${stderrMatch[1].slice(0, 300)}`
}
}
}
} catch { /* debug file not available */ }
}
return { connected: true, models: FALLBACK_CLAUDE_MODELS, ...claudeInfo, ...(warning ? { warning } : {}) }
}
return { connected: false, models: [], error: friendlyClaudeError(msg) }
}

View file

@ -41,13 +41,21 @@ function resolveMcpServerPath(): string {
const electronPath = join(electronResources, 'mcp-server.cjs')
if (existsSync(electronPath)) return electronPath
}
// Try dist/ in the project root (dev + web build)
const projectDist = resolve(process.cwd(), 'dist', 'mcp-server.cjs')
if (existsSync(projectDist)) return projectDist
// Fallback: try relative to this file
const serverDist = resolve(__dirname, '..', '..', '..', 'dist', 'mcp-server.cjs')
if (existsSync(serverDist)) return serverDist
return projectDist // Return expected path even if not yet compiled
// Monorepo root: cwd may be apps/web (dev) or project root (Electron)
// Walk up from cwd to find monorepo root (has package.json with workspaces)
let root = process.cwd()
for (let i = 0; i < 5; i++) {
const candidate = join(root, 'out', 'mcp-server.cjs')
if (existsSync(candidate)) return candidate
const parent = dirname(root)
if (parent === root) break
root = parent
}
// Fallback: try relative to this file (Nitro bundles server code)
const fromFile = resolve(__dirname, '..', '..', '..', 'out', 'mcp-server.cjs')
if (existsSync(fromFile)) return fromFile
// Return expected monorepo root path
return join(root, 'out', 'mcp-server.cjs')
}
/**

View file

@ -1,8 +1,10 @@
import { spawn } from 'node:child_process'
import { mkdirSync, readFileSync } from 'node:fs'
import { homedir, tmpdir } from 'node:os'
import { mkdirSync, readFileSync, writeFileSync, existsSync, appendFileSync } from 'node:fs'
import { homedir, tmpdir, platform } from 'node:os'
import { join } from 'node:path'
const IS_WIN = platform() === 'win32'
type EnvLike = Record<string, string | undefined>
interface ClaudeSettings {
@ -72,11 +74,41 @@ function isValidJson(str: string): boolean {
}
}
/**
* On Windows, Claude Code SDK may fail with EPERM when writing to ~/.claude.json
* or ~/.claude/ config files. Ensure the directory and config file exist and are writable.
*/
function ensureClaudeConfigWritable(): void {
if (!IS_WIN) return
try {
const claudeDir = join(homedir(), '.claude')
mkdirSync(claudeDir, { recursive: true })
// Ensure .claude.json exists — Claude SDK crashes if it can't write/lock it
const configFile = join(homedir(), '.claude.json')
if (!existsSync(configFile)) {
writeFileSync(configFile, '{}', 'utf-8')
}
// Ensure credentials.json exists — SDK may crash trying to read/write it
const credFile = join(claudeDir, 'credentials.json')
if (!existsSync(credFile)) {
writeFileSync(credFile, '{}', 'utf-8')
}
// Ensure statsig/ cache dir exists — SDK crashes writing feature gate cache
const statsigDir = join(claudeDir, 'statsig')
mkdirSync(statsigDir, { recursive: true })
} catch {
// Best effort — if we can't fix it, the SDK error hint will guide the user
}
}
/**
* Build env passed to Claude Agent SDK.
* Priority: current process env > ~/.claude/settings.json env.
*/
export function buildClaudeAgentEnv(): EnvLike {
// On Windows, pre-create config files to avoid EPERM errors
ensureClaudeConfigWritable()
const fromSettings = readClaudeSettingsEnv()
const fromProcess = process.env as EnvLike
@ -102,6 +134,50 @@ export function buildClaudeAgentEnv(): EnvLike {
// Running inside Claude terminal can break nested Claude invocations.
delete merged.CLAUDECODE
// Remove Electron-specific env vars that may confuse spawned CLI processes
delete merged.ELECTRON_RUN_AS_NODE
delete merged.ELECTRON_RESOURCES_PATH
delete merged.CHROME_CRASHPAD_PIPE_NAME
// Enable Agent SDK debug stderr so we can capture CLI crash diagnostics.
// Without this, the SDK sets stderr to "ignore" and crash output is lost.
if (!merged.DEBUG_CLAUDE_AGENT_SDK) {
merged.DEBUG_CLAUDE_AGENT_SDK = '1'
}
// Apply NODE_TLS_REJECT_UNAUTHORIZED to the current process as well,
// so Node.js HTTP/TLS in this process (used by the SDK internals) respects it.
if (merged.NODE_TLS_REJECT_UNAUTHORIZED && !process.env.NODE_TLS_REJECT_UNAUTHORIZED) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = merged.NODE_TLS_REJECT_UNAUTHORIZED
}
if (IS_WIN) {
// Redirect Claude debug output to temp to avoid write permission issues
if (!merged.CLAUDE_DEBUG_FILE) {
const debugPath = getClaudeAgentDebugFilePath()
if (debugPath) merged.CLAUDE_DEBUG_FILE = debugPath
}
// Set CLAUDE_CONFIG_DIR to a writable temp location as fallback
// if the default ~/.claude directory is not writable (common in Windows Electron)
if (!merged.CLAUDE_CONFIG_DIR) {
try {
const fallbackDir = join(tmpdir(), 'openpencil-claude-config')
mkdirSync(fallbackDir, { recursive: true })
// Only use fallback if we can't write to the default location
const defaultDir = join(homedir(), '.claude')
const testFile = join(defaultDir, '.write-test')
try {
writeFileSync(testFile, '', 'utf-8')
const { unlinkSync } = require('node:fs')
unlinkSync(testFile)
} catch {
// Default dir is not writable — use fallback
merged.CLAUDE_CONFIG_DIR = fallbackDir
}
} catch { /* ignore */ }
}
}
return merged
}
@ -126,7 +202,11 @@ export function getClaudeAgentDebugFilePath(): string | undefined {
*
* - `.cmd` files: use `cmd.exe /c` (PowerShell can't run .cmd directly)
* - `.ps1` files: use `powershell.exe`
* - `.exe` files or others: use `cmd.exe /c` as safe default
* - `.exe` files: spawned directly without shell
* - Others: use `cmd.exe /c` as safe default
*
* Also captures stderr to the debug file when Claude Code crashes early,
* the debug file may be empty but stderr often contains the root cause.
*/
export function buildSpawnClaudeCodeProcess() {
if (process.platform !== 'win32') return undefined
@ -134,24 +214,56 @@ export function buildSpawnClaudeCodeProcess() {
const cmd = options.command
const isPowerShell = cmd.endsWith('.ps1')
let child
if (isPowerShell) {
// For .ps1 scripts, invoke via PowerShell
const psArgs = ['-ExecutionPolicy', 'Bypass', '-File', cmd, ...options.args]
return spawn('powershell.exe', psArgs, {
child = spawn('powershell.exe', psArgs, {
cwd: options.cwd,
env: options.env as NodeJS.ProcessEnv,
signal: options.signal,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
})
} else if (cmd.endsWith('.exe')) {
// .exe files can be spawned directly without shell
child = spawn(cmd, options.args, {
cwd: options.cwd,
env: options.env as NodeJS.ProcessEnv,
signal: options.signal,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
})
} else {
// For .cmd or extensionless binaries, use shell
child = spawn(cmd, options.args, {
cwd: options.cwd,
env: options.env as NodeJS.ProcessEnv,
signal: options.signal,
stdio: ['pipe', 'pipe', 'pipe'],
shell: true,
windowsHide: true,
})
}
// For .cmd, .exe, or extensionless binaries, use cmd.exe /c
return spawn(cmd, options.args, {
cwd: options.cwd,
env: options.env as NodeJS.ProcessEnv,
signal: options.signal,
stdio: ['pipe', 'pipe', 'pipe'],
shell: true,
// Capture stderr to debug file — helps diagnose crashes where the process
// exits before writing anything to the debug log
const stderrChunks: Buffer[] = []
child.stderr?.on('data', (chunk: Buffer) => { stderrChunks.push(chunk) })
child.on('exit', (code) => {
if (code !== 0 && stderrChunks.length > 0) {
const stderr = Buffer.concat(stderrChunks).toString('utf-8').trim()
if (stderr) {
const debugPath = getClaudeAgentDebugFilePath()
if (debugPath) {
try {
appendFileSync(debugPath, `\n[stderr exit=${code}] ${stderr}\n`)
} catch { /* best effort */ }
}
}
}
})
return child
}
}

View file

@ -226,19 +226,6 @@ export class SkiaEngine {
this.renderer.drawNodeWithSelection(canvas, rn, selectedIds)
}
// Draw frame labels (root frames + reusable components + instances at any depth)
for (const rn of this.renderNodes) {
if (!rn.node.name) continue
const isRootFrame = rn.node.type === 'frame' && !rn.clipRect
const isReusable = this.reusableIds.has(rn.node.id)
const isInstance = this.instanceIds.has(rn.node.id)
if (!isRootFrame && !isReusable && !isInstance) continue
this.renderer.drawFrameLabelColored(
canvas, rn.node.name, rn.absX, rn.absY,
isReusable, isInstance, this.zoom,
)
}
// Draw agent indicators (glow, badges, node borders, preview fills)
const agentIndicators = getActiveAgentIndicators()
const agentFrames = getActiveAgentFrames()
@ -346,6 +333,23 @@ export class SkiaEngine {
}
canvas.restore()
// Draw frame labels outside viewport transform so fontSize stays constant
// (avoids Math.ceil(12/zoom) integer-boundary jumps causing label size flicker)
canvas.save()
canvas.scale(dpr, dpr)
for (const rn of this.renderNodes) {
if (!rn.node.name) continue
const isRootFrame = rn.node.type === 'frame' && !rn.clipRect
const isReusable = this.reusableIds.has(rn.node.id)
const isInstance = this.instanceIds.has(rn.node.id)
if (!isRootFrame && !isReusable && !isInstance) continue
const sx = rn.absX * this.zoom + this.panX
const sy = rn.absY * this.zoom + this.panY
this.renderer.drawFrameLabelColored(canvas, rn.node.name, sx, sy, isReusable, isInstance, 1)
}
canvas.restore()
this.surface.flush()
// Keep animating while agent overlays are active (spinning dot + node flashes)

View file

@ -1,5 +1,5 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { Send, Plus, ChevronDown, ChevronUp, Check, MessageSquare, Loader2, Paperclip, X, Square, Zap } from 'lucide-react'
import { Send, Plus, ChevronDown, ChevronUp, Check, MessageSquare, Loader2, Paperclip, X, Square, Zap, Search } from 'lucide-react'
import { nanoid } from 'nanoid'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
@ -162,6 +162,8 @@ export default function AIChatPanel() {
const providers = useAgentSettingsStore((s) => s.providers)
const providersHydrated = useAgentSettingsStore((s) => s.isHydrated)
const [modelDropdownOpen, setModelDropdownOpen] = useState(false)
const [modelSearch, setModelSearch] = useState('')
const modelSearchRef = useRef<HTMLInputElement>(null)
const pendingAttachments = useAIStore((s) => s.pendingAttachments)
const addPendingAttachment = useAIStore((s) => s.addPendingAttachment)
const removePendingAttachment = useAIStore((s) => s.removePendingAttachment)
@ -233,7 +235,11 @@ export default function AIChatPanel() {
// Close model dropdown when clicking outside
useEffect(() => {
if (!modelDropdownOpen) return
if (!modelDropdownOpen) {
setModelSearch('')
return
}
requestAnimationFrame(() => modelSearchRef.current?.focus())
const handler = (e: MouseEvent) => {
const panel = panelRef.current
if (panel && !panel.contains(e.target as Node)) {
@ -621,7 +627,7 @@ export default function AIChatPanel() {
/>
{/* --- Bottom bar: model selector + actions --- */}
<div className="flex items-center justify-between px-2 pb-2">
<div className="relative flex items-center justify-between px-2 pb-2">
{/* Model selector */}
<button
type="button"
@ -703,13 +709,63 @@ export default function AIChatPanel() {
)}
</div>
</div>
</div>
{/* Upward model dropdown */}
{modelDropdownOpen && availableModels.length > 0 && (
<div className="absolute bottom-full left-2 right-2 mb-1 z-[60] rounded-lg border border-border bg-card shadow-xl py-1 max-h-72 overflow-y-auto">
{modelGroups.length > 0
? modelGroups.map((group) => {
{/* Upward model dropdown */}
{modelDropdownOpen && availableModels.length > 0 && (
<div className="absolute bottom-full left-0 right-0 mb-1 z-[60] rounded-lg border border-border bg-card shadow-xl py-1 max-h-72 flex flex-col">
{/* Search input */}
<div className="px-2 pt-1 pb-1.5 border-b border-border shrink-0">
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-secondary/50">
<Search size={12} className="text-muted-foreground shrink-0" />
<input
ref={modelSearchRef}
value={modelSearch}
onChange={(e) => setModelSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
setModelDropdownOpen(false)
}
}}
placeholder={t('ai.searchModels')}
className="w-full bg-transparent text-xs text-foreground placeholder-muted-foreground outline-none"
/>
{modelSearch && (
<button
type="button"
onClick={() => setModelSearch('')}
className="text-muted-foreground hover:text-foreground shrink-0"
>
<X size={10} />
</button>
)}
</div>
</div>
<div className="overflow-y-auto">
{(() => {
const q = modelSearch.toLowerCase().trim()
if (modelGroups.length > 0) {
const filtered = modelGroups
.map((group) => ({
...group,
models: group.models.filter(
(m) =>
!q ||
m.displayName.toLowerCase().includes(q) ||
m.value.toLowerCase().includes(q) ||
group.providerName.toLowerCase().includes(q),
),
}))
.filter((group) => group.models.length > 0)
if (filtered.length === 0) {
return (
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
{t('ai.noModelsFound')}
</div>
)
}
return filtered.map((group) => {
const GIcon = PROVIDER_ICON[group.provider]
return (
<div key={group.provider}>
@ -740,7 +796,7 @@ export default function AIChatPanel() {
{isSelected && <Check size={12} />}
</span>
<span className="font-medium">{m.displayName}</span>
{idx === 0 && (
{idx === 0 && !q && (
<span className="text-[9px] text-muted-foreground bg-secondary px-1 py-0.5 rounded ml-auto">
{t('common.best')}
</span>
@ -751,32 +807,52 @@ export default function AIChatPanel() {
</div>
)
})
: availableModels.map((m) => {
const isSelected = m.value === model
return (
<button
key={m.value}
type="button"
onClick={() => {
selectModel(m.value)
setModelDropdownOpen(false)
}}
className={cn(
'w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors',
isSelected
? 'bg-secondary text-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
>
<span className="w-3.5 shrink-0">
{isSelected && <Check size={12} />}
</span>
<span className="font-medium">{m.displayName}</span>
</button>
)
})}
}
const filtered = availableModels.filter(
(m) =>
!q ||
m.displayName.toLowerCase().includes(q) ||
m.value.toLowerCase().includes(q),
)
if (filtered.length === 0) {
return (
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
{t('ai.noModelsFound')}
</div>
)
}
return filtered.map((m) => {
const isSelected = m.value === model
return (
<button
key={m.value}
type="button"
onClick={() => {
selectModel(m.value)
setModelDropdownOpen(false)
}}
className={cn(
'w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors',
isSelected
? 'bg-secondary text-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
>
<span className="w-3.5 shrink-0">
{isSelected && <Check size={12} />}
</span>
<span className="font-medium">{m.displayName}</span>
</button>
)
})
})()}
</div>
</div>
)}
)}
</div>
</div>
</div>
)

View file

@ -317,6 +317,8 @@ const de: TranslationKeys = {
'ai.sendMessage': 'Nachricht senden',
'ai.loadingModels': 'Modelle werden geladen...',
'ai.noModelsConnected': 'Keine Modelle verbunden',
'ai.searchModels': 'Modelle suchen...',
'ai.noModelsFound': 'Keine Modelle gefunden',
'ai.quickAction.loginScreen': 'Einen mobilen Anmeldebildschirm gestalten',
'ai.quickAction.loginScreenPrompt':
'Design a modern mobile login screen with email input, password input, login button, and social login options',

View file

@ -313,6 +313,8 @@ const en = {
'ai.sendMessage': 'Send message',
'ai.loadingModels': 'Loading models...',
'ai.noModelsConnected': 'No models connected',
'ai.searchModels': 'Search models...',
'ai.noModelsFound': 'No models found',
'ai.quickAction.loginScreen': 'Design a mobile login screen',
'ai.quickAction.loginScreenPrompt':
'Design a modern mobile login screen with email input, password input, login button, and social login options',

View file

@ -318,6 +318,8 @@ const es: TranslationKeys = {
'ai.sendMessage': 'Enviar mensaje',
'ai.loadingModels': 'Cargando modelos...',
'ai.noModelsConnected': 'Sin modelos conectados',
'ai.searchModels': 'Buscar modelos...',
'ai.noModelsFound': 'No se encontraron modelos',
'ai.quickAction.loginScreen':
'Diseñar una pantalla de inicio de sesión móvil',
'ai.quickAction.loginScreenPrompt':

View file

@ -318,6 +318,8 @@ const fr: TranslationKeys = {
'ai.sendMessage': 'Envoyer le message',
'ai.loadingModels': 'Chargement des modèles...',
'ai.noModelsConnected': 'Aucun modèle connecté',
'ai.searchModels': 'Rechercher des modèles...',
'ai.noModelsFound': 'Aucun modèle trouvé',
'ai.quickAction.loginScreen': 'Concevoir un écran de connexion mobile',
'ai.quickAction.loginScreenPrompt':
'Concevez un écran de connexion mobile moderne avec un champ e-mail, un champ mot de passe, un bouton de connexion et des options de connexion sociale',

View file

@ -315,6 +315,8 @@ const hi: TranslationKeys = {
'ai.sendMessage': 'संदेश भेजें',
'ai.loadingModels': 'मॉडल लोड हो रहे हैं...',
'ai.noModelsConnected': 'कोई मॉडल कनेक्ट नहीं है',
'ai.searchModels': 'मॉडल खोजें...',
'ai.noModelsFound': 'कोई मॉडल नहीं मिला',
'ai.quickAction.loginScreen': 'मोबाइल लॉगिन स्क्रीन डिज़ाइन करें',
'ai.quickAction.loginScreenPrompt':
'ईमेल इनपुट, पासवर्ड इनपुट, लॉगिन बटन और सोशल लॉगिन विकल्पों के साथ एक आधुनिक मोबाइल लॉगिन स्क्रीन डिज़ाइन करें',

View file

@ -315,6 +315,8 @@ const id: TranslationKeys = {
'ai.sendMessage': 'Kirim pesan',
'ai.loadingModels': 'Memuat model...',
'ai.noModelsConnected': 'Tidak ada model terhubung',
'ai.searchModels': 'Cari model...',
'ai.noModelsFound': 'Model tidak ditemukan',
'ai.quickAction.loginScreen': 'Desain layar login mobile',
'ai.quickAction.loginScreenPrompt':
'Desain layar login mobile modern dengan input email, input kata sandi, tombol login, dan opsi login sosial',

View file

@ -319,6 +319,8 @@ const ja: TranslationKeys = {
'ai.sendMessage': 'メッセージを送信',
'ai.loadingModels': 'モデルを読み込み中...',
'ai.noModelsConnected': 'モデルが接続されていません',
'ai.searchModels': 'モデルを検索...',
'ai.noModelsFound': 'モデルが見つかりません',
'ai.quickAction.loginScreen': 'モバイルログイン画面をデザイン',
'ai.quickAction.loginScreenPrompt':
'メール入力、パスワード入力、ログインボタン、ソーシャルログインオプションを含む、モダンなモバイルログイン画面をデザインしてください',

View file

@ -315,6 +315,8 @@ const ko: TranslationKeys = {
'ai.sendMessage': '메시지 보내기',
'ai.loadingModels': '모델 로딩 중...',
'ai.noModelsConnected': '연결된 모델 없음',
'ai.searchModels': '모델 검색...',
'ai.noModelsFound': '일치하는 모델이 없습니다',
'ai.quickAction.loginScreen': '모바일 로그인 화면 디자인',
'ai.quickAction.loginScreenPrompt':
'이메일 입력, 비밀번호 입력, 로그인 버튼, 소셜 로그인 옵션이 있는 모던 모바일 로그인 화면을 디자인해 주세요',

View file

@ -317,6 +317,8 @@ const pt: TranslationKeys = {
'ai.sendMessage': 'Enviar mensagem',
'ai.loadingModels': 'Carregando modelos...',
'ai.noModelsConnected': 'Nenhum modelo conectado',
'ai.searchModels': 'Pesquisar modelos...',
'ai.noModelsFound': 'Nenhum modelo encontrado',
'ai.quickAction.loginScreen': 'Criar uma tela de login mobile',
'ai.quickAction.loginScreenPrompt':
'Design a modern mobile login screen with email input, password input, login button, and social login options',

View file

@ -317,6 +317,8 @@ const ru: TranslationKeys = {
'ai.sendMessage': 'Отправить сообщение',
'ai.loadingModels': 'Загрузка моделей...',
'ai.noModelsConnected': 'Нет подключённых моделей',
'ai.searchModels': 'Поиск моделей...',
'ai.noModelsFound': 'Модели не найдены',
'ai.quickAction.loginScreen': 'Создать экран входа для мобильного',
'ai.quickAction.loginScreenPrompt':
'Design a modern mobile login screen with email input, password input, login button, and social login options',

View file

@ -315,6 +315,8 @@ const th: TranslationKeys = {
'ai.sendMessage': 'ส่งข้อความ',
'ai.loadingModels': 'กำลังโหลดโมเดล...',
'ai.noModelsConnected': 'ไม่มีโมเดลที่เชื่อมต่อ',
'ai.searchModels': 'ค้นหาโมเดล...',
'ai.noModelsFound': 'ไม่พบโมเดล',
'ai.quickAction.loginScreen': 'ออกแบบหน้าจอเข้าสู่ระบบมือถือ',
'ai.quickAction.loginScreenPrompt':
'ออกแบบหน้าจอเข้าสู่ระบบมือถือที่ทันสมัย พร้อมช่องกรอกอีเมล รหัสผ่าน ปุ่มเข้าสู่ระบบ และตัวเลือกเข้าสู่ระบบผ่านโซเชียล',

View file

@ -315,6 +315,8 @@ const tr: TranslationKeys = {
'ai.sendMessage': 'Mesaj gönder',
'ai.loadingModels': 'Modeller yükleniyor...',
'ai.noModelsConnected': 'Bağlı model yok',
'ai.searchModels': 'Model ara...',
'ai.noModelsFound': 'Model bulunamadı',
'ai.quickAction.loginScreen': 'Mobil giriş ekranı tasarla',
'ai.quickAction.loginScreenPrompt':
'E-posta girişi, şifre girişi, giriş butonu ve sosyal giriş seçenekleri ile modern bir mobil giriş ekranı tasarla',

View file

@ -315,6 +315,8 @@ const vi: TranslationKeys = {
'ai.sendMessage': 'Gửi tin nhắn',
'ai.loadingModels': 'Đang tải mô hình...',
'ai.noModelsConnected': 'Chưa kết nối mô hình nào',
'ai.searchModels': 'Tìm kiếm mô hình...',
'ai.noModelsFound': 'Không tìm thấy mô hình',
'ai.quickAction.loginScreen': 'Thiết kế màn hình đăng nhập di động',
'ai.quickAction.loginScreenPrompt':
'Thiết kế màn hình đăng nhập di động hiện đại với ô nhập email, ô nhập mật khẩu, nút đăng nhập và các tuỳ chọn đăng nhập bằng mạng xã hội',

View file

@ -309,6 +309,8 @@ const zhTW: TranslationKeys = {
'ai.sendMessage': '傳送訊息',
'ai.loadingModels': '正在載入模型...',
'ai.noModelsConnected': '尚未連線模型',
'ai.searchModels': '搜尋模型...',
'ai.noModelsFound': '未找到匹配的模型',
'ai.quickAction.loginScreen': '設計行動裝置登入頁面',
'ai.quickAction.loginScreenPrompt':
'設計一個現代的行動裝置登入頁面,包含電子郵件輸入框、密碼輸入框、登入按鈕和社群登入選項',

View file

@ -309,6 +309,8 @@ const zh: TranslationKeys = {
'ai.sendMessage': '发送消息',
'ai.loadingModels': '正在加载模型...',
'ai.noModelsConnected': '未连接模型',
'ai.searchModels': '搜索模型...',
'ai.noModelsFound': '未找到匹配的模型',
'ai.quickAction.loginScreen': '设计一个移动端登录页面',
'ai.quickAction.loginScreenPrompt':
'设计一个现代的移动端登录页面,包含邮箱输入框、密码输入框、登录按钮和社交登录选项',

View file

@ -482,21 +482,63 @@ function applyDescendantOverrides(
/** Parse a JSON-like argument, handling unquoted keys. */
function parseJsonArg(str: string): Record<string, unknown> {
let normalized = str.trim()
const trimmed = str.trim()
// Try strict JSON first (most common case — avoids mangling values like "Don't")
try {
return sanitizeObject(JSON.parse(trimmed))
} catch { /* fall through to lenient parsing */ }
let normalized = trimmed
// Convert JavaScript-style object to JSON: unquoted keys → quoted
normalized = normalized.replace(
/(?<=\{|,)\s*(\w+)\s*:/g,
' "$1":',
)
// Handle single-quoted strings → double-quoted
normalized = normalized.replace(/'/g, '"')
// Replace single-quoted string delimiters with double quotes (not quotes inside strings)
normalized = replaceSingleQuoteDelimiters(normalized)
try {
return sanitizeObject(JSON.parse(normalized))
} catch {
throw new Error(`Failed to parse JSON: ${str}`)
throw new Error(`Failed to parse JSON: ${str.slice(0, 200)}`)
}
}
/** Replace single-quote string delimiters with double quotes, leaving apostrophes inside strings. */
function replaceSingleQuoteDelimiters(str: string): string {
const chars: string[] = []
let inDouble = false
let inSingle = false
for (let i = 0; i < str.length; i++) {
const ch = str[i]
if (ch === '\\' && (inDouble || inSingle)) {
chars.push(ch, str[++i] ?? '')
continue
}
if (inDouble) {
if (ch === '"') inDouble = false
chars.push(ch)
} else if (inSingle) {
if (ch === "'") {
inSingle = false
chars.push('"') // closing single quote → double quote
} else {
chars.push(ch)
}
} else {
if (ch === '"') {
inDouble = true
chars.push(ch)
} else if (ch === "'") {
inSingle = true
chars.push('"') // opening single quote → double quote
} else {
chars.push(ch)
}
}
}
return chars.join('')
}
/** Find the index of the first comma not inside braces/brackets/quotes. */
function findTopLevelComma(str: string): number {
let depth = 0

View file

@ -23,8 +23,8 @@ export const LAYERED_DESIGN_TOOLS = [
height: { type: 'number' },
layout: { type: 'string', enum: ['vertical', 'horizontal'] },
gap: { type: 'number' },
fill: { type: 'array' },
padding: {},
fill: { type: 'array', items: { type: 'object' } },
padding: { type: 'object' },
},
required: ['width', 'height'],
},
@ -39,9 +39,9 @@ export const LAYERED_DESIGN_TOOLS = [
name: { type: 'string' },
height: { type: 'number' },
layout: { type: 'string', enum: ['vertical', 'horizontal'] },
padding: {},
padding: { type: 'object' },
gap: { type: 'number' },
fill: { type: 'array' },
fill: { type: 'array', items: { type: 'object' } },
role: { type: 'string' },
justifyContent: { type: 'string' },
alignItems: { type: 'string' },

View file

@ -82,20 +82,27 @@
"vitest": "^3.0.5",
},
},
"apps/cli": {
"name": "@zseven-w/openpencil",
"version": "0.5.1",
"bin": {
"op": "dist/openpencil-cli.cjs",
},
},
"apps/desktop": {
"name": "@zseven-w/desktop",
"version": "0.5.0",
"version": "0.5.1",
},
"apps/web": {
"name": "@zseven-w/web",
"version": "0.5.0",
"version": "0.5.1",
},
"packages/pen-codegen": {
"name": "@zseven-w/pen-codegen",
"version": "0.5.0",
"version": "0.5.1",
"dependencies": {
"@zseven-w/pen-core": "workspace:*",
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-core": "0.5.1-beta.1",
"@zseven-w/pen-types": "0.5.1-beta.1",
},
"devDependencies": {
"typescript": "^5.7.2",
@ -103,9 +110,9 @@
},
"packages/pen-core": {
"name": "@zseven-w/pen-core",
"version": "0.5.0",
"version": "0.5.1",
"dependencies": {
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-types": "0.5.1-beta.1",
"nanoid": "^5.1.6",
"paper": "^0.12.18",
},
@ -115,9 +122,9 @@
},
"packages/pen-figma": {
"name": "@zseven-w/pen-figma",
"version": "0.5.0",
"version": "0.5.1",
"dependencies": {
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-types": "0.5.1-beta.1",
"fzstd": "^0.1.1",
"kiwi-schema": "^0.5.0",
"uzip": "^0.20201231.0",
@ -129,10 +136,10 @@
},
"packages/pen-renderer": {
"name": "@zseven-w/pen-renderer",
"version": "0.5.0",
"version": "0.5.1",
"dependencies": {
"@zseven-w/pen-core": "workspace:*",
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-core": "0.5.1-beta.1",
"@zseven-w/pen-types": "0.5.1-beta.1",
"rbush": "^4.0.1",
},
"devDependencies": {
@ -146,13 +153,13 @@
},
"packages/pen-sdk": {
"name": "@zseven-w/pen-sdk",
"version": "0.5.0",
"version": "0.5.1",
"dependencies": {
"@zseven-w/pen-codegen": "workspace:*",
"@zseven-w/pen-core": "workspace:*",
"@zseven-w/pen-figma": "workspace:*",
"@zseven-w/pen-renderer": "workspace:*",
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-codegen": "0.5.1-beta.1",
"@zseven-w/pen-core": "0.5.1-beta.1",
"@zseven-w/pen-figma": "0.5.1-beta.1",
"@zseven-w/pen-renderer": "0.5.1-beta.1",
"@zseven-w/pen-types": "0.5.1-beta.1",
},
"devDependencies": {
"typescript": "^5.7.2",
@ -160,7 +167,7 @@
},
"packages/pen-types": {
"name": "@zseven-w/pen-types",
"version": "0.5.0",
"version": "0.5.1",
"devDependencies": {
"typescript": "^5.7.2",
},
@ -769,6 +776,8 @@
"@zseven-w/desktop": ["@zseven-w/desktop@workspace:apps/desktop"],
"@zseven-w/openpencil": ["@zseven-w/openpencil@workspace:apps/cli"],
"@zseven-w/pen-codegen": ["@zseven-w/pen-codegen@workspace:packages/pen-codegen"],
"@zseven-w/pen-core": ["@zseven-w/pen-core@workspace:packages/pen-core"],
@ -1919,6 +1928,28 @@
"@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.40.tgz", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="],
"@zseven-w/pen-codegen/@zseven-w/pen-core": ["@zseven-w/pen-core@0.5.1-beta.1", "", { "dependencies": { "@zseven-w/pen-types": "0.5.1-beta.1", "nanoid": "^5.1.6", "paper": "^0.12.18" } }, "sha512-TWLK8lhGLOiVD0nrHNEiR/M/Xk9ndt2/WJI9XyBfqpdiTSgl5JatqdwIhAZjvZtce8wpG3Y9GG4k1SUfpvzU5w=="],
"@zseven-w/pen-codegen/@zseven-w/pen-types": ["@zseven-w/pen-types@0.5.1-beta.1", "", {}, "sha512-ozTon9G2BH0GvHTwhgZQ92keGqJX/ZyXqP0RDuQTc9Av8MVPY8nfLHmw5HaMVD+QAFRRxuvItUINroNNIv+E0Q=="],
"@zseven-w/pen-core/@zseven-w/pen-types": ["@zseven-w/pen-types@0.5.1-beta.1", "", {}, "sha512-ozTon9G2BH0GvHTwhgZQ92keGqJX/ZyXqP0RDuQTc9Av8MVPY8nfLHmw5HaMVD+QAFRRxuvItUINroNNIv+E0Q=="],
"@zseven-w/pen-figma/@zseven-w/pen-types": ["@zseven-w/pen-types@0.5.1-beta.1", "", {}, "sha512-ozTon9G2BH0GvHTwhgZQ92keGqJX/ZyXqP0RDuQTc9Av8MVPY8nfLHmw5HaMVD+QAFRRxuvItUINroNNIv+E0Q=="],
"@zseven-w/pen-renderer/@zseven-w/pen-core": ["@zseven-w/pen-core@0.5.1-beta.1", "", { "dependencies": { "@zseven-w/pen-types": "0.5.1-beta.1", "nanoid": "^5.1.6", "paper": "^0.12.18" } }, "sha512-TWLK8lhGLOiVD0nrHNEiR/M/Xk9ndt2/WJI9XyBfqpdiTSgl5JatqdwIhAZjvZtce8wpG3Y9GG4k1SUfpvzU5w=="],
"@zseven-w/pen-renderer/@zseven-w/pen-types": ["@zseven-w/pen-types@0.5.1-beta.1", "", {}, "sha512-ozTon9G2BH0GvHTwhgZQ92keGqJX/ZyXqP0RDuQTc9Av8MVPY8nfLHmw5HaMVD+QAFRRxuvItUINroNNIv+E0Q=="],
"@zseven-w/pen-sdk/@zseven-w/pen-codegen": ["@zseven-w/pen-codegen@0.5.1-beta.1", "", { "dependencies": { "@zseven-w/pen-core": "0.5.1-beta.1", "@zseven-w/pen-types": "0.5.1-beta.1" } }, "sha512-a+qVPoiq1uigVL4iCL8RTjax3GJwiGDIEBT1lrL4VZfckZ20rj6SyJFVjK9raM1Gf/XlFPu+VsWpTPBFTxuZiA=="],
"@zseven-w/pen-sdk/@zseven-w/pen-core": ["@zseven-w/pen-core@0.5.1-beta.1", "", { "dependencies": { "@zseven-w/pen-types": "0.5.1-beta.1", "nanoid": "^5.1.6", "paper": "^0.12.18" } }, "sha512-TWLK8lhGLOiVD0nrHNEiR/M/Xk9ndt2/WJI9XyBfqpdiTSgl5JatqdwIhAZjvZtce8wpG3Y9GG4k1SUfpvzU5w=="],
"@zseven-w/pen-sdk/@zseven-w/pen-figma": ["@zseven-w/pen-figma@0.5.1-beta.1", "", { "dependencies": { "@zseven-w/pen-types": "0.5.1-beta.1", "fzstd": "^0.1.1", "kiwi-schema": "^0.5.0", "uzip": "^0.20201231.0" } }, "sha512-bAq2qxJ+XPwYLsf2oFouE4DsClyGNvqLhOuQ2hXCOGK06nMRKExa+iGWAXEwIaZ+CYxQCe3zgWXlg/DgaqvEKg=="],
"@zseven-w/pen-sdk/@zseven-w/pen-renderer": ["@zseven-w/pen-renderer@0.5.1-beta.1", "", { "dependencies": { "@zseven-w/pen-core": "0.5.1-beta.1", "@zseven-w/pen-types": "0.5.1-beta.1", "rbush": "^4.0.1" }, "peerDependencies": { "canvaskit-wasm": "^0.40.0" } }, "sha512-0iuqNiP2ZFdS8s1/teU62869Mt49Pre+FSWTgfs42E3wJ4zx6DIAzSzidw9drPTNy5Xjo3j9qIV/j6Tv4Qmapw=="],
"@zseven-w/pen-sdk/@zseven-w/pen-types": ["@zseven-w/pen-types@0.5.1-beta.1", "", {}, "sha512-ozTon9G2BH0GvHTwhgZQ92keGqJX/ZyXqP0RDuQTc9Av8MVPY8nfLHmw5HaMVD+QAFRRxuvItUINroNNIv+E0Q=="],
"ajv-keywords/ajv": ["ajv@6.12.6", "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"anymatch/picomatch": ["picomatch@2.3.1", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],

View file

@ -1,6 +1,6 @@
{
"name": "openpencil",
"version": "0.5.0",
"version": "0.5.1",
"description": "The world's first open-source AI-native vector design tool and the first to feature concurrent Agent Teams. Design-as-Code. Turn prompts into UI directly on the live canvas. A modern alternative to Pencil.",
"author": {
"name": "ZSeven-W",
@ -25,11 +25,14 @@
"mcp:dev": "bun run apps/web/src/mcp/server.ts",
"electron:dev": "bun run apps/desktop/dev.ts",
"electron:compile": "esbuild apps/desktop/main.ts apps/desktop/preload.ts --bundle --platform=node --target=node20 --outdir=out/desktop --external:electron --format=cjs --out-extension:.js=.cjs --sourcemap",
"electron:build": "BUILD_TARGET=electron bun --bun run build && bun run electron:compile && bun run mcp:compile && npx electron-builder --config apps/desktop/electron-builder.yml",
"electron:build:mac-arm64": "BUILD_TARGET=electron bun --bun run build && bun run electron:compile && bun run mcp:compile && npx electron-builder --config apps/desktop/electron-builder.yml --mac --arm64 && if [ -f out/release/latest-mac.yml ]; then mv out/release/latest-mac.yml out/release/latest-mac-arm64.yml; fi",
"electron:build:mac-x64": "BUILD_TARGET=electron bun --bun run build && bun run electron:compile && bun run mcp:compile && npx electron-builder --config apps/desktop/electron-builder.yml --mac --x64",
"electron:build:mac-universal": "BUILD_TARGET=electron bun --bun run build && bun run electron:compile && bun run mcp:compile && npx electron-builder --config apps/desktop/electron-builder.yml --mac --arm64 --x64",
"electron:build": "BUILD_TARGET=electron bun --bun run build && bun run electron:compile && bun run mcp:compile && bun run cli:compile && npx electron-builder --config apps/desktop/electron-builder.yml",
"electron:build:mac-arm64": "BUILD_TARGET=electron bun --bun run build && bun run electron:compile && bun run mcp:compile && bun run cli:compile && npx electron-builder --config apps/desktop/electron-builder.yml --mac --arm64 && if [ -f out/release/latest-mac.yml ]; then mv out/release/latest-mac.yml out/release/latest-mac-arm64.yml; fi",
"electron:build:mac-x64": "BUILD_TARGET=electron bun --bun run build && bun run electron:compile && bun run mcp:compile && bun run cli:compile && npx electron-builder --config apps/desktop/electron-builder.yml --mac --x64",
"electron:build:mac-universal": "BUILD_TARGET=electron bun --bun run build && bun run electron:compile && bun run mcp:compile && bun run cli:compile && npx electron-builder --config apps/desktop/electron-builder.yml --mac --arm64 --x64",
"electron:build:mac-both": "bun run electron:build:mac-arm64 && bun run electron:build:mac-x64",
"cli:compile": "cd apps/web && esbuild ../cli/src/index.ts --bundle --platform=node --target=node20 --outfile=../cli/dist/openpencil-cli.cjs --format=cjs --sourcemap --alias:@=src --define:import.meta.env={} --external:canvas --external:paper",
"cli:dev": "bun run apps/cli/src/index.ts",
"publish:beta": "bash scripts/publish-beta.sh",
"bump": "sh -c 'V=$0; [ -z \"$V\" ] && echo \"Usage: bun run bump <version>\" && exit 1; for f in package.json apps/*/package.json packages/*/package.json; do [ -f \"$f\" ] && jq --arg v \"$V\" \".version=\\$v\" \"$f\" > \"$f.tmp\" && mv \"$f.tmp\" \"$f\" && echo \"$f → $V\"; done'"
},
"dependencies": {

View file

@ -0,0 +1,55 @@
# @zseven-w/pen-codegen
Multi-platform code generators for [OpenPencil](https://github.com/nicepkg/openpencil) designs. Turn your design files into production-ready code for 8 frameworks.
## Install
```bash
npm install @zseven-w/pen-codegen
```
## Supported Platforms
| Platform | Generator | Output |
|---|---|---|
| React + Tailwind | `generateReactCode` | `.tsx` with Tailwind classes |
| HTML + CSS | `generateHTMLCode` | Vanilla HTML/CSS |
| Vue 3 | `generateVueCode` | `.vue` SFC |
| Svelte | `generateSvelteCode` | `.svelte` component |
| Flutter | `generateFlutterCode` | Dart widget |
| SwiftUI | `generateSwiftUICode` | Swift view |
| Jetpack Compose | `generateComposeCode` | Kotlin composable |
| React Native | `generateReactNativeCode` | `.tsx` with StyleSheet |
## Usage
Generate code from a single node:
```ts
import { generateReactCode } from '@zseven-w/pen-codegen'
const code = generateReactCode(node, { indent: 2 })
```
Generate from an entire document (resolves variables, computes layout):
```ts
import { generateReactFromDocument } from '@zseven-w/pen-codegen'
const code = generateReactFromDocument(document)
```
### CSS Variables
Extract design variables as CSS custom properties:
```ts
import { generateCSSVariables } from '@zseven-w/pen-codegen'
const css = generateCSSVariables(variables, themes)
// :root { --color-primary: #3b82f6; ... }
```
## License
MIT

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-codegen",
"version": "0.5.0",
"version": "0.5.1",
"description": "Multi-platform code generators for OpenPencil designs",
"type": "module",
"exports": {
@ -16,8 +16,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-core": "workspace:*"
"@zseven-w/pen-types": "0.5.1-beta.1",
"@zseven-w/pen-core": "0.5.1-beta.1"
},
"devDependencies": {
"typescript": "^5.7.2"

View file

@ -0,0 +1,80 @@
# @zseven-w/pen-core
Core document operations for [OpenPencil](https://github.com/nicepkg/openpencil) — tree manipulation, layout engine, design variables, boolean path operations, and more.
## Install
```bash
npm install @zseven-w/pen-core
```
## Features
### Document Tree Operations
Create, query, and mutate the document tree:
```ts
import {
createEmptyDocument,
findNodeInTree,
insertNodeInTree,
removeNodeFromTree,
updateNodeInTree,
deepCloneNode,
flattenNodes,
} from '@zseven-w/pen-core'
const doc = createEmptyDocument()
const node = findNodeInTree(doc.children, 'node-id')
```
### Multi-Page Support
```ts
import { getActivePage, getActivePageChildren, migrateToPages } from '@zseven-w/pen-core'
```
### Layout Engine
Automatic layout computation with auto-sizing, padding, and gap support:
```ts
import { inferLayout, computeLayoutPositions, fitContentWidth, fitContentHeight } from '@zseven-w/pen-core'
```
### Design Variables
Resolve `$variable` references against theme axes:
```ts
import { resolveVariableRef, resolveNodeForCanvas, replaceVariableRefsInTree } from '@zseven-w/pen-core'
```
### Boolean Path Operations
Union, subtract, intersect, and exclude paths via Paper.js:
```ts
import { executeBooleanOp, BooleanOpType } from '@zseven-w/pen-core'
```
### Text Measurement
Estimate text dimensions for layout without a browser:
```ts
import { estimateTextWidth, estimateTextHeight } from '@zseven-w/pen-core'
```
### Document Normalization
Sanitize and fix documents imported from external sources:
```ts
import { normalizePenDocument } from '@zseven-w/pen-core'
```
## License
MIT

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-core",
"version": "0.5.0",
"version": "0.5.1",
"description": "Core document operations, tree utils, variables, layout engine for OpenPencil",
"type": "module",
"exports": {
@ -16,7 +16,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-types": "0.5.1-beta.1",
"nanoid": "^5.1.6",
"paper": "^0.12.18"
},

View file

@ -0,0 +1,53 @@
# @zseven-w/pen-figma
Figma `.fig` file parser and converter for [OpenPencil](https://github.com/nicepkg/openpencil). Import Figma designs directly into the OpenPencil document model.
## Install
```bash
npm install @zseven-w/pen-figma
```
## Features
- Parse binary `.fig` files (Kiwi schema + zstd/zip compression)
- Convert Figma node trees to `PenDocument`
- Multi-page support — import all pages or a single page
- Clipboard paste — detect and convert Figma clipboard HTML
- Image blob resolution
## Usage
### Parse a `.fig` file
```ts
import { parseFigFile, figmaAllPagesToPenDocument } from '@zseven-w/pen-figma'
const figFile = parseFigFile(buffer)
const document = figmaAllPagesToPenDocument(figFile)
```
### Single page import
```ts
import { parseFigFile, getFigmaPages, figmaToPenDocument } from '@zseven-w/pen-figma'
const figFile = parseFigFile(buffer)
const pages = getFigmaPages(figFile)
const document = figmaToPenDocument(figFile, pages[0])
```
### Clipboard paste
```ts
import { isFigmaClipboardHtml, extractFigmaClipboardData, figmaClipboardToNodes } from '@zseven-w/pen-figma'
if (isFigmaClipboardHtml(html)) {
const data = extractFigmaClipboardData(html)
const nodes = figmaClipboardToNodes(data)
}
```
## License
MIT

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-figma",
"version": "0.5.0",
"version": "0.5.1",
"description": "Figma .fig file parser and converter for OpenPencil",
"type": "module",
"exports": {
@ -16,7 +16,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-types": "0.5.1-beta.1",
"kiwi-schema": "^0.5.0",
"uzip": "^0.20201231.0",
"fzstd": "^0.1.1"

View file

@ -0,0 +1,64 @@
# @zseven-w/pen-renderer
Standalone CanvasKit/Skia renderer for [OpenPencil](https://github.com/nicepkg/openpencil) design files. Render `.op` documents to a GPU-accelerated canvas — works in browsers, Node.js, and headless environments.
## Install
```bash
npm install @zseven-w/pen-renderer canvaskit-wasm
```
`canvaskit-wasm` is a peer dependency — you provide the WASM binary.
## Usage
```ts
import { loadCanvasKit, PenRenderer } from '@zseven-w/pen-renderer'
// Initialize CanvasKit
await loadCanvasKit()
// Create renderer on a canvas element
const renderer = new PenRenderer(canvas, document, {
width: 1920,
height: 1080,
})
// Render
renderer.render()
```
## API
### High-level
- **`loadCanvasKit()`** — Initialize the CanvasKit WASM module
- **`PenRenderer`** — Full-featured renderer with viewport, selection, and interaction support
### Document Flattening
Pre-process documents for rendering:
```ts
import { flattenToRenderNodes, resolveRefs, premeasureTextHeights } from '@zseven-w/pen-renderer'
```
### Viewport Utilities
```ts
import { viewportMatrix, screenToScene, sceneToScreen, zoomToPoint } from '@zseven-w/pen-renderer'
```
### Low-level Renderers
For custom rendering pipelines:
- `SkiaNodeRenderer` — Renders individual nodes to a Skia canvas
- `SkiaTextRenderer` — Text layout and rendering
- `SkiaFontManager` — Font loading and management
- `SkiaImageLoader` — Async image loading with caching
- `SpatialIndex` — R-tree spatial index for hit testing
## License
MIT

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-renderer",
"version": "0.5.0",
"version": "0.5.1",
"description": "Standalone CanvasKit/Skia renderer for OpenPencil (.op) design files",
"type": "module",
"exports": {
@ -16,8 +16,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-core": "workspace:*",
"@zseven-w/pen-types": "0.5.1-beta.1",
"@zseven-w/pen-core": "0.5.1-beta.1",
"rbush": "^4.0.1"
},
"peerDependencies": {

View file

@ -27,6 +27,13 @@ export class SkiaTextRenderer {
// 64 MB — each entry is estimated as content.length*64+4096 bytes (WASM heap approximation)
private static PARA_CACHE_BYTE_LIMIT = 64 * 1024 * 1024
// Pre-rasterized paragraph image cache (SkImage, same key as paraCache, zoom-independent)
// Allows drawImageRect instead of drawParagraph on every frame — avoids per-frame glyph rasterization.
private paraImageCache = new Map<string, SkImage | null>()
private paraImageCacheBytes = 0
// 128 MB — each entry is sw*sh*4 bytes (RGBA pixels at up to 2x DPR scale)
private static PARA_IMAGE_CACHE_BYTE_LIMIT = 128 * 1024 * 1024
private static estimateParaBytes(content: string): number {
return content.length * 64 + 4096
}
@ -37,6 +44,10 @@ export class SkiaTextRenderer {
// Device pixel ratio override
devicePixelRatio: number | undefined
private get _dpr(): number {
return this.devicePixelRatio ?? (typeof window !== 'undefined' ? window.devicePixelRatio : 1) ?? 1
}
// Font manager for vector text rendering
fontManager: SkiaFontManager
@ -59,6 +70,11 @@ export class SkiaTextRenderer {
}
this.paraCache.clear()
this.paraCacheBytes = 0
for (const img of this.paraImageCache.values()) {
img?.delete()
}
this.paraImageCache.clear()
this.paraImageCacheBytes = 0
}
// Evict oldest entries (Map head = first inserted) until there is room for `incoming` bytes.
@ -71,6 +87,17 @@ export class SkiaTextRenderer {
}
}
private evictParaImageCache(incoming: number) {
while (this.paraImageCacheBytes + incoming > SkiaTextRenderer.PARA_IMAGE_CACHE_BYTE_LIMIT && this.paraImageCache.size > 0) {
const [key, img] = this.paraImageCache.entries().next().value!
if (img) {
this.paraImageCacheBytes -= img.width() * img.height() * 4
img.delete()
}
this.paraImageCache.delete(key)
}
}
private evictTextCache(incoming: number) {
while (this.textCacheBytes + incoming > SkiaTextRenderer.TEXT_CACHE_BYTE_LIMIT && this.textCache.size > 0) {
const [key, img] = this.textCache.entries().next().value!
@ -246,13 +273,63 @@ export class SkiaTextRenderer {
if (!para) return false
// Compute drawX and surface dimensions
let drawX = x
if (!isFixedWidth && w > 0 && textAlign !== 'left') {
let surfaceW: number
if (!isFixedWidth) {
const longestLine = para.getLongestLine()
if (textAlign === 'center') drawX = x + Math.max(0, (w - longestLine) / 2)
else if (textAlign === 'right') drawX = x + Math.max(0, w - longestLine)
surfaceW = longestLine + 2
if (w > 0 && textAlign !== 'left') {
if (textAlign === 'center') drawX = x + Math.max(0, (w - longestLine) / 2)
else if (textAlign === 'right') drawX = x + Math.max(0, w - longestLine)
}
} else {
surfaceW = layoutWidth
}
const surfaceH = para.getHeight() + 2
// Try paragraph image cache: drawImageRect is far cheaper than drawParagraph per frame
const imgScale = Math.min(this._dpr, 2)
let cachedImg = this.paraImageCache.get(cacheKey)
if (cachedImg === undefined) {
cachedImg = null
const sw = Math.min(Math.ceil(surfaceW * imgScale), 4096)
const sh = Math.min(Math.ceil(surfaceH * imgScale), 4096)
if (sw > 0 && sh > 0) {
const surf: any = (ck as any).MakeSurface?.(sw, sh)
if (surf) {
const offCanvas = surf.getCanvas()
offCanvas.scale(imgScale, imgScale)
offCanvas.drawParagraph(para, 0, 0)
cachedImg = (surf.makeImageSnapshot() as SkImage | null) ?? null
surf.delete()
if (cachedImg) {
const imgBytes = sw * sh * 4
this.evictParaImageCache(imgBytes)
this.paraImageCacheBytes += imgBytes
}
}
}
this.paraImageCache.set(cacheKey, cachedImg)
}
if (cachedImg) {
const imgW = cachedImg.width() / imgScale
const imgH = cachedImg.height() / imgScale
const paint = new ck.Paint()
paint.setAntiAlias(true)
if (opacity < 1) paint.setAlphaf(opacity)
canvas.drawImageRect(
cachedImg,
ck.LTRBRect(0, 0, cachedImg.width(), cachedImg.height()),
ck.LTRBRect(drawX, y, drawX + imgW, y + imgH),
paint,
)
paint.delete()
return true
}
// Fallback: surface creation failed, draw directly
if (opacity < 1) {
const paint = new ck.Paint()
paint.setAlphaf(opacity)
@ -371,8 +448,7 @@ export class SkiaTextRenderer {
: (wrappedLines.length - 1) * lineHeight + glyphH + 2,
)
const dpr = this.devicePixelRatio ?? ((typeof window !== 'undefined' ? window.devicePixelRatio : 1) || 1)
const rawScale = this.zoom * dpr
const rawScale = this.zoom * this._dpr
const scale = rawScale <= 2 ? 2 : rawScale <= 4 ? 4 : 8
const cacheKey = `${content}|${fontSize}|${fillColor}|${fontWeight}|${fontFamily}|${textAlign}|${Math.round(renderW)}|${Math.round(textH)}|${scale}`

View file

@ -0,0 +1,56 @@
# @zseven-w/pen-sdk
The umbrella SDK for [OpenPencil](https://github.com/nicepkg/openpencil). One import gives you everything — types, document operations, code generation, Figma import, and rendering.
## Install
```bash
npm install @zseven-w/pen-sdk
```
## What's Included
This package re-exports all OpenPencil packages:
| Package | Provides |
|---|---|
| `@zseven-w/pen-types` | TypeScript types for the document model |
| `@zseven-w/pen-core` | Tree operations, layout engine, variables, boolean ops |
| `@zseven-w/pen-codegen` | Code generators (React, HTML, Vue, Svelte, Flutter, SwiftUI, Compose, RN) |
| `@zseven-w/pen-figma` | Figma `.fig` parser and converter |
| `@zseven-w/pen-renderer` | CanvasKit/Skia GPU renderer |
## Usage
```ts
import {
// Types
type PenDocument,
type PenNode,
// Document operations
createEmptyDocument,
findNodeInTree,
insertNodeInTree,
normalizePenDocument,
// Code generation
generateReactFromDocument,
generateHTMLFromDocument,
generateFlutterFromDocument,
// Figma import
parseFigFile,
figmaAllPagesToPenDocument,
// Rendering
loadCanvasKit,
PenRenderer,
} from '@zseven-w/pen-sdk'
```
Or install individual packages for smaller bundles — see each package's README for details.
## License
MIT

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-sdk",
"version": "0.5.0",
"version": "0.5.1",
"description": "OpenPencil SDK — parse, manipulate, and generate code from .op design files",
"type": "module",
"exports": {
@ -16,11 +16,11 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@zseven-w/pen-types": "workspace:*",
"@zseven-w/pen-core": "workspace:*",
"@zseven-w/pen-codegen": "workspace:*",
"@zseven-w/pen-figma": "workspace:*",
"@zseven-w/pen-renderer": "workspace:*"
"@zseven-w/pen-types": "0.5.1-beta.1",
"@zseven-w/pen-core": "0.5.1-beta.1",
"@zseven-w/pen-codegen": "0.5.1-beta.1",
"@zseven-w/pen-figma": "0.5.1-beta.1",
"@zseven-w/pen-renderer": "0.5.1-beta.1"
},
"devDependencies": {
"typescript": "^5.7.2"

View file

@ -0,0 +1,31 @@
# @zseven-w/pen-types
Type definitions for the [OpenPencil](https://github.com/nicepkg/openpencil) document model.
## Install
```bash
npm install @zseven-w/pen-types
```
## What's Included
This package provides all TypeScript types and interfaces for the OpenPencil design file format (`.op`):
- **Document model**`PenDocument`, `PenPage`, `PenNode` and all node types (`FrameNode`, `RectangleNode`, `EllipseNode`, `TextNode`, `ImageNode`, `PathNode`, etc.)
- **Styles**`PenFill` (solid, gradient, image), `PenStroke`, `PenEffect` (blur, shadow), `BlendMode`, `StyledTextSegment`
- **Variables & Themes**`VariableDefinition`, `VariableValue`, `ThemedValue`
- **Canvas state**`ToolType`, `ViewportState`, `SelectionState`, `CanvasInteraction`
- **UIKit**`UIKit`, `KitComponent`, `ComponentCategory`
- **Theme presets**`ThemePreset`, `ThemePresetFile`
- **Design spec**`DesignMdSpec`, `DesignMdColor`, `DesignMdTypography`
## Usage
```ts
import type { PenDocument, PenNode, FrameNode } from '@zseven-w/pen-types'
```
## License
MIT

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-types",
"version": "0.5.0",
"version": "0.5.1",
"description": "Type definitions for OpenPencil document model",
"type": "module",
"exports": {

117
scripts/publish-beta.sh Executable file
View file

@ -0,0 +1,117 @@
#!/bin/bash
# Publish all @zseven-w packages to npm with auto-incrementing beta version.
#
# Usage:
# bun run publish:beta # auto-increment beta number
# bun run publish:beta 5 # force beta.5
#
# Publishes: pen-types → pen-core → pen-codegen, pen-figma → pen-renderer → pen-sdk → openpencil CLI
# All under the "beta" dist-tag, so `npm install` won't pick them up by default.
# Install with: npm install @zseven-w/openpencil@beta
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BASE_VERSION=$(jq -r .version "$ROOT/package.json")
FORCE_NUM="${1:-}"
# Packages in topological order
PACKAGES=(
packages/pen-types
packages/pen-core
packages/pen-codegen
packages/pen-figma
packages/pen-renderer
packages/pen-sdk
apps/cli
)
# --- Determine beta number ---
if [ -n "$FORCE_NUM" ]; then
BETA_NUM="$FORCE_NUM"
else
# Query npm for the latest beta of this base version.
# npm view returns a string (1 version) or array (multiple), or errors (404) if not found.
RAW=$(npm view "@zseven-w/pen-types" versions --json 2>/dev/null || true)
LATEST=$(echo "$RAW" | jq -r --arg base "$BASE_VERSION" '
if type == "object" and .error then empty # npm 404 error object
elif type == "array" then
map(select(type == "string" and startswith($base + "-beta."))) | last // empty
elif type == "string" and startswith($base + "-beta.") then .
else empty
end
' 2>/dev/null || true)
if [ -n "$LATEST" ]; then
PREV_NUM=$(echo "$LATEST" | sed "s/${BASE_VERSION}-beta\.//")
BETA_NUM=$((PREV_NUM + 1))
else
BETA_NUM=0
fi
fi
BETA_VERSION="${BASE_VERSION}-beta.${BETA_NUM}"
echo "Publishing version: $BETA_VERSION"
echo ""
# --- Set beta version in all package.json files ---
MODIFIED_FILES=()
for pkg in "${PACKAGES[@]}"; do
f="$ROOT/$pkg/package.json"
if [ -f "$f" ]; then
# Backup original
cp "$f" "$f.bak"
MODIFIED_FILES+=("$f")
# Set version and replace workspace:* refs
jq --arg v "$BETA_VERSION" '
.version = $v |
if .dependencies then
.dependencies |= with_entries(
if .value == "workspace:*" then .value = $v else . end
)
else . end |
if .devDependencies then
.devDependencies |= with_entries(
if .value == "workspace:*" then .value = $v else . end
)
else . end
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
fi
done
# --- Restore on exit ---
cleanup() {
echo ""
echo "Restoring original package.json files..."
for f in "${MODIFIED_FILES[@]}"; do
if [ -f "$f.bak" ]; then
mv "$f.bak" "$f"
fi
done
echo "Done."
}
trap cleanup EXIT
# --- Compile CLI ---
echo "Compiling CLI..."
(cd "$ROOT" && bun run cli:compile)
echo ""
# --- Verify CLI ---
node "$ROOT/apps/cli/dist/openpencil-cli.cjs" --version
echo ""
# --- Publish ---
for pkg in "${PACKAGES[@]}"; do
dir="$ROOT/$pkg"
name=$(jq -r .name "$dir/package.json")
echo "Publishing $name@$BETA_VERSION ..."
(cd "$dir" && npm publish --access public --tag beta) || echo " ⚠ Failed (may already exist)"
echo ""
done
echo "================================"
echo "Published: $BETA_VERSION"
echo "Install: npm install @zseven-w/openpencil@beta"
echo "================================"