2026-02-23 14:40:32 -06:00
|
|
|
import express, { Router, type Request as ExpressRequest } from "express";
|
2026-02-16 13:31:58 -06:00
|
|
|
import path from "node:path";
|
2026-02-18 11:45:43 -06:00
|
|
|
import fs from "node:fs";
|
2026-02-16 13:31:58 -06:00
|
|
|
import { fileURLToPath } from "node:url";
|
2026-03-03 08:45:26 -06:00
|
|
|
import type { Db } from "@paperclipai/db";
|
|
|
|
|
import type { DeploymentExposure, DeploymentMode } from "@paperclipai/shared";
|
2026-02-20 10:31:56 -06:00
|
|
|
import type { StorageService } from "./storage/types.js";
|
2026-02-16 13:31:58 -06:00
|
|
|
import { httpLogger, errorHandler } from "./middleware/index.js";
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
import { actorMiddleware } from "./middleware/auth.js";
|
2026-02-20 15:48:30 -06:00
|
|
|
import { boardMutationGuard } from "./middleware/board-mutation-guard.js";
|
2026-02-23 19:43:52 -06:00
|
|
|
import { privateHostnameGuard, resolvePrivateHostnameAllowSet } from "./middleware/private-hostname-guard.js";
|
2026-02-16 13:31:58 -06:00
|
|
|
import { healthRoutes } from "./routes/health.js";
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
import { companyRoutes } from "./routes/companies.js";
|
2026-02-16 13:31:58 -06:00
|
|
|
import { agentRoutes } from "./routes/agents.js";
|
|
|
|
|
import { projectRoutes } from "./routes/projects.js";
|
|
|
|
|
import { issueRoutes } from "./routes/issues.js";
|
|
|
|
|
import { goalRoutes } from "./routes/goals.js";
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
import { approvalRoutes } from "./routes/approvals.js";
|
2026-02-19 15:43:52 -06:00
|
|
|
import { secretRoutes } from "./routes/secrets.js";
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
import { costRoutes } from "./routes/costs.js";
|
2026-02-16 13:31:58 -06:00
|
|
|
import { activityRoutes } from "./routes/activity.js";
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
import { dashboardRoutes } from "./routes/dashboard.js";
|
Implement agent hiring, approval workflows, config revisions, LLM reflection, and sidebar badges
Agent management: hire endpoint with permission gates and pending_approval status,
config revision tracking with rollback, agent duplicate route, permission CRUD.
Block pending_approval agents from auth, heartbeat, and assignments.
Approvals: revision request/resubmit flow, approval comments CRUD, issue-approval
linking, auto-wake agents on approval decisions with context snapshot.
Costs: per-agent breakdown, period filtering (month/week/day/all), cost by agent
list endpoint.
Adapters: agentConfigurationDoc on all adapters, /llms/agent-configuration.txt
reflection routes. Inject PAPERCLIP_APPROVAL_ID, PAPERCLIP_APPROVAL_STATUS,
PAPERCLIP_LINKED_ISSUE_IDS into adapter environments.
Sidebar badges endpoint for pending approval/inbox counts. Dashboard and company
settings extensions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:02:41 -06:00
|
|
|
import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js";
|
|
|
|
|
import { llmRoutes } from "./routes/llms.js";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import { assetRoutes } from "./routes/assets.js";
|
2026-02-23 14:40:32 -06:00
|
|
|
import { accessRoutes } from "./routes/access.js";
|
2026-03-13 16:22:34 -05:00
|
|
|
import { pluginRoutes } from "./routes/plugins.js";
|
|
|
|
|
import { pluginUiStaticRoutes } from "./routes/plugin-ui-static.js";
|
2026-03-10 16:15:11 -05:00
|
|
|
import { applyUiBranding } from "./ui-branding.js";
|
2026-03-13 16:22:34 -05:00
|
|
|
import { logger } from "./middleware/logger.js";
|
|
|
|
|
import { DEFAULT_LOCAL_PLUGIN_DIR, pluginLoader } from "./services/plugin-loader.js";
|
|
|
|
|
import { createPluginWorkerManager } from "./services/plugin-worker-manager.js";
|
|
|
|
|
import { createPluginJobScheduler } from "./services/plugin-job-scheduler.js";
|
|
|
|
|
import { pluginJobStore } from "./services/plugin-job-store.js";
|
|
|
|
|
import { createPluginToolDispatcher } from "./services/plugin-tool-dispatcher.js";
|
|
|
|
|
import { pluginLifecycleManager } from "./services/plugin-lifecycle.js";
|
|
|
|
|
import { createPluginJobCoordinator } from "./services/plugin-job-coordinator.js";
|
|
|
|
|
import { buildHostServices, flushPluginLogBuffer } from "./services/plugin-host-services.js";
|
|
|
|
|
import { createPluginEventBus } from "./services/plugin-event-bus.js";
|
|
|
|
|
import { createPluginDevWatcher } from "./services/plugin-dev-watcher.js";
|
|
|
|
|
import { createPluginHostServiceCleanup } from "./services/plugin-host-service-cleanup.js";
|
|
|
|
|
import { pluginRegistryService } from "./services/plugin-registry.js";
|
|
|
|
|
import { createHostClientHandlers } from "@paperclipai/plugin-sdk";
|
2026-02-23 14:40:32 -06:00
|
|
|
import type { BetterAuthSessionResult } from "./auth/better-auth.js";
|
2026-02-16 13:31:58 -06:00
|
|
|
|
2026-02-18 11:45:43 -06:00
|
|
|
type UiMode = "none" | "static" | "vite-dev";
|
|
|
|
|
|
2026-02-23 14:40:32 -06:00
|
|
|
export async function createApp(
|
|
|
|
|
db: Db,
|
|
|
|
|
opts: {
|
|
|
|
|
uiMode: UiMode;
|
2026-03-10 10:08:58 -05:00
|
|
|
serverPort: number;
|
2026-02-23 14:40:32 -06:00
|
|
|
storageService: StorageService;
|
|
|
|
|
deploymentMode: DeploymentMode;
|
|
|
|
|
deploymentExposure: DeploymentExposure;
|
2026-02-23 19:43:52 -06:00
|
|
|
allowedHostnames: string[];
|
|
|
|
|
bindHost: string;
|
2026-02-23 14:40:32 -06:00
|
|
|
authReady: boolean;
|
2026-03-02 16:43:59 -06:00
|
|
|
companyDeletionEnabled: boolean;
|
2026-03-13 16:22:34 -05:00
|
|
|
instanceId?: string;
|
|
|
|
|
hostVersion?: string;
|
|
|
|
|
localPluginDir?: string;
|
2026-02-23 14:40:32 -06:00
|
|
|
betterAuthHandler?: express.RequestHandler;
|
|
|
|
|
resolveSession?: (req: ExpressRequest) => Promise<BetterAuthSessionResult | null>;
|
|
|
|
|
},
|
|
|
|
|
) {
|
2026-02-16 13:31:58 -06:00
|
|
|
const app = express();
|
|
|
|
|
|
2026-03-13 16:22:34 -05:00
|
|
|
app.use(express.json({
|
|
|
|
|
verify: (req, _res, buf) => {
|
|
|
|
|
(req as unknown as { rawBody: Buffer }).rawBody = buf;
|
|
|
|
|
},
|
|
|
|
|
}));
|
2026-02-16 13:31:58 -06:00
|
|
|
app.use(httpLogger);
|
2026-02-23 19:43:52 -06:00
|
|
|
const privateHostnameGateEnabled =
|
|
|
|
|
opts.deploymentMode === "authenticated" && opts.deploymentExposure === "private";
|
|
|
|
|
const privateHostnameAllowSet = resolvePrivateHostnameAllowSet({
|
|
|
|
|
allowedHostnames: opts.allowedHostnames,
|
|
|
|
|
bindHost: opts.bindHost,
|
|
|
|
|
});
|
|
|
|
|
app.use(
|
|
|
|
|
privateHostnameGuard({
|
|
|
|
|
enabled: privateHostnameGateEnabled,
|
|
|
|
|
allowedHostnames: opts.allowedHostnames,
|
|
|
|
|
bindHost: opts.bindHost,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-02-23 14:40:32 -06:00
|
|
|
app.use(
|
|
|
|
|
actorMiddleware(db, {
|
|
|
|
|
deploymentMode: opts.deploymentMode,
|
|
|
|
|
resolveSession: opts.resolveSession,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-03-02 17:02:01 -06:00
|
|
|
app.get("/api/auth/get-session", (req, res) => {
|
|
|
|
|
if (req.actor.type !== "board" || !req.actor.userId) {
|
|
|
|
|
res.status(401).json({ error: "Unauthorized" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
res.json({
|
|
|
|
|
session: {
|
|
|
|
|
id: `paperclip:${req.actor.source}:${req.actor.userId}`,
|
|
|
|
|
userId: req.actor.userId,
|
|
|
|
|
},
|
|
|
|
|
user: {
|
|
|
|
|
id: req.actor.userId,
|
|
|
|
|
email: null,
|
|
|
|
|
name: req.actor.source === "local_implicit" ? "Local Board" : null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-02-23 14:40:32 -06:00
|
|
|
if (opts.betterAuthHandler) {
|
|
|
|
|
app.all("/api/auth/*authPath", opts.betterAuthHandler);
|
|
|
|
|
}
|
Implement agent hiring, approval workflows, config revisions, LLM reflection, and sidebar badges
Agent management: hire endpoint with permission gates and pending_approval status,
config revision tracking with rollback, agent duplicate route, permission CRUD.
Block pending_approval agents from auth, heartbeat, and assignments.
Approvals: revision request/resubmit flow, approval comments CRUD, issue-approval
linking, auto-wake agents on approval decisions with context snapshot.
Costs: per-agent breakdown, period filtering (month/week/day/all), cost by agent
list endpoint.
Adapters: agentConfigurationDoc on all adapters, /llms/agent-configuration.txt
reflection routes. Inject PAPERCLIP_APPROVAL_ID, PAPERCLIP_APPROVAL_STATUS,
PAPERCLIP_LINKED_ISSUE_IDS into adapter environments.
Sidebar badges endpoint for pending approval/inbox counts. Dashboard and company
settings extensions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:02:41 -06:00
|
|
|
app.use(llmRoutes(db));
|
2026-02-16 13:31:58 -06:00
|
|
|
|
|
|
|
|
// Mount API routes
|
|
|
|
|
const api = Router();
|
2026-02-20 15:48:30 -06:00
|
|
|
api.use(boardMutationGuard());
|
2026-02-23 14:40:32 -06:00
|
|
|
api.use(
|
|
|
|
|
"/health",
|
|
|
|
|
healthRoutes(db, {
|
|
|
|
|
deploymentMode: opts.deploymentMode,
|
|
|
|
|
deploymentExposure: opts.deploymentExposure,
|
|
|
|
|
authReady: opts.authReady,
|
2026-03-02 16:43:59 -06:00
|
|
|
companyDeletionEnabled: opts.companyDeletionEnabled,
|
2026-02-23 14:40:32 -06:00
|
|
|
}),
|
|
|
|
|
);
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
api.use("/companies", companyRoutes(db));
|
|
|
|
|
api.use(agentRoutes(db));
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
api.use(assetRoutes(db, opts.storageService));
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
api.use(projectRoutes(db));
|
2026-02-20 10:31:56 -06:00
|
|
|
api.use(issueRoutes(db, opts.storageService));
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
api.use(goalRoutes(db));
|
|
|
|
|
api.use(approvalRoutes(db));
|
2026-02-19 15:43:52 -06:00
|
|
|
api.use(secretRoutes(db));
|
Add server routes for companies, approvals, costs, and dashboard
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:27 -06:00
|
|
|
api.use(costRoutes(db));
|
|
|
|
|
api.use(activityRoutes(db));
|
|
|
|
|
api.use(dashboardRoutes(db));
|
Implement agent hiring, approval workflows, config revisions, LLM reflection, and sidebar badges
Agent management: hire endpoint with permission gates and pending_approval status,
config revision tracking with rollback, agent duplicate route, permission CRUD.
Block pending_approval agents from auth, heartbeat, and assignments.
Approvals: revision request/resubmit flow, approval comments CRUD, issue-approval
linking, auto-wake agents on approval decisions with context snapshot.
Costs: per-agent breakdown, period filtering (month/week/day/all), cost by agent
list endpoint.
Adapters: agentConfigurationDoc on all adapters, /llms/agent-configuration.txt
reflection routes. Inject PAPERCLIP_APPROVAL_ID, PAPERCLIP_APPROVAL_STATUS,
PAPERCLIP_LINKED_ISSUE_IDS into adapter environments.
Sidebar badges endpoint for pending approval/inbox counts. Dashboard and company
settings extensions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:02:41 -06:00
|
|
|
api.use(sidebarBadgeRoutes(db));
|
2026-03-13 16:22:34 -05:00
|
|
|
const hostServicesDisposers = new Map<string, () => void>();
|
|
|
|
|
const workerManager = createPluginWorkerManager();
|
|
|
|
|
const pluginRegistry = pluginRegistryService(db);
|
|
|
|
|
const eventBus = createPluginEventBus({
|
|
|
|
|
async isPluginEnabledForCompany(pluginKey, companyId) {
|
|
|
|
|
const plugin = await pluginRegistry.getByKey(pluginKey);
|
|
|
|
|
if (!plugin) return false;
|
|
|
|
|
const availability = await pluginRegistry.getCompanyAvailability(companyId, plugin.id);
|
|
|
|
|
return availability?.available ?? true;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const jobStore = pluginJobStore(db);
|
|
|
|
|
const lifecycle = pluginLifecycleManager(db, { workerManager });
|
|
|
|
|
const scheduler = createPluginJobScheduler({
|
|
|
|
|
db,
|
|
|
|
|
jobStore,
|
|
|
|
|
workerManager,
|
|
|
|
|
});
|
|
|
|
|
const toolDispatcher = createPluginToolDispatcher({
|
|
|
|
|
workerManager,
|
|
|
|
|
lifecycleManager: lifecycle,
|
|
|
|
|
db,
|
|
|
|
|
});
|
|
|
|
|
const jobCoordinator = createPluginJobCoordinator({
|
|
|
|
|
db,
|
|
|
|
|
lifecycle,
|
|
|
|
|
scheduler,
|
|
|
|
|
jobStore,
|
|
|
|
|
});
|
|
|
|
|
const hostServiceCleanup = createPluginHostServiceCleanup(lifecycle, hostServicesDisposers);
|
|
|
|
|
const loader = pluginLoader(
|
|
|
|
|
db,
|
|
|
|
|
{ localPluginDir: opts.localPluginDir ?? DEFAULT_LOCAL_PLUGIN_DIR },
|
|
|
|
|
{
|
|
|
|
|
workerManager,
|
|
|
|
|
eventBus,
|
|
|
|
|
jobScheduler: scheduler,
|
|
|
|
|
jobStore,
|
|
|
|
|
toolDispatcher,
|
|
|
|
|
lifecycleManager: lifecycle,
|
|
|
|
|
instanceInfo: {
|
|
|
|
|
instanceId: opts.instanceId ?? "default",
|
|
|
|
|
hostVersion: opts.hostVersion ?? "0.0.0",
|
|
|
|
|
},
|
|
|
|
|
buildHostHandlers: (pluginId, manifest) => {
|
|
|
|
|
const notifyWorker = (method: string, params: unknown) => {
|
|
|
|
|
const handle = workerManager.getWorker(pluginId);
|
|
|
|
|
if (handle) handle.notify(method, params);
|
|
|
|
|
};
|
|
|
|
|
const services = buildHostServices(db, pluginId, manifest.id, eventBus, notifyWorker);
|
|
|
|
|
hostServicesDisposers.set(pluginId, () => services.dispose());
|
|
|
|
|
return createHostClientHandlers({
|
|
|
|
|
pluginId,
|
|
|
|
|
capabilities: manifest.capabilities,
|
|
|
|
|
services,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
api.use(
|
|
|
|
|
pluginRoutes(
|
|
|
|
|
db,
|
|
|
|
|
loader,
|
|
|
|
|
{ scheduler, jobStore },
|
|
|
|
|
{ workerManager },
|
|
|
|
|
{ toolDispatcher },
|
|
|
|
|
{ workerManager },
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-03-02 16:43:59 -06:00
|
|
|
api.use(
|
|
|
|
|
accessRoutes(db, {
|
|
|
|
|
deploymentMode: opts.deploymentMode,
|
|
|
|
|
deploymentExposure: opts.deploymentExposure,
|
|
|
|
|
bindHost: opts.bindHost,
|
|
|
|
|
allowedHostnames: opts.allowedHostnames,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-02-16 13:31:58 -06:00
|
|
|
app.use("/api", api);
|
2026-03-07 15:15:51 -06:00
|
|
|
app.use("/api", (_req, res) => {
|
|
|
|
|
res.status(404).json({ error: "API route not found" });
|
|
|
|
|
});
|
2026-03-13 16:22:34 -05:00
|
|
|
app.use(pluginUiStaticRoutes(db, {
|
|
|
|
|
localPluginDir: opts.localPluginDir ?? DEFAULT_LOCAL_PLUGIN_DIR,
|
|
|
|
|
}));
|
2026-02-16 13:31:58 -06:00
|
|
|
|
2026-02-18 11:45:43 -06:00
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
if (opts.uiMode === "static") {
|
2026-03-03 15:45:45 -06:00
|
|
|
// Try published location first (server/ui-dist/), then monorepo dev location (../../ui/dist)
|
|
|
|
|
const candidates = [
|
|
|
|
|
path.resolve(__dirname, "../ui-dist"),
|
|
|
|
|
path.resolve(__dirname, "../../ui/dist"),
|
|
|
|
|
];
|
|
|
|
|
const uiDist = candidates.find((p) => fs.existsSync(path.join(p, "index.html")));
|
|
|
|
|
if (uiDist) {
|
2026-03-10 16:15:11 -05:00
|
|
|
const indexHtml = applyUiBranding(fs.readFileSync(path.join(uiDist, "index.html"), "utf-8"));
|
2026-03-03 15:45:45 -06:00
|
|
|
app.use(express.static(uiDist));
|
|
|
|
|
app.get(/.*/, (_req, res) => {
|
2026-03-07 16:20:19 -08:00
|
|
|
res.status(200).set("Content-Type", "text/html").end(indexHtml);
|
2026-03-03 15:45:45 -06:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
console.warn("[paperclip] UI dist not found; running in API-only mode");
|
|
|
|
|
}
|
2026-02-16 13:31:58 -06:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 11:45:43 -06:00
|
|
|
if (opts.uiMode === "vite-dev") {
|
|
|
|
|
const uiRoot = path.resolve(__dirname, "../../ui");
|
2026-03-10 10:08:58 -05:00
|
|
|
const hmrPort = opts.serverPort + 10000;
|
2026-02-18 11:45:43 -06:00
|
|
|
const { createServer: createViteServer } = await import("vite");
|
|
|
|
|
const vite = await createViteServer({
|
|
|
|
|
root: uiRoot,
|
|
|
|
|
appType: "spa",
|
|
|
|
|
server: {
|
|
|
|
|
middlewareMode: true,
|
2026-03-10 10:08:58 -05:00
|
|
|
hmr: {
|
|
|
|
|
host: opts.bindHost,
|
|
|
|
|
port: hmrPort,
|
|
|
|
|
clientPort: hmrPort,
|
|
|
|
|
},
|
2026-02-23 19:43:52 -06:00
|
|
|
allowedHosts: privateHostnameGateEnabled ? Array.from(privateHostnameAllowSet) : undefined,
|
2026-02-18 11:45:43 -06:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.use(vite.middlewares);
|
|
|
|
|
app.get(/.*/, async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const templatePath = path.resolve(uiRoot, "index.html");
|
|
|
|
|
const template = fs.readFileSync(templatePath, "utf-8");
|
2026-03-10 16:15:11 -05:00
|
|
|
const html = applyUiBranding(await vite.transformIndexHtml(req.originalUrl, template));
|
2026-02-18 11:45:43 -06:00
|
|
|
res.status(200).set({ "Content-Type": "text/html" }).end(html);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 13:31:58 -06:00
|
|
|
app.use(errorHandler);
|
|
|
|
|
|
2026-03-13 16:22:34 -05:00
|
|
|
jobCoordinator.start();
|
|
|
|
|
scheduler.start();
|
|
|
|
|
void toolDispatcher.initialize().catch((err) => {
|
|
|
|
|
logger.error({ err }, "Failed to initialize plugin tool dispatcher");
|
|
|
|
|
});
|
|
|
|
|
const devWatcher = createPluginDevWatcher(
|
|
|
|
|
lifecycle,
|
|
|
|
|
async (pluginId) => (await pluginRegistry.getById(pluginId))?.packagePath ?? null,
|
|
|
|
|
);
|
|
|
|
|
void loader.loadAll().then((result) => {
|
|
|
|
|
if (!result) return;
|
|
|
|
|
for (const loaded of result.results) {
|
|
|
|
|
if (loaded.success && loaded.plugin.packagePath) {
|
|
|
|
|
devWatcher.watch(loaded.plugin.id, loaded.plugin.packagePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
logger.error({ err }, "Failed to load ready plugins on startup");
|
|
|
|
|
});
|
|
|
|
|
process.once("exit", () => {
|
|
|
|
|
devWatcher.close();
|
|
|
|
|
hostServiceCleanup.disposeAll();
|
|
|
|
|
hostServiceCleanup.teardown();
|
|
|
|
|
});
|
|
|
|
|
process.once("beforeExit", () => {
|
|
|
|
|
void flushPluginLogBuffer();
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-16 13:31:58 -06:00
|
|
|
return app;
|
|
|
|
|
}
|