swallowkit 1.0.0-beta.55 → 1.0.0-beta.56

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 (135) 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 +111 -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 +53 -0
  27. package/dist/core/operations/file-session.d.ts.map +1 -0
  28. package/dist/core/operations/file-session.js +195 -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 +195 -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/nextjs-generator.js +6 -6
  78. package/dist/core/scaffold/ui-generator.js +19 -19
  79. package/dist/core/scaffold/ui-generator.js.map +1 -1
  80. package/dist/core/verify/index.d.ts +62 -0
  81. package/dist/core/verify/index.d.ts.map +1 -0
  82. package/dist/core/verify/index.js +343 -0
  83. package/dist/core/verify/index.js.map +1 -0
  84. package/dist/machine/contracts.d.ts +8 -0
  85. package/dist/machine/contracts.d.ts.map +1 -1
  86. package/dist/machine/errors.d.ts +4 -1
  87. package/dist/machine/errors.d.ts.map +1 -1
  88. package/dist/machine/errors.js +9 -1
  89. package/dist/machine/errors.js.map +1 -1
  90. package/dist/machine/index.d.ts.map +1 -1
  91. package/dist/machine/index.js +350 -16
  92. package/dist/machine/index.js.map +1 -1
  93. package/dist/mcp/index.d.ts.map +1 -1
  94. package/dist/mcp/index.js +202 -1
  95. package/dist/mcp/index.js.map +1 -1
  96. package/dist/types/index.d.ts +14 -0
  97. package/dist/types/index.d.ts.map +1 -1
  98. package/package.json +1 -1
  99. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
  100. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
  101. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
  102. package/src/__tests__/agent-loop-phase34.test.ts +449 -0
  103. package/src/__tests__/file-session.test.ts +98 -0
  104. package/src/__tests__/init.test.ts +50 -0
  105. package/src/__tests__/mcp.test.ts +113 -5
  106. package/src/__tests__/plan-apply.test.ts +294 -0
  107. package/src/cli/commands/add-auth.ts +164 -164
  108. package/src/cli/commands/init.ts +186 -24
  109. package/src/cli/commands/scaffold.ts +128 -129
  110. package/src/cli/index.ts +148 -2
  111. package/src/core/config.ts +5 -4
  112. package/src/core/operations/auth-operations.ts +353 -0
  113. package/src/core/operations/create-model.ts +25 -13
  114. package/src/core/operations/file-session.ts +210 -0
  115. package/src/core/operations/provision-operations.ts +297 -0
  116. package/src/core/operations/runtime.ts +23 -0
  117. package/src/core/operations/scaffold-apply.ts +106 -0
  118. package/src/core/operations/scaffold-machine.ts +2 -16
  119. package/src/core/operations/scaffold-plan.ts +224 -0
  120. package/src/core/project/artifacts.ts +205 -0
  121. package/src/core/project/boundaries.ts +170 -0
  122. package/src/core/project/drift.ts +202 -0
  123. package/src/core/project/infra.ts +196 -0
  124. package/src/core/project/state.ts +77 -0
  125. package/src/core/project/workflows.ts +249 -0
  126. package/src/core/scaffold/auth-generator.ts +24 -27
  127. package/src/core/scaffold/functions-generator.ts +9 -9
  128. package/src/core/scaffold/nextjs-generator.ts +6 -6
  129. package/src/core/scaffold/ui-generator.ts +19 -19
  130. package/src/core/verify/index.ts +392 -0
  131. package/src/machine/contracts.ts +16 -0
  132. package/src/machine/errors.ts +12 -1
  133. package/src/machine/index.ts +444 -15
  134. package/src/mcp/index.ts +198 -2
  135. package/src/types/index.ts +17 -0
@@ -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 {
@@ -130,32 +141,38 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
130
141
  }
131
142
 
132
143
  // 5. Generate BFF callFunction helper
133
- await generateCallFunctionHelper();
144
+ await generateCallFunctionHelper(session);
134
145
 
135
146
  // 6. Generate Azure Functions code
136
147
  if (isConnectorModel) {
137
- await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
148
+ await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage, session);
138
149
  // 6b. Install connector driver dependencies
139
- await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage);
150
+ await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage, session);
140
151
  } else {
141
- await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
152
+ await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage, session);
142
153
 
