swallowkit 1.0.0-beta.22 → 1.0.0-beta.24

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 (38) hide show
  1. package/README.ja.md +9 -4
  2. package/README.md +9 -4
  3. package/dist/cli/commands/dev.d.ts +14 -0
  4. package/dist/cli/commands/dev.d.ts.map +1 -1
  5. package/dist/cli/commands/dev.js +187 -54
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts.map +1 -1
  8. package/dist/cli/commands/init.js +33 -18
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/commands/scaffold.d.ts +0 -3
  11. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  12. package/dist/cli/commands/scaffold.js +3 -172
  13. package/dist/cli/commands/scaffold.js.map +1 -1
  14. package/dist/core/project/validation.js +2 -2
  15. package/dist/core/project/validation.js.map +1 -1
  16. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  17. package/dist/core/scaffold/model-parser.js +5 -6
  18. package/dist/core/scaffold/model-parser.js.map +1 -1
  19. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  20. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  21. package/dist/core/scaffold/native-schema-generator.js +677 -0
  22. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  23. package/dist/utils/python-uv.d.ts +21 -0
  24. package/dist/utils/python-uv.d.ts.map +1 -0
  25. package/dist/utils/python-uv.js +112 -0
  26. package/dist/utils/python-uv.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/__tests__/dev.test.ts +95 -0
  29. package/src/__tests__/model-parser.test.ts +44 -64
  30. package/src/__tests__/python-uv.test.ts +48 -0
  31. package/src/__tests__/scaffold.test.ts +54 -26
  32. package/src/cli/commands/dev.ts +258 -74
  33. package/src/cli/commands/init.ts +45 -19
  34. package/src/cli/commands/scaffold.ts +3 -213
  35. package/src/core/project/validation.ts +2 -2
  36. package/src/core/scaffold/model-parser.ts +7 -7
  37. package/src/core/scaffold/native-schema-generator.ts +798 -0
  38. package/src/utils/python-uv.ts +97 -0
package/README.ja.md CHANGED
@@ -22,8 +22,8 @@ Zod スキーマから自動的に CRUD 操作を生成する Scaffold 機能を
22
22
  - **🔄 Zod スキーマ共有** - フロントエンド、BFF、Azure Functions、Cosmos DB をまたいで Zod を唯一のソースとして維持
23
23
  - **⚡ CRUD コード生成** - `swallowkit scaffold` で Azure Functions + Next.js コードを自動生成
24
24
  - **🌐 Functions バックエンド多言語対応** - `init` 時に Azure Functions の言語として TypeScript、C#、Python を選択可能
25
- - **🧬 OpenAPI スキーマブリッジ** - C#/Python バックエンドでは `scaffold` が Zod から OpenAPI を出力し、各言語向けスキーマ資産を生成
26
- - **🛡️ 契約安全性** - 共有 Zod または OpenAPI 由来のモデルにより、フロント/BFF とバックエンドの契約を整合
25
+ - **🧬 OpenAPI 出力 + ネイティブコード生成** - C#/Python バックエンドでは `scaffold` が Zod から OpenAPI を出力し、各言語のネイティブツールでスキーマ資産を生成
26
+ - **🛡️ 契約安全性** - 共有 Zod またはネイティブ生成されたモデルにより、フロント/BFF とバックエンドの契約を整合
27
27
  - **🎯 BFF パターン** - Next.js API Routes が BFF レイヤーとして機能、自動検証・リソース名推論
28
28
  - **☁️ Azure 最適化** - Static Web Apps + Functions + Cosmos DB で最小コスト構成
29
29
  - **🚀 簡単デプロイ** - Bicep IaC + CI/CD ワークフローを自動生成
@@ -132,7 +132,7 @@ pnpm dlx swallowkit scaffold shared/models/todo.ts
132
132
  - ✅ Next.js BFF API Routes (自動検証・リソース名推論)
133
133
  - ✅ React コンポーネント (型安全なフォーム)
134
134
 
135
- `init` で `csharp` または `python` を選んだ場合、`swallowkit scaffold` はあわせて `functions/openapi/` に OpenAPI ドキュメントを出力し、`functions/generated/` に各言語向けスキーマ資産を生成します。
135
+ `init` で `csharp` または `python` を選んだ場合、`swallowkit scaffold` はあわせて `functions/openapi/` に OpenAPI ドキュメントを出力し、`functions/generated/` にネイティブ生成された各言語向けスキーマ資産を生成します。
136
136
 
