swallowkit 1.1.2 → 2.0.0-alpha.10

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 (262) hide show
  1. package/README.ja.md +61 -7
  2. package/README.md +61 -7
  3. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  4. package/dist/cli/commands/add-auth.js +103 -27
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  7. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  8. package/dist/cli/commands/configure-mcp.js +42 -0
  9. package/dist/cli/commands/configure-mcp.js.map +1 -0
  10. package/dist/cli/commands/dev.d.ts +5 -2
  11. package/dist/cli/commands/dev.d.ts.map +1 -1
  12. package/dist/cli/commands/dev.js +143 -67
  13. package/dist/cli/commands/dev.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts +14 -7
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/init.js +149 -170
  17. package/dist/cli/commands/init.js.map +1 -1
  18. package/dist/cli/commands/migrations.d.ts.map +1 -1
  19. package/dist/cli/commands/migrations.js +25 -0
  20. package/dist/cli/commands/migrations.js.map +1 -1
  21. package/dist/cli/commands/provision.d.ts +5 -0
  22. package/dist/cli/commands/provision.d.ts.map +1 -1
  23. package/dist/cli/commands/provision.js +278 -55
  24. package/dist/cli/commands/provision.js.map +1 -1
  25. package/dist/cli/commands/scaffold.d.ts +5 -1
  26. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  27. package/dist/cli/commands/scaffold.js +123 -18
  28. package/dist/cli/commands/scaffold.js.map +1 -1
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +49 -8
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/core/config.d.ts +2 -1
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +19 -0
  36. package/dist/core/config.js.map +1 -1
  37. package/dist/core/operations/create-model.d.ts +2 -0
  38. package/dist/core/operations/create-model.d.ts.map +1 -1
  39. package/dist/core/operations/create-model.js +60 -8
  40. package/dist/core/operations/create-model.js.map +1 -1
  41. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  42. package/dist/core/operations/migration-generator.js +12 -1
  43. package/dist/core/operations/migration-generator.js.map +1 -1
  44. package/dist/core/operations/model-migration.d.ts +22 -0
  45. package/dist/core/operations/model-migration.d.ts.map +1 -0
  46. package/dist/core/operations/model-migration.js +36 -0
  47. package/dist/core/operations/model-migration.js.map +1 -0
  48. package/dist/core/operations/provision-deployment.d.ts +52 -0
  49. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  50. package/dist/core/operations/provision-deployment.js +164 -0
  51. package/dist/core/operations/provision-deployment.js.map +1 -0
  52. package/dist/core/operations/provision-operations.d.ts +62 -6
  53. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  54. package/dist/core/operations/provision-operations.js +653 -115
  55. package/dist/core/operations/provision-operations.js.map +1 -1
  56. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  57. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  58. package/dist/core/operations/scaffold-apply.js +1 -0
  59. package/dist/core/operations/scaffold-apply.js.map +1 -1
  60. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  61. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  62. package/dist/core/operations/scaffold-machine.js.map +1 -1
  63. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  64. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  65. package/dist/core/operations/scaffold-plan.js +3 -1
  66. package/dist/core/operations/scaffold-plan.js.map +1 -1
  67. package/dist/core/project/agent-instructions.d.ts +2 -1
  68. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  69. package/dist/core/project/agent-instructions.js +15 -0
  70. package/dist/core/project/agent-instructions.js.map +1 -1
  71. package/dist/core/project/boundaries.d.ts +5 -2
  72. package/dist/core/project/boundaries.d.ts.map +1 -1
  73. package/dist/core/project/boundaries.js +51 -20
  74. package/dist/core/project/boundaries.js.map +1 -1
  75. package/dist/core/project/capabilities.d.ts +17 -0
  76. package/dist/core/project/capabilities.d.ts.map +1 -1
  77. package/dist/core/project/capabilities.js +45 -6
  78. package/dist/core/project/capabilities.js.map +1 -1
  79. package/dist/core/project/home-page.d.ts +4 -0
  80. package/dist/core/project/home-page.d.ts.map +1 -0
  81. package/dist/core/project/home-page.js +113 -0
  82. package/dist/core/project/home-page.js.map +1 -0
  83. package/dist/core/project/manifest.d.ts +3 -1
  84. package/dist/core/project/manifest.d.ts.map +1 -1
  85. package/dist/core/project/manifest.js +47 -25
  86. package/dist/core/project/manifest.js.map +1 -1
  87. package/dist/core/project/mcp-config.d.ts +26 -0
  88. package/dist/core/project/mcp-config.d.ts.map +1 -0
  89. package/dist/core/project/mcp-config.js +117 -0
  90. package/dist/core/project/mcp-config.js.map +1 -0
  91. package/dist/core/project/react-project.d.ts +26 -0
  92. package/dist/core/project/react-project.d.ts.map +1 -0
  93. package/dist/core/project/react-project.js +443 -0
  94. package/dist/core/project/react-project.js.map +1 -0
  95. package/dist/core/project/state.d.ts +7 -0
  96. package/dist/core/project/state.d.ts.map +1 -1
  97. package/dist/core/project/state.js +86 -0
  98. package/dist/core/project/state.js.map +1 -1
  99. package/dist/core/project/validation.d.ts.map +1 -1
  100. package/dist/core/project/validation.js +32 -10
  101. package/dist/core/project/validation.js.map +1 -1
  102. package/dist/core/project/workflows.d.ts.map +1 -1
  103. package/dist/core/project/workflows.js +53 -7
  104. package/dist/core/project/workflows.js.map +1 -1
  105. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/auth-generator.js +26 -27
  107. package/dist/core/scaffold/auth-generator.js.map +1 -1
  108. package/dist/core/scaffold/model-parser.d.ts +23 -0
  109. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  110. package/dist/core/scaffold/model-parser.js +407 -137
  111. package/dist/core/scaffold/model-parser.js.map +1 -1
  112. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  113. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  114. package/dist/core/scaffold/native-schema-generator.js +294 -91
  115. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  116. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  117. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  118. package/dist/core/scaffold/openapi-generator.js +53 -3
  119. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  120. package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
  121. package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
  122. package/dist/core/scaffold/react-bff-generator.js +252 -0
  123. package/dist/core/scaffold/react-bff-generator.js.map +1 -0
  124. package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
  125. package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
  126. package/dist/core/scaffold/react-ui-generator.js +41 -0
  127. package/dist/core/scaffold/react-ui-generator.js.map +1 -0
  128. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  129. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  130. package/dist/core/scaffold/ui-generator.js +68 -7
  131. package/dist/core/scaffold/ui-generator.js.map +1 -1
  132. package/dist/core/verify/index.d.ts +4 -3
  133. package/dist/core/verify/index.d.ts.map +1 -1
  134. package/dist/core/verify/index.js +122 -5
  135. package/dist/core/verify/index.js.map +1 -1
  136. package/dist/index.d.ts +3 -3
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +3 -2
  139. package/dist/index.js.map +1 -1
  140. package/dist/machine/contracts.d.ts +1 -0
  141. package/dist/machine/contracts.d.ts.map +1 -1
  142. package/dist/machine/errors.d.ts +3 -2
  143. package/dist/machine/errors.d.ts.map +1 -1
  144. package/dist/machine/errors.js +2 -1
  145. package/dist/machine/errors.js.map +1 -1
  146. package/dist/machine/index.d.ts.map +1 -1
  147. package/dist/machine/index.js +61 -30
  148. package/dist/machine/index.js.map +1 -1
  149. package/dist/mcp/index.d.ts +6 -4
  150. package/dist/mcp/index.d.ts.map +1 -1
  151. package/dist/mcp/index.js +210 -25
  152. package/dist/mcp/index.js.map +1 -1
  153. package/dist/mcp/version-selection.d.ts +55 -0
  154. package/dist/mcp/version-selection.d.ts.map +1 -0
  155. package/dist/mcp/version-selection.js +152 -0
  156. package/dist/mcp/version-selection.js.map +1 -0
  157. package/dist/types/index.d.ts +4 -0
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/utils/azure-cli.d.ts +56 -0
  160. package/dist/utils/azure-cli.d.ts.map +1 -0
  161. package/dist/utils/azure-cli.js +326 -0
  162. package/dist/utils/azure-cli.js.map +1 -0
  163. package/dist/utils/azure-diagnostics.d.ts +16 -0
  164. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  165. package/dist/utils/azure-diagnostics.js +68 -0
  166. package/dist/utils/azure-diagnostics.js.map +1 -0
  167. package/dist/utils/package-manager.d.ts +2 -2
  168. package/dist/utils/package-manager.d.ts.map +1 -1
  169. package/dist/utils/package-manager.js +6 -3
  170. package/dist/utils/package-manager.js.map +1 -1
  171. package/dist/utils/python-uv.d.ts +5 -7
  172. package/dist/utils/python-uv.d.ts.map +1 -1
  173. package/dist/utils/python-uv.js +50 -24
  174. package/dist/utils/python-uv.js.map +1 -1
  175. package/dist/utils/react-dev.d.ts +4 -0
  176. package/dist/utils/react-dev.d.ts.map +1 -0
  177. package/dist/utils/react-dev.js +35 -0
  178. package/dist/utils/react-dev.js.map +1 -0
  179. package/dist/utils/semver.d.ts +14 -0
  180. package/dist/utils/semver.d.ts.map +1 -0
  181. package/dist/utils/semver.js +61 -0
  182. package/dist/utils/semver.js.map +1 -0
  183. package/package.json +7 -4
  184. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  185. package/src/__tests__/agent-instructions.test.ts +6 -1
  186. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  187. package/src/__tests__/auth.test.ts +9 -0
  188. package/src/__tests__/azure-cli.test.ts +202 -0
  189. package/src/__tests__/boundaries.test.ts +42 -0
  190. package/src/__tests__/csharp-schema.test.ts +261 -0
  191. package/src/__tests__/dev.test.ts +13 -6
  192. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  193. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  194. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  195. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  196. package/src/__tests__/home-page.test.ts +50 -0
  197. package/src/__tests__/init.test.ts +145 -5
  198. package/src/__tests__/mcp-version.test.ts +262 -0
  199. package/src/__tests__/mcp.test.ts +120 -3
  200. package/src/__tests__/multi-auth.test.ts +115 -4
  201. package/src/__tests__/package-manager.test.ts +17 -2
  202. package/src/__tests__/plan-apply.test.ts +179 -0
  203. package/src/__tests__/provision-runtime.test.ts +722 -0
  204. package/src/__tests__/python-uv.test.ts +50 -14
  205. package/src/__tests__/python-venv.test.ts +48 -0
  206. package/src/__tests__/react-dev.test.ts +72 -0
  207. package/src/__tests__/react.test.ts +838 -0
  208. package/src/__tests__/release-metadata.test.ts +121 -0
  209. package/src/__tests__/schema-analysis.test.ts +329 -0
  210. package/src/cli/commands/add-auth.ts +103 -29
  211. package/src/cli/commands/configure-mcp.ts +54 -0
  212. package/src/cli/commands/dev.ts +145 -73
  213. package/src/cli/commands/init.ts +147 -172
  214. package/src/cli/commands/migrations.ts +22 -0
  215. package/src/cli/commands/provision.ts +294 -56
  216. package/src/cli/commands/scaffold.ts +145 -22
  217. package/src/cli/index.ts +46 -10
  218. package/src/core/config.ts +21 -1
  219. package/src/core/operations/create-model.ts +51 -13
  220. package/src/core/operations/migration-generator.ts +14 -1
  221. package/src/core/operations/model-migration.ts +52 -0
  222. package/src/core/operations/provision-deployment.ts +183 -0
  223. package/src/core/operations/provision-operations.ts +662 -154
  224. package/src/core/operations/scaffold-apply.ts +2 -0
  225. package/src/core/operations/scaffold-machine.ts +1 -0
  226. package/src/core/operations/scaffold-plan.ts +5 -1
  227. package/src/core/project/agent-instructions.ts +17 -1
  228. package/src/core/project/agent-templates/AGENTS.md +113 -15
  229. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  230. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  231. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  232. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  233. package/src/core/project/boundaries.ts +62 -20
  234. package/src/core/project/capabilities.ts +63 -6
  235. package/src/core/project/home-page.ts +114 -0
  236. package/src/core/project/manifest.ts +52 -27
  237. package/src/core/project/mcp-config.ts +84 -0
  238. package/src/core/project/react-project.ts +529 -0
  239. package/src/core/project/state.ts +70 -0
  240. package/src/core/project/validation.ts +32 -10
  241. package/src/core/project/workflows.ts +53 -7
  242. package/src/core/scaffold/auth-generator.ts +26 -27
  243. package/src/core/scaffold/model-parser.ts +413 -129
  244. package/src/core/scaffold/native-schema-generator.ts +319 -103
  245. package/src/core/scaffold/openapi-generator.ts +63 -5
  246. package/src/core/scaffold/react-bff-generator.ts +292 -0
  247. package/src/core/scaffold/react-ui-generator.ts +51 -0
  248. package/src/core/scaffold/ui-generator.ts +70 -8
  249. package/src/core/verify/index.ts +118 -6
  250. package/src/index.ts +3 -1
  251. package/src/machine/contracts.ts +1 -0
  252. package/src/machine/errors.ts +4 -2
  253. package/src/machine/index.ts +67 -36
  254. package/src/mcp/index.ts +221 -31
  255. package/src/mcp/version-selection.ts +163 -0
  256. package/src/types/index.ts +4 -0
  257. package/src/utils/azure-cli.ts +315 -0
  258. package/src/utils/azure-diagnostics.ts +75 -0
  259. package/src/utils/package-manager.ts +7 -4
  260. package/src/utils/python-uv.ts +53 -33
  261. package/src/utils/react-dev.ts +31 -0
  262. package/src/utils/semver.ts +59 -0
