Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@ RUN curl -fsSL https://bun.sh/install | bash \
&& export PATH="/root/.bun/bin:$PATH" \
&& bun install --production --ignore-scripts || true

# Copy pre-built workspace packages (must be before copying app contents)
COPY node_modules/@trycompai ./node_modules/@trycompai
COPY node_modules/@prisma ./node_modules/@prisma
COPY node_modules/.prisma ./node_modules/.prisma

# Now copy the pre-built app contents (dist/, prisma/, etc.)
COPY . .

# Generate Prisma client inside the image (ensures runtime client matches installed deps)
RUN npx prisma generate

# Set environment variables
ENV NODE_ENV=production
ENV PORT=3333
Expand Down
31 changes: 6 additions & 25 deletions apps/api/buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,67 +32,48 @@ phases:
- echo "Installing API dependencies only..."
- bun install --filter=@comp/api --frozen-lockfile || bun install --filter=@comp/api --ignore-scripts || bun install --ignore-scripts

# Build email package (required dependency for API)
- echo "Building @trycompai/email package..."
- cd packages/email
- bun run build
- cd ../..

# Build NestJS application (prebuild automatically handles Prisma)
- echo "Building NestJS application..."
- echo "APP_NAME is set to $APP_NAME"
- echo "Current directory $(pwd)"
- echo "Available apps $(ls -la apps/)"
- cd apps/api
- echo "Changed to $(pwd)"
- echo "Running build (includes automatic prebuild db:generate)..."
- bun run build

# Verify build output exists
- echo "Checking build output..."
- ls -la dist/
- ls -la dist/src/
- '[ -f "dist/src/main.js" ] || { echo "❌ main.js not found in dist/src"; exit 1; }'

# Create self-contained bundle for Docker (stay in apps/api)
- echo "Creating self-contained bundle..."
- mkdir -p ../docker-build

