@hamedb89/localghost 0.1.13 → 0.2.0

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
@@ -80,6 +80,7 @@ function registerLocalghostRun(input, path = getLocalghostActivityPath()) {
80
80
  ...input.configPath ? { configPath: input.configPath } : {},
81
81
  ...input.caddyfilePath ? { caddyfilePath: input.caddyfilePath } : {},
82
82
  ...input.caddyPid ? { caddyPid: input.caddyPid } : {},
83
+ ...input.caddyPgid ? { caddyPgid: input.caddyPgid } : {},
83
84
  ...input.childPid ? { childPid: input.childPid } : {},
84
85
  ...input.childCommand ? { childCommand: input.childCommand } : {},
85
86
  ...typeof input.https === "boolean" ? { https: input.https } : {},
@@ -265,10 +266,72 @@ function getProjectName(cwd = process.cwd()) {
265
266
  }
266
267
  }
267
268
  function sanitizeProjectName(value) {
268
- const projectName = value.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
269
+ const sanitized = value.replace(/[^\w.-]+/g, "-");
270
+ let start = 0;
271
+ let end = sanitized.length;
272
+ while (start < end && sanitized.charCodeAt(start) === 45) start += 1;
273
+ while (end > start && sanitized.charCodeAt(end - 1) === 45) end -= 1;
274
+ const projectName = sanitized.slice(start, end);
269
275
  return projectName || "app";
270
276
  }
271
277
 
278
+ // src/guide.ts
279
+ var LOCALGHOST_AGENT_GUIDE = `# Localghost agent guide
280
+
281
+ Localghost owns the local development proxy and the app process boundary.
282
+
283
+ ## Preferred repository setup
284
+
285
+ For a normal repository, use this package script:
286
+
287
+ "dev": "localghost"
288
+
289
+ For an explicit app command, keep the raw command separate:
290
+
291
+ "dev": "localghost run -- vite"
292
+ "dev:raw": "vite"
293
+
294
+ Use \`localghost dev\` only when the Caddy proxy should run without starting the app.
295
+
296
+ ## Useful commands
297
+
298
+ - \`localghost\`: detect and run the repository development command.
299
+ - \`localghost run -- <command>\`: wrap an explicit app command.
300
+ - \`localghost dev\`: run only the local Caddy proxy.
301
+ - \`localghost status --ready\`: check project setup.
302
+ - \`localghost repair\`: repair managed hosts and Caddy setup.
303
+ - \`localghost ps --json\`: inspect Localghost-managed repositories, instances, and ports.
304
+ - \`localghost routes\`: inspect hostname-to-port routing.
305
+ - \`localghost doctor\`: check machine prerequisites, ports, and registry state.
306
+ - \`localghost repair --reallocate-port\`: move an occupied project port to a stable available port.
307
+
308
+ ## Configuration
309
+
310
+ - Commit repository defaults in \`localghost.config.mjs\`.
311
+ - Keep hostname and requested-port routes in \`.localghost\`.
312
+ - CLI flags override repository configuration for one invocation.
313
+ - Localghost remembers active project and instance port assignments in user state under \`~/.localghost\`.
314
+ - Do not edit the registry manually and do not start Caddy separately.
315
+
316
+ ## Port behavior
317
+
318
+ 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.
319
+ `;
320
+ function formatLocalghostAgentGuide(format = "text") {
321
+ if (format === "json") {
322
+ return JSON.stringify({
323
+ preferredScript: "localghost",
324
+ explicitScript: "localghost run -- <command>",
325
+ proxyOnlyCommand: "localghost dev",
326
+ inspectionCommands: ["localghost status --ready", "localghost ps --json", "localghost routes", "localghost doctor"],
327
+ projectConfig: "localghost.config.mjs",
328
+ routeConfig: ".localghost",
329
+ userState: "~/.localghost"
330
+ }, null, 2);
331
+ }
332
+ return LOCALGHOST_AGENT_GUIDE;
333
+ }
334
+
272
335
  // src/ghost-file.ts
273
336
  var LOCALGHOST_GHOST_TUNNEL_FILE = ".ghosttunnel";
