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,206 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { MachineCommandError } from "../../machine/errors";
|
|
4
|
+
import { FileOperationSession } from "./file-session";
|
|
5
|
+
import { recordSessionOperations } from "../project/artifacts";
|
|
6
|
+
import {
|
|
7
|
+
InfrastructureMigration,
|
|
8
|
+
MIGRATION_MANIFEST_PATH,
|
|
9
|
+
MigrationManifest,
|
|
10
|
+
createMigrationManifest,
|
|
11
|
+
findBaselineDrift,
|
|
12
|
+
loadMigrationManifest,
|
|
13
|
+
validateMigrationManifest,
|
|
14
|
+
} from "../project/migrations";
|
|
15
|
+
|
|
16
|
+
const GENERATOR = "infrastructure-migration";
|
|
17
|
+
|
|
18
|
+
function normalizeSlug(value: string): string {
|
|
19
|
+
const slug = value
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
22
|
+
.replace(/[^A-Za-z0-9]+/g, "-")
|
|
23
|
+
.replace(/^-|-$/g, "")
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
if (!slug) {
|
|
26
|
+
throw new MachineCommandError("invalid-migration-slug", "Migration slug must contain letters or numbers.", undefined, "failed");
|
|
27
|
+
}
|
|
28
|
+
return slug;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatVersion(version: number): string {
|
|
32
|
+
return String(version).padStart(4, "0");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readManifestForSession(session: FileOperationSession): MigrationManifest {
|
|
36
|
+
const manifestPath = path.join(session.rootDirectory, MIGRATION_MANIFEST_PATH);
|
|
37
|
+
if (!session.fileExists(manifestPath)) {
|
|
38
|
+
throw new MachineCommandError(
|
|
39
|
+
"migration-manifest-not-found",
|
|
40
|
+
`Migration manifest not found. Run "swallowkit migrations init --legacy-baseline" before scaffolding new models.`,
|
|
41
|
+
undefined,
|
|
42
|
+
"blocked"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const manifest = JSON.parse(session.readFile(manifestPath)) as MigrationManifest;
|
|
47
|
+
validateMigrationManifest(manifest, session.rootDirectory);
|
|
48
|
+
const baselineDrift = findBaselineDrift(manifest, session.rootDirectory);
|
|
49
|
+
if (baselineDrift.length > 0) {
|
|
50
|
+
throw new MachineCommandError(
|
|
51
|
+
"baseline-drift",
|
|
52
|
+
"Baseline infrastructure files changed after migration tracking was initialized. Create an explicit migration or reconcile the baseline first.",
|
|
53
|
+
{ changedFiles: baselineDrift },
|
|
54
|
+
"blocked"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return manifest;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof MachineCommandError) throw error;
|
|
60
|
+
throw new MachineCommandError("invalid-migration-manifest", `${MIGRATION_MANIFEST_PATH} is not valid JSON.`, undefined, "blocked");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function appendMigration(manifest: MigrationManifest, migration: InfrastructureMigration): MigrationManifest {
|
|
65
|
+
return {
|
|
66
|
+
...manifest,
|
|
67
|
+
migrations: [...manifest.migrations, migration].sort((left, right) => left.version - right.version),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function initializeMigrations(projectRoot: string, legacy: boolean): MigrationManifest {
|
|
72
|
+
const manifest = createMigrationManifest(projectRoot, { legacy });
|
|
73
|
+
const session = new FileOperationSession("commit", projectRoot);
|
|
74
|
+
session.writeFile(
|
|
75
|
+
path.join(projectRoot, MIGRATION_MANIFEST_PATH),
|
|
76
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
77
|
+
{ ownership: "metadata", generator: GENERATOR }
|
|
78
|
+
);
|
|
79
|
+
recordSessionOperations(session.operations, {}, projectRoot);
|
|
80
|
+
return manifest;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function initializeLegacyMigrations(projectRoot: string = process.cwd()): MigrationManifest {
|
|
84
|
+
return initializeMigrations(projectRoot, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function initializeNewProjectMigrations(projectRoot: string): MigrationManifest {
|
|
88
|
+
return initializeMigrations(projectRoot, false);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createCustomMigration(slugValue: string, projectRoot: string = process.cwd()): InfrastructureMigration {
|
|
92
|
+
const manifest = loadMigrationManifest(projectRoot);
|
|
93
|
+
const slug = normalizeSlug(slugValue);
|
|
94
|
+
if (manifest.migrations.some((migration) => migration.slug === slug)) {
|
|
95
|
+
throw new MachineCommandError("migration-exists", `Migration slug already exists: ${slug}`, undefined, "blocked");
|
|
96
|
+
}
|
|
97
|
+
const version = manifest.migrations.length + 1;
|
|
98
|
+
const directory = path.join("infra", "migrations", `${formatVersion(version)}-${slug}`);
|
|
99
|
+
const template = path.join(directory, "main.bicep").replace(/\\/g, "/");
|
|
100
|
+
const absoluteTemplate = path.join(projectRoot, template);
|
|
101
|
+
if (fs.existsSync(absoluteTemplate)) {
|
|
102
|
+
throw new MachineCommandError("migration-exists", `Migration path already exists: ${template}`, undefined, "blocked");
|
|
103
|
+
}
|
|
104
|
+
const source = `targetScope = 'resourceGroup'
|
|
105
|
+
|
|
106
|
+
@description('SwallowKit project name')
|
|
107
|
+
param projectName string
|
|
108
|
+
|
|
109
|
+
@description('Azure resource location')
|
|
110
|
+
param location string
|
|
111
|
+
|
|
112
|
+
// Add only new or intentionally updated resources in this migration.
|
|
113
|
+
// Reference existing resources with the Bicep "existing" keyword.
|
|
114
|
+
`;
|
|
115
|
+
fs.mkdirSync(path.dirname(absoluteTemplate), { recursive: true });
|
|
116
|
+
fs.writeFileSync(absoluteTemplate, source, "utf-8");
|
|
117
|
+
|
|
118
|
+
const migration: InfrastructureMigration = { version, slug, template, source: "custom" };
|
|
119
|
+
const manifestPath = path.join(projectRoot, MIGRATION_MANIFEST_PATH);
|
|
120
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(appendMigration(manifest, migration), null, 2)}\n`, "utf-8");
|
|
121
|
+
return migration;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function generateModelContainerMigration(
|
|
125
|
+
model: { name: string; partitionKey: string },
|
|
126
|
+
session: FileOperationSession = new FileOperationSession("commit")
|
|
127
|
+
): InfrastructureMigration {
|
|
128
|
+
const manifest = readManifestForSession(session);
|
|
129
|
+
const existing = manifest.migrations.find((migration) => migration.source === "model" && migration.sourceModel === model.name);
|
|
130
|
+
const slug = normalizeSlug(`create-${model.name}-container`);
|
|
131
|
+
const version = existing?.version ?? manifest.migrations.length + 1;
|
|
132
|
+
const directory = path.join("infra", "migrations", `${formatVersion(version)}-${slug}`);
|
|
133
|
+
const template = (existing?.template ?? path.join(directory, "main.bicep")).replace(/\\/g, "/");
|
|
134
|
+
const containerName = `${model.name}s`;
|
|
135
|
+
const bicep = `targetScope = 'resourceGroup'
|
|
136
|
+
|
|
137
|
+
@description('SwallowKit project name')
|
|
138
|
+
param projectName string
|
|
139
|
+
|
|
140
|
+
@description('Azure resource location')
|
|
141
|
+
param location string
|
|
142
|
+
|
|
143
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
144
|
+
name: 'cosmos-\${projectName}'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
|
|
148
|
+
parent: cosmosAccount
|
|
149
|
+
name: '\${projectName}Database'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = {
|
|
153
|
+
parent: database
|
|
154
|
+
name: '${containerName}'
|
|
155
|
+
properties: {
|
|
156
|
+
resource: {
|
|
157
|
+
id: '${containerName}'
|
|
158
|
+
partitionKey: {
|
|
159
|
+
paths: [
|
|
160
|
+
'${model.partitionKey}'
|
|
161
|
+
]
|
|
162
|
+
kind: 'Hash'
|
|
163
|
+
}
|
|
164
|
+
indexingPolicy: {
|
|
165
|
+
automatic: true
|
|
166
|
+
indexingMode: 'consistent'
|
|
167
|
+
includedPaths: [
|
|
168
|
+
{ path: '/*' }
|
|
169
|
+
]
|
|
170
|
+
excludedPaths: [
|
|
171
|
+
{ path: '/_etag/?' }
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
output containerName string = container.name
|
|
179
|
+
`;
|
|
180
|
+
const absoluteTemplate = path.join(session.rootDirectory, template);
|
|
181
|
+
if (existing && session.fileExists(absoluteTemplate) && session.readFile(absoluteTemplate) !== bicep) {
|
|
182
|
+
throw new MachineCommandError(
|
|
183
|
+
"applied-migration-immutable",
|
|
184
|
+
`The generated migration for model ${model.name} differs from the current schema. Create a new explicit migration instead of changing container properties.`,
|
|
185
|
+
{ version: existing.version, template },
|
|
186
|
+
"blocked"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
session.writeFile(absoluteTemplate, bicep, { ownership: "generated-once", generator: GENERATOR, sourceModel: model.name });
|
|
190
|
+
|
|
191
|
+
const migration: InfrastructureMigration = existing ?? {
|
|
192
|
+
version,
|
|
193
|
+
slug,
|
|
194
|
+
template,
|
|
195
|
+
source: "model",
|
|
196
|
+
sourceModel: model.name,
|
|
197
|
+
};
|
|
198
|
+
if (!existing) {
|
|
199
|
+
session.writeFile(
|
|
200
|
+
path.join(session.rootDirectory, MIGRATION_MANIFEST_PATH),
|
|
201
|
+
`${JSON.stringify(appendMigration(manifest, migration), null, 2)}\n`,
|
|
202
|
+
{ ownership: "metadata", generator: GENERATOR, sourceModel: model.name }
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return migration;
|
|
206
|
+
}
|