@colmbus72/yeehaw 0.1.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/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +414 -0
- package/dist/components/BarnHeader.d.ts +6 -0
- package/dist/components/BarnHeader.js +21 -0
- package/dist/components/BottomBar.d.ts +16 -0
- package/dist/components/BottomBar.js +7 -0
- package/dist/components/Header.d.ts +8 -0
- package/dist/components/Header.js +83 -0
- package/dist/components/HelpOverlay.d.ts +7 -0
- package/dist/components/HelpOverlay.js +17 -0
- package/dist/components/List.d.ts +17 -0
- package/dist/components/List.js +53 -0
- package/dist/components/Markdown.d.ts +8 -0
- package/dist/components/Markdown.js +23 -0
- package/dist/components/Panel.d.ts +10 -0
- package/dist/components/Panel.js +5 -0
- package/dist/components/PathInput.d.ts +9 -0
- package/dist/components/PathInput.js +141 -0
- package/dist/components/ScrollableMarkdown.d.ts +11 -0
- package/dist/components/ScrollableMarkdown.js +56 -0
- package/dist/components/StatusBar.d.ts +5 -0
- package/dist/components/StatusBar.js +20 -0
- package/dist/components/TextArea.d.ts +17 -0
- package/dist/components/TextArea.js +140 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +5 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/useConfig.d.ts +11 -0
- package/dist/hooks/useConfig.js +36 -0
- package/dist/hooks/useRemoteYeehaw.d.ts +13 -0
- package/dist/hooks/useRemoteYeehaw.js +49 -0
- package/dist/hooks/useSessions.d.ts +11 -0
- package/dist/hooks/useSessions.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +34 -0
- package/dist/lib/config.d.ts +27 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/detection.d.ts +16 -0
- package/dist/lib/detection.js +41 -0
- package/dist/lib/editor.d.ts +5 -0
- package/dist/lib/editor.js +35 -0
- package/dist/lib/errors.d.ts +28 -0
- package/dist/lib/errors.js +48 -0
- package/dist/lib/git.d.ts +11 -0
- package/dist/lib/git.js +73 -0
- package/dist/lib/github.d.ts +43 -0
- package/dist/lib/github.js +111 -0
- package/dist/lib/hotkeys.d.ts +27 -0
- package/dist/lib/hotkeys.js +92 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/livestock.d.ts +51 -0
- package/dist/lib/livestock.js +233 -0
- package/dist/lib/mcp-validation.d.ts +33 -0
- package/dist/lib/mcp-validation.js +62 -0
- package/dist/lib/paths.d.ts +8 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/shell.d.ts +34 -0
- package/dist/lib/shell.js +61 -0
- package/dist/lib/ssh.d.ts +15 -0
- package/dist/lib/ssh.js +77 -0
- package/dist/lib/tmux-config.d.ts +3 -0
- package/dist/lib/tmux-config.js +42 -0
- package/dist/lib/tmux.d.ts +32 -0
- package/dist/lib/tmux.js +397 -0
- package/dist/mcp-server.d.ts +23 -0
- package/dist/mcp-server.js +825 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +2 -0
- package/dist/views/BarnContext.d.ts +22 -0
- package/dist/views/BarnContext.js +252 -0
- package/dist/views/GlobalDashboard.d.ts +16 -0
- package/dist/views/GlobalDashboard.js +253 -0
- package/dist/views/Home.d.ts +11 -0
- package/dist/views/Home.js +27 -0
- package/dist/views/IssuesView.d.ts +7 -0
- package/dist/views/IssuesView.js +157 -0
- package/dist/views/LivestockDetailView.d.ts +11 -0
- package/dist/views/LivestockDetailView.js +140 -0
- package/dist/views/LogsView.d.ts +8 -0
- package/dist/views/LogsView.js +84 -0
- package/dist/views/NightSkyView.d.ts +5 -0
- package/dist/views/NightSkyView.js +441 -0
- package/dist/views/ProjectContext.d.ts +18 -0
- package/dist/views/ProjectContext.js +333 -0
- package/dist/views/Projects.d.ts +8 -0
- package/dist/views/Projects.js +20 -0
- package/dist/views/WikiView.d.ts +8 -0
- package/dist/views/WikiView.js +138 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/package.json +65 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { Header } from '../components/Header.js';
|
|
6
|
+
import { Panel } from '../components/Panel.js';
|
|
7
|
+
import { List } from '../components/List.js';
|
|
8
|
+
import { PathInput } from '../components/PathInput.js';
|
|
9
|
+
import { getWindowStatus } from '../lib/tmux.js';
|
|
10
|
+
import { detectGitInfo, detectRemoteGitInfo } from '../lib/git.js';
|
|
11
|
+
import { detectLivestockConfig } from '../lib/livestock.js';
|
|
12
|
+
export function ProjectContext({ project, barns, windows, onBack, onNewClaude, onSelectWindow, onSelectLivestock, onOpenLivestockSession, onUpdateProject, onDeleteProject, onOpenWiki, onOpenIssues, }) {
|
|
13
|
+
const [focusedPanel, setFocusedPanel] = useState('livestock');
|
|
14
|
+
const [mode, setMode] = useState('normal');
|
|
15
|
+
// Edit form state
|
|
16
|
+
const [editName, setEditName] = useState(project.name);
|
|
17
|
+
const [editPath, setEditPath] = useState(project.path);
|
|
18
|
+
const [editSummary, setEditSummary] = useState(project.summary || '');
|
|
19
|
+
const [editColor, setEditColor] = useState(project.color || '');
|
|
20
|
+
// New livestock form state
|
|
21
|
+
const [newLivestockName, setNewLivestockName] = useState('');
|
|
22
|
+
const [newLivestockPath, setNewLivestockPath] = useState('');
|
|
23
|
+
const [newLivestockLogPath, setNewLivestockLogPath] = useState('');
|
|
24
|
+
const [newLivestockEnvPath, setNewLivestockEnvPath] = useState('');
|
|
25
|
+
const [selectedBarn, setSelectedBarn] = useState(null); // null = local
|
|
26
|
+
// Track selected livestock index for deletion
|
|
27
|
+
const [selectedLivestockIndex, setSelectedLivestockIndex] = useState(0);
|
|
28
|
+
// Delete project confirmation
|
|
29
|
+
const [deleteConfirmInput, setDeleteConfirmInput] = useState('');
|
|
30
|
+
// Delete livestock confirmation
|
|
31
|
+
const [deleteLivestockTarget, setDeleteLivestockTarget] = useState(null);
|
|
32
|
+
// Git detection for new livestock
|
|
33
|
+
const [detectedGit, setDetectedGit] = useState(null);
|
|
34
|
+
// Framework detection for config suggestions
|
|
35
|
+
const [detectedConfig, setDetectedConfig] = useState(null);
|
|
36
|
+
// Filter windows to this project (by name prefix)
|
|
37
|
+
const projectWindows = windows.filter((w) => w.index > 0 && w.name.startsWith(project.name));
|
|
38
|
+
// Create a map from display number (1-9) to window for quick access
|
|
39
|
+
const windowsByDisplayNum = new Map();
|
|
40
|
+
projectWindows.forEach((w, i) => {
|
|
41
|
+
if (i < 9) {
|
|
42
|
+
windowsByDisplayNum.set(i + 1, w);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const startEdit = () => {
|
|
46
|
+
setEditName(project.name);
|
|
47
|
+
setEditPath(project.path);
|
|
48
|
+
setEditSummary(project.summary || '');
|
|
49
|
+
setEditColor(project.color || '');
|
|
50
|
+
setMode('edit-name');
|
|
51
|
+
};
|
|
52
|
+
const cancelEdit = () => {
|
|
53
|
+
setMode('normal');
|
|
54
|
+
};
|
|
55
|
+
const saveAndNext = (nextMode) => {
|
|
56
|
+
if (nextMode === 'done') {
|
|
57
|
+
// Save the project
|
|
58
|
+
onUpdateProject({
|
|
59
|
+
...project,
|
|
60
|
+
name: editName,
|
|
61
|
+
path: editPath,
|
|
62
|
+
summary: editSummary || undefined,
|
|
63
|
+
color: editColor || undefined,
|
|
64
|
+
});
|
|
65
|
+
setMode('normal');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
setMode(nextMode);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const startAddLivestock = () => {
|
|
72
|
+
setNewLivestockName('');
|
|
73
|
+
setNewLivestockPath('');
|
|
74
|
+
setNewLivestockLogPath('');
|
|
75
|
+
setNewLivestockEnvPath('');
|
|
76
|
+
setSelectedBarn(null);
|
|
77
|
+
setDetectedGit(null);
|
|
78
|
+
setDetectedConfig(null);
|
|
79
|
+
setMode('add-livestock-name');
|
|
80
|
+
};
|
|
81
|
+
const handlePathSubmit = async () => {
|
|
82
|
+
// Detect git info from the path (local or remote)
|
|
83
|
+
if (newLivestockPath) {
|
|
84
|
+
const gitInfo = selectedBarn
|
|
85
|
+
? detectRemoteGitInfo(newLivestockPath, selectedBarn)
|
|
86
|
+
: detectGitInfo(newLivestockPath);
|
|
87
|
+
setDetectedGit(gitInfo);
|
|
88
|
+
// Detect framework and pre-fill config suggestions
|
|
89
|
+
const config = await detectLivestockConfig(newLivestockPath, selectedBarn || undefined);
|
|
90
|
+
setDetectedConfig(config);
|
|
91
|
+
if (config.log_path) {
|
|
92
|
+
setNewLivestockLogPath(config.log_path);
|
|
93
|
+
}
|
|
94
|
+
if (config.env_path) {
|
|
95
|
+
setNewLivestockEnvPath(config.env_path);
|
|
96
|
+
}
|
|
97
|
+
// Continue to optional config fields
|
|
98
|
+
setMode('add-livestock-log-path');
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const saveLivestock = () => {
|
|
102
|
+
const newLivestock = {
|
|
103
|
+
name: newLivestockName,
|
|
104
|
+
path: newLivestockPath,
|
|
105
|
+
barn: selectedBarn?.name,
|
|
106
|
+
repo: detectedGit?.remoteUrl,
|
|
107
|
+
branch: detectedGit?.branch,
|
|
108
|
+
log_path: newLivestockLogPath || undefined,
|
|
109
|
+
env_path: newLivestockEnvPath || undefined,
|
|
110
|
+
};
|
|
111
|
+
const updatedLivestock = [...(project.livestock || [])];
|
|
112
|
+
// Replace if same name exists, otherwise add
|
|
113
|
+
const existingIdx = updatedLivestock.findIndex((l) => l.name === newLivestock.name);
|
|
114
|
+
if (existingIdx >= 0) {
|
|
115
|
+
updatedLivestock[existingIdx] = newLivestock;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
updatedLivestock.push(newLivestock);
|
|
119
|
+
}
|
|
120
|
+
onUpdateProject({
|
|
121
|
+
...project,
|
|
122
|
+
livestock: updatedLivestock,
|
|
123
|
+
});
|
|
124
|
+
setMode('normal');
|
|
125
|
+
};
|
|
126
|
+
useInput((input, key) => {
|
|
127
|
+
// Handle escape
|
|
128
|
+
if (key.escape) {
|
|
129
|
+
if (mode !== 'normal') {
|
|
130
|
+
cancelEdit();
|
|
131
|
+
setDeleteLivestockTarget(null);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
onBack();
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Handle delete livestock confirmation
|
|
139
|
+
if (mode === 'delete-livestock-confirm' && deleteLivestockTarget) {
|
|
140
|
+
if (input === 'y') {
|
|
141
|
+
// Perform the actual delete
|
|
142
|
+
const updatedLivestock = (project.livestock || []).filter((l) => l.name !== deleteLivestockTarget.name);
|
|
143
|
+
onUpdateProject({
|
|
144
|
+
...project,
|
|
145
|
+
livestock: updatedLivestock,
|
|
146
|
+
});
|
|
147
|
+
// Adjust selection if needed
|
|
148
|
+
if (selectedLivestockIndex >= updatedLivestock.length && updatedLivestock.length > 0) {
|
|
149
|
+
setSelectedLivestockIndex(updatedLivestock.length - 1);
|
|
150
|
+
}
|
|
151
|
+
setDeleteLivestockTarget(null);
|
|
152
|
+
setMode('normal');
|
|
153
|
+
}
|
|
154
|
+
else if (input === 'n') {
|
|
155
|
+
setDeleteLivestockTarget(null);
|
|
156
|
+
setMode('normal');
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (mode !== 'normal')
|
|
161
|
+
return;
|
|
162
|
+
if (input === 'q') {
|
|
163
|
+
onBack();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (key.tab) {
|
|
167
|
+
setFocusedPanel((p) => (p === 'livestock' ? 'sessions' : 'livestock'));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (input === 'c') {
|
|
171
|
+
onNewClaude();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (input === 'e') {
|
|
175
|
+
startEdit();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (input === 'w') {
|
|
179
|
+
onOpenWiki();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (input === 'i') {
|
|
183
|
+
onOpenIssues();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (input === 'D') {
|
|
187
|
+
setDeleteConfirmInput('');
|
|
188
|
+
setMode('delete-project-confirm');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Livestock management (when livestock panel focused)
|
|
192
|
+
if (focusedPanel === 'livestock') {
|
|
193
|
+
if (input === 'n') {
|
|
194
|
+
startAddLivestock();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (input === 's') {
|
|
198
|
+
// Open shell session for selected livestock
|
|
199
|
+
const livestock = project.livestock || [];
|
|
200
|
+
if (livestock.length > 0 && selectedLivestockIndex < livestock.length) {
|
|
201
|
+
const selected = livestock[selectedLivestockIndex];
|
|
202
|
+
const barn = selected.barn ? barns.find((b) => b.name === selected.barn) || null : null;
|
|
203
|
+
onOpenLivestockSession(selected, barn);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (input === 'd') {
|
|
208
|
+
const livestock = project.livestock || [];
|
|
209
|
+
if (livestock.length > 0 && selectedLivestockIndex < livestock.length) {
|
|
210
|
+
setDeleteLivestockTarget(livestock[selectedLivestockIndex]);
|
|
211
|
+
setMode('delete-livestock-confirm');
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Number hotkeys 1-9 for quick window switching
|
|
217
|
+
const num = parseInt(input, 10);
|
|
218
|
+
if (num >= 1 && num <= 9) {
|
|
219
|
+
const window = windowsByDisplayNum.get(num);
|
|
220
|
+
if (window) {
|
|
221
|
+
onSelectWindow(window);
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// Edit mode screens
|
|
227
|
+
if (mode === 'edit-name') {
|
|
228
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Editing project", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Project" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: editName, onChange: setEditName, onSubmit: () => saveAndNext('edit-path') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
|
|
229
|
+
}
|
|
230
|
+
if (mode === 'edit-path') {
|
|
231
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Editing project", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Edit Project: ", editName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: editPath, onChange: setEditPath, onSubmit: () => saveAndNext('edit-summary') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: autocomplete, Enter: next field, Esc: cancel" }) })] })] }));
|
|
232
|
+
}
|
|
233
|
+
if (mode === 'edit-summary') {
|
|
234
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Editing project", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Edit Project: ", editName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Summary: " }), _jsx(TextInput, { value: editSummary, onChange: setEditSummary, onSubmit: () => saveAndNext('edit-color'), placeholder: "Short description..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel (leave blank to skip)" }) })] })] }));
|
|
235
|
+
}
|
|
236
|
+
if (mode === 'edit-color') {
|
|
237
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Editing project", color: editColor || project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Edit Project: ", editName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Color (hex): " }), _jsx(TextInput, { value: editColor, onChange: setEditColor, onSubmit: () => saveAndNext('done'), placeholder: "#ff6b6b" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save project, Esc: cancel (leave blank for default)" }) })] })] }));
|
|
238
|
+
}
|
|
239
|
+
// Add livestock flow: name → barn → path (with auto-detect)
|
|
240
|
+
if (mode === 'add-livestock-name') {
|
|
241
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Adding livestock", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "green", children: "Add Livestock" }), _jsx(Text, { dimColor: true, children: "Livestock are deployed instances of your app" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newLivestockName, onChange: setNewLivestockName, onSubmit: () => {
|
|
242
|
+
if (newLivestockName.trim()) {
|
|
243
|
+
setMode('add-livestock-barn');
|
|
244
|
+
}
|
|
245
|
+
}, placeholder: "local, dev, production..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Esc: cancel" }) })] })] }));
|
|
246
|
+
}
|
|
247
|
+
if (mode === 'add-livestock-barn') {
|
|
248
|
+
// Build barn options including "Local" option
|
|
249
|
+
const barnOptions = [
|
|
250
|
+
{ id: '__local__', label: 'Local (this machine)', status: 'active' },
|
|
251
|
+
...barns.map((b) => ({
|
|
252
|
+
id: b.name,
|
|
253
|
+
label: b.name,
|
|
254
|
+
status: 'active',
|
|
255
|
+
meta: `${b.user}@${b.host}`,
|
|
256
|
+
})),
|
|
257
|
+
];
|
|
258
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Adding livestock", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Livestock: ", newLivestockName] }), _jsx(Text, { dimColor: true, children: "Where is this livestock deployed?" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(List, { items: barnOptions, focused: true, onSelect: (item) => {
|
|
259
|
+
if (item.id === '__local__') {
|
|
260
|
+
setSelectedBarn(null);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const barn = barns.find((b) => b.name === item.id);
|
|
264
|
+
setSelectedBarn(barn || null);
|
|
265
|
+
}
|
|
266
|
+
setMode('add-livestock-path');
|
|
267
|
+
} }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: select, Esc: cancel" }) })] })] }));
|
|
268
|
+
}
|
|
269
|
+
if (mode === 'add-livestock-path') {
|
|
270
|
+
const locationLabel = selectedBarn
|
|
271
|
+
? `on ${selectedBarn.name} (${selectedBarn.host})`
|
|
272
|
+
: 'local';
|
|
273
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Adding livestock", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Livestock: ", newLivestockName] }), _jsxs(Text, { dimColor: true, children: ["Location: ", locationLabel] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: newLivestockPath, onChange: setNewLivestockPath, onSubmit: handlePathSubmit, barn: selectedBarn || undefined })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: autocomplete, Enter: next, Esc: cancel" }) })] })] }));
|
|
274
|
+
}
|
|
275
|
+
if (mode === 'add-livestock-log-path') {
|
|
276
|
+
const locationLabel = selectedBarn
|
|
277
|
+
? `on ${selectedBarn.name} (${selectedBarn.host})`
|
|
278
|
+
: 'local';
|
|
279
|
+
const frameworkLabel = detectedConfig?.framework && detectedConfig.framework !== 'unknown'
|
|
280
|
+
? ` (${detectedConfig.framework} detected)`
|
|
281
|
+
: '';
|
|
282
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Adding livestock", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Livestock: ", newLivestockName] }), _jsxs(Text, { dimColor: true, children: ["Location: ", locationLabel, " \u2022 Path: ", newLivestockPath] }), frameworkLabel && _jsx(Text, { color: "cyan", children: frameworkLabel }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Log path (optional): " }), _jsx(TextInput, { value: newLivestockLogPath, onChange: setNewLivestockLogPath, onSubmit: () => setMode('add-livestock-env-path'), placeholder: "storage/logs/ or logs/" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Relative to livestock path. Enter: next (leave blank to skip), Esc: cancel" }) })] })] }));
|
|
283
|
+
}
|
|
284
|
+
if (mode === 'add-livestock-env-path') {
|
|
285
|
+
const locationLabel = selectedBarn
|
|
286
|
+
? `on ${selectedBarn.name} (${selectedBarn.host})`
|
|
287
|
+
: 'local';
|
|
288
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Adding livestock", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Livestock: ", newLivestockName] }), _jsxs(Text, { dimColor: true, children: ["Location: ", locationLabel, " \u2022 Path: ", newLivestockPath] }), newLivestockLogPath && _jsxs(Text, { dimColor: true, children: ["Log path: ", newLivestockLogPath] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Env path (optional): " }), _jsx(TextInput, { value: newLivestockEnvPath, onChange: setNewLivestockEnvPath, onSubmit: saveLivestock, placeholder: ".env" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Relative to livestock path. Enter: save livestock, Esc: cancel" }) })] })] }));
|
|
289
|
+
}
|
|
290
|
+
if (mode === 'delete-project-confirm') {
|
|
291
|
+
const handleDeleteConfirm = () => {
|
|
292
|
+
if (deleteConfirmInput === project.name) {
|
|
293
|
+
onDeleteProject(project.name);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "DELETE PROJECT", color: "#ff0000" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "\u26A0\uFE0F Delete Project" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "This will permanently delete the project configuration." }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Type the project name to confirm: " }), _jsx(Text, { bold: true, color: "yellow", children: project.name })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Confirm: " }), _jsx(TextInput, { value: deleteConfirmInput, onChange: setDeleteConfirmInput, onSubmit: handleDeleteConfirm })] }), deleteConfirmInput && deleteConfirmInput !== project.name && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: "Name does not match" }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: delete (if name matches), Esc: cancel" }) })] })] }));
|
|
297
|
+
}
|
|
298
|
+
// Delete livestock confirmation
|
|
299
|
+
if (mode === 'delete-livestock-confirm' && deleteLivestockTarget) {
|
|
300
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Remove livestock", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Remove Livestock" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Remove \"", deleteLivestockTarget.name, "\" from this project?"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Path: ", deleteLivestockTarget.path] }) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: "red", bold: true, children: "[y] Yes, remove" }), _jsx(Text, { dimColor: true, children: "[n/Esc] Cancel" })] })] })] }));
|
|
301
|
+
}
|
|
302
|
+
// Get livestock with resolved barn info
|
|
303
|
+
const livestockWithBarns = (project.livestock || []).map((livestock) => ({
|
|
304
|
+
livestock,
|
|
305
|
+
barn: livestock.barn ? barns.find((b) => b.name === livestock.barn) || null : null,
|
|
306
|
+
}));
|
|
307
|
+
const livestockItems = livestockWithBarns.map(({ livestock, barn }) => ({
|
|
308
|
+
id: livestock.name,
|
|
309
|
+
label: barn ? `${livestock.name} (${barn.host})` : `${livestock.name} (local)`,
|
|
310
|
+
status: 'active', // TODO: actual health check
|
|
311
|
+
meta: livestock.path,
|
|
312
|
+
}));
|
|
313
|
+
// Use display numbers (1-9) instead of tmux window index
|
|
314
|
+
const sessionItems = projectWindows.map((w, i) => ({
|
|
315
|
+
id: String(w.index),
|
|
316
|
+
label: `[${i + 1}] ${w.name.replace(`${project.name}-`, '')}`,
|
|
317
|
+
status: w.active ? 'active' : 'inactive',
|
|
318
|
+
meta: getWindowStatus(w),
|
|
319
|
+
}));
|
|
320
|
+
// Panel-specific hints (page-level hotkeys like c/w/i are in BottomBar)
|
|
321
|
+
const livestockHints = '[s] shell [n] new [d] delete';
|
|
322
|
+
const sessionHints = '1-9 switch';
|
|
323
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: project.path, summary: project.summary, color: project.color }), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Livestock", focused: focusedPanel === 'livestock', width: "50%", hints: livestockHints, children: livestockItems.length > 0 ? (_jsx(List, { items: livestockItems, focused: focusedPanel === 'livestock', selectedIndex: selectedLivestockIndex, onSelectionChange: setSelectedLivestockIndex, onSelect: (item) => {
|
|
324
|
+
const found = livestockWithBarns.find((l) => l.livestock.name === item.id);
|
|
325
|
+
if (found) {
|
|
326
|
+
onSelectLivestock(found.livestock, found.barn);
|
|
327
|
+
}
|
|
328
|
+
} })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No livestock configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Livestock are your deployed app instances" })] })) }), _jsx(Panel, { title: "Sessions", focused: focusedPanel === 'sessions', width: "50%", hints: sessionHints, children: sessionItems.length > 0 ? (_jsx(List, { items: sessionItems, focused: focusedPanel === 'sessions', onSelect: (item) => {
|
|
329
|
+
const window = projectWindows.find((w) => String(w.index) === item.id);
|
|
330
|
+
if (window)
|
|
331
|
+
onSelectWindow(window);
|
|
332
|
+
} })) : (_jsx(Text, { dimColor: true, children: "No active sessions" })) })] })] }));
|
|
333
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Project } from '../types.js';
|
|
2
|
+
interface ProjectsProps {
|
|
3
|
+
projects: Project[];
|
|
4
|
+
currentProject: Project | null;
|
|
5
|
+
onSelect: (project: Project) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function Projects({ projects, currentProject, onSelect }: ProjectsProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Panel } from '../components/Panel.js';
|
|
4
|
+
import { List } from '../components/List.js';
|
|
5
|
+
export function Projects({ projects, currentProject, onSelect }) {
|
|
6
|
+
const items = projects.map((project) => ({
|
|
7
|
+
id: project.name,
|
|
8
|
+
label: project.name,
|
|
9
|
+
status: project.name === currentProject?.name ? 'active' : undefined,
|
|
10
|
+
meta: project.path,
|
|
11
|
+
}));
|
|
12
|
+
if (projects.length === 0) {
|
|
13
|
+
return (_jsxs(Panel, { title: "Projects", focused: true, children: [_jsx(Text, { dimColor: true, children: "No projects configured." }), _jsx(Text, { dimColor: true, children: "Add projects to ~/.yeehaw/projects/" })] }));
|
|
14
|
+
}
|
|
15
|
+
return (_jsxs(Panel, { title: "Select Project", focused: true, children: [_jsx(List, { items: items, focused: true, onSelect: (item) => {
|
|
16
|
+
const project = projects.find((p) => p.name === item.id);
|
|
17
|
+
if (project)
|
|
18
|
+
onSelect(project);
|
|
19
|
+
} }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to select, Esc to cancel" }) })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Project } from '../types.js';
|
|
2
|
+
interface WikiViewProps {
|
|
3
|
+
project: Project;
|
|
4
|
+
onBack: () => void;
|
|
5
|
+
onUpdateProject: (project: Project) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function WikiView({ project, onBack, onUpdateProject }: WikiViewProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { Header } from '../components/Header.js';
|
|
6
|
+
import { Panel } from '../components/Panel.js';
|
|
7
|
+
import { List } from '../components/List.js';
|
|
8
|
+
import { TextArea } from '../components/TextArea.js';
|
|
9
|
+
import { ScrollableMarkdown } from '../components/ScrollableMarkdown.js';
|
|
10
|
+
export function WikiView({ project, onBack, onUpdateProject }) {
|
|
11
|
+
const [mode, setMode] = useState('normal');
|
|
12
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
13
|
+
const [focusedPanel, setFocusedPanel] = useState('sections');
|
|
14
|
+
// Form state
|
|
15
|
+
const [editTitle, setEditTitle] = useState('');
|
|
16
|
+
const [editContent, setEditContent] = useState('');
|
|
17
|
+
const wiki = project.wiki || [];
|
|
18
|
+
const selectedSection = wiki[selectedIndex];
|
|
19
|
+
const resetForm = () => {
|
|
20
|
+
setEditTitle('');
|
|
21
|
+
setEditContent('');
|
|
22
|
+
};
|
|
23
|
+
const saveSection = (title, content, isNew) => {
|
|
24
|
+
const newWiki = [...wiki];
|
|
25
|
+
if (isNew) {
|
|
26
|
+
newWiki.push({ title, content });
|
|
27
|
+
setSelectedIndex(newWiki.length - 1);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
newWiki[selectedIndex] = { title, content };
|
|
31
|
+
}
|
|
32
|
+
onUpdateProject({ ...project, wiki: newWiki });
|
|
33
|
+
setMode('normal');
|
|
34
|
+
resetForm();
|
|
35
|
+
};
|
|
36
|
+
const deleteSection = () => {
|
|
37
|
+
const newWiki = wiki.filter((_, i) => i !== selectedIndex);
|
|
38
|
+
onUpdateProject({ ...project, wiki: newWiki });
|
|
39
|
+
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
|
40
|
+
setMode('normal');
|
|
41
|
+
};
|
|
42
|
+
// Single useInput hook that handles all modes
|
|
43
|
+
useInput((input, key) => {
|
|
44
|
+
// Handle escape - works in all modes
|
|
45
|
+
if (key.escape) {
|
|
46
|
+
if (mode !== 'normal') {
|
|
47
|
+
setMode('normal');
|
|
48
|
+
resetForm();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
onBack();
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Delete confirmation mode
|
|
56
|
+
if (mode === 'delete-confirm') {
|
|
57
|
+
if (input === 'y') {
|
|
58
|
+
deleteSection();
|
|
59
|
+
}
|
|
60
|
+
else if (input === 'n') {
|
|
61
|
+
setMode('normal');
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Only process these in normal mode
|
|
66
|
+
if (mode !== 'normal')
|
|
67
|
+
return;
|
|
68
|
+
if (input === 'q') {
|
|
69
|
+
onBack();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Tab to switch focus between panels
|
|
73
|
+
if (key.tab) {
|
|
74
|
+
setFocusedPanel((prev) => (prev === 'sections' ? 'content' : 'sections'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Only handle section navigation when sections panel is focused
|
|
78
|
+
if (focusedPanel !== 'sections')
|
|
79
|
+
return;
|
|
80
|
+
if (input === 'j' || key.downArrow) {
|
|
81
|
+
setSelectedIndex((i) => Math.min(i + 1, wiki.length - 1));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (input === 'k' || key.upArrow) {
|
|
85
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (input === 'n') {
|
|
89
|
+
setEditTitle('');
|
|
90
|
+
setEditContent('');
|
|
91
|
+
setMode('add-title');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (input === 'e' && selectedSection) {
|
|
95
|
+
setEditTitle(selectedSection.title);
|
|
96
|
+
setEditContent(selectedSection.content);
|
|
97
|
+
setMode('edit-title');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (input === 'd' && selectedSection) {
|
|
101
|
+
setMode('delete-confirm');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Add/Edit title screen
|
|
106
|
+
if (mode === 'add-title' || mode === 'edit-title') {
|
|
107
|
+
const isNew = mode === 'add-title';
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Wiki", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: isNew ? 'New Wiki Section' : 'Edit Wiki Section' }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Title: " }), _jsx(TextInput, { value: editTitle, onChange: setEditTitle, onSubmit: () => {
|
|
109
|
+
if (editTitle.trim()) {
|
|
110
|
+
setMode(isNew ? 'add-content' : 'edit-content');
|
|
111
|
+
}
|
|
112
|
+
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Esc: cancel" }) })] })] }));
|
|
113
|
+
}
|
|
114
|
+
// Add/Edit content screen
|
|
115
|
+
if (mode === 'add-content' || mode === 'edit-content') {
|
|
116
|
+
const isNew = mode === 'add-content';
|
|
117
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Wiki", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: editTitle }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: "Content (markdown):" }), _jsx(Box, { marginTop: 1, children: _jsx(TextArea, { value: editContent, onChange: setEditContent, onSubmit: () => {
|
|
118
|
+
if (editContent.trim()) {
|
|
119
|
+
saveSection(editTitle, editContent, isNew);
|
|
120
|
+
}
|
|
121
|
+
}, placeholder: "Write your content here...", height: 10 }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: new line | Ctrl+S: save | Esc: cancel" }) })] })] }));
|
|
122
|
+
}
|
|
123
|
+
// Delete confirmation screen
|
|
124
|
+
if (mode === 'delete-confirm') {
|
|
125
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Wiki", color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Delete Section" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Delete \"", selectedSection?.title, "\"?"] }) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: "red", bold: true, children: "[y] Yes, delete" }), _jsx(Text, { dimColor: true, children: "[n/Esc] Cancel" })] })] })] }));
|
|
126
|
+
}
|
|
127
|
+
// Build section items for list (no status dot)
|
|
128
|
+
const sectionItems = wiki.map((section, i) => ({
|
|
129
|
+
id: String(i),
|
|
130
|
+
label: section.title,
|
|
131
|
+
}));
|
|
132
|
+
// Panel-specific hints
|
|
133
|
+
const sectionsHints = '[n] new [e] edit [d] delete';
|
|
134
|
+
const contentHints = 'j/k scroll';
|
|
135
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Wiki", color: project.color }), _jsxs(Box, { flexGrow: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Sections", focused: focusedPanel === 'sections', width: "30%", hints: sectionsHints, children: sectionItems.length > 0 ? (_jsx(List, { items: sectionItems, focused: focusedPanel === 'sections', selectedIndex: selectedIndex, onSelectionChange: setSelectedIndex, onSelect: (item) => {
|
|
136
|
+
setSelectedIndex(parseInt(item.id, 10));
|
|
137
|
+
} })) : (_jsx(Text, { dimColor: true, children: "No wiki sections yet" })) }), _jsx(Panel, { title: selectedSection?.title || 'Content', focused: focusedPanel === 'content', width: "70%", hints: contentHints, children: selectedSection ? (_jsx(ScrollableMarkdown, { focused: focusedPanel === 'content', height: 20, children: selectedSection.content })) : (_jsx(Text, { dimColor: true, children: "Select a section to view its content" })) })] })] }));
|
|
138
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@colmbus72/yeehaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal dashboard for managing projects, servers, and deployments",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"yeehaw": "dist/index.js",
|
|
9
|
+
"yeehaw-mcp": "dist/mcp-server.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "tsx watch src/index.tsx",
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"cli",
|
|
25
|
+
"terminal",
|
|
26
|
+
"dashboard",
|
|
27
|
+
"tmux",
|
|
28
|
+
"ssh",
|
|
29
|
+
"devops",
|
|
30
|
+
"infrastructure",
|
|
31
|
+
"claude",
|
|
32
|
+
"ink",
|
|
33
|
+
"react"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": ""
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
43
|
+
"chalk": "^5.3.0",
|
|
44
|
+
"chokidar": "^3.6.0",
|
|
45
|
+
"execa": "^8.0.1",
|
|
46
|
+
"figlet": "^1.7.0",
|
|
47
|
+
"ink": "^4.4.1",
|
|
48
|
+
"ink-text-input": "^5.0.1",
|
|
49
|
+
"js-yaml": "^4.1.0",
|
|
50
|
+
"marked": "^9.1.6",
|
|
51
|
+
"marked-terminal": "^6.2.0",
|
|
52
|
+
"react": "^18.2.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/figlet": "^1.5.8",
|
|
56
|
+
"@types/js-yaml": "^4.0.9",
|
|
57
|
+
"@types/node": "^25.0.10",
|
|
58
|
+
"@types/react": "^18.2.0",
|
|
59
|
+
"tsx": "^4.7.0",
|
|
60
|
+
"typescript": "^5.3.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=20.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|