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
31 changes: 16 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,57 +66,58 @@ jobs:
# - On main: uses last successful CI run
- uses: nrwl/nx-set-shas@v4

- uses: pnpm/action-setup@v4

# Cache node_modules
- uses: actions/setup-node@v4
with:
node-version: 24

- uses: oven-sh/setup-bun@v2
cache: pnpm

- name: Install dependencies
run: bun install --frozen-lockfile
run: pnpm install --frozen-lockfile

- name: Install chromium browser
run: bunx playwright install chromium --with-deps
run: pnpm exec playwright install chromium --with-deps

- name: Generate Prisma client
run: bunx nx run @zuko/models:prisma:generate
run: pnpm exec nx run @zuko/models:prisma:generate

- name: Migrate Prisma (deploy)
run: bunx prisma migrate deploy
run: pnpm exec prisma migrate deploy
working-directory: libs/models

- name: Seed E2E test data
run: bunx nx run @zuko/models:seed
run: pnpm exec nx run @zuko/models:seed

# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud

# Lint & Format (parallel)
- name: Format check
run: bun run fmt:check
run: pnpm run fmt:check

- name: Lint
run: bunx nx affected -t lint --parallel=3 --nxBail
run: pnpm exec nx affected -t lint --parallel=3 --nxBail

# Dead code check (per-project, cached — @berenddeboer/nx-knip plugin)
# TODO: remove continue-on-error once existing dead code is resolved
- name: Knip
run: bunx nx affected -t knip --parallel=3
run: pnpm exec nx affected -t knip --parallel=3

# typecheck & Build (parallel)
- name: Typecheck
run: bunx nx affected -t typecheck --parallel=50% --nxBail
run: pnpm exec nx affected -t typecheck --parallel=50% --nxBail

- name: Build
run: bunx nx affected -t build --parallel=50% --nxBail
run: pnpm exec nx affected -t build --parallel=50% --nxBail

# Tests (Sequential)
- name: Unit tests
run: bunx nx affected -t test --parallel=1 --nxBail
run: pnpm exec nx affected -t test --parallel=1 --nxBail

- name: E2E tests
run: bunx nx affected -t e2e --exclude=backend-e2e --exclude=ai-agents-e2e --nxBail
run: pnpm exec nx affected -t e2e --exclude=backend-e2e --exclude=ai-agents-e2e --nxBail

# Nx Cloud recommends fixes for failures to help you get CI green faster. Learn more: https://nx.dev/ci/features/self-healing-ci
- run: bunx nx fix-ci
- run: pnpm exec nx fix-ci
if: always()
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,7 @@ nango-data/
# Claude Code agent skills
.agents/
skills-lock.json

.nx/polygraph
.nx/self-healing
.nx/migrate-runs
1 change: 0 additions & 1 deletion .npmrc

This file was deleted.

36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Zuko is an **agentic CRM** — a monorepo with a **Next.js web app** and **NestJ
## Prerequisites

- **Node.js** 24 (matches CI)
- **bun**
- **pnpm** 11 (`corepack enable pnpm` — the version is pinned in `package.json`)
- **PostgreSQL** (for the backend database)

## Setup
Expand All @@ -21,7 +21,7 @@ git clone <repository-url> zuko && cd zuko
### 2. Install dependencies

```sh
bun install
pnpm install
```

### 3. Environment variables
Expand Down Expand Up @@ -54,58 +54,58 @@ Generate the Prisma client and run migrations:

```sh
# Generate Prisma client
bun nx run @zuko/models:prisma:generate
pnpm exec nx run @zuko/models:prisma:generate

# Run migrations (creates/updates DB schema)
bun nx run @zuko/models:prisma:migrate -- --name init
pnpm exec nx run @zuko/models:prisma:migrate -- --name init

# Optional: seed test data
bun nx run @zuko/models:seed
pnpm exec nx run @zuko/models:seed
```

## Running the app

**Recommended — backend + web together:**

