swallowkit 2.0.0-alpha.3 → 2.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  4. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  5. package/dist/cli/commands/configure-mcp.js +42 -0
  6. package/dist/cli/commands/configure-mcp.js.map +1 -0
  7. package/dist/cli/commands/init.d.ts +6 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +19 -5
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/migrations.d.ts.map +1 -1
  12. package/dist/cli/commands/migrations.js +25 -0
  13. package/dist/cli/commands/migrations.js.map +1 -1
  14. package/dist/cli/commands/provision.d.ts +2 -0
  15. package/dist/cli/commands/provision.d.ts.map +1 -1
  16. package/dist/cli/commands/provision.js +45 -16
  17. package/dist/cli/commands/provision.js.map +1 -1
  18. package/dist/cli/commands/scaffold.d.ts +4 -0
  19. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  20. package/dist/cli/commands/scaffold.js +85 -3
  21. package/dist/cli/commands/scaffold.js.map +1 -1
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +29 -4
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/core/operations/create-model.d.ts +2 -0
  27. package/dist/core/operations/create-model.d.ts.map +1 -1
  28. package/dist/core/operations/create-model.js +60 -8
  29. package/dist/core/operations/create-model.js.map +1 -1
  30. package/dist/core/operations/model-migration.d.ts +22 -0
  31. package/dist/core/operations/model-migration.d.ts.map +1 -0
  32. package/dist/core/operations/model-migration.js +36 -0
  33. package/dist/core/operations/model-migration.js.map +1 -0
  34. package/dist/core/operations/provision-operations.d.ts +19 -1
  35. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  36. package/dist/core/operations/provision-operations.js +62 -35
  37. package/dist/core/operations/provision-operations.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  40. package/dist/core/operations/scaffold-apply.js +1 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -1
  42. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  43. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  47. package/dist/core/operations/scaffold-plan.js +3 -1
  48. package/dist/core/operations/scaffold-plan.js.map +1 -1
  49. package/dist/core/project/boundaries.d.ts +5 -2
  50. package/dist/core/project/boundaries.d.ts.map +1 -1
  51. package/dist/core/project/boundaries.js +51 -20
  52. package/dist/core/project/boundaries.js.map +1 -1
  53. package/dist/core/project/capabilities.d.ts +2 -0
  54. package/dist/core/project/capabilities.d.ts.map +1 -1
  55. package/dist/core/project/capabilities.js +29 -5
  56. package/dist/core/project/capabilities.js.map +1 -1
  57. package/dist/core/project/mcp-config.d.ts +26 -0
  58. package/dist/core/project/mcp-config.d.ts.map +1 -0
  59. package/dist/core/project/mcp-config.js +117 -0
  60. package/dist/core/project/mcp-config.js.map +1 -0
  61. package/dist/core/project/react-project.js +1 -1
  62. package/dist/core/project/workflows.d.ts.map +1 -1
  63. package/dist/core/project/workflows.js +23 -5
  64. package/dist/core/project/workflows.js.map +1 -1
  65. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  66. package/dist/core/scaffold/auth-generator.js +18 -19
  67. package/dist/core/scaffold/auth-generator.js.map +1 -1
  68. package/dist/core/scaffold/model-parser.d.ts +23 -0
  69. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  70. package/dist/core/scaffold/model-parser.js +407 -137
  71. package/dist/core/scaffold/model-parser.js.map +1 -1
  72. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  73. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  74. package/dist/core/scaffold/native-schema-generator.js +289 -76
  75. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  76. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  77. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  78. package/dist/core/scaffold/openapi-generator.js +53 -3
  79. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  80. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  81. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  82. package/dist/core/scaffold/ui-generator.js +68 -7
  83. package/dist/core/scaffold/ui-generator.js.map +1 -1
  84. package/dist/core/verify/index.d.ts +4 -3
  85. package/dist/core/verify/index.d.ts.map +1 -1
  86. package/dist/core/verify/index.js +122 -5
  87. package/dist/core/verify/index.js.map +1 -1
  88. package/dist/machine/contracts.d.ts +1 -0
  89. package/dist/machine/contracts.d.ts.map +1 -1
  90. package/dist/machine/errors.d.ts +3 -2
  91. package/dist/machine/errors.d.ts.map +1 -1
  92. package/dist/machine/errors.js +2 -1
  93. package/dist/machine/errors.js.map +1 -1
  94. package/dist/machine/index.d.ts.map +1 -1
  95. package/dist/machine/index.js +42 -17
  96. package/dist/machine/index.js.map +1 -1
  97. package/dist/mcp/index.d.ts +4 -2
  98. package/dist/mcp/index.d.ts.map +1 -1
  99. package/dist/mcp/index.js +154 -15
  100. package/dist/mcp/index.js.map +1 -1
  101. package/dist/mcp/version-selection.d.ts +55 -0
  102. package/dist/mcp/version-selection.d.ts.map +1 -0
  103. package/dist/mcp/version-selection.js +152 -0
  104. package/dist/mcp/version-selection.js.map +1 -0
  105. package/dist/utils/azure-cli.d.ts +44 -0
  106. package/dist/utils/azure-cli.d.ts.map +1 -0
  107. package/dist/utils/azure-cli.js +164 -0
  108. package/dist/utils/azure-cli.js.map +1 -0
  109. package/dist/utils/semver.d.ts +14 -0
  110. package/dist/utils/semver.d.ts.map +1 -0
  111. package/dist/utils/semver.js +61 -0
  112. package/dist/utils/semver.js.map +1 -0
  113. package/package.json +6 -3
  114. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  115. package/src/__tests__/agent-loop-phase34.test.ts +124 -0
  116. package/src/__tests__/azure-cli.test.ts +199 -0
  117. package/src/__tests__/boundaries.test.ts +42 -0
  118. package/src/__tests__/csharp-schema.test.ts +261 -0
  119. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  120. package/src/__tests__/mcp-version.test.ts +262 -0
  121. package/src/__tests__/mcp.test.ts +64 -3
  122. package/src/__tests__/multi-auth.test.ts +115 -4
  123. package/src/__tests__/plan-apply.test.ts +170 -0
  124. package/src/__tests__/schema-analysis.test.ts +329 -0
  125. package/src/cli/commands/add-auth.ts +36 -6
  126. package/src/cli/commands/configure-mcp.ts +54 -0
  127. package/src/cli/commands/init.ts +23 -8
  128. package/src/cli/commands/migrations.ts +22 -0
  129. package/src/cli/commands/provision.ts +53 -17
  130. package/src/cli/commands/scaffold.ts +105 -3
  131. package/src/cli/index.ts +28 -5
  132. package/src/core/operations/create-model.ts +51 -13
  133. package/src/core/operations/model-migration.ts +52 -0
  134. package/src/core/operations/provision-operations.ts +86 -42
  135. package/src/core/operations/scaffold-apply.ts +2 -0
  136. package/src/core/operations/scaffold-machine.ts +1 -0
  137. package/src/core/operations/scaffold-plan.ts +5 -1
  138. package/src/core/project/agent-templates/AGENTS.md +67 -6
  139. package/src/core/project/agent-templates/CLAUDE.md +7 -1
  140. package/src/core/project/agent-templates/copilot-instructions.md +7 -1
  141. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  142. package/src/core/project/boundaries.ts +62 -20
  143. package/src/core/project/capabilities.ts +32 -5
  144. package/src/core/project/mcp-config.ts +84 -0
  145. package/src/core/project/react-project.ts +1 -1
  146. package/src/core/project/workflows.ts +23 -5
  147. package/src/core/scaffold/auth-generator.ts +18 -19
  148. package/src/core/scaffold/model-parser.ts +413 -129
  149. package/src/core/scaffold/native-schema-generator.ts +313 -78
  150. package/src/core/scaffold/openapi-generator.ts +63 -5
  151. package/src/core/scaffold/ui-generator.ts +70 -8
  152. package/src/core/verify/index.ts +118 -6
  153. package/src/machine/contracts.ts +1 -0
  154. package/src/machine/errors.ts +4 -2
  155. package/src/machine/index.ts +45 -20
  156. package/src/mcp/index.ts +173 -18
  157. package/src/mcp/version-selection.ts +163 -0
  158. package/src/utils/azure-cli.ts +154 -0
  159. package/src/utils/semver.ts +59 -0
