swallowkit 1.0.3 → 1.1.0
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/dist/cli/commands/create-migration.d.ts +3 -0
- package/dist/cli/commands/create-migration.d.ts.map +1 -0
- package/dist/cli/commands/create-migration.js +16 -0
- package/dist/cli/commands/create-migration.js.map +1 -0
- package/dist/cli/commands/index.d.ts +2 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +5 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts +3 -0
- package/dist/cli/commands/migrations.d.ts.map +1 -0
- package/dist/cli/commands/migrations.js +22 -0
- package/dist/cli/commands/migrations.js.map +1 -0
- package/dist/cli/commands/provision.d.ts +1 -1
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +95 -185
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +6 -130
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts +10 -0
- package/dist/core/operations/migration-generator.d.ts.map +1 -0
- package/dist/core/operations/migration-generator.js +194 -0
- package/dist/core/operations/migration-generator.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +19 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +299 -30
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/project/capabilities.js +1 -1
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/infra.d.ts +4 -0
- package/dist/core/project/infra.d.ts.map +1 -1
- package/dist/core/project/infra.js +19 -1
- package/dist/core/project/infra.js.map +1 -1
- package/dist/core/project/migrations.d.ts +53 -0
- package/dist/core/project/migrations.d.ts.map +1 -0
- package/dist/core/project/migrations.js +262 -0
- package/dist/core/project/migrations.js.map +1 -0
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +7 -3
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +6 -0
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +11 -2
- package/dist/mcp/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/agent-loop-phase34.test.ts +139 -0
- package/src/__tests__/init.test.ts +8 -0
- package/src/__tests__/migration.test.ts +116 -0
- package/src/__tests__/scaffold.test.ts +7 -4
- package/src/cli/commands/create-migration.ts +13 -0
- package/src/cli/commands/index.ts +2 -0
- package/src/cli/commands/init.ts +2 -0
- package/src/cli/commands/migrations.ts +20 -0
- package/src/cli/commands/provision.ts +115 -184
- package/src/cli/commands/scaffold.ts +6 -145
- package/src/cli/index.ts +4 -0
- package/src/core/operations/migration-generator.ts +188 -0
- package/src/core/operations/provision-operations.ts +391 -44
- package/src/core/project/capabilities.ts +1 -1
- package/src/core/project/infra.ts +23 -1
- package/src/core/project/migrations.ts +274 -0
- package/src/core/project/workflows.ts +7 -3
- package/src/machine/index.ts +7 -1
- package/src/mcp/index.ts +8 -2
|
@@ -1,193 +1,124 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import prompts from "prompts";
|
|
3
|
+
import { ensureSwallowKitProject } from "../../core/config";
|
|
4
|
+
import { applyProvisionOperation, planProvisionOperation } from "../../core/operations/provision-operations";
|
|
5
|
+
|
|
6
|
+
const PRIMARY_REGIONS = [
|
|
7
|
+
{ title: "Japan East (japaneast)", value: "japaneast" },
|
|
8
|
+
{ title: "Japan West (japanwest)", value: "japanwest" },
|
|
9
|
+
{ title: "East Asia (eastasia)", value: "eastasia" },
|
|
10
|
+
{ title: "Southeast Asia (southeastasia)", value: "southeastasia" },
|
|
11
|
+
{ title: "East US (eastus)", value: "eastus" },
|
|
12
|
+
{ title: "East US 2 (eastus2)", value: "eastus2" },
|
|
13
|
+
{ title: "West US 2 (westus2)", value: "westus2" },
|
|
14
|
+
{ title: "Central US (centralus)", value: "centralus" },
|
|
15
|
+
{ title: "West Europe (westeurope)", value: "westeurope" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const SWA_REGIONS = [
|
|
19
|
+
{ title: "East Asia (eastasia) - Recommended for Japan", value: "eastasia" },
|
|
20
|
+
{ title: "West US 2 (westus2)", value: "westus2" },
|
|
21
|
+
{ title: "Central US (centralus)", value: "centralus" },
|
|
22
|
+
{ title: "East US 2 (eastus2)", value: "eastus2" },
|
|
23
|
+
{ title: "West Europe (westeurope)", value: "westeurope" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
interface ProvisionCliOptions {
|
|
27
|
+
resourceGroup: string;
|
|
28
|
+
subscription?: string;
|
|
29
|
+
location?: string;
|
|
30
|
+
swaLocation?: string;
|
|
31
|
+
whatIf?: boolean;
|
|
32
|
+
adoptExisting?: boolean;
|
|
33
|
+
baseline?: string;
|
|
34
|
+
reconcile?: boolean;
|
|
35
|
+
approve?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function resolveLocations(options: ProvisionCliOptions): Promise<{ location: string; swaLocation: string }> {
|
|
39
|
+
if (options.location && options.swaLocation) {
|
|
40
|
+
return { location: options.location, swaLocation: options.swaLocation };
|
|
41
|
+
}
|
|
42
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
43
|
+
throw new Error("Non-interactive provision requires --location and --swa-location.");
|
|
44
|
+
}
|
|
45
|
+
const answer = await prompts([
|
|
46
|
+
{
|
|
47
|
+
type: options.location ? null : "select",
|
|
48
|
+
name: "location",
|
|
49
|
+
message: "Primary location for Functions and Cosmos DB",
|
|
50
|
+
choices: PRIMARY_REGIONS,
|
|
51
|
+
initial: 0,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: options.swaLocation ? null : "select",
|
|
55
|
+
name: "swaLocation",
|
|
56
|
+
message: "Static Web App location",
|
|
57
|
+
choices: SWA_REGIONS,
|
|
58
|
+
initial: 0,
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
const location = options.location ?? answer.location;
|
|
62
|
+
const swaLocation = options.swaLocation ?? answer.swaLocation;
|
|
63
|
+
if (!location || !swaLocation) throw new Error("Region selection cancelled.");
|
|
64
|
+
return { location, swaLocation };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const provisionCommand = new Command("provision")
|
|
68
|
+
.description("Bootstrap, adopt, migrate, or explicitly reconcile Azure resources")
|
|
69
|
+
.requiredOption("-g, --resource-group <name>", "Resource group name")
|
|
70
|
+
.option("--subscription <id>", "Azure subscription ID")
|
|
71
|
+
.option("--location <region>", "Primary location for Functions and Cosmos DB")
|
|
72
|
+
.option("--swa-location <region>", "Static Web App location")
|
|
73
|
+
.option("--what-if", "Include Azure what-if evidence", false)
|
|
74
|
+
.option("--adopt-existing", "Adopt an existing untagged resource group without deploying main.bicep", false)
|
|
75
|
+
.option("--baseline <version>", "Baseline version for adoption (currently 0)")
|
|
76
|
+
.option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
|
|
77
|
+
.option("--approve", "Apply the displayed plan without an interactive confirmation", false)
|
|
78
|
+
.action(async (options: ProvisionCliOptions) => {
|
|
14
79
|
ensureSwallowKitProject("provision");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
80
|
+
const locations = await resolveLocations(options);
|
|
81
|
+
const baseline = options.baseline === undefined ? undefined : Number(options.baseline);
|
|
82
|
+
const plan = await planProvisionOperation({
|
|
83
|
+
resourceGroup: options.resourceGroup,
|
|
84
|
+
subscription: options.subscription,
|
|
85
|
+
location: locations.location,
|
|
86
|
+
swaLocation: locations.swaLocation,
|
|
87
|
+
whatIf: options.whatIf,
|
|
88
|
+
adoptExisting: options.adoptExisting,
|
|
89
|
+
baseline,
|
|
90
|
+
reconcile: options.reconcile,
|
|
91
|
+
});
|
|
92
|
+
|
|
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(", ")}`);
|
|
24
97
|
}
|
|
98
|
+
for (const command of plan.commands) console.log(` ${command}`);
|
|
25
99
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const mainBicepPath = path.join(infraDir, 'main.bicep');
|
|
29
|
-
const parametersPath = path.join(infraDir, 'main.parameters.json');
|
|
30
|
-
|
|
31
|
-
if (!fs.existsSync(mainBicepPath)) {
|
|
32
|
-
console.error('❌ Bicep files not found. Run "swallowkit init" first to generate infrastructure files.');
|
|
33
|
-
process.exit(1);
|
|
100
|
+
if (plan.mode === "adoption-required") {
|
|
101
|
+
throw new Error('Existing resource group requires: migrations init --legacy-baseline, then provision --adopt-existing --baseline 0 --what-if.');
|
|
34
102
|
}
|
|
35
103
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const regionChoices = await prompts([
|
|
41
|
-
{
|
|
42
|
-
type: 'select',
|
|
43
|
-
name: 'primaryLocation',
|
|
44
|
-
message: 'Primary location for Functions and Cosmos DB',
|
|
45
|
-
choices: [
|
|
46
|
-
{ title: 'Japan East (japaneast)', value: 'japaneast' },
|
|
47
|
-
{ title: 'Japan West (japanwest)', value: 'japanwest' },
|
|
48
|
-
{ title: 'East Asia (eastasia)', value: 'eastasia' },
|
|
49
|
-
{ title: 'Southeast Asia (southeastasia)', value: 'southeastasia' },
|
|
50
|
-
{ title: 'East US (eastus)', value: 'eastus' },
|
|
51
|
-
{ title: 'East US 2 (eastus2)', value: 'eastus2' },
|
|
52
|
-
{ title: 'West US 2 (westus2)', value: 'westus2' },
|
|
53
|
-
{ title: 'Central US (centralus)', value: 'centralus' },
|
|
54
|
-
{ title: 'West Europe (westeurope)', value: 'westeurope' },
|
|
55
|
-
],
|
|
56
|
-
initial: 0, // Default to japaneast
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
type: 'select',
|
|
60
|
-
name: 'swaLocation',
|
|
61
|
-
message: 'Static Web App location (limited availability)',
|
|
62
|
-
choices: [
|
|
63
|
-
{ title: 'East Asia (eastasia) - Recommended for Japan', value: 'eastasia' },
|
|
64
|
-
{ title: 'West US 2 (westus2)', value: 'westus2' },
|
|
65
|
-
{ title: 'Central US (centralus)', value: 'centralus' },
|
|
66
|
-
{ title: 'East US 2 (eastus2)', value: 'eastus2' },
|
|
67
|
-
{ title: 'West Europe (westeurope)', value: 'westeurope' },
|
|
68
|
-
],
|
|
69
|
-
initial: 0, // Default to eastasia
|
|
70
|
-
},
|
|
71
|
-
], {
|
|
72
|
-
onCancel: () => {
|
|
73
|
-
throw new Error('User cancelled');
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (!regionChoices.primaryLocation || !regionChoices.swaLocation) {
|
|
78
|
-
console.log('\n❌ Region selection cancelled.');
|
|
79
|
-
process.exit(1);
|
|
104
|
+
let approved = options.approve === true;
|
|
105
|
+
if (!approved) {
|
|
106
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
107
|
+
throw new Error("Non-interactive provision requires --approve after reviewing the plan.");
|
|
80
108
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.log(`✓ Static Web App location: ${swaLocation}`);
|
|
87
|
-
|
|
88
|
-
// Confirmation prompt to prevent accidental wrong selection
|
|
89
|
-
const confirmation = await prompts({
|
|
90
|
-
type: 'confirm',
|
|
91
|
-
name: 'proceed',
|
|
92
|
-
message: `Proceed with deployment to ${primaryLocation} (primary) and ${swaLocation} (SWA)?`,
|
|
93
|
-
initial: true
|
|
94
|
-
}, {
|
|
95
|
-
onCancel: () => {
|
|
96
|
-
throw new Error('User cancelled');
|
|
97
|
-
}
|
|
109
|
+
const answer = await prompts({
|
|
110
|
+
type: "confirm",
|
|
111
|
+
name: "approved",
|
|
112
|
+
message: `Apply the ${plan.mode} plan?`,
|
|
113
|
+
initial: false,
|
|
98
114
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
console.log('');
|
|
106
|
-
// Set subscription if provided
|
|
107
|
-
if (options.subscription) {
|
|
108
|
-
console.log(`🔧 Setting subscription: ${options.subscription}`);
|
|
109
|
-
execSync(`az account set --subscription ${options.subscription}`, { stdio: 'inherit' });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Create resource group if it doesn't exist (use primary location)
|
|
113
|
-
console.log(`🔧 Ensuring resource group exists: ${options.resourceGroup}`);
|
|
114
|
-
execSync(
|
|
115
|
-
`az group create --name ${options.resourceGroup} --location ${primaryLocation}`,
|
|
116
|
-
{ stdio: 'inherit' }
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
// Deploy Bicep template with both locations
|
|
120
|
-
console.log('\n📦 Deploying resources (this may take several minutes)...\n');
|
|
121
|
-
const deployCommand = `az deployment group create --resource-group ${options.resourceGroup} --template-file "${mainBicepPath}" --parameters "${parametersPath}" --parameters location=${primaryLocation} --parameters swaLocation=${swaLocation}`;
|
|
122
|
-
|
|
123
|
-
const output = execSync(deployCommand, { encoding: 'utf-8', stdio: 'pipe' });
|
|
124
|
-
const deployment = JSON.parse(output);
|
|
125
|
-
|
|
126
|
-
// Display outputs
|
|
127
|
-
console.log('\n✅ Deployment completed successfully!\n');
|
|
128
|
-
console.log('📋 Resource Information:');
|
|
129
|
-
|
|
130
|
-
let swaName = '<swa-name>';
|
|
131
|
-
let functionAppName = '<function-name>';
|
|
132
|
-
if (deployment.properties?.outputs) {
|
|
133
|
-
const outputs = deployment.properties.outputs;
|
|
134
|
-
if (outputs.staticWebAppName) {
|
|
135
|
-
swaName = outputs.staticWebAppName.value;
|
|
136
|
-
console.log(` - Static Web App: ${swaName}`);
|
|
137
|
-
console.log(` - URL: https://${outputs.staticWebAppUrl.value}`);
|
|
138
|
-
}
|
|
139
|
-
if (outputs.functionsAppName) {
|
|
140
|
-
functionAppName = outputs.functionsAppName.value;
|
|
141
|
-
console.log(` - Function App: ${functionAppName}`);
|
|
142
|
-
console.log(` - URL: https://${outputs.functionsAppUrl.value}`);
|
|
143
|
-
}
|
|
144
|
-
if (outputs.cosmosDbAccountName) {
|
|
145
|
-
console.log(` - Cosmos DB: ${outputs.cosmosDbAccountName.value}`);
|
|
146
|
-
console.log(` - Database: ${outputs.cosmosDatabaseName.value}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Next steps guidance
|
|
151
|
-
console.log('\n📝 Next Steps:');
|
|
152
|
-
console.log(' 1. Configure CI/CD secrets/variables:\n');
|
|
153
|
-
|
|
154
|
-
// Fetch SWA deployment token
|
|
155
|
-
console.log(' [AZURE_STATIC_WEB_APPS_API_TOKEN]');
|
|
156
|
-
try {
|
|
157
|
-
const swaToken = execSync(
|
|
158
|
-
`az staticwebapp secrets list --name ${swaName} --resource-group ${options.resourceGroup} --query "properties.apiKey" -o tsv`,
|
|
159
|
-
{ encoding: 'utf-8', stdio: 'pipe' }
|
|
160
|
-
).trim();
|
|
161
|
-
console.log(` ${swaToken}\n`);
|
|
162
|
-
} catch {
|
|
163
|
-
console.log(' ⚠️ Failed to retrieve. Run manually:');
|
|
164
|
-
console.log(` az staticwebapp secrets list --name ${swaName} --resource-group ${options.resourceGroup} --query "properties.apiKey" -o tsv\n`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Function App name (already known)
|
|
168
|
-
console.log(' [AZURE_FUNCTIONAPP_NAME]');
|
|
169
|
-
console.log(` ${functionAppName}\n`);
|
|
170
|
-
|
|
171
|
-
// Fetch Function App publish profile
|
|
172
|
-
console.log(' [AZURE_FUNCTIONAPP_PUBLISH_PROFILE]');
|
|
173
|
-
try {
|
|
174
|
-
const publishProfile = execSync(
|
|
175
|
-
`az webapp deployment list-publishing-profiles --name ${functionAppName} --resource-group ${options.resourceGroup} --xml`,
|
|
176
|
-
{ encoding: 'utf-8', stdio: 'pipe' }
|
|
177
|
-
).trim();
|
|
178
|
-
console.log(` ${publishProfile}\n`);
|
|
179
|
-
} catch {
|
|
180
|
-
console.log(' ⚠️ Failed to retrieve. Run manually:');
|
|
181
|
-
console.log(` az webapp deployment list-publishing-profiles --name ${functionAppName} --resource-group ${options.resourceGroup} --xml\n`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
console.log(' 2. Set up your CI/CD pipeline (GitHub Actions or Azure Pipelines)');
|
|
185
|
-
console.log(' 3. Manually trigger the first deployment in your CI/CD pipeline');
|
|
186
|
-
console.log(' (Automatic deployments will run on subsequent pushes)\n');
|
|
187
|
-
|
|
188
|
-
} catch (error: any) {
|
|
189
|
-
console.error('❌ Deployment failed:');
|
|
190
|
-
console.error(error.message);
|
|
191
|
-
process.exit(1);
|
|
115
|
+
approved = answer.approved === true;
|
|
116
|
+
}
|
|
117
|
+
if (!approved) {
|
|
118
|
+
console.log("Provision cancelled.");
|
|
119
|
+
return;
|
|
192
120
|
}
|
|
121
|
+
|
|
122
|
+
const result = await applyProvisionOperation({ planId: plan.planId, approve: true });
|
|
123
|
+
console.log(`Provision completed (${plan.mode}). ${result.executedCommands.length} command(s) executed.`);
|
|
193
124
|
});
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
getActiveFileSession,
|
|
51
51
|
SchemaAnalysisSnapshot,
|
|
52
52
|
} from "../../core/operations/file-session";
|
|
53
|
+
import { generateModelContainerMigration } from "../../core/operations/migration-generator";
|
|
53
54
|
import { recordSessionOperations } from "../../core/project/artifacts";
|
|
54
55
|
|
|
55
56
|
const SCAFFOLD_GENERATOR = "scaffold";
|
|
@@ -842,155 +843,15 @@ async function generateBFFRoutes(
|
|
|
842
843
|
* Generate Cosmos DB container Bicep file
|
|
843
844
|
*/
|
|
844
845
|
export async function generateCosmosContainer(modelInfo: any, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
|
|
845
|
-
console.log("\n🗄️ Generating Cosmos DB container
|
|
846
|
+
console.log("\n🗄️ Generating Cosmos DB container migration...");
|
|
846
847
|
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
const cwd = process.cwd();
|
|
850
|
-
|
|
851
|
-
// Check if infra directory exists
|
|
852
|
-
const infraDir = path.join(cwd, "infra");
|
|
853
|
-
if (!fs.existsSync(infraDir)) {
|
|
848
|
+
const infraDir = path.join(process.cwd(), "infra");
|
|
849
|
+
if (!session.fileExists(infraDir) && !fs.existsSync(infraDir)) {
|
|
854
850
|
console.log("ℹ️ infra directory not found. Skipping Cosmos DB container generation.");
|
|
855
851
|
return;
|
|
856
852
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const containersDir = path.join(infraDir, "containers");
|
|
860
|
-
const containerFileName = `${modelKebab}-container.bicep`;
|
|
861
|
-
const containerFilePath = path.join(containersDir, containerFileName);
|
|
862
|
-
|
|
863
|
-
const bicepContent = `@description('Cosmos DB account name')
|
|
864
|
-
param cosmosAccountName string
|
|
865
|
-
|
|
866
|
-
@description('Database name')
|
|
867
|
-
param databaseName string
|
|
868
|
-
|
|
869
|
-
@description('Container name')
|
|
870
|
-
param containerName string = '${modelPascal}s'
|
|
871
|
-
|
|
872
|
-
@description('Partition key path')
|
|
873
|
-
param partitionKeyPath string = '${modelInfo.partitionKey}'
|
|
874
|
-
|
|
875
|
-
// Reference existing Cosmos DB account
|
|
876
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
877
|
-
name: cosmosAccountName
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Reference existing database
|
|
881
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
|
|
882
|
-
parent: cosmosAccount
|
|
883
|
-
name: databaseName
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// Container for ${modelPascal}
|
|
887
|
-
resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = {
|
|
888
|
-
parent: database
|
|
889
|
-
name: containerName
|
|
890
|
-
properties: {
|
|
891
|
-
resource: {
|
|
892
|
-
id: containerName
|
|
893
|
-
partitionKey: {
|
|
894
|
-
paths: [
|
|
895
|
-
partitionKeyPath
|
|
896
|
-
]
|
|
897
|
-
kind: 'Hash'
|
|
898
|
-
}
|
|
899
|
-
indexingPolicy: {
|
|
900
|
-
automatic: true
|
|
901
|
-
indexingMode: 'consistent'
|
|
902
|
-
includedPaths: [
|
|
903
|
-
{
|
|
904
|
-
path: '/*'
|
|
905
|
-
}
|
|
906
|
-
]
|
|
907
|
-
excludedPaths: [
|
|
908
|
-
{
|
|
909
|
-
path: '/_etag/?'
|
|
910
|
-
}
|
|
911
|
-
]
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
output containerName string = container.name
|
|
918
|
-
`;
|
|
919
|
-
|
|
920
|
-
// Write Bicep file (overwrite if exists)
|
|
921
|
-
session.writeFile(containerFilePath, bicepContent, { ownership: "managed", generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name });
|
|
922
|
-
console.log(`✅ Created: ${containerFilePath}`);
|
|
923
|
-
|
|
924
|
-
// Update main.bicep to include this container module
|
|
925
|
-
await updateMainBicepWithContainer(modelKebab, modelPascal, session);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
/**
|
|
929
|
-
* Update main.bicep to include new container module
|
|
930
|
-
*/
|
|
931
|
-
async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string, session: FileOperationSession): Promise<void> {
|
|
932
|
-
const cwd = process.cwd();
|
|
933
|
-
const mainBicepPath = path.join(cwd, "infra", "main.bicep");
|
|
934
|
-
|
|
935
|
-
if (!session.fileExists(mainBicepPath)) {
|
|
936
|
-
console.log("ℹ️ main.bicep not found. Please manually add the container module.");
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
let mainBicepContent = session.readFile(mainBicepPath);
|
|
941
|
-
|
|
942
|
-
// Check if container module already exists
|
|
943
|
-
const containerModuleName = `${modelPascal.charAt(0).toLowerCase()}${modelPascal.slice(1)}Container`;
|
|
944
|
-
if (mainBicepContent.includes(`module ${containerModuleName}`)) {
|
|
945
|
-
console.log(`ℹ️ Container module '${containerModuleName}' already exists in main.bicep`);
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Find the position to insert the container module (after cosmosDb modules)
|
|
950
|
-
// Look for the end of both cosmosDb modules (FreeTier and Serverless)
|
|
951
|
-
const cosmosModulePattern = /module cosmosDbServerless ['"]modules\/cosmosdb-serverless\.bicep['"] = if \(cosmosDbMode == 'serverless'\) \{[\s\S]*?\n\}/;
|
|
952
|
-
const cosmosModuleMatch = mainBicepContent.match(cosmosModulePattern);
|
|
953
|
-
|
|
954
|
-
if (!cosmosModuleMatch) {
|
|
955
|
-
console.log("⚠️ Could not find Cosmos DB Serverless module in main.bicep. Please manually add the container module:");
|
|
956
|
-
console.log(`\nmodule ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
|
|
957
|
-
name: '${modelKebab}-container'
|
|
958
|
-
params: {
|
|
959
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
960
|
-
databaseName: databaseName
|
|
961
|
-
cosmosDbMode: cosmosDbMode
|
|
962
|
-
}
|
|
963
|
-
dependsOn: [
|
|
964
|
-
cosmosDbFreeTier
|
|
965
|
-
cosmosDbServerless
|
|
966
|
-
]
|
|
967
|
-
}\n`);
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Find the end of the cosmosDbServerless module
|
|
972
|
-
const insertPosition = cosmosModuleMatch.index! + cosmosModuleMatch[0].length;
|
|
973
|
-
|
|
974
|
-
// Create the container module declaration
|
|
975
|
-
const containerModule = `
|
|
976
|
-
|
|
977
|
-
// ${modelPascal} Container
|
|
978
|
-
module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
|
|
979
|
-
name: '${modelKebab}-container'
|
|
980
|
-
params: {
|
|
981
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
982
|
-
databaseName: '\${projectName}Database'
|
|
983
|
-
}
|
|
984
|
-
}`;
|
|
985
|
-
|
|
986
|
-
// Insert the module
|
|
987
|
-
mainBicepContent =
|
|
988
|
-
mainBicepContent.slice(0, insertPosition) +
|
|
989
|
-
containerModule +
|
|
990
|
-
mainBicepContent.slice(insertPosition);
|
|
991
|
-
|
|
992
|
-
session.writeFile(mainBicepPath, mainBicepContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
|
|
993
|
-
console.log(`✅ Added container module to main.bicep`);
|
|
853
|
+
const migration = generateModelContainerMigration(modelInfo, session);
|
|
854
|
+
console.log(`✅ Created migration ${migration.version}: ${migration.template}`);
|
|
994
855
|
}
|
|
995
856
|
|
|
996
857
|
/**
|
package/src/cli/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ import * as path from "path";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelCommand } from "./commands";
|
|
8
8
|
import { provisionCommand } from "./commands/provision";
|
|
9
|
+
import { createMigrationCommand } from "./commands/create-migration";
|
|
10
|
+
import { migrationsCommand } from "./commands/migrations";
|
|
9
11
|
import { addConnectorCommand } from "./commands/add-connector";
|
|
10
12
|
import { addAuthCommand } from "./commands/add-auth";
|
|
11
13
|
import { planScaffoldOperation } from "../core/operations/scaffold-plan";
|
|
@@ -133,6 +135,8 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
133
135
|
program.addCommand(devSeedsCommand);
|
|
134
136
|
|
|
135
137
|
program.addCommand(provisionCommand);
|
|
138
|
+
program.addCommand(migrationsCommand);
|
|
139
|
+
program.addCommand(createMigrationCommand);
|
|
136
140
|
|
|
137
141
|
program
|
|
138
142
|
.command("create-model <names...>")
|