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.
Files changed (77) hide show
  1. package/dist/cli/commands/create-migration.d.ts +3 -0
  2. package/dist/cli/commands/create-migration.d.ts.map +1 -0
  3. package/dist/cli/commands/create-migration.js +16 -0
  4. package/dist/cli/commands/create-migration.js.map +1 -0
  5. package/dist/cli/commands/index.d.ts +2 -0
  6. package/dist/cli/commands/index.d.ts.map +1 -1
  7. package/dist/cli/commands/index.js +5 -1
  8. package/dist/cli/commands/index.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts.map +1 -1
  10. package/dist/cli/commands/init.js +2 -0
  11. package/dist/cli/commands/init.js.map +1 -1
  12. package/dist/cli/commands/migrations.d.ts +3 -0
  13. package/dist/cli/commands/migrations.d.ts.map +1 -0
  14. package/dist/cli/commands/migrations.js +22 -0
  15. package/dist/cli/commands/migrations.js.map +1 -0
  16. package/dist/cli/commands/provision.d.ts +1 -1
  17. package/dist/cli/commands/provision.d.ts.map +1 -1
  18. package/dist/cli/commands/provision.js +95 -185
  19. package/dist/cli/commands/provision.js.map +1 -1
  20. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  21. package/dist/cli/commands/scaffold.js +6 -130
  22. package/dist/cli/commands/scaffold.js.map +1 -1
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +4 -0
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/core/operations/migration-generator.d.ts +10 -0
  27. package/dist/core/operations/migration-generator.d.ts.map +1 -0
  28. package/dist/core/operations/migration-generator.js +202 -0
  29. package/dist/core/operations/migration-generator.js.map +1 -0
  30. package/dist/core/operations/provision-operations.d.ts +19 -0
  31. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  32. package/dist/core/operations/provision-operations.js +299 -30
  33. package/dist/core/operations/provision-operations.js.map +1 -1
  34. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  35. package/dist/core/operations/scaffold-plan.js +13 -1
  36. package/dist/core/operations/scaffold-plan.js.map +1 -1
  37. package/dist/core/project/capabilities.js +1 -1
  38. package/dist/core/project/capabilities.js.map +1 -1
  39. package/dist/core/project/infra.d.ts +4 -0
  40. package/dist/core/project/infra.d.ts.map +1 -1
  41. package/dist/core/project/infra.js +19 -1
  42. package/dist/core/project/infra.js.map +1 -1
  43. package/dist/core/project/migrations.d.ts +53 -0
  44. package/dist/core/project/migrations.d.ts.map +1 -0
  45. package/dist/core/project/migrations.js +262 -0
  46. package/dist/core/project/migrations.js.map +1 -0
  47. package/dist/core/project/workflows.d.ts.map +1 -1
  48. package/dist/core/project/workflows.js +7 -3
  49. package/dist/core/project/workflows.js.map +1 -1
  50. package/dist/machine/index.d.ts.map +1 -1
  51. package/dist/machine/index.js +6 -0
  52. package/dist/machine/index.js.map +1 -1
  53. package/dist/mcp/index.d.ts.map +1 -1
  54. package/dist/mcp/index.js +11 -2
  55. package/dist/mcp/index.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/__tests__/agent-loop-phase34.test.ts +139 -0
  58. package/src/__tests__/init.test.ts +8 -0
  59. package/src/__tests__/migration.test.ts +116 -0
  60. package/src/__tests__/plan-apply.test.ts +132 -0
  61. package/src/__tests__/scaffold.test.ts +7 -4
  62. package/src/cli/commands/create-migration.ts +13 -0
  63. package/src/cli/commands/index.ts +2 -0
  64. package/src/cli/commands/init.ts +2 -0
  65. package/src/cli/commands/migrations.ts +20 -0
  66. package/src/cli/commands/provision.ts +115 -184
  67. package/src/cli/commands/scaffold.ts +6 -145
  68. package/src/cli/index.ts +4 -0
  69. package/src/core/operations/migration-generator.ts +206 -0
  70. package/src/core/operations/provision-operations.ts +391 -44
  71. package/src/core/operations/scaffold-plan.ts +17 -1
  72. package/src/core/project/capabilities.ts +1 -1
  73. package/src/core/project/infra.ts +23 -1
  74. package/src/core/project/migrations.ts +274 -0
  75. package/src/core/project/workflows.ts +7 -3
  76. package/src/machine/index.ts +7 -1
  77. 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
+ }