swallowkit 1.1.2 → 2.0.0-alpha.10
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/README.ja.md +61 -7
- package/README.md +61 -7
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +103 -27
- package/dist/cli/commands/add-auth.js.map +1 -1
- 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 +5 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +143 -67
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +14 -7
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +149 -170
- 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 +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +5 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +123 -18
- 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 +49 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.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-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- 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 +2 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +15 -0
- 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 +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +17 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +45 -6
- 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/manifest.d.ts +3 -1
- package/dist/core/project/manifest.d.ts.map +1 -1
- package/dist/core/project/manifest.js +47 -25
- package/dist/core/project/manifest.js.map +1 -1
- 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 +26 -0
- package/dist/core/project/react-project.d.ts.map +1 -0
- package/dist/core/project/react-project.js +443 -0
- package/dist/core/project/react-project.js.map +1 -0
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +32 -10
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +53 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- 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 +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -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/react-bff-generator.d.ts +10 -0
- package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-bff-generator.js +252 -0
- package/dist/core/scaffold/react-bff-generator.js.map +1 -0
- package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
- package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-ui-generator.js +41 -0
- package/dist/core/scaffold/react-ui-generator.js.map +1 -0
- 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 +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +61 -30
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- 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/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/dist/utils/package-manager.d.ts +2 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +6 -3
- 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 +7 -4
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +6 -1
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -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__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +145 -5
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -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 +838 -0
- package/src/__tests__/release-metadata.test.ts +121 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +103 -29
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +145 -73
- package/src/cli/commands/init.ts +147 -172
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +145 -22
- package/src/cli/index.ts +46 -10
- package/src/core/config.ts +21 -1
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- 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 +17 -1
- package/src/core/project/agent-templates/AGENTS.md +113 -15
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +63 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/manifest.ts +52 -27
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +529 -0
- package/src/core/project/state.ts +70 -0
- package/src/core/project/validation.ts +32 -10
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/react-bff-generator.ts +292 -0
- package/src/core/scaffold/react-ui-generator.ts +51 -0
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/index.ts +3 -1
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +67 -36
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/types/index.ts +4 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +7 -4
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SwallowKit Scaffold コマンド
|
|
3
|
-
* Zod モデルから Azure Functions
|
|
3
|
+
* Zod モデルから Azure Functions、BFF、選択したフロントエンドの CRUD を生成
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
-
import { getBackendLanguage, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
|
|
10
|
-
import {
|
|
9
|
+
import { getBackendLanguage, getFrontendFramework, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
|
|
10
|
+
import { generateManagedFunctionConfig, generateReactBffHelper, generateReactBffRoute } from "../../core/scaffold/react-bff-generator";
|
|
11
|
+
import { adaptReactPage, generateReactModelRoutes } from "../../core/scaffold/react-ui-generator";
|
|
12
|
+
import { ModelInfo, parseModelFile, toKebabCase, toCamelCase } from "../../core/scaffold/model-parser";
|
|
11
13
|
import { MachineCommandError } from "../../machine/errors";
|
|
12
14
|
import {
|
|
13
15
|
generateCSharpAzureFunctionsCRUD,
|
|
@@ -52,17 +54,44 @@ import {
|
|
|
52
54
|
} from "../../core/operations/file-session";
|
|
53
55
|
import { generateModelContainerMigration } from "../../core/operations/migration-generator";
|
|
54
56
|
import { recordSessionOperations } from "../../core/project/artifacts";
|
|
57
|
+
import { updateSharedIndex } from "../../core/operations/create-model";
|
|
55
58
|
|
|
56
59
|
const SCAFFOLD_GENERATOR = "scaffold";
|
|
57
60
|
|
|
58
61
|
interface ScaffoldOptions {
|
|
59
62
|
model: string; // モデルファイルのパス(例: "lib/models/todo.ts" or "todo")
|
|
60
63
|
functionsDir?: string; // Azure Functions のディレクトリ(デフォルト: "functions")
|
|
61
|
-
apiDir?: string; // Next.js
|
|
64
|
+
apiDir?: string; // BFF directory (Next.js: app/api, React: bff)
|
|
62
65
|
apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
|
|
66
|
+
skipInfra?: boolean; // true の場合、infra/ 配下(コンテナー migration)を変更しない
|
|
63
67
|
expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
export function buildInfraSkippedWarning(modelName: string, modelArgument: string): string {
|
|
71
|
+
return `infra-skipped: The Cosmos DB container migration for ${modelName} was not generated (--skip-infra); infra/ was left unchanged. ` +
|
|
72
|
+
`Generate it later with "swallowkit migrations add-model ${modelArgument}", or re-plan scaffold without --skip-infra.`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const USER_SCOPE_FIELD = /^(user|owner|learner|member|account|customer|student|author|creator|profile|person|employee|patient)Id$/i;
|
|
76
|
+
|
|
77
|
+
function isAuthenticationConfigured(authConfig: AuthConfig | undefined): boolean {
|
|
78
|
+
if (!authConfig || authConfig.provider === "none") return false;
|
|
79
|
+
return Boolean(authConfig.provider) || Object.keys(authConfig.schemes ?? {}).length > 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Warn when a model looks per-user (partition key or owner-like id field) but no authentication is configured. */
|
|
83
|
+
export function buildUserScopedDataWarning(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): string | undefined {
|
|
84
|
+
if (isAuthenticationConfigured(authConfig)) return undefined;
|
|
85
|
+
const partitionField = modelInfo.partitionKey.replace(/^\//, "");
|
|
86
|
+
const fieldNames = modelInfo.fields.filter((field) => field.type === "string" && !field.isArray).map((field) => field.name);
|
|
87
|
+
const ownerField = [partitionField, ...fieldNames].find((name) => USER_SCOPE_FIELD.test(name) && fieldNames.includes(name));
|
|
88
|
+
if (!ownerField) return undefined;
|
|
89
|
+
const reason = ownerField === partitionField ? `is the partition key and looks like a per-user owner id` : `looks like a per-user owner id`;
|
|
90
|
+
return `user-scoped-data-without-auth: ${modelInfo.name}.${ownerField} ${reason}, but authentication is not configured. ` +
|
|
91
|
+
`Generated CRUD does not scope data to a principal, so any caller can read and write every ${modelInfo.name} document. ` +
|
|
92
|
+
`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.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
function getMachineAwareStdio(): "inherit" | "pipe" {
|
|
67
96
|
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
68
97
|
}
|
|
@@ -130,7 +159,20 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
130
159
|
session.setSchemaAnalysis(schemaAnalysis);
|
|
131
160
|
console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
|
|
132
161
|
|
|
162
|
+
// Validate policy prerequisites before any generated artifacts are written.
|
|
163
|
+
resolveAuthPolicy(modelInfo, getAuthConfig());
|
|
164
|
+
for (const configName of ["swallowkit.config.js", "swallowkit.config.json", ".swallowkitrc.json"]) {
|
|
165
|
+
session.registerInput(path.join(process.cwd(), configName));
|
|
166
|
+
}
|
|
167
|
+
const userScopeWarning = buildUserScopedDataWarning(modelInfo, getAuthConfig());
|
|
168
|
+
if (userScopeWarning) {
|
|
169
|
+
session.addWarning(userScopeWarning);
|
|
170
|
+
console.warn(`⚠️ ${userScopeWarning}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
133
173
|
const backendLanguage = getBackendLanguage();
|
|
174
|
+
const frontend = getFrontendFramework();
|
|
175
|
+
const apiDir = options.apiDir || (frontend === "react" ? "bff" : "app/api");
|
|
134
176
|
console.log(`🧠 Backend language: ${backendLanguage}`);
|
|
135
177
|
|
|
136
178
|
// コネクタモデルかどうかを判定
|
|
@@ -181,8 +223,10 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
181
223
|
console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
|
|
182
224
|
}
|
|
183
225
|
|
|
226
|
+
updateSharedIndex(modelPath, modelInfo.schemaName, session);
|
|
227
|
+
|
|
184
228
|
// 5. Generate BFF callFunction helper
|
|
185
|
-
await generateCallFunctionHelper(session);
|
|
229
|
+
await generateCallFunctionHelper(session, apiDir);
|
|
186
230
|
|
|
187
231
|
// 6. Generate Azure Functions code
|
|
188
232
|
if (isConnectorModel) {
|
|
@@ -197,17 +241,24 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
197
241
|
}
|
|
198
242
|
}
|
|
199
243
|
|
|
200
|
-
// 7. Generate
|
|
201
|
-
|
|
202
|
-
|
|
244
|
+
// 7. Generate framework-specific BFF routes (the standalone backend is unchanged).
|
|
245
|
+
if (frontend === "react") {
|
|
246
|
+
generateManagedBffRoutes(modelInfo, apiDir, sharedPackageName, session);
|
|
247
|
+
} else if (isConnectorModel) {
|
|
203
248
|
await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName, session);
|
|
204
249
|
} else {
|
|
205
250
|
await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
|
|
206
251
|
}
|
|
207
252
|
|
|
208
|
-
// 8. Generate Cosmos DB container Bicep file (skip for connector models)
|
|
253
|
+
// 8. Generate Cosmos DB container Bicep file (skip for connector models or when infra changes are excluded)
|
|
209
254
|
if (!isConnectorModel) {
|
|
210
|
-
|
|
255
|
+
if (options.skipInfra) {
|
|
256
|
+
const warning = buildInfraSkippedWarning(modelInfo.name, toKebabCase(modelInfo.name));
|
|
257
|
+
session.addWarning(warning);
|
|
258
|
+
console.warn(`\n⚠️ ${warning}`);
|
|
259
|
+
} else {
|
|
260
|
+
await generateCosmosContainer(modelInfo, session);
|
|
261
|
+
}
|
|
211
262
|
}
|
|
212
263
|
|
|
213
264
|
// 9. Generate UI components (unless --api-only)
|
|
@@ -237,14 +288,14 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
237
288
|
console.log("\n📝 Next steps:");
|
|
238
289
|
console.log(` 1. Review generated files in ${describeFunctionsOutputPath(functionsDir, backendLanguage)} and ${apiDir}/`);
|
|
239
290
|
if (!options.apiOnly) {
|
|
240
|
-
console.log(` 2. Check the generated UI pages in app/${toKebabCase(modelInfo.name)}/`);
|
|
291
|
+
console.log(` 2. Check the generated UI pages in ${frontend === "react" ? "src/pages" : "app"}/${toKebabCase(modelInfo.name)}/`);
|
|
241
292
|
console.log(" 3. Navigate to the model from the homepage menu");
|
|
242
293
|
}
|
|
243
294
|
if (backendLanguage !== "typescript") {
|
|
244
295
|
console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI export and native schema assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
|
|
245
296
|
}
|
|
246
297
|
console.log(
|
|
247
|
-
` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
|
|
298
|
+
` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in ${frontend === "react" ? `${apiDir}/local.settings.json` : "your .env.local file"}`
|
|
248
299
|
);
|
|
249
300
|
console.log(
|
|
250
301
|
` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
|
|
@@ -258,7 +309,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
258
309
|
}
|
|
259
310
|
|
|
260
311
|
function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: FileOperationSession, importPath = '@/lib/auth/authenticated-fetch'): void {
|
|
261
|
-
const uiPrefix =
|
|
312
|
+
const uiPrefix = `${getFrontendFramework() === "react" ? "src/pages" : "app"}/${toKebabCase(modelInfo.name)}/`;
|
|
262
313
|
for (const operation of session.operations) {
|
|
263
314
|
if (!operation.path.startsWith(uiPrefix) || !operation.path.endsWith('.tsx') || operation.action === 'delete') continue;
|
|
264
315
|
let content = session.readFile(operation.absolutePath);
|
|
@@ -289,10 +340,43 @@ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: Fil
|
|
|
289
340
|
export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
290
341
|
// モデルに明示的な authPolicy がある場合はそれを使用
|
|
291
342
|
if (modelInfo.authPolicy) {
|
|
343
|
+
if (!isAuthenticationConfigured(authConfig)) {
|
|
344
|
+
throw new MachineCommandError(
|
|
345
|
+
"auth-policy-without-auth",
|
|
346
|
+
`Model '${modelInfo.name}' declares authPolicy, but authentication is not configured. No artifacts were written. Run plan auth and apply auth before scaffolding this model.`,
|
|
347
|
+
{ model: modelInfo.name },
|
|
348
|
+
"blocked",
|
|
349
|
+
[{
|
|
350
|
+
command: "swallowkit machine plan auth --provider <custom-jwt|swa|external-token>",
|
|
351
|
+
description: "Choose an authentication provider, review and apply its plan, then re-plan scaffold.",
|
|
352
|
+
}]
|
|
353
|
+
);
|
|
354
|
+
}
|
|
292
355
|
const normalized = normalizeAuthConfig(authConfig);
|
|
356
|
+
const usesNamedPolicy = Boolean(modelInfo.authPolicy.policy ||
|
|
357
|
+
typeof modelInfo.authPolicy.read === "string" || typeof modelInfo.authPolicy.write === "string");
|
|
358
|
+
// Guards resolve each operation independently: a string selects the named router, an array (or
|
|
359
|
+
// the roles fallback) selects the single-provider helper. Named schemes only have the router.
|
|
360
|
+
const operationIsNamed = (selected: string | string[] | undefined) =>
|
|
361
|
+
typeof selected === "string" || (selected === undefined && Boolean(modelInfo.authPolicy!.policy));
|
|
362
|
+
const everyOperationNamed = operationIsNamed(modelInfo.authPolicy.read) && operationIsNamed(modelInfo.authPolicy.write) &&
|
|
363
|
+
!modelInfo.authPolicy.roles;
|
|
364
|
+
if (authConfig?.schemes && !authConfig.provider && !everyOperationNamed) {
|
|
365
|
+
throw new MachineCommandError(
|
|
366
|
+
"auth-policy-requires-named-policy",
|
|
367
|
+
`Model '${modelInfo.name}' declares an authPolicy whose read or write operation does not resolve to a named policy, but authentication uses named schemes. Use authPolicy = { policy: '<name>' } or name both read and write policies (role arrays are not supported with named schemes); no artifacts were written.`,
|
|
368
|
+
{ model: modelInfo.name },
|
|
369
|
+
"blocked",
|
|
370
|
+
[{
|
|
371
|
+
command: "swallowkit machine inspect capabilities",
|
|
372
|
+
description: "Define a named authorization policy selecting the required schemes/roles, reference it from the model, then re-plan scaffold.",
|
|
373
|
+
}]
|
|
374
|
+
);
|
|
375
|
+
}
|
|
293
376
|
for (const policyName of [modelInfo.authPolicy.policy, typeof modelInfo.authPolicy.read === 'string' ? modelInfo.authPolicy.read : undefined, typeof modelInfo.authPolicy.write === 'string' ? modelInfo.authPolicy.write : undefined].filter((value): value is string => Boolean(value))) {
|
|
294
377
|
if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
|
|
295
378
|
}
|
|
379
|
+
if (usesNamedPolicy) assertNamedSchemesConfigured(modelInfo.name, "authPolicy", authConfig);
|
|
296
380
|
return modelInfo.authPolicy;
|
|
297
381
|
}
|
|
298
382
|
|
|
@@ -307,12 +391,35 @@ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig |
|
|
|
307
391
|
return {}; // 空のポリシー = 認証のみ、ロール制限なし
|
|
308
392
|
}
|
|
309
393
|
|
|
310
|
-
if (defaultPolicy !== 'anonymous')
|
|
394
|
+
if (defaultPolicy !== 'anonymous') {
|
|
395
|
+
assertNamedSchemesConfigured(modelInfo.name, `auth.authorization.defaultPolicy '${defaultPolicy}'`, authConfig);
|
|
396
|
+
return { policy: defaultPolicy };
|
|
397
|
+
}
|
|
311
398
|
|
|
312
399
|
// defaultPolicy が 'anonymous' なら認証不要
|
|
313
400
|
return undefined;
|
|
314
401
|
}
|
|
315
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Named authorization policies are enforced by the generated multi-scheme router and scheme-specific
|
|
405
|
+
* client contexts, which exist only for named auth.schemes. A single-provider configuration
|
|
406
|
+
* (auth.provider) has no router, so referencing a named policy would generate code that cannot build.
|
|
407
|
+
*/
|
|
408
|
+
function assertNamedSchemesConfigured(modelName: string, source: string, authConfig: AuthConfig | undefined): void {
|
|
409
|
+
if (Object.keys(authConfig?.schemes ?? {}).some((name) => name !== "default")) return;
|
|
410
|
+
throw new MachineCommandError(
|
|
411
|
+
"auth-named-policy-requires-named-scheme",
|
|
412
|
+
`Model '${modelName}' uses a named authorization policy via ${source}, but authentication is configured with a single auth.provider. ` +
|
|
413
|
+
`Named policies require named auth.schemes; no artifacts were written. Use role lists (authPolicy = { roles: [...] } or read/write role arrays) with auth.provider, or add a named scheme.`,
|
|
414
|
+
{ model: modelName, source },
|
|
415
|
+
"blocked",
|
|
416
|
+
[{
|
|
417
|
+
command: "swallowkit machine plan auth --provider <swa|external-token|custom-jwt> --scheme <name>",
|
|
418
|
+
description: "Add a named scheme and reference it from auth.authorization.policies, or switch the model to role lists; then re-plan scaffold.",
|
|
419
|
+
}]
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
316
423
|
/**
|
|
317
424
|
* モデルファイルのパスを解決
|
|
318
425
|
*/
|
|
@@ -384,10 +491,17 @@ function readSharedPackageName(): string {
|
|
|
384
491
|
/**
|
|
385
492
|
* Generate BFF callFunction helper (lib/api/call-function.ts)
|
|
386
493
|
*/
|
|
387
|
-
async function generateCallFunctionHelper(session: FileOperationSession): Promise<void> {
|
|
494
|
+
async function generateCallFunctionHelper(session: FileOperationSession, apiDir: string): Promise<void> {
|
|
388
495
|
console.log("\n📦 Generating BFF callFunction helper...");
|
|
389
496
|
|
|
390
497
|
const cwd = process.cwd();
|
|
498
|
+
if (getFrontendFramework() === "react") {
|
|
499
|
+
const projectName = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")).name;
|
|
500
|
+
session.writeFile(path.join(cwd, apiDir, "src", "lib", "call-function.ts"), generateReactBffHelper(projectName, getAuthConfig()), {
|
|
501
|
+
ownership: "managed", generator: SCAFFOLD_GENERATOR,
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
391
505
|
|
|
392
506
|
const helperDir = path.join(cwd, "lib", "api");
|
|
393
507
|
const helperPath = path.join(helperDir, "call-function.ts");
|
|
@@ -800,6 +914,13 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
|
|
|
800
914
|
/**
|
|
801
915
|
* Next.js BFF API Routes を生成
|
|
802
916
|
*/
|
|
917
|
+
function generateManagedBffRoutes(modelInfo: ModelInfo, apiDir: string, sharedPackageName: string, session: FileOperationSession): void {
|
|
918
|
+
const name = toCamelCase(modelInfo.name);
|
|
919
|
+
const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
|
|
920
|
+
session.writeFile(path.join(process.cwd(), apiDir, "src", name, "index.ts"), generateReactBffRoute(modelInfo, sharedPackageName), meta);
|
|
921
|
+
session.writeFile(path.join(process.cwd(), apiDir, name, "function.json"), generateManagedFunctionConfig(`${name}/{id?}`, ["GET", "POST", "PUT", "DELETE"], name), meta);
|
|
922
|
+
}
|
|
923
|
+
|
|
803
924
|
async function generateBFFRoutes(
|
|
804
925
|
modelInfo: any,
|
|
805
926
|
apiDir: string,
|
|
@@ -867,9 +988,10 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
|
|
|
867
988
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
868
989
|
const modelName = modelInfo.name;
|
|
869
990
|
const cwd = process.cwd();
|
|
991
|
+
const react = getFrontendFramework() === "react";
|
|
870
992
|
|
|
871
993
|
// Create directory structure: app/[model]/
|
|
872
|
-
const modelDir = path.join(cwd, "app", modelKebab);
|
|
994
|
+
const modelDir = path.join(cwd, react ? path.join("src", "pages") : "app", modelKebab);
|
|
873
995
|
const componentsDir = path.join(modelDir, "_components");
|
|
874
996
|
const newDir = path.join(modelDir, "new");
|
|
875
997
|
const idDir = path.join(modelDir, "[id]");
|
|
@@ -884,14 +1006,16 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
|
|
|
884
1006
|
const authLayout = generateAuthLayout(modelInfo, authOptions);
|
|
885
1007
|
|
|
886
1008
|
const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
|
|
887
|
-
|
|
888
|
-
session.writeFile(path.join(
|
|
889
|
-
session.writeFile(path.join(
|
|
890
|
-
session.writeFile(path.join(
|
|
891
|
-
session.writeFile(path.join(
|
|
1009
|
+
const adapt = react ? adaptReactPage : (source: string) => source;
|
|
1010
|
+
session.writeFile(path.join(modelDir, "page.tsx"), adapt(listPage), meta);
|
|
1011
|
+
session.writeFile(path.join(idDir, "page.tsx"), adapt(detailPage), meta);
|
|
1012
|
+
session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), adapt(formComponent), meta);
|
|
1013
|
+
session.writeFile(path.join(newDir, "page.tsx"), adapt(newPage), meta);
|
|
1014
|
+
session.writeFile(path.join(editDir, "page.tsx"), adapt(editPage), meta);
|
|
892
1015
|
if (authLayout) {
|
|
893
1016
|
session.writeFile(path.join(modelDir, "layout.tsx"), authLayout, meta);
|
|
894
1017
|
}
|
|
1018
|
+
if (react) session.writeFile(path.join(modelDir, "routes.tsx"), generateReactModelRoutes(modelInfo, authOptions), meta);
|
|
895
1019
|
|
|
896
1020
|
console.log(`✅ Created: ${path.join(modelDir, "page.tsx")}`);
|
|
897
1021
|
console.log(`✅ Created: ${path.join(idDir, "page.tsx")}`);
|
|
@@ -971,4 +1095,3 @@ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label
|
|
|
971
1095
|
session.writeFile(configPath, newConfigContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
|
|
972
1096
|
console.log(`✅ Added ${modelInfo.name} to navigation menu`);
|
|
973
1097
|
}
|
|
974
|
-
|
package/src/cli/index.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { readFileSync } from "fs";
|
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelCommand } from "./commands";
|
|
8
|
-
import {
|
|
8
|
+
import { createProvisionCommand } from "./commands/provision";
|
|
9
|
+
import { AzureCliDiagnostic, formatAzureDiagnostic } from "../utils/azure-diagnostics";
|
|
9
10
|
import { createMigrationCommand } from "./commands/create-migration";
|
|
10
11
|
import { migrationsCommand } from "./commands/migrations";
|
|
12
|
+
import { configureMcpCommand } from "./commands/configure-mcp";
|
|
11
13
|
import { addConnectorCommand } from "./commands/add-connector";
|
|
12
14
|
import { addAuthCommand } from "./commands/add-auth";
|
|
13
15
|
import { planScaffoldOperation } from "../core/operations/scaffold-plan";
|
|
@@ -15,6 +17,7 @@ import { inspectArtifacts } from "../core/project/artifacts";
|
|
|
15
17
|
import { detectDrift } from "../core/project/drift";
|
|
16
18
|
import { runVerify } from "../core/verify";
|
|
17
19
|
import { isMachineCommand, runMachineCli } from "../machine";
|
|
20
|
+
import { MachineCommandError } from "../machine/errors";
|
|
18
21
|
|
|
19
22
|
const packageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
20
23
|
export const CLI_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf8")).version as string;
|
|
@@ -24,6 +27,7 @@ const DEV_OPTION_ARITY = new Map<string, 0 | 1>([
|
|
|
24
27
|
["--port", 1],
|
|
25
28
|
["-f", 1],
|
|
26
29
|
["--functions-port", 1],
|
|
30
|
+
["--bff-port", 1],
|
|
27
31
|
["--host", 1],
|
|
28
32
|
["--seed-env", 1],
|
|
29
33
|
["-o", 0],
|
|
@@ -102,7 +106,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
102
106
|
|
|
103
107
|
program
|
|
104
108
|
.name("swallowkit")
|
|
105
|
-
.description("Next.js
|
|
109
|
+
.description("Schema-driven React and Next.js applications on Azure Static Web Apps and Functions")
|
|
106
110
|
.version(CLI_VERSION);
|
|
107
111
|
|
|
108
112
|
// Register commands
|
|
@@ -110,6 +114,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
110
114
|
.command("init [project-name]")
|
|
111
115
|
.description("Initialize a new SwallowKit project")
|
|
112
116
|
.option("--template <template>", "Template to use", "default")
|
|
117
|
+
.option("--frontend <framework>", "Frontend framework: nextjs | react (defaults to nextjs in non-interactive mode)")
|
|
113
118
|
.option("--next-version <version>", "Next.js version to install (e.g., 16.0.7, latest)", "latest")
|
|
114
119
|
.option("--package-manager <manager>", "Package manager: npm | pnpm (defaults to invocation context)")
|
|
115
120
|
.option("--cicd <provider>", "CI/CD provider: github | azure | skip")
|
|
@@ -121,6 +126,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
121
126
|
initCommand({
|
|
122
127
|
name: projectName || "swallowkit-app",
|
|
123
128
|
template: options.template,
|
|
129
|
+
frontend: options.frontend,
|
|
124
130
|
nextVersion: options.nextVersion,
|
|
125
131
|
packageManager: options.packageManager,
|
|
126
132
|
cicd: options.cicd,
|
|
@@ -134,9 +140,10 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
134
140
|
program.addCommand(devCommandOverride);
|
|
135
141
|
program.addCommand(devSeedsCommand);
|
|
136
142
|
|
|
137
|
-
program.addCommand(
|
|
143
|
+
program.addCommand(createProvisionCommand());
|
|
138
144
|
program.addCommand(migrationsCommand);
|
|
139
145
|
program.addCommand(createMigrationCommand);
|
|
146
|
+
program.addCommand(configureMcpCommand);
|
|
140
147
|
|
|
141
148
|
program
|
|
142
149
|
.command("create-model <names...>")
|
|
@@ -153,10 +160,11 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
153
160
|
|
|
154
161
|
program
|
|
155
162
|
.command("scaffold <model>")
|
|
156
|
-
.description("Generate CRUD code for Azure Functions and
|
|
163
|
+
.description("Generate CRUD code for Azure Functions, BFF, and the selected frontend from Zod models")
|
|
157
164
|
.option("--functions-dir <dir>", "Azure Functions directory", "functions")
|
|
158
|
-
.option("--api-dir <dir>", "
|
|
165
|
+
.option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
|
|
159
166
|
.option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
|
|
167
|
+
.option("--skip-infra", "Do not create or update anything under infra/ (generate the container migration later with migrations add-model)", false)
|
|
160
168
|
.option("--dry-run", "Show planned file changes without writing anything", false)
|
|
161
169
|
.action(async (model, options) => {
|
|
162
170
|
if (options.dryRun) {
|
|
@@ -165,6 +173,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
165
173
|
functionsDir: options.functionsDir,
|
|
166
174
|
apiDir: options.apiDir,
|
|
167
175
|
apiOnly: options.apiOnly,
|
|
176
|
+
skipInfra: options.skipInfra,
|
|
168
177
|
});
|
|
169
178
|
return;
|
|
170
179
|
}
|
|
@@ -174,6 +183,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
174
183
|
functionsDir: options.functionsDir,
|
|
175
184
|
apiDir: options.apiDir,
|
|
176
185
|
apiOnly: options.apiOnly,
|
|
186
|
+
skipInfra: options.skipInfra,
|
|
177
187
|
});
|
|
178
188
|
});
|
|
179
189
|
|
|
@@ -214,8 +224,8 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
214
224
|
|
|
215
225
|
program
|
|
216
226
|
.command("verify")
|
|
217
|
-
.description("Run verification checks (structure, drift, typecheck)")
|
|
218
|
-
.option("--checks <ids>", "Comma-separated check ids
|
|
227
|
+
.description("Run verification checks (structure, drift, typecheck, backend-build)")
|
|
228
|
+
.option("--checks <ids>", "Comma-separated check ids (structure,drift,typecheck,backend-build,build,lint,test); quote CSV values in PowerShell")
|
|
219
229
|
.action(async (options) => {
|
|
220
230
|
await runVerifyCommand({ checks: options.checks });
|
|
221
231
|
});
|
|
@@ -223,7 +233,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
223
233
|
return program;
|
|
224
234
|
}
|
|
225
235
|
|
|
226
|
-
async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }): Promise<void> {
|
|
236
|
+
async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }): Promise<void> {
|
|
227
237
|
console.log("🔍 Scaffold dry-run — no files will be written.\n");
|
|
228
238
|
|
|
229
239
|
const plan = await planScaffoldOperation(options);
|
|
@@ -261,7 +271,7 @@ async function runScaffoldDryRun(options: { model: string; functionsDir?: string
|
|
|
261
271
|
console.log(`⚠️ ${plan.conflicts.length} file(s) were modified after generation and would be overwritten.`);
|
|
262
272
|
console.log(` Review the conflicts, then run: swallowkit scaffold ${options.model} (or machine apply scaffold --plan ${plan.planId} --approve)`);
|
|
263
273
|
} else {
|
|
264
|
-
console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}`);
|
|
274
|
+
console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}${options.skipInfra ? " --skip-infra" : ""}`);
|
|
265
275
|
}
|
|
266
276
|
}
|
|
267
277
|
|
|
@@ -348,9 +358,35 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
348
358
|
await createProgram().parseAsync(normalizeDevCommandArgv(argv));
|
|
349
359
|
}
|
|
350
360
|
|
|
361
|
+
/** Human-readable error output for regular CLI commands (machine commands print JSON). */
|
|
362
|
+
export function formatCliError(error: unknown): string {
|
|
363
|
+
if (error instanceof MachineCommandError) {
|
|
364
|
+
const lines = [`❌ ${error.message}`, ` (code: ${error.code})`];
|
|
365
|
+
const details = error.details as Record<string, unknown> | undefined;
|
|
366
|
+
if (details && typeof details === "object") {
|
|
367
|
+
if (typeof details.nextAction === "string") lines.push(` Next: ${details.nextAction}`);
|
|
368
|
+
if (Array.isArray(details.changedFiles)) lines.push(` Changed files: ${details.changedFiles.join(", ")}`);
|
|
369
|
+
if (typeof details.runId === "string") lines.push(` Run: ${details.runId}`);
|
|
370
|
+
if (details.deployment && typeof details.deployment === "object") {
|
|
371
|
+
const deployment = details.deployment as { state?: string; reference?: { name?: string; id?: string; resourceGroup?: string; subscription?: string } };
|
|
372
|
+
lines.push(` Deployment: ${deployment.reference?.id ?? deployment.reference?.name ?? "unknown"}; state: ${deployment.state ?? "unknown"}`);
|
|
373
|
+
}
|
|
374
|
+
if (Array.isArray(details.diagnostics)) {
|
|
375
|
+
for (const diagnostic of details.diagnostics as AzureCliDiagnostic[]) lines.push(formatAzureDiagnostic(diagnostic));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const action of error.nextActions ?? []) lines.push(` Next: ${action.command} — ${action.description}`);
|
|
379
|
+
return lines.join("\n");
|
|
380
|
+
}
|
|
381
|
+
if (error instanceof Error) {
|
|
382
|
+
return process.env.SWALLOWKIT_DEBUG ? String(error.stack ?? error.message) : `❌ ${error.message}`;
|
|
383
|
+
}
|
|
384
|
+
return `❌ ${String(error)}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
351
387
|
if (require.main === module) {
|
|
352
388
|
void runCli().catch((error) => {
|
|
353
|
-
console.error(error);
|
|
389
|
+
console.error(formatCliError(error));
|
|
354
390
|
process.exitCode = 1;
|
|
355
391
|
});
|
|
356
392
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, NormalizedAuthConfig, SwallowKitConfig } from "../types";
|
|
4
|
+
import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, FrontendFramework, NormalizedAuthConfig, SwallowKitConfig } from "../types";
|
|
5
5
|
import { detectFromProject, getCommands } from "../utils/package-manager";
|
|
6
6
|
|
|
7
7
|
const requireFromHere = createRequire(__filename);
|
|
@@ -21,6 +21,7 @@ const VALID_BACKEND_LANGUAGES: BackendLanguage[] = ["typescript", "csharp", "pyt
|
|
|
21
21
|
* デフォルト設定
|
|
22
22
|
*/
|
|
23
23
|
const DEFAULT_CONFIG: SwallowKitConfig = {
|
|
24
|
+
frontend: { framework: "nextjs" },
|
|
24
25
|
database: {},
|
|
25
26
|
backend: {
|
|
26
27
|
language: "typescript",
|
|
@@ -78,6 +79,10 @@ export function loadConfig(configPath?: string, throwOnError = false, quiet = fa
|
|
|
78
79
|
*/
|
|
79
80
|
function mergeConfig(defaultConfig: SwallowKitConfig, userConfig: Partial<SwallowKitConfig>): SwallowKitConfig {
|
|
80
81
|
return {
|
|
82
|
+
frontend: {
|
|
83
|
+
...defaultConfig.frontend,
|
|
84
|
+
...userConfig.frontend,
|
|
85
|
+
},
|
|
81
86
|
database: {
|
|
82
87
|
...defaultConfig.database,
|
|
83
88
|
...userConfig.database,
|
|
@@ -108,6 +113,9 @@ function mergeConfig(defaultConfig: SwallowKitConfig, userConfig: Partial<Swallo
|
|
|
108
113
|
export function generateConfig(outputPath: string = "swallowkit.config.json"): void {
|
|
109
114
|
const config = {
|
|
110
115
|
$schema: "https://swallowkit.dev/schema.json",
|
|
116
|
+
frontend: {
|
|
117
|
+
framework: "nextjs",
|
|
118
|
+
},
|
|
111
119
|
database: {
|
|
112
120
|
connectionString: "your-cosmos-connection-string",
|
|
113
121
|
databaseName: "SwallowKitDB",
|
|
@@ -176,6 +184,14 @@ export function getBackendLanguage(configPath?: string): BackendLanguage {
|
|
|
176
184
|
return "typescript";
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
export function getFrontendFramework(configPath?: string): FrontendFramework {
|
|
188
|
+
const framework = getFullConfig(configPath).frontend?.framework ?? "nextjs";
|
|
189
|
+
if (framework !== "nextjs" && framework !== "react") {
|
|
190
|
+
throw new Error(`Unsupported frontend framework '${framework}'; use nextjs or react`);
|
|
191
|
+
}
|
|
192
|
+
return framework;
|
|
193
|
+
}
|
|
194
|
+
|
|
179
195
|
/**
|
|
180
196
|
* 完全な設定を取得(ファイル + 環境変数)
|
|
181
197
|
*/
|
|
@@ -250,6 +266,10 @@ function hasConfiguredDatabase(config: SwallowKitConfig): boolean {
|
|
|
250
266
|
export function validateConfig(config: SwallowKitConfig): { valid: boolean; errors: string[] } {
|
|
251
267
|
const errors: string[] = [];
|
|
252
268
|
|
|
269
|
+
if (config.frontend?.framework && !["nextjs", "react"].includes(config.frontend.framework)) {
|
|
270
|
+
errors.push("Frontend framework must be one of: nextjs, react");
|
|
271
|
+
}
|
|
272
|
+
|
|
253
273
|
const auth = normalizeAuthConfig(config.auth);
|
|
254
274
|
if (auth) {
|
|
255
275
|
for (const [name, scheme] of Object.entries(auth.schemes)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
+
import * as ts from "typescript";
|
|
2
3
|
import { ensureSwallowKitProject, loadConfig } from "../config";
|
|
3
4
|
import { toPascalCase } from "../scaffold/model-parser";
|
|
4
5
|
import { syncProjectManifest } from "../project/manifest";
|
|
@@ -90,21 +91,58 @@ export const connectorConfig = {
|
|
|
90
91
|
`;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
function updateSharedIndex(
|
|
94
|
+
export function updateSharedIndex(modelPath: string, schemaName: string, session: FileOperationSession): boolean {
|
|
94
95
|
const indexPath = path.join(process.cwd(), "shared", "index.ts");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
session.registerInput(indexPath);
|
|
97
|
+
const content = session.fileExists(indexPath) ? session.readFile(indexPath) : "";
|
|
98
|
+
const modulePath = `./${path.relative(path.dirname(indexPath), modelPath).replace(/\\/g, "/").replace(/\.ts$/, "")}`;
|
|
99
|
+
const source = ts.createSourceFile(indexPath, content, ts.ScriptTarget.Latest, true);
|
|
100
|
+
// Text edits that turn a type-only re-export of this schema into nothing (a value export of the same
|
|
101
|
+
// name also exports its type); keeping both would be a duplicate-identifier error.
|
|
102
|
+
const edits: Array<{ start: number; end: number; text: string }> = [];
|
|
103
|
+
let hasValueExport = false;
|
|
104
|
+
for (const statement of source.statements) {
|
|
105
|
+
if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
106
|
+
const exportedPath = statement.moduleSpecifier.text.replace(/\.(ts|js)$/, "");
|
|
107
|
+
if (exportedPath !== modulePath) continue;
|
|
108
|
+
if (!statement.exportClause) {
|
|
109
|
+
if (!statement.isTypeOnly) hasValueExport = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!ts.isNamedExports(statement.exportClause)) continue;
|
|
113
|
+
const elements = statement.exportClause.elements;
|
|
114
|
+
const matches = elements.filter((element) =>
|
|
115
|
+
element.name.text === schemaName && (element.propertyName?.text ?? element.name.text) === schemaName);
|
|
116
|
+
for (const element of matches) {
|
|
117
|
+
if (!statement.isTypeOnly && !element.isTypeOnly) {
|
|
118
|
+
hasValueExport = true;
|
|
119
|
+
} else if (!statement.isTypeOnly) {
|
|
120
|
+
// export { type Todo } -> export { Todo }
|
|
121
|
+
edits.push({ start: element.getStart(source), end: element.getEnd(), text: schemaName });
|
|
122
|
+
hasValueExport = true;
|
|
123
|
+
} else if (elements.length === 1) {
|
|
124
|
+
let end = statement.getEnd();
|
|
125
|
+
if (content.slice(end, end + 2) === "\r\n") end += 2;
|
|
126
|
+
else if (content[end] === "\n") end += 1;
|
|
127
|
+
edits.push({ start: statement.getStart(source), end, text: "" });
|
|
128
|
+
} else {
|
|
129
|
+
const index = elements.indexOf(element);
|
|
130
|
+
const start = index === 0 ? element.getStart(source) : elements[index - 1].getEnd();
|
|
131
|
+
const end = index === 0 ? elements[index + 1].getStart(source) : element.getEnd();
|
|
132
|
+
edits.push({ start, end, text: "" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
98
135
|
}
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (content.includes(exportLine)) {
|
|
104
|
-
return false;
|
|
136
|
+
let updated = content;
|
|
137
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
138
|
+
updated = `${updated.slice(0, edit.start)}${edit.text}${updated.slice(edit.end)}`;
|
|
105
139
|
}
|
|
106
|
-
|
|
107
|
-
|
|
140
|
+
if (hasValueExport && updated === content) return false;
|
|
141
|
+
if (!hasValueExport) {
|
|
142
|
+
const exportLine = `export { ${schemaName} } from '${modulePath}';`;
|
|
143
|
+
updated = `${updated}${updated && !updated.endsWith("\n") ? "\n" : ""}${exportLine}\n`;
|
|
144
|
+
}
|
|
145
|
+
session.writeFile(indexPath, updated, {
|
|
108
146
|
ownership: "extension-point",
|
|
109
147
|
generator: CREATE_MODEL_GENERATOR,
|
|
110
148
|
});
|
|
@@ -172,7 +210,7 @@ export async function createModelOperation(options: CreateModelOperationOptions)
|
|
|
172
210
|
});
|
|
173
211
|
createdFiles.push(filePath.replace(/\\/g, "/"));
|
|
174
212
|
|
|
175
|
-
if (updateSharedIndex(
|
|
213
|
+
if (updateSharedIndex(resolveFromRoot(filePath), pascalName, session)) {
|
|
176
214
|
updatedIndex = true;
|
|
177
215
|
}
|
|
178
216
|
}
|
|
@@ -152,6 +152,19 @@ param location string
|
|
|
152
152
|
return migration;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
const SUFFIXED_COSMOS_ACCOUNT_NAME = "'cosmos-${take(toLower(projectName), 30)}-${take(uniqueString(resourceGroup().id, projectName), 6)}'";
|
|
156
|
+
const LEGACY_COSMOS_ACCOUNT_NAME = "'cosmos-${projectName}'";
|
|
157
|
+
|
|
158
|
+
// Projects initialized before resource-group suffixes keep their original
|
|
159
|
+
// account name so model migrations target the account their baseline created.
|
|
160
|
+
function cosmosAccountNameExpression(session: FileOperationSession): string {
|
|
161
|
+
const mainBicep = path.join(session.rootDirectory, "infra", "main.bicep");
|
|
162
|
+
if (session.fileExists(mainBicep) && /var\s+resourceSuffix\s*=/.test(session.readFile(mainBicep))) {
|
|
163
|
+
return SUFFIXED_COSMOS_ACCOUNT_NAME;
|
|
164
|
+
}
|
|
165
|
+
return LEGACY_COSMOS_ACCOUNT_NAME;
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
export function generateModelContainerMigration(
|
|
156
169
|
model: { name: string; partitionKey: string },
|
|
157
170
|
session: FileOperationSession = new FileOperationSession("commit")
|
|
@@ -175,7 +188,7 @@ param projectName string
|
|
|
175
188
|
param location string
|
|
176
189
|
|
|
177
190
|
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
178
|
-
name:
|
|
191
|
+
name: ${cosmosAccountNameExpression(session)}
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
|