@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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +414 -0
  5. package/dist/components/BarnHeader.d.ts +6 -0
  6. package/dist/components/BarnHeader.js +21 -0
  7. package/dist/components/BottomBar.d.ts +16 -0
  8. package/dist/components/BottomBar.js +7 -0
  9. package/dist/components/Header.d.ts +8 -0
  10. package/dist/components/Header.js +83 -0
  11. package/dist/components/HelpOverlay.d.ts +7 -0
  12. package/dist/components/HelpOverlay.js +17 -0
  13. package/dist/components/List.d.ts +17 -0
  14. package/dist/components/List.js +53 -0
  15. package/dist/components/Markdown.d.ts +8 -0
  16. package/dist/components/Markdown.js +23 -0
  17. package/dist/components/Panel.d.ts +10 -0
  18. package/dist/components/Panel.js +5 -0
  19. package/dist/components/PathInput.d.ts +9 -0
  20. package/dist/components/PathInput.js +141 -0
  21. package/dist/components/ScrollableMarkdown.d.ts +11 -0
  22. package/dist/components/ScrollableMarkdown.js +56 -0
  23. package/dist/components/StatusBar.d.ts +5 -0
  24. package/dist/components/StatusBar.js +20 -0
  25. package/dist/components/TextArea.d.ts +17 -0
  26. package/dist/components/TextArea.js +140 -0
  27. package/dist/components/index.d.ts +5 -0
  28. package/dist/components/index.js +5 -0
  29. package/dist/hooks/index.d.ts +3 -0
  30. package/dist/hooks/index.js +3 -0
  31. package/dist/hooks/useConfig.d.ts +11 -0
  32. package/dist/hooks/useConfig.js +36 -0
  33. package/dist/hooks/useRemoteYeehaw.d.ts +13 -0
  34. package/dist/hooks/useRemoteYeehaw.js +49 -0
  35. package/dist/hooks/useSessions.d.ts +11 -0
  36. package/dist/hooks/useSessions.js +46 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +34 -0
  39. package/dist/lib/config.d.ts +27 -0
  40. package/dist/lib/config.js +150 -0
  41. package/dist/lib/detection.d.ts +16 -0
  42. package/dist/lib/detection.js +41 -0
  43. package/dist/lib/editor.d.ts +5 -0
  44. package/dist/lib/editor.js +35 -0
  45. package/dist/lib/errors.d.ts +28 -0
  46. package/dist/lib/errors.js +48 -0
  47. package/dist/lib/git.d.ts +11 -0
  48. package/dist/lib/git.js +73 -0
  49. package/dist/lib/github.d.ts +43 -0
  50. package/dist/lib/github.js +111 -0
  51. package/dist/lib/hotkeys.d.ts +27 -0
  52. package/dist/lib/hotkeys.js +92 -0
  53. package/dist/lib/index.d.ts +10 -0
  54. package/dist/lib/index.js +10 -0
  55. package/dist/lib/livestock.d.ts +51 -0
  56. package/dist/lib/livestock.js +233 -0
  57. package/dist/lib/mcp-validation.d.ts +33 -0
  58. package/dist/lib/mcp-validation.js +62 -0
  59. package/dist/lib/paths.d.ts +8 -0
  60. package/dist/lib/paths.js +28 -0
  61. package/dist/lib/shell.d.ts +34 -0
  62. package/dist/lib/shell.js +61 -0
  63. package/dist/lib/ssh.d.ts +15 -0
  64. package/dist/lib/ssh.js +77 -0
  65. package/dist/lib/tmux-config.d.ts +3 -0
  66. package/dist/lib/tmux-config.js +42 -0
  67. package/dist/lib/tmux.d.ts +32 -0
  68. package/dist/lib/tmux.js +397 -0
  69. package/dist/mcp-server.d.ts +23 -0
  70. package/dist/mcp-server.js +825 -0
  71. package/dist/types.d.ts +89 -0
  72. package/dist/types.js +2 -0
  73. package/dist/views/BarnContext.d.ts +22 -0
  74. package/dist/views/BarnContext.js +252 -0
  75. package/dist/views/GlobalDashboard.d.ts +16 -0
  76. package/dist/views/GlobalDashboard.js +253 -0
  77. package/dist/views/Home.d.ts +11 -0
  78. package/dist/views/Home.js +27 -0
  79. package/dist/views/IssuesView.d.ts +7 -0
  80. package/dist/views/IssuesView.js +157 -0
  81. package/dist/views/LivestockDetailView.d.ts +11 -0
  82. package/dist/views/LivestockDetailView.js +140 -0
  83. package/dist/views/LogsView.d.ts +8 -0
  84. package/dist/views/LogsView.js +84 -0
  85. package/dist/views/NightSkyView.d.ts +5 -0
  86. package/dist/views/NightSkyView.js +441 -0
  87. package/dist/views/ProjectContext.d.ts +18 -0
  88. package/dist/views/ProjectContext.js +333 -0
  89. package/dist/views/Projects.d.ts +8 -0
  90. package/dist/views/Projects.js +20 -0
  91. package/dist/views/WikiView.d.ts +8 -0
  92. package/dist/views/WikiView.js +138 -0
  93. package/dist/views/index.d.ts +2 -0
  94. package/dist/views/index.js +2 -0
  95. 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
+ }
@@ -0,0 +1,2 @@
1
+ export { GlobalDashboard } from './GlobalDashboard.js';
2
+ export { ProjectContext } from './ProjectContext.js';
@@ -0,0 +1,2 @@
1
+ export { GlobalDashboard } from './GlobalDashboard.js';
2
+ export { ProjectContext } from './ProjectContext.js';
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
+ }