@hamedb89/localghost 0.1.8 → 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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync7, readFileSync as readFileSync6, unlinkSync } from "fs";
4
+ import { existsSync as existsSync7, readFileSync as readFileSync7, unlinkSync } from "fs";
5
5
  import { Command, InvalidArgumentError } from "commander";
6
6
 
7
7
  // src/activity.ts
@@ -25,7 +25,7 @@ function isProcessRunning(pid) {
25
25
  }
26
26
  }
27
27
  function emptyActivity() {
28
- return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [] };
28
+ return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [], setups: [] };
29
29
  }
30
30
  function readLocalghostActivity(path = getLocalghostActivityPath()) {
31
31
  if (!existsSync(path)) return emptyActivity();
@@ -33,7 +33,8 @@ function readLocalghostActivity(path = getLocalghostActivityPath()) {
33
33
  const parsed = JSON.parse(readFileSync(path, "utf8"));
34
34
  return {
35
35
  version: LOCALGHOST_ACTIVITY_VERSION,
36
- runs: Array.isArray(parsed.runs) ? parsed.runs : []
36
+ runs: Array.isArray(parsed.runs) ? parsed.runs : [],
37
+ setups: Array.isArray(parsed.setups) ? parsed.setups : []
37
38
  };
38
39
  } catch {
39
40
  return emptyActivity();
@@ -48,22 +49,29 @@ function writeLocalghostActivity(activity, path = getLocalghostActivityPath()) {
48
49
  function createRunId(input2, pid) {
49
50
  return `${input2.projectName}:${input2.mode}:${pid}:${Date.now()}`;
50
51
  }
52
+ function createSetupId(input2) {
53
+ return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
54
+ }
51
55
  function pruneLocalghostActivity(path = getLocalghostActivityPath()) {
52
56
  const activity = readLocalghostActivity(path);
53
57
  const activeRuns = activity.runs.filter((run) => isProcessRunning(run.pid));
54
58
  const pruned = activeRuns.length !== activity.runs.length;
55
59
  if (pruned) {
56
- writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
60
+ writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
57
61
  }
58
62
  return {
59
63
  path,
60
64
  pruned,
61
- runs: activeRuns
65
+ runs: activeRuns,
66
+ setups: activity.setups
62
67
  };
63
68
  }
64
69
  function listLocalghostRuns(path = getLocalghostActivityPath()) {
65
70
  return pruneLocalghostActivity(path).runs;
66
71
  }
72
+ function listLocalghostSetups(path = getLocalghostActivityPath()) {
73
+ return pruneLocalghostActivity(path).setups;
74
+ }
67
75
  function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
68
76
  const now = (/* @__PURE__ */ new Date()).toISOString();
69
77
  const pid = input2.pid ?? process.pid;
@@ -87,14 +95,43 @@ function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
87
95
  entries: input2.entries
88
96
  };
89
97
  const current = pruneLocalghostActivity(path).runs.filter((run) => run.id !== record.id);
90
- writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record] }, path);
98
+ const activity = readLocalghostActivity(path);
99
+ writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record], setups: activity.setups }, path);
100
+ return record;
101
+ }
102
+ function registerLocalghostSetup(input2, path = getLocalghostActivityPath()) {
103
+ const record = {
104
+ id: input2.id ?? createSetupId(input2),
105
+ cwd: input2.cwd,
106
+ projectName: input2.projectName,
107
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
108
+ ...input2.configPath ? { configPath: input2.configPath } : {},
109
+ ...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
110
+ ...typeof input2.https === "boolean" ? { https: input2.https } : {},
111
+ entries: input2.entries
112
+ };
113
+ const activity = pruneLocalghostActivity(path);
114
+ const setups = activity.setups.filter((setup) => setup.id !== record.id);
115
+ writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activity.runs, setups: [...setups, record] }, path);
91
116
  return record;
92
117
  }
93
118
  function unregisterLocalghostRun(id, path = getLocalghostActivityPath()) {
94
119
  const activity = readLocalghostActivity(path);
95
120
  const runs = activity.runs.filter((run) => run.id !== id);
96
121
  if (runs.length !== activity.runs.length) {
97
- writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
122
+ writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
123
+ }
124
+ }
125
+ function unregisterLocalghostSetup(options, path = getLocalghostActivityPath()) {
126
+ const activity = readLocalghostActivity(path);
127
+ const setups = activity.setups.filter((setup) => {
128
+ if (setup.cwd !== options.cwd) return true;
129
+ if (options.projectName && setup.projectName !== options.projectName) return true;
130
+ if (options.configPath && setup.configPath !== options.configPath) return true;
131
+ return false;
132
+ });
133
+ if (setups.length !== activity.setups.length) {
134
+ writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, setups }, path);
98
135
  }
99
136
  }
100
137
 
@@ -288,7 +325,8 @@ async function trustCaddy(path) {
288
325
  }
289
326
 
290
327
  // src/context.ts
291
- import { existsSync as existsSync3 } from "fs";
328
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
329
+ import { join as join4 } from "path";
292
330
  import { pathToFileURL } from "url";
293
331
 
294
332
  // src/port.ts
@@ -317,6 +355,257 @@ async function findAvailablePort(startPort, options = {}) {
317
355
  throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
318
356
  }
