swallowkit 1.0.0-beta.4 → 1.0.0-beta.40

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 (204) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +153 -215
  3. package/README.md +153 -216
  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 +10 -0
  9. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  10. package/dist/cli/commands/add-auth.js +426 -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 +21 -82
  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 +33 -0
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +628 -146
  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 +45 -0
  33. package/dist/cli/commands/init.d.ts.map +1 -1
  34. package/dist/cli/commands/init.js +2851 -1754
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/scaffold.d.ts +4 -0
  37. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  38. package/dist/cli/commands/scaffold.js +439 -130
  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 +200 -42
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/core/config.d.ts +8 -2
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +94 -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 +209 -0
  75. package/dist/core/project/validation.js.map +1 -0
  76. package/dist/core/scaffold/auth-generator.d.ts +38 -0
  77. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  78. package/dist/core/scaffold/auth-generator.js +1244 -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 +1027 -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 +920 -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 +328 -135
  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 +314 -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 +10 -4
  105. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/ui-generator.js +768 -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 +186 -0
  129. package/dist/mcp/index.js.map +1 -0
  130. package/dist/types/index.d.ts +65 -0
  131. package/dist/types/index.d.ts.map +1 -1
  132. package/dist/utils/package-manager.d.ts +110 -0
  133. package/dist/utils/package-manager.d.ts.map +1 -0
  134. package/dist/utils/package-manager.js +224 -0
  135. package/dist/utils/package-manager.js.map +1 -0
  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/package.json +84 -73
  141. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  142. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  143. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  144. package/src/__tests__/auth.test.ts +654 -0
  145. package/src/__tests__/config.test.ts +274 -0
  146. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  147. package/src/__tests__/connector-mock-server.test.ts +439 -0
  148. package/src/__tests__/connector-model-bff.test.ts +162 -0
  149. package/src/__tests__/dev-seeds.test.ts +173 -0
  150. package/src/__tests__/dev.test.ts +252 -0
  151. package/src/__tests__/fixtures.ts +144 -0
  152. package/src/__tests__/functions-generator.test.ts +237 -0
  153. package/src/__tests__/init.test.ts +197 -0
  154. package/src/__tests__/jest-globals.d.ts +8 -0
  155. package/src/__tests__/machine.test.ts +356 -0
  156. package/src/__tests__/mcp.test.ts +124 -0
  157. package/src/__tests__/model-parser.test.ts +52 -0
  158. package/src/__tests__/nextjs-generator.test.ts +97 -0
  159. package/src/__tests__/openapi-generator.test.ts +43 -0
  160. package/src/__tests__/package-manager.test.ts +201 -0
  161. package/src/__tests__/python-uv.test.ts +48 -0
  162. package/src/__tests__/scaffold.test.ts +191 -0
  163. package/src/__tests__/string-utils.test.ts +75 -0
  164. package/src/__tests__/tsconfig.json +15 -0
  165. package/src/__tests__/ui-generator.test.ts +144 -0
  166. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  167. package/src/cli/commands/add-auth.ts +482 -0
  168. package/src/cli/commands/add-connector.ts +158 -0
  169. package/src/cli/commands/create-model.ts +62 -0
  170. package/src/cli/commands/dev-seeds.ts +614 -0
  171. package/src/cli/commands/dev.ts +1134 -0
  172. package/src/cli/commands/index.ts +9 -0
  173. package/src/cli/commands/init.ts +3600 -0
  174. package/src/cli/commands/provision.ts +193 -0
  175. package/src/cli/commands/scaffold.ts +991 -0
  176. package/src/cli/index.ts +196 -0
  177. package/src/core/config.ts +312 -0
  178. package/src/core/mock/connector-mock-server.ts +555 -0
  179. package/src/core/mock/zod-mock-generator.ts +205 -0
  180. package/src/core/operations/create-model.ts +174 -0
  181. package/src/core/operations/runtime.ts +235 -0
  182. package/src/core/operations/scaffold-machine.ts +91 -0
  183. package/src/core/project/manifest.ts +402 -0
  184. package/src/core/project/validation.ts +229 -0
  185. package/src/core/scaffold/auth-generator.ts +1284 -0
  186. package/src/core/scaffold/connector-functions-generator.ts +1128 -0
  187. package/src/core/scaffold/functions-generator.ts +970 -0
  188. package/src/core/scaffold/model-parser.ts +841 -0
  189. package/src/core/scaffold/native-schema-generator.ts +822 -0
  190. package/src/core/scaffold/nextjs-generator.ts +370 -0
  191. package/src/core/scaffold/openapi-generator.ts +212 -0
  192. package/src/core/scaffold/ui-generator.ts +1061 -0
  193. package/src/database/base-model.ts +184 -0
  194. package/src/database/client.ts +140 -0
  195. package/src/database/repository.ts +104 -0
  196. package/src/database/runtime-check.ts +25 -0
  197. package/src/index.ts +27 -0
  198. package/src/machine/contracts.ts +17 -0
  199. package/src/machine/errors.ts +34 -0
  200. package/src/machine/index.ts +173 -0
  201. package/src/mcp/index.ts +191 -0
  202. package/src/types/index.ts +134 -0
  203. package/src/utils/package-manager.ts +238 -0
  204. package/src/utils/python-uv.ts +96 -0
