swallowkit 1.0.0-beta.55 → 1.0.0-beta.57

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 (141) hide show
  1. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  2. package/dist/cli/commands/add-auth.js +154 -157
  3. package/dist/cli/commands/add-auth.js.map +1 -1
  4. package/dist/cli/commands/init.d.ts +5 -0
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +178 -25
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/scaffold.d.ts +7 -2
  9. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  10. package/dist/cli/commands/scaffold.js +113 -123
  11. package/dist/cli/commands/scaffold.js.map +1 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +127 -2
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/core/config.d.ts +1 -1
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js +8 -4
  18. package/dist/core/config.js.map +1 -1
  19. package/dist/core/operations/auth-operations.d.ts +57 -0
  20. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  21. package/dist/core/operations/auth-operations.js +269 -0
  22. package/dist/core/operations/auth-operations.js.map +1 -0
  23. package/dist/core/operations/create-model.d.ts.map +1 -1
  24. package/dist/core/operations/create-model.js +23 -13
  25. package/dist/core/operations/create-model.js.map +1 -1
  26. package/dist/core/operations/file-session.d.ts +56 -0
  27. package/dist/core/operations/file-session.d.ts.map +1 -0
  28. package/dist/core/operations/file-session.js +203 -0
  29. package/dist/core/operations/file-session.js.map +1 -0
  30. package/dist/core/operations/provision-operations.d.ts +52 -0
  31. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  32. package/dist/core/operations/provision-operations.js +211 -0
  33. package/dist/core/operations/provision-operations.js.map +1 -0
  34. package/dist/core/operations/runtime.d.ts +5 -0
  35. package/dist/core/operations/runtime.d.ts.map +1 -1
  36. package/dist/core/operations/runtime.js +23 -0
  37. package/dist/core/operations/runtime.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  40. package/dist/core/operations/scaffold-apply.js +57 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -0
  42. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  43. package/dist/core/operations/scaffold-machine.js +1 -13
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  47. package/dist/core/operations/scaffold-plan.js +187 -0
  48. package/dist/core/operations/scaffold-plan.js.map +1 -0
  49. package/dist/core/project/artifacts.d.ts +58 -0
  50. package/dist/core/project/artifacts.d.ts.map +1 -0
  51. package/dist/core/project/artifacts.js +179 -0
  52. package/dist/core/project/artifacts.js.map +1 -0
  53. package/dist/core/project/boundaries.d.ts +50 -0
  54. package/dist/core/project/boundaries.d.ts.map +1 -0
  55. package/dist/core/project/boundaries.js +117 -0
  56. package/dist/core/project/boundaries.js.map +1 -0
  57. package/dist/core/project/drift.d.ts +29 -0
  58. package/dist/core/project/drift.d.ts.map +1 -0
  59. package/dist/core/project/drift.js +188 -0
  60. package/dist/core/project/drift.js.map +1 -0
  61. package/dist/core/project/infra.d.ts +49 -0
  62. package/dist/core/project/infra.d.ts.map +1 -0
  63. package/dist/core/project/infra.js +167 -0
  64. package/dist/core/project/infra.js.map +1 -0
  65. package/dist/core/project/state.d.ts +15 -0
  66. package/dist/core/project/state.d.ts.map +1 -0
  67. package/dist/core/project/state.js +106 -0
  68. package/dist/core/project/state.js.map +1 -0
  69. package/dist/core/project/workflows.d.ts +43 -0
  70. package/dist/core/project/workflows.d.ts.map +1 -0
  71. package/dist/core/project/workflows.js +255 -0
  72. package/dist/core/project/workflows.js.map +1 -0
  73. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  74. package/dist/core/scaffold/auth-generator.js +24 -27
  75. package/dist/core/scaffold/auth-generator.js.map +1 -1
  76. package/dist/core/scaffold/functions-generator.js +9 -9
  77. package/dist/core/scaffold/native-schema-generator.d.ts +7 -1
  78. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  79. package/dist/core/scaffold/native-schema-generator.js +104 -47
  80. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  81. package/dist/core/scaffold/nextjs-generator.js +6 -6
  82. package/dist/core/scaffold/ui-generator.js +19 -19
  83. package/dist/core/scaffold/ui-generator.js.map +1 -1
  84. package/dist/core/verify/index.d.ts +62 -0
  85. package/dist/core/verify/index.d.ts.map +1 -0
  86. package/dist/core/verify/index.js +343 -0
  87. package/dist/core/verify/index.js.map +1 -0
  88. package/dist/machine/contracts.d.ts +8 -0
  89. package/dist/machine/contracts.d.ts.map +1 -1
  90. package/dist/machine/errors.d.ts +4 -1
  91. package/dist/machine/errors.d.ts.map +1 -1
  92. package/dist/machine/errors.js +9 -1
  93. package/dist/machine/errors.js.map +1 -1
  94. package/dist/machine/index.d.ts.map +1 -1
  95. package/dist/machine/index.js +350 -16
  96. package/dist/machine/index.js.map +1 -1
  97. package/dist/mcp/index.d.ts.map +1 -1
  98. package/dist/mcp/index.js +202 -1
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/types/index.d.ts +14 -0
  101. package/dist/types/index.d.ts.map +1 -1
  102. package/package.json +1 -1
  103. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
  104. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
  105. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
  106. package/src/__tests__/agent-loop-phase34.test.ts +449 -0
  107. package/src/__tests__/file-session.test.ts +98 -0
  108. package/src/__tests__/init.test.ts +50 -0
  109. package/src/__tests__/mcp.test.ts +113 -5
  110. package/src/__tests__/plan-apply.test.ts +461 -0
  111. package/src/__tests__/scaffold.test.ts +2 -2
  112. package/src/cli/commands/add-auth.ts +164 -164
  113. package/src/cli/commands/init.ts +186 -24
  114. package/src/cli/commands/scaffold.ts +136 -130
  115. package/src/cli/index.ts +148 -2
  116. package/src/core/config.ts +5 -4
  117. package/src/core/operations/auth-operations.ts +353 -0
  118. package/src/core/operations/create-model.ts +25 -13
  119. package/src/core/operations/file-session.ts +222 -0
  120. package/src/core/operations/provision-operations.ts +297 -0
  121. package/src/core/operations/runtime.ts +23 -0
  122. package/src/core/operations/scaffold-apply.ts +106 -0
  123. package/src/core/operations/scaffold-machine.ts +2 -16
  124. package/src/core/operations/scaffold-plan.ts +215 -0
  125. package/src/core/project/artifacts.ts +205 -0
  126. package/src/core/project/boundaries.ts +170 -0
  127. package/src/core/project/drift.ts +202 -0
  128. package/src/core/project/infra.ts +196 -0
  129. package/src/core/project/state.ts +77 -0
  130. package/src/core/project/workflows.ts +249 -0
  131. package/src/core/scaffold/auth-generator.ts +24 -27
  132. package/src/core/scaffold/functions-generator.ts +9 -9
  133. package/src/core/scaffold/native-schema-generator.ts +152 -70
  134. package/src/core/scaffold/nextjs-generator.ts +6 -6
  135. package/src/core/scaffold/ui-generator.ts +19 -19
  136. package/src/core/verify/index.ts +392 -0
  137. package/src/machine/contracts.ts +16 -0
  138. package/src/machine/errors.ts +12 -1
  139. package/src/machine/index.ts +444 -15
  140. package/src/mcp/index.ts +198 -2
  141. package/src/types/index.ts +17 -0
