@colmbus72/yeehaw 0.4.2 → 0.5.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.
@@ -3,5 +3,6 @@
3
3
  "description": "Yeehaw CLI plugin with skills for project setup and configuration",
4
4
  "author": {
5
5
  "name": "Yeehaw"
6
- }
6
+ },
7
+ "hooks": "./hooks/hooks.json"
7
8
  }
@@ -0,0 +1,41 @@
1
+ {
2
+ "description": "Yeehaw session status tracking hooks",
3
+ "hooks": {
4
+ "PreToolUse": [
5
+ {
6
+ "matcher": "*",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-status.sh working",
11
+ "timeout": 5
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "Stop": [
17
+ {
18
+ "matcher": "*",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-status.sh idle",
23
+ "timeout": 5
24
+ }
25
+ ]
26
+ }
27
+ ],
28
+ "Notification": [
29
+ {
30
+ "matcher": "idle_prompt",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-status.sh waiting",
35
+ "timeout": 5
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }
41
+ }
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+ # Yeehaw Session Status Hook
3
+ # Writes session status to signal file for the Yeehaw CLI to read
4
+
5
+ STATUS="$1"
6
+ PANE_ID="${TMUX_PANE:-unknown}"
7
+ SIGNAL_DIR="$HOME/.yeehaw/session-signals"
8
+ SIGNAL_FILE="$SIGNAL_DIR/${PANE_ID//[^a-zA-Z0-9]/_}.json"
9
+
10
+ mkdir -p "$SIGNAL_DIR"
11
+ cat > "$SIGNAL_FILE" << EOF
12
+ {"status":"$STATUS","updated":$(date +%s)}
13
+ EOF
package/dist/app.js CHANGED
@@ -17,8 +17,8 @@ import { NightSkyView } from './views/NightSkyView.js';
17
17
  import { useConfig } from './hooks/useConfig.js';
18
18
  import { useSessions } from './hooks/useSessions.js';
19
19
  import { useRemoteYeehaw } from './hooks/useRemoteYeehaw.js';
20
- import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, enterRemoteMode, ensureCorrectStatusBar, } from './lib/tmux.js';
21
- import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig } from './lib/config.js';
20
+ import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, restartYeehaw, enterRemoteMode, ensureCorrectStatusBar, } from './lib/tmux.js';
21
+ import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig, addCritterToBarn, removeCritterFromBarn } from './lib/config.js';
22
22
  import { getVersionInfo } from './lib/update-check.js';