@@ -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,124 @@
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 (_args: Parameters<MachineCliRunner>[0]) => ({
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({ manifestSource: "file" });
28
+ });
29
+
30
+ it("delegates scaffold_model with explicit args", async () => {
31
+ const runner = jest.fn(async (_args: Parameters<MachineCliRunner>[0]) => ({
32
+ stdout: JSON.stringify({
33
+ ok: true,
34
+ command: "generate-scaffold",
35
+ data: { createdFiles: ["functions/src/todo.ts"] },
36
+ }),
37
+ stderr: "",
38
+ exitCode: 0,
39
+ }));
40
+
41
+ const tool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find((candidate) => candidate.name === "swallowkit_scaffold_model");
42
+ expect(tool).toBeDefined();
43
+
44
+ const result = await tool!.handler({
45
+ model: "todo",
46
+ functionsDir: "functions",
47
+ apiDir: "app/api",
48
+ apiOnly: true,
49
+ });
50
+
51
+ expect(runner).toHaveBeenCalledWith([
52
+ "generate",
53
+ "scaffold",
54
+ "todo",
55
+ "--functions-dir",
56
+ "functions",
57
+ "--api-dir",
58
+ "app/api",
59
+ "--api-only",
60
+ ]);
61
+ expect(JSON.parse(result.content[0].text)).toEqual({ createdFiles: ["functions/src/todo.ts"] });
62
+ });
63
+
64
+ itWhenBuiltEntrypointExists("keeps the built MCP entrypoint alive long enough to complete the handshake", async () => {
65
+ await new Promise<void>((resolve, reject) => {
66
+ const child = spawn(process.execPath, [builtMcpEntrypoint], {
67
+ cwd: path.resolve(__dirname, "..", ".."),
68
+ stdio: ["pipe", "pipe", "pipe"],
69
+ });
70
+ let settled = false;
71
+ let expectedShutdown = false;
72
+ let stderr = "";
73
+
74
+ const finishResolve = () => {
75
+ if (!settled) {
76
+ settled = true;
77
+ resolve();
78
+ }
79
+ };
80
+
81
+ const finishReject = (error: Error) => {
82
+ if (!settled) {
83
+ settled = true;
84
+ reject(error);
85
+ }
86
+ };
87
+
88
+ const timer = setTimeout(() => {
89
+ expectedShutdown = true;
90
+ child.kill();
91
+ }, 1000);
92
+
93
+ child.stderr.on("data", (chunk) => {
94
+ stderr += chunk.toString();
95
+ });
96
+
97
+ child.on("error", (error) => {
98
+ clearTimeout(timer);
99
+ finishReject(error);
100
+ });
101
+
102
+ child.on("close", (code, signal) => {
103
+ clearTimeout(timer);
104
+
105
+ if (expectedShutdown) {
106
+ finishResolve();
107
+ return;
108
+ }
109
+
110
+ finishReject(
111
+ new Error(
112
+ `Built MCP entrypoint exited early with code ${code ?? "null"} and signal ${signal ?? "null"}${stderr ? `: ${stderr}` : ""}`
113
+ )
114
+ );
115
+ });
116
+ });
117
+ });
118
+
119
+ itWhenBuiltEntrypointExists("preserves a runtime dynamic execa import in the built MCP entrypoint", () => {
120
+ const source = fs.readFileSync(builtMcpEntrypoint, "utf8");
121
+ expect(source).toContain('new Function("specifier", "return import(specifier);")');
122
+ expect(source).not.toContain('require("execa")');
123
+ });
124
+ });
@@ -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,97 @@
1
+ import {
2
+ generateBFFCallFunction,
3
+ generateCompactBFFRoutes,
4
+ } from "../core/scaffold/nextjs-generator";
5
+ import { createBasicModelInfo } from "./fixtures";
6
+
7
+ describe("generateBFFCallFunction", () => {
8
+ it("generates call function helper code", () => {
9
+ const code = generateBFFCallFunction();
10
+ expect(code).toMatchSnapshot();
11
+ });
12
+
13
+ it("includes getFunctionsBaseUrl helper", () => {
14
+ const code = generateBFFCallFunction();
15
+ expect(code).toContain("getFunctionsBaseUrl");
16
+ expect(code).toContain("BACKEND_FUNCTIONS_BASE_URL");
17
+ });
18
+
19
+ it("includes callFunction export", () => {
20
+ const code = generateBFFCallFunction();
21
+ expect(code).toContain("export async function callFunction");
22
+ });
23
+
24
+ it("includes z.ZodSchema typings", () => {
25
+ const code = generateBFFCallFunction();
26
+ expect(code).toContain("z.ZodSchema");
27
+ });
28
+ });
29
+
30
+ describe("generateCompactBFFRoutes", () => {
31
+ it("generates list and detail routes", () => {
32
+ const model = createBasicModelInfo();
33
+ const routes = generateCompactBFFRoutes(model, "@myapp/shared");
34
+
35
+ expect(routes.listRoute).toMatchSnapshot();
36
+ expect(routes.detailRoute).toMatchSnapshot();
37
+ });
38
+
39
+ it("imports the correct schema", () => {
40
+ const model = createBasicModelInfo();
41
+ const routes = generateCompactBFFRoutes(model, "@myapp/shared");
42
+
43
+ expect(routes.listRoute).toContain(
44
+ "import { todoSchema } from '@myapp/shared'"
45
+ );
46
+ expect(routes.detailRoute).toContain(
47
+ "import { todoSchema } from '@myapp/shared'"
48
+ );
49
+ });
50
+
51
+ it("uses correct API paths (camelCase)", () => {
52
+ const model = createBasicModelInfo();
53
+ const routes = generateCompactBFFRoutes(model, "@myapp/shared");
54
+
55
+ expect(routes.listRoute).toContain("path: '/api/todo'");
56
+ expect(routes.detailRoute).toContain("/api/todo/");
57
+ });
58
+
59
+ it("list route has GET and POST handlers", () => {
60
+ const model = createBasicModelInfo();
61
+ const routes = generateCompactBFFRoutes(model, "@myapp/shared");
62
+
63
+ expect(routes.listRoute).toContain("export async function GET()");
64
+ expect(routes.listRoute).toContain("export async function POST(");
65
+ });
66
+
67
+ it("detail route has GET, PUT, and DELETE handlers", () => {
68
+ const model = createBasicModelInfo();
69
+ const routes = generateCompactBFFRoutes(model, "@myapp/shared");
70
+
71
+ expect(routes.detailRoute).toContain("export async function GET(");
72
+ expect(routes.detailRoute).toContain("export async function PUT(");
73
+ expect(routes.detailRoute).toContain("export async function DELETE(");
74
+ });
75
+
76
+ it("creates InputSchema that omits managed fields", () => {
77
+ const model = createBasicModelInfo();
78
+ const routes = generateCompactBFFRoutes(model, "@myapp/shared");
79
+
80
+ expect(routes.listRoute).toContain(
81
+ "todoSchema.omit({ id: true, createdAt: true, updatedAt: true })"
82
+ );
83
+ });
84
+
85
+ it("handles multi-word model names", () => {
86
+ const model = createBasicModelInfo({
87
+ name: "BlogPost",
88
+ schemaName: "blogPostSchema",
89
+ });
90
+ const routes = generateCompactBFFRoutes(model, "@myapp/shared");
91
+
92
+ expect(routes.listRoute).toContain("path: '/api/blogPost'");
93
+ expect(routes.listRoute).toContain(
94
+ "import { blogPostSchema } from '@myapp/shared'"
95
+ );
96
+ });
97
+ });
@@ -0,0 +1,43 @@
1
+ import { generateOpenApiDocument } from "../core/scaffold/openapi-generator";
2
+ import { createBasicModelInfo } from "./fixtures";
3
+
4
+ describe("generateOpenApiDocument", () => {
5
+ it("emits an OpenAPI document for the root model", () => {
6
+ const model = createBasicModelInfo();
7
+ const document = JSON.parse(generateOpenApiDocument([model], model));
8
+
9
+ expect(document.openapi).toBe("3.0.3");
10
+ expect(document.components.schemas.Todo.properties.title.type).toBe("string");
11
+ expect(document.paths["/api/todo"].post.requestBody.content["application/json"].schema.$ref)
12
+ .toBe("#/components/schemas/Todo");
13
+ });
14
+
15
+ it("emits nested schema references when present", () => {
16
+ const todo = createBasicModelInfo({
17
+ fields: [
18
+ { name: "id", type: "string", isOptional: false, isArray: false },
19
+ { name: "title", type: "string", isOptional: false, isArray: false },
20
+ {
21
+ name: "category",
22
+ type: "object",
23
+ isOptional: false,
24
+ isArray: false,
25
+ isNestedSchema: true,
26
+ nestedModelName: "Category",
27
+ },
28
+ ],
29
+ });
30
+ const category = createBasicModelInfo({
31
+ name: "Category",
32
+ schemaName: "categorySchema",
33
+ fields: [
34
+ { name: "id", type: "string", isOptional: false, isArray: false },
35
+ { name: "name", type: "string", isOptional: false, isArray: false },
36
+ ],
37
+ });
38
+
39
+ const document = JSON.parse(generateOpenApiDocument([todo, category], todo));
40
+ expect(document.components.schemas.Todo.properties.category.$ref).toBe("#/components/schemas/Category");
41
+ expect(document.components.schemas.Category.properties.name.type).toBe("string");
42
+ });
43
+ });