@hamedb89/localghost 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync } from "fs";
4
+ import { existsSync as existsSync9, readFileSync as readFileSync9, unlinkSync } from "fs";
5
5
  import { Command, InvalidArgumentError } from "commander";
6
6
 
7
7
  // src/activity.ts
@@ -480,20 +480,20 @@ function formatDetectedDevServices(services) {
480
480
  }
481
481
 
482
482
  // src/context.ts
483
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
483
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
484
484
  import { join as join6 } from "path";
485
485
  import { pathToFileURL } from "url";
486
486
 
487
487
  // src/port.ts
488
488
  import { createServer } from "net";
489
489
  async function isPortAvailable(port, host = "127.0.0.1") {
490
- return new Promise((resolve5) => {
490
+ return new Promise((resolve6) => {
491
491
  const server = createServer();
492
492
  server.once("error", () => {
493
- resolve5(false);
493
+ resolve6(false);
494
494
  });
495
495
  server.once("listening", () => {
496
- server.close(() => resolve5(true));
496
+ server.close(() => resolve6(true));
497
497
  });
498
498
  server.listen(port, host);
499
499
  });
@@ -738,7 +738,7 @@ function createLocalghostRegistry(options = {}) {
738
738
  };
739
739
  }
740
740
 
741
- // src/tunnel.ts
741
+ // packages/ghost-tunnel/src/tunnel.ts
742
742
  import { domainToASCII } from "url";
743
743
  var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
744
744
  var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
@@ -1089,275 +1089,7 @@ function constructGhostTunnelUrl(input2) {
1089
1089
  return url.toString();
1090
1090
  }
1091
1091
 
1092
- // src/context.ts
1093
- var LOCALGHOST_PROJECT_CONFIG_FILES = [
1094
- "localghost.config.mjs",
1095
- "localghost.config.js",
1096
- "localghost.config.cjs"
1097
- ];
1098
- function parsePort(value) {
1099
- if (!value) return void 0;
1100
- const port = Number.parseInt(value, 10);
1101
- return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
1102
- }
1103
- function envPort() {
1104
- return parsePort(process.env.LOCALGHOST_PORT) ?? parsePort(process.env.VITE_PORT);
1105
- }
1106
- function envDynamicPort() {
1107
- const value = process.env.LOCALGHOST_DYNAMIC_PORT;
1108
- if (!value) return void 0;
1109
- return ["1", "true", "yes", "on"].includes(value.toLowerCase());
1110
- }
1111
- function envHttps() {
1112
- const value = process.env.LOCALGHOST_HTTPS;
1113
- if (!value) return void 0;
1114
- return ["1", "true", "yes", "on"].includes(value.toLowerCase());
1115
- }
1116
- function getPackageName(cwd) {
1117
- try {
1118
- const pkg = JSON.parse(readFileSync5(join6(cwd, "package.json"), "utf8"));
1119
- return typeof pkg.name === "string" ? pkg.name : void 0;
1120
- } catch {
1121
- return void 0;
1122
- }
1123
- }
1124
- function getPackageOwner(cwd) {
1125
- const packageName = getPackageName(cwd);
1126
- if (!packageName?.startsWith("@")) return void 0;
1127
- return packageName.slice(1).split("/")[0];
1128
- }
1129
- function getLocalOwner(cwd) {
1130
- return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
1131
- }
1132
- function getRouteName(primaryHost, fallback) {
1133
- return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
1134
- }
1135
- function readOptionsFromContext(options) {
1136
- return {
1137
- cwd: options.cwd ?? process.cwd(),
1138
- ...options.fileName ? { fileName: options.fileName } : {},
1139
- ...options.configFiles ? { configFiles: options.configFiles } : {},
1140
- ...options.configPattern ? { configPattern: options.configPattern } : {}
1141
- };
1142
- }
1143
- function withRuntimePort(entries, requestedPort, port) {
1144
- if (requestedPort === port) return entries;
1145
- const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
1146
- if (!hasRequestedPort) return entries;
1147
- return entries.map((entry) => entry.port === requestedPort ? { ...entry, port, target: `127.0.0.1:${port}` } : entry);
1148
- }
1149
- function uniqueHosts(entries) {
1150
- return [...new Set(entries.map((entry) => entry.host))];
1151
- }
1152
- function isAliasableHost(host) {
1153
- return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
1154
- }
1155
- function getDefaultWwwAlias(host) {
1156
- return isAliasableHost(host) ? `www.${host}` : null;
1157
- }
1158
- function addDefaultWwwAliases(entries) {
1159
- const seen = new Set(entries.map((entry) => entry.host));
1160
- const aliases = [];
1161
- for (const entry of entries) {
1162
- const alias = getDefaultWwwAlias(entry.host);
1163
- if (alias && !seen.has(alias)) {
1164
- aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
1165
- seen.add(alias);
1166
- }
1167
- }
1168
- return [...entries, ...aliases];
1169
- }
1170
- function defined(input2) {
1171
- return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
1172
- }
1173
- async function readLocalghostProjectConfig(options = {}) {
1174
- const cwd = options.cwd ?? process.cwd();
1175
- if (options.configFile === false) return { config: {} };
1176
- const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
1177
- const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync4(candidate));
1178
- if (!path) return { config: {} };
1179
- const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
1180
- const config = imported.default ?? imported;
1181
- return { config, path };
1182
- }
1183
- async function resolveLocalghostContext(options = {}) {
1184
- const cwd = options.cwd ?? process.cwd();
1185
- const projectConfig = await readLocalghostProjectConfig({
1186
- cwd,
1187
- ...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
1188
- });
1189
- const merged = {
1190
- ...projectConfig.config,
1191
- ...defined(options)
1192
- };
1193
- const readOptions = readOptionsFromContext({ ...merged, cwd });
1194
- const resolvedPath = resolveDevHostsPath(readOptions);
1195
- const configEntries = readDevHosts(readOptions);
1196
- const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
1197
- const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
1198
- const autoRepair = merged.autoRepair ?? true;
1199
- const bindHost = merged.bindHost ?? "127.0.0.1";
1200
- const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
1201
- let port = requestedPort;
1202
- let releasePort;
1203
- const reservePort = merged.reservePort ?? false;
1204
- const instanceKey = merged.instanceKey ?? "run";
1205
- if (reservePort && dynamicPort) {
1206
- const registry = createLocalghostRegistry({ cwd, ...merged.registryOwnerToken ? { ownerToken: merged.registryOwnerToken } : {} });
1207
- const lease = await registry.acquirePort({
1208
- projectCwd: cwd,
1209
- instanceKey,
1210
- startPort: requestedPort,
1211
- host: probeHost,
1212
- ...options.reservedPorts ? { reservedPorts: options.reservedPorts } : {}
1213
- });
1214
- port = lease.port;
1215
- releasePort = () => registry.releasePort({ projectCwd: cwd, instanceKey });
1216
- } else if (dynamicPort) {
1217
- port = await findAvailablePort(requestedPort, { host: probeHost });
1218
- }
1219
- const wwwAlias = merged.wwwAlias ?? true;
1220
- const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
1221
- const hosts = uniqueHosts(entries);
1222
- const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
1223
- const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
1224
- const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
1225
- route: getRouteName(primaryHost, projectName),
1226
- project: projectName,
1227
- owner: getLocalOwner(cwd)
1228
- });
1229
- return {
1230
- cwd,
1231
- projectName,
1232
- readOptions,
1233
- configPath: resolvedPath.path,
1234
- configFileName: resolvedPath.fileName,
1235
- configEntries,
1236
- entries,
1237
- hosts,
1238
- requestedPort,
1239
- port,
1240
- dynamicPort,
1241
- autoRepair,
1242
- bindHost,
1243
- primaryHost,
1244
- https: merged.https ?? envHttps() ?? false,
1245
- wwwAlias,
1246
- ghostTunnel,
1247
- ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {},
1248
- ...releasePort ? { releasePort } : {}
1249
- };
1250
- }
1251
-
1252
- // src/doctor.ts
1253
- import { execa as execa2 } from "execa";
1254
- async function checkCaddy() {
1255
- try {
1256
- const result = await execa2("caddy", ["version"], { reject: false });
1257
- const version = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
1258
- return {
1259
- found: result.exitCode === 0,
1260
- ...version ? { version } : {},
1261
- installHint: "brew install caddy"
1262
- };
1263
- } catch {
1264
- return {
1265
- found: false,
1266
- installHint: "brew install caddy"
1267
- };
1268
- }
1269
- }
1270
- async function runDoctor(options = {}) {
1271
- const caddy = await checkCaddy();
1272
- const cwd = options.cwd ?? process.cwd();
1273
- const registry = createLocalghostRegistry({ cwd });
1274
- const data = await registry.read();
1275
- const now = Date.now();
1276
- const staleLeases = data.leases.filter((lease) => lease.expiresAt <= now || !isProcessRunning(lease.pid)).map(({ projectCwd, instanceKey, port, pid }) => ({ projectCwd, instanceKey, port, pid }));
1277
- const allocationsByPort = /* @__PURE__ */ new Map();
1278
- for (const allocation of data.allocations) {
1279
- const projects = allocationsByPort.get(allocation.port) ?? [];
1280
- projects.push(`${allocation.projectCwd}#${allocation.instanceKey}`);
1281
- allocationsByPort.set(allocation.port, projects);
1282
- }
1283
- const duplicateAllocations = [...allocationsByPort.entries()].filter(([, projects]) => projects.length > 1).map(([port, projects]) => ({ port, projects }));
1284
- let configured;
1285
- let available;
1286
- try {
1287
- const context = await resolveLocalghostContext({
1288
- cwd,
1289
- ...options.configFiles ? { configFiles: options.configFiles } : {},
1290
- ...options.configPattern ? { configPattern: options.configPattern } : {},
1291
- dynamicPort: false
1292
- });
1293
- configured = context.requestedPort;
1294
- available = await isPortAvailable(configured);
1295
- } catch {
1296
- }
1297
- const currentProjectCwd = canonicalizeLocalghostProjectCwd(cwd);
1298
- const currentAllocation = data.allocations.find((allocation) => allocation.projectCwd === currentProjectCwd);
1299
- return {
1300
- ok: caddy.found && available !== false && staleLeases.length === 0 && duplicateAllocations.length === 0,
1301
- caddy,
1302
- ports: {
1303
- ...configured !== void 0 ? { configured } : {},
1304
- ...available !== void 0 ? { available } : {},
1305
- registryPath: registry.registryPath,
1306
- staleLeases,
1307
- duplicateAllocations,
1308
- ...currentAllocation ? {
1309
- currentAllocation: {
1310
- projectCwd: currentAllocation.projectCwd,
1311
- instanceKey: currentAllocation.instanceKey,
1312
- port: currentAllocation.port
1313
- }
1314
- } : {}
1315
- }
1316
- };
1317
- }
1318
-
1319
- // src/env.ts
1320
- function getProductionReason(env = process.env) {
1321
- if (env.LOCALGHOST_ENV === "production") return "LOCALGHOST_ENV=production";
1322
- if (env.NODE_ENV === "production") return "NODE_ENV=production";
1323
- if (env.VERCEL_ENV === "production") return "VERCEL_ENV=production";
1324
- if (env.NETLIFY === "true" && env.CONTEXT === "production") return "NETLIFY=true and CONTEXT=production";
1325
- if (env.CF_PAGES_BRANCH && env.CF_PAGES_BRANCH === env.CF_PAGES_PRODUCTION_BRANCH) {
1326
- return "CF_PAGES_BRANCH matches CF_PAGES_PRODUCTION_BRANCH";
1327
- }
1328
- return null;
1329
- }
1330
- function assertLocalDevelopment(command, env = process.env) {
1331
- const reason = getProductionReason(env);
1332
- if (!reason) return;
1333
- throw new Error(`Localghost only runs in local development. Refusing \`${command}\` because ${reason}.`);
1334
- }
1335
-
1336
- // src/ghost-file.ts
1337
- var LOCALGHOST_GHOST_TUNNEL_FILE = ".ghosttunnel";
1338
- function toGhostTunnelOptions(options = {}) {
1339
- const resolved = typeof options === "string" ? { cwd: options } : options;
1340
- return {
1341
- ...resolved,
1342
- fileName: resolved.fileName ?? LOCALGHOST_GHOST_TUNNEL_FILE
1343
- };
1344
- }
1345
- function resolveGhostTunnelPath(options = {}) {
1346
- return resolveDevHostsPath(toGhostTunnelOptions(options));
1347
- }
1348
- function readGhostTunnelEntries(options = {}) {
1349
- return readDevHosts(toGhostTunnelOptions(options));
1350
- }
1351
- function listGhostTunnelEntries(options = {}) {
1352
- const resolved = resolveGhostTunnelPath(options);
1353
- if (!resolved.exists) return [];
1354
- return readGhostTunnelEntries(options);
1355
- }
1356
-
1357
- // src/ghost-agent.ts
1358
- import { randomUUID as randomUUID3 } from "crypto";
1359
-
1360
- // src/relay.ts
1092
+ // packages/ghost-tunnel/src/relay.ts
1361
1093
  import { createHmac, timingSafeEqual } from "crypto";
