@colmbus72/yeehaw 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +7 -0
- package/claude-plugin/skills/yeehaw-project-setup/SKILL.md +129 -0
- package/claude-plugin/skills/yeehaw-project-setup/references/color-discovery.md +170 -0
- package/claude-plugin/skills/yeehaw-project-setup/references/wiki-templates.md +266 -0
- package/dist/app.js +20 -8
- package/dist/components/Header.d.ts +4 -1
- package/dist/components/Header.js +33 -23
- package/dist/components/LivestockHeader.js +6 -6
- package/dist/components/SplashScreen.d.ts +5 -0
- package/dist/components/SplashScreen.js +178 -0
- package/dist/index.js +2 -5
- package/dist/lib/tmux.js +7 -2
- package/dist/types.d.ts +2 -0
- package/dist/views/GlobalDashboard.js +16 -16
- package/dist/views/LivestockDetailView.d.ts +1 -1
- package/dist/views/LivestockDetailView.js +27 -45
- package/dist/views/ProjectContext.js +138 -18
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import figlet from 'figlet';
|
|
5
5
|
// Convert hex to RGB
|
|
@@ -21,34 +21,43 @@ function interpolateColor(color1, color2, factor) {
|
|
|
21
21
|
return `rgb(${r},${g},${b})`;
|
|
22
22
|
}
|
|
23
23
|
// Generate gradient colors for each line
|
|
24
|
-
function generateGradient(lines, baseColor) {
|
|
24
|
+
function generateGradient(lines, baseColor, spread = 5, inverted = false, theme = 'dark') {
|
|
25
25
|
const rgb = hexToRgb(baseColor);
|
|
26
26
|
if (!rgb)
|
|
27
27
|
return lines.map(() => baseColor);
|
|
28
|
+
// Spread controls how much the gradient changes (0 = no change, 10 = max change)
|
|
29
|
+
// Convert 0-10 scale to a multiplier (0 = 1.0, 10 = 0.1 for darkening factor)
|
|
30
|
+
const spreadFactor = 1 - (spread / 10) * 0.9; // 0->1.0, 5->0.55, 10->0.1
|
|
28
31
|
// Calculate luminance to detect dark colors
|
|
29
32
|
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
30
33
|
let startRgb;
|
|
31
34
|
let endRgb;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Determine gradient direction based on color luminance and theme
|
|
36
|
+
const isDarkColor = luminance < 0.3;
|
|
37
|
+
const shouldLighten = isDarkColor || theme === 'light';
|
|
38
|
+
if (shouldLighten) {
|
|
39
|
+
// Go from a lighter tint down to the base color (or adjusted end)
|
|
40
|
+
const liftFactor = 1 + (spread / 10) * 2; // More spread = more lift
|
|
36
41
|
startRgb = {
|
|
37
|
-
r: Math.min(255, Math.round(rgb.r * liftFactor +
|
|
38
|
-
g: Math.min(255, Math.round(rgb.g * liftFactor +
|
|
39
|
-
b: Math.min(255, Math.round(rgb.b * liftFactor +
|
|
42
|
+
r: Math.min(255, Math.round(rgb.r * liftFactor + spread * 8)),
|
|
43
|
+
g: Math.min(255, Math.round(rgb.g * liftFactor + spread * 8)),
|
|
44
|
+
b: Math.min(255, Math.round(rgb.b * liftFactor + spread * 8)),
|
|
40
45
|
};
|
|
41
46
|
endRgb = rgb;
|
|
42
47
|
}
|
|
43
48
|
else {
|
|
44
|
-
//
|
|
49
|
+
// Go from base to a darker version
|
|
45
50
|
startRgb = rgb;
|
|
46
51
|
endRgb = {
|
|
47
|
-
r: Math.round(rgb.r *
|
|
48
|
-
g: Math.round(rgb.g *
|
|
49
|
-
b: Math.round(rgb.b *
|
|
52
|
+
r: Math.round(rgb.r * spreadFactor),
|
|
53
|
+
g: Math.round(rgb.g * spreadFactor),
|
|
54
|
+
b: Math.round(rgb.b * spreadFactor),
|
|
50
55
|
};
|
|
51
56
|
}
|
|
57
|
+
// Invert if requested
|
|
58
|
+
if (inverted) {
|
|
59
|
+
[startRgb, endRgb] = [endRgb, startRgb];
|
|
60
|
+
}
|
|
52
61
|
return lines.map((_, i) => {
|
|
53
62
|
const factor = i / Math.max(lines.length - 1, 1);
|
|
54
63
|
return interpolateColor(startRgb, endRgb, factor);
|
|
@@ -63,21 +72,22 @@ const TUMBLEWEED = [
|
|
|
63
72
|
];
|
|
64
73
|
// Brownish tan color to complement yeehaw gold
|
|
65
74
|
const TUMBLEWEED_COLOR = '#b8860b';
|
|
66
|
-
export function Header({ text, subtitle, summary, color, versionInfo }) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
export function Header({ text, subtitle, summary, color, gradientSpread, gradientInverted, theme, versionInfo }) {
|
|
76
|
+
// Use sync figlet to avoid flash on initial render
|
|
77
|
+
const ascii = useMemo(() => {
|
|
78
|
+
try {
|
|
79
|
+
return figlet.textSync(text.toUpperCase(), { font: 'ANSI Shadow' });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return text.toUpperCase();
|
|
83
|
+
}
|
|
74
84
|
}, [text]);
|
|
75
85
|
const lines = ascii.split('\n').filter(line => line.trim() !== '');
|
|
76
86
|
const baseColor = color || '#f0c040'; // Default yeehaw gold
|
|
77
|
-
const gradientColors = generateGradient(lines, baseColor);
|
|
87
|
+
const gradientColors = generateGradient(lines, baseColor, gradientSpread ?? 5, gradientInverted ?? false, theme ?? 'dark');
|
|
78
88
|
// Show tumbleweed only for the main "yeehaw" title
|
|
79
89
|
const showTumbleweed = text.toLowerCase() === 'yeehaw';
|
|
80
90
|
// Vertically center tumbleweed next to ASCII art
|
|
81
91
|
const tumbleweedTopPadding = Math.max(0, Math.floor((lines.length - TUMBLEWEED.length) / 2));
|
|
82
|
-
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: [_jsxs(Box, { flexDirection: "row", children: [showTumbleweed && (_jsxs(Box, { flexDirection: "column", marginRight: 2, children: [Array(tumbleweedTopPadding).fill(null).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), TUMBLEWEED.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, children: line }, `tumbleweed-${i}`)))] })), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: gradientColors[i], children: line }, i))) }), versionInfo && showTumbleweed && (_jsx(Box, { marginLeft: 2, paddingRight: 1, alignItems: "flex-end", flexGrow: 1, justifyContent: "flex-end", children: versionInfo.latest && versionInfo.latest !== versionInfo.current ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] }), _jsx(Text, { dimColor: true, children: " \u2192 " }), _jsxs(Text, { color: "yellow", children: ["v", versionInfo.latest] })] })) : versionInfo.latest ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] }), _jsx(Text, { color: "green", children: " \u2713 latest" })] })) : (_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] })) }))] }), (subtitle || summary) && (_jsxs(Box, { gap: 2, children: [subtitle && _jsx(Text, { dimColor: true, children: subtitle }), summary && _jsxs(Text, { color: "gray", children: ["- ", summary] })] }))] }));
|
|
92
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: [_jsxs(Box, { flexDirection: "row", children: [showTumbleweed && (_jsxs(Box, { flexDirection: "column", marginRight: 2, children: [Array(tumbleweedTopPadding).fill(null).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), TUMBLEWEED.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, bold: true, children: line }, `tumbleweed-${i}`)))] })), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: gradientColors[i], children: line }, i))) }), versionInfo && showTumbleweed && (_jsx(Box, { marginLeft: 2, paddingRight: 1, alignItems: "flex-end", flexGrow: 1, justifyContent: "flex-end", children: versionInfo.latest && versionInfo.latest !== versionInfo.current ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] }), _jsx(Text, { dimColor: true, children: " \u2192 " }), _jsxs(Text, { color: "yellow", children: ["v", versionInfo.latest] })] })) : versionInfo.latest ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] }), _jsx(Text, { color: "green", children: " \u2713 latest" })] })) : (_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] })) }))] }), (subtitle || summary) && (_jsxs(Box, { gap: 2, children: [subtitle && _jsx(Text, { dimColor: true, children: subtitle }), summary && _jsxs(Text, { color: "gray", children: ["- ", summary] })] }))] }));
|
|
83
93
|
}
|
|
@@ -20,11 +20,11 @@ const COW_TEMPLATE = [
|
|
|
20
20
|
" /{_\\_/ `'\\____",
|
|
21
21
|
" \\___ (o) (o }",
|
|
22
22
|
" _______________________/ :--'",
|
|
23
|
-
" ,-,'
|
|
24
|
-
" ;:(
|
|
25
|
-
" :: )
|
|
26
|
-
" :: \\
|
|
27
|
-
" ;; /\\
|
|
23
|
+
" ,-,'`@@@@@@@@@@@@@@@@@@@@@@ \\_ `__\\",
|
|
24
|
+
" ;:( @@@@@@@@@@@@@@@@@@@@@@@@ \\___(o'o)",
|
|
25
|
+
" :: ) @@@@@@@@@@@@@@@@@@@@@@@,'@@( `===='",
|
|
26
|
+
" :: \\ @@@@@@: @@@@@@@) @@ ( '@@@'",
|
|
27
|
+
" ;; /\\ @@@ /`, @@@@@\\ :@@@@@)",
|
|
28
28
|
" ::/ ) {_----------: :~`,~~;",
|
|
29
29
|
" ;;'`; : ) : / `; ;",
|
|
30
30
|
"`'`' / : : : : : :",
|
|
@@ -118,5 +118,5 @@ export function LivestockHeader({ project, livestock }) {
|
|
|
118
118
|
// We'll show it to the right of the cow
|
|
119
119
|
const cowHeight = cowArt.length;
|
|
120
120
|
const infoStartLine = Math.floor(cowHeight / 2) - 1;
|
|
121
|
-
return (_jsx(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 1, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", children: cowArt.map((line, i) => (_jsx(Text, { color: color, children: line }, i))) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, justifyContent: "center", children: [_jsx(Text, { bold: true, color: color, children: livestock.name }), _jsxs(Text, { dimColor: true, children: ["project: ", project.name] }), _jsxs(Text, { dimColor: true, children: ["barn: ", livestock.barn || 'local'] }), livestock.branch && (_jsxs(Text, { dimColor: true, children: ["branch: ", livestock.branch] }))] })] }) }));
|
|
121
|
+
return (_jsx(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 1, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", children: cowArt.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: livestock.name }), _jsxs(Text, { dimColor: true, children: ["project: ", project.name] }), _jsxs(Text, { dimColor: true, children: ["barn: ", livestock.barn || 'local'] }), livestock.branch && (_jsxs(Text, { dimColor: true, children: ["branch: ", livestock.branch] }))] })] }) }));
|
|
122
122
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
|
+
import figlet from 'figlet';
|
|
5
|
+
// Tumbleweed ASCII art - same as in Header.tsx
|
|
6
|
+
const TUMBLEWEED = [
|
|
7
|
+
' ░ ░▒░ ░▒░',
|
|
8
|
+
'░▒ · ‿ · ▒░',
|
|
9
|
+
'▒░ ▒░▒░ ░▒',
|
|
10
|
+
' ░▒░ ░▒░ ░',
|
|
11
|
+
];
|
|
12
|
+
const TUMBLEWEED_COLOR = '#b8860b';
|
|
13
|
+
const BRAND_COLOR = '#f0c040';
|
|
14
|
+
// Match Header.tsx positioning exactly
|
|
15
|
+
const HEADER_PADDING_TOP = 1;
|
|
16
|
+
const TUMBLEWEED_TOP_PADDING = 1;
|
|
17
|
+
const HEADER_PADDING_LEFT = 2;
|
|
18
|
+
const TUMBLEWEED_WIDTH = 11;
|
|
19
|
+
const TITLE_OFFSET_LEFT = HEADER_PADDING_LEFT + TUMBLEWEED_WIDTH + 2;
|
|
20
|
+
// Gradient color helpers (matching Header.tsx)
|
|
21
|
+
function hexToRgb(hex) {
|
|
22
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
23
|
+
return result
|
|
24
|
+
? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) }
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
function interpolateColor(c1, c2, factor) {
|
|
28
|
+
const r = Math.round(c1.r + (c2.r - c1.r) * factor);
|
|
29
|
+
const g = Math.round(c1.g + (c2.g - c1.g) * factor);
|
|
30
|
+
const b = Math.round(c1.b + (c2.b - c1.b) * factor);
|
|
31
|
+
return `rgb(${r},${g},${b})`;
|
|
32
|
+
}
|
|
33
|
+
function getGradientColor(row, totalRows) {
|
|
34
|
+
const rgb = hexToRgb(BRAND_COLOR);
|
|
35
|
+
if (!rgb)
|
|
36
|
+
return BRAND_COLOR;
|
|
37
|
+
const startRgb = rgb;
|
|
38
|
+
const endRgb = { r: Math.round(rgb.r * 0.3), g: Math.round(rgb.g * 0.3), b: Math.round(rgb.b * 0.3) };
|
|
39
|
+
const factor = row / Math.max(totalRows - 1, 1);
|
|
40
|
+
return interpolateColor(startRgb, endRgb, factor);
|
|
41
|
+
}
|
|
42
|
+
export function SplashScreen({ onComplete }) {
|
|
43
|
+
const { stdout } = useStdout();
|
|
44
|
+
const terminalHeight = stdout?.rows || 24;
|
|
45
|
+
const terminalWidth = stdout?.columns || 80;
|
|
46
|
+
const [phase, setPhase] = useState('build');
|
|
47
|
+
const [visibleCount, setVisibleCount] = useState(0);
|
|
48
|
+
const [waveDistance, setWaveDistance] = useState(0);
|
|
49
|
+
const waveOriginRow = HEADER_PADDING_TOP + TUMBLEWEED_TOP_PADDING + 2;
|
|
50
|
+
const waveOriginCol = HEADER_PADDING_LEFT + 5;
|
|
51
|
+
// Generate title dots from figlet
|
|
52
|
+
const allDots = useMemo(() => {
|
|
53
|
+
const dots = [];
|
|
54
|
+
try {
|
|
55
|
+
const ascii = figlet.textSync('YEEHAW', { font: 'ANSI Shadow' });
|
|
56
|
+
const lines = ascii.split('\n').filter(line => line.trim() !== '');
|
|
57
|
+
const totalRows = lines.length;
|
|
58
|
+
lines.forEach((line, row) => {
|
|
59
|
+
for (let col = 0; col < line.length; col++) {
|
|
60
|
+
const char = line[col];
|
|
61
|
+
if (char !== ' ') {
|
|
62
|
+
const screenRow = HEADER_PADDING_TOP + row;
|
|
63
|
+
const screenCol = TITLE_OFFSET_LEFT + col;
|
|
64
|
+
const distance = Math.sqrt(Math.pow(screenRow - waveOriginRow, 2) +
|
|
65
|
+
Math.pow((screenCol - waveOriginCol) * 0.5, 2));
|
|
66
|
+
dots.push({ row: screenRow, col: screenCol, distance, gradientRow: row, totalRows });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Fallback if figlet fails
|
|
73
|
+
}
|
|
74
|
+
return dots;
|
|
75
|
+
}, [waveOriginRow, waveOriginCol]);
|
|
76
|
+
const maxDistance = useMemo(() => {
|
|
77
|
+
if (allDots.length === 0)
|
|
78
|
+
return 100;
|
|
79
|
+
return Math.max(...allDots.map((d) => d.distance)) + 5;
|
|
80
|
+
}, [allDots]);
|
|
81
|
+
// Flatten tumbleweed into array of { char, row, col } for animation
|
|
82
|
+
const allChars = useMemo(() => {
|
|
83
|
+
const chars = [];
|
|
84
|
+
TUMBLEWEED.forEach((line, row) => {
|
|
85
|
+
for (let col = 0; col < line.length; col++) {
|
|
86
|
+
const char = line[col];
|
|
87
|
+
if (char !== ' ') {
|
|
88
|
+
chars.push({ char, row, col });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return chars;
|
|
93
|
+
}, []);
|
|
94
|
+
// Shuffle the characters for random build order
|
|
95
|
+
const shuffledChars = useMemo(() => [...allChars].sort(() => Math.random() - 0.5), [allChars]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (phase === 'build') {
|
|
98
|
+
if (visibleCount < shuffledChars.length) {
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
setVisibleCount((c) => Math.min(c + 2, shuffledChars.length));
|
|
101
|
+
}, 30);
|
|
102
|
+
return () => clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Tumbleweed complete, start pulse
|
|
106
|
+
const timer = setTimeout(() => setPhase('pulse'), 100);
|
|
107
|
+
return () => clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (phase === 'pulse') {
|
|
111
|
+
if (waveDistance < maxDistance) {
|
|
112
|
+
// Advance the wave
|
|
113
|
+
const timer = setTimeout(() => {
|
|
114
|
+
setWaveDistance((d) => d + 3);
|
|
115
|
+
}, 20);
|
|
116
|
+
return () => clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Wave complete
|
|
120
|
+
const timer = setTimeout(onComplete, 200);
|
|
121
|
+
return () => clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, [phase, visibleCount, waveDistance, shuffledChars.length, maxDistance, onComplete]);
|
|
125
|
+
// Build the current visible state of the tumbleweed
|
|
126
|
+
const visibleSet = new Set(shuffledChars.slice(0, visibleCount).map((c) => `${c.row},${c.col}`));
|
|
127
|
+
const renderedTumbleweed = TUMBLEWEED.map((line, row) => {
|
|
128
|
+
let result = '';
|
|
129
|
+
for (let col = 0; col < line.length; col++) {
|
|
130
|
+
const char = line[col];
|
|
131
|
+
if (char === ' ' || visibleSet.has(`${row},${col}`)) {
|
|
132
|
+
result += char;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
result += ' ';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
});
|
|
140
|
+
const topPadding = HEADER_PADDING_TOP + TUMBLEWEED_TOP_PADDING;
|
|
141
|
+
// Render the wave revealing dots
|
|
142
|
+
const renderWave = () => {
|
|
143
|
+
const lines = [];
|
|
144
|
+
const waveWidth = 8;
|
|
145
|
+
// Group dots by row
|
|
146
|
+
const dotsByRow = new Map();
|
|
147
|
+
allDots.forEach((dot) => {
|
|
148
|
+
if (!dotsByRow.has(dot.row)) {
|
|
149
|
+
dotsByRow.set(dot.row, []);
|
|
150
|
+
}
|
|
151
|
+
dotsByRow.get(dot.row).push(dot);
|
|
152
|
+
});
|
|
153
|
+
for (let row = 0; row < terminalHeight; row++) {
|
|
154
|
+
const rowDots = dotsByRow.get(row) || [];
|
|
155
|
+
if (rowDots.length === 0) {
|
|
156
|
+
lines.push(_jsx(Text, { children: ' '.repeat(terminalWidth) }, row));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
rowDots.sort((a, b) => a.col - b.col);
|
|
160
|
+
const segments = [];
|
|
161
|
+
let lastCol = 0;
|
|
162
|
+
for (const dot of rowDots) {
|
|
163
|
+
if (dot.distance > waveDistance)
|
|
164
|
+
continue;
|
|
165
|
+
if (dot.col > lastCol) {
|
|
166
|
+
segments.push(_jsx(Text, { children: ' '.repeat(dot.col - lastCol) }, `space-${lastCol}`));
|
|
167
|
+
}
|
|
168
|
+
const atWaveFront = dot.distance >= waveDistance - waveWidth;
|
|
169
|
+
const color = getGradientColor(dot.gradientRow, dot.totalRows);
|
|
170
|
+
segments.push(_jsx(Text, { color: color, bold: atWaveFront, children: "\u00B7" }, `dot-${dot.col}`));
|
|
171
|
+
lastCol = dot.col + 1;
|
|
172
|
+
}
|
|
173
|
+
lines.push(_jsx(Box, { children: segments }, row));
|
|
174
|
+
}
|
|
175
|
+
return lines;
|
|
176
|
+
};
|
|
177
|
+
return (_jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [phase === 'pulse' && (_jsx(Box, { position: "absolute", flexDirection: "column", children: renderWave() })), _jsx(Box, { flexDirection: "column", paddingTop: topPadding, paddingLeft: HEADER_PADDING_LEFT, children: renderedTumbleweed.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, bold: true, children: line }, i))) })] }));
|
|
178
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { render } from 'ink';
|
|
4
|
-
import { execaSync } from 'execa';
|
|
5
4
|
import { App } from './app.js';
|
|
6
5
|
import { isInsideYeehawSession, yeehawSessionExists, createYeehawSession, attachToYeehaw, hasTmux, } from './lib/tmux.js';
|
|
7
6
|
import { ensureConfigDirs } from './lib/config.js';
|
|
@@ -33,11 +32,9 @@ function main() {
|
|
|
33
32
|
}
|
|
34
33
|
// We're not inside yeehaw session - need to create/attach
|
|
35
34
|
if (!yeehawSessionExists()) {
|
|
36
|
-
// Create new session with yeehaw running in window 0
|
|
35
|
+
// Create new session with yeehaw running directly in window 0
|
|
36
|
+
// (no shell intermediary - cleaner startup)
|
|
37
37
|
createYeehawSession();
|
|
38
|
-
// Now we need to run yeehaw inside window 0
|
|
39
|
-
// Send the command to the window
|
|
40
|
-
execaSync('tmux', ['send-keys', '-t', 'yeehaw:0', 'yeehaw', 'Enter']);
|
|
41
38
|
}
|
|
42
39
|
// Attach to the yeehaw session
|
|
43
40
|
// This will exec into tmux and not return
|
package/dist/lib/tmux.js
CHANGED
|
@@ -8,6 +8,8 @@ import { shellEscape } from './shell.js';
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = dirname(__filename);
|
|
10
10
|
const MCP_SERVER_PATH = join(__dirname, '..', 'mcp-server.js');
|
|
11
|
+
// Get the path to the bundled Claude plugin (at package root, sibling to dist/)
|
|
12
|
+
const CLAUDE_PLUGIN_PATH = join(__dirname, '..', '..', 'claude-plugin');
|
|
11
13
|
export const YEEHAW_SESSION = 'yeehaw';
|
|
12
14
|
// Remote mode state tracking
|
|
13
15
|
let remoteWindowIndex = null;
|
|
@@ -47,12 +49,14 @@ export function yeehawSessionExists() {
|
|
|
47
49
|
export function createYeehawSession() {
|
|
48
50
|
// Write the tmux config
|
|
49
51
|
writeTmuxConfig();
|
|
50
|
-
// Create the session with window 0 named "yeehaw"
|
|
52
|
+
// Create the session with window 0 named "yeehaw", running yeehaw directly
|
|
53
|
+
// This avoids the visible shell spawn - yeehaw runs immediately in the session
|
|
51
54
|
execaSync('tmux', [
|
|
52
55
|
'new-session',
|
|
53
56
|
'-d',
|
|
54
57
|
'-s', YEEHAW_SESSION,
|
|
55
58
|
'-n', 'yeehaw',
|
|
59
|
+
'yeehaw', // Run yeehaw directly instead of spawning a shell first
|
|
56
60
|
]);
|
|
57
61
|
// Source the config
|
|
58
62
|
execaSync('tmux', ['source-file', TMUX_CONFIG_PATH]);
|
|
@@ -157,7 +161,8 @@ export function createClaudeWindow(workingDir, windowName) {
|
|
|
157
161
|
const allowedTools = YEEHAW_MCP_TOOLS.join(',');
|
|
158
162
|
// Create new window running claude with yeehaw MCP server (-a appends after current window)
|
|
159
163
|
// Use shell escaping to safely handle special characters in JSON
|
|
160
|
-
|
|
164
|
+
// Include the bundled plugin directory for Yeehaw-specific skills
|
|
165
|
+
const claudeCmd = `claude --mcp-config ${shellEscape(mcpConfig)} --allowedTools ${shellEscape(allowedTools)} --plugin-dir ${shellEscape(CLAUDE_PLUGIN_PATH)}`;
|
|
161
166
|
execaSync('tmux', [
|
|
162
167
|
'new-window',
|
|
163
168
|
'-a',
|
package/dist/types.d.ts
CHANGED
|
@@ -190,7 +190,7 @@ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelec
|
|
|
190
190
|
if (parts.length >= 2) {
|
|
191
191
|
const projectName = parts.slice(0, -1).join('-');
|
|
192
192
|
const livestockName = parts[parts.length - 1];
|
|
193
|
-
return { label: `${projectName}
|
|
193
|
+
return { label: `${projectName} · ${livestockName}`, typeHint: 'shell' };
|
|
194
194
|
}
|
|
195
195
|
return { label: name, typeHint: '' };
|
|
196
196
|
};
|
|
@@ -259,22 +259,22 @@ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelec
|
|
|
259
259
|
const projectHints = '[n] new';
|
|
260
260
|
const sessionHints = '1-9 switch';
|
|
261
261
|
const barnHints = '[n] new [s] shell';
|
|
262
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Projects", focused: focusedPanel === 'projects',
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsxs(Box, { flexDirection: "column", width: "40%", gap: 1, children: [_jsx(Panel, { title: "Projects", focused: focusedPanel === 'projects', hints: projectHints, children: projectItems.length > 0 ? (_jsx(List, { items: projectItems, focused: focusedPanel === 'projects', onSelect: (item) => {
|
|
263
|
+
const project = projects.find((p) => p.name === item.id);
|
|
264
|
+
if (project)
|
|
265
|
+
onSelectProject(project);
|
|
266
|
+
} })) : (_jsx(Text, { dimColor: true, children: "No projects yet" })) }), _jsx(Box, { flexGrow: 1, width: "100%", children: _jsx(Panel, { title: "Barns", focused: focusedPanel === 'barns', width: "100%", hints: barnHints, children: barnItems.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsx(List, { items: barnItems, focused: focusedPanel === 'barns', onSelect: (item) => {
|
|
267
|
+
const barn = barns.find((b) => b.name === item.id);
|
|
268
|
+
if (barn)
|
|
269
|
+
onSelectBarn(barn);
|
|
270
|
+
}, onAction: (item) => {
|
|
271
|
+
// 's' key to SSH directly
|
|
272
|
+
const barn = barns.find((b) => b.name === item.id);
|
|
273
|
+
if (barn)
|
|
274
|
+
onSshToBarn(barn);
|
|
275
|
+
} }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No barns configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Barns are servers you manage" })] })) }) })] }), _jsx(Panel, { title: "Sessions", focused: focusedPanel === 'sessions', width: "60%", hints: sessionHints, children: sessionItems.length > 0 ? (_jsx(List, { items: sessionItems, focused: focusedPanel === 'sessions', onSelect: (item) => {
|
|
267
276
|
const window = sessionWindows.find((w) => String(w.index) === item.id);
|
|
268
277
|
if (window)
|
|
269
278
|
onSelectWindow(window);
|
|
270
|
-
} })) : (_jsx(Text, { dimColor: true, children: "No active sessions" })) })] })
|
|
271
|
-
const barn = barns.find((b) => b.name === item.id);
|
|
272
|
-
if (barn)
|
|
273
|
-
onSelectBarn(barn);
|
|
274
|
-
}, onAction: (item) => {
|
|
275
|
-
// 's' key to SSH directly
|
|
276
|
-
const barn = barns.find((b) => b.name === item.id);
|
|
277
|
-
if (barn)
|
|
278
|
-
onSshToBarn(barn);
|
|
279
|
-
} }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No barns configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Barns are servers you manage" })] })) }) })] }));
|
|
279
|
+
} })) : (_jsx(Text, { dimColor: true, children: "No active sessions" })) })] })] }));
|
|
280
280
|
}
|
|
@@ -10,7 +10,7 @@ interface LivestockDetailViewProps {
|
|
|
10
10
|
onOpenLogs: () => void;
|
|
11
11
|
onOpenSession: () => void;
|
|
12
12
|
onSelectWindow: (window: TmuxWindow) => void;
|
|
13
|
-
onUpdateLivestock: (
|
|
13
|
+
onUpdateLivestock: (originalLivestock: Livestock, updatedLivestock: Livestock) => void;
|
|
14
14
|
}
|
|
15
15
|
export declare function LivestockDetailView({ project, livestock, source, sourceBarn, windows, onBack, onOpenLogs, onOpenSession, onSelectWindow, onUpdateLivestock, }: LivestockDetailViewProps): import("react/jsx-runtime").JSX.Element;
|
|
16
16
|
export {};
|
|
@@ -37,29 +37,18 @@ export function LivestockDetailView({ project, livestock, source, sourceBarn, wi
|
|
|
37
37
|
setEditLogPath(livestock.log_path || '');
|
|
38
38
|
setEditEnvPath(livestock.env_path || '');
|
|
39
39
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
case 'branch':
|
|
53
|
-
updated.branch = value || undefined;
|
|
54
|
-
break;
|
|
55
|
-
case 'log_path':
|
|
56
|
-
updated.log_path = value || undefined;
|
|
57
|
-
break;
|
|
58
|
-
case 'env_path':
|
|
59
|
-
updated.env_path = value || undefined;
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
onUpdateLivestock(updated);
|
|
40
|
+
// Save all pending changes at once
|
|
41
|
+
const saveAllChanges = () => {
|
|
42
|
+
const updated = {
|
|
43
|
+
...livestock,
|
|
44
|
+
name: editName.trim() || livestock.name,
|
|
45
|
+
path: editPath.trim() || livestock.path,
|
|
46
|
+
repo: editRepo.trim() || undefined,
|
|
47
|
+
branch: editBranch.trim() || undefined,
|
|
48
|
+
log_path: editLogPath.trim() || undefined,
|
|
49
|
+
env_path: editEnvPath.trim() || undefined,
|
|
50
|
+
};
|
|
51
|
+
onUpdateLivestock(livestock, updated);
|
|
63
52
|
setMode('normal');
|
|
64
53
|
};
|
|
65
54
|
useInput((input, key) => {
|
|
@@ -74,6 +63,14 @@ export function LivestockDetailView({ project, livestock, source, sourceBarn, wi
|
|
|
74
63
|
}
|
|
75
64
|
return;
|
|
76
65
|
}
|
|
66
|
+
// Handle Ctrl+S to save and exit from any edit mode
|
|
67
|
+
// Note: Ctrl+S sends ASCII 19 (\x13), not 's'
|
|
68
|
+
if ((key.ctrl && input === 's') || input === '\x13') {
|
|
69
|
+
if (mode !== 'normal') {
|
|
70
|
+
saveAllChanges();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
77
74
|
// Only process these in normal mode
|
|
78
75
|
if (mode !== 'normal')
|
|
79
76
|
return;
|
|
@@ -99,48 +96,33 @@ export function LivestockDetailView({ project, livestock, source, sourceBarn, wi
|
|
|
99
96
|
if (mode === 'edit-name') {
|
|
100
97
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: editName, onChange: setEditName, onSubmit: () => {
|
|
101
98
|
if (editName.trim()) {
|
|
102
|
-
saveEdit('name', editName.trim());
|
|
103
99
|
setMode('edit-path');
|
|
104
100
|
}
|
|
105
|
-
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
|
|
101
|
+
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
106
102
|
}
|
|
107
103
|
// Edit path
|
|
108
104
|
if (mode === 'edit-path') {
|
|
109
105
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: editPath, onChange: setEditPath, onSubmit: () => {
|
|
110
106
|
if (editPath.trim()) {
|
|
111
|
-
saveEdit('path', editPath.trim());
|
|
112
107
|
setMode('edit-repo');
|
|
113
108
|
}
|
|
114
|
-
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next
|
|
109
|
+
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Tab: autocomplete, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
115
110
|
}
|
|
116
111
|
// Edit repo
|
|
117
112
|
if (mode === 'edit-repo') {
|
|
118
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Repo (optional): " }), _jsx(TextInput, { value: editRepo, onChange: setEditRepo, onSubmit: () => {
|
|
119
|
-
saveEdit('repo', editRepo.trim());
|
|
120
|
-
setMode('edit-branch');
|
|
121
|
-
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Repo (optional): " }), _jsx(TextInput, { value: editRepo, onChange: setEditRepo, onSubmit: () => setMode('edit-branch') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
122
114
|
}
|
|
123
115
|
// Edit branch
|
|
124
116
|
if (mode === 'edit-branch') {
|
|
125
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Branch (optional): " }), _jsx(TextInput, { value: editBranch, onChange: setEditBranch, onSubmit: () => {
|
|
126
|
-
saveEdit('branch', editBranch.trim());
|
|
127
|
-
setMode('edit-log-path');
|
|
128
|
-
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
|
|
117
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Branch (optional): " }), _jsx(TextInput, { value: editBranch, onChange: setEditBranch, onSubmit: () => setMode('edit-log-path') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
129
118
|
}
|
|
130
119
|
// Edit log path
|
|
131
120
|
if (mode === 'edit-log-path') {
|
|
132
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Log Path (optional, relative): " }), _jsx(TextInput, { value: editLogPath, onChange: setEditLogPath, onSubmit: () => {
|
|
133
|
-
saveEdit('log_path', editLogPath.trim());
|
|
134
|
-
setMode('edit-env-path');
|
|
135
|
-
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
|
|
121
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Log Path (optional, relative): " }), _jsx(TextInput, { value: editLogPath, onChange: setEditLogPath, onSubmit: () => setMode('edit-env-path') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
136
122
|
}
|
|
137
|
-
// Edit env path
|
|
123
|
+
// Edit env path (last field - saves all changes)
|
|
138
124
|
if (mode === 'edit-env-path') {
|
|
139
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Env Path (optional, relative): " }), _jsx(TextInput, { value: editEnvPath, onChange: setEditEnvPath, onSubmit: ()
|
|
140
|
-
saveEdit('env_path', editEnvPath.trim());
|
|
141
|
-
// All done - return to normal mode
|
|
142
|
-
setMode('normal');
|
|
143
|
-
} })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save & finish, Esc: cancel" }) })] })] }));
|
|
125
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Env Path (optional, relative): " }), _jsx(TextInput, { value: editEnvPath, onChange: setEditEnvPath, onSubmit: saveAllChanges })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save & finish, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
|
|
144
126
|
}
|
|
145
127
|
// Filter windows to this livestock (match pattern: projectname-livestockname)
|
|
146
128
|
const livestockWindowName = `${project.name}-${livestock.name}`;
|