swallowkit 1.0.0-beta.8 → 1.0.0

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 (272) hide show
  1. package/README.ja.md +87 -181
  2. package/README.md +87 -238
  3. package/dist/__tests__/fixtures.d.ts +12 -0
  4. package/dist/__tests__/fixtures.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures.js +101 -0
  6. package/dist/__tests__/fixtures.js.map +1 -1
  7. package/dist/cli/commands/add-auth.d.ts +15 -0
  8. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  9. package/dist/cli/commands/add-auth.js +790 -0
  10. package/dist/cli/commands/add-auth.js.map +1 -0
  11. package/dist/cli/commands/add-connector.d.ts +20 -0
  12. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  13. package/dist/cli/commands/add-connector.js +164 -0
  14. package/dist/cli/commands/add-connector.js.map +1 -0
  15. package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
  16. package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
  17. package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
  18. package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
  19. package/dist/cli/commands/create-model.d.ts +1 -4
  20. package/dist/cli/commands/create-model.d.ts.map +1 -1
  21. package/dist/cli/commands/create-model.js +20 -82
  22. package/dist/cli/commands/create-model.js.map +1 -1
  23. package/dist/cli/commands/dev-seeds.d.ts +27 -0
  24. package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
  25. package/dist/cli/commands/dev-seeds.js +188 -6
  26. package/dist/cli/commands/dev-seeds.js.map +1 -1
  27. package/dist/cli/commands/dev.d.ts +30 -0
  28. package/dist/cli/commands/dev.d.ts.map +1 -1
  29. package/dist/cli/commands/dev.js +495 -129
  30. package/dist/cli/commands/dev.js.map +1 -1
  31. package/dist/cli/commands/init.d.ts +70 -0
  32. package/dist/cli/commands/init.d.ts.map +1 -1
  33. package/dist/cli/commands/init.js +797 -768
  34. package/dist/cli/commands/init.js.map +1 -1
  35. package/dist/cli/commands/scaffold.d.ts +24 -3
  36. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  37. package/dist/cli/commands/scaffold.js +408 -189
  38. package/dist/cli/commands/scaffold.js.map +1 -1
  39. package/dist/cli/index.d.ts +5 -1
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +335 -58
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/core/config.d.ts +13 -3
  44. package/dist/core/config.d.ts.map +1 -1
  45. package/dist/core/config.js +151 -8
  46. package/dist/core/config.js.map +1 -1
  47. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  48. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  49. package/dist/core/mock/connector-mock-server.js +480 -0
  50. package/dist/core/mock/connector-mock-server.js.map +1 -0
  51. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  53. package/dist/core/mock/zod-mock-generator.js +163 -0
  54. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  55. package/dist/core/operations/auth-operations.d.ts +61 -0
  56. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  57. package/dist/core/operations/auth-operations.js +272 -0
  58. package/dist/core/operations/auth-operations.js.map +1 -0
  59. package/dist/core/operations/create-model.d.ts +15 -0
  60. package/dist/core/operations/create-model.d.ts.map +1 -0
  61. package/dist/core/operations/create-model.js +185 -0
  62. package/dist/core/operations/create-model.js.map +1 -0
  63. package/dist/core/operations/file-session.d.ts +56 -0
  64. package/dist/core/operations/file-session.d.ts.map +1 -0
  65. package/dist/core/operations/file-session.js +203 -0
  66. package/dist/core/operations/file-session.js.map +1 -0
  67. package/dist/core/operations/provision-operations.d.ts +52 -0
  68. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  69. package/dist/core/operations/provision-operations.js +211 -0
  70. package/dist/core/operations/provision-operations.js.map +1 -0
  71. package/dist/core/operations/runtime.d.ts +37 -0
  72. package/dist/core/operations/runtime.d.ts.map +1 -0
  73. package/dist/core/operations/runtime.js +248 -0
  74. package/dist/core/operations/runtime.js.map +1 -0
  75. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  76. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  77. package/dist/core/operations/scaffold-apply.js +57 -0
  78. package/dist/core/operations/scaffold-apply.js.map +1 -0
  79. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  80. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  81. package/dist/core/operations/scaffold-machine.js +51 -0
  82. package/dist/core/operations/scaffold-machine.js.map +1 -0
  83. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  84. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  85. package/dist/core/operations/scaffold-plan.js +187 -0
  86. package/dist/core/operations/scaffold-plan.js.map +1 -0
  87. package/dist/core/project/agent-instructions.d.ts +29 -0
  88. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  89. package/dist/core/project/agent-instructions.js +160 -0
  90. package/dist/core/project/agent-instructions.js.map +1 -0
  91. package/dist/core/project/artifacts.d.ts +58 -0
  92. package/dist/core/project/artifacts.d.ts.map +1 -0
  93. package/dist/core/project/artifacts.js +179 -0
  94. package/dist/core/project/artifacts.js.map +1 -0
  95. package/dist/core/project/boundaries.d.ts +50 -0
  96. package/dist/core/project/boundaries.d.ts.map +1 -0
  97. package/dist/core/project/boundaries.js +117 -0
  98. package/dist/core/project/boundaries.js.map +1 -0
  99. package/dist/core/project/capabilities.d.ts +58 -0
  100. package/dist/core/project/capabilities.d.ts.map +1 -0
  101. package/dist/core/project/capabilities.js +135 -0
  102. package/dist/core/project/capabilities.js.map +1 -0
  103. package/dist/core/project/drift.d.ts +29 -0
  104. package/dist/core/project/drift.d.ts.map +1 -0
  105. package/dist/core/project/drift.js +188 -0
  106. package/dist/core/project/drift.js.map +1 -0
  107. package/dist/core/project/infra.d.ts +49 -0
  108. package/dist/core/project/infra.d.ts.map +1 -0
  109. package/dist/core/project/infra.js +167 -0
  110. package/dist/core/project/infra.js.map +1 -0
  111. package/dist/core/project/manifest.d.ts +92 -0
  112. package/dist/core/project/manifest.d.ts.map +1 -0
  113. package/dist/core/project/manifest.js +321 -0
  114. package/dist/core/project/manifest.js.map +1 -0
  115. package/dist/core/project/state.d.ts +15 -0
  116. package/dist/core/project/state.d.ts.map +1 -0
  117. package/dist/core/project/state.js +106 -0
  118. package/dist/core/project/state.js.map +1 -0
  119. package/dist/core/project/validation.d.ts +20 -0
  120. package/dist/core/project/validation.d.ts.map +1 -0
  121. package/dist/core/project/validation.js +274 -0
  122. package/dist/core/project/validation.js.map +1 -0
  123. package/dist/core/project/workflows.d.ts +43 -0
  124. package/dist/core/project/workflows.d.ts.map +1 -0
  125. package/dist/core/project/workflows.js +299 -0
  126. package/dist/core/project/workflows.js.map +1 -0
  127. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  128. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  129. package/dist/core/scaffold/auth-generator.js +1774 -0
  130. package/dist/core/scaffold/auth-generator.js.map +1 -0
  131. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  132. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  133. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  134. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  135. package/dist/core/scaffold/functions-generator.d.ts +4 -3
  136. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  137. package/dist/core/scaffold/functions-generator.js +405 -134
  138. package/dist/core/scaffold/functions-generator.js.map +1 -1
  139. package/dist/core/scaffold/model-parser.d.ts +20 -0
  140. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  141. package/dist/core/scaffold/model-parser.js +229 -78
  142. package/dist/core/scaffold/model-parser.js.map +1 -1
  143. package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
  144. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  145. package/dist/core/scaffold/native-schema-generator.js +753 -0
  146. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  147. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  148. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  149. package/dist/core/scaffold/nextjs-generator.js +152 -8
  150. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  151. package/dist/core/scaffold/ui-generator.d.ts +17 -4
  152. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  153. package/dist/core/scaffold/ui-generator.js +174 -34
  154. package/dist/core/scaffold/ui-generator.js.map +1 -1
  155. package/dist/core/verify/index.d.ts +69 -0
  156. package/dist/core/verify/index.d.ts.map +1 -0
  157. package/dist/core/verify/index.js +412 -0
  158. package/dist/core/verify/index.js.map +1 -0
  159. package/dist/machine/contracts.d.ts +24 -0
  160. package/dist/machine/contracts.d.ts.map +1 -0
  161. package/dist/machine/contracts.js +3 -0
  162. package/dist/machine/contracts.js.map +1 -0
  163. package/dist/machine/errors.d.ts +14 -0
  164. package/dist/machine/errors.d.ts.map +1 -0
  165. package/dist/machine/errors.js +42 -0
  166. package/dist/machine/errors.js.map +1 -0
  167. package/dist/machine/index.d.ts +3 -0
  168. package/dist/machine/index.d.ts.map +1 -0
  169. package/dist/machine/index.js +566 -0
  170. package/dist/machine/index.js.map +1 -0
  171. package/dist/mcp/index.d.ts +25 -0
  172. package/dist/mcp/index.d.ts.map +1 -0
  173. package/dist/mcp/index.js +425 -0
  174. package/dist/mcp/index.js.map +1 -0
  175. package/dist/types/index.d.ts +105 -0
  176. package/dist/types/index.d.ts.map +1 -1
  177. package/dist/utils/package-manager.d.ts +7 -11
  178. package/dist/utils/package-manager.d.ts.map +1 -1
  179. package/dist/utils/package-manager.js +42 -17
  180. package/dist/utils/package-manager.js.map +1 -1
  181. package/dist/utils/python-uv.d.ts +21 -0
  182. package/dist/utils/python-uv.d.ts.map +1 -0
  183. package/dist/utils/python-uv.js +111 -0
  184. package/dist/utils/python-uv.js.map +1 -0
  185. package/dist/version.d.ts +2 -0
  186. package/dist/version.d.ts.map +1 -0
  187. package/dist/version.js +49 -0
  188. package/dist/version.js.map +1 -0
  189. package/package.json +15 -17
  190. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
  191. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
  192. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
  193. package/src/__tests__/agent-instructions.test.ts +237 -0
  194. package/src/__tests__/agent-loop-phase34.test.ts +513 -0
  195. package/src/__tests__/auth.test.ts +770 -0
  196. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  197. package/src/__tests__/config.test.ts +176 -3
  198. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  199. package/src/__tests__/connector-mock-server.test.ts +459 -0
  200. package/src/__tests__/connector-model-bff.test.ts +162 -0
  201. package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
  202. package/src/__tests__/dev-seeds.test.ts +124 -0
  203. package/src/__tests__/dev.test.ts +254 -0
  204. package/src/__tests__/file-session.test.ts +98 -0
  205. package/src/__tests__/fixtures.ts +101 -0
  206. package/src/__tests__/functions-generator.test.ts +137 -1
  207. package/src/__tests__/init.test.ts +386 -1
  208. package/src/__tests__/jest-globals.d.ts +8 -0
  209. package/src/__tests__/machine.test.ts +371 -0
  210. package/src/__tests__/mcp.test.ts +253 -0
  211. package/src/__tests__/model-parser.test.ts +142 -0
  212. package/src/__tests__/multi-auth.test.ts +160 -0
  213. package/src/__tests__/nextjs-generator.test.ts +8 -0
  214. package/src/__tests__/package-manager.test.ts +59 -4
  215. package/src/__tests__/plan-apply.test.ts +499 -0
  216. package/src/__tests__/python-uv.test.ts +48 -0
  217. package/src/__tests__/scaffold.test.ts +287 -24
  218. package/src/__tests__/tsconfig.json +15 -0
  219. package/src/__tests__/ui-generator.test.ts +104 -0
  220. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  221. package/src/cli/commands/add-auth.ts +853 -0
  222. package/src/cli/commands/add-connector.ts +159 -0
  223. package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
  224. package/src/cli/commands/create-model.ts +21 -100
  225. package/src/cli/commands/dev-seeds.ts +278 -7
  226. package/src/cli/commands/dev.ts +607 -160
  227. package/src/cli/commands/init.ts +880 -808
  228. package/src/cli/commands/scaffold.ts +531 -236
  229. package/src/cli/index.ts +338 -60
  230. package/src/core/config.ts +145 -9
  231. package/src/core/mock/connector-mock-server.ts +555 -0
  232. package/src/core/mock/zod-mock-generator.ts +205 -0
  233. package/src/core/operations/auth-operations.ts +360 -0
  234. package/src/core/operations/create-model.ts +191 -0
  235. package/src/core/operations/file-session.ts +222 -0
  236. package/src/core/operations/provision-operations.ts +297 -0
  237. package/src/core/operations/runtime.ts +258 -0
  238. package/src/core/operations/scaffold-apply.ts +106 -0
  239. package/src/core/operations/scaffold-machine.ts +77 -0
  240. package/src/core/operations/scaffold-plan.ts +215 -0
  241. package/src/core/project/agent-instructions.ts +167 -0
  242. package/src/core/project/agent-templates/AGENTS.md +504 -0
  243. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  244. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  245. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  246. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  247. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  248. package/src/core/project/artifacts.ts +205 -0
  249. package/src/core/project/boundaries.ts +170 -0
  250. package/src/core/project/capabilities.ts +193 -0
  251. package/src/core/project/drift.ts +202 -0
  252. package/src/core/project/infra.ts +196 -0
  253. package/src/core/project/manifest.ts +402 -0
  254. package/src/core/project/state.ts +77 -0
  255. package/src/core/project/validation.ts +295 -0
  256. package/src/core/project/workflows.ts +295 -0
  257. package/src/core/scaffold/auth-generator.ts +1824 -0
  258. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  259. package/src/core/scaffold/functions-generator.ts +432 -134
  260. package/src/core/scaffold/model-parser.ts +244 -90
  261. package/src/core/scaffold/native-schema-generator.ts +904 -0
  262. package/src/core/scaffold/nextjs-generator.ts +174 -9
  263. package/src/core/scaffold/ui-generator.ts +191 -36
  264. package/src/core/verify/index.ts +468 -0
  265. package/src/machine/contracts.ts +33 -0
  266. package/src/machine/errors.ts +45 -0
  267. package/src/machine/index.ts +690 -0
  268. package/src/mcp/index.ts +423 -0
  269. package/src/types/index.ts +137 -0
  270. package/src/utils/package-manager.ts +41 -19
  271. package/src/utils/python-uv.ts +96 -0
  272. package/src/version.ts +13 -0
