@hamedb89/localghost 0.1.6 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vite.js CHANGED
@@ -1,10 +1,125 @@
1
1
  // src/vite.ts
2
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
3
3
  import { normalize, resolve as resolve2 } from "path";
4
+ import { spawn } from "child_process";
5
+ import { emitKeypressEvents } from "readline";
6
+
7
+ // src/activity.ts
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { dirname, join } from "path";
11
+ var LOCALGHOST_ACTIVITY_VERSION = 1;
12
+ function getLocalghostActivityPath(env = process.env) {
13
+ if (env.LOCALGHOST_ACTIVITY_PATH) return env.LOCALGHOST_ACTIVITY_PATH;
14
+ const stateRoot = env.XDG_STATE_HOME || join(homedir(), ".local/state");
15
+ return join(stateRoot, "localghost", "activity.json");
16
+ }
17
+ function isProcessRunning(pid) {
18
+ if (!Number.isInteger(pid) || pid < 1) return false;
19
+ try {
20
+ process.kill(pid, 0);
21
+ return true;
22
+ } catch (error) {
23
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
24
+ return code === "EPERM";
25
+ }
26
+ }
27
+ function emptyActivity() {
28
+ return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [], setups: [] };
29
+ }
30
+ function readLocalghostActivity(path = getLocalghostActivityPath()) {
31
+ if (!existsSync(path)) return emptyActivity();
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
34
+ return {
35
+ version: LOCALGHOST_ACTIVITY_VERSION,
36
+ runs: Array.isArray(parsed.runs) ? parsed.runs : [],
37
+ setups: Array.isArray(parsed.setups) ? parsed.setups : []
38
+ };
39
+ } catch {
40
+ return emptyActivity();
41
+ }
42
+ }
43
+ function writeLocalghostActivity(activity, path = getLocalghostActivityPath()) {
44
+ mkdirSync(dirname(path), { recursive: true });
45
+ writeFileSync(path, `${JSON.stringify(activity, null, 2)}
46
+ `, "utf8");
47
+ return path;
48
+ }
49
+ function createRunId(input2, pid) {
50
+ return `${input2.projectName}:${input2.mode}:${pid}:${Date.now()}`;
51
+ }
52
+ function createSetupId(input2) {
53
+ return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
54
+ }
55
+ function pruneLocalghostActivity(path = getLocalghostActivityPath()) {
56
+ const activity = readLocalghostActivity(path);
57
+ const activeRuns = activity.runs.filter((run) => isProcessRunning(run.pid));
58
+ const pruned = activeRuns.length !== activity.runs.length;
59
+ if (pruned) {
60
+ writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
61
+ }
62
+ return {
63
+ path,
64
+ pruned,
65
+ runs: activeRuns,
66
+ setups: activity.setups
67
+ };
68
+ }
69
+ function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
70
+ const now = (/* @__PURE__ */ new Date()).toISOString();
71
+ const pid = input2.pid ?? process.pid;
72
+ const record = {
73
+ id: input2.id ?? createRunId(input2, pid),
74
+ mode: input2.mode,
75
+ pid,
76
+ cwd: input2.cwd,
77
+ projectName: input2.projectName,
78
+ startedAt: input2.startedAt ?? now,
79
+ updatedAt: now,
80
+ ...input2.configPath ? { configPath: input2.configPath } : {},
81
+ ...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
82
+ ...input2.caddyPid ? { caddyPid: input2.caddyPid } : {},
83
+ ...input2.childPid ? { childPid: input2.childPid } : {},
84
+ ...input2.childCommand ? { childCommand: input2.childCommand } : {},
85
+ ...typeof input2.https === "boolean" ? { https: input2.https } : {},
86
+ ...input2.requestedPort ? { requestedPort: input2.requestedPort } : {},
87
+ ...input2.port ? { port: input2.port } : {},
88
+ ...typeof input2.dynamicPort === "boolean" ? { dynamicPort: input2.dynamicPort } : {},
89
+ entries: input2.entries
90
+ };
91
+ const current = pruneLocalghostActivity(path).runs.filter((run) => run.id !== record.id);
92
+ const activity = readLocalghostActivity(path);
93
+ writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record], setups: activity.setups }, path);
94
+ return record;
95
+ }
96
+ function registerLocalghostSetup(input2, path = getLocalghostActivityPath()) {
97
+ const record = {
98
+ id: input2.id ?? createSetupId(input2),
99
+ cwd: input2.cwd,
100
+ projectName: input2.projectName,
101
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
102
+ ...input2.configPath ? { configPath: input2.configPath } : {},
103
+ ...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
104
+ ...typeof input2.https === "boolean" ? { https: input2.https } : {},
105
+ entries: input2.entries
106
+ };
107
+ const activity = pruneLocalghostActivity(path);
108
+ const setups = activity.setups.filter((setup) => setup.id !== record.id);
109
+ writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activity.runs, setups: [...setups, record] }, path);
110
+ return record;
111
+ }
112
+ function unregisterLocalghostRun(id, path = getLocalghostActivityPath()) {
113
+ const activity = readLocalghostActivity(path);
114
+ const runs = activity.runs.filter((run) => run.id !== id);
115
+ if (runs.length !== activity.runs.length) {
116
+ writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
117
+ }
118
+ }
4
119
 
5
120
  // src/config.ts
6
- import { existsSync, readFileSync, readdirSync } from "fs";
7
- import { basename, join, resolve } from "path";
121
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
122
+ import { basename, join as join2, resolve } from "path";
8
123
 
9
124
  // src/parse.ts
10
125
  var HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*\.?$/i;
