@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.
Files changed (45) hide show
  1. package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
  2. package/dist/app.js +166 -15
  3. package/dist/components/CritterHeader.d.ts +7 -0
  4. package/dist/components/CritterHeader.js +81 -0
  5. package/dist/components/List.d.ts +2 -0
  6. package/dist/components/List.js +1 -1
  7. package/dist/components/Panel.js +1 -1
  8. package/dist/components/ScrollableMarkdown.js +1 -1
  9. package/dist/lib/auth/index.d.ts +2 -0
  10. package/dist/lib/auth/index.js +3 -0
  11. package/dist/lib/auth/linear.d.ts +20 -0
  12. package/dist/lib/auth/linear.js +79 -0
  13. package/dist/lib/auth/storage.d.ts +12 -0
  14. package/dist/lib/auth/storage.js +53 -0
  15. package/dist/lib/context.d.ts +10 -0
  16. package/dist/lib/context.js +63 -0
  17. package/dist/lib/critters.d.ts +33 -0
  18. package/dist/lib/critters.js +164 -0
  19. package/dist/lib/hotkeys.d.ts +1 -1
  20. package/dist/lib/hotkeys.js +6 -2
  21. package/dist/lib/issues/github.d.ts +11 -0
  22. package/dist/lib/issues/github.js +154 -0
  23. package/dist/lib/issues/index.d.ts +14 -0
  24. package/dist/lib/issues/index.js +27 -0
  25. package/dist/lib/issues/linear.d.ts +24 -0
  26. package/dist/lib/issues/linear.js +345 -0
  27. package/dist/lib/issues/types.d.ts +82 -0
  28. package/dist/lib/issues/types.js +2 -0
  29. package/dist/lib/paths.d.ts +1 -0
  30. package/dist/lib/paths.js +1 -0
  31. package/dist/lib/tmux.d.ts +1 -0
  32. package/dist/lib/tmux.js +50 -1
  33. package/dist/types.d.ts +19 -0
  34. package/dist/views/BarnContext.d.ts +2 -1
  35. package/dist/views/BarnContext.js +136 -14
  36. package/dist/views/CritterDetailView.d.ts +10 -0
  37. package/dist/views/CritterDetailView.js +117 -0
  38. package/dist/views/CritterLogsView.d.ts +8 -0
  39. package/dist/views/CritterLogsView.js +100 -0
  40. package/dist/views/IssuesView.d.ts +2 -1
  41. package/dist/views/IssuesView.js +775 -103
  42. package/dist/views/LivestockDetailView.d.ts +2 -1
  43. package/dist/views/LivestockDetailView.js +8 -1
  44. package/dist/views/ProjectContext.js +35 -1
  45. 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
- export function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, onAddCritter, onRemoveCritter, }) {
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
- 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] }), _jsx(Text, { dimColor: true, children: "Enter the systemd service name" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Service: " }), _jsx(TextInput, { value: newCritterService, onChange: setNewCritterService, onSubmit: () => {
259
- if (newCritterService.trim()) {
260
- const critter = {
261
- name: newCritterName.trim(),
262
- service: newCritterService.trim(),
263
- use_journald: true,
264
- };
265
- onAddCritter(critter);
266
- setMode('normal');
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
- }, placeholder: "mysql.service, redis-server.service..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save, Esc: cancel" }) })] })] }));
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
- // Could show critter details or logs in future
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 {};