swallowkit 2.0.0-alpha.1 → 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 (224) hide show
  1. package/README.ja.md +14 -0
  2. package/README.md +14 -0
  3. package/dist/cli/commands/add-auth.js +37 -6
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  6. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  7. package/dist/cli/commands/configure-mcp.js +42 -0
  8. package/dist/cli/commands/configure-mcp.js.map +1 -0
  9. package/dist/cli/commands/dev.d.ts +2 -2
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +39 -27
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +8 -2
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +67 -132
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/migrations.d.ts.map +1 -1
  18. package/dist/cli/commands/migrations.js +25 -0
  19. package/dist/cli/commands/migrations.js.map +1 -1
  20. package/dist/cli/commands/provision.d.ts +5 -0
  21. package/dist/cli/commands/provision.d.ts.map +1 -1
  22. package/dist/cli/commands/provision.js +278 -55
  23. package/dist/cli/commands/provision.js.map +1 -1
  24. package/dist/cli/commands/scaffold.d.ts +4 -0
  25. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  26. package/dist/cli/commands/scaffold.js +85 -3
  27. package/dist/cli/commands/scaffold.js.map +1 -1
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +43 -5
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/core/operations/create-model.d.ts +2 -0
  33. package/dist/core/operations/create-model.d.ts.map +1 -1
  34. package/dist/core/operations/create-model.js +60 -8
  35. package/dist/core/operations/create-model.js.map +1 -1
  36. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  37. package/dist/core/operations/migration-generator.js +12 -1
  38. package/dist/core/operations/migration-generator.js.map +1 -1
  39. package/dist/core/operations/model-migration.d.ts +22 -0
  40. package/dist/core/operations/model-migration.d.ts.map +1 -0
  41. package/dist/core/operations/model-migration.js +36 -0
  42. package/dist/core/operations/model-migration.js.map +1 -0
  43. package/dist/core/operations/provision-deployment.d.ts +52 -0
  44. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  45. package/dist/core/operations/provision-deployment.js +164 -0
  46. package/dist/core/operations/provision-deployment.js.map +1 -0
  47. package/dist/core/operations/provision-operations.d.ts +62 -6
  48. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  49. package/dist/core/operations/provision-operations.js +653 -115
  50. package/dist/core/operations/provision-operations.js.map +1 -1
  51. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  52. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  53. package/dist/core/operations/scaffold-apply.js +1 -0
  54. package/dist/core/operations/scaffold-apply.js.map +1 -1
  55. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  56. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  57. package/dist/core/operations/scaffold-machine.js.map +1 -1
  58. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  59. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  60. package/dist/core/operations/scaffold-plan.js +3 -1
  61. package/dist/core/operations/scaffold-plan.js.map +1 -1
  62. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  63. package/dist/core/project/agent-instructions.js +4 -1
  64. package/dist/core/project/agent-instructions.js.map +1 -1
  65. package/dist/core/project/boundaries.d.ts +5 -2
  66. package/dist/core/project/boundaries.d.ts.map +1 -1
  67. package/dist/core/project/boundaries.js +51 -20
  68. package/dist/core/project/boundaries.js.map +1 -1
  69. package/dist/core/project/capabilities.d.ts +10 -0
  70. package/dist/core/project/capabilities.d.ts.map +1 -1
  71. package/dist/core/project/capabilities.js +38 -6
  72. package/dist/core/project/capabilities.js.map +1 -1
  73. package/dist/core/project/home-page.d.ts +4 -0
  74. package/dist/core/project/home-page.d.ts.map +1 -0
  75. package/dist/core/project/home-page.js +113 -0
  76. package/dist/core/project/home-page.js.map +1 -0
  77. package/dist/core/project/mcp-config.d.ts +26 -0
  78. package/dist/core/project/mcp-config.d.ts.map +1 -0
  79. package/dist/core/project/mcp-config.js +117 -0
  80. package/dist/core/project/mcp-config.js.map +1 -0
  81. package/dist/core/project/react-project.d.ts +1 -1
  82. package/dist/core/project/react-project.d.ts.map +1 -1
  83. package/dist/core/project/react-project.js +46 -25
  84. package/dist/core/project/react-project.js.map +1 -1
  85. package/dist/core/project/state.d.ts +7 -0
  86. package/dist/core/project/state.d.ts.map +1 -1
  87. package/dist/core/project/state.js +86 -0
  88. package/dist/core/project/state.js.map +1 -1
  89. package/dist/core/project/workflows.d.ts.map +1 -1
  90. package/dist/core/project/workflows.js +53 -7
  91. package/dist/core/project/workflows.js.map +1 -1
  92. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  93. package/dist/core/scaffold/auth-generator.js +26 -27
  94. package/dist/core/scaffold/auth-generator.js.map +1 -1
  95. package/dist/core/scaffold/model-parser.d.ts +23 -0
  96. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  97. package/dist/core/scaffold/model-parser.js +407 -137
  98. package/dist/core/scaffold/model-parser.js.map +1 -1
  99. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  100. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  101. package/dist/core/scaffold/native-schema-generator.js +294 -91
  102. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  103. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  104. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  105. package/dist/core/scaffold/openapi-generator.js +53 -3
  106. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  107. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  108. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  109. package/dist/core/scaffold/ui-generator.js +68 -7
  110. package/dist/core/scaffold/ui-generator.js.map +1 -1
  111. package/dist/core/verify/index.d.ts +4 -3
  112. package/dist/core/verify/index.d.ts.map +1 -1
  113. package/dist/core/verify/index.js +122 -5
  114. package/dist/core/verify/index.js.map +1 -1
  115. package/dist/machine/contracts.d.ts +1 -0
  116. package/dist/machine/contracts.d.ts.map +1 -1
  117. package/dist/machine/errors.d.ts +3 -2
  118. package/dist/machine/errors.d.ts.map +1 -1
  119. package/dist/machine/errors.js +2 -1
  120. package/dist/machine/errors.js.map +1 -1
  121. package/dist/machine/index.d.ts.map +1 -1
  122. package/dist/machine/index.js +59 -28
  123. package/dist/machine/index.js.map +1 -1
  124. package/dist/mcp/index.d.ts +6 -4
  125. package/dist/mcp/index.d.ts.map +1 -1
  126. package/dist/mcp/index.js +210 -25
  127. package/dist/mcp/index.js.map +1 -1
  128. package/dist/mcp/version-selection.d.ts +55 -0
  129. package/dist/mcp/version-selection.d.ts.map +1 -0
  130. package/dist/mcp/version-selection.js +152 -0
  131. package/dist/mcp/version-selection.js.map +1 -0
  132. package/dist/utils/azure-cli.d.ts +56 -0
  133. package/dist/utils/azure-cli.d.ts.map +1 -0
  134. package/dist/utils/azure-cli.js +326 -0
  135. package/dist/utils/azure-cli.js.map +1 -0
  136. package/dist/utils/azure-diagnostics.d.ts +16 -0
  137. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  138. package/dist/utils/azure-diagnostics.js +68 -0
  139. package/dist/utils/azure-diagnostics.js.map +1 -0
  140. package/dist/utils/package-manager.js +2 -2
  141. package/dist/utils/package-manager.js.map +1 -1
  142. package/dist/utils/python-uv.d.ts +5 -7
  143. package/dist/utils/python-uv.d.ts.map +1 -1
  144. package/dist/utils/python-uv.js +50 -24
  145. package/dist/utils/python-uv.js.map +1 -1
  146. package/dist/utils/react-dev.d.ts +4 -0
  147. package/dist/utils/react-dev.d.ts.map +1 -0
  148. package/dist/utils/react-dev.js +35 -0
  149. package/dist/utils/react-dev.js.map +1 -0
  150. package/dist/utils/semver.d.ts +14 -0
  151. package/dist/utils/semver.d.ts.map +1 -0
  152. package/dist/utils/semver.js +61 -0
  153. package/dist/utils/semver.js.map +1 -0
  154. package/package.json +6 -3
  155. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  156. package/src/__tests__/agent-instructions.test.ts +5 -0
  157. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  158. package/src/__tests__/auth.test.ts +9 -0
  159. package/src/__tests__/azure-cli.test.ts +202 -0
  160. package/src/__tests__/boundaries.test.ts +42 -0
  161. package/src/__tests__/csharp-schema.test.ts +261 -0
  162. package/src/__tests__/dev.test.ts +13 -6
  163. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  164. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  165. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  166. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  167. package/src/__tests__/home-page.test.ts +50 -0
  168. package/src/__tests__/init.test.ts +116 -0
  169. package/src/__tests__/mcp-version.test.ts +262 -0
  170. package/src/__tests__/mcp.test.ts +120 -3
  171. package/src/__tests__/multi-auth.test.ts +115 -4
  172. package/src/__tests__/package-manager.test.ts +17 -2
  173. package/src/__tests__/plan-apply.test.ts +179 -0
  174. package/src/__tests__/provision-runtime.test.ts +722 -0
  175. package/src/__tests__/python-uv.test.ts +50 -14
  176. package/src/__tests__/python-venv.test.ts +48 -0
  177. package/src/__tests__/react-dev.test.ts +72 -0
  178. package/src/__tests__/react.test.ts +19 -0
  179. package/src/__tests__/schema-analysis.test.ts +329 -0
  180. package/src/cli/commands/add-auth.ts +36 -6
  181. package/src/cli/commands/configure-mcp.ts +54 -0
  182. package/src/cli/commands/dev.ts +40 -31
  183. package/src/cli/commands/init.ts +73 -138
  184. package/src/cli/commands/migrations.ts +22 -0
  185. package/src/cli/commands/provision.ts +294 -56
  186. package/src/cli/commands/scaffold.ts +105 -3
  187. package/src/cli/index.ts +40 -7
  188. package/src/core/operations/create-model.ts +51 -13
  189. package/src/core/operations/migration-generator.ts +14 -1
  190. package/src/core/operations/model-migration.ts +52 -0
  191. package/src/core/operations/provision-deployment.ts +183 -0
  192. package/src/core/operations/provision-operations.ts +662 -154
  193. package/src/core/operations/scaffold-apply.ts +2 -0
  194. package/src/core/operations/scaffold-machine.ts +1 -0
  195. package/src/core/operations/scaffold-plan.ts +5 -1
  196. package/src/core/project/agent-instructions.ts +4 -1
  197. package/src/core/project/agent-templates/AGENTS.md +89 -6
  198. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  199. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  200. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  201. package/src/core/project/boundaries.ts +62 -20
  202. package/src/core/project/capabilities.ts +49 -6
  203. package/src/core/project/home-page.ts +114 -0
  204. package/src/core/project/mcp-config.ts +84 -0
  205. package/src/core/project/react-project.ts +50 -28
  206. package/src/core/project/state.ts +70 -0
  207. package/src/core/project/workflows.ts +53 -7
  208. package/src/core/scaffold/auth-generator.ts +26 -27
  209. package/src/core/scaffold/model-parser.ts +413 -129
  210. package/src/core/scaffold/native-schema-generator.ts +319 -103
  211. package/src/core/scaffold/openapi-generator.ts +63 -5
  212. package/src/core/scaffold/ui-generator.ts +70 -8
  213. package/src/core/verify/index.ts +118 -6
  214. package/src/machine/contracts.ts +1 -0
  215. package/src/machine/errors.ts +4 -2
  216. package/src/machine/index.ts +65 -34
  217. package/src/mcp/index.ts +221 -31
  218. package/src/mcp/version-selection.ts +163 -0
  219. package/src/utils/azure-cli.ts +315 -0
  220. package/src/utils/azure-diagnostics.ts +75 -0
  221. package/src/utils/package-manager.ts +2 -2
  222. package/src/utils/python-uv.ts +53 -33
  223. package/src/utils/react-dev.ts +31 -0
  224. package/src/utils/semver.ts +59 -0
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
+ import { spawnSync } from "child_process";
4
5
  import { addAuthCommand } from "../cli/commands/add-auth";
