swallowkit 2.0.0-alpha.2 → 2.0.0-alpha.4

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 (176) hide show
  1. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  2. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  3. package/dist/cli/commands/configure-mcp.js +42 -0
  4. package/dist/cli/commands/configure-mcp.js.map +1 -0
  5. package/dist/cli/commands/dev.d.ts +2 -2
  6. package/dist/cli/commands/dev.d.ts.map +1 -1
  7. package/dist/cli/commands/dev.js +39 -27
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts +6 -2
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +43 -122
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/migrations.d.ts.map +1 -1
  14. package/dist/cli/commands/migrations.js +25 -0
  15. package/dist/cli/commands/migrations.js.map +1 -1
  16. package/dist/cli/commands/provision.d.ts +2 -0
  17. package/dist/cli/commands/provision.d.ts.map +1 -1
  18. package/dist/cli/commands/provision.js +45 -16
  19. package/dist/cli/commands/provision.js.map +1 -1
  20. package/dist/cli/commands/scaffold.d.ts +4 -0
  21. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  22. package/dist/cli/commands/scaffold.js +40 -2
  23. package/dist/cli/commands/scaffold.js.map +1 -1
  24. package/dist/cli/index.d.ts +2 -0
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/index.js +27 -2
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/core/operations/model-migration.d.ts +22 -0
  29. package/dist/core/operations/model-migration.d.ts.map +1 -0
  30. package/dist/core/operations/model-migration.js +36 -0
  31. package/dist/core/operations/model-migration.js.map +1 -0
  32. package/dist/core/operations/provision-operations.d.ts +19 -1
  33. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  34. package/dist/core/operations/provision-operations.js +62 -35
  35. package/dist/core/operations/provision-operations.js.map +1 -1
  36. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  37. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  38. package/dist/core/operations/scaffold-apply.js +1 -0
  39. package/dist/core/operations/scaffold-apply.js.map +1 -1
  40. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  41. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  42. package/dist/core/operations/scaffold-machine.js.map +1 -1
  43. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  44. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  45. package/dist/core/operations/scaffold-plan.js +3 -1
  46. package/dist/core/operations/scaffold-plan.js.map +1 -1
  47. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  48. package/dist/core/project/agent-instructions.js +4 -1
  49. package/dist/core/project/agent-instructions.js.map +1 -1
  50. package/dist/core/project/boundaries.d.ts +5 -2
  51. package/dist/core/project/boundaries.d.ts.map +1 -1
  52. package/dist/core/project/boundaries.js +49 -18
  53. package/dist/core/project/boundaries.js.map +1 -1
  54. package/dist/core/project/capabilities.d.ts +1 -0
  55. package/dist/core/project/capabilities.d.ts.map +1 -1
  56. package/dist/core/project/capabilities.js +20 -2
  57. package/dist/core/project/capabilities.js.map +1 -1
  58. package/dist/core/project/home-page.d.ts +4 -0
  59. package/dist/core/project/home-page.d.ts.map +1 -0
  60. package/dist/core/project/home-page.js +113 -0
  61. package/dist/core/project/home-page.js.map +1 -0
  62. package/dist/core/project/mcp-config.d.ts +26 -0
  63. package/dist/core/project/mcp-config.d.ts.map +1 -0
  64. package/dist/core/project/mcp-config.js +117 -0
  65. package/dist/core/project/mcp-config.js.map +1 -0
  66. package/dist/core/project/react-project.d.ts +1 -1
  67. package/dist/core/project/react-project.d.ts.map +1 -1
  68. package/dist/core/project/react-project.js +45 -24
  69. package/dist/core/project/react-project.js.map +1 -1
  70. package/dist/core/project/workflows.d.ts.map +1 -1
  71. package/dist/core/project/workflows.js +9 -1
  72. package/dist/core/project/workflows.js.map +1 -1
  73. package/dist/core/scaffold/auth-generator.js +8 -8
  74. package/dist/core/scaffold/model-parser.d.ts +23 -0
  75. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  76. package/dist/core/scaffold/model-parser.js +407 -137
  77. package/dist/core/scaffold/model-parser.js.map +1 -1
  78. package/dist/core/scaffold/native-schema-generator.d.ts +11 -0
  79. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  80. package/dist/core/scaffold/native-schema-generator.js +279 -91
  81. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  82. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  83. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  84. package/dist/core/scaffold/openapi-generator.js +53 -3
  85. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  86. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  87. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  88. package/dist/core/scaffold/ui-generator.js +68 -7
  89. package/dist/core/scaffold/ui-generator.js.map +1 -1
  90. package/dist/core/verify/index.d.ts.map +1 -1
  91. package/dist/core/verify/index.js +24 -1
  92. package/dist/core/verify/index.js.map +1 -1
  93. package/dist/machine/index.d.ts.map +1 -1
  94. package/dist/machine/index.js +39 -15
  95. package/dist/machine/index.js.map +1 -1
  96. package/dist/mcp/index.d.ts +3 -2
  97. package/dist/mcp/index.d.ts.map +1 -1
  98. package/dist/mcp/index.js +137 -13
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/version-selection.d.ts +55 -0
  101. package/dist/mcp/version-selection.d.ts.map +1 -0
  102. package/dist/mcp/version-selection.js +152 -0
  103. package/dist/mcp/version-selection.js.map +1 -0
  104. package/dist/utils/azure-cli.d.ts +44 -0
  105. package/dist/utils/azure-cli.d.ts.map +1 -0
  106. package/dist/utils/azure-cli.js +164 -0
  107. package/dist/utils/azure-cli.js.map +1 -0
  108. package/dist/utils/package-manager.js +1 -1
  109. package/dist/utils/package-manager.js.map +1 -1
  110. package/dist/utils/python-uv.d.ts +5 -7
  111. package/dist/utils/python-uv.d.ts.map +1 -1
  112. package/dist/utils/python-uv.js +50 -24
  113. package/dist/utils/python-uv.js.map +1 -1
  114. package/dist/utils/react-dev.d.ts +4 -0
  115. package/dist/utils/react-dev.d.ts.map +1 -0
  116. package/dist/utils/react-dev.js +35 -0
  117. package/dist/utils/react-dev.js.map +1 -0
  118. package/dist/utils/semver.d.ts +14 -0
  119. package/dist/utils/semver.d.ts.map +1 -0
  120. package/dist/utils/semver.js +61 -0
  121. package/dist/utils/semver.js.map +1 -0
  122. package/package.json +1 -1
  123. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  124. package/src/__tests__/agent-loop-phase34.test.ts +29 -0
  125. package/src/__tests__/auth.test.ts +9 -0
  126. package/src/__tests__/azure-cli.test.ts +199 -0
  127. package/src/__tests__/boundaries.test.ts +42 -0
  128. package/src/__tests__/csharp-schema.test.ts +261 -0
  129. package/src/__tests__/dev.test.ts +13 -6
  130. package/src/__tests__/home-page.test.ts +50 -0
  131. package/src/__tests__/init.test.ts +57 -0
  132. package/src/__tests__/mcp-version.test.ts +262 -0
  133. package/src/__tests__/mcp.test.ts +49 -3
  134. package/src/__tests__/package-manager.test.ts +2 -2
  135. package/src/__tests__/plan-apply.test.ts +68 -0
  136. package/src/__tests__/python-uv.test.ts +50 -14
  137. package/src/__tests__/python-venv.test.ts +48 -0
  138. package/src/__tests__/react-dev.test.ts +72 -0
  139. package/src/__tests__/react.test.ts +19 -0
  140. package/src/__tests__/schema-analysis.test.ts +329 -0
  141. package/src/cli/commands/configure-mcp.ts +54 -0
  142. package/src/cli/commands/dev.ts +40 -31
  143. package/src/cli/commands/init.ts +48 -125
  144. package/src/cli/commands/migrations.ts +22 -0
  145. package/src/cli/commands/provision.ts +53 -17
  146. package/src/cli/commands/scaffold.ts +40 -2
  147. package/src/cli/index.ts +26 -3
  148. package/src/core/operations/model-migration.ts +52 -0
  149. package/src/core/operations/provision-operations.ts +86 -42
  150. package/src/core/operations/scaffold-apply.ts +2 -0
  151. package/src/core/operations/scaffold-machine.ts +1 -0
  152. package/src/core/operations/scaffold-plan.ts +5 -1
  153. package/src/core/project/agent-instructions.ts +4 -1
  154. package/src/core/project/agent-templates/AGENTS.md +39 -3
  155. package/src/core/project/agent-templates/CLAUDE.md +3 -1
  156. package/src/core/project/agent-templates/copilot-instructions.md +3 -1
  157. package/src/core/project/boundaries.ts +60 -18
  158. package/src/core/project/capabilities.ts +22 -2
  159. package/src/core/project/home-page.ts +114 -0
  160. package/src/core/project/mcp-config.ts +84 -0
  161. package/src/core/project/react-project.ts +49 -27
  162. package/src/core/project/workflows.ts +9 -1
  163. package/src/core/scaffold/auth-generator.ts +8 -8
  164. package/src/core/scaffold/model-parser.ts +413 -129
  165. package/src/core/scaffold/native-schema-generator.ts +305 -105
  166. package/src/core/scaffold/openapi-generator.ts +63 -5
  167. package/src/core/scaffold/ui-generator.ts +70 -8
  168. package/src/core/verify/index.ts +23 -1
  169. package/src/machine/index.ts +42 -18
  170. package/src/mcp/index.ts +156 -14
  171. package/src/mcp/version-selection.ts +163 -0
  172. package/src/utils/azure-cli.ts +154 -0
  173. package/src/utils/package-manager.ts +1 -1
  174. package/src/utils/python-uv.ts +53 -33
  175. package/src/utils/react-dev.ts +31 -0
  176. package/src/utils/semver.ts +59 -0
