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.
- package/README.ja.md +14 -0
- package/README.md +14 -0
- package/dist/cli/commands/add-auth.js +37 -6
- 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 +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +8 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +67 -132
- 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 +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +85 -3
- 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 +43 -5
- package/dist/cli/index.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.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +10 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +38 -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/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +46 -25
- package/dist/core/project/react-project.js.map +1 -1
- 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/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/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/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 +59 -28
- 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/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.js +2 -2
- 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 +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +5 -0
- 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 +116 -0
- 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 +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +73 -138
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +40 -7
- 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 +4 -1
- package/src/core/project/agent-templates/AGENTS.md +89 -6
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- 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 +49 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +50 -28
- package/src/core/project/state.ts +70 -0
- 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/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +65 -34
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +2 -2
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- 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 {
|
|
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(
|
|
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
|
|
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(
|
|
94
|
+
export function updateSharedIndex(modelPath: string, schemaName: string, session: FileOperationSession): boolean {
|
|
94
95
|
const indexPath = path.join(process.cwd(), "shared", "index.ts");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
session.registerInput(indexPath);
|
|
97
|
+
const content = session.fileExists(indexPath) ? session.readFile(indexPath) : "";
|
|
98
|
+
const modulePath = `./${path.relative(path.dirname(indexPath), modelPath).replace(/\\/g, "/").replace(/\.ts$/, "")}`;
|
|
99
|
+
const source = ts.createSourceFile(indexPath, content, ts.ScriptTarget.Latest, true);
|
|
100
|
+
// Text edits that turn a type-only re-export of this schema into nothing (a value export of the same
|
|
101
|
+
// name also exports its type); keeping both would be a duplicate-identifier error.
|
|
102
|
+
const edits: Array<{ start: number; end: number; text: string }> = [];
|
|
103
|
+
let hasValueExport = false;
|
|
104
|
+
for (const statement of source.statements) {
|
|
105
|
+
if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
106
|
+
const exportedPath = statement.moduleSpecifier.text.replace(/\.(ts|js)$/, "");
|
|
107
|
+
if (exportedPath !== modulePath) continue;
|
|
108
|
+
if (!statement.exportClause) {
|
|
109
|
+
if (!statement.isTypeOnly) hasValueExport = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!ts.isNamedExports(statement.exportClause)) continue;
|
|
113
|
+
const elements = statement.exportClause.elements;
|
|
114
|
+
const matches = elements.filter((element) =>
|
|
115
|
+
element.name.text === schemaName && (element.propertyName?.text ?? element.name.text) === schemaName);
|
|
116
|
+
for (const element of matches) {
|
|
117
|
+
if (!statement.isTypeOnly && !element.isTypeOnly) {
|
|
118
|
+
hasValueExport = true;
|
|
119
|
+
} else if (!statement.isTypeOnly) {
|
|
120
|
+
// export { type Todo } -> export { Todo }
|
|
121
|
+
edits.push({ start: element.getStart(source), end: element.getEnd(), text: schemaName });
|
|
122
|
+
hasValueExport = true;
|
|
123
|
+
} else if (elements.length === 1) {
|
|
124
|
+
let end = statement.getEnd();
|
|
125
|
+
if (content.slice(end, end + 2) === "\r\n") end += 2;
|
|
126
|
+
else if (content[end] === "\n") end += 1;
|
|
127
|
+
edits.push({ start: statement.getStart(source), end, text: "" });
|
|
128
|
+
} else {
|
|
129
|
+
const index = elements.indexOf(element);
|
|
130
|
+
const start = index === 0 ? element.getStart(source) : elements[index - 1].getEnd();
|
|
131
|
+
const end = index === 0 ? elements[index + 1].getStart(source) : element.getEnd();
|
|
132
|
+
edits.push({ start, end, text: "" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
98
135
|
}
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (content.includes(exportLine)) {
|
|
104
|
-
return false;
|
|
136
|
+
let updated = content;
|
|
137
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
138
|
+
updated = `${updated.slice(0, edit.start)}${edit.text}${updated.slice(edit.end)}`;
|
|
105
139
|
}
|
|
106
|
-
|
|
107
|
-
|
|
140
|
+
if (hasValueExport && updated === content) return false;
|
|
141
|
+
if (!hasValueExport) {
|
|
142
|
+
const exportLine = `export { ${schemaName} } from '${modulePath}';`;
|
|
143
|
+
updated = `${updated}${updated && !updated.endsWith("\n") ? "\n" : ""}${exportLine}\n`;
|
|
144
|
+
}
|
|
145
|
+
session.writeFile(indexPath, updated, {
|
|
108
146
|
ownership: "extension-point",
|
|
109
147
|
generator: CREATE_MODEL_GENERATOR,
|
|
110
148
|
});
|
|
@@ -172,7 +210,7 @@ export async function createModelOperation(options: CreateModelOperationOptions)
|
|
|
172
210
|
});
|
|
173
211
|
createdFiles.push(filePath.replace(/\\/g, "/"));
|
|
174
212
|
|
|
175
|
-
if (updateSharedIndex(
|
|
213
|
+
if (updateSharedIndex(resolveFromRoot(filePath), pascalName, session)) {
|
|
176
214
|
updatedIndex = true;
|
|
177
215
|
}
|
|
178
216
|
}
|
|
@@ -152,6 +152,19 @@ param location string
|
|
|
152
152
|
return migration;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
const SUFFIXED_COSMOS_ACCOUNT_NAME = "'cosmos-${take(toLower(projectName), 30)}-${take(uniqueString(resourceGroup().id, projectName), 6)}'";
|
|
156
|
+
const LEGACY_COSMOS_ACCOUNT_NAME = "'cosmos-${projectName}'";
|
|
157
|
+
|
|
158
|
+
// Projects initialized before resource-group suffixes keep their original
|
|
159
|
+
// account name so model migrations target the account their baseline created.
|
|
160
|
+
function cosmosAccountNameExpression(session: FileOperationSession): string {
|
|
161
|
+
const mainBicep = path.join(session.rootDirectory, "infra", "main.bicep");
|
|
162
|
+
if (session.fileExists(mainBicep) && /var\s+resourceSuffix\s*=/.test(session.readFile(mainBicep))) {
|
|
163
|
+
return SUFFIXED_COSMOS_ACCOUNT_NAME;
|
|
164
|
+
}
|
|
165
|
+
return LEGACY_COSMOS_ACCOUNT_NAME;
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
export function generateModelContainerMigration(
|
|
156
169
|
model: { name: string; partitionKey: string },
|
|
157
170
|
session: FileOperationSession = new FileOperationSession("commit")
|
|
@@ -175,7 +188,7 @@ param projectName string
|
|
|
175
188
|
param location string
|
|
176
189
|
|
|
177
190
|
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
178
|
-
name:
|
|
191
|
+
name: ${cosmosAccountNameExpression(session)}
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
|
|
@@ -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
|
+
}
|