@positronic/cli 0.0.3 → 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 (81) hide show
  1. package/dist/src/commands/helpers.js +11 -25
  2. package/dist/types/commands/helpers.d.ts.map +1 -1
  3. package/package.json +5 -1
  4. package/dist/src/commands/brain.test.js +0 -2936
  5. package/dist/src/commands/helpers.test.js +0 -832
  6. package/dist/src/commands/project.test.js +0 -1201
  7. package/dist/src/commands/resources.test.js +0 -2511
  8. package/dist/src/commands/schedule.test.js +0 -1235
  9. package/dist/src/commands/secret.test.d.js +0 -1
  10. package/dist/src/commands/secret.test.js +0 -761
  11. package/dist/src/commands/server.test.js +0 -1237
  12. package/dist/src/commands/test-utils.js +0 -737
  13. package/dist/src/components/secret-sync.js +0 -303
  14. package/dist/src/test/mock-api-client.js +0 -371
  15. package/dist/src/test/test-dev-server.js +0 -1376
  16. package/dist/types/commands/test-utils.d.ts +0 -45
  17. package/dist/types/commands/test-utils.d.ts.map +0 -1
  18. package/dist/types/components/secret-sync.d.ts +0 -9
  19. package/dist/types/components/secret-sync.d.ts.map +0 -1
  20. package/dist/types/test/mock-api-client.d.ts +0 -25
  21. package/dist/types/test/mock-api-client.d.ts.map +0 -1
  22. package/dist/types/test/test-dev-server.d.ts +0 -129
  23. package/dist/types/test/test-dev-server.d.ts.map +0 -1
  24. package/src/cli.ts +0 -997
  25. package/src/commands/backend.ts +0 -63
  26. package/src/commands/brain.test.ts +0 -1004
  27. package/src/commands/brain.ts +0 -215
  28. package/src/commands/helpers.test.ts +0 -487
  29. package/src/commands/helpers.ts +0 -870
  30. package/src/commands/project-config-manager.ts +0 -152
  31. package/src/commands/project.test.ts +0 -502
  32. package/src/commands/project.ts +0 -109
  33. package/src/commands/resources.test.ts +0 -1052
  34. package/src/commands/resources.ts +0 -97
  35. package/src/commands/schedule.test.ts +0 -481
  36. package/src/commands/schedule.ts +0 -65
  37. package/src/commands/secret.test.ts +0 -210
  38. package/src/commands/secret.ts +0 -50
  39. package/src/commands/server.test.ts +0 -493
  40. package/src/commands/server.ts +0 -353
  41. package/src/commands/test-utils.ts +0 -324
  42. package/src/components/brain-history.tsx +0 -198
  43. package/src/components/brain-list.tsx +0 -105
  44. package/src/components/brain-rerun.tsx +0 -111
  45. package/src/components/brain-show.tsx +0 -92
  46. package/src/components/error.tsx +0 -24
  47. package/src/components/project-add.tsx +0 -59
  48. package/src/components/project-create.tsx +0 -83
  49. package/src/components/project-list.tsx +0 -83
  50. package/src/components/project-remove.tsx +0 -55
  51. package/src/components/project-select.tsx +0 -200
  52. package/src/components/project-show.tsx +0 -58
  53. package/src/components/resource-clear.tsx +0 -127
  54. package/src/components/resource-delete.tsx +0 -160
  55. package/src/components/resource-list.tsx +0 -177
  56. package/src/components/resource-sync.tsx +0 -170
  57. package/src/components/resource-types.tsx +0 -55
  58. package/src/components/resource-upload.tsx +0 -182
  59. package/src/components/schedule-create.tsx +0 -90
  60. package/src/components/schedule-delete.tsx +0 -116
  61. package/src/components/schedule-list.tsx +0 -186
  62. package/src/components/schedule-runs.tsx +0 -151
  63. package/src/components/secret-bulk.tsx +0 -79
  64. package/src/components/secret-create.tsx +0 -49
  65. package/src/components/secret-delete.tsx +0 -41
  66. package/src/components/secret-list.tsx +0 -41
  67. package/src/components/watch.tsx +0 -155
  68. package/src/hooks/useApi.ts +0 -183
  69. package/src/positronic.ts +0 -40
  70. package/src/test/data/resources/config.json +0 -1
  71. package/src/test/data/resources/data/config.json +0 -1
  72. package/src/test/data/resources/data/logo.png +0 -2
  73. package/src/test/data/resources/docs/api.md +0 -3
  74. package/src/test/data/resources/docs/readme.md +0 -3
  75. package/src/test/data/resources/example.md +0 -3
  76. package/src/test/data/resources/file with spaces.txt +0 -1
  77. package/src/test/data/resources/readme.md +0 -3
  78. package/src/test/data/resources/test.txt +0 -1
  79. package/src/test/mock-api-client.ts +0 -145
  80. package/src/test/test-dev-server.ts +0 -1003
  81. package/tsconfig.json +0 -11
