@hamedb89/localghost 0.1.10 → 0.1.13
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 +524 -34
- package/dist/cli.js +995 -93
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +343 -93
- package/dist/index.js +2051 -929
- package/dist/index.js.map +1 -1
- package/dist/{tunnel-DzfLXZ8O.d.ts → tunnel-BA52DD9e.d.ts} +53 -1
- package/dist/vite.d.ts +2 -1
- package/dist/vite.js +126 -17
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +28 -25
- package/docs/ghost-tunnel.md +206 -20
- package/docs/github.md +4 -4
- package/docs/localghost.1.md +43 -7
- package/package.json +2 -1
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 existsSync8, readFileSync as readFileSync8, unlinkSync } from "fs";
|
|
5
5
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/activity.ts
|
|
@@ -254,6 +254,17 @@ function sanitizeProjectName(value) {
|
|
|
254
254
|
return projectName || "app";
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// src/brand.ts
|
|
258
|
+
function renderLocalghostBanner() {
|
|
259
|
+
return [
|
|
260
|
+
" .-.",
|
|
261
|
+
" (o o) LOCALGHOST",
|
|
262
|
+
" | O \\ friendly local domains",
|
|
263
|
+
" \\ \\",
|
|
264
|
+
" `~~~'"
|
|
265
|
+
].join("\n");
|
|
266
|
+
}
|
|
267
|
+
|
|
257
268
|
// src/caddy.ts
|
|
258
269
|
import { dirname as dirname3, join as join3 } from "path";
|
|
259
270
|
import { execa } from "execa";
|
|
@@ -271,6 +282,12 @@ function writeTextFile(path, value) {
|
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
// src/caddy.ts
|
|
285
|
+
function shouldShowCaddyLogs() {
|
|
286
|
+
return ["1", "true", "yes", "on"].includes((process.env.LOCALGHOST_CADDY_VERBOSE ?? "").toLowerCase());
|
|
287
|
+
}
|
|
288
|
+
function caddyStdio() {
|
|
289
|
+
return shouldShowCaddyLogs() ? "inherit" : "pipe";
|
|
290
|
+
}
|
|
274
291
|
function groupByPort(entries) {
|
|
275
292
|
const groups = /* @__PURE__ */ new Map();
|
|
276
293
|
for (const entry of entries) {
|
|
@@ -289,7 +306,7 @@ function renderCaddyfile(entries, options = {}) {
|
|
|
289
306
|
const blocks = [...groups.entries()].sort(([leftPort], [rightPort]) => leftPort - rightPort).map(([port, group]) => {
|
|
290
307
|
const hosts = group.map((entry) => https ? entry.host : `http://${entry.host}`).sort().join(", ");
|
|
291
308
|
return `${hosts} {
|
|
292
|
-
|
|
309
|
+
reverse_proxy 127.0.0.1:${port}
|
|
293
310
|
}`;
|
|
294
311
|
});
|
|
295
312
|
const globalOptions = https ? `{
|
|
@@ -308,13 +325,13 @@ async function writeCaddyfile(entries, cwd = process.cwd(), options = {}) {
|
|
|
308
325
|
async function validateCaddyfile(path) {
|
|
309
326
|
await execa("caddy", ["validate", "--config", path], {
|
|
310
327
|
cwd: dirname3(path),
|
|
311
|
-
stdio:
|
|
328
|
+
stdio: caddyStdio()
|
|
312
329
|
});
|
|
313
330
|
}
|
|
314
331
|
function startCaddy(path) {
|
|
315
332
|
return execa("caddy", ["run", "--config", path], {
|
|
316
333
|
cwd: dirname3(path),
|
|
317
|
-
stdio:
|
|
334
|
+
stdio: caddyStdio()
|
|
318
335
|
});
|
|
319
336
|
}
|
|
320
337
|
async function trustCaddy(path) {
|
|
@@ -324,21 +341,132 @@ async function trustCaddy(path) {
|
|
|
324
341
|
});
|
|
325
342
|
}
|
|
326
343
|
|
|
327
|
-
// src/
|
|
344
|
+
// src/command.ts
|
|
328
345
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
329
|
-
import { join as join4 } from "path";
|
|
346
|
+
import { isAbsolute, join as join4, relative, resolve as resolve2 } from "path";
|
|
347
|
+
function readPackageJson(cwd) {
|
|
348
|
+
const path = join4(cwd, "package.json");
|
|
349
|
+
if (!existsSync3(path)) {
|
|
350
|
+
throw new Error(`No package.json found in ${cwd}. Pass an explicit command with \`localghost run -- <command>\`.`);
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
354
|
+
} catch {
|
|
355
|
+
throw new Error(`Could not parse ${path}.`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function detectDevPackageManager(cwd, packageManager) {
|
|
359
|
+
if (typeof packageManager === "string") {
|
|
360
|
+
const name = packageManager.split("@")[0];
|
|
361
|
+
if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
|
|
362
|
+
}
|
|
363
|
+
if (existsSync3(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
364
|
+
if (existsSync3(join4(cwd, "yarn.lock"))) return "yarn";
|
|
365
|
+
if (existsSync3(join4(cwd, "bun.lock")) || existsSync3(join4(cwd, "bun.lockb"))) return "bun";
|
|
366
|
+
return "npm";
|
|
367
|
+
}
|
|
368
|
+
function scriptCommand(packageManager, script) {
|
|
369
|
+
if (packageManager === "yarn") return ["yarn", script];
|
|
370
|
+
return [packageManager, "run", script];
|
|
371
|
+
}
|
|
372
|
+
function invokesLocalghost(script) {
|
|
373
|
+
return /(^|[\s;&|])(?:npm\s+exec\s+|pnpm\s+exec\s+|bunx\s+|npx\s+)?localghost(?:\s|$)/.test(script);
|
|
374
|
+
}
|
|
375
|
+
function detectDevCommand(options = {}) {
|
|
376
|
+
const cwd = options.cwd ?? process.cwd();
|
|
377
|
+
if (options.command) {
|
|
378
|
+
if (options.command.length === 0 || options.command.some((part) => typeof part !== "string" || part.length === 0)) {
|
|
379
|
+
throw new Error("localghost.config.mjs command must be a non-empty array of strings.");
|
|
380
|
+
}
|
|
381
|
+
if (invokesLocalghost(options.command.join(" "))) {
|
|
382
|
+
throw new Error("localghost.config.mjs command cannot invoke Localghost recursively.");
|
|
383
|
+
}
|
|
384
|
+
return { command: [...options.command], source: "config" };
|
|
385
|
+
}
|
|
386
|
+
const pkg = readPackageJson(cwd);
|
|
387
|
+
const scripts = typeof pkg.scripts === "object" && pkg.scripts ? pkg.scripts : {};
|
|
388
|
+
const packageManager = detectDevPackageManager(cwd, pkg.packageManager);
|
|
389
|
+
for (const script of ["dev:raw", "dev"]) {
|
|
390
|
+
const value = scripts[script];
|
|
391
|
+
if (typeof value !== "string" || invokesLocalghost(value)) continue;
|
|
392
|
+
return {
|
|
393
|
+
command: scriptCommand(packageManager, script),
|
|
394
|
+
source: "script",
|
|
395
|
+
packageManager,
|
|
396
|
+
script
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
throw new Error([
|
|
400
|
+
`Could not detect a safe development command in ${join4(cwd, "package.json")}.`,
|
|
401
|
+
"Add a non-recursive dev or dev:raw script, configure command in localghost.config.mjs,",
|
|
402
|
+
"or pass an explicit command with `localghost run -- <command>`."
|
|
403
|
+
].join(" "));
|
|
404
|
+
}
|
|
405
|
+
function formatDetectedDevCommand(detected) {
|
|
406
|
+
const command = detected.command.map((part) => /^[A-Za-z0-9_./:@=-]+$/.test(part) ? part : JSON.stringify(part)).join(" ");
|
|
407
|
+
const source = detected.source === "config" ? "localghost.config.mjs" : `package.json#scripts.${detected.script}`;
|
|
408
|
+
return `${command} (${source})`;
|
|
409
|
+
}
|
|
410
|
+
function assertServicePath(root, serviceCwd, name) {
|
|
411
|
+
const cwd = resolve2(root, serviceCwd);
|
|
412
|
+
const relativeCwd = relative(root, cwd);
|
|
413
|
+
if (isAbsolute(relativeCwd) || relativeCwd === ".." || relativeCwd.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
|
|
414
|
+
throw new Error(`Service ${name} cwd must stay inside the project root.`);
|
|
415
|
+
}
|
|
416
|
+
return { cwd, relativeCwd: relativeCwd || "." };
|
|
417
|
+
}
|
|
418
|
+
function detectDevServices(options) {
|
|
419
|
+
const root = options.cwd ?? process.cwd();
|
|
420
|
+
if (options.services.length === 0) throw new Error("services must contain at least one service.");
|
|
421
|
+
const names = /* @__PURE__ */ new Set();
|
|
422
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
423
|
+
return options.services.map((service, index) => {
|
|
424
|
+
if (!service || typeof service !== "object") throw new Error(`Service at index ${index} must be an object.`);
|
|
425
|
+
if (!service.name || names.has(service.name)) throw new Error(`Service name must be unique: ${service.name || `<index ${index}>`}.`);
|
|
426
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(service.name)) throw new Error(`Invalid service name: ${service.name}.`);
|
|
427
|
+
if (!service.host || hosts.has(service.host)) throw new Error(`Service host must be unique: ${service.host || `<index ${index}>`}.`);
|
|
428
|
+
if (!Number.isInteger(service.port) || service.port < 1 || service.port > 65535) {
|
|
429
|
+
throw new Error(`Invalid port for service ${service.name}: ${service.port}.`);
|
|
430
|
+
}
|
|
431
|
+
names.add(service.name);
|
|
432
|
+
hosts.add(service.host);
|
|
433
|
+
const path = assertServicePath(root, service.cwd, service.name);
|
|
434
|
+
const detected = detectDevCommand({
|
|
435
|
+
cwd: path.cwd,
|
|
436
|
+
...service.command ? { command: service.command } : {}
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
name: service.name,
|
|
440
|
+
...path,
|
|
441
|
+
host: service.host,
|
|
442
|
+
requestedPort: service.port,
|
|
443
|
+
command: detected.command,
|
|
444
|
+
commandSource: detected.source
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function formatDetectedDevServices(services) {
|
|
449
|
+
return [
|
|
450
|
+
`Localghost detected ${services.length} services:`,
|
|
451
|
+
...services.map((service) => `${service.name}: ${service.command.map((part) => /^[A-Za-z0-9_./:@=-]+$/.test(part) ? part : JSON.stringify(part)).join(" ")} (${service.relativeCwd}, ${service.host} -> ${service.requestedPort})`)
|
|
452
|
+
].join("\n");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/context.ts
|
|
456
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
457
|
+
import { join as join5 } from "path";
|
|
330
458
|
import { pathToFileURL } from "url";
|
|
331
459
|
|
|
332
460
|
// src/port.ts
|
|
333
461
|
import { createServer } from "net";
|
|
334
462
|
async function isPortAvailable(port, host = "127.0.0.1") {
|
|
335
|
-
return new Promise((
|
|
463
|
+
return new Promise((resolve3) => {
|
|
336
464
|
const server = createServer();
|
|
337
465
|
server.once("error", () => {
|
|
338
|
-
|
|
466
|
+
resolve3(false);
|
|
339
467
|
});
|
|
340
468
|
server.once("listening", () => {
|
|
341
|
-
server.close(() =>
|
|
469
|
+
server.close(() => resolve3(true));
|
|
342
470
|
});
|
|
343
471
|
server.listen(port, host);
|
|
344
472
|
});
|
|
@@ -361,6 +489,17 @@ var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
|
361
489
|
var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
|
|
362
490
|
var DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR = "-";
|
|
363
491
|
var DEFAULT_GHOST_TUNNEL_MODE = "manual";
|
|
492
|
+
var DEFAULT_GHOST_TUNNEL_ADAPTER_STRATEGY = "same-project";
|
|
493
|
+
var DEFAULT_GHOST_TUNNEL_TRANSPORT_KIND = "none";
|
|
494
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_STORE_PROVIDER = "vercel-redis";
|
|
495
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_STORE_ENV = "auto";
|
|
496
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_STORE_NAMESPACE = "localghost";
|
|
497
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_WAIT_MS = 25e3;
|
|
498
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_POLL_INTERVAL_MS = 250;
|
|
499
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_ROUTE_TTL_SECONDS = 30;
|
|
500
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_REQUEST_TTL_SECONDS = 60;
|
|
501
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
502
|
+
var DEFAULT_GHOST_TUNNEL_TUNNEL_MAX_RESPONSE_BODY_BYTES = 5 * 1024 * 1024;
|
|
364
503
|
function isResolvedGhostTunnelConfig(value) {
|
|
365
504
|
return typeof value === "object" && value !== null && "enabled" in value;
|
|
366
505
|
}
|
|
@@ -410,6 +549,86 @@ function normalizeDomains(domains) {
|
|
|
410
549
|
function parseGhostTunnelMode(value) {
|
|
411
550
|
return value ?? DEFAULT_GHOST_TUNNEL_MODE;
|
|
412
551
|
}
|
|
552
|
+
function parseGhostTunnelAdapterStrategy(value) {
|
|
553
|
+
if (typeof value === "undefined") return DEFAULT_GHOST_TUNNEL_ADAPTER_STRATEGY;
|
|
554
|
+
if (value === "same-project" || value === "separate-relay") return value;
|
|
555
|
+
throw new Error(`Unsupported ghost tunnel adapter strategy: ${String(value)}`);
|
|
556
|
+
}
|
|
557
|
+
function parseGhostTunnelTransportKind(value) {
|
|
558
|
+
if (typeof value === "undefined") return DEFAULT_GHOST_TUNNEL_TRANSPORT_KIND;
|
|
559
|
+
if (value === "none" || value === "ip" || value === "tunnel") return value;
|
|
560
|
+
throw new Error(`Unsupported ghost tunnel transport: ${String(value)}`);
|
|
561
|
+
}
|
|
562
|
+
function parsePositiveInteger(value, fallback, name) {
|
|
563
|
+
if (typeof value === "undefined") return fallback;
|
|
564
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
565
|
+
throw new Error(`Invalid ghost tunnel ${name}: ${value}`);
|
|
566
|
+
}
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
function parseTunnelStoreProvider(value) {
|
|
570
|
+
if (typeof value === "undefined") return DEFAULT_GHOST_TUNNEL_TUNNEL_STORE_PROVIDER;
|
|
571
|
+
if (value === "vercel-redis" || value === "redis") return value;
|
|
572
|
+
throw new Error(`Unsupported ghost tunnel tunnel store provider: ${String(value)}`);
|
|
573
|
+
}
|
|
574
|
+
function parseTunnelStoreEnv(value) {
|
|
575
|
+
if (typeof value === "undefined") return DEFAULT_GHOST_TUNNEL_TUNNEL_STORE_ENV;
|
|
576
|
+
if (value === "auto") return value;
|
|
577
|
+
throw new Error(`Unsupported ghost tunnel tunnel store env: ${String(value)}`);
|
|
578
|
+
}
|
|
579
|
+
function parseTunnelStoreNamespace(value) {
|
|
580
|
+
const namespace = value ?? DEFAULT_GHOST_TUNNEL_TUNNEL_STORE_NAMESPACE;
|
|
581
|
+
if (!/^[a-z][a-z0-9:_-]{0,63}$/i.test(namespace)) {
|
|
582
|
+
throw new Error(`Invalid ghost tunnel tunnel store namespace: ${namespace}`);
|
|
583
|
+
}
|
|
584
|
+
return namespace;
|
|
585
|
+
}
|
|
586
|
+
function resolveGhostTunnelAdapter(input2) {
|
|
587
|
+
if (!input2) return void 0;
|
|
588
|
+
const provider = typeof input2 === "string" ? input2 : input2.provider;
|
|
589
|
+
if (provider !== "vercel") {
|
|
590
|
+
throw new Error(`Unsupported ghost tunnel adapter provider: ${String(provider)}`);
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
provider,
|
|
594
|
+
strategy: typeof input2 === "string" ? DEFAULT_GHOST_TUNNEL_ADAPTER_STRATEGY : parseGhostTunnelAdapterStrategy(input2.strategy)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function getLegacyGhostTunnelTransport(input2) {
|
|
598
|
+
if (!input2 || typeof input2 === "string" || !("transport" in input2)) return void 0;
|
|
599
|
+
return input2.transport;
|
|
600
|
+
}
|
|
601
|
+
function resolveGhostTunnelTransport(input2) {
|
|
602
|
+
if (!input2) {
|
|
603
|
+
return { kind: "none" };
|
|
604
|
+
}
|
|
605
|
+
const kind = typeof input2 === "string" ? parseGhostTunnelTransportKind(input2) : parseGhostTunnelTransportKind(input2.kind);
|
|
606
|
+
if (kind === "ip") {
|
|
607
|
+
return {
|
|
608
|
+
kind,
|
|
609
|
+
allowPrivateNetworkAddress: typeof input2 === "string" ? false : input2.kind === "ip" ? input2.allowPrivateNetworkAddress ?? false : false
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
if (kind === "tunnel") {
|
|
613
|
+
const config = typeof input2 === "string" || input2.kind !== "tunnel" ? void 0 : input2;
|
|
614
|
+
const store = config?.store ?? {};
|
|
615
|
+
return {
|
|
616
|
+
kind,
|
|
617
|
+
store: {
|
|
618
|
+
provider: parseTunnelStoreProvider(store.provider),
|
|
619
|
+
env: parseTunnelStoreEnv(store.env),
|
|
620
|
+
namespace: parseTunnelStoreNamespace(store.namespace)
|
|
621
|
+
},
|
|
622
|
+
waitMs: parsePositiveInteger(config?.waitMs, DEFAULT_GHOST_TUNNEL_TUNNEL_WAIT_MS, "tunnel waitMs"),
|
|
623
|
+
pollIntervalMs: parsePositiveInteger(config?.pollIntervalMs, DEFAULT_GHOST_TUNNEL_TUNNEL_POLL_INTERVAL_MS, "tunnel pollIntervalMs"),
|
|
624
|
+
routeTtlSeconds: parsePositiveInteger(config?.routeTtlSeconds, DEFAULT_GHOST_TUNNEL_TUNNEL_ROUTE_TTL_SECONDS, "tunnel routeTtlSeconds"),
|
|
625
|
+
requestTtlSeconds: parsePositiveInteger(config?.requestTtlSeconds, DEFAULT_GHOST_TUNNEL_TUNNEL_REQUEST_TTL_SECONDS, "tunnel requestTtlSeconds"),
|
|
626
|
+
maxRequestBodyBytes: parsePositiveInteger(config?.maxRequestBodyBytes, DEFAULT_GHOST_TUNNEL_TUNNEL_MAX_REQUEST_BODY_BYTES, "tunnel maxRequestBodyBytes"),
|
|
627
|
+
maxResponseBodyBytes: parsePositiveInteger(config?.maxResponseBodyBytes, DEFAULT_GHOST_TUNNEL_TUNNEL_MAX_RESPONSE_BODY_BYTES, "tunnel maxResponseBodyBytes")
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return { kind: "none" };
|
|
631
|
+
}
|
|
413
632
|
function resolveNamespaceConfig(options) {
|
|
414
633
|
const tags = isNamespaceTagList(options) ? [...options] : [...options?.tags ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS];
|
|
415
634
|
let separator = DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
@@ -495,11 +714,11 @@ function getDisplayValues(input2) {
|
|
|
495
714
|
...input2.values
|
|
496
715
|
};
|
|
497
716
|
}
|
|
498
|
-
function getDisplayDefaults(
|
|
499
|
-
return
|
|
717
|
+
function getDisplayDefaults(defaults) {
|
|
718
|
+
return defaults;
|
|
500
719
|
}
|
|
501
720
|
function createDisplayUrl(config, defaults, domain) {
|
|
502
|
-
const input2 = getPreviewDefaults(config.preview, getDisplayDefaults(
|
|
721
|
+
const input2 = getPreviewDefaults(config.preview, getDisplayDefaults(defaults));
|
|
503
722
|
const protocol = input2.protocol ?? "https";
|
|
504
723
|
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
|
|
505
724
|
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
|
|
@@ -508,7 +727,7 @@ function createDisplayUrl(config, defaults, domain) {
|
|
|
508
727
|
return `${url}${input2.path.replace(/^\/+/, "")}`;
|
|
509
728
|
}
|
|
510
729
|
function createDisplayUrls(config, defaults) {
|
|
511
|
-
const displayDefaults = getDisplayDefaults(
|
|
730
|
+
const displayDefaults = getDisplayDefaults(defaults);
|
|
512
731
|
const domains = config.domains.length > 0 ? config.domains : displayDefaults?.domain ? [displayDefaults.domain] : [];
|
|
513
732
|
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, displayDefaults, domain)) : [createDisplayUrl(config, displayDefaults)];
|
|
514
733
|
return [...new Set(urls)];
|
|
@@ -538,7 +757,8 @@ function resolveGhostTunnelConfig(options, defaults) {
|
|
|
538
757
|
namespace: resolveNamespaceConfig(void 0),
|
|
539
758
|
displayUrls: [],
|
|
540
759
|
requireHttps: true,
|
|
541
|
-
requireAuth: true
|
|
760
|
+
requireAuth: true,
|
|
761
|
+
transport: resolveGhostTunnelTransport(void 0)
|
|
542
762
|
};
|
|
543
763
|
}
|
|
544
764
|
const config = typeof options === "string" ? { mode: options } : options;
|
|
@@ -546,6 +766,8 @@ function resolveGhostTunnelConfig(options, defaults) {
|
|
|
546
766
|
assertValidSubdomain(subdomain);
|
|
547
767
|
const domains = normalizeDomains(config.domains);
|
|
548
768
|
const enabled = config.enabled ?? true;
|
|
769
|
+
const adapter = resolveGhostTunnelAdapter(config.adapter);
|
|
770
|
+
const transport = resolveGhostTunnelTransport(config.transport ?? getLegacyGhostTunnelTransport(config.adapter));
|
|
549
771
|
const resolved = {
|
|
550
772
|
enabled,
|
|
551
773
|
mode: parseGhostTunnelMode(config.mode),
|
|
@@ -555,7 +777,9 @@ function resolveGhostTunnelConfig(options, defaults) {
|
|
|
555
777
|
...config.preview ? { preview: config.preview } : {},
|
|
556
778
|
displayUrls: [],
|
|
557
779
|
requireHttps: config.requireHttps ?? true,
|
|
558
|
-
requireAuth: config.requireAuth ?? true
|
|
780
|
+
requireAuth: config.requireAuth ?? true,
|
|
781
|
+
transport,
|
|
782
|
+
...adapter ? { adapter } : {}
|
|
559
783
|
};
|
|
560
784
|
if (!enabled) {
|
|
561
785
|
return resolved;
|
|
@@ -636,7 +860,7 @@ function envHttps() {
|
|
|
636
860
|
}
|
|
637
861
|
function getPackageName(cwd) {
|
|
638
862
|
try {
|
|
639
|
-
const pkg = JSON.parse(
|
|
863
|
+
const pkg = JSON.parse(readFileSync5(join5(cwd, "package.json"), "utf8"));
|
|
640
864
|
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
641
865
|
} catch {
|
|
642
866
|
return void 0;
|
|
@@ -665,7 +889,7 @@ function withRuntimePort(entries, requestedPort, port) {
|
|
|
665
889
|
if (requestedPort === port) return entries;
|
|
666
890
|
const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
|
|
667
891
|
if (!hasRequestedPort) return entries;
|
|
668
|
-
return entries.map((entry) => entry.port === requestedPort ? { ...entry, port } : entry);
|
|
892
|
+
return entries.map((entry) => entry.port === requestedPort ? { ...entry, port, target: `127.0.0.1:${port}` } : entry);
|
|
669
893
|
}
|
|
670
894
|
function uniqueHosts(entries) {
|
|
671
895
|
return [...new Set(entries.map((entry) => entry.host))];
|
|
@@ -695,7 +919,7 @@ async function readLocalghostProjectConfig(options = {}) {
|
|
|
695
919
|
const cwd = options.cwd ?? process.cwd();
|
|
696
920
|
if (options.configFile === false) return { config: {} };
|
|
697
921
|
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
698
|
-
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) =>
|
|
922
|
+
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync4(candidate));
|
|
699
923
|
if (!path) return { config: {} };
|
|
700
924
|
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
701
925
|
const config = imported.default ?? imported;
|
|
@@ -716,6 +940,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
716
940
|
const configEntries = readDevHosts(readOptions);
|
|
717
941
|
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
718
942
|
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
943
|
+
const autoRepair = merged.autoRepair ?? true;
|
|
719
944
|
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
720
945
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
721
946
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
@@ -741,6 +966,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
741
966
|
requestedPort,
|
|
742
967
|
port,
|
|
743
968
|
dynamicPort,
|
|
969
|
+
autoRepair,
|
|
744
970
|
bindHost,
|
|
745
971
|
primaryHost,
|
|
746
972
|
https: merged.https ?? envHttps() ?? false,
|
|
@@ -793,10 +1019,418 @@ function assertLocalDevelopment(command, env = process.env) {
|
|
|
793
1019
|
throw new Error(`Localghost only runs in local development. Refusing \`${command}\` because ${reason}.`);
|
|
794
1020
|
}
|
|
795
1021
|
|
|
1022
|
+
// src/ghost-file.ts
|
|
1023
|
+
var LOCALGHOST_GHOST_TUNNEL_FILE = ".ghosttunnel";
|
|
1024
|
+
function toGhostTunnelOptions(options = {}) {
|
|
1025
|
+
const resolved = typeof options === "string" ? { cwd: options } : options;
|
|
1026
|
+
return {
|
|
1027
|
+
...resolved,
|
|
1028
|
+
fileName: resolved.fileName ?? LOCALGHOST_GHOST_TUNNEL_FILE
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function resolveGhostTunnelPath(options = {}) {
|
|
1032
|
+
return resolveDevHostsPath(toGhostTunnelOptions(options));
|
|
1033
|
+
}
|
|
1034
|
+
function readGhostTunnelEntries(options = {}) {
|
|
1035
|
+
return readDevHosts(toGhostTunnelOptions(options));
|
|
1036
|
+
}
|
|
1037
|
+
function listGhostTunnelEntries(options = {}) {
|
|
1038
|
+
const resolved = resolveGhostTunnelPath(options);
|
|
1039
|
+
if (!resolved.exists) return [];
|
|
1040
|
+
return readGhostTunnelEntries(options);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/ghost-agent.ts
|
|
1044
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1045
|
+
|
|
1046
|
+
// src/relay.ts
|
|
1047
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
1048
|
+
import { domainToASCII as domainToASCII2 } from "url";
|
|
1049
|
+
var DEFAULT_RELAY_ALLOWED_TARGET_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
1050
|
+
var DEFAULT_RELAY_BLOCKED_PORTS = [22, 2375, 2376, 5432, 6379, 9200, 9229, 27017];
|
|
1051
|
+
var DEFAULT_RELAY_LIMITS = {
|
|
1052
|
+
requestBodyBytes: 5 * 1024 * 1024,
|
|
1053
|
+
responseBytes: 25 * 1024 * 1024,
|
|
1054
|
+
timeoutMs: 3e4,
|
|
1055
|
+
maxConcurrentRequests: 20,
|
|
1056
|
+
perRouteRequestsPerMinute: 120,
|
|
1057
|
+
perIpRequestsPerMinute: 60
|
|
1058
|
+
};
|
|
1059
|
+
var DEFAULT_RELAY_TARGET_POLICY = {
|
|
1060
|
+
allowedHosts: [...DEFAULT_RELAY_ALLOWED_TARGET_HOSTS],
|
|
1061
|
+
blockedPorts: [...DEFAULT_RELAY_BLOCKED_PORTS],
|
|
1062
|
+
allowPrivateNetworkTargets: false
|
|
1063
|
+
};
|
|
1064
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1065
|
+
"connection",
|
|
1066
|
+
"keep-alive",
|
|
1067
|
+
"proxy-authenticate",
|
|
1068
|
+
"proxy-authorization",
|
|
1069
|
+
"te",
|
|
1070
|
+
"trailer",
|
|
1071
|
+
"transfer-encoding",
|
|
1072
|
+
"upgrade"
|
|
1073
|
+
]);
|
|
1074
|
+
var HOST_PATTERN2 = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*$/i;
|
|
1075
|
+
var IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
1076
|
+
function normalizeHost(host) {
|
|
1077
|
+
const trimmed = host.trim().toLowerCase().replace(/\.$/, "");
|
|
1078
|
+
if (!trimmed || trimmed.includes("*") || trimmed.includes("/") || trimmed.includes(":")) return null;
|
|
1079
|
+
const ascii = domainToASCII2(trimmed);
|
|
1080
|
+
if (!ascii || ascii.includes("..")) return null;
|
|
1081
|
+
return HOST_PATTERN2.test(ascii) ? ascii : null;
|
|
1082
|
+
}
|
|
1083
|
+
function normalizeTargetHost(host) {
|
|
1084
|
+
const trimmed = host.trim().toLowerCase();
|
|
1085
|
+
if (trimmed === "::1" || trimmed === "[::1]") return "::1";
|
|
1086
|
+
if (trimmed.includes("/") || trimmed.includes("*")) return null;
|
|
1087
|
+
if (IPV4_PATTERN.test(trimmed)) return isValidIpv4(trimmed) ? trimmed : null;
|
|
1088
|
+
return normalizeHost(trimmed);
|
|
1089
|
+
}
|
|
1090
|
+
function isValidIpv4(value) {
|
|
1091
|
+
return value.split(".").every((part) => {
|
|
1092
|
+
const octet = Number(part);
|
|
1093
|
+
return Number.isInteger(octet) && octet >= 0 && octet <= 255 && String(octet) === part;
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
function isPrivateIpv4(value) {
|
|
1097
|
+
if (!isValidIpv4(value)) return false;
|
|
1098
|
+
const [first = 0, second = 0] = value.split(".").map((part) => Number(part));
|
|
1099
|
+
return first === 10 || first === 127 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 169 && second === 254;
|
|
1100
|
+
}
|
|
1101
|
+
function isLocalTargetHost(host) {
|
|
1102
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
1103
|
+
}
|
|
1104
|
+
function mergeTargetPolicy(policy) {
|
|
1105
|
+
return {
|
|
1106
|
+
allowedHosts: policy?.allowedHosts ?? DEFAULT_RELAY_TARGET_POLICY.allowedHosts,
|
|
1107
|
+
blockedPorts: policy?.blockedPorts ?? DEFAULT_RELAY_TARGET_POLICY.blockedPorts,
|
|
1108
|
+
allowPrivateNetworkTargets: policy?.allowPrivateNetworkTargets ?? DEFAULT_RELAY_TARGET_POLICY.allowPrivateNetworkTargets
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function assertRelayLocalTarget(target, policyInput) {
|
|
1112
|
+
if (!target || typeof target !== "object") {
|
|
1113
|
+
throw new Error("Relay target must be an explicit local target object.");
|
|
1114
|
+
}
|
|
1115
|
+
const policy = mergeTargetPolicy(policyInput);
|
|
1116
|
+
const host = normalizeTargetHost(target.host);
|
|
1117
|
+
if (!host) {
|
|
1118
|
+
throw new Error(`Invalid relay target host: ${target.host}`);
|
|
1119
|
+
}
|
|
1120
|
+
if (!Number.isInteger(target.port) || target.port < 1 || target.port > 65535) {
|
|
1121
|
+
throw new Error(`Invalid relay target port: ${target.port}`);
|
|
1122
|
+
}
|
|
1123
|
+
const protocol = target.protocol ?? "http";
|
|
1124
|
+
if (protocol !== "http" && protocol !== "https") {
|
|
1125
|
+
throw new Error(`Invalid relay target protocol: ${String(protocol)}`);
|
|
1126
|
+
}
|
|
1127
|
+
if (policy.blockedPorts.includes(target.port)) {
|
|
1128
|
+
throw new Error(`Relay target port is blocked: ${target.port}`);
|
|
1129
|
+
}
|
|
1130
|
+
const allowedHosts = new Set(policy.allowedHosts.map((allowedHost) => normalizeTargetHost(allowedHost)).filter((value) => Boolean(value)));
|
|
1131
|
+
if (!allowedHosts.has(host)) {
|
|
1132
|
+
throw new Error(`Relay target host is not explicitly allowed: ${host}`);
|
|
1133
|
+
}
|
|
1134
|
+
if (!policy.allowPrivateNetworkTargets && !isLocalTargetHost(host) && (host === "::1" || isPrivateIpv4(host))) {
|
|
1135
|
+
throw new Error(`Private-network relay target requires explicit opt-in: ${host}`);
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
protocol,
|
|
1139
|
+
host,
|
|
1140
|
+
port: target.port
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function stripRelayForwardHeaders(headers) {
|
|
1144
|
+
const stripped = {};
|
|
1145
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1146
|
+
if (typeof value === "undefined") continue;
|
|
1147
|
+
const lowerName = name.toLowerCase();
|
|
1148
|
+
if (HOP_BY_HOP_HEADERS.has(lowerName)) continue;
|
|
1149
|
+
if (lowerName.startsWith("x-localghost-")) continue;
|
|
1150
|
+
stripped[name] = value;
|
|
1151
|
+
}
|
|
1152
|
+
return stripped;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/ghost-tunnel-store.ts
|
|
1156
|
+
import { randomUUID } from "crypto";
|
|
1157
|
+
function base64Encode(value) {
|
|
1158
|
+
return value.toString("base64");
|
|
1159
|
+
}
|
|
1160
|
+
function encodeGhostTunnelBody(value) {
|
|
1161
|
+
return base64Encode(Buffer.isBuffer(value) ? value : Buffer.from(value));
|
|
1162
|
+
}
|
|
1163
|
+
function decodeGhostTunnelBody(value) {
|
|
1164
|
+
return value ? Buffer.from(value, "base64") : void 0;
|
|
1165
|
+
}
|
|
1166
|
+
function createGhostTunnelRouteHeartbeat(input2) {
|
|
1167
|
+
const now = input2.now ?? /* @__PURE__ */ new Date();
|
|
1168
|
+
return {
|
|
1169
|
+
host: input2.host,
|
|
1170
|
+
agentId: input2.agentId,
|
|
1171
|
+
target: input2.target,
|
|
1172
|
+
updatedAt: now.toISOString(),
|
|
1173
|
+
expiresAt: new Date(now.getTime() + input2.ttlSeconds * 1e3).toISOString()
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function isExpired(expiresAt, now = /* @__PURE__ */ new Date()) {
|
|
1177
|
+
const timestamp = Date.parse(expiresAt);
|
|
1178
|
+
return Number.isNaN(timestamp) || timestamp <= now.getTime();
|
|
1179
|
+
}
|
|
1180
|
+
function serializeJson(value) {
|
|
1181
|
+
return JSON.stringify(value);
|
|
1182
|
+
}
|
|
1183
|
+
function parseJson(value) {
|
|
1184
|
+
if (typeof value !== "string") return null;
|
|
1185
|
+
try {
|
|
1186
|
+
return JSON.parse(value);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function keyPart(value) {
|
|
1192
|
+
return value.toLowerCase().replace(/[^a-z0-9._:-]/g, "_");
|
|
1193
|
+
}
|
|
1194
|
+
var RedisGhostTunnelStore = class {
|
|
1195
|
+
url;
|
|
1196
|
+
token;
|
|
1197
|
+
namespace;
|
|
1198
|
+
fetchImpl;
|
|
1199
|
+
constructor(options) {
|
|
1200
|
+
this.url = options.url.replace(/\/+$/, "");
|
|
1201
|
+
this.token = options.token;
|
|
1202
|
+
this.namespace = options.namespace ?? "localghost";
|
|
1203
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
1204
|
+
}
|
|
1205
|
+
key(kind, id) {
|
|
1206
|
+
return `${this.namespace}:ghost-tunnel:${kind}:${keyPart(id)}`;
|
|
1207
|
+
}
|
|
1208
|
+
async command(command, ...args) {
|
|
1209
|
+
const response = await this.fetchImpl(this.url, {
|
|
1210
|
+
method: "POST",
|
|
1211
|
+
headers: {
|
|
1212
|
+
authorization: `Bearer ${this.token}`,
|
|
1213
|
+
"content-type": "application/json"
|
|
1214
|
+
},
|
|
1215
|
+
body: JSON.stringify([command, ...args])
|
|
1216
|
+
});
|
|
1217
|
+
if (!response.ok) {
|
|
1218
|
+
throw new Error(`Redis Ghost Tunnel command failed: ${response.status} ${response.statusText}`);
|
|
1219
|
+
}
|
|
1220
|
+
const payload = await response.json();
|
|
1221
|
+
if (payload.error) {
|
|
1222
|
+
throw new Error(`Redis Ghost Tunnel command failed: ${payload.error}`);
|
|
1223
|
+
}
|
|
1224
|
+
return typeof payload.result === "undefined" ? null : payload.result;
|
|
1225
|
+
}
|
|
1226
|
+
async heartbeatRoute(route, ttlSeconds) {
|
|
1227
|
+
await this.command("SET", this.key("route", route.host), serializeJson(route), "EX", ttlSeconds);
|
|
1228
|
+
}
|
|
1229
|
+
async getRoute(host) {
|
|
1230
|
+
const route = parseJson(await this.command("GET", this.key("route", host)));
|
|
1231
|
+
return route && !isExpired(route.expiresAt) ? route : null;
|
|
1232
|
+
}
|
|
1233
|
+
async enqueueRequest(request, ttlSeconds) {
|
|
1234
|
+
const queueKey = this.key("queue", request.host);
|
|
1235
|
+
await this.command("RPUSH", queueKey, serializeJson(request));
|
|
1236
|
+
await this.command("EXPIRE", queueKey, ttlSeconds);
|
|
1237
|
+
}
|
|
1238
|
+
async claimRequest(host) {
|
|
1239
|
+
const queueKey = this.key("queue", host);
|
|
1240
|
+
while (true) {
|
|
1241
|
+
const request = parseJson(await this.command("LPOP", queueKey));
|
|
1242
|
+
if (!request) return null;
|
|
1243
|
+
if (!isExpired(request.expiresAt)) return request;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
async writeResponse(response, ttlSeconds) {
|
|
1247
|
+
await this.command("SET", this.key("response", response.id), serializeJson(response), "EX", ttlSeconds);
|
|
1248
|
+
}
|
|
1249
|
+
async readResponse(requestId) {
|
|
1250
|
+
return parseJson(await this.command("GET", this.key("response", requestId)));
|
|
1251
|
+
}
|
|
1252
|
+
async cleanup(requestId) {
|
|
1253
|
+
await this.command("DEL", this.key("response", requestId));
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
function createRedisGhostTunnelStore(options) {
|
|
1257
|
+
return new RedisGhostTunnelStore(options);
|
|
1258
|
+
}
|
|
1259
|
+
function resolveRedisGhostTunnelEnv(env = process.env) {
|
|
1260
|
+
const candidates = [
|
|
1261
|
+
env.LOCALGHOST_REDIS_REST_URL && env.LOCALGHOST_REDIS_REST_TOKEN ? { url: env.LOCALGHOST_REDIS_REST_URL, token: env.LOCALGHOST_REDIS_REST_TOKEN, source: "localghost" } : null,
|
|
1262
|
+
env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN ? { url: env.UPSTASH_REDIS_REST_URL, token: env.UPSTASH_REDIS_REST_TOKEN, source: "upstash" } : null,
|
|
1263
|
+
env.KV_REST_API_URL && env.KV_REST_API_TOKEN ? { url: env.KV_REST_API_URL, token: env.KV_REST_API_TOKEN, source: "vercel-kv" } : null,
|
|
1264
|
+
env.REDIS_REST_API_URL && env.REDIS_REST_API_TOKEN ? { url: env.REDIS_REST_API_URL, token: env.REDIS_REST_API_TOKEN, source: "redis" } : null
|
|
1265
|
+
];
|
|
1266
|
+
const match = candidates.find((candidate) => Boolean(candidate));
|
|
1267
|
+
if (!match) {
|
|
1268
|
+
throw new Error("Ghost Tunnel Redis transport requires REST env vars: UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKEN, KV_REST_API_URL/KV_REST_API_TOKEN, or LOCALGHOST_REDIS_REST_URL/LOCALGHOST_REDIS_REST_TOKEN.");
|
|
1269
|
+
}
|
|
1270
|
+
return match;
|
|
1271
|
+
}
|
|
1272
|
+
function createRedisGhostTunnelStoreFromEnv(input2 = {}) {
|
|
1273
|
+
const resolved = resolveRedisGhostTunnelEnv(input2.env);
|
|
1274
|
+
return createRedisGhostTunnelStore({
|
|
1275
|
+
url: resolved.url,
|
|
1276
|
+
token: resolved.token,
|
|
1277
|
+
...input2.namespace ? { namespace: input2.namespace } : {},
|
|
1278
|
+
...input2.fetch ? { fetch: input2.fetch } : {}
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/ghost-agent.ts
|
|
1283
|
+
function isStopped(signal, localSignal) {
|
|
1284
|
+
return localSignal.aborted || signal?.aborted === true;
|
|
1285
|
+
}
|
|
1286
|
+
function wait(ms, signal, localSignal) {
|
|
1287
|
+
if (isStopped(signal, localSignal)) return Promise.resolve();
|
|
1288
|
+
return new Promise((resolve3) => {
|
|
1289
|
+
const timeout = setTimeout(resolve3, ms);
|
|
1290
|
+
const stop = () => {
|
|
1291
|
+
clearTimeout(timeout);
|
|
1292
|
+
resolve3();
|
|
1293
|
+
};
|
|
1294
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
1295
|
+
localSignal.addEventListener("abort", stop, { once: true });
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
function toHeaderRecord(headers) {
|
|
1299
|
+
const result = {};
|
|
1300
|
+
headers.forEach((value, name) => {
|
|
1301
|
+
result[name] = value;
|
|
1302
|
+
});
|
|
1303
|
+
return result;
|
|
1304
|
+
}
|
|
1305
|
+
function hasRequestBody(method) {
|
|
1306
|
+
return method !== "GET" && method !== "HEAD";
|
|
1307
|
+
}
|
|
1308
|
+
async function serveGhostTunnelLocalRequest(input2) {
|
|
1309
|
+
const fetchImpl = input2.fetch ?? fetch;
|
|
1310
|
+
const localUrl = new URL(`${input2.target.protocol}://${input2.target.host}:${input2.target.port}/`);
|
|
1311
|
+
const requestPath = new URL(input2.request.path, "http://localghost.invalid");
|
|
1312
|
+
localUrl.pathname = requestPath.pathname;
|
|
1313
|
+
localUrl.search = requestPath.search;
|
|
1314
|
+
try {
|
|
1315
|
+
const body = hasRequestBody(input2.request.method) ? decodeGhostTunnelBody(input2.request.bodyBase64) : void 0;
|
|
1316
|
+
const response = await fetchImpl(localUrl, {
|
|
1317
|
+
method: input2.request.method,
|
|
1318
|
+
headers: {
|
|
1319
|
+
...stripRelayForwardHeaders(input2.request.headers),
|
|
1320
|
+
"x-forwarded-host": input2.request.host,
|
|
1321
|
+
"x-localghost-tunnel": "1"
|
|
1322
|
+
},
|
|
1323
|
+
...body ? { body } : {}
|
|
1324
|
+
});
|
|
1325
|
+
const responseBody = Buffer.from(await response.arrayBuffer());
|
|
1326
|
+
if (responseBody.byteLength > input2.maxResponseBodyBytes) {
|
|
1327
|
+
throw new Error(`Ghost Tunnel response exceeded ${input2.maxResponseBodyBytes} bytes.`);
|
|
1328
|
+
}
|
|
1329
|
+
return {
|
|
1330
|
+
id: input2.request.id,
|
|
1331
|
+
status: response.status,
|
|
1332
|
+
headers: toHeaderRecord(response.headers),
|
|
1333
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1334
|
+
...responseBody.byteLength > 0 ? { bodyBase64: encodeGhostTunnelBody(responseBody) } : {}
|
|
1335
|
+
};
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
return {
|
|
1338
|
+
id: input2.request.id,
|
|
1339
|
+
status: 502,
|
|
1340
|
+
headers: {
|
|
1341
|
+
"content-type": "text/plain; charset=utf-8",
|
|
1342
|
+
"cache-control": "no-store"
|
|
1343
|
+
},
|
|
1344
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1345
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1346
|
+
bodyBase64: encodeGhostTunnelBody("Ghost Tunnel local target failed.")
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
async function heartbeatRoutes(input2) {
|
|
1351
|
+
for (const entry of input2.entries) {
|
|
1352
|
+
const target = assertRelayLocalTarget({ host: input2.targetHost, port: entry.port });
|
|
1353
|
+
await input2.store.heartbeatRoute(createGhostTunnelRouteHeartbeat({
|
|
1354
|
+
host: entry.host,
|
|
1355
|
+
agentId: input2.agentId,
|
|
1356
|
+
target,
|
|
1357
|
+
ttlSeconds: input2.routeTtlSeconds
|
|
1358
|
+
}), input2.routeTtlSeconds);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
async function claimAndServe(input2) {
|
|
1362
|
+
const request = await input2.store.claimRequest(input2.entry.host);
|
|
1363
|
+
if (!request) return false;
|
|
1364
|
+
const target = assertRelayLocalTarget({ host: input2.targetHost, port: input2.entry.port });
|
|
1365
|
+
const response = await serveGhostTunnelLocalRequest({
|
|
1366
|
+
request,
|
|
1367
|
+
target,
|
|
1368
|
+
maxResponseBodyBytes: input2.maxResponseBodyBytes,
|
|
1369
|
+
...input2.fetch ? { fetch: input2.fetch } : {}
|
|
1370
|
+
});
|
|
1371
|
+
await input2.store.writeResponse(response, input2.requestTtlSeconds);
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
function startGhostTunnelAgent(options) {
|
|
1375
|
+
const controller = new AbortController();
|
|
1376
|
+
const localSignal = controller.signal;
|
|
1377
|
+
const signal = options.signal;
|
|
1378
|
+
const agentId = options.agentId ?? `localghost-${randomUUID2()}`;
|
|
1379
|
+
const targetHost = options.targetHost ?? "127.0.0.1";
|
|
1380
|
+
const routeTtlSeconds = options.routeTtlSeconds ?? 30;
|
|
1381
|
+
const requestTtlSeconds = options.requestTtlSeconds ?? 60;
|
|
1382
|
+
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
1383
|
+
const maxResponseBodyBytes = options.maxResponseBodyBytes ?? 5 * 1024 * 1024;
|
|
1384
|
+
const done = (async () => {
|
|
1385
|
+
if (options.entries.length === 0) {
|
|
1386
|
+
throw new Error("Ghost Tunnel agent requires at least one .ghosttunnel entry.");
|
|
1387
|
+
}
|
|
1388
|
+
options.log?.(`localghost tunnel agent ${agentId}`);
|
|
1389
|
+
for (const entry of options.entries) {
|
|
1390
|
+
options.log?.(` ${entry.host} -> ${targetHost}:${entry.port}`);
|
|
1391
|
+
}
|
|
1392
|
+
let lastHeartbeat = 0;
|
|
1393
|
+
while (!isStopped(signal, localSignal)) {
|
|
1394
|
+
const now = Date.now();
|
|
1395
|
+
if (now - lastHeartbeat >= Math.max(1e3, Math.floor(routeTtlSeconds * 1e3 / 3))) {
|
|
1396
|
+
await heartbeatRoutes({
|
|
1397
|
+
entries: options.entries,
|
|
1398
|
+
store: options.store,
|
|
1399
|
+
agentId,
|
|
1400
|
+
targetHost,
|
|
1401
|
+
routeTtlSeconds
|
|
1402
|
+
});
|
|
1403
|
+
lastHeartbeat = now;
|
|
1404
|
+
}
|
|
1405
|
+
let served = false;
|
|
1406
|
+
for (const entry of options.entries) {
|
|
1407
|
+
served = await claimAndServe({
|
|
1408
|
+
entry,
|
|
1409
|
+
store: options.store,
|
|
1410
|
+
targetHost,
|
|
1411
|
+
requestTtlSeconds,
|
|
1412
|
+
maxResponseBodyBytes,
|
|
1413
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
1414
|
+
}) || served;
|
|
1415
|
+
}
|
|
1416
|
+
if (!served) {
|
|
1417
|
+
await wait(pollIntervalMs, signal, localSignal);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
})();
|
|
1421
|
+
return {
|
|
1422
|
+
agentId,
|
|
1423
|
+
stop() {
|
|
1424
|
+
controller.abort();
|
|
1425
|
+
},
|
|
1426
|
+
done
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
796
1430
|
// src/hosts-file.ts
|
|
797
1431
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
798
1432
|
import { tmpdir } from "os";
|
|
799
|
-
import { join as
|
|
1433
|
+
import { join as join6 } from "path";
|
|
800
1434
|
import { execa as execa3 } from "execa";
|
|
801
1435
|
function escapeRegExp(value) {
|
|
802
1436
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -807,7 +1441,8 @@ function getManagedBlockPattern(projectName) {
|
|
|
807
1441
|
const end = `# localghost:end ${sanitizedProjectName}`;
|
|
808
1442
|
return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "m");
|
|
809
1443
|
}
|
|
810
|
-
function getSystemHostsPath() {
|
|
1444
|
+
function getSystemHostsPath(env = process.env) {
|
|
1445
|
+
if (env.LOCALGHOST_HOSTS_PATH) return env.LOCALGHOST_HOSTS_PATH;
|
|
811
1446
|
return process.platform === "win32" ? "C:\\Windows\\System32\\drivers\\etc\\hosts" : "/etc/hosts";
|
|
812
1447
|
}
|
|
813
1448
|
function renderHostsBlock(projectName, entries) {
|
|
@@ -838,8 +1473,12 @@ function removeManagedBlock(existing, projectName) {
|
|
|
838
1473
|
}
|
|
839
1474
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
840
1475
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
841
|
-
const tempPath =
|
|
1476
|
+
const tempPath = join6(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
842
1477
|
writeFileSync3(tempPath, next, "utf8");
|
|
1478
|
+
if (process.env.LOCALGHOST_HOSTS_PATH) {
|
|
1479
|
+
writeFileSync3(hostsPath, next, "utf8");
|
|
1480
|
+
return tempPath;
|
|
1481
|
+
}
|
|
843
1482
|
if (process.platform === "win32") {
|
|
844
1483
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
845
1484
|
}
|
|
@@ -871,16 +1510,18 @@ async function removeSystemHosts(projectName) {
|
|
|
871
1510
|
}
|
|
872
1511
|
|
|
873
1512
|
// src/init.ts
|
|
874
|
-
import { existsSync as
|
|
875
|
-
import { join as
|
|
1513
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1514
|
+
import { join as join7 } from "path";
|
|
876
1515
|
function detectPackageManager(cwd = process.cwd()) {
|
|
877
|
-
if (
|
|
878
|
-
if (
|
|
1516
|
+
if (existsSync5(join7(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1517
|
+
if (existsSync5(join7(cwd, "yarn.lock"))) return "yarn";
|
|
1518
|
+
if (existsSync5(join7(cwd, "bun.lock")) || existsSync5(join7(cwd, "bun.lockb"))) return "bun";
|
|
879
1519
|
return "npm";
|
|
880
1520
|
}
|
|
881
1521
|
function packageRunCommand(packageManager, script) {
|
|
882
1522
|
if (packageManager === "yarn") return `yarn ${script}`;
|
|
883
1523
|
if (packageManager === "pnpm") return `pnpm ${script}`;
|
|
1524
|
+
if (packageManager === "bun") return `bun run ${script}`;
|
|
884
1525
|
return `npm run ${script}`;
|
|
885
1526
|
}
|
|
886
1527
|
function renderConfig(options) {
|
|
@@ -893,9 +1534,9 @@ function renderConfig(options) {
|
|
|
893
1534
|
""
|
|
894
1535
|
].join("\n");
|
|
895
1536
|
}
|
|
896
|
-
function
|
|
1537
|
+
function readPackageJson2(path) {
|
|
897
1538
|
try {
|
|
898
|
-
return JSON.parse(
|
|
1539
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
899
1540
|
} catch {
|
|
900
1541
|
return null;
|
|
901
1542
|
}
|
|
@@ -908,7 +1549,7 @@ function getConfigFlag(configFile) {
|
|
|
908
1549
|
return configFile === LOCALGHOST_CONFIG_FILE ? "" : ` --config ${shellQuote(configFile)}`;
|
|
909
1550
|
}
|
|
910
1551
|
function updatePackageScripts(packageJsonPath, configFile) {
|
|
911
|
-
const pkg =
|
|
1552
|
+
const pkg = readPackageJson2(packageJsonPath);
|
|
912
1553
|
if (!pkg) return false;
|
|
913
1554
|
const scripts = typeof pkg.scripts === "object" && pkg.scripts ? pkg.scripts : {};
|
|
914
1555
|
const configFlag = getConfigFlag(configFile);
|
|
@@ -919,6 +1560,7 @@ function updatePackageScripts(packageJsonPath, configFile) {
|
|
|
919
1560
|
"localghost:proxy:https": scripts["localghost:proxy:https"] ?? `localghost dev${configFlag} --https`,
|
|
920
1561
|
"localghost:run": scripts["localghost:run"] ?? `localghost run${configFlag} --`,
|
|
921
1562
|
"localghost:ready": scripts["localghost:ready"] ?? `localghost status${configFlag} --ready`,
|
|
1563
|
+
"localghost:repair": scripts["localghost:repair"] ?? `localghost repair${configFlag}`,
|
|
922
1564
|
"localghost:trust": scripts["localghost:trust"] ?? `localghost trust${configFlag}`,
|
|
923
1565
|
"localghost:ps": scripts["localghost:ps"] ?? "localghost ps",
|
|
924
1566
|
"localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
|
|
@@ -947,8 +1589,8 @@ function initLocalghost(options = {}) {
|
|
|
947
1589
|
const apiPort = options.apiPort ?? 8787;
|
|
948
1590
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
949
1591
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
950
|
-
const configPath =
|
|
951
|
-
const configExists =
|
|
1592
|
+
const configPath = join7(cwd, configFile);
|
|
1593
|
+
const configExists = existsSync5(configPath);
|
|
952
1594
|
if (configExists && !options.force) {
|
|
953
1595
|
return {
|
|
954
1596
|
configPath,
|
|
@@ -964,12 +1606,12 @@ function initLocalghost(options = {}) {
|
|
|
964
1606
|
};
|
|
965
1607
|
}
|
|
966
1608
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
967
|
-
const packageJsonPath =
|
|
1609
|
+
const packageJsonPath = join7(cwd, "package.json");
|
|
968
1610
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
969
1611
|
return {
|
|
970
1612
|
configPath,
|
|
971
1613
|
configCreated: true,
|
|
972
|
-
...
|
|
1614
|
+
...existsSync5(packageJsonPath) ? { packageJsonPath } : {},
|
|
973
1615
|
packageJsonChanged,
|
|
974
1616
|
packageManager,
|
|
975
1617
|
nextSteps: [
|
|
@@ -1061,21 +1703,22 @@ function formatGhostTunnel(config, options = {}) {
|
|
|
1061
1703
|
if (options.verbose) {
|
|
1062
1704
|
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
1063
1705
|
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
1064
|
-
lines.push(`
|
|
1706
|
+
lines.push(` protocol: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
1707
|
+
lines.push(` transport: ${config.transport.kind}`);
|
|
1065
1708
|
}
|
|
1066
1709
|
return lines.join("\n");
|
|
1067
1710
|
}
|
|
1068
1711
|
|
|
1069
1712
|
// src/state.ts
|
|
1070
|
-
import { existsSync as
|
|
1071
|
-
import { join as
|
|
1713
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1714
|
+
import { join as join8 } from "path";
|
|
1072
1715
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
1073
1716
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
1074
|
-
return
|
|
1717
|
+
return join8(cwd, LOCALGHOST_STATE_FILE);
|
|
1075
1718
|
}
|
|
1076
1719
|
function readLocalghostState(cwd = process.cwd()) {
|
|
1077
1720
|
const path = getLocalghostStatePath(cwd);
|
|
1078
|
-
if (!
|
|
1721
|
+
if (!existsSync6(path)) return null;
|
|
1079
1722
|
return JSON.parse(readTextFile(path));
|
|
1080
1723
|
}
|
|
1081
1724
|
function writeLocalghostState(cwd, state) {
|
|
@@ -1091,11 +1734,11 @@ function patchLocalghostState(cwd, patch) {
|
|
|
1091
1734
|
}
|
|
1092
1735
|
|
|
1093
1736
|
// src/update-check.ts
|
|
1094
|
-
import { existsSync as
|
|
1737
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1095
1738
|
import { homedir as homedir2 } from "os";
|
|
1096
|
-
import { dirname as dirname4, join as
|
|
1739
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
1097
1740
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
1098
|
-
var LOCALGHOST_VERSION = "0.1.
|
|
1741
|
+
var LOCALGHOST_VERSION = "0.1.13";
|
|
1099
1742
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1100
1743
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1101
1744
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -1107,13 +1750,13 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
1107
1750
|
}
|
|
1108
1751
|
function getUpdateCheckCachePath(env = process.env) {
|
|
1109
1752
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
1110
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
1111
|
-
return
|
|
1753
|
+
const cacheRoot = env.XDG_CACHE_HOME || join9(homedir2(), ".cache");
|
|
1754
|
+
return join9(cacheRoot, "localghost", "update-check.json");
|
|
1112
1755
|
}
|
|
1113
1756
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
1114
|
-
if (!
|
|
1757
|
+
if (!existsSync7(path)) return null;
|
|
1115
1758
|
try {
|
|
1116
|
-
return JSON.parse(
|
|
1759
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
1117
1760
|
} catch {
|
|
1118
1761
|
return null;
|
|
1119
1762
|
}
|
|
@@ -1270,6 +1913,10 @@ function warnAboutLocalMdns(entries) {
|
|
|
1270
1913
|
function shouldColor() {
|
|
1271
1914
|
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1272
1915
|
}
|
|
1916
|
+
function printLocalghostBanner() {
|
|
1917
|
+
console.log(renderLocalghostBanner());
|
|
1918
|
+
console.log("");
|
|
1919
|
+
}
|
|
1273
1920
|
function logDomainRoutes(entries, options = {}) {
|
|
1274
1921
|
console.log(formatDomainRoutes(entries, options));
|
|
1275
1922
|
if (options.ghostTunnel?.enabled) {
|
|
@@ -1288,8 +1935,8 @@ function parsePort2(value) {
|
|
|
1288
1935
|
return port;
|
|
1289
1936
|
}
|
|
1290
1937
|
function parsePackageManager(value) {
|
|
1291
|
-
if (value === "npm" || value === "yarn" || value === "pnpm") return value;
|
|
1292
|
-
throw new InvalidArgumentError("Package manager must be npm, yarn, or
|
|
1938
|
+
if (value === "npm" || value === "yarn" || value === "pnpm" || value === "bun") return value;
|
|
1939
|
+
throw new InvalidArgumentError("Package manager must be npm, pnpm, yarn, or bun.");
|
|
1293
1940
|
}
|
|
1294
1941
|
function collect(value, previous = []) {
|
|
1295
1942
|
return [...previous, value];
|
|
@@ -1308,7 +1955,8 @@ function contextOptionsFromCli(options) {
|
|
|
1308
1955
|
...options.project ? { project: options.project } : {},
|
|
1309
1956
|
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
1310
1957
|
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1311
|
-
...useHttps(options) ? { https: true } : {}
|
|
1958
|
+
...useHttps(options) ? { https: true } : {},
|
|
1959
|
+
...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
|
|
1312
1960
|
};
|
|
1313
1961
|
}
|
|
1314
1962
|
function readOptionsFromCli(options) {
|
|
@@ -1372,7 +2020,7 @@ function getSetupReadiness(options) {
|
|
|
1372
2020
|
}
|
|
1373
2021
|
const hostsPath = getSystemHostsPath();
|
|
1374
2022
|
try {
|
|
1375
|
-
const hosts =
|
|
2023
|
+
const hosts = readFileSync8(hostsPath, "utf8");
|
|
1376
2024
|
const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
|
|
1377
2025
|
if (!hosts.includes(expectedHostsBlock)) {
|
|
1378
2026
|
reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
|
|
@@ -1382,11 +2030,11 @@ function getSetupReadiness(options) {
|
|
|
1382
2030
|
reasons.push(`Could not read ${hostsPath}: ${message}`);
|
|
1383
2031
|
}
|
|
1384
2032
|
if (!options.ignoreCaddyfile) {
|
|
1385
|
-
if (!
|
|
2033
|
+
if (!existsSync8(caddyfilePath)) {
|
|
1386
2034
|
reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
|
|
1387
2035
|
} else {
|
|
1388
2036
|
const expectedCaddyfile = renderCaddyfile(entries, { https });
|
|
1389
|
-
const currentCaddyfile =
|
|
2037
|
+
const currentCaddyfile = readFileSync8(caddyfilePath, "utf8");
|
|
1390
2038
|
if (currentCaddyfile !== expectedCaddyfile) {
|
|
1391
2039
|
reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
|
|
1392
2040
|
}
|
|
@@ -1430,15 +2078,15 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
1430
2078
|
entries: readiness.entries
|
|
1431
2079
|
});
|
|
1432
2080
|
}
|
|
1433
|
-
function
|
|
1434
|
-
return new Promise((
|
|
2081
|
+
function wait2(ms) {
|
|
2082
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1435
2083
|
}
|
|
1436
2084
|
async function runTrust(cwd, caddyfilePath) {
|
|
1437
|
-
await
|
|
2085
|
+
await wait2(350);
|
|
1438
2086
|
try {
|
|
1439
2087
|
await trustCaddy(caddyfilePath);
|
|
1440
2088
|
} catch {
|
|
1441
|
-
await
|
|
2089
|
+
await wait2(750);
|
|
1442
2090
|
await trustCaddy(caddyfilePath);
|
|
1443
2091
|
}
|
|
1444
2092
|
patchLocalghostState(cwd, { caddyTrustedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -1478,6 +2126,131 @@ function registerCleanup(id) {
|
|
|
1478
2126
|
process.off("exit", cleanup);
|
|
1479
2127
|
};
|
|
1480
2128
|
}
|
|
2129
|
+
async function resolveServiceRuntimeEntries(services, dynamicPort) {
|
|
2130
|
+
const usedPorts = /* @__PURE__ */ new Set();
|
|
2131
|
+
const resolved = [];
|
|
2132
|
+
for (const service of services) {
|
|
2133
|
+
let port = service.requestedPort;
|
|
2134
|
+
if (dynamicPort) {
|
|
2135
|
+
let found = false;
|
|
2136
|
+
for (let offset = 0; offset < 50; offset += 1) {
|
|
2137
|
+
const candidate = service.requestedPort + offset;
|
|
2138
|
+
if (candidate > 65535 || usedPorts.has(candidate)) continue;
|
|
2139
|
+
if (await isPortAvailable(candidate)) {
|
|
2140
|
+
port = candidate;
|
|
2141
|
+
found = true;
|
|
2142
|
+
break;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (!found) {
|
|
2146
|
+
throw new Error(`No available port found for service ${service.name} from ${service.requestedPort}.`);
|
|
2147
|
+
}
|
|
2148
|
+
} else if (usedPorts.has(port)) {
|
|
2149
|
+
throw new Error(`Services cannot start separate commands on the same fixed port: ${port}.`);
|
|
2150
|
+
}
|
|
2151
|
+
usedPorts.add(port);
|
|
2152
|
+
resolved.push({
|
|
2153
|
+
...service,
|
|
2154
|
+
port,
|
|
2155
|
+
entry: {
|
|
2156
|
+
host: service.host,
|
|
2157
|
+
port,
|
|
2158
|
+
target: `127.0.0.1:${port}`
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
return resolved;
|
|
2163
|
+
}
|
|
2164
|
+
async function waitForServicePorts(entries, timeoutMs = 1e4) {
|
|
2165
|
+
const deadline = Date.now() + timeoutMs;
|
|
2166
|
+
const ports = [...new Set(entries.map((entry) => entry.port))];
|
|
2167
|
+
while (Date.now() < deadline) {
|
|
2168
|
+
const availability = await Promise.all(ports.map((port) => isPortAvailable(port)));
|
|
2169
|
+
if (availability.every((available) => !available)) return true;
|
|
2170
|
+
await wait2(50);
|
|
2171
|
+
}
|
|
2172
|
+
return false;
|
|
2173
|
+
}
|
|
2174
|
+
async function runDetectedServices(options) {
|
|
2175
|
+
assertLocalDevelopment("run");
|
|
2176
|
+
await assertCaddyReady();
|
|
2177
|
+
const runtimeServices = await resolveServiceRuntimeEntries(options.services, options.dynamicPort);
|
|
2178
|
+
const entries = runtimeServices.map((service) => service.entry);
|
|
2179
|
+
const readiness = getSetupReadiness({
|
|
2180
|
+
cwd: options.cwd,
|
|
2181
|
+
https: options.https,
|
|
2182
|
+
ignoreCaddyfile: true,
|
|
2183
|
+
entries,
|
|
2184
|
+
configPath: options.configPath,
|
|
2185
|
+
projectName: options.projectName
|
|
2186
|
+
});
|
|
2187
|
+
if (!readiness.ready) {
|
|
2188
|
+
if (!options.autoRepair) {
|
|
2189
|
+
throw new Error([
|
|
2190
|
+
"Localghost setup is missing or stale.",
|
|
2191
|
+
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
2192
|
+
"Automatic repair is disabled. Enable autoRepair or run localghost repair."
|
|
2193
|
+
].join("\n"));
|
|
2194
|
+
}
|
|
2195
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
2196
|
+
await runSetupFromReadiness(options.cwd, options.https, readiness);
|
|
2197
|
+
}
|
|
2198
|
+
for (const service of runtimeServices) {
|
|
2199
|
+
if (service.port !== service.requestedPort) {
|
|
2200
|
+
console.log(`${service.name}: port ${service.requestedPort} is busy; using ${service.port}.`);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
const caddyfile = await writeCaddyfile(entries, options.cwd, { https: options.https });
|
|
2204
|
+
await validateCaddyfile(caddyfile);
|
|
2205
|
+
const caddy = startCaddy(caddyfile);
|
|
2206
|
+
const caddyExit = caddy.catch((error) => {
|
|
2207
|
+
if (!caddy.killed) throw error;
|
|
2208
|
+
});
|
|
2209
|
+
const children = runtimeServices.map((service) => execa4(service.command[0], service.command.slice(1), {
|
|
2210
|
+
cwd: service.cwd,
|
|
2211
|
+
stdio: "inherit",
|
|
2212
|
+
env: {
|
|
2213
|
+
...process.env,
|
|
2214
|
+
LOCALGHOST_PORT: String(service.port),
|
|
2215
|
+
LOCALGHOST_DYNAMIC_PORT: options.dynamicPort ? "1" : "0",
|
|
2216
|
+
LOCALGHOST_SERVICE: service.name,
|
|
2217
|
+
VITE_PORT: String(service.port)
|
|
2218
|
+
}
|
|
2219
|
+
}));
|
|
2220
|
+
const caddyPid = maybePid(caddy.pid);
|
|
2221
|
+
const runRecord = registerLocalghostRun({
|
|
2222
|
+
mode: "run",
|
|
2223
|
+
cwd: options.cwd,
|
|
2224
|
+
projectName: options.projectName,
|
|
2225
|
+
configPath: options.configPath,
|
|
2226
|
+
caddyfilePath: caddyfile,
|
|
2227
|
+
...caddyPid ? { caddyPid } : {},
|
|
2228
|
+
childCommand: ["services", ...runtimeServices.map((service) => service.name)],
|
|
2229
|
+
https: options.https,
|
|
2230
|
+
dynamicPort: options.dynamicPort,
|
|
2231
|
+
entries
|
|
2232
|
+
});
|
|
2233
|
+
const cleanupRun = registerCleanup(runRecord.id);
|
|
2234
|
+
const processExit = Promise.race([caddyExit, ...children]);
|
|
2235
|
+
try {
|
|
2236
|
+
const ready = await Promise.race([
|
|
2237
|
+
waitForServicePorts(entries),
|
|
2238
|
+
processExit.then(() => false)
|
|
2239
|
+
]);
|
|
2240
|
+
if (ready) {
|
|
2241
|
+
console.log("");
|
|
2242
|
+
logDomainRoutes(entries, { https: options.https });
|
|
2243
|
+
}
|
|
2244
|
+
await processExit;
|
|
2245
|
+
} finally {
|
|
2246
|
+
for (const child of children) {
|
|
2247
|
+
if (!child.killed) child.kill("SIGINT");
|
|
2248
|
+
}
|
|
2249
|
+
if (!caddy.killed) caddy.kill("SIGINT");
|
|
2250
|
+
await Promise.allSettled([caddyExit, ...children]);
|
|
2251
|
+
cleanupRun();
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
1481
2254
|
async function getRouteViews(entries) {
|
|
1482
2255
|
const portStatus = /* @__PURE__ */ new Map();
|
|
1483
2256
|
for (const entry of entries) {
|
|
@@ -1575,7 +2348,7 @@ program.hook("postAction", async (_thisCommand, actionCommand) => {
|
|
|
1575
2348
|
const options = program.opts();
|
|
1576
2349
|
await maybeNotifyAboutUpdate({ disabled: options.updateCheck === false });
|
|
1577
2350
|
});
|
|
1578
|
-
program.command("init").description("Create a .localghost config for this project").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to create", ".localghost").option("--host <host>", "Primary local hostname").option("--port <number>", "Primary app port", parsePort2).option("--api-host <host>", "API local hostname").option("--api-port <number>", "API port", parsePort2).option("--package-manager <npm|yarn|
|
|
2351
|
+
program.command("init").description("Create a .localghost config for this project").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to create", ".localghost").option("--host <host>", "Primary local hostname").option("--port <number>", "Primary app port", parsePort2).option("--api-host <host>", "API local hostname").option("--api-port <number>", "API port", parsePort2).option("--package-manager <npm|pnpm|yarn|bun>", "Package manager for suggested commands", parsePackageManager).option("--write-scripts", "Add localghost scripts to package.json").option("--force", "Overwrite an existing config file").action((options) => {
|
|
1579
2352
|
const result = initLocalghost({ ...options, configFile: options.config });
|
|
1580
2353
|
if (result.configCreated) {
|
|
1581
2354
|
console.log(`Buh. Created ${result.configPath}`);
|
|
@@ -1629,6 +2402,7 @@ program.command("update").description("Check npm for a newer localghost release"
|
|
|
1629
2402
|
});
|
|
1630
2403
|
program.command("setup").description("Update /etc/hosts and generate/validate Caddyfile").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--https", "Generate a local HTTPS Caddy proxy with Caddy local certificates").option("--ssl", "Alias for --https").action(async (options) => {
|
|
1631
2404
|
assertLocalDevelopment("setup");
|
|
2405
|
+
printLocalghostBanner();
|
|
1632
2406
|
await assertCaddyReady();
|
|
1633
2407
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1634
2408
|
const https = context.https;
|
|
@@ -1686,6 +2460,32 @@ program.command("trust").description("Trust Caddy's local HTTPS CA for this proj
|
|
|
1686
2460
|
await validateCaddyfile(caddyfile);
|
|
1687
2461
|
await runTrust(options.cwd, caddyfile);
|
|
1688
2462
|
});
|
|
2463
|
+
program.command("repair").description("Reconcile stale hosts, Caddyfile, setup state, and optional HTTPS trust").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--https", "Repair an HTTPS Caddy setup").option("--ssl", "Alias for --https").option("--trust", "Re-run Caddy's local HTTPS trust step").action(async (options) => {
|
|
2464
|
+
assertLocalDevelopment("repair");
|
|
2465
|
+
printLocalghostBanner();
|
|
2466
|
+
await assertCaddyReady();
|
|
2467
|
+
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
2468
|
+
const readiness = getSetupReadiness({
|
|
2469
|
+
...options,
|
|
2470
|
+
https: context.https,
|
|
2471
|
+
entries: context.entries,
|
|
2472
|
+
configPath: context.configPath,
|
|
2473
|
+
projectName: context.projectName
|
|
2474
|
+
});
|
|
2475
|
+
if (options.trust && !context.https) {
|
|
2476
|
+
throw new Error("Cannot repair HTTPS trust unless HTTPS is enabled. Pass --https or configure https: true.");
|
|
2477
|
+
}
|
|
2478
|
+
warnAboutLocalMdns(context.entries);
|
|
2479
|
+
logDomainRoutes(context.entries, { https: context.https, ghostTunnel: context.ghostTunnel });
|
|
2480
|
+
await runSetupFromReadiness(options.cwd, context.https, readiness);
|
|
2481
|
+
if (options.trust) {
|
|
2482
|
+
await runTrust(options.cwd, readiness.caddyfilePath);
|
|
2483
|
+
}
|
|
2484
|
+
console.log(`Repaired hosts: ${getSystemHostsPath()}`);
|
|
2485
|
+
console.log(`Repaired Caddyfile: ${readiness.caddyfilePath}`);
|
|
2486
|
+
console.log(`Repaired state: ${readiness.statePath}`);
|
|
2487
|
+
console.log("Repair complete.");
|
|
2488
|
+
});
|
|
1689
2489
|
program.command("reset").description("Remove Localghost setup state without deleting .localghost").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).action(async (options) => {
|
|
1690
2490
|
assertLocalDevelopment("reset");
|
|
1691
2491
|
const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
|
|
@@ -1693,13 +2493,13 @@ program.command("reset").description("Remove Localghost setup state without dele
|
|
|
1693
2493
|
const statePath = getLocalghostStatePath(options.cwd);
|
|
1694
2494
|
explainHostsPassword();
|
|
1695
2495
|
const hostsResult = await removeSystemHosts(projectName);
|
|
1696
|
-
if (
|
|
2496
|
+
if (existsSync8(caddyfilePath)) {
|
|
1697
2497
|
unlinkSync(caddyfilePath);
|
|
1698
2498
|
console.log(`Removed ${caddyfilePath}`);
|
|
1699
2499
|
} else {
|
|
1700
2500
|
console.log(`${caddyfilePath} was not present`);
|
|
1701
2501
|
}
|
|
1702
|
-
if (
|
|
2502
|
+
if (existsSync8(statePath)) {
|
|
1703
2503
|
unlinkSync(statePath);
|
|
1704
2504
|
console.log(`Removed ${statePath}`);
|
|
1705
2505
|
} else {
|
|
@@ -1720,7 +2520,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
1720
2520
|
const hostsResult = await removeSystemHosts(projectName);
|
|
1721
2521
|
const caddyfilePath = getCaddyfilePath(options.cwd);
|
|
1722
2522
|
let caddyfileRemoved = false;
|
|
1723
|
-
if (options.removeCaddyfile &&
|
|
2523
|
+
if (options.removeCaddyfile && existsSync8(caddyfilePath)) {
|
|
1724
2524
|
unlinkSync(caddyfilePath);
|
|
1725
2525
|
caddyfileRemoved = true;
|
|
1726
2526
|
}
|
|
@@ -1800,7 +2600,7 @@ program.command("routes").description("Print domain to upstream routes").option(
|
|
|
1800
2600
|
}));
|
|
1801
2601
|
}
|
|
1802
2602
|
});
|
|
1803
|
-
program.command("dev").description("Run the Localghost Caddy proxy
|
|
2603
|
+
program.command("dev").description("Run the Localghost Caddy proxy, repairing stale setup when needed").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Alias for automatic repair when setup is missing or stale").option("--auto-repair [yes|no]", "Repair stale setup before starting (default: yes)", parseBooleanLike).option("--trust", "Trust Caddy's local HTTPS CA before starting the proxy").action(async (options) => {
|
|
1804
2604
|
assertLocalDevelopment("dev");
|
|
1805
2605
|
await assertCaddyReady();
|
|
1806
2606
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
@@ -1813,41 +2613,18 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1813
2613
|
projectName: context.projectName
|
|
1814
2614
|
});
|
|
1815
2615
|
if (!readiness.ready) {
|
|
1816
|
-
if (!options.setup) {
|
|
2616
|
+
if (!options.setup && !context.autoRepair) {
|
|
1817
2617
|
throw new Error(
|
|
1818
2618
|
[
|
|
1819
2619
|
"Localghost setup is missing or stale.",
|
|
1820
2620
|
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
1821
2621
|
`Run: ${readiness.setupCommand}`,
|
|
1822
|
-
"
|
|
2622
|
+
"Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
|
|
1823
2623
|
].join("\n")
|
|
1824
2624
|
);
|
|
1825
2625
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const caddyfilePath = await writeCaddyfile(readiness.entries, options.cwd, { https });
|
|
1829
|
-
await validateCaddyfile(caddyfilePath);
|
|
1830
|
-
writeLocalghostState(options.cwd, {
|
|
1831
|
-
action: "setup",
|
|
1832
|
-
projectName: readiness.projectName,
|
|
1833
|
-
cwd: options.cwd,
|
|
1834
|
-
configPath: readiness.configPath,
|
|
1835
|
-
hostsPath: hostsResult.hostsPath,
|
|
1836
|
-
hostsChanged: hostsResult.changed,
|
|
1837
|
-
...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
|
|
1838
|
-
caddyfilePath,
|
|
1839
|
-
caddyHttps: https,
|
|
1840
|
-
...existingTrustMarkers(options.cwd),
|
|
1841
|
-
entries: readiness.entries
|
|
1842
|
-
});
|
|
1843
|
-
registerLocalghostSetup({
|
|
1844
|
-
cwd: options.cwd,
|
|
1845
|
-
projectName: readiness.projectName,
|
|
1846
|
-
configPath: readiness.configPath,
|
|
1847
|
-
caddyfilePath,
|
|
1848
|
-
https,
|
|
1849
|
-
entries: readiness.entries
|
|
1850
|
-
});
|
|
2626
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
2627
|
+
await runSetupFromReadiness(options.cwd, https, readiness);
|
|
1851
2628
|
}
|
|
1852
2629
|
warnAboutLocalMdns(readiness.entries);
|
|
1853
2630
|
logDomainRoutes(readiness.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
@@ -1883,7 +2660,7 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1883
2660
|
cleanupRun();
|
|
1884
2661
|
}
|
|
1885
2662
|
});
|
|
1886
|
-
program.command("run").description("Run Caddy and a dev command from the same Localghost context").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--port <number>", "Initial app port", parsePort2).option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "
|
|
2663
|
+
program.command("run").description("Run Caddy and a dev command from the same Localghost context").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--port <number>", "Initial app port", parsePort2).option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Alias for automatic repair when setup is missing or stale").option("--auto-repair [yes|no]", "Repair stale setup before starting (default: yes)", parseBooleanLike).option("--trust", "Trust Caddy's local HTTPS CA before starting the child command").option("--dynamic-port [yes|no]", "Use the requested port if free, otherwise continue upward", parseBooleanLike).argument("<command...>", "Command to run after --, for example: localghost run -- vite").action(async (command, options) => {
|
|
1887
2664
|
assertLocalDevelopment("run");
|
|
1888
2665
|
await assertCaddyReady();
|
|
1889
2666
|
const context = await resolveLocalghostContext({
|
|
@@ -1893,7 +2670,8 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1893
2670
|
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1894
2671
|
...options.port ? { port: options.port } : {},
|
|
1895
2672
|
...useHttps(options) ? { https: true } : {},
|
|
1896
|
-
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {}
|
|
2673
|
+
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {},
|
|
2674
|
+
...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
|
|
1897
2675
|
});
|
|
1898
2676
|
const https = context.https;
|
|
1899
2677
|
const readiness = getSetupReadiness({
|
|
@@ -1905,18 +2683,19 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1905
2683
|
projectName: context.projectName
|
|
1906
2684
|
});
|
|
1907
2685
|
if (!readiness.ready) {
|
|
1908
|
-
|
|
1909
|
-
if (!shouldSetup) {
|
|
2686
|
+
if (!options.setup && !context.autoRepair) {
|
|
1910
2687
|
throw new Error(
|
|
1911
2688
|
[
|
|
1912
2689
|
"Localghost setup is missing or stale.",
|
|
1913
2690
|
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
1914
|
-
`Run: ${readiness.setupCommand}
|
|
2691
|
+
`Run: ${readiness.setupCommand}`,
|
|
2692
|
+
"Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
|
|
1915
2693
|
].join("\n")
|
|
1916
2694
|
);
|
|
1917
2695
|
}
|
|
2696
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
1918
2697
|
await runSetupFromReadiness(options.cwd, https, readiness);
|
|
1919
|
-
console.log(`
|
|
2698
|
+
console.log(`Repair complete. Setup state: ${getLocalghostStatePath(options.cwd)}`);
|
|
1920
2699
|
}
|
|
1921
2700
|
if (context.dynamicPort && context.port !== context.requestedPort) {
|
|
1922
2701
|
console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
|
|
@@ -1987,6 +2766,53 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1987
2766
|
cleanupRun();
|
|
1988
2767
|
}
|
|
1989
2768
|
});
|
|
2769
|
+
program.command("tunnel").description("Run the local Ghost Tunnel agent").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--ghost-config <file>", "Exact Ghost Tunnel route file", ".ghosttunnel").option("--target-host <host>", "Local target host for .ghosttunnel ports", "127.0.0.1").action(async (options) => {
|
|
2770
|
+
assertLocalDevelopment("tunnel");
|
|
2771
|
+
const context = await resolveLocalghostContext({
|
|
2772
|
+
cwd: options.cwd,
|
|
2773
|
+
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
2774
|
+
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
2775
|
+
dynamicPort: false
|
|
2776
|
+
});
|
|
2777
|
+
if (!context.ghostTunnel.enabled) {
|
|
2778
|
+
throw new Error("Ghost Tunnel is not enabled in localghost.config.mjs.");
|
|
2779
|
+
}
|
|
2780
|
+
if (context.ghostTunnel.transport.kind !== "tunnel") {
|
|
2781
|
+
throw new Error(`Ghost Tunnel transport must be tunnel for localghost tunnel. Current: ${context.ghostTunnel.transport.kind}`);
|
|
2782
|
+
}
|
|
2783
|
+
const entries = listGhostTunnelEntries({
|
|
2784
|
+
cwd: options.cwd,
|
|
2785
|
+
fileName: options.ghostConfig
|
|
2786
|
+
});
|
|
2787
|
+
if (entries.length === 0) {
|
|
2788
|
+
throw new Error(`No exact Ghost Tunnel routes found in ${options.ghostConfig}.`);
|
|
2789
|
+
}
|
|
2790
|
+
const transport = context.ghostTunnel.transport;
|
|
2791
|
+
const store = createRedisGhostTunnelStoreFromEnv({
|
|
2792
|
+
namespace: transport.store.namespace
|
|
2793
|
+
});
|
|
2794
|
+
const controller = new AbortController();
|
|
2795
|
+
const stop = () => controller.abort();
|
|
2796
|
+
process.once("SIGINT", stop);
|
|
2797
|
+
process.once("SIGTERM", stop);
|
|
2798
|
+
const agent = startGhostTunnelAgent({
|
|
2799
|
+
entries,
|
|
2800
|
+
store,
|
|
2801
|
+
targetHost: options.targetHost,
|
|
2802
|
+
routeTtlSeconds: transport.routeTtlSeconds,
|
|
2803
|
+
requestTtlSeconds: transport.requestTtlSeconds,
|
|
2804
|
+
pollIntervalMs: transport.pollIntervalMs,
|
|
2805
|
+
maxResponseBodyBytes: transport.maxResponseBodyBytes,
|
|
2806
|
+
signal: controller.signal,
|
|
2807
|
+
log: (message) => console.log(message)
|
|
2808
|
+
});
|
|
2809
|
+
try {
|
|
2810
|
+
await agent.done;
|
|
2811
|
+
} finally {
|
|
2812
|
+
process.off("SIGINT", stop);
|
|
2813
|
+
process.off("SIGTERM", stop);
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
1990
2816
|
program.command("ps").description("Show Localghost setups and currently running sessions").option("--json", "Print raw JSON").action(async (options) => {
|
|
1991
2817
|
const setups = listLocalghostSetups();
|
|
1992
2818
|
const runs = listLocalghostRuns();
|
|
@@ -2002,7 +2828,83 @@ program.command("print").description("Print parsed host config").option("--cwd <
|
|
|
2002
2828
|
warnAboutLocalMdns(entries);
|
|
2003
2829
|
console.log(JSON.stringify(entries, null, 2));
|
|
2004
2830
|
});
|
|
2005
|
-
|
|
2831
|
+
function readImplicitInvocation(args) {
|
|
2832
|
+
if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V")) return null;
|
|
2833
|
+
let cwd = process.cwd();
|
|
2834
|
+
let dryRun = false;
|
|
2835
|
+
let updateCheck = true;
|
|
2836
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2837
|
+
const arg = args[index];
|
|
2838
|
+
if (!arg) continue;
|
|
2839
|
+
if (arg === "--dry-run") {
|
|
2840
|
+
dryRun = true;
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2843
|
+
if (arg === "--no-update-check") {
|
|
2844
|
+
updateCheck = false;
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
if (arg === "--cwd") {
|
|
2848
|
+
const value = args[index + 1];
|
|
2849
|
+
if (!value) throw new Error("--cwd requires a path.");
|
|
2850
|
+
cwd = value;
|
|
2851
|
+
index += 1;
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
if (arg.startsWith("--cwd=")) {
|
|
2855
|
+
cwd = arg.slice("--cwd=".length);
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
return null;
|
|
2859
|
+
}
|
|
2860
|
+
return { cwd, dryRun, updateCheck };
|
|
2861
|
+
}
|
|
2862
|
+
async function main() {
|
|
2863
|
+
const implicit = readImplicitInvocation(process.argv.slice(2));
|
|
2864
|
+
if (!implicit) {
|
|
2865
|
+
await program.parseAsync();
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
const projectConfig = await readLocalghostProjectConfig({ cwd: implicit.cwd });
|
|
2869
|
+
printLocalghostBanner();
|
|
2870
|
+
if (projectConfig.config.services) {
|
|
2871
|
+
if (!projectConfig.path) throw new Error("Multi-service configuration must come from localghost.config.mjs.");
|
|
2872
|
+
const services = detectDevServices({
|
|
2873
|
+
cwd: implicit.cwd,
|
|
2874
|
+
services: projectConfig.config.services
|
|
2875
|
+
});
|
|
2876
|
+
console.log(formatDetectedDevServices(services));
|
|
2877
|
+
if (implicit.dryRun) return;
|
|
2878
|
+
await runDetectedServices({
|
|
2879
|
+
cwd: implicit.cwd,
|
|
2880
|
+
services,
|
|
2881
|
+
configPath: projectConfig.path,
|
|
2882
|
+
projectName: sanitizeProjectName(projectConfig.config.project ?? getProjectName(implicit.cwd)),
|
|
2883
|
+
https: projectConfig.config.https ?? false,
|
|
2884
|
+
dynamicPort: projectConfig.config.dynamicPort ?? true,
|
|
2885
|
+
autoRepair: projectConfig.config.autoRepair ?? true
|
|
2886
|
+
});
|
|
2887
|
+
await maybeNotifyAboutUpdate({ disabled: !implicit.updateCheck });
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
const detected = detectDevCommand({
|
|
2891
|
+
cwd: implicit.cwd,
|
|
2892
|
+
...projectConfig.config.command ? { command: projectConfig.config.command } : {}
|
|
2893
|
+
});
|
|
2894
|
+
console.log(`Localghost detected: ${formatDetectedDevCommand(detected)}`);
|
|
2895
|
+
if (implicit.dryRun) return;
|
|
2896
|
+
await program.parseAsync([
|
|
2897
|
+
process.argv[0] ?? process.execPath,
|
|
2898
|
+
process.argv[1] ?? "localghost",
|
|
2899
|
+
...implicit.updateCheck ? [] : ["--no-update-check"],
|
|
2900
|
+
"run",
|
|
2901
|
+
"--cwd",
|
|
2902
|
+
implicit.cwd,
|
|
2903
|
+
"--",
|
|
2904
|
+
...detected.command
|
|
2905
|
+
]);
|
|
2906
|
+
}
|
|
2907
|
+
main().catch((error) => {
|
|
2006
2908
|
const message = error instanceof Error ? error.message : String(error);
|
|
2007
2909
|
console.error(message);
|
|
2008
2910
|
process.exitCode = 1;
|