claude-code-pilot 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +14 -9
  3. package/bin/install.js +113 -15
  4. package/manifest.json +18 -3
  5. package/package.json +3 -2
  6. package/src/agents/django-build-resolver.md +252 -0
  7. package/src/agents/django-reviewer.md +169 -0
  8. package/src/agents/fastapi-reviewer.md +79 -0
  9. package/src/agents/fsharp-reviewer.md +109 -0
  10. package/src/agents/swift-build-resolver.md +170 -0
  11. package/src/agents/swift-reviewer.md +116 -0
  12. package/src/commands/ccp/cost-report.md +107 -0
  13. package/src/commands/ccp/intel.md +3 -3
  14. package/src/commands/ccp/mvp-phase.md +45 -0
  15. package/src/commands/ccp/plan-prd.md +160 -0
  16. package/src/commands/ccp/pr-ecc.md +184 -0
  17. package/src/commands/ccp/security-scan.md +74 -0
  18. package/src/hooks/ccp-bash-hook-dispatcher.js +96 -0
  19. package/src/hooks/ccp-context-monitor.js +23 -0
  20. package/src/hooks/ccp-doc-file-warning.js +93 -0
  21. package/src/hooks/ccp-pre-bash-dispatcher.js +24 -0
  22. package/src/hooks/ccp-write-gateguard.js +868 -0
  23. package/src/lib/project-detect.js +0 -2
  24. package/src/lib/shell-substitution.js +499 -0
  25. package/src/pilot/references/execute-mvp-tdd.md +81 -0
  26. package/src/pilot/references/mvp-concepts.md +49 -0
  27. package/src/pilot/references/planner-graphify-auto-update.md +67 -0
  28. package/src/pilot/references/planner-human-verify-mode.md +57 -0
  29. package/src/pilot/references/planner-mvp-mode.md +53 -0
  30. package/src/pilot/references/skeleton-template.md +48 -0
  31. package/src/pilot/references/spidr-splitting.md +69 -0
  32. package/src/pilot/references/user-story-template.md +58 -0
  33. package/src/pilot/references/verify-mvp-mode.md +85 -0
  34. package/src/pilot/references/worktree-path-safety.md +89 -0
  35. package/src/pilot/workflows/help.md +5 -0
  36. package/src/pilot/workflows/mvp-phase.md +199 -0
  37. package/src/skills/agent-architecture-audit/SKILL.md +256 -0
  38. package/src/skills/agent-harness-design/SKILL.md +73 -0
  39. package/src/skills/angular-developer/SKILL.md +154 -0
  40. package/src/skills/angular-developer/references/angular-animations.md +160 -0
  41. package/src/skills/angular-developer/references/angular-aria.md +410 -0
  42. package/src/skills/angular-developer/references/cli.md +86 -0
  43. package/src/skills/angular-developer/references/component-harnesses.md +59 -0
  44. package/src/skills/angular-developer/references/component-styling.md +91 -0
  45. package/src/skills/angular-developer/references/components.md +117 -0
  46. package/src/skills/angular-developer/references/creating-services.md +97 -0
  47. package/src/skills/angular-developer/references/data-resolvers.md +69 -0
  48. package/src/skills/angular-developer/references/define-routes.md +67 -0
  49. package/src/skills/angular-developer/references/defining-providers.md +72 -0
  50. package/src/skills/angular-developer/references/di-fundamentals.md +120 -0
  51. package/src/skills/angular-developer/references/e2e-testing.md +56 -0
  52. package/src/skills/angular-developer/references/effects.md +83 -0
  53. package/src/skills/angular-developer/references/hierarchical-injectors.md +43 -0
  54. package/src/skills/angular-developer/references/host-elements.md +80 -0
  55. package/src/skills/angular-developer/references/injection-context.md +63 -0
  56. package/src/skills/angular-developer/references/inputs.md +101 -0
  57. package/src/skills/angular-developer/references/linked-signal.md +59 -0
  58. package/src/skills/angular-developer/references/loading-strategies.md +61 -0
  59. package/src/skills/angular-developer/references/mcp.md +108 -0
  60. package/src/skills/angular-developer/references/navigate-to-routes.md +69 -0
  61. package/src/skills/angular-developer/references/outputs.md +86 -0
  62. package/src/skills/angular-developer/references/reactive-forms.md +122 -0
  63. package/src/skills/angular-developer/references/rendering-strategies.md +44 -0
  64. package/src/skills/angular-developer/references/resource.md +77 -0
  65. package/src/skills/angular-developer/references/route-animations.md +56 -0
  66. package/src/skills/angular-developer/references/route-guards.md +52 -0
  67. package/src/skills/angular-developer/references/router-lifecycle.md +45 -0
  68. package/src/skills/angular-developer/references/router-testing.md +87 -0
  69. package/src/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
  70. package/src/skills/angular-developer/references/signal-forms.md +795 -0
  71. package/src/skills/angular-developer/references/signals-overview.md +94 -0
  72. package/src/skills/angular-developer/references/tailwind-css.md +69 -0
  73. package/src/skills/angular-developer/references/template-driven-forms.md +114 -0
  74. package/src/skills/angular-developer/references/testing-fundamentals.md +65 -0
  75. package/src/skills/error-handling/SKILL.md +376 -0
  76. package/src/skills/fastapi-patterns/SKILL.md +327 -0
  77. package/src/skills/flox-environments/SKILL.md +496 -0
  78. package/src/skills/fsharp-testing/SKILL.md +280 -0
  79. package/src/skills/ios-icon-gen/SKILL.md +157 -0
  80. package/src/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
  81. package/src/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
  82. package/src/skills/make-interfaces-feel-better/SKILL.md +151 -0
  83. package/src/skills/mysql-patterns/SKILL.md +412 -0
  84. package/src/skills/plan-orchestrate/SKILL.md +220 -0
  85. package/src/skills/prisma-patterns/SKILL.md +371 -0
  86. package/src/skills/production-audit/SKILL.md +206 -0
  87. package/src/skills/security-scan/references/agentshield-policy-exception/candidate-playbook.md +49 -0
  88. package/src/skills/security-scan/references/agentshield-policy-exception/report.json +35 -0
  89. package/src/skills/security-scan/references/agentshield-policy-exception/scenario.json +62 -0
  90. package/src/skills/security-scan/references/agentshield-policy-exception/trace.json +45 -0
  91. package/src/skills/security-scan/references/agentshield-policy-exception/verifier-result.json +35 -0
  92. package/src/skills/vite-patterns/SKILL.md +449 -0
  93. package/src/skills/windows-desktop-e2e/SKILL.md +887 -0