@@ -0,0 +1,54 @@
1
+ import { Command } from "commander";
2
+ import { ensureSwallowKitProject } from "../../core/config";
3
+ import {
4
+ MCP_CONFIG_PATH,
5
+ MCP_LATEST,
6
+ lookupPublishedLatestVersion,
7
+ readMcpConfigVersion,
8
+ resolveMcpPinVersion,
9
+ writeMcpConfigVersion,
10
+ } from "../../core/project/mcp-config";
11
+ import { resolveProjectSwallowKit } from "../../mcp/version-selection";
12
+ import { parseSemver } from "../../utils/semver";
13
+ import { getSwallowKitVersion } from "../../version";
14
+
15
+ export interface ConfigureMcpResult {
16
+ previousVersion: string | null;
17
+ version: string;
18
+ reason: string;
19
+ }
20
+
21
+ export function configureMcp(
22
+ options: { version?: string; cwd?: string },
23
+ lookupLatest: (packageName: string) => string | null = lookupPublishedLatestVersion
24
+ ): ConfigureMcpResult {
25
+ const cwd = options.cwd ?? process.cwd();
26
+ const requested = (options.version ?? "auto").trim();
27
+ const previousVersion = readMcpConfigVersion(cwd);
28
+
29
+ let decision: { version: string; reason: string };
30
+ if (requested === "auto") {
31
+ const projectVersion = resolveProjectSwallowKit(cwd)?.version ?? getSwallowKitVersion();
32
+ decision = resolveMcpPinVersion(projectVersion, lookupLatest("swallowkit"));
33
+ } else if (requested === MCP_LATEST) {
34
+ decision = { version: MCP_LATEST, reason: "Explicitly requested latest." };
35
+ } else if (parseSemver(requested)) {
36
+ decision = { version: requested, reason: "Explicitly pinned version." };
37
+ } else {
38
+ throw new Error(`--version must be "auto", "latest", or a semantic version (got "${requested}").`);
39
+ }
40
+
41
+ writeMcpConfigVersion(cwd, decision.version);
42
+ return { previousVersion, ...decision };
43
+ }
44
+
45
+ export const configureMcpCommand = new Command("configure-mcp")
46
+ .description(`Select the SwallowKit MCP version used by ${MCP_CONFIG_PATH} (auto: latest unless the project is newer)`)
47
+ .option("--version <version>", "auto | latest | <semver>", "auto")
48
+ .action((options: { version?: string }) => {
49
+ ensureSwallowKitProject("configure-mcp");
50
+ const result = configureMcp({ version: options.version });
51
+ console.log(`${MCP_CONFIG_PATH}: SWALLOWKIT_MCP_VERSION ${result.previousVersion ?? "(unset)"} -> ${result.version}`);
52
+ console.log(`Reason: ${result.reason}`);
53
+ console.log("Restart the MCP server (or reload your agent) to apply the change.");
54
+ });
@@ -1,4 +1,4 @@
1
- import * as fs from "fs";
1
+ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { spawn, execSync } from "child_process";
4
4
  import prompts from "prompts";
