@colmbus72/yeehaw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +414 -0
  5. package/dist/components/BarnHeader.d.ts +6 -0
  6. package/dist/components/BarnHeader.js +21 -0
  7. package/dist/components/BottomBar.d.ts +16 -0
  8. package/dist/components/BottomBar.js +7 -0
  9. package/dist/components/Header.d.ts +8 -0
  10. package/dist/components/Header.js +83 -0
  11. package/dist/components/HelpOverlay.d.ts +7 -0
  12. package/dist/components/HelpOverlay.js +17 -0
  13. package/dist/components/List.d.ts +17 -0
  14. package/dist/components/List.js +53 -0
  15. package/dist/components/Markdown.d.ts +8 -0
  16. package/dist/components/Markdown.js +23 -0
  17. package/dist/components/Panel.d.ts +10 -0
  18. package/dist/components/Panel.js +5 -0
  19. package/dist/components/PathInput.d.ts +9 -0
  20. package/dist/components/PathInput.js +141 -0
  21. package/dist/components/ScrollableMarkdown.d.ts +11 -0
  22. package/dist/components/ScrollableMarkdown.js +56 -0
  23. package/dist/components/StatusBar.d.ts +5 -0
  24. package/dist/components/StatusBar.js +20 -0
  25. package/dist/components/TextArea.d.ts +17 -0
  26. package/dist/components/TextArea.js +140 -0
  27. package/dist/components/index.d.ts +5 -0
  28. package/dist/components/index.js +5 -0
  29. package/dist/hooks/index.d.ts +3 -0
  30. package/dist/hooks/index.js +3 -0
  31. package/dist/hooks/useConfig.d.ts +11 -0
  32. package/dist/hooks/useConfig.js +36 -0
  33. package/dist/hooks/useRemoteYeehaw.d.ts +13 -0
  34. package/dist/hooks/useRemoteYeehaw.js +49 -0
  35. package/dist/hooks/useSessions.d.ts +11 -0
  36. package/dist/hooks/useSessions.js +46 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +34 -0
  39. package/dist/lib/config.d.ts +27 -0
  40. package/dist/lib/config.js +150 -0
  41. package/dist/lib/detection.d.ts +16 -0
  42. package/dist/lib/detection.js +41 -0
  43. package/dist/lib/editor.d.ts +5 -0
  44. package/dist/lib/editor.js +35 -0
  45. package/dist/lib/errors.d.ts +28 -0
  46. package/dist/lib/errors.js +48 -0
  47. package/dist/lib/git.d.ts +11 -0
  48. package/dist/lib/git.js +73 -0
  49. package/dist/lib/github.d.ts +43 -0
  50. package/dist/lib/github.js +111 -0
  51. package/dist/lib/hotkeys.d.ts +27 -0
  52. package/dist/lib/hotkeys.js +92 -0
  53. package/dist/lib/index.d.ts +10 -0
  54. package/dist/lib/index.js +10 -0
  55. package/dist/lib/livestock.d.ts +51 -0
  56. package/dist/lib/livestock.js +233 -0
  57. package/dist/lib/mcp-validation.d.ts +33 -0
  58. package/dist/lib/mcp-validation.js +62 -0
  59. package/dist/lib/paths.d.ts +8 -0
  60. package/dist/lib/paths.js +28 -0
  61. package/dist/lib/shell.d.ts +34 -0
  62. package/dist/lib/shell.js +61 -0
  63. package/dist/lib/ssh.d.ts +15 -0
  64. package/dist/lib/ssh.js +77 -0
  65. package/dist/lib/tmux-config.d.ts +3 -0
  66. package/dist/lib/tmux-config.js +42 -0
  67. package/dist/lib/tmux.d.ts +32 -0
  68. package/dist/lib/tmux.js +397 -0
  69. package/dist/mcp-server.d.ts +23 -0
  70. package/dist/mcp-server.js +825 -0
  71. package/dist/types.d.ts +89 -0
  72. package/dist/types.js +2 -0
  73. package/dist/views/BarnContext.d.ts +22 -0
  74. package/dist/views/BarnContext.js +252 -0
  75. package/dist/views/GlobalDashboard.d.ts +16 -0
  76. package/dist/views/GlobalDashboard.js +253 -0
  77. package/dist/views/Home.d.ts +11 -0
  78. package/dist/views/Home.js +27 -0
  79. package/dist/views/IssuesView.d.ts +7 -0
  80. package/dist/views/IssuesView.js +157 -0
  81. package/dist/views/LivestockDetailView.d.ts +11 -0
  82. package/dist/views/LivestockDetailView.js +140 -0
  83. package/dist/views/LogsView.d.ts +8 -0
  84. package/dist/views/LogsView.js +84 -0
  85. package/dist/views/NightSkyView.d.ts +5 -0
  86. package/dist/views/NightSkyView.js +441 -0
  87. package/dist/views/ProjectContext.d.ts +18 -0
  88. package/dist/views/ProjectContext.js +333 -0
  89. package/dist/views/Projects.d.ts +8 -0
  90. package/dist/views/Projects.js +20 -0
  91. package/dist/views/WikiView.d.ts +8 -0
  92. package/dist/views/WikiView.js +138 -0
  93. package/dist/views/index.d.ts +2 -0
  94. package/dist/views/index.js +2 -0
  95. package/package.json +65 -0
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { Panel } from '../components/Panel.js';
4
+ import { List } from '../components/List.js';
5
+ export function Home({ barns, sessions, focusedPanel, onSelectBarn, onSelectSession }) {
6
+ const barnItems = barns.map((barn) => ({
7
+ id: barn.name,
8
+ label: barn.name,
9
+ status: 'active', // TODO: actual status check
10
+ meta: barn.host,
11
+ }));
12
+ const sessionItems = sessions.map((session) => ({
13
+ id: session.name,
14
+ label: session.name.replace(/^yh-/, ''),
15
+ status: session.attached ? 'active' : 'inactive',
16
+ meta: session.attached ? 'attached' : 'detached',
17
+ }));
18
+ return (_jsxs(Box, { gap: 2, children: [_jsx(Panel, { title: "Barns", focused: focusedPanel === 'barns', width: "50%", children: _jsx(List, { items: barnItems, focused: focusedPanel === 'barns', onSelect: (item) => {
19
+ const barn = barns.find((b) => b.name === item.id);
20
+ if (barn)
21
+ onSelectBarn(barn);
22
+ } }) }), _jsx(Panel, { title: "Sessions", focused: focusedPanel === 'sessions', width: "50%", children: _jsx(List, { items: sessionItems, focused: focusedPanel === 'sessions', onSelect: (item) => {
23
+ const session = sessions.find((s) => s.name === item.id);
24
+ if (session)
25
+ onSelectSession(session);
26
+ } }) })] }));
27
+ }
@@ -0,0 +1,7 @@
1
+ import type { Project } from '../types.js';
2
+ interface IssuesViewProps {
3
+ project: Project;
4
+ onBack: () => void;
5
+ }
6
+ export declare function IssuesView({ project, onBack }: IssuesViewProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,157 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { Header } from '../components/Header.js';
5
+ import { Panel } from '../components/Panel.js';
6
+ import { List } from '../components/List.js';
7
+ import { ScrollableMarkdown } from '../components/ScrollableMarkdown.js';
8
+ import { parseGitHubUrl, fetchGitHubIssues, openIssueInBrowser, isGhCliAvailable, } from '../lib/github.js';
9
+ export function IssuesView({ project, onBack }) {
10
+ const [focusedPanel, setFocusedPanel] = useState('list');
11
+ const [selectedIndex, setSelectedIndex] = useState(0);
12
+ const [issues, setIssues] = useState([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [error, setError] = useState(null);
15
+ // Extract unique GitHub repos from local livestock
16
+ const getLocalRepos = () => {
17
+ const livestock = project.livestock || [];
18
+ const localLivestock = livestock.filter((l) => !l.barn && l.repo);
19
+ const seen = new Set();
20
+ const repos = [];
21
+ for (const l of localLivestock) {
22
+ if (!l.repo)
23
+ continue; // TypeScript narrows l.repo to string
24
+ const parsed = parseGitHubUrl(l.repo);
25
+ if (parsed) {
26
+ const key = `${parsed.owner}/${parsed.repo}`;
27
+ if (!seen.has(key)) {
28
+ seen.add(key);
29
+ repos.push({ ...parsed, name: l.name });
30
+ }
31
+ }
32
+ }
33
+ return repos;
34
+ };
35
+ // Fetch issues on mount
36
+ useEffect(() => {
37
+ const loadIssues = () => {
38
+ setLoading(true);
39
+ setError(null);
40
+ if (!isGhCliAvailable()) {
41
+ setError('GitHub CLI (gh) not found or not authenticated. Run: gh auth login');
42
+ setLoading(false);
43
+ return;
44
+ }
45
+ const repos = getLocalRepos();
46
+ if (repos.length === 0) {
47
+ setError('No GitHub repos found in local livestock');
48
+ setLoading(false);
49
+ return;
50
+ }
51
+ const allIssues = [];
52
+ for (const repo of repos) {
53
+ const repoIssues = fetchGitHubIssues(repo.owner, repo.repo);
54
+ for (const issue of repoIssues) {
55
+ allIssues.push({
56
+ ...issue,
57
+ repoName: repo.name,
58
+ fullRepo: `${repo.owner}/${repo.repo}`,
59
+ });
60
+ }
61
+ }
62
+ // Sort by updated date (most recent first)
63
+ allIssues.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
64
+ setIssues(allIssues);
65
+ setLoading(false);
66
+ };
67
+ loadIssues();
68
+ }, [project]);
69
+ const selectedIssue = issues[selectedIndex];
70
+ // Handle input
71
+ useInput((input, key) => {
72
+ if (key.escape || input === 'q') {
73
+ onBack();
74
+ return;
75
+ }
76
+ // Tab to switch focus between panels
77
+ if (key.tab) {
78
+ setFocusedPanel((prev) => (prev === 'list' ? 'details' : 'list'));
79
+ return;
80
+ }
81
+ // Refresh issues
82
+ if (input === 'r') {
83
+ setLoading(true);
84
+ setError(null);
85
+ // Trigger re-fetch by updating a dependency
86
+ const repos = getLocalRepos();
87
+ const allIssues = [];
88
+ for (const repo of repos) {
89
+ const repoIssues = fetchGitHubIssues(repo.owner, repo.repo);
90
+ for (const issue of repoIssues) {
91
+ allIssues.push({
92
+ ...issue,
93
+ repoName: repo.name,
94
+ fullRepo: `${repo.owner}/${repo.repo}`,
95
+ });
96
+ }
97
+ }
98
+ allIssues.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
99
+ setIssues(allIssues);
100
+ setSelectedIndex(0);
101
+ setLoading(false);
102
+ return;
103
+ }
104
+ // Open in browser
105
+ if (input === 'o' && selectedIssue) {
106
+ openIssueInBrowser(selectedIssue.url);
107
+ return;
108
+ }
109
+ // List navigation is handled by the List component via controlled selection
110
+ });
111
+ // Truncate title if too long
112
+ const truncate = (str, maxLen) => {
113
+ if (str.length <= maxLen)
114
+ return str;
115
+ return str.slice(0, maxLen - 1) + '…';
116
+ };
117
+ // Build list items - put repo tag as prefix, truncate long titles
118
+ const issueItems = issues.map((issue, i) => ({
119
+ id: String(i),
120
+ label: `[${issue.repoName}] #${issue.number} ${truncate(issue.title, 35)}`,
121
+ status: issue.state === 'open' ? 'active' : 'inactive',
122
+ }));
123
+ // Build issue details markdown
124
+ const buildIssueDetails = (issue) => {
125
+ const lines = [];
126
+ lines.push(`**State:** ${issue.state}`);
127
+ lines.push(`**Author:** @${issue.author}`);
128
+ lines.push(`**Repo:** ${issue.fullRepo}`);
129
+ if (issue.labels.length > 0) {
130
+ lines.push(`**Labels:** ${issue.labels.join(', ')}`);
131
+ }
132
+ lines.push(`**Comments:** ${issue.commentsCount}`);
133
+ lines.push(`**Updated:** ${new Date(issue.updatedAt).toLocaleDateString()}`);
134
+ lines.push('');
135
+ lines.push('---');
136
+ lines.push('');
137
+ if (issue.body) {
138
+ lines.push(issue.body);
139
+ }
140
+ else {
141
+ lines.push('*No description provided.*');
142
+ }
143
+ return lines.join('\n');
144
+ };
145
+ // Panel-specific hints (page-level hotkeys like r/o are in BottomBar)
146
+ const listHints = '';
147
+ const detailsHints = 'j/k scroll';
148
+ // Loading state
149
+ if (loading) {
150
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsx(Box, { padding: 2, children: _jsx(Text, { children: "Loading issues..." }) })] }));
151
+ }
152
+ // Error state
153
+ if (error) {
154
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsx(Box, { padding: 2, flexDirection: "column", children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })] }));
155
+ }
156
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsxs(Box, { flexGrow: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Issues", focused: focusedPanel === 'list', width: "40%", hints: listHints, children: issueItems.length > 0 ? (_jsx(List, { items: issueItems, focused: focusedPanel === 'list', selectedIndex: selectedIndex, onSelectionChange: setSelectedIndex })) : (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No open issues" }) })) }), _jsx(Panel, { title: selectedIssue ? `#${selectedIssue.number} ${selectedIssue.title}` : 'Details', focused: focusedPanel === 'details', width: "60%", hints: detailsHints, children: selectedIssue ? (_jsx(ScrollableMarkdown, { focused: focusedPanel === 'details', height: 20, children: buildIssueDetails(selectedIssue) })) : (_jsx(Text, { dimColor: true, children: "Select an issue to view details" })) })] })] }));
157
+ }
@@ -0,0 +1,11 @@
1
+ import type { Project, Livestock } from '../types.js';
2
+ interface LivestockDetailViewProps {
3
+ project: Project;
4
+ livestock: Livestock;
5
+ onBack: () => void;
6
+ onOpenLogs: () => void;
7
+ onOpenSession: () => void;
8
+ onUpdateLivestock: (livestock: Livestock) => void;
9
+ }
10
+ export declare function LivestockDetailView({ project, livestock, onBack, onOpenLogs, onOpenSession, onUpdateLivestock, }: LivestockDetailViewProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { Header } from '../components/Header.js';
6
+ import { Panel } from '../components/Panel.js';
7
+ import { PathInput } from '../components/PathInput.js';
8
+ import { loadBarn } from '../lib/config.js';
9
+ export function LivestockDetailView({ project, livestock, onBack, onOpenLogs, onOpenSession, onUpdateLivestock, }) {
10
+ const [mode, setMode] = useState('normal');
11
+ // Edit form state
12
+ const [editName, setEditName] = useState(livestock.name);
13
+ const [editPath, setEditPath] = useState(livestock.path);
14
+ const [editRepo, setEditRepo] = useState(livestock.repo || '');
15
+ const [editBranch, setEditBranch] = useState(livestock.branch || '');
16
+ const [editLogPath, setEditLogPath] = useState(livestock.log_path || '');
17
+ const [editEnvPath, setEditEnvPath] = useState(livestock.env_path || '');
18
+ // Get barn info if remote
19
+ const barn = livestock.barn ? loadBarn(livestock.barn) : null;
20
+ const resetForm = () => {
21
+ setEditName(livestock.name);
22
+ setEditPath(livestock.path);
23
+ setEditRepo(livestock.repo || '');
24
+ setEditBranch(livestock.branch || '');
25
+ setEditLogPath(livestock.log_path || '');
26
+ setEditEnvPath(livestock.env_path || '');
27
+ };
28
+ const saveEdit = (field, value) => {
29
+ const updated = { ...livestock };
30
+ switch (field) {
31
+ case 'name':
32
+ updated.name = value;
33
+ break;
34
+ case 'path':
35
+ updated.path = value;
36
+ break;
37
+ case 'repo':
38
+ updated.repo = value || undefined;
39
+ break;
40
+ case 'branch':
41
+ updated.branch = value || undefined;
42
+ break;
43
+ case 'log_path':
44
+ updated.log_path = value || undefined;
45
+ break;
46
+ case 'env_path':
47
+ updated.env_path = value || undefined;
48
+ break;
49
+ }
50
+ onUpdateLivestock(updated);
51
+ setMode('normal');
52
+ };
53
+ useInput((input, key) => {
54
+ // Handle escape - works in all modes
55
+ if (key.escape) {
56
+ if (mode !== 'normal') {
57
+ setMode('normal');
58
+ resetForm();
59
+ }
60
+ else {
61
+ onBack();
62
+ }
63
+ return;
64
+ }
65
+ // Only process these in normal mode
66
+ if (mode !== 'normal')
67
+ return;
68
+ if (input === 'q') {
69
+ onBack();
70
+ return;
71
+ }
72
+ if (input === 's') {
73
+ onOpenSession();
74
+ return;
75
+ }
76
+ if (input === 'l') {
77
+ if (!livestock.log_path) {
78
+ // Could show an error, but for now just ignore
79
+ return;
80
+ }
81
+ onOpenLogs();
82
+ return;
83
+ }
84
+ if (input === 'e') {
85
+ // Start edit flow with name
86
+ setMode('edit-name');
87
+ return;
88
+ }
89
+ });
90
+ // Edit name
91
+ if (mode === 'edit-name') {
92
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: editName, onChange: setEditName, onSubmit: () => {
93
+ if (editName.trim()) {
94
+ saveEdit('name', editName.trim());
95
+ setMode('edit-path');
96
+ }
97
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
98
+ }
99
+ // Edit path
100
+ if (mode === 'edit-path') {
101
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: editPath, onChange: setEditPath, onSubmit: () => {
102
+ if (editPath.trim()) {
103
+ saveEdit('path', editPath.trim());
104
+ setMode('edit-repo');
105
+ }
106
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Tab: autocomplete, Esc: cancel" }) })] })] }));
107
+ }
108
+ // Edit repo
109
+ if (mode === 'edit-repo') {
110
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Repo (optional): " }), _jsx(TextInput, { value: editRepo, onChange: setEditRepo, onSubmit: () => {
111
+ saveEdit('repo', editRepo.trim());
112
+ setMode('edit-branch');
113
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
114
+ }
115
+ // Edit branch
116
+ if (mode === 'edit-branch') {
117
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Branch (optional): " }), _jsx(TextInput, { value: editBranch, onChange: setEditBranch, onSubmit: () => {
118
+ saveEdit('branch', editBranch.trim());
119
+ setMode('edit-log-path');
120
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
121
+ }
122
+ // Edit log path
123
+ if (mode === 'edit-log-path') {
124
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Log Path (optional, relative): " }), _jsx(TextInput, { value: editLogPath, onChange: setEditLogPath, onSubmit: () => {
125
+ saveEdit('log_path', editLogPath.trim());
126
+ setMode('edit-env-path');
127
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
128
+ }
129
+ // Edit env path
130
+ if (mode === 'edit-env-path') {
131
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Env Path (optional, relative): " }), _jsx(TextInput, { value: editEnvPath, onChange: setEditEnvPath, onSubmit: () => {
132
+ saveEdit('env_path', editEnvPath.trim());
133
+ // All done - return to normal mode
134
+ setMode('normal');
135
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save & finish, Esc: cancel" }) })] })] }));
136
+ }
137
+ // Normal view - show livestock info
138
+ const hints = `[s] shell ${livestock.log_path ? '[l] logs ' : ''}[e] edit [q] back`;
139
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: livestock.name, color: project.color }), _jsx(Box, { flexGrow: 1, paddingX: 1, children: _jsx(Panel, { title: "Livestock Details", focused: true, hints: hints, children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Name:" }) }), _jsx(Text, { children: livestock.name })] }), _jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Path:" }) }), _jsx(Text, { children: livestock.path })] }), _jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Location:" }) }), barn ? (_jsxs(Text, { children: [barn.name, " ", _jsxs(Text, { dimColor: true, children: ["(", barn.host, ")"] })] })) : (_jsx(Text, { children: "Local" }))] }), livestock.repo && (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Repo:" }) }), _jsx(Text, { children: livestock.repo })] })), livestock.branch && (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Branch:" }) }), _jsx(Text, { children: livestock.branch })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: "yellow", children: "Operational Config" }) }), _jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Log Path:" }) }), livestock.log_path ? (_jsx(Text, { children: livestock.log_path })) : (_jsx(Text, { dimColor: true, children: "Not configured" }))] }), _jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Env Path:" }) }), livestock.env_path ? (_jsx(Text, { children: livestock.env_path })) : (_jsx(Text, { dimColor: true, children: "Not configured" }))] })] }) }) })] }));
140
+ }
@@ -0,0 +1,8 @@
1
+ import type { Project, Livestock } from '../types.js';
2
+ interface LogsViewProps {
3
+ project: Project;
4
+ livestock: Livestock;
5
+ onBack: () => void;
6
+ }
7
+ export declare function LogsView({ project, livestock, onBack }: LogsViewProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { Header } from '../components/Header.js';
5
+ import { Panel } from '../components/Panel.js';
6
+ import { readLivestockLogs } from '../lib/livestock.js';
7
+ export function LogsView({ project, livestock, onBack }) {
8
+ const [logs, setLogs] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ const [error, setError] = useState(null);
11
+ const [scrollOffset, setScrollOffset] = useState(0);
12
+ // Calculate visible height (leave room for header, panel border, hints)
13
+ const visibleHeight = 20;
14
+ const fetchLogs = async () => {
15
+ setLoading(true);
16
+ setError(null);
17
+ const result = await readLivestockLogs(livestock, { lines: 200 });
18
+ if (result.error) {
19
+ setError(result.error);
20
+ setLogs([]);
21
+ }
22
+ else {
23
+ const lines = result.content.split('\n');
24
+ setLogs(lines);
25
+ // Scroll to bottom by default (most recent logs)
26
+ setScrollOffset(Math.max(0, lines.length - visibleHeight));
27
+ }
28
+ setLoading(false);
29
+ };
30
+ // Fetch logs on mount
31
+ useEffect(() => {
32
+ fetchLogs();
33
+ }, [livestock]);
34
+ const totalLines = logs.length;
35
+ useInput((input, key) => {
36
+ if (key.escape || input === 'q') {
37
+ onBack();
38
+ return;
39
+ }
40
+ // Refresh logs
41
+ if (input === 'r') {
42
+ fetchLogs();
43
+ return;
44
+ }
45
+ // Scroll navigation
46
+ if (input === 'j' || key.downArrow) {
47
+ setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, totalLines - visibleHeight)));
48
+ return;
49
+ }
50
+ if (input === 'k' || key.upArrow) {
51
+ setScrollOffset((prev) => Math.max(prev - 1, 0));
52
+ return;
53
+ }
54
+ if (input === 'g') {
55
+ setScrollOffset(0);
56
+ return;
57
+ }
58
+ if (input === 'G') {
59
+ setScrollOffset(Math.max(0, totalLines - visibleHeight));
60
+ return;
61
+ }
62
+ if (key.pageDown) {
63
+ setScrollOffset((prev) => Math.min(prev + visibleHeight, Math.max(0, totalLines - visibleHeight)));
64
+ return;
65
+ }
66
+ if (key.pageUp) {
67
+ setScrollOffset((prev) => Math.max(prev - visibleHeight, 0));
68
+ return;
69
+ }
70
+ });
71
+ // Loading state
72
+ if (loading) {
73
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Logs: ${livestock.name}`, color: project.color }), _jsx(Box, { padding: 2, children: _jsx(Text, { children: "Loading logs..." }) })] }));
74
+ }
75
+ // Error state
76
+ if (error) {
77
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Logs: ${livestock.name}`, color: project.color }), _jsxs(Box, { padding: 2, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[r] retry [q] back" }) })] })] }));
78
+ }
79
+ // Get visible slice of lines
80
+ const displayLines = logs.slice(scrollOffset, scrollOffset + visibleHeight);
81
+ const showScrollIndicator = totalLines > visibleHeight;
82
+ const hints = '[r] refresh [j/k] scroll [g/G] top/bottom [q] back';
83
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Logs: ${livestock.name}`, color: project.color }), _jsx(Box, { flexGrow: 1, paddingX: 1, children: _jsx(Panel, { title: livestock.log_path || 'Logs', focused: true, hints: hints, children: _jsxs(Box, { flexDirection: "column", height: visibleHeight + 1, children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, children: displayLines.length > 0 ? (displayLines.map((line, idx) => (_jsx(Text, { wrap: "truncate", children: line || ' ' }, scrollOffset + idx)))) : (_jsx(Text, { dimColor: true, children: "No log content" })) }), showScrollIndicator && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: ["[", scrollOffset + 1, "-", Math.min(scrollOffset + visibleHeight, totalLines), "/", totalLines, "]"] }) }))] }) }) })] }));
84
+ }
@@ -0,0 +1,5 @@
1
+ interface NightSkyViewProps {
2
+ onExit: () => void;
3
+ }
4
+ export declare function NightSkyView({ onExit }: NightSkyViewProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};