diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15f60127..88501f5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cf554795..4eb1ac9c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 }} diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml new file mode 100644 index 00000000..b4a9f924 --- /dev/null +++ b/.github/workflows/publish-cli.yml @@ -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 }} diff --git a/CLAUDE.md b/CLAUDE.md index 5f69f44e..202391e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ` — 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 ` +- **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: ` **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. diff --git a/README.de.md b/README.de.md index 6a3120a0..422f0e8b 100644 --- a/README.de.md +++ b/README.de.md @@ -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. + + + + + +### ⌨️ 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. + + + + +### 🎯 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. + @@ -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 ü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 diff --git a/README.es.md b/README.es.md index 5dfa99c5..cb6ab346 100644 --- a/README.es.md +++ b/README.es.md @@ -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. + + + + + +### ⌨️ 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. + + + + +### 🎯 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. + @@ -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 # 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 diff --git a/README.fr.md b/README.fr.md index 250a18c7..a2ca7347 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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. + + + + + +### ⌨️ 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. + + + + +### 🎯 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. + @@ -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 # 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 diff --git a/README.hi.md b/README.hi.md index dda30046..5bd70157 100644 --- a/README.hi.md +++ b/README.hi.md @@ -80,6 +80,22 @@ Claude Code, Codex, Gemini, OpenCode, Kiro, या Copilot CLIs में वन वेब ऐप + Electron के ज़रिए macOS, Windows और Linux पर नेटिव डेस्कटॉप। GitHub Releases से ऑटो-अपडेट। `.op` फ़ाइल एसोसिएशन — डबल-क्लिक से खोलें। + + + + + +### ⌨️ CLI — `op` + +अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें। `op design`, `op insert`, `op export` — बैच डिज़ाइन DSL, नोड मैनिपुलेशन, कोड एक्सपोर्ट। फ़ाइलों या stdin से पाइप करें। डेस्कटॉप ऐप या वेब सर्वर के साथ काम करता है। + + + + +### 🎯 मल्टी-प्लेटफ़ॉर्म कोड एक्सपोर्ट + +एक `.op` फ़ाइल से React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native में एक्सपोर्ट करें। डिज़ाइन वेरिएबल CSS कस्टम प्रॉपर्टीज़ बन जाते हैं। + @@ -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 # सभी 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`) टर्मिनल नियंत्रण - [ ] सहयोगी संपादन - [ ] प्लगइन सिस्टम diff --git a/README.id.md b/README.id.md index 92581c7a..59b39ffc 100644 --- a/README.id.md +++ b/README.id.md @@ -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. + + + + + +### ⌨️ 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. + + + + +### 🎯 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. + @@ -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 # 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 diff --git a/README.ja.md b/README.ja.md index 08d4f8d6..766e5fd5 100644 --- a/README.ja.md +++ b/README.ja.md @@ -80,6 +80,22 @@ Claude Code、Codex、Gemini、OpenCode、Kiro、Copilot CLI にワンクリッ Web アプリ + Electron による macOS・Windows・Linux ネイティブデスクトップ。GitHub Releases からの自動アップデート。`.op` ファイル関連付け — ダブルクリックで開く。 + + + + + +### ⌨️ CLI — `op` + +ターミナルからデザインツールを操作。`op design`、`op insert`、`op export` — バッチデザインDSL、ノード操作、コードエクスポート。ファイルやstdinからのパイプ入力に対応。デスクトップアプリまたはWebサーバーと連携。 + + + + +### 🎯 マルチプラットフォームコードエクスポート + +1つの`.op`ファイルからReact + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Nativeへエクスポート。デザイン変数はCSSカスタムプロパティに変換。 + @@ -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 # すべての 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`)ターミナル制御 - [ ] 共同編集 - [ ] プラグインシステム diff --git a/README.ko.md b/README.ko.md index bdadb097..913679d4 100644 --- a/README.ko.md +++ b/README.ko.md @@ -80,6 +80,22 @@ Claude Code, Codex, Gemini, OpenCode, Kiro 또는 Copilot CLI에 원클릭 설 웹 앱 + Electron을 통한 macOS, Windows, Linux 네이티브 데스크톱. GitHub Releases에서 자동 업데이트. `.op` 파일 연결 — 더블 클릭으로 열기. + + + + + +### ⌨️ CLI — `op` + +터미널에서 디자인 도구 제어. `op design`, `op insert`, `op export` — 배치 디자인 DSL, 노드 조작, 코드 내보내기. 파일이나 stdin에서 파이프 입력 지원. 데스크톱 앱 또는 웹 서버와 연동. + + + + +### 🎯 멀티 플랫폼 코드 내보내기 + +하나의 `.op` 파일에서 React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native로 내보내기. 디자인 변수는 CSS 커스텀 프로퍼티로 변환. + @@ -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 # 모든 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`) 터미널 제어 - [ ] 공동 편집 - [ ] 플러그인 시스템 diff --git a/README.md b/README.md index 316f092d..b16212f1 100644 --- a/README.md +++ b/README.md @@ -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. + + + + + +### ⌨️ 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. + + + + +### 🎯 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. + @@ -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 # 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 diff --git a/README.pt.md b/README.pt.md index 841b40ce..28843f02 100644 --- a/README.pt.md +++ b/README.pt.md @@ -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. + + + + + +### ⌨️ 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. + + + + +### 🎯 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. + @@ -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 # 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 diff --git a/README.ru.md b/README.ru.md index fdabce78..917d36f4 100644 --- a/README.ru.md +++ b/README.ru.md @@ -80,6 +80,22 @@ Веб-приложение + нативный десктоп на macOS, Windows и Linux через Electron. Автообновление из GitHub Releases. Ассоциация файлов `.op` — двойной клик для открытия. + + + + + +### ⌨️ CLI — `op` + +Управляйте инструментом дизайна из терминала. `op design`, `op insert`, `op export` — пакетный DSL дизайна, манипуляция узлами, экспорт кода. Ввод через pipe из файлов или stdin. Работает с десктопным приложением или веб-сервером. + + + + +### 🎯 Мультиплатформенный экспорт кода + +Экспорт из одного файла `.op` в React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Переменные дизайна превращаются в пользовательские свойства CSS. + @@ -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 # Синхронизация версий во всех 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`) для управления из терминала - [ ] Совместное редактирование - [ ] Система плагинов diff --git a/README.th.md b/README.th.md index a3947287..d8536167 100644 --- a/README.th.md +++ b/README.th.md @@ -80,6 +80,22 @@ Orchestrator แบ่งหน้าที่ซับซ้อนออกเ เว็บแอป + เดสก์ท็อปแบบ native บน macOS, Windows และ Linux ผ่าน Electron อัปเดตอัตโนมัติจาก GitHub Releases เชื่อมโยงไฟล์ `.op` — ดับเบิลคลิกเพื่อเปิด + + + + + +### ⌨️ CLI — `op` + +ควบคุมเครื่องมือออกแบบจาก terminal ของคุณ `op design`, `op insert`, `op export` — batch design DSL, จัดการ node, ส่งออกโค้ด Pipe จากไฟล์หรือ stdin ทำงานร่วมกับแอปเดสก์ท็อปหรือ web server + + + + +### 🎯 ส่งออกโค้ดหลายแพลตฟอร์ม + +ส่งออกจากไฟล์ `.op` ไฟล์เดียวไปยัง React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native Design variables กลายเป็น CSS custom properties + @@ -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 # 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 - [ ] การแก้ไขร่วมกัน - [ ] ระบบปลั๊กอิน diff --git a/README.tr.md b/README.tr.md index 967e149d..04df2ab1 100644 --- a/README.tr.md +++ b/README.tr.md @@ -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. + + + + + +### ⌨️ 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. + + + + +### 🎯 Ç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. + @@ -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 # 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 diff --git a/README.vi.md b/README.vi.md index 3e9b419c..64865b01 100644 --- a/README.vi.md +++ b/README.vi.md @@ -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ở. + + + + + +### ⌨️ 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. + + + + +### 🎯 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. + @@ -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 # Đồ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 diff --git a/README.zh-TW.md b/README.zh-TW.md index bf9ba7f5..3a71308d 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -80,6 +80,22 @@ Web 應用程式 + 透過 Electron 在 macOS、Windows 和 Linux 上原生執行。從 GitHub Releases 自動更新。`.op` 檔案關聯 — 雙擊即可開啟。 + + + + + +### ⌨️ CLI — `op` + +從終端機控制設計工具。`op design`、`op insert`、`op export` — 批次設計 DSL、節點操作、程式碼匯出。支援從檔案或 stdin 管道輸入。可搭配桌面應用程式或 Web 伺服器使用。 + + + + +### 🎯 多平台程式碼匯出 + +從單個 `.op` 檔案匯出至 React + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native。設計變數自動轉換為 CSS 自訂屬性。 + @@ -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 # 在所有 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`)終端控制 - [ ] 協同編輯 - [ ] 外掛程式系統 diff --git a/README.zh.md b/README.zh.md index 2b477801..a9a6ab77 100644 --- a/README.zh.md +++ b/README.zh.md @@ -80,6 +80,22 @@ Web 应用 + 通过 Electron 支持 macOS、Windows 和 Linux 原生桌面端。从 GitHub Releases 自动更新。`.op` 文件关联 — 双击即可打开。 + + + + + +### ⌨️ CLI — `op` + +从终端控制设计工具。`op design`、`op insert`、`op export` — 批量设计 DSL、节点操作、代码导出。支持从文件或 stdin 管道输入。可搭配桌面应用或 Web 服务器使用。 + + + + +### 🎯 多平台代码导出 + +从单个 `.op` 文件导出到 React + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native。设计变量自动转换为 CSS 自定义属性。 + @@ -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 # 同步所有 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`)终端控制 - [ ] 协同编辑 - [ ] 插件系统 diff --git a/apps/cli/.gitignore b/apps/cli/.gitignore new file mode 100644 index 00000000..849ddff3 --- /dev/null +++ b/apps/cli/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/apps/cli/CLAUDE.md b/apps/cli/CLAUDE.md new file mode 100644 index 00000000..3361eadc --- /dev/null +++ b/apps/cli/CLAUDE.md @@ -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 ` (target .op file), `--page ` (target page) diff --git a/apps/cli/README.de.md b/apps/cli/README.de.md new file mode 100644 index 00000000..a9d9674a --- /dev/null +++ b/apps/cli/README.de.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### Dokumentoperationen + +```bash +op open [file.op] # Datei oeffnen oder mit aktivem Canvas verbinden +op save # Aktuelles Dokument speichern +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # Aktuelle Canvas-Auswahl abrufen +``` + +### Knotenmanipulation + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Code-Export + +```bash +op export [--out file] +# Formate: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### Variablen und Themes + +```bash +op vars # Variablen abrufen +op vars:set # Variablen setzen +op themes # Themes abrufen +op themes:set # Themes setzen +op theme:save # Theme-Preset speichern +op theme:load # 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 # Eine Seite entfernen +op page rename # Eine Seite umbenennen +op page reorder # Eine Seite neu anordnen +op page duplicate # Eine Seite duplizieren +``` + +### Import + +```bash +op import:svg # SVG-Datei importieren +op import:figma # Figma-.fig-Datei importieren +``` + +### Layout + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### Globale Optionen + +```text +--file Ziel-.op-Datei (Standard: aktives Canvas) +--page Zielseiten-ID +--pretty Menschenlesbare JSON-Ausgabe +--help Hilfe anzeigen +--version Version anzeigen +``` + +## Lizenz + +MIT diff --git a/apps/cli/README.es.md b/apps/cli/README.es.md new file mode 100644 index 00000000..789a7b85 --- /dev/null +++ b/apps/cli/README.es.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### Operaciones de documento + +```bash +op open [file.op] # Abrir archivo o conectar al lienzo activo +op save # 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 [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Exportacion de codigo + +```bash +op export [--out file] +# Formatos: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### Variables y temas + +```bash +op vars # Obtener variables +op vars:set # Establecer variables +op themes # Obtener temas +op themes:set # Establecer temas +op theme:save # Guardar preset de tema +op theme:load # 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 # Eliminar una pagina +op page rename # Renombrar una pagina +op page reorder # Reordenar una pagina +op page duplicate # Duplicar una pagina +``` + +### Importacion + +```bash +op import:svg # Importar archivo SVG +op import:figma # Importar archivo Figma .fig +``` + +### Disposicion + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### Opciones globales + +```text +--file Archivo .op de destino (por defecto: lienzo activo) +--page ID de la pagina de destino +--pretty Salida JSON legible +--help Mostrar ayuda +--version Mostrar version +``` + +## Licencia + +MIT diff --git a/apps/cli/README.fr.md b/apps/cli/README.fr.md new file mode 100644 index 00000000..4566c25f --- /dev/null +++ b/apps/cli/README.fr.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### Operations sur les documents + +```bash +op open [file.op] # Ouvrir un fichier ou se connecter au canevas actif +op save # 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 [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Export de code + +```bash +op export [--out file] +# Formats : react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### Variables et themes + +```bash +op vars # Obtenir les variables +op vars:set # Definir les variables +op themes # Obtenir les themes +op themes:set # Definir les themes +op theme:save # Enregistrer un preset de theme +op theme:load # 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 # Supprimer une page +op page rename # Renommer une page +op page reorder # Reordonner une page +op page duplicate # Dupliquer une page +``` + +### Importation + +```bash +op import:svg # Importer un fichier SVG +op import:figma # 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 Fichier .op cible (par defaut : canevas actif) +--page ID de la page cible +--pretty Sortie JSON lisible +--help Afficher l'aide +--version Afficher la version +``` + +## Licence + +MIT diff --git a/apps/cli/README.hi.md b/apps/cli/README.hi.md new file mode 100644 index 00000000..643cafb5 --- /dev/null +++ b/apps/cli/README.hi.md @@ -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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### दस्तावेज़ संचालन + +```bash +op open [file.op] # फ़ाइल खोलें या लाइव कैनवास से कनेक्ट करें +op save # वर्तमान दस्तावेज़ सहेजें +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # वर्तमान कैनवास चयन प्राप्त करें +``` + +### नोड हेरफेर + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### कोड निर्यात + +```bash +op export [--out file] +# प्रारूप: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### वेरिएबल और थीम + +```bash +op vars # वेरिएबल प्राप्त करें +op vars:set # वेरिएबल सेट करें +op themes # थीम प्राप्त करें +op themes:set # थीम सेट करें +op theme:save # थीम प्रीसेट सहेजें +op theme:load # थीम प्रीसेट लोड करें +op theme:list [dir] # थीम प्रीसेट सूचीबद्ध करें +``` + +### पेज + +```bash +op page list # पेज सूचीबद्ध करें +op page add [--name N] # एक पेज जोड़ें +op page remove # एक पेज हटाएँ +op page rename # एक पेज का नाम बदलें +op page reorder # एक पेज का क्रम बदलें +op page duplicate # एक पेज डुप्लिकेट करें +``` + +### आयात + +```bash +op import:svg # SVG फ़ाइल आयात करें +op import:figma # Figma .fig फ़ाइल आयात करें +``` + +### लेआउट + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### वैश्विक फ़्लैग + +```text +--file लक्ष्य .op फ़ाइल (डिफ़ॉल्ट: लाइव कैनवास) +--page लक्ष्य पेज ID +--pretty मानव-पठनीय JSON आउटपुट +--help सहायता दिखाएँ +--version संस्करण दिखाएँ +``` + +## लाइसेंस + +MIT diff --git a/apps/cli/README.id.md b/apps/cli/README.id.md new file mode 100644 index 00000000..a52ec0eb --- /dev/null +++ b/apps/cli/README.id.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### Operasi Dokumen + +```bash +op open [file.op] # Buka file atau hubungkan ke kanvas langsung +op save # 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 [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Ekspor Kode + +```bash +op export [--out file] +# Format: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### Variabel & Tema + +```bash +op vars # Dapatkan variabel +op vars:set # Atur variabel +op themes # Dapatkan tema +op themes:set # Atur tema +op theme:save # Simpan preset tema +op theme:load # 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 # Hapus halaman +op page rename # Ganti nama halaman +op page reorder # Urutkan ulang halaman +op page duplicate # Duplikasi halaman +``` + +### Impor + +```bash +op import:svg # Impor file SVG +op import:figma # 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 File .op target (default: kanvas langsung) +--page ID halaman target +--pretty Output JSON yang mudah dibaca +--help Tampilkan bantuan +--version Tampilkan versi +``` + +## Lisensi + +MIT diff --git a/apps/cli/README.ja.md b/apps/cli/README.ja.md new file mode 100644 index 00000000..a24cfffe --- /dev/null +++ b/apps/cli/README.ja.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### ドキュメント操作 + +```bash +op open [file.op] # ファイルを開く、またはライブキャンバスに接続 +op save # 現在のドキュメントを保存 +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # 現在のキャンバスの選択を取得 +``` + +### ノード操作 + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### コードエクスポート + +```bash +op export [--out file] +# フォーマット: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### 変数とテーマ + +```bash +op vars # 変数を取得 +op vars:set # 変数を設定 +op themes # テーマを取得 +op themes:set # テーマを設定 +op theme:save # テーマプリセットを保存 +op theme:load # テーマプリセットを読み込み +op theme:list [dir] # テーマプリセットを一覧表示 +``` + +### ページ + +```bash +op page list # ページを一覧表示 +op page add [--name N] # ページを追加 +op page remove # ページを削除 +op page rename # ページの名前を変更 +op page reorder # ページを並べ替え +op page duplicate # ページを複製 +``` + +### インポート + +```bash +op import:svg # SVG ファイルをインポート +op import:figma # Figma .fig ファイルをインポート +``` + +### レイアウト + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### グローバルフラグ + +```text +--file 対象の .op ファイル(デフォルト: ライブキャンバス) +--page 対象のページ ID +--pretty 人間が読みやすい JSON 出力 +--help ヘルプを表示 +--version バージョンを表示 +``` + +## ライセンス + +MIT diff --git a/apps/cli/README.ko.md b/apps/cli/README.ko.md new file mode 100644 index 00000000..b600c950 --- /dev/null +++ b/apps/cli/README.ko.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### 문서 작업 + +```bash +op open [file.op] # 파일 열기 또는 라이브 캔버스에 연결 +op save # 현재 문서 저장 +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # 현재 캔버스 선택 항목 가져오기 +``` + +### 노드 조작 + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### 코드 내보내기 + +```bash +op export [--out file] +# 형식: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### 변수 및 테마 + +```bash +op vars # 변수 가져오기 +op vars:set # 변수 설정 +op themes # 테마 가져오기 +op themes:set # 테마 설정 +op theme:save # 테마 프리셋 저장 +op theme:load # 테마 프리셋 불러오기 +op theme:list [dir] # 테마 프리셋 목록 보기 +``` + +### 페이지 + +```bash +op page list # 페이지 목록 보기 +op page add [--name N] # 페이지 추가 +op page remove # 페이지 제거 +op page rename # 페이지 이름 변경 +op page reorder # 페이지 순서 변경 +op page duplicate # 페이지 복제 +``` + +### 가져오기 + +```bash +op import:svg # SVG 파일 가져오기 +op import:figma # Figma .fig 파일 가져오기 +``` + +### 레이아웃 + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### 전역 플래그 + +```text +--file 대상 .op 파일 (기본값: 라이브 캔버스) +--page 대상 페이지 ID +--pretty 사람이 읽기 쉬운 JSON 출력 +--help 도움말 표시 +--version 버전 표시 +``` + +## 라이선스 + +MIT diff --git a/apps/cli/README.md b/apps/cli/README.md new file mode 100644 index 00000000..896638ef --- /dev/null +++ b/apps/cli/README.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### Document Operations + +```bash +op open [file.op] # Open file or connect to live canvas +op save # Save current document +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # Get current canvas selection +``` + +### Node Manipulation + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Code Export + +```bash +op export [--out file] +# Formats: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### Variables & Themes + +```bash +op vars # Get variables +op vars:set # Set variables +op themes # Get themes +op themes:set # Set themes +op theme:save # Save theme preset +op theme:load # 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 # Remove a page +op page rename # Rename a page +op page reorder # Reorder a page +op page duplicate # Duplicate a page +``` + +### Import + +```bash +op import:svg # Import SVG file +op import:figma # Import Figma .fig file +``` + +### Layout + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### Global Flags + +```text +--file Target .op file (default: live canvas) +--page Target page ID +--pretty Human-readable JSON output +--help Show help +--version Show version +``` + +## License + +MIT diff --git a/apps/cli/README.pt.md b/apps/cli/README.pt.md new file mode 100644 index 00000000..a7d7f3ff --- /dev/null +++ b/apps/cli/README.pt.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### Operacoes de Documento + +```bash +op open [file.op] # Abrir arquivo ou conectar ao canvas ativo +op save # 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 [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Exportacao de Codigo + +```bash +op export [--out file] +# Formatos: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### Variaveis e Temas + +```bash +op vars # Obter variaveis +op vars:set # Definir variaveis +op themes # Obter temas +op themes:set # Definir temas +op theme:save # Salvar preset de tema +op theme:load # 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 # Remover uma pagina +op page rename # Renomear uma pagina +op page reorder # Reordenar uma pagina +op page duplicate # Duplicar uma pagina +``` + +### Importacao + +```bash +op import:svg # Importar arquivo SVG +op import:figma # 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 Arquivo .op alvo (padrao: canvas ativo) +--page ID da pagina alvo +--pretty Saida JSON legivel +--help Mostrar ajuda +--version Mostrar versao +``` + +## Licenca + +MIT diff --git a/apps/cli/README.ru.md b/apps/cli/README.ru.md new file mode 100644 index 00000000..2933c626 --- /dev/null +++ b/apps/cli/README.ru.md @@ -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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### Операции с документом + +```bash +op open [file.op] # Открыть файл или подключиться к активному холсту +op save # Сохранить текущий документ +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # Получить текущее выделение на холсте +``` + +### Работа с узлами + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Экспорт кода + +```bash +op export [--out file] +# Форматы: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### Переменные и темы + +```bash +op vars # Получить переменные +op vars:set # Задать переменные +op themes # Получить темы +op themes:set # Задать темы +op theme:save # Сохранить пресет темы +op theme:load # Загрузить пресет темы +op theme:list [dir] # Список пресетов тем +``` + +### Страницы + +```bash +op page list # Список страниц +op page add [--name N] # Добавить страницу +op page remove # Удалить страницу +op page rename # Переименовать страницу +op page reorder # Изменить порядок страницы +op page duplicate # Дублировать страницу +``` + +### Импорт + +```bash +op import:svg # Импортировать SVG-файл +op import:figma # Импортировать файл Figma .fig +``` + +### Макет + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### Глобальные флаги + +```text +--file Целевой файл .op (по умолчанию: активный холст) +--page ID целевой страницы +--pretty Читаемый вывод JSON +--help Показать справку +--version Показать версию +``` + +## Лицензия + +MIT diff --git a/apps/cli/README.th.md b/apps/cli/README.th.md new file mode 100644 index 00000000..43df37bf --- /dev/null +++ b/apps/cli/README.th.md @@ -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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### การดำเนินการเอกสาร + +```bash +op open [file.op] # เปิดไฟล์หรือเชื่อมต่อกับแคนวาสสด +op save # บันทึกเอกสารปัจจุบัน +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # รับการเลือกแคนวาสปัจจุบัน +``` + +### การจัดการโหนด + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### การส่งออกโค้ด + +```bash +op export [--out file] +# รูปแบบ: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### ตัวแปรและธีม + +```bash +op vars # รับตัวแปร +op vars:set # ตั้งค่าตัวแปร +op themes # รับธีม +op themes:set # ตั้งค่าธีม +op theme:save # บันทึกพรีเซ็ตธีม +op theme:load # โหลดพรีเซ็ตธีม +op theme:list [dir] # แสดงรายการพรีเซ็ตธีม +``` + +### หน้า + +```bash +op page list # แสดงรายการหน้า +op page add [--name N] # เพิ่มหน้า +op page remove # ลบหน้า +op page rename # เปลี่ยนชื่อหน้า +op page reorder # จัดลำดับหน้าใหม่ +op page duplicate # ทำสำเนาหน้า +``` + +### การนำเข้า + +```bash +op import:svg # นำเข้าไฟล์ SVG +op import:figma # นำเข้าไฟล์ Figma .fig +``` + +### เลย์เอาต์ + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### แฟล็กทั่วไป + +```text +--file ไฟล์ .op เป้าหมาย (ค่าเริ่มต้น: แคนวาสสด) +--page ID หน้าเป้าหมาย +--pretty แสดงผล JSON แบบอ่านง่าย +--help แสดงความช่วยเหลือ +--version แสดงเวอร์ชัน +``` + +## สัญญาอนุญาต + +MIT diff --git a/apps/cli/README.tr.md b/apps/cli/README.tr.md new file mode 100644 index 00000000..4bddc287 --- /dev/null +++ b/apps/cli/README.tr.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### Belge Islemleri + +```bash +op open [dosya.op] # Dosya ac veya canli tuvale baglan +op save # Mevcut belgeyi kaydet +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # Mevcut tuval secimini al +``` + +### Dugum Manipulasyonu + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Kod Disari Aktarimi + +```bash +op export [--out dosya] +# Formatlar: react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### Degiskenler ve Temalar + +```bash +op vars # Degiskenleri al +op vars:set # Degiskenleri ayarla +op themes # Temalari al +op themes:set # Temalari ayarla +op theme:save # Tema onayarini kaydet +op theme:load # 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 # Sayfa kaldir +op page rename # Sayfayi yeniden adlandir +op page reorder # Sayfayi yeniden sirala +op page duplicate # Sayfayi cogalt +``` + +### Iceri Aktarma + +```bash +op import:svg # SVG dosyasi iceri aktar +op import:figma # 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 Hedef .op dosyasi (varsayilan: canli tuval) +--page Hedef sayfa kimligi +--pretty Okunabilir JSON ciktisi +--help Yardimi goster +--version Surumu goster +``` + +## Lisans + +MIT diff --git a/apps/cli/README.vi.md b/apps/cli/README.vi.md new file mode 100644 index 00000000..2cfbfd7a --- /dev/null +++ b/apps/cli/README.vi.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-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 # 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 [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### Xuất mã nguồn + +```bash +op export [--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 # Đặt biến +op themes # Lấy giao diện +op themes:set # Đặt giao diện +op theme:save # Lưu bộ giao diện mẫu +op theme:load # 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 # Xóa trang +op page rename # Đổi tên trang +op page reorder # Sắp xếp lại trang +op page duplicate # Nhân bản trang +``` + +### Nhập + +```bash +op import:svg # Nhập tệp SVG +op import:figma # 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 Tệp .op đích (mặc định: canvas trực tiếp) +--page 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 diff --git a/apps/cli/README.zh-TW.md b/apps/cli/README.zh-TW.md new file mode 100644 index 00000000..3402d04b --- /dev/null +++ b/apps/cli/README.zh-TW.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### 文件操作 + +```bash +op open [file.op] # 開啟檔案或連線至即時畫布 +op save # 儲存目前的文件 +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # 取得目前畫布的選取項目 +``` + +### 節點操作 + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### 程式碼匯出 + +```bash +op export [--out file] +# 格式:react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### 變數與主題 + +```bash +op vars # 取得變數 +op vars:set # 設定變數 +op themes # 取得主題 +op themes:set # 設定主題 +op theme:save # 儲存主題預設 +op theme:load # 載入主題預設 +op theme:list [dir] # 列出主題預設 +``` + +### 頁面 + +```bash +op page list # 列出頁面 +op page add [--name N] # 新增頁面 +op page remove # 移除頁面 +op page rename # 重新命名頁面 +op page reorder # 重新排序頁面 +op page duplicate # 複製頁面 +``` + +### 匯入 + +```bash +op import:svg # 匯入 SVG 檔案 +op import:figma # 匯入 Figma .fig 檔案 +``` + +### 版面配置 + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### 全域旗標 + +```text +--file 目標 .op 檔案(預設:即時畫布) +--page 目標頁面 ID +--pretty 人類可讀的 JSON 輸出 +--help 顯示說明 +--version 顯示版本 +``` + +## 授權條款 + +MIT diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md new file mode 100644 index 00000000..9af6996c --- /dev/null +++ b/apps/cli/README.zh.md @@ -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 [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 [--post-process] [--canvas-width N] +op design:skeleton +op design:content +op design:refine --root-id +``` + +### 文档操作 + +```bash +op open [file.op] # 打开文件或连接到实时画布 +op save # 保存当前文档 +op get [--type X] [--name Y] [--id Z] [--depth N] +op selection # 获取当前画布选中项 +``` + +### 节点操作 + +```bash +op insert [--parent P] [--index N] [--post-process] +op update [--post-process] +op delete +op move --parent

