@positronic/cli 0.0.2 → 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 (83) hide show
  1. package/dist/src/cli.js +16 -1
  2. package/dist/src/commands/helpers.js +11 -25
  3. package/dist/types/cli.d.ts.map +1 -1
  4. package/dist/types/commands/helpers.d.ts.map +1 -1
  5. package/package.json +11 -4
  6. package/dist/src/commands/brain.test.js +0 -2936
  7. package/dist/src/commands/helpers.test.js +0 -832
  8. package/dist/src/commands/project.test.js +0 -1201
  9. package/dist/src/commands/resources.test.js +0 -2511
  10. package/dist/src/commands/schedule.test.js +0 -1235
  11. package/dist/src/commands/secret.test.d.js +0 -1
  12. package/dist/src/commands/secret.test.js +0 -761
  13. package/dist/src/commands/server.test.js +0 -1237
  14. package/dist/src/commands/test-utils.js +0 -737
  15. package/dist/src/components/secret-sync.js +0 -303
  16. package/dist/src/test/mock-api-client.js +0 -371
  17. package/dist/src/test/test-dev-server.js +0 -1376
  18. package/dist/types/commands/test-utils.d.ts +0 -45
  19. package/dist/types/commands/test-utils.d.ts.map +0 -1
  20. package/dist/types/components/secret-sync.d.ts +0 -9
  21. package/dist/types/components/secret-sync.d.ts.map +0 -1
  22. package/dist/types/test/mock-api-client.d.ts +0 -25
  23. package/dist/types/test/mock-api-client.d.ts.map +0 -1
  24. package/dist/types/test/test-dev-server.d.ts +0 -129
  25. package/dist/types/test/test-dev-server.d.ts.map +0 -1
  26. package/src/cli.ts +0 -981
  27. package/src/commands/backend.ts +0 -63
  28. package/src/commands/brain.test.ts +0 -1004
  29. package/src/commands/brain.ts +0 -215
  30. package/src/commands/helpers.test.ts +0 -487
  31. package/src/commands/helpers.ts +0 -870
  32. package/src/commands/project-config-manager.ts +0 -152
  33. package/src/commands/project.test.ts +0 -502
  34. package/src/commands/project.ts +0 -109
  35. package/src/commands/resources.test.ts +0 -1052
  36. package/src/commands/resources.ts +0 -97
  37. package/src/commands/schedule.test.ts +0 -481
  38. package/src/commands/schedule.ts +0 -65
  39. package/src/commands/secret.test.ts +0 -210
  40. package/src/commands/secret.ts +0 -50
  41. package/src/commands/server.test.ts +0 -493
  42. package/src/commands/server.ts +0 -353
  43. package/src/commands/test-utils.ts +0 -324
  44. package/src/components/brain-history.tsx +0 -198
  45. package/src/components/brain-list.tsx +0 -105
  46. package/src/components/brain-rerun.tsx +0 -111
  47. package/src/components/brain-show.tsx +0 -92
  48. package/src/components/error.tsx +0 -24
  49. package/src/components/project-add.tsx +0 -59
  50. package/src/components/project-create.tsx +0 -83
  51. package/src/components/project-list.tsx +0 -83
  52. package/src/components/project-remove.tsx +0 -55
  53. package/src/components/project-select.tsx +0 -200
  54. package/src/components/project-show.tsx +0 -58
  55. package/src/components/resource-clear.tsx +0 -127
  56. package/src/components/resource-delete.tsx +0 -160
  57. package/src/components/resource-list.tsx +0 -177
  58. package/src/components/resource-sync.tsx +0 -170
  59. package/src/components/resource-types.tsx +0 -55
  60. package/src/components/resource-upload.tsx +0 -182
  61. package/src/components/schedule-create.tsx +0 -90
  62. package/src/components/schedule-delete.tsx +0 -116
  63. package/src/components/schedule-list.tsx +0 -186
  64. package/src/components/schedule-runs.tsx +0 -151
  65. package/src/components/secret-bulk.tsx +0 -79
  66. package/src/components/secret-create.tsx +0 -49
  67. package/src/components/secret-delete.tsx +0 -41
  68. package/src/components/secret-list.tsx +0 -41
  69. package/src/components/watch.tsx +0 -155
  70. package/src/hooks/useApi.ts +0 -183
  71. package/src/positronic.ts +0 -40
  72. package/src/test/data/resources/config.json +0 -1
  73. package/src/test/data/resources/data/config.json +0 -1
  74. package/src/test/data/resources/data/logo.png +0 -2
  75. package/src/test/data/resources/docs/api.md +0 -3
  76. package/src/test/data/resources/docs/readme.md +0 -3
  77. package/src/test/data/resources/example.md +0 -3
  78. package/src/test/data/resources/file with spaces.txt +0 -1
  79. package/src/test/data/resources/readme.md +0 -3
  80. package/src/test/data/resources/test.txt +0 -1
  81. package/src/test/mock-api-client.ts +0 -145
  82. package/src/test/test-dev-server.ts +0 -1003
  83. 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
- };