@kapeta/local-cluster-service 0.50.0 → 0.51.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.51.0](https://github.com/kapetacom/local-cluster-service/compare/v0.50.0...v0.51.0) (2024-06-05)
2
+
3
+
4
+ ### Features
5
+
6
+ * Handle file streaming events ([#164](https://github.com/kapetacom/local-cluster-service/issues/164)) ([441847a](https://github.com/kapetacom/local-cluster-service/commit/441847a12d20826d843e62627ea9bf584043effb))
7
+
1
8
  # [0.50.0](https://github.com/kapetacom/local-cluster-service/compare/v0.49.0...v0.50.0) (2024-06-05)
2
9
 
3
10
 
@@ -18,7 +18,8 @@ export declare class StormCodegen {
18
18
  getStream(): StormStream;
19
19
  private handleTemplateFileOutput;
20
20
  private handleUiOutput;
21
- private handleFileOutput;
21
+ private handleFileEvents;
22
+ private handleFileDoneOutput;
22
23
  private getBasePath;
23
24
  /**
24
25
  * Generates the code for a block and sends it to the AI
@@ -34,7 +35,7 @@ export declare class StormCodegen {
34
35
  /**
35
36
  * Emits the text-based files to the stream
36
37
  */
37
- private emitFiles;
38
+ private emitStaticFiles;
38
39
  private emitFile;
39
40
  /**
40
41
  * Sends the template to the AI and processes the response
@@ -42,6 +42,49 @@ const path_1 = __importStar(require("path"));
42
42
  const node_os_1 = __importDefault(require("node:os"));
43
43
  const fs_1 = require("fs");
44
44
  const path_2 = __importDefault(require("path"));
45
+ const SIMULATED_DELAY = 1000;
46
+ class SimulatedFileDelay {
47
+ file;
48
+ stream;
49
+ constructor(file, stream) {
50
+ this.file = file;
51
+ this.stream = stream;
52
+ }
53
+ async start() {
54
+ const commonPayload = {
55
+ filename: this.file.payload.filename,
56
+ path: this.file.payload.path,
57
+ blockName: this.file.payload.blockName,
58
+ blockRef: this.file.payload.blockRef,
59
+ instanceId: this.file.payload.instanceId,
60
+ };
61
+ this.stream.emit('data', {
62
+ type: 'FILE_START',
63
+ created: Date.now(),
64
+ reason: 'File start',
65
+ payload: commonPayload,
66
+ });
67
+ const lines = this.file.payload.content.split('\n');
68
+ const delayPerLine = SIMULATED_DELAY / lines.length;
69
+ for (const line of lines) {
70
+ await new Promise((resolve) => {
71
+ setTimeout(() => {
72
+ this.stream.emit('data', {
73
+ type: 'FILE_CHUNK',
74
+ created: Date.now(),
75
+ reason: 'File chunk',
76
+ payload: {
77
+ ...commonPayload,
78
+ content: line,
79
+ },
80
+ });
81
+ resolve();
82
+ }, delayPerLine);
83
+ });
84
+ }
85
+ this.stream.emit('data', this.file);
86
+ }
87
+ }
45
88
  class StormCodegen {
46
89
  userPrompt;
47
90
  blocks;
@@ -70,42 +113,132 @@ class StormCodegen {
70
113
  return this.out;
71
114
  }
72
115
  handleTemplateFileOutput(blockUri, aiName, template, data) {
116
+ if (this.handleFileEvents(blockUri, aiName, data)) {
117
+ return;
118
+ }
73
119
  switch (data.type) {
74
- case 'FILE':
120
+ case 'FILE_DONE':
75
121
  template.filename = data.payload.filename;
76
122
  template.content = data.payload.content;
77
- return this.handleFileOutput(blockUri, aiName, data);
123
+ this.handleFileDoneOutput(blockUri, aiName, data);
124
+ break;
78
125
  }
79
126
  }
80
- handleUiOutput(blockUri, aiName, data) {
127
+ handleUiOutput(blockUri, blockName, data) {
128
+ const blockRef = blockUri.toNormalizedString();
129
+ const instanceId = event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef);
130
+ if (this.handleFileEvents(blockUri, blockName, data)) {
131
+ return;
132
+ }
81
133
  switch (data.type) {
82
134
  case 'SCREEN':
83
- const ref = blockUri.toNormalizedString();
84
135
  this.out.emit('data', {
85
136
  type: 'SCREEN',
86
137
  reason: data.reason,
87
138
  created: Date.now(),
88
139
  payload: {
89
140
  ...data.payload,
90
- blockName: aiName,
91
- blockRef: ref,
92
- instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
141
+ blockName,
142
+ blockRef,
143
+ instanceId,
144
+ },
145
+ });
146
+ break;
147
+ case 'FILE_START':
148
+ case 'FILE_CHUNK_RESET':
149
+ this.out.emit('data', {
150
+ ...data,
151
+ payload: {
152
+ ...data.payload,
153
+ blockName,
154
+ blockRef,
155
+ instanceId,
93
156
  },
94
157
  });
95
- case 'FILE':
96
- return this.handleFileOutput(blockUri, aiName, data);
158
+ break;
159
+ case 'FILE_CHUNK':
160
+ this.out.emit('data', {
161
+ ...data,
162
+ payload: {
163
+ ...data.payload,
164
+ blockName,
165
+ blockRef,
166
+ instanceId,
167
+ },
168
+ });
169
+ break;
170
+ case 'FILE_STATE':
171
+ this.out.emit('data', {
172
+ ...data,
173
+ payload: {
174
+ ...data.payload,
175
+ blockName,
176
+ blockRef,
177
+ instanceId,
178
+ },
179
+ });
180
+ break;
181
+ case 'FILE_DONE':
182
+ this.handleFileDoneOutput(blockUri, blockName, data);
183
+ break;
97
184
  }
98
185
  }
99
- handleFileOutput(blockUri, aiName, data) {
186
+ handleFileEvents(blockUri, blockName, data) {
187
+ const blockRef = blockUri.toNormalizedString();
188
+ const instanceId = event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef);
189
+ const basePath = this.getBasePath(blockUri.fullName);
100
190
  switch (data.type) {
101
- case 'FILE':
191
+ case 'FILE_START':
192
+ case 'FILE_CHUNK_RESET':
193
+ this.out.emit('data', {
194
+ ...data,
195
+ payload: {
196
+ ...data.payload,
197
+ path: (0, path_1.join)(basePath, data.payload.filename),
198
+ blockName,
199
+ blockRef,
200
+ instanceId,
201
+ },
202
+ });
203
+ return true;
204
+ case 'FILE_CHUNK':
205
+ this.out.emit('data', {
206
+ ...data,
207
+ payload: {
208
+ ...data.payload,
209
+ path: (0, path_1.join)(basePath, data.payload.filename),
210
+ blockName,
211
+ blockRef,
212
+ instanceId,
213
+ },
214
+ });
215
+ return true;
216
+ case 'FILE_STATE':
217
+ this.out.emit('data', {
218
+ ...data,
219
+ payload: {
220
+ ...data.payload,
221
+ path: (0, path_1.join)(basePath, data.payload.filename),
222
+ blockName,
223
+ blockRef,
224
+ instanceId,
225
+ },
226
+ });
227
+ return true;
228
+ }
229
+ return false;
230
+ }
231
+ handleFileDoneOutput(blockUri, aiName, data) {
232
+ switch (data.type) {
233
+ case 'FILE_DONE':
102
234
  const ref = blockUri.toNormalizedString();
103
235
  this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
104
236
  return {
105
- type: 'FILE',
237
+ type: 'FILE_DONE',
106
238
  created: Date.now(),
107
239
  payload: {
108
240
  filename: data.payload.filename,
241
+ path: (0, path_1.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
109
242
  content: data.payload.content,
110
243
  blockRef: ref,
111
244
  instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
@@ -130,7 +263,7 @@ class StormCodegen {
130
263
  }
131
264
  const allFiles = this.toStormFiles(generatedResult);
132
265
  // Send all the non-ai files to the stream
133
- this.emitFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
266
+ await this.emitStaticFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
134
267
  if (this.isAborted()) {
135
268
  return;
136
269
  }
@@ -285,8 +418,8 @@ class StormCodegen {
285
418
  /**
286
419
  * Emits the text-based files to the stream
287
420
  */
288
- emitFiles(uri, aiName, files) {
289
- files.forEach((file) => {
421
+ async emitStaticFiles(uri, aiName, files) {
422
+ const promises = files.map((file) => {
290
423
  if (!file.content || typeof file.content !== 'string') {
291
424
  return;
292
425
  }
@@ -300,14 +433,30 @@ class StormCodegen {
300
433
  // They will need to be implemented by the AI
301
434
  return;
302
435
  }
303
- this.emitFile(uri, aiName, file.filename, file.content);
436
+ const basePath = this.getBasePath(uri.fullName);
437
+ const ref = uri.toNormalizedString();
438
+ const fileEvent = {
439
+ type: 'FILE_DONE',
440
+ reason: 'File generated',
441
+ created: Date.now(),
442
+ payload: {
443
+ filename: file.filename,
444
+ path: (0, path_1.join)(basePath, file.filename),
445
+ content: file.content,
446
+ blockName: aiName,
447
+ blockRef: ref,
448
+ instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
449
+ },
450
+ };
451
+ return new SimulatedFileDelay(fileEvent, this.out).start();
304
452
  });
453
+ return Promise.all(promises);
305
454
  }
306
455
  emitFile(uri, blockName, filename, content, reason = 'File generated') {
307
456
  const basePath = this.getBasePath(uri.fullName);
308
457
  const ref = uri.toNormalizedString();
309
458
  this.out.emit('data', {
310
- type: 'FILE',
459
+ type: 'FILE_DONE',
311
460
  reason,
312
461
  created: Date.now(),
313
462
  payload: {
@@ -330,21 +479,15 @@ class StormCodegen {
330
479
  template: templateFile,
331
480
  prompt: this.userPrompt,
332
481
  });
333
- const files = [];
334
482
  this.out.on('aborted', () => {
335
483
  stream.abort();
336
484
  });
337
485
  stream.on('data', (evt) => {
338
- const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
339
- if (file) {
340
- files.push(file);
341
- }
486
+ this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
342
487
  });
343
488
  await stream.waitForDone();
344
- return files;
345
489
  });
346
- const fileChunks = await Promise.all(promises);
347
- return fileChunks.flat();
490
+ await Promise.all(promises);
348
491
  }
349
492
  /**
350
493
  * Converts the generated files to a format that can be sent to the AI
@@ -211,10 +211,6 @@ class StormEventParser {
211
211
  evt.payload.toBlockId = StormEventParser.toInstanceId(handle, evt.payload.toComponent);
212
212
  this.connections.push(evt.payload);
213
213
  break;
214
- default:
215
- case 'SCREEN_CANDIDATE':
216
- case 'FILE':
217
- break;
218
214
  }
219
215
  return this.toResult(handle);
220
216
  }
@@ -150,19 +150,35 @@ export interface StormEventScreenCandidate {
150
150
  url: string;
151
151
  };
152
152
  }
153
- export interface StormEventFile {
154
- type: 'FILE';
153
+ export interface StormEventFileBasePayload {
154
+ filename: string;
155
+ path: string;
156
+ blockName: string;
157
+ blockRef: string;
158
+ instanceId: string;
159
+ }
160
+ export interface StormEventFileBase {
161
+ type: string;
155
162
  reason: string;
156
163
  created: number;
157
- payload: {
158
- filename: string;
159
- path: string;
164
+ payload: StormEventFileBasePayload;
165
+ }
166
+ export interface StormEventFileLogical extends StormEventFileBase {
167
+ type: 'FILE_START' | 'FILE_CHUNK_RESET';
168
+ }
169
+ export interface StormEventFileState extends StormEventFileBase {
170
+ type: 'FILE_STATE';
171
+ payload: StormEventFileBasePayload & {
172
+ state: string;
173
+ };
174
+ }
175
+ export interface StormEventFileContent extends StormEventFileBase {
176
+ type: 'FILE_DONE' | 'FILE_CHUNK';
177
+ payload: StormEventFileBasePayload & {
160
178
  content: string;
161
- blockName: string;
162
- blockRef: string;
163
- instanceId: string;
164
179
  };
165
180
  }
181
+ export type StormEventFile = StormEventFileLogical | StormEventFileState | StormEventFileContent;
166
182
  export interface StormEventBlockReady {
167
183
  type: 'BLOCK_READY';
168
184
  reason: string;
@@ -196,4 +212,4 @@ export interface StormEventPhases {
196
212
  phaseType: StormEventPhaseType;
197
213
  };
198
214
  }
199
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
215
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileContent | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
@@ -18,7 +18,8 @@ export declare class StormCodegen {
18
18
  getStream(): StormStream;
19
19
  private handleTemplateFileOutput;
20
20
  private handleUiOutput;
21
- private handleFileOutput;
21
+ private handleFileEvents;
22
+ private handleFileDoneOutput;
22
23
  private getBasePath;
23
24
  /**
24
25
  * Generates the code for a block and sends it to the AI
@@ -34,7 +35,7 @@ export declare class StormCodegen {
34
35
  /**
35
36
  * Emits the text-based files to the stream
36
37
  */
37
- private emitFiles;
38
+ private emitStaticFiles;
38
39
  private emitFile;
39
40
  /**
40
41
  * Sends the template to the AI and processes the response
@@ -42,6 +42,49 @@ const path_1 = __importStar(require("path"));
42
42
  const node_os_1 = __importDefault(require("node:os"));
43
43
  const fs_1 = require("fs");
44
44
  const path_2 = __importDefault(require("path"));
45
+ const SIMULATED_DELAY = 1000;
46
+ class SimulatedFileDelay {
47
+ file;
48
+ stream;
49
+ constructor(file, stream) {
50
+ this.file = file;
51
+ this.stream = stream;
52
+ }
53
+ async start() {
54
+ const commonPayload = {
55
+ filename: this.file.payload.filename,
56
+ path: this.file.payload.path,
57
+ blockName: this.file.payload.blockName,
58
+ blockRef: this.file.payload.blockRef,
59
+ instanceId: this.file.payload.instanceId,
60
+ };
61
+ this.stream.emit('data', {
62
+ type: 'FILE_START',
63
+ created: Date.now(),
64
+ reason: 'File start',
65
+ payload: commonPayload,
66
+ });
67
+ const lines = this.file.payload.content.split('\n');
68
+ const delayPerLine = SIMULATED_DELAY / lines.length;
69
+ for (const line of lines) {
70
+ await new Promise((resolve) => {
71
+ setTimeout(() => {
72
+ this.stream.emit('data', {
73
+ type: 'FILE_CHUNK',
74
+ created: Date.now(),
75
+ reason: 'File chunk',
76
+ payload: {
77
+ ...commonPayload,
78
+ content: line,
79
+ },
80
+ });
81
+ resolve();
82
+ }, delayPerLine);
83
+ });
84
+ }
85
+ this.stream.emit('data', this.file);
86
+ }
87
+ }
45
88
  class StormCodegen {
46
89
  userPrompt;
47
90
  blocks;
@@ -70,42 +113,132 @@ class StormCodegen {
70
113
  return this.out;
71
114
  }
72
115
  handleTemplateFileOutput(blockUri, aiName, template, data) {
116
+ if (this.handleFileEvents(blockUri, aiName, data)) {
117
+ return;
118
+ }
73
119
  switch (data.type) {
74
- case 'FILE':
120
+ case 'FILE_DONE':
75
121
  template.filename = data.payload.filename;
76
122
  template.content = data.payload.content;
77
- return this.handleFileOutput(blockUri, aiName, data);
123
+ this.handleFileDoneOutput(blockUri, aiName, data);
124
+ break;
78
125
  }
79
126
  }
80
- handleUiOutput(blockUri, aiName, data) {
127
+ handleUiOutput(blockUri, blockName, data) {
128
+ const blockRef = blockUri.toNormalizedString();
129
+ const instanceId = event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef);
130
+ if (this.handleFileEvents(blockUri, blockName, data)) {
131
+ return;
132
+ }
81
133
  switch (data.type) {
82
134
  case 'SCREEN':
83
- const ref = blockUri.toNormalizedString();
84
135
  this.out.emit('data', {
85
136
  type: 'SCREEN',
86
137
  reason: data.reason,
87
138
  created: Date.now(),
88
139
  payload: {
89
140
  ...data.payload,
90
- blockName: aiName,
91
- blockRef: ref,
92
- instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
141
+ blockName,
142
+ blockRef,
143
+ instanceId,
144
+ },
145
+ });
146
+ break;
147
+ case 'FILE_START':
148
+ case 'FILE_CHUNK_RESET':
149
+ this.out.emit('data', {
150
+ ...data,
151
+ payload: {
152
+ ...data.payload,
153
+ blockName,
154
+ blockRef,
155
+ instanceId,
93
156
  },
94
157
  });
95
- case 'FILE':
96
- return this.handleFileOutput(blockUri, aiName, data);
158
+ break;
159
+ case 'FILE_CHUNK':
160
+ this.out.emit('data', {
161
+ ...data,
162
+ payload: {
163
+ ...data.payload,
164
+ blockName,
165
+ blockRef,
166
+ instanceId,
167
+ },
168
+ });
169
+ break;
170
+ case 'FILE_STATE':
171
+ this.out.emit('data', {
172
+ ...data,
173
+ payload: {
174
+ ...data.payload,
175
+ blockName,
176
+ blockRef,
177
+ instanceId,
178
+ },
179
+ });
180
+ break;
181
+ case 'FILE_DONE':
182
+ this.handleFileDoneOutput(blockUri, blockName, data);
183
+ break;
97
184
  }
98
185
  }
99
- handleFileOutput(blockUri, aiName, data) {
186
+ handleFileEvents(blockUri, blockName, data) {
187
+ const blockRef = blockUri.toNormalizedString();
188
+ const instanceId = event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef);
189
+ const basePath = this.getBasePath(blockUri.fullName);
100
190
  switch (data.type) {
101
- case 'FILE':
191
+ case 'FILE_START':
192
+ case 'FILE_CHUNK_RESET':
193
+ this.out.emit('data', {
194
+ ...data,
195
+ payload: {
196
+ ...data.payload,
197
+ path: (0, path_1.join)(basePath, data.payload.filename),
198
+ blockName,
199
+ blockRef,
200
+ instanceId,
201
+ },
202
+ });
203
+ return true;
204
+ case 'FILE_CHUNK':
205
+ this.out.emit('data', {
206
+ ...data,
207
+ payload: {
208
+ ...data.payload,
209
+ path: (0, path_1.join)(basePath, data.payload.filename),
210
+ blockName,
211
+ blockRef,
212
+ instanceId,
213
+ },
214
+ });
215
+ return true;
216
+ case 'FILE_STATE':
217
+ this.out.emit('data', {
218
+ ...data,
219
+ payload: {
220
+ ...data.payload,
221
+ path: (0, path_1.join)(basePath, data.payload.filename),
222
+ blockName,
223
+ blockRef,
224
+ instanceId,
225
+ },
226
+ });
227
+ return true;
228
+ }
229
+ return false;
230
+ }
231
+ handleFileDoneOutput(blockUri, aiName, data) {
232
+ switch (data.type) {
233
+ case 'FILE_DONE':
102
234
  const ref = blockUri.toNormalizedString();
103
235
  this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
104
236
  return {
105
- type: 'FILE',
237
+ type: 'FILE_DONE',
106
238
  created: Date.now(),
107
239
  payload: {
108
240
  filename: data.payload.filename,
241
+ path: (0, path_1.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
109
242
  content: data.payload.content,
110
243
  blockRef: ref,
111
244
  instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
@@ -130,7 +263,7 @@ class StormCodegen {
130
263
  }
131
264
  const allFiles = this.toStormFiles(generatedResult);
132
265
  // Send all the non-ai files to the stream
133
- this.emitFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
266
+ await this.emitStaticFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
134
267
  if (this.isAborted()) {
135
268
  return;
136
269
  }
@@ -285,8 +418,8 @@ class StormCodegen {
285
418
  /**
286
419
  * Emits the text-based files to the stream
287
420
  */
288
- emitFiles(uri, aiName, files) {
289
- files.forEach((file) => {
421
+ async emitStaticFiles(uri, aiName, files) {
422
+ const promises = files.map((file) => {
290
423
  if (!file.content || typeof file.content !== 'string') {
291
424
  return;
292
425
  }
@@ -300,14 +433,30 @@ class StormCodegen {
300
433
  // They will need to be implemented by the AI
301
434
  return;
302
435
  }
303
- this.emitFile(uri, aiName, file.filename, file.content);
436
+ const basePath = this.getBasePath(uri.fullName);
437
+ const ref = uri.toNormalizedString();
438
+ const fileEvent = {
439
+ type: 'FILE_DONE',
440
+ reason: 'File generated',
441
+ created: Date.now(),
442
+ payload: {
443
+ filename: file.filename,
444
+ path: (0, path_1.join)(basePath, file.filename),
445
+ content: file.content,
446
+ blockName: aiName,
447
+ blockRef: ref,
448
+ instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
449
+ },
450
+ };
451
+ return new SimulatedFileDelay(fileEvent, this.out).start();
304
452
  });
453
+ return Promise.all(promises);
305
454
  }
306
455
  emitFile(uri, blockName, filename, content, reason = 'File generated') {
307
456
  const basePath = this.getBasePath(uri.fullName);
308
457
  const ref = uri.toNormalizedString();
309
458
  this.out.emit('data', {
310
- type: 'FILE',
459
+ type: 'FILE_DONE',
311
460
  reason,
312
461
  created: Date.now(),
313
462
  payload: {
@@ -330,21 +479,15 @@ class StormCodegen {
330
479
  template: templateFile,
331
480
  prompt: this.userPrompt,
332
481
  });
333
- const files = [];
334
482
  this.out.on('aborted', () => {
335
483
  stream.abort();
336
484
  });
337
485
  stream.on('data', (evt) => {
338
- const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
339
- if (file) {
340
- files.push(file);
341
- }
486
+ this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
342
487
  });
343
488
  await stream.waitForDone();
344
- return files;
345
489
  });
346
- const fileChunks = await Promise.all(promises);
347
- return fileChunks.flat();
490
+ await Promise.all(promises);
348
491
  }
349
492
  /**
350
493
  * Converts the generated files to a format that can be sent to the AI
@@ -211,10 +211,6 @@ class StormEventParser {
211
211
  evt.payload.toBlockId = StormEventParser.toInstanceId(handle, evt.payload.toComponent);
212
212
  this.connections.push(evt.payload);
213
213
  break;
214
- default:
215
- case 'SCREEN_CANDIDATE':
216
- case 'FILE':
217
- break;
218
214
  }
219
215
  return this.toResult(handle);
220
216
  }
@@ -150,19 +150,35 @@ export interface StormEventScreenCandidate {
150
150
  url: string;
151
151
  };
152
152
  }
153
- export interface StormEventFile {
154
- type: 'FILE';
153
+ export interface StormEventFileBasePayload {
154
+ filename: string;
155
+ path: string;
156
+ blockName: string;
157
+ blockRef: string;
158
+ instanceId: string;
159
+ }
160
+ export interface StormEventFileBase {
161
+ type: string;
155
162
  reason: string;
156
163
  created: number;
157
- payload: {
158
- filename: string;
159
- path: string;
164
+ payload: StormEventFileBasePayload;
165
+ }
166
+ export interface StormEventFileLogical extends StormEventFileBase {
167
+ type: 'FILE_START' | 'FILE_CHUNK_RESET';
168
+ }
169
+ export interface StormEventFileState extends StormEventFileBase {
170
+ type: 'FILE_STATE';
171
+ payload: StormEventFileBasePayload & {
172
+ state: string;
173
+ };
174
+ }
175
+ export interface StormEventFileContent extends StormEventFileBase {
176
+ type: 'FILE_DONE' | 'FILE_CHUNK';
177
+ payload: StormEventFileBasePayload & {
160
178
  content: string;
161
- blockName: string;
162
- blockRef: string;
163
- instanceId: string;
164
179
  };
165
180
  }
181
+ export type StormEventFile = StormEventFileLogical | StormEventFileState | StormEventFileContent;
166
182
  export interface StormEventBlockReady {
167
183
  type: 'BLOCK_READY';
168
184
  reason: string;
@@ -196,4 +212,4 @@ export interface StormEventPhases {
196
212
  phaseType: StormEventPhaseType;
197
213
  };
198
214
  }
199
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
215
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileContent | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -15,7 +15,7 @@ import {
15
15
  import { BlockDefinition } from '@kapeta/schemas';
16
16
  import { codeGeneratorManager } from '../codeGeneratorManager';
17
17
  import { STORM_ID, stormClient } from './stormClient';
18
- import { StormEvent, StormEventFile } from './events';
18
+ import { StormEvent, StormEventFileContent, StormEventFileLogical } from './events';
19
19
  import { BlockDefinitionInfo, StormEventParser } from './event-parser';
20
20
  import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
21
21
  import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
@@ -27,6 +27,54 @@ import Path from 'path';
27
27
 
28
28
  type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
29
29
 
30
+ const SIMULATED_DELAY = 1000;
31
+
32
+ class SimulatedFileDelay {
33
+ private readonly file: StormEventFileContent;
34
+ public readonly stream;
35
+ constructor(file: StormEventFileContent, stream: StormStream) {
36
+ this.file = file;
37
+ this.stream = stream;
38
+ }
39
+
40
+ public async start() {
41
+ const commonPayload = {
42
+ filename: this.file.payload.filename,
43
+ path: this.file.payload.path,
44
+ blockName: this.file.payload.blockName,
45
+ blockRef: this.file.payload.blockRef,
46
+ instanceId: this.file.payload.instanceId,
47
+ };
48
+ this.stream.emit('data', {
49
+ type: 'FILE_START',
50
+ created: Date.now(),
51
+ reason: 'File start',
52
+ payload: commonPayload,
53
+ } satisfies StormEventFileLogical);
54
+
55
+ const lines = this.file.payload.content.split('\n');
56
+ const delayPerLine = SIMULATED_DELAY / lines.length;
57
+ for (const line of lines) {
58
+ await new Promise<void>((resolve) => {
59
+ setTimeout(() => {
60
+ this.stream.emit('data', {
61
+ type: 'FILE_CHUNK',
62
+ created: Date.now(),
63
+ reason: 'File chunk',
64
+ payload: {
65
+ ...commonPayload,
66
+ content: line,
67
+ },
68
+ } satisfies StormEventFileContent);
69
+ resolve();
70
+ }, delayPerLine);
71
+ });
72
+ }
73
+
74
+ this.stream.emit('data', this.file);
75
+ }
76
+ }
77
+
30
78
  export class StormCodegen {
31
79
  private readonly userPrompt: string;
32
80
  private readonly blocks: BlockDefinitionInfo[];
@@ -59,50 +107,150 @@ export class StormCodegen {
59
107
  return this.out;
60
108
  }
61
109
 
62
- private handleTemplateFileOutput(blockUri: KapetaURI, aiName: string, template: StormFileInfo, data: StormEvent) {
110
+ private handleTemplateFileOutput(
111
+ blockUri: KapetaURI,
112
+ aiName: string,
113
+ template: StormFileInfo,
114
+ data: StormEvent
115
+ ): void {
116
+ if (this.handleFileEvents(blockUri, aiName, data)) {
117
+ return;
118
+ }
119
+
63
120
  switch (data.type) {
64
- case 'FILE':
121
+ case 'FILE_DONE':
65
122
  template.filename = data.payload.filename;
66
123
  template.content = data.payload.content;
67
- return this.handleFileOutput(blockUri, aiName, data);
124
+ this.handleFileDoneOutput(blockUri, aiName, data);
125
+ break;
68
126
  }
69
127
  }
70
128
 
71
- private handleUiOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
129
+ private handleUiOutput(blockUri: KapetaURI, blockName: string, data: StormEvent) {
130
+ const blockRef = blockUri.toNormalizedString();
131
+ const instanceId = StormEventParser.toInstanceIdFromRef(blockRef);
132
+
133
+ if (this.handleFileEvents(blockUri, blockName, data)) {
134
+ return;
135
+ }
136
+
72
137
  switch (data.type) {
73
138
  case 'SCREEN':
74
- const ref = blockUri.toNormalizedString();
75
139
  this.out.emit('data', {
76
140
  type: 'SCREEN',
77
141
  reason: data.reason,
78
142
  created: Date.now(),
79
143
  payload: {
80
144
  ...data.payload,
81
- blockName: aiName,
82
- blockRef: ref,
83
- instanceId: StormEventParser.toInstanceIdFromRef(ref),
145
+ blockName,
146
+ blockRef,
147
+ instanceId,
148
+ },
149
+ });
150
+ break;
151
+ case 'FILE_START':
152
+ case 'FILE_CHUNK_RESET':
153
+ this.out.emit('data', {
154
+ ...data,
155
+ payload: {
156
+ ...data.payload,
157
+ blockName,
158
+ blockRef,
159
+ instanceId,
160
+ },
161
+ });
162
+ break;
163
+ case 'FILE_CHUNK':
164
+ this.out.emit('data', {
165
+ ...data,
166
+ payload: {
167
+ ...data.payload,
168
+ blockName,
169
+ blockRef,
170
+ instanceId,
84
171
  },
85
172
  });
86
- case 'FILE':
87
- return this.handleFileOutput(blockUri, aiName, data);
173
+ break;
174
+ case 'FILE_STATE':
175
+ this.out.emit('data', {
176
+ ...data,
177
+ payload: {
178
+ ...data.payload,
179
+ blockName,
180
+ blockRef,
181
+ instanceId,
182
+ },
183
+ });
184
+ break;
185
+ case 'FILE_DONE':
186
+ this.handleFileDoneOutput(blockUri, blockName, data);
187
+ break;
188
+ }
189
+ }
190
+
191
+ private handleFileEvents(blockUri: KapetaURI, blockName: string, data: StormEvent) {
192
+ const blockRef = blockUri.toNormalizedString();
193
+ const instanceId = StormEventParser.toInstanceIdFromRef(blockRef);
194
+ const basePath = this.getBasePath(blockUri.fullName);
195
+ switch (data.type) {
196
+ case 'FILE_START':
197
+ case 'FILE_CHUNK_RESET':
198
+ this.out.emit('data', {
199
+ ...data,
200
+ payload: {
201
+ ...data.payload,
202
+ path: join(basePath, data.payload.filename),
203
+ blockName,
204
+ blockRef,
205
+ instanceId,
206
+ },
207
+ });
208
+ return true;
209
+ case 'FILE_CHUNK':
210
+ this.out.emit('data', {
211
+ ...data,
212
+ payload: {
213
+ ...data.payload,
214
+ path: join(basePath, data.payload.filename),
215
+ blockName,
216
+ blockRef,
217
+ instanceId,
218
+ },
219
+ });
220
+ return true;
221
+ case 'FILE_STATE':
222
+ this.out.emit('data', {
223
+ ...data,
224
+ payload: {
225
+ ...data.payload,
226
+ path: join(basePath, data.payload.filename),
227
+ blockName,
228
+ blockRef,
229
+ instanceId,
230
+ },
231
+ });
232
+ return true;
88
233
  }
234
+
235
+ return false;
89
236
  }
90
237
 
91
- private handleFileOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
238
+ private handleFileDoneOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
92
239
  switch (data.type) {
93
- case 'FILE':
240
+ case 'FILE_DONE':
94
241
  const ref = blockUri.toNormalizedString();
95
242
  this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
96
243
  return {
97
- type: 'FILE',
244
+ type: 'FILE_DONE',
98
245
  created: Date.now(),
99
246
  payload: {
100
247
  filename: data.payload.filename,
248
+ path: join(this.getBasePath(blockUri.fullName), data.payload.filename),
101
249
  content: data.payload.content,
102
250
  blockRef: ref,
103
251
  instanceId: StormEventParser.toInstanceIdFromRef(ref),
104
252
  },
105
- } as StormEventFile;
253
+ } as StormEventFileContent;
106
254
  }
107
255
  }
108
256
 
@@ -126,7 +274,7 @@ export class StormCodegen {
126
274
  const allFiles = this.toStormFiles(generatedResult);
127
275
 
128
276
  // Send all the non-ai files to the stream
129
- this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
277
+ await this.emitStaticFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
130
278
 
131
279
  if (this.isAborted()) {
132
280
  return;
@@ -318,8 +466,8 @@ export class StormCodegen {
318
466
  /**
319
467
  * Emits the text-based files to the stream
320
468
  */
321
- private emitFiles(uri: KapetaURI, aiName: string, files: StormFileInfo[]) {
322
- files.forEach((file) => {
469
+ private async emitStaticFiles(uri: KapetaURI, aiName: string, files: StormFileInfo[]) {
470
+ const promises = files.map((file) => {
323
471
  if (!file.content || typeof file.content !== 'string') {
324
472
  return;
325
473
  }
@@ -336,8 +484,25 @@ export class StormCodegen {
336
484
  return;
337
485
  }
338
486
 
339
- this.emitFile(uri, aiName, file.filename, file.content);
487
+ const basePath = this.getBasePath(uri.fullName);
488
+ const ref = uri.toNormalizedString();
489
+ const fileEvent: StormEventFileContent = {
490
+ type: 'FILE_DONE',
491
+ reason: 'File generated',
492
+ created: Date.now(),
493
+ payload: {
494
+ filename: file.filename,
495
+ path: join(basePath, file.filename),
496
+ content: file.content,
497
+ blockName: aiName,
498
+ blockRef: ref,
499
+ instanceId: StormEventParser.toInstanceIdFromRef(ref),
500
+ },
501
+ };
502
+ return new SimulatedFileDelay(fileEvent, this.out).start();
340
503
  });
504
+
505
+ return Promise.all(promises);
341
506
  }
342
507
 
343
508
  private emitFile(
@@ -350,7 +515,7 @@ export class StormCodegen {
350
515
  const basePath = this.getBasePath(uri.fullName);
351
516
  const ref = uri.toNormalizedString();
352
517
  this.out.emit('data', {
353
- type: 'FILE',
518
+ type: 'FILE_DONE',
354
519
  reason,
355
520
  created: Date.now(),
356
521
  payload: {
@@ -361,7 +526,7 @@ export class StormCodegen {
361
526
  blockRef: ref,
362
527
  instanceId: StormEventParser.toInstanceIdFromRef(ref),
363
528
  },
364
- } satisfies StormEventFile);
529
+ } satisfies StormEventFileContent);
365
530
  }
366
531
 
367
532
  /**
@@ -373,7 +538,7 @@ export class StormCodegen {
373
538
  generator: ImplementationGenerator,
374
539
  templates: StormFileInfo[],
375
540
  contextFiles: StormFileInfo[]
376
- ) {
541
+ ): Promise<void> {
377
542
  const promises = templates.map(async (templateFile) => {
378
543
  const stream = await generator({
379
544
  context: contextFiles,
@@ -381,26 +546,18 @@ export class StormCodegen {
381
546
  prompt: this.userPrompt,
382
547
  });
383
548
 
384
- const files: StormEventFile[] = [];
385
-
386
549
  this.out.on('aborted', () => {
387
550
  stream.abort();
388
551
  });
389
552
 
390
553
  stream.on('data', (evt) => {
391
- const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
392
- if (file) {
393
- files.push(file);
394
- }
554
+ this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
395
555
  });
396
556
 
397
557
  await stream.waitForDone();
398
- return files;
399
558
  });
400
559
 
401
- const fileChunks = await Promise.all(promises);
402
-
403
- return fileChunks.flat();
560
+ await Promise.all(promises);
404
561
  }
405
562
 
406
563
  /**
@@ -325,11 +325,6 @@ export class StormEventParser {
325
325
  evt.payload.toBlockId = StormEventParser.toInstanceId(handle, evt.payload.toComponent);
326
326
  this.connections.push(evt.payload);
327
327
  break;
328
-
329
- default:
330
- case 'SCREEN_CANDIDATE':
331
- case 'FILE':
332
- break;
333
328
  }
334
329
 
335
330
  return this.toResult(handle);
@@ -182,20 +182,41 @@ export interface StormEventScreenCandidate {
182
182
  };
183
183
  }
184
184
 
185
- export interface StormEventFile {
186
- type: 'FILE';
185
+ export interface StormEventFileBasePayload {
186
+ filename: string;
187
+ path: string;
188
+ blockName: string;
189
+ blockRef: string;
190
+ instanceId: string;
191
+ }
192
+
193
+ export interface StormEventFileBase {
194
+ type: string;
187
195
  reason: string;
188
196
  created: number;
189
- payload: {
190
- filename: string;
191
- path: string;
197
+ payload: StormEventFileBasePayload;
198
+ }
199
+
200
+ export interface StormEventFileLogical extends StormEventFileBase {
201
+ type: 'FILE_START' | 'FILE_CHUNK_RESET';
202
+ }
203
+
204
+ export interface StormEventFileState extends StormEventFileBase {
205
+ type: 'FILE_STATE';
206
+ payload: StormEventFileBasePayload & {
207
+ state: string;
208
+ };
209
+ }
210
+
211
+ export interface StormEventFileContent extends StormEventFileBase {
212
+ type: 'FILE_DONE' | 'FILE_CHUNK';
213
+ payload: StormEventFileBasePayload & {
192
214
  content: string;
193
- blockName: string;
194
- blockRef: string;
195
- instanceId: string;
196
215
  };
197
216
  }
198
217
 
218
+ export type StormEventFile = StormEventFileLogical | StormEventFileState | StormEventFileContent;
219
+
199
220
  export interface StormEventBlockReady {
200
221
  type: 'BLOCK_READY';
201
222
  reason: string;
@@ -245,7 +266,9 @@ export type StormEvent =
245
266
  | StormEventError
246
267
  | StormEventScreen
247
268
  | StormEventScreenCandidate
248
- | StormEventFile
269
+ | StormEventFileLogical
270
+ | StormEventFileState
271
+ | StormEventFileContent
249
272
  | StormEventDone
250
273
  | StormEventDefinitionChange
251
274
  | StormEventErrorClassifier