@@ -1,215 +0,0 @@
1
- import type { ArgumentsCamelCase } from 'yargs';
2
- import { apiClient } from './helpers.js';
3
- import React from 'react';
4
- import { Text } from 'ink';
5
- import { Watch } from '../components/watch.js';
6
- import { BrainList } from '../components/brain-list.js';
7
- import { BrainHistory } from '../components/brain-history.js';
8
- import { BrainShow } from '../components/brain-show.js';
9
- import { BrainRerun } from '../components/brain-rerun.js';
10
- import { ErrorComponent } from '../components/error.js';
11
-
12
- interface BrainListArgs {}
13
- interface BrainHistoryArgs {
14
- name: string;
15
- limit: number;
16
- }
17
- interface BrainShowArgs {
18
- name: string;
19
- }
20
- interface BrainRerunArgs {
21
- name: string;
22
- runId?: string;
23
- startsAt?: number;
24
- stopsAfter?: number;
25
- }
26
- interface BrainRunArgs {
27
- name: string;
28
- watch?: boolean;
29
- }
30
- interface BrainWatchArgs {
31
- runId?: string;
32
- name?: string;
33
- }
34
-
35
- export class BrainCommand {
36
- list(argv: ArgumentsCamelCase<BrainListArgs>): React.ReactElement {
37
- return React.createElement(BrainList);
38
- }
39
-
40
- history({
41
- name: brainName,
42
- limit,
43
- }: ArgumentsCamelCase<BrainHistoryArgs>): React.ReactElement {
44
- return React.createElement(BrainHistory, { brainName, limit });
45
- }
46
-
47
- show({
48
- name: brainName,
49
- }: ArgumentsCamelCase<BrainShowArgs>): React.ReactElement {
50
- return React.createElement(BrainShow, { brainName });
51
- }
52
-
53
- rerun({
54
- name: brainName,
55
- runId,
56
- startsAt,
57
- stopsAfter,
58
- }: ArgumentsCamelCase<BrainRerunArgs>): React.ReactElement {
59
- return React.createElement(BrainRerun, {
60
- brainName,
61
- runId,
62
- startsAt,
63
- stopsAfter,
64
- });
65
- }
66
-
67
- async run({ name: brainName, watch }: ArgumentsCamelCase<BrainRunArgs>): Promise<React.ReactElement> {
68
- const apiPath = '/brains/runs';
69
- try {
70
- const response = await apiClient.fetch(apiPath, {
71
- method: 'POST',
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- },
75
- body: JSON.stringify({ brainName }),
76
- });
77
-
78
- if (response.status === 201) {
79
- const result = (await response.json()) as { brainRunId: string };
80
-
81
- if (watch) {
82
- // Return Watch component for CLI to render
83
- return this.watch({
84
- runId: result.brainRunId,
85
- _: [],
86
- $0: '',
87
- });
88
- } else {
89
- // Return React element displaying the run ID
90
- return React.createElement(
91
- Text,
92
- null,
93
- `Run ID: ${result.brainRunId}`
94
- );
95
- }
96
- } else if (response.status === 404) {
97
- // Handle brain not found with a helpful message
98
- return React.createElement(ErrorComponent, {
99
- error: {
100
- title: 'Brain Not Found',
101
- message: `Brain '${brainName}' not found.`,
102
- details: 'Please check that:\n 1. The brain name is spelled correctly\n 2. The brain exists in your project\n 3. The brain has been properly defined and exported\n\nYou can list available brains with: positronic list'
103
- }
104
- });
105
- } else {
106
- const errorText = await response.text();
107
- console.error(
108
- `Error starting brain run: ${response.status} ${response.statusText}`
109
- );
110
- console.error(`Server response: ${errorText}`);
111
- process.exit(1);
112
- }
113
- } catch (error: any) {
114
- console.error(`Error connecting to the local development server.`);
115
- console.error(
116
- "Please ensure the server is running ('positronic server' or 'px s')."
117
- );
118
- if (error.code === 'ECONNREFUSED') {
119
- console.error(
120
- 'Reason: Connection refused. The server might not be running or is listening on a different port.'
121
- );
122
- } else {
123
- console.error(`Fetch error details: ${error.message}`);
124
- }
125
- process.exit(1);
126
- }
127
- }
128
-
129
- async watch({
130
- runId,
131
- name: brainName,
132
- }: ArgumentsCamelCase<BrainWatchArgs>): Promise<React.ReactElement> {
133
- // If a specific run ID is provided, return the Watch component
134
- if (runId) {
135
- const port = process.env.POSITRONIC_PORT || '8787';
136
- return React.createElement(Watch, { runId, port });
137
- }
138
-
139
- // If watching by brain name is requested, look up active runs
140
- if (brainName) {
141
- try {
142
- const apiPath = `/brains/${encodeURIComponent(brainName)}/active-runs`;
143
- const response = await apiClient.fetch(apiPath, {
144
- method: 'GET',
145
- });
146
-
147
- if (response.status === 200) {
148
- const result = await response.json() as { runs: Array<{ brainRunId: string; brainTitle: string; status: string; createdAt: number }> };
149
-
150
- if (result.runs.length === 0) {
151
- return React.createElement(
152
- ErrorComponent,
153
- {
154
- error: {
155
- title: 'No Active Runs',
156
- message: `No currently running brain runs found for brain "${brainName}".`,
157
- details: `To start a new run, use: positronic run ${brainName}`
158
- }
159
- }
160
- );
161
- }
162
-
163
- if (result.runs.length > 1) {
164
- return React.createElement(
165
- ErrorComponent,
166
- {
167
- error: {
168
- title: 'Multiple Active Runs',
169
- message: `Found ${result.runs.length} active runs for brain "${brainName}".`,
170
- details: `Please specify a specific run ID with --run-id:\n${result.runs.map(run => ` positronic watch --run-id ${run.brainRunId}`).join('\n')}`
171
- }
172
- }
173
- );
174
- }
175
-
176
- // Exactly one active run found - watch it
177
- const activeRun = result.runs[0];
178
- const port = process.env.POSITRONIC_PORT || '8787';
179
- return React.createElement(Watch, { runId: activeRun.brainRunId, port });
180
- } else {
181
- const errorText = await response.text();
182
- return React.createElement(
183
- ErrorComponent,
184
- {
185
- error: {
186
- title: 'API Error',
187
- message: `Failed to get active runs for brain "${brainName}".`,
188
- details: `Server returned ${response.status}: ${errorText}`
189
- }
190
- }
191
- );
192
- }
193
- } catch (error: any) {
194
- return React.createElement(
195
- ErrorComponent,
196
- {
197
- error: {
198
- title: 'Connection Error',
199
- message: 'Error connecting to the local development server.',
200
- details: `Please ensure the server is running ('positronic server' or 'px s').\n\nError details: ${error.message}`
201
- }
202
- }
203
- );
204
- }
205
- }
206
-
207
- // Neither runId nor brainName provided – return an error element.
208
- return React.createElement(
209
- Text,
210
- { color: 'red' as any }, // Ink Text color prop
211
- 'Error: You must provide either a brain run ID or a brain name.'
212
- );
213
- }
214
-
215
- }
@@ -1,487 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
- import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
5
- import { Response } from 'node-fetch';
6
- import { syncResources, generateTypes } from './helpers.js';
7
- import { createMockApiClient } from '../test/mock-api-client.js';
8
-
9
- describe('Helper Functions Unit Tests', () => {
10
- let tempDir: string;
11
- let projectPath: string;
12
- let mockClient: ReturnType<typeof createMockApiClient>;
13
-
14
- beforeEach(() => {
15
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'positronic-unit-test-'));
16
- projectPath = path.join(tempDir, 'test-project');
17
- fs.mkdirSync(projectPath, { recursive: true });
18
- mockClient = createMockApiClient();
19
- });
20
-
21
- afterEach(() => {
22
- fs.rmSync(tempDir, { recursive: true, force: true });
23
- mockClient.reset();
24
- });
25
-
26
- describe('syncResources', () => {
27
- it('should create resources directory if it does not exist', async () => {
28
- const resourcesDir = path.join(projectPath, 'resources');
29
- expect(fs.existsSync(resourcesDir)).toBe(false);
30
-
31
- await syncResources(projectPath, mockClient);
32
-
33
- expect(fs.existsSync(resourcesDir)).toBe(true);
34
- });
35
-
36
- it('should return zero counts for empty resources directory', async () => {
37
- const resourcesDir = path.join(projectPath, 'resources');
38
- fs.mkdirSync(resourcesDir, { recursive: true });
39
-
40
- const result = await syncResources(projectPath, mockClient);
41
-
42
- expect(result).toEqual({
43
- uploadCount: 0,
44
- skipCount: 0,
45
- errorCount: 0,
46
- totalCount: 0,
47
- deleteCount: 0,
48
- errors: [],
49
- });
50
-
51
- // Should not make any API calls for empty directory
52
- expect(mockClient.calls.length).toBe(1); // Only GET /resources call
53
- });
54
-
55
- it('should upload new resources', async () => {
56
- const resourcesDir = path.join(projectPath, 'resources');
57
- fs.mkdirSync(resourcesDir, { recursive: true });
58
-
59
- // Create test files
60
- fs.writeFileSync(path.join(resourcesDir, 'test.txt'), 'Hello World');
61
- fs.writeFileSync(
62
- path.join(resourcesDir, 'data.json'),
63
- '{"key": "value"}'
64
- );
65
-
66
- const result = await syncResources(projectPath, mockClient);
67
-
68
- expect(result.uploadCount).toBe(2);
69
- expect(result.skipCount).toBe(0);
70
- expect(result.errorCount).toBe(0);
71
- expect(result.totalCount).toBe(2);
72
-
73
- // Verify API calls
74
- expect(mockClient.calls).toHaveLength(3); // 1 GET + 2 POSTs
75
- expect(mockClient.calls[0].path).toBe('/resources');
76
- expect(mockClient.calls[1].path).toBe('/resources');
77
- expect(mockClient.calls[1].options?.method).toBe('POST');
78
-
79
- // Verify uploaded resources
80
- const resources = mockClient.getResources();
81
- expect(resources).toHaveLength(2);
82
- expect(resources.find((r) => r.key === 'test.txt')).toBeDefined();
83
- expect(resources.find((r) => r.key === 'data.json')).toBeDefined();
84
- });
85
-
86
- it('should skip unchanged resources', async () => {
87
- const resourcesDir = path.join(projectPath, 'resources');
88
- fs.mkdirSync(resourcesDir, { recursive: true });
89
-
90
- // Add existing resource to mock with future lastModified to ensure it's newer than the file
91
- mockClient.addResource({
92
- key: 'existing.txt',
93
- type: 'text',
94
- size: 8,
95
- lastModified: new Date(Date.now() + 10000).toISOString(), // 10 seconds in the future
96
- local: false, // Default to false for existing tests
97
- });
98
-
99
- // Create matching file
100
- fs.writeFileSync(path.join(resourcesDir, 'existing.txt'), '12345678'); // 8 bytes
101
-
102
- const result = await syncResources(projectPath, mockClient);
103
-
104
- expect(result.uploadCount).toBe(0);
105
- expect(result.skipCount).toBe(1);
106
- expect(result.errorCount).toBe(0);
107
-
108
- // Should only make GET call
109
- expect(mockClient.calls).toHaveLength(1);
110
- expect(mockClient.calls[0].path).toBe('/resources');
111
- });
112
-
113
- it('should upload modified resources based on size', async () => {
114
- const resourcesDir = path.join(projectPath, 'resources');
115
- fs.mkdirSync(resourcesDir, { recursive: true });
116
-
117
- // Add existing resource to mock with different size
118
- mockClient.addResource({
119
- key: 'modified.txt',
120
- type: 'text',
121
- size: 5,
122
- lastModified: new Date(Date.now() - 10000).toISOString(),
123
- local: false, // Default to false for existing tests
124
- });
125
-
126
- // Create file with different content
127
- fs.writeFileSync(path.join(resourcesDir, 'modified.txt'), 'New content');
128
-
129
- const result = await syncResources(projectPath, mockClient);
130
-
131
- expect(result.uploadCount).toBe(1);
132
- expect(result.skipCount).toBe(0);
133
- });
134
-
135
- it('should upload resources based on modification time when size matches', async () => {
136
- const resourcesDir = path.join(projectPath, 'resources');
137
- fs.mkdirSync(resourcesDir, { recursive: true });
138
-
139
- // Add existing resource to mock with same size but older timestamp
140
- mockClient.addResource({
141
- key: 'updated.txt',
142
- type: 'text',
143
- size: 11, // Same size as "Same content"
144
- lastModified: new Date(Date.now() - 10000).toISOString(), // 10 seconds ago
145
- local: false, // Default to false for existing tests
146
- });
147
-
148
- // Create file with same size but it will have a newer modification time
149
- fs.writeFileSync(path.join(resourcesDir, 'updated.txt'), 'Same content'); // 11 bytes
150
-
151
- const result = await syncResources(projectPath, mockClient);
152
-
153
- expect(result.uploadCount).toBe(1);
154
- expect(result.skipCount).toBe(0);
155
- });
156
-
157
- it('should handle nested directories', async () => {
158
- const resourcesDir = path.join(projectPath, 'resources');
159
- const docsDir = path.join(resourcesDir, 'docs');
160
- fs.mkdirSync(docsDir, { recursive: true });
161
-
162
- fs.writeFileSync(path.join(docsDir, 'readme.md'), '# README');
163
-
164
- const result = await syncResources(projectPath, mockClient);
165
-
166
- expect(result.uploadCount).toBe(1);
167
-
168
- const resources = mockClient.getResources();
169
- expect(resources[0].key).toBe('docs/readme.md');
170
- });
171
-
172
- it('should handle upload errors gracefully', async () => {
173
- const resourcesDir = path.join(projectPath, 'resources');
174
- fs.mkdirSync(resourcesDir, { recursive: true });
175
-
176
- // Create a file that will trigger an error in our mock
177
- // We'll modify the mock to fail on specific file names
178
- const badClient = createMockApiClient();
179
- const originalFetch = badClient.fetch;
180
- badClient.fetch = async (path, options) => {
181
- if (
182
- options?.method === 'POST' &&
183
- options.body instanceof FormData &&
184
- options.body.get('key') === 'error.txt'
185
- ) {
186
- throw new Error('Upload failed');
187
- }
188
- return originalFetch.call(badClient, path, options);
189
- };
190
-
191
- fs.writeFileSync(path.join(resourcesDir, 'error.txt'), 'This will fail');
192
- fs.writeFileSync(
193
- path.join(resourcesDir, 'good.txt'),
194
- 'This will succeed'
195
- );
196
-
197
- const result = await syncResources(projectPath, badClient);
198
-
199
- expect(result.uploadCount).toBe(1);
200
- expect(result.errorCount).toBe(1);
201
- expect(result.errors).toHaveLength(1);
202
- expect(result.errors[0]).toEqual({
203
- file: 'error.txt',
204
- message: 'Upload failed',
205
- });
206
- });
207
-
208
- it('should delete server resources with local=true when files are removed', async () => {
209
- const resourcesDir = path.join(projectPath, 'resources');
210
- fs.mkdirSync(resourcesDir, { recursive: true });
211
-
212
- // Create the file that should still exist first
213
- fs.writeFileSync(
214
- path.join(resourcesDir, 'still-exists.txt'),
215
- 'I still exist'
216
- );
217
-
218
- // Add existing resources to mock - some synced (local=true), some manual (local=false)
219
- mockClient.addResource({
220
- key: 'deleted-file.txt',
221
- type: 'text',
222
- size: 100,
223
- lastModified: new Date().toISOString(),
224
- local: true, // This one should be deleted
225
- });
226
- mockClient.addResource({
227
- key: 'still-exists.txt',
228
- type: 'text',
229
- size: 13, // Exact size of "I still exist"
230
- lastModified: new Date(Date.now() + 10000).toISOString(), // Future timestamp to prevent re-upload
231
- local: true, // This one should NOT be deleted
232
- });
233
- mockClient.addResource({
234
- key: 'docs/removed-doc.md',
235
- type: 'text',
236
- size: 200,
237
- lastModified: new Date().toISOString(),
238
- local: true, // This one should be deleted
239
- });
240
-
241
- const result = await syncResources(projectPath, mockClient);
242
-
243
- expect(result.deleteCount).toBe(2);
244
- expect(result.uploadCount).toBe(0); // No new uploads
245
- expect(result.skipCount).toBe(1); // still-exists.txt is up to date
246
-
247
- // Verify the correct resources were deleted
248
- const remainingResources = mockClient.getResources();
249
- expect(remainingResources).toHaveLength(1);
250
- expect(remainingResources[0].key).toBe('still-exists.txt');
251
-
252
- // Verify DELETE API calls were made
253
- const deleteCalls = mockClient.calls.filter(
254
- (call) => call.options?.method === 'DELETE'
255
- );
256
- expect(deleteCalls).toHaveLength(2);
257
- expect(deleteCalls[0].path).toBe('/resources/deleted-file.txt');
258
- expect(deleteCalls[1].path).toBe('/resources/docs%2Fremoved-doc.md'); // URL encoded
259
- });
260
-
261
- it('should preserve manually uploaded resources (local=false)', async () => {
262
- const resourcesDir = path.join(projectPath, 'resources');
263
- fs.mkdirSync(resourcesDir, { recursive: true });
264
-
265
- // Add existing resources to mock
266
- mockClient.addResource({
267
- key: 'manual-upload.txt',
268
- type: 'text',
269
- size: 100,
270
- lastModified: new Date().toISOString(),
271
- local: false, // Manually uploaded - should NOT be deleted
272
- });
273
- mockClient.addResource({
274
- key: 'another-manual.png',
275
- type: 'binary',
276
- size: 5000,
277
- lastModified: new Date().toISOString(),
278
- local: false, // Manually uploaded - should NOT be deleted
279
- });
280
- mockClient.addResource({
281
- key: 'synced-file.txt',
282
- type: 'text',
283
- size: 75,
284
- lastModified: new Date().toISOString(),
285
- local: true, // Synced file that was deleted locally
286
- });
287
-
288
- // Don't create any local files - all are "deleted" locally
289
-
290
- const result = await syncResources(projectPath, mockClient);
291
-
292
- expect(result.deleteCount).toBe(1); // Only the synced file
293
- expect(result.uploadCount).toBe(0);
294
- expect(result.skipCount).toBe(0);
295
-
296
- // Verify manual uploads are still there
297
- const remainingResources = mockClient.getResources();
298
- expect(remainingResources).toHaveLength(2);
299
- expect(
300
- remainingResources.find((r) => r.key === 'manual-upload.txt')
301
- ).toBeDefined();
302
- expect(
303
- remainingResources.find((r) => r.key === 'another-manual.png')
304
- ).toBeDefined();
305
- expect(
306
- remainingResources.find((r) => r.key === 'synced-file.txt')
307
- ).toBeUndefined();
308
-
309
- // Verify only one DELETE call was made
310
- const deleteCalls = mockClient.calls.filter(
311
- (call) => call.options?.method === 'DELETE'
312
- );
313
- expect(deleteCalls).toHaveLength(1);
314
- expect(deleteCalls[0].path).toBe('/resources/synced-file.txt');
315
- });
316
-
317
- it('should handle delete errors gracefully', async () => {
318
- const resourcesDir = path.join(projectPath, 'resources');
319
- fs.mkdirSync(resourcesDir, { recursive: true });
320
-
321
- // Create a client that fails on DELETE requests
322
- const badClient = createMockApiClient();
323
- badClient.addResource({
324
- key: 'will-fail-delete.txt',
325
- type: 'text',
326
- size: 100,
327
- lastModified: new Date().toISOString(),
328
- local: true,
329
- });
330
-
331
- const originalFetch = badClient.fetch;
332
- badClient.fetch = (async (path: string, options?: RequestInit) => {
333
- if (options?.method === 'DELETE') {
334
- return new Response('Internal Server Error', { status: 500 });
335
- }
336
- return originalFetch.call(badClient, path, options);
337
- }) as typeof badClient.fetch;
338
-
339
- // Don't create the file locally so it should be deleted
340
-
341
- const result = await syncResources(projectPath, badClient);
342
-
343
- expect(result.deleteCount).toBe(0); // Failed to delete
344
- expect(result.errorCount).toBe(1);
345
- expect(result.errors).toHaveLength(1);
346
- expect(result.errors[0]).toEqual({
347
- file: 'will-fail-delete.txt',
348
- message: 'Failed to delete: Delete failed: 500 Internal Server Error',
349
- });
350
- });
351
- });
352
-
353
- describe('generateTypes', () => {
354
- it('should generate types file for empty resources', async () => {
355
- const result = await generateTypes(projectPath, mockClient);
356
-
357
- const typesPath = path.join(projectPath, 'resources.d.ts');
358
- expect(fs.existsSync(typesPath)).toBe(true);
359
-
360
- const content = fs.readFileSync(typesPath, 'utf-8');
361
- expect(content).toContain("declare module '@positronic/core'");
362
- expect(content).toContain('interface Resources');
363
- expect(content).toContain('loadText(path: string): Promise<string>');
364
- expect(content).toContain('loadBinary(path: string): Promise<Buffer>');
365
- });
366
-
367
- it('should generate types for text and binary resources', async () => {
368
- // Add mock resources
369
- mockClient.addResource({
370
- key: 'readme.md',
371
- type: 'text',
372
- size: 100,
373
- lastModified: new Date().toISOString(),
374
- local: false, // Default to false for existing tests
375
- });
376
- mockClient.addResource({
377
- key: 'logo.png',
378
- type: 'binary',
379
- size: 1000,
380
- lastModified: new Date().toISOString(),
381
- local: false, // Default to false for existing tests
382
- });
383
- mockClient.addResource({
384
- key: 'data.json',
385
- type: 'text',
386
- size: 50,
387
- lastModified: new Date().toISOString(),
388
- local: false, // Default to false for existing tests
389
- });
390
-
391
- await generateTypes(projectPath, mockClient);
392
-
393
- const typesPath = path.join(projectPath, 'resources.d.ts');
394
- const content = fs.readFileSync(typesPath, 'utf-8');
395
-
396
- // Check resource declarations
397
- expect(content).toContain('readme: TextResource;');
398
- expect(content).toContain('logo: BinaryResource;');
399
- expect(content).toContain('data: TextResource;');
400
- });
401
-
402
- it('should handle nested resources', async () => {
403
- mockClient.addResource({
404
- key: 'docs/api.md',
405
- type: 'text',
406
- size: 100,
407
- lastModified: new Date().toISOString(),
408
- local: false, // Default to false for existing tests
409
- });
410
- mockClient.addResource({
411
- key: 'docs/images/diagram.png',
412
- type: 'binary',
413
- size: 500,
414
- lastModified: new Date().toISOString(),
415
- local: false, // Default to false for existing tests
416
- });
417
- mockClient.addResource({
418
- key: 'config/settings.json',
419
- type: 'text',
420
- size: 200,
421
- lastModified: new Date().toISOString(),
422
- local: false, // Default to false for existing tests
423
- });
424
-
425
- await generateTypes(projectPath, mockClient);
426
-
427
- const typesPath = path.join(projectPath, 'resources.d.ts');
428
- const content = fs.readFileSync(typesPath, 'utf-8');
429
-
430
- // Check nested structure
431
- expect(content).toContain('docs: {');
432
- expect(content).toContain('api: TextResource;');
433
- expect(content).toContain('images: {');
434
- expect(content).toContain('diagram: BinaryResource;');
435
- expect(content).toContain('config: {');
436
- expect(content).toContain('settings: TextResource;');
437
- });
438
-
439
- it('should exclude invalid JavaScript identifiers', async () => {
440
- mockClient.addResource({
441
- key: '123invalid.txt',
442
- type: 'text',
443
- size: 100,
444
- lastModified: new Date().toISOString(),
445
- local: false, // Default to false for existing tests
446
- });
447
- mockClient.addResource({
448
- key: 'file-with-dash.txt',
449
- type: 'text',
450
- size: 100,
451
- lastModified: new Date().toISOString(),
452
- local: false, // Default to false for existing tests
453
- });
454
- mockClient.addResource({
455
- key: 'valid_file.txt',
456
- type: 'text',
457
- size: 100,
458
- lastModified: new Date().toISOString(),
459
- local: false, // Default to false for existing tests
460
- });
461
-
462
- await generateTypes(projectPath, mockClient);
463
-
464
- const typesPath = path.join(projectPath, 'resources.d.ts');
465
- const content = fs.readFileSync(typesPath, 'utf-8');
466
-
467
- // Should include valid identifier
468
- expect(content).toContain('valid_file: TextResource;');
469
-
470
- // Should not include invalid identifiers
471
- expect(content).not.toContain('123invalid');
472
- expect(content).not.toContain('file-with-dash');
473
- });
474
-
475
- it('should handle API errors gracefully', async () => {
476
- // Create a client that returns an error
477
- const errorClient = createMockApiClient();
478
- errorClient.fetch = (async (path: string, options?: RequestInit) => {
479
- return new Response('Internal Server Error', { status: 500 });
480
- }) as any;
481
-
482
- await expect(generateTypes(projectPath, errorClient)).rejects.toThrow(
483
- 'Failed to fetch resources: 500 Internal Server Error'
484
- );
485
- });
486
- });
487
- });