@hamedb89/localghost 0.4.1 → 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/index.d.ts CHANGED
@@ -543,18 +543,6 @@ declare function packageRunCommand(packageManager: PackageManager, script: strin
543
543
  declare function packageAddCommand(packageManager: PackageManager, packageName?: string): string;
544
544
  declare function initLocalghost(options?: InitOptions): InitResult;
545
545
 
546
- type FindAvailablePortOptions = {
547
- host?: string;
548
- maxAttempts?: number;
549
- };
550
- declare function isPortAvailable(port: number, host?: string): Promise<boolean>;
551
- declare function findAvailablePort(startPort: number, options?: FindAvailablePortOptions): Promise<number>;
552
-
553
- type ProcessSignal = NodeJS.Signals;
554
- type ProcessKiller = (pid: number, signal: ProcessSignal) => void;
555
- declare function signalManagedProcessPid(pid: number | undefined, signal: ProcessSignal, killProcess?: ProcessKiller): boolean;
556
- declare function signalManagedProcess(child: Pick<ChildProcess, "pid" | "kill" | "killed">, signal: ProcessSignal): boolean;
557
-
558
546
  declare const LOCALGHOST_REGISTRY_FILE = "registry.json";
559
547
  declare const LOCALGHOST_REGISTRY_LOCK_FILE = "registry.lock";
560
548
  type PortAvailabilityCheck = (port: number, host?: string) => boolean | Promise<boolean>;
@@ -599,12 +587,18 @@ type AcquireLocalghostPortOptions = {
599
587
  leaseTtlMs?: number;
600
588
  host?: string;
601
589
  };
590
+ type RenewLocalghostPortOptions = {
591
+ projectCwd?: string;
592
+ instanceKey: string;
593
+ leaseTtlMs?: number;
594
+ };
602
595
  type LocalghostRegistry = {
603
596
  root: string;
604
597
  registryPath: string;
605
598
  lockPath: string;
606
599
  ownerToken: string;
607
600
  acquirePort(options: AcquireLocalghostPortOptions): Promise<LocalghostLease>;
601
+ renewPort(options: RenewLocalghostPortOptions): Promise<LocalghostLease | undefined>;
608
602
  releasePort(options: {
609
603
  projectCwd?: string;
610
604
  instanceKey: string;
@@ -613,11 +607,48 @@ type LocalghostRegistry = {
613
607
  prune(): Promise<{
614
608
  removedLeases: number;
615
609
  }>;
610
+ pruneTestSessions(): Promise<{
611
+ removedLeases: number;
612
+ removedAllocations: number;
613
+ }>;
614
+ reset(): Promise<void>;
616
615
  };
617
616
  declare function getLocalghostRegistryRoot(env?: NodeJS.ProcessEnv): string;
618
617
  declare function canonicalizeLocalghostProjectCwd(cwd?: string): string;
619
618
  declare function createLocalghostRegistry(options?: LocalghostRegistryOptions): LocalghostRegistry;
620
619
 
620
+ type LocalghostTestSessionOptions = {
621
+ cwd?: string;
622
+ instanceKey: string;
623
+ services: Record<string, {
624
+ startPort: number;
625
+ maxAttempts?: number;
626
+ host?: string;
627
+ }>;
628
+ leaseTtlMs?: number;
629
+ };
630
+ type LocalghostTestSession = {
631
+ instanceKey: string;
632
+ ports: Record<string, number>;
633
+ leases: LocalghostLease[];
634
+ renew: () => Promise<void>;
635
+ startHeartbeat: (intervalMs?: number) => NodeJS.Timeout;
636
+ release: () => Promise<void>;
637
+ };
638
+ declare function createLocalghostTestSession(options: LocalghostTestSessionOptions): Promise<LocalghostTestSession>;
639
+
640
+ type FindAvailablePortOptions = {
641
+ host?: string;
642
+ maxAttempts?: number;
643
+ };
644
+ declare function isPortAvailable(port: number, host?: string): Promise<boolean>;
645
+ declare function findAvailablePort(startPort: number, options?: FindAvailablePortOptions): Promise<number>;
646
+
647
+ type ProcessSignal = NodeJS.Signals;
648
+ type ProcessKiller = (pid: number, signal: ProcessSignal) => void;
649
+ declare function signalManagedProcessPid(pid: number | undefined, signal: ProcessSignal, killProcess?: ProcessKiller): boolean;
650
+ declare function signalManagedProcess(child: Pick<ChildProcess, "pid" | "kill" | "killed">, signal: ProcessSignal): boolean;
651
+
621
652
  type DomainRoute = {
622
653
  host: string;
623
654
  port: number;
@@ -685,7 +716,7 @@ type CreateVercelGhostTunnelHandlerOptions = {
685
716
  declare function createVercelGhostTunnelHandler(options: CreateVercelGhostTunnelHandlerOptions): (request: VercelGhostTunnelRequestLike, response: VercelGhostTunnelResponseLike) => Promise<void>;
686
717
 
687
718
  declare const LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
688
- declare const LOCALGHOST_VERSION = "0.4.1";
719
+ declare const LOCALGHOST_VERSION = "0.6.1";
689
720
  declare const UPDATE_CHECK_CACHE_TTL_MS: number;
690
721
  declare const UPDATE_CHECK_NOTIFY_TTL_MS: number;
691
722
  declare const UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -722,4 +753,4 @@ declare function maybeNotifyAboutUpdate(options?: {
722
753
  disabled?: boolean;
723
754
  }): Promise<void>;
724
755
 
725
- export { type AcquireLocalghostPortOptions, type ActiveRelayRoute, type CaddyModeOptions, type CaddyProcessKiller, type CaddyProcessStopResult, ConfigPattern, type ConstructGhostTunnelIpUrlInput, ConstructGhostTunnelUrlInput, type CreateVercelGhostTunnelHandlerOptions, DEFAULT_GHOST_TUNNEL_IP_TRANSPORT_TTL_SECONDS, DEFAULT_GHOST_TUNNEL_RESPONSE_TTL_SECONDS, DEFAULT_GHOST_TUNNEL_TRANSPORT_QUERY_PARAM, DEFAULT_RELAY_ALLOWED_TARGET_HOSTS, DEFAULT_RELAY_BLOCKED_PORTS, DEFAULT_RELAY_LIMITS, DEFAULT_RELAY_TARGET_POLICY, type DetectedDevCommand, type DetectedDevService, DevHostEntry, type DoctorOptions, type DoctorResult, type DomainRoute, type DomainRouteOptions, type FindAvailablePortOptions, type GhostTunnelAgent, type GhostTunnelAgentOptions, GhostTunnelConfig, type GhostTunnelHttpResponse, type GhostTunnelIpTransportClaim, GhostTunnelOptions, type GhostTunnelQueuedRequest, type GhostTunnelQueuedResponse, GhostTunnelRoute, type GhostTunnelRouteHeartbeat, type GhostTunnelStore, type GhostTunnelStoreEnv, GhostTunnelTransportConfig, GhostTunnelTransportOptions, type InitOptions, type InitResult, LOCALGHOST_ACTIVITY_VERSION, LOCALGHOST_AGENT_GUIDE, LOCALGHOST_GHOST_TUNNEL_FILE, LOCALGHOST_PACKAGE_NAME, LOCALGHOST_REGISTRY_FILE, LOCALGHOST_REGISTRY_LOCK_FILE, LOCALGHOST_STATE_FILE, LOCALGHOST_VERSION, type LocalghostActivity, type LocalghostContext, type LocalghostContextOptions, type LocalghostEnvironment, type LocalghostLease, type LocalghostPackageManager, type LocalghostProjectConfig, type LocalghostProjectConfigResult, type LocalghostRegistry, type LocalghostRegistryData, type LocalghostRegistryEntry, type LocalghostRegistryOptions, type LocalghostRunMode, type LocalghostRunRecord, type LocalghostServiceOptions, type LocalghostSetupRecord, type LocalghostState, type LocalghostStateAction, type PackageManager, type PortAvailabilityCheck, ReadDevHostsOptions, type ReadGhostTunnelOptions, type RedisGhostTunnelEnvResolution, type RedisGhostTunnelStoreOptions, type RegisterLocalghostRunInput, type RegisterLocalghostSetupInput, type RelayAccessMode, type RelayLimits, type RelayLocalTarget, type RelayOfflineResponse, type RelayProtocol, type RelayRouteClaim, type RelayRouteRegistrationInput, type RelayTargetPolicy, type RemoveSystemHostsResult, type ResolveGhostTunnelIpRedirectInput, type ResolveGhostTunnelRequestInput, ResolvedDevHostsPath, type ResolvedGhostTunnelIpRedirect, type ResolvedGhostTunnelRequest, type ServeGhostTunnelLocalRequestInput, type SignedGhostTunnelIpTransportClaim, type SignedRelayRouteClaim, UPDATE_CHECK_CACHE_TTL_MS, UPDATE_CHECK_NOTIFY_TTL_MS, UPDATE_CHECK_TIMEOUT_MS, type UpdateCheckCache, type UpdateCheckResult, type UpdateSystemHostsResult, type VercelGhostTunnelRequestLike, type VercelGhostTunnelResponseLike, type WriteLocalghostStateInput, assertExactRelayHost, assertLocalDevelopment, assertRelayLocalTarget, authenticateRelayAgentToken, canonicalizeLocalghostProjectCwd, checkCaddy, checkForUpdate, compareVersions, constructGhostTunnelIpUrl, createGhostTunnelQueuedRequest, createGhostTunnelRouteHeartbeat, createLocalghostRegistry, createMemoryGhostTunnelStore, createRedisGhostTunnelStore, createRedisGhostTunnelStoreFromEnv, createRelayRouteRegistration, createVercelGhostTunnelHandler, decodeGhostTunnelBody, defineLocalghostConfig, detectDevCommand, detectDevPackageManager, detectDevServices, detectPackageManager, encodeGhostTunnelBody, findAvailablePort, findGhostTunnelEntry, formatDetectedDevCommand, formatDetectedDevServices, formatDomainRoutes, formatGhostTunnel, formatLocalghostAgentGuide, formatUpdateMessage, getCaddyfilePath, getDomainRoutes, getGhostTunnelPath, getLocalghostActivityPath, getLocalghostRegistryRoot, getLocalghostStatePath, getProductionEnvKeys, getProductionReason, getSystemHostsPath, getUpdateCheckCachePath, initLocalghost, isNewerVersion, isPortAvailable, isProcessRunning, isProductionLike, isRelayRouteActive, isUpdateCheckDisabled, listGhostTunnelEntries, listLocalghostRuns, listLocalghostSetups, markUpdateNotified, maybeNotifyAboutUpdate, packageAddCommand, packageRunCommand, patchLocalghostState, pruneLocalghostActivity, readGhostTunnelEntries, readLocalghostActivity, readLocalghostProjectConfig, readLocalghostState, redactRelayHeaders, redactRelayLogUrl, registerLocalghostRun, registerLocalghostSetup, removeManagedBlock, removeSystemHosts, renderCaddyfile, renderCompactLocalghostBanner, renderGhostTunnelRelayOfflineResponse, renderGhostTunnelRouteMissingResponse, renderHostsBlock, renderLocalghostBanner, renderRelayOfflineResponse, resolveGhostTunnelIpRedirect, resolveGhostTunnelPath, resolveGhostTunnelRequest, resolveLocalghostContext, resolveRedisGhostTunnelEnv, runCaddy, runDoctor, serveGhostTunnelLocalRequest, shouldNotifyAboutUpdate, signGhostTunnelIpTransportClaim, signRelayRouteClaim, signalManagedProcess, signalManagedProcessPid, startCaddy, startGhostTunnelAgent, stopCaddyProcesses, stripRelayForwardHeaders, trustCaddy, unregisterLocalghostRun, unregisterLocalghostSetup, updateSystemHosts, upsertManagedBlock, validateCaddyfile, verifyGhostTunnelIpTransportClaim, verifyRelayRouteClaim, writeCaddyfile, writeLocalghostActivity, writeLocalghostState };
756
+ export { type AcquireLocalghostPortOptions, type ActiveRelayRoute, type CaddyModeOptions, type CaddyProcessKiller, type CaddyProcessStopResult, ConfigPattern, type ConstructGhostTunnelIpUrlInput, ConstructGhostTunnelUrlInput, type CreateVercelGhostTunnelHandlerOptions, DEFAULT_GHOST_TUNNEL_IP_TRANSPORT_TTL_SECONDS, DEFAULT_GHOST_TUNNEL_RESPONSE_TTL_SECONDS, DEFAULT_GHOST_TUNNEL_TRANSPORT_QUERY_PARAM, DEFAULT_RELAY_ALLOWED_TARGET_HOSTS, DEFAULT_RELAY_BLOCKED_PORTS, DEFAULT_RELAY_LIMITS, DEFAULT_RELAY_TARGET_POLICY, type DetectedDevCommand, type DetectedDevService, DevHostEntry, type DoctorOptions, type DoctorResult, type DomainRoute, type DomainRouteOptions, type FindAvailablePortOptions, type GhostTunnelAgent, type GhostTunnelAgentOptions, GhostTunnelConfig, type GhostTunnelHttpResponse, type GhostTunnelIpTransportClaim, GhostTunnelOptions, type GhostTunnelQueuedRequest, type GhostTunnelQueuedResponse, GhostTunnelRoute, type GhostTunnelRouteHeartbeat, type GhostTunnelStore, type GhostTunnelStoreEnv, GhostTunnelTransportConfig, GhostTunnelTransportOptions, type InitOptions, type InitResult, LOCALGHOST_ACTIVITY_VERSION, LOCALGHOST_AGENT_GUIDE, LOCALGHOST_GHOST_TUNNEL_FILE, LOCALGHOST_PACKAGE_NAME, LOCALGHOST_REGISTRY_FILE, LOCALGHOST_REGISTRY_LOCK_FILE, LOCALGHOST_STATE_FILE, LOCALGHOST_VERSION, type LocalghostActivity, type LocalghostContext, type LocalghostContextOptions, type LocalghostEnvironment, type LocalghostLease, type LocalghostPackageManager, type LocalghostProjectConfig, type LocalghostProjectConfigResult, type LocalghostRegistry, type LocalghostRegistryData, type LocalghostRegistryEntry, type LocalghostRegistryOptions, type LocalghostRunMode, type LocalghostRunRecord, type LocalghostServiceOptions, type LocalghostSetupRecord, type LocalghostState, type LocalghostStateAction, type LocalghostTestSession, type LocalghostTestSessionOptions, type PackageManager, type PortAvailabilityCheck, ReadDevHostsOptions, type ReadGhostTunnelOptions, type RedisGhostTunnelEnvResolution, type RedisGhostTunnelStoreOptions, type RegisterLocalghostRunInput, type RegisterLocalghostSetupInput, type RelayAccessMode, type RelayLimits, type RelayLocalTarget, type RelayOfflineResponse, type RelayProtocol, type RelayRouteClaim, type RelayRouteRegistrationInput, type RelayTargetPolicy, type RemoveSystemHostsResult, type ResolveGhostTunnelIpRedirectInput, type ResolveGhostTunnelRequestInput, ResolvedDevHostsPath, type ResolvedGhostTunnelIpRedirect, type ResolvedGhostTunnelRequest, type ServeGhostTunnelLocalRequestInput, type SignedGhostTunnelIpTransportClaim, type SignedRelayRouteClaim, UPDATE_CHECK_CACHE_TTL_MS, UPDATE_CHECK_NOTIFY_TTL_MS, UPDATE_CHECK_TIMEOUT_MS, type UpdateCheckCache, type UpdateCheckResult, type UpdateSystemHostsResult, type VercelGhostTunnelRequestLike, type VercelGhostTunnelResponseLike, type WriteLocalghostStateInput, assertExactRelayHost, assertLocalDevelopment, assertRelayLocalTarget, authenticateRelayAgentToken, canonicalizeLocalghostProjectCwd, checkCaddy, checkForUpdate, compareVersions, constructGhostTunnelIpUrl, createGhostTunnelQueuedRequest, createGhostTunnelRouteHeartbeat, createLocalghostRegistry, createLocalghostTestSession, createMemoryGhostTunnelStore, createRedisGhostTunnelStore, createRedisGhostTunnelStoreFromEnv, createRelayRouteRegistration, createVercelGhostTunnelHandler, decodeGhostTunnelBody, defineLocalghostConfig, detectDevCommand, detectDevPackageManager, detectDevServices, detectPackageManager, encodeGhostTunnelBody, findAvailablePort, findGhostTunnelEntry, formatDetectedDevCommand, formatDetectedDevServices, formatDomainRoutes, formatGhostTunnel, formatLocalghostAgentGuide, formatUpdateMessage, getCaddyfilePath, getDomainRoutes, getGhostTunnelPath, getLocalghostActivityPath, getLocalghostRegistryRoot, getLocalghostStatePath, getProductionEnvKeys, getProductionReason, getSystemHostsPath, getUpdateCheckCachePath, initLocalghost, isNewerVersion, isPortAvailable, isProcessRunning, isProductionLike, isRelayRouteActive, isUpdateCheckDisabled, listGhostTunnelEntries, listLocalghostRuns, listLocalghostSetups, markUpdateNotified, maybeNotifyAboutUpdate, packageAddCommand, packageRunCommand, patchLocalghostState, pruneLocalghostActivity, readGhostTunnelEntries, readLocalghostActivity, readLocalghostProjectConfig, readLocalghostState, redactRelayHeaders, redactRelayLogUrl, registerLocalghostRun, registerLocalghostSetup, removeManagedBlock, removeSystemHosts, renderCaddyfile, renderCompactLocalghostBanner, renderGhostTunnelRelayOfflineResponse, renderGhostTunnelRouteMissingResponse, renderHostsBlock, renderLocalghostBanner, renderRelayOfflineResponse, resolveGhostTunnelIpRedirect, resolveGhostTunnelPath, resolveGhostTunnelRequest, resolveLocalghostContext, resolveRedisGhostTunnelEnv, runCaddy, runDoctor, serveGhostTunnelLocalRequest, shouldNotifyAboutUpdate, signGhostTunnelIpTransportClaim, signRelayRouteClaim, signalManagedProcess, signalManagedProcessPid, startCaddy, startGhostTunnelAgent, stopCaddyProcesses, stripRelayForwardHeaders, trustCaddy, unregisterLocalghostRun, unregisterLocalghostSetup, updateSystemHosts, upsertManagedBlock, validateCaddyfile, verifyGhostTunnelIpTransportClaim, verifyRelayRouteClaim, writeCaddyfile, writeLocalghostActivity, writeLocalghostState };
package/dist/index.js CHANGED
@@ -844,11 +844,11 @@ function isStopped(signal, localSignal) {
844
844
  }
845
845
  function wait(ms, signal, localSignal) {
846
846
  if (isStopped(signal, localSignal)) return Promise.resolve();
847
- return new Promise((resolve4) => {
848
- const timeout = setTimeout(resolve4, ms);
847
+ return new Promise((resolve5) => {
848
+ const timeout = setTimeout(resolve5, ms);
849
849
  const stop = () => {
850
850
  clearTimeout(timeout);
851
- resolve4();
851
+ resolve5();
852
852
  };
853
853
  signal?.addEventListener("abort", stop, { once: true });
854
854
  localSignal.addEventListener("abort", stop, { once: true });
@@ -1696,13 +1696,13 @@ import { pathToFileURL } from "url";
1696
1696
  // src/port.ts
1697
1697
  import { createServer } from "net";
1698
1698
  async function isPortAvailable(port, host = "127.0.0.1") {
1699
- return new Promise((resolve4) => {
1699
+ return new Promise((resolve5) => {
1700
1700
  const server = createServer();
1701
1701
  server.once("error", () => {
1702
- resolve4(false);
1702
+ resolve5(false);
1703
1703
  });
1704
1704
  server.once("listening", () => {
1705
- server.close(() => resolve4(true));
1705
+ server.close(() => resolve5(true));
1706
1706
  });
1707
1707
  server.listen(port, host);
1708
1708
  });
@@ -1755,6 +1755,9 @@ function validRegistry(value) {
1755
1755
  function pruneRegistry(registry, now, isRunning) {
1756
1756
  registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
1757
1757
  }
1758
+ function isTestSessionKey(instanceKey) {
1759
+ return instanceKey.startsWith("test:");
1760
+ }
1758
1761
  async function readJson(path) {
1759
1762
  try {
1760
1763
  return JSON.parse(await readFile(path, "utf8"));
@@ -1851,6 +1854,37 @@ function createLocalghostRegistry(options = {}) {
1851
1854
  await releaseLock();
1852
1855
  }
1853
1856
  },
1857
+ async pruneTestSessions() {
1858
+ const releaseLock = await lock();
1859
+ try {
1860
+ const registry = await readRegistry();
1861
+ const staleTestKeys = new Set(
1862
+ registry.leases.filter((lease) => isTestSessionKey(lease.instanceKey) && (lease.expiresAt <= now() || !isRunning(lease.pid))).map((lease) => leaseKey(lease.projectCwd, lease.instanceKey))
1863
+ );
1864
+ const beforeLeases = registry.leases.length;
1865
+ registry.leases = registry.leases.filter((lease) => !staleTestKeys.has(leaseKey(lease.projectCwd, lease.instanceKey)));
1866
+ const activeKeys = new Set(registry.leases.map((lease) => leaseKey(lease.projectCwd, lease.instanceKey)));
1867
+ const beforeAllocations = registry.allocations.length;
1868
+ registry.allocations = registry.allocations.filter(
1869
+ (allocation) => !isTestSessionKey(allocation.instanceKey) || activeKeys.has(leaseKey(allocation.projectCwd, allocation.instanceKey))
1870
+ );
1871
+ await writeRegistry(registry);
1872
+ return {
1873
+ removedLeases: beforeLeases - registry.leases.length,
1874
+ removedAllocations: beforeAllocations - registry.allocations.length
1875
+ };
1876
+ } finally {
1877
+ await releaseLock();
1878
+ }
1879
+ },
1880
+ async reset() {
1881
+ const releaseLock = await lock();
1882
+ try {
1883
+ await writeRegistry({ version: 1, allocations: [], leases: [] });
1884
+ } finally {
1885
+ await releaseLock();
1886
+ }
1887
+ },
1854
1888
  async acquirePort(acquireOptions) {
1855
1889
  const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
1856
1890
  if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
@@ -1888,6 +1922,18 @@ function createLocalghostRegistry(options = {}) {
1888
1922
  return lease;
1889
1923
  });
1890
1924
  },
1925
+ async renewPort(renewOptions) {
1926
+ const projectCwd = canonicalizeLocalghostProjectCwd(renewOptions.projectCwd ?? cwd);
1927
+ return withLock(async (registry) => {
1928
+ const lease = registry.leases.find(
1929
+ (candidate) => candidate.projectCwd === projectCwd && candidate.instanceKey === renewOptions.instanceKey && candidate.ownerToken === ownerToken
1930
+ );
1931
+ if (!lease || lease.expiresAt <= now() || !isRunning(lease.pid)) return void 0;
1932
+ lease.expiresAt = now() + (renewOptions.leaseTtlMs ?? 30 * 60 * 1e3);
1933
+ await writeRegistry(registry);
1934
+ return lease;
1935
+ });
1936
+ },
1891
1937
  async releasePort(releaseOptions) {
1892
1938
  const projectCwd = canonicalizeLocalghostProjectCwd(releaseOptions.projectCwd ?? cwd);
1893
1939
  return withLock(async (registry) => {
@@ -2429,7 +2475,7 @@ async function removeSystemHosts(projectName) {
2429
2475
 
2430
2476
  // src/init.ts
2431
2477
  import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2432
- import { join as join8 } from "path";
2478
+ import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
2433
2479
  function detectPackageManager(cwd = process.cwd()) {
2434
2480
  if (existsSync5(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
2435
2481
  if (existsSync5(join8(cwd, "yarn.lock"))) return "yarn";
@@ -2561,9 +2607,62 @@ function initLocalghost(options = {}) {
2561
2607
  };
2562
2608
  }
2563
2609
 
2610
+ // src/test-session.ts
2611
+ async function createLocalghostTestSession(options) {
2612
+ if (!options.instanceKey) throw new Error("instanceKey is required");
2613
+ const registry = createLocalghostRegistry(options.cwd ? { cwd: options.cwd } : {});
2614
+ const leases = [];
2615
+ const ports = {};
2616
+ const leaseTtlMs = options.leaseTtlMs ?? 30 * 60 * 1e3;
2617
+ if (!Number.isFinite(leaseTtlMs) || leaseTtlMs < 1e3) throw new Error("leaseTtlMs must be at least 1000 milliseconds.");
2618
+ try {
2619
+ for (const [name, service] of Object.entries(options.services)) {
2620
+ const lease = await registry.acquirePort({
2621
+ ...options.cwd ? { projectCwd: options.cwd } : {},
2622
+ instanceKey: `test:${options.instanceKey}:${name}`,
2623
+ startPort: service.startPort,
2624
+ ...service.maxAttempts !== void 0 ? { maxAttempts: service.maxAttempts } : {},
2625
+ ...service.host !== void 0 ? { host: service.host } : {},
2626
+ leaseTtlMs,
2627
+ reservedPorts: Object.values(ports)
2628
+ });
2629
+ leases.push(lease);
2630
+ ports[name] = lease.port;
2631
+ }
2632
+ } catch (error) {
2633
+ await Promise.all(leases.map((lease) => registry.releasePort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey })));
2634
+ throw error;
2635
+ }
2636
+ let released = false;
2637
+ const renew = async () => {
2638
+ if (released) return;
2639
+ await Promise.all(leases.map(async (lease) => {
2640
+ const renewed = await registry.renewPort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey, leaseTtlMs });
2641
+ if (!renewed) throw new Error(`Localghost test lease expired: ${lease.instanceKey}`);
2642
+ }));
2643
+ };
2644
+ return {
2645
+ instanceKey: options.instanceKey,
2646
+ ports,
2647
+ leases,
2648
+ renew,
2649
+ startHeartbeat(intervalMs = Math.max(1e3, Math.floor(leaseTtlMs / 3))) {
2650
+ if (!Number.isFinite(intervalMs) || intervalMs < 1e3) throw new Error("Heartbeat interval must be at least 1000 milliseconds.");
2651
+ const timer = setInterval(() => void renew().catch(() => void 0), intervalMs);
2652
+ timer.unref();
2653
+ return timer;
2654
+ },
2655
+ async release() {
2656
+ if (released) return;
2657
+ released = true;
2658
+ await Promise.all(leases.map((lease) => registry.releasePort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey })));
2659
+ }
2660
+ };
2661
+ }
2662
+
2564
2663
  // src/process.ts
2565
2664
  function signalManagedProcessPid(pid, signal, killProcess = (value, processSignal) => process.kill(value, processSignal)) {
2566
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid < 1) return false;
2665
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false;
2567
2666
  try {
2568
2667
  killProcess(process.platform === "win32" ? pid : -pid, signal);
2569
2668
  return true;
@@ -2796,7 +2895,7 @@ async function waitForTunnelResponse(input) {
2796
2895
  await input.store.cleanup(input.requestId);
2797
2896
  return response;
2798
2897
  }
2799
- await new Promise((resolve4) => setTimeout(resolve4, input.pollIntervalMs));
2898
+ await new Promise((resolve5) => setTimeout(resolve5, input.pollIntervalMs));
2800
2899
  }
2801
2900
  return null;
2802
2901
  }
@@ -2897,9 +2996,9 @@ function createVercelGhostTunnelHandler(options) {
2897
2996
  // src/update-check.ts
2898
2997
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2899
2998
  import { homedir as homedir3 } from "os";
2900
- import { dirname as dirname4, join as join10 } from "path";
2999
+ import { dirname as dirname5, join as join10 } from "path";
2901
3000
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
2902
- var LOCALGHOST_VERSION = "0.4.1";
3001
+ var LOCALGHOST_VERSION = "0.6.1";
2903
3002
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
2904
3003
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
2905
3004
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -2924,7 +3023,7 @@ function readCache(path = getUpdateCheckCachePath()) {
2924
3023
  }
2925
3024
  function writeCache(cache, path = getUpdateCheckCachePath()) {
2926
3025
  try {
2927
- mkdirSync3(dirname4(path), { recursive: true });
3026
+ mkdirSync3(dirname5(path), { recursive: true });
2928
3027
  writeFileSync5(path, `${JSON.stringify(cache, null, 2)}
2929
3028
  `, "utf8");
2930
3029
  } catch {
@@ -3097,6 +3196,7 @@ export {
3097
3196
  createGhostTunnelQueuedRequest,
3098
3197
  createGhostTunnelRouteHeartbeat,
3099
3198
  createLocalghostRegistry,
3199
+ createLocalghostTestSession,
3100
3200
  createMemoryGhostTunnelStore,
3101
3201
  createRedisGhostTunnelStore,
3102
3202
  createRedisGhostTunnelStoreFromEnv,