swallowkit 1.0.0-beta.5 → 1.0.0-beta.51

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 (222) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +153 -242
  3. package/README.md +153 -243
  4. package/dist/__tests__/fixtures.d.ts +22 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +146 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/add-auth.d.ts +13 -0
  9. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  10. package/dist/cli/commands/add-auth.js +759 -0
  11. package/dist/cli/commands/add-auth.js.map +1 -0
  12. package/dist/cli/commands/add-connector.d.ts +20 -0
  13. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  14. package/dist/cli/commands/add-connector.js +163 -0
  15. package/dist/cli/commands/add-connector.js.map +1 -0
  16. package/dist/cli/commands/create-model.d.ts +1 -4
  17. package/dist/cli/commands/create-model.d.ts.map +1 -1
  18. package/dist/cli/commands/create-model.js +19 -81
  19. package/dist/cli/commands/create-model.js.map +1 -1
  20. package/dist/cli/commands/dev-seeds.d.ts +57 -0
  21. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  22. package/dist/cli/commands/dev-seeds.js +470 -0
  23. package/dist/cli/commands/dev-seeds.js.map +1 -0
  24. package/dist/cli/commands/dev.d.ts +37 -0
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +720 -156
  27. package/dist/cli/commands/dev.js.map +1 -1
  28. package/dist/cli/commands/index.d.ts +1 -0
  29. package/dist/cli/commands/index.d.ts.map +1 -1
  30. package/dist/cli/commands/index.js +3 -1
  31. package/dist/cli/commands/index.js.map +1 -1
  32. package/dist/cli/commands/init.d.ts +73 -0
  33. package/dist/cli/commands/init.d.ts.map +1 -1
  34. package/dist/cli/commands/init.js +3089 -1779
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/scaffold.d.ts +13 -0
  37. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  38. package/dist/cli/commands/scaffold.js +513 -129
  39. package/dist/cli/commands/scaffold.js.map +1 -1
  40. package/dist/cli/index.d.ts +5 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +206 -55
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/core/config.d.ts +14 -3
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +172 -5
  47. package/dist/core/config.js.map +1 -1
  48. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  49. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  50. package/dist/core/mock/connector-mock-server.js +480 -0
  51. package/dist/core/mock/connector-mock-server.js.map +1 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  53. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  54. package/dist/core/mock/zod-mock-generator.js +163 -0
  55. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  56. package/dist/core/operations/create-model.d.ts +15 -0
  57. package/dist/core/operations/create-model.d.ts.map +1 -0
  58. package/dist/core/operations/create-model.js +171 -0
  59. package/dist/core/operations/create-model.js.map +1 -0
  60. package/dist/core/operations/runtime.d.ts +32 -0
  61. package/dist/core/operations/runtime.d.ts.map +1 -0
  62. package/dist/core/operations/runtime.js +225 -0
  63. package/dist/core/operations/runtime.js.map +1 -0
  64. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  65. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  66. package/dist/core/operations/scaffold-machine.js +63 -0
  67. package/dist/core/operations/scaffold-machine.js.map +1 -0
  68. package/dist/core/project/manifest.d.ts +92 -0
  69. package/dist/core/project/manifest.d.ts.map +1 -0
  70. package/dist/core/project/manifest.js +321 -0
  71. package/dist/core/project/manifest.js.map +1 -0
  72. package/dist/core/project/validation.d.ts +20 -0
  73. package/dist/core/project/validation.d.ts.map +1 -0
  74. package/dist/core/project/validation.js +226 -0
  75. package/dist/core/project/validation.js.map +1 -0
  76. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  77. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  78. package/dist/core/scaffold/auth-generator.js +1714 -0
  79. package/dist/core/scaffold/auth-generator.js.map +1 -0
  80. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  81. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  82. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  83. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  84. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  85. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  86. package/dist/core/scaffold/functions-generator.js +922 -220
  87. package/dist/core/scaffold/functions-generator.js.map +1 -1
  88. package/dist/core/scaffold/model-parser.d.ts +20 -1
  89. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  90. package/dist/core/scaffold/model-parser.js +333 -139
  91. package/dist/core/scaffold/model-parser.js.map +1 -1
  92. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  93. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  94. package/dist/core/scaffold/native-schema-generator.js +696 -0
  95. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  96. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  97. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  98. package/dist/core/scaffold/nextjs-generator.js +326 -182
  99. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  100. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  101. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  102. package/dist/core/scaffold/openapi-generator.js +190 -0
  103. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  104. package/dist/core/scaffold/ui-generator.d.ts +13 -4
  105. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/ui-generator.js +772 -663
  107. package/dist/core/scaffold/ui-generator.js.map +1 -1
  108. package/dist/database/base-model.d.ts +3 -3
  109. package/dist/database/base-model.js +3 -3
  110. package/dist/index.d.ts +2 -2
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +2 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/machine/contracts.d.ts +16 -0
  115. package/dist/machine/contracts.d.ts.map +1 -0
  116. package/dist/machine/contracts.js +3 -0
  117. package/dist/machine/contracts.js.map +1 -0
  118. package/dist/machine/errors.d.ts +11 -0
  119. package/dist/machine/errors.d.ts.map +1 -0
  120. package/dist/machine/errors.js +34 -0
  121. package/dist/machine/errors.js.map +1 -0
  122. package/dist/machine/index.d.ts +3 -0
  123. package/dist/machine/index.d.ts.map +1 -0
  124. package/dist/machine/index.js +156 -0
  125. package/dist/machine/index.js.map +1 -0
  126. package/dist/mcp/index.d.ts +25 -0
  127. package/dist/mcp/index.d.ts.map +1 -0
  128. package/dist/mcp/index.js +202 -0
  129. package/dist/mcp/index.js.map +1 -0
  130. package/dist/types/index.d.ts +95 -0
  131. package/dist/types/index.d.ts.map +1 -1
  132. package/dist/utils/package-manager.d.ts +5 -2
  133. package/dist/utils/package-manager.d.ts.map +1 -1
  134. package/dist/utils/package-manager.js +34 -13
  135. package/dist/utils/package-manager.js.map +1 -1
  136. package/dist/utils/python-uv.d.ts +21 -0
  137. package/dist/utils/python-uv.d.ts.map +1 -0
  138. package/dist/utils/python-uv.js +111 -0
  139. package/dist/utils/python-uv.js.map +1 -0
  140. package/dist/version.d.ts +2 -0
  141. package/dist/version.d.ts.map +1 -0
  142. package/dist/version.js +49 -0
  143. package/dist/version.js.map +1 -0
  144. package/package.json +84 -74
  145. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  146. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +206 -0
  147. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  148. package/src/__tests__/auth.test.ts +761 -0
  149. package/src/__tests__/config.test.ts +295 -0
  150. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  151. package/src/__tests__/connector-mock-server.test.ts +439 -0
  152. package/src/__tests__/connector-model-bff.test.ts +162 -0
  153. package/src/__tests__/dev-seeds.test.ts +173 -0
  154. package/src/__tests__/dev.test.ts +287 -0
  155. package/src/__tests__/fixtures.ts +144 -0
  156. package/src/__tests__/functions-generator.test.ts +237 -0
  157. package/src/__tests__/init.test.ts +356 -0
  158. package/src/__tests__/jest-globals.d.ts +8 -0
  159. package/src/__tests__/machine.test.ts +356 -0
  160. package/src/__tests__/mcp.test.ts +145 -0
  161. package/src/__tests__/model-parser.test.ts +52 -0
  162. package/src/__tests__/multi-auth.test.ts +133 -0
  163. package/src/__tests__/nextjs-generator.test.ts +105 -0
  164. package/src/__tests__/openapi-generator.test.ts +43 -0
  165. package/src/__tests__/package-manager.test.ts +227 -0
  166. package/src/__tests__/python-uv.test.ts +48 -0
  167. package/src/__tests__/scaffold.test.ts +191 -0
  168. package/src/__tests__/string-utils.test.ts +75 -0
  169. package/src/__tests__/tsconfig.json +15 -0
  170. package/src/__tests__/ui-generator.test.ts +144 -0
  171. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  172. package/src/cli/commands/add-auth.ts +815 -0
  173. package/src/cli/commands/add-connector.ts +158 -0
  174. package/src/cli/commands/create-model.ts +62 -0
  175. package/src/cli/commands/dev-seeds.ts +614 -0
  176. package/src/cli/commands/dev.ts +1234 -0
  177. package/src/cli/commands/index.ts +9 -0
  178. package/src/cli/commands/init.ts +3902 -0
  179. package/src/cli/commands/provision.ts +193 -0
  180. package/src/cli/commands/scaffold.ts +1067 -0
  181. package/src/cli/index.ts +202 -0
  182. package/src/core/config.ts +379 -0
  183. package/src/core/mock/connector-mock-server.ts +555 -0
  184. package/src/core/mock/zod-mock-generator.ts +205 -0
  185. package/src/core/operations/create-model.ts +174 -0
  186. package/src/core/operations/runtime.ts +235 -0
  187. package/src/core/operations/scaffold-machine.ts +91 -0
  188. package/src/core/project/manifest.ts +402 -0
  189. package/src/core/project/validation.ts +245 -0
  190. package/src/core/scaffold/auth-generator.ts +1760 -0
  191. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  192. package/src/core/scaffold/functions-generator.ts +972 -0
  193. package/src/core/scaffold/model-parser.ts +847 -0
  194. package/src/core/scaffold/native-schema-generator.ts +822 -0
  195. package/src/core/scaffold/nextjs-generator.ts +382 -0
  196. package/src/core/scaffold/openapi-generator.ts +212 -0
  197. package/src/core/scaffold/ui-generator.ts +1068 -0
  198. package/src/database/base-model.ts +184 -0
  199. package/src/database/client.ts +140 -0
  200. package/src/database/repository.ts +104 -0
  201. package/src/database/runtime-check.ts +25 -0
  202. package/src/index.ts +27 -0
  203. package/src/machine/contracts.ts +17 -0
  204. package/src/machine/errors.ts +34 -0
  205. package/src/machine/index.ts +173 -0
  206. package/src/mcp/index.ts +209 -0
  207. package/src/types/index.ts +165 -0
  208. package/src/utils/package-manager.ts +246 -0
  209. package/src/utils/python-uv.ts +96 -0
  210. package/src/version.ts +13 -0
  211. package/dist/cli/commands/build.d.ts +0 -6
  212. package/dist/cli/commands/build.d.ts.map +0 -1
  213. package/dist/cli/commands/build.js +0 -177
  214. package/dist/cli/commands/build.js.map +0 -1
  215. package/dist/cli/commands/deploy.d.ts +0 -3
  216. package/dist/cli/commands/deploy.d.ts.map +0 -1
  217. package/dist/cli/commands/deploy.js +0 -147
  218. package/dist/cli/commands/deploy.js.map +0 -1
  219. package/dist/cli/commands/setup.d.ts +0 -6
  220. package/dist/cli/commands/setup.d.ts.map +0 -1
  221. package/dist/cli/commands/setup.js +0 -254
  222. package/dist/cli/commands/setup.js.map +0 -1
