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
10 changes: 8 additions & 2 deletions .github/workflows/trigger-tasks-deploy-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ jobs:
- name: Install Email package dependencies
working-directory: ./packages/email
run: bun install --frozen-lockfile --ignore-scripts
- name: Generate Prisma client
- name: Build DB package
working-directory: ./packages/db
run: bunx prisma generate
run: bun run build
- name: Copy schema to app and generate client
working-directory: ./apps/app
run: |
mkdir -p prisma
cp ../../packages/db/dist/schema.prisma prisma/schema.prisma
bunx prisma generate
- name: 🚀 Deploy Trigger.dev
working-directory: ./apps/app
timeout-minutes: 20
Expand Down
3 changes: 3 additions & 0 deletions apps/api/buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ phases:
- '[ -n "$BASE_URL" ] || { echo "❌ BASE_URL is not set"; exit 1; }'
- '[ -n "$BETTER_AUTH_URL" ] || { echo "❌ BETTER_AUTH_URL is not set"; exit 1; }'
- '[ -n "$TRUST_APP_URL" ] || { echo "❌ TRUST_APP_URL is not set"; exit 1; }'
- '[ -n "$APP_AWS_BUCKET_NAME" ] || { echo "❌ APP_AWS_BUCKET_NAME is not set"; exit 1; }'
- '[ -n "$APP_AWS_ACCESS_KEY_ID" ] || { echo "❌ APP_AWS_ACCESS_KEY_ID is not set"; exit 1; }'
- '[ -n "$APP_AWS_SECRET_ACCESS_KEY" ] || { echo "❌ APP_AWS_SECRET_ACCESS_KEY is not set"; exit 1; }'

