@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.
- package/dist/src/cli.js +739 -0
- package/dist/src/commands/backend.js +199 -0
- package/dist/src/commands/brain.js +446 -0
- package/dist/src/commands/brain.test.js +2936 -0
- package/dist/src/commands/helpers.js +1315 -0
- package/dist/src/commands/helpers.test.js +832 -0
- package/dist/src/commands/project-config-manager.js +197 -0
- package/dist/src/commands/project.js +130 -0
- package/dist/src/commands/project.test.js +1201 -0
- package/dist/src/commands/resources.js +272 -0
- package/dist/src/commands/resources.test.js +2511 -0
- package/dist/src/commands/schedule.js +73 -0
- package/dist/src/commands/schedule.test.js +1235 -0
- package/dist/src/commands/secret.js +87 -0
- package/dist/src/commands/secret.test.d.js +1 -0
- package/dist/src/commands/secret.test.js +761 -0
- package/dist/src/commands/server.js +816 -0
- package/dist/src/commands/server.test.js +1237 -0
- package/dist/src/commands/test-utils.js +737 -0
- package/dist/src/components/brain-history.js +169 -0
- package/dist/src/components/brain-list.js +108 -0
- package/dist/src/components/brain-rerun.js +313 -0
- package/dist/src/components/brain-show.js +65 -0
- package/dist/src/components/error.js +19 -0
- package/dist/src/components/project-add.js +95 -0
- package/dist/src/components/project-create.js +276 -0
- package/dist/src/components/project-list.js +88 -0
- package/dist/src/components/project-remove.js +91 -0
- package/dist/src/components/project-select.js +224 -0
- package/dist/src/components/project-show.js +41 -0
- package/dist/src/components/resource-clear.js +152 -0
- package/dist/src/components/resource-delete.js +189 -0
- package/dist/src/components/resource-list.js +174 -0
- package/dist/src/components/resource-sync.js +386 -0
- package/dist/src/components/resource-types.js +243 -0
- package/dist/src/components/resource-upload.js +366 -0
- package/dist/src/components/schedule-create.js +259 -0
- package/dist/src/components/schedule-delete.js +161 -0
- package/dist/src/components/schedule-list.js +176 -0
- package/dist/src/components/schedule-runs.js +103 -0
- package/dist/src/components/secret-bulk.js +262 -0
- package/dist/src/components/secret-create.js +199 -0
- package/dist/src/components/secret-delete.js +190 -0
- package/dist/src/components/secret-list.js +190 -0
- package/dist/src/components/secret-sync.js +303 -0
- package/dist/src/components/watch.js +184 -0
- package/dist/src/hooks/useApi.js +512 -0
- package/dist/src/positronic.js +33 -0
- package/dist/src/test/mock-api-client.js +371 -0
- package/dist/src/test/test-dev-server.js +1376 -0
- package/dist/types/cli.d.ts +9 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/commands/backend.d.ts +6 -0
- package/dist/types/commands/backend.d.ts.map +1 -0
- package/dist/types/commands/brain.d.ts +35 -0
- package/dist/types/commands/brain.d.ts.map +1 -0
- package/dist/types/commands/helpers.d.ts +55 -0
- package/dist/types/commands/helpers.d.ts.map +1 -0
- package/dist/types/commands/project-config-manager.d.ts +37 -0
- package/dist/types/commands/project-config-manager.d.ts.map +1 -0
- package/dist/types/commands/project.d.ts +55 -0
- package/dist/types/commands/project.d.ts.map +1 -0
- package/dist/types/commands/resources.d.ts +13 -0
- package/dist/types/commands/resources.d.ts.map +1 -0
- package/dist/types/commands/schedule.d.ts +27 -0
- package/dist/types/commands/schedule.d.ts.map +1 -0
- package/dist/types/commands/secret.d.ts +23 -0
- package/dist/types/commands/secret.d.ts.map +1 -0
- package/dist/types/commands/server.d.ts +12 -0
- package/dist/types/commands/server.d.ts.map +1 -0
- package/dist/types/commands/test-utils.d.ts +45 -0
- package/dist/types/commands/test-utils.d.ts.map +1 -0
- package/dist/types/components/brain-history.d.ts +7 -0
- package/dist/types/components/brain-history.d.ts.map +1 -0
- package/dist/types/components/brain-list.d.ts +2 -0
- package/dist/types/components/brain-list.d.ts.map +1 -0
- package/dist/types/components/brain-rerun.d.ts +9 -0
- package/dist/types/components/brain-rerun.d.ts.map +1 -0
- package/dist/types/components/brain-show.d.ts +6 -0
- package/dist/types/components/brain-show.d.ts.map +1 -0
- package/dist/types/components/error.d.ts +10 -0
- package/dist/types/components/error.d.ts.map +1 -0
- package/dist/types/components/project-add.d.ts +9 -0
- package/dist/types/components/project-add.d.ts.map +1 -0
- package/dist/types/components/project-create.d.ts +6 -0
- package/dist/types/components/project-create.d.ts.map +1 -0
- package/dist/types/components/project-list.d.ts +7 -0
- package/dist/types/components/project-list.d.ts.map +1 -0
- package/dist/types/components/project-remove.d.ts +8 -0
- package/dist/types/components/project-remove.d.ts.map +1 -0
- package/dist/types/components/project-select.d.ts +8 -0
- package/dist/types/components/project-select.d.ts.map +1 -0
- package/dist/types/components/project-show.d.ts +7 -0
- package/dist/types/components/project-show.d.ts.map +1 -0
- package/dist/types/components/resource-clear.d.ts +2 -0
- package/dist/types/components/resource-clear.d.ts.map +1 -0
- package/dist/types/components/resource-delete.d.ts +9 -0
- package/dist/types/components/resource-delete.d.ts.map +1 -0
- package/dist/types/components/resource-list.d.ts +2 -0
- package/dist/types/components/resource-list.d.ts.map +1 -0
- package/dist/types/components/resource-sync.d.ts +8 -0
- package/dist/types/components/resource-sync.d.ts.map +1 -0
- package/dist/types/components/resource-types.d.ts +7 -0
- package/dist/types/components/resource-types.d.ts.map +1 -0
- package/dist/types/components/resource-upload.d.ts +8 -0
- package/dist/types/components/resource-upload.d.ts.map +1 -0
- package/dist/types/components/schedule-create.d.ts +7 -0
- package/dist/types/components/schedule-create.d.ts.map +1 -0
- package/dist/types/components/schedule-delete.d.ts +7 -0
- package/dist/types/components/schedule-delete.d.ts.map +1 -0
- package/dist/types/components/schedule-list.d.ts +6 -0
- package/dist/types/components/schedule-list.d.ts.map +1 -0
- package/dist/types/components/schedule-runs.d.ts +8 -0
- package/dist/types/components/schedule-runs.d.ts.map +1 -0
- package/dist/types/components/secret-bulk.d.ts +8 -0
- package/dist/types/components/secret-bulk.d.ts.map +1 -0
- package/dist/types/components/secret-create.d.ts +9 -0
- package/dist/types/components/secret-create.d.ts.map +1 -0
- package/dist/types/components/secret-delete.d.ts +8 -0
- package/dist/types/components/secret-delete.d.ts.map +1 -0
- package/dist/types/components/secret-list.d.ts +7 -0
- package/dist/types/components/secret-list.d.ts.map +1 -0
- package/dist/types/components/secret-sync.d.ts +9 -0
- package/dist/types/components/secret-sync.d.ts.map +1 -0
- package/dist/types/components/watch.d.ts +7 -0
- package/dist/types/components/watch.d.ts.map +1 -0
- package/dist/types/hooks/useApi.d.ts +29 -0
- package/dist/types/hooks/useApi.d.ts.map +1 -0
- package/dist/types/positronic.d.ts +3 -0
- package/dist/types/positronic.d.ts.map +1 -0
- package/dist/types/test/mock-api-client.d.ts +25 -0
- package/dist/types/test/mock-api-client.d.ts.map +1 -0
- package/dist/types/test/test-dev-server.d.ts +129 -0
- package/dist/types/test/test-dev-server.d.ts.map +1 -0
- package/package.json +37 -0
- package/src/cli.ts +981 -0
- package/src/commands/backend.ts +63 -0
- package/src/commands/brain.test.ts +1004 -0
- package/src/commands/brain.ts +215 -0
- package/src/commands/helpers.test.ts +487 -0
- package/src/commands/helpers.ts +870 -0
- package/src/commands/project-config-manager.ts +152 -0
- package/src/commands/project.test.ts +502 -0
- package/src/commands/project.ts +109 -0
- package/src/commands/resources.test.ts +1052 -0
- package/src/commands/resources.ts +97 -0
- package/src/commands/schedule.test.ts +481 -0
- package/src/commands/schedule.ts +65 -0
- package/src/commands/secret.test.ts +210 -0
- package/src/commands/secret.ts +50 -0
- package/src/commands/server.test.ts +493 -0
- package/src/commands/server.ts +353 -0
- package/src/commands/test-utils.ts +324 -0
- package/src/components/brain-history.tsx +198 -0
- package/src/components/brain-list.tsx +105 -0
- package/src/components/brain-rerun.tsx +111 -0
- package/src/components/brain-show.tsx +92 -0
- package/src/components/error.tsx +24 -0
- package/src/components/project-add.tsx +59 -0
- package/src/components/project-create.tsx +83 -0
- package/src/components/project-list.tsx +83 -0
- package/src/components/project-remove.tsx +55 -0
- package/src/components/project-select.tsx +200 -0
- package/src/components/project-show.tsx +58 -0
- package/src/components/resource-clear.tsx +127 -0
- package/src/components/resource-delete.tsx +160 -0
- package/src/components/resource-list.tsx +177 -0
- package/src/components/resource-sync.tsx +170 -0
- package/src/components/resource-types.tsx +55 -0
- package/src/components/resource-upload.tsx +182 -0
- package/src/components/schedule-create.tsx +90 -0
- package/src/components/schedule-delete.tsx +116 -0
- package/src/components/schedule-list.tsx +186 -0
- package/src/components/schedule-runs.tsx +151 -0
- package/src/components/secret-bulk.tsx +79 -0
- package/src/components/secret-create.tsx +49 -0
- package/src/components/secret-delete.tsx +41 -0
- package/src/components/secret-list.tsx +41 -0
- package/src/components/watch.tsx +155 -0
- package/src/hooks/useApi.ts +183 -0
- package/src/positronic.ts +40 -0
- package/src/test/data/resources/config.json +1 -0
- package/src/test/data/resources/data/config.json +1 -0
- package/src/test/data/resources/data/logo.png +2 -0
- package/src/test/data/resources/docs/api.md +3 -0
- package/src/test/data/resources/docs/readme.md +3 -0
- package/src/test/data/resources/example.md +3 -0
- package/src/test/data/resources/file with spaces.txt +1 -0
- package/src/test/data/resources/readme.md +3 -0
- package/src/test/data/resources/test.txt +1 -0
- package/src/test/mock-api-client.ts +145 -0
- package/src/test/test-dev-server.ts +1003 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { ReactElement } from 'react';
|
|
2
|
+
import { scanLocalResources, generateTypes } from './helpers.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { ResourceList } from '../components/resource-list.js';
|
|
6
|
+
import { ResourceSync } from '../components/resource-sync.js';
|
|
7
|
+
import { ResourceClear } from '../components/resource-clear.js';
|
|
8
|
+
import { ResourceDelete } from '../components/resource-delete.js';
|
|
9
|
+
import { ResourceUpload } from '../components/resource-upload.js';
|
|
10
|
+
import { ResourceTypes } from '../components/resource-types.js';
|
|
11
|
+
import { ErrorComponent } from '../components/error.js';
|
|
12
|
+
import type { PositronicDevServer } from '@positronic/spec';
|
|
13
|
+
|
|
14
|
+
export class ResourcesCommand {
|
|
15
|
+
constructor(private server?: PositronicDevServer) {}
|
|
16
|
+
|
|
17
|
+
list(): ReactElement {
|
|
18
|
+
return React.createElement(ResourceList);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
sync(): ReactElement {
|
|
22
|
+
if (!this.server) {
|
|
23
|
+
return React.createElement(ErrorComponent, {
|
|
24
|
+
error: {
|
|
25
|
+
title: 'Command Not Available',
|
|
26
|
+
message: 'This command is only available in local dev mode',
|
|
27
|
+
details:
|
|
28
|
+
'Please run this command from within a Positronic project directory.',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if resources directory exists, create if it doesn't
|
|
34
|
+
const resourcesDir = path.join(this.server.projectRootDir, 'resources');
|
|
35
|
+
if (!fs.existsSync(resourcesDir)) {
|
|
36
|
+
fs.mkdirSync(resourcesDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
const localResources = scanLocalResources(resourcesDir);
|
|
39
|
+
|
|
40
|
+
return React.createElement(ResourceSync, {
|
|
41
|
+
localResources,
|
|
42
|
+
resourcesDir,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
types(): ReactElement {
|
|
47
|
+
if (!this.server) {
|
|
48
|
+
return React.createElement(ErrorComponent, {
|
|
49
|
+
error: {
|
|
50
|
+
title: 'Command Not Available',
|
|
51
|
+
message: 'This command is only available in local dev mode',
|
|
52
|
+
details:
|
|
53
|
+
'Please run this command from within a Positronic project directory.',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return React.createElement(ResourceTypes, {
|
|
59
|
+
projectRootDir: this.server.projectRootDir,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
delete(resourcePath: string, force: boolean = false): ReactElement {
|
|
64
|
+
if (!resourcePath) {
|
|
65
|
+
return React.createElement(ErrorComponent, {
|
|
66
|
+
error: {
|
|
67
|
+
title: 'Missing Resource Path',
|
|
68
|
+
message: 'Please provide a resource path to delete.',
|
|
69
|
+
details: 'Usage: positronic resources delete <path>',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The resourcePath should be relative to the resources directory
|
|
75
|
+
const resourceKey = resourcePath;
|
|
76
|
+
|
|
77
|
+
return React.createElement(ResourceDelete, {
|
|
78
|
+
resourceKey,
|
|
79
|
+
resourcePath,
|
|
80
|
+
projectRootPath: this.server?.projectRootDir,
|
|
81
|
+
force,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async clear() {
|
|
86
|
+
// Import and render ResourceClear component
|
|
87
|
+
return React.createElement(ResourceClear);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
upload(filePath: string, customKey?: string): ReactElement {
|
|
91
|
+
return React.createElement(ResourceUpload, {
|
|
92
|
+
filePath,
|
|
93
|
+
customKey,
|
|
94
|
+
projectRootPath: this.server?.projectRootDir,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
4
|
+
import { createTestEnv, px, type TestEnv } from './test-utils.js';
|
|
5
|
+
import { ScheduleCommand } from './schedule.js';
|
|
6
|
+
import { TestDevServer } from '../test/test-dev-server.js';
|
|
7
|
+
|
|
8
|
+
describe('schedule command', () => {
|
|
9
|
+
describe('schedule create', () => {
|
|
10
|
+
it('should create a new schedule', async () => {
|
|
11
|
+
const env = await createTestEnv();
|
|
12
|
+
const px = await env.start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const { waitForOutput, instance } = await px([
|
|
16
|
+
'schedule',
|
|
17
|
+
'create',
|
|
18
|
+
'test-brain',
|
|
19
|
+
'0 3 * * *',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// Wait for success message
|
|
23
|
+
const foundSuccess = await waitForOutput(/Schedule created successfully/i, 50);
|
|
24
|
+
expect(foundSuccess).toBe(true);
|
|
25
|
+
|
|
26
|
+
// Verify the output contains the brain name and cron expression
|
|
27
|
+
const output = instance.lastFrame() || '';
|
|
28
|
+
expect(output).toContain('test-brain');
|
|
29
|
+
expect(output).toContain('0 3 * * *');
|
|
30
|
+
|
|
31
|
+
// Verify the API call was made
|
|
32
|
+
const methodCalls = env.server.getLogs();
|
|
33
|
+
const createCall = methodCalls.find((call) => call.method === 'createSchedule');
|
|
34
|
+
expect(createCall).toBeDefined();
|
|
35
|
+
expect(createCall!.args[0]).toEqual({
|
|
36
|
+
brainName: 'test-brain',
|
|
37
|
+
cronExpression: '0 3 * * *',
|
|
38
|
+
});
|
|
39
|
+
} finally {
|
|
40
|
+
await env.stopAndCleanup();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle server connection errors gracefully', async () => {
|
|
45
|
+
const env = await createTestEnv();
|
|
46
|
+
// Don't start the server to simulate connection error
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const { waitForOutput } = await px([
|
|
50
|
+
'schedule',
|
|
51
|
+
'create',
|
|
52
|
+
'test-brain',
|
|
53
|
+
'0 3 * * *',
|
|
54
|
+
], { server: env.server });
|
|
55
|
+
|
|
56
|
+
const foundError = await waitForOutput(/Error connecting to the local development server/i);
|
|
57
|
+
expect(foundError).toBe(true);
|
|
58
|
+
} finally {
|
|
59
|
+
env.cleanup();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('schedule list', () => {
|
|
65
|
+
let originalExit: typeof process.exit;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
// Mock process.exit but don't throw - just track the call
|
|
69
|
+
originalExit = process.exit;
|
|
70
|
+
process.exit = jest.fn() as any;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
process.exit = originalExit;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should list schedules when no schedules exist', async () => {
|
|
78
|
+
const env = await createTestEnv();
|
|
79
|
+
const px = await env.start();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const { waitForOutput } = await px(['schedule', '-l']);
|
|
83
|
+
|
|
84
|
+
// Wait for the empty state message
|
|
85
|
+
const foundEmpty = await waitForOutput(/No schedules found/i, 30);
|
|
86
|
+
expect(foundEmpty).toBe(true);
|
|
87
|
+
|
|
88
|
+
// Verify process.exit was called
|
|
89
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
90
|
+
|
|
91
|
+
// Verify API call was made
|
|
92
|
+
const calls = env.server.getLogs();
|
|
93
|
+
const listCall = calls.find(c => c.method === 'getSchedules');
|
|
94
|
+
expect(listCall).toBeDefined();
|
|
95
|
+
} finally {
|
|
96
|
+
await env.stopAndCleanup();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should list schedules when schedules exist', async () => {
|
|
101
|
+
const env = await createTestEnv();
|
|
102
|
+
const { server } = env;
|
|
103
|
+
|
|
104
|
+
// Add test schedules
|
|
105
|
+
server.addSchedule({
|
|
106
|
+
id: 'schedule-1',
|
|
107
|
+
brainName: 'daily-report',
|
|
108
|
+
cronExpression: '0 9 * * *',
|
|
109
|
+
enabled: true,
|
|
110
|
+
createdAt: Date.now() - 86400000, // 1 day ago
|
|
111
|
+
nextRunAt: Date.now() + 3600000, // 1 hour from now
|
|
112
|
+
});
|
|
113
|
+
server.addSchedule({
|
|
114
|
+
id: 'schedule-2',
|
|
115
|
+
brainName: 'hourly-sync',
|
|
116
|
+
cronExpression: '0 * * * *',
|
|
117
|
+
enabled: true,
|
|
118
|
+
createdAt: Date.now() - 172800000, // 2 days ago
|
|
119
|
+
nextRunAt: Date.now() + 1800000, // 30 mins from now
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const px = await env.start();
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const { waitForOutput, instance } = await px(['schedule', '-l']);
|
|
126
|
+
|
|
127
|
+
// Wait for schedules to appear
|
|
128
|
+
const foundSchedules = await waitForOutput(/daily-report/i, 30);
|
|
129
|
+
expect(foundSchedules).toBe(true);
|
|
130
|
+
|
|
131
|
+
// Check that both schedules are shown
|
|
132
|
+
const output = instance.lastFrame() || '';
|
|
133
|
+
expect(output).toContain('daily-report');
|
|
134
|
+
expect(output).toContain('hourly-sync');
|
|
135
|
+
expect(output).toContain('0 9 * * *');
|
|
136
|
+
expect(output).toContain('0 * * * *');
|
|
137
|
+
|
|
138
|
+
// Verify process.exit was called
|
|
139
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
140
|
+
|
|
141
|
+
// Verify API call was made
|
|
142
|
+
const calls = server.getLogs();
|
|
143
|
+
const listCall = calls.find(c => c.method === 'getSchedules');
|
|
144
|
+
expect(listCall).toBeDefined();
|
|
145
|
+
} finally {
|
|
146
|
+
await env.stopAndCleanup();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should filter schedules by brain name', async () => {
|
|
151
|
+
const env = await createTestEnv();
|
|
152
|
+
const { server } = env;
|
|
153
|
+
|
|
154
|
+
// Add test schedules
|
|
155
|
+
server.addSchedule({
|
|
156
|
+
id: 'schedule-1',
|
|
157
|
+
brainName: 'daily-report',
|
|
158
|
+
cronExpression: '0 9 * * *',
|
|
159
|
+
enabled: true,
|
|
160
|
+
createdAt: Date.now(),
|
|
161
|
+
nextRunAt: Date.now() + 3600000,
|
|
162
|
+
});
|
|
163
|
+
server.addSchedule({
|
|
164
|
+
id: 'schedule-2',
|
|
165
|
+
brainName: 'hourly-sync',
|
|
166
|
+
cronExpression: '0 * * * *',
|
|
167
|
+
enabled: true,
|
|
168
|
+
createdAt: Date.now(),
|
|
169
|
+
nextRunAt: Date.now() + 1800000,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const px = await env.start();
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const { waitForOutput, instance } = await px(['schedule', '-l', '--brain', 'daily-report']);
|
|
176
|
+
|
|
177
|
+
// Wait for filtered results
|
|
178
|
+
const foundSchedule = await waitForOutput(/daily-report/i, 30);
|
|
179
|
+
expect(foundSchedule).toBe(true);
|
|
180
|
+
|
|
181
|
+
// Verify only the filtered schedule is shown
|
|
182
|
+
const output = instance.lastFrame() || '';
|
|
183
|
+
expect(output).toContain('daily-report');
|
|
184
|
+
expect(output).not.toContain('hourly-sync');
|
|
185
|
+
|
|
186
|
+
// Verify process.exit was called
|
|
187
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
188
|
+
|
|
189
|
+
// Verify API call was made
|
|
190
|
+
const calls = server.getLogs();
|
|
191
|
+
const listCall = calls.find(c => c.method === 'getSchedules');
|
|
192
|
+
expect(listCall).toBeDefined();
|
|
193
|
+
} finally {
|
|
194
|
+
await env.stopAndCleanup();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should handle server connection errors', async () => {
|
|
199
|
+
const env = await createTestEnv();
|
|
200
|
+
// Don't start the server to simulate connection error
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const { waitForOutput } = await px(['schedule', '-l'], { server: env.server });
|
|
204
|
+
|
|
205
|
+
const foundError = await waitForOutput(/Error connecting to the local development server/i, 30);
|
|
206
|
+
expect(foundError).toBe(true);
|
|
207
|
+
|
|
208
|
+
// Verify process.exit was called
|
|
209
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
210
|
+
} finally {
|
|
211
|
+
env.cleanup();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('schedule delete', () => {
|
|
217
|
+
let originalExit: typeof process.exit;
|
|
218
|
+
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
// Mock process.exit but don't throw - just track the call
|
|
221
|
+
originalExit = process.exit;
|
|
222
|
+
process.exit = jest.fn() as any;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterEach(() => {
|
|
226
|
+
process.exit = originalExit;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should delete a schedule successfully', async () => {
|
|
230
|
+
const env = await createTestEnv();
|
|
231
|
+
const { server } = env;
|
|
232
|
+
|
|
233
|
+
// Add a schedule to delete
|
|
234
|
+
server.addSchedule({
|
|
235
|
+
id: 'schedule-to-delete',
|
|
236
|
+
brainName: 'test-brain',
|
|
237
|
+
cronExpression: '0 * * * *',
|
|
238
|
+
enabled: true,
|
|
239
|
+
createdAt: Date.now(),
|
|
240
|
+
nextRunAt: Date.now() + 3600000,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const px = await env.start();
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const { waitForOutput } = await px(['schedule', '-d', 'schedule-to-delete', '--force']);
|
|
247
|
+
|
|
248
|
+
// Wait for success message
|
|
249
|
+
const foundSuccess = await waitForOutput(/Schedule deleted successfully/i, 30);
|
|
250
|
+
expect(foundSuccess).toBe(true);
|
|
251
|
+
|
|
252
|
+
// Verify process.exit was called
|
|
253
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
254
|
+
|
|
255
|
+
// Verify API call was made
|
|
256
|
+
const calls = server.getLogs();
|
|
257
|
+
const deleteCall = calls.find(c => c.method === 'deleteSchedule');
|
|
258
|
+
expect(deleteCall).toBeDefined();
|
|
259
|
+
expect(deleteCall!.args[0]).toBe('schedule-to-delete');
|
|
260
|
+
|
|
261
|
+
// Verify schedule was removed from server
|
|
262
|
+
expect(server.getSchedules().length).toBe(0);
|
|
263
|
+
} finally {
|
|
264
|
+
await env.stopAndCleanup();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should handle deleting non-existent schedule', async () => {
|
|
269
|
+
const env = await createTestEnv();
|
|
270
|
+
const { server } = env;
|
|
271
|
+
const px = await env.start();
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const { waitForOutput } = await px(['schedule', '-d', 'non-existent-id', '--force']);
|
|
275
|
+
|
|
276
|
+
// Wait for error message
|
|
277
|
+
const foundError = await waitForOutput(/not found/i, 30);
|
|
278
|
+
expect(foundError).toBe(true);
|
|
279
|
+
|
|
280
|
+
// Verify process.exit was called
|
|
281
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
282
|
+
|
|
283
|
+
// Verify no deleteSchedule call was logged (404 handled by nock)
|
|
284
|
+
const calls = server.getLogs();
|
|
285
|
+
// The server still logs the attempt, but nock returns 404
|
|
286
|
+
expect(calls.some(c => c.method === 'start')).toBe(true);
|
|
287
|
+
} finally {
|
|
288
|
+
await env.stopAndCleanup();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Skipping this test due to complexities with stdin handling in test environment
|
|
293
|
+
// The delete functionality is covered by the force flag test
|
|
294
|
+
|
|
295
|
+
it('should handle server connection errors', async () => {
|
|
296
|
+
const env = await createTestEnv();
|
|
297
|
+
// Don't start the server to simulate connection error
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const { waitForOutput } = await px(['schedule', '-d', 'some-id', '--force'], { server: env.server });
|
|
301
|
+
|
|
302
|
+
const foundError = await waitForOutput(/Error connecting to the local development server/i, 30);
|
|
303
|
+
expect(foundError).toBe(true);
|
|
304
|
+
|
|
305
|
+
// Verify process.exit was called
|
|
306
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
307
|
+
} finally {
|
|
308
|
+
env.cleanup();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('schedule runs', () => {
|
|
314
|
+
it('should show empty state when no runs exist', async () => {
|
|
315
|
+
const env = await createTestEnv();
|
|
316
|
+
const px = await env.start();
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const { waitForOutput } = await px(['schedule', 'runs']);
|
|
320
|
+
|
|
321
|
+
const foundMessage = await waitForOutput(/No scheduled runs found/i, 30);
|
|
322
|
+
expect(foundMessage).toBe(true);
|
|
323
|
+
|
|
324
|
+
// Verify API call was made
|
|
325
|
+
const calls = env.server.getLogs();
|
|
326
|
+
const runsCall = calls.find(c => c.method === 'getScheduleRuns');
|
|
327
|
+
expect(runsCall).toBeDefined();
|
|
328
|
+
} finally {
|
|
329
|
+
await env.stopAndCleanup();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should list scheduled runs when they exist', async () => {
|
|
334
|
+
const env = await createTestEnv();
|
|
335
|
+
const { server } = env;
|
|
336
|
+
|
|
337
|
+
// Add some scheduled runs
|
|
338
|
+
server.addScheduleRun({
|
|
339
|
+
id: 'run-1',
|
|
340
|
+
scheduleId: 'schedule-1',
|
|
341
|
+
status: 'triggered',
|
|
342
|
+
ranAt: Date.now() - 3600000, // 1 hour ago
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
server.addScheduleRun({
|
|
346
|
+
id: 'run-2',
|
|
347
|
+
scheduleId: 'schedule-2',
|
|
348
|
+
status: 'failed',
|
|
349
|
+
ranAt: Date.now() - 7200000, // 2 hours ago
|
|
350
|
+
error: 'Connection timeout',
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const px = await env.start();
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const { waitForOutput } = await px(['schedule', 'runs']);
|
|
357
|
+
|
|
358
|
+
// Check for runs in output
|
|
359
|
+
const foundRuns = await waitForOutput(/Found 2 scheduled runs/i, 30);
|
|
360
|
+
expect(foundRuns).toBe(true);
|
|
361
|
+
|
|
362
|
+
// Check for run IDs
|
|
363
|
+
const foundRun1 = await waitForOutput(/run-1/i, 30);
|
|
364
|
+
expect(foundRun1).toBe(true);
|
|
365
|
+
|
|
366
|
+
const foundRun2 = await waitForOutput(/run-2/i, 30);
|
|
367
|
+
expect(foundRun2).toBe(true);
|
|
368
|
+
} finally {
|
|
369
|
+
await env.stopAndCleanup();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should filter runs by schedule ID', async () => {
|
|
374
|
+
const env = await createTestEnv();
|
|
375
|
+
const { server } = env;
|
|
376
|
+
|
|
377
|
+
// Add runs for different schedules
|
|
378
|
+
server.addScheduleRun({
|
|
379
|
+
id: 'run-1',
|
|
380
|
+
scheduleId: 'schedule-abc',
|
|
381
|
+
status: 'triggered',
|
|
382
|
+
ranAt: Date.now() - 3600000,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
server.addScheduleRun({
|
|
386
|
+
id: 'run-2',
|
|
387
|
+
scheduleId: 'schedule-xyz',
|
|
388
|
+
status: 'triggered',
|
|
389
|
+
ranAt: Date.now() - 7200000,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const px = await env.start();
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const { waitForOutput, instance } = await px([
|
|
396
|
+
'schedule',
|
|
397
|
+
'runs',
|
|
398
|
+
'--schedule-id',
|
|
399
|
+
'schedule-abc'
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
const foundMessage = await waitForOutput(/Found 1 scheduled run for schedule schedule-abc/i, 30);
|
|
403
|
+
expect(foundMessage).toBe(true);
|
|
404
|
+
|
|
405
|
+
// Verify only the filtered run is shown
|
|
406
|
+
const output = instance.lastFrame() || '';
|
|
407
|
+
expect(output).toContain('run-1');
|
|
408
|
+
expect(output).not.toContain('run-2');
|
|
409
|
+
} finally {
|
|
410
|
+
await env.stopAndCleanup();
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should filter runs by status', async () => {
|
|
415
|
+
const env = await createTestEnv();
|
|
416
|
+
const { server } = env;
|
|
417
|
+
|
|
418
|
+
// Add runs with different statuses
|
|
419
|
+
server.addScheduleRun({
|
|
420
|
+
id: 'run-1',
|
|
421
|
+
scheduleId: 'schedule-1',
|
|
422
|
+
status: 'triggered',
|
|
423
|
+
ranAt: Date.now() - 3600000,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
server.addScheduleRun({
|
|
427
|
+
id: 'run-2',
|
|
428
|
+
scheduleId: 'schedule-1',
|
|
429
|
+
status: 'failed',
|
|
430
|
+
ranAt: Date.now() - 7200000,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const px = await env.start();
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const { waitForOutput, instance } = await px([
|
|
437
|
+
'schedule',
|
|
438
|
+
'runs',
|
|
439
|
+
'--status',
|
|
440
|
+
'failed'
|
|
441
|
+
]);
|
|
442
|
+
|
|
443
|
+
const foundMessage = await waitForOutput(/Found 1 scheduled run with status failed/i, 30);
|
|
444
|
+
expect(foundMessage).toBe(true);
|
|
445
|
+
|
|
446
|
+
// Verify only failed run is shown
|
|
447
|
+
const output = instance.lastFrame() || '';
|
|
448
|
+
expect(output).toContain('run-2');
|
|
449
|
+
expect(output).not.toContain('run-1');
|
|
450
|
+
} finally {
|
|
451
|
+
await env.stopAndCleanup();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('should respect limit parameter', async () => {
|
|
456
|
+
const env = await createTestEnv();
|
|
457
|
+
const px = await env.start();
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const { waitForOutput } = await px([
|
|
461
|
+
'schedule',
|
|
462
|
+
'runs',
|
|
463
|
+
'--limit',
|
|
464
|
+
'50'
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
// Wait for the component to render and make the API call
|
|
468
|
+
const found = await waitForOutput(/No scheduled runs found|Found \d+ scheduled run/i, 30);
|
|
469
|
+
expect(found).toBe(true);
|
|
470
|
+
|
|
471
|
+
// Just verify the API was called with the right limit
|
|
472
|
+
const calls = env.server.getLogs();
|
|
473
|
+
const runsCall = calls.find(c => c.method === 'getScheduleRuns');
|
|
474
|
+
expect(runsCall).toBeDefined();
|
|
475
|
+
expect(runsCall?.args[0]).toContain('limit=50');
|
|
476
|
+
} finally {
|
|
477
|
+
await env.stopAndCleanup();
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ArgumentsCamelCase } from 'yargs';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ScheduleCreate } from '../components/schedule-create.js';
|
|
4
|
+
import { ScheduleList } from '../components/schedule-list.js';
|
|
5
|
+
import { ScheduleDelete } from '../components/schedule-delete.js';
|
|
6
|
+
import { ScheduleRuns } from '../components/schedule-runs.js';
|
|
7
|
+
|
|
8
|
+
interface ScheduleCreateArgs {
|
|
9
|
+
brainName: string;
|
|
10
|
+
cronExpression: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ScheduleListArgs {
|
|
14
|
+
brain?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ScheduleDeleteArgs {
|
|
18
|
+
scheduleId: string;
|
|
19
|
+
force: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ScheduleRunsArgs {
|
|
23
|
+
scheduleId?: string;
|
|
24
|
+
limit: number;
|
|
25
|
+
status?: 'triggered' | 'failed' | 'complete';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ScheduleCommand {
|
|
29
|
+
constructor() {}
|
|
30
|
+
|
|
31
|
+
create({
|
|
32
|
+
brainName,
|
|
33
|
+
cronExpression,
|
|
34
|
+
}: ArgumentsCamelCase<ScheduleCreateArgs>): React.ReactElement {
|
|
35
|
+
return React.createElement(ScheduleCreate, {
|
|
36
|
+
brainName,
|
|
37
|
+
cronExpression,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
list({ brain }: ArgumentsCamelCase<ScheduleListArgs>): React.ReactElement {
|
|
42
|
+
return React.createElement(ScheduleList, {
|
|
43
|
+
brainFilter: brain,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
delete({ scheduleId, force }: ArgumentsCamelCase<ScheduleDeleteArgs>): React.ReactElement {
|
|
48
|
+
return React.createElement(ScheduleDelete, {
|
|
49
|
+
scheduleId,
|
|
50
|
+
force,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
runs({
|
|
55
|
+
scheduleId,
|
|
56
|
+
limit,
|
|
57
|
+
status,
|
|
58
|
+
}: ArgumentsCamelCase<ScheduleRunsArgs>): React.ReactElement {
|
|
59
|
+
return React.createElement(ScheduleRuns, {
|
|
60
|
+
scheduleId,
|
|
61
|
+
limit,
|
|
62
|
+
status,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|