@@ -31,7 +31,7 @@ import {
31
31
  generateAuthLayout,
32
32
  UIAuthOptions,
33
33
  } from "../../core/scaffold/ui-generator";
34
- import { generateLanguageSchemaArtifacts } from "../../core/scaffold/native-schema-generator";
34
+ import { generateLanguageSchemaArtifacts, validateNativeSchemaToolchain } from "../../core/scaffold/native-schema-generator";
35
35
  import { detectFromProject, getCommands } from "../../utils/package-manager";
36
36
  import {
37
37
  BackendLanguage,
@@ -44,6 +44,14 @@ import {
44
44
  } from "../../types";
45
45
  import { syncProjectManifest } from "../../core/project/manifest";
46
46
  import { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithMultipleAuth, generateBFFCallFunctionWithSwaAuth, generateNamedAuthRouterCSharp, generateNamedAuthRouterPython, generateNamedAuthRouterTS } from "../../core/scaffold/auth-generator";
47
+ import {
48
+ FileOperationSession,
49
+ getActiveFileSession,
50
+ hashFileIfExists,
51
+ } from "../../core/operations/file-session";
52
+ import { recordSessionOperations } from "../../core/project/artifacts";
53
+
54
+ const SCAFFOLD_GENERATOR = "scaffold";
47
55
 
48
56
  interface ScaffoldOptions {
49
57
  model: string; // モデルファイルのパス(例: "lib/models/todo.ts" or "todo")
@@ -79,6 +87,9 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
79
87
  const configValidation = validateConfig(getFullConfig());
80
88
  if (!configValidation.valid) throw new Error(`Invalid SwallowKit configuration:\n${configValidation.errors.map(error => `- ${error}`).join("\n")}`);
81
89
 
90
+ // Plan(dry-run) や Apply からセッションが供給される。単体実行時は commit セッションを生成。
91
+ const session = getActiveFileSession() ?? new FileOperationSession("commit");
92
+
82
93
  console.log("🏗️ SwallowKit Scaffold: Generating CRUD operations...\n");
83
94
 
84
95
  try {
@@ -124,38 +135,51 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
124
135
  const relatedModels = backendLanguage === "typescript"
125
136
  ? [modelInfo]
126
137
  : await collectModelGraph(modelPath);
138
+ for (const relatedModel of relatedModels) {
139
+ session.registerInput(relatedModel.filePath);
140
+ }
141
+
142
+ // 4b. 書き込み前に外部 codegen toolchain を検証し、失敗時はプロジェクト無変更で終了する
143
+ if (session.mode === "commit" && backendLanguage !== "typescript" && !isConnectorModel) {
144
+ await validateNativeSchemaToolchain(
145
+ relatedModels,
146
+ modelInfo,
147
+ backendLanguage,
148
+ path.join(process.cwd(), functionsDir)
149
+ );
150
+ }
127
151
 
128
152
  if (options.apiOnly) {
129
153
  console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
130
154
  }
131
155
 
132
156
  // 5. Generate BFF callFunction helper
133
- await generateCallFunctionHelper();
157
+ await generateCallFunctionHelper(session);
134
158
 
135
159
  // 6. Generate Azure Functions code
136
160
  if (isConnectorModel) {
137
- await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
161
+ await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage, session);
138
162
  // 6b. Install connector driver dependencies
139
- await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage);
163
+ await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage, session);
140
164
  } else {
141
- await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
165
+ await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage, session);
142
166
 
