Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 58 additions & 27 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ Tests/ # Test targets mirror source structure

## Code Patterns

### Generated PKL Config URIs

Templates in `*Config.swift` use `.exfig/schemas/` as placeholder paths. `GenerateConfigFile.substitutePackageURI()`
replaces them with `package://github.com/DesignPipe/exfig/releases/download/v{VERSION}/exfig@{VERSION}#/` at generation
time. Version comes from `ExFigCommand.version`. `exfig init` does NOT extract local schemas — config references the
published PKL package directly.

### PKL Consumer Config DRY Patterns

Consumer `exfig.pkl` configs can use `local` Mapping + `for`-generators to eliminate entry duplication:
Expand Down Expand Up @@ -260,6 +267,22 @@ See `ExFigCLI/CLAUDE.md` (Adding a New Subcommand).

**Important:** When adding/changing CLI flags or subcommands, update `exfig.usage.kdl` (Usage spec) to keep shell completions and docs in sync. When bumping the app version in `ExFigCommand.swift`, also update the `version` field in `exfig.usage.kdl`.

### Adding an Interactive Wizard

Follow `InitWizard.swift` / `FetchWizard.swift` pattern:

- `enum` with `static func run()` for interactive flow (NooraUI prompts)
- Pure function for testable transformation logic (e.g., `applyResult(_:to:)`)
- Reuse `WizardPlatform` from `FetchWizard.swift` (has `asPlatform` property)
- Gate on `TTYDetector.isTTY`; throw `ValidationError` for non-TTY without required flags
- Use `extractFigmaFileId(from:)` for file ID inputs (auto-extracts ID from full Figma URLs)
- Trim text prompt results with `.trimmingCharacters(in: .whitespacesAndNewlines)` before `.isEmpty` default checks

### Adding a NooraUI Prompt Wrapper

Follow the existing pattern in `NooraUI.swift`: static method delegating to `shared` instance with matching parameter names.
Noora's `multipleChoicePrompt` uses `MultipleChoiceLimit` — `.unlimited` or `.limited(count:errorMessage:)`.

### Adding a Figma API Endpoint

FigmaAPI is now an external package (`swift-figma-api`). See its repository for endpoint patterns.
Expand All @@ -282,6 +305,10 @@ See `ExFigCore/CLAUDE.md` (Modification Checklist) and platform module CLAUDE.md
| Terminal output | `TerminalUI` facade | Direct `print()` calls |
| README.md | Keep compact (~80 lines, pain-driven) | Detailed docs (use CONFIG.md / DocC) |

**Documentation structure:** README is a short pain-driven intro (~80 lines). Detailed docs live in DocC articles
(`Sources/ExFigCLI/ExFig.docc/`). When adding new features, mention briefly in README Quick Start AND update
relevant DocC articles (`Usage.md` for CLI, `ExFig.md` landing page for capabilities).

**JSONCodec usage:**