5
6
  import { createUIAuthOptions } from "../cli/commands/scaffold";
6
7
  import {
@@ -9,12 +10,12 @@ import {
9
10
  generateNamedAuthRouterPython,
10
11
  generateNamedAuthRouterTS,
11
12
  } from "../core/scaffold/auth-generator";
12
- import { normalizeAuthConfig } from "../core/config";
13
+ import { normalizeAuthConfig, validateConfig } from "../core/config";
13
14
  import { parseAuthPolicy } from "../core/scaffold/model-parser";
14
15
  import { createBasicModelInfo } from "./fixtures";
15
16
  import { generateCSharpAzureFunctionsCRUD, generateCompactAzureFunctionsCRUD, generatePythonAzureFunctionsCRUD } from "../core/scaffold/functions-generator";
16
17
  import { generateAuthLayout, generateNewPage } from "../core/scaffold/ui-generator";
17
- import { AuthConfig } from "../types";
18
+ import { AuthConfig, SwallowKitConfig } from "../types";
18
19
 
19
20
  const namedAuthConfig: AuthConfig = {
20
21
  schemes: {
@@ -57,10 +58,24 @@ describe("named authentication routers", () => {
57
58
  it("generates valid branch structure for an SWA-only Python router", () => {
58
59
  const swaOnly = normalizeAuthConfig({ schemes: { admin: { provider: "swa", swa: { allowedProviders: ["github"] } } }, authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } } })!;
59
60
  const python = generateNamedAuthRouterPython(swaOnly);
60
- expect(python).toContain(' if PROVIDERS[scheme] == "swa":');
61
- expect(python).not.toContain(' elif PROVIDERS[scheme] == "swa":');
61
+ expect(python).toContain('\n if PROVIDERS[scheme] == "swa":');
62
+ expect(python).not.toContain('\n elif PROVIDERS[scheme] == "swa":');
62
63
  expect(python).toContain("ALLOWED_PROVIDERS");
63
64
  expect(generateNamedAuthRouterCSharp(swaOnly)).toContain("AllowedProviders[scheme]");
65
+ expect(generateNamedAuthRouterCSharp(swaOnly)).not.toContain("using Functions.Auth.Schemes;");
66
+ });
67
+
68
+ const pythonCommand = ["python3", "python"].find(command => spawnSync(command, ["--version"], { encoding: "utf-8" }).status === 0);
69
+ (pythonCommand ? it : it.skip).each([false, true])("compiles named Python auth router syntax (external schemes: %s)", (external) => {
70
+ const config = external ? auth : normalizeAuthConfig({
71
+ schemes: { admin: { provider: "swa" } },
72
+ authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } },
73
+ })!;
74
+ const result = spawnSync(pythonCommand!, ["-c", "import ast,sys; ast.parse(sys.stdin.read())"], {
75
+ encoding: "utf-8", input: generateNamedAuthRouterPython(config),
76
+ });
77
+ expect(result.stderr).toBe("");
78
+ expect(result.status).toBe(0);
64
79
  });
65
80
 
66
81
  it("emits Array.Empty<string>() instead of untyped new[] { } for empty C# arrays (CS0826)", () => {
@@ -157,4 +172,100 @@ describe("add-auth named scheme CLI", () => {
157
172
  new Function("module", content)(configModule);
158
173
  expect((configModule.exports as { auth: { schemes: { admin: { provider: string } } } }).auth.schemes.admin.provider).toBe("swa");
159
174
  });
175
+
176
+ it("writes named schemes to swallowkit.config.json when both config files exist (loadConfig precedence)", async () => {
177
+ const js = "module.exports = { backend: { language: 'typescript' } };\n";
178
+ fs.writeFileSync(path.join(root, "swallowkit.config.js"), js);
179
+ await addAuthCommand({ scheme: "admin", provider: "swa" });
180
+ expect(JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8")).auth.schemes.admin.provider).toBe("swa");
181
+ expect(fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8")).toBe(js);
182
+ });
183
+
184
+ it.each(["json", "js"] as const)("disables auth in a %s config with an anonymous default that passes validation", async (format) => {
185
+ fs.rmSync(path.join(root, "swallowkit.config.json"));
186
+ if (format === "json") fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" } }));
187
+ else fs.writeFileSync(path.join(root, "swallowkit.config.js"), "module.exports = {\n backend: { language: 'typescript' },\n}\n");
188
+ await addAuthCommand({ provider: "none" });
189
+ let config: SwallowKitConfig;
190
+ if (format === "json") {
191
+ config = JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8"));
192
+ } else {
193
+ const configModule = { exports: {} as SwallowKitConfig };
194
+ new Function("module", fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8"))(configModule);
195
+ config = configModule.exports;
196
+ }
197
+ expect(config.auth).toMatchObject({ provider: "none", authorization: { defaultPolicy: "anonymous" } });
198
+ expect(validateConfig(config)).toEqual({ valid: true, errors: [] });
199
+ });
200
+
201
+ it("records single-provider auth in swallowkit.config.json instead of silently skipping it", async () => {
202
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" } }));
203
+ fs.mkdirSync(path.join(root, "app"), { recursive: true });
204
+ fs.writeFileSync(path.join(root, "app", "layout.tsx"), "export default function L({ children }: any) { return <html><body>{children}</body></html>; }\n");
205
+ await addAuthCommand({ provider: "swa", allowedProviders: ["github"] });
206
+ expect(JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8")).auth).toEqual({
207
+ provider: "swa",
208
+ swa: { allowedProviders: ["github"], roleSource: "swa-roles" },
209
+ authorization: { defaultPolicy: "authenticated" },
210
+ });
211
+ });
212
+
213
+ it.each(["typescript", "csharp", "python"] as const)("declares %s backend dependencies for a named custom-jwt scheme", async (language) => {
214
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language } }));
215
+ fs.mkdirSync(path.join(root, "functions"), { recursive: true });
216
+ if (language === "typescript") fs.writeFileSync(path.join(root, "functions", "package.json"), JSON.stringify({ name: "functions", dependencies: {} }));
217
+ if (language === "csharp") fs.writeFileSync(path.join(root, "functions", "App.Functions.csproj"), "<Project>\n <ItemGroup>\n </ItemGroup>\n</Project>\n");
218
+ if (language === "python") fs.writeFileSync(path.join(root, "functions", "requirements.txt"), "azure-functions\n");
219
+ await addAuthCommand({ scheme: "staff", provider: "custom-jwt" });
220
+ if (language === "typescript") {
221
+ expect(JSON.parse(fs.readFileSync(path.join(root, "functions", "package.json"), "utf-8")).dependencies).toEqual(expect.objectContaining({ jsonwebtoken: expect.any(String), bcryptjs: expect.any(String) }));
222
+ } else if (language === "csharp") {
223
+ const csproj = fs.readFileSync(path.join(root, "functions", "App.Functions.csproj"), "utf-8");
224
+ expect(csproj).toContain('Include="System.IdentityModel.Tokens.Jwt"');
225
+ expect(csproj).toContain('Include="BCrypt.Net-Next"');
226
+ } else {
227
+ expect(fs.readFileSync(path.join(root, "functions", "requirements.txt"), "utf-8")).toMatch(/PyJWT[\s\S]*bcrypt/);
228
+ }
229
+ });
230
+ });
231
+
232
+ describe("Python custom-jwt blueprint registration", () => {
233
+ const originalCwd = process.cwd();
234
+ let root: string;
235
+
236
+ beforeEach(() => {
237
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-py-auth-"));
238
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
239
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "python" } }));
240
+ fs.mkdirSync(path.join(root, "shared"), { recursive: true });
241
+ fs.writeFileSync(path.join(root, "shared", "package.json"), JSON.stringify({ name: "@fixture/shared" }));
242
+ fs.mkdirSync(path.join(root, "functions"), { recursive: true });
243
+ fs.mkdirSync(path.join(root, "app"), { recursive: true });
244
+ fs.writeFileSync(path.join(root, "app", "layout.tsx"), "export default function L({ children }: any) { return <html><body>{children}</body></html>; }\n");
245
+ process.chdir(root);
246
+ });
247
+
248
+ afterEach(() => {
249
+ process.chdir(originalCwd);
250
+ fs.rmSync(root, { recursive: true, force: true });
251
+ });
252
+
253
+ const register = (body: string) => fs.writeFileSync(path.join(root, "functions", "function_app.py"), body);
254
+
255
+ it("imports the blueprint object the generated module actually defines", async () => {
256
+ register("import azure.functions as func\napp = func.FunctionApp()\n# SwallowKit scaffold registrations\n");
257
+ await addAuthCommand({ provider: "custom-jwt" });
258
+ const blueprint = fs.readFileSync(path.join(root, "functions", "blueprints", "auth.py"), "utf-8");
259
+ const app = fs.readFileSync(path.join(root, "functions", "function_app.py"), "utf-8");
260
+ expect(blueprint).toMatch(/^auth_bp = func\.Blueprint\(\)$/m);
261
+ expect(app).toContain("from blueprints.auth import auth_bp\napp.register_blueprint(auth_bp)");
262
+ });
263
+
264
+ it("repairs the broken import written by earlier releases without duplicating the registration", async () => {
265
+ register("import azure.functions as func\napp = func.FunctionApp()\nfrom blueprints.auth import bp as auth_bp\napp.register_blueprint(auth_bp)\n# SwallowKit scaffold registrations\n");
266
+ await addAuthCommand({ provider: "custom-jwt" });
267
+ const app = fs.readFileSync(path.join(root, "functions", "function_app.py"), "utf-8");
268
+ expect(app).not.toContain("import bp as auth_bp");
269
+ expect(app.match(/app\.register_blueprint\(auth_bp\)/g)).toHaveLength(1);
270
+ });
160
271
  });