@@ -27,6 +27,7 @@ import {
27
27
  } from "../../core/scaffold/native-schema-generator";
28
28
  import { DEFAULT_AZURE_PYTHON_VERSION } from "../../utils/python-uv";
29
29
  import { buildHomePage } from "../../core/project/home-page";
30
+ import { MCP_LATEST, McpPinDecision, lookupPublishedLatestVersion, resolveMcpPinVersion } from "../../core/project/mcp-config";
30
31
 
31
32
  interface InitOptions {
32
33
  name: string;
@@ -802,7 +803,7 @@ export function ensureNextTailwindPostcssConfig(projectDir: string): void {
802
803
  `);
803
804
  }
804
805
 
805
- export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm'): string {
806
+ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm', mcpVersion: string = MCP_LATEST): string {
806
807
  const { name } = getSwallowKitPackageMetadata();
807
808
  return JSON.stringify(
808
809
  {
@@ -814,7 +815,7 @@ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm
814
815
  : ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
815
816
  cwd: ".",
816
817
  env: {
817
- SWALLOWKIT_MCP_VERSION: "latest",
818
+ SWALLOWKIT_MCP_VERSION: mcpVersion,
818
819
  },
819
820
  },
820
821
  },
@@ -824,6 +825,15 @@ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm
824
825
  );
825
826
  }
826
827
 
828
+ /** latest by default; pinned to this CLI version only when it is newer than published latest. */
829
+ export function selectInitialMcpVersion(
830
+ lookupLatest: (packageName: string) => string | null = lookupPublishedLatestVersion
831
+ ): McpPinDecision {
832
+ const { name, version } = getSwallowKitPackageMetadata();
833
+ if (version === MCP_LATEST) return { version: MCP_LATEST, reason: "SwallowKit package metadata is unavailable." };
834
+ return resolveMcpPinVersion(version, lookupLatest(name));
835
+ }
836
+
827
837
  async function addSwallowKitFiles(
828
838
  projectDir: string,
829
839
  options: InitOptions,
@@ -1204,7 +1214,7 @@ export async function register() {
1204
1214
  createAiAgentFiles(projectDir, projectName, backendLanguage, pm, frontend);
1205
1215
  }
1206
1216
 
1207
- async function createSharedPackage(projectDir: string, projectName: string) {
1217
+ export async function createSharedPackage(projectDir: string, projectName: string) {
1208
1218
  console.log('📦 Creating shared workspace package for Zod models...\n');
1209
1219
 
1210
1220
  const sharedDir = path.join(projectDir, 'shared');
@@ -1244,7 +1254,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
1244
1254
  // shared/index.ts (empty re-export file, scaffold will add entries)
1245
1255
  fs.writeFileSync(
1246
1256
  path.join(sharedDir, 'index.ts'),
1247
- `// Shared Zod models — auto-managed by SwallowKit scaffold command\n// Do not edit the export list below manually\n`
1257
+ `// Shared Zod models. SwallowKit create-model/scaffold append missing model exports.\n// You may add exports manually; existing exports and comments are preserved. No managed markers.\n`
1248
1258
  );
1249
1259
 
1250
1260
  // shared/.gitignore
@@ -1256,7 +1266,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
1256
1266
  console.log('✅ Shared package created\n');
1257
1267
  }