@@ -19,15 +19,15 @@ import {
19
19
  import { BackendLanguage } from '../../types';
20
20
  import { detectFromProject, getCommands } from '../../utils/package-manager';
21
21
  import {
22
- buildProjectLocalUvEnv,
23
22
  buildProjectLocalUvInstallerEnv,
24
23
  buildUvPipInstallArgs,
25
- buildUvVenvArgs,
24
+ ensureUvVirtualEnvironment,
26
25
  getProjectLocalUvInstallerCommand,
27
26
  getProjectLocalUvPaths,
28
27
  getPythonProjectRoot,
29
28
  } from '../../utils/python-uv';
30
29
  import { ConnectorMockServer } from '../../core/mock/connector-mock-server';
30
+ import { buildReactDevConfigSource } from '../../utils/react-dev';
31
31
 
32
32
  export interface DevOptions {
33
33
  port?: string;
@@ -65,8 +65,8 @@ function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions
65
65
  };
66
66
  }
67
67
 
68
- export function buildFunctionsStartArgs(functionsPort: string): string[] {
69
- return ['start', '--port', functionsPort];
68
+ export function buildFunctionsStartArgs(functionsPort: string, backendLanguage: BackendLanguage = 'typescript'): string[] {
69
+ return [...(backendLanguage === 'csharp' ? [] : ['start']), '--port', functionsPort];
70
70
  }
71
71
 
72
72
  export function parseCoreToolsVersion(output: string): string | null {
@@ -105,15 +105,15 @@ export function buildFunctionsCoreToolsCommand(
105
105
 
106
106
  return {
107
107
  command: 'npm',
108
- argsPrefix: ['exec', '--yes', NPM_CORE_TOOLS_PACKAGE, '--'],
109
- label: `npm exec ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
108
+ argsPrefix: ['exec', '--yes', '--package', NPM_CORE_TOOLS_PACKAGE, '--', 'dotnet', 'run', '--no-launch-profile', '--'],
109
+ label: `dotnet run with npm ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
110
110
  };
111
111
  }
112
112
 
113
113
  return {
114
- command: 'func',
115
- argsPrefix: [],
116
- label: installedVersion ? `func ${installedVersion}` : 'func',
114
+ command: backendLanguage === 'csharp' ? 'dotnet' : 'func',
115
+ argsPrefix: backendLanguage === 'csharp' ? ['run', '--no-launch-profile', '--'] : [],
116
+ label: backendLanguage === 'csharp' ? 'dotnet run' : installedVersion ? `func ${installedVersion}` : 'func',
117
117
  };
118
118
  }
119
119
 
@@ -138,8 +138,8 @@ export function buildSwaStartArgs(host: string | undefined, nextPort: string, sw
138
138
  ];
139
139
  }
