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