From f90f0deb2f36c32542e0d0314f2dc925c8302c9b Mon Sep 17 00:00:00 2001 From: Michael Villari <147255440+mikevillari@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:56:07 -0400 Subject: [PATCH 1/4] fix(cli): format usable startup URLs for wildcard and IPv6 hosts --- bin/lib/launcher-utils.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bin/lib/launcher-utils.mjs b/bin/lib/launcher-utils.mjs index 1e6d6bce..ac14c3ee 100644 --- a/bin/lib/launcher-utils.mjs +++ b/bin/lib/launcher-utils.mjs @@ -10,6 +10,22 @@ import { createHash } from "node:crypto"; import { createReadStream, existsSync, renameSync, rmSync } from "node:fs"; import * as path from "node:path"; +export const DEFAULT_PORT = "3000"; + +/** + * Format a local startup link without changing the server's bind address. + * @param {string | null | undefined} hostname + * @param {string | number | null | undefined} port + * @returns {string} + */ +export function startupUrl(hostname, port) { + let host = hostname || "127.0.0.1"; + if (host === "0.0.0.0") host = "127.0.0.1"; + else if (host === "::" || host === "[::]") host = "[::1]"; + else if (host.includes(":") && !host.startsWith("[")) host = `[${host}]`; + return `http://${host}:${port || DEFAULT_PORT}`; +} + /** * Platform/arch pairs the release workflow builds standalone payloads for * (must mirror the build jobs in .github/workflows/release-artifacts.yml). From aedfd630748605f514fd0eaedfea33868486ded0 Mon Sep 17 00:00:00 2001 From: Michael Villari <147255440+mikevillari@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:56:56 -0400 Subject: [PATCH 2/4] fix(cli): use startup URL helper without changing bind settings --- bin/studio.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/studio.js b/bin/studio.js index a6090e91..3638f3b0 100644 --- a/bin/studio.js +++ b/bin/studio.js @@ -38,6 +38,8 @@ import { assessNodeRuntime, assessProvenance, extractArchive, + DEFAULT_PORT, + startupUrl, LauncherUsageError, parseLauncherArgs, parseSha256Sums, @@ -67,7 +69,7 @@ network only prints a warning; an archive whose provenance is actively rejected stops the launcher (override: LIBREDB_STUDIO_SKIP_PROVENANCE=1). Options: - --port Port to listen on (default: $PORT or 3000) + --port Port to listen on (default: $PORT or ${DEFAULT_PORT}) --host Address to bind (default: $HOSTNAME or 127.0.0.1; use --host 0.0.0.0 to expose on the network) --archive Start from a local standalone archive instead of @@ -316,7 +318,7 @@ function startServer(payloadDir, port, host) { if (!env.WORKFLOW_LOCAL_DATA_DIR) env.WORKFLOW_LOCAL_DATA_DIR = resolveLedgerDir(os.homedir()); // Log-line contract: npx-engine-smoke.yml parses the resolved version from // "Starting LibreDB Studio " - keep the prefix stable. - console.log(`Starting LibreDB Studio ${pkg.version} on http://${env.HOSTNAME}:${env.PORT || "3000"}`); + console.log(`Starting LibreDB Studio ${pkg.version} on ${startupUrl(env.HOSTNAME, env.PORT)}`); const child = spawn(process.execPath, ["server.js"], { cwd: payloadDir, env, stdio: "inherit" }); child.on("error", (error) => fail(`Could not start server.js: ${error.message}`)); for (const signal of /** @type {const} */ (["SIGINT", "SIGTERM"])) { From 8df5f0bd0d81b9e7a8d8f693ee44164c47015b79 Mon Sep 17 00:00:00 2001 From: Michael Villari <147255440+mikevillari@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:00:43 -0400 Subject: [PATCH 3/4] test(cli): cover startup URLs and unchanged bind addresses --- tests/unit/launcher-utils.test.ts | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/unit/launcher-utils.test.ts b/tests/unit/launcher-utils.test.ts index b368af18..b0bdf588 100644 --- a/tests/unit/launcher-utils.test.ts +++ b/tests/unit/launcher-utils.test.ts @@ -8,6 +8,7 @@ import * as os from "os"; import * as path from "path"; import { artifactName, + startupUrl, assertReleaseVersion, assessNodeRuntime, assessProvenance, @@ -566,3 +567,55 @@ describe("assessProvenance", () => { expect(result.message).toContain(ARTIFACT); }); }); + +describe("launcher startup URL", () => { + test.each([ + ["0.0.0.0", "http://127.0.0.1:3000"], + ["::", "http://[::1]:3000"], + ["127.0.0.1", "http://127.0.0.1:3000"], + ])("prints a usable URL for %s without changing the bind address", (host, url) => { + const home = fs.mkdtempSync(path.join(tempDir, "startup-")); + const root = path.resolve(import.meta.dir, "../.."); + const version = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version; + const payload = path.join(home, ".libredb-studio", version, "payload"); + fs.mkdirSync(payload, { recursive: true }); + fs.writeFileSync(path.join(payload, "server.js"), 'console.log("BIND=" + process.env.HOSTNAME);'); + const preload = path.join(home, "home-fixture.mjs"); + fs.writeFileSync( + preload, + 'import os from "node:os"; import { syncBuiltinESMExports } from "node:module"; ' + + `os.homedir = () => ${JSON.stringify(home)}; syncBuiltinESMExports();`, + ); + const node = Bun.which("node"); + expect(node).not.toBeNull(); + const run = Bun.spawnSync([node!, "--import", preload, path.join(root, "bin/studio.js"), "--host", host], { + env: { PATH: process.env.PATH }, + stdout: "pipe", + stderr: "pipe", + }); + expect(run.exitCode).toBe(0); + const output = run.stdout.toString(); + expect(output).toContain(`Starting LibreDB Studio ${version} on ${url}\n`); + expect(output).toContain(`BIND=${host}\n`); + expect(output.match(/^Starting LibreDB Studio ([0-9][0-9.]*) /m)?.[1]).toBe(version); + }); +}); + +describe("startupUrl", () => { + test.each([ + ["0.0.0.0", "4000", "http://127.0.0.1:4000"], + ["::", "4000", "http://[::1]:4000"], + ["[::]", "4000", "http://[::1]:4000"], + ["fe80::1", "4000", "http://[fe80::1]:4000"], + ["[fe80::1]", "4000", "http://[fe80::1]:4000"], + ["127.0.0.1", "4000", "http://127.0.0.1:4000"], + ["example.internal", "4000", "http://example.internal:4000"], + ["", "4000", "http://127.0.0.1:4000"], + [undefined, undefined, "http://127.0.0.1:3000"], + [null, null, "http://127.0.0.1:3000"], + ["127.0.0.1", "", "http://127.0.0.1:3000"], + ["127.0.0.1", 4000, "http://127.0.0.1:4000"], + ])("formats host %s and port %s", (host, port, expected) => { + expect(startupUrl(host, port)).toBe(expected); + }); +}); From 39c99656433e44280254c2842492411a8f3a320c Mon Sep 17 00:00:00 2001 From: Michael Villari <147255440+mikevillari@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:20:14 -0400 Subject: [PATCH 4/4] test(cli): keep startup URL coverage independent of ambient Node --- tests/unit/launcher-utils.test.ts | 33 ------------------------------- 1 file changed, 33 deletions(-) diff --git a/tests/unit/launcher-utils.test.ts b/tests/unit/launcher-utils.test.ts index b0bdf588..93c45067 100644 --- a/tests/unit/launcher-utils.test.ts +++ b/tests/unit/launcher-utils.test.ts @@ -568,39 +568,6 @@ describe("assessProvenance", () => { }); }); -describe("launcher startup URL", () => { - test.each([ - ["0.0.0.0", "http://127.0.0.1:3000"], - ["::", "http://[::1]:3000"], - ["127.0.0.1", "http://127.0.0.1:3000"], - ])("prints a usable URL for %s without changing the bind address", (host, url) => { - const home = fs.mkdtempSync(path.join(tempDir, "startup-")); - const root = path.resolve(import.meta.dir, "../.."); - const version = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version; - const payload = path.join(home, ".libredb-studio", version, "payload"); - fs.mkdirSync(payload, { recursive: true }); - fs.writeFileSync(path.join(payload, "server.js"), 'console.log("BIND=" + process.env.HOSTNAME);'); - const preload = path.join(home, "home-fixture.mjs"); - fs.writeFileSync( - preload, - 'import os from "node:os"; import { syncBuiltinESMExports } from "node:module"; ' + - `os.homedir = () => ${JSON.stringify(home)}; syncBuiltinESMExports();`, - ); - const node = Bun.which("node"); - expect(node).not.toBeNull(); - const run = Bun.spawnSync([node!, "--import", preload, path.join(root, "bin/studio.js"), "--host", host], { - env: { PATH: process.env.PATH }, - stdout: "pipe", - stderr: "pipe", - }); - expect(run.exitCode).toBe(0); - const output = run.stdout.toString(); - expect(output).toContain(`Starting LibreDB Studio ${version} on ${url}\n`); - expect(output).toContain(`BIND=${host}\n`); - expect(output.match(/^Starting LibreDB Studio ([0-9][0-9.]*) /m)?.[1]).toBe(version); - }); -}); - describe("startupUrl", () => { test.each([ ["0.0.0.0", "4000", "http://127.0.0.1:4000"],