Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6da3de3
feat(core): add Penpot API client and design source integration
alexey1312 Mar 21, 2026
897c5ac
docs: update CLAUDE.md with PenpotAPI module and source kind patterns
alexey1312 Mar 21, 2026
1192a2c
docs: update module docs for PenpotAPI (15 modules, source dispatch)
alexey1312 Mar 21, 2026
4672f8f
docs: add Penpot support to README, DocC articles, and llms-full.txt
alexey1312 Mar 21, 2026
ad88f06
fix(penpot): resolve URL construction, base URL, error handling, and …
alexey1312 Mar 21, 2026
a7ac06b
test(penpot): add tests for resolvedSourceKind, hexToRGBA, SourceFact…
alexey1312 Mar 21, 2026
f4ec909
fix(penpot): fix GetProfile empty body and duplicate --timeout in fet…
alexey1312 Mar 21, 2026
091ea83
docs(penpot): add Penpot file structure guide, MCP resources, and pro…
alexey1312 Mar 21, 2026
b477448
feat(cli): add Penpot support to init and fetch wizards
alexey1312 Mar 22, 2026
2c4b1eb
fix(penpot): fix API client bugs, improve error handling, and add mis…
alexey1312 Mar 22, 2026
26f54b8
feat(cli): make FIGMA_PERSONAL_TOKEN optional and add --source penpot…
alexey1312 Mar 22, 2026
de3ef65
feat(penpot): add SVG reconstruction from shape tree for icon/image e…
alexey1312 Mar 22, 2026
9751341
docs(penpot): update docs for SVG reconstruction — remove raster-only…
alexey1312 Mar 22, 2026
f30febe
fix(penpot): allow file:// URLs in FileDownloader for local SVG assets
alexey1312 Mar 22, 2026
9266b11
fix(penpot): handle file:// URLs before download phase instead of wea…
alexey1312 Mar 22, 2026
9c2b487
docs: add FileDownloader file:// pattern and iOS PKL xcassetsPath gotcha
alexey1312 Mar 22, 2026
ed1cdfc
fix(penpot): improve type safety, error handling, and diagnostics acr…
alexey1312 Mar 22, 2026
a70914c
ci: trigger DocC deploy on tag push instead of release event
alexey1312 Mar 22, 2026
257cfee
chore(hk): disable stash in pre-commit hook
alexey1312 Mar 22, 2026
d392085
refactor(penpot): extract PenpotAPI into external package swift-penpo…
alexey1312 Mar 22, 2026
69f8cbf
docs: update module structure, org name, and external package references
alexey1312 Mar 22, 2026
ee276c2
docs: consolidate docs/ into ExFig.docc, remove duplicate directory
alexey1312 Mar 22, 2026
da9e815
docs: clarify that docs/ is DocC output, source docs live in ExFig.docc
alexey1312 Mar 22, 2026
d2990b9
docs: document nil-token behavior in resolveClient and list all sourc…
alexey1312 Mar 22, 2026
1db0526
fix(penpot): improve error handling, validation, and DI across Penpot…
alexey1312 Mar 22, 2026
b8e968b
chore: update cover
alexey1312 Mar 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions .github/app-release-secrets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# ExFig Studio Release Secrets Configuration

This document describes the GitHub secrets required for automated ExFig Studio releases.

## Required Secrets

| Secret | Description | Required For |
| ----------------------------- | --------------------------------------------------- | -------------------- |
| `APPLE_TEAM_ID` | Apple Developer Team ID (10-character alphanumeric) | Code signing |
| `APPLE_IDENTITY_NAME` | Code signing identity name (e.g., "Your Name") | Code signing |
| `APPLE_CERTIFICATE_BASE64` | Developer ID certificate (.p12) as base64 | Code signing |
| `APPLE_CERTIFICATE_PASSWORD` | Password for the .p12 certificate | Code signing |
| `APPLE_ID` | Apple ID email for notarization | Notarization |
| `APPLE_NOTARIZATION_PASSWORD` | App-specific password for notarization | Notarization |
| `HOMEBREW_TAP_TOKEN` | GitHub PAT with repo access to homebrew-exfig | Homebrew Cask update |