@@ -0,0 +1,356 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { EventEmitter } from "events";
5
+ import * as childProcess from "child_process";
6
+ import { runMachineCli } from "../machine";
7
+
8
+ jest.mock("child_process", () => {
9
+ const actual = jest.requireActual<typeof import("child_process")>("child_process");
10
+ return {
11
+ ...actual,
12
+ spawn: jest.fn(),
13
+ spawnSync: jest.fn(),
14
+ };
15
+ });
16
+
17
+ const repoRoot = process.cwd();
18
+
19
+ function writeFile(filePath: string, content: string): void {
20
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
21
+ fs.writeFileSync(filePath, content, "utf-8");
22
+ }
23
+
24
+ function createModelSource(name: string): string {
25
+ return `import { z } from 'zod/v4';
26
+
27
+ export const ${name} = z.object({
28
+ id: z.string(),
29
+ name: z.string().min(1),
30
+ createdAt: z.string().optional(),
31
+ updatedAt: z.string().optional(),
32
+ });
33
+
34
+ export type ${name} = z.infer<typeof ${name}>;
35
+
36
+ export const displayName = '${name}';
37
+ `;
38
+ }
39
+
40
+ function createProjectFixture(rootDir: string, options: { includeGeneratedArtifacts?: boolean; forbiddenBffDependency?: boolean } = {}): void {
41
+ writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
42
+ writeFile(
43
+ path.join(rootDir, "swallowkit.config.js"),
44
+ `module.exports = {
45
+ database: {
46
+ connectionString: 'AccountEndpoint=https://example.local;',
47
+ },
48
+ backend: {
49
+ language: 'typescript',
50
+ },
51
+ api: {
52
+ endpoint: '/api/_swallowkit',
53
+ },
54
+ };
55
+ `
56
+ );
57
+ writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
58
+ writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
59
+ writeFile(path.join(rootDir, "shared", "models", "todo.ts"), createModelSource("Todo"));
60
+ fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
61
+ fs.symlinkSync(
62
+ path.join(repoRoot, "node_modules", "zod"),
63
+ path.join(rootDir, "node_modules", "zod"),
64
+ "junction"
65
+ );
66
+
67
+ if (options.includeGeneratedArtifacts) {
68
+ writeFile(path.join(rootDir, "lib", "api", "call-function.ts"), "export function callFunction() {}\n");
69
+ writeFile(path.join(rootDir, "functions", "src", "todo.ts"), "export {};\n");
70
+ writeFile(path.join(rootDir, "app", "api", "todo", "route.ts"), "export async function GET() { return Response.json([]); }\n");
71
+ writeFile(path.join(rootDir, "app", "api", "todo", "[id]", "route.ts"), "export async function GET() { return Response.json({}); }\n");
72
+ }
73
+
74
+ if (options.forbiddenBffDependency) {
75
+ writeFile(
76
+ path.join(rootDir, "app", "api", "forbidden", "route.ts"),
77
+ "import { CosmosClient } from '@azure/cosmos';\nexport async function GET() { return Response.json({ ok: Boolean(CosmosClient) }); }\n"
78
+ );
79
+ }
80
+ }
81
+
82
+ function createCSharpScaffoldFixture(rootDir: string): void {
83
+ writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
84
+ writeFile(
85
+ path.join(rootDir, "swallowkit.config.js"),
86
+ `module.exports = {
87
+ backend: {
88
+ language: 'csharp',
89
+ },
90
+ api: {
91
+ endpoint: '/api/_swallowkit',
92
+ },
93
+ };
94
+ `
95
+ );
96
+ writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
97
+ writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
98
+ writeFile(path.join(rootDir, "shared", "models", "estimate.ts"), createModelSource("Estimate"));
99
+ writeFile(path.join(rootDir, "shared", "models", "member.ts"), createModelSource("Member"));
100
+ writeFile(path.join(rootDir, "shared", "models", "team.ts"), createModelSource("Team"));
101
+ fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
102
+ fs.symlinkSync(
103
+ path.join(repoRoot, "node_modules", "zod"),
104
+ path.join(rootDir, "node_modules", "zod"),
105
+ "junction"
106
+ );
107
+
108
+ writeFile(
109
+ path.join(rootDir, "functions", "Crud", "EstimateFunctions.cs"),
110
+ "namespace SwallowKit.Functions;\npublic sealed class EstimateFunctions { public void Approve() {} public void Submit() {} public void Remand() {} }\n"
111
+ );
112
+ writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "README.md"), "keep generated readme\n");
113
+ writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.csproj"), "<Project />\n");
114
+ writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.sln"), "solution\n");
115
+ writeFile(
116
+ path.join(rootDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Member.cs"),
117
+ "// existing member\n"
118
+ );
119
+ writeFile(
120
+ path.join(rootDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Team.cs"),
121
+ "// existing team\n"
122
+ );
123
+ }
124
+
125
+ function mockSuccessfulSpawn() {
126
+ const spawnMock = childProcess.spawn as unknown as any;
127
+ spawnMock.mockImplementation(() => {
128
+ const child = new EventEmitter() as childProcess.ChildProcess;
129
+ process.nextTick(() => child.emit("close", 0));
130
+ return child;
131
+ });
132
+ return spawnMock;
133
+ }
134
+
135
+ function createFreshInitProjectFixture(rootDir: string, backendLanguage: "csharp" | "python" = "csharp"): void {
136
+ writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
137
+ writeFile(
138
+ path.join(rootDir, "swallowkit.config.js"),
139
+ `module.exports = {
140
+ backend: {
141
+ language: '${backendLanguage}',
142
+ },
143
+ functions: {
144
+ baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
145
+ },
146
+ deployment: {
147
+ resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
148
+ swaName: process.env.AZURE_SWA_NAME || '',
149
+ },
150
+ };
151
+ `
152
+ );
153
+ writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
154
+ writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
155
+ fs.mkdirSync(path.join(rootDir, "shared", "models"), { recursive: true });
156
+ writeFile(path.join(rootDir, "app", "api", "greet", "route.ts"), "export async function GET() { return Response.json({}); }\n");
157
+ writeFile(path.join(rootDir, "lib", "api", "backend.ts"), "export const api = {};\n");
158
+ fs.mkdirSync(path.join(rootDir, "functions"), { recursive: true });
159
+ }
160
+
161
+ async function runMachine(argv: string[]): Promise<{ response: any; exitCode: number }> {
162
+ const writes: string[] = [];
163
+ const originalWrite = process.stdout.write.bind(process.stdout);
164
+ const originalExitCode = process.exitCode;
165
+
166
+ (process.stdout.write as unknown as (chunk: string | Uint8Array) => boolean) = ((chunk: string | Uint8Array) => {
167
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
168
+ return true;
169
+ }) as typeof process.stdout.write;
170
+
171
+ process.exitCode = 0;
172
+
173
+ try {
174
+ await runMachineCli(argv);
175
+ return {
176
+ response: JSON.parse(writes.join("")),
177
+ exitCode: process.exitCode || 0,
178
+ };
179
+ } finally {
180
+ process.stdout.write = originalWrite;
181
+ process.exitCode = originalExitCode;
182
+ }
183
+ }
184
+
185
+ describe("machine CLI", () => {
186
+ const originalCwd = process.cwd();
187
+ let tempDir: string;
188
+
189
+ beforeEach(() => {
190
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-machine-"));
191
+ process.chdir(tempDir);
192
+ });
193
+
194
+ afterEach(() => {
195
+ process.chdir(originalCwd);
196
+ fs.rmSync(tempDir, { recursive: true, force: true });
197
+ });
198
+
199
+ it("inspects SwallowKit project metadata as JSON", async () => {
200
+ createProjectFixture(tempDir, { includeGeneratedArtifacts: true });
201
+
202
+ const { response, exitCode } = await runMachine(["node", "swallowkit", "machine", "inspect", "project"]);
203
+
204
+ expect(exitCode).toBe(0);
205
+ expect(response.ok).toBe(true);
206
+ expect(response.command).toBe("inspect-project");
207
+ expect(response.data.manifest.entities[0].name).toBe("Todo");
208
+ expect(response.data.manifest.routes).toEqual(
209
+ expect.arrayContaining([
210
+ expect.objectContaining({
211
+ name: "Todo-bff-list",
212
+ publicPath: "/api/todo",
213
+ exists: true,
214
+ }),
215
+ ])
216
+ );
217
+ });
218
+
219
+ it("validates forbidden BFF dependencies", async () => {
220
+ createProjectFixture(tempDir, {
221
+ includeGeneratedArtifacts: true,
222
+ forbiddenBffDependency: true,
223
+ });
224
+
225
+ const { response, exitCode } = await runMachine(["node", "swallowkit", "machine", "validate", "project"]);
226
+
227
+ expect(exitCode).toBe(0);
228
+ expect(response.ok).toBe(true);
229
+ expect(response.command).toBe("validate-project");
230
+ expect(response.data.violations).toEqual(
231
+ expect.arrayContaining([
232
+ expect.objectContaining({
233
+ code: "forbidden-bff-dependency",
234
+ severity: "error",
235
+ }),
236
+ ])
237
+ );
238
+ });
239
+
240
+ it("does not report false positives for a fresh C# init project", async () => {
241
+ createFreshInitProjectFixture(tempDir, "csharp");
242
+
243
+ const { response, exitCode } = await runMachine(["node", "swallowkit", "machine", "validate", "project"]);
244
+
245
+ expect(exitCode).toBe(0);
246
+ expect(response.ok).toBe(true);
247
+ expect(response.command).toBe("validate-project");
248
+ expect(response.data.manifest.backendLanguage).toBe("csharp");
249
+ expect(response.data.manifest.configValidation.valid).toBe(true);
250
+ expect(response.data.violations).toEqual([]);
251
+ });
252
+
253
+ it("generates model templates through the machine interface", async () => {
254
+ writeFile(path.join(tempDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
255
+ writeFile(
256
+ path.join(tempDir, "swallowkit.config.js"),
257
+ `module.exports = {
258
+ backend: { language: 'typescript' },
259
+ api: { endpoint: '/api/_swallowkit' },
260
+ };
261
+ `
262
+ );
263
+ fs.mkdirSync(path.join(tempDir, "node_modules"), { recursive: true });
264
+ fs.symlinkSync(
265
+ path.join(repoRoot, "node_modules", "zod"),
266
+ path.join(tempDir, "node_modules", "zod"),
267
+ "junction"
268
+ );
269
+ writeFile(path.join(tempDir, "shared", "index.ts"), "export {};\n");
270
+
271
+ const { response, exitCode } = await runMachine([
272
+ "node",
273
+ "swallowkit",
274
+ "machine",
275
+ "generate",
276
+ "model",
277
+ "Task",
278
+ "--overwrite",
279
+ "never",
280
+ ]);
281
+
282
+ expect(exitCode).toBe(0);
283
+ expect(response.ok).toBe(true);
284
+ expect(response.command).toBe("generate-model");
285
+ expect(response.data.createdFiles).toContain("shared/models/task.ts");
286
+ expect(fs.existsSync(path.join(tempDir, ".swallowkit", "project.json"))).toBe(true);
287
+ });
288
+
289
+ it("generates scaffold artifacts through the machine interface", async () => {
290
+ createProjectFixture(tempDir);
291
+
292
+ const { response, exitCode } = await runMachine([
293
+ "node",
294
+ "swallowkit",
295
+ "machine",
296
+ "generate",
297
+ "scaffold",
298
+ "todo",
299
+ "--api-only",
300
+ ]);
301
+
302
+ expect(exitCode).toBe(0);
303
+ expect(response.ok).toBe(true);
304
+ expect(response.command).toBe("generate-scaffold");
305
+ expect(response.data.createdFiles).toEqual(
306
+ expect.arrayContaining([
307
+ "app/api/todo/route.ts",
308
+ "app/api/todo/[id]/route.ts",
309
+ "functions/src/todo.ts",
310
+ "lib/api/call-function.ts",
311
+ ])
312
+ );
313
+ expect(fs.existsSync(path.join(tempDir, ".swallowkit", "project.json"))).toBe(true);
314
+ });
315
+
316
+ it("preserves C# custom Functions and shared generated assets when api-only scaffolding one model", async () => {
317
+ createCSharpScaffoldFixture(tempDir);
318
+ const spawnSyncMock = childProcess.spawnSync as unknown as any;
319
+ spawnSyncMock.mockReturnValue({ status: 0 });
320
+ const spawnMock = mockSuccessfulSpawn();
321
+
322
+ try {
323
+ const { response, exitCode } = await runMachine([
324
+ "node",
325
+ "swallowkit",
326
+ "machine",
327
+ "generate",
328
+ "scaffold",
329
+ "estimate",
330
+ "--api-only",
331
+ ]);
332
+
333
+ expect(exitCode).toBe(0);
334
+ expect(response.ok).toBe(true);
335
+ expect(response.command).toBe("generate-scaffold");
336
+ expect(response.data.createdFiles).toEqual(
337
+ expect.arrayContaining([
338
+ "functions/Crud/EstimateCrudFunctions.cs",
339
+ "app/api/estimate/route.ts",
340
+ "app/api/estimate/[id]/route.ts",
341
+ ])
342
+ );
343
+ expect(response.data.deletedFiles).toEqual([]);
344
+ expect(fs.readFileSync(path.join(tempDir, "functions", "Crud", "EstimateFunctions.cs"), "utf-8")).toContain("Approve");
345
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "README.md"))).toBe(true);
346
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.csproj"))).toBe(true);
347
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.sln"))).toBe(true);
348
+ expect(fs.readFileSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Member.cs"), "utf-8")).toBe("// existing member\n");
349
+ expect(fs.readFileSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Team.cs"), "utf-8")).toBe("// existing team\n");
350
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Estimate.cs"))).toBe(true);
351
+ } finally {
352
+ spawnMock.mockReset();
353
+ spawnSyncMock.mockReset();
354
+ }
355
+ });
356
+ });
@@ -0,0 +1,145 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { buildSwallowKitToolDefinitions } from "../mcp";
5
+
6
+ type MachineCliRunner = NonNullable<Parameters<typeof buildSwallowKitToolDefinitions>[0]>;
7
+ const builtMcpEntrypoint = path.resolve(__dirname, "..", "..", "dist", "mcp", "index.js");
8
+ const itWhenBuiltEntrypointExists = fs.existsSync(builtMcpEntrypoint) ? it : it.skip;
9
+
10
+ describe("SwallowKit MCP tool definitions", () => {
11
+ it("delegates inspect_project to the machine CLI", async () => {
12
+ const runner = jest.fn(async () => ({
13
+ stdout: JSON.stringify({
14
+ ok: true,
15
+ command: "inspect-project",
16
+ data: { manifestSource: "file" },
17
+ }),
18
+ stderr: "",
19
+ exitCode: 0,
20
+ }));
21
+
22
+ const tool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find((candidate) => candidate.name === "swallowkit_inspect_project");
23
+ expect(tool).toBeDefined();
24
+
25
+ const result = await tool!.handler({});
26
+ expect(runner).toHaveBeenCalledWith(["inspect", "project"]);
27
+ expect(JSON.parse(result.content[0].text)).toEqual({
28
+ manifestSource: "file",
29
+ metadata: { swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) },
30
+ });
31
+ });
32
+
33
+ it("includes the MCP version in validate_project metadata", async () => {
34
+ const runner = jest.fn(async () => ({
35
+ stdout: JSON.stringify({ ok: true, command: "validate-project", data: { valid: true } }),
36
+ stderr: "",
37
+ exitCode: 0,
38
+ }));
39
+ const tool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find(
40
+ (candidate) => candidate.name === "swallowkit_validate_project"
41
+ );
42
+
43
+ const result = await tool!.handler({});
44
+ expect(JSON.parse(result.content[0].text)).toEqual({
45
+ valid: true,
46
+ metadata: { swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) },
47
+ });
48
+ });
49
+
50
+ it("delegates scaffold_model with explicit args", async () => {
51
+ const runner = jest.fn(async () => ({
52
+ stdout: JSON.stringify({
53
+ ok: true,
54
+ command: "generate-scaffold",
55
+ data: { createdFiles: ["functions/src/todo.ts"] },
56
+ }),
57
+ stderr: "",
58
+ exitCode: 0,
59
+ }));
60
+
61
+ const tool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find((candidate) => candidate.name === "swallowkit_scaffold_model");
62
+ expect(tool).toBeDefined();
63
+
64
+ const result = await tool!.handler({
65
+ model: "todo",
66
+ functionsDir: "functions",
67
+ apiDir: "app/api",
68
+ apiOnly: true,
69
+ });
70
+
71
+ expect(runner).toHaveBeenCalledWith([
72
+ "generate",
73
+ "scaffold",
74
+ "todo",
75
+ "--functions-dir",
76
+ "functions",
77
+ "--api-dir",
78
+ "app/api",
79
+ "--api-only",
80
+ ]);
81
+ expect(JSON.parse(result.content[0].text)).toEqual({ createdFiles: ["functions/src/todo.ts"] });
82
+ });
83
+
84
+ itWhenBuiltEntrypointExists("keeps the built MCP entrypoint alive long enough to complete the handshake", async () => {
85
+ await new Promise<void>((resolve, reject) => {
86
+ const child = spawn(process.execPath, [builtMcpEntrypoint], {
87
+ cwd: path.resolve(__dirname, "..", ".."),
88
+ stdio: ["pipe", "pipe", "pipe"],
89
+ });
90
+ let settled = false;
91
+ let expectedShutdown = false;
92
+ let stderr = "";
93
+
94
+ const finishResolve = () => {
95
+ if (!settled) {
96
+ settled = true;
97
+ resolve();
98
+ }
99
+ };
100
+
101
+ const finishReject = (error: Error) => {
102
+ if (!settled) {
103
+ settled = true;
104
+ reject(error);
105
+ }
106
+ };
107
+
108
+ const timer = setTimeout(() => {
109
+ expectedShutdown = true;
110
+ child.kill();
111
+ }, 1000);
112
+
113
+ child.stderr.on("data", (chunk) => {
114
+ stderr += chunk.toString();
115
+ });
116
+
117
+ child.on("error", (error) => {
118
+ clearTimeout(timer);
119
+ finishReject(error);
120
+ });
121
+
122
+ child.on("close", (code, signal) => {
123
+ clearTimeout(timer);
124
+
125
+ if (expectedShutdown) {
126
+ expect(stderr).toMatch(/\[swallowkit-mcp\] version: \d+\.\d+\.\d+/);
127
+ finishResolve();
128
+ return;
129
+ }
130
+
131
+ finishReject(
132
+ new Error(
133
+ `Built MCP entrypoint exited early with code ${code ?? "null"} and signal ${signal ?? "null"}${stderr ? `: ${stderr}` : ""}`
134
+ )
135
+ );
136
+ });
137
+ });
138
+ });
139
+
140
+ itWhenBuiltEntrypointExists("preserves a runtime dynamic execa import in the built MCP entrypoint", () => {
141
+ const source = fs.readFileSync(builtMcpEntrypoint, "utf8");
142
+ expect(source).toContain('new Function("specifier", "return import(specifier);")');
143
+ expect(source).not.toContain('require("execa")');
144
+ });
145
+ });
@@ -0,0 +1,52 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { parseModelFile } from "../core/scaffold/model-parser";
4
+
5
+ describe("parseModelFile", () => {
6
+ it("preserves array and enum metadata for schemas with defaults", async () => {
7
+ const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
8
+ const modelPath = path.join(tempDir, "product.ts");
9
+
10
+ try {
11
+ fs.writeFileSync(
12
+ modelPath,
13
+ `import { z } from 'zod/v4';
14
+
15
+ export const Product = z.object({
16
+ id: z.string(),
17
+ name: z.string().min(1),
18
+ price: z.number().min(0),
19
+ tags: z.array(z.string()).default([]),
20
+ status: z.enum(['draft', 'active', 'archived']).default('draft'),
21
+ createdAt: z.string().optional(),
22
+ updatedAt: z.string().optional(),
23
+ });
24
+
25
+ export type Product = z.infer<typeof Product>;
26
+ `,
27
+ "utf-8"
28
+ );
29
+
30
+ const model = await parseModelFile(modelPath);
31
+ expect(model.fields).toEqual(
32
+ expect.arrayContaining([
33
+ expect.objectContaining({
34
+ name: "tags",
35
+ type: "string",
36
+ isArray: true,
37
+ isOptional: true,
38
+ }),
39
+ expect.objectContaining({
40
+ name: "status",
41
+ type: "string",
42
+ isArray: false,
43
+ isOptional: true,
44
+ enumValues: ["draft", "active", "archived"],
45
+ }),
46
+ ])
47
+ );
48
+ } finally {
49
+ fs.rmSync(tempDir, { recursive: true, force: true });
50
+ }
51
+ });
52
+ });
@@ -0,0 +1,133 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { addAuthCommand } from "../cli/commands/add-auth";
5
+ import {
6
+ generateBFFCallFunctionWithMultipleAuth,
7
+ generateNamedAuthRouterCSharp,
8
+ generateNamedAuthRouterPython,
9
+ generateNamedAuthRouterTS,
10
+ } from "../core/scaffold/auth-generator";
11
+ import { normalizeAuthConfig } from "../core/config";
12
+ import { parseAuthPolicy } from "../core/scaffold/model-parser";
13
+ import { createBasicModelInfo } from "./fixtures";
14
+ import { generateCSharpAzureFunctionsCRUD, generateCompactAzureFunctionsCRUD, generatePythonAzureFunctionsCRUD } from "../core/scaffold/functions-generator";
15
+
16
+ const auth = normalizeAuthConfig({
17
+ schemes: {
18
+ admin: { provider: "swa" },
19
+ lineUser: { provider: "external-token" },
20
+ },
21
+ authorization: {
22
+ defaultPolicy: "anonymous",
23
+ policies: {
24
+ adminOnly: { schemes: ["admin"], roles: ["authenticated"] },
25
+ lineUserOnly: { schemes: ["lineUser"], roles: ["authenticated"] },
26
+ },
27
+ },
28
+ })!;
29
+
30
+ describe("named authentication routers", () => {
31
+ it("generates deterministic TypeScript policy routing and normalized principals", () => {
32
+ const code = generateNamedAuthRouterTS(auth);
33
+ expect(code).toContain("policy.schemes.filter");
34
+ expect(code).toContain("candidates.length !== 1");
35
+ expect(code).toContain("subject: value.userId");
36
+ expect(code).toContain("scheme + ':' + principal.subject");
37
+ expect(code).toContain("new AuthError(403");
38
+ expect(code).toContain("new AuthError(503");
39
+ expect(code).not.toContain("JWT_SECRET");
40
+ });
41
+
42
+ it("generates equivalent C# and Python named guards", () => {
43
+ const csharp = generateNamedAuthRouterCSharp(auth);
44
+ const python = generateNamedAuthRouterPython(auth);
45
+ expect(csharp).toContain("record AuthPrincipal(string Subject, string Scheme, string Issuer");
46
+ expect(csharp).toContain("candidates.Length != 1");
47
+ expect(csharp).toContain("HttpStatusCode.Forbidden");
48
+ expect(python).toContain('principal["scheme"] + ":" + principal["subject"]');
49
+ expect(python).toContain("len(candidates) != 1");
50
+ expect(python).toContain("AuthError(503");
51
+ });
52
+
53
+ it("generates valid branch structure for an SWA-only Python router", () => {
54
+ const swaOnly = normalizeAuthConfig({ schemes: { admin: { provider: "swa", swa: { allowedProviders: ["github"] } } }, authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } } })!;
55
+ const python = generateNamedAuthRouterPython(swaOnly);
56
+ expect(python).toContain(' if PROVIDERS[scheme] == "swa":');
57
+ expect(python).not.toContain(' elif PROVIDERS[scheme] == "swa":');
58
+ expect(python).toContain("ALLOWED_PROVIDERS");
59
+ expect(generateNamedAuthRouterCSharp(swaOnly)).toContain("AllowedProviders[scheme]");
60
+ });
61
+
62
+ it("scaffolds named read/write guards for all Functions languages", () => {
63
+ const policy = { read: "adminOnly", write: "lineUserOnly" };
64
+ const model = createBasicModelInfo();
65
+ const ts = generateCompactAzureFunctionsCRUD(model, "@fixture/shared", policy);
66
+ const cs = generateCSharpAzureFunctionsCRUD(model, policy);
67
+ const py = generatePythonAzureFunctionsCRUD(model, policy).blueprint;
68
+ expect(ts).toContain("./auth/auth-router");
69
+ expect(ts).toContain('requireAuth(request, "adminOnly")');
70
+ expect(ts).toContain('requireAuth(request, "lineUserOnly")');
71
+ expect(cs).toContain('AuthRouter.Authorize(request, "adminOnly")');
72
+ expect(cs).toContain('AuthRouter.Authorize(request, "lineUserOnly")');
73
+ expect(py).toContain("from auth.auth_router import");
74
+ expect(py).toContain('require_auth(req, "adminOnly")');
75
+ expect(py).toContain('require_auth(req, "lineUserOnly")');
76
+ });
77
+
78
+ it("forwards both credential sources and the Functions host key", () => {
79
+ const code = generateBFFCallFunctionWithMultipleAuth();
80
+ expect(code).toContain("fetchHeaders['Authorization']");
81
+ expect(code).toContain("fetchHeaders['x-ms-client-principal']");
82
+ expect(code).toContain("'x-functions-key'");
83
+ expect(code).not.toMatch(/console\.log\([^\n]*(authorization|principal|cookie)/i);
84
+ });
85
+
86
+ it("parses model-wide and operation-specific named policies", () => {
87
+ expect(parseAuthPolicy("export const authPolicy = { policy: 'adminOnly' };"))
88
+ .toEqual({ policy: "adminOnly" });
89
+ expect(parseAuthPolicy("export const authPolicy = { read: 'adminOnly', write: 'lineUserOnly' };"))
90
+ .toEqual({ read: "adminOnly", write: "lineUserOnly" });
91
+ });
92
+ });
93
+
94
+ describe("add-auth named scheme CLI", () => {
95
+ const originalCwd = process.cwd();
96
+ let root: string;
97
+
98
+ beforeEach(() => {
99
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-multi-auth-"));
100
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
101
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" }, auth: { schemes: {}, authorization: { defaultPolicy: "anonymous", policies: {} } } }, null, 2));
102
+ process.chdir(root);
103
+ });
104
+
105
+ afterEach(() => {
106
+ process.chdir(originalCwd);
107
+ fs.rmSync(root, { recursive: true, force: true });
108
+ });
109
+
110
+ it("adds two schemes without overwriting the first verifier", async () => {
111
+ await addAuthCommand({ scheme: "admin", provider: "swa" });
112
+ await addAuthCommand({ scheme: "lineUser", provider: "external-token" });
113
+ const verifier = path.join(root, "functions", "src", "auth", "schemes", "line-user", "verifier.ts");
114
+ fs.writeFileSync(verifier, "// user verifier\n", "utf-8");
115
+ const config = JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8"));
116
+ expect(config.auth.schemes.admin.provider).toBe("swa");
117
+ expect(config.auth.schemes.lineUser.provider).toBe("external-token");
118
+ await expect(addAuthCommand({ scheme: "lineUser", provider: "external-token" })).rejects.toThrow("already exists");
119
+ expect(fs.readFileSync(verifier, "utf-8")).toBe("// user verifier\n");
120
+ expect(fs.readFileSync(path.join(root, "lib", "api", "call-function.ts"), "utf-8")).toContain("x-functions-key");
121
+ });
122
+
123
+ it("updates a JavaScript config that already has a trailing comma", async () => {
124
+ fs.rmSync(path.join(root, "swallowkit.config.json"));
125
+ fs.writeFileSync(path.join(root, "swallowkit.config.js"), "module.exports = {\n backend: { language: 'typescript' },\n api: { endpoint: '/api/test' },\n};\n");
126
+ await addAuthCommand({ scheme: "admin", provider: "swa" });
127
+ const content = fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8");
128
+ expect(content).not.toContain(",,");
129
+ const configModule = { exports: {} as Record<string, unknown> };
130
+ new Function("module", content)(configModule);
131
+ expect((configModule.exports as { auth: { schemes: { admin: { provider: string } } } }).auth.schemes.admin.provider).toBe("swa");
132
+ });
133
+ });