@colmbus72/yeehaw 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +2 -1
- package/claude-plugin/hooks/hooks.json +41 -0
- package/claude-plugin/hooks/session-status.sh +13 -0
- package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
- package/dist/app.js +228 -28
- package/dist/components/CritterHeader.d.ts +7 -0
- package/dist/components/CritterHeader.js +81 -0
- package/dist/components/HelpOverlay.js +4 -2
- package/dist/components/List.d.ts +10 -1
- package/dist/components/List.js +14 -5
- package/dist/components/Panel.js +27 -1
- package/dist/components/SplashScreen.js +1 -1
- package/dist/hooks/useSessions.js +2 -2
- package/dist/index.js +41 -1
- package/dist/lib/auth/index.d.ts +2 -0
- package/dist/lib/auth/index.js +3 -0
- package/dist/lib/auth/linear.d.ts +20 -0
- package/dist/lib/auth/linear.js +79 -0
- package/dist/lib/auth/storage.d.ts +12 -0
- package/dist/lib/auth/storage.js +53 -0
- package/dist/lib/config.d.ts +13 -1
- package/dist/lib/config.js +51 -0
- package/dist/lib/context.d.ts +10 -0
- package/dist/lib/context.js +63 -0
- package/dist/lib/critters.d.ts +61 -0
- package/dist/lib/critters.js +365 -0
- package/dist/lib/hooks.d.ts +20 -0
- package/dist/lib/hooks.js +91 -0
- package/dist/lib/hotkeys.d.ts +1 -1
- package/dist/lib/hotkeys.js +28 -20
- package/dist/lib/issues/github.d.ts +11 -0
- package/dist/lib/issues/github.js +154 -0
- package/dist/lib/issues/index.d.ts +14 -0
- package/dist/lib/issues/index.js +27 -0
- package/dist/lib/issues/linear.d.ts +24 -0
- package/dist/lib/issues/linear.js +345 -0
- package/dist/lib/issues/types.d.ts +82 -0
- package/dist/lib/issues/types.js +2 -0
- package/dist/lib/paths.d.ts +3 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/signals.d.ts +30 -0
- package/dist/lib/signals.js +104 -0
- package/dist/lib/tmux.d.ts +9 -2
- package/dist/lib/tmux.js +114 -18
- package/dist/mcp-server.js +161 -1
- package/dist/types.d.ts +23 -2
- package/dist/views/BarnContext.d.ts +5 -2
- package/dist/views/BarnContext.js +202 -21
- package/dist/views/CritterDetailView.d.ts +10 -0
- package/dist/views/CritterDetailView.js +117 -0
- package/dist/views/CritterLogsView.d.ts +8 -0
- package/dist/views/CritterLogsView.js +100 -0
- package/dist/views/GlobalDashboard.d.ts +2 -2
- package/dist/views/GlobalDashboard.js +20 -18
- package/dist/views/IssuesView.d.ts +2 -1
- package/dist/views/IssuesView.js +661 -98
- package/dist/views/LivestockDetailView.d.ts +2 -1
- package/dist/views/LivestockDetailView.js +19 -8
- package/dist/views/ProjectContext.d.ts +2 -2
- package/dist/views/ProjectContext.js +68 -25
- package/package.json +5 -5
package/dist/components/List.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
// Yeehaw brand gold (darker for light mode readability)
|
|
5
|
+
const BRAND_COLOR = '#d4a020';
|
|
4
6
|
export function List({ items, focused = false, selectedIndex: controlledIndex, onSelect, onAction, onHighlight, onSelectionChange }) {
|
|
5
7
|
const [internalIndex, setInternalIndex] = useState(0);
|
|
6
8
|
// Use controlled index if provided, otherwise internal
|
|
@@ -35,8 +37,13 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
|
|
|
35
37
|
if (key.return && items[selectedIndex] && onSelect) {
|
|
36
38
|
onSelect(items[selectedIndex]);
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
// Handle row-level actions (replaces hardcoded 's' check)
|
|
41
|
+
const currentItem = items[selectedIndex];
|
|
42
|
+
if (currentItem?.actions && onAction) {
|
|
43
|
+
const action = currentItem.actions.find(a => a.key === input);
|
|
44
|
+
if (action) {
|
|
45
|
+
onAction(currentItem, action.key);
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
48
|
});
|
|
42
49
|
if (items.length === 0) {
|
|
@@ -44,10 +51,12 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
|
|
|
44
51
|
}
|
|
45
52
|
return (_jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
|
|
46
53
|
const isSelected = index === selectedIndex && focused;
|
|
54
|
+
// Session status takes priority for meta coloring
|
|
55
|
+
const sessionStatusColor = item.sessionStatus === 'waiting' ? 'yellow' :
|
|
56
|
+
item.sessionStatus === 'working' ? 'cyan' :
|
|
57
|
+
item.sessionStatus === 'error' ? 'red' : undefined;
|
|
47
58
|
const statusColor = item.status === 'active' ? 'green' :
|
|
48
59
|
item.status === 'error' ? 'red' : 'gray';
|
|
49
|
-
|
|
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));
|
|
60
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 0, width: 2, children: _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, children: isSelected ? '›' : ' ' }) }), _jsxs(Box, { gap: 1, flexShrink: 1, flexGrow: 1, overflow: "hidden", children: [item.prefix && (_jsx(Box, { flexShrink: 0, children: item.prefix })), _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, bold: isSelected, wrap: "truncate", children: item.label }), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" })), item.meta && (_jsx(Text, { color: sessionStatusColor, dimColor: !sessionStatusColor, wrap: "truncate", children: item.meta }))] }), isSelected && item.actions && item.actions.length > 0 && (_jsx(Box, { gap: 2, flexShrink: 0, marginLeft: 1, children: item.actions.map((action) => (_jsxs(Text, { children: [_jsxs(Text, { color: BRAND_COLOR, children: ["[", action.key, "]"] }), _jsxs(Text, { dimColor: true, children: [" ", action.label] })] }, action.key))) }))] }, item.id));
|
|
52
61
|
}) }));
|
|
53
62
|
}
|
package/dist/components/Panel.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
// Yeehaw brand gold (darker for light mode readability)
|
|
4
|
+
const BRAND_COLOR = '#d4a020';
|
|
5
|
+
// Render hints with gold keys and gray labels: "[n] new" -> gold "[n]" + gray " new"
|
|
6
|
+
function renderHints(hints) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
// Match [key] patterns and split around them
|
|
9
|
+
const regex = /(\[[^\]]+\])/g;
|
|
10
|
+
let lastIndex = 0;
|
|
11
|
+
let match;
|
|
12
|
+
let keyIndex = 0;
|
|
13
|
+
while ((match = regex.exec(hints)) !== null) {
|
|
14
|
+
// Add text before the match (gray)
|
|
15
|
+
if (match.index > lastIndex) {
|
|
16
|
+
parts.push(_jsx(Text, { dimColor: true, children: hints.slice(lastIndex, match.index) }, `text-${keyIndex}`));
|
|
17
|
+
}
|
|
18
|
+
// Add the [key] part (gold)
|
|
19
|
+
parts.push(_jsx(Text, { color: BRAND_COLOR, children: match[1] }, `key-${keyIndex}`));
|
|
20
|
+
lastIndex = regex.lastIndex;
|
|
21
|
+
keyIndex++;
|
|
22
|
+
}
|
|
23
|
+
// Add remaining text after last match (gray)
|
|
24
|
+
if (lastIndex < hints.length) {
|
|
25
|
+
parts.push(_jsx(Text, { dimColor: true, children: hints.slice(lastIndex) }, `text-end`));
|
|
26
|
+
}
|
|
27
|
+
return parts;
|
|
28
|
+
}
|
|
3
29
|
export function Panel({ title, children, focused = false, width, hints }) {
|
|
4
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ?
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? BRAND_COLOR : 'gray', width: width, children: [_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsx(Text, { bold: true, color: focused ? BRAND_COLOR : 'gray', children: title }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: children }), _jsx(Box, { paddingX: 1, justifyContent: "flex-end", height: 1, children: focused && hints ? (_jsx(Text, { children: renderHints(hints) })) : (_jsx(Text, { children: " " })) })] }));
|
|
5
31
|
}
|
|
@@ -10,7 +10,7 @@ const TUMBLEWEED = [
|
|
|
10
10
|
' ░▒░ ░▒░ ░',
|
|
11
11
|
];
|
|
12
12
|
const TUMBLEWEED_COLOR = '#b8860b';
|
|
13
|
-
const BRAND_COLOR = '#
|
|
13
|
+
const BRAND_COLOR = '#d4a020'; // Darker for light mode readability
|
|
14
14
|
// Match Header.tsx positioning exactly
|
|
15
15
|
const HEADER_PADDING_TOP = 1;
|
|
16
16
|
const TUMBLEWEED_TOP_PADDING = 1;
|
|
@@ -18,8 +18,8 @@ export function useSessions() {
|
|
|
18
18
|
const result = listYeehawWindows();
|
|
19
19
|
setWindows(result);
|
|
20
20
|
setLoading(false);
|
|
21
|
-
// Poll
|
|
22
|
-
const interval = setInterval(refresh,
|
|
21
|
+
// Poll for window updates (without loading state changes)
|
|
22
|
+
const interval = setInterval(refresh, 800);
|
|
23
23
|
return () => clearInterval(interval);
|
|
24
24
|
}, [refresh]);
|
|
25
25
|
const createClaude = useCallback((workingDir, name) => {
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,43 @@ import { App } from './app.js';
|
|
|
5
5
|
import { isInsideYeehawSession, yeehawSessionExists, createYeehawSession, attachToYeehaw, hasTmux, } from './lib/tmux.js';
|
|
6
6
|
import { ensureConfigDirs } from './lib/config.js';
|
|
7
7
|
import { checkForUpdates, formatUpdateMessage } from './lib/update-check.js';
|
|
8
|
+
import { installHookScript, getClaudeHooksConfig, checkClaudeHooksInstalled } from './lib/hooks.js';
|
|
9
|
+
/**
|
|
10
|
+
* Handle CLI subcommands
|
|
11
|
+
*/
|
|
12
|
+
function handleSubcommands() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
if (args[0] === 'hooks' && args[1] === 'install') {
|
|
15
|
+
const scriptPath = installHookScript();
|
|
16
|
+
console.log(`\x1b[32m✓\x1b[0m Hook script installed: ${scriptPath}`);
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log('\x1b[33mNote:\x1b[0m Claude sessions started from Yeehaw already have hooks enabled.');
|
|
19
|
+
console.log('This command is only needed for Claude sessions started outside Yeehaw.');
|
|
20
|
+
if (checkClaudeHooksInstalled()) {
|
|
21
|
+
console.log('\n\x1b[32m✓\x1b[0m Claude hooks already configured in ~/.claude/settings.json');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log('\nTo enable status tracking for external Claude sessions,');
|
|
25
|
+
console.log('add this to ~/.claude/settings.json:');
|
|
26
|
+
console.log(JSON.stringify(getClaudeHooksConfig(), null, 2));
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (args[0] === 'hooks') {
|
|
31
|
+
console.log('Usage: yeehaw hooks install');
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log('Install Claude hooks for session status tracking.');
|
|
34
|
+
console.log('Note: Sessions started from Yeehaw already have hooks enabled automatically.');
|
|
35
|
+
console.log('This is only needed for Claude sessions started outside Yeehaw.');
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
8
40
|
function main() {
|
|
41
|
+
// Handle subcommands first (before tmux checks)
|
|
42
|
+
if (handleSubcommands()) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
9
45
|
// Ensure config directories exist
|
|
10
46
|
ensureConfigDirs();
|
|
11
47
|
// Check for updates (non-blocking, uses cache)
|
|
@@ -27,7 +63,11 @@ function main() {
|
|
|
27
63
|
}
|
|
28
64
|
// If we're already inside the yeehaw tmux session, just render the TUI
|
|
29
65
|
if (isInsideYeehawSession()) {
|
|
30
|
-
render(_jsx(App, {})
|
|
66
|
+
render(_jsx(App, {}), {
|
|
67
|
+
patchConsole: true,
|
|
68
|
+
incrementalRendering: true,
|
|
69
|
+
// maxFps: 60,
|
|
70
|
+
});
|
|
31
71
|
return;
|
|
32
72
|
}
|
|
33
73
|
// We're not inside yeehaw session - need to create/attach
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { clearLinearToken } from './storage.js';
|
|
2
|
+
export declare const LINEAR_API_KEY_URL = "https://linear.app/settings/api";
|
|
3
|
+
export { clearLinearToken };
|
|
4
|
+
/**
|
|
5
|
+
* Check if Linear is currently authenticated.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isLinearAuthenticated(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Save a Linear API key.
|
|
10
|
+
*/
|
|
11
|
+
export declare function saveLinearApiKey(apiKey: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Validate a Linear API key by making a test request.
|
|
14
|
+
* Returns true if valid, false otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateLinearApiKey(apiKey: string): Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Make an authenticated request to Linear's GraphQL API.
|
|
19
|
+
*/
|
|
20
|
+
export declare function linearGraphQL<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/lib/auth/linear.ts
|
|
2
|
+
// Linear authentication using Personal API Keys
|
|
3
|
+
// Users create keys at: https://linear.app/settings/api
|
|
4
|
+
import { setLinearToken, getLinearToken, clearLinearToken } from './storage.js';
|
|
5
|
+
export const LINEAR_API_KEY_URL = 'https://linear.app/settings/api';
|
|
6
|
+
export { clearLinearToken };
|
|
7
|
+
/**
|
|
8
|
+
* Check if Linear is currently authenticated.
|
|
9
|
+
*/
|
|
10
|
+
export function isLinearAuthenticated() {
|
|
11
|
+
return getLinearToken() !== null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Save a Linear API key.
|
|
15
|
+
*/
|
|
16
|
+
export function saveLinearApiKey(apiKey) {
|
|
17
|
+
setLinearToken(apiKey);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate a Linear API key by making a test request.
|
|
21
|
+
* Returns true if valid, false otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export async function validateLinearApiKey(apiKey) {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Authorization': apiKey,
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
query: '{ viewer { id } }',
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
// Check if we got HTML instead of JSON (indicates auth/routing issue)
|
|
40
|
+
if (text.startsWith('<!') || text.startsWith('<html')) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const result = JSON.parse(text);
|
|
44
|
+
return !!(result.data?.viewer?.id);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Make an authenticated request to Linear's GraphQL API.
|
|
52
|
+
*/
|
|
53
|
+
export async function linearGraphQL(query, variables) {
|
|
54
|
+
const token = getLinearToken();
|
|
55
|
+
if (!token) {
|
|
56
|
+
throw new Error('Not authenticated with Linear');
|
|
57
|
+
}
|
|
58
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
'Authorization': token,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ query, variables }),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Linear API error: ${response.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
const text = await response.text();
|
|
70
|
+
// Check if we got HTML instead of JSON
|
|
71
|
+
if (text.startsWith('<!') || text.startsWith('<html')) {
|
|
72
|
+
throw new Error('Linear API returned HTML instead of JSON - check your API key');
|
|
73
|
+
}
|
|
74
|
+
const result = JSON.parse(text);
|
|
75
|
+
if (result.errors?.length) {
|
|
76
|
+
throw new Error(result.errors[0].message);
|
|
77
|
+
}
|
|
78
|
+
return result.data;
|
|
79
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LinearAuth {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
expiresAt?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AuthConfig {
|
|
6
|
+
linear?: LinearAuth;
|
|
7
|
+
}
|
|
8
|
+
export declare function loadAuth(): AuthConfig;
|
|
9
|
+
export declare function saveAuth(auth: AuthConfig): void;
|
|
10
|
+
export declare function getLinearToken(): string | null;
|
|
11
|
+
export declare function setLinearToken(accessToken: string, expiresAt?: Date): void;
|
|
12
|
+
export declare function clearLinearToken(): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/lib/auth/storage.ts
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import YAML from 'js-yaml';
|
|
4
|
+
import { AUTH_FILE, YEEHAW_DIR } from '../paths.js';
|
|
5
|
+
import { mkdirSync } from 'fs';
|
|
6
|
+
export function loadAuth() {
|
|
7
|
+
if (!existsSync(YEEHAW_DIR)) {
|
|
8
|
+
mkdirSync(YEEHAW_DIR, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
if (!existsSync(AUTH_FILE)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(AUTH_FILE, 'utf-8');
|
|
15
|
+
return YAML.load(content) || {};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function saveAuth(auth) {
|
|
22
|
+
if (!existsSync(YEEHAW_DIR)) {
|
|
23
|
+
mkdirSync(YEEHAW_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
writeFileSync(AUTH_FILE, YAML.dump(auth), 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
export function getLinearToken() {
|
|
28
|
+
const auth = loadAuth();
|
|
29
|
+
if (!auth.linear?.accessToken) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Check expiration if set
|
|
33
|
+
if (auth.linear.expiresAt) {
|
|
34
|
+
const expiresAt = new Date(auth.linear.expiresAt);
|
|
35
|
+
if (expiresAt <= new Date()) {
|
|
36
|
+
return null; // Token expired
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return auth.linear.accessToken;
|
|
40
|
+
}
|
|
41
|
+
export function setLinearToken(accessToken, expiresAt) {
|
|
42
|
+
const auth = loadAuth();
|
|
43
|
+
auth.linear = {
|
|
44
|
+
accessToken,
|
|
45
|
+
expiresAt: expiresAt?.toISOString(),
|
|
46
|
+
};
|
|
47
|
+
saveAuth(auth);
|
|
48
|
+
}
|
|
49
|
+
export function clearLinearToken() {
|
|
50
|
+
const auth = loadAuth();
|
|
51
|
+
delete auth.linear;
|
|
52
|
+
saveAuth(auth);
|
|
53
|
+
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config, Project, Barn, Livestock } from '../types.js';
|
|
1
|
+
import type { Config, Project, Barn, Livestock, Critter } from '../types.js';
|
|
2
2
|
export declare const LOCAL_BARN: Barn;
|
|
3
3
|
export declare function isLocalBarn(barn: Barn): boolean;
|
|
4
4
|
/**
|
|
@@ -25,3 +25,15 @@ export declare function getLivestockForBarn(barnName: string): Array<{
|
|
|
25
25
|
project: Project;
|
|
26
26
|
livestock: Livestock;
|
|
27
27
|
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Add a critter to a barn
|
|
30
|
+
*/
|
|
31
|
+
export declare function addCritterToBarn(barnName: string, critter: Critter): void;
|
|
32
|
+
/**
|
|
33
|
+
* Remove a critter from a barn
|
|
34
|
+
*/
|
|
35
|
+
export declare function removeCritterFromBarn(barnName: string, critterName: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Get a specific critter from a barn
|
|
38
|
+
*/
|
|
39
|
+
export declare function getCritter(barnName: string, critterName: string): Critter | undefined;
|
package/dist/lib/config.js
CHANGED
|
@@ -148,3 +148,54 @@ export function getLivestockForBarn(barnName) {
|
|
|
148
148
|
}
|
|
149
149
|
return result;
|
|
150
150
|
}
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Critter operations
|
|
153
|
+
// ============================================================================
|
|
154
|
+
/**
|
|
155
|
+
* Add a critter to a barn
|
|
156
|
+
*/
|
|
157
|
+
export function addCritterToBarn(barnName, critter) {
|
|
158
|
+
const barn = loadBarn(barnName);
|
|
159
|
+
if (!barn) {
|
|
160
|
+
throw new Error(`Barn not found: ${barnName}`);
|
|
161
|
+
}
|
|
162
|
+
barn.critters = barn.critters || [];
|
|
163
|
+
// Check for duplicate
|
|
164
|
+
if (barn.critters.some(c => c.name === critter.name)) {
|
|
165
|
+
throw new Error(`Critter "${critter.name}" already exists on barn "${barnName}"`);
|
|
166
|
+
}
|
|
167
|
+
barn.critters.push(critter);
|
|
168
|
+
// Only save to file if it's not the local barn
|
|
169
|
+
if (barnName !== 'local') {
|
|
170
|
+
saveBarn(barn);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Remove a critter from a barn
|
|
175
|
+
*/
|
|
176
|
+
export function removeCritterFromBarn(barnName, critterName) {
|
|
177
|
+
const barn = loadBarn(barnName);
|
|
178
|
+
if (!barn) {
|
|
179
|
+
throw new Error(`Barn not found: ${barnName}`);
|
|
180
|
+
}
|
|
181
|
+
const originalLength = (barn.critters || []).length;
|
|
182
|
+
barn.critters = (barn.critters || []).filter(c => c.name !== critterName);
|
|
183
|
+
if (barn.critters.length === originalLength) {
|
|
184
|
+
return false; // Critter wasn't found
|
|
185
|
+
}
|
|
186
|
+
// Only save to file if it's not the local barn
|
|
187
|
+
if (barnName !== 'local') {
|
|
188
|
+
saveBarn(barn);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get a specific critter from a barn
|
|
194
|
+
*/
|
|
195
|
+
export function getCritter(barnName, critterName) {
|
|
196
|
+
const barn = loadBarn(barnName);
|
|
197
|
+
if (!barn) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return barn.critters?.find(c => c.name === critterName);
|
|
201
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a context string to inject into Claude sessions spawned from Yeehaw.
|
|
3
|
+
* Includes project name and wiki section titles (not content) to hint at available context.
|
|
4
|
+
*/
|
|
5
|
+
export declare function buildProjectContext(projectName: string): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Build context for a livestock-specific session.
|
|
8
|
+
* Includes project context plus livestock details.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildLivestockContext(projectName: string, livestockName: string): string | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { loadProject } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a context string to inject into Claude sessions spawned from Yeehaw.
|
|
4
|
+
* Includes project name and wiki section titles (not content) to hint at available context.
|
|
5
|
+
*/
|
|
6
|
+
export function buildProjectContext(projectName) {
|
|
7
|
+
const project = loadProject(projectName);
|
|
8
|
+
if (!project) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const lines = [];
|
|
12
|
+
lines.push(`You are working on the "${project.name}" project.`);
|
|
13
|
+
if (project.summary) {
|
|
14
|
+
lines.push(`Project: ${project.summary}`);
|
|
15
|
+
}
|
|
16
|
+
// Add wiki section titles as hints
|
|
17
|
+
const wikiSections = project.wiki || [];
|
|
18
|
+
if (wikiSections.length > 0) {
|
|
19
|
+
lines.push('');
|
|
20
|
+
lines.push('Yeehaw wiki sections available:');
|
|
21
|
+
for (const section of wikiSections) {
|
|
22
|
+
lines.push(`- ${section.title}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push('Use mcp__yeehaw__get_wiki_section to fetch relevant context before making architectural decisions.');
|
|
26
|
+
}
|
|
27
|
+
return lines.join('\n');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build context for a livestock-specific session.
|
|
31
|
+
* Includes project context plus livestock details.
|
|
32
|
+
*/
|
|
33
|
+
export function buildLivestockContext(projectName, livestockName) {
|
|
34
|
+
const project = loadProject(projectName);
|
|
35
|
+
if (!project) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const livestock = project.livestock?.find(l => l.name === livestockName);
|
|
39
|
+
if (!livestock) {
|
|
40
|
+
return buildProjectContext(projectName);
|
|
41
|
+
}
|
|
42
|
+
const lines = [];
|
|
43
|
+
lines.push(`You are working on the "${project.name}" project, in the "${livestock.name}" environment.`);
|
|
44
|
+
if (project.summary) {
|
|
45
|
+
lines.push(`Project: ${project.summary}`);
|
|
46
|
+
}
|
|
47
|
+
// Add livestock details
|
|
48
|
+
if (livestock.branch) {
|
|
49
|
+
lines.push(`Branch: ${livestock.branch}`);
|
|
50
|
+
}
|
|
51
|
+
// Add wiki section titles as hints
|
|
52
|
+
const wikiSections = project.wiki || [];
|
|
53
|
+
if (wikiSections.length > 0) {
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push('Yeehaw wiki sections available:');
|
|
56
|
+
for (const section of wikiSections) {
|
|
57
|
+
lines.push(`- ${section.title}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push('Use mcp__yeehaw__get_wiki_section to fetch relevant context before making architectural decisions.');
|
|
61
|
+
}
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Critter, Barn } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discovered critter from scanning a barn
|
|
4
|
+
*/
|
|
5
|
+
export interface DiscoveredCritter {
|
|
6
|
+
service: string;
|
|
7
|
+
suggested_name: string;
|
|
8
|
+
binary?: string;
|
|
9
|
+
config_path?: string;
|
|
10
|
+
status: 'running' | 'stopped';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read logs from a critter (via journald or custom path)
|
|
14
|
+
*/
|
|
15
|
+
export declare function readCritterLogs(critter: Critter, barn: Barn, options?: {
|
|
16
|
+
lines?: number;
|
|
17
|
+
pattern?: string;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
content: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Discover critters (running services) on a barn
|
|
24
|
+
*/
|
|
25
|
+
export declare function discoverCritters(barn: Barn): Promise<{
|
|
26
|
+
critters: DiscoveredCritter[];
|
|
27
|
+
error?: string;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Service info from systemctl
|
|
31
|
+
*/
|
|
32
|
+
export interface SystemService {
|
|
33
|
+
name: string;
|
|
34
|
+
state: 'running' | 'stopped' | 'unknown';
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* List systemd services on a barn
|
|
39
|
+
* @param barn - The barn to query
|
|
40
|
+
* @param activeOnly - If true, only return running services (default: true)
|
|
41
|
+
*/
|
|
42
|
+
export declare function listSystemServices(barn: Barn, activeOnly?: boolean): Promise<{
|
|
43
|
+
services: SystemService[];
|
|
44
|
+
error?: string;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Details extracted from a systemd service file
|
|
48
|
+
*/
|
|
49
|
+
export interface ServiceDetails {
|
|
50
|
+
service_path: string;
|
|
51
|
+
config_path?: string;
|
|
52
|
+
log_path?: string;
|
|
53
|
+
use_journald: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get details about a systemd service by parsing its unit file
|
|
57
|
+
*/
|
|
58
|
+
export declare function getServiceDetails(barn: Barn, serviceName: string): Promise<{
|
|
59
|
+
details?: ServiceDetails;
|
|
60
|
+
error?: string;
|
|
61
|
+
}>;
|