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