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
package/src/cli/index.ts CHANGED
@@ -8,6 +8,10 @@ import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelC
8
8
  import { provisionCommand } from "./commands/provision";
9
9
  import { addConnectorCommand } from "./commands/add-connector";
10
10
  import { addAuthCommand } from "./commands/add-auth";
11
+ import { planScaffoldOperation } from "../core/operations/scaffold-plan";
12
+ import { inspectArtifacts } from "../core/project/artifacts";
13
+ import { detectDrift } from "../core/project/drift";
14
+ import { runVerify } from "../core/verify";
11
15
  import { isMachineCommand, runMachineCli } from "../machine";
12
16
 
13
17
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
@@ -147,8 +151,19 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
147
151
  .option("--functions-dir <dir>", "Azure Functions directory", "functions")
148
152
  .option("--api-dir <dir>", "Next.js API routes directory", "app/api")
149
153
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
150
- .action((model, options) => {
151
- scaffoldCommand({
154
+ .option("--dry-run", "Show planned file changes without writing anything", false)
155
+ .action(async (model, options) => {
156
+ if (options.dryRun) {
157
+ await runScaffoldDryRun({
158
+ model,
159
+ functionsDir: options.functionsDir,
160
+ apiDir: options.apiDir,
161
+ apiOnly: options.apiOnly,
162
+ });
163
+ return;
164
+ }
165
+
166
+ await scaffoldCommand({
152
167
  model,
153
168
  functionsDir: options.functionsDir,
154
169
  apiDir: options.apiDir,
@@ -181,9 +196,140 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
181
196
  });
182
197
  });
183
198
 
199
+ program
200
+ .command("status")
201
+ .description("Show generated artifact status and drift against the current project state")
202
+ .option("--artifacts", "List all tracked artifacts with ownership", false)
203
+ .action(async (options) => {
204
+ await runStatusCommand({ artifacts: options.artifacts });
205
+ });
206
+
207
+ program
208
+ .command("verify")
209
+ .description("Run verification checks (structure, drift, typecheck)")
210
+ .option("--checks <ids>", "Comma-separated check ids to run (structure,drift,typecheck)")
211
+ .action(async (options) => {
212
+ await runVerifyCommand({ checks: options.checks });
213
+ });
214
+
184
215
  return program;
185
216
  }
186
217
 
218
+ async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }): Promise<void> {
219
+ console.log("🔍 Scaffold dry-run — no files will be written.\n");
220
+
221
+ const plan = await planScaffoldOperation(options);
222
+
223
+ const actionIcons: Record<string, string> = {
224
+ create: "➕",
225
+ update: "♻️ ",
226
+ overwrite: "⚠️ ",
227
+ append: "🔗",
228
+ delete: "🗑️ ",
229
+ skip: "⏭️ ",
230
+ };
231
+
232
+ for (const operation of plan.operations) {
233
+ if (operation.action === "skip") continue;
234
+ const icon = actionIcons[operation.action] ?? "•";
235
+ const conflictNote = operation.conflict ? ` (conflict: ${operation.conflictReason})` : "";
236
+ console.log(` ${icon} ${operation.action.padEnd(9)} ${operation.path} [${operation.ownership}]${conflictNote}`);
237
+ }
238
+
239
+ const skipped = plan.operations.filter((operation) => operation.action === "skip").length;
240
+ if (skipped > 0) {
241
+ console.log(` ⏭️ ${skipped} file(s) unchanged (skipped)`);
242
+ }
243
+
244
+ if (plan.warnings.length > 0) {
245
+ console.log("\n⚠️ Warnings:");
246
+ for (const warning of plan.warnings) {
247
+ console.log(` - ${warning}`);
248
+ }
249
+ }
250
+
251
+ console.log(`\n📋 Plan ${plan.planId} saved.`);
252
+ if (plan.requiresApproval) {
253
+ console.log(`⚠️ ${plan.conflicts.length} file(s) were modified after generation and would be overwritten.`);
254
+ console.log(` Review the conflicts, then run: swallowkit scaffold ${options.model} (or machine apply scaffold --plan ${plan.planId} --approve)`);
255
+ } else {
256
+ console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}`);
257
+ }
258
+ }
259
+
260
+ async function runStatusCommand(options: { artifacts?: boolean }): Promise<void> {
261
+ const inspection = inspectArtifacts();
262
+ const drift = await detectDrift();
263
+
264
+ console.log("📦 SwallowKit project status\n");
265
+
266
+ if (!inspection.ledgerFound) {
267
+ console.log("ℹ️ No artifact ledger (.swallowkit/artifacts.json) found yet.");
268
+ console.log(" Run scaffold once with this SwallowKit version to start tracking artifacts.\n");
269
+ } else {
270
+ const total = inspection.artifacts.length;
271
+ const missing = inspection.artifacts.filter((artifact) => !artifact.exists).length;
272
+ const modified = inspection.artifacts.filter((artifact) => artifact.modified).length;
273
+ console.log(` Tracked artifacts: ${total}`);
274
+ console.log(` Modified after generation: ${modified}`);
275
+ console.log(` Missing: ${missing}\n`);
276
+
277
+ if (options.artifacts) {
278
+ for (const artifact of inspection.artifacts) {
279
+ const flags = [
280
+ artifact.modified ? "modified" : null,
281
+ !artifact.exists ? "missing" : null,
282
+ ].filter(Boolean);
283
+ const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
284
+ console.log(` - ${artifact.path} [${artifact.ownership}]${suffix}`);
285
+ }
286
+ console.log("");
287
+ }
288
+ }
289
+
290
+ if (drift.findings.length === 0) {
291
+ console.log("✅ No drift detected.");
292
+ return;
293
+ }
294
+
295
+ console.log(`⚠️ Drift findings (${drift.findings.length}):`);
296
+ for (const finding of drift.findings) {
297
+ console.log(` [${finding.severity}] ${finding.kind}: ${finding.message}`);
298
+ console.log(` → ${finding.repairAction}`);
299
+ }
300
+ }
301
+
302
+ async function runVerifyCommand(options: { checks?: string }): Promise<void> {
303
+ console.log("🔎 Running verification checks...\n");
304
+
305
+ const result = await runVerify(options.checks ? options.checks.split(",") : undefined);
306
+
307
+ const statusIcons: Record<string, string> = {
308
+ pass: "✅",
309
+ fail: "❌",
310
+ skip: "⏭️ ",
311
+ error: "💥",
312
+ };
313
+
314
+ for (const check of result.checks) {
315
+ console.log(` ${statusIcons[check.status]} ${check.id.padEnd(10)} ${check.title} (${check.durationMs}ms)`);
316
+ if (check.status === "fail" || check.status === "error") {
317
+ for (const action of check.suggestedActions) {
318
+ console.log(` → ${action}`);
319
+ }
320
+ }
321
+ }
322
+
323
+ console.log(
324
+ `\n${result.summary.done ? "✅" : "❌"} ${result.summary.passed} passed, ${result.summary.failed} failed, ${result.summary.skipped} skipped, ${result.summary.errors} error(s)`
325
+ );
326
+
327
+ if (!result.summary.done) {
328
+ console.log(' Details: swallowkit machine explain failure');
329
+ process.exitCode = 1;
330
+ }
331
+ }
332
+
187
333
  export async function runCli(argv: string[] = process.argv): Promise<void> {
188
334
  ensureUtf8ConsoleOnWindows();
189
335
  if (isMachineCommand(argv)) {
@@ -37,7 +37,7 @@ const DEFAULT_CONFIG: SwallowKitConfig = {
37
37
  /**
38
38
  * 設定ファイルを読み込み
39
39
  */
40
- export function loadConfig(configPath?: string, throwOnError = false): SwallowKitConfig {
40
+ export function loadConfig(configPath?: string, throwOnError = false, quiet = false): SwallowKitConfig {
41
41
  const defaultPaths = [
42
42
  "swallowkit.config.json",
43
43
  "swallowkit.config.js",
@@ -51,7 +51,7 @@ export function loadConfig(configPath?: string, throwOnError = false): SwallowKi
51
51
 
52
52
  if (fs.existsSync(fullPath)) {
53
53
  try {
54
- console.log(`📋 設定ファイルを読み込み: ${filePath}`);
54
+ if (!quiet) console.log(`📋 設定ファイルを読み込み: ${filePath}`);
55
55
 
56
56
  if (filePath.endsWith(".json")) {
57
57
  const configData = fs.readFileSync(fullPath, "utf-8");
@@ -64,12 +64,12 @@ export function loadConfig(configPath?: string, throwOnError = false): SwallowKi
64
64
  }
65
65
  } catch (error) {
66
66
  if (throwOnError) throw error;
67
- console.warn(`⚠️ 設定ファイルの読み込みに失敗: ${filePath}`, error);
67
+ if (!quiet) console.warn(`⚠️ 設定ファイルの読み込みに失敗: ${filePath}`, error);
68
68
  }
69
69
  }
70
70
  }
71
71
 
72
- console.log("📋 デフォルト設定を使用");
72
+ if (!quiet) console.log("📋 デフォルト設定を使用");
73
73
  return DEFAULT_CONFIG;
74
74
  }
75
75
 
@@ -98,6 +98,7 @@ function mergeConfig(defaultConfig: SwallowKitConfig, userConfig: Partial<Swallo
98
98
  auth: userConfig.auth
99
99
  ? { ...defaultConfig.auth, ...userConfig.auth }
100
100
  : defaultConfig.auth,
101
+ verify: userConfig.verify ?? defaultConfig.verify,
101
102
  };
102
103
  }
103
104
 
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Plan / Apply Auth — add-auth を collect モードで実行して差分計画を作成し、
3
+ * 承認・鮮度チェックを経て実際に適用する。
4
+ */
5
+
6
+ import * as crypto from "crypto";
7
+ import * as path from "path";
8
+ import { addAuthCommand } from "../../cli/commands/add-auth";
9
+ import { MachineCommandError } from "../../machine/errors";
10
+ import { getSwallowKitVersion } from "../../version";
11
+ import { findArtifactRecord, loadArtifactLedger } from "../project/artifacts";
12
+ import { deletePlanState, loadPlanState, savePlanState } from "../project/state";
13
+ import {
14
+ ArtifactOwnership,
15
+ FileOperationSession,
16
+ hashFileIfExists,
17
+ runWithFileSession,
18
+ } from "./file-session";
19
+ import {
20
+ captureConsoleMessagesWithError,
21
+ deriveCapturedErrorMessage,
22
+ interceptProcessExit,
23
+ ProcessExitInterceptError,
24
+ trackFileMutations,
25
+ } from "./runtime";
26
+
27
+ export interface PlanAuthOptions {
28
+ provider: string;
29
+ scheme?: string;
30
+ }
31
+
32
+ export type AuthPlanAction = "create" | "update" | "overwrite" | "append" | "delete" | "skip";
33
+
34
+ export interface AuthPlanOperation {
35
+ path: string;
36
+ action: AuthPlanAction;
37
+ ownership: ArtifactOwnership;
38
+ conflict: boolean;
39
+ conflictReason?: string;
40
+ }
41
+
42
+ export interface AuthPlanData {
43
+ planId: string;
44
+ planType: "auth";
45
+ provider: string;
46
+ scheme?: string;
47
+ createdAt: string;
48
+ swallowkitVersion: string;
49
+ operations: AuthPlanOperation[];
50
+ warnings: string[];
51
+ conflicts: AuthPlanOperation[];
52
+ requiresApproval: boolean;
53
+ /** project-relative path -> pre-plan content hash (null = did not exist). Used for stale-plan detection. */
54
+ fingerprints: Record<string, string | null>;
55
+ }
56
+
57
+ function deriveAuthErrorMessage(messages: { errors: string[]; warnings: string[]; logs: string[] }): string {
58
+ return deriveCapturedErrorMessage(messages, "Auth operation failed.");
59
+ }
60
+
61
+ /**
62
+ * add-auth を collect セッションで実行し、記録された操作を返す。ディスクには書き込まない。
63
+ */
64
+ async function collectAuthOperations(options: PlanAuthOptions): Promise<FileOperationSession> {
65
+ const session = new FileOperationSession("collect");
66
+ const originalMachineOutput = process.env.SWALLOWKIT_MACHINE_OUTPUT;
67
+ process.env.SWALLOWKIT_MACHINE_OUTPUT = "1";
68
+
69
+ try {
70
+ const captured = await captureConsoleMessagesWithError(async () => {
71
+ await interceptProcessExit(async () => {
72
+ await runWithFileSession(session, async () => {
73
+ await addAuthCommand({ provider: options.provider, scheme: options.scheme });
74
+ });
75
+ });
76
+ });
77
+
78
+ if (captured.error) {
79
+ if (captured.error instanceof ProcessExitInterceptError) {
80
+ throw new Error(deriveAuthErrorMessage(captured.messages));
81
+ }
82
+ if (captured.error instanceof Error) {
83
+ throw captured.error;
84
+ }
85
+ throw new Error(deriveAuthErrorMessage(captured.messages));
86
+ }
87
+
88
+ return session;
89
+ } finally {
90
+ if (originalMachineOutput === undefined) {
91
+ delete process.env.SWALLOWKIT_MACHINE_OUTPUT;
92
+ } else {
93
+ process.env.SWALLOWKIT_MACHINE_OUTPUT = originalMachineOutput;
94
+ }
95
+ }
96
+ }
97
+
98
+ export async function planAuthOperation(options: PlanAuthOptions): Promise<AuthPlanData> {
99
+ const projectRoot = process.cwd();
100
+ const session = await collectAuthOperations(options);
101
+ const ledger = loadArtifactLedger(projectRoot);
102
+
103
+ const operations: AuthPlanOperation[] = [];
104
+ const fingerprints: Record<string, string | null> = {};
105
+ const warnings = [...session.warnings];
106
+
107
+ for (const op of session.operations) {
108
+ fingerprints[op.path] = op.previousHash;
109
+
110
+ let action: AuthPlanAction;
111
+ let conflict = false;
112
+ let conflictReason: string | undefined;
113
+
114
+ switch (op.action) {
115
+ case "create":
116
+ action = "create";
117
+ break;
118
+ case "skip":
119
+ action = "skip";
120
+ break;
121
+ case "append":
122
+ action = "append";
123
+ break;
124
+ case "delete":
125
+ action = "delete";
126
+ break;
127
+ case "modify": {
128
+ if (op.ownership === "extension-point") {
129
+ // 設定ファイル・登録ポイントへの追記型変更は想定内の更新として扱う
130
+ action = "update";
131
+ break;
132
+ }
133
+ const record = ledger ? findArtifactRecord(ledger, op.path) : undefined;
134
+ if (record && record.contentHash === op.previousHash) {
135
+ action = "update";
136
+ } else if (record) {
137
+ action = "overwrite";
138
+ conflict = true;
139
+ conflictReason = "modified-after-generation";
140
+ } else {
141
+ action = "overwrite";
142
+ conflict = true;
143
+ conflictReason = "unknown-provenance";
144
+ warnings.push(
145
+ `unknown-provenance: ${op.path} exists but is not tracked in .swallowkit/artifacts.json; regenerating will overwrite it.`
146
+ );
147
+ }
148
+ break;
149
+ }
150
+ }
151
+
152
+ operations.push({
153
+ path: op.path,
154
+ action,
155
+ ownership: op.ownership,
156
+ conflict,
157
+ ...(conflictReason ? { conflictReason } : {}),
158
+ });
159
+ }
160
+
161
+ const conflicts = operations.filter((op) => op.conflict);
162
+
163
+ const planId = crypto
164
+ .createHash("sha256")
165
+ .update(
166
+ JSON.stringify({
167
+ provider: options.provider,
168
+ scheme: options.scheme,
169
+ operations: operations.map((op) => ({ path: op.path, action: op.action })),
170
+ fingerprints,
171
+ })
172
+ )
173
+ .digest("hex")
174
+ .slice(0, 12);
175
+
176
+ const plan: AuthPlanData = {
177
+ planId,
178
+ planType: "auth",
179
+ provider: options.provider,
180
+ ...(options.scheme ? { scheme: options.scheme } : {}),
181
+ createdAt: new Date().toISOString(),
182
+ swallowkitVersion: getSwallowKitVersion(),
183
+ operations,
184
+ warnings,
185
+ conflicts,
186
+ requiresApproval: conflicts.length > 0,
187
+ fingerprints,
188
+ };
189
+
190
+ savePlanState(plan, projectRoot);
191
+ return plan;
192
+ }
193
+
194
+ export function verifyAuthPlanFingerprints(
195
+ plan: AuthPlanData,
196
+ projectRoot: string = process.cwd()
197
+ ): { fresh: boolean; changedFiles: string[] } {
198
+ const changedFiles: string[] = [];
199
+ for (const [relativePath, expectedHash] of Object.entries(plan.fingerprints)) {
200
+ const currentHash = hashFileIfExists(path.join(projectRoot, relativePath));
201
+ if (currentHash !== expectedHash) {
202
+ changedFiles.push(relativePath);
203
+ }
204
+ }
205
+ return { fresh: changedFiles.length === 0, changedFiles };
206
+ }
207
+
208
+ export interface ApplyAuthOptions {
209
+ provider?: string;
210
+ scheme?: string;
211
+ planId?: string;
212
+ approve?: boolean;
213
+ }
214
+
215
+ export interface ApplyAuthResult {
216
+ provider: string;
217
+ scheme?: string;
218
+ planId: string;
219
+ createdFiles: string[];
220
+ updatedFiles: string[];
221
+ appendedFiles: string[];
222
+ deletedFiles: string[];
223
+ createdDirectories: string[];
224
+ diagnostics: string[];
225
+ approvedConflicts: AuthPlanOperation[];
226
+ warnings: string[];
227
+ }
228
+
229
+ async function runCommitAuthOperation(options: PlanAuthOptions): Promise<{
230
+ createdFiles: string[];
231
+ updatedFiles: string[];
232
+ appendedFiles: string[];
233
+ deletedFiles: string[];
234
+ createdDirectories: string[];
235
+ diagnostics: string[];
236
+ }> {
237
+ const originalMachineOutput = process.env.SWALLOWKIT_MACHINE_OUTPUT;
238
+ process.env.SWALLOWKIT_MACHINE_OUTPUT = "1";
239
+
240
+ try {
241
+ const tracked = await trackFileMutations(async () => {
242
+ const captured = await captureConsoleMessagesWithError(async () => {
243
+ await interceptProcessExit(async () => {
244
+ await addAuthCommand({ provider: options.provider, scheme: options.scheme });
245
+ });
246
+ });
247
+
248
+ if (captured.error) {
249
+ if (captured.error instanceof ProcessExitInterceptError) {
250
+ throw new Error(deriveAuthErrorMessage(captured.messages));
251
+ }
252
+ if (captured.error instanceof Error) {
253
+ throw captured.error;
254
+ }
255
+ throw new Error(deriveAuthErrorMessage(captured.messages));
256
+ }
257
+
258
+ return captured.messages;
259
+ });
260
+
261
+ return {
262
+ createdFiles: tracked.mutations.createdFiles,
263
+ updatedFiles: tracked.mutations.updatedFiles,
264
+ appendedFiles: tracked.mutations.appendedFiles,
265
+ deletedFiles: tracked.mutations.deletedFiles,
266
+ createdDirectories: tracked.mutations.createdDirectories,
267
+ diagnostics: [
268
+ ...tracked.result.warnings.map((warning) => `warning:${warning}`),
269
+ ...tracked.result.errors.map((error) => `error:${error}`),
270
+ ],
271
+ };
272
+ } finally {
273
+ if (originalMachineOutput === undefined) {
274
+ delete process.env.SWALLOWKIT_MACHINE_OUTPUT;
275
+ } else {
276
+ process.env.SWALLOWKIT_MACHINE_OUTPUT = originalMachineOutput;
277
+ }
278
+ }
279
+ }
280
+
281
+ export async function applyAuthOperation(options: ApplyAuthOptions): Promise<ApplyAuthResult> {
282
+ const projectRoot = process.cwd();
283
+
284
+ let basePlan: AuthPlanData | null = null;
285
+
286
+ if (options.planId) {
287
+ basePlan = loadPlanState<AuthPlanData>(options.planId, projectRoot);
288
+ if (!basePlan) {
289
+ throw new MachineCommandError(
290
+ "plan-not-found",
291
+ `Plan "${options.planId}" was not found. Run "plan auth" to create a new plan.`,
292
+ { planId: options.planId },
293
+ "blocked"
294
+ );
295
+ }
296
+
297
+ const freshness = verifyAuthPlanFingerprints(basePlan, projectRoot);
298
+ if (!freshness.fresh) {
299
+ throw new MachineCommandError(
300
+ "stale-plan",
301
+ `Plan "${options.planId}" is stale: ${freshness.changedFiles.length} file(s) changed after the plan was created. Re-run "plan auth".`,
302
+ { planId: options.planId, changedFiles: freshness.changedFiles },
303
+ "blocked"
304
+ );
305
+ }
306
+ }
307
+
308
+ const provider = options.provider ?? basePlan?.provider;
309
+ if (!provider) {
310
+ throw new MachineCommandError(
311
+ "invalid-arguments",
312
+ "Either --provider or --plan <planId> must be provided.",
313
+ undefined,
314
+ "failed"
315
+ );
316
+ }
317
+
318
+ const effectiveOptions: PlanAuthOptions = {
319
+ provider,
320
+ scheme: options.scheme ?? basePlan?.scheme,
321
+ };
322
+
323
+ // 常に最新状態で collect し、競合を再確認する
324
+ const currentPlan = await planAuthOperation(effectiveOptions);
325
+
326
+ if (currentPlan.requiresApproval && !options.approve) {
327
+ throw new MachineCommandError(
328
+ "approval-required",
329
+ `${currentPlan.conflicts.length} file(s) were modified after generation and would be overwritten. Review the conflicts and re-run with --approve to proceed.`,
330
+ {
331
+ planId: currentPlan.planId,
332
+ conflicts: currentPlan.conflicts,
333
+ },
334
+ "requires-human"
335
+ );
336
+ }
337
+
338
+ const result = await runCommitAuthOperation(effectiveOptions);
339
+
340
+ deletePlanState(currentPlan.planId, projectRoot);
341
+ if (options.planId && options.planId !== currentPlan.planId) {
342
+ deletePlanState(options.planId, projectRoot);
343
+ }
344
+
345
+ return {
346
+ ...result,
347
+ provider,
348
+ ...(effectiveOptions.scheme ? { scheme: effectiveOptions.scheme } : {}),
349
+ planId: currentPlan.planId,
350
+ approvedConflicts: options.approve ? currentPlan.conflicts : [],
351
+ warnings: currentPlan.warnings,
352
+ };
353
+ }
@@ -1,8 +1,11 @@
1
- import * as fs from "fs";
2
1
  import * as path from "path";
3
2
  import { ensureSwallowKitProject, loadConfig } from "../config";
4
3
  import { toPascalCase } from "../scaffold/model-parser";
5
4
  import { syncProjectManifest } from "../project/manifest";
5
+ import { recordSessionOperations } from "../project/artifacts";
6
+ import { FileOperationSession, getActiveFileSession } from "./file-session";
7
+
8
+ const CREATE_MODEL_GENERATOR = "create-model";
6
9
 
7
10
  export interface CreateModelOperationOptions {
8
11
  names: string[];
@@ -87,21 +90,24 @@ export const connectorConfig = {
87
90
  `;
88
91
  }
89
92
 
90
- function updateSharedIndex(kebabName: string, pascalName: string): boolean {
91
- const indexPath = path.join("shared", "index.ts");
93
+ function updateSharedIndex(kebabName: string, pascalName: string, session: FileOperationSession): boolean {
94
+ const indexPath = path.join(process.cwd(), "shared", "index.ts");
92
95
 
93
- if (!fs.existsSync(indexPath)) {
96
+ if (!session.fileExists(indexPath)) {
94
97
  return false;
95
98
  }
96
99
 
97
- const content = fs.readFileSync(indexPath, "utf-8");
100
+ const content = session.readFile(indexPath);
98
101
  const exportLine = `export { ${pascalName} } from './models/${kebabName}';`;
99
102
 
100
103
  if (content.includes(exportLine)) {
101
104
  return false;
102
105
  }
103
106
 
104
- fs.appendFileSync(indexPath, `${exportLine}\n`);
107
+ session.writeFile(indexPath, `${content}${exportLine}\n`, {
108
+ ownership: "extension-point",
109
+ generator: CREATE_MODEL_GENERATOR,
110
+ });
105
111
  return true;
106
112
  }
107
113
 
@@ -123,9 +129,8 @@ export async function createModelOperation(options: CreateModelOperationOptions)
123
129
  }
124
130
 
125
131
  const modelsDir = options.modelsDir || "shared/models";
126
- if (!fs.existsSync(modelsDir)) {
127
- fs.mkdirSync(modelsDir, { recursive: true });
128
- }
132
+
133
+ const session = getActiveFileSession() ?? new FileOperationSession("commit");
129
134
 
130
135
  const createdFiles: string[] = [];
131
136
  const skippedFiles: string[] = [];
@@ -136,7 +141,7 @@ export async function createModelOperation(options: CreateModelOperationOptions)
136
141
  const filePath = path.join(modelsDir, `${kebabName}.ts`);
137
142
  const pascalName = toPascalCase(name);
138
143
 
139
- if (fs.existsSync(filePath)) {
144
+ if (session.fileExists(path.resolve(filePath))) {
140
145
  const overwriteMode = options.overwriteMode || "prompt";
141
146
  const shouldOverwrite = overwriteMode === "always"
142
147
  ? true
@@ -155,15 +160,22 @@ export async function createModelOperation(options: CreateModelOperationOptions)
155
160
  const content = options.connector && connectorType
156
161
  ? generateConnectorModelTemplate(name, options.connector, connectorType)
157
162
  : generateModelTemplate(name);
158
- fs.writeFileSync(filePath, content, "utf-8");
163
+ session.writeFile(path.resolve(filePath), content, {
164
+ ownership: "user-owned",
165
+ generator: CREATE_MODEL_GENERATOR,
166
+ sourceModel: pascalName,
167
+ });
159
168
  createdFiles.push(filePath.replace(/\\/g, "/"));
160
169
 
161
- if (updateSharedIndex(kebabName, pascalName)) {
170
+ if (updateSharedIndex(kebabName, pascalName, session)) {
162
171
  updatedIndex = true;
163
172
  }
164
173
  }
165
174
 
166
- await syncProjectManifest();
175
+ if (session.mode === "commit") {
176
+ recordSessionOperations(session.operations, {});
177
+ await syncProjectManifest();
178
+ }
167
179
 
168
180
  return {
169
181
  createdFiles,