# PIG — production image.
#
# Multi-stage so the runtime image carries no build toolchain and no source
# maps. The front end is built into the API's static directory and served from
# the same origin, which matters for more than tidiness: auth sessions are
# per-origin, so splitting the app across two hostnames turns sign-in into a
# redirect loop that looks like a broken deployment.

FROM node:22-alpine AS build
WORKDIR /app

# Corepack installs the exact pnpm pinned by `packageManager`, so the image
# builds with the same version as CI and as a developer's laptop.
#
# Both variables are load-bearing in a container build, and neither is
# optional:
#   - the download prompt cannot be answered by a non-interactive build;
#   - CI=true is what stops pnpm asking for confirmation before it touches a
#     modules directory it considers stale. Without it the build fails with
#     ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY, which reads like a bug but is
#     pnpm correctly refusing to delete files nobody confirmed.
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
ENV CI=true
RUN corepack enable

# Manifests and the lockfile first, so a dependency install is cached across
# source-only edits. pnpm needs every workspace manifest present to resolve the
# graph, hence the file-by-file copy rather than `COPY . .`.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/core/package.json packages/core/
COPY packages/db/package.json packages/db/
COPY packages/prime/package.json packages/prime/
COPY apps/api/package.json apps/api/
COPY apps/web/package.json apps/web/
COPY apps/mcp/package.json apps/mcp/
COPY apps/cli/package.json apps/cli/
COPY apps/piggy/package.json apps/piggy/
RUN pnpm install --frozen-lockfile

COPY . .

# Typecheck as a build gate. A deploy that does not compile should fail here,
# loudly, rather than at runtime in front of a user.
RUN pnpm run typecheck

RUN pnpm -F @pig/web run build

# ---------------------------------------------------------------- runtime
FROM node:22-alpine AS runtime
WORKDIR /app

ENV NODE_ENV=production
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
ENV CI=true
RUN corepack enable

# Install production dependencies only. The server runs TypeScript directly, so
# tsx is declared in `dependencies` rather than `devDependencies` — it is
# genuinely needed at runtime, and pretending otherwise meant the old image had
# to reinstall it by hand after pruning.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/core/package.json packages/core/
COPY packages/db/package.json packages/db/
COPY packages/prime/package.json packages/prime/
COPY apps/api/package.json apps/api/
COPY apps/mcp/package.json apps/mcp/
COPY apps/cli/package.json apps/cli/
COPY apps/piggy/package.json apps/piggy/
# apps/web is a build-time workspace only; its manifest is still required for
# the lockfile to resolve, but none of its dependencies are installed here.
COPY apps/web/package.json apps/web/
RUN pnpm install --frozen-lockfile --prod --ignore-scripts

COPY packages ./packages
COPY apps/api ./apps/api
COPY apps/mcp ./apps/mcp
COPY apps/cli ./apps/cli
COPY apps/piggy ./apps/piggy
COPY --from=build /app/apps/web/dist ./apps/web/dist

# Two build gates for Piggy, both of which exist because the alternative is a
# container that crash-loops in production for a reason no log makes obvious.
#
# 1. apps/piggy/src/agent/models.json is READ AT BOOT, not imported — it is the
#    provider document the harness registers Prime Inference from, and Piggy's
#    config schema validates the default model against it before the process
#    will start. It arrives here inside `COPY apps/piggy`, so nothing special
#    is needed to ship it; what is needed is a guard against a future
#    .dockerignore rule or a narrowed COPY quietly dropping it. Parsed rather
#    than merely stat'd, because a truncated copy is the interesting failure.
#
# 2. The Prime Agent SDK is installed with --ignore-scripts, and it drags in a
#    large transitive tree (@google/genai, protobufjs) whose install scripts are
#    denied in pnpm-workspace.yaml on purpose. Importing the SDK here proves the
#    scriptless install produced a loadable module graph rather than one that
#    needs a postinstall to have generated something. If a future version of the
#    harness genuinely requires a build step, this fails at `docker build` in
#    front of whoever changed the dependency, not at 03:00 in front of the
#    on-call.
RUN node -e "const d=JSON.parse(require('node:fs').readFileSync('apps/piggy/src/agent/models.json','utf8'));const n=d.providers['prime-inference'].models.length;if(!n)throw new Error('models.json has no models');console.log('models.json ok:',n,'models')" \
  && cd apps/piggy \
  && node --input-type=module -e "await import('@earendil-works/pi-coding-agent');console.log('pi-coding-agent imports under a scriptless production install')"

# Where the Prime Agent harness keeps its own state: the models.json Piggy
# writes for it at boot, plus whatever else it decides to keep alongside —
# a models-store.json appeared there on the first real turn.
#
# Deliberately NOT the default `~/.pig/piggy-agent`. Under `docker run` that
# resolves to /home/node and happens to work, because Docker sets HOME from the
# passwd entry. It is not a property to rely on: a runtime that starts this
# image with a numeric user and no matching passwd entry — `runAsUser: 1000`
# under Kubernetes, most obviously — leaves HOME unset, os.homedir() falls back
# to `/`, and the mkdir fails against a root-owned root directory. That takes
# the agent down on its first turn, long after the deploy reported success.
#
# Deliberately NOT under /app either, and this one is a security property
# rather than a convenience. The harness discovers extensions, skills and
# context files from its cwd, and Piggy points the harness's cwd at this
# directory. Anything reachable from here can end up in a CRM agent's prompt,
# so it must never be the checkout and must never be a bind mount of one.
#
# Created in the image, owned by node, 0700: a directory that exists with the
# right owner is one the unprivileged process can write without a startup
# chown, and a named volume mounted here would inherit this ownership rather
# than arriving root-owned.
RUN mkdir -p /var/lib/piggy-agent && chown node:node /var/lib/piggy-agent && chmod 700 /var/lib/piggy-agent

# Run unprivileged. The node image ships a `node` user for exactly this.
RUN chown -R node:node /app
USER node

EXPOSE 8920

# The health endpoint is unauthenticated by design, so this works without
# credentials baked into the image.
#
# This is the API's check, and only the API's. The piggy container runs a
# different command on this same image and serves nothing on 8920, so it MUST
# override this — it does, in docker-compose.yml, against Piggy's own
# /internal/health. Inherited unchanged it reported unhealthy for ever while
# working perfectly, which is worse than no check at all.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:8920/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

CMD ["pnpm", "exec", "tsx", "apps/api/src/server.ts"]