23
23
  function getHotkeyScope(view) {
24
24
  switch (view.type) {
@@ -39,37 +39,41 @@ function expandPath(path) {
39
39
  }
40
40
  return path;
41
41
  }
42
- // Global bottom bar items by view type
42
+ // Global bottom bar items - minimal, consistent across all views
43
43
  function getBottomBarItems(viewType) {
44
- const common = [
45
- { key: 'Tab', label: 'switch' },
46
- { key: '?', label: 'help' },
47
- ];
48
- if (viewType === 'global') {
49
- return [...common, { key: 'c', label: 'claude' }, { key: 'q', label: 'detach' }, { key: 'Q', label: 'quit' }];
50
- }
51
- if (viewType === 'project') {
52
- return [...common, { key: 'c', label: 'claude' }, { key: 'w', label: 'wiki' }, { key: 'i', label: 'issues' }, { key: 'Esc', label: 'back' }];
53
- }
54
- if (viewType === 'barn') {
55
- return [...common, { key: 's', label: 'shell' }, { key: 'Esc', label: 'back' }];
56
- }
57
- if (viewType === 'wiki') {
58
- return [...common, { key: 'Esc', label: 'back' }];
44
+ // Night sky has its own unique actions
45
+ if (viewType === 'night-sky') {
46
+ return [
47
+ { key: 'c', label: 'cloud' },
48
+ { key: 'r', label: 'randomize' },
49
+ { key: 'Esc', label: 'exit' },
50
+ ];
59
51
  }
60
- if (viewType === 'issues') {
61
- return [...common, { key: 'r', label: 'refresh' }, { key: 'o', label: 'open' }, { key: 'Esc', label: 'back' }];
52
+ // Global dashboard: exit options
53
+ if (viewType === 'global') {
54
+ return [
55
+ { key: 'q', label: 'detach' },
56
+ { key: 'Q', label: 'quit' },
57
+ { key: 'Tab', label: '' },
58
+ { key: '?', label: 'help' },
59
+ ];
62
60
  }
61
+ // Livestock detail: page-level actions
63
62
  if (viewType === 'livestock') {
64
- return [...common, { key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: 'Esc', label: 'back' }];
65
- }
66
- if (viewType === 'logs') {
67
- return [...common, { key: 'r', label: 'refresh' }, { key: 'Esc', label: 'back' }];
68
- }
69
- if (viewType === 'night-sky') {
70
- return [{ key: 'c', label: 'cloud' }, { key: 'r', label: 'randomize' }, { key: 'Esc', label: 'exit' }];
63
+ return [
64
+ { key: 's', label: 'shell' },
65
+ { key: 'l', label: 'logs' },
66
+ { key: 'e', label: 'edit' },
67
+ { key: 'Esc', label: 'back' },
68
+ { key: '?', label: 'help' },
69
+ ];
71
70
  }
72
- return [...common, { key: 'Esc', label: 'back' }];
71
+ // All other views: back + help
72
+ return [
73
+ { key: 'Esc', label: 'back' },
74
+ { key: 'Tab', label: '' },
75
+ { key: '?', label: 'help' },
76
+ ];
73
77
  }
74
78
  export function App() {
75
79
  const { exit } = useApp();
@@ -158,6 +162,38 @@ export function App() {
158
162
  setError(`Failed to create Claude session: ${message}`);
159
163
  }
160
164
  }, [tmuxAvailable, view, createClaude]);
165
+ const handleNewClaudeForProject = useCallback((project) => {
166
+ if (!tmuxAvailable) {
167
+ setError('tmux is not installed');
168
+ return;
169
+ }
170
+ try {
171
+ const workingDir = expandPath(project.path);
172
+ const windowName = `${project.name}-claude`;
173
+ const windowIndex = createClaude(workingDir, windowName);
174
+ switchToWindow(windowIndex);
175
+ }
176
+ catch (err) {
177
+ const message = err instanceof Error ? err.message : String(err);
178
+ setError(`Failed to create Claude session: ${message}`);
179
+ }
180
+ }, [tmuxAvailable, createClaude]);
181
+ const handleNewClaudeForLivestock = useCallback((livestock, projectName) => {
182
+ if (!tmuxAvailable) {
183
+ setError('tmux is not installed');
184
+ return;
185
+ }
186
+ try {
187
+ const workingDir = expandPath(livestock.path);
188
+ const windowName = `${projectName}-${livestock.name}-claude`;
189
+ const windowIndex = createClaude(workingDir, windowName);
190
+ switchToWindow(windowIndex);
191
+ }
192
+ catch (err) {
193
+ const message = err instanceof Error ? err.message : String(err);
194
+ setError(`Failed to create Claude session: ${message}`);
195
+ }
196
+ }, [tmuxAvailable, createClaude]);
161
197
  const handleOpenLivestockSession = useCallback((livestock, barn, projectName) => {
162
198
  if (!tmuxAvailable) {
163
199
  setError('tmux is not installed');
@@ -353,6 +389,11 @@ export function App() {
353
389
  exit();
354
390
  return;
355
391
  }
392
+ // Ctrl-R: Restart Yeehaw (preserves other tmux windows)
393
+ if (key.ctrl && input === 'r') {
394
+ restartYeehaw();
395
+ return;
396
+ }
356
397
  // ESC: Navigate back (handled by individual views for their sub-modes,
357
398
  // but also handled here as a fallback for consistent navigation)
358
399
  if (key.escape) {
@@ -401,9 +442,9 @@ export function App() {
401
442
  }
402
443
  switch (view.type) {
403
444
  case 'global':
404
- 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, onInputModeChange: setIsChildInputMode }));
445
+ return (_jsx(GlobalDashboard, { projects: projects, barns: barns, windows: windows, versionInfo: versionInfo, onSelectProject: handleSelectProject, onSelectBarn: handleSelectBarn, onSelectWindow: handleSelectWindow, onNewClaudeForProject: handleNewClaudeForProject, onCreateProject: handleCreateProject, onCreateBarn: handleCreateBarn, onSshToBarn: handleSshToBarn, onInputModeChange: setIsChildInputMode }));
405
446
  case 'project':
406
- 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) }));
447
+ return (_jsx(ProjectContext, { project: view.project, barns: barns, windows: windows, onBack: handleBack, onNewClaudeForLivestock: (livestock) => handleNewClaudeForLivestock(livestock, view.project.name), 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) }));
407
448
  case 'barn':
