@colmbus72/yeehaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +414 -0
- package/dist/components/BarnHeader.d.ts +6 -0
- package/dist/components/BarnHeader.js +21 -0
- package/dist/components/BottomBar.d.ts +16 -0
- package/dist/components/BottomBar.js +7 -0
- package/dist/components/Header.d.ts +8 -0
- package/dist/components/Header.js +83 -0
- package/dist/components/HelpOverlay.d.ts +7 -0
- package/dist/components/HelpOverlay.js +17 -0
- package/dist/components/List.d.ts +17 -0
- package/dist/components/List.js +53 -0
- package/dist/components/Markdown.d.ts +8 -0
- package/dist/components/Markdown.js +23 -0
- package/dist/components/Panel.d.ts +10 -0
- package/dist/components/Panel.js +5 -0
- package/dist/components/PathInput.d.ts +9 -0
- package/dist/components/PathInput.js +141 -0
- package/dist/components/ScrollableMarkdown.d.ts +11 -0
- package/dist/components/ScrollableMarkdown.js +56 -0
- package/dist/components/StatusBar.d.ts +5 -0
- package/dist/components/StatusBar.js +20 -0
- package/dist/components/TextArea.d.ts +17 -0
- package/dist/components/TextArea.js +140 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +5 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/useConfig.d.ts +11 -0
- package/dist/hooks/useConfig.js +36 -0
- package/dist/hooks/useRemoteYeehaw.d.ts +13 -0
- package/dist/hooks/useRemoteYeehaw.js +49 -0
- package/dist/hooks/useSessions.d.ts +11 -0
- package/dist/hooks/useSessions.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +34 -0
- package/dist/lib/config.d.ts +27 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/detection.d.ts +16 -0
- package/dist/lib/detection.js +41 -0
- package/dist/lib/editor.d.ts +5 -0
- package/dist/lib/editor.js +35 -0
- package/dist/lib/errors.d.ts +28 -0
- package/dist/lib/errors.js +48 -0
- package/dist/lib/git.d.ts +11 -0
- package/dist/lib/git.js +73 -0
- package/dist/lib/github.d.ts +43 -0
- package/dist/lib/github.js +111 -0
- package/dist/lib/hotkeys.d.ts +27 -0
- package/dist/lib/hotkeys.js +92 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/livestock.d.ts +51 -0
- package/dist/lib/livestock.js +233 -0
- package/dist/lib/mcp-validation.d.ts +33 -0
- package/dist/lib/mcp-validation.js +62 -0
- package/dist/lib/paths.d.ts +8 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/shell.d.ts +34 -0
- package/dist/lib/shell.js +61 -0
- package/dist/lib/ssh.d.ts +15 -0
- package/dist/lib/ssh.js +77 -0
- package/dist/lib/tmux-config.d.ts +3 -0
- package/dist/lib/tmux-config.js +42 -0
- package/dist/lib/tmux.d.ts +32 -0
- package/dist/lib/tmux.js +397 -0
- package/dist/mcp-server.d.ts +23 -0
- package/dist/mcp-server.js +825 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +2 -0
- package/dist/views/BarnContext.d.ts +22 -0
- package/dist/views/BarnContext.js +252 -0
- package/dist/views/GlobalDashboard.d.ts +16 -0
- package/dist/views/GlobalDashboard.js +253 -0
- package/dist/views/Home.d.ts +11 -0
- package/dist/views/Home.js +27 -0
- package/dist/views/IssuesView.d.ts +7 -0
- package/dist/views/IssuesView.js +157 -0
- package/dist/views/LivestockDetailView.d.ts +11 -0
- package/dist/views/LivestockDetailView.js +140 -0
- package/dist/views/LogsView.d.ts +8 -0
- package/dist/views/LogsView.js +84 -0
- package/dist/views/NightSkyView.d.ts +5 -0
- package/dist/views/NightSkyView.js +441 -0
- package/dist/views/ProjectContext.d.ts +18 -0
- package/dist/views/ProjectContext.js +333 -0
- package/dist/views/Projects.d.ts +8 -0
- package/dist/views/Projects.js +20 -0
- package/dist/views/WikiView.d.ts +8 -0
- package/dist/views/WikiView.js +138 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/package.json +65 -0
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,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
|
+
}
|