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). 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"])) { diff --git a/tests/unit/launcher-utils.test.ts b/tests/unit/launcher-utils.test.ts index b368af18..93c45067 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,22 @@ describe("assessProvenance", () => { expect(result.message).toContain(ARTIFACT); }); }); + +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); + }); +});