mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## CHANGES
- Finds npm-installed CLIs inside nvm/fnm/volta/mise/asdf paths automatically 🔍 - Adds macOS Apple Events entitlement for better automation support 🔐 - Updates Discord invite link across app, docs, and README 🗣️ - Makes FilePreview scroll listener passive for smoother scrolling performance 🏎️ - Refines Activity Heatmap hour labels and spacing for cleaner readability 📊 - Repositions heatmap tooltip slightly above cells for nicer hover UX 🎯
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Maestro
|
||||
|
||||
[](https://github.com/pedramamini/Maestro)
|
||||
[](https://discord.gg/SrBsykvG)
|
||||
[](https://discord.gg/SVSRy593)
|
||||
[](https://docs.runmaestro.ai/)
|
||||
|
||||
> Maestro hones fractured attention into focused intent.
|
||||
@@ -163,7 +163,7 @@ Full documentation and usage guide available at **[docs.runmaestro.ai](https://d
|
||||
|
||||
## Community
|
||||
|
||||
- **Discord**: [Join Us](https://discord.gg/SrBsykvG)
|
||||
- **Discord**: [Join Us](https://discord.gg/SVSRy593)
|
||||
- **GitHub Issues**: [Report bugs & request features](https://github.com/pedramamini/Maestro/issues)
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -10,5 +10,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<false/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"links": [
|
||||
{
|
||||
"label": "Discord",
|
||||
"href": "https://discord.gg/SrBsykvG"
|
||||
"href": "https://discord.gg/SVSRy593"
|
||||
},
|
||||
{
|
||||
"label": "GitHub",
|
||||
@@ -120,7 +120,7 @@
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"discord": "https://discord.gg/SrBsykvG",
|
||||
"discord": "https://discord.gg/SVSRy593",
|
||||
"github": "https://github.com/pedramamini/Maestro"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,5 +138,5 @@ For new projects, always clone to the Linux filesystem from the start.
|
||||
## Getting Help
|
||||
|
||||
- **GitHub Issues**: [Report bugs or request features](https://github.com/pedramamini/Maestro/issues)
|
||||
- **Discord**: [Join the community](https://discord.gg/SrBsykvG)
|
||||
- **Discord**: [Join the community](https://discord.gg/SVSRy593)
|
||||
- **Documentation**: [Docs site](https://docs.runmaestro.ai), [CONTRIBUTING.md](https://github.com/pedramamini/Maestro/blob/main/CONTRIBUTING.md), and [ARCHITECTURE.md](https://github.com/pedramamini/Maestro/blob/main/ARCHITECTURE.md)
|
||||
|
||||
@@ -522,6 +522,99 @@ export class AgentDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Node version manager paths on Unix systems (macOS/Linux).
|
||||
* Returns an array of bin paths from nvm, fnm, volta, mise, asdf, and n.
|
||||
* These paths are needed to find npm-installed CLIs like codex, claude, gemini.
|
||||
*/
|
||||
private detectNodeVersionManagerBinPaths(): string[] {
|
||||
const home = os.homedir();
|
||||
const detectedPaths: string[] = [];
|
||||
|
||||
// nvm: Check for ~/.nvm and find installed node versions
|
||||
const nvmDir = process.env.NVM_DIR || path.join(home, '.nvm');
|
||||
if (fs.existsSync(nvmDir)) {
|
||||
const versionsDir = path.join(nvmDir, 'versions', 'node');
|
||||
if (fs.existsSync(versionsDir)) {
|
||||
try {
|
||||
const versions = fs.readdirSync(versionsDir).filter(v => v.startsWith('v'));
|
||||
// Sort versions descending to check newest first
|
||||
versions.sort((a, b) => {
|
||||
const parseVersion = (v: string) => v.replace('v', '').split('.').map(n => parseInt(n, 10));
|
||||
const av = parseVersion(a);
|
||||
const bv = parseVersion(b);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((bv[i] || 0) !== (av[i] || 0)) return (bv[i] || 0) - (av[i] || 0);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
for (const version of versions) {
|
||||
const versionBin = path.join(versionsDir, version, 'bin');
|
||||
if (fs.existsSync(versionBin)) {
|
||||
detectedPaths.push(versionBin);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors reading versions directory
|
||||
}
|
||||
}
|
||||
// Also check nvm/current symlink
|
||||
const nvmCurrentBin = path.join(nvmDir, 'current', 'bin');
|
||||
if (fs.existsSync(nvmCurrentBin) && !detectedPaths.includes(nvmCurrentBin)) {
|
||||
detectedPaths.unshift(nvmCurrentBin);
|
||||
}
|
||||
}
|
||||
|
||||
// fnm: Fast Node Manager
|
||||
const fnmPaths = [
|
||||
path.join(home, 'Library', 'Application Support', 'fnm'), // macOS default
|
||||
path.join(home, '.local', 'share', 'fnm'), // Linux default
|
||||
path.join(home, '.fnm'), // Legacy/custom location
|
||||
];
|
||||
for (const fnmDir of fnmPaths) {
|
||||
if (fs.existsSync(fnmDir)) {
|
||||
const fnmCurrentBin = path.join(fnmDir, 'aliases', 'default', 'bin');
|
||||
if (fs.existsSync(fnmCurrentBin)) {
|
||||
detectedPaths.push(fnmCurrentBin);
|
||||
}
|
||||
const fnmNodeVersions = path.join(fnmDir, 'node-versions');
|
||||
if (fs.existsSync(fnmNodeVersions)) {
|
||||
try {
|
||||
const versions = fs.readdirSync(fnmNodeVersions).filter(v => v.startsWith('v'));
|
||||
for (const version of versions) {
|
||||
const versionBin = path.join(fnmNodeVersions, version, 'installation', 'bin');
|
||||
if (fs.existsSync(versionBin)) {
|
||||
detectedPaths.push(versionBin);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// volta: Uses ~/.volta/bin for shims
|
||||
const voltaBin = path.join(home, '.volta', 'bin');
|
||||
if (fs.existsSync(voltaBin)) {
|
||||
detectedPaths.push(voltaBin);
|
||||
}
|
||||
|
||||
// mise (formerly rtx): Uses ~/.local/share/mise/shims
|
||||
const miseShims = path.join(home, '.local', 'share', 'mise', 'shims');
|
||||
if (fs.existsSync(miseShims)) {
|
||||
detectedPaths.push(miseShims);
|
||||
}
|
||||
|
||||
// asdf: Uses ~/.asdf/shims
|
||||
const asdfShims = path.join(home, '.asdf', 'shims');
|
||||
if (fs.existsSync(asdfShims)) {
|
||||
detectedPaths.push(asdfShims);
|
||||
}
|
||||
|
||||
return detectedPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* On macOS/Linux, directly probe known installation paths for a binary.
|
||||
* This is necessary because packaged Electron apps don't inherit shell aliases,
|
||||
@@ -531,6 +624,9 @@ export class AgentDetector {
|
||||
private async probeUnixPaths(binaryName: string): Promise<string | null> {
|
||||
const home = os.homedir();
|
||||
|
||||
// Get dynamic paths from Node version managers (nvm, fnm, volta, etc.)
|
||||
const versionManagerPaths = this.detectNodeVersionManagerBinPaths();
|
||||
|
||||
// Define known installation paths for each binary, in priority order
|
||||
const knownPaths: Record<string, string[]> = {
|
||||
claude: [
|
||||
@@ -546,6 +642,8 @@ export class AgentDetector {
|
||||
path.join(home, '.npm-global', 'bin', 'claude'),
|
||||
// User bin directory
|
||||
path.join(home, 'bin', 'claude'),
|
||||
// Add paths from Node version managers (nvm, fnm, volta, etc.)
|
||||
...versionManagerPaths.map(p => path.join(p, 'claude')),
|
||||
],
|
||||
codex: [
|
||||
// User local bin
|
||||
@@ -555,6 +653,8 @@ export class AgentDetector {
|
||||
'/usr/local/bin/codex',
|
||||
// npm global
|
||||
path.join(home, '.npm-global', 'bin', 'codex'),
|
||||
// Add paths from Node version managers (nvm, fnm, volta, etc.)
|
||||
...versionManagerPaths.map(p => path.join(p, 'codex')),
|
||||
],
|
||||
opencode: [
|
||||
// OpenCode installer default location
|
||||
@@ -566,12 +666,16 @@ export class AgentDetector {
|
||||
// Homebrew paths
|
||||
'/opt/homebrew/bin/opencode',
|
||||
'/usr/local/bin/opencode',
|
||||
// Add paths from Node version managers (nvm, fnm, volta, etc.)
|
||||
...versionManagerPaths.map(p => path.join(p, 'opencode')),
|
||||
],
|
||||
gemini: [
|
||||
// npm global paths
|
||||
path.join(home, '.npm-global', 'bin', 'gemini'),
|
||||
'/opt/homebrew/bin/gemini',
|
||||
'/usr/local/bin/gemini',
|
||||
// Add paths from Node version managers (nvm, fnm, volta, etc.)
|
||||
...versionManagerPaths.map(p => path.join(p, 'gemini')),
|
||||
],
|
||||
aider: [
|
||||
// pip installation
|
||||
@@ -579,6 +683,8 @@ export class AgentDetector {
|
||||
// Homebrew paths
|
||||
'/opt/homebrew/bin/aider',
|
||||
'/usr/local/bin/aider',
|
||||
// Add paths from Node version managers (in case installed via npm)
|
||||
...versionManagerPaths.map(p => path.join(p, 'aider')),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ export function AboutModal({ theme, autoRunStats, usageStats, handsOnTimeMs, onC
|
||||
<Globe className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.maestro.shell.openExternal('https://discord.gg/SrBsykvG')}
|
||||
onClick={() => window.maestro.shell.openExternal('https://discord.gg/SVSRy593')}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
title="Join our Discord"
|
||||
style={{ color: theme.colors.accent }}
|
||||
|
||||
@@ -577,7 +577,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
|
||||
setShowStatsBar(contentEl.scrollTop <= 10);
|
||||
};
|
||||
|
||||
contentEl.addEventListener('scroll', handleScroll);
|
||||
contentEl.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => contentEl.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -376,7 +376,7 @@ export function QuickActionsModal(props: QuickActionsModalProps) {
|
||||
{ id: 'about', label: 'About Maestro', action: () => { setAboutModalOpen(true); setQuickActionOpen(false); } },
|
||||
{ id: 'website', label: 'Maestro Website', subtext: 'Open the Maestro website', action: () => { window.maestro.shell.openExternal('https://runmaestro.ai/'); setQuickActionOpen(false); } },
|
||||
{ id: 'docs', label: 'Documentation and User Guide', subtext: 'Open the Maestro documentation', action: () => { window.maestro.shell.openExternal('https://docs.runmaestro.ai/'); setQuickActionOpen(false); } },
|
||||
{ id: 'discord', label: 'Join Discord', subtext: 'Join the Maestro community', action: () => { window.maestro.shell.openExternal('https://discord.gg/SrBsykvG'); setQuickActionOpen(false); } },
|
||||
{ id: 'discord', label: 'Join Discord', subtext: 'Join the Maestro community', action: () => { window.maestro.shell.openExternal('https://discord.gg/SVSRy593'); setQuickActionOpen(false); } },
|
||||
...(setUpdateCheckModalOpen ? [{ id: 'updateCheck', label: 'Check for Updates', action: () => { setUpdateCheckModalOpen(true); setQuickActionOpen(false); } }] : []),
|
||||
{ id: 'createDebugPackage', label: 'Create Debug Package', subtext: 'Generate a support bundle for bug reporting', action: () => {
|
||||
setQuickActionOpen(false);
|
||||
|
||||
@@ -192,9 +192,10 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
|
||||
|
||||
// Determine hour rows based on mode
|
||||
const hours = useAmPm ? [0, 12] : Array.from({ length: 24 }, (_, i) => i);
|
||||
// Labels for Y-axis: show every 2 hours for readability
|
||||
const labels = useAmPm
|
||||
? ['AM', 'PM']
|
||||
: ['12a', '', '2a', '', '4a', '', '6a', '', '8a', '', '10a', '', '12p', '', '2p', '', '4p', '', '6p', '', '8p', '', '10p', ''];
|
||||
: ['12a', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a', '10a', '11a', '12p', '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', '10p', '11p'];
|
||||
|
||||
// Track max values for intensity calculation
|
||||
let maxCount = 0;
|
||||
@@ -272,9 +273,10 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
|
||||
(cell: HourData, event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setHoveredCell(cell);
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
// Position tooltip above and centered on the cell
|
||||
setTooltipPos({
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top,
|
||||
y: rect.top - 4,
|
||||
});
|
||||
},
|
||||
[]
|
||||
@@ -354,7 +356,7 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
|
||||
|
||||
{/* Heatmap grid */}
|
||||
<div className="flex gap-2">
|
||||
{/* Hour labels (Y-axis) */}
|
||||
{/* Hour labels (Y-axis) - only show every 2 hours for readability */}
|
||||
<div className="flex flex-col flex-shrink-0" style={{ width: 28, paddingTop: 20 }}>
|
||||
{hourLabels.map((label, idx) => (
|
||||
<div
|
||||
@@ -362,10 +364,11 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
|
||||
className="text-xs text-right flex items-center justify-end"
|
||||
style={{
|
||||
color: theme.colors.textDim,
|
||||
height: useAmPm ? 32 : 12,
|
||||
height: useAmPm ? 34 : 14,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{/* Only show labels for even hours (0, 2, 4, etc.) */}
|
||||
{useAmPm || idx % 2 === 0 ? label : ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -393,7 +396,7 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
|
||||
key={hourData.hourKey}
|
||||
className="rounded-sm cursor-default"
|
||||
style={{
|
||||
height: useAmPm ? 32 : 12,
|
||||
height: useAmPm ? 34 : 14,
|
||||
backgroundColor: getIntensityColor(
|
||||
hourData.intensity,
|
||||
theme,
|
||||
|
||||
Reference in New Issue
Block a user