swallowkit 1.0.3 → 1.1.1
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 +202 -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/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +13 -1
- package/dist/core/operations/scaffold-plan.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__/plan-apply.test.ts +132 -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 +206 -0
- package/src/core/operations/provision-operations.ts +391 -44
- package/src/core/operations/scaffold-plan.ts +17 -1
- 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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { MachineCommandError } from "../../machine/errors";
|
|
5
|
+
import { hashContent } from "../operations/file-session";
|
|
6
|
+
|
|
7
|
+
export const MIGRATION_MANIFEST_VERSION = 1;
|
|
8
|
+
export const MIGRATION_MANIFEST_PATH = path.join("infra", "migrations", "manifest.json");
|
|
9
|
+
|
|
10
|
+
export interface MigrationBaselineFile {
|
|
11
|
+
path: string;
|
|
12
|
+
checksum: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MigrationBaseline {
|
|
16
|
+
version: 0;
|
|
17
|
+
legacy: boolean;
|
|
18
|
+
files: MigrationBaselineFile[];
|
|
19
|
+
checksum: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InfrastructureMigration {
|
|
23
|
+
version: number;
|
|
24
|
+
slug: string;
|
|
25
|
+
template: string;
|
|
26
|
+
parameters?: string;
|
|
27
|
+
source: "model" | "custom";
|
|
28
|
+
sourceModel?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MigrationManifest {
|
|
32
|
+
schemaVersion: 1;
|
|
33
|
+
projectId: string;
|
|
34
|
+
baseline: MigrationBaseline;
|
|
35
|
+
migrations: InfrastructureMigration[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ResolvedMigration extends InfrastructureMigration {
|
|
39
|
+
checksum: string;
|
|
40
|
+
cumulativeChecksum: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MigrationState {
|
|
44
|
+
schemaVersion: 1;
|
|
45
|
+
version: number;
|
|
46
|
+
checksum: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fail(code: string, message: string, details?: unknown): never {
|
|
50
|
+
throw new MachineCommandError(code, message, details, "blocked");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeRelativePath(value: string): string {
|
|
54
|
+
const normalized = value.replace(/\\/g, "/");
|
|
55
|
+
if (path.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
56
|
+
fail("invalid-migration-manifest", `Migration path must stay inside the project: ${value}`);
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readRequiredFile(projectRoot: string, relativePath: string): string {
|
|
62
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
63
|
+
const absolutePath = path.resolve(projectRoot, normalized);
|
|
64
|
+
const rootWithSeparator = `${path.resolve(projectRoot)}${path.sep}`;
|
|
65
|
+
if (!absolutePath.startsWith(rootWithSeparator) || !fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
66
|
+
fail("migration-file-not-found", `Migration file not found: ${normalized}`);
|
|
67
|
+
}
|
|
68
|
+
const realPath = fs.realpathSync(absolutePath);
|
|
69
|
+
if (!realPath.startsWith(rootWithSeparator)) {
|
|
70
|
+
fail("invalid-migration-manifest", `Migration file resolves outside the project: ${normalized}`);
|
|
71
|
+
}
|
|
72
|
+
return fs.readFileSync(absolutePath, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function checksumObject(value: unknown): string {
|
|
76
|
+
return hashContent(JSON.stringify(value));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function computeBaselineChecksum(files: MigrationBaselineFile[]): string {
|
|
80
|
+
return checksumObject([...files].sort((left, right) => left.path.localeCompare(right.path)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function findBaselineDrift(manifest: MigrationManifest, projectRoot: string = process.cwd()): string[] {
|
|
84
|
+
return manifest.baseline.files
|
|
85
|
+
.filter((file) => {
|
|
86
|
+
const absolutePath = path.join(projectRoot, normalizeRelativePath(file.path));
|
|
87
|
+
return !fs.existsSync(absolutePath) || hashContent(fs.readFileSync(absolutePath, "utf-8")) !== file.checksum;
|
|
88
|
+
})
|
|
89
|
+
.map((file) => file.path);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function collectLegacyBaselineFiles(projectRoot: string = process.cwd()): MigrationBaselineFile[] {
|
|
93
|
+
const infraRoot = path.join(projectRoot, "infra");
|
|
94
|
+
if (!fs.existsSync(path.join(infraRoot, "main.bicep"))) {
|
|
95
|
+
fail("infra-not-found", 'infra/main.bicep not found. Run this command from a SwallowKit project.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const files: MigrationBaselineFile[] = [];
|
|
99
|
+
const visit = (directory: string): void => {
|
|
100
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
101
|
+
const absolutePath = path.join(directory, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
if (entry.name !== "migrations") visit(absolutePath);
|
|
104
|
+
} else if (entry.isFile() && (entry.name.endsWith(".bicep") || entry.name.endsWith(".json"))) {
|
|
105
|
+
const relativePath = path.relative(projectRoot, absolutePath).replace(/\\/g, "/");
|
|
106
|
+
files.push({ path: relativePath, checksum: hashContent(fs.readFileSync(absolutePath, "utf-8")) });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
visit(infraRoot);
|
|
111
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createMigrationManifest(
|
|
115
|
+
projectRoot: string = process.cwd(),
|
|
116
|
+
options: { legacy?: boolean; projectId?: string } = {}
|
|
117
|
+
): MigrationManifest {
|
|
118
|
+
const files = collectLegacyBaselineFiles(projectRoot);
|
|
119
|
+
return {
|
|
120
|
+
schemaVersion: MIGRATION_MANIFEST_VERSION,
|
|
121
|
+
projectId: options.projectId ?? crypto.randomUUID(),
|
|
122
|
+
baseline: {
|
|
123
|
+
version: 0,
|
|
124
|
+
legacy: options.legacy ?? false,
|
|
125
|
+
files,
|
|
126
|
+
checksum: computeBaselineChecksum(files),
|
|
127
|
+
},
|
|
128
|
+
migrations: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function saveMigrationManifest(manifest: MigrationManifest, projectRoot: string = process.cwd()): void {
|
|
133
|
+
const manifestPath = path.join(projectRoot, MIGRATION_MANIFEST_PATH);
|
|
134
|
+
if (fs.existsSync(manifestPath)) {
|
|
135
|
+
fail("migration-manifest-exists", `${MIGRATION_MANIFEST_PATH} already exists.`);
|
|
136
|
+
}
|
|
137
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
138
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function replaceMigrationManifest(manifest: MigrationManifest, projectRoot: string = process.cwd()): void {
|
|
142
|
+
validateMigrationManifest(manifest, projectRoot);
|
|
143
|
+
const manifestPath = path.join(projectRoot, MIGRATION_MANIFEST_PATH);
|
|
144
|
+
const temporaryPath = `${manifestPath}.tmp-${process.pid}`;
|
|
145
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
146
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
147
|
+
fs.renameSync(temporaryPath, manifestPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function loadMigrationManifest(projectRoot: string = process.cwd()): MigrationManifest {
|
|
151
|
+
const manifestPath = path.join(projectRoot, MIGRATION_MANIFEST_PATH);
|
|
152
|
+
if (!fs.existsSync(manifestPath)) {
|
|
153
|
+
fail("migration-manifest-not-found", `${MIGRATION_MANIFEST_PATH} not found. Initialize migrations first.`);
|
|
154
|
+
}
|
|
155
|
+
let manifest: MigrationManifest;
|
|
156
|
+
try {
|
|
157
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as MigrationManifest;
|
|
158
|
+
} catch {
|
|
159
|
+
fail("invalid-migration-manifest", `${MIGRATION_MANIFEST_PATH} is not valid JSON.`);
|
|
160
|
+
}
|
|
161
|
+
validateMigrationManifest(manifest, projectRoot);
|
|
162
|
+
return manifest;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function validateMigrationManifest(manifest: MigrationManifest, projectRoot: string = process.cwd()): void {
|
|
166
|
+
if (
|
|
167
|
+
!manifest || typeof manifest !== "object" ||
|
|
168
|
+
manifest.schemaVersion !== MIGRATION_MANIFEST_VERSION ||
|
|
169
|
+
typeof manifest.projectId !== "string" || !manifest.projectId ||
|
|
170
|
+
!manifest.baseline || typeof manifest.baseline !== "object" ||
|
|
171
|
+
manifest.baseline.version !== 0 || typeof manifest.baseline.legacy !== "boolean" ||
|
|
172
|
+
!Array.isArray(manifest.baseline.files) || typeof manifest.baseline.checksum !== "string" ||
|
|
173
|
+
!Array.isArray(manifest.migrations)
|
|
174
|
+
) {
|
|
175
|
+
fail("invalid-migration-manifest", "Unsupported or incomplete migration manifest.");
|
|
176
|
+
}
|
|
177
|
+
for (const file of manifest.baseline.files) {
|
|
178
|
+
if (!file || typeof file.path !== "string" || !/^[a-f0-9]{64}$/.test(file.checksum)) {
|
|
179
|
+
fail("invalid-migration-manifest", "Migration baseline contains an invalid file record.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const baselineFiles = manifest.baseline.files.map((file) => ({
|
|
183
|
+
path: normalizeRelativePath(file.path),
|
|
184
|
+
checksum: file.checksum,
|
|
185
|
+
}));
|
|
186
|
+
if (new Set(baselineFiles.map((file) => file.path)).size !== baselineFiles.length) {
|
|
187
|
+
fail("invalid-migration-manifest", "Migration baseline contains duplicate paths.");
|
|
188
|
+
}
|
|
189
|
+
if (computeBaselineChecksum(baselineFiles) !== manifest.baseline.checksum) {
|
|
190
|
+
fail("migration-checksum-mismatch", "The migration baseline inventory was modified.");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sorted = [...manifest.migrations].sort((left, right) => left.version - right.version);
|
|
194
|
+
for (let index = 0; index < sorted.length; index += 1) {
|
|
195
|
+
const migration = sorted[index];
|
|
196
|
+
if (!migration || !Number.isSafeInteger(migration.version) || typeof migration.slug !== "string" || typeof migration.template !== "string") {
|
|
197
|
+
fail("invalid-migration-manifest", "Migration manifest contains an invalid entry.");
|
|
198
|
+
}
|
|
199
|
+
const expectedVersion = index + 1;
|
|
200
|
+
if (migration.version !== expectedVersion || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(migration.slug)) {
|
|
201
|
+
fail("invalid-migration-manifest", `Migration versions must be contiguous and slugs must be kebab-case (expected version ${expectedVersion}).`);
|
|
202
|
+
}
|
|
203
|
+
if (migration.source !== "model" && migration.source !== "custom") {
|
|
204
|
+
fail("invalid-migration-manifest", `Migration ${migration.version} has an invalid source.`);
|
|
205
|
+
}
|
|
206
|
+
const template = normalizeRelativePath(migration.template);
|
|
207
|
+
if (!template.startsWith("infra/migrations/") || !template.endsWith(".bicep")) {
|
|
208
|
+
fail("invalid-migration-manifest", `Migration template must be a .bicep file under infra/migrations: ${template}`);
|
|
209
|
+
}
|
|
210
|
+
readRequiredFile(projectRoot, template);
|
|
211
|
+
if (migration.parameters) {
|
|
212
|
+
const parameters = normalizeRelativePath(migration.parameters);
|
|
213
|
+
if (!parameters.startsWith("infra/migrations/") || !parameters.endsWith(".json")) {
|
|
214
|
+
fail("invalid-migration-manifest", `Migration parameters must be JSON under infra/migrations: ${parameters}`);
|
|
215
|
+
}
|
|
216
|
+
readRequiredFile(projectRoot, parameters);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function resolveMigrations(manifest: MigrationManifest, projectRoot: string = process.cwd()): ResolvedMigration[] {
|
|
222
|
+
validateMigrationManifest(manifest, projectRoot);
|
|
223
|
+
let cumulativeChecksum = manifest.baseline.checksum;
|
|
224
|
+
return [...manifest.migrations]
|
|
225
|
+
.sort((left, right) => left.version - right.version)
|
|
226
|
+
.map((migration) => {
|
|
227
|
+
const templateContent = readRequiredFile(projectRoot, migration.template);
|
|
228
|
+
const parametersContent = migration.parameters ? readRequiredFile(projectRoot, migration.parameters) : null;
|
|
229
|
+
const checksum = checksumObject({ migration, templateContent, parametersContent });
|
|
230
|
+
cumulativeChecksum = checksumObject({ previous: cumulativeChecksum, version: migration.version, checksum });
|
|
231
|
+
return { ...migration, checksum, cumulativeChecksum };
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function checksumAtVersion(manifest: MigrationManifest, version: number, projectRoot: string = process.cwd()): string {
|
|
236
|
+
if (version === 0) return manifest.baseline.checksum;
|
|
237
|
+
const migration = resolveMigrations(manifest, projectRoot).find((item) => item.version === version);
|
|
238
|
+
if (!migration) fail("migration-version-not-found", `Migration version ${version} does not exist locally.`);
|
|
239
|
+
return migration.cumulativeChecksum;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function getMigrationTagKey(projectId: string): string {
|
|
243
|
+
const projectHash = crypto.createHash("sha256").update(projectId).digest("hex").slice(0, 12);
|
|
244
|
+
return `swallowkit-migration-${projectHash}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function encodeMigrationState(state: MigrationState): string {
|
|
248
|
+
if (state.schemaVersion !== 1 || !Number.isSafeInteger(state.version) || state.version < 0 || !/^[a-f0-9]{64}$/.test(state.checksum)) {
|
|
249
|
+
fail("invalid-migration-state", "Cannot encode an invalid migration state.");
|
|
250
|
+
}
|
|
251
|
+
return `1:${state.version}:${state.checksum}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function decodeMigrationState(value: string): MigrationState {
|
|
255
|
+
const match = /^1:(0|[1-9]\d*):([a-f0-9]{64})$/.exec(value);
|
|
256
|
+
if (!match) fail("invalid-migration-state", "The Azure migration state tag is invalid.");
|
|
257
|
+
return { schemaVersion: 1, version: Number(match[1]), checksum: match[2] };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function selectPendingMigrations(
|
|
261
|
+
manifest: MigrationManifest,
|
|
262
|
+
remoteState: MigrationState,
|
|
263
|
+
projectRoot: string = process.cwd()
|
|
264
|
+
): ResolvedMigration[] {
|
|
265
|
+
const latestVersion = manifest.migrations.length;
|
|
266
|
+
if (remoteState.version > latestVersion) {
|
|
267
|
+
fail("migration-state-ahead", `Azure is at migration ${remoteState.version}, but local latest is ${latestVersion}.`);
|
|
268
|
+
}
|
|
269
|
+
const expectedChecksum = checksumAtVersion(manifest, remoteState.version, projectRoot);
|
|
270
|
+
if (remoteState.checksum !== expectedChecksum) {
|
|
271
|
+
fail("migration-checksum-mismatch", `Azure migration ${remoteState.version} does not match the local history.`);
|
|
272
|
+
}
|
|
273
|
+
return resolveMigrations(manifest, projectRoot).filter((migration) => migration.version > remoteState.version);
|
|
274
|
+
}
|
|
@@ -187,7 +187,7 @@ Stop and ask a human when:
|
|
|
187
187
|
"Provision Azure resources (Static Web Apps, Functions, Cosmos DB) for a SwallowKit project using the two-phase plan/apply flow with a mandatory human approval gate. Use when asked to deploy infrastructure, provision Azure, create resource groups, or run Bicep deployments.",
|
|
188
188
|
content: `# Workflow: Provision Azure resources
|
|
189
189
|
|
|
190
|
-
Goal:
|
|
190
|
+
Goal: bootstrap Azure once, then apply only pending versioned infrastructure migrations with an explicit human approval gate.
|
|
191
191
|
|
|
192
192
|
## Rules
|
|
193
193
|
|
|
@@ -196,6 +196,8 @@ Goal: deploy the Bicep infrastructure with an explicit human approval gate.
|
|
|
196
196
|
- Never pass \`--approve\` to \`apply provision\` unless a human explicitly
|
|
197
197
|
instructed you to provision in this session.
|
|
198
198
|
- Inspect infra locally first — no \`az\` calls are needed for inspection.
|
|
199
|
+
- Never edit an applied migration. Create a corrective migration instead.
|
|
200
|
+
- Never use \`--reconcile\` for an ordinary model or resource addition.
|
|
199
201
|
|
|
200
202
|
## Steps
|
|
201
203
|
|
|
@@ -204,8 +206,10 @@ Goal: deploy the Bicep infrastructure with an explicit human approval gate.
|
|
|
204
206
|
- review parameters, modules, outputs, and container wiring warnings.
|
|
205
207
|
2. Compute the provisioning plan (no resources are created):
|
|
206
208
|
\`${runCmd} swallowkit machine plan provision -g <resource-group> --location <region>\`
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
- the plan reports \`bootstrap\`, \`migrate\`, \`noop\`, or \`adoption-required\`.
|
|
210
|
+
- it lists only pending migrations and the exact \`az\` commands that would run.
|
|
211
|
+
- for an older untagged environment, first run \`${runCmd} swallowkit migrations init --legacy-baseline\`, then plan with \`--adopt-existing --baseline 0 --what-if\`.
|
|
212
|
+
- only intentional baseline changes use \`--reconcile --what-if\`; this can reset template-managed settings.
|
|
209
213
|
3. Present the plan to the human and wait for explicit approval.
|
|
210
214
|
4. Only after approval:
|
|
211
215
|
\`${runCmd} swallowkit machine apply provision --plan <planId> --approve\`
|
package/src/machine/index.ts
CHANGED
|
@@ -366,7 +366,10 @@ function createMachineProgram(): Command {
|
|
|
366
366
|
.requiredOption("--swa-location <region>", "Static Web App location")
|
|
367
367
|
.option("--subscription <id>", "Azure subscription GUID")
|
|
368
368
|
.option("--what-if", "Run az deployment group what-if (requires az login)", false)
|
|
369
|
-
.
|
|
369
|
+
.option("--adopt-existing", "Adopt an existing untagged resource group without deploying main.bicep", false)
|
|
370
|
+
.option("--baseline <version>", "Baseline version for adoption", (value) => Number(value))
|
|
371
|
+
.option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
|
|
372
|
+
.action(async (options: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean }) => {
|
|
370
373
|
await handleMachineAction(
|
|
371
374
|
"plan-provision",
|
|
372
375
|
async () => {
|
|
@@ -377,6 +380,9 @@ function createMachineProgram(): Command {
|
|
|
377
380
|
swaLocation: options.swaLocation,
|
|
378
381
|
subscription: options.subscription,
|
|
379
382
|
whatIf: options.whatIf,
|
|
383
|
+
adoptExisting: options.adoptExisting,
|
|
384
|
+
baseline: options.baseline,
|
|
385
|
+
reconcile: options.reconcile,
|
|
380
386
|
});
|
|
381
387
|
},
|
|
382
388
|
(planData) => ({
|
package/src/mcp/index.ts
CHANGED
|
@@ -352,18 +352,24 @@ export function buildSwallowKitToolDefinitions(
|
|
|
352
352
|
},
|
|
353
353
|
{
|
|
354
354
|
name: "swallowkit_plan_provision",
|
|
355
|
-
description: "
|
|
355
|
+
description: "Plan Azure bootstrap or pending infrastructure migrations. Existing untagged environments require explicit adoption with what-if; full main.bicep redeployment requires reconcile=true. Never deploys and always requires human approval.",
|
|
356
356
|
inputSchema: z.object({
|
|
357
357
|
resourceGroup: z.string(),
|
|
358
358
|
location: z.string(),
|
|
359
359
|
swaLocation: z.string(),
|
|
360
360
|
subscription: z.string().optional(),
|
|
361
361
|
whatIf: z.boolean().optional(),
|
|
362
|
+
adoptExisting: z.boolean().optional(),
|
|
363
|
+
baseline: z.number().int().nonnegative().optional(),
|
|
364
|
+
reconcile: z.boolean().optional(),
|
|
362
365
|
}),
|
|
363
|
-
handler: async ({ resourceGroup, location, swaLocation, subscription, whatIf }: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean }) => {
|
|
366
|
+
handler: async ({ resourceGroup, location, swaLocation, subscription, whatIf, adoptExisting, baseline, reconcile }: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean }) => {
|
|
364
367
|
const args = ["plan", "provision", "--resource-group", resourceGroup, "--location", location, "--swa-location", swaLocation];
|
|
365
368
|
if (subscription) args.push("--subscription", subscription);
|
|
366
369
|
if (whatIf) args.push("--what-if");
|
|
370
|
+
if (adoptExisting) args.push("--adopt-existing");
|
|
371
|
+
if (baseline !== undefined) args.push("--baseline", String(baseline));
|
|
372
|
+
if (reconcile) args.push("--reconcile");
|
|
367
373
|
|
|
368
374
|
const response = await executeMachineCommand(args, runMachineCli);
|
|
369
375
|
return jsonTextContent(withOperationStatus(response));
|