@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,116 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { Box, Text, useStdin, useApp } from 'ink';
|
|
3
|
-
import { ErrorComponent } from './error.js';
|
|
4
|
-
import { useApiDelete } from '../hooks/useApi.js';
|
|
5
|
-
|
|
6
|
-
interface ScheduleDeleteProps {
|
|
7
|
-
scheduleId: string;
|
|
8
|
-
force: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const ScheduleDelete = ({ scheduleId, force }: ScheduleDeleteProps) => {
|
|
12
|
-
const [confirmed, setConfirmed] = useState(force);
|
|
13
|
-
const [deleted, setDeleted] = useState(false);
|
|
14
|
-
const [input, setInput] = useState('');
|
|
15
|
-
const { stdin, setRawMode } = useStdin();
|
|
16
|
-
const { exit } = useApp();
|
|
17
|
-
const isDeleting = useRef(false);
|
|
18
|
-
|
|
19
|
-
const { execute: deleteSchedule, loading, error } = useApiDelete('schedule');
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (stdin && !confirmed && !deleted) {
|
|
23
|
-
setRawMode(true);
|
|
24
|
-
|
|
25
|
-
const handleData = (data: Buffer) => {
|
|
26
|
-
const char = data.toString();
|
|
27
|
-
|
|
28
|
-
if (char === '\r' || char === '\n') {
|
|
29
|
-
if (input.toLowerCase() === 'yes') {
|
|
30
|
-
setConfirmed(true);
|
|
31
|
-
} else {
|
|
32
|
-
exit();
|
|
33
|
-
}
|
|
34
|
-
} else if (char === '\u0003') { // Ctrl+C
|
|
35
|
-
exit();
|
|
36
|
-
} else if (char === '\u007F' || char === '\b') { // Backspace
|
|
37
|
-
setInput(prev => prev.slice(0, -1));
|
|
38
|
-
} else {
|
|
39
|
-
setInput(prev => prev + char);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
stdin.on('data', handleData);
|
|
44
|
-
|
|
45
|
-
return () => {
|
|
46
|
-
stdin.off('data', handleData);
|
|
47
|
-
setRawMode(false);
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}, [stdin, setRawMode, confirmed, deleted, input, exit]);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (confirmed && !deleted && !isDeleting.current) {
|
|
54
|
-
isDeleting.current = true;
|
|
55
|
-
deleteSchedule(`/brains/schedules/${scheduleId}`)
|
|
56
|
-
.then(() => {
|
|
57
|
-
setDeleted(true);
|
|
58
|
-
})
|
|
59
|
-
.catch(() => {
|
|
60
|
-
// Error is already handled by useApiDelete
|
|
61
|
-
})
|
|
62
|
-
.finally(() => {
|
|
63
|
-
isDeleting.current = false;
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}, [confirmed, deleted, scheduleId]);
|
|
67
|
-
|
|
68
|
-
if (error) {
|
|
69
|
-
// Check if it's a 404 error
|
|
70
|
-
if (error.details?.includes('404')) {
|
|
71
|
-
return (
|
|
72
|
-
<Box flexDirection="column">
|
|
73
|
-
<Text color="red">❌ Schedule not found</Text>
|
|
74
|
-
<Box marginTop={1} paddingLeft={2}>
|
|
75
|
-
<Text>No schedule found with ID: {scheduleId}</Text>
|
|
76
|
-
</Box>
|
|
77
|
-
</Box>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
return <ErrorComponent error={error} />;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (loading) {
|
|
84
|
-
return (
|
|
85
|
-
<Box>
|
|
86
|
-
<Text>🗑️ Deleting schedule...</Text>
|
|
87
|
-
</Box>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (deleted) {
|
|
92
|
-
return (
|
|
93
|
-
<Box flexDirection="column">
|
|
94
|
-
<Text color="green">✅ Schedule deleted successfully!</Text>
|
|
95
|
-
<Box marginTop={1} paddingLeft={2}>
|
|
96
|
-
<Text dimColor>Schedule ID: {scheduleId}</Text>
|
|
97
|
-
</Box>
|
|
98
|
-
</Box>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!confirmed) {
|
|
103
|
-
return (
|
|
104
|
-
<Box flexDirection="column">
|
|
105
|
-
<Text bold color="yellow">⚠️ Warning: This will permanently delete the schedule</Text>
|
|
106
|
-
<Box marginTop={1} marginBottom={1} paddingLeft={2} flexDirection="column">
|
|
107
|
-
<Text>Schedule ID: {scheduleId}</Text>
|
|
108
|
-
<Text dimColor>All future runs for this schedule will be cancelled.</Text>
|
|
109
|
-
</Box>
|
|
110
|
-
<Text>Type "yes" to confirm deletion: {input}</Text>
|
|
111
|
-
</Box>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return null;
|
|
116
|
-
};
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { ErrorComponent } from './error.js';
|
|
4
|
-
import { useApiGet } from '../hooks/useApi.js';
|
|
5
|
-
|
|
6
|
-
interface ScheduleListProps {
|
|
7
|
-
brainFilter?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface Schedule {
|
|
11
|
-
id: string;
|
|
12
|
-
brainName: string;
|
|
13
|
-
cronExpression: string;
|
|
14
|
-
enabled: boolean;
|
|
15
|
-
createdAt: number;
|
|
16
|
-
nextRunAt?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface SchedulesResponse {
|
|
20
|
-
schedules: Schedule[];
|
|
21
|
-
count: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Helper to format dates consistently
|
|
25
|
-
const formatDate = (timestamp: number): string => {
|
|
26
|
-
const date = new Date(timestamp);
|
|
27
|
-
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Helper to format relative time
|
|
31
|
-
const formatRelativeTime = (date: Date): string => {
|
|
32
|
-
const now = new Date();
|
|
33
|
-
const diffMs = date.getTime() - now.getTime();
|
|
34
|
-
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
35
|
-
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
36
|
-
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
37
|
-
|
|
38
|
-
if (diffMs < 0) {
|
|
39
|
-
return '(overdue)';
|
|
40
|
-
} else if (diffMins < 1) {
|
|
41
|
-
return '< 1 min';
|
|
42
|
-
} else if (diffMins < 60) {
|
|
43
|
-
return `${diffMins} min`;
|
|
44
|
-
} else if (diffHours < 24) {
|
|
45
|
-
return `${diffHours} hr`;
|
|
46
|
-
} else {
|
|
47
|
-
return `${diffDays} day${diffDays === 1 ? '' : 's'}`;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Helper to truncate text to fit column width
|
|
52
|
-
const truncate = (text: string, maxWidth: number): string => {
|
|
53
|
-
if (text.length <= maxWidth) return text;
|
|
54
|
-
return text.substring(0, maxWidth - 3) + '...';
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// Helper to pad text to column width
|
|
58
|
-
const padRight = (text: string, width: number): string => {
|
|
59
|
-
return text + ' '.repeat(Math.max(0, width - text.length));
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const ScheduleList = ({ brainFilter }: ScheduleListProps) => {
|
|
63
|
-
const { data, loading, error } = useApiGet<SchedulesResponse>('/brains/schedules');
|
|
64
|
-
|
|
65
|
-
if (error) {
|
|
66
|
-
return <ErrorComponent error={error} />;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (loading) {
|
|
70
|
-
return (
|
|
71
|
-
<Box>
|
|
72
|
-
<Text>📋 Loading schedules...</Text>
|
|
73
|
-
</Box>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!data || data.schedules.length === 0) {
|
|
78
|
-
return (
|
|
79
|
-
<Box flexDirection="column">
|
|
80
|
-
<Text>No schedules found.</Text>
|
|
81
|
-
<Box marginTop={1}>
|
|
82
|
-
<Text dimColor>
|
|
83
|
-
Tip: Create a schedule with "px schedule create <brain-name> <cron-expression>"
|
|
84
|
-
</Text>
|
|
85
|
-
</Box>
|
|
86
|
-
</Box>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Filter schedules if brain filter is provided
|
|
91
|
-
const filteredSchedules = brainFilter
|
|
92
|
-
? data.schedules.filter(s => s.brainName === brainFilter)
|
|
93
|
-
: data.schedules;
|
|
94
|
-
|
|
95
|
-
if (brainFilter && filteredSchedules.length === 0) {
|
|
96
|
-
return (
|
|
97
|
-
<Box flexDirection="column">
|
|
98
|
-
<Text>No schedules found for brain: {brainFilter}</Text>
|
|
99
|
-
<Box marginTop={1}>
|
|
100
|
-
<Text dimColor>
|
|
101
|
-
Tip: Create a schedule with "px schedule create {brainFilter} <cron-expression>"
|
|
102
|
-
</Text>
|
|
103
|
-
</Box>
|
|
104
|
-
</Box>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Sort schedules by creation date (newest first)
|
|
109
|
-
const sortedSchedules = [...filteredSchedules].sort((a, b) => b.createdAt - a.createdAt);
|
|
110
|
-
|
|
111
|
-
// Define column widths
|
|
112
|
-
const columns = {
|
|
113
|
-
brainName: { header: 'Brain Name', width: 20 },
|
|
114
|
-
schedule: { header: 'Schedule', width: 15 },
|
|
115
|
-
status: { header: 'Status', width: 10 },
|
|
116
|
-
nextRun: { header: 'Next Run', width: 12 },
|
|
117
|
-
created: { header: 'Created', width: 20 },
|
|
118
|
-
id: { header: 'ID', width: 36 },
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// Calculate total width for separator
|
|
122
|
-
const totalWidth = Object.values(columns).reduce((sum, col) => sum + col.width + 2, 0) - 2;
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<Box flexDirection="column" paddingTop={1} paddingBottom={1}>
|
|
126
|
-
<Text bold>
|
|
127
|
-
{brainFilter
|
|
128
|
-
? `Found ${filteredSchedules.length} schedule${filteredSchedules.length === 1 ? '' : 's'} for brain "${brainFilter}"`
|
|
129
|
-
: `Found ${data.count} schedule${data.count === 1 ? '' : 's'}`
|
|
130
|
-
}:
|
|
131
|
-
</Text>
|
|
132
|
-
|
|
133
|
-
<Box marginTop={1} flexDirection="column">
|
|
134
|
-
{/* Header row */}
|
|
135
|
-
<Box>
|
|
136
|
-
<Text bold color="cyan">{padRight(columns.brainName.header, columns.brainName.width)}</Text>
|
|
137
|
-
<Text> </Text>
|
|
138
|
-
<Text bold color="cyan">{padRight(columns.schedule.header, columns.schedule.width)}</Text>
|
|
139
|
-
<Text> </Text>
|
|
140
|
-
<Text bold color="cyan">{padRight(columns.status.header, columns.status.width)}</Text>
|
|
141
|
-
<Text> </Text>
|
|
142
|
-
<Text bold color="cyan">{padRight(columns.nextRun.header, columns.nextRun.width)}</Text>
|
|
143
|
-
<Text> </Text>
|
|
144
|
-
<Text bold color="cyan">{padRight(columns.created.header, columns.created.width)}</Text>
|
|
145
|
-
<Text> </Text>
|
|
146
|
-
<Text bold color="cyan">{padRight(columns.id.header, columns.id.width)}</Text>
|
|
147
|
-
</Box>
|
|
148
|
-
|
|
149
|
-
{/* Separator */}
|
|
150
|
-
<Box>
|
|
151
|
-
<Text dimColor>{'─'.repeat(totalWidth)}</Text>
|
|
152
|
-
</Box>
|
|
153
|
-
|
|
154
|
-
{/* Data rows */}
|
|
155
|
-
{sortedSchedules.map((schedule) => {
|
|
156
|
-
const nextRunDate = schedule.nextRunAt ? new Date(schedule.nextRunAt) : null;
|
|
157
|
-
const createdDate = new Date(schedule.createdAt);
|
|
158
|
-
const isOverdue = nextRunDate && nextRunDate.getTime() < Date.now();
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<Box key={schedule.id}>
|
|
162
|
-
<Text>{padRight(truncate(schedule.brainName, columns.brainName.width), columns.brainName.width)}</Text>
|
|
163
|
-
<Text> </Text>
|
|
164
|
-
<Text>{padRight(truncate(schedule.cronExpression, columns.schedule.width), columns.schedule.width)}</Text>
|
|
165
|
-
<Text> </Text>
|
|
166
|
-
<Text color={schedule.enabled ? 'green' : 'red'}>
|
|
167
|
-
{padRight(schedule.enabled ? 'Enabled' : 'Disabled', columns.status.width)}
|
|
168
|
-
</Text>
|
|
169
|
-
<Text> </Text>
|
|
170
|
-
<Text color={isOverdue ? 'red' : undefined}>
|
|
171
|
-
{padRight(
|
|
172
|
-
nextRunDate ? formatRelativeTime(nextRunDate) : 'N/A',
|
|
173
|
-
columns.nextRun.width
|
|
174
|
-
)}
|
|
175
|
-
</Text>
|
|
176
|
-
<Text> </Text>
|
|
177
|
-
<Text dimColor>{padRight(truncate(formatDate(schedule.createdAt), columns.created.width), columns.created.width)}</Text>
|
|
178
|
-
<Text> </Text>
|
|
179
|
-
<Text dimColor>{padRight(schedule.id, columns.id.width)}</Text>
|
|
180
|
-
</Box>
|
|
181
|
-
);
|
|
182
|
-
})}
|
|
183
|
-
</Box>
|
|
184
|
-
</Box>
|
|
185
|
-
);
|
|
186
|
-
};
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Text, Box } from 'ink';
|
|
3
|
-
import { useApiGet } from '../hooks/useApi.js';
|
|
4
|
-
import { ErrorComponent } from './error.js';
|
|
5
|
-
|
|
6
|
-
interface ScheduleRunsProps {
|
|
7
|
-
scheduleId?: string;
|
|
8
|
-
limit: number;
|
|
9
|
-
status?: 'triggered' | 'failed' | 'complete';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface ScheduledRun {
|
|
13
|
-
id: string;
|
|
14
|
-
scheduleId: string;
|
|
15
|
-
status: 'triggered' | 'failed';
|
|
16
|
-
ranAt: number;
|
|
17
|
-
brainRunId?: string;
|
|
18
|
-
error?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface ScheduleRunsResponse {
|
|
22
|
-
runs: ScheduledRun[];
|
|
23
|
-
count: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Helper to format dates
|
|
27
|
-
const formatDate = (timestamp: number): string => {
|
|
28
|
-
const date = new Date(timestamp);
|
|
29
|
-
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Helper to format relative time
|
|
33
|
-
const formatRelativeTime = (timestamp: number): string => {
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
const diffMs = now - timestamp;
|
|
36
|
-
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
37
|
-
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
38
|
-
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
39
|
-
|
|
40
|
-
if (diffMins < 1) {
|
|
41
|
-
return 'just now';
|
|
42
|
-
} else if (diffMins < 60) {
|
|
43
|
-
return `${diffMins} min ago`;
|
|
44
|
-
} else if (diffHours < 24) {
|
|
45
|
-
return `${diffHours} hr ago`;
|
|
46
|
-
} else {
|
|
47
|
-
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const ScheduleRuns = ({ scheduleId, limit, status }: ScheduleRunsProps) => {
|
|
52
|
-
// Build query params
|
|
53
|
-
const params = new URLSearchParams();
|
|
54
|
-
if (scheduleId) params.set('scheduleId', scheduleId);
|
|
55
|
-
params.set('limit', limit.toString());
|
|
56
|
-
|
|
57
|
-
const queryString = params.toString();
|
|
58
|
-
const url = `/brains/schedules/runs${queryString ? `?${queryString}` : ''}`;
|
|
59
|
-
|
|
60
|
-
const { data, loading, error } = useApiGet<ScheduleRunsResponse>(url);
|
|
61
|
-
|
|
62
|
-
if (error) {
|
|
63
|
-
return <ErrorComponent error={error} />;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (loading) {
|
|
67
|
-
return (
|
|
68
|
-
<Box>
|
|
69
|
-
<Text>📋 Loading scheduled runs...</Text>
|
|
70
|
-
</Box>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!data || data.runs.length === 0) {
|
|
75
|
-
return (
|
|
76
|
-
<Box flexDirection="column">
|
|
77
|
-
<Text>No scheduled runs found.</Text>
|
|
78
|
-
{scheduleId && (
|
|
79
|
-
<Box marginTop={1}>
|
|
80
|
-
<Text dimColor>No runs found for schedule ID: {scheduleId}</Text>
|
|
81
|
-
</Box>
|
|
82
|
-
)}
|
|
83
|
-
</Box>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Filter by status if provided (client-side filtering since API doesn't support it)
|
|
88
|
-
const filteredRuns = status
|
|
89
|
-
? data.runs.filter(run => run.status === status)
|
|
90
|
-
: data.runs;
|
|
91
|
-
|
|
92
|
-
if (status && filteredRuns.length === 0) {
|
|
93
|
-
return (
|
|
94
|
-
<Box flexDirection="column">
|
|
95
|
-
<Text>No runs found with status: {status}</Text>
|
|
96
|
-
</Box>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<Box flexDirection="column" paddingTop={1} paddingBottom={1}>
|
|
102
|
-
<Text bold>
|
|
103
|
-
Found {filteredRuns.length} scheduled run{filteredRuns.length === 1 ? '' : 's'}
|
|
104
|
-
{scheduleId && ` for schedule ${scheduleId}`}
|
|
105
|
-
{status && ` with status ${status}`}:
|
|
106
|
-
</Text>
|
|
107
|
-
|
|
108
|
-
<Box marginTop={1} flexDirection="column">
|
|
109
|
-
{/* Header row */}
|
|
110
|
-
<Box>
|
|
111
|
-
<Text bold color="cyan">{padRight('Run ID', 38)}</Text>
|
|
112
|
-
<Text> </Text>
|
|
113
|
-
<Text bold color="cyan">{padRight('Schedule ID', 38)}</Text>
|
|
114
|
-
<Text> </Text>
|
|
115
|
-
<Text bold color="cyan">{padRight('Status', 10)}</Text>
|
|
116
|
-
<Text> </Text>
|
|
117
|
-
<Text bold color="cyan">{padRight('Ran At', 20)}</Text>
|
|
118
|
-
<Text> </Text>
|
|
119
|
-
<Text bold color="cyan">{padRight('When', 12)}</Text>
|
|
120
|
-
</Box>
|
|
121
|
-
|
|
122
|
-
{/* Separator */}
|
|
123
|
-
<Box>
|
|
124
|
-
<Text dimColor>{'─'.repeat(130)}</Text>
|
|
125
|
-
</Box>
|
|
126
|
-
|
|
127
|
-
{/* Data rows */}
|
|
128
|
-
{filteredRuns.map((run) => (
|
|
129
|
-
<Box key={run.id}>
|
|
130
|
-
<Text>{padRight(run.id, 38)}</Text>
|
|
131
|
-
<Text> </Text>
|
|
132
|
-
<Text>{padRight(run.scheduleId, 38)}</Text>
|
|
133
|
-
<Text> </Text>
|
|
134
|
-
<Text color={run.status === 'triggered' ? 'green' : 'red'}>
|
|
135
|
-
{padRight(run.status, 10)}
|
|
136
|
-
</Text>
|
|
137
|
-
<Text> </Text>
|
|
138
|
-
<Text dimColor>{padRight(formatDate(run.ranAt), 20)}</Text>
|
|
139
|
-
<Text> </Text>
|
|
140
|
-
<Text dimColor>{padRight(formatRelativeTime(run.ranAt), 12)}</Text>
|
|
141
|
-
</Box>
|
|
142
|
-
))}
|
|
143
|
-
</Box>
|
|
144
|
-
</Box>
|
|
145
|
-
);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// Helper to pad text to column width
|
|
149
|
-
const padRight = (text: string, width: number): string => {
|
|
150
|
-
return text + ' '.repeat(Math.max(0, width - text.length));
|
|
151
|
-
};
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { Text, Box } from 'ink';
|
|
3
|
-
import type { PositronicDevServer } from '@positronic/spec';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
|
|
7
|
-
interface SecretBulkProps {
|
|
8
|
-
file?: string;
|
|
9
|
-
server?: PositronicDevServer;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const SecretBulk = ({ file = '.env', server }: SecretBulkProps) => {
|
|
13
|
-
const [loading, setLoading] = useState(true);
|
|
14
|
-
const [error, setError] = useState<string | null>(null);
|
|
15
|
-
const [completed, setCompleted] = useState(false);
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
const bulkUploadSecrets = async () => {
|
|
19
|
-
if (!server) {
|
|
20
|
-
setError('No server connection available');
|
|
21
|
-
setLoading(false);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
setLoading(true);
|
|
28
|
-
setError(null);
|
|
29
|
-
|
|
30
|
-
// Always read from .env file
|
|
31
|
-
const filePath = path.resolve(server.projectRootDir, file);
|
|
32
|
-
|
|
33
|
-
if (!fs.existsSync(filePath)) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`No .env file found at ${filePath}\n\n` +
|
|
36
|
-
'To use this command, create a .env file in your project root with your secrets:\n' +
|
|
37
|
-
' ANTHROPIC_API_KEY=sk-ant-...\n' +
|
|
38
|
-
' DATABASE_URL=postgres://...\n' +
|
|
39
|
-
' REDIS_URL=redis://...'
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Pass the file path to the backend
|
|
44
|
-
await server.bulkSecrets(filePath);
|
|
45
|
-
setCompleted(true);
|
|
46
|
-
setLoading(false);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
setError(error instanceof Error ? error.message : 'Failed to bulk upload secrets');
|
|
49
|
-
setLoading(false);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
bulkUploadSecrets();
|
|
54
|
-
}, [file, server]);
|
|
55
|
-
|
|
56
|
-
if (error) {
|
|
57
|
-
return (
|
|
58
|
-
<Box flexDirection="column">
|
|
59
|
-
<Text color="red">❌ Error: {error}</Text>
|
|
60
|
-
</Box>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (loading) {
|
|
65
|
-
return (
|
|
66
|
-
<Box>
|
|
67
|
-
<Text>🔄 Uploading secrets{file ? ` from ${file}` : ' from stdin'}...</Text>
|
|
68
|
-
</Box>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (completed) {
|
|
73
|
-
// The bulk command itself outputs success message to console
|
|
74
|
-
// We just need to ensure the component renders something to prevent React errors
|
|
75
|
-
return <Box />;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return null;
|
|
79
|
-
};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { useApp } from 'ink';
|
|
3
|
-
import type { PositronicDevServer } from '@positronic/spec';
|
|
4
|
-
|
|
5
|
-
interface SecretCreateProps {
|
|
6
|
-
name: string;
|
|
7
|
-
value?: string;
|
|
8
|
-
server?: PositronicDevServer;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const SecretCreate = ({ name, value: providedValue, server }: SecretCreateProps) => {
|
|
12
|
-
const { exit } = useApp();
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
const createSecret = async () => {
|
|
16
|
-
if (!server) {
|
|
17
|
-
console.error('No project found. Please run this command from within a Positronic project directory.');
|
|
18
|
-
exit();
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!server.setSecret) {
|
|
23
|
-
console.error('Secret management not supported for this backend');
|
|
24
|
-
exit();
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// If no value provided, backend will prompt for it
|
|
29
|
-
if (!providedValue) {
|
|
30
|
-
console.error('Please provide a value using --value flag');
|
|
31
|
-
exit();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
await server.setSecret(name, providedValue);
|
|
37
|
-
exit();
|
|
38
|
-
} catch (err) {
|
|
39
|
-
// Error was already printed by backend
|
|
40
|
-
exit();
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
createSecret();
|
|
45
|
-
}, [name, providedValue, server, exit]);
|
|
46
|
-
|
|
47
|
-
// This won't be shown because backend output is printed directly
|
|
48
|
-
return null;
|
|
49
|
-
};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { useApp } from 'ink';
|
|
3
|
-
import type { PositronicDevServer } from '@positronic/spec';
|
|
4
|
-
|
|
5
|
-
interface SecretDeleteProps {
|
|
6
|
-
name: string;
|
|
7
|
-
server?: PositronicDevServer;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const SecretDelete = ({ name, server }: SecretDeleteProps) => {
|
|
11
|
-
const { exit } = useApp();
|
|
12
|
-
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const deleteSecret = async () => {
|
|
15
|
-
if (!server) {
|
|
16
|
-
console.error('No project found. Please run this command from within a Positronic project directory.');
|
|
17
|
-
exit();
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!server.deleteSecret) {
|
|
22
|
-
console.error('Secret management not supported for this backend');
|
|
23
|
-
exit();
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
await server.deleteSecret(name);
|
|
29
|
-
exit();
|
|
30
|
-
} catch (err) {
|
|
31
|
-
// Error was already printed by backend
|
|
32
|
-
exit();
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
deleteSecret();
|
|
37
|
-
}, [name, server, exit]);
|
|
38
|
-
|
|
39
|
-
// This won't be shown because backend output is printed directly
|
|
40
|
-
return null;
|
|
41
|
-
};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { useApp } from 'ink';
|
|
3
|
-
import type { PositronicDevServer } from '@positronic/spec';
|
|
4
|
-
|
|
5
|
-
interface SecretListProps {
|
|
6
|
-
server?: PositronicDevServer;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const SecretList = ({ server }: SecretListProps) => {
|
|
10
|
-
const { exit } = useApp();
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const loadSecrets = async () => {
|
|
14
|
-
if (!server) {
|
|
15
|
-
console.error('No project found. Please run this command from within a Positronic project directory.');
|
|
16
|
-
exit();
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (!server.listSecrets) {
|
|
21
|
-
console.error('Secret management not supported for this backend');
|
|
22
|
-
exit();
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
// listSecrets will print output directly via stdio: 'inherit'
|
|
28
|
-
await server.listSecrets();
|
|
29
|
-
exit();
|
|
30
|
-
} catch (err) {
|
|
31
|
-
// Error was already printed by backend
|
|
32
|
-
exit();
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
loadSecrets();
|
|
37
|
-
}, [server, exit]);
|
|
38
|
-
|
|
39
|
-
// This won't be shown because backend output is printed directly
|
|
40
|
-
return null;
|
|
41
|
-
};
|