[--index N] +op copy [--parent P] +op replace [--post-process] +``` + +### 代码导出 + +```bash +op export [--out file] +# 格式:react, html, vue, svelte, flutter, swiftui, compose, rn, css +``` + +### 变量与主题 + +```bash +op vars # 获取变量 +op vars:set # 设置变量 +op themes # 获取主题 +op themes:set # 设置主题 +op theme:save # 保存主题预设 +op theme:load # 加载主题预设 +op theme:list [dir] # 列出主题预设 +``` + +### 页面 + +```bash +op page list # 列出页面 +op page add [--name N] # 添加页面 +op page remove # 删除页面 +op page rename # 重命名页面 +op page reorder # 调整页面顺序 +op page duplicate # 复制页面 +``` + +### 导入 + +```bash +op import:svg # 导入 SVG 文件 +op import:figma # 导入 Figma .fig 文件 +``` + +### 布局 + +```bash +op layout [--parent P] [--depth N] +op find-space [--direction right|bottom|left|top] +``` + +### 全局选项 + +```text +--file 目标 .op 文件(默认:实时画布) +--page 目标页面 ID +--pretty 人类可读的 JSON 输出 +--help 显示帮助 +--version 显示版本 +``` + +## 许可证 + +MIT diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 00000000..5b6b4dd2 --- /dev/null +++ b/apps/cli/package.json @@ -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" + } +} diff --git a/apps/cli/src/commands/app.ts b/apps/cli/src/commands/app.ts new file mode 100644 index 00000000..26d49aa7 --- /dev/null +++ b/apps/cli/src/commands/app.ts @@ -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 { + 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 { + 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 { + 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 }) + } +} diff --git a/apps/cli/src/commands/design.ts b/apps/cli/src/commands/design.ts new file mode 100644 index 00000000..30cedc0e --- /dev/null +++ b/apps/cli/src/commands/design.ts @@ -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 { + 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 { + const json = (await parseJsonArg(args[0])) as Record + 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 { + const sectionId = args[0] + if (!sectionId) outputError('Usage: openpencil design:content ') + const json = (await parseJsonArg(args[1])) as Record + 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 { + if (!flags.rootId) outputError('Usage: openpencil design:refine --root-id ') + const result = await handleDesignRefine({ + filePath: flags.file, + rootId: flags.rootId!, + canvasWidth: flags.canvasWidth ? parseInt(flags.canvasWidth, 10) : undefined, + pageId: flags.page, + }) + output(result) +} diff --git a/apps/cli/src/commands/document.ts b/apps/cli/src/commands/document.ts new file mode 100644 index 00000000..c64adb4e --- /dev/null +++ b/apps/cli/src/commands/document.ts @@ -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 { + const result = await handleOpenDocument({ filePath: flags.file ?? args[0] }) + output(result) +} + +export async function cmdSave(args: string[], flags: GlobalFlags): Promise { + const target = args[0] + if (!target) outputError('Usage: openpencil save ') + 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 { + 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 { + const result = await handleGetSelection({ + filePath: flags.file, + readDepth: flags.depth ? parseInt(flags.depth, 10) : undefined, + }) + output(result) +} diff --git a/apps/cli/src/commands/export.ts b/apps/cli/src/commands/export.ts new file mode 100644 index 00000000..2142884b --- /dev/null +++ b/apps/cli/src/commands/export.ts @@ -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 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(``) + parts.push(result.html) + return parts.join('\n\n') +} + +export async function cmdExport( + args: string[], + flags: { file?: string; out?: string }, +): Promise { + const format = args[0] + if (!format) { + outputError( + `Usage: op export [--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) + } +} diff --git a/apps/cli/src/commands/import.ts b/apps/cli/src/commands/import.ts new file mode 100644 index 00000000..2dfea3fa --- /dev/null +++ b/apps/cli/src/commands/import.ts @@ -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 { + const svgPath = args[0] + if (!svgPath) outputError('Usage: op import: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 { + const figPath = args[0] + if (!figPath) outputError('Usage: op import:figma [--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, + }) +} diff --git a/apps/cli/src/commands/layout.ts b/apps/cli/src/commands/layout.ts new file mode 100644 index 00000000..9f48229c --- /dev/null +++ b/apps/cli/src/commands/layout.ts @@ -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 { + 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 { + 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) +} diff --git a/apps/cli/src/commands/nodes.ts b/apps/cli/src/commands/nodes.ts new file mode 100644 index 00000000..5315001c --- /dev/null +++ b/apps/cli/src/commands/nodes.ts @@ -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 { + const data = (await parseJsonArg(args[0])) as Record + 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 { + const nodeId = args[0] + if (!nodeId) outputError('Usage: openpencil update ') + const data = (await parseJsonArg(args[1])) as Record + 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 { + const nodeId = args[0] + if (!nodeId) outputError('Usage: openpencil delete ') + 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 { + const nodeId = args[0] + if (!nodeId) outputError('Usage: openpencil move --parent ') + 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 { + const sourceId = args[0] + if (!sourceId) outputError('Usage: openpencil copy [--parent ]') + 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 { + const nodeId = args[0] + if (!nodeId) outputError('Usage: openpencil replace ') + const data = (await parseJsonArg(args[1])) as Record + const result = await handleReplaceNode({ + filePath: flags.file, + nodeId, + data, + postProcess: flags.postProcess, + pageId: flags.page, + }) + output(result) +} diff --git a/apps/cli/src/commands/pages.ts b/apps/cli/src/commands/pages.ts new file mode 100644 index 00000000..0c2e7443 --- /dev/null +++ b/apps/cli/src/commands/pages.ts @@ -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 { + 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 { + const result = await handleAddPage({ + filePath: flags.file, + name: flags.name ?? args[0], + }) + output(result) +} + +export async function cmdPageRemove( + args: string[], + flags: GlobalFlags, +): Promise { + const pageId = args[0] + if (!pageId) outputError('Usage: op page remove ') + const result = await handleRemovePage({ + filePath: flags.file, + pageId, + }) + output(result) +} + +export async function cmdPageRename( + args: string[], + flags: GlobalFlags, +): Promise { + const [pageId, name] = args + if (!pageId || !name) outputError('Usage: op page rename ') + const result = await handleRenamePage({ + filePath: flags.file, + pageId, + name, + }) + output(result) +} + +export async function cmdPageReorder( + args: string[], + flags: GlobalFlags, +): Promise { + const [pageId, indexStr] = args + if (!pageId || !indexStr) outputError('Usage: op page reorder ') + const result = await handleReorderPage({ + filePath: flags.file, + pageId, + index: parseInt(indexStr, 10), + }) + output(result) +} + +export async function cmdPageDuplicate( + args: string[], + flags: GlobalFlags, +): Promise { + const pageId = args[0] + if (!pageId) outputError('Usage: op page duplicate ') + const result = await handleDuplicatePage({ + filePath: flags.file, + pageId, + }) + output(result) +} diff --git a/apps/cli/src/commands/variables.ts b/apps/cli/src/commands/variables.ts new file mode 100644 index 00000000..1fe0e92c --- /dev/null +++ b/apps/cli/src/commands/variables.ts @@ -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 { + const result = await handleGetVariables({ filePath: flags.file }) + output(result) +} + +export async function cmdVarsSet( + args: string[], + flags: GlobalFlags & { replace?: boolean }, +): Promise { + const data = (await parseJsonArg(args[0])) as Record + const result = await handleSetVariables({ + filePath: flags.file, + variables: data as any, + replace: flags.replace, + }) + output(result) +} + +export async function cmdThemes(flags: GlobalFlags): Promise { + const result = await handleGetVariables({ filePath: flags.file }) + output({ themes: result.themes }) +} + +export async function cmdThemesSet( + args: string[], + flags: GlobalFlags & { replace?: boolean }, +): Promise { + const data = (await parseJsonArg(args[0])) as Record + 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 { + const presetPath = args[0] + if (!presetPath) outputError('Usage: op theme:save ') + const result = await handleSaveThemePreset({ + filePath: flags.file, + presetPath, + }) + output(result) +} + +export async function cmdThemeLoad( + args: string[], + flags: GlobalFlags, +): Promise { + const presetPath = args[0] + if (!presetPath) outputError('Usage: op theme:load ') + const result = await handleLoadThemePreset({ + filePath: flags.file, + presetPath, + }) + output(result) +} + +export async function cmdThemeList(args: string[]): Promise { + if (!args[0]) outputError('Usage: op theme:list ') + const result = await handleListThemePresets({ + directory: args[0], + }) + output(result) +} diff --git a/apps/cli/src/connection.ts b/apps/cli/src/connection.ts new file mode 100644 index 00000000..2a17c3fc --- /dev/null +++ b/apps/cli/src/connection.ts @@ -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 { + 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 { + 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 { + return (await getAppInfo()) !== null +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts new file mode 100644 index 00000000..ed5ef18e --- /dev/null +++ b/apps/cli/src/index.ts @@ -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 +} + +function parseArgs(argv: string[]): ParsedArgs { + const args = argv.slice(2) + const positionals: string[] = [] + const flags: Record = {} + + 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 [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 Save current document to file + op get [--type X] [--name Y] [--id Z] [--depth N] + op selection Get current canvas selection + +Nodes: + op insert [--parent P] [--index N] [--post-process] + op update [--post-process] + op delete + op move --parent

