@allpepper/task-orchestrator-tui 1.0.0
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/README.md +78 -0
- package/package.json +54 -0
- package/src/tui/app.tsx +308 -0
- package/src/tui/components/column-filter-bar.tsx +52 -0
- package/src/tui/components/confirm-dialog.tsx +45 -0
- package/src/tui/components/dependency-list.tsx +115 -0
- package/src/tui/components/empty-state.tsx +28 -0
- package/src/tui/components/entity-table.tsx +120 -0
- package/src/tui/components/error-message.tsx +41 -0
- package/src/tui/components/feature-kanban-card.tsx +216 -0
- package/src/tui/components/footer.tsx +34 -0
- package/src/tui/components/form-dialog.tsx +338 -0
- package/src/tui/components/header.tsx +54 -0
- package/src/tui/components/index.ts +16 -0
- package/src/tui/components/kanban-board.tsx +335 -0
- package/src/tui/components/kanban-card.tsx +70 -0
- package/src/tui/components/kanban-column.tsx +173 -0
- package/src/tui/components/priority-badge.tsx +16 -0
- package/src/tui/components/section-list.tsx +96 -0
- package/src/tui/components/status-actions.tsx +87 -0
- package/src/tui/components/status-badge.tsx +22 -0
- package/src/tui/components/tree-view.tsx +295 -0
- package/src/tui/components/view-mode-chips.tsx +23 -0
- package/src/tui/index.tsx +33 -0
- package/src/tui/screens/dashboard.tsx +248 -0
- package/src/tui/screens/feature-detail.tsx +312 -0
- package/src/tui/screens/index.ts +6 -0
- package/src/tui/screens/kanban-view.tsx +251 -0
- package/src/tui/screens/project-detail.tsx +305 -0
- package/src/tui/screens/project-view.tsx +498 -0
- package/src/tui/screens/search.tsx +257 -0
- package/src/tui/screens/task-detail.tsx +294 -0
- package/src/ui/adapters/direct.ts +429 -0
- package/src/ui/adapters/index.ts +14 -0
- package/src/ui/adapters/types.ts +269 -0
- package/src/ui/context/adapter-context.tsx +31 -0
- package/src/ui/context/theme-context.tsx +43 -0
- package/src/ui/hooks/index.ts +20 -0
- package/src/ui/hooks/use-data.ts +919 -0
- package/src/ui/hooks/use-debounce.ts +37 -0
- package/src/ui/hooks/use-feature-kanban.ts +151 -0
- package/src/ui/hooks/use-kanban.ts +96 -0
- package/src/ui/hooks/use-navigation.tsx +94 -0
- package/src/ui/index.ts +73 -0
- package/src/ui/lib/colors.ts +79 -0
- package/src/ui/lib/format.ts +114 -0
- package/src/ui/lib/types.ts +157 -0
- package/src/ui/themes/dark.ts +63 -0
- package/src/ui/themes/light.ts +63 -0
- package/src/ui/themes/types.ts +71 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { StatusBadge } from './status-badge';
|
|
4
|
+
import { useTheme } from '../../ui/context/theme-context';
|
|
5
|
+
|
|
6
|
+
export interface StatusActionsProps {
|
|
7
|
+
currentStatus: string;
|
|
8
|
+
allowedTransitions: string[];
|
|
9
|
+
onTransition: (newStatus: string) => void;
|
|
10
|
+
isActive?: boolean;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function StatusActions({
|
|
15
|
+
currentStatus,
|
|
16
|
+
allowedTransitions,
|
|
17
|
+
onTransition,
|
|
18
|
+
isActive = true,
|
|
19
|
+
loading = false,
|
|
20
|
+
}: StatusActionsProps) {
|
|
21
|
+
const { theme } = useTheme();
|
|
22
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
23
|
+
|
|
24
|
+
useInput((input, key) => {
|
|
25
|
+
if (!isActive || loading || allowedTransitions.length === 0) return;
|
|
26
|
+
|
|
27
|
+
if (input === 'j' || key.downArrow) {
|
|
28
|
+
const nextIndex = (selectedIndex + 1) % allowedTransitions.length;
|
|
29
|
+
setSelectedIndex(nextIndex);
|
|
30
|
+
} else if (input === 'k' || key.upArrow) {
|
|
31
|
+
const prevIndex = (selectedIndex - 1 + allowedTransitions.length) % allowedTransitions.length;
|
|
32
|
+
setSelectedIndex(prevIndex);
|
|
33
|
+
} else if (key.return) {
|
|
34
|
+
const selectedStatus = allowedTransitions[selectedIndex];
|
|
35
|
+
if (selectedStatus !== undefined) {
|
|
36
|
+
onTransition(selectedStatus);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Box flexDirection="column">
|
|
43
|
+
{/* Current Status */}
|
|
44
|
+
<Box marginBottom={1}>
|
|
45
|
+
<Text>Status: </Text>
|
|
46
|
+
<StatusBadge status={currentStatus} />
|
|
47
|
+
</Box>
|
|
48
|
+
|
|
49
|
+
{/* Loading Indicator */}
|
|
50
|
+
{loading && (
|
|
51
|
+
<Box>
|
|
52
|
+
<Text dimColor>Loading...</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Transitions Section */}
|
|
57
|
+
{!loading && (
|
|
58
|
+
<>
|
|
59
|
+
{allowedTransitions.length === 0 ? (
|
|
60
|
+
<Box>
|
|
61
|
+
<Text dimColor>No transitions available</Text>
|
|
62
|
+
</Box>
|
|
63
|
+
) : (
|
|
64
|
+
<Box flexDirection="column">
|
|
65
|
+
<Box marginBottom={1}>
|
|
66
|
+
<Text>Change to:</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
{allowedTransitions.map((status, index) => {
|
|
69
|
+
const isSelected = index === selectedIndex;
|
|
70
|
+
return (
|
|
71
|
+
<Box key={status} marginLeft={2}>
|
|
72
|
+
<Text color={isSelected ? theme.colors.highlight : undefined}>
|
|
73
|
+
{isSelected ? '▎' : ' '}
|
|
74
|
+
</Text>
|
|
75
|
+
<Text bold={isSelected}>
|
|
76
|
+
{' '}{status}
|
|
77
|
+
</Text>
|
|
78
|
+
</Box>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</Box>
|
|
82
|
+
)}
|
|
83
|
+
</>
|
|
84
|
+
)}
|
|
85
|
+
</Box>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { getStatusColor } from '../../ui/lib/colors';
|
|
4
|
+
import { useTheme } from '../../ui/context/theme-context';
|
|
5
|
+
import { formatStatus } from '../../ui/lib/format';
|
|
6
|
+
|
|
7
|
+
interface StatusBadgeProps {
|
|
8
|
+
status: string;
|
|
9
|
+
isSelected?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StatusBadge({ status }: StatusBadgeProps) {
|
|
13
|
+
const { theme } = useTheme();
|
|
14
|
+
const color = getStatusColor(status, theme);
|
|
15
|
+
const formattedStatus = formatStatus(status);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Text color={color}>
|
|
19
|
+
● {formattedStatus}
|
|
20
|
+
</Text>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { StatusBadge } from './status-badge';
|
|
4
|
+
import { PriorityBadge } from './priority-badge';
|
|
5
|
+
import { useTheme } from '../../ui/context/theme-context';
|
|
6
|
+
import type { Feature, Task } from 'task-orchestrator-bun/src/domain/types';
|
|
7
|
+
|
|
8
|
+
export type TreeRow =
|
|
9
|
+
| { type: 'feature'; feature: Feature; taskCount: number; expanded: boolean; expandable?: boolean }
|
|
10
|
+
| { type: 'task'; task: Task; isLast: boolean; featureName?: string; depth?: number }
|
|
11
|
+
| { type: 'separator'; label: string }
|
|
12
|
+
| { type: 'group'; id: string; label: string; status: string; taskCount: number; expanded: boolean; depth?: number; expandable?: boolean; featureId?: string };
|
|
13
|
+
|
|
14
|
+
export interface TreeViewProps {
|
|
15
|
+
rows: TreeRow[];
|
|
16
|
+
selectedIndex: number;
|
|
17
|
+
onSelectedIndexChange: (index: number) => void;
|
|
18
|
+
onToggleFeature: (featureId: string) => void;
|
|
19
|
+
onToggleGroup?: (groupId: string) => void;
|
|
20
|
+
onSelectTask: (taskId: string) => void;
|
|
21
|
+
onSelectFeature?: (featureId: string) => void;
|
|
22
|
+
onBack?: () => void;
|
|
23
|
+
isActive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TreeView({
|
|
27
|
+
rows,
|
|
28
|
+
selectedIndex,
|
|
29
|
+
onSelectedIndexChange,
|
|
30
|
+
onToggleFeature,
|
|
31
|
+
onToggleGroup,
|
|
32
|
+
onSelectTask,
|
|
33
|
+
onSelectFeature,
|
|
34
|
+
onBack,
|
|
35
|
+
isActive = true,
|
|
36
|
+
}: TreeViewProps) {
|
|
37
|
+
const { theme } = useTheme();
|
|
38
|
+
const getRowDepth = (row: TreeRow): number => {
|
|
39
|
+
if (row.type === 'group') return row.depth ?? 0;
|
|
40
|
+
if (row.type === 'task') return row.depth ?? 1;
|
|
41
|
+
return 0;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isRowExpandable = (row: TreeRow): boolean => {
|
|
45
|
+
if (row.type === 'feature') {
|
|
46
|
+
return row.expandable ?? row.taskCount > 0;
|
|
47
|
+
}
|
|
48
|
+
if (row.type === 'group') {
|
|
49
|
+
return row.expandable ?? row.taskCount > 0;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const toggleRow = (row: TreeRow) => {
|
|
55
|
+
if (row.type === 'feature') {
|
|
56
|
+
onToggleFeature(row.feature.id);
|
|
57
|
+
} else if (row.type === 'group' && onToggleGroup) {
|
|
58
|
+
onToggleGroup(row.id);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const findFirstChildIndex = (index: number): number => {
|
|
63
|
+
const current = rows[index];
|
|
64
|
+
if (!current) return -1;
|
|
65
|
+
const currentDepth = getRowDepth(current);
|
|
66
|
+
|
|
67
|
+
for (let i = index + 1; i < rows.length; i++) {
|
|
68
|
+
const nextDepth = getRowDepth(rows[i]!);
|
|
69
|
+
if (nextDepth <= currentDepth) return -1;
|
|
70
|
+
if (nextDepth === currentDepth + 1) return i;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return -1;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const findParentIndex = (index: number): number => {
|
|
77
|
+
const current = rows[index];
|
|
78
|
+
if (!current) return -1;
|
|
79
|
+
const currentDepth = getRowDepth(current);
|
|
80
|
+
if (currentDepth <= 0) return -1;
|
|
81
|
+
|
|
82
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
83
|
+
if (getRowDepth(rows[i]!) === currentDepth - 1) {
|
|
84
|
+
return i;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return -1;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleNavigateRight = () => {
|
|
92
|
+
const row = rows[selectedIndex];
|
|
93
|
+
if (!row) return;
|
|
94
|
+
|
|
95
|
+
if (row.type === 'task') {
|
|
96
|
+
onSelectTask(row.task.id);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (row.type === 'separator') return;
|
|
101
|
+
|
|
102
|
+
const expandable = isRowExpandable(row);
|
|
103
|
+
if (expandable && !row.expanded) {
|
|
104
|
+
toggleRow(row);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (row.type === 'feature') {
|
|
109
|
+
onSelectFeature?.(row.feature.id);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const childIndex = findFirstChildIndex(selectedIndex);
|
|
114
|
+
if (childIndex >= 0) {
|
|
115
|
+
onSelectedIndexChange(childIndex);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleNavigateLeft = () => {
|
|
120
|
+
const row = rows[selectedIndex];
|
|
121
|
+
if (!row) return;
|
|
122
|
+
|
|
123
|
+
if (row.type === 'task') {
|
|
124
|
+
const parentIndex = findParentIndex(selectedIndex);
|
|
125
|
+
if (parentIndex >= 0) {
|
|
126
|
+
const parentRow = rows[parentIndex];
|
|
127
|
+
if (
|
|
128
|
+
parentRow &&
|
|
129
|
+
(parentRow.type === 'feature' || parentRow.type === 'group') &&
|
|
130
|
+
isRowExpandable(parentRow) &&
|
|
131
|
+
parentRow.expanded
|
|
132
|
+
) {
|
|
133
|
+
toggleRow(parentRow);
|
|
134
|
+
}
|
|
135
|
+
onSelectedIndexChange(parentIndex);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onBack?.();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if ((row.type === 'feature' || row.type === 'group') && isRowExpandable(row) && row.expanded) {
|
|
144
|
+
toggleRow(row);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const parentIndex = findParentIndex(selectedIndex);
|
|
149
|
+
if (parentIndex >= 0) {
|
|
150
|
+
onSelectedIndexChange(parentIndex);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
onBack?.();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
useInput((input, key) => {
|
|
158
|
+
if (!isActive) return;
|
|
159
|
+
if (rows.length === 0) return;
|
|
160
|
+
|
|
161
|
+
// Navigation: j/down or k/up
|
|
162
|
+
if (input === 'j' || key.downArrow) {
|
|
163
|
+
const nextIndex = (selectedIndex + 1) % rows.length;
|
|
164
|
+
onSelectedIndexChange(nextIndex);
|
|
165
|
+
} else if (input === 'k' || key.upArrow) {
|
|
166
|
+
const prevIndex = (selectedIndex - 1 + rows.length) % rows.length;
|
|
167
|
+
onSelectedIndexChange(prevIndex);
|
|
168
|
+
} else if (input === 'l' || key.rightArrow) {
|
|
169
|
+
handleNavigateRight();
|
|
170
|
+
} else if (input === 'h' || key.leftArrow) {
|
|
171
|
+
handleNavigateLeft();
|
|
172
|
+
} else if ((key.return || key.tab || input === ' ') && rows[selectedIndex]) {
|
|
173
|
+
// Selection: Enter or Space
|
|
174
|
+
const row = rows[selectedIndex];
|
|
175
|
+
if (row.type === 'feature') {
|
|
176
|
+
if (isRowExpandable(row)) {
|
|
177
|
+
onToggleFeature(row.feature.id);
|
|
178
|
+
}
|
|
179
|
+
} else if (row.type === 'group' && onToggleGroup) {
|
|
180
|
+
if (isRowExpandable(row)) {
|
|
181
|
+
onToggleGroup(row.id);
|
|
182
|
+
}
|
|
183
|
+
} else if (row.type === 'task') {
|
|
184
|
+
onSelectTask(row.task.id);
|
|
185
|
+
}
|
|
186
|
+
// Separator rows do nothing on selection
|
|
187
|
+
}
|
|
188
|
+
}, { isActive });
|
|
189
|
+
|
|
190
|
+
const renderRow = (row: TreeRow, index: number) => {
|
|
191
|
+
const isSelected = index === selectedIndex;
|
|
192
|
+
|
|
193
|
+
if (row.type === 'separator') {
|
|
194
|
+
return (
|
|
195
|
+
<Box key={`separator-${index}`} width="100%">
|
|
196
|
+
{/* Selection gutter */}
|
|
197
|
+
<Text color={isSelected ? theme.colors.highlight : undefined}>
|
|
198
|
+
{isSelected ? '▎' : ' '}
|
|
199
|
+
</Text>
|
|
200
|
+
<Text dimColor bold={isSelected}>
|
|
201
|
+
─────────── {row.label} ───────────
|
|
202
|
+
</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (row.type === 'group') {
|
|
208
|
+
const expandable = isRowExpandable(row);
|
|
209
|
+
const expandIcon = row.expanded ? '▼' : '▶';
|
|
210
|
+
const indent = row.depth ? ' '.repeat(row.depth) : '';
|
|
211
|
+
return (
|
|
212
|
+
<Box key={row.id} width="100%">
|
|
213
|
+
{/* Selection gutter */}
|
|
214
|
+
<Text color={isSelected ? theme.colors.highlight : undefined}>
|
|
215
|
+
{isSelected ? '▎' : ' '}
|
|
216
|
+
</Text>
|
|
217
|
+
<Text color={theme.colors.muted}>
|
|
218
|
+
{indent}
|
|
219
|
+
</Text>
|
|
220
|
+
{expandable && (
|
|
221
|
+
<Text color={theme.colors.muted}>
|
|
222
|
+
{expandIcon}{' '}
|
|
223
|
+
</Text>
|
|
224
|
+
)}
|
|
225
|
+
<Text bold={isSelected}>
|
|
226
|
+
{row.label}
|
|
227
|
+
{' '}
|
|
228
|
+
</Text>
|
|
229
|
+
<StatusBadge status={row.status} />
|
|
230
|
+
<Text bold={isSelected}>
|
|
231
|
+
{' '}
|
|
232
|
+
({row.taskCount})
|
|
233
|
+
</Text>
|
|
234
|
+
</Box>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (row.type === 'feature') {
|
|
239
|
+
const expandable = isRowExpandable(row);
|
|
240
|
+
const expandIcon = row.expanded ? '▼' : '▶';
|
|
241
|
+
return (
|
|
242
|
+
<Box key={row.feature.id} width="100%">
|
|
243
|
+
{/* Selection gutter */}
|
|
244
|
+
<Text color={isSelected ? theme.colors.highlight : undefined}>
|
|
245
|
+
{isSelected ? '▎' : ' '}
|
|
246
|
+
</Text>
|
|
247
|
+
{expandable && (
|
|
248
|
+
<Text color={theme.colors.muted}>
|
|
249
|
+
{expandIcon}{' '}
|
|
250
|
+
</Text>
|
|
251
|
+
)}
|
|
252
|
+
<Text bold={isSelected}>
|
|
253
|
+
{row.feature.name}
|
|
254
|
+
{' '}
|
|
255
|
+
</Text>
|
|
256
|
+
<StatusBadge status={row.feature.status} />
|
|
257
|
+
<Text bold={isSelected}>
|
|
258
|
+
{' '}
|
|
259
|
+
{row.taskCount} tasks
|
|
260
|
+
</Text>
|
|
261
|
+
</Box>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (row.type === 'task') {
|
|
266
|
+
const indent = row.depth ? ' '.repeat(row.depth) : '';
|
|
267
|
+
const treePrefix = row.isLast ? '└─ ' : '├─ ';
|
|
268
|
+
return (
|
|
269
|
+
<Box key={row.task.id} width="100%">
|
|
270
|
+
{/* Selection gutter */}
|
|
271
|
+
<Text color={isSelected ? theme.colors.highlight : undefined}>
|
|
272
|
+
{isSelected ? '▎' : ' '}
|
|
273
|
+
</Text>
|
|
274
|
+
<Text color={theme.colors.muted}>
|
|
275
|
+
{indent} {treePrefix}
|
|
276
|
+
</Text>
|
|
277
|
+
<Text bold={isSelected}>
|
|
278
|
+
{row.task.title}
|
|
279
|
+
{row.featureName !== undefined && (row.featureName ? ` [${row.featureName}]` : ' [unassigned]')}
|
|
280
|
+
{' '}
|
|
281
|
+
</Text>
|
|
282
|
+
<PriorityBadge priority={row.task.priority} />
|
|
283
|
+
</Box>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return null;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<Box flexDirection="column">
|
|
292
|
+
{rows.map((row, index) => renderRow(row, index))}
|
|
293
|
+
</Box>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
interface ViewModeChipsProps {
|
|
5
|
+
modes: Array<{ key: string; label: string }>;
|
|
6
|
+
activeMode: string;
|
|
7
|
+
onModeChange: (mode: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ViewModeChips({ modes, activeMode }: ViewModeChipsProps) {
|
|
11
|
+
return (
|
|
12
|
+
<Box flexDirection="row" gap={1}>
|
|
13
|
+
{modes.map((mode) => {
|
|
14
|
+
const isActive = mode.key === activeMode;
|
|
15
|
+
return (
|
|
16
|
+
<Text key={mode.key} inverse={isActive} dimColor={!isActive}>
|
|
17
|
+
{isActive ? `[${mode.label}]` : ` ${mode.label} `}
|
|
18
|
+
</Text>
|
|
19
|
+
);
|
|
20
|
+
})}
|
|
21
|
+
</Box>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Set default DB path BEFORE dynamically importing modules that read it
|
|
6
|
+
if (!process.env.DATABASE_PATH) {
|
|
7
|
+
process.env.DATABASE_PATH = join(homedir(), '.task-orchestrator', 'tasks.db');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
// Check if we're in a TTY environment
|
|
12
|
+
if (!process.stdin.isTTY) {
|
|
13
|
+
console.error('TUI requires an interactive terminal. Run directly in a terminal, not through a pipe.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Dynamic imports so DATABASE_PATH is set before db/client.ts loads
|
|
18
|
+
const [{ render }, React, { runMigrations }, { App }] = await Promise.all([
|
|
19
|
+
import('ink'),
|
|
20
|
+
import('react'),
|
|
21
|
+
import('task-orchestrator-bun/src/db/migrate'),
|
|
22
|
+
import('./app'),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// Run database migrations first
|
|
26
|
+
await runMigrations();
|
|
27
|
+
|
|
28
|
+
// Render the TUI
|
|
29
|
+
const { waitUntilExit } = render(<App />);
|
|
30
|
+
await waitUntilExit();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main().catch(console.error);
|