mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## CHANGES
- Share release-to-release project evolution once you provide repository data. 🚀
This commit is contained in:
127
src/cli/commands/clean-playbooks.ts
Normal file
127
src/cli/commands/clean-playbooks.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// Clean playbooks command
|
||||
// Removes orphaned playbooks (playbooks for sessions that no longer exist)
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { readSessions, getConfigDirectory } from '../services/storage';
|
||||
import { formatError, formatSuccess } from '../output/formatter';
|
||||
|
||||
interface CleanPlaybooksOptions {
|
||||
json?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the playbooks directory path
|
||||
*/
|
||||
function getPlaybooksDir(): string {
|
||||
return path.join(getConfigDirectory(), 'playbooks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find orphaned playbook files (files for sessions that no longer exist)
|
||||
*/
|
||||
function findOrphanedPlaybooks(): Array<{ sessionId: string; filePath: string }> {
|
||||
const playbooksDir = getPlaybooksDir();
|
||||
const orphaned: Array<{ sessionId: string; filePath: string }> = [];
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(playbooksDir)) {
|
||||
return orphaned;
|
||||
}
|
||||
|
||||
const sessions = readSessions();
|
||||
const sessionIds = new Set(sessions.map((s) => s.id));
|
||||
|
||||
const files = fs.readdirSync(playbooksDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.json')) continue;
|
||||
|
||||
const sessionId = file.replace('.json', '');
|
||||
if (!sessionIds.has(sessionId)) {
|
||||
orphaned.push({
|
||||
sessionId,
|
||||
filePath: path.join(playbooksDir, file),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return orphaned;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return orphaned;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanPlaybooks(options: CleanPlaybooksOptions): void {
|
||||
try {
|
||||
const orphaned = findOrphanedPlaybooks();
|
||||
|
||||
if (orphaned.length === 0) {
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ removed: [], count: 0 }));
|
||||
} else {
|
||||
console.log(formatSuccess('No orphaned playbooks found'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
if (options.json) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
dryRun: true,
|
||||
wouldRemove: orphaned.map((o) => ({
|
||||
sessionId: o.sessionId,
|
||||
filePath: o.filePath,
|
||||
})),
|
||||
count: orphaned.length,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log(`\nWould remove ${orphaned.length} orphaned playbook file(s):\n`);
|
||||
for (const o of orphaned) {
|
||||
console.log(` ${o.sessionId.slice(0, 8)} ${o.filePath}`);
|
||||
}
|
||||
console.log('\nRun without --dry-run to actually remove these files.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually remove the files
|
||||
const removed: string[] = [];
|
||||
for (const o of orphaned) {
|
||||
try {
|
||||
fs.unlinkSync(o.filePath);
|
||||
removed.push(o.sessionId);
|
||||
} catch (error) {
|
||||
console.error(`Failed to remove ${o.filePath}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
removed: removed.map((id) => id.slice(0, 8)),
|
||||
count: removed.length,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log(formatSuccess(`Removed ${removed.length} orphaned playbook file(s)`));
|
||||
for (const id of removed) {
|
||||
console.log(` ${id.slice(0, 8)}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
if (options.json) {
|
||||
console.error(JSON.stringify({ error: message }));
|
||||
} else {
|
||||
console.error(formatError(`Failed to clean playbooks: ${message}`));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { listPlaybooks } from './commands/list-playbooks';
|
||||
import { showPlaybook } from './commands/show-playbook';
|
||||
import { showAgent } from './commands/show-agent';
|
||||
import { runPlaybook } from './commands/run-playbook';
|
||||
import { cleanPlaybooks } from './commands/clean-playbooks';
|
||||
|
||||
// Read version from package.json at runtime
|
||||
function getVersion(): string {
|
||||
@@ -81,4 +82,14 @@ program
|
||||
.option('--wait', 'Wait for agent to become available if busy')
|
||||
.action(runPlaybook);
|
||||
|
||||
// Clean command
|
||||
const clean = program.command('clean').description('Clean up orphaned resources');
|
||||
|
||||
clean
|
||||
.command('playbooks')
|
||||
.description('Remove playbooks for deleted sessions')
|
||||
.option('--dry-run', 'Show what would be removed without actually removing')
|
||||
.option('--json', 'Output as JSON (for scripting)')
|
||||
.action(cleanPlaybooks);
|
||||
|
||||
program.parse();
|
||||
|
||||
@@ -216,6 +216,24 @@ export function registerPlaybooksHandlers(deps: PlaybooksHandlerDependencies): v
|
||||
})
|
||||
);
|
||||
|
||||
// Delete all playbooks for a session (used when session is deleted)
|
||||
ipcMain.handle(
|
||||
'playbooks:deleteAll',
|
||||
createIpcHandler(handlerOpts('deleteAll'), async (sessionId: string) => {
|
||||
const filePath = getPlaybooksFilePath(app, sessionId);
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
logger.info(`Deleted all playbooks for session ${sessionId}`, LOG_CONTEXT);
|
||||
} catch (error) {
|
||||
// File doesn't exist, that's fine
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
})
|
||||
);
|
||||
|
||||
// Export a playbook as a ZIP file
|
||||
ipcMain.handle(
|
||||
'playbooks:export',
|
||||
|
||||
@@ -907,6 +907,8 @@ contextBridge.exposeInMainWorld('maestro', {
|
||||
) => ipcRenderer.invoke('playbooks:update', sessionId, playbookId, updates),
|
||||
delete: (sessionId: string, playbookId: string) =>
|
||||
ipcRenderer.invoke('playbooks:delete', sessionId, playbookId),
|
||||
deleteAll: (sessionId: string) =>
|
||||
ipcRenderer.invoke('playbooks:deleteAll', sessionId),
|
||||
export: (sessionId: string, playbookId: string, autoRunFolderPath: string) =>
|
||||
ipcRenderer.invoke('playbooks:export', sessionId, playbookId, autoRunFolderPath),
|
||||
import: (sessionId: string, autoRunFolderPath: string) =>
|
||||
@@ -1796,6 +1798,10 @@ export interface MaestroAPI {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
deleteAll: (sessionId: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
debug: {
|
||||
createPackage: (options?: {
|
||||
|
||||
@@ -3541,6 +3541,13 @@ export default function MaestroConsole() {
|
||||
console.error('Failed to kill terminal process:', error);
|
||||
}
|
||||
|
||||
// Delete associated playbooks
|
||||
try {
|
||||
await window.maestro.playbooks.deleteAll(id);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete playbooks:', error);
|
||||
}
|
||||
|
||||
const newSessions = sessions.filter(s => s.id !== id);
|
||||
setSessions(newSessions);
|
||||
// Flush immediately for critical operation (session deletion)
|
||||
|
||||
@@ -750,7 +750,7 @@ export function AgentSessionsBrowser({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Context Window with visual gauge */}
|
||||
{/* Total Tokens */}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Zap className="w-4 h-4" style={{ color: theme.colors.accent }} />
|
||||
@@ -761,32 +761,13 @@ export function AgentSessionsBrowser({
|
||||
{(() => {
|
||||
const totalTokens = viewingSession.inputTokens + viewingSession.outputTokens;
|
||||
const contextUsage = Math.min(100, (totalTokens / 200000) * 100);
|
||||
const getContextColor = (usage: number) => {
|
||||
if (usage >= 90) return theme.colors.error;
|
||||
if (usage >= 70) return theme.colors.warning;
|
||||
return theme.colors.accent;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<span className="text-lg font-mono font-semibold" style={{ color: theme.colors.textMain }}>
|
||||
{formatNumber(totalTokens)}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="w-24 h-2 rounded-full overflow-hidden" style={{ backgroundColor: theme.colors.border }}>
|
||||
<div
|
||||
className="h-full transition-all duration-500 ease-out"
|
||||
style={{
|
||||
width: `${contextUsage}%`,
|
||||
backgroundColor: getContextColor(contextUsage)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono font-bold" style={{ color: getContextColor(contextUsage) }}>
|
||||
{contextUsage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] mt-0.5" style={{ color: theme.colors.textDim }}>
|
||||
of 200k context
|
||||
of 200k context <span className="font-mono font-medium" style={{ color: theme.colors.accent }}>{contextUsage.toFixed(1)}%</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -190,10 +190,13 @@ export function ProcessMonitor(props: ProcessMonitorProps) {
|
||||
containerRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Focus detail view when it opens
|
||||
// Focus detail view when it opens, restore focus to container when it closes
|
||||
useEffect(() => {
|
||||
if (detailView && detailViewRef.current) {
|
||||
detailViewRef.current.focus();
|
||||
} else if (!detailView && containerRef.current) {
|
||||
// Restore focus to the container when returning from detail view
|
||||
containerRef.current.focus();
|
||||
}
|
||||
}, [detailView]);
|
||||
|
||||
@@ -1035,7 +1038,7 @@ export function ProcessMonitor(props: ProcessMonitorProps) {
|
||||
</div>
|
||||
|
||||
{/* Detail Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin p-6 space-y-6">
|
||||
{/* Process Name & Status */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
|
||||
1
src/renderer/global.d.ts
vendored
1
src/renderer/global.d.ts
vendored
@@ -743,6 +743,7 @@ interface MaestroAPI {
|
||||
};
|
||||
}>) => Promise<{ success: boolean; playbook?: any; error?: string }>;
|
||||
delete: (sessionId: string, playbookId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
deleteAll: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
export: (sessionId: string, playbookId: string, autoRunFolderPath: string) => Promise<{ success: boolean; filePath?: string; error?: string }>;
|
||||
import: (sessionId: string, autoRunFolderPath: string) => Promise<{ success: boolean; playbook?: any; importedDocs?: string[]; error?: string }>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user