[--index N] + op copy [--parent P] + op replace [--post-process] + +Design: + op design + op design:skeleton + op design:content + op design:refine --root-id + +Export: + op export [--out file] + Formats: react, html, vue, svelte, flutter, swiftui, compose, rn, css + +Variables & Themes: + op vars Get variables + op vars:set Set variables + op themes Get themes + op themes:set Set themes + op theme:save Save theme preset + op theme:load Load theme preset + op theme:list [dir] List theme presets + +Pages: + op page list + op page add [--name N] + op page remove + op page rename + op page reorder + op page duplicate + +Import: + op import:svg Import SVG file + op import:figma 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: + Inline string + @filepath Read from file (e.g. @design.txt) + - Read from stdin (e.g. cat design.txt | op design -) + +Global Flags: + --file Target .op file (default: live canvas) + --page Target page ID + --pretty Human-readable JSON output + --help Show this help + --version Show version +` + +// --- Main --- + +async function main(): Promise { + 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)) +}) diff --git a/apps/cli/src/launcher.ts b/apps/cli/src/launcher.ts new file mode 100644 index 00000000..7fb41857 --- /dev/null +++ b/apps/cli/src/launcher.ts @@ -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 { + 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 { + 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 +} diff --git a/apps/cli/src/output.ts b/apps/cli/src/output.ts new file mode 100644 index 00000000..672c4883 --- /dev/null +++ b/apps/cli/src/output.ts @@ -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 = {}): 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 { + 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 { + 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 { + const raw = await resolveArg(arg) + try { + return JSON.parse(raw) + } catch { + outputError(`Invalid JSON: ${raw.slice(0, 200)}...`) + } +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json new file mode 100644 index 00000000..137a94bb --- /dev/null +++ b/apps/cli/tsconfig.json @@ -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": [] +} diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml index c0ec0db9..892ffb57 100644 --- a/apps/desktop/electron-builder.yml +++ b/apps/desktop/electron-builder.yml @@ -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 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3a165265..f91c7190 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@zseven-w/desktop", - "version": "0.5.0", + "version": "0.5.1", "private": true, "type": "module" } diff --git a/apps/web/package.json b/apps/web/package.json index b76e1e0b..2b69e986 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@zseven-w/web", - "version": "0.5.0", + "version": "0.5.1", "private": true, "type": "module" } diff --git a/apps/web/server/api/ai/chat.ts b/apps/web/server/api/ai/chat.ts index 65b79c8d..2c6f356d 100644 --- a/apps/web/server/api/ai/chat.ts +++ b/apps/web/server/api/ai/chat.ts @@ -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 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`), ) diff --git a/apps/web/server/api/ai/connect-agent.ts b/apps/web/server/api/ai/connect-agent.ts index cffe7fdb..b8709779 100644 --- a/apps/web/server/api/ai/connect-agent.ts +++ b/apps/web/server/api/ai/connect-agent.ts @@ -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 { 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 { env, ...(debugFile ? { debugFile } : {}), ...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}), - ...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}), + ...(spawnProcess ? { spawnClaudeCodeProcess: spawnProcess } : {}), }, }) @@ -168,7 +170,34 @@ async function connectClaudeCode(): Promise { 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) } } diff --git a/apps/web/server/api/ai/mcp-install.ts b/apps/web/server/api/ai/mcp-install.ts index 87fd6392..dfeb5d9a 100644 --- a/apps/web/server/api/ai/mcp-install.ts +++ b/apps/web/server/api/ai/mcp-install.ts @@ -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') } /** diff --git a/apps/web/server/utils/resolve-claude-agent-env.ts b/apps/web/server/utils/resolve-claude-agent-env.ts index a1adcecf..b31ee7f5 100644 --- a/apps/web/server/utils/resolve-claude-agent-env.ts +++ b/apps/web/server/utils/resolve-claude-agent-env.ts @@ -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 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 } } diff --git a/apps/web/src/canvas/skia/skia-engine.ts b/apps/web/src/canvas/skia/skia-engine.ts index 5cc675d5..49ea019b 100644 --- a/apps/web/src/canvas/skia/skia-engine.ts +++ b/apps/web/src/canvas/skia/skia-engine.ts @@ -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) diff --git a/apps/web/src/components/panels/ai-chat-panel.tsx b/apps/web/src/components/panels/ai-chat-panel.tsx index e58d7dcc..4428eb65 100644 --- a/apps/web/src/components/panels/ai-chat-panel.tsx +++ b/apps/web/src/components/panels/ai-chat-panel.tsx @@ -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(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 --- */} -