143
154
  if (backendLanguage !== "typescript") {
144
- await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage);
155
+ if (session.mode === "collect") {
156
+ session.addWarning(
157
+ `external-codegen: ${functionsDir}/openapi and ${functionsDir}/generated are regenerated by external tooling on apply and are not included in this plan.`
158
+ );
159
+ } else {
160
+ await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage);
161
+ }
145
162
  }
146
163
  }
147
164
 
148
165
  // 7. Generate Next.js BFF API Routes
149
166
  const apiDir = options.apiDir || "app/api";
150
167
  if (isConnectorModel) {
151
- await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName);
168
+ await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName, session);
152
169
  } else {
153
- await generateBFFRoutes(modelInfo, apiDir, sharedPackageName);
170
+ await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
154
171
  }
155
172
 
156
173
  // 8. Generate Cosmos DB container Bicep file (skip for connector models)
157
174
  if (!isConnectorModel) {
158
- await generateCosmosContainer(modelInfo);
175
+ await generateCosmosContainer(modelInfo, session);
159
176
  }
160
177
 
161
178
  // 9. Generate UI components (unless --api-only)
@@ -166,14 +183,20 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
166
183
  uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
167
184
  ? createUIAuthOptions(uiAuthPolicy, uiAuthConfig)
168
185
  : undefined;
169
- await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
186
+ await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions, session);
170
187
  if (uiAuthOptions?.authenticatedFetchImport) {
171
- applyExternalTokenFetchToGeneratedUi(modelInfo, uiAuthOptions.authenticatedFetchImport);
188
+ applyExternalTokenFetchToGeneratedUi(modelInfo, session, uiAuthOptions.authenticatedFetchImport);
172
189
  }
173
- await updateNavigationMenu(modelInfo);
190
+ await updateNavigationMenu(modelInfo, session);
174
191
  }
175
192
 
176
- await syncProjectManifest();
193
+ if (session.mode === "commit") {
194
+ recordSessionOperations(session.operations, {
195
+ schemaHash: hashFileIfExists(modelPath) ?? undefined,
196
+ sourceModel: modelInfo.name,
197
+ });
198
+ await syncProjectManifest();
199
+ }
177
200
 
178
201
  console.log("\n✅ Scaffold completed successfully!");
179
202
  console.log("\n📝 Next steps:");
@@ -198,29 +221,27 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
198
221
  }
199
222
  }
200
223
 
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');
224
+ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: FileOperationSession, importPath = '@/lib/auth/authenticated-fetch'): void {
225
+ const uiPrefix = `app/${toKebabCase(modelInfo.name)}/`;
226
+ for (const operation of session.operations) {
227
+ if (!operation.path.startsWith(uiPrefix) || !operation.path.endsWith('.tsx') || operation.action === 'delete') continue;
228
+ let content = session.readFile(operation.absolutePath);
229
+ if (!content.includes('fetch(')) continue;
230
+ content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
231
+ content = content.replace(
232
+ /(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
233
+ `if (res.status === 401) throw new Error('Authentication required');\n $1`
234
+ );
235
+ if (!content.includes(importPath)) {
236
+ const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
237
+ content = `${directive}import { authenticatedFetch } from '${importPath}';\n${content.slice(directive.length)}`;
221
238
  }
222
- };
223
- visit(uiRoot);
239
+ session.writeFile(operation.absolutePath, content, {
240
+ ownership: 'managed',
241
+ generator: SCAFFOLD_GENERATOR,
242
+ sourceModel: modelInfo.name,
243
+ });
244
+ }
224
245
  }
225
246
 
226
247
  /**
@@ -259,7 +280,7 @@ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig |
259
280
  /**
260
281
  * モデルファイルのパスを解決
261
282
  */
