@colmbus72/yeehaw 0.5.0 → 0.6.1
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/skills/yeehaw-development/SKILL.md +70 -0
- package/dist/app.js +166 -15
- package/dist/components/CritterHeader.d.ts +7 -0
- package/dist/components/CritterHeader.js +81 -0
- package/dist/components/List.d.ts +2 -0
- package/dist/components/List.js +1 -1
- package/dist/components/Panel.js +1 -1
- package/dist/components/ScrollableMarkdown.js +1 -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/context.d.ts +10 -0
- package/dist/lib/context.js +63 -0
- package/dist/lib/critters.d.ts +33 -0
- package/dist/lib/critters.js +164 -0
- package/dist/lib/hotkeys.d.ts +1 -1
- package/dist/lib/hotkeys.js +6 -2
- 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 +1 -0
- package/dist/lib/paths.js +1 -0
- package/dist/lib/tmux.d.ts +1 -0
- package/dist/lib/tmux.js +50 -1
- package/dist/types.d.ts +19 -0
- package/dist/views/BarnContext.d.ts +2 -1
- package/dist/views/BarnContext.js +136 -14
- 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/IssuesView.d.ts +2 -1
- package/dist/views/IssuesView.js +775 -103
- package/dist/views/LivestockDetailView.d.ts +2 -1
- package/dist/views/LivestockDetailView.js +8 -1
- package/dist/views/ProjectContext.js +35 -1
- package/package.json +1 -1
|
@@ -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
|
|
@@ -33,8 +34,43 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
33
34
|
const [newCritterName, setNewCritterName] = useState('');
|
|
34
35
|
const [newCritterService, setNewCritterService] = useState('');
|
|
35
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);
|
|
36
52
|
// Filter windows that are barn sessions
|
|
37
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]);
|
|
38
74
|
const startEdit = () => {
|
|
39
75
|
if (isLocal)
|
|
40
76
|
return; // Cannot edit local barn
|
|
@@ -99,6 +135,44 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
99
135
|
}
|
|
100
136
|
return;
|
|
101
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
|
+
}
|
|
102
176
|
if (mode !== 'normal')
|
|
103
177
|
return;
|
|
104
178
|
if (key.tab) {
|
|
@@ -136,6 +210,15 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
136
210
|
// Start add critter flow
|
|
137
211
|
setNewCritterName('');
|
|
138
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);
|
|
139
222
|
setMode('add-critter-name');
|
|
140
223
|
return;
|
|
141
224
|
}
|
|
@@ -255,17 +338,53 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
255
338
|
}, placeholder: "mysql, redis, nginx..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Esc: cancel" }) })] })] }));
|
|
256
339
|
}
|
|
257
340
|
if (mode === 'add-critter-service') {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
});
|
|
267
370
|
}
|
|
268
|
-
},
|
|
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" }) })] })] }));
|
|
269
388
|
}
|
|
270
389
|
// Delete critter confirmation
|
|
271
390
|
if (mode === 'delete-critter-confirm' && deleteCritterTarget) {
|
|
@@ -301,7 +420,10 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
301
420
|
onOpenLivestockSession(found.project, found.livestock);
|
|
302
421
|
}
|
|
303
422
|
}
|
|
304
|
-
} })) : (_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: () => {
|
|
305
|
-
|
|
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
|
+
}
|
|
306
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" })] })) })] })] }));
|
|
307
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
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { Project } from '../types.js';
|
|
|
2
2
|
interface IssuesViewProps {
|
|
3
3
|
project: Project;
|
|
4
4
|
onBack: () => void;
|
|
5
|
+
onOpenClaude?: (workingDir: string, issueContext: string) => void;
|
|
5
6
|
}
|
|
6
|
-
export declare function IssuesView({ project, onBack }: IssuesViewProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function IssuesView({ project, onBack, onOpenClaude }: IssuesViewProps): import("react/jsx-runtime").JSX.Element | null;
|
|
7
8
|
export {};
|