@@ -69,7 +184,7 @@ function resolveDevHostsPath(options = {}) {
69
184
  const searchedFiles = getConfigFileCandidates(options);
70
185
  for (const fileName2 of searchedFiles) {
71
186
  const path = resolve(cwd, fileName2);
72
- if (existsSync(path)) {
187
+ if (existsSync2(path)) {
73
188
  return {
74
189
  path,
75
190
  fileName: basename(fileName2),
@@ -102,11 +217,11 @@ function readDevHosts(options = {}) {
102
217
  `Missing Localghost config in ${cwd}. Looked for ${formatSearchedFiles(resolvedPath.searchedFiles, resolvedPath.configPattern)}. Run \`localghost init\` or pass --config/--config-pattern.`
103
218
  );
104
219
  }
105
- return parseDevHosts(readFileSync(resolvedPath.path, "utf8"), resolvedPath.fileName);
220
+ return parseDevHosts(readFileSync2(resolvedPath.path, "utf8"), resolvedPath.fileName);
106
221
  }
107
222
  function getProjectName(cwd = process.cwd()) {
108
223
  try {
109
- const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
224
+ const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
110
225
  const name = typeof pkg.name === "string" && pkg.name ? pkg.name : "app";
111
226
  return sanitizeProjectName(name.replace(/^@/, ""));
112
227
  } catch {
@@ -118,6 +233,11 @@ function sanitizeProjectName(value) {
118
233
  return projectName || "app";
119
234
  }
120
235
 
236
+ // src/context.ts
237
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
238
+ import { join as join3 } from "path";
239
+ import { pathToFileURL } from "url";
240
+
121
241
  // src/port.ts
122
242
  import { createServer } from "net";
123
243
  async function isPortAvailable(port, host = "127.0.0.1") {
@@ -144,7 +264,263 @@ async function findAvailablePort(startPort, options = {}) {
144
264
  throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
145
265
  }
146
266
 
267
+ // src/tunnel.ts
268
+ import { domainToASCII } from "url";
269
+ var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
270
+ var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
271
+ var DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR = "-";
272
+ var DEFAULT_GHOST_TUNNEL_MODE = "manual";
273
+ function isResolvedGhostTunnelConfig(value) {
274
+ return typeof value === "object" && value !== null && "enabled" in value;
275
+ }
276
+ function toGhostTunnelConfig(options) {
277
+ return isResolvedGhostTunnelConfig(options) ? options : resolveGhostTunnelConfig(options);
278
+ }
279
+ function stripHostPort(value) {
280
+ const trimmed = value.trim().toLowerCase();
281
+ if (trimmed.startsWith("[") || trimmed.includes("/")) return "";
282
+ const portSeparator = trimmed.lastIndexOf(":");
283
+ if (portSeparator === -1) return trimmed;
284
+ const port = trimmed.slice(portSeparator + 1);
285
+ return /^\d+$/.test(port) ? trimmed.slice(0, portSeparator) : trimmed;
286
+ }
287
+ function normalizeDomain(value) {
288
+ const host = stripHostPort(value.replace(/^\*\./, ""));
289
+ const ascii = domainToASCII(host);
290
+ if (!ascii || ascii.length > 253 || ascii.includes("..")) return null;
291
+ if (ascii.startsWith(".") || ascii.endsWith(".")) return null;
292
+ if (ascii.includes("*")) return null;
293
+ if (!ascii.split(".").every(isValidHostLabel)) return null;
294
+ return ascii;
295
+ }
296
+ function isValidHostLabel(value) {
297
+ return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
298
+ }
299
+ function isValidNamespaceTag(value) {
300
+ return /^[a-z][a-z0-9_]*$/i.test(value);
301
+ }
302
+ function isNamespaceTagList(options) {
303
+ return Array.isArray(options);
304
+ }
305
+ function assertValidSubdomain(value) {
306
+ if (!isValidHostLabel(value)) {
307
+ throw new Error(`Invalid ghost tunnel subdomain: ${value}`);
308
+ }
309
+ }
310
+ function normalizeDomains(domains) {
311
+ const values = typeof domains === "string" ? [domains] : [...domains ?? []];
312
+ const normalized = values.map((value) => value.trim()).filter(Boolean).map((value) => {
313
+ const domain = normalizeDomain(value);
314
+ if (!domain) throw new Error(`Invalid ghost tunnel domain: ${value}`);
315
+ return domain;
316
+ });
317
+ return [...new Set(normalized)];
318
+ }
319
+ function parseGhostTunnelMode(value) {
320
+ return value ?? DEFAULT_GHOST_TUNNEL_MODE;
321
+ }
322
+ function resolveNamespaceConfig(options) {
323
+ const tags = isNamespaceTagList(options) ? [...options] : [...options?.tags ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS];
324
+ let separator = DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
325
+ let spreadTag = tags.includes("project") ? "project" : void 0;
326
+ if (options && !isNamespaceTagList(options)) {
327
+ separator = options.separator ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
328
+ spreadTag = options.spreadTag === false ? false : options.spreadTag ?? spreadTag;
329
+ }
330
+ if (tags.length === 0) {
331
+ throw new Error("Ghost tunnel namespace must include at least one tag.");
332
+ }
333
+ for (const tag of tags) {
334
+ if (!isValidNamespaceTag(tag)) {
335
+ throw new Error(`Invalid ghost tunnel namespace tag: ${tag}`);
336
+ }
337
+ }
338
+ if (spreadTag && !tags.includes(spreadTag)) {
339
+ throw new Error(`Ghost tunnel namespace spreadTag must be listed in tags: ${spreadTag}`);
340
+ }
341
+ if (!separator || separator.length > 8 || !/^[a-z0-9-]+$/.test(separator)) {
342
+ throw new Error(`Invalid ghost tunnel namespace separator: ${separator}`);
343
+ }
344
+ return {
345
+ tags,
346
+ separator,
347
+ ...spreadTag ? { spreadTag } : {}
348
+ };
349
+ }
350
+ function normalizeNamespaceValue(tag, value, separator, options = {}) {
351
+ const normalized = normalizeDomain(value);
352
+ if (!normalized || normalized.includes(".")) {
353
+ throw new Error(`Invalid ghost tunnel namespace value for ${tag}: ${value}`);
354
+ }
355
+ if (!options.allowSeparator && normalized.includes(separator)) {
356
+ throw new Error(`Ghost tunnel namespace value for ${tag} cannot include separator "${separator}": ${value}`);
357
+ }
358
+ return normalized;
359
+ }
360
+ function createNamespaceSlug(config, values) {
361
+ const parts = config.tags.map((tag) => {
362
+ const value = values[tag];
363
+ if (!value) {
364
+ throw new Error(`Missing ghost tunnel namespace value: ${tag}`);
365
+ }
366
+ return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
367
+ });
368
+ const slug = parts.join(config.separator);
369
+ if (!isValidHostLabel(slug)) {
370
+ throw new Error(`Ghost tunnel namespace is too long for a DNS label: ${slug}`);
371
+ }
372
+ return slug;
373
+ }
374
+ function createNamespaceDisplaySlug(config, values = {}) {
375
+ return config.tags.map((tag) => {
376
+ const value = values[tag];
377
+ if (!value) return `<${tag}>`;
378
+ try {
379
+ return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
380
+ } catch {
381
+ return `<${tag}>`;
382
+ }
383
+ }).join(config.separator);
384
+ }
385
+ function getPreviewDefaults(preview, defaults) {
386
+ return {
387
+ domain: preview?.domain ?? defaults?.domain,
388
+ route: preview?.route ?? defaults?.route,
389
+ project: preview?.project ?? defaults?.project,
390
+ owner: preview?.owner ?? defaults?.owner,
391
+ values: {
392
+ ...defaults?.values ?? {},
393
+ ...preview?.values ?? {}
394
+ },
395
+ path: preview?.path,
396
+ protocol: preview?.protocol
397
+ };
398
+ }
399
+ function getDisplayValues(input2) {
400
+ return {
401
+ ...input2.route ? { route: input2.route } : {},
402
+ ...input2.project ? { project: input2.project } : {},
403
+ ...input2.owner ? { owner: input2.owner } : {},
404
+ ...input2.values
405
+ };
406
+ }
407
+ function createDisplayUrl(config, defaults, domain) {
408
+ const input2 = getPreviewDefaults(config.preview, defaults);
409
+ const protocol = input2.protocol ?? "https";
410
+ const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
411
+ const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
412
+ const url = `${protocol}://${slug}.${entryHost}/`;
413
+ if (!input2.path) return url;
414
+ return `${url}${input2.path.replace(/^\/+/, "")}`;
415
+ }
416
+ function createDisplayUrls(config, defaults) {
417
+ const domains = config.domains.length > 0 ? config.domains : defaults?.domain ? [defaults.domain] : [];
418
+ const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, defaults, domain)) : [createDisplayUrl(config, defaults)];
419
+ return [...new Set(urls)];
420
+ }
421
+ function maybeConstructPreviewUrl(config, defaults) {
422
+ if (!config.preview) return void 0;
423
+ const input2 = getPreviewDefaults(config.preview, defaults);
424
+ if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
425
+ return constructGhostTunnelUrl({
426
+ domain: input2.domain,
427
+ route: input2.route,
428
+ project: input2.project,
429
+ owner: input2.owner,
430
+ values: input2.values,
431
+ ...input2.path ? { path: input2.path } : {},
432
+ ...input2.protocol ? { protocol: input2.protocol } : {},
433
+ ghostTunnel: config
434
+ });
435
+ }
436
+ function resolveGhostTunnelConfig(options, defaults) {
437
+ if (options === false || typeof options === "undefined") {
438
+ return {
439
+ enabled: false,
440
+ mode: DEFAULT_GHOST_TUNNEL_MODE,
441
+ domains: [],
442
+ subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
443
+ namespace: resolveNamespaceConfig(void 0),
444
+ displayUrls: [],
445
+ requireHttps: true,
446
+ requireAuth: true
447
+ };
448
+ }
449
+ const config = typeof options === "string" ? { mode: options } : options;
450
+ const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
451
+ assertValidSubdomain(subdomain);
452
+ const domains = normalizeDomains(config.domains);
453
+ const enabled = config.enabled ?? true;
454
+ const resolved = {
455
+ enabled,
456
+ mode: parseGhostTunnelMode(config.mode),
457
+ domains,
458
+ subdomain,
459
+ namespace: resolveNamespaceConfig(config.namespace),
460
+ ...config.preview ? { preview: config.preview } : {},
461
+ displayUrls: [],
462
+ requireHttps: config.requireHttps ?? true,
463
+ requireAuth: config.requireAuth ?? true
464
+ };
465
+ if (!enabled) {
466
+ return resolved;
467
+ }
468
+ const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
469
+ const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
470
+ return {
471
+ ...resolved,
472
+ displayUrls,
473
+ ...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
474
+ ...previewUrl ? { previewUrl } : {}
475
+ };
476
+ }
477
+ function getGhostTunnelEntryHost(domain, options = {}) {
478
+ const config = toGhostTunnelConfig(options);
479
+ const normalizedDomain = normalizeDomain(domain);
480
+ if (!normalizedDomain) {
481
+ throw new Error(`Invalid ghost tunnel domain: ${domain}`);
482
+ }
483
+ return `${config.subdomain}.${normalizedDomain}`;
484
+ }
485
+ function constructGhostTunnelHost(input2) {
486
+ const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
487
+ if (!config.enabled) {
488
+ throw new Error("Ghost tunnel is not enabled.");
489
+ }
490
+ const namespaceValues = {
491
+ route: input2.route,
492
+ project: input2.project,
493
+ owner: input2.owner,
494
+ ...input2.values ?? {}
495
+ };
496
+ const slug = createNamespaceSlug(config.namespace, namespaceValues);
497
+ return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
498
+ }
499
+ function constructGhostTunnelUrl(input2) {
500
+ const protocol = input2.protocol ?? "https";
501
+ const host = constructGhostTunnelHost(input2);
502
+ const url = new URL(`${protocol}://${host}/`);
503
+ if (input2.path) {
504
+ url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
505
+ }
506
+ if (input2.searchParams instanceof URLSearchParams) {
507
+ url.search = input2.searchParams.toString();
508
+ } else if (input2.searchParams) {
509
+ for (const [key, value] of Object.entries(input2.searchParams)) {
510
+ if (typeof value !== "undefined" && value !== null) {
511
+ url.searchParams.set(key, String(value));
512
+ }
513
+ }
514
+ }
515
+ return url.toString();
516
+ }
517
+
147
518
  // src/context.ts
519
+ var LOCALGHOST_PROJECT_CONFIG_FILES = [
520
+ "localghost.config.mjs",
521
+ "localghost.config.js",
522
+ "localghost.config.cjs"
523
+ ];
148
524
  function parsePort(value) {
149
525
  if (!value) return void 0;
150
526
  const port = Number.parseInt(value, 10);
@@ -158,6 +534,30 @@ function envDynamicPort() {
158
534
  if (!value) return void 0;
159
535
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
160
536
  }
537
+ function envHttps() {
538
+ const value = process.env.LOCALGHOST_HTTPS;
539
+ if (!value) return void 0;
540
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
541
+ }
542
+ function getPackageName(cwd) {
543
+ try {
544
+ const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
545
+ return typeof pkg.name === "string" ? pkg.name : void 0;
546
+ } catch {
547
+ return void 0;
548
+ }
549
+ }
550
+ function getPackageOwner(cwd) {
551
+ const packageName = getPackageName(cwd);
552
+ if (!packageName?.startsWith("@")) return void 0;
553
+ return packageName.slice(1).split("/")[0];
554
+ }
555
+ function getLocalOwner(cwd) {
556
+ return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
557
+ }
558
+ function getRouteName(primaryHost, fallback) {
559
+ return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
560
+ }
161
561
  function readOptionsFromContext(options) {
162
562
  return {
163
563
  cwd: options.cwd ?? process.cwd(),
@@ -175,22 +575,68 @@ function withRuntimePort(entries, requestedPort, port) {
175
575
  function uniqueHosts(entries) {
176
576
  return [...new Set(entries.map((entry) => entry.host))];
177
577
  }
578
+ function isAliasableHost(host) {
579
+ return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
580
+ }
581
+ function getDefaultWwwAlias(host) {
582
+ return isAliasableHost(host) ? `www.${host}` : null;
583
+ }
584
+ function addDefaultWwwAliases(entries) {
585
+ const seen = new Set(entries.map((entry) => entry.host));
586
+ const aliases = [];
587
+ for (const entry of entries) {
588
+ const alias = getDefaultWwwAlias(entry.host);
589
+ if (alias && !seen.has(alias)) {
590
+ aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
591
+ seen.add(alias);
592
+ }
593
+ }
594
+ return [...entries, ...aliases];
595
+ }
596
+ function defined(input2) {
597
+ return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
598
+ }
599
+ async function readLocalghostProjectConfig(options = {}) {
600
+ const cwd = options.cwd ?? process.cwd();
601
+ if (options.configFile === false) return { config: {} };
602
+ const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
603
+ const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
604
+ if (!path) return { config: {} };
605
+ const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
606
+ const config = imported.default ?? imported;
607
+ return { config, path };
608
+ }
178
609
  async function resolveLocalghostContext(options = {}) {
179
610
  const cwd = options.cwd ?? process.cwd();
180
- const readOptions = readOptionsFromContext({ ...options, cwd });
611
+ const projectConfig = await readLocalghostProjectConfig({
612
+ cwd,
613
+ ...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
614
+ });
615
+ const merged = {
616
+ ...projectConfig.config,
617
+ ...defined(options)
618
+ };
619
+ const readOptions = readOptionsFromContext({ ...merged, cwd });
181
620
  const resolvedPath = resolveDevHostsPath(readOptions);
182
621
  const configEntries = readDevHosts(readOptions);
183
- const requestedPort = options.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
184
- const dynamicPort = options.dynamicPort ?? envDynamicPort() ?? false;
185
- const bindHost = options.bindHost ?? "127.0.0.1";
622
+ const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
623
+ const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
624
+ const bindHost = merged.bindHost ?? "127.0.0.1";
186
625
  const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
187
626
  const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
188
- const entries = withRuntimePort(configEntries, requestedPort, port);
627
+ const wwwAlias = merged.wwwAlias ?? true;
628
+ const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
189
629
  const hosts = uniqueHosts(entries);
190
- const primaryHost = options.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
630
+ const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
631
+ const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
632
+ const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
633
+ route: getRouteName(primaryHost, projectName),
634
+ project: projectName,
635
+ owner: getLocalOwner(cwd)
636
+ });
191
637
  return {
192
638
  cwd,
193
- projectName: sanitizeProjectName(options.project ?? getProjectName(cwd)),
639
+ projectName,
194
640
  readOptions,
195
641
  configPath: resolvedPath.path,
196
642
  configFileName: resolvedPath.fileName,
@@ -202,7 +648,10 @@ async function resolveLocalghostContext(options = {}) {
202
648
  dynamicPort,
203
649
  bindHost,
204
650
  primaryHost,
205
- https: options.https === true
651
+ https: merged.https ?? envHttps() ?? false,
652
+ wwwAlias,
653
+ ghostTunnel,
654
+ ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
206
655
  };
207
656
  }
208
657
 
@@ -241,21 +690,21 @@ function isProductionLike(env = process.env) {
241
690
  }
242
691
 
243
692
  // src/fs.ts
244
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
245
- import { dirname } from "path";
693
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
694
+ import { dirname as dirname2 } from "path";
246
695
  function readTextFile(path) {
247
- return readFileSync2(path, "utf8");
696
+ return readFileSync4(path, "utf8");
248
697
  }
249
698
  function writeTextFile(path, value) {
250
- mkdirSync(dirname(path), { recursive: true });
251
- writeFileSync(path, value, "utf8");
699
+ mkdirSync2(dirname2(path), { recursive: true });
700
+ writeFileSync2(path, value, "utf8");
252
701
  return path;
253
702
  }
254
703
 
255
704
  // src/hosts-file.ts
256
- import { writeFileSync as writeFileSync2 } from "fs";
705
+ import { writeFileSync as writeFileSync3 } from "fs";
257
706
  import { tmpdir } from "os";
258
- import { join as join2 } from "path";
707
+ import { join as join4 } from "path";
259
708
  import { execa as execa2 } from "execa";
260
709
  function escapeRegExp(value) {
261
710
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -290,8 +739,8 @@ ${block}`;
290
739
  }
291
740
  async function writeSystemHostsFile(hostsPath, next, projectName) {
292
741
  const sanitizedProjectName = sanitizeProjectName(projectName);
293
- const tempPath = join2(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
294
- writeFileSync2(tempPath, next, "utf8");
742
+ const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
743
+ writeFileSync3(tempPath, next, "utf8");
295
744
  if (process.platform === "win32") {
296
745
  throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
297
746
  }
@@ -342,26 +791,26 @@ async function ask(question, defaultValue) {
342
791
  }
343
792
 
344
793
  // src/state.ts
345
- import { existsSync as existsSync2 } from "fs";
346
- import { join as join3 } from "path";
794
+ import { existsSync as existsSync4 } from "fs";
795
+ import { join as join5 } from "path";
347
796
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
348
797
  function getLocalghostStatePath(cwd = process.cwd()) {
349
- return join3(cwd, LOCALGHOST_STATE_FILE);
798
+ return join5(cwd, LOCALGHOST_STATE_FILE);
350
799
  }
351
800
  function readLocalghostState(cwd = process.cwd()) {
352
801
  const path = getLocalghostStatePath(cwd);
353
- if (!existsSync2(path)) return null;
802
+ if (!existsSync4(path)) return null;
354
803
  return JSON.parse(readTextFile(path));
355
804
  }
356
805
  function writeLocalghostState(cwd, state) {
357
806
  const path = getLocalghostStatePath(cwd);
358
- writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...state }, null, 2)}
807
+ writeTextFile(path, `${JSON.stringify({ ...state, version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
359
808
  `);
360
809
  return path;
361
810
  }
362
811
 
363
812
  // src/caddy.ts
364
- import { dirname as dirname2, join as join4 } from "path";
813
+ import { dirname as dirname3, join as join6 } from "path";
365
814
  import { execa as execa3 } from "execa";
366
815
  function groupByPort(entries) {
367
816
  const groups = /* @__PURE__ */ new Map();
@@ -373,7 +822,7 @@ function groupByPort(entries) {
373
822
  return groups;
374
823
  }
375
824
  function getCaddyfilePath(cwd = process.cwd()) {
376
- return join4(cwd, "ops/local/Caddyfile");
825
+ return join6(cwd, "ops/local/Caddyfile");
377
826
  }
378
827
  function renderCaddyfile(entries, options = {}) {
379
828
  const groups = groupByPort(entries);
@@ -399,11 +848,54 @@ async function writeCaddyfile(entries, cwd = process.cwd(), options = {}) {
399
848
  }
400
849
  async function validateCaddyfile(path) {
401
850
  await execa3("caddy", ["validate", "--config", path], {
402
- cwd: dirname2(path),
851
+ cwd: dirname3(path),
403
852
  stdio: "inherit"
404
853
  });
405
854
  }
406
855
 
856
+ // src/routes.ts
857
+ var ansi = {
858
+ cyan: "\x1B[36m",
859
+ dim: "\x1B[2m",
860
+ green: "\x1B[32m",
861
+ reset: "\x1B[0m",
862
+ yellow: "\x1B[33m"
863
+ };
864
+ function colorize(value, color, enabled) {
865
+ return enabled ? `${color}${value}${ansi.reset}` : value;
866
+ }
867
+ function colorizeUrl(value, enabled) {
868
+ if (!enabled) return value;
869
+ return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
870
+ }
871
+ function formatGhostTunnel(config, options = {}) {
872
+ if (!config.enabled) return null;
873
+ const color = options.color === true;
874
+ const label = options.label ?? "expected";
875
+ const labelColor = label === "running" ? ansi.green : ansi.dim;
876
+ const lines = [
877
+ "localghost ghost tunnel",
878
+ ` mode: ${config.mode}`
879
+ ];
880
+ const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
881
+ if (urls.length === 0) {
882
+ lines.push(` ${label}: unavailable`);
883
+ } else if (urls.length === 1) {
884
+ lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
885
+ } else {
886
+ lines.push(` ${colorize(label, labelColor, color)}:`);
887
+ for (const url of urls) {
888
+ lines.push(` ${colorizeUrl(url, color)}`);
889
+ }
890
+ }
891
+ if (options.verbose) {
892
+ lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
893
+ lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
894
+ lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
895
+ }
896
+ return lines.join("\n");
897
+ }
898
+
407
899
  // src/vite.ts
408
900
  function mergeAllowedHosts(current, hosts) {
409
901
  if (Array.isArray(current)) {
@@ -418,7 +910,11 @@ function getDisplayEntries(entries, vitePort) {
418
910
  const matchingEntries = entries.filter((entry) => entry.port === vitePort);
419
911
  return matchingEntries.length > 0 ? matchingEntries : entries;
420
912
  }
421
- function printLocalHosts(server, entries, vitePort, https) {
913
+ function printLocalHosts(server, context) {
914
+ if (!context) return;
915
+ const entries = context.entries;
916
+ const vitePort = context.port;
917
+ const https = context.https;
422
918
  const displayEntries = getDisplayEntries(entries, vitePort);
423
919
  const protocol = https ? "https" : "http";
424
920
  const urls = displayEntries.map((entry) => `${protocol}://${entry.host}/`);
@@ -432,13 +928,24 @@ function printLocalHosts(server, entries, vitePort, https) {
432
928
  ` local: ${primaryUrl}`,
433
929
  ...urls.slice(1).map((url) => ` also: ${url}`),
434
930
  vitePort ? ` target: http://127.0.0.1:${vitePort}/` : void 0,
435
- https ? " proxy: Caddy local HTTPS" : void 0
931
+ https ? " proxy: Caddy local HTTPS" : void 0,
932
+ context.ghostTunnel.enabled ? formatGhostTunnel(context.ghostTunnel, {
933
+ color: shouldColor(),
934
+ label: "ready",
935
+ verbose: optionsVerbose(context)
936
+ }) : void 0
436
937
  ].filter((line) => Boolean(line));
437
938
  server.config.logger.info(lines.join("\n"), {
438
939
  clear: false,
439
940
  timestamp: false
440
941
  });
441
942
  }
943
+ function shouldColor() {
944
+ return process.stdout.isTTY && !process.env.NO_COLOR;
945
+ }
946
+ function optionsVerbose(context) {
947
+ return process.env.LOCALGHOST_VERBOSE === "1" || process.env.LOCALGHOST_VERBOSE === "true" || context.ghostTunnel.displayUrls.length > 1;
948
+ }
442
949
  function readOptionsFromPlugin(options) {
443
950
  return {
444
951
  cwd: options.cwd ?? process.cwd(),
@@ -452,7 +959,8 @@ function getConfigWatchFiles(options) {
452
959
  const cwd = readOptions.cwd ?? process.cwd();
453
960
  const resolvedPath = resolveDevHostsPath(readOptions);
454
961
  const candidatePaths = getConfigFileCandidates(readOptions).map((fileName) => resolve2(cwd, fileName));
455
- return [.../* @__PURE__ */ new Set([...candidatePaths, resolvedPath.path])];
962
+ const projectConfigPaths = options.localghostConfig === false ? [] : options.localghostConfig ? [resolve2(cwd, options.localghostConfig)] : ["localghost.config.mjs", "localghost.config.js", "localghost.config.cjs"].map((fileName) => resolve2(cwd, fileName));
963
+ return [.../* @__PURE__ */ new Set([...candidatePaths, resolvedPath.path, ...projectConfigPaths])];
456
964
  }
457
965
  function normalizeWatchPath(filePath) {
458
966
  return normalize(resolve2(filePath));
@@ -469,6 +977,57 @@ function defaultHost(cwd) {
469
977
  const projectName = sanitizeProjectName(getProjectName(cwd).split("/").pop() ?? "app");
470
978
  return `${projectName}.localhost`;
471
979
  }
980
+ function getPackageOwner2(cwd) {
981
+ try {
982
+ const pkg = JSON.parse(readFileSync5(resolve2(cwd, "package.json"), "utf8"));
983
+ if (typeof pkg.name === "string" && pkg.name.startsWith("@")) {
984
+ return pkg.name.slice(1).split("/")[0];
985
+ }
986
+ } catch {
987
+ return void 0;
988
+ }
989
+ return void 0;
990
+ }
991
+ function getLocalOwner2(cwd) {
992
+ return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner2(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
993
+ }
994
+ function getRouteName2(primaryHost, fallback) {
995
+ return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
996
+ }
997
+ function readBuildEntries(options) {
998
+ const readOptions = readOptionsFromPlugin(options);
999
+ const resolved = resolveDevHostsPath(readOptions);
1000
+ if (!resolved.exists) return [];
1001
+ try {
1002
+ return readDevHosts(readOptions);
1003
+ } catch {
1004
+ return [];
1005
+ }
1006
+ }
1007
+ async function maybePrintBuildGhostTunnel(options) {
1008
+ const cwd = options.cwd ?? process.cwd();
1009
+ const projectConfig = await readLocalghostProjectConfig({
1010
+ cwd,
1011
+ ...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
1012
+ });
1013
+ const explicitGhostTunnel = typeof options.ghostTunnel !== "undefined" ? options.ghostTunnel : projectConfig.config.ghostTunnel;
1014
+ if (!explicitGhostTunnel) return;
1015
+ const entries = readBuildEntries(options);
1016
+ const projectName = sanitizeProjectName(projectConfig.config.project ?? getProjectName(cwd));
1017
+ const primaryHost = options.primaryHost ?? entries[0]?.host ?? defaultHost(cwd);
1018
+ const ghostTunnel = resolveGhostTunnelConfig(explicitGhostTunnel, {
1019
+ route: getRouteName2(primaryHost, projectName),
1020
+ project: sanitizeProjectName(projectConfig.config.project ?? getProjectName(cwd)),
1021
+ owner: getLocalOwner2(cwd)
1022
+ });
1023
+ if (!ghostTunnel.enabled) return;
1024
+ const formatted = formatGhostTunnel(ghostTunnel, {
1025
+ color: shouldColor(),
1026
+ label: "configured",
1027
+ verbose: options.verbose === true || process.env.LOCALGHOST_VERBOSE === "1" || process.env.LOCALGHOST_VERBOSE === "true"
1028
+ });
1029
+ if (formatted) console.log(formatted);
1030
+ }
472
1031
  async function promptForHosts(cwd, port) {
473
1032
  const primaryHost = await ask("Primary local domain", defaultHost(cwd));
474
1033
  const hosts = [primaryHost.toLowerCase()];
@@ -476,20 +1035,20 @@ async function promptForHosts(cwd, port) {
476
1035
  const host = await ask("Domain");
477
1036
  if (host) hosts.push(host.toLowerCase());
478
1037
  }
479
- return [...new Set(hosts)];
1038
+ return [...new Set(addDefaultWwwAliases(hosts.map((host) => ({ host, port, target: `127.0.0.1:${port}` }))).map((entry) => entry.host))];
480
1039
  }
481
1040
  function hasReadySetup(cwd, entries, configPath, https) {
482
1041
  const state = readLocalghostState(cwd);
483
1042
  const projectName = sanitizeProjectName(getProjectName(cwd));
484
1043
  if (state?.action !== "setup" || state.configPath !== configPath) return false;
485
1044
  try {
486
- const hosts = readFileSync3(getSystemHostsPath(), "utf8");
1045
+ const hosts = readFileSync5(getSystemHostsPath(), "utf8");
487
1046
  if (!hosts.includes(renderHostsBlock(projectName, entries).trimEnd())) return false;
488
1047
  } catch {
489
1048
  return false;
490
1049
  }
491
1050
  const caddyfilePath = getCaddyfilePath(cwd);
492
- return existsSync3(caddyfilePath) && readFileSync3(caddyfilePath, "utf8") === renderCaddyfile(entries, { https });
1051
+ return existsSync5(caddyfilePath) && readFileSync5(caddyfilePath, "utf8") === renderCaddyfile(entries, { https });
493
1052
  }
494
1053
  async function setupProject(cwd, entries, configPath, https) {
495
1054
  const caddy = await checkCaddy();
@@ -518,6 +1077,14 @@ async function setupProject(cwd, entries, configPath, https) {
518
1077
  caddyHttps: https,
519
1078
  entries
520
1079
  });
1080
+ registerLocalghostSetup({
1081
+ cwd,
1082
+ projectName,
1083
+ configPath,
1084
+ caddyfilePath,
1085
+ https,
1086
+ entries
1087
+ });
521
1088
  }
522
1089
  async function ensureLocalghostContext(options, vitePort, https) {
523
1090
  const cwd = options.cwd ?? process.cwd();
@@ -541,38 +1108,116 @@ async function ensureLocalghostContext(options, vitePort, https) {
541
1108
  ...options,
542
1109
  cwd,
543
1110
  port: vitePort,
544
- https
1111
+ ...typeof https === "boolean" ? { https } : {}
545
1112
  });
546
- if (!hasReadySetup(cwd, context.entries, resolved.path, https)) {
1113
+ if (!hasReadySetup(cwd, context.entries, resolved.path, context.https)) {
547
1114
  if (options.setup === false || !canPrompt()) return context;
548
1115
  const setup = await confirm("Run caddy:setup now?", true);
549
1116
  if (setup) {
550
- await setupProject(cwd, context.entries, resolved.path, https);
1117
+ await setupProject(cwd, context.entries, resolved.path, context.https);
551
1118
  console.log(`All set. Setup state: ${getLocalghostStatePath(cwd)}`);
552
1119
  }
553
1120
  }
554
1121
  return context;
555
1122
  }
1123
+ function isConcreteGhostUrl(url) {
1124
+ return !url.includes("*") && !url.includes("<") && /^https?:\/\//.test(url);
1125
+ }
1126
+ function getConcreteGhostUrls(context) {
1127
+ return context.ghostTunnel.displayUrls.filter(isConcreteGhostUrl);
1128
+ }
1129
+ function openExternalUrl(url) {
1130
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
1131
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
1132
+ const child = spawn(command, args, {
1133
+ detached: true,
1134
+ stdio: "ignore"
1135
+ });
1136
+ child.unref();
1137
+ }
1138
+ function installGhostTunnelMenu(server, context) {
1139
+ if (!context?.ghostTunnel.enabled || !process.stdin.isTTY) return void 0;
1140
+ emitKeypressEvents(process.stdin);
1141
+ let active = false;
1142
+ const concreteUrls = getConcreteGhostUrls(context);
1143
+ const printMenu = () => {
1144
+ active = true;
1145
+ const lines = [
1146
+ "",
1147
+ formatGhostTunnel(context.ghostTunnel, {
1148
+ color: shouldColor(),
1149
+ label: "ready",
1150
+ verbose: true
1151
+ }) ?? "localghost ghost tunnel",
1152
+ ""
1153
+ ];
1154
+ if (concreteUrls.length === 0) {
1155
+ lines.push(" No concrete Ghost Tunnel domain configured.");
1156
+ lines.push(" Add ghostTunnel.domains to localghost.config.mjs to open a URL from this menu.");
1157
+ active = false;
1158
+ } else {
1159
+ concreteUrls.forEach((url, index) => {
1160
+ lines.push(` ${index + 1}. ${url}`);
1161
+ });
1162
+ lines.push("");
1163
+ lines.push(" Press a number to open, or escape to cancel.");
1164
+ }
1165
+ server.config.logger.info(lines.join("\n"), {
1166
+ clear: false,
1167
+ timestamp: false
1168
+ });
1169
+ };
1170
+ const onKeypress = (_input, key = {}) => {
1171
+ if (key.ctrl && key.name === "c") return;
1172
+ if (!active) {
1173
+ if (key.name === "g") printMenu();
1174
+ return;
1175
+ }
1176
+ if (key.name === "escape") {
1177
+ active = false;
1178
+ return;
1179
+ }
1180
+ const index = Number.parseInt(key.name ?? "", 10) - 1;
1181
+ const url = concreteUrls[index];
1182
+ if (!url) return;
1183
+ active = false;
1184
+ openExternalUrl(url);
1185
+ server.config.logger.info(`localghost opened ${url}`, {
1186
+ clear: false,
1187
+ timestamp: false
1188
+ });
1189
+ };
1190
+ process.stdin.on("keypress", onKeypress);
1191
+ return () => {
1192
+ process.stdin.off("keypress", onKeypress);
1193
+ };
1194
+ }
556
1195
  function localGhostPlugin(options = {}) {
557
1196
  let resolvedEntries = [];
558
1197
  let resolvedVitePort;
1198
+ let resolvedHttps = false;
1199
+ let resolvedContext;
559
1200
  let restartTimer;
1201
+ let activityRunId;
560
1202
  return {
561
1203
  name: "localghost:vite",
562
1204
  enforce: "pre",
563
1205
  async config(userConfig, configEnv) {
564
1206
  if (configEnv.command !== "serve" || configEnv.mode === "production" || isProductionLike()) {
1207
+ await maybePrintBuildGhostTunnel(options);
565
1208
  return {};
566
1209
  }
567
1210
  const existingServer = userConfig.server ?? {};
568
1211
  const envVitePort = Number.parseInt(process.env.LOCALGHOST_PORT ?? process.env.VITE_PORT ?? "", 10);
569
1212
  const requestedVitePort = options.port ?? existingServer.port ?? (Number.isInteger(envVitePort) ? envVitePort : 5173);
570
- const context = await ensureLocalghostContext(options, requestedVitePort, Boolean(options.https));
1213
+ const context = await ensureLocalghostContext(options, requestedVitePort, options.https);
571
1214
  const entries = context.entries;
572
1215
  const hosts = context.hosts;
573
1216
  const primaryHost = context.primaryHost;
574
1217
  resolvedEntries = entries;
575
1218
  resolvedVitePort = context.port;
1219
+ resolvedHttps = context.https;
1220
+ resolvedContext = context;
576
1221
  const server = {
577
1222
  ...existingServer,
578
1223
  allowedHosts: mergeAllowedHosts(existingServer.allowedHosts, hosts),
@@ -584,7 +1229,7 @@ function localGhostPlugin(options = {}) {
584
1229
  if (context.port) {
585
1230
  server.port = context.port;
586
1231
  }
587
- if (options.https && primaryHost) {
1232
+ if (context.https && primaryHost) {
588
1233
  const existingWs = typeof server.ws === "object" && server.ws ? server.ws : {};
589
1234
  const existingHmr = typeof existingServer.hmr === "object" && existingServer.hmr ? existingServer.hmr : {};
590
1235
  server.ws = {
@@ -632,9 +1277,38 @@ function localGhostPlugin(options = {}) {
632
1277
  server.watcher.on("unlink", restartOnLocalghostConfigChange);
633
1278
  if (options.log !== false) {
634
1279
  server.printUrls = () => {
635
- printLocalHosts(server, resolvedEntries, resolvedVitePort, Boolean(options.https));
1280
+ printLocalHosts(server, resolvedContext);
636
1281
  };
637
1282
  }
1283
+ if (resolvedContext) {
1284
+ const run = registerLocalghostRun({
1285
+ id: `${resolvedContext.projectName}:vite:${process.pid}:${resolvedContext.cwd}`,
1286
+ mode: "vite",
1287
+ pid: process.pid,
1288
+ cwd: resolvedContext.cwd,
1289
+ projectName: resolvedContext.projectName,
1290
+ configPath: resolvedContext.configPath,
1291
+ childCommand: ["vite"],
1292
+ https: resolvedContext.https,
1293
+ requestedPort: resolvedContext.requestedPort,
1294
+ port: resolvedContext.port,
1295
+ dynamicPort: resolvedContext.dynamicPort,
1296
+ entries: resolvedContext.entries
1297
+ });
1298
+ activityRunId = run.id;
1299
+ }
1300
+ const cleanupGhostMenu = installGhostTunnelMenu(server, resolvedContext);
1301
+ const cleanupActivity = () => {
1302
+ if (!activityRunId) return;
1303
+ unregisterLocalghostRun(activityRunId);
1304
+ activityRunId = void 0;
1305
+ };
1306
+ const cleanup = () => {
1307
+ cleanupActivity();
1308
+ cleanupGhostMenu?.();
1309
+ };
1310
+ server.httpServer?.once("close", cleanup);
1311
+ process.once("exit", cleanup);
638
1312
  }
639
1313
  };
640
1314
  }