# Install only API workspace dependencies
- echo "Installing API dependencies only..."
Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/attachments/attachments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export class AttachmentsService {
// AWS configuration is validated at startup via ConfigModule
// Safe to access environment variables directly since they're validated
this.bucketName = process.env.APP_AWS_BUCKET_NAME!;

if (!process.env.APP_AWS_ACCESS_KEY_ID || !process.env.APP_AWS_SECRET_ACCESS_KEY) {
console.warn('AWS credentials are missing, S3 client may fail');
}

this.s3Client = new S3Client({
region: process.env.APP_AWS_REGION || 'us-east-1',
credentials: {
Expand Down
189 changes: 152 additions & 37 deletions apps/app/customPrismaExtension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { binaryForRuntime, BuildContext, BuildExtension, BuildManifest } from '@trigger.dev/build';
import assert from 'node:assert';
import { spawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import { cp } from 'node:fs/promises';
import { cp, mkdir } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';

export type PrismaExtensionOptions = {
Expand All @@ -14,6 +15,12 @@ export type PrismaExtensionOptions = {
dbPackageVersion?: string;
};

type ExtendedBuildContext = BuildContext & { workspaceDir?: string };
type SchemaResolution = {
path?: string;
searched: string[];
};

export function prismaExtension(options: PrismaExtensionOptions = {}): PrismaExtension {
return new PrismaExtension(options);
}
Expand Down Expand Up @@ -43,51 +50,47 @@ export class PrismaExtension implements BuildExtension {
return;
}

// Resolve the path to the schema from the published @trycompai/db package
// In a monorepo, node_modules are typically hoisted to the workspace root
// Walk up the directory tree to find the workspace root (where node_modules/@trycompai/db exists)
let workspaceRoot = context.workingDir;
let dbPackagePath = resolve(workspaceRoot, 'node_modules/@trycompai/db/dist/schema.prisma');

// If not found in working dir, try parent directories
while (!existsSync(dbPackagePath) && workspaceRoot !== dirname(workspaceRoot)) {
workspaceRoot = dirname(workspaceRoot);
dbPackagePath = resolve(workspaceRoot, 'node_modules/@trycompai/db/dist/schema.prisma');
}

this._resolvedSchemaPath = dbPackagePath;

context.logger.debug(`Workspace root: ${workspaceRoot}`);
context.logger.debug(
`Resolved the prisma schema from @trycompai/db package to: ${this._resolvedSchemaPath}`,
);
const resolution = this.tryResolveSchemaPath(context as ExtendedBuildContext);

// Debug: List contents of the @trycompai/db package directory
const dbPackageDir = resolve(workspaceRoot, 'node_modules/@trycompai/db');
const dbDistDir = resolve(workspaceRoot, 'node_modules/@trycompai/db/dist');

try {
const { readdirSync } = require('node:fs');
context.logger.debug(`@trycompai/db package directory contents:`, readdirSync(dbPackageDir));
context.logger.debug(`@trycompai/db/dist directory contents:`, readdirSync(dbDistDir));
} catch (err) {
context.logger.debug(`Failed to list directory contents:`, err);
}

// Check that the prisma schema exists in the published package
if (!existsSync(this._resolvedSchemaPath)) {
throw new Error(
`PrismaExtension could not find the prisma schema at ${this._resolvedSchemaPath}. Make sure @trycompai/db package is installed with version ${this.options.dbPackageVersion || 'latest'}`,
if (!resolution.path) {
context.logger.debug(
'Prisma schema not found during build start, likely before dependencies are installed.',
{ searched: resolution.searched },
);
return;
}

this._resolvedSchemaPath = resolution.path;
context.logger.debug(`Resolved prisma schema to ${resolution.path}`);
await this.ensureLocalPrismaClient(context as ExtendedBuildContext, resolution.path);
}

async onBuildComplete(context: BuildContext, manifest: BuildManifest) {
if (context.target === 'dev') {
return;
}

if (!this._resolvedSchemaPath || !existsSync(this._resolvedSchemaPath)) {
const resolution = this.tryResolveSchemaPath(context as ExtendedBuildContext);

if (!resolution.path) {
throw new Error(
[
'PrismaExtension could not find the prisma schema. Make sure @trycompai/db is installed',
`with version ${this.options.dbPackageVersion || 'latest'} and that its dist files are built.`,
'Searched the following locations:',
...resolution.searched.map((candidate) => ` - ${candidate}`),
].join('\n'),
);
}

this._resolvedSchemaPath = resolution.path;
}

assert(this._resolvedSchemaPath, 'Resolved schema path is not set');
const schemaPath = this._resolvedSchemaPath;

await this.ensureLocalPrismaClient(context as ExtendedBuildContext, schemaPath);

context.logger.debug('Looking for @prisma/client in the externals', {
externals: manifest.externals,
Expand All @@ -114,9 +117,9 @@ export class PrismaExtension implements BuildExtension {
// Copy the prisma schema from the published package to the build output path
const schemaDestinationPath = join(manifest.outputPath, 'prisma', 'schema.prisma');
context.logger.debug(
`Copying the prisma schema from ${this._resolvedSchemaPath} to ${schemaDestinationPath}`,
`Copying the prisma schema from ${schemaPath} to ${schemaDestinationPath}`,
);
await cp(this._resolvedSchemaPath, schemaDestinationPath);
await cp(schemaPath, schemaDestinationPath);

// Add prisma generate command to generate the client from the copied schema
commands.push(
Expand Down Expand Up @@ -176,4 +179,116 @@ export class PrismaExtension implements BuildExtension {
},
});
}

private async ensureLocalPrismaClient(
context: ExtendedBuildContext,
schemaSourcePath: string,
): Promise<void> {
const schemaDir = resolve(context.workingDir, 'prisma');
const schemaDestinationPath = resolve(schemaDir, 'schema.prisma');

await mkdir(schemaDir, { recursive: true });
await cp(schemaSourcePath, schemaDestinationPath);

const clientEntryPoint = resolve(context.workingDir, 'node_modules/.prisma/client/default.js');

if (existsSync(clientEntryPoint) && !process.env.TRIGGER_PRISMA_FORCE_GENERATE) {
context.logger.debug('Prisma client already generated locally, skipping regenerate.');
return;
}

const prismaBinary = this.resolvePrismaBinary(context.workingDir);

if (!prismaBinary) {
context.logger.debug(
'Prisma CLI not available yet, skipping local generate until install finishes.',
);
return;
}

context.logger.log('Prisma client missing. Generating before Trigger indexing.');
await this.runPrismaGenerate(context, prismaBinary, schemaDestinationPath);
}

private runPrismaGenerate(
context: ExtendedBuildContext,
prismaBinary: string,
schemaPath: string,
): Promise<void> {
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn(prismaBinary, ['generate', `--schema=${schemaPath}`], {
cwd: context.workingDir,
env: {
...process.env,
PRISMA_HIDE_UPDATE_MESSAGE: '1',
},
});

child.stdout?.on('data', (data: Buffer) => {
context.logger.debug(data.toString().trim());
});

child.stderr?.on('data', (data: Buffer) => {
context.logger.warn(data.toString().trim());
});

child.on('error', (error) => {
rejectPromise(error);
});

child.on('close', (code) => {
if (code === 0) {
resolvePromise();
} else {
rejectPromise(new Error(`prisma generate exited with code ${code}`));
}
});
});
}

private resolvePrismaBinary(workingDir: string): string | undefined {
const binDir = resolve(workingDir, 'node_modules', '.bin');
const executable = process.platform === 'win32' ? 'prisma.cmd' : 'prisma';
const binaryPath = resolve(binDir, executable);

if (!existsSync(binaryPath)) {
return undefined;
}

return binaryPath;
}

private tryResolveSchemaPath(context: ExtendedBuildContext): SchemaResolution {
const candidates = this.buildSchemaCandidates(context);
const path = candidates.find((candidate) => existsSync(candidate));
return { path, searched: candidates };
}

private buildSchemaCandidates(context: ExtendedBuildContext): string[] {
const candidates = new Set<string>();

const addNodeModuleCandidates = (start: string | undefined) => {
if (!start) {
return;
}

let current = start;
while (true) {
candidates.add(resolve(current, 'node_modules/@trycompai/db/dist/schema.prisma'));
const parent = dirname(current);
if (parent === current) {
break;
}
current = parent;
}
};

addNodeModuleCandidates(context.workingDir);
addNodeModuleCandidates(context.workspaceDir);

candidates.add(resolve(context.workingDir, '../../packages/db/dist/schema.prisma'));
candidates.add(resolve(context.workingDir, '../packages/db/dist/schema.prisma'));

return Array.from(candidates);
}
}
3 changes: 2 additions & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"playwright-core": "^1.52.0",
"posthog-js": "^1.236.6",
"posthog-node": "^5.8.2",
"prisma": "^6.13.0",
"puppeteer-core": "^24.7.2",
"react": "^19.1.1",
"react-dom": "^19.1.0",
Expand Down Expand Up @@ -133,7 +134,6 @@
"glob": "^11.0.3",
"jsdom": "^26.1.0",
"postcss": "^8.5.4",
"prisma": "^6.13.0",
"raw-loader": "^4.0.2",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3",
Expand Down Expand Up @@ -164,6 +164,7 @@
"dev": "bun i && bunx concurrently --kill-others --names \"next,trigger\" --prefix-colors \"yellow,blue\" \"next dev --turbo -p 3000\" \"bunx trigger.dev@4.0.6 dev\"",
"lint": "next lint && prettier --check .",
"prebuild": "bun run db:generate",
"postinstall": "prisma generate --schema=./prisma/schema.prisma || exit 0",
"start": "next start",
"test": "vitest",
"test:all": "./scripts/test-all.sh",
Expand Down
1 change: 1 addition & 0 deletions apps/app/scripts/trigger-generate-prisma-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
'use client';

import {
MAC_APPLE_SILICON_FILENAME,
MAC_INTEL_FILENAME,
WINDOWS_FILENAME,
} from '@/app/api/download-agent/constants';
import { detectOSFromUserAgent, SupportedOS } from '@/utils/os';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@comp/ui/accordion';
import { Button } from '@comp/ui/button';
Expand Down Expand Up @@ -51,6 +56,11 @@ export function DeviceAgentAccordionItem({
const isCompleted = hasInstalledAgent && failedPoliciesCount === 0;

const handleDownload = async () => {
if (!detectedOS) {
toast.error('Could not detect your OS. Please refresh and try again.');
return;
}

setIsDownloading(true);

try {
Expand All @@ -61,6 +71,7 @@ export function DeviceAgentAccordionItem({
body: JSON.stringify({
orgId: member.organizationId,
employeeId: member.id,
os: detectedOS,
}),
});

Expand All @@ -73,20 +84,17 @@ export function DeviceAgentAccordionItem({

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

// Method 1: Using a temporary link (most reliable)
const a = document.createElement('a');
a.href = downloadUrl;

// Set filename based on OS and architecture
if (isMacOS) {
a.download =
detectedOS === 'macos'
? 'Comp AI Agent-1.0.0-arm64.dmg'
: 'Comp AI Agent-1.0.0-intel.dmg';
a.download = detectedOS === 'macos' ? MAC_APPLE_SILICON_FILENAME : MAC_INTEL_FILENAME;
} else {
a.download = 'Comp AI Agent 1.0.0.exe';
a.download = WINDOWS_FILENAME;
}

document.body.appendChild(a);
Expand Down
3 changes: 3 additions & 0 deletions apps/portal/src/app/api/download-agent/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const MAC_APPLE_SILICON_FILENAME = 'Comp AI Agent-1.0.0-arm64.dmg';
export const MAC_INTEL_FILENAME = 'Comp AI Agent-1.0.0.dmg';
export const WINDOWS_FILENAME = 'Comp AI Agent 1.0.0.exe';
Loading
Loading