## Setup Instructions

### 1. Export Developer ID Certificate

```bash
# Open Keychain Access, find "Developer ID Application" certificate
# Right-click → Export → Save as .p12 with password

# Encode to base64
base64 -i DeveloperID.p12 | pbcopy
# Paste into APPLE_CERTIFICATE_BASE64 secret
```

### 2. Create App-Specific Password

1. Go to [appleid.apple.com](https://appleid.apple.com)
2. Sign in and navigate to Security → App-Specific Passwords
3. Generate a new password for "ExFig Studio Notarization"
4. Save as `APPLE_NOTARIZATION_PASSWORD` secret

### 3. Create Homebrew Tap Token

1. Go to [github.com/settings/tokens](https://github.com/settings/tokens)
2. Create a new PAT (classic) with `repo` scope
3. Save as `HOMEBREW_TAP_TOKEN` secret

### 4. Find Your Team ID

```bash
# If you have Xcode installed
security find-identity -v -p codesigning | grep "Developer ID Application"
# Team ID is in parentheses: "Developer ID Application: Name (TEAM_ID)"
```

## Release Process

### Create a Release

```bash
# Tag format: studio-v<major>.<minor>.<patch>
git tag studio-v1.0.0
git push origin studio-v1.0.0
```

### Manual Build (for testing)

```bash
# Local build without signing
./Scripts/build-app-release.sh

# Local build with signing
APPLE_TEAM_ID=YOUR_TEAM_ID ./Scripts/build-app-release.sh
```

### Workflow Dispatch

You can also trigger a release manually from the GitHub Actions tab:

1. Go to Actions → Release App
2. Click "Run workflow"
3. Enter the version number
4. Optionally skip notarization for testing

## Homebrew Cask

After a successful release, the workflow automatically updates the Homebrew Cask formula at:
`alexey1312/homebrew-exfig/Casks/exfig-studio.rb`

Users can install with:

```bash
brew tap alexey1312/exfig
brew install --cask exfig-studio
```

## Troubleshooting

### Certificate Issues

```bash
# Verify certificate is in keychain
security find-identity -v -p codesigning

# Verify certificate chain
codesign -vvv --deep "dist/ExFig Studio.app"
```

### Notarization Failures

```bash
# Check notarization status
xcrun notarytool history --apple-id YOUR_APPLE_ID --team-id YOUR_TEAM_ID

# Get detailed log for a submission
xcrun notarytool log SUBMISSION_ID --apple-id YOUR_APPLE_ID --team-id YOUR_TEAM_ID
```

### Gatekeeper Issues

```bash
# Verify app is properly signed and notarized
spctl --assess --verbose "dist/ExFig Studio.app"

# Check stapling
xcrun stapler validate "dist/ExFig Studio.app"
```
6 changes: 4 additions & 2 deletions .github/workflows/deploy-docc.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: Deploy DocC

on:
release:
types: [published]
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:

permissions:
Expand Down
182 changes: 121 additions & 61 deletions CLAUDE.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions MIGRATION_FROM_FIGMA_EXPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,6 @@ exfig icons --concurrent-downloads 50 # Increase CDN parallelism
## Getting Help

- Configuration reference: [CONFIG.md](CONFIG.md)
- PKL guide: [docs/PKL.md](docs/PKL.md)
- Migration guide (YAML to PKL): [MIGRATION.md](MIGRATION.md)
- PKL guide: [PKLGuide](https://DesignPipe.github.io/exfig/documentation/exfigcli/pklguide)
- Migration guide (YAML to PKL): [Migration](https://DesignPipe.github.io/exfig/documentation/exfigcli/migration)
- Issues: [GitHub Issues](https://github.com/DesignPipe/exfig/issues)
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ let package = Package(
.package(url: "https://github.com/apple/pkl-swift", from: "0.8.0"),
.package(url: "https://github.com/DesignPipe/swift-svgkit.git", from: "0.1.0"),
.package(url: "https://github.com/DesignPipe/swift-figma-api.git", from: "0.2.0"),
.package(url: "https://github.com/DesignPipe/swift-penpot-api.git", from: "0.1.0"),
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0"),
],
targets: [
Expand All @@ -36,6 +37,7 @@ let package = Package(
name: "ExFigCLI",
dependencies: [
.product(name: "FigmaAPI", package: "swift-figma-api"),
.product(name: "PenpotAPI", package: "swift-penpot-api"),
"ExFigCore",
"ExFigConfig",
"XcodeExport",
Expand All @@ -60,6 +62,7 @@ let package = Package(
exclude: ["CLAUDE.md", "AGENTS.md"],
resources: [
.copy("Resources/Schemas/"),
.copy("Resources/Guides/"),
]
),

Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
[![CI](https://github.com/DesignPipe/exfig/actions/workflows/ci.yml/badge.svg)](https://github.com/DesignPipe/exfig/actions/workflows/ci.yml)
[![Release](https://github.com/DesignPipe/exfig/actions/workflows/release.yml/badge.svg)](https://github.com/DesignPipe/exfig/actions/workflows/release.yml)
[![Docs](https://github.com/DesignPipe/exfig/actions/workflows/deploy-docc.yml/badge.svg)](https://DesignPipe.github.io/exfig/documentation/exfigcli)
![Coverage](https://img.shields.io/badge/coverage-50.65%25-yellow)
![Coverage](https://img.shields.io/badge/coverage-43.65%25-yellow)
[![License](https://img.shields.io/github/license/DesignPipe/exfig.svg)](LICENSE)

Export colors, typography, icons, and images from Figma to Xcode, Android Studio, Flutter, and Web projects — automatically.
Export colors, typography, icons, and images from Figma and Penpot to Xcode, Android Studio, Flutter, and Web projects — automatically.

## The Problem

- Figma has no "Export to Xcode" button. You copy hex codes by hand, one by one.
- Switching from Figma to Penpot? Your export pipeline shouldn't break.
- Every color change means updating files across 3 platforms manually.
- Dark mode variant? An afternoon spent on light/dark pairs and @1x/@2x/@3x PNGs.
- Android gets XML. iOS gets xcassets. Flutter gets Dart. Someone maintains all three.
Expand All @@ -24,7 +25,7 @@ Export colors, typography, icons, and images from Figma to Xcode, Android Studio

**Flutter developer** — You need dark mode icon variants and `@2x`/`@3x` image scales. ExFig exports SVG icons with dark suffixes, raster images with scale directories, and Dart constants.

**Design Systems lead** — One Figma file feeds four platforms. ExFig's unified PKL config exports everything from a single `exfig batch` run. One CI pipeline, one source of truth.
**Design Systems lead** — One Figma or Penpot file feeds four platforms. ExFig's unified PKL config exports everything from a single `exfig batch` run. One CI pipeline, one source of truth.

**CI/CD engineer** — Quiet mode, JSON reports, exit codes, version tracking, and checkpoint/resume. The [GitHub Action](https://github.com/DesignPipe/exfig-action) handles installation and caching.

Expand All @@ -34,7 +35,7 @@ Export colors, typography, icons, and images from Figma to Xcode, Android Studio
# 1. Install
brew install designpipe/tap/exfig

# 2. Set Figma token
# 2. Set Figma token (or PENPOT_ACCESS_TOKEN for Penpot)
export FIGMA_PERSONAL_TOKEN=your_token_here

# 3a. Quick one-off export (interactive wizard)
Expand Down
7 changes: 4 additions & 3 deletions Sources/ExFig-Android/Config/AndroidIconsEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public extension Android.IconsEntry {
/// Returns an IconsSourceInput for use with IconsExportContext.
func iconsSourceInput(darkFileId: String? = nil) -> IconsSourceInput {
IconsSourceInput(
sourceKind: sourceKind?.coreSourceKind ?? .figma,
figmaFileId: figmaFileId,
sourceKind: resolvedSourceKind,
figmaFileId: resolvedFileId,
darkFileId: darkFileId,
frameName: figmaFrameName ?? "Icons",
pageName: figmaPageName,
Expand All @@ -24,7 +24,8 @@ public extension Android.IconsEntry {
darkModeSuffix: "_dark",
rtlProperty: rtlProperty,
nameValidateRegexp: nameValidateRegexp,
nameReplaceRegexp: nameReplaceRegexp
nameReplaceRegexp: nameReplaceRegexp,
penpotBaseURL: resolvedPenpotBaseURL
)
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/ExFig-Android/Config/AndroidImagesEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public extension Android.ImagesEntry {
/// Returns an ImagesSourceInput for use with ImagesExportContext.
func imagesSourceInput(darkFileId: String? = nil) -> ImagesSourceInput {
ImagesSourceInput(
sourceKind: sourceKind?.coreSourceKind ?? .figma,
figmaFileId: figmaFileId,
sourceKind: resolvedSourceKind,
figmaFileId: resolvedFileId,
darkFileId: darkFileId,
frameName: figmaFrameName ?? "Images",
pageName: figmaPageName,
Expand All @@ -42,7 +42,8 @@ public extension Android.ImagesEntry {
darkModeSuffix: "_dark",
rtlProperty: rtlProperty,
nameValidateRegexp: nameValidateRegexp,
nameReplaceRegexp: nameReplaceRegexp
nameReplaceRegexp: nameReplaceRegexp,
penpotBaseURL: resolvedPenpotBaseURL
)
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/ExFig-Flutter/Config/FlutterIconsEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ public extension Flutter.IconsEntry {
/// Returns an IconsSourceInput for use with IconsExportContext.
func iconsSourceInput(darkFileId: String? = nil) -> IconsSourceInput {
IconsSourceInput(
sourceKind: sourceKind?.coreSourceKind ?? .figma,
figmaFileId: figmaFileId,
sourceKind: resolvedSourceKind,
figmaFileId: resolvedFileId,
darkFileId: darkFileId,
frameName: figmaFrameName ?? "Icons",
pageName: figmaPageName,
useSingleFile: darkFileId == nil,
darkModeSuffix: "_dark",
rtlProperty: rtlProperty,
nameValidateRegexp: nameValidateRegexp,
nameReplaceRegexp: nameReplaceRegexp
nameReplaceRegexp: nameReplaceRegexp,
penpotBaseURL: resolvedPenpotBaseURL
)
}

Expand Down
14 changes: 8 additions & 6 deletions Sources/ExFig-Flutter/Config/FlutterImagesEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public extension Flutter.ImagesEntry {
/// Returns an ImagesSourceInput for use with ImagesExportContext.
func imagesSourceInput(darkFileId: String? = nil) -> ImagesSourceInput {
ImagesSourceInput(
sourceKind: sourceKind?.coreSourceKind ?? .figma,
figmaFileId: figmaFileId,
sourceKind: resolvedSourceKind,
figmaFileId: resolvedFileId,
darkFileId: darkFileId,
frameName: figmaFrameName ?? "Images",
pageName: figmaPageName,
Expand All @@ -25,7 +25,8 @@ public extension Flutter.ImagesEntry {
darkModeSuffix: "_dark",
rtlProperty: rtlProperty,
nameValidateRegexp: nameValidateRegexp,
nameReplaceRegexp: nameReplaceRegexp
nameReplaceRegexp: nameReplaceRegexp,
penpotBaseURL: resolvedPenpotBaseURL
)
}

Expand All @@ -44,8 +45,8 @@ public extension Flutter.ImagesEntry {
/// Returns an ImagesSourceInput configured for SVG source.
func svgSourceInput(darkFileId: String? = nil) -> ImagesSourceInput {
ImagesSourceInput(
sourceKind: sourceKind?.coreSourceKind ?? .figma,
figmaFileId: figmaFileId,
sourceKind: resolvedSourceKind,
figmaFileId: resolvedFileId,
darkFileId: darkFileId,
frameName: figmaFrameName ?? "Images",
pageName: figmaPageName,
Expand All @@ -55,7 +56,8 @@ public extension Flutter.ImagesEntry {
darkModeSuffix: "_dark",
rtlProperty: rtlProperty,
nameValidateRegexp: nameValidateRegexp,
nameReplaceRegexp: nameReplaceRegexp
nameReplaceRegexp: nameReplaceRegexp,
penpotBaseURL: resolvedPenpotBaseURL
)
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/ExFig-Web/Config/WebIconsEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ public extension Web.IconsEntry {
/// Returns an IconsSourceInput for use with IconsExportContext.
func iconsSourceInput(darkFileId: String? = nil) -> IconsSourceInput {
IconsSourceInput(
sourceKind: sourceKind?.coreSourceKind ?? .figma,
figmaFileId: figmaFileId,
sourceKind: resolvedSourceKind,
figmaFileId: resolvedFileId,
darkFileId: darkFileId,
frameName: figmaFrameName ?? "Icons",
pageName: figmaPageName,
useSingleFile: darkFileId == nil,
darkModeSuffix: "_dark",
rtlProperty: rtlProperty,
nameValidateRegexp: nameValidateRegexp,
nameReplaceRegexp: nameReplaceRegexp
nameReplaceRegexp: nameReplaceRegexp,
penpotBaseURL: resolvedPenpotBaseURL
)
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/ExFig-Web/Config/WebImagesEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public extension Web.ImagesEntry {
/// Returns an ImagesSourceInput for use with ImagesExportContext.
func imagesSourceInput(darkFileId: String? = nil) -> ImagesSourceInput {
ImagesSourceInput(
sourceKind: sourceKind?.coreSourceKind ?? .figma,
figmaFileId: figmaFileId,
sourceKind: resolvedSourceKind,
figmaFileId: resolvedFileId,
darkFileId: darkFileId,
frameName: figmaFrameName ?? "Images",
pageName: figmaPageName,
Expand All @@ -22,7 +22,8 @@ public extension Web.ImagesEntry {
darkModeSuffix: "_dark",
rtlProperty: rtlProperty,
nameValidateRegexp: nameValidateRegexp,
nameReplaceRegexp: nameReplaceRegexp
nameReplaceRegexp: nameReplaceRegexp,
penpotBaseURL: resolvedPenpotBaseURL
)
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/ExFig-iOS/Config/iOSIconsEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public extension iOS.IconsEntry {
/// Returns an IconsSourceInput for use with IconsExportContext.
func iconsSourceInput(darkFileId: String? = nil) -> IconsSourceInput {
IconsSourceInput(
sourceKind: sourceKind?.coreSourceKind ?? .figma,
figmaFileId: figmaFileId,
sourceKind: resolvedSourceKind,
figmaFileId: resolvedFileId,
darkFileId: darkFileId,
frameName: figmaFrameName ?? "Icons",
pageName: figmaPageName,
Expand All @@ -27,7 +27,8 @@ public extension iOS.IconsEntry {
renderModeTemplateSuffix: renderModeTemplateSuffix,
rtlProperty: rtlProperty,
nameValidateRegexp: nameValidateRegexp,
nameReplaceRegexp: nameReplaceRegexp
nameReplaceRegexp: nameReplaceRegexp,
penpotBaseURL: resolvedPenpotBaseURL
)
}

Expand Down
Loading
Loading