319
357
 
358
+ // src/tunnel.ts
359
+ import { domainToASCII } from "url";
360
+ var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
361
+ var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
362
+ var DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR = "-";
363
+ var DEFAULT_GHOST_TUNNEL_MODE = "manual";
364
+ function isResolvedGhostTunnelConfig(value) {
365
+ return typeof value === "object" && value !== null && "enabled" in value;
366
+ }
367
+ function toGhostTunnelConfig(options) {
368
+ return isResolvedGhostTunnelConfig(options) ? options : resolveGhostTunnelConfig(options);
369
+ }
370
+ function stripHostPort(value) {
371
+ const trimmed = value.trim().toLowerCase();
372
+ if (trimmed.startsWith("[") || trimmed.includes("/")) return "";
373
+ const portSeparator = trimmed.lastIndexOf(":");
374
+ if (portSeparator === -1) return trimmed;
375
+ const port = trimmed.slice(portSeparator + 1);
376
+ return /^\d+$/.test(port) ? trimmed.slice(0, portSeparator) : trimmed;
377
+ }
378
+ function normalizeDomain(value) {
379
+ const host = stripHostPort(value.replace(/^\*\./, ""));
380
+ const ascii = domainToASCII(host);
381
+ if (!ascii || ascii.length > 253 || ascii.includes("..")) return null;
382
+ if (ascii.startsWith(".") || ascii.endsWith(".")) return null;
383
+ if (ascii.includes("*")) return null;
384
+ if (!ascii.split(".").every(isValidHostLabel)) return null;
385
+ return ascii;
386
+ }
387
+ function isValidHostLabel(value) {
388
+ return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
389
+ }
390
+ function isValidNamespaceTag(value) {
391
+ return /^[a-z][a-z0-9_]*$/i.test(value);
392
+ }
393
+ function isNamespaceTagList(options) {
394
+ return Array.isArray(options);
395
+ }
396
+ function assertValidSubdomain(value) {
397
+ if (!isValidHostLabel(value)) {
398
+ throw new Error(`Invalid ghost tunnel subdomain: ${value}`);
399
+ }
400
+ }
401
+ function normalizeDomains(domains) {
402
+ const values = typeof domains === "string" ? [domains] : [...domains ?? []];
403
+ const normalized = values.map((value) => value.trim()).filter(Boolean).map((value) => {
404
+ const domain = normalizeDomain(value);
405
+ if (!domain) throw new Error(`Invalid ghost tunnel domain: ${value}`);
406
+ return domain;
407
+ });
408
+ return [...new Set(normalized)];
409
+ }
410
+ function parseGhostTunnelMode(value) {
411
+ return value ?? DEFAULT_GHOST_TUNNEL_MODE;
412
+ }
413
+ function resolveNamespaceConfig(options) {
414
+ const tags = isNamespaceTagList(options) ? [...options] : [...options?.tags ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS];
415
+ let separator = DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
416
+ let spreadTag = tags.includes("project") ? "project" : void 0;
417
+ if (options && !isNamespaceTagList(options)) {
418
+ separator = options.separator ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
419
+ spreadTag = options.spreadTag === false ? false : options.spreadTag ?? spreadTag;
420
+ }
421
+ if (tags.length === 0) {
422
+ throw new Error("Ghost tunnel namespace must include at least one tag.");
423
+ }
424
+ for (const tag of tags) {
425
+ if (!isValidNamespaceTag(tag)) {
426
+ throw new Error(`Invalid ghost tunnel namespace tag: ${tag}`);
427
+ }
428
+ }
429
+ if (spreadTag && !tags.includes(spreadTag)) {
430
+ throw new Error(`Ghost tunnel namespace spreadTag must be listed in tags: ${spreadTag}`);
431
+ }
432
+ if (!separator || separator.length > 8 || !/^[a-z0-9-]+$/.test(separator)) {
433
+ throw new Error(`Invalid ghost tunnel namespace separator: ${separator}`);
434
+ }
435
+ return {
436
+ tags,
437
+ separator,
438
+ ...spreadTag ? { spreadTag } : {}
439
+ };
440
+ }
441
+ function normalizeNamespaceValue(tag, value, separator, options = {}) {
442
+ const normalized = normalizeDomain(value);
443
+ if (!normalized || normalized.includes(".")) {
444
+ throw new Error(`Invalid ghost tunnel namespace value for ${tag}: ${value}`);
445
+ }
446
+ if (!options.allowSeparator && normalized.includes(separator)) {
447
+ throw new Error(`Ghost tunnel namespace value for ${tag} cannot include separator "${separator}": ${value}`);
448
+ }
449
+ return normalized;
450
+ }
451
+ function createNamespaceSlug(config, values) {
452
+ const parts = config.tags.map((tag) => {
453
+ const value = values[tag];
454
+ if (!value) {
455
+ throw new Error(`Missing ghost tunnel namespace value: ${tag}`);
456
+ }
457
+ return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
458
+ });
459
+ const slug = parts.join(config.separator);
460
+ if (!isValidHostLabel(slug)) {
461
+ throw new Error(`Ghost tunnel namespace is too long for a DNS label: ${slug}`);
462
+ }
463
+ return slug;
464
+ }
465
+ function createNamespaceDisplaySlug(config, values = {}) {
466
+ return config.tags.map((tag) => {
467
+ const value = values[tag];
468
+ if (!value) return `<${tag}>`;
469
+ try {
470
+ return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
471
+ } catch {
472
+ return `<${tag}>`;
473
+ }
474
+ }).join(config.separator);
475
+ }
476
+ function getPreviewDefaults(preview, defaults) {
477
+ return {
478
+ domain: preview?.domain ?? defaults?.domain,
479
+ route: preview?.route ?? defaults?.route,
480
+ project: preview?.project ?? defaults?.project,
481
+ owner: preview?.owner ?? defaults?.owner,
482
+ values: {
483
+ ...defaults?.values ?? {},
484
+ ...preview?.values ?? {}
485
+ },
486
+ path: preview?.path,
487
+ protocol: preview?.protocol
488
+ };
489
+ }
490
+ function getDisplayValues(input2) {
491
+ return {
492
+ ...input2.route ? { route: input2.route } : {},
493
+ ...input2.project ? { project: input2.project } : {},
494
+ ...input2.owner ? { owner: input2.owner } : {},
495
+ ...input2.values
496
+ };
497
+ }
498
+ function createDisplayUrl(config, defaults, domain) {
499
+ const input2 = getPreviewDefaults(config.preview, defaults);
500
+ const protocol = input2.protocol ?? "https";
501
+ const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
502
+ const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
503
+ const url = `${protocol}://${slug}.${entryHost}/`;
504
+ if (!input2.path) return url;
505
+ return `${url}${input2.path.replace(/^\/+/, "")}`;
506
+ }
507
+ function createDisplayUrls(config, defaults) {
508
+ const domains = config.domains.length > 0 ? config.domains : defaults?.domain ? [defaults.domain] : [];
509
+ const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, defaults, domain)) : [createDisplayUrl(config, defaults)];
510
+ return [...new Set(urls)];
511
+ }
512
+ function maybeConstructPreviewUrl(config, defaults) {
513
+ if (!config.preview) return void 0;
514
+ const input2 = getPreviewDefaults(config.preview, defaults);
515
+ if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
516
+ return constructGhostTunnelUrl({
517
+ domain: input2.domain,
518
+ route: input2.route,
519
+ project: input2.project,
520
+ owner: input2.owner,
521
+ values: input2.values,
522
+ ...input2.path ? { path: input2.path } : {},
523
+ ...input2.protocol ? { protocol: input2.protocol } : {},
524
+ ghostTunnel: config
525
+ });
526
+ }
527
+ function resolveGhostTunnelConfig(options, defaults) {
528
+ if (options === false || typeof options === "undefined") {
529
+ return {
530
+ enabled: false,
531
+ mode: DEFAULT_GHOST_TUNNEL_MODE,
532
+ domains: [],
533
+ subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
534
+ namespace: resolveNamespaceConfig(void 0),
535
+ displayUrls: [],
536
+ requireHttps: true,
537
+ requireAuth: true
538
+ };
539
+ }
540
+ const config = typeof options === "string" ? { mode: options } : options;
541
+ const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
542
+ assertValidSubdomain(subdomain);
543
+ const domains = normalizeDomains(config.domains);
544
+ const enabled = config.enabled ?? true;
545
+ const resolved = {
546
+ enabled,
547
+ mode: parseGhostTunnelMode(config.mode),
548
+ domains,
549
+ subdomain,
550
+ namespace: resolveNamespaceConfig(config.namespace),
551
+ ...config.preview ? { preview: config.preview } : {},
552
+ displayUrls: [],
553
+ requireHttps: config.requireHttps ?? true,
554
+ requireAuth: config.requireAuth ?? true
555
+ };
556
+ if (!enabled) {
557
+ return resolved;
558
+ }
559
+ const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
560
+ const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
561
+ return {
562
+ ...resolved,
563
+ displayUrls,
564
+ ...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
565
+ ...previewUrl ? { previewUrl } : {}
566
+ };
567
+ }
568
+ function getGhostTunnelEntryHost(domain, options = {}) {
569
+ const config = toGhostTunnelConfig(options);
570
+ const normalizedDomain = normalizeDomain(domain);
571
+ if (!normalizedDomain) {
572
+ throw new Error(`Invalid ghost tunnel domain: ${domain}`);
573
+ }
574
+ return `${config.subdomain}.${normalizedDomain}`;
575
+ }
576
+ function constructGhostTunnelHost(input2) {
577
+ const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
578
+ if (!config.enabled) {
579
+ throw new Error("Ghost tunnel is not enabled.");
580
+ }
581
+ const namespaceValues = {
582
+ route: input2.route,
583
+ project: input2.project,
584
+ owner: input2.owner,
585
+ ...input2.values ?? {}
586
+ };
587
+ const slug = createNamespaceSlug(config.namespace, namespaceValues);
588
+ return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
589
+ }
590
+ function constructGhostTunnelUrl(input2) {
591
+ const protocol = input2.protocol ?? "https";
592
+ const host = constructGhostTunnelHost(input2);
593
+ const url = new URL(`${protocol}://${host}/`);
594
+ if (input2.path) {
595
+ url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
596
+ }
597
+ if (input2.searchParams instanceof URLSearchParams) {
598
+ url.search = input2.searchParams.toString();
599
+ } else if (input2.searchParams) {
600
+ for (const [key, value] of Object.entries(input2.searchParams)) {
601
+ if (typeof value !== "undefined" && value !== null) {
602
+ url.searchParams.set(key, String(value));
603
+ }
604
+ }
605
+ }
606
+ return url.toString();
607
+ }
608
+
320
609
  // src/context.ts