1258
1268
 
1259
- async function createAzureFunctionsProject(
1269
+ export async function createAzureFunctionsProject(
1260
1270
  projectDir: string,
1261
1271
  pm: PackageManager = 'pnpm',
1262
1272
  backendLanguage: BackendLanguage = 'typescript'
@@ -1939,11 +1949,16 @@ function createAiAgentFiles(
1939
1949
  console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
1940
1950
  console.log(' ✅ .github/instructions/ (path-specific instructions)');
1941
1951
 
1952
+ const mcpVersion = selectInitialMcpVersion();
1942
1953
  fs.writeFileSync(
1943
1954
  path.join(projectDir, '.mcp.json'),
1944
- buildSwallowKitMcpProjectConfigSource(pm)
1955
+ buildSwallowKitMcpProjectConfigSource(pm, mcpVersion.version)
1956
+ );
1957
+ console.log(
1958
+ mcpVersion.version === MCP_LATEST
1959
+ ? ' ✅ .mcp.json (project-scoped MCP bootstrap, resolves latest)'
1960
+ : ` ✅ .mcp.json (project-scoped MCP bootstrap, pinned to ${mcpVersion.version}: ${mcpVersion.reason} Run "${runCmd} swallowkit configure-mcp" to re-select later.)`
1945
1961
  );
1946
- console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
1947
1962
 
1948
1963
  writeWorkflowDocs(projectDir, runCmd);
1949
1964
  console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { ensureSwallowKitProject } from "../../core/config";
3
3
  import { initializeLegacyMigrations } from "../../core/operations/migration-generator";
4
+ import { addModelContainerMigration } from "../../core/operations/model-migration";
4
5
 
5
6
  export const migrationsCommand = new Command("migrations")
6
7
  .description("Manage versioned Azure infrastructure migrations");
@@ -18,3 +19,24 @@ migrationsCommand
18
19
  console.log(`Migration baseline 0 created for project ${manifest.projectId}.`);
19
20
  console.log("Commit infra/migrations/manifest.json, then run provision with --adopt-existing --baseline 0.");
20
21
  });
22
+
23
+ migrationsCommand
24
+ .command("add-model")
25
+ .description("Generate only the Cosmos DB container migration for a model (e.g. after scaffold --skip-infra)")
26
+ .argument("<model>", "Model file or model name")
27
+ .option("--dry-run", "Show the planned infra changes without writing anything", false)
28
+ .action(async (model: string, options: { dryRun?: boolean }) => {
29
+ ensureSwallowKitProject("migrations add-model");
30
+ const result = await addModelContainerMigration(model, { dryRun: options.dryRun });
31
+ for (const operation of result.operations) {
32
+ if (operation.action !== "skip") console.log(` ${operation.action.padEnd(7)} ${operation.path}`);
33
+ }
34
+ if (!result.migration) {
35
+ console.log(`No migration needed for ${result.model}: ${result.reason}`);
36
+ } else if (result.operations.every((operation) => operation.action === "skip")) {
37
+ console.log(`Migration ${result.migration.version} for ${result.model} already exists: ${result.migration.template}`);
38
+ } else {
39
+ console.log(`${result.dryRun ? "Would create" : "Created"} migration ${result.migration.version} for ${result.model}: ${result.migration.template}`);
40
+ if (!result.dryRun) console.log("Commit the migration, then run provision to plan and apply it.");
41
+ }
42
+ });
@@ -1,7 +1,27 @@
1
1
  import { Command } from "commander";
2
2
  import prompts from "prompts";
3
3
  import { ensureSwallowKitProject } from "../../core/config";
4
- import { applyProvisionOperation, planProvisionOperation } from "../../core/operations/provision-operations";
4
+ import {
5
+ INTERACTIVE_PROVISION_RETRY_HINT,
6
+ ProvisionPlanData,
7
+ executeProvisionPlan,
8
+ planProvisionOperation,
9
+ } from "../../core/operations/provision-operations";
10
+
11
+ export function describeProvisionAction(plan: Pick<ProvisionPlanData, "mode" | "resourceGroup" | "location" | "pendingMigrations">): string {
12
+ switch (plan.mode) {
13
+ case "bootstrap":
14
+ return `Resource group ${plan.resourceGroup} does not exist yet. It will be created in ${plan.location} and the infrastructure in infra/main.bicep will be deployed.`;
15
+ case "migrate":
16
+ return `${plan.pendingMigrations.length} infrastructure migration(s) will be deployed to ${plan.resourceGroup}: ${plan.pendingMigrations.map((migration) => `${migration.version}-${migration.slug}`).join(", ")}.`;
17
+ case "adopt":
18
+ return `The existing resource group ${plan.resourceGroup} will be recorded as SwallowKit baseline 0. Nothing will be deployed.`;
19
+ case "reconcile":
20
+ return `infra/main.bicep will be redeployed to ${plan.resourceGroup}. Template-managed settings may be reset.`;
21
+ default:
22
+ return `Resource group ${plan.resourceGroup}:`;
23
+ }
24
+ }
5
25
 
6
26
  const PRIMARY_REGIONS = [
7
27
  { title: "Japan East (japaneast)", value: "japaneast" },
@@ -70,16 +90,17 @@ export const provisionCommand = new Command("provision")
70
90
  .option("--subscription <id>", "Azure subscription ID")
71
91
  .option("--location <region>", "Primary location for Functions and Cosmos DB")
72
92
  .option("--swa-location <region>", "Static Web App location")
73
- .option("--what-if", "Include Azure what-if evidence", false)
93
+ .option("--what-if", "Include Azure what-if evidence in the preview", false)
74
94
  .option("--adopt-existing", "Adopt an existing untagged resource group without deploying main.bicep", false)
75
95
  .option("--baseline <version>", "Baseline version for adoption (currently 0)")
76
96
  .option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
77
- .option("--approve", "Apply the displayed plan without an interactive confirmation", false)
97
+ .option("--approve", "Run the displayed Azure commands without the confirmation prompt", false)
78
98
  .action(async (options: ProvisionCliOptions) => {
79
99
  ensureSwallowKitProject("provision");
80
100
  const locations = await resolveLocations(options);
81
101
  const baseline = options.baseline === undefined ? undefined : Number(options.baseline);
82
- const plan = await planProvisionOperation({
102
+ // Preview in memory: the same Azure checks as the agent workflow, but no saved plan file.
103
+ const preview = await planProvisionOperation({
83
104
  resourceGroup: options.resourceGroup,
84
105
  subscription: options.subscription,
85
106
  location: locations.location,
@@ -88,37 +109,52 @@ export const provisionCommand = new Command("provision")
88
109
  adoptExisting: options.adoptExisting,
89
110
  baseline,
90
111
  reconcile: options.reconcile,
91
- });
112
+ }, { persist: false });
92
113
 
93
- console.log(`Provision mode: ${plan.mode}`);
94
- for (const warning of plan.warnings) console.warn(`Warning: ${warning}`);
95
- if (plan.pendingMigrations.length > 0) {
96
- console.log(`Pending migrations: ${plan.pendingMigrations.map((migration) => `${migration.version}-${migration.slug}`).join(", ")}`);
114
+ for (const warning of preview.warnings) console.warn(`Warning: ${warning}`);
115
+
116
+ if (preview.mode === "offline") {
117
+ console.log([
118
+ "",
119
+ "❌ Azure CLI is unavailable, so the current state of Azure could not be checked. Nothing was changed in Azure.",
120
+ " 1. Make sure `az --version` works in this terminal (install: https://aka.ms/azure-cli).",
121
+ " 2. Sign in with `az login` (and `az account set --subscription <id>` if needed).",
122
+ " 3. Run this command again.",
123
+ ].join("\n"));
124
+ process.exitCode = 1;
125
+ return;
97
126
  }
98
- for (const command of plan.commands) console.log(` ${command}`);
99
127
 
100
- if (plan.mode === "adoption-required") {
128
+ if (preview.mode === "adoption-required") {
101
129
  throw new Error('Existing resource group requires: migrations init --legacy-baseline, then provision --adopt-existing --baseline 0 --what-if.');
102
130
  }
103
131
 
132
+ if (preview.mode === "noop") {
133
+ console.log(`Resource group ${preview.resourceGroup} is already up to date. Nothing to do.`);
134
+ return;
135
+ }
136
+
137
+ console.log(`\n${describeProvisionAction(preview)}`);
138
+ console.log("The following Azure CLI commands will run:");
139
+ for (const command of preview.commands) console.log(` ${command}`);
140
+
104
141
  let approved = options.approve === true;
105
142
  if (!approved) {
106
143
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
107
- throw new Error("Non-interactive provision requires --approve after reviewing the plan.");
144
+ throw new Error("Non-interactive provision requires --approve after reviewing the commands above.");
108
145
  }
109
146
  const answer = await prompts({
110
147
  type: "confirm",
111
148
  name: "approved",
112
- message: `Apply the ${plan.mode} plan?`,
149
+ message: "Run these Azure commands? This may create billable resources.",
113
150
  initial: false,
114
151
  });
115
152
  approved = answer.approved === true;
116
153
  }
117
154
  if (!approved) {
118
- console.log("Provision cancelled.");
155
+ console.log("Provision cancelled. Nothing was changed in Azure.");
119
156
  return;
120
157
  }
121
158
 
122
- const result = await applyProvisionOperation({ planId: plan.planId, approve: true });
123
- console.log(`Provision completed (${plan.mode}). ${result.executedCommands.length} command(s) executed.`);
124
- });
159
+ const result = await executeProvisionPlan(preview, { approve: true, retryHint: INTERACTIVE_PROVISION_RETRY_HINT });
160
+ console.log(`✅ Provisioning completed. ${result.executedCommands.length} Azure CLI command(s) ran.`); });
@@ -54,6 +54,7 @@ import {
54
54
  } from "../../core/operations/file-session";
55
55
  import { generateModelContainerMigration } from "../../core/operations/migration-generator";
56
56
  import { recordSessionOperations } from "../../core/project/artifacts";
57
+ import { updateSharedIndex } from "../../core/operations/create-model";
57
58
 
58
59
  const SCAFFOLD_GENERATOR = "scaffold";
59
60
 
@@ -62,9 +63,35 @@ interface ScaffoldOptions {
62
63
  functionsDir?: string; // Azure Functions のディレクトリ(デフォルト: "functions")
63
64
  apiDir?: string; // BFF directory (Next.js: app/api, React: bff)
64
65
  apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
66
+ skipInfra?: boolean; // true の場合、infra/ 配下(コンテナー migration)を変更しない
65
67
  expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
66
68
  }
67
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
+
68
95
  function getMachineAwareStdio(): "inherit" | "pipe" {
69
96
  return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
70
97
  }
@@ -132,6 +159,17 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
132
159
  session.setSchemaAnalysis(schemaAnalysis);
133
160
  console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
134
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
+
135
173
  const backendLanguage = getBackendLanguage();
136
174
  const frontend = getFrontendFramework();
137
175
  const apiDir = options.apiDir || (frontend === "react" ? "bff" : "app/api");
@@ -185,6 +223,8 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
185
223
  console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
186
224
  }