```sh
bun nx run @zuko/web:dev
pnpm exec nx run @zuko/web:dev
```

This starts the NestJS backend (e.g. port 3001) and the Next.js app (e.g. port 3000).

**AI Agents only:**

```sh
bun nx run @zuko/ai-agents:dev
pnpm exec nx run @zuko/ai-agents:dev
```

Starts the LangGraph-based agents service.

**Backend only:**

```sh
bun nx run @zuko/backend:serve
pnpm exec nx run @zuko/backend:serve
```

**Build (production):**

```sh
bun nx run @zuko/backend:build
bun nx run @zuko/web:build
bun nx run @zuko/ai-agents:build
pnpm exec nx run @zuko/backend:build
pnpm exec nx run @zuko/web:build
pnpm exec nx run @zuko/ai-agents:build
```

## Tests

- **Unit tests:** `bun nx run @zuko/backend:test`, `bun nx run @zuko/web:test`
Or for affected projects: `bun nx affected -t test`
- **Unit tests:** `pnpm exec nx run @zuko/backend:test`, `pnpm exec nx run @zuko/web:test`
Or for affected projects: `pnpm exec nx affected -t test`
- **E2E (web):** Run Playwright against the web app locally with the test environment (see [apps/web-e2e/README.md](apps/web-e2e/README.md) for setup):

```sh
NODE_ENV=test bunx nx run web-e2e:e2e
NODE_ENV=test pnpm exec nx run web-e2e:e2e
```

- **Lint / typecheck:** `bun nx affected -t lint`, `bun nx affected -t typecheck`
- **Lint / typecheck:** `pnpm exec nx affected -t lint`, `pnpm exec nx affected -t typecheck`

## Project structure

Expand Down Expand Up @@ -136,9 +136,9 @@ bun nx run @zuko/ai-agents:build

