@hamedb89/localghost 0.1.13 → 0.1.15

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.js CHANGED
@@ -265,10 +265,71 @@ function getProjectName(cwd = process.cwd()) {
265
265
  }
266
266
  }
267
267
  function sanitizeProjectName(value) {
268
- const projectName = value.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
268
+ const sanitized = value.replace(/[^\w.-]+/g, "-");
269
+ let start = 0;
270
+ let end = sanitized.length;
271
+ while (start < end && sanitized.charCodeAt(start) === 45) start += 1;
272
+ while (end > start && sanitized.charCodeAt(end - 1) === 45) end -= 1;
273
+ const projectName = sanitized.slice(start, end);
269
274
  return projectName || "app";
270
275
  }
271
276
 
277
+ // src/guide.ts
278
+ var LOCALGHOST_AGENT_GUIDE = `# Localghost agent guide
279
+
280
+ Localghost owns the local development proxy and the app process boundary.
281
+
282
+ ## Preferred repository setup
283
+
284
+ For a normal repository, use this package script:
285
+
286
+ "dev": "localghost"
287
+
288
+ For an explicit app command, keep the raw command separate:
289
+
290
+ "dev": "localghost run -- vite"
291
+ "dev:raw": "vite"
292
+
293
+ Use \`localghost dev\` only when the Caddy proxy should run without starting the app.
294
+
295
+ ## Useful commands
296
+
297
+ - \`localghost\`: detect and run the repository development command.
298
+ - \`localghost run -- <command>\`: wrap an explicit app command.
299
+ - \`localghost dev\`: run only the local Caddy proxy.
300
+ - \`localghost status --ready\`: check project setup.
301
+ - \`localghost repair\`: repair managed hosts and Caddy setup.
302
+ - \`localghost ps --json\`: inspect Localghost-managed repositories, instances, and ports.
303
+ - \`localghost routes\`: inspect hostname-to-port routing.
304
+ - \`localghost doctor\`: check machine prerequisites.
305
+
306
+ ## Configuration
307
+
308
+ - Commit repository defaults in \`localghost.config.mjs\`.
309
+ - Keep hostname and requested-port routes in \`.localghost\`.
310
+ - CLI flags override repository configuration for one invocation.
311
+ - Localghost remembers active project and instance port assignments in user state under \`~/.localghost\`.
312
+ - Do not edit the registry manually and do not start Caddy separately.
313
+
314
+ ## Port behavior
315
+
316
+ Localghost remembers ports by canonical repository path and instance key. Concurrent Localghost instances receive distinct ports. The operating-system bind check remains authoritative when another tool already owns a port.
317
+ `;
318
+ function formatLocalghostAgentGuide(format = "text") {
319
+ if (format === "json") {
320
+ return JSON.stringify({
321
+ preferredScript: "localghost",
322
+ explicitScript: "localghost run -- <command>",
323
+ proxyOnlyCommand: "localghost dev",
324
+ inspectionCommands: ["localghost status --ready", "localghost ps --json", "localghost routes", "localghost doctor"],
325
+ projectConfig: "localghost.config.mjs",
326
+ routeConfig: ".localghost",
327
+ userState: "~/.localghost"
328
+ }, null, 2);
329
+ }
330
+ return LOCALGHOST_AGENT_GUIDE;
331
+ }
332
+
272
333
  // src/ghost-file.ts
273
334
  var LOCALGHOST_GHOST_TUNNEL_FILE = ".ghosttunnel";
