swallowkit 2.0.0-alpha.1 → 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.
Files changed (224) hide show
  1. package/README.ja.md +14 -0
  2. package/README.md +14 -0
  3. package/dist/cli/commands/add-auth.js +37 -6
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  6. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  7. package/dist/cli/commands/configure-mcp.js +42 -0
  8. package/dist/cli/commands/configure-mcp.js.map +1 -0
  9. package/dist/cli/commands/dev.d.ts +2 -2
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +39 -27
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +8 -2
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +67 -132
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/migrations.d.ts.map +1 -1
  18. package/dist/cli/commands/migrations.js +25 -0
  19. package/dist/cli/commands/migrations.js.map +1 -1
  20. package/dist/cli/commands/provision.d.ts +5 -0
  21. package/dist/cli/commands/provision.d.ts.map +1 -1
  22. package/dist/cli/commands/provision.js +278 -55
  23. package/dist/cli/commands/provision.js.map +1 -1
  24. package/dist/cli/commands/scaffold.d.ts +4 -0
  25. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  26. package/dist/cli/commands/scaffold.js +85 -3
  27. package/dist/cli/commands/scaffold.js.map +1 -1
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +43 -5
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/core/operations/create-model.d.ts +2 -0
  33. package/dist/core/operations/create-model.d.ts.map +1 -1
  34. package/dist/core/operations/create-model.js +60 -8
  35. package/dist/core/operations/create-model.js.map +1 -1
  36. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  37. package/dist/core/operations/migration-generator.js +12 -1
  38. package/dist/core/operations/migration-generator.js.map +1 -1
  39. package/dist/core/operations/model-migration.d.ts +22 -0
  40. package/dist/core/operations/model-migration.d.ts.map +1 -0
  41. package/dist/core/operations/model-migration.js +36 -0
  42. package/dist/core/operations/model-migration.js.map +1 -0
  43. package/dist/core/operations/provision-deployment.d.ts +52 -0
  44. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  45. package/dist/core/operations/provision-deployment.js +164 -0
  46. package/dist/core/operations/provision-deployment.js.map +1 -0
  47. package/dist/core/operations/provision-operations.d.ts +62 -6
  48. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  49. package/dist/core/operations/provision-operations.js +653 -115
  50. package/dist/core/operations/provision-operations.js.map +1 -1
  51. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  52. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  53. package/dist/core/operations/scaffold-apply.js +1 -0
  54. package/dist/core/operations/scaffold-apply.js.map +1 -1
  55. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  56. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  57. package/dist/core/operations/scaffold-machine.js.map +1 -1
  58. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  59. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  60. package/dist/core/operations/scaffold-plan.js +3 -1
  61. package/dist/core/operations/scaffold-plan.js.map +1 -1
  62. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  63. package/dist/core/project/agent-instructions.js +4 -1
  64. package/dist/core/project/agent-instructions.js.map +1 -1
  65. package/dist/core/project/boundaries.d.ts +5 -2
  66. package/dist/core/project/boundaries.d.ts.map +1 -1
  67. package/dist/core/project/boundaries.js +51 -20
  68. package/dist/core/project/boundaries.js.map +1 -1
  69. package/dist/core/project/capabilities.d.ts +10 -0
  70. package/dist/core/project/capabilities.d.ts.map +1 -1
  71. package/dist/core/project/capabilities.js +38 -6
  72. package/dist/core/project/capabilities.js.map +1 -1
  73. package/dist/core/project/home-page.d.ts +4 -0
  74. package/dist/core/project/home-page.d.ts.map +1 -0
  75. package/dist/core/project/home-page.js +113 -0
  76. package/dist/core/project/home-page.js.map +1 -0
  77. package/dist/core/project/mcp-config.d.ts +26 -0
  78. package/dist/core/project/mcp-config.d.ts.map +1 -0
  79. package/dist/core/project/mcp-config.js +117 -0
  80. package/dist/core/project/mcp-config.js.map +1 -0
  81. package/dist/core/project/react-project.d.ts +1 -1
  82. package/dist/core/project/react-project.d.ts.map +1 -1
  83. package/dist/core/project/react-project.js +46 -25
  84. package/dist/core/project/react-project.js.map +1 -1
  85. package/dist/core/project/state.d.ts +7 -0
  86. package/dist/core/project/state.d.ts.map +1 -1
  87. package/dist/core/project/state.js +86 -0
  88. package/dist/core/project/state.js.map +1 -1
  89. package/dist/core/project/workflows.d.ts.map +1 -1
  90. package/dist/core/project/workflows.js +53 -7
  91. package/dist/core/project/workflows.js.map +1 -1
  92. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  93. package/dist/core/scaffold/auth-generator.js +26 -27
  94. package/dist/core/scaffold/auth-generator.js.map +1 -1
  95. package/dist/core/scaffold/model-parser.d.ts +23 -0
  96. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  97. package/dist/core/scaffold/model-parser.js +407 -137
  98. package/dist/core/scaffold/model-parser.js.map +1 -1
  99. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  100. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  101. package/dist/core/scaffold/native-schema-generator.js +294 -91
  102. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  103. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  104. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  105. package/dist/core/scaffold/openapi-generator.js +53 -3
  106. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  107. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  108. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  109. package/dist/core/scaffold/ui-generator.js +68 -7
  110. package/dist/core/scaffold/ui-generator.js.map +1 -1
  111. package/dist/core/verify/index.d.ts +4 -3
  112. package/dist/core/verify/index.d.ts.map +1 -1
  113. package/dist/core/verify/index.js +122 -5
  114. package/dist/core/verify/index.js.map +1 -1
  115. package/dist/machine/contracts.d.ts +1 -0
  116. package/dist/machine/contracts.d.ts.map +1 -1
  117. package/dist/machine/errors.d.ts +3 -2
  118. package/dist/machine/errors.d.ts.map +1 -1
  119. package/dist/machine/errors.js +2 -1
  120. package/dist/machine/errors.js.map +1 -1
  121. package/dist/machine/index.d.ts.map +1 -1
  122. package/dist/machine/index.js +59 -28
  123. package/dist/machine/index.js.map +1 -1
  124. package/dist/mcp/index.d.ts +6 -4
  125. package/dist/mcp/index.d.ts.map +1 -1
  126. package/dist/mcp/index.js +210 -25
  127. package/dist/mcp/index.js.map +1 -1
  128. package/dist/mcp/version-selection.d.ts +55 -0
  129. package/dist/mcp/version-selection.d.ts.map +1 -0
  130. package/dist/mcp/version-selection.js +152 -0
  131. package/dist/mcp/version-selection.js.map +1 -0
  132. package/dist/utils/azure-cli.d.ts +56 -0
  133. package/dist/utils/azure-cli.d.ts.map +1 -0
  134. package/dist/utils/azure-cli.js +326 -0
  135. package/dist/utils/azure-cli.js.map +1 -0
  136. package/dist/utils/azure-diagnostics.d.ts +16 -0
  137. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  138. package/dist/utils/azure-diagnostics.js +68 -0
  139. package/dist/utils/azure-diagnostics.js.map +1 -0
  140. package/dist/utils/package-manager.js +2 -2
  141. package/dist/utils/package-manager.js.map +1 -1
  142. package/dist/utils/python-uv.d.ts +5 -7
  143. package/dist/utils/python-uv.d.ts.map +1 -1
  144. package/dist/utils/python-uv.js +50 -24
  145. package/dist/utils/python-uv.js.map +1 -1
  146. package/dist/utils/react-dev.d.ts +4 -0
  147. package/dist/utils/react-dev.d.ts.map +1 -0
  148. package/dist/utils/react-dev.js +35 -0
  149. package/dist/utils/react-dev.js.map +1 -0
  150. package/dist/utils/semver.d.ts +14 -0
  151. package/dist/utils/semver.d.ts.map +1 -0
  152. package/dist/utils/semver.js +61 -0
  153. package/dist/utils/semver.js.map +1 -0
  154. package/package.json +6 -3
  155. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  156. package/src/__tests__/agent-instructions.test.ts +5 -0
  157. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  158. package/src/__tests__/auth.test.ts +9 -0
  159. package/src/__tests__/azure-cli.test.ts +202 -0
  160. package/src/__tests__/boundaries.test.ts +42 -0
  161. package/src/__tests__/csharp-schema.test.ts +261 -0
  162. package/src/__tests__/dev.test.ts +13 -6
  163. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  164. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  165. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  166. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  167. package/src/__tests__/home-page.test.ts +50 -0
  168. package/src/__tests__/init.test.ts +116 -0
  169. package/src/__tests__/mcp-version.test.ts +262 -0
  170. package/src/__tests__/mcp.test.ts +120 -3
  171. package/src/__tests__/multi-auth.test.ts +115 -4
  172. package/src/__tests__/package-manager.test.ts +17 -2
  173. package/src/__tests__/plan-apply.test.ts +179 -0
  174. package/src/__tests__/provision-runtime.test.ts +722 -0
  175. package/src/__tests__/python-uv.test.ts +50 -14
  176. package/src/__tests__/python-venv.test.ts +48 -0
  177. package/src/__tests__/react-dev.test.ts +72 -0
  178. package/src/__tests__/react.test.ts +19 -0
  179. package/src/__tests__/schema-analysis.test.ts +329 -0
  180. package/src/cli/commands/add-auth.ts +36 -6
  181. package/src/cli/commands/configure-mcp.ts +54 -0
  182. package/src/cli/commands/dev.ts +40 -31
  183. package/src/cli/commands/init.ts +73 -138
  184. package/src/cli/commands/migrations.ts +22 -0
  185. package/src/cli/commands/provision.ts +294 -56
  186. package/src/cli/commands/scaffold.ts +105 -3
  187. package/src/cli/index.ts +40 -7
  188. package/src/core/operations/create-model.ts +51 -13
  189. package/src/core/operations/migration-generator.ts +14 -1
  190. package/src/core/operations/model-migration.ts +52 -0
  191. package/src/core/operations/provision-deployment.ts +183 -0
  192. package/src/core/operations/provision-operations.ts +662 -154
  193. package/src/core/operations/scaffold-apply.ts +2 -0
  194. package/src/core/operations/scaffold-machine.ts +1 -0
  195. package/src/core/operations/scaffold-plan.ts +5 -1
  196. package/src/core/project/agent-instructions.ts +4 -1
  197. package/src/core/project/agent-templates/AGENTS.md +89 -6
  198. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  199. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  200. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  201. package/src/core/project/boundaries.ts +62 -20
  202. package/src/core/project/capabilities.ts +49 -6
  203. package/src/core/project/home-page.ts +114 -0
  204. package/src/core/project/mcp-config.ts +84 -0
  205. package/src/core/project/react-project.ts +50 -28
  206. package/src/core/project/state.ts +70 -0
  207. package/src/core/project/workflows.ts +53 -7
  208. package/src/core/scaffold/auth-generator.ts +26 -27
  209. package/src/core/scaffold/model-parser.ts +413 -129
  210. package/src/core/scaffold/native-schema-generator.ts +319 -103
  211. package/src/core/scaffold/openapi-generator.ts +63 -5
  212. package/src/core/scaffold/ui-generator.ts +70 -8
  213. package/src/core/verify/index.ts +118 -6
  214. package/src/machine/contracts.ts +1 -0
  215. package/src/machine/errors.ts +4 -2
  216. package/src/machine/index.ts +65 -34
  217. package/src/mcp/index.ts +221 -31
  218. package/src/mcp/version-selection.ts +163 -0
  219. package/src/utils/azure-cli.ts +315 -0
  220. package/src/utils/azure-diagnostics.ts +75 -0
  221. package/src/utils/package-manager.ts +2 -2
  222. package/src/utils/python-uv.ts +53 -33
  223. package/src/utils/react-dev.ts +31 -0
  224. package/src/utils/semver.ts +59 -0