262
- function resolveModelPath(modelInput: string): string {
283
+ export function resolveModelPath(modelInput: string): string {
263
284
  const cwd = process.cwd();
264
285
 
265
286
  // 絶対パスの場合
@@ -327,7 +348,7 @@ function readSharedPackageName(): string {
327
348
  /**
328
349
  * Generate BFF callFunction helper (lib/api/call-function.ts)
329
350
  */
330
- async function generateCallFunctionHelper(): Promise<void> {
351
+ async function generateCallFunctionHelper(session: FileOperationSession): Promise<void> {
331
352
  console.log("\n📦 Generating BFF callFunction helper...");
332
353
 
333
354
  const cwd = process.cwd();
@@ -335,10 +356,6 @@ async function generateCallFunctionHelper(): Promise<void> {
335
356
  const helperDir = path.join(cwd, "lib", "api");
336
357
  const helperPath = path.join(helperDir, "call-function.ts");
337
358
 
338
- if (!fs.existsSync(helperDir)) {
339
- fs.mkdirSync(helperDir, { recursive: true });
340
- }
341
-
342
359
  // auth 設定がある場合は Authorization ヘッダー転送版を生成
343
360
  const authConfig = getAuthConfig();
344
361
  const normalizedAuth = normalizeAuthConfig(authConfig);
@@ -353,7 +370,7 @@ async function generateCallFunctionHelper(): Promise<void> {
353
370
  helperCode = generateBFFCallFunction();
354
371
  }
355
372
 
356
- fs.writeFileSync(helperPath, helperCode, "utf-8");
373
+ session.writeFile(helperPath, helperCode, { ownership: "managed", generator: SCAFFOLD_GENERATOR });
357
374
  console.log(`✅ Created: ${helperPath}`);
358
375
  }
359
376
 
@@ -364,7 +381,8 @@ async function generateFunctionsCode(
364
381
  modelInfo: ModelInfo,
365
382
  functionsDir: string,
366
383
  sharedPackageName: string,
367
- backendLanguage: BackendLanguage
384
+ backendLanguage: BackendLanguage,
385
+ session: FileOperationSession
368
386
  ): Promise<void> {
369
387
  console.log("\n🔨 Generating Azure Functions CRUD code...");
370
388
 
@@ -376,12 +394,12 @@ async function generateFunctionsCode(
376
394
  }
377
395
 
378
396
  const modelKebab = toKebabCase(modelInfo.name);
397
+ const managedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
379
398
  if (backendLanguage === "typescript") {
380
399
  const normalizedAuth = getNormalizedAuthConfig();
381
400
  if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
382
401
  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");
402
+ session.writeFile(routerPath, generateNamedAuthRouterTS(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
385
403
  }
386
404
  const functionFilePath = path.join(
387
405
  process.cwd(),
@@ -390,13 +408,8 @@ async function generateFunctionsCode(
390
408
  `${modelKebab}.ts`
391
409
  );
392
410
 
393
- const functionDir = path.dirname(functionFilePath);
394
- if (!fs.existsSync(functionDir)) {
395
- fs.mkdirSync(functionDir, { recursive: true });
396
- }
397
-
398
411
  const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
399
- fs.writeFileSync(functionFilePath, code, "utf-8");
412
+ session.writeFile(functionFilePath, code, managedMeta);
400
413
  console.log(`✅ Created: ${functionFilePath}`);
401
414
  return;
402
415
  }
@@ -405,21 +418,19 @@ async function generateFunctionsCode(
405
418
  const normalizedAuth = getNormalizedAuthConfig();
406
419
  if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
407
420
  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");
421
+ session.writeFile(routerPath, generateNamedAuthRouterCSharp(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
410
422
  }
411
423
  const crudDir = path.join(process.cwd(), functionsDir, "Crud");
412
424
  const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
413
- fs.mkdirSync(crudDir, { recursive: true });
414
425
 
415
426
  // Remove init-generated template (singular) to avoid route conflicts
416
427
  const templatePath = path.join(crudDir, `${modelInfo.name}Function.cs`);
417
- if (fs.existsSync(templatePath)) {
418
- fs.unlinkSync(templatePath);
428
+ if (session.fileExists(templatePath)) {
429
+ session.deleteFile(templatePath, managedMeta);
419
430
  console.log(`🗑️ Removed template: ${templatePath}`);
420
431
  }
421
432
 
422
- fs.writeFileSync(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), "utf-8");
433
+ session.writeFile(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), managedMeta);
423
434
  console.log(`✅ Created: ${functionFilePath}`);
424
435
  return;
425
436
  }
@@ -427,17 +438,15 @@ async function generateFunctionsCode(
427
438
  const normalizedAuth = getNormalizedAuthConfig();
428
439
  if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
429
440
  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");
441
+ session.writeFile(routerPath, generateNamedAuthRouterPython(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
432
442
  }
433
443
 
434
444
  const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
435
445
  const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
436
- fs.mkdirSync(blueprintsDir, { recursive: true });
437
446
 
438
447
  const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo, authPolicy);
439
- fs.writeFileSync(blueprintPath, blueprint, "utf-8");
440
- updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration);
448
+ session.writeFile(blueprintPath, blueprint, managedMeta);
449
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration, session);
441
450
  console.log(`✅ Created: ${blueprintPath}`);
442
451
  }
443
452
 
@@ -462,7 +471,8 @@ async function generateConnectorFunctionsCode(
462
471
  modelInfo: ModelInfo,
463
472
  functionsDir: string,
464
473
  sharedPackageName: string,
465
- backendLanguage: BackendLanguage
474
+ backendLanguage: BackendLanguage,
475
+ session: FileOperationSession
466
476
  ): Promise<void> {
467
477
  console.log("\n🔌 Generating Connector Azure Functions code...");
468
478
 
@@ -484,6 +494,7 @@ async function generateConnectorFunctionsCode(
484
494
  }
485
495
 
486
496
  const modelKebab = toKebabCase(modelInfo.name);
497
+ const connectorManagedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
487
498
 
488
499
  if (backendLanguage === "typescript") {
489
500
  const functionFilePath = path.join(
@@ -492,7 +503,6 @@ async function generateConnectorFunctionsCode(
492
503
  "src",
493
504
  `${modelKebab}.ts`
494
505
  );
495
- fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
496
506
 
497
507
  let code: string;
498
508
  if (connectorDef.type === "rdb") {
@@ -513,7 +523,7 @@ async function generateConnectorFunctionsCode(
513
523
  );
514
524
  }
515
525
 
516
- fs.writeFileSync(functionFilePath, code, "utf-8");
526
+ session.writeFile(functionFilePath, code, connectorManagedMeta);
517
527
  console.log(`✅ Created: ${functionFilePath}`);
518
528
  return;
519
529
  }
@@ -525,7 +535,6 @@ async function generateConnectorFunctionsCode(
525
535
  "Connectors",
526
536
  `${modelInfo.name}ConnectorFunctions.cs`
527
537
  );
528
- fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
529
538
 
530
539
  let code: string;
531
540
  if (connectorDef.type === "rdb") {
@@ -542,7 +551,7 @@ async function generateConnectorFunctionsCode(
542
551
  );
543
552
  }
544
553
 
545
- fs.writeFileSync(functionFilePath, code, "utf-8");
554
+ session.writeFile(functionFilePath, code, connectorManagedMeta);
546
555
  console.log(`✅ Created: ${functionFilePath}`);
547
556
  return;
548
557
  }
@@ -550,7 +559,6 @@ async function generateConnectorFunctionsCode(
550
559
  // Python
551
560
  const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
552
561
  const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
553
- fs.mkdirSync(blueprintsDir, { recursive: true });
554
562
 
555
563
  let result: { blueprint: string; registration: string };
556
564
  if (connectorDef.type === "rdb") {
@@ -567,8 +575,8 @@ async function generateConnectorFunctionsCode(
567
575
  );
568
576
  }
569
577
 
570
- fs.writeFileSync(blueprintPath, result.blueprint, "utf-8");
571
- updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), result.registration);
578
+ session.writeFile(blueprintPath, result.blueprint, connectorManagedMeta);
579
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), result.registration, session);
572
580
  console.log(`✅ Created: ${blueprintPath}`);
573
581
  }
574
582
 
@@ -579,7 +587,8 @@ async function generateConnectorFunctionsCode(
579
587
  async function installConnectorDriverDependencies(
580
588
  modelInfo: ModelInfo,
581
589
  functionsDir: string,
582
- backendLanguage: BackendLanguage
590
+ backendLanguage: BackendLanguage,
591
+ session: FileOperationSession
583
592
  ): Promise<void> {
584
593
  const connectorConfig = modelInfo.connectorConfig!;
585
594
  const connectorDef = getConnectorDefinition(connectorConfig.connector);
@@ -607,6 +616,13 @@ async function installConnectorDriverDependencies(
607
616
  if (missingDeps.length === 0 && missingDevDeps.length === 0) return;
608
617
  }
609
618
 
619
+ if (session.mode === "collect") {
620
+ session.addWarning(
621
+ `external-install: ${rdbDef.provider} driver dependencies (${[...entry.deps, ...entry.devDeps].join(", ")}) will be installed into ${functionsDir}/ on apply.`
622
+ );
623
+ return;
624
+ }
625
+
610
626
  console.log(`\n📦 Installing ${rdbDef.provider} driver dependencies...`);
611
627
  const pm = detectFromProject(functionsPath);
612
628
  const cmds = getCommands(pm);
@@ -629,6 +645,10 @@ async function installConnectorDriverDependencies(
629
645
  };
630
646
  const pkg = nugetMap[rdbDef.provider];
631
647
  if (!pkg) return;
648
+ if (session.mode === "collect") {
649
+ session.addWarning(`external-install: NuGet package ${pkg} will be added to ${functionsDir}/functions.csproj on apply.`);
650
+ return;
651
+ }
632
652
  console.log(`\n📦 Installing ${rdbDef.provider} NuGet package...`);
633
653
  runSpawnSyncCommand("dotnet", ["add", path.join(functionsPath, "functions.csproj"), "package", pkg], functionsPath);
634
654
  console.log(`✅ ${pkg} installed`);
@@ -644,11 +664,11 @@ async function installConnectorDriverDependencies(
644
664
  const pipPkg = pipMap[rdbDef.provider];
645
665
  if (!pipPkg) return;
646
666
  const reqPath = path.join(functionsPath, "requirements.txt");
647
- if (fs.existsSync(reqPath)) {
648
- const existing = fs.readFileSync(reqPath, "utf-8");
667
+ if (session.fileExists(reqPath)) {
668
+ const existing = session.readFile(reqPath);
649
669
  if (!existing.includes(pipPkg)) {
650
670
  console.log(`\n📦 Adding ${pipPkg} to requirements.txt...`);
651
- fs.appendFileSync(reqPath, `\n${pipPkg}\n`);
671
+ session.writeFile(reqPath, `${existing}\n${pipPkg}\n`, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
652
672
  console.log(`✅ ${pipPkg} added`);
653
673
  }
654
674
  }
@@ -660,7 +680,8 @@ async function installConnectorDriverDependencies(
660
680
  async function generateConnectorBFFRoutesFiles(
661
681
  modelInfo: ModelInfo,
662
682
  apiDir: string,
663
- sharedPackageName: string
683
+ sharedPackageName: string,
684
+ session: FileOperationSession
664
685
  ): Promise<void> {
665
686
  console.log("\n🔌 Generating Connector BFF API routes...");
666
687
 
@@ -670,13 +691,11 @@ async function generateConnectorBFFRoutesFiles(
670
691
  const listRoutePath = path.join(process.cwd(), apiDir, modelCamel, "route.ts");
671
692
  const detailRoutePath = path.join(process.cwd(), apiDir, modelCamel, "[id]", "route.ts");
672
693
 
673
- fs.mkdirSync(path.dirname(listRoutePath), { recursive: true });
674
- fs.mkdirSync(path.dirname(detailRoutePath), { recursive: true });
675
-
676
694
  const routes = generateConnectorBFFRoutes(modelInfo, sharedPackageName, operations);
677
695
 
678
- fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
679
- fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
696
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
697
+ session.writeFile(listRoutePath, routes.listRoute, meta);
698
+ session.writeFile(detailRoutePath, routes.detailRoute, meta);
680
699
 
681
700
  console.log(`✅ Created: ${listRoutePath}`);
682
701
  console.log(`✅ Created: ${detailRoutePath}`);
@@ -717,12 +736,12 @@ function describeFunctionsOutputPath(functionsDir: string, backendLanguage: Back
717
736
  return `${functionsDir}/blueprints/`;
718
737
  }
719
738
 
720
- function updatePythonFunctionRegistrations(functionAppPath: string, registration: string): void {
721
- if (!fs.existsSync(functionAppPath)) {
739
+ function updatePythonFunctionRegistrations(functionAppPath: string, registration: string, session: FileOperationSession): void {
740
+ if (!session.fileExists(functionAppPath)) {
722
741
  throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
723
742
  }
724
743
 
725
- const content = fs.readFileSync(functionAppPath, "utf-8");
744
+ const content = session.readFile(functionAppPath);
726
745
 
727
746
  // Check if import line already exists (handles init-generated layout)
728
747
  const importLine = registration.split("\n").find((l) => l.startsWith("from ") || l.startsWith("import "));
@@ -739,7 +758,7 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
739
758
  }
740
759
 
741
760
  const updated = content.replace(marker, `${registration}\n${marker}`);
742
- fs.writeFileSync(functionAppPath, updated, "utf-8");
761
+ session.writeFile(functionAppPath, updated, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
743
762
  }
744
763
 
745
764
  /**
@@ -748,7 +767,8 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
748
767
  async function generateBFFRoutes(
749
768
  modelInfo: any,
750
769
  apiDir: string,
751
- sharedPackageName: string
770
+ sharedPackageName: string,
771
+ session: FileOperationSession
752
772
  ): Promise<void> {
753
773
  console.log("\n🔨 Generating Next.js BFF API routes...");
754
774
 
@@ -771,24 +791,13 @@ async function generateBFFRoutes(
771
791
  "route.ts"
772
792
  );
773
793
 
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
794
  // コードを生成
787
795
  const routes = generateCompactBFFRoutes(modelInfo, sharedPackageName);
788
796
 
789
797
  // ファイルに書き込み
790
- fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
791
- fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
798
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
799
+ session.writeFile(listRoutePath, routes.listRoute, meta);
800
+ session.writeFile(detailRoutePath, routes.detailRoute, meta);
792
801
 
793
802
  console.log(`✅ Created: ${listRoutePath}`);
794
803
  console.log(`✅ Created: ${detailRoutePath}`);
@@ -797,7 +806,7 @@ async function generateBFFRoutes(
797
806
  /**
798
807
  * Generate Cosmos DB container Bicep file
799
808
  */
800
- export async function generateCosmosContainer(modelInfo: any): Promise<void> {
809
+ export async function generateCosmosContainer(modelInfo: any, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
801
810
  console.log("\n🗄️ Generating Cosmos DB container Bicep file...");
802
811
 
803
812
  const modelKebab = toKebabCase(modelInfo.name);
@@ -811,13 +820,8 @@ export async function generateCosmosContainer(modelInfo: any): Promise<void> {
811
820
  return;
812
821
  }
813
822
 
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
823
  // Generate container Bicep file
824
+ const containersDir = path.join(infraDir, "containers");
821
825
  const containerFileName = `${modelKebab}-container.bicep`;
822
826
  const containerFilePath = path.join(containersDir, containerFileName);
823
827
 
@@ -879,26 +883,26 @@ output containerName string = container.name
879
883
  `;
880
884
 
881
885
  // Write Bicep file (overwrite if exists)
882
- fs.writeFileSync(containerFilePath, bicepContent, "utf-8");
886
+ session.writeFile(containerFilePath, bicepContent, { ownership: "managed", generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name });
883
887
  console.log(`✅ Created: ${containerFilePath}`);
884
888
 
885
889
  // Update main.bicep to include this container module
886
- await updateMainBicepWithContainer(modelKebab, modelPascal);
890
+ await updateMainBicepWithContainer(modelKebab, modelPascal, session);
887
891
  }
888
892
 
889
893
  /**
890
894
  * Update main.bicep to include new container module
891
895
  */
892
- async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string): Promise<void> {
896
+ async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string, session: FileOperationSession): Promise<void> {
893
897
  const cwd = process.cwd();
894
898
  const mainBicepPath = path.join(cwd, "infra", "main.bicep");
895
899
 
896
- if (!fs.existsSync(mainBicepPath)) {
900
+ if (!session.fileExists(mainBicepPath)) {
897
901
  console.log("ℹ️ main.bicep not found. Please manually add the container module.");
898
902
  return;
899
903
  }
900
904
 
901
- let mainBicepContent = fs.readFileSync(mainBicepPath, "utf-8");
905
+ let mainBicepContent = session.readFile(mainBicepPath);
902
906
 
903
907
  // Check if container module already exists
904
908
  const containerModuleName = `${modelPascal.charAt(0).toLowerCase()}${modelPascal.slice(1)}Container`;
@@ -950,14 +954,14 @@ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
950
954
  containerModule +
951
955
  mainBicepContent.slice(insertPosition);
952
956
 
953
- fs.writeFileSync(mainBicepPath, mainBicepContent, "utf-8");
957
+ session.writeFile(mainBicepPath, mainBicepContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
954
958
  console.log(`✅ Added container module to main.bicep`);
955
959
  }
956
960
 
957
961
  /**
958
962
  * Generate UI components (list, detail, form, create, edit pages)
959
963
  */
960
- export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions): Promise<void> {
964
+ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
961
965
  console.log("\n🎨 Generating UI components...");
962
966
 
963
967
  const modelKebab = toKebabCase(modelInfo.name);
@@ -971,13 +975,6 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
971
975
  const idDir = path.join(modelDir, "[id]");
972
976
  const editDir = path.join(idDir, "edit");
973
977
 
974
- // Create directories
975
- [componentsDir, newDir, editDir].forEach(dir => {
976
- if (!fs.existsSync(dir)) {
977
- fs.mkdirSync(dir, { recursive: true });
978
- }
979
- });
980
-
981
978
  // Generate and write files
982
979
  const listPage = generateListPage(modelInfo, sharedPackageName, authOptions);
983
980
  const detailPage = generateDetailPage(modelInfo, sharedPackageName, authOptions);
@@ -986,13 +983,14 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
986
983
  const editPage = generateEditPage(modelInfo, sharedPackageName, authOptions);
987
984
  const authLayout = generateAuthLayout(modelInfo, authOptions);
988
985
 
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");
986
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
987
+ session.writeFile(path.join(modelDir, "page.tsx"), listPage, meta);
988
+ session.writeFile(path.join(idDir, "page.tsx"), detailPage, meta);
989
+ session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, meta);
990
+ session.writeFile(path.join(newDir, "page.tsx"), newPage, meta);
991
+ session.writeFile(path.join(editDir, "page.tsx"), editPage, meta);
994
992
  if (authLayout) {
995
- fs.writeFileSync(path.join(modelDir, "layout.tsx"), authLayout, "utf-8");
993
+ session.writeFile(path.join(modelDir, "layout.tsx"), authLayout, meta);
996
994
  }
997
995
 
998
996
  console.log(`✅ Created: ${path.join(modelDir, "page.tsx")}`);
@@ -1008,7 +1006,7 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
1008
1006
  /**
1009
1007
  * Update navigation menu on the homepage
1010
1008
  */
1011
- async function updateNavigationMenu(modelInfo: any): Promise<void> {
1009
+ async function updateNavigationMenu(modelInfo: any, session: FileOperationSession): Promise<void> {
1012
1010
  console.log("\n📋 Updating navigation menu...");
1013
1011
 
1014
1012
  const modelKebab = toKebabCase(modelInfo.name);
@@ -1017,13 +1015,13 @@ async function updateNavigationMenu(modelInfo: any): Promise<void> {
1017
1015
  // Update scaffold config
1018
1016
  const configPath = path.join(cwd, "lib", "scaffold-config.ts");
1019
1017
 
1020
- if (!fs.existsSync(configPath)) {
1018
+ if (!session.fileExists(configPath)) {
1021
1019
  console.log("⚠️ scaffold-config.ts not found. Skipping navigation menu update.");
1022
1020
  return;
1023
1021
  }
1024
1022
 
1025
1023
  // Read existing config
1026
- const configContent = fs.readFileSync(configPath, "utf-8");
1024
+ const configContent = session.readFile(configPath);
1027
1025
 
1028
1026
  // Parse models array - extract each model entry
1029
1027
  const models: Array<{ name: string; path: string; label: string }> = [];
@@ -1070,6 +1068,7 @@ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label
1070
1068
  };
1071
1069
  `;
1072
1070
 
1073
- fs.writeFileSync(configPath, newConfigContent, "utf-8");
1071
+ session.writeFile(configPath, newConfigContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
1074
1072
  console.log(`✅ Added ${modelInfo.name} to navigation menu`);
1075
1073
  }
1074
+