@colmbus72/yeehaw 0.4.2 → 0.6.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/claude-plugin/.claude-plugin/plugin.json +2 -1
- package/claude-plugin/hooks/hooks.json +41 -0
- package/claude-plugin/hooks/session-status.sh +13 -0
- package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
- package/dist/app.js +228 -28
- package/dist/components/CritterHeader.d.ts +7 -0
- package/dist/components/CritterHeader.js +81 -0
- package/dist/components/HelpOverlay.js +4 -2
- package/dist/components/List.d.ts +10 -1
- package/dist/components/List.js +14 -5
- package/dist/components/Panel.js +27 -1
- package/dist/components/SplashScreen.js +1 -1
- package/dist/hooks/useSessions.js +2 -2
- package/dist/index.js +41 -1
- package/dist/lib/auth/index.d.ts +2 -0
- package/dist/lib/auth/index.js +3 -0
- package/dist/lib/auth/linear.d.ts +20 -0
- package/dist/lib/auth/linear.js +79 -0
- package/dist/lib/auth/storage.d.ts +12 -0
- package/dist/lib/auth/storage.js +53 -0
- package/dist/lib/config.d.ts +13 -1
- package/dist/lib/config.js +51 -0
- package/dist/lib/context.d.ts +10 -0
- package/dist/lib/context.js +63 -0
- package/dist/lib/critters.d.ts +61 -0
- package/dist/lib/critters.js +365 -0
- package/dist/lib/hooks.d.ts +20 -0
- package/dist/lib/hooks.js +91 -0
- package/dist/lib/hotkeys.d.ts +1 -1
- package/dist/lib/hotkeys.js +28 -20
- package/dist/lib/issues/github.d.ts +11 -0
- package/dist/lib/issues/github.js +154 -0
- package/dist/lib/issues/index.d.ts +14 -0
- package/dist/lib/issues/index.js +27 -0
- package/dist/lib/issues/linear.d.ts +24 -0
- package/dist/lib/issues/linear.js +345 -0
- package/dist/lib/issues/types.d.ts +82 -0
- package/dist/lib/issues/types.js +2 -0
- package/dist/lib/paths.d.ts +3 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/signals.d.ts +30 -0
- package/dist/lib/signals.js +104 -0
- package/dist/lib/tmux.d.ts +9 -2
- package/dist/lib/tmux.js +114 -18
- package/dist/mcp-server.js +161 -1
- package/dist/types.d.ts +23 -2
- package/dist/views/BarnContext.d.ts +5 -2
- package/dist/views/BarnContext.js +202 -21
- package/dist/views/CritterDetailView.d.ts +10 -0
- package/dist/views/CritterDetailView.js +117 -0
- package/dist/views/CritterLogsView.d.ts +8 -0
- package/dist/views/CritterLogsView.js +100 -0
- package/dist/views/GlobalDashboard.d.ts +2 -2
- package/dist/views/GlobalDashboard.js +20 -18
- package/dist/views/IssuesView.d.ts +2 -1
- package/dist/views/IssuesView.js +661 -98
- package/dist/views/LivestockDetailView.d.ts +2 -1
- package/dist/views/LivestockDetailView.js +19 -8
- package/dist/views/ProjectContext.d.ts +2 -2
- package/dist/views/ProjectContext.js +68 -25
- package/package.json +5 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
5
|
import { BarnHeader } from '../components/BarnHeader.js';
|
|
@@ -9,7 +9,8 @@ import { PathInput } from '../components/PathInput.js';
|
|
|
9
9
|
import { detectRemoteGitInfo, detectGitInfo } from '../lib/git.js';
|
|
10
10
|
import { isLocalBarn } from '../lib/config.js';
|
|
11
11
|
import { parseGitHubUrl } from '../lib/github.js';
|
|
12
|
-
|
|
12
|
+
import { listSystemServices, getServiceDetails } from '../lib/critters.js';
|
|
13
|
+
export function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, onAddCritter, onRemoveCritter, onSelectCritter, }) {
|
|
13
14
|
const [focusedPanel, setFocusedPanel] = useState('livestock');
|
|
14
15
|
const [mode, setMode] = useState('normal');
|
|
15
16
|
// Check if this is the local barn
|
|
@@ -28,8 +29,48 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
28
29
|
// Delete livestock state
|
|
29
30
|
const [selectedLivestockIndex, setSelectedLivestockIndex] = useState(0);
|
|
30
31
|
const [deleteLivestockTarget, setDeleteLivestockTarget] = useState(null);
|
|
32
|
+
// Critter state
|
|
33
|
+
const [selectedCritterIndex, setSelectedCritterIndex] = useState(0);
|
|
34
|
+
const [newCritterName, setNewCritterName] = useState('');
|
|
35
|
+
const [newCritterService, setNewCritterService] = useState('');
|
|
36
|
+
const [deleteCritterTarget, setDeleteCritterTarget] = useState(null);
|
|
37
|
+
// Service selection state
|
|
38
|
+
const [availableServices, setAvailableServices] = useState([]);
|
|
39
|
+
const [showAllServices, setShowAllServices] = useState(false);
|
|
40
|
+
const [serviceFilter, setServiceFilter] = useState('');
|
|
41
|
+
const [servicesLoading, setServicesLoading] = useState(false);
|
|
42
|
+
const [servicesError, setServicesError] = useState(null);
|
|
43
|
+
const [selectedServiceIndex, setSelectedServiceIndex] = useState(0);
|
|
44
|
+
// Auto-detected critter details (used to pre-fill editable fields)
|
|
45
|
+
const [detectedDetails, setDetectedDetails] = useState(null);
|
|
46
|
+
const [detectedLoading, setDetectedLoading] = useState(false);
|
|
47
|
+
// Editable critter fields (pre-filled from detection, user can modify)
|
|
48
|
+
const [newCritterServicePath, setNewCritterServicePath] = useState('');
|
|
49
|
+
const [newCritterConfigPath, setNewCritterConfigPath] = useState('');
|
|
50
|
+
const [newCritterLogPath, setNewCritterLogPath] = useState('');
|
|
51
|
+
const [newCritterUseJournald, setNewCritterUseJournald] = useState(true);
|
|
31
52
|
// Filter windows that are barn sessions
|
|
32
53
|
const barnWindows = windows.filter((w) => w.index > 0 && w.name.startsWith(`barn-${barn.name}`));
|
|
54
|
+
// Fetch available services from the barn
|
|
55
|
+
const fetchServices = useCallback(async (showAll) => {
|
|
56
|
+
setServicesLoading(true);
|
|
57
|
+
setServicesError(null);
|
|
58
|
+
const result = await listSystemServices(barn, !showAll);
|
|
59
|
+
setServicesLoading(false);
|
|
60
|
+
if (result.error) {
|
|
61
|
+
setServicesError(result.error);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
setAvailableServices(result.services);
|
|
65
|
+
setSelectedServiceIndex(0);
|
|
66
|
+
}
|
|
67
|
+
}, [barn]);
|
|
68
|
+
// Fetch services when entering service selection mode
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (mode === 'add-critter-service') {
|
|
71
|
+
fetchServices(showAllServices);
|
|
72
|
+
}
|
|
73
|
+
}, [mode, fetchServices, showAllServices]);
|
|
33
74
|
const startEdit = () => {
|
|
34
75
|
if (isLocal)
|
|
35
76
|
return; // Cannot edit local barn
|
|
@@ -80,24 +121,64 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
80
121
|
}
|
|
81
122
|
return;
|
|
82
123
|
}
|
|
124
|
+
// Handle delete critter confirmation
|
|
125
|
+
if (mode === 'delete-critter-confirm' && deleteCritterTarget) {
|
|
126
|
+
if (input === 'y') {
|
|
127
|
+
onRemoveCritter(deleteCritterTarget.name);
|
|
128
|
+
setDeleteCritterTarget(null);
|
|
129
|
+
setSelectedCritterIndex(Math.max(0, selectedCritterIndex - 1));
|
|
130
|
+
setMode('normal');
|
|
131
|
+
}
|
|
132
|
+
else if (input === 'n') {
|
|
133
|
+
setDeleteCritterTarget(null);
|
|
134
|
+
setMode('normal');
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Handle arrow keys and toggle for service selection
|
|
139
|
+
if (mode === 'add-critter-service') {
|
|
140
|
+
const filteredServices = availableServices.filter((s) => s.name.toLowerCase().includes(serviceFilter.toLowerCase()));
|
|
141
|
+
if (key.upArrow) {
|
|
142
|
+
setSelectedServiceIndex((prev) => Math.max(0, prev - 1));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.downArrow) {
|
|
146
|
+
setSelectedServiceIndex((prev) => Math.min(filteredServices.length - 1, prev + 1));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (input === 'a') {
|
|
150
|
+
const newShowAll = !showAllServices;
|
|
151
|
+
setShowAllServices(newShowAll);
|
|
152
|
+
fetchServices(newShowAll);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Handle use-journald toggle and save
|
|
157
|
+
if (mode === 'add-critter-use-journald') {
|
|
158
|
+
if (input === ' ') {
|
|
159
|
+
setNewCritterUseJournald(!newCritterUseJournald);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (key.return) {
|
|
163
|
+
const critter = {
|
|
164
|
+
name: newCritterName.trim(),
|
|
165
|
+
service: newCritterService,
|
|
166
|
+
service_path: newCritterServicePath.trim() || undefined,
|
|
167
|
+
config_path: newCritterConfigPath.trim() || undefined,
|
|
168
|
+
log_path: newCritterLogPath.trim() || undefined,
|
|
169
|
+
use_journald: newCritterUseJournald,
|
|
170
|
+
};
|
|
171
|
+
onAddCritter(critter);
|
|
172
|
+
setMode('normal');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
83
176
|
if (mode !== 'normal')
|
|
84
177
|
return;
|
|
85
178
|
if (key.tab) {
|
|
86
179
|
setFocusedPanel((p) => (p === 'livestock' ? 'critters' : 'livestock'));
|
|
87
180
|
return;
|
|
88
181
|
}
|
|
89
|
-
if (input === 's') {
|
|
90
|
-
// Context-aware shell: livestock session if focused on livestock, otherwise SSH to barn
|
|
91
|
-
if (focusedPanel === 'livestock' && livestock.length > 0) {
|
|
92
|
-
const target = livestock[selectedLivestockIndex];
|
|
93
|
-
if (target) {
|
|
94
|
-
onOpenLivestockSession(target.project, target.livestock);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
onSshToBarn();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
182
|
if (input === 'e' && !isLocal) {
|
|
102
183
|
startEdit();
|
|
103
184
|
return;
|
|
@@ -125,6 +206,32 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
125
206
|
}
|
|
126
207
|
return;
|
|
127
208
|
}
|
|
209
|
+
if (input === 'n' && focusedPanel === 'critters') {
|
|
210
|
+
// Start add critter flow
|
|
211
|
+
setNewCritterName('');
|
|
212
|
+
setNewCritterService('');
|
|
213
|
+
setServiceFilter('');
|
|
214
|
+
setSelectedServiceIndex(0);
|
|
215
|
+
setShowAllServices(false);
|
|
216
|
+
setDetectedDetails(null);
|
|
217
|
+
// Reset editable fields
|
|
218
|
+
setNewCritterServicePath('');
|
|
219
|
+
setNewCritterConfigPath('');
|
|
220
|
+
setNewCritterLogPath('');
|
|
221
|
+
setNewCritterUseJournald(true);
|
|
222
|
+
setMode('add-critter-name');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (input === 'd' && focusedPanel === 'critters' && (barn.critters || []).length > 0) {
|
|
226
|
+
// Delete selected critter
|
|
227
|
+
const critters = barn.critters || [];
|
|
228
|
+
const target = critters[selectedCritterIndex];
|
|
229
|
+
if (target) {
|
|
230
|
+
setDeleteCritterTarget(target);
|
|
231
|
+
setMode('delete-critter-confirm');
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
128
235
|
});
|
|
129
236
|
// Edit mode screens
|
|
130
237
|
if (mode === 'edit-host') {
|
|
@@ -220,29 +327,103 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
220
327
|
if (mode === 'delete-livestock-confirm' && deleteLivestockTarget) {
|
|
221
328
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Remove livestock" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Remove Livestock" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Remove \"", deleteLivestockTarget.livestock.name, "\" from ", deleteLivestockTarget.project.name, "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Path: ", deleteLivestockTarget.livestock.path] }) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: "red", bold: true, children: "[y] Yes, remove" }), _jsx(Text, { dimColor: true, children: "[n/Esc] Cancel" })] })] })] }));
|
|
222
329
|
}
|
|
330
|
+
// Add critter flow: name → service
|
|
331
|
+
if (mode === 'add-critter-name') {
|
|
332
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter to ", barn.name] }), _jsx(Text, { dimColor: true, children: "A critter is a system service like mysql, redis, nginx, etc." }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newCritterName, onChange: setNewCritterName, onSubmit: () => {
|
|
333
|
+
if (newCritterName.trim()) {
|
|
334
|
+
// Auto-suggest service name
|
|
335
|
+
setNewCritterService(`${newCritterName.trim()}.service`);
|
|
336
|
+
setMode('add-critter-service');
|
|
337
|
+
}
|
|
338
|
+
}, placeholder: "mysql, redis, nginx..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Esc: cancel" }) })] })] }));
|
|
339
|
+
}
|
|
340
|
+
if (mode === 'add-critter-service') {
|
|
341
|
+
const filteredServices = availableServices.filter((s) => s.name.toLowerCase().includes(serviceFilter.toLowerCase()));
|
|
342
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter: ", newCritterName] }), _jsxs(Text, { dimColor: true, children: [showAllServices ? 'All services' : 'Running services', " - Press [a] to toggle"] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Filter: " }), _jsx(TextInput, { value: serviceFilter, onChange: (val) => {
|
|
343
|
+
setServiceFilter(val);
|
|
344
|
+
setSelectedServiceIndex(0);
|
|
345
|
+
}, onSubmit: () => {
|
|
346
|
+
if (filteredServices.length > 0) {
|
|
347
|
+
const selected = filteredServices[selectedServiceIndex];
|
|
348
|
+
setNewCritterService(selected.name);
|
|
349
|
+
// Fetch service details and pre-fill fields
|
|
350
|
+
setDetectedLoading(true);
|
|
351
|
+
getServiceDetails(barn, selected.name).then((result) => {
|
|
352
|
+
setDetectedLoading(false);
|
|
353
|
+
if (result.details) {
|
|
354
|
+
setDetectedDetails(result.details);
|
|
355
|
+
// Pre-fill editable fields
|
|
356
|
+
setNewCritterServicePath(result.details.service_path || '');
|
|
357
|
+
setNewCritterConfigPath(result.details.config_path || '');
|
|
358
|
+
setNewCritterLogPath(result.details.log_path || '');
|
|
359
|
+
setNewCritterUseJournald(result.details.use_journald ?? true);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// No details, use defaults
|
|
363
|
+
setNewCritterServicePath('');
|
|
364
|
+
setNewCritterConfigPath('');
|
|
365
|
+
setNewCritterLogPath('');
|
|
366
|
+
setNewCritterUseJournald(true);
|
|
367
|
+
}
|
|
368
|
+
setMode('add-critter-service-path');
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
} })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", height: 10, children: servicesLoading ? (_jsx(Text, { dimColor: true, children: "Loading services..." })) : servicesError ? (_jsx(Text, { color: "red", children: servicesError })) : filteredServices.length === 0 ? (_jsx(Text, { dimColor: true, children: "No services match filter" })) : (filteredServices.slice(0, 10).map((service, i) => (_jsxs(Text, { children: [i === selectedServiceIndex ? _jsx(Text, { color: "cyan", children: '> ' }) : ' ', service.name, service.description && _jsxs(Text, { dimColor: true, children: [" - ", service.description] })] }, service.name)))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Up/Down: navigate, Enter: select, [a] toggle all/running, Esc: cancel" }) })] })] }));
|
|
372
|
+
}
|
|
373
|
+
// Add critter: service path (pre-filled from detection)
|
|
374
|
+
if (mode === 'add-critter-service-path') {
|
|
375
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter: ", newCritterName] }), _jsxs(Text, { dimColor: true, children: ["Service: ", newCritterService] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Service file path (optional): " }), _jsx(TextInput, { value: newCritterServicePath, onChange: setNewCritterServicePath, onSubmit: () => setMode('add-critter-config-path') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
|
|
376
|
+
}
|
|
377
|
+
// Add critter: config path (pre-filled from detection)
|
|
378
|
+
if (mode === 'add-critter-config-path') {
|
|
379
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter: ", newCritterName] }), _jsxs(Text, { dimColor: true, children: ["Service: ", newCritterService] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Config path (optional): " }), _jsx(TextInput, { value: newCritterConfigPath, onChange: setNewCritterConfigPath, onSubmit: () => setMode('add-critter-log-path') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
|
|
380
|
+
}
|
|
381
|
+
// Add critter: log path (pre-filled from detection)
|
|
382
|
+
if (mode === 'add-critter-log-path') {
|
|
383
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter: ", newCritterName] }), _jsxs(Text, { dimColor: true, children: ["Service: ", newCritterService] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Log path (optional, if not using journald): " }), _jsx(TextInput, { value: newCritterLogPath, onChange: setNewCritterLogPath, onSubmit: () => setMode('add-critter-use-journald') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
|
|
384
|
+
}
|
|
385
|
+
// Add critter: use journald toggle (pre-filled from detection)
|
|
386
|
+
if (mode === 'add-critter-use-journald') {
|
|
387
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter: ", newCritterName] }), _jsxs(Text, { dimColor: true, children: ["Service: ", newCritterService] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Use journald for logs: " }), _jsx(Text, { bold: true, color: newCritterUseJournald ? 'green' : 'red', children: newCritterUseJournald ? 'Yes' : 'No' }), _jsx(Text, { dimColor: true, children: " (press space to toggle)" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Space: toggle, Enter: save critter, Esc: cancel" }) })] })] }));
|
|
388
|
+
}
|
|
389
|
+
// Delete critter confirmation
|
|
390
|
+
if (mode === 'delete-critter-confirm' && deleteCritterTarget) {
|
|
391
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Remove critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Remove Critter" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Remove \"", deleteCritterTarget.name, "\" from ", barn.name, "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Service: ", deleteCritterTarget.service] }) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: "red", bold: true, children: "[y] Yes, remove" }), _jsx(Text, { dimColor: true, children: "[n/Esc] Cancel" })] })] })] }));
|
|
392
|
+
}
|
|
223
393
|
// Build livestock items
|
|
224
394
|
const livestockItems = livestock.map((l) => ({
|
|
225
395
|
id: `${l.project.name}/${l.livestock.name}`,
|
|
226
396
|
label: `${l.project.name}/${l.livestock.name}`,
|
|
227
397
|
status: 'active',
|
|
228
398
|
meta: l.livestock.path,
|
|
399
|
+
actions: [{ key: 's', label: 'shell' }],
|
|
229
400
|
}));
|
|
230
401
|
// Build critter items
|
|
231
402
|
const critterItems = (barn.critters || []).map((c) => ({
|
|
232
403
|
id: c.name,
|
|
233
404
|
label: c.name,
|
|
234
|
-
status:
|
|
235
|
-
meta: c.
|
|
405
|
+
status: 'active', // Critters are assumed active (discovery only finds running services)
|
|
406
|
+
meta: c.service,
|
|
236
407
|
}));
|
|
237
408
|
// Panel-specific hints (page-level hotkeys like s are in BottomBar)
|
|
238
|
-
const livestockHints = '[
|
|
239
|
-
const crittersHints = '';
|
|
409
|
+
const livestockHints = '[n] new [d] delete';
|
|
410
|
+
const crittersHints = '[n] new [d] delete';
|
|
240
411
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: isLocal ? 'Local machine' : `${barn.user}@${barn.host}:${barn.port}` }), !isLocal && (_jsx(Box, { paddingX: 2, marginY: 1, children: _jsxs(Box, { gap: 4, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Host:" }), " ", barn.host] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "User:" }), " ", barn.user] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Port:" }), " ", barn.port] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Key:" }), " ", barn.identity_file] })] }) })), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Livestock", focused: focusedPanel === 'livestock', width: "50%", hints: livestockHints, children: livestockItems.length > 0 ? (_jsx(List, { items: livestockItems, focused: focusedPanel === 'livestock', selectedIndex: selectedLivestockIndex, onSelectionChange: setSelectedLivestockIndex, onSelect: (item) => {
|
|
241
412
|
const found = livestock.find((l) => `${l.project.name}/${l.livestock.name}` === item.id);
|
|
242
413
|
if (found) {
|
|
243
414
|
onSelectLivestock(found.project, found.livestock);
|
|
244
415
|
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
416
|
+
}, onAction: (item, actionKey) => {
|
|
417
|
+
if (actionKey === 's') {
|
|
418
|
+
const found = livestock.find((l) => `${l.project.name}/${l.livestock.name}` === item.id);
|
|
419
|
+
if (found) {
|
|
420
|
+
onOpenLivestockSession(found.project, found.livestock);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} })) : (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No livestock deployed to this barn" }) })) }), _jsx(Panel, { title: "Critters", focused: focusedPanel === 'critters', width: "50%", hints: crittersHints, children: critterItems.length > 0 ? (_jsx(List, { items: critterItems, focused: focusedPanel === 'critters', selectedIndex: selectedCritterIndex, onSelectionChange: setSelectedCritterIndex, onSelect: (item) => {
|
|
424
|
+
const critter = (barn.critters || []).find((c) => c.name === item.id);
|
|
425
|
+
if (critter) {
|
|
426
|
+
onSelectCritter(critter);
|
|
427
|
+
}
|
|
428
|
+
} })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No critters configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Press [n] to add a critter" })] })) })] })] }));
|
|
248
429
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Barn, Critter } from '../types.js';
|
|
2
|
+
interface CritterDetailViewProps {
|
|
3
|
+
barn: Barn;
|
|
4
|
+
critter: Critter;
|
|
5
|
+
onBack: () => void;
|
|
6
|
+
onOpenLogs: () => void;
|
|
7
|
+
onUpdateCritter: (originalCritter: Critter, updatedCritter: Critter) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function CritterDetailView({ barn, critter, onBack, onOpenLogs, onUpdateCritter, }: CritterDetailViewProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
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 TextInput from 'ink-text-input';
|
|
5
|
+
import { CritterHeader } from '../components/CritterHeader.js';
|
|
6
|
+
export function CritterDetailView({ barn, critter, onBack, onOpenLogs, onUpdateCritter, }) {
|
|
7
|
+
const [mode, setMode] = useState('normal');
|
|
8
|
+
// Edit form state
|
|
9
|
+
const [editName, setEditName] = useState(critter.name);
|
|
10
|
+
const [editService, setEditService] = useState(critter.service);
|
|
11
|
+
const [editConfigPath, setEditConfigPath] = useState(critter.config_path || '');
|
|
12
|
+
const [editLogPath, setEditLogPath] = useState(critter.log_path || '');
|
|
13
|
+
const [editUseJournald, setEditUseJournald] = useState(critter.use_journald !== false);
|
|
14
|
+
// Sync form state when critter prop changes (e.g., after save)
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setEditName(critter.name);
|
|
17
|
+
setEditService(critter.service);
|
|
18
|
+
setEditConfigPath(critter.config_path || '');
|
|
19
|
+
setEditLogPath(critter.log_path || '');
|
|
20
|
+
setEditUseJournald(critter.use_journald !== false);
|
|
21
|
+
}, [critter]);
|
|
22
|
+
const resetForm = () => {
|
|
23
|
+
setEditName(critter.name);
|
|
24
|
+
setEditService(critter.service);
|
|
25
|
+
setEditConfigPath(critter.config_path || '');
|
|
26
|
+
setEditLogPath(critter.log_path || '');
|
|
27
|
+
setEditUseJournald(critter.use_journald !== false);
|
|
28
|
+
};
|
|
29
|
+
// Save all pending changes at once
|
|
30
|
+
const saveAllChanges = () => {
|
|
31
|
+
const updated = {
|
|
32
|
+
...critter,
|
|
33
|
+
name: editName.trim() || critter.name,
|
|
34
|
+
service: editService.trim() || critter.service,
|
|
35
|
+
config_path: editConfigPath.trim() || undefined,
|
|
36
|
+
log_path: editLogPath.trim() || undefined,
|
|
37
|
+
use_journald: editUseJournald,
|
|
38
|
+
};
|
|
39
|
+
onUpdateCritter(critter, updated);
|
|
40
|
+
setMode('normal');
|
|
41
|
+
};
|
|
42
|
+
useInput((input, key) => {
|
|
43
|
+
// Handle escape - works in all modes
|
|
44
|
+
if (key.escape) {
|
|
45
|
+
if (mode !== 'normal') {
|
|
46
|
+
setMode('normal');
|
|
47
|
+
resetForm();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
onBack();
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Handle Ctrl+S to save and exit from any edit mode
|
|
55
|
+
// Note: Ctrl+S sends ASCII 19 (\x13), not 's'
|
|
56
|
+
if ((key.ctrl && input === 's') || input === '\x13') {
|
|
57
|
+
if (mode !== 'normal') {
|
|
58
|
+
saveAllChanges();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Handle space to toggle journald in edit mode
|
|
63
|
+
if (mode === 'edit-use-journald') {
|
|
64
|
+
if (input === ' ') {
|
|
65
|
+
setEditUseJournald(!editUseJournald);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.return) {
|
|
69
|
+
saveAllChanges();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Only process these in normal mode
|
|
75
|
+
if (mode !== 'normal')
|
|
76
|
+
return;
|
|
77
|
+
if (input === 'l') {
|
|
78
|
+
onOpenLogs();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (input === 'e') {
|
|
82
|
+
// Start edit flow with name
|
|
83
|
+
setMode('edit-name');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// Edit name
|
|
88
|
+
if (mode === 'edit-name') {
|
|
89
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(CritterHeader, { barn: barn, critter: critter }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Critter" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: editName, onChange: setEditName, onSubmit: () => {
|
|
90
|
+
if (editName.trim()) {
|
|
91
|
+
setMode('edit-service');
|
|
92
|
+
}
|
|
93
|
+
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
94
|
+
}
|
|
95
|
+
// Edit service
|
|
96
|
+
if (mode === 'edit-service') {
|
|
97
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(CritterHeader, { barn: barn, critter: critter }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Critter" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Service (systemd): " }), _jsx(TextInput, { value: editService, onChange: setEditService, onSubmit: () => {
|
|
98
|
+
if (editService.trim()) {
|
|
99
|
+
setMode('edit-config-path');
|
|
100
|
+
}
|
|
101
|
+
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
102
|
+
}
|
|
103
|
+
// Edit config path
|
|
104
|
+
if (mode === 'edit-config-path') {
|
|
105
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(CritterHeader, { barn: barn, critter: critter }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Critter" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Config Path (optional): " }), _jsx(TextInput, { value: editConfigPath, onChange: setEditConfigPath, onSubmit: () => setMode('edit-log-path') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
106
|
+
}
|
|
107
|
+
// Edit log path
|
|
108
|
+
if (mode === 'edit-log-path') {
|
|
109
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(CritterHeader, { barn: barn, critter: critter }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Critter" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Log Path (optional, if not using journald): " }), _jsx(TextInput, { value: editLogPath, onChange: setEditLogPath, onSubmit: () => setMode('edit-use-journald') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
110
|
+
}
|
|
111
|
+
// Edit use_journald (toggle with space)
|
|
112
|
+
if (mode === 'edit-use-journald') {
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(CritterHeader, { barn: barn, critter: critter }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Critter" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Use Journald: " }), _jsx(Text, { color: editUseJournald ? 'green' : 'red', bold: true, children: editUseJournald ? '[x] Yes' : '[ ] No' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Space: toggle, Enter: save & finish, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
114
|
+
}
|
|
115
|
+
// Normal view - show critter info
|
|
116
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(CritterHeader, { barn: barn, critter: critter }), _jsxs(Box, { paddingX: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 3, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "service:" }), " ", critter.service] }), critter.service_path && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "unit:" }), " ", critter.service_path] }))] }), _jsxs(Box, { gap: 3, marginTop: 1, children: [critter.config_path && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "config:" }), " ", critter.config_path] })), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "logs:" }), ' ', critter.use_journald !== false ? (_jsx(Text, { children: "journald" })) : critter.log_path ? (_jsx(Text, { children: critter.log_path })) : (_jsx(Text, { dimColor: true, children: "not configured" }))] })] })] }), _jsx(Box, { paddingX: 2, marginTop: 2, children: _jsx(Text, { dimColor: true, children: "[l] view logs [e] edit [esc] back" }) })] }));
|
|
117
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Barn, Critter } from '../types.js';
|
|
2
|
+
interface CritterLogsViewProps {
|
|
3
|
+
barn: Barn;
|
|
4
|
+
critter: Critter;
|
|
5
|
+
onBack: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function CritterLogsView({ barn, critter, onBack }: CritterLogsViewProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { CritterHeader } from '../components/CritterHeader.js';
|
|
5
|
+
import { readCritterLogs } from '../lib/critters.js';
|
|
6
|
+
import { loadBarn } from '../lib/config.js';
|
|
7
|
+
export function CritterLogsView({ barn, critter, onBack }) {
|
|
8
|
+
const { stdout } = useStdout();
|
|
9
|
+
const [logs, setLogs] = useState([]);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
13
|
+
const terminalHeight = stdout?.rows || 24;
|
|
14
|
+
const visibleLines = terminalHeight - 6; // Account for header and padding
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let mounted = true;
|
|
17
|
+
async function fetchLogs() {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setError(null);
|
|
20
|
+
const fullBarn = loadBarn(barn.name);
|
|
21
|
+
if (!fullBarn) {
|
|
22
|
+
setError(`Barn not found: ${barn.name}`);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const result = await readCritterLogs(critter, fullBarn, { lines: 200 });
|
|
27
|
+
if (!mounted)
|
|
28
|
+
return;
|
|
29
|
+
if (result.error) {
|
|
30
|
+
setError(result.error);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const lines = result.content.split('\n');
|
|
34
|
+
setLogs(lines);
|
|
35
|
+
// Scroll to bottom initially
|
|
36
|
+
setScrollOffset(Math.max(0, lines.length - visibleLines));
|
|
37
|
+
}
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
fetchLogs();
|
|
41
|
+
return () => {
|
|
42
|
+
mounted = false;
|
|
43
|
+
};
|
|
44
|
+
}, [barn.name, critter, visibleLines]);
|
|
45
|
+
useInput((input, key) => {
|
|
46
|
+
if (key.escape) {
|
|
47
|
+
onBack();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Scroll with arrow keys
|
|
51
|
+
if (key.upArrow) {
|
|
52
|
+
setScrollOffset((prev) => Math.max(0, prev - 1));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (key.downArrow) {
|
|
56
|
+
setScrollOffset((prev) => Math.min(logs.length - visibleLines, prev + 1));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Page up/down
|
|
60
|
+
if (key.pageUp) {
|
|
61
|
+
setScrollOffset((prev) => Math.max(0, prev - visibleLines));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (key.pageDown) {
|
|
65
|
+
setScrollOffset((prev) => Math.min(logs.length - visibleLines, prev + visibleLines));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Home/End
|
|
69
|
+
if (input === 'g') {
|
|
70
|
+
setScrollOffset(0);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (input === 'G') {
|
|
74
|
+
setScrollOffset(Math.max(0, logs.length - visibleLines));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Refresh
|
|
78
|
+
if (input === 'r') {
|
|
79
|
+
setLoading(true);
|
|
80
|
+
(async () => {
|
|
81
|
+
const fullBarn = loadBarn(barn.name);
|
|
82
|
+
if (!fullBarn)
|
|
83
|
+
return;
|
|
84
|
+
const result = await readCritterLogs(critter, fullBarn, { lines: 200 });
|
|
85
|
+
if (result.error) {
|
|
86
|
+
setError(result.error);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const lines = result.content.split('\n');
|
|
90
|
+
setLogs(lines);
|
|
91
|
+
setScrollOffset(Math.max(0, lines.length - visibleLines));
|
|
92
|
+
}
|
|
93
|
+
setLoading(false);
|
|
94
|
+
})();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const visibleLogs = logs.slice(scrollOffset, scrollOffset + visibleLines);
|
|
99
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(CritterHeader, { barn: barn, critter: critter }), _jsx(Box, { flexDirection: "column", paddingX: 2, flexGrow: 1, children: loading ? (_jsx(Text, { dimColor: true, children: "Loading logs..." })) : error ? (_jsx(Text, { color: "red", children: error })) : logs.length === 0 ? (_jsx(Text, { dimColor: true, children: "No logs found" })) : (_jsx(Box, { flexDirection: "column", children: visibleLogs.map((line, i) => (_jsx(Text, { wrap: "truncate", children: line }, scrollOffset + i))) })) }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { dimColor: true, children: [logs.length > 0 && `${scrollOffset + 1}-${Math.min(scrollOffset + visibleLines, logs.length)} of ${logs.length} | `, "\u2191\u2193 scroll | g/G top/bottom | r refresh | Esc back"] }) })] }));
|
|
100
|
+
}
|
|
@@ -11,11 +11,11 @@ interface GlobalDashboardProps {
|
|
|
11
11
|
onSelectProject: (project: Project) => void;
|
|
12
12
|
onSelectBarn: (barn: Barn) => void;
|
|
13
13
|
onSelectWindow: (window: TmuxWindow) => void;
|
|
14
|
-
|
|
14
|
+
onNewClaudeForProject: (project: Project) => void;
|
|
15
15
|
onCreateProject: (name: string, path: string) => void;
|
|
16
16
|
onCreateBarn: (barn: Barn) => void;
|
|
17
17
|
onSshToBarn: (barn: Barn) => void;
|
|
18
18
|
onInputModeChange?: (isInputMode: boolean) => void;
|
|
19
19
|
}
|
|
20
|
-
export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow,
|
|
20
|
+
export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaudeForProject, onCreateProject, onCreateBarn, onSshToBarn, onInputModeChange, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
|
|
21
21
|
export {};
|