package/src/cli/index.ts CHANGED
@@ -5,9 +5,11 @@ import { readFileSync } from "fs";
5
5
  import * as path from "path";
6
6
  import { Command } from "commander";
7
7
  import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelCommand } from "./commands";
8
- import { provisionCommand } from "./commands/provision";
8
+ import { createProvisionCommand } from "./commands/provision";
9
+ import { AzureCliDiagnostic, formatAzureDiagnostic } from "../utils/azure-diagnostics";
9
10
  import { createMigrationCommand } from "./commands/create-migration";
10
11
  import { migrationsCommand } from "./commands/migrations";
12
+ import { configureMcpCommand } from "./commands/configure-mcp";
11
13
  import { addConnectorCommand } from "./commands/add-connector";
12
14
  import { addAuthCommand } from "./commands/add-auth";
13
15
  import { planScaffoldOperation } from "../core/operations/scaffold-plan";
@@ -15,6 +17,7 @@ import { inspectArtifacts } from "../core/project/artifacts";
15
17
  import { detectDrift } from "../core/project/drift";
16
18
  import { runVerify } from "../core/verify";
17
19
  import { isMachineCommand, runMachineCli } from "../machine";
20
+ import { MachineCommandError } from "../machine/errors";
18
21
 
19
22
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
20
23
  export const CLI_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf8")).version as string;
