feat(runtime): start and ship the local agent

This commit is contained in:
Nicolas Meienberger 2026-04-07 22:00:43 +02:00
parent 70c7de1efc
commit 78758a6c2c
No known key found for this signature in database
6 changed files with 81 additions and 2 deletions

View file

@ -11,6 +11,7 @@
!**/components.json !**/components.json
!app/** !app/**
!apps/agent/**
!packages/** !packages/**
!public/** !public/**

View file

@ -64,6 +64,8 @@ COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr
COPY ./package.json ./bun.lock ./ COPY ./package.json ./bun.lock ./
COPY ./packages/core/package.json ./packages/core/package.json COPY ./packages/core/package.json ./packages/core/package.json
COPY ./packages/contracts/package.json ./packages/contracts/package.json
COPY ./apps/agent/package.json ./apps/agent/package.json
RUN bun install --frozen-lockfile --ignore-scripts RUN bun install --frozen-lockfile --ignore-scripts
@ -86,11 +88,14 @@ WORKDIR /app
COPY ./package.json ./bun.lock ./ COPY ./package.json ./bun.lock ./
COPY ./packages/core/package.json ./packages/core/package.json COPY ./packages/core/package.json ./packages/core/package.json
COPY ./packages/contracts/package.json ./packages/contracts/package.json
COPY ./apps/agent/package.json ./apps/agent/package.json
RUN bun install --frozen-lockfile RUN bun install --frozen-lockfile
COPY . . COPY . .
RUN bun run build RUN bun run build
RUN bun build apps/agent/src/index.ts --outfile .output/agent/index.mjs --target bun
FROM base AS production FROM base AS production

View file

@ -0,0 +1,58 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { Scheduler } from "../../../core/scheduler";
import * as backendModule from "../../backends/backend";
import type { VolumeBackend } from "../../backends/backend";
import * as bootstrapModule from "../bootstrap";
import { createTestVolume } from "~/test/helpers/volume";
const loadShutdownModule = async () => {
const moduleUrl = new URL("../shutdown.ts", import.meta.url);
moduleUrl.searchParams.set("test", crypto.randomUUID());
return import(moduleUrl.href);
};
afterEach(() => {
mock.restore();
});
describe("shutdown", () => {
test("stops the agent runtime before unmounting mounted volumes", async () => {
const events: string[] = [];
const stopScheduler = mock(async () => {
events.push("scheduler.stop");
});
const stopApplicationRuntime = mock(async () => {
events.push("agents.stop");
});
const unmountVolume = mock(async () => {
events.push("backend.unmount");
return { status: "unmounted" as const };
});
await createTestVolume({
name: "Shutdown test volume",
config: {
backend: "directory",
path: "/Applications",
},
status: "mounted",
});
spyOn(Scheduler, "stop").mockImplementation(stopScheduler);
spyOn(bootstrapModule, "stopApplicationRuntime").mockImplementation(stopApplicationRuntime);
spyOn(backendModule, "createVolumeBackend").mockImplementation(
() =>
({
mount: async () => ({ status: "mounted" as const }),
unmount: unmountVolume,
checkHealth: async () => ({ status: "mounted" as const }),
}) satisfies VolumeBackend,
);
const { shutdown } = await loadShutdownModule();
await shutdown();
expect(events).toEqual(["scheduler.stop", "agents.stop", "backend.unmount"]);
});
});

View file

@ -1,4 +1,5 @@
import { runDbMigrations } from "../../db/db"; import { runDbMigrations } from "../../db/db";
import { agentManager, spawnLocalAgent, stopAgentRuntime } from "../agents/agents-manager";
import { runMigrations } from "./migrations"; import { runMigrations } from "./migrations";
import { startup } from "./startup"; import { startup } from "./startup";
@ -7,6 +8,8 @@ let bootstrapPromise: Promise<void> | undefined;
const runBootstrap = async () => { const runBootstrap = async () => {
await runDbMigrations(); await runDbMigrations();
await runMigrations(); await runMigrations();
agentManager.start();
await spawnLocalAgent();
await startup(); await startup();
}; };
@ -22,3 +25,11 @@ export const bootstrapApplication = async () => {
throw err; throw err;
} }
}; };
export const stopApplicationRuntime = async () => {
try {
await stopAgentRuntime();
} finally {
bootstrapPromise = undefined;
}
};

View file

@ -2,9 +2,11 @@ import { Scheduler } from "../../core/scheduler";
import { db } from "../../db/db"; import { db } from "../../db/db";
import { logger } from "@zerobyte/core/node"; import { logger } from "@zerobyte/core/node";
import { createVolumeBackend } from "../backends/backend"; import { createVolumeBackend } from "../backends/backend";
import { stopApplicationRuntime } from "./bootstrap";
export const shutdown = async () => { export const shutdown = async () => {
await Scheduler.stop(); await Scheduler.stop();
await stopApplicationRuntime();
const volumes = await db.query.volumesTable.findMany({ const volumes = await db.query.volumesTable.findMany({
where: { status: "mounted" }, where: { status: "mounted" },

View file

@ -1,9 +1,11 @@
import { definePlugin } from "nitro"; import { definePlugin } from "nitro";
import { bootstrapApplication } from "../modules/lifecycle/bootstrap"; import { bootstrapApplication, stopApplicationRuntime } from "../modules/lifecycle/bootstrap";
import { logger } from "@zerobyte/core/node"; import { logger } from "@zerobyte/core/node";
import { toMessage } from "../utils/errors"; import { toMessage } from "../utils/errors";
export default definePlugin(async () => { export default definePlugin(async (nitroApp) => {
nitroApp.hooks.hook("close", stopApplicationRuntime);
await bootstrapApplication().catch((err) => { await bootstrapApplication().catch((err) => {
logger.error(`Bootstrap failed: ${toMessage(err)}`); logger.error(`Bootstrap failed: ${toMessage(err)}`);
process.exit(1); process.exit(1);