274
335
  function toGhostTunnelOptions(options = {}) {
@@ -613,6 +674,11 @@ function parseJson(value) {
613
674
  function keyPart(value) {
614
675
  return value.toLowerCase().replace(/[^a-z0-9._:-]/g, "_");
615
676
  }
677
+ function removeTrailingSlashes(value) {
678
+ let end = value.length;
679
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
680
+ return value.slice(0, end);
681
+ }
616
682
  var MemoryGhostTunnelStore = class {
617
683
  routes = /* @__PURE__ */ new Map();
618
684
  queues = /* @__PURE__ */ new Map();
@@ -668,7 +734,7 @@ var RedisGhostTunnelStore = class {
668
734
  namespace;
669
735
  fetchImpl;
670
736
  constructor(options) {
671
- this.url = options.url.replace(/\/+$/, "");
737
+ this.url = removeTrailingSlashes(options.url);
672
738
  this.token = options.token;
673
739
  this.namespace = options.namespace ?? "localghost";
674
740
  this.fetchImpl = options.fetch ?? fetch;
@@ -756,11 +822,11 @@ function isStopped(signal, localSignal) {
756
822
  }
757
823
  function wait(ms, signal, localSignal) {
758
824
  if (isStopped(signal, localSignal)) return Promise.resolve();
759
- return new Promise((resolve3) => {
760
- const timeout = setTimeout(resolve3, ms);
825
+ return new Promise((resolve4) => {
826
+ const timeout = setTimeout(resolve4, ms);
761
827
  const stop = () => {
762
828
  clearTimeout(timeout);
763
- resolve3();
829
+ resolve4();
764
830
  };
765
831
  signal?.addEventListener("abort", stop, { once: true });
766
832
  localSignal.addEventListener("abort", stop, { once: true });
@@ -1572,6 +1638,26 @@ function startCaddy(path) {
1572
1638
  stdio: caddyStdio()
1573
1639
  });
1574
1640
  }
1641
+ function stopCaddyProcesses(pids, killProcess = (pid, signal) => process.kill(pid, signal)) {
1642
+ const result = {
1643
+ stopped: [],
1644
+ alreadyExited: [],
1645
+ failed: []
1646
+ };
1647
+ for (const pid of new Set(pids)) {
1648
+ try {
1649
+ killProcess(pid, "SIGINT");
1650
+ result.stopped.push(pid);
1651
+ } catch (error) {
1652
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
1653
+ result.alreadyExited.push(pid);
1654
+ } else {
1655
+ result.failed.push({ pid, error });
1656
+ }
1657
+ }
1658
+ }
1659
+ return result;
1660
+ }
1575
1661
  async function trustCaddy(path) {
1576
1662
  await execa("caddy", ["trust", "--config", path], {
1577
1663
  cwd: dirname3(path),
@@ -1581,19 +1667,19 @@ async function trustCaddy(path) {
1581
1667
 
1582
1668
  // src/context.ts
1583
1669
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
1584
- import { join as join4 } from "path";
1670
+ import { join as join5 } from "path";
1585
1671
  import { pathToFileURL } from "url";
1586
1672
 
1587
1673
  // src/port.ts
1588
1674
  import { createServer } from "net";
1589
1675
  async function isPortAvailable(port, host = "127.0.0.1") {
1590
- return new Promise((resolve3) => {
1676
+ return new Promise((resolve4) => {
1591
1677
  const server = createServer();
1592
1678
  server.once("error", () => {
1593
- resolve3(false);
1679
+ resolve4(false);
1594
1680
  });
1595
1681
  server.once("listening", () => {
1596
- server.close(() => resolve3(true));
1682
+ server.close(() => resolve4(true));
1597
1683
  });
1598
1684
  server.listen(port, host);
1599
1685
  });
@@ -1610,6 +1696,176 @@ async function findAvailablePort(startPort, options = {}) {
1610
1696
  throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
1611
1697
  }
1612
1698
 
1699
+ // src/registry.ts
1700
+ import { randomUUID as randomUUID3 } from "crypto";
1701
+ import { mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "fs/promises";
1702
+ import { homedir as homedir2 } from "os";
1703
+ import { join as join4, normalize, resolve as resolve2 } from "path";
1704
+ var LOCALGHOST_REGISTRY_FILE = "registry.json";
1705
+ var LOCALGHOST_REGISTRY_LOCK_FILE = "registry.lock";
1706
+ function defaultProcessRunning(pid) {
1707
+ if (pid <= 0) return false;
1708
+ try {
1709
+ process.kill(pid, 0);
1710
+ return true;
1711
+ } catch (error) {
1712
+ return error.code === "EPERM";
1713
+ }
1714
+ }
1715
+ function getLocalghostRegistryRoot(env = process.env) {
1716
+ return resolve2(env.LOCALGHOST_HOME || join4(homedir2(), ".localghost"));
1717
+ }
1718
+ function canonicalizeLocalghostProjectCwd(cwd = process.cwd()) {
1719
+ return normalize(resolve2(cwd));
1720
+ }
1721
+ function emptyRegistry() {
1722
+ return { version: 1, allocations: [], leases: [] };
1723
+ }
1724
+ function leaseKey(projectCwd, instanceKey) {
1725
+ return `${projectCwd}\0${instanceKey}`;
1726
+ }
1727
+ function validRegistry(value) {
1728
+ if (!value || typeof value !== "object") return false;
1729
+ const candidate = value;
1730
+ return candidate.version === 1 && Array.isArray(candidate.allocations) && Array.isArray(candidate.leases);
1731
+ }
1732
+ function pruneRegistry(registry, now, isRunning) {
1733
+ registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
1734
+ }
1735
+ async function readJson(path) {
1736
+ try {
1737
+ return JSON.parse(await readFile(path, "utf8"));
1738
+ } catch (error) {
1739
+ if (error.code === "ENOENT") return void 0;
1740
+ return void 0;
1741
+ }
1742
+ }
1743
+ function createLocalghostRegistry(options = {}) {
1744
+ const root = resolve2(options.stateRoot ?? getLocalghostRegistryRoot());
1745
+ const registryPath = join4(root, LOCALGHOST_REGISTRY_FILE);
1746
+ const lockPath = join4(root, LOCALGHOST_REGISTRY_LOCK_FILE);
1747
+ const cwd = canonicalizeLocalghostProjectCwd(options.cwd);
1748
+ const now = options.now ?? Date.now;
1749
+ const pid = options.pid ?? process.pid;
1750
+ const ownerToken = options.ownerToken ?? randomUUID3();
1751
+ const isRunning = options.isProcessRunning ?? defaultProcessRunning;
1752
+ const availabilityCheck = options.availabilityCheck ?? isPortAvailable;
1753
+ const lockTimeoutMs = options.lockTimeoutMs ?? 5e3;
1754
+ const lockRetryMs = options.lockRetryMs ?? 25;
1755
+ const lockStaleMs = options.lockStaleMs ?? 3e4;
1756
+ async function readRegistry() {
1757
+ const value = await readJson(registryPath);
1758
+ return validRegistry(value) ? value : emptyRegistry();
1759
+ }
1760
+ async function writeRegistry(registry) {
1761
+ await mkdir(root, { recursive: true });
1762
+ const temporaryPath = join4(root, `.registry.${process.pid}.${randomUUID3()}.tmp`);
1763
+ await writeFile(temporaryPath, `${JSON.stringify(registry, null, 2)}
1764
+ `, { mode: 384 });
1765
+ await rename(temporaryPath, registryPath);
1766
+ }
1767
+ async function lock() {
1768
+ await mkdir(root, { recursive: true });
1769
+ const deadline = now() + lockTimeoutMs;
1770
+ const token = randomUUID3();
1771
+ while (true) {
1772
+ try {
1773
+ const handle = await open(lockPath, "wx", 384);
1774
+ await handle.writeFile(`${JSON.stringify({ pid, createdAt: now(), token })}
1775
+ `);
1776
+ await handle.close();
1777
+ return async () => {
1778
+ const current = await readJson(lockPath);
1779
+ if (current?.token === token) await unlink(lockPath).catch(() => void 0);
1780
+ };
1781
+ } catch (error) {
1782
+ if (error.code !== "EEXIST") throw error;
1783
+ const lockInfo = await readJson(lockPath);
1784
+ let stale = false;
1785
+ if (lockInfo && typeof lockInfo.pid === "number") {
1786
+ stale = !isRunning(lockInfo.pid) && now() - lockInfo.createdAt >= 0;
1787
+ } else {
1788
+ try {
1789
+ stale = now() - (await stat(lockPath)).mtimeMs > lockStaleMs;
1790
+ } catch {
1791
+ continue;
1792
+ }
1793
+ }
1794
+ if (stale) {
1795
+ await rm(lockPath, { force: true }).catch(() => void 0);
1796
+ continue;
1797
+ }
1798
+ if (now() >= deadline) throw new Error(`Timed out waiting for Localghost registry lock: ${lockPath}`);
1799
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, lockRetryMs));
1800
+ }
1801
+ }
1802
+ }
1803
+ async function withLock(operation) {
1804
+ const releaseLock = await lock();
1805
+ try {
1806
+ const registry = await readRegistry();
1807
+ pruneRegistry(registry, now(), isRunning);
1808
+ return await operation(registry);
1809
+ } finally {
1810
+ await releaseLock();
1811
+ }
1812
+ }
1813
+ return {
1814
+ root,
1815
+ registryPath,
1816
+ lockPath,
1817
+ ownerToken,
1818
+ read: readRegistry,
1819
+ async acquirePort(acquireOptions) {
1820
+ const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
1821
+ if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
1822
+ return withLock(async (registry) => {
1823
+ const key = leaseKey(projectCwd, acquireOptions.instanceKey);
1824
+ const existing = registry.allocations.find((entry2) => leaseKey(entry2.projectCwd, entry2.instanceKey) === key);
1825
+ const reserved = new Set(acquireOptions.reservedPorts ?? []);
1826
+ const activePorts = new Set(registry.leases.map((lease2) => lease2.port));
1827
+ const port = existing?.port;
1828
+ const ownsActiveLease = registry.leases.some((lease2) => lease2.port === port && leaseKey(lease2.projectCwd, lease2.instanceKey) === key && lease2.ownerToken === ownerToken);
1829
+ const reusable = port !== void 0 && !reserved.has(port) && (!activePorts.has(port) || ownsActiveLease) && (ownsActiveLease || await availabilityCheck(port, acquireOptions.host));
1830
+ let selectedPort = reusable ? port : void 0;
1831
+ if (selectedPort === void 0) {
1832
+ const startPort = acquireOptions.startPort ?? 3e3;
1833
+ const maxAttempts = acquireOptions.maxAttempts ?? 50;
1834
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
1835
+ const candidate = startPort + offset;
1836
+ if (reserved.has(candidate) || activePorts.has(candidate)) continue;
1837
+ if (await availabilityCheck(candidate, acquireOptions.host)) {
1838
+ selectedPort = candidate;
1839
+ break;
1840
+ }
1841
+ }
1842
+ if (selectedPort === void 0) throw new Error(`No available registry port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
1843
+ }
1844
+ const timestamp = now();
1845
+ const entry = existing ?? { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, updatedAt: timestamp };
1846
+ entry.port = selectedPort;
1847
+ entry.updatedAt = timestamp;
1848
+ if (!existing) registry.allocations.push(entry);
1849
+ registry.leases = registry.leases.filter((lease2) => leaseKey(lease2.projectCwd, lease2.instanceKey) !== key);
1850
+ const lease = { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, pid, acquiredAt: timestamp, expiresAt: timestamp + (acquireOptions.leaseTtlMs ?? 30 * 60 * 1e3), ownerToken };
1851
+ registry.leases.push(lease);
1852
+ await writeRegistry(registry);
1853
+ return lease;
1854
+ });
1855
+ },
1856
+ async releasePort(releaseOptions) {
1857
+ const projectCwd = canonicalizeLocalghostProjectCwd(releaseOptions.projectCwd ?? cwd);
1858
+ return withLock(async (registry) => {
1859
+ const key = leaseKey(projectCwd, releaseOptions.instanceKey);
1860
+ const before = registry.leases.length;
1861
+ registry.leases = registry.leases.filter((lease) => leaseKey(lease.projectCwd, lease.instanceKey) !== key || lease.ownerToken !== ownerToken);
1862
+ if (registry.leases.length !== before) await writeRegistry(registry);
1863
+ return registry.leases.length !== before;
1864
+ });
1865
+ }
1866
+ };
1867
+ }
1868
+
1613
1869
  // src/context.ts
1614
1870
  var LOCALGHOST_PROJECT_CONFIG_FILES = [
1615
1871
  "localghost.config.mjs",
@@ -1636,7 +1892,7 @@ function envHttps() {
1636
1892
  }
1637
1893
  function getPackageName(cwd) {
1638
1894
  try {
1639
- const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf8"));
1895
+ const pkg = JSON.parse(readFileSync4(join5(cwd, "package.json"), "utf8"));
1640
1896
  return typeof pkg.name === "string" ? pkg.name : void 0;
1641
1897
  } catch {
1642
1898
  return void 0;
@@ -1722,7 +1978,24 @@ async function resolveLocalghostContext(options = {}) {
1722
1978
  const autoRepair = merged.autoRepair ?? true;
1723
1979
  const bindHost = merged.bindHost ?? "127.0.0.1";
1724
1980
  const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
1725
- const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
1981
+ let port = requestedPort;
1982
+ let releasePort;
1983
+ const reservePort = merged.reservePort ?? false;
1984
+ const instanceKey = merged.instanceKey ?? "run";
1985
+ if (reservePort && dynamicPort) {
1986
+ const registry = createLocalghostRegistry({ cwd, ...merged.registryOwnerToken ? { ownerToken: merged.registryOwnerToken } : {} });
1987
+ const lease = await registry.acquirePort({
1988
+ projectCwd: cwd,
1989
+ instanceKey,
1990
+ startPort: requestedPort,
1991
+ host: probeHost,
1992
+ ...options.reservedPorts ? { reservedPorts: options.reservedPorts } : {}
1993
+ });
1994
+ port = lease.port;
1995
+ releasePort = () => registry.releasePort({ projectCwd: cwd, instanceKey });
1996
+ } else if (dynamicPort) {
1997
+ port = await findAvailablePort(requestedPort, { host: probeHost });
1998
+ }
1726
1999
  const wwwAlias = merged.wwwAlias ?? true;
1727
2000
  const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
1728
2001
  const hosts = uniqueHosts(entries);
@@ -1751,7 +2024,8 @@ async function resolveLocalghostContext(options = {}) {
1751
2024
  https: merged.https ?? envHttps() ?? false,
1752
2025
  wwwAlias,
1753
2026
  ghostTunnel,
1754
- ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
2027
+ ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {},
2028
+ ...releasePort ? { releasePort } : {}
1755
2029
  };
1756
2030
  }
1757
2031
 
@@ -1862,9 +2136,9 @@ async function runDoctor() {
1862
2136
 
1863
2137
  // src/command.ts
1864
2138
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
1865
- import { isAbsolute, join as join5, relative, resolve as resolve2 } from "path";
2139
+ import { isAbsolute, join as join6, relative, resolve as resolve3 } from "path";
1866
2140
  function readPackageJson(cwd) {
1867
- const path = join5(cwd, "package.json");
2141
+ const path = join6(cwd, "package.json");
1868
2142
  if (!existsSync4(path)) {
1869
2143
  throw new Error(`No package.json found in ${cwd}. Pass an explicit command with \`localghost run -- <command>\`.`);
1870
2144
  }
@@ -1879,9 +2153,9 @@ function detectDevPackageManager(cwd, packageManager) {
1879
2153
  const name = packageManager.split("@")[0];
1880
2154
  if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
1881
2155
  }
1882
- if (existsSync4(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
1883
- if (existsSync4(join5(cwd, "yarn.lock"))) return "yarn";
1884
- if (existsSync4(join5(cwd, "bun.lock")) || existsSync4(join5(cwd, "bun.lockb"))) return "bun";
2156
+ if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
2157
+ if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
2158
+ if (existsSync4(join6(cwd, "bun.lock")) || existsSync4(join6(cwd, "bun.lockb"))) return "bun";
1885
2159
  return "npm";
1886
2160
  }
1887
2161
  function scriptCommand(packageManager, script) {
@@ -1916,7 +2190,7 @@ function detectDevCommand(options = {}) {
1916
2190
  };
1917
2191
  }
1918
2192
  throw new Error([
1919
- `Could not detect a safe development command in ${join5(cwd, "package.json")}.`,
2193
+ `Could not detect a safe development command in ${join6(cwd, "package.json")}.`,
1920
2194
  "Add a non-recursive dev or dev:raw script, configure command in localghost.config.mjs,",
1921
2195
  "or pass an explicit command with `localghost run -- <command>`."
1922
2196
  ].join(" "));
@@ -1927,7 +2201,7 @@ function formatDetectedDevCommand(detected) {
1927
2201
  return `${command} (${source})`;
1928
2202
  }
1929
2203
  function assertServicePath(root, serviceCwd, name) {
1930
- const cwd = resolve2(root, serviceCwd);
2204
+ const cwd = resolve3(root, serviceCwd);
1931
2205
  const relativeCwd = relative(root, cwd);
1932
2206
  if (isAbsolute(relativeCwd) || relativeCwd === ".." || relativeCwd.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
1933
2207
  throw new Error(`Service ${name} cwd must stay inside the project root.`);
@@ -1998,7 +2272,7 @@ function getProductionEnvKeys() {
1998
2272
  // src/hosts-file.ts
1999
2273
  import { writeFileSync as writeFileSync3 } from "fs";
2000
2274
  import { tmpdir } from "os";
2001
- import { join as join6 } from "path";
2275
+ import { join as join7 } from "path";
2002
2276
  import { execa as execa3 } from "execa";
2003
2277
  function escapeRegExp(value) {
2004
2278
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -2041,7 +2315,7 @@ function removeManagedBlock(existing, projectName) {
2041
2315
  }
2042
2316
  async function writeSystemHostsFile(hostsPath, next, projectName) {
2043
2317
  const sanitizedProjectName = sanitizeProjectName(projectName);
2044
- const tempPath = join6(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
2318
+ const tempPath = join7(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
2045
2319
  writeFileSync3(tempPath, next, "utf8");
2046
2320
  if (process.env.LOCALGHOST_HOSTS_PATH) {
2047
2321
  writeFileSync3(hostsPath, next, "utf8");
@@ -2079,11 +2353,11 @@ async function removeSystemHosts(projectName) {
2079
2353
 
2080
2354
  // src/init.ts
2081
2355
  import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2082
- import { join as join7 } from "path";
2356
+ import { join as join8 } from "path";
2083
2357
  function detectPackageManager(cwd = process.cwd()) {
2084
- if (existsSync5(join7(cwd, "pnpm-lock.yaml"))) return "pnpm";
2085
- if (existsSync5(join7(cwd, "yarn.lock"))) return "yarn";
2086
- if (existsSync5(join7(cwd, "bun.lock")) || existsSync5(join7(cwd, "bun.lockb"))) return "bun";
2358
+ if (existsSync5(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
2359
+ if (existsSync5(join8(cwd, "yarn.lock"))) return "yarn";
2360
+ if (existsSync5(join8(cwd, "bun.lock")) || existsSync5(join8(cwd, "bun.lockb"))) return "bun";
2087
2361
  return "npm";
2088
2362
  }
2089
2363
  function packageRunCommand(packageManager, script) {
@@ -2163,7 +2437,7 @@ function initLocalghost(options = {}) {
2163
2437
  const apiPort = options.apiPort ?? 8787;
2164
2438
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
2165
2439
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
2166
- const configPath = join7(cwd, configFile);
2440
+ const configPath = join8(cwd, configFile);
2167
2441
  const configExists = existsSync5(configPath);
2168
2442
  if (configExists && !options.force) {
2169
2443
  return {
@@ -2180,7 +2454,7 @@ function initLocalghost(options = {}) {
2180
2454
  };
2181
2455
  }
2182
2456
  writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
2183
- const packageJsonPath = join7(cwd, "package.json");
2457
+ const packageJsonPath = join8(cwd, "package.json");
2184
2458
  const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
2185
2459
  return {
2186
2460
  configPath,
@@ -2262,10 +2536,10 @@ function formatGhostTunnel(config, options = {}) {
2262
2536
 
2263
2537
  // src/state.ts
2264
2538
  import { existsSync as existsSync6 } from "fs";
2265
- import { join as join8 } from "path";
2539
+ import { join as join9 } from "path";
2266
2540
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
2267
2541
  function getLocalghostStatePath(cwd = process.cwd()) {
2268
- return join8(cwd, LOCALGHOST_STATE_FILE);
2542
+ return join9(cwd, LOCALGHOST_STATE_FILE);
2269
2543
  }
2270
2544
  function readLocalghostState(cwd = process.cwd()) {
2271
2545
  const path = getLocalghostStatePath(cwd);
@@ -2413,7 +2687,7 @@ async function waitForTunnelResponse(input) {
2413
2687
  await input.store.cleanup(input.requestId);
2414
2688
  return response;
2415
2689
  }
2416
- await new Promise((resolve3) => setTimeout(resolve3, input.pollIntervalMs));
2690
+ await new Promise((resolve4) => setTimeout(resolve4, input.pollIntervalMs));
2417
2691
  }
2418
2692
  return null;
2419
2693
  }
@@ -2513,10 +2787,10 @@ function createVercelGhostTunnelHandler(options) {
2513
2787
 
2514
2788
  // src/update-check.ts
2515
2789
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2516
- import { homedir as homedir2 } from "os";
2517
- import { dirname as dirname4, join as join9 } from "path";
2790
+ import { homedir as homedir3 } from "os";
2791
+ import { dirname as dirname4, join as join10 } from "path";
2518
2792
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
2519
- var LOCALGHOST_VERSION = "0.1.13";
2793
+ var LOCALGHOST_VERSION = "0.1.15";
2520
2794
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
2521
2795
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
2522
2796
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -2528,8 +2802,8 @@ function isUpdateCheckDisabled(env = process.env) {
2528
2802
  }
2529
2803
  function getUpdateCheckCachePath(env = process.env) {
2530
2804
  if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
2531
- const cacheRoot = env.XDG_CACHE_HOME || join9(homedir2(), ".cache");
2532
- return join9(cacheRoot, "localghost", "update-check.json");
2805
+ const cacheRoot = env.XDG_CACHE_HOME || join10(homedir3(), ".cache");
2806
+ return join10(cacheRoot, "localghost", "update-check.json");
2533
2807
  }
2534
2808
  function readCache(path = getUpdateCheckCachePath()) {
2535
2809
  if (!existsSync7(path)) return null;
@@ -2581,7 +2855,7 @@ function isNewerVersion(candidate, current = LOCALGHOST_VERSION) {
2581
2855
  return Boolean(candidate && compareVersions(candidate, current) > 0);
2582
2856
  }
2583
2857
  async function fetchLatestVersion(packageName, timeoutMs) {
2584
- const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replace("/", "%2f")}` : packageName;
2858
+ const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replaceAll("/", "%2f")}` : packageName;
2585
2859
  const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
2586
2860
  signal: AbortSignal.timeout(timeoutMs),
2587
2861
  headers: {
@@ -2686,9 +2960,12 @@ export {
2686
2960
  DEFAULT_RELAY_LIMITS,
2687
2961
  DEFAULT_RELAY_TARGET_POLICY,
2688
2962
  LOCALGHOST_ACTIVITY_VERSION,
2963
+ LOCALGHOST_AGENT_GUIDE,
2689
2964
  LOCALGHOST_CONFIG_FILE,
2690
2965
  LOCALGHOST_GHOST_TUNNEL_FILE,
2691
2966
  LOCALGHOST_PACKAGE_NAME,
2967
+ LOCALGHOST_REGISTRY_FILE,
2968
+ LOCALGHOST_REGISTRY_LOCK_FILE,
2692
2969
  LOCALGHOST_STATE_FILE,
2693
2970
  LOCALGHOST_VERSION,
2694
2971
  UPDATE_CHECK_CACHE_TTL_MS,
@@ -2699,6 +2976,7 @@ export {
2699
2976
  assertRelayLocalTarget,
2700
2977
  assertSecureGhostTunnelRequest,
2701
2978
  authenticateRelayAgentToken,
2979
+ canonicalizeLocalghostProjectCwd,
2702
2980
  checkCaddy,
2703
2981
  checkForUpdate,
2704
2982
  compareVersions,
@@ -2708,6 +2986,7 @@ export {
2708
2986
  constructGhostTunnelUrl,
2709
2987
  createGhostTunnelQueuedRequest,
2710
2988
  createGhostTunnelRouteHeartbeat,
2989
+ createLocalghostRegistry,
2711
2990
  createMemoryGhostTunnelStore,
2712
2991
  createRedisGhostTunnelStore,
2713
2992
  createRedisGhostTunnelStoreFromEnv,
@@ -2727,6 +3006,7 @@ export {
2727
3006
  formatDetectedDevServices,
2728
3007
  formatDomainRoutes,
2729
3008
  formatGhostTunnel,
3009
+ formatLocalghostAgentGuide,
2730
3010
  formatUpdateMessage,
2731
3011
  getCaddyfilePath,
2732
3012
  getConfigFileCandidates,
@@ -2740,6 +3020,7 @@ export {
2740
3020
  getGhostTunnelPreviewUrl,
2741
3021
  getGhostTunnelWildcardHost,
2742
3022
  getLocalghostActivityPath,
3023
+ getLocalghostRegistryRoot,
2743
3024
  getLocalghostStatePath,
2744
3025
  getProductionEnvKeys,
2745
3026
  getProductionReason,
@@ -2798,6 +3079,7 @@ export {
2798
3079
  signRelayRouteClaim,
2799
3080
  startCaddy,
2800
3081
  startGhostTunnelAgent,
3082
+ stopCaddyProcesses,
2801
3083
  stripRelayForwardHeaders,
2802
3084
  trustCaddy,
2803
3085
  unregisterLocalghostRun,