408
449
  const barnLivestock = getLivestockForBarn(view.barn.name);
409
450
  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) => {
@@ -428,6 +469,12 @@ export function App() {
428
469
  const updatedProject = { ...project, livestock: updatedLivestock };
429
470
  saveProject(updatedProject);
430
471
  reload();
472
+ }, onAddCritter: (critter) => {
473
+ addCritterToBarn(view.barn.name, critter);
474
+ reload();
475
+ }, onRemoveCritter: (critterName) => {
476
+ removeCritterFromBarn(view.barn.name, critterName);
477
+ reload();
431
478
  } }));
432
479
  case 'wiki':
433
480
  return (_jsx(WikiView, { project: view.project, onBack: () => handleBackFromSubview(view.project), onUpdateProject: handleUpdateProject }));
@@ -447,9 +494,11 @@ export function App() {
447
494
  return (_jsx(NightSkyView, { onExit: handleExitNightSky }));
448
495
  }
449
496
  };
497
+ // Memoized callback for splash screen to prevent unnecessary re-renders
498
+ const handleSplashComplete = useCallback(() => setShowSplash(false), []);
450
499
  // Show splash screen on first load
451
500
  if (showSplash) {
452
- return _jsx(SplashScreen, { onComplete: () => setShowSplash(false) });
501
+ return _jsx(SplashScreen, { onComplete: handleSplashComplete });
453
502
  }
454
503
  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 }))] }));
455
504
  }
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { getHotkeysGrouped } from '../lib/hotkeys.js';
4
+ // Yeehaw brand gold (darker for light mode readability)
5
+ const BRAND_COLOR = '#d4a020';
4
6
  function HotkeyRow({ hotkey }) {
5
- return (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: "cyan", children: hotkey.key }) }), _jsx(Text, { children: hotkey.description })] }));
7
+ return (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: BRAND_COLOR, children: hotkey.key }) }), _jsx(Text, { children: hotkey.description })] }));
6
8
  }
7
9
  function HotkeySection({ title, hotkeys }) {
8
10
  if (hotkeys.length === 0)
@@ -13,5 +15,5 @@ export function HelpOverlay({ scope, focusedPanel }) {
13
15
  const grouped = getHotkeysGrouped(scope, focusedPanel);
14
16
  // Also include list navigation if we're in a view with lists
15
17
  const listHotkeys = scope !== 'global' ? getHotkeysGrouped('list').navigation : [];
16
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Keyboard Shortcuts" }), _jsx(HotkeySection, { title: "Navigation", hotkeys: [...grouped.navigation, ...listHotkeys] }), _jsx(HotkeySection, { title: "Actions", hotkeys: grouped.action }), _jsx(HotkeySection, { title: "System", hotkeys: grouped.system }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press ? to close" }) })] }));
18
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: BRAND_COLOR, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: BRAND_COLOR, children: "Keyboard Shortcuts" }), _jsx(HotkeySection, { title: "Navigation", hotkeys: [...grouped.navigation, ...listHotkeys] }), _jsx(HotkeySection, { title: "Actions", hotkeys: grouped.action }), _jsx(HotkeySection, { title: "System", hotkeys: grouped.system }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press ? to close" }) })] }));
17
19
  }
@@ -1,15 +1,22 @@
1
+ import type { SessionStatus } from '../lib/signals.js';
2
+ export interface RowAction {
3
+ key: string;
4
+ label: string;
5
+ }
1
6
  export interface ListItem {
2
7
  id: string;
3
8
  label: string;
4
9
  status?: 'active' | 'inactive' | 'error';
5
10
  meta?: string;
11
+ sessionStatus?: SessionStatus;
12
+ actions?: RowAction[];
6
13
  }