@@ -137,9 +140,10 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
137
140
  program.addCommand(devCommandOverride);
138
141
  program.addCommand(devSeedsCommand);
139
142
 
140
- program.addCommand(provisionCommand);
143
+ program.addCommand(createProvisionCommand());
141
144
  program.addCommand(migrationsCommand);
142
145
  program.addCommand(createMigrationCommand);
146
+ program.addCommand(configureMcpCommand);
143
147
 
144
148
  program
145
149
  .command("create-model <names...>")
@@ -160,6 +164,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
160
164
  .option("--functions-dir <dir>", "Azure Functions directory", "functions")
161
165
  .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
162
166
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
167
+ .option("--skip-infra", "Do not create or update anything under infra/ (generate the container migration later with migrations add-model)", false)
163
168
  .option("--dry-run", "Show planned file changes without writing anything", false)
164
169
  .action(async (model, options) => {
165
170
  if (options.dryRun) {
@@ -168,6 +173,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
168
173
  functionsDir: options.functionsDir,
169
174
  apiDir: options.apiDir,
170
175
  apiOnly: options.apiOnly,
176
+ skipInfra: options.skipInfra,
171
177
  });
172
178
  return;
173
179
  }
@@ -177,6 +183,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
177
183
  functionsDir: options.functionsDir,
