@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,89 @@
1
+ export interface Config {
2
+ version: number;
3
+ default_project: string | null;
4
+ editor: string;
5
+ theme: 'dark' | 'light';
6
+ show_activity: boolean;
7
+ claude: ClaudeConfig;
8
+ tmux: TmuxConfig;
9
+ }
10
+ export interface ClaudeConfig {
11
+ model: string;
12
+ auto_attach: boolean;
13
+ }
14
+ export interface TmuxConfig {
15
+ session_prefix: string;
16
+ default_shell: string;
17
+ }
18
+ export interface Project {
19
+ name: string;
20
+ path: string;
21
+ summary?: string;
22
+ color?: string;
23
+ livestock?: Livestock[];
24
+ wiki?: WikiSection[];
25
+ }
26
+ export interface WikiSection {
27
+ title: string;
28
+ content: string;
29
+ }
30
+ export interface Livestock {
31
+ name: string;
32
+ path: string;
33
+ barn?: string;
34
+ repo?: string;
35
+ branch?: string;
36
+ log_path?: string;
37
+ env_path?: string;
38
+ }
39
+ export interface Critter {
40
+ name: string;
41
+ type: string;
42
+ status?: 'running' | 'stopped' | 'unknown';
43
+ }
44
+ export interface Barn {
45
+ name: string;
46
+ host?: string;
47
+ user?: string;
48
+ port?: number;
49
+ identity_file?: string;
50
+ critters?: Critter[];
51
+ }
52
+ export interface Session {
53
+ id: string;
54
+ type: 'claude' | 'shell';
55
+ project: string | null;
56
+ livestock: string | null;
57
+ barn: string | null;
58
+ tmux_session: string;
59
+ tmux_window: number | null;
60
+ started_at: string;
61
+ working_directory: string;
62
+ notes: string;
63
+ status: 'active' | 'detached' | 'ended';
64
+ }
65
+ export type AppView = {
66
+ type: 'global';
67
+ } | {
68
+ type: 'project';
69
+ project: Project;
70
+ } | {
71
+ type: 'barn';
72
+ barn: Barn;
73
+ } | {
74
+ type: 'wiki';
75
+ project: Project;
76
+ } | {
77
+ type: 'issues';
78
+ project: Project;
79
+ } | {
80
+ type: 'livestock';
81
+ project: Project;
82
+ livestock: Livestock;
83
+ } | {
84
+ type: 'logs';
85
+ project: Project;
86
+ livestock: Livestock;
87
+ } | {
88
+ type: 'night-sky';
89
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // Core domain types
2
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { Barn, Project, Livestock } from '../types.js';
2
+ import type { TmuxWindow } from '../lib/tmux.js';
3
+ interface LivestockWithProject {
4
+ project: Project;
5
+ livestock: Livestock;
6
+ }
7
+ interface BarnContextProps {
8
+ barn: Barn;
9
+ livestock: LivestockWithProject[];
10
+ projects: Project[];
11
+ windows: TmuxWindow[];
12
+ onBack: () => void;
13
+ onSshToBarn: () => void;
14
+ onSelectLivestock: (project: Project, livestock: Livestock) => void;
15
+ onOpenLivestockSession: (project: Project, livestock: Livestock) => void;
16
+ onUpdateBarn: (barn: Barn) => void;
17
+ onDeleteBarn: (barnName: string) => void;
18
+ onAddLivestock: (project: Project, livestock: Livestock) => void;
19
+ onRemoveLivestock: (project: Project, livestockName: string) => void;
20
+ }
21
+ export declare function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, }: BarnContextProps): import("react/jsx-runtime").JSX.Element;
22
+ export {};
@@ -0,0 +1,252 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { BarnHeader } from '../components/BarnHeader.js';
6
+ import { Panel } from '../components/Panel.js';
7
+ import { List } from '../components/List.js';
8
+ import { PathInput } from '../components/PathInput.js';
9
+ import { detectRemoteGitInfo, detectGitInfo } from '../lib/git.js';
10
+ import { isLocalBarn } from '../lib/config.js';
11
+ import { parseGitHubUrl } from '../lib/github.js';
12
+ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, }) {
13
+ const [focusedPanel, setFocusedPanel] = useState('livestock');
14
+ const [mode, setMode] = useState('normal');
15
+ // Check if this is the local barn
16
+ const isLocal = isLocalBarn(barn);
17
+ // Edit form state (only used for remote barns)
18
+ const [editHost, setEditHost] = useState(barn.host || '');
19
+ const [editUser, setEditUser] = useState(barn.user || '');
20
+ const [editPort, setEditPort] = useState(String(barn.port || 22));
21
+ // Delete confirmation
22
+ const [deleteConfirmInput, setDeleteConfirmInput] = useState('');
23
+ // Add livestock state (new flow: path → detect git → match/pick project → name)
24
+ const [newLivestockPath, setNewLivestockPath] = useState('');
25
+ const [detectedGit, setDetectedGit] = useState(null);
26
+ const [selectedProject, setSelectedProject] = useState(null);
27
+ const [newLivestockName, setNewLivestockName] = useState('');
28
+ // Delete livestock state
29
+ const [selectedLivestockIndex, setSelectedLivestockIndex] = useState(0);
30
+ const [deleteLivestockTarget, setDeleteLivestockTarget] = useState(null);
31
+ // Filter windows that are barn sessions
32
+ const barnWindows = windows.filter((w) => w.index > 0 && w.name.startsWith(`barn-${barn.name}`));
33
+ const startEdit = () => {
34
+ if (isLocal)
35
+ return; // Cannot edit local barn
36
+ setEditHost(barn.host || '');
37
+ setEditUser(barn.user || '');
38
+ setEditPort(String(barn.port || 22));
39
+ setMode('edit-host');
40
+ };
41
+ const cancelEdit = () => {
42
+ setMode('normal');
43
+ };
44
+ const saveAndNext = (nextMode) => {
45
+ if (nextMode === 'done') {
46
+ onUpdateBarn({
47
+ ...barn,
48
+ host: editHost,
49
+ user: editUser,
50
+ port: parseInt(editPort, 10) || 22,
51
+ });
52
+ setMode('normal');
53
+ }
54
+ else {
55
+ setMode(nextMode);
56
+ }
57
+ };
58
+ useInput((input, key) => {
59
+ // Handle escape
60
+ if (key.escape) {
61
+ if (mode !== 'normal') {
62
+ cancelEdit();
63
+ }
64
+ else {
65
+ onBack();
66
+ }
67
+ return;
68
+ }
69
+ // Handle delete livestock confirmation
70
+ if (mode === 'delete-livestock-confirm' && deleteLivestockTarget) {
71
+ if (input === 'y') {
72
+ onRemoveLivestock(deleteLivestockTarget.project, deleteLivestockTarget.livestock.name);
73
+ setDeleteLivestockTarget(null);
74
+ setSelectedLivestockIndex(Math.max(0, selectedLivestockIndex - 1));
75
+ setMode('normal');
76
+ }
77
+ else if (input === 'n') {
78
+ setDeleteLivestockTarget(null);
79
+ setMode('normal');
80
+ }
81
+ return;
82
+ }
83
+ if (mode !== 'normal')
84
+ return;
85
+ if (input === 'q') {
86
+ onBack();
87
+ return;
88
+ }
89
+ if (key.tab) {
90
+ setFocusedPanel((p) => (p === 'livestock' ? 'critters' : 'livestock'));
91
+ return;
92
+ }
93
+ if (input === 's') {
94
+ // Context-aware shell: livestock session if focused on livestock, otherwise SSH to barn
95
+ if (focusedPanel === 'livestock' && livestock.length > 0) {
96
+ const target = livestock[selectedLivestockIndex];
97
+ if (target) {
98
+ onOpenLivestockSession(target.project, target.livestock);
99
+ return;
100
+ }
101
+ }
102
+ onSshToBarn();
103
+ return;
104
+ }
105
+ if (input === 'e' && !isLocal) {
106
+ startEdit();
107
+ return;
108
+ }
109
+ if (input === 'D' && !isLocal) {
110
+ setDeleteConfirmInput('');
111
+ setMode('delete-barn-confirm');
112
+ return;
113
+ }
114
+ if (input === 'n' && focusedPanel === 'livestock') {
115
+ // Start add livestock flow with path first
116
+ setNewLivestockPath('');
117
+ setDetectedGit(null);
118
+ setSelectedProject(null);
119
+ setNewLivestockName('');
120
+ setMode('add-livestock-path');
121
+ return;
122
+ }
123
+ if (input === 'd' && focusedPanel === 'livestock' && livestock.length > 0) {
124
+ // Delete selected livestock
125
+ const target = livestock[selectedLivestockIndex];
126
+ if (target) {
127
+ setDeleteLivestockTarget(target);
128
+ setMode('delete-livestock-confirm');
129
+ }
130
+ return;
131
+ }
132
+ });
133
+ // Edit mode screens
134
+ if (mode === 'edit-host') {
135
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Editing barn" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Barn" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Host: " }), _jsx(TextInput, { value: editHost, onChange: setEditHost, onSubmit: () => saveAndNext('edit-user') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
136
+ }
137
+ if (mode === 'edit-user') {
138
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Editing barn" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Edit Barn: ", barn.name] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "User: " }), _jsx(TextInput, { value: editUser, onChange: setEditUser, onSubmit: () => saveAndNext('edit-port') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
139
+ }
140
+ if (mode === 'edit-port') {
141
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Editing barn" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Edit Barn: ", barn.name] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Port: " }), _jsx(TextInput, { value: editPort, onChange: setEditPort, onSubmit: () => saveAndNext('done') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save barn, Esc: cancel" }) })] })] }));
142
+ }
143
+ if (mode === 'delete-barn-confirm') {
144
+ const handleDeleteConfirm = () => {
145
+ if (deleteConfirmInput === barn.name) {
146
+ onDeleteBarn(barn.name);
147
+ }
148
+ };
149
+ const hasLivestock = livestock.length > 0;
150
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "DELETE BARN" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "\u26A0\uFE0F Delete Barn" }), hasLivestock ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "red", children: "Cannot delete barn - livestock still deployed:" }), livestock.map((l) => (_jsxs(Text, { dimColor: true, children: ["\u2022 ", l.project.name, "/", l.livestock.name] }, `${l.project.name}-${l.livestock.name}`))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Remove livestock from projects first, then delete the barn." }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "This will permanently delete the barn configuration." }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Type the barn name to confirm: " }), _jsx(Text, { bold: true, color: "yellow", children: barn.name })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Confirm: " }), _jsx(TextInput, { value: deleteConfirmInput, onChange: setDeleteConfirmInput, onSubmit: handleDeleteConfirm })] }), deleteConfirmInput && deleteConfirmInput !== barn.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: "Esc: cancel" }) })] })] }));
151
+ }
152
+ // Add livestock flow: path → detect git → match/pick project → name
153
+ // Step 1: Enter path (with remote tab completion)
154
+ if (mode === 'add-livestock-path') {
155
+ const handlePathSubmit = () => {
156
+ if (!newLivestockPath.trim())
157
+ return;
158
+ // Detect git info from path (local or remote)
159
+ const gitInfo = isLocal
160
+ ? detectGitInfo(newLivestockPath)
161
+ : detectRemoteGitInfo(newLivestockPath, barn);
162
+ setDetectedGit(gitInfo);
163
+ // Try to match project by repo URL
164
+ if (gitInfo.isGitRepo && gitInfo.remoteUrl) {
165
+ const parsed = parseGitHubUrl(gitInfo.remoteUrl);
166
+ if (parsed) {
167
+ // Find project with matching repo in any livestock
168
+ const matchedProject = projects.find((p) => (p.livestock || []).some((l) => {
169
+ if (!l.repo)
170
+ return false;
171
+ const lParsed = parseGitHubUrl(l.repo);
172
+ return lParsed && lParsed.owner === parsed.owner && lParsed.repo === parsed.repo;
173
+ }));
174
+ if (matchedProject) {
175
+ setSelectedProject(matchedProject);
176
+ setMode('add-livestock-name');
177
+ return;
178
+ }
179
+ }
180
+ }
181
+ // No match found - let user pick project
182
+ if (projects.length === 0) {
183
+ // No projects available
184
+ setMode('normal');
185
+ return;
186
+ }
187
+ setMode('add-livestock-project');
188
+ };
189
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding livestock" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Livestock to ", barn.name] }), _jsxs(Text, { dimColor: true, children: ["Enter the ", isLocal ? 'local' : `path on ${barn.host}`, " path"] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: newLivestockPath, onChange: setNewLivestockPath, onSubmit: handlePathSubmit, barn: isLocal ? undefined : barn })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: autocomplete, Enter: next (auto-detects git), Esc: cancel" }) })] })] }));
190
+ }
191
+ // Step 2 (if no auto-match): Select project
192
+ if (mode === 'add-livestock-project') {
193
+ const projectOptions = projects.map((p) => ({
194
+ id: p.name,
195
+ label: p.name,
196
+ status: 'active',
197
+ meta: p.path,
198
+ }));
199
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding livestock" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "green", children: "Add Livestock to Barn" }), detectedGit?.isGitRepo ? (_jsxs(Text, { dimColor: true, children: ["Repo detected: ", detectedGit.remoteUrl, " (no matching project found)"] })) : (_jsx(Text, { dimColor: true, children: "No git repo detected. Which project does this belong to?" })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(List, { items: projectOptions, focused: true, onSelect: (item) => {
200
+ const project = projects.find((p) => p.name === item.id);
201
+ if (project) {
202
+ setSelectedProject(project);
203
+ setMode('add-livestock-name');
204
+ }
205
+ } }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: select, Esc: cancel" }) })] })] }));
206
+ }
207
+ // Step 3: Enter name
208
+ if (mode === 'add-livestock-name') {
209
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding livestock" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Livestock: ", selectedProject?.name] }), detectedGit?.isGitRepo && (_jsxs(Text, { color: "cyan", children: ["Git detected: ", detectedGit.branch || 'unknown branch'] })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newLivestockName, onChange: setNewLivestockName, onSubmit: () => {
210
+ if (newLivestockName.trim() && selectedProject) {
211
+ const newLivestock = {
212
+ name: newLivestockName,
213
+ path: newLivestockPath,
214
+ barn: isLocal ? undefined : barn.name, // Don't set barn for local livestock
215
+ repo: detectedGit?.remoteUrl,
216
+ branch: detectedGit?.branch,
217
+ };
218
+ onAddLivestock(selectedProject, newLivestock);
219
+ setMode('normal');
220
+ }
221
+ }, placeholder: "production, staging, etc." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save, Esc: cancel" }) })] })] }));
222
+ }
223
+ // Delete livestock confirmation
224
+ if (mode === 'delete-livestock-confirm' && deleteLivestockTarget) {
225
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Remove livestock" }), _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.livestock.name, "\" from ", deleteLivestockTarget.project.name, "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Path: ", deleteLivestockTarget.livestock.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" })] })] })] }));
226
+ }
227
+ // Build livestock items
228
+ const livestockItems = livestock.map((l) => ({
229
+ id: `${l.project.name}/${l.livestock.name}`,
230
+ label: `${l.project.name}/${l.livestock.name}`,
231
+ status: 'active',
232
+ meta: l.livestock.path,
233
+ }));
234
+ // Build critter items
235
+ const critterItems = (barn.critters || []).map((c) => ({
236
+ id: c.name,
237
+ label: c.name,
238
+ status: c.status === 'running' ? 'active' : c.status === 'stopped' ? 'inactive' : 'inactive',
239
+ meta: c.type,
240
+ }));
241
+ // Panel-specific hints (page-level hotkeys like s are in BottomBar)
242
+ const livestockHints = '[s] shell [n] new [d] delete';
243
+ const crittersHints = '';
244
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: isLocal ? 'Local machine' : `${barn.user}@${barn.host}:${barn.port}` }), !isLocal && (_jsx(Box, { paddingX: 2, marginY: 1, children: _jsxs(Box, { gap: 4, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Host:" }), " ", barn.host] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "User:" }), " ", barn.user] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Port:" }), " ", barn.port] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Key:" }), " ", barn.identity_file] })] }) })), _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) => {
245
+ const found = livestock.find((l) => `${l.project.name}/${l.livestock.name}` === item.id);
246
+ if (found) {
247
+ onSelectLivestock(found.project, found.livestock);
248
+ }
249
+ } })) : (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No livestock deployed to this barn" }) })) }), _jsx(Panel, { title: "Critters", focused: focusedPanel === 'critters', width: "50%", hints: crittersHints, children: critterItems.length > 0 ? (_jsx(List, { items: critterItems, focused: focusedPanel === 'critters', onSelect: () => {
250
+ // TODO: Could show critter details or manage service
251
+ } })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No critters configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Critters are system services (nginx, mysql, etc.)" })] })) })] })] }));
252
+ }
@@ -0,0 +1,16 @@
1
+ import type { Project, Barn } from '../types.js';
2
+ import { type TmuxWindow } from '../lib/tmux.js';
3
+ interface GlobalDashboardProps {
4
+ projects: Project[];
5
+ barns: Barn[];
6
+ windows: TmuxWindow[];
7
+ onSelectProject: (project: Project) => void;
8
+ onSelectBarn: (barn: Barn) => void;
9
+ onSelectWindow: (window: TmuxWindow) => void;
10
+ onNewClaude: () => void;
11
+ onCreateProject: (name: string, path: string) => void;
12
+ onCreateBarn: (barn: Barn) => void;
13
+ onSshToBarn: (barn: Barn) => void;
14
+ }
15
+ export declare function GlobalDashboard({ projects, barns, windows, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -0,0 +1,253 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo } 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 { parseSshConfig } from '../lib/ssh.js';
10
+ import { getWindowStatus } from '../lib/tmux.js';
11
+ function countSessionsForProject(projectName, windows) {
12
+ return windows.filter((w) => w.name.startsWith(projectName)).length;
13
+ }
14
+ export function GlobalDashboard({ projects, barns, windows, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }) {
15
+ const [focusedPanel, setFocusedPanel] = useState('projects');
16
+ const [mode, setMode] = useState('normal');
17
+ // New project form state
18
+ const [newProjectName, setNewProjectName] = useState('');
19
+ const [newProjectPath, setNewProjectPath] = useState('');
20
+ // New barn form state
21
+ const [newBarnName, setNewBarnName] = useState('');
22
+ const [newBarnHost, setNewBarnHost] = useState('');
23
+ const [newBarnUser, setNewBarnUser] = useState('');
24
+ const [newBarnPort, setNewBarnPort] = useState('22');
25
+ const [newBarnKey, setNewBarnKey] = useState('');
26
+ // SSH hosts from config
27
+ const sshHosts = useMemo(() => parseSshConfig(), []);
28
+ // Filter out window 0 (yeehaw TUI)
29
+ const sessionWindows = windows.filter((w) => w.index > 0);
30
+ // Create a map from display number (1-9) to window for quick access
31
+ const windowsByDisplayNum = new Map();
32
+ sessionWindows.forEach((w, i) => {
33
+ if (i < 9) {
34
+ windowsByDisplayNum.set(i + 1, w);
35
+ }
36
+ });
37
+ const resetNewProject = () => {
38
+ setNewProjectName('');
39
+ setNewProjectPath('');
40
+ };
41
+ const resetNewBarn = () => {
42
+ setNewBarnName('');
43
+ setNewBarnHost('');
44
+ setNewBarnUser('');
45
+ setNewBarnPort('22');
46
+ setNewBarnKey('');
47
+ };
48
+ useInput((input, key) => {
49
+ // Handle escape to cancel creation
50
+ if (key.escape && mode !== 'normal') {
51
+ setMode('normal');
52
+ resetNewProject();
53
+ resetNewBarn();
54
+ return;
55
+ }
56
+ // Only process these in normal mode
57
+ if (mode !== 'normal')
58
+ return;
59
+ // Tab to cycle panels
60
+ if (key.tab) {
61
+ setFocusedPanel((p) => {
62
+ if (p === 'projects')
63
+ return 'sessions';
64
+ if (p === 'sessions')
65
+ return 'barns';
66
+ return 'projects';
67
+ });
68
+ return;
69
+ }
70
+ if (input === 'c') {
71
+ onNewClaude();
72
+ return;
73
+ }
74
+ if (input === 'n') {
75
+ if (focusedPanel === 'projects') {
76
+ setMode('new-project-name');
77
+ return;
78
+ }
79
+ if (focusedPanel === 'barns') {
80
+ // If we have SSH hosts, show selection first
81
+ if (sshHosts.length > 0) {
82
+ setMode('new-barn-select-ssh');
83
+ }
84
+ else {
85
+ setMode('new-barn-name');
86
+ }
87
+ return;
88
+ }
89
+ }
90
+ if (input === 's' && focusedPanel === 'barns') {
91
+ // SSH to selected barn - handled by list selection
92
+ return;
93
+ }
94
+ // Number hotkeys 1-9 for quick session switching
95
+ const num = parseInt(input, 10);
96
+ if (num >= 1 && num <= 9) {
97
+ const window = windowsByDisplayNum.get(num);
98
+ if (window) {
99
+ onSelectWindow(window);
100
+ }
101
+ return;
102
+ }
103
+ });
104
+ const handleProjectNameSubmit = (name) => {
105
+ if (name.trim()) {
106
+ setNewProjectName(name.trim());
107
+ setNewProjectPath('');
108
+ setMode('new-project-path');
109
+ }
110
+ };
111
+ const handleProjectPathSubmit = (path) => {
112
+ if (path.trim() && newProjectName) {
113
+ onCreateProject(newProjectName, path.trim());
114
+ setMode('normal');
115
+ resetNewProject();
116
+ }
117
+ };
118
+ const handleBarnNameSubmit = (name) => {
119
+ if (name.trim()) {
120
+ setNewBarnName(name.trim());
121
+ setMode('new-barn-host');
122
+ }
123
+ };
124
+ const handleBarnHostSubmit = (host) => {
125
+ if (host.trim()) {
126
+ setNewBarnHost(host.trim());
127
+ setMode('new-barn-user');
128
+ }
129
+ };
130
+ const handleBarnUserSubmit = (user) => {
131
+ if (user.trim()) {
132
+ setNewBarnUser(user.trim());
133
+ setMode('new-barn-port');
134
+ }
135
+ };
136
+ const handleBarnPortSubmit = (port) => {
137
+ setNewBarnPort(port.trim() || '22');
138
+ setMode('new-barn-key');
139
+ };
140
+ const handleBarnKeySubmit = (key) => {
141
+ if (key.trim()) {
142
+ const barn = {
143
+ name: newBarnName,
144
+ host: newBarnHost,
145
+ user: newBarnUser,
146
+ port: parseInt(newBarnPort, 10) || 22,
147
+ identity_file: key.trim(),
148
+ critters: [],
149
+ };
150
+ onCreateBarn(barn);
151
+ setMode('normal');
152
+ resetNewBarn();
153
+ }
154
+ };
155
+ const handleSshHostSelect = (host) => {
156
+ // Pre-fill from SSH config
157
+ setNewBarnName(host.name);
158
+ setNewBarnHost(host.hostname || host.name);
159
+ setNewBarnUser(host.user || 'root');
160
+ setNewBarnPort(String(host.port || 22));
161
+ setNewBarnKey(host.identityFile || '');
162
+ setMode('new-barn-name');
163
+ };
164
+ const projectItems = projects.map((p) => {
165
+ const sessionCount = countSessionsForProject(p.name, windows);
166
+ return {
167
+ id: p.name,
168
+ label: p.name,
169
+ status: sessionCount > 0 ? 'active' : 'inactive',
170
+ meta: sessionCount > 0 ? `${sessionCount} session${sessionCount > 1 ? 's' : ''}` : undefined,
171
+ };
172
+ });
173
+ // Use display numbers (1-9) instead of window index
174
+ const sessionItems = sessionWindows.map((w, i) => ({
175
+ id: String(w.index),
176
+ label: `[${i + 1}] ${w.name}`,
177
+ status: w.active ? 'active' : 'inactive',
178
+ meta: getWindowStatus(w),
179
+ }));
180
+ const barnItems = barns.map((b) => ({
181
+ id: b.name,
182
+ label: b.name,
183
+ status: 'active',
184
+ meta: `${b.user}@${b.host}`,
185
+ }));
186
+ // New project modals
187
+ if (mode === 'new-project-name') {
188
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Create New Project" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newProjectName, onChange: setNewProjectName, onSubmit: handleProjectNameSubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue, Esc to cancel" }) })] })] }));
189
+ }
190
+ if (mode === 'new-project-path') {
191
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Create New Project: ", newProjectName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: newProjectPath, onChange: setNewProjectPath, onSubmit: handleProjectPathSubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab to autocomplete, Enter to create, Esc to cancel" }) })] })] }));
192
+ }
193
+ // SSH host selection for new barn
194
+ if (mode === 'new-barn-select-ssh') {
195
+ const sshHostItems = [
196
+ { id: '__manual__', label: 'Enter manually...', status: 'inactive' },
197
+ ...sshHosts.map((h) => ({
198
+ id: h.name,
199
+ label: h.name,
200
+ status: 'active',
201
+ meta: h.hostname ? `${h.user || 'root'}@${h.hostname}` : undefined,
202
+ })),
203
+ ];
204
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "green", children: "Add New Barn" }), _jsx(Text, { dimColor: true, children: "Select from SSH config or enter manually" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(List, { items: sshHostItems, focused: true, onSelect: (item) => {
205
+ if (item.id === '__manual__') {
206
+ setMode('new-barn-name');
207
+ }
208
+ else {
209
+ const host = sshHosts.find((h) => h.name === item.id);
210
+ if (host)
211
+ handleSshHostSelect(host);
212
+ }
213
+ } }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: select, Esc: cancel" }) })] })] }));
214
+ }
215
+ // New barn modals
216
+ if (mode === 'new-barn-name') {
217
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "green", children: "Add New Barn" }), _jsx(Text, { dimColor: true, children: "A barn is a server you manage" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newBarnName, onChange: setNewBarnName, onSubmit: handleBarnNameSubmit, placeholder: "my-server" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
218
+ }
219
+ if (mode === 'new-barn-host') {
220
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Host: " }), _jsx(TextInput, { value: newBarnHost, onChange: setNewBarnHost, onSubmit: handleBarnHostSubmit, placeholder: "192.168.1.100 or server.example.com" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
221
+ }
222
+ if (mode === 'new-barn-user') {
223
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH User: " }), _jsx(TextInput, { value: newBarnUser, onChange: setNewBarnUser, onSubmit: handleBarnUserSubmit, placeholder: "root" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
224
+ }
225
+ if (mode === 'new-barn-port') {
226
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH Port: " }), _jsx(TextInput, { value: newBarnPort, onChange: setNewBarnPort, onSubmit: handleBarnPortSubmit, placeholder: "22" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field (leave blank for 22), Esc: cancel" }) })] })] }));
227
+ }
228
+ if (mode === 'new-barn-key') {
229
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH Key Path: " }), _jsx(PathInput, { value: newBarnKey, onChange: setNewBarnKey, onSubmit: handleBarnKeySubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: autocomplete, Enter: create barn, Esc: cancel" }) })] })] }));
230
+ }
231
+ // Panel-specific hints (page-level hotkeys like c are in BottomBar)
232
+ const projectHints = '[n] new';
233
+ const sessionHints = '1-9 switch';
234
+ const barnHints = '[n] new [s] shell';
235
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Projects", focused: focusedPanel === 'projects', width: "40%", hints: projectHints, children: projectItems.length > 0 ? (_jsx(List, { items: projectItems, focused: focusedPanel === 'projects', onSelect: (item) => {
236
+ const project = projects.find((p) => p.name === item.id);
237
+ if (project)
238
+ onSelectProject(project);
239
+ } })) : (_jsx(Text, { dimColor: true, children: "No projects yet" })) }), _jsx(Panel, { title: "Sessions", focused: focusedPanel === 'sessions', width: "60%", hints: sessionHints, children: sessionItems.length > 0 ? (_jsx(List, { items: sessionItems, focused: focusedPanel === 'sessions', onSelect: (item) => {
240
+ const window = sessionWindows.find((w) => String(w.index) === item.id);
241
+ if (window)
242
+ onSelectWindow(window);
243
+ } })) : (_jsx(Text, { dimColor: true, children: "No active sessions" })) })] }), _jsx(Box, { paddingX: 1, marginBottom: 1, children: _jsx(Panel, { title: "Barns", focused: focusedPanel === 'barns', hints: barnHints, children: barnItems.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsx(List, { items: barnItems, focused: focusedPanel === 'barns', onSelect: (item) => {
244
+ const barn = barns.find((b) => b.name === item.id);
245
+ if (barn)
246
+ onSelectBarn(barn);
247
+ }, onAction: (item) => {
248
+ // 's' key to SSH directly
249
+ const barn = barns.find((b) => b.name === item.id);
250
+ if (barn)
251
+ onSshToBarn(barn);
252
+ } }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No barns configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Barns are servers you manage" })] })) }) })] }));
253
+ }
@@ -0,0 +1,11 @@
1
+ import type { Barn } from '../types.js';
2
+ import type { TmuxSession } from '../lib/tmux.js';
3
+ interface HomeProps {
4
+ barns: Barn[];
5
+ sessions: TmuxSession[];
6
+ focusedPanel: 'barns' | 'sessions';
7
+ onSelectBarn: (barn: Barn) => void;
8
+ onSelectSession: (session: TmuxSession) => void;
9
+ }
10
+ export declare function Home({ barns, sessions, focusedPanel, onSelectBarn, onSelectSession }: HomeProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};