137
137
  ### 4. 開発サーバー起動
138
138
 
@@ -145,6 +145,11 @@ pnpm dlx swallowkit dev
145
145
  - Next.js: http://localhost:3000
146
146
  - Azure Functions: http://localhost:7071
147
147
 
148
+ バックエンド言語ごとの補足:
149
+
150
+ - **Python バックエンド**: `swallowkit dev` はローカルの Python 実行環境管理に **uv** を使います。プロジェクト内の `.uv/bin` に `uv` 本体を導入または再利用し、`.uv/python` に uv 管理の Python を保持し、Functions 本体用に `functions/.venv`、Python スキーマ生成用に `functions/.codegen-venv` を作成します。
151
+ - **C# バックエンド**: Azure Functions isolated worker はコールドスタート時にワーカービルドが入るため、応答開始まで少し時間がかかることがあります。`swallowkit dev` は Functions ホストが HTTP 応答できる状態になるまで待ってから、バックエンド URL を ready として表示します。
152
+
148
153
  ### 5. フロントエンドから使用
149
154
 
150
155
  ```typescript
@@ -255,7 +260,7 @@ MCP server は explicit な Tool だけを公開する薄い adapter で、実
255
260
  **重要なパターン:**
256
261
  - **BFF (Backend For Frontend)**: Next.js API Routes が Azure Functions へのプロキシ
257
262
  - **共有スキーマ**: `shared/models/` の Zod スキーマを唯一のソースとして扱う
258
- - **C#/Python 向け OpenAPI ブリッジ**: TypeScript 以外の Functions は `functions/generated/` の生成資産を利用
263
+ - **C#/Python 向け OpenAPI 出力**: TypeScript 以外の Functions は `functions/generated/` のネイティブ生成資産を利用
259
264
  - **外部コネクタ**: MySQL・PostgreSQL・REST API — scaffold 生成の Functions で同じ BFF パターンを維持
260
265
  - **契約安全性**: 共有 Zod または生成モデルで BFF とバックエンドの整合を保つ
261
266
  - **マネージド ID**: サービス間の安全な接続(接続文字列不要)
package/README.md CHANGED
@@ -24,8 +24,8 @@ Featuring Scaffold functionality to automatically generate CRUD operations from
24
24
  - **🔄 Zod Schema Sharing** - Keep Zod as the source of truth across frontend, BFF, Azure Functions, and Cosmos DB
25
25
  - **⚡ CRUD Code Generation** - Auto-generate Azure Functions + Next.js code with `swallowkit scaffold`
26
26
  - **🌐 Multi-language Functions Backends** - Choose TypeScript, C#, or Python for Azure Functions during `init`
27
- - **🧬 OpenAPI Schema Bridge** - For C#/Python backends, `scaffold` exports OpenAPI from Zod and generates backend schema assets
28
- - **🛡️ Contract Safety** - Keep frontend/BFF contracts aligned with backend implementations through shared Zod or generated OpenAPI-derived models
27
+ - **🧬 OpenAPI Export + Native Codegen** - For C#/Python backends, `scaffold` exports OpenAPI from Zod and generates backend schema assets with native language tooling
28
+ - **🛡️ Contract Safety** - Keep frontend/BFF contracts aligned with backend implementations through shared Zod or native-generated backend models
29
29
  - **🎯 BFF Pattern** - Next.js API Routes as BFF layer with auto-validation and resource inference
30
30
  - **☁️ Azure Optimized** - Minimal-cost architecture with Static Web Apps + Functions + Cosmos DB
31
31
  - **🚀 Easy Deployment** - Auto-generated Bicep IaC + CI/CD workflows
@@ -133,7 +133,7 @@ This auto-generates:
133
133
  - ✅ Next.js BFF API Routes (auto-validation + resource inference)
134
134
  - ✅ React Components (type-safe forms)
135
135
 
136
- If you selected `csharp` or `python` at `init` time, `swallowkit scaffold` also writes an OpenAPI document under `functions/openapi/` and generates backend schema assets under `functions/generated/`.
136
+ If you selected `csharp` or `python` at `init` time, `swallowkit scaffold` also writes an OpenAPI document under `functions/openapi/` and generates native backend schema assets under `functions/generated/`.
137
137
 
138
138
  ### 4. Start Development Server
139
139
 
@@ -146,6 +146,11 @@ pnpm dlx swallowkit dev
146
146
  - Next.js: http://localhost:3000
147
147
  - Azure Functions: http://localhost:7071
148
148
 
149
+ Backend-specific notes:
150
+
151
+ - **Python backends**: `swallowkit dev` uses **uv** for local runtime management. It installs or reuses a project-local `uv` binary under `.uv/bin`, keeps uv-managed Python under `.uv/python`, creates `functions/.venv` for the Functions app, and creates `functions/.codegen-venv` for Python schema generation.
152
+ - **C# backends**: Azure Functions isolated worker can take longer to answer on cold start while the worker build completes. `swallowkit dev` waits for the Functions host to start responding before it prints the backend URL as ready.
153
+
149
154
  If you want to replace Cosmos DB Emulator data before startup, generate an environment template and then launch `dev` with that environment:
150
155
 
151
156
  ```bash