@@ -27,6 +27,13 @@ export interface CapabilityAuthProvider {
27
27
  export interface CapabilitiesContract {
28
28
  contractVersion: number;
29
29
  swallowkitVersion: string;
30
+ frontends: {
31
+ supported: string[];
32
+ default: string;
33
+ configuration: string;
34
+ reactArchitecture: string;
35
+ development: string;
36
+ };
30
37
  modelDeclarations: CapabilityModelDeclaration[];
31
38
  authentication: {
32
39
  correctWorkflow: string[];
@@ -46,6 +53,7 @@ export interface CapabilitiesContract {
46
53
  sharedModels: {
47
54
  idFieldRule: string;
48
55
  buildNote: string;
56
+ nativeSchemaMapping: string[];
49
57
  };
50
58
  seeding: {
51
59
  createTemplates: string;
@@ -53,10 +61,19 @@ export interface CapabilitiesContract {
53
61
  notes: string[];
54
62
  };
55
63
  machineCommands: string[];
64
+ provisioning: {
65
+ execution: string;
66
+ completion: string;
67
+ tracking: string;
68
+ recovery: string;
69
+ diagnostics: string;
70
+ approval: string;
71
+ };
56
72
  verifyChecks: {
57
73
  defaultChecks: string[];
58
74
  optInChecks: string[];
59
75
  customChecks: string;
76
+ backendBuild: string;
60
77
  };
61
78
  }
62
79
 
@@ -64,6 +81,13 @@ export function inspectCapabilities(): CapabilitiesContract {
64
81
  return {
65
82
  contractVersion: 1,
66
83
  swallowkitVersion: getSwallowKitVersion(),
84
+ frontends: {
85
+ supported: ["nextjs", "react"],
86
+ default: "nextjs",
87
+ configuration: "init --frontend nextjs|react; stored as frontend.framework in swallowkit.config.js. Missing settings preserve Next.js.",
88
+ reactArchitecture: "React/Vite SPA on SWA -> HTTP-only SWA managed Functions BFF (bff/) -> standalone Functions (functions/). Never link standalone Functions to SWA. Backend keys stay server-side.",
89
+ development: "React dev runs Vite:3000, BFF:7073, Functions:7071 and SWA CLI:4280; use the SWA port for same-origin APIs and authentication. Build/verify include BFF typechecking.",
90
+ },
67
91
  modelDeclarations: [
68
92
  {
69
93
  name: "schema",
@@ -94,6 +118,19 @@ export function inspectCapabilities(): CapabilitiesContract {
94
118
  "Role-based access control metadata applied to the generated CRUD Functions. `roles` guards all operations; `read`/`write` guard per-operation; `policy` references a named authorization policy from swallowkit.config auth.authorization.policies.",
95
119
  format:
96
120
  "export const authPolicy = { roles: ['admin'] };\n// or per-operation:\nexport const authPolicy = { read: ['user'], write: ['admin'] };\n// or a named policy:\nexport const authPolicy = { policy: 'adminOnly' };",
121
+ notes: [
122
+ "An explicit authPolicy without configured authentication blocks plan/apply/generate scaffold with auth-policy-without-auth before any artifacts are written. Run plan auth, apply auth, then re-plan scaffold. An empty {} declaration keeps the existing behavior: no explicit policy.",
123
+ "With named auth.schemes, define auth.authorization.policies selecting schemes and roles, then reference them via policy or read/write names. A role-only declaration cannot select a named scheme and blocks with auth-policy-requires-named-policy; it is supported by legacy single-provider authentication.",
124
+ "Conversely, single-provider authentication (auth.provider) supports role lists only. Referencing a named policy (authPolicy.policy, read/write names, or a named auth.authorization.defaultPolicy) blocks with auth-named-policy-requires-named-scheme; add a named scheme first.",
125
+ ],
126
+ },
127
+ {
128
+ name: "references",
129
+ required: false,
130
+ description:
131
+ "Explicit foreign keys used by generated UI lookups. Without it, a string field named <model>Id is treated as a foreign key only when shared/models contains that model (userId -> User only if user.ts exists). Map a field to false to disable inference. References to missing models are ignored with a reference-model-not-found warning.",
132
+ format: "export const references = { authorId: 'User', legacyId: false };",
133
+ notes: ["Only single string id fields can reference a model.", "No lookup UI, fetch, or OpenAPI reference is generated for fields that do not reference an existing model."],
97
134
  },
98
135
  {
99
136
  name: "connectorConfig",
@@ -160,15 +197,24 @@ export function inspectCapabilities(): CapabilitiesContract {
160
197
  ],
161
198
  notGuaranteed: [
162
199
  "Owner scoping: generated CRUD does NOT restrict rows to the authenticated principal. Any authenticated caller passing the policy can read/write any document.",
200
+ "Without authentication, plan scaffold reports user-scoped-data-without-auth when the partition key or an owner-like field (userId, learnerId, ownerId, ...) suggests per-user data.",
163
201
  ],
164
202
  guidance: [
165
- "For multi-user data where users may only touch their own records, add ai-authored custom endpoints that derive the scoping key (e.g. learnerId) from the verified principal — never from the request body — and restrict generated write endpoints to admin roles or disable them.",
203
+ "Configure authentication via plan/apply auth before declaring authPolicy. For multi-user data where users may only touch their own records, add ai-authored custom endpoints that derive the scoping key (e.g. learnerId) from the verified principal — never from the request body — and restrict generated write endpoints to admin roles or disable them.",
166
204
  ],
167
205
  },
168
206
  sharedModels: {
169
207
  idFieldRule: "id: z.string() (required). createdAt/updatedAt: z.string().optional(). The backend owns their values.",
208
+ nativeSchemaMapping: [
209
+ "C#/Python schema assets derive types from Zod, never from field names: z.string() -> string/str (including createdAt/updatedAt).",
210
+ "Date types only when declared: z.iso.datetime() -> DateTime/datetime, z.iso.date() -> DateOnly/date, z.date() -> DateTime/datetime.",
211
+ "z.number() -> double / float|int; z.number().int() or z.int() -> long / int.",
212
+ "Inline z.object({...}) fields become named classes <Model><Field> (e.g. ProbeItemNested); imported model schemas keep their own class.",
213
+ "min/max/length/nonempty/regex/int/gt/gte/lt/lte/positive/nonnegative and array min/max are enforced by C# IValidatableObject.Validate and Python Field bounds/validators. Regexes use search semantics like Zod.",
214
+ "String formats (email/uuid/url), refinements, and transforms are not re-implemented natively; the BFF Zod schemas remain the authoritative validation.",
215
+ ],
170
216
  buildNote:
171
- "verify typecheck resolves the shared package from its built dist/. SwallowKit rebuilds shared automatically during verify; when typechecking manually, build the shared package first (errors like \"no exported member\" usually mean a stale dist).",
217
+ "create-model and plan/apply scaffold append missing model exports to shared/index.ts, including manually authored models. Manual exports are allowed (no markers); existing content is preserved. verify typecheck resolves the shared package from its built dist/. SwallowKit rebuilds shared automatically during verify; when typechecking manually, build shared first.",
172
218
  },
173
219
  seeding: {
174
220
  createTemplates: "swallowkit create-dev-seeds <environment> — writes dev-seeds/<environment>/<model>.json templates.",
@@ -176,18 +222,29 @@ export function inspectCapabilities(): CapabilitiesContract {
176
222
  notes: ["Editing dev-seeds/*.json alone does nothing; seeds are only applied via dev --seed-env."],
177
223
  },
178
224
  machineCommands: [
179
- "inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities",
225
+ "inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities; inspect provision --plan <runId> [--wait-seconds <0-3600>]",
180
226
  "validate project",
181
227
  "generate model <names...> | generate scaffold <model>",
182
- "plan scaffold <models...> | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if]",
183
- "apply scaffold [models...] --plan <id> [--approve] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve",
228
+ "plan scaffold <models...> [--api-only] [--skip-infra] | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if] [--resume-deployment <name>]",
229
+ "apply scaffold [models...] --plan <id> [--approve] [--skip-infra] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve [--wait-seconds <0-3600>] (also resumes the same execution checkpoint)",
184
230
  "verify project [--checks <ids>] [--compact]",
185
231
  "explain failure [--check <id>]",
232
+ "(regular CLI) migrations add-model <model> [--dry-run] — generate only the container migration after scaffold --skip-infra",
233
+ "(regular CLI) configure-mcp [--version auto|latest|<semver>] — re-select the MCP launcher version in .mcp.json",
186
234
  ],
235
+ provisioning: {
236
+ execution: "Submit Bicep once with az deployment group create --no-wait, then bounded state polling (default 300 seconds, configurable 0-3600) for machine/MCP; the human `swallowkit provision` command waits until Azure reaches a terminal state. Azure CLI invocations are bounded separately. Warning-only stderr with exit 0 is not a failure; output capture limits do not terminate az.",
237
+ completion: "status complete/outcome succeeded requires Azure Succeeded plus verified migration tags and local ledger completion. Running/Accepted -> in-progress; unreadable/not-found state -> blocked/outcome unknown; bookkeeping interruption after Azure Succeeded -> blocked/outcome partial; Failed/Canceled -> failed with Azure causes.",
238
+ tracking: "Approved runs persist before Azure writes under .swallowkit/state/provision/. runId equals the original planId and preserves the subscription, RG, deployment ID/correlation, fingerprints and migration state. inspect provision is read-only; apply provision --plan resumes without re-submitting attempted deployments. Ctrl+C/termination may leave Azure running; never cancel/delete without explicit consent.",
239
+ recovery: "For an older untracked baseline, plan provision --resume-deployment <name> compares the existing ARM template and non-secure parameters to the local Bicep/parameters. It observes that deployment and only tags/records it after Succeeded. Mismatches, unreadable evidence, secure parameters or non-baseline migrations block; do not use reconcile just to repair bookkeeping.",
240
+ diagnostics: "Machine/MCP results include bounded/redacted stdout/stderr, omitted-output flags, exit code, signal, failure kind, elapsed time and deployment identity, but no deployment outputs. Normal CLI shows named resource information and CI/CD setup guidance after success. Never print --debug output, keys, tokens or connection strings.",
241
+ approval: "Machine/MCP initial apply and every resume require explicit human approval. A human invoking normal provision or provision --resume explicitly requests execution without another confirmation prompt. Agents must not switch to normal CLI to bypass approval. A passing local command or an ok envelope is not proof of completed Azure provisioning; inspect status, outcome and nextActions.",
242
+ },
187
243
  verifyChecks: {
188
- defaultChecks: ["structure", "drift", "typecheck"],
244
+ defaultChecks: ["structure", "drift", "typecheck", "backend-build"],
189
245
  optInChecks: ["build", "lint", "test"],
190
246
  customChecks: "swallowkit.config verify.checks: [{ id, command, title?, timeoutMs? }]",
247
+ backendBuild: "Runs dotnet build in functions/ (C#), the project tsc --noEmit (TypeScript), or Python 3 compileall (syntax only, using functions/.venv before PATH). Missing toolchains are skip with a reason, not proof of validation. Root build scripts do not replace backend-build; use --checks \"structure,drift,typecheck,backend-build,build\" for both.",
191
248
  },
192
249
  };
193
250
  }
@@ -0,0 +1,114 @@
1
+ import { FrontendFramework } from "../../types";
2
+ import { PackageManager, getCommands } from "../../utils/package-manager";
3
+ import { adaptReactPage } from "../scaffold/react-ui-generator";
4
+
5
+ export function buildHomePage(
6
+ frontend: FrontendFramework,
7
+ pm: PackageManager
8
+ ): string {
9
+ const framework = frontend === "react" ? "React" : "Next.js";
10
+ const page = `import { useState } from 'react';
11
+ import Link from 'next/link';
12
+ import { scaffoldConfig } from '@/lib/scaffold-config';
13
+
14
+ export default function Home() {
15
+ const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
16
+ const [message, setMessage] = useState('');
17
+
18
+ const testConnection = async () => {
19
+ setGreetingStatus('loading');
20
+ try {
21
+ const response = await fetch('/api/greet?name=SwallowKit');
22
+ const data = await response.json();
23
+ if (!response.ok) {
24
+ throw new Error(data.error || \`Server error: \${response.status}\`);
25
+ }
26
+ setMessage(data.message);
27
+ setGreetingStatus('success');
28
+ } catch (error) {
29
+ setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
30
+ setGreetingStatus('error');
31
+ }
32
+ };
33
+
34
+ return (
35
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
36
+ <div className="container mx-auto px-4 py-12">
37
+ <header className="text-center mb-16">
38
+ <h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
39
+ Welcome to SwallowKit
40
+ </h1>
41
+ <p className="text-xl text-gray-600 dark:text-gray-400">
42
+ ${framework} on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
43
+ </p>
44
+ </header>
45
+
46
+ <section className="max-w-2xl mx-auto mb-12">
47
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
48
+ <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
49
+ Test BFF → Functions Connection
50
+ </h2>
51
+ <button
52
+ onClick={testConnection}
53
+ disabled={greetingStatus === 'loading'}
54
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
55
+ >
56
+ {greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
57
+ </button>
58
+ {greetingStatus === 'success' && (
59
+ <div role="status" className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
60
+ <p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
61
+ <p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
62
+ </div>
63
+ )}
64
+ {greetingStatus === 'error' && (
65
+ <div role="alert" className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
66
+ <p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
67
+ <p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
68
+ </div>
69
+ )}
70
+ </div>
71
+ </section>
72
+
73
+ {scaffoldConfig.models.length > 0 ? (
74
+ <section className="max-w-6xl mx-auto">
75
+ <h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
76
+ <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
77
+ {scaffoldConfig.models.map((model) => (
78
+ <Link
79
+ key={model.name}
80
+ href={model.path}
81
+ className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
82
+ >
83
+ <h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
84
+ <p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
85
+ </Link>
86
+ ))}
87
+ </div>
88
+ </section>
89
+ ) : (
90
+ <section className="max-w-2xl mx-auto text-center">
91
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
92
+ <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
93
+ <p className="text-gray-600 dark:text-gray-400 mb-6">
94
+ Create your first model with Zod and generate CRUD operations automatically.
95
+ </p>
96
+ <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
97
+ ${getCommands(pm).exec} swallowkit scaffold shared/models/your-model.ts
98
+ </code>
99
+ </div>
100
+ </section>
101
+ )}
102
+
103
+ <footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
104
+ <p>Built with SwallowKit</p>
105
+ </footer>
106
+ </div>
107
+ </div>
108
+ );
109
+ }
110
+ `;
111
+ return frontend === "react"
112
+ ? adaptReactPage(page)
113
+ : `'use client'\n\nexport const dynamic = 'force-dynamic';\n\n${page}`;
114
+ }
@@ -1,8 +1,8 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { getBackendLanguage, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
3
+ import { getBackendLanguage, getFrontendFramework, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
4
4
  import { getAllModels, toCamelCase, toKebabCase } from "../scaffold/model-parser";
5
- import { AuthConfig, BackendLanguage, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
5
+ import { AuthConfig, BackendLanguage, FrontendFramework, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
6
6
  import { captureConsoleMessages, withWorkingDirectory } from "../operations/runtime";
7
7
 
8
8
  const CONFIG_CANDIDATES = [
@@ -69,6 +69,7 @@ export interface ProjectManifestArtifacts {
69
69
 
70
70
  export interface ProjectManifestArchitecture {
71
71
  pattern: string;
72
+ frontendFramework?: FrontendFramework;
72
73
  backendLanguage: BackendLanguage;
73
74
  layerBoundaries: string[];
74
75
  hasSharedWorkspace: boolean;
@@ -81,6 +82,7 @@ export interface ProjectManifest {
81
82
  generatedAt: string;
82
83
  configPath: string | null;
83
84
  backendLanguage: BackendLanguage;
85
+ frontendFramework?: FrontendFramework;
84
86
  configValidation: {
85
87
  valid: boolean;
86
88
  errors: string[];
@@ -115,7 +117,10 @@ function relativeProjectPath(projectRoot: string, targetPath: string): string {
115
117
  return path.relative(projectRoot, targetPath).replace(/\\/g, "/");
116
118
  }
117
119
 
118
- function buildModules(projectRoot: string): ProjectManifestModule[] {
120
+ function buildModules(projectRoot: string, frontend: FrontendFramework): ProjectManifestModule[] {
121
+ const bffRoot = frontend === "react" ? "bff" : "app/api";
122
+ const uiRoot = frontend === "react" ? "src" : "app";
123
+ const authRoot = frontend === "react" ? "bff/src/auth" : "app/api/auth";
119
124
  const candidates: ProjectManifestModule[] = [
120
125
  {
121
126
  name: "shared-models",
@@ -126,8 +131,8 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
126
131
  {
127
132
  name: "bff",
128
133
  kind: "bff",
129
- rootPath: "app/api",
130
- exists: fs.existsSync(path.join(projectRoot, "app", "api")),
134
+ rootPath: bffRoot,
135
+ exists: fs.existsSync(path.join(projectRoot, bffRoot)),
131
136
  },
132
137
  {
133
138
  name: "functions",
@@ -138,8 +143,8 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
138
143
  {
139
144
  name: "ui",
140
145
  kind: "ui",
141
- rootPath: "app",
142
- exists: fs.existsSync(path.join(projectRoot, "app")),
146
+ rootPath: uiRoot,
147
+ exists: fs.existsSync(path.join(projectRoot, uiRoot)),
143
148
  },
144
149
  {
145
150
  name: "infrastructure",
@@ -150,21 +155,21 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
150
155
  {
151
156
  name: "auth",
152
157
  kind: "auth",
153
- rootPath: "app/api/auth",
154
- exists: fs.existsSync(path.join(projectRoot, "app", "api", "auth")),
158
+ rootPath: authRoot,
159
+ exists: fs.existsSync(path.join(projectRoot, authRoot)),
155
160
  },
156
161
  ];
157
162
 
158
163
  return candidates;
159
164
  }
160
165
 
161
- function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
166
+ function buildArtifacts(projectRoot: string, frontend: FrontendFramework): ProjectManifestArtifacts {
162
167
  const openApiDir = path.join(projectRoot, "functions", "openapi");
163
168
  const generatedDir = path.join(projectRoot, "functions", "generated");
164
169
 
165
170
  return {
166
- callFunctionHelperPath: "lib/api/call-function.ts",
167
- callFunctionHelperExists: fs.existsSync(path.join(projectRoot, "lib", "api", "call-function.ts")),
171
+ callFunctionHelperPath: frontend === "react" ? "bff/src/lib/call-function.ts" : "lib/api/call-function.ts",
172
+ callFunctionHelperExists: fs.existsSync(path.join(projectRoot, ...(frontend === "react" ? ["bff", "src", "lib", "call-function.ts"] : ["lib", "api", "call-function.ts"]))),
168
173
  openApiSpecs: fs.existsSync(openApiDir)
169
174
  ? fs.readdirSync(openApiDir)
170
175
  .filter((entry) => entry.endsWith(".json"))
@@ -178,8 +183,8 @@ function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
178
183
  : [],
179
184
  proxyPath: "proxy.ts",
180
185
  proxyExists: fs.existsSync(path.join(projectRoot, "proxy.ts")),
181
- loginPagePath: "app/login/page.tsx",
182
- loginPageExists: fs.existsSync(path.join(projectRoot, "app", "login", "page.tsx")),
186
+ loginPagePath: frontend === "react" ? "src/pages/login.tsx" : "app/login/page.tsx",
187
+ loginPageExists: fs.existsSync(path.join(projectRoot, ...(frontend === "react" ? ["src", "pages", "login.tsx"] : ["app", "login", "page.tsx"]))),
183
188
  authContextPath: "lib/auth/auth-context.tsx",
184
189
  authContextExists: fs.existsSync(path.join(projectRoot, "lib", "auth", "auth-context.tsx")),
185
190
  };
@@ -188,7 +193,8 @@ function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
188
193
  function buildEntityRoutes(
189
194
  projectRoot: string,
190
195
  backendLanguage: BackendLanguage,
191
- entity: ProjectManifestEntity
196
+ entity: ProjectManifestEntity,
197
+ frontend: FrontendFramework
192
198
  ): ProjectManifestRoute[] {
193
199
  const modelCamel = toCamelCase(entity.name);
194
200
  const modelKebab = toKebabCase(entity.name);
@@ -229,8 +235,10 @@ function buildEntityRoutes(
229
235
  entityName: entity.name,
230
236
  methods: listMethods,
231
237
  publicPath: `/api/${modelCamel}`,
232
- filePath: `app/api/${modelCamel}/route.ts`,
233
- exists: fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "route.ts")),
238
+ filePath: frontend === "react" ? `bff/src/${modelCamel}/index.ts` : `app/api/${modelCamel}/route.ts`,
239
+ exists: frontend === "react"
240
+ ? fs.existsSync(path.join(projectRoot, "bff", "src", modelCamel, "index.ts")) && fs.existsSync(path.join(projectRoot, "bff", modelCamel, "function.json"))
241
+ : fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "route.ts")),
234
242
  },
235
243
  {
236
244
  name: `${entity.name}-bff-detail`,
@@ -239,8 +247,10 @@ function buildEntityRoutes(
239
247
  entityName: entity.name,
240
248
  methods: detailMethods,
241
249
  publicPath: `/api/${modelCamel}/{id}`,
242
- filePath: `app/api/${modelCamel}/[id]/route.ts`,
243
- exists: fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "[id]", "route.ts")),
250
+ filePath: frontend === "react" ? `bff/src/${modelCamel}/index.ts` : `app/api/${modelCamel}/[id]/route.ts`,
251
+ exists: frontend === "react"
252
+ ? fs.existsSync(path.join(projectRoot, "bff", "src", modelCamel, "index.ts")) && fs.existsSync(path.join(projectRoot, "bff", modelCamel, "function.json"))
253
+ : fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "[id]", "route.ts")),
244
254
  },
245
255
  {
246
256
  name: `${entity.name}-functions`,
@@ -255,7 +265,18 @@ function buildEntityRoutes(
255
265
  ];
256
266
  }
257
267
 
258
- function buildSystemRoutes(projectRoot: string): ProjectManifestRoute[] {
268
+ function buildSystemRoutes(projectRoot: string, frontend: FrontendFramework, auth?: AuthConfig): ProjectManifestRoute[] {
269
+ if (frontend === "react") {
270
+ const providers = Object.values(normalizeAuthConfig(auth)?.schemes ?? {}).map(scheme => scheme.provider);
271
+ return ["login", "logout", "me"].map(action => ({
272
+ name: `auth-${action}`, kind: "system", surface: "bff",
273
+ methods: [action === "me" ? "GET" : "POST"], publicPath: `/api/auth/${action}`,
274
+ filePath: "bff/src/auth/index.ts",
275
+ exists: (providers.includes("custom-jwt") || (action === "me" && providers.includes("external-token"))) &&
276
+ fs.existsSync(path.join(projectRoot, "bff", "src", "auth", "index.ts")) &&
277
+ fs.existsSync(path.join(projectRoot, "bff", "auth", "function.json")),
278
+ }));
279
+ }
259
280
  return [
260
281
  {
261
282
  name: "auth-login",
@@ -296,6 +317,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
296
317
  return withWorkingDirectory(projectRoot, async () => {
297
318
  const configPath = resolveConfigPath(projectRoot);
298
319
  const backendLanguage = getBackendLanguage(configPath || undefined);
320
+ const frontendFramework = getFrontendFramework(configPath || undefined);
299
321
  const config = getFullConfig(configPath || undefined);
300
322
  const configValidation = validateConfig(config);
301
323
  const { result: entities, messages } = await captureConsoleMessages(async () => {
@@ -327,8 +349,8 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
327
349
  );
328
350
 
329
351
  const routes = [
330
- ...entities.flatMap((entity) => buildEntityRoutes(projectRoot, backendLanguage, entity)),
331
- ...buildSystemRoutes(projectRoot),
352
+ ...entities.flatMap((entity) => buildEntityRoutes(projectRoot, backendLanguage, entity, frontendFramework)),
353
+ ...buildSystemRoutes(projectRoot, frontendFramework, config.auth),
332
354
  ].sort((left, right) => left.name.localeCompare(right.name));
333
355
 
334
356
  const manifest: ProjectManifest = {
@@ -336,6 +358,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
336
358
  generatedAt: new Date().toISOString(),
337
359
  configPath,
338
360
  backendLanguage,
361
+ frontendFramework,
339
362
  configValidation,
340
363
  connectors: Object.entries(config.connectors || {})
341
364
  .map(([name, definition]) => ({ name, definition }))
@@ -343,16 +366,18 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
343
366
  auth: normalizeAuthConfig(config.auth) || null,
344
367
  entities: entities.sort((left, right) => left.name.localeCompare(right.name)),
345
368
  routes,
346
- modules: buildModules(projectRoot),
347
- artifacts: buildArtifacts(projectRoot),
369
+ modules: buildModules(projectRoot, frontendFramework),
370
+ artifacts: buildArtifacts(projectRoot, frontendFramework),
348
371
  architecture: {
349
- pattern: "Next.js BFF + Azure Functions + shared Zod models",
372
+ pattern: frontendFramework === "react" ? "React SPA + SWA managed Functions BFF + standalone Azure Functions + shared Zod models" : "Next.js BFF + Azure Functions + shared Zod models",
373
+ frontendFramework,
350
374
  backendLanguage,
351
375
  layerBoundaries: [
352
376
  "shared/models -> schema source of truth",
353
- "app/api -> BFF layer only",
377
+ `${frontendFramework === "react" ? "bff" : "app/api"} -> BFF layer only`,
354
378
  "functions -> backend logic and data access",
355
- "app -> UI and pages",
379
+ `${frontendFramework === "react" ? "src" : "app"} -> UI and pages`,
380
+ "SWA and standalone Functions are not linked; backend credentials stay in the BFF",
356
381
  ],
357
382
  hasSharedWorkspace: fs.existsSync(path.join(projectRoot, "shared", "package.json")),
358
383
  hasConnectors: Object.keys(config.connectors || {}).length > 0,
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Project-scoped MCP launcher version management.
3
+ *
4
+ * `.mcp.json` intentionally resolves `swallowkit@latest` so that projects created
5
+ * with older releases still get the current stable tools. A prerelease project can
6
+ * be newer than `latest`; in that case the launcher is pinned to the project
7
+ * version until a stable release that understands project-aware delegation catches up.
8
+ */
9
+
10
+ import { spawnSync } from "child_process";
11
+ import * as fs from "fs";
12
+ import * as os from "os";
13
+ import * as path from "path";
14
+ import { compareSemver, isPrerelease, parseSemver } from "../../utils/semver";
15
+
16
+ export const MCP_CONFIG_PATH = ".mcp.json";
17
+ export const MCP_VERSION_ENV = "SWALLOWKIT_MCP_VERSION";
18
+ export const MCP_LATEST = "latest";
19
+
20
+ export interface McpPinDecision {
21
+ version: string;
22
+ reason: string;
23
+ }
24
+
25
+ /**
26
+ * Decide the SWALLOWKIT_MCP_VERSION value for a project that uses `projectVersion`.
27
+ * Uses `latest` unless the project version is newer than the published `latest`.
28
+ * When the registry could not be reached, prereleases are pinned conservatively.
29
+ */
30
+ export function resolveMcpPinVersion(projectVersion: string, publishedLatest: string | null): McpPinDecision {
31
+ if (!parseSemver(projectVersion)) {
32
+ return { version: MCP_LATEST, reason: `Project version ${projectVersion} is not a valid semantic version.` };
33
+ }
34
+ if (publishedLatest && parseSemver(publishedLatest)) {
35
+ const comparison = compareSemver(projectVersion, publishedLatest)!;
36
+ return comparison > 0
37
+ ? { version: projectVersion, reason: `Project version ${projectVersion} is newer than published latest ${publishedLatest}.` }
38
+ : { version: MCP_LATEST, reason: `Published latest ${publishedLatest} is the same as or newer than project version ${projectVersion}.` };
39
+ }
40
+ return isPrerelease(projectVersion)
41
+ ? { version: projectVersion, reason: `Published latest is unknown; prerelease ${projectVersion} is pinned so MCP matches the project.` }
42
+ : { version: MCP_LATEST, reason: "Published latest is unknown; stable projects keep latest." };
43
+ }
44
+
45
+ export function lookupPublishedLatestVersion(packageName: string, timeoutMs = 15000): string | null {
46
+ // Run outside the project so its devEngines/packageManager policy does not reject npm.
47
+ const result = spawnSync("npm", ["view", `${packageName}@latest`, "version"], {
48
+ cwd: os.tmpdir(),
49
+ encoding: "utf8",
50
+ timeout: timeoutMs,
51
+ shell: process.platform === "win32",
52
+ stdio: ["ignore", "pipe", "ignore"],
53
+ });
54
+ if (result.error || result.status !== 0) return null;
55
+ const version = String(result.stdout).trim().split(/\s+/).pop() ?? "";
56
+ return parseSemver(version) ? version : null;
57
+ }
58
+
59
+ export function readMcpConfigVersion(projectDir: string): string | null {
60
+ const configPath = path.join(projectDir, MCP_CONFIG_PATH);
61
+ if (!fs.existsSync(configPath)) return null;
62
+ try {
63
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
64
+ const value = config?.mcpServers?.swallowkit?.env?.[MCP_VERSION_ENV];
65
+ return typeof value === "string" ? value : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /** Update only the SwallowKit MCP version in .mcp.json, preserving all other content. */
72
+ export function writeMcpConfigVersion(projectDir: string, version: string): void {
73
+ const configPath = path.join(projectDir, MCP_CONFIG_PATH);
74
+ if (!fs.existsSync(configPath)) {
75
+ throw new Error(`${MCP_CONFIG_PATH} was not found. Run this command from a SwallowKit project root.`);
76
+ }
77
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
78
+ const server = config?.mcpServers?.swallowkit;
79
+ if (!server || typeof server !== "object") {
80
+ throw new Error(`${MCP_CONFIG_PATH} does not define mcpServers.swallowkit.`);
81
+ }
82
+ server.env = { ...(server.env ?? {}), [MCP_VERSION_ENV]: version };
83
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
84
+ }