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
@@ -0,0 +1,50 @@
1
+ import { buildHomePage } from "../core/project/home-page";
2
+ import { adaptReactPage } from "../core/scaffold/react-ui-generator";
3
+
4
+ describe("shared home page presentation", () => {
5
+ it.each(["npm", "pnpm"] as const)(
6
+ "differs only in framework label and routing with %s",
7
+ (pm) => {
8
+ const next = buildHomePage("nextjs", pm);
9
+ const react = buildHomePage("react", pm);
10
+ const adaptedNext = adaptReactPage(next)
11
+ .replace("export const dynamic = 'force-dynamic';\n\n", "")
12
+ .replace("Next.js on Azure", "React on Azure");
13
+
14
+ expect(react).toBe(adaptedNext);
15
+ expect(react).toContain("Welcome to SwallowKit");
16
+ expect(react).toContain("Your Models");
17
+ expect(react).toContain("Get Started");
18
+ expect(react).toContain("Built with SwallowKit");
19
+ expect(react).toContain(
20
+ `${pm === "npm" ? "npx" : "pnpm exec"} swallowkit scaffold shared/models/your-model.ts`
21
+ );
22
+ }
23
+ );
24
+
25
+ it("keeps Next.js-only directives out of the SPA", () => {
26
+ const next = buildHomePage("nextjs", "pnpm");
27
+ const react = buildHomePage("react", "pnpm");
28
+
29
+ expect(next).toContain("'use client'");
30
+ expect(next).toContain("export const dynamic = 'force-dynamic'");
31
+ expect(next).toContain("href={model.path}");
32
+ expect(react).not.toMatch(/next\/|use client|force-dynamic/);
33
+ expect(react).toContain("import { Link } from 'react-router-dom'");
34
+ expect(react).toContain("to={model.path}");
35
+ });
36
+
37
+ it.each(["nextjs", "react"] as const)(
38
+ "preserves connection states and same-origin requests for %s",
39
+ (frontend) => {
40
+ const source = buildHomePage(frontend, "pnpm");
41
+ expect(source).toContain("fetch('/api/greet?name=SwallowKit')");
42
+ expect(source).toContain("disabled={greetingStatus === 'loading'}");
43
+ expect(source).toContain("setGreetingStatus('success')");
44
+ expect(source).toContain("setGreetingStatus('error')");
45
+ expect(source).toContain('role="status"');
46
+ expect(source).toContain('role="alert"');
47
+ expect(source).not.toMatch(/BACKEND_FUNCTIONS_|VITE_/);
48
+ }
49
+ );
50
+ });
@@ -16,6 +16,7 @@ import {
16
16
  getAzureFunctionsPipeline,
17
17
  getGitHubFunctionsWorkflow,
18
18
  injectSwallowKitNextConfig,
19
+ ensureNextTailwindPostcssConfig,
19
20
  parseIgnoredBuilds,
20
21
  buildCosmosDbFreeTierBicepSource,
21
22
  buildFunctionsHostKeyBicepExpression,
@@ -25,6 +26,8 @@ import {
25
26
  getStaticWebAppSku,
26
27
  withNpmLockfileSafeManifests,
27
28
  } from "../cli/commands/init";
29
+ import { generateModelContainerMigration, initializeLegacyMigrations } from "../core/operations/migration-generator";
30
+ import { FileOperationSession } from "../core/operations/file-session";
28
31
 
29
32
  describe("injectSwallowKitNextConfig", () => {
30
33
  it("adds standalone settings without deprecated experimental options", () => {
@@ -40,6 +43,8 @@ export default nextConfig;
40
43
  const updated = injectSwallowKitNextConfig(original, "sample-app");
41
44
 
42
45
  expect(updated).toContain("output: 'standalone'");
46
+ expect(updated).toContain("cacheComponents: false");
47
+ expect(updated).not.toContain("partialPrefetching");
43
48
  expect(updated).toContain("agentRules: false");
44
49
  expect(updated).toContain("transpilePackages: ['@sample-app/shared']");
45
50
  expect(updated).toContain("serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers']");
@@ -60,6 +65,26 @@ module.exports = nextConfig;
60
65
  expect(updated).toContain("transpilePackages: ['@sample-app/shared']");
61
66
  expect(updated).toContain("agentRules: false");
62
67
  expect(updated).toContain("module.exports = nextConfig;");
68
+ expect(updated).toContain("cacheComponents: false");
69
+ });
70
+
71
+ it.each([true, false])("disables create-next-app Cache Components without duplicate keys (initially %s)", (enabled) => {
72
+ const original = `import type { NextConfig } from 'next';
73
+ const nextConfig: NextConfig = {
74
+ cacheComponents: ${enabled},
75
+ partialPrefetching: true,
76
+ reactCompiler: true,
77
+ };
78
+ export default nextConfig;
79
+ `;
80
+ const updated = injectSwallowKitNextConfig(original, "sample-app");
81
+ expect(updated).toContain("cacheComponents: false");
82
+ expect(updated).not.toContain("cacheComponents: true");
83
+ expect(updated).toContain("partialPrefetching: false");
84
+ expect(updated.match(/\bpartialPrefetching\s*:/g)).toHaveLength(1);
85
+ expect(updated.match(/\bcacheComponents\s*:/g)).toHaveLength(1);
86
+ expect(updated).toContain("reactCompiler: true");
87
+ expect(updated).toContain("output: 'standalone'");
63
88
  });
64
89
 
65
90
  it("generates a C# Program.cs compatible with the current worker packages", () => {
@@ -87,6 +112,13 @@ module.exports = nextConfig;
87
112
  expect(source).toContain('<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />');
88
113
  });
89
114
 
115
+ it("uses a current Cosmos SDK with binary query response fixes", () => {
116
+ const source = buildCSharpFunctionsProjectSource();
117
+
118
+ expect(source).toContain('<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.63.2" />');
119
+ expect(source).not.toContain('Version="3.47.0"');
120
+ });
121
+
90
122
  it("builds swallowkit.config.js without a local swallowkit package type import", () => {
91
123
  const source = buildSwallowKitConfigSource("typescript");
92
124
 
@@ -133,9 +165,43 @@ module.exports = nextConfig;
133
165
 
134
166
  expect(devDependencies).toEqual({
135
167
  swallowkit: expect.stringMatching(/.+/),
168
+ "@tailwindcss/postcss": "^4.0.0",
136
169
  });
137
170
  });
138
171
 
172
+ it("does not add the Next.js PostCSS plugin to React projects", () => {
173
+ expect(buildGeneratedProjectDevDependencies("react")).not.toHaveProperty("@tailwindcss/postcss");
174
+ });
175
+
176
+ it("configures Tailwind for webpack even when create-next-app only configured Turbopack", () => {
177
+ const project = fs.mkdtempSync(path.join(process.cwd(), ".tmp-postcss-"));
178
+ try {
179
+ ensureNextTailwindPostcssConfig(project);
180
+ const config = path.join(project, "postcss.config.mjs");
181
+ expect(fs.readFileSync(config, "utf8")).toContain('"@tailwindcss/postcss": {}');
182
+ fs.writeFileSync(config, "export default { plugins: { custom: {} } };");
183
+ ensureNextTailwindPostcssConfig(project);
184
+ expect(fs.readFileSync(config, "utf8")).toBe("export default { plugins: { custom: {} } };");
185
+ } finally {
186
+ fs.rmSync(project, { recursive: true, force: true });
187
+ }
188
+ });
189
+
190
+ it.each(["postcss.config.js", "postcss.config.cjs", "postcss.config.json"])(
191
+ "preserves an existing %s",
192
+ file => {
193
+ const project = fs.mkdtempSync(path.join(process.cwd(), ".tmp-postcss-"));
194
+ try {
195
+ fs.writeFileSync(path.join(project, file), "{}");
196
+ ensureNextTailwindPostcssConfig(project);
197
+ expect(fs.existsSync(path.join(project, "postcss.config.mjs"))).toBe(false);
198
+ expect(fs.readFileSync(path.join(project, file), "utf8")).toBe("{}");
199
+ } finally {
200
+ fs.rmSync(project, { recursive: true, force: true });
201
+ }
202
+ }
203
+ );
204
+
139
205
  it("builds a project-scoped MCP config that resolves the configured swallowkit-mcp version", () => {
140
206
  const source = buildSwallowKitMcpProjectConfigSource();
141
207
  const parsed = JSON.parse(source) as {
@@ -310,9 +376,81 @@ describe("Infrastructure generation", () => {
310
376
  });
311
377
  },
312
378
  );
379
+
380
+ it("derives globally unique Cosmos DB and Function App names from the resource group", async () => {
381
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-infra-names-"));
382
+ try {
383
+ await createInfrastructure(projectDir, "sample-app", {
384
+ cosmosDbMode: "freetier", vnetOption: "none", swaPlan: "free",
385
+ }, "typescript");
386
+ const mainBicep = fs.readFileSync(path.join(projectDir, "infra", "main.bicep"), "utf-8");
387
+
388
+ expect(mainBicep).toContain("var resourceSuffix = take(uniqueString(resourceGroup().id, projectName), 6)");
389
+ expect(mainBicep).toContain("var cosmosAccountName = 'cosmos-${take(toLower(projectName), 30)}-${resourceSuffix}'");
390
+ expect(mainBicep).toContain("var functionsAppName = 'func-${take(projectName, 40)}-${resourceSuffix}'");
391
+ expect(mainBicep.match(/accountName: cosmosAccountName/g)).toHaveLength(2);
392
+ expect(mainBicep).toContain("name: functionsAppName");
393
+ expect(mainBicep).not.toContain("'cosmos-${projectName}'");
394
+ expect(mainBicep).not.toContain("'func-${projectName}'");
395
+ // Azure limits: Cosmos DB account <= 44 chars, Function App <= 60 chars.
396
+ expect("cosmos-".length + 30 + 1 + 6).toBeLessThanOrEqual(44);
397
+ expect("func-".length + 40 + 1 + 6).toBeLessThanOrEqual(60);
398
+
399
+ const migration = generateModelContainerMigration(
400
+ { name: "Todo", partitionKey: "/id" },
401
+ new FileOperationSession("commit", projectDir)
402
+ );
403
+ const migrationBicep = fs.readFileSync(path.join(projectDir, migration!.template), "utf-8");
404
+ expect(migrationBicep).toContain(
405
+ "name: 'cosmos-${take(toLower(projectName), 30)}-${take(uniqueString(resourceGroup().id, projectName), 6)}'"
406
+ );
407
+ } finally {
408
+ fs.rmSync(projectDir, { recursive: true, force: true });
409
+ }
410
+ });
411
+
412
+ it("keeps the original Cosmos DB account name in model migrations for projects without suffixed names", async () => {
413
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-infra-legacy-names-"));
414
+ try {
415
+ await createInfrastructure(projectDir, "sample-app", {
416
+ cosmosDbMode: "freetier", vnetOption: "none", swaPlan: "free",
417
+ }, "typescript");
418
+ const mainPath = path.join(projectDir, "infra", "main.bicep");
419
+ const legacyMain = fs.readFileSync(mainPath, "utf-8")
420
+ .replace(/^var (resourceSuffix|cosmosAccountName|functionsAppName) = .*\r?\n/gm, "")
421
+ .replace(/accountName: cosmosAccountName/g, "accountName: 'cosmos-${projectName}'")
422
+ .replace("name: functionsAppName", "name: 'func-${projectName}'");
423
+ fs.writeFileSync(mainPath, legacyMain);
424
+ initializeLegacyMigrations(projectDir);
425
+
426
+ const migration = generateModelContainerMigration(
427
+ { name: "Todo", partitionKey: "/id" },
428
+ new FileOperationSession("commit", projectDir)
429
+ );
430
+ const migrationBicep = fs.readFileSync(path.join(projectDir, migration!.template), "utf-8");
431
+ expect(migrationBicep).toContain("name: 'cosmos-${projectName}'");
432
+ } finally {
433
+ fs.rmSync(projectDir, { recursive: true, force: true });
434
+ }
435
+ });
313
436
  });
314
437
 
315
438
  describe("GitHub Actions workflow generation", () => {
439
+ it("keeps Python deployment and both CI providers on the same GA runtime", async () => {
440
+ expect(getGitHubFunctionsWorkflow("npm", "python")).toContain("python-version: '3.14'");
441
+ expect(getAzureFunctionsPipeline("npm", "python")).toContain("versionSpec: '3.14'");
442
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-infra-"));
443
+ try {
444
+ await createInfrastructure(projectDir, "sample-app", {
445
+ cosmosDbMode: "serverless", vnetOption: "none", swaPlan: "free",
446
+ }, "python");
447
+ const source = fs.readFileSync(path.join(projectDir, "infra", "main.bicep"), "utf8");
448
+ expect(source).toContain("functionsRuntimeVersion: '3.14'");
449
+ } finally {
450
+ fs.rmSync(projectDir, { recursive: true, force: true });
451
+ }
452
+ });
453
+
316
454
  it("only enables setup-node caching when no package-manager conversion is needed", () => {
317
455
  expect(buildGitHubSwaWorkflow("npm")).toContain("Select Node.js 22 for SWA");
318
456
  expect(buildGitHubSwaWorkflow("npm")).toContain("app_build_command: 'npm run build'");
@@ -411,11 +549,13 @@ describe("SWA npm compatibility with pnpm projects", () => {
411
549
  scripts: { prestart: "pnpm run build" },
412
550
  });
413
551
  const env = { ...process.env, npm_config_cache: path.join(dir, "cache") };
552
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
553
+ const npmOptions = { cwd: dir, env, encoding: "utf8" as const, shell: process.platform === "win32" };
414
554
  // setup-node's npm cache lookup is an npm command too, before any install.
415
- const cacheBefore = spawnSync("npm", ["config", "get", "cache"], { cwd: dir, env, encoding: "utf8" });
555
+ const cacheBefore = spawnSync(npmCommand, ["config", "get", "cache"], npmOptions);
416
556
  expect(cacheBefore.status).not.toBe(0);
417
557
  expect(cacheBefore.stderr).toContain("EBADDEVENGINES");
418
- const before = spawnSync("npm", ["run", "probe"], { cwd: dir, env, encoding: "utf8" });
558
+ const before = spawnSync(npmCommand, ["run", "probe"], npmOptions);
419
559
  expect(before.status).not.toBe(0);
420
560
  expect(before.stderr).toContain("EBADDEVENGINES");
421
561
 
@@ -430,7 +570,7 @@ describe("SWA npm compatibility with pnpm projects", () => {
430
570
  }
431
571
  const runtimeCommand = workflow.match(/node -e "const fs=require\('fs'\);const p=JSON.parse[^\n]+/);
432
572
  expect(runtimeCommand).not.toBeNull();
433
- execFileSync("bash", ["-c", runtimeCommand![0]], { cwd: dir });
573
+ execFileSync(process.execPath, ["-e", runtimeCommand![0].slice('node -e "'.length, -1)], { cwd: dir });
434
574
  const match = workflow.match(/node <<'NODE'\n([\s\S]*?)\n\s+NODE/);
435
575
  expect(match).not.toBeNull();
436
576
  execFileSync(process.execPath, ["-e", match![1]], { cwd: dir });
@@ -441,9 +581,9 @@ describe("SWA npm compatibility with pnpm projects", () => {
441
581
  expect(normalized.devEngines.runtime.version).toBe(">=22.14.0");
442
582
  expect(normalized.workspaces).toEqual(hasFunctions ? ["shared", "functions"] : ["shared"]);
443
583
  expect(normalized.dependencies["@sample/shared"]).toBe("file:shared");
444
- const cacheAfter = execFileSync("npm", ["config", "get", "cache"], { cwd: dir, env, encoding: "utf8" });
584
+ const cacheAfter = execFileSync(npmCommand, ["config", "get", "cache"], npmOptions);
445
585
  expect(cacheAfter.trim()).toBe(env.npm_config_cache);
446
- const after = execFileSync("npm", ["run", "probe"], { cwd: dir, env, encoding: "utf8" });
586
+ const after = execFileSync(npmCommand, ["run", "probe"], npmOptions);
447
587
  expect(after).toContain("npm/");
448
588
  } finally {
449
589
  fs.rmSync(dir, { recursive: true, force: true });
@@ -0,0 +1,262 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import { compareSemver, isPrerelease, parseSemver } from "../utils/semver";
6
+ import {
7
+ MCP_DELEGATED_FROM_ENV,
8
+ buildPinnedUpdateNotice,
9
+ decideMcpLaunch,
10
+ resolveProjectSwallowKit,
11
+ } from "../mcp/version-selection";
12
+ import { readMcpConfigVersion, resolveMcpPinVersion } from "../core/project/mcp-config";
13
+ import { configureMcp } from "../cli/commands/configure-mcp";
14
+ import { buildSwallowKitMcpProjectConfigSource, selectInitialMcpVersion } from "../cli/commands/init";
15
+ import { getSwallowKitVersion } from "../version";
16
+
17
+ const repositoryRoot = path.resolve(__dirname, "..", "..");
18
+ const builtMcpEntrypoint = path.join(repositoryRoot, "dist", "mcp", "index.js");
19
+ const itWhenBuilt = fs.existsSync(builtMcpEntrypoint) ? it : it.skip;
20
+
21
+ function createProject(version: string | null, options: { mcpScript?: string } = {}): string {
22
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-mcp-version-"));
23
+ fs.writeFileSync(path.join(project, "package.json"), JSON.stringify({ name: "sample", devEngines: { packageManager: { name: "pnpm" } } }));
24
+ fs.writeFileSync(path.join(project, ".mcp.json"), buildSwallowKitMcpProjectConfigSource("pnpm"));
25
+ if (version) {
26
+ const root = path.join(project, "node_modules", "swallowkit");
27
+ fs.mkdirSync(path.join(root, "dist", "mcp"), { recursive: true });
28
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({
29
+ name: "swallowkit",
30
+ version,
31
+ bin: { swallowkit: "dist/cli/index.js", "swallowkit-mcp": "dist/mcp/index.js" },
32
+ }));
33
+ fs.writeFileSync(path.join(root, "dist", "mcp", "index.js"), options.mcpScript ?? "");
34
+ }
35
+ return project;
36
+ }
37
+
38
+ describe("semantic version precedence", () => {
39
+ it.each([
40
+ ["2.0.0-alpha.3", "1.1.2", 1],
41
+ ["2.0.0-alpha.3", "2.0.0", -1],
42
+ ["2.0.0", "2.0.0-alpha.3", 1],
43
+ ["2.0.0-alpha.3", "2.0.0-alpha.3", 0],
44
+ ["2.0.0-alpha.3", "2.0.0-alpha.10", -1],
45
+ ["2.0.0-alpha.3", "2.0.0-beta.1", -1],
46
+ ["2.0.0-beta.1", "2.0.0-rc.1", -1],
47
+ ["2.0.0-alpha", "2.0.0-alpha.1", -1],
48
+ ["2.0.0-1", "2.0.0-alpha", -1],
49
+ ["1.10.0", "1.9.9", 1],
50
+ ["1.0.0+build.1", "1.0.0", 0],
51
+ ])("compares %s with %s", (left, right, expected) => {
52
+ expect(compareSemver(left, right)).toBe(expected);
53
+ expect(compareSemver(right, left)).toBe(-expected || 0);
54
+ });
55
+
56
+ it("rejects values that are not semantic versions", () => {
57
+ expect(parseSemver("latest")).toBeNull();
58
+ expect(parseSemver("1.2")).toBeNull();
59
+ expect(parseSemver("01.2.3")).toBeNull();
60
+ expect(compareSemver("latest", "1.0.0")).toBeNull();
61
+ expect(isPrerelease("2.0.0-alpha.3")).toBe(true);
62
+ expect(isPrerelease("2.0.0")).toBe(false);
63
+ });
64
+ });
65
+
66
+ describe("MCP launch version selection", () => {
67
+ const selfPackageRoot = path.join(os.tmpdir(), "published-swallowkit");
68
+ const project = (version: string, entrypoint: string | null = "/project/node_modules/swallowkit/dist/mcp/index.js") => ({
69
+ version,
70
+ packageRoot: "/project/node_modules/swallowkit",
71
+ mcpEntrypoint: entrypoint,
72
+ });
73
+
74
+ it("delegates when the project install is newer than the latest launcher", () => {
75
+ const decision = decideMcpLaunch({
76
+ selfVersion: "1.1.2",
77
+ selfPackageRoot,
78
+ env: { SWALLOWKIT_MCP_VERSION: "latest" },
79
+ project: project("2.0.0-alpha.3"),
80
+ });
81
+ expect(decision).toEqual({
82
+ action: "delegate",
83
+ entrypoint: "/project/node_modules/swallowkit/dist/mcp/index.js",
84
+ fromVersion: "1.1.2",
85
+ toVersion: "2.0.0-alpha.3",
86
+ });
87
+ });
88
+
89
+ it("treats an unset version like latest", () => {
90
+ expect(decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: {}, project: project("2.0.0-alpha.3") }).action)
91
+ .toBe("delegate");
92
+ });
93
+
94
+ it.each([
95
+ ["latest caught up with a stable release", "2.0.0", "2.0.0-alpha.3"],
96
+ ["same version", "2.0.0-alpha.3", "2.0.0-alpha.3"],
97
+ ["latest is newer", "2.1.0", "2.0.0"],
98
+ ])("keeps the launcher when %s", (_label, selfVersion, projectVersion) => {
99
+ const decision = decideMcpLaunch({ selfVersion, selfPackageRoot, env: { SWALLOWKIT_MCP_VERSION: "latest" }, project: project(projectVersion) });
100
+ expect(decision.action).toBe("serve");
101
+ if (decision.action !== "serve") return;
102
+ expect(decision.context).toMatchObject({ mcpVersion: selfVersion, projectVersion, selection: "running-version", delegated: false, warnings: [] });
103
+ });
104
+
105
+ it("serves the launcher when SwallowKit is not installed in the project", () => {
106
+ const decision = decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: { SWALLOWKIT_MCP_VERSION: "latest" }, project: null });
107
+ expect(decision).toMatchObject({ action: "serve", context: { projectVersion: null, warnings: [], selection: "running-version" } });
108
+ });
109
+
110
+ it("honors an explicit pin and warns, without delegating, when the pin is older than the project", () => {
111
+ const decision = decideMcpLaunch({
112
+ selfVersion: "2.0.0-alpha.3",
113
+ selfPackageRoot,
114
+ env: { SWALLOWKIT_MCP_VERSION: "2.0.0-alpha.3" },
115
+ project: project("2.0.0-alpha.4"),
116
+ execCommand: "pnpm exec",
117
+ });
118
+ expect(decision.action).toBe("serve");
119
+ if (decision.action !== "serve") return;
120
+ expect(decision.context.selection).toBe("pinned");
121
+ expect(decision.context.warnings[0]).toMatch(/^mcp-older-than-project: swallowkit-mcp 2\.0\.0-alpha\.3 is older than the project's swallowkit 2\.0\.0-alpha\.4/);
122
+ expect(decision.context.nextActions.map((action) => action.command)).toEqual([
123
+ "pnpm exec swallowkit machine inspect capabilities",
124
+ "pnpm exec swallowkit configure-mcp",
125
+ ]);
126
+ });
127
+
128
+ it("does not delegate again inside a delegated process", () => {
129
+ const decision = decideMcpLaunch({
130
+ selfVersion: "2.0.0-alpha.3",
131
+ selfPackageRoot,
132
+ env: { SWALLOWKIT_MCP_VERSION: "latest", [MCP_DELEGATED_FROM_ENV]: "1.1.2" },
133
+ project: project("2.0.0-alpha.3"),
134
+ });
135
+ expect(decision).toMatchObject({
136
+ action: "serve",
137
+ context: { selection: "project-version", delegated: true, delegatedFrom: "1.1.2", mcpVersion: "2.0.0-alpha.3" },
138
+ });
139
+ });
140
+
141
+ it("warns instead of delegating when the newer project install has no MCP entrypoint", () => {
142
+ const decision = decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: {}, project: project("2.0.0-alpha.3", null) });
143
+ expect(decision.action).toBe("serve");
144
+ if (decision.action === "serve") expect(decision.context.warnings).toHaveLength(1);
145
+ });
146
+
147
+ it("reports a pinned launcher once published latest catches up", () => {
148
+ const context = {
149
+ mcpVersion: "2.0.0-alpha.3", requestedVersion: "2.0.0-alpha.3", projectVersion: "2.0.0-alpha.3",
150
+ selection: "pinned" as const, delegated: false, warnings: [], nextActions: [],
151
+ };
152
+ expect(buildPinnedUpdateNotice(context, "1.1.2")).toBeNull();
153
+ expect(buildPinnedUpdateNotice(context, null)).toBeNull();
154
+ const notice = buildPinnedUpdateNotice(context, "2.0.0", "pnpm exec");
155
+ expect(notice?.warning).toMatch(/pinned to 2\.0\.0-alpha\.3, but published latest is 2\.0\.0/);
156
+ expect(notice?.nextAction.command).toBe("pnpm exec swallowkit configure-mcp");
157
+ expect(buildPinnedUpdateNotice({ ...context, selection: "running-version" }, "2.0.0")).toBeNull();
158
+ });
159
+
160
+ it("finds the project-installed package and its MCP entrypoint", () => {
161
+ const directory = createProject("2.0.0-alpha.3");
162
+ const empty = createProject(null);
163
+ try {
164
+ const install = resolveProjectSwallowKit(directory);
165
+ expect(install?.version).toBe("2.0.0-alpha.3");
166
+ expect(install?.mcpEntrypoint).toBe(path.join(fs.realpathSync(directory), "node_modules", "swallowkit", "dist", "mcp", "index.js"));
167
+ expect(resolveProjectSwallowKit(empty)).toBeNull();
168
+ } finally {
169
+ fs.rmSync(directory, { recursive: true, force: true });
170
+ fs.rmSync(empty, { recursive: true, force: true });
171
+ }
172
+ });
173
+ });
174
+
175
+ describe("MCP launcher version in .mcp.json", () => {
176
+ it.each([
177
+ ["2.0.0-alpha.3", "1.1.2", "2.0.0-alpha.3"],
178
+ ["2.0.0-alpha.3", "2.0.0", "latest"],
179
+ ["2.0.0-alpha.3", "2.0.0-alpha.3", "latest"],
180
+ ["1.1.2", "1.1.2", "latest"],
181
+ ["1.1.1", "1.1.2", "latest"],
182
+ ["2.0.0-alpha.3", null, "2.0.0-alpha.3"],
183
+ ["1.1.2", null, "latest"],
184
+ ])("project %s with published latest %s uses %s", (projectVersion, latest, expected) => {
185
+ expect(resolveMcpPinVersion(projectVersion, latest).version).toBe(expected);
186
+ });
187
+
188
+ it("selects the init value from this CLI version and the registry", () => {
189
+ const current = getSwallowKitVersion();
190
+ expect(selectInitialMcpVersion(() => current).version).toBe("latest");
191
+ expect(selectInitialMcpVersion(() => "0.0.1").version).toBe(current);
192
+ });
193
+
194
+ it("re-selects and preserves the rest of .mcp.json with configure-mcp", () => {
195
+ const directory = createProject("2.0.0-alpha.3");
196
+ try {
197
+ expect(configureMcp({ cwd: directory }, () => "1.1.2")).toMatchObject({ previousVersion: "latest", version: "2.0.0-alpha.3" });
198
+ expect(readMcpConfigVersion(directory)).toBe("2.0.0-alpha.3");
199
+ expect(configureMcp({ cwd: directory }, () => "2.0.0")).toMatchObject({ previousVersion: "2.0.0-alpha.3", version: "latest" });
200
+ expect(configureMcp({ cwd: directory, version: "2.0.0-alpha.3" }).version).toBe("2.0.0-alpha.3");
201
+ expect(configureMcp({ cwd: directory, version: "latest" }).version).toBe("latest");
202
+ expect(() => configureMcp({ cwd: directory, version: "next" })).toThrow(/auto/);
203
+ const config = JSON.parse(fs.readFileSync(path.join(directory, ".mcp.json"), "utf8"));
204
+ expect(config.mcpServers.swallowkit.args).toEqual(["dlx", "--package", "swallowkit@${SWALLOWKIT_MCP_VERSION}", "swallowkit-mcp"]);
205
+ } finally {
206
+ fs.rmSync(directory, { recursive: true, force: true });
207
+ }
208
+ });
209
+ });
210
+
211
+ describe("built MCP entrypoint version delegation", () => {
212
+ const runEntrypoint = (cwd: string, env: NodeJS.ProcessEnv, until = /version: .*selection/) =>
213
+ new Promise<{ stderr: string; code: number | null }>((resolve, reject) => {
214
+ const child = spawn(process.execPath, [builtMcpEntrypoint], {
215
+ cwd,
216
+ env: { ...process.env, SWALLOWKIT_MCP_UPDATE_CHECK: "0", ...env },
217
+ stdio: ["pipe", "pipe", "pipe"],
218
+ });
219
+ let stderr = "";
220
+ const timer = setTimeout(() => child.kill(), 10000);
221
+ child.stderr.on("data", (chunk) => {
222
+ stderr += chunk;
223
+ if (until.test(stderr)) child.kill();
224
+ });
225
+ child.on("error", reject);
226
+ child.on("close", (code) => {
227
+ clearTimeout(timer);
228
+ resolve({ stderr, code });
229
+ });
230
+ });
231
+
232
+ itWhenBuilt("hands the session to a newer project install", async () => {
233
+ const directory = createProject("999.0.0-alpha.1", {
234
+ mcpScript: `process.stderr.write("project-mcp delegatedFrom=" + process.env.${MCP_DELEGATED_FROM_ENV} + "\\n");`,
235
+ });
236
+ try {
237
+ const result = await runEntrypoint(directory, { SWALLOWKIT_MCP_VERSION: "latest" });
238
+ expect(result.stderr).toContain("delegating to the project-installed swallowkit-mcp 999.0.0-alpha.1");
239
+ expect(result.stderr).toContain(`project-mcp delegatedFrom=${getSwallowKitVersion()}`);
240
+ expect(result.code).toBe(0);
241
+ } finally {
242
+ fs.rmSync(directory, { recursive: true, force: true });
243
+ }
244
+ }, 20000);
245
+
246
+ itWhenBuilt("keeps serving when the project install is older or the version is pinned", async () => {
247
+ const older = createProject("0.0.1", { mcpScript: "process.stderr.write('should-not-run');" });
248
+ const newer = createProject("999.0.0", { mcpScript: "process.stderr.write('should-not-run');" });
249
+ try {
250
+ const fromOlder = await runEntrypoint(older, { SWALLOWKIT_MCP_VERSION: "latest" });
251
+ expect(fromOlder.stderr).toContain(`version: ${getSwallowKitVersion()} (selection: running-version)`);
252
+ expect(fromOlder.stderr).not.toContain("should-not-run");
253
+ const pinned = await runEntrypoint(newer, { SWALLOWKIT_MCP_VERSION: getSwallowKitVersion() }, /mcp-older-than-project/);
254
+ expect(pinned.stderr).toContain("(selection: pinned)");
255
+ expect(pinned.stderr).toContain("mcp-older-than-project");
256
+ expect(pinned.stderr).not.toContain("should-not-run");
257
+ } finally {
258
+ fs.rmSync(older, { recursive: true, force: true });
259
+ fs.rmSync(newer, { recursive: true, force: true });
260
+ }
261
+ }, 30000);
262
+ });