Files
kandev/apps/cli/src/start.ts
T

174 lines
5.6 KiB
TypeScript

/**
* Production start command for running local builds.
*
* This module implements the `kandev start` command, which runs the locally
* built backend binary and web app in production mode. Unlike `kandev dev`
* which uses hot-reloading, this runs the optimized production builds.
*
* Prerequisites:
* - Backend must be built: `make build-backend`
* - Web app must be built: `make build-web`
* - Or simply: `make build` (builds both)
*/
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { DATA_DIR, HEALTH_TIMEOUT_MS_RELEASE } from "./constants";
import { resolveHealthTimeoutMs, waitForHealth, waitForUrlReady } from "./health";
import { getBinaryName } from "./platform";
import { createProcessSupervisor } from "./process";
import {
attachBackendExitHandler,
buildBackendEnv,
buildWebEnv,
logStartupInfo,
pickPorts,
} from "./shared";
import { launchWebApp, openBrowser } from "./web";
export type StartOptions = {
/** Path to the repository root directory */
repoRoot: string;
/** Optional preferred backend port (finds available port if not specified) */
backendPort?: number;
/** Optional preferred web port (finds available port if not specified) */
webPort?: number;
/** Show info logs from backend + web */
verbose?: boolean;
/** Show debug logs + agent message dumps */
debug?: boolean;
};
/**
* Runs the application in production mode using local builds.
*
* This function:
* 1. Validates that build artifacts exist
* 2. Picks available ports for all services
* 3. Starts the backend binary (with warn log level for clean output)
* 4. Starts the web app via `pnpm start`
* 5. Waits for the backend to be healthy before announcing readiness
*
* @param options - Configuration for the start command
* @throws Error if backend binary or web build is not found
*/
export async function runStart({
repoRoot,
backendPort,
webPort,
verbose = false,
debug = false,
}: StartOptions): Promise<void> {
const ports = await pickPorts(backendPort, webPort);
const backendBin = path.join(repoRoot, "apps", "backend", "bin", getBinaryName("kandev"));
if (!fs.existsSync(backendBin)) {
throw new Error("Backend binary not found. Run `make build` first.");
}
// Check for standalone build (Next.js standalone output)
const webServerPath = path.join(
repoRoot,
"apps",
"web",
".next",
"standalone",
"web",
"server.js",
);
if (!fs.existsSync(webServerPath)) {
throw new Error("Web standalone build not found. Run `make build` first.");
}
const webStandaloneDir = path.dirname(webServerPath);
const webStaticDir = path.join(repoRoot, "apps", "web", ".next", "static");
const standaloneStaticDir = path.join(webStandaloneDir, ".next", "static");
if (fs.existsSync(webStaticDir) && !fs.existsSync(standaloneStaticDir)) {
fs.mkdirSync(path.dirname(standaloneStaticDir), { recursive: true });
try {
fs.symlinkSync(webStaticDir, standaloneStaticDir, "junction");
} catch (err) {
console.warn(
`[kandev] failed to link Next.js static assets: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
// Link public directory (fonts, images, etc.) into standalone output
const webPublicDir = path.join(repoRoot, "apps", "web", "public");
const standalonePublicDir = path.join(webStandaloneDir, "public");
if (fs.existsSync(webPublicDir) && !fs.existsSync(standalonePublicDir)) {
try {
fs.symlinkSync(webPublicDir, standalonePublicDir, "junction");
} catch (err) {
console.warn(
`[kandev] failed to link public assets: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
// Production mode: use warn log level for clean output unless verbose/debug
const showOutput = verbose || debug;
const dbPath = process.env.KANDEV_DATABASE_PATH || path.join(DATA_DIR, "kandev.db");
const logLevel =
process.env.KANDEV_LOG_LEVEL?.trim() || (debug ? "debug" : verbose ? "info" : "warn");
const backendEnv = buildBackendEnv({
ports,
logLevel,
extra: {
KANDEV_DATABASE_PATH: dbPath,
...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true" } : {}),
},
});
const webEnv = buildWebEnv({ ports, includeMcp: true, production: true, debug });
logStartupInfo({
header: "start mode: using local build",
ports,
dbPath,
includeMcp: true,
logLevel,
});
const supervisor = createProcessSupervisor();
supervisor.attachSignalHandlers();
// Start backend with piped stdio (quiet mode unless verbose/debug)
const backendProc = spawn(backendBin, [], {
cwd: path.dirname(backendBin),
env: backendEnv,
stdio: showOutput ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
});
supervisor.children.push(backendProc);
// Forward stderr only (warnings/errors) when quiet
if (!showOutput) {
backendProc.stderr?.pipe(process.stderr);
}
attachBackendExitHandler(backendProc, supervisor);
const healthTimeoutMs = resolveHealthTimeoutMs(HEALTH_TIMEOUT_MS_RELEASE);
console.log("[kandev] starting backend...");
await waitForHealth(ports.backendUrl, backendProc, healthTimeoutMs);
console.log(`[kandev] backend ready at ${ports.backendUrl}`);
// Use standalone server.js directly (not pnpm start)
const webUrl = `http://localhost:${ports.webPort}`;
console.log("[kandev] starting web...");
const webProc = launchWebApp({
command: "node",
args: [webServerPath],
cwd: webStandaloneDir,
env: webEnv,
supervisor,
label: "web",
quiet: !showOutput,
});
await waitForUrlReady(webUrl, webProc, healthTimeoutMs);
console.log(`[kandev] web ready at ${webUrl}`);
openBrowser(webUrl);
}