@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.
- package/dist/src/cli.js +16 -1
- package/dist/src/commands/helpers.js +11 -25
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/commands/helpers.d.ts.map +1 -1
- package/package.json +11 -4
- package/dist/src/commands/brain.test.js +0 -2936
- package/dist/src/commands/helpers.test.js +0 -832
- package/dist/src/commands/project.test.js +0 -1201
- package/dist/src/commands/resources.test.js +0 -2511
- package/dist/src/commands/schedule.test.js +0 -1235
- package/dist/src/commands/secret.test.d.js +0 -1
- package/dist/src/commands/secret.test.js +0 -761
- package/dist/src/commands/server.test.js +0 -1237
- package/dist/src/commands/test-utils.js +0 -737
- package/dist/src/components/secret-sync.js +0 -303
- package/dist/src/test/mock-api-client.js +0 -371
- package/dist/src/test/test-dev-server.js +0 -1376
- package/dist/types/commands/test-utils.d.ts +0 -45
- package/dist/types/commands/test-utils.d.ts.map +0 -1
- package/dist/types/components/secret-sync.d.ts +0 -9
- package/dist/types/components/secret-sync.d.ts.map +0 -1
- package/dist/types/test/mock-api-client.d.ts +0 -25
- package/dist/types/test/mock-api-client.d.ts.map +0 -1
- package/dist/types/test/test-dev-server.d.ts +0 -129
- package/dist/types/test/test-dev-server.d.ts.map +0 -1
- package/src/cli.ts +0 -981
- package/src/commands/backend.ts +0 -63
- package/src/commands/brain.test.ts +0 -1004
- package/src/commands/brain.ts +0 -215
- package/src/commands/helpers.test.ts +0 -487
- package/src/commands/helpers.ts +0 -870
- package/src/commands/project-config-manager.ts +0 -152
- package/src/commands/project.test.ts +0 -502
- package/src/commands/project.ts +0 -109
- package/src/commands/resources.test.ts +0 -1052
- package/src/commands/resources.ts +0 -97
- package/src/commands/schedule.test.ts +0 -481
- package/src/commands/schedule.ts +0 -65
- package/src/commands/secret.test.ts +0 -210
- package/src/commands/secret.ts +0 -50
- package/src/commands/server.test.ts +0 -493
- package/src/commands/server.ts +0 -353
- package/src/commands/test-utils.ts +0 -324
- package/src/components/brain-history.tsx +0 -198
- package/src/components/brain-list.tsx +0 -105
- package/src/components/brain-rerun.tsx +0 -111
- package/src/components/brain-show.tsx +0 -92
- package/src/components/error.tsx +0 -24
- package/src/components/project-add.tsx +0 -59
- package/src/components/project-create.tsx +0 -83
- package/src/components/project-list.tsx +0 -83
- package/src/components/project-remove.tsx +0 -55
- package/src/components/project-select.tsx +0 -200
- package/src/components/project-show.tsx +0 -58
- package/src/components/resource-clear.tsx +0 -127
- package/src/components/resource-delete.tsx +0 -160
- package/src/components/resource-list.tsx +0 -177
- package/src/components/resource-sync.tsx +0 -170
- package/src/components/resource-types.tsx +0 -55
- package/src/components/resource-upload.tsx +0 -182
- package/src/components/schedule-create.tsx +0 -90
- package/src/components/schedule-delete.tsx +0 -116
- package/src/components/schedule-list.tsx +0 -186
- package/src/components/schedule-runs.tsx +0 -151
- package/src/components/secret-bulk.tsx +0 -79
- package/src/components/secret-create.tsx +0 -49
- package/src/components/secret-delete.tsx +0 -41
- package/src/components/secret-list.tsx +0 -41
- package/src/components/watch.tsx +0 -155
- package/src/hooks/useApi.ts +0 -183
- package/src/positronic.ts +0 -40
- package/src/test/data/resources/config.json +0 -1
- package/src/test/data/resources/data/config.json +0 -1
- package/src/test/data/resources/data/logo.png +0 -2
- package/src/test/data/resources/docs/api.md +0 -3
- package/src/test/data/resources/docs/readme.md +0 -3
- package/src/test/data/resources/example.md +0 -3
- package/src/test/data/resources/file with spaces.txt +0 -1
- package/src/test/data/resources/readme.md +0 -3
- package/src/test/data/resources/test.txt +0 -1
- package/src/test/mock-api-client.ts +0 -145
- package/src/test/test-dev-server.ts +0 -1003
- 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
|
-
};
|