@hamedb89/localghost 0.1.10 → 0.1.12
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 +520 -32
- package/dist/cli.js +990 -90
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +342 -92
- package/dist/index.js +2048 -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 +2 -2
- package/docs/ghost-tunnel.md +206 -20
- package/docs/localghost.1.md +42 -6
- 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,11 +1510,11 @@ 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";
|
|
879
1518
|
return "npm";
|
|
880
1519
|
}
|
|
881
1520
|
function packageRunCommand(packageManager, script) {
|
|
@@ -893,9 +1532,9 @@ function renderConfig(options) {
|
|
|
893
1532
|
""
|
|
894
1533
|
].join("\n");
|
|
895
1534
|
}
|
|
896
|
-
function
|
|
1535
|
+
function readPackageJson2(path) {
|
|
897
1536
|
try {
|
|
898
|
-
return JSON.parse(
|
|
1537
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
899
1538
|
} catch {
|
|
900
1539
|
return null;
|
|
901
1540
|
}
|
|
@@ -908,7 +1547,7 @@ function getConfigFlag(configFile) {
|
|
|
908
1547
|
return configFile === LOCALGHOST_CONFIG_FILE ? "" : ` --config ${shellQuote(configFile)}`;
|
|
909
1548
|
}
|
|
910
1549
|
function updatePackageScripts(packageJsonPath, configFile) {
|
|
911
|
-
const pkg =
|
|
1550
|
+
const pkg = readPackageJson2(packageJsonPath);
|
|
912
1551
|
if (!pkg) return false;
|
|
913
1552
|
const scripts = typeof pkg.scripts === "object" && pkg.scripts ? pkg.scripts : {};
|
|
914
1553
|
const configFlag = getConfigFlag(configFile);
|
|
@@ -919,6 +1558,7 @@ function updatePackageScripts(packageJsonPath, configFile) {
|
|
|
919
1558
|
"localghost:proxy:https": scripts["localghost:proxy:https"] ?? `localghost dev${configFlag} --https`,
|
|
920
1559
|
"localghost:run": scripts["localghost:run"] ?? `localghost run${configFlag} --`,
|
|
921
1560
|
"localghost:ready": scripts["localghost:ready"] ?? `localghost status${configFlag} --ready`,
|
|
1561
|
+
"localghost:repair": scripts["localghost:repair"] ?? `localghost repair${configFlag}`,
|
|
922
1562
|
"localghost:trust": scripts["localghost:trust"] ?? `localghost trust${configFlag}`,
|
|
923
1563
|
"localghost:ps": scripts["localghost:ps"] ?? "localghost ps",
|
|
924
1564
|
"localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
|
|
@@ -947,8 +1587,8 @@ function initLocalghost(options = {}) {
|
|
|
947
1587
|
const apiPort = options.apiPort ?? 8787;
|
|
948
1588
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
949
1589
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
950
|
-
const configPath =
|
|
951
|
-
const configExists =
|
|
1590
|
+
const configPath = join7(cwd, configFile);
|
|
1591
|
+
const configExists = existsSync5(configPath);
|
|
952
1592
|
if (configExists && !options.force) {
|
|
953
1593
|
return {
|
|
954
1594
|
configPath,
|
|
@@ -964,12 +1604,12 @@ function initLocalghost(options = {}) {
|
|
|
964
1604
|
};
|
|
965
1605
|
}
|
|
966
1606
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
967
|
-
const packageJsonPath =
|
|
1607
|
+
const packageJsonPath = join7(cwd, "package.json");
|
|
968
1608
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
969
1609
|
return {
|
|
970
1610
|
configPath,
|
|
971
1611
|
configCreated: true,
|
|
972
|
-
...
|
|
1612
|
+
...existsSync5(packageJsonPath) ? { packageJsonPath } : {},
|
|
973
1613
|
packageJsonChanged,
|
|
974
1614
|
packageManager,
|
|
975
1615
|
nextSteps: [
|
|
@@ -1061,21 +1701,22 @@ function formatGhostTunnel(config, options = {}) {
|
|
|
1061
1701
|
if (options.verbose) {
|
|
1062
1702
|
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
1063
1703
|
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
1064
|
-
lines.push(`
|
|
1704
|
+
lines.push(` protocol: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
1705
|
+
lines.push(` transport: ${config.transport.kind}`);
|
|
1065
1706
|
}
|
|
1066
1707
|
return lines.join("\n");
|
|
1067
1708
|
}
|
|
1068
1709
|
|
|
1069
1710
|
// src/state.ts
|
|
1070
|
-
import { existsSync as
|
|
1071
|
-
import { join as
|
|
1711
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1712
|
+
import { join as join8 } from "path";
|
|
1072
1713
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
1073
1714
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
1074
|
-
return
|
|
1715
|
+
return join8(cwd, LOCALGHOST_STATE_FILE);
|
|
1075
1716
|
}
|
|
1076
1717
|
function readLocalghostState(cwd = process.cwd()) {
|
|
1077
1718
|
const path = getLocalghostStatePath(cwd);
|
|
1078
|
-
if (!
|
|
1719
|
+
if (!existsSync6(path)) return null;
|
|
1079
1720
|
return JSON.parse(readTextFile(path));
|
|
1080
1721
|
}
|
|
1081
1722
|
function writeLocalghostState(cwd, state) {
|
|
@@ -1091,11 +1732,11 @@ function patchLocalghostState(cwd, patch) {
|
|
|
1091
1732
|
}
|
|
1092
1733
|
|
|
1093
1734
|
// src/update-check.ts
|
|
1094
|
-
import { existsSync as
|
|
1735
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1095
1736
|
import { homedir as homedir2 } from "os";
|
|
1096
|
-
import { dirname as dirname4, join as
|
|
1737
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
1097
1738
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
1098
|
-
var LOCALGHOST_VERSION = "0.1.
|
|
1739
|
+
var LOCALGHOST_VERSION = "0.1.12";
|
|
1099
1740
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1100
1741
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1101
1742
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -1107,13 +1748,13 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
1107
1748
|
}
|
|
1108
1749
|
function getUpdateCheckCachePath(env = process.env) {
|
|
1109
1750
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
1110
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
1111
|
-
return
|
|
1751
|
+
const cacheRoot = env.XDG_CACHE_HOME || join9(homedir2(), ".cache");
|
|
1752
|
+
return join9(cacheRoot, "localghost", "update-check.json");
|
|
1112
1753
|
}
|
|
1113
1754
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
1114
|
-
if (!
|
|
1755
|
+
if (!existsSync7(path)) return null;
|
|
1115
1756
|
try {
|
|
1116
|
-
return JSON.parse(
|
|
1757
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
1117
1758
|
} catch {
|
|
1118
1759
|
return null;
|
|
1119
1760
|
}
|
|
@@ -1270,6 +1911,10 @@ function warnAboutLocalMdns(entries) {
|
|
|
1270
1911
|
function shouldColor() {
|
|
1271
1912
|
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1272
1913
|
}
|
|
1914
|
+
function printLocalghostBanner() {
|
|
1915
|
+
console.log(renderLocalghostBanner());
|
|
1916
|
+
console.log("");
|
|
1917
|
+
}
|
|
1273
1918
|
function logDomainRoutes(entries, options = {}) {
|
|
1274
1919
|
console.log(formatDomainRoutes(entries, options));
|
|
1275
1920
|
if (options.ghostTunnel?.enabled) {
|
|
@@ -1308,7 +1953,8 @@ function contextOptionsFromCli(options) {
|
|
|
1308
1953
|
...options.project ? { project: options.project } : {},
|
|
1309
1954
|
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
1310
1955
|
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1311
|
-
...useHttps(options) ? { https: true } : {}
|
|
1956
|
+
...useHttps(options) ? { https: true } : {},
|
|
1957
|
+
...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
|
|
1312
1958
|
};
|
|
1313
1959
|
}
|
|
1314
1960
|
function readOptionsFromCli(options) {
|
|
@@ -1372,7 +2018,7 @@ function getSetupReadiness(options) {
|
|
|
1372
2018
|
}
|
|
1373
2019
|
const hostsPath = getSystemHostsPath();
|
|
1374
2020
|
try {
|
|
1375
|
-
const hosts =
|
|
2021
|
+
const hosts = readFileSync8(hostsPath, "utf8");
|
|
1376
2022
|
const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
|
|
1377
2023
|
if (!hosts.includes(expectedHostsBlock)) {
|
|
1378
2024
|
reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
|
|
@@ -1382,11 +2028,11 @@ function getSetupReadiness(options) {
|
|
|
1382
2028
|
reasons.push(`Could not read ${hostsPath}: ${message}`);
|
|
1383
2029
|
}
|
|
1384
2030
|
if (!options.ignoreCaddyfile) {
|
|
1385
|
-
if (!
|
|
2031
|
+
if (!existsSync8(caddyfilePath)) {
|
|
1386
2032
|
reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
|
|
1387
2033
|
} else {
|
|
1388
2034
|
const expectedCaddyfile = renderCaddyfile(entries, { https });
|
|
1389
|
-
const currentCaddyfile =
|
|
2035
|
+
const currentCaddyfile = readFileSync8(caddyfilePath, "utf8");
|
|
1390
2036
|
if (currentCaddyfile !== expectedCaddyfile) {
|
|
1391
2037
|
reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
|
|
1392
2038
|
}
|
|
@@ -1430,15 +2076,15 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
1430
2076
|
entries: readiness.entries
|
|
1431
2077
|
});
|
|
1432
2078
|
}
|
|
1433
|
-
function
|
|
1434
|
-
return new Promise((
|
|
2079
|
+
function wait2(ms) {
|
|
2080
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1435
2081
|
}
|
|
1436
2082
|
async function runTrust(cwd, caddyfilePath) {
|
|
1437
|
-
await
|
|
2083
|
+
await wait2(350);
|
|
1438
2084
|
try {
|
|
1439
2085
|
await trustCaddy(caddyfilePath);
|
|
1440
2086
|
} catch {
|
|
1441
|
-
await
|
|
2087
|
+
await wait2(750);
|
|
1442
2088
|
await trustCaddy(caddyfilePath);
|
|
1443
2089
|
}
|
|
1444
2090
|
patchLocalghostState(cwd, { caddyTrustedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -1478,6 +2124,131 @@ function registerCleanup(id) {
|
|
|
1478
2124
|
process.off("exit", cleanup);
|
|
1479
2125
|
};
|
|
1480
2126
|
}
|
|
2127
|
+
async function resolveServiceRuntimeEntries(services, dynamicPort) {
|
|
2128
|
+
const usedPorts = /* @__PURE__ */ new Set();
|
|
2129
|
+
const resolved = [];
|
|
2130
|
+
for (const service of services) {
|
|
2131
|
+
let port = service.requestedPort;
|
|
2132
|
+
if (dynamicPort) {
|
|
2133
|
+
let found = false;
|
|
2134
|
+
for (let offset = 0; offset < 50; offset += 1) {
|
|
2135
|
+
const candidate = service.requestedPort + offset;
|
|
2136
|
+
if (candidate > 65535 || usedPorts.has(candidate)) continue;
|
|
2137
|
+
if (await isPortAvailable(candidate)) {
|
|
2138
|
+
port = candidate;
|
|
2139
|
+
found = true;
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (!found) {
|
|
2144
|
+
throw new Error(`No available port found for service ${service.name} from ${service.requestedPort}.`);
|
|
2145
|
+
}
|
|
2146
|
+
} else if (usedPorts.has(port)) {
|
|
2147
|
+
throw new Error(`Services cannot start separate commands on the same fixed port: ${port}.`);
|
|
2148
|
+
}
|
|
2149
|
+
usedPorts.add(port);
|
|
2150
|
+
resolved.push({
|
|
2151
|
+
...service,
|
|
2152
|
+
port,
|
|
2153
|
+
entry: {
|
|
2154
|
+
host: service.host,
|
|
2155
|
+
port,
|
|
2156
|
+
target: `127.0.0.1:${port}`
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
return resolved;
|
|
2161
|
+
}
|
|
2162
|
+
async function waitForServicePorts(entries, timeoutMs = 1e4) {
|
|
2163
|
+
const deadline = Date.now() + timeoutMs;
|
|
2164
|
+
const ports = [...new Set(entries.map((entry) => entry.port))];
|
|
2165
|
+
while (Date.now() < deadline) {
|
|
2166
|
+
const availability = await Promise.all(ports.map((port) => isPortAvailable(port)));
|
|
2167
|
+
if (availability.every((available) => !available)) return true;
|
|
2168
|
+
await wait2(50);
|
|
2169
|
+
}
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
async function runDetectedServices(options) {
|
|
2173
|
+
assertLocalDevelopment("run");
|
|
2174
|
+
await assertCaddyReady();
|
|
2175
|
+
const runtimeServices = await resolveServiceRuntimeEntries(options.services, options.dynamicPort);
|
|
2176
|
+
const entries = runtimeServices.map((service) => service.entry);
|
|
2177
|
+
const readiness = getSetupReadiness({
|
|
2178
|
+
cwd: options.cwd,
|
|
2179
|
+
https: options.https,
|
|
2180
|
+
ignoreCaddyfile: true,
|
|
2181
|
+
entries,
|
|
2182
|
+
configPath: options.configPath,
|
|
2183
|
+
projectName: options.projectName
|
|
2184
|
+
});
|
|
2185
|
+
if (!readiness.ready) {
|
|
2186
|
+
if (!options.autoRepair) {
|
|
2187
|
+
throw new Error([
|
|
2188
|
+
"Localghost setup is missing or stale.",
|
|
2189
|
+
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
2190
|
+
"Automatic repair is disabled. Enable autoRepair or run localghost repair."
|
|
2191
|
+
].join("\n"));
|
|
2192
|
+
}
|
|
2193
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
2194
|
+
await runSetupFromReadiness(options.cwd, options.https, readiness);
|
|
2195
|
+
}
|
|
2196
|
+
for (const service of runtimeServices) {
|
|
2197
|
+
if (service.port !== service.requestedPort) {
|
|
2198
|
+
console.log(`${service.name}: port ${service.requestedPort} is busy; using ${service.port}.`);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
const caddyfile = await writeCaddyfile(entries, options.cwd, { https: options.https });
|
|
2202
|
+
await validateCaddyfile(caddyfile);
|
|
2203
|
+
const caddy = startCaddy(caddyfile);
|
|
2204
|
+
const caddyExit = caddy.catch((error) => {
|
|
2205
|
+
if (!caddy.killed) throw error;
|
|
2206
|
+
});
|
|
2207
|
+
const children = runtimeServices.map((service) => execa4(service.command[0], service.command.slice(1), {
|
|
2208
|
+
cwd: service.cwd,
|
|
2209
|
+
stdio: "inherit",
|
|
2210
|
+
env: {
|
|
2211
|
+
...process.env,
|
|
2212
|
+
LOCALGHOST_PORT: String(service.port),
|
|
2213
|
+
LOCALGHOST_DYNAMIC_PORT: options.dynamicPort ? "1" : "0",
|
|
2214
|
+
LOCALGHOST_SERVICE: service.name,
|
|
2215
|
+
VITE_PORT: String(service.port)
|
|
2216
|
+
}
|
|
2217
|
+
}));
|
|
2218
|
+
const caddyPid = maybePid(caddy.pid);
|
|
2219
|
+
const runRecord = registerLocalghostRun({
|
|
2220
|
+
mode: "run",
|
|
2221
|
+
cwd: options.cwd,
|
|
2222
|
+
projectName: options.projectName,
|
|
2223
|
+
configPath: options.configPath,
|
|
2224
|
+
caddyfilePath: caddyfile,
|
|
2225
|
+
...caddyPid ? { caddyPid } : {},
|
|
2226
|
+
childCommand: ["services", ...runtimeServices.map((service) => service.name)],
|
|
2227
|
+
https: options.https,
|
|
2228
|
+
dynamicPort: options.dynamicPort,
|
|
2229
|
+
entries
|
|
2230
|
+
});
|
|
2231
|
+
const cleanupRun = registerCleanup(runRecord.id);
|
|
2232
|
+
const processExit = Promise.race([caddyExit, ...children]);
|
|
2233
|
+
try {
|
|
2234
|
+
const ready = await Promise.race([
|
|
2235
|
+
waitForServicePorts(entries),
|
|
2236
|
+
processExit.then(() => false)
|
|
2237
|
+
]);
|
|
2238
|
+
if (ready) {
|
|
2239
|
+
console.log("");
|
|
2240
|
+
logDomainRoutes(entries, { https: options.https });
|
|
2241
|
+
}
|
|
2242
|
+
await processExit;
|
|
2243
|
+
} finally {
|
|
2244
|
+
for (const child of children) {
|
|
2245
|
+
if (!child.killed) child.kill("SIGINT");
|
|
2246
|
+
}
|
|
2247
|
+
if (!caddy.killed) caddy.kill("SIGINT");
|
|
2248
|
+
await Promise.allSettled([caddyExit, ...children]);
|
|
2249
|
+
cleanupRun();
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
1481
2252
|
async function getRouteViews(entries) {
|
|
1482
2253
|
const portStatus = /* @__PURE__ */ new Map();
|
|
1483
2254
|
for (const entry of entries) {
|
|
@@ -1629,6 +2400,7 @@ program.command("update").description("Check npm for a newer localghost release"
|
|
|
1629
2400
|
});
|
|
1630
2401
|
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
2402
|
assertLocalDevelopment("setup");
|
|
2403
|
+
printLocalghostBanner();
|
|
1632
2404
|
await assertCaddyReady();
|
|
1633
2405
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1634
2406
|
const https = context.https;
|
|
@@ -1686,6 +2458,32 @@ program.command("trust").description("Trust Caddy's local HTTPS CA for this proj
|
|
|
1686
2458
|
await validateCaddyfile(caddyfile);
|
|
1687
2459
|
await runTrust(options.cwd, caddyfile);
|
|
1688
2460
|
});
|
|
2461
|
+
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) => {
|
|
2462
|
+
assertLocalDevelopment("repair");
|
|
2463
|
+
printLocalghostBanner();
|
|
2464
|
+
await assertCaddyReady();
|
|
2465
|
+
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
2466
|
+
const readiness = getSetupReadiness({
|
|
2467
|
+
...options,
|
|
2468
|
+
https: context.https,
|
|
2469
|
+
entries: context.entries,
|
|
2470
|
+
configPath: context.configPath,
|
|
2471
|
+
projectName: context.projectName
|
|
2472
|
+
});
|
|
2473
|
+
if (options.trust && !context.https) {
|
|
2474
|
+
throw new Error("Cannot repair HTTPS trust unless HTTPS is enabled. Pass --https or configure https: true.");
|
|
2475
|
+
}
|
|
2476
|
+
warnAboutLocalMdns(context.entries);
|
|
2477
|
+
logDomainRoutes(context.entries, { https: context.https, ghostTunnel: context.ghostTunnel });
|
|
2478
|
+
await runSetupFromReadiness(options.cwd, context.https, readiness);
|
|
2479
|
+
if (options.trust) {
|
|
2480
|
+
await runTrust(options.cwd, readiness.caddyfilePath);
|
|
2481
|
+
}
|
|
2482
|
+
console.log(`Repaired hosts: ${getSystemHostsPath()}`);
|
|
2483
|
+
console.log(`Repaired Caddyfile: ${readiness.caddyfilePath}`);
|
|
2484
|
+
console.log(`Repaired state: ${readiness.statePath}`);
|
|
2485
|
+
console.log("Repair complete.");
|
|
2486
|
+
});
|
|
1689
2487
|
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
2488
|
assertLocalDevelopment("reset");
|
|
1691
2489
|
const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
|
|
@@ -1693,13 +2491,13 @@ program.command("reset").description("Remove Localghost setup state without dele
|
|
|
1693
2491
|
const statePath = getLocalghostStatePath(options.cwd);
|
|
1694
2492
|
explainHostsPassword();
|
|
1695
2493
|
const hostsResult = await removeSystemHosts(projectName);
|
|
1696
|
-
if (
|
|
2494
|
+
if (existsSync8(caddyfilePath)) {
|
|
1697
2495
|
unlinkSync(caddyfilePath);
|
|
1698
2496
|
console.log(`Removed ${caddyfilePath}`);
|
|
1699
2497
|
} else {
|
|
1700
2498
|
console.log(`${caddyfilePath} was not present`);
|
|
1701
2499
|
}
|
|
1702
|
-
if (
|
|
2500
|
+
if (existsSync8(statePath)) {
|
|
1703
2501
|
unlinkSync(statePath);
|
|
1704
2502
|
console.log(`Removed ${statePath}`);
|
|
1705
2503
|
} else {
|
|
@@ -1720,7 +2518,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
1720
2518
|
const hostsResult = await removeSystemHosts(projectName);
|
|
1721
2519
|
const caddyfilePath = getCaddyfilePath(options.cwd);
|
|
1722
2520
|
let caddyfileRemoved = false;
|
|
1723
|
-
if (options.removeCaddyfile &&
|
|
2521
|
+
if (options.removeCaddyfile && existsSync8(caddyfilePath)) {
|
|
1724
2522
|
unlinkSync(caddyfilePath);
|
|
1725
2523
|
caddyfileRemoved = true;
|
|
1726
2524
|
}
|
|
@@ -1800,7 +2598,7 @@ program.command("routes").description("Print domain to upstream routes").option(
|
|
|
1800
2598
|
}));
|
|
1801
2599
|
}
|
|
1802
2600
|
});
|
|
1803
|
-
program.command("dev").description("Run the Localghost Caddy proxy
|
|
2601
|
+
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
2602
|
assertLocalDevelopment("dev");
|
|
1805
2603
|
await assertCaddyReady();
|
|
1806
2604
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
@@ -1813,41 +2611,18 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1813
2611
|
projectName: context.projectName
|
|
1814
2612
|
});
|
|
1815
2613
|
if (!readiness.ready) {
|
|
1816
|
-
if (!options.setup) {
|
|
2614
|
+
if (!options.setup && !context.autoRepair) {
|
|
1817
2615
|
throw new Error(
|
|
1818
2616
|
[
|
|
1819
2617
|
"Localghost setup is missing or stale.",
|
|
1820
2618
|
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
1821
2619
|
`Run: ${readiness.setupCommand}`,
|
|
1822
|
-
"
|
|
2620
|
+
"Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
|
|
1823
2621
|
].join("\n")
|
|
1824
2622
|
);
|
|
1825
2623
|
}
|
|
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
|
-
});
|
|
2624
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
2625
|
+
await runSetupFromReadiness(options.cwd, https, readiness);
|
|
1851
2626
|
}
|
|
1852
2627
|
warnAboutLocalMdns(readiness.entries);
|
|
1853
2628
|
logDomainRoutes(readiness.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
@@ -1883,7 +2658,7 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1883
2658
|
cleanupRun();
|
|
1884
2659
|
}
|
|
1885
2660
|
});
|
|
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", "
|
|
2661
|
+
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
2662
|
assertLocalDevelopment("run");
|
|
1888
2663
|
await assertCaddyReady();
|
|
1889
2664
|
const context = await resolveLocalghostContext({
|
|
@@ -1893,7 +2668,8 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1893
2668
|
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1894
2669
|
...options.port ? { port: options.port } : {},
|
|
1895
2670
|
...useHttps(options) ? { https: true } : {},
|
|
1896
|
-
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {}
|
|
2671
|
+
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {},
|
|
2672
|
+
...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
|
|
1897
2673
|
});
|
|
1898
2674
|
const https = context.https;
|
|
1899
2675
|
const readiness = getSetupReadiness({
|
|
@@ -1905,18 +2681,19 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1905
2681
|
projectName: context.projectName
|
|
1906
2682
|
});
|
|
1907
2683
|
if (!readiness.ready) {
|
|
1908
|
-
|
|
1909
|
-
if (!shouldSetup) {
|
|
2684
|
+
if (!options.setup && !context.autoRepair) {
|
|
1910
2685
|
throw new Error(
|
|
1911
2686
|
[
|
|
1912
2687
|
"Localghost setup is missing or stale.",
|
|
1913
2688
|
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
1914
|
-
`Run: ${readiness.setupCommand}
|
|
2689
|
+
`Run: ${readiness.setupCommand}`,
|
|
2690
|
+
"Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
|
|
1915
2691
|
].join("\n")
|
|
1916
2692
|
);
|
|
1917
2693
|
}
|
|
2694
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
1918
2695
|
await runSetupFromReadiness(options.cwd, https, readiness);
|
|
1919
|
-
console.log(`
|
|
2696
|
+
console.log(`Repair complete. Setup state: ${getLocalghostStatePath(options.cwd)}`);
|
|
1920
2697
|
}
|
|
1921
2698
|
if (context.dynamicPort && context.port !== context.requestedPort) {
|
|
1922
2699
|
console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
|
|
@@ -1987,6 +2764,53 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1987
2764
|
cleanupRun();
|
|
1988
2765
|
}
|
|
1989
2766
|
});
|
|
2767
|
+
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) => {
|
|
2768
|
+
assertLocalDevelopment("tunnel");
|
|
2769
|
+
const context = await resolveLocalghostContext({
|
|
2770
|
+
cwd: options.cwd,
|
|
2771
|
+
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
2772
|
+
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
2773
|
+
dynamicPort: false
|
|
2774
|
+
});
|
|
2775
|
+
if (!context.ghostTunnel.enabled) {
|
|
2776
|
+
throw new Error("Ghost Tunnel is not enabled in localghost.config.mjs.");
|
|
2777
|
+
}
|
|
2778
|
+
if (context.ghostTunnel.transport.kind !== "tunnel") {
|
|
2779
|
+
throw new Error(`Ghost Tunnel transport must be tunnel for localghost tunnel. Current: ${context.ghostTunnel.transport.kind}`);
|
|
2780
|
+
}
|
|
2781
|
+
const entries = listGhostTunnelEntries({
|
|
2782
|
+
cwd: options.cwd,
|
|
2783
|
+
fileName: options.ghostConfig
|
|
2784
|
+
});
|
|
2785
|
+
if (entries.length === 0) {
|
|
2786
|
+
throw new Error(`No exact Ghost Tunnel routes found in ${options.ghostConfig}.`);
|
|
2787
|
+
}
|
|
2788
|
+
const transport = context.ghostTunnel.transport;
|
|
2789
|
+
const store = createRedisGhostTunnelStoreFromEnv({
|
|
2790
|
+
namespace: transport.store.namespace
|
|
2791
|
+
});
|
|
2792
|
+
const controller = new AbortController();
|
|
2793
|
+
const stop = () => controller.abort();
|
|
2794
|
+
process.once("SIGINT", stop);
|
|
2795
|
+
process.once("SIGTERM", stop);
|
|
2796
|
+
const agent = startGhostTunnelAgent({
|
|
2797
|
+
entries,
|
|
2798
|
+
store,
|
|
2799
|
+
targetHost: options.targetHost,
|
|
2800
|
+
routeTtlSeconds: transport.routeTtlSeconds,
|
|
2801
|
+
requestTtlSeconds: transport.requestTtlSeconds,
|
|
2802
|
+
pollIntervalMs: transport.pollIntervalMs,
|
|
2803
|
+
maxResponseBodyBytes: transport.maxResponseBodyBytes,
|
|
2804
|
+
signal: controller.signal,
|
|
2805
|
+
log: (message) => console.log(message)
|
|
2806
|
+
});
|
|
2807
|
+
try {
|
|
2808
|
+
await agent.done;
|
|
2809
|
+
} finally {
|
|
2810
|
+
process.off("SIGINT", stop);
|
|
2811
|
+
process.off("SIGTERM", stop);
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
1990
2814
|
program.command("ps").description("Show Localghost setups and currently running sessions").option("--json", "Print raw JSON").action(async (options) => {
|
|
1991
2815
|
const setups = listLocalghostSetups();
|
|
1992
2816
|
const runs = listLocalghostRuns();
|
|
@@ -2002,7 +2826,83 @@ program.command("print").description("Print parsed host config").option("--cwd <
|
|
|
2002
2826
|
warnAboutLocalMdns(entries);
|
|
2003
2827
|
console.log(JSON.stringify(entries, null, 2));
|
|
2004
2828
|
});
|
|
2005
|
-
|
|
2829
|
+
function readImplicitInvocation(args) {
|
|
2830
|
+
if (args.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V")) return null;
|
|
2831
|
+
let cwd = process.cwd();
|
|
2832
|
+
let dryRun = false;
|
|
2833
|
+
let updateCheck = true;
|
|
2834
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2835
|
+
const arg = args[index];
|
|
2836
|
+
if (!arg) continue;
|
|
2837
|
+
if (arg === "--dry-run") {
|
|
2838
|
+
dryRun = true;
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2841
|
+
if (arg === "--no-update-check") {
|
|
2842
|
+
updateCheck = false;
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
if (arg === "--cwd") {
|
|
2846
|
+
const value = args[index + 1];
|
|
2847
|
+
if (!value) throw new Error("--cwd requires a path.");
|
|
2848
|
+
cwd = value;
|
|
2849
|
+
index += 1;
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
if (arg.startsWith("--cwd=")) {
|
|
2853
|
+
cwd = arg.slice("--cwd=".length);
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
return null;
|
|
2857
|
+
}
|
|
2858
|
+
return { cwd, dryRun, updateCheck };
|
|
2859
|
+
}
|
|
2860
|
+
async function main() {
|
|
2861
|
+
const implicit = readImplicitInvocation(process.argv.slice(2));
|
|
2862
|
+
if (!implicit) {
|
|
2863
|
+
await program.parseAsync();
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const projectConfig = await readLocalghostProjectConfig({ cwd: implicit.cwd });
|
|
2867
|
+
printLocalghostBanner();
|
|
2868
|
+
if (projectConfig.config.services) {
|
|
2869
|
+
if (!projectConfig.path) throw new Error("Multi-service configuration must come from localghost.config.mjs.");
|
|
2870
|
+
const services = detectDevServices({
|
|
2871
|
+
cwd: implicit.cwd,
|
|
2872
|
+
services: projectConfig.config.services
|
|
2873
|
+
});
|
|
2874
|
+
console.log(formatDetectedDevServices(services));
|
|
2875
|
+
if (implicit.dryRun) return;
|
|
2876
|
+
await runDetectedServices({
|
|
2877
|
+
cwd: implicit.cwd,
|
|
2878
|
+
services,
|
|
2879
|
+
configPath: projectConfig.path,
|
|
2880
|
+
projectName: sanitizeProjectName(projectConfig.config.project ?? getProjectName(implicit.cwd)),
|
|
2881
|
+
https: projectConfig.config.https ?? false,
|
|
2882
|
+
dynamicPort: projectConfig.config.dynamicPort ?? true,
|
|
2883
|
+
autoRepair: projectConfig.config.autoRepair ?? true
|
|
2884
|
+
});
|
|
2885
|
+
await maybeNotifyAboutUpdate({ disabled: !implicit.updateCheck });
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
const detected = detectDevCommand({
|
|
2889
|
+
cwd: implicit.cwd,
|
|
2890
|
+
...projectConfig.config.command ? { command: projectConfig.config.command } : {}
|
|
2891
|
+
});
|
|
2892
|
+
console.log(`Localghost detected: ${formatDetectedDevCommand(detected)}`);
|
|
2893
|
+
if (implicit.dryRun) return;
|
|
2894
|
+
await program.parseAsync([
|
|
2895
|
+
process.argv[0] ?? process.execPath,
|
|
2896
|
+
process.argv[1] ?? "localghost",
|
|
2897
|
+
...implicit.updateCheck ? [] : ["--no-update-check"],
|
|
2898
|
+
"run",
|
|
2899
|
+
"--cwd",
|
|
2900
|
+
implicit.cwd,
|
|
2901
|
+
"--",
|
|
2902
|
+
...detected.command
|
|
2903
|
+
]);
|
|
2904
|
+
}
|
|
2905
|
+
main().catch((error) => {
|
|
2006
2906
|
const message = error instanceof Error ? error.message : String(error);
|
|
2007
2907
|
console.error(message);
|
|
2008
2908
|
process.exitCode = 1;
|