ts-procedures 6.0.2 → 6.2.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 (199) hide show
  1. package/agent_config/bin/setup.mjs +2 -2
  2. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +2 -0
  3. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +2 -0
  4. package/agent_config/claude-code/skills/ts-procedures-kotlin/SKILL.md +106 -0
  5. package/agent_config/claude-code/skills/ts-procedures-swift/SKILL.md +119 -0
  6. package/agent_config/copilot/copilot-instructions.md +3 -0
  7. package/agent_config/cursor/cursorrules +3 -0
  8. package/agent_config/lib/install-claude.mjs +1 -1
  9. package/build/codegen/bin/cli.d.ts +39 -0
  10. package/build/codegen/bin/cli.js +164 -0
  11. package/build/codegen/bin/cli.js.map +1 -1
  12. package/build/codegen/bin/cli.test.js +180 -1
  13. package/build/codegen/bin/cli.test.js.map +1 -1
  14. package/build/codegen/index.d.ts +36 -0
  15. package/build/codegen/index.js +8 -0
  16. package/build/codegen/index.js.map +1 -1
  17. package/build/codegen/pipeline.d.ts +22 -4
  18. package/build/codegen/pipeline.js +44 -86
  19. package/build/codegen/pipeline.js.map +1 -1
  20. package/build/codegen/pipeline.test.js +162 -0
  21. package/build/codegen/pipeline.test.js.map +1 -1
  22. package/build/codegen/targets/_shared/error-schemas.d.ts +10 -0
  23. package/build/codegen/targets/_shared/error-schemas.js +17 -0
  24. package/build/codegen/targets/_shared/error-schemas.js.map +1 -0
  25. package/build/codegen/targets/_shared/error-schemas.test.d.ts +1 -0
  26. package/build/codegen/targets/_shared/error-schemas.test.js +38 -0
  27. package/build/codegen/targets/_shared/error-schemas.test.js.map +1 -0
  28. package/build/codegen/targets/_shared/indent.d.ts +6 -0
  29. package/build/codegen/targets/_shared/indent.js +13 -0
  30. package/build/codegen/targets/_shared/indent.js.map +1 -0
  31. package/build/codegen/targets/_shared/indent.test.d.ts +1 -0
  32. package/build/codegen/targets/_shared/indent.test.js +21 -0
  33. package/build/codegen/targets/_shared/indent.test.js.map +1 -0
  34. package/build/codegen/targets/_shared/pascal-case.d.ts +6 -0
  35. package/build/codegen/targets/_shared/pascal-case.js +13 -0
  36. package/build/codegen/targets/_shared/pascal-case.js.map +1 -0
  37. package/build/codegen/targets/_shared/pascal-case.test.d.ts +1 -0
  38. package/build/codegen/targets/_shared/pascal-case.test.js +25 -0
  39. package/build/codegen/targets/_shared/pascal-case.test.js.map +1 -0
  40. package/build/codegen/targets/_shared/path-utils.d.ts +12 -0
  41. package/build/codegen/targets/_shared/path-utils.js +20 -0
  42. package/build/codegen/targets/_shared/path-utils.js.map +1 -0
  43. package/build/codegen/targets/_shared/path-utils.test.d.ts +1 -0
  44. package/build/codegen/targets/_shared/path-utils.test.js +42 -0
  45. package/build/codegen/targets/_shared/path-utils.test.js.map +1 -0
  46. package/build/codegen/targets/_shared/pick-defined.d.ts +11 -0
  47. package/build/codegen/targets/_shared/pick-defined.js +21 -0
  48. package/build/codegen/targets/_shared/pick-defined.js.map +1 -0
  49. package/build/codegen/targets/_shared/pick-defined.test.d.ts +1 -0
  50. package/build/codegen/targets/_shared/pick-defined.test.js +25 -0
  51. package/build/codegen/targets/_shared/pick-defined.test.js.map +1 -0
  52. package/build/codegen/targets/_shared/route-slots.d.ts +17 -0
  53. package/build/codegen/targets/_shared/route-slots.js +17 -0
  54. package/build/codegen/targets/_shared/route-slots.js.map +1 -0
  55. package/build/codegen/targets/_shared/route-slots.test.d.ts +1 -0
  56. package/build/codegen/targets/_shared/route-slots.test.js +43 -0
  57. package/build/codegen/targets/_shared/route-slots.test.js.map +1 -0
  58. package/build/codegen/targets/_shared/target-run.d.ts +27 -0
  59. package/build/codegen/targets/_shared/target-run.js +2 -0
  60. package/build/codegen/targets/_shared/target-run.js.map +1 -0
  61. package/build/codegen/targets/_shared/write-files.d.ts +24 -0
  62. package/build/codegen/targets/_shared/write-files.js +35 -0
  63. package/build/codegen/targets/_shared/write-files.js.map +1 -0
  64. package/build/codegen/targets/_shared/write-files.test.d.ts +1 -0
  65. package/build/codegen/targets/_shared/write-files.test.js +79 -0
  66. package/build/codegen/targets/_shared/write-files.test.js.map +1 -0
  67. package/build/codegen/targets/kotlin/ajsc-adapter.d.ts +6 -4
  68. package/build/codegen/targets/kotlin/ajsc-adapter.js +12 -7
  69. package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -1
  70. package/build/codegen/targets/kotlin/ajsc-adapter.test.js +20 -2
  71. package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -1
  72. package/build/codegen/targets/kotlin/e2e-compile.test.js +41 -9
  73. package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -1
  74. package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +6 -2
  75. package/build/codegen/targets/kotlin/emit-route-kotlin.js +18 -28
  76. package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -1
  77. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +120 -1
  78. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
  79. package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +4 -1
  80. package/build/codegen/targets/kotlin/emit-scope-kotlin.js +12 -11
  81. package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -1
  82. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +39 -0
  83. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
  84. package/build/codegen/targets/kotlin/format-kotlin.d.ts +0 -1
  85. package/build/codegen/targets/kotlin/format-kotlin.js +0 -7
  86. package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -1
  87. package/build/codegen/targets/kotlin/format-kotlin.test.js +1 -8
  88. package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -1
  89. package/build/codegen/targets/kotlin/integration.test.js +27 -10
  90. package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
  91. package/build/codegen/targets/kotlin/probe-unsupported-unions.test.d.ts +1 -0
  92. package/build/codegen/targets/kotlin/probe-unsupported-unions.test.js +50 -0
  93. package/build/codegen/targets/kotlin/probe-unsupported-unions.test.js.map +1 -0
  94. package/build/codegen/targets/kotlin/run.d.ts +11 -0
  95. package/build/codegen/targets/kotlin/run.js +51 -0
  96. package/build/codegen/targets/kotlin/run.js.map +1 -0
  97. package/build/codegen/targets/swift/access-level.test.d.ts +1 -0
  98. package/build/codegen/targets/swift/access-level.test.js +98 -0
  99. package/build/codegen/targets/swift/access-level.test.js.map +1 -0
  100. package/build/codegen/targets/swift/ajsc-adapter.d.ts +27 -0
  101. package/build/codegen/targets/swift/ajsc-adapter.js +38 -0
  102. package/build/codegen/targets/swift/ajsc-adapter.js.map +1 -0
  103. package/build/codegen/targets/swift/ajsc-adapter.test.d.ts +1 -0
  104. package/build/codegen/targets/swift/ajsc-adapter.test.js +37 -0
  105. package/build/codegen/targets/swift/ajsc-adapter.test.js.map +1 -0
  106. package/build/codegen/targets/swift/e2e-compile.test.d.ts +1 -0
  107. package/build/codegen/targets/swift/e2e-compile.test.js +57 -0
  108. package/build/codegen/targets/swift/e2e-compile.test.js.map +1 -0
  109. package/build/codegen/targets/swift/emit-route-swift.d.ts +15 -0
  110. package/build/codegen/targets/swift/emit-route-swift.js +64 -0
  111. package/build/codegen/targets/swift/emit-route-swift.js.map +1 -0
  112. package/build/codegen/targets/swift/emit-route-swift.test.d.ts +1 -0
  113. package/build/codegen/targets/swift/emit-route-swift.test.js +258 -0
  114. package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -0
  115. package/build/codegen/targets/swift/emit-scope-swift.d.ts +13 -0
  116. package/build/codegen/targets/swift/emit-scope-swift.js +36 -0
  117. package/build/codegen/targets/swift/emit-scope-swift.js.map +1 -0
  118. package/build/codegen/targets/swift/emit-scope-swift.test.d.ts +1 -0
  119. package/build/codegen/targets/swift/emit-scope-swift.test.js +136 -0
  120. package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -0
  121. package/build/codegen/targets/swift/format-swift.d.ts +2 -0
  122. package/build/codegen/targets/swift/format-swift.js +10 -0
  123. package/build/codegen/targets/swift/format-swift.js.map +1 -0
  124. package/build/codegen/targets/swift/format-swift.test.d.ts +1 -0
  125. package/build/codegen/targets/swift/format-swift.test.js +14 -0
  126. package/build/codegen/targets/swift/format-swift.test.js.map +1 -0
  127. package/build/codegen/targets/swift/integration.test.d.ts +1 -0
  128. package/build/codegen/targets/swift/integration.test.js +53 -0
  129. package/build/codegen/targets/swift/integration.test.js.map +1 -0
  130. package/build/codegen/targets/swift/run.d.ts +11 -0
  131. package/build/codegen/targets/swift/run.js +47 -0
  132. package/build/codegen/targets/swift/run.js.map +1 -0
  133. package/build/codegen/targets/ts/run.d.ts +4 -0
  134. package/build/codegen/targets/ts/run.js +86 -0
  135. package/build/codegen/targets/ts/run.js.map +1 -0
  136. package/build/codegen/test-helpers/golden.d.ts +15 -0
  137. package/build/codegen/test-helpers/golden.js +30 -0
  138. package/build/codegen/test-helpers/golden.js.map +1 -0
  139. package/build/codegen/test-helpers/golden.test.d.ts +1 -0
  140. package/build/codegen/test-helpers/golden.test.js +76 -0
  141. package/build/codegen/test-helpers/golden.test.js.map +1 -0
  142. package/docs/codegen-kotlin.md +176 -0
  143. package/docs/codegen-swift.md +314 -0
  144. package/docs/superpowers/plans/2026-04-25-ajsc-v7-kotlin-polish.md +1993 -0
  145. package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +1 -1
  146. package/docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md +314 -0
  147. package/docs/superpowers/specs/2026-04-25-swift-codegen-design.md +264 -0
  148. package/package.json +2 -2
  149. package/src/codegen/__fixtures__/users-envelope.json +144 -0
  150. package/src/codegen/bin/cli.test.ts +200 -1
  151. package/src/codegen/bin/cli.ts +187 -0
  152. package/src/codegen/index.ts +50 -0
  153. package/src/codegen/pipeline.test.ts +175 -0
  154. package/src/codegen/pipeline.ts +58 -101
  155. package/src/codegen/targets/_shared/error-schemas.test.ts +42 -0
  156. package/src/codegen/targets/_shared/error-schemas.ts +17 -0
  157. package/src/codegen/targets/_shared/indent.test.ts +25 -0
  158. package/src/codegen/targets/_shared/indent.ts +12 -0
  159. package/src/codegen/targets/_shared/pascal-case.test.ts +30 -0
  160. package/src/codegen/targets/_shared/pascal-case.ts +12 -0
  161. package/src/codegen/targets/_shared/path-utils.test.ts +51 -0
  162. package/src/codegen/targets/_shared/path-utils.ts +21 -0
  163. package/src/codegen/targets/_shared/pick-defined.test.ts +48 -0
  164. package/src/codegen/targets/_shared/pick-defined.ts +23 -0
  165. package/src/codegen/targets/_shared/route-slots.test.ts +55 -0
  166. package/src/codegen/targets/_shared/route-slots.ts +32 -0
  167. package/src/codegen/targets/_shared/target-run.ts +28 -0
  168. package/src/codegen/targets/_shared/write-files.test.ts +110 -0
  169. package/src/codegen/targets/_shared/write-files.ts +53 -0
  170. package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +121 -0
  171. package/src/codegen/targets/kotlin/__snapshots__/probe-unsupported-unions.test.ts.snap +27 -0
  172. package/src/codegen/targets/kotlin/ajsc-adapter.test.ts +47 -0
  173. package/src/codegen/targets/kotlin/ajsc-adapter.ts +66 -0
  174. package/src/codegen/targets/kotlin/e2e-compile.test.ts +86 -0
  175. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +239 -0
  176. package/src/codegen/targets/kotlin/emit-route-kotlin.ts +89 -0
  177. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +112 -0
  178. package/src/codegen/targets/kotlin/emit-scope-kotlin.ts +60 -0
  179. package/src/codegen/targets/kotlin/format-kotlin.test.ts +26 -0
  180. package/src/codegen/targets/kotlin/format-kotlin.ts +13 -0
  181. package/src/codegen/targets/kotlin/integration.test.ts +77 -0
  182. package/src/codegen/targets/kotlin/probe-unsupported-unions.test.ts +64 -0
  183. package/src/codegen/targets/kotlin/run.ts +78 -0
  184. package/src/codegen/targets/swift/__fixtures__/users-golden.swift +123 -0
  185. package/src/codegen/targets/swift/access-level.test.ts +108 -0
  186. package/src/codegen/targets/swift/ajsc-adapter.test.ts +47 -0
  187. package/src/codegen/targets/swift/ajsc-adapter.ts +67 -0
  188. package/src/codegen/targets/swift/e2e-compile.test.ts +66 -0
  189. package/src/codegen/targets/swift/emit-route-swift.test.ts +300 -0
  190. package/src/codegen/targets/swift/emit-route-swift.ts +90 -0
  191. package/src/codegen/targets/swift/emit-scope-swift.test.ts +164 -0
  192. package/src/codegen/targets/swift/emit-scope-swift.ts +59 -0
  193. package/src/codegen/targets/swift/format-swift.test.ts +23 -0
  194. package/src/codegen/targets/swift/format-swift.ts +9 -0
  195. package/src/codegen/targets/swift/integration.test.ts +80 -0
  196. package/src/codegen/targets/swift/run.ts +74 -0
  197. package/src/codegen/targets/ts/run.ts +117 -0
  198. package/src/codegen/test-helpers/golden.test.ts +80 -0
  199. package/src/codegen/test-helpers/golden.ts +34 -0
