@hamedb89/localghost 0.5.0 → 0.6.1

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/cli.js CHANGED
@@ -546,6 +546,9 @@ function validRegistry(value) {
546
546
  function pruneRegistry(registry, now, isRunning) {
547
547
  registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
548
548
  }
549
+ function isTestSessionKey(instanceKey) {
550
+ return instanceKey.startsWith("test:");
551
+ }
549
552
  async function readJson(path) {
550
553
  try {
551
554
  return JSON.parse(await readFile(path, "utf8"));
@@ -642,6 +645,37 @@ function createLocalghostRegistry(options = {}) {
642
645
  await releaseLock();
643
646
  }
644
647
  },
648
+ async pruneTestSessions() {
649
+ const releaseLock = await lock();
650
+ try {
651
+ const registry = await readRegistry();
652
+ const staleTestKeys = new Set(
653
+ registry.leases.filter((lease) => isTestSessionKey(lease.instanceKey) && (lease.expiresAt <= now() || !isRunning(lease.pid))).map((lease) => leaseKey(lease.projectCwd, lease.instanceKey))
654
+ );
655
+ const beforeLeases = registry.leases.length;
656
+ registry.leases = registry.leases.filter((lease) => !staleTestKeys.has(leaseKey(lease.projectCwd, lease.instanceKey)));
657
+ const activeKeys = new Set(registry.leases.map((lease) => leaseKey(lease.projectCwd, lease.instanceKey)));
658
+ const beforeAllocations = registry.allocations.length;
659
+ registry.allocations = registry.allocations.filter(
660
+ (allocation) => !isTestSessionKey(allocation.instanceKey) || activeKeys.has(leaseKey(allocation.projectCwd, allocation.instanceKey))
661
+ );
662
+ await writeRegistry(registry);
663
+ return {
664
+ removedLeases: beforeLeases - registry.leases.length,
665
+ removedAllocations: beforeAllocations - registry.allocations.length
666
+ };
667
+ } finally {
668
+ await releaseLock();
669
+ }
670
+ },
671
+ async reset() {
672
+ const releaseLock = await lock();
673
+ try {
674
+ await writeRegistry({ version: 1, allocations: [], leases: [] });
675
+ } finally {
676
+ await releaseLock();
677
+ }
678
+ },
645
679
  async acquirePort(acquireOptions) {
646
680
  const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
647
681
  if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
@@ -2177,7 +2211,7 @@ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as rea
2177
2211
  import { homedir as homedir3 } from "os";
2178
2212
  import { dirname as dirname5, join as join10 } from "path";
2179
2213
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
2180
- var LOCALGHOST_VERSION = "0.5.0";
2214
+ var LOCALGHOST_VERSION = "0.6.1";
2181
2215
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
2182
2216
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
2183
2217
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -2345,7 +2379,7 @@ import { execa as execa4 } from "execa";
2345
2379
 
2346
2380
  // src/process.ts
2347
2381
  function signalManagedProcessPid(pid, signal, killProcess = (value, processSignal) => process.kill(value, processSignal)) {
2348
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid < 1) return false;
2382
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false;
2349
2383
  try {
2350
2384
  killProcess(process.platform === "win32" ? pid : -pid, signal);
2351
2385
  return true;
@@ -2697,10 +2731,13 @@ function registerSignalShutdown(stop) {
2697
2731
  };
2698
2732
  process.once("SIGINT", request);
2699
2733
  process.once("SIGTERM", request);
2700
- return () => {
2701
- process.off("SIGINT", request);
2702
- process.off("SIGTERM", request);
2703
- request();
2734
+ return {
2735
+ wasRequested: () => requested,
2736
+ finish: () => {
2737
+ process.off("SIGINT", request);
2738
+ process.off("SIGTERM", request);
2739
+ if (!requested) stop();
2740
+ }
2704
2741
  };
2705
2742
  }
2706
2743
  async function resolveServiceRuntimeEntries(services, dynamicPort, projectCwd) {
@@ -2752,11 +2789,22 @@ async function waitForPortsToBeAvailable(entries, timeoutMs = 1e4) {
2752
2789
  const deadline = Date.now() + timeoutMs;
2753
2790
  const ports = [...new Set(entries.map((entry) => entry.port))];
2754
2791
  while (Date.now() < deadline) {
2755
- const availability = await Promise.all(ports.map((port) => isPortAvailable(port)));
2756
- if (availability.every(Boolean)) return true;
2792
+ const availability2 = await Promise.all(ports.map((port) => isPortAvailable(port)));
2793
+ if (availability2.every(Boolean)) return { available: true, blocked: [] };
2757
2794
  await wait2(50);
2758
2795
  }
2759
- return false;
2796
+ const availability = await Promise.all(entries.map(async (entry) => ({
2797
+ entry,
2798
+ available: await isPortAvailable(entry.port)
2799
+ })));
2800
+ const blocked = availability.filter(({ available }) => !available).map(({ entry }) => entry);
2801
+ return { available: blocked.length === 0, blocked };
2802
+ }
2803
+ function formatBlockedPorts(entries) {
2804
+ return [...new Map(entries.map((entry) => [
2805
+ `${entry.host}:${entry.port}`,
2806
+ `${entry.host} (port ${entry.port})`
2807
+ ])).values()].join(", ");
2760
2808
  }
2761
2809
  async function waitForProcessShutdown(processes, forceStop, timeoutMs = 1e4) {
2762
2810
  const settled = Promise.allSettled(processes);
@@ -2847,7 +2895,7 @@ async function runDetectedServices(options) {
2847
2895
  }
2848
2896
  signalManagedProcess(caddy, signal);
2849
2897
  };
2850
- const finishShutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
2898
+ const shutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
2851
2899
  try {
2852
2900
  const ready = await Promise.race([
2853
2901
  waitForServicePorts(entries),
@@ -2859,15 +2907,18 @@ async function runDetectedServices(options) {
2859
2907
  }
2860
2908
  await processExit;
2861
2909
  } finally {
2862
- finishShutdown();
2910
+ shutdown.finish();
2863
2911
  await waitForProcessShutdown(
2864
2912
  [caddyExit, ...children],
2865
2913
  stopManaged
2866
2914
  );
2867
- if (!await waitForPortsToBeAvailable(entries)) {
2868
- console.warn("Localghost: timed out waiting for service ports to be released.");
2915
+ const drained = await waitForPortsToBeAvailable(entries);
2916
+ if (!drained.available && !shutdown.wasRequested()) {
2917
+ console.warn(`Localghost: timed out waiting for service ports to be released: ${formatBlockedPorts(drained.blocked)}.`);
2869
2918
  stopManaged("SIGTERM");
2870
- if (!await waitForPortsToBeAvailable(entries, 2e3)) {
2919
+ const terminated = await waitForPortsToBeAvailable(entries, 2e3);
2920
+ if (!terminated.available) {
2921
+ console.warn(`Localghost: service ports still occupied after termination: ${formatBlockedPorts(terminated.blocked)}.`);
2871
2922
  stopManaged("SIGKILL");
2872
2923
  }
2873
2924
  }
@@ -3245,6 +3296,15 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
3245
3296
  unregisterLocalghostSetup({ cwd: options.cwd, projectName });
3246
3297
  console.log(`State ${statePath}`);
3247
3298
  });
3299
+ program.command("ports").description("Manage Localghost port registry state").addCommand(new Command("prune").description("Remove expired or dead port leases").option("--cwd <path>", "Project directory", process.cwd()).option("--test-only", "Remove only stale test-session leases and records").option("--json", "Print raw JSON").action(async (options) => {
3300
+ const result = options.testOnly ? await createLocalghostRegistry({ cwd: options.cwd }).pruneTestSessions() : await createLocalghostRegistry({ cwd: options.cwd }).prune();
3301
+ if (options.json) console.log(JSON.stringify(result, null, 2));
3302
+ else console.log(`Pruned ${result.removedLeases} stale registry lease${result.removedLeases === 1 ? "" : "s"}${"removedAllocations" in result ? ` and ${result.removedAllocations} test allocation${result.removedAllocations === 1 ? "" : "s"}` : ""}.`);
3303
+ })).addCommand(new Command("reset").description("Clear all remembered allocations and leases").option("--cwd <path>", "Project directory", process.cwd()).option("--yes", "Confirm the destructive registry reset").action(async (options) => {
3304
+ if (!options.yes) throw new Error("Resetting the port registry is destructive. Re-run with --yes.");
3305
+ await createLocalghostRegistry({ cwd: options.cwd }).reset();
3306
+ console.log("Localghost port registry reset.");
3307
+ }));
3248
3308
  program.command("test").description("Run a test command with an isolated Localghost port").option("--cwd <path>", "Project directory", process.cwd()).option("--instance <name>", "Parallel test instance name", String(process.pid)).option("--port <number>", "Initial test port", parsePort2, 5173).option("--lease-ttl <milliseconds>", "Lease lifetime before heartbeat renewal", Number, 30 * 60 * 1e3).argument("<command...>", "Command to run after --").action(async (command, options) => {
3249
3309
  const [binary, ...args] = command;
3250
3310
  if (!binary) throw new Error("Missing command. Use: localghost test -- <command>");
@@ -3524,19 +3584,22 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
3524
3584
  signalManagedProcess(child, signal);
3525
3585
  signalManagedProcess(caddy, signal);
3526
3586
  };
3527
- const finishShutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
3587
+ const shutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
3528
3588
  try {
3529
3589
  await Promise.race([child, caddyExit]);
3530
3590
  } finally {
3531
- finishShutdown();
3591
+ shutdown.finish();
3532
3592
  await waitForProcessShutdown(
3533
3593
  [child, caddyExit],
3534
3594
  stopManaged
3535
3595
  );
3536
- if (!await waitForPortsToBeAvailable(context.entries)) {
3537
- console.warn("Localghost: timed out waiting for service ports to be released.");
3596
+ const drained = await waitForPortsToBeAvailable(context.entries);
3597
+ if (!drained.available && !shutdown.wasRequested()) {
3598
+ console.warn(`Localghost: timed out waiting for service ports to be released: ${formatBlockedPorts(drained.blocked)}.`);
3538
3599
  stopManaged("SIGTERM");
3539
- if (!await waitForPortsToBeAvailable(context.entries, 2e3)) {
3600
+ const terminated = await waitForPortsToBeAvailable(context.entries, 2e3);
3601
+ if (!terminated.available) {
3602
+ console.warn(`Localghost: service ports still occupied after termination: ${formatBlockedPorts(terminated.blocked)}.`);
3540
3603
  stopManaged("SIGKILL");
3541
3604
  }
3542
3605
  }