@hamedb89/localghost 0.5.0 → 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/README.md +5 -1
- package/dist/cli.js +427 -321
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +147 -107
- package/dist/index.js +1511 -1407
- package/dist/index.js.map +1 -1
- package/dist/{tunnel-BA52DD9e.d.ts → tunnel-DHYPpsX2.d.ts} +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +35 -1
- package/dist/vite.js.map +1 -1
- package/docs/ghost-tunnel.md +9 -3
- package/docs/localghost.1.md +10 -0
- package/package.json +5 -2
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
|
|
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
|
|
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((
|
|
490
|
+
return new Promise((resolve6) => {
|
|
491
491
|
const server = createServer();
|
|
492
492
|
server.once("error", () => {
|
|
493
|
-
|
|
493
|
+
resolve6(false);
|
|
494
494
|
});
|
|
495
495
|
server.once("listening", () => {
|
|
496
|
-
server.close(() =>
|
|
496
|
+
server.close(() => resolve6(true));
|
|
497
497
|
});
|
|
498
498
|
server.listen(port, host);
|
|
499
499
|
});
|
|
@@ -546,6 +546,9 @@ function validRegistry(value) {
|
|
|
546
546
|
function pruneRegistry(registry, now, isRunning) {
|
|
547
547
|
registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
|
|
548
548
|
}
|
|
549
|
+
function isTestSessionKey(instanceKey) {
|
|
550
|
+
return instanceKey.startsWith("test:");
|
|
551
|
+
}
|
|
549
552
|
async function readJson(path) {
|
|
550
553
|
try {
|
|
551
554
|
return JSON.parse(await readFile(path, "utf8"));
|
|
@@ -642,6 +645,37 @@ function createLocalghostRegistry(options = {}) {
|
|
|
642
645
|
await releaseLock();
|
|
643
646
|
}
|
|
644
647
|
},
|
|
648
|
+
async pruneTestSessions() {
|
|
649
|
+
const releaseLock = await lock();
|
|
650
|
+
try {
|
|
651
|
+
const registry = await readRegistry();
|
|
652
|
+
const staleTestKeys = new Set(
|
|
653
|
+
registry.leases.filter((lease) => isTestSessionKey(lease.instanceKey) && (lease.expiresAt <= now() || !isRunning(lease.pid))).map((lease) => leaseKey(lease.projectCwd, lease.instanceKey))
|
|
654
|
+
);
|
|
655
|
+
const beforeLeases = registry.leases.length;
|
|
656
|
+
registry.leases = registry.leases.filter((lease) => !staleTestKeys.has(leaseKey(lease.projectCwd, lease.instanceKey)));
|
|
657
|
+
const activeKeys = new Set(registry.leases.map((lease) => leaseKey(lease.projectCwd, lease.instanceKey)));
|
|
658
|
+
const beforeAllocations = registry.allocations.length;
|
|
659
|
+
registry.allocations = registry.allocations.filter(
|
|
660
|
+
(allocation) => !isTestSessionKey(allocation.instanceKey) || activeKeys.has(leaseKey(allocation.projectCwd, allocation.instanceKey))
|
|
661
|
+
);
|
|
662
|
+
await writeRegistry(registry);
|
|
663
|
+
return {
|
|
664
|
+
removedLeases: beforeLeases - registry.leases.length,
|
|
665
|
+
removedAllocations: beforeAllocations - registry.allocations.length
|
|
666
|
+
};
|
|
667
|
+
} finally {
|
|
668
|
+
await releaseLock();
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
async reset() {
|
|
672
|
+
const releaseLock = await lock();
|
|
673
|
+
try {
|
|
674
|
+
await writeRegistry({ version: 1, allocations: [], leases: [] });
|
|
675
|
+
} finally {
|
|
676
|
+
await releaseLock();
|
|
677
|
+
}
|
|
678
|
+
},
|
|
645
679
|
async acquirePort(acquireOptions) {
|
|
646
680
|
const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
|
|
647
681
|
if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
|
|
@@ -704,7 +738,7 @@ function createLocalghostRegistry(options = {}) {
|
|
|
704
738
|
};
|
|
705
739
|
}
|
|
706
740
|
|
|
707
|
-
// src/tunnel.ts
|
|
741
|
+
// packages/ghost-tunnel/src/tunnel.ts
|
|
708
742
|
import { domainToASCII } from "url";
|
|
709
743
|
var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
710
744
|
var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
|
|
@@ -1055,275 +1089,7 @@ function constructGhostTunnelUrl(input2) {
|
|
|
1055
1089
|
return url.toString();
|
|
1056
1090
|
}
|
|
1057
1091
|
|
|
1058
|
-
// src/
|
|
1059
|
-
var LOCALGHOST_PROJECT_CONFIG_FILES = [
|
|
1060
|
-
"localghost.config.mjs",
|
|
1061
|
-
"localghost.config.js",
|
|
1062
|
-
"localghost.config.cjs"
|
|
1063
|
-
];
|
|
1064
|
-
function parsePort(value) {
|
|
1065
|
-
if (!value) return void 0;
|
|
1066
|
-
const port = Number.parseInt(value, 10);
|
|
1067
|
-
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
|
|
1068
|
-
}
|
|
1069
|
-
function envPort() {
|
|
1070
|
-
return parsePort(process.env.LOCALGHOST_PORT) ?? parsePort(process.env.VITE_PORT);
|
|
1071
|
-
}
|
|
1072
|
-
function envDynamicPort() {
|
|
1073
|
-
const value = process.env.LOCALGHOST_DYNAMIC_PORT;
|
|
1074
|
-
if (!value) return void 0;
|
|
1075
|
-
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
1076
|
-
}
|
|
1077
|
-
function envHttps() {
|
|
1078
|
-
const value = process.env.LOCALGHOST_HTTPS;
|
|
1079
|
-
if (!value) return void 0;
|
|
1080
|
-
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
1081
|
-
}
|
|
1082
|
-
function getPackageName(cwd) {
|
|
1083
|
-
try {
|
|
1084
|
-
const pkg = JSON.parse(readFileSync5(join6(cwd, "package.json"), "utf8"));
|
|
1085
|
-
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
1086
|
-
} catch {
|
|
1087
|
-
return void 0;
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
function getPackageOwner(cwd) {
|
|
1091
|
-
const packageName = getPackageName(cwd);
|
|
1092
|
-
if (!packageName?.startsWith("@")) return void 0;
|
|
1093
|
-
return packageName.slice(1).split("/")[0];
|
|
1094
|
-
}
|
|
1095
|
-
function getLocalOwner(cwd) {
|
|
1096
|
-
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
1097
|
-
}
|
|
1098
|
-
function getRouteName(primaryHost, fallback) {
|
|
1099
|
-
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
1100
|
-
}
|
|
1101
|
-
function readOptionsFromContext(options) {
|
|
1102
|
-
return {
|
|
1103
|
-
cwd: options.cwd ?? process.cwd(),
|
|
1104
|
-
...options.fileName ? { fileName: options.fileName } : {},
|
|
1105
|
-
...options.configFiles ? { configFiles: options.configFiles } : {},
|
|
1106
|
-
...options.configPattern ? { configPattern: options.configPattern } : {}
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
function withRuntimePort(entries, requestedPort, port) {
|
|
1110
|
-
if (requestedPort === port) return entries;
|
|
1111
|
-
const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
|
|
1112
|
-
if (!hasRequestedPort) return entries;
|
|
1113
|
-
return entries.map((entry) => entry.port === requestedPort ? { ...entry, port, target: `127.0.0.1:${port}` } : entry);
|
|
1114
|
-
}
|
|
1115
|
-
function uniqueHosts(entries) {
|
|
1116
|
-
return [...new Set(entries.map((entry) => entry.host))];
|
|
1117
|
-
}
|
|
1118
|
-
function isAliasableHost(host) {
|
|
1119
|
-
return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
|
|
1120
|
-
}
|
|
1121
|
-
function getDefaultWwwAlias(host) {
|
|
1122
|
-
return isAliasableHost(host) ? `www.${host}` : null;
|
|
1123
|
-
}
|
|
1124
|
-
function addDefaultWwwAliases(entries) {
|
|
1125
|
-
const seen = new Set(entries.map((entry) => entry.host));
|
|
1126
|
-
const aliases = [];
|
|
1127
|
-
for (const entry of entries) {
|
|
1128
|
-
const alias = getDefaultWwwAlias(entry.host);
|
|
1129
|
-
if (alias && !seen.has(alias)) {
|
|
1130
|
-
aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
|
|
1131
|
-
seen.add(alias);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
return [...entries, ...aliases];
|
|
1135
|
-
}
|
|
1136
|
-
function defined(input2) {
|
|
1137
|
-
return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
|
|
1138
|
-
}
|
|
1139
|
-
async function readLocalghostProjectConfig(options = {}) {
|
|
1140
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1141
|
-
if (options.configFile === false) return { config: {} };
|
|
1142
|
-
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
1143
|
-
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync4(candidate));
|
|
1144
|
-
if (!path) return { config: {} };
|
|
1145
|
-
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
1146
|
-
const config = imported.default ?? imported;
|
|
1147
|
-
return { config, path };
|
|
1148
|
-
}
|
|
1149
|
-
async function resolveLocalghostContext(options = {}) {
|
|
1150
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1151
|
-
const projectConfig = await readLocalghostProjectConfig({
|
|
1152
|
-
cwd,
|
|
1153
|
-
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
1154
|
-
});
|
|
1155
|
-
const merged = {
|
|
1156
|
-
...projectConfig.config,
|
|
1157
|
-
...defined(options)
|
|
1158
|
-
};
|
|
1159
|
-
const readOptions = readOptionsFromContext({ ...merged, cwd });
|
|
1160
|
-
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
1161
|
-
const configEntries = readDevHosts(readOptions);
|
|
1162
|
-
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
1163
|
-
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
1164
|
-
const autoRepair = merged.autoRepair ?? true;
|
|
1165
|
-
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
1166
|
-
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
1167
|
-
let port = requestedPort;
|
|
1168
|
-
let releasePort;
|
|
1169
|
-
const reservePort = merged.reservePort ?? false;
|
|
1170
|
-
const instanceKey = merged.instanceKey ?? "run";
|
|
1171
|
-
if (reservePort && dynamicPort) {
|
|
1172
|
-
const registry = createLocalghostRegistry({ cwd, ...merged.registryOwnerToken ? { ownerToken: merged.registryOwnerToken } : {} });
|
|
1173
|
-
const lease = await registry.acquirePort({
|
|
1174
|
-
projectCwd: cwd,
|
|
1175
|
-
instanceKey,
|
|
1176
|
-
startPort: requestedPort,
|
|
1177
|
-
host: probeHost,
|
|
1178
|
-
...options.reservedPorts ? { reservedPorts: options.reservedPorts } : {}
|
|
1179
|
-
});
|
|
1180
|
-
port = lease.port;
|
|
1181
|
-
releasePort = () => registry.releasePort({ projectCwd: cwd, instanceKey });
|
|
1182
|
-
} else if (dynamicPort) {
|
|
1183
|
-
port = await findAvailablePort(requestedPort, { host: probeHost });
|
|
1184
|
-
}
|
|
1185
|
-
const wwwAlias = merged.wwwAlias ?? true;
|
|
1186
|
-
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
1187
|
-
const hosts = uniqueHosts(entries);
|
|
1188
|
-
const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
1189
|
-
const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
|
|
1190
|
-
const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
|
|
1191
|
-
route: getRouteName(primaryHost, projectName),
|
|
1192
|
-
project: projectName,
|
|
1193
|
-
owner: getLocalOwner(cwd)
|
|
1194
|
-
});
|
|
1195
|
-
return {
|
|
1196
|
-
cwd,
|
|
1197
|
-
projectName,
|
|
1198
|
-
readOptions,
|
|
1199
|
-
configPath: resolvedPath.path,
|
|
1200
|
-
configFileName: resolvedPath.fileName,
|
|
1201
|
-
configEntries,
|
|
1202
|
-
entries,
|
|
1203
|
-
hosts,
|
|
1204
|
-
requestedPort,
|
|
1205
|
-
port,
|
|
1206
|
-
dynamicPort,
|
|
1207
|
-
autoRepair,
|
|
1208
|
-
bindHost,
|
|
1209
|
-
primaryHost,
|
|
1210
|
-
https: merged.https ?? envHttps() ?? false,
|
|
1211
|
-
wwwAlias,
|
|
1212
|
-
ghostTunnel,
|
|
1213
|
-
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {},
|
|
1214
|
-
...releasePort ? { releasePort } : {}
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
// src/doctor.ts
|
|
1219
|
-
import { execa as execa2 } from "execa";
|
|
1220
|
-
async function checkCaddy() {
|
|
1221
|
-
try {
|
|
1222
|
-
const result = await execa2("caddy", ["version"], { reject: false });
|
|
1223
|
-
const version = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
1224
|
-
return {
|
|
1225
|
-
found: result.exitCode === 0,
|
|
1226
|
-
...version ? { version } : {},
|
|
1227
|
-
installHint: "brew install caddy"
|
|
1228
|
-
};
|
|
1229
|
-
} catch {
|
|
1230
|
-
return {
|
|
1231
|
-
found: false,
|
|
1232
|
-
installHint: "brew install caddy"
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
async function runDoctor(options = {}) {
|
|
1237
|
-
const caddy = await checkCaddy();
|
|
1238
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1239
|
-
const registry = createLocalghostRegistry({ cwd });
|
|
1240
|
-
const data = await registry.read();
|
|
1241
|
-
const now = Date.now();
|
|
1242
|
-
const staleLeases = data.leases.filter((lease) => lease.expiresAt <= now || !isProcessRunning(lease.pid)).map(({ projectCwd, instanceKey, port, pid }) => ({ projectCwd, instanceKey, port, pid }));
|
|
1243
|
-
const allocationsByPort = /* @__PURE__ */ new Map();
|
|
1244
|
-
for (const allocation of data.allocations) {
|
|
1245
|
-
const projects = allocationsByPort.get(allocation.port) ?? [];
|
|
1246
|
-
projects.push(`${allocation.projectCwd}#${allocation.instanceKey}`);
|
|
1247
|
-
allocationsByPort.set(allocation.port, projects);
|
|
1248
|
-
}
|
|
1249
|
-
const duplicateAllocations = [...allocationsByPort.entries()].filter(([, projects]) => projects.length > 1).map(([port, projects]) => ({ port, projects }));
|
|
1250
|
-
let configured;
|
|
1251
|
-
let available;
|
|
1252
|
-
try {
|
|
1253
|
-
const context = await resolveLocalghostContext({
|
|
1254
|
-
cwd,
|
|
1255
|
-
...options.configFiles ? { configFiles: options.configFiles } : {},
|
|
1256
|
-
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1257
|
-
dynamicPort: false
|
|
1258
|
-
});
|
|
1259
|
-
configured = context.requestedPort;
|
|
1260
|
-
available = await isPortAvailable(configured);
|
|
1261
|
-
} catch {
|
|
1262
|
-
}
|
|
1263
|
-
const currentProjectCwd = canonicalizeLocalghostProjectCwd(cwd);
|
|
1264
|
-
const currentAllocation = data.allocations.find((allocation) => allocation.projectCwd === currentProjectCwd);
|
|
1265
|
-
return {
|
|
1266
|
-
ok: caddy.found && available !== false && staleLeases.length === 0 && duplicateAllocations.length === 0,
|
|
1267
|
-
caddy,
|
|
1268
|
-
ports: {
|
|
1269
|
-
...configured !== void 0 ? { configured } : {},
|
|
1270
|
-
...available !== void 0 ? { available } : {},
|
|
1271
|
-
registryPath: registry.registryPath,
|
|
1272
|
-
staleLeases,
|
|
1273
|
-
duplicateAllocations,
|
|
1274
|
-
...currentAllocation ? {
|
|
1275
|
-
currentAllocation: {
|
|
1276
|
-
projectCwd: currentAllocation.projectCwd,
|
|
1277
|
-
instanceKey: currentAllocation.instanceKey,
|
|
1278
|
-
port: currentAllocation.port
|
|
1279
|
-
}
|
|
1280
|
-
} : {}
|
|
1281
|
-
}
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
// src/env.ts
|
|
1286
|
-
function getProductionReason(env = process.env) {
|
|
1287
|
-
if (env.LOCALGHOST_ENV === "production") return "LOCALGHOST_ENV=production";
|
|
1288
|
-
if (env.NODE_ENV === "production") return "NODE_ENV=production";
|
|
1289
|
-
if (env.VERCEL_ENV === "production") return "VERCEL_ENV=production";
|
|
1290
|
-
if (env.NETLIFY === "true" && env.CONTEXT === "production") return "NETLIFY=true and CONTEXT=production";
|
|
1291
|
-
if (env.CF_PAGES_BRANCH && env.CF_PAGES_BRANCH === env.CF_PAGES_PRODUCTION_BRANCH) {
|
|
1292
|
-
return "CF_PAGES_BRANCH matches CF_PAGES_PRODUCTION_BRANCH";
|
|
1293
|
-
}
|
|
1294
|
-
return null;
|
|
1295
|
-
}
|
|
1296
|
-
function assertLocalDevelopment(command, env = process.env) {
|
|
1297
|
-
const reason = getProductionReason(env);
|
|
1298
|
-
if (!reason) return;
|
|
1299
|
-
throw new Error(`Localghost only runs in local development. Refusing \`${command}\` because ${reason}.`);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// src/ghost-file.ts
|
|
1303
|
-
var LOCALGHOST_GHOST_TUNNEL_FILE = ".ghosttunnel";
|
|
1304
|
-
function toGhostTunnelOptions(options = {}) {
|
|
1305
|
-
const resolved = typeof options === "string" ? { cwd: options } : options;
|
|
1306
|
-
return {
|
|
1307
|
-
...resolved,
|
|
1308
|
-
fileName: resolved.fileName ?? LOCALGHOST_GHOST_TUNNEL_FILE
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
function resolveGhostTunnelPath(options = {}) {
|
|
1312
|
-
return resolveDevHostsPath(toGhostTunnelOptions(options));
|
|
1313
|
-
}
|
|
1314
|
-
function readGhostTunnelEntries(options = {}) {
|
|
1315
|
-
return readDevHosts(toGhostTunnelOptions(options));
|
|
1316
|
-
}
|
|
1317
|
-
function listGhostTunnelEntries(options = {}) {
|
|
1318
|
-
const resolved = resolveGhostTunnelPath(options);
|
|
1319
|
-
if (!resolved.exists) return [];
|
|
1320
|
-
return readGhostTunnelEntries(options);
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
// src/ghost-agent.ts
|
|
1324
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
1325
|
-
|
|
1326
|
-
// src/relay.ts
|
|
1092
|
+
// packages/ghost-tunnel/src/relay.ts
|
|
1327
1093
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
1328
1094
|
import { domainToASCII as domainToASCII2 } from "url";
|
|
1329
1095
|
var DEFAULT_RELAY_ALLOWED_TARGET_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
@@ -1432,7 +1198,73 @@ function stripRelayForwardHeaders(headers) {
|
|
|
1432
1198
|
return stripped;
|
|
1433
1199
|
}
|
|
1434
1200
|
|
|
1435
|
-
//
|
|
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);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// packages/ghost-tunnel/src/ghost-tunnel-store.ts
|
|
1436
1268
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1437
1269
|
function base64Encode(value) {
|
|
1438
1270
|
return value.toString("base64");
|
|
@@ -1564,17 +1396,18 @@ function createRedisGhostTunnelStoreFromEnv(input2 = {}) {
|
|
|
1564
1396
|
});
|
|
1565
1397
|
}
|
|
1566
1398
|
|
|
1567
|
-
// src/ghost-agent.ts
|
|
1399
|
+
// packages/ghost-tunnel/src/ghost-agent.ts
|
|
1400
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1568
1401
|
function isStopped(signal, localSignal) {
|
|
1569
1402
|
return localSignal.aborted || signal?.aborted === true;
|
|
1570
1403
|
}
|
|
1571
1404
|
function wait(ms, signal, localSignal) {
|
|
1572
1405
|
if (isStopped(signal, localSignal)) return Promise.resolve();
|
|
1573
|
-
return new Promise((
|
|
1574
|
-
const timeout = setTimeout(
|
|
1406
|
+
return new Promise((resolve6) => {
|
|
1407
|
+
const timeout = setTimeout(resolve6, ms);
|
|
1575
1408
|
const stop = () => {
|
|
1576
1409
|
clearTimeout(timeout);
|
|
1577
|
-
|
|
1410
|
+
resolve6();
|
|
1578
1411
|
};
|
|
1579
1412
|
signal?.addEventListener("abort", stop, { once: true });
|
|
1580
1413
|
localSignal.addEventListener("abort", stop, { once: true });
|
|
@@ -1712,6 +1545,250 @@ function startGhostTunnelAgent(options) {
|
|
|
1712
1545
|
};
|
|
1713
1546
|
}
|
|
1714
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
|
+
|
|
1715
1792
|
// src/hosts-file.ts
|
|
1716
1793
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
1717
1794
|
import { tmpdir } from "os";
|
|
@@ -1795,19 +1872,19 @@ async function removeSystemHosts(projectName) {
|
|
|
1795
1872
|
}
|
|
1796
1873
|
|
|
1797
1874
|
// src/init.ts
|
|
1798
|
-
import { existsSync as
|
|
1799
|
-
import { dirname as dirname4, join as join8, resolve as
|
|
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";
|
|
1800
1877
|
function detectPackageManager(cwd = process.cwd()) {
|
|
1801
|
-
if (
|
|
1802
|
-
if (
|
|
1803
|
-
if (
|
|
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";
|
|
1804
1881
|
return "npm";
|
|
1805
1882
|
}
|
|
1806
1883
|
function isPnpmWorkspaceRoot(cwd = process.cwd()) {
|
|
1807
|
-
const target =
|
|
1884
|
+
const target = resolve5(cwd);
|
|
1808
1885
|
let current = target;
|
|
1809
1886
|
while (true) {
|
|
1810
|
-
if (
|
|
1887
|
+
if (existsSync6(join8(current, "pnpm-workspace.yaml"))) return current === target;
|
|
1811
1888
|
const parent = dirname4(current);
|
|
1812
1889
|
if (parent === current) return false;
|
|
1813
1890
|
current = parent;
|
|
@@ -1831,7 +1908,7 @@ function renderConfig(options) {
|
|
|
1831
1908
|
}
|
|
1832
1909
|
function readPackageJson2(path) {
|
|
1833
1910
|
try {
|
|
1834
|
-
return JSON.parse(
|
|
1911
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
1835
1912
|
} catch {
|
|
1836
1913
|
return null;
|
|
1837
1914
|
}
|
|
@@ -1899,7 +1976,7 @@ function initLocalghost(options = {}) {
|
|
|
1899
1976
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
1900
1977
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
1901
1978
|
const configPath = join8(cwd, configFile);
|
|
1902
|
-
const configExists =
|
|
1979
|
+
const configExists = existsSync6(configPath);
|
|
1903
1980
|
if (configExists && !options.force) {
|
|
1904
1981
|
return {
|
|
1905
1982
|
configPath,
|
|
@@ -1920,7 +1997,7 @@ function initLocalghost(options = {}) {
|
|
|
1920
1997
|
return {
|
|
1921
1998
|
configPath,
|
|
1922
1999
|
configCreated: true,
|
|
1923
|
-
...
|
|
2000
|
+
...existsSync6(packageJsonPath) ? { packageJsonPath } : {},
|
|
1924
2001
|
packageJsonChanged,
|
|
1925
2002
|
packageManager,
|
|
1926
2003
|
nextSteps: [
|
|
@@ -2149,7 +2226,7 @@ function formatGhostTunnel(config, options = {}) {
|
|
|
2149
2226
|
}
|
|
2150
2227
|
|
|
2151
2228
|
// src/state.ts
|
|
2152
|
-
import { existsSync as
|
|
2229
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2153
2230
|
import { join as join9 } from "path";
|
|
2154
2231
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
2155
2232
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
@@ -2157,7 +2234,7 @@ function getLocalghostStatePath(cwd = process.cwd()) {
|
|
|
2157
2234
|
}
|
|
2158
2235
|
function readLocalghostState(cwd = process.cwd()) {
|
|
2159
2236
|
const path = getLocalghostStatePath(cwd);
|
|
2160
|
-
if (!
|
|
2237
|
+
if (!existsSync7(path)) return null;
|
|
2161
2238
|
return JSON.parse(readTextFile(path));
|
|
2162
2239
|
}
|
|
2163
2240
|
function writeLocalghostState(cwd, state) {
|
|
@@ -2173,11 +2250,11 @@ function patchLocalghostState(cwd, patch) {
|
|
|
2173
2250
|
}
|
|
2174
2251
|
|
|
2175
2252
|
// src/update-check.ts
|
|
2176
|
-
import { existsSync as
|
|
2253
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
2177
2254
|
import { homedir as homedir3 } from "os";
|
|
2178
2255
|
import { dirname as dirname5, join as join10 } from "path";
|
|
2179
2256
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
2180
|
-
var LOCALGHOST_VERSION = "0.
|
|
2257
|
+
var LOCALGHOST_VERSION = "0.6.2";
|
|
2181
2258
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2182
2259
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2183
2260
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -2193,9 +2270,9 @@ function getUpdateCheckCachePath(env = process.env) {
|
|
|
2193
2270
|
return join10(cacheRoot, "localghost", "update-check.json");
|
|
2194
2271
|
}
|
|
2195
2272
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
2196
|
-
if (!
|
|
2273
|
+
if (!existsSync8(path)) return null;
|
|
2197
2274
|
try {
|
|
2198
|
-
return JSON.parse(
|
|
2275
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
2199
2276
|
} catch {
|
|
2200
2277
|
return null;
|
|
2201
2278
|
}
|
|
@@ -2345,7 +2422,7 @@ import { execa as execa4 } from "execa";
|
|
|
2345
2422
|
|
|
2346
2423
|
// src/process.ts
|
|
2347
2424
|
function signalManagedProcessPid(pid, signal, killProcess = (value, processSignal) => process.kill(value, processSignal)) {
|
|
2348
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid
|
|
2425
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false;
|
|
2349
2426
|
try {
|
|
2350
2427
|
killProcess(process.platform === "win32" ? pid : -pid, signal);
|
|
2351
2428
|
return true;
|
|
@@ -2519,7 +2596,7 @@ function getSetupReadiness(options) {
|
|
|
2519
2596
|
}
|
|
2520
2597
|
const hostsPath = getSystemHostsPath();
|
|
2521
2598
|
try {
|
|
2522
|
-
const hosts =
|
|
2599
|
+
const hosts = readFileSync9(hostsPath, "utf8");
|
|
2523
2600
|
const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
|
|
2524
2601
|
if (!hosts.includes(expectedHostsBlock)) {
|
|
2525
2602
|
reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
|
|
@@ -2529,11 +2606,11 @@ function getSetupReadiness(options) {
|
|
|
2529
2606
|
reasons.push(`Could not read ${hostsPath}: ${message}`);
|
|
2530
2607
|
}
|
|
2531
2608
|
if (!options.ignoreCaddyfile) {
|
|
2532
|
-
if (!
|
|
2609
|
+
if (!existsSync9(caddyfilePath)) {
|
|
2533
2610
|
reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
|
|
2534
2611
|
} else {
|
|
2535
2612
|
const expectedCaddyfile = renderCaddyfile(entries, { https });
|
|
2536
|
-
const currentCaddyfile =
|
|
2613
|
+
const currentCaddyfile = readFileSync9(caddyfilePath, "utf8");
|
|
2537
2614
|
if (currentCaddyfile !== expectedCaddyfile) {
|
|
2538
2615
|
reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
|
|
2539
2616
|
}
|
|
@@ -2578,7 +2655,7 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
2578
2655
|
});
|
|
2579
2656
|
}
|
|
2580
2657
|
function wait2(ms) {
|
|
2581
|
-
return new Promise((
|
|
2658
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
2582
2659
|
}
|
|
2583
2660
|
async function runTrust(cwd, caddyfilePath) {
|
|
2584
2661
|
await wait2(350);
|
|
@@ -2697,10 +2774,13 @@ function registerSignalShutdown(stop) {
|
|
|
2697
2774
|
};
|
|
2698
2775
|
process.once("SIGINT", request);
|
|
2699
2776
|
process.once("SIGTERM", request);
|
|
2700
|
-
return
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2777
|
+
return {
|
|
2778
|
+
wasRequested: () => requested,
|
|
2779
|
+
finish: () => {
|
|
2780
|
+
process.off("SIGINT", request);
|
|
2781
|
+
process.off("SIGTERM", request);
|
|
2782
|
+
if (!requested) stop();
|
|
2783
|
+
}
|
|
2704
2784
|
};
|
|
2705
2785
|
}
|
|
2706
2786
|
async function resolveServiceRuntimeEntries(services, dynamicPort, projectCwd) {
|
|
@@ -2752,11 +2832,22 @@ async function waitForPortsToBeAvailable(entries, timeoutMs = 1e4) {
|
|
|
2752
2832
|
const deadline = Date.now() + timeoutMs;
|
|
2753
2833
|
const ports = [...new Set(entries.map((entry) => entry.port))];
|
|
2754
2834
|
while (Date.now() < deadline) {
|
|
2755
|
-
const
|
|
2756
|
-
if (
|
|
2835
|
+
const availability2 = await Promise.all(ports.map((port) => isPortAvailable(port)));
|
|
2836
|
+
if (availability2.every(Boolean)) return { available: true, blocked: [] };
|
|
2757
2837
|
await wait2(50);
|
|
2758
2838
|
}
|
|
2759
|
-
|
|
2839
|
+
const availability = await Promise.all(entries.map(async (entry) => ({
|
|
2840
|
+
entry,
|
|
2841
|
+
available: await isPortAvailable(entry.port)
|
|
2842
|
+
})));
|
|
2843
|
+
const blocked = availability.filter(({ available }) => !available).map(({ entry }) => entry);
|
|
2844
|
+
return { available: blocked.length === 0, blocked };
|
|
2845
|
+
}
|
|
2846
|
+
function formatBlockedPorts(entries) {
|
|
2847
|
+
return [...new Map(entries.map((entry) => [
|
|
2848
|
+
`${entry.host}:${entry.port}`,
|
|
2849
|
+
`${entry.host} (port ${entry.port})`
|
|
2850
|
+
])).values()].join(", ");
|
|
2760
2851
|
}
|
|
2761
2852
|
async function waitForProcessShutdown(processes, forceStop, timeoutMs = 1e4) {
|
|
2762
2853
|
const settled = Promise.allSettled(processes);
|
|
@@ -2847,7 +2938,7 @@ async function runDetectedServices(options) {
|
|
|
2847
2938
|
}
|
|
2848
2939
|
signalManagedProcess(caddy, signal);
|
|
2849
2940
|
};
|
|
2850
|
-
const
|
|
2941
|
+
const shutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
|
|
2851
2942
|
try {
|
|
2852
2943
|
const ready = await Promise.race([
|
|
2853
2944
|
waitForServicePorts(entries),
|
|
@@ -2859,15 +2950,18 @@ async function runDetectedServices(options) {
|
|
|
2859
2950
|
}
|
|
2860
2951
|
await processExit;
|
|
2861
2952
|
} finally {
|
|
2862
|
-
|
|
2953
|
+
shutdown.finish();
|
|
2863
2954
|
await waitForProcessShutdown(
|
|
2864
2955
|
[caddyExit, ...children],
|
|
2865
2956
|
stopManaged
|
|
2866
2957
|
);
|
|
2867
|
-
|
|
2868
|
-
|
|
2958
|
+
const drained = await waitForPortsToBeAvailable(entries);
|
|
2959
|
+
if (!drained.available && !shutdown.wasRequested()) {
|
|
2960
|
+
console.warn(`Localghost: timed out waiting for service ports to be released: ${formatBlockedPorts(drained.blocked)}.`);
|
|
2869
2961
|
stopManaged("SIGTERM");
|
|
2870
|
-
|
|
2962
|
+
const terminated = await waitForPortsToBeAvailable(entries, 2e3);
|
|
2963
|
+
if (!terminated.available) {
|
|
2964
|
+
console.warn(`Localghost: service ports still occupied after termination: ${formatBlockedPorts(terminated.blocked)}.`);
|
|
2871
2965
|
stopManaged("SIGKILL");
|
|
2872
2966
|
}
|
|
2873
2967
|
}
|
|
@@ -3193,13 +3287,13 @@ program.command("reset").description("Remove Localghost setup state without dele
|
|
|
3193
3287
|
const statePath = getLocalghostStatePath(options.cwd);
|
|
3194
3288
|
explainHostsPassword();
|
|
3195
3289
|
const hostsResult = await removeSystemHosts(projectName);
|
|
3196
|
-
if (
|
|
3290
|
+
if (existsSync9(caddyfilePath)) {
|
|
3197
3291
|
unlinkSync(caddyfilePath);
|
|
3198
3292
|
console.log(`Removed ${caddyfilePath}`);
|
|
3199
3293
|
} else {
|
|
3200
3294
|
console.log(`${caddyfilePath} was not present`);
|
|
3201
3295
|
}
|
|
3202
|
-
if (
|
|
3296
|
+
if (existsSync9(statePath)) {
|
|
3203
3297
|
unlinkSync(statePath);
|
|
3204
3298
|
console.log(`Removed ${statePath}`);
|
|
3205
3299
|
} else {
|
|
@@ -3220,7 +3314,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
3220
3314
|
const hostsResult = await removeSystemHosts(projectName);
|
|
3221
3315
|
const caddyfilePath = getCaddyfilePath(options.cwd);
|
|
3222
3316
|
let caddyfileRemoved = false;
|
|
3223
|
-
if (options.removeCaddyfile &&
|
|
3317
|
+
if (options.removeCaddyfile && existsSync9(caddyfilePath)) {
|
|
3224
3318
|
unlinkSync(caddyfilePath);
|
|
3225
3319
|
caddyfileRemoved = true;
|
|
3226
3320
|
}
|
|
@@ -3245,6 +3339,15 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
3245
3339
|
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
3246
3340
|
console.log(`State ${statePath}`);
|
|
3247
3341
|
});
|
|
3342
|
+
program.command("ports").description("Manage Localghost port registry state").addCommand(new Command("prune").description("Remove expired or dead port leases").option("--cwd <path>", "Project directory", process.cwd()).option("--test-only", "Remove only stale test-session leases and records").option("--json", "Print raw JSON").action(async (options) => {
|
|
3343
|
+
const result = options.testOnly ? await createLocalghostRegistry({ cwd: options.cwd }).pruneTestSessions() : await createLocalghostRegistry({ cwd: options.cwd }).prune();
|
|
3344
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
3345
|
+
else console.log(`Pruned ${result.removedLeases} stale registry lease${result.removedLeases === 1 ? "" : "s"}${"removedAllocations" in result ? ` and ${result.removedAllocations} test allocation${result.removedAllocations === 1 ? "" : "s"}` : ""}.`);
|
|
3346
|
+
})).addCommand(new Command("reset").description("Clear all remembered allocations and leases").option("--cwd <path>", "Project directory", process.cwd()).option("--yes", "Confirm the destructive registry reset").action(async (options) => {
|
|
3347
|
+
if (!options.yes) throw new Error("Resetting the port registry is destructive. Re-run with --yes.");
|
|
3348
|
+
await createLocalghostRegistry({ cwd: options.cwd }).reset();
|
|
3349
|
+
console.log("Localghost port registry reset.");
|
|
3350
|
+
}));
|
|
3248
3351
|
program.command("test").description("Run a test command with an isolated Localghost port").option("--cwd <path>", "Project directory", process.cwd()).option("--instance <name>", "Parallel test instance name", String(process.pid)).option("--port <number>", "Initial test port", parsePort2, 5173).option("--lease-ttl <milliseconds>", "Lease lifetime before heartbeat renewal", Number, 30 * 60 * 1e3).argument("<command...>", "Command to run after --").action(async (command, options) => {
|
|
3249
3352
|
const [binary, ...args] = command;
|
|
3250
3353
|
if (!binary) throw new Error("Missing command. Use: localghost test -- <command>");
|
|
@@ -3524,19 +3627,22 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
3524
3627
|
signalManagedProcess(child, signal);
|
|
3525
3628
|
signalManagedProcess(caddy, signal);
|
|
3526
3629
|
};
|
|
3527
|
-
const
|
|
3630
|
+
const shutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
|
|
3528
3631
|
try {
|
|
3529
3632
|
await Promise.race([child, caddyExit]);
|
|
3530
3633
|
} finally {
|
|
3531
|
-
|
|
3634
|
+
shutdown.finish();
|
|
3532
3635
|
await waitForProcessShutdown(
|
|
3533
3636
|
[child, caddyExit],
|
|
3534
3637
|
stopManaged
|
|
3535
3638
|
);
|
|
3536
|
-
|
|
3537
|
-
|
|
3639
|
+
const drained = await waitForPortsToBeAvailable(context.entries);
|
|
3640
|
+
if (!drained.available && !shutdown.wasRequested()) {
|
|
3641
|
+
console.warn(`Localghost: timed out waiting for service ports to be released: ${formatBlockedPorts(drained.blocked)}.`);
|
|
3538
3642
|
stopManaged("SIGTERM");
|
|
3539
|
-
|
|
3643
|
+
const terminated = await waitForPortsToBeAvailable(context.entries, 2e3);
|
|
3644
|
+
if (!terminated.available) {
|
|
3645
|
+
console.warn(`Localghost: service ports still occupied after termination: ${formatBlockedPorts(terminated.blocked)}.`);
|
|
3540
3646
|
stopManaged("SIGKILL");
|
|
3541
3647
|
}
|
|
3542
3648
|
}
|