@kapeta/local-cluster-service 0.54.3 → 0.54.5

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/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [0.54.5](https://github.com/kapetacom/local-cluster-service/compare/v0.54.4...v0.54.5) (2024-06-21)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * enable filesToBeFixed inclusion list in codefix ([4eaf272](https://github.com/kapetacom/local-cluster-service/commit/4eaf2720bc49b5709c170f88a85614b1a1a8bb8b))
7
+ * forward FILE_FAILED events from streams ([fc22b68](https://github.com/kapetacom/local-cluster-service/commit/fc22b68e129546166c117ea5d13efeebf4bd9d72))
8
+
9
+ ## [0.54.4](https://github.com/kapetacom/local-cluster-service/compare/v0.54.3...v0.54.4) (2024-06-20)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * merge error ([4c29cab](https://github.com/kapetacom/local-cluster-service/commit/4c29cab33c311a3ad21913b974d92207968a15db))
15
+ * minors ([c36e825](https://github.com/kapetacom/local-cluster-service/commit/c36e8258cf0d7a3fb0915080f64d524ffd925929))
16
+ * processTemplate will possibly return a list of files for each file when enhancing services ([3f186a3](https://github.com/kapetacom/local-cluster-service/commit/3f186a3afbb6b527c933ff051d2659e07ab39bbd))
17
+
1
18
  ## [0.54.3](https://github.com/kapetacom/local-cluster-service/compare/v0.54.2...v0.54.3) (2024-06-19)
2
19
 
3
20
 
@@ -117,16 +117,13 @@ class StormCodegen {
117
117
  getStream() {
118
118
  return this.out;
119
119
  }
120
- handleTemplateFileOutput(blockUri, aiName, template, data) {
120
+ handleTemplateFileOutput(blockUri, aiName, data) {
121
121
  if (this.handleFileEvents(blockUri, aiName, data)) {
122
122
  return;
123
123
  }
124
124
  switch (data.type) {
125
125
  case 'FILE_DONE':
126
- template.filename = data.payload.filename;
127
- template.content = data.payload.content;
128
- this.handleFileDoneOutput(blockUri, aiName, data);
129
- break;
126
+ return this.handleFileDoneOutput(blockUri, aiName, data);
130
127
  }
131
128
  }
132
129
  handleUiOutput(blockUri, blockName, data) {
@@ -198,6 +195,17 @@ class StormCodegen {
198
195
  },
199
196
  });
200
197
  return true;
198
+ case 'FILE_FAILED':
199
+ this.out.emit('data', {
200
+ ...data,
201
+ payload: {
202
+ ...data.payload,
203
+ path: (0, path_2.join)(basePath, data.payload.filename),
204
+ blockName,
205
+ blockRef,
206
+ instanceId,
207
+ },
208
+ });
201
209
  }
202
210
  return false;
203
211
  }
@@ -229,8 +237,10 @@ class StormCodegen {
229
237
  if (this.isAborted()) {
230
238
  return;
231
239
  }
240
+ const kapetaYaml = yaml_1.default.stringify(block.content);
241
+ const blockDefinition = block.content;
232
242
  // Generate the code for the block using the standard codegen templates
233
- const generatedResult = await this.generateBlock(block.content);
243
+ const generatedResult = await this.generateBlock(blockDefinition);
234
244
  if (!generatedResult) {
235
245
  return;
236
246
  }
@@ -321,7 +331,7 @@ class StormCodegen {
321
331
  if (this.isAborted()) {
322
332
  return;
323
333
  }
324
- const basePath = this.getBasePath(block.content.metadata.name);
334
+ const basePath = this.getBasePath(blockDefinition.metadata.name);
325
335
  const screenFilesConverted = screenFiles.map((screenFile) => {
326
336
  return {
327
337
  filename: screenFile.payload.filename,
@@ -342,7 +352,7 @@ class StormCodegen {
342
352
  };
343
353
  const stream = await stormClient_1.stormClient.generateCode(payload);
344
354
  stream.on('data', (evt) => {
345
- this.handleTemplateFileOutput(blockUri, block.aiName, webRouter, evt);
355
+ this.handleTemplateFileOutput(blockUri, block.aiName, evt);
346
356
  });
347
357
  this.out.on('aborted', () => {
348
358
  stream.abort();
@@ -352,36 +362,37 @@ class StormCodegen {
352
362
  // Gather the context files for implementation. These will be all be passed to the AI
353
363
  const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN, codegen_1.AIFileTypes.WEB_ROUTER].includes(file.type));
354
364
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
355
- const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
365
+ let serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
356
366
  if (serviceFiles.length > 0) {
357
- await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
367
+ serviceFiles = await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
358
368
  }
359
369
  if (this.isAborted()) {
360
370
  return;
361
371
  }
372
+ for (const contextFile of contextFiles) {
373
+ const filePath = (0, path_2.join)(basePath, contextFile.filename);
374
+ await (0, promises_1.writeFile)(filePath, contextFile.content);
375
+ }
362
376
  for (const screenFile of screenFilesConverted) {
363
377
  const filePath = (0, path_2.join)(basePath, screenFile.filename);
364
378
  await (0, promises_1.writeFile)(filePath, screenFile.content);
365
379
  }
380
+ // this might decide to overwrite a file that is also a context file
366
381
  for (const serviceFile of serviceFiles) {
367
382
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
368
383
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
369
384
  }
370
- for (const serviceFile of contextFiles) {
371
- const filePath = (0, path_2.join)(basePath, serviceFile.filename);
372
- await (0, promises_1.writeFile)(filePath, serviceFile.content);
373
- }
374
385
  // Write again after modifications
375
386
  for (const webRouterFile of webRouters) {
376
387
  const filePath = (0, path_2.join)(basePath, webRouterFile.filename);
377
388
  await (0, promises_1.writeFile)(filePath, webRouterFile.content);
378
389
  }
379
390
  const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
380
- await (0, promises_1.writeFile)(kapetaYmlPath, yaml_1.default.stringify(block.content));
391
+ await (0, promises_1.writeFile)(kapetaYmlPath, kapetaYaml);
381
392
  const blockRef = block.uri;
382
393
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
383
394
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
384
- const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
395
+ const codeGenerator = new codegen_1.BlockCodeGenerator(blockDefinition);
385
396
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.BUILDING);
386
397
  await this.verifyAndFixCode(blockUri, block.aiName, codeGenerator, basePath, filesToBeFixed, allFiles);
387
398
  this.out.emit('data', {
@@ -428,8 +439,9 @@ class StormCodegen {
428
439
  if (errors.size > 0) {
429
440
  this.emitBlockStatus(blockUri, blockName, events_1.StormEventBlockStatusType.FIXING);
430
441
  const promises = Array.from(errors.entries()).map(([filename, fileErrors]) => {
431
- // todo: only try to fix file if it is part of filesToBeFixed
432
- return this.tryToFixFile(blockUri, blockName, basePath, filename, fileErrors, allFiles, codeGenerator);
442
+ if (filesToBeFixed.some((file) => file.filename === filename)) {
443
+ return this.tryToFixFile(blockUri, blockName, basePath, filename, fileErrors, allFiles, codeGenerator);
444
+ }
433
445
  });
434
446
  await Promise.all(promises);
435
447
  }
@@ -676,6 +688,7 @@ class StormCodegen {
676
688
  */
677
689
  async processTemplates(blockUri, aiName, generator, templates, contextFiles) {
678
690
  const promises = templates.map(async (templateFile) => {
691
+ let changedFiles = [];
679
692
  const stream = await generator({
680
693
  context: contextFiles,
681
694
  template: templateFile,
@@ -685,11 +698,28 @@ class StormCodegen {
685
698
  stream.abort();
686
699
  });
687
700
  stream.on('data', (evt) => {
688
- this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
701
+ let changedFile = this.handleTemplateFileOutput(blockUri, aiName, evt);
702
+ if (changedFile) {
703
+ changedFiles.push(...changedFiles, changedFile);
704
+ }
689
705
  });
690
706
  await stream.waitForDone();
707
+ return changedFiles;
691
708
  });
692
- await Promise.all(promises);
709
+ const changedFiles = await Promise.all(promises);
710
+ let allFiles = templates.concat(contextFiles);
711
+ let result = [];
712
+ for (const changedFile of changedFiles.flat()) {
713
+ const find = allFiles.find((file) => file.filename === changedFile.payload.filename);
714
+ if (find) {
715
+ result.push({ type: find.type, filename: find.filename, mode: find.mode, permissions: find.permissions,
716
+ content: changedFile.payload.content });
717
+ }
718
+ else {
719
+ console.warn("processTemplates: AI changed a file that wasn't in the input [" + changedFile.payload.filename + "]");
720
+ }
721
+ }
722
+ return result;
693
723
  }
694
724
  /**
695
725
  * Converts the generated files to a format that can be sent to the AI
@@ -200,6 +200,12 @@ export interface StormEventFileDone extends StormEventFileBase {
200
200
  content: string;
201
201
  };
202
202
  }
203
+ export interface StormEventFileFailed extends StormEventFileBase {
204
+ type: 'FILE_FAILED';
205
+ payload: StormEventFileBasePayload & {
206
+ error: string;
207
+ };
208
+ }
203
209
  export interface StormEventFileChunk extends StormEventFileBase {
204
210
  type: 'FILE_CHUNK';
205
211
  payload: StormEventFileBasePayload & {
@@ -259,4 +265,4 @@ export interface StormEventPhases {
259
265
  phaseType: StormEventPhaseType;
260
266
  };
261
267
  }
262
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry;
268
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry;
@@ -117,16 +117,13 @@ class StormCodegen {
117
117
  getStream() {
118
118
  return this.out;
119
119
  }
120
- handleTemplateFileOutput(blockUri, aiName, template, data) {
120
+ handleTemplateFileOutput(blockUri, aiName, data) {
121
121
  if (this.handleFileEvents(blockUri, aiName, data)) {
122
122
  return;
123
123
  }
124
124
  switch (data.type) {
125
125
  case 'FILE_DONE':
126
- template.filename = data.payload.filename;
127
- template.content = data.payload.content;
128
- this.handleFileDoneOutput(blockUri, aiName, data);
129
- break;
126
+ return this.handleFileDoneOutput(blockUri, aiName, data);
130
127
  }
131
128
  }
132
129
  handleUiOutput(blockUri, blockName, data) {
@@ -198,6 +195,17 @@ class StormCodegen {
198
195
  },
199
196
  });
200
197
  return true;
198
+ case 'FILE_FAILED':
199
+ this.out.emit('data', {
200
+ ...data,
201
+ payload: {
202
+ ...data.payload,
203
+ path: (0, path_2.join)(basePath, data.payload.filename),
204
+ blockName,
205
+ blockRef,
206
+ instanceId,
207
+ },
208
+ });
201
209
  }
202
210
  return false;
203
211
  }
@@ -229,8 +237,10 @@ class StormCodegen {
229
237
  if (this.isAborted()) {
230
238
  return;
231
239
  }
240
+ const kapetaYaml = yaml_1.default.stringify(block.content);
241
+ const blockDefinition = block.content;
232
242
  // Generate the code for the block using the standard codegen templates
233
- const generatedResult = await this.generateBlock(block.content);
243
+ const generatedResult = await this.generateBlock(blockDefinition);
234
244
  if (!generatedResult) {
235
245
  return;
236
246
  }
@@ -321,7 +331,7 @@ class StormCodegen {
321
331
  if (this.isAborted()) {
322
332
  return;
323
333
  }
324
- const basePath = this.getBasePath(block.content.metadata.name);
334
+ const basePath = this.getBasePath(blockDefinition.metadata.name);
325
335
  const screenFilesConverted = screenFiles.map((screenFile) => {
326
336
  return {
327
337
  filename: screenFile.payload.filename,
@@ -342,7 +352,7 @@ class StormCodegen {
342
352
  };
343
353
  const stream = await stormClient_1.stormClient.generateCode(payload);
344
354
  stream.on('data', (evt) => {
345
- this.handleTemplateFileOutput(blockUri, block.aiName, webRouter, evt);
355
+ this.handleTemplateFileOutput(blockUri, block.aiName, evt);
346
356
  });
347
357
  this.out.on('aborted', () => {
348
358
  stream.abort();
@@ -352,36 +362,37 @@ class StormCodegen {
352
362
  // Gather the context files for implementation. These will be all be passed to the AI
353
363
  const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN, codegen_1.AIFileTypes.WEB_ROUTER].includes(file.type));
354
364
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
355
- const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
365
+ let serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
356
366
  if (serviceFiles.length > 0) {
357
- await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
367
+ serviceFiles = await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
358
368
  }
359
369
  if (this.isAborted()) {
360
370
  return;
361
371
  }
372
+ for (const contextFile of contextFiles) {
373
+ const filePath = (0, path_2.join)(basePath, contextFile.filename);
374
+ await (0, promises_1.writeFile)(filePath, contextFile.content);
375
+ }
362
376
  for (const screenFile of screenFilesConverted) {
363
377
  const filePath = (0, path_2.join)(basePath, screenFile.filename);
364
378
  await (0, promises_1.writeFile)(filePath, screenFile.content);
365
379
  }
380
+ // this might decide to overwrite a file that is also a context file
366
381
  for (const serviceFile of serviceFiles) {
367
382
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
368
383
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
369
384
  }
370
- for (const serviceFile of contextFiles) {
371
- const filePath = (0, path_2.join)(basePath, serviceFile.filename);
372
- await (0, promises_1.writeFile)(filePath, serviceFile.content);
373
- }
374
385
  // Write again after modifications
375
386
  for (const webRouterFile of webRouters) {
376
387
  const filePath = (0, path_2.join)(basePath, webRouterFile.filename);
377
388
  await (0, promises_1.writeFile)(filePath, webRouterFile.content);
378
389
  }
379
390
  const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
380
- await (0, promises_1.writeFile)(kapetaYmlPath, yaml_1.default.stringify(block.content));
391
+ await (0, promises_1.writeFile)(kapetaYmlPath, kapetaYaml);
381
392
  const blockRef = block.uri;
382
393
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
383
394
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
384
- const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
395
+ const codeGenerator = new codegen_1.BlockCodeGenerator(blockDefinition);
385
396
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.BUILDING);
386
397
  await this.verifyAndFixCode(blockUri, block.aiName, codeGenerator, basePath, filesToBeFixed, allFiles);
387
398
  this.out.emit('data', {
@@ -428,8 +439,9 @@ class StormCodegen {
428
439
  if (errors.size > 0) {
429
440
  this.emitBlockStatus(blockUri, blockName, events_1.StormEventBlockStatusType.FIXING);
430
441
  const promises = Array.from(errors.entries()).map(([filename, fileErrors]) => {
431
- // todo: only try to fix file if it is part of filesToBeFixed
432
- return this.tryToFixFile(blockUri, blockName, basePath, filename, fileErrors, allFiles, codeGenerator);
442
+ if (filesToBeFixed.some((file) => file.filename === filename)) {
443
+ return this.tryToFixFile(blockUri, blockName, basePath, filename, fileErrors, allFiles, codeGenerator);
444
+ }
433
445
  });
434
446
  await Promise.all(promises);
435
447
  }
@@ -676,6 +688,7 @@ class StormCodegen {
676
688
  */
677
689
  async processTemplates(blockUri, aiName, generator, templates, contextFiles) {
678
690
  const promises = templates.map(async (templateFile) => {
691
+ let changedFiles = [];
679
692
  const stream = await generator({
680
693
  context: contextFiles,
681
694
  template: templateFile,
@@ -685,11 +698,28 @@ class StormCodegen {
685
698
  stream.abort();
686
699
  });
687
700
  stream.on('data', (evt) => {
688
- this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
701
+ let changedFile = this.handleTemplateFileOutput(blockUri, aiName, evt);
702
+ if (changedFile) {
703
+ changedFiles.push(...changedFiles, changedFile);
704
+ }
689
705
  });
690
706
  await stream.waitForDone();
707
+ return changedFiles;
691
708
  });
692
- await Promise.all(promises);
709
+ const changedFiles = await Promise.all(promises);
710
+ let allFiles = templates.concat(contextFiles);
711
+ let result = [];
712
+ for (const changedFile of changedFiles.flat()) {
713
+ const find = allFiles.find((file) => file.filename === changedFile.payload.filename);
714
+ if (find) {
715
+ result.push({ type: find.type, filename: find.filename, mode: find.mode, permissions: find.permissions,
716
+ content: changedFile.payload.content });
717
+ }
718
+ else {
719
+ console.warn("processTemplates: AI changed a file that wasn't in the input [" + changedFile.payload.filename + "]");
720
+ }
721
+ }
722
+ return result;
693
723
  }
694
724
  /**
695
725
  * Converts the generated files to a format that can be sent to the AI
@@ -200,6 +200,12 @@ export interface StormEventFileDone extends StormEventFileBase {
200
200
  content: string;
201
201
  };
202
202
  }
203
+ export interface StormEventFileFailed extends StormEventFileBase {
204
+ type: 'FILE_FAILED';
205
+ payload: StormEventFileBasePayload & {
206
+ error: string;
207
+ };
208
+ }
203
209
  export interface StormEventFileChunk extends StormEventFileBase {
204
210
  type: 'FILE_CHUNK';
205
211
  payload: StormEventFileBasePayload & {
@@ -259,4 +265,4 @@ export interface StormEventPhases {
259
265
  phaseType: StormEventPhaseType;
260
266
  };
261
267
  }
262
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry;
268
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.54.3",
3
+ "version": "0.54.5",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -36,7 +36,6 @@ import Path, { join } from 'path';
36
36
  import os from 'node:os';
37
37
  import { readFileSync, writeFileSync, existsSync } from 'fs';
38
38
  import YAML from 'yaml';
39
- import assert from 'assert';
40
39
 
41
40
  type ImplementationGenerator<T = StormFileImplementationPrompt> = (
42
41
  prompt: T,
@@ -134,19 +133,15 @@ export class StormCodegen {
134
133
  private handleTemplateFileOutput(
135
134
  blockUri: KapetaURI,
136
135
  aiName: string,
137
- template: StormFileInfo,
138
136
  data: StormEvent
139
- ): void {
137
+ ): StormEventFileDone | undefined {
140
138
  if (this.handleFileEvents(blockUri, aiName, data)) {
141
139
  return;
142
140
  }
143
141
 
144
142
  switch (data.type) {
145
143
  case 'FILE_DONE':
146
- template.filename = data.payload.filename;
147
- template.content = data.payload.content;
148
- this.handleFileDoneOutput(blockUri, aiName, data);
149
- break;
144
+ return this.handleFileDoneOutput(blockUri, aiName, data);
150
145
  }
151
146
  }
152
147
 
@@ -222,12 +217,23 @@ export class StormCodegen {
222
217
  },
223
218
  });
224
219
  return true;
220
+ case 'FILE_FAILED':
221
+ this.out.emit('data', {
222
+ ...data,
223
+ payload: {
224
+ ...data.payload,
225
+ path: join(basePath, data.payload.filename),
226
+ blockName,
227
+ blockRef,
228
+ instanceId,
229
+ },
230
+ });
225
231
  }
226
232
 
227
233
  return false;
228
234
  }
229
235
 
230
- private handleFileDoneOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
236
+ private handleFileDoneOutput(blockUri: KapetaURI, aiName: string, data: StormEvent): StormEventFileDone | undefined {
231
237
  switch (data.type) {
232
238
  case 'FILE_DONE':
233
239
  const ref = blockUri.toNormalizedString();
@@ -257,8 +263,12 @@ export class StormCodegen {
257
263
  if (this.isAborted()) {
258
264
  return;
259
265
  }
266
+
267
+ const kapetaYaml = YAML.stringify(block.content);
268
+ const blockDefinition = block.content;
269
+
260
270
  // Generate the code for the block using the standard codegen templates
261
- const generatedResult = await this.generateBlock(block.content);
271
+ const generatedResult = await this.generateBlock(blockDefinition);
262
272
  if (!generatedResult) {
263
273
  return;
264
274
  }
@@ -374,7 +384,7 @@ export class StormCodegen {
374
384
  if (this.isAborted()) {
375
385
  return;
376
386
  }
377
- const basePath = this.getBasePath(block.content.metadata.name);
387
+ const basePath = this.getBasePath(blockDefinition.metadata.name);
378
388
 
379
389
  const screenFilesConverted = screenFiles.map((screenFile) => {
380
390
  return {
@@ -400,7 +410,7 @@ export class StormCodegen {
400
410
  const stream = await stormClient.generateCode(payload);
401
411
 
402
412
  stream.on('data', (evt) => {
403
- this.handleTemplateFileOutput(blockUri, block.aiName, webRouter, evt);
413
+ this.handleTemplateFileOutput(blockUri, block.aiName, evt);
404
414
  });
405
415
 
406
416
  this.out.on('aborted', () => {
@@ -417,9 +427,9 @@ export class StormCodegen {
417
427
  );
418
428
 
419
429
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
420
- const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
430
+ let serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
421
431
  if (serviceFiles.length > 0) {
422
- await this.processTemplates(
432
+ serviceFiles = await this.processTemplates(
423
433
  blockUri,
424
434
  block.aiName,
425
435
  stormClient.createServiceImplementation.bind(stormClient),
@@ -432,21 +442,22 @@ export class StormCodegen {
432
442
  return;
433
443
  }
434
444
 
445
+ for (const contextFile of contextFiles) {
446
+ const filePath = join(basePath, contextFile.filename);
447
+ await writeFile(filePath, contextFile.content);
448
+ }
449
+
435
450
  for (const screenFile of screenFilesConverted) {
436
451
  const filePath = join(basePath, screenFile.filename);
437
452
  await writeFile(filePath, screenFile.content);
438
453
  }
439
454
 
455
+ // this might decide to overwrite a file that is also a context file
440
456
  for (const serviceFile of serviceFiles) {
441
457
  const filePath = join(basePath, serviceFile.filename);
442
458
  await writeFile(filePath, serviceFile.content);
443
459
  }
444
460
 
445
- for (const serviceFile of contextFiles) {
446
- const filePath = join(basePath, serviceFile.filename);
447
- await writeFile(filePath, serviceFile.content);
448
- }
449
-
450
461
  // Write again after modifications
451
462
  for (const webRouterFile of webRouters) {
452
463
  const filePath = join(basePath, webRouterFile.filename);
@@ -454,14 +465,14 @@ export class StormCodegen {
454
465
  }
455
466
 
456
467
  const kapetaYmlPath = join(basePath, 'kapeta.yml');
457
- await writeFile(kapetaYmlPath, YAML.stringify(block.content));
468
+ await writeFile(kapetaYmlPath, kapetaYaml);
458
469
 
459
470
  const blockRef = block.uri;
460
471
 
461
472
  this.emitBlockStatus(blockUri, block.aiName, StormEventBlockStatusType.QA);
462
473
 
463
474
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
464
- const codeGenerator = new BlockCodeGenerator(block.content);
475
+ const codeGenerator = new BlockCodeGenerator(blockDefinition);
465
476
 
466
477
  this.emitBlockStatus(blockUri, block.aiName, StormEventBlockStatusType.BUILDING);
467
478
  await this.verifyAndFixCode(blockUri, block.aiName, codeGenerator, basePath, filesToBeFixed, allFiles);
@@ -524,16 +535,17 @@ export class StormCodegen {
524
535
  this.emitBlockStatus(blockUri, blockName, StormEventBlockStatusType.FIXING);
525
536
 
526
537
  const promises = Array.from(errors.entries()).map(([filename, fileErrors]) => {
527
- // todo: only try to fix file if it is part of filesToBeFixed
528
- return this.tryToFixFile(
529
- blockUri,
530
- blockName,
531
- basePath,
532
- filename,
533
- fileErrors,
534
- allFiles,
535
- codeGenerator
536
- );
538
+ if (filesToBeFixed.some((file) => file.filename === filename)) {
539
+ return this.tryToFixFile(
540
+ blockUri,
541
+ blockName,
542
+ basePath,
543
+ filename,
544
+ fileErrors,
545
+ allFiles,
546
+ codeGenerator
547
+ );
548
+ }
537
549
  });
538
550
 
539
551
  await Promise.all(promises);
@@ -864,8 +876,9 @@ export class StormCodegen {
864
876
  generator: ImplementationGenerator,
865
877
  templates: StormFileInfo[],
866
878
  contextFiles: StormFileInfo[]
867
- ): Promise<void> {
879
+ ): Promise<StormFileInfo[]> {
868
880
  const promises = templates.map(async (templateFile) => {
881
+ let changedFiles: StormEventFileDone[] = [];
869
882
  const stream = await generator({
870
883
  context: contextFiles,
871
884
  template: templateFile,
@@ -877,13 +890,29 @@ export class StormCodegen {
877
890
  });
878
891
 
879
892
  stream.on('data', (evt) => {
880
- this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
893
+ let changedFile = this.handleTemplateFileOutput(blockUri, aiName, evt);
894
+ if (changedFile) {
895
+ changedFiles.push(...changedFiles, changedFile);
896
+ }
881
897
  });
882
898
 
883
899
  await stream.waitForDone();
900
+ return changedFiles;
884
901
  });
885
902
 
886
- await Promise.all(promises);
903
+ const changedFiles: StormEventFileDone[][] = await Promise.all(promises);
904
+ let allFiles = templates.concat(contextFiles);
905
+ let result: StormFileInfo[] = [];
906
+ for (const changedFile of changedFiles.flat()) {
907
+ const find = allFiles.find((file) => file.filename === changedFile.payload.filename);
908
+ if (find) {
909
+ result.push({ type: find.type, filename: find.filename, mode: find.mode, permissions: find.permissions,
910
+ content: changedFile.payload.content });
911
+ } else {
912
+ console.warn("processTemplates: AI changed a file that wasn't in the input [" + changedFile.payload.filename + "]");
913
+ }
914
+ }
915
+ return result;
887
916
  }
888
917
 
889
918
  /**
@@ -241,6 +241,13 @@ export interface StormEventFileDone extends StormEventFileBase {
241
241
  };
242
242
  }
243
243
 
244
+ export interface StormEventFileFailed extends StormEventFileBase {
245
+ type: 'FILE_FAILED';
246
+ payload: StormEventFileBasePayload & {
247
+ error: string;
248
+ };
249
+ }
250
+
244
251
  export interface StormEventFileChunk extends StormEventFileBase {
245
252
  type: 'FILE_CHUNK';
246
253
  payload: StormEventFileBasePayload & {
@@ -322,6 +329,7 @@ export type StormEvent =
322
329
  | StormEventFileLogical
323
330
  | StormEventFileState
324
331
  | StormEventFileDone
332
+ | StormEventFileFailed
325
333
  | StormEventFileChunk
326
334
  | StormEventDone
327
335
  | StormEventDefinitionChange