@colmbus72/yeehaw 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.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 +54 -11
- package/dist/components/Header.d.ts +4 -1
- package/dist/components/Header.js +48 -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/mcp-server.js +57 -4
- package/dist/types.d.ts +2 -0
- package/dist/views/GlobalDashboard.d.ts +2 -1
- package/dist/views/GlobalDashboard.js +23 -18
- package/dist/views/LivestockDetailView.d.ts +1 -1
- package/dist/views/LivestockDetailView.js +27 -45
- package/dist/views/NightSkyView.js +66 -63
- package/dist/views/ProjectContext.js +138 -18
- package/dist/views/WikiView.js +2 -9
- package/package.json +2 -1
|
@@ -3,10 +3,13 @@ interface HeaderProps {
|
|
|
3
3
|
subtitle?: string;
|
|
4
4
|
summary?: string;
|
|
5
5
|
color?: string;
|
|
6
|
+
gradientSpread?: number;
|
|
7
|
+
gradientInverted?: boolean;
|
|
8
|
+
theme?: 'dark' | 'light';
|
|
6
9
|
versionInfo?: {
|
|
7
10
|
current: string;
|
|
8
11
|
latest: string | null;
|
|
9
12
|
};
|
|
10
13
|
}
|
|
11
|
-
export declare function Header({ text, subtitle, summary, color, versionInfo }: HeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare function Header({ text, subtitle, summary, color, gradientSpread, gradientInverted, theme, versionInfo }: HeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
12
15
|
export {};
|
|
@@ -1,7 +1,22 @@
|
|
|
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
|
+
// Compare semver versions - returns true if latest > current
|
|
6
|
+
function isNewerVersion(latest, current) {
|
|
7
|
+
const parseVersion = (v) => v.split('.').map(n => parseInt(n, 10) || 0);
|
|
8
|
+
const [lMajor, lMinor, lPatch] = parseVersion(latest);
|
|
9
|
+
const [cMajor, cMinor, cPatch] = parseVersion(current);
|
|
10
|
+
if (lMajor > cMajor)
|
|
11
|
+
return true;
|
|
12
|
+
if (lMajor < cMajor)
|
|
13
|
+
return false;
|
|
14
|
+
if (lMinor > cMinor)
|
|
15
|
+
return true;
|
|
16
|
+
if (lMinor < cMinor)
|
|
17
|
+
return false;
|
|
18
|
+
return lPatch > cPatch;
|
|
19
|
+
}
|
|
5
20
|
// Convert hex to RGB
|
|
6
21
|
function hexToRgb(hex) {
|
|
7
22
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
@@ -21,34 +36,43 @@ function interpolateColor(color1, color2, factor) {
|
|
|
21
36
|
return `rgb(${r},${g},${b})`;
|
|
22
37
|
}
|
|
23
38
|
// Generate gradient colors for each line
|
|
24
|
-
function generateGradient(lines, baseColor) {
|
|
39
|
+
function generateGradient(lines, baseColor, spread = 5, inverted = false, theme = 'dark') {
|
|
25
40
|
const rgb = hexToRgb(baseColor);
|
|
26
41
|
if (!rgb)
|
|
27
42
|
return lines.map(() => baseColor);
|
|
43
|
+
// Spread controls how much the gradient changes (0 = no change, 10 = max change)
|
|
44
|
+
// Convert 0-10 scale to a multiplier (0 = 1.0, 10 = 0.1 for darkening factor)
|
|
45
|
+
const spreadFactor = 1 - (spread / 10) * 0.9; // 0->1.0, 5->0.55, 10->0.1
|
|
28
46
|
// Calculate luminance to detect dark colors
|
|
29
47
|
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
30
48
|
let startRgb;
|
|
31
49
|
let endRgb;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
// Determine gradient direction based on color luminance and theme
|
|
51
|
+
const isDarkColor = luminance < 0.3;
|
|
52
|
+
const shouldLighten = isDarkColor || theme === 'light';
|
|
53
|
+
if (shouldLighten) {
|
|
54
|
+
// Go from a lighter tint down to the base color (or adjusted end)
|
|
55
|
+
const liftFactor = 1 + (spread / 10) * 2; // More spread = more lift
|
|
36
56
|
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 +
|
|
57
|
+
r: Math.min(255, Math.round(rgb.r * liftFactor + spread * 8)),
|
|
58
|
+
g: Math.min(255, Math.round(rgb.g * liftFactor + spread * 8)),
|
|
59
|
+
b: Math.min(255, Math.round(rgb.b * liftFactor + spread * 8)),
|
|
40
60
|
};
|
|
41
61
|
endRgb = rgb;
|
|
42
62
|
}
|
|
43
63
|
else {
|
|
44
|
-
//
|
|
64
|
+
// Go from base to a darker version
|
|
45
65
|
startRgb = rgb;
|
|
46
66
|
endRgb = {
|
|
47
|
-
r: Math.round(rgb.r *
|
|
48
|
-
g: Math.round(rgb.g *
|
|
49
|
-
b: Math.round(rgb.b *
|
|
67
|
+
r: Math.round(rgb.r * spreadFactor),
|
|
68
|
+
g: Math.round(rgb.g * spreadFactor),
|
|
69
|
+
b: Math.round(rgb.b * spreadFactor),
|
|
50
70
|
};
|
|
51
71
|
}
|
|
72
|
+
// Invert if requested
|
|
73
|
+
if (inverted) {
|
|
74
|
+
[startRgb, endRgb] = [endRgb, startRgb];
|
|
75
|
+
}
|
|
52
76
|
return lines.map((_, i) => {
|
|
53
77
|
const factor = i / Math.max(lines.length - 1, 1);
|
|
54
78
|
return interpolateColor(startRgb, endRgb, factor);
|
|
@@ -63,21 +87,22 @@ const TUMBLEWEED = [
|
|
|
63
87
|
];
|
|
64
88
|
// Brownish tan color to complement yeehaw gold
|
|
65
89
|
const TUMBLEWEED_COLOR = '#b8860b';
|
|
66
|
-
export function Header({ text, subtitle, summary, color, versionInfo }) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
export function Header({ text, subtitle, summary, color, gradientSpread, gradientInverted, theme, versionInfo }) {
|
|
91
|
+
// Use sync figlet to avoid flash on initial render
|
|
92
|
+
const ascii = useMemo(() => {
|
|
93
|
+
try {
|
|
94
|
+
return figlet.textSync(text.toUpperCase(), { font: 'ANSI Shadow' });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return text.toUpperCase();
|
|
98
|
+
}
|
|
74
99
|
}, [text]);
|
|
75
100
|
const lines = ascii.split('\n').filter(line => line.trim() !== '');
|
|
76
101
|
const baseColor = color || '#f0c040'; // Default yeehaw gold
|
|
77
|
-
const gradientColors = generateGradient(lines, baseColor);
|
|
102
|
+
const gradientColors = generateGradient(lines, baseColor, gradientSpread ?? 5, gradientInverted ?? false, theme ?? 'dark');
|
|
78
103
|
// Show tumbleweed only for the main "yeehaw" title
|
|
79
104
|
const showTumbleweed = text.toLowerCase() === 'yeehaw';
|
|
80
105
|
// Vertically center tumbleweed next to ASCII art
|
|
81
106
|
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
|
|
107
|
+
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 && isNewerVersion(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] })] })) : (_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
108
|
}
|
|
@@ -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/mcp-server.js
CHANGED
|
@@ -321,7 +321,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
321
321
|
// Wiki tools
|
|
322
322
|
{
|
|
323
323
|
name: 'get_wiki',
|
|
324
|
-
description: 'Get all wiki
|
|
324
|
+
description: 'Get all wiki section titles for a project (use get_wiki_section to fetch content)',
|
|
325
325
|
inputSchema: {
|
|
326
326
|
type: 'object',
|
|
327
327
|
properties: {
|
|
@@ -330,6 +330,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
330
330
|
required: ['project'],
|
|
331
331
|
},
|
|
332
332
|
},
|
|
333
|
+
{
|
|
334
|
+
name: 'get_wiki_section',
|
|
335
|
+
description: 'Get the content of a specific wiki section',
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: 'object',
|
|
338
|
+
properties: {
|
|
339
|
+
project: { type: 'string', description: 'Project name' },
|
|
340
|
+
title: { type: 'string', description: 'Section title' },
|
|
341
|
+
},
|
|
342
|
+
required: ['project', 'title'],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
333
345
|
{
|
|
334
346
|
name: 'add_wiki_section',
|
|
335
347
|
description: 'Add a new wiki section to a project',
|
|
@@ -379,11 +391,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
379
391
|
// Project operations
|
|
380
392
|
case 'list_projects': {
|
|
381
393
|
const projects = loadProjects();
|
|
394
|
+
// Return simplified project data to reduce context usage
|
|
395
|
+
const simplified = projects.map((p) => ({
|
|
396
|
+
name: p.name,
|
|
397
|
+
path: p.path,
|
|
398
|
+
summary: p.summary,
|
|
399
|
+
color: p.color,
|
|
400
|
+
livestock: (p.livestock || []).map((l) => ({
|
|
401
|
+
name: l.name,
|
|
402
|
+
path: l.path,
|
|
403
|
+
barn: l.barn,
|
|
404
|
+
})),
|
|
405
|
+
}));
|
|
382
406
|
return {
|
|
383
407
|
content: [
|
|
384
408
|
{
|
|
385
409
|
type: 'text',
|
|
386
|
-
text: JSON.stringify(
|
|
410
|
+
text: JSON.stringify(simplified, null, 2),
|
|
387
411
|
},
|
|
388
412
|
],
|
|
389
413
|
};
|
|
@@ -397,8 +421,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
397
421
|
isError: true,
|
|
398
422
|
};
|
|
399
423
|
}
|
|
424
|
+
// Return project with wiki section titles only (not full content)
|
|
425
|
+
// Use get_wiki_section to fetch individual section content
|
|
426
|
+
const projectWithWikiTitles = {
|
|
427
|
+
...project,
|
|
428
|
+
wiki: (project.wiki || []).map((s) => ({ title: s.title })),
|
|
429
|
+
};
|
|
400
430
|
return {
|
|
401
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
431
|
+
content: [{ type: 'text', text: JSON.stringify(projectWithWikiTitles, null, 2) }],
|
|
402
432
|
};
|
|
403
433
|
}
|
|
404
434
|
case 'create_project': {
|
|
@@ -712,8 +742,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
712
742
|
isError: true,
|
|
713
743
|
};
|
|
714
744
|
}
|
|
745
|
+
// Return only section titles to reduce context usage
|
|
746
|
+
const titles = (project.wiki || []).map((s) => ({ title: s.title }));
|
|
747
|
+
return {
|
|
748
|
+
content: [{ type: 'text', text: JSON.stringify(titles, null, 2) }],
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
case 'get_wiki_section': {
|
|
752
|
+
const projectName = requireString(args, 'project');
|
|
753
|
+
const title = requireString(args, 'title');
|
|
754
|
+
const project = loadProject(projectName);
|
|
755
|
+
if (!project) {
|
|
756
|
+
return {
|
|
757
|
+
content: [{ type: 'text', text: `Project not found: ${projectName}` }],
|
|
758
|
+
isError: true,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
const section = (project.wiki || []).find((s) => s.title === title);
|
|
762
|
+
if (!section) {
|
|
763
|
+
return {
|
|
764
|
+
content: [{ type: 'text', text: `Wiki section not found: ${title}` }],
|
|
765
|
+
isError: true,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
715
768
|
return {
|
|
716
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
769
|
+
content: [{ type: 'text', text: JSON.stringify(section, null, 2) }],
|
|
717
770
|
};
|
|
718
771
|
}
|
|
719
772
|
case 'add_wiki_section': {
|
package/dist/types.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface GlobalDashboardProps {
|
|
|
15
15
|
onCreateProject: (name: string, path: string) => void;
|
|
16
16
|
onCreateBarn: (barn: Barn) => void;
|
|
17
17
|
onSshToBarn: (barn: Barn) => void;
|
|
18
|
+
onInputModeChange?: (isInputMode: boolean) => void;
|
|
18
19
|
}
|
|
19
|
-
export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
|
|
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
21
|
export {};
|
|
@@ -12,9 +12,14 @@ import { isLocalBarn } from '../lib/config.js';
|
|
|
12
12
|
function countSessionsForProject(projectName, windows) {
|
|
13
13
|
return windows.filter((w) => w.name.startsWith(projectName)).length;
|
|
14
14
|
}
|
|
15
|
-
export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }) {
|
|
15
|
+
export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, onInputModeChange, }) {
|
|
16
16
|
const [focusedPanel, setFocusedPanel] = useState('projects');
|
|
17
|
-
const [mode,
|
|
17
|
+
const [mode, setModeInternal] = useState('normal');
|
|
18
|
+
// Wrapper to notify parent when input mode changes
|
|
19
|
+
const setMode = (newMode) => {
|
|
20
|
+
setModeInternal(newMode);
|
|
21
|
+
onInputModeChange?.(newMode !== 'normal');
|
|
22
|
+
};
|
|
18
23
|
// New project form state
|
|
19
24
|
const [newProjectName, setNewProjectName] = useState('');
|
|
20
25
|
const [newProjectPath, setNewProjectPath] = useState('');
|
|
@@ -190,7 +195,7 @@ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelec
|
|
|
190
195
|
if (parts.length >= 2) {
|
|
191
196
|
const projectName = parts.slice(0, -1).join('-');
|
|
192
197
|
const livestockName = parts[parts.length - 1];
|
|
193
|
-
return { label: `${projectName}
|
|
198
|
+
return { label: `${projectName} · ${livestockName}`, typeHint: 'shell' };
|
|
194
199
|
}
|
|
195
200
|
return { label: name, typeHint: '' };
|
|
196
201
|
};
|
|
@@ -259,22 +264,22 @@ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelec
|
|
|
259
264
|
const projectHints = '[n] new';
|
|
260
265
|
const sessionHints = '1-9 switch';
|
|
261
266
|
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
|
-
|
|
267
|
+
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) => {
|
|
268
|
+
const project = projects.find((p) => p.name === item.id);
|
|
269
|
+
if (project)
|
|
270
|
+
onSelectProject(project);
|
|
271
|
+
} })) : (_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) => {
|
|
272
|
+
const barn = barns.find((b) => b.name === item.id);
|
|
273
|
+
if (barn)
|
|
274
|
+
onSelectBarn(barn);
|
|
275
|
+
}, onAction: (item) => {
|
|
276
|
+
// 's' key to SSH directly
|
|
277
|
+
const barn = barns.find((b) => b.name === item.id);
|
|
278
|
+
if (barn)
|
|
279
|
+
onSshToBarn(barn);
|
|
280
|
+
} }) })) : (_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
281
|
const window = sessionWindows.find((w) => String(w.index) === item.id);
|
|
268
282
|
if (window)
|
|
269
283
|
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" })] })) }) })] }));
|
|
284
|
+
} })) : (_jsx(Text, { dimColor: true, children: "No active sessions" })) })] })] }));
|
|
280
285
|
}
|
|
@@ -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 {};
|