143
167
  if (backendLanguage !== "typescript") {
144
- await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage);
168
+ await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage, session);
145
169
  }
146
170
  }
147
171
 
148
172
  // 7. Generate Next.js BFF API Routes
149
173
  const apiDir = options.apiDir || "app/api";
150
174
  if (isConnectorModel) {
151
- await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName);
175
+ await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName, session);
152
176
  } else {
153
- await generateBFFRoutes(modelInfo, apiDir, sharedPackageName);
177
+ await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
154
178
  }
155
179
 
156
180
  // 8. Generate Cosmos DB container Bicep file (skip for connector models)
157
181
  if (!isConnectorModel) {
158
- await generateCosmosContainer(modelInfo);
182
+ await generateCosmosContainer(modelInfo, session);
159
183
  }
160
184
 
161
185
  // 9. Generate UI components (unless --api-only)
@@ -166,14 +190,20 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
166
190
  uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
167
191
  ? createUIAuthOptions(uiAuthPolicy, uiAuthConfig)
168
192
  : undefined;
169
- await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
193
+ await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions, session);
170
194
  if (uiAuthOptions?.authenticatedFetchImport) {
171
- applyExternalTokenFetchToGeneratedUi(modelInfo, uiAuthOptions.authenticatedFetchImport);
195
+ applyExternalTokenFetchToGeneratedUi(modelInfo, session, uiAuthOptions.authenticatedFetchImport);
172
196
  }
173
- await updateNavigationMenu(modelInfo);
197
+ await updateNavigationMenu(modelInfo, session);
174
198
  }
175
199
 
176
- await syncProjectManifest();
200
+ if (session.mode === "commit") {
201
+ recordSessionOperations(session.operations, {
202
+ schemaHash: hashFileIfExists(modelPath) ?? undefined,
203
+ sourceModel: modelInfo.name,
204
+ });
205
+ await syncProjectManifest();
206
+ }
177
207
 
178
208
  console.log("\n✅ Scaffold completed successfully!");
179
209
  console.log("\n📝 Next steps:");
@@ -198,29 +228,27 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
198
228
  }
199
229
  }
200
230
 
