@positronic/cli 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/src/cli.js +16 -1
  2. package/dist/src/commands/helpers.js +11 -25
  3. package/dist/types/cli.d.ts.map +1 -1
  4. package/dist/types/commands/helpers.d.ts.map +1 -1
  5. package/package.json +11 -4
  6. package/dist/src/commands/brain.test.js +0 -2936
  7. package/dist/src/commands/helpers.test.js +0 -832
  8. package/dist/src/commands/project.test.js +0 -1201
  9. package/dist/src/commands/resources.test.js +0 -2511
  10. package/dist/src/commands/schedule.test.js +0 -1235
  11. package/dist/src/commands/secret.test.d.js +0 -1
  12. package/dist/src/commands/secret.test.js +0 -761
  13. package/dist/src/commands/server.test.js +0 -1237
  14. package/dist/src/commands/test-utils.js +0 -737
  15. package/dist/src/components/secret-sync.js +0 -303
  16. package/dist/src/test/mock-api-client.js +0 -371
  17. package/dist/src/test/test-dev-server.js +0 -1376
  18. package/dist/types/commands/test-utils.d.ts +0 -45
  19. package/dist/types/commands/test-utils.d.ts.map +0 -1
  20. package/dist/types/components/secret-sync.d.ts +0 -9
  21. package/dist/types/components/secret-sync.d.ts.map +0 -1
  22. package/dist/types/test/mock-api-client.d.ts +0 -25
  23. package/dist/types/test/mock-api-client.d.ts.map +0 -1
  24. package/dist/types/test/test-dev-server.d.ts +0 -129
  25. package/dist/types/test/test-dev-server.d.ts.map +0 -1
  26. package/src/cli.ts +0 -981
  27. package/src/commands/backend.ts +0 -63
  28. package/src/commands/brain.test.ts +0 -1004
  29. package/src/commands/brain.ts +0 -215
  30. package/src/commands/helpers.test.ts +0 -487
  31. package/src/commands/helpers.ts +0 -870
  32. package/src/commands/project-config-manager.ts +0 -152
  33. package/src/commands/project.test.ts +0 -502
  34. package/src/commands/project.ts +0 -109
  35. package/src/commands/resources.test.ts +0 -1052
  36. package/src/commands/resources.ts +0 -97
  37. package/src/commands/schedule.test.ts +0 -481
  38. package/src/commands/schedule.ts +0 -65
  39. package/src/commands/secret.test.ts +0 -210
  40. package/src/commands/secret.ts +0 -50
  41. package/src/commands/server.test.ts +0 -493
  42. package/src/commands/server.ts +0 -353
  43. package/src/commands/test-utils.ts +0 -324
  44. package/src/components/brain-history.tsx +0 -198
  45. package/src/components/brain-list.tsx +0 -105
  46. package/src/components/brain-rerun.tsx +0 -111
  47. package/src/components/brain-show.tsx +0 -92
  48. package/src/components/error.tsx +0 -24
  49. package/src/components/project-add.tsx +0 -59
  50. package/src/components/project-create.tsx +0 -83
  51. package/src/components/project-list.tsx +0 -83
  52. package/src/components/project-remove.tsx +0 -55
  53. package/src/components/project-select.tsx +0 -200
  54. package/src/components/project-show.tsx +0 -58
  55. package/src/components/resource-clear.tsx +0 -127
  56. package/src/components/resource-delete.tsx +0 -160
  57. package/src/components/resource-list.tsx +0 -177
  58. package/src/components/resource-sync.tsx +0 -170
  59. package/src/components/resource-types.tsx +0 -55
  60. package/src/components/resource-upload.tsx +0 -182
  61. package/src/components/schedule-create.tsx +0 -90
  62. package/src/components/schedule-delete.tsx +0 -116
  63. package/src/components/schedule-list.tsx +0 -186
  64. package/src/components/schedule-runs.tsx +0 -151
  65. package/src/components/secret-bulk.tsx +0 -79
  66. package/src/components/secret-create.tsx +0 -49
  67. package/src/components/secret-delete.tsx +0 -41
  68. package/src/components/secret-list.tsx +0 -41
  69. package/src/components/watch.tsx +0 -155
  70. package/src/hooks/useApi.ts +0 -183
  71. package/src/positronic.ts +0 -40
  72. package/src/test/data/resources/config.json +0 -1
  73. package/src/test/data/resources/data/config.json +0 -1
  74. package/src/test/data/resources/data/logo.png +0 -2
  75. package/src/test/data/resources/docs/api.md +0 -3
  76. package/src/test/data/resources/docs/readme.md +0 -3
  77. package/src/test/data/resources/example.md +0 -3
  78. package/src/test/data/resources/file with spaces.txt +0 -1
  79. package/src/test/data/resources/readme.md +0 -3
  80. package/src/test/data/resources/test.txt +0 -1
  81. package/src/test/mock-api-client.ts +0 -145
  82. package/src/test/test-dev-server.ts +0 -1003
  83. package/tsconfig.json +0 -11