274
337
  function toGhostTunnelOptions(options = {}) {
@@ -613,6 +676,11 @@ function parseJson(value) {
613
676
  function keyPart(value) {
614
677
  return value.toLowerCase().replace(/[^a-z0-9._:-]/g, "_");
615
678
  }
679
+ function removeTrailingSlashes(value) {
680
+ let end = value.length;
681
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
682
+ return value.slice(0, end);
683
+ }
616
684
  var MemoryGhostTunnelStore = class {
617
685
  routes = /* @__PURE__ */ new Map();
618
686
  queues = /* @__PURE__ */ new Map();
@@ -668,7 +736,7 @@ var RedisGhostTunnelStore = class {
668
736
  namespace;
669
737
  fetchImpl;
670
738
  constructor(options) {
671
- this.url = options.url.replace(/\/+$/, "");
739
+ this.url = removeTrailingSlashes(options.url);
672
740
  this.token = options.token;
673
741
  this.namespace = options.namespace ?? "localghost";
674
742
  this.fetchImpl = options.fetch ?? fetch;
@@ -756,11 +824,11 @@ function isStopped(signal, localSignal) {
756
824
  }
757
825
  function wait(ms, signal, localSignal) {
758
826
  if (isStopped(signal, localSignal)) return Promise.resolve();
759
- return new Promise((resolve3) => {
760
- const timeout = setTimeout(resolve3, ms);
827
+ return new Promise((resolve4) => {
828
+ const timeout = setTimeout(resolve4, ms);
761
829
  const stop = () => {
762
830
  clearTimeout(timeout);
763
- resolve3();
831
+ resolve4();
764
832
  };
765
833
  signal?.addEventListener("abort", stop, { once: true });
766
834
  localSignal.addEventListener("abort", stop, { once: true });
@@ -1569,9 +1637,30 @@ async function runCaddy(path) {
1569
1637
  function startCaddy(path) {
1570
1638
  return execa("caddy", ["run", "--config", path], {
1571
1639
  cwd: dirname3(path),
1572
- stdio: caddyStdio()
1640
+ stdio: caddyStdio(),
1641
+ detached: process.platform !== "win32"
1573
1642
  });
1574
1643
  }
1644
+ function stopCaddyProcesses(pids, killProcess = (pid, signal) => process.kill(pid, signal)) {
1645
+ const result = {
1646
+ stopped: [],
1647
+ alreadyExited: [],
1648
+ failed: []
1649
+ };
1650
+ for (const pid of new Set(pids)) {
1651
+ try {
1652
+ killProcess(pid, "SIGINT");
1653
+ result.stopped.push(pid);
1654
+ } catch (error) {
1655
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
1656
+ result.alreadyExited.push(pid);
1657
+ } else {
1658
+ result.failed.push({ pid, error });
1659
+ }
1660
+ }
1661
+ }
1662
+ return result;
1663
+ }
1575
1664
  async function trustCaddy(path) {
1576
1665
  await execa("caddy", ["trust", "--config", path], {
1577
1666
  cwd: dirname3(path),
@@ -1581,19 +1670,19 @@ async function trustCaddy(path) {
1581
1670
 
1582
1671
  // src/context.ts
1583
1672
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
1584
- import { join as join4 } from "path";
1673
+ import { join as join5 } from "path";
1585
1674
  import { pathToFileURL } from "url";
1586
1675
 
1587
1676
  // src/port.ts
1588
1677
  import { createServer } from "net";
1589
1678
  async function isPortAvailable(port, host = "127.0.0.1") {
1590
- return new Promise((resolve3) => {
1679
+ return new Promise((resolve4) => {
1591
1680
  const server = createServer();
1592
1681
  server.once("error", () => {
1593
- resolve3(false);
1682
+ resolve4(false);
1594
1683
  });
1595
1684
  server.once("listening", () => {
1596
- server.close(() => resolve3(true));
1685
+ server.close(() => resolve4(true));
1597
1686
  });
1598
1687
  server.listen(port, host);
1599
1688
  });
@@ -1610,6 +1699,188 @@ async function findAvailablePort(startPort, options = {}) {
1610
1699
  throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
1611
1700
  }
1612
1701
 
1702
+ // src/registry.ts
1703
+ import { randomUUID as randomUUID3 } from "crypto";
1704
+ import { mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "fs/promises";
1705
+ import { homedir as homedir2 } from "os";
1706
+ import { join as join4, normalize, resolve as resolve2 } from "path";
1707
+ var LOCALGHOST_REGISTRY_FILE = "registry.json";
1708
+ var LOCALGHOST_REGISTRY_LOCK_FILE = "registry.lock";
1709
+ function defaultProcessRunning(pid) {
1710
+ if (pid <= 0) return false;
1711
+ try {
1712
+ process.kill(pid, 0);
1713
+ return true;
1714
+ } catch (error) {
1715
+ return error.code === "EPERM";
1716
+ }
1717
+ }
1718
+ function getLocalghostRegistryRoot(env = process.env) {
1719
+ return resolve2(env.LOCALGHOST_HOME || join4(homedir2(), ".localghost"));
1720
+ }
1721
+ function canonicalizeLocalghostProjectCwd(cwd = process.cwd()) {
1722
+ return normalize(resolve2(cwd));
1723
+ }
1724
+ function emptyRegistry() {
1725
+ return { version: 1, allocations: [], leases: [] };
1726
+ }
1727
+ function leaseKey(projectCwd, instanceKey) {
1728
+ return `${projectCwd}\0${instanceKey}`;
1729
+ }
1730
+ function validRegistry(value) {
1731
+ if (!value || typeof value !== "object") return false;
1732
+ const candidate = value;
1733
+ return candidate.version === 1 && Array.isArray(candidate.allocations) && Array.isArray(candidate.leases);
1734
+ }
1735
+ function pruneRegistry(registry, now, isRunning) {
1736
+ registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
1737
+ }
1738
+ async function readJson(path) {
1739
+ try {
1740
+ return JSON.parse(await readFile(path, "utf8"));
1741
+ } catch (error) {
1742
+ if (error.code === "ENOENT") return void 0;
1743
+ return void 0;
1744
+ }
1745
+ }
1746
+ function createLocalghostRegistry(options = {}) {
1747
+ const root = resolve2(options.stateRoot ?? getLocalghostRegistryRoot());
1748
+ const registryPath = join4(root, LOCALGHOST_REGISTRY_FILE);
1749
+ const lockPath = join4(root, LOCALGHOST_REGISTRY_LOCK_FILE);
1750
+ const cwd = canonicalizeLocalghostProjectCwd(options.cwd);
1751
+ const now = options.now ?? Date.now;
1752
+ const pid = options.pid ?? process.pid;
1753
+ const ownerToken = options.ownerToken ?? randomUUID3();
1754
+ const isRunning = options.isProcessRunning ?? defaultProcessRunning;
1755
+ const availabilityCheck = options.availabilityCheck ?? isPortAvailable;
1756
+ const lockTimeoutMs = options.lockTimeoutMs ?? 5e3;
1757
+ const lockRetryMs = options.lockRetryMs ?? 25;
1758
+ const lockStaleMs = options.lockStaleMs ?? 3e4;
1759
+ async function readRegistry() {
1760
+ const value = await readJson(registryPath);
1761
+ return validRegistry(value) ? value : emptyRegistry();
1762
+ }
1763
+ async function writeRegistry(registry) {
1764
+ await mkdir(root, { recursive: true });
1765
+ const temporaryPath = join4(root, `.registry.${process.pid}.${randomUUID3()}.tmp`);
1766
+ await writeFile(temporaryPath, `${JSON.stringify(registry, null, 2)}
1767
+ `, { mode: 384 });
1768
+ await rename(temporaryPath, registryPath);
1769
+ }
1770
+ async function lock() {
1771
+ await mkdir(root, { recursive: true });
1772
+ const deadline = now() + lockTimeoutMs;
1773
+ const token = randomUUID3();
1774
+ while (true) {
1775
+ try {
1776
+ const handle = await open(lockPath, "wx", 384);
1777
+ await handle.writeFile(`${JSON.stringify({ pid, createdAt: now(), token })}
1778
+ `);
1779
+ await handle.close();
1780
+ return async () => {
1781
+ const current = await readJson(lockPath);
1782
+ if (current?.token === token) await unlink(lockPath).catch(() => void 0);
1783
+ };
1784
+ } catch (error) {
1785
+ if (error.code !== "EEXIST") throw error;
1786
+ const lockInfo = await readJson(lockPath);
1787
+ let stale = false;
1788
+ if (lockInfo && typeof lockInfo.pid === "number") {
1789
+ stale = !isRunning(lockInfo.pid) && now() - lockInfo.createdAt >= 0;
1790
+ } else {
1791
+ try {
1792
+ stale = now() - (await stat(lockPath)).mtimeMs > lockStaleMs;
1793
+ } catch {
1794
+ continue;
1795
+ }
1796
+ }
1797
+ if (stale) {
1798
+ await rm(lockPath, { force: true }).catch(() => void 0);
1799
+ continue;
1800
+ }
1801
+ if (now() >= deadline) throw new Error(`Timed out waiting for Localghost registry lock: ${lockPath}`);
1802
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, lockRetryMs));
1803
+ }
1804
+ }
1805
+ }
1806
+ async function withLock(operation) {
1807
+ const releaseLock = await lock();
1808
+ try {
1809
+ const registry = await readRegistry();
1810
+ pruneRegistry(registry, now(), isRunning);
1811
+ return await operation(registry);
1812
+ } finally {
1813
+ await releaseLock();
1814
+ }
1815
+ }
1816
+ return {
1817
+ root,
1818
+ registryPath,
1819
+ lockPath,
1820
+ ownerToken,
1821
+ read: readRegistry,
1822
+ async prune() {
1823
+ const releaseLock = await lock();
1824
+ try {
1825
+ const registry = await readRegistry();
1826
+ const before = registry.leases.length;
1827
+ pruneRegistry(registry, now(), isRunning);
1828
+ await writeRegistry(registry);
1829
+ return { removedLeases: before - registry.leases.length };
1830
+ } finally {
1831
+ await releaseLock();
1832
+ }
1833
+ },
1834
+ async acquirePort(acquireOptions) {
1835
+ const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
1836
+ if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
1837
+ return withLock(async (registry) => {
1838
+ const key = leaseKey(projectCwd, acquireOptions.instanceKey);
1839
+ const existing = registry.allocations.find((entry2) => leaseKey(entry2.projectCwd, entry2.instanceKey) === key);
1840
+ const reserved = new Set(acquireOptions.reservedPorts ?? []);
1841
+ const activePorts = new Set(registry.leases.map((lease2) => lease2.port));
1842
+ const port = existing?.port;
1843
+ const ownsActiveLease = registry.leases.some((lease2) => lease2.port === port && leaseKey(lease2.projectCwd, lease2.instanceKey) === key && lease2.ownerToken === ownerToken);
1844
+ const reusable = port !== void 0 && !reserved.has(port) && (!activePorts.has(port) || ownsActiveLease) && (ownsActiveLease || await availabilityCheck(port, acquireOptions.host));
1845
+ let selectedPort = reusable ? port : void 0;
1846
+ if (selectedPort === void 0) {
1847
+ const startPort = acquireOptions.startPort ?? 3e3;
1848
+ const maxAttempts = acquireOptions.maxAttempts ?? 50;
1849
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
1850
+ const candidate = startPort + offset;
1851
+ if (reserved.has(candidate) || activePorts.has(candidate)) continue;
1852
+ if (await availabilityCheck(candidate, acquireOptions.host)) {
1853
+ selectedPort = candidate;
1854
+ break;
1855
+ }
1856
+ }
1857
+ if (selectedPort === void 0) throw new Error(`No available registry port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
1858
+ }
1859
+ const timestamp = now();
1860
+ const entry = existing ?? { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, updatedAt: timestamp };
1861
+ entry.port = selectedPort;
1862
+ entry.updatedAt = timestamp;
1863
+ if (!existing) registry.allocations.push(entry);
1864
+ registry.leases = registry.leases.filter((lease2) => leaseKey(lease2.projectCwd, lease2.instanceKey) !== key);
1865
+ const lease = { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, pid, acquiredAt: timestamp, expiresAt: timestamp + (acquireOptions.leaseTtlMs ?? 30 * 60 * 1e3), ownerToken };
1866
+ registry.leases.push(lease);
1867
+ await writeRegistry(registry);
1868
+ return lease;
1869
+ });
1870
+ },
1871
+ async releasePort(releaseOptions) {
1872
+ const projectCwd = canonicalizeLocalghostProjectCwd(releaseOptions.projectCwd ?? cwd);
1873
+ return withLock(async (registry) => {
1874
+ const key = leaseKey(projectCwd, releaseOptions.instanceKey);
1875
+ const before = registry.leases.length;
1876
+ registry.leases = registry.leases.filter((lease) => leaseKey(lease.projectCwd, lease.instanceKey) !== key || lease.ownerToken !== ownerToken);
1877
+ if (registry.leases.length !== before) await writeRegistry(registry);
1878
+ return registry.leases.length !== before;
1879
+ });
1880
+ }
1881
+ };
1882
+ }
1883
+
1613
1884
  // src/context.ts
1614
1885
  var LOCALGHOST_PROJECT_CONFIG_FILES = [
1615
1886
  "localghost.config.mjs",
@@ -1636,7 +1907,7 @@ function envHttps() {
1636
1907
  }
1637
1908
  function getPackageName(cwd) {
1638
1909
  try {
1639
- const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf8"));
1910
+ const pkg = JSON.parse(readFileSync4(join5(cwd, "package.json"), "utf8"));
1640
1911
  return typeof pkg.name === "string" ? pkg.name : void 0;
1641
1912
  } catch {
1642
1913
  return void 0;
@@ -1722,7 +1993,24 @@ async function resolveLocalghostContext(options = {}) {
1722
1993
  const autoRepair = merged.autoRepair ?? true;
1723
1994
  const bindHost = merged.bindHost ?? "127.0.0.1";
1724
1995
  const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
1725
- const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
1996
+ let port = requestedPort;
1997
+ let releasePort;
1998
+ const reservePort = merged.reservePort ?? false;
1999
+ const instanceKey = merged.instanceKey ?? "run";
2000
+ if (reservePort && dynamicPort) {
2001
+ const registry = createLocalghostRegistry({ cwd, ...merged.registryOwnerToken ? { ownerToken: merged.registryOwnerToken } : {} });
2002
+ const lease = await registry.acquirePort({
2003
+ projectCwd: cwd,
2004
+ instanceKey,
2005
+ startPort: requestedPort,
2006
+ host: probeHost,
2007
+ ...options.reservedPorts ? { reservedPorts: options.reservedPorts } : {}
2008
+ });
2009
+ port = lease.port;
2010
+ releasePort = () => registry.releasePort({ projectCwd: cwd, instanceKey });
2011
+ } else if (dynamicPort) {
2012
+ port = await findAvailablePort(requestedPort, { host: probeHost });
2013
+ }
1726
2014
  const wwwAlias = merged.wwwAlias ?? true;
1727
2015
  const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
1728
2016
  const hosts = uniqueHosts(entries);
@@ -1751,7 +2039,8 @@ async function resolveLocalghostContext(options = {}) {
1751
2039
  https: merged.https ?? envHttps() ?? false,
1752
2040
  wwwAlias,
1753
2041
  ghostTunnel,
1754
- ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
2042
+ ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {},
2043
+ ...releasePort ? { releasePort } : {}
1755
2044
  };
1756
2045
  }
1757
2046
 
@@ -1852,19 +2141,60 @@ async function checkCaddy() {
1852
2141
  };
1853
2142
  }
1854
2143
  }
1855
- async function runDoctor() {
2144
+ async function runDoctor(options = {}) {
1856
2145
  const caddy = await checkCaddy();
2146
+ const cwd = options.cwd ?? process.cwd();
2147
+ const registry = createLocalghostRegistry({ cwd });
2148
+ const data = await registry.read();
2149
+ const now = Date.now();
2150
+ const staleLeases = data.leases.filter((lease) => lease.expiresAt <= now || !isProcessRunning(lease.pid)).map(({ projectCwd, instanceKey, port, pid }) => ({ projectCwd, instanceKey, port, pid }));
2151
+ const allocationsByPort = /* @__PURE__ */ new Map();
2152
+ for (const allocation of data.allocations) {
2153
+ const projects = allocationsByPort.get(allocation.port) ?? [];
2154
+ projects.push(`${allocation.projectCwd}#${allocation.instanceKey}`);
2155
+ allocationsByPort.set(allocation.port, projects);
2156
+ }
2157
+ const duplicateAllocations = [...allocationsByPort.entries()].filter(([, projects]) => projects.length > 1).map(([port, projects]) => ({ port, projects }));
2158
+ let configured;
2159
+ let available;
2160
+ try {
2161
+ const context = await resolveLocalghostContext({
2162
+ cwd,
2163
+ ...options.configFiles ? { configFiles: options.configFiles } : {},
2164
+ ...options.configPattern ? { configPattern: options.configPattern } : {},
2165
+ dynamicPort: false
2166
+ });
2167
+ configured = context.requestedPort;
2168
+ available = await isPortAvailable(configured);
2169
+ } catch {
2170
+ }
2171
+ const currentProjectCwd = canonicalizeLocalghostProjectCwd(cwd);
2172
+ const currentAllocation = data.allocations.find((allocation) => allocation.projectCwd === currentProjectCwd);
1857
2173
  return {
1858
- ok: caddy.found,
1859
- caddy
2174
+ ok: caddy.found && available !== false && staleLeases.length === 0 && duplicateAllocations.length === 0,
2175
+ caddy,
2176
+ ports: {
2177
+ ...configured !== void 0 ? { configured } : {},
2178
+ ...available !== void 0 ? { available } : {},
2179
+ registryPath: registry.registryPath,
2180
+ staleLeases,
2181
+ duplicateAllocations,
2182
+ ...currentAllocation ? {
2183
+ currentAllocation: {
2184
+ projectCwd: currentAllocation.projectCwd,
2185
+ instanceKey: currentAllocation.instanceKey,
2186
+ port: currentAllocation.port
2187
+ }
2188
+ } : {}
2189
+ }
1860
2190
  };
1861
2191
  }
1862
2192
 
1863
2193
  // src/command.ts
1864
2194
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
1865
- import { isAbsolute, join as join5, relative, resolve as resolve2 } from "path";
2195
+ import { isAbsolute, join as join6, relative, resolve as resolve3 } from "path";
1866
2196
  function readPackageJson(cwd) {
1867
- const path = join5(cwd, "package.json");
2197
+ const path = join6(cwd, "package.json");
1868
2198
  if (!existsSync4(path)) {
1869
2199
  throw new Error(`No package.json found in ${cwd}. Pass an explicit command with \`localghost run -- <command>\`.`);
1870
2200
  }
@@ -1879,9 +2209,9 @@ function detectDevPackageManager(cwd, packageManager) {
1879
2209
  const name = packageManager.split("@")[0];
1880
2210
  if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
1881
2211
  }
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";
2212
+ if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
2213
+ if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
2214
+ if (existsSync4(join6(cwd, "bun.lock")) || existsSync4(join6(cwd, "bun.lockb"))) return "bun";
1885
2215
  return "npm";
1886
2216
  }
1887
2217
  function scriptCommand(packageManager, script) {
@@ -1916,7 +2246,7 @@ function detectDevCommand(options = {}) {
1916
2246
  };
1917
2247
  }
1918
2248
  throw new Error([
1919
- `Could not detect a safe development command in ${join5(cwd, "package.json")}.`,
2249
+ `Could not detect a safe development command in ${join6(cwd, "package.json")}.`,
1920
2250
  "Add a non-recursive dev or dev:raw script, configure command in localghost.config.mjs,",
1921
2251
  "or pass an explicit command with `localghost run -- <command>`."
1922
2252
  ].join(" "));
@@ -1927,7 +2257,7 @@ function formatDetectedDevCommand(detected) {
1927
2257
  return `${command} (${source})`;
1928
2258
  }
1929
2259
  function assertServicePath(root, serviceCwd, name) {
1930
- const cwd = resolve2(root, serviceCwd);
2260
+ const cwd = resolve3(root, serviceCwd);
1931
2261
  const relativeCwd = relative(root, cwd);
1932
2262
  if (isAbsolute(relativeCwd) || relativeCwd === ".." || relativeCwd.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
1933
2263
  throw new Error(`Service ${name} cwd must stay inside the project root.`);
@@ -1998,7 +2328,7 @@ function getProductionEnvKeys() {
1998
2328
  // src/hosts-file.ts
1999
2329
  import { writeFileSync as writeFileSync3 } from "fs";
2000
2330
  import { tmpdir } from "os";
2001
- import { join as join6 } from "path";
2331
+ import { join as join7 } from "path";
2002
2332
  import { execa as execa3 } from "execa";
2003
2333
  function escapeRegExp(value) {
2004
2334
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -2041,7 +2371,7 @@ function removeManagedBlock(existing, projectName) {
2041
2371
  }
2042
2372
  async function writeSystemHostsFile(hostsPath, next, projectName) {
2043
2373
  const sanitizedProjectName = sanitizeProjectName(projectName);
2044
- const tempPath = join6(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
2374
+ const tempPath = join7(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
2045
2375
  writeFileSync3(tempPath, next, "utf8");
2046
2376
  if (process.env.LOCALGHOST_HOSTS_PATH) {
2047
2377
  writeFileSync3(hostsPath, next, "utf8");
@@ -2079,11 +2409,11 @@ async function removeSystemHosts(projectName) {
2079
2409
 
2080
2410
  // src/init.ts
2081
2411
  import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2082
- import { join as join7 } from "path";
2412
+ import { join as join8 } from "path";
2083
2413
  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";
2414
+ if (existsSync5(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
2415
+ if (existsSync5(join8(cwd, "yarn.lock"))) return "yarn";
2416
+ if (existsSync5(join8(cwd, "bun.lock")) || existsSync5(join8(cwd, "bun.lockb"))) return "bun";
2087
2417
  return "npm";
2088
2418
  }
2089
2419
  function packageRunCommand(packageManager, script) {
@@ -2163,7 +2493,7 @@ function initLocalghost(options = {}) {
2163
2493
  const apiPort = options.apiPort ?? 8787;
2164
2494
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
2165
2495
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
2166
- const configPath = join7(cwd, configFile);
2496
+ const configPath = join8(cwd, configFile);
2167
2497
  const configExists = existsSync5(configPath);
2168
2498
  if (configExists && !options.force) {
2169
2499
  return {
@@ -2180,7 +2510,7 @@ function initLocalghost(options = {}) {
2180
2510
  };
2181
2511
  }
2182
2512
  writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
2183
- const packageJsonPath = join7(cwd, "package.json");
2513
+ const packageJsonPath = join8(cwd, "package.json");
2184
2514
  const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
2185
2515
  return {
2186
2516
  configPath,
@@ -2197,6 +2527,25 @@ function initLocalghost(options = {}) {
2197
2527
  };
2198
2528
  }
2199
2529
 
2530
+ // src/process.ts
2531
+ function signalManagedProcessPid(pid, signal, killProcess = (value, processSignal) => process.kill(value, processSignal)) {
2532
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid < 1) return false;
2533
+ try {
2534
+ killProcess(process.platform === "win32" ? pid : -pid, signal);
2535
+ return true;
2536
+ } catch (error) {
2537
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") return false;
2538
+ throw error;
2539
+ }
2540
+ }
2541
+ function signalManagedProcess(child, signal) {
2542
+ if (process.platform === "win32") {
2543
+ if (!child.killed) child.kill(signal);
2544
+ return true;
2545
+ }
2546
+ return signalManagedProcessPid(child.pid, signal);
2547
+ }
2548
+
2200
2549
  // src/routes.ts
2201
2550
  var ansi = {
2202
2551
  cyan: "\x1B[36m",
@@ -2262,10 +2611,10 @@ function formatGhostTunnel(config, options = {}) {
2262
2611
 
2263
2612
  // src/state.ts
2264
2613
  import { existsSync as existsSync6 } from "fs";
2265
- import { join as join8 } from "path";
2614
+ import { join as join9 } from "path";
2266
2615
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
2267
2616
  function getLocalghostStatePath(cwd = process.cwd()) {
2268
- return join8(cwd, LOCALGHOST_STATE_FILE);
2617
+ return join9(cwd, LOCALGHOST_STATE_FILE);
2269
2618
  }
2270
2619
  function readLocalghostState(cwd = process.cwd()) {
2271
2620
  const path = getLocalghostStatePath(cwd);
@@ -2413,7 +2762,7 @@ async function waitForTunnelResponse(input) {
2413
2762
  await input.store.cleanup(input.requestId);
2414
2763
  return response;
2415
2764
  }
2416
- await new Promise((resolve3) => setTimeout(resolve3, input.pollIntervalMs));
2765
+ await new Promise((resolve4) => setTimeout(resolve4, input.pollIntervalMs));
2417
2766
  }
2418
2767
  return null;
2419
2768
  }
@@ -2513,10 +2862,10 @@ function createVercelGhostTunnelHandler(options) {
2513
2862
 
2514
2863
  // src/update-check.ts
2515
2864
  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";
2865
+ import { homedir as homedir3 } from "os";
2866
+ import { dirname as dirname4, join as join10 } from "path";
2518
2867
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
2519
- var LOCALGHOST_VERSION = "0.1.13";
2868
+ var LOCALGHOST_VERSION = "0.2.0";
2520
2869
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
2521
2870
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
2522
2871
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -2528,8 +2877,8 @@ function isUpdateCheckDisabled(env = process.env) {
2528
2877
  }
2529
2878
  function getUpdateCheckCachePath(env = process.env) {
2530
2879
  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");
2880
+ const cacheRoot = env.XDG_CACHE_HOME || join10(homedir3(), ".cache");
2881
+ return join10(cacheRoot, "localghost", "update-check.json");
2533
2882
  }
2534
2883
  function readCache(path = getUpdateCheckCachePath()) {
2535
2884
  if (!existsSync7(path)) return null;
@@ -2581,7 +2930,7 @@ function isNewerVersion(candidate, current = LOCALGHOST_VERSION) {
2581
2930
  return Boolean(candidate && compareVersions(candidate, current) > 0);
2582
2931
  }
2583
2932
  async function fetchLatestVersion(packageName, timeoutMs) {
2584
- const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replace("/", "%2f")}` : packageName;
2933
+ const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replaceAll("/", "%2f")}` : packageName;
2585
2934
  const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
2586
2935
  signal: AbortSignal.timeout(timeoutMs),
2587
2936
  headers: {
@@ -2686,9 +3035,12 @@ export {
2686
3035
  DEFAULT_RELAY_LIMITS,
2687
3036
  DEFAULT_RELAY_TARGET_POLICY,
2688
3037
  LOCALGHOST_ACTIVITY_VERSION,
3038
+ LOCALGHOST_AGENT_GUIDE,
2689
3039
  LOCALGHOST_CONFIG_FILE,
2690
3040
  LOCALGHOST_GHOST_TUNNEL_FILE,
2691
3041
  LOCALGHOST_PACKAGE_NAME,
3042
+ LOCALGHOST_REGISTRY_FILE,
3043
+ LOCALGHOST_REGISTRY_LOCK_FILE,
2692
3044
  LOCALGHOST_STATE_FILE,
2693
3045
  LOCALGHOST_VERSION,
2694
3046
  UPDATE_CHECK_CACHE_TTL_MS,
@@ -2699,6 +3051,7 @@ export {
2699
3051
  assertRelayLocalTarget,
2700
3052
  assertSecureGhostTunnelRequest,
2701
3053
  authenticateRelayAgentToken,
3054
+ canonicalizeLocalghostProjectCwd,
2702
3055
  checkCaddy,
2703
3056
  checkForUpdate,
2704
3057
  compareVersions,
@@ -2708,6 +3061,7 @@ export {
2708
3061
  constructGhostTunnelUrl,
2709
3062
  createGhostTunnelQueuedRequest,
2710
3063
  createGhostTunnelRouteHeartbeat,
3064
+ createLocalghostRegistry,
2711
3065
  createMemoryGhostTunnelStore,
2712
3066
  createRedisGhostTunnelStore,
2713
3067
  createRedisGhostTunnelStoreFromEnv,
@@ -2727,6 +3081,7 @@ export {
2727
3081
  formatDetectedDevServices,
2728
3082
  formatDomainRoutes,
2729
3083
  formatGhostTunnel,
3084
+ formatLocalghostAgentGuide,
2730
3085
  formatUpdateMessage,
2731
3086
  getCaddyfilePath,
2732
3087
  getConfigFileCandidates,
@@ -2740,6 +3095,7 @@ export {
2740
3095
  getGhostTunnelPreviewUrl,
2741
3096
  getGhostTunnelWildcardHost,
2742
3097
  getLocalghostActivityPath,
3098
+ getLocalghostRegistryRoot,
2743
3099
  getLocalghostStatePath,
2744
3100
  getProductionEnvKeys,
2745
3101
  getProductionReason,
@@ -2796,8 +3152,11 @@ export {
2796
3152
  shouldNotifyAboutUpdate,
2797
3153
  signGhostTunnelIpTransportClaim,
2798
3154
  signRelayRouteClaim,
3155
+ signalManagedProcess,
3156
+ signalManagedProcessPid,
2799
3157
  startCaddy,
2800
3158
  startGhostTunnelAgent,
3159
+ stopCaddyProcesses,
2801
3160
  stripRelayForwardHeaders,
2802
3161
  trustCaddy,
2803
3162
  unregisterLocalghostRun,