@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/README.md +30 -4
- package/dist/cli.js +657 -146
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +117 -3
- package/dist/index.js +399 -40
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +231 -19
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +3 -1
- package/docs/ghost-tunnel.md +4 -1
- package/docs/localghost.1.md +35 -6
- package/package.json +1 -1
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
|
|
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
|
|
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((
|
|
760
|
-
const timeout = setTimeout(
|
|
827
|
+
return new Promise((resolve4) => {
|
|
828
|
+
const timeout = setTimeout(resolve4, ms);
|
|
761
829
|
const stop = () => {
|
|
762
830
|
clearTimeout(timeout);
|
|
763
|
-
|
|
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
|
|
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((
|
|
1679
|
+
return new Promise((resolve4) => {
|
|
1591
1680
|
const server = createServer();
|
|
1592
1681
|
server.once("error", () => {
|
|
1593
|
-
|
|
1682
|
+
resolve4(false);
|
|
1594
1683
|
});
|
|
1595
1684
|
server.once("listening", () => {
|
|
1596
|
-
server.close(() =>
|
|
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(
|
|
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
|
-
|
|
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
|
|
2195
|
+
import { isAbsolute, join as join6, relative, resolve as resolve3 } from "path";
|
|
1866
2196
|
function readPackageJson(cwd) {
|
|
1867
|
-
const path =
|
|
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(
|
|
1883
|
-
if (existsSync4(
|
|
1884
|
-
if (existsSync4(
|
|
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 ${
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
2412
|
+
import { join as join8 } from "path";
|
|
2083
2413
|
function detectPackageManager(cwd = process.cwd()) {
|
|
2084
|
-
if (existsSync5(
|
|
2085
|
-
if (existsSync5(
|
|
2086
|
-
if (existsSync5(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
2517
|
-
import { dirname as dirname4, join as
|
|
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.
|
|
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 ||
|
|
2532
|
-
return
|
|
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).
|
|
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,
|