321
610
  var LOCALGHOST_PROJECT_CONFIG_FILES = [
322
611
  "localghost.config.mjs",
@@ -341,6 +630,25 @@ function envHttps() {
341
630
  if (!value) return void 0;
342
631
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
343
632
  }
633
+ function getPackageName(cwd) {
634
+ try {
635
+ const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf8"));
636
+ return typeof pkg.name === "string" ? pkg.name : void 0;
637
+ } catch {
638
+ return void 0;
639
+ }
640
+ }
641
+ function getPackageOwner(cwd) {
642
+ const packageName = getPackageName(cwd);
643
+ if (!packageName?.startsWith("@")) return void 0;
644
+ return packageName.slice(1).split("/")[0];
645
+ }
646
+ function getLocalOwner(cwd) {
647
+ return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
648
+ }
649
+ function getRouteName(primaryHost, fallback) {
650
+ return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
651
+ }
344
652
  function readOptionsFromContext(options) {
345
653
  return {
346
654
  cwd: options.cwd ?? process.cwd(),
@@ -379,18 +687,22 @@ function addDefaultWwwAliases(entries) {
379
687
  function defined(input2) {
380
688
  return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
381
689
  }
382
- async function readProjectConfig(cwd, configFile) {
383
- if (configFile === false) return {};
384
- const candidates = configFile ? [configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
690
+ async function readLocalghostProjectConfig(options = {}) {
691
+ const cwd = options.cwd ?? process.cwd();
692
+ if (options.configFile === false) return { config: {} };
693
+ const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
385
694
  const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
386
- if (!path) return {};
695
+ if (!path) return { config: {} };
387
696
  const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
388
697
  const config = imported.default ?? imported;
389
698
  return { config, path };
390
699
  }
391
700
  async function resolveLocalghostContext(options = {}) {
392
701
  const cwd = options.cwd ?? process.cwd();
393
- const projectConfig = await readProjectConfig(cwd, options.localghostConfig);
702
+ const projectConfig = await readLocalghostProjectConfig({
703
+ cwd,
704
+ ...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
705
+ });
394
706
  const merged = {
395
707
  ...projectConfig.config,
396
708
  ...defined(options)
@@ -399,7 +711,7 @@ async function resolveLocalghostContext(options = {}) {
399
711
  const resolvedPath = resolveDevHostsPath(readOptions);
400
712
  const configEntries = readDevHosts(readOptions);
401
713
  const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
402
- const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? false;
714
+ const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
403
715
  const bindHost = merged.bindHost ?? "127.0.0.1";
404
716
  const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
405
717
  const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
@@ -407,9 +719,15 @@ async function resolveLocalghostContext(options = {}) {
407
719
  const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
408
720
  const hosts = uniqueHosts(entries);
409
721
  const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
722
+ const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
723
+ const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
724
+ route: getRouteName(primaryHost, projectName),
725
+ project: projectName,
726
+ owner: getLocalOwner(cwd)
727
+ });
410
728
  return {
411
729
  cwd,
412
- projectName: sanitizeProjectName(merged.project ?? getProjectName(cwd)),
730
+ projectName,
413
731
  readOptions,
414
732
  configPath: resolvedPath.path,
415
733
  configFileName: resolvedPath.fileName,
@@ -423,6 +741,7 @@ async function resolveLocalghostContext(options = {}) {
423
741
  primaryHost,
424
742
  https: merged.https ?? envHttps() ?? false,
425
743
  wwwAlias,
744
+ ghostTunnel,
426
745
  ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
427
746
  };
428
747
  }
@@ -473,7 +792,7 @@ function assertLocalDevelopment(command, env = process.env) {
473
792
  // src/hosts-file.ts
474
793
  import { writeFileSync as writeFileSync3 } from "fs";
475
794
  import { tmpdir } from "os";
476
- import { join as join4 } from "path";
795
+ import { join as join5 } from "path";
477
796
  import { execa as execa3 } from "execa";
478
797
  function escapeRegExp(value) {
479
798
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -515,7 +834,7 @@ function removeManagedBlock(existing, projectName) {
515
834
  }
516
835
  async function writeSystemHostsFile(hostsPath, next, projectName) {
517
836
  const sanitizedProjectName = sanitizeProjectName(projectName);
518
- const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
837
+ const tempPath = join5(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
519
838
  writeFileSync3(tempPath, next, "utf8");
520
839
  if (process.platform === "win32") {
521
840
  throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
@@ -548,11 +867,11 @@ async function removeSystemHosts(projectName) {
548
867
  }
549
868
 
550
869
  // src/init.ts
551
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
552
- import { join as join5 } from "path";
870
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
871
+ import { join as join6 } from "path";
553
872
  function detectPackageManager(cwd = process.cwd()) {
554
- if (existsSync4(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
555
- if (existsSync4(join5(cwd, "yarn.lock"))) return "yarn";
873
+ if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
874
+ if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
556
875
  return "npm";
557
876
  }
558
877
  function packageRunCommand(packageManager, script) {
@@ -572,7 +891,7 @@ function renderConfig(options) {
572
891
  }
573
892
  function readPackageJson(path) {
574
893
  try {
575
- return JSON.parse(readFileSync4(path, "utf8"));
894
+ return JSON.parse(readFileSync5(path, "utf8"));
576
895
  } catch {
577
896
  return null;
578
897
  }
@@ -624,7 +943,7 @@ function initLocalghost(options = {}) {
624
943
  const apiPort = options.apiPort ?? 8787;
625
944
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
626
945
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
627
- const configPath = join5(cwd, configFile);
946
+ const configPath = join6(cwd, configFile);
628
947
  const configExists = existsSync4(configPath);
629
948
  if (configExists && !options.force) {
630
949
  return {
@@ -641,7 +960,7 @@ function initLocalghost(options = {}) {
641
960
  };
642
961
  }
643
962
  writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
644
- const packageJsonPath = join5(cwd, "package.json");
963
+ const packageJsonPath = join6(cwd, "package.json");
645
964
  const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
646
965
  return {
647
966
  configPath,
@@ -682,6 +1001,20 @@ async function confirm(question, defaultValue = true) {
682
1001
  }
683
1002
 
684
1003
  // src/routes.ts
1004
+ var ansi = {
1005
+ cyan: "\x1B[36m",
1006
+ dim: "\x1B[2m",
1007
+ green: "\x1B[32m",
1008
+ reset: "\x1B[0m",
1009
+ yellow: "\x1B[33m"
1010
+ };
1011
+ function colorize(value, color, enabled) {
1012
+ return enabled ? `${color}${value}${ansi.reset}` : value;
1013
+ }
1014
+ function colorizeUrl(value, enabled) {
1015
+ if (!enabled) return value;
1016
+ return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
1017
+ }
685
1018
  function getDomainRoutes(entries, options = {}) {
686
1019
  const protocol = options.https === true ? "https" : "http";
687
1020
  return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
@@ -701,13 +1034,40 @@ function formatDomainRoutes(entries, options = {}) {
701
1034
  ...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
702
1035
  ].join("\n");
703
1036
  }
1037
+ function formatGhostTunnel(config, options = {}) {
1038
+ if (!config.enabled) return null;
1039
+ const color = options.color === true;
1040
+ const label = options.label ?? "expected";
1041
+ const labelColor = label === "running" ? ansi.green : ansi.dim;
1042
+ const lines = [
1043
+ "localghost ghost tunnel",
1044
+ ` mode: ${config.mode}`
1045
+ ];
1046
+ const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
1047
+ if (urls.length === 0) {
1048
+ lines.push(` ${label}: unavailable`);
1049
+ } else if (urls.length === 1) {
1050
+ lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
1051
+ } else {
1052
+ lines.push(` ${colorize(label, labelColor, color)}:`);
1053
+ for (const url of urls) {
1054
+ lines.push(` ${colorizeUrl(url, color)}`);
1055
+ }
1056
+ }
1057
+ if (options.verbose) {
1058
+ lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
1059
+ lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
1060
+ lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
1061
+ }
1062
+ return lines.join("\n");
1063
+ }
704
1064
 
705
1065
  // src/state.ts
706
1066
  import { existsSync as existsSync5 } from "fs";
707
- import { join as join6 } from "path";
1067
+ import { join as join7 } from "path";
708
1068
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
709
1069
  function getLocalghostStatePath(cwd = process.cwd()) {
710
- return join6(cwd, LOCALGHOST_STATE_FILE);
1070
+ return join7(cwd, LOCALGHOST_STATE_FILE);
711
1071
  }
712
1072
  function readLocalghostState(cwd = process.cwd()) {
713
1073
  const path = getLocalghostStatePath(cwd);
@@ -727,11 +1087,11 @@ function patchLocalghostState(cwd, patch) {
727
1087
  }
728
1088
 
729
1089
  // src/update-check.ts
730
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
1090
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
731
1091
  import { homedir as homedir2 } from "os";
732
- import { dirname as dirname4, join as join7 } from "path";
1092
+ import { dirname as dirname4, join as join8 } from "path";
733
1093
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
734
- var LOCALGHOST_VERSION = "0.1.8";
1094
+ var LOCALGHOST_VERSION = "0.1.9";
735
1095
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
736
1096
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
737
1097
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -743,13 +1103,13 @@ function isUpdateCheckDisabled(env = process.env) {
743
1103
  }
744
1104
  function getUpdateCheckCachePath(env = process.env) {
745
1105
  if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
746
- const cacheRoot = env.XDG_CACHE_HOME || join7(homedir2(), ".cache");
747
- return join7(cacheRoot, "localghost", "update-check.json");
1106
+ const cacheRoot = env.XDG_CACHE_HOME || join8(homedir2(), ".cache");
1107
+ return join8(cacheRoot, "localghost", "update-check.json");
748
1108
  }
749
1109
  function readCache(path = getUpdateCheckCachePath()) {
750
1110
  if (!existsSync6(path)) return null;
751
1111
  try {
752
- return JSON.parse(readFileSync5(path, "utf8"));
1112
+ return JSON.parse(readFileSync6(path, "utf8"));
753
1113
  } catch {
754
1114
  return null;
755
1115
  }
@@ -903,8 +1263,18 @@ function warnAboutLocalMdns(entries) {
903
1263
  );
904
1264
  }
905
1265
  }
1266
+ function shouldColor() {
1267
+ return process.stdout.isTTY && !process.env.NO_COLOR;
1268
+ }
906
1269
  function logDomainRoutes(entries, options = {}) {
907
1270
  console.log(formatDomainRoutes(entries, options));
1271
+ if (options.ghostTunnel?.enabled) {
1272
+ console.log(formatGhostTunnel(options.ghostTunnel, {
1273
+ color: shouldColor(),
1274
+ label: "expected",
1275
+ verbose: options.verbose === true
1276
+ }));
1277
+ }
908
1278
  }
909
1279
  function parsePort2(value) {
910
1280
  const port = Number.parseInt(value, 10);
@@ -998,7 +1368,7 @@ function getSetupReadiness(options) {
998
1368
  }
999
1369
  const hostsPath = getSystemHostsPath();
1000
1370
  try {
1001
- const hosts = readFileSync6(hostsPath, "utf8");
1371
+ const hosts = readFileSync7(hostsPath, "utf8");
1002
1372
  const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
1003
1373
  if (!hosts.includes(expectedHostsBlock)) {
1004
1374
  reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
@@ -1012,7 +1382,7 @@ function getSetupReadiness(options) {
1012
1382
  reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
1013
1383
  } else {
1014
1384
  const expectedCaddyfile = renderCaddyfile(entries, { https });
1015
- const currentCaddyfile = readFileSync6(caddyfilePath, "utf8");
1385
+ const currentCaddyfile = readFileSync7(caddyfilePath, "utf8");
1016
1386
  if (currentCaddyfile !== expectedCaddyfile) {
1017
1387
  reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
1018
1388
  }
@@ -1047,6 +1417,14 @@ async function runSetupFromReadiness(cwd, https, readiness) {
1047
1417
  ...existingTrustMarkers(cwd),
1048
1418
  entries: readiness.entries
1049
1419
  });
1420
+ registerLocalghostSetup({
1421
+ cwd,
1422
+ projectName: readiness.projectName,
1423
+ configPath: readiness.configPath,
1424
+ caddyfilePath,
1425
+ https,
1426
+ entries: readiness.entries
1427
+ });
1050
1428
  }
1051
1429
  function wait(ms) {
1052
1430
  return new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -1096,35 +1474,91 @@ function registerCleanup(id) {
1096
1474
  process.off("exit", cleanup);
1097
1475
  };
1098
1476
  }
1099
- async function getRunView(run) {
1477
+ async function getRouteViews(entries) {
1100
1478
  const portStatus = /* @__PURE__ */ new Map();
1101
- for (const entry of run.entries) {
1479
+ for (const entry of entries) {
1102
1480
  if (!portStatus.has(entry.port)) {
1103
1481
  portStatus.set(entry.port, !await isPortAvailable(entry.port));
1104
1482
  }
1105
1483
  }
1484
+ return entries.map((entry) => ({
1485
+ host: entry.host,
1486
+ port: entry.port,
1487
+ target: `127.0.0.1:${entry.port}`,
1488
+ listening: portStatus.get(entry.port) ?? false
1489
+ }));
1490
+ }
1491
+ function setupKey(input2) {
1492
+ return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
1493
+ }
1494
+ function runKey(input2) {
1495
+ return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
1496
+ }
1497
+ async function getInstanceViews(setups, runs) {
1498
+ const runBySetup = new Map(runs.map((run) => [runKey(run), run]));
1499
+ const instances = [];
1500
+ for (const setup of setups) {
1501
+ const run = runBySetup.get(setupKey(setup));
1502
+ if (run) {
1503
+ instances.push(await getRunInstanceView(run, setup));
1504
+ runBySetup.delete(setupKey(setup));
1505
+ continue;
1506
+ }
1507
+ instances.push({
1508
+ id: setup.id,
1509
+ cwd: setup.cwd,
1510
+ projectName: setup.projectName,
1511
+ running: false,
1512
+ mode: "setup",
1513
+ updatedAt: setup.updatedAt,
1514
+ ...setup.configPath ? { configPath: setup.configPath } : {},
1515
+ ...setup.caddyfilePath ? { caddyfilePath: setup.caddyfilePath } : {},
1516
+ ...typeof setup.https === "boolean" ? { https: setup.https } : {},
1517
+ routes: await getRouteViews(setup.entries)
1518
+ });
1519
+ }
1520
+ for (const run of runBySetup.values()) {
1521
+ instances.push(await getRunInstanceView(run));
1522
+ }
1523
+ return instances.sort((left, right) => {
1524
+ if (left.running !== right.running) return left.running ? -1 : 1;
1525
+ return left.projectName.localeCompare(right.projectName);
1526
+ });
1527
+ }
1528
+ async function getRunInstanceView(run, setup) {
1106
1529
  return {
1107
- ...run,
1108
- routes: run.entries.map((entry) => ({
1109
- host: entry.host,
1110
- port: entry.port,
1111
- target: `127.0.0.1:${entry.port}`,
1112
- listening: portStatus.get(entry.port) ?? false
1113
- }))
1530
+ id: setup?.id ?? run.id,
1531
+ cwd: run.cwd,
1532
+ projectName: run.projectName,
1533
+ running: true,
1534
+ mode: run.mode,
1535
+ updatedAt: setup?.updatedAt ?? run.updatedAt,
1536
+ startedAt: run.startedAt,
1537
+ pid: run.pid,
1538
+ ...run.caddyPid ? { caddyPid: run.caddyPid } : {},
1539
+ ...run.childPid ? { childPid: run.childPid } : {},
1540
+ ...run.childCommand ? { childCommand: run.childCommand } : {},
1541
+ ...run.configPath ? { configPath: run.configPath } : {},
1542
+ ...run.caddyfilePath ? { caddyfilePath: run.caddyfilePath } : {},
1543
+ ...typeof run.https === "boolean" ? { https: run.https } : {},
1544
+ routes: await getRouteViews(run.entries)
1114
1545
  };
1115
1546
  }
1116
- function formatRunViews(runs) {
1117
- if (runs.length === 0) return "No Localghost apps are running.";
1547
+ function formatInstanceViews(instances) {
1548
+ if (instances.length === 0) return "No Localghost setups found.";
1118
1549
  const lines = ["localghost ps"];
1119
- for (const run of runs) {
1120
- const command = run.childCommand?.length ? ` ${run.childCommand.join(" ")}` : "";
1121
- const mode = command ? `${run.mode}:${command}` : run.mode;
1550
+ for (const instance of instances) {
1551
+ const command = instance.childCommand?.length ? ` ${instance.childCommand.join(" ")}` : "";
1552
+ const mode = command ? `${instance.mode}:${command}` : instance.mode === "setup" ? "" : instance.mode;
1122
1553
  lines.push("");
1123
- lines.push(`${run.projectName} ${mode}`);
1124
- lines.push(` cwd: ${run.cwd}`);
1125
- lines.push(` pid: ${run.pid}${run.caddyPid ? `, caddy: ${run.caddyPid}` : ""}${run.childPid ? `, child: ${run.childPid}` : ""}`);
1126
- lines.push(` started: ${run.startedAt}`);
1127
- for (const route of run.routes) {
1554
+ lines.push(`${instance.projectName} ${instance.running ? "running" : "setup"}${mode ? ` ${mode}` : ""}`);
1555
+ lines.push(` cwd: ${instance.cwd}`);
1556
+ if (instance.pid) {
1557
+ lines.push(` pid: ${instance.pid}${instance.caddyPid ? `, caddy: ${instance.caddyPid}` : ""}${instance.childPid ? `, child: ${instance.childPid}` : ""}`);
1558
+ }
1559
+ if (instance.startedAt) lines.push(` started: ${instance.startedAt}`);
1560
+ if (!instance.startedAt && instance.updatedAt) lines.push(` setup: ${instance.updatedAt}`);
1561
+ for (const route of instance.routes) {
1128
1562
  lines.push(` ${route.host} -> ${route.target} (${route.listening ? "listening" : "not listening"})`);
1129
1563
  }
1130
1564
  }
@@ -1198,7 +1632,7 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
1198
1632
  const configPath = context.configPath;
1199
1633
  const entries = context.entries;
1200
1634
  warnAboutLocalMdns(entries);
1201
- logDomainRoutes(entries, { https });
1635
+ logDomainRoutes(entries, { https, ghostTunnel: context.ghostTunnel });
1202
1636
  explainHostsPassword();
1203
1637
  const hostsResult = await updateSystemHosts(projectName, entries);
1204
1638
  if (hostsResult.changed) {
@@ -1221,6 +1655,14 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
1221
1655
  ...existingTrustMarkers(options.cwd),
1222
1656
  entries
1223
1657
  });
1658
+ registerLocalghostSetup({
1659
+ cwd: options.cwd,
1660
+ projectName,
1661
+ configPath,
1662
+ caddyfilePath: caddyfile,
1663
+ https,
1664
+ entries
1665
+ });
1224
1666
  console.log(`Generated ${caddyfile}`);
1225
1667
  console.log(`Mode ${https ? "HTTPS" : "HTTP"}`);
1226
1668
  console.log(`State ${statePath}`);
@@ -1234,7 +1676,7 @@ program.command("trust").description("Trust Caddy's local HTTPS CA for this proj
1234
1676
  throw new Error("Localghost HTTPS is not enabled for this context. Set https: true in localghost.config.mjs or pass --https.");
1235
1677
  }
1236
1678
  warnAboutLocalMdns(context.entries);
1237
- logDomainRoutes(context.entries, { https: true });
1679
+ logDomainRoutes(context.entries, { https: true, ghostTunnel: context.ghostTunnel });
1238
1680
  explainTrustPassword();
1239
1681
  const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https: true });
1240
1682
  await validateCaddyfile(caddyfile);
@@ -1264,6 +1706,7 @@ program.command("reset").description("Remove Localghost setup state without dele
1264
1706
  } else {
1265
1707
  console.log(`No Localghost hosts block found in ${hostsResult.hostsPath}`);
1266
1708
  }
1709
+ unregisterLocalghostSetup({ cwd: options.cwd, projectName });
1267
1710
  console.log(".localghost was left in place. Run localghost setup when you are ready.");
1268
1711
  });
1269
1712
  program.command("teardown").description("Remove Localghost's managed /etc/hosts block").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--remove-caddyfile", "Also remove ops/local/Caddyfile").action(async (options) => {
@@ -1295,6 +1738,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
1295
1738
  if (options.removeCaddyfile) {
1296
1739
  console.log(caddyfileRemoved ? `Removed ${caddyfilePath}` : `${caddyfilePath} was not present`);
1297
1740
  }
1741
+ unregisterLocalghostSetup({ cwd: options.cwd, projectName });
1298
1742
  console.log(`State ${statePath}`);
1299
1743
  });
1300
1744
  program.command("status").description("Print Localghost's project-local state file").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("--ready", "Exit non-zero when setup is missing or stale").option("--https", "Check setup readiness for HTTPS mode").option("--ssl", "Alias for --https").option("--json", "Print raw JSON").action(async (options) => {
@@ -1340,10 +1784,17 @@ program.command("status").description("Print Localghost's project-local state fi
1340
1784
  process.exitCode = 1;
1341
1785
  }
1342
1786
  });
1343
- program.command("routes").description("Print domain to upstream routes").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("--http", "Print domain URLs with http instead of https").option("--https", "Print domain URLs with https").option("--ssl", "Alias for --https").action(async (options) => {
1787
+ program.command("routes").description("Print domain to upstream routes").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("--http", "Print domain URLs with http instead of https").option("--https", "Print domain URLs with https").option("--ssl", "Alias for --https").option("--verbose", "Print Ghost Tunnel mode, domains, and guardrails").action(async (options) => {
1344
1788
  const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
1345
1789
  warnAboutLocalMdns(context.entries);
1346
1790
  console.log(formatDomainRoutes(context.entries, { https: options.http ? false : context.https }));
1791
+ if (context.ghostTunnel.enabled) {
1792
+ console.log(formatGhostTunnel(context.ghostTunnel, {
1793
+ color: shouldColor(),
1794
+ label: "expected",
1795
+ verbose: options.verbose === true
1796
+ }));
1797
+ }
1347
1798
  });
1348
1799
  program.command("dev").description("Run the Localghost Caddy proxy after setup").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", "Run setup before starting the proxy when setup is missing or stale").option("--trust", "Trust Caddy's local HTTPS CA before starting the proxy").action(async (options) => {
1349
1800
  assertLocalDevelopment("dev");
@@ -1385,9 +1836,17 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
1385
1836
  ...existingTrustMarkers(options.cwd),
1386
1837
  entries: readiness.entries
1387
1838
  });
1839
+ registerLocalghostSetup({
1840
+ cwd: options.cwd,
1841
+ projectName: readiness.projectName,
1842
+ configPath: readiness.configPath,
1843
+ caddyfilePath,
1844
+ https,
1845
+ entries: readiness.entries
1846
+ });
1388
1847
  }
1389
1848
  warnAboutLocalMdns(readiness.entries);
1390
- logDomainRoutes(readiness.entries, { https });
1849
+ logDomainRoutes(readiness.entries, { https, ghostTunnel: context.ghostTunnel });
1391
1850
  const caddyfile = await writeCaddyfile(readiness.entries, options.cwd, { https });
1392
1851
  await validateCaddyfile(caddyfile);
1393
1852
  const caddy = startCaddy(caddyfile);
@@ -1420,7 +1879,7 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
1420
1879
  cleanupRun();
1421
1880
  }
1422
1881
  });
1423
- 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", "Run setup before starting when setup is missing or stale").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, false).argument("<command...>", "Command to run after --, for example: localghost run -- vite").action(async (command, options) => {
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", "Run setup before starting when setup is missing or stale").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) => {
1424
1883
  assertLocalDevelopment("run");
1425
1884
  await assertCaddyReady();
1426
1885
  const context = await resolveLocalghostContext({
@@ -1459,7 +1918,7 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
1459
1918
  console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
1460
1919
  }
1461
1920
  warnAboutLocalMdns(context.entries);
1462
- logDomainRoutes(context.entries, { https });
1921
+ logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
1463
1922
  const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
1464
1923
  await validateCaddyfile(caddyfile);
1465
1924
  const caddy = startCaddy(caddyfile);
@@ -1524,13 +1983,15 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
1524
1983
  cleanupRun();
1525
1984
  }
1526
1985
  });
1527
- program.command("ps").description("Show Localghost dev sessions that are currently running").option("--json", "Print raw JSON").action(async (options) => {
1528
- const runs = await Promise.all(listLocalghostRuns().map((run) => getRunView(run)));
1986
+ program.command("ps").description("Show Localghost setups and currently running sessions").option("--json", "Print raw JSON").action(async (options) => {
1987
+ const setups = listLocalghostSetups();
1988
+ const runs = listLocalghostRuns();
1989
+ const instances = await getInstanceViews(setups, runs);
1529
1990
  if (options.json) {
1530
- console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), runs }, null, 2));
1991
+ console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), setups, runs, instances }, null, 2));
1531
1992
  return;
1532
1993
  }
1533
- console.log(formatRunViews(runs));
1994
+ console.log(formatInstanceViews(instances));
1534
1995
  });
1535
1996
  program.command("print").description("Print parsed host config").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").action((options) => {
1536
1997
  const entries = readDevHosts(readOptionsFromCli(options));