187
225
 
226
+ updateSharedIndex(modelPath, modelInfo.schemaName, session);
227
+
188
228
  // 5. Generate BFF callFunction helper
189
229
  await generateCallFunctionHelper(session, apiDir);
190
230
 
@@ -210,9 +250,15 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
210
250
  await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
211
251
  }
212
252
 
213
- // 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)
214
254
  if (!isConnectorModel) {
215
- await generateCosmosContainer(modelInfo, session);
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
+ }
216
262
  }
217
263
 
218
264
  // 9. Generate UI components (unless --api-only)
@@ -294,10 +340,43 @@ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: Fil
294
340
  export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
295
341
  // モデルに明示的な authPolicy がある場合はそれを使用
296
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
+ }
297
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
+ }
298
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))) {
299
377
  if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
300
378
  }
379
+ if (usesNamedPolicy) assertNamedSchemesConfigured(modelInfo.name, "authPolicy", authConfig);
301
380
  return modelInfo.authPolicy;
302
381
  }
303
382
 
@@ -312,12 +391,35 @@ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig |
312
391
  return {}; // 空のポリシー = 認証のみ、ロール制限なし
313
392
  }
314
393
 
315
- if (defaultPolicy !== 'anonymous') return { policy: defaultPolicy };
394
+ if (defaultPolicy !== 'anonymous') {
395
+ assertNamedSchemesConfigured(modelInfo.name, `auth.authorization.defaultPolicy '${defaultPolicy}'`, authConfig);
396
+ return { policy: defaultPolicy };
397
+ }
316
398
 
