feat(cli): make --port the user-facing port flag (#737)

* feat(cli): add --port flag as the user-facing port knob

Introduces --port (and KANDEV_PORT) as the primary, command-aware port
flag: in start/run it sets the Go backend port (the proxy front door
the browser actually hits), and in dev it sets the Next dev server port.
--backend-port and --web-port still work but emit a one-time stderr
deprecation note pointing at --port and --web-internal-port respectively.

* feat(cli): print labelled "open:" URL on startup

Replaces the ambiguous "[kandev] url:" / "ready at" lines with a
labelled block that always shows the user-facing URL first, plus the
backend (api) and web (internal) URLs when they differ. Each command
now declares its own publicUrl: backend in start/run (where the Go
server reverse-proxies Next.js), web in dev (browser hits Next direct).

* style(cli): apply prettier formatting to args.test.ts

* refactor(cli): tighten arg parsing per review feedback

- Validate value-taking flags (--port, --backend-port, --web-internal-port,
  --web-port, --version) and reject missing/non-numeric values with a clear
  ParseError instead of leaking NaN into the port validator.
- Drop the test-only _resetWarnedFlagsForTest export and the module-level
  warned-flag set; parseArgs now returns deduped deprecatedFlags and cli.ts
  emits warnings command-aware (e.g. --backend-port → KANDEV_BACKEND_PORT
  in dev, where --port maps to web).
- Stop double-printing the "open:" URL: logStartupInfo now lists labelled
  backend (api) / web (internal) URLs only, leaving the post-readiness
  open: line as the single source of truth.
- Fix help text: --web-internal-port works in dev too.
- Drop multi-line JSDoc blocks per CLAUDE.md style guide.

* fix(cli): validate env-var ports and collapse comment to one-liner

Catches non-numeric KANDEV_PORT / KANDEV_BACKEND_PORT / KANDEV_WEB_PORT in
envPort with a clear ParseError instead of letting NaN reach
ensureValidPort. Drops a multi-line comment per CLAUDE.md style.

* fix(cli): reject non-integer port values

Number.isFinite accepts floats like 3000.5 which then fail downstream
in ensureValidPort with a generic message. Tighten parsePort and
envPort to require integers and add float tests for both flag and
env-var paths.

* fix(cli): reject out-of-range port values with flag-named errors

Adds 1-65535 range checks to parsePort and envPort so values like 0,
-1, or 65536 fail with a flag/env-var-named ParseError instead of
reaching ensureValidPort with a generic 'backend port' label.

* test(cli): cover KANDEV_WEB_PORT happy path and range validation

* test(cli): cover KANDEV_WEB_PORT in run/start mode too

* fix(cli): explicit CLI flag wins over env var for port resolution

Previously KANDEV_BACKEND_PORT silently overrode --port because each
process port resolved its own env var before the generic --port
fallback. New precedence per process: explicit CLI flag > generic
--port flag > explicit env var > generic KANDEV_PORT. Also relabel
the startup info from "backend (api)" / "web (internal)" to plain
"backend" / "web" — "internal" was misleading in dev where the web
URL is what users open.

* style(cli): collapse resolvePorts comment to one line

* fix(cli): reject empty --version= and empty port env vars

- --version= (no value after =) now throws ParseError instead of
  silently storing the empty string as the version tag.
- envPort distinguishes unset (undefined) from empty (""); an empty
  KANDEV_PORT / KANDEV_BACKEND_PORT / KANDEV_WEB_PORT is now treated
  as misconfiguration and fails fast.

* refactor(cli): make --port a plain alias for --backend-port

Drops the command-aware split where --port mapped to web in dev and
backend in run/start. --port and --backend-port now both set
options.backendPort regardless of command, and KANDEV_PORT is a plain
fallback for KANDEV_BACKEND_PORT. The whole point of the original
fix was that the *backend port* is the URL users actually open in
run/start (because the Go server reverse-proxies Next.js); a single
flag that always means "the backend port" matches that mental model
without per-command surprises.

Removes deprecationReplacement and the command-aware warning logic
since the two flags are now interchangeable. --web-port → --web-internal-port
deprecation stays.

* test(cli): re-add KANDEV_WEB_PORT run-mode coverage dropped in refactor
This commit is contained in:
José Almeida
2026-04-29 22:25:10 +01:00
committed by GitHub
parent 3d93d9e93c
commit 47ff0804a5
7 changed files with 335 additions and 100 deletions
+50 -94
View File
@@ -2,32 +2,22 @@ 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 { runStart } from "./start";
import { ensureValidPort } from "./ports";
import { maybePromptForUpdate } from "./update";
type Command = "run" | "dev" | "start";
type CliOptions = {
command: Command;
version?: string;
backendPort?: number;
webPort?: number;
verbose?: boolean;
debug?: boolean;
};
function printHelp() {
console.log(`kandev launcher
Usage:
kandev run [--version <tag>] [--backend-port <port>] [--web-port <port>] [--verbose] [--debug]
kandev dev [--backend-port <port>] [--web-port <port>]
kandev start [--backend-port <port>] [--web-port <port>] [--verbose] [--debug]
kandev [--version <tag>] [--backend-port <port>] [--web-port <port>] [--verbose] [--debug]
kandev --dev [--backend-port <port>] [--web-port <port>]
kandev run [--version <tag>] [--port <port>] [--verbose] [--debug]
kandev dev [--port <port>]
kandev start [--port <port>] [--verbose] [--debug]
kandev [--version <tag>] [--port <port>] [--verbose] [--debug]
kandev --dev [--port <port>]
Examples:
kandev
@@ -36,83 +26,31 @@ Examples:
kandev dev
kandev start
kandev --version v0.1.0
kandev --backend-port 18080 --web-port 13000
kandev --port 3000
kandev --debug
Options:
dev Use local repo for dev (make dev + next dev) if available.
start Use local production build (make build + next start).
run Use release bundles (default).
dev Use local repo for dev (make dev + next dev) if available.
start Use local production build (make build + next start).
run Use release bundles (default).
--dev Alias for "dev".
--version Release tag to install (default: latest).
--backend-port Override backend port (or KANDEV_BACKEND_PORT env var).
--web-port Override web port (or KANDEV_WEB_PORT env var).
--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.
--help, -h Show help.
Advanced:
--backend-port Same as --port.
--web-internal-port Override the internal Next.js port. The Go backend
reverse-proxies to it; users hit the backend port.
Also reads KANDEV_WEB_PORT.
--web-port Deprecated alias for --web-internal-port.
`);
}
function parseArgs(argv: string[]): CliOptions {
const opts: CliOptions = { command: "run" };
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--help" || arg === "-h") {
printHelp();
process.exit(0);
}
if (arg === "dev" || arg === "run" || arg === "start") {
opts.command = arg;
continue;
}
if (arg === "--version") {
opts.version = argv[i + 1];
i += 1;
continue;
}
if (arg.startsWith("--version=")) {
opts.version = arg.split("=")[1];
continue;
}
if (arg === "--dev") {
opts.command = "dev";
continue;
}
if (arg === "--backend-port") {
opts.backendPort = Number(argv[i + 1]);
i += 1;
continue;
}
if (arg.startsWith("--backend-port=")) {
opts.backendPort = Number(arg.split("=")[1]);
continue;
}
if (arg === "--web-port") {
opts.webPort = Number(argv[i + 1]);
i += 1;
continue;
}
if (arg.startsWith("--web-port=")) {
opts.webPort = Number(arg.split("=")[1]);
continue;
}
if (arg === "--verbose" || arg === "-v") {
opts.verbose = true;
continue;
}
if (arg === "--debug") {
opts.debug = true;
continue;
}
}
return opts;
}
function envPort(name: string): number | undefined {
const val = process.env[name];
return val ? Number(val) : undefined;
}
function findRepoRoot(startDir: string): string | null {
let current = path.resolve(startDir);
while (true) {
@@ -137,14 +75,21 @@ function findRepoRoot(startDir: string): string | null {
}
async function main(): Promise<void> {
const raw = parseArgs(process.argv.slice(2));
const backendPort = ensureValidPort(
raw.backendPort ?? envPort("KANDEV_BACKEND_PORT"),
"backend port",
);
const webPort = ensureValidPort(raw.webPort ?? envPort("KANDEV_WEB_PORT"), "web port");
const { options, showHelp, deprecatedFlags } = parseArgs(process.argv.slice(2));
if (showHelp) {
printHelp();
return;
}
if (raw.command === "dev") {
for (const flag of deprecatedFlags) {
process.stderr.write(`[kandev] ${flag} is deprecated; use --web-internal-port\n`);
}
const resolved = resolvePorts(options, process.env);
const backendPort = ensureValidPort(resolved.backendPort, "backend port");
const webPort = ensureValidPort(resolved.webPort, "web port");
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.");
@@ -153,26 +98,37 @@ async function main(): Promise<void> {
return;
}
if (raw.command === "start") {
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, webPort, verbose: raw.verbose, debug: raw.debug });
await runStart({
repoRoot,
backendPort,
webPort,
verbose: options.verbose,
debug: options.debug,
});
return;
}
await maybePromptForUpdate(pkg.version, process.argv.slice(2));
await runRelease({
version: raw.version,
version: options.version,
backendPort,
webPort,
verbose: raw.verbose,
debug: raw.debug,
verbose: options.verbose,
debug: options.debug,
});
}
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);
});