@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yeehaw Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Yeehaw
2
+
3
+ A terminal dashboard for developers who manage multiple projects and servers. Uses the "infrastructure as farm" metaphor: your servers are **barns**, your deployed apps are **livestock**.
4
+
5
+ Built with React Ink for a responsive, keyboard-driven interface.
6
+
7
+ ## Features
8
+
9
+ - **Project management** - Switch between projects, each with its own wiki and deployments
10
+ - **Server management** - SSH into remote servers (barns) with one keystroke
11
+ - **Deployment tracking** - Track where your apps run (livestock) across local and remote environments
12
+ - **Claude Code integration** - Launch Claude sessions directly from any project context
13
+ - **GitHub issues** - View and manage issues without leaving the terminal
14
+ - **Per-project wiki** - Markdown knowledge base for each project
15
+ - **Vim-style navigation** - `j/k` to move, `g/G` for first/last, `Enter` to select
16
+
17
+ ## Requirements
18
+
19
+ - Node.js 20+
20
+ - tmux (for Claude and shell sessions)
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install -g @colmbus72/yeehaw
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ 1. Create config directory:
31
+ ```bash
32
+ mkdir -p ~/.yeehaw/projects ~/.yeehaw/barns
33
+ ```
34
+
35
+ 2. Add a project (`~/.yeehaw/projects/myapp.yaml`):
36
+ ```yaml
37
+ name: myapp
38
+ path: ~/Code/myapp
39
+ summary: My web application
40
+ livestock:
41
+ - name: local
42
+ path: ~/Code/myapp
43
+ - name: production
44
+ path: /var/www/myapp
45
+ barn: prod-server
46
+ ```
47
+
48
+ 3. Add a barn/server (`~/.yeehaw/barns/prod-server.yaml`):
49
+ ```yaml
50
+ name: prod-server
51
+ host: myserver.com
52
+ user: deploy
53
+ port: 22
54
+ identity_file: ~/.ssh/id_rsa
55
+ ```
56
+
57
+ 4. Run:
58
+ ```bash
59
+ yeehaw
60
+ ```
61
+
62
+ ## Keyboard Shortcuts
63
+
64
+ ### Global
65
+ | Key | Action |
66
+ |-----|--------|
67
+ | `j/k` | Navigate up/down |
68
+ | `g/G` | Go to first/last |
69
+ | `Tab` | Switch panel |
70
+ | `Enter` | Select item |
71
+ | `?` | Toggle help |
72
+ | `q` | Back / Detach |
73
+ | `Q` | Quit (from dashboard) |
74
+
75
+ ### Actions
76
+ | Key | Action |
77
+ |-----|--------|
78
+ | `n` | New item (context-aware) |
79
+ | `e` | Edit selected |
80
+ | `d` | Delete selected |
81
+ | `c` | New Claude session |
82
+ | `s` | Open shell session |
83
+ | `w` | Open wiki (in project) |
84
+ | `i` | Open issues (in project) |
85
+ | `1-9` | Quick switch tmux window |
86
+
87
+ ## Concepts
88
+
89
+ ### Projects
90
+ A project is a codebase you work on. Each project can have:
91
+ - A local path to the source code
92
+ - A wiki for documentation
93
+ - Livestock (deployments) across different environments
94
+
95
+ ### Barns
96
+ A barn is a server - either your local machine or a remote server you SSH into. Barns can host multiple livestock from different projects.
97
+
98
+ ### Livestock
99
+ Livestock are deployed instances of your projects. A project might have:
100
+ - `local` - your development machine
101
+ - `staging` - a test server
102
+ - `production` - live deployment
103
+
104
+ Each livestock entry tracks the path where the app lives and which barn it's on.
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ # Install dependencies
110
+ npm install
111
+
112
+ # Run in development mode
113
+ npm run dev
114
+
115
+ # Build
116
+ npm run build
117
+
118
+ # Type check
119
+ npm run typecheck
120
+ ```
121
+
122
+ ## License
123
+
124
+ MIT
package/dist/app.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function App(): import("react/jsx-runtime").JSX.Element;
package/dist/app.js ADDED
@@ -0,0 +1,414 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback } from 'react';
3
+ import { Box, Text, useApp, useInput, useStdout } from 'ink';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ import { HelpOverlay } from './components/HelpOverlay.js';
7
+ import { BottomBar } from './components/BottomBar.js';
8
+ import { GlobalDashboard } from './views/GlobalDashboard.js';
9
+ import { ProjectContext } from './views/ProjectContext.js';
10
+ import { BarnContext } from './views/BarnContext.js';
11
+ import { WikiView } from './views/WikiView.js';
12
+ import { IssuesView } from './views/IssuesView.js';
13
+ import { LivestockDetailView } from './views/LivestockDetailView.js';
14
+ import { LogsView } from './views/LogsView.js';
15
+ import { NightSkyView } from './views/NightSkyView.js';
16
+ import { useConfig } from './hooks/useConfig.js';
17
+ import { useSessions } from './hooks/useSessions.js';
18
+ import { useRemoteYeehaw } from './hooks/useRemoteYeehaw.js';
19
+ import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, enterRemoteMode, } from './lib/tmux.js';
20
+ import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig } from './lib/config.js';
21
+ function getHotkeyScope(view) {
22
+ switch (view.type) {
23
+ case 'global': return 'global-dashboard';
24
+ case 'project': return 'project-context';
25
+ case 'barn': return 'barn-context';
26
+ case 'wiki': return 'wiki-view';
27
+ case 'issues': return 'issues-view';
28
+ case 'livestock': return 'livestock-detail';
29
+ case 'logs': return 'logs-view';
30
+ case 'night-sky': return 'night-sky';
31
+ default: return 'global-dashboard';
32
+ }
33
+ }
34
+ function expandPath(path) {
35
+ if (path.startsWith('~/')) {
36
+ return join(homedir(), path.slice(2));
37
+ }
38
+ return path;
39
+ }
40
+ // Global bottom bar items by view type
41
+ function getBottomBarItems(viewType) {
42
+ const common = [
43
+ { key: 'Tab', label: 'switch' },
44
+ { key: '?', label: 'help' },
45
+ ];
46
+ if (viewType === 'global') {
47
+ return [...common, { key: 'c', label: 'claude' }, { key: 'q', label: 'detach' }, { key: 'Q', label: 'quit' }];
48
+ }
49
+ if (viewType === 'project') {
50
+ return [...common, { key: 'c', label: 'claude' }, { key: 'w', label: 'wiki' }, { key: 'i', label: 'issues' }, { key: 'q', label: 'back' }];
51
+ }
52
+ if (viewType === 'barn') {
53
+ return [...common, { key: 's', label: 'shell' }, { key: 'q', label: 'back' }];
54
+ }
55
+ if (viewType === 'wiki') {
56
+ return [...common, { key: 'q', label: 'back' }];
57
+ }
58
+ if (viewType === 'issues') {
59
+ return [...common, { key: 'r', label: 'refresh' }, { key: 'o', label: 'open' }, { key: 'q', label: 'back' }];
60
+ }
61
+ if (viewType === 'livestock') {
62
+ return [...common, { key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: 'q', label: 'back' }];
63
+ }
64
+ if (viewType === 'logs') {
65
+ return [...common, { key: 'r', label: 'refresh' }, { key: 'q', label: 'back' }];
66
+ }
67
+ if (viewType === 'night-sky') {
68
+ return [{ key: 'c', label: 'cloud' }, { key: 'r', label: 'randomize' }, { key: 'Esc', label: 'exit' }];
69
+ }
70
+ return [...common, { key: 'q', label: 'back' }];
71
+ }
72
+ export function App() {
73
+ const { exit } = useApp();
74
+ const { projects, barns, reload } = useConfig();
75
+ const { windows, createClaude, attachToWindow } = useSessions();
76
+ const { stdout } = useStdout();
77
+ const { environments, isDetecting } = useRemoteYeehaw(barns);
78
+ const [view, setView] = useState({ type: 'global' });
79
+ const [previousView, setPreviousView] = useState(null);
80
+ const [showHelp, setShowHelp] = useState(false);
81
+ const [error, setError] = useState(null);
82
+ const [pendingGo, setPendingGo] = useState(false); // For g+number sequence
83
+ // Get terminal height for full-height layout
84
+ const terminalHeight = stdout?.rows || 24;
85
+ // Check tmux availability
86
+ const tmuxAvailable = hasTmux();
87
+ const handleSelectProject = useCallback((project) => {
88
+ setView({ type: 'project', project });
89
+ updateStatusBar(project.name);
90
+ }, []);
91
+ const handleSelectBarn = useCallback((barn) => {
92
+ setView({ type: 'barn', barn });
93
+ updateStatusBar(`Barn: ${barn.name}`);
94
+ }, []);
95
+ const handleBack = useCallback(() => {
96
+ setView({ type: 'global' });
97
+ updateStatusBar();
98
+ }, []);
99
+ const handleSelectWindow = useCallback((window) => {
100
+ attachToWindow(window.index);
101
+ }, [attachToWindow]);
102
+ const handleNewClaude = useCallback(() => {
103
+ if (!tmuxAvailable) {
104
+ setError('tmux is not installed');
105
+ return;
106
+ }
107
+ const projectName = view.type === 'project' ? view.project.name : 'yeehaw';
108
+ const workingDir = view.type === 'project' ? expandPath(view.project.path) : process.cwd();
109
+ const windowName = `${projectName}-claude`;
110
+ const windowIndex = createClaude(workingDir, windowName);
111
+ switchToWindow(windowIndex);
112
+ }, [tmuxAvailable, view, createClaude]);
113
+ const handleOpenLivestockSession = useCallback((livestock, barn, projectName) => {
114
+ if (!tmuxAvailable) {
115
+ setError('tmux is not installed');
116
+ return;
117
+ }
118
+ const windowName = `${projectName}-${livestock.name}`;
119
+ if (barn && !isLocalBarn(barn)) {
120
+ // Remote livestock - SSH into it
121
+ if (!hasValidSshConfig(barn)) {
122
+ setError(`Barn '${barn.name}' is missing SSH configuration`);
123
+ return;
124
+ }
125
+ const windowIndex = createSshWindow(windowName, barn.host, barn.user, barn.port, barn.identity_file, livestock.path);
126
+ switchToWindow(windowIndex);
127
+ }
128
+ else {
129
+ // Local livestock - open shell window
130
+ const workingDir = expandPath(livestock.path);
131
+ const windowIndex = createShellWindow(workingDir, windowName);
132
+ switchToWindow(windowIndex);
133
+ }
134
+ }, [tmuxAvailable]);
135
+ const handleCreateProject = useCallback((name, path) => {
136
+ const project = {
137
+ name,
138
+ path,
139
+ livestock: [],
140
+ };
141
+ saveProject(project);
142
+ reload();
143
+ }, [reload]);
144
+ const handleUpdateProject = useCallback((updatedProject) => {
145
+ saveProject(updatedProject);
146
+ reload();
147
+ // Update the view with the new project data, preserving wiki view if active
148
+ setView((currentView) => {
149
+ if (currentView.type === 'wiki') {
150
+ return { type: 'wiki', project: updatedProject };
151
+ }
152
+ return { type: 'project', project: updatedProject };
153
+ });
154
+ }, [reload]);
155
+ const handleOpenWiki = useCallback((project) => {
156
+ setView({ type: 'wiki', project });
157
+ updateStatusBar(`${project.name} Wiki`);
158
+ }, []);
159
+ const handleOpenIssues = useCallback((project) => {
160
+ setView({ type: 'issues', project });
161
+ updateStatusBar(`${project.name} Issues`);
162
+ }, []);
163
+ const handleBackFromSubview = useCallback((project) => {
164
+ setView({ type: 'project', project });
165
+ updateStatusBar(project.name);
166
+ }, []);
167
+ const handleOpenLivestockDetail = useCallback((project, livestock) => {
168
+ setView({ type: 'livestock', project, livestock });
169
+ updateStatusBar(`${project.name} / ${livestock.name}`);
170
+ }, []);
171
+ const handleOpenLogs = useCallback((project, livestock) => {
172
+ setView({ type: 'logs', project, livestock });
173
+ updateStatusBar(`${project.name} / ${livestock.name} Logs`);
174
+ }, []);
175
+ const handleBackFromLivestock = useCallback((project) => {
176
+ setView({ type: 'project', project });
177
+ updateStatusBar(project.name);
178
+ }, []);
179
+ const handleUpdateLivestock = useCallback((project, updatedLivestock) => {
180
+ const updatedProject = {
181
+ ...project,
182
+ livestock: (project.livestock || []).map((l) => l.name === updatedLivestock.name ? updatedLivestock : l),
183
+ };
184
+ saveProject(updatedProject);
185
+ reload();
186
+ // Update the view with the new livestock data
187
+ setView({ type: 'livestock', project: updatedProject, livestock: updatedLivestock });
188
+ }, [reload]);
189
+ const handleDeleteProject = useCallback((projectName) => {
190
+ deleteProject(projectName);
191
+ reload();
192
+ // Go back to global view after deletion
193
+ setView({ type: 'global' });
194
+ updateStatusBar();
195
+ }, [reload]);
196
+ const handleCreateBarn = useCallback((barn) => {
197
+ saveBarn(barn);
198
+ reload();
199
+ }, [reload]);
200
+ const handleUpdateBarn = useCallback((updatedBarn) => {
201
+ saveBarn(updatedBarn);
202
+ reload();
203
+ // Update the view with the new barn data
204
+ setView({ type: 'barn', barn: updatedBarn });
205
+ }, [reload]);
206
+ const handleDeleteBarn = useCallback((barnName) => {
207
+ deleteBarn(barnName);
208
+ reload();
209
+ // Go back to global view after deletion
210
+ setView({ type: 'global' });
211
+ updateStatusBar();
212
+ }, [reload]);
213
+ const handleSshToBarn = useCallback((barn) => {
214
+ if (!tmuxAvailable) {
215
+ setError('tmux is not installed');
216
+ return;
217
+ }
218
+ if (isLocalBarn(barn)) {
219
+ // Local barn - just open a shell in home directory
220
+ const windowName = `barn-${barn.name}`;
221
+ const windowIndex = createShellWindow(homedir(), windowName);
222
+ switchToWindow(windowIndex);
223
+ return;
224
+ }
225
+ if (!hasValidSshConfig(barn)) {
226
+ setError(`Barn '${barn.name}' is missing SSH configuration`);
227
+ return;
228
+ }
229
+ const windowName = `barn-${barn.name}`;
230
+ const windowIndex = createSshWindow(windowName, barn.host, barn.user, barn.port, barn.identity_file, '~' // SSH to home directory
231
+ );
232
+ switchToWindow(windowIndex);
233
+ }, [tmuxAvailable]);
234
+ const handleEnterNightSky = useCallback(() => {
235
+ setPreviousView(view);
236
+ setView({ type: 'night-sky' });
237
+ updateStatusBar('Night Sky');
238
+ }, [view]);
239
+ const handleExitNightSky = useCallback(() => {
240
+ if (previousView) {
241
+ setView(previousView);
242
+ setPreviousView(null);
243
+ // Restore status bar based on previous view
244
+ if (previousView.type === 'project') {
245
+ updateStatusBar(previousView.project.name);
246
+ }
247
+ else if (previousView.type === 'barn') {
248
+ updateStatusBar(`Barn: ${previousView.barn.name}`);
249
+ }
250
+ else {
251
+ updateStatusBar();
252
+ }
253
+ }
254
+ else {
255
+ setView({ type: 'global' });
256
+ updateStatusBar();
257
+ }
258
+ }, [previousView]);
259
+ const handleConnectToRemote = useCallback((envIndex) => {
260
+ if (!tmuxAvailable) {
261
+ setError('tmux is not installed');
262
+ return;
263
+ }
264
+ const env = environments[envIndex];
265
+ if (!env || env.state !== 'available') {
266
+ setError('Remote Yeehaw not available');
267
+ return;
268
+ }
269
+ const { barn } = env;
270
+ if (!barn.host || !barn.user || !barn.port || !barn.identity_file) {
271
+ setError(`Barn '${barn.name}' is missing SSH configuration`);
272
+ return;
273
+ }
274
+ enterRemoteMode(barn.name, barn.host, barn.user, barn.port, barn.identity_file);
275
+ }, [tmuxAvailable, environments]);
276
+ useInput((input, key) => {
277
+ // Clear error on any input
278
+ if (error)
279
+ setError(null);
280
+ // Help toggle
281
+ if (input === '?') {
282
+ setShowHelp((s) => !s);
283
+ return;
284
+ }
285
+ // Don't process other keys when help is shown
286
+ if (showHelp) {
287
+ if (key.escape)
288
+ setShowHelp(false);
289
+ return;
290
+ }
291
+ // Global shortcuts
292
+ if (input === 'q') {
293
+ if (view.type === 'wiki' || view.type === 'issues') {
294
+ handleBackFromSubview(view.project);
295
+ }
296
+ else if (view.type === 'logs') {
297
+ // Back to livestock detail
298
+ setView({ type: 'livestock', project: view.project, livestock: view.livestock });
299
+ updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
300
+ }
301
+ else if (view.type === 'livestock') {
302
+ handleBackFromLivestock(view.project);
303
+ }
304
+ else if (view.type === 'project' || view.type === 'barn') {
305
+ handleBack();
306
+ }
307
+ else {
308
+ // Detach from session (keeps yeehaw running in background)
309
+ // Don't call exit() - let the TUI keep running so reattach works
310
+ detachFromSession();
311
+ }
312
+ return;
313
+ }
314
+ // Shift-Q: Kill everything
315
+ if (input === 'Q' && view.type === 'global') {
316
+ killYeehawSession();
317
+ exit();
318
+ return;
319
+ }
320
+ if (key.escape) {
321
+ if (view.type === 'wiki' || view.type === 'issues') {
322
+ handleBackFromSubview(view.project);
323
+ }
324
+ else if (view.type === 'logs') {
325
+ // Back to livestock detail
326
+ setView({ type: 'livestock', project: view.project, livestock: view.livestock });
327
+ updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
328
+ }
329
+ else if (view.type === 'livestock') {
330
+ handleBackFromLivestock(view.project);
331
+ }
332
+ else if (view.type === 'project' || view.type === 'barn') {
333
+ handleBack();
334
+ }
335
+ return;
336
+ }
337
+ // g+number: Connect to remote environment (two-key sequence)
338
+ if (pendingGo) {
339
+ setPendingGo(false);
340
+ if (/^[1-9]$/.test(input)) {
341
+ const envIndex = parseInt(input, 10) - 1;
342
+ if (envIndex < environments.length) {
343
+ handleConnectToRemote(envIndex);
344
+ }
345
+ }
346
+ // Any key after 'g' clears the pending state
347
+ return;
348
+ }
349
+ // 'g' initiates the go sequence (only when environments exist)
350
+ if (input === 'g' && environments.length > 0) {
351
+ setPendingGo(true);
352
+ return;
353
+ }
354
+ // v: Enter night sky visualizer (only from global dashboard to avoid text input conflicts)
355
+ if (input === 'v' && view.type === 'global') {
356
+ handleEnterNightSky();
357
+ return;
358
+ }
359
+ });
360
+ // Render based on view type
361
+ const renderView = () => {
362
+ if (showHelp) {
363
+ return _jsx(HelpOverlay, { scope: getHotkeyScope(view) });
364
+ }
365
+ switch (view.type) {
366
+ case 'global':
367
+ return (_jsx(GlobalDashboard, { projects: projects, barns: barns, windows: windows, onSelectProject: handleSelectProject, onSelectBarn: handleSelectBarn, onSelectWindow: handleSelectWindow, onNewClaude: handleNewClaude, onCreateProject: handleCreateProject, onCreateBarn: handleCreateBarn, onSshToBarn: handleSshToBarn }));
368
+ case 'project':
369
+ return (_jsx(ProjectContext, { project: view.project, barns: barns, windows: windows, onBack: handleBack, onNewClaude: handleNewClaude, onSelectWindow: handleSelectWindow, onSelectLivestock: (livestock, barn) => handleOpenLivestockDetail(view.project, livestock), onOpenLivestockSession: (livestock, barn) => handleOpenLivestockSession(livestock, barn, view.project.name), onUpdateProject: handleUpdateProject, onDeleteProject: handleDeleteProject, onOpenWiki: () => handleOpenWiki(view.project), onOpenIssues: () => handleOpenIssues(view.project) }));
370
+ case 'barn':
371
+ const barnLivestock = getLivestockForBarn(view.barn.name);
372
+ return (_jsx(BarnContext, { barn: view.barn, livestock: barnLivestock, projects: projects, windows: windows, onBack: handleBack, onSshToBarn: () => handleSshToBarn(view.barn), onSelectLivestock: (project, livestock) => handleOpenLivestockDetail(project, livestock), onOpenLivestockSession: (project, livestock) => {
373
+ const barn = barns.find((b) => b.name === livestock.barn) || null;
374
+ handleOpenLivestockSession(livestock, barn, project.name);
375
+ }, onUpdateBarn: handleUpdateBarn, onDeleteBarn: handleDeleteBarn, onAddLivestock: (project, livestock) => {
376
+ // Add livestock to project
377
+ const updatedLivestock = [...(project.livestock || [])];
378
+ const existingIdx = updatedLivestock.findIndex((l) => l.name === livestock.name);
379
+ if (existingIdx >= 0) {
380
+ updatedLivestock[existingIdx] = livestock;
381
+ }
382
+ else {
383
+ updatedLivestock.push(livestock);
384
+ }
385
+ const updatedProject = { ...project, livestock: updatedLivestock };
386
+ saveProject(updatedProject);
387
+ reload();
388
+ }, onRemoveLivestock: (project, livestockName) => {
389
+ // Remove livestock from project
390
+ const updatedLivestock = (project.livestock || []).filter((l) => l.name !== livestockName);
391
+ const updatedProject = { ...project, livestock: updatedLivestock };
392
+ saveProject(updatedProject);
393
+ reload();
394
+ } }));
395
+ case 'wiki':
396
+ return (_jsx(WikiView, { project: view.project, onBack: () => handleBackFromSubview(view.project), onUpdateProject: handleUpdateProject }));
397
+ case 'issues':
398
+ return (_jsx(IssuesView, { project: view.project, onBack: () => handleBackFromSubview(view.project) }));
399
+ case 'livestock':
400
+ return (_jsx(LivestockDetailView, { project: view.project, livestock: view.livestock, onBack: () => handleBackFromLivestock(view.project), onOpenLogs: () => handleOpenLogs(view.project, view.livestock), onOpenSession: () => {
401
+ const barn = barns.find((b) => b.name === view.livestock.barn) || null;
402
+ handleOpenLivestockSession(view.livestock, barn, view.project.name);
403
+ }, onUpdateLivestock: (updatedLivestock) => handleUpdateLivestock(view.project, updatedLivestock) }));
404
+ case 'logs':
405
+ return (_jsx(LogsView, { project: view.project, livestock: view.livestock, onBack: () => {
406
+ setView({ type: 'livestock', project: view.project, livestock: view.livestock });
407
+ updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
408
+ } }));
409
+ case 'night-sky':
410
+ return (_jsx(NightSkyView, { onExit: handleExitNightSky }));
411
+ }
412
+ };
413
+ return (_jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [error && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: renderView() }), !showHelp && (_jsx(BottomBar, { items: getBottomBarItems(view.type), environments: environments, isDetecting: isDetecting }))] }));
414
+ }
@@ -0,0 +1,6 @@
1
+ interface BarnHeaderProps {
2
+ name: string;
3
+ subtitle?: string;
4
+ }
5
+ export declare function BarnHeader({ name, subtitle }: BarnHeaderProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const BARN_DOOR_ART = `
4
+ ╔═══════╦═══════╗
5
+ ║ ╲ ╱ ║ ╲ ╱ ║
6
+ ║ ╳ ║ ╳ ║
7
+ ║ ╱ ╲ ║ ╱ ╲ ║
8
+ ╚═══════╩═══════╝
9
+ `.trim();
10
+ // Grey gradient from light to dark
11
+ const GREY_GRADIENT = [
12
+ 'rgb(160,160,160)',
13
+ 'rgb(130,130,130)',
14
+ 'rgb(100,100,100)',
15
+ 'rgb(80,80,80)',
16
+ 'rgb(60,60,60)',
17
+ ];
18
+ export function BarnHeader({ name, subtitle }) {
19
+ const lines = BARN_DOOR_ART.split('\n');
20
+ return (_jsx(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: _jsxs(Box, { children: [_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: GREY_GRADIENT[i] || GREY_GRADIENT[GREY_GRADIENT.length - 1], children: line }, i))) }), _jsxs(Box, { flexDirection: "column", marginLeft: 3, justifyContent: "center", children: [_jsx(Text, { bold: true, color: "rgb(200,200,200)", children: name.toUpperCase() }), subtitle && (_jsx(Text, { dimColor: true, children: subtitle }))] })] }) }));
21
+ }
@@ -0,0 +1,16 @@
1
+ interface RemoteEnvironment {
2
+ barn: {
3
+ name: string;
4
+ };
5
+ state: 'available' | 'not-checked' | 'checking' | 'unavailable' | 'unreachable';
6
+ }
7
+ interface BottomBarProps {
8
+ items: Array<{
9
+ key: string;
10
+ label: string;
11
+ }>;
12
+ environments?: RemoteEnvironment[];
13
+ isDetecting?: boolean;
14
+ }
15
+ export declare function BottomBar({ items, environments, isDetecting }: BottomBarProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ // Yeehaw brand gold
4
+ const BRAND_COLOR = '#f0c040';
5
+ export function BottomBar({ items, environments = [], isDetecting = false }) {
6
+ return (_jsxs(Box, { paddingX: 2, justifyContent: "space-between", children: [_jsx(Box, { gap: 2, children: items.map((item, i) => (_jsxs(Text, { children: [_jsx(Text, { color: BRAND_COLOR, children: item.key }), _jsxs(Text, { dimColor: true, children: [" ", item.label] })] }, i))) }), _jsxs(Box, { gap: 2, children: [_jsx(Text, { children: _jsx(Text, { color: "green", children: "[Local]" }) }), environments.map((env, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: BRAND_COLOR, children: ["g", i + 1] }), _jsxs(Text, { dimColor: true, children: [" ", env.barn.name] })] }, env.barn.name))), isDetecting && (_jsx(Text, { dimColor: true, children: "..." }))] })] }));
7
+ }
@@ -0,0 +1,8 @@
1
+ interface HeaderProps {
2
+ text: string;
3
+ subtitle?: string;
4
+ summary?: string;
5
+ color?: string;
6
+ }
7
+ export declare function Header({ text, subtitle, summary, color }: HeaderProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};