@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,152 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
-
5
- export interface Project {
6
- name: string;
7
- url: string;
8
- addedAt: string;
9
- }
10
-
11
- export interface ProjectConfig {
12
- version: string;
13
- currentProject: string | null;
14
- projects: Project[];
15
- }
16
-
17
- export class ProjectConfigManager {
18
- private configPath: string;
19
- private configDir: string;
20
-
21
- constructor(customConfigDir?: string) {
22
- // Priority: customConfigDir > env variable > default home directory
23
- this.configDir =
24
- customConfigDir ||
25
- process.env.POSITRONIC_CONFIG_DIR ||
26
- path.join(os.homedir(), '.positronic');
27
- this.configPath = path.join(this.configDir, 'config.json');
28
- }
29
-
30
- private ensureConfigDir(): void {
31
- if (!fs.existsSync(this.configDir)) {
32
- fs.mkdirSync(this.configDir, { recursive: true });
33
- }
34
- }
35
-
36
- private getDefaultConfig(): ProjectConfig {
37
- return {
38
- version: '1',
39
- currentProject: null,
40
- projects: [],
41
- };
42
- }
43
-
44
- read(): ProjectConfig {
45
- this.ensureConfigDir();
46
-
47
- if (!fs.existsSync(this.configPath)) {
48
- const defaultConfig = this.getDefaultConfig();
49
- this.write(defaultConfig);
50
- return defaultConfig;
51
- }
52
-
53
- try {
54
- const content = fs.readFileSync(this.configPath, 'utf-8');
55
- return JSON.parse(content) as ProjectConfig;
56
- } catch (error) {
57
- console.error('Error reading config file:', error);
58
- // Return default config if file is corrupted
59
- return this.getDefaultConfig();
60
- }
61
- }
62
-
63
- write(config: ProjectConfig): void {
64
- this.ensureConfigDir();
65
- fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
66
- }
67
-
68
- addProject(name: string, url: string): { success: boolean; error?: string } {
69
- const config = this.read();
70
-
71
- // Check for duplicate names
72
- if (config.projects.some((p) => p.name === name)) {
73
- return {
74
- success: false,
75
- error: `A project named "${name}" already exists`,
76
- };
77
- }
78
-
79
- // Validate URL
80
- try {
81
- new URL(url);
82
- } catch {
83
- return { success: false, error: 'Invalid URL format' };
84
- }
85
-
86
- const newProject: Project = {
87
- name,
88
- url,
89
- addedAt: new Date().toISOString(),
90
- };
91
-
92
- config.projects.push(newProject);
93
-
94
- // If this is the first project, make it current
95
- if (config.projects.length === 1) {
96
- config.currentProject = name;
97
- }
98
-
99
- this.write(config);
100
- return { success: true };
101
- }
102
-
103
- selectProject(name: string): { success: boolean; error?: string } {
104
- const config = this.read();
105
-
106
- const project = config.projects.find((p) => p.name === name);
107
- if (!project) {
108
- return { success: false, error: `Project "${name}" not found` };
109
- }
110
-
111
- config.currentProject = name;
112
- this.write(config);
113
- return { success: true };
114
- }
115
-
116
- removeProject(name: string): { success: boolean; error?: string } {
117
- const config = this.read();
118
-
119
- const projectIndex = config.projects.findIndex((p) => p.name === name);
120
- if (projectIndex === -1) {
121
- return { success: false, error: `Project "${name}" not found` };
122
- }
123
-
124
- config.projects.splice(projectIndex, 1);
125
-
126
- // If we removed the current project, clear it
127
- if (config.currentProject === name) {
128
- config.currentProject =
129
- config.projects.length > 0 ? config.projects[0].name : null;
130
- }
131
-
132
- this.write(config);
133
- return { success: true };
134
- }
135
-
136
- getCurrentProject(): Project | null {
137
- const config = this.read();
138
- if (!config.currentProject) return null;
139
-
140
- return (
141
- config.projects.find((p) => p.name === config.currentProject) || null
142
- );
143
- }
144
-
145
- listProjects(): { projects: Project[]; current: string | null } {
146
- const config = this.read();
147
- return {
148
- projects: config.projects,
149
- current: config.currentProject,
150
- };
151
- }
152
- }
@@ -1,502 +0,0 @@
1
- /**
2
- * CLI Integration Tests - Testing Philosophy
3
- *
4
- * These tests are designed to be maintainable and resilient to UI changes.
5
- *
6
- * Key principles:
7
- * 1. **Test behavior, not formatting** - Don't check for specific icons, emojis, or exact formatting
8
- * 2. **Look for essential keywords only** - Focus on the minimum text that indicates success/failure
9
- * 3. **Use simple assertions** - Prefer toContain() over complex regex when possible
10
- * 4. **Be case-insensitive** - Use toLowerCase() to avoid breaking on capitalization changes
11
- *
12
- * Examples:
13
- * - For success: look for "added", "switched", etc. (not specific success emojis)
14
- * - For errors: look for "not found", "invalid", "already exists" (not error icons)
15
- * - For data: check that project names and URLs appear, but not their exact formatting
16
- *
17
- * This approach ensures tests remain stable as the CLI's output formatting evolves,
18
- * while still verifying that core functionality works correctly.
19
- */
20
-
21
- // Import nock to use in tests, but configuration happens in jest.setup.js
22
-
23
- import * as fs from 'fs';
24
- import * as path from 'path';
25
- import * as os from 'os';
26
- import { describe, it, expect, afterEach, jest } from '@jest/globals';
27
- import { createTestEnv, px } from './test-utils.js';
28
-
29
- describe('CLI Integration: positronic server with project', () => {
30
- it('runs a brain', async () => {
31
- const env = await createTestEnv();
32
-
33
- // Setup test brain
34
- env.setup((dir: string) => {
35
- const brainsDir = path.join(dir, 'brains');
36
- fs.mkdirSync(brainsDir, { recursive: true });
37
-
38
- // Create a simple test brain
39
- fs.writeFileSync(
40
- path.join(brainsDir, 'test-brain.ts'),
41
- `
42
- export default function testBrain() {
43
- return {
44
- title: 'Test Brain',
45
- steps: [
46
- {
47
- title: 'Test Step',
48
- run: async () => {
49
- return { success: true };
50
- }
51
- }
52
- ]
53
- };
54
- }
55
- `
56
- );
57
- });
58
-
59
- const px = await env.start();
60
-
61
- try {
62
- const { waitForOutput } = await px(['run', 'test-brain']);
63
-
64
- // Verify the run command connected to the server and got a run ID
65
- const outputContainsRunId = await waitForOutput(/Run ID:/);
66
- const outputContainsRunPrefix = await waitForOutput(/run-/);
67
-
68
- expect(outputContainsRunId).toBe(true);
69
- expect(outputContainsRunPrefix).toBe(true);
70
- } finally {
71
- await env.stopAndCleanup();
72
- }
73
- });
74
- });
75
-
76
- describe('CLI Integration: project commands', () => {
77
- let tempDir: string;
78
- let configDir: string;
79
-
80
- beforeEach(() => {
81
- // Create a temp directory for testing
82
- tempDir = fs.mkdtempSync(
83
- path.join(os.tmpdir(), 'positronic-project-test-')
84
- );
85
- configDir = path.join(tempDir, '.positronic');
86
- });
87
-
88
- afterEach(() => {
89
- // Clean up test directory
90
- fs.rmSync(tempDir, { recursive: true, force: true });
91
- });
92
-
93
- describe('project add', () => {
94
- it('should add a new project successfully', async () => {
95
- const { waitForOutput, instance } = await px(
96
- ['project', 'add', 'My App', '--url', 'https://my-app.positronic.sh'],
97
- { configDir }
98
- );
99
-
100
- const isReady = await waitForOutput(/added/i);
101
- expect(isReady).toBe(true);
102
- const output = instance.lastFrame();
103
- expect(output).toMatch(/my app/i);
104
- expect(output).toContain('https://my-app.positronic.sh');
105
-
106
- // Verify config file was created
107
- const configPath = path.join(configDir, 'config.json');
108
- expect(fs.existsSync(configPath)).toBe(true);
109
-
110
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
111
- expect(config.projects).toHaveLength(1);
112
- expect(config.projects[0].name).toBe('My App');
113
- expect(config.currentProject).toBe('My App');
114
- });
115
-
116
- it('should reject duplicate project names', async () => {
117
- // Add first project
118
- const addFirstProject = await px(
119
- ['project', 'add', 'My App', '--url', 'https://my-app.positronic.sh'],
120
- { configDir }
121
- );
122
-
123
- expect(await addFirstProject.waitForOutput(/added/i)).toBe(true);
124
-
125
- // Try to add duplicate
126
- const addDuplicateProject = await px(
127
- ['project', 'add', 'My App', '--url', 'https://other.positronic.sh'],
128
- { configDir }
129
- );
130
- expect(await addDuplicateProject.waitForOutput(/already exists/i)).toBe(
131
- true
132
- );
133
- });
134
-
135
- it('should reject invalid URLs', async () => {
136
- const { waitForOutput } = await px(
137
- ['project', 'add', 'My App', '--url', 'not-a-valid-url'],
138
- { configDir }
139
- );
140
- expect(await waitForOutput(/invalid/i)).toBe(true);
141
- });
142
-
143
- it('should handle project names with spaces', async () => {
144
- const { waitForOutput, instance } = await px(
145
- [
146
- 'project',
147
- 'add',
148
- 'My Production App',
149
- '--url',
150
- 'https://prod.positronic.sh',
151
- ],
152
- { configDir }
153
- );
154
- expect(await waitForOutput(/added/i)).toBe(true);
155
- expect(instance.lastFrame()).toMatch(/my production app/i);
156
-
157
- const config = JSON.parse(
158
- fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
159
- );
160
- expect(config.projects[0].name).toBe('My Production App');
161
- });
162
- });
163
-
164
- describe('project list', () => {
165
- it('should show empty state when no projects configured', async () => {
166
- const { instance } = await px(['project', 'list'], { configDir });
167
- const output = instance.lastFrame() || '';
168
-
169
- expect(output.toLowerCase()).toContain('no projects');
170
- });
171
-
172
- it('should list all projects with current indicator', async () => {
173
- // Add some projects
174
- await px(
175
- ['project', 'add', 'Project One', '--url', 'https://one.positronic.sh'],
176
- { configDir }
177
- );
178
- await px(
179
- ['project', 'add', 'Project Two', '--url', 'https://two.positronic.sh'],
180
- { configDir }
181
- );
182
- await px(['project', 'select', 'Project Two'], { configDir });
183
-
184
- const { instance } = await px(['project', 'list'], { configDir });
185
- const output = instance.lastFrame() || '';
186
-
187
- expect(output).toMatch(/project one/i);
188
- expect(output).toMatch(/project two/i);
189
- });
190
- });
191
-
192
- describe('project select', () => {
193
- beforeEach(async () => {
194
- // Add some projects for selection tests
195
- await px(
196
- [
197
- 'project',
198
- 'add',
199
- 'Project Alpha',
200
- '--url',
201
- 'https://alpha.positronic.sh',
202
- ],
203
- { configDir }
204
- );
205
- await px(
206
- [
207
- 'project',
208
- 'add',
209
- 'Project Beta',
210
- '--url',
211
- 'https://beta.positronic.sh',
212
- ],
213
- { configDir }
214
- );
215
- await px(
216
- [
217
- 'project',
218
- 'add',
219
- 'Project Gamma',
220
- '--url',
221
- 'https://gamma.positronic.sh',
222
- ],
223
- { configDir }
224
- );
225
- });
226
-
227
- it('should select a project by name', async () => {
228
- const { instance } = await px(['project', 'select', 'Project Beta'], {
229
- configDir,
230
- });
231
- const output = instance.lastFrame() || '';
232
-
233
- expect(output.toLowerCase()).toMatch(/switched|selected/);
234
- expect(output).toMatch(/project beta/i);
235
-
236
- // Verify config was updated
237
- const config = JSON.parse(
238
- fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
239
- );
240
- expect(config.currentProject).toBe('Project Beta');
241
- });
242
-
243
- it('should show error for non-existent project', async () => {
244
- const { instance } = await px(['project', 'select', 'Non Existent'], {
245
- configDir,
246
- });
247
- const output = instance.lastFrame() || '';
248
-
249
- expect(output.toLowerCase()).toContain('not found');
250
- expect(output).toMatch(/project alpha/i);
251
- });
252
-
253
- it('should show interactive selection when no name provided', async () => {
254
- const { instance } = await px(['project', 'select'], { configDir });
255
- const output = instance.lastFrame() || '';
256
-
257
- // In test environment, raw mode isn't supported so it shows a non-interactive list
258
- expect(output).toMatch(/project alpha/i);
259
- expect(output).toMatch(/project beta/i);
260
- expect(output).toMatch(/project gamma/i);
261
- });
262
- });
263
-
264
- describe('project show', () => {
265
- it('should show no project selected when empty', async () => {
266
- const { instance } = await px(['project', 'show'], { configDir });
267
- const output = instance.lastFrame() || '';
268
-
269
- expect(output.toLowerCase()).toContain('no project');
270
- });
271
-
272
- it('should show current project details', async () => {
273
- await px(
274
- [
275
- 'project',
276
- 'add',
277
- 'My Current Project',
278
- '--url',
279
- 'https://current.positronic.sh',
280
- ],
281
- { configDir }
282
- );
283
-
284
- const { instance } = await px(['project', 'show'], { configDir });
285
- const output = instance.lastFrame() || '';
286
-
287
- expect(output).toMatch(/my current project/i);
288
- expect(output).toContain('https://current.positronic.sh');
289
- });
290
-
291
- it('should show other projects count when multiple exist', async () => {
292
- await px(
293
- ['project', 'add', 'Project 1', '--url', 'https://one.positronic.sh'],
294
- { configDir }
295
- );
296
- await px(
297
- ['project', 'add', 'Project 2', '--url', 'https://two.positronic.sh'],
298
- { configDir }
299
- );
300
- await px(
301
- ['project', 'add', 'Project 3', '--url', 'https://three.positronic.sh'],
302
- { configDir }
303
- );
304
-
305
- const { instance } = await px(['project', 'show'], { configDir });
306
- const output = instance.lastFrame() || '';
307
-
308
- expect(output.toLowerCase()).toContain('2 other');
309
- });
310
- });
311
-
312
- describe('project rm', () => {
313
- beforeEach(async () => {
314
- // Add some projects for removal tests
315
- await px(
316
- ['project', 'add', 'Project A', '--url', 'https://a.positronic.sh'],
317
- { configDir }
318
- );
319
- await px(
320
- ['project', 'add', 'Project B', '--url', 'https://b.positronic.sh'],
321
- { configDir }
322
- );
323
- await px(
324
- ['project', 'add', 'Project C', '--url', 'https://c.positronic.sh'],
325
- { configDir }
326
- );
327
- });
328
-
329
- it('should remove a project successfully', async () => {
330
- const { waitForOutput, instance } = await px(
331
- ['project', 'rm', 'Project B'],
332
- {
333
- configDir,
334
- }
335
- );
336
-
337
- const isReady = await waitForOutput(/removed successfully/i);
338
- expect(isReady).toBe(true);
339
-
340
- const output = instance.lastFrame() || '';
341
- expect(output).toMatch(/project b/i);
342
-
343
- // Verify project was removed from config
344
- const config = JSON.parse(
345
- fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
346
- );
347
- expect(config.projects).toHaveLength(2);
348
- expect(
349
- config.projects.find((p: any) => p.name === 'Project B')
350
- ).toBeUndefined();
351
- });
352
-
353
- it('should handle removing the current project', async () => {
354
- // Select Project B as current
355
- await px(['project', 'select', 'Project B'], { configDir });
356
-
357
- // Remove the current project
358
- const { waitForOutput, instance } = await px(
359
- ['project', 'rm', 'Project B'],
360
- {
361
- configDir,
362
- }
363
- );
364
-
365
- const isReady = await waitForOutput(/removed successfully/i);
366
- expect(isReady).toBe(true);
367
-
368
- const output = instance.lastFrame() || '';
369
- expect(output).toMatch(/project b/i);
370
-
371
- // Verify current project was switched to another project
372
- const config = JSON.parse(
373
- fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
374
- );
375
- expect(config.currentProject).not.toBe('Project B');
376
- expect(config.currentProject).toBeTruthy(); // Should be either Project A or Project C
377
- });
378
-
379
- it('should handle removing the last project', async () => {
380
- // Remove all but one project
381
- await px(['project', 'rm', 'Project A'], { configDir });
382
- await px(['project', 'rm', 'Project B'], { configDir });
383
-
384
- // Remove the last project
385
- const { waitForOutput, instance } = await px(
386
- ['project', 'rm', 'Project C'],
387
- {
388
- configDir,
389
- }
390
- );
391
-
392
- const isReady = await waitForOutput(/removed successfully/i);
393
- expect(isReady).toBe(true);
394
-
395
- const output = instance.lastFrame() || '';
396
- expect(output.toLowerCase()).toMatch(/no active project/);
397
-
398
- // Verify no projects remain and current project is null
399
- const config = JSON.parse(
400
- fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
401
- );
402
- expect(config.projects).toHaveLength(0);
403
- expect(config.currentProject).toBeNull();
404
- });
405
-
406
- it('should show error for non-existent project', async () => {
407
- const { waitForOutput, instance } = await px(
408
- ['project', 'rm', 'Non Existent'],
409
- {
410
- configDir,
411
- }
412
- );
413
-
414
- const isReady = await waitForOutput(/failed to remove/i);
415
- expect(isReady).toBe(true);
416
-
417
- const output = instance.lastFrame() || '';
418
- expect(output.toLowerCase()).toMatch(/not found/);
419
- });
420
- });
421
-
422
- describe('project command interactions', () => {
423
- it('should maintain state across commands', async () => {
424
- // Add multiple projects
425
- await px(
426
- ['project', 'add', 'First', '--url', 'https://first.positronic.sh'],
427
- { configDir }
428
- );
429
- await px(
430
- ['project', 'add', 'Second', '--url', 'https://second.positronic.sh'],
431
- { configDir }
432
- );
433
-
434
- let { instance } = await px(['project', 'show'], { configDir });
435
- let output = instance.lastFrame() || '';
436
- expect(output).toMatch(/first/i);
437
-
438
- // Switch projects
439
- await px(['project', 'select', 'Second'], { configDir });
440
-
441
- // Verify switch worked
442
- ({ instance } = await px(['project', 'show'], { configDir }));
443
- output = instance.lastFrame() || '';
444
- expect(output).toMatch(/second/i);
445
-
446
- // List should show second project
447
- ({ instance } = await px(['project', 'list'], { configDir }));
448
- output = instance.lastFrame() || '';
449
- expect(output).toMatch(/second/i);
450
- });
451
- });
452
-
453
- describe('CLI Integration: project new', () => {
454
- let tmpRoot: string;
455
- let originalLocalPath: string | undefined;
456
-
457
- beforeEach(() => {
458
- // Set POSITRONIC_LOCAL_PATH to use local template instead of npm
459
- originalLocalPath = process.env.POSITRONIC_LOCAL_PATH;
460
- process.env.POSITRONIC_LOCAL_PATH = path.resolve('.');
461
- });
462
-
463
- afterEach(() => {
464
- // Restore original environment
465
- if (originalLocalPath) {
466
- process.env.POSITRONIC_LOCAL_PATH = originalLocalPath;
467
- } else {
468
- delete process.env.POSITRONIC_LOCAL_PATH;
469
- }
470
-
471
- if (tmpRoot && fs.existsSync(tmpRoot)) {
472
- fs.rmSync(tmpRoot, { recursive: true, force: true });
473
- }
474
- });
475
-
476
- it('should create a new project directory and output success message', async () => {
477
- tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'positronic-new-test-'));
478
- const projectDir = path.join(tmpRoot, 'my-new-project');
479
-
480
- const { waitForOutput, instance } = await px([
481
- 'project',
482
- 'new',
483
- projectDir,
484
- ]);
485
-
486
- // Wait for success text from the UI component
487
- const isReady = await waitForOutput(/project created successfully/i, 200);
488
- expect(isReady).toBe(true);
489
-
490
- // Validate CLI output contains the project name
491
- const output = instance.lastFrame() || '';
492
- expect(output.toLowerCase()).toContain('my-new-project');
493
-
494
- // Ensure project directory and essential files exist
495
- expect(fs.existsSync(projectDir)).toBe(true);
496
- expect(
497
- fs.existsSync(path.join(projectDir, 'positronic.config.json'))
498
- ).toBe(true);
499
- expect(fs.existsSync(path.join(projectDir, 'package.json'))).toBe(true);
500
- });
501
- });
502
- });