317
399
  // defaultPolicy が 'anonymous' なら認証不要
318
400
  return undefined;
319
401
  }
320
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
+
321
423
  /**
322
424
  * モデルファイルのパスを解決
323
425
  */
package/src/cli/index.ts CHANGED
@@ -8,6 +8,7 @@ import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelC
8
8
  import { provisionCommand } from "./commands/provision";
9
9
  import { createMigrationCommand } from "./commands/create-migration";
10
10
  import { migrationsCommand } from "./commands/migrations";
11
+ import { configureMcpCommand } from "./commands/configure-mcp";
11
12
  import { addConnectorCommand } from "./commands/add-connector";
12
13
  import { addAuthCommand } from "./commands/add-auth";
13
14
  import { planScaffoldOperation } from "../core/operations/scaffold-plan";
@@ -15,6 +16,7 @@ import { inspectArtifacts } from "../core/project/artifacts";
15
16
  import { detectDrift } from "../core/project/drift";
16
17
  import { runVerify } from "../core/verify";
17
18
  import { isMachineCommand, runMachineCli } from "../machine";
19
+ import { MachineCommandError } from "../machine/errors";
18
20
 
19
21
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
20
22
  export const CLI_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf8")).version as string;
@@ -140,6 +142,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
140
142
  program.addCommand(provisionCommand);
