Skip to content

Commit e020f6d

Browse files
committed
Refactor package manager implementations and add comprehensive tests
- Removed unnecessary constructor parameters and simplified isConfigured methods in Chocolatey, Gentoo, Homebrew, Nix, RPM, Scoop, and Winget package managers. - Updated generateManifest methods to return promises consistently across package managers. - Enhanced error handling and logging in RPM package manager. - Added a new CrossForkPRSubmissionParams interface for better PR submission handling. - Implemented unit tests for all package managers, ensuring correct manifest generation and submission logic. - Created a Vitest configuration file for running tests. release:patch
1 parent ce8f61e commit e020f6d

25 files changed

Lines changed: 895 additions & 273 deletions

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@
202202
"Bash(set +a)",
203203
"Bash(gh workflow run:*)",
204204
"Bash(gh workflow:*)",
205-
"Bash(pnpm -F @pairux/web typecheck:*)"
205+
"Bash(pnpm -F @pairux/web typecheck:*)",
206+
"Bash(./.husky/pre-commit)"
206207
],
207208
"deny": [
208209
"Bash(npm *)",

.github/workflows/submit-packages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
# If "all" or contains chocolatey, we need to exclude chocolatey from linux job
9696
if [ "$INPUT_PMS" = "all" ]; then
9797
# Run all except chocolatey on Linux
98-
PMS="homebrew,scoop,winget,aur,apt,rpm"
98+
PMS="homebrew,scoop,winget,aur,apt,rpm,gentoo,nix"
9999
else
100100
# Remove chocolatey from the list for Linux job
101101
PMS=$(echo "$INPUT_PMS" | sed 's/chocolatey,//g' | sed 's/,chocolatey//g' | sed 's/^chocolatey$//g')

apps/desktop/src/main/capture/sources.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export async function getCaptureSources(types: ('screen' | 'window')[]): Promise
3131
} satisfies CaptureSource;
3232
});
3333
} catch (error) {
34+
// On Wayland and some Linux systems, desktopCapturer may not work
35+
// Return empty array instead of throwing to allow fallback to getDisplayMedia
3436
console.error('[Capture] Failed to get capture sources:', error);
35-
throw error;
37+
return [];
3638
}
3739
}

