swallowkit 1.0.0-beta.54 → 1.0.0-beta.56

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 (138) hide show
  1. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  2. package/dist/cli/commands/add-auth.js +154 -157
  3. package/dist/cli/commands/add-auth.js.map +1 -1
  4. package/dist/cli/commands/init.d.ts +5 -0
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +178 -25
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/scaffold.d.ts +7 -2
  9. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  10. package/dist/cli/commands/scaffold.js +111 -123
  11. package/dist/cli/commands/scaffold.js.map +1 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +127 -2
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/core/config.d.ts +1 -1
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js +8 -4
  18. package/dist/core/config.js.map +1 -1
  19. package/dist/core/operations/auth-operations.d.ts +57 -0
  20. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  21. package/dist/core/operations/auth-operations.js +269 -0
  22. package/dist/core/operations/auth-operations.js.map +1 -0
  23. package/dist/core/operations/create-model.d.ts.map +1 -1
  24. package/dist/core/operations/create-model.js +23 -13
  25. package/dist/core/operations/create-model.js.map +1 -1
  26. package/dist/core/operations/file-session.d.ts +53 -0
  27. package/dist/core/operations/file-session.d.ts.map +1 -0
  28. package/dist/core/operations/file-session.js +195 -0
  29. package/dist/core/operations/file-session.js.map +1 -0
  30. package/dist/core/operations/provision-operations.d.ts +52 -0
  31. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  32. package/dist/core/operations/provision-operations.js +211 -0
  33. package/dist/core/operations/provision-operations.js.map +1 -0
  34. package/dist/core/operations/runtime.d.ts +5 -0
  35. package/dist/core/operations/runtime.d.ts.map +1 -1
  36. package/dist/core/operations/runtime.js +23 -0
  37. package/dist/core/operations/runtime.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  40. package/dist/core/operations/scaffold-apply.js +57 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -0
  42. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  43. package/dist/core/operations/scaffold-machine.js +1 -13
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  47. package/dist/core/operations/scaffold-plan.js +195 -0
  48. package/dist/core/operations/scaffold-plan.js.map +1 -0
  49. package/dist/core/project/artifacts.d.ts +58 -0
  50. package/dist/core/project/artifacts.d.ts.map +1 -0
  51. package/dist/core/project/artifacts.js +179 -0
  52. package/dist/core/project/artifacts.js.map +1 -0
  53. package/dist/core/project/boundaries.d.ts +50 -0
  54. package/dist/core/project/boundaries.d.ts.map +1 -0
  55. package/dist/core/project/boundaries.js +117 -0
  56. package/dist/core/project/boundaries.js.map +1 -0
  57. package/dist/core/project/drift.d.ts +29 -0
  58. package/dist/core/project/drift.d.ts.map +1 -0
  59. package/dist/core/project/drift.js +188 -0
  60. package/dist/core/project/drift.js.map +1 -0
  61. package/dist/core/project/infra.d.ts +49 -0
  62. package/dist/core/project/infra.d.ts.map +1 -0
  63. package/dist/core/project/infra.js +167 -0
  64. package/dist/core/project/infra.js.map +1 -0
  65. package/dist/core/project/state.d.ts +15 -0
  66. package/dist/core/project/state.d.ts.map +1 -0
  67. package/dist/core/project/state.js +106 -0
  68. package/dist/core/project/state.js.map +1 -0
  69. package/dist/core/project/workflows.d.ts +43 -0
  70. package/dist/core/project/workflows.d.ts.map +1 -0
  71. package/dist/core/project/workflows.js +255 -0
  72. package/dist/core/project/workflows.js.map +1 -0
  73. package/dist/core/scaffold/auth-generator.d.ts +1 -1
  74. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  75. package/dist/core/scaffold/auth-generator.js +108 -52
  76. package/dist/core/scaffold/auth-generator.js.map +1 -1
  77. package/dist/core/scaffold/functions-generator.js +9 -9
  78. package/dist/core/scaffold/nextjs-generator.js +6 -6
  79. package/dist/core/scaffold/ui-generator.js +19 -19
  80. package/dist/core/scaffold/ui-generator.js.map +1 -1
  81. package/dist/core/verify/index.d.ts +62 -0
  82. package/dist/core/verify/index.d.ts.map +1 -0
  83. package/dist/core/verify/index.js +343 -0
  84. package/dist/core/verify/index.js.map +1 -0
  85. package/dist/machine/contracts.d.ts +8 -0
  86. package/dist/machine/contracts.d.ts.map +1 -1
  87. package/dist/machine/errors.d.ts +4 -1
  88. package/dist/machine/errors.d.ts.map +1 -1
  89. package/dist/machine/errors.js +9 -1
  90. package/dist/machine/errors.js.map +1 -1
  91. package/dist/machine/index.d.ts.map +1 -1
  92. package/dist/machine/index.js +350 -16
  93. package/dist/machine/index.js.map +1 -1
  94. package/dist/mcp/index.d.ts.map +1 -1
  95. package/dist/mcp/index.js +202 -1
  96. package/dist/mcp/index.js.map +1 -1
  97. package/dist/types/index.d.ts +14 -0
  98. package/dist/types/index.d.ts.map +1 -1
  99. package/package.json +1 -1
  100. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
  101. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
  102. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
  103. package/src/__tests__/agent-loop-phase34.test.ts +449 -0
  104. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  105. package/src/__tests__/file-session.test.ts +98 -0
  106. package/src/__tests__/init.test.ts +50 -0
  107. package/src/__tests__/mcp.test.ts +113 -5
  108. package/src/__tests__/multi-auth.test.ts +1 -1
  109. package/src/__tests__/plan-apply.test.ts +294 -0
  110. package/src/cli/commands/add-auth.ts +164 -164
  111. package/src/cli/commands/init.ts +186 -24
  112. package/src/cli/commands/scaffold.ts +128 -129
  113. package/src/cli/index.ts +148 -2
  114. package/src/core/config.ts +5 -4
  115. package/src/core/operations/auth-operations.ts +353 -0
  116. package/src/core/operations/create-model.ts +25 -13
  117. package/src/core/operations/file-session.ts +210 -0
  118. package/src/core/operations/provision-operations.ts +297 -0
  119. package/src/core/operations/runtime.ts +23 -0
  120. package/src/core/operations/scaffold-apply.ts +106 -0
  121. package/src/core/operations/scaffold-machine.ts +2 -16
  122. package/src/core/operations/scaffold-plan.ts +224 -0
  123. package/src/core/project/artifacts.ts +205 -0
  124. package/src/core/project/boundaries.ts +170 -0
  125. package/src/core/project/drift.ts +202 -0
  126. package/src/core/project/infra.ts +196 -0
  127. package/src/core/project/state.ts +77 -0
  128. package/src/core/project/workflows.ts +249 -0
  129. package/src/core/scaffold/auth-generator.ts +119 -60
  130. package/src/core/scaffold/functions-generator.ts +9 -9
  131. package/src/core/scaffold/nextjs-generator.ts +6 -6
  132. package/src/core/scaffold/ui-generator.ts +19 -19
  133. package/src/core/verify/index.ts +392 -0
  134. package/src/machine/contracts.ts +16 -0
  135. package/src/machine/errors.ts +12 -1
  136. package/src/machine/index.ts +444 -15
  137. package/src/mcp/index.ts +198 -2
  138. package/src/types/index.ts +17 -0