1362
1094
  import { domainToASCII as domainToASCII2 } from "url";
1363
1095
  var DEFAULT_RELAY_ALLOWED_TARGET_HOSTS = ["localhost", "127.0.0.1", "::1"];
@@ -1463,10 +1195,76 @@ function stripRelayForwardHeaders(headers) {
1463
1195
  if (lowerName.startsWith("x-localghost-")) continue;
1464
1196
  stripped[name] = value;
1465
1197
  }
1466
- return stripped;
1198
+ return stripped;
1199
+ }
1200
+
1201
+ // packages/ghost-tunnel/src/ghost-file.ts
1202
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
1203
+ import { basename as basename2, resolve as resolve4 } from "path";
1204
+ var LOCALGHOST_GHOST_TUNNEL_FILE = ".ghosttunnel";
1205
+ function getCandidates(options) {
1206
+ const exact = [.../* @__PURE__ */ new Set([...options.fileName ? [options.fileName] : [], ...options.configFiles ?? []])];
1207
+ const pattern = options.configPattern ? readdirSync2(options.cwd ?? process.cwd(), { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).filter((name) => {
1208
+ const matcher = typeof options.configPattern === "string" ? new RegExp(options.configPattern) : options.configPattern;
1209
+ if (!matcher) return false;
1210
+ matcher.lastIndex = 0;
1211
+ return matcher.test(name);
1212
+ }).sort() : [];
1213
+ if (exact.length > 0 || pattern.length > 0) return [.../* @__PURE__ */ new Set([...exact, ...pattern])];
1214
+ return [LOCALGHOST_GHOST_TUNNEL_FILE];
1215
+ }
1216
+ function parseGhostTunnelEntries(input2, fileName) {
1217
+ const entries = [];
1218
+ input2.split(/\r?\n/).forEach((rawLine, index) => {
1219
+ const line = rawLine.replace(/#.*/, "").trim();
1220
+ if (!line) return;
1221
+ const parts = line.split(/\s+/);
1222
+ const host = parts[0];
1223
+ const portRaw = parts[1];
1224
+ if (!host || !portRaw || parts.length > 2) {
1225
+ throw new Error(`Invalid ${fileName} line ${index + 1}: "${rawLine}"`);
1226
+ }
1227
+ if (!/^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*\.?$/i.test(host)) {
1228
+ throw new Error(`Invalid host on line ${index + 1}: "${host}"`);
1229
+ }
1230
+ const port = Number(portRaw);
1231
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1232
+ throw new Error(`Invalid port on line ${index + 1}: "${portRaw}"`);
1233
+ }
1234
+ entries.push({ host: host.toLowerCase().replace(/\.$/, ""), port, target: `127.0.0.1:${port}` });
1235
+ });
1236
+ return entries;
1237
+ }
1238
+ function toGhostTunnelOptions(options = {}) {
1239
+ const resolved = typeof options === "string" ? { cwd: options } : options;
1240
+ return {
1241
+ ...resolved,
1242
+ fileName: resolved.fileName ?? LOCALGHOST_GHOST_TUNNEL_FILE
1243
+ };
1244
+ }
1245
+ function resolveGhostTunnelPath(options = {}) {
1246
+ const resolved = toGhostTunnelOptions(options);
1247
+ const cwd = resolved.cwd ?? process.cwd();
1248
+ const searchedFiles = getCandidates(resolved);
1249
+ for (const fileName2 of searchedFiles) {
1250
+ const path = resolve4(cwd, fileName2);
1251
+ if (existsSync4(path)) return { path, fileName: basename2(fileName2), exists: true, searchedFiles };
1252
+ }
1253
+ const fileName = searchedFiles[0] ?? LOCALGHOST_GHOST_TUNNEL_FILE;
1254
+ return { path: resolve4(cwd, fileName), fileName: basename2(fileName), exists: false, searchedFiles };
1255
+ }
1256
+ function readGhostTunnelEntries(options = {}) {
1257
+ const resolved = resolveGhostTunnelPath(options);
1258
+ if (!resolved.exists) throw new Error(`Missing Ghost Tunnel file: ${resolved.path}`);
1259
+ return parseGhostTunnelEntries(readFileSync5(resolved.path, "utf8"), resolved.fileName);
1260
+ }
1261
+ function listGhostTunnelEntries(options = {}) {
1262
+ const resolved = resolveGhostTunnelPath(options);
1263
+ if (!resolved.exists) return [];
1264
+ return readGhostTunnelEntries(options);
1467
1265
  }
1468
1266
 
1469
- // src/ghost-tunnel-store.ts
1267
+ // packages/ghost-tunnel/src/ghost-tunnel-store.ts
1470
1268
  import { randomUUID as randomUUID2 } from "crypto";
1471
1269
  function base64Encode(value) {
1472
1270
  return value.toString("base64");
@@ -1598,17 +1396,18 @@ function createRedisGhostTunnelStoreFromEnv(input2 = {}) {
1598
1396
  });
1599
1397
  }