This workspace is powered by [Nx](https://nx.dev). Useful commands:

- **Explore project graph:** `bun nx graph`
- **List targets for a project:** `bun nx show project @zuko/backend` (or `@zuko/web`)
- **Run tasks:** Use `bun nx run <project>:<target>` — e.g. `@zuko/backend`, `@zuko/web`, `@zuko/models`. [Nx run tasks](https://nx.dev/features/run-tasks).
- **Explore project graph:** `pnpm exec nx graph`
- **List targets for a project:** `pnpm exec nx show project @zuko/backend` (or `@zuko/web`)
- **Run tasks:** Use `pnpm exec nx run <project>:<target>` — e.g. `@zuko/backend`, `@zuko/web`, `@zuko/models`. [Nx run tasks](https://nx.dev/features/run-tasks).
- **IDE:** [Nx Console](https://nx.dev/getting-started/editor-setup) for VSCode/IntelliJ.

## License
Expand Down
21 changes: 13 additions & 8 deletions apps/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Build the docker image with `bunx nx docker:build @zuko/backend`.
# This expects the build output from `bunx nx run @zuko/backend:build` and `bunx nx run @zuko/backend:prune`
# Build the docker image with `pnpm exec nx docker:build @zuko/backend`.
# This expects the build output from `pnpm exec nx run @zuko/backend:build` and `pnpm exec nx run @zuko/backend:prune`
#
FROM node:24-bullseye AS base

RUN apt-get update && apt-get install -y openssl curl && \
curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:$PATH"
RUN apt-get update && apt-get install -y openssl && \
rm -rf /var/lib/apt/lists/* && \
corepack enable && corepack prepare pnpm@11.26.0 --activate

ENV HOST=0.0.0.0
ENV PORT=3000
Expand All @@ -21,8 +21,13 @@ COPY apps/backend/dist .

EXPOSE 3001

# Install production dependencies using bun
RUN bun install --production
# Install production dependencies. dist/package.json has every version pinned
# exactly by the pin-dist-versions target, so this resolves deterministically
# without a lockfile, and the dist/pnpm-workspace.yaml that prune-lockfile emits
# carries the workspace's build-script approvals (@prisma/engines needs one).
# --node-linker=hoisted keeps the flat node_modules layout the bundle resolves
# against.
RUN pnpm install --prod --node-linker=hoisted

# Copy multi-file prisma schemas (not included by copy-workspace-modules)
COPY libs/models/prisma ./workspace_modules/@zuko/models/prisma
Expand All @@ -31,6 +36,6 @@ COPY libs/models/prisma ./workspace_modules/@zuko/models/prisma
# Point to directory for multi-file schema support
# Use dummy DATABASE_URL since generate doesn't connect to database
RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" \
bunx prisma generate --schema=./workspace_modules/@zuko/models/prisma
pnpm exec prisma generate --schema=./workspace_modules/@zuko/models/prisma

CMD [ "node", "main.js" ]
32 changes: 10 additions & 22 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
"version": "0.0.1",
"private": true,
"scripts": {
"pin:dist-versions": "bun scripts/pin-dist-versions.ts",
"provision:agent-host": "bun scripts/provision-agent-host.ts"
"pin:dist-versions": "tsx scripts/pin-dist-versions.ts",
"provision:agent-host": "tsx scripts/provision-agent-host.ts"
},
"type": "module",
"dependencies": {
"@ai-sdk/openai": "^4.0.17",
"@auth/agent": "^0.4.5",
"@better-auth/agent-auth": "^0.4.5",
"@better-auth/oauth-provider": "^1.6.22",
"@better-auth/oauth-provider": "1.6.22",
"@modelcontextprotocol/sdk": "^1.29.0",
"@nangohq/node": "^0.70.9",
"@nestjs/common": "^11.0.0",
Expand All @@ -26,12 +26,12 @@
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"@thallesp/nestjs-better-auth": "^2.5.1",
"@zuko/core": "^0.0.1",
"@zuko/models": "^0.0.1",
"@zuko/sales": "^0.0.1",
"@zuko/core": "workspace:*",
"@zuko/models": "workspace:*",
"@zuko/sales": "workspace:*",
"ai": "^6.0.70",
"axios": "^1.6.0",
"better-auth": "^1.6.22",
"better-auth": "1.6.22",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"dayjs": "^1.11.19",
Expand Down Expand Up @@ -98,7 +98,7 @@
"executor": "@nx/js:prune-lockfile",
"outputs": [
"{workspaceRoot}/apps/backend/dist/package.json",
"{workspaceRoot}/apps/backend/dist/package-lock.json"
"{workspaceRoot}/apps/backend/dist/pnpm-workspace.yaml"
],
"options": {
"buildTarget": "build"
Expand All @@ -117,32 +117,20 @@
"buildTarget": "build"
}
},
"fix-lockfile-symlinks": {
"dependsOn": [
"prune-lockfile"
],
"executor": "nx:run-commands",
"options": {
"command": "for lib in agents core models sales; do if [ -f dist/package-lock.json ]; then sed -e \"s|\\\"resolved\\\": \\\"libs/$lib\\\"|\\\"resolved\\\": \\\"workspace_modules/@zuko/$lib\\\"|g\" -e \"s|\\\"libs/$lib\\\":|\\\"workspace_modules/@zuko/$lib\\\":|g\" dist/package-lock.json > dist/package-lock.json.tmp && mv dist/package-lock.json.tmp dist/package-lock.json; fi; if [ -f dist/package.json ]; then sed -e \"s|\\\"@zuko/$lib\\\": \\\"[^,}]*\\\"|\\\"@zuko/$lib\\\": \\\"file:./workspace_modules/@zuko/$lib\\\"|g\" dist/package.json > dist/package.json.tmp && mv dist/package.json.tmp dist/package.json; fi; done",
"cwd": "apps/backend"
}
},
"pin-dist-versions": {
"dependsOn": [
"prune-lockfile",
"fix-lockfile-symlinks"
"prune-lockfile"
],
"executor": "nx:run-commands",
"options": {
"command": "bun run pin:dist-versions",
"command": "pnpm run pin:dist-versions",
"cwd": "apps/backend"
}
},
"prune": {
"dependsOn": [
"prune-lockfile",
"copy-workspace-modules",
"fix-lockfile-symlinks",
"pin-dist-versions"
],
"executor": "nx:noop"
Expand Down
33 changes: 20 additions & 13 deletions apps/backend/scripts/pin-dist-versions.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
/**
* Pin the pruned dist/package.json to the versions the workspace actually installed.
*
* The backend image runs `bun install --production` against dist/package.json
* (see apps/backend/Dockerfile). That install has no lockfile to go on, because
* @nx/js:prune-lockfile cannot emit one for bun -- it prints "Bun lockfile
* generation is not supported. Only package.json will be generated."
* The backend image runs `pnpm install --prod` against dist/package.json (see
* apps/backend/Dockerfile). That install has no lockfile to go on:
* @nx/js:prune-lockfile emits a pnpm-lock.yaml that is a copy of the whole
* workspace graph rather than a pruned one, so its importers do not match the
* single-package dist/package.json. We delete it below.
*
* So every dependency's caret range is re-resolved against the registry at image
* build time, and two builds of the same commit can produce different images.
* That is not hypothetical: `better-auth` is declared `^1.6.22`, the workspace
* That is not hypothetical: `better-auth` was declared `^1.6.22`, the workspace
* resolved 1.6.22, and a later clean build picked up 1.7.5, which dropped the
* `verifyAccessToken` export that mcp-bearer.guard.ts imports -- so the bundle
* threw at module load and the machine never came up. The root `overrides` block
* is dropped by pruning too, which is why the image installed ai@6 while the
* workspace runs ai@7.
* threw at module load and the machine never came up. (That one is now pinned
* to 1.6.22 in the manifests, but every other caret range is still live.) The
* root `overrides` block is dropped by pruning too, which is why the image
* installed ai@6 while the workspace runs ai@7.
*
* Rewriting each range to the exact installed version makes the image match what
* was built, linted and tested. Workspace packages are left alone: they are
* already rewritten to `file:` paths by the fix-lockfile-symlinks target.
* was built, linted and tested. Workspace packages are left alone: prune-lockfile
* already rewrites their `workspace:*` ranges to `file:` paths.
*
* This also deletes the lockfile prune-lockfile leaves behind, so the image does
* not install against a manifest and lockfile that disagree.
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';

const distPackageJson = resolve(import.meta.dirname, '../dist/package.json');
const distLockfile = resolve(import.meta.dirname, '../dist/pnpm-lock.yaml');
const appDir = resolve(import.meta.dirname, '..');
const workspaceRoot = resolve(import.meta.dirname, '../../..');

Expand Down Expand Up @@ -69,18 +75,19 @@ for (const [name, range] of Object.entries(deps)) {
if (unresolved.length > 0) {
console.error(
`Could not resolve an installed version for: ${unresolved.join(', ')}.\n` +
'Run `bun install` at the workspace root before building the image.',
'Run `pnpm install` at the workspace root before building the image.',
);
process.exit(1);
}

// The image only ever runs `bun install --production`, and the nx block is build
// The image only ever runs `pnpm install --prod`, and the nx block is build
// metadata that the runtime has no use for. Dropping both keeps the manifest to
// what the image actually installs.
delete pkg.devDependencies;
delete pkg.nx;

writeFileSync(distPackageJson, `${JSON.stringify(pkg, null, 2)}\n`);
rmSync(distLockfile, { force: true });

console.log(
`Pinned ${Object.keys(deps).length} dependencies to installed versions.`,
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/scripts/provision-agent-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* external harness needs to register its agents using the numeric-issuer path.
*
* Run from apps/backend (reads DATABASE_URL and BETTER_AUTH_SECRET from env):
* bun run provision:agent-host [host-name]
* pnpm run provision:agent-host [host-name]
*/
import { PrismaClient } from '@zuko/models';
import { PrismaPg } from '@prisma/adapter-pg';
Expand Down
Loading