@@ -312,7 +317,7 @@ The MCP server is intentionally a thin adapter with explicit tools only. It dele
312
317
  **Key Patterns:**
313
318
  - **BFF (Backend For Frontend)**: Next.js API Routes proxy to Azure Functions
314
319
  - **Shared Schemas**: Zod schemas stay in `shared/models/` as the source of truth
315
- - **OpenAPI Bridge for C#/Python**: Non-TypeScript Functions consume generated assets under `functions/generated/`
320
+ - **OpenAPI Export for C#/Python**: Non-TypeScript Functions consume native-generated assets under `functions/generated/`
316
321
  - **External Connectors**: MySQL, PostgreSQL, REST APIs — scaffold-generated Functions with the same BFF pattern
317
322
  - **Contract Safety**: BFF and backend stay aligned through shared Zod or generated backend models
318
323
  - **Managed Identity**: Secure service connections (no connection strings)
@@ -1,4 +1,5 @@
1
1
  import { Command } from 'commander';
2
+ import { BackendLanguage } from '../../types';
2
3
  export interface DevOptions {
3
4
  port?: string;
4
5
  functionsPort?: string;
@@ -9,14 +10,27 @@ export interface DevOptions {
9
10
  seedEnv?: string;
10
11
  mockConnectors?: boolean;
11
12
  }
13
+ interface FunctionsCoreToolsCommand {
14
+ command: string;
15
+ argsPrefix: string[];
16
+ label: string;
17
+ }
12
18
  export declare function buildFunctionsStartArgs(functionsPort: string): string[];
19
+ export declare function parseCoreToolsVersion(output: string): string | null;
20
+ export declare function compareVersionNumbers(left: string, right: string): number;
21
+ export declare function buildFunctionsCoreToolsCommand(backendLanguage: BackendLanguage, installedVersion: string | null): FunctionsCoreToolsCommand;
13
22
  export declare function buildNextDevArgs(pm: string, port: string): string[];
23
+ export declare function buildFunctionsBaseUrl(host: string | undefined, functionsPort: string): string;
24
+ export declare function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number;
25
+ export declare function waitForHttpServerReady(url: string, timeoutMs?: number, intervalMs?: number): Promise<boolean>;
14
26
  export declare function getPythonVirtualEnvPaths(functionsDir: string): {
15
27
  venvDir: string;
16
28
  binDir: string;
17
29
  pythonExecutable: string;
18
30
  };
31
+ export declare function getCSharpFunctionsBuildArtifactPaths(functionsDir: string): string[];
19
32
  export declare function buildPythonFunctionsEnv(baseEnv: NodeJS.ProcessEnv, functionsDir: string): NodeJS.ProcessEnv;
20
33
  export declare function buildDevCommand(runDevEnvironment?: (options: DevOptions) => Promise<void>, verifyProject?: (commandName: string, projectRoot?: string) => void): Command;
21
34
  export declare const devCommand: Command;
35
+ export {};
22
36
  //# sourceMappingURL=dev.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAaD,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAGnE;AAED,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAUA;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,UAAU,CAW3G;AAgQD,wBAAgB,eAAe,CAC7B,iBAAiB,GAAE,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAuB,EAC/E,aAAa,GAAE,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,IAA8B,GAC3F,OAAO,CAyBT;AAED,eAAO,MAAM,UAAU,SAAoB,CAAC"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAa9C,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAMD,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAYD,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGnE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAezE;AAED,wBAAgB,8BAA8B,CAC5C,eAAe,EAAE,eAAe,EAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAC9B,yBAAyB,CAqB3B;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAGnE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,eAAe,GAAG,MAAM,CAEvF;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,SAAS,SAAS,EAClB,UAAU,SAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAUA;AAED,wBAAgB,oCAAoC,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAKnF;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,UAAU,CAW3G;AAsTD,wBAAgB,eAAe,CAC7B,iBAAiB,GAAE,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAuB,EAC/E,aAAa,GAAE,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,IAA8B,GAC3F,OAAO,CAyBT;AAED,eAAO,MAAM,UAAU,SAAoB,CAAC"}
@@ -35,12 +35,21 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.devCommand = void 0;
37
37
  exports.buildFunctionsStartArgs = buildFunctionsStartArgs;
38
+ exports.parseCoreToolsVersion = parseCoreToolsVersion;
39
+ exports.compareVersionNumbers = compareVersionNumbers;
40
+ exports.buildFunctionsCoreToolsCommand = buildFunctionsCoreToolsCommand;
38
41
  exports.buildNextDevArgs = buildNextDevArgs;
42
+ exports.buildFunctionsBaseUrl = buildFunctionsBaseUrl;
43
+ exports.getFunctionsReadinessTimeoutMs = getFunctionsReadinessTimeoutMs;
44
+ exports.waitForHttpServerReady = waitForHttpServerReady;
39
45
  exports.getPythonVirtualEnvPaths = getPythonVirtualEnvPaths;
46
+ exports.getCSharpFunctionsBuildArtifactPaths = getCSharpFunctionsBuildArtifactPaths;
40
47
  exports.buildPythonFunctionsEnv = buildPythonFunctionsEnv;
41
48
  exports.buildDevCommand = buildDevCommand;
42
49
  const commander_1 = require("commander");
43
50
  const child_process_1 = require("child_process");
51
+ const http = __importStar(require("http"));
52
+ const https = __importStar(require("https"));
44
53
  const path = __importStar(require("path"));
45
54
  const fs = __importStar(require("fs"));
46
55
  const os = __importStar(require("os"));
@@ -49,7 +58,10 @@ const cosmos_1 = require("@azure/cosmos");
49
58
  const config_1 = require("../../core/config");
50
59
  const dev_seeds_1 = require("./dev-seeds");
51
60
  const package_manager_1 = require("../../utils/package-manager");
61
+ const python_uv_1 = require("../../utils/python-uv");
52
62
  const connector_mock_server_1 = require("../../core/mock/connector-mock-server");
63
+ const MINIMUM_CSHARP_CORE_TOOLS_VERSION = '4.6.0';
64
+ const NPM_CORE_TOOLS_PACKAGE = 'azure-functions-core-tools@4';
53
65
  function normalizeParsedDevOptions(options) {
54
66
  return {
55
67
  ...options,
@@ -59,10 +71,64 @@ function normalizeParsedDevOptions(options) {
59
71
  function buildFunctionsStartArgs(functionsPort) {
60
72
  return ['start', '--port', functionsPort];
61
73
  }
74
+ function parseCoreToolsVersion(output) {
75
+ const match = output.match(/\d+\.\d+\.\d+/);
76
+ return match ? match[0] : null;
77
+ }
78
+ function compareVersionNumbers(left, right) {
79
+ const leftParts = left.split('.').map((value) => Number.parseInt(value, 10));
80
+ const rightParts = right.split('.').map((value) => Number.parseInt(value, 10));
81
+ const length = Math.max(leftParts.length, rightParts.length);
82
+ for (let index = 0; index < length; index += 1) {
83
+ const leftPart = leftParts[index] ?? 0;
84
+ const rightPart = rightParts[index] ?? 0;
85
+ if (leftPart !== rightPart) {
86
+ return leftPart - rightPart;
87
+ }
88
+ }
89
+ return 0;
90
+ }
91
+ function buildFunctionsCoreToolsCommand(backendLanguage, installedVersion) {
92
+ if (backendLanguage === 'csharp' &&
93
+ (!installedVersion || compareVersionNumbers(installedVersion, MINIMUM_CSHARP_CORE_TOOLS_VERSION) < 0)) {
94
+ const reason = installedVersion
95
+ ? `installed func ${installedVersion} is too old for C# isolated`
96
+ : 'func is not installed';
97
+ return {
98
+ command: 'npm',
99
+ argsPrefix: ['exec', '--yes', NPM_CORE_TOOLS_PACKAGE, '--'],
100
+ label: `npm exec ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
101
+ };
102
+ }
103
+ return {
104
+ command: 'func',
105
+ argsPrefix: [],
106
+ label: installedVersion ? `func ${installedVersion}` : 'func',
107
+ };
108
+ }
62
109
  function buildNextDevArgs(pm, port) {
63
110
  const baseArgs = ['next', 'dev', '--port', port, '--webpack'];
64
111
  return pm === 'pnpm' ? ['exec', ...baseArgs] : baseArgs;
65
112
  }
113
+ function buildFunctionsBaseUrl(host, functionsPort) {
114
+ return `http://${host || 'localhost'}:${functionsPort}`;
115
+ }
116
+ function getFunctionsReadinessTimeoutMs(backendLanguage) {
117
+ return backendLanguage === 'csharp' ? 90000 : 30000;
118
+ }
119
+ async function waitForHttpServerReady(url, timeoutMs = 30000, intervalMs = 500) {
120
+ const deadline = Date.now() + timeoutMs;
121
+ while (Date.now() <= deadline) {
122
+ if (await probeHttpServer(url)) {
123
+ return true;
124
+ }
125
+ if (Date.now() + intervalMs > deadline) {
126
+ break;
127
+ }
128
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
129
+ }
130
+ return probeHttpServer(url);
131
+ }
66
132
  function getPythonVirtualEnvPaths(functionsDir) {
67
133
  const venvDir = path.join(functionsDir, '.venv');
68
134
  const binDir = process.platform === 'win32'
@@ -73,6 +139,12 @@ function getPythonVirtualEnvPaths(functionsDir) {
73
139
  : path.join(binDir, 'python');
74
140
  return { venvDir, binDir, pythonExecutable };
75
141
  }
142
+ function getCSharpFunctionsBuildArtifactPaths(functionsDir) {
143
+ return [
144
+ path.join(functionsDir, 'bin'),
145
+ path.join(functionsDir, 'obj'),
146
+ ];
147
+ }
76
148
  function buildPythonFunctionsEnv(baseEnv, functionsDir) {
77
149
  const { venvDir, binDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
78
150
  const pathKey = getPathEnvKey(baseEnv);
@@ -101,10 +173,12 @@ function prependToPathEnv(env, entry) {
101
173
  async function checkCoreTools() {
102
174
  return checkCommand('func', ['--version']);
103
175
  }
104
- async function checkCommand(command, args = ['--version']) {
176
+ async function checkCommand(command, args = ['--version'], options) {
105
177
  return new Promise((resolve) => {
106
178
  const checkProcess = (0, child_process_1.spawn)(command, args, {
107
- shell: true,
179
+ cwd: options?.cwd,
180
+ env: options?.env ?? process.env,
181
+ shell: options?.shell ?? true,
108
182
  stdio: 'pipe',
109
183
  });
110
184
  checkProcess.on('close', (code) => {
@@ -115,22 +189,22 @@ async function checkCommand(command, args = ['--version']) {
115
189
  });
116
190
  });
117
191
  }
118
- async function resolvePythonBootstrapCommand() {
119
- const candidates = process.platform === 'win32'
120
- ? [
121
- { command: 'py', argsPrefix: ['-3.11'], label: 'py -3.11' },
122
- { command: 'python', argsPrefix: [], label: 'python' },
123
- ]
124
- : [
125
- { command: 'python3', argsPrefix: [], label: 'python3' },
126
- { command: 'python', argsPrefix: [], label: 'python' },
127
- ];
128
- for (const candidate of candidates) {
129
- if (await checkCommand(candidate.command, [...candidate.argsPrefix, '--version'])) {
130
- return candidate;
131
- }
192
+ async function resolveProjectLocalUvCommand(projectRoot) {
193
+ const uvEnv = (0, python_uv_1.buildProjectLocalUvEnv)(process.env, projectRoot);
194
+ const { localUvExecutable } = (0, python_uv_1.getProjectLocalUvPaths)(projectRoot);
195
+ if (await checkCommand('uv', ['--version'], { shell: false })) {
196
+ return { command: 'uv', env: uvEnv };
132
197
  }
133
- throw new Error('Python 3.11 was not found. Install Python 3.11 and make sure `python`, `python3`, or `py -3.11` is available.');
198
+ if (fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false })) {
199
+ return { command: localUvExecutable, env: uvEnv };
200
+ }
201
+ console.log('📦 Installing project-local uv...');
202
+ const installer = (0, python_uv_1.getProjectLocalUvInstallerCommand)();
203
+ await runCommand(installer.command, installer.args, projectRoot, 'uv installation', (0, python_uv_1.buildProjectLocalUvInstallerEnv)(process.env, projectRoot), false);
204
+ if (!(fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false }))) {
205
+ throw new Error('Failed to install project-local uv.');
206
+ }
207
+ return { command: localUvExecutable, env: uvEnv };
134
208
  }
135
209
  async function getCommandPath(command) {
136
210
  const locator = process.platform === 'win32' ? 'where' : 'which';
@@ -141,6 +215,17 @@ async function getCommandPath(command) {
141
215
  .find(Boolean);
142
216
  return firstLine || null;
143
217
  }
218
+ async function resolveInstalledCoreToolsVersion() {
219
+ if (!(await checkCoreTools())) {
220
+ return null;
221
+ }
222
+ try {
223
+ return parseCoreToolsVersion(await captureCommandOutput('func', ['--version']));
224
+ }
225
+ catch {
226
+ return null;
227
+ }
228
+ }
144
229
  async function captureCommandOutput(command, args, cwd, env) {
145
230
  return new Promise((resolve, reject) => {
146
231
  const child = (0, child_process_1.spawn)(command, args, {
@@ -168,6 +253,35 @@ async function captureCommandOutput(command, args, cwd, env) {
168
253
  child.on('error', reject);
169
254
  });
170
255
  }
256
+ async function probeHttpServer(url) {
257
+ return new Promise((resolve) => {
258
+ const target = new URL(url);
259
+ const requestFactory = target.protocol === 'https:' ? https.request : http.request;
260
+ let settled = false;
261
+ const finish = (value) => {
262
+ if (!settled) {
263
+ settled = true;
264
+ resolve(value);
265
+ }
266
+ };
267
+ const request = requestFactory({
268
+ hostname: target.hostname,
269
+ port: target.port,
270
+ path: target.pathname || '/',
271
+ method: 'GET',
272
+ timeout: 1000,
273
+ }, (response) => {
274
+ response.resume();
275
+ finish(true);
276
+ });
277
+ request.on('timeout', () => {
278
+ request.destroy();
279
+ finish(false);
280
+ });
281
+ request.on('error', () => finish(false));
282
+ request.end();
283
+ });
284
+ }
171
285
  async function resolvePythonRuntimeDetails(functionsDir, env) {
172
286
  const { pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
173
287
  const output = await captureCommandOutput(pythonExecutable, [
@@ -216,28 +330,25 @@ async function bridgePythonCoreToolsForWindowsArm64(functionsDir, env) {
216
330
  return prependToPathEnv(env, patchedRoot);
217
331
  }
218
332
  async function preparePythonFunctionsEnvironment(functionsDir) {
219
- const { pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
220
- const hasUv = await checkCommand('uv', ['--version']);
221
- if (!fs.existsSync(pythonExecutable)) {
222
- if (hasUv) {
223
- console.log('📦 Creating Python virtual environment with uv...');
224
- await runCommand('uv', ['venv', '.venv', '--python', '3.11'], functionsDir, 'python virtual environment setup');
225
- }
226
- else {
227
- const bootstrap = await resolvePythonBootstrapCommand();
228
- console.log(`📦 Creating Python virtual environment with ${bootstrap.label}...`);
229
- await runCommand(bootstrap.command, [...bootstrap.argsPrefix, '-m', 'venv', '.venv'], functionsDir, 'python virtual environment setup');
333
+ const projectRoot = (0, python_uv_1.getPythonProjectRoot)(functionsDir);
334
+ const { command: uvCommand, env: uvEnv } = await resolveProjectLocalUvCommand(projectRoot);
335
+ const { venvDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
336
+ const hasUsableVirtualEnv = fs.existsSync(pythonExecutable) && await checkCommand(pythonExecutable, ['--version'], {
337
+ cwd: functionsDir,
338
+ env: uvEnv,
339
+ shell: false,
340
+ });
341
+ if (!hasUsableVirtualEnv) {
342
+ const venvArgs = (0, python_uv_1.buildUvVenvArgs)('.venv');
343
+ if (fs.existsSync(venvDir)) {
344
+ venvArgs.push('--clear');
230
345
  }
346
+ console.log('📦 Creating Python virtual environment with uv...');
347
+ await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
231
348
  }
232
- const pythonEnv = buildPythonFunctionsEnv(process.env, functionsDir);
233
- console.log(`📦 Installing Python Azure Functions dependencies${hasUv ? ' with uv' : ''}...`);
234
- if (hasUv) {
235
- await runCommand('uv', ['pip', 'install', '--python', pythonExecutable, '-r', 'requirements.txt'], functionsDir, 'python dependency installation', pythonEnv);
236
- }
237
- else {
238
- await runCommand('python', ['-m', 'pip', 'install', '--upgrade', 'pip'], functionsDir, 'python pip upgrade', pythonEnv);
239
- await runCommand('python', ['-m', 'pip', 'install', '-r', 'requirements.txt'], functionsDir, 'python dependency installation', pythonEnv);
240
- }
349
+ console.log('📦 Installing Python Azure Functions dependencies with uv...');
350
+ await runCommand(uvCommand, (0, python_uv_1.buildUvPipInstallArgs)(pythonExecutable, 'requirements.txt'), functionsDir, 'python dependency installation', uvEnv, false);
351
+ const pythonEnv = buildPythonFunctionsEnv(uvEnv, functionsDir);
241
352
  return bridgePythonCoreToolsForWindowsArm64(functionsDir, pythonEnv);
242
353
  }
243
354
  /**
@@ -271,7 +382,7 @@ function buildDevCommand(runDevEnvironment = startDevEnvironment, verifyProject
271
382
  .option('--host <host>', 'Host name', 'localhost')
272
383
  .option('--open', 'Open browser automatically', false)
273
384
  .option('--verbose', 'Show verbose logs', false)
274
- .option('--no-functions', 'Skip Azure Functions startup', false)
385
+ .option('--no-functions', 'Skip Azure Functions startup')
275
386
  .option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
276
387
  .option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
277
388
  .action(async (options) => {
@@ -387,6 +498,9 @@ async function startDevEnvironment(options) {
387
498
  let mockServer = null;
388
499
  let envLocalPath = '';
389
500
  let envLocalDefaultUrl = ''; // default Functions URL to restore on shutdown
501
+ const functionsBaseUrl = buildFunctionsBaseUrl(options.host, functionsPort);
502
+ let functionsReadinessPromise = null;
503
+ let functionsReady = !!options.noFunctions;
390
504
  // Cleanup processes on Ctrl+C
391
505
  process.on('SIGINT', async () => {
392
506
  console.log('\n🛑 Stopping development servers...');
@@ -431,10 +545,12 @@ async function startDevEnvironment(options) {
431
545
  // 2. Check if Azure Functions exists
432
546
  const functionsDir = path.join(process.cwd(), 'functions');
433
547
  const hasFunctions = fs.existsSync(functionsDir) && hasFunctionsProject(functionsDir, backendLanguage);
548
+ let functionsCoreToolsCommand = null;
549
+ let installedCoreToolsVersion = null;
434
550
  if (hasFunctions && !options.noFunctions) {
435
- // Check if Azure Functions Core Tools is installed
436
- const coreToolsInstalled = await checkCoreTools();
437
- if (!coreToolsInstalled) {
551
+ installedCoreToolsVersion = await resolveInstalledCoreToolsVersion();
552
+ functionsCoreToolsCommand = buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
553
+ if (functionsCoreToolsCommand.command === 'func' && !installedCoreToolsVersion) {
438
554
  console.log('');
439
555
  console.log('⚠️ Azure Functions Core Tools not found.');
440
556
  console.log('');
@@ -481,6 +597,9 @@ async function startDevEnvironment(options) {
481
597
  options.noFunctions = true;
482
598
  }
483
599
  }
600
+ else if (functionsCoreToolsCommand.command !== 'func') {
601
+ console.log(`ℹ️ Using ${functionsCoreToolsCommand.label}.`);
602
+ }
484
603
  if (!options.noFunctions) {
485
604
  // Check if Cosmos DB Emulator is running
486
605
  const cosmosRunning = await checkCosmosDBEmulator();
@@ -521,8 +640,7 @@ async function startDevEnvironment(options) {
521
640
  }
522
641
  }
523
642
  else if (backendLanguage === 'csharp') {
524
- console.log('📦 Building C# Azure Functions project...');
525
- await runCommand('dotnet', ['build'], functionsDir, 'dotnet build');
643
+ console.log('ℹ️ C# Azure Functions can take longer on cold start while the worker builds.');
526
644
  }
527
645
  else {
528
646
  functionsEnv = await preparePythonFunctionsEnvironment(functionsDir);
@@ -574,7 +692,8 @@ async function startDevEnvironment(options) {
574
692
  }
575
693
  }
576
694
  // Azure Functions を起動
577
- const funcProcess = (0, child_process_1.spawn)('func', buildFunctionsStartArgs(functionsPort), {
695
+ const functionsCommand = functionsCoreToolsCommand ?? buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
696
+ const funcProcess = (0, child_process_1.spawn)(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort)], {
578
697
  cwd: functionsDir,
579
698
  shell: true,
580
699
  stdio: 'pipe', // Always pipe to capture output
@@ -625,7 +744,8 @@ async function startDevEnvironment(options) {
625
744
  }
626
745
  }
627
746
  });
628
- console.log(`✅ Azure Functions started (port: ${functionsPort})`);
747
+ console.log(`⏳ Waiting for Azure Functions to accept requests at ${functionsBaseUrl}...`);
748
+ functionsReadinessPromise = waitForHttpServerReady(functionsBaseUrl, getFunctionsReadinessTimeoutMs(backendLanguage));
629
749
  }
630
750
  else if (!hasFunctions) {
631
751
  console.log('');
@@ -714,8 +834,8 @@ async function startDevEnvironment(options) {
714
834
  // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
715
835
  // Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
716
836
  envLocalPath = path.join(process.cwd(), '.env.local');
717
- envLocalDefaultUrl = `http://${options.host || 'localhost'}:${functionsPort}`;
718
- const bffTargetUrl = `http://${options.host || 'localhost'}:${bffTargetPort}`;
837
+ envLocalDefaultUrl = functionsBaseUrl;
838
+ const bffTargetUrl = buildFunctionsBaseUrl(options.host, bffTargetPort);
719
839
  try {
720
840
  if (fs.existsSync(envLocalPath)) {
721
841
  const envContent = fs.readFileSync(envLocalPath, 'utf-8');
@@ -729,8 +849,8 @@ async function startDevEnvironment(options) {
729
849
  catch { /* ignore */ }
730
850
  const nextEnv = {
731
851
  ...process.env,
732
- BACKEND_FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${bffTargetPort}`,
733
- FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${bffTargetPort}`,
852
+ BACKEND_FUNCTIONS_BASE_URL: bffTargetUrl,
853
+ FUNCTIONS_BASE_URL: bffTargetUrl,
734
854
  };
735
855
  const nextProcess = (0, child_process_1.spawn)(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
736
856
  cwd: process.cwd(),
@@ -758,20 +878,33 @@ async function startDevEnvironment(options) {
758
878
  });
759
879
  process.exit(code || 0);
760
880
  });
881
+ if (functionsReadinessPromise) {
882
+ functionsReady = await functionsReadinessPromise;
883
+ console.log('');
884
+ if (functionsReady) {
885
+ console.log(`✅ Azure Functions ready (port: ${functionsPort})`);
886
+ }
887
+ else {
888
+ console.log(`⚠️ Azure Functions is still starting: ${functionsBaseUrl}`);
889
+ }
890
+ }
761
891
  console.log('');
762
892
  console.log('✅ SwallowKit development environment is running!');
763
893
  console.log('');
764
894
  console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
765
895
  if (hasFunctions && !options.noFunctions) {
766
- console.log(`⚡ Azure Functions: http://${options.host || 'localhost'}:${functionsPort}`);
896
+ console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
767
897
  }
768
898
  if (mockServer) {
769
- console.log(`🔌 Mock Proxy: http://${options.host || 'localhost'}:${bffTargetPort} (BFF → here)`);
899
+ console.log(`🔌 Mock Proxy: ${bffTargetUrl} (BFF → here)`);
770
900
  }
771
901
  console.log('');
772
- if (hasFunctions && !options.noFunctions) {
902
+ if (hasFunctions && !options.noFunctions && functionsReady) {
773
903
  console.log('💡 Azure Functions and Next.js BFF are connected');
774
904
  }
905
+ else if (hasFunctions && !options.noFunctions) {
906
+ console.log('💡 Azure Functions is still warming up; BFF routes can fail until the backend responds.');
907
+ }
775
908
  if (mockServer) {
776
909
  console.log('💡 Connector models served from mock server (Zod-generated data)');
777
910
  }
@@ -789,11 +922,11 @@ async function startDevEnvironment(options) {
789
922
  process.exit(1);
790
923
  }
791
924
  }
792
- async function runCommand(command, args, cwd, label, env) {
925
+ async function runCommand(command, args, cwd, label, env, shell = true) {
793
926
  await new Promise((resolve, reject) => {
794
927
  const child = (0, child_process_1.spawn)(command, args, {
795
928
  cwd,
796
- shell: true,
929
+ shell,
797
930
  stdio: 'inherit',
798
931
  env,
799
932
  });