@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/dist/lib/tmux.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { execaSync } from 'execa';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { writeTmuxConfig, TMUX_CONFIG_PATH } from './tmux-config.js';
|
|
6
|
+
import { shellEscape } from './shell.js';
|
|
7
|
+
// Get the path to the MCP server (it's in dist/, not dist/lib/)
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const MCP_SERVER_PATH = join(__dirname, '..', 'mcp-server.js');
|
|
11
|
+
export const YEEHAW_SESSION = 'yeehaw';
|
|
12
|
+
// Remote mode state tracking
|
|
13
|
+
let remoteWindowIndex = null;
|
|
14
|
+
// Keys to unbind when entering remote mode (so they pass through to inner tmux)
|
|
15
|
+
const REMOTE_MODE_UNBIND_KEYS = ['C-h', 'C-l', 'C-y'];
|
|
16
|
+
export function hasTmux() {
|
|
17
|
+
try {
|
|
18
|
+
execaSync('which', ['tmux']);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function isInsideYeehawSession() {
|
|
26
|
+
const tmuxEnv = process.env.TMUX;
|
|
27
|
+
if (!tmuxEnv)
|
|
28
|
+
return false;
|
|
29
|
+
// Check if we're in the yeehaw session
|
|
30
|
+
try {
|
|
31
|
+
const result = execaSync('tmux', ['display-message', '-p', '#{session_name}']);
|
|
32
|
+
return result.stdout.trim() === YEEHAW_SESSION;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function yeehawSessionExists() {
|
|
39
|
+
try {
|
|
40
|
+
execaSync('tmux', ['has-session', '-t', YEEHAW_SESSION]);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function createYeehawSession() {
|
|
48
|
+
// Write the tmux config
|
|
49
|
+
writeTmuxConfig();
|
|
50
|
+
// Create the session with window 0 named "yeehaw"
|
|
51
|
+
execaSync('tmux', [
|
|
52
|
+
'new-session',
|
|
53
|
+
'-d',
|
|
54
|
+
'-s', YEEHAW_SESSION,
|
|
55
|
+
'-n', 'yeehaw',
|
|
56
|
+
]);
|
|
57
|
+
// Source the config
|
|
58
|
+
execaSync('tmux', ['source-file', TMUX_CONFIG_PATH]);
|
|
59
|
+
// Set up hook to hide status bar in window 0
|
|
60
|
+
setupStatusBarHooks();
|
|
61
|
+
}
|
|
62
|
+
export function setupStatusBarHooks() {
|
|
63
|
+
// Hide status bar when in window 0, show in other windows
|
|
64
|
+
try {
|
|
65
|
+
// Start with status off (we begin in window 0)
|
|
66
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'off']);
|
|
67
|
+
// Hook for when window changes
|
|
68
|
+
execaSync('tmux', [
|
|
69
|
+
'set-hook', '-t', YEEHAW_SESSION,
|
|
70
|
+
'after-select-window',
|
|
71
|
+
'if-shell -F "#{==:#{window_index},0}" "set status off" "set status on"'
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Hooks might fail on older tmux versions, not critical
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function attachToYeehaw() {
|
|
79
|
+
// This replaces the current process
|
|
80
|
+
spawnSync('tmux', ['attach-session', '-t', YEEHAW_SESSION], {
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
});
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
// All yeehaw MCP tools that should be auto-approved
|
|
86
|
+
const YEEHAW_MCP_TOOLS = [
|
|
87
|
+
'mcp__yeehaw__list_projects',
|
|
88
|
+
'mcp__yeehaw__get_project',
|
|
89
|
+
'mcp__yeehaw__create_project',
|
|
90
|
+
'mcp__yeehaw__update_project',
|
|
91
|
+
'mcp__yeehaw__delete_project',
|
|
92
|
+
'mcp__yeehaw__add_livestock',
|
|
93
|
+
'mcp__yeehaw__remove_livestock',
|
|
94
|
+
'mcp__yeehaw__list_barns',
|
|
95
|
+
'mcp__yeehaw__get_barn',
|
|
96
|
+
'mcp__yeehaw__create_barn',
|
|
97
|
+
'mcp__yeehaw__update_barn',
|
|
98
|
+
'mcp__yeehaw__delete_barn',
|
|
99
|
+
'mcp__yeehaw__get_wiki',
|
|
100
|
+
'mcp__yeehaw__add_wiki_section',
|
|
101
|
+
'mcp__yeehaw__update_wiki_section',
|
|
102
|
+
'mcp__yeehaw__delete_wiki_section',
|
|
103
|
+
];
|
|
104
|
+
export function createClaudeWindow(workingDir, windowName) {
|
|
105
|
+
// Build MCP config for yeehaw server
|
|
106
|
+
const mcpConfig = JSON.stringify({
|
|
107
|
+
mcpServers: {
|
|
108
|
+
yeehaw: {
|
|
109
|
+
command: 'node',
|
|
110
|
+
args: [MCP_SERVER_PATH],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
// Build allowed tools list for auto-approval
|
|
115
|
+
const allowedTools = YEEHAW_MCP_TOOLS.join(',');
|
|
116
|
+
// Create new window running claude with yeehaw MCP server (-a appends after current window)
|
|
117
|
+
// Use shell escaping to safely handle special characters in JSON
|
|
118
|
+
const claudeCmd = `claude --mcp-config ${shellEscape(mcpConfig)} --allowedTools ${shellEscape(allowedTools)}`;
|
|
119
|
+
execaSync('tmux', [
|
|
120
|
+
'new-window',
|
|
121
|
+
'-a',
|
|
122
|
+
'-t', YEEHAW_SESSION,
|
|
123
|
+
'-n', windowName,
|
|
124
|
+
'-c', workingDir,
|
|
125
|
+
claudeCmd,
|
|
126
|
+
]);
|
|
127
|
+
// Get the window index we just created (new window is now current)
|
|
128
|
+
const result = execaSync('tmux', [
|
|
129
|
+
'display-message', '-p', '#{window_index}'
|
|
130
|
+
]);
|
|
131
|
+
return parseInt(result.stdout.trim(), 10);
|
|
132
|
+
}
|
|
133
|
+
export function createShellWindow(workingDir, windowName, shell = '/bin/zsh') {
|
|
134
|
+
// Create new window running shell (-a appends after current window)
|
|
135
|
+
execaSync('tmux', [
|
|
136
|
+
'new-window',
|
|
137
|
+
'-a',
|
|
138
|
+
'-t', YEEHAW_SESSION,
|
|
139
|
+
'-n', windowName,
|
|
140
|
+
'-c', workingDir,
|
|
141
|
+
shell,
|
|
142
|
+
]);
|
|
143
|
+
// Get the window index we just created (new window is now current)
|
|
144
|
+
const result = execaSync('tmux', [
|
|
145
|
+
'display-message', '-p', '#{window_index}'
|
|
146
|
+
]);
|
|
147
|
+
return parseInt(result.stdout.trim(), 10);
|
|
148
|
+
}
|
|
149
|
+
export function createSshWindow(windowName, host, user, port, identityFile, remotePath) {
|
|
150
|
+
// Two levels of escaping needed:
|
|
151
|
+
// 1. Remote shell sees: cd /path && exec $SHELL -l
|
|
152
|
+
// 2. Local shell (tmux) sees: ssh ... -t 'cd /path && ...'
|
|
153
|
+
// Build the remote command - escape remotePath for the remote shell
|
|
154
|
+
const remoteCmd = `cd ${shellEscape(remotePath)} && exec $SHELL -l`;
|
|
155
|
+
// Build SSH command parts - escape for local shell (tmux passes to sh)
|
|
156
|
+
const sshParts = [
|
|
157
|
+
'ssh',
|
|
158
|
+
'-p', String(port),
|
|
159
|
+
'-i', shellEscape(identityFile),
|
|
160
|
+
shellEscape(`${user}@${host}`),
|
|
161
|
+
'-t',
|
|
162
|
+
shellEscape(remoteCmd) // Double-escaped: once for remote, once for local
|
|
163
|
+
];
|
|
164
|
+
const sshCmd = sshParts.join(' ');
|
|
165
|
+
execaSync('tmux', [
|
|
166
|
+
'new-window',
|
|
167
|
+
'-a',
|
|
168
|
+
'-t', YEEHAW_SESSION,
|
|
169
|
+
'-n', windowName,
|
|
170
|
+
sshCmd,
|
|
171
|
+
]);
|
|
172
|
+
const result = execaSync('tmux', [
|
|
173
|
+
'display-message', '-p', '#{window_index}'
|
|
174
|
+
]);
|
|
175
|
+
return parseInt(result.stdout.trim(), 10);
|
|
176
|
+
}
|
|
177
|
+
export function detachFromSession() {
|
|
178
|
+
execaSync('tmux', ['detach-client']);
|
|
179
|
+
}
|
|
180
|
+
export function killYeehawSession() {
|
|
181
|
+
try {
|
|
182
|
+
execaSync('tmux', ['kill-session', '-t', YEEHAW_SESSION]);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Session might already be dead
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function switchToWindow(windowIndex) {
|
|
189
|
+
execaSync('tmux', ['select-window', '-t', `${YEEHAW_SESSION}:${windowIndex}`]);
|
|
190
|
+
}
|
|
191
|
+
export function listYeehawWindows() {
|
|
192
|
+
try {
|
|
193
|
+
// Use tab as delimiter since pane_title can contain colons
|
|
194
|
+
const result = execaSync('tmux', [
|
|
195
|
+
'list-windows',
|
|
196
|
+
'-t', YEEHAW_SESSION,
|
|
197
|
+
'-F', '#{window_index}\t#{window_name}\t#{window_active}\t#{pane_title}\t#{pane_current_command}\t#{window_activity}',
|
|
198
|
+
]);
|
|
199
|
+
return result.stdout
|
|
200
|
+
.split('\n')
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.map((line) => {
|
|
203
|
+
const [index, name, active, paneTitle, paneCurrentCommand, windowActivity] = line.split('\t');
|
|
204
|
+
return {
|
|
205
|
+
index: parseInt(index, 10),
|
|
206
|
+
name,
|
|
207
|
+
active: active === '1',
|
|
208
|
+
paneTitle: paneTitle || '',
|
|
209
|
+
paneCurrentCommand: paneCurrentCommand || '',
|
|
210
|
+
windowActivity: parseInt(windowActivity, 10) || 0,
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function killWindow(windowIndex) {
|
|
219
|
+
try {
|
|
220
|
+
execaSync('tmux', ['kill-window', '-t', `${YEEHAW_SESSION}:${windowIndex}`]);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Window might already be dead
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Format relative time from a Unix timestamp
|
|
228
|
+
*/
|
|
229
|
+
function formatRelativeTime(timestamp) {
|
|
230
|
+
const now = Math.floor(Date.now() / 1000);
|
|
231
|
+
const diff = now - timestamp;
|
|
232
|
+
if (diff < 60)
|
|
233
|
+
return 'now';
|
|
234
|
+
if (diff < 3600)
|
|
235
|
+
return `${Math.floor(diff / 60)}m`;
|
|
236
|
+
if (diff < 86400)
|
|
237
|
+
return `${Math.floor(diff / 3600)}h`;
|
|
238
|
+
return `${Math.floor(diff / 86400)}d`;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Check if a pane title indicates Claude is actively working (has spinner)
|
|
242
|
+
*/
|
|
243
|
+
function isClaudeWorking(paneTitle) {
|
|
244
|
+
// Braille spinner characters used by Claude Code
|
|
245
|
+
const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', '⠂', '⠐', '⠈'];
|
|
246
|
+
return spinnerChars.some(char => paneTitle.startsWith(char));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get formatted status text for a tmux window
|
|
250
|
+
*/
|
|
251
|
+
export function getWindowStatus(window) {
|
|
252
|
+
const isClaudeSession = window.name.includes('claude') && window.paneCurrentCommand === 'node';
|
|
253
|
+
const relativeTime = formatRelativeTime(window.windowActivity);
|
|
254
|
+
if (isClaudeSession) {
|
|
255
|
+
// For Claude sessions, use pane title (contains task context)
|
|
256
|
+
if (window.paneTitle) {
|
|
257
|
+
const working = isClaudeWorking(window.paneTitle);
|
|
258
|
+
if (working) {
|
|
259
|
+
return window.paneTitle; // Shows spinner + task description
|
|
260
|
+
}
|
|
261
|
+
// Not working - show title with idle time if stale
|
|
262
|
+
if (relativeTime !== 'now' && relativeTime !== '1m') {
|
|
263
|
+
return `${window.paneTitle} (${relativeTime})`;
|
|
264
|
+
}
|
|
265
|
+
return window.paneTitle;
|
|
266
|
+
}
|
|
267
|
+
return relativeTime === 'now' ? 'active' : `idle ${relativeTime}`;
|
|
268
|
+
}
|
|
269
|
+
// For shell sessions, show current command
|
|
270
|
+
const cmd = window.paneCurrentCommand;
|
|
271
|
+
if (cmd && cmd !== 'zsh' && cmd !== 'bash' && cmd !== 'sh' && cmd !== 'fish') {
|
|
272
|
+
// Running a specific command
|
|
273
|
+
return cmd;
|
|
274
|
+
}
|
|
275
|
+
// At shell prompt - show idle time
|
|
276
|
+
return relativeTime === 'now' ? 'ready' : `idle ${relativeTime}`;
|
|
277
|
+
}
|
|
278
|
+
export function updateStatusBar(projectName) {
|
|
279
|
+
const left = projectName
|
|
280
|
+
? `#[bold] YEEHAW | ${projectName} `
|
|
281
|
+
: '#[bold] YEEHAW ';
|
|
282
|
+
try {
|
|
283
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status-left', left]);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// Not critical if this fails
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
export function enterRemoteMode(barnName, host, user, port, identityFile) {
|
|
290
|
+
// 1. Create SSH window that attaches to remote yeehaw tmux
|
|
291
|
+
const windowName = `remote:${barnName}`;
|
|
292
|
+
const remoteCmd = 'tmux attach -t yeehaw';
|
|
293
|
+
const sshParts = [
|
|
294
|
+
'ssh',
|
|
295
|
+
'-p', String(port),
|
|
296
|
+
'-i', shellEscape(identityFile),
|
|
297
|
+
'-t', // Force TTY allocation
|
|
298
|
+
shellEscape(`${user}@${host}`),
|
|
299
|
+
shellEscape(remoteCmd)
|
|
300
|
+
];
|
|
301
|
+
const sshCmd = sshParts.join(' ');
|
|
302
|
+
execaSync('tmux', [
|
|
303
|
+
'new-window',
|
|
304
|
+
'-a',
|
|
305
|
+
'-t', YEEHAW_SESSION,
|
|
306
|
+
'-n', windowName,
|
|
307
|
+
sshCmd,
|
|
308
|
+
]);
|
|
309
|
+
const result = execaSync('tmux', [
|
|
310
|
+
'display-message', '-p', '#{window_index}'
|
|
311
|
+
]);
|
|
312
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
313
|
+
remoteWindowIndex = windowIndex;
|
|
314
|
+
// 2. Unbind navigation keys so they pass through to inner tmux
|
|
315
|
+
for (const key of REMOTE_MODE_UNBIND_KEYS) {
|
|
316
|
+
try {
|
|
317
|
+
execaSync('tmux', ['unbind-key', '-n', key]);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Key might not be bound, ignore
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// 3. Bind Ctrl-\ as escape hatch
|
|
324
|
+
try {
|
|
325
|
+
execaSync('tmux', [
|
|
326
|
+
'bind-key', '-n', 'C-\\',
|
|
327
|
+
`run-shell "tmux kill-window -t ${YEEHAW_SESSION}:${windowIndex}; exit 0"`
|
|
328
|
+
]);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Ignore errors
|
|
332
|
+
}
|
|
333
|
+
// 4. Show minimal status bar with connection info
|
|
334
|
+
try {
|
|
335
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'on']);
|
|
336
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status-left', `#[bold] Connected to: ${barnName} `]);
|
|
337
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status-right', ' Ctrl-\\ back ']);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Not critical
|
|
341
|
+
}
|
|
342
|
+
// 5. Set up hook to restore when remote window closes
|
|
343
|
+
try {
|
|
344
|
+
execaSync('tmux', [
|
|
345
|
+
'set-hook', '-t', YEEHAW_SESSION,
|
|
346
|
+
'window-unlinked',
|
|
347
|
+
`if-shell "[ ! -z \\"#{@remote_mode}\\" ]" "run-shell \\"tmux set -u @remote_mode; tmux source-file ${TMUX_CONFIG_PATH}; tmux select-window -t ${YEEHAW_SESSION}:0; tmux set status off\\""`
|
|
348
|
+
]);
|
|
349
|
+
// Mark that we're in remote mode
|
|
350
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, '@remote_mode', '1']);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Hooks might fail on older tmux
|
|
354
|
+
}
|
|
355
|
+
return windowIndex;
|
|
356
|
+
}
|
|
357
|
+
export function exitRemoteMode() {
|
|
358
|
+
// Kill the remote window if it exists
|
|
359
|
+
if (remoteWindowIndex !== null) {
|
|
360
|
+
try {
|
|
361
|
+
execaSync('tmux', ['kill-window', '-t', `${YEEHAW_SESSION}:${remoteWindowIndex}`]);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Window might already be dead
|
|
365
|
+
}
|
|
366
|
+
remoteWindowIndex = null;
|
|
367
|
+
}
|
|
368
|
+
// Restore keybindings by re-sourcing config
|
|
369
|
+
try {
|
|
370
|
+
execaSync('tmux', ['source-file', TMUX_CONFIG_PATH]);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Not critical
|
|
374
|
+
}
|
|
375
|
+
// Unbind the escape hatch
|
|
376
|
+
try {
|
|
377
|
+
execaSync('tmux', ['unbind-key', '-n', 'C-\\']);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Ignore
|
|
381
|
+
}
|
|
382
|
+
// Hide status bar and switch to window 0
|
|
383
|
+
try {
|
|
384
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'off']);
|
|
385
|
+
execaSync('tmux', ['select-window', '-t', `${YEEHAW_SESSION}:0`]);
|
|
386
|
+
execaSync('tmux', ['set', '-u', '-t', YEEHAW_SESSION, '@remote_mode']);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Not critical
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
export function isInRemoteMode() {
|
|
393
|
+
return remoteWindowIndex !== null;
|
|
394
|
+
}
|
|
395
|
+
export function getRemoteWindowIndex() {
|
|
396
|
+
return remoteWindowIndex;
|
|
397
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Yeehaw MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes Yeehaw project/barn/livestock management to Claude Code sessions.
|
|
6
|
+
*
|
|
7
|
+
* Terminology:
|
|
8
|
+
* - Project: A codebase you're working on
|
|
9
|
+
* - Barn: A server you manage
|
|
10
|
+
* - Livestock: Deployed instances of your apps (local or on barns)
|
|
11
|
+
* - Critter: System services that support livestock (nginx, mysql, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Usage: Add to Claude Code's MCP config:
|
|
14
|
+
* {
|
|
15
|
+
* "mcpServers": {
|
|
16
|
+
* "yeehaw": {
|
|
17
|
+
* "command": "node",
|
|
18
|
+
* "args": ["/path/to/yeehaw/dist/mcp-server.js"]
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
export {};
|