@@ -0,0 +1,294 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { runMachineCli } from "../machine";
5
+
6
+ jest.mock("child_process", () => {
7
+ const actual = jest.requireActual<typeof import("child_process")>("child_process");
8
+ return {
9
+ ...actual,
10
+ spawn: jest.fn(),
11
+ spawnSync: jest.fn(),
12
+ };
13
+ });
14
+
15
+ const repoRoot = process.cwd();
16
+
17
+ function writeFile(filePath: string, content: string): void {
18
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
19
+ fs.writeFileSync(filePath, content, "utf-8");
20
+ }
21
+
22
+ function createModelSource(name: string, extraField = ""): string {
23
+ return `import { z } from 'zod/v4';
24
+
25
+ export const ${name} = z.object({
26
+ id: z.string(),
27
+ name: z.string().min(1),${extraField}
28
+ createdAt: z.string().optional(),
29
+ updatedAt: z.string().optional(),
30
+ });
31
+
32
+ export type ${name} = z.infer<typeof ${name}>;
33
+
34
+ export const displayName = '${name}';
35
+ `;
36
+ }
37
+
38
+ function createProjectFixture(rootDir: string): void {
39
+ writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
40
+ writeFile(
41
+ path.join(rootDir, "swallowkit.config.js"),
42
+ `module.exports = {
43
+ database: {
44
+ connectionString: 'AccountEndpoint=https://example.local;',
45
+ },
46
+ backend: {
47
+ language: 'typescript',
48
+ },
49
+ api: {
50
+ endpoint: '/api/_swallowkit',
51
+ },
52
+ };
53
+ `
54
+ );
55
+ writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
56
+ writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
57
+ writeFile(path.join(rootDir, "shared", "models", "todo.ts"), createModelSource("Todo"));
58
+ fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
59
+ fs.symlinkSync(
60
+ path.join(repoRoot, "node_modules", "zod"),
61
+ path.join(rootDir, "node_modules", "zod"),
62
+ "junction"
63
+ );
64
+ }
65
+
66
+ async function runMachine(argv: string[]): Promise<{ response: any; exitCode: number }> {
67
+ const writes: string[] = [];
68
+ const originalWrite = process.stdout.write.bind(process.stdout);
69
+ const originalExitCode = process.exitCode;
70
+
71
+ (process.stdout.write as unknown as (chunk: string | Uint8Array) => boolean) = ((chunk: string | Uint8Array) => {
72
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
73
+ return true;
74
+ }) as typeof process.stdout.write;
75
+
76
+ process.exitCode = 0;
77
+
78
+ try {
79
+ await runMachineCli(argv);
80
+ return {
81
+ response: JSON.parse(writes.join("")),
82
+ exitCode: process.exitCode || 0,
83
+ };
84
+ } finally {
85
+ process.stdout.write = originalWrite;
86
+ process.exitCode = originalExitCode;
87
+ }
88
+ }
89
+
90
+ function machineArgs(...args: string[]): string[] {
91
+ return ["node", "swallowkit", "machine", ...args];
92
+ }
93
+
94
+ describe("plan / apply / drift / verify machine commands", () => {
95
+ const originalCwd = process.cwd();
96
+ let tempDir: string;
97
+
98
+ beforeEach(() => {
99
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-agent-"));
100
+ process.chdir(tempDir);
101
+ });
102
+
103
+ afterEach(() => {
104
+ process.chdir(originalCwd);
105
+ fs.rmSync(tempDir, { recursive: true, force: true });
106
+ });
107
+
108
+ it("plans a scaffold without writing files and reports operations", async () => {
109
+ createProjectFixture(tempDir);
110
+
111
+ const { response, exitCode } = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
112
+
113
+ expect(exitCode).toBe(0);
114
+ expect(response.ok).toBe(true);
115
+ expect(response.command).toBe("plan-scaffold");
116
+ expect(response.status).toBe("complete");
117
+ expect(response.data.planId).toMatch(/^[0-9a-f]{12}$/);
118
+ expect(response.data.requiresApproval).toBe(false);
119
+ expect(response.data.operations).toEqual(
120
+ expect.arrayContaining([
121
+ expect.objectContaining({ path: "app/api/todo/route.ts", action: "create", ownership: "managed" }),
122
+ expect.objectContaining({ path: "functions/src/todo.ts", action: "create" }),
123
+ ])
124
+ );
125
+ expect(response.nextActions[0].command).toContain(`apply scaffold --plan ${response.data.planId}`);
126
+
127
+ // No generated files were written
128
+ expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
129
+ expect(fs.existsSync(path.join(tempDir, "functions"))).toBe(false);
130
+ // But the plan state was persisted
131
+ expect(
132
+ fs.existsSync(path.join(tempDir, ".swallowkit", "state", "plans", `${response.data.planId}.json`))
133
+ ).toBe(true);
134
+ });
135
+
136
+ it("applies a previously computed plan and records the artifact ledger", async () => {
137
+ createProjectFixture(tempDir);
138
+
139
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
140
+ const planId = plan.response.data.planId;
141
+
142
+ const { response, exitCode } = await runMachine(machineArgs("apply", "scaffold", "--plan", planId));
143
+
144
+ expect(exitCode).toBe(0);
145
+ expect(response.ok).toBe(true);
146
+ expect(response.command).toBe("apply-scaffold");
147
+ expect(response.status).toBe("complete");
148
+ expect(response.data.createdFiles).toEqual(
149
+ expect.arrayContaining(["app/api/todo/route.ts", "functions/src/todo.ts"])
150
+ );
151
+
152
+ // Artifact ledger created and tracks the generated files
153
+ const ledgerPath = path.join(tempDir, ".swallowkit", "artifacts.json");
154
+ expect(fs.existsSync(ledgerPath)).toBe(true);
155
+ const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf-8"));
156
+ expect(ledger.artifacts).toEqual(
157
+ expect.arrayContaining([
158
+ expect.objectContaining({ path: "app/api/todo/route.ts", ownership: "managed", sourceModel: "Todo" }),
159
+ ])
160
+ );
161
+
162
+ // Applied plan state is cleaned up
163
+ expect(fs.existsSync(path.join(tempDir, ".swallowkit", "state", "plans", `${planId}.json`))).toBe(false);
164
+ });
165
+
166
+ it("rejects a stale plan when files change after planning", async () => {
167
+ createProjectFixture(tempDir);
168
+
169
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
170
+ const planId = plan.response.data.planId;
171
+
172
+ // Model schema changes after the plan was computed
173
+ writeFile(
174
+ path.join(tempDir, "shared", "models", "todo.ts"),
175
+ createModelSource("Todo", "\n priority: z.number().optional(),")
176
+ );
177
+
178
+ const { response, exitCode } = await runMachine(machineArgs("apply", "scaffold", "--plan", planId));
179
+
180
+ expect(exitCode).toBe(1);
181
+ expect(response.ok).toBe(false);
182
+ expect(response.error.code).toBe("stale-plan");
183
+ expect(response.status).toBe("blocked");
184
+ expect(response.error.details.changedFiles).toContain("shared/models/todo.ts");
185
+ // Nothing was applied
186
+ expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
187
+ });
188
+
189
+ it("requires approval to overwrite files modified after generation, then applies with --approve", async () => {
190
+ createProjectFixture(tempDir);
191
+
192
+ await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
193
+
194
+ // User modifies a managed artifact
195
+ const routePath = path.join(tempDir, "app", "api", "todo", "route.ts");
196
+ const customized = `// customized by user\n${fs.readFileSync(routePath, "utf-8")}`;
197
+ fs.writeFileSync(routePath, customized, "utf-8");
198
+
199
+ const denied = await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
200
+
201
+ expect(denied.exitCode).toBe(1);
202
+ expect(denied.response.ok).toBe(false);
203
+ expect(denied.response.error.code).toBe("approval-required");
204
+ expect(denied.response.status).toBe("requires-human");
205
+ expect(denied.response.error.details.conflicts).toEqual(
206
+ expect.arrayContaining([
207
+ expect.objectContaining({
208
+ path: "app/api/todo/route.ts",
209
+ action: "overwrite",
210
+ conflictReason: "modified-after-generation",
211
+ }),
212
+ ])
213
+ );
214
+ // The customization is untouched
215
+ expect(fs.readFileSync(routePath, "utf-8")).toBe(customized);
216
+
217
+ const approved = await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only", "--approve"));
218
+
219
+ expect(approved.exitCode).toBe(0);
220
+ expect(approved.response.ok).toBe(true);
221
+ expect(approved.response.data.approvedConflicts).toEqual(
222
+ expect.arrayContaining([expect.objectContaining({ path: "app/api/todo/route.ts" })])
223
+ );
224
+ expect(fs.readFileSync(routePath, "utf-8")).not.toContain("customized by user");
225
+ });
226
+
227
+ it("inspects artifacts and detects drift after manual edits and schema changes", async () => {
228
+ createProjectFixture(tempDir);
229
+ await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
230
+
231
+ const artifacts = await runMachine(machineArgs("inspect", "artifacts"));
232
+ expect(artifacts.response.ok).toBe(true);
233
+ expect(artifacts.response.data.ledgerFound).toBe(true);
234
+ expect(artifacts.response.data.artifacts).toEqual(
235
+ expect.arrayContaining([
236
+ expect.objectContaining({ path: "functions/src/todo.ts", exists: true, modified: false }),
237
+ ])
238
+ );
239
+
240
+ // Clean project → no error/warning drift about artifacts
241
+ const cleanDrift = await runMachine(machineArgs("inspect", "drift"));
242
+ expect(cleanDrift.response.ok).toBe(true);
243
+ expect(
244
+ cleanDrift.response.data.findings.filter((f: any) => f.kind === "artifact-modified" && f.severity !== "info")
245
+ ).toEqual([]);
246
+
247
+ // Manual edit to a managed artifact + schema change
248
+ fs.appendFileSync(path.join(tempDir, "functions", "src", "todo.ts"), "\n// manual edit\n");
249
+ writeFile(
250
+ path.join(tempDir, "shared", "models", "todo.ts"),
251
+ createModelSource("Todo", "\n priority: z.number().optional(),")
252
+ );
253
+
254
+ const drift = await runMachine(machineArgs("inspect", "drift"));
255
+ expect(drift.response.ok).toBe(true);
256
+ expect(drift.response.data.findings).toEqual(
257
+ expect.arrayContaining([
258
+ expect.objectContaining({ kind: "artifact-modified", path: "functions/src/todo.ts", severity: "warning" }),
259
+ expect.objectContaining({ kind: "schema-drift", entity: "Todo" }),
260
+ ])
261
+ );
262
+ expect(drift.response.data.findings.every((f: any) => typeof f.repairAction === "string")).toBe(true);
263
+ });
264
+
265
+ it("verifies the project and explains failures", async () => {
266
+ createProjectFixture(tempDir);
267
+ await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
268
+
269
+ const verify = await runMachine(machineArgs("verify", "project", "--checks", "structure,drift"));
270
+
271
+ expect(verify.exitCode).toBe(0);
272
+ expect(verify.response.ok).toBe(true);
273
+ expect(verify.response.command).toBe("verify-project");
274
+ expect(verify.response.status).toBe("complete");
275
+ expect(verify.response.data.summary.done).toBe(true);
276
+ expect(verify.response.data.checks.map((check: any) => check.id)).toEqual(["structure", "drift"]);
277
+
278
+ // explain failure with no failing checks returns an empty list
279
+ const explain = await runMachine(machineArgs("explain", "failure"));
280
+ expect(explain.response.ok).toBe(true);
281
+ expect(explain.response.data.failures).toEqual([]);
282
+ });
283
+
284
+ it("returns no-verification-result when explain failure runs before verify", async () => {
285
+ createProjectFixture(tempDir);
286
+
287
+ const { response, exitCode } = await runMachine(machineArgs("explain", "failure"));
288
+
289
+ expect(exitCode).toBe(1);
290
+ expect(response.ok).toBe(false);
291
+ expect(response.error.code).toBe("no-verification-result");
292
+ expect(response.status).toBe("blocked");
293
+ });
294
+ });