@@ -1,1003 +0,0 @@
1
- import type { PositronicDevServer, ServerHandle } from '@positronic/spec';
2
- import nock from 'nock';
3
- import { parse } from 'dotenv';
4
- import fs from 'fs';
5
-
6
- interface MockResource {
7
- key: string;
8
- type: 'text' | 'binary';
9
- size: number;
10
- lastModified: string;
11
- local?: boolean;
12
- }
13
-
14
- export interface MethodCall {
15
- method: string;
16
- args: any[];
17
- timestamp: number;
18
- }
19
-
20
- /**
21
- * Extended ServerHandle interface for testing that includes test-specific methods
22
- */
23
- export interface TestServerHandle extends ServerHandle {
24
- /**
25
- * Get the method call logs
26
- */
27
- getLogs(): MethodCall[];
28
-
29
- /**
30
- * Clear the method call logs
31
- */
32
- clearLogs(): void;
33
- }
34
-
35
- /**
36
- * Mock implementation of TestServerHandle for testing
37
- * Since we're using nock, there's no real process to manage
38
- */
39
- class MockServerHandle implements TestServerHandle {
40
- private _killed = false;
41
- private _errorCallback?: (error: Error) => void;
42
- private _closeCallback?: (code?: number | null) => void;
43
-
44
- constructor(
45
- private stopFn: () => void,
46
- private getLogsFn: () => MethodCall[],
47
- private clearLogsFn: () => void,
48
- private port: number
49
- ) {}
50
-
51
- onClose(callback: (code?: number | null) => void): void {
52
- this._closeCallback = callback;
53
- }
54
-
55
- onError(callback: (error: Error) => void): void {
56
- this._errorCallback = callback;
57
- }
58
-
59
- kill(signal?: string): boolean {
60
- if (!this._killed) {
61
- this.stopFn();
62
- this._killed = true;
63
- return true;
64
- }
65
- return false;
66
- }
67
-
68
- get killed(): boolean {
69
- return this._killed;
70
- }
71
-
72
- async waitUntilReady(maxWaitMs?: number): Promise<boolean> {
73
- // Test server with nock is always ready immediately
74
- return true;
75
- }
76
-
77
- getLogs(): MethodCall[] {
78
- return this.getLogsFn();
79
- }
80
-
81
- clearLogs(): void {
82
- this.clearLogsFn();
83
- }
84
- }
85
-
86
- interface MockSchedule {
87
- id: string;
88
- brainName: string;
89
- cronExpression: string;
90
- enabled: boolean;
91
- createdAt: number;
92
- nextRunAt?: number;
93
- }
94
-
95
- interface MockScheduleRun {
96
- id: string;
97
- scheduleId: string;
98
- status: 'triggered' | 'failed';
99
- ranAt: number;
100
- brainRunId?: string;
101
- error?: string;
102
- }
103
-
104
- interface MockBrain {
105
- name: string;
106
- title: string;
107
- description: string;
108
- createdAt?: number;
109
- lastModified?: number;
110
- steps?: Array<{
111
- type: 'step' | 'brain';
112
- title: string;
113
- innerBrain?: {
114
- title: string;
115
- description?: string;
116
- steps: any[];
117
- };
118
- }>;
119
- }
120
-
121
- interface MockBrainRun {
122
- brainRunId: string;
123
- brainTitle: string;
124
- brainDescription?: string;
125
- type: string;
126
- status: 'PENDING' | 'RUNNING' | 'COMPLETE' | 'ERROR';
127
- options?: any;
128
- error?: any;
129
- createdAt: number;
130
- startedAt?: number;
131
- completedAt?: number;
132
- }
133
-
134
- interface MockSecret {
135
- name: string;
136
- value: string;
137
- createdAt: string;
138
- updatedAt: string;
139
- }
140
-
141
- export class TestDevServer implements PositronicDevServer {
142
- private resources: Map<string, MockResource> = new Map();
143
- private schedules: Map<string, MockSchedule> = new Map();
144
- private scheduleRuns: MockScheduleRun[] = [];
145
- private brains: Map<string, MockBrain> = new Map();
146
- private brainRuns: MockBrainRun[] = [];
147
- private secrets: Map<string, MockSecret> = new Map();
148
- public port: number = 0;
149
- private callLog: MethodCall[] = [];
150
- private nockScope: nock.Scope | null = null;
151
- private logCallbacks: Array<(message: string) => void> = [];
152
- private errorCallbacks: Array<(message: string) => void> = [];
153
- private warningCallbacks: Array<(message: string) => void> = [];
154
-
155
- constructor(public projectRootDir: string = '') {}
156
-
157
- private logCall(method: string, args: any[]) {
158
- const call: MethodCall = {
159
- method,
160
- args,
161
- timestamp: Date.now(),
162
- };
163
- this.callLog.push(call);
164
- }
165
-
166
- async deploy(config?: any): Promise<void> {
167
- this.logCall('deploy', [this.projectRootDir, config]);
168
- }
169
-
170
- async setup(force?: boolean): Promise<void> {
171
- this.logCall('setup', [force]);
172
- // For tests, we don't need to set up .positronic directory
173
- // Just ensure we're ready to serve
174
- }
175
-
176
- public getLogs(): MethodCall[] {
177
- return this.callLog;
178
- }
179
-
180
- async start(port?: number): Promise<TestServerHandle> {
181
- this.logCall('start', [port]);
182
-
183
- this.port = port || 9000 + Math.floor(Math.random() * 1000);
184
-
185
- process.env.POSITRONIC_PORT = this.port.toString();
186
-
187
- // Set up nock interceptors for all endpoints
188
- const nockInstance = nock(`http://localhost:${this.port}`).persist();
189
-
190
- // GET /resources
191
- nockInstance.get('/resources').reply(200, () => {
192
- // Return the current in-memory resource list (populated by uploads)
193
- const resources = Array.from(this.resources.values());
194
- return {
195
- resources,
196
- truncated: false,
197
- count: resources.length,
198
- };
199
- });
200
-
201
- // POST /resources
202
- nockInstance.post('/resources').reply(201, (uri, requestBody) => {
203
- // Convert request body to string using latin1 encoding to preserve binary data
204
- let bodyString = Buffer.isBuffer(requestBody)
205
- ? requestBody.toString('latin1')
206
- : typeof requestBody === 'string'
207
- ? requestBody
208
- : JSON.stringify(requestBody);
209
-
210
- // Check if the body string appears to be hex-encoded (all characters are hex)
211
- const isHexEncoded = /^[0-9a-fA-F]+$/.test(bodyString);
212
- if (isHexEncoded) {
213
- // Convert hex string back to buffer and then to latin1 string
214
- const hexBuffer = Buffer.from(bodyString, 'hex');
215
- bodyString = hexBuffer.toString('latin1');
216
- }
217
-
218
- // Attempt to extract the "key" (resource path) and "type" fields from the multipart data
219
- const keyMatch = bodyString.match(/name="key"\s*\r?\n\r?\n([^\r\n]+)/);
220
- const typeMatch = bodyString.match(/name="type"\s*\r?\n\r?\n([^\r\n]+)/);
221
-
222
- const key = keyMatch ? keyMatch[1] : undefined;
223
- const type = typeMatch ? (typeMatch[1] as 'text' | 'binary') : 'text';
224
-
225
- if (key) {
226
- this.resources.set(key, {
227
- key,
228
- type,
229
- size: 0,
230
- lastModified: new Date().toISOString(),
231
- });
232
- }
233
-
234
- // Log the upload so tests can verify resource sync behavior
235
- this.logCall('upload', [bodyString]);
236
-
237
- // Success response
238
- return '';
239
- });
240
-
241
- // DELETE /resources (bulk delete all)
242
- nockInstance.delete('/resources').reply(204, () => {
243
- this.resources.clear();
244
- this.logCall('deleteAllResources', []);
245
- return '';
246
- });
247
-
248
- // DELETE /resources/:key
249
- nockInstance.delete(/^\/resources\/(.+)$/).reply((uri) => {
250
- const match = uri.match(/^\/resources\/(.+)$/);
251
- if (match) {
252
- const key = decodeURIComponent(match[1]);
253
- if (this.resources.has(key)) {
254
- this.resources.delete(key);
255
- this.logCall('deleteResource', [key]);
256
- return [204, ''];
257
- } else {
258
- // Check if it was already deleted (idempotent delete)
259
- const wasDeleted = this.callLog.some(
260
- (call) => call.method === 'deleteResource' && call.args[0] === key
261
- );
262
- if (wasDeleted) {
263
- // Return success for idempotent delete
264
- return [204, ''];
265
- }
266
- return [
267
- 404,
268
- JSON.stringify({ error: `Resource "${key}" not found` }),
269
- ];
270
- }
271
- }
272
- return [404, 'Not Found'];
273
- });
274
-
275
- // POST /brains/runs
276
- nockInstance.post('/brains/runs').reply((uri, requestBody) => {
277
- const body =
278
- typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
279
-
280
- // Check if brain exists (for testing brain not found scenario)
281
- if (body.brainName === 'non-existent-brain') {
282
- this.logCall('createBrainRun', [body.brainName]);
283
- return [404, { error: `Brain '${body.brainName}' not found` }];
284
- }
285
-
286
- let brainRunId = `run-${Date.now()}`;
287
-
288
- // Return specific runIds for specific test scenarios
289
- if (body.brainName === 'error-brain') {
290
- brainRunId = 'test-error-brain';
291
- } else if (body.brainName === 'restart-brain') {
292
- brainRunId = 'test-restart-brain';
293
- } else if (body.brainName === 'multi-status-brain') {
294
- brainRunId = 'test-multi-status';
295
- }
296
-
297
- this.logCall('createBrainRun', [brainRunId]);
298
- return [201, { brainRunId }];
299
- });
300
-
301
- // POST /brains/runs/rerun
302
- nockInstance.post('/brains/runs/rerun').reply((uri, requestBody) => {
303
- const body =
304
- typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
305
-
306
- // Check if brain exists
307
- if (body.brainName === 'non-existent-brain') {
308
- this.logCall('rerunBrain', [
309
- body.brainName,
310
- body.runId,
311
- body.startsAt,
312
- body.stopsAfter,
313
- ]);
314
- return [404, { error: `Brain '${body.brainName}' not found` }];
315
- }
316
-
317
- // Check if run ID exists (if provided)
318
- if (body.runId === 'non-existent-run') {
319
- this.logCall('rerunBrain', [
320
- body.brainName,
321
- body.runId,
322
- body.startsAt,
323
- body.stopsAfter,
324
- ]);
325
- return [404, { error: `Brain run '${body.runId}' not found` }];
326
- }
327
-
328
- const newBrainRunId = `rerun-${Date.now()}`;
329
-
330
- this.logCall('rerunBrain', [
331
- body.brainName,
332
- body.runId,
333
- body.startsAt,
334
- body.stopsAfter,
335
- ]);
336
- return [201, { brainRunId: newBrainRunId }];
337
- });
338
-
339
- // GET /brains/runs/:runId/watch (SSE endpoint)
340
- nockInstance.get(/^\/brains\/runs\/(.+)\/watch$/).reply(
341
- 200,
342
- function (uri) {
343
- const match = uri.match(/^\/brains\/runs\/(.+)\/watch$/);
344
- if (match) {
345
- const runId = match[1];
346
-
347
- // Different scenarios based on runId
348
- if (runId === 'test-error-brain') {
349
- // Error scenario
350
- return [
351
- `data: ${JSON.stringify({
352
- type: 'brain:start',
353
- brainTitle: 'Error Brain',
354
- brainRunId: runId,
355
- options: {},
356
- status: 'running',
357
- initialState: {},
358
- })}\n\n`,
359
- `data: ${JSON.stringify({
360
- type: 'brain:error',
361
- brainRunId: runId,
362
- brainTitle: 'Error Brain',
363
- options: {},
364
- status: 'error',
365
- error: {
366
- name: 'TestError',
367
- message: 'Something went wrong in the brain',
368
- stack: 'Error: Something went wrong\n at test.js:1:1',
369
- },
370
- })}\n\n`,
371
- ].join('');
372
- } else if (runId === 'test-restart-brain') {
373
- // Restart scenario
374
- return [
375
- `data: ${JSON.stringify({
376
- type: 'brain:restart',
377
- brainTitle: 'Restarted Brain',
378
- brainRunId: runId,
379
- options: {},
380
- status: 'running',
381
- initialState: {},
382
- })}\n\n`,
383
- `data: ${JSON.stringify({
384
- type: 'step:status',
385
- brainRunId: runId,
386
- options: {},
387
- steps: [
388
- {
389
- id: 'restart-step-1',
390
- title: 'Restart Step',
391
- status: 'pending',
392
- },
393
- ],
394
- })}\n\n`,
395
- ].join('');
396
- } else if (runId === 'test-multi-status') {
397
- // Multiple step statuses
398
- return [
399
- `data: ${JSON.stringify({
400
- type: 'brain:start',
401
- brainTitle: 'Multi Status Brain',
402
- brainRunId: runId,
403
- options: {},
404
- status: 'running',
405
- initialState: {},
406
- })}\n\n`,
407
- `data: ${JSON.stringify({
408
- type: 'step:status',
409
- brainRunId: runId,
410
- options: {},
411
- steps: [
412
- { id: 'step-1', title: 'Complete Step', status: 'complete' },
413
- { id: 'step-2', title: 'Error Step', status: 'error' },
414
- { id: 'step-3', title: 'Running Step', status: 'running' },
415
- { id: 'step-4', title: 'Pending Step', status: 'pending' },
416
- ],
417
- })}\n\n`,
418
- ].join('');
419
- } else if (runId === 'test-complete-flow') {
420
- // Full flow from start to complete
421
- return [
422
- `data: ${JSON.stringify({
423
- type: 'brain:start',
424
- brainTitle: 'Complete Flow Brain',
425
- brainRunId: runId,
426
- options: {},
427
- status: 'running',
428
- initialState: {},
429
- })}\n\n`,
430
- `data: ${JSON.stringify({
431
- type: 'step:status',
432
- brainRunId: runId,
433
- options: {},
434
- steps: [
435
- { id: 'step-1', title: 'First Step', status: 'complete' },
436
- { id: 'step-2', title: 'Second Step', status: 'complete' },
437
- ],
438
- })}\n\n`,
439
- `data: ${JSON.stringify({
440
- type: 'brain:complete',
441
- brainRunId: runId,
442
- brainTitle: 'Complete Flow Brain',
443
- options: {},
444
- status: 'complete',
445
- })}\n\n`,
446
- ].join('');
447
- } else if (runId === 'test-brain-error') {
448
- // Brain error scenario
449
- return [
450
- `data: ${JSON.stringify({
451
- type: 'brain:start',
452
- brainTitle: 'Error Brain',
453
- brainRunId: runId,
454
- options: {},
455
- status: 'running',
456
- initialState: {},
457
- })}\n\n`,
458
- `data: ${JSON.stringify({
459
- type: 'brain:error',
460
- brainRunId: runId,
461
- brainTitle: 'Error Brain',
462
- error: {
463
- name: 'BrainExecutionError',
464
- message: 'Something went wrong during brain execution',
465
- stack:
466
- 'Error: Something went wrong during brain execution\n at BrainRunner.run',
467
- },
468
- })}\n\n`,
469
- ].join('');
470
- } else if (runId === 'test-malformed-event') {
471
- // Send malformed JSON
472
- return 'data: {invalid json here}\n\n';
473
- } else if (runId === 'test-no-steps') {
474
- // Brain with no steps initially
475
- return [
476
- `data: ${JSON.stringify({
477
- type: 'brain:start',
478
- brainTitle: 'No Steps Brain',
479
- brainRunId: runId,
480
- options: {},
481
- status: 'running',
482
- initialState: {},
483
- })}\n\n`,
484
- `data: ${JSON.stringify({
485
- type: 'step:status',
486
- brainRunId: runId,
487
- options: {},
488
- steps: [],
489
- })}\n\n`,
490
- ].join('');
491
- } else if (runId === 'test-connection-error') {
492
- // Simulate connection error by returning error
493
- throw new Error('ECONNREFUSED');
494
- } else {
495
- // Default scenario
496
- const mockEvents = [
497
- `data: ${JSON.stringify({
498
- type: 'brain:start',
499
- brainTitle: 'test-brain',
500
- brainRunId: runId,
501
- options: {},
502
- status: 'running',
503
- initialState: {},
504
- })}\n\n`,
505
- `data: ${JSON.stringify({
506
- type: 'step:status',
507
- brainRunId: runId,
508
- options: {},
509
- steps: [
510
- {
511
- id: 'step-1',
512
- title: 'Test Step 1',
513
- status: 'running',
514
- },
515
- ],
516
- })}\n\n`,
517
- `data: ${JSON.stringify({
518
- type: 'brain:complete',
519
- brainRunId: runId,
520
- brainTitle: 'test-brain',
521
- options: {},
522
- status: 'complete',
523
- })}\n\n`,
524
- ];
525
- return mockEvents.join('');
526
- }
527
- }
528
- return 'data: {"type":"ERROR","error":{"message":"Invalid run ID"}}\n\n';
529
- },
530
- {
531
- 'Content-Type': 'text/event-stream',
532
- 'Cache-Control': 'no-cache',
533
- Connection: 'keep-alive',
534
- }
535
- );
536
-
537
- // GET /brains
538
- nockInstance.get('/brains').reply(200, () => {
539
- const brains = Array.from(this.brains.values());
540
- this.logCall('getBrains', []);
541
- return {
542
- brains,
543
- count: brains.length,
544
- };
545
- });
546
-
547
- // GET /brains/schedules
548
- nockInstance.get('/brains/schedules').reply(200, () => {
549
- const schedules = Array.from(this.schedules.values());
550
- this.logCall('getSchedules', []);
551
- return {
552
- schedules,
553
- count: schedules.length,
554
- };
555
- });
556
-
557
- // POST /brains/schedules
558
- nockInstance.post('/brains/schedules').reply(201, (uri, requestBody) => {
559
- const body =
560
- typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
561
- const scheduleId = `schedule-${Date.now()}`;
562
- const schedule: MockSchedule = {
563
- id: scheduleId,
564
- brainName: body.brainName,
565
- cronExpression: body.cronExpression,
566
- enabled: true,
567
- createdAt: Date.now(),
568
- nextRunAt: Date.now() + 3600000, // 1 hour from now
569
- };
570
- this.schedules.set(scheduleId, schedule);
571
- this.logCall('createSchedule', [body]);
572
- return schedule;
573
- });
574
-
575
- // GET /brains/schedules/runs
576
- nockInstance
577
- .get('/brains/schedules/runs')
578
- .query(true)
579
- .reply((uri) => {
580
- const url = new URL(uri, 'http://example.com');
581
- const scheduleId = url.searchParams.get('scheduleId');
582
- const limit = parseInt(url.searchParams.get('limit') || '100', 10);
583
-
584
- this.logCall('getScheduleRuns', [uri]);
585
-
586
- let runs = this.scheduleRuns;
587
-
588
- // Filter by scheduleId if provided
589
- if (scheduleId) {
590
- runs = runs.filter((run) => run.scheduleId === scheduleId);
591
- }
592
-
593
- // Sort by ranAt descending (newest first)
594
- runs = runs.sort((a, b) => b.ranAt - a.ranAt);
595
-
596
- // Apply limit
597
- runs = runs.slice(0, limit);
598
-
599
- return [
600
- 200,
601
- {
602
- runs,
603
- count: runs.length,
604
- },
605
- ];
606
- });
607
-
608
- // DELETE /brains/schedules/:id
609
- nockInstance.delete(/^\/brains\/schedules\/(.+)$/).reply((uri) => {
610
- const match = uri.match(/^\/brains\/schedules\/(.+)$/);
611
- if (match) {
612
- const scheduleId = decodeURIComponent(match[1]);
613
- if (this.schedules.has(scheduleId)) {
614
- this.schedules.delete(scheduleId);
615
- this.logCall('deleteSchedule', [scheduleId]);
616
- return [204, ''];
617
- } else {
618
- return [
619
- 404,
620
- JSON.stringify({ error: `Schedule "${scheduleId}" not found` }),
621
- ];
622
- }
623
- }
624
- return [404, 'Not Found'];
625
- });
626
-
627
- // GET /brains/:brainName/history
628
- nockInstance
629
- .get(/^\/brains\/(.+)\/history$/)
630
- .query(true)
631
- .reply((uri) => {
632
- const parts = uri.split('/');
633
- const brainName = decodeURIComponent(parts[2]);
634
- const url = new URL(uri, 'http://example.com');
635
- const limit = parseInt(url.searchParams.get('limit') || '10', 10);
636
-
637
- this.logCall('getBrainHistory', [brainName, limit]);
638
-
639
- // Filter runs by brain title
640
- const runs = this.brainRuns
641
- .filter(
642
- (run) =>
643
- run.brainTitle.toLowerCase() ===
644
- brainName.toLowerCase().replace(/-/g, ' ')
645
- )
646
- .sort((a, b) => b.createdAt - a.createdAt)
647
- .slice(0, limit);
648
-
649
- return [200, { runs }];
650
- });
651
-
652
- // GET /brains/:brainName/active-runs
653
- nockInstance.get(/^\/brains\/(.+)\/active-runs$/).reply((uri) => {
654
- const parts = uri.split('/');
655
- const brainName = decodeURIComponent(parts[2]);
656
-
657
- this.logCall('getBrainActiveRuns', [brainName]);
658
-
659
- // Filter brain runs by brain title and status RUNNING
660
- const activeRuns = this.brainRuns
661
- .filter(
662
- (run) =>
663
- run.brainTitle.toLowerCase() ===
664
- brainName.toLowerCase().replace(/-/g, ' ') &&
665
- run.status === 'RUNNING'
666
- )
667
- .sort((a, b) => b.createdAt - a.createdAt);
668
-
669
- return [200, { runs: activeRuns }];
670
- });
671
-
672
- // GET /brains/:brainName
673
- nockInstance.get(/^\/brains\/(.+)$/).reply((uri) => {
674
- const brainName = decodeURIComponent(uri.split('/')[2]);
675
- const brain = this.brains.get(brainName);
676
- this.logCall('getBrain', [brainName]);
677
-
678
- if (!brain) {
679
- return [404, { error: `Brain '${brainName}' not found` }];
680
- }
681
-
682
- return [
683
- 200,
684
- {
685
- name: brain.name,
686
- title: brain.title,
687
- description: brain.description || `${brain.title} brain`,
688
- steps: brain.steps || [],
689
- },
690
- ];
691
- });
692
-
693
- // Secret Management Endpoints
694
-
695
- // POST /secrets
696
- nockInstance.post('/secrets').reply(201, (uri, requestBody) => {
697
- const body =
698
- typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
699
-
700
- const now = new Date().toISOString();
701
- const secret: MockSecret = {
702
- name: body.name,
703
- value: body.value,
704
- createdAt: now,
705
- updatedAt: now,
706
- };
707
-
708
- this.secrets.set(body.name, secret);
709
- this.logCall('createSecret', [body.name]);
710
-
711
- // Return without the value for security
712
- return {
713
- name: secret.name,
714
- createdAt: secret.createdAt,
715
- updatedAt: secret.updatedAt,
716
- };
717
- });
718
-
719
- // GET /secrets
720
- nockInstance.get('/secrets').reply(200, () => {
721
- this.logCall('listSecrets', []);
722
-
723
- // Return secrets without values
724
- const secrets = Array.from(this.secrets.values()).map((secret) => ({
725
- name: secret.name,
726
- createdAt: secret.createdAt,
727
- updatedAt: secret.updatedAt,
728
- }));
729
-
730
- return {
731
- secrets,
732
- count: secrets.length,
733
- };
734
- });
735
-
736
- // DELETE /secrets/:name
737
- nockInstance.delete(/^\/secrets\/(.+)$/).reply((uri) => {
738
- const match = uri.match(/^\/secrets\/(.+)$/);
739
- if (match) {
740
- const secretName = decodeURIComponent(match[1]);
741
-
742
- if (this.secrets.has(secretName)) {
743
- this.secrets.delete(secretName);
744
- this.logCall('deleteSecret', [secretName]);
745
- return [204, ''];
746
- } else {
747
- return [404, { error: `Secret "${secretName}" not found` }];
748
- }
749
- }
750
- return [404, 'Not Found'];
751
- });
752
-
753
- // GET /secrets/:name/exists
754
- nockInstance.get(/^\/secrets\/(.+)\/exists$/).reply((uri) => {
755
- const match = uri.match(/^\/secrets\/(.+)\/exists$/);
756
- if (match) {
757
- const secretName = decodeURIComponent(match[1]);
758
- const exists = this.secrets.has(secretName);
759
-
760
- this.logCall('secretExists', [secretName]);
761
-
762
- return [200, { exists }];
763
- }
764
- return [404, 'Not Found'];
765
- });
766
-
767
- // POST /secrets/bulk
768
- nockInstance.post('/secrets/bulk').reply(201, (uri, requestBody) => {
769
- const body =
770
- typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
771
-
772
- let created = 0;
773
- let updated = 0;
774
- const now = new Date().toISOString();
775
-
776
- for (const secretData of body.secrets) {
777
- const existing = this.secrets.has(secretData.name);
778
-
779
- const secret: MockSecret = {
780
- name: secretData.name,
781
- value: secretData.value,
782
- createdAt: existing
783
- ? this.secrets.get(secretData.name)!.createdAt
784
- : now,
785
- updatedAt: now,
786
- };
787
-
788
- this.secrets.set(secretData.name, secret);
789
-
790
- if (existing) {
791
- updated++;
792
- } else {
793
- created++;
794
- }
795
- }
796
-
797
- this.logCall('bulkCreateSecrets', [body.secrets.length]);
798
-
799
- return { created, updated };
800
- });
801
-
802
- this.nockScope = nockInstance;
803
-
804
- // Simulate some initial log output after server starts
805
- setTimeout(() => {
806
- this.logCallbacks.forEach((cb) =>
807
- cb('✅ Synced 3 resources (0 up to date, 0 deleted)\n')
808
- );
809
- this.logCallbacks.forEach((cb) =>
810
- cb('🚀 Server started on port ' + this.port + '\n')
811
- );
812
- }, 100);
813
-
814
- return new MockServerHandle(
815
- () => {
816
- this.stop();
817
- },
818
- () => this.callLog,
819
- () => {
820
- this.callLog = [];
821
- },
822
- this.port
823
- );
824
- }
825
-
826
- // Add watch method to track calls
827
- async watch(event: 'add' | 'change' | 'unlink') {
828
- this.logCall('watch', [this.projectRootDir, event]);
829
- }
830
-
831
- // Helper methods for tests to manipulate the server state
832
- addResource(resource: MockResource) {
833
- this.resources.set(resource.key, resource);
834
- }
835
-
836
- clearResources() {
837
- this.resources.clear();
838
- }
839
-
840
- getResources(): MockResource[] {
841
- return Array.from(this.resources.values());
842
- }
843
-
844
- // Schedule helper methods
845
- addSchedule(schedule: MockSchedule) {
846
- this.schedules.set(schedule.id, schedule);
847
- }
848
-
849
- clearSchedules() {
850
- this.schedules.clear();
851
- }
852
-
853
- addScheduleRun(run: MockScheduleRun) {
854
- this.scheduleRuns.push(run);
855
- }
856
-
857
- clearScheduleRuns() {
858
- this.scheduleRuns = [];
859
- }
860
-
861
- getSchedules(): MockSchedule[] {
862
- return Array.from(this.schedules.values());
863
- }
864
-
865
- // Secret helper methods
866
- addSecret(name: string, value: string) {
867
- const now = new Date().toISOString();
868
- this.secrets.set(name, {
869
- name,
870
- value,
871
- createdAt: now,
872
- updatedAt: now,
873
- });
874
- }
875
-
876
- clearSecrets() {
877
- this.secrets.clear();
878
- }
879
-
880
- getSecrets(): MockSecret[] {
881
- return Array.from(this.secrets.values());
882
- }
883
-
884
- getSecret(name: string): MockSecret | undefined {
885
- return this.secrets.get(name);
886
- }
887
-
888
- // Brain helper methods
889
- addBrain(brain: MockBrain) {
890
- this.brains.set(brain.name, brain);
891
- }
892
-
893
- addBrainRun(run: MockBrainRun) {
894
- this.brainRuns.push(run);
895
- }
896
-
897
- clearBrainRuns() {
898
- this.brainRuns = [];
899
- }
900
-
901
- clearBrains() {
902
- this.brains.clear();
903
- }
904
-
905
- getBrains(): MockBrain[] {
906
- return Array.from(this.brains.values());
907
- }
908
-
909
- stop() {
910
- if (this.nockScope) {
911
- // Clean up all nock interceptors
912
- nock.cleanAll();
913
- this.nockScope = null;
914
- }
915
- }
916
-
917
- onLog(callback: (message: string) => void): void {
918
- this.logCall('onLog', ['callback registered']);
919
- this.logCallbacks.push(callback);
920
- }
921
-
922
- onError(callback: (message: string) => void): void {
923
- this.logCall('onError', ['callback registered']);
924
- this.errorCallbacks.push(callback);
925
- }
926
-
927
- onWarning(callback: (message: string) => void): void {
928
- this.logCall('onWarning', ['callback registered']);
929
- this.warningCallbacks.push(callback);
930
- }
931
-
932
- async listSecrets(): Promise<
933
- Array<{ name: string; createdAt?: Date; updatedAt?: Date }>
934
- > {
935
- this.logCall('listSecrets', []);
936
- return Array.from(this.secrets.values()).map((secret) => ({
937
- name: secret.name,
938
- createdAt: new Date(secret.createdAt),
939
- updatedAt: new Date(secret.updatedAt),
940
- }));
941
- }
942
-
943
- async setSecret(name: string, value: string): Promise<void> {
944
- this.logCall('setSecret', [name, value]);
945
- const now = new Date().toISOString();
946
- const existing = this.secrets.get(name);
947
-
948
- this.secrets.set(name, {
949
- name,
950
- value,
951
- createdAt: existing?.createdAt || now,
952
- updatedAt: now,
953
- });
954
- }
955
-
956
- async deleteSecret(name: string): Promise<boolean> {
957
- this.logCall('deleteSecret', [name]);
958
- if (this.secrets.has(name)) {
959
- this.secrets.delete(name);
960
- return true;
961
- }
962
- return false;
963
- }
964
-
965
- async bulkSecrets(filePath: string): Promise<void> {
966
- this.logCall('bulkSecrets', [filePath]);
967
-
968
- try {
969
- // Read and parse the .env file
970
- if (!fs.existsSync(filePath)) {
971
- throw new Error(`File not found: ${filePath}`);
972
- }
973
-
974
- const envContent = fs.readFileSync(filePath, 'utf8');
975
- const secrets = parse(envContent);
976
-
977
- const secretsArray = Object.entries(secrets);
978
-
979
- if (secretsArray.length === 0) {
980
- throw new Error('No secrets found in the .env file');
981
- }
982
-
983
- // Simulate the bulk upload - just store them
984
- const now = new Date().toISOString();
985
- for (const [name, value] of secretsArray) {
986
- const existing = this.secrets.has(name);
987
- this.secrets.set(name, {
988
- name,
989
- value,
990
- createdAt: existing ? this.secrets.get(name)!.createdAt : now,
991
- updatedAt: now,
992
- });
993
- }
994
-
995
- // Simulate console output
996
- this.logCallbacks.forEach((cb) =>
997
- cb(`✨ Successfully uploaded ${secretsArray.length} secrets\n`)
998
- );
999
- } catch (error) {
1000
- throw error;
1001
- }
1002
- }
1003
- }