@@ -92,7 +92,7 @@ function setupClaude({ dryRun, check } = {}) {
92
92
  // Mirror install-claude.mjs: copy every file under each source skill dir, plus the agent.
93
93
  const claudeFiles = [];
94
94
  const skillsSrc = join(AGENT_CONFIG_DIR, 'claude-code', 'skills');
95
- for (const skill of ['ts-procedures', 'ts-procedures-review', 'ts-procedures-scaffold']) {
95
+ for (const skill of ['ts-procedures', 'ts-procedures-review', 'ts-procedures-scaffold', 'ts-procedures-kotlin', 'ts-procedures-swift']) {
96
96
  const walk = (dir, prefix) => {
97
97
  for (const entry of readdirSync(dir)) {
98
98
  const full = join(dir, entry);
@@ -128,7 +128,7 @@ function setupClaude({ dryRun, check } = {}) {
128
128
  console.log(` ${f}`);
129
129
  }
130
130
  console.log('');
131
- console.log(' Skills: ts-procedures, ts-procedures-scaffold, ts-procedures-review');
131
+ console.log(' Skills: ts-procedures, ts-procedures-scaffold, ts-procedures-review, ts-procedures-kotlin, ts-procedures-swift');
132
132
  console.log(' Agent: ts-procedures-architect (architecture planning)\n');
133
133
  return false;
134
134
  }
@@ -15,6 +15,8 @@ Load the right reference for your task:
15
15
  - **Writing new procedures or HTTP routes?** Read [patterns.md](patterns.md) — prescribed code examples for every procedure type and HTTP integration
16
16
  - **Reviewing or debugging existing code?** Read [anti-patterns.md](anti-patterns.md) — 20 common mistakes with before/after fixes and severity ratings
17
17
  - **Need exact API signatures or type definitions?** Read [api-reference.md](api-reference.md) — complete API documentation with type signatures for every export
18
+ - **Generating a Kotlin client for Android/JVM consumers?** Use the separate `ts-procedures-kotlin` skill — it covers `--target kotlin` end-to-end and won't load unless the user mentions Kotlin/Android.
19
+ - **Generating a Swift client for iOS/macOS/Apple-platform consumers?** Use the separate `ts-procedures-swift` skill — it covers `--target swift` end-to-end and won't load unless the user mentions Swift/iOS/Apple platforms.
18
20
 
19
21
  ## Core Flow
20
22
 
@@ -1013,6 +1013,8 @@ const adapter = createFetchAdapter({ fetch: customFetch })
1013
1013
 
1014
1014
  ## generateClient(options)
1015
1015
 
1016
+ > For **Kotlin** client codegen (Android/JVM, types-only output), see the dedicated `ts-procedures-kotlin` skill. For **Swift** client codegen (iOS/macOS/Apple platforms, types-only output), see the dedicated `ts-procedures-swift` skill. The reference below covers TypeScript codegen only.
1017
+
1016
1018
  Build-time CLI and programmatic API for generating typed client files from a `DocEnvelope`.
1017
1019
 
1018
1020
  ```typescript
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: ts-procedures-kotlin
3
+ description: "Kotlin client codegen for ts-procedures — generate types-only Kotlin source from a ts-procedures DocEnvelope for Android/JVM consumers. Use when the user mentions Kotlin, Android, mobile clients, kotlinx-serialization, or asks how to generate non-TypeScript types from a ts-procedures server."
4
+ user-invocable: false
5
+ ---
6
+
7
+ # ts-procedures — Kotlin Client Codegen
8
+
9
+ You are assisting a developer who needs to generate Kotlin types from a `ts-procedures` server's `DocEnvelope`. The Kotlin target is **types-only** — no runtime, no adapter, no error registry. Mobile/Android consumers own the HTTP layer.
10
+
11
+ ## When this skill applies
12
+
13
+ - The user mentions Kotlin, Android, kotlinx-serialization, mobile client, or `--target kotlin`.
14
+ - The user wants to share API types between a `ts-procedures` server and a Kotlin/JVM consumer.
15
+ - The user is debugging Kotlin codegen output, Gradle setup, or contextual serializer registration.
16
+
17
+ If the user is generating a **TypeScript** client, redirect them to the main `ts-procedures` skill. For **Swift / iOS / macOS / Apple-platform** consumers, redirect to `ts-procedures-swift`.
18
+
19
+ ## Quickstart
20
+
21
+ ```bash
22
+ npx ts-procedures-codegen \
23
+ --target kotlin \
24
+ --kotlin-package com.example.api \
25
+ --url https://api.example.com/_ts-procedures.json \
26
+ --out ./src/main/kotlin/com/example/api
27
+ ```
28
+
29
+ One `.kt` file per scope. Types accessed as `Users.GetUser.Response`, `Users.GetUser.Body.Address` (nested classes via `inlineTypes: true`), `Users.GetUser.Errors.NotFound`.
30
+
31
+ ## CLI flags (Kotlin-specific)
32
+
33
+ | Flag | Default | Purpose |
34
+ |---|---|---|
35
+ | `--target kotlin` | `ts` | Switch to the Kotlin codegen path |
36
+ | `--kotlin-package <com.example.api>` | required | Sets the `package` declaration on every emitted `.kt` file |
37
+ | `--kotlin-serializer <kotlinx\|none>` | `kotlinx` | `kotlinx` emits `@Serializable`; `none` emits plain data classes for Moshi/Gson/hand-written serialization |
38
+ | `--unsupported-unions <throw\|fallback>` | `throw` | **Currently a no-op for Kotlin** — ajsc v7.2 silently emits an empty `data class` for untagged `oneOf` regardless. CLI warns when set |
39
+
40
+ `--array-item-naming`, `--depluralize`, `--uncountable-words` also apply to the Kotlin target.
41
+
42
+ ## Output shape (what consumers see)
43
+
44
+ ```kotlin
45
+ package com.example.api
46
+
47
+ import kotlinx.serialization.Serializable
48
+ import kotlinx.serialization.SerialName
49
+ import kotlinx.serialization.Contextual
50
+ import kotlinx.serialization.json.JsonClassDiscriminator
51
+
52
+ object Users {
53
+ object GetUser {
54
+ const val method = "GET"
55
+ const val pathTemplate = "/users/{id}"
56
+ fun path(p: PathParams): String = "/users/${p.id}"
57
+
58
+ @Serializable data class PathParams(val id: String)
59
+
60
+ @Serializable
61
+ data class Response(
62
+ val id: String,
63
+ @SerialName("created-at") @Contextual val createdAt: java.time.Instant,
64
+ val address: Address,
65
+ ) {
66
+ @Serializable data class Address(val street: String, val city: String)
67
+ }
68
+
69
+ object Errors {
70
+ @Serializable
71
+ data class NotFound(val name: String = "NotFound", val message: String)
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ For routes without path params, `path` is a `const val`, not a function.
78
+
79
+ ## Consumer-side setup the dev MUST do
80
+
81
+ The generated code requires these on the Android/JVM side. **Don't let the user assume the codegen handles them.**
82
+
83
+ 1. **Gradle:** `kotlin("plugin.serialization")` plugin + `org.jetbrains.kotlinx:kotlinx-serialization-json` dependency. (`kotlinx-serialization-core` is a transitive dep; no need to declare it explicitly.)
84
+
85
+ 2. **Contextual serializers:** `format: date-time`/`uuid`/`uri`/`date`/`time` map to JVM stdlib types annotated with `@Contextual`. The consumer's `Json` configuration MUST register `contextual(java.time.Instant::class, ...)` etc., otherwise decoding fails. We don't ship the serializers — choice between ISO-8601 and epoch ms is application-specific.
86
+
87
+ 3. **Discriminated unions:** `@JsonClassDiscriminator` is read automatically by `kotlinx-serialization-json` — no extra config needed.
88
+
89
+ 4. **No runtime dispatch:** error types are emitted as nested data classes (`Users.GetUser.Errors.NotFound`), but there's no `instanceof`-style registry, no `dispatchTypedError`. Consumers catch HTTP failures themselves and inspect `body.name` (a regular `String` field, not a type-system discriminator) to decide which error data class to deserialize against. This is by design; don't suggest implementing it.
90
+
91
+ The full setup guide lives at `docs/codegen-kotlin.md` in the `ts-procedures` repo.
92
+
93
+ ## Documented limitations to flag during reviews
94
+
95
+ - **Untagged `oneOf` produces an empty `data class`.** Won't round-trip. Add a server-side discriminator, hand-write a `KSerializer`, or pre-process the envelope.
96
+ - **Tuples > 3 elements throw** at codegen time. Refactor to a struct schema upstream.
97
+ - **`additionalProperties: { type: T }` is silently dropped** with a KDoc note. Add a sibling `Map<String, T>` field by hand if your contract uses extra keys.
98
+ - **Schema-level `examples` are not modeled.** They're documentation-only on the server side; consumers don't see them.
99
+
100
+ ## Anti-patterns
101
+
102
+ - Suggesting the Kotlin target ships an HTTP adapter or error registry.
103
+ - Recommending `--kotlin-serializer none` without noting the consumer is responsible for adapter setup.
104
+ - Treating `--unsupported-unions fallback` as functional for Kotlin — it's a no-op (the CLI itself warns when set).
105
+ - Saying KMP (Kotlin Multiplatform) is supported — JVM only for now.
106
+ - Mixing `--target kotlin` flags into a TypeScript-target invocation; some flags are silently ignored, others (like `--kotlin-package`) are required only for kotlin.
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: ts-procedures-swift
3
+ description: "Swift client codegen for ts-procedures — generate types-only Swift source from a ts-procedures DocEnvelope for iOS/macOS/Apple-platform consumers. Use when the user mentions Swift, iOS, macOS, Apple platforms, Codable, or asks how to generate non-TypeScript types from a ts-procedures server."
4
+ user-invocable: false
5
+ ---
6
+
7
+ # ts-procedures — Swift Client Codegen
8
+
9
+ You are assisting a developer who needs to generate Swift types from a `ts-procedures` server's `DocEnvelope`. The Swift target is **types-only** — no runtime, no adapter, no error registry. iOS / macOS / Apple-platform consumers own the HTTP layer.
10
+
11
+ ## When this skill applies
12
+
13
+ - The user mentions Swift, iOS, macOS, watchOS, tvOS, visionOS, Apple platforms, `Codable`, `URLSession`, or `--target swift`.
14
+ - The user wants to share API types between a `ts-procedures` server and a Swift consumer.
15
+ - The user is debugging Swift codegen output, SPM/Xcode integration, `JSONDecoder` configuration, or `Codable` conformance issues.
16
+
17
+ If the user is generating a **TypeScript** client, redirect them to the main `ts-procedures` skill. For **Kotlin/Android** consumers, redirect to `ts-procedures-kotlin`.
18
+
19
+ ## Quickstart
20
+
21
+ ```bash
22
+ npx ts-procedures-codegen \
23
+ --target swift \
24
+ --url https://api.example.com/_ts-procedures.json \
25
+ --out ./Sources/MyApp/Generated
26
+ ```
27
+
28
+ One `.swift` file per scope. Types accessed as `Users.GetUser.Response`, `Users.GetUser.PathParams`, `Users.GetUser.Response.Address` (nested structs via `inlineTypes: true`), `Users.GetUser.Errors.NotFound`.
29
+
30
+ **Note:** unlike the Kotlin target, no `--swift-package` flag exists or is required. Swift modules are defined by Xcode/SPM targets.
31
+
32
+ ## CLI flags (Swift-specific)
33
+
34
+ | Flag | Default | Purpose |
35
+ |---|---|---|
36
+ | `--target swift` | `ts` | Switch to the Swift codegen path |
37
+ | `--swift-serializer <codable\|none>` | `codable` | `codable` emits `: Codable` + `CodingKeys`; `none` emits plain structs (consumer handles serialization) |
38
+ | `--swift-access-level <public\|internal>` | `public` | Threads through to ajsc's `accessLevel`; use `internal` when generated types shouldn't appear in module ABI |
39
+ | `--unsupported-unions <throw\|fallback>` | `throw` | **Functional for Swift** (unlike Kotlin where it's a no-op) — `fallback` emits a self-contained `AnyCodable` helper for untagged `anyOf`/`oneOf` |
40
+
41
+ `--array-item-naming`, `--depluralize`, `--uncountable-words` also apply to the Swift target.
42
+
43
+ ## Output shape (what consumers see)
44
+
45
+ ```swift
46
+ import Foundation
47
+
48
+ public enum Users {
49
+ public enum GetUser {
50
+ public static let method = "GET"
51
+ public static let pathTemplate = "/users/{id}"
52
+ public static func path(_ p: PathParams) -> String { return "/users/\(p.id)" }
53
+
54
+ public struct PathParams: Codable {
55
+ public let id: String
56
+ }
57
+
58
+ public struct Response: Codable {
59
+ public let id: String
60
+ public let createdAt: Date
61
+ public let address: Address
62
+
63
+ enum CodingKeys: String, CodingKey {
64
+ case id
65
+ case createdAt = "created-at"
66
+ case address
67
+ }
68
+
69
+ public struct Address: Codable {
70
+ public let street: String
71
+ public let city: String
72
+ }
73
+ }
74
+
75
+ public enum Errors {
76
+ public struct NotFound: Codable {
77
+ public let name: String
78
+ public let message: String
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ For routes without path params, `path` is a `static let`, not a function. Namespaces are caseless `enum`s (the standard Swift idiom — uninstantiable, zero runtime cost).
86
+
87
+ ## Consumer-side setup the dev MUST do
88
+
89
+ The generated code requires these on the Apple-platform side. **Don't let the user assume the codegen handles them.**
90
+
91
+ 1. **Drop generated files into your SPM target or Xcode source group.** Any target containing the generated dir picks them up — no special build config. SPM globs sources recursively; for Xcode, use **File → Add Files to "<TargetName>"…** and ensure target membership is checked.
92
+
93
+ 2. **Configure `JSONDecoder.dateDecodingStrategy = .iso8601`.** **Required** if the schema uses any `format: date-time` field (which become `Foundation.Date`). Without this, decoding fails with `DecodingError.typeMismatch`. Symmetric: set `JSONEncoder.dateEncodingStrategy = .iso8601` for request bodies.
94
+
95
+ 3. **HTTP transport is the consumer's choice.** `URLSession` (with `async/await`) is the default recommendation — it's built-in and works on all Apple platforms with no dependencies. Alamofire / other libraries are fine but never required.
96
+
97
+ 4. **No runtime dispatch.** Error types are emitted as nested structs (`Users.GetUser.Errors.NotFound`), but there's no registry, no `instanceof`-style lookup, no `dispatchTypedError`. Consumers catch HTTP failures themselves and dispatch on status code or `body.name` (a regular `String` field, not a type-system discriminator) to decide which error struct to decode against. To make error structs throwable, declare a one-line `extension X.Errors.NotFound: Error {}` in user code (keep it out of the generated file so re-runs don't clobber it). This is by design; don't suggest implementing a registry.
98
+
99
+ The full setup guide lives at `docs/codegen-swift.md` in the `ts-procedures` repo.
100
+
101
+ ## Documented limitations to flag during reviews
102
+
103
+ - **`format: date` and `format: time` map to `String`.** Foundation has no native date-only or time-only type. Parse with `DateFormatter` if a typed value is needed.
104
+ - **`type: integer` maps to `Int64`** (not `Int`). 32-bit Apple platforms exist; `Int64` guarantees range parity.
105
+ - **`type: number` maps to `Double`.** For monetary values, convert to `Decimal` at the parse boundary.
106
+ - **Heterogeneous tuples throw under `Codable`.** Swift tuples aren't `Codable`. Schemas with positional-tuple `items: [...]` arrays throw at codegen time. Refactor to a struct schema upstream.
107
+ - **`additionalProperties: { type: T }` is silently dropped** with a doc-comment. Add a sibling `[String: T]` field by hand if your contract uses extra keys.
108
+ - **`not` and `patternProperties` throw at codegen time.** Simplify the schema upstream.
109
+ - **Untagged `oneOf` throws by default.** Add a discriminator, or pass `--unsupported-unions fallback` to emit `AnyCodable`-typed values (loses static typing).
110
+
111
+ ## Anti-patterns
112
+
113
+ - **Suggesting the Swift target ships a networking layer or HTTP adapter.** It does not — consumers own `URLSession`/etc. entirely.
114
+ - **Recommending Alamofire as required.** `URLSession` + `async/await` is fine and ships with the OS. Alamofire is a personal-preference choice, not a dependency of the generated code.
115
+ - **Conflating `--swift-serializer none` with "no setup needed".** `none` removes `Codable` conformance and `CodingKeys` — the consumer then needs their own serialization plan (hand-rolled, SwiftyJSON, etc.). It's strictly *more* setup, not less.
116
+ - **Treating `--unsupported-unions fallback` as a no-op for Swift.** Unlike Kotlin (where it's silently dropped), the Swift target actually emits an `AnyCodable` helper that round-trips correctly. Use it when needed.
117
+ - **Suggesting backwards compatibility with Swift 4 / pre-async-await.** Assume Swift 5.5+ (the async/await era). Sample dispatch code in docs uses `async throws`.
118
+ - **Suggesting the codegen emits `: Error` conformance on error structs.** It doesn't (`Codable` only) — consumers add a one-line extension. Don't ask for this to be added to the codegen; it would force users into a particular error model.
119
+ - **Mixing `--target swift` flags into a TypeScript-target invocation.** The flags are silently ignored; no harm, but the user is probably running the wrong target.
@@ -386,6 +386,9 @@ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated
386
386
  # --clean-out-dir # recursively wipe --out before writing (prunes stale scope files)
387
387
  ```
388
388
 
389
+ For Kotlin codegen (Android/JVM consumers), see `docs/codegen-kotlin.md` in the ts-procedures repo. The Kotlin target is types-only; consumer apps own HTTP/error handling.
390
+ For Swift codegen (iOS/macOS/Apple-platform consumers), see `docs/codegen-swift.md` in the ts-procedures repo. The Swift target is types-only; consumer apps own HTTP/error handling.
391
+
389
392
  Generates one `.ts` file per scope plus a root `index.ts` that imports each scope as a namespace and exports a `create${ServiceName}Bindings(client)` factory AND a `create${ServiceName}Client(config)` convenience factory that pre-wires the error registry (defaults to `createApiBindings` / `createApiClient`; pass `--service-name <Name>` to rename). When namespace mode is on (the default), `index.ts` also wraps every scope namespace in an outer `export namespace ${ServiceName} { ... }` block so types are reachable as `Api.Users.GetUser.Params`, `Api.Errors.UseCaseError`, etc. The errors file (`_errors.ts`) emits runtime error classes extending a shared `${ServiceName}ProcedureError` base, each with `static fromResponse(body, meta)`, plus `${ServiceName}ErrorRegistry` (runtime dispatch map) and `${ServiceName}ProcedureErrorUnion` (type union). Defaults: `ApiErrors`, `ApiProcedureError`, `ApiErrorRegistry`, `ApiProcedureErrorUnion`.
390
393
  By default, types are wrapped in nested TS namespaces (`Scope.Route.Params`), JSDoc comments are emitted, and output is self-contained (no runtime dependency on `ts-procedures`). Use `--no-namespace-types` to revert to flat type names (`RouteParams`); this also disables the outer service namespace in `index.ts` and skips importing `_errors` from there.
391
394
  Note: ajsc formatting options (`--enum-style enum`, `--depluralize`, etc.) only take effect in namespace mode (the default). They are ignored with `--no-namespace-types`.
@@ -386,6 +386,9 @@ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated
386
386
  # --clean-out-dir # recursively wipe --out before writing (prunes stale scope files)
387
387
  ```
388
388
 
389
+ For Kotlin codegen (Android/JVM consumers), see `docs/codegen-kotlin.md` in the ts-procedures repo. The Kotlin target is types-only; consumer apps own HTTP/error handling.
390
+ For Swift codegen (iOS/macOS/Apple-platform consumers), see `docs/codegen-swift.md` in the ts-procedures repo. The Swift target is types-only; consumer apps own HTTP/error handling.
391
+
389
392
  Generates one `.ts` file per scope plus a root `index.ts` that imports each scope as a namespace and exports a `create${ServiceName}Bindings(client)` factory AND a `create${ServiceName}Client(config)` convenience factory that pre-wires the error registry (defaults to `createApiBindings` / `createApiClient`; pass `--service-name <Name>` to rename). When namespace mode is on (the default), `index.ts` also wraps every scope namespace in an outer `export namespace ${ServiceName} { ... }` block so types are reachable as `Api.Users.GetUser.Params`, `Api.Errors.UseCaseError`, etc. The errors file (`_errors.ts`) emits runtime error classes extending a shared `${ServiceName}ProcedureError` base, each with `static fromResponse(body, meta)`, plus `${ServiceName}ErrorRegistry` (runtime dispatch map) and `${ServiceName}ProcedureErrorUnion` (type union). Defaults: `ApiErrors`, `ApiProcedureError`, `ApiErrorRegistry`, `ApiProcedureErrorUnion`.
390
393
  By default, types are wrapped in nested TS namespaces (`Scope.Route.Params`), JSDoc comments are emitted, and output is self-contained (no runtime dependency on `ts-procedures`). Use `--no-namespace-types` to revert to flat type names (`RouteParams`); this also disables the outer service namespace in `index.ts` and skips importing `_errors` from there.
391
394
  Note: ajsc formatting options (`--enum-style enum`, `--depluralize`, etc.) only take effect in namespace mode (the default). They are ignored with `--no-namespace-types`.
@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
7
  const SOURCE_DIR = join(__dirname, '..', 'claude-code');
8
8
 
9
- const SKILL_NAMES = ['ts-procedures', 'ts-procedures-review', 'ts-procedures-scaffold'];
9
+ const SKILL_NAMES = ['ts-procedures', 'ts-procedures-review', 'ts-procedures-scaffold', 'ts-procedures-kotlin', 'ts-procedures-swift'];
10
10
  const AGENT_FILES = ['ts-procedures-architect.md'];
11
11
 
12
12
  function listFilesRecursive(dir) {
@@ -13,6 +13,16 @@ export interface CodegenConfig {
13
13
  selfContained?: boolean;
14
14
  serviceName?: string;
15
15
  cleanOutDir?: boolean;
16
+ target?: 'ts' | 'kotlin' | 'swift';
17
+ kotlin?: {
18
+ package: string;
19
+ serializer?: 'kotlinx' | 'none';
20
+ };
21
+ swift?: {
22
+ serializer?: 'codable' | 'none';
23
+ accessLevel?: 'public' | 'internal';
24
+ };
25
+ unsupportedUnions?: 'throw' | 'fallback';
16
26
  }
17
27
  export interface ParsedArgs {
18
28
  url?: string;
@@ -27,6 +37,16 @@ export interface ParsedArgs {
27
37
  selfContained: boolean;
28
38
  serviceName?: string;
29
39
  cleanOutDir: boolean;
40
+ target?: 'ts' | 'kotlin' | 'swift';
41
+ kotlin?: {
42
+ package: string;
43
+ serializer?: 'kotlinx' | 'none';
44
+ };
45
+ swift?: {
46
+ serializer?: 'codable' | 'none';
47
+ accessLevel?: 'public' | 'internal';
48
+ };
49
+ unsupportedUnions?: 'throw' | 'fallback';
30
50
  }
31
51
  /**
32
52
  * Loads a JSON config file. Returns undefined if the file doesn't exist.
@@ -45,3 +65,22 @@ export declare function parseArgs(argv: string[], config?: CodegenConfig): Parse
45
65
  * Extracts the --config value from argv without full parsing.
46
66
  */
47
67
  export declare function extractConfigPath(argv: string[]): string | undefined;
68
+ export declare function printPostRunHints(parsed: {
69
+ target?: 'ts' | 'kotlin' | 'swift';
70
+ }): void;
71
+ /**
72
+ * Warns about flags that are currently no-ops for the Kotlin target.
73
+ *
74
+ * Scope is intentionally Kotlin-only — the param type omits `'swift'` so a
75
+ * reader can tell at a glance this function will never act on swift. Other
76
+ * targets get their own warner if/when they grow no-op flags. The call site
77
+ * narrows `parsed.target` before invoking.
78
+ *
79
+ * Currently: `--unsupported-unions` is a no-op because ajsc v7.2's Kotlin
80
+ * emitter silently emits an empty data class for untagged oneOf regardless
81
+ * of the flag (see docs/codegen-kotlin.md#untagged-unions).
82
+ */
83
+ export declare function warnIfKotlinNoOpFlags(parsed: {
84
+ target?: 'ts' | 'kotlin';
85
+ unsupportedUnions?: 'throw' | 'fallback';
86
+ }): void;
@@ -49,6 +49,12 @@ export function parseArgs(argv, config) {
49
49
  let selfContained = config?.selfContained ?? true;
50
50
  let serviceName = config?.serviceName;
51
51
  let cleanOutDir = config?.cleanOutDir ?? false;
52
+ let target = config?.target;
53
+ let kotlinPackage = config?.kotlin?.package;
54
+ let kotlinSerializer = config?.kotlin?.serializer;
55
+ let swiftSerializer = config?.swift?.serializer;
56
+ let swiftAccessLevel = config?.swift?.accessLevel;
57
+ let unsupportedUnions = config?.unsupportedUnions;
52
58
  let configPath;
53
59
  for (let i = 0; i < argv.length; i++) {
54
60
  const arg = argv[i];
@@ -116,18 +122,80 @@ export function parseArgs(argv, config) {
116
122
  else if (arg === '--no-clean-out-dir') {
117
123
  cleanOutDir = false;
118
124
  }
125
+ else if (arg === '--target') {
126
+ const val = argv[++i];
127
+ if (val === 'ts' || val === 'kotlin' || val === 'swift') {
128
+ target = val;
129
+ }
130
+ else {
131
+ throw new Error(`Invalid --target value: ${val ?? '(missing)'} (expected 'ts', 'kotlin', or 'swift')`);
132
+ }
133
+ }
134
+ else if (arg === '--kotlin-package') {
135
+ kotlinPackage = argv[++i];
136
+ }
137
+ else if (arg === '--kotlin-serializer') {
138
+ const val = argv[++i];
139
+ if (val === 'kotlinx' || val === 'none') {
140
+ kotlinSerializer = val;
141
+ }
142
+ else {
143
+ throw new Error(`Invalid --kotlin-serializer value: ${val ?? '(missing)'} (expected 'kotlinx' or 'none')`);
144
+ }
145
+ }
146
+ else if (arg === '--swift-serializer') {
147
+ const val = argv[++i];
148
+ if (val === 'codable' || val === 'none') {
149
+ swiftSerializer = val;
150
+ }
151
+ else {
152
+ throw new Error(`Invalid --swift-serializer value: ${val ?? '(missing)'} (expected 'codable' or 'none')`);
153
+ }
154
+ }
155
+ else if (arg === '--swift-access-level') {
156
+ const val = argv[++i];
157
+ if (val === 'public' || val === 'internal') {
158
+ swiftAccessLevel = val;
159
+ }
160
+ else {
161
+ throw new Error(`Invalid --swift-access-level value: ${val ?? '(missing)'} (expected 'public' or 'internal')`);
162
+ }
163
+ }
164
+ else if (arg === '--unsupported-unions') {
165
+ const val = argv[++i];
166
+ if (val === 'throw' || val === 'fallback') {
167
+ unsupportedUnions = val;
168
+ }
169
+ else {
170
+ throw new Error(`Invalid --unsupported-unions value: ${val ?? '(missing)'} (expected 'throw' or 'fallback')`);
171
+ }
172
+ }
119
173
  else if (arg === '--config') {
120
174
  configPath = argv[++i];
121
175
  }
122
176
  }
123
177
  // configPath is consumed by the caller (main) before parseArgs is called with the loaded config.
124
178
  // When called from main, config is already loaded. When called directly (tests), configPath is ignored.
179
+ // ---------------------------------------------------------------------------
180
+ // Validation — fails fast on user-controllable errors before envelope resolve.
181
+ // Runtime checks (emitter availability, etc.) happen later in pipeline.ts;
182
+ // those guards are aimed at non-CLI callers (direct API consumers, tests).
183
+ // The CLI resolves emitters before invoking `runPipeline`, so users only ever
184
+ // see flag-shape errors from this block, not pipeline-internal throws.
185
+ // ---------------------------------------------------------------------------
125
186
  if (outDir === undefined) {
126
187
  throw new Error('Missing required argument: --out <dir>');
127
188
  }
128
189
  if (url === undefined && file === undefined) {
129
190
  throw new Error('Missing required input source: provide --url <url> or --file <path>');
130
191
  }
192
+ // Kotlin target requires a package; surface this before any I/O happens.
193
+ if (target === 'kotlin' && (kotlinPackage === undefined || kotlinPackage === '')) {
194
+ throw new Error('Missing required argument: --kotlin-package <pkg> (required when --target kotlin)');
195
+ }
196
+ // Swift target currently has no required flags. If that changes, add the
197
+ // guard here so the failure mode stays consistent (flag-shape errors fire
198
+ // from parseArgs; runtime/emitter wiring errors fire from pipeline.ts).
131
199
  return {
132
200
  url,
133
201
  file,
@@ -141,6 +209,24 @@ export function parseArgs(argv, config) {
141
209
  selfContained,
142
210
  ...(serviceName !== undefined ? { serviceName } : {}),
143
211
  cleanOutDir,
212
+ ...(target !== undefined ? { target } : {}),
213
+ ...(kotlinPackage !== undefined
214
+ ? {
215
+ kotlin: {
216
+ package: kotlinPackage,
217
+ ...(kotlinSerializer !== undefined ? { serializer: kotlinSerializer } : {}),
218
+ },
219
+ }
220
+ : {}),
221
+ ...(swiftSerializer !== undefined || swiftAccessLevel !== undefined
222
+ ? {
223
+ swift: {
224
+ ...(swiftSerializer !== undefined ? { serializer: swiftSerializer } : {}),
225
+ ...(swiftAccessLevel !== undefined ? { accessLevel: swiftAccessLevel } : {}),
226
+ },
227
+ }
228
+ : {}),
229
+ ...(unsupportedUnions !== undefined ? { unsupportedUnions } : {}),
144
230
  };
145
231
  }
146
232
  /**
@@ -172,6 +258,21 @@ async function runWithWatch(parsed) {
172
258
  serviceName: parsed.serviceName,
173
259
  cleanOutDir: parsed.cleanOutDir,
174
260
  };
261
+ // Resolve the kotlin emitter once at watch start; it's stateless and reused per tick.
262
+ const kotlinWiring = parsed.target === 'kotlin'
263
+ ? {
264
+ target: 'kotlin',
265
+ kotlinPackage: parsed.kotlin.package,
266
+ kotlinEmitter: await (await import('../targets/kotlin/ajsc-adapter.js')).resolveProductionKotlinEmitter(),
267
+ }
268
+ : {};
269
+ // Resolve the swift emitter once at watch start; it's stateless and reused per tick.
270
+ const swiftWiring = parsed.target === 'swift'
271
+ ? {
272
+ target: 'swift',
273
+ swiftEmitter: await (await import('../targets/swift/ajsc-adapter.js')).resolveProductionSwiftEmitter(),
274
+ }
275
+ : {};
175
276
  let lastHash;
176
277
  const run = async () => {
177
278
  try {
@@ -194,6 +295,12 @@ async function runWithWatch(parsed) {
194
295
  selfContained: parsed.selfContained,
195
296
  serviceName: parsed.serviceName,
196
297
  cleanOutDir: parsed.cleanOutDir,
298
+ ...(parsed.kotlin?.serializer !== undefined ? { kotlinSerializer: parsed.kotlin.serializer } : {}),
299
+ ...(parsed.unsupportedUnions !== undefined ? { unsupportedUnions: parsed.unsupportedUnions } : {}),
300
+ ...(parsed.swift?.serializer !== undefined ? { swiftSerializer: parsed.swift.serializer } : {}),
301
+ ...(parsed.swift?.accessLevel !== undefined ? { swiftAccessLevel: parsed.swift.accessLevel } : {}),
302
+ ...kotlinWiring,
303
+ ...swiftWiring,
197
304
  });
198
305
  console.log(`[ts-procedures-codegen] Generated client files → ${parsed.outDir}`);
199
306
  }
@@ -209,6 +316,34 @@ async function runWithWatch(parsed) {
209
316
  // ---------------------------------------------------------------------------
210
317
  // Main
211
318
  // ---------------------------------------------------------------------------
319
+ const KOTLIN_SETUP_GUIDE_URL = 'https://bitbucket.org/thermsio/ts-procedures/src/master/docs/codegen-kotlin.md';
320
+ const SWIFT_SETUP_GUIDE_URL = 'https://bitbucket.org/thermsio/ts-procedures/src/master/docs/codegen-swift.md';
321
+ export function printPostRunHints(parsed) {
322
+ if (parsed.target === 'kotlin') {
323
+ console.log(`[ts-procedures-codegen] Kotlin setup guide: ${KOTLIN_SETUP_GUIDE_URL}`);
324
+ }
325
+ if (parsed.target === 'swift') {
326
+ console.log(`[ts-procedures-codegen] Swift setup guide: ${SWIFT_SETUP_GUIDE_URL}`);
327
+ }
328
+ }
329
+ /**
330
+ * Warns about flags that are currently no-ops for the Kotlin target.
331
+ *
332
+ * Scope is intentionally Kotlin-only — the param type omits `'swift'` so a
333
+ * reader can tell at a glance this function will never act on swift. Other
334
+ * targets get their own warner if/when they grow no-op flags. The call site
335
+ * narrows `parsed.target` before invoking.
336
+ *
337
+ * Currently: `--unsupported-unions` is a no-op because ajsc v7.2's Kotlin
338
+ * emitter silently emits an empty data class for untagged oneOf regardless
339
+ * of the flag (see docs/codegen-kotlin.md#untagged-unions).
340
+ */
341
+ export function warnIfKotlinNoOpFlags(parsed) {
342
+ if (parsed.target === 'kotlin' && parsed.unsupportedUnions !== undefined) {
343
+ console.warn('[ts-procedures-codegen] Note: --unsupported-unions is currently a no-op for --target kotlin ' +
344
+ '(ajsc v7.2 emits an empty data class regardless). See docs/codegen-kotlin.md#untagged-unions.');
345
+ }
346
+ }
212
347
  async function main() {
213
348
  const argv = process.argv.slice(2);
214
349
  const configPath = extractConfigPath(argv);
@@ -217,12 +352,34 @@ async function main() {
217
352
  console.log(`[ts-procedures-codegen] Loaded config from ${configPath ?? DEFAULT_CONFIG_NAME}`);
218
353
  }
219
354
  const parsed = parseArgs(argv, config);
355
+ // The warner is intentionally Kotlin-only; pass the relevant fields and
356
+ // narrow `target` away from 'swift' here so the function's param type can
357
+ // stay tight.
358
+ if (parsed.target !== 'swift') {
359
+ warnIfKotlinNoOpFlags({
360
+ target: parsed.target,
361
+ ...(parsed.unsupportedUnions !== undefined ? { unsupportedUnions: parsed.unsupportedUnions } : {}),
362
+ });
363
+ }
220
364
  const source = parsed.url ?? parsed.file;
221
365
  console.log(`[ts-procedures-codegen] Reading docs from ${source}...`);
222
366
  if (parsed.watch) {
223
367
  await runWithWatch(parsed);
224
368
  }
225
369
  else {
370
+ const kotlinWiring = parsed.target === 'kotlin'
371
+ ? {
372
+ target: 'kotlin',
373
+ kotlinPackage: parsed.kotlin.package,
374
+ kotlinEmitter: await (await import('../targets/kotlin/ajsc-adapter.js')).resolveProductionKotlinEmitter(),
375
+ }
376
+ : {};
377
+ const swiftWiring = parsed.target === 'swift'
378
+ ? {
379
+ target: 'swift',
380
+ swiftEmitter: await (await import('../targets/swift/ajsc-adapter.js')).resolveProductionSwiftEmitter(),
381
+ }
382
+ : {};
226
383
  const result = await generateClient({
227
384
  url: parsed.url,
228
385
  file: parsed.file,
@@ -234,12 +391,19 @@ async function main() {
234
391
  selfContained: parsed.selfContained,
235
392
  serviceName: parsed.serviceName,
236
393
  cleanOutDir: parsed.cleanOutDir,
394
+ ...(parsed.kotlin?.serializer !== undefined ? { kotlinSerializer: parsed.kotlin.serializer } : {}),
395
+ ...(parsed.unsupportedUnions !== undefined ? { unsupportedUnions: parsed.unsupportedUnions } : {}),
396
+ ...(parsed.swift?.serializer !== undefined ? { swiftSerializer: parsed.swift.serializer } : {}),
397
+ ...(parsed.swift?.accessLevel !== undefined ? { swiftAccessLevel: parsed.swift.accessLevel } : {}),
398
+ ...kotlinWiring,
399
+ ...swiftWiring,
237
400
  });
238
401
  if (parsed.dryRun) {
239
402
  console.log(`[ts-procedures-codegen] Dry run complete — ${result.length} files would be generated`);
240
403
  }
241
404
  else {
242
405
  console.log(`[ts-procedures-codegen] Generated ${result.length} files → ${parsed.outDir}`);
406
+ printPostRunHints(parsed);
243
407
  }
244
408
  }
245
409
  }