@colmbus72/yeehaw 0.1.0 → 0.3.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/dist/app.js +48 -50
- package/dist/components/Header.d.ts +5 -1
- package/dist/components/Header.js +2 -2
- package/dist/components/LivestockHeader.d.ts +7 -0
- package/dist/components/LivestockHeader.js +122 -0
- package/dist/components/PathInput.js +81 -19
- package/dist/index.js +12 -0
- package/dist/lib/tmux-config.js +11 -0
- package/dist/lib/tmux.d.ts +5 -0
- package/dist/lib/tmux.js +50 -5
- package/dist/lib/update-check.d.ts +22 -0
- package/dist/lib/update-check.js +128 -0
- package/dist/mcp-server.js +0 -0
- package/dist/types.d.ts +4 -0
- package/dist/views/BarnContext.js +0 -4
- package/dist/views/GlobalDashboard.d.ts +5 -1
- package/dist/views/GlobalDashboard.js +45 -18
- package/dist/views/IssuesView.js +1 -1
- package/dist/views/LivestockDetailView.d.ts +7 -2
- package/dist/views/LivestockDetailView.js +29 -9
- package/dist/views/LogsView.js +1 -1
- package/dist/views/ProjectContext.js +4 -6
- package/dist/views/WikiView.js +0 -4
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback } from 'react';
|
|
2
|
+
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { join } from 'path';
|
|
@@ -16,8 +16,9 @@ import { NightSkyView } from './views/NightSkyView.js';
|
|
|
16
16
|
import { useConfig } from './hooks/useConfig.js';
|
|
17
17
|
import { useSessions } from './hooks/useSessions.js';
|
|
18
18
|
import { useRemoteYeehaw } from './hooks/useRemoteYeehaw.js';
|
|
19
|
-
import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, enterRemoteMode, } from './lib/tmux.js';
|
|
19
|
+
import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, enterRemoteMode, ensureCorrectStatusBar, } from './lib/tmux.js';
|
|
20
20
|
import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig } from './lib/config.js';
|
|
21
|
+
import { getVersionInfo } from './lib/update-check.js';
|
|
21
22
|
function getHotkeyScope(view) {
|
|
22
23
|
switch (view.type) {
|
|
23
24
|
case 'global': return 'global-dashboard';
|
|
@@ -47,27 +48,27 @@ function getBottomBarItems(viewType) {
|
|
|
47
48
|
return [...common, { key: 'c', label: 'claude' }, { key: 'q', label: 'detach' }, { key: 'Q', label: 'quit' }];
|
|
48
49
|
}
|
|
49
50
|
if (viewType === 'project') {
|
|
50
|
-
return [...common, { key: 'c', label: 'claude' }, { key: 'w', label: 'wiki' }, { key: 'i', label: 'issues' }, { key: '
|
|
51
|
+
return [...common, { key: 'c', label: 'claude' }, { key: 'w', label: 'wiki' }, { key: 'i', label: 'issues' }, { key: 'Esc', label: 'back' }];
|
|
51
52
|
}
|
|
52
53
|
if (viewType === 'barn') {
|
|
53
|
-
return [...common, { key: 's', label: 'shell' }, { key: '
|
|
54
|
+
return [...common, { key: 's', label: 'shell' }, { key: 'Esc', label: 'back' }];
|
|
54
55
|
}
|
|
55
56
|
if (viewType === 'wiki') {
|
|
56
|
-
return [...common, { key: '
|
|
57
|
+
return [...common, { key: 'Esc', label: 'back' }];
|
|
57
58
|
}
|
|
58
59
|
if (viewType === 'issues') {
|
|
59
|
-
return [...common, { key: 'r', label: 'refresh' }, { key: 'o', label: 'open' }, { key: '
|
|
60
|
+
return [...common, { key: 'r', label: 'refresh' }, { key: 'o', label: 'open' }, { key: 'Esc', label: 'back' }];
|
|
60
61
|
}
|
|
61
62
|
if (viewType === 'livestock') {
|
|
62
|
-
return [...common, { key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: '
|
|
63
|
+
return [...common, { key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: 'Esc', label: 'back' }];
|
|
63
64
|
}
|
|
64
65
|
if (viewType === 'logs') {
|
|
65
|
-
return [...common, { key: 'r', label: 'refresh' }, { key: '
|
|
66
|
+
return [...common, { key: 'r', label: 'refresh' }, { key: 'Esc', label: 'back' }];
|
|
66
67
|
}
|
|
67
68
|
if (viewType === 'night-sky') {
|
|
68
69
|
return [{ key: 'c', label: 'cloud' }, { key: 'r', label: 'randomize' }, { key: 'Esc', label: 'exit' }];
|
|
69
70
|
}
|
|
70
|
-
return [...common, { key: '
|
|
71
|
+
return [...common, { key: 'Esc', label: 'back' }];
|
|
71
72
|
}
|
|
72
73
|
export function App() {
|
|
73
74
|
const { exit } = useApp();
|
|
@@ -84,6 +85,14 @@ export function App() {
|
|
|
84
85
|
const terminalHeight = stdout?.rows || 24;
|
|
85
86
|
// Check tmux availability
|
|
86
87
|
const tmuxAvailable = hasTmux();
|
|
88
|
+
// Get version info (cached, so safe to call synchronously)
|
|
89
|
+
const versionInfo = useMemo(() => getVersionInfo(), []);
|
|
90
|
+
// Ensure status bar is hidden when on global dashboard
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (view.type === 'global') {
|
|
93
|
+
ensureCorrectStatusBar();
|
|
94
|
+
}
|
|
95
|
+
}, [view.type]);
|
|
87
96
|
const handleSelectProject = useCallback((project) => {
|
|
88
97
|
setView({ type: 'project', project });
|
|
89
98
|
updateStatusBar(project.name);
|
|
@@ -164,27 +173,33 @@ export function App() {
|
|
|
164
173
|
setView({ type: 'project', project });
|
|
165
174
|
updateStatusBar(project.name);
|
|
166
175
|
}, []);
|
|
167
|
-
const handleOpenLivestockDetail = useCallback((project, livestock) => {
|
|
168
|
-
setView({ type: 'livestock', project, livestock });
|
|
176
|
+
const handleOpenLivestockDetail = useCallback((project, livestock, source, sourceBarn) => {
|
|
177
|
+
setView({ type: 'livestock', project, livestock, source, sourceBarn });
|
|
169
178
|
updateStatusBar(`${project.name} / ${livestock.name}`);
|
|
170
179
|
}, []);
|
|
171
|
-
const handleOpenLogs = useCallback((project, livestock) => {
|
|
172
|
-
setView({ type: 'logs', project, livestock });
|
|
180
|
+
const handleOpenLogs = useCallback((project, livestock, source, sourceBarn) => {
|
|
181
|
+
setView({ type: 'logs', project, livestock, source, sourceBarn });
|
|
173
182
|
updateStatusBar(`${project.name} / ${livestock.name} Logs`);
|
|
174
183
|
}, []);
|
|
175
|
-
const handleBackFromLivestock = useCallback((project) => {
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
const handleBackFromLivestock = useCallback((source, project, sourceBarn) => {
|
|
185
|
+
if (source === 'barn' && sourceBarn) {
|
|
186
|
+
setView({ type: 'barn', barn: sourceBarn });
|
|
187
|
+
updateStatusBar(`Barn: ${sourceBarn.name}`);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
setView({ type: 'project', project });
|
|
191
|
+
updateStatusBar(project.name);
|
|
192
|
+
}
|
|
178
193
|
}, []);
|
|
179
|
-
const handleUpdateLivestock = useCallback((project, updatedLivestock) => {
|
|
194
|
+
const handleUpdateLivestock = useCallback((project, updatedLivestock, source, sourceBarn) => {
|
|
180
195
|
const updatedProject = {
|
|
181
196
|
...project,
|
|
182
197
|
livestock: (project.livestock || []).map((l) => l.name === updatedLivestock.name ? updatedLivestock : l),
|
|
183
198
|
};
|
|
184
199
|
saveProject(updatedProject);
|
|
185
200
|
reload();
|
|
186
|
-
// Update the view with the new livestock data
|
|
187
|
-
setView({ type: 'livestock', project: updatedProject, livestock: updatedLivestock });
|
|
201
|
+
// Update the view with the new livestock data, preserving navigation context
|
|
202
|
+
setView({ type: 'livestock', project: updatedProject, livestock: updatedLivestock, source, sourceBarn });
|
|
188
203
|
}, [reload]);
|
|
189
204
|
const handleDeleteProject = useCallback((projectName) => {
|
|
190
205
|
deleteProject(projectName);
|
|
@@ -288,46 +303,29 @@ export function App() {
|
|
|
288
303
|
setShowHelp(false);
|
|
289
304
|
return;
|
|
290
305
|
}
|
|
291
|
-
//
|
|
292
|
-
if (input === 'q') {
|
|
293
|
-
|
|
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
|
-
}
|
|
306
|
+
// q: Detach from session (only on global dashboard)
|
|
307
|
+
if (input === 'q' && view.type === 'global') {
|
|
308
|
+
detachFromSession();
|
|
312
309
|
return;
|
|
313
310
|
}
|
|
314
|
-
// Shift-Q: Kill everything
|
|
311
|
+
// Shift-Q: Kill everything (only on global dashboard)
|
|
315
312
|
if (input === 'Q' && view.type === 'global') {
|
|
316
313
|
killYeehawSession();
|
|
317
314
|
exit();
|
|
318
315
|
return;
|
|
319
316
|
}
|
|
317
|
+
// ESC: Navigate back (handled by individual views for their sub-modes,
|
|
318
|
+
// but also handled here as a fallback for consistent navigation)
|
|
320
319
|
if (key.escape) {
|
|
321
320
|
if (view.type === 'wiki' || view.type === 'issues') {
|
|
322
321
|
handleBackFromSubview(view.project);
|
|
323
322
|
}
|
|
324
323
|
else if (view.type === 'logs') {
|
|
325
|
-
|
|
326
|
-
setView({ type: 'livestock', project: view.project, livestock: view.livestock });
|
|
324
|
+
setView({ type: 'livestock', project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn });
|
|
327
325
|
updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
|
|
328
326
|
}
|
|
329
327
|
else if (view.type === 'livestock') {
|
|
330
|
-
handleBackFromLivestock(view.project);
|
|
328
|
+
handleBackFromLivestock(view.source, view.project, view.sourceBarn);
|
|
331
329
|
}
|
|
332
330
|
else if (view.type === 'project' || view.type === 'barn') {
|
|
333
331
|
handleBack();
|
|
@@ -364,12 +362,12 @@ export function App() {
|
|
|
364
362
|
}
|
|
365
363
|
switch (view.type) {
|
|
366
364
|
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 }));
|
|
365
|
+
return (_jsx(GlobalDashboard, { projects: projects, barns: barns, windows: windows, versionInfo: versionInfo, onSelectProject: handleSelectProject, onSelectBarn: handleSelectBarn, onSelectWindow: handleSelectWindow, onNewClaude: handleNewClaude, onCreateProject: handleCreateProject, onCreateBarn: handleCreateBarn, onSshToBarn: handleSshToBarn }));
|
|
368
366
|
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) }));
|
|
367
|
+
return (_jsx(ProjectContext, { project: view.project, barns: barns, windows: windows, onBack: handleBack, onNewClaude: handleNewClaude, onSelectWindow: handleSelectWindow, onSelectLivestock: (livestock, barn) => handleOpenLivestockDetail(view.project, livestock, 'project'), onOpenLivestockSession: (livestock, barn) => handleOpenLivestockSession(livestock, barn, view.project.name), onUpdateProject: handleUpdateProject, onDeleteProject: handleDeleteProject, onOpenWiki: () => handleOpenWiki(view.project), onOpenIssues: () => handleOpenIssues(view.project) }));
|
|
370
368
|
case 'barn':
|
|
371
369
|
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) => {
|
|
370
|
+
return (_jsx(BarnContext, { barn: view.barn, livestock: barnLivestock, projects: projects, windows: windows, onBack: handleBack, onSshToBarn: () => handleSshToBarn(view.barn), onSelectLivestock: (project, livestock) => handleOpenLivestockDetail(project, livestock, 'barn', view.barn), onOpenLivestockSession: (project, livestock) => {
|
|
373
371
|
const barn = barns.find((b) => b.name === livestock.barn) || null;
|
|
374
372
|
handleOpenLivestockSession(livestock, barn, project.name);
|
|
375
373
|
}, onUpdateBarn: handleUpdateBarn, onDeleteBarn: handleDeleteBarn, onAddLivestock: (project, livestock) => {
|
|
@@ -397,13 +395,13 @@ export function App() {
|
|
|
397
395
|
case 'issues':
|
|
398
396
|
return (_jsx(IssuesView, { project: view.project, onBack: () => handleBackFromSubview(view.project) }));
|
|
399
397
|
case 'livestock':
|
|
400
|
-
return (_jsx(LivestockDetailView, { project: view.project, livestock: view.livestock, onBack: () => handleBackFromLivestock(view.project), onOpenLogs: () => handleOpenLogs(view.project, view.livestock), onOpenSession: () => {
|
|
398
|
+
return (_jsx(LivestockDetailView, { project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn, windows: windows, onBack: () => handleBackFromLivestock(view.source, view.project, view.sourceBarn), onOpenLogs: () => handleOpenLogs(view.project, view.livestock, view.source, view.sourceBarn), onOpenSession: () => {
|
|
401
399
|
const barn = barns.find((b) => b.name === view.livestock.barn) || null;
|
|
402
400
|
handleOpenLivestockSession(view.livestock, barn, view.project.name);
|
|
403
|
-
}, onUpdateLivestock: (updatedLivestock) => handleUpdateLivestock(view.project, updatedLivestock) }));
|
|
401
|
+
}, onSelectWindow: handleSelectWindow, onUpdateLivestock: (updatedLivestock) => handleUpdateLivestock(view.project, updatedLivestock, view.source, view.sourceBarn) }));
|
|
404
402
|
case 'logs':
|
|
405
403
|
return (_jsx(LogsView, { project: view.project, livestock: view.livestock, onBack: () => {
|
|
406
|
-
setView({ type: 'livestock', project: view.project, livestock: view.livestock });
|
|
404
|
+
setView({ type: 'livestock', project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn });
|
|
407
405
|
updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
|
|
408
406
|
} }));
|
|
409
407
|
case 'night-sky':
|
|
@@ -3,6 +3,10 @@ interface HeaderProps {
|
|
|
3
3
|
subtitle?: string;
|
|
4
4
|
summary?: string;
|
|
5
5
|
color?: string;
|
|
6
|
+
versionInfo?: {
|
|
7
|
+
current: string;
|
|
8
|
+
latest: string | null;
|
|
9
|
+
};
|
|
6
10
|
}
|
|
7
|
-
export declare function Header({ text, subtitle, summary, color }: HeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function Header({ text, subtitle, summary, color, versionInfo }: HeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
8
12
|
export {};
|
|
@@ -63,7 +63,7 @@ const TUMBLEWEED = [
|
|
|
63
63
|
];
|
|
64
64
|
// Brownish tan color to complement yeehaw gold
|
|
65
65
|
const TUMBLEWEED_COLOR = '#b8860b';
|
|
66
|
-
export function Header({ text, subtitle, summary, color }) {
|
|
66
|
+
export function Header({ text, subtitle, summary, color, versionInfo }) {
|
|
67
67
|
const [ascii, setAscii] = useState('');
|
|
68
68
|
useEffect(() => {
|
|
69
69
|
figlet.text(text.toUpperCase(), { font: 'ANSI Shadow' }, (err, result) => {
|
|
@@ -79,5 +79,5 @@ export function Header({ text, subtitle, summary, color }) {
|
|
|
79
79
|
const showTumbleweed = text.toLowerCase() === 'yeehaw';
|
|
80
80
|
// Vertically center tumbleweed next to ASCII art
|
|
81
81
|
const tumbleweedTopPadding = Math.max(0, Math.floor((lines.length - TUMBLEWEED.length) / 2));
|
|
82
|
-
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: [_jsxs(Box, { flexDirection: "row", children: [showTumbleweed && (_jsxs(Box, { flexDirection: "column", marginRight: 2, children: [Array(tumbleweedTopPadding).fill(null).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), TUMBLEWEED.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, children: line }, `tumbleweed-${i}`)))] })), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: gradientColors[i], children: line }, i))) })] }), (subtitle || summary) && (_jsxs(Box, { gap: 2, children: [subtitle && _jsx(Text, { dimColor: true, children: subtitle }), summary && _jsxs(Text, { color: "gray", children: ["- ", summary] })] }))] }));
|
|
82
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: [_jsxs(Box, { flexDirection: "row", children: [showTumbleweed && (_jsxs(Box, { flexDirection: "column", marginRight: 2, children: [Array(tumbleweedTopPadding).fill(null).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), TUMBLEWEED.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, children: line }, `tumbleweed-${i}`)))] })), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: gradientColors[i], children: line }, i))) }), versionInfo && showTumbleweed && (_jsx(Box, { marginLeft: 2, paddingRight: 1, alignItems: "flex-end", flexGrow: 1, justifyContent: "flex-end", children: versionInfo.latest && versionInfo.latest !== versionInfo.current ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] }), _jsx(Text, { dimColor: true, children: " \u2192 " }), _jsxs(Text, { color: "yellow", children: ["v", versionInfo.latest] })] })) : versionInfo.latest ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] }), _jsx(Text, { color: "green", children: " \u2713 latest" })] })) : (_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] })) }))] }), (subtitle || summary) && (_jsxs(Box, { gap: 2, children: [subtitle && _jsx(Text, { dimColor: true, children: subtitle }), summary && _jsxs(Text, { color: "gray", children: ["- ", summary] })] }))] }));
|
|
83
83
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Project, Livestock } from '../types.js';
|
|
2
|
+
interface LivestockHeaderProps {
|
|
3
|
+
project: Project;
|
|
4
|
+
livestock: Livestock;
|
|
5
|
+
}
|
|
6
|
+
export declare function LivestockHeader({ project, livestock }: LivestockHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
// Base cow ASCII art with @ symbols as pattern spots
|
|
4
|
+
// The @ symbols will be replaced with pattern characters for variation
|
|
5
|
+
// const COW_TEMPLATE = [
|
|
6
|
+
// " ___,,,___ _..............._",
|
|
7
|
+
// " '-/~'~\-'` @@@@@' .@@ \\",
|
|
8
|
+
// " |a a | '@@@' `@@ ||",
|
|
9
|
+
// " | /@@. @@@. .@@@. @@||",
|
|
10
|
+
// " (oo/@@' .@@@@..@@@@@@. /|",
|
|
11
|
+
// " `` '._ /@@@ @@@@' <`\/",
|
|
12
|
+
// " | /`--....-'`Y \))",
|
|
13
|
+
// " ||| //'. |((",
|
|
14
|
+
// " ||| // ||",
|
|
15
|
+
// " //( ` /(",
|
|
16
|
+
// ];
|
|
17
|
+
const COW_TEMPLATE = [
|
|
18
|
+
" /; ;\\",
|
|
19
|
+
" __ \\\\____//",
|
|
20
|
+
" /{_\\_/ `'\\____",
|
|
21
|
+
" \\___ (o) (o }",
|
|
22
|
+
" _______________________/ :--'",
|
|
23
|
+
" ,-,'`@@@@@@@@ @@@@ \\_ `__\\",
|
|
24
|
+
" ;:( @@@@@@@@@ @@ \\___(o'o)",
|
|
25
|
+
" :: ) @@@@ @@@ ,'@@( `===='",
|
|
26
|
+
" :: \\ @@@@@: @@@@) ( '@@@'",
|
|
27
|
+
" ;; /\\ /`, @@@@@\\ :@@@@@)",
|
|
28
|
+
" ::/ ) {_----------: :~`,~~;",
|
|
29
|
+
" ;;'`; : ) : / `; ;",
|
|
30
|
+
"`'`' / : : : : : :",
|
|
31
|
+
" )_ \\__; :_ ; \\_\\",
|
|
32
|
+
" :__\\ \\ \\ \\ : \\",
|
|
33
|
+
" `^' `^' `-^-'",
|
|
34
|
+
];
|
|
35
|
+
// " /; ;\\",
|
|
36
|
+
// " __ \\\\____//",
|
|
37
|
+
// " /{_\\_/ `'\\____",
|
|
38
|
+
// " \\___ (o) (o }",
|
|
39
|
+
// " _____________________________/ :--'",
|
|
40
|
+
// ",-,'`@@@@@@@@@@@@@@@@@@@@@ \\_ `__\\",
|
|
41
|
+
// ";:( @@@@@@@@@@@@@@@@ @@@ @@@@@@@ \\___(o'o)",
|
|
42
|
+
// ":: ) @@@@@@@@@@@ @@@@@@@@@@@@@ ,'@@( `===='",
|
|
43
|
+
// ":: : @@@@@: @@@@@@@ @@@@ @@@@@@@ `@@@:",
|
|
44
|
+
// ":: \\ @@@@@: @@@@@@@) ( '@@@'",
|
|
45
|
+
// ";; /\\ /`, @@@@@@@@@\\ :@@@@@)",
|
|
46
|
+
// "::/ ) {_----------------: :~`,~~;",
|
|
47
|
+
// ";;'`; : ) : / `; ;",
|
|
48
|
+
// ";;;; : : ; : ; ; :",
|
|
49
|
+
// "`'`' / : : : : : :",
|
|
50
|
+
// " )_ \\__; :_ ; \\_\\ `,','",
|
|
51
|
+
// " :__\\ \\ \\ \\ : \\ * 8`;'* *",
|
|
52
|
+
// " `^' `^' `-^-' \\v/ : \\/",
|
|
53
|
+
//];
|
|
54
|
+
// Generate multiple hash values from a string for better distribution
|
|
55
|
+
function multiHash(str) {
|
|
56
|
+
const hashes = [];
|
|
57
|
+
// First hash - djb2
|
|
58
|
+
let hash1 = 5381;
|
|
59
|
+
for (let i = 0; i < str.length; i++) {
|
|
60
|
+
hash1 = ((hash1 << 5) + hash1) ^ str.charCodeAt(i);
|
|
61
|
+
}
|
|
62
|
+
hashes.push(Math.abs(hash1));
|
|
63
|
+
// Second hash - sdbm
|
|
64
|
+
let hash2 = 0;
|
|
65
|
+
for (let i = 0; i < str.length; i++) {
|
|
66
|
+
hash2 = str.charCodeAt(i) + (hash2 << 6) + (hash2 << 16) - hash2;
|
|
67
|
+
}
|
|
68
|
+
hashes.push(Math.abs(hash2));
|
|
69
|
+
// Third hash - fnv-1a inspired
|
|
70
|
+
let hash3 = 2166136261;
|
|
71
|
+
for (let i = 0; i < str.length; i++) {
|
|
72
|
+
hash3 ^= str.charCodeAt(i);
|
|
73
|
+
hash3 = (hash3 * 16777619) >>> 0;
|
|
74
|
+
}
|
|
75
|
+
hashes.push(hash3);
|
|
76
|
+
return hashes;
|
|
77
|
+
}
|
|
78
|
+
// Pattern characters - space and block characters only
|
|
79
|
+
// Space creates gaps in the spots, blocks create varying density
|
|
80
|
+
const PATTERN_CHARS = [' ', ' ', '░', '░', '▒', '▓', '█'];
|
|
81
|
+
// Generate pattern variation for the cow based on livestock/project data
|
|
82
|
+
function generateCowArt(livestock, project) {
|
|
83
|
+
// Use git branch (environment) + livestock name for unique seed
|
|
84
|
+
// Branch typically indicates environment (main, staging, production, etc.)
|
|
85
|
+
const seed = `${livestock.branch || 'default'}-${livestock.name}-${project.name}`;
|
|
86
|
+
const hashes = multiHash(seed);
|
|
87
|
+
// Replace @ symbols with pattern characters
|
|
88
|
+
// Use multiple hashes and position for maximum variety
|
|
89
|
+
let charIndex = 0;
|
|
90
|
+
const cowArt = COW_TEMPLATE.map((line, lineIndex) => {
|
|
91
|
+
let result = '';
|
|
92
|
+
for (const char of line) {
|
|
93
|
+
if (char === '@') {
|
|
94
|
+
// Mix multiple hashes with position data for variety
|
|
95
|
+
const h1 = hashes[0];
|
|
96
|
+
const h2 = hashes[1];
|
|
97
|
+
const h3 = hashes[2];
|
|
98
|
+
// Combine hashes with position in different ways
|
|
99
|
+
const mix = (h1 >> (charIndex % 17)) ^
|
|
100
|
+
(h2 >> ((charIndex + lineIndex) % 13)) ^
|
|
101
|
+
(h3 >> ((charIndex * 7 + lineIndex * 3) % 19));
|
|
102
|
+
const charChoice = Math.abs(mix) % PATTERN_CHARS.length;
|
|
103
|
+
result += PATTERN_CHARS[charChoice];
|
|
104
|
+
charIndex++;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
result += char;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
});
|
|
112
|
+
return cowArt;
|
|
113
|
+
}
|
|
114
|
+
export function LivestockHeader({ project, livestock }) {
|
|
115
|
+
const cowArt = generateCowArt(livestock, project);
|
|
116
|
+
const color = project.color || '#b8860b'; // Use project color or default tan
|
|
117
|
+
// Calculate where to place the livestock name (in the body area)
|
|
118
|
+
// We'll show it to the right of the cow
|
|
119
|
+
const cowHeight = cowArt.length;
|
|
120
|
+
const infoStartLine = Math.floor(cowHeight / 2) - 1;
|
|
121
|
+
return (_jsx(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 1, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", children: cowArt.map((line, i) => (_jsx(Text, { color: color, children: line }, i))) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, justifyContent: "center", children: [_jsx(Text, { bold: true, color: color, children: livestock.name }), _jsxs(Text, { dimColor: true, children: ["project: ", project.name] }), _jsxs(Text, { dimColor: true, children: ["barn: ", livestock.barn || 'local'] }), livestock.branch && (_jsxs(Text, { dimColor: true, children: ["branch: ", livestock.branch] }))] })] }) }));
|
|
122
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { readdirSync, existsSync } from 'fs';
|
|
5
5
|
import { join, dirname, basename } from 'path';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { execaSync } from 'execa';
|
|
8
8
|
import { hasValidSshConfig } from '../lib/config.js';
|
|
9
|
+
// Cache for remote directory listings: Map<"barnName:dirPath", string[]>
|
|
10
|
+
const remoteCompletionCache = new Map();
|
|
11
|
+
function getCacheKey(barnName, dirPath) {
|
|
12
|
+
return `${barnName}:${dirPath}`;
|
|
13
|
+
}
|
|
9
14
|
function expandPath(path) {
|
|
10
15
|
if (path.startsWith('~/')) {
|
|
11
16
|
return join(homedir(), path.slice(2));
|
|
@@ -31,26 +36,22 @@ function getLocalCompletions(partialPath) {
|
|
|
31
36
|
return [];
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
|
-
function
|
|
35
|
-
if (!partialPath)
|
|
36
|
-
return [];
|
|
37
|
-
// Verify barn has valid SSH config
|
|
39
|
+
function fetchRemoteDirectoryListing(dir, barn) {
|
|
38
40
|
if (!hasValidSshConfig(barn)) {
|
|
39
|
-
return [];
|
|
41
|
+
return [];
|
|
40
42
|
}
|
|
41
|
-
// Handle ~ expansion for display
|
|
42
|
-
const dir = partialPath.endsWith('/') ? partialPath : dirname(partialPath) || '~';
|
|
43
|
-
const prefix = partialPath.endsWith('/') ? '' : basename(partialPath);
|
|
44
43
|
try {
|
|
45
|
-
//
|
|
44
|
+
// Fetch ALL directories in this directory (not filtered by prefix)
|
|
45
|
+
// This allows us to cache the full listing and filter client-side
|
|
46
46
|
const result = execaSync('ssh', [
|
|
47
47
|
'-p', String(barn.port),
|
|
48
48
|
'-i', barn.identity_file,
|
|
49
49
|
'-o', 'BatchMode=yes',
|
|
50
|
-
'-o', 'ConnectTimeout=
|
|
50
|
+
'-o', 'ConnectTimeout=3',
|
|
51
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
51
52
|
`${barn.user}@${barn.host}`,
|
|
52
|
-
`ls -
|
|
53
|
-
], { timeout:
|
|
53
|
+
`ls -1F ${dir} 2>/dev/null | grep '/$' | sed 's|/$||' || true`
|
|
54
|
+
], { timeout: 5000 });
|
|
54
55
|
const output = result.stdout.trim();
|
|
55
56
|
if (!output)
|
|
56
57
|
return [];
|
|
@@ -60,21 +61,78 @@ function getRemoteCompletions(partialPath, barn) {
|
|
|
60
61
|
return [];
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
function getRemoteCompletions(partialPath, barn) {
|
|
65
|
+
if (!partialPath)
|
|
66
|
+
return [];
|
|
67
|
+
// Verify barn has valid SSH config
|
|
68
|
+
if (!hasValidSshConfig(barn)) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
// Handle ~ expansion for display
|
|
72
|
+
const dir = partialPath.endsWith('/') ? partialPath.slice(0, -1) || '~' : dirname(partialPath) || '~';
|
|
73
|
+
const prefix = partialPath.endsWith('/') ? '' : basename(partialPath);
|
|
74
|
+
const cacheKey = getCacheKey(barn.name, dir);
|
|
75
|
+
// Check cache first
|
|
76
|
+
let allDirs = remoteCompletionCache.get(cacheKey);
|
|
77
|
+
if (!allDirs) {
|
|
78
|
+
// Fetch and cache
|
|
79
|
+
allDirs = fetchRemoteDirectoryListing(dir, barn);
|
|
80
|
+
remoteCompletionCache.set(cacheKey, allDirs);
|
|
81
|
+
}
|
|
82
|
+
// Filter by prefix
|
|
83
|
+
if (prefix) {
|
|
84
|
+
return allDirs.filter((d) => d.startsWith(prefix));
|
|
85
|
+
}
|
|
86
|
+
return allDirs;
|
|
87
|
+
}
|
|
88
|
+
// Pre-fetch a directory's contents in the background
|
|
89
|
+
function prefetchRemoteDirectory(dir, barn) {
|
|
90
|
+
if (!hasValidSshConfig(barn))
|
|
91
|
+
return;
|
|
92
|
+
const cacheKey = getCacheKey(barn.name, dir);
|
|
93
|
+
if (remoteCompletionCache.has(cacheKey))
|
|
94
|
+
return; // Already cached
|
|
95
|
+
// Fetch asynchronously (don't block)
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
const dirs = fetchRemoteDirectoryListing(dir, barn);
|
|
98
|
+
remoteCompletionCache.set(cacheKey, dirs);
|
|
99
|
+
}, 0);
|
|
100
|
+
}
|
|
63
101
|
export function PathInput({ value, onChange, onSubmit, barn }) {
|
|
64
102
|
const [cursorPos, setCursorPos] = useState(value.length);
|
|
65
103
|
const [completions, setCompletions] = useState([]);
|
|
66
104
|
const [loading, setLoading] = useState(false);
|
|
67
|
-
|
|
105
|
+
const pendingFetch = useRef(null);
|
|
106
|
+
// Update completions when value changes
|
|
68
107
|
useEffect(() => {
|
|
69
108
|
if (barn) {
|
|
70
|
-
// Remote completion -
|
|
71
|
-
|
|
72
|
-
const
|
|
109
|
+
// Remote completion - check cache first
|
|
110
|
+
const dir = value.endsWith('/') ? value.slice(0, -1) || '~' : dirname(value) || '~';
|
|
111
|
+
const cacheKey = getCacheKey(barn.name, dir);
|
|
112
|
+
const cached = remoteCompletionCache.has(cacheKey);
|
|
113
|
+
if (cached) {
|
|
114
|
+
// Use cached data immediately
|
|
73
115
|
const results = getRemoteCompletions(value, barn);
|
|
74
116
|
setCompletions(results);
|
|
75
117
|
setLoading(false);
|
|
76
|
-
}
|
|
77
|
-
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Not cached - debounce the fetch
|
|
121
|
+
setLoading(true);
|
|
122
|
+
if (pendingFetch.current) {
|
|
123
|
+
clearTimeout(pendingFetch.current);
|
|
124
|
+
}
|
|
125
|
+
pendingFetch.current = setTimeout(() => {
|
|
126
|
+
const results = getRemoteCompletions(value, barn);
|
|
127
|
+
setCompletions(results);
|
|
128
|
+
setLoading(false);
|
|
129
|
+
}, 200);
|
|
130
|
+
}
|
|
131
|
+
return () => {
|
|
132
|
+
if (pendingFetch.current) {
|
|
133
|
+
clearTimeout(pendingFetch.current);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
78
136
|
}
|
|
79
137
|
else {
|
|
80
138
|
// Local completion - immediate
|
|
@@ -94,6 +152,10 @@ export function PathInput({ value, onChange, onSubmit, barn }) {
|
|
|
94
152
|
const newPath = displayDir + completions[0] + '/';
|
|
95
153
|
onChange(newPath);
|
|
96
154
|
setCursorPos(newPath.length);
|
|
155
|
+
// Pre-fetch the subdirectory contents for faster subsequent completions
|
|
156
|
+
if (barn) {
|
|
157
|
+
prefetchRemoteDirectory(newPath.slice(0, -1), barn);
|
|
158
|
+
}
|
|
97
159
|
}
|
|
98
160
|
else if (completions.length > 1) {
|
|
99
161
|
// Multiple matches - find common prefix
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,21 @@ import { execaSync } from 'execa';
|
|
|
5
5
|
import { App } from './app.js';
|
|
6
6
|
import { isInsideYeehawSession, yeehawSessionExists, createYeehawSession, attachToYeehaw, hasTmux, } from './lib/tmux.js';
|
|
7
7
|
import { ensureConfigDirs } from './lib/config.js';
|
|
8
|
+
import { checkForUpdates, formatUpdateMessage } from './lib/update-check.js';
|
|
8
9
|
function main() {
|
|
9
10
|
// Ensure config directories exist
|
|
10
11
|
ensureConfigDirs();
|
|
12
|
+
// Check for updates (non-blocking, uses cache)
|
|
13
|
+
try {
|
|
14
|
+
const updateInfo = checkForUpdates();
|
|
15
|
+
if (updateInfo?.updateAvailable) {
|
|
16
|
+
console.log('\x1b[33m%s\x1b[0m', formatUpdateMessage(updateInfo));
|
|
17
|
+
console.log('');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore update check errors
|
|
22
|
+
}
|
|
11
23
|
// Check if tmux is available
|
|
12
24
|
if (!hasTmux()) {
|
|
13
25
|
console.error('Error: tmux is required but not installed');
|
package/dist/lib/tmux-config.js
CHANGED
|
@@ -11,6 +11,17 @@ export function generateTmuxConfig() {
|
|
|
11
11
|
set -g mouse on
|
|
12
12
|
set -g history-limit 50000
|
|
13
13
|
|
|
14
|
+
# macOS clipboard support
|
|
15
|
+
# Enable copying to system clipboard when selecting with mouse
|
|
16
|
+
set -g set-clipboard on
|
|
17
|
+
bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"
|
|
18
|
+
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"
|
|
19
|
+
# Also support keyboard-based copy (Enter key in copy mode)
|
|
20
|
+
bind-key -T copy-mode Enter send-keys -X copy-pipe-and-cancel "pbcopy"
|
|
21
|
+
bind-key -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "pbcopy"
|
|
22
|
+
# Use y to yank in vi mode
|
|
23
|
+
bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "pbcopy"
|
|
24
|
+
|
|
14
25
|
# Yeehaw keybindings
|
|
15
26
|
bind-key -n C-y select-window -t :0 # Return to dashboard
|
|
16
27
|
bind-key -n C-h previous-window # Go left one window
|
package/dist/lib/tmux.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export declare function isInsideYeehawSession(): boolean;
|
|
|
12
12
|
export declare function yeehawSessionExists(): boolean;
|
|
13
13
|
export declare function createYeehawSession(): void;
|
|
14
14
|
export declare function setupStatusBarHooks(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Force check and correct the status bar visibility based on current window.
|
|
17
|
+
* Call this when you suspect the status bar might be in the wrong state.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureCorrectStatusBar(): void;
|
|
15
20
|
export declare function attachToYeehaw(): void;
|
|
16
21
|
export declare function createClaudeWindow(workingDir: string, windowName: string): number;
|
|
17
22
|
export declare function createShellWindow(workingDir: string, windowName: string, shell?: string): number;
|
package/dist/lib/tmux.js
CHANGED
|
@@ -61,6 +61,7 @@ export function createYeehawSession() {
|
|
|
61
61
|
}
|
|
62
62
|
export function setupStatusBarHooks() {
|
|
63
63
|
// Hide status bar when in window 0, show in other windows
|
|
64
|
+
const statusCheck = 'if-shell -F "#{==:#{window_index},0}" "set status off" "set status on"';
|
|
64
65
|
try {
|
|
65
66
|
// Start with status off (we begin in window 0)
|
|
66
67
|
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'off']);
|
|
@@ -68,15 +69,56 @@ export function setupStatusBarHooks() {
|
|
|
68
69
|
execaSync('tmux', [
|
|
69
70
|
'set-hook', '-t', YEEHAW_SESSION,
|
|
70
71
|
'after-select-window',
|
|
71
|
-
|
|
72
|
+
statusCheck
|
|
73
|
+
]);
|
|
74
|
+
// Hook for when a window is killed (we might land back on window 0)
|
|
75
|
+
execaSync('tmux', [
|
|
76
|
+
'set-hook', '-t', YEEHAW_SESSION,
|
|
77
|
+
'window-unlinked',
|
|
78
|
+
statusCheck
|
|
79
|
+
]);
|
|
80
|
+
// Hook for when pane focus changes (covers edge cases)
|
|
81
|
+
execaSync('tmux', [
|
|
82
|
+
'set-hook', '-t', YEEHAW_SESSION,
|
|
83
|
+
'pane-focus-in',
|
|
84
|
+
statusCheck
|
|
85
|
+
]);
|
|
86
|
+
// Hook for client attachment (ensure status is correct when reattaching)
|
|
87
|
+
execaSync('tmux', [
|
|
88
|
+
'set-hook', '-t', YEEHAW_SESSION,
|
|
89
|
+
'client-attached',
|
|
90
|
+
statusCheck
|
|
72
91
|
]);
|
|
73
92
|
}
|
|
74
93
|
catch {
|
|
75
94
|
// Hooks might fail on older tmux versions, not critical
|
|
76
95
|
}
|
|
77
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Force check and correct the status bar visibility based on current window.
|
|
99
|
+
* Call this when you suspect the status bar might be in the wrong state.
|
|
100
|
+
*/
|
|
101
|
+
export function ensureCorrectStatusBar() {
|
|
102
|
+
try {
|
|
103
|
+
// Get current window index
|
|
104
|
+
const result = execaSync('tmux', ['display-message', '-p', '#{window_index}']);
|
|
105
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
106
|
+
// Set status based on window
|
|
107
|
+
if (windowIndex === 0) {
|
|
108
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'off']);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'on']);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Ignore errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
78
118
|
export function attachToYeehaw() {
|
|
79
|
-
//
|
|
119
|
+
// Attach to the existing yeehaw session
|
|
120
|
+
// Note: Multiple terminals attaching to the same session will share the same view
|
|
121
|
+
// (this is a tmux limitation - they'll see the same window selections)
|
|
80
122
|
spawnSync('tmux', ['attach-session', '-t', YEEHAW_SESSION], {
|
|
81
123
|
stdio: 'inherit',
|
|
82
124
|
});
|
|
@@ -130,15 +172,18 @@ export function createClaudeWindow(workingDir, windowName) {
|
|
|
130
172
|
]);
|
|
131
173
|
return parseInt(result.stdout.trim(), 10);
|
|
132
174
|
}
|
|
133
|
-
export function createShellWindow(workingDir, windowName, shell
|
|
134
|
-
//
|
|
175
|
+
export function createShellWindow(workingDir, windowName, shell) {
|
|
176
|
+
// Use the user's configured shell from $SHELL, fallback to /bin/bash
|
|
177
|
+
const userShell = shell || process.env.SHELL || '/bin/bash';
|
|
178
|
+
// Create new window running shell as a login shell (-l) so it loads .bashrc/.bash_profile/.zshrc etc.
|
|
179
|
+
// This ensures PS1 and other environment customizations are loaded
|
|
135
180
|
execaSync('tmux', [
|
|
136
181
|
'new-window',
|
|
137
182
|
'-a',
|
|
138
183
|
'-t', YEEHAW_SESSION,
|
|
139
184
|
'-n', windowName,
|
|
140
185
|
'-c', workingDir,
|
|
141
|
-
|
|
186
|
+
`${userShell} -l`,
|
|
142
187
|
]);
|
|
143
188
|
// Get the window index we just created (new window is now current)
|
|
144
189
|
const result = execaSync('tmux', [
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface UpdateInfo {
|
|
2
|
+
updateAvailable: boolean;
|
|
3
|
+
currentVersion: string;
|
|
4
|
+
latestVersion: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Get version information synchronously using cached data.
|
|
8
|
+
* Safe to call from React components.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getVersionInfo(): {
|
|
11
|
+
current: string;
|
|
12
|
+
latest: string | null;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Check for updates. Returns immediately with cached data if available,
|
|
16
|
+
* otherwise fetches from npm (with 5s timeout).
|
|
17
|
+
*/
|
|
18
|
+
export declare function checkForUpdates(): UpdateInfo | null;
|
|
19
|
+
/**
|
|
20
|
+
* Format update notification message
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatUpdateMessage(info: UpdateInfo): string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { YEEHAW_DIR } from './paths.js';
|
|
5
|
+
const PACKAGE_NAME = '@colmbus72/yeehaw';
|
|
6
|
+
const CACHE_FILE = join(YEEHAW_DIR, '.update-check');
|
|
7
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
8
|
+
function getCurrentVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const packagePath = new URL('../../package.json', import.meta.url);
|
|
11
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
12
|
+
return pkg.version;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return '0.0.0';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function readCache() {
|
|
19
|
+
try {
|
|
20
|
+
if (!existsSync(CACHE_FILE))
|
|
21
|
+
return null;
|
|
22
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
23
|
+
if (Date.now() - data.checkedAt < CACHE_TTL_MS) {
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
return null; // Cache expired
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function writeCache(latestVersion) {
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(YEEHAW_DIR)) {
|
|
35
|
+
mkdirSync(YEEHAW_DIR, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
const data = {
|
|
38
|
+
latestVersion,
|
|
39
|
+
checkedAt: Date.now(),
|
|
40
|
+
};
|
|
41
|
+
writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Ignore cache write errors
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function fetchLatestVersion() {
|
|
48
|
+
try {
|
|
49
|
+
// Use execFileSync for safety (no shell injection possible)
|
|
50
|
+
const result = execFileSync('npm', ['view', PACKAGE_NAME, 'version'], {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
return result.trim();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function compareVersions(current, latest) {
|
|
62
|
+
const parseVersion = (v) => v.split('.').map(n => parseInt(n, 10) || 0);
|
|
63
|
+
const [cMajor, cMinor, cPatch] = parseVersion(current);
|
|
64
|
+
const [lMajor, lMinor, lPatch] = parseVersion(latest);
|
|
65
|
+
if (lMajor > cMajor)
|
|
66
|
+
return 1;
|
|
67
|
+
if (lMajor < cMajor)
|
|
68
|
+
return -1;
|
|
69
|
+
if (lMinor > cMinor)
|
|
70
|
+
return 1;
|
|
71
|
+
if (lMinor < cMinor)
|
|
72
|
+
return -1;
|
|
73
|
+
if (lPatch > cPatch)
|
|
74
|
+
return 1;
|
|
75
|
+
if (lPatch < cPatch)
|
|
76
|
+
return -1;
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get version information synchronously using cached data.
|
|
81
|
+
* Safe to call from React components.
|
|
82
|
+
*/
|
|
83
|
+
export function getVersionInfo() {
|
|
84
|
+
const current = getCurrentVersion();
|
|
85
|
+
const cached = readCache();
|
|
86
|
+
return {
|
|
87
|
+
current,
|
|
88
|
+
latest: cached?.latestVersion || null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check for updates. Returns immediately with cached data if available,
|
|
93
|
+
* otherwise fetches from npm (with 5s timeout).
|
|
94
|
+
*/
|
|
95
|
+
export function checkForUpdates() {
|
|
96
|
+
try {
|
|
97
|
+
const currentVersion = getCurrentVersion();
|
|
98
|
+
// Try cache first
|
|
99
|
+
const cached = readCache();
|
|
100
|
+
if (cached) {
|
|
101
|
+
return {
|
|
102
|
+
updateAvailable: compareVersions(currentVersion, cached.latestVersion) > 0,
|
|
103
|
+
currentVersion,
|
|
104
|
+
latestVersion: cached.latestVersion,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Fetch from npm
|
|
108
|
+
const latestVersion = fetchLatestVersion();
|
|
109
|
+
if (!latestVersion)
|
|
110
|
+
return null;
|
|
111
|
+
// Update cache
|
|
112
|
+
writeCache(latestVersion);
|
|
113
|
+
return {
|
|
114
|
+
updateAvailable: compareVersions(currentVersion, latestVersion) > 0,
|
|
115
|
+
currentVersion,
|
|
116
|
+
latestVersion,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Format update notification message
|
|
125
|
+
*/
|
|
126
|
+
export function formatUpdateMessage(info) {
|
|
127
|
+
return `Update available: ${info.currentVersion} → ${info.latestVersion}\nRun: npm install -g ${PACKAGE_NAME}`;
|
|
128
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
File without changes
|
package/dist/types.d.ts
CHANGED
|
@@ -80,10 +80,14 @@ export type AppView = {
|
|
|
80
80
|
type: 'livestock';
|
|
81
81
|
project: Project;
|
|
82
82
|
livestock: Livestock;
|
|
83
|
+
source: 'project' | 'barn';
|
|
84
|
+
sourceBarn?: Barn;
|
|
83
85
|
} | {
|
|
84
86
|
type: 'logs';
|
|
85
87
|
project: Project;
|
|
86
88
|
livestock: Livestock;
|
|
89
|
+
source: 'project' | 'barn';
|
|
90
|
+
sourceBarn?: Barn;
|
|
87
91
|
} | {
|
|
88
92
|
type: 'night-sky';
|
|
89
93
|
};
|
|
@@ -82,10 +82,6 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
82
82
|
}
|
|
83
83
|
if (mode !== 'normal')
|
|
84
84
|
return;
|
|
85
|
-
if (input === 'q') {
|
|
86
|
-
onBack();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
85
|
if (key.tab) {
|
|
90
86
|
setFocusedPanel((p) => (p === 'livestock' ? 'critters' : 'livestock'));
|
|
91
87
|
return;
|
|
@@ -4,6 +4,10 @@ interface GlobalDashboardProps {
|
|
|
4
4
|
projects: Project[];
|
|
5
5
|
barns: Barn[];
|
|
6
6
|
windows: TmuxWindow[];
|
|
7
|
+
versionInfo?: {
|
|
8
|
+
current: string;
|
|
9
|
+
latest: string | null;
|
|
10
|
+
};
|
|
7
11
|
onSelectProject: (project: Project) => void;
|
|
8
12
|
onSelectBarn: (barn: Barn) => void;
|
|
9
13
|
onSelectWindow: (window: TmuxWindow) => void;
|
|
@@ -12,5 +16,5 @@ interface GlobalDashboardProps {
|
|
|
12
16
|
onCreateBarn: (barn: Barn) => void;
|
|
13
17
|
onSshToBarn: (barn: Barn) => void;
|
|
14
18
|
}
|
|
15
|
-
export declare function GlobalDashboard({ projects, barns, windows, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
|
|
16
20
|
export {};
|
|
@@ -8,10 +8,11 @@ import { List } from '../components/List.js';
|
|
|
8
8
|
import { PathInput } from '../components/PathInput.js';
|
|
9
9
|
import { parseSshConfig } from '../lib/ssh.js';
|
|
10
10
|
import { getWindowStatus } from '../lib/tmux.js';
|
|
11
|
+
import { isLocalBarn } from '../lib/config.js';
|
|
11
12
|
function countSessionsForProject(projectName, windows) {
|
|
12
13
|
return windows.filter((w) => w.name.startsWith(projectName)).length;
|
|
13
14
|
}
|
|
14
|
-
export function GlobalDashboard({ projects, barns, windows, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }) {
|
|
15
|
+
export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }) {
|
|
15
16
|
const [focusedPanel, setFocusedPanel] = useState('projects');
|
|
16
17
|
const [mode, setMode] = useState('normal');
|
|
17
18
|
// New project form state
|
|
@@ -170,25 +171,51 @@ export function GlobalDashboard({ projects, barns, windows, onSelectProject, onS
|
|
|
170
171
|
meta: sessionCount > 0 ? `${sessionCount} session${sessionCount > 1 ? 's' : ''}` : undefined,
|
|
171
172
|
};
|
|
172
173
|
});
|
|
174
|
+
// Parse window name to show clearer labels
|
|
175
|
+
const formatSessionLabel = (name) => {
|
|
176
|
+
// Remote yeehaw connection
|
|
177
|
+
if (name.startsWith('remote:')) {
|
|
178
|
+
return { label: name.replace('remote:', ''), typeHint: 'remote' };
|
|
179
|
+
}
|
|
180
|
+
// Barn shell session
|
|
181
|
+
if (name.startsWith('barn-')) {
|
|
182
|
+
return { label: name.replace('barn-', ''), typeHint: 'barn' };
|
|
183
|
+
}
|
|
184
|
+
// Claude session
|
|
185
|
+
if (name.endsWith('-claude')) {
|
|
186
|
+
return { label: name.replace('-claude', ''), typeHint: 'claude' };
|
|
187
|
+
}
|
|
188
|
+
// Livestock session (project-livestock format)
|
|
189
|
+
const parts = name.split('-');
|
|
190
|
+
if (parts.length >= 2) {
|
|
191
|
+
const projectName = parts.slice(0, -1).join('-');
|
|
192
|
+
const livestockName = parts[parts.length - 1];
|
|
193
|
+
return { label: `${projectName} / ${livestockName}`, typeHint: 'shell' };
|
|
194
|
+
}
|
|
195
|
+
return { label: name, typeHint: '' };
|
|
196
|
+
};
|
|
173
197
|
// Use display numbers (1-9) instead of window index
|
|
174
|
-
const sessionItems = sessionWindows.map((w, i) =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
const sessionItems = sessionWindows.map((w, i) => {
|
|
199
|
+
const { label, typeHint } = formatSessionLabel(w.name);
|
|
200
|
+
return {
|
|
201
|
+
id: String(w.index),
|
|
202
|
+
label: `[${i + 1}] ${label}`,
|
|
203
|
+
status: w.active ? 'active' : 'inactive',
|
|
204
|
+
meta: typeHint ? `${typeHint} · ${getWindowStatus(w)}` : getWindowStatus(w),
|
|
205
|
+
};
|
|
206
|
+
});
|
|
180
207
|
const barnItems = barns.map((b) => ({
|
|
181
208
|
id: b.name,
|
|
182
|
-
label: b.name,
|
|
209
|
+
label: isLocalBarn(b) ? 'local' : b.name,
|
|
183
210
|
status: 'active',
|
|
184
|
-
meta: `${b.user}@${b.host}`,
|
|
211
|
+
meta: isLocalBarn(b) ? 'this machine' : `${b.user}@${b.host}`,
|
|
185
212
|
}));
|
|
186
213
|
// New project modals
|
|
187
214
|
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" }) })] })] }));
|
|
215
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
216
|
}
|
|
190
217
|
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" }) })] })] }));
|
|
218
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
219
|
}
|
|
193
220
|
// SSH host selection for new barn
|
|
194
221
|
if (mode === 'new-barn-select-ssh') {
|
|
@@ -201,7 +228,7 @@ export function GlobalDashboard({ projects, barns, windows, onSelectProject, onS
|
|
|
201
228
|
meta: h.hostname ? `${h.user || 'root'}@${h.hostname}` : undefined,
|
|
202
229
|
})),
|
|
203
230
|
];
|
|
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) => {
|
|
231
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
232
|
if (item.id === '__manual__') {
|
|
206
233
|
setMode('new-barn-name');
|
|
207
234
|
}
|
|
@@ -214,25 +241,25 @@ export function GlobalDashboard({ projects, barns, windows, onSelectProject, onS
|
|
|
214
241
|
}
|
|
215
242
|
// New barn modals
|
|
216
243
|
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" }) })] })] }));
|
|
244
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
245
|
}
|
|
219
246
|
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" }) })] })] }));
|
|
247
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
248
|
}
|
|
222
249
|
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" }) })] })] }));
|
|
250
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
251
|
}
|
|
225
252
|
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" }) })] })] }));
|
|
253
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
254
|
}
|
|
228
255
|
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" }) })] })] }));
|
|
256
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
257
|
}
|
|
231
258
|
// Panel-specific hints (page-level hotkeys like c are in BottomBar)
|
|
232
259
|
const projectHints = '[n] new';
|
|
233
260
|
const sessionHints = '1-9 switch';
|
|
234
261
|
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) => {
|
|
262
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _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
263
|
const project = projects.find((p) => p.name === item.id);
|
|
237
264
|
if (project)
|
|
238
265
|
onSelectProject(project);
|
package/dist/views/IssuesView.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type TmuxWindow } from '../lib/tmux.js';
|
|
2
|
+
import type { Project, Livestock, Barn } from '../types.js';
|
|
2
3
|
interface LivestockDetailViewProps {
|
|
3
4
|
project: Project;
|
|
4
5
|
livestock: Livestock;
|
|
6
|
+
source: 'project' | 'barn';
|
|
7
|
+
sourceBarn?: Barn;
|
|
8
|
+
windows: TmuxWindow[];
|
|
5
9
|
onBack: () => void;
|
|
6
10
|
onOpenLogs: () => void;
|
|
7
11
|
onOpenSession: () => void;
|
|
12
|
+
onSelectWindow: (window: TmuxWindow) => void;
|
|
8
13
|
onUpdateLivestock: (livestock: Livestock) => void;
|
|
9
14
|
}
|
|
10
|
-
export declare function LivestockDetailView({ project, livestock, onBack, onOpenLogs, onOpenSession, onUpdateLivestock, }: LivestockDetailViewProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare function LivestockDetailView({ project, livestock, source, sourceBarn, windows, onBack, onOpenLogs, onOpenSession, onSelectWindow, onUpdateLivestock, }: LivestockDetailViewProps): import("react/jsx-runtime").JSX.Element;
|
|
11
16
|
export {};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
5
|
import { Header } from '../components/Header.js';
|
|
6
|
+
import { LivestockHeader } from '../components/LivestockHeader.js';
|
|
7
|
+
import { List } from '../components/List.js';
|
|
6
8
|
import { Panel } from '../components/Panel.js';
|
|
7
9
|
import { PathInput } from '../components/PathInput.js';
|
|
8
10
|
import { loadBarn } from '../lib/config.js';
|
|
9
|
-
|
|
11
|
+
import { getWindowStatus } from '../lib/tmux.js';
|
|
12
|
+
export function LivestockDetailView({ project, livestock, source, sourceBarn, windows, onBack, onOpenLogs, onOpenSession, onSelectWindow, onUpdateLivestock, }) {
|
|
10
13
|
const [mode, setMode] = useState('normal');
|
|
11
14
|
// Edit form state
|
|
12
15
|
const [editName, setEditName] = useState(livestock.name);
|
|
@@ -17,6 +20,15 @@ export function LivestockDetailView({ project, livestock, onBack, onOpenLogs, on
|
|
|
17
20
|
const [editEnvPath, setEditEnvPath] = useState(livestock.env_path || '');
|
|
18
21
|
// Get barn info if remote
|
|
19
22
|
const barn = livestock.barn ? loadBarn(livestock.barn) : null;
|
|
23
|
+
// Sync form state when livestock prop changes (e.g., after save)
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setEditName(livestock.name);
|
|
26
|
+
setEditPath(livestock.path);
|
|
27
|
+
setEditRepo(livestock.repo || '');
|
|
28
|
+
setEditBranch(livestock.branch || '');
|
|
29
|
+
setEditLogPath(livestock.log_path || '');
|
|
30
|
+
setEditEnvPath(livestock.env_path || '');
|
|
31
|
+
}, [livestock]);
|
|
20
32
|
const resetForm = () => {
|
|
21
33
|
setEditName(livestock.name);
|
|
22
34
|
setEditPath(livestock.path);
|
|
@@ -65,10 +77,6 @@ export function LivestockDetailView({ project, livestock, onBack, onOpenLogs, on
|
|
|
65
77
|
// Only process these in normal mode
|
|
66
78
|
if (mode !== 'normal')
|
|
67
79
|
return;
|
|
68
|
-
if (input === 'q') {
|
|
69
|
-
onBack();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
80
|
if (input === 's') {
|
|
73
81
|
onOpenSession();
|
|
74
82
|
return;
|
|
@@ -134,7 +142,19 @@ export function LivestockDetailView({ project, livestock, onBack, onOpenLogs, on
|
|
|
134
142
|
setMode('normal');
|
|
135
143
|
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save & finish, Esc: cancel" }) })] })] }));
|
|
136
144
|
}
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
|
|
145
|
+
// Filter windows to this livestock (match pattern: projectname-livestockname)
|
|
146
|
+
const livestockWindowName = `${project.name}-${livestock.name}`;
|
|
147
|
+
const livestockWindows = windows.filter(w => w.name === livestockWindowName || w.name.startsWith(`${livestockWindowName}-`));
|
|
148
|
+
const sessionItems = livestockWindows.map((w, i) => ({
|
|
149
|
+
id: String(w.index),
|
|
150
|
+
label: `[${i + 1}] shell`,
|
|
151
|
+
status: w.active ? 'active' : 'inactive',
|
|
152
|
+
meta: getWindowStatus(w),
|
|
153
|
+
}));
|
|
154
|
+
// Normal view - show livestock info inline with sessions
|
|
155
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(LivestockHeader, { project: project, livestock: livestock }), _jsxs(Box, { paddingX: 2, gap: 3, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "path:" }), " ", livestock.path] }), barn && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "barn:" }), " ", barn.name, " ", _jsxs(Text, { dimColor: true, children: ["(", barn.host, ")"] })] }))] }), _jsxs(Box, { paddingX: 2, gap: 3, marginBottom: 1, children: [livestock.repo && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "repo:" }), " ", livestock.repo] })), livestock.log_path && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "logs:" }), " ", livestock.log_path] })), livestock.env_path && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "env:" }), " ", livestock.env_path] }))] }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(Panel, { title: "Sessions", focused: true, hints: `[s] shell ${livestock.log_path ? '[l] logs ' : ''}[e] edit`, children: sessionItems.length > 0 ? (_jsx(List, { items: sessionItems, focused: true, onSelect: (item) => {
|
|
156
|
+
const window = livestockWindows.find(w => String(w.index) === item.id);
|
|
157
|
+
if (window)
|
|
158
|
+
onSelectWindow(window);
|
|
159
|
+
} })) : (_jsx(Text, { dimColor: true, italic: true, children: "No active sessions. Press [s] to start a shell." })) }) })] }));
|
|
140
160
|
}
|
package/dist/views/LogsView.js
CHANGED
|
@@ -9,6 +9,7 @@ import { PathInput } from '../components/PathInput.js';
|
|
|
9
9
|
import { getWindowStatus } from '../lib/tmux.js';
|
|
10
10
|
import { detectGitInfo, detectRemoteGitInfo } from '../lib/git.js';
|
|
11
11
|
import { detectLivestockConfig } from '../lib/livestock.js';
|
|
12
|
+
import { isLocalBarn } from '../lib/config.js';
|
|
12
13
|
export function ProjectContext({ project, barns, windows, onBack, onNewClaude, onSelectWindow, onSelectLivestock, onOpenLivestockSession, onUpdateProject, onDeleteProject, onOpenWiki, onOpenIssues, }) {
|
|
13
14
|
const [focusedPanel, setFocusedPanel] = useState('livestock');
|
|
14
15
|
const [mode, setMode] = useState('normal');
|
|
@@ -159,10 +160,6 @@ export function ProjectContext({ project, barns, windows, onBack, onNewClaude, o
|
|
|
159
160
|
}
|
|
160
161
|
if (mode !== 'normal')
|
|
161
162
|
return;
|
|
162
|
-
if (input === 'q') {
|
|
163
|
-
onBack();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
163
|
if (key.tab) {
|
|
167
164
|
setFocusedPanel((p) => (p === 'livestock' ? 'sessions' : 'livestock'));
|
|
168
165
|
return;
|
|
@@ -245,10 +242,11 @@ export function ProjectContext({ project, barns, windows, onBack, onNewClaude, o
|
|
|
245
242
|
}, placeholder: "local, dev, production..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Esc: cancel" }) })] })] }));
|
|
246
243
|
}
|
|
247
244
|
if (mode === 'add-livestock-barn') {
|
|
248
|
-
// Build barn options including "Local" option
|
|
245
|
+
// Build barn options including "Local" option (filter out local barn from array to avoid duplicates)
|
|
246
|
+
const remoteBarns = barns.filter((b) => !isLocalBarn(b));
|
|
249
247
|
const barnOptions = [
|
|
250
248
|
{ id: '__local__', label: 'Local (this machine)', status: 'active' },
|
|
251
|
-
...
|
|
249
|
+
...remoteBarns.map((b) => ({
|
|
252
250
|
id: b.name,
|
|
253
251
|
label: b.name,
|
|
254
252
|
status: 'active',
|
package/dist/views/WikiView.js
CHANGED
|
@@ -65,10 +65,6 @@ export function WikiView({ project, onBack, onUpdateProject }) {
|
|
|
65
65
|
// Only process these in normal mode
|
|
66
66
|
if (mode !== 'normal')
|
|
67
67
|
return;
|
|
68
|
-
if (input === 'q') {
|
|
69
|
-
onBack();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
68
|
// Tab to switch focus between panels
|
|
73
69
|
if (key.tab) {
|
|
74
70
|
setFocusedPanel((prev) => (prev === 'sections' ? 'content' : 'sections'));
|