178
184
  apiDir: options.apiDir,
179
185
  apiOnly: options.apiOnly,
186
+ skipInfra: options.skipInfra,
180
187
  });
181
188
  });
182
189
 
@@ -217,8 +224,8 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
217
224
 
218
225
  program
219
226
  .command("verify")
220
- .description("Run verification checks (structure, drift, typecheck)")
221
- .option("--checks <ids>", "Comma-separated check ids to run (structure,drift,typecheck)")
227
+ .description("Run verification checks (structure, drift, typecheck, backend-build)")
228
+ .option("--checks <ids>", "Comma-separated check ids (structure,drift,typecheck,backend-build,build,lint,test); quote CSV values in PowerShell")
222
229
  .action(async (options) => {
223
230
  await runVerifyCommand({ checks: options.checks });
224
231
  });
@@ -226,7 +233,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
226
233
  return program;
227
234
  }
228
235
 
229
- async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }): Promise<void> {
236
+ async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }): Promise<void> {
230
237
  console.log("🔍 Scaffold dry-run — no files will be written.\n");
231
238
 
232
239
  const plan = await planScaffoldOperation(options);
@@ -264,7 +271,7 @@ async function runScaffoldDryRun(options: { model: string; functionsDir?: string
264
271
  console.log(`⚠️ ${plan.conflicts.length} file(s) were modified after generation and would be overwritten.`);
265
272
  console.log(` Review the conflicts, then run: swallowkit scaffold ${options.model} (or machine apply scaffold --plan ${plan.planId} --approve)`);
266
273
  } else {
267
- console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}`);
274
+ console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}${options.skipInfra ? " --skip-infra" : ""}`);
268
275
  }
