@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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { buildSshCommand } from './livestock.js';
|
|
3
|
+
import { shellEscape } from './shell.js';
|
|
4
|
+
import { getErrorMessage } from './errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Services that are interesting for developers (databases, web servers, etc.)
|
|
7
|
+
*/
|
|
8
|
+
const INTERESTING_SERVICE_PATTERNS = [
|
|
9
|
+
// Databases
|
|
10
|
+
'mysql', 'mariadb', 'postgres', 'postgresql', 'mongodb', 'mongo',
|
|
11
|
+
// Caches
|
|
12
|
+
'redis', 'memcached',
|
|
13
|
+
// Web servers
|
|
14
|
+
'nginx', 'apache', 'httpd', 'caddy',
|
|
15
|
+
// PHP
|
|
16
|
+
'php-fpm', 'php7', 'php8',
|
|
17
|
+
// Python
|
|
18
|
+
'gunicorn', 'uvicorn', 'celery',
|
|
19
|
+
// Node
|
|
20
|
+
'node', 'pm2',
|
|
21
|
+
// Mail
|
|
22
|
+
'postfix', 'dovecot',
|
|
23
|
+
// Queue
|
|
24
|
+
'rabbitmq', 'kafka',
|
|
25
|
+
// Search
|
|
26
|
+
'elasticsearch', 'opensearch', 'meilisearch',
|
|
27
|
+
// Other
|
|
28
|
+
'supervisor', 'docker',
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Check if a service name matches any interesting pattern
|
|
32
|
+
*/
|
|
33
|
+
function isInterestingService(serviceName) {
|
|
34
|
+
const lower = serviceName.toLowerCase();
|
|
35
|
+
return INTERESTING_SERVICE_PATTERNS.some(pattern => lower.includes(pattern));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extract a friendly name from a service name
|
|
39
|
+
* e.g., "mysql.service" -> "mysql", "php8.1-fpm.service" -> "php8.1-fpm"
|
|
40
|
+
*/
|
|
41
|
+
function extractServiceName(service) {
|
|
42
|
+
return service.replace(/\.service$/, '');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read logs from a critter (via journald or custom path)
|
|
46
|
+
*/
|
|
47
|
+
export async function readCritterLogs(critter, barn, options = {}) {
|
|
48
|
+
const { lines = 100, pattern } = options;
|
|
49
|
+
const escapedLines = String(lines);
|
|
50
|
+
let cmd;
|
|
51
|
+
if (critter.use_journald !== false) {
|
|
52
|
+
// Use journalctl
|
|
53
|
+
const escapedService = shellEscape(critter.service);
|
|
54
|
+
cmd = `journalctl -u ${escapedService} -n ${escapedLines} --no-pager`;
|
|
55
|
+
if (pattern) {
|
|
56
|
+
const escapedPattern = shellEscape(pattern);
|
|
57
|
+
cmd += ` | grep -i ${escapedPattern} || true`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (critter.log_path) {
|
|
61
|
+
// Use custom log path
|
|
62
|
+
const escapedLogPath = shellEscape(critter.log_path);
|
|
63
|
+
cmd = `tail -n ${escapedLines} ${escapedLogPath}`;
|
|
64
|
+
if (pattern) {
|
|
65
|
+
const escapedPattern = shellEscape(pattern);
|
|
66
|
+
cmd += ` | grep -i ${escapedPattern} || true`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
return { content: '', error: 'Critter has no log_path and use_journald is disabled' };
|
|
71
|
+
}
|
|
72
|
+
// Local barn
|
|
73
|
+
if (barn.name === 'local') {
|
|
74
|
+
try {
|
|
75
|
+
const result = await execa('sh', ['-c', cmd]);
|
|
76
|
+
if (!result.stdout.trim()) {
|
|
77
|
+
return { content: '', error: `No logs found for ${critter.name}` };
|
|
78
|
+
}
|
|
79
|
+
return { content: result.stdout };
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return { content: '', error: `Failed to read logs: ${getErrorMessage(err)}` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Remote barn - SSH
|
|
86
|
+
if (!barn.host || !barn.user) {
|
|
87
|
+
return { content: '', error: `Barn '${barn.name}' is not configured for SSH` };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const sshArgs = buildSshCommand(barn);
|
|
91
|
+
const result = await execa(sshArgs[0], [...sshArgs.slice(1), cmd]);
|
|
92
|
+
if (!result.stdout.trim()) {
|
|
93
|
+
return { content: '', error: `No logs found for ${critter.name}` };
|
|
94
|
+
}
|
|
95
|
+
return { content: result.stdout };
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return { content: '', error: `SSH error: ${getErrorMessage(err)}` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse systemctl show output into key-value pairs
|
|
103
|
+
*/
|
|
104
|
+
function parseSystemctlShow(output) {
|
|
105
|
+
const result = {};
|
|
106
|
+
for (const line of output.split('\n')) {
|
|
107
|
+
const eqIndex = line.indexOf('=');
|
|
108
|
+
if (eqIndex !== -1) {
|
|
109
|
+
const key = line.slice(0, eqIndex);
|
|
110
|
+
const value = line.slice(eqIndex + 1);
|
|
111
|
+
result[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Discover critters (running services) on a barn
|
|
118
|
+
*/
|
|
119
|
+
export async function discoverCritters(barn) {
|
|
120
|
+
// Command to list running services
|
|
121
|
+
const listCmd = 'systemctl list-units --type=service --state=running --no-pager --plain';
|
|
122
|
+
let output;
|
|
123
|
+
// Local barn
|
|
124
|
+
if (barn.name === 'local') {
|
|
125
|
+
try {
|
|
126
|
+
const result = await execa('sh', ['-c', listCmd]);
|
|
127
|
+
output = result.stdout;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return { critters: [], error: `Failed to list services: ${getErrorMessage(err)}` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Remote barn - SSH
|
|
135
|
+
if (!barn.host || !barn.user) {
|
|
136
|
+
return { critters: [], error: `Barn '${barn.name}' is not configured for SSH` };
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const sshArgs = buildSshCommand(barn);
|
|
140
|
+
const result = await execa(sshArgs[0], [...sshArgs.slice(1), listCmd]);
|
|
141
|
+
output = result.stdout;
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
return { critters: [], error: `SSH error: ${getErrorMessage(err)}` };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Parse the output - each line: "unit.service loaded active running description"
|
|
148
|
+
const lines = output.split('\n').filter(line => line.trim());
|
|
149
|
+
const discovered = [];
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
const parts = line.trim().split(/\s+/);
|
|
152
|
+
if (parts.length < 1)
|
|
153
|
+
continue;
|
|
154
|
+
const service = parts[0];
|
|
155
|
+
if (!service.endsWith('.service'))
|
|
156
|
+
continue;
|
|
157
|
+
if (!isInterestingService(service))
|
|
158
|
+
continue;
|
|
159
|
+
const suggested_name = extractServiceName(service);
|
|
160
|
+
// Try to get more details about this service
|
|
161
|
+
let binary;
|
|
162
|
+
let config_path;
|
|
163
|
+
const showCmd = `systemctl show ${shellEscape(service)} --property=ExecStart`;
|
|
164
|
+
try {
|
|
165
|
+
let showOutput;
|
|
166
|
+
if (barn.name === 'local') {
|
|
167
|
+
const showResult = await execa('sh', ['-c', showCmd]);
|
|
168
|
+
showOutput = showResult.stdout;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const sshArgs = buildSshCommand(barn);
|
|
172
|
+
const showResult = await execa(sshArgs[0], [...sshArgs.slice(1), showCmd]);
|
|
173
|
+
showOutput = showResult.stdout;
|
|
174
|
+
}
|
|
175
|
+
const props = parseSystemctlShow(showOutput);
|
|
176
|
+
// Extract binary from ExecStart (format: { path=/usr/bin/mysqld ; argv[]=... })
|
|
177
|
+
if (props.ExecStart) {
|
|
178
|
+
const pathMatch = props.ExecStart.match(/path=([^\s;]+)/);
|
|
179
|
+
if (pathMatch) {
|
|
180
|
+
binary = pathMatch[1];
|
|
181
|
+
}
|
|
182
|
+
// Try to find config flags like --config= or -c
|
|
183
|
+
const configMatch = props.ExecStart.match(/--config[=\s]([^\s;]+)/);
|
|
184
|
+
if (configMatch) {
|
|
185
|
+
config_path = configMatch[1];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Ignore errors getting details - just use basic info
|
|
191
|
+
}
|
|
192
|
+
discovered.push({
|
|
193
|
+
service,
|
|
194
|
+
suggested_name,
|
|
195
|
+
binary,
|
|
196
|
+
config_path,
|
|
197
|
+
status: 'running',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return { critters: discovered };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* List systemd services on a barn
|
|
204
|
+
* @param barn - The barn to query
|
|
205
|
+
* @param activeOnly - If true, only return running services (default: true)
|
|
206
|
+
*/
|
|
207
|
+
export async function listSystemServices(barn, activeOnly = true) {
|
|
208
|
+
// For active only: list-units shows running services
|
|
209
|
+
// For all: list-unit-files shows all installed services
|
|
210
|
+
const cmd = activeOnly
|
|
211
|
+
? 'systemctl list-units --type=service --state=running --no-pager --no-legend'
|
|
212
|
+
: 'systemctl list-unit-files --type=service --no-pager --no-legend';
|
|
213
|
+
let output;
|
|
214
|
+
// Local barn
|
|
215
|
+
if (barn.name === 'local') {
|
|
216
|
+
try {
|
|
217
|
+
const result = await execa('sh', ['-c', cmd]);
|
|
218
|
+
output = result.stdout;
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
return { services: [], error: `Failed to list services: ${getErrorMessage(err)}` };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Remote barn - SSH
|
|
226
|
+
if (!barn.host || !barn.user) {
|
|
227
|
+
return { services: [], error: `Barn '${barn.name}' is not configured for SSH` };
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const sshArgs = buildSshCommand(barn);
|
|
231
|
+
const result = await execa(sshArgs[0], [...sshArgs.slice(1), cmd]);
|
|
232
|
+
output = result.stdout;
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
return { services: [], error: `SSH error: ${getErrorMessage(err)}` };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const services = [];
|
|
239
|
+
const lines = output.split('\n').filter(line => line.trim());
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
const parts = line.trim().split(/\s+/);
|
|
242
|
+
if (parts.length < 1)
|
|
243
|
+
continue;
|
|
244
|
+
const serviceName = parts[0];
|
|
245
|
+
if (!serviceName.endsWith('.service'))
|
|
246
|
+
continue;
|
|
247
|
+
if (activeOnly) {
|
|
248
|
+
// list-units format: UNIT LOAD ACTIVE SUB DESCRIPTION...
|
|
249
|
+
services.push({
|
|
250
|
+
name: serviceName,
|
|
251
|
+
state: 'running',
|
|
252
|
+
description: parts.slice(4).join(' ') || undefined,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// list-unit-files format: UNIT STATE PRESET
|
|
257
|
+
const stateStr = parts[1]?.toLowerCase() || '';
|
|
258
|
+
services.push({
|
|
259
|
+
name: serviceName,
|
|
260
|
+
state: stateStr === 'enabled' ? 'unknown' : 'stopped',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { services };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get details about a systemd service by parsing its unit file
|
|
268
|
+
*/
|
|
269
|
+
export async function getServiceDetails(barn, serviceName) {
|
|
270
|
+
// Get the unit file path
|
|
271
|
+
const pathCmd = `systemctl show -p FragmentPath ${shellEscape(serviceName)} --value`;
|
|
272
|
+
let servicePath;
|
|
273
|
+
if (barn.name === 'local') {
|
|
274
|
+
try {
|
|
275
|
+
const result = await execa('sh', ['-c', pathCmd]);
|
|
276
|
+
servicePath = result.stdout.trim();
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
return { error: `Failed to get service path: ${getErrorMessage(err)}` };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
if (!barn.host || !barn.user) {
|
|
284
|
+
return { error: `Barn '${barn.name}' is not configured for SSH` };
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const sshArgs = buildSshCommand(barn);
|
|
288
|
+
const result = await execa(sshArgs[0], [...sshArgs.slice(1), pathCmd]);
|
|
289
|
+
servicePath = result.stdout.trim();
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
return { error: `SSH error: ${getErrorMessage(err)}` };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!servicePath) {
|
|
296
|
+
return { error: 'Could not find service unit file' };
|
|
297
|
+
}
|
|
298
|
+
// Read the service file to extract details
|
|
299
|
+
const catCmd = `cat ${shellEscape(servicePath)}`;
|
|
300
|
+
let serviceContent;
|
|
301
|
+
if (barn.name === 'local') {
|
|
302
|
+
try {
|
|
303
|
+
const result = await execa('sh', ['-c', catCmd]);
|
|
304
|
+
serviceContent = result.stdout;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
return { error: `Failed to read service file: ${getErrorMessage(err)}` };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
try {
|
|
312
|
+
const sshArgs = buildSshCommand(barn);
|
|
313
|
+
const result = await execa(sshArgs[0], [...sshArgs.slice(1), catCmd]);
|
|
314
|
+
serviceContent = result.stdout;
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
return { error: `SSH error: ${getErrorMessage(err)}` };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Parse the service file
|
|
321
|
+
let config_path;
|
|
322
|
+
let log_path;
|
|
323
|
+
let use_journald = true;
|
|
324
|
+
for (const line of serviceContent.split('\n')) {
|
|
325
|
+
const trimmed = line.trim();
|
|
326
|
+
// Look for ExecStart to find config flags
|
|
327
|
+
if (trimmed.startsWith('ExecStart=')) {
|
|
328
|
+
const execLine = trimmed.slice('ExecStart='.length);
|
|
329
|
+
// Common config flag patterns
|
|
330
|
+
const configPatterns = [
|
|
331
|
+
/--config[=\s]([^\s]+)/,
|
|
332
|
+
/--defaults-file[=\s]([^\s]+)/,
|
|
333
|
+
/-c\s+([^\s]+)/,
|
|
334
|
+
/--conf[=\s]([^\s]+)/,
|
|
335
|
+
];
|
|
336
|
+
for (const pattern of configPatterns) {
|
|
337
|
+
const match = execLine.match(pattern);
|
|
338
|
+
if (match) {
|
|
339
|
+
config_path = match[1];
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Check StandardOutput/StandardError for log paths
|
|
345
|
+
if (trimmed.startsWith('StandardOutput=') || trimmed.startsWith('StandardError=')) {
|
|
346
|
+
const value = trimmed.split('=')[1];
|
|
347
|
+
if (value && !value.startsWith('journal') && !value.startsWith('inherit')) {
|
|
348
|
+
// Could be file:path or append:path
|
|
349
|
+
const fileMatch = value.match(/(?:file|append):(.+)/);
|
|
350
|
+
if (fileMatch) {
|
|
351
|
+
log_path = fileMatch[1];
|
|
352
|
+
use_journald = false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
details: {
|
|
359
|
+
service_path: servicePath,
|
|
360
|
+
config_path,
|
|
361
|
+
log_path,
|
|
362
|
+
use_journald,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install the Claude hook script to ~/.yeehaw/bin/
|
|
3
|
+
*/
|
|
4
|
+
export declare function installHookScript(): string;
|
|
5
|
+
/**
|
|
6
|
+
* Get the path to the hook script
|
|
7
|
+
*/
|
|
8
|
+
export declare function getHookScriptPath(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Get the Claude settings.json hooks configuration
|
|
11
|
+
*/
|
|
12
|
+
export declare function getClaudeHooksConfig(): object;
|
|
13
|
+
/**
|
|
14
|
+
* Check if Claude hooks are already configured
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkClaudeHooksInstalled(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Check if hook script exists
|
|
19
|
+
*/
|
|
20
|
+
export declare function hookScriptExists(): boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { HOOKS_DIR, SIGNALS_DIR } from './paths.js';
|
|
5
|
+
const HOOK_SCRIPT_NAME = 'claude-hook';
|
|
6
|
+
const HOOK_SCRIPT_CONTENT = `#!/bin/bash
|
|
7
|
+
# Yeehaw Claude Hook - writes session status for the CLI to read
|
|
8
|
+
STATUS="$1"
|
|
9
|
+
PANE_ID="\${TMUX_PANE:-unknown}"
|
|
10
|
+
SIGNAL_DIR="$HOME/.yeehaw/session-signals"
|
|
11
|
+
SIGNAL_FILE="$SIGNAL_DIR/\${PANE_ID//[^a-zA-Z0-9]/_}.json"
|
|
12
|
+
|
|
13
|
+
mkdir -p "$SIGNAL_DIR"
|
|
14
|
+
cat > "$SIGNAL_FILE" << EOF
|
|
15
|
+
{"status":"$STATUS","updated":$(date +%s)}
|
|
16
|
+
EOF
|
|
17
|
+
`;
|
|
18
|
+
/**
|
|
19
|
+
* Install the Claude hook script to ~/.yeehaw/bin/
|
|
20
|
+
*/
|
|
21
|
+
export function installHookScript() {
|
|
22
|
+
// Ensure directories exist
|
|
23
|
+
if (!existsSync(HOOKS_DIR)) {
|
|
24
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
if (!existsSync(SIGNALS_DIR)) {
|
|
27
|
+
mkdirSync(SIGNALS_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
const scriptPath = join(HOOKS_DIR, HOOK_SCRIPT_NAME);
|
|
30
|
+
writeFileSync(scriptPath, HOOK_SCRIPT_CONTENT, 'utf-8');
|
|
31
|
+
chmodSync(scriptPath, 0o755);
|
|
32
|
+
return scriptPath;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the path to the hook script
|
|
36
|
+
*/
|
|
37
|
+
export function getHookScriptPath() {
|
|
38
|
+
return join(HOOKS_DIR, HOOK_SCRIPT_NAME);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the Claude settings.json hooks configuration
|
|
42
|
+
*/
|
|
43
|
+
export function getClaudeHooksConfig() {
|
|
44
|
+
const hookPath = join(HOOKS_DIR, HOOK_SCRIPT_NAME);
|
|
45
|
+
return {
|
|
46
|
+
hooks: {
|
|
47
|
+
PreToolUse: [
|
|
48
|
+
{
|
|
49
|
+
matcher: '*',
|
|
50
|
+
hooks: [`${hookPath} working`],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
Stop: [
|
|
54
|
+
{
|
|
55
|
+
matcher: '*',
|
|
56
|
+
hooks: [`${hookPath} waiting`],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
Notification: [
|
|
60
|
+
{
|
|
61
|
+
matcher: 'idle_prompt',
|
|
62
|
+
hooks: [`${hookPath} waiting`],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if Claude hooks are already configured
|
|
70
|
+
*/
|
|
71
|
+
export function checkClaudeHooksInstalled() {
|
|
72
|
+
const claudeSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
73
|
+
if (!existsSync(claudeSettingsPath)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const content = readFileSync(claudeSettingsPath, 'utf-8');
|
|
78
|
+
const settings = JSON.parse(content);
|
|
79
|
+
return settings.hooks?.PreToolUse?.some((h) => h.hooks?.some((cmd) => cmd.includes('yeehaw')));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if hook script exists
|
|
87
|
+
*/
|
|
88
|
+
export function hookScriptExists() {
|
|
89
|
+
const scriptPath = join(HOOKS_DIR, HOOK_SCRIPT_NAME);
|
|
90
|
+
return existsSync(scriptPath);
|
|
91
|
+
}
|
package/dist/lib/hotkeys.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type HotkeyScope = 'global' | 'global-dashboard' | 'project-context' | 'barn-context' | 'wiki-view' | 'issues-view' | 'livestock-detail' | 'logs-view' | 'night-sky' | 'list' | 'content';
|
|
1
|
+
export type HotkeyScope = 'global' | 'global-dashboard' | 'project-context' | 'barn-context' | 'wiki-view' | 'issues-view' | 'livestock-detail' | 'logs-view' | 'critter-detail' | 'critter-logs' | 'night-sky' | 'list' | 'content';
|
|
2
2
|
export type HotkeyCategory = 'navigation' | 'action' | 'system';
|
|
3
3
|
export interface Hotkey {
|
|
4
4
|
key: string;
|
package/dist/lib/hotkeys.js
CHANGED
|
@@ -2,38 +2,46 @@
|
|
|
2
2
|
export const HOTKEYS = [
|
|
3
3
|
// === GLOBAL (everywhere) ===
|
|
4
4
|
{ key: '?', description: 'Toggle help', category: 'system', scopes: ['global'] },
|
|
5
|
-
{ key: 'q', description: '
|
|
5
|
+
{ key: 'q', description: 'Detach', category: 'system', scopes: ['global-dashboard'] },
|
|
6
|
+
{ key: 'Q', description: 'Quit Yeehaw', category: 'system', scopes: ['global-dashboard'] },
|
|
6
7
|
{ key: 'Esc', description: 'Back / Cancel', category: 'system', scopes: ['global'] },
|
|
7
8
|
// === LIST NAVIGATION (any focused list) ===
|
|
8
|
-
{ key: 'j/k', description: '
|
|
9
|
-
{ key: 'g/G', description: '
|
|
10
|
-
{ key: 'Enter', description: '
|
|
9
|
+
{ key: 'j/k', description: 'Move up/down', category: 'navigation', scopes: ['list'] },
|
|
10
|
+
{ key: 'g/G', description: 'Jump to first/last', category: 'navigation', scopes: ['list'] },
|
|
11
|
+
{ key: 'Enter', description: 'Open selected item', category: 'navigation', scopes: ['list'] },
|
|
11
12
|
// === CONTENT NAVIGATION (markdown panels) ===
|
|
12
13
|
{ key: 'j/k', description: 'Scroll up/down', category: 'navigation', scopes: ['content'] },
|
|
13
14
|
{ key: 'g/G', description: 'Jump to top/bottom', category: 'navigation', scopes: ['content'] },
|
|
14
15
|
{ key: 'PgUp/PgDn', description: 'Scroll page', category: 'navigation', scopes: ['content'] },
|
|
15
|
-
// ===
|
|
16
|
+
// === PANEL NAVIGATION ===
|
|
16
17
|
{ key: 'Tab', description: 'Switch panel', category: 'navigation', scopes: ['global-dashboard', 'project-context', 'barn-context', 'wiki-view', 'issues-view'] },
|
|
17
|
-
|
|
18
|
-
{ key: '
|
|
19
|
-
|
|
20
|
-
{ key: '
|
|
21
|
-
|
|
18
|
+
// === GLOBAL SESSION SWITCHING ===
|
|
19
|
+
{ key: '1-9', description: 'Switch to session', category: 'action', scopes: ['global-dashboard', 'project-context'] },
|
|
20
|
+
// === ROW-LEVEL ACTIONS (shown on selected rows) ===
|
|
21
|
+
{ key: 'c', description: 'Claude session (at path)', category: 'action', scopes: ['global-dashboard'], panel: 'projects' },
|
|
22
|
+
{ key: 'c', description: 'Claude session (at path)', category: 'action', scopes: ['project-context'], panel: 'livestock' },
|
|
23
|
+
{ key: 's', description: 'Shell into server', category: 'action', scopes: ['global-dashboard'], panel: 'barns' },
|
|
24
|
+
{ key: 's', description: 'Shell into server', category: 'action', scopes: ['project-context', 'barn-context'], panel: 'livestock' },
|
|
25
|
+
// === CARD/PAGE-LEVEL ACTIONS ===
|
|
22
26
|
{ key: 'n', description: 'New (in focused panel)', category: 'action', scopes: ['global-dashboard', 'project-context', 'barn-context', 'wiki-view'] },
|
|
23
|
-
{ key: 'e', description: 'Edit
|
|
24
|
-
{ key: 'd', description: 'Delete
|
|
25
|
-
{ key: 'D', description: 'Delete container
|
|
26
|
-
// === PROJECT CONTEXT ===
|
|
27
|
+
{ key: 'e', description: 'Edit', category: 'action', scopes: ['project-context', 'barn-context', 'wiki-view', 'livestock-detail'] },
|
|
28
|
+
{ key: 'd', description: 'Delete', category: 'action', scopes: ['project-context', 'barn-context', 'wiki-view'] },
|
|
29
|
+
{ key: 'D', description: 'Delete container', category: 'action', scopes: ['project-context', 'barn-context'] },
|
|
30
|
+
// === PROJECT CONTEXT PAGE-LEVEL ===
|
|
27
31
|
{ key: 'w', description: 'Open wiki', category: 'action', scopes: ['project-context'] },
|
|
28
32
|
{ key: 'i', description: 'Open issues', category: 'action', scopes: ['project-context'] },
|
|
33
|
+
// === LIVESTOCK DETAIL PAGE-LEVEL ===
|
|
34
|
+
{ key: 'c', description: 'Claude session (local only)', category: 'action', scopes: ['livestock-detail'] },
|
|
35
|
+
{ key: 's', description: 'Shell session', category: 'action', scopes: ['livestock-detail'] },
|
|
36
|
+
{ key: 'l', description: 'View logs', category: 'action', scopes: ['livestock-detail'] },
|
|
37
|
+
// === CRITTER DETAIL PAGE-LEVEL ===
|
|
38
|
+
{ key: 'l', description: 'View logs', category: 'action', scopes: ['critter-detail'] },
|
|
39
|
+
{ key: 'e', description: 'Edit', category: 'action', scopes: ['critter-detail'] },
|
|
29
40
|
// === ISSUES VIEW ===
|
|
30
|
-
{ key: 'r', description: 'Refresh
|
|
41
|
+
{ key: 'r', description: 'Refresh', category: 'action', scopes: ['issues-view', 'logs-view', 'critter-logs'] },
|
|
31
42
|
{ key: 'o', description: 'Open in browser', category: 'action', scopes: ['issues-view'] },
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// === LOGS VIEW ===
|
|
35
|
-
{ key: 'r', description: 'Refresh logs', category: 'action', scopes: ['logs-view'] },
|
|
36
|
-
// === NIGHT SKY (screensaver) ===
|
|
43
|
+
{ key: 'c', description: 'Open in Claude', category: 'action', scopes: ['issues-view'] },
|
|
44
|
+
// === NIGHT SKY ===
|
|
37
45
|
{ key: 'v', description: 'Visualizer', category: 'navigation', scopes: ['global-dashboard'] },
|
|
38
46
|
{ key: 'c', description: 'Spawn cloud', category: 'action', scopes: ['night-sky'] },
|
|
39
47
|
{ key: 'r', description: 'Randomize', category: 'action', scopes: ['night-sky'] },
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Issue, IssueProvider, FetchIssuesOptions } from './types.js';
|
|
2
|
+
import type { Livestock } from '../../types.js';
|
|
3
|
+
export declare class GitHubProvider implements IssueProvider {
|
|
4
|
+
readonly type: "github";
|
|
5
|
+
private repos;
|
|
6
|
+
constructor(livestock: Livestock[]);
|
|
7
|
+
isAuthenticated(): Promise<boolean>;
|
|
8
|
+
authenticate(): Promise<void>;
|
|
9
|
+
fetchIssues(options?: FetchIssuesOptions): Promise<Issue[]>;
|
|
10
|
+
getIssue(id: string): Promise<Issue>;
|
|
11
|
+
}
|