# Copy built application (preserves NestJS structure)
- echo "Copying built application..."
- cp -r dist/* ../docker-build/

# Copy prisma folder (needed for runtime imports)
- echo "Copying prisma folder..."
- cp -r prisma ../docker-build/

# Verify files were copied correctly
- echo "Verifying copied files..."
- ls -la ../docker-build/
- ls -la ../docker-build/src/
- '[ -f "../docker-build/src/main.js" ] || { echo "❌ main.js not found in docker-build/src"; exit 1; }'

# Copy entire node_modules for runtime (includes @trycompai/db from npm)
- echo "Skipping host node_modules copy; Dockerfile installs prod deps inside image"

# Copy built workspace packages (needed for runtime)
# Only copy packages that are actually imported at runtime
# Email package is already bundled, so only need its dist
- echo "Copying built workspace packages..."
- mkdir -p ../docker-build/node_modules/@trycompai
# Email package - copy dist and package.json (needed at runtime)
- mkdir -p ../docker-build/node_modules/@trycompai/email
- cp -r ../../packages/email/dist ../docker-build/node_modules/@trycompai/email/
- cp ../../packages/email/package.json ../docker-build/node_modules/@trycompai/email/
- mkdir -p ../docker-build/node_modules/@trycompai/utils
- cp -r ../../packages/utils/src ../docker-build/node_modules/@trycompai/utils/
- cp ../../packages/utils/package.json ../docker-build/node_modules/@trycompai/utils/

- cp -r ../../node_modules/@prisma ../docker-build/node_modules/@prisma
- cp -r ../../node_modules/.prisma ../docker-build/node_modules/.prisma

# Copy Dockerfile
- echo "Copying Dockerfile..."
- cp Dockerfile ../docker-build/
- echo "Copying package manifests for image install..."
- cp package.json ../docker-build/
- cp ../../bun.lock ../docker-build/ || true

# Build Docker image
- echo "Building Docker image..."
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t $ECR_REPOSITORY_URI:$IMAGE_TAG ../docker-build/
- docker tag $ECR_REPOSITORY_URI:$IMAGE_TAG $ECR_REPOSITORY_URI:latest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@comp/ui/tooltip';
import type { Member } from '@db';
import { CheckCircle2, Circle, Download, HelpCircle, Loader2, XCircle } from 'lucide-react';
import Image from 'next/image';
import { useEffect, useMemo, useState } from 'react';
import { toast } from 'sonner';
import type { FleetPolicy, Host } from '../../types';
Expand Down Expand Up @@ -43,7 +42,10 @@ export function DeviceAgentAccordionItem({

const hasInstalledAgent = host !== null;
const failedPoliciesCount = useMemo(() => {
return fleetPolicies.filter((policy) => policy.response !== 'pass').length + (!isMacOS || mdmEnabledStatus.response === 'pass' ? 0 : 1);
return (
fleetPolicies.filter((policy) => policy.response !== 'pass').length +
(!isMacOS || mdmEnabledStatus.response === 'pass' ? 0 : 1)
);
}, [fleetPolicies, mdmEnabledStatus, isMacOS]);

const isCompleted = hasInstalledAgent && failedPoliciesCount === 0;
Expand Down Expand Up @@ -84,7 +86,7 @@ export function DeviceAgentAccordionItem({
? 'Comp AI Agent-1.0.0-arm64.dmg'
: 'Comp AI Agent-1.0.0-intel.dmg';
} else {
a.download = 'compai-device-agent.zip';
a.download = 'Comp AI Agent 1.0.0.exe';
}

document.body.appendChild(a);
Expand Down Expand Up @@ -189,35 +191,13 @@ export function DeviceAgentAccordionItem({
</Button>
</div>
</li>
{!isMacOS && (
<li>
<strong>Run the "Install Me First" file</strong>
<p className="mt-1">
After extracting the downloaded zip file, locate and run the "Install Me
First" file to prepare your system.
</p>
</li>
)}
<li>
<strong>
{isMacOS
? 'Install the Comp AI Device Agent'
: 'Run the Comp AI Device Agent installer'}
</strong>
<strong>Install the Comp AI Device Agent</strong>
<p className="mt-1">
{isMacOS
? 'Double-click the downloaded DMG file and follow the installation instructions.'
: 'Follow the installation wizard steps. When you reach the introduction screen (as shown below), click "Continue" to proceed through the installation.'}
: 'Double-click the downloaded EXE file and follow the installation instructions.'}
</p>
{!isMacOS && (
<Image
src="/osquery-agent.jpeg"
alt="Fleet osquery installer introduction screen"
width={600}
height={400}
className="mt-2 rounded-xs border"
/>
)}
</li>
{isMacOS ? (
<li>
Expand Down Expand Up @@ -283,7 +263,9 @@ export function DeviceAgentAccordionItem({
<div
className={cn(
'hover:bg-muted/50 flex items-center justify-between rounded-md border border-l-4 p-3 shadow-sm transition-colors',
mdmEnabledStatus.response === 'pass' ? 'border-l-green-500' : 'border-l-red-500',
mdmEnabledStatus.response === 'pass'
? 'border-l-green-500'
: 'border-l-red-500',
)}
>
<div className="flex items-center gap-2">
Expand Down
81 changes: 10 additions & 71 deletions apps/portal/src/app/api/download-agent/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,8 @@ import { logger } from '@/utils/logger';
import { s3Client } from '@/utils/s3';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { client as kv } from '@comp/kv';
import archiver from 'archiver';
import { type NextRequest, NextResponse } from 'next/server';
import { PassThrough, Readable } from 'stream';
import {
generateWindowsScript,
getPackageFilename,
getReadmeContent,
getScriptFilename,
} from './scripts';
import type { SupportedOS } from './types';
import { Readable } from 'stream';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -41,15 +33,7 @@ export async function GET(req: NextRequest) {
// Delete token after retrieval (one-time use)
await kv.del(`download:${token}`);

const { orgId, employeeId } = downloadInfo as {
orgId: string;
employeeId: string;
userId: string;
};

// Hardcoded device marker paths used by the setup scripts
const fleetDevicePathMac = '/Users/Shared/.fleet';
const fleetDevicePathWindows = 'C:\\ProgramData\\CompAI\\Fleet';
const fleetBucketName = process.env.FLEET_AGENT_BUCKET_NAME;

if (!fleetBucketName) {
Expand Down Expand Up @@ -94,44 +78,9 @@ export async function GET(req: NextRequest) {
}
}

// Windows flow: Generate script and create zip
const fleetDevicePath = fleetDevicePathWindows;
const script = generateWindowsScript({ orgId, employeeId, fleetDevicePath });

// Windows flow: Generate script and create zip const fleetDevicePath = fleetDevicePathWindows;
try {
// Create a passthrough stream for the response
const passThrough = new PassThrough();
const archive = archiver('zip', { zlib: { level: 9 } });

// Pipe archive to passthrough
archive.pipe(passThrough);

// Robust error handling for staging/prod reliability
archive.on('error', (err) => {
logger('archiver_error', { message: err?.message, stack: (err as Error)?.stack });
passThrough.destroy(err as Error);
});
archive.on('warning', (warn) => {
logger('archiver_warning', { message: (warn as Error)?.message });
});
passThrough.on('error', (err) => {
logger('download_stream_error', {
message: (err as Error)?.message,
stack: (err as Error)?.stack,
});
});

// Add script file
const scriptFilename = getScriptFilename(os as SupportedOS);
archive.append(script, { name: scriptFilename, mode: 0o755 });

// Add README
const readmeContent = getReadmeContent(os as SupportedOS);
archive.append(readmeContent, { name: 'README.txt' });

// Get package from S3 and stream it
const packageFilename = getPackageFilename(os as SupportedOS);
const windowsPackageFilename = 'fleet-osquery.msi';
const windowsPackageFilename = 'Comp AI Agent 1.0.0.exe';
const packageKey = `windows/${windowsPackageFilename}`;

const getObjectCommand = new GetObjectCommand({
Expand All @@ -141,29 +90,19 @@ export async function GET(req: NextRequest) {

const s3Response = await s3Client.send(getObjectCommand);

if (s3Response.Body) {
const s3Stream = s3Response.Body as Readable;
s3Stream.on('error', (err) => {
logger('s3_stream_error', {
message: (err as Error)?.message,
stack: (err as Error)?.stack,
});
passThrough.destroy(err as Error);
});
archive.append(s3Stream, { name: packageFilename, store: true });
if (!s3Response.Body) {
return new NextResponse('Executable file not found', { status: 404 });
}

// Finalize the archive
archive.finalize();

// Convert Node.js stream to Web Stream for NextResponse
const webStream = Readable.toWeb(passThrough) as unknown as ReadableStream;
// Convert S3 stream to Web Stream for NextResponse
const s3Stream = s3Response.Body as Readable;
const webStream = Readable.toWeb(s3Stream) as unknown as ReadableStream;

// Return streaming response with headers that trigger browser download
return new NextResponse(webStream, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="compai-device-agent-${os}.zip"`,
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${windowsPackageFilename}"`,
'Cache-Control': 'no-cache, no-store, must-revalidate',
'X-Accel-Buffering': 'no',
},
Expand Down
Loading
Loading