269
276
  }
270
277
 
@@ -351,9 +358,35 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
351
358
  await createProgram().parseAsync(normalizeDevCommandArgv(argv));
352
359
  }
353
360
 
361
+ /** Human-readable error output for regular CLI commands (machine commands print JSON). */
362
+ export function formatCliError(error: unknown): string {
363
+ if (error instanceof MachineCommandError) {
364
+ const lines = [`❌ ${error.message}`, ` (code: ${error.code})`];
365
+ const details = error.details as Record<string, unknown> | undefined;
366
+ if (details && typeof details === "object") {
367
+ if (typeof details.nextAction === "string") lines.push(` Next: ${details.nextAction}`);
368
+ if (Array.isArray(details.changedFiles)) lines.push(` Changed files: ${details.changedFiles.join(", ")}`);
369
+ if (typeof details.runId === "string") lines.push(` Run: ${details.runId}`);
370
+ if (details.deployment && typeof details.deployment === "object") {
371
+ const deployment = details.deployment as { state?: string; reference?: { name?: string; id?: string; resourceGroup?: string; subscription?: string } };
372
+ lines.push(` Deployment: ${deployment.reference?.id ?? deployment.reference?.name ?? "unknown"}; state: ${deployment.state ?? "unknown"}`);
373
+ }
374
+ if (Array.isArray(details.diagnostics)) {
375
+ for (const diagnostic of details.diagnostics as AzureCliDiagnostic[]) lines.push(formatAzureDiagnostic(diagnostic));
376
+ }
377
+ }
378
+ for (const action of error.nextActions ?? []) lines.push(` Next: ${action.command} — ${action.description}`);
379
+ return lines.join("\n");
380
+ }
381
+ if (error instanceof Error) {
382
+ return process.env.SWALLOWKIT_DEBUG ? String(error.stack ?? error.message) : `❌ ${error.message}`;
383
+ }
384
+ return `❌ ${String(error)}`;
385
+ }
386
+
354
387
  if (require.main === module) {
355
388
  void runCli().catch((error) => {
356
- console.error(error);
389
+ console.error(formatCliError(error));
357
390
  process.exitCode = 1;
358
391
  });
359
392
  }
@@ -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
  }
@@ -152,6 +152,19 @@ param location string
152
152
  return migration;
153
153
  }
154
154
 
155
+ const SUFFIXED_COSMOS_ACCOUNT_NAME = "'cosmos-${take(toLower(projectName), 30)}-${take(uniqueString(resourceGroup().id, projectName), 6)}'";
156
+ const LEGACY_COSMOS_ACCOUNT_NAME = "'cosmos-${projectName}'";
157
+
158
+ // Projects initialized before resource-group suffixes keep their original
159
+ // account name so model migrations target the account their baseline created.
160
+ function cosmosAccountNameExpression(session: FileOperationSession): string {
161
+ const mainBicep = path.join(session.rootDirectory, "infra", "main.bicep");
162
+ if (session.fileExists(mainBicep) && /var\s+resourceSuffix\s*=/.test(session.readFile(mainBicep))) {
163
+ return SUFFIXED_COSMOS_ACCOUNT_NAME;
164
+ }
165
+ return LEGACY_COSMOS_ACCOUNT_NAME;
166
+ }
167
+
155
168
  export function generateModelContainerMigration(
156
169
  model: { name: string; partitionKey: string },
157
170
  session: FileOperationSession = new FileOperationSession("commit")
@@ -175,7 +188,7 @@ param projectName string
175
188
  param location string
176
189
 
177
190
  resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
178
- name: 'cosmos-\${projectName}'
191
+ name: ${cosmosAccountNameExpression(session)}
179
192
  }
180
193
 
181
194
  resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Generate only the Cosmos DB container migration for a model. Used after