140
140
 
141
- export function buildReactDevArgs(pm: string, port: string, host = 'localhost'): string[] {
142
- const args = ['vite', '--port', port, '--host', host, '--strictPort'];
141
+ export function buildReactDevArgs(pm: string, port: string, host = 'localhost', configPath?: string): string[] {
142
+ const args = ['vite', '--port', port, '--host', host, '--strictPort', ...(configPath ? ['--config', configPath] : [])];
143
143
  return pm === 'pnpm' ? ['exec', ...args] : args;
144
144
  }
145
145
 
@@ -270,7 +270,7 @@ async function checkCommand(
270
270
  }
271
271
 
272
272
  async function resolveProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
273
- const uvEnv = buildProjectLocalUvEnv(process.env, projectRoot);
273
+ const uvEnv = { ...process.env };
274
274
  const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
275
275
 
276
276
  if (await checkCommand('uv', ['--version'], { shell: false })) {
@@ -471,22 +471,8 @@ async function bridgePythonCoreToolsForWindowsArm64(
471
471
  async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<NodeJS.ProcessEnv> {
472
472
  const projectRoot = getPythonProjectRoot(functionsDir);
473
473
  const { command: uvCommand, env: uvEnv } = await resolveProjectLocalUvCommand(projectRoot);
474
- const { venvDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
475
- const hasUsableVirtualEnv = fs.existsSync(pythonExecutable) && await checkCommand(pythonExecutable, ['--version'], {
476
- cwd: functionsDir,
477
- env: uvEnv,
478
- shell: false,
479
- });
480
-
481
- if (!hasUsableVirtualEnv) {
482
- const venvArgs = buildUvVenvArgs('.venv');
483
- if (fs.existsSync(venvDir)) {
484
- venvArgs.push('--clear');
485
- }
486
-
487
- console.log('📦 Creating Python virtual environment with uv...');
488
- await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
489
- }
474
+ const { venvDir } = getPythonVirtualEnvPaths(functionsDir);
475
+ const pythonExecutable = await ensureUvVirtualEnvironment(uvCommand, venvDir, functionsDir, uvEnv);
490
476
 
491
477
  console.log('📦 Installing Python Azure Functions dependencies with uv...');
492
478
  await runCommand(
@@ -538,7 +524,7 @@ export async function assertDevPortAvailable(port: number, host: string): Promis
538
524
  }
539
525
 
540
526
  function stopDevProcesses(processes: ChildProcess[]): void {
541
- const active = processes.filter(child => child.pid && !child.killed);
527
+ const active = processes.filter(child => child.pid && !child.killed && child.exitCode === null && child.signalCode === null);
542
528
  if (process.platform === 'win32' && active.length) {
543
529
  // Windows shell termination alone leaves Vite, Core Tools, and SWA running.
544
530
  const ids = active.map(child => child.pid).join(',');
@@ -721,6 +707,14 @@ async function startDevEnvironment(options: DevOptions) {
721
707
  const functionsBaseUrl = buildFunctionsBaseUrl(options.host, functionsPort);
722
708
  let functionsReadinessPromise: Promise<boolean> | null = null;
723
709
  let functionsReady = !!options.noFunctions;
710
+ let reactDevConfigDir: string | undefined;
711
+ const cleanupReactDevConfig = () => {
712
+ if (reactDevConfigDir) {
713
+ fs.rmSync(reactDevConfigDir, { recursive: true, force: true });
714
+ reactDevConfigDir = undefined;
715
+ }
716
+ };
717
+ process.once('exit', cleanupReactDevConfig);
724
718
 
725
719
  // Cleanup processes on Ctrl+C
726
720
  process.on('SIGINT', async () => {
@@ -939,7 +933,7 @@ async function startDevEnvironment(options: DevOptions) {
939
933
 
940
934
  // Azure Functions を起動
941
935
  const functionsCommand = functionsCoreToolsCommand ?? buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
942
- const funcProcess = spawn(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort)], {
936
+ const funcProcess = spawn(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort, backendLanguage)], {
943
937
  cwd: functionsDir,
944
938
  shell: true,
945
939
  stdio: 'pipe', // Always pipe to capture output
@@ -987,6 +981,8 @@ async function startDevEnvironment(options: DevOptions) {
987
981
  console.error('⚠️ Azure Functions startup error:', error.message);
988
982
  console.log('💡 Please ensure Azure Functions Core Tools is installed');
989
983
  console.log(` ${pmCmd.addGlobal} azure-functions-core-tools@4`);
984
+ stopDevProcesses(processes);
985
+ process.exit(1);
990
986
  });
991
987
 
992
988
  funcProcess.on('close', (code) => {
@@ -997,6 +993,8 @@ async function startDevEnvironment(options: DevOptions) {
997
993
  console.log(' Reinstall a matching Core Tools v4 package (Windows users should prefer the official x64/x86 MSI for their machine).');
998
994
  console.log(' SwallowKit local Python dev uses functions/.venv and requirements.txt, but Core Tools still needs its own bundled Python worker.');
999
995
  }
996
+ stopDevProcesses(processes);
997
+ process.exit(code || 1);
1000
998
  }
1001
999
  });
1002
1000
 
@@ -1083,7 +1081,17 @@ async function startDevEnvironment(options: DevOptions) {
1083
1081
  }
1084
1082
 
1085
1083
  // 5. Start Next.js development server
1086
- const nextArgs = react ? buildReactDevArgs(pm, port, options.host) : buildNextDevArgs(pm, port);
1084
+ let reactDevConfigPath: string | undefined;
1085
+ if (react) {
1086
+ const stateDir = path.join(process.cwd(), '.swallowkit');
1087
+ fs.mkdirSync(stateDir, { recursive: true });
1088
+ reactDevConfigDir = fs.mkdtempSync(path.join(stateDir, 'vite-dev-'));
1089
+ reactDevConfigPath = path.join(reactDevConfigDir, 'config.mjs');
1090
+ fs.writeFileSync(reactDevConfigPath, buildReactDevConfigSource());
1091
+ }
1092
+ const nextArgs = react
1093
+ ? buildReactDevArgs(pm, port, options.host, reactDevConfigPath && path.relative(process.cwd(), reactDevConfigPath))
1094
+ : buildNextDevArgs(pm, port);
1087
1095
 
1088
1096
  // Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
1089
1097
  // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
@@ -1141,6 +1149,7 @@ async function startDevEnvironment(options: DevOptions) {
1141
1149
 
1142
1150
  nextProcess.on('error', (error) => {
1143
1151
  console.error(`❌ ${frontendLabel} startup error:`, error.message);
1152
+ stopDevProcesses(processes);
1144
1153
  process.exit(1);
1145
1154
  });
1146
1155
 
@@ -25,7 +25,9 @@ import {
25
25
  buildCSharpCodegenToolManifestSource,
26
26
  buildPythonCodegenRequirementsSource,
27
27
  } from "../../core/scaffold/native-schema-generator";
28
- import { getProjectLocalUvPaths } from "../../utils/python-uv";
28
+ import { DEFAULT_AZURE_PYTHON_VERSION } from "../../utils/python-uv";
29
+ import { buildHomePage } from "../../core/project/home-page";
30
+ import { MCP_LATEST, McpPinDecision, lookupPublishedLatestVersion, resolveMcpPinVersion } from "../../core/project/mcp-config";
29
31
 
30
32
  interface InitOptions {
31
33
  name: string;
@@ -144,7 +146,7 @@ function getFunctionsRuntimeConfig(backendLanguage: BackendLanguage): { name: st
144
146
  return { name: "dotnet-isolated", version: "10.0" };
145
147
  }
146
148
  if (backendLanguage === "python") {
147
- return { name: "python", version: "3.11" };
149
+ return { name: "python", version: DEFAULT_AZURE_PYTHON_VERSION };
148
150
  }
149
151
  return { name: "node", version: "22" };
150
152
  }
@@ -344,7 +346,7 @@ export async function initCommand(options: InitOptions) {
344
346
  const azureConfig = await promptAzureConfig(options);
345
347
 
346
348
  if (frontend === "react") {
347
- createReactProject(projectDir, options.name);
349
+ createReactProject(projectDir, options.name, pm);
348
350
  } else {
349
351
  await createNextJsProject(options.name, pm);
350
352
  await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
@@ -711,7 +713,7 @@ export function buildCSharpFunctionsProjectSource(): string {
711
713
  <None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
712
714
  </ItemGroup>
713
715
  <ItemGroup>
714
- <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
716
+ <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.63.2" />
715
717
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
716
718
  <PackageReference Include="Azure.Identity" Version="1.17.0" />
717
719
  <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
@@ -778,14 +780,30 @@ function getSwallowKitPackageMetadata(): { name: string; version: string } {
778
780
  };
779
781
  }
780
782
 
781
- export function buildGeneratedProjectDevDependencies(): Record<string, string> {
783
+ export function buildGeneratedProjectDevDependencies(frontend: FrontendFramework = "nextjs"): Record<string, string> {
782
784
  const { name, version } = getSwallowKitPackageMetadata();
783
785
  return {
784
786
  [name]: version,
787
+ ...(frontend === "nextjs" ? { "@tailwindcss/postcss": "^4.0.0" } : {}),
785
788
  };
786
789
  }
787
790
 
788
- export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm'): string {
791
+ export function ensureNextTailwindPostcssConfig(projectDir: string): void {
792
+ if (["postcss.config.mjs", "postcss.config.js", "postcss.config.cjs", "postcss.config.json"]
793
+ .some(file => fs.existsSync(path.join(projectDir, file)))) {
794
+ return;
795
+ }
796
+
797
+ // New create-next-app templates can configure Tailwind for Turbopack only.
798
+ fs.writeFileSync(path.join(projectDir, "postcss.config.mjs"), `export default {
799
+ plugins: {
800
+ "@tailwindcss/postcss": {},
801
+ },
802
+ };
803
+ `);
804
+ }
805
+
806
+ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm', mcpVersion: string = MCP_LATEST): string {
789
807
  const { name } = getSwallowKitPackageMetadata();
790
808
  return JSON.stringify(
791
809
  {
@@ -797,7 +815,7 @@ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm
797
815
  : ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
798
816
  cwd: ".",
799
817
  env: {
800
- SWALLOWKIT_MCP_VERSION: "latest",
818
+ SWALLOWKIT_MCP_VERSION: mcpVersion,
801
819
  },
802
820
  },
803
821
  },
@@ -807,6 +825,15 @@ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm
807
825
  );
808
826
  }
809
827
 
828
+ /** latest by default; pinned to this CLI version only when it is newer than published latest. */
829
+ export function selectInitialMcpVersion(
830
+ lookupLatest: (packageName: string) => string | null = lookupPublishedLatestVersion
831
+ ): McpPinDecision {
832
+ const { name, version } = getSwallowKitPackageMetadata();
833
+ if (version === MCP_LATEST) return { version: MCP_LATEST, reason: "SwallowKit package metadata is unavailable." };
834
+ return resolveMcpPinVersion(version, lookupLatest(name));
835
+ }
836
+
810
837
  async function addSwallowKitFiles(
811
838
  projectDir: string,
812
839
  options: InitOptions,
@@ -831,7 +858,7 @@ async function addSwallowKitFiles(
831
858
  };
832
859
  packageJson.devDependencies = {
833
860
  ...packageJson.devDependencies,
834
- ...buildGeneratedProjectDevDependencies(),
861
+ ...buildGeneratedProjectDevDependencies(frontend),
835
862
  };
836
863
 
837
864
  packageJson.scripts = {
@@ -865,6 +892,8 @@ async function addSwallowKitFiles(
865
892
 
866
893
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
867
894
 
895
+ if (frontend === "nextjs") ensureNextTailwindPostcssConfig(projectDir);
896
+
868
897
  // Create pnpm-workspace.yaml for local pnpm development when needed.
869
898
  if (wsConfig.type === 'file') {
870
899
  fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content + (frontend === "react" ? " esbuild: true\n" : ""));
@@ -1492,7 +1521,6 @@ public sealed class GreetFunction
1492
1521
  }
1493
1522
 
1494
1523
  function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
1495
- fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1496
1524
  ensureProjectGitignoreEntry(projectDir, '.uv');
1497
1525
 
1498
1526
  fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
@@ -1632,117 +1660,8 @@ export async function POST(request: NextRequest) {
1632
1660
 
1633
1661
  async function createHomePage(projectDir: string, pm: PackageManager = 'pnpm') {
1634
1662
  console.log('📦 Creating home page...\n');
1635
-
1636
- const pmCmd = getCommands(pm);
1637
-
1638
- const pageContent = `'use client'
1639
1663
 
1640
- export const dynamic = 'force-dynamic';
1641
-
1642
- import { useState } from 'react';
1643
- import { scaffoldConfig } from '@/lib/scaffold-config';
1644
-
1645
- export default function Home() {
1646
- const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
1647
- const [message, setMessage] = useState('');
1648
-
1649
- const testConnection = async () => {
1650
- setGreetingStatus('loading');
1651
- try {
1652
- const response = await fetch('/api/greet?name=SwallowKit');
1653
- const data = await response.json();
1654
- if (!response.ok) {
1655
- throw new Error(data.error || \`Server error: \${response.status}\`);
1656
- }
1657
- setMessage(data.message);
1658
- setGreetingStatus('success');
1659
- } catch (error) {
1660
- setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
1661
- setGreetingStatus('error');
1662
- }
1663
- };
1664
-
1665
- return (
1666
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
1667
- <div className="container mx-auto px-4 py-12">
1668
- <header className="text-center mb-16">
1669
- <h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
1670
- Welcome to SwallowKit
1671
- </h1>
1672
- <p className="text-xl text-gray-600 dark:text-gray-400">
1673
- Next.js on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
1674
- </p>
1675
- </header>
1676
-
1677
- {/* Connection Test */}
1678
- <section className="max-w-2xl mx-auto mb-12">
1679
- <div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
1680
- <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
1681
- Test BFF → Functions Connection
1682
- </h2>
1683
- <button
1684
- onClick={testConnection}
1685
- disabled={greetingStatus === 'loading'}
1686
- className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
1687
- >
1688
- {greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
1689
- </button>
1690
- {greetingStatus === 'success' && (
1691
- <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
1692
- <p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
1693
- <p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
1694
- </div>
1695
- )}
1696
- {greetingStatus === 'error' && (
1697
- <div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
1698
- <p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
1699
- <p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
1700
- </div>
1701
- )}
1702
- </div>
1703
- </section>
1704
-
1705
- {/* Scaffolded Models Menu */}
1706
- {scaffoldConfig.models.length > 0 ? (
1707
- <section className="max-w-6xl mx-auto">
1708
- <h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
1709
- <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
1710
- {scaffoldConfig.models.map((model) => (
1711
- <a
1712
- key={model.name}
1713
- href={model.path}
1714
- className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
1715
- >
1716
- <h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
1717
- <p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
1718
- </a>
1719
- ))}
1720
- </div>
1721
- </section>
1722
- ) : (
1723
- <section className="max-w-2xl mx-auto text-center">
1724
- <div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
1725
- <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
1726
- <p className="text-gray-600 dark:text-gray-400 mb-6">
1727
- Create your first model with Zod and generate CRUD operations automatically.
1728
- </p>
1729
- <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
1730
- ${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
1731
- </code>
1732
- </div>
1733
- </section>
1734
- )}
1735
-
1736
- <footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
1737
- <p>Built with SwallowKit</p>
1738
- </footer>
1739
- </div>
1740
- </div>
1741
- );
1742
- }
1743
- `;
1744
-
1745
- fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), pageContent);
1664
+ fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), buildHomePage("nextjs", pm));
1746
1665
 
1747
1666
  console.log('✅ Home page created\n');
1748
1667
 
@@ -1796,9 +1715,8 @@ function createReadme(
1796
1715
  const backendScaffoldNote = backendLanguage === 'typescript'
1797
1716
  ? '- Azure Functions CRUD endpoints'
1798
1717
  : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1799
- const pythonUvPaths = getProjectLocalUvPaths(projectDir);
1800
1718
  const pythonLocalDevNote = backendLanguage === 'python'
1801
- ? `\n**Python local dev note**: SwallowKit uses \`uv\` for Python backends and keeps the managed Python runtime under \`${path.relative(projectDir, pythonUvPaths.pythonInstallDir)}\`. Local Azure Functions runs from \`functions/.venv\`, schema generation uses \`functions/.codegen-venv\`, and \`swallowkit dev\` bootstraps a project-local \`uv\` binary automatically when needed. Keep \`functions/requirements.txt\` and \`functions/requirements.codegen.txt\` as the dependency sources of truth.\n`
1719
+ ? `\n**Python local dev note**: SwallowKit uses normal \`uv\` interpreter discovery and settings, including installed Python, \`.python-version\`, and \`UV_PYTHON\`. It does not pin local Python to the Azure deployment version (${DEFAULT_AZURE_PYTHON_VERSION}). Local Functions uses \`functions/.venv\` and schema generation uses \`functions/.codegen-venv\`. Existing environments are preserved; explicitly recreate them with uv when switching interpreters. A missing uv binary is bootstrapped under \`.uv/bin\` without changing PATH. Keep \`functions/requirements.txt\` and \`functions/requirements.codegen.txt\` as the dependency sources of truth.\n`
1802
1720
  : '';
1803
1721
 
1804
1722
  const readme = `# ${projectName}
@@ -2031,11 +1949,16 @@ function createAiAgentFiles(
2031
1949
  console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
2032
1950
  console.log(' ✅ .github/instructions/ (path-specific instructions)');
2033
1951
 
1952
+ const mcpVersion = selectInitialMcpVersion();
2034
1953
  fs.writeFileSync(
2035
1954
  path.join(projectDir, '.mcp.json'),
2036
- buildSwallowKitMcpProjectConfigSource(pm)
1955
+ buildSwallowKitMcpProjectConfigSource(pm, mcpVersion.version)
1956
+ );
1957
+ console.log(
1958
+ mcpVersion.version === MCP_LATEST
1959
+ ? ' ✅ .mcp.json (project-scoped MCP bootstrap, resolves latest)'
1960
+ : ` ✅ .mcp.json (project-scoped MCP bootstrap, pinned to ${mcpVersion.version}: ${mcpVersion.reason} Run "${runCmd} swallowkit configure-mcp" to re-select later.)`
2037
1961
  );
2038
- console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
2039
1962
 
2040
1963
  writeWorkflowDocs(projectDir, runCmd);
2041
1964
  console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
@@ -2949,7 +2872,7 @@ jobs:
2949
2872
  ${commonSetup} - name: Setup Python
2950
2873
  uses: actions/setup-python@v5
2951
2874
  with:
2952
- python-version: '3.11'
2875
+ python-version: '${DEFAULT_AZURE_PYTHON_VERSION}'
2953
2876
 
2954
2877
  - name: Install Functions dependencies
2955
2878
  run: |
@@ -3138,7 +3061,7 @@ variables:
3138
3061
  steps:
3139
3062
  ${commonSetup} - task: UsePythonVersion@0
3140
3063
  inputs:
3141
- versionSpec: '3.11'
3064
+ versionSpec: '${DEFAULT_AZURE_PYTHON_VERSION}'
3142
3065
  displayName: 'Install Python'
3143
3066
 
3144
3067
  - script: |
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { ensureSwallowKitProject } from "../../core/config";
3
3
  import { initializeLegacyMigrations } from "../../core/operations/migration-generator";
4
+ import { addModelContainerMigration } from "../../core/operations/model-migration";
4
5
 
5
6
  export const migrationsCommand = new Command("migrations")
6
7
  .description("Manage versioned Azure infrastructure migrations");
@@ -18,3 +19,24 @@ migrationsCommand
18
19
  console.log(`Migration baseline 0 created for project ${manifest.projectId}.`);
19
20
  console.log("Commit infra/migrations/manifest.json, then run provision with --adopt-existing --baseline 0.");
20
21
  });
22
+
23
+ migrationsCommand
24
+ .command("add-model")
25
+ .description("Generate only the Cosmos DB container migration for a model (e.g. after scaffold --skip-infra)")
26
+ .argument("<model>", "Model file or model name")
27
+ .option("--dry-run", "Show the planned infra changes without writing anything", false)
28
+ .action(async (model: string, options: { dryRun?: boolean }) => {
29
+ ensureSwallowKitProject("migrations add-model");
30
+ const result = await addModelContainerMigration(model, { dryRun: options.dryRun });
31
+ for (const operation of result.operations) {
32
+ if (operation.action !== "skip") console.log(` ${operation.action.padEnd(7)} ${operation.path}`);
33
+ }
34
+ if (!result.migration) {
35
+ console.log(`No migration needed for ${result.model}: ${result.reason}`);
36
+ } else if (result.operations.every((operation) => operation.action === "skip")) {
37
+ console.log(`Migration ${result.migration.version} for ${result.model} already exists: ${result.migration.template}`);
38
+ } else {
39
+ console.log(`${result.dryRun ? "Would create" : "Created"} migration ${result.migration.version} for ${result.model}: ${result.migration.template}`);
40
+ if (!result.dryRun) console.log("Commit the migration, then run provision to plan and apply it.");
41
+ }
42
+ });
@@ -1,7 +1,27 @@
1
1
  import { Command } from "commander";
2
2
  import prompts from "prompts";
3
3
  import { ensureSwallowKitProject } from "../../core/config";
4
- import { applyProvisionOperation, planProvisionOperation } from "../../core/operations/provision-operations";
4
+ import {
5
+ INTERACTIVE_PROVISION_RETRY_HINT,
6
+ ProvisionPlanData,
7
+ executeProvisionPlan,
8
+ planProvisionOperation,
9
+ } from "../../core/operations/provision-operations";
10
+
11
+ export function describeProvisionAction(plan: Pick<ProvisionPlanData, "mode" | "resourceGroup" | "location" | "pendingMigrations">): string {
12
+ switch (plan.mode) {
13
+ case "bootstrap":
14
+ return `Resource group ${plan.resourceGroup} does not exist yet. It will be created in ${plan.location} and the infrastructure in infra/main.bicep will be deployed.`;
15
+ case "migrate":
16
+ return `${plan.pendingMigrations.length} infrastructure migration(s) will be deployed to ${plan.resourceGroup}: ${plan.pendingMigrations.map((migration) => `${migration.version}-${migration.slug}`).join(", ")}.`;
17
+ case "adopt":
18
+ return `The existing resource group ${plan.resourceGroup} will be recorded as SwallowKit baseline 0. Nothing will be deployed.`;
19
+ case "reconcile":
20
+ return `infra/main.bicep will be redeployed to ${plan.resourceGroup}. Template-managed settings may be reset.`;
21
+ default:
22
+ return `Resource group ${plan.resourceGroup}:`;
23
+ }
24
+ }
5
25
 
6
26
  const PRIMARY_REGIONS = [
7
27
  { title: "Japan East (japaneast)", value: "japaneast" },
@@ -70,16 +90,17 @@ export const provisionCommand = new Command("provision")
70
90
  .option("--subscription <id>", "Azure subscription ID")
71
91
  .option("--location <region>", "Primary location for Functions and Cosmos DB")
72
92
  .option("--swa-location <region>", "Static Web App location")
73
- .option("--what-if", "Include Azure what-if evidence", false)
93
+ .option("--what-if", "Include Azure what-if evidence in the preview", false)
74
94
  .option("--adopt-existing", "Adopt an existing untagged resource group without deploying main.bicep", false)
75
95
  .option("--baseline <version>", "Baseline version for adoption (currently 0)")
76
96
  .option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
77
- .option("--approve", "Apply the displayed plan without an interactive confirmation", false)
97
+ .option("--approve", "Run the displayed Azure commands without the confirmation prompt", false)
78
98
  .action(async (options: ProvisionCliOptions) => {
79
99
  ensureSwallowKitProject("provision");
80
100
  const locations = await resolveLocations(options);
81
101
  const baseline = options.baseline === undefined ? undefined : Number(options.baseline);
82
- const plan = await planProvisionOperation({
102
+ // Preview in memory: the same Azure checks as the agent workflow, but no saved plan file.
103
+ const preview = await planProvisionOperation({
83
104
  resourceGroup: options.resourceGroup,
84
105
  subscription: options.subscription,
85
106
  location: locations.location,
@@ -88,37 +109,52 @@ export const provisionCommand = new Command("provision")
88
109
  adoptExisting: options.adoptExisting,
89
110
  baseline,
90
111
  reconcile: options.reconcile,
91
- });
112
+ }, { persist: false });
92
113
 
93
- console.log(`Provision mode: ${plan.mode}`);
94
- for (const warning of plan.warnings) console.warn(`Warning: ${warning}`);
95
- if (plan.pendingMigrations.length > 0) {
96
- console.log(`Pending migrations: ${plan.pendingMigrations.map((migration) => `${migration.version}-${migration.slug}`).join(", ")}`);
114
+ for (const warning of preview.warnings) console.warn(`Warning: ${warning}`);
115
+
116
+ if (preview.mode === "offline") {
117
+ console.log([
118
+ "",
119
+ "❌ Azure CLI is unavailable, so the current state of Azure could not be checked. Nothing was changed in Azure.",
120
+ " 1. Make sure `az --version` works in this terminal (install: https://aka.ms/azure-cli).",
121
+ " 2. Sign in with `az login` (and `az account set --subscription <id>` if needed).",
122
+ " 3. Run this command again.",
123
+ ].join("\n"));
124
+ process.exitCode = 1;
125
+ return;
97
126
  }
98
- for (const command of plan.commands) console.log(` ${command}`);
99
127
 
100
- if (plan.mode === "adoption-required") {
128
+ if (preview.mode === "adoption-required") {
101
129
  throw new Error('Existing resource group requires: migrations init --legacy-baseline, then provision --adopt-existing --baseline 0 --what-if.');
102
130
  }
103
131
 
132
+ if (preview.mode === "noop") {
133
+ console.log(`Resource group ${preview.resourceGroup} is already up to date. Nothing to do.`);
134
+ return;
135
+ }
136
+
137
+ console.log(`\n${describeProvisionAction(preview)}`);
138
+ console.log("The following Azure CLI commands will run:");
139
+ for (const command of preview.commands) console.log(` ${command}`);
140
+
104
141
  let approved = options.approve === true;
105
142
  if (!approved) {
106
143
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
107
- throw new Error("Non-interactive provision requires --approve after reviewing the plan.");
144
+ throw new Error("Non-interactive provision requires --approve after reviewing the commands above.");
108
145
  }
109
146
  const answer = await prompts({
110
147
  type: "confirm",
111
148
  name: "approved",
112
- message: `Apply the ${plan.mode} plan?`,
149
+ message: "Run these Azure commands? This may create billable resources.",
113
150
  initial: false,
114
151
  });
115
152
  approved = answer.approved === true;
116
153
  }
117
154
  if (!approved) {
118
- console.log("Provision cancelled.");
155
+ console.log("Provision cancelled. Nothing was changed in Azure.");
119
156
  return;
120
157
  }
121
158
 
122
- const result = await applyProvisionOperation({ planId: plan.planId, approve: true });
123
- console.log(`Provision completed (${plan.mode}). ${result.executedCommands.length} command(s) executed.`);
124
- });
159
+ const result = await executeProvisionPlan(preview, { approve: true, retryHint: INTERACTIVE_PROVISION_RETRY_HINT });
160
+ console.log(`✅ Provisioning completed. ${result.executedCommands.length} Azure CLI command(s) ran.`); });