7
14
  interface ListProps {
8
15
  items: ListItem[];
9
16
  focused?: boolean;
10
17
  selectedIndex?: number;
11
18
  onSelect?: (item: ListItem) => void;
12
- onAction?: (item: ListItem) => void;
19
+ onAction?: (item: ListItem, action: string) => void;
13
20
  onHighlight?: (item: ListItem | null) => void;
14
21
  onSelectionChange?: (index: number) => void;
15
22
  }
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
+ // Yeehaw brand gold (darker for light mode readability)
5
+ const BRAND_COLOR = '#d4a020';
4
6
  export function List({ items, focused = false, selectedIndex: controlledIndex, onSelect, onAction, onHighlight, onSelectionChange }) {
5
7
  const [internalIndex, setInternalIndex] = useState(0);
6
8
  // Use controlled index if provided, otherwise internal
@@ -35,8 +37,13 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
35
37
  if (key.return && items[selectedIndex] && onSelect) {
36
38
  onSelect(items[selectedIndex]);
37
39
  }
38
- if (input === 's' && items[selectedIndex] && onAction) {
39
- onAction(items[selectedIndex]);
40
+ // Handle row-level actions (replaces hardcoded 's' check)
41
+ const currentItem = items[selectedIndex];
42
+ if (currentItem?.actions && onAction) {
43
+ const action = currentItem.actions.find(a => a.key === input);
44
+ if (action) {
45
+ onAction(currentItem, action.key);
46
+ }
40
47
  }
41
48
  });
42
49
  if (items.length === 0) {
@@ -44,10 +51,12 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
44
51
  }
45
52
  return (_jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
46
53
  const isSelected = index === selectedIndex && focused;
54
+ // Session status takes priority for meta coloring
55
+ const sessionStatusColor = item.sessionStatus === 'waiting' ? 'yellow' :
56
+ item.sessionStatus === 'working' ? 'cyan' :
57
+ item.sessionStatus === 'error' ? 'red' : undefined;
47
58
  const statusColor = item.status === 'active' ? 'green' :
48
59
  item.status === 'error' ? 'red' : 'gray';
49
- // Yeehaw brand gold for selection
50
- const selectionColor = '#f0c040';
51
- return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isSelected ? selectionColor : undefined, children: isSelected ? '›' : ' ' }, "arrow"), _jsx(Text, { color: isSelected ? selectionColor : undefined, bold: isSelected, children: item.label }, "label"), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" }, "status")), item.meta && (_jsx(Text, { dimColor: true, children: item.meta }, "meta"))] }, item.id));
60
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 0, width: 2, children: _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, children: isSelected ? '›' : ' ' }) }), _jsxs(Box, { gap: 1, flexShrink: 1, flexGrow: 1, overflow: "hidden", children: [_jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, bold: isSelected, wrap: "truncate", children: item.label }), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" })), item.meta && (_jsx(Text, { color: sessionStatusColor, dimColor: !sessionStatusColor, wrap: "truncate", children: item.meta }))] }), isSelected && item.actions && item.actions.length > 0 && (_jsx(Box, { gap: 2, flexShrink: 0, marginLeft: 1, children: item.actions.map((action) => (_jsxs(Text, { children: [_jsxs(Text, { color: BRAND_COLOR, children: ["[", action.key, "]"] }), _jsxs(Text, { dimColor: true, children: [" ", action.label] })] }, action.key))) }))] }, item.id));
52
61
  }) }));
53
62
  }
@@ -1,5 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ // Yeehaw brand gold (darker for light mode readability)
4
+ const BRAND_COLOR = '#d4a020';
5
+ // Render hints with gold keys and gray labels: "[n] new" -> gold "[n]" + gray " new"
6
+ function renderHints(hints) {
7
+ const parts = [];
8
+ // Match [key] patterns and split around them
9
+ const regex = /(\[[^\]]+\])/g;
10
+ let lastIndex = 0;
11
+ let match;
12
+ let keyIndex = 0;
13
+ while ((match = regex.exec(hints)) !== null) {
14
+ // Add text before the match (gray)
15
+ if (match.index > lastIndex) {
16
+ parts.push(_jsx(Text, { dimColor: true, children: hints.slice(lastIndex, match.index) }, `text-${keyIndex}`));
17
+ }
18
+ // Add the [key] part (gold)
19
+ parts.push(_jsx(Text, { color: BRAND_COLOR, children: match[1] }, `key-${keyIndex}`));
20
+ lastIndex = regex.lastIndex;
21
+ keyIndex++;
22
+ }
23
+ // Add remaining text after last match (gray)
24
+ if (lastIndex < hints.length) {
25
+ parts.push(_jsx(Text, { dimColor: true, children: hints.slice(lastIndex) }, `text-end`));
26
+ }
27
+ return parts;
28
+ }
3
29
  export function Panel({ title, children, focused = false, width, hints }) {
4
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', width: width, children: [_jsxs(Box, { paddingX: 1, marginBottom: 0, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: focused ? 'cyan' : 'white', children: title }), focused && hints && (_jsx(Text, { color: "cyan", children: hints }))] }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: children })] }));
30
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? BRAND_COLOR : 'gray', width: width, children: [_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsx(Text, { bold: true, color: focused ? BRAND_COLOR : 'gray', children: title }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: children }), _jsx(Box, { paddingX: 1, justifyContent: "flex-end", height: 1, children: focused && hints ? (_jsx(Text, { children: renderHints(hints) })) : (_jsx(Text, { children: " " })) })] }));
5
31
  }