```swift
Expand Down Expand Up @@ -339,33 +366,37 @@ NooraUI.formatLink("url", useColors: true) // underlined primary

## Troubleshooting

| Problem | Solution |
| --------------------------- | ------------------------------------------------------------------------------------------------------------- |
| codegen:pkl gen.pkl error | gen.pkl `read?` bug: needs `--generator-settings` + `--project-dir` flags (see mise.toml) |
| xcsift "signal code 5" | False positive when piping `swift test` through xcsift; run `swift test` directly to verify |
| PKL tests need Pkl 0.31+ | Schemas use `isNotEmpty`; run tests via `./bin/mise exec -- swift test` to get correct Pkl in PATH |
| PKL FrameSource change | Update ALL entry init calls in tests (EnumBridgingTests, IconsLoaderConfigTests) |
| Build fails | `swift package clean && swift build` |
| Tests fail | Check `FIGMA_PERSONAL_TOKEN` is set |
| Formatting fails | Run `./bin/mise run setup` to install tools |
| test:filter no matches | SPM converts hyphens→underscores: use `ExFig_FlutterTests` not `ExFig-FlutterTests` |
| Template errors | Check Jinja2 syntax and context variables |
| Linux test hangs | Build first: `swift build --build-tests`, then `swift test --skip-build --parallel` |
| Android pathData long | Simplify in Figma or use `--strict-path-validation` |
| PKL parse error 1 | Check `PklError.message` — actual error is in `.message`, not `.localizedDescription` |
| Test target won't compile | Broken test files block entire target; use `swift test --filter Target.Class` after `build` |
| Test helper JSON decode | `ContainingFrame` uses default Codable (camelCase: `nodeId`, `pageName`), NOT snake_case |
| Web entry test fails | Web entry types use `outputDirectory` field, while Android/Flutter use `output` |
| Logger concatenation err | `Logger.Message` (swift-log) requires interpolation `"\(a) \(b)"`, not concatenation `a + b` |
| Deleted variables in output | Filter `VariableValue.deletedButReferenced != true` in variable loaders AND `CodeSyntaxSyncer` |
| mise "sources up-to-date" | mise caches tasks with `sources`/`outputs` — run script directly via `bash` when debugging |
| Jinja trailing `\n` | `{% if false %}...{% endif %}\n` renders `"\n"`, not `""` — strip whitespace-only partial template results |
| `Bundle.module` in tests | SPM test targets without declared resources don't have `Bundle.module` — use `Bundle.main` or temp bundle |
| SwiftLint trailing closure | When function takes 2+ closures, use explicit label for last closure (`export: { ... }`) not trailing syntax |
| MCP `Client` ambiguous | `FigmaAPI.Client` vs `MCP.Client` — always use `FigmaAPI.Client` in MCP/ files |
| MCP `FigmaConfig` fields | No `colorsFileId` — use `config.getFileIds()` or `figma.lightFileId`/`darkFileId` |
| `distantFuture` on clock | `ContinuousClock.Instant` has no `distantFuture`; use `withCheckedContinuation { _ in }` for infinite suspend |
| MCP stderr duplication | `TerminalOutputManager.setStderrMode(true)` handles all output routing — don't duplicate in `ExFigLogHandler` |
| Problem | Solution |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| codegen:pkl gen.pkl error | gen.pkl `read?` bug: needs `--generator-settings` + `--project-dir` flags (see mise.toml) |
| xcsift "signal code 5" | False positive when piping `swift test` through xcsift; run `swift test` directly to verify |
| PKL tests need Pkl 0.31+ | Schemas use `isNotEmpty`; run tests via `./bin/mise exec -- swift test` to get correct Pkl in PATH |
| PKL FrameSource change | Update ALL entry init calls in tests (EnumBridgingTests, IconsLoaderConfigTests) |
| Build fails | `swift package clean && swift build` |
| Tests fail | Check `FIGMA_PERSONAL_TOKEN` is set |
| Formatting fails | Run `./bin/mise run setup` to install tools |
| test:filter no matches | SPM converts hyphens→underscores: use `ExFig_FlutterTests` not `ExFig-FlutterTests` |
| Template errors | Check Jinja2 syntax and context variables |
| Linux test hangs | Build first: `swift build --build-tests`, then `swift test --skip-build --parallel` |
| Android pathData long | Simplify in Figma or use `--strict-path-validation` |
| PKL parse error 1 | Check `PklError.message` — actual error is in `.message`, not `.localizedDescription` |
| Test target won't compile | Broken test files block entire target; use `swift test --filter Target.Class` after `build` |
| Test helper JSON decode | `ContainingFrame` uses default Codable (camelCase: `nodeId`, `pageName`), NOT snake_case |
| Web entry test fails | Web entry types use `outputDirectory` field, while Android/Flutter use `output` |
| Logger concatenation err | `Logger.Message` (swift-log) requires interpolation `"\(a) \(b)"`, not concatenation `a + b` |
| Deleted variables in output | Filter `VariableValue.deletedButReferenced != true` in variable loaders AND `CodeSyntaxSyncer` |
| mise "sources up-to-date" | mise caches tasks with `sources`/`outputs` — run script directly via `bash` when debugging |
| Jinja trailing `\n` | `{% if false %}...{% endif %}\n` renders `"\n"`, not `""` — strip whitespace-only partial template results |
| `Bundle.module` in tests | SPM test targets without declared resources don't have `Bundle.module` — use `Bundle.main` or temp bundle |
| SwiftLint trailing closure | When function takes 2+ closures, use explicit label for last closure (`export: { ... }`) not trailing syntax |
| CLI flag default vs absent | swift-argument-parser can't distinguish explicit `--flag default_value` from omitted. Use `Optional` + computed `effectiveX` property for flags that wizard may override |
| MCP `Client` ambiguous | `FigmaAPI.Client` vs `MCP.Client` — always use `FigmaAPI.Client` in MCP/ files |
| MCP `FigmaConfig` fields | No `colorsFileId` — use `config.getFileIds()` or `figma.lightFileId`/`darkFileId` |
| `distantFuture` on clock | `ContinuousClock.Instant` has no `distantFuture`; use `withCheckedContinuation { _ in }` for infinite suspend |
| MCP stderr duplication | `TerminalOutputManager.setStderrMode(true)` handles all output routing — don't duplicate in `ExFigLogHandler` |
| PKL template word search | Template comments on `lightFileId` contain cross-refs (`variablesColors`, `typography`); test section removal by matching full markers (`colors = new Common.Colors {`) not bare words |
| CI llms-full.txt stale | `llms-full.txt` is generated from README + DocC articles; after editing `Usage.md`, `ExFig.md`, or `README.md`, run `./bin/mise run generate:llms` and commit the result |
| Release build .pcm warnings | Stale `ModuleCache` — clean with: `rm -r .build/*/release/ModuleCache` then rebuild |

## Additional Rules

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ brew install designpipe/tap/exfig
# 2. Set Figma token
export FIGMA_PERSONAL_TOKEN=your_token_here

# 3. Generate config and export
exfig init -p ios
# 3a. Quick one-off export (interactive wizard)
exfig fetch

# 3b. Or generate config for full pipeline (interactive wizard)
exfig init
exfig batch exfig.pkl
```

Expand Down
16 changes: 16 additions & 0 deletions Scripts/check-llms-freshness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
# Check if llms.txt / llms-full.txt are up to date with documentation sources.
# Generates to a temp directory (parallel-safe) and compares with repo versions.
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"

TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

LLMS_TXT="$TMPDIR/llms.txt" LLMS_FULL="$TMPDIR/llms-full.txt" \
bash Scripts/generate-llms.sh > /dev/null 2>&1

diff -q llms.txt "$TMPDIR/llms.txt" > /dev/null 2>&1 \
&& diff -q llms-full.txt "$TMPDIR/llms-full.txt" > /dev/null 2>&1
4 changes: 2 additions & 2 deletions Scripts/generate-llms.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ cd "$REPO_ROOT"
GITHUB_BLOB="https://github.com/DesignPipe/exfig/blob/main"
DOCC_BASE="https://designpipe.github.io/exfig/documentation/exfigcli"

LLMS_TXT="llms.txt"
LLMS_FULL="llms-full.txt"
LLMS_TXT="${LLMS_TXT:-llms.txt}"
LLMS_FULL="${LLMS_FULL:-llms-full.txt}"

# ── Header (shared) ──────────────────────────────────────────────────────────

Expand Down
21 changes: 21 additions & 0 deletions Sources/ExFigCLI/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,27 @@ reserved for MCP JSON-RPC protocol.
3. Use `@OptionGroup` for shared options
4. Call `initializeTerminalUI()` + `checkSchemaVersionIfNeeded()` in `run()`

### Adding an Interactive Wizard

Follow `InitWizard.swift` / `FetchWizard.swift` pattern:

1. Create `Subcommands/NewWizard.swift` as `enum` with `static func run() -> Result`
2. Use NooraUI prompts (`textPrompt`, `singleChoicePrompt`, `multipleChoicePrompt`, `yesOrNoPrompt`)
3. Extract testable pure logic into a separate static method (e.g., `applyResult(_:to:)`)
4. Reuse `WizardPlatform` from `FetchWizard.swift` (has `asPlatform` → `Platform` mapping)
5. Gate on `TTYDetector.isTTY` in the calling command; throw `ValidationError` for non-TTY

**File split pattern:** Keep types + interactive prompts in `*Wizard.swift` (~230 lines), extract pure
transformation logic into `*WizardTransform.swift` as `extension`. SwiftLint enforces 400-line file / 300-line type body limits.

**Test file split pattern:** When a `@Suite` struct exceeds 300 lines, extract groups of tests into separate `@Suite` structs in the same file (e.g., `InitWizardCrossPlatformTests`, `InitWizardTransformUtilityTests`).

**Template transformations** (three operations on PKL templates):

- **Remove section** — brace-counting (`removeSection`), strips preceding comments
- **Substitute value** — simple `replacingOccurrences` for file IDs, frame names
- **Uncomment block** — strip `//` prefix, substitute values (variablesColors, figmaPageName)

### Adding a New Platform Export

1. Create platform export orchestrator in `Subcommands/Export/` (e.g., `NewPlatformColorsExport.swift`)
Expand Down
3 changes: 2 additions & 1 deletion Sources/ExFigCLI/ExFig.docc/ExFig.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ and Figma Variables integration.
**Export Formats**
PNG, SVG, PDF, JPEG, WebP, HEIC output formats with quality control.
W3C Design Tokens (DTCG v2025) for token pipelines.
Quick fetch mode for one-off downloads without a config file.
Quick fetch mode with interactive wizard for one-off downloads without a config file.
Interactive `exfig init` wizard for guided config setup with file IDs and asset selection.

**Performance and Reliability**
Parallel downloads and writes, batch processing with shared rate limiting,
Expand Down
28 changes: 26 additions & 2 deletions Sources/ExFigCLI/ExFig.docc/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ exfig images
exfig typography
```

## Getting Started

Generate a config file with the interactive wizard:

```bash
exfig init
```

The wizard guides you through platform selection, asset types, and Figma file IDs.
For non-interactive use, specify the platform directly:

```bash
exfig init -p ios # Full iOS template
exfig init -p android # Full Android template
```

## Configuration File

By default, ExFig looks for `exfig.pkl` in the current directory. Specify a different location:
Expand Down Expand Up @@ -141,16 +157,24 @@ exfig icons --resume

## Quick Fetch

Download images without a configuration file:
Download images without a configuration file. Run `exfig fetch` with no arguments for an
interactive wizard that guides you through file ID, asset type, platform, frame selection,
format, output directory, and more:

```bash
# Download PNG images at 3x scale
# Interactive wizard — asks platform, format, output step by step
exfig fetch

# Or pass all options directly
exfig fetch --file-id YOUR_FILE_ID --frame "Illustrations" --output ./images

# Using short options
exfig fetch -f YOUR_FILE_ID -r "Icons" -o ./icons
```

The wizard provides smart defaults per platform (e.g., SVG + camelCase for iOS icons,
WebP + snake_case for Android illustrations) and only runs in interactive terminals.

### Format Options

```bash
Expand Down
Loading
Loading