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
|
@@ -27,6 +27,13 @@ export interface CapabilityAuthProvider {
|
|
|
27
27
|
export interface CapabilitiesContract {
|
|
28
28
|
contractVersion: number;
|
|
29
29
|
swallowkitVersion: string;
|
|
30
|
+
frontends: {
|
|
31
|
+
supported: string[];
|
|
32
|
+
default: string;
|
|
33
|
+
configuration: string;
|
|
34
|
+
reactArchitecture: string;
|
|
35
|
+
development: string;
|
|
36
|
+
};
|
|
30
37
|
modelDeclarations: CapabilityModelDeclaration[];
|
|
31
38
|
authentication: {
|
|
32
39
|
correctWorkflow: string[];
|
|
@@ -46,6 +53,7 @@ export interface CapabilitiesContract {
|
|
|
46
53
|
sharedModels: {
|
|
47
54
|
idFieldRule: string;
|
|
48
55
|
buildNote: string;
|
|
56
|
+
nativeSchemaMapping: string[];
|
|
49
57
|
};
|
|
50
58
|
seeding: {
|
|
51
59
|
createTemplates: string;
|
|
@@ -53,10 +61,19 @@ export interface CapabilitiesContract {
|
|
|
53
61
|
notes: string[];
|
|
54
62
|
};
|
|
55
63
|
machineCommands: string[];
|
|
64
|
+
provisioning: {
|
|
65
|
+
execution: string;
|
|
66
|
+
completion: string;
|
|
67
|
+
tracking: string;
|
|
68
|
+
recovery: string;
|
|
69
|
+
diagnostics: string;
|
|
70
|
+
approval: string;
|
|
71
|
+
};
|
|
56
72
|
verifyChecks: {
|
|
57
73
|
defaultChecks: string[];
|
|
58
74
|
optInChecks: string[];
|
|
59
75
|
customChecks: string;
|
|
76
|
+
backendBuild: string;
|
|
60
77
|
};
|
|
61
78
|
}
|
|
62
79
|
|
|
@@ -64,6 +81,13 @@ export function inspectCapabilities(): CapabilitiesContract {
|
|
|
64
81
|
return {
|
|
65
82
|
contractVersion: 1,
|
|
66
83
|
swallowkitVersion: getSwallowKitVersion(),
|
|
84
|
+
frontends: {
|
|
85
|
+
supported: ["nextjs", "react"],
|
|
86
|
+
default: "nextjs",
|
|
87
|
+
configuration: "init --frontend nextjs|react; stored as frontend.framework in swallowkit.config.js. Missing settings preserve Next.js.",
|
|
88
|
+
reactArchitecture: "React/Vite SPA on SWA -> HTTP-only SWA managed Functions BFF (bff/) -> standalone Functions (functions/). Never link standalone Functions to SWA. Backend keys stay server-side.",
|
|
89
|
+
development: "React dev runs Vite:3000, BFF:7073, Functions:7071 and SWA CLI:4280; use the SWA port for same-origin APIs and authentication. Build/verify include BFF typechecking.",
|
|
90
|
+
},
|
|
67
91
|
modelDeclarations: [
|
|
68
92
|
{
|
|
69
93
|
name: "schema",
|
|
@@ -94,6 +118,19 @@ export function inspectCapabilities(): CapabilitiesContract {
|
|
|
94
118
|
"Role-based access control metadata applied to the generated CRUD Functions. `roles` guards all operations; `read`/`write` guard per-operation; `policy` references a named authorization policy from swallowkit.config auth.authorization.policies.",
|
|
95
119
|
format:
|
|
96
120
|
"export const authPolicy = { roles: ['admin'] };\n// or per-operation:\nexport const authPolicy = { read: ['user'], write: ['admin'] };\n// or a named policy:\nexport const authPolicy = { policy: 'adminOnly' };",
|
|
121
|
+
notes: [
|
|
122
|
+
"An explicit authPolicy without configured authentication blocks plan/apply/generate scaffold with auth-policy-without-auth before any artifacts are written. Run plan auth, apply auth, then re-plan scaffold. An empty {} declaration keeps the existing behavior: no explicit policy.",
|
|
123
|
+
"With named auth.schemes, define auth.authorization.policies selecting schemes and roles, then reference them via policy or read/write names. A role-only declaration cannot select a named scheme and blocks with auth-policy-requires-named-policy; it is supported by legacy single-provider authentication.",
|
|
124
|
+
"Conversely, single-provider authentication (auth.provider) supports role lists only. Referencing a named policy (authPolicy.policy, read/write names, or a named auth.authorization.defaultPolicy) blocks with auth-named-policy-requires-named-scheme; add a named scheme first.",
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "references",
|
|
129
|
+
required: false,
|
|
130
|
+
description:
|
|
131
|
+
"Explicit foreign keys used by generated UI lookups. Without it, a string field named <model>Id is treated as a foreign key only when shared/models contains that model (userId -> User only if user.ts exists). Map a field to false to disable inference. References to missing models are ignored with a reference-model-not-found warning.",
|
|
132
|
+
format: "export const references = { authorId: 'User', legacyId: false };",
|
|
133
|
+
notes: ["Only single string id fields can reference a model.", "No lookup UI, fetch, or OpenAPI reference is generated for fields that do not reference an existing model."],
|
|
97
134
|
},
|
|
98
135
|
{
|
|
99
136
|
name: "connectorConfig",
|
|
@@ -160,15 +197,24 @@ export function inspectCapabilities(): CapabilitiesContract {
|
|
|
160
197
|
],
|
|
161
198
|
notGuaranteed: [
|
|
162
199
|
"Owner scoping: generated CRUD does NOT restrict rows to the authenticated principal. Any authenticated caller passing the policy can read/write any document.",
|
|
200
|
+
"Without authentication, plan scaffold reports user-scoped-data-without-auth when the partition key or an owner-like field (userId, learnerId, ownerId, ...) suggests per-user data.",
|
|
163
201
|
],
|
|
164
202
|
guidance: [
|
|
165
|
-
"For multi-user data where users may only touch their own records, add ai-authored custom endpoints that derive the scoping key (e.g. learnerId) from the verified principal — never from the request body — and restrict generated write endpoints to admin roles or disable them.",
|
|
203
|
+
"Configure authentication via plan/apply auth before declaring authPolicy. For multi-user data where users may only touch their own records, add ai-authored custom endpoints that derive the scoping key (e.g. learnerId) from the verified principal — never from the request body — and restrict generated write endpoints to admin roles or disable them.",
|
|
166
204
|
],
|
|
167
205
|
},
|
|
168
206
|
sharedModels: {
|
|
169
207
|
idFieldRule: "id: z.string() (required). createdAt/updatedAt: z.string().optional(). The backend owns their values.",
|
|
208
|
+
nativeSchemaMapping: [
|
|
209
|
+
"C#/Python schema assets derive types from Zod, never from field names: z.string() -> string/str (including createdAt/updatedAt).",
|
|
210
|
+
"Date types only when declared: z.iso.datetime() -> DateTime/datetime, z.iso.date() -> DateOnly/date, z.date() -> DateTime/datetime.",
|
|
211
|
+
"z.number() -> double / float|int; z.number().int() or z.int() -> long / int.",
|
|
212
|
+
"Inline z.object({...}) fields become named classes <Model><Field> (e.g. ProbeItemNested); imported model schemas keep their own class.",
|
|
213
|
+
"min/max/length/nonempty/regex/int/gt/gte/lt/lte/positive/nonnegative and array min/max are enforced by C# IValidatableObject.Validate and Python Field bounds/validators. Regexes use search semantics like Zod.",
|
|
214
|
+
"String formats (email/uuid/url), refinements, and transforms are not re-implemented natively; the BFF Zod schemas remain the authoritative validation.",
|
|
215
|
+
],
|
|
170
216
|
buildNote:
|
|
171
|
-
"verify typecheck resolves the shared package from its built dist/. SwallowKit rebuilds shared automatically during verify; when typechecking manually, build
|
|
217
|
+
"create-model and plan/apply scaffold append missing model exports to shared/index.ts, including manually authored models. Manual exports are allowed (no markers); existing content is preserved. verify typecheck resolves the shared package from its built dist/. SwallowKit rebuilds shared automatically during verify; when typechecking manually, build shared first.",
|
|
172
218
|
},
|
|
173
219
|
seeding: {
|
|
174
220
|
createTemplates: "swallowkit create-dev-seeds <environment> — writes dev-seeds/<environment>/<model>.json templates.",
|
|
@@ -176,18 +222,29 @@ export function inspectCapabilities(): CapabilitiesContract {
|
|
|
176
222
|
notes: ["Editing dev-seeds/*.json alone does nothing; seeds are only applied via dev --seed-env."],
|
|
177
223
|
},
|
|
178
224
|
machineCommands: [
|
|
179
|
-
"inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities",
|
|
225
|
+
"inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities; inspect provision --plan <runId> [--wait-seconds <0-3600>]",
|
|
180
226
|
"validate project",
|
|
181
227
|
"generate model <names...> | generate scaffold <model>",
|
|
182
|
-
"plan scaffold <models...> | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if]",
|
|
183
|
-
"apply scaffold [models...] --plan <id> [--approve] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve",
|
|
228
|
+
"plan scaffold <models...> [--api-only] [--skip-infra] | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if] [--resume-deployment <name>]",
|
|
229
|
+
"apply scaffold [models...] --plan <id> [--approve] [--skip-infra] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve [--wait-seconds <0-3600>] (also resumes the same execution checkpoint)",
|
|
184
230
|
"verify project [--checks <ids>] [--compact]",
|
|
185
231
|
"explain failure [--check <id>]",
|
|
232
|
+
"(regular CLI) migrations add-model <model> [--dry-run] — generate only the container migration after scaffold --skip-infra",
|
|
233
|
+
"(regular CLI) configure-mcp [--version auto|latest|<semver>] — re-select the MCP launcher version in .mcp.json",
|
|
186
234
|
],
|
|
235
|
+
provisioning: {
|
|
236
|
+
execution: "Submit Bicep once with az deployment group create --no-wait, then bounded state polling (default 300 seconds, configurable 0-3600) for machine/MCP; the human `swallowkit provision` command waits until Azure reaches a terminal state. Azure CLI invocations are bounded separately. Warning-only stderr with exit 0 is not a failure; output capture limits do not terminate az.",
|
|
237
|
+
completion: "status complete/outcome succeeded requires Azure Succeeded plus verified migration tags and local ledger completion. Running/Accepted -> in-progress; unreadable/not-found state -> blocked/outcome unknown; bookkeeping interruption after Azure Succeeded -> blocked/outcome partial; Failed/Canceled -> failed with Azure causes.",
|
|
238
|
+
tracking: "Approved runs persist before Azure writes under .swallowkit/state/provision/. runId equals the original planId and preserves the subscription, RG, deployment ID/correlation, fingerprints and migration state. inspect provision is read-only; apply provision --plan resumes without re-submitting attempted deployments. Ctrl+C/termination may leave Azure running; never cancel/delete without explicit consent.",
|
|
239
|
+
recovery: "For an older untracked baseline, plan provision --resume-deployment <name> compares the existing ARM template and non-secure parameters to the local Bicep/parameters. It observes that deployment and only tags/records it after Succeeded. Mismatches, unreadable evidence, secure parameters or non-baseline migrations block; do not use reconcile just to repair bookkeeping.",
|
|
240
|
+
diagnostics: "Machine/MCP results include bounded/redacted stdout/stderr, omitted-output flags, exit code, signal, failure kind, elapsed time and deployment identity, but no deployment outputs. Normal CLI shows named resource information and CI/CD setup guidance after success. Never print --debug output, keys, tokens or connection strings.",
|
|
241
|
+
approval: "Machine/MCP initial apply and every resume require explicit human approval. A human invoking normal provision or provision --resume explicitly requests execution without another confirmation prompt. Agents must not switch to normal CLI to bypass approval. A passing local command or an ok envelope is not proof of completed Azure provisioning; inspect status, outcome and nextActions.",
|
|
242
|
+
},
|
|
187
243
|
verifyChecks: {
|
|
188
|
-
defaultChecks: ["structure", "drift", "typecheck"],
|
|
244
|
+
defaultChecks: ["structure", "drift", "typecheck", "backend-build"],
|
|
189
245
|
optInChecks: ["build", "lint", "test"],
|
|
190
246
|
customChecks: "swallowkit.config verify.checks: [{ id, command, title?, timeoutMs? }]",
|
|
247
|
+
backendBuild: "Runs dotnet build in functions/ (C#), the project tsc --noEmit (TypeScript), or Python 3 compileall (syntax only, using functions/.venv before PATH). Missing toolchains are skip with a reason, not proof of validation. Root build scripts do not replace backend-build; use --checks \"structure,drift,typecheck,backend-build,build\" for both.",
|
|
191
248
|
},
|
|
192
249
|
};
|
|
193
250
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { FrontendFramework } from "../../types";
|
|
2
|
+
import { PackageManager, getCommands } from "../../utils/package-manager";
|
|
3
|
+
import { adaptReactPage } from "../scaffold/react-ui-generator";
|
|
4
|
+
|
|
5
|
+
export function buildHomePage(
|
|
6
|
+
frontend: FrontendFramework,
|
|
7
|
+
pm: PackageManager
|
|
8
|
+
): string {
|
|
9
|
+
const framework = frontend === "react" ? "React" : "Next.js";
|
|
10
|
+
const page = `import { useState } from 'react';
|
|
11
|
+
import Link from 'next/link';
|
|
12
|
+
import { scaffoldConfig } from '@/lib/scaffold-config';
|
|
13
|
+
|
|
14
|
+
export default function Home() {
|
|
15
|
+
const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
|
16
|
+
const [message, setMessage] = useState('');
|
|
17
|
+
|
|
18
|
+
const testConnection = async () => {
|
|
19
|
+
setGreetingStatus('loading');
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch('/api/greet?name=SwallowKit');
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(data.error || \`Server error: \${response.status}\`);
|
|
25
|
+
}
|
|
26
|
+
setMessage(data.message);
|
|
27
|
+
setGreetingStatus('success');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
|
|
30
|
+
setGreetingStatus('error');
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
|
|
36
|
+
<div className="container mx-auto px-4 py-12">
|
|
37
|
+
<header className="text-center mb-16">
|
|
38
|
+
<h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
|
|
39
|
+
Welcome to SwallowKit
|
|
40
|
+
</h1>
|
|
41
|
+
<p className="text-xl text-gray-600 dark:text-gray-400">
|
|
42
|
+
${framework} on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
|
|
43
|
+
</p>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<section className="max-w-2xl mx-auto mb-12">
|
|
47
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
|
|
48
|
+
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
|
49
|
+
Test BFF → Functions Connection
|
|
50
|
+
</h2>
|
|
51
|
+
<button
|
|
52
|
+
onClick={testConnection}
|
|
53
|
+
disabled={greetingStatus === 'loading'}
|
|
54
|
+
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
|
|
55
|
+
>
|
|
56
|
+
{greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
|
|
57
|
+
</button>
|
|
58
|
+
{greetingStatus === 'success' && (
|
|
59
|
+
<div role="status" className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
60
|
+
<p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
|
|
61
|
+
<p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
{greetingStatus === 'error' && (
|
|
65
|
+
<div role="alert" className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
66
|
+
<p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
|
|
67
|
+
<p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
{scaffoldConfig.models.length > 0 ? (
|
|
74
|
+
<section className="max-w-6xl mx-auto">
|
|
75
|
+
<h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
|
|
76
|
+
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
77
|
+
{scaffoldConfig.models.map((model) => (
|
|
78
|
+
<Link
|
|
79
|
+
key={model.name}
|
|
80
|
+
href={model.path}
|
|
81
|
+
className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
|
|
82
|
+
>
|
|
83
|
+
<h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
|
|
84
|
+
<p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
|
|
85
|
+
</Link>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
) : (
|
|
90
|
+
<section className="max-w-2xl mx-auto text-center">
|
|
91
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
|
|
92
|
+
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
|
|
93
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
94
|
+
Create your first model with Zod and generate CRUD operations automatically.
|
|
95
|
+
</p>
|
|
96
|
+
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
97
|
+
${getCommands(pm).exec} swallowkit scaffold shared/models/your-model.ts
|
|
98
|
+
</code>
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
|
|
104
|
+
<p>Built with SwallowKit</p>
|
|
105
|
+
</footer>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
return frontend === "react"
|
|
112
|
+
? adaptReactPage(page)
|
|
113
|
+
: `'use client'\n\nexport const dynamic = 'force-dynamic';\n\n${page}`;
|
|
114
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { getBackendLanguage, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
|
|
3
|
+
import { getBackendLanguage, getFrontendFramework, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
|
|
4
4
|
import { getAllModels, toCamelCase, toKebabCase } from "../scaffold/model-parser";
|
|
5
|
-
import { AuthConfig, BackendLanguage, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
|
|
5
|
+
import { AuthConfig, BackendLanguage, FrontendFramework, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
|
|
6
6
|
import { captureConsoleMessages, withWorkingDirectory } from "../operations/runtime";
|
|
7
7
|
|
|
8
8
|
const CONFIG_CANDIDATES = [
|
|
@@ -69,6 +69,7 @@ export interface ProjectManifestArtifacts {
|
|
|
69
69
|
|
|
70
70
|
export interface ProjectManifestArchitecture {
|
|
71
71
|
pattern: string;
|
|
72
|
+
frontendFramework?: FrontendFramework;
|
|
72
73
|
backendLanguage: BackendLanguage;
|
|
73
74
|
layerBoundaries: string[];
|
|
74
75
|
hasSharedWorkspace: boolean;
|
|
@@ -81,6 +82,7 @@ export interface ProjectManifest {
|
|
|
81
82
|
generatedAt: string;
|
|
82
83
|
configPath: string | null;
|
|
83
84
|
backendLanguage: BackendLanguage;
|
|
85
|
+
frontendFramework?: FrontendFramework;
|
|
84
86
|
configValidation: {
|
|
85
87
|
valid: boolean;
|
|
86
88
|
errors: string[];
|
|
@@ -115,7 +117,10 @@ function relativeProjectPath(projectRoot: string, targetPath: string): string {
|
|
|
115
117
|
return path.relative(projectRoot, targetPath).replace(/\\/g, "/");
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
function buildModules(projectRoot: string): ProjectManifestModule[] {
|
|
120
|
+
function buildModules(projectRoot: string, frontend: FrontendFramework): ProjectManifestModule[] {
|
|
121
|
+
const bffRoot = frontend === "react" ? "bff" : "app/api";
|
|
122
|
+
const uiRoot = frontend === "react" ? "src" : "app";
|
|
123
|
+
const authRoot = frontend === "react" ? "bff/src/auth" : "app/api/auth";
|
|
119
124
|
const candidates: ProjectManifestModule[] = [
|
|
120
125
|
{
|
|
121
126
|
name: "shared-models",
|
|
@@ -126,8 +131,8 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
|
|
|
126
131
|
{
|
|
127
132
|
name: "bff",
|
|
128
133
|
kind: "bff",
|
|
129
|
-
rootPath:
|
|
130
|
-
exists: fs.existsSync(path.join(projectRoot,
|
|
134
|
+
rootPath: bffRoot,
|
|
135
|
+
exists: fs.existsSync(path.join(projectRoot, bffRoot)),
|
|
131
136
|
},
|
|
132
137
|
{
|
|
133
138
|
name: "functions",
|
|
@@ -138,8 +143,8 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
|
|
|
138
143
|
{
|
|
139
144
|
name: "ui",
|
|
140
145
|
kind: "ui",
|
|
141
|
-
rootPath:
|
|
142
|
-
exists: fs.existsSync(path.join(projectRoot,
|
|
146
|
+
rootPath: uiRoot,
|
|
147
|
+
exists: fs.existsSync(path.join(projectRoot, uiRoot)),
|
|
143
148
|
},
|
|
144
149
|
{
|
|
145
150
|
name: "infrastructure",
|
|
@@ -150,21 +155,21 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
|
|
|
150
155
|
{
|
|
151
156
|
name: "auth",
|
|
152
157
|
kind: "auth",
|
|
153
|
-
rootPath:
|
|
154
|
-
exists: fs.existsSync(path.join(projectRoot,
|
|
158
|
+
rootPath: authRoot,
|
|
159
|
+
exists: fs.existsSync(path.join(projectRoot, authRoot)),
|
|
155
160
|
},
|
|
156
161
|
];
|
|
157
162
|
|
|
158
163
|
return candidates;
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
|
|
166
|
+
function buildArtifacts(projectRoot: string, frontend: FrontendFramework): ProjectManifestArtifacts {
|
|
162
167
|
const openApiDir = path.join(projectRoot, "functions", "openapi");
|
|
163
168
|
const generatedDir = path.join(projectRoot, "functions", "generated");
|
|
164
169
|
|
|
165
170
|
return {
|
|
166
|
-
callFunctionHelperPath: "lib/api/call-function.ts",
|
|
167
|
-
callFunctionHelperExists: fs.existsSync(path.join(projectRoot, "lib", "api", "call-function.ts")),
|
|
171
|
+
callFunctionHelperPath: frontend === "react" ? "bff/src/lib/call-function.ts" : "lib/api/call-function.ts",
|
|
172
|
+
callFunctionHelperExists: fs.existsSync(path.join(projectRoot, ...(frontend === "react" ? ["bff", "src", "lib", "call-function.ts"] : ["lib", "api", "call-function.ts"]))),
|
|
168
173
|
openApiSpecs: fs.existsSync(openApiDir)
|
|
169
174
|
? fs.readdirSync(openApiDir)
|
|
170
175
|
.filter((entry) => entry.endsWith(".json"))
|
|
@@ -178,8 +183,8 @@ function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
|
|
|
178
183
|
: [],
|
|
179
184
|
proxyPath: "proxy.ts",
|
|
180
185
|
proxyExists: fs.existsSync(path.join(projectRoot, "proxy.ts")),
|
|
181
|
-
loginPagePath: "app/login/page.tsx",
|
|
182
|
-
loginPageExists: fs.existsSync(path.join(projectRoot, "app", "login", "page.tsx")),
|
|
186
|
+
loginPagePath: frontend === "react" ? "src/pages/login.tsx" : "app/login/page.tsx",
|
|
187
|
+
loginPageExists: fs.existsSync(path.join(projectRoot, ...(frontend === "react" ? ["src", "pages", "login.tsx"] : ["app", "login", "page.tsx"]))),
|
|
183
188
|
authContextPath: "lib/auth/auth-context.tsx",
|
|
184
189
|
authContextExists: fs.existsSync(path.join(projectRoot, "lib", "auth", "auth-context.tsx")),
|
|
185
190
|
};
|
|
@@ -188,7 +193,8 @@ function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
|
|
|
188
193
|
function buildEntityRoutes(
|
|
189
194
|
projectRoot: string,
|
|
190
195
|
backendLanguage: BackendLanguage,
|
|
191
|
-
entity: ProjectManifestEntity
|
|
196
|
+
entity: ProjectManifestEntity,
|
|
197
|
+
frontend: FrontendFramework
|
|
192
198
|
): ProjectManifestRoute[] {
|
|
193
199
|
const modelCamel = toCamelCase(entity.name);
|
|
194
200
|
const modelKebab = toKebabCase(entity.name);
|
|
@@ -229,8 +235,10 @@ function buildEntityRoutes(
|
|
|
229
235
|
entityName: entity.name,
|
|
230
236
|
methods: listMethods,
|
|
231
237
|
publicPath: `/api/${modelCamel}`,
|
|
232
|
-
filePath: `app/api/${modelCamel}/route.ts`,
|
|
233
|
-
exists:
|
|
238
|
+
filePath: frontend === "react" ? `bff/src/${modelCamel}/index.ts` : `app/api/${modelCamel}/route.ts`,
|
|
239
|
+
exists: frontend === "react"
|
|
240
|
+
? fs.existsSync(path.join(projectRoot, "bff", "src", modelCamel, "index.ts")) && fs.existsSync(path.join(projectRoot, "bff", modelCamel, "function.json"))
|
|
241
|
+
: fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "route.ts")),
|
|
234
242
|
},
|
|
235
243
|
{
|
|
236
244
|
name: `${entity.name}-bff-detail`,
|
|
@@ -239,8 +247,10 @@ function buildEntityRoutes(
|
|
|
239
247
|
entityName: entity.name,
|
|
240
248
|
methods: detailMethods,
|
|
241
249
|
publicPath: `/api/${modelCamel}/{id}`,
|
|
242
|
-
filePath: `app/api/${modelCamel}/[id]/route.ts`,
|
|
243
|
-
exists:
|
|
250
|
+
filePath: frontend === "react" ? `bff/src/${modelCamel}/index.ts` : `app/api/${modelCamel}/[id]/route.ts`,
|
|
251
|
+
exists: frontend === "react"
|
|
252
|
+
? fs.existsSync(path.join(projectRoot, "bff", "src", modelCamel, "index.ts")) && fs.existsSync(path.join(projectRoot, "bff", modelCamel, "function.json"))
|
|
253
|
+
: fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "[id]", "route.ts")),
|
|
244
254
|
},
|
|
245
255
|
{
|
|
246
256
|
name: `${entity.name}-functions`,
|
|
@@ -255,7 +265,18 @@ function buildEntityRoutes(
|
|
|
255
265
|
];
|
|
256
266
|
}
|
|
257
267
|
|
|
258
|
-
function buildSystemRoutes(projectRoot: string): ProjectManifestRoute[] {
|
|
268
|
+
function buildSystemRoutes(projectRoot: string, frontend: FrontendFramework, auth?: AuthConfig): ProjectManifestRoute[] {
|
|
269
|
+
if (frontend === "react") {
|
|
270
|
+
const providers = Object.values(normalizeAuthConfig(auth)?.schemes ?? {}).map(scheme => scheme.provider);
|
|
271
|
+
return ["login", "logout", "me"].map(action => ({
|
|
272
|
+
name: `auth-${action}`, kind: "system", surface: "bff",
|
|
273
|
+
methods: [action === "me" ? "GET" : "POST"], publicPath: `/api/auth/${action}`,
|
|
274
|
+
filePath: "bff/src/auth/index.ts",
|
|
275
|
+
exists: (providers.includes("custom-jwt") || (action === "me" && providers.includes("external-token"))) &&
|
|
276
|
+
fs.existsSync(path.join(projectRoot, "bff", "src", "auth", "index.ts")) &&
|
|
277
|
+
fs.existsSync(path.join(projectRoot, "bff", "auth", "function.json")),
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
259
280
|
return [
|
|
260
281
|
{
|
|
261
282
|
name: "auth-login",
|
|
@@ -296,6 +317,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
|
|
|
296
317
|
return withWorkingDirectory(projectRoot, async () => {
|
|
297
318
|
const configPath = resolveConfigPath(projectRoot);
|
|
298
319
|
const backendLanguage = getBackendLanguage(configPath || undefined);
|
|
320
|
+
const frontendFramework = getFrontendFramework(configPath || undefined);
|
|
299
321
|
const config = getFullConfig(configPath || undefined);
|
|
300
322
|
const configValidation = validateConfig(config);
|
|
301
323
|
const { result: entities, messages } = await captureConsoleMessages(async () => {
|
|
@@ -327,8 +349,8 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
|
|
|
327
349
|
);
|
|
328
350
|
|
|
329
351
|
const routes = [
|
|
330
|
-
...entities.flatMap((entity) => buildEntityRoutes(projectRoot, backendLanguage, entity)),
|
|
331
|
-
...buildSystemRoutes(projectRoot),
|
|
352
|
+
...entities.flatMap((entity) => buildEntityRoutes(projectRoot, backendLanguage, entity, frontendFramework)),
|
|
353
|
+
...buildSystemRoutes(projectRoot, frontendFramework, config.auth),
|
|
332
354
|
].sort((left, right) => left.name.localeCompare(right.name));
|
|
333
355
|
|
|
334
356
|
const manifest: ProjectManifest = {
|
|
@@ -336,6 +358,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
|
|
|
336
358
|
generatedAt: new Date().toISOString(),
|
|
337
359
|
configPath,
|
|
338
360
|
backendLanguage,
|
|
361
|
+
frontendFramework,
|
|
339
362
|
configValidation,
|
|
340
363
|
connectors: Object.entries(config.connectors || {})
|
|
341
364
|
.map(([name, definition]) => ({ name, definition }))
|
|
@@ -343,16 +366,18 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
|
|
|
343
366
|
auth: normalizeAuthConfig(config.auth) || null,
|
|
344
367
|
entities: entities.sort((left, right) => left.name.localeCompare(right.name)),
|
|
345
368
|
routes,
|
|
346
|
-
modules: buildModules(projectRoot),
|
|
347
|
-
artifacts: buildArtifacts(projectRoot),
|
|
369
|
+
modules: buildModules(projectRoot, frontendFramework),
|
|
370
|
+
artifacts: buildArtifacts(projectRoot, frontendFramework),
|
|
348
371
|
architecture: {
|
|
349
|
-
pattern: "Next.js BFF + Azure Functions + shared Zod models",
|
|
372
|
+
pattern: frontendFramework === "react" ? "React SPA + SWA managed Functions BFF + standalone Azure Functions + shared Zod models" : "Next.js BFF + Azure Functions + shared Zod models",
|
|
373
|
+
frontendFramework,
|
|
350
374
|
backendLanguage,
|
|
351
375
|
layerBoundaries: [
|
|
352
376
|
"shared/models -> schema source of truth",
|
|
353
|
-
"app/api -> BFF layer only
|
|
377
|
+
`${frontendFramework === "react" ? "bff" : "app/api"} -> BFF layer only`,
|
|
354
378
|
"functions -> backend logic and data access",
|
|
355
|
-
"app -> UI and pages
|
|
379
|
+
`${frontendFramework === "react" ? "src" : "app"} -> UI and pages`,
|
|
380
|
+
"SWA and standalone Functions are not linked; backend credentials stay in the BFF",
|
|
356
381
|
],
|
|
357
382
|
hasSharedWorkspace: fs.existsSync(path.join(projectRoot, "shared", "package.json")),
|
|
358
383
|
hasConnectors: Object.keys(config.connectors || {}).length > 0,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-scoped MCP launcher version management.
|
|
3
|
+
*
|
|
4
|
+
* `.mcp.json` intentionally resolves `swallowkit@latest` so that projects created
|
|
5
|
+
* with older releases still get the current stable tools. A prerelease project can
|
|
6
|
+
* be newer than `latest`; in that case the launcher is pinned to the project
|
|
7
|
+
* version until a stable release that understands project-aware delegation catches up.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { compareSemver, isPrerelease, parseSemver } from "../../utils/semver";
|
|
15
|
+
|
|
16
|
+
export const MCP_CONFIG_PATH = ".mcp.json";
|
|
17
|
+
export const MCP_VERSION_ENV = "SWALLOWKIT_MCP_VERSION";
|
|
18
|
+
export const MCP_LATEST = "latest";
|
|
19
|
+
|
|
20
|
+
export interface McpPinDecision {
|
|
21
|
+
version: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Decide the SWALLOWKIT_MCP_VERSION value for a project that uses `projectVersion`.
|
|
27
|
+
* Uses `latest` unless the project version is newer than the published `latest`.
|
|
28
|
+
* When the registry could not be reached, prereleases are pinned conservatively.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveMcpPinVersion(projectVersion: string, publishedLatest: string | null): McpPinDecision {
|
|
31
|
+
if (!parseSemver(projectVersion)) {
|
|
32
|
+
return { version: MCP_LATEST, reason: `Project version ${projectVersion} is not a valid semantic version.` };
|
|
33
|
+
}
|
|
34
|
+
if (publishedLatest && parseSemver(publishedLatest)) {
|
|
35
|
+
const comparison = compareSemver(projectVersion, publishedLatest)!;
|
|
36
|
+
return comparison > 0
|
|
37
|
+
? { version: projectVersion, reason: `Project version ${projectVersion} is newer than published latest ${publishedLatest}.` }
|
|
38
|
+
: { version: MCP_LATEST, reason: `Published latest ${publishedLatest} is the same as or newer than project version ${projectVersion}.` };
|
|
39
|
+
}
|
|
40
|
+
return isPrerelease(projectVersion)
|
|
41
|
+
? { version: projectVersion, reason: `Published latest is unknown; prerelease ${projectVersion} is pinned so MCP matches the project.` }
|
|
42
|
+
: { version: MCP_LATEST, reason: "Published latest is unknown; stable projects keep latest." };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function lookupPublishedLatestVersion(packageName: string, timeoutMs = 15000): string | null {
|
|
46
|
+
// Run outside the project so its devEngines/packageManager policy does not reject npm.
|
|
47
|
+
const result = spawnSync("npm", ["view", `${packageName}@latest`, "version"], {
|
|
48
|
+
cwd: os.tmpdir(),
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
timeout: timeoutMs,
|
|
51
|
+
shell: process.platform === "win32",
|
|
52
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
53
|
+
});
|
|
54
|
+
if (result.error || result.status !== 0) return null;
|
|
55
|
+
const version = String(result.stdout).trim().split(/\s+/).pop() ?? "";
|
|
56
|
+
return parseSemver(version) ? version : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readMcpConfigVersion(projectDir: string): string | null {
|
|
60
|
+
const configPath = path.join(projectDir, MCP_CONFIG_PATH);
|
|
61
|
+
if (!fs.existsSync(configPath)) return null;
|
|
62
|
+
try {
|
|
63
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
64
|
+
const value = config?.mcpServers?.swallowkit?.env?.[MCP_VERSION_ENV];
|
|
65
|
+
return typeof value === "string" ? value : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Update only the SwallowKit MCP version in .mcp.json, preserving all other content. */
|
|
72
|
+
export function writeMcpConfigVersion(projectDir: string, version: string): void {
|
|
73
|
+
const configPath = path.join(projectDir, MCP_CONFIG_PATH);
|
|
74
|
+
if (!fs.existsSync(configPath)) {
|
|
75
|
+
throw new Error(`${MCP_CONFIG_PATH} was not found. Run this command from a SwallowKit project root.`);
|
|
76
|
+
}
|
|
77
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
78
|
+
const server = config?.mcpServers?.swallowkit;
|
|
79
|
+
if (!server || typeof server !== "object") {
|
|
80
|
+
throw new Error(`${MCP_CONFIG_PATH} does not define mcpServers.swallowkit.`);
|
|
81
|
+
}
|
|
82
|
+
server.env = { ...(server.env ?? {}), [MCP_VERSION_ENV]: version };
|
|
83
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
84
|
+
}
|