3
+ * `scaffold --skip-infra`, when infrastructure changes become allowed.
4
+ */
5
+
6
+ import { resolveModelPath } from "../../cli/commands/scaffold";
7
+ import { MachineCommandError } from "../../machine/errors";
8
+ import { parseModelFile } from "../scaffold/model-parser";
9
+ import { recordSessionOperations } from "../project/artifacts";
10
+ import { FileOperationSession } from "./file-session";
11
+ import { generateModelContainerMigration } from "./migration-generator";
12
+
13
+ export interface ModelMigrationResult {
14
+ model: string;
15
+ dryRun: boolean;
16
+ migration: { version: number; template: string } | null;
17
+ reason?: string;
18
+ operations: Array<{ path: string; action: string }>;
19
+ }
20
+
21
+ export async function addModelContainerMigration(
22
+ modelInput: string,
23
+ options: { dryRun?: boolean; projectRoot?: string } = {}
24
+ ): Promise<ModelMigrationResult> {
25
+ const projectRoot = options.projectRoot ?? process.cwd();
26
+ const modelInfo = await parseModelFile(resolveModelPath(modelInput));
27
+ if (modelInfo.connectorConfig) {
28
+ throw new MachineCommandError(
29
+ "connector-model-has-no-container",
30
+ `${modelInfo.name} uses connector ${modelInfo.connectorConfig.connector}; connector models do not get a Cosmos DB container.`,
31
+ undefined,
32
+ "failed"
33
+ );
34
+ }
35
+
36
+ const session = new FileOperationSession(options.dryRun ? "collect" : "commit", projectRoot);
37
+ const migration = generateModelContainerMigration(modelInfo, session);
38
+ if (!options.dryRun) {
39
+ recordSessionOperations(session.operations, {
40
+ schemaHash: modelInfo.semanticFingerprint,
41
+ sourceModel: modelInfo.name,
42
+ }, projectRoot);
43
+ }
44
+
45
+ return {
46
+ model: modelInfo.name,
47
+ dryRun: Boolean(options.dryRun),
48
+ migration: migration ? { version: migration.version, template: migration.template } : null,
49
+ ...(migration ? {} : { reason: "The container is already owned by the legacy migration baseline." }),
50
+ operations: session.operations.map((operation) => ({ path: operation.path, action: operation.action })),
51
+ };
52
+ }
@@ -0,0 +1,183 @@
1
+ import { runAzCliAsync } from "../../utils/azure-cli";
2
+ import { AzureCliDiagnostic, azureCliDiagnostic, redactAzureText } from "../../utils/azure-diagnostics";
3
+
4
+ export const DEPLOYMENT_QUERY = "{id:id,name:name,state:properties.provisioningState,correlationId:properties.correlationId,templateHash:properties.templateHash,timestamp:properties.timestamp,error:properties.error}";
5
+ export const DEFAULT_PROVISION_WAIT_SECONDS = 300;
6
+ export const MAX_PROVISION_WAIT_SECONDS = 3_600;
7
+ /** Consecutive transient state-read failures tolerated while the human CLI waits for a terminal state. */
8
+ export const UNBOUNDED_WAIT_TRANSIENT_RETRIES = 24;
9
+
10
+ export interface DeploymentReference {
11
+ subscription: string;
12
+ resourceGroup: string;
13
+ name: string;
14
+ id?: string;
15
+ correlationId?: string;
16
+ templateHash?: string;
17
+ /** Do not mistake a previous deployment under the same name for this submission. */
18
+ previousCorrelationId?: string;
19
+ }
20
+
21
+ export interface AzureDeploymentError {
22
+ code: string;
23
+ message: string;
24
+ }
25
+
26
+ export interface DeploymentObservation {
27
+ state: "Accepted" | "Running" | "Succeeded" | "Failed" | "Canceled" | "unknown";
28
+ reference: DeploymentReference;
29
+ reason?: "not-found" | "query-failed" | "invalid-response" | "identity-changed" | "not-yet-observed" | "cancelled";
30
+ errors: AzureDeploymentError[];
31
+ diagnostic: AzureCliDiagnostic;
32
+ }
33
+
34
+ function record(value: unknown): Record<string, unknown> | null {
35
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
36
+ }
37
+
38
+ export function extractAzureErrors(value: unknown, secretValues: readonly string[] = []): AzureDeploymentError[] {
39
+ const result: AzureDeploymentError[] = [];
40
+ const visit = (entry: unknown, depth: number) => {
41
+ if (depth > 8 || result.length >= 20) return;
42
+ if (typeof entry === "string") {
43
+ try { visit(JSON.parse(entry), depth + 1); } catch { /* Only structured Azure errors are projected. */ }
44
+ return;
45
+ }
46
+ const item = record(entry);
47
+ if (!item) return;
48
+ if (typeof item.code === "string" && typeof item.message === "string") {
49
+ result.push({ code: redactAzureText(item.code, secretValues).slice(0, 200), message: redactAzureText(item.message, secretValues).slice(0, 2_000) });
50
+ }
51
+ if (item.error) visit(item.error, depth + 1);
52
+ if (Array.isArray(item.details)) item.details.forEach((detail) => visit(detail, depth + 1));
53
+ };
54
+ visit(value, 0);
55
+ return result;
56
+ }
57
+
58
+ export function deploymentId(reference: DeploymentReference): string {
59
+ return reference.id ?? `/subscriptions/${reference.subscription}/resourceGroups/${reference.resourceGroup}/providers/Microsoft.Resources/deployments/${reference.name}`;
60
+ }
61
+
62
+ export async function queryAzureDeployment(
63
+ reference: DeploymentReference,
64
+ options: { timeoutMs?: number; signal?: AbortSignal; secretValues?: readonly string[] } = {}
65
+ ): Promise<DeploymentObservation> {
66
+ const result = await runAzCliAsync([
67
+ "deployment", "group", "show", "--name", reference.name,
68
+ "--resource-group", reference.resourceGroup, "--subscription", reference.subscription,
69
+ "--query", DEPLOYMENT_QUERY, "-o", "json",
70
+ ], { timeout: options.timeoutMs ?? 30_000, signal: options.signal });
71
+ const diagnostic = azureCliDiagnostic(result, "deployment-state", options.secretValues);
72
+ const unknown = (reason: DeploymentObservation["reason"]): DeploymentObservation => ({
73
+ state: "unknown", reference, reason, errors: [], diagnostic,
74
+ });
75
+ if (result.status !== 0 || result.failureKind) {
76
+ if (result.failureKind === "cancelled") return unknown("cancelled");
77
+ return unknown(/DeploymentNotFound|ResourceGroupNotFound/i.test(`${result.stderr}\n${result.stdout}`) ? "not-found" : "query-failed");
78
+ }
79
+ if (result.stdoutTruncated) return unknown("invalid-response");
80
+ let parsed: Record<string, unknown> | null;
81
+ try { parsed = record(JSON.parse(result.stdout)); } catch { return unknown("invalid-response"); }
82
+ if (!parsed || typeof parsed.id !== "string" || typeof parsed.state !== "string" || typeof parsed.correlationId !== "string") {
83
+ return unknown("invalid-response");
84
+ }
85
+ if (parsed.id.toLowerCase() !== deploymentId(reference).toLowerCase()
86
+ || (reference.correlationId && parsed.correlationId !== reference.correlationId)
87
+ || (reference.templateHash && parsed.templateHash !== reference.templateHash)) {
88
+ return unknown("identity-changed");
89
+ }
90
+ if (reference.previousCorrelationId && parsed.correlationId === reference.previousCorrelationId) {
91
+ return unknown("not-yet-observed");
92
+ }
93
+ const states: Record<string, DeploymentObservation["state"]> = {
94
+ accepted: "Accepted", running: "Running", creating: "Running", updating: "Running",
95
+ succeeded: "Succeeded", failed: "Failed", canceled: "Canceled", cancelled: "Canceled",
96
+ };
97
+ const state = states[parsed.state.toLowerCase()];
98
+ if (!state) return unknown("invalid-response");
99
+ return {
100
+ state,
101
+ reference: {
102
+ ...reference,
103
+ id: parsed.id,
104
+ correlationId: parsed.correlationId,
105
+ ...(typeof parsed.templateHash === "string" ? { templateHash: parsed.templateHash } : {}),
106
+ },
107
+ errors: extractAzureErrors(parsed.error, options.secretValues),
108
+ diagnostic,
109
+ };
110
+ }
111
+
112
+ export async function waitForAzureDeployment(
113
+ reference: DeploymentReference,
114
+ options: {
115
+ deadline: number;
116
+ signal?: AbortSignal;
117
+ pollIntervalMs?: number;
118
+ onObservation?: (observation: DeploymentObservation) => void;
119
+ secretValues?: readonly string[];
120
+ queryTimeoutMs?: number;
121
+ /**
122
+ * Consecutive non-terminal "unknown" reads (query failure, not yet visible) to keep polling through.
123
+ * Required for an unbounded deadline so a missing deployment cannot be polled forever.
124
+ */
125
+ transientRetries?: number;
126
+ }
127
+ ): Promise<DeploymentObservation> {
128
+ let observation: DeploymentObservation;
129
+ let unknownReads = 0;
130
+ do {
131
+ const remaining = options.deadline - Date.now();
132
+ // Even when the wait budget is exhausted, one bounded read can identify an accepted submission.
133
+ observation = await queryAzureDeployment(reference, {
134
+ timeoutMs: remaining > 0 ? Math.min(options.queryTimeoutMs ?? 30_000, remaining) : options.queryTimeoutMs ?? 30_000,
135
+ signal: options.signal,
136
+ secretValues: options.secretValues,
137
+ });
138
+ if (observation.state !== "unknown") reference = observation.reference;
139
+ options.onObservation?.(observation);
140
+ if (observation.state === "Succeeded" || observation.state === "Failed" || observation.state === "Canceled") return observation;
141
+ if (observation.state === "unknown") {
142
+ unknownReads += 1;
143
+ const retryable = observation.reason === "not-yet-observed" || observation.reason === "not-found"
144
+ || (options.transientRetries !== undefined && (observation.reason === "query-failed" || observation.reason === "invalid-response"));
145
+ if (!retryable) return observation;
146
+ if (!Number.isFinite(options.deadline) && unknownReads > (options.transientRetries ?? 0)) return observation;
147
+ } else {
148
+ unknownReads = 0;
149
+ }
150
+ if (Date.now() >= options.deadline || options.signal?.aborted) return observation;
151
+ await new Promise<void>((resolve) => {
152
+ const done = () => {
153
+ clearTimeout(timer);
154
+ options.signal?.removeEventListener("abort", done);
155
+ resolve();
156
+ };
157
+ const timer = setTimeout(done, Math.min(options.pollIntervalMs ?? 5_000, Math.max(options.deadline - Date.now(), 0)));
158
+ options.signal?.addEventListener("abort", done, { once: true });
159
+ if (options.signal?.aborted) done();
160
+ });
161
+ } while (!options.signal?.aborted);
162
+ return observation;
163
+ }
164
+
165
+ export async function queryDeploymentOperationErrors(reference: DeploymentReference, secretValues: readonly string[] = [], signal?: AbortSignal): Promise<{
166
+ errors: AzureDeploymentError[];
167
+ diagnostic: AzureCliDiagnostic;
168
+ }> {
169
+ const result = await runAzCliAsync([
170
+ "deployment", "operation", "group", "list",
171
+ "--resource-group", reference.resourceGroup, "--name", reference.name, "--subscription", reference.subscription,
172
+ "--query", "[?properties.provisioningState=='Failed' || properties.provisioningState=='Canceled'].{error:properties.statusMessage}",
173
+ "-o", "json",
174
+ ], { timeout: 30_000, signal });
175
+ const errors: AzureDeploymentError[] = [];
176
+ if (result.status === 0 && !result.failureKind && !result.stdoutTruncated) {
177
+ try {
178
+ const parsed: unknown = JSON.parse(result.stdout);
179
+ if (Array.isArray(parsed)) parsed.slice(0, 20).forEach((item) => errors.push(...extractAzureErrors(item, secretValues)));
180
+ } catch { /* The separate diagnostic explicitly records an unreadable operations response. */ }
181
+ }
182
+ return { errors: errors.slice(0, 20), diagnostic: azureCliDiagnostic(result, "deployment-operations", secretValues) };
183
+ }