141
143
  program.addCommand(migrationsCommand);
142
144
  program.addCommand(createMigrationCommand);
145
+ program.addCommand(configureMcpCommand);
143
146
 
144
147
  program
145
148
  .command("create-model <names...>")
@@ -160,6 +163,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
160
163
  .option("--functions-dir <dir>", "Azure Functions directory", "functions")
161
164
  .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
162
165
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
166
+ .option("--skip-infra", "Do not create or update anything under infra/ (generate the container migration later with migrations add-model)", false)
163
167
  .option("--dry-run", "Show planned file changes without writing anything", false)
164
168
  .action(async (model, options) => {
165
169
  if (options.dryRun) {
@@ -168,6 +172,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
168
172
  functionsDir: options.functionsDir,
169
173
  apiDir: options.apiDir,
170
174
  apiOnly: options.apiOnly,
175
+ skipInfra: options.skipInfra,
171
176
  });
172
177
  return;
173
178
  }
@@ -177,6 +182,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
177
182
  functionsDir: options.functionsDir,
178
183
  apiDir: options.apiDir,
179
184
  apiOnly: options.apiOnly,
185
+ skipInfra: options.skipInfra,
180
186
  });
181
187
  });
182
188
 
@@ -217,8 +223,8 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
217
223
 
218
224
  program