@@ -10,7 +10,7 @@ const TUMBLEWEED = [
10
10
  ' ░▒░ ░▒░ ░',
11
11
  ];
12
12
  const TUMBLEWEED_COLOR = '#b8860b';
13
- const BRAND_COLOR = '#f0c040';
13
+ const BRAND_COLOR = '#d4a020'; // Darker for light mode readability
14
14
  // Match Header.tsx positioning exactly
15
15
  const HEADER_PADDING_TOP = 1;
16
16
  const TUMBLEWEED_TOP_PADDING = 1;
@@ -18,8 +18,8 @@ export function useSessions() {
18
18
  const result = listYeehawWindows();
19
19
  setWindows(result);
20
20
  setLoading(false);
21
- // Poll every 5 seconds for window updates (without loading state changes)
22
- const interval = setInterval(refresh, 5000);
21
+ // Poll for window updates (without loading state changes)
22
+ const interval = setInterval(refresh, 800);
23
23
  return () => clearInterval(interval);
24
24
  }, [refresh]);
25
25
  const createClaude = useCallback((workingDir, name) => {
package/dist/index.js CHANGED
@@ -5,7 +5,43 @@ import { App } from './app.js';
5
5
  import { isInsideYeehawSession, yeehawSessionExists, createYeehawSession, attachToYeehaw, hasTmux, } from './lib/tmux.js';
6
6
  import { ensureConfigDirs } from './lib/config.js';
7
7
  import { checkForUpdates, formatUpdateMessage } from './lib/update-check.js';
8
+ import { installHookScript, getClaudeHooksConfig, checkClaudeHooksInstalled } from './lib/hooks.js';
9
+ /**
10
+ * Handle CLI subcommands
11
+ */
12
+ function handleSubcommands() {
13
+ const args = process.argv.slice(2);
14
+ if (args[0] === 'hooks' && args[1] === 'install') {
15
+ const scriptPath = installHookScript();
16
+ console.log(`\x1b[32m✓\x1b[0m Hook script installed: ${scriptPath}`);
17
+ console.log('');
18
+ console.log('\x1b[33mNote:\x1b[0m Claude sessions started from Yeehaw already have hooks enabled.');
19
+ console.log('This command is only needed for Claude sessions started outside Yeehaw.');
20
+ if (checkClaudeHooksInstalled()) {
21
+ console.log('\n\x1b[32m✓\x1b[0m Claude hooks already configured in ~/.claude/settings.json');
22
+ }
23
+ else {
24
+ console.log('\nTo enable status tracking for external Claude sessions,');
25
+ console.log('add this to ~/.claude/settings.json:');
26
+ console.log(JSON.stringify(getClaudeHooksConfig(), null, 2));
27
+ }
28
+ return true;
29
+ }
30
+ if (args[0] === 'hooks') {
31
+ console.log('Usage: yeehaw hooks install');
32
+ console.log('');
33
+ console.log('Install Claude hooks for session status tracking.');
34
+ console.log('Note: Sessions started from Yeehaw already have hooks enabled automatically.');
35
+ console.log('This is only needed for Claude sessions started outside Yeehaw.');
36
+ return true;
37
+ }
38
+ return false;
39
+ }
8
40
  function main() {
41
+ // Handle subcommands first (before tmux checks)
42
+ if (handleSubcommands()) {
43
+ process.exit(0);
44
+ }
9
45
  // Ensure config directories exist
10
46
  ensureConfigDirs();
11
47
  // Check for updates (non-blocking, uses cache)
@@ -27,7 +63,11 @@ function main() {
27
63
  }
28
64
  // If we're already inside the yeehaw tmux session, just render the TUI
29
65
  if (isInsideYeehawSession()) {
30
- render(_jsx(App, {}));
66
+ render(_jsx(App, {}), {
67
+ patchConsole: true,
68
+ incrementalRendering: true,
69
+ // maxFps: 60,
70
+ });
31
71
  return;
32
72
  }
33
73
  // We're not inside yeehaw session - need to create/attach
@@ -1,4 +1,4 @@
1
- import type { Config, Project, Barn, Livestock } from '../types.js';
1
+ import type { Config, Project, Barn, Livestock, Critter } from '../types.js';
2
2
  export declare const LOCAL_BARN: Barn;
3
3
  export declare function isLocalBarn(barn: Barn): boolean;
4
4
  /**
@@ -25,3 +25,15 @@ export declare function getLivestockForBarn(barnName: string): Array<{
25
25
  project: Project;
26
26
  livestock: Livestock;
27
27
  }>;
28
+ /**
29
+ * Add a critter to a barn
30
+ */
31
+ export declare function addCritterToBarn(barnName: string, critter: Critter): void;
32
+ /**
33
+ * Remove a critter from a barn
34
+ */
35
+ export declare function removeCritterFromBarn(barnName: string, critterName: string): boolean;
36
+ /**
37
+ * Get a specific critter from a barn
38
+ */
39
+ export declare function getCritter(barnName: string, critterName: string): Critter | undefined;
@@ -148,3 +148,54 @@ export function getLivestockForBarn(barnName) {
148
148
  }
149
149
  return result;
150
150
  }
151
+ // ============================================================================
152
+ // Critter operations
153
+ // ============================================================================
154
+ /**
155
+ * Add a critter to a barn
156
+ */
157
+ export function addCritterToBarn(barnName, critter) {
158
+ const barn = loadBarn(barnName);
159
+ if (!barn) {
160
+ throw new Error(`Barn not found: ${barnName}`);
161
+ }
162
+ barn.critters = barn.critters || [];
163
+ // Check for duplicate
164
+ if (barn.critters.some(c => c.name === critter.name)) {
165
+ throw new Error(`Critter "${critter.name}" already exists on barn "${barnName}"`);
166
+ }
167
+ barn.critters.push(critter);
168
+ // Only save to file if it's not the local barn
169
+ if (barnName !== 'local') {
170
+ saveBarn(barn);
171
+ }
172
+ }
173
+ /**
174
+ * Remove a critter from a barn
175
+ */
176
+ export function removeCritterFromBarn(barnName, critterName) {
177
+ const barn = loadBarn(barnName);
178
+ if (!barn) {
179
+ throw new Error(`Barn not found: ${barnName}`);
180
+ }
181
+ const originalLength = (barn.critters || []).length;
182
+ barn.critters = (barn.critters || []).filter(c => c.name !== critterName);
183
+ if (barn.critters.length === originalLength) {
184
+ return false; // Critter wasn't found
185
+ }
186
+ // Only save to file if it's not the local barn
187
+ if (barnName !== 'local') {
188
+ saveBarn(barn);
189
+ }
190
+ return true;
191
+ }
192
+ /**
193
+ * Get a specific critter from a barn
194
+ */
195
+ export function getCritter(barnName, critterName) {
196
+ const barn = loadBarn(barnName);
197
+ if (!barn) {
198
+ return undefined;
199
+ }
200
+ return barn.critters?.find(c => c.name === critterName);
201
+ }
@@ -0,0 +1,28 @@
1
+ import type { Critter, Barn } from '../types.js';
2
+ /**
3
+ * Discovered critter from scanning a barn
4
+ */
5
+ export interface DiscoveredCritter {
6
+ service: string;
7
+ suggested_name: string;
8
+ binary?: string;
9
+ config_path?: string;
10
+ status: 'running' | 'stopped';
11
+ }
12
+ /**
13
+ * Read logs from a critter (via journald or custom path)
14
+ */
15
+ export declare function readCritterLogs(critter: Critter, barn: Barn, options?: {
16
+ lines?: number;
17
+ pattern?: string;
18
+ }): Promise<{
19
+ content: string;
20
+ error?: string;
21
+ }>;
22
+ /**
23
+ * Discover critters (running services) on a barn
24
+ */
25
+ export declare function discoverCritters(barn: Barn): Promise<{
26
+ critters: DiscoveredCritter[];
27
+ error?: string;
28
+ }>;