201
- function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, importPath = '@/lib/auth/authenticated-fetch'): void {
202
- const uiRoot = path.join(process.cwd(), 'app', toKebabCase(modelInfo.name));
203
- if (!fs.existsSync(uiRoot)) return;
204
- const visit = (dir: string): void => {
205
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
206
- const target = path.join(dir, entry.name);
207
- if (entry.isDirectory()) { visit(target); continue; }
208
- if (!entry.name.endsWith('.tsx')) continue;
209
- let content = fs.readFileSync(target, 'utf-8');
210
- if (!content.includes('fetch(')) continue;
211
- content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
212
- content = content.replace(
213
- /(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
214
- `if (res.status === 401) throw new Error('Authentication required');\n $1`
215
- );
216
- if (!content.includes(importPath)) {
217
- const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
218
- content = `${directive}import { authenticatedFetch } from '${importPath}';\n${content.slice(directive.length)}`;
219
- }
220
- fs.writeFileSync(target, content, 'utf-8');
231
+ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: FileOperationSession, importPath = '@/lib/auth/authenticated-fetch'): void {
232
+ const uiPrefix = `app/${toKebabCase(modelInfo.name)}/`;
233
+ for (const operation of session.operations) {
234
+ if (!operation.path.startsWith(uiPrefix) || !operation.path.endsWith('.tsx') || operation.action === 'delete') continue;
235
+ let content = session.readFile(operation.absolutePath);
236
+ if (!content.includes('fetch(')) continue;
237
+ content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
238
+ content = content.replace(
239
+ /(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
240
+ `if (res.status === 401) throw new Error('Authentication required');\n $1`
241
+ );
242
+ if (!content.includes(importPath)) {
243
+ const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
244
+ content = `${directive}import { authenticatedFetch } from '${importPath}';\n${content.slice(directive.length)}`;
221
245
  }
222
- };
223
- visit(uiRoot);
246
+ session.writeFile(operation.absolutePath, content, {
247
+ ownership: 'managed',
248
+ generator: SCAFFOLD_GENERATOR,
249
+ sourceModel: modelInfo.name,
250
+ });
251
+ }
224
252
  }
225
253
 
226
254
  /**
@@ -259,7 +287,7 @@ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig |
259
287
  /**
260
288
  * モデルファイルのパスを解決
261
289
  */
262
- function resolveModelPath(modelInput: string): string {
290
+ export function resolveModelPath(modelInput: string): string {
263
291
  const cwd = process.cwd();
264
292
 
265
293
  // 絶対パスの場合
@@ -327,7 +355,7 @@ function readSharedPackageName(): string {
327
355
  /**
328
356
  * Generate BFF callFunction helper (lib/api/call-function.ts)
329
357
  */
330
- async function generateCallFunctionHelper(): Promise<void> {
358
+ async function generateCallFunctionHelper(session: FileOperationSession): Promise<void> {
331
359
  console.log("\n📦 Generating BFF callFunction helper...");
332
360
 
333
361
  const cwd = process.cwd();
@@ -335,10 +363,6 @@ async function generateCallFunctionHelper(): Promise<void> {
335
363
  const helperDir = path.join(cwd, "lib", "api");
336
364
  const helperPath = path.join(helperDir, "call-function.ts");
337
365
 
338
- if (!fs.existsSync(helperDir)) {
339
- fs.mkdirSync(helperDir, { recursive: true });
340
- }
341
-
342
366
  // auth 設定がある場合は Authorization ヘッダー転送版を生成
343
367
  const authConfig = getAuthConfig();
344
368
  const normalizedAuth = normalizeAuthConfig(authConfig);
@@ -353,7 +377,7 @@ async function generateCallFunctionHelper(): Promise<void> {
353
377
  helperCode = generateBFFCallFunction();
354
378
  }
355
379
 
356
- fs.writeFileSync(helperPath, helperCode, "utf-8");
380
+ session.writeFile(helperPath, helperCode, { ownership: "managed", generator: SCAFFOLD_GENERATOR });
357
381
  console.log(`✅ Created: ${helperPath}`);
358
382
  }
359
383
 
@@ -364,7 +388,8 @@ async function generateFunctionsCode(
364
388
  modelInfo: ModelInfo,
365
389
  functionsDir: string,
366
390
  sharedPackageName: string,
367
- backendLanguage: BackendLanguage
391
+ backendLanguage: BackendLanguage,
392
+ session: FileOperationSession
368
393
  ): Promise<void> {
369
394
  console.log("\n🔨 Generating Azure Functions CRUD code...");
370
395
 
@@ -376,12 +401,12 @@ async function generateFunctionsCode(
376
401
  }
377
402
 
378
403
  const modelKebab = toKebabCase(modelInfo.name);
404
+ const managedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
379
405
  if (backendLanguage === "typescript") {
380
406
  const normalizedAuth = getNormalizedAuthConfig();
381
407
  if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
382
408
  const routerPath = path.join(process.cwd(), functionsDir, "src", "auth", "auth-router.ts");
383
- fs.mkdirSync(path.dirname(routerPath), { recursive: true });
384
- fs.writeFileSync(routerPath, generateNamedAuthRouterTS(normalizedAuth), "utf-8");
409
+ session.writeFile(routerPath, generateNamedAuthRouterTS(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
385
410
  }
386
411
  const functionFilePath = path.join(
387
412
  process.cwd(),
@@ -390,13 +415,8 @@ async function generateFunctionsCode(
390
415
  `${modelKebab}.ts`
391
416
  );
392
417
 
393
- const functionDir = path.dirname(functionFilePath);
394
- if (!fs.existsSync(functionDir)) {
395
- fs.mkdirSync(functionDir, { recursive: true });
396
- }
397
-
398
418
  const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
399
- fs.writeFileSync(functionFilePath, code, "utf-8");
419
+ session.writeFile(functionFilePath, code, managedMeta);
400
420
  console.log(`✅ Created: ${functionFilePath}`);
401
421
  return;
402
422
  }
@@ -405,21 +425,19 @@ async function generateFunctionsCode(
405
425
  const normalizedAuth = getNormalizedAuthConfig();
406
426
  if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
407
427
  const routerPath = path.join(process.cwd(), functionsDir, "Auth", "AuthRouter.cs");
408
- fs.mkdirSync(path.dirname(routerPath), { recursive: true });
409
- fs.writeFileSync(routerPath, generateNamedAuthRouterCSharp(normalizedAuth), "utf-8");
428
+ session.writeFile(routerPath, generateNamedAuthRouterCSharp(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
410
429
  }
411
430
  const crudDir = path.join(process.cwd(), functionsDir, "Crud");
412
431
  const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
413
- fs.mkdirSync(crudDir, { recursive: true });
414
432
 
415
433
  // Remove init-generated template (singular) to avoid route conflicts
416
434
  const templatePath = path.join(crudDir, `${modelInfo.name}Function.cs`);
417
- if (fs.existsSync(templatePath)) {
418
- fs.unlinkSync(templatePath);
435
+ if (session.fileExists(templatePath)) {
436
+ session.deleteFile(templatePath, managedMeta);
419
437
  console.log(`🗑️ Removed template: ${templatePath}`);
420
438
  }
421
439
 
422
- fs.writeFileSync(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), "utf-8");
440
+ session.writeFile(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), managedMeta);
423
441
  console.log(`✅ Created: ${functionFilePath}`);
424
442
  return;
425
443
  }
@@ -427,17 +445,15 @@ async function generateFunctionsCode(
427
445
  const normalizedAuth = getNormalizedAuthConfig();
428
446
  if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
429
447
  const routerPath = path.join(process.cwd(), functionsDir, "auth", "auth_router.py");
430
- fs.mkdirSync(path.dirname(routerPath), { recursive: true });
431
- fs.writeFileSync(routerPath, generateNamedAuthRouterPython(normalizedAuth), "utf-8");
448
+ session.writeFile(routerPath, generateNamedAuthRouterPython(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
432
449
  }
433
450
 
434
451
  const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
435
452
  const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
436
- fs.mkdirSync(blueprintsDir, { recursive: true });
437
453
 
438
454
  const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo, authPolicy);
439
- fs.writeFileSync(blueprintPath, blueprint, "utf-8");
440
- updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration);
455
+ session.writeFile(blueprintPath, blueprint, managedMeta);
456
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration, session);
441
457
  console.log(`✅ Created: ${blueprintPath}`);
442
458
  }
443
459
 
@@ -462,7 +478,8 @@ async function generateConnectorFunctionsCode(
462
478
  modelInfo: ModelInfo,
463
479
  functionsDir: string,
464
480
  sharedPackageName: string,
465
- backendLanguage: BackendLanguage
481
+ backendLanguage: BackendLanguage,
482
+ session: FileOperationSession
466
483
  ): Promise<void> {
467
484
  console.log("\n🔌 Generating Connector Azure Functions code...");
468
485
 
@@ -484,6 +501,7 @@ async function generateConnectorFunctionsCode(
484
501
  }
485
502
 
486
503
  const modelKebab = toKebabCase(modelInfo.name);
504
+ const connectorManagedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
487
505
 
488
506
  if (backendLanguage === "typescript") {
489
507
  const functionFilePath = path.join(
@@ -492,7 +510,6 @@ async function generateConnectorFunctionsCode(
492
510
  "src",
493
511
  `${modelKebab}.ts`
494
512
  );
495
- fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
496
513
 
497
514
  let code: string;
498
515
  if (connectorDef.type === "rdb") {
@@ -513,7 +530,7 @@ async function generateConnectorFunctionsCode(
513
530
  );
514
531
  }
515
532
 
516
- fs.writeFileSync(functionFilePath, code, "utf-8");
533
+ session.writeFile(functionFilePath, code, connectorManagedMeta);
517
534
  console.log(`✅ Created: ${functionFilePath}`);
518
535
  return;
519
536
  }
@@ -525,7 +542,6 @@ async function generateConnectorFunctionsCode(
525
542
  "Connectors",
526
543
  `${modelInfo.name}ConnectorFunctions.cs`
527
544
  );
528
- fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
529
545
 
530
546
  let code: string;
531
547
  if (connectorDef.type === "rdb") {
@@ -542,7 +558,7 @@ async function generateConnectorFunctionsCode(
542
558
  );
543
559
  }
544
560
 
545
- fs.writeFileSync(functionFilePath, code, "utf-8");
561
+ session.writeFile(functionFilePath, code, connectorManagedMeta);
546
562
  console.log(`✅ Created: ${functionFilePath}`);
547
563
  return;
548
564
  }
@@ -550,7 +566,6 @@ async function generateConnectorFunctionsCode(
550
566
  // Python
551
567
  const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
552
568
  const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
553
- fs.mkdirSync(blueprintsDir, { recursive: true });
554
569
 
555
570
  let result: { blueprint: string; registration: string };
556
571
  if (connectorDef.type === "rdb") {
@@ -567,8 +582,8 @@ async function generateConnectorFunctionsCode(
567
582
  );
568
583
  }
569
584
 
570
- fs.writeFileSync(blueprintPath, result.blueprint, "utf-8");
571
- updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), result.registration);
585
+ session.writeFile(blueprintPath, result.blueprint, connectorManagedMeta);
586
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), result.registration, session);
572
587
  console.log(`✅ Created: ${blueprintPath}`);
573
588
  }
574
589
 
@@ -579,7 +594,8 @@ async function generateConnectorFunctionsCode(
579
594
  async function installConnectorDriverDependencies(
580
595
  modelInfo: ModelInfo,
581
596
  functionsDir: string,
582
- backendLanguage: BackendLanguage
597
+ backendLanguage: BackendLanguage,
598
+ session: FileOperationSession
583
599
  ): Promise<void> {
584
600
  const connectorConfig = modelInfo.connectorConfig!;
585
601
  const connectorDef = getConnectorDefinition(connectorConfig.connector);
@@ -607,6 +623,13 @@ async function installConnectorDriverDependencies(
607
623
  if (missingDeps.length === 0 && missingDevDeps.length === 0) return;
608
624
  }
609
625
 
626
+ if (session.mode === "collect") {
627
+ session.addWarning(
628
+ `external-install: ${rdbDef.provider} driver dependencies (${[...entry.deps, ...entry.devDeps].join(", ")}) will be installed into ${functionsDir}/ on apply.`
629
+ );
630
+ return;
631
+ }
632
+
610
633
  console.log(`\n📦 Installing ${rdbDef.provider} driver dependencies...`);
611
634
  const pm = detectFromProject(functionsPath);
612
635
  const cmds = getCommands(pm);
@@ -629,6 +652,10 @@ async function installConnectorDriverDependencies(
629
652
  };
630
653
  const pkg = nugetMap[rdbDef.provider];
631
654
  if (!pkg) return;
655
+ if (session.mode === "collect") {
656
+ session.addWarning(`external-install: NuGet package ${pkg} will be added to ${functionsDir}/functions.csproj on apply.`);
657
+ return;
658
+ }
632
659
  console.log(`\n📦 Installing ${rdbDef.provider} NuGet package...`);
633
660
  runSpawnSyncCommand("dotnet", ["add", path.join(functionsPath, "functions.csproj"), "package", pkg], functionsPath);
634
661
  console.log(`✅ ${pkg} installed`);
@@ -644,11 +671,11 @@ async function installConnectorDriverDependencies(
644
671
  const pipPkg = pipMap[rdbDef.provider];
645
672
  if (!pipPkg) return;
646
673
  const reqPath = path.join(functionsPath, "requirements.txt");
647
- if (fs.existsSync(reqPath)) {
648
- const existing = fs.readFileSync(reqPath, "utf-8");
674
+ if (session.fileExists(reqPath)) {
675
+ const existing = session.readFile(reqPath);
649
676
  if (!existing.includes(pipPkg)) {
650
677
  console.log(`\n📦 Adding ${pipPkg} to requirements.txt...`);
651
- fs.appendFileSync(reqPath, `\n${pipPkg}\n`);
678
+ session.writeFile(reqPath, `${existing}\n${pipPkg}\n`, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
652
679
  console.log(`✅ ${pipPkg} added`);
653
680
  }
654
681
  }
@@ -660,7 +687,8 @@ async function installConnectorDriverDependencies(
660
687
  async function generateConnectorBFFRoutesFiles(
661
688
  modelInfo: ModelInfo,
662
689
  apiDir: string,
663
- sharedPackageName: string
690
+ sharedPackageName: string,
691
+ session: FileOperationSession
664
692
  ): Promise<void> {
665
693
  console.log("\n🔌 Generating Connector BFF API routes...");
666
694
 
@@ -670,13 +698,11 @@ async function generateConnectorBFFRoutesFiles(
670
698
  const listRoutePath = path.join(process.cwd(), apiDir, modelCamel, "route.ts");
671
699
  const detailRoutePath = path.join(process.cwd(), apiDir, modelCamel, "[id]", "route.ts");
672
700
 
673
- fs.mkdirSync(path.dirname(listRoutePath), { recursive: true });
674
- fs.mkdirSync(path.dirname(detailRoutePath), { recursive: true });
675
-
676
701
  const routes = generateConnectorBFFRoutes(modelInfo, sharedPackageName, operations);
677
702
 
678
- fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
679
- fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
703
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
704
+ session.writeFile(listRoutePath, routes.listRoute, meta);
705
+ session.writeFile(detailRoutePath, routes.detailRoute, meta);
680
706
 
681
707
  console.log(`✅ Created: ${listRoutePath}`);
682
708
  console.log(`✅ Created: ${detailRoutePath}`);
@@ -717,12 +743,12 @@ function describeFunctionsOutputPath(functionsDir: string, backendLanguage: Back
717
743
  return `${functionsDir}/blueprints/`;
718
744
  }
719
745
 
720
- function updatePythonFunctionRegistrations(functionAppPath: string, registration: string): void {
721
- if (!fs.existsSync(functionAppPath)) {
746
+ function updatePythonFunctionRegistrations(functionAppPath: string, registration: string, session: FileOperationSession): void {
747
+ if (!session.fileExists(functionAppPath)) {
722
748
  throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
723
749
  }
724
750
 
725
- const content = fs.readFileSync(functionAppPath, "utf-8");
751
+ const content = session.readFile(functionAppPath);
726
752
 
727
753
  // Check if import line already exists (handles init-generated layout)
728
754
  const importLine = registration.split("\n").find((l) => l.startsWith("from ") || l.startsWith("import "));
@@ -739,7 +765,7 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
739
765
  }
740
766
 
741
767
  const updated = content.replace(marker, `${registration}\n${marker}`);
742
- fs.writeFileSync(functionAppPath, updated, "utf-8");
768
+ session.writeFile(functionAppPath, updated, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
743
769
  }
744
770
 
745
771
  /**
@@ -748,7 +774,8 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
748
774
  async function generateBFFRoutes(
749
775
  modelInfo: any,
750
776
  apiDir: string,
751
- sharedPackageName: string
777
+ sharedPackageName: string,
778
+ session: FileOperationSession
752
779
  ): Promise<void> {
753
780
  console.log("\n🔨 Generating Next.js BFF API routes...");
754
781
 
@@ -771,24 +798,13 @@ async function generateBFFRoutes(
771
798
  "route.ts"
772
799
  );
773
800
 
774
- // ディレクトリを作成
775
- const listRouteDir = path.dirname(listRoutePath);
776
- const detailRouteDir = path.dirname(detailRoutePath);
777
-
778
- if (!fs.existsSync(listRouteDir)) {
779
- fs.mkdirSync(listRouteDir, { recursive: true });
780
- }
781
-
782
- if (!fs.existsSync(detailRouteDir)) {
783
- fs.mkdirSync(detailRouteDir, { recursive: true });
784
- }
785
-
786
801
  // コードを生成
787
802
  const routes = generateCompactBFFRoutes(modelInfo, sharedPackageName);
788
803
 
789
804
  // ファイルに書き込み
790
- fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
791
- fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
805
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
806
+ session.writeFile(listRoutePath, routes.listRoute, meta);
807
+ session.writeFile(detailRoutePath, routes.detailRoute, meta);
792
808
 
793
809
  console.log(`✅ Created: ${listRoutePath}`);
794
810
  console.log(`✅ Created: ${detailRoutePath}`);
@@ -797,7 +813,7 @@ async function generateBFFRoutes(
797
813
  /**
798
814
  * Generate Cosmos DB container Bicep file
799
815
  */
800
- export async function generateCosmosContainer(modelInfo: any): Promise<void> {
816
+ export async function generateCosmosContainer(modelInfo: any, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
801
817
  console.log("\n🗄️ Generating Cosmos DB container Bicep file...");
802
818
 
803
819
  const modelKebab = toKebabCase(modelInfo.name);
@@ -811,13 +827,8 @@ export async function generateCosmosContainer(modelInfo: any): Promise<void> {
811
827
  return;
812
828
  }
813
829
 
814
- // Check if containers directory exists, if not create it
815
- const containersDir = path.join(infraDir, "containers");
816
- if (!fs.existsSync(containersDir)) {
817
- fs.mkdirSync(containersDir, { recursive: true });
818
- }
819
-
820
830
  // Generate container Bicep file
831
+ const containersDir = path.join(infraDir, "containers");
821
832
  const containerFileName = `${modelKebab}-container.bicep`;
822
833
  const containerFilePath = path.join(containersDir, containerFileName);
823
834
 
@@ -879,26 +890,26 @@ output containerName string = container.name
879
890
  `;
880
891
 
881
892
  // Write Bicep file (overwrite if exists)
882
- fs.writeFileSync(containerFilePath, bicepContent, "utf-8");
893
+ session.writeFile(containerFilePath, bicepContent, { ownership: "managed", generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name });
883
894
  console.log(`✅ Created: ${containerFilePath}`);
884
895
 
885
896
  // Update main.bicep to include this container module
886
- await updateMainBicepWithContainer(modelKebab, modelPascal);
897
+ await updateMainBicepWithContainer(modelKebab, modelPascal, session);
887
898
  }
888
899
 
889
900
  /**
890
901
  * Update main.bicep to include new container module
891
902
  */
892
- async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string): Promise<void> {
903
+ async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string, session: FileOperationSession): Promise<void> {
893
904
  const cwd = process.cwd();
894
905
  const mainBicepPath = path.join(cwd, "infra", "main.bicep");
895
906
 
896
- if (!fs.existsSync(mainBicepPath)) {
907
+ if (!session.fileExists(mainBicepPath)) {
897
908
  console.log("ℹ️ main.bicep not found. Please manually add the container module.");
898
909
  return;
899
910
  }
900
911
 
901
- let mainBicepContent = fs.readFileSync(mainBicepPath, "utf-8");
912
+ let mainBicepContent = session.readFile(mainBicepPath);
902
913
 
903
914
  // Check if container module already exists
904
915
  const containerModuleName = `${modelPascal.charAt(0).toLowerCase()}${modelPascal.slice(1)}Container`;
@@ -950,14 +961,14 @@ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
950
961
  containerModule +
951
962
  mainBicepContent.slice(insertPosition);
952
963
 
953
- fs.writeFileSync(mainBicepPath, mainBicepContent, "utf-8");
964
+ session.writeFile(mainBicepPath, mainBicepContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
954
965
  console.log(`✅ Added container module to main.bicep`);
955
966
  }
956
967
 
957
968
  /**
958
969
  * Generate UI components (list, detail, form, create, edit pages)
959
970
  */
960
- export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions): Promise<void> {
971
+ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
961
972
  console.log("\n🎨 Generating UI components...");
962
973
 
963
974
  const modelKebab = toKebabCase(modelInfo.name);
@@ -971,13 +982,6 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
971
982
  const idDir = path.join(modelDir, "[id]");
972
983
  const editDir = path.join(idDir, "edit");
973
984
 
974
- // Create directories
975
- [componentsDir, newDir, editDir].forEach(dir => {
976
- if (!fs.existsSync(dir)) {
977
- fs.mkdirSync(dir, { recursive: true });
978
- }
979
- });
980
-
981
985
  // Generate and write files
982
986
  const listPage = generateListPage(modelInfo, sharedPackageName, authOptions);
983
987
  const detailPage = generateDetailPage(modelInfo, sharedPackageName, authOptions);
@@ -986,13 +990,14 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
986
990
  const editPage = generateEditPage(modelInfo, sharedPackageName, authOptions);
987
991
  const authLayout = generateAuthLayout(modelInfo, authOptions);
988
992
 
989
- fs.writeFileSync(path.join(modelDir, "page.tsx"), listPage, "utf-8");
990
- fs.writeFileSync(path.join(idDir, "page.tsx"), detailPage, "utf-8");
991
- fs.writeFileSync(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, "utf-8");
992
- fs.writeFileSync(path.join(newDir, "page.tsx"), newPage, "utf-8");
993
- fs.writeFileSync(path.join(editDir, "page.tsx"), editPage, "utf-8");
993
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
994
+ session.writeFile(path.join(modelDir, "page.tsx"), listPage, meta);
995
+ session.writeFile(path.join(idDir, "page.tsx"), detailPage, meta);
996
+ session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, meta);
997
+ session.writeFile(path.join(newDir, "page.tsx"), newPage, meta);
998
+ session.writeFile(path.join(editDir, "page.tsx"), editPage, meta);
994
999
  if (authLayout) {
995
- fs.writeFileSync(path.join(modelDir, "layout.tsx"), authLayout, "utf-8");
1000
+ session.writeFile(path.join(modelDir, "layout.tsx"), authLayout, meta);
996
1001
  }
997
1002
 
998
1003
  console.log(`✅ Created: ${path.join(modelDir, "page.tsx")}`);
@@ -1008,7 +1013,7 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
1008
1013
  /**
1009
1014
  * Update navigation menu on the homepage
1010
1015
  */
1011
- async function updateNavigationMenu(modelInfo: any): Promise<void> {
1016
+ async function updateNavigationMenu(modelInfo: any, session: FileOperationSession): Promise<void> {
1012
1017
  console.log("\n📋 Updating navigation menu...");
1013
1018
 
1014
1019
  const modelKebab = toKebabCase(modelInfo.name);
@@ -1017,13 +1022,13 @@ async function updateNavigationMenu(modelInfo: any): Promise<void> {
1017
1022
  // Update scaffold config
1018
1023
  const configPath = path.join(cwd, "lib", "scaffold-config.ts");
1019
1024
 
1020
- if (!fs.existsSync(configPath)) {
1025
+ if (!session.fileExists(configPath)) {
1021
1026
  console.log("⚠️ scaffold-config.ts not found. Skipping navigation menu update.");
1022
1027
  return;
1023
1028
  }
1024
1029
 
1025
1030
  // Read existing config
1026
- const configContent = fs.readFileSync(configPath, "utf-8");
1031
+ const configContent = session.readFile(configPath);
1027
1032
 
1028
1033
  // Parse models array - extract each model entry
1029
1034
  const models: Array<{ name: string; path: string; label: string }> = [];
@@ -1070,6 +1075,7 @@ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label
1070
1075
  };
1071
1076
  `;
1072
1077
 
1073
- fs.writeFileSync(configPath, newConfigContent, "utf-8");
1078
+ session.writeFile(configPath, newConfigContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
1074
1079
  console.log(`✅ Added ${modelInfo.name} to navigation menu`);
1075
1080
  }
1081
+