219
225
  .command("verify")
220
- .description("Run verification checks (structure, drift, typecheck)")
221
- .option("--checks <ids>", "Comma-separated check ids to run (structure,drift,typecheck)")
226
+ .description("Run verification checks (structure, drift, typecheck, backend-build)")
227
+ .option("--checks <ids>", "Comma-separated check ids (structure,drift,typecheck,backend-build,build,lint,test); quote CSV values in PowerShell")
222
228
  .action(async (options) => {
223
229
  await runVerifyCommand({ checks: options.checks });
224
230
  });
@@ -226,7 +232,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
226
232
  return program;
227
233
  }
228
234
 
229
- async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }): Promise<void> {
235
+ async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }): Promise<void> {
230
236
  console.log("🔍 Scaffold dry-run — no files will be written.\n");
231
237
 
232
238
  const plan = await planScaffoldOperation(options);
@@ -264,7 +270,7 @@ async function runScaffoldDryRun(options: { model: string; functionsDir?: string
264
270
  console.log(`⚠️ ${plan.conflicts.length} file(s) were modified after generation and would be overwritten.`);
265
271
  console.log(` Review the conflicts, then run: swallowkit scaffold ${options.model} (or machine apply scaffold --plan ${plan.planId} --approve)`);
266
272
  } else {
267
- console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}`);
273
+ console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}${options.skipInfra ? " --skip-infra" : ""}`);
268
274
  }
269
275
  }
270
276
 
@@ -351,9 +357,26 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
351
357
  await createProgram().parseAsync(normalizeDevCommandArgv(argv));
352
358
  }
353
359
 
360
+ /** Human-readable error output for regular CLI commands (machine commands print JSON). */
361
+ export function formatCliError(error: unknown): string {
362
+ if (error instanceof MachineCommandError) {
363
+ const lines = [`❌ ${error.message}`, ` (code: ${error.code})`];
364
+ const details = error.details as Record<string, unknown> | undefined;
365
+ if (details && typeof details === "object") {
366
+ if (typeof details.nextAction === "string") lines.push(` Next: ${details.nextAction}`);
367
+ if (Array.isArray(details.changedFiles)) lines.push(` Changed files: ${details.changedFiles.join(", ")}`);
368
+ }
369
+ return lines.join("\n");
370
+ }
371
+ if (error instanceof Error) {
372
+ return process.env.SWALLOWKIT_DEBUG ? String(error.stack ?? error.message) : `❌ ${error.message}`;
373
+ }
374
+ return `❌ ${String(error)}`;
375
+ }
376
+
354
377
  if (require.main === module) {
355
378
  void runCli().catch((error) => {
356
- console.error(error);
379
+ console.error(formatCliError(error));
357
380
  process.exitCode = 1;
358
381
  });
359
382
  }
@@ -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(kebabName: string, pascalName: string, session: FileOperationSession): boolean {
94
+ export function updateSharedIndex(modelPath: string, schemaName: string, session: FileOperationSession): boolean {
94
95
  const indexPath = path.join(process.cwd(), "shared", "index.ts");
95
-
96
- if (!session.fileExists(indexPath)) {
97
- return false;
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 content = session.readFile(indexPath);
101
- const exportLine = `export { ${pascalName} } from './models/${kebabName}';`;
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
- session.writeFile(indexPath, `${content}${exportLine}\n`, {
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(kebabName, pascalName, session)) {
213
+ if (updateSharedIndex(resolveFromRoot(filePath), pascalName, session)) {
176
214
  updatedIndex = true;
177
215
  }
178
216
  }