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

159 lines
5.0 KiB
TypeScript

import path from "node:path";
import fs from "node:fs";
import pkg from "../package.json";
import { parseArgs, ParseError, resolvePorts } from "./args";
import { runDev } from "./dev";
import { runRelease } from "./run";
import { runServiceCommand } from "./service";
import { warnIfStaleServiceUnit } from "./service/stale_check";
import { runStart } from "./start";
import { ensureValidPort } from "./ports";
function printHelp() {
console.log(`kandev launcher
Usage:
kandev run [--port <port>] [--verbose] [--debug]
kandev dev [--port <port>]
kandev start [--port <port>] [--verbose] [--debug]
kandev [--port <port>] [--verbose] [--debug]
kandev --dev [--port <port>]
kandev service install|uninstall|start|stop|restart|status|logs [--system]
Examples:
kandev
kandev run
kandev --dev
kandev dev
kandev start
kandev --version
kandev --port 3000
kandev --debug
Options:
dev Use local repo for dev (Go backend + Vite dev server).
start Use local production build (Go backend serves Vite dist).
run Use installed runtime bundle (default).
service Manage kandev as an OS service (systemd / launchd).
Run 'kandev service --help' for details.
--dev Alias for "dev".
--version, -V Print CLI version and exit.
--port Port for the Go backend (the URL kandev opens on in
start/run). Alias for --backend-port. Also reads
KANDEV_PORT or KANDEV_BACKEND_PORT.
--verbose, -v Show info logs from backend + web.
--debug Show debug logs + agent message dumps.
--headless Skip opening the browser. Used by service units.
--help, -h Show help.
Advanced:
--backend-port Same as --port.
--web-internal-port Override the internal dev web port. The Go backend
reverse-proxies to it in dev; start/run serve from Go.
Also reads KANDEV_WEB_PORT.
--runtime-version <tag> Download and use a specific release tag instead of
the installed runtime. For debugging only.
Example: kandev --runtime-version v0.16.0
`);
}
function findRepoRoot(startDir: string): string | null {
let current = path.resolve(startDir);
while (true) {
if (current.endsWith(`${path.sep}apps`)) {
const backendInApps = path.join(current, "backend");
const webInApps = path.join(current, "web");
if (fs.existsSync(backendInApps) && fs.existsSync(webInApps)) {
return path.dirname(current);
}
}
const backendDir = path.join(current, "apps", "backend");
const webDir = path.join(current, "apps", "web");
if (fs.existsSync(backendDir) && fs.existsSync(webDir)) {
return current;
}
const parent = path.dirname(current);
if (parent === current) {
return null;
}
current = parent;
}
}
async function main(): Promise<void> {
// The `service` subcommand has its own argv parser (own subcommands, flags).
// Handle it before the top-level flag parser to keep the two parsers isolated.
if (process.argv[2] === "service") {
await runServiceCommand(process.argv.slice(3));
return;
}
const { options, showHelp } = parseArgs(process.argv.slice(2));
if (options.showVersion) {
console.log(pkg.version);
return;
}
if (showHelp) {
printHelp();
return;
}
const resolved = resolvePorts(options, process.env);
const backendPort = ensureValidPort(resolved.backendPort, "backend port");
const webPort =
options.command === "dev" ? ensureValidPort(resolved.webPort, "web port") : undefined;
// After an npm/brew upgrade, paths baked into an installed service unit may
// be stale. Warn once before launch so the user knows to re-run install.
// Skip when invoked from a service unit (--headless) — the warning would
// land in journalctl/launchd logs and pile up on every restart.
if (!options.headless) {
warnIfStaleServiceUnit();
}
if (options.command === "dev") {
const repoRoot = findRepoRoot(process.cwd());
if (!repoRoot) {
throw new Error("Unable to locate repo root for dev. Run from the repo.");
}
await runDev({ repoRoot, backendPort, webPort });
return;
}
if (options.command === "start") {
const repoRoot = findRepoRoot(process.cwd());
if (!repoRoot) {
throw new Error("Unable to locate repo root for start. Run from the repo.");
}
await runStart({
repoRoot,
backendPort,
verbose: options.verbose,
debug: options.debug,
headless: options.headless,
});
return;
}
await runRelease({
runtimeVersion: options.runtimeVersion,
backendPort,
verbose: options.verbose,
debug: options.debug,
headless: options.headless,
});
}
main().catch((err) => {
if (err instanceof ParseError) {
console.error(`[kandev] ${err.message}`);
console.error("[kandev] run --help for usage");
process.exit(2);
}
console.error(`[kandev] ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});