@colmbus72/yeehaw 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/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/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 +45 -0
- 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 +661 -98
- 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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yeehaw-development
|
|
3
|
+
description: Guide development using Yeehaw's wiki as context source and living documentation. Use when working on any project tracked in Yeehaw - consult wiki before architectural decisions and contribute back when patterns evolve.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Yeehaw Development
|
|
7
|
+
|
|
8
|
+
You're working on a project tracked in Yeehaw. The project wiki contains curated context to guide development - architecture patterns, conventions, gotchas, and common tasks documented by the project owner and evolved through development.
|
|
9
|
+
|
|
10
|
+
## Wiki-First Development
|
|
11
|
+
|
|
12
|
+
Before making architectural decisions or implementing significant features:
|
|
13
|
+
|
|
14
|
+
1. **Check available sections** with `mcp__yeehaw__get_wiki`
|
|
15
|
+
2. **Fetch relevant sections** with `mcp__yeehaw__get_wiki_section`
|
|
16
|
+
3. **Respect documented patterns** - they exist for a reason
|
|
17
|
+
|
|
18
|
+
The wiki is your primary source of project-specific context. It contains knowledge that isn't obvious from the code alone.
|
|
19
|
+
|
|
20
|
+
## When to Consult the Wiki
|
|
21
|
+
|
|
22
|
+
| Task | Check These Sections |
|
|
23
|
+
|------|---------------------|
|
|
24
|
+
| Adding features | Architecture, Conventions, Common Tasks |
|
|
25
|
+
| Debugging issues | Gotchas, Domain Context |
|
|
26
|
+
| Unsure about patterns | Conventions, Architecture |
|
|
27
|
+
| New integrations | Architecture, Gotchas |
|
|
28
|
+
| Understanding domain | Domain Context |
|
|
29
|
+
|
|
30
|
+
If a section exists that's relevant to your task, read it before proceeding.
|
|
31
|
+
|
|
32
|
+
## Proactive Wiki Updates
|
|
33
|
+
|
|
34
|
+
The wiki is a living document. After completing significant work, consider whether the wiki should be updated:
|
|
35
|
+
|
|
36
|
+
**When to update existing sections:**
|
|
37
|
+
- A documented pattern has evolved
|
|
38
|
+
- You discovered nuances worth capturing
|
|
39
|
+
- Steps in Common Tasks have changed
|
|
40
|
+
|
|
41
|
+
**When to add new sections:**
|
|
42
|
+
- Introduced a new architectural pattern
|
|
43
|
+
- Discovered gotchas during debugging
|
|
44
|
+
- Built a feature type that others might repeat
|
|
45
|
+
- Established new conventions
|
|
46
|
+
|
|
47
|
+
**How to update:**
|
|
48
|
+
- `mcp__yeehaw__update_wiki_section` for edits
|
|
49
|
+
- `mcp__yeehaw__add_wiki_section` for new content
|
|
50
|
+
|
|
51
|
+
**Guidelines:**
|
|
52
|
+
- Not every session needs to update the wiki
|
|
53
|
+
- Only update when something genuinely changes project direction or establishes new patterns
|
|
54
|
+
- Keep sections focused and scannable
|
|
55
|
+
- Be specific to THIS codebase, not generic advice
|
|
56
|
+
|
|
57
|
+
## Project Context
|
|
58
|
+
|
|
59
|
+
Use `mcp__yeehaw__get_project` to understand:
|
|
60
|
+
- Project structure and configured paths
|
|
61
|
+
- Deployment environments (livestock)
|
|
62
|
+
- Available wiki sections
|
|
63
|
+
|
|
64
|
+
When spawned from a specific livestock, you may be focused on deployment-specific work - check if there's relevant context in the wiki.
|
|
65
|
+
|
|
66
|
+
## Summary
|
|
67
|
+
|
|
68
|
+
1. **Before major decisions** → check the wiki
|
|
69
|
+
2. **After significant work** → consider updating the wiki
|
|
70
|
+
3. **When unsure** → the wiki likely has guidance
|
package/dist/app.js
CHANGED
|
@@ -13,12 +13,15 @@ import { WikiView } from './views/WikiView.js';
|
|
|
13
13
|
import { IssuesView } from './views/IssuesView.js';
|
|
14
14
|
import { LivestockDetailView } from './views/LivestockDetailView.js';
|
|
15
15
|
import { LogsView } from './views/LogsView.js';
|
|
16
|
+
import { CritterDetailView } from './views/CritterDetailView.js';
|
|
17
|
+
import { CritterLogsView } from './views/CritterLogsView.js';
|
|
16
18
|
import { NightSkyView } from './views/NightSkyView.js';
|
|
17
19
|
import { useConfig } from './hooks/useConfig.js';
|
|
18
20
|
import { useSessions } from './hooks/useSessions.js';
|
|
19
21
|
import { useRemoteYeehaw } from './hooks/useRemoteYeehaw.js';
|
|
20
|
-
import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, restartYeehaw, enterRemoteMode, ensureCorrectStatusBar, } from './lib/tmux.js';
|
|
22
|
+
import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, restartYeehaw, enterRemoteMode, ensureCorrectStatusBar, createClaudeWindowWithPrompt, } from './lib/tmux.js';
|
|
21
23
|
import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig, addCritterToBarn, removeCritterFromBarn } from './lib/config.js';
|
|
24
|
+
import { buildProjectContext, buildLivestockContext } from './lib/context.js';
|
|
22
25
|
import { getVersionInfo } from './lib/update-check.js';
|
|
23
26
|
function getHotkeyScope(view) {
|
|
24
27
|
switch (view.type) {
|
|
@@ -29,6 +32,8 @@ function getHotkeyScope(view) {
|
|
|
29
32
|
case 'issues': return 'issues-view';
|
|
30
33
|
case 'livestock': return 'livestock-detail';
|
|
31
34
|
case 'logs': return 'logs-view';
|
|
35
|
+
case 'critter': return 'critter-detail';
|
|
36
|
+
case 'critter-logs': return 'critter-logs';
|
|
32
37
|
case 'night-sky': return 'night-sky';
|
|
33
38
|
default: return 'global-dashboard';
|
|
34
39
|
}
|
|
@@ -40,7 +45,8 @@ function expandPath(path) {
|
|
|
40
45
|
return path;
|
|
41
46
|
}
|
|
42
47
|
// Global bottom bar items - minimal, consistent across all views
|
|
43
|
-
|
|
48
|
+
// Every hotkey should be visible somewhere in our 3-tier system
|
|
49
|
+
function getBottomBarItems(viewType, options) {
|
|
44
50
|
// Night sky has its own unique actions
|
|
45
51
|
if (viewType === 'night-sky') {
|
|
46
52
|
return [
|
|
@@ -49,29 +55,92 @@ function getBottomBarItems(viewType) {
|
|
|
49
55
|
{ key: 'Esc', label: 'exit' },
|
|
50
56
|
];
|
|
51
57
|
}
|
|
52
|
-
// Global dashboard: exit options
|
|
58
|
+
// Global dashboard: exit options + visualizer
|
|
53
59
|
if (viewType === 'global') {
|
|
54
60
|
return [
|
|
61
|
+
{ key: 'v', label: 'visualizer' },
|
|
55
62
|
{ key: 'q', label: 'detach' },
|
|
56
63
|
{ key: 'Q', label: 'quit' },
|
|
57
64
|
{ key: 'Tab', label: '' },
|
|
58
65
|
{ key: '?', label: 'help' },
|
|
59
66
|
];
|
|
60
67
|
}
|
|
68
|
+
// Project context: page-level actions
|
|
69
|
+
if (viewType === 'project') {
|
|
70
|
+
return [
|
|
71
|
+
{ key: 'w', label: 'wiki' },
|
|
72
|
+
{ key: 'i', label: 'issues' },
|
|
73
|
+
{ key: 'e', label: 'edit' },
|
|
74
|
+
{ key: 'Esc', label: 'back' },
|
|
75
|
+
{ key: 'Tab', label: '' },
|
|
76
|
+
{ key: '?', label: 'help' },
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
// Barn context: page-level actions (edit only for remote barns)
|
|
80
|
+
if (viewType === 'barn') {
|
|
81
|
+
const items = [];
|
|
82
|
+
if (!options?.isLocalBarn) {
|
|
83
|
+
items.push({ key: 'e', label: 'edit' });
|
|
84
|
+
}
|
|
85
|
+
items.push({ key: 'Esc', label: 'back' }, { key: 'Tab', label: '' }, { key: '?', label: 'help' });
|
|
86
|
+
return items;
|
|
87
|
+
}
|
|
88
|
+
// Wiki view: panel hints handle n/e/d, bottom bar just needs navigation
|
|
89
|
+
if (viewType === 'wiki') {
|
|
90
|
+
return [
|
|
91
|
+
{ key: 'Esc', label: 'back' },
|
|
92
|
+
{ key: 'Tab', label: '' },
|
|
93
|
+
{ key: '?', label: 'help' },
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
// Issues view: page-level actions (f/r are page-level, c/o are row-level shown on selected item)
|
|
97
|
+
if (viewType === 'issues') {
|
|
98
|
+
return [
|
|
99
|
+
{ key: 'f', label: 'filter' },
|
|
100
|
+
{ key: 'r', label: 'refresh' },
|
|
101
|
+
{ key: 'Esc', label: 'back' },
|
|
102
|
+
{ key: 'Tab', label: '' },
|
|
103
|
+
{ key: '?', label: 'help' },
|
|
104
|
+
];
|
|
105
|
+
}
|
|
61
106
|
// Livestock detail: page-level actions
|
|
107
|
+
// Local livestock gets [c] claude, remote only gets [s] shell
|
|
62
108
|
if (viewType === 'livestock') {
|
|
109
|
+
const items = [];
|
|
110
|
+
if (options?.isLocalLivestock) {
|
|
111
|
+
items.push({ key: 'c', label: 'claude' });
|
|
112
|
+
}
|
|
113
|
+
items.push({ key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: 'Esc', label: 'back' }, { key: '?', label: 'help' });
|
|
114
|
+
return items;
|
|
115
|
+
}
|
|
116
|
+
// Logs view: page-level actions
|
|
117
|
+
if (viewType === 'logs') {
|
|
118
|
+
return [
|
|
119
|
+
{ key: 'r', label: 'refresh' },
|
|
120
|
+
{ key: 'Esc', label: 'back' },
|
|
121
|
+
{ key: '?', label: 'help' },
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
// Critter detail: page-level actions
|
|
125
|
+
if (viewType === 'critter') {
|
|
63
126
|
return [
|
|
64
|
-
{ key: 's', label: 'shell' },
|
|
65
127
|
{ key: 'l', label: 'logs' },
|
|
66
128
|
{ key: 'e', label: 'edit' },
|
|
67
129
|
{ key: 'Esc', label: 'back' },
|
|
68
130
|
{ key: '?', label: 'help' },
|
|
69
131
|
];
|
|
70
132
|
}
|
|
71
|
-
//
|
|
133
|
+
// Critter logs view: page-level actions
|
|
134
|
+
if (viewType === 'critter-logs') {
|
|
135
|
+
return [
|
|
136
|
+
{ key: 'r', label: 'refresh' },
|
|
137
|
+
{ key: 'Esc', label: 'back' },
|
|
138
|
+
{ key: '?', label: 'help' },
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
// Fallback (shouldn't reach here)
|
|
72
142
|
return [
|
|
73
143
|
{ key: 'Esc', label: 'back' },
|
|
74
|
-
{ key: 'Tab', label: '' },
|
|
75
144
|
{ key: '?', label: 'help' },
|
|
76
145
|
];
|
|
77
146
|
}
|
|
@@ -127,6 +196,16 @@ export function App() {
|
|
|
127
196
|
return { ...currentView, barn: freshBarn };
|
|
128
197
|
}
|
|
129
198
|
}
|
|
199
|
+
// Update critter detail view
|
|
200
|
+
if (currentView.type === 'critter' || currentView.type === 'critter-logs') {
|
|
201
|
+
const freshBarn = barns.find((b) => b.name === currentView.barn.name);
|
|
202
|
+
if (freshBarn && freshBarn !== currentView.barn) {
|
|
203
|
+
const freshCritter = (freshBarn.critters || []).find((c) => c.name === currentView.critter.name);
|
|
204
|
+
if (freshCritter) {
|
|
205
|
+
return { ...currentView, barn: freshBarn, critter: freshCritter };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
130
209
|
return currentView;
|
|
131
210
|
});
|
|
132
211
|
}, [projects, barns]);
|
|
@@ -154,7 +233,11 @@ export function App() {
|
|
|
154
233
|
const projectName = view.type === 'project' ? view.project.name : 'yeehaw';
|
|
155
234
|
const workingDir = view.type === 'project' ? expandPath(view.project.path) : process.cwd();
|
|
156
235
|
const windowName = `${projectName}-claude`;
|
|
157
|
-
|
|
236
|
+
// Inject context when in a project view
|
|
237
|
+
const context = view.type === 'project' ? buildProjectContext(view.project.name) : null;
|
|
238
|
+
const windowIndex = context
|
|
239
|
+
? createClaudeWindowWithPrompt(workingDir, windowName, context)
|
|
240
|
+
: createClaude(workingDir, windowName);
|
|
158
241
|
switchToWindow(windowIndex);
|
|
159
242
|
}
|
|
160
243
|
catch (err) {
|
|
@@ -170,7 +253,11 @@ export function App() {
|
|
|
170
253
|
try {
|
|
171
254
|
const workingDir = expandPath(project.path);
|
|
172
255
|
const windowName = `${project.name}-claude`;
|
|
173
|
-
const
|
|
256
|
+
const context = buildProjectContext(project.name);
|
|
257
|
+
// Use context injection if we have project context, otherwise fall back to basic
|
|
258
|
+
const windowIndex = context
|
|
259
|
+
? createClaudeWindowWithPrompt(workingDir, windowName, context)
|
|
260
|
+
: createClaude(workingDir, windowName);
|
|
174
261
|
switchToWindow(windowIndex);
|
|
175
262
|
}
|
|
176
263
|
catch (err) {
|
|
@@ -186,7 +273,11 @@ export function App() {
|
|
|
186
273
|
try {
|
|
187
274
|
const workingDir = expandPath(livestock.path);
|
|
188
275
|
const windowName = `${projectName}-${livestock.name}-claude`;
|
|
189
|
-
const
|
|
276
|
+
const context = buildLivestockContext(projectName, livestock.name);
|
|
277
|
+
// Use context injection if we have project context, otherwise fall back to basic
|
|
278
|
+
const windowIndex = context
|
|
279
|
+
? createClaudeWindowWithPrompt(workingDir, windowName, context)
|
|
280
|
+
: createClaude(workingDir, windowName);
|
|
190
281
|
switchToWindow(windowIndex);
|
|
191
282
|
}
|
|
192
283
|
catch (err) {
|
|
@@ -194,6 +285,24 @@ export function App() {
|
|
|
194
285
|
setError(`Failed to create Claude session: ${message}`);
|
|
195
286
|
}
|
|
196
287
|
}, [tmuxAvailable, createClaude]);
|
|
288
|
+
const handleOpenClaudeWithContext = useCallback((workingDir, issueContext) => {
|
|
289
|
+
if (!tmuxAvailable) {
|
|
290
|
+
setError('tmux is not installed');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
// Get the project name for the window name
|
|
295
|
+
const projectName = (view.type === 'issues' ? view.project.name : 'issue');
|
|
296
|
+
const windowName = `${projectName}-claude`;
|
|
297
|
+
// Create claude window with system prompt
|
|
298
|
+
const index = createClaudeWindowWithPrompt(workingDir, windowName, issueContext);
|
|
299
|
+
switchToWindow(index);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
303
|
+
setError(`Failed to create Claude session: ${message}`);
|
|
304
|
+
}
|
|
305
|
+
}, [tmuxAvailable, view]);
|
|
197
306
|
const handleOpenLivestockSession = useCallback((livestock, barn, projectName) => {
|
|
198
307
|
if (!tmuxAvailable) {
|
|
199
308
|
setError('tmux is not installed');
|
|
@@ -276,6 +385,26 @@ export function App() {
|
|
|
276
385
|
// Update the view with the new livestock data, preserving navigation context
|
|
277
386
|
setView({ type: 'livestock', project: updatedProject, livestock: updatedLivestock, source, sourceBarn });
|
|
278
387
|
}, [reload]);
|
|
388
|
+
const handleOpenCritterDetail = useCallback((barn, critter) => {
|
|
389
|
+
setView({ type: 'critter', barn, critter });
|
|
390
|
+
updateStatusBar(`${barn.name} / ${critter.name}`);
|
|
391
|
+
}, []);
|
|
392
|
+
const handleOpenCritterLogs = useCallback((barn, critter) => {
|
|
393
|
+
setView({ type: 'critter-logs', barn, critter });
|
|
394
|
+
updateStatusBar(`${barn.name} / ${critter.name} Logs`);
|
|
395
|
+
}, []);
|
|
396
|
+
const handleBackFromCritter = useCallback((barn) => {
|
|
397
|
+
setView({ type: 'barn', barn });
|
|
398
|
+
updateStatusBar(`Barn: ${barn.name}`);
|
|
399
|
+
}, []);
|
|
400
|
+
const handleUpdateCritter = useCallback((barn, originalCritter, updatedCritter) => {
|
|
401
|
+
// Remove old critter and add updated one
|
|
402
|
+
removeCritterFromBarn(barn.name, originalCritter.name);
|
|
403
|
+
addCritterToBarn(barn.name, updatedCritter);
|
|
404
|
+
reload();
|
|
405
|
+
// Update view with new critter data
|
|
406
|
+
setView({ type: 'critter', barn, critter: updatedCritter });
|
|
407
|
+
}, [reload]);
|
|
279
408
|
const handleDeleteProject = useCallback((projectName) => {
|
|
280
409
|
deleteProject(projectName);
|
|
281
410
|
reload();
|
|
@@ -407,6 +536,13 @@ export function App() {
|
|
|
407
536
|
else if (view.type === 'livestock') {
|
|
408
537
|
handleBackFromLivestock(view.source, view.project, view.sourceBarn);
|
|
409
538
|
}
|
|
539
|
+
else if (view.type === 'critter-logs') {
|
|
540
|
+
setView({ type: 'critter', barn: view.barn, critter: view.critter });
|
|
541
|
+
updateStatusBar(`${view.barn.name} / ${view.critter.name}`);
|
|
542
|
+
}
|
|
543
|
+
else if (view.type === 'critter') {
|
|
544
|
+
handleBackFromCritter(view.barn);
|
|
545
|
+
}
|
|
410
546
|
else if (view.type === 'project' || view.type === 'barn') {
|
|
411
547
|
handleBack();
|
|
412
548
|
}
|
|
@@ -475,21 +611,29 @@ export function App() {
|
|
|
475
611
|
}, onRemoveCritter: (critterName) => {
|
|
476
612
|
removeCritterFromBarn(view.barn.name, critterName);
|
|
477
613
|
reload();
|
|
478
|
-
} }));
|
|
614
|
+
}, onSelectCritter: (critter) => handleOpenCritterDetail(view.barn, critter) }));
|
|
479
615
|
case 'wiki':
|
|
480
616
|
return (_jsx(WikiView, { project: view.project, onBack: () => handleBackFromSubview(view.project), onUpdateProject: handleUpdateProject }));
|
|
481
617
|
case 'issues':
|
|
482
|
-
return (_jsx(IssuesView, { project: view.project, onBack: () => handleBackFromSubview(view.project) }));
|
|
618
|
+
return (_jsx(IssuesView, { project: view.project, onBack: () => handleBackFromSubview(view.project), onOpenClaude: handleOpenClaudeWithContext }));
|
|
483
619
|
case 'livestock':
|
|
620
|
+
const livestockBarn = barns.find((b) => b.name === view.livestock.barn) || null;
|
|
621
|
+
const isLocalLivestock = !livestockBarn || isLocalBarn(livestockBarn);
|
|
484
622
|
return (_jsx(LivestockDetailView, { project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn, windows: windows, onBack: () => handleBackFromLivestock(view.source, view.project, view.sourceBarn), onOpenLogs: () => handleOpenLogs(view.project, view.livestock, view.source, view.sourceBarn), onOpenSession: () => {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}, onSelectWindow: handleSelectWindow, onUpdateLivestock: (originalLivestock, updatedLivestock) => handleUpdateLivestock(view.project, originalLivestock, updatedLivestock, view.source, view.sourceBarn) }));
|
|
623
|
+
handleOpenLivestockSession(view.livestock, livestockBarn, view.project.name);
|
|
624
|
+
}, onOpenClaude: isLocalLivestock ? () => handleNewClaudeForLivestock(view.livestock, view.project.name) : undefined, onSelectWindow: handleSelectWindow, onUpdateLivestock: (originalLivestock, updatedLivestock) => handleUpdateLivestock(view.project, originalLivestock, updatedLivestock, view.source, view.sourceBarn) }));
|
|
488
625
|
case 'logs':
|
|
489
626
|
return (_jsx(LogsView, { project: view.project, livestock: view.livestock, onBack: () => {
|
|
490
627
|
setView({ type: 'livestock', project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn });
|
|
491
628
|
updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
|
|
492
629
|
} }));
|
|
630
|
+
case 'critter':
|
|
631
|
+
return (_jsx(CritterDetailView, { barn: view.barn, critter: view.critter, onBack: () => handleBackFromCritter(view.barn), onOpenLogs: () => handleOpenCritterLogs(view.barn, view.critter), onUpdateCritter: (original, updated) => handleUpdateCritter(view.barn, original, updated) }));
|
|
632
|
+
case 'critter-logs':
|
|
633
|
+
return (_jsx(CritterLogsView, { barn: view.barn, critter: view.critter, onBack: () => {
|
|
634
|
+
setView({ type: 'critter', barn: view.barn, critter: view.critter });
|
|
635
|
+
updateStatusBar(`${view.barn.name} / ${view.critter.name}`);
|
|
636
|
+
} }));
|
|
493
637
|
case 'night-sky':
|
|
494
638
|
return (_jsx(NightSkyView, { onExit: handleExitNightSky }));
|
|
495
639
|
}
|
|
@@ -500,5 +644,12 @@ export function App() {
|
|
|
500
644
|
if (showSplash) {
|
|
501
645
|
return _jsx(SplashScreen, { onComplete: handleSplashComplete });
|
|
502
646
|
}
|
|
503
|
-
return (_jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [error && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: renderView() }), !showHelp && (_jsx(BottomBar, { items: getBottomBarItems(view.type
|
|
647
|
+
return (_jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [error && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: renderView() }), !showHelp && (_jsx(BottomBar, { items: getBottomBarItems(view.type, {
|
|
648
|
+
isLocalLivestock: view.type === 'livestock'
|
|
649
|
+
? !view.livestock.barn || isLocalBarn(barns.find((b) => b.name === view.livestock.barn) || { name: 'local' })
|
|
650
|
+
: undefined,
|
|
651
|
+
isLocalBarn: view.type === 'barn'
|
|
652
|
+
? isLocalBarn(view.barn)
|
|
653
|
+
: undefined,
|
|
654
|
+
}), environments: environments, isDetecting: isDetecting }))] }));
|
|
504
655
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
// Rabbit ASCII art template - @ symbols are pattern spots
|
|
4
|
+
const RABBIT_TEMPLATE = [
|
|
5
|
+
" __",
|
|
6
|
+
" /\\ .-\" /",
|
|
7
|
+
" / ; .' .' ",
|
|
8
|
+
" : :/ .' ",
|
|
9
|
+
" \\ ;-.' ",
|
|
10
|
+
" .--\"\"\"\"--..__/ `. ",
|
|
11
|
+
" .'@@@@@@@@@@@.' o \\ ",
|
|
12
|
+
" /@@@@@@@@@@@@@@@@ ; ",
|
|
13
|
+
" :@@@@@@@@@@@@@@@@\\ : ",
|
|
14
|
+
" .-;@@@@@@@@-.@@@@@@@`.__.--' ",
|
|
15
|
+
": ;@@@@@@@@@@\\@@@@@, ; ",
|
|
16
|
+
"'._:@@@@@@@@@@@;@@@: ( ",
|
|
17
|
+
" \\/ .__ ; \\ `-. ",
|
|
18
|
+
" ; \"-,/_..--\"`-..__) ",
|
|
19
|
+
" '\"\"--.._: ",
|
|
20
|
+
];
|
|
21
|
+
// Generate multiple hash values from a string for better distribution
|
|
22
|
+
function multiHash(str) {
|
|
23
|
+
const hashes = [];
|
|
24
|
+
// First hash - djb2
|
|
25
|
+
let hash1 = 5381;
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
hash1 = ((hash1 << 5) + hash1) ^ str.charCodeAt(i);
|
|
28
|
+
}
|
|
29
|
+
hashes.push(Math.abs(hash1));
|
|
30
|
+
// Second hash - sdbm
|
|
31
|
+
let hash2 = 0;
|
|
32
|
+
for (let i = 0; i < str.length; i++) {
|
|
33
|
+
hash2 = str.charCodeAt(i) + (hash2 << 6) + (hash2 << 16) - hash2;
|
|
34
|
+
}
|
|
35
|
+
hashes.push(Math.abs(hash2));
|
|
36
|
+
// Third hash - fnv-1a inspired
|
|
37
|
+
let hash3 = 2166136261;
|
|
38
|
+
for (let i = 0; i < str.length; i++) {
|
|
39
|
+
hash3 ^= str.charCodeAt(i);
|
|
40
|
+
hash3 = (hash3 * 16777619) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
hashes.push(hash3);
|
|
43
|
+
return hashes;
|
|
44
|
+
}
|
|
45
|
+
// Pattern characters - space and block characters
|
|
46
|
+
const PATTERN_CHARS = [' ', ' ', '░', '░', '▒', '▓', '█'];
|
|
47
|
+
// Generate pattern variation for the rabbit based on critter/barn data
|
|
48
|
+
function generateRabbitArt(critter, barn) {
|
|
49
|
+
// Use barn name + critter name + service for unique seed
|
|
50
|
+
const seed = `${barn.name}-${critter.name}-${critter.service}`;
|
|
51
|
+
const hashes = multiHash(seed);
|
|
52
|
+
// Replace @ symbols with pattern characters
|
|
53
|
+
let charIndex = 0;
|
|
54
|
+
const rabbitArt = RABBIT_TEMPLATE.map((line, lineIndex) => {
|
|
55
|
+
let result = '';
|
|
56
|
+
for (const char of line) {
|
|
57
|
+
if (char === '@') {
|
|
58
|
+
const h1 = hashes[0];
|
|
59
|
+
const h2 = hashes[1];
|
|
60
|
+
const h3 = hashes[2];
|
|
61
|
+
const mix = (h1 >> (charIndex % 17)) ^
|
|
62
|
+
(h2 >> ((charIndex + lineIndex) % 13)) ^
|
|
63
|
+
(h3 >> ((charIndex * 7 + lineIndex * 3) % 19));
|
|
64
|
+
const charChoice = Math.abs(mix) % PATTERN_CHARS.length;
|
|
65
|
+
result += PATTERN_CHARS[charChoice];
|
|
66
|
+
charIndex++;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result += char;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
});
|
|
74
|
+
return rabbitArt;
|
|
75
|
+
}
|
|
76
|
+
export function CritterHeader({ barn, critter }) {
|
|
77
|
+
const rabbitArt = generateRabbitArt(critter, barn);
|
|
78
|
+
// Use a default color for critters (could be configurable later)
|
|
79
|
+
const color = '#8fbc8f'; // Dark sea green
|
|
80
|
+
return (_jsx(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 1, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", children: rabbitArt.map((line, i) => (_jsx(Text, { color: color, bold: true, children: line }, i))) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, justifyContent: "center", children: [_jsx(Text, { bold: true, color: color, children: critter.name }), _jsxs(Text, { dimColor: true, children: ["barn: ", barn.name] }), _jsxs(Text, { dimColor: true, children: ["service: ", critter.service] })] })] }) }));
|
|
81
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import type { SessionStatus } from '../lib/signals.js';
|
|
2
3
|
export interface RowAction {
|
|
3
4
|
key: string;
|
|
@@ -10,6 +11,7 @@ export interface ListItem {
|
|
|
10
11
|
meta?: string;
|
|
11
12
|
sessionStatus?: SessionStatus;
|
|
12
13
|
actions?: RowAction[];
|
|
14
|
+
prefix?: React.ReactNode;
|
|
13
15
|
}
|
|
14
16
|
interface ListProps {
|
|
15
17
|
items: ListItem[];
|
package/dist/components/List.js
CHANGED
|
@@ -57,6 +57,6 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
|
|
|
57
57
|
item.sessionStatus === 'error' ? 'red' : undefined;
|
|
58
58
|
const statusColor = item.status === 'active' ? 'green' :
|
|
59
59
|
item.status === 'error' ? 'red' : 'gray';
|
|
60
|
-
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 0, width: 2, children: _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, children: isSelected ? '›' : ' ' }) }), _jsxs(Box, { gap: 1, flexShrink: 1, flexGrow: 1, overflow: "hidden", children: [_jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, bold: isSelected, wrap: "truncate", children: item.label }), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" })), item.meta && (_jsx(Text, { color: sessionStatusColor, dimColor: !sessionStatusColor, wrap: "truncate", children: item.meta }))] }), isSelected && item.actions && item.actions.length > 0 && (_jsx(Box, { gap: 2, flexShrink: 0, marginLeft: 1, children: item.actions.map((action) => (_jsxs(Text, { children: [_jsxs(Text, { color: BRAND_COLOR, children: ["[", action.key, "]"] }), _jsxs(Text, { dimColor: true, children: [" ", action.label] })] }, action.key))) }))] }, item.id));
|
|
60
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 0, width: 2, children: _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, children: isSelected ? '›' : ' ' }) }), _jsxs(Box, { gap: 1, flexShrink: 1, flexGrow: 1, overflow: "hidden", children: [item.prefix && (_jsx(Box, { flexShrink: 0, children: item.prefix })), _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, bold: isSelected, wrap: "truncate", children: item.label }), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" })), item.meta && (_jsx(Text, { color: sessionStatusColor, dimColor: !sessionStatusColor, wrap: "truncate", children: item.meta }))] }), isSelected && item.actions && item.actions.length > 0 && (_jsx(Box, { gap: 2, flexShrink: 0, marginLeft: 1, children: item.actions.map((action) => (_jsxs(Text, { children: [_jsxs(Text, { color: BRAND_COLOR, children: ["[", action.key, "]"] }), _jsxs(Text, { dimColor: true, children: [" ", action.label] })] }, action.key))) }))] }, item.id));
|
|
61
61
|
}) }));
|
|
62
62
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { clearLinearToken } from './storage.js';
|
|
2
|
+
export declare const LINEAR_API_KEY_URL = "https://linear.app/settings/api";
|
|
3
|
+
export { clearLinearToken };
|
|
4
|
+
/**
|
|
5
|
+
* Check if Linear is currently authenticated.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isLinearAuthenticated(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Save a Linear API key.
|
|
10
|
+
*/
|
|
11
|
+
export declare function saveLinearApiKey(apiKey: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Validate a Linear API key by making a test request.
|
|
14
|
+
* Returns true if valid, false otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateLinearApiKey(apiKey: string): Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Make an authenticated request to Linear's GraphQL API.
|
|
19
|
+
*/
|
|
20
|
+
export declare function linearGraphQL<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/lib/auth/linear.ts
|
|
2
|
+
// Linear authentication using Personal API Keys
|
|
3
|
+
// Users create keys at: https://linear.app/settings/api
|
|
4
|
+
import { setLinearToken, getLinearToken, clearLinearToken } from './storage.js';
|
|
5
|
+
export const LINEAR_API_KEY_URL = 'https://linear.app/settings/api';
|
|
6
|
+
export { clearLinearToken };
|
|
7
|
+
/**
|
|
8
|
+
* Check if Linear is currently authenticated.
|
|
9
|
+
*/
|
|
10
|
+
export function isLinearAuthenticated() {
|
|
11
|
+
return getLinearToken() !== null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Save a Linear API key.
|
|
15
|
+
*/
|
|
16
|
+
export function saveLinearApiKey(apiKey) {
|
|
17
|
+
setLinearToken(apiKey);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate a Linear API key by making a test request.
|
|
21
|
+
* Returns true if valid, false otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export async function validateLinearApiKey(apiKey) {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Authorization': apiKey,
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
query: '{ viewer { id } }',
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
// Check if we got HTML instead of JSON (indicates auth/routing issue)
|
|
40
|
+
if (text.startsWith('<!') || text.startsWith('<html')) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const result = JSON.parse(text);
|
|
44
|
+
return !!(result.data?.viewer?.id);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Make an authenticated request to Linear's GraphQL API.
|
|
52
|
+
*/
|
|
53
|
+
export async function linearGraphQL(query, variables) {
|
|
54
|
+
const token = getLinearToken();
|
|
55
|
+
if (!token) {
|
|
56
|
+
throw new Error('Not authenticated with Linear');
|
|
57
|
+
}
|
|
58
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
'Authorization': token,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ query, variables }),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Linear API error: ${response.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
const text = await response.text();
|
|
70
|
+
// Check if we got HTML instead of JSON
|
|
71
|
+
if (text.startsWith('<!') || text.startsWith('<html')) {
|
|
72
|
+
throw new Error('Linear API returned HTML instead of JSON - check your API key');
|
|
73
|
+
}
|
|
74
|
+
const result = JSON.parse(text);
|
|
75
|
+
if (result.errors?.length) {
|
|
76
|
+
throw new Error(result.errors[0].message);
|
|
77
|
+
}
|
|
78
|
+
return result.data;
|
|
79
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LinearAuth {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
expiresAt?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AuthConfig {
|
|
6
|
+
linear?: LinearAuth;
|
|
7
|
+
}
|
|
8
|
+
export declare function loadAuth(): AuthConfig;
|
|
9
|
+
export declare function saveAuth(auth: AuthConfig): void;
|
|
10
|
+
export declare function getLinearToken(): string | null;
|
|
11
|
+
export declare function setLinearToken(accessToken: string, expiresAt?: Date): void;
|
|
12
|
+
export declare function clearLinearToken(): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/lib/auth/storage.ts
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import YAML from 'js-yaml';
|
|
4
|
+
import { AUTH_FILE, YEEHAW_DIR } from '../paths.js';
|
|
5
|
+
import { mkdirSync } from 'fs';
|
|
6
|
+
export function loadAuth() {
|
|
7
|
+
if (!existsSync(YEEHAW_DIR)) {
|
|
8
|
+
mkdirSync(YEEHAW_DIR, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
if (!existsSync(AUTH_FILE)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(AUTH_FILE, 'utf-8');
|
|
15
|
+
return YAML.load(content) || {};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function saveAuth(auth) {
|
|
22
|
+
if (!existsSync(YEEHAW_DIR)) {
|
|
23
|
+
mkdirSync(YEEHAW_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
writeFileSync(AUTH_FILE, YAML.dump(auth), 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
export function getLinearToken() {
|
|
28
|
+
const auth = loadAuth();
|
|
29
|
+
if (!auth.linear?.accessToken) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Check expiration if set
|
|
33
|
+
if (auth.linear.expiresAt) {
|
|
34
|
+
const expiresAt = new Date(auth.linear.expiresAt);
|
|
35
|
+
if (expiresAt <= new Date()) {
|
|
36
|
+
return null; // Token expired
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return auth.linear.accessToken;
|
|
40
|
+
}
|
|
41
|
+
export function setLinearToken(accessToken, expiresAt) {
|
|
42
|
+
const auth = loadAuth();
|
|
43
|
+
auth.linear = {
|
|
44
|
+
accessToken,
|
|
45
|
+
expiresAt: expiresAt?.toISOString(),
|
|
46
|
+
};
|
|
47
|
+
saveAuth(auth);
|
|
48
|
+
}
|
|
49
|
+
export function clearLinearToken() {
|
|
50
|
+
const auth = loadAuth();
|
|
51
|
+
delete auth.linear;
|
|
52
|
+
saveAuth(auth);
|
|
53
|
+
}
|