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