@@ -0,0 +1,142 @@
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
+ afterEach(() => { jest.restoreAllMocks(); });
7
+
8
+ it.each(["zod", "zod/v4"])("executes TypeScript modules and nested imports with %s without fallback", async (zodImport) => {
9
+ const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
10
+ const warning = jest.spyOn(console, "warn").mockImplementation(() => {});
11
+ try {
12
+ fs.writeFileSync(path.join(tempDir, "leaf.ts"), `
13
+ import { z } from '${zodImport}';
14
+ export const displayName = 'Leaf';
15
+ export const leafSchema = z.object({ name: z.string() });
16
+ `);
17
+ fs.writeFileSync(path.join(tempDir, "nested.ts"), `
18
+ import { z } from '${zodImport}';
19
+ import { leafSchema } from './leaf';
20
+ export const displayName = 'Nested';
21
+ export const nestedSchema = z.object({ leaf: leafSchema });
22
+ `);
23
+ const modelPath = path.join(tempDir, "record.ts");
24
+ fs.writeFileSync(modelPath, `
25
+ import { z } from '${zodImport}';
26
+ import { nestedSchema } from './nested';
27
+ import { leafSchema as otherSchema } from './leaf';
28
+ interface Settings { inner: { count: number }; }
29
+ type Label = string;
30
+ const label: Label = 'https://example.com';
31
+ const statuses = ['draft', 'active'] as const;
32
+ export const recordSchema = z.object({
33
+ id: z.string(), title: z.string().default(label), count: z.number(), enabled: z.boolean(),
34
+ optional: z.string().optional(), nullable: z.number().nullable(), status: z.enum(statuses),
35
+ numbers: z.array(z.number()), nested: nestedSchema, others: z.array(otherSchema),
36
+ inline: z.object({ name: z.string() }), tenantId: z.string(),
37
+ });
38
+ export type Record = z.infer<typeof recordSchema>;
39
+ export const displayName = 'Record';
40
+ export const partitionKey = '/tenantId';
41
+ export const connectorConfig = { connector: 'catalog', table: 'records', operations: ['read'] } as const;
42
+ export const authPolicy = { read: ['reader'], write: ['editor'] } as const;
43
+ `);
44
+ const model = await parseModelFile(modelPath);
45
+ expect(warning).not.toHaveBeenCalled();
46
+ expect(model.fields).toHaveLength(12);
47
+ expect(model.fields).toEqual(expect.arrayContaining([
48
+ expect.objectContaining({ name: "count", type: "number" }),
49
+ expect.objectContaining({ name: "enabled", type: "boolean" }),
50
+ expect.objectContaining({ name: "title", isOptional: true }),
51
+ expect.objectContaining({ name: "optional", isOptional: true }),
52
+ expect.objectContaining({ name: "nullable", isNullable: true }),
53
+ expect.objectContaining({ name: "status", enumValues: ["draft", "active"] }),
54
+ expect.objectContaining({ name: "numbers", type: "number", isArray: true }),
55
+ expect.objectContaining({ name: "nested", type: "object" }),
56
+ expect.objectContaining({ name: "others", type: "object", isArray: true }),
57
+ ]));
58
+ expect(model.connectorConfig).toEqual(expect.objectContaining({ connector: "catalog", table: "records", operations: ["read"] }));
59
+ expect(model.partitionKey).toBe("/tenantId");
60
+ expect(model.authPolicy).toEqual({ read: ["reader"], write: ["editor"] });
61
+ expect(fs.readdirSync(tempDir).some(name => name.startsWith(".swallowkit-parser-"))).toBe(false);
62
+ } finally {
63
+ fs.rmSync(tempDir, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ it("preserves array and enum metadata for schemas with defaults", async () => {
68
+ const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
69
+ const modelPath = path.join(tempDir, "product.ts");
70
+
71
+ try {
72
+ fs.writeFileSync(
73
+ modelPath,
74
+ `import { z } from 'zod/v4';
75
+
76
+ export const Product = z.object({
77
+ id: z.string(),
78
+ name: z.string().min(1),
79
+ price: z.number().min(0),
80
+ tags: z.array(z.string()).default([]),
81
+ status: z.enum(['draft', 'active', 'archived']).default('draft'),
82
+ createdAt: z.string().optional(),
83
+ updatedAt: z.string().optional(),
84
+ });
85
+
86
+ export type Product = z.infer<typeof Product>;
87
+ `,
88
+ "utf-8"
89
+ );
90
+
91
+ const model = await parseModelFile(modelPath);
92
+ expect(model.fields).toEqual(
93
+ expect.arrayContaining([
94
+ expect.objectContaining({
95
+ name: "tags",
96
+ type: "string",
97
+ isArray: true,
98
+ isOptional: true,
99
+ }),
100
+ expect.objectContaining({
101
+ name: "status",
102
+ type: "string",
103
+ isArray: false,
104
+ isOptional: true,
105
+ enumValues: ["draft", "active", "archived"],
106
+ }),
107
+ ])
108
+ );
109
+ } finally {
110
+ fs.rmSync(tempDir, { recursive: true, force: true });
111
+ }
112
+ });
113
+
114
+ it("distinguishes optional and nullable fields", async () => {
115
+ const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
116
+ const modelPath = path.join(tempDir, "coupon.ts");
117
+
118
+ try {
119
+ fs.writeFileSync(
120
+ modelPath,
121
+ `import { z } from 'zod/v4';
122
+
123
+ export const couponSchema = z.object({
124
+ requiredUserId: z.string(),
125
+ optionalUserId: z.string().optional(),
126
+ nullableUserId: z.string().nullable(),
127
+ });
128
+ `,
129
+ "utf-8"
130
+ );
131
+
132
+ const model = await parseModelFile(modelPath);
133
+ expect(model.fields).toEqual(expect.arrayContaining([
134
+ expect.objectContaining({ name: "requiredUserId", isOptional: false, isNullable: false }),
135
+ expect.objectContaining({ name: "optionalUserId", isOptional: true, isNullable: false }),
136
+ expect.objectContaining({ name: "nullableUserId", isOptional: false, isNullable: true }),
137
+ ]));
138
+ } finally {
139
+ fs.rmSync(tempDir, { recursive: true, force: true });
140
+ }
141
+ });
142
+ });
@@ -0,0 +1,160 @@
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 { createUIAuthOptions } from "../cli/commands/scaffold";
6
+ import {
7
+ generateBFFCallFunctionWithMultipleAuth,
8
+ generateNamedAuthRouterCSharp,
9
+ generateNamedAuthRouterPython,
10
+ generateNamedAuthRouterTS,
11
+ } from "../core/scaffold/auth-generator";
12
+ import { normalizeAuthConfig } from "../core/config";
13
+ import { parseAuthPolicy } from "../core/scaffold/model-parser";
14
+ import { createBasicModelInfo } from "./fixtures";
15
+ import { generateCSharpAzureFunctionsCRUD, generateCompactAzureFunctionsCRUD, generatePythonAzureFunctionsCRUD } from "../core/scaffold/functions-generator";
16
+ import { generateAuthLayout, generateNewPage } from "../core/scaffold/ui-generator";
17
+ import { AuthConfig } from "../types";
18
+
19
+ const namedAuthConfig: AuthConfig = {
20
+ schemes: {
21
+ admin: { provider: "swa" },
22
+ lineUser: { provider: "external-token" },
23
+ },
24
+ authorization: {
25
+ defaultPolicy: "anonymous",
26
+ policies: {
27
+ adminOnly: { schemes: ["admin"], roles: ["authenticated"] },
28
+ lineUserOnly: { schemes: ["lineUser"], roles: ["authenticated"] },
29
+ },
30
+ },
31
+ };
32
+ const auth = normalizeAuthConfig(namedAuthConfig)!;
33
+
34
+ describe("named authentication routers", () => {
35
+ it("generates deterministic TypeScript policy routing and normalized principals", () => {
36
+ const code = generateNamedAuthRouterTS(auth);
37
+ expect(code).toContain("policy.schemes.filter");
38
+ expect(code).toContain("candidates.length !== 1");
39
+ expect(code).toContain("subject: value.userId");
40
+ expect(code).toContain("scheme + ':' + principal.subject");
41
+ expect(code).toContain("new AuthError(403");
42
+ expect(code).toContain("new AuthError(503");
43
+ expect(code).not.toContain("JWT_SECRET");
44
+ });
45
+
46
+ it("generates equivalent C# and Python named guards", () => {
47
+ const csharp = generateNamedAuthRouterCSharp(auth);
48
+ const python = generateNamedAuthRouterPython(auth);
49
+ expect(csharp).toContain("record AuthPrincipal(string Subject, string Scheme, string Issuer");
50
+ expect(csharp).toContain("candidates.Length != 1");
51
+ expect(csharp).toContain("HttpStatusCode.Forbidden");
52
+ expect(python).toContain('principal["scheme"] + ":" + principal["subject"]');
53
+ expect(python).toContain("len(candidates) != 1");
54
+ expect(python).toContain("AuthError(503");
55
+ });
56
+
57
+ it("generates valid branch structure for an SWA-only Python router", () => {
58
+ const swaOnly = normalizeAuthConfig({ schemes: { admin: { provider: "swa", swa: { allowedProviders: ["github"] } } }, authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } } })!;
59
+ const python = generateNamedAuthRouterPython(swaOnly);
60
+ expect(python).toContain(' if PROVIDERS[scheme] == "swa":');
61
+ expect(python).not.toContain(' elif PROVIDERS[scheme] == "swa":');
62
+ expect(python).toContain("ALLOWED_PROVIDERS");
63
+ expect(generateNamedAuthRouterCSharp(swaOnly)).toContain("AllowedProviders[scheme]");
64
+ });
65
+
66
+ it("emits Array.Empty<string>() instead of untyped new[] { } for empty C# arrays (CS0826)", () => {
67
+ const emptyLists = normalizeAuthConfig({
68
+ schemes: { api: { provider: "external-token" } },
69
+ authorization: { defaultPolicy: "anonymous", policies: { apiOnly: { schemes: ["api"] } } },
70
+ })!;
71
+ const csharp = generateNamedAuthRouterCSharp(emptyLists);
72
+ expect(csharp).not.toMatch(/new\[\]\s*\{\s*\}/);
73
+ expect(csharp).toContain("Array.Empty<string>()");
74
+ // 非空のリストは従来通り new[] リテラル
75
+ expect(csharp).toContain('new[] { "api" }');
76
+ });
77
+
78
+ it("scaffolds named read/write guards for all Functions languages", () => {
79
+ const policy = { read: "adminOnly", write: "lineUserOnly" };
80
+ const model = createBasicModelInfo();
81
+ const ts = generateCompactAzureFunctionsCRUD(model, "@fixture/shared", policy);
82
+ const cs = generateCSharpAzureFunctionsCRUD(model, policy);
83
+ const py = generatePythonAzureFunctionsCRUD(model, policy).blueprint;
84
+ expect(ts).toContain("./auth/auth-router");
85
+ expect(ts).toContain('requireAuth(request, "adminOnly")');
86
+ expect(ts).toContain('requireAuth(request, "lineUserOnly")');
87
+ expect(cs).toContain('AuthRouter.Authorize(request, "adminOnly")');
88
+ expect(cs).toContain('AuthRouter.Authorize(request, "lineUserOnly")');
89
+ expect(py).toContain("from auth.auth_router import");
90
+ expect(py).toContain('require_auth(req, "adminOnly")');
91
+ expect(py).toContain('require_auth(req, "lineUserOnly")');
92
+ });
93
+
94
+ it("forwards both credential sources and the Functions host key", () => {
95
+ const code = generateBFFCallFunctionWithMultipleAuth();
96
+ expect(code).toContain("fetchHeaders.Authorization");
97
+ expect(code).toContain("fetchHeaders['x-ms-client-principal']");
98
+ expect(code).toContain("'x-functions-key'");
99
+ expect(code).not.toMatch(/console\.log\([^\n]*(authorization|principal|cookie)/i);
100
+ });
101
+
102
+ it("parses model-wide and operation-specific named policies", () => {
103
+ expect(parseAuthPolicy("export const authPolicy = { policy: 'adminOnly' };"))
104
+ .toEqual({ policy: "adminOnly" });
105
+ expect(parseAuthPolicy("export const authPolicy = { read: 'adminOnly', write: 'lineUserOnly' };"))
106
+ .toEqual({ read: "adminOnly", write: "lineUserOnly" });
107
+ });
108
+
109
+ it("resolves the same named auth context for the model layout and CRUD pages", () => {
110
+ const model = createBasicModelInfo({ name: "Product", authPolicy: { policy: "adminOnly" } });
111
+ const options = createUIAuthOptions(model.authPolicy!, namedAuthConfig);
112
+ const layout = generateAuthLayout(model, options);
113
+ const page = generateNewPage(model, options);
114
+
115
+ expect(options.authContextImport).toBe("@/lib/auth/schemes/admin/auth-context");
116
+ expect(layout).toContain("AuthProvider } from '@/lib/auth/schemes/admin/auth-context'");
117
+ expect(page).toContain("useAuth } from '@/lib/auth/schemes/admin/auth-context'");
118
+ });
119
+ });
120
+
121
+ describe("add-auth named scheme CLI", () => {
122
+ const originalCwd = process.cwd();
123
+ let root: string;
124
+
125
+ beforeEach(() => {
126
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-multi-auth-"));
127
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
128
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" }, auth: { schemes: {}, authorization: { defaultPolicy: "anonymous", policies: {} } } }, null, 2));
129
+ process.chdir(root);
130
+ });
131
+
132
+ afterEach(() => {
133
+ process.chdir(originalCwd);
134
+ fs.rmSync(root, { recursive: true, force: true });
135
+ });
136
+
137
+ it("adds two schemes without overwriting the first verifier", async () => {
138
+ await addAuthCommand({ scheme: "admin", provider: "swa" });
139
+ await addAuthCommand({ scheme: "lineUser", provider: "external-token" });
140
+ const verifier = path.join(root, "functions", "src", "auth", "schemes", "line-user", "verifier.ts");
141
+ fs.writeFileSync(verifier, "// user verifier\n", "utf-8");
142
+ const config = JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8"));
143
+ expect(config.auth.schemes.admin.provider).toBe("swa");
144
+ expect(config.auth.schemes.lineUser.provider).toBe("external-token");
145
+ await expect(addAuthCommand({ scheme: "lineUser", provider: "external-token" })).rejects.toThrow("already exists");
146
+ expect(fs.readFileSync(verifier, "utf-8")).toBe("// user verifier\n");
147
+ expect(fs.readFileSync(path.join(root, "lib", "api", "call-function.ts"), "utf-8")).toContain("x-functions-key");
148
+ });
149
+
150
+ it("updates a JavaScript config that already has a trailing comma", async () => {
151
+ fs.rmSync(path.join(root, "swallowkit.config.json"));
152
+ fs.writeFileSync(path.join(root, "swallowkit.config.js"), "module.exports = {\n backend: { language: 'typescript' },\n api: { endpoint: '/api/test' },\n};\n");
153
+ await addAuthCommand({ scheme: "admin", provider: "swa" });
154
+ const content = fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8");
155
+ expect(content).not.toContain(",,");
156
+ const configModule = { exports: {} as Record<string, unknown> };
157
+ new Function("module", content)(configModule);
158
+ expect((configModule.exports as { auth: { schemes: { admin: { provider: string } } } }).auth.schemes.admin.provider).toBe("swa");
159
+ });
160
+ });
@@ -21,6 +21,14 @@ describe("generateBFFCallFunction", () => {
21
21
  expect(code).toContain("export async function callFunction");
22
22
  });
23
23
 
24
+ it("uses a server-side Functions key and fails closed outside localhost", () => {
25
+ const code = generateBFFCallFunction();
26
+ expect(code).toContain("BACKEND_FUNCTIONS_KEY");
27
+ expect(code).toContain("'x-functions-key'");
28
+ expect(code).toContain("required for non-local Functions calls");
29
+ expect(code).not.toContain("?code=");
30
+ });
31
+
24
32
  it("includes z.ZodSchema typings", () => {
25
33
  const code = generateBFFCallFunction();
26
34
  expect(code).toContain("z.ZodSchema");
@@ -1,14 +1,41 @@
1
1
  import {
2
+ detectFromUserAgent,
2
3
  getCommands,
3
4
  getWorkspaceConfig,
4
5
  getCiSetupStep,
6
+ getCiInstallCommand,
5
7
  getAzurePipelinesSetup,
6
8
  getBuildScript,
9
+ getSwaAppBuildCommand,
7
10
  getFunctionsPrestart,
8
11
  getFunctionsStartScript,
9
12
  spawnArgs,
10
13
  } from "../utils/package-manager";
11
14
 
15
+ describe("detectFromUserAgent", () => {
16
+ const originalAgent = process.env.npm_config_user_agent;
17
+ beforeEach(() => { delete process.env.npm_config_user_agent; });
18
+ afterEach(() => {
19
+ if (originalAgent === undefined) delete process.env.npm_config_user_agent;
20
+ else process.env.npm_config_user_agent = originalAgent;
21
+ });
22
+ it("honors npm invocation even with pnpm installed", () => {
23
+ process.env.npm_config_user_agent = "npm/11.0.0 node/v22.14.0";
24
+ expect(detectFromUserAgent(() => true)).toBe("npm");
25
+ });
26
+ it("honors pnpm invocation", () => {
27
+ process.env.npm_config_user_agent = "pnpm/11.26.0 npm/? node/v22.14.0";
28
+ expect(detectFromUserAgent(() => false)).toBe("pnpm");
29
+ });
30
+ it("falls back to npm when pnpm is not installed", () => {
31
+ expect(detectFromUserAgent(() => false)).toBe("npm");
32
+ });
33
+
34
+ it("prefers pnpm when it is installed", () => {
35
+ expect(detectFromUserAgent(() => true)).toBe("pnpm");
36
+ });
37
+ });
38
+
12
39
  describe("getCommands", () => {
13
40
  describe("pnpm", () => {
14
41
  const cmds = getCommands("pnpm");
@@ -42,7 +69,7 @@ describe("getCommands", () => {
42
69
  });
43
70
 
44
71
  it("returns correct runFilter for workspace", () => {
45
- expect(cmds.runFilter("shared")).toBe("pnpm run --filter shared");
72
+ expect(cmds.runFilter("shared")).toBe("pnpm --filter shared run");
46
73
  });
47
74
 
48
75
  it("returns --use-pnpm flag for create-next-app", () => {
@@ -85,8 +112,8 @@ describe("getCommands", () => {
85
112
  expect(cmds.runFilter("shared")).toBe("npm run --workspace=shared");
86
113
  });
87
114
 
88
- it("returns null for create-next-app flag", () => {
89
- expect(cmds.createNextAppFlag).toBeNull();
115
+ it("explicitly selects npm for create-next-app", () => {
116
+ expect(cmds.createNextAppFlag).toBe("--use-npm");
90
117
  });
91
118
  });
92
119
  });
@@ -100,6 +127,9 @@ describe("getWorkspaceConfig", () => {
100
127
  expect(config.content).toContain("packages:");
101
128
  expect(config.content).toContain(" - packages/*");
102
129
  expect(config.content).toContain(" - apps/*");
130
+ expect(config.content).toContain("protobufjs: false");
131
+ expect(config.content).toContain("unrs-resolver: false");
132
+ expect(config.content).not.toContain(": true");
103
133
  }
104
134
  });
105
135
 
@@ -117,6 +147,7 @@ describe("getCiSetupStep", () => {
117
147
  it("returns pnpm setup step for pnpm", () => {
118
148
  const step = getCiSetupStep("pnpm");
119
149
  expect(step).toContain("pnpm/action-setup");
150
+ expect(step).toContain("version: 11");
120
151
  });
121
152
 
122
153
  it("returns empty string for npm", () => {
@@ -124,6 +155,30 @@ describe("getCiSetupStep", () => {
124
155
  });
125
156
  });
126
157
 
158
+ describe("getSwaAppBuildCommand", () => {
159
+ it("returns a pnpm build command for pnpm projects", () => {
160
+ expect(getSwaAppBuildCommand("pnpm")).toBe("pnpm install --frozen-lockfile && pnpm run build");
161
+ });
162
+
163
+ it("returns an npm build command for npm projects", () => {
164
+ expect(getSwaAppBuildCommand("npm")).toBe("npm ci && npm run build");
165
+ });
166
+
167
+ it("uses npm ci for pnpm-initialized projects once an npm lockfile is generated", () => {
168
+ expect(getSwaAppBuildCommand("npm", "pnpm")).toBe("npm ci && npm run build");
169
+ });
170
+ });
171
+
172
+ describe("getCiInstallCommand", () => {
173
+ it("returns npm ci for npm projects", () => {
174
+ expect(getCiInstallCommand("npm", "npm")).toBe("npm ci");
175
+ });
176
+
177
+ it("returns npm ci for pnpm projects once an npm lockfile is generated", () => {
178
+ expect(getCiInstallCommand("npm", "pnpm")).toBe("npm ci");
179
+ });
180
+ });
181
+
127
182
  describe("getAzurePipelinesSetup", () => {
128
183
  it("returns corepack step for pnpm", () => {
129
184
  const step = getAzurePipelinesSetup("pnpm");
@@ -138,7 +193,7 @@ describe("getAzurePipelinesSetup", () => {
138
193
 
139
194
  describe("getBuildScript", () => {
140
195
  it("uses pnpm run --filter for pnpm", () => {
141
- expect(getBuildScript("pnpm")).toContain("pnpm run --filter shared build");
196
+ expect(getBuildScript("pnpm")).toContain("pnpm --filter shared run build");
142
197
  expect(getBuildScript("pnpm")).toContain("fs.cpSync");
143
198
  });
144
199