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.
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +154 -157
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +178 -25
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +7 -2
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +113 -123
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +127 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +8 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/operations/auth-operations.d.ts +57 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +269 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +23 -13
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +5 -0
- package/dist/core/operations/runtime.d.ts.map +1 -1
- package/dist/core/operations/runtime.js +23 -0
- package/dist/core/operations/runtime.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js +1 -13
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +255 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +24 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.js +9 -9
- package/dist/core/scaffold/native-schema-generator.d.ts +7 -1
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +104 -47
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +6 -6
- package/dist/core/scaffold/ui-generator.js +19 -19
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +62 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +343 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +8 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +4 -1
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +9 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +350 -16
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +202 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
- package/src/__tests__/agent-loop-phase34.test.ts +449 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/init.test.ts +50 -0
- package/src/__tests__/mcp.test.ts +113 -5
- package/src/__tests__/plan-apply.test.ts +461 -0
- package/src/__tests__/scaffold.test.ts +2 -2
- package/src/cli/commands/add-auth.ts +164 -164
- package/src/cli/commands/init.ts +186 -24
- package/src/cli/commands/scaffold.ts +136 -130
- package/src/cli/index.ts +148 -2
- package/src/core/config.ts +5 -4
- package/src/core/operations/auth-operations.ts +353 -0
- package/src/core/operations/create-model.ts +25 -13
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +23 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +2 -16
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/workflows.ts +249 -0
- package/src/core/scaffold/auth-generator.ts +24 -27
- package/src/core/scaffold/functions-generator.ts +9 -9
- package/src/core/scaffold/native-schema-generator.ts +152 -70
- package/src/core/scaffold/nextjs-generator.ts +6 -6
- package/src/core/scaffold/ui-generator.ts +19 -19
- package/src/core/verify/index.ts +392 -0
- package/src/machine/contracts.ts +16 -0
- package/src/machine/errors.ts +12 -1
- package/src/machine/index.ts +444 -15
- package/src/mcp/index.ts +198 -2
- 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
|
-
.
|
|
151
|
-
|
|
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)) {
|
package/src/core/config.ts
CHANGED
|
@@ -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 (!
|
|
96
|
+
if (!session.fileExists(indexPath)) {
|
|
94
97
|
return false;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
const content =
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
if (session.mode === "commit") {
|
|
176
|
+
recordSessionOperations(session.operations, {});
|
|
177
|
+
await syncProjectManifest();
|
|
178
|
+
}
|
|
167
179
|
|
|
168
180
|
return {
|
|
169
181
|
createdFiles,
|