@colmbus72/yeehaw 0.1.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/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +414 -0
- package/dist/components/BarnHeader.d.ts +6 -0
- package/dist/components/BarnHeader.js +21 -0
- package/dist/components/BottomBar.d.ts +16 -0
- package/dist/components/BottomBar.js +7 -0
- package/dist/components/Header.d.ts +8 -0
- package/dist/components/Header.js +83 -0
- package/dist/components/HelpOverlay.d.ts +7 -0
- package/dist/components/HelpOverlay.js +17 -0
- package/dist/components/List.d.ts +17 -0
- package/dist/components/List.js +53 -0
- package/dist/components/Markdown.d.ts +8 -0
- package/dist/components/Markdown.js +23 -0
- package/dist/components/Panel.d.ts +10 -0
- package/dist/components/Panel.js +5 -0
- package/dist/components/PathInput.d.ts +9 -0
- package/dist/components/PathInput.js +141 -0
- package/dist/components/ScrollableMarkdown.d.ts +11 -0
- package/dist/components/ScrollableMarkdown.js +56 -0
- package/dist/components/StatusBar.d.ts +5 -0
- package/dist/components/StatusBar.js +20 -0
- package/dist/components/TextArea.d.ts +17 -0
- package/dist/components/TextArea.js +140 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +5 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/useConfig.d.ts +11 -0
- package/dist/hooks/useConfig.js +36 -0
- package/dist/hooks/useRemoteYeehaw.d.ts +13 -0
- package/dist/hooks/useRemoteYeehaw.js +49 -0
- package/dist/hooks/useSessions.d.ts +11 -0
- package/dist/hooks/useSessions.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +34 -0
- package/dist/lib/config.d.ts +27 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/detection.d.ts +16 -0
- package/dist/lib/detection.js +41 -0
- package/dist/lib/editor.d.ts +5 -0
- package/dist/lib/editor.js +35 -0
- package/dist/lib/errors.d.ts +28 -0
- package/dist/lib/errors.js +48 -0
- package/dist/lib/git.d.ts +11 -0
- package/dist/lib/git.js +73 -0
- package/dist/lib/github.d.ts +43 -0
- package/dist/lib/github.js +111 -0
- package/dist/lib/hotkeys.d.ts +27 -0
- package/dist/lib/hotkeys.js +92 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/livestock.d.ts +51 -0
- package/dist/lib/livestock.js +233 -0
- package/dist/lib/mcp-validation.d.ts +33 -0
- package/dist/lib/mcp-validation.js +62 -0
- package/dist/lib/paths.d.ts +8 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/shell.d.ts +34 -0
- package/dist/lib/shell.js +61 -0
- package/dist/lib/ssh.d.ts +15 -0
- package/dist/lib/ssh.js +77 -0
- package/dist/lib/tmux-config.d.ts +3 -0
- package/dist/lib/tmux-config.js +42 -0
- package/dist/lib/tmux.d.ts +32 -0
- package/dist/lib/tmux.js +397 -0
- package/dist/mcp-server.d.ts +23 -0
- package/dist/mcp-server.js +825 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +2 -0
- package/dist/views/BarnContext.d.ts +22 -0
- package/dist/views/BarnContext.js +252 -0
- package/dist/views/GlobalDashboard.d.ts +16 -0
- package/dist/views/GlobalDashboard.js +253 -0
- package/dist/views/Home.d.ts +11 -0
- package/dist/views/Home.js +27 -0
- package/dist/views/IssuesView.d.ts +7 -0
- package/dist/views/IssuesView.js +157 -0
- package/dist/views/LivestockDetailView.d.ts +11 -0
- package/dist/views/LivestockDetailView.js +140 -0
- package/dist/views/LogsView.d.ts +8 -0
- package/dist/views/LogsView.js +84 -0
- package/dist/views/NightSkyView.d.ts +5 -0
- package/dist/views/NightSkyView.js +441 -0
- package/dist/views/ProjectContext.d.ts +18 -0
- package/dist/views/ProjectContext.js +333 -0
- package/dist/views/Projects.d.ts +8 -0
- package/dist/views/Projects.js +20 -0
- package/dist/views/WikiView.d.ts +8 -0
- package/dist/views/WikiView.js +138 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/package.json +65 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import figlet from 'figlet';
|
|
5
|
+
// Convert hex to RGB
|
|
6
|
+
function hexToRgb(hex) {
|
|
7
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
8
|
+
return result
|
|
9
|
+
? {
|
|
10
|
+
r: parseInt(result[1], 16),
|
|
11
|
+
g: parseInt(result[2], 16),
|
|
12
|
+
b: parseInt(result[3], 16),
|
|
13
|
+
}
|
|
14
|
+
: null;
|
|
15
|
+
}
|
|
16
|
+
// Interpolate between two colors
|
|
17
|
+
function interpolateColor(color1, color2, factor) {
|
|
18
|
+
const r = Math.round(color1.r + (color2.r - color1.r) * factor);
|
|
19
|
+
const g = Math.round(color1.g + (color2.g - color1.g) * factor);
|
|
20
|
+
const b = Math.round(color1.b + (color2.b - color1.b) * factor);
|
|
21
|
+
return `rgb(${r},${g},${b})`;
|
|
22
|
+
}
|
|
23
|
+
// Generate gradient colors for each line
|
|
24
|
+
function generateGradient(lines, baseColor) {
|
|
25
|
+
const rgb = hexToRgb(baseColor);
|
|
26
|
+
if (!rgb)
|
|
27
|
+
return lines.map(() => baseColor);
|
|
28
|
+
// Calculate luminance to detect dark colors
|
|
29
|
+
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
30
|
+
let startRgb;
|
|
31
|
+
let endRgb;
|
|
32
|
+
if (luminance < 0.3) {
|
|
33
|
+
// Dark color: go from a lighter tint down to the base color
|
|
34
|
+
// Lift toward white while preserving hue
|
|
35
|
+
const liftFactor = 2.5;
|
|
36
|
+
startRgb = {
|
|
37
|
+
r: Math.min(255, Math.round(rgb.r * liftFactor + 40)),
|
|
38
|
+
g: Math.min(255, Math.round(rgb.g * liftFactor + 40)),
|
|
39
|
+
b: Math.min(255, Math.round(rgb.b * liftFactor + 40)),
|
|
40
|
+
};
|
|
41
|
+
endRgb = rgb;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Light/medium color: go from base to a darker version
|
|
45
|
+
startRgb = rgb;
|
|
46
|
+
endRgb = {
|
|
47
|
+
r: Math.round(rgb.r * 0.3),
|
|
48
|
+
g: Math.round(rgb.g * 0.3),
|
|
49
|
+
b: Math.round(rgb.b * 0.3),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return lines.map((_, i) => {
|
|
53
|
+
const factor = i / Math.max(lines.length - 1, 1);
|
|
54
|
+
return interpolateColor(startRgb, endRgb, factor);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// Tumbleweed mascot art
|
|
58
|
+
const TUMBLEWEED = [
|
|
59
|
+
' ░ ░▒░ ░▒░',
|
|
60
|
+
'░▒ · ‿ · ▒░',
|
|
61
|
+
'▒░ ▒░▒░ ░▒',
|
|
62
|
+
' ░▒░ ░▒░ ░',
|
|
63
|
+
];
|
|
64
|
+
// Brownish tan color to complement yeehaw gold
|
|
65
|
+
const TUMBLEWEED_COLOR = '#b8860b';
|
|
66
|
+
export function Header({ text, subtitle, summary, color }) {
|
|
67
|
+
const [ascii, setAscii] = useState('');
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
figlet.text(text.toUpperCase(), { font: 'ANSI Shadow' }, (err, result) => {
|
|
70
|
+
if (!err && result) {
|
|
71
|
+
setAscii(result);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}, [text]);
|
|
75
|
+
const lines = ascii.split('\n').filter(line => line.trim() !== '');
|
|
76
|
+
const baseColor = color || '#f0c040'; // Default yeehaw gold
|
|
77
|
+
const gradientColors = generateGradient(lines, baseColor);
|
|
78
|
+
// Show tumbleweed only for the main "yeehaw" title
|
|
79
|
+
const showTumbleweed = text.toLowerCase() === 'yeehaw';
|
|
80
|
+
// Vertically center tumbleweed next to ASCII art
|
|
81
|
+
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))) })] }), (subtitle || summary) && (_jsxs(Box, { gap: 2, children: [subtitle && _jsx(Text, { dimColor: true, children: subtitle }), summary && _jsxs(Text, { color: "gray", children: ["- ", summary] })] }))] }));
|
|
83
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type HotkeyScope } from '../lib/hotkeys.js';
|
|
2
|
+
interface HelpOverlayProps {
|
|
3
|
+
scope: HotkeyScope;
|
|
4
|
+
focusedPanel?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function HelpOverlay({ scope, focusedPanel }: HelpOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { getHotkeysGrouped } from '../lib/hotkeys.js';
|
|
4
|
+
function HotkeyRow({ hotkey }) {
|
|
5
|
+
return (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: "cyan", children: hotkey.key }) }), _jsx(Text, { children: hotkey.description })] }));
|
|
6
|
+
}
|
|
7
|
+
function HotkeySection({ title, hotkeys }) {
|
|
8
|
+
if (hotkeys.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: title }), hotkeys.map((h, i) => (_jsx(HotkeyRow, { hotkey: h }, `${h.key}-${i}`)))] }));
|
|
11
|
+
}
|
|
12
|
+
export function HelpOverlay({ scope, focusedPanel }) {
|
|
13
|
+
const grouped = getHotkeysGrouped(scope, focusedPanel);
|
|
14
|
+
// Also include list navigation if we're in a view with lists
|
|
15
|
+
const listHotkeys = scope !== 'global' ? getHotkeysGrouped('list').navigation : [];
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Keyboard Shortcuts" }), _jsx(HotkeySection, { title: "Navigation", hotkeys: [...grouped.navigation, ...listHotkeys] }), _jsx(HotkeySection, { title: "Actions", hotkeys: grouped.action }), _jsx(HotkeySection, { title: "System", hotkeys: grouped.system }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press ? to close" }) })] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ListItem {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
status?: 'active' | 'inactive' | 'error';
|
|
5
|
+
meta?: string;
|
|
6
|
+
}
|
|
7
|
+
interface ListProps {
|
|
8
|
+
items: ListItem[];
|
|
9
|
+
focused?: boolean;
|
|
10
|
+
selectedIndex?: number;
|
|
11
|
+
onSelect?: (item: ListItem) => void;
|
|
12
|
+
onAction?: (item: ListItem) => void;
|
|
13
|
+
onHighlight?: (item: ListItem | null) => void;
|
|
14
|
+
onSelectionChange?: (index: number) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function List({ items, focused, selectedIndex: controlledIndex, onSelect, onAction, onHighlight, onSelectionChange }: ListProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
export function List({ items, focused = false, selectedIndex: controlledIndex, onSelect, onAction, onHighlight, onSelectionChange }) {
|
|
5
|
+
const [internalIndex, setInternalIndex] = useState(0);
|
|
6
|
+
// Use controlled index if provided, otherwise internal
|
|
7
|
+
const selectedIndex = controlledIndex ?? internalIndex;
|
|
8
|
+
const setSelectedIndex = (indexOrFn) => {
|
|
9
|
+
const newIndex = typeof indexOrFn === 'function' ? indexOrFn(selectedIndex) : indexOrFn;
|
|
10
|
+
if (controlledIndex === undefined) {
|
|
11
|
+
setInternalIndex(newIndex);
|
|
12
|
+
}
|
|
13
|
+
onSelectionChange?.(newIndex);
|
|
14
|
+
};
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (items.length > 0 && onHighlight) {
|
|
17
|
+
onHighlight(items[selectedIndex] ?? null);
|
|
18
|
+
}
|
|
19
|
+
}, [selectedIndex, items, onHighlight]);
|
|
20
|
+
useInput((input, key) => {
|
|
21
|
+
if (!focused)
|
|
22
|
+
return;
|
|
23
|
+
if (input === 'j' || key.downArrow) {
|
|
24
|
+
setSelectedIndex((i) => Math.min(i + 1, items.length - 1));
|
|
25
|
+
}
|
|
26
|
+
if (input === 'k' || key.upArrow) {
|
|
27
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
28
|
+
}
|
|
29
|
+
if (input === 'g') {
|
|
30
|
+
setSelectedIndex(0);
|
|
31
|
+
}
|
|
32
|
+
if (input === 'G') {
|
|
33
|
+
setSelectedIndex(items.length - 1);
|
|
34
|
+
}
|
|
35
|
+
if (key.return && items[selectedIndex] && onSelect) {
|
|
36
|
+
onSelect(items[selectedIndex]);
|
|
37
|
+
}
|
|
38
|
+
if (input === 's' && items[selectedIndex] && onAction) {
|
|
39
|
+
onAction(items[selectedIndex]);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
if (items.length === 0) {
|
|
43
|
+
return _jsx(Text, { dimColor: true, children: "No items" });
|
|
44
|
+
}
|
|
45
|
+
return (_jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
|
|
46
|
+
const isSelected = index === selectedIndex && focused;
|
|
47
|
+
const statusColor = item.status === 'active' ? 'green' :
|
|
48
|
+
item.status === 'error' ? 'red' : 'gray';
|
|
49
|
+
// Yeehaw brand gold for selection
|
|
50
|
+
const selectionColor = '#f0c040';
|
|
51
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isSelected ? selectionColor : undefined, children: isSelected ? '›' : ' ' }, "arrow"), _jsx(Text, { color: isSelected ? selectionColor : undefined, bold: isSelected, children: item.label }, "label"), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" }, "status")), item.meta && (_jsx(Text, { dimColor: true, children: item.meta }, "meta"))] }, item.id));
|
|
52
|
+
}) }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
// @ts-ignore - no types available
|
|
5
|
+
import TerminalRenderer from 'marked-terminal';
|
|
6
|
+
// Configure marked to use terminal renderer
|
|
7
|
+
marked.setOptions({
|
|
8
|
+
renderer: new TerminalRenderer({
|
|
9
|
+
// Customize terminal rendering options
|
|
10
|
+
showSectionPrefix: false,
|
|
11
|
+
reflowText: true,
|
|
12
|
+
width: 80,
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Render markdown content in the terminal with formatting.
|
|
17
|
+
*/
|
|
18
|
+
export function Markdown({ children }) {
|
|
19
|
+
const rendered = marked.parse(children);
|
|
20
|
+
// marked-terminal returns a string with ANSI codes
|
|
21
|
+
// Ink's Text component will render these correctly
|
|
22
|
+
return _jsx(Text, { children: String(rendered).trim() });
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
interface PanelProps {
|
|
3
|
+
title: string;
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
focused?: boolean;
|
|
6
|
+
width?: number | string;
|
|
7
|
+
hints?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Panel({ title, children, focused, width, hints }: PanelProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function Panel({ title, children, focused = false, width, hints }) {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', width: width, children: [_jsxs(Box, { paddingX: 1, marginBottom: 0, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: focused ? 'cyan' : 'white', children: title }), focused && hints && (_jsx(Text, { color: "cyan", children: hints }))] }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: children })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Barn } from '../types.js';
|
|
2
|
+
interface PathInputProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
onSubmit: (value: string) => void;
|
|
6
|
+
barn?: Barn;
|
|
7
|
+
}
|
|
8
|
+
export declare function PathInput({ value, onChange, onSubmit, barn }: PathInputProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { readdirSync, existsSync } from 'fs';
|
|
5
|
+
import { join, dirname, basename } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { execaSync } from 'execa';
|
|
8
|
+
import { hasValidSshConfig } from '../lib/config.js';
|
|
9
|
+
function expandPath(path) {
|
|
10
|
+
if (path.startsWith('~/')) {
|
|
11
|
+
return join(homedir(), path.slice(2));
|
|
12
|
+
}
|
|
13
|
+
return path;
|
|
14
|
+
}
|
|
15
|
+
function getLocalCompletions(partialPath) {
|
|
16
|
+
if (!partialPath)
|
|
17
|
+
return [];
|
|
18
|
+
const expanded = expandPath(partialPath);
|
|
19
|
+
const dir = partialPath.endsWith('/') ? expanded : dirname(expanded);
|
|
20
|
+
const prefix = partialPath.endsWith('/') ? '' : basename(expanded);
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(dir))
|
|
23
|
+
return [];
|
|
24
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
25
|
+
const matches = entries
|
|
26
|
+
.filter((e) => e.name.startsWith(prefix) && e.isDirectory())
|
|
27
|
+
.map((e) => e.name);
|
|
28
|
+
return matches;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function getRemoteCompletions(partialPath, barn) {
|
|
35
|
+
if (!partialPath)
|
|
36
|
+
return [];
|
|
37
|
+
// Verify barn has valid SSH config
|
|
38
|
+
if (!hasValidSshConfig(barn)) {
|
|
39
|
+
return []; // Can't do remote completion without SSH config
|
|
40
|
+
}
|
|
41
|
+
// Handle ~ expansion for display
|
|
42
|
+
const dir = partialPath.endsWith('/') ? partialPath : dirname(partialPath) || '~';
|
|
43
|
+
const prefix = partialPath.endsWith('/') ? '' : basename(partialPath);
|
|
44
|
+
try {
|
|
45
|
+
// Use SSH to list directories on the remote server
|
|
46
|
+
const result = execaSync('ssh', [
|
|
47
|
+
'-p', String(barn.port),
|
|
48
|
+
'-i', barn.identity_file,
|
|
49
|
+
'-o', 'BatchMode=yes',
|
|
50
|
+
'-o', 'ConnectTimeout=5',
|
|
51
|
+
`${barn.user}@${barn.host}`,
|
|
52
|
+
`ls -1d ${dir}/${prefix}*/ 2>/dev/null | xargs -n1 basename 2>/dev/null || true`
|
|
53
|
+
], { timeout: 10000 });
|
|
54
|
+
const output = result.stdout.trim();
|
|
55
|
+
if (!output)
|
|
56
|
+
return [];
|
|
57
|
+
return output.split('\n').filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function PathInput({ value, onChange, onSubmit, barn }) {
|
|
64
|
+
const [cursorPos, setCursorPos] = useState(value.length);
|
|
65
|
+
const [completions, setCompletions] = useState([]);
|
|
66
|
+
const [loading, setLoading] = useState(false);
|
|
67
|
+
// Update completions when value changes (debounced for remote)
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (barn) {
|
|
70
|
+
// Remote completion - debounce
|
|
71
|
+
setLoading(true);
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
const results = getRemoteCompletions(value, barn);
|
|
74
|
+
setCompletions(results);
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}, 300);
|
|
77
|
+
return () => clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Local completion - immediate
|
|
81
|
+
setCompletions(getLocalCompletions(value));
|
|
82
|
+
}
|
|
83
|
+
}, [value, barn]);
|
|
84
|
+
useInput((input, key) => {
|
|
85
|
+
if (key.return) {
|
|
86
|
+
onSubmit(value);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.tab) {
|
|
90
|
+
// Tab completion
|
|
91
|
+
if (completions.length === 1) {
|
|
92
|
+
// Single match - complete it
|
|
93
|
+
const displayDir = value.endsWith('/') ? value : value.slice(0, value.lastIndexOf('/') + 1) || (barn ? '~/' : '');
|
|
94
|
+
const newPath = displayDir + completions[0] + '/';
|
|
95
|
+
onChange(newPath);
|
|
96
|
+
setCursorPos(newPath.length);
|
|
97
|
+
}
|
|
98
|
+
else if (completions.length > 1) {
|
|
99
|
+
// Multiple matches - find common prefix
|
|
100
|
+
const commonPrefix = completions.reduce((acc, curr) => {
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < acc.length && i < curr.length && acc[i] === curr[i])
|
|
103
|
+
i++;
|
|
104
|
+
return acc.slice(0, i);
|
|
105
|
+
});
|
|
106
|
+
if (commonPrefix) {
|
|
107
|
+
const displayDir = value.endsWith('/') ? value : value.slice(0, value.lastIndexOf('/') + 1) || (barn ? '~/' : '');
|
|
108
|
+
const newPath = displayDir + commonPrefix;
|
|
109
|
+
onChange(newPath);
|
|
110
|
+
setCursorPos(newPath.length);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (key.backspace || key.delete) {
|
|
116
|
+
if (value.length > 0) {
|
|
117
|
+
const newValue = value.slice(0, -1);
|
|
118
|
+
onChange(newValue);
|
|
119
|
+
setCursorPos(Math.max(0, cursorPos - 1));
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (key.leftArrow) {
|
|
124
|
+
setCursorPos(Math.max(0, cursorPos - 1));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (key.rightArrow) {
|
|
128
|
+
setCursorPos(Math.min(value.length, cursorPos + 1));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Regular character input
|
|
132
|
+
if (input && !key.ctrl && !key.meta) {
|
|
133
|
+
const newValue = value + input;
|
|
134
|
+
onChange(newValue);
|
|
135
|
+
setCursorPos(newValue.length);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// Show completions hint
|
|
139
|
+
const showHint = completions.length > 1 && completions.length <= 5;
|
|
140
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: value }), _jsx(Text, { backgroundColor: "white", color: "black", children: " " }), loading && _jsx(Text, { dimColor: true, children: " (loading...)" })] }), showHint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Tab: ", completions.join(' ')] }) }))] }));
|
|
141
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface ScrollableMarkdownProps {
|
|
2
|
+
children: string;
|
|
3
|
+
focused?: boolean;
|
|
4
|
+
height?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Scrollable markdown content panel.
|
|
8
|
+
* When focused, j/k or arrow keys scroll the content.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ScrollableMarkdown({ children, focused, height, }: ScrollableMarkdownProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { marked } from 'marked';
|
|
5
|
+
// @ts-ignore - no types available
|
|
6
|
+
import TerminalRenderer from 'marked-terminal';
|
|
7
|
+
// Configure marked to use terminal renderer
|
|
8
|
+
marked.setOptions({
|
|
9
|
+
renderer: new TerminalRenderer({
|
|
10
|
+
showSectionPrefix: false,
|
|
11
|
+
reflowText: true,
|
|
12
|
+
width: 80,
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Scrollable markdown content panel.
|
|
17
|
+
* When focused, j/k or arrow keys scroll the content.
|
|
18
|
+
*/
|
|
19
|
+
export function ScrollableMarkdown({ children, focused = false, height = 20, }) {
|
|
20
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
21
|
+
// Reset scroll when content changes
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setScrollOffset(0);
|
|
24
|
+
}, [children]);
|
|
25
|
+
// Render markdown to string with ANSI codes
|
|
26
|
+
const rendered = String(marked.parse(children)).trim();
|
|
27
|
+
const lines = rendered.split('\n');
|
|
28
|
+
const totalLines = lines.length;
|
|
29
|
+
const visibleLines = height - 1; // Leave room for scroll indicator
|
|
30
|
+
useInput((input, key) => {
|
|
31
|
+
if (!focused)
|
|
32
|
+
return;
|
|
33
|
+
if (input === 'j' || key.downArrow) {
|
|
34
|
+
setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, totalLines - visibleLines)));
|
|
35
|
+
}
|
|
36
|
+
if (input === 'k' || key.upArrow) {
|
|
37
|
+
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
38
|
+
}
|
|
39
|
+
if (input === 'g') {
|
|
40
|
+
setScrollOffset(0);
|
|
41
|
+
}
|
|
42
|
+
if (input === 'G') {
|
|
43
|
+
setScrollOffset(Math.max(0, totalLines - visibleLines));
|
|
44
|
+
}
|
|
45
|
+
if (key.pageDown) {
|
|
46
|
+
setScrollOffset((prev) => Math.min(prev + visibleLines, Math.max(0, totalLines - visibleLines)));
|
|
47
|
+
}
|
|
48
|
+
if (key.pageUp) {
|
|
49
|
+
setScrollOffset((prev) => Math.max(prev - visibleLines, 0));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// Get visible slice of lines
|
|
53
|
+
const displayLines = lines.slice(scrollOffset, scrollOffset + visibleLines);
|
|
54
|
+
const showScrollIndicator = totalLines > visibleLines;
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, children: displayLines.map((line, idx) => (_jsx(Text, { children: line || ' ' }, scrollOffset + idx))) }), showScrollIndicator && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: ["[", scrollOffset + 1, "-", Math.min(scrollOffset + visibleLines, totalLines), "/", totalLines, "]", focused ? ' (j/k to scroll)' : ''] }) }))] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const globalShortcuts = [
|
|
4
|
+
{ key: 'Enter', label: 'select' },
|
|
5
|
+
{ key: 'Tab', label: 'switch panel' },
|
|
6
|
+
{ key: 'n', label: 'new project' },
|
|
7
|
+
{ key: 'c', label: 'claude' },
|
|
8
|
+
{ key: '?', label: 'help' },
|
|
9
|
+
];
|
|
10
|
+
const projectShortcuts = [
|
|
11
|
+
{ key: 'Enter', label: 'shell' },
|
|
12
|
+
{ key: 'c', label: 'claude' },
|
|
13
|
+
{ key: 'Tab', label: 'switch panel' },
|
|
14
|
+
{ key: '?', label: 'help' },
|
|
15
|
+
{ key: 'Esc', label: 'back' },
|
|
16
|
+
];
|
|
17
|
+
export function StatusBar({ view }) {
|
|
18
|
+
const shortcuts = view === 'global' ? globalShortcuts : projectShortcuts;
|
|
19
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsx(Box, { gap: 2, children: shortcuts.map((shortcut) => (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: ["[", shortcut.key, "]"] }), _jsx(Text, { dimColor: true, children: shortcut.label })] }, shortcut.key))) }), view === 'global' ? (_jsxs(Box, { gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "q" }), _jsx(Text, { dimColor: true, children: ":detach" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "red", children: "Q" }), _jsx(Text, { dimColor: true, children: ":quit all" })] })] })) : (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "q" }), _jsx(Text, { dimColor: true, children: ":back" })] }))] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface TextAreaProps {
|
|
2
|
+
value: string;
|
|
3
|
+
onChange: (value: string) => void;
|
|
4
|
+
onSubmit?: (value: string) => void;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
height?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Multiline text input component.
|
|
10
|
+
* - Enter: add newline
|
|
11
|
+
* - Ctrl+S: save/submit
|
|
12
|
+
* - Ctrl+D: also save/submit (alternative)
|
|
13
|
+
* - Backspace: delete character
|
|
14
|
+
* - Arrow keys: navigate
|
|
15
|
+
*/
|
|
16
|
+
export declare function TextArea({ value, onChange, onSubmit, placeholder, height, }: TextAreaProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
/**
|
|
5
|
+
* Multiline text input component.
|
|
6
|
+
* - Enter: add newline
|
|
7
|
+
* - Ctrl+S: save/submit
|
|
8
|
+
* - Ctrl+D: also save/submit (alternative)
|
|
9
|
+
* - Backspace: delete character
|
|
10
|
+
* - Arrow keys: navigate
|
|
11
|
+
*/
|
|
12
|
+
export function TextArea({ value, onChange, onSubmit, placeholder, height = 8, }) {
|
|
13
|
+
const [cursorPos, setCursorPos] = useState(value.length);
|
|
14
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
15
|
+
// Keep cursor in bounds when value changes
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (cursorPos > value.length) {
|
|
18
|
+
setCursorPos(value.length);
|
|
19
|
+
}
|
|
20
|
+
}, [value, cursorPos]);
|
|
21
|
+
// Calculate cursor line for auto-scroll
|
|
22
|
+
const getCursorLine = () => {
|
|
23
|
+
const beforeCursor = value.slice(0, cursorPos);
|
|
24
|
+
return (beforeCursor.match(/\n/g) || []).length;
|
|
25
|
+
};
|
|
26
|
+
// Auto-scroll to keep cursor visible
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const cursorLine = getCursorLine();
|
|
29
|
+
const visibleLines = height - 2;
|
|
30
|
+
if (cursorLine < scrollOffset) {
|
|
31
|
+
setScrollOffset(cursorLine);
|
|
32
|
+
}
|
|
33
|
+
else if (cursorLine >= scrollOffset + visibleLines) {
|
|
34
|
+
setScrollOffset(cursorLine - visibleLines + 1);
|
|
35
|
+
}
|
|
36
|
+
}, [cursorPos, value, height]);
|
|
37
|
+
useInput((input, key) => {
|
|
38
|
+
// Ctrl+S or Ctrl+D to submit
|
|
39
|
+
if ((key.ctrl && input === 's') || (key.ctrl && input === 'd')) {
|
|
40
|
+
onSubmit?.(value);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Enter adds newline
|
|
44
|
+
if (key.return) {
|
|
45
|
+
const newValue = value.slice(0, cursorPos) + '\n' + value.slice(cursorPos);
|
|
46
|
+
onChange(newValue);
|
|
47
|
+
setCursorPos(cursorPos + 1);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Backspace
|
|
51
|
+
if (key.backspace || key.delete) {
|
|
52
|
+
if (cursorPos > 0) {
|
|
53
|
+
const newValue = value.slice(0, cursorPos - 1) + value.slice(cursorPos);
|
|
54
|
+
onChange(newValue);
|
|
55
|
+
setCursorPos(cursorPos - 1);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Arrow keys for cursor movement
|
|
60
|
+
if (key.leftArrow) {
|
|
61
|
+
setCursorPos(Math.max(0, cursorPos - 1));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (key.rightArrow) {
|
|
65
|
+
setCursorPos(Math.min(value.length, cursorPos + 1));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.upArrow) {
|
|
69
|
+
const beforeCursor = value.slice(0, cursorPos);
|
|
70
|
+
const currentLineStart = beforeCursor.lastIndexOf('\n') + 1;
|
|
71
|
+
if (currentLineStart > 0) {
|
|
72
|
+
const posInLine = cursorPos - currentLineStart;
|
|
73
|
+
const prevLineEnd = currentLineStart - 1;
|
|
74
|
+
const prevLineStart = value.lastIndexOf('\n', prevLineEnd - 1) + 1;
|
|
75
|
+
const prevLineLength = prevLineEnd - prevLineStart;
|
|
76
|
+
const newPos = prevLineStart + Math.min(posInLine, prevLineLength);
|
|
77
|
+
setCursorPos(newPos);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key.downArrow) {
|
|
82
|
+
const nextLineStart = value.indexOf('\n', cursorPos);
|
|
83
|
+
if (nextLineStart !== -1) {
|
|
84
|
+
const currentLineStart = value.lastIndexOf('\n', cursorPos - 1) + 1;
|
|
85
|
+
const posInLine = cursorPos - currentLineStart;
|
|
86
|
+
const nextLineEnd = value.indexOf('\n', nextLineStart + 1);
|
|
87
|
+
const nextLineLength = (nextLineEnd === -1 ? value.length : nextLineEnd) - nextLineStart - 1;
|
|
88
|
+
const newPos = nextLineStart + 1 + Math.min(posInLine, nextLineLength);
|
|
89
|
+
setCursorPos(newPos);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Page up/down for scrolling
|
|
94
|
+
if (key.pageUp) {
|
|
95
|
+
setScrollOffset(Math.max(0, scrollOffset - (height - 2)));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (key.pageDown) {
|
|
99
|
+
const totalLines = (value.match(/\n/g) || []).length + 1;
|
|
100
|
+
setScrollOffset(Math.min(totalLines - 1, scrollOffset + (height - 2)));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Regular character input
|
|
104
|
+
if (input && !key.ctrl && !key.meta && input.charCodeAt(0) >= 32) {
|
|
105
|
+
const newValue = value.slice(0, cursorPos) + input + value.slice(cursorPos);
|
|
106
|
+
onChange(newValue);
|
|
107
|
+
setCursorPos(cursorPos + input.length);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// Render
|
|
111
|
+
const displayValue = value || '';
|
|
112
|
+
const showPlaceholder = !value && placeholder;
|
|
113
|
+
const lines = (showPlaceholder ? placeholder : displayValue).split('\n');
|
|
114
|
+
const totalLines = lines.length;
|
|
115
|
+
const visibleLines = height - 2;
|
|
116
|
+
// Calculate cursor position
|
|
117
|
+
let cursorLine = 0;
|
|
118
|
+
let cursorCol = 0;
|
|
119
|
+
if (!showPlaceholder && value) {
|
|
120
|
+
const beforeCursor = value.slice(0, cursorPos);
|
|
121
|
+
cursorLine = (beforeCursor.match(/\n/g) || []).length;
|
|
122
|
+
const lastNewline = beforeCursor.lastIndexOf('\n');
|
|
123
|
+
cursorCol = lastNewline === -1 ? cursorPos : cursorPos - lastNewline - 1;
|
|
124
|
+
}
|
|
125
|
+
// Get visible lines with scroll
|
|
126
|
+
const visibleStart = scrollOffset;
|
|
127
|
+
const visibleEnd = Math.min(scrollOffset + visibleLines, totalLines);
|
|
128
|
+
const displayLines = lines.slice(visibleStart, visibleEnd);
|
|
129
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", padding: 1, height: height, children: [displayLines.map((line, displayIdx) => {
|
|
130
|
+
const actualLineIdx = visibleStart + displayIdx;
|
|
131
|
+
const isCursorLine = actualLineIdx === cursorLine && !showPlaceholder;
|
|
132
|
+
if (isCursorLine) {
|
|
133
|
+
const before = line.slice(0, cursorCol);
|
|
134
|
+
const cursorChar = line[cursorCol] || ' ';
|
|
135
|
+
const after = line.slice(cursorCol + 1);
|
|
136
|
+
return (_jsx(Box, { children: _jsxs(Text, { children: [before, _jsx(Text, { inverse: true, children: cursorChar }), after] }) }, actualLineIdx));
|
|
137
|
+
}
|
|
138
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: showPlaceholder ? true : undefined, children: line || ' ' }) }, actualLineIdx));
|
|
139
|
+
}), totalLines > visibleLines && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: ["[", visibleStart + 1, "-", visibleEnd, "/", totalLines, "]"] }) }))] }));
|
|
140
|
+
}
|