@hamedb89/localghost 0.1.9 → 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 +340 -254
- package/dist/cli.js +993 -89
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +342 -92
- package/dist/index.js +2048 -925
- 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 +131 -17
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +3 -3
- package/docs/ghost-tunnel.md +248 -18
- 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,8 +714,11 @@ function getDisplayValues(input2) {
|
|
|
495
714
|
...input2.values
|
|
496
715
|
};
|
|
497
716
|
}
|
|
717
|
+
function getDisplayDefaults(defaults) {
|
|
718
|
+
return defaults;
|
|
719
|
+
}
|
|
498
720
|
function createDisplayUrl(config, defaults, domain) {
|
|
499
|
-
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
721
|
+
const input2 = getPreviewDefaults(config.preview, getDisplayDefaults(defaults));
|
|
500
722
|
const protocol = input2.protocol ?? "https";
|
|
501
723
|
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
|
|
502
724
|
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
|
|
@@ -505,8 +727,9 @@ function createDisplayUrl(config, defaults, domain) {
|
|
|
505
727
|
return `${url}${input2.path.replace(/^\/+/, "")}`;
|
|
506
728
|
}
|
|
507
729
|
function createDisplayUrls(config, defaults) {
|
|
508
|
-
const
|
|
509
|
-
const
|
|
730
|
+
const displayDefaults = getDisplayDefaults(defaults);
|
|
731
|
+
const domains = config.domains.length > 0 ? config.domains : displayDefaults?.domain ? [displayDefaults.domain] : [];
|
|
732
|
+
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, displayDefaults, domain)) : [createDisplayUrl(config, displayDefaults)];
|
|
510
733
|
return [...new Set(urls)];
|
|
511
734
|
}
|
|
512
735
|
function maybeConstructPreviewUrl(config, defaults) {
|
|
@@ -534,7 +757,8 @@ function resolveGhostTunnelConfig(options, defaults) {
|
|
|
534
757
|
namespace: resolveNamespaceConfig(void 0),
|
|
535
758
|
displayUrls: [],
|
|
536
759
|
requireHttps: true,
|
|
537
|
-
requireAuth: true
|
|
760
|
+
requireAuth: true,
|
|
761
|
+
transport: resolveGhostTunnelTransport(void 0)
|
|
538
762
|
};
|
|
539
763
|
}
|
|
540
764
|
const config = typeof options === "string" ? { mode: options } : options;
|
|
@@ -542,6 +766,8 @@ function resolveGhostTunnelConfig(options, defaults) {
|
|
|
542
766
|
assertValidSubdomain(subdomain);
|
|
543
767
|
const domains = normalizeDomains(config.domains);
|
|
544
768
|
const enabled = config.enabled ?? true;
|
|
769
|
+
const adapter = resolveGhostTunnelAdapter(config.adapter);
|
|
770
|
+
const transport = resolveGhostTunnelTransport(config.transport ?? getLegacyGhostTunnelTransport(config.adapter));
|
|
545
771
|
const resolved = {
|
|
546
772
|
enabled,
|
|
547
773
|
mode: parseGhostTunnelMode(config.mode),
|
|
@@ -551,7 +777,9 @@ function resolveGhostTunnelConfig(options, defaults) {
|
|
|
551
777
|
...config.preview ? { preview: config.preview } : {},
|
|
552
778
|
displayUrls: [],
|
|
553
779
|
requireHttps: config.requireHttps ?? true,
|
|
554
|
-
requireAuth: config.requireAuth ?? true
|
|
780
|
+
requireAuth: config.requireAuth ?? true,
|
|
781
|
+
transport,
|
|
782
|
+
...adapter ? { adapter } : {}
|
|
555
783
|
};
|
|
556
784
|
if (!enabled) {
|
|
557
785
|
return resolved;
|
|
@@ -632,7 +860,7 @@ function envHttps() {
|
|
|
632
860
|
}
|
|
633
861
|
function getPackageName(cwd) {
|
|
634
862
|
try {
|
|
635
|
-
const pkg = JSON.parse(
|
|
863
|
+
const pkg = JSON.parse(readFileSync5(join5(cwd, "package.json"), "utf8"));
|
|
636
864
|
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
637
865
|
} catch {
|
|
638
866
|
return void 0;
|
|
@@ -661,7 +889,7 @@ function withRuntimePort(entries, requestedPort, port) {
|
|
|
661
889
|
if (requestedPort === port) return entries;
|
|
662
890
|
const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
|
|
663
891
|
if (!hasRequestedPort) return entries;
|
|
664
|
-
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);
|
|
665
893
|
}
|
|
666
894
|
function uniqueHosts(entries) {
|
|
667
895
|
return [...new Set(entries.map((entry) => entry.host))];
|
|
@@ -691,7 +919,7 @@ async function readLocalghostProjectConfig(options = {}) {
|
|
|
691
919
|
const cwd = options.cwd ?? process.cwd();
|
|
692
920
|
if (options.configFile === false) return { config: {} };
|
|
693
921
|
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
694
|
-
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));
|
|
695
923
|
if (!path) return { config: {} };
|
|
696
924
|
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
697
925
|
const config = imported.default ?? imported;
|
|
@@ -712,6 +940,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
712
940
|
const configEntries = readDevHosts(readOptions);
|
|
713
941
|
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
714
942
|
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
943
|
+
const autoRepair = merged.autoRepair ?? true;
|
|
715
944
|
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
716
945
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
717
946
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
@@ -737,6 +966,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
737
966
|
requestedPort,
|
|
738
967
|
port,
|
|
739
968
|
dynamicPort,
|
|
969
|
+
autoRepair,
|
|
740
970
|
bindHost,
|
|
741
971
|
primaryHost,
|
|
742
972
|
https: merged.https ?? envHttps() ?? false,
|
|
@@ -789,10 +1019,418 @@ function assertLocalDevelopment(command, env = process.env) {
|
|
|
789
1019
|
throw new Error(`Localghost only runs in local development. Refusing \`${command}\` because ${reason}.`);
|
|
790
1020
|
}
|
|
791
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
|
+
|
|
792
1430
|
// src/hosts-file.ts
|
|
793
1431
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
794
1432
|
import { tmpdir } from "os";
|
|
795
|
-
import { join as
|
|
1433
|
+
import { join as join6 } from "path";
|
|
796
1434
|
import { execa as execa3 } from "execa";
|
|
797
1435
|
function escapeRegExp(value) {
|
|
798
1436
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -803,7 +1441,8 @@ function getManagedBlockPattern(projectName) {
|
|
|
803
1441
|
const end = `# localghost:end ${sanitizedProjectName}`;
|
|
804
1442
|
return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "m");
|
|
805
1443
|
}
|
|
806
|
-
function getSystemHostsPath() {
|
|
1444
|
+
function getSystemHostsPath(env = process.env) {
|
|
1445
|
+
if (env.LOCALGHOST_HOSTS_PATH) return env.LOCALGHOST_HOSTS_PATH;
|
|
807
1446
|
return process.platform === "win32" ? "C:\\Windows\\System32\\drivers\\etc\\hosts" : "/etc/hosts";
|
|
808
1447
|
}
|
|
809
1448
|
function renderHostsBlock(projectName, entries) {
|
|
@@ -834,8 +1473,12 @@ function removeManagedBlock(existing, projectName) {
|
|
|
834
1473
|
}
|
|
835
1474
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
836
1475
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
837
|
-
const tempPath =
|
|
1476
|
+
const tempPath = join6(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
838
1477
|
writeFileSync3(tempPath, next, "utf8");
|
|
1478
|
+
if (process.env.LOCALGHOST_HOSTS_PATH) {
|
|
1479
|
+
writeFileSync3(hostsPath, next, "utf8");
|
|
1480
|
+
return tempPath;
|
|
1481
|
+
}
|
|
839
1482
|
if (process.platform === "win32") {
|
|
840
1483
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
841
1484
|
}
|
|
@@ -867,11 +1510,11 @@ async function removeSystemHosts(projectName) {
|
|
|
867
1510
|
}
|
|
868
1511
|
|
|
869
1512
|
// src/init.ts
|
|
870
|
-
import { existsSync as
|
|
871
|
-
import { join as
|
|
1513
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1514
|
+
import { join as join7 } from "path";
|
|
872
1515
|
function detectPackageManager(cwd = process.cwd()) {
|
|
873
|
-
if (
|
|
874
|
-
if (
|
|
1516
|
+
if (existsSync5(join7(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1517
|
+
if (existsSync5(join7(cwd, "yarn.lock"))) return "yarn";
|
|
875
1518
|
return "npm";
|
|
876
1519
|
}
|
|
877
1520
|
function packageRunCommand(packageManager, script) {
|
|
@@ -889,9 +1532,9 @@ function renderConfig(options) {
|
|
|
889
1532
|
""
|
|
890
1533
|
].join("\n");
|
|
891
1534
|
}
|
|
892
|
-
function
|
|
1535
|
+
function readPackageJson2(path) {
|
|
893
1536
|
try {
|
|
894
|
-
return JSON.parse(
|
|
1537
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
895
1538
|
} catch {
|
|
896
1539
|
return null;
|
|
897
1540
|
}
|
|
@@ -904,7 +1547,7 @@ function getConfigFlag(configFile) {
|
|
|
904
1547
|
return configFile === LOCALGHOST_CONFIG_FILE ? "" : ` --config ${shellQuote(configFile)}`;
|
|
905
1548
|
}
|
|
906
1549
|
function updatePackageScripts(packageJsonPath, configFile) {
|
|
907
|
-
const pkg =
|
|
1550
|
+
const pkg = readPackageJson2(packageJsonPath);
|
|
908
1551
|
if (!pkg) return false;
|
|
909
1552
|
const scripts = typeof pkg.scripts === "object" && pkg.scripts ? pkg.scripts : {};
|
|
910
1553
|
const configFlag = getConfigFlag(configFile);
|
|
@@ -915,6 +1558,7 @@ function updatePackageScripts(packageJsonPath, configFile) {
|
|
|
915
1558
|
"localghost:proxy:https": scripts["localghost:proxy:https"] ?? `localghost dev${configFlag} --https`,
|
|
916
1559
|
"localghost:run": scripts["localghost:run"] ?? `localghost run${configFlag} --`,
|
|
917
1560
|
"localghost:ready": scripts["localghost:ready"] ?? `localghost status${configFlag} --ready`,
|
|
1561
|
+
"localghost:repair": scripts["localghost:repair"] ?? `localghost repair${configFlag}`,
|
|
918
1562
|
"localghost:trust": scripts["localghost:trust"] ?? `localghost trust${configFlag}`,
|
|
919
1563
|
"localghost:ps": scripts["localghost:ps"] ?? "localghost ps",
|
|
920
1564
|
"localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
|
|
@@ -943,8 +1587,8 @@ function initLocalghost(options = {}) {
|
|
|
943
1587
|
const apiPort = options.apiPort ?? 8787;
|
|
944
1588
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
945
1589
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
946
|
-
const configPath =
|
|
947
|
-
const configExists =
|
|
1590
|
+
const configPath = join7(cwd, configFile);
|
|
1591
|
+
const configExists = existsSync5(configPath);
|
|
948
1592
|
if (configExists && !options.force) {
|
|
949
1593
|
return {
|
|
950
1594
|
configPath,
|
|
@@ -960,12 +1604,12 @@ function initLocalghost(options = {}) {
|
|
|
960
1604
|
};
|
|
961
1605
|
}
|
|
962
1606
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
963
|
-
const packageJsonPath =
|
|
1607
|
+
const packageJsonPath = join7(cwd, "package.json");
|
|
964
1608
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
965
1609
|
return {
|
|
966
1610
|
configPath,
|
|
967
1611
|
configCreated: true,
|
|
968
|
-
...
|
|
1612
|
+
...existsSync5(packageJsonPath) ? { packageJsonPath } : {},
|
|
969
1613
|
packageJsonChanged,
|
|
970
1614
|
packageManager,
|
|
971
1615
|
nextSteps: [
|
|
@@ -1057,21 +1701,22 @@ function formatGhostTunnel(config, options = {}) {
|
|
|
1057
1701
|
if (options.verbose) {
|
|
1058
1702
|
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
1059
1703
|
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
1060
|
-
lines.push(`
|
|
1704
|
+
lines.push(` protocol: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
1705
|
+
lines.push(` transport: ${config.transport.kind}`);
|
|
1061
1706
|
}
|
|
1062
1707
|
return lines.join("\n");
|
|
1063
1708
|
}
|
|
1064
1709
|
|
|
1065
1710
|
// src/state.ts
|
|
1066
|
-
import { existsSync as
|
|
1067
|
-
import { join as
|
|
1711
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1712
|
+
import { join as join8 } from "path";
|
|
1068
1713
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
1069
1714
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
1070
|
-
return
|
|
1715
|
+
return join8(cwd, LOCALGHOST_STATE_FILE);
|
|
1071
1716
|
}
|
|
1072
1717
|
function readLocalghostState(cwd = process.cwd()) {
|
|
1073
1718
|
const path = getLocalghostStatePath(cwd);
|
|
1074
|
-
if (!
|
|
1719
|
+
if (!existsSync6(path)) return null;
|
|
1075
1720
|
return JSON.parse(readTextFile(path));
|
|
1076
1721
|
}
|
|
1077
1722
|
function writeLocalghostState(cwd, state) {
|
|
@@ -1087,11 +1732,11 @@ function patchLocalghostState(cwd, patch) {
|
|
|
1087
1732
|
}
|
|
1088
1733
|
|
|
1089
1734
|
// src/update-check.ts
|
|
1090
|
-
import { existsSync as
|
|
1735
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1091
1736
|
import { homedir as homedir2 } from "os";
|
|
1092
|
-
import { dirname as dirname4, join as
|
|
1737
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
1093
1738
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
1094
|
-
var LOCALGHOST_VERSION = "0.1.
|
|
1739
|
+
var LOCALGHOST_VERSION = "0.1.12";
|
|
1095
1740
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1096
1741
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1097
1742
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -1103,13 +1748,13 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
1103
1748
|
}
|
|
1104
1749
|
function getUpdateCheckCachePath(env = process.env) {
|
|
1105
1750
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
1106
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
1107
|
-
return
|
|
1751
|
+
const cacheRoot = env.XDG_CACHE_HOME || join9(homedir2(), ".cache");
|
|
1752
|
+
return join9(cacheRoot, "localghost", "update-check.json");
|
|
1108
1753
|
}
|
|
1109
1754
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
1110
|
-
if (!
|
|
1755
|
+
if (!existsSync7(path)) return null;
|
|
1111
1756
|
try {
|
|
1112
|
-
return JSON.parse(
|
|
1757
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
1113
1758
|
} catch {
|
|
1114
1759
|
return null;
|
|
1115
1760
|
}
|
|
@@ -1266,6 +1911,10 @@ function warnAboutLocalMdns(entries) {
|
|
|
1266
1911
|
function shouldColor() {
|
|
1267
1912
|
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1268
1913
|
}
|
|
1914
|
+
function printLocalghostBanner() {
|
|
1915
|
+
console.log(renderLocalghostBanner());
|
|
1916
|
+
console.log("");
|
|
1917
|
+
}
|
|
1269
1918
|
function logDomainRoutes(entries, options = {}) {
|
|
1270
1919
|
console.log(formatDomainRoutes(entries, options));
|
|
1271
1920
|
if (options.ghostTunnel?.enabled) {
|
|
@@ -1304,7 +1953,8 @@ function contextOptionsFromCli(options) {
|
|
|
1304
1953
|
...options.project ? { project: options.project } : {},
|
|
1305
1954
|
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
1306
1955
|
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1307
|
-
...useHttps(options) ? { https: true } : {}
|
|
1956
|
+
...useHttps(options) ? { https: true } : {},
|
|
1957
|
+
...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
|
|
1308
1958
|
};
|
|
1309
1959
|
}
|
|
1310
1960
|
function readOptionsFromCli(options) {
|
|
@@ -1368,7 +2018,7 @@ function getSetupReadiness(options) {
|
|
|
1368
2018
|
}
|
|
1369
2019
|
const hostsPath = getSystemHostsPath();
|
|
1370
2020
|
try {
|
|
1371
|
-
const hosts =
|
|
2021
|
+
const hosts = readFileSync8(hostsPath, "utf8");
|
|
1372
2022
|
const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
|
|
1373
2023
|
if (!hosts.includes(expectedHostsBlock)) {
|
|
1374
2024
|
reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
|
|
@@ -1378,11 +2028,11 @@ function getSetupReadiness(options) {
|
|
|
1378
2028
|
reasons.push(`Could not read ${hostsPath}: ${message}`);
|
|
1379
2029
|
}
|
|
1380
2030
|
if (!options.ignoreCaddyfile) {
|
|
1381
|
-
if (!
|
|
2031
|
+
if (!existsSync8(caddyfilePath)) {
|
|
1382
2032
|
reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
|
|
1383
2033
|
} else {
|
|
1384
2034
|
const expectedCaddyfile = renderCaddyfile(entries, { https });
|
|
1385
|
-
const currentCaddyfile =
|
|
2035
|
+
const currentCaddyfile = readFileSync8(caddyfilePath, "utf8");
|
|
1386
2036
|
if (currentCaddyfile !== expectedCaddyfile) {
|
|
1387
2037
|
reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
|
|
1388
2038
|
}
|
|
@@ -1426,15 +2076,15 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
1426
2076
|
entries: readiness.entries
|
|
1427
2077
|
});
|
|
1428
2078
|
}
|
|
1429
|
-
function
|
|
1430
|
-
return new Promise((
|
|
2079
|
+
function wait2(ms) {
|
|
2080
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1431
2081
|
}
|
|
1432
2082
|
async function runTrust(cwd, caddyfilePath) {
|
|
1433
|
-
await
|
|
2083
|
+
await wait2(350);
|
|
1434
2084
|
try {
|
|
1435
2085
|
await trustCaddy(caddyfilePath);
|
|
1436
2086
|
} catch {
|
|
1437
|
-
await
|
|
2087
|
+
await wait2(750);
|
|
1438
2088
|
await trustCaddy(caddyfilePath);
|
|
1439
2089
|
}
|
|
1440
2090
|
patchLocalghostState(cwd, { caddyTrustedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -1474,6 +2124,131 @@ function registerCleanup(id) {
|
|
|
1474
2124
|
process.off("exit", cleanup);
|
|
1475
2125
|
};
|
|
1476
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
|
+
}
|
|
1477
2252
|
async function getRouteViews(entries) {
|
|
1478
2253
|
const portStatus = /* @__PURE__ */ new Map();
|
|
1479
2254
|
for (const entry of entries) {
|
|
@@ -1625,6 +2400,7 @@ program.command("update").description("Check npm for a newer localghost release"
|
|
|
1625
2400
|
});
|
|
1626
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) => {
|
|
1627
2402
|
assertLocalDevelopment("setup");
|
|
2403
|
+
printLocalghostBanner();
|
|
1628
2404
|
await assertCaddyReady();
|
|
1629
2405
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1630
2406
|
const https = context.https;
|
|
@@ -1682,6 +2458,32 @@ program.command("trust").description("Trust Caddy's local HTTPS CA for this proj
|
|
|
1682
2458
|
await validateCaddyfile(caddyfile);
|
|
1683
2459
|
await runTrust(options.cwd, caddyfile);
|
|
1684
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
|
+
});
|
|
1685
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) => {
|
|
1686
2488
|
assertLocalDevelopment("reset");
|
|
1687
2489
|
const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
|
|
@@ -1689,13 +2491,13 @@ program.command("reset").description("Remove Localghost setup state without dele
|
|
|
1689
2491
|
const statePath = getLocalghostStatePath(options.cwd);
|
|
1690
2492
|
explainHostsPassword();
|
|
1691
2493
|
const hostsResult = await removeSystemHosts(projectName);
|
|
1692
|
-
if (
|
|
2494
|
+
if (existsSync8(caddyfilePath)) {
|
|
1693
2495
|
unlinkSync(caddyfilePath);
|
|
1694
2496
|
console.log(`Removed ${caddyfilePath}`);
|
|
1695
2497
|
} else {
|
|
1696
2498
|
console.log(`${caddyfilePath} was not present`);
|
|
1697
2499
|
}
|
|
1698
|
-
if (
|
|
2500
|
+
if (existsSync8(statePath)) {
|
|
1699
2501
|
unlinkSync(statePath);
|
|
1700
2502
|
console.log(`Removed ${statePath}`);
|
|
1701
2503
|
} else {
|
|
@@ -1716,7 +2518,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
1716
2518
|
const hostsResult = await removeSystemHosts(projectName);
|
|
1717
2519
|
const caddyfilePath = getCaddyfilePath(options.cwd);
|
|
1718
2520
|
let caddyfileRemoved = false;
|
|
1719
|
-
if (options.removeCaddyfile &&
|
|
2521
|
+
if (options.removeCaddyfile && existsSync8(caddyfilePath)) {
|
|
1720
2522
|
unlinkSync(caddyfilePath);
|
|
1721
2523
|
caddyfileRemoved = true;
|
|
1722
2524
|
}
|
|
@@ -1796,7 +2598,7 @@ program.command("routes").description("Print domain to upstream routes").option(
|
|
|
1796
2598
|
}));
|
|
1797
2599
|
}
|
|
1798
2600
|
});
|
|
1799
|
-
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) => {
|
|
1800
2602
|
assertLocalDevelopment("dev");
|
|
1801
2603
|
await assertCaddyReady();
|
|
1802
2604
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
@@ -1809,41 +2611,18 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1809
2611
|
projectName: context.projectName
|
|
1810
2612
|
});
|
|
1811
2613
|
if (!readiness.ready) {
|
|
1812
|
-
if (!options.setup) {
|
|
2614
|
+
if (!options.setup && !context.autoRepair) {
|
|
1813
2615
|
throw new Error(
|
|
1814
2616
|
[
|
|
1815
2617
|
"Localghost setup is missing or stale.",
|
|
1816
2618
|
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
1817
2619
|
`Run: ${readiness.setupCommand}`,
|
|
1818
|
-
"
|
|
2620
|
+
"Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
|
|
1819
2621
|
].join("\n")
|
|
1820
2622
|
);
|
|
1821
2623
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
const caddyfilePath = await writeCaddyfile(readiness.entries, options.cwd, { https });
|
|
1825
|
-
await validateCaddyfile(caddyfilePath);
|
|
1826
|
-
writeLocalghostState(options.cwd, {
|
|
1827
|
-
action: "setup",
|
|
1828
|
-
projectName: readiness.projectName,
|
|
1829
|
-
cwd: options.cwd,
|
|
1830
|
-
configPath: readiness.configPath,
|
|
1831
|
-
hostsPath: hostsResult.hostsPath,
|
|
1832
|
-
hostsChanged: hostsResult.changed,
|
|
1833
|
-
...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
|
|
1834
|
-
caddyfilePath,
|
|
1835
|
-
caddyHttps: https,
|
|
1836
|
-
...existingTrustMarkers(options.cwd),
|
|
1837
|
-
entries: readiness.entries
|
|
1838
|
-
});
|
|
1839
|
-
registerLocalghostSetup({
|
|
1840
|
-
cwd: options.cwd,
|
|
1841
|
-
projectName: readiness.projectName,
|
|
1842
|
-
configPath: readiness.configPath,
|
|
1843
|
-
caddyfilePath,
|
|
1844
|
-
https,
|
|
1845
|
-
entries: readiness.entries
|
|
1846
|
-
});
|
|
2624
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
2625
|
+
await runSetupFromReadiness(options.cwd, https, readiness);
|
|
1847
2626
|
}
|
|
1848
2627
|
warnAboutLocalMdns(readiness.entries);
|
|
1849
2628
|
logDomainRoutes(readiness.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
@@ -1879,7 +2658,7 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1879
2658
|
cleanupRun();
|
|
1880
2659
|
}
|
|
1881
2660
|
});
|
|
1882
|
-
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) => {
|
|
1883
2662
|
assertLocalDevelopment("run");
|
|
1884
2663
|
await assertCaddyReady();
|
|
1885
2664
|
const context = await resolveLocalghostContext({
|
|
@@ -1889,7 +2668,8 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1889
2668
|
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1890
2669
|
...options.port ? { port: options.port } : {},
|
|
1891
2670
|
...useHttps(options) ? { https: true } : {},
|
|
1892
|
-
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {}
|
|
2671
|
+
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {},
|
|
2672
|
+
...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
|
|
1893
2673
|
});
|
|
1894
2674
|
const https = context.https;
|
|
1895
2675
|
const readiness = getSetupReadiness({
|
|
@@ -1901,18 +2681,19 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1901
2681
|
projectName: context.projectName
|
|
1902
2682
|
});
|
|
1903
2683
|
if (!readiness.ready) {
|
|
1904
|
-
|
|
1905
|
-
if (!shouldSetup) {
|
|
2684
|
+
if (!options.setup && !context.autoRepair) {
|
|
1906
2685
|
throw new Error(
|
|
1907
2686
|
[
|
|
1908
2687
|
"Localghost setup is missing or stale.",
|
|
1909
2688
|
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
1910
|
-
`Run: ${readiness.setupCommand}
|
|
2689
|
+
`Run: ${readiness.setupCommand}`,
|
|
2690
|
+
"Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
|
|
1911
2691
|
].join("\n")
|
|
1912
2692
|
);
|
|
1913
2693
|
}
|
|
2694
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
1914
2695
|
await runSetupFromReadiness(options.cwd, https, readiness);
|
|
1915
|
-
console.log(`
|
|
2696
|
+
console.log(`Repair complete. Setup state: ${getLocalghostStatePath(options.cwd)}`);
|
|
1916
2697
|
}
|
|
1917
2698
|
if (context.dynamicPort && context.port !== context.requestedPort) {
|
|
1918
2699
|
console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
|
|
@@ -1983,6 +2764,53 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1983
2764
|
cleanupRun();
|
|
1984
2765
|
}
|
|
1985
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
|
+
});
|
|
1986
2814
|
program.command("ps").description("Show Localghost setups and currently running sessions").option("--json", "Print raw JSON").action(async (options) => {
|
|
1987
2815
|
const setups = listLocalghostSetups();
|
|
1988
2816
|
const runs = listLocalghostRuns();
|
|
@@ -1998,7 +2826,83 @@ program.command("print").description("Print parsed host config").option("--cwd <
|
|
|
1998
2826
|
warnAboutLocalMdns(entries);
|
|
1999
2827
|
console.log(JSON.stringify(entries, null, 2));
|
|
2000
2828
|
});
|
|
2001
|
-
|
|
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) => {
|
|
2002
2906
|
const message = error instanceof Error ? error.message : String(error);
|
|
2003
2907
|
console.error(message);
|
|
2004
2908
|
process.exitCode = 1;
|