From 47ff0804a5cd53b375d179d2ab04c660880937c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= <57680069+zeval@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:25:10 +0100 Subject: [PATCH] feat(cli): make --port the user-facing port flag (#737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- apps/cli/src/args.test.ts | 142 +++++++++++++++++++++++++++++++++++++ apps/cli/src/args.ts | 134 +++++++++++++++++++++++++++++++++++ apps/cli/src/cli.ts | 144 +++++++++++++------------------------- apps/cli/src/dev.ts | 4 +- apps/cli/src/run.ts | 2 +- apps/cli/src/shared.ts | 7 +- apps/cli/src/start.ts | 2 +- 7 files changed, 335 insertions(+), 100 deletions(-) create mode 100644 apps/cli/src/args.test.ts create mode 100644 apps/cli/src/args.ts diff --git a/apps/cli/src/args.test.ts b/apps/cli/src/args.test.ts new file mode 100644 index 0000000000..1e83ab7ede --- /dev/null +++ b/apps/cli/src/args.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from "vitest"; + +import { parseArgs, ParseError, resolvePorts } from "./args"; + +describe("parseArgs", () => { + it("defaults to the run command with no args", () => { + const { options, showHelp, deprecatedFlags } = parseArgs([]); + expect(options.command).toBe("run"); + expect(showHelp).toBe(false); + expect(deprecatedFlags).toEqual([]); + }); + + it("parses --port and --port= as the backend port", () => { + expect(parseArgs(["--port", "3000"]).options.backendPort).toBe(3000); + expect(parseArgs(["--port=3000"]).options.backendPort).toBe(3000); + }); + + it("parses --backend-port the same as --port (no deprecation note)", () => { + expect(parseArgs(["--backend-port=3447"]).options.backendPort).toBe(3447); + expect(parseArgs(["--backend-port=3447"]).deprecatedFlags).toEqual([]); + }); + + it("parses --web-internal-port without a deprecation note", () => { + const r = parseArgs(["--web-internal-port", "12345"]); + expect(r.options.webPort).toBe(12345); + expect(r.deprecatedFlags).toEqual([]); + }); + + it("records --web-port as deprecated", () => { + const r = parseArgs(["--web-port=8080"]); + expect(r.options.webPort).toBe(8080); + expect(r.deprecatedFlags).toEqual(["--web-port"]); + }); + + it("dedupes deprecated flags across repeats", () => { + const r = parseArgs(["--web-port=1", "--web-port=2"]); + expect(r.deprecatedFlags).toEqual(["--web-port"]); + }); + + it("reports --help via showHelp without exiting", () => { + expect(parseArgs(["--help"]).showHelp).toBe(true); + }); + + it("throws ParseError when a value-taking flag has no value", () => { + expect(() => parseArgs(["--port"])).toThrow(ParseError); + expect(() => parseArgs(["--port"])).toThrow(/--port requires a value/); + }); + + it("throws ParseError when the next token is another flag", () => { + expect(() => parseArgs(["--port", "--debug"])).toThrow(/--port requires a value/); + }); + + it("throws ParseError on a non-numeric port value", () => { + expect(() => parseArgs(["--port=abc"])).toThrow(/--port value must be an integer/); + }); + + it("throws ParseError on a non-integer port value", () => { + expect(() => parseArgs(["--port=3000.5"])).toThrow(/--port value must be an integer/); + }); + + it.each(["0", "-1", "65536"])("throws ParseError on out-of-range port %s", (val) => { + expect(() => parseArgs([`--port=${val}`])).toThrow(/--port value must be an integer between/); + }); + + it("throws ParseError on empty --port=", () => { + expect(() => parseArgs(["--port="])).toThrow(/--port value must be an integer/); + }); + + it("throws ParseError on empty --version=", () => { + expect(() => parseArgs(["--version="])).toThrow(/--version requires a value/); + }); +}); + +describe("resolvePorts", () => { + it("returns undefined for both ports when nothing is set", () => { + const r = resolvePorts({ command: "run" }, {} as NodeJS.ProcessEnv); + expect(r).toEqual({ backendPort: undefined, webPort: undefined }); + }); + + it("CLI backendPort wins over env vars", () => { + const r = resolvePorts({ command: "start", backendPort: 3000 }, { + KANDEV_BACKEND_PORT: "4000", + KANDEV_PORT: "5000", + } as NodeJS.ProcessEnv); + expect(r.backendPort).toBe(3000); + }); + + it("KANDEV_BACKEND_PORT wins over KANDEV_PORT (more specific env wins)", () => { + const r = resolvePorts({ command: "run" }, { + KANDEV_PORT: "5555", + KANDEV_BACKEND_PORT: "6666", + } as NodeJS.ProcessEnv); + expect(r.backendPort).toBe(6666); + }); + + it("falls back to KANDEV_PORT when KANDEV_BACKEND_PORT is not set", () => { + const r = resolvePorts({ command: "run" }, { KANDEV_PORT: "5555" } as NodeJS.ProcessEnv); + expect(r.backendPort).toBe(5555); + }); + + it("KANDEV_WEB_PORT sets the internal web port in dev", () => { + const r = resolvePorts({ command: "dev" }, { KANDEV_WEB_PORT: "8080" } as NodeJS.ProcessEnv); + expect(r.webPort).toBe(8080); + }); + + it("KANDEV_WEB_PORT sets the internal web port in run/start (backend stays undefined)", () => { + const r = resolvePorts({ command: "run" }, { KANDEV_WEB_PORT: "8080" } as NodeJS.ProcessEnv); + expect(r).toEqual({ backendPort: undefined, webPort: 8080 }); + }); + + it("--port maps to backend in every command (including dev)", () => { + const r = resolvePorts({ command: "dev", backendPort: 3447 }, {} as NodeJS.ProcessEnv); + expect(r).toEqual({ backendPort: 3447, webPort: undefined }); + }); + + it("throws ParseError when KANDEV_PORT is not a number", () => { + expect(() => + resolvePorts({ command: "run" }, { KANDEV_PORT: "abc" } as NodeJS.ProcessEnv), + ).toThrow(ParseError); + }); + + it("throws ParseError when KANDEV_PORT is a float", () => { + expect(() => + resolvePorts({ command: "run" }, { KANDEV_PORT: "3000.5" } as NodeJS.ProcessEnv), + ).toThrow(/KANDEV_PORT must be an integer/); + }); + + it.each(["0", "-1", "65536"])("throws ParseError when KANDEV_PORT is out-of-range %s", (val) => { + expect(() => + resolvePorts({ command: "run" }, { KANDEV_PORT: val } as NodeJS.ProcessEnv), + ).toThrow(/KANDEV_PORT must be an integer between/); + }); + + it.each(["KANDEV_PORT", "KANDEV_BACKEND_PORT", "KANDEV_WEB_PORT"])( + "throws ParseError when %s is set to empty string", + (name) => { + expect(() => resolvePorts({ command: "run" }, { [name]: "" } as NodeJS.ProcessEnv)).toThrow( + new RegExp(`${name} must be an integer`), + ); + }, + ); +}); diff --git a/apps/cli/src/args.ts b/apps/cli/src/args.ts new file mode 100644 index 0000000000..147a0cca45 --- /dev/null +++ b/apps/cli/src/args.ts @@ -0,0 +1,134 @@ +export type Command = "run" | "dev" | "start"; + +export type CliOptions = { + command: Command; + version?: string; + backendPort?: number; + webPort?: number; + verbose?: boolean; + debug?: boolean; +}; + +export type ParseResult = { + options: CliOptions; + showHelp: boolean; + /** Deprecated flags seen on the command line. cli.ts emits warnings after parsing. */ + deprecatedFlags: string[]; +}; + +export class ParseError extends Error {} + +export function parseArgs(argv: string[]): ParseResult { + const opts: CliOptions = { command: "run" }; + let showHelp = false; + const deprecatedFlags: string[] = []; + const noteDeprecated = (flag: string) => { + if (!deprecatedFlags.includes(flag)) deprecatedFlags.push(flag); + }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "--help" || arg === "-h") { + showHelp = true; + continue; + } + if (arg === "dev" || arg === "run" || arg === "start") { + opts.command = arg; + continue; + } + if (arg === "--version") { + opts.version = takeValue(argv, i, "--version"); + i += 1; + continue; + } + if (arg.startsWith("--version=")) { + const value = arg.slice("--version=".length); + if (value.length === 0) throw new ParseError("--version requires a value"); + opts.version = value; + continue; + } + if (arg === "--dev") { + opts.command = "dev"; + continue; + } + // --port is an alias for --backend-port (the user-facing port in run/start). + if (arg === "--port" || arg === "--backend-port") { + opts.backendPort = parsePort(takeValue(argv, i, arg), arg); + i += 1; + continue; + } + if (arg.startsWith("--port=") || arg.startsWith("--backend-port=")) { + const flag = arg.startsWith("--port=") ? "--port" : "--backend-port"; + opts.backendPort = parsePort(arg.slice(flag.length + 1), flag); + continue; + } + if (arg === "--web-internal-port") { + opts.webPort = parsePort(takeValue(argv, i, "--web-internal-port"), "--web-internal-port"); + i += 1; + continue; + } + if (arg.startsWith("--web-internal-port=")) { + opts.webPort = parsePort(arg.slice("--web-internal-port=".length), "--web-internal-port"); + continue; + } + if (arg === "--web-port") { + opts.webPort = parsePort(takeValue(argv, i, "--web-port"), "--web-port"); + noteDeprecated("--web-port"); + i += 1; + continue; + } + if (arg.startsWith("--web-port=")) { + opts.webPort = parsePort(arg.slice("--web-port=".length), "--web-port"); + noteDeprecated("--web-port"); + continue; + } + if (arg === "--verbose" || arg === "-v") { + opts.verbose = true; + continue; + } + if (arg === "--debug") { + opts.debug = true; + continue; + } + } + return { options: opts, showHelp, deprecatedFlags }; +} + +function takeValue(argv: string[], i: number, flag: string): string { + const v = argv[i + 1]; + if (v === undefined || v.startsWith("-")) { + throw new ParseError(`${flag} requires a value`); + } + return v; +} + +function parsePort(raw: string, flag: string): number { + const n = Number(raw); + if (raw === "" || !Number.isInteger(n) || n < 1 || n > 65535) { + throw new ParseError(`${flag} value must be an integer between 1 and 65535, got "${raw}"`); + } + return n; +} + +export type ResolvedPorts = { + backendPort?: number; + webPort?: number; +}; + +// CLI flags beat env vars; KANDEV_PORT is an alias for KANDEV_BACKEND_PORT. +export function resolvePorts(options: CliOptions, env: NodeJS.ProcessEnv): ResolvedPorts { + return { + backendPort: + options.backendPort ?? envPort(env, "KANDEV_BACKEND_PORT") ?? envPort(env, "KANDEV_PORT"), + webPort: options.webPort ?? envPort(env, "KANDEV_WEB_PORT"), + }; +} + +function envPort(env: NodeJS.ProcessEnv, name: string): number | undefined { + const val = env[name]; + if (val === undefined) return undefined; + const n = Number(val); + if (val === "" || !Number.isInteger(n) || n < 1 || n > 65535) { + throw new ParseError(`${name} must be an integer between 1 and 65535, got "${val}"`); + } + return n; +} diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts index 0fe1bebe60..b1d02c7dc5 100644 --- a/apps/cli/src/cli.ts +++ b/apps/cli/src/cli.ts @@ -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 ] [--backend-port ] [--web-port ] [--verbose] [--debug] - kandev dev [--backend-port ] [--web-port ] - kandev start [--backend-port ] [--web-port ] [--verbose] [--debug] - kandev [--version ] [--backend-port ] [--web-port ] [--verbose] [--debug] - kandev --dev [--backend-port ] [--web-port ] + kandev run [--version ] [--port ] [--verbose] [--debug] + kandev dev [--port ] + kandev start [--port ] [--verbose] [--debug] + kandev [--version ] [--port ] [--verbose] [--debug] + kandev --dev [--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 { - 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 { 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); }); diff --git a/apps/cli/src/dev.ts b/apps/cli/src/dev.ts index 0c39039e5b..8688145149 100644 --- a/apps/cli/src/dev.ts +++ b/apps/cli/src/dev.ts @@ -27,6 +27,7 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr const webEnv = buildWebEnv({ ports, debug: true }); const logLevel = process.env.KANDEV_LOGGING_LEVEL?.trim() || process.env.KANDEV_LOG_LEVEL?.trim() || "info"; + const webUrl = `http://localhost:${ports.webPort}`; logStartupInfo({ header: "dev mode: using local repo", @@ -52,7 +53,6 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr await waitForHealth(ports.backendUrl, backendProc, healthTimeoutMs); console.log(`[kandev] backend ready at ${ports.backendUrl}`); - const webUrl = `http://localhost:${ports.webPort}`; console.log("[kandev] starting web..."); const webProc = launchWebApp({ command: "pnpm", @@ -63,7 +63,7 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr label: "web", }); await waitForUrlReady(webUrl, webProc, healthTimeoutMs); - console.log(`[kandev] web ready at ${webUrl}`); + console.log(`[kandev] open: ${webUrl}`); openBrowser(webUrl); } diff --git a/apps/cli/src/run.ts b/apps/cli/src/run.ts index d6283c1bcc..82693ab875 100644 --- a/apps/cli/src/run.ts +++ b/apps/cli/src/run.ts @@ -318,6 +318,6 @@ export async function runRelease({ quiet: !prepared.showOutput, }); await waitForUrlReady(webUrl, webProc, healthTimeoutMs); - console.log("[kandev] ready at " + prepared.backendUrl); + console.log("[kandev] open: " + prepared.backendUrl); openBrowser(prepared.backendUrl); } diff --git a/apps/cli/src/shared.ts b/apps/cli/src/shared.ts index f1325a70f5..71ac99f0a7 100644 --- a/apps/cli/src/shared.ts +++ b/apps/cli/src/shared.ts @@ -120,10 +120,13 @@ export type StartupInfoOptions = { */ export function logStartupInfo(options: StartupInfoOptions): void { const { header, ports, dbPath, logLevel } = options; + const backendUrl = ports.backendUrl; + const webUrl = `http://localhost:${ports.webPort}`; console.log(`[kandev] ${header}`); - console.log("[kandev] url: http://localhost:" + ports.backendPort); + console.log("[kandev] backend:", backendUrl); + console.log("[kandev] web:", webUrl); console.log("[kandev] agentctl port:", ports.agentctlPort); - console.log("[kandev] mcp url:", `http://localhost:${ports.backendPort}/mcp`); + console.log("[kandev] mcp url:", `${backendUrl}/mcp`); if (dbPath) { console.log("[kandev] db path:", dbPath); } diff --git a/apps/cli/src/start.ts b/apps/cli/src/start.ts index 47b37af4b7..6ba00df316 100644 --- a/apps/cli/src/start.ts +++ b/apps/cli/src/start.ts @@ -207,6 +207,6 @@ export async function runStart({ }); await waitForUrlReady(webUrl, webProc, healthTimeoutMs); - console.log("[kandev] ready at " + ports.backendUrl); + console.log("[kandev] open: " + ports.backendUrl); openBrowser(ports.backendUrl); }