@hamedb89/localghost 0.1.12 → 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 +31 -13
- package/dist/cli.js +499 -128
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +81 -3
- package/dist/index.js +320 -35
- 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 +29 -24
- package/docs/ghost-tunnel.md +4 -1
- package/docs/github.md +4 -4
- package/docs/localghost.1.md +32 -5
- 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,20 +2353,23 @@ 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(
|
|
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";
|
|
2086
2361
|
return "npm";
|
|
2087
2362
|
}
|
|
2088
2363
|
function packageRunCommand(packageManager, script) {
|
|
2089
2364
|
if (packageManager === "yarn") return `yarn ${script}`;
|
|
2090
2365
|
if (packageManager === "pnpm") return `pnpm ${script}`;
|
|
2366
|
+
if (packageManager === "bun") return `bun run ${script}`;
|
|
2091
2367
|
return `npm run ${script}`;
|
|
2092
2368
|
}
|
|
2093
2369
|
function packageAddCommand(packageManager, packageName = "@hamedb89/localghost") {
|
|
2094
2370
|
if (packageManager === "yarn") return `yarn add -D ${packageName}`;
|
|
2095
2371
|
if (packageManager === "pnpm") return `pnpm add -D ${packageName}`;
|
|
2372
|
+
if (packageManager === "bun") return `bun add -d ${packageName}`;
|
|
2096
2373
|
return `npm install -D ${packageName}`;
|
|
2097
2374
|
}
|
|
2098
2375
|
function renderConfig(options) {
|
|
@@ -2160,7 +2437,7 @@ function initLocalghost(options = {}) {
|
|
|
2160
2437
|
const apiPort = options.apiPort ?? 8787;
|
|
2161
2438
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
2162
2439
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
2163
|
-
const configPath =
|
|
2440
|
+
const configPath = join8(cwd, configFile);
|
|
2164
2441
|
const configExists = existsSync5(configPath);
|
|
2165
2442
|
if (configExists && !options.force) {
|
|
2166
2443
|
return {
|
|
@@ -2177,7 +2454,7 @@ function initLocalghost(options = {}) {
|
|
|
2177
2454
|
};
|
|
2178
2455
|
}
|
|
2179
2456
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
2180
|
-
const packageJsonPath =
|
|
2457
|
+
const packageJsonPath = join8(cwd, "package.json");
|
|
2181
2458
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
2182
2459
|
return {
|
|
2183
2460
|
configPath,
|
|
@@ -2259,10 +2536,10 @@ function formatGhostTunnel(config, options = {}) {
|
|
|
2259
2536
|
|
|
2260
2537
|
// src/state.ts
|
|
2261
2538
|
import { existsSync as existsSync6 } from "fs";
|
|
2262
|
-
import { join as
|
|
2539
|
+
import { join as join9 } from "path";
|
|
2263
2540
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
2264
2541
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
2265
|
-
return
|
|
2542
|
+
return join9(cwd, LOCALGHOST_STATE_FILE);
|
|
2266
2543
|
}
|
|
2267
2544
|
function readLocalghostState(cwd = process.cwd()) {
|
|
2268
2545
|
const path = getLocalghostStatePath(cwd);
|
|
@@ -2410,7 +2687,7 @@ async function waitForTunnelResponse(input) {
|
|
|
2410
2687
|
await input.store.cleanup(input.requestId);
|
|
2411
2688
|
return response;
|
|
2412
2689
|
}
|
|
2413
|
-
await new Promise((
|
|
2690
|
+
await new Promise((resolve4) => setTimeout(resolve4, input.pollIntervalMs));
|
|
2414
2691
|
}
|
|
2415
2692
|
return null;
|
|
2416
2693
|
}
|
|
@@ -2510,10 +2787,10 @@ function createVercelGhostTunnelHandler(options) {
|
|
|
2510
2787
|
|
|
2511
2788
|
// src/update-check.ts
|
|
2512
2789
|
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
2513
|
-
import { homedir as
|
|
2514
|
-
import { dirname as dirname4, join as
|
|
2790
|
+
import { homedir as homedir3 } from "os";
|
|
2791
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
2515
2792
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
2516
|
-
var LOCALGHOST_VERSION = "0.1.
|
|
2793
|
+
var LOCALGHOST_VERSION = "0.1.15";
|
|
2517
2794
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2518
2795
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2519
2796
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -2525,8 +2802,8 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
2525
2802
|
}
|
|
2526
2803
|
function getUpdateCheckCachePath(env = process.env) {
|
|
2527
2804
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
2528
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
2529
|
-
return
|
|
2805
|
+
const cacheRoot = env.XDG_CACHE_HOME || join10(homedir3(), ".cache");
|
|
2806
|
+
return join10(cacheRoot, "localghost", "update-check.json");
|
|
2530
2807
|
}
|
|
2531
2808
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
2532
2809
|
if (!existsSync7(path)) return null;
|
|
@@ -2578,7 +2855,7 @@ function isNewerVersion(candidate, current = LOCALGHOST_VERSION) {
|
|
|
2578
2855
|
return Boolean(candidate && compareVersions(candidate, current) > 0);
|
|
2579
2856
|
}
|
|
2580
2857
|
async function fetchLatestVersion(packageName, timeoutMs) {
|
|
2581
|
-
const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).
|
|
2858
|
+
const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replaceAll("/", "%2f")}` : packageName;
|
|
2582
2859
|
const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
|
|
2583
2860
|
signal: AbortSignal.timeout(timeoutMs),
|
|
2584
2861
|
headers: {
|
|
@@ -2683,9 +2960,12 @@ export {
|
|
|
2683
2960
|
DEFAULT_RELAY_LIMITS,
|
|
2684
2961
|
DEFAULT_RELAY_TARGET_POLICY,
|
|
2685
2962
|
LOCALGHOST_ACTIVITY_VERSION,
|
|
2963
|
+
LOCALGHOST_AGENT_GUIDE,
|
|
2686
2964
|
LOCALGHOST_CONFIG_FILE,
|
|
2687
2965
|
LOCALGHOST_GHOST_TUNNEL_FILE,
|
|
2688
2966
|
LOCALGHOST_PACKAGE_NAME,
|
|
2967
|
+
LOCALGHOST_REGISTRY_FILE,
|
|
2968
|
+
LOCALGHOST_REGISTRY_LOCK_FILE,
|
|
2689
2969
|
LOCALGHOST_STATE_FILE,
|
|
2690
2970
|
LOCALGHOST_VERSION,
|
|
2691
2971
|
UPDATE_CHECK_CACHE_TTL_MS,
|
|
@@ -2696,6 +2976,7 @@ export {
|
|
|
2696
2976
|
assertRelayLocalTarget,
|
|
2697
2977
|
assertSecureGhostTunnelRequest,
|
|
2698
2978
|
authenticateRelayAgentToken,
|
|
2979
|
+
canonicalizeLocalghostProjectCwd,
|
|
2699
2980
|
checkCaddy,
|
|
2700
2981
|
checkForUpdate,
|
|
2701
2982
|
compareVersions,
|
|
@@ -2705,6 +2986,7 @@ export {
|
|
|
2705
2986
|
constructGhostTunnelUrl,
|
|
2706
2987
|
createGhostTunnelQueuedRequest,
|
|
2707
2988
|
createGhostTunnelRouteHeartbeat,
|
|
2989
|
+
createLocalghostRegistry,
|
|
2708
2990
|
createMemoryGhostTunnelStore,
|
|
2709
2991
|
createRedisGhostTunnelStore,
|
|
2710
2992
|
createRedisGhostTunnelStoreFromEnv,
|
|
@@ -2724,6 +3006,7 @@ export {
|
|
|
2724
3006
|
formatDetectedDevServices,
|
|
2725
3007
|
formatDomainRoutes,
|
|
2726
3008
|
formatGhostTunnel,
|
|
3009
|
+
formatLocalghostAgentGuide,
|
|
2727
3010
|
formatUpdateMessage,
|
|
2728
3011
|
getCaddyfilePath,
|
|
2729
3012
|
getConfigFileCandidates,
|
|
@@ -2737,6 +3020,7 @@ export {
|
|
|
2737
3020
|
getGhostTunnelPreviewUrl,
|
|
2738
3021
|
getGhostTunnelWildcardHost,
|
|
2739
3022
|
getLocalghostActivityPath,
|
|
3023
|
+
getLocalghostRegistryRoot,
|
|
2740
3024
|
getLocalghostStatePath,
|
|
2741
3025
|
getProductionEnvKeys,
|
|
2742
3026
|
getProductionReason,
|
|
@@ -2795,6 +3079,7 @@ export {
|
|
|
2795
3079
|
signRelayRouteClaim,
|
|
2796
3080
|
startCaddy,
|
|
2797
3081
|
startGhostTunnelAgent,
|
|
3082
|
+
stopCaddyProcesses,
|
|
2798
3083
|
stripRelayForwardHeaders,
|
|
2799
3084
|
trustCaddy,
|
|
2800
3085
|
unregisterLocalghostRun,
|