+
{/* Model selector */}
-
- {/* Upward model dropdown */} - {modelDropdownOpen && availableModels.length > 0 && ( -
- {modelGroups.length > 0 - ? modelGroups.map((group) => { + {/* Upward model dropdown */} + {modelDropdownOpen && availableModels.length > 0 && ( +
+ {/* Search input */} +
+
+ + 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 && ( + + )} +
+
+
+ {(() => { + 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 ( +
+ {t('ai.noModelsFound')} +
+ ) + } + + return filtered.map((group) => { const GIcon = PROVIDER_ICON[group.provider] return (
@@ -740,7 +796,7 @@ export default function AIChatPanel() { {isSelected && } {m.displayName} - {idx === 0 && ( + {idx === 0 && !q && ( {t('common.best')} @@ -751,32 +807,52 @@ export default function AIChatPanel() {
) }) - : availableModels.map((m) => { - const isSelected = m.value === model - return ( - - ) - })} + } + + const filtered = availableModels.filter( + (m) => + !q || + m.displayName.toLowerCase().includes(q) || + m.value.toLowerCase().includes(q), + ) + + if (filtered.length === 0) { + return ( +
+ {t('ai.noModelsFound')} +
+ ) + } + + return filtered.map((m) => { + const isSelected = m.value === model + return ( + + ) + }) + })()} +
- )} + )} +
) diff --git a/apps/web/src/i18n/locales/de.ts b/apps/web/src/i18n/locales/de.ts index c8654264..e26234e2 100644 --- a/apps/web/src/i18n/locales/de.ts +++ b/apps/web/src/i18n/locales/de.ts @@ -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', diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index 6feb979a..8caff6bf 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -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', diff --git a/apps/web/src/i18n/locales/es.ts b/apps/web/src/i18n/locales/es.ts index a1c91cda..33d98b6e 100644 --- a/apps/web/src/i18n/locales/es.ts +++ b/apps/web/src/i18n/locales/es.ts @@ -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': diff --git a/apps/web/src/i18n/locales/fr.ts b/apps/web/src/i18n/locales/fr.ts index cbdc538e..516fc7c6 100644 --- a/apps/web/src/i18n/locales/fr.ts +++ b/apps/web/src/i18n/locales/fr.ts @@ -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', diff --git a/apps/web/src/i18n/locales/hi.ts b/apps/web/src/i18n/locales/hi.ts index 7bf51052..c9ba8c9f 100644 --- a/apps/web/src/i18n/locales/hi.ts +++ b/apps/web/src/i18n/locales/hi.ts @@ -315,6 +315,8 @@ const hi: TranslationKeys = { 'ai.sendMessage': 'संदेश भेजें', 'ai.loadingModels': 'मॉडल लोड हो रहे हैं...', 'ai.noModelsConnected': 'कोई मॉडल कनेक्ट नहीं है', + 'ai.searchModels': 'मॉडल खोजें...', + 'ai.noModelsFound': 'कोई मॉडल नहीं मिला', 'ai.quickAction.loginScreen': 'मोबाइल लॉगिन स्क्रीन डिज़ाइन करें', 'ai.quickAction.loginScreenPrompt': 'ईमेल इनपुट, पासवर्ड इनपुट, लॉगिन बटन और सोशल लॉगिन विकल्पों के साथ एक आधुनिक मोबाइल लॉगिन स्क्रीन डिज़ाइन करें', diff --git a/apps/web/src/i18n/locales/id.ts b/apps/web/src/i18n/locales/id.ts index 6c119b35..b6de8c40 100644 --- a/apps/web/src/i18n/locales/id.ts +++ b/apps/web/src/i18n/locales/id.ts @@ -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', diff --git a/apps/web/src/i18n/locales/ja.ts b/apps/web/src/i18n/locales/ja.ts index 858c9419..dc6dae2a 100644 --- a/apps/web/src/i18n/locales/ja.ts +++ b/apps/web/src/i18n/locales/ja.ts @@ -319,6 +319,8 @@ const ja: TranslationKeys = { 'ai.sendMessage': 'メッセージを送信', 'ai.loadingModels': 'モデルを読み込み中...', 'ai.noModelsConnected': 'モデルが接続されていません', + 'ai.searchModels': 'モデルを検索...', + 'ai.noModelsFound': 'モデルが見つかりません', 'ai.quickAction.loginScreen': 'モバイルログイン画面をデザイン', 'ai.quickAction.loginScreenPrompt': 'メール入力、パスワード入力、ログインボタン、ソーシャルログインオプションを含む、モダンなモバイルログイン画面をデザインしてください', diff --git a/apps/web/src/i18n/locales/ko.ts b/apps/web/src/i18n/locales/ko.ts index a1cfe92d..7e932740 100644 --- a/apps/web/src/i18n/locales/ko.ts +++ b/apps/web/src/i18n/locales/ko.ts @@ -315,6 +315,8 @@ const ko: TranslationKeys = { 'ai.sendMessage': '메시지 보내기', 'ai.loadingModels': '모델 로딩 중...', 'ai.noModelsConnected': '연결된 모델 없음', + 'ai.searchModels': '모델 검색...', + 'ai.noModelsFound': '일치하는 모델이 없습니다', 'ai.quickAction.loginScreen': '모바일 로그인 화면 디자인', 'ai.quickAction.loginScreenPrompt': '이메일 입력, 비밀번호 입력, 로그인 버튼, 소셜 로그인 옵션이 있는 모던 모바일 로그인 화면을 디자인해 주세요', diff --git a/apps/web/src/i18n/locales/pt.ts b/apps/web/src/i18n/locales/pt.ts index a827d278..97fe7c5a 100644 --- a/apps/web/src/i18n/locales/pt.ts +++ b/apps/web/src/i18n/locales/pt.ts @@ -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', diff --git a/apps/web/src/i18n/locales/ru.ts b/apps/web/src/i18n/locales/ru.ts index 9156ce62..06145f1d 100644 --- a/apps/web/src/i18n/locales/ru.ts +++ b/apps/web/src/i18n/locales/ru.ts @@ -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', diff --git a/apps/web/src/i18n/locales/th.ts b/apps/web/src/i18n/locales/th.ts index 38ab5b7f..4b7d37bc 100644 --- a/apps/web/src/i18n/locales/th.ts +++ b/apps/web/src/i18n/locales/th.ts @@ -315,6 +315,8 @@ const th: TranslationKeys = { 'ai.sendMessage': 'ส่งข้อความ', 'ai.loadingModels': 'กำลังโหลดโมเดล...', 'ai.noModelsConnected': 'ไม่มีโมเดลที่เชื่อมต่อ', + 'ai.searchModels': 'ค้นหาโมเดล...', + 'ai.noModelsFound': 'ไม่พบโมเดล', 'ai.quickAction.loginScreen': 'ออกแบบหน้าจอเข้าสู่ระบบมือถือ', 'ai.quickAction.loginScreenPrompt': 'ออกแบบหน้าจอเข้าสู่ระบบมือถือที่ทันสมัย พร้อมช่องกรอกอีเมล รหัสผ่าน ปุ่มเข้าสู่ระบบ และตัวเลือกเข้าสู่ระบบผ่านโซเชียล', diff --git a/apps/web/src/i18n/locales/tr.ts b/apps/web/src/i18n/locales/tr.ts index 0071ae8a..28b4f5a2 100644 --- a/apps/web/src/i18n/locales/tr.ts +++ b/apps/web/src/i18n/locales/tr.ts @@ -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', diff --git a/apps/web/src/i18n/locales/vi.ts b/apps/web/src/i18n/locales/vi.ts index e65f395c..5ac1c7c0 100644 --- a/apps/web/src/i18n/locales/vi.ts +++ b/apps/web/src/i18n/locales/vi.ts @@ -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', diff --git a/apps/web/src/i18n/locales/zh-tw.ts b/apps/web/src/i18n/locales/zh-tw.ts index c62fda2f..b260456a 100644 --- a/apps/web/src/i18n/locales/zh-tw.ts +++ b/apps/web/src/i18n/locales/zh-tw.ts @@ -309,6 +309,8 @@ const zhTW: TranslationKeys = { 'ai.sendMessage': '傳送訊息', 'ai.loadingModels': '正在載入模型...', 'ai.noModelsConnected': '尚未連線模型', + 'ai.searchModels': '搜尋模型...', + 'ai.noModelsFound': '未找到匹配的模型', 'ai.quickAction.loginScreen': '設計行動裝置登入頁面', 'ai.quickAction.loginScreenPrompt': '設計一個現代的行動裝置登入頁面,包含電子郵件輸入框、密碼輸入框、登入按鈕和社群登入選項', diff --git a/apps/web/src/i18n/locales/zh.ts b/apps/web/src/i18n/locales/zh.ts index e1e65e22..89efbe3e 100644 --- a/apps/web/src/i18n/locales/zh.ts +++ b/apps/web/src/i18n/locales/zh.ts @@ -309,6 +309,8 @@ const zh: TranslationKeys = { 'ai.sendMessage': '发送消息', 'ai.loadingModels': '正在加载模型...', 'ai.noModelsConnected': '未连接模型', + 'ai.searchModels': '搜索模型...', + 'ai.noModelsFound': '未找到匹配的模型', 'ai.quickAction.loginScreen': '设计一个移动端登录页面', 'ai.quickAction.loginScreenPrompt': '设计一个现代的移动端登录页面,包含邮箱输入框、密码输入框、登录按钮和社交登录选项', diff --git a/apps/web/src/mcp/tools/batch-design.ts b/apps/web/src/mcp/tools/batch-design.ts index 909f1c9d..bbf15718 100644 --- a/apps/web/src/mcp/tools/batch-design.ts +++ b/apps/web/src/mcp/tools/batch-design.ts @@ -482,21 +482,63 @@ function applyDescendantOverrides( /** Parse a JSON-like argument, handling unquoted keys. */ function parseJsonArg(str: string): Record { - 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 diff --git a/apps/web/src/mcp/tools/layered-design-defs.ts b/apps/web/src/mcp/tools/layered-design-defs.ts index c5803a66..94f434e7 100644 --- a/apps/web/src/mcp/tools/layered-design-defs.ts +++ b/apps/web/src/mcp/tools/layered-design-defs.ts @@ -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' }, diff --git a/bun.lock b/bun.lock index effcb51d..bdb868c8 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index a31493d3..48c2969d 100644 --- a/package.json +++ b/package.json @@ -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 \" && 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": { diff --git a/packages/pen-codegen/README.md b/packages/pen-codegen/README.md new file mode 100644 index 00000000..31f12422 --- /dev/null +++ b/packages/pen-codegen/README.md @@ -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 diff --git a/packages/pen-codegen/package.json b/packages/pen-codegen/package.json index 7266e5fd..19e84f19 100644 --- a/packages/pen-codegen/package.json +++ b/packages/pen-codegen/package.json @@ -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" diff --git a/packages/pen-core/README.md b/packages/pen-core/README.md new file mode 100644 index 00000000..8ecfeea7 --- /dev/null +++ b/packages/pen-core/README.md @@ -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 diff --git a/packages/pen-core/package.json b/packages/pen-core/package.json index 57e1d9f8..79c5707d 100644 --- a/packages/pen-core/package.json +++ b/packages/pen-core/package.json @@ -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" }, diff --git a/packages/pen-figma/README.md b/packages/pen-figma/README.md new file mode 100644 index 00000000..931987b5 --- /dev/null +++ b/packages/pen-figma/README.md @@ -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 diff --git a/packages/pen-figma/package.json b/packages/pen-figma/package.json index 48894a57..63a13328 100644 --- a/packages/pen-figma/package.json +++ b/packages/pen-figma/package.json @@ -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" diff --git a/packages/pen-renderer/README.md b/packages/pen-renderer/README.md new file mode 100644 index 00000000..04b62374 --- /dev/null +++ b/packages/pen-renderer/README.md @@ -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 diff --git a/packages/pen-renderer/package.json b/packages/pen-renderer/package.json index 343cd772..462a5e12 100644 --- a/packages/pen-renderer/package.json +++ b/packages/pen-renderer/package.json @@ -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": { diff --git a/packages/pen-renderer/src/text-renderer.ts b/packages/pen-renderer/src/text-renderer.ts index 814432e0..89bfdb28 100644 --- a/packages/pen-renderer/src/text-renderer.ts +++ b/packages/pen-renderer/src/text-renderer.ts @@ -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() + 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}` diff --git a/packages/pen-sdk/README.md b/packages/pen-sdk/README.md new file mode 100644 index 00000000..bd562fbe --- /dev/null +++ b/packages/pen-sdk/README.md @@ -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 diff --git a/packages/pen-sdk/package.json b/packages/pen-sdk/package.json index 359f69f5..4ce111ef 100644 --- a/packages/pen-sdk/package.json +++ b/packages/pen-sdk/package.json @@ -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" diff --git a/packages/pen-types/README.md b/packages/pen-types/README.md new file mode 100644 index 00000000..e8cfdc4d --- /dev/null +++ b/packages/pen-types/README.md @@ -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 diff --git a/packages/pen-types/package.json b/packages/pen-types/package.json index 5aaec637..e43377b7 100644 --- a/packages/pen-types/package.json +++ b/packages/pen-types/package.json @@ -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": { diff --git a/scripts/publish-beta.sh b/scripts/publish-beta.sh new file mode 100755 index 00000000..7ef80179 --- /dev/null +++ b/scripts/publish-beta.sh @@ -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 "================================"