@@ -0,0 +1,280 @@
1
+ ---
2
+ name: fsharp-testing
3
+ description: F# testing patterns with xUnit, FsUnit, Unquote, FsCheck property-based testing, integration tests, and test organization best practices.
4
+ origin: ECC
5
+ ---
6
+
7
+ # F# Testing Patterns
8
+
9
+ Comprehensive testing patterns for F# applications using xUnit, FsUnit, Unquote, FsCheck, and modern .NET testing practices.
10
+
11
+ ## When to Activate
12
+
13
+ - Writing new tests for F# code
14
+ - Reviewing test quality and coverage
15
+ - Setting up test infrastructure for F# projects
16
+ - Debugging flaky or slow tests
17
+
18
+ ## Test Framework Stack
19
+
20
+ | Tool | Purpose |
21
+ |---|---|
22
+ | **xUnit** | Test framework (standard .NET ecosystem choice) |
23
+ | **FsUnit.xUnit** | F#-friendly assertion syntax for xUnit |
24
+ | **Unquote** | Assertion library using F# quotations for clear failure messages |
25
+ | **FsCheck.xUnit** | Property-based testing integrated with xUnit |
26
+ | **NSubstitute** | Mocking .NET dependencies |
27
+ | **Testcontainers** | Real infrastructure in integration tests |
28
+ | **WebApplicationFactory** | ASP.NET Core integration tests |
29
+
30
+ ## Unit Tests with xUnit + FsUnit
31
+
32
+ ### Basic Test Structure
33
+
34
+ ```fsharp
35
+ module OrderServiceTests
36
+
37
+ open Xunit
38
+ open FsUnit.Xunit
39
+
40
+ [<Fact>]
41
+ let ``create sets status to Pending`` () =
42
+ let order = Order.create "cust-1" [ validItem ]
43
+ order.Status |> should equal Pending
44
+
45
+ [<Fact>]
46
+ let ``confirm changes status to Confirmed`` () =
47
+ let order = Order.create "cust-1" [ validItem ]
48
+ let confirmed = Order.confirm order
49
+ confirmed.Status |> should be (ofCase <@ Confirmed @>)
50
+ ```
51
+
52
+ ### Assertions with Unquote
53
+
54
+ Unquote uses F# quotations so failure messages show the full expression that failed, not just "expected X got Y".
55
+
56
+ ```fsharp
57
+ module OrderValidationTests
58
+
59
+ open Xunit
60
+ open Swensen.Unquote
61
+
62
+ [<Fact>]
63
+ let ``PlaceOrder returns success when request is valid`` () =
64
+ let request = { CustomerId = "cust-123"; Items = [ validItem ] }
65
+ let result = OrderService.placeOrder request
66
+ test <@ Result.isOk result @>
67
+
68
+ [<Fact>]
69
+ let ``order total sums item prices`` () =
70
+ let items = [ { Sku = "A"; Quantity = 2; Price = 10m }
71
+ { Sku = "B"; Quantity = 1; Price = 5m } ]
72
+ let total = Order.calculateTotal items
73
+ test <@ total = 25m @>
74
+
75
+ [<Fact>]
76
+ let ``validated email rejects empty input`` () =
77
+ let result = ValidatedEmail.create ""
78
+ test <@ Result.isError result @>
79
+ ```
80
+
81
+ ### Async Tests
82
+
83
+ ```fsharp
84
+ [<Fact>]
85
+ let ``PlaceOrder returns success when request is valid`` () = task {
86
+ let deps = createTestDeps ()
87
+ let request = { CustomerId = "cust-123"; Items = [ validItem ] }
88
+
89
+ let! result = OrderService.placeOrder deps request
90
+
91
+ test <@ Result.isOk result @>
92
+ }
93
+
94
+ [<Fact>]
95
+ let ``PlaceOrder returns error when items are empty`` () = task {
96
+ let deps = createTestDeps ()
97
+ let request = { CustomerId = "cust-123"; Items = [] }
98
+
99
+ let! result = OrderService.placeOrder deps request
100
+
101
+ test <@ Result.isError result @>
102
+ }
103
+ ```
104
+
105
+ ### Parameterized Tests with Theory
106
+
107
+ ```fsharp
108
+ [<Theory>]
109
+ [<InlineData("")>]
110
+ [<InlineData(" ")>]
111
+ let ``PlaceOrder rejects empty customer ID`` (customerId: string) =
112
+ let request = { CustomerId = customerId; Items = [ validItem ] }
113
+ let result = OrderService.placeOrder request
114
+ result |> should be (ofCase <@ Error @>)
115
+
116
+ [<Theory>]
117
+ [<InlineData("", false)>]
118
+ [<InlineData("a", false)>]
119
+ [<InlineData("user@example.com", true)>]
120
+ [<InlineData("user+tag@example.co.uk", true)>]
121
+ let ``IsValidEmail returns expected result`` (email: string, expected: bool) =
122
+ test <@ EmailValidator.isValid email = expected @>
123
+ ```
124
+
125
+ ## Property-Based Testing with FsCheck
126
+
127
+ ### Using FsCheck.xUnit
128
+
129
+ ```fsharp
130
+ open FsCheck
131
+ open FsCheck.Xunit
132
+
133
+ [<Property>]
134
+ let ``order total is always non-negative`` (items: NonEmptyList<PositiveInt * decimal>) =
135
+ let orderItems =
136
+ items.Get
137
+ |> List.map (fun (qty, price) ->
138
+ { Sku = "SKU"; Quantity = qty.Get; Price = abs price })
139
+ let total = Order.calculateTotal orderItems
140
+ total >= 0m
141
+
142
+ [<Property>]
143
+ let ``serialization roundtrips`` (order: Order) =
144
+ let json = JsonSerializer.Serialize order
145
+ let deserialized = JsonSerializer.Deserialize<Order> json
146
+ deserialized = order
147
+ ```
148
+
149
+ ### Custom Generators
150
+
151
+ ```fsharp
152
+ type OrderGenerators =
153
+ static member ValidEmail () =
154
+ gen {
155
+ let! user = Gen.elements [ "alice"; "bob"; "carol" ]
156
+ let! domain = Gen.elements [ "example.com"; "test.org" ]
157
+ return $"{user}@{domain}"
158
+ }
159
+ |> Arb.fromGen
160
+
161
+ [<Property(Arbitrary = [| typeof<OrderGenerators> |])>]
162
+ let ``valid emails pass validation`` (email: string) =
163
+ EmailValidator.isValid email
164
+ ```
165
+
166
+ ## Mocking Dependencies
167
+
168
+ ### Function Stubs (Preferred)
169
+
170
+ ```fsharp
171
+ let createTestDeps () =
172
+ let mutable savedOrders = []
173
+ { FindOrder = fun id -> task { return Map.tryFind id testData }
174
+ SaveOrder = fun order -> task { savedOrders <- order :: savedOrders }
175
+ SendNotification = fun _ -> Task.CompletedTask }
176
+
177
+ [<Fact>]
178
+ let ``PlaceOrder saves the confirmed order`` () = task {
179
+ let mutable saved = []
180
+ let deps =
181
+ { createTestDeps () with
182
+ SaveOrder = fun order -> task { saved <- order :: saved } }
183
+
184
+ let! _ = OrderService.placeOrder deps validRequest
185
+
186
+ test <@ saved.Length = 1 @>
187
+ }
188
+ ```
189
+
190
+ ### NSubstitute for .NET Interfaces
191
+
192
+ ```fsharp
193
+ open NSubstitute
194
+
195
+ [<Fact>]
196
+ let ``calls repository with correct ID`` () = task {
197
+ let repo = Substitute.For<IOrderRepository>()
198
+ repo.FindByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
199
+ .Returns(Task.FromResult(Some testOrder))
200
+
201
+ let service = OrderService(repo)
202
+ let! _ = service.GetOrder(testOrder.Id, CancellationToken.None)
203
+
204
+ do! repo.Received(1).FindByIdAsync(testOrder.Id, Arg.Any<CancellationToken>())
205
+ }
206
+ ```
207
+
208
+ ## ASP.NET Core Integration Tests
209
+
210
+ ```fsharp
211
+ type OrderApiTests (factory: WebApplicationFactory<Program>) =
212
+ interface IClassFixture<WebApplicationFactory<Program>>
213
+
214
+ let client =
215
+ factory.WithWebHostBuilder(fun builder ->
216
+ builder.ConfigureServices(fun services ->
217
+ services.RemoveAll<DbContextOptions<AppDbContext>>() |> ignore
218
+ services.AddDbContext<AppDbContext>(fun options ->
219
+ options.UseInMemoryDatabase("TestDb") |> ignore) |> ignore))
220
+ .CreateClient()
221
+
222
+ [<Fact>]
223
+ member _.``GET order returns 404 when not found`` () = task {
224
+ let! response = client.GetAsync($"/api/orders/{Guid.NewGuid()}")
225
+ test <@ response.StatusCode = HttpStatusCode.NotFound @>
226
+ }
227
+ ```
228
+
229
+ ## Test Organization
230
+
231
+ ```
232
+ tests/
233
+ MyApp.Tests/
234
+ Unit/
235
+ OrderServiceTests.fs
236
+ PaymentServiceTests.fs
237
+ Integration/
238
+ OrderApiTests.fs
239
+ OrderRepositoryTests.fs
240
+ Properties/
241
+ OrderPropertyTests.fs
242
+ Helpers/
243
+ TestData.fs
244
+ TestDeps.fs
245
+ ```
246
+
247
+ ## Common Anti-Patterns
248
+
249
+ | Anti-Pattern | Fix |
250
+ |---|---|
251
+ | Testing implementation details | Test behavior and outcomes |
252
+ | Mutable shared test state | Fresh state per test |
253
+ | `Thread.Sleep` in async tests | Use `Task.Delay` with timeout, or polling helpers |
254
+ | Asserting on `sprintf` output | Assert on typed values and pattern matches |
255
+ | Ignoring `CancellationToken` | Always pass and verify cancellation |
256
+ | Skipping property-based tests | Use FsCheck for any function with clear invariants |
257
+
258
+ ## Related Skills
259
+
260
+ - `dotnet-patterns` - Idiomatic .NET patterns, dependency injection, and architecture
261
+ - `csharp-testing` - C# testing patterns (shared infrastructure like WebApplicationFactory and Testcontainers applies to F# too)
262
+
263
+ ## Running Tests
264
+
265
+ ```bash
266
+ # Run all tests
267
+ dotnet test
268
+
269
+ # Run with coverage
270
+ dotnet test --collect:"XPlat Code Coverage"
271
+
272
+ # Run specific project
273
+ dotnet test tests/MyApp.Tests/
274
+
275
+ # Filter by test name
276
+ dotnet test --filter "FullyQualifiedName~OrderService"
277
+
278
+ # Watch mode during development
279
+ dotnet watch test --project tests/MyApp.Tests/
280
+ ```
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: ios-icon-gen
3
+ description: Generate iOS app icons as PNG imagesets for Xcode asset catalogs from SF Symbols (5000+ Apple-native) or Iconify API (275k+ open source icons from 200+ collections). Use when generating icons, creating icon assets, adding icons to asset catalog, or searching for icons for iOS projects.
4
+ origin: community
5
+ ---
6
+
7
+ # iOS Icon Generator
8
+
9
+ Generate PNG icon imagesets for Xcode asset catalogs from two sources.
10
+
11
+ ## When to Activate
12
+
13
+ - Generating icon assets for an iOS/macOS Xcode project
14
+ - Searching for icons across open source collections
15
+ - Creating PNG imagesets (1x, 2x, 3x) for asset catalogs
16
+ - Replacing placeholder icons with production-quality assets
17
+ - Matching existing icon styles in an Xcode project
18
+
19
+ ## Core Principles
20
+
21
+ ### 1. Two Sources, One Output Format
22
+ Both sources produce identical Xcode-compatible imagesets. Choose based on need:
23
+
24
+ | Source | Icons | Requires | Best for |
25
+ |--------|-------|----------|----------|
26
+ | **Iconify API** | 275,000+ from 200+ collections | Internet | Wide selection, specific styles, open source icons |
27
+ | **SF Symbols** | 5,000+ Apple symbols | macOS only | Apple-native style, offline use |
28
+
29
+ ### 2. Always Match Existing Style
30
+ Before generating, check the project's existing icons for size, color, and weight consistency.
31
+
32
+ ### 3. Output Structure
33
+ Both methods produce a complete Xcode imageset:
34
+
35
+ ```
36
+ <output-dir>/<asset-name>.imageset/
37
+ Contents.json
38
+ <asset-name>.png # 1x (68px default)
39
+ <asset-name>@2x.png # 2x (136px default)
40
+ <asset-name>@3x.png # 3x (204px default)
41
+ ```
42
+
43
+ ## Examples
44
+
45
+ ### Step 1: Assess Requirements
46
+
47
+ Determine icon needs: what the icon represents, preferred style, target color, and size.
48
+
49
+ If the project already has icons, check existing style:
50
+ ```bash
51
+ # Check dimensions of existing icon
52
+ sips -g pixelWidth -g pixelHeight path/to/existing@2x.png
53
+ ```
54
+
55
+ ### Step 2: Search for Icons
56
+
57
+ **Iconify API (recommended for wide selection):**
58
+ ```bash
59
+ # Search all collections
60
+ $SKILL_DIR/scripts/iconify_gen.sh search "receipt"
61
+
62
+ # Search within a specific collection
63
+ $SKILL_DIR/scripts/iconify_gen.sh search "business card" --prefix mdi
64
+
65
+ # List available collections
66
+ $SKILL_DIR/scripts/iconify_gen.sh collections
67
+ ```
68
+
69
+ **SF Symbols (for Apple-native style):**
70
+ Browse the SF Symbols app or reference common names:
71
+
72
+ | Use Case | Symbol Name |
73
+ |----------|-------------|
74
+ | Document | `doc.text`, `doc.fill` |
75
+ | Receipt | `doc.text.below.ecg`, `receipt` |
76
+ | Person | `person.crop.rectangle`, `person.text.rectangle` |
77
+ | Camera | `camera`, `camera.fill` |
78
+ | Scan | `doc.viewfinder`, `qrcode.viewfinder` |
79
+ | Settings | `gearshape`, `slider.horizontal.3` |
80
+
81
+ ### Step 3: Preview (Optional)
82
+
83
+ ```bash
84
+ # Iconify preview
85
+ $SKILL_DIR/scripts/iconify_gen.sh preview mdi:receipt-text-outline
86
+ ```
87
+
88
+ ### Step 4: Generate
89
+
90
+ **Iconify API:**
91
+ ```bash
92
+ # Basic generation
93
+ $SKILL_DIR/scripts/iconify_gen.sh mdi:receipt-text-outline editTool_expenseReport
94
+
95
+ # Custom color and output location
96
+ $SKILL_DIR/scripts/iconify_gen.sh mdi:receipt-text-outline myIcon --color 007AFF --output ./Assets.xcassets/icons
97
+ ```
98
+
99
+ Options: `--size <pt>` (default: 68), `--color <hex>` (default: 8E8E93), `--output <dir>` (default: /tmp/icons)
100
+
101
+ **SF Symbols:**
102
+ ```bash
103
+ # Basic generation
104
+ swift $SKILL_DIR/scripts/generate_icons.swift doc.text.below.ecg editTool_expenseReport
105
+
106
+ # Custom color, weight, and output
107
+ swift $SKILL_DIR/scripts/generate_icons.swift person.crop.rectangle myIcon --color 007AFF --weight regular --output ./Assets.xcassets/icons
108
+ ```
109
+
110
+ Options: `--size <pt>` (default: 68), `--color <hex>` (default: 8E8E93), `--weight <name>` (default: thin), `--output <dir>` (default: /tmp/icons)
111
+
112
+ ### Step 5: Verify and Integrate
113
+
114
+ 1. Read the generated @2x PNG to verify visually
115
+ 2. Copy to asset catalog if not output there directly:
116
+ ```bash
117
+ cp -r /tmp/icons/<name>.imageset path/to/Assets.xcassets/<group>/
118
+ ```
119
+ 3. Build the project to verify Xcode picks up the new assets
120
+
121
+ ## Popular Iconify Collections
122
+
123
+ | Prefix | Name | Count | Style |
124
+ |--------|------|-------|-------|
125
+ | `mdi` | Material Design Icons | 7400+ | Filled + outline variants |
126
+ | `ph` | Phosphor | 9000+ | 6 weights per icon |
127
+ | `solar` | Solar | 7400+ | Bold, linear, outline |
128
+ | `tabler` | Tabler Icons | 6000+ | Consistent stroke width |
129
+ | `lucide` | Lucide | 1700+ | Clean, minimal |
130
+ | `ri` | Remix Icon | 3100+ | Filled + line variants |
131
+ | `carbon` | Carbon | 2400+ | IBM design language |
132
+ | `heroicons` | HeroIcons | 1200+ | Tailwind CSS companion |
133
+
134
+ Browse all: <https://icon-sets.iconify.design/>
135
+
136
+ ## Scripts Reference
137
+
138
+ | Script | Source | Path |
139
+ |--------|--------|------|
140
+ | `iconify_gen.sh` | Iconify API (275k+ icons) | `$SKILL_DIR/scripts/iconify_gen.sh` |
141
+ | `generate_icons.swift` | SF Symbols (5k+ icons) | `$SKILL_DIR/scripts/generate_icons.swift` |
142
+
143
+ ## Best Practices
144
+
145
+ - **Search before generating** -- browse available icons to find the best match
146
+ - **Match existing project style** -- check dimensions, color, and weight of existing icons before generating new ones
147
+ - **Use Iconify for variety** -- 200+ collections means you can find the exact style you need
148
+ - **Use SF Symbols for Apple consistency** -- they match system UI perfectly
149
+ - **Generate directly to asset catalog** -- use `--output ./Assets.xcassets/icons` to skip manual copying
150
+ - **Verify visually** -- always preview the @2x PNG before committing
151
+
152
+ ## Anti-Patterns
153
+
154
+ - Generating icons without checking existing project icon style
155
+ - Using default colors when the project has a defined color palette
156
+ - Generating at wrong sizes (check existing icons first)
157
+ - Committing generated icons without visual verification
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env swift
2
+
3
+ import AppKit
4
+ import Foundation
5
+
6
+ // MARK: - Configuration
7
+
8
+ struct IconSpec {
9
+ let symbolName: String
10
+ let assetName: String
11
+ let baseSize: CGFloat
12
+ let color: NSColor
13
+ let weight: NSFont.Weight
14
+ }
15
+
16
+ func parseColor(_ hex: String) -> NSColor {
17
+ var hex = hex.trimmingCharacters(in: .whitespacesAndNewlines)
18
+ if hex.hasPrefix("#") { hex.removeFirst() }
19
+ guard hex.count == 6, let value = UInt64(hex, radix: 16) else {
20
+ return NSColor(red: 142/255, green: 142/255, blue: 147/255, alpha: 1.0)
21
+ }
22
+ return NSColor(
23
+ red: CGFloat((value >> 16) & 0xFF) / 255,
24
+ green: CGFloat((value >> 8) & 0xFF) / 255,
25
+ blue: CGFloat(value & 0xFF) / 255,
26
+ alpha: 1.0
27
+ )
28
+ }
29
+
30
+ func parseWeight(_ name: String) -> NSFont.Weight {
31
+ switch name.lowercased() {
32
+ case "ultralight": return .ultraLight
33
+ case "thin": return .thin
34
+ case "light": return .light
35
+ case "regular": return .regular
36
+ case "medium": return .medium
37
+ case "semibold": return .semibold
38
+ case "bold": return .bold
39
+ case "heavy": return .heavy
40
+ case "black": return .black
41
+ default: return .thin
42
+ }
43
+ }
44
+
45
+ // MARK: - Generation
46
+
47
+ enum IconError: Error, CustomStringConvertible {
48
+ case directoryCreation(String)
49
+ case symbolNotFound(String)
50
+ case configurationFailed(String)
51
+ case pngCreation(String)
52
+ case fileWrite(String)
53
+
54
+ var description: String {
55
+ switch self {
56
+ case .directoryCreation(let msg): return msg
57
+ case .symbolNotFound(let msg): return msg
58
+ case .configurationFailed(let msg): return msg
59
+ case .pngCreation(let msg): return msg
60
+ case .fileWrite(let msg): return msg
61
+ }
62
+ }
63
+ }
64
+
65
+ func generateIcon(_ spec: IconSpec, outputDir: String) throws {
66
+ let dir = "\(outputDir)/\(spec.assetName).imageset"
67
+ do {
68
+ try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
69
+ } catch {
70
+ throw IconError.directoryCreation("Could not create output directory '\(dir)': \(error.localizedDescription)")
71
+ }
72
+
73
+ let scales: [(suffix: String, multiplier: CGFloat)] = [("", 1), ("@2x", 2), ("@3x", 3)]
74
+
75
+ for scale in scales {
76
+ let pixelSize = spec.baseSize * scale.multiplier
77
+ let imageSize = NSSize(width: pixelSize, height: pixelSize)
78
+
79
+ let config = NSImage.SymbolConfiguration(
80
+ pointSize: pixelSize * 0.40,
81
+ weight: spec.weight,
82
+ scale: .large
83
+ )
84
+
85
+ guard let symbol = NSImage(systemSymbolName: spec.symbolName, accessibilityDescription: nil) else {
86
+ throw IconError.symbolNotFound("SF Symbol '\(spec.symbolName)' not found. Run 'SF Symbols' app to browse available names.")
87
+ }
88
+
89
+ guard let configured = symbol.withSymbolConfiguration(config) else {
90
+ throw IconError.configurationFailed("Could not apply symbol configuration to '\(spec.symbolName)'")
91
+ }
92
+
93
+ let image = NSImage(size: imageSize, flipped: false) { rect in
94
+ let symSize = configured.size
95
+ let x = (rect.width - symSize.width) / 2
96
+ let y = (rect.height - symSize.height) / 2
97
+ let drawRect = NSRect(x: x, y: y, width: symSize.width, height: symSize.height)
98
+
99
+ let tinted = NSImage(size: symSize, flipped: false) { tintRect in
100
+ configured.draw(in: tintRect)
101
+ spec.color.set()
102
+ tintRect.fill(using: .sourceAtop)
103
+ return true
104
+ }
105
+
106
+ tinted.draw(in: drawRect, from: .zero, operation: .sourceOver, fraction: 1.0)
107
+ return true
108
+ }
109
+
110
+ guard let tiffData = image.tiffRepresentation,
111
+ let bitmap = NSBitmapImageRep(data: tiffData),
112
+ let pngData = bitmap.representation(using: .png, properties: [:]) else {
113
+ throw IconError.pngCreation("Failed to create PNG for \(spec.assetName)\(scale.suffix)")
114
+ }
115
+
116
+ let fileName = "\(spec.assetName)\(scale.suffix).png"
117
+ do {
118
+ try pngData.write(to: URL(fileURLWithPath: "\(dir)/\(fileName)"))
119
+ } catch {
120
+ throw IconError.fileWrite("Failed to write \(fileName): \(error.localizedDescription)")
121
+ }
122
+ print(" \(fileName) (\(Int(pixelSize))x\(Int(pixelSize)))")
123
+ }
124
+
125
+ // Write Contents.json
126
+ let json = """
127
+ {
128
+ "images" : [
129
+ {
130
+ "filename" : "\(spec.assetName).png",
131
+ "idiom" : "universal",
132
+ "scale" : "1x"
133
+ },
134
+ {
135
+ "filename" : "\(spec.assetName)@2x.png",
136
+ "idiom" : "universal",
137
+ "scale" : "2x"
138
+ },
139
+ {
140
+ "filename" : "\(spec.assetName)@3x.png",
141
+ "idiom" : "universal",
142
+ "scale" : "3x"
143
+ }
144
+ ],
145
+ "info" : {
146
+ "author" : "xcode",
147
+ "version" : 1
148
+ }
149
+ }
150
+ """
151
+ do {
152
+ try json.write(toFile: "\(dir)/Contents.json", atomically: true, encoding: .utf8)
153
+ } catch {
154
+ throw IconError.fileWrite("Failed to write Contents.json: \(error.localizedDescription)")
155
+ }
156
+ }
157
+
158
+ func requireOptionValue(_ args: [String], at index: Int, flag: String) -> String {
159
+ guard index < args.count else {
160
+ fputs("ERROR: Missing value for \(flag)\n", stderr)
161
+ exit(1)
162
+ }
163
+ let value = args[index]
164
+ if value.hasPrefix("--") {
165
+ fputs("ERROR: Missing value for \(flag)\n", stderr)
166
+ exit(1)
167
+ }
168
+ return value
169
+ }
170
+
171
+ // MARK: - CLI
172
+
173
+ let args = CommandLine.arguments
174
+
175
+ if args.count < 3 || args.contains("--help") || args.contains("-h") {
176
+ print("""
177
+ Usage: generate_icons.swift <sf-symbol-name> <asset-name> [options]
178
+
179
+ Options:
180
+ --size <pt> Base size in points (default: 68)
181
+ --color <hex> Color hex code (default: 8E8E93)
182
+ --weight <name> Font weight: ultralight|thin|light|regular|medium|semibold|bold|heavy|black (default: thin)
183
+ --output <dir> Output directory (default: /tmp/icons)
184
+
185
+ Examples:
186
+ generate_icons.swift doc.text.below.ecg editTool_expenseReport
187
+ generate_icons.swift person.crop.rectangle editTool_businessCard --color 007AFF --weight regular
188
+ generate_icons.swift receipt myReceipt --size 48 --output ./Assets.xcassets/icons
189
+
190
+ Browse SF Symbol names: open the SF Symbols app (free from Apple) or https://developer.apple.com/sf-symbols/
191
+ """)
192
+ exit(0)
193
+ }
194
+
195
+ let symbolName = args[1]
196
+ let assetName = args[2]
197
+
198
+ var baseSize: CGFloat = 68
199
+ var colorHex = "8E8E93"
200
+ var weightName = "thin"
201
+ var outputDir = "/tmp/icons"
202
+
203
+ var i = 3
204
+ while i < args.count {
205
+ switch args[i] {
206
+ case "--size":
207
+ let raw = requireOptionValue(args, at: i + 1, flag: "--size")
208
+ guard let size = Double(raw), size > 0 else {
209
+ fputs("ERROR: --size must be a positive number\n", stderr)
210
+ exit(1)
211
+ }
212
+ baseSize = CGFloat(size)
213
+ i += 2
214
+ continue
215
+ case "--color":
216
+ colorHex = requireOptionValue(args, at: i + 1, flag: "--color")
217
+ let stripped = colorHex.hasPrefix("#") ? String(colorHex.dropFirst()) : colorHex
218
+ guard stripped.count == 6, UInt64(stripped, radix: 16) != nil else {
219
+ fputs("ERROR: --color must be a 6-digit hex code (e.g. 007AFF)\n", stderr)
220
+ exit(1)
221
+ }
222
+ i += 2
223
+ continue
224
+ case "--weight":
225
+ weightName = requireOptionValue(args, at: i + 1, flag: "--weight")
226
+ let validWeights = ["ultralight", "thin", "light", "regular", "medium", "semibold", "bold", "heavy", "black"]
227
+ guard validWeights.contains(weightName.lowercased()) else {
228
+ fputs("ERROR: --weight must be one of: \(validWeights.joined(separator: ", "))\n", stderr)
229
+ exit(1)
230
+ }
231
+ i += 2
232
+ continue
233
+ case "--output":
234
+ outputDir = requireOptionValue(args, at: i + 1, flag: "--output")
235
+ i += 2
236
+ continue
237
+ default:
238
+ fputs("WARNING: Unknown option \(args[i])\n", stderr)
239
+ }
240
+ i += 1
241
+ }
242
+
243
+ let spec = IconSpec(
244
+ symbolName: symbolName,
245
+ assetName: assetName,
246
+ baseSize: baseSize,
247
+ color: parseColor(colorHex),
248
+ weight: parseWeight(weightName)
249
+ )
250
+
251
+ print("Generating \(assetName) from SF Symbol '\(symbolName)':")
252
+ do {
253
+ try generateIcon(spec, outputDir: outputDir)
254
+ print("Output: \(outputDir)/\(assetName).imageset/")
255
+ } catch {
256
+ fputs("ERROR: \(error)\n", stderr)
257
+ exit(1)
258
+ }