@positronic/cli 0.0.2

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 (193) hide show
  1. package/dist/src/cli.js +739 -0
  2. package/dist/src/commands/backend.js +199 -0
  3. package/dist/src/commands/brain.js +446 -0
  4. package/dist/src/commands/brain.test.js +2936 -0
  5. package/dist/src/commands/helpers.js +1315 -0
  6. package/dist/src/commands/helpers.test.js +832 -0
  7. package/dist/src/commands/project-config-manager.js +197 -0
  8. package/dist/src/commands/project.js +130 -0
  9. package/dist/src/commands/project.test.js +1201 -0
  10. package/dist/src/commands/resources.js +272 -0
  11. package/dist/src/commands/resources.test.js +2511 -0
  12. package/dist/src/commands/schedule.js +73 -0
  13. package/dist/src/commands/schedule.test.js +1235 -0
  14. package/dist/src/commands/secret.js +87 -0
  15. package/dist/src/commands/secret.test.d.js +1 -0
  16. package/dist/src/commands/secret.test.js +761 -0
  17. package/dist/src/commands/server.js +816 -0
  18. package/dist/src/commands/server.test.js +1237 -0
  19. package/dist/src/commands/test-utils.js +737 -0
  20. package/dist/src/components/brain-history.js +169 -0
  21. package/dist/src/components/brain-list.js +108 -0
  22. package/dist/src/components/brain-rerun.js +313 -0
  23. package/dist/src/components/brain-show.js +65 -0
  24. package/dist/src/components/error.js +19 -0
  25. package/dist/src/components/project-add.js +95 -0
  26. package/dist/src/components/project-create.js +276 -0
  27. package/dist/src/components/project-list.js +88 -0
  28. package/dist/src/components/project-remove.js +91 -0
  29. package/dist/src/components/project-select.js +224 -0
  30. package/dist/src/components/project-show.js +41 -0
  31. package/dist/src/components/resource-clear.js +152 -0
  32. package/dist/src/components/resource-delete.js +189 -0
  33. package/dist/src/components/resource-list.js +174 -0
  34. package/dist/src/components/resource-sync.js +386 -0
  35. package/dist/src/components/resource-types.js +243 -0
  36. package/dist/src/components/resource-upload.js +366 -0
  37. package/dist/src/components/schedule-create.js +259 -0
  38. package/dist/src/components/schedule-delete.js +161 -0
  39. package/dist/src/components/schedule-list.js +176 -0
  40. package/dist/src/components/schedule-runs.js +103 -0
  41. package/dist/src/components/secret-bulk.js +262 -0
  42. package/dist/src/components/secret-create.js +199 -0
  43. package/dist/src/components/secret-delete.js +190 -0
  44. package/dist/src/components/secret-list.js +190 -0
  45. package/dist/src/components/secret-sync.js +303 -0
  46. package/dist/src/components/watch.js +184 -0
  47. package/dist/src/hooks/useApi.js +512 -0
  48. package/dist/src/positronic.js +33 -0
  49. package/dist/src/test/mock-api-client.js +371 -0
  50. package/dist/src/test/test-dev-server.js +1376 -0
  51. package/dist/types/cli.d.ts +9 -0
  52. package/dist/types/cli.d.ts.map +1 -0
  53. package/dist/types/commands/backend.d.ts +6 -0
  54. package/dist/types/commands/backend.d.ts.map +1 -0
  55. package/dist/types/commands/brain.d.ts +35 -0
  56. package/dist/types/commands/brain.d.ts.map +1 -0
  57. package/dist/types/commands/helpers.d.ts +55 -0
  58. package/dist/types/commands/helpers.d.ts.map +1 -0
  59. package/dist/types/commands/project-config-manager.d.ts +37 -0
  60. package/dist/types/commands/project-config-manager.d.ts.map +1 -0
  61. package/dist/types/commands/project.d.ts +55 -0
  62. package/dist/types/commands/project.d.ts.map +1 -0
  63. package/dist/types/commands/resources.d.ts +13 -0
  64. package/dist/types/commands/resources.d.ts.map +1 -0
  65. package/dist/types/commands/schedule.d.ts +27 -0
  66. package/dist/types/commands/schedule.d.ts.map +1 -0
  67. package/dist/types/commands/secret.d.ts +23 -0
  68. package/dist/types/commands/secret.d.ts.map +1 -0
  69. package/dist/types/commands/server.d.ts +12 -0
  70. package/dist/types/commands/server.d.ts.map +1 -0
  71. package/dist/types/commands/test-utils.d.ts +45 -0
  72. package/dist/types/commands/test-utils.d.ts.map +1 -0
  73. package/dist/types/components/brain-history.d.ts +7 -0
  74. package/dist/types/components/brain-history.d.ts.map +1 -0
  75. package/dist/types/components/brain-list.d.ts +2 -0
  76. package/dist/types/components/brain-list.d.ts.map +1 -0
  77. package/dist/types/components/brain-rerun.d.ts +9 -0
  78. package/dist/types/components/brain-rerun.d.ts.map +1 -0
  79. package/dist/types/components/brain-show.d.ts +6 -0
  80. package/dist/types/components/brain-show.d.ts.map +1 -0
  81. package/dist/types/components/error.d.ts +10 -0
  82. package/dist/types/components/error.d.ts.map +1 -0
  83. package/dist/types/components/project-add.d.ts +9 -0
  84. package/dist/types/components/project-add.d.ts.map +1 -0
  85. package/dist/types/components/project-create.d.ts +6 -0
  86. package/dist/types/components/project-create.d.ts.map +1 -0
  87. package/dist/types/components/project-list.d.ts +7 -0
  88. package/dist/types/components/project-list.d.ts.map +1 -0
  89. package/dist/types/components/project-remove.d.ts +8 -0
  90. package/dist/types/components/project-remove.d.ts.map +1 -0
  91. package/dist/types/components/project-select.d.ts +8 -0
  92. package/dist/types/components/project-select.d.ts.map +1 -0
  93. package/dist/types/components/project-show.d.ts +7 -0
  94. package/dist/types/components/project-show.d.ts.map +1 -0
  95. package/dist/types/components/resource-clear.d.ts +2 -0
  96. package/dist/types/components/resource-clear.d.ts.map +1 -0
  97. package/dist/types/components/resource-delete.d.ts +9 -0
  98. package/dist/types/components/resource-delete.d.ts.map +1 -0
  99. package/dist/types/components/resource-list.d.ts +2 -0
  100. package/dist/types/components/resource-list.d.ts.map +1 -0
  101. package/dist/types/components/resource-sync.d.ts +8 -0
  102. package/dist/types/components/resource-sync.d.ts.map +1 -0
  103. package/dist/types/components/resource-types.d.ts +7 -0
  104. package/dist/types/components/resource-types.d.ts.map +1 -0
  105. package/dist/types/components/resource-upload.d.ts +8 -0
  106. package/dist/types/components/resource-upload.d.ts.map +1 -0
  107. package/dist/types/components/schedule-create.d.ts +7 -0
  108. package/dist/types/components/schedule-create.d.ts.map +1 -0
  109. package/dist/types/components/schedule-delete.d.ts +7 -0
  110. package/dist/types/components/schedule-delete.d.ts.map +1 -0
  111. package/dist/types/components/schedule-list.d.ts +6 -0
  112. package/dist/types/components/schedule-list.d.ts.map +1 -0
  113. package/dist/types/components/schedule-runs.d.ts +8 -0
  114. package/dist/types/components/schedule-runs.d.ts.map +1 -0
  115. package/dist/types/components/secret-bulk.d.ts +8 -0
  116. package/dist/types/components/secret-bulk.d.ts.map +1 -0
  117. package/dist/types/components/secret-create.d.ts +9 -0
  118. package/dist/types/components/secret-create.d.ts.map +1 -0
  119. package/dist/types/components/secret-delete.d.ts +8 -0
  120. package/dist/types/components/secret-delete.d.ts.map +1 -0
  121. package/dist/types/components/secret-list.d.ts +7 -0
  122. package/dist/types/components/secret-list.d.ts.map +1 -0
  123. package/dist/types/components/secret-sync.d.ts +9 -0
  124. package/dist/types/components/secret-sync.d.ts.map +1 -0
  125. package/dist/types/components/watch.d.ts +7 -0
  126. package/dist/types/components/watch.d.ts.map +1 -0
  127. package/dist/types/hooks/useApi.d.ts +29 -0
  128. package/dist/types/hooks/useApi.d.ts.map +1 -0
  129. package/dist/types/positronic.d.ts +3 -0
  130. package/dist/types/positronic.d.ts.map +1 -0
  131. package/dist/types/test/mock-api-client.d.ts +25 -0
  132. package/dist/types/test/mock-api-client.d.ts.map +1 -0
  133. package/dist/types/test/test-dev-server.d.ts +129 -0
  134. package/dist/types/test/test-dev-server.d.ts.map +1 -0
  135. package/package.json +37 -0
  136. package/src/cli.ts +981 -0
  137. package/src/commands/backend.ts +63 -0
  138. package/src/commands/brain.test.ts +1004 -0
  139. package/src/commands/brain.ts +215 -0
  140. package/src/commands/helpers.test.ts +487 -0
  141. package/src/commands/helpers.ts +870 -0
  142. package/src/commands/project-config-manager.ts +152 -0
  143. package/src/commands/project.test.ts +502 -0
  144. package/src/commands/project.ts +109 -0
  145. package/src/commands/resources.test.ts +1052 -0
  146. package/src/commands/resources.ts +97 -0
  147. package/src/commands/schedule.test.ts +481 -0
  148. package/src/commands/schedule.ts +65 -0
  149. package/src/commands/secret.test.ts +210 -0
  150. package/src/commands/secret.ts +50 -0
  151. package/src/commands/server.test.ts +493 -0
  152. package/src/commands/server.ts +353 -0
  153. package/src/commands/test-utils.ts +324 -0
  154. package/src/components/brain-history.tsx +198 -0
  155. package/src/components/brain-list.tsx +105 -0
  156. package/src/components/brain-rerun.tsx +111 -0
  157. package/src/components/brain-show.tsx +92 -0
  158. package/src/components/error.tsx +24 -0
  159. package/src/components/project-add.tsx +59 -0
  160. package/src/components/project-create.tsx +83 -0
  161. package/src/components/project-list.tsx +83 -0
  162. package/src/components/project-remove.tsx +55 -0
  163. package/src/components/project-select.tsx +200 -0
  164. package/src/components/project-show.tsx +58 -0
  165. package/src/components/resource-clear.tsx +127 -0
  166. package/src/components/resource-delete.tsx +160 -0
  167. package/src/components/resource-list.tsx +177 -0
  168. package/src/components/resource-sync.tsx +170 -0
  169. package/src/components/resource-types.tsx +55 -0
  170. package/src/components/resource-upload.tsx +182 -0
  171. package/src/components/schedule-create.tsx +90 -0
  172. package/src/components/schedule-delete.tsx +116 -0
  173. package/src/components/schedule-list.tsx +186 -0
  174. package/src/components/schedule-runs.tsx +151 -0
  175. package/src/components/secret-bulk.tsx +79 -0
  176. package/src/components/secret-create.tsx +49 -0
  177. package/src/components/secret-delete.tsx +41 -0
  178. package/src/components/secret-list.tsx +41 -0
  179. package/src/components/watch.tsx +155 -0
  180. package/src/hooks/useApi.ts +183 -0
  181. package/src/positronic.ts +40 -0
  182. package/src/test/data/resources/config.json +1 -0
  183. package/src/test/data/resources/data/config.json +1 -0
  184. package/src/test/data/resources/data/logo.png +2 -0
  185. package/src/test/data/resources/docs/api.md +3 -0
  186. package/src/test/data/resources/docs/readme.md +3 -0
  187. package/src/test/data/resources/example.md +3 -0
  188. package/src/test/data/resources/file with spaces.txt +1 -0
  189. package/src/test/data/resources/readme.md +3 -0
  190. package/src/test/data/resources/test.txt +1 -0
  191. package/src/test/mock-api-client.ts +145 -0
  192. package/src/test/test-dev-server.ts +1003 -0
  193. package/tsconfig.json +11 -0
