@devrouter/cli 0.0.43 → 0.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/devrouter.js CHANGED
@@ -2509,7 +2509,7 @@ function loadRepoConfig(repoPath) {
2509
2509
  const config = parseConfig(parsed ?? {}, configPath);
2510
2510
  const requiredVersion = config.devrouter?.version;
2511
2511
  if (requiredVersion && !hasWarnedVersionMismatch) {
2512
- const cliVersion = true ? "0.0.43" : "0.0.0-dev";
2512
+ const cliVersion = true ? "0.0.44" : "0.0.0-dev";
2513
2513
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2514
2514
  hasWarnedVersionMismatch = true;
2515
2515
  process.stderr.write(
@@ -5964,6 +5964,9 @@ function parseCertificateDnsHosts(pem) {
5964
5964
  function isHostCoveredByCertificateHost(host, certificateHost) {
5965
5965
  const normalizedHost = normalizeHost(host);
5966
5966
  const normalizedCertificateHost = normalizeHost(certificateHost);
5967
+ if (normalizedHost === normalizedCertificateHost) {
5968
+ return true;
5969
+ }
5967
5970
  if (normalizedCertificateHost.startsWith("*.")) {
5968
5971
  if (normalizedCertificateHost === "*.localhost") {
5969
5972
  return false;
@@ -5975,7 +5978,7 @@ function isHostCoveredByCertificateHost(host, certificateHost) {
5975
5978
  const wildcardPart = normalizedHost.slice(0, normalizedHost.length - suffix.length);
5976
5979
  return wildcardPart.length > 0 && !wildcardPart.includes(".");
5977
5980
  }
5978
- return normalizedHost === normalizedCertificateHost;
5981
+ return false;
5979
5982
  }
5980
5983
  function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
5981
5984
  const normalizedRequired = normalizeUniqueHosts(requiredHosts);
@@ -5986,22 +5989,54 @@ function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
5986
5989
  )
5987
5990
  );
5988
5991
  }
5989
- function readCurrentCertificateHosts() {
5992
+ function compactTLSCertificateHosts(hosts) {
5993
+ const normalizedHosts = normalizeUniqueHosts(hosts);
5994
+ const wildcardHosts = new Set(normalizedHosts.filter((host) => host.startsWith("*.")));
5995
+ const siblingGroups = /* @__PURE__ */ new Map();
5996
+ const compacted = new Set(wildcardHosts);
5997
+ for (const host of normalizedHosts) {
5998
+ if (host.startsWith("*.")) {
5999
+ continue;
6000
+ }
6001
+ const labels = host.split(".");
6002
+ if (labels.length < 3 || labels.at(-1) !== "localhost") {
6003
+ compacted.add(host);
6004
+ continue;
6005
+ }
6006
+ const suffix = labels.slice(1).join(".");
6007
+ const siblings = siblingGroups.get(suffix) ?? [];
6008
+ siblings.push(host);
6009
+ siblingGroups.set(suffix, siblings);
6010
+ }
6011
+ for (const [suffix, siblings] of siblingGroups) {
6012
+ const wildcard = `*.${suffix}`;
6013
+ if (siblings.length > 1 || wildcardHosts.has(wildcard)) {
6014
+ compacted.add(wildcard);
6015
+ continue;
6016
+ }
6017
+ compacted.add(siblings[0]);
6018
+ }
6019
+ const selectedWildcards = Array.from(compacted).filter((host) => host.startsWith("*."));
6020
+ return normalizeUniqueHosts(Array.from(compacted)).filter(
6021
+ (host) => host.startsWith("*.") || !selectedWildcards.some((wildcard) => isHostCoveredByCertificateHost(host, wildcard))
6022
+ );
6023
+ }
6024
+ function readCurrentCertificateHosts(options = {}) {
5990
6025
  if (!import_node_fs16.default.existsSync(CERT_FILE)) {
5991
6026
  return [];
5992
6027
  }
5993
- const pem = import_node_fs16.default.readFileSync(CERT_FILE, "utf-8");
5994
- return parseCertificateDnsHosts(pem);
5995
- }
5996
- function currentCertificateHostsOrEmpty() {
5997
6028
  try {
5998
- return readCurrentCertificateHosts();
5999
- } catch {
6000
- return [];
6029
+ const pem = import_node_fs16.default.readFileSync(CERT_FILE, "utf-8");
6030
+ return parseCertificateDnsHosts(pem);
6031
+ } catch (error) {
6032
+ if (options.replaceMalformed) {
6033
+ return [];
6034
+ }
6035
+ throw error;
6001
6036
  }
6002
6037
  }
6003
6038
  function buildDesiredTLSCertificateHosts(requestedHosts, existingCertificateHosts) {
6004
- return normalizeUniqueHosts([
6039
+ return compactTLSCertificateHosts([
6005
6040
  ...DEFAULT_TLS_CERT_HOSTS,
6006
6041
  ...existingCertificateHosts,
6007
6042
  ...requestedHosts
@@ -6009,35 +6044,75 @@ function buildDesiredTLSCertificateHosts(requestedHosts, existingCertificateHost
6009
6044
  }
6010
6045
  function getTLSHostCoverage(hosts) {
6011
6046
  const requiredHosts = normalizeUniqueHosts([...DEFAULT_TLS_CERT_HOSTS, ...hosts]);
6012
- const certificateHosts = readCurrentCertificateHosts();
6013
- const uncoveredHosts = findUncoveredCertificateHosts(requiredHosts, certificateHosts);
6014
- return {
6015
- requiredHosts,
6016
- certificateHosts,
6017
- uncoveredHosts
6018
- };
6047
+ return withFileLockSync(
6048
+ TLS_CERTIFICATE_LOCK_FILE,
6049
+ { activity: "TLS certificate inspection", waitMs: TLS_CERTIFICATE_LOCK_WAIT_MS },
6050
+ () => {
6051
+ const certificateHosts = readCurrentCertificateHosts();
6052
+ const uncoveredHosts = findUncoveredCertificateHosts(requiredHosts, certificateHosts);
6053
+ return {
6054
+ requiredHosts,
6055
+ certificateHosts,
6056
+ uncoveredHosts
6057
+ };
6058
+ }
6059
+ );
6019
6060
  }
6020
6061
  async function applyTLSCertificate(options, installTrust) {
6021
6062
  ensureRouterFiles();
6022
- const alreadyEnabled = isTLSEnabled();
6023
- const desiredHosts = buildDesiredTLSCertificateHosts(
6024
- options.hosts ?? [],
6025
- currentCertificateHostsOrEmpty()
6063
+ const result = withFileLockSync(
6064
+ TLS_CERTIFICATE_LOCK_FILE,
6065
+ {
6066
+ activity: "TLS certificate refresh",
6067
+ target: options.repoPath,
6068
+ waitMs: TLS_CERTIFICATE_LOCK_WAIT_MS
6069
+ },
6070
+ () => {
6071
+ const alreadyEnabled = isTLSEnabled();
6072
+ if (installTrust) {
6073
+ ensureMkcert();
6074
+ runOrThrow("mkcert", ["-install"]);
6075
+ } else {
6076
+ getMkcertRootCAPath({ repoPath: options.repoPath });
6077
+ }
6078
+ const existingCertificateHosts = readCurrentCertificateHosts({
6079
+ replaceMalformed: installTrust
6080
+ });
6081
+ const desiredHosts = buildDesiredTLSCertificateHosts(
6082
+ options.hosts ?? [],
6083
+ existingCertificateHosts
6084
+ );
6085
+ let certificateHosts = [];
6086
+ let uncoveredHosts = [];
6087
+ for (let attempt = 1; attempt <= TLS_CERTIFICATE_WRITE_ATTEMPTS; attempt += 1) {
6088
+ runOrThrow("mkcert", [
6089
+ "-cert-file",
6090
+ CERT_FILE,
6091
+ "-key-file",
6092
+ CERT_KEY_FILE,
6093
+ ...desiredHosts
6094
+ ]);
6095
+ certificateHosts = readCurrentCertificateHosts();
6096
+ uncoveredHosts = findUncoveredCertificateHosts(desiredHosts, certificateHosts);
6097
+ if (uncoveredHosts.length === 0) {
6098
+ break;
6099
+ }
6100
+ }
6101
+ if (uncoveredHosts.length > 0) {
6102
+ throw new Error(
6103
+ `mkcert did not produce certificate coverage for host(s): ${uncoveredHosts.join(", ")} after ${TLS_CERTIFICATE_WRITE_ATTEMPTS} attempts`
6104
+ );
6105
+ }
6106
+ setTLSEnabled(true);
6107
+ refreshHostRoutesDynamicFile();
6108
+ return { alreadyEnabled, hosts: certificateHosts };
6109
+ }
6026
6110
  );
6027
- if (installTrust) {
6028
- ensureMkcert();
6029
- runOrThrow("mkcert", ["-install"]);
6030
- } else {
6031
- getMkcertRootCAPath({ repoPath: options.repoPath });
6032
- }
6033
- runOrThrow("mkcert", ["-cert-file", CERT_FILE, "-key-file", CERT_KEY_FILE, ...desiredHosts]);
6034
- setTLSEnabled(true);
6035
- refreshHostRoutesDynamicFile();
6036
6111
  const routerContainer = await findContainerByName("devrouter-traefik");
6037
6112
  if (routerContainer && await isContainerRunning("devrouter-traefik")) {
6038
6113
  startRouterStack();
6039
6114
  }
6040
- return { alreadyEnabled, hosts: desiredHosts };
6115
+ return result;
6041
6116
  }
6042
6117
  async function installTLS(options = {}) {
6043
6118
  return applyTLSCertificate(options, true);
@@ -6079,7 +6154,7 @@ Run: ${tlsSetupCommand(options.repoPath)}`
6079
6154
  );
6080
6155
  }
6081
6156
  }
6082
- var import_node_child_process8, import_node_crypto6, import_node_fs16, import_node_path15, DEFAULT_TLS_CERT_HOSTS;
6157
+ var import_node_child_process8, import_node_crypto6, import_node_fs16, import_node_path15, DEFAULT_TLS_CERT_HOSTS, TLS_CERTIFICATE_LOCK_FILE, TLS_CERTIFICATE_LOCK_WAIT_MS, TLS_CERTIFICATE_WRITE_ATTEMPTS;
6083
6158
  var init_tls = __esm({
6084
6159
  "src/core/tls.ts"() {
6085
6160
  "use strict";
@@ -6088,9 +6163,13 @@ var init_tls = __esm({
6088
6163
  import_node_fs16 = __toESM(require("fs"));
6089
6164
  import_node_path15 = __toESM(require("path"));
6090
6165
  init_docker();
6166
+ init_file_lock();
6091
6167
  init_host_routes();
6092
6168
  init_router();
6093
6169
  DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
6170
+ TLS_CERTIFICATE_LOCK_FILE = import_node_path15.default.join(DEVROUTER_HOME, "tls-certificate.lock");
6171
+ TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
6172
+ TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
6094
6173
  }
6095
6174
  });
6096
6175
 
@@ -8074,7 +8153,7 @@ async function buildDoctorReport(options = {}) {
8074
8153
  const config = runtimeConfig.config;
8075
8154
  loadedConfig = config;
8076
8155
  loadedWorkspace = runtimeConfig.workspace;
8077
- const cliVersion = true ? "0.0.43" : "0.0.0-dev";
8156
+ const cliVersion = true ? "0.0.44" : "0.0.0-dev";
8078
8157
  const configVersion = config.devrouter?.version;
8079
8158
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
8080
8159
  addCheck(checks, {
@@ -14280,7 +14359,7 @@ var init_version = __esm({
14280
14359
 
14281
14360
  // src/cli.ts
14282
14361
  var import_commander = require("commander");
14283
- var CLI_VERSION = true ? "0.0.43" : "0.0.0-dev";
14362
+ var CLI_VERSION = true ? "0.0.44" : "0.0.0-dev";
14284
14363
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
14285
14364
  function withErrorHandling(action2) {
14286
14365
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.43",
3
+ "version": "0.0.44",
4
4
  "description": "Local dev routing CLI with shared Traefik reverse proxy",
5
5
  "author": "Roland Schlaefli",
6
6
  "homepage": "https://github.com/rschlaefli/devrouter#readme",
@@ -0,0 +1,30 @@
1
+ # Upgrade to devrouter 0.0.44
2
+
3
+ Shared local TLS certificate refresh now serializes inspection, generation,
4
+ verification, and route-configuration refresh across parallel worktrees.
5
+
6
+ 1. Install `@devrouter/cli@0.0.44` on the host and bump `.devrouter.yml` to
7
+ `devrouter.version: 0.0.44`.
8
+ 2. Keep using `devrouter ensure <checkout>` for primary and linked checkouts.
9
+ Do not run `mkcert` manually for individual worktree hosts; Devrouter now
10
+ preserves and compacts shared certificate coverage under its own lock.
11
+ 3. Do not remove the shared certificate or lock as routine cleanup. Routine
12
+ refresh still fails closed on a malformed certificate; the explicit
13
+ repository-scoped setup command printed by Devrouter replaces that generated
14
+ certificate while holding the same machine-global lock.
15
+
16
+ Verification:
17
+
18
+ - Run `devrouter -V --repo <checkout>` and confirm the installed CLI and local
19
+ repository report `0.0.44`.
20
+ - Start two linked checkouts concurrently with `devrouter ensure`; require both
21
+ namespaced HTTPS routes to pass readiness and remain covered afterward.
22
+ - Run `devrouter doctor --repo <checkout>` and confirm no TLS host-coverage
23
+ warning remains.
24
+
25
+ Report template:
26
+
27
+ - CLI/config version: `0.0.44`
28
+ - Parallel worktree routes: `<passed or details>`
29
+ - TLS host coverage: `<passed or details>`
30
+ - Remaining blockers: `<none or details>`