@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.
Files changed (61) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +2 -1
  2. package/claude-plugin/hooks/hooks.json +41 -0
  3. package/claude-plugin/hooks/session-status.sh +13 -0
  4. package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
  5. package/dist/app.js +228 -28
  6. package/dist/components/CritterHeader.d.ts +7 -0
  7. package/dist/components/CritterHeader.js +81 -0
  8. package/dist/components/HelpOverlay.js +4 -2
  9. package/dist/components/List.d.ts +10 -1
  10. package/dist/components/List.js +14 -5
  11. package/dist/components/Panel.js +27 -1
  12. package/dist/components/SplashScreen.js +1 -1
  13. package/dist/hooks/useSessions.js +2 -2
  14. package/dist/index.js +41 -1
  15. package/dist/lib/auth/index.d.ts +2 -0
  16. package/dist/lib/auth/index.js +3 -0
  17. package/dist/lib/auth/linear.d.ts +20 -0
  18. package/dist/lib/auth/linear.js +79 -0
  19. package/dist/lib/auth/storage.d.ts +12 -0
  20. package/dist/lib/auth/storage.js +53 -0
  21. package/dist/lib/config.d.ts +13 -1
  22. package/dist/lib/config.js +51 -0
  23. package/dist/lib/context.d.ts +10 -0
  24. package/dist/lib/context.js +63 -0
  25. package/dist/lib/critters.d.ts +61 -0
  26. package/dist/lib/critters.js +365 -0
  27. package/dist/lib/hooks.d.ts +20 -0
  28. package/dist/lib/hooks.js +91 -0
  29. package/dist/lib/hotkeys.d.ts +1 -1
  30. package/dist/lib/hotkeys.js +28 -20
  31. package/dist/lib/issues/github.d.ts +11 -0
  32. package/dist/lib/issues/github.js +154 -0
  33. package/dist/lib/issues/index.d.ts +14 -0
  34. package/dist/lib/issues/index.js +27 -0
  35. package/dist/lib/issues/linear.d.ts +24 -0
  36. package/dist/lib/issues/linear.js +345 -0
  37. package/dist/lib/issues/types.d.ts +82 -0
  38. package/dist/lib/issues/types.js +2 -0
  39. package/dist/lib/paths.d.ts +3 -0
  40. package/dist/lib/paths.js +3 -0
  41. package/dist/lib/signals.d.ts +30 -0
  42. package/dist/lib/signals.js +104 -0
  43. package/dist/lib/tmux.d.ts +9 -2
  44. package/dist/lib/tmux.js +114 -18
  45. package/dist/mcp-server.js +161 -1
  46. package/dist/types.d.ts +23 -2
  47. package/dist/views/BarnContext.d.ts +5 -2
  48. package/dist/views/BarnContext.js +202 -21
  49. package/dist/views/CritterDetailView.d.ts +10 -0
  50. package/dist/views/CritterDetailView.js +117 -0
  51. package/dist/views/CritterLogsView.d.ts +8 -0
  52. package/dist/views/CritterLogsView.js +100 -0
  53. package/dist/views/GlobalDashboard.d.ts +2 -2
  54. package/dist/views/GlobalDashboard.js +20 -18
  55. package/dist/views/IssuesView.d.ts +2 -1
  56. package/dist/views/IssuesView.js +661 -98
  57. package/dist/views/LivestockDetailView.d.ts +2 -1
  58. package/dist/views/LivestockDetailView.js +19 -8
  59. package/dist/views/ProjectContext.d.ts +2 -2
  60. package/dist/views/ProjectContext.js +68 -25
  61. 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
- export function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, }) {
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: c.status === 'running' ? 'active' : c.status === 'stopped' ? 'inactive' : 'inactive',
235
- meta: c.type,
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 = '[s] shell [n] new [d] delete';
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
- } })) : (_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', onSelect: () => {
246
- // TODO: Could show critter details or manage service
247
- } })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No critters configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Critters are system services (nginx, mysql, etc.)" })] })) })] })] }));
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
- onNewClaude: () => void;
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, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, onInputModeChange, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
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 {};