apps/desktop/src/main/tray.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ vi.mock('electron', () => {
2525
nativeImage: {
2626
createFromPath: vi.fn().mockReturnValue({}),
2727
createFromBuffer: vi.fn().mockReturnValue({}),
28+
createEmpty: vi.fn().mockReturnValue({}),
2829
},
2930
app: {
3031
isPackaged: false,

apps/desktop/src/main/tray.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,36 +44,34 @@ function createTrayIcon(status: TrayStatus): Electron.NativeImage {
4444
};
4545

4646
const iconPath = join(resourcePath, iconMap[status]);
47+
console.log('[Tray] Looking for icon at:', iconPath, 'exists:', existsSync(iconPath));
4748

4849
if (existsSync(iconPath)) {
49-
return nativeImage.createFromPath(iconPath);
50+
const icon = nativeImage.createFromPath(iconPath);
51+
if (!icon.isEmpty()) {
52+
return icon;
53+
}
54+
console.warn('[Tray] Icon loaded but is empty:', iconPath);
5055
}
5156

52-
// Fallback: create simple colored circle icons programmatically
53-
const size = process.platform === 'darwin' ? 22 : 16;
54-
const colors: Record<TrayStatus, string> = {
55-
idle: '#6b7280', // Gray
56-
active: '#22c55e', // Green
57-
paused: '#f59e0b', // Amber
58-
error: '#ef4444', // Red
59-
};
57+
// Fallback: use the main app icon
58+
const mainIconPath = app.isPackaged
59+
? join(process.resourcesPath, 'icon.png')
60+
: join(__dirname, '../../resources/icon.png');
61+
62+
console.log('[Tray] Trying fallback icon at:', mainIconPath, 'exists:', existsSync(mainIconPath));
6063

61-
const color = colors[status];
62-
63-
// Create a simple SVG circle
64-
const cx = String(size / 2);
65-
const cy = String(size / 2);
66-
const outerR = String(size / 2 - 1);
67-
const innerR = String(size / 4);
68-
const svg = `
69-
<svg width="${String(size)}" height="${String(size)}" xmlns="http://www.w3.org/2000/svg">
70-
<circle cx="${cx}" cy="${cy}" r="${outerR}" fill="${color}"/>
71-
<circle cx="${cx}" cy="${cy}" r="${innerR}" fill="white"/>
72-
</svg>
73-
`;
74-
75-
const buffer = Buffer.from(svg);
76-
return nativeImage.createFromBuffer(buffer);
64+
if (existsSync(mainIconPath)) {
65+
const icon = nativeImage.createFromPath(mainIconPath);
66+
if (!icon.isEmpty()) {
67+
// Resize for tray
68+
const size = process.platform === 'darwin' ? 22 : 16;
69+
return icon.resize({ width: size, height: size });
70+
}
71+
}
72+
73+
console.warn('[Tray] No icon found, using empty image');
74+
return nativeImage.createEmpty();
7775
}
7876

7977
/**
@@ -194,10 +192,23 @@ export function initializeTray(trayCallbacks: TrayCallbacks): Tray {
194192
const icon = createTrayIcon('idle');
195193
tray = new Tray(icon);
196194

197-
tray.setContextMenu(buildContextMenu());
195+
const contextMenu = buildContextMenu();
196+
tray.setContextMenu(contextMenu);
198197
updateTooltip();
199198

200-
// Double-click to show window (Windows/Linux)
199+
// On Linux, single-click should show the context menu
200+
// On Windows/macOS, single-click behavior varies by platform
201+
tray.on('click', () => {
202+
if (process.platform === 'linux') {
203+
// On Linux, explicitly pop up the context menu on click
204+
tray?.popUpContextMenu(contextMenu);
205+
} else {
206+
// On Windows/macOS, single-click shows window
207+
callbacks?.onShowWindow();
208+
}
209+
});
210+
211+
// Double-click to show window (all platforms)
201212
tray.on('double-click', () => {
202213
callbacks?.onShowWindow();
203214
});

apps/desktop/src/renderer/components/capture/CapturePreview.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,14 +333,16 @@ export function CapturePreview({
333333
setRecordingQuality(e.target.value as RecordingQuality);
334334
}}
335335
className="rounded bg-background px-2 py-1 text-xs"
336+
title="Recording quality - affects file size and bitrate"
336337
>
337-
<option value="720p">720p</option>
338-
<option value="1080p">1080p</option>
339-
<option value="4k">4K</option>
338+
<option value="720p">720p (2.5 Mbps)</option>
339+
<option value="1080p">1080p (5 Mbps)</option>
340+
<option value="4k">4K (15 Mbps)</option>
340341
</select>
341342
<button
342343
onClick={() => void handleStartRecording()}
343344
className="flex items-center gap-1.5 rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-red-700"
345+
title="Start recording to local file"
344346
>
345347
<Circle className="h-3 w-3 fill-current" />
346348
Record

apps/desktop/src/renderer/components/layout/TitleBar.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ export function TitleBar() {
2323
} pr-4`}
2424
>
2525
<div className="no-drag flex items-center gap-2">
26-
<img src="/logo.svg" alt="PairUX" className="h-4 w-4" />
27-
<span className="text-sm font-medium">PairUX</span>
26+
<img src="/favicon.svg" alt="PairUX" className="h-5 w-auto" />
2827
</div>
2928

3029
<div className="flex-1" />

apps/web/src/components/Logo.tsx

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,34 @@
1-
import { Monitor } from 'lucide-react';
1+
import Image from 'next/image';
22
import Link from 'next/link';
33
import { cn } from '@/lib/utils';
44

55
interface LogoProps {
66
size?: 'sm' | 'md' | 'lg';
7-
showText?: boolean;
87
className?: string;
98
}
109

1110
const sizes = {
12-
sm: {
13-
container: 'h-8 w-8',
14-
icon: 'h-4 w-4',
15-
text: 'text-lg',
16-
},
17-
md: {
18-
container: 'h-9 w-9',
19-
icon: 'h-5 w-5',
20-
text: 'text-xl',
21-
},
22-
lg: {
23-
container: 'h-10 w-10',
24-
icon: 'h-6 w-6',
25-
text: 'text-2xl',
26-
},
11+
sm: { height: 24 },
12+
md: { height: 32 },
13+
lg: { height: 40 },
2714
};
2815

29-
export function Logo({ size = 'md', showText = true, className }: LogoProps) {
16+
export function Logo({ size = 'md', className }: LogoProps) {
3017
const sizeConfig = sizes[size];
18+
// Logo aspect ratio is approximately 2.83:1 (512.75 / 181.44)
19+
const width = Math.round(sizeConfig.height * 2.83);
3120

3221
return (
33-
<Link href="/" className={cn('flex items-center gap-2', className)}>
34-
<div
35-
className={cn(
36-
'bg-primary-600 flex items-center justify-center rounded-lg',
37-
sizeConfig.container
38-
)}
39-
>
40-
<Monitor className={cn('text-white', sizeConfig.icon)} />
41-
</div>
42-
{showText && <span className={cn('font-bold text-gray-900', sizeConfig.text)}>PairUX</span>}
22+
<Link href="/" className={cn('flex items-center', className)}>
23+
<Image
24+
src="/logo.svg"
25+
alt="PairUX"
26+
width={width}
27+
height={sizeConfig.height}
28+
className="h-auto"
29+
style={{ height: sizeConfig.height }}
30+
priority
31+
/>
4332
</Link>
4433
);
4534
}

apps/web/src/components/footer.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const footerLinks = {
4747
href: 'https://community.chocolatey.org/packages/pairux',
4848
external: true,
4949
},
50-
{ name: 'AUR (Arch)', href: 'https://aur.archlinux.org/packages/pairux', external: true },
50+
{ name: 'AUR (Arch)', href: 'https://aur.archlinux.org/packages/pairux-bin', external: true },
5151
{ name: 'APT (Debian)', href: 'https://github.com/profullstack/pairux-apt', external: true },
5252
{ name: 'RPM (Fedora)', href: 'https://github.com/profullstack/pairux-rpm', external: true },
5353
{ name: 'Gentoo', href: 'https://github.com/profullstack/gentoo-pairux', external: true },
@@ -87,9 +87,8 @@ export function Footer() {
8787
<div className="grid grid-cols-2 gap-8 lg:grid-cols-5">
8888
{/* Brand */}
8989
<div className="col-span-2 lg:col-span-1">
90-
<Link href="/" className="flex items-center gap-2">
91-
<Image src="/logo.svg" alt="PairUX" width={36} height={36} className="h-9 w-9" />
92-
<span className="text-xl font-bold text-gray-900">PairUX</span>
90+
<Link href="/" className="flex items-center">
91+
<Image src="/logo.svg" alt="PairUX" width={113} height={40} className="h-10 w-auto" />
9392
</Link>
9493
<p className="mt-4 text-sm text-gray-600">
9594
Collaborative screen sharing with simultaneous remote control. Like Screenhero, but

apps/web/src/lib/supabase/server.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ import { createClient as createSupabaseClient } from '@supabase/supabase-js';
44
import { cookies, headers } from 'next/headers';
55
import type { Database } from '@pairux/shared-types';
66

7+
/**
8+
* Extract Bearer token from Authorization header if present.
9+
*/
10+
export async function getBearerToken(): Promise<string | null> {
11+
const headerStore = await headers();
12+
const authHeader = headerStore.get('authorization');
13+
if (authHeader?.startsWith('Bearer ')) {
14+
return authHeader.slice(7);
15+
}
16+
return null;
17+
}
18+
719
/**
820
* Create a Supabase client for server-side use.
921
* Supports both cookie-based auth (web browser) and Bearer token auth (desktop app).
@@ -17,23 +29,25 @@ export async function createClient() {
1729
}
1830

1931
// Check for Bearer token in Authorization header (desktop app)
20-
const headerStore = await headers();
21-
const authHeader = headerStore.get('authorization');
32+
const token = await getBearerToken();
2233

23-
if (authHeader?.startsWith('Bearer ')) {
24-
const token = authHeader.slice(7);
25-
// Desktop app: use token-based auth
26-
return createSupabaseClient<Database>(url, key, {
27-
global: {
28-
headers: {
29-
Authorization: `Bearer ${token}`,
30-
},
31-
},
34+
if (token) {
35+
// Desktop app: create client and set the session with the access token
36+
const supabase = createSupabaseClient<Database>(url, key, {
3237
auth: {
3338
persistSession: false,
3439
autoRefreshToken: false,
3540
},
3641
});
42+
43+
// Set the auth session using the access token
44+
// This allows getUser() to work correctly
45+
await supabase.auth.setSession({
46+
access_token: token,
47+
refresh_token: '', // We don't have refresh token in header, but it's required
48+
});
49+
50+
return supabase;
3751
}
3852

3953
// Web browser: use cookie-based auth

0 commit comments

Comments
 (0)