mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-05-31 19:04:29 +07:00
V0.5.1 (#77)
* fix(docker): support multi-platform builds and fix monorepo paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * perf(renderer): cache pre-rasterized paragraph images to avoid per-frame glyph rasterization (#76) * fix(canvas): stabilize frame label size during zoom Draw frame labels in screen-space after the viewport transform restore, converting scene coords manually. Previously fontSize=12/zoom fed into Math.ceil caused integer-boundary jumps that made labels flicker during zoom. Also skip shadow rendering while actively zooming for smoother performance. * perf(renderer): cache pre-rasterized paragraph images to avoid per-frame glyph rasterization - Add paraImageCache (SkImage, 128 MB LRU limit) keyed on the same key as paraCache - Use drawImageRect instead of drawParagraph on cache hit, skipping per-frame glyph shaping and rasterization - Fall back to direct drawParagraph only when off-screen surface creation (MakeSurface) fails - Extract _dpr getter to deduplicate device-pixel-ratio resolution logic across draw paths - Evict oldest entries when cache exceeds byte limit; delete SkImage on eviction and dispose() * feat(cli): introduce OpenPencil CLI for terminal control of the design tool - Added a new CLI application under `apps/cli` to manage OpenPencil from the terminal. - Implemented commands for app control (`start`, `stop`, `status`), document operations (`open`, `save`, `get`, `selection`), and design manipulation (`design`, `import`). - Enhanced documentation with usage instructions and platform support details. - Updated build scripts to include CLI compilation and publishing processes. - Introduced a new GitHub Actions workflow for publishing the CLI to npm. - Updated existing workflows to integrate CLI build steps and ensure proper versioning across packages. * docs: update README files to include CLI tool details and multi-platform code export - Added CLI section to README files in multiple languages, detailing commands for terminal control of the design tool. - Included instructions for global installation and usage examples for the CLI. - Expanded documentation on multi-platform code export capabilities from a single `.op` file to various frameworks. - Updated CLAUDE.md to reference the new CLI documentation and its integration with the design tool. * chore(bun.lock): update package dependencies to specific versions - Removed workspace references for several packages in the bun.lock file. - Updated dependencies for `@zseven-w/pen-core`, `@zseven-w/pen-types`, `@zseven-w/pen-codegen`, `@zseven-w/pen-figma`, and `@zseven-w/pen-renderer` to version `0.5.1-beta.1`. - Ensured consistency in dependency management across the project. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: leinaldo <60176594+leinaldo@users.noreply.github.com>
This commit is contained in:
parent
6c89c6dc9f
commit
b4d1d2a7bb
93 changed files with 5302 additions and 213 deletions
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
11
.github/workflows/docker.yml
vendored
11
.github/workflows/docker.yml
vendored
|
|
@ -24,24 +24,31 @@ jobs:
|
|||
- variant: base
|
||||
target: base
|
||||
suffix: ''
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- variant: claude
|
||||
target: with-claude
|
||||
suffix: '-claude'
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- variant: codex
|
||||
target: with-codex
|
||||
suffix: '-codex'
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- variant: opencode
|
||||
target: with-opencode
|
||||
suffix: '-opencode'
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- variant: copilot
|
||||
target: with-copilot
|
||||
suffix: '-copilot'
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- variant: gemini
|
||||
target: with-gemini
|
||||
suffix: '-gemini'
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- variant: full
|
||||
target: full
|
||||
suffix: '-full'
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -65,6 +72,9 @@ jobs:
|
|||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
|
@ -73,6 +83,7 @@ jobs:
|
|||
with:
|
||||
context: .
|
||||
target: ${{ matrix.target }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
|
|
|||
105
.github/workflows/publish-cli.yml
vendored
Normal file
105
.github/workflows/publish-cli.yml
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
name: Publish npm
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Replace workspace:* with version
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
for f in packages/*/package.json apps/cli/package.json; do
|
||||
if [ -f "$f" ]; then
|
||||
jq --arg v "$VERSION" '
|
||||
if .dependencies then
|
||||
.dependencies |= with_entries(
|
||||
if .value == "workspace:*" then .value = $v else . end
|
||||
)
|
||||
else . end |
|
||||
if .devDependencies then
|
||||
.devDependencies |= with_entries(
|
||||
if .value == "workspace:*" then .value = $v else . end
|
||||
)
|
||||
else . end
|
||||
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
|
||||
echo "Updated $f"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Compile CLI
|
||||
run: bun run cli:compile
|
||||
|
||||
- name: Verify CLI build
|
||||
run: node apps/cli/dist/openpencil-cli.cjs --version
|
||||
|
||||
# Publish in topological order
|
||||
- name: Publish pen-types
|
||||
run: npm publish --access public || true
|
||||
working-directory: packages/pen-types
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish pen-core
|
||||
run: npm publish --access public || true
|
||||
working-directory: packages/pen-core
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish pen-codegen
|
||||
run: npm publish --access public || true
|
||||
working-directory: packages/pen-codegen
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish pen-figma
|
||||
run: npm publish --access public || true
|
||||
working-directory: packages/pen-figma
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish pen-renderer
|
||||
run: npm publish --access public || true
|
||||
working-directory: packages/pen-renderer
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish pen-sdk
|
||||
run: npm publish --access public || true
|
||||
working-directory: packages/pen-sdk
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish CLI
|
||||
run: npm publish --access public || true
|
||||
working-directory: apps/cli
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
25
CLAUDE.md
25
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 <dsl|@file|->` — batch design DSL operations
|
||||
- **Document:** `op open`, `op save`, `op get`, `op selection`
|
||||
- **Nodes:** `op insert`, `op update`, `op delete`, `op move`, `op copy`, `op replace`
|
||||
- **Export:** `op export <react|html|vue|svelte|flutter|swiftui|compose|rn|css>`
|
||||
- **Cross-platform:** macOS, Windows (NSIS/portable), Linux (AppImage/deb/snap/flatpak)
|
||||
|
||||
### CI / CD
|
||||
|
||||
- **`.github/workflows/ci.yml`** — Push/PR: type check, tests, web build
|
||||
- **`.github/workflows/ci.yml`** — Push/PR on `main` and `v*` branches: type check, tests, web build
|
||||
- **`.github/workflows/build-electron.yml`** — Tag push (`v*`) or manual: builds Electron for all platforms, creates draft GitHub Release
|
||||
- **`.github/workflows/publish-cli.yml`** — Tag push (`v*`) or manual: publishes all `@zseven-w/*` npm packages in topological order
|
||||
- **`.github/workflows/docker.yml`** — Docker image build and push
|
||||
|
||||
### Version Sync
|
||||
|
|
@ -119,7 +136,7 @@ Use [Conventional Commits](https://www.conventionalcommits.org/) format: `<type>
|
|||
|
||||
**Types:** `feat`, `fix`, `refactor`, `perf`, `style`, `docs`, `test`, `chore`
|
||||
|
||||
**Scopes:** `editor`, `canvas`, `panels`, `history`, `ai`, `codegen`, `store`, `types`, `variables`, `figma`, `mcp`, `electron`, `renderer`, `sdk`
|
||||
**Scopes:** `editor`, `canvas`, `panels`, `history`, `ai`, `codegen`, `store`, `types`, `variables`, `figma`, `mcp`, `electron`, `renderer`, `sdk`, `cli`
|
||||
|
||||
**Rules:** Subject in English, lowercase start, no period, imperative mood. Body is optional; explain **why** not what. One commit per change.
|
||||
|
||||
|
|
|
|||
51
README.de.md
51
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.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Steuern Sie das Design-Tool vom Terminal aus. `op design`, `op insert`, `op export` — Batch-Design-DSL, Knotenmanipulation, Code-Export. Pipe-Eingabe von Dateien oder stdin. Funktioniert mit der Desktop-App oder dem Webserver.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Multiplattform-Code-Export
|
||||
|
||||
Export aus einer einzigen `.op`-Datei nach React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Design-Variablen werden zu CSS Custom Properties.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Global installieren und das Design-Tool vom Terminal aus steuern:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Desktop-App starten
|
||||
op design @landing.txt # Batch-Design aus Datei
|
||||
op insert '{"type":"RECT"}' # Knoten einfügen
|
||||
op export react --out . # Nach React + Tailwind exportieren
|
||||
op import:figma design.fig # Figma-Datei importieren
|
||||
cat design.dsl | op design - # Pipe von stdin
|
||||
```
|
||||
|
||||
Unterstützt drei Eingabemethoden: Inline-String, `@filepath` (aus Datei lesen) oder `-` (von stdin lesen). Funktioniert mit der Desktop-App oder dem Web-Entwicklungsserver. Siehe [CLI README](./apps/cli/README.md) für die vollständige Befehlsreferenz.
|
||||
|
||||
## Funktionen
|
||||
|
||||
**Canvas und Zeichnen**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **State** | Zustand v5 |
|
||||
| **Server** | Nitro |
|
||||
| **Desktop** | Electron 35 |
|
||||
| **CLI** | `op` — Terminal-Steuerung, Batch-Design-DSL, Code-Export |
|
||||
| **KI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Laufzeit** | Bun · Vite 7 |
|
||||
| **Dateiformat** | `.op` — JSON-basiert, menschenlesbar, Git-freundlich |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro-API — Streaming-Chat, Generierung, Validierung
|
||||
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot-Wrapper
|
||||
│ └── desktop/ Electron-Desktop-App
|
||||
│ ├── main.ts Fenster, Nitro-Fork, natives Menü, Auto-Updater
|
||||
│ ├── ipc-handlers.ts Native Dateidialoge, Theme-Sync, Einstellungen-IPC
|
||||
│ └── preload.ts IPC-Brücke
|
||||
│ ├── desktop/ Electron-Desktop-App
|
||||
│ │ ├── main.ts Fenster, Nitro-Fork, natives Menü, Auto-Updater
|
||||
│ │ ├── ipc-handlers.ts Native Dateidialoge, Theme-Sync, Einstellungen-IPC
|
||||
│ │ └── preload.ts IPC-Brücke
|
||||
│ └── cli/ CLI-Tool — `op`-Befehl
|
||||
│ ├── src/commands/ Design-, Dokument-, Export-, Import-, Knoten-, Seiten-, Variablen-Befehle
|
||||
│ ├── connection.ts WebSocket-Verbindung zur laufenden App
|
||||
│ └── launcher.ts Automatische Erkennung und Start der Desktop-App oder des Webservers
|
||||
├── packages/
|
||||
│ ├── pen-types/ Typdefinitionen für das PenDocument-Modell
|
||||
│ ├── pen-core/ Dokumentbaum-Operationen, Layout-Engine, Variablen
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Typprüfung
|
|||
bun run bump <version> # Version über alle package.json synchronisieren
|
||||
bun run electron:dev # Electron-Entwicklung
|
||||
bun run electron:build # Electron-Paketierung
|
||||
bun run cli:dev # CLI aus Quellcode ausführen
|
||||
bun run cli:compile # CLI nach dist kompilieren
|
||||
```
|
||||
|
||||
## Mitwirken
|
||||
|
|
@ -305,6 +347,7 @@ Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetail
|
|||
- [x] Boolesche Operationen (Vereinigung, Subtraktion, Schnittmenge)
|
||||
- [x] Multi-Modell-Fähigkeitsprofile
|
||||
- [x] Monorepo-Umstrukturierung mit wiederverwendbaren Paketen
|
||||
- [x] CLI-Tool (`op`) für Terminal-Steuerung
|
||||
- [ ] Kollaboratives Bearbeiten
|
||||
- [ ] Plugin-System
|
||||
|
||||
|
|
|
|||
51
README.es.md
51
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.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Controla la herramienta de diseño desde la terminal. `op design`, `op insert`, `op export` — DSL de diseño por lotes, manipulación de nodos, exportación de código. Entrada por pipe desde archivos o stdin. Funciona con la app de escritorio o el servidor web.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Exportación de Código Multiplataforma
|
||||
|
||||
Exporta desde un solo archivo `.op` a React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Las variables de diseño se convierten en propiedades CSS personalizadas.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Instala globalmente y controla la herramienta de diseño desde tu terminal:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Iniciar la app de escritorio
|
||||
op design @landing.txt # Diseño por lotes desde archivo
|
||||
op insert '{"type":"RECT"}' # Insertar un nodo
|
||||
op export react --out . # Exportar a React + Tailwind
|
||||
op import:figma design.fig # Importar archivo de Figma
|
||||
cat design.dsl | op design - # Entrada por pipe desde stdin
|
||||
```
|
||||
|
||||
Soporta tres métodos de entrada: cadena inline, `@filepath` (leer desde archivo), o `-` (leer desde stdin). Funciona con la app de escritorio o el servidor de desarrollo web. Consulta el [README del CLI](./apps/cli/README.md) para la referencia completa de comandos.
|
||||
|
||||
## Características
|
||||
|
||||
**Lienzo y Dibujo**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **Estado** | Zustand v5 |
|
||||
| **Servidor** | Nitro |
|
||||
| **Escritorio** | Electron 35 |
|
||||
| **CLI** | `op` — control desde terminal, DSL de diseño por lotes, exportación de código |
|
||||
| **IA** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Runtime** | Bun · Vite 7 |
|
||||
| **Formato de archivo** | `.op` — basado en JSON, legible por humanos, compatible con Git |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ API Nitro — chat en streaming, generación, validación
|
||||
│ │ └── utils/ Wrappers de Claude CLI, OpenCode, Codex, Copilot
|
||||
│ └── desktop/ Aplicación de escritorio Electron
|
||||
│ ├── main.ts Ventana, fork Nitro, menú nativo, actualizador automático
|
||||
│ ├── ipc-handlers.ts Diálogos de archivos nativos, sincronización de tema, preferencias IPC
|
||||
│ └── preload.ts Puente IPC
|
||||
│ ├── desktop/ Aplicación de escritorio Electron
|
||||
│ │ ├── main.ts Ventana, fork Nitro, menú nativo, actualizador automático
|
||||
│ │ ├── ipc-handlers.ts Diálogos de archivos nativos, sincronización de tema, preferencias IPC
|
||||
│ │ └── preload.ts Puente IPC
|
||||
│ └── cli/ Herramienta CLI — comando `op`
|
||||
│ ├── src/commands/ Comandos de diseño, documento, exportación, importación, nodo, página, variable
|
||||
│ ├── connection.ts Conexión WebSocket a la app en ejecución
|
||||
│ └── launcher.ts Auto-detección e inicio de la app de escritorio o servidor web
|
||||
├── packages/
|
||||
│ ├── pen-types/ Definiciones de tipos para el modelo PenDocument
|
||||
│ ├── pen-core/ Operaciones de árbol del documento, motor de diseño, variables
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Verificación de tipos
|
|||
bun run bump <version> # Sincronizar versión en todos los package.json
|
||||
bun run electron:dev # Desarrollo con Electron
|
||||
bun run electron:build # Empaquetado de Electron
|
||||
bun run cli:dev # Ejecutar CLI desde el código fuente
|
||||
bun run cli:compile # Compilar CLI a dist
|
||||
```
|
||||
|
||||
## Contribuir
|
||||
|
|
@ -305,6 +347,7 @@ bun run electron:build # Empaquetado de Electron
|
|||
- [x] Operaciones booleanas (unión, sustracción, intersección)
|
||||
- [x] Perfiles de capacidad multimodelo
|
||||
- [x] Reestructuración en monorepo con paquetes reutilizables
|
||||
- [x] Herramienta CLI (`op`) para control desde terminal
|
||||
- [ ] Edición colaborativa
|
||||
- [ ] Sistema de plugins
|
||||
|
||||
|
|
|
|||
51
README.fr.md
51
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.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Contrôlez l'outil de design depuis le terminal. `op design`, `op insert`, `op export` — DSL de design par lots, manipulation de nœuds, export de code. Entrée par pipe depuis des fichiers ou stdin. Fonctionne avec l'app de bureau ou le serveur web.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Export de Code Multi-Plateforme
|
||||
|
||||
Exportez depuis un seul fichier `.op` vers React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Les variables de design deviennent des propriétés CSS personnalisées.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Installez globalement et contrôlez l'outil de design depuis votre terminal :
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Lancer l'app de bureau
|
||||
op design @landing.txt # Design par lots depuis un fichier
|
||||
op insert '{"type":"RECT"}' # Insérer un nœud
|
||||
op export react --out . # Exporter en React + Tailwind
|
||||
op import:figma design.fig # Importer un fichier Figma
|
||||
cat design.dsl | op design - # Pipe depuis stdin
|
||||
```
|
||||
|
||||
Supporte trois méthodes d'entrée : chaîne en ligne, `@filepath` (lecture depuis un fichier), ou `-` (lecture depuis stdin). Fonctionne avec l'app de bureau ou le serveur de développement web. Voir le [README du CLI](./apps/cli/README.md) pour la référence complète des commandes.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
**Canevas et dessin**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **État** | Zustand v5 |
|
||||
| **Serveur** | Nitro |
|
||||
| **Bureau** | Electron 35 |
|
||||
| **CLI** | `op` — contrôle depuis le terminal, DSL de design par lots, export de code |
|
||||
| **IA** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Runtime** | Bun · Vite 7 |
|
||||
| **Format de fichier** | `.op` — basé sur JSON, lisible par l'humain, compatible Git |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ API Nitro — chat en streaming, génération, validation
|
||||
│ │ └── utils/ Enveloppes Claude CLI, OpenCode, Codex, Copilot
|
||||
│ └── desktop/ Application de bureau Electron
|
||||
│ ├── main.ts Fenêtre, fork Nitro, menu natif, mise à jour automatique
|
||||
│ ├── ipc-handlers.ts Dialogues fichiers natifs, sync thème, préférences IPC
|
||||
│ └── preload.ts Pont IPC
|
||||
│ ├── desktop/ Application de bureau Electron
|
||||
│ │ ├── main.ts Fenêtre, fork Nitro, menu natif, mise à jour automatique
|
||||
│ │ ├── ipc-handlers.ts Dialogues fichiers natifs, sync thème, préférences IPC
|
||||
│ │ └── preload.ts Pont IPC
|
||||
│ └── cli/ Outil CLI — commande `op`
|
||||
│ ├── src/commands/ Commandes design, document, export, import, nœud, page, variable
|
||||
│ ├── connection.ts Connexion WebSocket à l'app en cours d'exécution
|
||||
│ └── launcher.ts Détection automatique et lancement de l'app de bureau ou du serveur web
|
||||
├── packages/
|
||||
│ ├── pen-types/ Définitions de types pour le modèle PenDocument
|
||||
│ ├── pen-core/ Opérations sur l'arbre du document, moteur de mise en page, variables
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Vérification des types
|
|||
bun run bump <version> # Synchroniser la version dans tous les package.json
|
||||
bun run electron:dev # Développement Electron
|
||||
bun run electron:build # Packaging Electron
|
||||
bun run cli:dev # Exécuter le CLI depuis les sources
|
||||
bun run cli:compile # Compiler le CLI vers dist
|
||||
```
|
||||
|
||||
## Contribuer
|
||||
|
|
@ -305,6 +347,7 @@ Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour
|
|||
- [x] Opérations booléennes (union, soustraction, intersection)
|
||||
- [x] Profils de capacités multi-modèles
|
||||
- [x] Restructuration en monorepo avec packages réutilisables
|
||||
- [x] Outil CLI (`op`) pour le contrôle depuis le terminal
|
||||
- [ ] Édition collaborative
|
||||
- [ ] Système de plugins
|
||||
|
||||
|
|
|
|||
51
README.hi.md
51
README.hi.md
|
|
@ -80,6 +80,22 @@ Claude Code, Codex, Gemini, OpenCode, Kiro, या Copilot CLIs में वन
|
|||
|
||||
वेब ऐप + Electron के ज़रिए macOS, Windows और Linux पर नेटिव डेस्कटॉप। GitHub Releases से ऑटो-अपडेट। `.op` फ़ाइल एसोसिएशन — डबल-क्लिक से खोलें।
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें। `op design`, `op insert`, `op export` — बैच डिज़ाइन DSL, नोड मैनिपुलेशन, कोड एक्सपोर्ट। फ़ाइलों या stdin से पाइप करें। डेस्कटॉप ऐप या वेब सर्वर के साथ काम करता है।
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 मल्टी-प्लेटफ़ॉर्म कोड एक्सपोर्ट
|
||||
|
||||
एक `.op` फ़ाइल से React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native में एक्सपोर्ट करें। डिज़ाइन वेरिएबल CSS कस्टम प्रॉपर्टीज़ बन जाते हैं।
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
वैश्विक रूप से इंस्टॉल करें और अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # डेस्कटॉप ऐप लॉन्च करें
|
||||
op design @landing.txt # फ़ाइल से बैच डिज़ाइन
|
||||
op insert '{"type":"RECT"}' # एक नोड डालें
|
||||
op export react --out . # React + Tailwind में एक्सपोर्ट
|
||||
op import:figma design.fig # Figma फ़ाइल इम्पोर्ट करें
|
||||
cat design.dsl | op design - # stdin से पाइप करें
|
||||
```
|
||||
|
||||
तीन इनपुट विधियाँ समर्थित हैं: इनलाइन स्ट्रिंग, `@filepath` (फ़ाइल से पढ़ें), या `-` (stdin से पढ़ें)। डेस्कटॉप ऐप या वेब डेव सर्वर के साथ काम करता है। पूर्ण कमांड संदर्भ के लिए [CLI README](./apps/cli/README.md) देखें।
|
||||
|
||||
## विशेषताएँ
|
||||
|
||||
**कैनवास और ड्रॉइंग**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **स्टेट** | Zustand v5 |
|
||||
| **सर्वर** | Nitro |
|
||||
| **डेस्कटॉप** | Electron 35 |
|
||||
| **CLI** | `op` — टर्मिनल नियंत्रण, बैच डिज़ाइन DSL, कोड एक्सपोर्ट |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **रनटाइम** | Bun · Vite 7 |
|
||||
| **फ़ाइल फ़ॉर्मेट** | `.op` — JSON-आधारित, मानव-पठनीय, Git-फ्रेंडली |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — स्ट्रीमिंग चैट, जनरेशन, वैलिडेशन
|
||||
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot रैपर
|
||||
│ └── desktop/ Electron डेस्कटॉप ऐप
|
||||
│ ├── main.ts विंडो, Nitro फ़ोर्क, नेटिव मेनू, ऑटो-अपडेटर
|
||||
│ ├── ipc-handlers.ts नेटिव फ़ाइल डायलॉग, थीम सिंक, प्राथमिकताएँ IPC
|
||||
│ └── preload.ts IPC ब्रिज
|
||||
│ ├── desktop/ Electron डेस्कटॉप ऐप
|
||||
│ │ ├── main.ts विंडो, Nitro फ़ोर्क, नेटिव मेनू, ऑटो-अपडेटर
|
||||
│ │ ├── ipc-handlers.ts नेटिव फ़ाइल डायलॉग, थीम सिंक, प्राथमिकताएँ IPC
|
||||
│ │ └── preload.ts IPC ब्रिज
|
||||
│ └── cli/ CLI टूल — `op` कमांड
|
||||
│ ├── src/commands/ डिज़ाइन, दस्तावेज़, एक्सपोर्ट, इम्पोर्ट, नोड, पेज, वेरिएबल कमांड
|
||||
│ ├── connection.ts चालू ऐप से WebSocket कनेक्शन
|
||||
│ └── launcher.ts डेस्कटॉप ऐप या वेब सर्वर का स्वचालित पता लगाना और लॉन्च
|
||||
├── packages/
|
||||
│ ├── pen-types/ PenDocument मॉडल के लिए टाइप परिभाषाएँ
|
||||
│ ├── pen-core/ दस्तावेज़ ट्री ऑपरेशन, लेआउट इंजन, वेरिएबल
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # टाइप चेक
|
|||
bun run bump <version> # सभी package.json में वर्शन सिंक करें
|
||||
bun run electron:dev # Electron डेव
|
||||
bun run electron:build # Electron पैकेज
|
||||
bun run cli:dev # सोर्स से CLI चलाएँ
|
||||
bun run cli:compile # CLI को dist में कंपाइल करें
|
||||
```
|
||||
|
||||
## योगदान
|
||||
|
|
@ -305,6 +347,7 @@ bun run electron:build # Electron पैकेज
|
|||
- [x] बूलियन ऑपरेशन (यूनियन, सबट्रैक्ट, इंटरसेक्ट)
|
||||
- [x] मल्टी-मॉडल क्षमता प्रोफ़ाइल
|
||||
- [x] पुन: उपयोगी पैकेज के साथ मोनोरेपो पुनर्गठन
|
||||
- [x] CLI टूल (`op`) टर्मिनल नियंत्रण
|
||||
- [ ] सहयोगी संपादन
|
||||
- [ ] प्लगइन सिस्टम
|
||||
|
||||
|
|
|
|||
51
README.id.md
51
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.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Kontrol alat desain dari terminal Anda. `op design`, `op insert`, `op export` — batch design DSL, manipulasi node, ekspor kode. Pipe dari file atau stdin. Bekerja dengan aplikasi desktop atau web server.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Ekspor Kode Multi-Platform
|
||||
|
||||
Ekspor dari satu file `.op` ke React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Variabel desain menjadi CSS custom properties.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Instal secara global dan kontrol alat desain dari terminal Anda:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Jalankan aplikasi desktop
|
||||
op design @landing.txt # Desain batch dari file
|
||||
op insert '{"type":"RECT"}' # Sisipkan sebuah node
|
||||
op export react --out . # Ekspor ke React + Tailwind
|
||||
op import:figma design.fig # Impor file Figma
|
||||
cat design.dsl | op design - # Pipe dari stdin
|
||||
```
|
||||
|
||||
Mendukung tiga metode input: string inline, `@filepath` (baca dari file), atau `-` (baca dari stdin). Bekerja dengan aplikasi desktop atau web dev server. Lihat [CLI README](./apps/cli/README.md) untuk referensi perintah lengkap.
|
||||
|
||||
## Fitur
|
||||
|
||||
**Kanvas & Menggambar**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **State** | Zustand v5 |
|
||||
| **Server** | Nitro |
|
||||
| **Desktop** | Electron 35 |
|
||||
| **CLI** | `op` — kontrol terminal, batch design DSL, ekspor kode |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Runtime** | Bun · Vite 7 |
|
||||
| **Format file** | `.op` — berbasis JSON, mudah dibaca manusia, ramah Git |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — chat streaming, pembuatan, validasi
|
||||
│ │ └── utils/ Pembungkus Claude CLI, OpenCode, Codex, Copilot
|
||||
│ └── desktop/ Aplikasi desktop Electron
|
||||
│ ├── main.ts Jendela, fork Nitro, menu native, pembaruan otomatis
|
||||
│ ├── ipc-handlers.ts Dialog file native, sinkronisasi tema, preferensi IPC
|
||||
│ └── preload.ts Jembatan IPC
|
||||
│ ├── desktop/ Aplikasi desktop Electron
|
||||
│ │ ├── main.ts Jendela, fork Nitro, menu native, pembaruan otomatis
|
||||
│ │ ├── ipc-handlers.ts Dialog file native, sinkronisasi tema, preferensi IPC
|
||||
│ │ └── preload.ts Jembatan IPC
|
||||
│ └── cli/ Alat CLI — perintah `op`
|
||||
│ ├── src/commands/ Perintah design, document, export, import, node, page, variable
|
||||
│ ├── connection.ts Koneksi WebSocket ke aplikasi yang berjalan
|
||||
│ └── launcher.ts Deteksi otomatis dan jalankan aplikasi desktop atau web server
|
||||
├── packages/
|
||||
│ ├── pen-types/ Definisi tipe untuk model PenDocument
|
||||
│ ├── pen-core/ Operasi pohon dokumen, mesin tata letak, variabel
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Pemeriksaan tipe
|
|||
bun run bump <version> # Sinkronisasi versi di semua package.json
|
||||
bun run electron:dev # Pengembangan Electron
|
||||
bun run electron:build # Paket Electron
|
||||
bun run cli:dev # Jalankan CLI dari sumber
|
||||
bun run cli:compile # Kompilasi CLI ke dist
|
||||
```
|
||||
|
||||
## Berkontribusi
|
||||
|
|
@ -305,6 +347,7 @@ Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitekt
|
|||
- [x] Operasi boolean (gabung, kurangi, potong)
|
||||
- [x] Profil kemampuan multi-model
|
||||
- [x] Restrukturisasi monorepo dengan paket yang dapat digunakan ulang
|
||||
- [x] Alat CLI (`op`) kontrol terminal
|
||||
- [ ] Pengeditan kolaboratif
|
||||
- [ ] Sistem plugin
|
||||
|
||||
|
|
|
|||
51
README.ja.md
51
README.ja.md
|
|
@ -80,6 +80,22 @@ Claude Code、Codex、Gemini、OpenCode、Kiro、Copilot CLI にワンクリッ
|
|||
|
||||
Web アプリ + Electron による macOS・Windows・Linux ネイティブデスクトップ。GitHub Releases からの自動アップデート。`.op` ファイル関連付け — ダブルクリックで開く。
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
ターミナルからデザインツールを操作。`op design`、`op insert`、`op export` — バッチデザインDSL、ノード操作、コードエクスポート。ファイルやstdinからのパイプ入力に対応。デスクトップアプリまたはWebサーバーと連携。
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 マルチプラットフォームコードエクスポート
|
||||
|
||||
1つの`.op`ファイルからReact + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Nativeへエクスポート。デザイン変数はCSSカスタムプロパティに変換。
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS、HTML + CSS、CSS Variables
|
||||
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
グローバルインストールしてターミナルからデザインツールを操作:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # デスクトップアプリを起動
|
||||
op design @landing.txt # ファイルからバッチデザイン
|
||||
op insert '{"type":"RECT"}' # ノードを挿入
|
||||
op export react --out . # React + Tailwind にエクスポート
|
||||
op import:figma design.fig # Figma ファイルをインポート
|
||||
cat design.dsl | op design - # stdin からパイプ入力
|
||||
```
|
||||
|
||||
3つの入力方法に対応:インライン文字列、`@filepath`(ファイルから読み込み)、`-`(stdin から読み込み)。デスクトップアプリまたは Web 開発サーバーと連携。完全なコマンドリファレンスは [CLI README](./apps/cli/README.md) を参照。
|
||||
|
||||
## 機能
|
||||
|
||||
**キャンバスと描画**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **状態管理** | Zustand v5 |
|
||||
| **サーバー** | Nitro |
|
||||
| **デスクトップ** | Electron 35 |
|
||||
| **CLI** | `op` — ターミナル制御、バッチデザインDSL、コードエクスポート |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **ランタイム** | Bun · Vite 7 |
|
||||
| **ファイル形式** | `.op` — JSON ベース、人間が読みやすく、Git フレンドリー |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — ストリーミングチャット、生成、バリデーション
|
||||
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot ラッパー
|
||||
│ └── desktop/ Electron デスクトップアプリ
|
||||
│ ├── main.ts ウィンドウ、Nitro フォーク、ネイティブメニュー、自動アップデーター
|
||||
│ ├── ipc-handlers.ts ネイティブファイルダイアログ、テーマ同期、設定 IPC
|
||||
│ └── preload.ts IPC ブリッジ
|
||||
│ ├── desktop/ Electron デスクトップアプリ
|
||||
│ │ ├── main.ts ウィンドウ、Nitro フォーク、ネイティブメニュー、自動アップデーター
|
||||
│ │ ├── ipc-handlers.ts ネイティブファイルダイアログ、テーマ同期、設定 IPC
|
||||
│ │ └── preload.ts IPC ブリッジ
|
||||
│ └── cli/ CLIツール — `op` コマンド
|
||||
│ ├── src/commands/ デザイン、ドキュメント、エクスポート、インポート、ノード、ページ、変数コマンド
|
||||
│ ├── connection.ts 実行中アプリへのWebSocket接続
|
||||
│ └── launcher.ts デスクトップアプリまたはWebサーバーの自動検出・起動
|
||||
├── packages/
|
||||
│ ├── pen-types/ PenDocument モデルの型定義
|
||||
│ ├── pen-core/ ドキュメントツリー操作、レイアウトエンジン、変数
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # 型チェック
|
|||
bun run bump <version> # すべての package.json のバージョンを同期
|
||||
bun run electron:dev # Electron 開発モード
|
||||
bun run electron:build # Electron パッケージング
|
||||
bun run cli:dev # ソースから CLI を実行
|
||||
bun run cli:compile # CLI を dist にコンパイル
|
||||
```
|
||||
|
||||
## コントリビュート
|
||||
|
|
@ -305,6 +347,7 @@ bun run electron:build # Electron パッケージング
|
|||
- [x] ブーリアン演算(合体、型抜き、交差)
|
||||
- [x] マルチモデル能力プロファイル
|
||||
- [x] 再利用可能なパッケージによるモノレポ構成
|
||||
- [x] CLIツール(`op`)ターミナル制御
|
||||
- [ ] 共同編集
|
||||
- [ ] プラグインシステム
|
||||
|
||||
|
|
|
|||
51
README.ko.md
51
README.ko.md
|
|
@ -80,6 +80,22 @@ Claude Code, Codex, Gemini, OpenCode, Kiro 또는 Copilot CLI에 원클릭 설
|
|||
|
||||
웹 앱 + Electron을 통한 macOS, Windows, Linux 네이티브 데스크톱. GitHub Releases에서 자동 업데이트. `.op` 파일 연결 — 더블 클릭으로 열기.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
터미널에서 디자인 도구 제어. `op design`, `op insert`, `op export` — 배치 디자인 DSL, 노드 조작, 코드 내보내기. 파일이나 stdin에서 파이프 입력 지원. 데스크톱 앱 또는 웹 서버와 연동.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 멀티 플랫폼 코드 내보내기
|
||||
|
||||
하나의 `.op` 파일에서 React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native로 내보내기. 디자인 변수는 CSS 커스텀 프로퍼티로 변환.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
전역 설치 후 터미널에서 디자인 도구를 제어하세요:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # 데스크톱 앱 실행
|
||||
op design @landing.txt # 파일에서 배치 디자인
|
||||
op insert '{"type":"RECT"}' # 노드 삽입
|
||||
op export react --out . # React + Tailwind로 내보내기
|
||||
op import:figma design.fig # Figma 파일 가져오기
|
||||
cat design.dsl | op design - # stdin에서 파이프 입력
|
||||
```
|
||||
|
||||
세 가지 입력 방식을 지원합니다: 인라인 문자열, `@filepath` (파일에서 읽기), `-` (stdin에서 읽기). 데스크톱 앱 또는 웹 개발 서버와 연동됩니다. 전체 명령어 레퍼런스는 [CLI README](./apps/cli/README.md)를 참고하세요.
|
||||
|
||||
## 기능
|
||||
|
||||
**캔버스 & 드로잉**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **상태 관리** | Zustand v5 |
|
||||
| **서버** | Nitro |
|
||||
| **데스크톱** | Electron 35 |
|
||||
| **CLI** | `op` — 터미널 제어, 배치 디자인 DSL, 코드 내보내기 |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **런타임** | Bun · Vite 7 |
|
||||
| **파일 형식** | `.op` — JSON 기반, 사람이 읽을 수 있는, Git 친화적 |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — 스트리밍 채팅, 생성, 유효성 검사
|
||||
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot 래퍼
|
||||
│ └── desktop/ Electron 데스크톱 앱
|
||||
│ ├── main.ts 윈도우, Nitro 포크, 네이티브 메뉴, 자동 업데이터
|
||||
│ ├── ipc-handlers.ts 네이티브 파일 대화상자, 테마 동기화, 환경설정 IPC
|
||||
│ └── preload.ts IPC 브리지
|
||||
│ ├── desktop/ Electron 데스크톱 앱
|
||||
│ │ ├── main.ts 윈도우, Nitro 포크, 네이티브 메뉴, 자동 업데이터
|
||||
│ │ ├── ipc-handlers.ts 네이티브 파일 대화상자, 테마 동기화, 환경설정 IPC
|
||||
│ │ └── preload.ts IPC 브리지
|
||||
│ └── cli/ CLI 도구 — `op` 명령어
|
||||
│ ├── src/commands/ 디자인, 문서, 내보내기, 가져오기, 노드, 페이지, 변수 명령어
|
||||
│ ├── connection.ts 실행 중인 앱과의 WebSocket 연결
|
||||
│ └── launcher.ts 데스크톱 앱 또는 웹 서버 자동 감지 및 실행
|
||||
├── packages/
|
||||
│ ├── pen-types/ PenDocument 모델 타입 정의
|
||||
│ ├── pen-core/ 문서 트리 연산, 레이아웃 엔진, 변수
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # 타입 검사
|
|||
bun run bump <version> # 모든 package.json에 버전 동기화
|
||||
bun run electron:dev # Electron 개발 모드
|
||||
bun run electron:build # Electron 패키징
|
||||
bun run cli:dev # 소스에서 CLI 실행
|
||||
bun run cli:compile # CLI를 dist로 컴파일
|
||||
```
|
||||
|
||||
## 기여하기
|
||||
|
|
@ -305,6 +347,7 @@ bun run electron:build # Electron 패키징
|
|||
- [x] 불리언 연산 (합치기, 빼기, 교차)
|
||||
- [x] 멀티 모델 역량 프로파일
|
||||
- [x] 재사용 가능한 패키지를 포함한 모노레포 구조 변경
|
||||
- [x] CLI 도구 (`op`) 터미널 제어
|
||||
- [ ] 공동 편집
|
||||
- [ ] 플러그인 시스템
|
||||
|
||||
|
|
|
|||
51
README.md
51
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.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Control the design tool from your terminal. `op design`, `op insert`, `op export` — batch design DSL, node manipulation, code export. Pipe in from files or stdin. Works with desktop app or web server.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Multi-Platform Code Export
|
||||
|
||||
Export to React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native — all from one `.op` file. Design variables become CSS custom properties.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Install globally and control the design tool from your terminal:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Launch desktop app
|
||||
op design @landing.txt # Batch design from file
|
||||
op insert '{"type":"RECT"}' # Insert a node
|
||||
op export react --out . # Export to React + Tailwind
|
||||
op import:figma design.fig # Import Figma file
|
||||
cat design.dsl | op design - # Pipe from stdin
|
||||
```
|
||||
|
||||
Supports three input methods: inline string, `@filepath` (read from file), or `-` (read from stdin). Works with desktop app or web dev server. See [CLI README](./apps/cli/README.md) for full command reference.
|
||||
|
||||
## Features
|
||||
|
||||
**Canvas & Drawing**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **State** | Zustand v5 |
|
||||
| **Server** | Nitro |
|
||||
| **Desktop** | Electron 35 |
|
||||
| **CLI** | `op` — terminal control, batch design DSL, code export |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Runtime** | Bun · Vite 7 |
|
||||
| **File format** | `.op` — JSON-based, human-readable, Git-friendly |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
|
||||
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
|
||||
│ └── desktop/ Electron desktop app
|
||||
│ ├── main.ts Window, Nitro fork, native menu, auto-updater
|
||||
│ ├── ipc-handlers.ts Native file dialogs, theme sync, prefs IPC
|
||||
│ └── preload.ts IPC bridge
|
||||
│ ├── desktop/ Electron desktop app
|
||||
│ │ ├── main.ts Window, Nitro fork, native menu, auto-updater
|
||||
│ │ ├── ipc-handlers.ts Native file dialogs, theme sync, prefs IPC
|
||||
│ │ └── preload.ts IPC bridge
|
||||
│ └── cli/ CLI tool — `op` command
|
||||
│ ├── src/commands/ Design, document, export, import, node, page, variable commands
|
||||
│ ├── connection.ts WebSocket connection to running app
|
||||
│ └── launcher.ts Auto-detect and launch desktop app or web server
|
||||
├── packages/
|
||||
│ ├── pen-types/ Type definitions for PenDocument model
|
||||
│ ├── pen-core/ Document tree ops, layout engine, variables
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Type check
|
|||
bun run bump <version> # Sync version across all package.json
|
||||
bun run electron:dev # Electron dev
|
||||
bun run electron:build # Electron package
|
||||
bun run cli:dev # Run CLI from source
|
||||
bun run cli:compile # Compile CLI to dist
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
|
@ -305,6 +347,7 @@ Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details
|
|||
- [x] Boolean operations (union, subtract, intersect)
|
||||
- [x] Multi-model capability profiles
|
||||
- [x] Monorepo restructure with reusable packages
|
||||
- [x] CLI tool (`op`) for terminal control
|
||||
- [ ] Collaborative editing
|
||||
- [ ] Plugin system
|
||||
|
||||
|
|
|
|||
51
README.pt.md
51
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.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Controle a ferramenta de design pelo terminal. `op design`, `op insert`, `op export` — DSL de design em lote, manipulação de nós, exportação de código. Entrada por pipe de arquivos ou stdin. Funciona com o app desktop ou servidor web.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Exportação de Código Multiplataforma
|
||||
|
||||
Exporte de um único arquivo `.op` para React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Variáveis de design se tornam propriedades CSS customizadas.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Instale globalmente e controle a ferramenta de design pelo terminal:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Iniciar app desktop
|
||||
op design @landing.txt # Design em lote a partir de arquivo
|
||||
op insert '{"type":"RECT"}' # Inserir um nó
|
||||
op export react --out . # Exportar para React + Tailwind
|
||||
op import:figma design.fig # Importar arquivo Figma
|
||||
cat design.dsl | op design - # Entrada por pipe via stdin
|
||||
```
|
||||
|
||||
Suporta três métodos de entrada: string inline, `@filepath` (ler de arquivo) ou `-` (ler de stdin). Funciona com o app desktop ou servidor web de desenvolvimento. Veja o [CLI README](./apps/cli/README.md) para referência completa de comandos.
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
**Canvas e Desenho**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **Estado** | Zustand v5 |
|
||||
| **Servidor** | Nitro |
|
||||
| **Desktop** | Electron 35 |
|
||||
| **CLI** | `op` — controle pelo terminal, DSL de design em lote, exportação de código |
|
||||
| **IA** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Runtime** | Bun · Vite 7 |
|
||||
| **Formato de arquivo** | `.op` — baseado em JSON, legível por humanos, compatível com Git |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ API Nitro — chat em streaming, geração, validação
|
||||
│ │ └── utils/ Wrappers de cliente Claude CLI, OpenCode, Codex, Copilot
|
||||
│ └── desktop/ Aplicativo desktop Electron
|
||||
│ ├── main.ts Janela, fork do Nitro, menu nativo, atualizador automático
|
||||
│ ├── ipc-handlers.ts Diálogos de arquivo nativos, sincronização de tema, preferências IPC
|
||||
│ └── preload.ts Ponte IPC
|
||||
│ ├── desktop/ Aplicativo desktop Electron
|
||||
│ │ ├── main.ts Janela, fork do Nitro, menu nativo, atualizador automático
|
||||
│ │ ├── ipc-handlers.ts Diálogos de arquivo nativos, sincronização de tema, preferências IPC
|
||||
│ │ └── preload.ts Ponte IPC
|
||||
│ └── cli/ Ferramenta CLI — comando `op`
|
||||
│ ├── src/commands/ Comandos de design, documento, exportação, importação, nó, página, variável
|
||||
│ ├── connection.ts Conexão WebSocket com o app em execução
|
||||
│ └── launcher.ts Detecção automática e inicialização do app desktop ou servidor web
|
||||
├── packages/
|
||||
│ ├── pen-types/ Definições de tipos para o modelo PenDocument
|
||||
│ ├── pen-core/ Operações de árvore de documento, motor de layout, variáveis
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Verificação de tipos
|
|||
bun run bump <version> # Sincronizar versão em todos os package.json
|
||||
bun run electron:dev # Desenvolvimento com Electron
|
||||
bun run electron:build # Empacotamento do Electron
|
||||
bun run cli:dev # Executar CLI a partir do código-fonte
|
||||
bun run cli:compile # Compilar CLI para dist
|
||||
```
|
||||
|
||||
## Contribuindo
|
||||
|
|
@ -305,6 +347,7 @@ Contribuições são bem-vindas! Consulte o [CLAUDE.md](./CLAUDE.md) para detalh
|
|||
- [x] Operações booleanas (união, subtração, interseção)
|
||||
- [x] Perfis de capacidade multi-modelo
|
||||
- [x] Reestruturação em monorepo com pacotes reutilizáveis
|
||||
- [x] Ferramenta CLI (`op`) para controle pelo terminal
|
||||
- [ ] Edição colaborativa
|
||||
- [ ] Sistema de plugins
|
||||
|
||||
|
|
|
|||
51
README.ru.md
51
README.ru.md
|
|
@ -80,6 +80,22 @@
|
|||
|
||||
Веб-приложение + нативный десктоп на macOS, Windows и Linux через Electron. Автообновление из GitHub Releases. Ассоциация файлов `.op` — двойной клик для открытия.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Управляйте инструментом дизайна из терминала. `op design`, `op insert`, `op export` — пакетный DSL дизайна, манипуляция узлами, экспорт кода. Ввод через pipe из файлов или stdin. Работает с десктопным приложением или веб-сервером.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Мультиплатформенный экспорт кода
|
||||
|
||||
Экспорт из одного файла `.op` в React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Переменные дизайна превращаются в пользовательские свойства CSS.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Установите глобально и управляйте инструментом дизайна из терминала:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Запустить десктопное приложение
|
||||
op design @landing.txt # Пакетный дизайн из файла
|
||||
op insert '{"type":"RECT"}' # Вставить узел
|
||||
op export react --out . # Экспорт в React + Tailwind
|
||||
op import:figma design.fig # Импортировать файл Figma
|
||||
cat design.dsl | op design - # Передача через stdin
|
||||
```
|
||||
|
||||
Поддерживает три метода ввода: строка, `@filepath` (чтение из файла) или `-` (чтение из stdin). Работает с десктопным приложением или веб-сервером разработки. Подробнее в [CLI README](./apps/cli/README.md).
|
||||
|
||||
## Возможности
|
||||
|
||||
**Холст и рисование**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **Состояние** | Zustand v5 |
|
||||
| **Сервер** | Nitro |
|
||||
| **Десктоп** | Electron 35 |
|
||||
| **CLI** | `op` — управление из терминала, пакетный DSL дизайна, экспорт кода |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Среда выполнения** | Bun · Vite 7 |
|
||||
| **Формат файла** | `.op` — на основе JSON, удобочитаемый, дружественный к Git |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — стриминговый чат, генерация, валидация
|
||||
│ │ └── utils/ Обёртки клиентов Claude CLI, OpenCode, Codex, Copilot
|
||||
│ └── desktop/ Десктопное приложение Electron
|
||||
│ ├── main.ts Окно, форк Nitro, нативное меню, автообновление
|
||||
│ ├── ipc-handlers.ts Нативные файловые диалоги, синхронизация темы, настройки IPC
|
||||
│ └── preload.ts IPC-мост
|
||||
│ ├── desktop/ Десктопное приложение Electron
|
||||
│ │ ├── main.ts Окно, форк Nitro, нативное меню, автообновление
|
||||
│ │ ├── ipc-handlers.ts Нативные файловые диалоги, синхронизация темы, настройки IPC
|
||||
│ │ └── preload.ts IPC-мост
|
||||
│ └── cli/ CLI-инструмент — команда `op`
|
||||
│ ├── src/commands/ Команды: дизайн, документ, экспорт, импорт, узлы, страницы, переменные
|
||||
│ ├── connection.ts WebSocket-соединение с запущенным приложением
|
||||
│ └── launcher.ts Автоопределение и запуск десктопного приложения или веб-сервера
|
||||
├── packages/
|
||||
│ ├── pen-types/ Определения типов для модели PenDocument
|
||||
│ ├── pen-core/ Операции с деревом документа, движок раскладки, переменные
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Проверка типов
|
|||
bun run bump <version> # Синхронизация версий во всех package.json
|
||||
bun run electron:dev # Разработка Electron
|
||||
bun run electron:build # Упаковка Electron
|
||||
bun run cli:dev # Запуск CLI из исходников
|
||||
bun run cli:compile # Компиляция CLI в dist
|
||||
```
|
||||
|
||||
## Участие в разработке
|
||||
|
|
@ -305,6 +347,7 @@ bun run electron:build # Упаковка Electron
|
|||
- [x] Булевы операции (объединение, вычитание, пересечение)
|
||||
- [x] Мультимодельные профили возможностей
|
||||
- [x] Реструктуризация в монорепозиторий с переиспользуемыми пакетами
|
||||
- [x] CLI-инструмент (`op`) для управления из терминала
|
||||
- [ ] Совместное редактирование
|
||||
- [ ] Система плагинов
|
||||
|
||||
|
|
|
|||
51
README.th.md
51
README.th.md
|
|
@ -80,6 +80,22 @@ Orchestrator แบ่งหน้าที่ซับซ้อนออกเ
|
|||
|
||||
เว็บแอป + เดสก์ท็อปแบบ native บน macOS, Windows และ Linux ผ่าน Electron อัปเดตอัตโนมัติจาก GitHub Releases เชื่อมโยงไฟล์ `.op` — ดับเบิลคลิกเพื่อเปิด
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
ควบคุมเครื่องมือออกแบบจาก terminal ของคุณ `op design`, `op insert`, `op export` — batch design DSL, จัดการ node, ส่งออกโค้ด Pipe จากไฟล์หรือ stdin ทำงานร่วมกับแอปเดสก์ท็อปหรือ web server
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 ส่งออกโค้ดหลายแพลตฟอร์ม
|
||||
|
||||
ส่งออกจากไฟล์ `.op` ไฟล์เดียวไปยัง React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native Design variables กลายเป็น CSS custom properties
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
ติดตั้งแบบ global และควบคุมเครื่องมือออกแบบจาก terminal ของคุณ:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # เปิดแอปเดสก์ท็อป
|
||||
op design @landing.txt # ออกแบบแบบ batch จากไฟล์
|
||||
op insert '{"type":"RECT"}' # แทรก node
|
||||
op export react --out . # ส่งออกเป็น React + Tailwind
|
||||
op import:figma design.fig # นำเข้าไฟล์ Figma
|
||||
cat design.dsl | op design - # Pipe จาก stdin
|
||||
```
|
||||
|
||||
รองรับ 3 วิธีการป้อนข้อมูล: สตริงแบบ inline, `@filepath` (อ่านจากไฟล์) หรือ `-` (อ่านจาก stdin) ทำงานร่วมกับแอปเดสก์ท็อปหรือ web dev server ดู [CLI README](./apps/cli/README.md) สำหรับคู่มือคำสั่งฉบับเต็ม
|
||||
|
||||
## ฟีเจอร์
|
||||
|
||||
**Canvas และการวาด**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **State** | Zustand v5 |
|
||||
| **Server** | Nitro |
|
||||
| **Desktop** | Electron 35 |
|
||||
| **CLI** | `op` — ควบคุมจาก terminal, batch design DSL, ส่งออกโค้ด |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Runtime** | Bun · Vite 7 |
|
||||
| **รูปแบบไฟล์** | `.op` — ใช้ JSON, อ่านได้โดยมนุษย์, Git-friendly |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
|
||||
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
|
||||
│ └── desktop/ Electron desktop app
|
||||
│ ├── main.ts Window, Nitro fork, native menu, auto-updater
|
||||
│ ├── ipc-handlers.ts ไดอะล็อกไฟล์เนทีฟ, ซิงค์ธีม, การตั้งค่า IPC
|
||||
│ └── preload.ts IPC bridge
|
||||
│ ├── desktop/ Electron desktop app
|
||||
│ │ ├── main.ts Window, Nitro fork, native menu, auto-updater
|
||||
│ │ ├── ipc-handlers.ts ไดอะล็อกไฟล์เนทีฟ, ซิงค์ธีม, การตั้งค่า IPC
|
||||
│ │ └── preload.ts IPC bridge
|
||||
│ └── cli/ เครื่องมือ CLI — คำสั่ง `op`
|
||||
│ ├── src/commands/ คำสั่ง design, document, export, import, node, page, variable
|
||||
│ ├── connection.ts การเชื่อมต่อ WebSocket ไปยังแอปที่กำลังทำงาน
|
||||
│ └── launcher.ts ตรวจจับและเปิดแอปเดสก์ท็อปหรือ web server อัตโนมัติ
|
||||
├── packages/
|
||||
│ ├── pen-types/ Type definitions สำหรับ PenDocument model
|
||||
│ ├── pen-core/ Document tree ops, layout engine, variables
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # ตรวจสอบ type
|
|||
bun run bump <version> # Sync version ในทุก package.json
|
||||
bun run electron:dev # Electron dev
|
||||
bun run electron:build # Electron package
|
||||
bun run cli:dev # รัน CLI จาก source
|
||||
bun run cli:compile # คอมไพล์ CLI ไปยัง dist
|
||||
```
|
||||
|
||||
## การมีส่วนร่วม
|
||||
|
|
@ -305,6 +347,7 @@ bun run electron:build # Electron package
|
|||
- [x] Boolean operations (union, subtract, intersect)
|
||||
- [x] โปรไฟล์ความสามารถหลายโมเดล
|
||||
- [x] ปรับโครงสร้างเป็น monorepo พร้อม package ที่นำกลับมาใช้ใหม่ได้
|
||||
- [x] เครื่องมือ CLI (`op`) ควบคุมจาก terminal
|
||||
- [ ] การแก้ไขร่วมกัน
|
||||
- [ ] ระบบปลั๊กอิน
|
||||
|
||||
|
|
|
|||
51
README.tr.md
51
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.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Tasarım aracını terminalinizden kontrol edin. `op design`, `op insert`, `op export` — toplu tasarım DSL, düğüm manipülasyonu, kod dışa aktarımı. Dosyalardan veya stdin'den pipe ile besleyin. Masaüstü uygulama veya web sunucusuyla çalışır.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Çok Platformlu Kod Dışa Aktarımı
|
||||
|
||||
Tek bir `.op` dosyasından React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native'e dışa aktarın. Tasarım değişkenleri CSS özel özelliklerine dönüşür.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Global olarak yükleyin ve tasarım aracını terminalinizden kontrol edin:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Masaüstü uygulamayı başlat
|
||||
op design @landing.txt # Dosyadan toplu tasarım
|
||||
op insert '{"type":"RECT"}' # Bir düğüm ekle
|
||||
op export react --out . # React + Tailwind'e dışa aktar
|
||||
op import:figma design.fig # Figma dosyasını içe aktar
|
||||
cat design.dsl | op design - # stdin'den pipe ile besle
|
||||
```
|
||||
|
||||
Üç giriş yöntemini destekler: satır içi metin, `@filepath` (dosyadan oku) veya `-` (stdin'den oku). Masaüstü uygulama veya web geliştirme sunucusuyla çalışır. Tam komut referansı için [CLI README](./apps/cli/README.md) dosyasına bakın.
|
||||
|
||||
## Özellikler
|
||||
|
||||
**Kanvas ve Çizim**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **Durum Yönetimi** | Zustand v5 |
|
||||
| **Sunucu** | Nitro |
|
||||
| **Masaüstü** | Electron 35 |
|
||||
| **CLI** | `op` — terminal kontrolü, toplu tasarım DSL, kod dışa aktarımı |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Çalışma Ortamı** | Bun · Vite 7 |
|
||||
| **Dosya Formatı** | `.op` — JSON tabanlı, insan tarafından okunabilir, Git dostu |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — akış sohbet, üretim, doğrulama
|
||||
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot sarmalayıcıları
|
||||
│ └── desktop/ Electron masaüstü uygulaması
|
||||
│ ├── main.ts Pencere, Nitro çatallanması, yerel menü, otomatik güncelleyici
|
||||
│ ├── ipc-handlers.ts Yerel dosya diyalogları, tema senkronizasyonu, tercihler IPC
|
||||
│ └── preload.ts IPC köprüsü
|
||||
│ ├── desktop/ Electron masaüstü uygulaması
|
||||
│ │ ├── main.ts Pencere, Nitro çatallanması, yerel menü, otomatik güncelleyici
|
||||
│ │ ├── ipc-handlers.ts Yerel dosya diyalogları, tema senkronizasyonu, tercihler IPC
|
||||
│ │ └── preload.ts IPC köprüsü
|
||||
│ └── cli/ CLI aracı — `op` komutu
|
||||
│ ├── src/commands/ Tasarım, belge, dışa aktarma, içe aktarma, düğüm, sayfa, değişken komutları
|
||||
│ ├── connection.ts Çalışan uygulamaya WebSocket bağlantısı
|
||||
│ └── launcher.ts Masaüstü uygulamayı veya web sunucusunu otomatik algıla ve başlat
|
||||
├── packages/
|
||||
│ ├── pen-types/ PenDocument modeli için tür tanımları
|
||||
│ ├── pen-core/ Belge ağacı işlemleri, düzen motoru, değişkenler
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Tür denetimi
|
|||
bun run bump <version> # Tüm package.json dosyalarında sürümü eşitle
|
||||
bun run electron:dev # Electron geliştirme modu
|
||||
bun run electron:build # Electron paketleme
|
||||
bun run cli:dev # CLI'yi kaynaktan çalıştır
|
||||
bun run cli:compile # CLI'yi dist'e derle
|
||||
```
|
||||
|
||||
## Katkıda Bulunma
|
||||
|
|
@ -305,6 +347,7 @@ Katkılarınızı bekliyoruz! Mimari ayrıntılar ve kod stili için [CLAUDE.md]
|
|||
- [x] Boolean işlemler (birleştirme, çıkarma, kesişim)
|
||||
- [x] Çoklu model yetenek profilleri
|
||||
- [x] Yeniden kullanılabilir paketlerle monorepo yapılandırması
|
||||
- [x] CLI aracı (`op`) terminal kontrolü
|
||||
- [ ] Ortak düzenleme
|
||||
- [ ] Eklenti sistemi
|
||||
|
||||
|
|
|
|||
51
README.vi.md
51
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ở.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
Điều khiển công cụ thiết kế từ terminal của bạn. `op design`, `op insert`, `op export` — batch design DSL, thao tác node, xuất mã. Pipe từ tệp hoặc stdin. Hoạt động với ứng dụng desktop hoặc web server.
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 Xuất mã Đa nền tảng
|
||||
|
||||
Xuất từ một tệp `.op` duy nhất sang React + Tailwind, HTML + CSS, Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native. Biến thiết kế trở thành thuộc tính tùy chỉnh CSS.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS, HTML + CSS, CSS Variables
|
||||
- Vue, Svelte, Flutter, SwiftUI, Jetpack Compose, React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
Cài đặt toàn cục và điều khiển công cụ thiết kế từ terminal của bạn:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # Khởi chạy ứng dụng desktop
|
||||
op design @landing.txt # Thiết kế hàng loạt từ tệp
|
||||
op insert '{"type":"RECT"}' # Chèn một node
|
||||
op export react --out . # Xuất sang React + Tailwind
|
||||
op import:figma design.fig # Nhập tệp Figma
|
||||
cat design.dsl | op design - # Pipe từ stdin
|
||||
```
|
||||
|
||||
Hỗ trợ ba phương thức nhập liệu: chuỗi inline, `@filepath` (đọc từ tệp), hoặc `-` (đọc từ stdin). Hoạt động với ứng dụng desktop hoặc web dev server. Xem [CLI README](./apps/cli/README.md) để biết đầy đủ các lệnh.
|
||||
|
||||
## Tính năng
|
||||
|
||||
**Canvas và Vẽ**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **Trạng thái** | Zustand v5 |
|
||||
| **Máy chủ** | Nitro |
|
||||
| **Desktop** | Electron 35 |
|
||||
| **CLI** | `op` — điều khiển từ terminal, batch design DSL, xuất mã |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **Runtime** | Bun · Vite 7 |
|
||||
| **Định dạng tệp** | `.op` — dựa trên JSON, dễ đọc, thân thiện với Git |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
|
||||
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
|
||||
│ └── desktop/ Ứng dụng desktop Electron
|
||||
│ ├── main.ts Cửa sổ, Nitro fork, menu gốc, auto-updater
|
||||
│ ├── ipc-handlers.ts Hộp thoại file gốc, đồng bộ theme, tùy chọn IPC
|
||||
│ └── preload.ts IPC bridge
|
||||
│ ├── desktop/ Ứng dụng desktop Electron
|
||||
│ │ ├── main.ts Cửa sổ, Nitro fork, menu gốc, auto-updater
|
||||
│ │ ├── ipc-handlers.ts Hộp thoại file gốc, đồng bộ theme, tùy chọn IPC
|
||||
│ │ └── preload.ts IPC bridge
|
||||
│ └── cli/ Công cụ CLI — lệnh `op`
|
||||
│ ├── src/commands/ Lệnh design, document, export, import, node, page, variable
|
||||
│ ├── connection.ts Kết nối WebSocket đến ứng dụng đang chạy
|
||||
│ └── launcher.ts Tự động phát hiện và khởi chạy ứng dụng desktop hoặc web server
|
||||
├── packages/
|
||||
│ ├── pen-types/ Định nghĩa kiểu cho mô hình PenDocument
|
||||
│ ├── pen-core/ Thao tác cây tài liệu, layout engine, biến
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # Kiểm tra kiểu
|
|||
bun run bump <version> # Đồng bộ phiên bản trên tất cả package.json
|
||||
bun run electron:dev # Electron dev
|
||||
bun run electron:build # Đóng gói Electron
|
||||
bun run cli:dev # Chạy CLI từ mã nguồn
|
||||
bun run cli:compile # Biên dịch CLI sang dist
|
||||
```
|
||||
|
||||
## Đóng góp
|
||||
|
|
@ -305,6 +347,7 @@ Chào mừng đóng góp! Xem [CLAUDE.md](./CLAUDE.md) để biết chi tiết v
|
|||
- [x] Phép toán Boolean (hợp nhất, trừ, giao)
|
||||
- [x] Hồ sơ năng lực đa mô hình
|
||||
- [x] Tái cấu trúc monorepo với các gói tái sử dụng
|
||||
- [x] Công cụ CLI (`op`) điều khiển từ terminal
|
||||
- [ ] Chỉnh sửa cộng tác
|
||||
- [ ] Hệ thống plugin
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,22 @@
|
|||
|
||||
Web 應用程式 + 透過 Electron 在 macOS、Windows 和 Linux 上原生執行。從 GitHub Releases 自動更新。`.op` 檔案關聯 — 雙擊即可開啟。
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
從終端機控制設計工具。`op design`、`op insert`、`op export` — 批次設計 DSL、節點操作、程式碼匯出。支援從檔案或 stdin 管道輸入。可搭配桌面應用程式或 Web 伺服器使用。
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 多平台程式碼匯出
|
||||
|
||||
從單個 `.op` 檔案匯出至 React + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native。設計變數自動轉換為 CSS 自訂屬性。
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS、HTML + CSS、CSS Variables
|
||||
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
全域安裝後即可從終端機控制設計工具:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # 啟動桌面應用程式
|
||||
op design @landing.txt # 從檔案批次設計
|
||||
op insert '{"type":"RECT"}' # 插入節點
|
||||
op export react --out . # 匯出為 React + Tailwind
|
||||
op import:figma design.fig # 匯入 Figma 檔案
|
||||
cat design.dsl | op design - # 從 stdin 管道輸入
|
||||
```
|
||||
|
||||
支援三種輸入方式:內嵌字串、`@filepath`(從檔案讀取)、`-`(從 stdin 讀取)。可搭配桌面應用程式或 Web 開發伺服器使用。完整命令參考請查閱 [CLI README](./apps/cli/README.md)。
|
||||
|
||||
## 功能特色
|
||||
|
||||
**畫布與繪圖**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **狀態管理** | Zustand v5 |
|
||||
| **伺服器** | Nitro |
|
||||
| **桌面端** | Electron 35 |
|
||||
| **CLI** | `op` — 終端機控制、批次設計 DSL、程式碼匯出 |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **執行環境** | Bun · Vite 7 |
|
||||
| **檔案格式** | `.op` — 基於 JSON,人類可讀,對 Git 友好 |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — 串流聊天、生成、驗證
|
||||
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot 客戶端封裝
|
||||
│ └── desktop/ Electron 桌面應用程式
|
||||
│ ├── main.ts 視窗、Nitro 子處理序、原生選單、自動更新
|
||||
│ ├── ipc-handlers.ts 原生檔案對話框、主題同步、偏好設定 IPC
|
||||
│ └── preload.ts IPC 橋接
|
||||
│ ├── desktop/ Electron 桌面應用程式
|
||||
│ │ ├── main.ts 視窗、Nitro 子處理序、原生選單、自動更新
|
||||
│ │ ├── ipc-handlers.ts 原生檔案對話框、主題同步、偏好設定 IPC
|
||||
│ │ └── preload.ts IPC 橋接
|
||||
│ └── cli/ CLI 工具 — `op` 命令
|
||||
│ ├── src/commands/ 設計、文件、匯出、匯入、節點、頁面、變數命令
|
||||
│ ├── connection.ts 與執行中應用程式的 WebSocket 連線
|
||||
│ └── launcher.ts 自動偵測並啟動桌面應用程式或 Web 伺服器
|
||||
├── packages/
|
||||
│ ├── pen-types/ PenDocument 模型型別定義
|
||||
│ ├── pen-core/ 文件樹操作、版面配置引擎、變數
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # 型別檢查
|
|||
bun run bump <version> # 在所有 package.json 間同步版本號
|
||||
bun run electron:dev # Electron 開發模式
|
||||
bun run electron:build # Electron 封裝
|
||||
bun run cli:dev # 從原始碼執行 CLI
|
||||
bun run cli:compile # 編譯 CLI 到 dist
|
||||
```
|
||||
|
||||
## 參與貢獻
|
||||
|
|
@ -305,6 +347,7 @@ bun run electron:build # Electron 封裝
|
|||
- [x] 布林運算(聯集、減去、交集)
|
||||
- [x] 多模型能力設定檔
|
||||
- [x] Monorepo 重構,支援可重複使用套件
|
||||
- [x] CLI 工具(`op`)終端控制
|
||||
- [ ] 協同編輯
|
||||
- [ ] 外掛程式系統
|
||||
|
||||
|
|
|
|||
51
README.zh.md
51
README.zh.md
|
|
@ -80,6 +80,22 @@
|
|||
|
||||
Web 应用 + 通过 Electron 支持 macOS、Windows 和 Linux 原生桌面端。从 GitHub Releases 自动更新。`.op` 文件关联 — 双击即可打开。
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### ⌨️ CLI — `op`
|
||||
|
||||
从终端控制设计工具。`op design`、`op insert`、`op export` — 批量设计 DSL、节点操作、代码导出。支持从文件或 stdin 管道输入。可搭配桌面应用或 Web 服务器使用。
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### 🎯 多平台代码导出
|
||||
|
||||
从单个 `.op` 文件导出到 React + Tailwind、HTML + CSS、Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native。设计变量自动转换为 CSS 自定义属性。
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -184,6 +200,25 @@ docker build --target full -t openpencil-full .
|
|||
- React + Tailwind CSS、HTML + CSS、CSS Variables
|
||||
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
|
||||
|
||||
## CLI — `op`
|
||||
|
||||
全局安装后即可从终端控制设计工具:
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
```bash
|
||||
op start # 启动桌面应用
|
||||
op design @landing.txt # 从文件批量设计
|
||||
op insert '{"type":"RECT"}' # 插入节点
|
||||
op export react --out . # 导出为 React + Tailwind
|
||||
op import:figma design.fig # 导入 Figma 文件
|
||||
cat design.dsl | op design - # 从 stdin 管道输入
|
||||
```
|
||||
|
||||
支持三种输入方式:内联字符串、`@filepath`(从文件读取)、`-`(从 stdin 读取)。可搭配桌面应用或 Web 开发服务器使用。完整命令参考请查阅 [CLI README](./apps/cli/README.md)。
|
||||
|
||||
## 功能特性
|
||||
|
||||
**画布与绘图**
|
||||
|
|
@ -218,6 +253,7 @@ docker build --target full -t openpencil-full .
|
|||
| **状态管理** | Zustand v5 |
|
||||
| **服务器** | Nitro |
|
||||
| **桌面端** | Electron 35 |
|
||||
| **CLI** | `op` — 终端控制、批量设计 DSL、代码导出 |
|
||||
| **AI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||
| **运行时** | Bun · Vite 7 |
|
||||
| **文件格式** | `.op` — 基于 JSON,人类可读,对 Git 友好 |
|
||||
|
|
@ -239,10 +275,14 @@ openpencil/
|
|||
│ │ └── server/
|
||||
│ │ ├── api/ai/ Nitro API — 流式聊天、生成、验证
|
||||
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot 客户端封装
|
||||
│ └── desktop/ Electron 桌面应用
|
||||
│ ├── main.ts 窗口、Nitro 子进程、原生菜单、自动更新
|
||||
│ ├── ipc-handlers.ts 原生文件对话框、主题同步、偏好设置 IPC
|
||||
│ └── preload.ts IPC 桥接
|
||||
│ ├── desktop/ Electron 桌面应用
|
||||
│ │ ├── main.ts 窗口、Nitro 子进程、原生菜单、自动更新
|
||||
│ │ ├── ipc-handlers.ts 原生文件对话框、主题同步、偏好设置 IPC
|
||||
│ │ └── preload.ts IPC 桥接
|
||||
│ └── cli/ CLI 工具 — `op` 命令
|
||||
│ ├── src/commands/ 设计、文档、导出、导入、节点、页面、变量命令
|
||||
│ ├── connection.ts 与运行中应用的 WebSocket 连接
|
||||
│ └── launcher.ts 自动检测并启动桌面应用或 Web 服务器
|
||||
├── packages/
|
||||
│ ├── pen-types/ PenDocument 模型类型定义
|
||||
│ ├── pen-core/ 文档树操作、布局引擎、变量
|
||||
|
|
@ -281,6 +321,8 @@ npx tsc --noEmit # 类型检查
|
|||
bun run bump <version> # 同步所有 package.json 的版本号
|
||||
bun run electron:dev # Electron 开发模式
|
||||
bun run electron:build # Electron 打包
|
||||
bun run cli:dev # 从源码运行 CLI
|
||||
bun run cli:compile # 编译 CLI 到 dist
|
||||
```
|
||||
|
||||
## 参与贡献
|
||||
|
|
@ -305,6 +347,7 @@ bun run electron:build # Electron 打包
|
|||
- [x] 布尔运算(合并、减去、相交)
|
||||
- [x] 多模型能力配置
|
||||
- [x] Monorepo 重构与可复用包
|
||||
- [x] CLI 工具(`op`)终端控制
|
||||
- [ ] 协同编辑
|
||||
- [ ] 插件系统
|
||||
|
||||
|
|
|
|||
1
apps/cli/.gitignore
vendored
Normal file
1
apps/cli/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
dist/
|
||||
41
apps/cli/CLAUDE.md
Normal file
41
apps/cli/CLAUDE.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# apps/cli/ — OpenPencil CLI
|
||||
|
||||
The `op` command-line tool controls the OpenPencil desktop app or web server from the terminal.
|
||||
|
||||
## Structure
|
||||
|
||||
```text
|
||||
apps/cli/
|
||||
├── src/
|
||||
│ ├── index.ts Entry point — arg parsing, command dispatch, help text
|
||||
│ ├── connection.ts WebSocket connection to running app instance
|
||||
│ ├── launcher.ts Auto-detect and launch desktop app or web dev server
|
||||
│ ├── output.ts JSON output formatting (--pretty support)
|
||||
│ └── commands/
|
||||
│ ├── app.ts start, stop, status
|
||||
│ ├── design.ts design, design:skeleton, design:content, design:refine
|
||||
│ ├── document.ts open, save, get, selection
|
||||
│ ├── export.ts export (react, html, vue, svelte, flutter, swiftui, compose, rn, css)
|
||||
│ ├── import.ts import:svg, import:figma
|
||||
│ ├── layout.ts layout, find-space
|
||||
│ ├── nodes.ts insert, update, delete, move, copy, replace
|
||||
│ ├── pages.ts page list/add/remove/rename/reorder/duplicate
|
||||
│ └── variables.ts vars, vars:set, themes, themes:set, theme:save/load/list
|
||||
├── dist/ Compiled output (openpencil-cli.cjs)
|
||||
├── package.json @zseven-w/openpencil, bin: { op }
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
- **Compile:** `bun run cli:compile` (esbuild to `dist/openpencil-cli.cjs`)
|
||||
- **Dev run:** `bun run cli:dev` (run from source via Bun)
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- **Input methods:** Commands accepting JSON/DSL support inline string, `@filepath`, or `-` (stdin)
|
||||
- **Connection:** WebSocket to running app instance (desktop or web server)
|
||||
- **Launcher:** Auto-detects installed desktop app paths per platform (macOS, Windows, Linux)
|
||||
- **esbuild:** Compiles with `--alias:@=src` to resolve web app imports, `--external:canvas --external:paper`
|
||||
- **Output:** All commands output JSON; `--pretty` flag for human-readable formatting
|
||||
- **Global flags:** `--file <path>` (target .op file), `--page <id>` (target page)
|
||||
132
apps/cli/README.de.md
Normal file
132
apps/cli/README.de.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [**Deutsch**](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
CLI fuer [OpenPencil](https://github.com/ZSeven-W/openpencil) — steuere das Design-Tool von deinem Terminal aus.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Plattformunterstuetzung
|
||||
|
||||
Das CLI erkennt und startet die OpenPencil-Desktop-App automatisch auf allen Plattformen:
|
||||
|
||||
| Plattform | Erkannte Installationspfade |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS pro Benutzer (`%LOCALAPPDATA%`), systemweit (`%PROGRAMFILES%`), portabel |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Verwendung
|
||||
|
||||
```bash
|
||||
op <Befehl> [Optionen]
|
||||
```
|
||||
|
||||
### Eingabemethoden
|
||||
|
||||
Argumente, die JSON oder DSL akzeptieren, koennen auf drei Arten uebergeben werden:
|
||||
|
||||
```bash
|
||||
op design '...' # Inline-Zeichenkette (kleine Nutzlasten)
|
||||
op design @design.txt # Aus Datei lesen (empfohlen fuer grosse Designs)
|
||||
cat design.txt | op design - # Von stdin lesen (Piping)
|
||||
```
|
||||
|
||||
### App-Steuerung
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # OpenPencil starten (standardmaessig Desktop)
|
||||
op stop # Laufende Instanz beenden
|
||||
op status # Pruefen, ob die App laeuft
|
||||
```
|
||||
|
||||
### Design (Batch-DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Dokumentoperationen
|
||||
|
||||
```bash
|
||||
op open [file.op] # Datei oeffnen oder mit aktivem Canvas verbinden
|
||||
op save <file.op> # Aktuelles Dokument speichern
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Aktuelle Canvas-Auswahl abrufen
|
||||
```
|
||||
|
||||
### Knotenmanipulation
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Code-Export
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# Formate: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Variablen und Themes
|
||||
|
||||
```bash
|
||||
op vars # Variablen abrufen
|
||||
op vars:set <json> # Variablen setzen
|
||||
op themes # Themes abrufen
|
||||
op themes:set <json> # Themes setzen
|
||||
op theme:save <file.optheme> # Theme-Preset speichern
|
||||
op theme:load <file.optheme> # Theme-Preset laden
|
||||
op theme:list [dir] # Theme-Presets auflisten
|
||||
```
|
||||
|
||||
### Seiten
|
||||
|
||||
```bash
|
||||
op page list # Seiten auflisten
|
||||
op page add [--name N] # Eine Seite hinzufuegen
|
||||
op page remove <id> # Eine Seite entfernen
|
||||
op page rename <id> <name> # Eine Seite umbenennen
|
||||
op page reorder <id> <index> # Eine Seite neu anordnen
|
||||
op page duplicate <id> # Eine Seite duplizieren
|
||||
```
|
||||
|
||||
### Import
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # SVG-Datei importieren
|
||||
op import:figma <file.fig> # Figma-.fig-Datei importieren
|
||||
```
|
||||
|
||||
### Layout
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Globale Optionen
|
||||
|
||||
```text
|
||||
--file <path> Ziel-.op-Datei (Standard: aktives Canvas)
|
||||
--page <id> Zielseiten-ID
|
||||
--pretty Menschenlesbare JSON-Ausgabe
|
||||
--help Hilfe anzeigen
|
||||
--version Version anzeigen
|
||||
```
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.es.md
Normal file
132
apps/cli/README.es.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [**Español**](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
CLI para [OpenPencil](https://github.com/ZSeven-W/openpencil) — controla la herramienta de diseno desde tu terminal.
|
||||
|
||||
## Instalacion
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Soporte de plataformas
|
||||
|
||||
El CLI detecta y lanza automaticamente la aplicacion de escritorio OpenPencil en todas las plataformas:
|
||||
|
||||
| Plataforma | Rutas de instalacion detectadas |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS por usuario (`%LOCALAPPDATA%`), por maquina (`%PROGRAMFILES%`), portable |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Uso
|
||||
|
||||
```bash
|
||||
op <comando> [opciones]
|
||||
```
|
||||
|
||||
### Metodos de entrada
|
||||
|
||||
Los argumentos que aceptan JSON o DSL se pueden pasar de tres maneras:
|
||||
|
||||
```bash
|
||||
op design '...' # Cadena en linea (cargas pequenas)
|
||||
op design @design.txt # Leer desde archivo (recomendado para disenos grandes)
|
||||
cat design.txt | op design - # Leer desde stdin (tuberia)
|
||||
```
|
||||
|
||||
### Control de la aplicacion
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # Iniciar OpenPencil (escritorio por defecto)
|
||||
op stop # Detener la instancia en ejecucion
|
||||
op status # Verificar si esta en ejecucion
|
||||
```
|
||||
|
||||
### Diseno (DSL por lotes)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Operaciones de documento
|
||||
|
||||
```bash
|
||||
op open [file.op] # Abrir archivo o conectar al lienzo activo
|
||||
op save <file.op> # Guardar el documento actual
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Obtener la seleccion actual del lienzo
|
||||
```
|
||||
|
||||
### Manipulacion de nodos
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Exportacion de codigo
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# Formatos: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Variables y temas
|
||||
|
||||
```bash
|
||||
op vars # Obtener variables
|
||||
op vars:set <json> # Establecer variables
|
||||
op themes # Obtener temas
|
||||
op themes:set <json> # Establecer temas
|
||||
op theme:save <file.optheme> # Guardar preset de tema
|
||||
op theme:load <file.optheme> # Cargar preset de tema
|
||||
op theme:list [dir] # Listar presets de tema
|
||||
```
|
||||
|
||||
### Paginas
|
||||
|
||||
```bash
|
||||
op page list # Listar paginas
|
||||
op page add [--name N] # Agregar una pagina
|
||||
op page remove <id> # Eliminar una pagina
|
||||
op page rename <id> <name> # Renombrar una pagina
|
||||
op page reorder <id> <index> # Reordenar una pagina
|
||||
op page duplicate <id> # Duplicar una pagina
|
||||
```
|
||||
|
||||
### Importacion
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # Importar archivo SVG
|
||||
op import:figma <file.fig> # Importar archivo Figma .fig
|
||||
```
|
||||
|
||||
### Disposicion
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Opciones globales
|
||||
|
||||
```text
|
||||
--file <path> Archivo .op de destino (por defecto: lienzo activo)
|
||||
--page <id> ID de la pagina de destino
|
||||
--pretty Salida JSON legible
|
||||
--help Mostrar ayuda
|
||||
--version Mostrar version
|
||||
```
|
||||
|
||||
## Licencia
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.fr.md
Normal file
132
apps/cli/README.fr.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [**Français**](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
CLI pour [OpenPencil](https://github.com/ZSeven-W/openpencil) — controlez l'outil de design depuis votre terminal.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Plateformes supportees
|
||||
|
||||
Le CLI detecte et lance automatiquement l'application de bureau OpenPencil sur toutes les plateformes :
|
||||
|
||||
| Plateforme | Chemins d'installation detectes |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS par utilisateur (`%LOCALAPPDATA%`), par machine (`%PROGRAMFILES%`), portable |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Utilisation
|
||||
|
||||
```bash
|
||||
op <commande> [options]
|
||||
```
|
||||
|
||||
### Methodes de saisie
|
||||
|
||||
Les arguments acceptant du JSON ou du DSL peuvent etre passes de trois manieres :
|
||||
|
||||
```bash
|
||||
op design '...' # Chaine en ligne (petites charges)
|
||||
op design @design.txt # Lecture depuis un fichier (recommande pour les grands designs)
|
||||
cat design.txt | op design - # Lecture depuis stdin (pipe)
|
||||
```
|
||||
|
||||
### Controle de l'application
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # Lancer OpenPencil (bureau par defaut)
|
||||
op stop # Arreter l'instance en cours
|
||||
op status # Verifier si l'application est en cours d'execution
|
||||
```
|
||||
|
||||
### Design (DSL par lot)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Operations sur les documents
|
||||
|
||||
```bash
|
||||
op open [file.op] # Ouvrir un fichier ou se connecter au canevas actif
|
||||
op save <file.op> # Enregistrer le document actuel
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Obtenir la selection actuelle du canevas
|
||||
```
|
||||
|
||||
### Manipulation des noeuds
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Export de code
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# Formats : react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Variables et themes
|
||||
|
||||
```bash
|
||||
op vars # Obtenir les variables
|
||||
op vars:set <json> # Definir les variables
|
||||
op themes # Obtenir les themes
|
||||
op themes:set <json> # Definir les themes
|
||||
op theme:save <file.optheme> # Enregistrer un preset de theme
|
||||
op theme:load <file.optheme> # Charger un preset de theme
|
||||
op theme:list [dir] # Lister les presets de theme
|
||||
```
|
||||
|
||||
### Pages
|
||||
|
||||
```bash
|
||||
op page list # Lister les pages
|
||||
op page add [--name N] # Ajouter une page
|
||||
op page remove <id> # Supprimer une page
|
||||
op page rename <id> <name> # Renommer une page
|
||||
op page reorder <id> <index> # Reordonner une page
|
||||
op page duplicate <id> # Dupliquer une page
|
||||
```
|
||||
|
||||
### Importation
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # Importer un fichier SVG
|
||||
op import:figma <file.fig> # Importer un fichier Figma .fig
|
||||
```
|
||||
|
||||
### Mise en page
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Options globales
|
||||
|
||||
```text
|
||||
--file <path> Fichier .op cible (par defaut : canevas actif)
|
||||
--page <id> ID de la page cible
|
||||
--pretty Sortie JSON lisible
|
||||
--help Afficher l'aide
|
||||
--version Afficher la version
|
||||
```
|
||||
|
||||
## Licence
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.hi.md
Normal file
132
apps/cli/README.hi.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [**हिन्दी**](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
[OpenPencil](https://github.com/ZSeven-W/openpencil) के लिए CLI — अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें।
|
||||
|
||||
## इंस्टॉल करें
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## प्लेटफ़ॉर्म समर्थन
|
||||
|
||||
CLI सभी प्लेटफ़ॉर्म पर OpenPencil डेस्कटॉप ऐप को स्वचालित रूप से पहचानता और लॉन्च करता है:
|
||||
|
||||
| प्लेटफ़ॉर्म | पहचाने गए इंस्टॉलेशन पथ |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS प्रति-उपयोगकर्ता (`%LOCALAPPDATA%`), प्रति-मशीन (`%PROGRAMFILES%`), पोर्टेबल |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## उपयोग
|
||||
|
||||
```bash
|
||||
op <कमांड> [विकल्प]
|
||||
```
|
||||
|
||||
### इनपुट विधियाँ
|
||||
|
||||
JSON या DSL स्वीकार करने वाले आर्गुमेंट तीन तरीकों से पास किए जा सकते हैं:
|
||||
|
||||
```bash
|
||||
op design '...' # इनलाइन स्ट्रिंग (छोटे पेलोड)
|
||||
op design @design.txt # फ़ाइल से पढ़ें (बड़े डिज़ाइन के लिए अनुशंसित)
|
||||
cat design.txt | op design - # stdin से पढ़ें (पाइपिंग)
|
||||
```
|
||||
|
||||
### ऐप नियंत्रण
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # OpenPencil लॉन्च करें (डिफ़ॉल्ट रूप से डेस्कटॉप)
|
||||
op stop # चल रहे इंस्टेंस को बंद करें
|
||||
op status # जाँचें कि चल रहा है या नहीं
|
||||
```
|
||||
|
||||
### डिज़ाइन (बैच DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### दस्तावेज़ संचालन
|
||||
|
||||
```bash
|
||||
op open [file.op] # फ़ाइल खोलें या लाइव कैनवास से कनेक्ट करें
|
||||
op save <file.op> # वर्तमान दस्तावेज़ सहेजें
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # वर्तमान कैनवास चयन प्राप्त करें
|
||||
```
|
||||
|
||||
### नोड हेरफेर
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### कोड निर्यात
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# प्रारूप: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### वेरिएबल और थीम
|
||||
|
||||
```bash
|
||||
op vars # वेरिएबल प्राप्त करें
|
||||
op vars:set <json> # वेरिएबल सेट करें
|
||||
op themes # थीम प्राप्त करें
|
||||
op themes:set <json> # थीम सेट करें
|
||||
op theme:save <file.optheme> # थीम प्रीसेट सहेजें
|
||||
op theme:load <file.optheme> # थीम प्रीसेट लोड करें
|
||||
op theme:list [dir] # थीम प्रीसेट सूचीबद्ध करें
|
||||
```
|
||||
|
||||
### पेज
|
||||
|
||||
```bash
|
||||
op page list # पेज सूचीबद्ध करें
|
||||
op page add [--name N] # एक पेज जोड़ें
|
||||
op page remove <id> # एक पेज हटाएँ
|
||||
op page rename <id> <name> # एक पेज का नाम बदलें
|
||||
op page reorder <id> <index> # एक पेज का क्रम बदलें
|
||||
op page duplicate <id> # एक पेज डुप्लिकेट करें
|
||||
```
|
||||
|
||||
### आयात
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # SVG फ़ाइल आयात करें
|
||||
op import:figma <file.fig> # Figma .fig फ़ाइल आयात करें
|
||||
```
|
||||
|
||||
### लेआउट
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### वैश्विक फ़्लैग
|
||||
|
||||
```text
|
||||
--file <path> लक्ष्य .op फ़ाइल (डिफ़ॉल्ट: लाइव कैनवास)
|
||||
--page <id> लक्ष्य पेज ID
|
||||
--pretty मानव-पठनीय JSON आउटपुट
|
||||
--help सहायता दिखाएँ
|
||||
--version संस्करण दिखाएँ
|
||||
```
|
||||
|
||||
## लाइसेंस
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.id.md
Normal file
132
apps/cli/README.id.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [**Bahasa Indonesia**](./README.id.md)
|
||||
|
||||
CLI untuk [OpenPencil](https://github.com/ZSeven-W/openpencil) — kendalikan alat desain dari terminal Anda.
|
||||
|
||||
## Instalasi
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Dukungan Platform
|
||||
|
||||
CLI secara otomatis mendeteksi dan meluncurkan aplikasi desktop OpenPencil di semua platform:
|
||||
|
||||
| Platform | Jalur instalasi yang terdeteksi |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS per-pengguna (`%LOCALAPPDATA%`), per-mesin (`%PROGRAMFILES%`), portabel |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Penggunaan
|
||||
|
||||
```bash
|
||||
op <perintah> [opsi]
|
||||
```
|
||||
|
||||
### Metode Input
|
||||
|
||||
Argumen yang menerima JSON atau DSL dapat diberikan dengan tiga cara:
|
||||
|
||||
```bash
|
||||
op design '...' # String inline (data kecil)
|
||||
op design @design.txt # Baca dari file (disarankan untuk desain besar)
|
||||
cat design.txt | op design - # Baca dari stdin (piping)
|
||||
```
|
||||
|
||||
### Kontrol Aplikasi
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # Jalankan OpenPencil (desktop secara default)
|
||||
op stop # Hentikan instance yang berjalan
|
||||
op status # Periksa apakah sedang berjalan
|
||||
```
|
||||
|
||||
### Desain (Batch DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Operasi Dokumen
|
||||
|
||||
```bash
|
||||
op open [file.op] # Buka file atau hubungkan ke kanvas langsung
|
||||
op save <file.op> # Simpan dokumen saat ini
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Dapatkan seleksi kanvas saat ini
|
||||
```
|
||||
|
||||
### Manipulasi Node
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Ekspor Kode
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# Format: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Variabel & Tema
|
||||
|
||||
```bash
|
||||
op vars # Dapatkan variabel
|
||||
op vars:set <json> # Atur variabel
|
||||
op themes # Dapatkan tema
|
||||
op themes:set <json> # Atur tema
|
||||
op theme:save <file.optheme> # Simpan preset tema
|
||||
op theme:load <file.optheme> # Muat preset tema
|
||||
op theme:list [dir] # Daftar preset tema
|
||||
```
|
||||
|
||||
### Halaman
|
||||
|
||||
```bash
|
||||
op page list # Daftar halaman
|
||||
op page add [--name N] # Tambah halaman
|
||||
op page remove <id> # Hapus halaman
|
||||
op page rename <id> <name> # Ganti nama halaman
|
||||
op page reorder <id> <index> # Urutkan ulang halaman
|
||||
op page duplicate <id> # Duplikasi halaman
|
||||
```
|
||||
|
||||
### Impor
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # Impor file SVG
|
||||
op import:figma <file.fig> # Impor file Figma .fig
|
||||
```
|
||||
|
||||
### Tata Letak
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Flag Global
|
||||
|
||||
```text
|
||||
--file <path> File .op target (default: kanvas langsung)
|
||||
--page <id> ID halaman target
|
||||
--pretty Output JSON yang mudah dibaca
|
||||
--help Tampilkan bantuan
|
||||
--version Tampilkan versi
|
||||
```
|
||||
|
||||
## Lisensi
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.ja.md
Normal file
132
apps/cli/README.ja.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [**日本語**](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
[OpenPencil](https://github.com/ZSeven-W/openpencil) 用 CLI — ターミナルからデザインツールを操作できます。
|
||||
|
||||
## インストール
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## プラットフォーム対応
|
||||
|
||||
CLI はすべてのプラットフォームで OpenPencil デスクトップアプリを自動検出して起動します:
|
||||
|
||||
| プラットフォーム | 検出されるインストールパス |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS ユーザー単位 (`%LOCALAPPDATA%`)、マシン単位 (`%PROGRAMFILES%`)、ポータブル |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## 使い方
|
||||
|
||||
```bash
|
||||
op <command> [options]
|
||||
```
|
||||
|
||||
### 入力方法
|
||||
|
||||
JSON または DSL を受け付ける引数は、3 つの方法で渡すことができます:
|
||||
|
||||
```bash
|
||||
op design '...' # インライン文字列(小さなペイロード向け)
|
||||
op design @design.txt # ファイルから読み込み(大きなデザインに推奨)
|
||||
cat design.txt | op design - # 標準入力から読み込み(パイプ)
|
||||
```
|
||||
|
||||
### アプリ制御
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # OpenPencil を起動(デフォルトはデスクトップ)
|
||||
op stop # 実行中のインスタンスを停止
|
||||
op status # 実行中かどうかを確認
|
||||
```
|
||||
|
||||
### デザイン(バッチ DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### ドキュメント操作
|
||||
|
||||
```bash
|
||||
op open [file.op] # ファイルを開く、またはライブキャンバスに接続
|
||||
op save <file.op> # 現在のドキュメントを保存
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # 現在のキャンバスの選択を取得
|
||||
```
|
||||
|
||||
### ノード操作
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### コードエクスポート
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# フォーマット: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### 変数とテーマ
|
||||
|
||||
```bash
|
||||
op vars # 変数を取得
|
||||
op vars:set <json> # 変数を設定
|
||||
op themes # テーマを取得
|
||||
op themes:set <json> # テーマを設定
|
||||
op theme:save <file.optheme> # テーマプリセットを保存
|
||||
op theme:load <file.optheme> # テーマプリセットを読み込み
|
||||
op theme:list [dir] # テーマプリセットを一覧表示
|
||||
```
|
||||
|
||||
### ページ
|
||||
|
||||
```bash
|
||||
op page list # ページを一覧表示
|
||||
op page add [--name N] # ページを追加
|
||||
op page remove <id> # ページを削除
|
||||
op page rename <id> <name> # ページの名前を変更
|
||||
op page reorder <id> <index> # ページを並べ替え
|
||||
op page duplicate <id> # ページを複製
|
||||
```
|
||||
|
||||
### インポート
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # SVG ファイルをインポート
|
||||
op import:figma <file.fig> # Figma .fig ファイルをインポート
|
||||
```
|
||||
|
||||
### レイアウト
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### グローバルフラグ
|
||||
|
||||
```text
|
||||
--file <path> 対象の .op ファイル(デフォルト: ライブキャンバス)
|
||||
--page <id> 対象のページ ID
|
||||
--pretty 人間が読みやすい JSON 出力
|
||||
--help ヘルプを表示
|
||||
--version バージョンを表示
|
||||
```
|
||||
|
||||
## ライセンス
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.ko.md
Normal file
132
apps/cli/README.ko.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [**한국어**](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
[OpenPencil](https://github.com/ZSeven-W/openpencil)용 CLI — 터미널에서 디자인 도구를 제어합니다.
|
||||
|
||||
## 설치
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## 플랫폼 지원
|
||||
|
||||
CLI는 모든 플랫폼에서 OpenPencil 데스크톱 앱을 자동으로 감지하고 실행합니다:
|
||||
|
||||
| 플랫폼 | 감지되는 설치 경로 |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS 사용자별 (`%LOCALAPPDATA%`), 시스템 전체 (`%PROGRAMFILES%`), 포터블 |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## 사용법
|
||||
|
||||
```bash
|
||||
op <command> [options]
|
||||
```
|
||||
|
||||
### 입력 방식
|
||||
|
||||
JSON 또는 DSL을 받는 인자는 세 가지 방법으로 전달할 수 있습니다:
|
||||
|
||||
```bash
|
||||
op design '...' # 인라인 문자열 (작은 페이로드)
|
||||
op design @design.txt # 파일에서 읽기 (대규모 디자인에 권장)
|
||||
cat design.txt | op design - # 표준 입력에서 읽기 (파이핑)
|
||||
```
|
||||
|
||||
### 앱 제어
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # OpenPencil 실행 (기본값: 데스크톱)
|
||||
op stop # 실행 중인 인스턴스 중지
|
||||
op status # 실행 상태 확인
|
||||
```
|
||||
|
||||
### 디자인 (배치 DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### 문서 작업
|
||||
|
||||
```bash
|
||||
op open [file.op] # 파일 열기 또는 라이브 캔버스에 연결
|
||||
op save <file.op> # 현재 문서 저장
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # 현재 캔버스 선택 항목 가져오기
|
||||
```
|
||||
|
||||
### 노드 조작
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### 코드 내보내기
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# 형식: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### 변수 및 테마
|
||||
|
||||
```bash
|
||||
op vars # 변수 가져오기
|
||||
op vars:set <json> # 변수 설정
|
||||
op themes # 테마 가져오기
|
||||
op themes:set <json> # 테마 설정
|
||||
op theme:save <file.optheme> # 테마 프리셋 저장
|
||||
op theme:load <file.optheme> # 테마 프리셋 불러오기
|
||||
op theme:list [dir] # 테마 프리셋 목록 보기
|
||||
```
|
||||
|
||||
### 페이지
|
||||
|
||||
```bash
|
||||
op page list # 페이지 목록 보기
|
||||
op page add [--name N] # 페이지 추가
|
||||
op page remove <id> # 페이지 제거
|
||||
op page rename <id> <name> # 페이지 이름 변경
|
||||
op page reorder <id> <index> # 페이지 순서 변경
|
||||
op page duplicate <id> # 페이지 복제
|
||||
```
|
||||
|
||||
### 가져오기
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # SVG 파일 가져오기
|
||||
op import:figma <file.fig> # Figma .fig 파일 가져오기
|
||||
```
|
||||
|
||||
### 레이아웃
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### 전역 플래그
|
||||
|
||||
```text
|
||||
--file <path> 대상 .op 파일 (기본값: 라이브 캔버스)
|
||||
--page <id> 대상 페이지 ID
|
||||
--pretty 사람이 읽기 쉬운 JSON 출력
|
||||
--help 도움말 표시
|
||||
--version 버전 표시
|
||||
```
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.md
Normal file
132
apps/cli/README.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[**English**](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
CLI for [OpenPencil](https://github.com/ZSeven-W/openpencil) — control the design tool from your terminal.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Platform Support
|
||||
|
||||
The CLI automatically detects and launches the OpenPencil desktop app on all platforms:
|
||||
|
||||
| Platform | Installation paths detected |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS per-user (`%LOCALAPPDATA%`), per-machine (`%PROGRAMFILES%`), portable |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
op <command> [options]
|
||||
```
|
||||
|
||||
### Input Methods
|
||||
|
||||
Arguments that accept JSON or DSL can be passed in three ways:
|
||||
|
||||
```bash
|
||||
op design '...' # Inline string (small payloads)
|
||||
op design @design.txt # Read from file (recommended for large designs)
|
||||
cat design.txt | op design - # Read from stdin (piping)
|
||||
```
|
||||
|
||||
### App Control
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # Launch OpenPencil (desktop by default)
|
||||
op stop # Stop running instance
|
||||
op status # Check if running
|
||||
```
|
||||
|
||||
### Design (Batch DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Document Operations
|
||||
|
||||
```bash
|
||||
op open [file.op] # Open file or connect to live canvas
|
||||
op save <file.op> # Save current document
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Get current canvas selection
|
||||
```
|
||||
|
||||
### Node Manipulation
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Code Export
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# Formats: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Variables & Themes
|
||||
|
||||
```bash
|
||||
op vars # Get variables
|
||||
op vars:set <json> # Set variables
|
||||
op themes # Get themes
|
||||
op themes:set <json> # Set themes
|
||||
op theme:save <file.optheme> # Save theme preset
|
||||
op theme:load <file.optheme> # Load theme preset
|
||||
op theme:list [dir] # List theme presets
|
||||
```
|
||||
|
||||
### Pages
|
||||
|
||||
```bash
|
||||
op page list # List pages
|
||||
op page add [--name N] # Add a page
|
||||
op page remove <id> # Remove a page
|
||||
op page rename <id> <name> # Rename a page
|
||||
op page reorder <id> <index> # Reorder a page
|
||||
op page duplicate <id> # Duplicate a page
|
||||
```
|
||||
|
||||
### Import
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # Import SVG file
|
||||
op import:figma <file.fig> # Import Figma .fig file
|
||||
```
|
||||
|
||||
### Layout
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Global Flags
|
||||
|
||||
```text
|
||||
--file <path> Target .op file (default: live canvas)
|
||||
--page <id> Target page ID
|
||||
--pretty Human-readable JSON output
|
||||
--help Show help
|
||||
--version Show version
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.pt.md
Normal file
132
apps/cli/README.pt.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [**Português**](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
CLI para o [OpenPencil](https://github.com/ZSeven-W/openpencil) — controle a ferramenta de design pelo seu terminal.
|
||||
|
||||
## Instalar
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Suporte a Plataformas
|
||||
|
||||
A CLI detecta e inicia automaticamente o aplicativo desktop OpenPencil em todas as plataformas:
|
||||
|
||||
| Plataforma | Caminhos de instalacao detectados |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS por usuario (`%LOCALAPPDATA%`), por maquina (`%PROGRAMFILES%`), portatil |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Uso
|
||||
|
||||
```bash
|
||||
op <comando> [opcoes]
|
||||
```
|
||||
|
||||
### Metodos de Entrada
|
||||
|
||||
Argumentos que aceitam JSON ou DSL podem ser passados de tres formas:
|
||||
|
||||
```bash
|
||||
op design '...' # String inline (payloads pequenos)
|
||||
op design @design.txt # Ler de arquivo (recomendado para designs grandes)
|
||||
cat design.txt | op design - # Ler da entrada padrao (piping)
|
||||
```
|
||||
|
||||
### Controle do Aplicativo
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # Iniciar o OpenPencil (desktop por padrao)
|
||||
op stop # Parar a instancia em execucao
|
||||
op status # Verificar se esta em execucao
|
||||
```
|
||||
|
||||
### Design (DSL em Lote)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Operacoes de Documento
|
||||
|
||||
```bash
|
||||
op open [file.op] # Abrir arquivo ou conectar ao canvas ativo
|
||||
op save <file.op> # Salvar o documento atual
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Obter a selecao atual do canvas
|
||||
```
|
||||
|
||||
### Manipulacao de Nos
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Exportacao de Codigo
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# Formatos: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Variaveis e Temas
|
||||
|
||||
```bash
|
||||
op vars # Obter variaveis
|
||||
op vars:set <json> # Definir variaveis
|
||||
op themes # Obter temas
|
||||
op themes:set <json> # Definir temas
|
||||
op theme:save <file.optheme> # Salvar preset de tema
|
||||
op theme:load <file.optheme> # Carregar preset de tema
|
||||
op theme:list [dir] # Listar presets de temas
|
||||
```
|
||||
|
||||
### Paginas
|
||||
|
||||
```bash
|
||||
op page list # Listar paginas
|
||||
op page add [--name N] # Adicionar uma pagina
|
||||
op page remove <id> # Remover uma pagina
|
||||
op page rename <id> <name> # Renomear uma pagina
|
||||
op page reorder <id> <index> # Reordenar uma pagina
|
||||
op page duplicate <id> # Duplicar uma pagina
|
||||
```
|
||||
|
||||
### Importacao
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # Importar arquivo SVG
|
||||
op import:figma <file.fig> # Importar arquivo .fig do Figma
|
||||
```
|
||||
|
||||
### Layout
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Flags Globais
|
||||
|
||||
```text
|
||||
--file <path> Arquivo .op alvo (padrao: canvas ativo)
|
||||
--page <id> ID da pagina alvo
|
||||
--pretty Saida JSON legivel
|
||||
--help Mostrar ajuda
|
||||
--version Mostrar versao
|
||||
```
|
||||
|
||||
## Licenca
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.ru.md
Normal file
132
apps/cli/README.ru.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [**Русский**](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
CLI для [OpenPencil](https://github.com/ZSeven-W/openpencil) — управляйте инструментом дизайна из терминала.
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Поддержка платформ
|
||||
|
||||
CLI автоматически обнаруживает и запускает настольное приложение OpenPencil на всех платформах:
|
||||
|
||||
| Платформа | Обнаруживаемые пути установки |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS для пользователя (`%LOCALAPPDATA%`), для машины (`%PROGRAMFILES%`), портативная версия |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Использование
|
||||
|
||||
```bash
|
||||
op <команда> [параметры]
|
||||
```
|
||||
|
||||
### Методы ввода
|
||||
|
||||
Аргументы, принимающие JSON или DSL, можно передать тремя способами:
|
||||
|
||||
```bash
|
||||
op design '...' # Встроенная строка (небольшие данные)
|
||||
op design @design.txt # Чтение из файла (рекомендуется для больших дизайнов)
|
||||
cat design.txt | op design - # Чтение из stdin (через конвейер)
|
||||
```
|
||||
|
||||
### Управление приложением
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # Запустить OpenPencil (по умолчанию — настольное приложение)
|
||||
op stop # Остановить запущенный экземпляр
|
||||
op status # Проверить, запущено ли приложение
|
||||
```
|
||||
|
||||
### Дизайн (пакетный DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Операции с документом
|
||||
|
||||
```bash
|
||||
op open [file.op] # Открыть файл или подключиться к активному холсту
|
||||
op save <file.op> # Сохранить текущий документ
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Получить текущее выделение на холсте
|
||||
```
|
||||
|
||||
### Работа с узлами
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Экспорт кода
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# Форматы: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Переменные и темы
|
||||
|
||||
```bash
|
||||
op vars # Получить переменные
|
||||
op vars:set <json> # Задать переменные
|
||||
op themes # Получить темы
|
||||
op themes:set <json> # Задать темы
|
||||
op theme:save <file.optheme> # Сохранить пресет темы
|
||||
op theme:load <file.optheme> # Загрузить пресет темы
|
||||
op theme:list [dir] # Список пресетов тем
|
||||
```
|
||||
|
||||
### Страницы
|
||||
|
||||
```bash
|
||||
op page list # Список страниц
|
||||
op page add [--name N] # Добавить страницу
|
||||
op page remove <id> # Удалить страницу
|
||||
op page rename <id> <name> # Переименовать страницу
|
||||
op page reorder <id> <index> # Изменить порядок страницы
|
||||
op page duplicate <id> # Дублировать страницу
|
||||
```
|
||||
|
||||
### Импорт
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # Импортировать SVG-файл
|
||||
op import:figma <file.fig> # Импортировать файл Figma .fig
|
||||
```
|
||||
|
||||
### Макет
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Глобальные флаги
|
||||
|
||||
```text
|
||||
--file <path> Целевой файл .op (по умолчанию: активный холст)
|
||||
--page <id> ID целевой страницы
|
||||
--pretty Читаемый вывод JSON
|
||||
--help Показать справку
|
||||
--version Показать версию
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.th.md
Normal file
132
apps/cli/README.th.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [**ไทย**](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
CLI สำหรับ [OpenPencil](https://github.com/ZSeven-W/openpencil) — ควบคุมเครื่องมือออกแบบจากเทอร์มินัลของคุณ
|
||||
|
||||
## การติดตั้ง
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## การรองรับแพลตฟอร์ม
|
||||
|
||||
CLI จะตรวจจับและเปิดแอปเดสก์ท็อป OpenPencil โดยอัตโนมัติบนทุกแพลตฟอร์ม:
|
||||
|
||||
| แพลตฟอร์ม | เส้นทางการติดตั้งที่ตรวจพบ |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS ต่อผู้ใช้ (`%LOCALAPPDATA%`), ต่อเครื่อง (`%PROGRAMFILES%`), แบบพกพา |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## การใช้งาน
|
||||
|
||||
```bash
|
||||
op <คำสั่ง> [ตัวเลือก]
|
||||
```
|
||||
|
||||
### วิธีการป้อนข้อมูล
|
||||
|
||||
อาร์กิวเมนต์ที่รับ JSON หรือ DSL สามารถส่งได้สามวิธี:
|
||||
|
||||
```bash
|
||||
op design '...' # ข้อความแบบอินไลน์ (ข้อมูลขนาดเล็ก)
|
||||
op design @design.txt # อ่านจากไฟล์ (แนะนำสำหรับการออกแบบขนาดใหญ่)
|
||||
cat design.txt | op design - # อ่านจาก stdin (การไพพ์)
|
||||
```
|
||||
|
||||
### การควบคุมแอป
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # เปิด OpenPencil (เดสก์ท็อปเป็นค่าเริ่มต้น)
|
||||
op stop # หยุดอินสแตนซ์ที่กำลังทำงาน
|
||||
op status # ตรวจสอบว่ากำลังทำงานอยู่หรือไม่
|
||||
```
|
||||
|
||||
### การออกแบบ (Batch DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### การดำเนินการเอกสาร
|
||||
|
||||
```bash
|
||||
op open [file.op] # เปิดไฟล์หรือเชื่อมต่อกับแคนวาสสด
|
||||
op save <file.op> # บันทึกเอกสารปัจจุบัน
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # รับการเลือกแคนวาสปัจจุบัน
|
||||
```
|
||||
|
||||
### การจัดการโหนด
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### การส่งออกโค้ด
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# รูปแบบ: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### ตัวแปรและธีม
|
||||
|
||||
```bash
|
||||
op vars # รับตัวแปร
|
||||
op vars:set <json> # ตั้งค่าตัวแปร
|
||||
op themes # รับธีม
|
||||
op themes:set <json> # ตั้งค่าธีม
|
||||
op theme:save <file.optheme> # บันทึกพรีเซ็ตธีม
|
||||
op theme:load <file.optheme> # โหลดพรีเซ็ตธีม
|
||||
op theme:list [dir] # แสดงรายการพรีเซ็ตธีม
|
||||
```
|
||||
|
||||
### หน้า
|
||||
|
||||
```bash
|
||||
op page list # แสดงรายการหน้า
|
||||
op page add [--name N] # เพิ่มหน้า
|
||||
op page remove <id> # ลบหน้า
|
||||
op page rename <id> <name> # เปลี่ยนชื่อหน้า
|
||||
op page reorder <id> <index> # จัดลำดับหน้าใหม่
|
||||
op page duplicate <id> # ทำสำเนาหน้า
|
||||
```
|
||||
|
||||
### การนำเข้า
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # นำเข้าไฟล์ SVG
|
||||
op import:figma <file.fig> # นำเข้าไฟล์ Figma .fig
|
||||
```
|
||||
|
||||
### เลย์เอาต์
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### แฟล็กทั่วไป
|
||||
|
||||
```text
|
||||
--file <path> ไฟล์ .op เป้าหมาย (ค่าเริ่มต้น: แคนวาสสด)
|
||||
--page <id> ID หน้าเป้าหมาย
|
||||
--pretty แสดงผล JSON แบบอ่านง่าย
|
||||
--help แสดงความช่วยเหลือ
|
||||
--version แสดงเวอร์ชัน
|
||||
```
|
||||
|
||||
## สัญญาอนุญาต
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.tr.md
Normal file
132
apps/cli/README.tr.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [**Türkçe**](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
[OpenPencil](https://github.com/ZSeven-W/openpencil) icin CLI — tasarim aracini terminalinizden kontrol edin.
|
||||
|
||||
## Kurulum
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Platform Destegi
|
||||
|
||||
CLI, tum platformlarda OpenPencil masaustu uygulamasini otomatik olarak algilar ve baslatir:
|
||||
|
||||
| Platform | Algilanan kurulum yollari |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | Kullanici basina NSIS (`%LOCALAPPDATA%`), makine basina (`%PROGRAMFILES%`), tasinabilir |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Kullanim
|
||||
|
||||
```bash
|
||||
op <komut> [secenekler]
|
||||
```
|
||||
|
||||
### Girdi Yontemleri
|
||||
|
||||
JSON veya DSL kabul eden argumanlar uc sekilde iletilebilir:
|
||||
|
||||
```bash
|
||||
op design '...' # Satir ici metin (kucuk veriler)
|
||||
op design @design.txt # Dosyadan oku (buyuk tasarimlar icin onerilir)
|
||||
cat design.txt | op design - # Stdin'den oku (borulama)
|
||||
```
|
||||
|
||||
### Uygulama Kontrolu
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # OpenPencil'i baslat (varsayilan: masaustu)
|
||||
op stop # Calisan ornegi durdur
|
||||
op status # Calisip calismadigini kontrol et
|
||||
```
|
||||
|
||||
### Tasarim (Toplu DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@dosya|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@dosya|->
|
||||
op design:content <bolum-id> <json|@dosya|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Belge Islemleri
|
||||
|
||||
```bash
|
||||
op open [dosya.op] # Dosya ac veya canli tuvale baglan
|
||||
op save <dosya.op> # Mevcut belgeyi kaydet
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Mevcut tuval secimini al
|
||||
```
|
||||
|
||||
### Dugum Manipulasyonu
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Kod Disari Aktarimi
|
||||
|
||||
```bash
|
||||
op export <format> [--out dosya]
|
||||
# Formatlar: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Degiskenler ve Temalar
|
||||
|
||||
```bash
|
||||
op vars # Degiskenleri al
|
||||
op vars:set <json> # Degiskenleri ayarla
|
||||
op themes # Temalari al
|
||||
op themes:set <json> # Temalari ayarla
|
||||
op theme:save <dosya.optheme> # Tema onayarini kaydet
|
||||
op theme:load <dosya.optheme> # Tema onayarini yukle
|
||||
op theme:list [dizin] # Tema onayarlarini listele
|
||||
```
|
||||
|
||||
### Sayfalar
|
||||
|
||||
```bash
|
||||
op page list # Sayfalari listele
|
||||
op page add [--name N] # Sayfa ekle
|
||||
op page remove <id> # Sayfa kaldir
|
||||
op page rename <id> <ad> # Sayfayi yeniden adlandir
|
||||
op page reorder <id> <indeks> # Sayfayi yeniden sirala
|
||||
op page duplicate <id> # Sayfayi cogalt
|
||||
```
|
||||
|
||||
### Iceri Aktarma
|
||||
|
||||
```bash
|
||||
op import:svg <dosya.svg> # SVG dosyasi iceri aktar
|
||||
op import:figma <dosya.fig> # Figma .fig dosyasi iceri aktar
|
||||
```
|
||||
|
||||
### Yerlesim
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Genel Bayraklar
|
||||
|
||||
```text
|
||||
--file <yol> Hedef .op dosyasi (varsayilan: canli tuval)
|
||||
--page <id> Hedef sayfa kimligi
|
||||
--pretty Okunabilir JSON ciktisi
|
||||
--help Yardimi goster
|
||||
--version Surumu goster
|
||||
```
|
||||
|
||||
## Lisans
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.vi.md
Normal file
132
apps/cli/README.vi.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [**Tiếng Việt**](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
CLI cho [OpenPencil](https://github.com/ZSeven-W/openpencil) — điều khiển công cụ thiết kế từ terminal của bạn.
|
||||
|
||||
## Cài đặt
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## Hỗ trợ nền tảng
|
||||
|
||||
CLI tự động phát hiện và khởi chạy ứng dụng desktop OpenPencil trên tất cả các nền tảng:
|
||||
|
||||
| Nền tảng | Đường dẫn cài đặt được phát hiện |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS theo người dùng (`%LOCALAPPDATA%`), theo máy (`%PROGRAMFILES%`), di động |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## Sử dụng
|
||||
|
||||
```bash
|
||||
op <lệnh> [tùy-chọn]
|
||||
```
|
||||
|
||||
### Phương thức nhập liệu
|
||||
|
||||
Các đối số chấp nhận JSON hoặc DSL có thể được truyền theo ba cách:
|
||||
|
||||
```bash
|
||||
op design '...' # Chuỗi nội tuyến (dữ liệu nhỏ)
|
||||
op design @design.txt # Đọc từ tệp (khuyến nghị cho thiết kế lớn)
|
||||
cat design.txt | op design - # Đọc từ stdin (đường ống)
|
||||
```
|
||||
|
||||
### Điều khiển ứng dụng
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # Khởi chạy OpenPencil (mặc định: desktop)
|
||||
op stop # Dừng phiên bản đang chạy
|
||||
op status # Kiểm tra trạng thái hoạt động
|
||||
```
|
||||
|
||||
### Thiết kế (Batch DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### Thao tác tài liệu
|
||||
|
||||
```bash
|
||||
op open [file.op] # Mở tệp hoặc kết nối với canvas trực tiếp
|
||||
op save <file.op> # Lưu tài liệu hiện tại
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # Lấy vùng chọn canvas hiện tại
|
||||
```
|
||||
|
||||
### Thao tác nút
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### Xuất mã nguồn
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# Định dạng: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### Biến và giao diện
|
||||
|
||||
```bash
|
||||
op vars # Lấy biến
|
||||
op vars:set <json> # Đặt biến
|
||||
op themes # Lấy giao diện
|
||||
op themes:set <json> # Đặt giao diện
|
||||
op theme:save <file.optheme> # Lưu bộ giao diện mẫu
|
||||
op theme:load <file.optheme> # Tải bộ giao diện mẫu
|
||||
op theme:list [dir] # Liệt kê bộ giao diện mẫu
|
||||
```
|
||||
|
||||
### Trang
|
||||
|
||||
```bash
|
||||
op page list # Liệt kê trang
|
||||
op page add [--name N] # Thêm trang
|
||||
op page remove <id> # Xóa trang
|
||||
op page rename <id> <name> # Đổi tên trang
|
||||
op page reorder <id> <index> # Sắp xếp lại trang
|
||||
op page duplicate <id> # Nhân bản trang
|
||||
```
|
||||
|
||||
### Nhập
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # Nhập tệp SVG
|
||||
op import:figma <file.fig> # Nhập tệp Figma .fig
|
||||
```
|
||||
|
||||
### Bố cục
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### Cờ toàn cục
|
||||
|
||||
```text
|
||||
--file <path> Tệp .op đích (mặc định: canvas trực tiếp)
|
||||
--page <id> ID trang đích
|
||||
--pretty Xuất JSON dễ đọc
|
||||
--help Hiển thị trợ giúp
|
||||
--version Hiển thị phiên bản
|
||||
```
|
||||
|
||||
## Giấy phép
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.zh-TW.md
Normal file
132
apps/cli/README.zh-TW.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [简体中文](./README.zh.md) · [**繁體中文**](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
[OpenPencil](https://github.com/ZSeven-W/openpencil) 的命令列工具 — 從終端機控制設計工具。
|
||||
|
||||
## 安裝
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## 平台支援
|
||||
|
||||
CLI 會自動偵測並啟動所有平台上的 OpenPencil 桌面應用程式:
|
||||
|
||||
| 平台 | 偵測的安裝路徑 |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`、`~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS 使用者安裝(`%LOCALAPPDATA%`)、全域安裝(`%PROGRAMFILES%`)、可攜版 |
|
||||
| **Linux** | `/usr/bin`、`/usr/local/bin`、`~/.local/bin`、AppImage(`~/Applications`、`~/Downloads`)、Snap、Flatpak |
|
||||
|
||||
## 使用方式
|
||||
|
||||
```bash
|
||||
op <command> [options]
|
||||
```
|
||||
|
||||
### 輸入方式
|
||||
|
||||
接受 JSON 或 DSL 的參數可透過三種方式傳入:
|
||||
|
||||
```bash
|
||||
op design '...' # 內嵌字串(適合小型內容)
|
||||
op design @design.txt # 從檔案讀取(建議用於大型設計)
|
||||
cat design.txt | op design - # 從標準輸入讀取(管線傳輸)
|
||||
```
|
||||
|
||||
### 應用程式控制
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # 啟動 OpenPencil(預設為桌面版)
|
||||
op stop # 停止執行中的實例
|
||||
op status # 檢查是否正在執行
|
||||
```
|
||||
|
||||
### 設計(批次 DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### 文件操作
|
||||
|
||||
```bash
|
||||
op open [file.op] # 開啟檔案或連線至即時畫布
|
||||
op save <file.op> # 儲存目前的文件
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # 取得目前畫布的選取項目
|
||||
```
|
||||
|
||||
### 節點操作
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### 程式碼匯出
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# 格式:react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### 變數與主題
|
||||
|
||||
```bash
|
||||
op vars # 取得變數
|
||||
op vars:set <json> # 設定變數
|
||||
op themes # 取得主題
|
||||
op themes:set <json> # 設定主題
|
||||
op theme:save <file.optheme> # 儲存主題預設
|
||||
op theme:load <file.optheme> # 載入主題預設
|
||||
op theme:list [dir] # 列出主題預設
|
||||
```
|
||||
|
||||
### 頁面
|
||||
|
||||
```bash
|
||||
op page list # 列出頁面
|
||||
op page add [--name N] # 新增頁面
|
||||
op page remove <id> # 移除頁面
|
||||
op page rename <id> <name> # 重新命名頁面
|
||||
op page reorder <id> <index> # 重新排序頁面
|
||||
op page duplicate <id> # 複製頁面
|
||||
```
|
||||
|
||||
### 匯入
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # 匯入 SVG 檔案
|
||||
op import:figma <file.fig> # 匯入 Figma .fig 檔案
|
||||
```
|
||||
|
||||
### 版面配置
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### 全域旗標
|
||||
|
||||
```text
|
||||
--file <path> 目標 .op 檔案(預設:即時畫布)
|
||||
--page <id> 目標頁面 ID
|
||||
--pretty 人類可讀的 JSON 輸出
|
||||
--help 顯示說明
|
||||
--version 顯示版本
|
||||
```
|
||||
|
||||
## 授權條款
|
||||
|
||||
MIT
|
||||
132
apps/cli/README.zh.md
Normal file
132
apps/cli/README.zh.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# @zseven-w/openpencil
|
||||
|
||||
[English](./README.md) · [**简体中文**](./README.zh.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Français](./README.fr.md) · [Español](./README.es.md) · [Deutsch](./README.de.md) · [Português](./README.pt.md) · [Русский](./README.ru.md) · [हिन्दी](./README.hi.md) · [Türkçe](./README.tr.md) · [ไทย](./README.th.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md)
|
||||
|
||||
[OpenPencil](https://github.com/ZSeven-W/openpencil) 的命令行工具 — 从终端控制设计工具。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install -g @zseven-w/openpencil
|
||||
```
|
||||
|
||||
## 平台支持
|
||||
|
||||
CLI 会自动检测并启动各平台上的 OpenPencil 桌面应用:
|
||||
|
||||
| 平台 | 检测的安装路径 |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | `/Applications/OpenPencil.app`, `~/Applications/OpenPencil.app` |
|
||||
| **Windows** | NSIS 用户级 (`%LOCALAPPDATA%`)、系统级 (`%PROGRAMFILES%`)、便携版 |
|
||||
| **Linux** | `/usr/bin`, `/usr/local/bin`, `~/.local/bin`, AppImage (`~/Applications`, `~/Downloads`), Snap, Flatpak |
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
op <command> [options]
|
||||
```
|
||||
|
||||
### 输入方式
|
||||
|
||||
接受 JSON 或 DSL 的参数支持三种传入方式:
|
||||
|
||||
```bash
|
||||
op design '...' # 内联字符串(适合小型内容)
|
||||
op design @design.txt # 从文件读取(推荐用于大型设计)
|
||||
cat design.txt | op design - # 从标准输入读取(管道传入)
|
||||
```
|
||||
|
||||
### 应用控制
|
||||
|
||||
```bash
|
||||
op start [--desktop|--web] # 启动 OpenPencil(默认桌面版)
|
||||
op stop # 停止运行中的实例
|
||||
op status # 检查运行状态
|
||||
```
|
||||
|
||||
### 设计(批量 DSL)
|
||||
|
||||
```bash
|
||||
op design <dsl|@file|-> [--post-process] [--canvas-width N]
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
```
|
||||
|
||||
### 文档操作
|
||||
|
||||
```bash
|
||||
op open [file.op] # 打开文件或连接到实时画布
|
||||
op save <file.op> # 保存当前文档
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection # 获取当前画布选中项
|
||||
```
|
||||
|
||||
### 节点操作
|
||||
|
||||
```bash
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
```
|
||||
|
||||
### 代码导出
|
||||
|
||||
```bash
|
||||
op export <format> [--out file]
|
||||
# 格式:react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
```
|
||||
|
||||
### 变量与主题
|
||||
|
||||
```bash
|
||||
op vars # 获取变量
|
||||
op vars:set <json> # 设置变量
|
||||
op themes # 获取主题
|
||||
op themes:set <json> # 设置主题
|
||||
op theme:save <file.optheme> # 保存主题预设
|
||||
op theme:load <file.optheme> # 加载主题预设
|
||||
op theme:list [dir] # 列出主题预设
|
||||
```
|
||||
|
||||
### 页面
|
||||
|
||||
```bash
|
||||
op page list # 列出页面
|
||||
op page add [--name N] # 添加页面
|
||||
op page remove <id> # 删除页面
|
||||
op page rename <id> <name> # 重命名页面
|
||||
op page reorder <id> <index> # 调整页面顺序
|
||||
op page duplicate <id> # 复制页面
|
||||
```
|
||||
|
||||
### 导入
|
||||
|
||||
```bash
|
||||
op import:svg <file.svg> # 导入 SVG 文件
|
||||
op import:figma <file.fig> # 导入 Figma .fig 文件
|
||||
```
|
||||
|
||||
### 布局
|
||||
|
||||
```bash
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
```
|
||||
|
||||
### 全局选项
|
||||
|
||||
```text
|
||||
--file <path> 目标 .op 文件(默认:实时画布)
|
||||
--page <id> 目标页面 ID
|
||||
--pretty 人类可读的 JSON 输出
|
||||
--help 显示帮助
|
||||
--version 显示版本
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
20
apps/cli/package.json
Normal file
20
apps/cli/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@zseven-w/openpencil",
|
||||
"version": "0.5.1",
|
||||
"description": "CLI for OpenPencil — control the design tool from your terminal",
|
||||
"author": {
|
||||
"name": "ZSeven-W",
|
||||
"email": "xkayshen@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"op": "dist/openpencil-cli.cjs"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"compile": "cd ../web && esbuild ../cli/src/index.ts --bundle --platform=node --target=node20 --outfile=../cli/dist/openpencil-cli.cjs --format=cjs --sourcemap --alias:@=src --define:import.meta.env={} --external:canvas --external:paper"
|
||||
}
|
||||
}
|
||||
44
apps/cli/src/commands/app.ts
Normal file
44
apps/cli/src/commands/app.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { getAppInfo } from '../connection'
|
||||
import { startDesktop, startWeb, stopApp } from '../launcher'
|
||||
import { output, outputError } from '../output'
|
||||
|
||||
export async function cmdStart(flags: {
|
||||
desktop?: boolean
|
||||
web?: boolean
|
||||
}): Promise<void> {
|
||||
try {
|
||||
let result: { port: number; pid: number }
|
||||
if (flags.web) {
|
||||
result = await startWeb()
|
||||
} else {
|
||||
result = await startDesktop()
|
||||
}
|
||||
output({ ok: true, ...result, url: `http://127.0.0.1:${result.port}` })
|
||||
} catch (err) {
|
||||
outputError((err as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdStop(): Promise<void> {
|
||||
const stopped = await stopApp()
|
||||
if (stopped) {
|
||||
output({ ok: true, message: 'OpenPencil stopped' })
|
||||
} else {
|
||||
output({ ok: true, message: 'No running instance found' })
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdStatus(): Promise<void> {
|
||||
const info = await getAppInfo()
|
||||
if (info) {
|
||||
output({
|
||||
running: true,
|
||||
port: info.port,
|
||||
pid: info.pid,
|
||||
url: info.url,
|
||||
uptime: Math.floor((Date.now() - info.timestamp) / 1000),
|
||||
})
|
||||
} else {
|
||||
output({ running: false })
|
||||
}
|
||||
}
|
||||
70
apps/cli/src/commands/design.ts
Normal file
70
apps/cli/src/commands/design.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { handleBatchDesign } from '@/mcp/tools/batch-design'
|
||||
import { handleDesignSkeleton } from '@/mcp/tools/design-skeleton'
|
||||
import { handleDesignContent } from '@/mcp/tools/design-content'
|
||||
import { handleDesignRefine } from '@/mcp/tools/design-refine'
|
||||
import { output, outputError, parseJsonArg, resolveArg } from '../output'
|
||||
|
||||
interface GlobalFlags {
|
||||
file?: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
export async function cmdDesign(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { postProcess?: boolean; canvasWidth?: string },
|
||||
): Promise<void> {
|
||||
const operations = await resolveArg(args[0])
|
||||
const result = await handleBatchDesign({
|
||||
filePath: flags.file,
|
||||
operations,
|
||||
postProcess: flags.postProcess !== false,
|
||||
canvasWidth: flags.canvasWidth ? parseInt(flags.canvasWidth, 10) : undefined,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdDesignSkeleton(
|
||||
args: string[],
|
||||
flags: GlobalFlags,
|
||||
): Promise<void> {
|
||||
const json = (await parseJsonArg(args[0])) as Record<string, unknown>
|
||||
const result = await handleDesignSkeleton({
|
||||
filePath: flags.file,
|
||||
rootFrame: json.rootFrame as any,
|
||||
sections: json.sections as any,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdDesignContent(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { canvasWidth?: string },
|
||||
): Promise<void> {
|
||||
const sectionId = args[0]
|
||||
if (!sectionId) outputError('Usage: openpencil design:content <section-id> <json>')
|
||||
const json = (await parseJsonArg(args[1])) as Record<string, unknown>
|
||||
const result = await handleDesignContent({
|
||||
filePath: flags.file,
|
||||
sectionId,
|
||||
children: json.children as any,
|
||||
canvasWidth: flags.canvasWidth ? parseInt(flags.canvasWidth, 10) : undefined,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdDesignRefine(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { rootId?: string; canvasWidth?: string },
|
||||
): Promise<void> {
|
||||
if (!flags.rootId) outputError('Usage: openpencil design:refine --root-id <id>')
|
||||
const result = await handleDesignRefine({
|
||||
filePath: flags.file,
|
||||
rootId: flags.rootId!,
|
||||
canvasWidth: flags.canvasWidth ? parseInt(flags.canvasWidth, 10) : undefined,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
57
apps/cli/src/commands/document.ts
Normal file
57
apps/cli/src/commands/document.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { handleOpenDocument } from '@/mcp/tools/open-document'
|
||||
import { handleBatchGet } from '@/mcp/tools/batch-get'
|
||||
import { handleGetSelection } from '@/mcp/tools/get-selection'
|
||||
import { openDocument, saveDocument, resolveDocPath } from '@/mcp/document-manager'
|
||||
import { output, outputError } from '../output'
|
||||
|
||||
interface GlobalFlags {
|
||||
file?: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
export async function cmdOpen(args: string[], flags: GlobalFlags): Promise<void> {
|
||||
const result = await handleOpenDocument({ filePath: flags.file ?? args[0] })
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdSave(args: string[], flags: GlobalFlags): Promise<void> {
|
||||
const target = args[0]
|
||||
if (!target) outputError('Usage: openpencil save <file.op>')
|
||||
const doc = await openDocument(resolveDocPath(flags.file))
|
||||
await saveDocument(target, doc)
|
||||
output({ ok: true, filePath: target })
|
||||
}
|
||||
|
||||
export async function cmdGet(args: string[], flags: GlobalFlags & {
|
||||
type?: string
|
||||
name?: string
|
||||
id?: string
|
||||
depth?: string
|
||||
parent?: string
|
||||
}): Promise<void> {
|
||||
const patterns: { type?: string; name?: string }[] = []
|
||||
if (flags.type || flags.name) {
|
||||
patterns.push({ type: flags.type, name: flags.name })
|
||||
}
|
||||
|
||||
const nodeIds: string[] = []
|
||||
if (flags.id) nodeIds.push(flags.id)
|
||||
|
||||
const result = await handleBatchGet({
|
||||
filePath: flags.file,
|
||||
patterns: patterns.length ? patterns : undefined,
|
||||
nodeIds: nodeIds.length ? nodeIds : undefined,
|
||||
parentId: flags.parent,
|
||||
readDepth: flags.depth ? parseInt(flags.depth, 10) : undefined,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdSelection(flags: GlobalFlags & { depth?: string }): Promise<void> {
|
||||
const result = await handleGetSelection({
|
||||
filePath: flags.file,
|
||||
readDepth: flags.depth ? parseInt(flags.depth, 10) : undefined,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
66
apps/cli/src/commands/export.ts
Normal file
66
apps/cli/src/commands/export.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { openDocument, resolveDocPath } from '@/mcp/document-manager'
|
||||
import {
|
||||
generateReactFromDocument,
|
||||
generateHTMLFromDocument,
|
||||
generateVueFromDocument,
|
||||
generateSvelteFromDocument,
|
||||
generateFlutterFromDocument,
|
||||
generateSwiftUIFromDocument,
|
||||
generateComposeFromDocument,
|
||||
generateReactNativeFromDocument,
|
||||
generateCSSVariables,
|
||||
} from '@zseven-w/pen-codegen'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { output, outputError } from '../output'
|
||||
|
||||
type GeneratorResult = string | { html: string; css: string }
|
||||
|
||||
const GENERATORS: Record<string, (doc: any) => GeneratorResult> = {
|
||||
react: generateReactFromDocument,
|
||||
html: generateHTMLFromDocument,
|
||||
vue: generateVueFromDocument,
|
||||
svelte: generateSvelteFromDocument,
|
||||
flutter: generateFlutterFromDocument,
|
||||
swiftui: generateSwiftUIFromDocument,
|
||||
compose: generateComposeFromDocument,
|
||||
rn: generateReactNativeFromDocument,
|
||||
'react-native': generateReactNativeFromDocument,
|
||||
css: (doc: any) => generateCSSVariables(doc.variables ?? {}),
|
||||
}
|
||||
|
||||
function resultToString(result: GeneratorResult): string {
|
||||
if (typeof result === 'string') return result
|
||||
// HTML generator returns { html, css }
|
||||
const parts: string[] = []
|
||||
if (result.css) parts.push(`<style>\n${result.css}\n</style>`)
|
||||
parts.push(result.html)
|
||||
return parts.join('\n\n')
|
||||
}
|
||||
|
||||
export async function cmdExport(
|
||||
args: string[],
|
||||
flags: { file?: string; out?: string },
|
||||
): Promise<void> {
|
||||
const format = args[0]
|
||||
if (!format) {
|
||||
outputError(
|
||||
`Usage: op export <format> [--out file]\nFormats: ${Object.keys(GENERATORS).join(', ')}`,
|
||||
)
|
||||
}
|
||||
const generator = GENERATORS[format]
|
||||
if (!generator) {
|
||||
outputError(`Unknown format: "${format}". Available: ${Object.keys(GENERATORS).join(', ')}`)
|
||||
}
|
||||
|
||||
const filePath = resolveDocPath(flags.file)
|
||||
const doc = await openDocument(filePath)
|
||||
const result = generator(doc)
|
||||
const code = resultToString(result)
|
||||
|
||||
if (flags.out) {
|
||||
await writeFile(flags.out, code, 'utf-8')
|
||||
output({ ok: true, format, file: flags.out, length: code.length })
|
||||
} else {
|
||||
process.stdout.write(code)
|
||||
}
|
||||
}
|
||||
48
apps/cli/src/commands/import.ts
Normal file
48
apps/cli/src/commands/import.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { handleImportSvg } from '@/mcp/tools/import-svg'
|
||||
import { openDocument, saveDocument, resolveDocPath } from '@/mcp/document-manager'
|
||||
import { parseFigFile, figmaAllPagesToPenDocument } from '@zseven-w/pen-figma'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { output, outputError } from '../output'
|
||||
|
||||
interface GlobalFlags {
|
||||
file?: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
export async function cmdImportSvg(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { parent?: string },
|
||||
): Promise<void> {
|
||||
const svgPath = args[0]
|
||||
if (!svgPath) outputError('Usage: op import:svg <file.svg>')
|
||||
const result = await handleImportSvg({
|
||||
filePath: flags.file,
|
||||
svgPath,
|
||||
parent: flags.parent ?? null,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdImportFigma(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { out?: string },
|
||||
): Promise<void> {
|
||||
const figPath = args[0]
|
||||
if (!figPath) outputError('Usage: op import:figma <file.fig> [--out output.op]')
|
||||
|
||||
const buf = await readFile(figPath)
|
||||
const figFile = parseFigFile(new Uint8Array(buf))
|
||||
const doc = figmaAllPagesToPenDocument(figFile)
|
||||
|
||||
const outPath = flags.out ?? figPath.replace(/\.fig$/, '.op')
|
||||
await saveDocument(outPath, doc)
|
||||
output({
|
||||
ok: true,
|
||||
filePath: outPath,
|
||||
pageCount: doc.pages?.length ?? 1,
|
||||
nodeCount: doc.pages
|
||||
? doc.pages.reduce((s, p) => s + p.children.length, 0)
|
||||
: doc.children.length,
|
||||
})
|
||||
}
|
||||
33
apps/cli/src/commands/layout.ts
Normal file
33
apps/cli/src/commands/layout.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { handleSnapshotLayout } from '@/mcp/tools/snapshot-layout'
|
||||
import { handleFindEmptySpace } from '@/mcp/tools/find-empty-space'
|
||||
import { output, outputError } from '../output'
|
||||
|
||||
interface GlobalFlags {
|
||||
file?: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
export async function cmdLayout(
|
||||
flags: GlobalFlags & { parent?: string; depth?: string },
|
||||
): Promise<void> {
|
||||
const result = await handleSnapshotLayout({
|
||||
filePath: flags.file,
|
||||
parentId: flags.parent,
|
||||
maxDepth: flags.depth ? parseInt(flags.depth, 10) : undefined,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdFindSpace(
|
||||
flags: GlobalFlags & { direction?: string; width?: string; height?: string },
|
||||
): Promise<void> {
|
||||
const result = await handleFindEmptySpace({
|
||||
filePath: flags.file,
|
||||
direction: (flags.direction as 'right' | 'bottom' | 'left' | 'top') ?? 'right',
|
||||
width: flags.width ? parseInt(flags.width, 10) : undefined,
|
||||
height: flags.height ? parseInt(flags.height, 10) : undefined,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
108
apps/cli/src/commands/nodes.ts
Normal file
108
apps/cli/src/commands/nodes.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import {
|
||||
handleInsertNode,
|
||||
handleUpdateNode,
|
||||
handleDeleteNode,
|
||||
handleMoveNode,
|
||||
handleCopyNode,
|
||||
handleReplaceNode,
|
||||
} from '@/mcp/tools/node-crud'
|
||||
import { output, outputError, parseJsonArg } from '../output'
|
||||
|
||||
interface GlobalFlags {
|
||||
file?: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
export async function cmdInsert(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { parent?: string; index?: string; postProcess?: boolean },
|
||||
): Promise<void> {
|
||||
const data = (await parseJsonArg(args[0])) as Record<string, unknown>
|
||||
const result = await handleInsertNode({
|
||||
filePath: flags.file,
|
||||
parent: flags.parent ?? null,
|
||||
data,
|
||||
postProcess: flags.postProcess,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdUpdate(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { postProcess?: boolean },
|
||||
): Promise<void> {
|
||||
const nodeId = args[0]
|
||||
if (!nodeId) outputError('Usage: openpencil update <node-id> <json>')
|
||||
const data = (await parseJsonArg(args[1])) as Record<string, unknown>
|
||||
const result = await handleUpdateNode({
|
||||
filePath: flags.file,
|
||||
nodeId,
|
||||
data,
|
||||
postProcess: flags.postProcess,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdDelete(
|
||||
args: string[],
|
||||
flags: GlobalFlags,
|
||||
): Promise<void> {
|
||||
const nodeId = args[0]
|
||||
if (!nodeId) outputError('Usage: openpencil delete <node-id>')
|
||||
const result = await handleDeleteNode({
|
||||
filePath: flags.file,
|
||||
nodeId,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdMove(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { parent?: string; index?: string },
|
||||
): Promise<void> {
|
||||
const nodeId = args[0]
|
||||
if (!nodeId) outputError('Usage: openpencil move <node-id> --parent <parent-id>')
|
||||
const result = await handleMoveNode({
|
||||
filePath: flags.file,
|
||||
nodeId,
|
||||
parent: flags.parent ?? null,
|
||||
index: flags.index ? parseInt(flags.index, 10) : undefined,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdCopy(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { parent?: string },
|
||||
): Promise<void> {
|
||||
const sourceId = args[0]
|
||||
if (!sourceId) outputError('Usage: openpencil copy <source-id> [--parent <parent-id>]')
|
||||
const result = await handleCopyNode({
|
||||
filePath: flags.file,
|
||||
sourceId,
|
||||
parent: flags.parent ?? null,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdReplace(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { postProcess?: boolean },
|
||||
): Promise<void> {
|
||||
const nodeId = args[0]
|
||||
if (!nodeId) outputError('Usage: openpencil replace <node-id> <json>')
|
||||
const data = (await parseJsonArg(args[1])) as Record<string, unknown>
|
||||
const result = await handleReplaceNode({
|
||||
filePath: flags.file,
|
||||
nodeId,
|
||||
data,
|
||||
postProcess: flags.postProcess,
|
||||
pageId: flags.page,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
87
apps/cli/src/commands/pages.ts
Normal file
87
apps/cli/src/commands/pages.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import {
|
||||
handleAddPage,
|
||||
handleRemovePage,
|
||||
handleRenamePage,
|
||||
handleReorderPage,
|
||||
handleDuplicatePage,
|
||||
} from '@/mcp/tools/pages'
|
||||
import { handleOpenDocument } from '@/mcp/tools/open-document'
|
||||
import { output, outputError } from '../output'
|
||||
|
||||
interface GlobalFlags {
|
||||
file?: string
|
||||
}
|
||||
|
||||
export async function cmdPageList(flags: GlobalFlags): Promise<void> {
|
||||
const result = await handleOpenDocument({ filePath: flags.file })
|
||||
output({
|
||||
pages: result.document.pages ?? [
|
||||
{ id: 'default', name: 'Page 1', childCount: result.document.childCount },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export async function cmdPageAdd(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { name?: string },
|
||||
): Promise<void> {
|
||||
const result = await handleAddPage({
|
||||
filePath: flags.file,
|
||||
name: flags.name ?? args[0],
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdPageRemove(
|
||||
args: string[],
|
||||
flags: GlobalFlags,
|
||||
): Promise<void> {
|
||||
const pageId = args[0]
|
||||
if (!pageId) outputError('Usage: op page remove <page-id>')
|
||||
const result = await handleRemovePage({
|
||||
filePath: flags.file,
|
||||
pageId,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdPageRename(
|
||||
args: string[],
|
||||
flags: GlobalFlags,
|
||||
): Promise<void> {
|
||||
const [pageId, name] = args
|
||||
if (!pageId || !name) outputError('Usage: op page rename <page-id> <name>')
|
||||
const result = await handleRenamePage({
|
||||
filePath: flags.file,
|
||||
pageId,
|
||||
name,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdPageReorder(
|
||||
args: string[],
|
||||
flags: GlobalFlags,
|
||||
): Promise<void> {
|
||||
const [pageId, indexStr] = args
|
||||
if (!pageId || !indexStr) outputError('Usage: op page reorder <page-id> <index>')
|
||||
const result = await handleReorderPage({
|
||||
filePath: flags.file,
|
||||
pageId,
|
||||
index: parseInt(indexStr, 10),
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdPageDuplicate(
|
||||
args: string[],
|
||||
flags: GlobalFlags,
|
||||
): Promise<void> {
|
||||
const pageId = args[0]
|
||||
if (!pageId) outputError('Usage: op page duplicate <page-id>')
|
||||
const result = await handleDuplicatePage({
|
||||
filePath: flags.file,
|
||||
pageId,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
81
apps/cli/src/commands/variables.ts
Normal file
81
apps/cli/src/commands/variables.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { handleGetVariables, handleSetVariables, handleSetThemes } from '@/mcp/tools/variables'
|
||||
import {
|
||||
handleSaveThemePreset,
|
||||
handleLoadThemePreset,
|
||||
handleListThemePresets,
|
||||
} from '@/mcp/tools/theme-presets'
|
||||
import { output, outputError, parseJsonArg } from '../output'
|
||||
|
||||
interface GlobalFlags {
|
||||
file?: string
|
||||
}
|
||||
|
||||
export async function cmdVars(flags: GlobalFlags): Promise<void> {
|
||||
const result = await handleGetVariables({ filePath: flags.file })
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdVarsSet(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { replace?: boolean },
|
||||
): Promise<void> {
|
||||
const data = (await parseJsonArg(args[0])) as Record<string, unknown>
|
||||
const result = await handleSetVariables({
|
||||
filePath: flags.file,
|
||||
variables: data as any,
|
||||
replace: flags.replace,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdThemes(flags: GlobalFlags): Promise<void> {
|
||||
const result = await handleGetVariables({ filePath: flags.file })
|
||||
output({ themes: result.themes })
|
||||
}
|
||||
|
||||
export async function cmdThemesSet(
|
||||
args: string[],
|
||||
flags: GlobalFlags & { replace?: boolean },
|
||||
): Promise<void> {
|
||||
const data = (await parseJsonArg(args[0])) as Record<string, unknown>
|
||||
const result = await handleSetThemes({
|
||||
filePath: flags.file,
|
||||
themes: data as any,
|
||||
replace: flags.replace,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdThemeSave(
|
||||
args: string[],
|
||||
flags: GlobalFlags,
|
||||
): Promise<void> {
|
||||
const presetPath = args[0]
|
||||
if (!presetPath) outputError('Usage: op theme:save <file.optheme>')
|
||||
const result = await handleSaveThemePreset({
|
||||
filePath: flags.file,
|
||||
presetPath,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdThemeLoad(
|
||||
args: string[],
|
||||
flags: GlobalFlags,
|
||||
): Promise<void> {
|
||||
const presetPath = args[0]
|
||||
if (!presetPath) outputError('Usage: op theme:load <file.optheme>')
|
||||
const result = await handleLoadThemePreset({
|
||||
filePath: flags.file,
|
||||
presetPath,
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
|
||||
export async function cmdThemeList(args: string[]): Promise<void> {
|
||||
if (!args[0]) outputError('Usage: op theme:list <directory>')
|
||||
const result = await handleListThemePresets({
|
||||
directory: args[0],
|
||||
})
|
||||
output(result)
|
||||
}
|
||||
57
apps/cli/src/connection.ts
Normal file
57
apps/cli/src/connection.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/** Port file discovery and app health check. */
|
||||
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
|
||||
const PORT_FILE_DIR = '.openpencil'
|
||||
const PORT_FILE_NAME = '.port'
|
||||
const PORT_FILE_PATH = join(homedir(), PORT_FILE_DIR, PORT_FILE_NAME)
|
||||
|
||||
function isPidAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0)
|
||||
return true
|
||||
} catch (err: unknown) {
|
||||
return (err as NodeJS.ErrnoException).code === 'EPERM'
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppInfo {
|
||||
port: number
|
||||
pid: number
|
||||
timestamp: number
|
||||
url: string
|
||||
}
|
||||
|
||||
/** Read port file and return app info, or null if no running instance. */
|
||||
export async function getAppInfo(): Promise<AppInfo | null> {
|
||||
try {
|
||||
const raw = await readFile(PORT_FILE_PATH, 'utf-8')
|
||||
const { port, pid, timestamp } = JSON.parse(raw) as {
|
||||
port: number
|
||||
pid: number
|
||||
timestamp: number
|
||||
}
|
||||
if (!isPidAlive(pid)) return null
|
||||
return { port, pid, timestamp, url: `http://127.0.0.1:${port}` }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Get app URL or throw if not running. */
|
||||
export async function requireApp(): Promise<string> {
|
||||
const info = await getAppInfo()
|
||||
if (!info) {
|
||||
throw new Error(
|
||||
'No running OpenPencil instance found. Run `openpencil start` first.',
|
||||
)
|
||||
}
|
||||
return info.url
|
||||
}
|
||||
|
||||
/** Quick check if app is running. */
|
||||
export async function isAppRunning(): Promise<boolean> {
|
||||
return (await getAppInfo()) !== null
|
||||
}
|
||||
403
apps/cli/src/index.ts
Normal file
403
apps/cli/src/index.ts
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import pkg from '../package.json'
|
||||
import { setPretty, output, outputError } from './output'
|
||||
|
||||
// --- Arg parsing ---
|
||||
|
||||
interface ParsedArgs {
|
||||
command: string
|
||||
positionals: string[]
|
||||
flags: Record<string, string | boolean>
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): ParsedArgs {
|
||||
const args = argv.slice(2)
|
||||
const positionals: string[] = []
|
||||
const flags: Record<string, string | boolean> = {}
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i]
|
||||
if (arg.startsWith('--')) {
|
||||
const key = arg.slice(2)
|
||||
const next = args[i + 1]
|
||||
if (next && !next.startsWith('--')) {
|
||||
flags[key] = next
|
||||
i++
|
||||
} else {
|
||||
flags[key] = true
|
||||
}
|
||||
} else {
|
||||
positionals.push(arg)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
command: positionals[0] ?? '',
|
||||
positionals: positionals.slice(1),
|
||||
flags,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Help ---
|
||||
|
||||
const HELP = `OpenPencil CLI v${pkg.version}
|
||||
|
||||
Usage: op <command> [options]
|
||||
|
||||
App:
|
||||
op start [--desktop|--web] Launch OpenPencil
|
||||
op stop Stop running instance
|
||||
op status Check if running
|
||||
|
||||
Document:
|
||||
op open [file.op] Open file or connect to live canvas
|
||||
op save <file.op> Save current document to file
|
||||
op get [--type X] [--name Y] [--id Z] [--depth N]
|
||||
op selection Get current canvas selection
|
||||
|
||||
Nodes:
|
||||
op insert <json> [--parent P] [--index N] [--post-process]
|
||||
op update <id> <json> [--post-process]
|
||||
op delete <id>
|
||||
op move <id> --parent <P> [--index N]
|
||||
op copy <id> [--parent P]
|
||||
op replace <id> <json> [--post-process]
|
||||
|
||||
Design:
|
||||
op design <dsl|@file|->
|
||||
op design:skeleton <json|@file|->
|
||||
op design:content <section-id> <json|@file|->
|
||||
op design:refine --root-id <id>
|
||||
|
||||
Export:
|
||||
op export <format> [--out file]
|
||||
Formats: react, html, vue, svelte, flutter, swiftui, compose, rn, css
|
||||
|
||||
Variables & Themes:
|
||||
op vars Get variables
|
||||
op vars:set <json> Set variables
|
||||
op themes Get themes
|
||||
op themes:set <json> Set themes
|
||||
op theme:save <file.optheme> Save theme preset
|
||||
op theme:load <file.optheme> Load theme preset
|
||||
op theme:list [dir] List theme presets
|
||||
|
||||
Pages:
|
||||
op page list
|
||||
op page add [--name N]
|
||||
op page remove <id>
|
||||
op page rename <id> <name>
|
||||
op page reorder <id> <index>
|
||||
op page duplicate <id>
|
||||
|
||||
Import:
|
||||
op import:svg <file.svg> Import SVG file
|
||||
op import:figma <file.fig> Import Figma file
|
||||
|
||||
Layout:
|
||||
op layout [--parent P] [--depth N]
|
||||
op find-space [--direction right|bottom|left|top]
|
||||
|
||||
Arguments that accept JSON or DSL can be passed as:
|
||||
<value> Inline string
|
||||
@filepath Read from file (e.g. @design.txt)
|
||||
- Read from stdin (e.g. cat design.txt | op design -)
|
||||
|
||||
Global Flags:
|
||||
--file <path> Target .op file (default: live canvas)
|
||||
--page <id> Target page ID
|
||||
--pretty Human-readable JSON output
|
||||
--help Show this help
|
||||
--version Show version
|
||||
`
|
||||
|
||||
// --- Main ---
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { command, positionals, flags } = parseArgs(process.argv)
|
||||
|
||||
if (flags.pretty) setPretty(true)
|
||||
if (flags.help || command === 'help') {
|
||||
process.stdout.write(HELP)
|
||||
return
|
||||
}
|
||||
if (flags.version || command === 'version') {
|
||||
output({ version: pkg.version })
|
||||
return
|
||||
}
|
||||
if (!command) {
|
||||
process.stdout.write(HELP)
|
||||
return
|
||||
}
|
||||
|
||||
const globalFlags = {
|
||||
file: flags.file as string | undefined,
|
||||
page: flags.page as string | undefined,
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
// --- App ---
|
||||
case 'start': {
|
||||
const { cmdStart } = await import('./commands/app')
|
||||
await cmdStart({ desktop: !!flags.desktop, web: !!flags.web })
|
||||
break
|
||||
}
|
||||
case 'stop': {
|
||||
const { cmdStop } = await import('./commands/app')
|
||||
await cmdStop()
|
||||
break
|
||||
}
|
||||
case 'status': {
|
||||
const { cmdStatus } = await import('./commands/app')
|
||||
await cmdStatus()
|
||||
break
|
||||
}
|
||||
|
||||
// --- Document ---
|
||||
case 'open': {
|
||||
const { cmdOpen } = await import('./commands/document')
|
||||
await cmdOpen(positionals, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'save': {
|
||||
const { cmdSave } = await import('./commands/document')
|
||||
await cmdSave(positionals, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'get': {
|
||||
const { cmdGet } = await import('./commands/document')
|
||||
await cmdGet(positionals, {
|
||||
...globalFlags,
|
||||
type: flags.type as string | undefined,
|
||||
name: flags.name as string | undefined,
|
||||
id: flags.id as string | undefined,
|
||||
depth: flags.depth as string | undefined,
|
||||
parent: flags.parent as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'selection': {
|
||||
const { cmdSelection } = await import('./commands/document')
|
||||
await cmdSelection({ ...globalFlags, depth: flags.depth as string | undefined })
|
||||
break
|
||||
}
|
||||
|
||||
// --- Nodes ---
|
||||
case 'insert': {
|
||||
const { cmdInsert } = await import('./commands/nodes')
|
||||
await cmdInsert(positionals, {
|
||||
...globalFlags,
|
||||
parent: flags.parent as string | undefined,
|
||||
index: flags.index as string | undefined,
|
||||
postProcess: !!flags['post-process'],
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'update': {
|
||||
const { cmdUpdate } = await import('./commands/nodes')
|
||||
await cmdUpdate(positionals, {
|
||||
...globalFlags,
|
||||
postProcess: !!flags['post-process'],
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
const { cmdDelete } = await import('./commands/nodes')
|
||||
await cmdDelete(positionals, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'move': {
|
||||
const { cmdMove } = await import('./commands/nodes')
|
||||
await cmdMove(positionals, {
|
||||
...globalFlags,
|
||||
parent: flags.parent as string | undefined,
|
||||
index: flags.index as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'copy': {
|
||||
const { cmdCopy } = await import('./commands/nodes')
|
||||
await cmdCopy(positionals, {
|
||||
...globalFlags,
|
||||
parent: flags.parent as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'replace': {
|
||||
const { cmdReplace } = await import('./commands/nodes')
|
||||
await cmdReplace(positionals, {
|
||||
...globalFlags,
|
||||
postProcess: !!flags['post-process'],
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// --- Design ---
|
||||
case 'design': {
|
||||
const { cmdDesign } = await import('./commands/design')
|
||||
await cmdDesign(positionals, {
|
||||
...globalFlags,
|
||||
postProcess: flags['post-process'] !== false ? true : undefined,
|
||||
canvasWidth: flags['canvas-width'] as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'design:skeleton': {
|
||||
const { cmdDesignSkeleton } = await import('./commands/design')
|
||||
await cmdDesignSkeleton(positionals, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'design:content': {
|
||||
const { cmdDesignContent } = await import('./commands/design')
|
||||
await cmdDesignContent(positionals, {
|
||||
...globalFlags,
|
||||
canvasWidth: flags['canvas-width'] as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'design:refine': {
|
||||
const { cmdDesignRefine } = await import('./commands/design')
|
||||
await cmdDesignRefine(positionals, {
|
||||
...globalFlags,
|
||||
rootId: flags['root-id'] as string | undefined,
|
||||
canvasWidth: flags['canvas-width'] as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// --- Export ---
|
||||
case 'export': {
|
||||
const { cmdExport } = await import('./commands/export')
|
||||
await cmdExport(positionals, {
|
||||
file: globalFlags.file,
|
||||
out: flags.out as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// --- Variables & Themes ---
|
||||
case 'vars': {
|
||||
const { cmdVars } = await import('./commands/variables')
|
||||
await cmdVars(globalFlags)
|
||||
break
|
||||
}
|
||||
case 'vars:set': {
|
||||
const { cmdVarsSet } = await import('./commands/variables')
|
||||
await cmdVarsSet(positionals, { ...globalFlags, replace: !!flags.replace })
|
||||
break
|
||||
}
|
||||
case 'themes': {
|
||||
const { cmdThemes } = await import('./commands/variables')
|
||||
await cmdThemes(globalFlags)
|
||||
break
|
||||
}
|
||||
case 'themes:set': {
|
||||
const { cmdThemesSet } = await import('./commands/variables')
|
||||
await cmdThemesSet(positionals, { ...globalFlags, replace: !!flags.replace })
|
||||
break
|
||||
}
|
||||
case 'theme:save': {
|
||||
const { cmdThemeSave } = await import('./commands/variables')
|
||||
await cmdThemeSave(positionals, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'theme:load': {
|
||||
const { cmdThemeLoad } = await import('./commands/variables')
|
||||
await cmdThemeLoad(positionals, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'theme:list': {
|
||||
const { cmdThemeList } = await import('./commands/variables')
|
||||
await cmdThemeList(positionals)
|
||||
break
|
||||
}
|
||||
|
||||
// --- Pages ---
|
||||
case 'page': {
|
||||
const subCmd = positionals[0]
|
||||
const subArgs = positionals.slice(1)
|
||||
switch (subCmd) {
|
||||
case 'list': {
|
||||
const { cmdPageList } = await import('./commands/pages')
|
||||
await cmdPageList(globalFlags)
|
||||
break
|
||||
}
|
||||
case 'add': {
|
||||
const { cmdPageAdd } = await import('./commands/pages')
|
||||
await cmdPageAdd(subArgs, { ...globalFlags, name: flags.name as string | undefined })
|
||||
break
|
||||
}
|
||||
case 'remove': {
|
||||
const { cmdPageRemove } = await import('./commands/pages')
|
||||
await cmdPageRemove(subArgs, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'rename': {
|
||||
const { cmdPageRename } = await import('./commands/pages')
|
||||
await cmdPageRename(subArgs, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'reorder': {
|
||||
const { cmdPageReorder } = await import('./commands/pages')
|
||||
await cmdPageReorder(subArgs, globalFlags)
|
||||
break
|
||||
}
|
||||
case 'duplicate': {
|
||||
const { cmdPageDuplicate } = await import('./commands/pages')
|
||||
await cmdPageDuplicate(subArgs, globalFlags)
|
||||
break
|
||||
}
|
||||
default:
|
||||
outputError(`Unknown page subcommand: "${subCmd}". Use: list, add, remove, rename, reorder, duplicate`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// --- Import ---
|
||||
case 'import:svg': {
|
||||
const { cmdImportSvg } = await import('./commands/import')
|
||||
await cmdImportSvg(positionals, {
|
||||
...globalFlags,
|
||||
parent: flags.parent as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'import:figma': {
|
||||
const { cmdImportFigma } = await import('./commands/import')
|
||||
await cmdImportFigma(positionals, {
|
||||
...globalFlags,
|
||||
out: flags.out as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// --- Layout ---
|
||||
case 'layout': {
|
||||
const { cmdLayout } = await import('./commands/layout')
|
||||
await cmdLayout({
|
||||
...globalFlags,
|
||||
parent: flags.parent as string | undefined,
|
||||
depth: flags.depth as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'find-space': {
|
||||
const { cmdFindSpace } = await import('./commands/layout')
|
||||
await cmdFindSpace({
|
||||
...globalFlags,
|
||||
direction: flags.direction as string | undefined,
|
||||
width: flags.width as string | undefined,
|
||||
height: flags.height as string | undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
outputError(`Unknown command: "${command}". Run "op --help" for usage.`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
outputError(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
203
apps/cli/src/launcher.ts
Normal file
203
apps/cli/src/launcher.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/** Start/stop OpenPencil app from the CLI. */
|
||||
|
||||
import { spawn, fork, execSync } from 'node:child_process'
|
||||
import { createServer } from 'node:net'
|
||||
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { getAppInfo } from './connection'
|
||||
|
||||
const IS_WIN = process.platform === 'win32'
|
||||
const PORT_FILE_DIR = join(homedir(), '.openpencil')
|
||||
const PORT_FILE_PATH = join(PORT_FILE_DIR, '.port')
|
||||
|
||||
function getFreePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer()
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const addr = server.address()
|
||||
if (addr && typeof addr === 'object') {
|
||||
const { port } = addr
|
||||
server.close(() => resolve(port))
|
||||
} else {
|
||||
reject(new Error('Failed to get free port'))
|
||||
}
|
||||
})
|
||||
server.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForPortFile(timeoutMs = 15_000): Promise<{ port: number; pid: number }> {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const raw = await readFile(PORT_FILE_PATH, 'utf-8')
|
||||
const data = JSON.parse(raw)
|
||||
if (data.port && data.pid) return data
|
||||
} catch {
|
||||
// not ready yet
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 300))
|
||||
}
|
||||
throw new Error('Timeout waiting for OpenPencil to start')
|
||||
}
|
||||
|
||||
/** Find the installed desktop app binary. */
|
||||
function findDesktopBinary(): string | null {
|
||||
const candidates: string[] = []
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
candidates.push('/Applications/OpenPencil.app/Contents/MacOS/OpenPencil')
|
||||
candidates.push(
|
||||
join(homedir(), 'Applications', 'OpenPencil.app', 'Contents', 'MacOS', 'OpenPencil'),
|
||||
)
|
||||
} else if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local')
|
||||
const programFiles = process.env.PROGRAMFILES ?? 'C:\\Program Files'
|
||||
const programFilesX86 = process.env['PROGRAMFILES(X86)'] ?? 'C:\\Program Files (x86)'
|
||||
// NSIS per-user install (default)
|
||||
candidates.push(join(localAppData, 'Programs', 'openpencil', 'OpenPencil.exe'))
|
||||
// NSIS per-machine install
|
||||
candidates.push(join(programFiles, 'OpenPencil', 'OpenPencil.exe'))
|
||||
candidates.push(join(programFilesX86, 'OpenPencil', 'OpenPencil.exe'))
|
||||
// Portable — same directory as CLI
|
||||
candidates.push(join(__dirname, '..', 'OpenPencil.exe'))
|
||||
} else {
|
||||
// Linux — AppImage, deb, snap, flatpak, manual
|
||||
candidates.push('/usr/bin/openpencil')
|
||||
candidates.push('/usr/local/bin/openpencil')
|
||||
candidates.push(join(homedir(), '.local', 'bin', 'openpencil'))
|
||||
// AppImage in common download locations
|
||||
const appImageDirs = [
|
||||
join(homedir(), 'Applications'),
|
||||
join(homedir(), 'Downloads'),
|
||||
join(homedir(), '.local', 'share', 'applications'),
|
||||
]
|
||||
for (const dir of appImageDirs) {
|
||||
// Match OpenPencil*.AppImage (version may vary)
|
||||
try {
|
||||
if (existsSync(dir)) {
|
||||
const files = require('node:fs').readdirSync(dir) as string[]
|
||||
const appImage = files.find(
|
||||
(f: string) => f.startsWith('OpenPencil') && f.endsWith('.AppImage'),
|
||||
)
|
||||
if (appImage) candidates.push(join(dir, appImage))
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
// Snap
|
||||
candidates.push('/snap/bin/openpencil')
|
||||
// Flatpak
|
||||
candidates.push('/var/lib/flatpak/exports/bin/dev.openpencil.app')
|
||||
candidates.push(join(homedir(), '.local', 'share', 'flatpak', 'exports', 'bin', 'dev.openpencil.app'))
|
||||
}
|
||||
|
||||
for (const path of candidates) {
|
||||
if (existsSync(path)) return path
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Find the Nitro server entry relative to CLI's location. */
|
||||
function findServerEntry(): string | null {
|
||||
// When compiled, __dirname points to dist/
|
||||
// Server is at ../../out/web/server/index.mjs or relative to monorepo root
|
||||
const candidates = [
|
||||
join(__dirname, '..', '..', '..', 'out', 'web', 'server', 'index.mjs'),
|
||||
join(__dirname, '..', '..', 'out', 'web', 'server', 'index.mjs'),
|
||||
join(__dirname, '..', 'server', 'index.mjs'), // when bundled in Electron resources
|
||||
]
|
||||
for (const path of candidates) {
|
||||
if (existsSync(path)) return path
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function startDesktop(): Promise<{ port: number; pid: number }> {
|
||||
const info = await getAppInfo()
|
||||
if (info) return { port: info.port, pid: info.pid }
|
||||
|
||||
const binary = findDesktopBinary()
|
||||
if (!binary) {
|
||||
throw new Error(
|
||||
'OpenPencil desktop app not found. Install it or use `op start --web` for the web server.',
|
||||
)
|
||||
}
|
||||
|
||||
const child = spawn(binary, [], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
...(IS_WIN ? { windowsHide: true, shell: false } : {}),
|
||||
})
|
||||
child.unref()
|
||||
|
||||
return waitForPortFile()
|
||||
}
|
||||
|
||||
export async function startWeb(): Promise<{ port: number; pid: number }> {
|
||||
const info = await getAppInfo()
|
||||
if (info) return { port: info.port, pid: info.pid }
|
||||
|
||||
const entry = findServerEntry()
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
'Nitro server not found. Run `bun run build` first, or use `op start --desktop`.',
|
||||
)
|
||||
}
|
||||
|
||||
const port = await getFreePort()
|
||||
|
||||
const child = fork(entry, [], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
...(IS_WIN ? { windowsHide: true } : {}),
|
||||
env: {
|
||||
...process.env,
|
||||
HOST: '127.0.0.1',
|
||||
PORT: String(port),
|
||||
NITRO_HOST: '127.0.0.1',
|
||||
NITRO_PORT: String(port),
|
||||
},
|
||||
})
|
||||
child.unref()
|
||||
|
||||
// Write port file (the Nitro plugin also writes it, but write early for faster discovery)
|
||||
await mkdir(PORT_FILE_DIR, { recursive: true })
|
||||
await writeFile(
|
||||
PORT_FILE_PATH,
|
||||
JSON.stringify({ port, pid: child.pid, timestamp: Date.now() }),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
return { port, pid: child.pid! }
|
||||
}
|
||||
|
||||
export async function stopApp(): Promise<boolean> {
|
||||
const info = await getAppInfo()
|
||||
if (!info) return false
|
||||
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
// Windows: SIGTERM is not supported, use taskkill for graceful shutdown
|
||||
execSync(`taskkill /PID ${info.pid}`, { stdio: 'ignore' })
|
||||
} else {
|
||||
process.kill(info.pid, 'SIGTERM')
|
||||
}
|
||||
} catch {
|
||||
// already dead — try force kill on Windows
|
||||
if (IS_WIN) {
|
||||
try {
|
||||
execSync(`taskkill /F /PID ${info.pid}`, { stdio: 'ignore' })
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await unlink(PORT_FILE_PATH)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
73
apps/cli/src/output.ts
Normal file
73
apps/cli/src/output.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/** Output formatting and process exit helpers for the CLI. */
|
||||
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
let prettyMode = false
|
||||
|
||||
export function setPretty(v: boolean): void {
|
||||
prettyMode = v
|
||||
}
|
||||
|
||||
export function output(data: unknown): void {
|
||||
const json = prettyMode
|
||||
? JSON.stringify(data, null, 2)
|
||||
: JSON.stringify(data)
|
||||
process.stdout.write(json + '\n')
|
||||
}
|
||||
|
||||
export function outputSuccess(data: Record<string, unknown> = {}): void {
|
||||
output({ ok: true, ...data })
|
||||
}
|
||||
|
||||
export function outputError(message: string, code = 1): never {
|
||||
process.stderr.write(
|
||||
JSON.stringify({ error: message }) + '\n',
|
||||
)
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
/** Read all of stdin when not a TTY (piped input). */
|
||||
export async function readStdin(): Promise<string> {
|
||||
if (process.stdin.isTTY) return ''
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of process.stdin) chunks.push(chunk as Buffer)
|
||||
return Buffer.concat(chunks).toString('utf-8')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a CLI argument that may be:
|
||||
* - `@filepath` → read file contents
|
||||
* - `-` → read from stdin
|
||||
* - raw string → use as-is
|
||||
* - undefined → fall back to stdin (if piped)
|
||||
*/
|
||||
export async function resolveArg(arg: string | undefined): Promise<string> {
|
||||
if (arg === '-') {
|
||||
const stdin = await readStdin()
|
||||
if (!stdin.trim()) outputError('No data received from stdin.')
|
||||
return stdin.trim()
|
||||
}
|
||||
if (arg && arg.startsWith('@')) {
|
||||
const filePath = arg.slice(1)
|
||||
try {
|
||||
return (await readFile(filePath, 'utf-8')).trim()
|
||||
} catch {
|
||||
outputError(`Cannot read file: ${filePath}`)
|
||||
}
|
||||
}
|
||||
if (arg) return arg
|
||||
// No explicit arg — try stdin if piped
|
||||
const stdin = await readStdin()
|
||||
if (stdin.trim()) return stdin.trim()
|
||||
outputError('No data provided. Pass as argument, @filepath, or pipe via stdin.')
|
||||
}
|
||||
|
||||
/** Parse JSON from a CLI positional arg, @filepath, or stdin. */
|
||||
export async function parseJsonArg(arg: string | undefined): Promise<unknown> {
|
||||
const raw = await resolveArg(arg)
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
outputError(`Invalid JSON: ${raw.slice(0, 200)}...`)
|
||||
}
|
||||
}
|
||||
19
apps/cli/tsconfig.json
Normal file
19
apps/cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"paths": {
|
||||
"@/*": ["../../apps/web/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": []
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@zseven-w/desktop",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@zseven-w/web",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
buildSpawnClaudeCodeProcess,
|
||||
getClaudeAgentDebugFilePath,
|
||||
} from '../../utils/resolve-claude-agent-env'
|
||||
|
||||
/** Pattern for detecting sensitive data in debug log output */
|
||||
export const SENSITIVE_LOG_PATTERN = /ANTHROPIC_API_KEY=|Authorization:\s*Bearer|api[_-]?key\s*[:=]/i
|
||||
|
||||
|
|
@ -51,26 +50,68 @@ async function readDebugTail(path?: string, maxLines = 40): Promise<string[] | u
|
|||
|
||||
function buildClaudeExitHint(rawError: string, debugTail?: string[]): string | undefined {
|
||||
if (!/process exited with code 1/i.test(rawError)) return undefined
|
||||
if (!debugTail || debugTail.length === 0) return undefined
|
||||
const text = debugTail.join('\n')
|
||||
|
||||
const hints: string[] = []
|
||||
if (/Failed to save config with lock: Error: EPERM|operation not permitted, .*\.claude\.json/i.test(text)) {
|
||||
hints.push('Claude Code cannot write ~/.claude.json in the current runtime (permission denied).')
|
||||
|
||||
if (debugTail && debugTail.length > 0) {
|
||||
const text = debugTail.join('\n')
|
||||
if (/Failed to save config with lock: Error: EPERM|operation not permitted, .*\.claude\.json/i.test(text)) {
|
||||
hints.push(
|
||||
'Claude Code cannot write ~/.claude.json (permission denied). ' +
|
||||
'On Windows, try running as Administrator or manually create the file: echo {} > %USERPROFILE%\\.claude.json',
|
||||
)
|
||||
}
|
||||
if (/Connection error|Could not resolve host|Failed to connect|ECONNREFUSED|ETIMEDOUT/i.test(text)) {
|
||||
hints.push('Upstream API connection failed. Check proxy/DNS/network reachability to your ANTHROPIC_BASE_URL.')
|
||||
}
|
||||
if (/ANTHROPIC_CUSTOM_HEADERS present: false, has Authorization header: false/i.test(text)) {
|
||||
hints.push(
|
||||
'No API auth header detected. Run "claude login" to authenticate, ' +
|
||||
'or set ANTHROPIC_API_KEY in ~/.claude/settings.json ' +
|
||||
'(env: { "ANTHROPIC_API_KEY": "sk-..." }).',
|
||||
)
|
||||
}
|
||||
if (/invalid.*api.?key|unauthorized|401|authentication/i.test(text)) {
|
||||
hints.push('API key authentication failed. Verify your ANTHROPIC_API_KEY is correct and has not expired.')
|
||||
}
|
||||
if (/ENOTFOUND|getaddrinfo/i.test(text)) {
|
||||
hints.push('DNS resolution failed for the API endpoint. Check your ANTHROPIC_BASE_URL is correct.')
|
||||
}
|
||||
if (/certificate|CERT_|ssl|tls/i.test(text)) {
|
||||
hints.push(
|
||||
'TLS/SSL certificate error. If using a corporate proxy, set NODE_TLS_REJECT_UNAUTHORIZED=0 in ~/.claude/settings.json env (not recommended for production).',
|
||||
)
|
||||
}
|
||||
}
|
||||
if (/Connection error|Could not resolve host|Failed to connect/i.test(text)) {
|
||||
hints.push('Upstream API connection failed (check proxy/DNS/network reachability to your ANTHROPIC_BASE_URL).')
|
||||
}
|
||||
if (/ANTHROPIC_CUSTOM_HEADERS present: false, has Authorization header: false/i.test(text)) {
|
||||
|
||||
// Detect proxy/custom endpoint — most common cause of exit-code-1 on Windows
|
||||
const env = process.env
|
||||
const hasProxy = !!(env.http_proxy || env.https_proxy || env.HTTP_PROXY || env.HTTPS_PROXY)
|
||||
const hasCustomBaseUrl = !!env.ANTHROPIC_BASE_URL
|
||||
if ((hasProxy || hasCustomBaseUrl) && !env.NODE_TLS_REJECT_UNAUTHORIZED) {
|
||||
hints.push(
|
||||
'No API auth header detected. Run "claude login" to authenticate, ' +
|
||||
'or set ANTHROPIC_API_KEY in ~/.claude/settings.json ' +
|
||||
'(env: { "ANTHROPIC_API_KEY": "sk-..." }).',
|
||||
'Proxy or custom ANTHROPIC_BASE_URL detected but NODE_TLS_REJECT_UNAUTHORIZED is not set. ' +
|
||||
'If your proxy uses a self-signed or corporate certificate, add ' +
|
||||
'"NODE_TLS_REJECT_UNAUTHORIZED": "0" to the env section of ~/.claude/settings.json.',
|
||||
)
|
||||
}
|
||||
|
||||
if (hints.length === 0) return undefined
|
||||
return `${rawError}\n${hints.join(' ')}`
|
||||
// If no debug info available, provide generic Windows guidance
|
||||
if (hints.length === 0) {
|
||||
const isWin = process.platform === 'win32'
|
||||
if (isWin) {
|
||||
hints.push(
|
||||
'Claude Code process crashed on Windows. Common fixes: ' +
|
||||
'(1) Ensure ~/.claude.json exists: echo {} > %USERPROFILE%\\.claude.json ' +
|
||||
'(2) Check ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL in ~/.claude/settings.json ' +
|
||||
'(3) If using a proxy, set NODE_TLS_REJECT_UNAUTHORIZED=0 in env.',
|
||||
)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return `${rawError}\n${hints.join('\n')}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -210,6 +251,7 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
|
|||
debugFile = getClaudeAgentDebugFilePath()
|
||||
|
||||
const claudePath = resolveClaudeCli()
|
||||
const spawnProcess = buildSpawnClaudeCodeProcess()
|
||||
const thinking = getAgentThinkingConfig(body)
|
||||
|
||||
// When images are attached, strip the "NEVER use tools" restriction from
|
||||
|
|
@ -238,7 +280,7 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
|
|||
env,
|
||||
...(debugFile ? { debugFile } : {}),
|
||||
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
|
||||
...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}),
|
||||
...(spawnProcess ? { spawnClaudeCodeProcess: spawnProcess } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -288,7 +330,7 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
|
|||
env,
|
||||
...(debugFile ? { debugFile } : {}),
|
||||
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
|
||||
...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}),
|
||||
...(spawnProcess ? { spawnClaudeCodeProcess: spawnProcess } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -332,9 +374,16 @@ function streamViaAgentSDK(body: ChatBody, model?: string) {
|
|||
)
|
||||
} catch (error) {
|
||||
const rawContent = error instanceof Error ? error.message : 'Unknown error'
|
||||
|
||||
const tail = await readDebugTail(debugFile)
|
||||
|
||||
const hintedContent = buildClaudeExitHint(rawContent, tail)
|
||||
const content = hintedContent ?? rawContent
|
||||
// Append debug log tail so the user can see what Claude Code actually reported
|
||||
let content = hintedContent ?? rawContent
|
||||
if (tail && tail.length > 0 && /process exited with code/i.test(rawContent)) {
|
||||
const debugSnippet = tail.slice(-10).join('\n')
|
||||
content += `\n\n[Debug log]:\n${debugSnippet}`
|
||||
}
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ type: 'error', content })}\n\n`),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { defineEventHandler, readBody, setResponseHeaders } from 'h3'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { GroupedModel } from '../../../src/types/agent-settings'
|
||||
import { resolveClaudeCli } from '../../utils/resolve-claude-cli'
|
||||
|
|
@ -121,6 +121,8 @@ async function connectClaudeCode(): Promise<ConnectResult> {
|
|||
serverLog.info(`[connect-agent] claude env keys: ${Object.keys(env).join(', ')}`)
|
||||
serverLog.info(`[connect-agent] claude debugFile: ${debugFile ?? 'none'}`)
|
||||
|
||||
const spawnProcess = buildSpawnClaudeCodeProcess()
|
||||
|
||||
const q = query({
|
||||
prompt: '',
|
||||
options: {
|
||||
|
|
@ -131,7 +133,7 @@ async function connectClaudeCode(): Promise<ConnectResult> {
|
|||
env,
|
||||
...(debugFile ? { debugFile } : {}),
|
||||
...(claudePath ? { pathToClaudeCodeExecutable: claudePath } : {}),
|
||||
...(buildSpawnClaudeCodeProcess() ? { spawnClaudeCodeProcess: buildSpawnClaudeCodeProcess() } : {}),
|
||||
...(spawnProcess ? { spawnClaudeCodeProcess: spawnProcess } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -168,7 +170,34 @@ async function connectClaudeCode(): Promise<ConnectResult> {
|
|||
serverLog.info('[connect-agent] using fallback model list (proxy detected)')
|
||||
const fallbackEnv = buildClaudeAgentEnv()
|
||||
const claudeInfo = buildClaudeConnectionInfo(fallbackEnv, null)
|
||||
return { connected: true, models: FALLBACK_CLAUDE_MODELS, ...claudeInfo }
|
||||
|
||||
// Read debug log for diagnostic warning — the process may have written
|
||||
// useful info (e.g. TLS errors, auth failures) before exiting
|
||||
let warning: string | undefined
|
||||
const debugPath = getClaudeAgentDebugFilePath()
|
||||
if (debugPath) {
|
||||
try {
|
||||
const raw = readFileSync(debugPath, 'utf-8')
|
||||
const lines = raw.split('\n').filter((l) => l.trim().length > 0)
|
||||
const tail = lines.slice(-10).join('\n')
|
||||
if (tail) {
|
||||
// Surface specific issues as warnings
|
||||
if (/certificate|CERT_|ssl|tls/i.test(tail)) {
|
||||
warning = 'TLS/SSL error detected. If using a proxy, add "NODE_TLS_REJECT_UNAUTHORIZED": "0" to ~/.claude/settings.json env.'
|
||||
} else if (/EPERM|operation not permitted/i.test(tail)) {
|
||||
warning = 'Permission error writing config. Try: echo {} > %USERPROFILE%\\.claude.json'
|
||||
} else if (/stderr exit=/i.test(tail)) {
|
||||
// Show captured stderr
|
||||
const stderrMatch = tail.match(/\[stderr exit=\d+\]\s*(.+)/s)
|
||||
if (stderrMatch) {
|
||||
warning = `Claude Code stderr: ${stderrMatch[1].slice(0, 300)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* debug file not available */ }
|
||||
}
|
||||
|
||||
return { connected: true, models: FALLBACK_CLAUDE_MODELS, ...claudeInfo, ...(warning ? { warning } : {}) }
|
||||
}
|
||||
return { connected: false, models: [], error: friendlyClaudeError(msg) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { spawn } from 'node:child_process'
|
||||
import { mkdirSync, readFileSync } from 'node:fs'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { mkdirSync, readFileSync, writeFileSync, existsSync, appendFileSync } from 'node:fs'
|
||||
import { homedir, tmpdir, platform } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const IS_WIN = platform() === 'win32'
|
||||
|
||||
type EnvLike = Record<string, string | undefined>
|
||||
|
||||
interface ClaudeSettings {
|
||||
|
|
@ -72,11 +74,41 @@ function isValidJson(str: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On Windows, Claude Code SDK may fail with EPERM when writing to ~/.claude.json
|
||||
* or ~/.claude/ config files. Ensure the directory and config file exist and are writable.
|
||||
*/
|
||||
function ensureClaudeConfigWritable(): void {
|
||||
if (!IS_WIN) return
|
||||
try {
|
||||
const claudeDir = join(homedir(), '.claude')
|
||||
mkdirSync(claudeDir, { recursive: true })
|
||||
// Ensure .claude.json exists — Claude SDK crashes if it can't write/lock it
|
||||
const configFile = join(homedir(), '.claude.json')
|
||||
if (!existsSync(configFile)) {
|
||||
writeFileSync(configFile, '{}', 'utf-8')
|
||||
}
|
||||
// Ensure credentials.json exists — SDK may crash trying to read/write it
|
||||
const credFile = join(claudeDir, 'credentials.json')
|
||||
if (!existsSync(credFile)) {
|
||||
writeFileSync(credFile, '{}', 'utf-8')
|
||||
}
|
||||
// Ensure statsig/ cache dir exists — SDK crashes writing feature gate cache
|
||||
const statsigDir = join(claudeDir, 'statsig')
|
||||
mkdirSync(statsigDir, { recursive: true })
|
||||
} catch {
|
||||
// Best effort — if we can't fix it, the SDK error hint will guide the user
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build env passed to Claude Agent SDK.
|
||||
* Priority: current process env > ~/.claude/settings.json env.
|
||||
*/
|
||||
export function buildClaudeAgentEnv(): EnvLike {
|
||||
// On Windows, pre-create config files to avoid EPERM errors
|
||||
ensureClaudeConfigWritable()
|
||||
|
||||
const fromSettings = readClaudeSettingsEnv()
|
||||
const fromProcess = process.env as EnvLike
|
||||
|
||||
|
|
@ -102,6 +134,50 @@ export function buildClaudeAgentEnv(): EnvLike {
|
|||
// Running inside Claude terminal can break nested Claude invocations.
|
||||
delete merged.CLAUDECODE
|
||||
|
||||
// Remove Electron-specific env vars that may confuse spawned CLI processes
|
||||
delete merged.ELECTRON_RUN_AS_NODE
|
||||
delete merged.ELECTRON_RESOURCES_PATH
|
||||
delete merged.CHROME_CRASHPAD_PIPE_NAME
|
||||
|
||||
// Enable Agent SDK debug stderr so we can capture CLI crash diagnostics.
|
||||
// Without this, the SDK sets stderr to "ignore" and crash output is lost.
|
||||
if (!merged.DEBUG_CLAUDE_AGENT_SDK) {
|
||||
merged.DEBUG_CLAUDE_AGENT_SDK = '1'
|
||||
}
|
||||
|
||||
// Apply NODE_TLS_REJECT_UNAUTHORIZED to the current process as well,
|
||||
// so Node.js HTTP/TLS in this process (used by the SDK internals) respects it.
|
||||
if (merged.NODE_TLS_REJECT_UNAUTHORIZED && !process.env.NODE_TLS_REJECT_UNAUTHORIZED) {
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = merged.NODE_TLS_REJECT_UNAUTHORIZED
|
||||
}
|
||||
|
||||
if (IS_WIN) {
|
||||
// Redirect Claude debug output to temp to avoid write permission issues
|
||||
if (!merged.CLAUDE_DEBUG_FILE) {
|
||||
const debugPath = getClaudeAgentDebugFilePath()
|
||||
if (debugPath) merged.CLAUDE_DEBUG_FILE = debugPath
|
||||
}
|
||||
// Set CLAUDE_CONFIG_DIR to a writable temp location as fallback
|
||||
// if the default ~/.claude directory is not writable (common in Windows Electron)
|
||||
if (!merged.CLAUDE_CONFIG_DIR) {
|
||||
try {
|
||||
const fallbackDir = join(tmpdir(), 'openpencil-claude-config')
|
||||
mkdirSync(fallbackDir, { recursive: true })
|
||||
// Only use fallback if we can't write to the default location
|
||||
const defaultDir = join(homedir(), '.claude')
|
||||
const testFile = join(defaultDir, '.write-test')
|
||||
try {
|
||||
writeFileSync(testFile, '', 'utf-8')
|
||||
const { unlinkSync } = require('node:fs')
|
||||
unlinkSync(testFile)
|
||||
} catch {
|
||||
// Default dir is not writable — use fallback
|
||||
merged.CLAUDE_CONFIG_DIR = fallbackDir
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +202,11 @@ export function getClaudeAgentDebugFilePath(): string | undefined {
|
|||
*
|
||||
* - `.cmd` files: use `cmd.exe /c` (PowerShell can't run .cmd directly)
|
||||
* - `.ps1` files: use `powershell.exe`
|
||||
* - `.exe` files or others: use `cmd.exe /c` as safe default
|
||||
* - `.exe` files: spawned directly without shell
|
||||
* - Others: use `cmd.exe /c` as safe default
|
||||
*
|
||||
* Also captures stderr to the debug file — when Claude Code crashes early,
|
||||
* the debug file may be empty but stderr often contains the root cause.
|
||||
*/
|
||||
export function buildSpawnClaudeCodeProcess() {
|
||||
if (process.platform !== 'win32') return undefined
|
||||
|
|
@ -134,24 +214,56 @@ export function buildSpawnClaudeCodeProcess() {
|
|||
const cmd = options.command
|
||||
const isPowerShell = cmd.endsWith('.ps1')
|
||||
|
||||
let child
|
||||
if (isPowerShell) {
|
||||
// For .ps1 scripts, invoke via PowerShell
|
||||
const psArgs = ['-ExecutionPolicy', 'Bypass', '-File', cmd, ...options.args]
|
||||
return spawn('powershell.exe', psArgs, {
|
||||
child = spawn('powershell.exe', psArgs, {
|
||||
cwd: options.cwd,
|
||||
env: options.env as NodeJS.ProcessEnv,
|
||||
signal: options.signal,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
})
|
||||
} else if (cmd.endsWith('.exe')) {
|
||||
// .exe files can be spawned directly without shell
|
||||
child = spawn(cmd, options.args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env as NodeJS.ProcessEnv,
|
||||
signal: options.signal,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
})
|
||||
} else {
|
||||
// For .cmd or extensionless binaries, use shell
|
||||
child = spawn(cmd, options.args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env as NodeJS.ProcessEnv,
|
||||
signal: options.signal,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
windowsHide: true,
|
||||
})
|
||||
}
|
||||
|
||||
// For .cmd, .exe, or extensionless binaries, use cmd.exe /c
|
||||
return spawn(cmd, options.args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env as NodeJS.ProcessEnv,
|
||||
signal: options.signal,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
// Capture stderr to debug file — helps diagnose crashes where the process
|
||||
// exits before writing anything to the debug log
|
||||
const stderrChunks: Buffer[] = []
|
||||
child.stderr?.on('data', (chunk: Buffer) => { stderrChunks.push(chunk) })
|
||||
child.on('exit', (code) => {
|
||||
if (code !== 0 && stderrChunks.length > 0) {
|
||||
const stderr = Buffer.concat(stderrChunks).toString('utf-8').trim()
|
||||
if (stderr) {
|
||||
const debugPath = getClaudeAgentDebugFilePath()
|
||||
if (debugPath) {
|
||||
try {
|
||||
appendFileSync(debugPath, `\n[stderr exit=${code}] ${stderr}\n`)
|
||||
} catch { /* best effort */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return child
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { Send, Plus, ChevronDown, ChevronUp, Check, MessageSquare, Loader2, Paperclip, X, Square, Zap } from 'lucide-react'
|
||||
import { Send, Plus, ChevronDown, ChevronUp, Check, MessageSquare, Loader2, Paperclip, X, Square, Zap, Search } from 'lucide-react'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -162,6 +162,8 @@ export default function AIChatPanel() {
|
|||
const providers = useAgentSettingsStore((s) => s.providers)
|
||||
const providersHydrated = useAgentSettingsStore((s) => s.isHydrated)
|
||||
const [modelDropdownOpen, setModelDropdownOpen] = useState(false)
|
||||
const [modelSearch, setModelSearch] = useState('')
|
||||
const modelSearchRef = useRef<HTMLInputElement>(null)
|
||||
const pendingAttachments = useAIStore((s) => s.pendingAttachments)
|
||||
const addPendingAttachment = useAIStore((s) => s.addPendingAttachment)
|
||||
const removePendingAttachment = useAIStore((s) => s.removePendingAttachment)
|
||||
|
|
@ -233,7 +235,11 @@ export default function AIChatPanel() {
|
|||
|
||||
// Close model dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
if (!modelDropdownOpen) return
|
||||
if (!modelDropdownOpen) {
|
||||
setModelSearch('')
|
||||
return
|
||||
}
|
||||
requestAnimationFrame(() => modelSearchRef.current?.focus())
|
||||
const handler = (e: MouseEvent) => {
|
||||
const panel = panelRef.current
|
||||
if (panel && !panel.contains(e.target as Node)) {
|
||||
|
|
@ -621,7 +627,7 @@ export default function AIChatPanel() {
|
|||
/>
|
||||
|
||||
{/* --- Bottom bar: model selector + actions --- */}
|
||||
<div className="flex items-center justify-between px-2 pb-2">
|
||||
<div className="relative flex items-center justify-between px-2 pb-2">
|
||||
{/* Model selector */}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -703,13 +709,63 @@ export default function AIChatPanel() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upward model dropdown */}
|
||||
{modelDropdownOpen && availableModels.length > 0 && (
|
||||
<div className="absolute bottom-full left-2 right-2 mb-1 z-[60] rounded-lg border border-border bg-card shadow-xl py-1 max-h-72 overflow-y-auto">
|
||||
{modelGroups.length > 0
|
||||
? modelGroups.map((group) => {
|
||||
{/* Upward model dropdown */}
|
||||
{modelDropdownOpen && availableModels.length > 0 && (
|
||||
<div className="absolute bottom-full left-0 right-0 mb-1 z-[60] rounded-lg border border-border bg-card shadow-xl py-1 max-h-72 flex flex-col">
|
||||
{/* Search input */}
|
||||
<div className="px-2 pt-1 pb-1.5 border-b border-border shrink-0">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-secondary/50">
|
||||
<Search size={12} className="text-muted-foreground shrink-0" />
|
||||
<input
|
||||
ref={modelSearchRef}
|
||||
value={modelSearch}
|
||||
onChange={(e) => setModelSearch(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setModelDropdownOpen(false)
|
||||
}
|
||||
}}
|
||||
placeholder={t('ai.searchModels')}
|
||||
className="w-full bg-transparent text-xs text-foreground placeholder-muted-foreground outline-none"
|
||||
/>
|
||||
{modelSearch && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setModelSearch('')}
|
||||
className="text-muted-foreground hover:text-foreground shrink-0"
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
{(() => {
|
||||
const q = modelSearch.toLowerCase().trim()
|
||||
if (modelGroups.length > 0) {
|
||||
const filtered = modelGroups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
models: group.models.filter(
|
||||
(m) =>
|
||||
!q ||
|
||||
m.displayName.toLowerCase().includes(q) ||
|
||||
m.value.toLowerCase().includes(q) ||
|
||||
group.providerName.toLowerCase().includes(q),
|
||||
),
|
||||
}))
|
||||
.filter((group) => group.models.length > 0)
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return (
|
||||
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
|
||||
{t('ai.noModelsFound')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return filtered.map((group) => {
|
||||
const GIcon = PROVIDER_ICON[group.provider]
|
||||
return (
|
||||
<div key={group.provider}>
|
||||
|
|
@ -740,7 +796,7 @@ export default function AIChatPanel() {
|
|||
{isSelected && <Check size={12} />}
|
||||
</span>
|
||||
<span className="font-medium">{m.displayName}</span>
|
||||
{idx === 0 && (
|
||||
{idx === 0 && !q && (
|
||||
<span className="text-[9px] text-muted-foreground bg-secondary px-1 py-0.5 rounded ml-auto">
|
||||
{t('common.best')}
|
||||
</span>
|
||||
|
|
@ -751,32 +807,52 @@ export default function AIChatPanel() {
|
|||
</div>
|
||||
)
|
||||
})
|
||||
: availableModels.map((m) => {
|
||||
const isSelected = m.value === model
|
||||
return (
|
||||
<button
|
||||
key={m.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
selectModel(m.value)
|
||||
setModelDropdownOpen(false)
|
||||
}}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors',
|
||||
isSelected
|
||||
? 'bg-secondary text-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
<span className="w-3.5 shrink-0">
|
||||
{isSelected && <Check size={12} />}
|
||||
</span>
|
||||
<span className="font-medium">{m.displayName}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
}
|
||||
|
||||
const filtered = availableModels.filter(
|
||||
(m) =>
|
||||
!q ||
|
||||
m.displayName.toLowerCase().includes(q) ||
|
||||
m.value.toLowerCase().includes(q),
|
||||
)
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return (
|
||||
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
|
||||
{t('ai.noModelsFound')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return filtered.map((m) => {
|
||||
const isSelected = m.value === model
|
||||
return (
|
||||
<button
|
||||
key={m.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
selectModel(m.value)
|
||||
setModelDropdownOpen(false)
|
||||
}}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors',
|
||||
isSelected
|
||||
? 'bg-secondary text-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
<span className="w-3.5 shrink-0">
|
||||
{isSelected && <Check size={12} />}
|
||||
</span>
|
||||
<span className="font-medium">{m.displayName}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -315,6 +315,8 @@ const hi: TranslationKeys = {
|
|||
'ai.sendMessage': 'संदेश भेजें',
|
||||
'ai.loadingModels': 'मॉडल लोड हो रहे हैं...',
|
||||
'ai.noModelsConnected': 'कोई मॉडल कनेक्ट नहीं है',
|
||||
'ai.searchModels': 'मॉडल खोजें...',
|
||||
'ai.noModelsFound': 'कोई मॉडल नहीं मिला',
|
||||
'ai.quickAction.loginScreen': 'मोबाइल लॉगिन स्क्रीन डिज़ाइन करें',
|
||||
'ai.quickAction.loginScreenPrompt':
|
||||
'ईमेल इनपुट, पासवर्ड इनपुट, लॉगिन बटन और सोशल लॉगिन विकल्पों के साथ एक आधुनिक मोबाइल लॉगिन स्क्रीन डिज़ाइन करें',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -319,6 +319,8 @@ const ja: TranslationKeys = {
|
|||
'ai.sendMessage': 'メッセージを送信',
|
||||
'ai.loadingModels': 'モデルを読み込み中...',
|
||||
'ai.noModelsConnected': 'モデルが接続されていません',
|
||||
'ai.searchModels': 'モデルを検索...',
|
||||
'ai.noModelsFound': 'モデルが見つかりません',
|
||||
'ai.quickAction.loginScreen': 'モバイルログイン画面をデザイン',
|
||||
'ai.quickAction.loginScreenPrompt':
|
||||
'メール入力、パスワード入力、ログインボタン、ソーシャルログインオプションを含む、モダンなモバイルログイン画面をデザインしてください',
|
||||
|
|
|
|||
|
|
@ -315,6 +315,8 @@ const ko: TranslationKeys = {
|
|||
'ai.sendMessage': '메시지 보내기',
|
||||
'ai.loadingModels': '모델 로딩 중...',
|
||||
'ai.noModelsConnected': '연결된 모델 없음',
|
||||
'ai.searchModels': '모델 검색...',
|
||||
'ai.noModelsFound': '일치하는 모델이 없습니다',
|
||||
'ai.quickAction.loginScreen': '모바일 로그인 화면 디자인',
|
||||
'ai.quickAction.loginScreenPrompt':
|
||||
'이메일 입력, 비밀번호 입력, 로그인 버튼, 소셜 로그인 옵션이 있는 모던 모바일 로그인 화면을 디자인해 주세요',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -315,6 +315,8 @@ const th: TranslationKeys = {
|
|||
'ai.sendMessage': 'ส่งข้อความ',
|
||||
'ai.loadingModels': 'กำลังโหลดโมเดล...',
|
||||
'ai.noModelsConnected': 'ไม่มีโมเดลที่เชื่อมต่อ',
|
||||
'ai.searchModels': 'ค้นหาโมเดล...',
|
||||
'ai.noModelsFound': 'ไม่พบโมเดล',
|
||||
'ai.quickAction.loginScreen': 'ออกแบบหน้าจอเข้าสู่ระบบมือถือ',
|
||||
'ai.quickAction.loginScreenPrompt':
|
||||
'ออกแบบหน้าจอเข้าสู่ระบบมือถือที่ทันสมัย พร้อมช่องกรอกอีเมล รหัสผ่าน ปุ่มเข้าสู่ระบบ และตัวเลือกเข้าสู่ระบบผ่านโซเชียล',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -309,6 +309,8 @@ const zhTW: TranslationKeys = {
|
|||
'ai.sendMessage': '傳送訊息',
|
||||
'ai.loadingModels': '正在載入模型...',
|
||||
'ai.noModelsConnected': '尚未連線模型',
|
||||
'ai.searchModels': '搜尋模型...',
|
||||
'ai.noModelsFound': '未找到匹配的模型',
|
||||
'ai.quickAction.loginScreen': '設計行動裝置登入頁面',
|
||||
'ai.quickAction.loginScreenPrompt':
|
||||
'設計一個現代的行動裝置登入頁面,包含電子郵件輸入框、密碼輸入框、登入按鈕和社群登入選項',
|
||||
|
|
|
|||
|
|
@ -309,6 +309,8 @@ const zh: TranslationKeys = {
|
|||
'ai.sendMessage': '发送消息',
|
||||
'ai.loadingModels': '正在加载模型...',
|
||||
'ai.noModelsConnected': '未连接模型',
|
||||
'ai.searchModels': '搜索模型...',
|
||||
'ai.noModelsFound': '未找到匹配的模型',
|
||||
'ai.quickAction.loginScreen': '设计一个移动端登录页面',
|
||||
'ai.quickAction.loginScreenPrompt':
|
||||
'设计一个现代的移动端登录页面,包含邮箱输入框、密码输入框、登录按钮和社交登录选项',
|
||||
|
|
|
|||
|
|
@ -482,21 +482,63 @@ function applyDescendantOverrides(
|
|||
|
||||
/** Parse a JSON-like argument, handling unquoted keys. */
|
||||
function parseJsonArg(str: string): Record<string, unknown> {
|
||||
let normalized = str.trim()
|
||||
const trimmed = str.trim()
|
||||
// Try strict JSON first (most common case — avoids mangling values like "Don't")
|
||||
try {
|
||||
return sanitizeObject(JSON.parse(trimmed))
|
||||
} catch { /* fall through to lenient parsing */ }
|
||||
|
||||
let normalized = trimmed
|
||||
// Convert JavaScript-style object to JSON: unquoted keys → quoted
|
||||
normalized = normalized.replace(
|
||||
/(?<=\{|,)\s*(\w+)\s*:/g,
|
||||
' "$1":',
|
||||
)
|
||||
// Handle single-quoted strings → double-quoted
|
||||
normalized = normalized.replace(/'/g, '"')
|
||||
// Replace single-quoted string delimiters with double quotes (not quotes inside strings)
|
||||
normalized = replaceSingleQuoteDelimiters(normalized)
|
||||
try {
|
||||
return sanitizeObject(JSON.parse(normalized))
|
||||
} catch {
|
||||
throw new Error(`Failed to parse JSON: ${str}`)
|
||||
throw new Error(`Failed to parse JSON: ${str.slice(0, 200)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Replace single-quote string delimiters with double quotes, leaving apostrophes inside strings. */
|
||||
function replaceSingleQuoteDelimiters(str: string): string {
|
||||
const chars: string[] = []
|
||||
let inDouble = false
|
||||
let inSingle = false
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str[i]
|
||||
if (ch === '\\' && (inDouble || inSingle)) {
|
||||
chars.push(ch, str[++i] ?? '')
|
||||
continue
|
||||
}
|
||||
if (inDouble) {
|
||||
if (ch === '"') inDouble = false
|
||||
chars.push(ch)
|
||||
} else if (inSingle) {
|
||||
if (ch === "'") {
|
||||
inSingle = false
|
||||
chars.push('"') // closing single quote → double quote
|
||||
} else {
|
||||
chars.push(ch)
|
||||
}
|
||||
} else {
|
||||
if (ch === '"') {
|
||||
inDouble = true
|
||||
chars.push(ch)
|
||||
} else if (ch === "'") {
|
||||
inSingle = true
|
||||
chars.push('"') // opening single quote → double quote
|
||||
} else {
|
||||
chars.push(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
return chars.join('')
|
||||
}
|
||||
|
||||
/** Find the index of the first comma not inside braces/brackets/quotes. */
|
||||
function findTopLevelComma(str: string): number {
|
||||
let depth = 0
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
69
bun.lock
69
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=="],
|
||||
|
|
|
|||
13
package.json
13
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 <version>\" && exit 1; for f in package.json apps/*/package.json packages/*/package.json; do [ -f \"$f\" ] && jq --arg v \"$V\" \".version=\\$v\" \"$f\" > \"$f.tmp\" && mv \"$f.tmp\" \"$f\" && echo \"$f → $V\"; done'"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
55
packages/pen-codegen/README.md
Normal file
55
packages/pen-codegen/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# @zseven-w/pen-codegen
|
||||
|
||||
Multi-platform code generators for [OpenPencil](https://github.com/nicepkg/openpencil) designs. Turn your design files into production-ready code for 8 frameworks.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @zseven-w/pen-codegen
|
||||
```
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
| Platform | Generator | Output |
|
||||
|---|---|---|
|
||||
| React + Tailwind | `generateReactCode` | `.tsx` with Tailwind classes |
|
||||
| HTML + CSS | `generateHTMLCode` | Vanilla HTML/CSS |
|
||||
| Vue 3 | `generateVueCode` | `.vue` SFC |
|
||||
| Svelte | `generateSvelteCode` | `.svelte` component |
|
||||
| Flutter | `generateFlutterCode` | Dart widget |
|
||||
| SwiftUI | `generateSwiftUICode` | Swift view |
|
||||
| Jetpack Compose | `generateComposeCode` | Kotlin composable |
|
||||
| React Native | `generateReactNativeCode` | `.tsx` with StyleSheet |
|
||||
|
||||
## Usage
|
||||
|
||||
Generate code from a single node:
|
||||
|
||||
```ts
|
||||
import { generateReactCode } from '@zseven-w/pen-codegen'
|
||||
|
||||
const code = generateReactCode(node, { indent: 2 })
|
||||
```
|
||||
|
||||
Generate from an entire document (resolves variables, computes layout):
|
||||
|
||||
```ts
|
||||
import { generateReactFromDocument } from '@zseven-w/pen-codegen'
|
||||
|
||||
const code = generateReactFromDocument(document)
|
||||
```
|
||||
|
||||
### CSS Variables
|
||||
|
||||
Extract design variables as CSS custom properties:
|
||||
|
||||
```ts
|
||||
import { generateCSSVariables } from '@zseven-w/pen-codegen'
|
||||
|
||||
const css = generateCSSVariables(variables, themes)
|
||||
// :root { --color-primary: #3b82f6; ... }
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
80
packages/pen-core/README.md
Normal file
80
packages/pen-core/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# @zseven-w/pen-core
|
||||
|
||||
Core document operations for [OpenPencil](https://github.com/nicepkg/openpencil) — tree manipulation, layout engine, design variables, boolean path operations, and more.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @zseven-w/pen-core
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Document Tree Operations
|
||||
|
||||
Create, query, and mutate the document tree:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createEmptyDocument,
|
||||
findNodeInTree,
|
||||
insertNodeInTree,
|
||||
removeNodeFromTree,
|
||||
updateNodeInTree,
|
||||
deepCloneNode,
|
||||
flattenNodes,
|
||||
} from '@zseven-w/pen-core'
|
||||
|
||||
const doc = createEmptyDocument()
|
||||
const node = findNodeInTree(doc.children, 'node-id')
|
||||
```
|
||||
|
||||
### Multi-Page Support
|
||||
|
||||
```ts
|
||||
import { getActivePage, getActivePageChildren, migrateToPages } from '@zseven-w/pen-core'
|
||||
```
|
||||
|
||||
### Layout Engine
|
||||
|
||||
Automatic layout computation with auto-sizing, padding, and gap support:
|
||||
|
||||
```ts
|
||||
import { inferLayout, computeLayoutPositions, fitContentWidth, fitContentHeight } from '@zseven-w/pen-core'
|
||||
```
|
||||
|
||||
### Design Variables
|
||||
|
||||
Resolve `$variable` references against theme axes:
|
||||
|
||||
```ts
|
||||
import { resolveVariableRef, resolveNodeForCanvas, replaceVariableRefsInTree } from '@zseven-w/pen-core'
|
||||
```
|
||||
|
||||
### Boolean Path Operations
|
||||
|
||||
Union, subtract, intersect, and exclude paths via Paper.js:
|
||||
|
||||
```ts
|
||||
import { executeBooleanOp, BooleanOpType } from '@zseven-w/pen-core'
|
||||
```
|
||||
|
||||
### Text Measurement
|
||||
|
||||
Estimate text dimensions for layout without a browser:
|
||||
|
||||
```ts
|
||||
import { estimateTextWidth, estimateTextHeight } from '@zseven-w/pen-core'
|
||||
```
|
||||
|
||||
### Document Normalization
|
||||
|
||||
Sanitize and fix documents imported from external sources:
|
||||
|
||||
```ts
|
||||
import { normalizePenDocument } from '@zseven-w/pen-core'
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
53
packages/pen-figma/README.md
Normal file
53
packages/pen-figma/README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# @zseven-w/pen-figma
|
||||
|
||||
Figma `.fig` file parser and converter for [OpenPencil](https://github.com/nicepkg/openpencil). Import Figma designs directly into the OpenPencil document model.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @zseven-w/pen-figma
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Parse binary `.fig` files (Kiwi schema + zstd/zip compression)
|
||||
- Convert Figma node trees to `PenDocument`
|
||||
- Multi-page support — import all pages or a single page
|
||||
- Clipboard paste — detect and convert Figma clipboard HTML
|
||||
- Image blob resolution
|
||||
|
||||
## Usage
|
||||
|
||||
### Parse a `.fig` file
|
||||
|
||||
```ts
|
||||
import { parseFigFile, figmaAllPagesToPenDocument } from '@zseven-w/pen-figma'
|
||||
|
||||
const figFile = parseFigFile(buffer)
|
||||
const document = figmaAllPagesToPenDocument(figFile)
|
||||
```
|
||||
|
||||
### Single page import
|
||||
|
||||
```ts
|
||||
import { parseFigFile, getFigmaPages, figmaToPenDocument } from '@zseven-w/pen-figma'
|
||||
|
||||
const figFile = parseFigFile(buffer)
|
||||
const pages = getFigmaPages(figFile)
|
||||
const document = figmaToPenDocument(figFile, pages[0])
|
||||
```
|
||||
|
||||
### Clipboard paste
|
||||
|
||||
```ts
|
||||
import { isFigmaClipboardHtml, extractFigmaClipboardData, figmaClipboardToNodes } from '@zseven-w/pen-figma'
|
||||
|
||||
if (isFigmaClipboardHtml(html)) {
|
||||
const data = extractFigmaClipboardData(html)
|
||||
const nodes = figmaClipboardToNodes(data)
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
64
packages/pen-renderer/README.md
Normal file
64
packages/pen-renderer/README.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# @zseven-w/pen-renderer
|
||||
|
||||
Standalone CanvasKit/Skia renderer for [OpenPencil](https://github.com/nicepkg/openpencil) design files. Render `.op` documents to a GPU-accelerated canvas — works in browsers, Node.js, and headless environments.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @zseven-w/pen-renderer canvaskit-wasm
|
||||
```
|
||||
|
||||
`canvaskit-wasm` is a peer dependency — you provide the WASM binary.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { loadCanvasKit, PenRenderer } from '@zseven-w/pen-renderer'
|
||||
|
||||
// Initialize CanvasKit
|
||||
await loadCanvasKit()
|
||||
|
||||
// Create renderer on a canvas element
|
||||
const renderer = new PenRenderer(canvas, document, {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
})
|
||||
|
||||
// Render
|
||||
renderer.render()
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### High-level
|
||||
|
||||
- **`loadCanvasKit()`** — Initialize the CanvasKit WASM module
|
||||
- **`PenRenderer`** — Full-featured renderer with viewport, selection, and interaction support
|
||||
|
||||
### Document Flattening
|
||||
|
||||
Pre-process documents for rendering:
|
||||
|
||||
```ts
|
||||
import { flattenToRenderNodes, resolveRefs, premeasureTextHeights } from '@zseven-w/pen-renderer'
|
||||
```
|
||||
|
||||
### Viewport Utilities
|
||||
|
||||
```ts
|
||||
import { viewportMatrix, screenToScene, sceneToScreen, zoomToPoint } from '@zseven-w/pen-renderer'
|
||||
```
|
||||
|
||||
### Low-level Renderers
|
||||
|
||||
For custom rendering pipelines:
|
||||
|
||||
- `SkiaNodeRenderer` — Renders individual nodes to a Skia canvas
|
||||
- `SkiaTextRenderer` — Text layout and rendering
|
||||
- `SkiaFontManager` — Font loading and management
|
||||
- `SkiaImageLoader` — Async image loading with caching
|
||||
- `SpatialIndex` — R-tree spatial index for hit testing
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ export class SkiaTextRenderer {
|
|||
// 64 MB — each entry is estimated as content.length*64+4096 bytes (WASM heap approximation)
|
||||
private static PARA_CACHE_BYTE_LIMIT = 64 * 1024 * 1024
|
||||
|
||||
// Pre-rasterized paragraph image cache (SkImage, same key as paraCache, zoom-independent)
|
||||
// Allows drawImageRect instead of drawParagraph on every frame — avoids per-frame glyph rasterization.
|
||||
private paraImageCache = new Map<string, SkImage | null>()
|
||||
private paraImageCacheBytes = 0
|
||||
// 128 MB — each entry is sw*sh*4 bytes (RGBA pixels at up to 2x DPR scale)
|
||||
private static PARA_IMAGE_CACHE_BYTE_LIMIT = 128 * 1024 * 1024
|
||||
|
||||
private static estimateParaBytes(content: string): number {
|
||||
return content.length * 64 + 4096
|
||||
}
|
||||
|
|
@ -37,6 +44,10 @@ export class SkiaTextRenderer {
|
|||
// Device pixel ratio override
|
||||
devicePixelRatio: number | undefined
|
||||
|
||||
private get _dpr(): number {
|
||||
return this.devicePixelRatio ?? (typeof window !== 'undefined' ? window.devicePixelRatio : 1) ?? 1
|
||||
}
|
||||
|
||||
// Font manager for vector text rendering
|
||||
fontManager: SkiaFontManager
|
||||
|
||||
|
|
@ -59,6 +70,11 @@ export class SkiaTextRenderer {
|
|||
}
|
||||
this.paraCache.clear()
|
||||
this.paraCacheBytes = 0
|
||||
for (const img of this.paraImageCache.values()) {
|
||||
img?.delete()
|
||||
}
|
||||
this.paraImageCache.clear()
|
||||
this.paraImageCacheBytes = 0
|
||||
}
|
||||
|
||||
// Evict oldest entries (Map head = first inserted) until there is room for `incoming` bytes.
|
||||
|
|
@ -71,6 +87,17 @@ export class SkiaTextRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
private evictParaImageCache(incoming: number) {
|
||||
while (this.paraImageCacheBytes + incoming > SkiaTextRenderer.PARA_IMAGE_CACHE_BYTE_LIMIT && this.paraImageCache.size > 0) {
|
||||
const [key, img] = this.paraImageCache.entries().next().value!
|
||||
if (img) {
|
||||
this.paraImageCacheBytes -= img.width() * img.height() * 4
|
||||
img.delete()
|
||||
}
|
||||
this.paraImageCache.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
private evictTextCache(incoming: number) {
|
||||
while (this.textCacheBytes + incoming > SkiaTextRenderer.TEXT_CACHE_BYTE_LIMIT && this.textCache.size > 0) {
|
||||
const [key, img] = this.textCache.entries().next().value!
|
||||
|
|
@ -246,13 +273,63 @@ export class SkiaTextRenderer {
|
|||
|
||||
if (!para) return false
|
||||
|
||||
// Compute drawX and surface dimensions
|
||||
let drawX = x
|
||||
if (!isFixedWidth && w > 0 && textAlign !== 'left') {
|
||||
let surfaceW: number
|
||||
if (!isFixedWidth) {
|
||||
const longestLine = para.getLongestLine()
|
||||
if (textAlign === 'center') drawX = x + Math.max(0, (w - longestLine) / 2)
|
||||
else if (textAlign === 'right') drawX = x + Math.max(0, w - longestLine)
|
||||
surfaceW = longestLine + 2
|
||||
if (w > 0 && textAlign !== 'left') {
|
||||
if (textAlign === 'center') drawX = x + Math.max(0, (w - longestLine) / 2)
|
||||
else if (textAlign === 'right') drawX = x + Math.max(0, w - longestLine)
|
||||
}
|
||||
} else {
|
||||
surfaceW = layoutWidth
|
||||
}
|
||||
const surfaceH = para.getHeight() + 2
|
||||
|
||||
// Try paragraph image cache: drawImageRect is far cheaper than drawParagraph per frame
|
||||
const imgScale = Math.min(this._dpr, 2)
|
||||
let cachedImg = this.paraImageCache.get(cacheKey)
|
||||
if (cachedImg === undefined) {
|
||||
cachedImg = null
|
||||
const sw = Math.min(Math.ceil(surfaceW * imgScale), 4096)
|
||||
const sh = Math.min(Math.ceil(surfaceH * imgScale), 4096)
|
||||
if (sw > 0 && sh > 0) {
|
||||
const surf: any = (ck as any).MakeSurface?.(sw, sh)
|
||||
if (surf) {
|
||||
const offCanvas = surf.getCanvas()
|
||||
offCanvas.scale(imgScale, imgScale)
|
||||
offCanvas.drawParagraph(para, 0, 0)
|
||||
cachedImg = (surf.makeImageSnapshot() as SkImage | null) ?? null
|
||||
surf.delete()
|
||||
if (cachedImg) {
|
||||
const imgBytes = sw * sh * 4
|
||||
this.evictParaImageCache(imgBytes)
|
||||
this.paraImageCacheBytes += imgBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
this.paraImageCache.set(cacheKey, cachedImg)
|
||||
}
|
||||
|
||||
if (cachedImg) {
|
||||
const imgW = cachedImg.width() / imgScale
|
||||
const imgH = cachedImg.height() / imgScale
|
||||
const paint = new ck.Paint()
|
||||
paint.setAntiAlias(true)
|
||||
if (opacity < 1) paint.setAlphaf(opacity)
|
||||
canvas.drawImageRect(
|
||||
cachedImg,
|
||||
ck.LTRBRect(0, 0, cachedImg.width(), cachedImg.height()),
|
||||
ck.LTRBRect(drawX, y, drawX + imgW, y + imgH),
|
||||
paint,
|
||||
)
|
||||
paint.delete()
|
||||
return true
|
||||
}
|
||||
|
||||
// Fallback: surface creation failed, draw directly
|
||||
if (opacity < 1) {
|
||||
const paint = new ck.Paint()
|
||||
paint.setAlphaf(opacity)
|
||||
|
|
@ -371,8 +448,7 @@ export class SkiaTextRenderer {
|
|||
: (wrappedLines.length - 1) * lineHeight + glyphH + 2,
|
||||
)
|
||||
|
||||
const dpr = this.devicePixelRatio ?? ((typeof window !== 'undefined' ? window.devicePixelRatio : 1) || 1)
|
||||
const rawScale = this.zoom * dpr
|
||||
const rawScale = this.zoom * this._dpr
|
||||
const scale = rawScale <= 2 ? 2 : rawScale <= 4 ? 4 : 8
|
||||
|
||||
const cacheKey = `${content}|${fontSize}|${fillColor}|${fontWeight}|${fontFamily}|${textAlign}|${Math.round(renderW)}|${Math.round(textH)}|${scale}`
|
||||
|
|
|
|||
56
packages/pen-sdk/README.md
Normal file
56
packages/pen-sdk/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# @zseven-w/pen-sdk
|
||||
|
||||
The umbrella SDK for [OpenPencil](https://github.com/nicepkg/openpencil). One import gives you everything — types, document operations, code generation, Figma import, and rendering.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @zseven-w/pen-sdk
|
||||
```
|
||||
|
||||
## What's Included
|
||||
|
||||
This package re-exports all OpenPencil packages:
|
||||
|
||||
| Package | Provides |
|
||||
|---|---|
|
||||
| `@zseven-w/pen-types` | TypeScript types for the document model |
|
||||
| `@zseven-w/pen-core` | Tree operations, layout engine, variables, boolean ops |
|
||||
| `@zseven-w/pen-codegen` | Code generators (React, HTML, Vue, Svelte, Flutter, SwiftUI, Compose, RN) |
|
||||
| `@zseven-w/pen-figma` | Figma `.fig` parser and converter |
|
||||
| `@zseven-w/pen-renderer` | CanvasKit/Skia GPU renderer |
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import {
|
||||
// Types
|
||||
type PenDocument,
|
||||
type PenNode,
|
||||
|
||||
// Document operations
|
||||
createEmptyDocument,
|
||||
findNodeInTree,
|
||||
insertNodeInTree,
|
||||
normalizePenDocument,
|
||||
|
||||
// Code generation
|
||||
generateReactFromDocument,
|
||||
generateHTMLFromDocument,
|
||||
generateFlutterFromDocument,
|
||||
|
||||
// Figma import
|
||||
parseFigFile,
|
||||
figmaAllPagesToPenDocument,
|
||||
|
||||
// Rendering
|
||||
loadCanvasKit,
|
||||
PenRenderer,
|
||||
} from '@zseven-w/pen-sdk'
|
||||
```
|
||||
|
||||
Or install individual packages for smaller bundles — see each package's README for details.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
31
packages/pen-types/README.md
Normal file
31
packages/pen-types/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# @zseven-w/pen-types
|
||||
|
||||
Type definitions for the [OpenPencil](https://github.com/nicepkg/openpencil) document model.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @zseven-w/pen-types
|
||||
```
|
||||
|
||||
## What's Included
|
||||
|
||||
This package provides all TypeScript types and interfaces for the OpenPencil design file format (`.op`):
|
||||
|
||||
- **Document model** — `PenDocument`, `PenPage`, `PenNode` and all node types (`FrameNode`, `RectangleNode`, `EllipseNode`, `TextNode`, `ImageNode`, `PathNode`, etc.)
|
||||
- **Styles** — `PenFill` (solid, gradient, image), `PenStroke`, `PenEffect` (blur, shadow), `BlendMode`, `StyledTextSegment`
|
||||
- **Variables & Themes** — `VariableDefinition`, `VariableValue`, `ThemedValue`
|
||||
- **Canvas state** — `ToolType`, `ViewportState`, `SelectionState`, `CanvasInteraction`
|
||||
- **UIKit** — `UIKit`, `KitComponent`, `ComponentCategory`
|
||||
- **Theme presets** — `ThemePreset`, `ThemePresetFile`
|
||||
- **Design spec** — `DesignMdSpec`, `DesignMdColor`, `DesignMdTypography`
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import type { PenDocument, PenNode, FrameNode } from '@zseven-w/pen-types'
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@zseven-w/pen-types",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Type definitions for OpenPencil document model",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
117
scripts/publish-beta.sh
Executable file
117
scripts/publish-beta.sh
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
#!/bin/bash
|
||||
# Publish all @zseven-w packages to npm with auto-incrementing beta version.
|
||||
#
|
||||
# Usage:
|
||||
# bun run publish:beta # auto-increment beta number
|
||||
# bun run publish:beta 5 # force beta.5
|
||||
#
|
||||
# Publishes: pen-types → pen-core → pen-codegen, pen-figma → pen-renderer → pen-sdk → openpencil CLI
|
||||
# All under the "beta" dist-tag, so `npm install` won't pick them up by default.
|
||||
# Install with: npm install @zseven-w/openpencil@beta
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
BASE_VERSION=$(jq -r .version "$ROOT/package.json")
|
||||
FORCE_NUM="${1:-}"
|
||||
|
||||
# Packages in topological order
|
||||
PACKAGES=(
|
||||
packages/pen-types
|
||||
packages/pen-core
|
||||
packages/pen-codegen
|
||||
packages/pen-figma
|
||||
packages/pen-renderer
|
||||
packages/pen-sdk
|
||||
apps/cli
|
||||
)
|
||||
|
||||
# --- Determine beta number ---
|
||||
if [ -n "$FORCE_NUM" ]; then
|
||||
BETA_NUM="$FORCE_NUM"
|
||||
else
|
||||
# Query npm for the latest beta of this base version.
|
||||
# npm view returns a string (1 version) or array (multiple), or errors (404) if not found.
|
||||
RAW=$(npm view "@zseven-w/pen-types" versions --json 2>/dev/null || true)
|
||||
LATEST=$(echo "$RAW" | jq -r --arg base "$BASE_VERSION" '
|
||||
if type == "object" and .error then empty # npm 404 error object
|
||||
elif type == "array" then
|
||||
map(select(type == "string" and startswith($base + "-beta."))) | last // empty
|
||||
elif type == "string" and startswith($base + "-beta.") then .
|
||||
else empty
|
||||
end
|
||||
' 2>/dev/null || true)
|
||||
|
||||
if [ -n "$LATEST" ]; then
|
||||
PREV_NUM=$(echo "$LATEST" | sed "s/${BASE_VERSION}-beta\.//")
|
||||
BETA_NUM=$((PREV_NUM + 1))
|
||||
else
|
||||
BETA_NUM=0
|
||||
fi
|
||||
fi
|
||||
|
||||
BETA_VERSION="${BASE_VERSION}-beta.${BETA_NUM}"
|
||||
echo "Publishing version: $BETA_VERSION"
|
||||
echo ""
|
||||
|
||||
# --- Set beta version in all package.json files ---
|
||||
MODIFIED_FILES=()
|
||||
for pkg in "${PACKAGES[@]}"; do
|
||||
f="$ROOT/$pkg/package.json"
|
||||
if [ -f "$f" ]; then
|
||||
# Backup original
|
||||
cp "$f" "$f.bak"
|
||||
MODIFIED_FILES+=("$f")
|
||||
|
||||
# Set version and replace workspace:* refs
|
||||
jq --arg v "$BETA_VERSION" '
|
||||
.version = $v |
|
||||
if .dependencies then
|
||||
.dependencies |= with_entries(
|
||||
if .value == "workspace:*" then .value = $v else . end
|
||||
)
|
||||
else . end |
|
||||
if .devDependencies then
|
||||
.devDependencies |= with_entries(
|
||||
if .value == "workspace:*" then .value = $v else . end
|
||||
)
|
||||
else . end
|
||||
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Restore on exit ---
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Restoring original package.json files..."
|
||||
for f in "${MODIFIED_FILES[@]}"; do
|
||||
if [ -f "$f.bak" ]; then
|
||||
mv "$f.bak" "$f"
|
||||
fi
|
||||
done
|
||||
echo "Done."
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- Compile CLI ---
|
||||
echo "Compiling CLI..."
|
||||
(cd "$ROOT" && bun run cli:compile)
|
||||
echo ""
|
||||
|
||||
# --- Verify CLI ---
|
||||
node "$ROOT/apps/cli/dist/openpencil-cli.cjs" --version
|
||||
echo ""
|
||||
|
||||
# --- Publish ---
|
||||
for pkg in "${PACKAGES[@]}"; do
|
||||
dir="$ROOT/$pkg"
|
||||
name=$(jq -r .name "$dir/package.json")
|
||||
echo "Publishing $name@$BETA_VERSION ..."
|
||||
(cd "$dir" && npm publish --access public --tag beta) || echo " ⚠ Failed (may already exist)"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "================================"
|
||||
echo "Published: $BETA_VERSION"
|
||||
echo "Install: npm install @zseven-w/openpencil@beta"
|
||||
echo "================================"
|
||||
Loading…
Reference in a new issue