@@ -141,6 +141,21 @@ describe("getWorkspaceConfig", () => {
141
141
  expect(config.value).toEqual(["packages/*", "apps/*"]);
142
142
  }
143
143
  });
144
+
145
+ it.each([
146
+ ["shared", "functions"],
147
+ ["shared", "functions", "bff"],
148
+ ["shared", "bff"],
149
+ ])("denies SWA CLI keytar builds without approving unknown scripts (%j)", (...workspaces) => {
150
+ const config = getWorkspaceConfig("pnpm", workspaces);
151
+ expect(config.type).toBe("file");
152
+ if (config.type === "file") {
153
+ expect(config.content).toBe(
154
+ `packages:\n${workspaces.map((workspace) => ` - ${workspace}`).join("\n")}\n` +
155
+ "allowBuilds:\n keytar: false\n protobufjs: false\n unrs-resolver: false\n"
156
+ );
157
+ }
158
+ });
144
159
  });
145
160
 
146
161
  describe("getCiSetupStep", () => {
@@ -222,8 +237,8 @@ describe("getFunctionsStartScript", () => {
222
237
  expect(getFunctionsStartScript("npm")).toContain("npm start");
223
238
  });
224
239
 
225
- it("uses func start for csharp backends", () => {
226
- expect(getFunctionsStartScript("pnpm", "csharp")).toBe("cd functions && func start");
240
+ it("uses dotnet run for csharp isolated backends", () => {
241
+ expect(getFunctionsStartScript("pnpm", "csharp")).toBe("cd functions && dotnet run");
227
242
  });
228
243
 
229
244
  it("uses func start for python backends", () => {
@@ -2,10 +2,12 @@ import * as fs from "fs";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
4
  import * as childProcess from "child_process";
5
+ import * as ts from "typescript";
5
6
  import { EventEmitter } from "events";
6
7
  import { runMachineCli } from "../machine";
7
8
  import { getCSharpSchemaModelPath } from "../core/scaffold/native-schema-generator";
8
9
  import { initializeLegacyMigrations } from "../core/operations/migration-generator";
10
+ import { addModelContainerMigration } from "../core/operations/model-migration";
9
11
  import {
10
12
  MIGRATION_MANIFEST_PATH,
11
13
  createMigrationManifest,
@@ -17,6 +19,12 @@ jest.mock("child_process", () => {
17
19
  const actual = jest.requireActual<typeof import("child_process")>("child_process");
18
20
  return {
19
21
  ...actual,
22
+ execFile: jest.fn((
23
+ _command: string,
24
+ _args: string[],
25
+ _options: object,
26
+ callback: (error: Error | null, stdout: string, stderr: string) => void
27
+ ) => process.nextTick(() => callback(null, "", ""))),
20
28
  execFileSync: jest.fn(actual.execFileSync),
21
29
  spawn: jest.fn(),
22
30
  spawnSync: jest.fn(),
@@ -125,6 +133,7 @@ function createNativeBackendFixture(rootDir: string, backendLanguage: "csharp" |
125
133
  }
126
134
 
127
135
  function mockSuccessfulNativeCodegen(): () => void {
136
+ jest.mocked(childProcess.execFile).mockClear();
128
137
  const spawnSyncMock = childProcess.spawnSync as unknown as any;
129
138
  const spawnMock = childProcess.spawn as unknown as any;
130
139
  spawnSyncMock.mockReturnValue({ status: 0 });
@@ -134,6 +143,7 @@ function mockSuccessfulNativeCodegen(): () => void {
134
143
  return child;
135
144
  });
136
145
  return () => {
146
+ jest.mocked(childProcess.execFile).mockClear();
137
147
  spawnMock.mockReset();
138
148
  spawnSyncMock.mockReset();
139
149
  };
@@ -203,6 +213,7 @@ describe("plan / apply / drift / verify machine commands", () => {
203
213
  }),
204
214
  warnings: [],
205
215
  });
216
+
206
217
  expect(response.data.diagnostics).toEqual([]);
207
218
  expect(response.data.operations).toEqual(
208
219
  expect.arrayContaining([
@@ -221,6 +232,115 @@ describe("plan / apply / drift / verify machine commands", () => {
221
232
  ).toBe(true);
222
233
  });
223
234
 
235
+ it.each(["typescript", "csharp", "python"] as const)(
236
+ "blocks %s scaffold with an explicit authPolicy until authentication is configured",
237
+ async (language) => {
238
+ if (language === "typescript") createProjectFixture(tempDir);
239
+ else createNativeBackendFixture(tempDir, language);
240
+ fs.appendFileSync(path.join(tempDir, "shared", "models", "todo.ts"), "\nexport const authPolicy = { roles: ['admin'] };\n");
241
+ const reset = mockSuccessfulNativeCodegen();
242
+ try {
243
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
244
+ expect(plan.response).toMatchObject({
245
+ ok: false, status: "blocked", error: { code: "auth-policy-without-auth" },
246
+ nextActions: [expect.objectContaining({ command: expect.stringContaining("plan auth") })],
247
+ });
248
+ expect(fs.existsSync(path.join(tempDir, ".swallowkit", "state", "plans"))).toBe(false);
249
+ const generate = await runMachine(machineArgs("generate", "scaffold", "todo", "--api-only", "--skip-infra"));
250
+ expect(generate.response).toMatchObject({ ok: false, status: "blocked", error: { code: "auth-policy-without-auth" } });
251
+ expect(fs.existsSync(path.join(tempDir, "functions", "Crud"))).toBe(false);
252
+ expect(fs.readFileSync(path.join(tempDir, "shared", "index.ts"), "utf-8")).toBe("export {};\n");
253
+ } finally {
254
+ reset();
255
+ }
256
+ }
257
+ );
258
+
259
+ it("blocks an explicit role policy when the provider is none", async () => {
260
+ createProjectFixture(tempDir);
261
+ fs.unlinkSync(path.join(tempDir, "swallowkit.config.js"));
262
+ writeFile(path.join(tempDir, "swallowkit.config.json"), JSON.stringify({ auth: { provider: "none" } }));
263
+ fs.appendFileSync(path.join(tempDir, "shared", "models", "todo.ts"), "\nexport const authPolicy = { roles: ['admin'] };\n");
264
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
265
+ expect(plan.response).toMatchObject({ status: "blocked", error: { code: "auth-policy-without-auth" } });
266
+ });
267
+
268
+ it("appends a manually authored model export during plan/apply scaffold without duplicating it", async () => {
269
+ createProjectFixture(tempDir);
270
+ const indexPath = path.join(tempDir, "shared", "index.ts");
271
+ const originalIndex = "// Custom exports are preserved.\nexport {};\n";
272
+ fs.writeFileSync(indexPath, originalIndex);
273
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
274
+ expect(plan.response.data.operations).toContainEqual(expect.objectContaining({
275
+ path: "shared/index.ts", action: "append", ownership: "extension-point", conflict: false,
276
+ }));
277
+ expect(fs.readFileSync(indexPath, "utf-8")).toBe(originalIndex);
278
+ const apply = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
279
+ expect(apply.response.ok).toBe(true);
280
+ expect(fs.readFileSync(indexPath, "utf-8")).toBe(`${originalIndex}export { Todo } from './models/todo';\n`);
281
+ const repeat = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
282
+ expect(repeat.response.data.operations.filter((op: { path: string; action: string }) =>
283
+ op.path === "shared/index.ts" && op.action !== "skip")).toEqual([]);
284
+ });
285
+
286
+ it.each<["typescript" | "csharp" | "python", "nextjs" | "react"]>([
287
+ ["typescript", "nextjs"], ["csharp", "react"], ["python", "react"],
288
+ ])("validates auth prerequisites in %s/%s before plan and apply", async (language, frontend) => {
289
+ if (language === "typescript") createProjectFixture(tempDir);
290
+ else createNativeBackendFixture(tempDir, language);
291
+ fs.unlinkSync(path.join(tempDir, "swallowkit.config.js"));
292
+ const configPath = path.join(tempDir, "swallowkit.config.json");
293
+ const config = { backend: { language }, frontend: { framework: frontend },
294
+ auth: { schemes: { admin: { provider: "swa" } }, authorization: {
295
+ defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"], roles: ["admin"] } },
296
+ } },
297
+ };
298
+ writeFile(configPath, JSON.stringify(config));
299
+ const modelPath = path.join(tempDir, "shared", "models", "todo.ts");
300
+ writeFile(modelPath, `${createModelSource("Todo")}\nexport const authPolicy = { roles: ['admin'] };`);
301
+ const reset = mockSuccessfulNativeCodegen();
302
+ try {
303
+ const invalid = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
304
+ expect(invalid.response).toMatchObject({ status: "blocked", error: { code: "auth-policy-requires-named-policy" } });
305
+ writeFile(modelPath, `${createModelSource("Todo")}\nexport const authPolicy = { policy: 'adminOnly' };`);
306
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
307
+ expect(plan.response.status).toBe("complete");
308
+ writeFile(configPath, JSON.stringify({ ...config, auth: undefined }));
309
+ const stale = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
310
+ expect(stale.response).toMatchObject({ status: "blocked", error: { code: "stale-plan" } });
311
+ const direct = await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only", "--skip-infra"));
312
+ expect(direct.response).toMatchObject({ status: "blocked", error: { code: "auth-policy-without-auth" } });
313
+ expect(fs.readFileSync(path.join(tempDir, "shared", "index.ts"), "utf-8")).toBe("export {};\n");
314
+ } finally { reset(); }
315
+ });
316
+
317
+ it.each([
318
+ ['export { Todo } from "./models/todo";\n', false],
319
+ ["export * from './models/todo';\n", false],
320
+ ["export type { Todo } from './models/todo';\n", true],
321
+ ["export type { Todo, displayName } from './models/todo';\n", true],
322
+ ["export { type Todo } from './models/todo';\n", true],
323
+ ["export { Todo as MyTodo } from './models/todo';", true],
324
+ ])("preserves existing exports and handles syntax variations: %s", async (content, append) => {
325
+ createProjectFixture(tempDir);
326
+ const indexPath = path.join(tempDir, "shared", "index.ts");
327
+ fs.writeFileSync(indexPath, content);
328
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
329
+ expect(plan.response.data.operations.some((op: { path: string; action: string }) =>
330
+ op.path === "shared/index.ts" && op.action === "append")).toBe(append);
331
+ expect(fs.readFileSync(indexPath, "utf-8")).toBe(content);
332
+
333
+ const apply = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
334
+ expect(apply.response.ok).toBe(true);
335
+ // The shared package must still compile: no duplicate Todo binding, and Todo is usable as a value.
336
+ writeFile(path.join(tempDir, "shared", "consumer.ts"), "import { Todo } from './index';\nexport const parsed: Todo = Todo.parse({ id: '1', name: 'x' });\n");
337
+ const program = ts.createProgram([indexPath, path.join(tempDir, "shared", "consumer.ts")], {
338
+ strict: true, noEmit: true, skipLibCheck: true, module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020,
339
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
340
+ });
341
+ expect(ts.getPreEmitDiagnostics(program).map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"))).toEqual([]);
342
+ });
343
+
224
344
  it("updates a ledger-unregistered legacy manifest without approval and preserves baseline identity", async () => {
225
345
  createProjectFixture(tempDir);
226
346
  createInfrastructureFixture(tempDir);
@@ -372,6 +492,64 @@ describe("plan / apply / drift / verify machine commands", () => {
372
492
  }));
373
493
  });
374
494
 
495
+ it("plans and applies with --skip-infra without touching infra/, then adds only the migration later", async () => {
496
+ createProjectFixture(tempDir);
497
+ createInfrastructureFixture(tempDir);
498
+ saveMigrationManifest(createMigrationManifest(tempDir, { legacy: false, projectId: "new-project" }), tempDir);
499
+ const snapshotInfra = () => {
500
+ const files: Record<string, string> = {};
501
+ const visit = (directory: string) => {
502
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
503
+ const absolute = path.join(directory, entry.name);
504
+ if (entry.isDirectory()) visit(absolute);
505
+ else files[path.relative(tempDir, absolute).replace(/\\/g, "/")] = fs.readFileSync(absolute, "utf-8");
506
+ }
507
+ };
508
+ visit(path.join(tempDir, "infra"));
509
+ return files;
510
+ };
511
+ const before = snapshotInfra();
512
+
513
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
514
+ expect(plan.exitCode).toBe(0);
515
+ expect(plan.response.data.options.skipInfra).toBe(true);
516
+ expect(plan.response.data.operations.filter((operation: { path: string }) => operation.path.startsWith("infra/"))).toEqual([]);
517
+ expect(plan.response.data.warnings).toEqual(expect.arrayContaining([
518
+ expect.stringMatching(/^infra-skipped: The Cosmos DB container migration for Todo was not generated/),
519
+ ]));
520
+ expect(plan.response.nextActions).toContainEqual(expect.objectContaining({ command: "swallowkit migrations add-model todo" }));
521
+
522
+ const apply = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
523
+ expect(apply.exitCode).toBe(0);
524
+ expect(apply.response.data.createdFiles.filter((file: string) => file.startsWith("infra/"))).toEqual([]);
525
+ expect(apply.response.nextActions).toContainEqual(expect.objectContaining({ command: "swallowkit migrations add-model todo" }));
526
+ expect(snapshotInfra()).toEqual(before);
527
+
528
+ const verify = await runMachine(machineArgs("verify", "project", "--checks", "structure,drift"));
529
+ expect(verify.exitCode).toBe(0);
530
+ expect(verify.response.data.summary.done).toBe(true);
531
+ const drift = await runMachine(machineArgs("inspect", "drift"));
532
+ expect(drift.response.data.findings.filter((finding: { severity: string }) => finding.severity !== "info")).toEqual([]);
533
+
534
+ const dryRun = await addModelContainerMigration("todo", { dryRun: true });
535
+ expect(dryRun.operations.map((operation) => operation.path)).toEqual([
536
+ "infra/migrations/0001-create-todo-container/main.bicep",
537
+ MIGRATION_MANIFEST_PATH.replace(/\\/g, "/"),
538
+ ]);
539
+ expect(snapshotInfra()).toEqual(before);
540
+
541
+ const added = await addModelContainerMigration("todo");
542
+ expect(added.migration).toEqual({ version: 1, template: "infra/migrations/0001-create-todo-container/main.bicep" });
543
+ expect(fs.readFileSync(path.join(tempDir, added.migration!.template), "utf-8")).toContain("'/id'");
544
+ expect(loadMigrationManifest(tempDir).migrations).toEqual([
545
+ expect.objectContaining({ version: 1, source: "model", sourceModel: "Todo" }),
546
+ ]);
547
+ const again = await addModelContainerMigration("todo");
548
+ expect(again.operations.every((operation) => operation.action === "skip")).toBe(true);
549
+ const verifyAfter = await runMachine(machineArgs("verify", "project", "--checks", "structure,drift"));
550
+ expect(verifyAfter.response.data.summary.done).toBe(true);
551
+ });
552
+
375
553
  it.each([
376
554
  {
377
555
  name: "invalid manifest",
@@ -525,6 +703,7 @@ describe("plan / apply / drift / verify machine commands", () => {
525
703
  expect(fs.existsSync(path.join(tempDir, "functions", "openapi"))).toBe(false);
526
704
  expect(fs.existsSync(path.join(tempDir, modelPath))).toBe(false);
527
705
  expect(childProcess.spawn).not.toHaveBeenCalled();
706
+ expect(childProcess.execFile).not.toHaveBeenCalled();
528
707
 
529
708
  const applied = await runMachine(
530
709
  machineArgs("apply", "scaffold", "--plan", plan.response.data.planId)