@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,177 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { useApiGet } from '../hooks/useApi.js';
4
- import { ErrorComponent } from './error.js';
5
- import { ResourceEntry } from '@positronic/core';
6
-
7
- interface ApiResourceEntry extends ResourceEntry {
8
- size: number;
9
- lastModified: string;
10
- local: boolean;
11
- }
12
-
13
- interface ResourcesResponse {
14
- resources: ApiResourceEntry[];
15
- truncated: boolean;
16
- count: number;
17
- }
18
-
19
- interface TreeNode {
20
- name: string;
21
- type: 'directory' | 'file';
22
- resource?: ApiResourceEntry;
23
- children: Map<string, TreeNode>;
24
- }
25
-
26
- export const ResourceList = () => {
27
- const { data, loading, error } = useApiGet<ResourcesResponse>('/resources');
28
-
29
- if (error) {
30
- return <ErrorComponent error={error} />;
31
- }
32
-
33
- if (loading) {
34
- return <Text>Loading resources...</Text>;
35
- }
36
-
37
- if (!data || data.resources.length === 0) {
38
- return <Text>No resources found in the project.</Text>;
39
- }
40
-
41
- const { resources, truncated } = data;
42
-
43
- // Build tree structure
44
- const root: TreeNode = {
45
- name: 'resources',
46
- type: 'directory',
47
- children: new Map(),
48
- };
49
-
50
- // Build the tree from flat resources
51
- resources.forEach((resource) => {
52
- const parts = resource.key.split('/');
53
- let current = root;
54
-
55
- for (let i = 0; i < parts.length; i++) {
56
- const part = parts[i];
57
- const isLast = i === parts.length - 1;
58
-
59
- if (!current.children.has(part)) {
60
- current.children.set(part, {
61
- name: part,
62
- type: isLast ? 'file' : 'directory',
63
- resource: isLast ? resource : undefined,
64
- children: new Map(),
65
- });
66
- }
67
- current = current.children.get(part)!;
68
- }
69
- });
70
-
71
- return (
72
- <Box flexDirection="column" paddingTop={1} paddingBottom={1}>
73
- <Text bold>
74
- Found {resources.length} resource{resources.length === 1 ? '' : 's'}:
75
- </Text>
76
-
77
- <Box marginTop={1}>
78
- <TreeView node={root} />
79
- </Box>
80
-
81
- {truncated && (
82
- <Box marginTop={1}>
83
- <Text color="yellow">⚠️ Results truncated. More resources exist than shown.</Text>
84
- </Box>
85
- )}
86
-
87
- {/* Legend for resource types */}
88
- {resources.some(r => !r.local) && (
89
- <Box marginTop={1} flexDirection="column">
90
- <Text color="gray">─────</Text>
91
- <Text color="blueBright">
92
- <Text color="blueBright">↗</Text> = uploaded resource (not in local filesystem)
93
- </Text>
94
- </Box>
95
- )}
96
- </Box>
97
- );
98
- };
99
-
100
- interface TreeViewProps {
101
- node: TreeNode;
102
- prefix?: string;
103
- isLast?: boolean;
104
- depth?: number;
105
- }
106
-
107
- const TreeView = ({ node, prefix = '', isLast = true, depth = 0 }: TreeViewProps) => {
108
- const children = Array.from(node.children.entries()).sort(([a], [b]) => {
109
- // Sort directories first, then alphabetically
110
- const aIsDir = node.children.get(a)!.type === 'directory';
111
- const bIsDir = node.children.get(b)!.type === 'directory';
112
- if (aIsDir && !bIsDir) return -1;
113
- if (!aIsDir && bIsDir) return 1;
114
- return a.localeCompare(b);
115
- });
116
-
117
- // Don't render the root node itself, just its children
118
- if (node.name === 'resources' && node.type === 'directory') {
119
- return (
120
- <Box flexDirection="column">
121
- {children.map(([name, child], index) => (
122
- <React.Fragment key={name}>
123
- {index > 0 && <Box><Text> </Text></Box>}
124
- <TreeView
125
- node={child}
126
- prefix=""
127
- isLast={index === children.length - 1}
128
- depth={0}
129
- />
130
- </React.Fragment>
131
- ))}
132
- </Box>
133
- );
134
- }
135
-
136
- const connector = isLast ? '└── ' : '├── ';
137
- const extension = isLast ? ' ' : '│ ';
138
- const isTopLevel = depth === 0;
139
-
140
- // Determine if this is a remote resource
141
- const isRemote = node.resource && !node.resource.local;
142
-
143
- return (
144
- <Box flexDirection="column">
145
- <Box>
146
- {!isTopLevel && <Text dimColor>{prefix}{connector}</Text>}
147
- <Text
148
- color={isRemote ? 'blueBright' : undefined}
149
- >
150
- {node.name}
151
- {node.resource && (
152
- <>
153
- <Text color={isRemote ? 'blueBright' : 'gray'}> ({formatSize(node.resource.size)})</Text>
154
- {isRemote && <Text color="blueBright"> ↗</Text>}
155
- </>
156
- )}
157
- </Text>
158
- </Box>
159
- {children.map(([name, child], index) => (
160
- <TreeView
161
- key={name}
162
- node={child}
163
- prefix={prefix + (isTopLevel ? '' : extension)}
164
- isLast={index === children.length - 1}
165
- depth={depth + 1}
166
- />
167
- ))}
168
- </Box>
169
- );
170
- };
171
-
172
- function formatSize(bytes: number): string {
173
- if (bytes < 1024) return `${bytes} B`;
174
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
175
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
176
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
177
- }
@@ -1,170 +0,0 @@
1
- import React from 'react';
2
- import { useState, useEffect } from 'react';
3
- import { Box, Text } from 'ink';
4
- import { ErrorComponent } from './error.js';
5
- import { syncResources, generateTypes, type SyncProgressCallback } from '../commands/helpers.js';
6
- import { ResourceEntry } from '@positronic/core';
7
-
8
- interface SyncStats {
9
- uploadCount: number;
10
- skipCount: number;
11
- errorCount: number;
12
- totalCount: number;
13
- deleteCount: number;
14
- currentFile?: string;
15
- currentAction?: 'connecting' | 'uploading' | 'checking' | 'deleting' | 'done' | 'error';
16
- errors: Array<{ file: string; message: string }>;
17
- }
18
-
19
- interface ResourceSyncProps {
20
- localResources: ResourceEntry[];
21
- resourcesDir: string | null;
22
- }
23
-
24
- export const ResourceSync = ({
25
- localResources,
26
- resourcesDir
27
- }: ResourceSyncProps) => {
28
- const [stats, setStats] = useState<SyncStats>({
29
- uploadCount: 0,
30
- skipCount: 0,
31
- errorCount: 0,
32
- totalCount: localResources.length,
33
- deleteCount: 0,
34
- errors: [],
35
- currentAction: 'connecting'
36
- });
37
- const [error, setError] = useState<{ title: string; message: string; details?: string } | null>(null);
38
-
39
- useEffect(() => {
40
- if (!resourcesDir) {
41
- return;
42
- }
43
-
44
- const performSync = async () => {
45
- try {
46
- // Get the project root path (parent of resources dir)
47
- const projectRootPath = resourcesDir.replace(/[/\\]resources$/, '');
48
-
49
- const onProgress: SyncProgressCallback = (progress) => {
50
- setStats({
51
- uploadCount: progress.stats.uploadCount || 0,
52
- skipCount: progress.stats.skipCount || 0,
53
- errorCount: progress.stats.errorCount || 0,
54
- totalCount: progress.stats.totalCount || localResources.length,
55
- deleteCount: progress.stats.deleteCount || 0,
56
- errors: progress.stats.errors || [],
57
- currentFile: progress.currentFile,
58
- currentAction: progress.action,
59
- });
60
- };
61
-
62
- const result = await syncResources(projectRootPath, undefined, onProgress);
63
-
64
- // Update final stats with done status
65
- setStats({
66
- ...result,
67
- currentAction: 'done',
68
- });
69
-
70
- // Generate types after successful sync
71
- try {
72
- await generateTypes(projectRootPath);
73
- } catch (typeError) {
74
- // Don't fail the sync if type generation fails
75
- console.error('Failed to generate types:', typeError);
76
- }
77
- } catch (err: any) {
78
- setError({
79
- title: 'Sync Failed',
80
- message: err.message || 'An unknown error occurred',
81
- details: err.code === 'ECONNREFUSED'
82
- ? "Please ensure the server is running ('positronic server' or 'px s')"
83
- : undefined,
84
- });
85
- setStats(prev => ({ ...prev, currentAction: 'error' }));
86
- }
87
- };
88
-
89
- performSync();
90
- }, [localResources, resourcesDir]);
91
-
92
- const {
93
- uploadCount,
94
- skipCount,
95
- errorCount,
96
- totalCount,
97
- deleteCount,
98
- currentFile,
99
- currentAction,
100
- errors,
101
- } = stats;
102
-
103
- const processedCount = uploadCount + skipCount + errorCount;
104
-
105
- if (error) {
106
- return <ErrorComponent error={error} />;
107
- }
108
-
109
- if (currentAction === 'connecting') {
110
- return (
111
- <Box>
112
- <Text>🔌 Connecting to server...</Text>
113
- </Box>
114
- );
115
- }
116
-
117
- return (
118
- <Box flexDirection="column">
119
- {currentAction !== 'done' && currentFile && (
120
- <Box>
121
- <Text>
122
- {currentAction === 'uploading' ? '⬆️ Uploading' :
123
- currentAction === 'deleting' ? '🗑️ Deleting' :
124
- '🔍 Checking'} {currentFile}...
125
- </Text>
126
- </Box>
127
- )}
128
-
129
- {totalCount > 0 && currentAction !== 'done' && (
130
- <Box marginTop={1}>
131
- <Text dimColor>Progress: {processedCount}/{totalCount} files processed</Text>
132
- </Box>
133
- )}
134
-
135
- {totalCount === 0 && currentAction === 'done' && (
136
- <Box flexDirection="column">
137
- <Text>📁 No files found in the resources directory.</Text>
138
- <Text dimColor>Resources directory has been created and is ready for use.</Text>
139
- </Box>
140
- )}
141
-
142
- {errors.length > 0 && (
143
- <Box flexDirection="column" marginTop={1}>
144
- <Text color="red" bold>Errors:</Text>
145
- {errors.map((error, i) => (
146
- <Box key={i} paddingLeft={2}>
147
- <Text color="red">❌ {error.file}: {error.message}</Text>
148
- </Box>
149
- ))}
150
- </Box>
151
- )}
152
-
153
- {currentAction === 'done' && totalCount > 0 && (
154
- <Box flexDirection="column" marginTop={1}>
155
- <Text bold>📊 Sync Summary:</Text>
156
- <Box paddingLeft={2} flexDirection="column">
157
- <Text color="green"> • Uploaded: {uploadCount}</Text>
158
- <Text color="blue"> • Skipped (up to date): {skipCount}</Text>
159
- {deleteCount > 0 && (
160
- <Text color="yellow"> • Deleted: {deleteCount}</Text>
161
- )}
162
- {errorCount > 0 && (
163
- <Text color="red"> • Errors: {errorCount}</Text>
164
- )}
165
- </Box>
166
- </Box>
167
- )}
168
- </Box>
169
- );
170
- };
@@ -1,55 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { Text, Box } from 'ink';
3
- import { generateTypes } from '../commands/helpers.js';
4
- import { ErrorComponent } from './error.js';
5
-
6
- interface ResourceTypesProps {
7
- projectRootDir: string;
8
- }
9
-
10
- export const ResourceTypes: React.FC<ResourceTypesProps> = ({ projectRootDir }) => {
11
- const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
12
- const [typesFilePath, setTypesFilePath] = useState<string>('');
13
- const [error, setError] = useState<Error | null>(null);
14
-
15
- useEffect(() => {
16
- const generateResourceTypes = async () => {
17
- try {
18
- const filePath = await generateTypes(projectRootDir);
19
- setTypesFilePath(filePath);
20
- setStatus('success');
21
- } catch (err) {
22
- setError(err instanceof Error ? err : new Error(String(err)));
23
- setStatus('error');
24
- }
25
- };
26
-
27
- generateResourceTypes();
28
- }, [projectRootDir]);
29
-
30
- if (status === 'loading') {
31
- return (
32
- <Box>
33
- <Text>🔄 Generating resource types...</Text>
34
- </Box>
35
- );
36
- }
37
-
38
- if (status === 'error') {
39
- return (
40
- <ErrorComponent
41
- error={{
42
- title: 'Type Generation Failed',
43
- message: 'Failed to generate resource types.',
44
- details: error?.message || 'Unknown error',
45
- }}
46
- />
47
- );
48
- }
49
-
50
- return (
51
- <Box>
52
- <Text color="green">✅ Generated resource types at {typesFilePath}</Text>
53
- </Box>
54
- );
55
- };
@@ -1,182 +0,0 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
- import { Box, Text } from 'ink';
3
- import { ErrorComponent } from './error.js';
4
- import { uploadFileWithPresignedUrl, generateTypes } from '../commands/helpers.js';
5
- import * as fs from 'fs';
6
- import * as path from 'path';
7
-
8
- interface ResourceUploadProps {
9
- filePath: string;
10
- customKey?: string;
11
- projectRootPath?: string;
12
- }
13
-
14
- interface UploadProgress {
15
- loaded: number;
16
- total: number;
17
- percentage: number;
18
- }
19
-
20
- export const ResourceUpload = ({ filePath, customKey, projectRootPath }: ResourceUploadProps) => {
21
- const [uploading, setUploading] = useState(false);
22
- const [progress, setProgress] = useState<UploadProgress | null>(null);
23
- const [complete, setComplete] = useState(false);
24
- const [error, setError] = useState<{ title: string; message: string; details?: string } | null>(null);
25
- const [resourceKey, setResourceKey] = useState<string>('');
26
- const abortControllerRef = useRef<AbortController | null>(null);
27
-
28
- useEffect(() => {
29
- performUpload();
30
- return () => {
31
- // Cleanup: abort upload if component unmounts
32
- if (abortControllerRef.current) {
33
- abortControllerRef.current.abort();
34
- }
35
- };
36
- }, []);
37
-
38
- const performUpload = async () => {
39
- try {
40
- // Check if file exists
41
- if (!fs.existsSync(filePath)) {
42
- setError({
43
- title: 'File Not Found',
44
- message: `The file "${filePath}" does not exist.`,
45
- });
46
- return;
47
- }
48
-
49
- const stats = fs.statSync(filePath);
50
- if (!stats.isFile()) {
51
- setError({
52
- title: 'Invalid Path',
53
- message: `"${filePath}" is not a file.`,
54
- });
55
- return;
56
- }
57
-
58
- // Determine resource key
59
- const key = customKey || path.basename(filePath);
60
- setResourceKey(key);
61
-
62
- setUploading(true);
63
- setProgress({
64
- loaded: 0,
65
- total: stats.size,
66
- percentage: 0,
67
- });
68
-
69
- // Create abort controller for cancellation
70
- abortControllerRef.current = new AbortController();
71
-
72
- // Use presigned URL upload with progress callback
73
- await uploadFileWithPresignedUrl(
74
- filePath,
75
- key,
76
- undefined, // Use default apiClient
77
- (progressInfo) => {
78
- setProgress(progressInfo);
79
- },
80
- abortControllerRef.current?.signal
81
- );
82
-
83
- setProgress({
84
- loaded: stats.size,
85
- total: stats.size,
86
- percentage: 100,
87
- });
88
- setComplete(true);
89
-
90
- // Generate types after successful upload if in local dev mode
91
- if (projectRootPath) {
92
- try {
93
- await generateTypes(projectRootPath);
94
- } catch (typeError) {
95
- // Don't fail the upload if type generation fails
96
- console.error('Failed to generate types:', typeError);
97
- }
98
- }
99
- } catch (err: any) {
100
- if (err.name === 'AbortError' || err.message === 'AbortError') {
101
- setError({
102
- title: 'Upload Cancelled',
103
- message: 'The upload was cancelled.',
104
- });
105
- } else if (err.message?.includes('R2 credentials not configured')) {
106
- setError({
107
- title: 'R2 Configuration Required',
108
- message: 'Large file uploads require R2 configuration.',
109
- details: 'Set R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ACCOUNT_ID, and R2_BUCKET_NAME in your .env file.',
110
- });
111
- } else {
112
- setError({
113
- title: 'Upload Failed',
114
- message: err.message || 'An unknown error occurred',
115
- details: err.code === 'ECONNREFUSED'
116
- ? "Please ensure the server is running ('positronic server' or 'px s')"
117
- : undefined,
118
- });
119
- }
120
- } finally {
121
- setUploading(false);
122
- abortControllerRef.current = null;
123
- }
124
- };
125
-
126
- if (error) {
127
- return <ErrorComponent error={error} />;
128
- }
129
-
130
- if (complete) {
131
- return (
132
- <Box flexDirection="column">
133
- <Text color="green">✅ Upload complete!</Text>
134
- <Text dimColor>Resource key: {resourceKey}</Text>
135
- </Box>
136
- );
137
- }
138
-
139
- if (uploading && progress) {
140
- return (
141
- <Box flexDirection="column">
142
- <Text>⬆️ Uploading {path.basename(filePath)}...</Text>
143
- <Box marginTop={1}>
144
- <Text dimColor>Size: {formatSize(progress.total)}</Text>
145
- </Box>
146
- {progress.total > 1024 * 1024 && ( // Show progress bar for files > 1MB
147
- <Box marginTop={1}>
148
- <ProgressBar percentage={progress.percentage} />
149
- </Box>
150
- )}
151
- </Box>
152
- );
153
- }
154
-
155
- return <Text>Preparing upload...</Text>;
156
- };
157
-
158
- interface ProgressBarProps {
159
- percentage: number;
160
- }
161
-
162
- const ProgressBar = ({ percentage }: ProgressBarProps) => {
163
- const width = 30;
164
- const filled = Math.round((percentage / 100) * width);
165
- const empty = width - filled;
166
-
167
- return (
168
- <Box>
169
- <Text>[</Text>
170
- <Text color="green">{'█'.repeat(filled)}</Text>
171
- <Text dimColor>{'░'.repeat(empty)}</Text>
172
- <Text>] {percentage.toFixed(0)}%</Text>
173
- </Box>
174
- );
175
- };
176
-
177
- function formatSize(bytes: number): string {
178
- if (bytes < 1024) return `${bytes} B`;
179
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
180
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
181
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
182
- }
@@ -1,90 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { Box, Text } from 'ink';
3
- import { ErrorComponent } from './error.js';
4
- import { useApiPost } from '../hooks/useApi.js';
5
-
6
- interface ScheduleCreateProps {
7
- brainName: string;
8
- cronExpression: string;
9
- }
10
-
11
- interface CreateScheduleResponse {
12
- id: string;
13
- brainName: string;
14
- cronExpression: string;
15
- enabled: boolean;
16
- createdAt: number;
17
- nextRunAt?: number;
18
- }
19
-
20
- export const ScheduleCreate = ({ brainName, cronExpression }: ScheduleCreateProps) => {
21
- const [created, setCreated] = useState(false);
22
- const [schedule, setSchedule] = useState<CreateScheduleResponse | null>(null);
23
-
24
- const { execute, loading, error } = useApiPost<CreateScheduleResponse>('/brains/schedules', {
25
- headers: {
26
- 'Content-Type': 'application/json',
27
- },
28
- });
29
-
30
- useEffect(() => {
31
- const createSchedule = async () => {
32
- try {
33
- const body = JSON.stringify({ brainName, cronExpression });
34
- const result = await execute(body);
35
- setSchedule(result);
36
- setCreated(true);
37
- } catch (err) {
38
- // Error is already handled by useApiPost
39
- }
40
- };
41
-
42
- createSchedule();
43
- }, []);
44
-
45
- if (error) {
46
- return <ErrorComponent error={error} />;
47
- }
48
-
49
- if (loading) {
50
- return (
51
- <Box>
52
- <Text>⏰ Creating schedule...</Text>
53
- </Box>
54
- );
55
- }
56
-
57
- if (created && schedule) {
58
- return (
59
- <Box flexDirection="column">
60
- <Text color="green">✅ Schedule created successfully!</Text>
61
- <Box marginTop={1} paddingLeft={2} flexDirection="column">
62
- <Text>
63
- <Text bold>Schedule ID:</Text> {schedule.id}
64
- </Text>
65
- <Text>
66
- <Text bold>Brain:</Text> {schedule.brainName}
67
- </Text>
68
- <Text>
69
- <Text bold>Cron Expression:</Text> {schedule.cronExpression}
70
- </Text>
71
- <Text>
72
- <Text bold>Status:</Text> {schedule.enabled ? 'Enabled' : 'Disabled'}
73
- </Text>
74
- {schedule.nextRunAt && (
75
- <Text>
76
- <Text bold>Next Run:</Text> {new Date(schedule.nextRunAt).toLocaleString()}
77
- </Text>
78
- )}
79
- </Box>
80
- <Box marginTop={1}>
81
- <Text dimColor>
82
- Tip: Use "px schedule list" to view all schedules
83
- </Text>
84
- </Box>
85
- </Box>
86
- );
87
- }
88
-
89
- return null;
90
- };