Files
kandev/apps/cli/src/start.ts
T
2026-06-20 19:40:15 +01:00

123 lines
4.0 KiB
TypeScript

/**
* Production start command for running local builds.
*
* This module implements the `kandev start` command, which runs the locally
* built backend binary in production mode. Unlike `kandev dev`
* which uses hot-reloading, this runs the optimized production builds.
*
* Prerequisites:
* - Backend must be built: `make build-backend`
* - Or simply: `make build`
*/
import fs from "node:fs";
import path from "node:path";
import {
HEALTH_TIMEOUT_MS_RELEASE,
resolveDataDir,
resolveDatabasePath,
resolveKandevHomeDir,
} from "./constants";
import { resolveHealthTimeoutMs, waitForHealth } from "./health";
import { getBinaryName } from "./platform";
import { createProcessSupervisor } from "./process";
import { buildBackendEnv, logStartupInfo, pickBackendPorts } from "./shared";
import { launchRestartableBackend } from "./supervisor/backend";
import { 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;
/** Show info logs from backend + web */
verbose?: boolean;
/** Show debug logs + agent message dumps */
debug?: boolean;
/** Skip browser open. Set by service units and preview environments. */
headless?: 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. Waits for the backend to be healthy before announcing readiness
*
* @param options - Configuration for the start command
* @throws Error if backend binary is not found
*/
export async function runStart({
repoRoot,
backendPort,
verbose = false,
debug = false,
headless = false,
}: StartOptions): Promise<void> {
const ports = await pickBackendPorts(backendPort);
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.");
}
// Production mode: use warn log level for clean output unless verbose/debug
const showOutput = verbose || debug;
fs.mkdirSync(resolveDataDir(), { recursive: true });
// The data dir holds the SQLite DB; keep it owner-only even if it pre-existed
// with a looser umask-derived mode.
fs.chmodSync(resolveDataDir(), 0o700);
const dbPath = resolveDatabasePath();
const logLevel =
process.env.KANDEV_LOG_LEVEL?.trim() || (debug ? "debug" : verbose ? "info" : "warn");
const backendEnv = buildBackendEnv({
ports,
logLevel,
webProxy: false,
extra: {
KANDEV_DATABASE_PATH: dbPath,
...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true", KANDEV_DEBUG_PPROF_ENABLED: "true" } : {}),
},
});
logStartupInfo({
header: "start mode: using local build",
ports,
dbPath,
logLevel,
});
const supervisor = createProcessSupervisor();
supervisor.attachSignalHandlers();
// Start backend: ignore stdin, show stdout only in verbose/debug mode, always show stderr
// Stderr is always inherited to ensure error messages are visible immediately (no pipe buffering)
const backend = await launchRestartableBackend({
command: backendBin,
args: [],
cwd: path.dirname(backendBin),
env: backendEnv,
homeDir: resolveKandevHomeDir(),
ports,
mode: "start",
stdio: showOutput ? ["ignore", "inherit", "inherit"] : ["ignore", "ignore", "inherit"],
supervisor,
});
const healthTimeoutMs = resolveHealthTimeoutMs(HEALTH_TIMEOUT_MS_RELEASE);
console.log("[kandev] starting backend...");
await waitForHealth(ports.backendUrl, backend.proc, healthTimeoutMs);
console.log(`[kandev] backend ready at ${ports.backendUrl}`);
console.log("[kandev] open: " + ports.backendUrl);
if (headless) {
console.log(`[kandev] ready (headless) at ${ports.backendUrl}`);
return;
}
openBrowser(ports.backendUrl);
}