@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/README.md +18 -2
- package/dist/cli.js +495 -126
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +80 -2
- package/dist/index.js +318 -36
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +218 -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 +29 -2
- package/package.json +1 -1
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
|
|
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
|
|
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((
|
|
760
|
-
const timeout = setTimeout(
|
|
825
|
+
return new Promise((resolve4) => {
|
|
826
|
+
const timeout = setTimeout(resolve4, ms);
|
|
761
827
|
const stop = () => {
|
|
762
828
|
clearTimeout(timeout);
|
|
763
|
-
|
|
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
|
|
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((
|
|
1676
|
+
return new Promise((resolve4) => {
|
|
1591
1677
|
const server = createServer();
|
|
1592
1678
|
server.once("error", () => {
|
|
1593
|
-
|
|
1679
|
+
resolve4(false);
|
|
1594
1680
|
});
|
|
1595
1681
|
server.once("listening", () => {
|
|
1596
|
-
server.close(() =>
|
|
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(
|
|
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
|
-
|
|
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
|
|
2139
|
+
import { isAbsolute, join as join6, relative, resolve as resolve3 } from "path";
|
|
1866
2140
|
function readPackageJson(cwd) {
|
|
1867
|
-
const path =
|
|
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(
|
|
1883
|
-
if (existsSync4(
|
|
1884
|
-
if (existsSync4(
|
|
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 ${
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
2356
|
+
import { join as join8 } from "path";
|
|
2083
2357
|
function detectPackageManager(cwd = process.cwd()) {
|
|
2084
|
-
if (existsSync5(
|
|
2085
|
-
if (existsSync5(
|
|
2086
|
-
if (existsSync5(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
2517
|
-
import { dirname as dirname4, join as
|
|
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.
|
|
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 ||
|
|
2532
|
-
return
|
|
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).
|
|
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,
|