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
@@ -1,16 +1,35 @@
1
1
  import { Command } from 'commander';
2
2
  import { spawn, ChildProcess } from 'child_process';
3
+ import * as http from 'http';
4
+ import * as https from 'https';
3
5
  import * as path from 'path';
4
6
  import * as fs from 'fs';
5
7
  import * as os from 'os';
8
+ import * as net from 'net';
6
9
  import { CosmosClient, PartitionKeyKind } from '@azure/cosmos';
7
- import { ensureSwallowKitProject, getBackendLanguage } from '../../core/config';
10
+ import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig, normalizeAuthConfig } from '../../core/config';
8
11
  import { ModelInfo } from '../../core/scaffold/model-parser';
9
- import { applyDevSeedEnvironment, getContainerNameForModel, loadProjectModels } from './dev-seeds';
12
+ import {
13
+ applyDevSeedEnvironment,
14
+ getContainerNameForModel,
15
+ getDefaultCosmosDatabaseName,
16
+ loadProjectModels,
17
+ resolveLocalCosmosConnectionInfo,
18
+ } from './dev-seeds';
10
19
  import { BackendLanguage } from '../../types';
11
20
  import { detectFromProject, getCommands } from '../../utils/package-manager';
12
-
13
- interface DevOptions {
21
+ import {
22
+ buildProjectLocalUvEnv,
23
+ buildProjectLocalUvInstallerEnv,
24
+ buildUvPipInstallArgs,
25
+ buildUvVenvArgs,
26
+ getProjectLocalUvInstallerCommand,
27
+ getProjectLocalUvPaths,
28
+ getPythonProjectRoot,
29
+ } from '../../utils/python-uv';
30
+ import { ConnectorMockServer } from '../../core/mock/connector-mock-server';
31
+
32
+ export interface DevOptions {
14
33
  port?: string;
15
34
  functionsPort?: string;
16
35
  host?: string;
@@ -18,17 +37,137 @@ interface DevOptions {
18
37
  verbose?: boolean;
19
38
  noFunctions?: boolean;
20
39
  seedEnv?: string;
40
+ mockConnectors?: boolean;
41
+ swaPort?: string;
42
+ noSwa?: boolean;
43
+ }
44
+
45
+ type ParsedDevActionOptions = DevOptions & {
46
+ functions?: boolean;
47
+ swa?: boolean;
48
+ };
49
+
50
+ interface FunctionsCoreToolsCommand {
51
+ command: string;
52
+ argsPrefix: string[];
53
+ label: string;
54
+ }
55
+
56
+ const MINIMUM_CSHARP_CORE_TOOLS_VERSION = '4.6.0';
57
+ const NPM_CORE_TOOLS_PACKAGE = 'azure-functions-core-tools@4';
58
+
59
+ function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions {
60
+ return {
61
+ ...options,
62
+ noFunctions: options.noFunctions ?? options.functions === false,
63
+ noSwa: options.noSwa ?? options.swa === false,
64
+ };
21
65
  }
22
66
 
23
67
  export function buildFunctionsStartArgs(functionsPort: string): string[] {
24
68
  return ['start', '--port', functionsPort];
25
69
  }
26
70
 
71
+ export function parseCoreToolsVersion(output: string): string | null {
72
+ const match = output.match(/\d+\.\d+\.\d+/);
73
+ return match ? match[0] : null;
74
+ }
75
+
76
+ export function compareVersionNumbers(left: string, right: string): number {
77
+ const leftParts = left.split('.').map((value) => Number.parseInt(value, 10));
78
+ const rightParts = right.split('.').map((value) => Number.parseInt(value, 10));
79
+ const length = Math.max(leftParts.length, rightParts.length);
80
+
81
+ for (let index = 0; index < length; index += 1) {
82
+ const leftPart = leftParts[index] ?? 0;
83
+ const rightPart = rightParts[index] ?? 0;
84
+
85
+ if (leftPart !== rightPart) {
86
+ return leftPart - rightPart;
87
+ }
88
+ }
89
+
90
+ return 0;
91
+ }
92
+
93
+ export function buildFunctionsCoreToolsCommand(
94
+ backendLanguage: BackendLanguage,
95
+ installedVersion: string | null
96
+ ): FunctionsCoreToolsCommand {
97
+ if (
98
+ backendLanguage === 'csharp' &&
99
+ (!installedVersion || compareVersionNumbers(installedVersion, MINIMUM_CSHARP_CORE_TOOLS_VERSION) < 0)
100
+ ) {
101
+ const reason = installedVersion
102
+ ? `installed func ${installedVersion} is too old for C# isolated`
103
+ : 'func is not installed';
104
+
105
+ return {
106
+ command: 'npm',
107
+ argsPrefix: ['exec', '--yes', NPM_CORE_TOOLS_PACKAGE, '--'],
108
+ label: `npm exec ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
109
+ };
110
+ }
111
+
112
+ return {
113
+ command: 'func',
114
+ argsPrefix: [],
115
+ label: installedVersion ? `func ${installedVersion}` : 'func',
116
+ };
117
+ }
118
+
27
119
  export function buildNextDevArgs(pm: string, port: string): string[] {
28
120
  const baseArgs = ['next', 'dev', '--port', port, '--webpack'];
29
121
  return pm === 'pnpm' ? ['exec', ...baseArgs] : baseArgs;
30
122
  }
31
123
 
124
+ export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: string): string {
125
+ return `http://${host || 'localhost'}:${functionsPort}`;
126
+ }
127
+
128
+ export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string): string[] {
129
+ return [
130
+ 'start',
131
+ `http://${host || 'localhost'}:${nextPort}`,
132
+ '--swa-config-location',
133
+ '.',
134
+ '--port',
135
+ swaPort,
136
+ ];
137
+ }
138
+
139
+ export function getSwaCliInstallCommand(pm: string): string {
140
+ return pm === 'pnpm'
141
+ ? 'pnpm add -Dw @azure/static-web-apps-cli'
142
+ : 'npm install -D @azure/static-web-apps-cli';
143
+ }
144
+
145
+ export function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number {
146
+ return backendLanguage === 'csharp' ? 90_000 : 30_000;
147
+ }
148
+
149
+ export async function waitForHttpServerReady(
150
+ url: string,
151
+ timeoutMs = 30_000,
152
+ intervalMs = 500
153
+ ): Promise<boolean> {
154
+ const deadline = Date.now() + timeoutMs;
155
+
156
+ while (Date.now() <= deadline) {
157
+ if (await probeHttpServer(url)) {
158
+ return true;
159
+ }
160
+
161
+ if (Date.now() + intervalMs > deadline) {
162
+ break;
163
+ }
164
+
165
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
166
+ }
167
+
168
+ return probeHttpServer(url);
169
+ }
170
+
32
171
  export function getPythonVirtualEnvPaths(functionsDir: string): {
33
172
  venvDir: string;
34
173
  binDir: string;
@@ -45,6 +184,13 @@ export function getPythonVirtualEnvPaths(functionsDir: string): {
45
184
  return { venvDir, binDir, pythonExecutable };
46
185
  }
47
186
 
187
+ export function getCSharpFunctionsBuildArtifactPaths(functionsDir: string): string[] {
188
+ return [
189
+ path.join(functionsDir, 'bin'),
190
+ path.join(functionsDir, 'obj'),
191
+ ];
192
+ }
193
+
48
194
  export function buildPythonFunctionsEnv(baseEnv: NodeJS.ProcessEnv, functionsDir: string): NodeJS.ProcessEnv {
49
195
  const { venvDir, binDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
50
196
  const pathKey = getPathEnvKey(baseEnv);
@@ -58,6 +204,16 @@ export function buildPythonFunctionsEnv(baseEnv: NodeJS.ProcessEnv, functionsDir
58
204
  };
59
205
  }
60
206
 
207
+ export function buildFunctionsCosmosEnvironment(
208
+ baseEnv: NodeJS.ProcessEnv,
209
+ databaseName: string
210
+ ): NodeJS.ProcessEnv {
211
+ return {
212
+ ...baseEnv,
213
+ COSMOS_DB_DATABASE_NAME: databaseName,
214
+ };
215
+ }
216
+
61
217
  function getPathEnvKey(env: NodeJS.ProcessEnv): string {
62
218
  return Object.keys(env).find((key) => key.toUpperCase() === 'PATH') || 'PATH';
63
219
  }
@@ -79,10 +235,20 @@ async function checkCoreTools(): Promise<boolean> {
79
235
  return checkCommand('func', ['--version']);
80
236
  }
81
237
 
82
- async function checkCommand(command: string, args: string[] = ['--version']): Promise<boolean> {
238
+ async function checkCommand(
239
+ command: string,
240
+ args: string[] = ['--version'],
241
+ options?: {
242
+ cwd?: string;
243
+ env?: NodeJS.ProcessEnv;
244
+ shell?: boolean;
245
+ }
246
+ ): Promise<boolean> {
83
247
  return new Promise((resolve) => {
84
248
  const checkProcess = spawn(command, args, {
85
- shell: false,
249
+ cwd: options?.cwd,
250
+ env: options?.env ?? process.env,
251
+ shell: options?.shell ?? true,
86
252
  stdio: 'pipe',
87
253
  });
88
254
 
@@ -96,26 +262,34 @@ async function checkCommand(command: string, args: string[] = ['--version']): Pr
96
262
  });
97
263
  }
98
264
 
99
- async function resolvePythonBootstrapCommand(): Promise<{ command: string; argsPrefix: string[]; label: string }> {
100
- const candidates = process.platform === 'win32'
101
- ? [
102
- { command: 'py', argsPrefix: ['-3.11'], label: 'py -3.11' },
103
- { command: 'python', argsPrefix: [], label: 'python' },
104
- ]
105
- : [
106
- { command: 'python3', argsPrefix: [], label: 'python3' },
107
- { command: 'python', argsPrefix: [], label: 'python' },
108
- ];
109
-
110
- for (const candidate of candidates) {
111
- if (await checkCommand(candidate.command, [...candidate.argsPrefix, '--version'])) {
112
- return candidate;
113
- }
265
+ async function resolveProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
266
+ const uvEnv = buildProjectLocalUvEnv(process.env, projectRoot);
267
+ const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
268
+
269
+ if (await checkCommand('uv', ['--version'], { shell: false })) {
270
+ return { command: 'uv', env: uvEnv };
271
+ }
272
+
273
+ if (fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false })) {
274
+ return { command: localUvExecutable, env: uvEnv };
114
275
  }
115
276
 
116
- throw new Error(
117
- 'Python 3.11 was not found. Install Python 3.11 and make sure `python`, `python3`, or `py -3.11` is available.'
277
+ console.log('📦 Installing project-local uv...');
278
+ const installer = getProjectLocalUvInstallerCommand();
279
+ await runCommand(
280
+ installer.command,
281
+ installer.args,
282
+ projectRoot,
283
+ 'uv installation',
284
+ buildProjectLocalUvInstallerEnv(process.env, projectRoot),
285
+ false
118
286
  );
287
+
288
+ if (!(fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false }))) {
289
+ throw new Error('Failed to install project-local uv.');
290
+ }
291
+
292
+ return { command: localUvExecutable, env: uvEnv };
119
293
  }
120
294
 
121
295
  async function getCommandPath(command: string): Promise<string | null> {
@@ -129,6 +303,18 @@ async function getCommandPath(command: string): Promise<string | null> {
129
303
  return firstLine || null;
130
304
  }
131
305
 
306
+ async function resolveInstalledCoreToolsVersion(): Promise<string | null> {
307
+ if (!(await checkCoreTools())) {
308
+ return null;
309
+ }
310
+
311
+ try {
312
+ return parseCoreToolsVersion(await captureCommandOutput('func', ['--version']));
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+
132
318
  async function captureCommandOutput(
133
319
  command: string,
134
320
  args: string[],
@@ -166,6 +352,41 @@ async function captureCommandOutput(
166
352
  });
167
353
  }
168
354
 
355
+ async function probeHttpServer(url: string): Promise<boolean> {
356
+ return new Promise((resolve) => {
357
+ const target = new URL(url);
358
+ const requestFactory = target.protocol === 'https:' ? https.request : http.request;
359
+ let settled = false;
360
+ const finish = (value: boolean) => {
361
+ if (!settled) {
362
+ settled = true;
363
+ resolve(value);
364
+ }
365
+ };
366
+
367
+ const request = requestFactory(
368
+ {
369
+ hostname: target.hostname,
370
+ port: target.port,
371
+ path: target.pathname || '/',
372
+ method: 'GET',
373
+ timeout: 1000,
374
+ },
375
+ (response) => {
376
+ response.resume();
377
+ finish(true);
378
+ }
379
+ );
380
+
381
+ request.on('timeout', () => {
382
+ request.destroy();
383
+ finish(false);
384
+ });
385
+ request.on('error', () => finish(false));
386
+ request.end();
387
+ });
388
+ }
389
+
169
390
  async function resolvePythonRuntimeDetails(
170
391
  functionsDir: string,
171
392
  env: NodeJS.ProcessEnv
@@ -241,47 +462,36 @@ async function bridgePythonCoreToolsForWindowsArm64(
241
462
  }
242
463
 
243
464
  async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<NodeJS.ProcessEnv> {
244
- const { pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
245
- const hasUv = await checkCommand('uv', ['--version']);
465
+ const projectRoot = getPythonProjectRoot(functionsDir);
466
+ const { command: uvCommand, env: uvEnv } = await resolveProjectLocalUvCommand(projectRoot);
467
+ const { venvDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
468
+ const hasUsableVirtualEnv = fs.existsSync(pythonExecutable) && await checkCommand(pythonExecutable, ['--version'], {
469
+ cwd: functionsDir,
470
+ env: uvEnv,
471
+ shell: false,
472
+ });
246
473
 
247
- if (!fs.existsSync(pythonExecutable)) {
248
- if (hasUv) {
249
- console.log('📦 Creating Python virtual environment with uv...');
250
- await runCommand('uv', ['venv', '.venv', '--python', '3.11'], functionsDir, 'python virtual environment setup');
251
- } else {
252
- const bootstrap = await resolvePythonBootstrapCommand();
253
- console.log(`📦 Creating Python virtual environment with ${bootstrap.label}...`);
254
- await runCommand(
255
- bootstrap.command,
256
- [...bootstrap.argsPrefix, '-m', 'venv', '.venv'],
257
- functionsDir,
258
- 'python virtual environment setup'
259
- );
474
+ if (!hasUsableVirtualEnv) {
475
+ const venvArgs = buildUvVenvArgs('.venv');
476
+ if (fs.existsSync(venvDir)) {
477
+ venvArgs.push('--clear');
260
478
  }
261
- }
262
479
 
263
- const pythonEnv = buildPythonFunctionsEnv(process.env, functionsDir);
264
- console.log(`📦 Installing Python Azure Functions dependencies${hasUv ? ' with uv' : ''}...`);
265
-
266
- if (hasUv) {
267
- await runCommand(
268
- 'uv',
269
- ['pip', 'install', '--python', pythonExecutable, '-r', 'requirements.txt'],
270
- functionsDir,
271
- 'python dependency installation',
272
- pythonEnv
273
- );
274
- } else {
275
- await runCommand('python', ['-m', 'pip', 'install', '--upgrade', 'pip'], functionsDir, 'python pip upgrade', pythonEnv);
276
- await runCommand(
277
- 'python',
278
- ['-m', 'pip', 'install', '-r', 'requirements.txt'],
279
- functionsDir,
280
- 'python dependency installation',
281
- pythonEnv
282
- );
480
+ console.log('📦 Creating Python virtual environment with uv...');
481
+ await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
283
482
  }
284
483
 
484
+ console.log('📦 Installing Python Azure Functions dependencies with uv...');
485
+ await runCommand(
486
+ uvCommand,
487
+ buildUvPipInstallArgs(pythonExecutable, 'requirements.txt'),
488
+ functionsDir,
489
+ 'python dependency installation',
490
+ uvEnv,
491
+ false
492
+ );
493
+
494
+ const pythonEnv = buildPythonFunctionsEnv(uvEnv, functionsDir);
285
495
  return bridgePythonCoreToolsForWindowsArm64(functionsDir, pythonEnv);
286
496
  }
287
497
 
@@ -290,7 +500,6 @@ async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<
290
500
  */
291
501
  async function checkCosmosDBEmulator(): Promise<boolean> {
292
502
  return new Promise((resolve) => {
293
- const net = require('net');
294
503
  const socket = new net.Socket();
295
504
 
296
505
  const timeout = setTimeout(() => {
@@ -313,27 +522,39 @@ async function checkCosmosDBEmulator(): Promise<boolean> {
313
522
  });
314
523
  }
315
524
 
316
- export const devCommand = new Command()
317
- .name('dev')
318
- .description('Start SwallowKit development server (Cosmos DB + Next.js + Azure Functions)')
319
- .option('-p, --port <port>', 'Next.js port', '3000')
320
- .option('-f, --functions-port <port>', 'Azure Functions port', '7071')
321
- .option('--host <host>', 'Host name', 'localhost')
322
- .option('--open', 'Open browser automatically', false)
323
- .option('--verbose', 'Show verbose logs', false)
324
- .option('--no-functions', 'Skip Azure Functions startup', false)
325
- .option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
326
- .action(async (options: DevOptions & { functionsPort?: string; noFunctions?: boolean; seedEnv?: string }) => {
327
- // SwallowKit プロジェクトディレクトリかどうかを検証
328
- ensureSwallowKitProject("dev");
329
-
330
- console.log('🚀 Starting SwallowKit development environment...');
331
- if (options.verbose) {
332
- console.log('⚙️ Options:', options);
333
- }
525
+ export function buildDevCommand(
526
+ runDevEnvironment: (options: DevOptions) => Promise<void> = startDevEnvironment,
527
+ verifyProject: (commandName: string, projectRoot?: string) => void = ensureSwallowKitProject
528
+ ): Command {
529
+ return new Command()
530
+ .name('dev')
531
+ .description('Start SwallowKit development server (Cosmos DB + Next.js + Azure Functions)')
532
+ .option('-p, --port <port>', 'Next.js port', '3000')
533
+ .option('-f, --functions-port <port>', 'Azure Functions port', '7071')
534
+ .option('--host <host>', 'Host name', 'localhost')
535
+ .option('--open', 'Open browser automatically', false)
536
+ .option('--verbose', 'Show verbose logs', false)
537
+ .option('--no-functions', 'Skip Azure Functions startup')
538
+ .option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
539
+ .option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
540
+ .option('--swa-port <port>', 'SWA authentication emulator port', '4280')
541
+ .option('--no-swa', 'Skip the SWA authentication emulator')
542
+ .action(async (options: ParsedDevActionOptions) => {
543
+ const normalizedOptions = normalizeParsedDevOptions(options);
544
+
545
+ // SwallowKit プロジェクトディレクトリかどうかを検証
546
+ verifyProject("dev");
547
+
548
+ console.log('🚀 Starting SwallowKit development environment...');
549
+ if (normalizedOptions.verbose) {
550
+ console.log('⚙️ Options:', normalizedOptions);
551
+ }
334
552
 
335
- await startDevEnvironment(options);
336
- });
553
+ await runDevEnvironment(normalizedOptions);
554
+ });
555
+ }
556
+
557
+ export const devCommand = buildDevCommand();
337
558
 
338
559
  interface CosmosInitializationResult {
339
560
  endpoint: string;
@@ -344,40 +565,29 @@ interface CosmosInitializationResult {
344
565
 
345
566
  async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializationResult | null> {
346
567
  try {
347
- // Read local.settings.json from functions directory
348
- const functionsDir = path.join(process.cwd(), 'functions');
349
- const localSettingsPath = path.join(functionsDir, 'local.settings.json');
350
-
351
- if (!fs.existsSync(localSettingsPath)) {
352
- console.log('⚠️ local.settings.json not found. Skipping Cosmos DB initialization.');
353
- return null;
354
- }
568
+ const connectionInfoResult = resolveLocalCosmosConnectionInfo(databaseName);
569
+ if (!connectionInfoResult.ok) {
570
+ if (connectionInfoResult.reason === 'missing-local-settings') {
571
+ console.log('⚠️ local.settings.json not found. Skipping Cosmos DB initialization.');
572
+ } else if (connectionInfoResult.reason === 'missing-connection-string') {
573
+ console.log('⚠️ CosmosDBConnection not found in local.settings.json. Skipping Cosmos DB initialization.');
574
+ } else {
575
+ console.log('⚠️ Invalid CosmosDB connection string format.');
576
+ }
355
577
 
356
- const localSettings = JSON.parse(fs.readFileSync(localSettingsPath, 'utf-8'));
357
- const connectionString = localSettings.Values?.CosmosDBConnection;
358
- const dbName = localSettings.Values?.COSMOS_DB_DATABASE_NAME || databaseName;
359
-
360
- if (!connectionString) {
361
- console.log('⚠️ CosmosDBConnection not found in local.settings.json. Skipping Cosmos DB initialization.');
362
578
  return null;
363
579
  }
364
580
 
365
581
  console.log('🗄️ Initializing Cosmos DB...');
366
-
367
- // Parse connection string
368
- const endpointMatch = connectionString.match(/AccountEndpoint=([^;]+)/);
369
- const keyMatch = connectionString.match(/AccountKey=([^;]+)/);
370
-
371
- if (!endpointMatch || !keyMatch) {
372
- console.log('⚠️ Invalid CosmosDB connection string format.');
373
- return null;
582
+ const { endpoint, key, databaseName: dbName, worktreeIsolationEnabled } = connectionInfoResult.value;
583
+ console.log(`📦 Cosmos DB database: ${dbName}`);
584
+ if (worktreeIsolationEnabled) {
585
+ console.log('🌳 Worktree isolation: enabled');
374
586
  }
375
587
 
376
- const endpoint = endpointMatch[1];
377
-
378
588
  const client = new CosmosClient({
379
589
  endpoint: endpoint,
380
- key: keyMatch[1]
590
+ key,
381
591
  });
382
592
 
383
593
  // Create database if not exists
@@ -392,11 +602,11 @@ async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializ
392
602
  let containerCreated = false;
393
603
 
394
604
  try {
395
- console.log(`🔧 Creating container "${containerName}" with partition key /id...`);
605
+ console.log(`🔧 Creating container "${containerName}" with partition key ${model.partitionKey}...`);
396
606
  const containerResponse = await database.containers.createIfNotExists({
397
607
  id: containerName,
398
608
  partitionKey: {
399
- paths: ['/id'],
609
+ paths: [model.partitionKey],
400
610
  kind: PartitionKeyKind.Hash,
401
611
  version: 2
402
612
  }
@@ -415,7 +625,7 @@ async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializ
415
625
  const containerResponse = await database.containers.createIfNotExists({
416
626
  id: containerName,
417
627
  partitionKey: {
418
- paths: ['/id']
628
+ paths: [model.partitionKey]
419
629
  }
420
630
  });
421
631
  console.log(`✅ Container "${containerName}" ready (status: ${containerResponse.statusCode})`);
@@ -431,7 +641,7 @@ async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializ
431
641
  }
432
642
 
433
643
  console.log('✅ Cosmos DB initialization complete\n');
434
- return { endpoint, key: keyMatch[1], databaseName: dbName, models };
644
+ return { endpoint, key, databaseName: dbName, models };
435
645
  } catch (error: any) {
436
646
  console.error('⚠️ Cosmos DB initialization failed:', error.message);
437
647
  if (error.stack) {
@@ -445,19 +655,58 @@ async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializ
445
655
  async function startDevEnvironment(options: DevOptions) {
446
656
  const port = options.port || '3000';
447
657
  const functionsPort = options.functionsPort || '7071';
658
+ const swaPort = options.swaPort || '4280';
448
659
 
449
660
  // Detect package manager from project lockfile
450
661
  const pm = detectFromProject();
451
662
  const pmCmd = getCommands(pm);
452
663
  const backendLanguage = getBackendLanguage();
664
+ const authConfig = getAuthConfig();
665
+ const normalizedAuth = normalizeAuthConfig(authConfig);
666
+ const useSwaEmulator = Boolean(normalizedAuth && Object.values(normalizedAuth.schemes).some(scheme => scheme.provider === 'swa')) && !options.noSwa;
667
+ const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
668
+
669
+ if (useSwaEmulator && !swaCommand) {
670
+ const installCommand = getSwaCliInstallCommand(pm);
671
+ console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
672
+ console.error(' Install it in this project and retry:');
673
+ console.error(`\n ${installCommand}\n`);
674
+ process.exitCode = 1;
675
+ return;
676
+ }
453
677
 
454
678
  // プロセスを管理する配列
455
679
  const processes: ChildProcess[] = [];
456
680
  let functionsEnv: NodeJS.ProcessEnv = process.env;
681
+ let mockServer: ConnectorMockServer | null = null;
682
+ let envLocalPath = '';
683
+ let envLocalDefaultUrl = ''; // default Functions URL to restore on shutdown
684
+ const functionsBaseUrl = buildFunctionsBaseUrl(options.host, functionsPort);
685
+ let functionsReadinessPromise: Promise<boolean> | null = null;
686
+ let functionsReady = !!options.noFunctions;
457
687
 
458
688
  // Cleanup processes on Ctrl+C
459
- process.on('SIGINT', () => {
689
+ process.on('SIGINT', async () => {
460
690
  console.log('\n🛑 Stopping development servers...');
691
+ // Restore .env.local to default Functions port on shutdown
692
+ if (envLocalPath && envLocalDefaultUrl) {
693
+ try {
694
+ if (fs.existsSync(envLocalPath)) {
695
+ const content = fs.readFileSync(envLocalPath, 'utf-8');
696
+ if (content.includes('BACKEND_FUNCTIONS_BASE_URL=') &&
697
+ !content.includes(`BACKEND_FUNCTIONS_BASE_URL=${envLocalDefaultUrl}`)) {
698
+ const restored = content.replace(
699
+ /^BACKEND_FUNCTIONS_BASE_URL=.*/m,
700
+ `BACKEND_FUNCTIONS_BASE_URL=${envLocalDefaultUrl}`
701
+ );
702
+ fs.writeFileSync(envLocalPath, restored, 'utf-8');
703
+ }
704
+ }
705
+ } catch { /* ignore */ }
706
+ }
707
+ if (mockServer) {
708
+ await mockServer.stop();
709
+ }
461
710
  processes.forEach((proc) => {
462
711
  if (proc && !proc.killed) {
463
712
  proc.kill();
@@ -487,11 +736,14 @@ async function startDevEnvironment(options: DevOptions) {
487
736
  const functionsDir = path.join(process.cwd(), 'functions');
488
737
  const hasFunctions = fs.existsSync(functionsDir) && hasFunctionsProject(functionsDir, backendLanguage);
489
738
 
739
+ let functionsCoreToolsCommand: FunctionsCoreToolsCommand | null = null;
740
+ let installedCoreToolsVersion: string | null = null;
741
+
490
742
  if (hasFunctions && !options.noFunctions) {
491
- // Check if Azure Functions Core Tools is installed
492
- const coreToolsInstalled = await checkCoreTools();
493
-
494
- if (!coreToolsInstalled) {
743
+ installedCoreToolsVersion = await resolveInstalledCoreToolsVersion();
744
+ functionsCoreToolsCommand = buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
745
+
746
+ if (functionsCoreToolsCommand.command === 'func' && !installedCoreToolsVersion) {
495
747
  console.log('');
496
748
  console.log('⚠️ Azure Functions Core Tools not found.');
497
749
  console.log('');
@@ -540,6 +792,8 @@ async function startDevEnvironment(options: DevOptions) {
540
792
  // Skip Azure Functions startup
541
793
  options.noFunctions = true;
542
794
  }
795
+ } else if (functionsCoreToolsCommand.command !== 'func') {
796
+ console.log(`ℹ️ Using ${functionsCoreToolsCommand.label}.`);
543
797
  }
544
798
 
545
799
  if (!options.noFunctions) {
@@ -559,10 +813,7 @@ async function startDevEnvironment(options: DevOptions) {
559
813
  }
560
814
 
561
815
  // Initialize Cosmos DB before starting Functions
562
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
563
- const appName = packageJson.name || 'App';
564
- const databaseName = `${appName.charAt(0).toUpperCase() + appName.slice(1)}Database`;
565
-
816
+ const databaseName = getDefaultCosmosDatabaseName();
566
817
  const cosmosInitialization = await initializeCosmosDB(databaseName);
567
818
  if (options.seedEnv && cosmosInitialization) {
568
819
  await applyDevSeedEnvironment({
@@ -586,43 +837,65 @@ async function startDevEnvironment(options: DevOptions) {
586
837
  await runCommand(pm, ['install'], functionsDir, `${pm} install`);
587
838
  }
588
839
  } else if (backendLanguage === 'csharp') {
589
- console.log('📦 Building C# Azure Functions project...');
590
- await runCommand('dotnet', ['build'], functionsDir, 'dotnet build');
840
+ console.log('ℹ️ C# Azure Functions can take longer on cold start while the worker builds.');
591
841
  } else {
592
842
  functionsEnv = await preparePythonFunctionsEnvironment(functionsDir);
593
843
  }
844
+
845
+ if (cosmosInitialization) {
846
+ functionsEnv = buildFunctionsCosmosEnvironment(functionsEnv, cosmosInitialization.databaseName);
847
+ }
594
848
  }
595
849
  }
596
850
 
597
851
  if (hasFunctions && !options.noFunctions) {
598
852
  // Build shared package before starting Functions
599
853
  const sharedDir = path.join(process.cwd(), 'shared');
600
- if (fs.existsSync(sharedDir) && fs.existsSync(path.join(sharedDir, 'package.json'))) {
601
- console.log('📦 Building shared package...');
602
- const filterArgs = pm === 'pnpm'
603
- ? ['run', '--filter', 'shared', 'build']
604
- : ['run', '--workspace=shared', 'build'];
605
- const sharedBuild = spawn(pm, filterArgs, {
606
- cwd: process.cwd(),
607
- shell: true,
608
- stdio: 'inherit',
609
- });
854
+ const sharedPkgPath = path.join(sharedDir, 'package.json');
855
+ if (fs.existsSync(sharedDir) && fs.existsSync(sharedPkgPath)) {
856
+ const sharedPkg = JSON.parse(fs.readFileSync(sharedPkgPath, 'utf-8'));
857
+ if (sharedPkg.scripts?.build) {
858
+ console.log('📦 Building shared package...');
859
+ const filterArgs = pm === 'pnpm'
860
+ ? ['run', '--filter', 'shared', 'build']
861
+ : ['run', '--workspace=shared', 'build'];
862
+ const sharedBuild = spawn(pm, filterArgs, {
863
+ cwd: process.cwd(),
864
+ shell: true,
865
+ stdio: 'inherit',
866
+ });
610
867
 
611
- await new Promise<void>((resolve, reject) => {
612
- sharedBuild.on('close', (code) => {
613
- if (code === 0) {
614
- console.log('✅ Shared package built successfully');
615
- resolve();
616
- } else {
617
- reject(new Error(`Shared package build failed with code ${code}`));
618
- }
868
+ await new Promise<void>((resolve, reject) => {
869
+ sharedBuild.on('close', (code) => {
870
+ if (code === 0) {
871
+ console.log('✅ Shared package built successfully');
872
+ resolve();
873
+ } else {
874
+ reject(new Error(`Shared package build failed with code ${code}`));
875
+ }
876
+ });
877
+ sharedBuild.on('error', reject);
619
878
  });
620
- sharedBuild.on('error', reject);
621
- });
879
+ } else {
880
+ console.log('⚠️ Shared package has no build script — skipping build. Run "swallowkit add-auth" to fix.');
881
+ }
882
+ }
883
+
884
+ // Build TypeScript functions after shared package (functions import from shared)
885
+ if (backendLanguage === 'typescript') {
886
+ const functionsPkgPath = path.join(functionsDir, 'package.json');
887
+ if (fs.existsSync(functionsPkgPath)) {
888
+ const functionsPkg = JSON.parse(fs.readFileSync(functionsPkgPath, 'utf-8'));
889
+ if (functionsPkg.scripts?.build) {
890
+ console.log('📦 Building TypeScript Azure Functions...');
891
+ await runCommand(pm, ['run', 'build'], functionsDir, `${pm} run build`);
892
+ }
893
+ }
622
894
  }
623
895
 
624
896
  // Azure Functions を起動
625
- const funcProcess = spawn('func', buildFunctionsStartArgs(functionsPort), {
897
+ const functionsCommand = functionsCoreToolsCommand ?? buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
898
+ const funcProcess = spawn(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort)], {
626
899
  cwd: functionsDir,
627
900
  shell: true,
628
901
  stdio: 'pipe', // Always pipe to capture output
@@ -683,7 +956,11 @@ async function startDevEnvironment(options: DevOptions) {
683
956
  }
684
957
  });
685
958
 
686
- console.log(`✅ Azure Functions started (port: ${functionsPort})`);
959
+ console.log(`⏳ Waiting for Azure Functions to accept requests at ${functionsBaseUrl}...`);
960
+ functionsReadinessPromise = waitForHttpServerReady(
961
+ functionsBaseUrl,
962
+ getFunctionsReadinessTimeoutMs(backendLanguage)
963
+ );
687
964
  } else if (!hasFunctions) {
688
965
  console.log('');
689
966
  console.log('ℹ️ functions/ directory not found. Starting Next.js only.');
@@ -695,25 +972,99 @@ async function startDevEnvironment(options: DevOptions) {
695
972
  console.log('');
696
973
  console.log('🚀 Starting Next.js development server...');
697
974
 
975
+ // Mock connector server — start proxy if --mock-connectors is enabled
976
+ let bffTargetPort = functionsPort;
977
+
978
+ if (options.mockConnectors) {
979
+ const allModels = await loadProjectModels();
980
+ const connectorModels = allModels.filter((m) => m.connectorConfig);
981
+
982
+ // Resolve auth config — auth functions use RDB connectors, mocked alongside other models
983
+ let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
984
+ if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
985
+ // Read JWT_SECRET from functions/local.settings.json if available
986
+ let jwtSecret = 'dev-jwt-secret-change-in-production-min-32-chars!!';
987
+ try {
988
+ const localSettingsPath = path.join(process.cwd(), 'functions', 'local.settings.json');
989
+ if (fs.existsSync(localSettingsPath)) {
990
+ const localSettings = JSON.parse(fs.readFileSync(localSettingsPath, 'utf-8'));
991
+ jwtSecret = localSettings.Values?.[authConfig.customJwt.jwtSecretEnv || 'JWT_SECRET'] || jwtSecret;
992
+ }
993
+ } catch { /* ignore */ }
994
+ mockAuthConfig = {
995
+ jwtSecret,
996
+ tokenExpiry: authConfig.customJwt.tokenExpiry,
997
+ customJwt: {
998
+ userTable: authConfig.customJwt.userTable,
999
+ loginIdColumn: authConfig.customJwt.loginIdColumn,
1000
+ passwordHashColumn: authConfig.customJwt.passwordHashColumn,
1001
+ rolesColumn: authConfig.customJwt.rolesColumn,
1002
+ },
1003
+ defaultPolicy: authConfig.authorization?.defaultPolicy === "public" ? "anonymous" :
1004
+ authConfig.authorization?.defaultPolicy === "authenticated" || authConfig.authorization?.defaultPolicy === "anonymous"
1005
+ ? authConfig.authorization.defaultPolicy : undefined,
1006
+ };
1007
+ }
1008
+
1009
+ if (connectorModels.length > 0 || mockAuthConfig) {
1010
+ const mockPort = parseInt(functionsPort, 10) + 1;
1011
+ mockServer = new ConnectorMockServer({
1012
+ port: mockPort,
1013
+ functionsTarget: `${options.host || 'localhost'}:${functionsPort}`,
1014
+ connectorModels,
1015
+ allModels,
1016
+ seedEnv: options.seedEnv,
1017
+ host: options.host || 'localhost',
1018
+ authConfig: mockAuthConfig,
1019
+ });
1020
+
1021
+ await mockServer.start();
1022
+ bffTargetPort = String(mockPort);
1023
+
1024
+ const modelCount = connectorModels.length + (mockAuthConfig ? 1 : 0);
1025
+ console.log('');
1026
+ console.log(`🔌 Mock server started (port: ${mockPort}) — ${modelCount} model(s) mocked via Zod/seed data`);
1027
+ for (const m of connectorModels) {
1028
+ const ops = m.connectorConfig!.operations.join(', ');
1029
+ console.log(` - ${m.name} [${ops}]`);
1030
+ }
1031
+ if (mockAuthConfig) {
1032
+ console.log(` - auth [login, me, logout]`);
1033
+ }
1034
+ console.log(` Other routes → proxied to Azure Functions (port: ${functionsPort})`);
1035
+ } else {
1036
+ console.log('');
1037
+ console.log('ℹ️ --mock-connectors specified but no connector models found. Skipping mock server.');
1038
+ }
1039
+ }
1040
+
698
1041
  // 5. Start Next.js development server
699
1042
  const nextArgs = buildNextDevArgs(pm, port);
700
1043
 
701
- if (options.open) {
702
- // Next.js 14+ deprecated --open option, so we open browser manually
703
- setTimeout(() => {
704
- const url = `http://${options.host || 'localhost'}:${port}`;
705
- console.log(`\n🌐 Opening browser: ${url}`);
706
-
707
- const start = process.platform === 'darwin' ? 'open' :
708
- process.platform === 'win32' ? 'start' : 'xdg-open';
709
- spawn(start, [url], { shell: true });
710
- }, 3000);
711
- }
1044
+ // Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
1045
+ // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
1046
+ // Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
1047
+ envLocalPath = path.join(process.cwd(), '.env.local');
1048
+ envLocalDefaultUrl = functionsBaseUrl;
1049
+ const bffTargetUrl = buildFunctionsBaseUrl(options.host, bffTargetPort);
1050
+ try {
1051
+ if (fs.existsSync(envLocalPath)) {
1052
+ const envContent = fs.readFileSync(envLocalPath, 'utf-8');
1053
+ if (envContent.includes('BACKEND_FUNCTIONS_BASE_URL=') &&
1054
+ !envContent.includes(`BACKEND_FUNCTIONS_BASE_URL=${bffTargetUrl}`)) {
1055
+ const updated = envContent.replace(
1056
+ /^BACKEND_FUNCTIONS_BASE_URL=.*/m,
1057
+ `BACKEND_FUNCTIONS_BASE_URL=${bffTargetUrl}`
1058
+ );
1059
+ fs.writeFileSync(envLocalPath, updated, 'utf-8');
1060
+ }
1061
+ }
1062
+ } catch { /* ignore */ }
712
1063
 
713
1064
  const nextEnv: NodeJS.ProcessEnv = {
714
1065
  ...process.env,
715
- BACKEND_FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${functionsPort}`,
716
- FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${functionsPort}`,
1066
+ BACKEND_FUNCTIONS_BASE_URL: bffTargetUrl,
1067
+ FUNCTIONS_BASE_URL: bffTargetUrl,
717
1068
  };
718
1069
 
719
1070
  const nextProcess = spawn(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
@@ -735,6 +1086,9 @@ async function startDevEnvironment(options: DevOptions) {
735
1086
  console.log(`\n⏹️ Next.js exited (exit code: ${code})`);
736
1087
  }
737
1088
  // Exit all processes when Next.js exits
1089
+ if (mockServer) {
1090
+ mockServer.stop().catch(() => {});
1091
+ }
738
1092
  processes.forEach((proc) => {
739
1093
  if (proc && !proc.killed) {
740
1094
  proc.kill();
@@ -743,18 +1097,93 @@ async function startDevEnvironment(options: DevOptions) {
743
1097
  process.exit(code || 0);
744
1098
  });
745
1099
 
1100
+ if (functionsReadinessPromise) {
1101
+ functionsReady = await functionsReadinessPromise;
1102
+ console.log('');
1103
+ if (functionsReady) {
1104
+ console.log(`✅ Azure Functions ready (port: ${functionsPort})`);
1105
+ } else {
1106
+ console.log(`⚠️ Azure Functions is still starting: ${functionsBaseUrl}`);
1107
+ }
1108
+ }
1109
+
1110
+ let swaReady = false;
1111
+ if (useSwaEmulator && swaCommand) {
1112
+ const nextUrl = `http://${options.host || 'localhost'}:${port}`;
1113
+ console.log(`⏳ Waiting for Next.js before starting the SWA emulator at ${nextUrl}...`);
1114
+ const nextReady = await waitForHttpServerReady(nextUrl);
1115
+ if (!nextReady) {
1116
+ throw new Error(`Next.js did not become ready at ${nextUrl}`);
1117
+ }
1118
+
1119
+ console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
1120
+ const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
1121
+ cwd: process.cwd(),
1122
+ shell: true,
1123
+ stdio: 'inherit',
1124
+ env: process.env,
1125
+ });
1126
+ processes.push(swaProcess);
1127
+ swaProcess.on('error', (error) => {
1128
+ console.error('❌ SWA CLI startup error:', error.message);
1129
+ });
1130
+
1131
+ const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
1132
+ swaReady = await waitForHttpServerReady(swaUrl);
1133
+ if (!swaReady) {
1134
+ throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
1135
+ }
1136
+ }
1137
+
746
1138
  console.log('');
747
1139
  console.log('✅ SwallowKit development environment is running!');
748
1140
  console.log('');
749
- console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
1141
+ if (useSwaEmulator) {
1142
+ const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
1143
+ console.log('🔐 SWA authenticated app:');
1144
+ console.log(` ${swaUrl}`);
1145
+ console.log('');
1146
+ console.log('🔑 Local sign-in:');
1147
+ console.log(` ${swaUrl}/.auth/login/aad`);
1148
+ console.log(' Enter any username and select Login.');
1149
+ console.log(' The "authenticated" role is added automatically.');
1150
+ console.log('');
1151
+ console.log('📱 Next.js direct access (authentication is not emulated):');
1152
+ console.log(` http://${options.host || 'localhost'}:${port}`);
1153
+ } else {
1154
+ console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
1155
+ }
750
1156
  if (hasFunctions && !options.noFunctions) {
751
- console.log(`⚡ Azure Functions: http://${options.host || 'localhost'}:${functionsPort}`);
1157
+ console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
1158
+ }
1159
+ if (mockServer) {
1160
+ console.log(`🔌 Mock Proxy: ${bffTargetUrl} (BFF → here)`);
752
1161
  }
753
1162
  console.log('');
754
- if (hasFunctions && !options.noFunctions) {
1163
+ if (hasFunctions && !options.noFunctions && functionsReady) {
755
1164
  console.log('💡 Azure Functions and Next.js BFF are connected');
1165
+ } else if (hasFunctions && !options.noFunctions) {
1166
+ console.log('💡 Azure Functions is still warming up; BFF routes can fail until the backend responds.');
1167
+ }
1168
+ if (mockServer) {
1169
+ console.log('💡 Connector models served from mock server (Zod-generated data)');
1170
+ }
1171
+ if (swaReady) {
1172
+ console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
756
1173
  }
757
1174
  console.log('');
1175
+
1176
+ if (options.open) {
1177
+ const url = useSwaEmulator
1178
+ ? `http://${options.host || 'localhost'}:${swaPort}`
1179
+ : `http://${options.host || 'localhost'}:${port}`;
1180
+ console.log(`🌐 Opening browser: ${url}`);
1181
+ const start = process.platform === 'darwin' ? 'open' :
1182
+ process.platform === 'win32' ? 'start' : 'xdg-open';
1183
+ spawn(start, [url], { shell: true });
1184
+ console.log('');
1185
+ }
1186
+
758
1187
  console.log('🛑 Press Ctrl+C to stop');
759
1188
  console.log('');
760
1189
 
@@ -769,17 +1198,35 @@ async function startDevEnvironment(options: DevOptions) {
769
1198
  }
770
1199
  }
771
1200
 
1201
+ async function resolveSwaCliCommand(projectRoot: string): Promise<string | null> {
1202
+ const binaryName = process.platform === 'win32' ? 'swa.cmd' : 'swa';
1203
+ const localCommand = path.join(projectRoot, 'node_modules', '.bin', binaryName);
1204
+ if (fs.existsSync(localCommand) && await checkCommand(localCommand, ['--version'], {
1205
+ cwd: projectRoot,
1206
+ shell: process.platform === 'win32',
1207
+ })) {
1208
+ return localCommand;
1209
+ }
1210
+
1211
+ if (await checkCommand('swa', ['--version'], { cwd: projectRoot })) {
1212
+ return 'swa';
1213
+ }
1214
+
1215
+ return null;
1216
+ }
1217
+
772
1218
  async function runCommand(
773
1219
  command: string,
774
1220
  args: string[],
775
1221
  cwd: string,
776
1222
  label: string,
777
- env?: NodeJS.ProcessEnv
1223
+ env?: NodeJS.ProcessEnv,
1224
+ shell = true
778
1225
  ): Promise<void> {
779
1226
  await new Promise<void>((resolve, reject) => {
780
1227
  const child = spawn(command, args, {
781
1228
  cwd,
782
- shell: true,
1229
+ shell,
783
1230
  stdio: 'inherit',
784
1231
  env,
785
1232
  });