@colmbus72/yeehaw 0.4.2 → 0.5.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/dist/app.js +80 -31
- package/dist/components/HelpOverlay.js +4 -2
- package/dist/components/List.d.ts +8 -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/config.d.ts +13 -1
- package/dist/lib/config.js +51 -0
- package/dist/lib/critters.d.ts +28 -0
- package/dist/lib/critters.js +201 -0
- package/dist/lib/hooks.d.ts +20 -0
- package/dist/lib/hooks.js +91 -0
- package/dist/lib/hotkeys.js +24 -20
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.js +2 -0
- package/dist/lib/signals.d.ts +30 -0
- package/dist/lib/signals.js +104 -0
- package/dist/lib/tmux.d.ts +8 -2
- package/dist/lib/tmux.js +69 -18
- package/dist/mcp-server.js +161 -1
- package/dist/types.d.ts +4 -2
- package/dist/views/BarnContext.d.ts +4 -2
- package/dist/views/BarnContext.js +79 -20
- package/dist/views/GlobalDashboard.d.ts +2 -2
- package/dist/views/GlobalDashboard.js +20 -18
- package/dist/views/LivestockDetailView.js +11 -7
- package/dist/views/ProjectContext.d.ts +2 -2
- package/dist/views/ProjectContext.js +33 -24
- package/package.json +5 -5
package/dist/lib/tmux.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { writeTmuxConfig, TMUX_CONFIG_PATH } from './tmux-config.js';
|
|
6
6
|
import { shellEscape } from './shell.js';
|
|
7
|
+
import { readSignal, getStatusIcon } from './signals.js';
|
|
7
8
|
// Get the path to the MCP server (it's in dist/, not dist/lib/)
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = dirname(__filename);
|
|
@@ -147,6 +148,15 @@ const YEEHAW_MCP_TOOLS = [
|
|
|
147
148
|
'mcp__yeehaw__update_wiki_section',
|
|
148
149
|
'mcp__yeehaw__delete_wiki_section',
|
|
149
150
|
];
|
|
151
|
+
/**
|
|
152
|
+
* Set the window type option for a window (used for reliable window type detection)
|
|
153
|
+
*/
|
|
154
|
+
function setWindowType(windowIndex, type) {
|
|
155
|
+
execaSync('tmux', [
|
|
156
|
+
'set-option', '-w', '-t', `${YEEHAW_SESSION}:${windowIndex}`,
|
|
157
|
+
'@yeehaw_type', type,
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
150
160
|
export function createClaudeWindow(workingDir, windowName) {
|
|
151
161
|
// Build MCP config for yeehaw server
|
|
152
162
|
const mcpConfig = JSON.stringify({
|
|
@@ -175,7 +185,10 @@ export function createClaudeWindow(workingDir, windowName) {
|
|
|
175
185
|
const result = execaSync('tmux', [
|
|
176
186
|
'display-message', '-p', '#{window_index}'
|
|
177
187
|
]);
|
|
178
|
-
|
|
188
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
189
|
+
// Mark this window as a Claude session
|
|
190
|
+
setWindowType(windowIndex, 'claude');
|
|
191
|
+
return windowIndex;
|
|
179
192
|
}
|
|
180
193
|
export function createShellWindow(workingDir, windowName, shell) {
|
|
181
194
|
// Use the user's configured shell from $SHELL, fallback to /bin/bash
|
|
@@ -194,7 +207,10 @@ export function createShellWindow(workingDir, windowName, shell) {
|
|
|
194
207
|
const result = execaSync('tmux', [
|
|
195
208
|
'display-message', '-p', '#{window_index}'
|
|
196
209
|
]);
|
|
197
|
-
|
|
210
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
211
|
+
// Mark this window as a shell session
|
|
212
|
+
setWindowType(windowIndex, 'shell');
|
|
213
|
+
return windowIndex;
|
|
198
214
|
}
|
|
199
215
|
export function createSshWindow(windowName, host, user, port, identityFile, remotePath) {
|
|
200
216
|
// Two levels of escaping needed:
|
|
@@ -222,7 +238,10 @@ export function createSshWindow(windowName, host, user, port, identityFile, remo
|
|
|
222
238
|
const result = execaSync('tmux', [
|
|
223
239
|
'display-message', '-p', '#{window_index}'
|
|
224
240
|
]);
|
|
225
|
-
|
|
241
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
242
|
+
// Mark this window as an SSH session
|
|
243
|
+
setWindowType(windowIndex, 'ssh');
|
|
244
|
+
return windowIndex;
|
|
226
245
|
}
|
|
227
246
|
export function detachFromSession() {
|
|
228
247
|
execaSync('tmux', ['detach-client']);
|
|
@@ -235,29 +254,37 @@ export function killYeehawSession() {
|
|
|
235
254
|
// Session might already be dead
|
|
236
255
|
}
|
|
237
256
|
}
|
|
257
|
+
export function restartYeehaw() {
|
|
258
|
+
// Respawn window 0 with a fresh yeehaw process
|
|
259
|
+
// This kills the current process but preserves all other windows
|
|
260
|
+
execaSync('tmux', ['respawn-window', '-k', '-t', `${YEEHAW_SESSION}:0`, 'yeehaw']);
|
|
261
|
+
}
|
|
238
262
|
export function switchToWindow(windowIndex) {
|
|
239
263
|
execaSync('tmux', ['select-window', '-t', `${YEEHAW_SESSION}:${windowIndex}`]);
|
|
240
264
|
}
|
|
241
265
|
export function listYeehawWindows() {
|
|
242
266
|
try {
|
|
243
267
|
// Use tab as delimiter since pane_title can contain colons
|
|
268
|
+
// Include @yeehaw_type window option for reliable window type detection
|
|
244
269
|
const result = execaSync('tmux', [
|
|
245
270
|
'list-windows',
|
|
246
271
|
'-t', YEEHAW_SESSION,
|
|
247
|
-
'-F', '#{window_index}\t#{window_name}\t#{window_active}\t#{pane_title}\t#{pane_current_command}\t#{window_activity}',
|
|
272
|
+
'-F', '#{window_index}\t#{window_name}\t#{window_active}\t#{pane_id}\t#{pane_title}\t#{pane_current_command}\t#{window_activity}\t#{@yeehaw_type}',
|
|
248
273
|
]);
|
|
249
274
|
return result.stdout
|
|
250
275
|
.split('\n')
|
|
251
276
|
.filter(Boolean)
|
|
252
277
|
.map((line) => {
|
|
253
|
-
const [index, name, active, paneTitle, paneCurrentCommand, windowActivity] = line.split('\t');
|
|
278
|
+
const [index, name, active, paneId, paneTitle, paneCurrentCommand, windowActivity, type] = line.split('\t');
|
|
254
279
|
return {
|
|
255
280
|
index: parseInt(index, 10),
|
|
256
281
|
name,
|
|
257
282
|
active: active === '1',
|
|
283
|
+
paneId: paneId || '',
|
|
258
284
|
paneTitle: paneTitle || '',
|
|
259
285
|
paneCurrentCommand: paneCurrentCommand || '',
|
|
260
286
|
windowActivity: parseInt(windowActivity, 10) || 0,
|
|
287
|
+
type: (type || ''),
|
|
261
288
|
};
|
|
262
289
|
});
|
|
263
290
|
}
|
|
@@ -296,34 +323,58 @@ function isClaudeWorking(paneTitle) {
|
|
|
296
323
|
return spinnerChars.some(char => paneTitle.startsWith(char));
|
|
297
324
|
}
|
|
298
325
|
/**
|
|
299
|
-
* Get formatted status
|
|
326
|
+
* Get formatted status info for a tmux window
|
|
300
327
|
*/
|
|
301
328
|
export function getWindowStatus(window) {
|
|
302
|
-
const isClaudeSession = window.
|
|
329
|
+
const isClaudeSession = window.type === 'claude';
|
|
303
330
|
const relativeTime = formatRelativeTime(window.windowActivity);
|
|
331
|
+
// Check for signal file first (written by Claude hooks)
|
|
332
|
+
if (isClaudeSession && window.paneId) {
|
|
333
|
+
const signal = readSignal(window.paneId);
|
|
334
|
+
if (signal) {
|
|
335
|
+
const icon = getStatusIcon(signal.status);
|
|
336
|
+
const text = signal.status === 'waiting'
|
|
337
|
+
? 'Waiting for input'
|
|
338
|
+
: signal.status === 'working'
|
|
339
|
+
? window.paneTitle || 'Working...'
|
|
340
|
+
: signal.status === 'error'
|
|
341
|
+
? 'Error'
|
|
342
|
+
: `idle ${relativeTime}`;
|
|
343
|
+
return { text: `${icon} ${text}`, status: signal.status, icon };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Fallback to tmux-native detection for Claude sessions
|
|
304
347
|
if (isClaudeSession) {
|
|
305
|
-
// For Claude sessions, use pane title (contains task context)
|
|
306
348
|
if (window.paneTitle) {
|
|
307
349
|
const working = isClaudeWorking(window.paneTitle);
|
|
308
350
|
if (working) {
|
|
309
|
-
return
|
|
351
|
+
return {
|
|
352
|
+
text: window.paneTitle,
|
|
353
|
+
status: 'working',
|
|
354
|
+
icon: getStatusIcon('working'),
|
|
355
|
+
};
|
|
310
356
|
}
|
|
311
|
-
// Not working -
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return
|
|
357
|
+
// Not working - likely idle
|
|
358
|
+
const text = relativeTime !== 'now' && relativeTime !== '1m'
|
|
359
|
+
? `${window.paneTitle} (${relativeTime})`
|
|
360
|
+
: window.paneTitle;
|
|
361
|
+
return { text, status: 'idle', icon: getStatusIcon('idle') };
|
|
316
362
|
}
|
|
317
|
-
|
|
363
|
+
const text = relativeTime === 'now' ? 'active' : `idle ${relativeTime}`;
|
|
364
|
+
return { text: `○ ${text}`, status: 'idle', icon: '○' };
|
|
365
|
+
}
|
|
366
|
+
// For shell sessions, check if pane is dead
|
|
367
|
+
if (window.paneCurrentCommand === '') {
|
|
368
|
+
return { text: '✖ disconnected', status: 'error', icon: '✖' };
|
|
318
369
|
}
|
|
319
370
|
// For shell sessions, show current command
|
|
320
371
|
const cmd = window.paneCurrentCommand;
|
|
321
372
|
if (cmd && cmd !== 'zsh' && cmd !== 'bash' && cmd !== 'sh' && cmd !== 'fish') {
|
|
322
|
-
|
|
323
|
-
return cmd;
|
|
373
|
+
return { text: cmd, status: 'working', icon: getStatusIcon('working') };
|
|
324
374
|
}
|
|
325
375
|
// At shell prompt - show idle time
|
|
326
|
-
|
|
376
|
+
const text = relativeTime === 'now' ? 'ready' : `idle ${relativeTime}`;
|
|
377
|
+
return { text: `○ ${text}`, status: 'idle', icon: '○' };
|
|
327
378
|
}
|
|
328
379
|
export function updateStatusBar(projectName) {
|
|
329
380
|
const left = projectName
|
package/dist/mcp-server.js
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
24
24
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
25
25
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
26
|
-
import { loadProjects, loadProject, loadBarns, loadBarn, saveProject, saveBarn, deleteProject, deleteBarn, getLivestockForBarn, ensureConfigDirs, } from './lib/config.js';
|
|
26
|
+
import { loadProjects, loadProject, loadBarns, loadBarn, saveProject, saveBarn, deleteProject, deleteBarn, getLivestockForBarn, ensureConfigDirs, addCritterToBarn, removeCritterFromBarn, getCritter, } from './lib/config.js';
|
|
27
27
|
import { readLivestockLogs, readLivestockEnv } from './lib/livestock.js';
|
|
28
|
+
import { readCritterLogs, discoverCritters } from './lib/critters.js';
|
|
28
29
|
import { requireString, optionalString, optionalNumber, optionalBoolean, } from './lib/mcp-validation.js';
|
|
29
30
|
const server = new Server({
|
|
30
31
|
name: 'yeehaw',
|
|
@@ -381,6 +382,60 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
381
382
|
required: ['project', 'title'],
|
|
382
383
|
},
|
|
383
384
|
},
|
|
385
|
+
// Critter tools
|
|
386
|
+
{
|
|
387
|
+
name: 'add_critter',
|
|
388
|
+
description: 'Add a critter (system service like mysql, redis, nginx) to a barn',
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
barn: { type: 'string', description: 'Barn name' },
|
|
393
|
+
name: { type: 'string', description: 'Critter name (user-friendly, e.g., "mysql", "redis-cache")' },
|
|
394
|
+
service: { type: 'string', description: 'systemd service name (e.g., "mysql.service")' },
|
|
395
|
+
config_path: { type: 'string', description: 'Path to config file (optional)' },
|
|
396
|
+
log_path: { type: 'string', description: 'Custom log path if not using journald (optional)' },
|
|
397
|
+
use_journald: { type: 'boolean', description: 'Use journalctl for logs (default: true)' },
|
|
398
|
+
},
|
|
399
|
+
required: ['barn', 'name', 'service'],
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'remove_critter',
|
|
404
|
+
description: 'Remove a critter from a barn',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: 'object',
|
|
407
|
+
properties: {
|
|
408
|
+
barn: { type: 'string', description: 'Barn name' },
|
|
409
|
+
name: { type: 'string', description: 'Critter name to remove' },
|
|
410
|
+
},
|
|
411
|
+
required: ['barn', 'name'],
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: 'read_critter_logs',
|
|
416
|
+
description: 'Read logs from a critter (via journald or custom path)',
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
properties: {
|
|
420
|
+
barn: { type: 'string', description: 'Barn name' },
|
|
421
|
+
critter: { type: 'string', description: 'Critter name' },
|
|
422
|
+
lines: { type: 'number', description: 'Last N lines (default: 100)' },
|
|
423
|
+
pattern: { type: 'string', description: 'Grep pattern to filter logs (case-insensitive)' },
|
|
424
|
+
},
|
|
425
|
+
required: ['barn', 'critter'],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'discover_critters',
|
|
430
|
+
description: 'Scan a barn for running services and return suggestions for critters to add',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
barn: { type: 'string', description: 'Barn name to scan' },
|
|
435
|
+
},
|
|
436
|
+
required: ['barn'],
|
|
437
|
+
},
|
|
438
|
+
},
|
|
384
439
|
],
|
|
385
440
|
};
|
|
386
441
|
});
|
|
@@ -851,6 +906,111 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
851
906
|
content: [{ type: 'text', text: `Deleted wiki section '${title}' from project '${project.name}'` }],
|
|
852
907
|
};
|
|
853
908
|
}
|
|
909
|
+
// Critter operations
|
|
910
|
+
case 'add_critter': {
|
|
911
|
+
const barnName = requireString(args, 'barn');
|
|
912
|
+
const critterName = requireString(args, 'name');
|
|
913
|
+
const service = requireString(args, 'service');
|
|
914
|
+
const config_path = optionalString(args, 'config_path');
|
|
915
|
+
const log_path = optionalString(args, 'log_path');
|
|
916
|
+
const use_journald = optionalBoolean(args, 'use_journald', true);
|
|
917
|
+
const critter = {
|
|
918
|
+
name: critterName,
|
|
919
|
+
service,
|
|
920
|
+
config_path,
|
|
921
|
+
log_path,
|
|
922
|
+
use_journald,
|
|
923
|
+
};
|
|
924
|
+
try {
|
|
925
|
+
addCritterToBarn(barnName, critter);
|
|
926
|
+
return {
|
|
927
|
+
content: [{ type: 'text', text: `Added critter '${critterName}' (${service}) to barn '${barnName}'` }],
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
return {
|
|
932
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
933
|
+
isError: true,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
case 'remove_critter': {
|
|
938
|
+
const barnName = requireString(args, 'barn');
|
|
939
|
+
const critterName = requireString(args, 'name');
|
|
940
|
+
try {
|
|
941
|
+
const removed = removeCritterFromBarn(barnName, critterName);
|
|
942
|
+
if (!removed) {
|
|
943
|
+
return {
|
|
944
|
+
content: [{ type: 'text', text: `Critter '${critterName}' not found on barn '${barnName}'` }],
|
|
945
|
+
isError: true,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
content: [{ type: 'text', text: `Removed critter '${critterName}' from barn '${barnName}'` }],
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
return {
|
|
954
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
955
|
+
isError: true,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
case 'read_critter_logs': {
|
|
960
|
+
const barnName = requireString(args, 'barn');
|
|
961
|
+
const critterName = requireString(args, 'critter');
|
|
962
|
+
const lines = optionalNumber(args, 'lines');
|
|
963
|
+
const pattern = optionalString(args, 'pattern');
|
|
964
|
+
const barn = loadBarn(barnName);
|
|
965
|
+
if (!barn) {
|
|
966
|
+
return {
|
|
967
|
+
content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
|
|
968
|
+
isError: true,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const critter = getCritter(barnName, critterName);
|
|
972
|
+
if (!critter) {
|
|
973
|
+
return {
|
|
974
|
+
content: [{ type: 'text', text: `Critter '${critterName}' not found on barn '${barnName}'` }],
|
|
975
|
+
isError: true,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const result = await readCritterLogs(critter, barn, { lines, pattern });
|
|
979
|
+
if (result.error) {
|
|
980
|
+
return {
|
|
981
|
+
content: [{ type: 'text', text: result.error }],
|
|
982
|
+
isError: true,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
content: [{ type: 'text', text: result.content }],
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
case 'discover_critters': {
|
|
990
|
+
const barnName = requireString(args, 'barn');
|
|
991
|
+
const barn = loadBarn(barnName);
|
|
992
|
+
if (!barn) {
|
|
993
|
+
return {
|
|
994
|
+
content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
|
|
995
|
+
isError: true,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const result = await discoverCritters(barn);
|
|
999
|
+
if (result.error) {
|
|
1000
|
+
return {
|
|
1001
|
+
content: [{ type: 'text', text: result.error }],
|
|
1002
|
+
isError: true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (result.critters.length === 0) {
|
|
1006
|
+
return {
|
|
1007
|
+
content: [{ type: 'text', text: 'No interesting services discovered on this barn' }],
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
content: [{ type: 'text', text: JSON.stringify(result.critters, null, 2) }],
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
854
1014
|
default:
|
|
855
1015
|
return {
|
|
856
1016
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/dist/types.d.ts
CHANGED
|
@@ -40,8 +40,10 @@ export interface Livestock {
|
|
|
40
40
|
}
|
|
41
41
|
export interface Critter {
|
|
42
42
|
name: string;
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
service: string;
|
|
44
|
+
config_path?: string;
|
|
45
|
+
log_path?: string;
|
|
46
|
+
use_journald?: boolean;
|
|
45
47
|
}
|
|
46
48
|
export interface Barn {
|
|
47
49
|
name: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Barn, Project, Livestock } from '../types.js';
|
|
1
|
+
import type { Barn, Project, Livestock, Critter } from '../types.js';
|
|
2
2
|
import type { TmuxWindow } from '../lib/tmux.js';
|
|
3
3
|
interface LivestockWithProject {
|
|
4
4
|
project: Project;
|
|
@@ -17,6 +17,8 @@ interface BarnContextProps {
|
|
|
17
17
|
onDeleteBarn: (barnName: string) => void;
|
|
18
18
|
onAddLivestock: (project: Project, livestock: Livestock) => void;
|
|
19
19
|
onRemoveLivestock: (project: Project, livestockName: string) => void;
|
|
20
|
+
onAddCritter: (critter: Critter) => void;
|
|
21
|
+
onRemoveCritter: (critterName: string) => void;
|
|
20
22
|
}
|
|
21
|
-
export declare function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, }: BarnContextProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export declare function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, onAddCritter, onRemoveCritter, }: BarnContextProps): import("react/jsx-runtime").JSX.Element;
|
|
22
24
|
export {};
|
|
@@ -9,7 +9,7 @@ import { PathInput } from '../components/PathInput.js';
|
|
|
9
9
|
import { detectRemoteGitInfo, detectGitInfo } from '../lib/git.js';
|
|
10
10
|
import { isLocalBarn } from '../lib/config.js';
|
|
11
11
|
import { parseGitHubUrl } from '../lib/github.js';
|
|
12
|
-
export function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, }) {
|
|
12
|
+
export function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, onAddCritter, onRemoveCritter, }) {
|
|
13
13
|
const [focusedPanel, setFocusedPanel] = useState('livestock');
|
|
14
14
|
const [mode, setMode] = useState('normal');
|
|
15
15
|
// Check if this is the local barn
|
|
@@ -28,6 +28,11 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
28
28
|
// Delete livestock state
|
|
29
29
|
const [selectedLivestockIndex, setSelectedLivestockIndex] = useState(0);
|
|
30
30
|
const [deleteLivestockTarget, setDeleteLivestockTarget] = useState(null);
|
|
31
|
+
// Critter state
|
|
32
|
+
const [selectedCritterIndex, setSelectedCritterIndex] = useState(0);
|
|
33
|
+
const [newCritterName, setNewCritterName] = useState('');
|
|
34
|
+
const [newCritterService, setNewCritterService] = useState('');
|
|
35
|
+
const [deleteCritterTarget, setDeleteCritterTarget] = useState(null);
|
|
31
36
|
// Filter windows that are barn sessions
|
|
32
37
|
const barnWindows = windows.filter((w) => w.index > 0 && w.name.startsWith(`barn-${barn.name}`));
|
|
33
38
|
const startEdit = () => {
|
|
@@ -80,24 +85,26 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
80
85
|
}
|
|
81
86
|
return;
|
|
82
87
|
}
|
|
88
|
+
// Handle delete critter confirmation
|
|
89
|
+
if (mode === 'delete-critter-confirm' && deleteCritterTarget) {
|
|
90
|
+
if (input === 'y') {
|
|
91
|
+
onRemoveCritter(deleteCritterTarget.name);
|
|
92
|
+
setDeleteCritterTarget(null);
|
|
93
|
+
setSelectedCritterIndex(Math.max(0, selectedCritterIndex - 1));
|
|
94
|
+
setMode('normal');
|
|
95
|
+
}
|
|
96
|
+
else if (input === 'n') {
|
|
97
|
+
setDeleteCritterTarget(null);
|
|
98
|
+
setMode('normal');
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
83
102
|
if (mode !== 'normal')
|
|
84
103
|
return;
|
|
85
104
|
if (key.tab) {
|
|
86
105
|
setFocusedPanel((p) => (p === 'livestock' ? 'critters' : 'livestock'));
|
|
87
106
|
return;
|
|
88
107
|
}
|
|
89
|
-
if (input === 's') {
|
|
90
|
-
// Context-aware shell: livestock session if focused on livestock, otherwise SSH to barn
|
|
91
|
-
if (focusedPanel === 'livestock' && livestock.length > 0) {
|
|
92
|
-
const target = livestock[selectedLivestockIndex];
|
|
93
|
-
if (target) {
|
|
94
|
-
onOpenLivestockSession(target.project, target.livestock);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
onSshToBarn();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
108
|
if (input === 'e' && !isLocal) {
|
|
102
109
|
startEdit();
|
|
103
110
|
return;
|
|
@@ -125,6 +132,23 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
125
132
|
}
|
|
126
133
|
return;
|
|
127
134
|
}
|
|
135
|
+
if (input === 'n' && focusedPanel === 'critters') {
|
|
136
|
+
// Start add critter flow
|
|
137
|
+
setNewCritterName('');
|
|
138
|
+
setNewCritterService('');
|
|
139
|
+
setMode('add-critter-name');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (input === 'd' && focusedPanel === 'critters' && (barn.critters || []).length > 0) {
|
|
143
|
+
// Delete selected critter
|
|
144
|
+
const critters = barn.critters || [];
|
|
145
|
+
const target = critters[selectedCritterIndex];
|
|
146
|
+
if (target) {
|
|
147
|
+
setDeleteCritterTarget(target);
|
|
148
|
+
setMode('delete-critter-confirm');
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
128
152
|
});
|
|
129
153
|
// Edit mode screens
|
|
130
154
|
if (mode === 'edit-host') {
|
|
@@ -220,29 +244,64 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
|
|
|
220
244
|
if (mode === 'delete-livestock-confirm' && deleteLivestockTarget) {
|
|
221
245
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Remove livestock" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Remove Livestock" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Remove \"", deleteLivestockTarget.livestock.name, "\" from ", deleteLivestockTarget.project.name, "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Path: ", deleteLivestockTarget.livestock.path] }) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: "red", bold: true, children: "[y] Yes, remove" }), _jsx(Text, { dimColor: true, children: "[n/Esc] Cancel" })] })] })] }));
|
|
222
246
|
}
|
|
247
|
+
// Add critter flow: name → service
|
|
248
|
+
if (mode === 'add-critter-name') {
|
|
249
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter to ", barn.name] }), _jsx(Text, { dimColor: true, children: "A critter is a system service like mysql, redis, nginx, etc." }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newCritterName, onChange: setNewCritterName, onSubmit: () => {
|
|
250
|
+
if (newCritterName.trim()) {
|
|
251
|
+
// Auto-suggest service name
|
|
252
|
+
setNewCritterService(`${newCritterName.trim()}.service`);
|
|
253
|
+
setMode('add-critter-service');
|
|
254
|
+
}
|
|
255
|
+
}, placeholder: "mysql, redis, nginx..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Esc: cancel" }) })] })] }));
|
|
256
|
+
}
|
|
257
|
+
if (mode === 'add-critter-service') {
|
|
258
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter: ", newCritterName] }), _jsx(Text, { dimColor: true, children: "Enter the systemd service name" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Service: " }), _jsx(TextInput, { value: newCritterService, onChange: setNewCritterService, onSubmit: () => {
|
|
259
|
+
if (newCritterService.trim()) {
|
|
260
|
+
const critter = {
|
|
261
|
+
name: newCritterName.trim(),
|
|
262
|
+
service: newCritterService.trim(),
|
|
263
|
+
use_journald: true,
|
|
264
|
+
};
|
|
265
|
+
onAddCritter(critter);
|
|
266
|
+
setMode('normal');
|
|
267
|
+
}
|
|
268
|
+
}, placeholder: "mysql.service, redis-server.service..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save, Esc: cancel" }) })] })] }));
|
|
269
|
+
}
|
|
270
|
+
// Delete critter confirmation
|
|
271
|
+
if (mode === 'delete-critter-confirm' && deleteCritterTarget) {
|
|
272
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Remove critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Remove Critter" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Remove \"", deleteCritterTarget.name, "\" from ", barn.name, "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Service: ", deleteCritterTarget.service] }) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: "red", bold: true, children: "[y] Yes, remove" }), _jsx(Text, { dimColor: true, children: "[n/Esc] Cancel" })] })] })] }));
|
|
273
|
+
}
|
|
223
274
|
// Build livestock items
|
|
224
275
|
const livestockItems = livestock.map((l) => ({
|
|
225
276
|
id: `${l.project.name}/${l.livestock.name}`,
|
|
226
277
|
label: `${l.project.name}/${l.livestock.name}`,
|
|
227
278
|
status: 'active',
|
|
228
279
|
meta: l.livestock.path,
|
|
280
|
+
actions: [{ key: 's', label: 'shell' }],
|
|
229
281
|
}));
|
|
230
282
|
// Build critter items
|
|
231
283
|
const critterItems = (barn.critters || []).map((c) => ({
|
|
232
284
|
id: c.name,
|
|
233
285
|
label: c.name,
|
|
234
|
-
status:
|
|
235
|
-
meta: c.
|
|
286
|
+
status: 'active', // Critters are assumed active (discovery only finds running services)
|
|
287
|
+
meta: c.service,
|
|
236
288
|
}));
|
|
237
289
|
// Panel-specific hints (page-level hotkeys like s are in BottomBar)
|
|
238
|
-
const livestockHints = '[
|
|
239
|
-
const crittersHints = '';
|
|
290
|
+
const livestockHints = '[n] new [d] delete';
|
|
291
|
+
const crittersHints = '[n] new [d] delete';
|
|
240
292
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: isLocal ? 'Local machine' : `${barn.user}@${barn.host}:${barn.port}` }), !isLocal && (_jsx(Box, { paddingX: 2, marginY: 1, children: _jsxs(Box, { gap: 4, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Host:" }), " ", barn.host] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "User:" }), " ", barn.user] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Port:" }), " ", barn.port] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Key:" }), " ", barn.identity_file] })] }) })), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Livestock", focused: focusedPanel === 'livestock', width: "50%", hints: livestockHints, children: livestockItems.length > 0 ? (_jsx(List, { items: livestockItems, focused: focusedPanel === 'livestock', selectedIndex: selectedLivestockIndex, onSelectionChange: setSelectedLivestockIndex, onSelect: (item) => {
|
|
241
293
|
const found = livestock.find((l) => `${l.project.name}/${l.livestock.name}` === item.id);
|
|
242
294
|
if (found) {
|
|
243
295
|
onSelectLivestock(found.project, found.livestock);
|
|
244
296
|
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
297
|
+
}, onAction: (item, actionKey) => {
|
|
298
|
+
if (actionKey === 's') {
|
|
299
|
+
const found = livestock.find((l) => `${l.project.name}/${l.livestock.name}` === item.id);
|
|
300
|
+
if (found) {
|
|
301
|
+
onOpenLivestockSession(found.project, found.livestock);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} })) : (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No livestock deployed to this barn" }) })) }), _jsx(Panel, { title: "Critters", focused: focusedPanel === 'critters', width: "50%", hints: crittersHints, children: critterItems.length > 0 ? (_jsx(List, { items: critterItems, focused: focusedPanel === 'critters', selectedIndex: selectedCritterIndex, onSelectionChange: setSelectedCritterIndex, onSelect: () => {
|
|
305
|
+
// Could show critter details or logs in future
|
|
306
|
+
} })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No critters configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Press [n] to add a critter" })] })) })] })] }));
|
|
248
307
|
}
|
|
@@ -11,11 +11,11 @@ interface GlobalDashboardProps {
|
|
|
11
11
|
onSelectProject: (project: Project) => void;
|
|
12
12
|
onSelectBarn: (barn: Barn) => void;
|
|
13
13
|
onSelectWindow: (window: TmuxWindow) => void;
|
|
14
|
-
|
|
14
|
+
onNewClaudeForProject: (project: Project) => void;
|
|
15
15
|
onCreateProject: (name: string, path: string) => void;
|
|
16
16
|
onCreateBarn: (barn: Barn) => void;
|
|
17
17
|
onSshToBarn: (barn: Barn) => void;
|
|
18
18
|
onInputModeChange?: (isInputMode: boolean) => void;
|
|
19
19
|
}
|
|
20
|
-
export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow,
|
|
20
|
+
export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaudeForProject, onCreateProject, onCreateBarn, onSshToBarn, onInputModeChange, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
|
|
21
21
|
export {};
|