@@ -0,0 +1,1052 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { createTestEnv, px, waitForTypesFile } from './test-utils.js';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+
6
+ describe('CLI Integration: positronic resources types', () => {
7
+ it('should generate type definitions for resources', async () => {
8
+ const env = await createTestEnv();
9
+ const px = await env.start();
10
+ try {
11
+ const { waitForOutput, waitForTypesFile } = await px([
12
+ 'resources',
13
+ 'sync',
14
+ ]);
15
+ const isOutputRendered = await waitForOutput(/sync summary/i, 50);
16
+ const typesContent = await waitForTypesFile('config: TextResource;');
17
+ expect(isOutputRendered).toBe(true);
18
+ const normalizeWhitespace = (str: string) =>
19
+ str.replace(/\s+/g, ' ').trim();
20
+
21
+ const expectedContent = `declare module '@positronic/core' {
22
+ interface TextResource {
23
+ load(): Promise<string>;
24
+ }
25
+ interface BinaryResource {
26
+ load(): Promise<Buffer>;
27
+ }
28
+ interface Resources {
29
+ // Method signatures for loading resources by path
30
+ loadText(path: string): Promise<string>;
31
+ loadBinary(path: string): Promise<Buffer>;
32
+ // Resource properties accessible via dot notation
33
+ config: TextResource;
34
+ data: {
35
+ config: TextResource;
36
+ logo: BinaryResource;
37
+ };
38
+ docs: {
39
+ api: TextResource;
40
+ readme: TextResource;
41
+ };
42
+ example: TextResource;
43
+ readme: TextResource;
44
+ test: TextResource;
45
+ }
46
+ }`;
47
+
48
+ expect(normalizeWhitespace(typesContent)).toContain(
49
+ normalizeWhitespace(expectedContent)
50
+ );
51
+ } finally {
52
+ await env.stopAndCleanup();
53
+ }
54
+ });
55
+
56
+ it('should handle empty resources directory', async () => {
57
+ const env = await createTestEnv();
58
+ const px = await env.start();
59
+
60
+ try {
61
+ // Remove the default resources created by createMinimalProject
62
+ const resourcesDir = path.join(env.projectRootDir, 'resources');
63
+
64
+ // Remove all contents recursively and recreate empty directory
65
+ fs.rmSync(resourcesDir, { recursive: true, force: true });
66
+ fs.mkdirSync(resourcesDir);
67
+
68
+ // Verify directory is empty
69
+ expect(fs.readdirSync(resourcesDir).length).toBe(0);
70
+
71
+ // Run the sync command using cli
72
+ const syncElement = await px(['resources', 'sync']);
73
+ const isOutputRendered = await syncElement.waitForOutput(
74
+ /no files found in the resources directory/i
75
+ );
76
+ expect(isOutputRendered).toBe(true);
77
+ const typesContent = await syncElement.waitForTypesFile(
78
+ 'loadText(path: string): Promise<string>;'
79
+ );
80
+
81
+ // Check if the types file was generated
82
+ expect(typesContent).toBeDefined();
83
+
84
+ // Should still have the basic structure
85
+ expect(typesContent).toContain("declare module '@positronic/core'");
86
+ expect(typesContent).toContain('interface Resources');
87
+ expect(typesContent).toContain('loadText(path: string): Promise<string>');
88
+ expect(typesContent).toContain(
89
+ 'loadBinary(path: string): Promise<Buffer>'
90
+ );
91
+
92
+ // Run the list command
93
+ const listElement = await px(['resources', 'list']);
94
+
95
+ const resourceListOutput = await listElement.waitForOutput(
96
+ /no resources found in the project/i
97
+ );
98
+
99
+ expect(resourceListOutput).toBe(true);
100
+ } finally {
101
+ await env.stopAndCleanup();
102
+ }
103
+ });
104
+
105
+ it('should handle resources with special characters', async () => {
106
+ const env = await createTestEnv();
107
+
108
+ // Setup files with special characters
109
+ env.setup((dir: string) => {
110
+ // Create resources directory
111
+ const resourcesDir = path.join(dir, 'resources');
112
+
113
+ // Remove existing resources directory if it exists
114
+ if (fs.existsSync(resourcesDir)) {
115
+ fs.rmSync(resourcesDir, { recursive: true, force: true });
116
+ }
117
+
118
+ // Create fresh resources directory
119
+ fs.mkdirSync(resourcesDir, { recursive: true });
120
+
121
+ // Create files with special characters
122
+ fs.writeFileSync(path.join(resourcesDir, 'valid_file.txt'), 'content');
123
+ fs.writeFileSync(path.join(resourcesDir, '$special.txt'), 'content'); // Valid JS identifier
124
+ fs.writeFileSync(path.join(resourcesDir, '_underscore.txt'), 'content'); // Valid JS identifier
125
+ fs.writeFileSync(path.join(resourcesDir, '123invalid.txt'), 'content'); // Invalid - starts with number
126
+ fs.writeFileSync(
127
+ path.join(resourcesDir, 'special-chars!@#.txt'),
128
+ 'content'
129
+ ); // Invalid
130
+ });
131
+
132
+ const px = await env.start();
133
+
134
+ try {
135
+ // Run the sync command first (types are generated as part of sync)
136
+ const { waitForOutput, waitForTypesFile } = await px([
137
+ 'resources',
138
+ 'sync',
139
+ ]);
140
+ const isOutputRendered = await waitForOutput(/sync summary/i);
141
+ expect(isOutputRendered).toBe(true);
142
+
143
+ // Wait for types file to be generated
144
+ const typesContent = await waitForTypesFile([
145
+ 'valid_file: TextResource;',
146
+ '$special: TextResource;',
147
+ '_underscore: TextResource;',
148
+ ]);
149
+
150
+ // Verify the generated content
151
+ expect(typesContent).toBeDefined();
152
+
153
+ // Check valid identifiers are included
154
+ expect(typesContent).toContain('valid_file: TextResource;');
155
+ expect(typesContent).toContain('$special: TextResource;');
156
+ expect(typesContent).toContain('_underscore: TextResource;');
157
+
158
+ // Check invalid identifiers are excluded
159
+ expect(typesContent).not.toContain('123invalid');
160
+ expect(typesContent).not.toContain('special-chars');
161
+ } finally {
162
+ await env.stopAndCleanup();
163
+ }
164
+ });
165
+
166
+ it('should correctly identify text vs binary files', async () => {
167
+ const env = await createTestEnv();
168
+
169
+ // Setup files with various types
170
+ env.setup((dir: string) => {
171
+ // Create resources directory
172
+ const resourcesDir = path.join(dir, 'resources');
173
+
174
+ // Remove existing resources directory if it exists
175
+ if (fs.existsSync(resourcesDir)) {
176
+ fs.rmSync(resourcesDir, { recursive: true, force: true });
177
+ }
178
+
179
+ // Create fresh resources directory
180
+ fs.mkdirSync(resourcesDir, { recursive: true });
181
+
182
+ // Create various file types
183
+ fs.writeFileSync(path.join(resourcesDir, 'text.txt'), 'text');
184
+ fs.writeFileSync(path.join(resourcesDir, 'script.js'), 'code');
185
+ fs.writeFileSync(path.join(resourcesDir, 'config.json'), '{}');
186
+ fs.writeFileSync(path.join(resourcesDir, 'styles.css'), 'css');
187
+
188
+ // Create actual binary content for binary files
189
+ // JPEG magic bytes
190
+ const jpegHeader = Buffer.from([
191
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
192
+ ]);
193
+ fs.writeFileSync(path.join(resourcesDir, 'image.jpg'), jpegHeader);
194
+
195
+ // Random binary data
196
+ const binaryData = Buffer.from([
197
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
198
+ ]);
199
+ fs.writeFileSync(path.join(resourcesDir, 'binary.bin'), binaryData);
200
+
201
+ // PDF magic bytes
202
+ const pdfHeader = Buffer.from('%PDF-1.4\n%âÌÊÓ\n');
203
+ fs.writeFileSync(path.join(resourcesDir, 'document.pdf'), pdfHeader);
204
+ });
205
+
206
+ const px = await env.start();
207
+
208
+ try {
209
+ // Run the sync command first (types are generated as part of sync)
210
+ const { waitForOutput, waitForTypesFile } = await px([
211
+ 'resources',
212
+ 'sync',
213
+ ]);
214
+
215
+ const isOutputRendered = await waitForOutput(/sync summary/i);
216
+ expect(isOutputRendered).toBe(true);
217
+
218
+ // Wait for types file to be generated with expected content
219
+ const typesContent = await waitForTypesFile([
220
+ 'text: TextResource;',
221
+ 'script: TextResource;',
222
+ 'config: TextResource;',
223
+ 'styles: TextResource;',
224
+ 'image: BinaryResource;',
225
+ 'binary: BinaryResource;',
226
+ 'document: BinaryResource;',
227
+ ]);
228
+
229
+ // Verify the generated content
230
+ expect(typesContent).toBeDefined();
231
+
232
+ // Check text resources
233
+ expect(typesContent).toContain('text: TextResource;');
234
+ expect(typesContent).toContain('script: TextResource;');
235
+ expect(typesContent).toContain('config: TextResource;');
236
+ expect(typesContent).toContain('styles: TextResource;');
237
+
238
+ // Check binary resources
239
+ expect(typesContent).toContain('image: BinaryResource;');
240
+ expect(typesContent).toContain('binary: BinaryResource;');
241
+ expect(typesContent).toContain('document: BinaryResource;');
242
+ } finally {
243
+ await env.stopAndCleanup();
244
+ }
245
+ });
246
+
247
+ describe('resources list command', () => {
248
+ it('should handle empty resources', async () => {
249
+ const env = await createTestEnv();
250
+ const px = await env.start();
251
+
252
+ try {
253
+ const { waitForOutput } = await px(['resources', 'list']);
254
+ const found = await waitForOutput(/No resources found in the project/);
255
+ expect(found).toBe(true);
256
+ } finally {
257
+ await env.stopAndCleanup();
258
+ }
259
+ });
260
+
261
+ it('should display resources with tree structure', async () => {
262
+ const env = await createTestEnv();
263
+
264
+ // Add resources with nested structure
265
+ env.server.addResource({
266
+ key: 'readme.txt',
267
+ type: 'text',
268
+ size: 1024,
269
+ lastModified: new Date().toISOString(),
270
+ local: true,
271
+ });
272
+ env.server.addResource({
273
+ key: 'data/config.json',
274
+ type: 'text',
275
+ size: 2048,
276
+ lastModified: new Date().toISOString(),
277
+ local: true,
278
+ });
279
+ env.server.addResource({
280
+ key: 'data/users/admin.json',
281
+ type: 'text',
282
+ size: 512,
283
+ lastModified: new Date().toISOString(),
284
+ local: true,
285
+ });
286
+ env.server.addResource({
287
+ key: 'images/logo.png',
288
+ type: 'binary',
289
+ size: 10240,
290
+ lastModified: new Date().toISOString(),
291
+ local: false,
292
+ });
293
+
294
+ const px = await env.start();
295
+
296
+ try {
297
+ const { waitForOutput } = await px(['resources', 'list']);
298
+
299
+ // Check tree structure is displayed
300
+ const foundTree = await waitForOutput(/resources/);
301
+ expect(foundTree).toBe(true);
302
+
303
+ // Check for files in root
304
+ const foundReadme = await waitForOutput(/readme\.txt/);
305
+ expect(foundReadme).toBe(true);
306
+
307
+ // Check for nested directories
308
+ const foundData = await waitForOutput(/data/);
309
+ expect(foundData).toBe(true);
310
+
311
+ const foundConfig = await waitForOutput(/config\.json/);
312
+ expect(foundConfig).toBe(true);
313
+
314
+ // Check for deeply nested
315
+ const foundUsers = await waitForOutput(/users/);
316
+ expect(foundUsers).toBe(true);
317
+
318
+ const foundAdmin = await waitForOutput(/admin\.json/);
319
+ expect(foundAdmin).toBe(true);
320
+
321
+ // Check for images directory
322
+ const foundImages = await waitForOutput(/images/);
323
+ expect(foundImages).toBe(true);
324
+
325
+ const foundLogo = await waitForOutput(/logo\.png/);
326
+ expect(foundLogo).toBe(true);
327
+ } finally {
328
+ await env.stopAndCleanup();
329
+ }
330
+ });
331
+
332
+ it('should display file sizes with correct formatting', async () => {
333
+ const env = await createTestEnv();
334
+
335
+ // Add resources with various sizes
336
+ env.server.addResource({
337
+ key: 'small.txt',
338
+ type: 'text',
339
+ size: 500, // 500 bytes
340
+ lastModified: new Date().toISOString(),
341
+ local: true,
342
+ });
343
+ env.server.addResource({
344
+ key: 'medium.txt',
345
+ type: 'text',
346
+ size: 1536, // 1.5 KB
347
+ lastModified: new Date().toISOString(),
348
+ local: true,
349
+ });
350
+ env.server.addResource({
351
+ key: 'large.bin',
352
+ type: 'binary',
353
+ size: 1048576, // 1 MB
354
+ lastModified: new Date().toISOString(),
355
+ local: true,
356
+ });
357
+ env.server.addResource({
358
+ key: 'huge.bin',
359
+ type: 'binary',
360
+ size: 10485760, // 10 MB
361
+ lastModified: new Date().toISOString(),
362
+ local: true,
363
+ });
364
+
365
+ const px = await env.start();
366
+
367
+ try {
368
+ const { waitForOutput } = await px(['resources', 'list']);
369
+
370
+ // Check size formatting
371
+ const foundSmall = await waitForOutput(/small\.txt.*500 B/);
372
+ expect(foundSmall).toBe(true);
373
+
374
+ const foundMedium = await waitForOutput(/medium\.txt.*1\.5 KB/);
375
+ expect(foundMedium).toBe(true);
376
+
377
+ const foundLarge = await waitForOutput(/large\.bin.*1\.0 MB/);
378
+ expect(foundLarge).toBe(true);
379
+
380
+ const foundHuge = await waitForOutput(/huge\.bin.*10\.0 MB/);
381
+ expect(foundHuge).toBe(true);
382
+ } finally {
383
+ await env.stopAndCleanup();
384
+ }
385
+ });
386
+
387
+ it('should indicate local vs remote resources', async () => {
388
+ const env = await createTestEnv();
389
+
390
+ // Add mix of local and remote resources
391
+ env.server.addResource({
392
+ key: 'local-file.txt',
393
+ type: 'text',
394
+ size: 100,
395
+ lastModified: new Date().toISOString(),
396
+ local: true,
397
+ });
398
+ env.server.addResource({
399
+ key: 'remote-file.txt',
400
+ type: 'text',
401
+ size: 200,
402
+ lastModified: new Date().toISOString(),
403
+ local: false,
404
+ });
405
+
406
+ const px = await env.start();
407
+
408
+ try {
409
+ const { waitForOutput } = await px(['resources', 'list']);
410
+
411
+ // Check both files are listed
412
+ const foundLocal = await waitForOutput(/local-file\.txt/);
413
+ expect(foundLocal).toBe(true);
414
+
415
+ const foundRemote = await waitForOutput(/remote-file\.txt/);
416
+ expect(foundRemote).toBe(true);
417
+
418
+ // Remote resources should have arrow indicator
419
+ const foundArrow = await waitForOutput(/↗/);
420
+ expect(foundArrow).toBe(true);
421
+
422
+ // Should show legend for uploaded resources
423
+ const foundLegend = await waitForOutput(
424
+ /uploaded resource \(not in local filesystem\)/
425
+ );
426
+ expect(foundLegend).toBe(true);
427
+ } finally {
428
+ await env.stopAndCleanup();
429
+ }
430
+ });
431
+
432
+ it('should show loading state', async () => {
433
+ const env = await createTestEnv();
434
+ const px = await env.start();
435
+
436
+ try {
437
+ // Stop server to simulate slow loading
438
+ env.server.stop();
439
+
440
+ const { waitForOutput, instance } = await px(['resources', 'list']);
441
+
442
+ // Should show loading message initially
443
+ const foundLoading = await waitForOutput(/Loading resources\.\.\./);
444
+ expect(foundLoading).toBe(true);
445
+
446
+ // Should eventually show error when server is down
447
+ const foundError = await waitForOutput(/Error connecting/);
448
+ expect(foundError).toBe(true);
449
+ } finally {
450
+ // Cleanup without stopping server (already stopped)
451
+ env.cleanup();
452
+ }
453
+ });
454
+
455
+ it('should display resource count summary', async () => {
456
+ const env = await createTestEnv();
457
+
458
+ // Add multiple resources
459
+ for (let i = 1; i <= 5; i++) {
460
+ env.server.addResource({
461
+ key: `file${i}.txt`,
462
+ type: 'text',
463
+ size: 100 * i,
464
+ lastModified: new Date().toISOString(),
465
+ local: i % 2 === 0, // Even numbers are local
466
+ });
467
+ }
468
+
469
+ const px = await env.start();
470
+
471
+ try {
472
+ const { waitForOutput } = await px(['resources', 'list']);
473
+
474
+ // Should show count in format "Found X resources:"
475
+ const foundCount = await waitForOutput(/Found 5 resources:/);
476
+ expect(foundCount).toBe(true);
477
+ } finally {
478
+ await env.stopAndCleanup();
479
+ }
480
+ });
481
+
482
+ it('should show single resource properly', async () => {
483
+ const env = await createTestEnv();
484
+
485
+ // Add a single resource to test singular form
486
+ env.server.addResource({
487
+ key: 'single.txt',
488
+ type: 'text',
489
+ size: 42,
490
+ lastModified: new Date().toISOString(),
491
+ local: true,
492
+ });
493
+
494
+ const px = await env.start();
495
+
496
+ try {
497
+ const { waitForOutput } = await px(['resources', 'list']);
498
+
499
+ // Should show "Found 1 resource:" (singular)
500
+ const foundCount = await waitForOutput(/Found 1 resource:/);
501
+ expect(foundCount).toBe(true);
502
+ } finally {
503
+ await env.stopAndCleanup();
504
+ }
505
+ });
506
+ });
507
+
508
+ describe('resources delete command', () => {
509
+ it('should show error when resource does not exist', async () => {
510
+ const env = await createTestEnv();
511
+ const px = await env.start();
512
+
513
+ try {
514
+ const { waitForOutput } = await px([
515
+ 'resources',
516
+ 'upload',
517
+ '-d',
518
+ 'non-existent.txt',
519
+ ]);
520
+
521
+ // Should show checking state
522
+ const foundChecking = await waitForOutput(/Checking resource\.\.\./);
523
+ expect(foundChecking).toBe(true);
524
+
525
+ // Should show warning for non-existent resource
526
+ const foundWarning = await waitForOutput(
527
+ /Warning: This will permanently delete/
528
+ );
529
+ expect(foundWarning).toBe(true);
530
+ } finally {
531
+ await env.stopAndCleanup();
532
+ }
533
+ });
534
+
535
+ it('should prevent deletion of local resources', async () => {
536
+ const env = await createTestEnv();
537
+
538
+ // Add a local resource
539
+ env.server.addResource({
540
+ key: 'local-file.txt',
541
+ type: 'text',
542
+ size: 100,
543
+ lastModified: new Date().toISOString(),
544
+ local: true,
545
+ });
546
+
547
+ const px = await env.start();
548
+
549
+ try {
550
+ const { waitForOutput } = await px([
551
+ 'resources',
552
+ 'upload',
553
+ '-d',
554
+ 'local-file.txt',
555
+ ]);
556
+
557
+ // Should show error about local resource
558
+ const foundError = await waitForOutput(/Cannot Delete Local Resource/);
559
+ expect(foundError).toBe(true);
560
+
561
+ // Should show explanation
562
+ const foundExplanation = await waitForOutput(
563
+ /This resource was synced from your local filesystem/
564
+ );
565
+ expect(foundExplanation).toBe(true);
566
+
567
+ // Should show instructions
568
+ const foundInstructions = await waitForOutput(
569
+ /delete the file locally and run 'px resources sync'/
570
+ );
571
+ expect(foundInstructions).toBe(true);
572
+ } finally {
573
+ await env.stopAndCleanup();
574
+ }
575
+ });
576
+
577
+ it('should show confirmation prompt for remote resources', async () => {
578
+ const env = await createTestEnv();
579
+
580
+ // Add a remote resource
581
+ env.server.addResource({
582
+ key: 'remote-file.txt',
583
+ type: 'text',
584
+ size: 200,
585
+ lastModified: new Date().toISOString(),
586
+ local: false,
587
+ });
588
+
589
+ const px = await env.start();
590
+
591
+ try {
592
+ const { waitForOutput } = await px([
593
+ 'resources',
594
+ 'upload',
595
+ '-d',
596
+ 'remote-file.txt',
597
+ ]);
598
+
599
+ // Should show warning
600
+ const foundWarning = await waitForOutput(
601
+ /Warning: This will permanently delete the following resource/
602
+ );
603
+ expect(foundWarning).toBe(true);
604
+
605
+ // Should show the resource path
606
+ const foundPath = await waitForOutput(/remote-file\.txt/);
607
+ expect(foundPath).toBe(true);
608
+
609
+ // Should show confirmation prompt
610
+ const foundPrompt = await waitForOutput(
611
+ /Type "yes" to confirm deletion:/
612
+ );
613
+ expect(foundPrompt).toBe(true);
614
+ } finally {
615
+ await env.stopAndCleanup();
616
+ }
617
+ });
618
+
619
+ it('should delete resource when using force flag', async () => {
620
+ const env = await createTestEnv();
621
+
622
+ // Add a remote resource BEFORE starting the server
623
+ env.server.addResource({
624
+ key: 'to-delete.txt',
625
+ type: 'text',
626
+ size: 300,
627
+ lastModified: new Date().toISOString(),
628
+ local: false,
629
+ });
630
+
631
+ const px = await env.start();
632
+
633
+ try {
634
+ const { waitForOutput } = await px([
635
+ 'resources',
636
+ 'upload',
637
+ '-d',
638
+ '-f',
639
+ 'to-delete.txt',
640
+ ]);
641
+
642
+ // Should check resource first
643
+ const foundChecking = await waitForOutput(/Checking resource\.\.\./);
644
+ expect(foundChecking).toBe(true);
645
+
646
+ // Should show success message (may skip the deleting message due to speed)
647
+ const foundSuccess = await waitForOutput(
648
+ /✅ Successfully deleted: to-delete\.txt/
649
+ );
650
+ expect(foundSuccess).toBe(true);
651
+
652
+ // Verify the resource was actually deleted
653
+ const calls = env.server.getLogs();
654
+ const deleteCall = calls.find(
655
+ (c) => c.method === 'deleteResource' && c.args[0] === 'to-delete.txt'
656
+ );
657
+ expect(deleteCall).toBeDefined();
658
+ } finally {
659
+ await env.stopAndCleanup();
660
+ }
661
+ });
662
+
663
+ it('should still prevent deletion of local resources with force flag', async () => {
664
+ const env = await createTestEnv();
665
+
666
+ // Add a local resource
667
+ env.server.addResource({
668
+ key: 'local-file.txt',
669
+ type: 'text',
670
+ size: 100,
671
+ lastModified: new Date().toISOString(),
672
+ local: true,
673
+ });
674
+
675
+ const px = await env.start();
676
+
677
+ try {
678
+ const { waitForOutput } = await px([
679
+ 'resources',
680
+ 'upload',
681
+ '-d',
682
+ '-f',
683
+ 'local-file.txt',
684
+ ]);
685
+
686
+ // Should still show error about local resource even with force flag
687
+ const foundError = await waitForOutput(/Cannot Delete Local Resource/);
688
+ expect(foundError).toBe(true);
689
+ } finally {
690
+ await env.stopAndCleanup();
691
+ }
692
+ });
693
+
694
+ it('should handle API connection errors', async () => {
695
+ const env = await createTestEnv();
696
+ // Don't start the server to simulate connection error
697
+
698
+ try {
699
+ const { waitForOutput } = await px(
700
+ ['resources', 'upload', '-d', 'any-resource.txt'],
701
+ { server: env.server }
702
+ );
703
+
704
+ // Should show checking message first
705
+ const foundChecking = await waitForOutput(/Checking resource\.\.\./);
706
+ expect(foundChecking).toBe(true);
707
+
708
+ // Should eventually show connection error
709
+ const foundError = await waitForOutput(/Error connecting/);
710
+ expect(foundError).toBe(true);
711
+ } finally {
712
+ // Just cleanup temp dir since server was never started
713
+ }
714
+ });
715
+
716
+ it('should handle nested resource paths with force flag', async () => {
717
+ const env = await createTestEnv();
718
+
719
+ // Add a nested resource
720
+ env.server.addResource({
721
+ key: 'folder/subfolder/nested-file.txt',
722
+ type: 'text',
723
+ size: 800,
724
+ lastModified: new Date().toISOString(),
725
+ local: false,
726
+ });
727
+
728
+ const px = await env.start();
729
+
730
+ try {
731
+ const { waitForOutput } = await px([
732
+ 'resources',
733
+ 'upload',
734
+ '-d',
735
+ '-f',
736
+ 'folder/subfolder/nested-file.txt',
737
+ ]);
738
+
739
+ // Should delete successfully
740
+ const foundSuccess = await waitForOutput(
741
+ /✅ Successfully deleted: folder\/subfolder\/nested-file\.txt/
742
+ );
743
+ expect(foundSuccess).toBe(true);
744
+
745
+ // Verify the resource was deleted
746
+ const calls = env.server.getLogs();
747
+ const deleteCall = calls.find(
748
+ (c) =>
749
+ c.method === 'deleteResource' &&
750
+ c.args[0] === 'folder/subfolder/nested-file.txt'
751
+ );
752
+ expect(deleteCall).toBeDefined();
753
+ } finally {
754
+ await env.stopAndCleanup();
755
+ }
756
+ });
757
+
758
+ it('should handle nested resource paths without force flag', async () => {
759
+ const env = await createTestEnv();
760
+
761
+ // Add a nested resource
762
+ env.server.addResource({
763
+ key: 'folder/nested.txt',
764
+ type: 'text',
765
+ size: 400,
766
+ lastModified: new Date().toISOString(),
767
+ local: false,
768
+ });
769
+
770
+ const px = await env.start();
771
+
772
+ try {
773
+ const { waitForOutput } = await px([
774
+ 'resources',
775
+ 'upload',
776
+ '-d',
777
+ 'folder/nested.txt',
778
+ ]);
779
+
780
+ // Should show the full nested path in the warning
781
+ const foundPath = await waitForOutput(/folder\/nested\.txt/);
782
+ expect(foundPath).toBe(true);
783
+
784
+ // Should show confirmation prompt
785
+ const foundPrompt = await waitForOutput(
786
+ /Type "yes" to confirm deletion:/
787
+ );
788
+ expect(foundPrompt).toBe(true);
789
+ } finally {
790
+ await env.stopAndCleanup();
791
+ }
792
+ });
793
+
794
+ it('should show loading state while checking resource', async () => {
795
+ const env = await createTestEnv();
796
+ const px = await env.start();
797
+
798
+ try {
799
+ // Stop server to simulate slow loading
800
+ env.server.stop();
801
+
802
+ const { waitForOutput } = await px([
803
+ 'resources',
804
+ 'upload',
805
+ '-d',
806
+ 'any-file.txt',
807
+ ]);
808
+
809
+ // Should show checking message
810
+ const foundChecking = await waitForOutput(/Checking resource\.\.\./);
811
+ expect(foundChecking).toBe(true);
812
+
813
+ // Should eventually show error when server is down
814
+ const foundError = await waitForOutput(/Error/);
815
+ expect(foundError).toBe(true);
816
+ } finally {
817
+ // Cleanup without stopping server (already stopped)
818
+ env.cleanup();
819
+ }
820
+ });
821
+ });
822
+
823
+ describe('resources clear command', () => {
824
+ it('should handle empty resources gracefully', async () => {
825
+ const env = await createTestEnv();
826
+ const px = await env.start();
827
+
828
+ try {
829
+ // TestDevServer starts with empty resources by default
830
+ const { waitForOutput } = await px(['resources', 'clear']);
831
+ const isOutputRendered = await waitForOutput(
832
+ /No resources to delete/,
833
+ 20
834
+ );
835
+
836
+ expect(isOutputRendered).toBe(true);
837
+ } finally {
838
+ await env.stopAndCleanup();
839
+ }
840
+ });
841
+
842
+ it('should show warning for non-empty resources', async () => {
843
+ const env = await createTestEnv();
844
+
845
+ // Add some mock resources to the test server
846
+ env.server.addResource({
847
+ key: 'test.txt',
848
+ type: 'text',
849
+ size: 100,
850
+ lastModified: new Date().toISOString(),
851
+ });
852
+ env.server.addResource({
853
+ key: 'data/config.json',
854
+ type: 'text',
855
+ size: 200,
856
+ lastModified: new Date().toISOString(),
857
+ });
858
+
859
+ const px = await env.start();
860
+
861
+ try {
862
+ const { waitForOutput } = await px(['resources', 'clear']);
863
+
864
+ // Should show danger warning
865
+ const isWarningShown = await waitForOutput(
866
+ /DANGER: This will permanently delete ALL resources!/
867
+ );
868
+ expect(isWarningShown).toBe(true);
869
+
870
+ // Should show resource count (2 resources)
871
+ const isCountShown = await waitForOutput(
872
+ /This action will delete 2 resource\(s\)/
873
+ );
874
+ expect(isCountShown).toBe(true);
875
+
876
+ // Should show the selection prompt
877
+ const isPromptShown = await waitForOutput(
878
+ /Use arrow keys to select, Enter to confirm:/
879
+ );
880
+ expect(isPromptShown).toBe(true);
881
+
882
+ // Should show cancel option as selected by default
883
+ const isCancelSelected = await waitForOutput(
884
+ /▶ Cancel \(keep resources\)/
885
+ );
886
+ expect(isCancelSelected).toBe(true);
887
+ } finally {
888
+ await env.stopAndCleanup();
889
+ }
890
+ });
891
+
892
+ it('should navigate between options with arrow keys', async () => {
893
+ const env = await createTestEnv();
894
+
895
+ // Add a mock resource so we get the confirmation prompt
896
+ env.server.addResource({
897
+ key: 'test.txt',
898
+ type: 'text',
899
+ size: 100,
900
+ lastModified: new Date().toISOString(),
901
+ });
902
+
903
+ const px = await env.start();
904
+
905
+ try {
906
+ const { waitForOutput, instance } = await px(['resources', 'clear']);
907
+
908
+ // Wait for the prompt to appear
909
+ await waitForOutput(/Use arrow keys to select, Enter to confirm:/);
910
+
911
+ // Verify cancel is selected by default
912
+ const isCancelSelected = await waitForOutput(
913
+ /▶ Cancel \(keep resources\)/
914
+ );
915
+ expect(isCancelSelected).toBe(true);
916
+
917
+ // Press down arrow
918
+ instance.stdin.write('\u001B[B');
919
+
920
+ // Should show delete option as selected
921
+ const isDeleteSelected = await waitForOutput(
922
+ /▶ Delete all resources/,
923
+ 30
924
+ );
925
+ expect(isDeleteSelected).toBe(true);
926
+
927
+ // Press up arrow to go back
928
+ instance.stdin.write('\u001B[A');
929
+
930
+ // Should show cancel option as selected again
931
+ const isCancelSelectedAgain = await waitForOutput(
932
+ /▶ Cancel \(keep resources\)/,
933
+ 30
934
+ );
935
+ expect(isCancelSelectedAgain).toBe(true);
936
+ } finally {
937
+ await env.stopAndCleanup();
938
+ }
939
+ });
940
+ });
941
+
942
+ describe('resources types command', () => {
943
+ it('should generate resource types successfully', async () => {
944
+ const env = await createTestEnv();
945
+ const px = await env.start();
946
+
947
+ try {
948
+ const { waitForOutput } = await px(['resources', 'types']);
949
+
950
+ // Test user-visible output
951
+ const foundSuccess = await waitForOutput(/Generated resource types at/i);
952
+ expect(foundSuccess).toBe(true);
953
+
954
+ // Verify the types file was actually created
955
+ const typesPath = path.join(env.projectRootDir, 'resources.d.ts');
956
+ expect(fs.existsSync(typesPath)).toBe(true);
957
+
958
+ // Verify basic content of the types file
959
+ const typesContent = fs.readFileSync(typesPath, 'utf-8');
960
+ expect(typesContent).toContain("declare module '@positronic/core'");
961
+ expect(typesContent).toContain('interface Resources');
962
+ } finally {
963
+ await env.stopAndCleanup();
964
+ }
965
+ });
966
+
967
+ it('should handle type generation errors gracefully', async () => {
968
+ const env = await createTestEnv();
969
+ const px = await env.start();
970
+
971
+ try {
972
+ // Make the project directory read-only to cause an error
973
+ const typesPath = path.join(env.projectRootDir, 'resources.d.ts');
974
+
975
+ // Create a directory with the same name to cause a write error
976
+ fs.mkdirSync(typesPath);
977
+
978
+ const { waitForOutput } = await px(['resources', 'types']);
979
+
980
+ // Should show error message
981
+ const foundError = await waitForOutput(/Type Generation Failed/i);
982
+ expect(foundError).toBe(true);
983
+
984
+ // Should show some error details
985
+ const foundDetails = await waitForOutput(/EISDIR|directory/i);
986
+ expect(foundDetails).toBe(true);
987
+ } finally {
988
+ await env.stopAndCleanup();
989
+ }
990
+ });
991
+ });
992
+
993
+ describe('resources upload command', () => {
994
+ // Testing the upload command's file validation behavior
995
+ // The actual upload requires presigned URLs which aren't available in test environment
996
+
997
+ it('should handle non-existent file error', async () => {
998
+ const env = await createTestEnv();
999
+ const px = await env.start();
1000
+
1001
+ try {
1002
+ const nonExistentPath = path.join(env.projectRootDir, 'does-not-exist.txt');
1003
+
1004
+ const { waitForOutput } = await px(['resources', 'upload', nonExistentPath]);
1005
+
1006
+ // Should show error immediately since file check happens first
1007
+ const foundError = await waitForOutput(/File Not Found/i);
1008
+ expect(foundError).toBe(true);
1009
+ } finally {
1010
+ await env.stopAndCleanup();
1011
+ }
1012
+ });
1013
+
1014
+ it('should handle directory path error', async () => {
1015
+ const env = await createTestEnv();
1016
+ const px = await env.start();
1017
+
1018
+ try {
1019
+ // Try to upload a directory instead of a file
1020
+ const dirPath = path.join(env.projectRootDir, 'resources');
1021
+
1022
+ const { waitForOutput } = await px(['resources', 'upload', dirPath]);
1023
+
1024
+ // Should show error
1025
+ const foundError = await waitForOutput(/Invalid Path/i);
1026
+ expect(foundError).toBe(true);
1027
+ } finally {
1028
+ await env.stopAndCleanup();
1029
+ }
1030
+ });
1031
+
1032
+ it('should validate files before attempting upload', async () => {
1033
+ const env = await createTestEnv();
1034
+ const px = await env.start();
1035
+
1036
+ try {
1037
+ // Create a test file
1038
+ const testFilePath = path.join(env.projectRootDir, 'test.txt');
1039
+ fs.writeFileSync(testFilePath, 'Hello, world!');
1040
+
1041
+ const { waitForOutput } = await px(['resources', 'upload', testFilePath]);
1042
+
1043
+ // The component will validate the file and attempt upload
1044
+ // Without mocked presigned URLs, it will show an error about the API
1045
+ const foundOutput = await waitForOutput(/Uploading test\.txt|Failed|Error/i);
1046
+ expect(foundOutput).toBe(true);
1047
+ } finally {
1048
+ await env.stopAndCleanup();
1049
+ }
1050
+ });
1051
+ });
1052
+ });