1600
1398
 
1601
- // src/ghost-agent.ts
1399
+ // packages/ghost-tunnel/src/ghost-agent.ts
1400
+ import { randomUUID as randomUUID3 } from "crypto";
1602
1401
  function isStopped(signal, localSignal) {
1603
1402
  return localSignal.aborted || signal?.aborted === true;
1604
1403
  }
1605
1404
  function wait(ms, signal, localSignal) {
1606
1405
  if (isStopped(signal, localSignal)) return Promise.resolve();
1607
- return new Promise((resolve5) => {
1608
- const timeout = setTimeout(resolve5, ms);
1406
+ return new Promise((resolve6) => {
1407
+ const timeout = setTimeout(resolve6, ms);
1609
1408
  const stop = () => {
1610
1409
  clearTimeout(timeout);
1611
- resolve5();
1410
+ resolve6();
1612
1411
  };
1613
1412
  signal?.addEventListener("abort", stop, { once: true });
1614
1413
  localSignal.addEventListener("abort", stop, { once: true });
@@ -1746,6 +1545,250 @@ function startGhostTunnelAgent(options) {
1746
1545
  };
1747
1546
  }
1748
1547
 
1548
+ // src/context.ts
1549
+ var LOCALGHOST_PROJECT_CONFIG_FILES = [
1550
+ "localghost.config.mjs",
1551
+ "localghost.config.js",
1552
+ "localghost.config.cjs"
1553
+ ];
1554
+ function parsePort(value) {
1555
+ if (!value) return void 0;
1556
+ const port = Number.parseInt(value, 10);
1557
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
1558
+ }
1559
+ function envPort() {
1560
+ return parsePort(process.env.LOCALGHOST_PORT) ?? parsePort(process.env.VITE_PORT);
1561
+ }
1562
+ function envDynamicPort() {
1563
+ const value = process.env.LOCALGHOST_DYNAMIC_PORT;
1564
+ if (!value) return void 0;
1565
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
1566
+ }
1567
+ function envHttps() {
1568
+ const value = process.env.LOCALGHOST_HTTPS;
1569
+ if (!value) return void 0;
1570
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
1571
+ }
1572
+ function getPackageName(cwd) {
1573
+ try {
1574
+ const pkg = JSON.parse(readFileSync6(join6(cwd, "package.json"), "utf8"));
1575
+ return typeof pkg.name === "string" ? pkg.name : void 0;
1576
+ } catch {
1577
+ return void 0;
1578
+ }
1579
+ }
1580
+ function getPackageOwner(cwd) {
1581
+ const packageName = getPackageName(cwd);
1582
+ if (!packageName?.startsWith("@")) return void 0;
1583
+ return packageName.slice(1).split("/")[0];
1584
+ }
1585
+ function getLocalOwner(cwd) {
1586
+ return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
1587
+ }
1588
+ function getRouteName(primaryHost, fallback) {
1589
+ return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
1590
+ }
1591
+ function readOptionsFromContext(options) {
1592
+ return {
1593
+ cwd: options.cwd ?? process.cwd(),
1594
+ ...options.fileName ? { fileName: options.fileName } : {},
1595
+ ...options.configFiles ? { configFiles: options.configFiles } : {},
1596
+ ...options.configPattern ? { configPattern: options.configPattern } : {}
1597
+ };
1598
+ }
1599
+ function withRuntimePort(entries, requestedPort, port) {
1600
+ if (requestedPort === port) return entries;
1601
+ const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
1602
+ if (!hasRequestedPort) return entries;
1603
+ return entries.map((entry) => entry.port === requestedPort ? { ...entry, port, target: `127.0.0.1:${port}` } : entry);
1604
+ }
1605
+ function uniqueHosts(entries) {
1606
+ return [...new Set(entries.map((entry) => entry.host))];
1607
+ }
1608
+ function isAliasableHost(host) {
1609
+ return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
1610
+ }
1611
+ function getDefaultWwwAlias(host) {
1612
+ return isAliasableHost(host) ? `www.${host}` : null;
1613
+ }
1614
+ function addDefaultWwwAliases(entries) {
1615
+ const seen = new Set(entries.map((entry) => entry.host));
1616
+ const aliases = [];
1617
+ for (const entry of entries) {
1618
+ const alias = getDefaultWwwAlias(entry.host);
1619
+ if (alias && !seen.has(alias)) {
1620
+ aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
1621
+ seen.add(alias);
1622
+ }
1623
+ }
1624
+ return [...entries, ...aliases];
1625
+ }
1626
+ function defined(input2) {
1627
+ return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
1628
+ }
1629
+ async function readLocalghostProjectConfig(options = {}) {
1630
+ const cwd = options.cwd ?? process.cwd();
1631
+ if (options.configFile === false) return { config: {} };
1632
+ const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
1633
+ const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync5(candidate));
1634
+ if (!path) return { config: {} };
1635
+ const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
1636
+ const config = imported.default ?? imported;
1637
+ return { config, path };
1638
+ }
1639
+ async function resolveLocalghostContext(options = {}) {
1640
+ const cwd = options.cwd ?? process.cwd();
1641
+ const projectConfig = await readLocalghostProjectConfig({
1642
+ cwd,
1643
+ ...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
1644
+ });
1645
+ const merged = {
1646
+ ...projectConfig.config,
1647
+ ...defined(options)
1648
+ };
1649
+ const readOptions = readOptionsFromContext({ ...merged, cwd });
1650
+ const resolvedPath = resolveDevHostsPath(readOptions);
1651
+ const configEntries = readDevHosts(readOptions);
1652
+ const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
1653
+ const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
1654
+ const autoRepair = merged.autoRepair ?? true;
1655
+ const bindHost = merged.bindHost ?? "127.0.0.1";
1656
+ const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
1657
+ let port = requestedPort;
1658
+ let releasePort;
1659
+ const reservePort = merged.reservePort ?? false;
1660
+ const instanceKey = merged.instanceKey ?? "run";
1661
+ if (reservePort && dynamicPort) {
1662
+ const registry = createLocalghostRegistry({ cwd, ...merged.registryOwnerToken ? { ownerToken: merged.registryOwnerToken } : {} });
1663
+ const lease = await registry.acquirePort({
1664
+ projectCwd: cwd,
1665
+ instanceKey,
1666
+ startPort: requestedPort,
1667
+ host: probeHost,
1668
+ ...options.reservedPorts ? { reservedPorts: options.reservedPorts } : {}
1669
+ });
1670
+ port = lease.port;
1671
+ releasePort = () => registry.releasePort({ projectCwd: cwd, instanceKey });
1672
+ } else if (dynamicPort) {
1673
+ port = await findAvailablePort(requestedPort, { host: probeHost });
1674
+ }
1675
+ const wwwAlias = merged.wwwAlias ?? true;
1676
+ const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
1677
+ const hosts = uniqueHosts(entries);
1678
+ const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
1679
+ const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
1680
+ const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
1681
+ route: getRouteName(primaryHost, projectName),
1682
+ project: projectName,
1683
+ owner: getLocalOwner(cwd)
1684
+ });
1685
+ return {
1686
+ cwd,
1687
+ projectName,
1688
+ readOptions,
1689
+ configPath: resolvedPath.path,
1690
+ configFileName: resolvedPath.fileName,
1691
+ configEntries,
1692
+ entries,
1693
+ hosts,
1694
+ requestedPort,
1695
+ port,
1696
+ dynamicPort,
1697
+ autoRepair,
1698
+ bindHost,
1699
+ primaryHost,
1700
+ https: merged.https ?? envHttps() ?? false,
1701
+ wwwAlias,
1702
+ ghostTunnel,
1703
+ ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {},
1704
+ ...releasePort ? { releasePort } : {}
1705
+ };
1706
+ }
1707
+
1708
+ // src/doctor.ts
1709
+ import { execa as execa2 } from "execa";
1710
+ async function checkCaddy() {
1711
+ try {
1712
+ const result = await execa2("caddy", ["version"], { reject: false });
1713
+ const version = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
1714
+ return {
1715
+ found: result.exitCode === 0,
1716
+ ...version ? { version } : {},
1717
+ installHint: "brew install caddy"
1718
+ };
1719
+ } catch {
1720
+ return {
1721
+ found: false,
1722
+ installHint: "brew install caddy"
1723
+ };
1724
+ }
1725
+ }
1726
+ async function runDoctor(options = {}) {
1727
+ const caddy = await checkCaddy();
1728
+ const cwd = options.cwd ?? process.cwd();
1729
+ const registry = createLocalghostRegistry({ cwd });
1730
+ const data = await registry.read();
1731
+ const now = Date.now();
1732
+ const staleLeases = data.leases.filter((lease) => lease.expiresAt <= now || !isProcessRunning(lease.pid)).map(({ projectCwd, instanceKey, port, pid }) => ({ projectCwd, instanceKey, port, pid }));
1733
+ const allocationsByPort = /* @__PURE__ */ new Map();
1734
+ for (const allocation of data.allocations) {
1735
+ const projects = allocationsByPort.get(allocation.port) ?? [];
1736
+ projects.push(`${allocation.projectCwd}#${allocation.instanceKey}`);
1737
+ allocationsByPort.set(allocation.port, projects);
1738
+ }
1739
+ const duplicateAllocations = [...allocationsByPort.entries()].filter(([, projects]) => projects.length > 1).map(([port, projects]) => ({ port, projects }));
1740
+ let configured;
1741
+ let available;
1742
+ try {
1743
+ const context = await resolveLocalghostContext({
1744
+ cwd,
1745
+ ...options.configFiles ? { configFiles: options.configFiles } : {},
1746
+ ...options.configPattern ? { configPattern: options.configPattern } : {},
1747
+ dynamicPort: false
1748
+ });
1749
+ configured = context.requestedPort;
1750
+ available = await isPortAvailable(configured);
1751
+ } catch {
1752
+ }
1753
+ const currentProjectCwd = canonicalizeLocalghostProjectCwd(cwd);
1754
+ const currentAllocation = data.allocations.find((allocation) => allocation.projectCwd === currentProjectCwd);
1755
+ return {
1756
+ ok: caddy.found && available !== false && staleLeases.length === 0 && duplicateAllocations.length === 0,
1757
+ caddy,
1758
+ ports: {
1759
+ ...configured !== void 0 ? { configured } : {},
1760
+ ...available !== void 0 ? { available } : {},
1761
+ registryPath: registry.registryPath,
1762
+ staleLeases,
1763
+ duplicateAllocations,
1764
+ ...currentAllocation ? {
1765
+ currentAllocation: {
1766
+ projectCwd: currentAllocation.projectCwd,
1767
+ instanceKey: currentAllocation.instanceKey,
1768
+ port: currentAllocation.port
1769
+ }
1770
+ } : {}
1771
+ }
1772
+ };
1773
+ }
1774
+
1775
+ // src/env.ts
1776
+ function getProductionReason(env = process.env) {
1777
+ if (env.LOCALGHOST_ENV === "production") return "LOCALGHOST_ENV=production";
1778
+ if (env.NODE_ENV === "production") return "NODE_ENV=production";
1779
+ if (env.VERCEL_ENV === "production") return "VERCEL_ENV=production";
1780
+ if (env.NETLIFY === "true" && env.CONTEXT === "production") return "NETLIFY=true and CONTEXT=production";
1781
+ if (env.CF_PAGES_BRANCH && env.CF_PAGES_BRANCH === env.CF_PAGES_PRODUCTION_BRANCH) {
1782
+ return "CF_PAGES_BRANCH matches CF_PAGES_PRODUCTION_BRANCH";
1783
+ }
1784
+ return null;
1785
+ }
1786
+ function assertLocalDevelopment(command, env = process.env) {
1787
+ const reason = getProductionReason(env);
1788
+ if (!reason) return;
1789
+ throw new Error(`Localghost only runs in local development. Refusing \`${command}\` because ${reason}.`);
1790
+ }
1791
+
1749
1792
  // src/hosts-file.ts
1750
1793
  import { writeFileSync as writeFileSync3 } from "fs";
1751
1794
  import { tmpdir } from "os";
@@ -1829,19 +1872,19 @@ async function removeSystemHosts(projectName) {
1829
1872
  }
1830
1873
 
1831
1874
  // src/init.ts
1832
- import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1833
- import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
1875
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1876
+ import { dirname as dirname4, join as join8, resolve as resolve5 } from "path";
1834
1877
  function detectPackageManager(cwd = process.cwd()) {
1835
- if (existsSync5(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
1836
- if (existsSync5(join8(cwd, "yarn.lock"))) return "yarn";
1837
- if (existsSync5(join8(cwd, "bun.lock")) || existsSync5(join8(cwd, "bun.lockb"))) return "bun";
1878
+ if (existsSync6(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
1879
+ if (existsSync6(join8(cwd, "yarn.lock"))) return "yarn";
1880
+ if (existsSync6(join8(cwd, "bun.lock")) || existsSync6(join8(cwd, "bun.lockb"))) return "bun";
1838
1881
  return "npm";
1839
1882
  }
1840
1883
  function isPnpmWorkspaceRoot(cwd = process.cwd()) {
1841
- const target = resolve4(cwd);
1884
+ const target = resolve5(cwd);
1842
1885
  let current = target;
1843
1886
  while (true) {
1844
- if (existsSync5(join8(current, "pnpm-workspace.yaml"))) return current === target;
1887
+ if (existsSync6(join8(current, "pnpm-workspace.yaml"))) return current === target;
1845
1888
  const parent = dirname4(current);
1846
1889
  if (parent === current) return false;
1847
1890
  current = parent;
@@ -1865,7 +1908,7 @@ function renderConfig(options) {
1865
1908
  }
1866
1909
  function readPackageJson2(path) {
1867
1910
  try {
1868
- return JSON.parse(readFileSync6(path, "utf8"));
1911
+ return JSON.parse(readFileSync7(path, "utf8"));
1869
1912
  } catch {
1870
1913
  return null;
1871
1914
  }
@@ -1933,7 +1976,7 @@ function initLocalghost(options = {}) {
1933
1976
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
1934
1977
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
1935
1978
  const configPath = join8(cwd, configFile);
1936
- const configExists = existsSync5(configPath);
1979
+ const configExists = existsSync6(configPath);
1937
1980
  if (configExists && !options.force) {
1938
1981
  return {
1939
1982
  configPath,
@@ -1954,7 +1997,7 @@ function initLocalghost(options = {}) {
1954
1997
  return {
1955
1998
  configPath,
1956
1999
  configCreated: true,
1957
- ...existsSync5(packageJsonPath) ? { packageJsonPath } : {},
2000
+ ...existsSync6(packageJsonPath) ? { packageJsonPath } : {},
1958
2001
  packageJsonChanged,
1959
2002
  packageManager,
1960
2003
  nextSteps: [
@@ -2183,7 +2226,7 @@ function formatGhostTunnel(config, options = {}) {
2183
2226
  }
2184
2227
 
2185
2228
  // src/state.ts
2186
- import { existsSync as existsSync6 } from "fs";
2229
+ import { existsSync as existsSync7 } from "fs";
2187
2230
  import { join as join9 } from "path";
2188
2231
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
2189
2232
  function getLocalghostStatePath(cwd = process.cwd()) {
@@ -2191,7 +2234,7 @@ function getLocalghostStatePath(cwd = process.cwd()) {
2191
2234
  }
2192
2235
  function readLocalghostState(cwd = process.cwd()) {
2193
2236
  const path = getLocalghostStatePath(cwd);
2194
- if (!existsSync6(path)) return null;
2237
+ if (!existsSync7(path)) return null;
2195
2238
  return JSON.parse(readTextFile(path));
2196
2239
  }
2197
2240
  function writeLocalghostState(cwd, state) {
@@ -2207,11 +2250,11 @@ function patchLocalghostState(cwd, patch) {
2207
2250
  }
2208
2251
 
2209
2252
  // src/update-check.ts
2210
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2253
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2211
2254
  import { homedir as homedir3 } from "os";
2212
2255
  import { dirname as dirname5, join as join10 } from "path";
2213
2256
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
2214
- var LOCALGHOST_VERSION = "0.6.1";
2257
+ var LOCALGHOST_VERSION = "0.6.2";
2215
2258
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
2216
2259
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
2217
2260
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -2227,9 +2270,9 @@ function getUpdateCheckCachePath(env = process.env) {
2227
2270
  return join10(cacheRoot, "localghost", "update-check.json");
2228
2271
  }
2229
2272
  function readCache(path = getUpdateCheckCachePath()) {
2230
- if (!existsSync7(path)) return null;
2273
+ if (!existsSync8(path)) return null;
2231
2274
  try {
2232
- return JSON.parse(readFileSync7(path, "utf8"));
2275
+ return JSON.parse(readFileSync8(path, "utf8"));
2233
2276
  } catch {
2234
2277
  return null;
2235
2278
  }
@@ -2553,7 +2596,7 @@ function getSetupReadiness(options) {
2553
2596
  }
2554
2597
  const hostsPath = getSystemHostsPath();
2555
2598
  try {
2556
- const hosts = readFileSync8(hostsPath, "utf8");
2599
+ const hosts = readFileSync9(hostsPath, "utf8");
2557
2600
  const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
2558
2601
  if (!hosts.includes(expectedHostsBlock)) {
2559
2602
  reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
@@ -2563,11 +2606,11 @@ function getSetupReadiness(options) {
2563
2606
  reasons.push(`Could not read ${hostsPath}: ${message}`);
2564
2607
  }
2565
2608
  if (!options.ignoreCaddyfile) {
2566
- if (!existsSync8(caddyfilePath)) {
2609
+ if (!existsSync9(caddyfilePath)) {
2567
2610
  reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
2568
2611
  } else {
2569
2612
  const expectedCaddyfile = renderCaddyfile(entries, { https });
2570
- const currentCaddyfile = readFileSync8(caddyfilePath, "utf8");
2613
+ const currentCaddyfile = readFileSync9(caddyfilePath, "utf8");
2571
2614
  if (currentCaddyfile !== expectedCaddyfile) {
2572
2615
  reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
2573
2616
  }
@@ -2612,7 +2655,7 @@ async function runSetupFromReadiness(cwd, https, readiness) {
2612
2655
  });
2613
2656
  }
2614
2657
  function wait2(ms) {
2615
- return new Promise((resolve5) => setTimeout(resolve5, ms));
2658
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
2616
2659
  }
2617
2660
  async function runTrust(cwd, caddyfilePath) {
2618
2661
  await wait2(350);
@@ -3244,13 +3287,13 @@ program.command("reset").description("Remove Localghost setup state without dele
3244
3287
  const statePath = getLocalghostStatePath(options.cwd);
3245
3288
  explainHostsPassword();
3246
3289
  const hostsResult = await removeSystemHosts(projectName);
3247
- if (existsSync8(caddyfilePath)) {
3290
+ if (existsSync9(caddyfilePath)) {
3248
3291
  unlinkSync(caddyfilePath);
3249
3292
  console.log(`Removed ${caddyfilePath}`);
3250
3293
  } else {
3251
3294
  console.log(`${caddyfilePath} was not present`);
3252
3295
  }
3253
- if (existsSync8(statePath)) {
3296
+ if (existsSync9(statePath)) {
3254
3297
  unlinkSync(statePath);
3255
3298
  console.log(`Removed ${statePath}`);
3256
3299
  } else {
@@ -3271,7 +3314,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
3271
3314
  const hostsResult = await removeSystemHosts(projectName);
3272
3315
  const caddyfilePath = getCaddyfilePath(options.cwd);
3273
3316
  let caddyfileRemoved = false;
3274
- if (options.removeCaddyfile && existsSync8(caddyfilePath)) {
3317
+ if (options.removeCaddyfile && existsSync9(caddyfilePath)) {
3275
3318
  unlinkSync(caddyfilePath);
3276
3319
  caddyfileRemoved = true;
3277
3320
  }