Skip to content

Commit 14a5d34

Browse files
github-actions[bot]chasprowebdevMarfuen
authored
ENG-36 Support Intel build for mac, not just Apple Silicon (#1714)
* fix(portal): download relevant device agent per macOS chip on portal * fix(portal): fix portal build issue * fix(portal): add log for testing * fix(portal): add log for testing * fix(portal): put a dropdown to allow users to select macOS chip type for downloading agent * style(portal): fix prettier lint errors --------- Co-authored-by: chasprowebdev <chasgarciaprowebdev@gmail.com> Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
1 parent d1288bb commit 14a5d34

File tree

4 files changed

+109
-25
lines changed

4 files changed

+109
-25
lines changed

apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use client';
22

3+
import { detectOSFromUserAgent, SupportedOS } from '@/utils/os';
34
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@comp/ui/accordion';
45
import { Button } from '@comp/ui/button';
56
import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card';
67
import { cn } from '@comp/ui/cn';
8+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select';
79
import type { Member } from '@db';
810
import { CheckCircle2, Circle, Download, Loader2, XCircle } from 'lucide-react';
911
import Image from 'next/image';
10-
import { useState } from 'react';
12+
import { useEffect, useMemo, useState } from 'react';
1113
import { toast } from 'sonner';
1214
import type { FleetPolicy, Host } from '../../types';
1315

@@ -23,9 +25,12 @@ export function DeviceAgentAccordionItem({
2325
fleetPolicies = [],
2426
}: DeviceAgentAccordionItemProps) {
2527
const [isDownloading, setIsDownloading] = useState(false);
28+
const [detectedOS, setDetectedOS] = useState<SupportedOS | null>(null);
2629

27-
// Detect OS from user agent
28-
const isMacOS = typeof window !== 'undefined' && navigator.userAgent.includes('Mac');
30+
const isMacOS = useMemo(
31+
() => detectedOS === 'macos' || detectedOS === 'macos-intel',
32+
[detectedOS],
33+
);
2934

3035
const hasInstalledAgent = host !== null;
3136
const allPoliciesPass =
@@ -55,12 +60,22 @@ export function DeviceAgentAccordionItem({
5560

5661
// Now trigger the actual download using the browser's native download mechanism
5762
// This will show in the browser's download UI immediately
58-
const downloadUrl = `/api/download-agent?token=${encodeURIComponent(token)}`;
63+
const downloadUrl = `/api/download-agent?token=${encodeURIComponent(token)}&os=${detectedOS}`;
5964

6065
// Method 1: Using a temporary link (most reliable)
6166
const a = document.createElement('a');
6267
a.href = downloadUrl;
63-
a.download = isMacOS ? 'Comp AI Agent-1.0.0-arm64.dmg' : 'compai-device-agent.zip';
68+
69+
// Set filename based on OS and architecture
70+
if (isMacOS) {
71+
a.download =
72+
detectedOS === 'macos'
73+
? 'Comp AI Agent-1.0.0-arm64.dmg'
74+
: 'Comp AI Agent-1.0.0-intel.dmg';
75+
} else {
76+
a.download = 'compai-device-agent.zip';
77+
}
78+
6479
document.body.appendChild(a);
6580
a.click();
6681
document.body.removeChild(a);
@@ -95,6 +110,14 @@ export function DeviceAgentAccordionItem({
95110
}
96111
};
97112

113+
useEffect(() => {
114+
const detectOS = async () => {
115+
const os = await detectOSFromUserAgent();
116+
setDetectedOS(os);
117+
};
118+
detectOS();
119+
}, []);
120+
98121
return (
99122
<AccordionItem value="device-agent" className="border rounded-xs">
100123
<AccordionTrigger className="px-4 hover:no-underline [&[data-state=open]]:pb-2">
@@ -129,15 +152,31 @@ export function DeviceAgentAccordionItem({
129152
<p className="mt-1">
130153
Click the download button below to get the Device Agent installer.
131154
</p>
132-
<Button
133-
size="sm"
134-
variant="default"
135-
onClick={handleDownload}
136-
disabled={isDownloading || hasInstalledAgent}
137-
className="gap-2 mt-2"
138-
>
139-
{getButtonContent()}
140-
</Button>
155+
<div className="flex items-center gap-2 mt-2">
156+
{isMacOS && !hasInstalledAgent && (
157+
<Select
158+
value={detectedOS || 'macos'}
159+
onValueChange={(value: 'macos' | 'macos-intel') => setDetectedOS(value)}
160+
>
161+
<SelectTrigger className="w-[136px]">
162+
<SelectValue />
163+
</SelectTrigger>
164+
<SelectContent>
165+
<SelectItem value="macos">Apple Silicon</SelectItem>
166+
<SelectItem value="macos-intel">Intel</SelectItem>
167+
</SelectContent>
168+
</Select>
169+
)}
170+
<Button
171+
size="sm"
172+
variant="default"
173+
onClick={handleDownload}
174+
disabled={isDownloading || hasInstalledAgent}
175+
className="gap-2"
176+
>
177+
{getButtonContent()}
178+
</Button>
179+
</div>
141180
</li>
142181
{!isMacOS && (
143182
<li>

apps/portal/src/app/api/download-agent/route.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export const maxDuration = 60;
2121
export async function GET(req: NextRequest) {
2222
const searchParams = req.nextUrl.searchParams;
2323
const token = searchParams.get('token');
24+
const os = searchParams.get('os');
25+
26+
if (!os) {
27+
return new NextResponse('Missing OS', { status: 400 });
28+
}
2429

2530
if (!token) {
2631
return new NextResponse('Missing download token', { status: 400 });
@@ -36,14 +41,12 @@ export async function GET(req: NextRequest) {
3641
// Delete token after retrieval (one-time use)
3742
await kv.del(`download:${token}`);
3843

39-
const { orgId, employeeId, os } = downloadInfo as {
44+
const { orgId, employeeId } = downloadInfo as {
4045
orgId: string;
4146
employeeId: string;
4247
userId: string;
43-
os: SupportedOS;
4448
};
45-
console.log(os);
46-
49+
4750
// Hardcoded device marker paths used by the setup scripts
4851
const fleetDevicePathMac = '/Users/Shared/.fleet';
4952
const fleetDevicePathWindows = 'C:\\ProgramData\\CompAI\\Fleet';
@@ -57,7 +60,8 @@ export async function GET(req: NextRequest) {
5760
if (os === 'macos' || os === 'macos-intel') {
5861
try {
5962
// Direct DMG download for macOS
60-
const macosPackageFilename = os === 'macos' ? 'Comp AI Agent-1.0.0-arm64.dmg' : 'Comp AI Agent-1.0.0.dmg';
63+
const macosPackageFilename =
64+
os === 'macos' ? 'Comp AI Agent-1.0.0-arm64.dmg' : 'Comp AI Agent-1.0.0.dmg';
6165
const packageKey = `macos/${macosPackageFilename}`;
6266

6367
const getObjectCommand = new GetObjectCommand({
@@ -118,15 +122,15 @@ export async function GET(req: NextRequest) {
118122
});
119123

120124
// Add script file
121-
const scriptFilename = getScriptFilename(os);
125+
const scriptFilename = getScriptFilename(os as SupportedOS);
122126
archive.append(script, { name: scriptFilename, mode: 0o755 });
123127

124128
// Add README
125-
const readmeContent = getReadmeContent(os);
129+
const readmeContent = getReadmeContent(os as SupportedOS);
126130
archive.append(readmeContent, { name: 'README.txt' });
127131

128132
// Get package from S3 and stream it
129-
const packageFilename = getPackageFilename(os);
133+
const packageFilename = getPackageFilename(os as SupportedOS);
130134
const windowsPackageFilename = 'fleet-osquery.msi';
131135
const packageKey = `windows/${windowsPackageFilename}`;
132136

apps/portal/src/app/api/download-agent/scripts/common.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import type { SupportedOS } from '../types';
22

33
export function getScriptFilename(os: SupportedOS): string {
4-
return os === 'macos' ? 'run_me_first.command' : 'run_me_first.bat';
4+
return os === 'macos' || os === 'macos-intel' ? 'run_me_first.command' : 'run_me_first.bat';
55
}
66

77
export function getPackageFilename(os: SupportedOS): string {
8-
return os === 'macos' ? 'compai-device-agent.pkg' : 'compai-device-agent.msi';
8+
return os === 'macos' || os === 'macos-intel'
9+
? 'compai-device-agent.pkg'
10+
: 'compai-device-agent.msi';
911
}
1012

1113
export function getReadmeContent(os: SupportedOS): string {
12-
if (os === 'macos') {
14+
if (os === 'macos' || os === 'macos-intel') {
1315
return `Installation Instructions for macOS:
1416
1517
1. First, run the setup script by double-clicking "run_me_first.command"

apps/portal/src/utils/os.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export type SupportedOS = 'macos' | 'windows' | 'macos-intel';
2+
3+
export async function detectOSFromUserAgent(): Promise<SupportedOS | null> {
4+
try {
5+
const ua = navigator.userAgent.toLowerCase();
6+
7+
// Detect Windows
8+
if (ua.includes('win')) {
9+
return 'windows';
10+
}
11+
12+
// Detect macOS
13+
if (ua.includes('mac')) {
14+
// Try modern userAgentData API first (Chrome, Edge)
15+
if ('userAgentData' in navigator && navigator.userAgentData) {
16+
const data: { architecture?: string } = await (
17+
navigator.userAgentData as {
18+
getHighEntropyValues: (hints: string[]) => Promise<{ architecture?: string }>;
19+
}
20+
).getHighEntropyValues(['architecture']);
21+
22+
if (data.architecture === 'arm') return 'macos';
23+
if (data.architecture === 'x86') return 'macos-intel';
24+
}
25+
26+
// Fallback to userAgent string parsing
27+
if (ua.includes('arm64')) return 'macos';
28+
if (ua.includes('intel')) return 'macos-intel';
29+
30+
// Default to macos if we can't determine architecture
31+
return 'macos';
32+
}
33+
34+
return null;
35+
} catch (error) {
36+
console.error('Error detecting OS:', error);
37+
return null;
38+
}
39+
}

0 commit comments

Comments
 (0)