swallowkit 2.0.0-alpha.2 → 2.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +43 -122
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +45 -16
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +40 -2
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +27 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +19 -1
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +62 -35
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +49 -18
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +1 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +20 -2
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +45 -24
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +9 -1
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.js +8 -8
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +11 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +279 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +24 -1
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +39 -15
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +137 -13
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +44 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +164 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/package-manager.js +1 -1
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +29 -0
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +199 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +57 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +49 -3
- package/src/__tests__/package-manager.test.ts +2 -2
- package/src/__tests__/plan-apply.test.ts +68 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +48 -125
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +40 -2
- package/src/cli/index.ts +26 -3
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-operations.ts +86 -42
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/agent-templates/AGENTS.md +39 -3
- package/src/core/project/agent-templates/CLAUDE.md +3 -1
- package/src/core/project/agent-templates/copilot-instructions.md +3 -1
- package/src/core/project/boundaries.ts +60 -18
- package/src/core/project/capabilities.ts +22 -2
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +49 -27
- package/src/core/project/workflows.ts +9 -1
- package/src/core/scaffold/auth-generator.ts +8 -8
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +305 -105
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +23 -1
- package/src/machine/index.ts +42 -18
- package/src/mcp/index.ts +156 -14
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- package/src/utils/package-manager.ts +1 -1
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -62,9 +62,35 @@ interface ScaffoldOptions {
|
|
|
62
62
|
functionsDir?: string; // Azure Functions のディレクトリ(デフォルト: "functions")
|
|
63
63
|
apiDir?: string; // BFF directory (Next.js: app/api, React: bff)
|
|
64
64
|
apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
|
|
65
|
+
skipInfra?: boolean; // true の場合、infra/ 配下(コンテナー migration)を変更しない
|
|
65
66
|
expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
export function buildInfraSkippedWarning(modelName: string, modelArgument: string): string {
|
|
70
|
+
return `infra-skipped: The Cosmos DB container migration for ${modelName} was not generated (--skip-infra); infra/ was left unchanged. ` +
|
|
71
|
+
`Generate it later with "swallowkit migrations add-model ${modelArgument}", or re-plan scaffold without --skip-infra.`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const USER_SCOPE_FIELD = /^(user|owner|learner|member|account|customer|student|author|creator|profile|person|employee|patient)Id$/i;
|
|
75
|
+
|
|
76
|
+
function isAuthenticationConfigured(authConfig: AuthConfig | undefined): boolean {
|
|
77
|
+
if (!authConfig || authConfig.provider === "none") return false;
|
|
78
|
+
return Boolean(authConfig.provider) || Object.keys(authConfig.schemes ?? {}).length > 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Warn when a model looks per-user (partition key or owner-like id field) but no authentication is configured. */
|
|
82
|
+
export function buildUserScopedDataWarning(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): string | undefined {
|
|
83
|
+
if (isAuthenticationConfigured(authConfig)) return undefined;
|
|
84
|
+
const partitionField = modelInfo.partitionKey.replace(/^\//, "");
|
|
85
|
+
const fieldNames = modelInfo.fields.filter((field) => field.type === "string" && !field.isArray).map((field) => field.name);
|
|
86
|
+
const ownerField = [partitionField, ...fieldNames].find((name) => USER_SCOPE_FIELD.test(name) && fieldNames.includes(name));
|
|
87
|
+
if (!ownerField) return undefined;
|
|
88
|
+
const reason = ownerField === partitionField ? `is the partition key and looks like a per-user owner id` : `looks like a per-user owner id`;
|
|
89
|
+
return `user-scoped-data-without-auth: ${modelInfo.name}.${ownerField} ${reason}, but authentication is not configured. ` +
|
|
90
|
+
`Generated CRUD does not scope data to a principal, so any caller can read and write every ${modelInfo.name} document. ` +
|
|
91
|
+
`Add authentication with "plan auth", restrict or remove the generated write endpoints, and add ai-authored endpoints that derive ${ownerField} from the verified principal instead of the request body.`;
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
function getMachineAwareStdio(): "inherit" | "pipe" {
|
|
69
95
|
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
70
96
|
}
|
|
@@ -132,6 +158,12 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
132
158
|
session.setSchemaAnalysis(schemaAnalysis);
|
|
133
159
|
console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
|
|
134
160
|
|
|
161
|
+
const userScopeWarning = buildUserScopedDataWarning(modelInfo, getAuthConfig());
|
|
162
|
+
if (userScopeWarning) {
|
|
163
|
+
session.addWarning(userScopeWarning);
|
|
164
|
+
console.warn(`⚠️ ${userScopeWarning}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
135
167
|
const backendLanguage = getBackendLanguage();
|
|
136
168
|
const frontend = getFrontendFramework();
|
|
137
169
|
const apiDir = options.apiDir || (frontend === "react" ? "bff" : "app/api");
|
|
@@ -210,9 +242,15 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
210
242
|
await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
|
|
211
243
|
}
|
|
212
244
|
|
|
213
|
-
// 8. Generate Cosmos DB container Bicep file (skip for connector models)
|
|
245
|
+
// 8. Generate Cosmos DB container Bicep file (skip for connector models or when infra changes are excluded)
|
|
214
246
|
if (!isConnectorModel) {
|
|
215
|
-
|
|
247
|
+
if (options.skipInfra) {
|
|
248
|
+
const warning = buildInfraSkippedWarning(modelInfo.name, toKebabCase(modelInfo.name));
|
|
249
|
+
session.addWarning(warning);
|
|
250
|
+
console.warn(`\n⚠️ ${warning}`);
|
|
251
|
+
} else {
|
|
252
|
+
await generateCosmosContainer(modelInfo, session);
|
|
253
|
+
}
|
|
216
254
|
}
|
|
217
255
|
|
|
218
256
|
// 9. Generate UI components (unless --api-only)
|
package/src/cli/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelC
|
|
|
8
8
|
import { provisionCommand } from "./commands/provision";
|
|
9
9
|
import { createMigrationCommand } from "./commands/create-migration";
|
|
10
10
|
import { migrationsCommand } from "./commands/migrations";
|
|
11
|
+
import { configureMcpCommand } from "./commands/configure-mcp";
|
|
11
12
|
import { addConnectorCommand } from "./commands/add-connector";
|
|
12
13
|
import { addAuthCommand } from "./commands/add-auth";
|
|
13
14
|
import { planScaffoldOperation } from "../core/operations/scaffold-plan";
|
|
@@ -15,6 +16,7 @@ import { inspectArtifacts } from "../core/project/artifacts";
|
|
|
15
16
|
import { detectDrift } from "../core/project/drift";
|
|
16
17
|
import { runVerify } from "../core/verify";
|
|
17
18
|
import { isMachineCommand, runMachineCli } from "../machine";
|
|
19
|
+
import { MachineCommandError } from "../machine/errors";
|
|
18
20
|
|
|
19
21
|
const packageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
20
22
|
export const CLI_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf8")).version as string;
|
|
@@ -140,6 +142,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
140
142
|
program.addCommand(provisionCommand);
|
|
141
143
|
program.addCommand(migrationsCommand);
|
|
142
144
|
program.addCommand(createMigrationCommand);
|
|
145
|
+
program.addCommand(configureMcpCommand);
|
|
143
146
|
|
|
144
147
|
program
|
|
145
148
|
.command("create-model <names...>")
|
|
@@ -160,6 +163,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
160
163
|
.option("--functions-dir <dir>", "Azure Functions directory", "functions")
|
|
161
164
|
.option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
|
|
162
165
|
.option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
|
|
166
|
+
.option("--skip-infra", "Do not create or update anything under infra/ (generate the container migration later with migrations add-model)", false)
|
|
163
167
|
.option("--dry-run", "Show planned file changes without writing anything", false)
|
|
164
168
|
.action(async (model, options) => {
|
|
165
169
|
if (options.dryRun) {
|
|
@@ -168,6 +172,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
168
172
|
functionsDir: options.functionsDir,
|
|
169
173
|
apiDir: options.apiDir,
|
|
170
174
|
apiOnly: options.apiOnly,
|
|
175
|
+
skipInfra: options.skipInfra,
|
|
171
176
|
});
|
|
172
177
|
return;
|
|
173
178
|
}
|
|
@@ -177,6 +182,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
177
182
|
functionsDir: options.functionsDir,
|
|
178
183
|
apiDir: options.apiDir,
|
|
179
184
|
apiOnly: options.apiOnly,
|
|
185
|
+
skipInfra: options.skipInfra,
|
|
180
186
|
});
|
|
181
187
|
});
|
|
182
188
|
|
|
@@ -226,7 +232,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
226
232
|
return program;
|
|
227
233
|
}
|
|
228
234
|
|
|
229
|
-
async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }): Promise<void> {
|
|
235
|
+
async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }): Promise<void> {
|
|
230
236
|
console.log("🔍 Scaffold dry-run — no files will be written.\n");
|
|
231
237
|
|
|
232
238
|
const plan = await planScaffoldOperation(options);
|
|
@@ -264,7 +270,7 @@ async function runScaffoldDryRun(options: { model: string; functionsDir?: string
|
|
|
264
270
|
console.log(`⚠️ ${plan.conflicts.length} file(s) were modified after generation and would be overwritten.`);
|
|
265
271
|
console.log(` Review the conflicts, then run: swallowkit scaffold ${options.model} (or machine apply scaffold --plan ${plan.planId} --approve)`);
|
|
266
272
|
} else {
|
|
267
|
-
console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}`);
|
|
273
|
+
console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}${options.skipInfra ? " --skip-infra" : ""}`);
|
|
268
274
|
}
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -351,9 +357,26 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
351
357
|
await createProgram().parseAsync(normalizeDevCommandArgv(argv));
|
|
352
358
|
}
|
|
353
359
|
|
|
360
|
+
/** Human-readable error output for regular CLI commands (machine commands print JSON). */
|
|
361
|
+
export function formatCliError(error: unknown): string {
|
|
362
|
+
if (error instanceof MachineCommandError) {
|
|
363
|
+
const lines = [`❌ ${error.message}`, ` (code: ${error.code})`];
|
|
364
|
+
const details = error.details as Record<string, unknown> | undefined;
|
|
365
|
+
if (details && typeof details === "object") {
|
|
366
|
+
if (typeof details.nextAction === "string") lines.push(` Next: ${details.nextAction}`);
|
|
367
|
+
if (Array.isArray(details.changedFiles)) lines.push(` Changed files: ${details.changedFiles.join(", ")}`);
|
|
368
|
+
}
|
|
369
|
+
return lines.join("\n");
|
|
370
|
+
}
|
|
371
|
+
if (error instanceof Error) {
|
|
372
|
+
return process.env.SWALLOWKIT_DEBUG ? String(error.stack ?? error.message) : `❌ ${error.message}`;
|
|
373
|
+
}
|
|
374
|
+
return `❌ ${String(error)}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
354
377
|
if (require.main === module) {
|
|
355
378
|
void runCli().catch((error) => {
|
|
356
|
-
console.error(error);
|
|
379
|
+
console.error(formatCliError(error));
|
|
357
380
|
process.exitCode = 1;
|
|
358
381
|
});
|
|
359
382
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate only the Cosmos DB container migration for a model. Used after
|
|
3
|
+
* `scaffold --skip-infra`, when infrastructure changes become allowed.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resolveModelPath } from "../../cli/commands/scaffold";
|
|
7
|
+
import { MachineCommandError } from "../../machine/errors";
|
|
8
|
+
import { parseModelFile } from "../scaffold/model-parser";
|
|
9
|
+
import { recordSessionOperations } from "../project/artifacts";
|
|
10
|
+
import { FileOperationSession } from "./file-session";
|
|
11
|
+
import { generateModelContainerMigration } from "./migration-generator";
|
|
12
|
+
|
|
13
|
+
export interface ModelMigrationResult {
|
|
14
|
+
model: string;
|
|
15
|
+
dryRun: boolean;
|
|
16
|
+
migration: { version: number; template: string } | null;
|
|
17
|
+
reason?: string;
|
|
18
|
+
operations: Array<{ path: string; action: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function addModelContainerMigration(
|
|
22
|
+
modelInput: string,
|
|
23
|
+
options: { dryRun?: boolean; projectRoot?: string } = {}
|
|
24
|
+
): Promise<ModelMigrationResult> {
|
|
25
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
26
|
+
const modelInfo = await parseModelFile(resolveModelPath(modelInput));
|
|
27
|
+
if (modelInfo.connectorConfig) {
|
|
28
|
+
throw new MachineCommandError(
|
|
29
|
+
"connector-model-has-no-container",
|
|
30
|
+
`${modelInfo.name} uses connector ${modelInfo.connectorConfig.connector}; connector models do not get a Cosmos DB container.`,
|
|
31
|
+
undefined,
|
|
32
|
+
"failed"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const session = new FileOperationSession(options.dryRun ? "collect" : "commit", projectRoot);
|
|
37
|
+
const migration = generateModelContainerMigration(modelInfo, session);
|
|
38
|
+
if (!options.dryRun) {
|
|
39
|
+
recordSessionOperations(session.operations, {
|
|
40
|
+
schemaHash: modelInfo.semanticFingerprint,
|
|
41
|
+
sourceModel: modelInfo.name,
|
|
42
|
+
}, projectRoot);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
model: modelInfo.name,
|
|
47
|
+
dryRun: Boolean(options.dryRun),
|
|
48
|
+
migration: migration ? { version: migration.version, template: migration.template } : null,
|
|
49
|
+
...(migration ? {} : { reason: "The container is already owned by the legacy migration baseline." }),
|
|
50
|
+
operations: session.operations.map((operation) => ({ path: operation.path, action: operation.action })),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* ネットワークアクセスや az login を必要とする操作は行わない。
|
|
6
6
|
* --what-if を明示した場合のみ az deployment group what-if を実行する。
|
|
7
7
|
* - apply provision: 常に承認(--approve)必須。承認なしでは requires-human で停止する。
|
|
8
|
+
* - 対話的な `swallowkit provision` は同じ計画をメモリ上で作成・表示し、確認後に
|
|
9
|
+
* executeProvisionPlan で同じ安全確認を経て実行する(計画ファイルは保存しない)。
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import * as crypto from "crypto";
|
|
11
13
|
import * as fs from "fs";
|
|
14
|
+
import { AzCliAvailability, checkAzCliAvailability, runAzCli } from "../../utils/azure-cli";
|
|
12
15
|
import * as path from "path";
|
|
13
|
-
import { spawnSync } from "child_process";
|
|
14
16
|
import { MachineCommandError } from "../../machine/errors";
|
|
15
17
|
import { getSwallowKitVersion } from "../../version";
|
|
16
18
|
import { inspectInfra, InfraInspection } from "../project/infra";
|
|
@@ -30,7 +32,6 @@ import {
|
|
|
30
32
|
getMigrationTagKey,
|
|
31
33
|
loadMigrationManifest,
|
|
32
34
|
replaceMigrationManifest,
|
|
33
|
-
resolveMigrations,
|
|
34
35
|
selectPendingMigrations,
|
|
35
36
|
} from "../project/migrations";
|
|
36
37
|
|
|
@@ -113,13 +114,10 @@ function validateProvisionInputs(options: PlanProvisionOptions): void {
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
} catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
117
|
+
function azCliUnavailableWarning(availability: Exclude<AzCliAvailability, { available: true }>): string {
|
|
118
|
+
return availability.reason === "not-found"
|
|
119
|
+
? "az-cli-not-found: Azure CLI was not found on PATH. Install it (https://aka.ms/azure-cli) or add it to PATH, then try again."
|
|
120
|
+
: `az-cli-not-runnable: Azure CLI was found but could not be started. ${availability.detail}`;
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
function displayCommand(command: ProvisionCommand): string {
|
|
@@ -133,14 +131,27 @@ interface AzResult {
|
|
|
133
131
|
}
|
|
134
132
|
|
|
135
133
|
function runAz(args: string[], timeout = 300_000): AzResult {
|
|
136
|
-
const result =
|
|
134
|
+
const result = runAzCli(args, { timeout });
|
|
137
135
|
return {
|
|
138
136
|
status: result.status,
|
|
139
|
-
stdout: result.stdout
|
|
140
|
-
stderr: result.stderr
|
|
137
|
+
stdout: result.stdout,
|
|
138
|
+
stderr: result.stderr || (result.error ? result.error.message : ""),
|
|
141
139
|
};
|
|
142
140
|
}
|
|
143
141
|
|
|
142
|
+
function azQueryError(code: string, message: string, result: AzResult): MachineCommandError {
|
|
143
|
+
const output = (result.stderr || result.stdout || "").trim();
|
|
144
|
+
if (/az login|not logged in|AADSTS|expired/i.test(output)) {
|
|
145
|
+
return new MachineCommandError(
|
|
146
|
+
"az-not-logged-in",
|
|
147
|
+
`Azure CLI is not signed in (or the session expired). Run "az login", then try again.\n${output.slice(0, 1000)}`,
|
|
148
|
+
undefined,
|
|
149
|
+
"requires-human"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return new MachineCommandError(code, `${message}: ${output}`, undefined, "blocked");
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
function subscriptionArgs(subscription?: string): string[] {
|
|
145
156
|
return subscription ? ["--subscription", subscription] : [];
|
|
146
157
|
}
|
|
@@ -176,12 +187,12 @@ interface RemoteGroupState {
|
|
|
176
187
|
function inspectRemoteGroup(options: PlanProvisionOptions): RemoteGroupState {
|
|
177
188
|
const existsResult = runAz(["group", "exists", "--name", options.resourceGroup, ...subscriptionArgs(options.subscription)], 60_000);
|
|
178
189
|
if (existsResult.status !== 0) {
|
|
179
|
-
throw
|
|
190
|
+
throw azQueryError("azure-state-query-failed", "Could not query resource group", existsResult);
|
|
180
191
|
}
|
|
181
192
|
if ((existsResult.stdout || "").trim() !== "true") return { exists: false, tags: {} };
|
|
182
193
|
const showResult = runAz(["group", "show", "--name", options.resourceGroup, ...subscriptionArgs(options.subscription), "--query", "{id:id,tags:tags}", "-o", "json"], 60_000);
|
|
183
194
|
if (showResult.status !== 0) {
|
|
184
|
-
throw
|
|
195
|
+
throw azQueryError("azure-state-query-failed", "Could not read resource group tags", showResult);
|
|
185
196
|
}
|
|
186
197
|
const parsed = JSON.parse(showResult.stdout || "{}");
|
|
187
198
|
return { exists: true, id: parsed.id, tags: parsed.tags ?? {} };
|
|
@@ -231,7 +242,14 @@ function assertAdoptionWhatIfIsSafe(whatIfResult: string): void {
|
|
|
231
242
|
}
|
|
232
243
|
}
|
|
233
244
|
|
|
234
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Builds the provision plan. Machine/MCP callers persist it for a later `apply provision --plan`;
|
|
247
|
+
* the interactive `swallowkit provision` command keeps it in memory and executes it after confirmation.
|
|
248
|
+
*/
|
|
249
|
+
export async function planProvisionOperation(
|
|
250
|
+
options: PlanProvisionOptions,
|
|
251
|
+
planOptions: { persist?: boolean } = {}
|
|
252
|
+
): Promise<ProvisionPlanData> {
|
|
235
253
|
const projectRoot = process.cwd();
|
|
236
254
|
validateProvisionInputs(options);
|
|
237
255
|
|
|
@@ -250,9 +268,10 @@ export async function planProvisionOperation(options: PlanProvisionOptions): Pro
|
|
|
250
268
|
warnings.push("infra/main.parameters.json not found; deployment will fail until it is created.");
|
|
251
269
|
}
|
|
252
270
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
271
|
+
const azCli = checkAzCliAvailability();
|
|
272
|
+
const azCliAvailable = azCli.available;
|
|
273
|
+
if (!azCli.available) {
|
|
274
|
+
warnings.push(azCliUnavailableWarning(azCli));
|
|
256
275
|
}
|
|
257
276
|
|
|
258
277
|
const mainBicepPath = path.join(projectRoot, "infra", "main.bicep");
|
|
@@ -396,7 +415,13 @@ export async function planProvisionOperation(options: PlanProvisionOptions): Pro
|
|
|
396
415
|
}
|
|
397
416
|
}
|
|
398
417
|
} else if (options.whatIf || options.adoptExisting || options.reconcile) {
|
|
399
|
-
|
|
418
|
+
const detail = azCli.available ? "" : ` ${azCliUnavailableWarning(azCli)}`;
|
|
419
|
+
throw new MachineCommandError(
|
|
420
|
+
!azCli.available && azCli.reason === "not-runnable" ? "az-cli-not-runnable" : "az-cli-not-found",
|
|
421
|
+
`Azure CLI is required for --what-if, --adopt-existing, and --reconcile.${detail}`,
|
|
422
|
+
undefined,
|
|
423
|
+
"blocked"
|
|
424
|
+
);
|
|
400
425
|
}
|
|
401
426
|
|
|
402
427
|
const fingerprints: Record<string, string | null> = {
|
|
@@ -460,7 +485,7 @@ export async function planProvisionOperation(options: PlanProvisionOptions): Pro
|
|
|
460
485
|
...(baselineSnapshotChecksum ? { baselineSnapshotChecksum } : {}),
|
|
461
486
|
};
|
|
462
487
|
|
|
463
|
-
savePlanState(plan, projectRoot);
|
|
488
|
+
if (planOptions.persist !== false) savePlanState(plan, projectRoot);
|
|
464
489
|
return plan;
|
|
465
490
|
}
|
|
466
491
|
|
|
@@ -490,37 +515,40 @@ function readRemoteMigrationState(plan: ProvisionPlanData): { group: RemoteGroup
|
|
|
490
515
|
return { group, state: tagValue ? decodeMigrationState(tagValue) : null };
|
|
491
516
|
}
|
|
492
517
|
|
|
493
|
-
function assertPlanRemoteState(plan: ProvisionPlanData, expected: MigrationState | null | undefined): void {
|
|
518
|
+
function assertPlanRemoteState(plan: ProvisionPlanData, expected: MigrationState | null | undefined, retryHint: string): void {
|
|
494
519
|
if (plan.mode === "offline") return;
|
|
495
520
|
const current = readRemoteMigrationState(plan);
|
|
496
521
|
if (plan.mode === "bootstrap") {
|
|
497
522
|
if (current.group.exists) {
|
|
498
|
-
throw new MachineCommandError("stale-azure-state",
|
|
523
|
+
throw new MachineCommandError("stale-azure-state", `The resource group was created after the preview was computed. ${retryHint}`, undefined, "blocked");
|
|
499
524
|
}
|
|
500
525
|
return;
|
|
501
526
|
}
|
|
502
527
|
if (!current.group.exists || !statesEqual(current.state, expected)) {
|
|
503
528
|
throw new MachineCommandError(
|
|
504
529
|
"stale-azure-state",
|
|
505
|
-
|
|
530
|
+
`Azure migration state changed after the preview was computed. ${retryHint}`,
|
|
506
531
|
{ expected, actual: current.state, resourceGroupExists: current.group.exists },
|
|
507
532
|
"blocked"
|
|
508
533
|
);
|
|
509
534
|
}
|
|
510
535
|
}
|
|
511
536
|
|
|
512
|
-
function assertTagExpectedState(plan: ProvisionPlanData, expected: MigrationState | null | undefined): void {
|
|
537
|
+
function assertTagExpectedState(plan: ProvisionPlanData, expected: MigrationState | null | undefined, retryHint: string): void {
|
|
513
538
|
const current = readRemoteMigrationState(plan);
|
|
514
539
|
if (!current.group.exists || !statesEqual(current.state, expected)) {
|
|
515
540
|
throw new MachineCommandError(
|
|
516
541
|
"stale-azure-state",
|
|
517
|
-
|
|
542
|
+
`Azure migration state changed before its tag could be updated. ${retryHint}`,
|
|
518
543
|
{ expected, actual: current.state, resourceGroupExists: current.group.exists },
|
|
519
544
|
"blocked"
|
|
520
545
|
);
|
|
521
546
|
}
|
|
522
547
|
}
|
|
523
548
|
|
|
549
|
+
const MACHINE_RETRY_HINT = "Re-run plan provision.";
|
|
550
|
+
export const INTERACTIVE_PROVISION_RETRY_HINT = 'Run "swallowkit provision" again to review the current state.';
|
|
551
|
+
|
|
524
552
|
export async function applyProvisionOperation(options: ApplyProvisionOptions): Promise<ApplyProvisionResult> {
|
|
525
553
|
const projectRoot = process.cwd();
|
|
526
554
|
|
|
@@ -534,6 +562,22 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
|
|
|
534
562
|
);
|
|
535
563
|
}
|
|
536
564
|
|
|
565
|
+
const result = await executeProvisionPlan(plan, { approve: options.approve, retryHint: MACHINE_RETRY_HINT, projectRoot });
|
|
566
|
+
deletePlanState(plan.planId, projectRoot);
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Runs an approved provision plan with every safety check: local files and Azure state must still
|
|
572
|
+
* match the reviewed preview, and each migration/tag step re-checks Azure state before it runs.
|
|
573
|
+
*/
|
|
574
|
+
export async function executeProvisionPlan(
|
|
575
|
+
plan: ProvisionPlanData,
|
|
576
|
+
options: { approve?: boolean; retryHint?: string; projectRoot?: string } = {}
|
|
577
|
+
): Promise<ApplyProvisionResult> {
|
|
578
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
579
|
+
const retryHint = options.retryHint ?? MACHINE_RETRY_HINT;
|
|
580
|
+
|
|
537
581
|
// Bicep が plan 後に変更されていないか確認
|
|
538
582
|
const changedFiles: string[] = [];
|
|
539
583
|
for (const [relativePath, expectedHash] of Object.entries(plan.fingerprints)) {
|
|
@@ -543,8 +587,8 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
|
|
|
543
587
|
if (changedFiles.length > 0) {
|
|
544
588
|
throw new MachineCommandError(
|
|
545
589
|
"stale-plan",
|
|
546
|
-
|
|
547
|
-
{ planId:
|
|
590
|
+
`${changedFiles.join(", ")} changed after the preview was computed. ${retryHint}`,
|
|
591
|
+
{ planId: plan.planId, changedFiles },
|
|
548
592
|
"blocked"
|
|
549
593
|
);
|
|
550
594
|
}
|
|
@@ -553,8 +597,8 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
|
|
|
553
597
|
if (currentBaselineChecksum !== plan.baselineSnapshotChecksum) {
|
|
554
598
|
throw new MachineCommandError(
|
|
555
599
|
"stale-plan",
|
|
556
|
-
`
|
|
557
|
-
{ planId:
|
|
600
|
+
`Baseline infrastructure files changed after the preview was computed. ${retryHint}`,
|
|
601
|
+
{ planId: plan.planId },
|
|
558
602
|
"blocked"
|
|
559
603
|
);
|
|
560
604
|
}
|
|
@@ -569,10 +613,15 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
|
|
|
569
613
|
);
|
|
570
614
|
}
|
|
571
615
|
if (!plan.mode || !plan.commandSpecs) {
|
|
572
|
-
throw new MachineCommandError("incompatible-plan",
|
|
616
|
+
throw new MachineCommandError("incompatible-plan", `This provision plan was created by an older SwallowKit version. ${retryHint}`, undefined, "blocked");
|
|
573
617
|
}
|
|
574
618
|
if (plan.mode === "offline") {
|
|
575
|
-
throw new MachineCommandError(
|
|
619
|
+
throw new MachineCommandError(
|
|
620
|
+
"online-plan-required",
|
|
621
|
+
`Azure state was not read because Azure CLI was unavailable, so nothing can be applied. Make az available and signed in (az login). ${retryHint}`,
|
|
622
|
+
{ planId: plan.planId, warnings: plan.warnings },
|
|
623
|
+
"blocked"
|
|
624
|
+
);
|
|
576
625
|
}
|
|
577
626
|
|
|
578
627
|
if (plan.mode === "adoption-required") {
|
|
@@ -584,27 +633,23 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
|
|
|
584
633
|
);
|
|
585
634
|
}
|
|
586
635
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
"Azure CLI is not installed. Install it first: https://aka.ms/azure-cli",
|
|
591
|
-
undefined,
|
|
592
|
-
"blocked"
|
|
593
|
-
);
|
|
636
|
+
const azCli = checkAzCliAvailability();
|
|
637
|
+
if (!azCli.available) {
|
|
638
|
+
throw new MachineCommandError(azCli.reason === "not-found" ? "az-cli-not-found" : "az-cli-not-runnable", azCliUnavailableWarning(azCli), undefined, "blocked");
|
|
594
639
|
}
|
|
595
640
|
|
|
596
641
|
const executedCommands: string[] = [];
|
|
597
642
|
const warnings = [...plan.warnings];
|
|
598
643
|
let deploymentOutputs: Record<string, unknown> | undefined;
|
|
599
644
|
|
|
600
|
-
assertPlanRemoteState(plan, plan.remoteState);
|
|
645
|
+
assertPlanRemoteState(plan, plan.remoteState, retryHint);
|
|
601
646
|
|
|
602
647
|
for (const command of plan.commandSpecs) {
|
|
603
648
|
if (command.kind === "migration") {
|
|
604
|
-
assertPlanRemoteState(plan, command.expectedState);
|
|
649
|
+
assertPlanRemoteState(plan, command.expectedState, retryHint);
|
|
605
650
|
}
|
|
606
651
|
if (command.kind === "tag") {
|
|
607
|
-
assertTagExpectedState(plan, command.expectedState);
|
|
652
|
+
assertTagExpectedState(plan, command.expectedState, retryHint);
|
|
608
653
|
}
|
|
609
654
|
const result = runAz(command.args);
|
|
610
655
|
const commandText = displayCommand(command);
|
|
@@ -630,7 +675,6 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
|
|
|
630
675
|
if (plan.manifestAfterApply) {
|
|
631
676
|
replaceMigrationManifest(plan.manifestAfterApply, projectRoot);
|
|
632
677
|
}
|
|
633
|
-
deletePlanState(plan.planId, projectRoot);
|
|
634
678
|
|
|
635
679
|
return {
|
|
636
680
|
planId: plan.planId,
|
|
@@ -19,6 +19,7 @@ export interface ApplyScaffoldOptions {
|
|
|
19
19
|
functionsDir?: string;
|
|
20
20
|
apiDir?: string;
|
|
21
21
|
apiOnly?: boolean;
|
|
22
|
+
skipInfra?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface ApplyScaffoldResult extends MachineScaffoldOperationResult {
|
|
@@ -70,6 +71,7 @@ export async function applyScaffoldOperation(options: ApplyScaffoldOptions): Pro
|
|
|
70
71
|
functionsDir: options.functionsDir ?? basePlan?.options.functionsDir,
|
|
71
72
|
apiDir: options.apiDir ?? basePlan?.options.apiDir,
|
|
72
73
|
apiOnly: options.apiOnly ?? basePlan?.options.apiOnly,
|
|
74
|
+
skipInfra: options.skipInfra ?? basePlan?.options.skipInfra,
|
|
73
75
|
};
|
|
74
76
|
|
|
75
77
|
// 常に最新状態で collect し、競合を検出する(plan 指定時も現況を再確認)
|
|
@@ -29,6 +29,7 @@ export interface PlanScaffoldOptions {
|
|
|
29
29
|
functionsDir?: string;
|
|
30
30
|
apiDir?: string;
|
|
31
31
|
apiOnly?: boolean;
|
|
32
|
+
skipInfra?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export type ScaffoldPlanAction = "create" | "update" | "overwrite" | "append" | "delete" | "skip";
|
|
@@ -59,6 +60,7 @@ export interface ScaffoldPlanData {
|
|
|
59
60
|
functionsDir?: string;
|
|
60
61
|
apiDir?: string;
|
|
61
62
|
apiOnly?: boolean;
|
|
63
|
+
skipInfra?: boolean;
|
|
62
64
|
};
|
|
63
65
|
createdAt: string;
|
|
64
66
|
swallowkitVersion: string;
|
|
@@ -93,6 +95,7 @@ async function collectScaffoldOperations(options: PlanScaffoldOptions): Promise<
|
|
|
93
95
|
functionsDir: options.functionsDir,
|
|
94
96
|
apiDir: options.apiDir,
|
|
95
97
|
apiOnly: options.apiOnly,
|
|
98
|
+
skipInfra: options.skipInfra,
|
|
96
99
|
});
|
|
97
100
|
});
|
|
98
101
|
});
|
|
@@ -193,7 +196,7 @@ export async function planScaffoldOperation(options: PlanScaffoldOptions): Promi
|
|
|
193
196
|
.update(
|
|
194
197
|
JSON.stringify({
|
|
195
198
|
model: options.model,
|
|
196
|
-
options: { functionsDir: options.functionsDir, apiDir: options.apiDir, apiOnly: options.apiOnly },
|
|
199
|
+
options: { functionsDir: options.functionsDir, apiDir: options.apiDir, apiOnly: options.apiOnly, skipInfra: options.skipInfra },
|
|
197
200
|
operations: operations.map((op) => ({ path: op.path, action: op.action })),
|
|
198
201
|
fingerprints,
|
|
199
202
|
schemaAnalysis,
|
|
@@ -210,6 +213,7 @@ export async function planScaffoldOperation(options: PlanScaffoldOptions): Promi
|
|
|
210
213
|
...(options.functionsDir ? { functionsDir: options.functionsDir } : {}),
|
|
211
214
|
...(options.apiDir ? { apiDir: options.apiDir } : {}),
|
|
212
215
|
...(options.apiOnly !== undefined ? { apiOnly: options.apiOnly } : {}),
|
|
216
|
+
...(options.skipInfra ? { skipInfra: true } : {}),
|
|
213
217
|
},
|
|
214
218
|
createdAt: new Date().toISOString(),
|
|
215
219
|
swallowkitVersion: getSwallowKitVersion(),
|
|
@@ -71,6 +71,9 @@ function getBackendRules(backendLanguage: BackendLanguage): string {
|
|
|
71
71
|
"- All backend business logic and data access live in `functions/`.",
|
|
72
72
|
`- SwallowKit derives OpenAPI and native ${BACKEND_LANGUAGE_LABELS[backendLanguage]} schema assets from the Zod source of truth.`,
|
|
73
73
|
"- Keep custom business logic in ai-authored locations identified by the current boundary contract.",
|
|
74
|
+
backendLanguage === "python"
|
|
75
|
+
? "- Use uv with the user's interpreter/version settings. Local Python is not pinned to the Azure deployment runtime. Preserve functions/.venv and functions/.codegen-venv; do not clear them or weaken OS application-control policy to fix interpreter errors."
|
|
76
|
+
: "- Start the isolated Functions backend with dotnet run, not func start from the source directory, so the SDK loads extensions from the build output.",
|
|
74
77
|
].join("\n");
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -91,7 +94,7 @@ function interpolateTemplate(template: string, context: AgentInstructionContext)
|
|
|
91
94
|
? "├── src/ # React SPA (Vite + React Router)\n│ └── pages/ # generated/custom UI and route registrations\n├── bff/ # SWA managed HTTP-only Functions\n│ ├── src/ # BFF handlers and request-local helpers\n│ └── {function}/ # function.json registrations"
|
|
92
95
|
: "├── app/ # Next.js App Router\n│ ├── api/ # BFF routes\n│ └── {model}/ # generated/custom UI",
|
|
93
96
|
devGuidance: react
|
|
94
|
-
? "Use http://localhost:4280 for the app. `swallowkit dev` runs Vite (3000), the managed BFF (7073), standalone Functions (7071), and SWA CLI (4280). SWA forwards /api only to the managed BFF, never directly to standalone Functions. `--no-functions` leaves the BFF running; `--no-swa` removes same-origin routing/auth emulation. BFF local settings belong in bff/local.settings.json. Build bundles BFF dependencies and copies staticwebapp.config.json into dist/. Deploy dist/ + bff/ together, and functions/ separately."
|
|
97
|
+
? "Use http://localhost:4280 for the app. `swallowkit dev` runs Vite (3000), the managed BFF (7073), standalone Functions (7071), and SWA CLI (4280). SWA forwards /api only to the managed BFF, never directly to standalone Functions. `--no-functions` leaves the BFF running; `--no-swa` removes same-origin routing/auth emulation. BFF local settings belong in bff/local.settings.json. Build bundles BFF dependencies and copies staticwebapp.config.json into dist/. Deploy dist/ + bff/ together, and functions/ separately. Generated home, CRUD, and login pages share the Next.js presentation templates. Preserve their Tailwind styles and the light/dark defaults in src/style.css when editing them."
|
|
95
98
|
: "Use http://localhost:3000 for the app, or the SWA emulator port (4280) when testing SWA authentication. `swallowkit dev` runs Next.js and standalone Functions (7071). The BFF remains Next.js API routes.",
|
|
96
99
|
pm: context.packageManager,
|
|
97
100
|
runCmd,
|
|
@@ -41,7 +41,14 @@ When a SwallowKit capability, project structure, or ownership boundary affects
|
|
|
41
41
|
a design decision, inspect the current project and the currently available
|
|
42
42
|
SwallowKit interfaces before deciding.
|
|
43
43
|
|
|
44
|
-
Prefer the `swallowkit_*` MCP tools when available
|
|
44
|
+
Prefer the `swallowkit_*` MCP tools when available, with one exception: every
|
|
45
|
+
MCP response reports the SwallowKit version that answered in
|
|
46
|
+
`metadata.swallowkitVersion` and `metadata.mcp`. If that version is older than
|
|
47
|
+
the project's installed SwallowKit (`{{runCmd}} swallowkit --version`), or
|
|
48
|
+
`metadata.warnings` contains `mcp-older-than-project`, its capabilities,
|
|
49
|
+
boundaries, and plan/apply contracts may be outdated for this project. Use
|
|
50
|
+
`{{runCmd}} swallowkit machine ...` instead and follow the response's
|
|
51
|
+
`nextActions` (for example `{{runCmd}} swallowkit configure-mcp`).
|
|
45
52
|
|
|
46
53
|
If MCP is unavailable, use the machine interface:
|
|
47
54
|
|
|
@@ -263,6 +270,18 @@ SwallowKit should generate framework-owned CRUD artifacts.
|
|
|
263
270
|
|
|
264
271
|
Do not manually recreate boilerplate that SwallowKit owns.
|
|
265
272
|
|
|
273
|
+
When the task must not change Bicep or other infrastructure, still scaffold the
|
|
274
|
+
model: pass `--skip-infra` to `plan scaffold` / `apply scaffold` (MCP
|
|
275
|
+
`skipInfra: true`, or `swallowkit scaffold --skip-infra`). Nothing under
|
|
276
|
+
`infra/` changes, the plan reports `infra-skipped`, and `verify project` still
|
|
277
|
+
passes. Generate only the pending container migration later with
|
|
278
|
+
`{{runCmd}} swallowkit migrations add-model <model>`.
|
|
279
|
+
|
|
280
|
+
Treat plan `warnings` as decisions to make, not noise. In particular,
|
|
281
|
+
`user-scoped-data-without-auth` means the model looks per-user (for example a
|
|
282
|
+
`/userId` partition key) while authentication is not configured, so generated
|
|
283
|
+
CRUD would let any caller read and write every document.
|
|
284
|
+
|
|
266
285
|
## Authentication
|
|
267
286
|
|
|
268
287
|
Authentication is a **plan/apply operation**, not a free-form config edit.
|
|
@@ -458,9 +477,19 @@ same generated structure.
|
|
|
458
477
|
- Re-export models from `shared/index.ts`.
|
|
459
478
|
- `id` is a required `z.string()`; only `createdAt` / `updatedAt` are optional
|
|
460
479
|
backend-managed fields.
|
|
461
|
-
- Models may declare `partitionKey`, `authPolicy`, `displayName`,
|
|
462
|
-
`connectorConfig` as exported constants. Exact formats:
|
|
480
|
+
- Models may declare `partitionKey`, `authPolicy`, `displayName`,
|
|
481
|
+
`references`, and `connectorConfig` as exported constants. Exact formats:
|
|
463
482
|
`{{runCmd}} swallowkit machine inspect capabilities` (modelDeclarations).
|
|
483
|
+
- A string field named `<model>Id` is treated as a foreign key only when that
|
|
484
|
+
model exists in `shared/models`. Declare other references explicitly with
|
|
485
|
+
`export const references = { authorId: 'User' };` (map a field to `false`
|
|
486
|
+
to opt out).
|
|
487
|
+
- Native C#/Python schema types come from Zod, not field names: use
|
|
488
|
+
`z.iso.datetime()` / `z.iso.date()` for date values (`createdAt` stays a
|
|
489
|
+
string), `z.number().int()` for integers, and inline `z.object` for nested
|
|
490
|
+
classes. Length/range/regex/item constraints are enforced natively; formats
|
|
491
|
+
such as email remain Zod (BFF) validation. See `inspect capabilities`
|
|
492
|
+
(sharedModels.nativeSchemaMapping).
|
|
464
493
|
- Cosmos DB containers use PascalCase plural names and default to `/id` as the
|
|
465
494
|
partition key unless the model declares another key, e.g.
|
|
466
495
|
`export const partitionKey = '/learnerId';` (the field must exist in the
|
|
@@ -517,3 +546,10 @@ Do:
|
|
|
517
546
|
- verify before final semantic review
|
|
518
547
|
|
|
519
548
|
Use `{{pm}}`, the package manager selected for this project. Do not introduce another package manager or lockfile. Use `{{runCmd}} swallowkit` for the project-local CLI. The MCP launcher intentionally resolves `latest` independently.
|
|
549
|
+
Exception for prerelease projects: when the project's SwallowKit is newer than
|
|
550
|
+
the published `latest` (for example `2.0.0-alpha.3` while `latest` is `1.1.2`),
|
|
551
|
+
`.mcp.json` may pin `SWALLOWKIT_MCP_VERSION` to the project version, and a
|
|
552
|
+
`latest` launcher that supports it hands the session to the project-installed
|
|
553
|
+
MCP. Run `{{runCmd}} swallowkit configure-mcp` to re-select the version (it
|
|
554
|
+
returns to `latest` once `latest` is at least the project version). Until MCP
|
|
555
|
+
reports the project version, prefer `{{runCmd}} swallowkit machine ...`.
|